summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2025-08-17 23:50:24 -0700
committerElizabeth Hunt <me@liz.coffee>2025-08-17 23:53:38 -0700
commit1a4fc9535a89b58e8b67c8996ade0b833116af3a (patch)
treed16f3129d7bb69f204bba8422e909354195a0042 /src
parent157dc327e8fe63541b517cfbeeaf202a3e8553a5 (diff)
downloaduptime-1a4fc9535a89b58e8b67c8996ade0b833116af3a.tar.gz
uptime-1a4fc9535a89b58e8b67c8996ade0b833116af3a.zip
Move to pengueno.
Diffstat (limited to 'src')
-rw-r--r--src/api.ts53
-rw-r--r--src/email.ts351
-rw-r--r--src/job.ts37
-rw-r--r--src/logger.ts16
4 files changed, 0 insertions, 457 deletions
diff --git a/src/api.ts b/src/api.ts
deleted file mode 100644
index 6f7db38..0000000
--- a/src/api.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { perform } from "./email";
-import { redactJob, type EmailJob } from "./job";
-import { ConsoleLogger, type Logger } from "./logger";
-
-export const main = (port: number) => {
- const server = Bun.serve({
- port,
- async fetch(req) {
- ConsoleLogger.info(`Received request: ${req.url}`)();
-
- const url = new URL(req.url);
- if (req.method === "POST" && url.pathname === "/api/email") {
- const job: EmailJob = await req.json();
- const jobInsensitive = redactJob(job);
-
- const uuid = crypto.randomUUID();
- const logger: Logger = {
- info: (message) => ConsoleLogger.info(`[${uuid}] ${message}`),
- warn: (message) => ConsoleLogger.warn(`[${uuid}] ${message}`),
- error: (message) => ConsoleLogger.error(`[${uuid}] ${message}`),
- }
- logger.info(
- `Received email job: ${JSON.stringify(jobInsensitive)}`,
- )();
-
- const performEmailTest = perform(job, undefined, logger)();
- return performEmailTest
- .then((result) => {
- if (result._tag === "Left") {
- const error = result.left;
- logger.warn(
- `job failure due to ${error.message}`,
- )();
- return new Response(error.message, {
- status: 400,
- });
- }
- logger.info('success')();
- return Response.json({ success: true });
- })
- .catch((e) => {
- logger.error(`internal failure due to ${e}`)();
- return new Response(e.message, {
- status: 500,
- });
- });
- }
- return new Response("404!", { status: 404 });
- },
- });
- ConsoleLogger.info(`Listening on port ${port}`)();
- return server;
-};
diff --git a/src/email.ts b/src/email.ts
deleted file mode 100644
index 328afab..0000000
--- a/src/email.ts
+++ /dev/null
@@ -1,351 +0,0 @@
-import type { EmailFromInstruction, EmailJob, EmailToInstruction } from "./job";
-import * as TE from "fp-ts/lib/TaskEither";
-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 * 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[]>;
- connect: () => Promise<void>;
- getMailboxLock: (mailbox: string) => Promise<MailboxLockObject>;
- messageDelete: (
- uids: number[],
- opts: Record<string, any>,
- ) => Promise<boolean>;
- logout: () => Promise<void>;
- mailboxClose: () => Promise<boolean>;
-}
-
-type Email = {
- from: string;
- to: string;
- subject: string;
- text: string;
-};
-
-class ErrorWithLock extends Error {
- lock: O.Option<MailboxLockObject>;
- imap: O.Option<ImapClientI>;
- constructor(message: string, lock?: MailboxLockObject, imap?: ImapClientI) {
- super(message);
- this.lock = O.fromNullable(lock);
- this.imap = O.fromNullable(imap);
- }
-}
-
-const ToErrorWithLock =
- (lock?: MailboxLockObject, imap?: ImapClientI) => (error: unknown) =>
- new ErrorWithLock(
- error instanceof Error ? error.message : "Unknown error",
- lock,
- imap,
- );
-
-/**
- * Generate a unique email.
- * @param from is the email to send from.
- * @param to is the email to send to.
- * @returns an {@link Email}.
- */
-type EmailGenerator = (
- from: EmailFromInstruction,
- to: EmailToInstruction,
- logger: Logger,
-) => IO.IO<Email>;
-const generateEmail: EmailGenerator = (
- from: EmailFromInstruction,
- to: EmailToInstruction,
- logger: Logger,
-) =>
- pipe(
- IO.of(logger.info("Generating email...")),
- IO.chain(() =>
- IO.of({
- 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,
- logger: Logger,
-) => (email: Email) => TE.TaskEither<Error, Email>;
-const getSendTransport: GetSendEmail = (
- { username, password, server, send_port },
- _logger,
-) => {
- const transport = createTransport({
- host: server,
- port: send_port,
- auth: {
- user: username,
- pass: password,
- },
- tls: {
- rejectUnauthorized: false,
- },
- });
- return (email: Email) =>
- TE.tryCatch(
- () =>
- new Promise<Email>((resolve, reject) =>
- transport.sendMail(email, (error) => {
- if (error) {
- reject(error);
- } else {
- resolve(email);
- }
- }),
- ),
- toError,
- );
-};
-
-/**
- * Get an Imap client connected to a mailbox.
- * @param param0 is the mailbox to read from.
- * @returns a Right({@link ImapFlow}) if it connected, else an Left(error).
- */
-type GetImapClient = (
- to: EmailToInstruction,
- logger: Logger,
-) => TE.TaskEither<Error, ImapClientI>;
-const getImap: GetImapClient = (
- { username, password, server, read_port },
- logger,
-) => {
- const imap = new ImapFlow({
- logger: false,
- host: server,
- port: read_port,
- secure: true,
- auth: {
- user: username,
- pass: password,
- },
- });
- return pipe(
- TE.fromIO(logger.info("Connecting to IMAP server...")),
- TE.flatMap(() =>
- TE.tryCatch(() => imap.connect().then(() => imap), toError),
- ),
- TE.tap(() => TE.fromIO(logger.info("Connected to IMAP server."))),
- TE.tapError((error) => TE.fromIO(logger.error(error.message))),
- );
-};
-
-/**
- * @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,
- logger: Logger,
-): TE.TaskEither<Error, FetchMessageObject[]> =>
- pipe(
- TE.fromIO(logger.info("Fetching messages...")),
- TE.chain(() =>
- TE.tryCatch(
- () =>
- imap.fetchAll("*", {
- uid: true,
- envelope: true,
- headers: true,
- bodyParts: ["text"],
- }),
- toError,
- ),
- ),
- TE.tap(() => TE.fromIO(logger.info("Fetched messages."))),
- TE.tapError((error) => TE.fromIO(logger.error(error.message))),
- );
-
-/**
- * Curry a function to check if a message matches an email.
- * @param email is the email to match.
- * @returns a function that takes a message and returns true if it matches the email.
- */
-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 headers = message.headers?.toLocaleString();
- const fromMatches = headers?.includes(`Return-Path: <${email.from}>`);
- const toMatches = headers?.includes(`Delivered-To: ${email.to}`);
- return subjectMatches && bodyMatches && fromMatches && toMatches;
-};
-
-/**
- * Find an email in the inbox.
- * @param imap is the Imap client to search with.
- * @param email is the email to search for.
- * @param retries is the number of retries left.
- * @param pollIntervalMs is the time to wait between retries.
- * @param logger is the logger instance.
- * @returns a Right(number) if the email was found, else a Left(error).
- */
-type FindEmailUidInInbox = (
- imap: ImapClientI,
- equalsEmail: (message: FetchMessageObject) => boolean,
- retries: number,
- pollIntervalMs: number,
- logger: Logger,
-) => TE.TaskEither<Error, number>;
-const findEmailUidInInbox: FindEmailUidInInbox = (
- imap,
- equalsEmail,
- retries,
- pollIntervalMs,
- logger,
-) =>
- pipe(
- fetchMessages(imap, logger),
- TE.flatMap((messages) => {
- const message = messages.find(equalsEmail);
- if (message) {
- return TE.right(message.uid);
- }
- return TE.left(new Error("Email message not found"));
- }),
- TE.fold(
- (e) =>
- pipe(
- TE.fromIO(
- logger.info(`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,
- logger,
- ),
- ),
- ),
- (s) =>
- pipe(
- s,
- TE.of,
- TE.tap(() => TE.fromIO(logger.info("Email succeeded"))),
- ),
- ),
- );
-
-export type EmailJobDependencies = {
- generateEmailImpl: EmailGenerator;
- getSendImpl: GetSendEmail;
- getImapImpl: GetImapClient;
- findEmailUidInInboxImpl: FindEmailUidInInbox;
- matchesEmailImpl: EmailMatcher;
-};
-
-/**
- * Perform an email job.
- * @param job is the job to perform.
- * @param logger is the logger instance.
- */
-export const perform = (
- { from, to, readRetry: { retries, interval } }: EmailJob,
- {
- generateEmailImpl = generateEmail,
- getSendImpl = getSendTransport,
- getImapImpl = getImap,
- findEmailUidInInboxImpl = findEmailUidInInbox,
- matchesEmailImpl = matchesEmail,
- }: Partial<EmailJobDependencies> = {},
- logger: Logger = ConsoleLogger,
-): TE.TaskEither<Error, boolean> =>
- pipe(
- // arrange.
- TE.fromIO(generateEmailImpl(from, to, logger)),
- TE.bindTo("email"),
- // act.
- TE.tap(({ email }) =>
- pipe(getSendImpl(from, logger)(email), TE.mapLeft(ToErrorWithLock())),
- ),
- TE.bind("imap", () =>
- pipe(getImapImpl(to, logger), TE.mapLeft(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,
- logger,
- ),
- TE.mapLeft(ToErrorWithLock(mailboxLock, imap)),
- ),
- ),
- // cleanup.
- TE.bind("deleted", ({ imap, uid, mailboxLock }) =>
- TE.tryCatch(
- () => imap.messageDelete([uid], { uid: true }),
- ToErrorWithLock(mailboxLock, imap),
- ),
- ),
- TE.fold(
- (e) => {
- if (O.isSome(e.lock)) {
- e.lock.value.release();
- }
- if (O.isSome(e.imap)) {
- const imap = e.imap.value;
- return pipe(
- TE.tryCatch(
- () => imap.mailboxClose().then(() => imap.logout()),
- toError,
- ),
- TE.flatMap(() => TE.left(e)),
- );
- }
- return TE.left(e);
- },
- ({ mailboxLock, deleted, imap }) => {
- mailboxLock.release();
- return pipe(
- TE.tryCatch(
- () => imap.mailboxClose().then(() => imap.logout()),
- toError,
- ),
- TE.flatMap(() => TE.right(deleted)),
- );
- },
- ),
- );
diff --git a/src/job.ts b/src/job.ts
deleted file mode 100644
index b1198f8..0000000
--- a/src/job.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-export interface EmailInstruction {
- email: string;
- username: string;
- password: string;
- server: string;
-}
-
-export interface EmailFromInstruction extends EmailInstruction {
- send_port: number;
-}
-
-export interface EmailToInstruction extends EmailInstruction {
- read_port: number;
-}
-
-export interface EmailJob {
- from: EmailFromInstruction;
- to: EmailToInstruction;
- readRetry: Retry;
-}
-
-export interface Retry {
- retries: number;
- interval: number;
-}
-
-export const redact = <T extends EmailInstruction>(instruction: T): T => ({
- ...instruction,
- password: "REDACTED",
- username: "REDACTED",
-});
-
-export const redactJob = (job: EmailJob): EmailJob => ({
- ...job,
- from: redact(job.from),
- to: redact(job.to),
-});
diff --git a/src/logger.ts b/src/logger.ts
deleted file mode 100644
index ffe8f51..0000000
--- a/src/logger.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { IO } from "fp-ts/lib/IO";
-
-export interface Logger {
- info: (message: string) => IO<void>;
- error: (message: string) => IO<void>;
- warn: (message: string) => IO<void>;
-}
-
-export const ConsoleLogger: Logger = {
- info: (message: string) => () =>
- console.log(`[${new Date().toUTCString()}] INFO ` + message),
- error: (message: string) => () =>
- console.error(`[${new Date().toUTCString()}] ERROR ` + message),
- warn: (message: string) => () =>
- console.warn(`[${new Date().toUTCString()}] WARN ` + message),
-};