import nodemailer, { type SendMailOptions, type Transporter } from "nodemailer"; import { SMTPS_URL } from "../config.ts"; export type EmailRecipient = string | string[]; export interface EmailMessage { from: string; to: EmailRecipient; subject: string; text?: string; html?: string; cc?: EmailRecipient; bcc?: EmailRecipient; replyTo?: string; } export interface EmailSendResult { messageId: string; accepted: string[]; rejected: string[]; } let transporter: Transporter | null = null; export function isEmailEnabled(): boolean { return SMTPS_URL.length > 0; } function normalizeRecipients( recipient: string | readonly (string | { address?: string })[] | undefined, ): string[] { if (!recipient) return []; if (typeof recipient === "string") { return recipient.split(",").map((s) => s.trim()).filter(Boolean); } return recipient.flatMap((entry) => typeof entry === "string" ? entry.trim() : (entry.address?.trim() ?? "") ).filter(Boolean); } function getTransporter(): Transporter { if (!isEmailEnabled()) { throw new Error( "GERBEUR_SMTPS_URL environment variable is required to send emails.", ); } if (!transporter) { transporter = nodemailer.createTransport(SMTPS_URL); } return transporter; } export async function verifyEmailTransport(): Promise { if (!isEmailEnabled()) return false; await getTransporter().verify(); return true; } export async function sendEmail(message: EmailMessage): Promise { if (normalizeRecipients(message.to).length === 0) { throw new Error("Email recipient is required."); } if (!message.subject.trim()) { throw new Error("Email subject is required."); } if (!message.text?.trim() && !message.html?.trim()) { throw new Error("Email body is required (text and/or html)."); } const options: SendMailOptions = { from: message.from, to: message.to, cc: message.cc, bcc: message.bcc, subject: message.subject, text: message.text, html: message.html, replyTo: message.replyTo, }; const info = await getTransporter().sendMail(options); return { messageId: info.messageId, accepted: normalizeRecipients(info.accepted), rejected: normalizeRecipients(info.rejected), }; }