@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/regexs.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 正则表达式常量
|
|
3
|
+
* >查看定义:@see {@link regexs}
|
|
4
|
+
*/
|
|
5
|
+
const regexs = {
|
|
6
|
+
LINE_SEPARATOR: /\r?\n/, // 按行分割丢弃换行符
|
|
7
|
+
SPLIT_KEEP_NEWLINE: /(\r?\n)/, // 按行分割并保留行结束符
|
|
8
|
+
LINE_BREAK: /^[^\n\r]*(\r?\n|\r)/, // 匹配行结束符
|
|
9
|
+
INVALID_ADDRESS_CHARS: /[\r\n<>]/, // 匹配地址中的非法字符(回车、换行、<>)
|
|
10
|
+
WHITESPACE: /\s+/, // 匹配空白字符
|
|
11
|
+
FOLDED_HEADER_LINE: /^\s/, // 匹配折叠的头信息行(以空白字符开头)
|
|
12
|
+
RESPONSE_CODE: /^(\d+)(?:\s(\d+\.\d+\.\d+))?\s/, // 匹配SMTP响应代码和可选状态码
|
|
13
|
+
NEXT_WORD: /^[^\s]+(\s*)/, // 匹配下一个单词及其后的空白
|
|
14
|
+
ADDRESS_CLEAN: /^[^<]*<\s*/, // 地址令牌清理:清理地址令牌中的多余字符
|
|
15
|
+
EMAIL_LOOSE: /\s*\b[^@\s]+@[^\s]+\b\s*/, // 邮箱地址宽松匹配:匹配文本中可能包含特殊字符的邮箱地址
|
|
16
|
+
UNSAFE_PARAM_VALUE: /[\s'"\\;:/=(),<>@[\]?]|^-/, // 匹配参数值中的不安全字符或以连字符开头
|
|
17
|
+
|
|
18
|
+
TRAILING_WSP: /[ \t]+$/, // 匹配行尾空白字符(WSP)
|
|
19
|
+
OPTIONAL_CRLF: /\r?\n$/, // 匹配可选的CRLF行结束符
|
|
20
|
+
TRAILING_NEWLINE: /\n.*?$/, // 匹配行尾的换行符及其后的内容
|
|
21
|
+
LAST_LINE: /(^|\n)([^\n]*)$/, // 匹配最后一行(用于换行处理)
|
|
22
|
+
EMAIL_CHECK: /^[^@]+@[^@]+$/, // 检查电子邮件格式
|
|
23
|
+
TRAILING_SPACES: /(\s+)[^\s]*$/, // 匹配行尾的空白字符及其后的非空白字符
|
|
24
|
+
EMAIL_EXACT: /^[^@\s]+@[^@\s]+$/, // 邮箱地址精确匹配:匹配简单的邮箱格式(名称@域名)
|
|
25
|
+
PRINTABLE_ASCII: /^[\x20-\x7e]*$/, // 匹配可打印ASCII字符
|
|
26
|
+
CRAM_MD5_CHALLENGE: /^334\s+(.+)$/, // 匹配CRAM-MD5挑战字符串
|
|
27
|
+
TRAILING_LINE_BREAK: /(?:\r\n|\r|\n)$/, // 检测字符串是否以行结束符结尾
|
|
28
|
+
RFC2231_KEY: /(\*(\d+)|\*(\d+)\*|\*)$/, // 匹配RFC2231格式的参数键名(带星号和编号)
|
|
29
|
+
RALPHANUMERIC_UNDERSCORE_SPACE: /^[\w ]*$/, // 匹配仅包含字母数字、下划线和空格的字符串
|
|
30
|
+
CHARSET_LANGUAGE_VALUE: /^([^']*)'[^']*'(.*)$/, // 匹配字符集、语言和值的格式(RFC2231)
|
|
31
|
+
IPV4_ADDRESS: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, // 匹配IPv4地址格式
|
|
32
|
+
UNSAFE_HEADER_PARAM: /[\s"\\;:/=(),<>@[\]?]|^[-']|'$/, // 匹配头参数值中的不安全字符
|
|
33
|
+
TRAILING_SPACE_OR_PUNCTUATION: /[ \t.,!?][^ \t.,!?]*$/, // 匹配空格、制表符或标点符号以及其后的非空格、非标点符号字符
|
|
34
|
+
LAST_NON_ASCII_WORD: /([^\x00-\x7F][^\s]*)[^"\x00-\x7F]*$/,// 匹配最后一个包含非ASCII字符的单词
|
|
35
|
+
|
|
36
|
+
AUTH: /[ -]AUTH\b/i, // 匹配AUTH支持
|
|
37
|
+
DATA_URL: /^data:/i, // 检测Data URL格式
|
|
38
|
+
TEXT_TYPE: /^text\//i, // 匹配所有text/*内容类型
|
|
39
|
+
IMAGE_TYPE: /^image\//i, // 检测图像内容类型
|
|
40
|
+
HTTP_URL: /^https?:\/\//i, // 检测HTTP/HTTPS协议
|
|
41
|
+
ERROR_SUFFIX: /Error\]$/i, // 匹配以"Error]"结尾的字符串(不区分大小写)
|
|
42
|
+
MULTIPART: /^multipart\//i, // 匹配multipart/*内容类型
|
|
43
|
+
HTML_TYPE: /^text\/html\b/i, // 检测HTML内容类型
|
|
44
|
+
MESSAGE_TYPE: /^message\//i, // 检测消息内容类型
|
|
45
|
+
ENCODED_SEQUENCE: /^[=]([0-9A-F]{2})/i, // 匹配编码序列(等号后跟2个十六进制数字)
|
|
46
|
+
CONTENT_FEATURES: /^Content-Features$/i, // 匹配Content-Features头部
|
|
47
|
+
UTF8_ENCODING: /^(?:=[\da-f]{2}){1,4}$/i, // 匹配UTF-8编码序列(1-4个连续的编码字节)
|
|
48
|
+
INCOMPLETE_ENCODING: /[=][\da-f]{0,2}$/i, // 匹配不完整的编码序列(等号后跟0-2个十六进制数字)
|
|
49
|
+
INCOMPLETE_ENCODING_END: /[=][0-9A-F]?$/i, // 匹配字符串末尾的不完整编码序列
|
|
50
|
+
TRANSPORTER_PROTOCOL: /^(smtps?|direct):/i, // 匹配传输器协议(smtp://, smtps://, direct://)
|
|
51
|
+
INCOMPLETE_ENCODING_SHORT: /[=][\da-f]{0,1}$/i, // 匹配更短的不完整编码序列(等号后跟0-1个十六进制数字)
|
|
52
|
+
MULTIPART_OR_MESSAGE: /^(multipart|message)\//i, // 匹配multipart/*或message/*内容类型
|
|
53
|
+
HTTP_RESPONSE_STATUS_LINE: /^HTTP\/\d+\.\d+ (\d+)/i, // 匹配 HTTP 响应状态行
|
|
54
|
+
AUTH_MECHANISM: /[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)/i, // 匹配AUTH机制
|
|
55
|
+
IPV6_ADDRESS: /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i, // 匹配IPv6地址格式
|
|
56
|
+
|
|
57
|
+
NON_ASCII: /\P{ASCII}/u, // 匹配非ASCII字符(Unicode字符)
|
|
58
|
+
NON_BASIC_ASCII: /[^\x00-\x7F]/, // 检测非基本ASCII字符(编码>127)
|
|
59
|
+
PROTOCOL_CHECK: /^(https?|mailto|ftp):/, // 检查URL协议
|
|
60
|
+
LIST_ID_PROTOCOL_REMOVE: /^<[^:]+\/{,2}/, // 移除List-ID中的协议部分
|
|
61
|
+
UTF16_HIGH_SURROGATE: /[\uD800-\uDBFF]/, // 匹配UTF-16高代理项(代理对的第一部分)
|
|
62
|
+
EMOJI_OR_SURROGATE: /[\ud83c\ud83d\ud83e]/, // 匹配emoji或UTF-16代理对起始字符
|
|
63
|
+
PLAIN_TEXT: /[^\x09\x0a\x0d\x20-\x7e]/, // 匹配非可打印7位ASCII字符(允许\t\n\r和\x20-\x7e)
|
|
64
|
+
PARAM_PLAIN_TEXT: /[^\x09\x0a\x0d\x20-\x7e]|"/, // 匹配非可打印字符或双引号(参数校验用)
|
|
65
|
+
FIRST_NON_ASCII_WORD: /(?:^|\s)([^\s]*[^\x00-\x7F])/, // 匹配第一个包含非ASCII字符的单词
|
|
66
|
+
NEED_ENCODING: /[\x00-\x20\x2B\x3D\x7F]|[^\x21-\x7E]/, // 不可打印ASCII字符(包括非ASCII字符)
|
|
67
|
+
|
|
68
|
+
DSN: /[ -]DSN\b/im, // 匹配DSN扩展支持
|
|
69
|
+
STARTTLS: /[ -]STARTTLS\b/im, // 匹配STARTTLS扩展支持
|
|
70
|
+
SMTPUTF8: /[ -]SMTPUTF8\b/im, // 匹配SMTPUTF8扩展支持
|
|
71
|
+
_8BITMIME: /[ -]8BITMIME\b/im, // 匹配8BITMIME扩展支持
|
|
72
|
+
PIPELINING: /[ -]PIPELINING\b/im, // 匹配PIPELINING扩展支持
|
|
73
|
+
SIZE: /[ -]SIZE(?:[ \t]+(\d+))?/im, // 匹配SIZE扩展和大小值
|
|
74
|
+
|
|
75
|
+
DATA_RESPONSE: /^[23]/, // 匹配DATA命令的2xx或3xx响应码
|
|
76
|
+
AUTH_SUCCESS: /^235\s+/, // 匹配认证成功的235响应码
|
|
77
|
+
AUTH_RESPONSE: /^334[ -]/, // 匹配AUTH认证的334响应码
|
|
78
|
+
CONTINUATION_RESPONSE: /^\d+-/, // 匹配以数字开头后跟连字符
|
|
79
|
+
DIGIT_HYPHEN_PREFIX: /^\d+[ -]/, // 匹配数字开头后跟空格或连字符
|
|
80
|
+
RESPONSE_4XX_5XX: /^[45]\d{2}\b/, // 匹配4xx或5xx错误响应码
|
|
81
|
+
TEXT_PLAIN: /^text\/plain\b/, // 匹配text/plain内容类型
|
|
82
|
+
|
|
83
|
+
// 需重置的正则表达式
|
|
84
|
+
NON_DIGIT: /\D/g, // 匹配非数字字符
|
|
85
|
+
CID_CLEAN: /[<>]/g, // 清理Content-ID中的尖括号
|
|
86
|
+
BLANK_REGEX: /\s/g, // 匹配所有空白字符
|
|
87
|
+
URL_SAFE_BASE64: /\W/g, // 匹配非单词字符(非字母、数字、下划线)
|
|
88
|
+
MULTIPLE_WSP: /[ \t]+/g, // 匹配行内多个空白字符
|
|
89
|
+
LINEBREAKS: /\r?\n|\r/g, // 匹配各种换行符(CRLF、LF、CR)
|
|
90
|
+
QUOTED_PAIR: /([\\"])/g, // 匹配需要转义的字符(反斜杠和引号)
|
|
91
|
+
HYPHEN_REPLACE: /-(\w)/g, // 连字符替换为驼峰命名
|
|
92
|
+
ANGLE_BRACKET: /<[^>]*>/g, // 匹配尖括号内的内容
|
|
93
|
+
GLOBAL_WHITESPACE: /\s+/g, // 匹配多个连续空白字符为单个空格
|
|
94
|
+
ENCODING_FORMAT: /[-_\s]/g, // 格式化编码字符串
|
|
95
|
+
CLEAN_MESSAGE_ID: /[<>\s]/g, // 清理message-id头,移除尖括号和空白字符
|
|
96
|
+
GLOBAL_CRLF_OR_LF: /\r?\n/g, // 匹配CRLF或LF换行符
|
|
97
|
+
URL_CLEAN: /[\s<]+|[\s>]+/g, // 清理URL中的空白和尖括号
|
|
98
|
+
RFC2231_SPECIAL_CHARS: /[=?_\s]/g, // RFC2231编码中需要转义的特殊字符:等号、问号、下划线和空白字符
|
|
99
|
+
FORBIDDEN_CHARS: /[\x00-\x1F<>]+/g, // 匹配控制字符和尖括号(不允许的字符)
|
|
100
|
+
SEPARATORS: /[\x2E\u3002\uFF0E\uFF61]/g, // RFC 3490 分隔符
|
|
101
|
+
NON_ALPHANUMERIC_DOT_HYPHEN: /[^a-zA-Z0-9.-]/g, // 匹配非字母数字、点号和连字符的字符
|
|
102
|
+
SIGNATURE_FOLDING: /(^.{73}|.{75}(?!\r?\n|\r))/g, // 签名折行处理正则
|
|
103
|
+
NON_LATIN: /[\x00-\x08\x0B\x0C\x0E-\x1F\P{ASCII}]/gu, // 匹配非拉丁字符(控制字符和Unicode)
|
|
104
|
+
NON_SAFE_URI: /[^\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]+/g, // 匹配URI中的不安全字符范围
|
|
105
|
+
NON_SAFE_URI_CHAR: /[\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]/g, // 匹配单个URI不安全字符
|
|
106
|
+
LATIN: /[a-z]/gi, // 匹配拉丁字母
|
|
107
|
+
NON_SAFE_CHAR: /[^a-z0-9!*+\-/=]/gi, // 匹配非安全字符(用于QP编码)
|
|
108
|
+
COMPLETE_ENCODING: /[=][\da-f]{2}$/gi, // 匹配完整的编码序列(等号后跟2个十六进制数字)
|
|
109
|
+
DATA_URL_IMAGE: /(]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi, // 匹配HTML中的data URL图片
|
|
110
|
+
NORMALIZE_HEADER_KEY: /^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, // 头部键规范化规则
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 重置正则表达式的lastIndex属性
|
|
115
|
+
* >查看定义:@see {@link resetRegex}
|
|
116
|
+
* @param {RegExp} regex - 需要重置的正则表达式
|
|
117
|
+
* @returns {RegExp} 重置后的正则表达式
|
|
118
|
+
*/
|
|
119
|
+
const resetRegex = regex => {
|
|
120
|
+
regex.lastIndex = 0;
|
|
121
|
+
return regex;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { regexs, resetRegex };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { PK, getLogger, prepareMessageForSending, cleanup } from './shared.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 生成Sendmail传输对象
|
|
6
|
+
*
|
|
7
|
+
* 可用的选项如下:
|
|
8
|
+
* **path** sendmail二进制文件的可选路径
|
|
9
|
+
* **newline** 换行符类型,可以是'windows'或'unix'
|
|
10
|
+
* **args** sendmail二进制文件的参数数组
|
|
11
|
+
* >查看定义:@see {@link SendmailTransport}
|
|
12
|
+
* @constructor
|
|
13
|
+
* @param {Object} options Sendmail的可选配置参数
|
|
14
|
+
*/
|
|
15
|
+
class SendmailTransport {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this._spawn = spawn; // 使用spawn的引用,便于模拟测试
|
|
18
|
+
this.options = options, this.name = 'SendmailTransport', this.version = PK.version;
|
|
19
|
+
// 默认sendmail路径,参数数组(默认为false),Windows换行标志(默认为false)
|
|
20
|
+
this.path = 'sendmail', this.args = false, this.winbreak = false;
|
|
21
|
+
|
|
22
|
+
const { component = 'sendmail' } = this.options;
|
|
23
|
+
this.logger = getLogger(this.options, { component }); // 获取日志记录器
|
|
24
|
+
|
|
25
|
+
// 根据选项配置参数
|
|
26
|
+
if (options) {
|
|
27
|
+
if (typeof options === 'string') this.path = options; // 如果options是字符串,直接作为sendmail路径
|
|
28
|
+
// 否则,如果options是对象,检查path和args属性
|
|
29
|
+
else if (typeof options === 'object') {
|
|
30
|
+
const { path, args, newline = '' } = options;
|
|
31
|
+
if (path) this.path = path;
|
|
32
|
+
if (Array.isArray(args)) this.args = args;
|
|
33
|
+
// 检查是否使用Windows换行符
|
|
34
|
+
this.winbreak = ['win', 'windows', 'dos', '\r\n'].includes(newline.toString().toLowerCase());
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* <p>编译mailcomposer消息并将其转发给发送处理器</p>
|
|
41
|
+
*
|
|
42
|
+
* @param {Object} mail MailComposer对象
|
|
43
|
+
* @param {Function} done 发送完成时运行的回调函数
|
|
44
|
+
*/
|
|
45
|
+
send(mail, done) {
|
|
46
|
+
let sendmail;
|
|
47
|
+
const { args, envelope, messageId, readStream } // 邮件预处理
|
|
48
|
+
= prepareMessageForSending(mail, this.logger, '', { sArgs: this.args, transportType: 'sendmail' }),
|
|
49
|
+
state = { returned: false },
|
|
50
|
+
|
|
51
|
+
// 资源清理和回调处理 cleanup
|
|
52
|
+
cleanUp = (err, result) => {
|
|
53
|
+
cleanup(state);
|
|
54
|
+
// 确保终止未退出的进程
|
|
55
|
+
if (sendmail && !sendmail.killed && sendmail.exitCode === null) sendmail.kill('SIGINT');
|
|
56
|
+
done?.(err, result);
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// 增强错误处理
|
|
60
|
+
handleError = (err, tnx, context) => {
|
|
61
|
+
this.logger.error({ err, tnx, messageId }, context, messageId, err.message), cleanUp(err);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
sendmail = this._spawn(this.path, args); // 创建子进程(args:sendmail参数)
|
|
66
|
+
} catch (E) {
|
|
67
|
+
return handleError(E, 'spawn', '生成sendmail进程失败;%s');
|
|
68
|
+
}
|
|
69
|
+
if (!sendmail) return cleanUp(new Error('未找到sendmail或生成进程失败'));
|
|
70
|
+
|
|
71
|
+
// 事件处理
|
|
72
|
+
sendmail.stdin.on('error', err => handleError(err, 'stdin', '向sendmail标准输入写入数据失败;%s'));
|
|
73
|
+
sendmail.on('error', err => handleError(err, 'process', 'sendmail进程执行失败;%s'))
|
|
74
|
+
.once('exit', code => {
|
|
75
|
+
if (code === 0) {
|
|
76
|
+
this.logger.info({ tnx: 'send', messageId }, '消息%s已成功投递', messageId);
|
|
77
|
+
cleanUp(null, { envelope, messageId, response: '消息已排队等待投递' });
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const err = new Error(`${code === 127 ? '未找到Sendmail命令' : 'Sendmail异常退出'} (退出码 ${code})`);
|
|
81
|
+
handleError(err, 'exit', 'sendmail进程异常退出;%s');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
readStream.once('error', err => handleError(err, 'source', '创建邮件数据流失败;%s'));
|
|
86
|
+
const pipeline = readStream.pipe(sendmail.stdin); // 创建数据管道
|
|
87
|
+
pipeline.on('error', err => handleError(err, 'pipe', '数据管道传输失败;%s'));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { SendmailTransport };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { LeWindows } from './mime-node/le-windows.js';
|
|
3
|
+
import { PK, getLogger, callbackPromise, prepareMessageForSending, resolveStream } from './shared.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 生成用于 AWS SES 的传输对象
|
|
7
|
+
* >查看定义:@see {@link SesTransport}
|
|
8
|
+
* @constructor
|
|
9
|
+
* @param {Object} options 可选配置参数
|
|
10
|
+
*/
|
|
11
|
+
class SesTransport extends EventEmitter {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
super();
|
|
14
|
+
this.options = options;
|
|
15
|
+
const { SES, component = 'sesTransport' } = this.options;
|
|
16
|
+
this.ses = SES, this.name = 'SesTransport', this.version = PK.version;
|
|
17
|
+
this.logger = getLogger(this.options, { component }); // 初始化日志记录器
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 获取 AWS 区域
|
|
22
|
+
* @param {Function} cb 回调函数
|
|
23
|
+
*/
|
|
24
|
+
getRegion(cb) {
|
|
25
|
+
const regionFunc = this.ses.sesClient.config?.region;
|
|
26
|
+
// 如果是函数,则调用该函数并返回结果,否则返回结果
|
|
27
|
+
return typeof regionFunc === 'function' ? regionFunc().then(r => cb(null, r)).catch(cb) : cb(null, false);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 编译 mailcomposer 消息并将其转发到 SES
|
|
32
|
+
*
|
|
33
|
+
* @param {Object} mail MailComposer 对象
|
|
34
|
+
* @param {Function} callback 发送完成时运行的回调函数
|
|
35
|
+
*/
|
|
36
|
+
send(mail, callback) {
|
|
37
|
+
const statObject = { ts: Date.now(), pending: true }, // 创建统计对象
|
|
38
|
+
{ messageId, _dkim = {}, from, to, readStream, ses = {} }
|
|
39
|
+
= prepareMessageForSending(mail, this.logger, '', { transportType: 'ses' }), // 邮件预处理
|
|
40
|
+
|
|
41
|
+
// 错误处理
|
|
42
|
+
handleError = (err, errorMessage) => {
|
|
43
|
+
this.logger.error({ err, tnx: 'send', messageId }, errorMessage, messageId, err.message);
|
|
44
|
+
statObject.pending = false, callback(err);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 获取原始消息内容
|
|
49
|
+
* @param {Function} next 回调函数
|
|
50
|
+
*/
|
|
51
|
+
getRawMessage = next => {
|
|
52
|
+
// 在 DKIM 签名中不使用 Message-ID 和 Date; 将流数据读取到Buffer中
|
|
53
|
+
const sF = _dkim.skipFields, dMsgId = 'date:message-id', stream = readStream.pipe(new LeWindows()); // 创建消息流
|
|
54
|
+
_dkim.skipFields = (sF && typeof sF === 'string') ? `${sF}:${dMsgId}` : dMsgId, resolveStream(stream, next);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// 异步执行发送过程
|
|
58
|
+
setImmediate(() =>
|
|
59
|
+
getRawMessage((err, raw) => {
|
|
60
|
+
if (err) return handleError(err, '为 %s 创建消息失败。%s');
|
|
61
|
+
|
|
62
|
+
// 构建 SES 消息对象(添加自定义参数: ses)
|
|
63
|
+
const sesMessage = {
|
|
64
|
+
Content: { Raw: { Data: raw } }, FromEmailAddress: from, Destination: { ToAddresses: to }, ...ses
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// 获取区域并发送邮件
|
|
68
|
+
this.getRegion((err, region) => {
|
|
69
|
+
if (err || !region) region = 'us-east-1'; // 默认区域
|
|
70
|
+
const command = new this.ses.SendEmailCommand(sesMessage);
|
|
71
|
+
this.ses.sesClient.send(command).then(d => {
|
|
72
|
+
if (region === 'us-east-1') region = 'email';
|
|
73
|
+
statObject.pending = false; // 邮件发送完成
|
|
74
|
+
callback(null, {
|
|
75
|
+
envelope: { from, to },
|
|
76
|
+
messageId: `<${d.MessageId}${(!d.MessageId.includes('@') ? `@${region}.amazonses.com` : '')}>`,
|
|
77
|
+
response: d.MessageId, raw
|
|
78
|
+
});
|
|
79
|
+
}).catch(err => handleError(err, '发送 %s 时出错: %s'));
|
|
80
|
+
});
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 验证 SES 配置
|
|
87
|
+
*
|
|
88
|
+
* @param {Function} callback 回调函数
|
|
89
|
+
* @return {Promise} 如果没有提供回调函数,则返回 Promise
|
|
90
|
+
*/
|
|
91
|
+
verify(callback) {
|
|
92
|
+
const promise = !callback ? new Promise((resolve, reject) => callback = callbackPromise(resolve, reject)) : null,
|
|
93
|
+
// 忽略某些特定错误,仍然认为验证成功
|
|
94
|
+
cb = err => callback(
|
|
95
|
+
err && !['InvalidParameterValue', 'MessageRejected'].includes(err?.code || err?.Code || err?.name) ? err : null, true
|
|
96
|
+
),
|
|
97
|
+
|
|
98
|
+
iEmail = 'invalid@invalid',
|
|
99
|
+
// 创建测试用的无效消息
|
|
100
|
+
sesMessage = {
|
|
101
|
+
Content: { Raw: { Data: Buffer.from(`From: <${iEmail}>\r\nTo: <${iEmail}>\r\nSubject: Invalid\r\n\r\nInvalid`) } },
|
|
102
|
+
FromEmailAddress: iEmail, Destination: { ToAddresses: [iEmail] }
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
this.getRegion((err, region) => {
|
|
106
|
+
if (err || !region) region = 'us-east-1';
|
|
107
|
+
const command = new this.ses.SendEmailCommand(sesMessage);
|
|
108
|
+
this.ses.sesClient.send(command).then(data => cb(null, data), err => cb(err));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return promise;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { SesTransport };
|