This commit is contained in:
parent
53187bede7
commit
4f1e974623
@ -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****";
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
24
src/email.ts
24
src/email.ts
@ -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);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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()),
|
||||||
)();
|
)();
|
||||||
|
Loading…
Reference in New Issue
Block a user