@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
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { MimeNode } from '../mime-node/index.js';
|
|
2
|
+
import { regexs, resetRegex, resolveContent } from '../shared.js';
|
|
3
|
+
import { detectExtension, detectMimeType, isPlainText, encodeWord } from '../mime-funcs/index.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 负责构建和处理邮件消息对象
|
|
7
|
+
* >查看定义:@see {@link MailMessage}
|
|
8
|
+
* @class MailMessage
|
|
9
|
+
* @description 该类封装了邮件的完整构建流程,包括邮件数据的初始化、内容解析、
|
|
10
|
+
* 头部规范化、地址处理、附件处理以及各类邮件头部的自动设置;
|
|
11
|
+
* 通常由 Mailer 实例内部创建和调用,不建议直接实例化;
|
|
12
|
+
*/
|
|
13
|
+
class MailMessage {
|
|
14
|
+
// 构造函数,初始化邮件消息对象
|
|
15
|
+
constructor(mailer, data = {}) {
|
|
16
|
+
this.mailer = mailer, this.message = null;
|
|
17
|
+
const { options = {}, _defaults = {} } = mailer;
|
|
18
|
+
this.data = this.#buildMailData(data, _defaults, options); // 初始化所有数据
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#buildMailData(data, defaults, options) {
|
|
22
|
+
const headers = { ...defaults.headers, ...data.headers }, mailData = { ...defaults, ...data, headers };
|
|
23
|
+
// 强制应用 transporter 选项中的特定键
|
|
24
|
+
['disableFileAccess', 'disableUrlAccess', 'normalizeHeaderKey'].forEach(key => {
|
|
25
|
+
if (key in options) mailData[key] = options[key]; // 如果在选项中设置,则使用选项值
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return mailData;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 解析内容方法(代理给 resolveContent)
|
|
32
|
+
resolveContent(...args) {
|
|
33
|
+
return resolveContent(...args);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 解析所有内容(文本、HTML、附件等)
|
|
37
|
+
resolveAll(callback) {
|
|
38
|
+
// 定义需要解析的内容键
|
|
39
|
+
const keys = [
|
|
40
|
+
[this.data, 'html'], [this.data, 'text'], [this.data, 'watchHtml'], [this.data, 'amp'], [this.data, 'icalEvent']
|
|
41
|
+
], { alternatives, attachments } = this.data;
|
|
42
|
+
alternatives?.forEach((_, i) => keys.push([alternatives, i])); // 如果有替代内容,则添加到键中
|
|
43
|
+
|
|
44
|
+
// 处理附件
|
|
45
|
+
attachments?.forEach((attachment, i) => {
|
|
46
|
+
const { filename, path, href = '', contentType } = attachment;
|
|
47
|
+
let newFname = filename;
|
|
48
|
+
// 如果没有文件名,则从路径或href中提取,或使用默认名称
|
|
49
|
+
if (!newFname) {
|
|
50
|
+
newFname = (path || href).split('/').pop().split('?').shift() || `attachment-${(i + 1)}`;
|
|
51
|
+
if (!newFname.includes('.')) newFname += `.${detectExtension(contentType)}`; // 如果没有扩展名,则根据内容类型添加
|
|
52
|
+
attachment.filename = newFname; // 设置回attachment对象
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 如果没有内容类型,则根据文件名、路径或href检测
|
|
56
|
+
if (!contentType) attachment.contentType = detectMimeType(newFname || path || href || 'bin');
|
|
57
|
+
keys.push([attachments, i]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// 创建 MimeNode 实例用于地址解析和处理地址字段
|
|
61
|
+
const mimeNode = new MimeNode(), addressKeys = ['from', 'to', 'cc', 'bcc', 'sender', 'replyTo'];
|
|
62
|
+
|
|
63
|
+
addressKeys.forEach(address => {
|
|
64
|
+
let value;
|
|
65
|
+
const rAddress = this.message.getHeader(address === 'replyTo' ? 'reply-to' : address);
|
|
66
|
+
// 如果已有消息,从消息头解析地址
|
|
67
|
+
if (this.message) value = [].concat(mimeNode.parseAddresses(rAddress) || []);
|
|
68
|
+
// 否则如果数据中有,则直接从数据中解析
|
|
69
|
+
else if (this.data[address]) value = [].concat(mimeNode.parseAddresses(this.data[address]) || []);
|
|
70
|
+
|
|
71
|
+
if (value?.length) this.data[address] = value; // 如果有值,则保存到数据中
|
|
72
|
+
else if (address in this.data) this.data[address] = null; // 否则如果数据中有地址字段,则删除地址字段
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// 处理单值地址字段(from, sender);如果数据中有地址,则保存为地址对象
|
|
76
|
+
['from', 'sender'].forEach(address => {
|
|
77
|
+
if (this.data[address]) this.data[address] = this.data[address].shift();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// 递归解析所有内容
|
|
81
|
+
let pos = 0;
|
|
82
|
+
const resolveNext = () => {
|
|
83
|
+
if (pos >= keys.length) return callback(null, this.data); // 所有内容解析完成
|
|
84
|
+
|
|
85
|
+
const args = keys[pos++], parent = args[0], key = args[1], currentItem = parent[key]; // 父对象, 属性名, 当前项
|
|
86
|
+
|
|
87
|
+
if (!parent || !currentItem) return resolveNext(); // 跳过空值
|
|
88
|
+
resolveContent(...args, (err, value) => {
|
|
89
|
+
if (err) return callback(err);
|
|
90
|
+
const node = { content: value }; // 创建内容节点,保留原始数据的其他属性
|
|
91
|
+
if (currentItem && typeof currentItem === 'object' && !Buffer.isBuffer(currentItem))
|
|
92
|
+
Object.keys(currentItem).forEach(key => {
|
|
93
|
+
// 如果没有在节点中,则从原始数据中复制属性
|
|
94
|
+
if (!(key in node) && !['content', 'path', 'href', 'raw'].includes(key)) node[key] = currentItem[key];
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
parent[key] = node, resolveNext(); // 更新数据为节点对象并继续解析下一个
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
setImmediate(() => resolveNext()); // 使用 setImmediate 异步开始解析
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 规范化邮件数据
|
|
105
|
+
normalize(callback) {
|
|
106
|
+
const envelope = this.data.envelope || this.message.getEnvelope(), messageId = this.message.messageId(); // 获取信封信息
|
|
107
|
+
// 首先解析所有内容
|
|
108
|
+
this.resolveAll((err, data) => {
|
|
109
|
+
if (err) return callback(err); // 如果解析失败,返回错误
|
|
110
|
+
|
|
111
|
+
data.envelope = envelope, data.messageId = messageId; // 设置信封和消息ID
|
|
112
|
+
['html', 'text', 'watchHtml', 'amp'].forEach(key => {
|
|
113
|
+
const content = data[key]?.content;
|
|
114
|
+
// 如果内容不是是字符串,则转换为字符串,否则直接使用,
|
|
115
|
+
if (content) data[key] = Buffer.isBuffer(content) ? content.toString() : content;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// 将Buffer转换为base64的通用函数
|
|
119
|
+
const convertToBase64 = (item) => {
|
|
120
|
+
const content = item?.content;
|
|
121
|
+
if (Buffer.isBuffer(content)) item.content = content.toString('base64'), item.encoding = 'base64';
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const { icalEvent, alternatives, attachments, headers = {}, list, references, inReplyTo } = data;
|
|
125
|
+
// 处理日历事件,替代内容,附件
|
|
126
|
+
if (icalEvent) convertToBase64(icalEvent);
|
|
127
|
+
alternatives?.forEach(convertToBase64), attachments?.forEach(convertToBase64);
|
|
128
|
+
|
|
129
|
+
// 规范化头部
|
|
130
|
+
const normalizedHeaders = {};
|
|
131
|
+
Object.keys(headers).forEach(key => {
|
|
132
|
+
let value = [].concat(headers[key] || []).shift();
|
|
133
|
+
value = value?.value ?? value;
|
|
134
|
+
if (value != null) {
|
|
135
|
+
// 对特定头部进行编码
|
|
136
|
+
if (['references', 'in-reply-to', 'message-id', 'content-id'].includes(key))
|
|
137
|
+
value = this.message.encodeHeaderValue(key, value);
|
|
138
|
+
normalizedHeaders[key] = value;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// 处理列表头部
|
|
143
|
+
if (list && typeof list === 'object') {
|
|
144
|
+
const listHeaders = this.#getListHeaders(list);
|
|
145
|
+
listHeaders.forEach(entry => {
|
|
146
|
+
normalizedHeaders[entry.key] = entry.value.map(val => val?.value ?? val).join(', ');
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 处理引用头部和回复头部
|
|
151
|
+
if (references) normalizedHeaders.references = this.message.encodeHeaderValue('references', references);
|
|
152
|
+
if (inReplyTo) normalizedHeaders['in-reply-to'] = this.message.encodeHeaderValue('in-reply-to', inReplyTo);
|
|
153
|
+
|
|
154
|
+
data.normalizedHeaders = normalizedHeaders;
|
|
155
|
+
return callback(null, data); // 返回规范化后的数据
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 设置邮件发送器头部
|
|
160
|
+
setMailerHeader() {
|
|
161
|
+
if (!this.message || !this.data.xMailer) return; // 如果没有消息或没有xMailer,则直接返回
|
|
162
|
+
this.message.setHeader('X-Mailer', this.data.xMailer);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 设置优先级头部
|
|
166
|
+
setPriorityHeaders() {
|
|
167
|
+
if (!this.message || !this.data.priority) return;
|
|
168
|
+
|
|
169
|
+
const configs = { high: ['1 (Highest)', 'High'], low: ['5 (Lowest)', 'Low'] },
|
|
170
|
+
priority = this.data.priority.toString().toLowerCase(), config = configs[priority];
|
|
171
|
+
if (config) {
|
|
172
|
+
this.message.setHeader('X-Priority', config[0]), this.message.setHeader('X-MSMail-Priority', config[1]);
|
|
173
|
+
this.message.setHeader('Importance', config[1].toLowerCase());
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 设置列表头部
|
|
178
|
+
setListHeaders() {
|
|
179
|
+
const list = this.data.list;
|
|
180
|
+
if (!this.message || !list || typeof list !== 'object') return; // 如果没有消息或没有列表头部,则直接返回
|
|
181
|
+
|
|
182
|
+
// 添加可选的 List-* 头部
|
|
183
|
+
if (list && typeof list === 'object')
|
|
184
|
+
this.#getListHeaders(list).forEach(listHeader => {
|
|
185
|
+
listHeader.value.forEach(value => this.message.addHeader(listHeader.key, value));
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 获取列表头部
|
|
190
|
+
#getListHeaders(listData) {
|
|
191
|
+
return Object.entries(listData).map(([key, values = []]) => [key.toLowerCase().trim(), values])
|
|
192
|
+
.map(([processedKey, values]) => ({
|
|
193
|
+
key: `list-${processedKey}`,
|
|
194
|
+
value: [{
|
|
195
|
+
prepared: true, foldLines: true,
|
|
196
|
+
value: [].concat(values)
|
|
197
|
+
.map(val => typeof val === 'string' ? { url: val } : val).filter(val => val?.url)
|
|
198
|
+
.map(val => {
|
|
199
|
+
const isId = processedKey === 'id';
|
|
200
|
+
let comment = val.comment;
|
|
201
|
+
comment = comment ? (!isPlainText(comment) ? encodeWord(comment) : comment) : '';
|
|
202
|
+
const formattedUrl = this.#formatListUrl(val.url); // 格式化URL
|
|
203
|
+
|
|
204
|
+
if (isId) {
|
|
205
|
+
const quotedComment = comment ? `"${comment}" ` : '',
|
|
206
|
+
url = formattedUrl.replace(regexs.LIST_ID_PROTOCOL_REMOVE, '');
|
|
207
|
+
return quotedComment + url; // List-ID: "comment" <domain>
|
|
208
|
+
}
|
|
209
|
+
// 返回格式化后的URL和注释(List-*: <http://domain> (comment))
|
|
210
|
+
return comment ? `${formattedUrl} (${comment})` : formattedUrl;
|
|
211
|
+
}).join(', ')
|
|
212
|
+
}]
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 格式化列表URL
|
|
217
|
+
#formatListUrl(url) {
|
|
218
|
+
const rUC = resetRegex(regexs.URL_CLEAN);
|
|
219
|
+
url = url.replace(rUC, ''); // 去除空白和尖括号
|
|
220
|
+
if (regexs.PROTOCOL_CHECK.test(url)) return `<${url}>`; // 如果包含协议,则使用尖括号包裹
|
|
221
|
+
// 如果包含@,则使用mailto协议,否则使用HTTP协议
|
|
222
|
+
return regexs.EMAIL_CHECK.test(url) ? `<mailto:${url}>` : `<http://${url}>`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export { MailMessage };
|