/** * Nodemailer / SMTP provider。 * * 纪律:transport 在本工厂内构造(注入凭据),**不在模块顶层 createTransport**、 * 不读 env。`nodemailer` 是可选 peer 依赖,按需 dynamic import,缺失时给出明确报错。 */ import { EmailError } from '../map-error.js' /** * @param {Object} credentials * @param {string} credentials.host * @param {number} credentials.port * @param {boolean} credentials.secure * @param {{ user: string, pass: string }} credentials.auth * @param {string} credentials.defaultFrom * @returns {import('../index.js').Provider} */ export function createNodemailerProvider({ host, port, secure, auth, defaultFrom }) { if (!host || !port || !auth || !defaultFrom) { throw new EmailError({ provider: 'nodemailer', reason: 'auth', retryable: false, message: 'nodemailer credentials require { host, port, auth, defaultFrom }', }) } // 惰性持有 transport:首次 send 时才构造,避免无谓 import;仍在工厂闭包内、非顶层。 /** @type {Promise|null} */ let transportPromise = null function getTransport() { if (!transportPromise) { transportPromise = import('nodemailer') .then((mod) => { const nodemailer = mod.default ?? mod return nodemailer.createTransport({ host, port, secure, auth }) }) .catch((err) => { transportPromise = null throw new EmailError({ provider: 'nodemailer', reason: 'unknown', retryable: false, cause: err, message: 'nodemailer is not installed; add it as a dependency in the composition root', }) }) } return transportPromise } return { async send(msg) { const transport = await getTransport() try { const info = await transport.sendMail({ from: msg.from ?? defaultFrom, to: msg.to, subject: msg.subject, html: msg.html, text: msg.text, replyTo: msg.reply_to, cc: msg.cc, bcc: msg.bcc, }) return { message_id: info.messageId, provider: 'nodemailer', to: msg.to } } catch (err) { if (err instanceof EmailError) throw err throw err // 由 email.js 编排层统一 mapError 归一 } }, } }