summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Hunt <elizabeth.hunt@simponic.xyz>2024-12-15 13:05:50 -0800
committerElizabeth Hunt <elizabeth.hunt@simponic.xyz>2024-12-15 13:53:43 -0800
commitc0a96e82af651724314114df2d0019ecb83c2830 (patch)
tree9e65dea31c62786cb8970b713a078bab2868be1b
parent6f45fe5a10174fd33932d17dc056898f06466067 (diff)
downloaduptime-c0a96e82af651724314114df2d0019ecb83c2830.tar.gz
uptime-c0a96e82af651724314114df2d0019ecb83c2830.zip
small refactorings
-rw-r--r--src/api.ts21
-rw-r--r--src/email.ts114
-rw-r--r--src/job.ts12
-rw-r--r--src/logger.ts12
4 files changed, 106 insertions, 53 deletions
diff --git a/src/api.ts b/src/api.ts
index 6132882..946826b 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -1,25 +1,20 @@
import { perform } from "./email";
-import type { EmailJob } from "./job";
+import { redactJob, type EmailJob } from "./job";
import { ConsoleLogger } from "./logger";
export const main = (port: number) => {
const server = Bun.serve({
port,
async fetch(req) {
- ConsoleLogger.log(`Received request: ${req.url}`)();
+ 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 = structuredClone(job);
- jobInsensitive.from.username = "****REDACTED****";
- jobInsensitive.from.password = "****REDACTED****";
- jobInsensitive.to.username = "****REDACTED****";
- jobInsensitive.to.password = "****REDACTED****";
+ const jobInsensitive = redactJob(job);
const uuid = crypto.randomUUID();
- ConsoleLogger.log(
+ ConsoleLogger.info(
`[${uuid}] Received email job: ${JSON.stringify(jobInsensitive)}`,
)();
@@ -28,18 +23,18 @@ export const main = (port: number) => {
.then((result) => {
if (result._tag === "Left") {
const error = result.left;
- ConsoleLogger.log(
+ ConsoleLogger.warn(
`[${uuid}] job failure due to ${error.message}`,
)();
return new Response(error.message, {
status: 400,
});
}
- ConsoleLogger.log(`[${uuid}] success`)();
+ ConsoleLogger.info(`[${uuid}] success`)();
return Response.json({ success: true });
})
.catch((e) => {
- ConsoleLogger.log(`[${uuid}] internal failure due to ${e}`)();
+ ConsoleLogger.error(`[${uuid}] internal failure due to ${e}`)();
return new Response(e.message, {
status: 500,
});
@@ -48,6 +43,6 @@ export const main = (port: number) => {
return new Response("404!", { status: 404 });
},
});
- ConsoleLogger.log(`Listening on port ${port}`)();
+ ConsoleLogger.info(`Listening on port ${port}`)();
return server;
};
diff --git a/src/email.ts b/src/email.ts
index 337572c..b822cd9 100644
--- a/src/email.ts
+++ b/src/email.ts
@@ -3,7 +3,7 @@ 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 { flow, pipe } from "fp-ts/lib/function";
import {
ImapFlow,
type FetchMessageObject,
@@ -26,7 +26,7 @@ interface ImapClientI {
opts: Record<string, any>,
) => Promise<boolean>;
logout: () => Promise<void>;
- mailboxClose: () => Promise<void>;
+ mailboxClose: () => Promise<boolean>;
}
type Email = {
@@ -63,14 +63,24 @@ const ToErrorWithLock =
type EmailGenerator = (
from: EmailFromInstruction,
to: EmailToInstruction,
+ logger: Logger,
) => 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(),
- });
+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.
@@ -79,13 +89,12 @@ const generateEmail: EmailGenerator =
*/
type GetSendEmail = (
from: EmailFromInstruction,
+ logger: Logger,
) => (email: Email) => TE.TaskEither<Error, Email>;
-const getSendTransport: GetSendEmail = ({
- username,
- password,
- server,
- send_port,
-}) => {
+const getSendTransport: GetSendEmail = (
+ { username, password, server, send_port },
+ _logger,
+) => {
const transport = createTransport({
host: server,
port: send_port,
@@ -120,8 +129,12 @@ const getSendTransport: GetSendEmail = ({
*/
type GetImapClient = (
to: EmailToInstruction,
+ logger: Logger,
) => TE.TaskEither<Error, ImapClientI>;
-const getImap: GetImapClient = ({ username, password, server, read_port }) => {
+const getImap: GetImapClient = (
+ { username, password, server, read_port },
+ logger,
+) => {
const imap = new ImapFlow({
logger: false,
host: server,
@@ -132,7 +145,14 @@ const getImap: GetImapClient = ({ username, password, server, read_port }) => {
pass: password,
},
});
- return TE.tryCatch(() => imap.connect().then(() => imap), toError);
+ 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))),
+ );
};
/**
@@ -141,16 +161,24 @@ const getImap: GetImapClient = ({ username, password, server, read_port }) => {
*/
const fetchMessages = (
imap: ImapClientI,
+ logger: Logger,
): TE.TaskEither<Error, FetchMessageObject[]> =>
- TE.tryCatch(
- () =>
- imap.fetchAll("*", {
- uid: true,
- envelope: true,
- headers: true,
- bodyParts: ["text"],
- }),
- toError,
+ 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))),
);
/**
@@ -164,8 +192,8 @@ const matchesEmail: EmailMatcher = (email) => (message) => {
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}`);
+ const fromMatches = headers?.includes(`Return-Path: <${email.from}>`);
+ const toMatches = headers?.includes(`Delivered-To: ${email.to}`);
return subjectMatches && bodyMatches && fromMatches && toMatches;
};
@@ -175,6 +203,7 @@ const matchesEmail: EmailMatcher = (email) => (message) => {
* @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 = (
@@ -182,17 +211,17 @@ type FindEmailUidInInbox = (
equalsEmail: (message: FetchMessageObject) => boolean,
retries: number,
pollIntervalMs: number,
- logger?: Logger,
+ logger: Logger,
) => TE.TaskEither<Error, number>;
const findEmailUidInInbox: FindEmailUidInInbox = (
imap,
equalsEmail,
retries,
pollIntervalMs,
- logger = ConsoleLogger,
+ logger,
) =>
pipe(
- fetchMessages(imap),
+ fetchMessages(imap, logger),
TE.flatMap((messages) => {
const message = messages.find(equalsEmail);
if (message) {
@@ -204,7 +233,7 @@ const findEmailUidInInbox: FindEmailUidInInbox = (
(e) =>
pipe(
TE.fromIO(
- logger.log(`failed to find email; ${retries} retries left.`),
+ logger.info(`Failed to find email; ${retries} retries left.`),
),
TE.chain(() =>
retries === 0
@@ -212,14 +241,20 @@ const findEmailUidInInbox: FindEmailUidInInbox = (
: T.delay(pollIntervalMs)(TE.right(null)),
),
TE.chain(() =>
- findEmailUidInInbox(imap, equalsEmail, retries - 1, pollIntervalMs),
+ findEmailUidInInbox(
+ imap,
+ equalsEmail,
+ retries - 1,
+ pollIntervalMs,
+ logger,
+ ),
),
),
(s) =>
pipe(
s,
TE.of,
- TE.tap(() => TE.fromIO(logger.log("Email succeeded"))),
+ TE.tap(() => TE.fromIO(logger.info("Email succeeded"))),
),
),
);
@@ -235,6 +270,7 @@ export type EmailJobDependencies = {
/**
* 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,
@@ -245,16 +281,19 @@ export const perform = (
findEmailUidInInboxImpl = findEmailUidInInbox,
matchesEmailImpl = matchesEmail,
}: Partial<EmailJobDependencies> = {},
+ logger: Logger = ConsoleLogger,
): TE.TaskEither<Error, boolean> =>
pipe(
// arrange.
- TE.fromIO(generateEmailImpl(from, to)),
+ TE.fromIO(generateEmailImpl(from, to, logger)),
TE.bindTo("email"),
// act.
TE.tap(({ email }) =>
- pipe(getSendImpl(from)(email), TE.mapLeft(ToErrorWithLock())),
+ pipe(getSendImpl(from, logger)(email), TE.mapLeft(ToErrorWithLock())),
+ ),
+ TE.bind("imap", () =>
+ pipe(getImapImpl(to, logger), TE.mapLeft(ToErrorWithLock())),
),
- TE.bind("imap", () => pipe(getImapImpl(to), TE.mapLeft(ToErrorWithLock()))),
TE.bind("mailboxLock", ({ imap }) =>
TE.tryCatch(
() => imap.getMailboxLock("INBOX"),
@@ -269,6 +308,7 @@ export const perform = (
matchesEmailImpl(email),
retries,
interval,
+ logger,
),
TE.mapLeft(ToErrorWithLock(mailboxLock, imap)),
),
diff --git a/src/job.ts b/src/job.ts
index 2beabca..b1198f8 100644
--- a/src/job.ts
+++ b/src/job.ts
@@ -23,3 +23,15 @@ 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
index 05d9fd9..ffe8f51 100644
--- a/src/logger.ts
+++ b/src/logger.ts
@@ -1,10 +1,16 @@
import type { IO } from "fp-ts/lib/IO";
export interface Logger {
- log: (message: string) => IO<void>;
+ info: (message: string) => IO<void>;
+ error: (message: string) => IO<void>;
+ warn: (message: string) => IO<void>;
}
export const ConsoleLogger: Logger = {
- log: (message: string) => () =>
- console.log(`[${new Date().toUTCString()}] ` + message),
+ 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),
};