@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/LICENSE +15 -0
  3. package/Mail.js +47 -0
  4. package/README.md +475 -0
  5. package/copy-files.js +36 -0
  6. package/index.d.ts +816 -0
  7. package/index.js +109 -0
  8. package/lib/addressparser.js +230 -0
  9. package/lib/base64.js +103 -0
  10. package/lib/dkim/index.js +211 -0
  11. package/lib/dkim/message-parser.js +114 -0
  12. package/lib/dkim/relaxed-body.js +133 -0
  13. package/lib/dkim/sign.js +121 -0
  14. package/lib/fetch/cookies.js +224 -0
  15. package/lib/fetch/index.js +233 -0
  16. package/lib/http-proxy-client.js +98 -0
  17. package/lib/json-transport.js +38 -0
  18. package/lib/mail-composer.js +316 -0
  19. package/lib/mailer/index.js +301 -0
  20. package/lib/mailer/mail-message.js +226 -0
  21. package/lib/mime-funcs/index.js +493 -0
  22. package/lib/mime-funcs/mime-types.js +2061 -0
  23. package/lib/mime-node/index.js +811 -0
  24. package/lib/mime-node/last-newline.js +44 -0
  25. package/lib/mime-node/le-unix.js +37 -0
  26. package/lib/mime-node/le-windows.js +46 -0
  27. package/lib/punycode.js +283 -0
  28. package/lib/qp.js +171 -0
  29. package/lib/regexs.js +124 -0
  30. package/lib/sendmail-transport.js +91 -0
  31. package/lib/ses-transport.js +115 -0
  32. package/lib/shared.js +725 -0
  33. package/lib/smtp-connection/data-stream.js +68 -0
  34. package/lib/smtp-connection/index.js +1062 -0
  35. package/lib/smtp-pool/index.js +294 -0
  36. package/lib/smtp-pool/pool-resource.js +93 -0
  37. package/lib/smtp-transport.js +81 -0
  38. package/lib/stream-transport.js +59 -0
  39. package/lib/well-known/index.js +54 -0
  40. package/lib/well-known/services.json +802 -0
  41. package/lib/xoauth2.js +249 -0
  42. package/package.json +78 -0
@@ -0,0 +1,233 @@
1
+ // 引入必要的Node.js内置模块
2
+ import http from 'http';
3
+ import https from 'https';
4
+ import { isIP } from 'net';
5
+ import { createUnzip } from 'zlib'; // 压缩解压模块
6
+ import PK from '../../package.json' with { type: 'json' };
7
+ import { PassThrough } from 'stream'; // 流处理模块
8
+ import { Cookies } from './cookies.js';
9
+
10
+ const MAX_REDIRECTS = 5, // 最大重定向次数常量
11
+
12
+ // 错误处理函数(fetchRes: 响应流)
13
+ _handleError = (fetchRes, error, url) => {
14
+ error.type = 'FETCH', error.sourceUrl = url, fetchRes.emit('error', error);
15
+ },
16
+
17
+ // 标记请求完成并返回错误
18
+ _markFinishedWithError = (finishedFlag, fetchRes, error, url) => {
19
+ if (finishedFlag.value) return;
20
+ finishedFlag.value = true, _handleError(fetchRes, error, url);
21
+ },
22
+
23
+ // 解析URL函数
24
+ _parseUrl = (url, fetchRes) => {
25
+ try {
26
+ return { success: true, result: new URL(url) };
27
+ } catch (error) {
28
+ _handleError(fetchRes, error, url);
29
+ return { success: false, error };
30
+ }
31
+ },
32
+
33
+ // 处理请求体函数
34
+ _processRequestBody = (options, url, fetchRes, finishedFlag) => {
35
+ let processedBody = null;
36
+ const { contentType, body, headers } = options;
37
+
38
+ // 设置内容类型(除非明确禁止)
39
+ if (contentType !== false) headers['Content-Type'] = contentType || 'application/x-www-form-urlencoded';
40
+
41
+ // 流式数据处理
42
+ if (typeof body?.pipe === 'function') {
43
+ headers['Transfer-Encoding'] = 'chunked';
44
+ processedBody = body;
45
+ processedBody.on('error', err => _markFinishedWithError(finishedFlag, fetchRes, err, url));
46
+ }
47
+ // 非流式数据处理
48
+ else {
49
+ try {
50
+ if (body instanceof Buffer) processedBody = body;
51
+ else if (typeof body === 'object')
52
+ // 对象转 URL 编码字符串
53
+ processedBody = Buffer.from(Object.entries(body)
54
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v.toString().trim())}`).join('&'));
55
+ else processedBody = Buffer.from(String(body).trim()); // 字符串处理
56
+
57
+ headers['Content-Length'] = processedBody.length; // 设置内容长度
58
+ } catch (error) {
59
+ _handleError(fetchRes, error, url);
60
+ return { success: false, error };
61
+ }
62
+ }
63
+
64
+ return { success: true, body: processedBody };
65
+ },
66
+
67
+ // 处理重定向函数
68
+ _handleRedirect = (res, url, options, fetchRes, finishedFlag, req) => {
69
+ if (![301, 302, 303, 307, 308].includes(res.statusCode) || !res.headers.location) return false;
70
+
71
+ options.redirects++;
72
+ // 如果超过最大重定向次数, 则返回错误
73
+ if (options.redirects > options.maxRedirects) {
74
+ const err = new Error('超过最大重定向次数');
75
+ _markFinishedWithError(finishedFlag, fetchRes, err, url), req.destroy();
76
+ return true;
77
+ }
78
+
79
+ // 准备重定向请求(GET方法,无请求体)
80
+ options.method = 'GET', options.body = false;
81
+
82
+ try {
83
+ const redirectUrl = new URL(res.headers.location, url).toString(); // 重定向URL为绝对URL
84
+ nmfetch(redirectUrl, options);
85
+ return true;
86
+ } catch (error) {
87
+ _markFinishedWithError(finishedFlag, fetchRes, error, url);
88
+ return true;
89
+ }
90
+ };
91
+
92
+ /**
93
+ * 执行 HTTP/HTTPS 请求,支持重定向、Cookie管理、请求体处理、响应解压等功能;
94
+ * >查看定义:@see {@link nmfetch}
95
+ * @param {string} url - 请求的目标 URL;
96
+ * @param {Object} [options={}] - 可选配置对象;
97
+ * @param {stream.Writable} [options.fetchRes] - 用于写入响应数据的可写流,默认创建一个 PassThrough 流;
98
+ * @param {Cookies|Array|string} [options.cookies] - Cookie 管理器实例、Cookie 字符串数组或单个 Cookie 字符串,
99
+ * 用于自动管理请求和响应的 Cookie;
100
+ * @param {number} [options.redirects=0] - 已发生的重定向次数(内部使用);
101
+ * @param {number} [options.maxRedirects] - 允许的最大重定向次数,默认为 MAX_REDIRECTS 常量;
102
+ * @param {string} [options.method=''] - HTTP 请求方法(如 'GET'、'POST' 等),不提供时若存在请求体则默认为 'POST',否则为 'GET';
103
+ * @param {Object} [options.headers] - 自定义请求头对象,键名会转为小写;
104
+ * @param {string} [options.userAgent] - 自定义 User-Agent,会覆盖默认值;
105
+ * @param {Object} [options.tls] - TLS/SSL 相关选项,直接传递给底层请求的 tls 配置;
106
+ * @param {number} [options.timeout] - 请求超时时间(毫秒),超时后触发错误并销毁请求;
107
+ * @param {*} [options.body] - 请求体内容,可以是字符串、Buffer 或可读流;若存在且 method 未指定,则方法自动设为 'POST';
108
+ * @param {boolean} [options.allowErrorResponse=false] - 是否允许状态码 ≥300 的响应被视为成功,若为 false 则触发错误;
109
+ *
110
+ * @returns {stream.Writable} 返回用于接收响应数据的可写流(通常为 PassThrough 实例),可通过监听 'finish' 或 'error' 事件获取结果;
111
+ */
112
+ const nmfetch = (url, options = {}) => {
113
+ const { fetchRes: fetchRes_, cookies, redirects = 0, maxRedirects, method = '', headers: headers_, userAgent, } = options,
114
+ // 设置默认响应流、Cookie管理器和重定向计数器
115
+ fetchRes = fetchRes_ || new PassThrough(), newCookies = cookies || new Cookies();
116
+ options.redirects = redirects, options.maxRedirects = isNaN(maxRedirects) ? MAX_REDIRECTS : maxRedirects;
117
+
118
+ // 处理传入的Cookie选项
119
+ if (newCookies) [].concat(newCookies).forEach(cookie => newCookies.set(cookie, url)), options.cookies = false;
120
+
121
+ // 解析URL
122
+ const parsedResult = _parseUrl(url, fetchRes);
123
+ if (!parsedResult.success) return fetchRes;
124
+ const { protocol, hostname: host, pathname, search = '', port, username, password } = parsedResult.result;
125
+
126
+ // 确定HTTP方法,默认为GET
127
+ let newMethod = method.toString().trim().toUpperCase() || 'GET';
128
+ const handler = protocol === 'https:' ? https : http,
129
+ // 设置默认请求头
130
+ headers = { 'accept-encoding': 'gzip,deflate', 'user-agent': '@flun/mailer/' + PK.version };
131
+
132
+ // 合并用户自定义头部
133
+ Object.keys(headers_ || {}).forEach(key => headers[key.toLowerCase().trim()] = headers_[key]);
134
+
135
+ if (userAgent) headers['user-agent'] = userAgent; // 设置自定义User-Agent(如果提供)
136
+ // 如果URL中包含认证信息,则设置Authorization头
137
+ if (username || password) {
138
+ const auth = `${username}:${password}`;
139
+ headers.Authorization = `Basic ${Buffer.from(auth).toString('base64')}`;
140
+ }
141
+
142
+ const cookie = newCookies.get(url);
143
+ if (cookie) headers.cookie = cookie; // 获取并设置Cookie(如果有)
144
+
145
+ // 处理请求体(使用对象包装finished标志,便于在函数间传递引用)
146
+ const finishedFlag = { value: false }, bodyResult = _processRequestBody(options, url, fetchRes, finishedFlag);
147
+ if (!bodyResult.success) return fetchRes;
148
+ newMethod = newMethod || 'POST'; // 默认 POST 方法
149
+ const { tls, timeout } = options;
150
+ let req;
151
+ // 配置请求选项
152
+ const reqOptions = {
153
+ method: newMethod, host, path: pathname + search, port: port ? port : protocol === 'https:' ? 443 : 80, headers,
154
+ rejectUnauthorized: false, agent: false // 不验证SSL证书,不使用连接池
155
+ };
156
+
157
+ if (tls) Object.assign(reqOptions, tls); // 合并TLS选项
158
+ // 处理HTTPS协议的SNI
159
+ if (protocol === 'https:' && host && !isIP(host) && !reqOptions.servername) reqOptions.servername = host;
160
+
161
+ // 创建请求对象
162
+ try {
163
+ req = handler.request(reqOptions);
164
+ } catch (error) {
165
+ setImmediate(() => _markFinishedWithError(finishedFlag, fetchRes, error, url));
166
+ return fetchRes;
167
+ }
168
+
169
+ // 设置超时处理
170
+ if (timeout) {
171
+ req.setTimeout(timeout, () => {
172
+ const err = new Error('Request Timeout');
173
+ _markFinishedWithError(finishedFlag, fetchRes, err, url), req.destroy();
174
+ });
175
+ }
176
+ // 处理请求错误和响应
177
+ req.on('error', err => _markFinishedWithError(finishedFlag, fetchRes, err, url))
178
+ .on('response', res => {
179
+ let inflate; // 解压流
180
+ if (finishedFlag.value) return;
181
+
182
+ // 根据内容编码创建解压流
183
+ switch (res.headers['content-encoding']) {
184
+ case 'gzip':
185
+ case 'deflate':
186
+ inflate = createUnzip();
187
+ break;
188
+ }
189
+
190
+ // 如果响应中的Set-Cookie头为真,则存储Cookie
191
+ const setCookie = res.headers['set-cookie'];
192
+ if (setCookie) [].concat(setCookie).forEach(cookie => newCookies.set(cookie, url));
193
+ if (_handleRedirect(res, url, options, fetchRes, finishedFlag, req)) return; // 处理重定向
194
+
195
+ fetchRes.statusCode = res.statusCode, fetchRes.headers = res.headers; // 设置响应状态码和头部
196
+ // 检查状态码是否允许
197
+ if (res.statusCode >= 300 && !options.allowErrorResponse) {
198
+ const err = new Error(`响应状态码无效:${res.statusCode}`);
199
+ return _markFinishedWithError(finishedFlag, fetchRes, err, url), req.destroy();
200
+ }
201
+
202
+ // 处理响应错误
203
+ res.on('error', err => {
204
+ _markFinishedWithError(finishedFlag, fetchRes, err, url), req.destroy();
205
+ });
206
+
207
+ // 将响应流pipe到输出流,支持解压
208
+ if (inflate) {
209
+ res.pipe(inflate).pipe(fetchRes);
210
+ inflate.on('error', err => {
211
+ _markFinishedWithError(finishedFlag, fetchRes, err, url), req.destroy();
212
+ });
213
+ }
214
+ else res.pipe(fetchRes);
215
+ });
216
+
217
+ // 发送请求体(如果有)
218
+ setImmediate(() => {
219
+ const body = bodyResult.body;
220
+ if (body) {
221
+ try {
222
+ return (typeof body.pipe === 'function') ? body.pipe(req) : req.write(body);
223
+ } catch (err) {
224
+ return _markFinishedWithError(finishedFlag, fetchRes, err, url);
225
+ }
226
+ }
227
+ req.end();
228
+ });
229
+
230
+ return fetchRes;
231
+ };
232
+
233
+ export { nmfetch };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * 最小化的 HTTP/S 代理客户端
3
+ */
4
+ import { connect as tlsConnect } from 'tls';
5
+ import { connect as netConnect } from 'net';
6
+ import { regexs } from './regexs.js';
7
+
8
+ /**
9
+ * 建立到目标端口的代理连接
10
+ * >查看定义:@see {@link httpProxyClient}
11
+ * - 使用示例:
12
+ * @example
13
+ * httpProxyClient("http://localhost:3128/", 80, "google.com", (err, socket)=>{
14
+ * socket.write("GET / HTTP/1.0\r\n\r\n");
15
+ * });
16
+ * @param {String} proxyUrl 代理配置,例如 "http://proxy.host:3128/"
17
+ * @param {Number} destinationPort 目标主机端口
18
+ * @param {String} destinationHost 目标主机名
19
+ * @param {Function} callback 连接建立后的回调函数,接收socket对象
20
+ */
21
+ const httpProxyClient = (proxyUrl, destinationPort, destinationHost, callback) => {
22
+ let proxy;
23
+ try {
24
+ proxy = new URL(proxyUrl);
25
+ } catch (err) {
26
+ return callback(new Error(`无效的代理URL:${err.message}`));
27
+ }
28
+
29
+ const { hostname, port, protocol, username = '', password = '' } = proxy, isHttps = protocol === 'https:',
30
+ options = { host: hostname, port: Number(port) || (isHttps ? 443 : 80), rejectUnauthorized: !isHttps }, // 配置连接选项
31
+ connect = isHttps ? (options, callback) => tlsConnect(options, callback)
32
+ : (options, callback) => netConnect(options, callback), // 根据代理协议类型选择连接方式
33
+ state = { finished: false },
34
+ tempSocketErr = err => {
35
+ if (state.finished) return;
36
+ state.finished = true;
37
+
38
+ try {
39
+ socket.destroy();
40
+ } catch (E) { }
41
+ callback(err);
42
+ },
43
+
44
+ // 超时错误处理
45
+ timeoutErr = () => {
46
+ let err = new Error('代理socket连接超时');
47
+ err.code = 'ETIMEDOUT', tempSocketErr(err);
48
+ },
49
+
50
+ // 建立到代理服务器的连接
51
+ socket = connect(options, () => {
52
+ if (state.finished) return;
53
+
54
+ // 构造CONNECT请求头(目标主机和端口,连接类型)
55
+ const reqHeaders = { Host: `${destinationHost}:${destinationPort}`, Connection: 'close' };
56
+ // 如果代理需要认证,添加认证头
57
+ if (username || password) {
58
+ const auth = `${username}:${password}`;
59
+ reqHeaders['Proxy-Authorization'] = `Basic ${Buffer.from(auth).toString('base64')}`;
60
+ }
61
+
62
+ // 发送CONNECT请求到代理服务器
63
+ socket.write(`CONNECT ${destinationHost}:${destinationPort} HTTP/1.1\r\n${Object.keys(reqHeaders)
64
+ .map(key => `${key}: ${reqHeaders[key]}`).join('\r\n')}\r\n\r\n`);
65
+
66
+ let headerChunks = [];
67
+ // 处理代理服务器响应
68
+ const onSocketData = chunk => {
69
+ if (state.finished) return;
70
+
71
+ headerChunks.push(chunk); // 使用数组累积数据块
72
+ const headers = Buffer.concat(headerChunks).toString('latin1'), headersEndIndex = headers.indexOf('\r\n\r\n');
73
+ if (headersEndIndex !== -1) {
74
+ socket.removeListener('data', onSocketData);
75
+
76
+ const remainder = headers.substring(headersEndIndex + 4), headerPart = headers.substring(0, headersEndIndex),
77
+ statusMatch = headerPart.match(regexs.HTTP_RESPONSE_STATUS_LINE);
78
+
79
+ if (remainder) socket.unshift(Buffer.from(remainder, 'latin1'));
80
+ state.finished = true;
81
+ if (!statusMatch || (statusMatch[1]?.[0] ?? '') !== '2') {
82
+ try { socket.destroy(); } catch (E) { }
83
+ headerChunks = [];
84
+ return callback(new Error(`代理服务器返回无效响应${((statusMatch && `: ${statusMatch[1]}`) || '')}`));
85
+ }
86
+
87
+ socket.removeListener('error', tempSocketErr).removeListener('timeout', timeoutErr).setTimeout(0);
88
+ headerChunks = []; // 成功时重置 headerChunks
89
+ return callback(null, socket);
90
+ }
91
+ };
92
+ socket.on('data', onSocketData);
93
+ });
94
+
95
+ socket.setTimeout(httpProxyClient.timeout || 30 * 1000).on('timeout', timeoutErr).once('error', tempSocketErr);//监听连接错误
96
+ }
97
+
98
+ export { httpProxyClient };
@@ -0,0 +1,38 @@
1
+ import { PK, getLogger, prepareMessageForSending } from './shared.js';
2
+
3
+ /**
4
+ * 生成用于输出JSON的传输对象
5
+ * >查看定义:@see {@link JsonTransport}
6
+ * @constructor
7
+ * @param {Object} options 可选配置参数
8
+ */
9
+ class JsonTransport {
10
+ constructor(options = {}) {
11
+ this.options = options, this.name = 'JsonTransport', this.version = PK.version; // 设置传输对象名称和版本
12
+ this.logger = getLogger(this.options, { component: this.options.component || 'jsonTransport' }); // 初始化日志记录器
13
+ }
14
+
15
+ /**
16
+ * <p>编译mailcomposer消息并将其转发给发送处理器</p>
17
+ *
18
+ * @param {Object} mail MailComposer对象
19
+ * @param {Function} done 发送完成后执行的回调函数
20
+ */
21
+ send(mail, done) {
22
+ const { normalize, envelope, messageId }
23
+ = prepareMessageForSending(mail, this.logger, '', { transportType: 'json' }, done); // 邮件预处理
24
+
25
+ setImmediate(() => {
26
+ normalize((err, data) => {
27
+ if (err) {
28
+ this.logger.error({ err, tnx: 'send', messageId }, '为 %s 构建JSON结构失败: %s', messageId, err.message);
29
+ return done(err);
30
+ }
31
+ delete data.envelope, delete data.normalizedHeaders;
32
+ return done(null, { envelope, messageId, message: this.options.skipEncoding ? data : JSON.stringify(data) });
33
+ });
34
+ });
35
+ }
36
+ }
37
+
38
+ export { JsonTransport };