/** * createEmail —— 编排层。 * 职责:按 provider 选实现、send 前校验、sendTemplate = 渲染 + send、把厂商错误归一为 EmailError。 * 不碰具体投递协议(那在 providers/.js),不生成/校验验证码(那是 @drama/auth)。 */ import { EmailError, mapError } from './map-error.js' import { assertSendable } from './validate.js' import { renderTemplate } from './templates/index.js' import { createNodemailerProvider } from './providers/nodemailer.js' import { createSesProvider } from './providers/ses.js' /** provider 名 → 工厂。加厂商 = 加 providers/.js + 这里登记一行。 */ const PROVIDERS = { nodemailer: createNodemailerProvider, ses: createSesProvider, } /** * @typedef {Object} Email * @property {(msg: import('./index.js').SendMessage) => Promise} send * @property {(name: string, data: object) => Promise} sendTemplate */ /** * 唯一入口工厂。SMTP/SES 凭据与发件域由调用方(composition root)注入,包内不读 env。 * * @param {Object} [opts] * @param {string} [opts.provider] 'nodemailer' | 'ses'(缺省 'nodemailer') * @param {any} [opts.credentials] * @param {import('./index.js').Provider} [opts.providerImpl] 直接注入 provider(测试用,绕过内建表) * @returns {Email} */ export function createEmail({ provider = 'nodemailer', credentials, providerImpl } = {}) { const impl = resolveProvider({ provider, credentials, providerImpl }) const providerName = provider /** @type {Email['send']} */ async function send(msg) { assertSendable(msg, providerName) try { const result = await impl.send(msg) return result } catch (err) { throw mapError(err, providerName) } } /** * @param {string} name * @param {any} data * @returns {Promise} */ async function sendTemplate(name, data) { if (!data || typeof data !== 'object') { throw new EmailError({ provider: providerName, reason: 'badTemplate', retryable: false, message: `template "${name}": data required` }) } // 渲染是纯函数,badTemplate / 缺字段在此抛出(早于任何投递)。 const rendered = renderTemplate(name, data) return send({ to: data.to, subject: rendered.subject, html: rendered.html, text: rendered.text, from: data.from, reply_to: data.reply_to, cc: data.cc, bcc: data.bcc, }) } return { send, sendTemplate } } /** * @param {{ provider: string, credentials: any, providerImpl?: import('./index.js').Provider }} args * @returns {import('./index.js').Provider} */ function resolveProvider({ provider, credentials, providerImpl }) { if (providerImpl) { if (typeof providerImpl.send !== 'function') { throw new EmailError({ provider: 'unknown', reason: 'unknown', retryable: false, message: 'providerImpl must expose send()' }) } return providerImpl } const factory = PROVIDERS[provider] if (!factory) { const supported = Object.keys(PROVIDERS).join(', ') throw new EmailError({ provider: String(provider), reason: 'unknown', retryable: false, message: `unknown provider "${provider}". supported: ${supported}`, }) } if (!credentials || typeof credentials !== 'object') { throw new EmailError({ provider, reason: 'auth', retryable: false, message: `provider "${provider}" requires credentials`, }) } return factory(credentials) } /** @returns {string[]} 支持的内建 provider 名 */ export function supportedProviders() { return Object.keys(PROVIDERS) }