@nocobase/plugin-notification-email 2.3.0-beta.12 → 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.
Files changed (48) hide show
  1. package/dist/client/index.js +1 -1
  2. package/dist/client-v2/locale.d.ts +1 -1
  3. package/dist/externalVersion.js +7 -7
  4. package/dist/node_modules/nodemailer/.ncurc.js +2 -0
  5. package/dist/node_modules/nodemailer/.prettierignore +8 -0
  6. package/dist/node_modules/nodemailer/.prettierrc +12 -0
  7. package/dist/node_modules/nodemailer/.prettierrc.js +2 -0
  8. package/dist/node_modules/nodemailer/.release-please-config.json +9 -0
  9. package/dist/node_modules/nodemailer/eslint.config.js +88 -0
  10. package/dist/node_modules/nodemailer/lib/addressparser/index.js +314 -71
  11. package/dist/node_modules/nodemailer/lib/base64/index.js +9 -13
  12. package/dist/node_modules/nodemailer/lib/dkim/index.js +17 -18
  13. package/dist/node_modules/nodemailer/lib/dkim/message-parser.js +12 -13
  14. package/dist/node_modules/nodemailer/lib/dkim/relaxed-body.js +3 -3
  15. package/dist/node_modules/nodemailer/lib/dkim/sign.js +23 -19
  16. package/dist/node_modules/nodemailer/lib/errors.js +59 -0
  17. package/dist/node_modules/nodemailer/lib/fetch/cookies.js +14 -19
  18. package/dist/node_modules/nodemailer/lib/fetch/index.js +164 -27
  19. package/dist/node_modules/nodemailer/lib/json-transport/index.js +4 -4
  20. package/dist/node_modules/nodemailer/lib/mail-composer/index.js +235 -161
  21. package/dist/node_modules/nodemailer/lib/mailer/index.js +106 -52
  22. package/dist/node_modules/nodemailer/lib/mailer/mail-message.js +106 -68
  23. package/dist/node_modules/nodemailer/lib/mime-funcs/index.js +105 -59
  24. package/dist/node_modules/nodemailer/lib/mime-funcs/mime-types.js +21 -14
  25. package/dist/node_modules/nodemailer/lib/mime-node/index.js +399 -138
  26. package/dist/node_modules/nodemailer/lib/mime-node/last-newline.js +1 -1
  27. package/dist/node_modules/nodemailer/lib/mime-node/le-unix.js +4 -7
  28. package/dist/node_modules/nodemailer/lib/mime-node/le-windows.js +1 -4
  29. package/dist/node_modules/nodemailer/lib/nodemailer.js +1 -1
  30. package/dist/node_modules/nodemailer/lib/punycode/index.js +460 -0
  31. package/dist/node_modules/nodemailer/lib/qp/index.js +33 -22
  32. package/dist/node_modules/nodemailer/lib/sendmail-transport/index.js +54 -44
  33. package/dist/node_modules/nodemailer/lib/ses-transport/index.js +110 -193
  34. package/dist/node_modules/nodemailer/lib/shared/index.js +316 -207
  35. package/dist/node_modules/nodemailer/lib/shared/objects.js +42 -0
  36. package/dist/node_modules/nodemailer/lib/shared/url.js +151 -0
  37. package/dist/node_modules/nodemailer/lib/smtp-connection/data-stream.js +3 -6
  38. package/dist/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +50 -19
  39. package/dist/node_modules/nodemailer/lib/smtp-connection/index.js +423 -218
  40. package/dist/node_modules/nodemailer/lib/smtp-pool/index.js +27 -34
  41. package/dist/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +19 -16
  42. package/dist/node_modules/nodemailer/lib/smtp-transport/index.js +66 -53
  43. package/dist/node_modules/nodemailer/lib/stream-transport/index.js +16 -7
  44. package/dist/node_modules/nodemailer/lib/well-known/index.js +7 -7
  45. package/dist/node_modules/nodemailer/lib/well-known/services.json +333 -38
  46. package/dist/node_modules/nodemailer/lib/xoauth2/index.js +105 -33
  47. package/dist/node_modules/nodemailer/package.json +1 -1
  48. 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').PassThrough;
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
- let node = new MimeNode(contentType, options);
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
- let stream = this.createReadStream();
407
- let buf = [];
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
- let contentType = (this.getHeader('Content-Type') || '').toString().toLowerCase().trim();
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
- let transferEncoding = this.getTransferEncoding();
478
- let headers = [];
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
- let options = {};
508
- let formattedHeaders = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
580
+ const options = {};
581
+ const formattedHeaders = FORMATTED_HEADERS;
509
582
 
510
583
  if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
511
- Object.keys(value).forEach(key => {
512
- if (key !== 'value') {
513
- options[key] = value[key];
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 (structured.value.match(/^text\/plain\b/) && typeof this.content === 'string' && /[\u0080-\uFFFF]/.test(this.content)) {
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
- param = this._encodeWords(this.filename);
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
- param = '"' + param + '"';
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
- let normalized = this.normalizeHeaderKey(key, value);
583
- if (normalized && typeof normalized === 'string' && normalized.length) {
584
- key = normalized;
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
- let stream = new PassThrough(options);
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
- let transferEncoding = this.getTransferEncoding();
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
- let callback = err => {
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
- let finalize = () => {
779
+ const finalize = () => {
688
780
  let childId = 0;
689
- let processChildNode = () => {
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
- let child = this.childNodes[childId++];
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
- let sendContent = () => {
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
- let createStream = () => {
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
- let chunks = [];
842
+ const chunks = [];
751
843
  let chunklen = 0;
752
844
  let returned = false;
753
- let sourceStream = this._getStream(this.content);
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
- let raw = this._getStream(this._raw);
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._parseAddresses(envelope.from), list);
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._parseAddresses(envelope[key]), this._envelope.to);
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
- let standardFields = ['to', 'cc', 'bcc', 'from'];
841
- Object.keys(envelope).forEach(key => {
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
- let addresses = {};
944
+ const addresses = {};
945
+ const seenByKey = new Map();
857
946
 
858
947
  this._headers.forEach(header => {
859
- let key = header.key.toLowerCase();
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
- let envelope = {
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
- let list = [];
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 (err) {
958
- contentStream.emit('error', err);
1073
+ } catch (_err) {
1074
+ contentStream.emit('error', _err);
959
1075
  }
960
1076
  });
961
1077
 
962
1078
  return contentStream;
963
- } else if (typeof content.pipe === 'function') {
1079
+ }
1080
+
1081
+ if (typeof content.pipe === 'function') {
964
1082
  // assume as stream
965
1083
  return content;
966
- } else if (content && typeof content.path === 'string' && !content.href) {
967
- if (this.disableFileAccess) {
1084
+ }
1085
+
1086
+ if (content && typeof content.path === 'string' && !content.href) {
1087
+ if (this._accessDisabled('disableFileAccess')) {
968
1088
  contentStream = new PassThrough();
969
- setImmediate(() => contentStream.emit('error', new Error('File access rejected for ' + content.path)));
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
- } else if (content && typeof content.href === 'string') {
975
- if (this.disableUrlAccess) {
1098
+ }
1099
+
1100
+ if (content && typeof content.href === 'string') {
1101
+ if (this._accessDisabled('disableUrlAccess')) {
976
1102
  contentStream = new PassThrough();
977
- setImmediate(() => contentStream.emit('error', new Error('Url access rejected for ' + content.href)));
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
- return nmfetch(content.href, { headers: content.httpHeaders });
982
- } else {
983
- // pass string or buffer content as a stream
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
- return [].concat.apply(
1006
- [],
1007
- [].concat(addresses).map(address => {
1008
- // eslint-disable-line prefer-spread
1009
- if (address && address.address) {
1010
- address.address = this._normalizeAddress(address.address);
1011
- address.name = address.name || '';
1012
- return [address];
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
- return addressparser(address);
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
- value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
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._encodeWords(value);
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
- // encodeWords only encodes if needed, otherwise the original string is returned
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
- let values = [];
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
- values.push(address.address.indexOf(' ') >= 0 ? `<${address.address}>` : `${address.address}`);
1165
- } else if (address.name) {
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
- if (!uniqueList.filter(a => a.address === address.address).length) {
1171
- uniqueList.push(address);
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
- let groupListAddresses = (address.group.length ? this._convertAddresses(address.group, uniqueList) : '').trim();
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
- let lastAt = address.lastIndexOf('@');
1196
- if (lastAt < 0) {
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
- let user = address.substr(0, lastAt);
1202
- let domain = address.substr(lastAt + 1);
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
- // Usernames are not touched and are kept as is even if these include unicode
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
- let encodedDomain;
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 = punycode.toASCII(domain.toLowerCase());
1213
- } catch (err) {
1214
- // keep as is?
1442
+ encodedDomain = normalizeDomain(domain.toLowerCase(), smtputf8);
1443
+ } catch (_err) {
1444
+ // keep domain as supplied
1215
1445
  }
1216
1446
 
1217
- if (user.indexOf(' ') >= 0) {
1218
- if (user.charAt(0) !== '"') {
1219
- user = '"' + user;
1220
- }
1221
- if (user.substr(-1) !== '"') {
1222
- user = user + '"';
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 `${user}@${encodedDomain}`;
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 ']*$/.test(name)) {
1477
+ if (!/^[\w ]*$/.test(name)) {
1237
1478
  if (/^[\x20-\x7e]*$/.test(name)) {
1238
- return '"' + name.replace(/([\\"])/g, '\\$1') + '"';
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
- let encoding = this.textEncoding;
1269
- let latinLen;
1270
- let nonLatinLen;
1271
-
1272
- if (!encoding) {
1273
- // count latin alphabet symbols and 8-bit range symbols + control symbols
1274
- // if there are more latin characters, then use quoted-printable
1275
- // encoding, otherwise use base64
1276
- nonLatinLen = (value.match(/[\x00-\x08\x0B\x0C\x0E-\x1F\u0080-\uFFFF]/g) || []).length; // eslint-disable-line no-control-regex
1277
- latinLen = (value.match(/[a-z]/gi) || []).length;
1278
- // if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
1279
- encoding = nonLatinLen < latinLen ? 'Q' : 'B';
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
- return encoding;
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
  /**