import nodemailer from "nodemailer"; import type { EmailEncryptionMode } from "./global-settings"; /** * A small, standalone nodemailer call — Manager and the main app are * separate deployable processes/images that share no code today (see * nextchair-manager's own package.json description); duplicating this * ~30-line transport setup is simpler than inventing a cross-package * shared module for one function, and mirrors the main app's own * src/services/notifications/providers/brevo-email-provider.ts closely * enough that a future shared package (if the two apps ever grow more * code in common) would be a mechanical extraction, not a rewrite. */ export interface TestEmailConfig { host: string; port: number; username: string; password: string; encryption: EmailEncryptionMode; fromAddress: string; fromName: string; replyTo: string; recipient: string; } export interface TestEmailResult { ok: boolean; message: string; } /** * Never rethrows the raw SMTP error to the caller — nodemailer/SMTP error * messages can echo back the configured username or other connection * details, and this result is ultimately shown in the browser. Categorized * instead: auth failure, connection failure, or a generic fallback. */ export async function sendTestSmtpEmail(config: TestEmailConfig): Promise { const transporter = nodemailer.createTransport({ host: config.host, port: config.port, secure: config.encryption === "tls", ignoreTLS: config.encryption === "none", auth: { user: config.username, pass: config.password }, connectionTimeout: 10_000, greetingTimeout: 10_000, socketTimeout: 20_000, }); try { await transporter.verify(); await transporter.sendMail({ from: config.fromName ? `"${config.fromName}" <${config.fromAddress}>` : config.fromAddress, to: config.recipient, replyTo: config.replyTo || undefined, subject: "NextChair test email", text: "This is a test email from your NextChair Manager Settings page — if you received this, your SMTP configuration is working.", html: "

This is a test email from your NextChair Manager Settings page — if you received this, your SMTP configuration is working.

", }); return { ok: true, message: `Test email sent to ${config.recipient}.` }; } catch (error) { return { ok: false, message: categorizeSmtpError(error) }; } finally { transporter.close(); } } export interface PlainEmailConfig extends Omit { recipient: string; subject: string; text: string; } /** * Same transport + error-categorising contract as sendTestSmtpEmail, for * the operator alert emails (health / backup failures). Plain text only; * never throws and never echoes raw SMTP errors. */ export async function sendPlainSmtpEmail(config: PlainEmailConfig): Promise { const transporter = nodemailer.createTransport({ host: config.host, port: config.port, secure: config.encryption === "tls", ignoreTLS: config.encryption === "none", auth: { user: config.username, pass: config.password }, connectionTimeout: 10_000, greetingTimeout: 10_000, socketTimeout: 20_000, }); try { await transporter.sendMail({ from: config.fromName ? `"${config.fromName}" <${config.fromAddress}>` : config.fromAddress, to: config.recipient, replyTo: config.replyTo || undefined, subject: config.subject, text: config.text, }); return { ok: true, message: `Sent to ${config.recipient}.` }; } catch (error) { return { ok: false, message: categorizeSmtpError(error) }; } finally { transporter.close(); } } function categorizeSmtpError(error: unknown): string { const code = typeof error === "object" && error !== null && "code" in error ? String((error as { code?: unknown }).code) : ""; const responseCode = typeof error === "object" && error !== null && "responseCode" in error ? Number((error as { responseCode?: unknown }).responseCode) : undefined; if (code === "EAUTH" || responseCode === 535) { return "Authentication failed — check the SMTP username and password."; } if (code === "ECONNECTION" || code === "ESOCKET" || code === "ECONNREFUSED") { return "Couldn't connect to the SMTP host — check the host, port, and encryption mode."; } if (code === "ETIMEDOUT") { return "Connection to the SMTP host timed out."; } if (code === "EENVELOPE") { return "The mail server rejected the sender or recipient address."; } return "Couldn't send the test email — check your SMTP settings and try again."; }