96 lines
2.2 KiB
TypeScript
96 lines
2.2 KiB
TypeScript
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<boolean> {
|
|
if (!isEmailEnabled()) return false;
|
|
await getTransporter().verify();
|
|
return true;
|
|
}
|
|
|
|
export async function sendEmail(
|
|
message: EmailMessage,
|
|
): Promise<EmailSendResult> {
|
|
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),
|
|
};
|
|
}
|