77 lines
2.4 KiB
TypeScript
77 lines
2.4 KiB
TypeScript
/**
|
|
* 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 { EMAIL_ENABLED, FROM_EMAIL, SMTPS_URL } from "../config.ts";
|
|
import type { RenderedEmail } from "../mail/layout.ts";
|
|
|
|
if (EMAIL_ENABLED && !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<string, string>;
|
|
}
|
|
|
|
/**
|
|
* 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<unknown>;
|
|
}
|
|
|
|
let transport: EmailTransport | null = EMAIL_ENABLED
|
|
? 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<void> {
|
|
if (!transport) {
|
|
throw new Error("Email is disabled: GERBEUR_SMTPS_URL is unset.");
|
|
}
|
|
await transport.sendMail({ ...email, from: FROM_EMAIL });
|
|
}
|