@nocobase/plugin-workflow-mailer 2.2.17 → 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.
Files changed (53) hide show
  1. package/dist/externalVersion.js +6 -6
  2. package/dist/node_modules/joi/dist/joi-browser.min.js +1 -1
  3. package/dist/node_modules/joi/lib/index.js +1 -1
  4. package/dist/node_modules/joi/lib/messages.js +35 -6
  5. package/dist/node_modules/joi/lib/types/keys.js +18 -1
  6. package/dist/node_modules/joi/lib/types/link.js +18 -2
  7. package/dist/node_modules/joi/lib/types/string.js +2 -2
  8. package/dist/node_modules/joi/package.json +1 -1
  9. package/dist/node_modules/nodemailer/.ncurc.js +2 -0
  10. package/dist/node_modules/nodemailer/.prettierignore +8 -0
  11. package/dist/node_modules/nodemailer/.prettierrc +12 -0
  12. package/dist/node_modules/nodemailer/.prettierrc.js +2 -0
  13. package/dist/node_modules/nodemailer/.release-please-config.json +9 -0
  14. package/dist/node_modules/nodemailer/eslint.config.js +88 -0
  15. package/dist/node_modules/nodemailer/lib/addressparser/index.js +314 -71
  16. package/dist/node_modules/nodemailer/lib/base64/index.js +9 -13
  17. package/dist/node_modules/nodemailer/lib/dkim/index.js +17 -18
  18. package/dist/node_modules/nodemailer/lib/dkim/message-parser.js +12 -13
  19. package/dist/node_modules/nodemailer/lib/dkim/relaxed-body.js +3 -3
  20. package/dist/node_modules/nodemailer/lib/dkim/sign.js +22 -18
  21. package/dist/node_modules/nodemailer/lib/errors.js +59 -0
  22. package/dist/node_modules/nodemailer/lib/fetch/cookies.js +14 -19
  23. package/dist/node_modules/nodemailer/lib/fetch/index.js +164 -27
  24. package/dist/node_modules/nodemailer/lib/json-transport/index.js +4 -4
  25. package/dist/node_modules/nodemailer/lib/mail-composer/index.js +229 -155
  26. package/dist/node_modules/nodemailer/lib/mailer/index.js +106 -54
  27. package/dist/node_modules/nodemailer/lib/mailer/mail-message.js +106 -68
  28. package/dist/node_modules/nodemailer/lib/mime-funcs/index.js +105 -59
  29. package/dist/node_modules/nodemailer/lib/mime-funcs/mime-types.js +21 -14
  30. package/dist/node_modules/nodemailer/lib/mime-node/index.js +389 -137
  31. package/dist/node_modules/nodemailer/lib/mime-node/last-newline.js +1 -1
  32. package/dist/node_modules/nodemailer/lib/mime-node/le-unix.js +4 -7
  33. package/dist/node_modules/nodemailer/lib/mime-node/le-windows.js +1 -4
  34. package/dist/node_modules/nodemailer/lib/nodemailer.js +1 -1
  35. package/dist/node_modules/nodemailer/lib/punycode/index.js +1 -1
  36. package/dist/node_modules/nodemailer/lib/qp/index.js +33 -22
  37. package/dist/node_modules/nodemailer/lib/sendmail-transport/index.js +54 -44
  38. package/dist/node_modules/nodemailer/lib/ses-transport/index.js +110 -193
  39. package/dist/node_modules/nodemailer/lib/shared/index.js +299 -240
  40. package/dist/node_modules/nodemailer/lib/shared/objects.js +42 -0
  41. package/dist/node_modules/nodemailer/lib/shared/url.js +151 -0
  42. package/dist/node_modules/nodemailer/lib/smtp-connection/data-stream.js +3 -6
  43. package/dist/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +50 -19
  44. package/dist/node_modules/nodemailer/lib/smtp-connection/index.js +415 -223
  45. package/dist/node_modules/nodemailer/lib/smtp-pool/index.js +27 -34
  46. package/dist/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +19 -16
  47. package/dist/node_modules/nodemailer/lib/smtp-transport/index.js +66 -53
  48. package/dist/node_modules/nodemailer/lib/stream-transport/index.js +16 -7
  49. package/dist/node_modules/nodemailer/lib/well-known/index.js +7 -7
  50. package/dist/node_modules/nodemailer/lib/well-known/services.json +328 -38
  51. package/dist/node_modules/nodemailer/lib/xoauth2/index.js +105 -33
  52. package/dist/node_modules/nodemailer/package.json +1 -1
  53. 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').PassThrough;
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);
@@ -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
- let header = this._headers[i];
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
- let options = {};
517
- let formattedHeaders = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
580
+ const options = {};
581
+ const formattedHeaders = FORMATTED_HEADERS;
518
582
 
519
583
  if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
520
- Object.keys(value).forEach(key => {
521
- if (key !== 'value') {
522
- options[key] = value[key];
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 (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
+ ) {
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
- 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);
566
643
 
567
644
  if (param !== this.filename || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(param)) {
568
- // include value in quotes if needed
569
- 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);
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
- let normalized = this.normalizeHeaderKey(key, value);
592
- if (normalized && typeof normalized === 'string' && normalized.length) {
593
- 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;
594
677
  }
595
678
  }
596
679
 
@@ -609,7 +692,7 @@ class MimeNode {
609
692
  createReadStream(options) {
610
693
  options = options || {};
611
694
 
612
- let stream = new PassThrough(options);
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
- let transferEncoding = this.getTransferEncoding();
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
- let callback = err => {
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
- let finalize = () => {
779
+ const finalize = () => {
697
780
  let childId = 0;
698
- let processChildNode = () => {
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
- let child = this.childNodes[childId++];
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
- let sendContent = () => {
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
- let createStream = () => {
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
- let chunks = [];
842
+ const chunks = [];
760
843
  let chunklen = 0;
761
844
  let returned = false;
762
- let sourceStream = this._getStream(this.content);
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
- let raw = this._getStream(this._raw);
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._parseAddresses(envelope.from), list);
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._parseAddresses(envelope[key]), this._envelope.to);
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
- let standardFields = ['to', 'cc', 'bcc', 'from'];
850
- Object.keys(envelope).forEach(key => {
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
- let addresses = {};
944
+ const addresses = {};
945
+ const seenByKey = new Map();
866
946
 
867
947
  this._headers.forEach(header => {
868
- let key = header.key.toLowerCase();
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
- let envelope = {
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
- let list = [];
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 (err) {
967
- contentStream.emit('error', err);
1073
+ } catch (_err) {
1074
+ contentStream.emit('error', _err);
968
1075
  }
969
1076
  });
970
1077
 
971
1078
  return contentStream;
972
- } else if (typeof content.pipe === 'function') {
1079
+ }
1080
+
1081
+ if (typeof content.pipe === 'function') {
973
1082
  // assume as stream
974
1083
  return content;
975
- } else if (content && typeof content.path === 'string' && !content.href) {
976
- if (this.disableFileAccess) {
1084
+ }
1085
+
1086
+ if (content && typeof content.path === 'string' && !content.href) {
1087
+ if (this._accessDisabled('disableFileAccess')) {
977
1088
  contentStream = new PassThrough();
978
- 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
+ });
979
1094
  return contentStream;
980
1095
  }
981
1096
  // read file
982
1097
  return fs.createReadStream(content.path);
983
- } else if (content && typeof content.href === 'string') {
984
- if (this.disableUrlAccess) {
1098
+ }
1099
+
1100
+ if (content && typeof content.href === 'string') {
1101
+ if (this._accessDisabled('disableUrlAccess')) {
985
1102
  contentStream = new PassThrough();
986
- 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
+ });
987
1108
  return contentStream;
988
1109
  }
989
- // fetch URL
990
- return nmfetch(content.href, { headers: content.httpHeaders });
991
- } else {
992
- // pass string or buffer content as a stream
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
- return [].concat.apply(
1015
- [],
1016
- [].concat(addresses).map(address => {
1017
- // eslint-disable-line prefer-spread
1018
- if (address && address.address) {
1019
- address.address = this._normalizeAddress(address.address);
1020
- address.name = address.name || '';
1021
- 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;
1022
1149
  }
1023
- return addressparser(address);
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
- 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, '');
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._encodeWords(value);
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
- // encodeWords only encodes if needed, otherwise the original string is returned
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
- let values = [];
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
- values.push(address.address.indexOf(' ') >= 0 ? `<${address.address}>` : `${address.address}`);
1174
- } 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 {
1175
1384
  values.push(`${this._encodeAddressName(address.name)} <${address.address}>`);
1176
1385
  }
1177
1386
 
1178
- if (address.address) {
1179
- if (!uniqueList.filter(a => a.address === address.address).length) {
1180
- uniqueList.push(address);
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
- 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();
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
- let lastAt = address.lastIndexOf('@');
1205
- if (lastAt < 0) {
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
- let user = address.substr(0, lastAt);
1211
- 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 '@'.
1212
1434
 
1213
- // Usernames are not touched and are kept as is even if these include unicode
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
- 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);
1219
1440
 
1220
1441
  try {
1221
- encodedDomain = punycode.toASCII(domain.toLowerCase());
1222
- } catch (err) {
1223
- // keep as is?
1442
+ encodedDomain = normalizeDomain(domain.toLowerCase(), smtputf8);
1443
+ } catch (_err) {
1444
+ // keep domain as supplied
1224
1445
  }
1225
1446
 
1226
- if (user.indexOf(' ') >= 0) {
1227
- if (user.charAt(0) !== '"') {
1228
- user = '"' + user;
1229
- }
1230
- if (user.substr(-1) !== '"') {
1231
- user = user + '"';
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 `${user}@${encodedDomain}`;
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 '"' + name.replace(/([\\"])/g, '\\$1') + '"';
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
- let encoding = this.textEncoding;
1278
- let latinLen;
1279
- let nonLatinLen;
1280
-
1281
- if (!encoding) {
1282
- // count latin alphabet symbols and 8-bit range symbols + control symbols
1283
- // if there are more latin characters, then use quoted-printable
1284
- // encoding, otherwise use base64
1285
- nonLatinLen = (value.match(/[\x00-\x08\x0B\x0C\x0E-\x1F\u0080-\uFFFF]/g) || []).length; // eslint-disable-line no-control-regex
1286
- latinLen = (value.match(/[a-z]/gi) || []).length;
1287
- // if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
1288
- 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
+ }
1289
1540
  }
1290
- return encoding;
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
  /**