logout on end
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Elizabeth Hunt 2024-12-15 01:57:28 -08:00
parent 53187bede7
commit 4f1e974623
Signed by: simponic
GPG Key ID: 2909B9A7FF6213EE
4 changed files with 50 additions and 11 deletions

View File

@ -1,5 +1,5 @@
import { transformDurations } from "./duration";
import { perform } from "./email"; import { perform } from "./email";
import type { EmailJob } from "./job";
import { ConsoleLogger } from "./logger"; import { ConsoleLogger } from "./logger";
export const main = (port: number) => { export const main = (port: number) => {
@ -10,7 +10,12 @@ export const main = (port: number) => {
const url = new URL(req.url); const url = new URL(req.url);
if (req.method === "POST" && url.pathname === "/api/email") { if (req.method === "POST" && url.pathname === "/api/email") {
const job: EmailJob = await req.json(); const prevalidatedJob = transformDurations(await req.json());
if (prevalidatedJob._tag === "Left") {
return new Response(prevalidatedJob.left, { status: 400 });
}
const job = prevalidatedJob.right;
const jobInsensitive = structuredClone(job); const jobInsensitive = structuredClone(job);
jobInsensitive.from.username = "****REDACTED****"; jobInsensitive.from.username = "****REDACTED****";
jobInsensitive.from.password = "****REDACTED****"; jobInsensitive.from.password = "****REDACTED****";

View File

@ -12,6 +12,7 @@ export enum DurationUnit {
MINUTE, MINUTE,
HOUR, HOUR,
} }
const durationUnitMap: Record<string, DurationUnit> = { const durationUnitMap: Record<string, DurationUnit> = {
ms: DurationUnit.MILLISECOND, ms: DurationUnit.MILLISECOND,
milliseconds: DurationUnit.MILLISECOND, milliseconds: DurationUnit.MILLISECOND,
@ -153,3 +154,26 @@ export const parse = (duration: string): E.Either<string, Duration> => {
E.map(build), E.map(build),
); );
}; };
export const transformDurations = (obj: any): E.Either<string, any> => {
const transform = (o: any): E.Either<string, any> => {
const entries = Object.entries(o);
for (let [key, value] of entries) {
if (key === "duration" && typeof value === "string") {
return parse(value);
} else if (typeof value === "object" && value !== null) {
const result = transform(value);
if (E.isLeft(result)) {
return result;
} else {
o[key] = result.right;
}
}
}
return E.right(o);
};
return transform(obj);
}

View File

@ -14,7 +14,7 @@ interface ImapClientI {
connect: () => Promise<void>; connect: () => Promise<void>;
getMailboxLock: (mailbox: string) => Promise<MailboxLockObject>; getMailboxLock: (mailbox: string) => Promise<MailboxLockObject>;
messageDelete: (uids: number[], opts: Record<string, any>) => Promise<boolean>; messageDelete: (uids: number[], opts: Record<string, any>) => Promise<boolean>;
close: () => void; logout: () => Promise<void>;
} }
type Email = { type Email = {
@ -26,13 +26,16 @@ type Email = {
class ErrorWithLock extends Error { class ErrorWithLock extends Error {
lock: O.Option<MailboxLockObject>; lock: O.Option<MailboxLockObject>;
constructor(message: string, lock?: MailboxLockObject) { imap: O.Option<ImapClientI>;
constructor(message: string, lock?: MailboxLockObject, imap?: ImapClientI) {
super(message); super(message);
this.lock = O.fromNullable(lock); this.lock = O.fromNullable(lock);
this.imap = O.fromNullable(imap);
} }
} }
const ToErrorWithLock = (lock?: MailboxLockObject) => (error: unknown) =>
new ErrorWithLock(error instanceof Error ? error.message : "Unknown error", lock); const ToErrorWithLock = (lock?: MailboxLockObject, imap?: ImapClientI) => (error: unknown) =>
new ErrorWithLock(error instanceof Error ? error.message : "Unknown error", lock, imap);
/** /**
* Generate a unique email. * Generate a unique email.
@ -203,19 +206,19 @@ export const perform = (
// act. // act.
TE.tap(({ email }) => pipe(getSendImpl(from)(email), TE.mapLeft(ToErrorWithLock()))), TE.tap(({ email }) => pipe(getSendImpl(from)(email), TE.mapLeft(ToErrorWithLock()))),
TE.bind("imap", () => pipe(getImapImpl(to), TE.mapLeft(ToErrorWithLock()))), TE.bind("imap", () => pipe(getImapImpl(to), TE.mapLeft(ToErrorWithLock()))),
TE.bind("mailboxLock", ({ imap }) => TE.tryCatch(() => imap.getMailboxLock("INBOX"), ToErrorWithLock())), TE.bind("mailboxLock", ({ imap }) => TE.tryCatch(() => imap.getMailboxLock("INBOX"), ToErrorWithLock(undefined, imap))),
// "assert". // "assert".
TE.bind("uid", ({ imap, email, mailboxLock }) => TE.bind("uid", ({ imap, email, mailboxLock }) =>
pipe( pipe(
findEmailUidInInboxImpl(imap, matchesEmailImpl(email), retries, interval), findEmailUidInInboxImpl(imap, matchesEmailImpl(email), retries, interval),
TE.mapLeft(ToErrorWithLock(mailboxLock)) TE.mapLeft(ToErrorWithLock(mailboxLock, imap))
) )
), ),
// cleanup. // cleanup.
TE.bind("deleted", ({ imap, uid, mailboxLock }) => TE.bind("deleted", ({ imap, uid, mailboxLock }) =>
TE.tryCatch( TE.tryCatch(
() => imap.messageDelete([uid], { uid: true }), () => imap.messageDelete([uid], { uid: true }),
ToErrorWithLock(mailboxLock), ToErrorWithLock(mailboxLock, imap),
), ),
), ),
TE.fold( TE.fold(
@ -223,10 +226,15 @@ export const perform = (
if (O.isSome(e.lock)) { if (O.isSome(e.lock)) {
e.lock.value.release(); e.lock.value.release();
} }
if (O.isSome(e.imap)) {
const imap = e.imap.value;
return pipe(TE.tryCatch(() => imap.logout(), toError), TE.flatMap(() => TE.left(e)));
}
return TE.left(e); return TE.left(e);
}, },
({ mailboxLock, deleted }) => { ({ mailboxLock, deleted, imap }) => {
mailboxLock.release(); mailboxLock.release();
imap.logout();
return TE.right(deleted); return TE.right(deleted);
} }
) )

View File

@ -31,7 +31,7 @@ const getMocks = () => {
connect: mock(() => Promise.resolve()), connect: mock(() => Promise.resolve()),
getMailboxLock: mock(() => Promise.resolve(lock)), getMailboxLock: mock(() => Promise.resolve(lock)),
messageDelete: mock(() => Promise.resolve(true)), messageDelete: mock(() => Promise.resolve(true)),
close: mock(() => constVoid()), logout: mock(() => Promise.resolve()),
}; };
const mockDependencies: Partial<EmailJobDependencies> = { const mockDependencies: Partial<EmailJobDependencies> = {
@ -116,6 +116,7 @@ test("releases lock on left", async () => {
TE.mapLeft(() => { TE.mapLeft(() => {
expect(imap.getMailboxLock).toHaveBeenCalledTimes(1); expect(imap.getMailboxLock).toHaveBeenCalledTimes(1);
expect(lock.release).toHaveBeenCalledTimes(1); expect(lock.release).toHaveBeenCalledTimes(1);
expect(imap.logout).toHaveBeenCalledTimes(1);
}), }),
)(); )();
}); });
@ -131,6 +132,7 @@ test("releases lock on right", async () => {
TE.map(() => { TE.map(() => {
expect(imap.getMailboxLock).toHaveBeenCalledTimes(1); expect(imap.getMailboxLock).toHaveBeenCalledTimes(1);
expect(lock.release).toHaveBeenCalledTimes(1); expect(lock.release).toHaveBeenCalledTimes(1);
expect(imap.logout).toHaveBeenCalledTimes(1);
}), }),
TE.mapLeft(() => expect(false).toBeTruthy()), TE.mapLeft(() => expect(false).toBeTruthy()),
)(); )();