@nocobase/plugin-workflow-mailer 2.2.20 → 2.2.21

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 (173) hide show
  1. package/dist/externalVersion.js +6 -6
  2. package/dist/node_modules/joi/package.json +1 -1
  3. package/dist/node_modules/nodemailer/dist/cjs/addressparser/index.d.ts +59 -0
  4. package/dist/node_modules/nodemailer/dist/cjs/addressparser/index.js +790 -0
  5. package/dist/node_modules/nodemailer/dist/cjs/base64/index.d.ts +36 -0
  6. package/dist/node_modules/nodemailer/dist/cjs/base64/index.js +119 -0
  7. package/dist/node_modules/nodemailer/dist/cjs/dkim/index.d.ts +52 -0
  8. package/dist/node_modules/nodemailer/dist/cjs/dkim/index.js +228 -0
  9. package/dist/node_modules/nodemailer/dist/cjs/dkim/message-parser.d.ts +48 -0
  10. package/dist/node_modules/nodemailer/dist/cjs/dkim/message-parser.js +180 -0
  11. package/dist/node_modules/nodemailer/dist/cjs/dkim/relaxed-body.d.ts +36 -0
  12. package/dist/node_modules/nodemailer/dist/cjs/dkim/relaxed-body.js +148 -0
  13. package/dist/node_modules/nodemailer/dist/cjs/dkim/sign.d.ts +52 -0
  14. package/dist/node_modules/nodemailer/dist/cjs/dkim/sign.js +165 -0
  15. package/dist/node_modules/nodemailer/dist/cjs/errors.d.ts +96 -0
  16. package/dist/node_modules/nodemailer/dist/cjs/errors.js +70 -0
  17. package/dist/node_modules/nodemailer/dist/cjs/fetch/cookies.d.ts +107 -0
  18. package/dist/node_modules/nodemailer/dist/cjs/fetch/cookies.js +282 -0
  19. package/dist/node_modules/nodemailer/dist/cjs/fetch/index.d.ts +65 -0
  20. package/dist/node_modules/nodemailer/dist/cjs/fetch/index.js +384 -0
  21. package/dist/node_modules/nodemailer/dist/cjs/json-transport/index.d.ts +64 -0
  22. package/dist/node_modules/nodemailer/dist/cjs/json-transport/index.js +99 -0
  23. package/dist/node_modules/nodemailer/dist/cjs/mail-composer/index.d.ts +174 -0
  24. package/dist/node_modules/nodemailer/dist/cjs/mail-composer/index.js +613 -0
  25. package/dist/node_modules/nodemailer/dist/cjs/mailer/index.d.ts +237 -0
  26. package/dist/node_modules/nodemailer/dist/cjs/mailer/index.js +415 -0
  27. package/dist/node_modules/nodemailer/dist/cjs/mailer/mail-message.d.ts +97 -0
  28. package/dist/node_modules/nodemailer/dist/cjs/mailer/mail-message.js +342 -0
  29. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/index.d.ts +161 -0
  30. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/index.js +787 -0
  31. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/mime-types.d.ts +2 -0
  32. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/mime-types.js +2103 -0
  33. package/dist/node_modules/nodemailer/dist/cjs/mime-node/index.d.ts +333 -0
  34. package/dist/node_modules/nodemailer/dist/cjs/mime-node/index.js +1493 -0
  35. package/dist/node_modules/nodemailer/dist/cjs/mime-node/last-newline.d.ts +5 -0
  36. package/dist/node_modules/nodemailer/dist/cjs/mime-node/last-newline.js +32 -0
  37. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-unix.d.ts +9 -0
  38. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-unix.js +40 -0
  39. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-windows.d.ts +10 -0
  40. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-windows.js +47 -0
  41. package/dist/node_modules/nodemailer/dist/cjs/nodemailer.d.ts +124 -0
  42. package/dist/node_modules/nodemailer/dist/cjs/nodemailer.js +1 -0
  43. package/dist/node_modules/nodemailer/dist/cjs/package-info.d.ts +3 -0
  44. package/dist/node_modules/nodemailer/dist/cjs/package-info.js +7 -0
  45. package/dist/node_modules/nodemailer/dist/cjs/package.json +3 -0
  46. package/dist/node_modules/nodemailer/dist/cjs/punycode/index.d.ts +73 -0
  47. package/dist/node_modules/nodemailer/{lib → dist/cjs}/punycode/index.js +62 -111
  48. package/dist/node_modules/nodemailer/dist/cjs/qp/index.d.ts +32 -0
  49. package/dist/node_modules/nodemailer/dist/cjs/qp/index.js +207 -0
  50. package/dist/node_modules/nodemailer/dist/cjs/sendmail-transport/index.d.ts +69 -0
  51. package/dist/node_modules/nodemailer/dist/cjs/sendmail-transport/index.js +210 -0
  52. package/dist/node_modules/nodemailer/dist/cjs/ses-transport/index.d.ts +112 -0
  53. package/dist/node_modules/nodemailer/dist/cjs/ses-transport/index.js +252 -0
  54. package/dist/node_modules/nodemailer/dist/cjs/shared/index.d.ts +260 -0
  55. package/dist/node_modules/nodemailer/dist/cjs/shared/index.js +680 -0
  56. package/dist/node_modules/nodemailer/dist/cjs/shared/objects.d.ts +23 -0
  57. package/dist/node_modules/nodemailer/dist/cjs/shared/objects.js +43 -0
  58. package/dist/node_modules/nodemailer/dist/cjs/shared/url.d.ts +21 -0
  59. package/dist/node_modules/nodemailer/dist/cjs/shared/url.js +255 -0
  60. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/data-stream.d.ts +15 -0
  61. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/data-stream.js +108 -0
  62. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.d.ts +47 -0
  63. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js +189 -0
  64. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/index.d.ts +416 -0
  65. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/index.js +1837 -0
  66. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/index.d.ts +133 -0
  67. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/index.js +569 -0
  68. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.d.ts +62 -0
  69. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.js +257 -0
  70. package/dist/node_modules/nodemailer/dist/cjs/smtp-transport/index.d.ts +156 -0
  71. package/dist/node_modules/nodemailer/dist/cjs/smtp-transport/index.js +371 -0
  72. package/dist/node_modules/nodemailer/dist/cjs/stream-transport/index.d.ts +65 -0
  73. package/dist/node_modules/nodemailer/dist/cjs/stream-transport/index.js +146 -0
  74. package/dist/node_modules/nodemailer/dist/cjs/well-known/index.d.ts +35 -0
  75. package/dist/node_modules/nodemailer/dist/cjs/well-known/index.js +41 -0
  76. package/dist/node_modules/nodemailer/dist/cjs/well-known/services.d.ts +2 -0
  77. package/dist/node_modules/nodemailer/dist/cjs/well-known/services.js +674 -0
  78. package/dist/node_modules/nodemailer/dist/cjs/xoauth2/index.d.ts +201 -0
  79. package/dist/node_modules/nodemailer/dist/cjs/xoauth2/index.js +426 -0
  80. package/dist/node_modules/nodemailer/dist/esm/addressparser/index.d.ts +59 -0
  81. package/dist/node_modules/nodemailer/{lib → dist/esm}/addressparser/index.js +361 -131
  82. package/dist/node_modules/nodemailer/dist/esm/base64/index.d.ts +36 -0
  83. package/dist/node_modules/nodemailer/{lib → dist/esm}/base64/index.js +31 -56
  84. package/dist/node_modules/nodemailer/dist/esm/dkim/index.d.ts +52 -0
  85. package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/index.js +49 -78
  86. package/dist/node_modules/nodemailer/dist/esm/dkim/message-parser.d.ts +48 -0
  87. package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/message-parser.js +64 -42
  88. package/dist/node_modules/nodemailer/dist/esm/dkim/relaxed-body.d.ts +36 -0
  89. package/dist/node_modules/nodemailer/dist/esm/dkim/relaxed-body.js +141 -0
  90. package/dist/node_modules/nodemailer/dist/esm/dkim/sign.d.ts +52 -0
  91. package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/sign.js +50 -46
  92. package/dist/node_modules/nodemailer/dist/esm/errors.d.ts +96 -0
  93. package/dist/node_modules/nodemailer/{lib → dist/esm}/errors.js +26 -18
  94. package/dist/node_modules/nodemailer/dist/esm/fetch/cookies.d.ts +107 -0
  95. package/dist/node_modules/nodemailer/dist/esm/fetch/cookies.js +242 -0
  96. package/dist/node_modules/nodemailer/dist/esm/fetch/index.d.ts +65 -0
  97. package/dist/node_modules/nodemailer/{lib → dist/esm}/fetch/index.js +80 -147
  98. package/dist/node_modules/nodemailer/dist/esm/json-transport/index.d.ts +64 -0
  99. package/dist/node_modules/nodemailer/{lib → dist/esm}/json-transport/index.js +19 -39
  100. package/dist/node_modules/nodemailer/dist/esm/mail-composer/index.d.ts +174 -0
  101. package/dist/node_modules/nodemailer/{lib → dist/esm}/mail-composer/index.js +199 -265
  102. package/dist/node_modules/nodemailer/dist/esm/mailer/index.d.ts +237 -0
  103. package/dist/node_modules/nodemailer/{lib → dist/esm}/mailer/index.js +128 -234
  104. package/dist/node_modules/nodemailer/dist/esm/mailer/mail-message.d.ts +97 -0
  105. package/dist/node_modules/nodemailer/{lib → dist/esm}/mailer/mail-message.js +62 -114
  106. package/dist/node_modules/nodemailer/dist/esm/mime-funcs/index.d.ts +161 -0
  107. package/dist/node_modules/nodemailer/dist/esm/mime-funcs/index.js +738 -0
  108. package/dist/node_modules/nodemailer/dist/esm/mime-funcs/mime-types.d.ts +2 -0
  109. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-funcs/mime-types.js +29 -42
  110. package/dist/node_modules/nodemailer/dist/esm/mime-node/index.d.ts +333 -0
  111. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/index.js +259 -372
  112. package/dist/node_modules/nodemailer/dist/esm/mime-node/last-newline.d.ts +5 -0
  113. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/last-newline.js +4 -10
  114. package/dist/node_modules/nodemailer/dist/esm/mime-node/le-unix.d.ts +9 -0
  115. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/le-unix.js +6 -11
  116. package/dist/node_modules/nodemailer/dist/esm/mime-node/le-windows.d.ts +10 -0
  117. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/le-windows.js +6 -13
  118. package/dist/node_modules/nodemailer/dist/esm/nodemailer.d.ts +124 -0
  119. package/dist/node_modules/nodemailer/dist/esm/nodemailer.js +166 -0
  120. package/dist/node_modules/nodemailer/dist/esm/package-info.d.ts +3 -0
  121. package/dist/node_modules/nodemailer/dist/esm/package-info.js +4 -0
  122. package/dist/node_modules/nodemailer/dist/esm/package.json +3 -0
  123. package/dist/node_modules/nodemailer/dist/esm/punycode/index.d.ts +73 -0
  124. package/dist/node_modules/nodemailer/dist/esm/punycode/index.js +405 -0
  125. package/dist/node_modules/nodemailer/dist/esm/qp/index.d.ts +32 -0
  126. package/dist/node_modules/nodemailer/{lib → dist/esm}/qp/index.js +38 -67
  127. package/dist/node_modules/nodemailer/dist/esm/sendmail-transport/index.d.ts +69 -0
  128. package/dist/node_modules/nodemailer/{lib → dist/esm}/sendmail-transport/index.js +51 -101
  129. package/dist/node_modules/nodemailer/dist/esm/ses-transport/index.d.ts +112 -0
  130. package/dist/node_modules/nodemailer/dist/esm/ses-transport/index.js +212 -0
  131. package/dist/node_modules/nodemailer/dist/esm/shared/index.d.ts +260 -0
  132. package/dist/node_modules/nodemailer/{lib → dist/esm}/shared/index.js +156 -272
  133. package/dist/node_modules/nodemailer/dist/esm/shared/objects.d.ts +23 -0
  134. package/dist/node_modules/nodemailer/{lib → dist/esm}/shared/objects.js +13 -17
  135. package/dist/node_modules/nodemailer/dist/esm/shared/url.d.ts +21 -0
  136. package/dist/node_modules/nodemailer/dist/esm/shared/url.js +214 -0
  137. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/data-stream.d.ts +15 -0
  138. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/data-stream.js +103 -0
  139. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.d.ts +47 -0
  140. package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-connection/http-proxy-client.js +49 -74
  141. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/index.d.ts +416 -0
  142. package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-connection/index.js +574 -794
  143. package/dist/node_modules/nodemailer/dist/esm/smtp-pool/index.d.ts +133 -0
  144. package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-pool/index.js +172 -284
  145. package/dist/node_modules/nodemailer/dist/esm/smtp-pool/pool-resource.d.ts +62 -0
  146. package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-pool/pool-resource.js +57 -97
  147. package/dist/node_modules/nodemailer/dist/esm/smtp-transport/index.d.ts +156 -0
  148. package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-transport/index.js +81 -179
  149. package/dist/node_modules/nodemailer/dist/esm/stream-transport/index.d.ts +65 -0
  150. package/dist/node_modules/nodemailer/{lib → dist/esm}/stream-transport/index.js +36 -74
  151. package/dist/node_modules/nodemailer/dist/esm/well-known/index.d.ts +35 -0
  152. package/dist/node_modules/nodemailer/{lib → dist/esm}/well-known/index.js +5 -16
  153. package/dist/node_modules/nodemailer/dist/esm/well-known/services.d.ts +2 -0
  154. package/dist/node_modules/nodemailer/dist/esm/well-known/services.js +671 -0
  155. package/dist/node_modules/nodemailer/dist/esm/xoauth2/index.d.ts +201 -0
  156. package/dist/node_modules/nodemailer/{lib → dist/esm}/xoauth2/index.js +125 -187
  157. package/dist/node_modules/nodemailer/{lib → dist}/well-known/services.json +9 -2
  158. package/dist/node_modules/nodemailer/package.json +1 -1
  159. package/package.json +3 -3
  160. package/dist/node_modules/nodemailer/.gitattributes +0 -6
  161. package/dist/node_modules/nodemailer/.ncurc.js +0 -9
  162. package/dist/node_modules/nodemailer/.prettierignore +0 -8
  163. package/dist/node_modules/nodemailer/.prettierrc +0 -12
  164. package/dist/node_modules/nodemailer/.prettierrc.js +0 -10
  165. package/dist/node_modules/nodemailer/.release-please-config.json +0 -9
  166. package/dist/node_modules/nodemailer/eslint.config.js +0 -88
  167. package/dist/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
  168. package/dist/node_modules/nodemailer/lib/fetch/cookies.js +0 -276
  169. package/dist/node_modules/nodemailer/lib/mime-funcs/index.js +0 -671
  170. package/dist/node_modules/nodemailer/lib/nodemailer.js +0 -1
  171. package/dist/node_modules/nodemailer/lib/ses-transport/index.js +0 -266
  172. package/dist/node_modules/nodemailer/lib/shared/url.js +0 -151
  173. package/dist/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -105
@@ -1,41 +1,31 @@
1
1
  /* eslint no-undefined: 0, prefer-spread: 0, no-control-regex: 0 */
2
-
3
- 'use strict';
4
-
5
- const crypto = require('crypto');
6
- const fs = require('fs');
7
- const punycode = require('../punycode');
8
- const { PassThrough } = require('stream');
9
- const shared = require('../shared');
10
- const urlModule = require('url');
11
-
12
- const mimeFuncs = require('../mime-funcs');
13
- const qp = require('../qp');
14
- const base64 = require('../base64');
15
- const addressparser = require('../addressparser');
16
- const nmfetch = require('../fetch');
17
- const errors = require('../errors');
18
- const LastNewline = require('./last-newline');
19
-
20
- const LeWindows = require('./le-windows');
21
- const LeUnix = require('./le-unix');
22
-
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import * as punycode from '../punycode/index.js';
5
+ import { PassThrough } from 'node:stream';
6
+ import * as shared from '../shared/index.js';
7
+ import urlModule from 'node:url';
8
+ import * as mimeFuncs from '../mime-funcs/index.js';
9
+ import * as qp from '../qp/index.js';
10
+ import * as base64 from '../base64/index.js';
11
+ import addressparser from '../addressparser/index.js';
12
+ import nmfetch from '../fetch/index.js';
13
+ import * as errors from '../errors.js';
14
+ import LastNewline from './last-newline.js';
15
+ import LeWindows from './le-windows.js';
16
+ import LeUnix from './le-unix.js';
23
17
  const FORMATTED_HEADERS = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
24
-
25
18
  // RFC 5321 atext, plus the non-ascii bytes that SMTPUTF8 (RFC 6531) adds to it. A local part
26
19
  // built from these, with '.' as a separator, is a dot-atom and can be emitted bare
27
20
  const ATEXT = "[A-Za-z0-9!#$%&'*+\\-/=?^_`{|}~\\x80-\\uFFFF]";
28
21
  const DOT_ATOM = new RegExp('^' + ATEXT + '+(?:\\.' + ATEXT + '+)*$');
29
-
30
22
  // A complete quoted-string: everything between the outer quotes is either a plain char or
31
23
  // a quoted-pair. Anchored, so a value that only starts and ends with a quote does not pass
32
24
  const QUOTED_STRING = /^"(?:[^"\\]|\\[\s\S])*"$/;
33
-
34
25
  // An address that carries no special anywhere can be emitted bare in a header, everything
35
26
  // else goes into angle brackets so that the header can not be read as more addresses than
36
27
  // the envelope carries
37
28
  const PLAIN_ADDRESS = /^[^\s"(),:;<>@[\\\]]+@[^\s"(),:;<>@[\\\]]+$/;
38
-
39
29
  // domainToASCII and domainToUnicode are WHATWG host parsers rather than plain IDNA
40
30
  // mappers, so they do more than map: they cut the host at '/', '\\', '?' and '#', drop C0
41
31
  // controls, and percent-decode. Handing them 'evil.example/mail.corp.example' returns the
@@ -43,7 +33,6 @@ const PLAIN_ADDRESS = /^[^\s"(),:;<>@[\\\]]+@[^\s"(),:;<>@[\\\]]+$/;
43
33
  // unroutable garbage into mail for a domain the sender never named. None of these
44
34
  // characters are legal in a domain, so keep them away from the mapper.
45
35
  const URL_PARSER_UNSAFE = /[/\\?#%\x00-\x20\x7F]/;
46
-
47
36
  /**
48
37
  * Encodes a domain the way browsers, the WHATWG URL Standard and DNS facing resolvers do,
49
38
  * which is with UTS-46 mapping applied before the Punycode step.
@@ -57,71 +46,78 @@ const URL_PARSER_UNSAFE = /[/\\?#%\x00-\x20\x7F]/;
57
46
  * '[127.0.0.1]' included, comes back empty and falls through to the bundled codec, which
58
47
  * leaves those as they were supplied.
59
48
  *
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
49
+ * @param domain Domain to encode, already lowercased by the caller
50
+ * @param toUnicode Return the U-label form instead of the A-label form
51
+ * @return Encoded domain
63
52
  */
64
53
  function normalizeDomain(domain, toUnicode) {
65
54
  // domainToASCII and domainToUnicode landed in Node 7, the bundled codec covers Node 6
66
55
  const mapper = toUnicode ? urlModule.domainToUnicode : urlModule.domainToASCII;
67
-
68
56
  if (typeof mapper === 'function' && !URL_PARSER_UNSAFE.test(domain)) {
69
57
  const mapped = mapper(domain);
70
58
  if (mapped) {
71
59
  return mapped;
72
60
  }
73
61
  }
74
-
75
62
  return toUnicode ? punycode.toUnicode(domain) : punycode.toASCII(domain);
76
63
  }
77
-
64
+ /**
65
+ * Removes the characters that must never reach a multipart delimiter line.
66
+ *
67
+ * A line break splits the delimiter, so the boundary declared in the header can never
68
+ * match it again and the parts go out as body lines instead. The other C0 controls and
69
+ * DEL do not split anything but must not be written either: RFC 5321 does not allow a
70
+ * NUL in DATA at all, and an MTA or a scanner that stops at one reads a different
71
+ * message than a client that does not, which is the same parser disagreement a split
72
+ * delimiter creates. Both sides are cleaned together, since the declared value and the
73
+ * delimiters come from this one result.
74
+ *
75
+ * @param value Value to clean
76
+ * @return Value with every control character removed
77
+ */
78
+ function _stripBoundaryControls(value) {
79
+ return value.replace(/[\x00-\x1f\x7f]+/g, '');
80
+ }
78
81
  /**
79
82
  * Creates a new mime tree node. Assumes 'multipart/*' as the content type
80
83
  * if it is a branch, anything else counts as leaf. If rootNode is missing from
81
84
  * the options, assumes this is the root.
82
85
  *
83
- * @param {String} contentType Define the content type for the node. Can be left blank for attachments (derived from filename)
84
- * @param {Object} [options] optional options
85
- * @param {Object} [options.rootNode] root node for this tree
86
- * @param {Object} [options.parentNode] immediate parent for this node
87
- * @param {Object} [options.filename] filename for an attachment node
88
- * @param {String} [options.baseBoundary] shared part of the unique multipart boundary
89
- * @param {Boolean} [options.keepBcc] If true, do not exclude Bcc from the generated headers
90
- * @param {Function} [options.normalizeHeaderKey] method to normalize header keys for custom caseing
91
- * @param {String} [options.textEncoding] either 'Q' (the default) or 'B'
86
+ * @param contentType Define the content type for the node. Can be left blank for attachments (derived from filename)
87
+ * @param [options] optional options
88
+ * @param [options.rootNode] root node for this tree
89
+ * @param [options.parentNode] immediate parent for this node
90
+ * @param [options.filename] filename for an attachment node
91
+ * @param [options.baseBoundary] shared part of the unique multipart boundary
92
+ * @param [options.keepBcc] If true, do not exclude Bcc from the generated headers
93
+ * @param [options.normalizeHeaderKey] method to normalize header keys for custom caseing
94
+ * @param [options.textEncoding] either 'Q' (the default) or 'B'
92
95
  */
93
96
  class MimeNode {
94
97
  constructor(contentType, options) {
95
98
  this.nodeCounter = 0;
96
-
97
99
  options = options || {};
98
-
99
100
  /**
100
- * shared part of the unique multipart boundary
101
+ * shared part of the unique multipart boundary. Control characters are dropped
102
+ * here rather than at the delimiter, see _stripBoundaryControls
101
103
  */
102
- this.baseBoundary = options.baseBoundary || crypto.randomBytes(8).toString('hex');
103
- this.boundaryPrefix = options.boundaryPrefix || '--_NmP';
104
-
104
+ this.baseBoundary = _stripBoundaryControls(options.baseBoundary || crypto.randomBytes(8).toString('hex'));
105
+ this.boundaryPrefix = _stripBoundaryControls(options.boundaryPrefix || '--_NmP');
105
106
  this.disableFileAccess = !!options.disableFileAccess;
106
107
  this.disableUrlAccess = !!options.disableUrlAccess;
107
-
108
108
  this.normalizeHeaderKey = options.normalizeHeaderKey;
109
-
110
109
  /**
111
110
  * If date headers is missing and current node is the root, this value is used instead
112
111
  */
113
112
  this.date = options.parentNode ? null : new Date();
114
-
115
113
  /**
116
114
  * Root node for current mime tree
117
115
  */
118
116
  this.rootNode = options.rootNode || this;
119
-
120
117
  /**
121
118
  * If true include Bcc in generated headers (if available)
122
119
  */
123
120
  this.keepBcc = !!options.keepBcc;
124
-
125
121
  /**
126
122
  * If filename is specified but contentType is not (probably an attachment)
127
123
  * detect the content type from filename extension
@@ -135,80 +131,66 @@ class MimeNode {
135
131
  contentType = mimeFuncs.detectMimeType(this.filename.split('.').pop());
136
132
  }
137
133
  }
138
-
139
134
  /**
140
135
  * Indicates which encoding should be used for header strings: "Q" or "B"
141
136
  */
142
137
  this.textEncoding = (options.textEncoding || '').toString().trim().charAt(0).toUpperCase();
143
-
144
138
  /**
145
139
  * Immediate parent for this node (or undefined if not set)
146
140
  */
147
141
  this.parentNode = options.parentNode;
148
-
149
142
  /**
150
143
  * Hostname for default message-id values
151
144
  */
152
145
  this.hostname = options.hostname;
153
-
154
146
  /**
155
147
  * If set to 'win' then uses \r\n, if 'linux' then \n. If not set (or `raw` is used) then newlines are kept as is.
156
148
  */
157
149
  this.newline = options.newline;
158
-
159
150
  /**
160
151
  * An array for possible child nodes
161
152
  */
162
153
  this.childNodes = [];
163
-
164
154
  /**
165
155
  * Used for generating unique boundaries (prepended to the shared base)
166
156
  */
167
157
  this._nodeId = ++this.rootNode.nodeCounter;
168
-
169
158
  /**
170
159
  * A list of header values for this node in the form of [{key:'', value:''}]
171
160
  */
172
161
  this._headers = [];
173
-
174
162
  /**
175
163
  * True if the content only uses ASCII printable characters
176
164
  * @type {Boolean}
177
165
  */
178
166
  this._isPlainText = false;
179
-
180
167
  /**
181
168
  * True if the content is plain text but has longer lines than allowed
182
169
  * @type {Boolean}
183
170
  */
184
171
  this._hasLongLines = false;
185
-
186
172
  /**
187
173
  * If set, use instead this value for envelopes instead of generating one
188
174
  * @type {Boolean}
189
175
  */
190
176
  this._envelope = false;
191
-
192
177
  /**
193
178
  * If set then use this value as the stream content instead of building it
194
179
  * @type {String|Buffer|Stream}
195
180
  */
196
181
  this._raw = false;
197
-
198
182
  /**
199
183
  * Additional transform streams that the message will be piped before
200
184
  * exposing by createReadStream
201
185
  * @type {Array}
202
186
  */
203
187
  this._transforms = [];
204
-
205
188
  /**
206
189
  * Additional process functions that the message will be piped through before
207
190
  * exposing by createReadStream. These functions are run after transforms
208
191
  * @type {Array}
209
192
  */
210
193
  this._processFuncs = [];
211
-
212
194
  /**
213
195
  * If content type is set (or derived from the filename) add it to headers
214
196
  */
@@ -216,15 +198,13 @@ class MimeNode {
216
198
  this.setHeader('Content-Type', contentType);
217
199
  }
218
200
  }
219
-
220
201
  /////// PUBLIC METHODS
221
-
222
202
  /**
223
203
  * Creates and appends a child node.Arguments provided are passed to MimeNode constructor
224
204
  *
225
- * @param {String} [contentType] Optional content type
226
- * @param {Object} [options] Optional options object
227
- * @return {Object} Created node object
205
+ * @param [contentType] Optional content type
206
+ * @param [options] Optional options object
207
+ * @return Created node object
228
208
  */
229
209
  createChild(contentType, options) {
230
210
  if (!options && typeof contentType === 'object') {
@@ -235,13 +215,12 @@ class MimeNode {
235
215
  this.appendChild(node);
236
216
  return node;
237
217
  }
238
-
239
218
  /**
240
219
  * Appends an existing node to the mime tree. Removes the node from an existing
241
220
  * tree if needed
242
221
  *
243
- * @param {Object} childNode node to be appended
244
- * @return {Object} Appended node object
222
+ * @param childNode node to be appended
223
+ * @return Appended node object
245
224
  */
246
225
  appendChild(childNode) {
247
226
  // Take the node out of the tree it is in first. Leaving it there keeps it in that
@@ -251,55 +230,45 @@ class MimeNode {
251
230
  if (childNode.parentNode && childNode.parentNode !== this) {
252
231
  childNode.remove();
253
232
  }
254
-
255
233
  if (childNode.rootNode !== this.rootNode) {
256
234
  childNode.rootNode = this.rootNode;
257
235
  childNode._nodeId = ++this.rootNode.nodeCounter;
258
236
  }
259
-
260
237
  childNode.parentNode = this;
261
-
262
238
  this.childNodes.push(childNode);
263
239
  return childNode;
264
240
  }
265
-
266
241
  /**
267
242
  * Replaces current node with another node
268
243
  *
269
- * @param {Object} node Replacement node
270
- * @return {Object} Replacement node
244
+ * @param node Replacement node
245
+ * @return Replacement node
271
246
  */
272
247
  replace(node) {
273
248
  if (node === this) {
274
249
  return this;
275
250
  }
276
-
277
251
  this.parentNode.childNodes.forEach((childNode, i) => {
278
252
  if (childNode === this) {
279
253
  node.rootNode = this.rootNode;
280
254
  node.parentNode = this.parentNode;
281
255
  node._nodeId = this._nodeId;
282
-
283
256
  this.rootNode = this;
284
257
  this.parentNode = undefined;
285
-
286
258
  node.parentNode.childNodes[i] = node;
287
259
  }
288
260
  });
289
-
290
261
  return node;
291
262
  }
292
-
293
263
  /**
294
264
  * Removes current node from the mime tree
295
265
  *
296
- * @return {Object} removed node
266
+ * @return removed node
297
267
  */
298
268
  remove() {
299
269
  if (!this.parentNode) {
300
270
  return this;
301
271
  }
302
-
303
272
  for (let i = this.parentNode.childNodes.length - 1; i >= 0; i--) {
304
273
  if (this.parentNode.childNodes[i] === this) {
305
274
  this.parentNode.childNodes.splice(i, 1);
@@ -309,30 +278,30 @@ class MimeNode {
309
278
  }
310
279
  }
311
280
  }
312
-
313
281
  /**
314
282
  * Sets a header value. If the value for selected key exists, it is overwritten.
315
283
  * You can set multiple values as well by using [{key:'', value:''}] or
316
284
  * {key: 'value'} as the first argument.
317
285
  *
318
- * @param {String|Array|Object} key Header key or a list of key value pairs
319
- * @param {String} value Header value
320
- * @return {Object} current node
286
+ * @param key Header key or a list of key value pairs
287
+ * @param value Header value
288
+ * @return current node
321
289
  */
322
290
  setHeader(key, value) {
323
291
  let added = false;
324
-
325
292
  // Allow setting multiple headers at once
326
293
  if (!value && key && typeof key === 'object') {
327
294
  // allow {key:'content-type', value: 'text/plain'}
328
295
  if (key.key && 'value' in key) {
329
296
  this.setHeader(key.key, key.value);
330
- } else if (Array.isArray(key)) {
297
+ }
298
+ else if (Array.isArray(key)) {
331
299
  // allow [{key:'content-type', value: 'text/plain'}]
332
300
  key.forEach(i => {
333
301
  this.setHeader(i.key, i.value);
334
302
  });
335
- } else {
303
+ }
304
+ else {
336
305
  // allow {'content-type': 'text/plain'}
337
306
  Object.keys(key).forEach(i => {
338
307
  this.setHeader(i, key[i]);
@@ -340,14 +309,11 @@ class MimeNode {
340
309
  }
341
310
  return this;
342
311
  }
343
-
344
312
  key = this._normalizeHeaderKey(key);
345
-
346
313
  const headerValue = {
347
314
  key,
348
315
  value
349
316
  };
350
-
351
317
  // Check if the value exists and overwrite
352
318
  for (let i = 0, len = this._headers.length; i < len; i++) {
353
319
  if (this._headers[i].key === key) {
@@ -355,7 +321,8 @@ class MimeNode {
355
321
  // replace the first match
356
322
  this._headers[i] = headerValue;
357
323
  added = true;
358
- } else {
324
+ }
325
+ else {
359
326
  // remove following matches
360
327
  this._headers.splice(i, 1);
361
328
  i--;
@@ -363,24 +330,21 @@ class MimeNode {
363
330
  }
364
331
  }
365
332
  }
366
-
367
333
  // match not found, append the value
368
334
  if (!added) {
369
335
  this._headers.push(headerValue);
370
336
  }
371
-
372
337
  return this;
373
338
  }
374
-
375
339
  /**
376
340
  * Adds a header value. If the value for selected key exists, the value is appended
377
341
  * as a new field and old one is not touched.
378
342
  * You can set multiple values as well by using [{key:'', value:''}] or
379
343
  * {key: 'value'} as the first argument.
380
344
  *
381
- * @param {String|Array|Object} key Header key or a list of key value pairs
382
- * @param {String} value Header value
383
- * @return {Object} current node
345
+ * @param key Header key or a list of key value pairs
346
+ * @param value Header value
347
+ * @return current node
384
348
  */
385
349
  addHeader(key, value) {
386
350
  // Allow setting multiple headers at once
@@ -388,38 +352,38 @@ class MimeNode {
388
352
  // allow {key:'content-type', value: 'text/plain'}
389
353
  if (key.key && key.value) {
390
354
  this.addHeader(key.key, key.value);
391
- } else if (Array.isArray(key)) {
355
+ }
356
+ else if (Array.isArray(key)) {
392
357
  // allow [{key:'content-type', value: 'text/plain'}]
393
358
  key.forEach(i => {
394
359
  this.addHeader(i.key, i.value);
395
360
  });
396
- } else {
361
+ }
362
+ else {
397
363
  // allow {'content-type': 'text/plain'}
398
364
  Object.keys(key).forEach(i => {
399
365
  this.addHeader(i, key[i]);
400
366
  });
401
367
  }
402
368
  return this;
403
- } else if (Array.isArray(value)) {
369
+ }
370
+ else if (Array.isArray(value)) {
404
371
  value.forEach(val => {
405
372
  this.addHeader(key, val);
406
373
  });
407
374
  return this;
408
375
  }
409
-
410
376
  this._headers.push({
411
377
  key: this._normalizeHeaderKey(key),
412
378
  value
413
379
  });
414
-
415
380
  return this;
416
381
  }
417
-
418
382
  /**
419
383
  * Retrieves the first mathcing value of a selected key
420
384
  *
421
- * @param {String} key Key to search for
422
- * @retun {String} Value for the key
385
+ * @param key Key to search for
386
+ * @retun Value for the key
423
387
  */
424
388
  getHeader(key) {
425
389
  key = this._normalizeHeaderKey(key);
@@ -429,14 +393,13 @@ class MimeNode {
429
393
  }
430
394
  }
431
395
  }
432
-
433
396
  /**
434
397
  * Sets body content for current node. If the value is a string, charset is added automatically
435
398
  * to Content-Type (if it is text/*). If the value is a Buffer, you need to specify
436
399
  * the charset yourself
437
400
  *
438
- * @param (String|Buffer) content Body content
439
- * @return {Object} current node
401
+ * @param content Body content
402
+ * @return current node
440
403
  */
441
404
  setContent(content) {
442
405
  this.content = content;
@@ -448,7 +411,8 @@ class MimeNode {
448
411
  this.content = err;
449
412
  };
450
413
  this.content.once('error', this._contentErrorHandler);
451
- } else if (typeof this.content === 'string') {
414
+ }
415
+ else if (typeof this.content === 'string') {
452
416
  this._isPlainText = mimeFuncs.isPlainText(this.content);
453
417
  if (this._isPlainText && mimeFuncs.hasLongerLines(this.content, 76)) {
454
418
  // If there are lines longer than 76 symbols/bytes do not use 7bit
@@ -457,59 +421,48 @@ class MimeNode {
457
421
  }
458
422
  return this;
459
423
  }
460
-
461
424
  build(callback) {
462
425
  let promise;
463
-
464
426
  if (!callback) {
465
427
  promise = new Promise((resolve, reject) => {
466
428
  callback = shared.callbackPromise(resolve, reject);
467
429
  });
468
430
  }
469
-
431
+ const done = callback;
470
432
  const stream = this.createReadStream();
471
433
  const buf = [];
472
434
  let buflen = 0;
473
435
  let returned = false;
474
-
475
436
  stream.on('readable', () => {
476
437
  let chunk;
477
-
478
438
  while ((chunk = stream.read()) !== null) {
479
439
  buf.push(chunk);
480
440
  buflen += chunk.length;
481
441
  }
482
442
  });
483
-
484
443
  stream.once('error', err => {
485
444
  if (returned) {
486
445
  return;
487
446
  }
488
447
  returned = true;
489
-
490
- return callback(err);
448
+ return done(err);
491
449
  });
492
-
493
- stream.once('end', chunk => {
450
+ stream.once('end', (chunk) => {
494
451
  if (returned) {
495
452
  return;
496
453
  }
497
454
  returned = true;
498
-
499
455
  if (chunk && chunk.length) {
500
456
  buf.push(chunk);
501
457
  buflen += chunk.length;
502
458
  }
503
- return callback(null, Buffer.concat(buf, buflen));
459
+ return done(null, Buffer.concat(buf, buflen));
504
460
  });
505
-
506
461
  return promise;
507
462
  }
508
-
509
463
  getTransferEncoding() {
510
464
  let transferEncoding = false;
511
465
  const contentType = (this.getHeader('Content-Type') || '').toString().toLowerCase().trim();
512
-
513
466
  if (this.content) {
514
467
  transferEncoding = (this.getHeader('Content-Transfer-Encoding') || '').toString().toLowerCase().trim();
515
468
  if (!transferEncoding || !['base64', 'quoted-printable'].includes(transferEncoding)) {
@@ -517,51 +470,47 @@ class MimeNode {
517
470
  // If there are no special symbols, no need to modify the text
518
471
  if (this._isPlainText && !this._hasLongLines) {
519
472
  transferEncoding = '7bit';
520
- } else if (typeof this.content === 'string' || this.content instanceof Buffer) {
473
+ }
474
+ else if (typeof this.content === 'string' || this.content instanceof Buffer) {
521
475
  // detect preferred encoding for string value
522
476
  transferEncoding = this._getTextEncoding(this.content) === 'Q' ? 'quoted-printable' : 'base64';
523
- } else {
477
+ }
478
+ else {
524
479
  // we can not check content for a stream, so either use preferred encoding or fallback to QP
525
480
  transferEncoding = this.textEncoding === 'B' ? 'base64' : 'quoted-printable';
526
481
  }
527
- } else if (!/^(multipart|message)\//i.test(contentType)) {
482
+ }
483
+ else if (!/^(multipart|message)\//i.test(contentType)) {
528
484
  transferEncoding = transferEncoding || 'base64';
529
485
  }
530
486
  }
531
487
  }
532
488
  return transferEncoding;
533
489
  }
534
-
535
490
  /**
536
491
  * Builds the header block for the mime node. Append \r\n\r\n before writing the content
537
492
  *
538
- * @returns {String} Headers
493
+ * @returns Headers
539
494
  */
540
495
  buildHeaders() {
541
496
  const transferEncoding = this.getTransferEncoding();
542
497
  const headers = [];
543
-
544
498
  if (transferEncoding) {
545
499
  this.setHeader('Content-Transfer-Encoding', transferEncoding);
546
500
  }
547
-
548
501
  if (this.filename && !this.getHeader('Content-Disposition')) {
549
502
  this.setHeader('Content-Disposition', 'attachment');
550
503
  }
551
-
552
504
  // Ensure mandatory header fields
553
505
  if (this.rootNode === this) {
554
506
  if (!this.getHeader('Date')) {
555
507
  this.setHeader('Date', this.date.toUTCString().replace(/GMT/, '+0000'));
556
508
  }
557
-
558
509
  // ensure that Message-Id is present
559
510
  this.messageId();
560
-
561
511
  if (!this.getHeader('MIME-Version')) {
562
512
  this.setHeader('MIME-Version', '1.0');
563
513
  }
564
-
565
514
  // Ensure that Content-Type is the last header for the root node
566
515
  for (let i = this._headers.length - 2; i >= 0; i--) {
567
516
  const header = this._headers[i];
@@ -571,7 +520,6 @@ class MimeNode {
571
520
  }
572
521
  }
573
522
  }
574
-
575
523
  this._headers.forEach(header => {
576
524
  let key = header.key;
577
525
  let value = header.value;
@@ -579,7 +527,6 @@ class MimeNode {
579
527
  let param;
580
528
  const options = {};
581
529
  const formattedHeaders = FORMATTED_HEADERS;
582
-
583
530
  if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
584
531
  // the keys come from a caller supplied header object and `options.prepared`
585
532
  // below decides whether the value is emitted raw, so an own "__proto__" key
@@ -590,17 +537,16 @@ class MimeNode {
590
537
  return;
591
538
  }
592
539
  }
593
-
594
540
  if (options.prepared) {
595
541
  // header value is
596
542
  if (options.foldLines) {
597
543
  headers.push(mimeFuncs.foldLines(key + ': ' + value));
598
- } else {
544
+ }
545
+ else {
599
546
  headers.push(key + ': ' + value);
600
547
  }
601
548
  return;
602
549
  }
603
-
604
550
  switch (header.key) {
605
551
  case 'Content-Disposition':
606
552
  structured = mimeFuncs.parseHeaderValue(value);
@@ -609,27 +555,19 @@ class MimeNode {
609
555
  }
610
556
  value = mimeFuncs.buildHeaderValue(structured);
611
557
  break;
612
-
613
558
  case 'Content-Type':
614
559
  structured = mimeFuncs.parseHeaderValue(value);
615
-
616
560
  // the type token decides multipart and charset below, so clean it before
617
561
  // those run and not just on the way out, otherwise a control char makes
618
562
  // the checks miss and the header ends up claiming a type it is not set up for
619
563
  structured.value = (structured.value || '').toString().replace(/[\x00-\x1f\x7f]/g, '');
620
-
621
564
  this._handleContentType(structured);
622
-
623
- if (
624
- structured.value.match(/^text\/plain\b/) &&
565
+ if (structured.value.match(/^text\/plain\b/) &&
625
566
  typeof this.content === 'string' &&
626
- /[\u0080-\uFFFF]/.test(this.content)
627
- ) {
567
+ /[\u0080-\uFFFF]/.test(this.content)) {
628
568
  structured.params.charset = 'utf-8';
629
569
  }
630
-
631
570
  value = mimeFuncs.buildHeaderValue(structured);
632
-
633
571
  if (this.filename) {
634
572
  // add support for non-compliant clients like QQ webmail
635
573
  // we can't build the value with buildHeaderValue as the value is non standard and
@@ -640,7 +578,6 @@ class MimeNode {
640
578
  param = /[\x00-\x1f\x7f]/.test(this.filename)
641
579
  ? mimeFuncs.encodeWord(this.filename, this._getTextEncoding(this.filename), 52)
642
580
  : this._encodeWords(this.filename);
643
-
644
581
  if (param !== this.filename || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(param)) {
645
582
  // include value in quotes if needed, escaping backslashes and quotes as
646
583
  // quoted-pairs exactly like buildHeaderValue does for filename=, otherwise
@@ -650,7 +587,6 @@ class MimeNode {
650
587
  value += '; name=' + param;
651
588
  }
652
589
  break;
653
-
654
590
  case 'Bcc':
655
591
  if (!this.keepBcc) {
656
592
  // skip BCC values
@@ -658,14 +594,11 @@ class MimeNode {
658
594
  }
659
595
  break;
660
596
  }
661
-
662
597
  value = this._encodeHeaderValue(key, value);
663
-
664
598
  // skip empty lines
665
599
  if (!(value || '').toString().trim()) {
666
600
  return;
667
601
  }
668
-
669
602
  if (typeof this.normalizeHeaderKey === 'function') {
670
603
  const normalized = this.normalizeHeaderKey(key, value);
671
604
  // the result replaces the key on the way into the header, so it gets the same
@@ -676,26 +609,21 @@ class MimeNode {
676
609
  key = cleaned;
677
610
  }
678
611
  }
679
-
680
612
  headers.push(mimeFuncs.foldLines(key + ': ' + value, 76));
681
613
  });
682
-
683
614
  return headers.join('\r\n');
684
615
  }
685
-
686
616
  /**
687
617
  * Streams the rfc2822 message from the current node. If this is a root node,
688
618
  * mandatory header fields are set if missing (Date, Message-Id, MIME-Version)
689
619
  *
690
- * @return {String} Compiled message
620
+ * @return Compiled message
691
621
  */
692
622
  createReadStream(options) {
693
623
  options = options || {};
694
-
695
624
  const stream = new PassThrough(options);
696
625
  let outputStream = stream;
697
626
  let transform;
698
-
699
627
  this.stream(stream, options, err => {
700
628
  if (err) {
701
629
  outputStream.emit('error', err);
@@ -703,77 +631,67 @@ class MimeNode {
703
631
  }
704
632
  stream.end();
705
633
  });
706
-
707
634
  for (let i = 0, len = this._transforms.length; i < len; i++) {
708
- transform = typeof this._transforms[i] === 'function' ? this._transforms[i]() : this._transforms[i];
635
+ transform =
636
+ typeof this._transforms[i] === 'function' ? this._transforms[i]() : this._transforms[i];
709
637
  outputStream.once('error', err => {
710
638
  transform.emit('error', err);
711
639
  });
712
640
  outputStream = outputStream.pipe(transform);
713
641
  }
714
-
715
642
  // ensure terminating newline after possible user transforms
716
643
  transform = new LastNewline();
717
644
  outputStream.once('error', err => {
718
645
  transform.emit('error', err);
719
646
  });
720
647
  outputStream = outputStream.pipe(transform);
721
-
722
648
  // dkim and stuff
723
649
  for (let i = 0, len = this._processFuncs.length; i < len; i++) {
724
650
  transform = this._processFuncs[i];
725
651
  outputStream = transform(outputStream);
726
652
  }
727
-
728
653
  if (this.newline) {
729
654
  const winbreak = ['win', 'windows', 'dos', '\r\n'].includes(this.newline.toString().toLowerCase());
730
655
  const newlineTransform = winbreak ? new LeWindows() : new LeUnix();
731
-
732
656
  const stream = outputStream.pipe(newlineTransform);
733
657
  outputStream.on('error', err => stream.emit('error', err));
734
658
  return stream;
735
659
  }
736
-
737
660
  return outputStream;
738
661
  }
739
-
740
662
  /**
741
663
  * Appends a transform stream object to the transforms list. Final output
742
664
  * is passed through this stream before exposing
743
665
  *
744
- * @param {Object} transform Read-Write stream
666
+ * @param transform Read-Write stream
745
667
  */
746
668
  transform(transform) {
747
669
  this._transforms.push(transform);
748
670
  }
749
-
750
671
  /**
751
672
  * Appends a post process function. The functon is run after transforms and
752
673
  * uses the following syntax
753
674
  *
754
675
  * processFunc(input) -> outputStream
755
676
  *
756
- * @param {Object} processFunc Read-Write stream
677
+ * @param processFunc Read-Write stream
757
678
  */
758
679
  processFunc(processFunc) {
759
680
  this._processFuncs.push(processFunc);
760
681
  }
761
-
762
682
  stream(outputStream, options, done) {
763
683
  const transferEncoding = this.getTransferEncoding();
764
684
  let contentStream;
765
685
  let localStream;
766
-
767
686
  // protect actual callback against multiple triggering
768
687
  let returned = false;
769
- const callback = err => {
688
+ const callback = (err) => {
770
689
  if (returned) {
771
690
  return;
772
691
  }
773
692
  returned = true;
774
693
  done(err);
775
694
  };
776
-
777
695
  // for multipart nodes, push child nodes
778
696
  // for content nodes end the stream
779
697
  const finalize = () => {
@@ -792,14 +710,13 @@ class MimeNode {
792
710
  setImmediate(processChildNode);
793
711
  });
794
712
  };
795
-
796
713
  if (this.multipart) {
797
714
  setImmediate(processChildNode);
798
- } else {
715
+ }
716
+ else {
799
717
  return callback();
800
718
  }
801
719
  };
802
-
803
720
  // pushes node content
804
721
  const sendContent = () => {
805
722
  if (this.content) {
@@ -807,26 +724,23 @@ class MimeNode {
807
724
  // content is already errored
808
725
  return callback(this.content);
809
726
  }
810
-
811
727
  if (typeof this.content.pipe === 'function') {
812
728
  this.content.removeListener('error', this._contentErrorHandler);
813
729
  this._contentErrorHandler = err => callback(err);
814
730
  this.content.once('error', this._contentErrorHandler);
815
731
  }
816
-
817
732
  const createStream = () => {
818
733
  if (['quoted-printable', 'base64'].includes(transferEncoding)) {
819
734
  contentStream = new (transferEncoding === 'base64' ? base64 : qp).Encoder(options);
820
-
821
735
  contentStream.pipe(outputStream, {
822
736
  end: false
823
737
  });
824
738
  contentStream.once('end', finalize);
825
739
  contentStream.once('error', err => callback(err));
826
-
827
740
  localStream = this._getStream(this.content);
828
741
  localStream.pipe(contentStream);
829
- } else {
742
+ }
743
+ else {
830
744
  // anything that is not QP or Base54 passes as-is
831
745
  localStream = this._getStream(this.content);
832
746
  localStream.pipe(outputStream, {
@@ -834,10 +748,8 @@ class MimeNode {
834
748
  });
835
749
  localStream.once('end', finalize);
836
750
  }
837
-
838
751
  localStream.once('error', err => callback(err));
839
752
  };
840
-
841
753
  if (this.content._resolve) {
842
754
  const chunks = [];
843
755
  let chunklen = 0;
@@ -866,26 +778,24 @@ class MimeNode {
866
778
  this.content._resolvedValue = Buffer.concat(chunks, chunklen);
867
779
  setImmediate(createStream);
868
780
  });
869
- } else {
781
+ }
782
+ else {
870
783
  setImmediate(createStream);
871
784
  }
872
785
  return;
873
786
  }
874
787
  return setImmediate(finalize);
875
788
  };
876
-
877
789
  if (this._raw) {
878
790
  setImmediate(() => {
879
791
  if (Object.prototype.toString.call(this._raw) === '[object Error]') {
880
792
  // content is already errored
881
793
  return callback(this._raw);
882
794
  }
883
-
884
795
  // remove default error handler (if set)
885
796
  if (typeof this._raw.pipe === 'function') {
886
797
  this._raw.removeListener('error', this._contentErrorHandler);
887
798
  }
888
-
889
799
  const raw = this._getStream(this._raw);
890
800
  raw.pipe(outputStream, {
891
801
  end: false
@@ -893,25 +803,23 @@ class MimeNode {
893
803
  raw.on('error', err => outputStream.emit('error', err));
894
804
  raw.on('end', finalize);
895
805
  });
896
- } else {
806
+ }
807
+ else {
897
808
  outputStream.write(this.buildHeaders() + '\r\n\r\n');
898
809
  setImmediate(sendContent);
899
810
  }
900
811
  }
901
-
902
812
  /**
903
813
  * Sets envelope to be used instead of the generated one
904
814
  *
905
- * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
815
+ * @return SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
906
816
  */
907
817
  setEnvelope(envelope) {
908
818
  let list;
909
-
910
819
  this._envelope = {
911
820
  from: false,
912
821
  to: []
913
822
  };
914
-
915
823
  if (envelope.from) {
916
824
  list = [];
917
825
  this._convertAddresses(this._parseEnvelopeAddresses(envelope.from), list);
@@ -921,29 +829,25 @@ class MimeNode {
921
829
  }
922
830
  }
923
831
  const seenRecipients = new Set();
832
+ const recipients = [];
924
833
  ['to', 'cc', 'bcc'].forEach(key => {
925
834
  if (envelope[key]) {
926
- this._convertAddresses(this._parseEnvelopeAddresses(envelope[key]), this._envelope.to, seenRecipients);
835
+ this._convertAddresses(this._parseEnvelopeAddresses(envelope[key]), recipients, seenRecipients);
927
836
  }
928
837
  });
929
-
930
- this._envelope.to = this._envelope.to.map(to => to.address).filter(address => address);
931
-
838
+ this._envelope.to = recipients.map(to => to.address).filter(address => address);
932
839
  const standardFields = ['to', 'cc', 'bcc', 'from'];
933
840
  shared.copyOwnKeys(this._envelope, envelope, key => standardFields.includes(key));
934
-
935
841
  return this;
936
842
  }
937
-
938
843
  /**
939
844
  * Generates and returns an object with parsed address fields
940
845
  *
941
- * @return {Object} Address object
846
+ * @return Address object
942
847
  */
943
848
  getAddresses() {
944
849
  const addresses = {};
945
850
  const seenByKey = new Map();
946
-
947
851
  this._headers.forEach(header => {
948
852
  const key = header.key.toLowerCase();
949
853
  if (['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].includes(key)) {
@@ -951,34 +855,29 @@ class MimeNode {
951
855
  addresses[key] = [];
952
856
  seenByKey.set(key, new Set());
953
857
  }
954
-
955
858
  this._convertAddresses(this._parseAddresses(header.value), addresses[key], seenByKey.get(key));
956
859
  }
957
860
  });
958
-
959
861
  return addresses;
960
862
  }
961
-
962
863
  /**
963
864
  * Generates and returns SMTP envelope with the sender address and a list of recipients addresses
964
865
  *
965
- * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
866
+ * @return SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
966
867
  */
967
868
  getEnvelope() {
968
869
  if (this._envelope) {
969
870
  return this._envelope;
970
871
  }
971
-
972
872
  const envelope = {
973
873
  from: false,
974
874
  to: []
975
875
  };
976
-
977
876
  // Built once and carried across the headers. Letting _convertAddresses seed it per
978
877
  // call would cost O(headers x recipients), and a message can carry many address
979
878
  // headers: `headers: { to: [...] }` emits one To per entry.
980
879
  const seenRecipients = new Set();
981
-
880
+ const recipients = [];
982
881
  this._headers.forEach(header => {
983
882
  const list = [];
984
883
  if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].includes(header.key))) {
@@ -986,20 +885,18 @@ class MimeNode {
986
885
  if (list.length && list[0]) {
987
886
  envelope.from = list[0].address;
988
887
  }
989
- } else if (['To', 'Cc', 'Bcc'].includes(header.key)) {
990
- this._convertAddresses(this._parseAddresses(header.value), envelope.to, seenRecipients);
888
+ }
889
+ else if (['To', 'Cc', 'Bcc'].includes(header.key)) {
890
+ this._convertAddresses(this._parseAddresses(header.value), recipients, seenRecipients);
991
891
  }
992
892
  });
993
-
994
- envelope.to = envelope.to.map(to => to.address);
995
-
893
+ envelope.to = recipients.map(to => to.address);
996
894
  return envelope;
997
895
  }
998
-
999
896
  /**
1000
897
  * Returns Message-Id value. If it does not exist, then creates one
1001
898
  *
1002
- * @return {String} Message-Id value
899
+ * @return Message-Id value
1003
900
  */
1004
901
  messageId() {
1005
902
  let messageId = this.getHeader('Message-ID');
@@ -1010,15 +907,13 @@ class MimeNode {
1010
907
  }
1011
908
  return messageId;
1012
909
  }
1013
-
1014
910
  /**
1015
911
  * Sets pregenerated content that will be used as the output of this node
1016
912
  *
1017
- * @param {String|Buffer|Stream} Raw MIME contents
913
+ * @param raw Raw MIME contents
1018
914
  */
1019
915
  setRaw(raw) {
1020
916
  this._raw = raw;
1021
-
1022
917
  if (this._raw && typeof this._raw.pipe === 'function') {
1023
918
  // pre-stream handler. might be triggered if a stream is set as content
1024
919
  // and 'error' fires before anything is done with this stream
@@ -1028,22 +923,21 @@ class MimeNode {
1028
923
  };
1029
924
  this._raw.once('error', this._contentErrorHandler);
1030
925
  }
1031
-
1032
926
  return this;
1033
927
  }
1034
-
1035
928
  /////// PRIVATE METHODS
1036
-
1037
929
  /**
1038
930
  * Checks an access policy flag for this node and every node above it. The flags are set
1039
931
  * from the options the node was built with, and createChild only ever sees the options
1040
932
  * the caller passed, so a child of a closed tree starts out open. Reading the answer off
1041
933
  * the parent chain keeps it right whatever order the tree was assembled in.
1042
934
  *
1043
- * @param {String} flag Either 'disableFileAccess' or 'disableUrlAccess'
1044
- * @return {Boolean} true if this node or an ancestor closed that access
935
+ * @param flag Either 'disableFileAccess' or 'disableUrlAccess'
936
+ * @return true if this node or an ancestor closed that access
937
+ * @internal
1045
938
  */
1046
939
  _accessDisabled(flag) {
940
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
1047
941
  let node = this;
1048
942
  while (node) {
1049
943
  if (node[flag]) {
@@ -1053,36 +947,32 @@ class MimeNode {
1053
947
  }
1054
948
  return false;
1055
949
  }
1056
-
1057
950
  /**
1058
951
  * Detects and returns handle to a stream related with the content.
1059
952
  *
1060
- * @param {Mixed} content Node content
1061
- * @returns {Object} Stream object
953
+ * @param content Node content
954
+ * @returns Stream object
955
+ * @internal
1062
956
  */
1063
957
  _getStream(content) {
1064
958
  let contentStream;
1065
-
1066
959
  if (content._resolvedValue) {
1067
960
  // pass string or buffer content as a stream
1068
961
  contentStream = new PassThrough();
1069
-
1070
962
  setImmediate(() => {
1071
963
  try {
1072
964
  contentStream.end(content._resolvedValue);
1073
- } catch (_err) {
965
+ }
966
+ catch (_err) {
1074
967
  contentStream.emit('error', _err);
1075
968
  }
1076
969
  });
1077
-
1078
970
  return contentStream;
1079
971
  }
1080
-
1081
972
  if (typeof content.pipe === 'function') {
1082
973
  // assume as stream
1083
974
  return content;
1084
975
  }
1085
-
1086
976
  if (content && typeof content.path === 'string' && !content.href) {
1087
977
  if (this._accessDisabled('disableFileAccess')) {
1088
978
  contentStream = new PassThrough();
@@ -1096,7 +986,6 @@ class MimeNode {
1096
986
  // read file
1097
987
  return fs.createReadStream(content.path);
1098
988
  }
1099
-
1100
989
  if (content && typeof content.href === 'string') {
1101
990
  if (this._accessDisabled('disableUrlAccess')) {
1102
991
  contentStream = new PassThrough();
@@ -1112,42 +1001,60 @@ class MimeNode {
1112
1001
  // forms the parser accepts, such as a leading space or a slash-less authority
1113
1002
  return nmfetch(content.href, { headers: content.httpHeaders, tls: content.tls });
1114
1003
  }
1115
-
1116
1004
  // pass string or buffer content as a stream
1117
1005
  contentStream = new PassThrough();
1118
-
1119
1006
  setImmediate(() => {
1120
1007
  try {
1121
1008
  contentStream.end(content || '');
1122
- } catch (_err) {
1009
+ }
1010
+ catch (_err) {
1123
1011
  contentStream.emit('error', _err);
1124
1012
  }
1125
1013
  });
1126
1014
  return contentStream;
1127
1015
  }
1128
-
1129
1016
  /**
1130
1017
  * Parses addresses. Takes in a single address or an array or an
1131
1018
  * array of address arrays (eg. To: [[first group], [second group],...])
1132
1019
  *
1133
- * @param {Mixed} addresses Addresses to be parsed
1134
- * @return {Array} An array of address objects
1020
+ * @param addresses Addresses to be parsed
1021
+ * @return An array of address objects
1022
+ * @internal
1135
1023
  */
1136
1024
  _parseAddresses(addresses) {
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
1025
  const flattened = [];
1141
-
1142
- [].concat(addresses).forEach(address => {
1026
+ // Nested arrays are flattened iteratively. concat.apply throws once a recipient list
1027
+ // is long enough to pass the argument limit, and an array handed to addressparser is
1028
+ // stringified, which recurses once per nesting level and exhausts the call stack on
1029
+ // a deep enough value. Every array is walked once, which also ends a self-referential
1030
+ // input
1031
+ const seen = new WeakSet();
1032
+ const stack = [];
1033
+ const enter = (list) => {
1034
+ if (!seen.has(list)) {
1035
+ seen.add(list);
1036
+ stack.push({ list, pos: 0 });
1037
+ }
1038
+ };
1039
+ enter(Array.isArray(addresses) ? addresses : [addresses]);
1040
+ while (stack.length) {
1041
+ const frame = stack[stack.length - 1];
1042
+ if (frame.pos >= frame.list.length) {
1043
+ stack.pop();
1044
+ continue;
1045
+ }
1046
+ const address = frame.list[frame.pos++];
1047
+ if (Array.isArray(address)) {
1048
+ enter(address);
1049
+ continue;
1050
+ }
1143
1051
  if (address && address.address) {
1144
1052
  const normalized = this._normalizeAddress(address.address);
1145
1053
  if (normalized === address.address && typeof address.name === 'string') {
1146
1054
  // there is nothing to rewrite, so there is nothing to keep off the original
1147
1055
  flattened.push(address);
1148
- return;
1056
+ continue;
1149
1057
  }
1150
-
1151
1058
  // rewriting would land on the object the caller passed in and might
1152
1059
  // still hold a reference to, so rewrite a copy of it instead. An own
1153
1060
  // "__proto__" key would make the copy inherit from caller data, and
@@ -1156,18 +1063,15 @@ class MimeNode {
1156
1063
  copy.address = normalized;
1157
1064
  copy.name = address.name || '';
1158
1065
  flattened.push(copy);
1159
- return;
1066
+ continue;
1160
1067
  }
1161
-
1162
1068
  const parsed = this._normalizeParsedAddresses(addressparser(address));
1163
1069
  for (let i = 0; i < parsed.length; i++) {
1164
1070
  flattened.push(parsed[i]);
1165
1071
  }
1166
- });
1167
-
1072
+ }
1168
1073
  return flattened;
1169
1074
  }
1170
-
1171
1075
  /**
1172
1076
  * Normalizes the addresses of a freshly parsed address list, groups included.
1173
1077
  *
@@ -1176,22 +1080,22 @@ class MimeNode {
1176
1080
  * a consumer reading the parsed form back is handed the ambiguous
1177
1081
  * 'user@evil.com@good.com' that the header and the envelope no longer carry.
1178
1082
  *
1179
- * @param {Array} parsed An array of address objects, as returned by addressparser
1180
- * @return {Array} The same array, with every address normalized
1083
+ * @param parsed An array of address objects, as returned by addressparser
1084
+ * @return The same array, with every address normalized
1085
+ * @internal
1181
1086
  */
1182
1087
  _normalizeParsedAddresses(parsed) {
1183
1088
  // addressparser builds these objects, so no caller holds a reference to rewrite around
1184
1089
  parsed.forEach(entry => {
1185
1090
  if (entry.address) {
1186
1091
  entry.address = this._normalizeAddress(entry.address);
1187
- } else if (entry.group) {
1092
+ }
1093
+ else if (entry.group) {
1188
1094
  this._normalizeParsedAddresses(entry.group);
1189
1095
  }
1190
1096
  });
1191
-
1192
1097
  return parsed;
1193
1098
  }
1194
-
1195
1099
  /**
1196
1100
  * Parses the addresses of an explicitly set envelope.
1197
1101
  *
@@ -1199,8 +1103,9 @@ class MimeNode {
1199
1103
  * such as 'root' is the address here. Header parsing has to read the same value as a
1200
1104
  * display name, as a value with no '@' in it can not be an addr-spec in a header.
1201
1105
  *
1202
- * @param {Mixed} addresses Addresses to be parsed
1203
- * @return {Array} An array of address objects
1106
+ * @param addresses Addresses to be parsed
1107
+ * @return An array of address objects
1108
+ * @internal
1204
1109
  */
1205
1110
  _parseEnvelopeAddresses(addresses) {
1206
1111
  return this._parseAddresses(addresses).map(entry => {
@@ -1210,12 +1115,12 @@ class MimeNode {
1210
1115
  return { address: this._normalizeAddress(entry.name), name: '' };
1211
1116
  });
1212
1117
  }
1213
-
1214
1118
  /**
1215
1119
  * Normalizes a header key, uses Camel-Case form, except for uppercase MIME-
1216
1120
  *
1217
- * @param {String} key Key to be normalized
1218
- * @return {String} key in Camel-Case form
1121
+ * @param key Key to be normalized
1122
+ * @return key in Camel-Case form
1123
+ * @internal
1219
1124
  */
1220
1125
  _normalizeHeaderKey(key) {
1221
1126
  key = (key || '')
@@ -1231,46 +1136,58 @@ class MimeNode {
1231
1136
  .replace(/^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, c => c.toUpperCase())
1232
1137
  // special case
1233
1138
  .replace(/^Content-Features$/i, 'Content-features');
1234
-
1235
1139
  return key;
1236
1140
  }
1237
-
1238
1141
  /**
1239
1142
  * Checks if the content type is multipart and defines boundary if needed.
1240
1143
  * Doesn't return anything, modifies object argument instead.
1241
1144
  *
1242
- * @param {Object} structured Parsed header value for 'Content-Type' key
1145
+ * @param structured Parsed header value for 'Content-Type' key
1146
+ * @internal
1243
1147
  */
1244
1148
  _handleContentType(structured) {
1245
1149
  this.contentType = structured.value.trim().toLowerCase();
1246
-
1247
1150
  this.multipart = /^multipart\//i.test(this.contentType) ? this.contentType.substr(this.contentType.indexOf('/') + 1) : false;
1248
-
1249
1151
  if (this.multipart) {
1250
- this.boundary = structured.params.boundary = structured.params.boundary || this.boundary || this._generateBoundary();
1251
- } else {
1152
+ // The declared value and the delimiters are assigned from the same expression
1153
+ // here, so whatever the header emitter then does with it, the two sides cannot
1154
+ // disagree about which characters the boundary is made of.
1155
+ //
1156
+ // Stripping runs before the fallback rather than over the whole chain: a
1157
+ // boundary made only of control characters would otherwise strip to '' and
1158
+ // leave the node declaring no boundary and streaming bare '--' delimiters.
1159
+ // _generateBoundary cleans what it builds, but this is the one place the boundary
1160
+ // is written, so it cleans the value it is about to write rather than trusting
1161
+ // where it came from. MimeNode is exported and subclassable, so the generator is
1162
+ // not necessarily the one below. Stripping twice costs nothing, it is idempotent.
1163
+ const declared = _stripBoundaryControls(structured.params.boundary || this.boundary || '');
1164
+ this.boundary = structured.params.boundary =
1165
+ declared || _stripBoundaryControls(this._generateBoundary());
1166
+ }
1167
+ else {
1252
1168
  this.boundary = false;
1253
1169
  }
1254
1170
  }
1255
-
1256
1171
  /**
1257
1172
  * Generates a multipart boundary value
1258
1173
  *
1259
- * @return {String} boundary value
1174
+ * @return boundary value
1175
+ * @internal
1260
1176
  */
1261
1177
  _generateBoundary() {
1262
- return this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary + '-Part_' + this._nodeId;
1178
+ // baseBoundary and boundaryPrefix are public, so they may hold something the
1179
+ // constructor never cleaned, and the -Part_ suffix keeps the result non empty
1180
+ return _stripBoundaryControls(this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary) + '-Part_' + this._nodeId;
1263
1181
  }
1264
-
1265
1182
  /**
1266
1183
  * Encodes a header value for use in the generated rfc2822 email.
1267
1184
  *
1268
- * @param {String} key Header key
1269
- * @param {String} value Header value
1185
+ * @param key Header key
1186
+ * @param value Header value
1187
+ * @internal
1270
1188
  */
1271
1189
  _encodeHeaderValue(key, value) {
1272
1190
  key = this._normalizeHeaderKey(key);
1273
-
1274
1191
  switch (key) {
1275
1192
  // Structured headers
1276
1193
  case 'From':
@@ -1280,7 +1197,6 @@ class MimeNode {
1280
1197
  case 'Bcc':
1281
1198
  case 'Reply-To':
1282
1199
  return this._convertAddresses(this._parseAddresses(value));
1283
-
1284
1200
  // values enclosed in <>
1285
1201
  case 'Message-ID':
1286
1202
  case 'In-Reply-To':
@@ -1292,73 +1208,61 @@ class MimeNode {
1292
1208
  .toString()
1293
1209
  .replace(/\r?\n|\r/g, ' ')
1294
1210
  .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
1295
-
1296
1211
  if (value.charAt(0) !== '<') {
1297
1212
  value = '<' + value;
1298
1213
  }
1299
-
1300
1214
  if (value.charAt(value.length - 1) !== '>') {
1301
1215
  value = value + '>';
1302
1216
  }
1303
1217
  return value;
1304
-
1305
1218
  // space separated list of values enclosed in <>
1306
1219
  case 'References':
1307
1220
  value = [].concat
1308
- .apply(
1309
- [],
1310
- [].concat(value || '').map(elm => {
1311
- elm = (elm || '')
1312
- .toString()
1313
- .replace(/\r?\n|\r/g, ' ')
1314
- .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
1315
- .trim();
1316
- return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
1317
- })
1318
- )
1221
+ .apply([], [].concat(value || '').map(elm => {
1222
+ elm = (elm || '')
1223
+ .toString()
1224
+ .replace(/\r?\n|\r/g, ' ')
1225
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
1226
+ .trim();
1227
+ return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
1228
+ }))
1319
1229
  .map(elm => {
1320
- if (elm.charAt(0) !== '<') {
1321
- elm = '<' + elm;
1322
- }
1323
- if (elm.charAt(elm.length - 1) !== '>') {
1324
- elm = elm + '>';
1325
- }
1326
- return elm;
1327
- });
1328
-
1230
+ if (elm.charAt(0) !== '<') {
1231
+ elm = '<' + elm;
1232
+ }
1233
+ if (elm.charAt(elm.length - 1) !== '>') {
1234
+ elm = elm + '>';
1235
+ }
1236
+ return elm;
1237
+ });
1329
1238
  return value.join(' ').trim();
1330
-
1331
1239
  case 'Date':
1332
1240
  if (Object.prototype.toString.call(value) === '[object Date]') {
1333
1241
  return value.toUTCString().replace(/GMT/, '+0000');
1334
1242
  }
1335
-
1336
1243
  value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
1337
1244
  return this._encodeHeaderText(value);
1338
-
1339
1245
  case 'Content-Type':
1340
1246
  case 'Content-Disposition':
1341
1247
  // if it includes a filename then it is already encoded
1342
1248
  return (value || '').toString().replace(/\r?\n|\r/g, ' ');
1343
-
1344
1249
  default:
1345
1250
  value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
1346
1251
  return this._encodeHeaderText(value);
1347
1252
  }
1348
1253
  }
1349
-
1350
1254
  /**
1351
1255
  * Rebuilds address object using punycode and other adjustments
1352
1256
  *
1353
- * @param {Array} addresses An array of address objects
1354
- * @param {Array} [uniqueList] An array to be populated with addresses
1355
- * @return {String} address string
1257
+ * @param addresses An array of address objects
1258
+ * @param [uniqueList] An array to be populated with addresses
1259
+ * @param [seenAddresses] Addresses already added to uniqueList, shared with recursive calls to keep deduplication linear
1260
+ * @return address string
1261
+ * @internal
1356
1262
  */
1357
1263
  _convertAddresses(addresses, uniqueList, seenAddresses) {
1358
1264
  const values = [];
1359
-
1360
1265
  uniqueList = uniqueList || [];
1361
-
1362
1266
  // Membership is checked once per address, so scanning uniqueList itself would make
1363
1267
  // a recipient list cost O(n^2). Groups recurse with the same set so that a nested
1364
1268
  // group still dedupes against the addresses collected around it, and a caller that
@@ -1369,84 +1273,72 @@ class MimeNode {
1369
1273
  seenAddresses.add(uniqueList[i].address);
1370
1274
  }
1371
1275
  }
1372
-
1373
1276
  [].concat(addresses || []).forEach(address => {
1374
1277
  if (address.address) {
1375
1278
  address.address = this._normalizeAddress(address.address);
1376
-
1377
1279
  if (!address.name) {
1378
1280
  // an address that carries a special, be it a quoted local part or a domain
1379
1281
  // that could not be normalized, is only unambiguous inside angle brackets.
1380
1282
  // Without them a ',' or a ';' anywhere in it reads as a recipient separator
1381
1283
  // and the header would list more recipients than the envelope carries
1382
1284
  values.push(PLAIN_ADDRESS.test(address.address) ? address.address : `<${address.address}>`);
1383
- } else {
1285
+ }
1286
+ else {
1384
1287
  values.push(`${this._encodeAddressName(address.name)} <${address.address}>`);
1385
1288
  }
1386
-
1387
1289
  if (!seenAddresses.has(address.address)) {
1388
1290
  seenAddresses.add(address.address);
1389
1291
  uniqueList.push(address);
1390
1292
  }
1391
- } else if (address.group) {
1392
- const groupListAddresses = (
1393
- address.group.length ? this._convertAddresses(address.group, uniqueList, seenAddresses) : ''
1394
- ).trim();
1293
+ }
1294
+ else if (address.group) {
1295
+ const groupListAddresses = (address.group.length ? this._convertAddresses(address.group, uniqueList, seenAddresses) : '').trim();
1395
1296
  values.push(`${this._encodeAddressName(address.name)}:${groupListAddresses};`);
1396
1297
  }
1397
1298
  });
1398
-
1399
1299
  return values.join(', ');
1400
1300
  }
1401
-
1402
1301
  /**
1403
1302
  * Normalizes an email address
1404
1303
  *
1405
- * @param {Array} address An array of address objects
1406
- * @return {String} address string
1304
+ * @param address An array of address objects
1305
+ * @return address string
1306
+ * @internal
1407
1307
  */
1408
1308
  _normalizeAddress(address) {
1409
1309
  address = (address || '')
1410
1310
  .toString()
1411
1311
  .replace(/[\x00-\x1F\x7F<>]+/g, ' ') // remove unallowed characters
1412
1312
  .trim();
1413
-
1414
1313
  if (!address) {
1415
1314
  // callers use an empty value to detect a missing address
1416
1315
  return address;
1417
1316
  }
1418
-
1419
1317
  const lastAt = address.lastIndexOf('@');
1420
1318
  if (lastAt < 0) {
1421
1319
  // Bare username, there is no domain to split off
1422
1320
  return this._normalizeLocalPart(address);
1423
1321
  }
1424
-
1425
1322
  const user = address.substr(0, lastAt);
1426
1323
  const domain = address.substr(lastAt + 1);
1427
-
1428
1324
  // Unicode in the local part is kept as is, see _normalizeLocalPart for the rest of it.
1429
1325
  // A domain has no quoting construct to fall back on, so whatever is not a valid domain
1430
1326
  // is kept as supplied and it is _convertAddresses that keeps such an address unambiguous.
1431
1327
  // Domains are punycoded when the local part is ASCII ('safe@jõgeva.ee' -> 'safe@xn--jgeva-dua.ee').
1432
1328
  // When the local part contains non-ASCII bytes the address already requires SMTPUTF8,
1433
1329
  // so the domain is kept (or decoded back) as UTF-8 for symmetry on both sides of '@'.
1434
-
1435
1330
  let encodedDomain = domain;
1436
-
1437
1331
  // A non-ASCII local part already requires SMTPUTF8, so the domain stays UTF-8 for
1438
1332
  // symmetry on both sides of the '@' rather than being encoded to an A-label
1439
1333
  const smtputf8 = /[\x80-\uFFFF]/.test(user);
1440
-
1441
1334
  try {
1442
1335
  encodedDomain = normalizeDomain(domain.toLowerCase(), smtputf8);
1443
- } catch (_err) {
1336
+ }
1337
+ catch (_err) {
1444
1338
  // keep domain as supplied
1445
1339
  }
1446
-
1447
1340
  return `${this._normalizeLocalPart(user)}@${encodedDomain}`;
1448
1341
  }
1449
-
1450
1342
  /**
1451
1343
  * Normalizes the local part of an address into a form that can be emitted as is.
1452
1344
  *
@@ -1456,54 +1348,55 @@ class MimeNode {
1456
1348
  * while the split here is always at the last one. So whatever is not already one of
1457
1349
  * the two valid forms goes back out as a quoted-string.
1458
1350
  *
1459
- * @param {String} user Local part of an address
1460
- * @return {String} Local part as a dot-atom or as a quoted-string
1351
+ * @param user Local part of an address
1352
+ * @return Local part as a dot-atom or as a quoted-string
1353
+ * @internal
1461
1354
  */
1462
1355
  _normalizeLocalPart(user) {
1463
1356
  if (DOT_ATOM.test(user) || QUOTED_STRING.test(user)) {
1464
1357
  return user;
1465
1358
  }
1466
-
1467
1359
  return mimeFuncs.quoteString(user);
1468
1360
  }
1469
-
1470
1361
  /**
1471
1362
  * If needed, mime encodes the name part
1472
1363
  *
1473
- * @param {String} name Name part of an address
1474
- * @returns {String} Mime word encoded string if needed
1364
+ * @param name Name part of an address
1365
+ * @returns Mime word encoded string if needed
1366
+ * @internal
1475
1367
  */
1476
1368
  _encodeAddressName(name) {
1477
1369
  if (!/^[\w ]*$/.test(name)) {
1478
1370
  if (/^[\x20-\x7e]*$/.test(name)) {
1479
1371
  return mimeFuncs.quoteString(name);
1480
- } else {
1372
+ }
1373
+ else {
1481
1374
  return mimeFuncs.encodeWord(name, this._getTextEncoding(name), 52);
1482
1375
  }
1483
1376
  }
1484
1377
  return name;
1485
1378
  }
1486
-
1487
1379
  /**
1488
1380
  * Encodes an unstructured header value. Such a value can only carry VCHAR and WSP, so a
1489
1381
  * control char or DEL has to be forced into the mime encoded word that a non-ascii value
1490
1382
  * would get anyway. HT stays as it is, it is valid folding whitespace here.
1491
1383
  *
1492
- * @param {String} value Header value to encode
1493
- * @returns {String} Mime word encoded string if needed
1384
+ * @param value Header value to encode
1385
+ * @returns Mime word encoded string if needed
1386
+ * @internal
1494
1387
  */
1495
1388
  _encodeHeaderText(value) {
1496
1389
  return /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value)
1497
1390
  ? mimeFuncs.encodeWord(value, this._getTextEncoding(value), 52)
1498
1391
  : // encodeWords only encodes if needed, otherwise the original string is returned
1499
- this._encodeWords(value);
1392
+ this._encodeWords(value);
1500
1393
  }
1501
-
1502
1394
  /**
1503
1395
  * If needed, mime encodes the name part
1504
1396
  *
1505
- * @param {String} name Name part of an address
1506
- * @returns {String} Mime word encoded string if needed
1397
+ * @param value Name part of an address
1398
+ * @returns Mime word encoded string if needed
1399
+ * @internal
1507
1400
  */
1508
1401
  _encodeWords(value) {
1509
1402
  // set encodeAll parameter to true even though it is against the recommendation of RFC2047,
@@ -1511,20 +1404,18 @@ class MimeNode {
1511
1404
  // but some clients (eg. Zimbra) do not handle it properly and remove surrounding whitespace
1512
1405
  return mimeFuncs.encodeWords(value, this._getTextEncoding(value), 52, true);
1513
1406
  }
1514
-
1515
1407
  /**
1516
1408
  * Detects best mime encoding for a text value
1517
1409
  *
1518
- * @param {String} value Value to check for
1519
- * @return {String} either 'Q' or 'B'
1410
+ * @param value Value to check for
1411
+ * @return either 'Q' or 'B'
1412
+ * @internal
1520
1413
  */
1521
1414
  _getTextEncoding(value) {
1522
1415
  value = (value || '').toString();
1523
-
1524
1416
  if (this.textEncoding) {
1525
1417
  return this.textEncoding;
1526
1418
  }
1527
-
1528
1419
  // count latin alphabet symbols and 8-bit range symbols + control symbols
1529
1420
  // if there are more latin characters, then use quoted-printable
1530
1421
  // encoding, otherwise use base64
@@ -1534,33 +1425,29 @@ class MimeNode {
1534
1425
  const code = value.charCodeAt(i);
1535
1426
  if ((code >= 0x00 && code <= 0x08) || code === 0x0b || code === 0x0c || (code >= 0x0e && code <= 0x1f) || code >= 0x80) {
1536
1427
  nonLatinLen++;
1537
- } else if ((code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a)) {
1428
+ }
1429
+ else if ((code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a)) {
1538
1430
  latinLen++;
1539
1431
  }
1540
1432
  }
1541
1433
  // if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
1542
1434
  return nonLatinLen < latinLen ? 'Q' : 'B';
1543
1435
  }
1544
-
1545
1436
  /**
1546
1437
  * Generates a message id
1547
1438
  *
1548
- * @return {String} Random Message-ID value
1439
+ * @return Random Message-ID value
1440
+ * @internal
1549
1441
  */
1550
1442
  _generateMessageId() {
1551
- return (
1552
- '<' +
1443
+ return ('<' +
1553
1444
  [2, 2, 2, 6].reduce(
1554
- // crux to generate UUID-like random strings
1555
- (prev, len) => prev + '-' + crypto.randomBytes(len).toString('hex'),
1556
- crypto.randomBytes(4).toString('hex')
1557
- ) +
1445
+ // crux to generate UUID-like random strings
1446
+ (prev, len) => prev + '-' + crypto.randomBytes(len).toString('hex'), crypto.randomBytes(4).toString('hex')) +
1558
1447
  '@' +
1559
1448
  // try to use the domain of the FROM address or fallback to server hostname
1560
1449
  (this.getEnvelope().from || this.hostname || 'localhost').split('@').pop() +
1561
- '>'
1562
- );
1450
+ '>');
1563
1451
  }
1564
1452
  }
1565
-
1566
- module.exports = MimeNode;
1453
+ export default MimeNode;