v3: search engine, responsive header with compact user menu
This commit is contained in:
93
api/services/email-service.ts
Normal file
93
api/services/email-service.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user