@flun/mailer 2.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/LICENSE +15 -0
- package/Mail.js +47 -0
- package/README.md +475 -0
- package/copy-files.js +36 -0
- package/index.d.ts +816 -0
- package/index.js +109 -0
- package/lib/addressparser.js +230 -0
- package/lib/base64.js +103 -0
- package/lib/dkim/index.js +211 -0
- package/lib/dkim/message-parser.js +114 -0
- package/lib/dkim/relaxed-body.js +133 -0
- package/lib/dkim/sign.js +121 -0
- package/lib/fetch/cookies.js +224 -0
- package/lib/fetch/index.js +233 -0
- package/lib/http-proxy-client.js +98 -0
- package/lib/json-transport.js +38 -0
- package/lib/mail-composer.js +316 -0
- package/lib/mailer/index.js +301 -0
- package/lib/mailer/mail-message.js +226 -0
- package/lib/mime-funcs/index.js +493 -0
- package/lib/mime-funcs/mime-types.js +2061 -0
- package/lib/mime-node/index.js +811 -0
- package/lib/mime-node/last-newline.js +44 -0
- package/lib/mime-node/le-unix.js +37 -0
- package/lib/mime-node/le-windows.js +46 -0
- package/lib/punycode.js +283 -0
- package/lib/qp.js +171 -0
- package/lib/regexs.js +124 -0
- package/lib/sendmail-transport.js +91 -0
- package/lib/ses-transport.js +115 -0
- package/lib/shared.js +725 -0
- package/lib/smtp-connection/data-stream.js +68 -0
- package/lib/smtp-connection/index.js +1062 -0
- package/lib/smtp-pool/index.js +294 -0
- package/lib/smtp-pool/pool-resource.js +93 -0
- package/lib/smtp-transport.js +81 -0
- package/lib/stream-transport.js +59 -0
- package/lib/well-known/index.js +54 -0
- package/lib/well-known/services.json +802 -0
- package/lib/xoauth2.js +249 -0
- package/package.json +78 -0
package/lib/shared.js
ADDED
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
import { lookup as dnsLookup, resolve as dnsResolve } from 'dns';
|
|
2
|
+
import { format } from 'util';
|
|
3
|
+
import { isIP, connect as netConnect } from 'net';
|
|
4
|
+
import { createReadStream as fsReadStream } from 'fs';
|
|
5
|
+
import { networkInterfaces, hostname as osHostname } from 'os';
|
|
6
|
+
import { wellKnown } from './well-known/index.js';
|
|
7
|
+
import { nmfetch } from './fetch/index.js';
|
|
8
|
+
import { regexs, resetRegex } from './regexs.js';
|
|
9
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* package.json 包对象(含 name,version,homepage);
|
|
13
|
+
* >查看定义:@see {@link PK}、{@link dnsLookup}、{@link dnsResolve}
|
|
14
|
+
*/
|
|
15
|
+
const PK = (({ name, version, homepage }) => ({ name, version, homepage }))(pkg),
|
|
16
|
+
DNS_TTL = 5 * 60 * 1000, DNS_MAP = new Map(),
|
|
17
|
+
/**
|
|
18
|
+
* @param {number} family - IP地址族(4=IPv4, 6=IPv6)
|
|
19
|
+
* @param {boolean} allowInternal - 是否允许内部网络接口
|
|
20
|
+
* @returns {boolean} 是否支持该地址族
|
|
21
|
+
*/
|
|
22
|
+
_isFamilySupported = (family, allowInternal) => {
|
|
23
|
+
try {
|
|
24
|
+
const netInter = networkInterfaces(); // 获取网络接口信息
|
|
25
|
+
// 如果没有网络接口信息,支持所有地址族;否则,筛选出指定地址族的网络接口
|
|
26
|
+
return !netInter ? true : Object.values(netInter).flat()
|
|
27
|
+
.some(i => (!i.internal || allowInternal) && (i.family === `IPv${family}` || i.family === family));
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error('获取网络接口信息失败:', err);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* DNS解析器
|
|
37
|
+
* @param {number} family - IP地址族
|
|
38
|
+
* @param {string} hostname - 要解析的主机名
|
|
39
|
+
* @param {object} options - 配置选项
|
|
40
|
+
* @param {function} callback - 回调函数
|
|
41
|
+
*/
|
|
42
|
+
_resolver = (family, hostname, options = {}, callback) => {
|
|
43
|
+
// 检查地址族是否支持
|
|
44
|
+
if (!_isFamilySupported(family, options.allowInternalNetworkInterfaces)) return callback(null, []);
|
|
45
|
+
const resolver = dns.Resolver ? new dns.Resolver(options) : dns, // 创建DNS解析器实例或使用默认的dns模块
|
|
46
|
+
|
|
47
|
+
// 可忽略的DNS错误代码列表
|
|
48
|
+
dnsErrs = new Set([dns.NODATA, dns.NOTFOUND, dns.NOTIMP, dns.SERVFAIL, dns.CONNREFUSED, dns.REFUSED, 'EAI_AGAIN']);
|
|
49
|
+
resolver[`resolve${family}`](hostname, (err, addresses) => {
|
|
50
|
+
if (err) return callback(dnsErrs.has(err?.code) ? null : err, []); // 处理特定的DNS错误代码,返回空数组而不是错误
|
|
51
|
+
const result = Array.isArray(addresses) ? addresses : [addresses].flat(); // 确保返回的是数组
|
|
52
|
+
return callback(null, result);
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 格式化DNS解析结果
|
|
58
|
+
* @param {object} value - DNS解析值
|
|
59
|
+
* @param {object} extra - 额外属性
|
|
60
|
+
* @returns {object} 格式化后的DNS信息
|
|
61
|
+
*/
|
|
62
|
+
_formatDNSValue = (value, extra = {}) => {
|
|
63
|
+
if (!value) return { ...extra };
|
|
64
|
+
|
|
65
|
+
const { servername, addresses = [] } = value;
|
|
66
|
+
return {
|
|
67
|
+
servername, host: addresses.length === 0 ? null : addresses.length === 1 ? addresses[0]
|
|
68
|
+
: addresses[Math.floor(Math.random() * addresses.length)], ...extra
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 处理可读流的 readable 事件
|
|
74
|
+
* @param {stream.Readable} stream 可读流实例
|
|
75
|
+
* @param {Buffer[]} chunkArr 存储数据块的数组
|
|
76
|
+
* @param {{value: number}} lenRef 包含value属性的对象,用于累加数据长度
|
|
77
|
+
*/
|
|
78
|
+
_handleReadableStream = (stream, chunkArr, lenRef) => {
|
|
79
|
+
let chunk;
|
|
80
|
+
while ((chunk = stream.read()) !== null) {
|
|
81
|
+
chunkArr.push(chunk), lenRef.value += chunk.length;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 回调错误处理函数
|
|
87
|
+
* @param {Object} state 状态对象,包含returned属性
|
|
88
|
+
* @param {Object} connection 连接对象
|
|
89
|
+
* @param {Function} callback 回调函数
|
|
90
|
+
* @param {Error} err 错误对象
|
|
91
|
+
* @param {Object} transport 传输对象 (SmtpPool, PoolResource 或 SmtpTransport)
|
|
92
|
+
*/
|
|
93
|
+
_callbackErr = (state = {}, connection, callback, err, transport) => {
|
|
94
|
+
transport.emit('error', err), state.returned = true, connection.close();
|
|
95
|
+
return callback(err);
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 生成一个类似bunyan的日志记录器,输出到控制台
|
|
100
|
+
* @param {array} levels - 日志级别数组
|
|
101
|
+
* @returns {object} Bunyan日志记录器实例
|
|
102
|
+
*/
|
|
103
|
+
_createDefaultLogger = levels => {
|
|
104
|
+
// 计算最长日志级别名称的长度并创建固定长度的级别名称
|
|
105
|
+
const levelMaxLen = Math.max(...levels.map(l => l.length)),
|
|
106
|
+
levelNames = new Map(levels.map(l => [l, l.toUpperCase().padEnd(levelMaxLen, ' ')])),
|
|
107
|
+
buildPrefix = entry => {
|
|
108
|
+
if (!entry) return '';
|
|
109
|
+
let prefix = '';
|
|
110
|
+
|
|
111
|
+
// 根据事务类型添加前缀
|
|
112
|
+
if (entry.tnx === 'server') prefix = 'S: ';
|
|
113
|
+
else if (entry.tnx === 'client') prefix = 'C: ';
|
|
114
|
+
|
|
115
|
+
if (entry.sid) prefix = `[${entry.sid}] ${prefix}`; // 添加会话ID前缀
|
|
116
|
+
if (entry.cid) prefix = `[#${entry.cid}] ${prefix}`; // 添加连接ID前缀
|
|
117
|
+
return prefix;
|
|
118
|
+
},
|
|
119
|
+
// 打印日志的函数
|
|
120
|
+
print = (level, entry, message, ...args) => {
|
|
121
|
+
const prefix = buildPrefix(entry), formattedMessage = format(message, ...args),// 构建前缀,格式化消息
|
|
122
|
+
timestamp = new Date().toISOString().substring(0, 19).replace('T', ' '); // 格式化时间戳
|
|
123
|
+
|
|
124
|
+
// 分行打印(处理多行消息)
|
|
125
|
+
formattedMessage.split(regexs.LINE_SEPARATOR).forEach(line => {
|
|
126
|
+
console.log('[%s] %s %s', timestamp, levelNames.get(level), prefix + line);
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
return Object.fromEntries(levels.map(l => [l, print.bind(null, l)])); // 为每个日志级别创建对应的打印函数
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 日志记录函数
|
|
134
|
+
* @param {object} logger - 日志记录器
|
|
135
|
+
* @param {string} level - 日志级别
|
|
136
|
+
* @param {object} defaults - 默认属性
|
|
137
|
+
* @param {object} data - 日志数据
|
|
138
|
+
* @param {string} message - 日志消息
|
|
139
|
+
* @param {...any} args - 其他参数
|
|
140
|
+
*/
|
|
141
|
+
_logFunc = (logger, level, defaults = {}, data = {}, message, ...args) => {
|
|
142
|
+
const { level: l, ...defaultProps } = defaults, { level: l_, ...dataProps } = data; // 排除level属性
|
|
143
|
+
logger[level]({ ...defaultProps, ...dataProps }, message, ...args); // 记录合并属性后的日志
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 返回bunyan兼容的日志记录器接口
|
|
148
|
+
* 查看定义:@see {@link getLogger}
|
|
149
|
+
* @param {object} options - 配置选项,可能包含'logger'
|
|
150
|
+
* @return {object} bunyan兼容的日志记录器
|
|
151
|
+
*/
|
|
152
|
+
const getLogger = (options = {}, defaults) => {
|
|
153
|
+
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'], { logger } = options;
|
|
154
|
+
if (!logger) return Object.fromEntries(levels.map(level => [level, () => false])); // 如果没有提供日志记录器,创建空实现
|
|
155
|
+
|
|
156
|
+
const logger_ = logger === true ? _createDefaultLogger(levels) : logger; // 如果logger为true,创建控制台日志记录器
|
|
157
|
+
// 包装日志方法
|
|
158
|
+
return Object.fromEntries(
|
|
159
|
+
levels.map(level => [level, (data, message, ...args) => _logFunc(logger_, level, defaults, data, message, ...args)])
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 解析主机名,支持IPv4/IPv6和DNS缓存
|
|
165
|
+
* >查看定义:@see {@link resolveHostname}
|
|
166
|
+
* @param {object} options - 配置选项
|
|
167
|
+
* @param {function} callback - 回调函数
|
|
168
|
+
*/
|
|
169
|
+
const resolveHostname = (options = {}, callback) => {
|
|
170
|
+
const { host, servername: sName, dnsTtl } = options,
|
|
171
|
+
// 如果没有host但有servername,使用servername作为host;如果都没有,设置servername为false
|
|
172
|
+
newHost = !host && sName ? sName : host, servername = sName || newHost || false;
|
|
173
|
+
|
|
174
|
+
// 如果host是IP地址或为空,直接返回
|
|
175
|
+
if (!newHost || isIP(newHost)) {
|
|
176
|
+
const value = { addresses: [newHost], servername };
|
|
177
|
+
return callback(null, _formatDNSValue(value, { cached: false }));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 检查DNS缓存
|
|
181
|
+
let cached;
|
|
182
|
+
if (DNS_MAP.has(newHost)) {
|
|
183
|
+
cached = DNS_MAP.get(newHost);
|
|
184
|
+
// 如果缓存未过期,使用缓存值
|
|
185
|
+
if (!cached.expires || cached.expires >= Date.now()) return callback(null, _formatDNSValue(cached.value, { cached: true }));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 处理解析错误
|
|
189
|
+
const handleError = err => {
|
|
190
|
+
return cached ? callback(null, _formatDNSValue(cached.value, { cached: true, error: err })) : callback(err);
|
|
191
|
+
},
|
|
192
|
+
// 处理解析成功(addresses:解析地址数组)
|
|
193
|
+
handleSuccess = addresses => {
|
|
194
|
+
const value = { addresses, servername };
|
|
195
|
+
DNS_MAP.set(newHost, { value, expires: Date.now() + (dnsTtl || DNS_TTL) }); // 更新DNS缓存
|
|
196
|
+
return callback(null, _formatDNSValue(value, { cached: false }));
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// 首先尝试IPv4解析
|
|
200
|
+
_resolver(4, newHost, options, (err, addrV4) => {
|
|
201
|
+
if (err) return handleError(err); // 如果出错但有缓存,使用过期的缓存值,否则返回错误
|
|
202
|
+
if (addrV4?.length) return handleSuccess(addrV4); // 如果IPv4解析成功,更新DNS缓存,返回成功
|
|
203
|
+
|
|
204
|
+
// IPv4失败,尝试IPv6解析
|
|
205
|
+
_resolver(6, newHost, options, (err, addrV6) => {
|
|
206
|
+
if (err) return handleError(err);
|
|
207
|
+
if (addrV6?.length) return handleSuccess(addrV6); // 如果IPv6解析成功,同理...
|
|
208
|
+
|
|
209
|
+
// 如果DNS解析都失败,尝试使用dns.lookup
|
|
210
|
+
try {
|
|
211
|
+
dnsLookup(newHost, { all: true }, (err, lookupAddr) => {
|
|
212
|
+
if (err) return handleError(err);
|
|
213
|
+
// 过滤出系统支持的地址族的地址(取第一个支持的地址)
|
|
214
|
+
const address = lookupAddr?.find(addr => _isFamilySupported(addr.family))?.address || false;
|
|
215
|
+
|
|
216
|
+
// 如果有地址但没有可用的,输出警告
|
|
217
|
+
if (lookupAddr?.length && !address) console.warn(`无法使用当前网络解析IPv${lookupAddr[0].family}地址`);
|
|
218
|
+
|
|
219
|
+
// 如果没有找到地址但有缓存,使用缓存;否则,使用找到的地址或原始主机名
|
|
220
|
+
return !address && cached ? callback(null, _formatDNSValue(cached.value, { cached: true })) :
|
|
221
|
+
handleSuccess(address ? [address] : [newHost]);
|
|
222
|
+
});
|
|
223
|
+
} catch (err) { return handleError(err); }
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 尝试将给定的字符串解析为URL对象
|
|
230
|
+
* >查看定义:@see {@link newURL}
|
|
231
|
+
* @param {string} str - 需要解析的URL字符串,默认为空字符串
|
|
232
|
+
* @returns {URL|boolean} 如果解析成功返回URL对象,否则返回false
|
|
233
|
+
*/
|
|
234
|
+
const newURL = (str = '') => {
|
|
235
|
+
try {
|
|
236
|
+
return str = new URL(str);
|
|
237
|
+
} catch (e) { return false; }
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 解析连接URL为结构化配置对象
|
|
242
|
+
* >查看定义:@see {@link parseConnectionUrl}
|
|
243
|
+
* @param {string} str - 连接URL字符串
|
|
244
|
+
* @return {object} 配置对象
|
|
245
|
+
*/
|
|
246
|
+
const parseConnectionUrl = str => {
|
|
247
|
+
const url = newURL(str);
|
|
248
|
+
if (!url) throw new Error(`无效的连接URL:${str}`);
|
|
249
|
+
const protocols = { 'smtp:': { secure: false }, 'smtps:': { secure: true }, 'direct:': { direct: true } }, // 协议映射
|
|
250
|
+
// 值转换
|
|
251
|
+
parseValue = val => {
|
|
252
|
+
if (!isNaN(val)) return Number(val);
|
|
253
|
+
if (val === 'true') return true;
|
|
254
|
+
if (val === 'false') return false;
|
|
255
|
+
return val;
|
|
256
|
+
},
|
|
257
|
+
options = {
|
|
258
|
+
...protocols[url.protocol], ...(url.port && { port: Number(url.port) }), ...(url.hostname && { host: url.hostname }),
|
|
259
|
+
...((url.username || url.password) && {
|
|
260
|
+
auth: { user: decodeURIComponent(url.username), pass: url.password ? decodeURIComponent(url.password) : '' }
|
|
261
|
+
})
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// 处理查询参数
|
|
265
|
+
for (const [key, value] of url.searchParams) {
|
|
266
|
+
const parsed = parseValue(value);
|
|
267
|
+
|
|
268
|
+
if (key.startsWith('tls.')) {
|
|
269
|
+
const tlsKey = key.slice(4);
|
|
270
|
+
options.tls = { ...options.tls, [tlsKey]: parsed };
|
|
271
|
+
}
|
|
272
|
+
else if (!key.includes('.')) options[key] = parsed; // 处理其他参数(跳过.*)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return options;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 解析Data URI字符串
|
|
280
|
+
* >查看定义:@see {@link parseDataURI}
|
|
281
|
+
* @param {string} uri - Data URI字符串
|
|
282
|
+
* @returns {object|null} 解析后的数据对象或null
|
|
283
|
+
*/
|
|
284
|
+
const parseDataURI = uri => {
|
|
285
|
+
if (typeof uri !== 'string' || !uri.startsWith('data:')) return null; // 检查参数是否为字符串且以data:开头
|
|
286
|
+
|
|
287
|
+
const commaIndex = uri.indexOf(','); // 安全地查找第一个逗号的索引 -防止ReDoS攻击
|
|
288
|
+
if (commaIndex === -1) return null; // 如果没有找到逗号,返回
|
|
289
|
+
|
|
290
|
+
const data = uri.slice(commaIndex + 1), metaStr = uri.slice(5, commaIndex), metaEntries = metaStr.split(';'),
|
|
291
|
+
// 提取编码类型(最后一个非键值对参数)
|
|
292
|
+
lastEntry = metaEntries.at(-1)?.toLowerCase().trim(), contentType = metaEntries.shift() || 'application/octet-stream',
|
|
293
|
+
encoding = ['base64', 'utf8', 'utf-8'].includes(lastEntry) && !lastEntry.includes('=') ? metaEntries.pop() : null,
|
|
294
|
+
// 解析参数为键值对
|
|
295
|
+
params = Object.fromEntries(
|
|
296
|
+
metaEntries.map(entry => {
|
|
297
|
+
const [key, ...values] = entry.split('=');
|
|
298
|
+
return key?.trim() && values.length ? [key.trim(), values.join('=').trim()] : null;
|
|
299
|
+
}).filter(Boolean)
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// 解码数据,带有错误处理
|
|
303
|
+
let bufferData;
|
|
304
|
+
try {
|
|
305
|
+
bufferData = encoding === 'base64' ? Buffer.from(data, 'base64') : Buffer.from(decodeURIComponent(data));
|
|
306
|
+
} catch {
|
|
307
|
+
try {
|
|
308
|
+
bufferData = Buffer.from(data); // 使用原始数据创建Buffer
|
|
309
|
+
} catch {
|
|
310
|
+
bufferData = Buffer.alloc(0); // 创建一个空Buffer
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return { data: bufferData, encoding, contentType, params };
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 创建Promise回调包装器
|
|
319
|
+
* >查看定义:@see {@link callbackPromise}
|
|
320
|
+
* @param {function} resolve - Promise resolve函数
|
|
321
|
+
* @param {function} reject - Promise reject函数
|
|
322
|
+
* @returns {function} 回调函数
|
|
323
|
+
*/
|
|
324
|
+
const callbackPromise = (resolve, reject) => {
|
|
325
|
+
return (err, ...args) => err ? reject(err) : resolve(...args);// 错误时reject,否则成功resolve
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* 将流数据读取到Buffer中
|
|
330
|
+
* >查看定义:@see {@link resolveStream}
|
|
331
|
+
* @param {object} stream - 可读流
|
|
332
|
+
* @param {function} callback - 回调函数
|
|
333
|
+
*/
|
|
334
|
+
const resolveStream = (stream, callback) => {
|
|
335
|
+
const chunks = [], chunklen = { value: 0 }, state = { returned: false };
|
|
336
|
+
|
|
337
|
+
stream.on('readable', () => _handleReadableStream(stream, chunks, chunklen))
|
|
338
|
+
.on('error', err => {
|
|
339
|
+
cleanup(state), stream.emit('error', err);
|
|
340
|
+
return callback(err);
|
|
341
|
+
})
|
|
342
|
+
.on('end', () => {
|
|
343
|
+
cleanup(state);
|
|
344
|
+
let value;
|
|
345
|
+
try {
|
|
346
|
+
value = Buffer.concat(chunks, chunklen.value);
|
|
347
|
+
} catch (E) { return callback(E); }
|
|
348
|
+
|
|
349
|
+
return callback(null, value);
|
|
350
|
+
});
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* 解析字符串或Buffer值的内容值
|
|
355
|
+
* >查看定义:@see {@link resolveContent}
|
|
356
|
+
* @param {object} data - 包含内容的对象或数组
|
|
357
|
+
* @param {string|number} key - 属性名或数组索引
|
|
358
|
+
* @param {function} callback - 回调函数
|
|
359
|
+
* @returns {Promise|null} 可能返回Promise
|
|
360
|
+
*/
|
|
361
|
+
const resolveContent = (data, key, callback) => {
|
|
362
|
+
// 如果没有提供回调,创建Promise
|
|
363
|
+
const promise = !callback ? new Promise((resolve, reject) => callback = callbackPromise(resolve, reject)) : null;
|
|
364
|
+
|
|
365
|
+
let content = data?.[key]?.content ?? data?.[key];
|
|
366
|
+
if (!content) {
|
|
367
|
+
setImmediate(() => callback(null, content));
|
|
368
|
+
return promise;
|
|
369
|
+
}
|
|
370
|
+
const rEF = resetRegex(regexs.ENCODING_FORMAT),
|
|
371
|
+
encoding = ((data[key]?.encoding || 'utf8')).toString().toLowerCase().replace(rEF, '');
|
|
372
|
+
|
|
373
|
+
// 处理不同类型的content
|
|
374
|
+
if (typeof content === 'object') {
|
|
375
|
+
const { path, href, pipe } = content, source = path || href;
|
|
376
|
+
// 如果content是流,则解析
|
|
377
|
+
if (typeof pipe === 'function')
|
|
378
|
+
return resolveStream(content, (err, value) => {
|
|
379
|
+
if (err) return callback(err);
|
|
380
|
+
data[key]?.content ? data[key].content = value : data[key] = value; // 不能两次流式传输相同内容,所以需要替换流对象
|
|
381
|
+
callback(null, value);
|
|
382
|
+
});
|
|
383
|
+
// 否则,如果content是URL,则解析URL
|
|
384
|
+
else if (regexs.HTTP_URL.test(source)) return resolveStream(nmfetch(source), callback);
|
|
385
|
+
// 否则,如果content是Data URI,则解析Data URI
|
|
386
|
+
else if (regexs.DATA_URL.test(source)) return callback(null, parseDataURI(source)?.data || Buffer.alloc(0));
|
|
387
|
+
// 处理文件路径
|
|
388
|
+
else if (path) return resolveStream(fsReadStream(path), callback);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// 处理特定编码的字符串
|
|
392
|
+
const keyC = data[key]?.content;
|
|
393
|
+
if (typeof keyC === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) content = Buffer.from(keyC, encoding);
|
|
394
|
+
setImmediate(() => callback(null, content)); // 默认操作,原样返回内容
|
|
395
|
+
return promise;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* 将属性从源对象复制到目标对象(支持嵌套的tls和auth属性)
|
|
400
|
+
* >查看定义:@see {@link assign}
|
|
401
|
+
* @returns {object} 目标对象
|
|
402
|
+
*/
|
|
403
|
+
const assign = (target, ...sources) => {
|
|
404
|
+
target = target && typeof target === 'object' ? target : {}; // 确保 target 是对象,如果是假值则重置为空对象
|
|
405
|
+
|
|
406
|
+
for (const source of sources) {
|
|
407
|
+
if (!source) continue;
|
|
408
|
+
for (const [k, v] of Object.entries(source))
|
|
409
|
+
// 如果key是tls或auth,并且value是对象,则递归合并对象
|
|
410
|
+
target[k] = ['tls', 'auth'].includes(k) && v && typeof v === 'object' ? { ...target[k], ...v } : v;
|
|
411
|
+
}
|
|
412
|
+
return target;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* 对字符串进行XText编码(SMTP扩展)
|
|
417
|
+
* >查看定义:@see {@link encodeXText}
|
|
418
|
+
* @param {string} str - 要编码的字符串
|
|
419
|
+
* @returns {string} 编码后的字符串
|
|
420
|
+
*/
|
|
421
|
+
const encodeXText = str => {
|
|
422
|
+
if (!regexs.NEED_ENCODING.test(str)) return str;
|
|
423
|
+
// 将字符串转换为字节数组,对需要编码的字符转换为"+十六进制"格式,否则保留原字符,最后拼接成字符串
|
|
424
|
+
return Array.from(Buffer.from(str)).map(c => c < 0x21 || c > 0x7E || c === 0x2B || c === 0x3D
|
|
425
|
+
? `+${c.toString(16).toUpperCase().padStart(2, '0')}` : String.fromCharCode(c)).join('');
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* 初始化SMTP传输对象的公共构造函数逻辑
|
|
430
|
+
* >查看定义:@see {@link initSmtpConstructor}
|
|
431
|
+
* @param {object} instance - SMTP传输对象实例
|
|
432
|
+
* @param {object} options - 配置选项
|
|
433
|
+
* @param {function} SmtpC - SMTP连接类
|
|
434
|
+
* @param {string} defaultComponent - 默认组件名称
|
|
435
|
+
*/
|
|
436
|
+
const initSmtpConstructor = (instance, options, SmtpC, defaultComponent) => {
|
|
437
|
+
if (typeof options === 'string') options = { url: options }; // 如果选项是字符串,则将其视为URL
|
|
438
|
+
const { getSocket, url, service: s } = options;
|
|
439
|
+
if (typeof getSocket === 'function') instance.getSocket = getSocket; // 如果提供了getSocket函数,则使用它
|
|
440
|
+
|
|
441
|
+
let urlData, service = s;
|
|
442
|
+
if (url) urlData = parseConnectionUrl(url), service = service || urlData.service; // 解析URL连接选项
|
|
443
|
+
|
|
444
|
+
// 合并选项:是否创建对象、常规选项、URL选项和知名服务选项
|
|
445
|
+
instance.options = assign(false, options, urlData, service && wellKnown(service));
|
|
446
|
+
const connection = new SmtpC(instance.options); // 创建临时连接对象用于获取版本信息
|
|
447
|
+
instance.version = `${PK.version}[client:${connection.version}]`;
|
|
448
|
+
|
|
449
|
+
// 创建日志记录器
|
|
450
|
+
instance.logger = getLogger(instance.options, { component: instance.options.component || defaultComponent });
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* 用于创建代理套接字的占位函数,此方法立即返回而不提供套接字
|
|
455
|
+
* >查看定义:@see {@link getSocket}
|
|
456
|
+
* @param {Object} options 连接选项
|
|
457
|
+
* @param {Function} callback 回调函数,用于处理套接字密钥
|
|
458
|
+
*/
|
|
459
|
+
const getSocket = (options, callback) => {
|
|
460
|
+
return setImmediate(() => callback(null, false)); // 立即返回空套接字
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* 用于清理状态对象
|
|
465
|
+
* >查看定义:@see {@link cleanup}
|
|
466
|
+
* @param {Object} state 状态对象
|
|
467
|
+
*/
|
|
468
|
+
const cleanup = state => {
|
|
469
|
+
if (state.returned) return;
|
|
470
|
+
state.returned = true;
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* 创建并配置 SMTP 连接
|
|
475
|
+
* >查看定义:@see {@link createSmtpConnection}
|
|
476
|
+
* @param {Object} transport 传输对象 (SmtpPool, PoolResource 或 SmtpTransport)
|
|
477
|
+
* @param {Object} socketOptions socket 选项
|
|
478
|
+
* @param {Function} SmtpC... SMTP 连接类
|
|
479
|
+
* @returns {Object} 包含处理后的选项和连接实例的对象
|
|
480
|
+
*/
|
|
481
|
+
const createSmtpConnection = (transport, socketOptions, SmtpC) => {
|
|
482
|
+
let newOptions = { ...transport.options };
|
|
483
|
+
|
|
484
|
+
// 处理代理socket情况
|
|
485
|
+
if (socketOptions?.connection) {
|
|
486
|
+
const { remoteAddress, remotePort } = socketOptions.connection, { host = '', port = '' } = newOptions;
|
|
487
|
+
transport.logger.info({ tnx: 'proxy', remoteAddress, remotePort, destHost: host, destPort: port, action: 'connected' },
|
|
488
|
+
'使用来自 %s:%s 到 %s:%s 的代理socket', remoteAddress, remotePort, host, port
|
|
489
|
+
);
|
|
490
|
+
newOptions = { ...assign(false, newOptions), ...socketOptions };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const connection = new SmtpC(newOptions); // 创建SMTP连接实例
|
|
494
|
+
return { options: newOptions, connection };
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* 处理连接错误和结束事件的通用逻辑
|
|
499
|
+
* >查看定义:@see {@link setupConnectionHandlers}
|
|
500
|
+
* @param {Object} state 状态对象
|
|
501
|
+
* @param {Object} connection 连接对象
|
|
502
|
+
* @param {Function} callback 回调函数
|
|
503
|
+
* @param {Object} transport 传输对象
|
|
504
|
+
* @returns {Object} 清理函数
|
|
505
|
+
*/
|
|
506
|
+
const setupConnectionHandlers = (state, connection, callback, transport) => {
|
|
507
|
+
const errorHandler = err => {
|
|
508
|
+
if (state.returned) return;
|
|
509
|
+
_callbackErr(state, connection, callback, err, transport);
|
|
510
|
+
},
|
|
511
|
+
endHandler = () => {
|
|
512
|
+
if (state.returned) return;
|
|
513
|
+
|
|
514
|
+
const timer = setTimeout(() => {
|
|
515
|
+
if (state.returned) return;
|
|
516
|
+
const err = new Error(connection?._socket?.upgrading ? 'TLS 升级失败' : '意外的套接字关闭');
|
|
517
|
+
if (connection?._socket?.upgrading) err.code = 'ETLS';
|
|
518
|
+
_callbackErr(state, connection, callback, err, transport);
|
|
519
|
+
}, 1000);
|
|
520
|
+
|
|
521
|
+
try { timer.unref(); } catch (E) { }
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
connection.once('error', errorHandler).once('end', endHandler);
|
|
525
|
+
return () => connection.removeListener('error', errorHandler).removeListener('end', endHandler); // 返回清理函数
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* 执行 SMTP 认证的通用逻辑
|
|
530
|
+
* >查看定义:@see {@link performSmtpAuthentication}
|
|
531
|
+
* @param {Object} state 状态对象
|
|
532
|
+
* @param {Object} connection 连接对象
|
|
533
|
+
* @param {Object} auth 认证信息
|
|
534
|
+
* @param {Object} options 连接选项
|
|
535
|
+
* @param {Function} callback 回调函数
|
|
536
|
+
* @param {Object} transport 传输对象
|
|
537
|
+
* @param {Function} onSuccess 认证成功后的回调函数
|
|
538
|
+
*/
|
|
539
|
+
const performSmtpAuthentication = (state, connection, auth, options, callback, transport, onSuccess) => {
|
|
540
|
+
if (state.returned) return;
|
|
541
|
+
|
|
542
|
+
const { allowsAuth } = connection, { forceAuth } = options;
|
|
543
|
+
// 如果需要且支持认证,则进行登录验证
|
|
544
|
+
if (auth && (allowsAuth || forceAuth)) {
|
|
545
|
+
connection.login(auth, err => {
|
|
546
|
+
auth && auth !== transport.auth && auth.oauth2?.removeAllListeners();
|
|
547
|
+
if (state.returned) return;
|
|
548
|
+
err ? _callbackErr(state, connection, callback, err, transport) : onSuccess();
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
// 否则,如果没有认证信息,则返回提供认证错误
|
|
552
|
+
else if (!auth && allowsAuth && forceAuth) {
|
|
553
|
+
const err = new Error('未提供认证信息');
|
|
554
|
+
err.code = 'NoAuth', _callbackErr(state, connection, callback, err, transport);
|
|
555
|
+
}
|
|
556
|
+
else onSuccess(); // 否则,直接执行成功回调
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* 创建认证配置的通用逻辑
|
|
561
|
+
* >查看定义:@see {@link createAuthConfig}
|
|
562
|
+
* @param {Object} authData 认证数据
|
|
563
|
+
* @param {Object} logger 日志记录器
|
|
564
|
+
* @param {Object} mailer 邮件器实例(可选)
|
|
565
|
+
* @returns {Object|string} 认证配置对象或空字符串
|
|
566
|
+
*/
|
|
567
|
+
const createAuthConfig = (authData, logger, mailer = null, OAUTH2) => {
|
|
568
|
+
if (!authData) return '';
|
|
569
|
+
|
|
570
|
+
let hasAuth = false;
|
|
571
|
+
const authConfig = {}, mergeAuthData = source => {
|
|
572
|
+
if (source?.constructor === Object && Object.keys(source).length > 0) Object.assign(authConfig, source), hasAuth = true;
|
|
573
|
+
};
|
|
574
|
+
mergeAuthData(authData);
|
|
575
|
+
|
|
576
|
+
if (!hasAuth) return '';
|
|
577
|
+
const { type: rawType, user = '', pass, options, method = '', authMethod = '' } = authConfig,
|
|
578
|
+
type = String(rawType || 'LOGIN').toUpperCase();
|
|
579
|
+
if (type === 'OAUTH2') {
|
|
580
|
+
if (!user) return '';
|
|
581
|
+
const oauth2 = new OAUTH2(authConfig, logger);
|
|
582
|
+
|
|
583
|
+
if (mailer) {
|
|
584
|
+
oauth2.provisionCallback = mailer.get('oauth2_provision_cb') || oauth2.provisionCallback;
|
|
585
|
+
oauth2.on('token', token => mailer.emit('token', token));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
oauth2.on('error', err => logger.error({ err, tnx: 'oauth2' }, 'OAuth2 错误: %s', err.message));
|
|
589
|
+
return { type: 'OAUTH2', user, oauth2, method: 'XOAUTH2' };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (user || pass)
|
|
593
|
+
return { type, user, credentials: { user, pass, options }, method: method.trim().toUpperCase() || authMethod };
|
|
594
|
+
|
|
595
|
+
return '';
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* 发送邮件的通用预处理逻辑
|
|
600
|
+
* >查看定义:@see {@link prepareMessageForSending}
|
|
601
|
+
* @param {Object} mail 邮件对象
|
|
602
|
+
* @param {Object} logger 日志记录器
|
|
603
|
+
* @param {String} connectionId 连接ID(可选)
|
|
604
|
+
* @param {Object} options 配置选项(可选)
|
|
605
|
+
* @param {Function} done 发送时执行的回调函数(可选)
|
|
606
|
+
* @returns {Object} 处理后的信封和消息信息
|
|
607
|
+
*/
|
|
608
|
+
const prepareMessageForSending = (mail, logger, connectionId = '', options = {}, done) => {
|
|
609
|
+
const { data, message, normalize } = mail, { envelope: En, dsn, _dkim, ses } = data, { _headers } = message;
|
|
610
|
+
message.keepBcc = true; // 保留BCC字段(适用于所有传输类型)
|
|
611
|
+
|
|
612
|
+
// 获取信封和消息ID
|
|
613
|
+
const envelope = En || message.getEnvelope(), messageId = message.messageId(), { from, to } = envelope,
|
|
614
|
+
rList = Array.from(to ?? []),
|
|
615
|
+
// 格式化收件人列表:仅显示前3个收件人,超过部分用省略号表示
|
|
616
|
+
recipients = rList.length > 3 ? [...rList.slice(0, 2), `...以及另外${rList.length - 2}个收件人`] : rList;
|
|
617
|
+
if (dsn) envelope.dsn = dsn; // 添加DSN信息(如果存在)
|
|
618
|
+
|
|
619
|
+
// 构建日志上下文
|
|
620
|
+
const logContext = { tnx: 'send', messageId, ...(connectionId && { cid: connectionId }) };
|
|
621
|
+
let logMessage, logParams, args, fromAddress = from; // 通用日志记录 - 适配不同传输类型的日志格式
|
|
622
|
+
if (options.transportType === 'json')
|
|
623
|
+
logMessage = '正在为 %s 构建JSON结构,收件人: <%s>', logParams = [messageId, recipients.join(', ')];
|
|
624
|
+
else if (options.transportType === 'sendmail') {
|
|
625
|
+
// 检查无效地址格式(以连字符开头)
|
|
626
|
+
const hasInvalidAddresses = [...(from || []), ...(to || [])].some(addr => addr.startsWith('-'));
|
|
627
|
+
if (hasInvalidAddresses) return done(new Error('无法发送邮件,信封地址无效;'));
|
|
628
|
+
// 构建sendmail参数
|
|
629
|
+
const { sArgs } = options;
|
|
630
|
+
args = sArgs ? ['-i', ...sArgs, ...to] : ['-i', ...(from ? ['-f', from] : []), ...to];
|
|
631
|
+
logMessage = '正在发送消息%s到<%s>', logParams = [messageId, recipients.join(', ')];
|
|
632
|
+
} else if (options.transportType === 'ses') {
|
|
633
|
+
// From 地址处理
|
|
634
|
+
try {
|
|
635
|
+
const fromHeader = _headers?.find(header => header.key.toLowerCase() === 'from');
|
|
636
|
+
if (fromHeader?.value) fromAddress = fromHeader.value;
|
|
637
|
+
} catch (err) {
|
|
638
|
+
logger.warn({ err, messageId }, '解析 From 头失败,使用默认发件人地址: %s', from);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
logMessage = '正在发送消息 %s 至 <%s>', logParams = [messageId, recipients.join(', ')];
|
|
642
|
+
} else if (options.transportType === 'stream') {
|
|
643
|
+
const lineBreakType = options.winbreak ? '<CR><LF>' : '<LF>';
|
|
644
|
+
logMessage = '正在发送消息 %s 到 <%s>,使用 %s 换行符', logParams = [messageId, recipients.join(', '), lineBreakType];
|
|
645
|
+
} else {
|
|
646
|
+
// SMTP 和其他传输类型的默认日志格式
|
|
647
|
+
logMessage = connectionId ? '正在使用资源 #%s 发送消息 %s 至 <%s>' : '正在发送消息 %s 给 <%s>';
|
|
648
|
+
logParams = connectionId ? [connectionId, messageId, recipients.join(', ')] : [messageId, recipients.join(', ')];
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
logger.info(logContext, logMessage, ...logParams);
|
|
652
|
+
return { normalize, envelope, messageId, from: fromAddress, to, _dkim, ses, args, readStream: message.createReadStream() };
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* 处理发送结果的通用逻辑
|
|
657
|
+
* >查看定义:@see {@link handleSendResult}
|
|
658
|
+
* @param {Object} result 发送结果
|
|
659
|
+
* @param {Function} callback 回调函数
|
|
660
|
+
* @param {Object} logger 日志记录器
|
|
661
|
+
*/
|
|
662
|
+
const handleSendResult = (result, callback, logger) => {
|
|
663
|
+
const { err, info, from, to, messageId } = result;
|
|
664
|
+
if (err) {
|
|
665
|
+
logger.error({ err, tnx: 'send', messageId }, '发送 %s 时出错: %s', messageId, err.message);
|
|
666
|
+
return callback(err);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// 构建完整的响应信息
|
|
670
|
+
const responseInfo = { ...info, envelope: { from, to }, messageId };
|
|
671
|
+
try {
|
|
672
|
+
callback(null, responseInfo);
|
|
673
|
+
} catch (E) {
|
|
674
|
+
logger.error({ err: E, tnx: 'callback', messageId }, '处理 %s 的回调时出错: %s', messageId, E.message);
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* 验证SMTP配置的通用函数
|
|
680
|
+
* >查看定义:@see {@link verifySmtp}
|
|
681
|
+
* @param {Object} transport 传输对象 (SmtpPool 或 SmtpTransport)
|
|
682
|
+
* @param {Function} callback 回调函数
|
|
683
|
+
* @param {Function} SmtpC... SMTP连接类
|
|
684
|
+
* @param {Object} auth 认证信息
|
|
685
|
+
*/
|
|
686
|
+
const verifySmtp = (transport, callback, SmtpC, auth = null) => {
|
|
687
|
+
// 如果没有提供回调,则创建Promise
|
|
688
|
+
const promise = !callback ? new Promise((resolve, reject) => callback = callbackPromise(resolve, reject)) : null;
|
|
689
|
+
|
|
690
|
+
getSocket(transport.options, (err, socketOp) => {
|
|
691
|
+
if (err) return callback(err);
|
|
692
|
+
const { options: newOp, connection } = createSmtpConnection(transport, socketOp, SmtpC), // 创建并配置SMTP连接
|
|
693
|
+
state = { returned: false }, // 标记是否返回对象
|
|
694
|
+
|
|
695
|
+
finalize = (state, connection, callback) => {
|
|
696
|
+
cleanup(state), connection.quit(); // 对于验证操作,发送 QUIT 命令优雅关闭
|
|
697
|
+
return callback(null, true);
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
// 处理连接错误和连接结束
|
|
701
|
+
connection.once('error', err => {
|
|
702
|
+
if (state.returned) return;
|
|
703
|
+
_callbackErr(state, connection, callback, err, transport);
|
|
704
|
+
}).once('end', () => {
|
|
705
|
+
if (state.returned) return;
|
|
706
|
+
_callbackErr(state, connection, callback, new Error('连接已关闭'), transport);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
// 连接建立后的处理(执行 SMTP 认证并发生才验证结果)
|
|
710
|
+
connection.connect(() => {
|
|
711
|
+
if (state.returned) return;
|
|
712
|
+
performSmtpAuthentication(state, connection, auth, newOp, callback, transport,
|
|
713
|
+
() => finalize(state, connection, callback));
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
return promise;
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
export {
|
|
721
|
+
PK, regexs, resetRegex, dnsLookup, dnsResolve, format, isIP, netConnect, fsReadStream, osHostname, nmfetch,
|
|
722
|
+
getLogger, resolveHostname, newURL, parseConnectionUrl, parseDataURI, callbackPromise, resolveStream, resolveContent,
|
|
723
|
+
assign, encodeXText, initSmtpConstructor, getSocket, cleanup, createSmtpConnection, setupConnectionHandlers,
|
|
724
|
+
performSmtpAuthentication, createAuthConfig, prepareMessageForSending, handleSendResult, verifySmtp
|
|
725
|
+
};
|