email.js
2.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/**
* 发信端口:验证码(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'
}