email.js
3.66 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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
* createEmail —— 编排层。
* 职责:按 provider 选实现、send 前校验、sendTemplate = 渲染 + send、把厂商错误归一为 EmailError。
* 不碰具体投递协议(那在 providers/<vendor>.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/<vendor>.js + 这里登记一行。 */
const PROVIDERS = {
nodemailer: createNodemailerProvider,
ses: createSesProvider,
}
/**
* @typedef {Object} Email
* @property {(msg: import('./index.js').SendMessage) => Promise<import('./index.js').SendResult>} send
* @property {(name: string, data: object) => Promise<import('./index.js').SendResult>} 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<import('./index.js').SendResult>}
*/
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)
}