nodemailer.js 2.31 KB
/**
 * 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<any>|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 归一
      }
    },
  }
}