@nocobase/plugin-notification-email 2.3.0-beta.11 → 2.3.0-beta.13
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/client/index.js +1 -1
- package/dist/client-v2/locale.d.ts +1 -1
- package/dist/externalVersion.js +7 -7
- 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 +23 -19
- 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 +235 -161
- package/dist/node_modules/nodemailer/lib/mailer/index.js +106 -52
- 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 +399 -138
- 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 +460 -0
- 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 +316 -207
- 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 +423 -218
- 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 +333 -38
- package/dist/node_modules/nodemailer/lib/xoauth2/index.js +105 -33
- package/dist/node_modules/nodemailer/package.json +1 -1
- package/package.json +3 -3
|
@@ -4,20 +4,77 @@
|
|
|
4
4
|
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const fs = require('fs');
|
|
7
|
-
const punycode = require('punycode');
|
|
8
|
-
const PassThrough = require('stream')
|
|
7
|
+
const punycode = require('../punycode');
|
|
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);
|
|
@@ -497,6 +561,15 @@ class MimeNode {
|
|
|
497
561
|
if (!this.getHeader('MIME-Version')) {
|
|
498
562
|
this.setHeader('MIME-Version', '1.0');
|
|
499
563
|
}
|
|
564
|
+
|
|
565
|
+
// Ensure that Content-Type is the last header for the root node
|
|
566
|
+
for (let i = this._headers.length - 2; i >= 0; i--) {
|
|
567
|
+
const header = this._headers[i];
|
|
568
|
+
if (header.key === 'Content-Type') {
|
|
569
|
+
this._headers.splice(i, 1);
|
|
570
|
+
this._headers.push(header);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
500
573
|
}
|
|
501
574
|
|
|
502
575
|
this._headers.forEach(header => {
|
|
@@ -504,15 +577,14 @@ class MimeNode {
|
|
|
504
577
|
let value = header.value;
|
|
505
578
|
let structured;
|
|
506
579
|
let param;
|
|
507
|
-
|
|
508
|
-
|
|
580
|
+
const options = {};
|
|
581
|
+
const formattedHeaders = FORMATTED_HEADERS;
|
|
509
582
|
|
|
510
583
|
if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
});
|
|
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');
|
|
516
588
|
value = (value.value || '').toString();
|
|
517
589
|
if (!value.trim()) {
|
|
518
590
|
return;
|
|
@@ -541,9 +613,18 @@ class MimeNode {
|
|
|
541
613
|
case 'Content-Type':
|
|
542
614
|
structured = mimeFuncs.parseHeaderValue(value);
|
|
543
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
|
+
|
|
544
621
|
this._handleContentType(structured);
|
|
545
622
|
|
|
546
|
-
if (
|
|
623
|
+
if (
|
|
624
|
+
structured.value.match(/^text\/plain\b/) &&
|
|
625
|
+
typeof this.content === 'string' &&
|
|
626
|
+
/[\u0080-\uFFFF]/.test(this.content)
|
|
627
|
+
) {
|
|
547
628
|
structured.params.charset = 'utf-8';
|
|
548
629
|
}
|
|
549
630
|
|
|
@@ -553,11 +634,18 @@ class MimeNode {
|
|
|
553
634
|
// add support for non-compliant clients like QQ webmail
|
|
554
635
|
// we can't build the value with buildHeaderValue as the value is non standard and
|
|
555
636
|
// would be converted to parameter continuation encoding that we do not want
|
|
556
|
-
|
|
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);
|
|
557
643
|
|
|
558
644
|
if (param !== this.filename || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(param)) {
|
|
559
|
-
// include value in quotes if needed
|
|
560
|
-
|
|
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);
|
|
561
649
|
}
|
|
562
650
|
value += '; name=' + param;
|
|
563
651
|
}
|
|
@@ -579,9 +667,13 @@ class MimeNode {
|
|
|
579
667
|
}
|
|
580
668
|
|
|
581
669
|
if (typeof this.normalizeHeaderKey === 'function') {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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;
|
|
585
677
|
}
|
|
586
678
|
}
|
|
587
679
|
|
|
@@ -600,7 +692,7 @@ class MimeNode {
|
|
|
600
692
|
createReadStream(options) {
|
|
601
693
|
options = options || {};
|
|
602
694
|
|
|
603
|
-
|
|
695
|
+
const stream = new PassThrough(options);
|
|
604
696
|
let outputStream = stream;
|
|
605
697
|
let transform;
|
|
606
698
|
|
|
@@ -668,13 +760,13 @@ class MimeNode {
|
|
|
668
760
|
}
|
|
669
761
|
|
|
670
762
|
stream(outputStream, options, done) {
|
|
671
|
-
|
|
763
|
+
const transferEncoding = this.getTransferEncoding();
|
|
672
764
|
let contentStream;
|
|
673
765
|
let localStream;
|
|
674
766
|
|
|
675
767
|
// protect actual callback against multiple triggering
|
|
676
768
|
let returned = false;
|
|
677
|
-
|
|
769
|
+
const callback = err => {
|
|
678
770
|
if (returned) {
|
|
679
771
|
return;
|
|
680
772
|
}
|
|
@@ -684,14 +776,14 @@ class MimeNode {
|
|
|
684
776
|
|
|
685
777
|
// for multipart nodes, push child nodes
|
|
686
778
|
// for content nodes end the stream
|
|
687
|
-
|
|
779
|
+
const finalize = () => {
|
|
688
780
|
let childId = 0;
|
|
689
|
-
|
|
781
|
+
const processChildNode = () => {
|
|
690
782
|
if (childId >= this.childNodes.length) {
|
|
691
783
|
outputStream.write('\r\n--' + this.boundary + '--\r\n');
|
|
692
784
|
return callback();
|
|
693
785
|
}
|
|
694
|
-
|
|
786
|
+
const child = this.childNodes[childId++];
|
|
695
787
|
outputStream.write((childId > 1 ? '\r\n' : '') + '--' + this.boundary + '\r\n');
|
|
696
788
|
child.stream(outputStream, options, err => {
|
|
697
789
|
if (err) {
|
|
@@ -709,7 +801,7 @@ class MimeNode {
|
|
|
709
801
|
};
|
|
710
802
|
|
|
711
803
|
// pushes node content
|
|
712
|
-
|
|
804
|
+
const sendContent = () => {
|
|
713
805
|
if (this.content) {
|
|
714
806
|
if (Object.prototype.toString.call(this.content) === '[object Error]') {
|
|
715
807
|
// content is already errored
|
|
@@ -722,7 +814,7 @@ class MimeNode {
|
|
|
722
814
|
this.content.once('error', this._contentErrorHandler);
|
|
723
815
|
}
|
|
724
816
|
|
|
725
|
-
|
|
817
|
+
const createStream = () => {
|
|
726
818
|
if (['quoted-printable', 'base64'].includes(transferEncoding)) {
|
|
727
819
|
contentStream = new (transferEncoding === 'base64' ? base64 : qp).Encoder(options);
|
|
728
820
|
|
|
@@ -747,10 +839,10 @@ class MimeNode {
|
|
|
747
839
|
};
|
|
748
840
|
|
|
749
841
|
if (this.content._resolve) {
|
|
750
|
-
|
|
842
|
+
const chunks = [];
|
|
751
843
|
let chunklen = 0;
|
|
752
844
|
let returned = false;
|
|
753
|
-
|
|
845
|
+
const sourceStream = this._getStream(this.content);
|
|
754
846
|
sourceStream.on('error', err => {
|
|
755
847
|
if (returned) {
|
|
756
848
|
return;
|
|
@@ -778,9 +870,8 @@ class MimeNode {
|
|
|
778
870
|
setImmediate(createStream);
|
|
779
871
|
}
|
|
780
872
|
return;
|
|
781
|
-
} else {
|
|
782
|
-
return setImmediate(finalize);
|
|
783
873
|
}
|
|
874
|
+
return setImmediate(finalize);
|
|
784
875
|
};
|
|
785
876
|
|
|
786
877
|
if (this._raw) {
|
|
@@ -795,7 +886,7 @@ class MimeNode {
|
|
|
795
886
|
this._raw.removeListener('error', this._contentErrorHandler);
|
|
796
887
|
}
|
|
797
888
|
|
|
798
|
-
|
|
889
|
+
const raw = this._getStream(this._raw);
|
|
799
890
|
raw.pipe(outputStream, {
|
|
800
891
|
end: false
|
|
801
892
|
});
|
|
@@ -823,26 +914,23 @@ class MimeNode {
|
|
|
823
914
|
|
|
824
915
|
if (envelope.from) {
|
|
825
916
|
list = [];
|
|
826
|
-
this._convertAddresses(this.
|
|
917
|
+
this._convertAddresses(this._parseEnvelopeAddresses(envelope.from), list);
|
|
827
918
|
list = list.filter(address => address && address.address);
|
|
828
919
|
if (list.length && list[0]) {
|
|
829
920
|
this._envelope.from = list[0].address;
|
|
830
921
|
}
|
|
831
922
|
}
|
|
923
|
+
const seenRecipients = new Set();
|
|
832
924
|
['to', 'cc', 'bcc'].forEach(key => {
|
|
833
925
|
if (envelope[key]) {
|
|
834
|
-
this._convertAddresses(this.
|
|
926
|
+
this._convertAddresses(this._parseEnvelopeAddresses(envelope[key]), this._envelope.to, seenRecipients);
|
|
835
927
|
}
|
|
836
928
|
});
|
|
837
929
|
|
|
838
930
|
this._envelope.to = this._envelope.to.map(to => to.address).filter(address => address);
|
|
839
931
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
if (!standardFields.includes(key)) {
|
|
843
|
-
this._envelope[key] = envelope[key];
|
|
844
|
-
}
|
|
845
|
-
});
|
|
932
|
+
const standardFields = ['to', 'cc', 'bcc', 'from'];
|
|
933
|
+
shared.copyOwnKeys(this._envelope, envelope, key => standardFields.includes(key));
|
|
846
934
|
|
|
847
935
|
return this;
|
|
848
936
|
}
|
|
@@ -853,16 +941,18 @@ class MimeNode {
|
|
|
853
941
|
* @return {Object} Address object
|
|
854
942
|
*/
|
|
855
943
|
getAddresses() {
|
|
856
|
-
|
|
944
|
+
const addresses = {};
|
|
945
|
+
const seenByKey = new Map();
|
|
857
946
|
|
|
858
947
|
this._headers.forEach(header => {
|
|
859
|
-
|
|
948
|
+
const key = header.key.toLowerCase();
|
|
860
949
|
if (['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].includes(key)) {
|
|
861
950
|
if (!Array.isArray(addresses[key])) {
|
|
862
951
|
addresses[key] = [];
|
|
952
|
+
seenByKey.set(key, new Set());
|
|
863
953
|
}
|
|
864
954
|
|
|
865
|
-
this._convertAddresses(this._parseAddresses(header.value), addresses[key]);
|
|
955
|
+
this._convertAddresses(this._parseAddresses(header.value), addresses[key], seenByKey.get(key));
|
|
866
956
|
}
|
|
867
957
|
});
|
|
868
958
|
|
|
@@ -879,19 +969,25 @@ class MimeNode {
|
|
|
879
969
|
return this._envelope;
|
|
880
970
|
}
|
|
881
971
|
|
|
882
|
-
|
|
972
|
+
const envelope = {
|
|
883
973
|
from: false,
|
|
884
974
|
to: []
|
|
885
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
|
+
|
|
886
982
|
this._headers.forEach(header => {
|
|
887
|
-
|
|
983
|
+
const list = [];
|
|
888
984
|
if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].includes(header.key))) {
|
|
889
985
|
this._convertAddresses(this._parseAddresses(header.value), list);
|
|
890
986
|
if (list.length && list[0]) {
|
|
891
987
|
envelope.from = list[0].address;
|
|
892
988
|
}
|
|
893
989
|
} else if (['To', 'Cc', 'Bcc'].includes(header.key)) {
|
|
894
|
-
this._convertAddresses(this._parseAddresses(header.value), envelope.to);
|
|
990
|
+
this._convertAddresses(this._parseAddresses(header.value), envelope.to, seenRecipients);
|
|
895
991
|
}
|
|
896
992
|
});
|
|
897
993
|
|
|
@@ -938,6 +1034,26 @@ class MimeNode {
|
|
|
938
1034
|
|
|
939
1035
|
/////// PRIVATE METHODS
|
|
940
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
|
+
|
|
941
1057
|
/**
|
|
942
1058
|
* Detects and returns handle to a stream related with the content.
|
|
943
1059
|
*
|
|
@@ -954,44 +1070,60 @@ class MimeNode {
|
|
|
954
1070
|
setImmediate(() => {
|
|
955
1071
|
try {
|
|
956
1072
|
contentStream.end(content._resolvedValue);
|
|
957
|
-
} catch (
|
|
958
|
-
contentStream.emit('error',
|
|
1073
|
+
} catch (_err) {
|
|
1074
|
+
contentStream.emit('error', _err);
|
|
959
1075
|
}
|
|
960
1076
|
});
|
|
961
1077
|
|
|
962
1078
|
return contentStream;
|
|
963
|
-
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (typeof content.pipe === 'function') {
|
|
964
1082
|
// assume as stream
|
|
965
1083
|
return content;
|
|
966
|
-
}
|
|
967
|
-
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (content && typeof content.path === 'string' && !content.href) {
|
|
1087
|
+
if (this._accessDisabled('disableFileAccess')) {
|
|
968
1088
|
contentStream = new PassThrough();
|
|
969
|
-
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
|
+
});
|
|
970
1094
|
return contentStream;
|
|
971
1095
|
}
|
|
972
1096
|
// read file
|
|
973
1097
|
return fs.createReadStream(content.path);
|
|
974
|
-
}
|
|
975
|
-
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
if (content && typeof content.href === 'string') {
|
|
1101
|
+
if (this._accessDisabled('disableUrlAccess')) {
|
|
976
1102
|
contentStream = new PassThrough();
|
|
977
|
-
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
|
+
});
|
|
978
1108
|
return contentStream;
|
|
979
1109
|
}
|
|
980
|
-
// fetch URL
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
contentStream = new PassThrough();
|
|
985
|
-
|
|
986
|
-
setImmediate(() => {
|
|
987
|
-
try {
|
|
988
|
-
contentStream.end(content || '');
|
|
989
|
-
} catch (err) {
|
|
990
|
-
contentStream.emit('error', err);
|
|
991
|
-
}
|
|
992
|
-
});
|
|
993
|
-
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 });
|
|
994
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;
|
|
995
1127
|
}
|
|
996
1128
|
|
|
997
1129
|
/**
|
|
@@ -1002,18 +1134,81 @@ class MimeNode {
|
|
|
1002
1134
|
* @return {Array} An array of address objects
|
|
1003
1135
|
*/
|
|
1004
1136
|
_parseAddresses(addresses) {
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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;
|
|
1013
1149
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
+
});
|
|
1017
1212
|
}
|
|
1018
1213
|
|
|
1019
1214
|
/**
|
|
@@ -1027,6 +1222,9 @@ class MimeNode {
|
|
|
1027
1222
|
.toString()
|
|
1028
1223
|
// no newlines in keys
|
|
1029
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, '')
|
|
1030
1228
|
.trim()
|
|
1031
1229
|
.toLowerCase()
|
|
1032
1230
|
// use uppercase words, except MIME
|
|
@@ -1087,7 +1285,13 @@ class MimeNode {
|
|
|
1087
1285
|
case 'Message-ID':
|
|
1088
1286
|
case 'In-Reply-To':
|
|
1089
1287
|
case 'Content-Id':
|
|
1090
|
-
|
|
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, '');
|
|
1091
1295
|
|
|
1092
1296
|
if (value.charAt(0) !== '<') {
|
|
1093
1297
|
value = '<' + value;
|
|
@@ -1104,10 +1308,10 @@ class MimeNode {
|
|
|
1104
1308
|
.apply(
|
|
1105
1309
|
[],
|
|
1106
1310
|
[].concat(value || '').map(elm => {
|
|
1107
|
-
// eslint-disable-line prefer-spread
|
|
1108
1311
|
elm = (elm || '')
|
|
1109
1312
|
.toString()
|
|
1110
1313
|
.replace(/\r?\n|\r/g, ' ')
|
|
1314
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
|
|
1111
1315
|
.trim();
|
|
1112
1316
|
return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
|
|
1113
1317
|
})
|
|
@@ -1130,7 +1334,7 @@ class MimeNode {
|
|
|
1130
1334
|
}
|
|
1131
1335
|
|
|
1132
1336
|
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
|
|
1133
|
-
return this.
|
|
1337
|
+
return this._encodeHeaderText(value);
|
|
1134
1338
|
|
|
1135
1339
|
case 'Content-Type':
|
|
1136
1340
|
case 'Content-Disposition':
|
|
@@ -1139,8 +1343,7 @@ class MimeNode {
|
|
|
1139
1343
|
|
|
1140
1344
|
default:
|
|
1141
1345
|
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
|
|
1142
|
-
|
|
1143
|
-
return this._encodeWords(value);
|
|
1346
|
+
return this._encodeHeaderText(value);
|
|
1144
1347
|
}
|
|
1145
1348
|
}
|
|
1146
1349
|
|
|
@@ -1151,28 +1354,44 @@ class MimeNode {
|
|
|
1151
1354
|
* @param {Array} [uniqueList] An array to be populated with addresses
|
|
1152
1355
|
* @return {String} address string
|
|
1153
1356
|
*/
|
|
1154
|
-
_convertAddresses(addresses, uniqueList) {
|
|
1155
|
-
|
|
1357
|
+
_convertAddresses(addresses, uniqueList, seenAddresses) {
|
|
1358
|
+
const values = [];
|
|
1156
1359
|
|
|
1157
1360
|
uniqueList = uniqueList || [];
|
|
1158
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
|
+
|
|
1159
1373
|
[].concat(addresses || []).forEach(address => {
|
|
1160
1374
|
if (address.address) {
|
|
1161
1375
|
address.address = this._normalizeAddress(address.address);
|
|
1162
1376
|
|
|
1163
1377
|
if (!address.name) {
|
|
1164
|
-
|
|
1165
|
-
|
|
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 {
|
|
1166
1384
|
values.push(`${this._encodeAddressName(address.name)} <${address.address}>`);
|
|
1167
1385
|
}
|
|
1168
1386
|
|
|
1169
|
-
if (address.address) {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
}
|
|
1387
|
+
if (!seenAddresses.has(address.address)) {
|
|
1388
|
+
seenAddresses.add(address.address);
|
|
1389
|
+
uniqueList.push(address);
|
|
1173
1390
|
}
|
|
1174
1391
|
} else if (address.group) {
|
|
1175
|
-
|
|
1392
|
+
const groupListAddresses = (
|
|
1393
|
+
address.group.length ? this._convertAddresses(address.group, uniqueList, seenAddresses) : ''
|
|
1394
|
+
).trim();
|
|
1176
1395
|
values.push(`${this._encodeAddressName(address.name)}:${groupListAddresses};`);
|
|
1177
1396
|
}
|
|
1178
1397
|
});
|
|
@@ -1189,41 +1408,63 @@ class MimeNode {
|
|
|
1189
1408
|
_normalizeAddress(address) {
|
|
1190
1409
|
address = (address || '')
|
|
1191
1410
|
.toString()
|
|
1192
|
-
.replace(/[\x00-\x1F<>]+/g, ' ') // remove unallowed characters
|
|
1411
|
+
.replace(/[\x00-\x1F\x7F<>]+/g, ' ') // remove unallowed characters
|
|
1193
1412
|
.trim();
|
|
1194
1413
|
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
// Bare username
|
|
1414
|
+
if (!address) {
|
|
1415
|
+
// callers use an empty value to detect a missing address
|
|
1198
1416
|
return address;
|
|
1199
1417
|
}
|
|
1200
1418
|
|
|
1201
|
-
|
|
1202
|
-
|
|
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 '@'.
|
|
1203
1434
|
|
|
1204
|
-
|
|
1205
|
-
// Domains are punycoded by default
|
|
1206
|
-
// 'jõgeva.ee' will be converted to 'xn--jgeva-dua.ee'
|
|
1207
|
-
// non-unicode domains are left as is
|
|
1435
|
+
let encodedDomain = domain;
|
|
1208
1436
|
|
|
1209
|
-
|
|
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);
|
|
1210
1440
|
|
|
1211
1441
|
try {
|
|
1212
|
-
encodedDomain =
|
|
1213
|
-
} catch (
|
|
1214
|
-
// keep as
|
|
1442
|
+
encodedDomain = normalizeDomain(domain.toLowerCase(), smtputf8);
|
|
1443
|
+
} catch (_err) {
|
|
1444
|
+
// keep domain as supplied
|
|
1215
1445
|
}
|
|
1216
1446
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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;
|
|
1224
1465
|
}
|
|
1225
1466
|
|
|
1226
|
-
return
|
|
1467
|
+
return mimeFuncs.quoteString(user);
|
|
1227
1468
|
}
|
|
1228
1469
|
|
|
1229
1470
|
/**
|
|
@@ -1233,9 +1474,9 @@ class MimeNode {
|
|
|
1233
1474
|
* @returns {String} Mime word encoded string if needed
|
|
1234
1475
|
*/
|
|
1235
1476
|
_encodeAddressName(name) {
|
|
1236
|
-
if (!/^[\w
|
|
1477
|
+
if (!/^[\w ]*$/.test(name)) {
|
|
1237
1478
|
if (/^[\x20-\x7e]*$/.test(name)) {
|
|
1238
|
-
return
|
|
1479
|
+
return mimeFuncs.quoteString(name);
|
|
1239
1480
|
} else {
|
|
1240
1481
|
return mimeFuncs.encodeWord(name, this._getTextEncoding(name), 52);
|
|
1241
1482
|
}
|
|
@@ -1243,6 +1484,21 @@ class MimeNode {
|
|
|
1243
1484
|
return name;
|
|
1244
1485
|
}
|
|
1245
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
|
+
|
|
1246
1502
|
/**
|
|
1247
1503
|
* If needed, mime encodes the name part
|
|
1248
1504
|
*
|
|
@@ -1265,20 +1521,25 @@ class MimeNode {
|
|
|
1265
1521
|
_getTextEncoding(value) {
|
|
1266
1522
|
value = (value || '').toString();
|
|
1267
1523
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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
|
+
}
|
|
1280
1540
|
}
|
|
1281
|
-
|
|
1541
|
+
// if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
|
|
1542
|
+
return nonLatinLen < latinLen ? 'Q' : 'B';
|
|
1282
1543
|
}
|
|
1283
1544
|
|
|
1284
1545
|
/**
|