import { encodeBase64 } from "@std/encoding/base64"; import { getLogger } from "./logger.ts"; export type MailDeliverStateNotSent = { kind: "not-sent" }; export type MailDeliverStateDelivered = { kind: "delivered"; deliverer: MailDeliverer; error?: Error; }; export type MailDeliverStateError = { kind: "error"; deliverer: MailDeliverer; error: Error; }; export type MailDeliverState = | MailDeliverStateNotSent | MailDeliverStateDelivered | MailDeliverStateError; class MailDeliverError extends Error { constructor( message: string, options: ErrorOptions, public readonly mail: Mail, ) { super(message, options); } } export class Mail { messageId: string | null = null; awsMessageId: string | null = null; deliverState: MailDeliverState = { kind: "not-sent" }; constructor(public raw: string) {} setDelivered(deliverer: MailDeliverer, error?: Error) { this.deliverState = { "kind": "delivered", deliverer, error }; } throwDeliverError( deliverer: MailDeliverer, reason: string, cause?: unknown, ): never { const error = new MailDeliverError( `Failed to deliver mail to ${deliverer.destination}: ${reason}`, { cause }, this, ); this.deliverState = { "kind": "error", deliverer, error }; throw error; } encodeUtf8(): Uint8Array { const utf8Encoder = new TextEncoder(); // TODO: A problem here is if mail is VERY long, this will block for a long time. // Maybe some task queue can be used. return utf8Encoder.encode(this.raw); } getRawBase64(): string { return encodeBase64(this.raw); } appendHeaders( rawMail: string, headers: [key: string, value: string][], ): string { const separatorMatch = rawMail.match(/(\r\n|\n)(\r\n|\n)/); if (separatorMatch == null) { throw new Error( "No header/body separator (2 successive EOLs) found. Cannot append headers.", ); } if (separatorMatch[1] !== separatorMatch[2]) { getLogger().warn("Different EOLs (\\r\\n and \\n) found in mail!"); } const headerStr = headers.map(([k, v]) => `${k}: ${v}${separatorMatch[1]}`) .join(""); const endOfHeadersIndex = separatorMatch.index! + separatorMatch[1].length; return rawMail.slice(0, endOfHeadersIndex) + headerStr + rawMail.slice(endOfHeadersIndex); } } type MailDeliverHook = (mail: Mail) => Promise; export abstract class MailDeliverer { preHooks: MailDeliverHook[] = []; postHooks: MailDeliverHook[] = []; constructor(public readonly destination: string) {} protected doPrepare(_mail: Mail): Promise { return Promise.resolve(); } protected abstract doDeliver(mail: Mail): Promise; protected doFinalize(_mail: Mail): Promise { return Promise.resolve(); } async deliverRaw(raw: string): Promise { const mail = new Mail(raw); await this.deliver(mail); } async deliver(mail: Mail): Promise { this.doPrepare(mail); for (const hook of this.preHooks) { await hook(mail); } await this.doDeliver(mail); if (mail.deliverState.kind === "not-sent") { mail.setDelivered(this); } for (const hook of this.postHooks) { await hook(mail); } await this.doFinalize(mail); } }