@nocobase/plugin-workflow-mailer 2.3.0-beta.12 → 2.3.0-beta.14

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