import nodemailer, { Transporter } from "nodemailer"; import Oy from "oy-vey"; import env from "@server/env"; import Logger from "@server/logging/Logger"; import { trace } from "@server/logging/tracing"; import isCloudHosted from "@server/utils/isCloudHosted"; import { baseStyles } from "./templates/components/EmailLayout"; const useTestEmailService = env.ENVIRONMENT === "development" && !env.SMTP_USERNAME; type SendMailOptions = { to: string; replyTo?: string; subject: string; previewText?: string; text: string; component: JSX.Element; headCSS?: string; }; /** * Mailer class to send emails. */ @trace({ serviceName: "mailer", }) export class Mailer { transporter: Transporter | undefined; constructor() { if (env.SMTP_HOST) { this.transporter = nodemailer.createTransport(this.getOptions()); } if (useTestEmailService) { Logger.info( "email", "SMTP_USERNAME not provided, generating test account…" ); this.getTestTransportOptions().then((options) => { if (!options) { Logger.info( "email", "Couldn't generate a test account with ethereal.email at this time – emails will not be sent." ); return; } this.transporter = nodemailer.createTransport(options); }); } } sendMail = async (data: SendMailOptions): Promise => { const { transporter } = this; if (!transporter) { Logger.info( "email", `Attempted to send email "${data.subject}" to ${data.to} but no transport configured.` ); return; } const html = Oy.renderTemplate(data.component, { title: data.subject, headCSS: [baseStyles, data.headCSS].join(" "), previewText: data.previewText ?? "", }); try { Logger.info("email", `Sending email "${data.subject}" to ${data.to}`); const info = await transporter.sendMail({ from: env.SMTP_FROM_EMAIL, replyTo: data.replyTo ?? env.SMTP_REPLY_EMAIL ?? env.SMTP_FROM_EMAIL, to: data.to, subject: data.subject, html, text: data.text, attachments: isCloudHosted ? undefined : [ { filename: "header-logo.png", path: process.cwd() + "/public/email/header-logo.png", cid: "header-image", }, ], }); if (useTestEmailService) { Logger.info( "email", `Preview Url: ${nodemailer.getTestMessageUrl(info)}` ); } } catch (err) { Logger.error(`Error sending email to ${data.to}`, err); throw err; // Re-throw for queue to re-try } }; private getOptions() { return { host: env.SMTP_HOST, port: env.SMTP_PORT, secure: env.SMTP_SECURE ?? env.ENVIRONMENT === "production", auth: env.SMTP_USERNAME ? { user: env.SMTP_USERNAME, pass: env.SMTP_PASSWORD, } : undefined, tls: env.SMTP_SECURE ? env.SMTP_TLS_CIPHERS ? { ciphers: env.SMTP_TLS_CIPHERS, } : undefined : { rejectUnauthorized: false, }, }; } private async getTestTransportOptions() { try { const testAccount = await nodemailer.createTestAccount(); return { host: "smtp.ethereal.email", port: 587, secure: false, auth: { user: testAccount.user, pass: testAccount.pass, }, }; } catch (err) { return undefined; } } } export default new Mailer();