/** * Sends the site's email through the SMTP server of {@link SMTPS_URL}, which * may be unset. The welcome email, the password reset link and the digests go * through it, each rendered first by its module in `api/mail/`. They check * {@link isEmailEnabled} before rendering. * * Importing this module throws when {@link SMTPS_URL} is set without * {@link FROM_EMAIL}, so a misconfigured server doesn't start rather than fail * at its first send. * * @module */ import nodemailer from "nodemailer"; import { FROM_EMAIL, SMTPS_URL } from "../config.ts"; import type { RenderedEmail } from "../mail/layout.ts"; if (SMTPS_URL && !FROM_EMAIL) { throw new Error( "GERBEUR_FROM_EMAIL environment variable is required when GERBEUR_SMTPS_URL is set.", ); } /** * Passed to {@link sendEmail}: a rendered email, its recipient, and the * headers some emails add (the digests' `List-Unsubscribe`). */ export interface OutgoingEmail extends RenderedEmail { to: string; headers?: Record; } /** * Hands a message to a mail server. nodemailer's SMTP transport is one, and * the tests put a recording one in its place with {@link setEmailTransport}. */ export interface EmailTransport { sendMail(message: OutgoingEmail & { from: string }): Promise; } let transport: EmailTransport | null = SMTPS_URL ? nodemailer.createTransport(SMTPS_URL) : null; /** * Checks whether email can be sent. Without an SMTP server, sign-up sends no * welcome email, a password reset request does nothing, and no digest goes * out. */ export function isEmailEnabled(): boolean { return transport !== null; } /** * Replaces the transport, so that email is enabled and every message goes to * `next`; null disables email again. The tests use it to read what was sent. */ export function setEmailTransport(next: EmailTransport | null): void { transport = next; } /** * Sends `email` from {@link FROM_EMAIL} and resolves once the SMTP server has * accepted it. The welcome and reset emails don't wait for it: a slow server * mustn't hold up the request. * * @throws {Error} When email is disabled, or when the server refuses the * message or can't be reached. */ export async function sendEmail(email: OutgoingEmail): Promise { if (!transport) { throw new Error("Email is disabled: GERBEUR_SMTPS_URL is unset."); } await transport.sendMail({ ...email, from: FROM_EMAIL }); }