diff --git a/src/api.ts b/src/api.ts index cfb446a..722dc70 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,5 +1,5 @@ +import { transformDurations } from "./duration"; import { perform } from "./email"; -import type { EmailJob } from "./job"; import { ConsoleLogger } from "./logger"; export const main = (port: number) => { @@ -10,7 +10,12 @@ export const main = (port: number) => { const url = new URL(req.url); 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); jobInsensitive.from.username = "****REDACTED****"; jobInsensitive.from.password = "****REDACTED****"; diff --git a/src/duration.ts b/src/duration.ts index 3d1a44c..e8dc7d1 100644 --- a/src/duration.ts +++ b/src/duration.ts @@ -12,6 +12,7 @@ export enum DurationUnit { MINUTE, HOUR, } + const durationUnitMap: Record = { ms: DurationUnit.MILLISECOND, milliseconds: DurationUnit.MILLISECOND, @@ -153,3 +154,26 @@ export const parse = (duration: string): E.Either => { E.map(build), ); }; + +export const transformDurations = (obj: any): E.Either => { + const transform = (o: any): E.Either => { + 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); +} diff --git a/src/email.ts b/src/email.ts index 6ad05a0..906b86d 100644 --- a/src/email.ts +++ b/src/email.ts @@ -14,7 +14,7 @@ interface ImapClientI { connect: () => Promise; getMailboxLock: (mailbox: string) => Promise; messageDelete: (uids: number[], opts: Record) => Promise; - close: () => void; + logout: () => Promise; } type Email = { @@ -26,13 +26,16 @@ type Email = { class ErrorWithLock extends Error { lock: O.Option; - constructor(message: string, lock?: MailboxLockObject) { + imap: O.Option; + constructor(message: string, lock?: MailboxLockObject, imap?: ImapClientI) { super(message); 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. @@ -203,19 +206,19 @@ export const perform = ( // act. TE.tap(({ email }) => pipe(getSendImpl(from)(email), 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". TE.bind("uid", ({ imap, email, mailboxLock }) => pipe( findEmailUidInInboxImpl(imap, matchesEmailImpl(email), retries, interval), - TE.mapLeft(ToErrorWithLock(mailboxLock)) + TE.mapLeft(ToErrorWithLock(mailboxLock, imap)) ) ), // cleanup. TE.bind("deleted", ({ imap, uid, mailboxLock }) => TE.tryCatch( () => imap.messageDelete([uid], { uid: true }), - ToErrorWithLock(mailboxLock), + ToErrorWithLock(mailboxLock, imap), ), ), TE.fold( @@ -223,10 +226,15 @@ export const perform = ( if (O.isSome(e.lock)) { 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); }, - ({ mailboxLock, deleted }) => { + ({ mailboxLock, deleted, imap }) => { mailboxLock.release(); + imap.logout(); return TE.right(deleted); } ) diff --git a/tst/email.spec.ts b/tst/email.spec.ts index 7315816..e966030 100644 --- a/tst/email.spec.ts +++ b/tst/email.spec.ts @@ -31,7 +31,7 @@ const getMocks = () => { connect: mock(() => Promise.resolve()), getMailboxLock: mock(() => Promise.resolve(lock)), messageDelete: mock(() => Promise.resolve(true)), - close: mock(() => constVoid()), + logout: mock(() => Promise.resolve()), }; const mockDependencies: Partial = { @@ -116,6 +116,7 @@ test("releases lock on left", async () => { TE.mapLeft(() => { expect(imap.getMailboxLock).toHaveBeenCalledTimes(1); expect(lock.release).toHaveBeenCalledTimes(1); + expect(imap.logout).toHaveBeenCalledTimes(1); }), )(); }); @@ -131,6 +132,7 @@ test("releases lock on right", async () => { TE.map(() => { expect(imap.getMailboxLock).toHaveBeenCalledTimes(1); expect(lock.release).toHaveBeenCalledTimes(1); + expect(imap.logout).toHaveBeenCalledTimes(1); }), TE.mapLeft(() => expect(false).toBeTruthy()), )();