summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Hunt <elizabeth.hunt@simponic.xyz>2024-12-15 02:07:48 -0800
committerElizabeth Hunt <elizabeth.hunt@simponic.xyz>2024-12-15 02:26:06 -0800
commitc4385abb3354ca5d4d647f07d2b02bf178dc52f7 (patch)
tree122b8ef06a25355ce184c4ab991387fbafb66a00
parent4f1e974623f7e38693d3e202cd387c51f652b9d8 (diff)
downloaduptime-c4385abb3354ca5d4d647f07d2b02bf178dc52f7.tar.gz
uptime-c4385abb3354ca5d4d647f07d2b02bf178dc52f7.zip
prettier
-rw-r--r--src/api.ts18
-rw-r--r--src/duration.ts23
-rw-r--r--src/email.ts147
3 files changed, 116 insertions, 72 deletions
diff --git a/src/api.ts b/src/api.ts
index 722dc70..678f40d 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -1,5 +1,6 @@
-import { transformDurations } from "./duration";
+import { parse } from "./duration";
import { perform } from "./email";
+import type { EmailJob } from "./job";
import { ConsoleLogger } from "./logger";
export const main = (port: number) => {
@@ -10,12 +11,17 @@ export const main = (port: number) => {
const url = new URL(req.url);
if (req.method === "POST" && url.pathname === "/api/email") {
- const prevalidatedJob = transformDurations(await req.json());
- if (prevalidatedJob._tag === "Left") {
- return new Response(prevalidatedJob.left, { status: 400 });
+ const prevalidatedJob = await req.json();
+ const interval = parse(prevalidatedJob.readRetry.interval);
+ if (interval._tag === "Left") {
+ return new Response(interval.left, { status: 400 });
}
- const job = prevalidatedJob.right;
-
+ prevalidatedJob.readRetry.interval = interval;
+ const job: EmailJob = {
+ ...prevalidatedJob,
+ readRetry: { ...prevalidatedJob.readRetry, interval },
+ };
+
const jobInsensitive = structuredClone(job);
jobInsensitive.from.username = "****REDACTED****";
jobInsensitive.from.password = "****REDACTED****";
diff --git a/src/duration.ts b/src/duration.ts
index e8dc7d1..ad19921 100644
--- a/src/duration.ts
+++ b/src/duration.ts
@@ -154,26 +154,3 @@ export const parse = (duration: string): E.Either<string, Duration> => {
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);
-}
diff --git a/src/email.ts b/src/email.ts
index 906b86d..0e4bd88 100644
--- a/src/email.ts
+++ b/src/email.ts
@@ -4,16 +4,27 @@ import * as O from "fp-ts/lib/Option";
import { createTransport } from "nodemailer";
import { toError } from "fp-ts/lib/Either";
import { pipe } from "fp-ts/lib/function";
-import { ImapFlow, type FetchMessageObject, type FetchQueryObject, type MailboxLockObject } from "imapflow";
+import {
+ ImapFlow,
+ type FetchMessageObject,
+ type FetchQueryObject,
+ type MailboxLockObject,
+} from "imapflow";
import * as IO from "fp-ts/lib/IO";
import * as T from "fp-ts/lib/Task";
import { ConsoleLogger, type Logger } from "./logger";
interface ImapClientI {
- fetchAll: (range: string, options: FetchQueryObject) => Promise<FetchMessageObject[]>;
+ fetchAll: (
+ range: string,
+ options: FetchQueryObject,
+ ) => Promise<FetchMessageObject[]>;
connect: () => Promise<void>;
getMailboxLock: (mailbox: string) => Promise<MailboxLockObject>;
- messageDelete: (uids: number[], opts: Record<string, any>) => Promise<boolean>;
+ messageDelete: (
+ uids: number[],
+ opts: Record<string, any>,
+ ) => Promise<boolean>;
logout: () => Promise<void>;
}
@@ -34,8 +45,13 @@ class ErrorWithLock extends Error {
}
}
-const ToErrorWithLock = (lock?: MailboxLockObject, imap?: ImapClientI) => (error: unknown) =>
- new ErrorWithLock(error instanceof Error ? error.message : "Unknown error", lock, imap);
+const ToErrorWithLock =
+ (lock?: MailboxLockObject, imap?: ImapClientI) => (error: unknown) =>
+ new ErrorWithLock(
+ error instanceof Error ? error.message : "Unknown error",
+ lock,
+ imap,
+ );
/**
* Generate a unique email.
@@ -43,31 +59,42 @@ const ToErrorWithLock = (lock?: MailboxLockObject, imap?: ImapClientI) => (error
* @param to is the email to send to.
* @returns an {@link Email}.
*/
-type EmailGenerator = (from: EmailFromInstruction, to: EmailToInstruction) => IO.IO<Email>;
-const generateEmail: EmailGenerator = (from: EmailFromInstruction, to: EmailToInstruction) => () => ({
- from: from.email,
- to: to.email,
- subject: [new Date().toISOString(), crypto.randomUUID()].join(" | "),
- text: crypto.randomUUID()
-});
+type EmailGenerator = (
+ from: EmailFromInstruction,
+ to: EmailToInstruction,
+) => IO.IO<Email>;
+const generateEmail: EmailGenerator =
+ (from: EmailFromInstruction, to: EmailToInstruction) => () => ({
+ from: from.email,
+ to: to.email,
+ subject: [new Date().toISOString(), crypto.randomUUID()].join(" | "),
+ text: crypto.randomUUID(),
+ });
/**
* Get the transport layer for a mailbox to send a piece of mail.
* @param param0 is the mailbox to send from.
* @returns a function that takes an email and sends it.
*/
-type GetSendEmail = (from: EmailFromInstruction) => (email: Email) => TE.TaskEither<Error, Email>;
-const getSendTransport: GetSendEmail = ({ username, password, server, send_port }) => {
+type GetSendEmail = (
+ from: EmailFromInstruction,
+) => (email: Email) => TE.TaskEither<Error, Email>;
+const getSendTransport: GetSendEmail = ({
+ username,
+ password,
+ server,
+ send_port,
+}) => {
const transport = createTransport({
host: server,
port: send_port,
auth: {
user: username,
- pass: password
+ pass: password,
},
tls: {
- rejectUnauthorized: false
- }
+ rejectUnauthorized: false,
+ },
});
return (email: Email) =>
TE.tryCatch(
@@ -79,9 +106,9 @@ const getSendTransport: GetSendEmail = ({ username, password, server, send_port
} else {
resolve(email);
}
- })
+ }),
),
- toError
+ toError,
);
};
@@ -90,7 +117,9 @@ const getSendTransport: GetSendEmail = ({ username, password, server, send_port
* @param param0 is the mailbox to read from.
* @returns a Right({@link ImapFlow}) if it connected, else an Left(error).
*/
-type GetImapClient = (to: EmailToInstruction) => TE.TaskEither<Error, ImapClientI>;
+type GetImapClient = (
+ to: EmailToInstruction,
+) => TE.TaskEither<Error, ImapClientI>;
const getImap: GetImapClient = ({ username, password, server, read_port }) => {
const imap = new ImapFlow({
logger: false,
@@ -99,8 +128,8 @@ const getImap: GetImapClient = ({ username, password, server, read_port }) => {
secure: true,
auth: {
user: username,
- pass: password
- }
+ pass: password,
+ },
});
return TE.tryCatch(() => imap.connect().then(() => imap), toError);
};
@@ -109,16 +138,18 @@ const getImap: GetImapClient = ({ username, password, server, read_port }) => {
* @param imap is the Imap client to fetch messages from.
* @returns a Right({@link FetchMessageObject}[]) if successful, else a Left(error).
*/
-const fetchMessages = (imap: ImapClientI): TE.TaskEither<Error, FetchMessageObject[]> =>
+const fetchMessages = (
+ imap: ImapClientI,
+): TE.TaskEither<Error, FetchMessageObject[]> =>
TE.tryCatch(
() =>
imap.fetchAll("*", {
uid: true,
envelope: true,
headers: true,
- bodyParts: ["text"]
+ bodyParts: ["text"],
}),
- toError
+ toError,
);
/**
@@ -129,7 +160,8 @@ const fetchMessages = (imap: ImapClientI): TE.TaskEither<Error, FetchMessageObje
type EmailMatcher = (email: Email) => (message: FetchMessageObject) => boolean;
const matchesEmail: EmailMatcher = (email) => (message) => {
const subjectMatches = email.subject === message.envelope.subject;
- const bodyMatches = message.bodyParts.get("text")?.toString().trim() === email.text.trim();
+ const bodyMatches =
+ message.bodyParts.get("text")?.toString().trim() === email.text.trim();
const headers = message.headers.toLocaleString();
const fromMatches = headers.includes(`Return-Path: <${email.from}>`);
const toMatches = headers.includes(`Delivered-To: ${email.to}`);
@@ -151,7 +183,13 @@ type FindEmailUidInInbox = (
pollIntervalMs: number,
logger?: Logger,
) => TE.TaskEither<Error, number>;
-const findEmailUidInInbox: FindEmailUidInInbox = (imap, equalsEmail, retries, pollIntervalMs, logger = ConsoleLogger) =>
+const findEmailUidInInbox: FindEmailUidInInbox = (
+ imap,
+ equalsEmail,
+ retries,
+ pollIntervalMs,
+ logger = ConsoleLogger,
+) =>
pipe(
fetchMessages(imap),
TE.flatMap((messages) => {
@@ -164,17 +202,25 @@ const findEmailUidInInbox: FindEmailUidInInbox = (imap, equalsEmail, retries, po
TE.fold(
(e) =>
pipe(
- TE.fromIO(logger.log(`failed to find email; ${retries} retries left.`)),
- TE.chain(() => (retries === 0 ? TE.left(e) : T.delay(pollIntervalMs)(TE.right(null)))),
- TE.chain(() => findEmailUidInInbox(imap, equalsEmail, retries - 1, pollIntervalMs))
+ TE.fromIO(
+ logger.log(`failed to find email; ${retries} retries left.`),
+ ),
+ TE.chain(() =>
+ retries === 0
+ ? TE.left(e)
+ : T.delay(pollIntervalMs)(TE.right(null)),
+ ),
+ TE.chain(() =>
+ findEmailUidInInbox(imap, equalsEmail, retries - 1, pollIntervalMs),
+ ),
),
(s) =>
pipe(
s,
TE.of,
- TE.tap(() => TE.fromIO(logger.log("Email succeeded")))
- )
- )
+ TE.tap(() => TE.fromIO(logger.log("Email succeeded"))),
+ ),
+ ),
);
export type EmailJobDependencies = {
@@ -196,23 +242,35 @@ export const perform = (
getSendImpl = getSendTransport,
getImapImpl = getImap,
findEmailUidInInboxImpl = findEmailUidInInbox,
- matchesEmailImpl = matchesEmail
- }: Partial<EmailJobDependencies> = {}
+ matchesEmailImpl = matchesEmail,
+ }: Partial<EmailJobDependencies> = {},
): TE.TaskEither<Error, boolean> =>
pipe(
// arrange.
TE.fromIO(generateEmailImpl(from, to)),
TE.bindTo("email"),
// 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("mailboxLock", ({ imap }) => TE.tryCatch(() => imap.getMailboxLock("INBOX"), ToErrorWithLock(undefined, imap))),
+ 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, imap))
- )
+ findEmailUidInInboxImpl(
+ imap,
+ matchesEmailImpl(email),
+ retries,
+ interval,
+ ),
+ TE.mapLeft(ToErrorWithLock(mailboxLock, imap)),
+ ),
),
// cleanup.
TE.bind("deleted", ({ imap, uid, mailboxLock }) =>
@@ -228,7 +286,10 @@ export const perform = (
}
if (O.isSome(e.imap)) {
const imap = e.imap.value;
- return pipe(TE.tryCatch(() => imap.logout(), toError), TE.flatMap(() => TE.left(e)));
+ return pipe(
+ TE.tryCatch(() => imap.logout(), toError),
+ TE.flatMap(() => TE.left(e)),
+ );
}
return TE.left(e);
},
@@ -236,6 +297,6 @@ export const perform = (
mailboxLock.release();
imap.logout();
return TE.right(deleted);
- }
- )
+ },
+ ),
);