/** * 发信端口:验证码(auth 的 EmailPort.sendCode)与加入申请通过通知。 * 每次投递写入 email_delivery_logs。本文件不读 `process.env`,配置经 env 注入。 */ import { createEmail } from '@drama/email' import { recordEmailDelivery } from './email-delivery-log.js' const EMAIL_LOCALES = Object.freeze(['en', 'zh', 'ja', 'ko', 'es', 'pt']) /** * @param {{ env: import('../env.js').VokoEnv, db: import('knex').Knex, logger: { error: Function } }} deps */ export function createEmailPorts({ env, db, logger }) { const { host, port, secure, user, pass, from } = env.email const email = createEmail({ provider: 'nodemailer', credentials: { host, port, secure, auth: { user, pass }, defaultFrom: from }, }) /** @type {import('@drama/auth').EmailPort} */ const authEmail = { async sendCode({ to, email: address, code, purpose, locale }) { const toEmail = to || address const result = await email.sendTemplate('verify-code', { to: toEmail, from, code, ttlMin: 10, locale: emailLocale(locale), }) await recordEmailDelivery(db, { provider: result.provider || 'nodemailer', messageId: result.messageId || null, purpose: purpose || 'verify', toEmail, status: 'sent', template: 'verify-code', metadata: env.nodeEnv === 'production' ? null : { code }, }) }, } const accessNotifier = { /** * 申请通过通知。暂复用 waitlist-invite 模板(文案为"邀请已就绪,完成注册"),链接指向登录页。 * @param {object} _ctx * @param {{ to_email: string }} input */ async sendApproved(_ctx, { to_email }) { try { const result = await email.sendTemplate('waitlist-invite', { to: to_email, from, locale: 'en', inviteUrl: `${env.publicUrl}/login`, }) await recordEmailDelivery(db, { provider: result.provider || 'nodemailer', messageId: result.message_id || null, purpose: 'access_approved', toEmail: to_email, status: 'sent', template: 'waitlist-invite', }) } catch (err) { // 外部 IO 的最小包裹:审核结果已提交,通知失败只记录,不回滚审核。 const errorMessage = err instanceof Error ? err.message : String(err) logger.error('申请通过通知投递失败', { to_email, error: errorMessage }) await recordEmailDelivery(db, { provider: 'nodemailer', purpose: 'access_approved', toEmail: to_email, status: 'failed', template: 'waitlist-invite', errorMessage, }) } }, } return { authEmail, accessNotifier } } /** * @param {unknown} value * @returns {string} */ function emailLocale(value) { return EMAIL_LOCALES.includes(/** @type {string} */ (value)) ? /** @type {string} */ (value) : 'en' }