diff --git a/src/email.ts b/src/email.ts index f8916d4..6ad05a0 100644 --- a/src/email.ts +++ b/src/email.ts @@ -4,24 +4,16 @@ 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 } from "./logger"; +import { ConsoleLogger, type Logger } from "./logger"; interface ImapClientI { - fetchAll: ( - range: string, - options: FetchQueryObject, - ) => Promise; + fetchAll: (range: string, options: FetchQueryObject) => Promise; connect: () => Promise; getMailboxLock: (mailbox: string) => Promise; - messageDelete: (uids: number[], opts: any) => Promise; + messageDelete: (uids: number[], opts: Record) => Promise; close: () => void; } @@ -40,10 +32,7 @@ class ErrorWithLock extends Error { } } const ToErrorWithLock = (lock?: MailboxLockObject) => (error: unknown) => - new ErrorWithLock( - error instanceof Error ? error.message : "Unknown error", - lock, - ); + new ErrorWithLock(error instanceof Error ? error.message : "Unknown error", lock); /** * Generate a unique email. @@ -51,42 +40,31 @@ const ToErrorWithLock = (lock?: MailboxLockObject) => (error: unknown) => * @param to is the email to send to. * @returns an {@link Email}. */ -type EmailGenerator = ( - from: EmailFromInstruction, - to: EmailToInstruction, -) => IO.IO; -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; +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; -const getSendTransport: GetSendEmail = ({ - username, - password, - server, - send_port, -}) => { +type GetSendEmail = (from: EmailFromInstruction) => (email: Email) => TE.TaskEither; +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( @@ -98,9 +76,9 @@ const getSendTransport: GetSendEmail = ({ } else { resolve(email); } - }), + }) ), - toError, + toError ); }; @@ -109,9 +87,7 @@ const getSendTransport: GetSendEmail = ({ * @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; +type GetImapClient = (to: EmailToInstruction) => TE.TaskEither; const getImap: GetImapClient = ({ username, password, server, read_port }) => { const imap = new ImapFlow({ logger: false, @@ -120,8 +96,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); }; @@ -130,18 +106,16 @@ 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 => +const fetchMessages = (imap: ImapClientI): TE.TaskEither => TE.tryCatch( () => imap.fetchAll("*", { uid: true, envelope: true, headers: true, - bodyParts: ["text"], + bodyParts: ["text"] }), - toError, + toError ); /** @@ -152,8 +126,7 @@ const fetchMessages = ( 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}`); @@ -173,13 +146,9 @@ type FindEmailUidInInbox = ( equalsEmail: (message: FetchMessageObject) => boolean, retries: number, pollIntervalMs: number, + logger?: Logger, ) => TE.TaskEither; -const findEmailUidInInbox: FindEmailUidInInbox = ( - imap, - equalsEmail, - retries, - pollIntervalMs, -) => +const findEmailUidInInbox: FindEmailUidInInbox = (imap, equalsEmail, retries, pollIntervalMs, logger = ConsoleLogger) => pipe( fetchMessages(imap), TE.flatMap((messages) => { @@ -192,20 +161,17 @@ const findEmailUidInInbox: FindEmailUidInInbox = ( TE.fold( (e) => pipe( - TE.fromIO( - ConsoleLogger.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)) ), - TE.of, - ), + (s) => + pipe( + s, + TE.of, + TE.tap(() => TE.fromIO(logger.log("Email succeeded"))) + ) + ) ); export type EmailJobDependencies = { @@ -227,32 +193,23 @@ export const perform = ( getSendImpl = getSendTransport, getImapImpl = getImap, findEmailUidInInboxImpl = findEmailUidInInbox, - matchesEmailImpl = matchesEmail, - }: Partial = {}, + matchesEmailImpl = matchesEmail + }: Partial = {} ): TE.TaskEither => 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()), - ), + TE.bind("mailboxLock", ({ imap }) => TE.tryCatch(() => imap.getMailboxLock("INBOX"), ToErrorWithLock())), // "assert". TE.bind("uid", ({ imap, email, mailboxLock }) => pipe( - findEmailUidInInboxImpl( - imap, - matchesEmailImpl(email), - retries, - interval, - ), - TE.mapLeft(ToErrorWithLock(mailboxLock)), - ), + findEmailUidInInboxImpl(imap, matchesEmailImpl(email), retries, interval), + TE.mapLeft(ToErrorWithLock(mailboxLock)) + ) ), // cleanup. TE.bind("deleted", ({ imap, uid, mailboxLock }) => @@ -271,6 +228,6 @@ export const perform = ( ({ mailboxLock, deleted }) => { mailboxLock.release(); return TE.right(deleted); - }, - ), + } + ) );