@nocobase/plugin-workflow-mailer 2.2.16 → 2.2.18
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/dist/externalVersion.js +6 -6
- package/dist/node_modules/joi/dist/joi-browser.min.js +1 -1
- package/dist/node_modules/joi/lib/index.js +1 -1
- package/dist/node_modules/joi/lib/messages.js +35 -6
- package/dist/node_modules/joi/lib/types/keys.js +18 -1
- package/dist/node_modules/joi/lib/types/link.js +18 -2
- package/dist/node_modules/joi/lib/types/string.js +2 -2
- package/dist/node_modules/joi/package.json +1 -1
- package/dist/node_modules/nodemailer/.ncurc.js +2 -0
- package/dist/node_modules/nodemailer/.prettierignore +8 -0
- package/dist/node_modules/nodemailer/.prettierrc +12 -0
- package/dist/node_modules/nodemailer/.prettierrc.js +2 -0
- package/dist/node_modules/nodemailer/.release-please-config.json +9 -0
- package/dist/node_modules/nodemailer/eslint.config.js +88 -0
- package/dist/node_modules/nodemailer/lib/addressparser/index.js +314 -71
- package/dist/node_modules/nodemailer/lib/base64/index.js +9 -13
- package/dist/node_modules/nodemailer/lib/dkim/index.js +17 -18
- package/dist/node_modules/nodemailer/lib/dkim/message-parser.js +12 -13
- package/dist/node_modules/nodemailer/lib/dkim/relaxed-body.js +3 -3
- package/dist/node_modules/nodemailer/lib/dkim/sign.js +22 -18
- package/dist/node_modules/nodemailer/lib/errors.js +59 -0
- package/dist/node_modules/nodemailer/lib/fetch/cookies.js +14 -19
- package/dist/node_modules/nodemailer/lib/fetch/index.js +164 -27
- package/dist/node_modules/nodemailer/lib/json-transport/index.js +4 -4
- package/dist/node_modules/nodemailer/lib/mail-composer/index.js +229 -155
- package/dist/node_modules/nodemailer/lib/mailer/index.js +106 -54
- package/dist/node_modules/nodemailer/lib/mailer/mail-message.js +106 -68
- package/dist/node_modules/nodemailer/lib/mime-funcs/index.js +105 -59
- package/dist/node_modules/nodemailer/lib/mime-funcs/mime-types.js +21 -14
- package/dist/node_modules/nodemailer/lib/mime-node/index.js +389 -137
- package/dist/node_modules/nodemailer/lib/mime-node/last-newline.js +1 -1
- package/dist/node_modules/nodemailer/lib/mime-node/le-unix.js +4 -7
- package/dist/node_modules/nodemailer/lib/mime-node/le-windows.js +1 -4
- package/dist/node_modules/nodemailer/lib/nodemailer.js +1 -1
- package/dist/node_modules/nodemailer/lib/punycode/index.js +1 -1
- package/dist/node_modules/nodemailer/lib/qp/index.js +33 -22
- package/dist/node_modules/nodemailer/lib/sendmail-transport/index.js +54 -44
- package/dist/node_modules/nodemailer/lib/ses-transport/index.js +110 -193
- package/dist/node_modules/nodemailer/lib/shared/index.js +299 -240
- package/dist/node_modules/nodemailer/lib/shared/objects.js +42 -0
- package/dist/node_modules/nodemailer/lib/shared/url.js +151 -0
- package/dist/node_modules/nodemailer/lib/smtp-connection/data-stream.js +3 -6
- package/dist/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +50 -19
- package/dist/node_modules/nodemailer/lib/smtp-connection/index.js +415 -223
- package/dist/node_modules/nodemailer/lib/smtp-pool/index.js +27 -34
- package/dist/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +19 -16
- package/dist/node_modules/nodemailer/lib/smtp-transport/index.js +66 -53
- package/dist/node_modules/nodemailer/lib/stream-transport/index.js +16 -7
- package/dist/node_modules/nodemailer/lib/well-known/index.js +7 -7
- package/dist/node_modules/nodemailer/lib/well-known/services.json +328 -38
- package/dist/node_modules/nodemailer/lib/xoauth2/index.js +105 -33
- package/dist/node_modules/nodemailer/package.json +1 -1
- package/package.json +4 -4
|
@@ -1,52 +1,211 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Restores the quoting of a local part that was read out of a quoted string.
|
|
5
|
+
*
|
|
6
|
+
* RFC 5321 allows '@' inside a quoted local part, so handing '"user@evil.com"@good.com'
|
|
7
|
+
* on as the bare 'user@evil.com@good.com' leaves it to the consumer which '@' splits the
|
|
8
|
+
* domain off. Getting that wrong is a misrouting vector, so the quotes go back on. The
|
|
9
|
+
* same holds for the other specials: a ',' or a ';' that loses its quotes reads as a
|
|
10
|
+
* recipient separator once the consumer puts the address back into a header.
|
|
11
|
+
*
|
|
12
|
+
* This module has no dependencies so that it can ship on its own, which is why the two
|
|
13
|
+
* grammar tests below are spelled out here instead of shared with lib/mime-node. Keeping
|
|
14
|
+
* only what is ambiguous quoted is deliberate, mime-node applies the stricter RFC 5321
|
|
15
|
+
* dot-atom rule on top of this when it emits an address.
|
|
16
|
+
*
|
|
17
|
+
* @param {String} address Address with an unquoted local part
|
|
18
|
+
* @return {String} Address with the local part as a quoted-string
|
|
19
|
+
*/
|
|
20
|
+
function _quoteLocalPart(address) {
|
|
21
|
+
const lastAt = address.lastIndexOf('@');
|
|
22
|
+
if (lastAt < 0) {
|
|
23
|
+
// no domain to split off, nothing can be misrouted
|
|
24
|
+
return address;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const user = address.substr(0, lastAt);
|
|
28
|
+
if (/^[^\s"(),:;<>@[\\\]]+$/.test(user) || /^"(?:[^"\\]|\\[\s\S])*"$/.test(user)) {
|
|
29
|
+
// a local part that carries no special reads the same with or without the quotes,
|
|
30
|
+
// and one that is already a complete quoted-string needs nothing either
|
|
31
|
+
return address;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return '"' + user.replace(/["\\]/g, '\\$&') + '"@' + address.substr(lastAt + 1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Reached for every parsed address, so it is built once rather than per call.
|
|
39
|
+
*/
|
|
40
|
+
const HAS_WHITESPACE = /\s/;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* An addr-spec that carries its whitespace legally, inside a quoted local part. The
|
|
44
|
+
* optional tail is the malformed shape: a real mailbox with wreckage trailing it.
|
|
45
|
+
*/
|
|
46
|
+
const QUOTED_LOCAL_ADDR = /^("(?:[^"\\]|\\[\s\S])*"@\S+)(?:\s+([\s\S]+))?$/;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* One run holding a single '@' and no whitespace, the shape an addr-spec has to have.
|
|
50
|
+
*/
|
|
51
|
+
const ADDR_SPEC = /^[^@\s]+@[^@\s]+$/;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The looser reading applied once the strict one finds nothing, which tolerates the
|
|
55
|
+
* further '@' that a domain should not have but malformed headers carry anyway.
|
|
56
|
+
*/
|
|
57
|
+
const LOOSE_ADDR_SPEC = /^[^@\s]+@\S+$/;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Recovers the addr-spec from an angle-addr that came back holding unquoted whitespace.
|
|
61
|
+
*
|
|
62
|
+
* A malformed header can put more than a mailbox between the angle brackets, most often
|
|
63
|
+
* because the generator wrote the recipient twice: '<user@example.com user@example.com>'
|
|
64
|
+
* or '<example.com user@example.com>'. Whitespace is not addr-spec, so the whole run can
|
|
65
|
+
* never be a mailbox anyone could deliver to, and passing it on as the address loses the
|
|
66
|
+
* recipient that is sitting right there in the header.
|
|
67
|
+
*
|
|
68
|
+
* The run that still reads as an addr-spec is kept and whatever is left over becomes
|
|
69
|
+
* display text rather than being dropped. Candidates are read strictly first and then
|
|
70
|
+
* under the looser grammar, the same two tiers the unquoted-text branch below applies to
|
|
71
|
+
* the same problem, so that '<a@b@c.com junk>' and a bare 'a@b@c.com junk' agree on the
|
|
72
|
+
* recipient. When several runs qualify the first wins, which is what that branch's looser
|
|
73
|
+
* tier does within a token.
|
|
74
|
+
*
|
|
75
|
+
* A quoted local part is left alone: RFC 5321 allows whitespace inside it, so
|
|
76
|
+
* '<"user name"@example.com>' is well formed and means exactly what it says.
|
|
77
|
+
*
|
|
78
|
+
* @param {Object} data Collected address parts, mutated in place
|
|
79
|
+
*/
|
|
80
|
+
function _recoverAddrSpec(data) {
|
|
81
|
+
if (!HAS_WHITESPACE.test(data.address)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let address;
|
|
86
|
+
let rest;
|
|
87
|
+
|
|
88
|
+
const quoted = data.address.match(QUOTED_LOCAL_ADDR);
|
|
89
|
+
if (quoted) {
|
|
90
|
+
if (!quoted[2]) {
|
|
91
|
+
// the whitespace sits inside the quoted local part, this is a well formed mailbox
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// a real mailbox with wreckage trailing it, so peel the addr-spec off whole rather
|
|
96
|
+
// than splitting into the quotes
|
|
97
|
+
address = quoted[1];
|
|
98
|
+
rest = [quoted[2]];
|
|
99
|
+
} else {
|
|
100
|
+
if (data.address.indexOf('"') >= 0) {
|
|
101
|
+
// Splitting on whitespace loses track of where the quoted string starts and ends,
|
|
102
|
+
// and this module does not take addresses out of quoted strings: the run picked out
|
|
103
|
+
// of '<junk "user@evil.com b"@good.com>' would be an address from the domain the
|
|
104
|
+
// quotes were hiding. Every well formed shape was already handled above, so what is
|
|
105
|
+
// left is wreckage either way and the original is the honest answer
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const parts = data.address.split(/\s+/);
|
|
110
|
+
|
|
111
|
+
let addrIndex = parts.findIndex(part => ADDR_SPEC.test(part));
|
|
112
|
+
if (addrIndex < 0) {
|
|
113
|
+
addrIndex = parts.findIndex(part => LOOSE_ADDR_SPEC.test(part));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (addrIndex < 0) {
|
|
117
|
+
// nothing in there reads as an address, there is no better answer than the original
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
address = parts.splice(addrIndex, 1)[0];
|
|
122
|
+
rest = parts;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
data.address = address;
|
|
126
|
+
data.text = [data.text]
|
|
127
|
+
.concat(rest)
|
|
128
|
+
.filter(part => part)
|
|
129
|
+
.join(' ');
|
|
130
|
+
}
|
|
131
|
+
|
|
3
132
|
/**
|
|
4
133
|
* Converts tokens for a single address into an address object
|
|
5
134
|
*
|
|
6
135
|
* @param {Array} tokens Tokens object
|
|
136
|
+
* @param {Number} depth Current recursion depth for nested group protection
|
|
7
137
|
* @return {Object} Address object
|
|
8
138
|
*/
|
|
9
|
-
function _handleAddress(tokens) {
|
|
10
|
-
let token;
|
|
139
|
+
function _handleAddress(tokens, depth) {
|
|
11
140
|
let isGroup = false;
|
|
12
141
|
let state = 'text';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
let data = {
|
|
142
|
+
const addresses = [];
|
|
143
|
+
const data = {
|
|
16
144
|
address: [],
|
|
17
145
|
comment: [],
|
|
18
146
|
group: [],
|
|
19
|
-
text: []
|
|
147
|
+
text: [],
|
|
148
|
+
textWasQuoted: []
|
|
20
149
|
};
|
|
21
|
-
let
|
|
22
|
-
let len;
|
|
150
|
+
let insideQuotes = false;
|
|
23
151
|
|
|
24
152
|
// Filter out <addresses>, (comments) and regular text
|
|
25
|
-
for (i = 0, len = tokens.length; i < len; i++) {
|
|
26
|
-
token = tokens[i];
|
|
153
|
+
for (let i = 0, len = tokens.length; i < len; i++) {
|
|
154
|
+
const token = tokens[i];
|
|
155
|
+
const prevToken = i ? tokens[i - 1] : null;
|
|
27
156
|
if (token.type === 'operator') {
|
|
28
157
|
switch (token.value) {
|
|
29
158
|
case '<':
|
|
30
159
|
state = 'address';
|
|
160
|
+
insideQuotes = false;
|
|
31
161
|
break;
|
|
32
162
|
case '(':
|
|
33
163
|
state = 'comment';
|
|
164
|
+
insideQuotes = false;
|
|
34
165
|
break;
|
|
35
166
|
case ':':
|
|
36
167
|
state = 'group';
|
|
37
168
|
isGroup = true;
|
|
169
|
+
insideQuotes = false;
|
|
170
|
+
break;
|
|
171
|
+
case '"':
|
|
172
|
+
insideQuotes = !insideQuotes;
|
|
173
|
+
state = 'text';
|
|
38
174
|
break;
|
|
39
175
|
default:
|
|
40
176
|
state = 'text';
|
|
177
|
+
insideQuotes = false;
|
|
178
|
+
break;
|
|
41
179
|
}
|
|
42
180
|
} else if (token.value) {
|
|
43
181
|
if (state === 'address') {
|
|
44
|
-
//
|
|
45
|
-
// Apple Mail truncates everything between an unexpected < and an address
|
|
46
|
-
// and so will we
|
|
182
|
+
// Handle unquoted name that includes a "<".
|
|
183
|
+
// Apple Mail truncates everything between an unexpected < and an address.
|
|
47
184
|
token.value = token.value.replace(/^[^<]*<\s*/, '');
|
|
48
185
|
}
|
|
49
|
-
|
|
186
|
+
|
|
187
|
+
// A comment is folding whitespace. It may sit inside an addr-spec, on either side
|
|
188
|
+
// of the '@', but it cannot join two atoms into one: gluing across it would read
|
|
189
|
+
// 'user@example.com(x)evil.com' as the single domain 'example.comevil.com' and
|
|
190
|
+
// deliver to a domain the sender never named.
|
|
191
|
+
const parts = data[state];
|
|
192
|
+
const joins =
|
|
193
|
+
prevToken &&
|
|
194
|
+
prevToken.noBreak &&
|
|
195
|
+
parts.length &&
|
|
196
|
+
(prevToken.value !== ')' || parts[parts.length - 1].slice(-1) === '@' || token.value.charAt(0) === '@');
|
|
197
|
+
|
|
198
|
+
if (joins) {
|
|
199
|
+
data[state][data[state].length - 1] += token.value;
|
|
200
|
+
if (state === 'text' && insideQuotes) {
|
|
201
|
+
data.textWasQuoted[data.textWasQuoted.length - 1] = true;
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
data[state].push(token.value);
|
|
205
|
+
if (state === 'text') {
|
|
206
|
+
data.textWasQuoted.push(insideQuotes);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
50
209
|
}
|
|
51
210
|
}
|
|
52
211
|
|
|
@@ -59,74 +218,101 @@ function _handleAddress(tokens) {
|
|
|
59
218
|
if (isGroup) {
|
|
60
219
|
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
|
|
61
220
|
data.text = data.text.join(' ');
|
|
221
|
+
|
|
222
|
+
// Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting)
|
|
223
|
+
let groupMembers = [];
|
|
224
|
+
if (data.group.length) {
|
|
225
|
+
const parsedGroup = addressparser(data.group.join(','), { _depth: depth + 1 });
|
|
226
|
+
parsedGroup.forEach(member => {
|
|
227
|
+
if (member.group) {
|
|
228
|
+
groupMembers = groupMembers.concat(member.group);
|
|
229
|
+
} else {
|
|
230
|
+
groupMembers.push(member);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
62
235
|
addresses.push({
|
|
63
|
-
name: data.text ||
|
|
64
|
-
group:
|
|
236
|
+
name: data.text || '',
|
|
237
|
+
group: groupMembers
|
|
65
238
|
});
|
|
66
239
|
} else {
|
|
67
240
|
// If no address was found, try to detect one from regular text
|
|
68
241
|
if (!data.address.length && data.text.length) {
|
|
69
|
-
for (i = data.text.length - 1; i >= 0; i--) {
|
|
70
|
-
|
|
242
|
+
for (let i = data.text.length - 1; i >= 0; i--) {
|
|
243
|
+
// Security: Do not extract email addresses from quoted strings.
|
|
244
|
+
// RFC 5321 allows @ inside quoted local-parts like "user@domain"@example.com.
|
|
245
|
+
// Extracting emails from quoted text leads to misrouting vulnerabilities.
|
|
246
|
+
if (!data.textWasQuoted[i] && ADDR_SPEC.test(data.text[i])) {
|
|
71
247
|
data.address = data.text.splice(i, 1);
|
|
248
|
+
data.textWasQuoted.splice(i, 1);
|
|
72
249
|
break;
|
|
73
250
|
}
|
|
74
251
|
}
|
|
75
252
|
|
|
76
|
-
|
|
77
|
-
if (!data.address.length) {
|
|
78
|
-
data.address = [address.trim()];
|
|
79
|
-
return ' ';
|
|
80
|
-
} else {
|
|
81
|
-
return address;
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
// still no address
|
|
253
|
+
// Try a looser regex match if strict match found nothing
|
|
86
254
|
if (!data.address.length) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (data.
|
|
91
|
-
|
|
255
|
+
let extracted = false;
|
|
256
|
+
for (let i = data.text.length - 1; i >= 0; i--) {
|
|
257
|
+
// Security: Do not extract email addresses from quoted strings
|
|
258
|
+
if (!data.textWasQuoted[i]) {
|
|
259
|
+
data.text[i] = data.text[i]
|
|
260
|
+
.replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, match => {
|
|
261
|
+
if (!extracted) {
|
|
262
|
+
data.address = [match.trim()];
|
|
263
|
+
extracted = true;
|
|
264
|
+
return ' ';
|
|
265
|
+
}
|
|
266
|
+
return match;
|
|
267
|
+
})
|
|
268
|
+
.trim();
|
|
269
|
+
if (extracted) {
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
92
272
|
}
|
|
93
273
|
}
|
|
94
274
|
}
|
|
95
275
|
}
|
|
96
276
|
|
|
97
|
-
// If there's still
|
|
277
|
+
// If there's still no text but a comment exists, replace the two
|
|
98
278
|
if (!data.text.length && data.comment.length) {
|
|
99
279
|
data.text = data.comment;
|
|
100
280
|
data.comment = [];
|
|
101
281
|
}
|
|
102
282
|
|
|
103
|
-
// Keep only the first address
|
|
283
|
+
// Keep only the first address occurrence, push others to regular text
|
|
104
284
|
if (data.address.length > 1) {
|
|
105
285
|
data.text = data.text.concat(data.address.splice(1));
|
|
106
286
|
}
|
|
107
287
|
|
|
288
|
+
// An address is only taken from unquoted text, so anything left in the text at this
|
|
289
|
+
// point that still has to serve as the address carries its quoting in this flag
|
|
290
|
+
const addressFromQuotedText = !data.address.length && data.textWasQuoted.some(wasQuoted => wasQuoted);
|
|
291
|
+
|
|
108
292
|
// Join values with spaces
|
|
109
293
|
data.text = data.text.join(' ');
|
|
110
294
|
data.address = data.address.join(' ');
|
|
111
295
|
|
|
112
|
-
|
|
113
|
-
return [];
|
|
114
|
-
} else {
|
|
115
|
-
address = {
|
|
116
|
-
address: data.address || data.text || '',
|
|
117
|
-
name: data.text || data.address || ''
|
|
118
|
-
};
|
|
296
|
+
_recoverAddrSpec(data);
|
|
119
297
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
298
|
+
const address = {
|
|
299
|
+
address: data.address || data.text || '',
|
|
300
|
+
name: data.text || data.address || ''
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
if (address.address === address.name) {
|
|
304
|
+
if (/@/.test(address.address || '')) {
|
|
305
|
+
address.name = '';
|
|
306
|
+
} else {
|
|
307
|
+
address.address = '';
|
|
126
308
|
}
|
|
309
|
+
}
|
|
127
310
|
|
|
128
|
-
|
|
311
|
+
if (addressFromQuotedText && address.address) {
|
|
312
|
+
address.address = _quoteLocalPart(address.address);
|
|
129
313
|
}
|
|
314
|
+
|
|
315
|
+
addresses.push(address);
|
|
130
316
|
}
|
|
131
317
|
|
|
132
318
|
return addresses;
|
|
@@ -145,6 +331,7 @@ class Tokenizer {
|
|
|
145
331
|
this.operatorExpecting = '';
|
|
146
332
|
this.node = null;
|
|
147
333
|
this.escaped = false;
|
|
334
|
+
this.inDomainLiteral = false;
|
|
148
335
|
|
|
149
336
|
this.list = [];
|
|
150
337
|
/**
|
|
@@ -172,11 +359,12 @@ class Tokenizer {
|
|
|
172
359
|
* @return {Array} An array of operator|text tokens
|
|
173
360
|
*/
|
|
174
361
|
tokenize() {
|
|
175
|
-
|
|
176
|
-
|
|
362
|
+
const list = [];
|
|
363
|
+
|
|
177
364
|
for (let i = 0, len = this.str.length; i < len; i++) {
|
|
178
|
-
chr = this.str.charAt(i);
|
|
179
|
-
this.
|
|
365
|
+
const chr = this.str.charAt(i);
|
|
366
|
+
const nextChr = i < len - 1 ? this.str.charAt(i + 1) : null;
|
|
367
|
+
this.checkChar(chr, nextChr);
|
|
180
368
|
}
|
|
181
369
|
|
|
182
370
|
this.list.forEach(node => {
|
|
@@ -194,7 +382,22 @@ class Tokenizer {
|
|
|
194
382
|
*
|
|
195
383
|
* @param {String} chr Character from the address field
|
|
196
384
|
*/
|
|
197
|
-
checkChar(chr) {
|
|
385
|
+
checkChar(chr, nextChr) {
|
|
386
|
+
// Track RFC 5322 domain-literals ("[" *dtext "]"). Operator characters such
|
|
387
|
+
// as the ":" of an IPv6 address-literal (user@[IPv6:2001:db8::1]) are dtext
|
|
388
|
+
// and must not be treated as the group delimiter while inside the brackets.
|
|
389
|
+
// Quoted strings and comments are handled separately via operatorExpecting,
|
|
390
|
+
// so only enter this state when no operator is open. The list separators ","
|
|
391
|
+
// and ";" are the exception: they always end the literal (and split the
|
|
392
|
+
// address list) so that an unclosed "[" cannot swallow later recipients.
|
|
393
|
+
if (!this.escaped && !this.operatorExpecting) {
|
|
394
|
+
if (!this.inDomainLiteral && chr === '[') {
|
|
395
|
+
this.inDomainLiteral = true;
|
|
396
|
+
} else if (this.inDomainLiteral && (chr === ']' || chr === ',' || chr === ';')) {
|
|
397
|
+
this.inDomainLiteral = false;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
198
401
|
if (this.escaped) {
|
|
199
402
|
// ignore next condition blocks
|
|
200
403
|
} else if (chr === this.operatorExpecting) {
|
|
@@ -202,12 +405,18 @@ class Tokenizer {
|
|
|
202
405
|
type: 'operator',
|
|
203
406
|
value: chr
|
|
204
407
|
};
|
|
408
|
+
|
|
409
|
+
if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) {
|
|
410
|
+
this.node.noBreak = true;
|
|
411
|
+
}
|
|
412
|
+
|
|
205
413
|
this.list.push(this.node);
|
|
206
414
|
this.node = null;
|
|
207
415
|
this.operatorExpecting = '';
|
|
208
416
|
this.escaped = false;
|
|
417
|
+
|
|
209
418
|
return;
|
|
210
|
-
} else if (!this.operatorExpecting && chr in this.operators) {
|
|
419
|
+
} else if (!this.operatorExpecting && !this.inDomainLiteral && chr in this.operators) {
|
|
211
420
|
this.node = {
|
|
212
421
|
type: 'operator',
|
|
213
422
|
value: chr
|
|
@@ -245,6 +454,13 @@ class Tokenizer {
|
|
|
245
454
|
}
|
|
246
455
|
}
|
|
247
456
|
|
|
457
|
+
/**
|
|
458
|
+
* Maximum recursion depth for parsing nested groups.
|
|
459
|
+
* RFC 5322 doesn't allow nested groups, so this is a safeguard against
|
|
460
|
+
* malicious input that could cause stack overflow.
|
|
461
|
+
*/
|
|
462
|
+
const MAX_NESTED_GROUP_DEPTH = 50;
|
|
463
|
+
|
|
248
464
|
/**
|
|
249
465
|
* Parses structured e-mail addresses from an address field
|
|
250
466
|
*
|
|
@@ -257,15 +473,23 @@ class Tokenizer {
|
|
|
257
473
|
* [{name: 'Name', address: 'address@domain'}]
|
|
258
474
|
*
|
|
259
475
|
* @param {String} str Address field
|
|
476
|
+
* @param {Object} options Optional options object
|
|
477
|
+
* @param {Number} options._depth Internal recursion depth counter (do not set manually)
|
|
260
478
|
* @return {Array} An array of address objects
|
|
261
479
|
*/
|
|
262
480
|
function addressparser(str, options) {
|
|
263
481
|
options = options || {};
|
|
482
|
+
const depth = options._depth || 0;
|
|
483
|
+
|
|
484
|
+
// Prevent stack overflow from deeply nested groups (DoS protection)
|
|
485
|
+
if (depth > MAX_NESTED_GROUP_DEPTH) {
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
264
488
|
|
|
265
|
-
|
|
266
|
-
|
|
489
|
+
const tokenizer = new Tokenizer(str);
|
|
490
|
+
const tokens = tokenizer.tokenize();
|
|
267
491
|
|
|
268
|
-
|
|
492
|
+
const addresses = [];
|
|
269
493
|
let address = [];
|
|
270
494
|
let parsedAddresses = [];
|
|
271
495
|
|
|
@@ -284,30 +508,49 @@ function addressparser(str, options) {
|
|
|
284
508
|
addresses.push(address);
|
|
285
509
|
}
|
|
286
510
|
|
|
287
|
-
addresses.forEach(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
511
|
+
addresses.forEach(addr => {
|
|
512
|
+
const handled = _handleAddress(addr, depth);
|
|
513
|
+
// Appended in place. Rebuilding the accumulator with concat() would copy every
|
|
514
|
+
// entry collected so far on each address, making a flat list cost O(n^2).
|
|
515
|
+
for (let i = 0; i < handled.length; i++) {
|
|
516
|
+
parsedAddresses.push(handled[i]);
|
|
291
517
|
}
|
|
292
518
|
});
|
|
293
519
|
|
|
520
|
+
// Merge fragments produced when unquoted display names contain commas.
|
|
521
|
+
// "Joe Foo, PhD <joe@example.com>" is split on the comma into
|
|
522
|
+
// [{name:"Joe Foo", address:""}, {name:"PhD", address:"joe@example.com"}].
|
|
523
|
+
// Recombine: a name-only entry followed by an entry with both name and address.
|
|
524
|
+
// Walked back to front so that a run of fragments folds into one entry in a single
|
|
525
|
+
// pass. Splicing each fragment out of the list instead would cost O(n^2).
|
|
526
|
+
const mergedAddresses = [];
|
|
527
|
+
for (let i = parsedAddresses.length - 1; i >= 0; i--) {
|
|
528
|
+
const current = parsedAddresses[i];
|
|
529
|
+
const next = mergedAddresses.length ? mergedAddresses[mergedAddresses.length - 1] : null;
|
|
530
|
+
if (next && current.address === '' && current.name && !current.group && next.address && next.name) {
|
|
531
|
+
next.name = current.name + ', ' + next.name;
|
|
532
|
+
} else {
|
|
533
|
+
mergedAddresses.push(current);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
mergedAddresses.reverse();
|
|
537
|
+
parsedAddresses = mergedAddresses;
|
|
538
|
+
|
|
294
539
|
if (options.flatten) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
list.forEach(
|
|
298
|
-
if (
|
|
299
|
-
return walkAddressList(
|
|
300
|
-
} else {
|
|
301
|
-
addresses.push(address);
|
|
540
|
+
const flatAddresses = [];
|
|
541
|
+
const walkAddressList = list => {
|
|
542
|
+
list.forEach(entry => {
|
|
543
|
+
if (entry.group) {
|
|
544
|
+
return walkAddressList(entry.group);
|
|
302
545
|
}
|
|
546
|
+
flatAddresses.push(entry);
|
|
303
547
|
});
|
|
304
548
|
};
|
|
305
549
|
walkAddressList(parsedAddresses);
|
|
306
|
-
return
|
|
550
|
+
return flatAddresses;
|
|
307
551
|
}
|
|
308
552
|
|
|
309
553
|
return parsedAddresses;
|
|
310
554
|
}
|
|
311
555
|
|
|
312
|
-
// expose to the world
|
|
313
556
|
module.exports = addressparser;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const Transform = require('stream')
|
|
3
|
+
const { Transform } = require('stream');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Encodes a Buffer into a base64 encoded string
|
|
@@ -31,14 +31,12 @@ function wrap(str, lineLength) {
|
|
|
31
31
|
return str;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
const result = [];
|
|
35
35
|
let pos = 0;
|
|
36
|
-
|
|
36
|
+
const chunkLength = lineLength * 1024;
|
|
37
|
+
const wrapRegex = new RegExp('.{' + lineLength + '}', 'g');
|
|
37
38
|
while (pos < str.length) {
|
|
38
|
-
|
|
39
|
-
.substr(pos, chunkLength)
|
|
40
|
-
.replace(new RegExp('.{' + lineLength + '}', 'g'), '$&\r\n')
|
|
41
|
-
.trim();
|
|
39
|
+
const wrappedLines = str.substr(pos, chunkLength).replace(wrapRegex, '$&\r\n').trim();
|
|
42
40
|
result.push(wrappedLines);
|
|
43
41
|
pos += chunkLength;
|
|
44
42
|
}
|
|
@@ -56,7 +54,6 @@ function wrap(str, lineLength) {
|
|
|
56
54
|
class Encoder extends Transform {
|
|
57
55
|
constructor(options) {
|
|
58
56
|
super();
|
|
59
|
-
// init Transform
|
|
60
57
|
this.options = options || {};
|
|
61
58
|
|
|
62
59
|
if (this.options.lineLength !== false) {
|
|
@@ -99,15 +96,15 @@ class Encoder extends Transform {
|
|
|
99
96
|
b64 = wrap(b64, this.options.lineLength);
|
|
100
97
|
|
|
101
98
|
// remove last line as it is still most probably incomplete
|
|
102
|
-
|
|
99
|
+
const lastLF = b64.lastIndexOf('\n');
|
|
103
100
|
if (lastLF < 0) {
|
|
104
101
|
this._curLine = b64;
|
|
105
102
|
b64 = '';
|
|
106
103
|
} else if (lastLF === b64.length - 1) {
|
|
107
104
|
this._curLine = '';
|
|
108
105
|
} else {
|
|
109
|
-
this._curLine = b64.
|
|
110
|
-
b64 = b64.
|
|
106
|
+
this._curLine = b64.substring(lastLF + 1);
|
|
107
|
+
b64 = b64.substring(0, lastLF + 1);
|
|
111
108
|
}
|
|
112
109
|
}
|
|
113
110
|
|
|
@@ -127,14 +124,13 @@ class Encoder extends Transform {
|
|
|
127
124
|
if (this._curLine) {
|
|
128
125
|
this._curLine = wrap(this._curLine, this.options.lineLength);
|
|
129
126
|
this.outputBytes += this._curLine.length;
|
|
130
|
-
this.push(this._curLine, 'ascii');
|
|
127
|
+
this.push(Buffer.from(this._curLine, 'ascii'));
|
|
131
128
|
this._curLine = '';
|
|
132
129
|
}
|
|
133
130
|
done();
|
|
134
131
|
}
|
|
135
132
|
}
|
|
136
133
|
|
|
137
|
-
// expose to the world
|
|
138
134
|
module.exports = {
|
|
139
135
|
encode,
|
|
140
136
|
wrap,
|