@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
|
@@ -5,19 +5,76 @@
|
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const punycode = require('../punycode');
|
|
8
|
-
const PassThrough = require('stream')
|
|
8
|
+
const { PassThrough } = require('stream');
|
|
9
9
|
const shared = require('../shared');
|
|
10
|
+
const urlModule = require('url');
|
|
10
11
|
|
|
11
12
|
const mimeFuncs = require('../mime-funcs');
|
|
12
13
|
const qp = require('../qp');
|
|
13
14
|
const base64 = require('../base64');
|
|
14
15
|
const addressparser = require('../addressparser');
|
|
15
16
|
const nmfetch = require('../fetch');
|
|
17
|
+
const errors = require('../errors');
|
|
16
18
|
const LastNewline = require('./last-newline');
|
|
17
19
|
|
|
18
20
|
const LeWindows = require('./le-windows');
|
|
19
21
|
const LeUnix = require('./le-unix');
|
|
20
22
|
|
|
23
|
+
const FORMATTED_HEADERS = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
|
|
24
|
+
|
|
25
|
+
// RFC 5321 atext, plus the non-ascii bytes that SMTPUTF8 (RFC 6531) adds to it. A local part
|
|
26
|
+
// built from these, with '.' as a separator, is a dot-atom and can be emitted bare
|
|
27
|
+
const ATEXT = "[A-Za-z0-9!#$%&'*+\\-/=?^_`{|}~\\x80-\\uFFFF]";
|
|
28
|
+
const DOT_ATOM = new RegExp('^' + ATEXT + '+(?:\\.' + ATEXT + '+)*$');
|
|
29
|
+
|
|
30
|
+
// A complete quoted-string: everything between the outer quotes is either a plain char or
|
|
31
|
+
// a quoted-pair. Anchored, so a value that only starts and ends with a quote does not pass
|
|
32
|
+
const QUOTED_STRING = /^"(?:[^"\\]|\\[\s\S])*"$/;
|
|
33
|
+
|
|
34
|
+
// An address that carries no special anywhere can be emitted bare in a header, everything
|
|
35
|
+
// else goes into angle brackets so that the header can not be read as more addresses than
|
|
36
|
+
// the envelope carries
|
|
37
|
+
const PLAIN_ADDRESS = /^[^\s"(),:;<>@[\\\]]+@[^\s"(),:;<>@[\\\]]+$/;
|
|
38
|
+
|
|
39
|
+
// domainToASCII and domainToUnicode are WHATWG host parsers rather than plain IDNA
|
|
40
|
+
// mappers, so they do more than map: they cut the host at '/', '\\', '?' and '#', drop C0
|
|
41
|
+
// controls, and percent-decode. Handing them 'evil.example/mail.corp.example' returns the
|
|
42
|
+
// deliverable 'evil.example', which would turn a value the bundled codec leaves as
|
|
43
|
+
// unroutable garbage into mail for a domain the sender never named. None of these
|
|
44
|
+
// characters are legal in a domain, so keep them away from the mapper.
|
|
45
|
+
const URL_PARSER_UNSAFE = /[/\\?#%\x00-\x20\x7F]/;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Encodes a domain the way browsers, the WHATWG URL Standard and DNS facing resolvers do,
|
|
49
|
+
* which is with UTS-46 mapping applied before the Punycode step.
|
|
50
|
+
*
|
|
51
|
+
* The bundled codec is plain RFC 3492 and maps nothing, so it disagrees with every
|
|
52
|
+
* conformant parser on any domain holding a mapped or ignored code point. An invisible
|
|
53
|
+
* U+00AD in 'compa\u00ADny.com' encoded to 'xn--company-pka.com' where a validator reads
|
|
54
|
+
* 'company.com', which let an allow-listed domain be checked and a different one mailed.
|
|
55
|
+
*
|
|
56
|
+
* Anything the URL parser does not accept as a hostname, an address literal such as
|
|
57
|
+
* '[127.0.0.1]' included, comes back empty and falls through to the bundled codec, which
|
|
58
|
+
* leaves those as they were supplied.
|
|
59
|
+
*
|
|
60
|
+
* @param {String} domain Domain to encode, already lowercased by the caller
|
|
61
|
+
* @param {Boolean} toUnicode Return the U-label form instead of the A-label form
|
|
62
|
+
* @return {String} Encoded domain
|
|
63
|
+
*/
|
|
64
|
+
function normalizeDomain(domain, toUnicode) {
|
|
65
|
+
// domainToASCII and domainToUnicode landed in Node 7, the bundled codec covers Node 6
|
|
66
|
+
const mapper = toUnicode ? urlModule.domainToUnicode : urlModule.domainToASCII;
|
|
67
|
+
|
|
68
|
+
if (typeof mapper === 'function' && !URL_PARSER_UNSAFE.test(domain)) {
|
|
69
|
+
const mapped = mapper(domain);
|
|
70
|
+
if (mapped) {
|
|
71
|
+
return mapped;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return toUnicode ? punycode.toUnicode(domain) : punycode.toASCII(domain);
|
|
76
|
+
}
|
|
77
|
+
|
|
21
78
|
/**
|
|
22
79
|
* Creates a new mime tree node. Assumes 'multipart/*' as the content type
|
|
23
80
|
* if it is a branch, anything else counts as leaf. If rootNode is missing from
|
|
@@ -53,7 +110,7 @@ class MimeNode {
|
|
|
53
110
|
/**
|
|
54
111
|
* If date headers is missing and current node is the root, this value is used instead
|
|
55
112
|
*/
|
|
56
|
-
this.date = new Date();
|
|
113
|
+
this.date = options.parentNode ? null : new Date();
|
|
57
114
|
|
|
58
115
|
/**
|
|
59
116
|
* Root node for current mime tree
|
|
@@ -174,7 +231,7 @@ class MimeNode {
|
|
|
174
231
|
options = contentType;
|
|
175
232
|
contentType = undefined;
|
|
176
233
|
}
|
|
177
|
-
|
|
234
|
+
const node = new MimeNode(contentType, options);
|
|
178
235
|
this.appendChild(node);
|
|
179
236
|
return node;
|
|
180
237
|
}
|
|
@@ -187,6 +244,14 @@ class MimeNode {
|
|
|
187
244
|
* @return {Object} Appended node object
|
|
188
245
|
*/
|
|
189
246
|
appendChild(childNode) {
|
|
247
|
+
// Take the node out of the tree it is in first. Leaving it there keeps it in that
|
|
248
|
+
// parent's childNodes, so it still streams as part of the old tree while parentNode
|
|
249
|
+
// already points at the new one, and anything read off the parent chain answers for
|
|
250
|
+
// the wrong tree.
|
|
251
|
+
if (childNode.parentNode && childNode.parentNode !== this) {
|
|
252
|
+
childNode.remove();
|
|
253
|
+
}
|
|
254
|
+
|
|
190
255
|
if (childNode.rootNode !== this.rootNode) {
|
|
191
256
|
childNode.rootNode = this.rootNode;
|
|
192
257
|
childNode._nodeId = ++this.rootNode.nodeCounter;
|
|
@@ -255,8 +320,7 @@ class MimeNode {
|
|
|
255
320
|
* @return {Object} current node
|
|
256
321
|
*/
|
|
257
322
|
setHeader(key, value) {
|
|
258
|
-
let added = false
|
|
259
|
-
headerValue;
|
|
323
|
+
let added = false;
|
|
260
324
|
|
|
261
325
|
// Allow setting multiple headers at once
|
|
262
326
|
if (!value && key && typeof key === 'object') {
|
|
@@ -279,7 +343,7 @@ class MimeNode {
|
|
|
279
343
|
|
|
280
344
|
key = this._normalizeHeaderKey(key);
|
|
281
345
|
|
|
282
|
-
headerValue = {
|
|
346
|
+
const headerValue = {
|
|
283
347
|
key,
|
|
284
348
|
value
|
|
285
349
|
};
|
|
@@ -403,8 +467,8 @@ class MimeNode {
|
|
|
403
467
|
});
|
|
404
468
|
}
|
|
405
469
|
|
|
406
|
-
|
|
407
|
-
|
|
470
|
+
const stream = this.createReadStream();
|
|
471
|
+
const buf = [];
|
|
408
472
|
let buflen = 0;
|
|
409
473
|
let returned = false;
|
|
410
474
|
|
|
@@ -444,7 +508,7 @@ class MimeNode {
|
|
|
444
508
|
|
|
445
509
|
getTransferEncoding() {
|
|
446
510
|
let transferEncoding = false;
|
|
447
|
-
|
|
511
|
+
const contentType = (this.getHeader('Content-Type') || '').toString().toLowerCase().trim();
|
|
448
512
|
|
|
449
513
|
if (this.content) {
|
|
450
514
|
transferEncoding = (this.getHeader('Content-Transfer-Encoding') || '').toString().toLowerCase().trim();
|
|
@@ -474,8 +538,8 @@ class MimeNode {
|
|
|
474
538
|
* @returns {String} Headers
|
|
475
539
|
*/
|
|
476
540
|
buildHeaders() {
|
|
477
|
-
|
|
478
|
-
|
|
541
|
+
const transferEncoding = this.getTransferEncoding();
|
|
542
|
+
const headers = [];
|
|
479
543
|
|
|
480
544
|
if (transferEncoding) {
|
|
481
545
|
this.setHeader('Content-Transfer-Encoding', transferEncoding);
|
|
@@ -500,7 +564,7 @@ class MimeNode {
|
|
|
500
564
|
|
|
501
565
|
// Ensure that Content-Type is the last header for the root node
|
|
502
566
|
for (let i = this._headers.length - 2; i >= 0; i--) {
|
|
503
|
-
|
|
567
|
+
const header = this._headers[i];
|
|
504
568
|
if (header.key === 'Content-Type') {
|
|
505
569
|
this._headers.splice(i, 1);
|
|
506
570
|
this._headers.push(header);
|
|
@@ -513,15 +577,14 @@ class MimeNode {
|
|
|
513
577
|
let value = header.value;
|
|
514
578
|
let structured;
|
|
515
579
|
let param;
|
|
516
|
-
|
|
517
|
-
|
|
580
|
+
const options = {};
|
|
581
|
+
const formattedHeaders = FORMATTED_HEADERS;
|
|
518
582
|
|
|
519
583
|
if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
});
|
|
584
|
+
// the keys come from a caller supplied header object and `options.prepared`
|
|
585
|
+
// below decides whether the value is emitted raw, so an own "__proto__" key
|
|
586
|
+
// here would turn an unfolded value into header injection
|
|
587
|
+
shared.copyOwnKeys(options, value, optionKey => optionKey === 'value');
|
|
525
588
|
value = (value.value || '').toString();
|
|
526
589
|
if (!value.trim()) {
|
|
527
590
|
return;
|
|
@@ -550,9 +613,18 @@ class MimeNode {
|
|
|
550
613
|
case 'Content-Type':
|
|
551
614
|
structured = mimeFuncs.parseHeaderValue(value);
|
|
552
615
|
|
|
616
|
+
// the type token decides multipart and charset below, so clean it before
|
|
617
|
+
// those run and not just on the way out, otherwise a control char makes
|
|
618
|
+
// the checks miss and the header ends up claiming a type it is not set up for
|
|
619
|
+
structured.value = (structured.value || '').toString().replace(/[\x00-\x1f\x7f]/g, '');
|
|
620
|
+
|
|
553
621
|
this._handleContentType(structured);
|
|
554
622
|
|
|
555
|
-
if (
|
|
623
|
+
if (
|
|
624
|
+
structured.value.match(/^text\/plain\b/) &&
|
|
625
|
+
typeof this.content === 'string' &&
|
|
626
|
+
/[\u0080-\uFFFF]/.test(this.content)
|
|
627
|
+
) {
|
|
556
628
|
structured.params.charset = 'utf-8';
|
|
557
629
|
}
|
|
558
630
|
|
|
@@ -562,11 +634,18 @@ class MimeNode {
|
|
|
562
634
|
// add support for non-compliant clients like QQ webmail
|
|
563
635
|
// we can't build the value with buildHeaderValue as the value is non standard and
|
|
564
636
|
// would be converted to parameter continuation encoding that we do not want
|
|
565
|
-
|
|
637
|
+
// control chars can not be quoted here: HT is a fold point that unfolding would
|
|
638
|
+
// turn into a space, CR/LF can not appear in a header at all and DEL is not
|
|
639
|
+
// qtext, so force the mime encoded word that a non-ascii filename would get anyway
|
|
640
|
+
param = /[\x00-\x1f\x7f]/.test(this.filename)
|
|
641
|
+
? mimeFuncs.encodeWord(this.filename, this._getTextEncoding(this.filename), 52)
|
|
642
|
+
: this._encodeWords(this.filename);
|
|
566
643
|
|
|
567
644
|
if (param !== this.filename || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(param)) {
|
|
568
|
-
// include value in quotes if needed
|
|
569
|
-
|
|
645
|
+
// include value in quotes if needed, escaping backslashes and quotes as
|
|
646
|
+
// quoted-pairs exactly like buildHeaderValue does for filename=, otherwise
|
|
647
|
+
// a trailing backslash would escape the closing quote
|
|
648
|
+
param = JSON.stringify(param);
|
|
570
649
|
}
|
|
571
650
|
value += '; name=' + param;
|
|
572
651
|
}
|
|
@@ -588,9 +667,13 @@ class MimeNode {
|
|
|
588
667
|
}
|
|
589
668
|
|
|
590
669
|
if (typeof this.normalizeHeaderKey === 'function') {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
670
|
+
const normalized = this.normalizeHeaderKey(key, value);
|
|
671
|
+
// the result replaces the key on the way into the header, so it gets the same
|
|
672
|
+
// treatment the key it replaces already had. a line break here would end the
|
|
673
|
+
// header and start one of the caller's own
|
|
674
|
+
const cleaned = typeof normalized === 'string' ? normalized.replace(/[\x00-\x1f\x7f]/g, '') : '';
|
|
675
|
+
if (cleaned) {
|
|
676
|
+
key = cleaned;
|
|
594
677
|
}
|
|
595
678
|
}
|
|
596
679
|
|
|
@@ -609,7 +692,7 @@ class MimeNode {
|
|
|
609
692
|
createReadStream(options) {
|
|
610
693
|
options = options || {};
|
|
611
694
|
|
|
612
|
-
|
|
695
|
+
const stream = new PassThrough(options);
|
|
613
696
|
let outputStream = stream;
|
|
614
697
|
let transform;
|
|
615
698
|
|
|
@@ -677,13 +760,13 @@ class MimeNode {
|
|
|
677
760
|
}
|
|
678
761
|
|
|
679
762
|
stream(outputStream, options, done) {
|
|
680
|
-
|
|
763
|
+
const transferEncoding = this.getTransferEncoding();
|
|
681
764
|
let contentStream;
|
|
682
765
|
let localStream;
|
|
683
766
|
|
|
684
767
|
// protect actual callback against multiple triggering
|
|
685
768
|
let returned = false;
|
|
686
|
-
|
|
769
|
+
const callback = err => {
|
|
687
770
|
if (returned) {
|
|
688
771
|
return;
|
|
689
772
|
}
|
|
@@ -693,14 +776,14 @@ class MimeNode {
|
|
|
693
776
|
|
|
694
777
|
// for multipart nodes, push child nodes
|
|
695
778
|
// for content nodes end the stream
|
|
696
|
-
|
|
779
|
+
const finalize = () => {
|
|
697
780
|
let childId = 0;
|
|
698
|
-
|
|
781
|
+
const processChildNode = () => {
|
|
699
782
|
if (childId >= this.childNodes.length) {
|
|
700
783
|
outputStream.write('\r\n--' + this.boundary + '--\r\n');
|
|
701
784
|
return callback();
|
|
702
785
|
}
|
|
703
|
-
|
|
786
|
+
const child = this.childNodes[childId++];
|
|
704
787
|
outputStream.write((childId > 1 ? '\r\n' : '') + '--' + this.boundary + '\r\n');
|
|
705
788
|
child.stream(outputStream, options, err => {
|
|
706
789
|
if (err) {
|
|
@@ -718,7 +801,7 @@ class MimeNode {
|
|
|
718
801
|
};
|
|
719
802
|
|
|
720
803
|
// pushes node content
|
|
721
|
-
|
|
804
|
+
const sendContent = () => {
|
|
722
805
|
if (this.content) {
|
|
723
806
|
if (Object.prototype.toString.call(this.content) === '[object Error]') {
|
|
724
807
|
// content is already errored
|
|
@@ -731,7 +814,7 @@ class MimeNode {
|
|
|
731
814
|
this.content.once('error', this._contentErrorHandler);
|
|
732
815
|
}
|
|
733
816
|
|
|
734
|
-
|
|
817
|
+
const createStream = () => {
|
|
735
818
|
if (['quoted-printable', 'base64'].includes(transferEncoding)) {
|
|
736
819
|
contentStream = new (transferEncoding === 'base64' ? base64 : qp).Encoder(options);
|
|
737
820
|
|
|
@@ -756,10 +839,10 @@ class MimeNode {
|
|
|
756
839
|
};
|
|
757
840
|
|
|
758
841
|
if (this.content._resolve) {
|
|
759
|
-
|
|
842
|
+
const chunks = [];
|
|
760
843
|
let chunklen = 0;
|
|
761
844
|
let returned = false;
|
|
762
|
-
|
|
845
|
+
const sourceStream = this._getStream(this.content);
|
|
763
846
|
sourceStream.on('error', err => {
|
|
764
847
|
if (returned) {
|
|
765
848
|
return;
|
|
@@ -787,9 +870,8 @@ class MimeNode {
|
|
|
787
870
|
setImmediate(createStream);
|
|
788
871
|
}
|
|
789
872
|
return;
|
|
790
|
-
} else {
|
|
791
|
-
return setImmediate(finalize);
|
|
792
873
|
}
|
|
874
|
+
return setImmediate(finalize);
|
|
793
875
|
};
|
|
794
876
|
|
|
795
877
|
if (this._raw) {
|
|
@@ -804,7 +886,7 @@ class MimeNode {
|
|
|
804
886
|
this._raw.removeListener('error', this._contentErrorHandler);
|
|
805
887
|
}
|
|
806
888
|
|
|
807
|
-
|
|
889
|
+
const raw = this._getStream(this._raw);
|
|
808
890
|
raw.pipe(outputStream, {
|
|
809
891
|
end: false
|
|
810
892
|
});
|
|
@@ -832,26 +914,23 @@ class MimeNode {
|
|
|
832
914
|
|
|
833
915
|
if (envelope.from) {
|
|
834
916
|
list = [];
|
|
835
|
-
this._convertAddresses(this.
|
|
917
|
+
this._convertAddresses(this._parseEnvelopeAddresses(envelope.from), list);
|
|
836
918
|
list = list.filter(address => address && address.address);
|
|
837
919
|
if (list.length && list[0]) {
|
|
838
920
|
this._envelope.from = list[0].address;
|
|
839
921
|
}
|
|
840
922
|
}
|
|
923
|
+
const seenRecipients = new Set();
|
|
841
924
|
['to', 'cc', 'bcc'].forEach(key => {
|
|
842
925
|
if (envelope[key]) {
|
|
843
|
-
this._convertAddresses(this.
|
|
926
|
+
this._convertAddresses(this._parseEnvelopeAddresses(envelope[key]), this._envelope.to, seenRecipients);
|
|
844
927
|
}
|
|
845
928
|
});
|
|
846
929
|
|
|
847
930
|
this._envelope.to = this._envelope.to.map(to => to.address).filter(address => address);
|
|
848
931
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
if (!standardFields.includes(key)) {
|
|
852
|
-
this._envelope[key] = envelope[key];
|
|
853
|
-
}
|
|
854
|
-
});
|
|
932
|
+
const standardFields = ['to', 'cc', 'bcc', 'from'];
|
|
933
|
+
shared.copyOwnKeys(this._envelope, envelope, key => standardFields.includes(key));
|
|
855
934
|
|
|
856
935
|
return this;
|
|
857
936
|
}
|
|
@@ -862,16 +941,18 @@ class MimeNode {
|
|
|
862
941
|
* @return {Object} Address object
|
|
863
942
|
*/
|
|
864
943
|
getAddresses() {
|
|
865
|
-
|
|
944
|
+
const addresses = {};
|
|
945
|
+
const seenByKey = new Map();
|
|
866
946
|
|
|
867
947
|
this._headers.forEach(header => {
|
|
868
|
-
|
|
948
|
+
const key = header.key.toLowerCase();
|
|
869
949
|
if (['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].includes(key)) {
|
|
870
950
|
if (!Array.isArray(addresses[key])) {
|
|
871
951
|
addresses[key] = [];
|
|
952
|
+
seenByKey.set(key, new Set());
|
|
872
953
|
}
|
|
873
954
|
|
|
874
|
-
this._convertAddresses(this._parseAddresses(header.value), addresses[key]);
|
|
955
|
+
this._convertAddresses(this._parseAddresses(header.value), addresses[key], seenByKey.get(key));
|
|
875
956
|
}
|
|
876
957
|
});
|
|
877
958
|
|
|
@@ -888,19 +969,25 @@ class MimeNode {
|
|
|
888
969
|
return this._envelope;
|
|
889
970
|
}
|
|
890
971
|
|
|
891
|
-
|
|
972
|
+
const envelope = {
|
|
892
973
|
from: false,
|
|
893
974
|
to: []
|
|
894
975
|
};
|
|
976
|
+
|
|
977
|
+
// Built once and carried across the headers. Letting _convertAddresses seed it per
|
|
978
|
+
// call would cost O(headers x recipients), and a message can carry many address
|
|
979
|
+
// headers: `headers: { to: [...] }` emits one To per entry.
|
|
980
|
+
const seenRecipients = new Set();
|
|
981
|
+
|
|
895
982
|
this._headers.forEach(header => {
|
|
896
|
-
|
|
983
|
+
const list = [];
|
|
897
984
|
if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].includes(header.key))) {
|
|
898
985
|
this._convertAddresses(this._parseAddresses(header.value), list);
|
|
899
986
|
if (list.length && list[0]) {
|
|
900
987
|
envelope.from = list[0].address;
|
|
901
988
|
}
|
|
902
989
|
} else if (['To', 'Cc', 'Bcc'].includes(header.key)) {
|
|
903
|
-
this._convertAddresses(this._parseAddresses(header.value), envelope.to);
|
|
990
|
+
this._convertAddresses(this._parseAddresses(header.value), envelope.to, seenRecipients);
|
|
904
991
|
}
|
|
905
992
|
});
|
|
906
993
|
|
|
@@ -947,6 +1034,26 @@ class MimeNode {
|
|
|
947
1034
|
|
|
948
1035
|
/////// PRIVATE METHODS
|
|
949
1036
|
|
|
1037
|
+
/**
|
|
1038
|
+
* Checks an access policy flag for this node and every node above it. The flags are set
|
|
1039
|
+
* from the options the node was built with, and createChild only ever sees the options
|
|
1040
|
+
* the caller passed, so a child of a closed tree starts out open. Reading the answer off
|
|
1041
|
+
* the parent chain keeps it right whatever order the tree was assembled in.
|
|
1042
|
+
*
|
|
1043
|
+
* @param {String} flag Either 'disableFileAccess' or 'disableUrlAccess'
|
|
1044
|
+
* @return {Boolean} true if this node or an ancestor closed that access
|
|
1045
|
+
*/
|
|
1046
|
+
_accessDisabled(flag) {
|
|
1047
|
+
let node = this;
|
|
1048
|
+
while (node) {
|
|
1049
|
+
if (node[flag]) {
|
|
1050
|
+
return true;
|
|
1051
|
+
}
|
|
1052
|
+
node = node.parentNode;
|
|
1053
|
+
}
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
950
1057
|
/**
|
|
951
1058
|
* Detects and returns handle to a stream related with the content.
|
|
952
1059
|
*
|
|
@@ -963,44 +1070,60 @@ class MimeNode {
|
|
|
963
1070
|
setImmediate(() => {
|
|
964
1071
|
try {
|
|
965
1072
|
contentStream.end(content._resolvedValue);
|
|
966
|
-
} catch (
|
|
967
|
-
contentStream.emit('error',
|
|
1073
|
+
} catch (_err) {
|
|
1074
|
+
contentStream.emit('error', _err);
|
|
968
1075
|
}
|
|
969
1076
|
});
|
|
970
1077
|
|
|
971
1078
|
return contentStream;
|
|
972
|
-
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (typeof content.pipe === 'function') {
|
|
973
1082
|
// assume as stream
|
|
974
1083
|
return content;
|
|
975
|
-
}
|
|
976
|
-
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (content && typeof content.path === 'string' && !content.href) {
|
|
1087
|
+
if (this._accessDisabled('disableFileAccess')) {
|
|
977
1088
|
contentStream = new PassThrough();
|
|
978
|
-
setImmediate(() =>
|
|
1089
|
+
setImmediate(() => {
|
|
1090
|
+
const err = new Error('File access rejected for ' + content.path);
|
|
1091
|
+
err.code = errors.EFILEACCESS;
|
|
1092
|
+
contentStream.emit('error', err);
|
|
1093
|
+
});
|
|
979
1094
|
return contentStream;
|
|
980
1095
|
}
|
|
981
1096
|
// read file
|
|
982
1097
|
return fs.createReadStream(content.path);
|
|
983
|
-
}
|
|
984
|
-
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
if (content && typeof content.href === 'string') {
|
|
1101
|
+
if (this._accessDisabled('disableUrlAccess')) {
|
|
985
1102
|
contentStream = new PassThrough();
|
|
986
|
-
setImmediate(() =>
|
|
1103
|
+
setImmediate(() => {
|
|
1104
|
+
const err = new Error('Url access rejected for ' + content.href);
|
|
1105
|
+
err.code = errors.EURLACCESS;
|
|
1106
|
+
contentStream.emit('error', err);
|
|
1107
|
+
});
|
|
987
1108
|
return contentStream;
|
|
988
1109
|
}
|
|
989
|
-
// fetch URL
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
contentStream = new PassThrough();
|
|
994
|
-
|
|
995
|
-
setImmediate(() => {
|
|
996
|
-
try {
|
|
997
|
-
contentStream.end(content || '');
|
|
998
|
-
} catch (err) {
|
|
999
|
-
contentStream.emit('error', err);
|
|
1000
|
-
}
|
|
1001
|
-
});
|
|
1002
|
-
return contentStream;
|
|
1110
|
+
// fetch URL. nmfetch refuses any scheme that is not http(s), and it decides
|
|
1111
|
+
// that on the parsed URL. Testing the raw string here instead would reject
|
|
1112
|
+
// forms the parser accepts, such as a leading space or a slash-less authority
|
|
1113
|
+
return nmfetch(content.href, { headers: content.httpHeaders, tls: content.tls });
|
|
1003
1114
|
}
|
|
1115
|
+
|
|
1116
|
+
// pass string or buffer content as a stream
|
|
1117
|
+
contentStream = new PassThrough();
|
|
1118
|
+
|
|
1119
|
+
setImmediate(() => {
|
|
1120
|
+
try {
|
|
1121
|
+
contentStream.end(content || '');
|
|
1122
|
+
} catch (_err) {
|
|
1123
|
+
contentStream.emit('error', _err);
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
return contentStream;
|
|
1004
1127
|
}
|
|
1005
1128
|
|
|
1006
1129
|
/**
|
|
@@ -1011,18 +1134,81 @@ class MimeNode {
|
|
|
1011
1134
|
* @return {Array} An array of address objects
|
|
1012
1135
|
*/
|
|
1013
1136
|
_parseAddresses(addresses) {
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1137
|
+
// Collected into one list as we go. concat.apply spreads the entries into arguments
|
|
1138
|
+
// and throws a RangeError once a recipient array is long enough to pass the
|
|
1139
|
+
// argument limit, which a large Bcc list reaches on its own.
|
|
1140
|
+
const flattened = [];
|
|
1141
|
+
|
|
1142
|
+
[].concat(addresses).forEach(address => {
|
|
1143
|
+
if (address && address.address) {
|
|
1144
|
+
const normalized = this._normalizeAddress(address.address);
|
|
1145
|
+
if (normalized === address.address && typeof address.name === 'string') {
|
|
1146
|
+
// there is nothing to rewrite, so there is nothing to keep off the original
|
|
1147
|
+
flattened.push(address);
|
|
1148
|
+
return;
|
|
1022
1149
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1150
|
+
|
|
1151
|
+
// rewriting would land on the object the caller passed in and might
|
|
1152
|
+
// still hold a reference to, so rewrite a copy of it instead. An own
|
|
1153
|
+
// "__proto__" key would make the copy inherit from caller data, and
|
|
1154
|
+
// _convertAddresses reads `group` off it straight into the envelope
|
|
1155
|
+
const copy = shared.copyOwnKeys({}, address);
|
|
1156
|
+
copy.address = normalized;
|
|
1157
|
+
copy.name = address.name || '';
|
|
1158
|
+
flattened.push(copy);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const parsed = this._normalizeParsedAddresses(addressparser(address));
|
|
1163
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
1164
|
+
flattened.push(parsed[i]);
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
return flattened;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Normalizes the addresses of a freshly parsed address list, groups included.
|
|
1173
|
+
*
|
|
1174
|
+
* Everything this method returns carries a normalized address, whether it arrived as an
|
|
1175
|
+
* object or was parsed out of a header value. Without this the two shapes disagree, and
|
|
1176
|
+
* a consumer reading the parsed form back is handed the ambiguous
|
|
1177
|
+
* 'user@evil.com@good.com' that the header and the envelope no longer carry.
|
|
1178
|
+
*
|
|
1179
|
+
* @param {Array} parsed An array of address objects, as returned by addressparser
|
|
1180
|
+
* @return {Array} The same array, with every address normalized
|
|
1181
|
+
*/
|
|
1182
|
+
_normalizeParsedAddresses(parsed) {
|
|
1183
|
+
// addressparser builds these objects, so no caller holds a reference to rewrite around
|
|
1184
|
+
parsed.forEach(entry => {
|
|
1185
|
+
if (entry.address) {
|
|
1186
|
+
entry.address = this._normalizeAddress(entry.address);
|
|
1187
|
+
} else if (entry.group) {
|
|
1188
|
+
this._normalizeParsedAddresses(entry.group);
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
return parsed;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Parses the addresses of an explicitly set envelope.
|
|
1197
|
+
*
|
|
1198
|
+
* An envelope value is an addr-spec and never a display name, so a bare local username
|
|
1199
|
+
* such as 'root' is the address here. Header parsing has to read the same value as a
|
|
1200
|
+
* display name, as a value with no '@' in it can not be an addr-spec in a header.
|
|
1201
|
+
*
|
|
1202
|
+
* @param {Mixed} addresses Addresses to be parsed
|
|
1203
|
+
* @return {Array} An array of address objects
|
|
1204
|
+
*/
|
|
1205
|
+
_parseEnvelopeAddresses(addresses) {
|
|
1206
|
+
return this._parseAddresses(addresses).map(entry => {
|
|
1207
|
+
if (entry.address || entry.group || !entry.name || /[\s@]/.test(entry.name)) {
|
|
1208
|
+
return entry;
|
|
1209
|
+
}
|
|
1210
|
+
return { address: this._normalizeAddress(entry.name), name: '' };
|
|
1211
|
+
});
|
|
1026
1212
|
}
|
|
1027
1213
|
|
|
1028
1214
|
/**
|
|
@@ -1036,6 +1222,9 @@ class MimeNode {
|
|
|
1036
1222
|
.toString()
|
|
1037
1223
|
// no newlines in keys
|
|
1038
1224
|
.replace(/\r?\n|\r/g, ' ')
|
|
1225
|
+
// a field name is printable ascii without the colon, so a control char or DEL
|
|
1226
|
+
// can only be dropped, there is no quoting construct around a field name
|
|
1227
|
+
.replace(/[\x00-\x1f\x7f]/g, '')
|
|
1039
1228
|
.trim()
|
|
1040
1229
|
.toLowerCase()
|
|
1041
1230
|
// use uppercase words, except MIME
|
|
@@ -1096,7 +1285,13 @@ class MimeNode {
|
|
|
1096
1285
|
case 'Message-ID':
|
|
1097
1286
|
case 'In-Reply-To':
|
|
1098
1287
|
case 'Content-Id':
|
|
1099
|
-
|
|
1288
|
+
// a msg-id is structured, so an encoded word inside the angle brackets would
|
|
1289
|
+
// be read as literal text. drop the characters that can not appear in a header
|
|
1290
|
+
// at all, but leave HT alone, it separates the ids of a multi id value
|
|
1291
|
+
value = (value || '')
|
|
1292
|
+
.toString()
|
|
1293
|
+
.replace(/\r?\n|\r/g, ' ')
|
|
1294
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
|
|
1100
1295
|
|
|
1101
1296
|
if (value.charAt(0) !== '<') {
|
|
1102
1297
|
value = '<' + value;
|
|
@@ -1113,10 +1308,10 @@ class MimeNode {
|
|
|
1113
1308
|
.apply(
|
|
1114
1309
|
[],
|
|
1115
1310
|
[].concat(value || '').map(elm => {
|
|
1116
|
-
// eslint-disable-line prefer-spread
|
|
1117
1311
|
elm = (elm || '')
|
|
1118
1312
|
.toString()
|
|
1119
1313
|
.replace(/\r?\n|\r/g, ' ')
|
|
1314
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
|
|
1120
1315
|
.trim();
|
|
1121
1316
|
return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
|
|
1122
1317
|
})
|
|
@@ -1139,7 +1334,7 @@ class MimeNode {
|
|
|
1139
1334
|
}
|
|
1140
1335
|
|
|
1141
1336
|
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
|
|
1142
|
-
return this.
|
|
1337
|
+
return this._encodeHeaderText(value);
|
|
1143
1338
|
|
|
1144
1339
|
case 'Content-Type':
|
|
1145
1340
|
case 'Content-Disposition':
|
|
@@ -1148,8 +1343,7 @@ class MimeNode {
|
|
|
1148
1343
|
|
|
1149
1344
|
default:
|
|
1150
1345
|
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
|
|
1151
|
-
|
|
1152
|
-
return this._encodeWords(value);
|
|
1346
|
+
return this._encodeHeaderText(value);
|
|
1153
1347
|
}
|
|
1154
1348
|
}
|
|
1155
1349
|
|
|
@@ -1160,28 +1354,44 @@ class MimeNode {
|
|
|
1160
1354
|
* @param {Array} [uniqueList] An array to be populated with addresses
|
|
1161
1355
|
* @return {String} address string
|
|
1162
1356
|
*/
|
|
1163
|
-
_convertAddresses(addresses, uniqueList) {
|
|
1164
|
-
|
|
1357
|
+
_convertAddresses(addresses, uniqueList, seenAddresses) {
|
|
1358
|
+
const values = [];
|
|
1165
1359
|
|
|
1166
1360
|
uniqueList = uniqueList || [];
|
|
1167
1361
|
|
|
1362
|
+
// Membership is checked once per address, so scanning uniqueList itself would make
|
|
1363
|
+
// a recipient list cost O(n^2). Groups recurse with the same set so that a nested
|
|
1364
|
+
// group still dedupes against the addresses collected around it, and a caller that
|
|
1365
|
+
// passes a partly filled list (To, then Cc, then Bcc) keeps deduping across headers.
|
|
1366
|
+
if (!seenAddresses) {
|
|
1367
|
+
seenAddresses = new Set();
|
|
1368
|
+
for (let i = 0; i < uniqueList.length; i++) {
|
|
1369
|
+
seenAddresses.add(uniqueList[i].address);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1168
1373
|
[].concat(addresses || []).forEach(address => {
|
|
1169
1374
|
if (address.address) {
|
|
1170
1375
|
address.address = this._normalizeAddress(address.address);
|
|
1171
1376
|
|
|
1172
1377
|
if (!address.name) {
|
|
1173
|
-
|
|
1174
|
-
|
|
1378
|
+
// an address that carries a special, be it a quoted local part or a domain
|
|
1379
|
+
// that could not be normalized, is only unambiguous inside angle brackets.
|
|
1380
|
+
// Without them a ',' or a ';' anywhere in it reads as a recipient separator
|
|
1381
|
+
// and the header would list more recipients than the envelope carries
|
|
1382
|
+
values.push(PLAIN_ADDRESS.test(address.address) ? address.address : `<${address.address}>`);
|
|
1383
|
+
} else {
|
|
1175
1384
|
values.push(`${this._encodeAddressName(address.name)} <${address.address}>`);
|
|
1176
1385
|
}
|
|
1177
1386
|
|
|
1178
|
-
if (address.address) {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
}
|
|
1387
|
+
if (!seenAddresses.has(address.address)) {
|
|
1388
|
+
seenAddresses.add(address.address);
|
|
1389
|
+
uniqueList.push(address);
|
|
1182
1390
|
}
|
|
1183
1391
|
} else if (address.group) {
|
|
1184
|
-
|
|
1392
|
+
const groupListAddresses = (
|
|
1393
|
+
address.group.length ? this._convertAddresses(address.group, uniqueList, seenAddresses) : ''
|
|
1394
|
+
).trim();
|
|
1185
1395
|
values.push(`${this._encodeAddressName(address.name)}:${groupListAddresses};`);
|
|
1186
1396
|
}
|
|
1187
1397
|
});
|
|
@@ -1198,41 +1408,63 @@ class MimeNode {
|
|
|
1198
1408
|
_normalizeAddress(address) {
|
|
1199
1409
|
address = (address || '')
|
|
1200
1410
|
.toString()
|
|
1201
|
-
.replace(/[\x00-\x1F<>]+/g, ' ') // remove unallowed characters
|
|
1411
|
+
.replace(/[\x00-\x1F\x7F<>]+/g, ' ') // remove unallowed characters
|
|
1202
1412
|
.trim();
|
|
1203
1413
|
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
// Bare username
|
|
1414
|
+
if (!address) {
|
|
1415
|
+
// callers use an empty value to detect a missing address
|
|
1207
1416
|
return address;
|
|
1208
1417
|
}
|
|
1209
1418
|
|
|
1210
|
-
|
|
1211
|
-
|
|
1419
|
+
const lastAt = address.lastIndexOf('@');
|
|
1420
|
+
if (lastAt < 0) {
|
|
1421
|
+
// Bare username, there is no domain to split off
|
|
1422
|
+
return this._normalizeLocalPart(address);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const user = address.substr(0, lastAt);
|
|
1426
|
+
const domain = address.substr(lastAt + 1);
|
|
1427
|
+
|
|
1428
|
+
// Unicode in the local part is kept as is, see _normalizeLocalPart for the rest of it.
|
|
1429
|
+
// A domain has no quoting construct to fall back on, so whatever is not a valid domain
|
|
1430
|
+
// is kept as supplied and it is _convertAddresses that keeps such an address unambiguous.
|
|
1431
|
+
// Domains are punycoded when the local part is ASCII ('safe@jõgeva.ee' -> 'safe@xn--jgeva-dua.ee').
|
|
1432
|
+
// When the local part contains non-ASCII bytes the address already requires SMTPUTF8,
|
|
1433
|
+
// so the domain is kept (or decoded back) as UTF-8 for symmetry on both sides of '@'.
|
|
1212
1434
|
|
|
1213
|
-
|
|
1214
|
-
// Domains are punycoded by default
|
|
1215
|
-
// 'jõgeva.ee' will be converted to 'xn--jgeva-dua.ee'
|
|
1216
|
-
// non-unicode domains are left as is
|
|
1435
|
+
let encodedDomain = domain;
|
|
1217
1436
|
|
|
1218
|
-
|
|
1437
|
+
// A non-ASCII local part already requires SMTPUTF8, so the domain stays UTF-8 for
|
|
1438
|
+
// symmetry on both sides of the '@' rather than being encoded to an A-label
|
|
1439
|
+
const smtputf8 = /[\x80-\uFFFF]/.test(user);
|
|
1219
1440
|
|
|
1220
1441
|
try {
|
|
1221
|
-
encodedDomain =
|
|
1222
|
-
} catch (
|
|
1223
|
-
// keep as
|
|
1442
|
+
encodedDomain = normalizeDomain(domain.toLowerCase(), smtputf8);
|
|
1443
|
+
} catch (_err) {
|
|
1444
|
+
// keep domain as supplied
|
|
1224
1445
|
}
|
|
1225
1446
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1447
|
+
return `${this._normalizeLocalPart(user)}@${encodedDomain}`;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* Normalizes the local part of an address into a form that can be emitted as is.
|
|
1452
|
+
*
|
|
1453
|
+
* A local part is either a dot-atom or a quoted-string, anything else is not a valid
|
|
1454
|
+
* addr-spec. The quotes of a quoted local part get lost along the way, and a bare
|
|
1455
|
+
* 'user@evil.com@good.com' leaves it to the receiver which '@' splits the domain off,
|
|
1456
|
+
* while the split here is always at the last one. So whatever is not already one of
|
|
1457
|
+
* the two valid forms goes back out as a quoted-string.
|
|
1458
|
+
*
|
|
1459
|
+
* @param {String} user Local part of an address
|
|
1460
|
+
* @return {String} Local part as a dot-atom or as a quoted-string
|
|
1461
|
+
*/
|
|
1462
|
+
_normalizeLocalPart(user) {
|
|
1463
|
+
if (DOT_ATOM.test(user) || QUOTED_STRING.test(user)) {
|
|
1464
|
+
return user;
|
|
1233
1465
|
}
|
|
1234
1466
|
|
|
1235
|
-
return
|
|
1467
|
+
return mimeFuncs.quoteString(user);
|
|
1236
1468
|
}
|
|
1237
1469
|
|
|
1238
1470
|
/**
|
|
@@ -1244,7 +1476,7 @@ class MimeNode {
|
|
|
1244
1476
|
_encodeAddressName(name) {
|
|
1245
1477
|
if (!/^[\w ]*$/.test(name)) {
|
|
1246
1478
|
if (/^[\x20-\x7e]*$/.test(name)) {
|
|
1247
|
-
return
|
|
1479
|
+
return mimeFuncs.quoteString(name);
|
|
1248
1480
|
} else {
|
|
1249
1481
|
return mimeFuncs.encodeWord(name, this._getTextEncoding(name), 52);
|
|
1250
1482
|
}
|
|
@@ -1252,6 +1484,21 @@ class MimeNode {
|
|
|
1252
1484
|
return name;
|
|
1253
1485
|
}
|
|
1254
1486
|
|
|
1487
|
+
/**
|
|
1488
|
+
* Encodes an unstructured header value. Such a value can only carry VCHAR and WSP, so a
|
|
1489
|
+
* control char or DEL has to be forced into the mime encoded word that a non-ascii value
|
|
1490
|
+
* would get anyway. HT stays as it is, it is valid folding whitespace here.
|
|
1491
|
+
*
|
|
1492
|
+
* @param {String} value Header value to encode
|
|
1493
|
+
* @returns {String} Mime word encoded string if needed
|
|
1494
|
+
*/
|
|
1495
|
+
_encodeHeaderText(value) {
|
|
1496
|
+
return /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value)
|
|
1497
|
+
? mimeFuncs.encodeWord(value, this._getTextEncoding(value), 52)
|
|
1498
|
+
: // encodeWords only encodes if needed, otherwise the original string is returned
|
|
1499
|
+
this._encodeWords(value);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1255
1502
|
/**
|
|
1256
1503
|
* If needed, mime encodes the name part
|
|
1257
1504
|
*
|
|
@@ -1274,20 +1521,25 @@ class MimeNode {
|
|
|
1274
1521
|
_getTextEncoding(value) {
|
|
1275
1522
|
value = (value || '').toString();
|
|
1276
1523
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1524
|
+
if (this.textEncoding) {
|
|
1525
|
+
return this.textEncoding;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// count latin alphabet symbols and 8-bit range symbols + control symbols
|
|
1529
|
+
// if there are more latin characters, then use quoted-printable
|
|
1530
|
+
// encoding, otherwise use base64
|
|
1531
|
+
let nonLatinLen = 0;
|
|
1532
|
+
let latinLen = 0;
|
|
1533
|
+
for (let i = 0, len = value.length; i < len; i++) {
|
|
1534
|
+
const code = value.charCodeAt(i);
|
|
1535
|
+
if ((code >= 0x00 && code <= 0x08) || code === 0x0b || code === 0x0c || (code >= 0x0e && code <= 0x1f) || code >= 0x80) {
|
|
1536
|
+
nonLatinLen++;
|
|
1537
|
+
} else if ((code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a)) {
|
|
1538
|
+
latinLen++;
|
|
1539
|
+
}
|
|
1289
1540
|
}
|
|
1290
|
-
|
|
1541
|
+
// if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
|
|
1542
|
+
return nonLatinLen < latinLen ? 'Q' : 'B';
|
|
1291
1543
|
}
|
|
1292
1544
|
|
|
1293
1545
|
/**
|