import type NodeMailer from "nodemailer" type Variables = Record export type MailTemplate = { subject: string html: string } export type SMTPConfiguration = { host: string port: number auth: { user: string pass: string } } export type MailerConfig = { emailFrom?: string baseUrl?: string port?: number smtp?: SMTPConfiguration } export type RenderFunction = (template: string, view: Record) => string type Logger = Pick interface MailOptions { from: string to: string subject: string html: string } interface MailTransport { sendMail(options: MailOptions, callback: (err?: Error) => void): void } export function Mailer( nodeMailer: typeof NodeMailer, render: RenderFunction, logger: Logger = console, config: MailerConfig, ) { const { baseUrl, emailFrom } = { baseUrl: "http://localhost:" + (config?.port || 8080), emailFrom: "me@localhost", ...config, } const transporter: MailTransport | undefined = config?.smtp && nodeMailer.createTransport(config.smtp) return { send: async (to: string, template: MailTemplate, variables: Variables) => { const from = (variables.from as string) || emailFrom const subject = render(template.subject, { baseUrl, ...variables }) const logPrefix = `mailto(${to}), ${subject}:` const html = render(template.html, { baseUrl, ...variables }) if (transporter) { const { err } = await new Promise<{ err?: Error | null }>(resolve => transporter.sendMail({ from, to, subject, html }, err => resolve({ err })), ) if (err) { logger.warn(`${logPrefix} ${err.message}`) throw new Error("sendMail failed") } logger.info(`${logPrefix} ok`) } else { logger.info(`${logPrefix} suppressed\n${html}`) } }, } } export type Mailer = ReturnType