nodemailer 9.0.4 → 9.0.6
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 +23 -0
- package/README.md +2 -5
- package/lib/addressparser/index.js +140 -1
- package/lib/dkim/index.js +6 -1
- package/lib/dkim/sign.js +8 -3
- package/lib/fetch/index.js +115 -5
- package/lib/json-transport/index.js +1 -1
- package/lib/mail-composer/index.js +5 -3
- package/lib/mailer/mail-message.js +39 -34
- package/lib/mime-funcs/index.js +61 -13
- package/lib/mime-node/index.js +161 -41
- package/lib/punycode/index.js +1 -1
- package/lib/sendmail-transport/index.js +5 -2
- package/lib/ses-transport/index.js +4 -3
- package/lib/shared/index.js +27 -11
- package/lib/shared/objects.js +42 -0
- package/lib/stream-transport/index.js +1 -1
- package/package.json +6 -6
package/lib/mime-funcs/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const base64 = require('../base64');
|
|
6
6
|
const qp = require('../qp');
|
|
7
7
|
const mimeTypes = require('./mime-types');
|
|
8
|
+
const { isProtoKey } = require('../shared/objects');
|
|
8
9
|
|
|
9
10
|
module.exports = {
|
|
10
11
|
/**
|
|
@@ -13,7 +14,8 @@ module.exports = {
|
|
|
13
14
|
* When isParam is set the value is destined for a header parameter, so HT, CR and LF
|
|
14
15
|
* are not plaintext either: a header parameter has no way to carry them. HT is a valid
|
|
15
16
|
* fold point, so folding and unfolding a header would rewrite it as a space, and CR/LF
|
|
16
|
-
* cannot appear in a header value at all.
|
|
17
|
+
* cannot appear in a header value at all. DEL is neither a token character nor qtext,
|
|
18
|
+
* so it can not be carried bare or quoted. Such values have to go through the rfc2231
|
|
17
19
|
* parameter continuation encoding instead, the same way a quote already does.
|
|
18
20
|
*
|
|
19
21
|
* @param {String} value String to be tested
|
|
@@ -21,10 +23,21 @@ module.exports = {
|
|
|
21
23
|
* @returns {Boolean} true if it is a plaintext string
|
|
22
24
|
*/
|
|
23
25
|
isPlainText(value, isParam) {
|
|
24
|
-
const re = isParam ? /[\x00-\x1f"\u0080-\uFFFF]/ : /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/;
|
|
26
|
+
const re = isParam ? /[\x00-\x1f\x7f"\u0080-\uFFFF]/ : /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/;
|
|
25
27
|
return typeof value === 'string' && !re.test(value);
|
|
26
28
|
},
|
|
27
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Wraps a value into a quoted-string. Inside one a quote would end the string early
|
|
32
|
+
* and a backslash would escape whatever follows it, so both go out as quoted-pairs.
|
|
33
|
+
*
|
|
34
|
+
* @param {String} value String to be quoted
|
|
35
|
+
* @returns {String} The value as a quoted-string, quotes included
|
|
36
|
+
*/
|
|
37
|
+
quoteString(value) {
|
|
38
|
+
return '"' + (value || '').toString().replace(/["\\]/g, '\\$&') + '"';
|
|
39
|
+
},
|
|
40
|
+
|
|
28
41
|
/**
|
|
29
42
|
* Checks if a multi line string containes lines longer than the selected value.
|
|
30
43
|
*
|
|
@@ -177,10 +190,12 @@ module.exports = {
|
|
|
177
190
|
buildHeaderValue(structured) {
|
|
178
191
|
const paramsArray = [];
|
|
179
192
|
|
|
180
|
-
Object.keys(structured.params || {}).forEach(
|
|
193
|
+
Object.keys(structured.params || {}).forEach(key => {
|
|
181
194
|
// filename might include unicode characters so it is a special case
|
|
182
195
|
// other values probably do not
|
|
183
|
-
const value = structured.params[
|
|
196
|
+
const value = structured.params[key];
|
|
197
|
+
// a parameter name is a token too and it is emitted without any quoting around it
|
|
198
|
+
const param = key.replace(/[\x00-\x1f\x7f]/g, '');
|
|
184
199
|
if (!this.isPlainText(value, true) || value.length >= 75) {
|
|
185
200
|
this.buildHeaderParam(param, value, 50).forEach(encodedParam => {
|
|
186
201
|
if (!/[\s"\\;:/=(),<>@[\]?]|^[-']|'$/.test(encodedParam.value) || encodedParam.key.substr(-1) === '*') {
|
|
@@ -196,7 +211,11 @@ module.exports = {
|
|
|
196
211
|
}
|
|
197
212
|
});
|
|
198
213
|
|
|
199
|
-
|
|
214
|
+
// the value ahead of the parameters is a token, it has no way to carry a control
|
|
215
|
+
// char or DEL and there is no quoting construct around it to escape one into
|
|
216
|
+
const value = typeof structured.value === 'string' ? structured.value.replace(/[\x00-\x1f\x7f]/g, '') : structured.value;
|
|
217
|
+
|
|
218
|
+
return value + (paramsArray.length ? '; ' + paramsArray.join('; ') : '');
|
|
200
219
|
},
|
|
201
220
|
|
|
202
221
|
/**
|
|
@@ -294,8 +313,11 @@ module.exports = {
|
|
|
294
313
|
line,
|
|
295
314
|
encoded
|
|
296
315
|
});
|
|
316
|
+
// the line we start here holds an encoded char, so it has to be
|
|
317
|
+
// flagged as one. otherwise it gets an unstarred continuation key
|
|
318
|
+
// and a receiver reads the percent escapes as literal text
|
|
297
319
|
line = '';
|
|
298
|
-
|
|
320
|
+
encoded = true;
|
|
299
321
|
} else {
|
|
300
322
|
encoded = true;
|
|
301
323
|
i = startPos;
|
|
@@ -360,6 +382,16 @@ module.exports = {
|
|
|
360
382
|
value: false,
|
|
361
383
|
params: {}
|
|
362
384
|
};
|
|
385
|
+
|
|
386
|
+
// Parameter names come from a caller supplied contentType/contentDisposition. A
|
|
387
|
+
// "__proto__" name would target the prototype chain of the params object instead of
|
|
388
|
+
// an own property of it, and read back as Object.prototype, so it is dropped.
|
|
389
|
+
const setParam = (name, value) => {
|
|
390
|
+
if (!isProtoKey(name)) {
|
|
391
|
+
response.params[name] = value;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
363
395
|
let key = false;
|
|
364
396
|
let value = '';
|
|
365
397
|
let type = 'value';
|
|
@@ -391,7 +423,7 @@ module.exports = {
|
|
|
391
423
|
if (key === false) {
|
|
392
424
|
response.value = value.trim();
|
|
393
425
|
} else {
|
|
394
|
-
|
|
426
|
+
setParam(key, value.trim());
|
|
395
427
|
}
|
|
396
428
|
type = 'key';
|
|
397
429
|
value = '';
|
|
@@ -406,10 +438,10 @@ module.exports = {
|
|
|
406
438
|
if (key === false) {
|
|
407
439
|
response.value = value.trim();
|
|
408
440
|
} else {
|
|
409
|
-
|
|
441
|
+
setParam(key, value.trim());
|
|
410
442
|
}
|
|
411
443
|
} else if (value.trim()) {
|
|
412
|
-
|
|
444
|
+
setParam(value.trim().toLowerCase(), '');
|
|
413
445
|
}
|
|
414
446
|
|
|
415
447
|
// handle parameter value continuations
|
|
@@ -422,6 +454,14 @@ module.exports = {
|
|
|
422
454
|
actualKey = key.substr(0, match.index);
|
|
423
455
|
nr = Number(match[2] || match[3]) || 0;
|
|
424
456
|
|
|
457
|
+
if (isProtoKey(actualKey)) {
|
|
458
|
+
// see setParam. Reading it back would yield Object.prototype, which is
|
|
459
|
+
// an object, so the initializer below would be skipped and the write
|
|
460
|
+
// that follows would throw out of a header build the caller can not catch
|
|
461
|
+
delete response.params[key];
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
425
465
|
if (!response.params[actualKey] || typeof response.params[actualKey] !== 'object') {
|
|
426
466
|
response.params[actualKey] = {
|
|
427
467
|
charset: false,
|
|
@@ -548,7 +588,7 @@ module.exports = {
|
|
|
548
588
|
*/
|
|
549
589
|
splitMimeEncodedString: (str, maxlen) => {
|
|
550
590
|
const lines = [];
|
|
551
|
-
let curLine, match, chr, done;
|
|
591
|
+
let curLine, fallbackLine, match, chr, done;
|
|
552
592
|
|
|
553
593
|
// require at least 12 symbols to fit possible 4 octet UTF-8 sequences
|
|
554
594
|
maxlen = Math.max(maxlen || 0, 12);
|
|
@@ -561,8 +601,14 @@ module.exports = {
|
|
|
561
601
|
curLine = curLine.substr(0, match.index);
|
|
562
602
|
}
|
|
563
603
|
|
|
604
|
+
// Malformed input (a run of stray UTF-8 continuation bytes) has no split point
|
|
605
|
+
// that keeps a character sequence whole, so the loop below walks back to an
|
|
606
|
+
// empty line looking for one. Keep the widest chunk that at least does not cut
|
|
607
|
+
// a "=XX" escape in half, so the part stays a decodable encoded word.
|
|
608
|
+
fallbackLine = curLine.length ? curLine : str.substr(0, maxlen);
|
|
609
|
+
|
|
564
610
|
done = false;
|
|
565
|
-
while (!done) {
|
|
611
|
+
while (!done && curLine.length) {
|
|
566
612
|
done = true;
|
|
567
613
|
// check if not middle of a unicode char sequence
|
|
568
614
|
if ((match = str.substr(curLine.length).match(/^[=]([0-9A-F]{2})/i))) {
|
|
@@ -575,9 +621,11 @@ module.exports = {
|
|
|
575
621
|
}
|
|
576
622
|
}
|
|
577
623
|
|
|
578
|
-
if (curLine.length) {
|
|
579
|
-
|
|
624
|
+
if (!curLine.length) {
|
|
625
|
+
curLine = fallbackLine;
|
|
580
626
|
}
|
|
627
|
+
|
|
628
|
+
lines.push(curLine);
|
|
581
629
|
str = str.substr(curLine.length);
|
|
582
630
|
}
|
|
583
631
|
|
package/lib/mime-node/index.js
CHANGED
|
@@ -21,6 +21,20 @@ const LeUnix = require('./le-unix');
|
|
|
21
21
|
|
|
22
22
|
const FORMATTED_HEADERS = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
|
|
23
23
|
|
|
24
|
+
// RFC 5321 atext, plus the non-ascii bytes that SMTPUTF8 (RFC 6531) adds to it. A local part
|
|
25
|
+
// built from these, with '.' as a separator, is a dot-atom and can be emitted bare
|
|
26
|
+
const ATEXT = "[A-Za-z0-9!#$%&'*+\\-/=?^_`{|}~\\x80-\\uFFFF]";
|
|
27
|
+
const DOT_ATOM = new RegExp('^' + ATEXT + '+(?:\\.' + ATEXT + '+)*$');
|
|
28
|
+
|
|
29
|
+
// A complete quoted-string: everything between the outer quotes is either a plain char or
|
|
30
|
+
// a quoted-pair. Anchored, so a value that only starts and ends with a quote does not pass
|
|
31
|
+
const QUOTED_STRING = /^"(?:[^"\\]|\\[\s\S])*"$/;
|
|
32
|
+
|
|
33
|
+
// An address that carries no special anywhere can be emitted bare in a header, everything
|
|
34
|
+
// else goes into angle brackets so that the header can not be read as more addresses than
|
|
35
|
+
// the envelope carries
|
|
36
|
+
const PLAIN_ADDRESS = /^[^\s"(),:;<>@[\\\]]+@[^\s"(),:;<>@[\\\]]+$/;
|
|
37
|
+
|
|
24
38
|
/**
|
|
25
39
|
* Creates a new mime tree node. Assumes 'multipart/*' as the content type
|
|
26
40
|
* if it is a branch, anything else counts as leaf. If rootNode is missing from
|
|
@@ -519,11 +533,10 @@ class MimeNode {
|
|
|
519
533
|
const formattedHeaders = FORMATTED_HEADERS;
|
|
520
534
|
|
|
521
535
|
if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
});
|
|
536
|
+
// the keys come from a caller supplied header object and `options.prepared`
|
|
537
|
+
// below decides whether the value is emitted raw, so an own "__proto__" key
|
|
538
|
+
// here would turn an unfolded value into header injection
|
|
539
|
+
shared.copyOwnKeys(options, value, optionKey => optionKey === 'value');
|
|
527
540
|
value = (value.value || '').toString();
|
|
528
541
|
if (!value.trim()) {
|
|
529
542
|
return;
|
|
@@ -552,6 +565,11 @@ class MimeNode {
|
|
|
552
565
|
case 'Content-Type':
|
|
553
566
|
structured = mimeFuncs.parseHeaderValue(value);
|
|
554
567
|
|
|
568
|
+
// the type token decides multipart and charset below, so clean it before
|
|
569
|
+
// those run and not just on the way out, otherwise a control char makes
|
|
570
|
+
// the checks miss and the header ends up claiming a type it is not set up for
|
|
571
|
+
structured.value = (structured.value || '').toString().replace(/[\x00-\x1f\x7f]/g, '');
|
|
572
|
+
|
|
555
573
|
this._handleContentType(structured);
|
|
556
574
|
|
|
557
575
|
if (
|
|
@@ -569,9 +587,9 @@ class MimeNode {
|
|
|
569
587
|
// we can't build the value with buildHeaderValue as the value is non standard and
|
|
570
588
|
// would be converted to parameter continuation encoding that we do not want
|
|
571
589
|
// control chars can not be quoted here: HT is a fold point that unfolding would
|
|
572
|
-
// turn into a space
|
|
573
|
-
// mime encoded word that a non-ascii filename would get anyway
|
|
574
|
-
param = /[\x00-\x1f]/.test(this.filename)
|
|
590
|
+
// turn into a space, CR/LF can not appear in a header at all and DEL is not
|
|
591
|
+
// qtext, so force the mime encoded word that a non-ascii filename would get anyway
|
|
592
|
+
param = /[\x00-\x1f\x7f]/.test(this.filename)
|
|
575
593
|
? mimeFuncs.encodeWord(this.filename, this._getTextEncoding(this.filename), 52)
|
|
576
594
|
: this._encodeWords(this.filename);
|
|
577
595
|
|
|
@@ -602,8 +620,12 @@ class MimeNode {
|
|
|
602
620
|
|
|
603
621
|
if (typeof this.normalizeHeaderKey === 'function') {
|
|
604
622
|
const normalized = this.normalizeHeaderKey(key, value);
|
|
605
|
-
|
|
606
|
-
|
|
623
|
+
// the result replaces the key on the way into the header, so it gets the same
|
|
624
|
+
// treatment the key it replaces already had. a line break here would end the
|
|
625
|
+
// header and start one of the caller's own
|
|
626
|
+
const cleaned = typeof normalized === 'string' ? normalized.replace(/[\x00-\x1f\x7f]/g, '') : '';
|
|
627
|
+
if (cleaned) {
|
|
628
|
+
key = cleaned;
|
|
607
629
|
}
|
|
608
630
|
}
|
|
609
631
|
|
|
@@ -844,7 +866,7 @@ class MimeNode {
|
|
|
844
866
|
|
|
845
867
|
if (envelope.from) {
|
|
846
868
|
list = [];
|
|
847
|
-
this._convertAddresses(this.
|
|
869
|
+
this._convertAddresses(this._parseEnvelopeAddresses(envelope.from), list);
|
|
848
870
|
list = list.filter(address => address && address.address);
|
|
849
871
|
if (list.length && list[0]) {
|
|
850
872
|
this._envelope.from = list[0].address;
|
|
@@ -852,18 +874,14 @@ class MimeNode {
|
|
|
852
874
|
}
|
|
853
875
|
['to', 'cc', 'bcc'].forEach(key => {
|
|
854
876
|
if (envelope[key]) {
|
|
855
|
-
this._convertAddresses(this.
|
|
877
|
+
this._convertAddresses(this._parseEnvelopeAddresses(envelope[key]), this._envelope.to);
|
|
856
878
|
}
|
|
857
879
|
});
|
|
858
880
|
|
|
859
881
|
this._envelope.to = this._envelope.to.map(to => to.address).filter(address => address);
|
|
860
882
|
|
|
861
883
|
const standardFields = ['to', 'cc', 'bcc', 'from'];
|
|
862
|
-
|
|
863
|
-
if (!standardFields.includes(key)) {
|
|
864
|
-
this._envelope[key] = envelope[key];
|
|
865
|
-
}
|
|
866
|
-
});
|
|
884
|
+
shared.copyOwnKeys(this._envelope, envelope, key => standardFields.includes(key));
|
|
867
885
|
|
|
868
886
|
return this;
|
|
869
887
|
}
|
|
@@ -1012,7 +1030,9 @@ class MimeNode {
|
|
|
1012
1030
|
});
|
|
1013
1031
|
return contentStream;
|
|
1014
1032
|
}
|
|
1015
|
-
// fetch URL
|
|
1033
|
+
// fetch URL. nmfetch refuses any scheme that is not http(s), and it decides
|
|
1034
|
+
// that on the parsed URL. Testing the raw string here instead would reject
|
|
1035
|
+
// forms the parser accepts, such as a leading space or a slash-less authority
|
|
1016
1036
|
return nmfetch(content.href, { headers: content.httpHeaders, tls: content.tls });
|
|
1017
1037
|
}
|
|
1018
1038
|
|
|
@@ -1041,15 +1061,69 @@ class MimeNode {
|
|
|
1041
1061
|
[],
|
|
1042
1062
|
[].concat(addresses).map(address => {
|
|
1043
1063
|
if (address && address.address) {
|
|
1044
|
-
|
|
1045
|
-
address.
|
|
1046
|
-
|
|
1064
|
+
const normalized = this._normalizeAddress(address.address);
|
|
1065
|
+
if (normalized === address.address && typeof address.name === 'string') {
|
|
1066
|
+
// there is nothing to rewrite, so there is nothing to keep off the original
|
|
1067
|
+
return [address];
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// rewriting would land on the object the caller passed in and might
|
|
1071
|
+
// still hold a reference to, so rewrite a copy of it instead. An own
|
|
1072
|
+
// "__proto__" key would make the copy inherit from caller data, and
|
|
1073
|
+
// _convertAddresses reads `group` off it straight into the envelope
|
|
1074
|
+
const copy = shared.copyOwnKeys({}, address);
|
|
1075
|
+
copy.address = normalized;
|
|
1076
|
+
copy.name = address.name || '';
|
|
1077
|
+
return [copy];
|
|
1047
1078
|
}
|
|
1048
|
-
return addressparser(address);
|
|
1079
|
+
return this._normalizeParsedAddresses(addressparser(address));
|
|
1049
1080
|
})
|
|
1050
1081
|
);
|
|
1051
1082
|
}
|
|
1052
1083
|
|
|
1084
|
+
/**
|
|
1085
|
+
* Normalizes the addresses of a freshly parsed address list, groups included.
|
|
1086
|
+
*
|
|
1087
|
+
* Everything this method returns carries a normalized address, whether it arrived as an
|
|
1088
|
+
* object or was parsed out of a header value. Without this the two shapes disagree, and
|
|
1089
|
+
* a consumer reading the parsed form back is handed the ambiguous
|
|
1090
|
+
* 'user@evil.com@good.com' that the header and the envelope no longer carry.
|
|
1091
|
+
*
|
|
1092
|
+
* @param {Array} parsed An array of address objects, as returned by addressparser
|
|
1093
|
+
* @return {Array} The same array, with every address normalized
|
|
1094
|
+
*/
|
|
1095
|
+
_normalizeParsedAddresses(parsed) {
|
|
1096
|
+
// addressparser builds these objects, so no caller holds a reference to rewrite around
|
|
1097
|
+
parsed.forEach(entry => {
|
|
1098
|
+
if (entry.address) {
|
|
1099
|
+
entry.address = this._normalizeAddress(entry.address);
|
|
1100
|
+
} else if (entry.group) {
|
|
1101
|
+
this._normalizeParsedAddresses(entry.group);
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
return parsed;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Parses the addresses of an explicitly set envelope.
|
|
1110
|
+
*
|
|
1111
|
+
* An envelope value is an addr-spec and never a display name, so a bare local username
|
|
1112
|
+
* such as 'root' is the address here. Header parsing has to read the same value as a
|
|
1113
|
+
* display name, as a value with no '@' in it can not be an addr-spec in a header.
|
|
1114
|
+
*
|
|
1115
|
+
* @param {Mixed} addresses Addresses to be parsed
|
|
1116
|
+
* @return {Array} An array of address objects
|
|
1117
|
+
*/
|
|
1118
|
+
_parseEnvelopeAddresses(addresses) {
|
|
1119
|
+
return this._parseAddresses(addresses).map(entry => {
|
|
1120
|
+
if (entry.address || entry.group || !entry.name || /[\s@]/.test(entry.name)) {
|
|
1121
|
+
return entry;
|
|
1122
|
+
}
|
|
1123
|
+
return { address: this._normalizeAddress(entry.name), name: '' };
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1053
1127
|
/**
|
|
1054
1128
|
* Normalizes a header key, uses Camel-Case form, except for uppercase MIME-
|
|
1055
1129
|
*
|
|
@@ -1061,6 +1135,9 @@ class MimeNode {
|
|
|
1061
1135
|
.toString()
|
|
1062
1136
|
// no newlines in keys
|
|
1063
1137
|
.replace(/\r?\n|\r/g, ' ')
|
|
1138
|
+
// a field name is printable ascii without the colon, so a control char or DEL
|
|
1139
|
+
// can only be dropped, there is no quoting construct around a field name
|
|
1140
|
+
.replace(/[\x00-\x1f\x7f]/g, '')
|
|
1064
1141
|
.trim()
|
|
1065
1142
|
.toLowerCase()
|
|
1066
1143
|
// use uppercase words, except MIME
|
|
@@ -1121,7 +1198,13 @@ class MimeNode {
|
|
|
1121
1198
|
case 'Message-ID':
|
|
1122
1199
|
case 'In-Reply-To':
|
|
1123
1200
|
case 'Content-Id':
|
|
1124
|
-
|
|
1201
|
+
// a msg-id is structured, so an encoded word inside the angle brackets would
|
|
1202
|
+
// be read as literal text. drop the characters that can not appear in a header
|
|
1203
|
+
// at all, but leave HT alone, it separates the ids of a multi id value
|
|
1204
|
+
value = (value || '')
|
|
1205
|
+
.toString()
|
|
1206
|
+
.replace(/\r?\n|\r/g, ' ')
|
|
1207
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
|
|
1125
1208
|
|
|
1126
1209
|
if (value.charAt(0) !== '<') {
|
|
1127
1210
|
value = '<' + value;
|
|
@@ -1141,6 +1224,7 @@ class MimeNode {
|
|
|
1141
1224
|
elm = (elm || '')
|
|
1142
1225
|
.toString()
|
|
1143
1226
|
.replace(/\r?\n|\r/g, ' ')
|
|
1227
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
|
|
1144
1228
|
.trim();
|
|
1145
1229
|
return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
|
|
1146
1230
|
})
|
|
@@ -1163,7 +1247,7 @@ class MimeNode {
|
|
|
1163
1247
|
}
|
|
1164
1248
|
|
|
1165
1249
|
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
|
|
1166
|
-
return this.
|
|
1250
|
+
return this._encodeHeaderText(value);
|
|
1167
1251
|
|
|
1168
1252
|
case 'Content-Type':
|
|
1169
1253
|
case 'Content-Disposition':
|
|
@@ -1172,8 +1256,7 @@ class MimeNode {
|
|
|
1172
1256
|
|
|
1173
1257
|
default:
|
|
1174
1258
|
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
|
|
1175
|
-
|
|
1176
|
-
return this._encodeWords(value);
|
|
1259
|
+
return this._encodeHeaderText(value);
|
|
1177
1260
|
}
|
|
1178
1261
|
}
|
|
1179
1262
|
|
|
@@ -1194,7 +1277,11 @@ class MimeNode {
|
|
|
1194
1277
|
address.address = this._normalizeAddress(address.address);
|
|
1195
1278
|
|
|
1196
1279
|
if (!address.name) {
|
|
1197
|
-
|
|
1280
|
+
// an address that carries a special, be it a quoted local part or a domain
|
|
1281
|
+
// that could not be normalized, is only unambiguous inside angle brackets.
|
|
1282
|
+
// Without them a ',' or a ';' anywhere in it reads as a recipient separator
|
|
1283
|
+
// and the header would list more recipients than the envelope carries
|
|
1284
|
+
values.push(PLAIN_ADDRESS.test(address.address) ? address.address : `<${address.address}>`);
|
|
1198
1285
|
} else {
|
|
1199
1286
|
values.push(`${this._encodeAddressName(address.name)} <${address.address}>`);
|
|
1200
1287
|
}
|
|
@@ -1220,19 +1307,26 @@ class MimeNode {
|
|
|
1220
1307
|
_normalizeAddress(address) {
|
|
1221
1308
|
address = (address || '')
|
|
1222
1309
|
.toString()
|
|
1223
|
-
.replace(/[\x00-\x1F<>]+/g, ' ') // remove unallowed characters
|
|
1310
|
+
.replace(/[\x00-\x1F\x7F<>]+/g, ' ') // remove unallowed characters
|
|
1224
1311
|
.trim();
|
|
1225
1312
|
|
|
1313
|
+
if (!address) {
|
|
1314
|
+
// callers use an empty value to detect a missing address
|
|
1315
|
+
return address;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1226
1318
|
const lastAt = address.lastIndexOf('@');
|
|
1227
1319
|
if (lastAt < 0) {
|
|
1228
|
-
// Bare username
|
|
1229
|
-
return address;
|
|
1320
|
+
// Bare username, there is no domain to split off
|
|
1321
|
+
return this._normalizeLocalPart(address);
|
|
1230
1322
|
}
|
|
1231
1323
|
|
|
1232
|
-
|
|
1324
|
+
const user = address.substr(0, lastAt);
|
|
1233
1325
|
const domain = address.substr(lastAt + 1);
|
|
1234
1326
|
|
|
1235
|
-
//
|
|
1327
|
+
// Unicode in the local part is kept as is, see _normalizeLocalPart for the rest of it.
|
|
1328
|
+
// A domain has no quoting construct to fall back on, so whatever is not a valid domain
|
|
1329
|
+
// is kept as supplied and it is _convertAddresses that keeps such an address unambiguous.
|
|
1236
1330
|
// Domains are punycoded when the local part is ASCII ('safe@jõgeva.ee' -> 'safe@xn--jgeva-dua.ee').
|
|
1237
1331
|
// When the local part contains non-ASCII bytes the address already requires SMTPUTF8,
|
|
1238
1332
|
// so the domain is kept (or decoded back) as UTF-8 for symmetry on both sides of '@'.
|
|
@@ -1249,16 +1343,27 @@ class MimeNode {
|
|
|
1249
1343
|
// keep domain as supplied
|
|
1250
1344
|
}
|
|
1251
1345
|
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1346
|
+
return `${this._normalizeLocalPart(user)}@${encodedDomain}`;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Normalizes the local part of an address into a form that can be emitted as is.
|
|
1351
|
+
*
|
|
1352
|
+
* A local part is either a dot-atom or a quoted-string, anything else is not a valid
|
|
1353
|
+
* addr-spec. The quotes of a quoted local part get lost along the way, and a bare
|
|
1354
|
+
* 'user@evil.com@good.com' leaves it to the receiver which '@' splits the domain off,
|
|
1355
|
+
* while the split here is always at the last one. So whatever is not already one of
|
|
1356
|
+
* the two valid forms goes back out as a quoted-string.
|
|
1357
|
+
*
|
|
1358
|
+
* @param {String} user Local part of an address
|
|
1359
|
+
* @return {String} Local part as a dot-atom or as a quoted-string
|
|
1360
|
+
*/
|
|
1361
|
+
_normalizeLocalPart(user) {
|
|
1362
|
+
if (DOT_ATOM.test(user) || QUOTED_STRING.test(user)) {
|
|
1363
|
+
return user;
|
|
1259
1364
|
}
|
|
1260
1365
|
|
|
1261
|
-
return
|
|
1366
|
+
return mimeFuncs.quoteString(user);
|
|
1262
1367
|
}
|
|
1263
1368
|
|
|
1264
1369
|
/**
|
|
@@ -1270,7 +1375,7 @@ class MimeNode {
|
|
|
1270
1375
|
_encodeAddressName(name) {
|
|
1271
1376
|
if (!/^[\w ]*$/.test(name)) {
|
|
1272
1377
|
if (/^[\x20-\x7e]*$/.test(name)) {
|
|
1273
|
-
return
|
|
1378
|
+
return mimeFuncs.quoteString(name);
|
|
1274
1379
|
} else {
|
|
1275
1380
|
return mimeFuncs.encodeWord(name, this._getTextEncoding(name), 52);
|
|
1276
1381
|
}
|
|
@@ -1278,6 +1383,21 @@ class MimeNode {
|
|
|
1278
1383
|
return name;
|
|
1279
1384
|
}
|
|
1280
1385
|
|
|
1386
|
+
/**
|
|
1387
|
+
* Encodes an unstructured header value. Such a value can only carry VCHAR and WSP, so a
|
|
1388
|
+
* control char or DEL has to be forced into the mime encoded word that a non-ascii value
|
|
1389
|
+
* would get anyway. HT stays as it is, it is valid folding whitespace here.
|
|
1390
|
+
*
|
|
1391
|
+
* @param {String} value Header value to encode
|
|
1392
|
+
* @returns {String} Mime word encoded string if needed
|
|
1393
|
+
*/
|
|
1394
|
+
_encodeHeaderText(value) {
|
|
1395
|
+
return /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value)
|
|
1396
|
+
? mimeFuncs.encodeWord(value, this._getTextEncoding(value), 52)
|
|
1397
|
+
: // encodeWords only encodes if needed, otherwise the original string is returned
|
|
1398
|
+
this._encodeWords(value);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1281
1401
|
/**
|
|
1282
1402
|
* If needed, mime encodes the name part
|
|
1283
1403
|
*
|
package/lib/punycode/index.js
CHANGED
|
@@ -250,7 +250,7 @@ const decode = function (input) {
|
|
|
250
250
|
// Main decoding loop: start just after the last delimiter if any basic code
|
|
251
251
|
// points were copied; start at the beginning otherwise.
|
|
252
252
|
|
|
253
|
-
for (let index = basic > 0 ? basic + 1 : 0; index < inputLength /* no final expression */;
|
|
253
|
+
for (let index = basic > 0 ? basic + 1 : 0; index < inputLength /* no final expression */;) {
|
|
254
254
|
// `index` is the index of the next character to be consumed.
|
|
255
255
|
// Decode a generalized variable-length integer into `delta`,
|
|
256
256
|
// which gets added to `i`. The overflow checking is easier
|
|
@@ -62,14 +62,17 @@ class SendmailTransport {
|
|
|
62
62
|
// Sendmail strips this header line by itself
|
|
63
63
|
mail.message.keepBcc = true;
|
|
64
64
|
|
|
65
|
-
const envelope = mail.
|
|
65
|
+
const envelope = mail.message.getEnvelope();
|
|
66
66
|
const messageId = mail.message.messageId();
|
|
67
67
|
let returned;
|
|
68
68
|
|
|
69
69
|
const hasInvalidAddresses = []
|
|
70
70
|
.concat(envelope.from || [])
|
|
71
71
|
.concat(envelope.to || [])
|
|
72
|
-
|
|
72
|
+
// a local part is either a dot-atom or a quoted-string, so a leading dash sits at
|
|
73
|
+
// offset 0 or, behind the opening quote, at offset 1. Only the first shape is read
|
|
74
|
+
// as an option by sendmail, but both are the address this guard keeps out of argv
|
|
75
|
+
.some(addr => /^"?-/.test(addr));
|
|
73
76
|
if (hasInvalidAddresses) {
|
|
74
77
|
const err = new Error('Can not send mail. Invalid envelope addresses.');
|
|
75
78
|
err.code = errors.ESENDMAIL;
|
|
@@ -66,7 +66,7 @@ class SESTransport extends EventEmitter {
|
|
|
66
66
|
fromHeader = mimeNode._convertAddresses(mimeNode._parseAddresses(fromHeader.value));
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
const envelope = mail.
|
|
69
|
+
const envelope = mail.message.getEnvelope();
|
|
70
70
|
const messageId = mail.message.messageId();
|
|
71
71
|
|
|
72
72
|
const recipients = [].concat(envelope.to || []);
|
|
@@ -130,7 +130,8 @@ class SESTransport extends EventEmitter {
|
|
|
130
130
|
return callback(err);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
// mail.data.ses is caller supplied message data, so copy its own keys only
|
|
134
|
+
const sesMessage = shared.copyOwnKeys(
|
|
134
135
|
{
|
|
135
136
|
Content: {
|
|
136
137
|
Raw: {
|
|
@@ -143,7 +144,7 @@ class SESTransport extends EventEmitter {
|
|
|
143
144
|
ToAddresses: envelope.to
|
|
144
145
|
}
|
|
145
146
|
},
|
|
146
|
-
mail.data.ses
|
|
147
|
+
mail.data.ses
|
|
147
148
|
);
|
|
148
149
|
|
|
149
150
|
this.getRegion((err, region) => {
|