@nocobase/plugin-notifications 2.4.0-alpha.1 → 2.4.0-alpha.11

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 (181) hide show
  1. package/dist/externalVersion.js +3 -3
  2. package/dist/node_modules/nodemailer/dist/cjs/addressparser/index.d.ts +59 -0
  3. package/dist/node_modules/nodemailer/dist/cjs/addressparser/index.js +790 -0
  4. package/dist/node_modules/nodemailer/dist/cjs/base64/index.d.ts +36 -0
  5. package/dist/node_modules/nodemailer/dist/cjs/base64/index.js +119 -0
  6. package/dist/node_modules/nodemailer/dist/cjs/dkim/index.d.ts +52 -0
  7. package/dist/node_modules/nodemailer/dist/cjs/dkim/index.js +228 -0
  8. package/dist/node_modules/nodemailer/dist/cjs/dkim/message-parser.d.ts +48 -0
  9. package/dist/node_modules/nodemailer/dist/cjs/dkim/message-parser.js +180 -0
  10. package/dist/node_modules/nodemailer/dist/cjs/dkim/relaxed-body.d.ts +36 -0
  11. package/dist/node_modules/nodemailer/dist/cjs/dkim/relaxed-body.js +148 -0
  12. package/dist/node_modules/nodemailer/dist/cjs/dkim/sign.d.ts +52 -0
  13. package/dist/node_modules/nodemailer/dist/cjs/dkim/sign.js +165 -0
  14. package/dist/node_modules/nodemailer/dist/cjs/errors.d.ts +96 -0
  15. package/dist/node_modules/nodemailer/dist/cjs/errors.js +70 -0
  16. package/dist/node_modules/nodemailer/dist/cjs/fetch/cookies.d.ts +107 -0
  17. package/dist/node_modules/nodemailer/dist/cjs/fetch/cookies.js +282 -0
  18. package/dist/node_modules/nodemailer/dist/cjs/fetch/index.d.ts +65 -0
  19. package/dist/node_modules/nodemailer/dist/cjs/fetch/index.js +384 -0
  20. package/dist/node_modules/nodemailer/dist/cjs/json-transport/index.d.ts +64 -0
  21. package/dist/node_modules/nodemailer/dist/cjs/json-transport/index.js +99 -0
  22. package/dist/node_modules/nodemailer/dist/cjs/mail-composer/index.d.ts +174 -0
  23. package/dist/node_modules/nodemailer/dist/cjs/mail-composer/index.js +613 -0
  24. package/dist/node_modules/nodemailer/dist/cjs/mailer/index.d.ts +237 -0
  25. package/dist/node_modules/nodemailer/dist/cjs/mailer/index.js +415 -0
  26. package/dist/node_modules/nodemailer/dist/cjs/mailer/mail-message.d.ts +97 -0
  27. package/dist/node_modules/nodemailer/dist/cjs/mailer/mail-message.js +342 -0
  28. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/index.d.ts +161 -0
  29. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/index.js +787 -0
  30. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/mime-types.d.ts +2 -0
  31. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/mime-types.js +2103 -0
  32. package/dist/node_modules/nodemailer/dist/cjs/mime-node/index.d.ts +333 -0
  33. package/dist/node_modules/nodemailer/dist/cjs/mime-node/index.js +1493 -0
  34. package/dist/node_modules/nodemailer/dist/cjs/mime-node/last-newline.d.ts +5 -0
  35. package/dist/node_modules/nodemailer/dist/cjs/mime-node/last-newline.js +32 -0
  36. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-unix.d.ts +9 -0
  37. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-unix.js +40 -0
  38. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-windows.d.ts +10 -0
  39. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-windows.js +47 -0
  40. package/dist/node_modules/nodemailer/dist/cjs/nodemailer.d.ts +124 -0
  41. package/dist/node_modules/nodemailer/dist/cjs/nodemailer.js +1 -0
  42. package/dist/node_modules/nodemailer/dist/cjs/package-info.d.ts +3 -0
  43. package/dist/node_modules/nodemailer/dist/cjs/package-info.js +7 -0
  44. package/dist/node_modules/nodemailer/dist/cjs/package.json +3 -0
  45. package/dist/node_modules/nodemailer/dist/cjs/punycode/index.d.ts +73 -0
  46. package/dist/node_modules/nodemailer/dist/cjs/punycode/index.js +411 -0
  47. package/dist/node_modules/nodemailer/dist/cjs/qp/index.d.ts +32 -0
  48. package/dist/node_modules/nodemailer/dist/cjs/qp/index.js +207 -0
  49. package/dist/node_modules/nodemailer/dist/cjs/sendmail-transport/index.d.ts +69 -0
  50. package/dist/node_modules/nodemailer/dist/cjs/sendmail-transport/index.js +210 -0
  51. package/dist/node_modules/nodemailer/dist/cjs/ses-transport/index.d.ts +112 -0
  52. package/dist/node_modules/nodemailer/dist/cjs/ses-transport/index.js +252 -0
  53. package/dist/node_modules/nodemailer/dist/cjs/shared/index.d.ts +260 -0
  54. package/dist/node_modules/nodemailer/dist/cjs/shared/index.js +680 -0
  55. package/dist/node_modules/nodemailer/dist/cjs/shared/objects.d.ts +23 -0
  56. package/dist/node_modules/nodemailer/dist/cjs/shared/objects.js +43 -0
  57. package/dist/node_modules/nodemailer/dist/cjs/shared/url.d.ts +21 -0
  58. package/dist/node_modules/nodemailer/dist/cjs/shared/url.js +255 -0
  59. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/data-stream.d.ts +15 -0
  60. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/data-stream.js +108 -0
  61. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.d.ts +47 -0
  62. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js +189 -0
  63. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/index.d.ts +416 -0
  64. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/index.js +1837 -0
  65. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/index.d.ts +133 -0
  66. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/index.js +569 -0
  67. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.d.ts +62 -0
  68. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.js +257 -0
  69. package/dist/node_modules/nodemailer/dist/cjs/smtp-transport/index.d.ts +156 -0
  70. package/dist/node_modules/nodemailer/dist/cjs/smtp-transport/index.js +371 -0
  71. package/dist/node_modules/nodemailer/dist/cjs/stream-transport/index.d.ts +65 -0
  72. package/dist/node_modules/nodemailer/dist/cjs/stream-transport/index.js +146 -0
  73. package/dist/node_modules/nodemailer/dist/cjs/well-known/index.d.ts +35 -0
  74. package/dist/node_modules/nodemailer/dist/cjs/well-known/index.js +41 -0
  75. package/dist/node_modules/nodemailer/dist/cjs/well-known/services.d.ts +2 -0
  76. package/dist/node_modules/nodemailer/dist/cjs/well-known/services.js +674 -0
  77. package/dist/node_modules/nodemailer/dist/cjs/xoauth2/index.d.ts +201 -0
  78. package/dist/node_modules/nodemailer/dist/cjs/xoauth2/index.js +426 -0
  79. package/dist/node_modules/nodemailer/dist/esm/addressparser/index.d.ts +59 -0
  80. package/dist/node_modules/nodemailer/dist/esm/addressparser/index.js +786 -0
  81. package/dist/node_modules/nodemailer/dist/esm/base64/index.d.ts +36 -0
  82. package/dist/node_modules/nodemailer/{lib → dist/esm}/base64/index.js +39 -68
  83. package/dist/node_modules/nodemailer/dist/esm/dkim/index.d.ts +52 -0
  84. package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/index.js +62 -92
  85. package/dist/node_modules/nodemailer/dist/esm/dkim/message-parser.d.ts +48 -0
  86. package/dist/node_modules/nodemailer/dist/esm/dkim/message-parser.js +176 -0
  87. package/dist/node_modules/nodemailer/dist/esm/dkim/relaxed-body.d.ts +36 -0
  88. package/dist/node_modules/nodemailer/dist/esm/dkim/relaxed-body.js +141 -0
  89. package/dist/node_modules/nodemailer/dist/esm/dkim/sign.d.ts +52 -0
  90. package/dist/node_modules/nodemailer/dist/esm/dkim/sign.js +125 -0
  91. package/dist/node_modules/nodemailer/dist/esm/errors.d.ts +96 -0
  92. package/dist/node_modules/nodemailer/dist/esm/errors.js +67 -0
  93. package/dist/node_modules/nodemailer/dist/esm/fetch/cookies.d.ts +107 -0
  94. package/dist/node_modules/nodemailer/dist/esm/fetch/cookies.js +242 -0
  95. package/dist/node_modules/nodemailer/dist/esm/fetch/index.d.ts +65 -0
  96. package/dist/node_modules/nodemailer/dist/esm/fetch/index.js +344 -0
  97. package/dist/node_modules/nodemailer/dist/esm/json-transport/index.d.ts +64 -0
  98. package/dist/node_modules/nodemailer/dist/esm/json-transport/index.js +62 -0
  99. package/dist/node_modules/nodemailer/dist/esm/mail-composer/index.d.ts +174 -0
  100. package/dist/node_modules/nodemailer/{lib → dist/esm}/mail-composer/index.js +288 -280
  101. package/dist/node_modules/nodemailer/dist/esm/mailer/index.d.ts +237 -0
  102. package/dist/node_modules/nodemailer/{lib → dist/esm}/mailer/index.js +142 -194
  103. package/dist/node_modules/nodemailer/dist/esm/mailer/mail-message.d.ts +97 -0
  104. package/dist/node_modules/nodemailer/{lib → dist/esm}/mailer/mail-message.js +111 -125
  105. package/dist/node_modules/nodemailer/dist/esm/mime-funcs/index.d.ts +161 -0
  106. package/dist/node_modules/nodemailer/dist/esm/mime-funcs/index.js +738 -0
  107. package/dist/node_modules/nodemailer/dist/esm/mime-funcs/mime-types.d.ts +2 -0
  108. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-funcs/mime-types.js +43 -49
  109. package/dist/node_modules/nodemailer/dist/esm/mime-node/index.d.ts +333 -0
  110. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/index.js +584 -436
  111. package/dist/node_modules/nodemailer/dist/esm/mime-node/last-newline.d.ts +5 -0
  112. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/last-newline.js +4 -10
  113. package/dist/node_modules/nodemailer/dist/esm/mime-node/le-unix.d.ts +9 -0
  114. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/le-unix.js +7 -15
  115. package/dist/node_modules/nodemailer/dist/esm/mime-node/le-windows.d.ts +10 -0
  116. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/le-windows.js +6 -16
  117. package/dist/node_modules/nodemailer/dist/esm/nodemailer.d.ts +124 -0
  118. package/dist/node_modules/nodemailer/dist/esm/nodemailer.js +166 -0
  119. package/dist/node_modules/nodemailer/dist/esm/package-info.d.ts +3 -0
  120. package/dist/node_modules/nodemailer/dist/esm/package-info.js +4 -0
  121. package/dist/node_modules/nodemailer/dist/esm/package.json +3 -0
  122. package/dist/node_modules/nodemailer/dist/esm/punycode/index.d.ts +73 -0
  123. package/dist/node_modules/nodemailer/dist/esm/punycode/index.js +405 -0
  124. package/dist/node_modules/nodemailer/dist/esm/qp/index.d.ts +32 -0
  125. package/dist/node_modules/nodemailer/{lib → dist/esm}/qp/index.js +59 -77
  126. package/dist/node_modules/nodemailer/dist/esm/sendmail-transport/index.d.ts +69 -0
  127. package/dist/node_modules/nodemailer/dist/esm/sendmail-transport/index.js +170 -0
  128. package/dist/node_modules/nodemailer/dist/esm/ses-transport/index.d.ts +112 -0
  129. package/dist/node_modules/nodemailer/dist/esm/ses-transport/index.js +212 -0
  130. package/dist/node_modules/nodemailer/dist/esm/shared/index.d.ts +260 -0
  131. package/dist/node_modules/nodemailer/dist/esm/shared/index.js +631 -0
  132. package/dist/node_modules/nodemailer/dist/esm/shared/objects.d.ts +23 -0
  133. package/dist/node_modules/nodemailer/dist/esm/shared/objects.js +38 -0
  134. package/dist/node_modules/nodemailer/dist/esm/shared/url.d.ts +21 -0
  135. package/dist/node_modules/nodemailer/dist/esm/shared/url.js +214 -0
  136. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/data-stream.d.ts +15 -0
  137. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/data-stream.js +103 -0
  138. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.d.ts +47 -0
  139. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.js +149 -0
  140. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/index.d.ts +416 -0
  141. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/index.js +1797 -0
  142. package/dist/node_modules/nodemailer/dist/esm/smtp-pool/index.d.ts +133 -0
  143. package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-pool/index.js +195 -314
  144. package/dist/node_modules/nodemailer/dist/esm/smtp-pool/pool-resource.d.ts +62 -0
  145. package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-pool/pool-resource.js +70 -107
  146. package/dist/node_modules/nodemailer/dist/esm/smtp-transport/index.d.ts +156 -0
  147. package/dist/node_modules/nodemailer/dist/esm/smtp-transport/index.js +331 -0
  148. package/dist/node_modules/nodemailer/dist/esm/stream-transport/index.d.ts +65 -0
  149. package/dist/node_modules/nodemailer/dist/esm/stream-transport/index.js +106 -0
  150. package/dist/node_modules/nodemailer/dist/esm/well-known/index.d.ts +35 -0
  151. package/dist/node_modules/nodemailer/{lib → dist/esm}/well-known/index.js +12 -23
  152. package/dist/node_modules/nodemailer/dist/esm/well-known/services.d.ts +2 -0
  153. package/dist/node_modules/nodemailer/dist/esm/well-known/services.js +671 -0
  154. package/dist/node_modules/nodemailer/dist/esm/xoauth2/index.d.ts +201 -0
  155. package/dist/node_modules/nodemailer/dist/esm/xoauth2/index.js +386 -0
  156. package/dist/node_modules/nodemailer/dist/well-known/services.json +640 -0
  157. package/dist/node_modules/nodemailer/package.json +1 -1
  158. package/dist/server/models/NotificationService.d.ts +1 -2
  159. package/package.json +3 -3
  160. package/dist/node_modules/nodemailer/.gitattributes +0 -6
  161. package/dist/node_modules/nodemailer/.ncurc.js +0 -7
  162. package/dist/node_modules/nodemailer/.prettierrc.js +0 -8
  163. package/dist/node_modules/nodemailer/lib/addressparser/index.js +0 -313
  164. package/dist/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
  165. package/dist/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
  166. package/dist/node_modules/nodemailer/lib/dkim/sign.js +0 -117
  167. package/dist/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
  168. package/dist/node_modules/nodemailer/lib/fetch/index.js +0 -274
  169. package/dist/node_modules/nodemailer/lib/json-transport/index.js +0 -82
  170. package/dist/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
  171. package/dist/node_modules/nodemailer/lib/nodemailer.js +0 -1
  172. package/dist/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
  173. package/dist/node_modules/nodemailer/lib/ses-transport/index.js +0 -349
  174. package/dist/node_modules/nodemailer/lib/shared/index.js +0 -638
  175. package/dist/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
  176. package/dist/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
  177. package/dist/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1812
  178. package/dist/node_modules/nodemailer/lib/smtp-transport/index.js +0 -416
  179. package/dist/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
  180. package/dist/node_modules/nodemailer/lib/well-known/services.json +0 -338
  181. 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,82 +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
  }
514
+ // Ensure that Content-Type is the last header for the root node
515
+ for (let i = this._headers.length - 2; i >= 0; i--) {
516
+ const header = this._headers[i];
517
+ if (header.key === 'Content-Type') {
518
+ this._headers.splice(i, 1);
519
+ this._headers.push(header);
520
+ }
521
+ }
500
522
  }
501
-
502
523
  this._headers.forEach(header => {
503
524
  let key = header.key;
504
525
  let value = header.value;
505
526
  let structured;
506
527
  let param;
507
- let options = {};
508
- let formattedHeaders = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
509
-
528
+ const options = {};
529
+ const formattedHeaders = FORMATTED_HEADERS;
510
530
  if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
511
- Object.keys(value).forEach(key => {
512
- if (key !== 'value') {
513
- options[key] = value[key];
514
- }
515
- });
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');
516
535
  value = (value.value || '').toString();
517
536
  if (!value.trim()) {
518
537
  return;
519
538
  }
520
539
  }
521
-
522
540
  if (options.prepared) {
523
541
  // header value is
524
542
  if (options.foldLines) {
525
543
  headers.push(mimeFuncs.foldLines(key + ': ' + value));
526
- } else {
544
+ }
545
+ else {
527
546
  headers.push(key + ': ' + value);
528
547
  }
529
548
  return;
530
549
  }
531
-
532
550
  switch (header.key) {
533
551
  case 'Content-Disposition':
534
552
  structured = mimeFuncs.parseHeaderValue(value);
@@ -537,32 +555,38 @@ class MimeNode {
537
555
  }
538
556
  value = mimeFuncs.buildHeaderValue(structured);
539
557
  break;
540
-
541
558
  case 'Content-Type':
542
559
  structured = mimeFuncs.parseHeaderValue(value);
543
-
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, '');
544
564
  this._handleContentType(structured);
545
-
546
- 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)) {
547
568
  structured.params.charset = 'utf-8';
548
569
  }
549
-
550
570
  value = mimeFuncs.buildHeaderValue(structured);
551
-
552
571
  if (this.filename) {
553
572
  // add support for non-compliant clients like QQ webmail
554
573
  // we can't build the value with buildHeaderValue as the value is non standard and
555
574
  // would be converted to parameter continuation encoding that we do not want
556
- param = this._encodeWords(this.filename);
557
-
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);
558
581
  if (param !== this.filename || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(param)) {
559
- // include value in quotes if needed
560
- 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);
561
586
  }
562
587
  value += '; name=' + param;
563
588
  }
564
589
  break;
565
-
566
590
  case 'Bcc':
567
591
  if (!this.keepBcc) {
568
592
  // skip BCC values
@@ -570,40 +594,36 @@ class MimeNode {
570
594
  }
571
595
  break;
572
596
  }
573
-
574
597
  value = this._encodeHeaderValue(key, value);
575
-
576
598
  // skip empty lines
577
599
  if (!(value || '').toString().trim()) {
578
600
  return;
579
601
  }
580
-
581
602
  if (typeof this.normalizeHeaderKey === 'function') {
582
- let normalized = this.normalizeHeaderKey(key, value);
583
- if (normalized && typeof normalized === 'string' && normalized.length) {
584
- 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;
585
610
  }
586
611
  }
587
-
588
612
  headers.push(mimeFuncs.foldLines(key + ': ' + value, 76));
589
613
  });
590
-
591
614
  return headers.join('\r\n');
592
615
  }
593
-
594
616
  /**
595
617
  * Streams the rfc2822 message from the current node. If this is a root node,
596
618
  * mandatory header fields are set if missing (Date, Message-Id, MIME-Version)
597
619
  *
598
- * @return {String} Compiled message
620
+ * @return Compiled message
599
621
  */
600
622
  createReadStream(options) {
601
623
  options = options || {};
602
-
603
- let stream = new PassThrough(options);
624
+ const stream = new PassThrough(options);
604
625
  let outputStream = stream;
605
626
  let transform;
606
-
607
627
  this.stream(stream, options, err => {
608
628
  if (err) {
609
629
  outputStream.emit('error', err);
@@ -611,87 +631,77 @@ class MimeNode {
611
631
  }
612
632
  stream.end();
613
633
  });
614
-
615
634
  for (let i = 0, len = this._transforms.length; i < len; i++) {
616
- 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];
617
637
  outputStream.once('error', err => {
618
638
  transform.emit('error', err);
619
639
  });
620
640
  outputStream = outputStream.pipe(transform);
621
641
  }
622
-
623
642
  // ensure terminating newline after possible user transforms
624
643
  transform = new LastNewline();
625
644
  outputStream.once('error', err => {
626
645
  transform.emit('error', err);
627
646
  });
628
647
  outputStream = outputStream.pipe(transform);
629
-
630
648
  // dkim and stuff
631
649
  for (let i = 0, len = this._processFuncs.length; i < len; i++) {
632
650
  transform = this._processFuncs[i];
633
651
  outputStream = transform(outputStream);
634
652
  }
635
-
636
653
  if (this.newline) {
637
654
  const winbreak = ['win', 'windows', 'dos', '\r\n'].includes(this.newline.toString().toLowerCase());
638
655
  const newlineTransform = winbreak ? new LeWindows() : new LeUnix();
639
-
640
656
  const stream = outputStream.pipe(newlineTransform);
641
657
  outputStream.on('error', err => stream.emit('error', err));
642
658
  return stream;
643
659
  }
644
-
645
660
  return outputStream;
646
661
  }
647
-
648
662
  /**
649
663
  * Appends a transform stream object to the transforms list. Final output
650
664
  * is passed through this stream before exposing
651
665
  *
652
- * @param {Object} transform Read-Write stream
666
+ * @param transform Read-Write stream
653
667
  */
654
668
  transform(transform) {
655
669
  this._transforms.push(transform);
656
670
  }
657
-
658
671
  /**
659
672
  * Appends a post process function. The functon is run after transforms and
660
673
  * uses the following syntax
661
674
  *
662
675
  * processFunc(input) -> outputStream
663
676
  *
664
- * @param {Object} processFunc Read-Write stream
677
+ * @param processFunc Read-Write stream
665
678
  */
666
679
  processFunc(processFunc) {
667
680
  this._processFuncs.push(processFunc);
668
681
  }
669
-
670
682
  stream(outputStream, options, done) {
671
- let transferEncoding = this.getTransferEncoding();
683
+ const transferEncoding = this.getTransferEncoding();
672
684
  let contentStream;
673
685
  let localStream;
674
-
675
686
  // protect actual callback against multiple triggering
676
687
  let returned = false;
677
- let callback = err => {
688
+ const callback = (err) => {
678
689
  if (returned) {
679
690
  return;
680
691
  }
681
692
  returned = true;
682
693
  done(err);
683
694
  };
684
-
685
695
  // for multipart nodes, push child nodes
686
696
  // for content nodes end the stream
687
- let finalize = () => {
697
+ const finalize = () => {
688
698
  let childId = 0;
689
- let processChildNode = () => {
699
+ const processChildNode = () => {
690
700
  if (childId >= this.childNodes.length) {
691
701
  outputStream.write('\r\n--' + this.boundary + '--\r\n');
692
702
  return callback();
693
703
  }
694
- let child = this.childNodes[childId++];
704
+ const child = this.childNodes[childId++];
695
705
  outputStream.write((childId > 1 ? '\r\n' : '') + '--' + this.boundary + '\r\n');
696
706
  child.stream(outputStream, options, err => {
697
707
  if (err) {
@@ -700,41 +710,37 @@ class MimeNode {
700
710
  setImmediate(processChildNode);
701
711
  });
702
712
  };
703
-
704
713
  if (this.multipart) {
705
714
  setImmediate(processChildNode);
706
- } else {
715
+ }
716
+ else {
707
717
  return callback();
708
718
  }
709
719
  };
710
-
711
720
  // pushes node content
712
- let sendContent = () => {
721
+ const sendContent = () => {
713
722
  if (this.content) {
714
723
  if (Object.prototype.toString.call(this.content) === '[object Error]') {
715
724
  // content is already errored
716
725
  return callback(this.content);
717
726
  }
718
-
719
727
  if (typeof this.content.pipe === 'function') {
720
728
  this.content.removeListener('error', this._contentErrorHandler);
721
729
  this._contentErrorHandler = err => callback(err);
722
730
  this.content.once('error', this._contentErrorHandler);
723
731
  }
724
-
725
- let createStream = () => {
732
+ const createStream = () => {
726
733
  if (['quoted-printable', 'base64'].includes(transferEncoding)) {
727
734
  contentStream = new (transferEncoding === 'base64' ? base64 : qp).Encoder(options);
728
-
729
735
  contentStream.pipe(outputStream, {
730
736
  end: false
731
737
  });
732
738
  contentStream.once('end', finalize);
733
739
  contentStream.once('error', err => callback(err));
734
-
735
740
  localStream = this._getStream(this.content);
736
741
  localStream.pipe(contentStream);
737
- } else {
742
+ }
743
+ else {
738
744
  // anything that is not QP or Base54 passes as-is
739
745
  localStream = this._getStream(this.content);
740
746
  localStream.pipe(outputStream, {
@@ -742,15 +748,13 @@ class MimeNode {
742
748
  });
743
749
  localStream.once('end', finalize);
744
750
  }
745
-
746
751
  localStream.once('error', err => callback(err));
747
752
  };
748
-
749
753
  if (this.content._resolve) {
750
- let chunks = [];
754
+ const chunks = [];
751
755
  let chunklen = 0;
752
756
  let returned = false;
753
- let sourceStream = this._getStream(this.content);
757
+ const sourceStream = this._getStream(this.content);
754
758
  sourceStream.on('error', err => {
755
759
  if (returned) {
756
760
  return;
@@ -774,136 +778,125 @@ class MimeNode {
774
778
  this.content._resolvedValue = Buffer.concat(chunks, chunklen);
775
779
  setImmediate(createStream);
776
780
  });
777
- } else {
781
+ }
782
+ else {
778
783
  setImmediate(createStream);
779
784
  }
780
785
  return;
781
- } else {
782
- return setImmediate(finalize);
783
786
  }
787
+ return setImmediate(finalize);
784
788
  };
785
-
786
789
  if (this._raw) {
787
790
  setImmediate(() => {
788
791
  if (Object.prototype.toString.call(this._raw) === '[object Error]') {
789
792
  // content is already errored
790
793
  return callback(this._raw);
791
794
  }
792
-
793
795
  // remove default error handler (if set)
794
796
  if (typeof this._raw.pipe === 'function') {
795
797
  this._raw.removeListener('error', this._contentErrorHandler);
796
798
  }
797
-
798
- let raw = this._getStream(this._raw);
799
+ const raw = this._getStream(this._raw);
799
800
  raw.pipe(outputStream, {
800
801
  end: false
801
802
  });
802
803
  raw.on('error', err => outputStream.emit('error', err));
803
804
  raw.on('end', finalize);
804
805
  });
805
- } else {
806
+ }
807
+ else {
806
808
  outputStream.write(this.buildHeaders() + '\r\n\r\n');
807
809
  setImmediate(sendContent);
808
810
  }
809
811
  }
810
-
811
812
  /**
812
813
  * Sets envelope to be used instead of the generated one
813
814
  *
814
- * @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']}
815
816
  */
816
817
  setEnvelope(envelope) {
817
818
  let list;
818
-
819
819
  this._envelope = {
820
820
  from: false,
821
821
  to: []
822
822
  };
823
-
824
823
  if (envelope.from) {
825
824
  list = [];
826
- this._convertAddresses(this._parseAddresses(envelope.from), list);
825
+ this._convertAddresses(this._parseEnvelopeAddresses(envelope.from), list);
827
826
  list = list.filter(address => address && address.address);
828
827
  if (list.length && list[0]) {
829
828
  this._envelope.from = list[0].address;
830
829
  }
831
830
  }
831
+ const seenRecipients = new Set();
832
+ const recipients = [];
832
833
  ['to', 'cc', 'bcc'].forEach(key => {
833
834
  if (envelope[key]) {
834
- this._convertAddresses(this._parseAddresses(envelope[key]), this._envelope.to);
835
+ this._convertAddresses(this._parseEnvelopeAddresses(envelope[key]), recipients, seenRecipients);
835
836
  }
836
837
  });
837
-
838
- this._envelope.to = this._envelope.to.map(to => to.address).filter(address => address);
839
-
840
- let standardFields = ['to', 'cc', 'bcc', 'from'];
841
- Object.keys(envelope).forEach(key => {
842
- if (!standardFields.includes(key)) {
843
- this._envelope[key] = envelope[key];
844
- }
845
- });
846
-
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));
847
841
  return this;
848
842
  }
849
-
850
843
  /**
851
844
  * Generates and returns an object with parsed address fields
852
845
  *
853
- * @return {Object} Address object
846
+ * @return Address object
854
847
  */
855
848
  getAddresses() {
856
- let addresses = {};
857
-
849
+ const addresses = {};
850
+ const seenByKey = new Map();
858
851
  this._headers.forEach(header => {
859
- let key = header.key.toLowerCase();
852
+ const key = header.key.toLowerCase();
860
853
  if (['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].includes(key)) {
861
854
  if (!Array.isArray(addresses[key])) {
862
855
  addresses[key] = [];
856
+ seenByKey.set(key, new Set());
863
857
  }
864
-
865
- this._convertAddresses(this._parseAddresses(header.value), addresses[key]);
858
+ this._convertAddresses(this._parseAddresses(header.value), addresses[key], seenByKey.get(key));
866
859
  }
867
860
  });
868
-
869
861
  return addresses;
870
862
  }
871
-
872
863
  /**
873
864
  * Generates and returns SMTP envelope with the sender address and a list of recipients addresses
874
865
  *
875
- * @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']}
876
867
  */
877
868
  getEnvelope() {
878
869
  if (this._envelope) {
879
870
  return this._envelope;
880
871
  }
881
-
882
- let envelope = {
872
+ const envelope = {
883
873
  from: false,
884
874
  to: []
885
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 = [];
886
881
  this._headers.forEach(header => {
887
- let list = [];
882
+ const list = [];
888
883
  if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].includes(header.key))) {
889
884
  this._convertAddresses(this._parseAddresses(header.value), list);
890
885
  if (list.length && list[0]) {
891
886
  envelope.from = list[0].address;
892
887
  }
893
- } else if (['To', 'Cc', 'Bcc'].includes(header.key)) {
894
- 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);
895
891
  }
896
892
  });
897
-
898
- envelope.to = envelope.to.map(to => to.address);
899
-
893
+ envelope.to = recipients.map(to => to.address);
900
894
  return envelope;
901
895
  }
902
-
903
896
  /**
904
897
  * Returns Message-Id value. If it does not exist, then creates one
905
898
  *
906
- * @return {String} Message-Id value
899
+ * @return Message-Id value
907
900
  */
908
901
  messageId() {
909
902
  let messageId = this.getHeader('Message-ID');
@@ -914,15 +907,13 @@ class MimeNode {
914
907
  }
915
908
  return messageId;
916
909
  }
917
-
918
910
  /**
919
911
  * Sets pregenerated content that will be used as the output of this node
920
912
  *
921
- * @param {String|Buffer|Stream} Raw MIME contents
913
+ * @param raw Raw MIME contents
922
914
  */
923
915
  setRaw(raw) {
924
916
  this._raw = raw;
925
-
926
917
  if (this._raw && typeof this._raw.pipe === 'function') {
927
918
  // pre-stream handler. might be triggered if a stream is set as content
928
919
  // and 'error' fires before anything is done with this stream
@@ -932,147 +923,271 @@ class MimeNode {
932
923
  };
933
924
  this._raw.once('error', this._contentErrorHandler);
934
925
  }
935
-
936
926
  return this;
937
927
  }
938
-
939
928
  /////// PRIVATE METHODS
940
-
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
+ }
941
950
  /**
942
951
  * Detects and returns handle to a stream related with the content.
943
952
  *
944
- * @param {Mixed} content Node content
945
- * @returns {Object} Stream object
953
+ * @param content Node content
954
+ * @returns Stream object
955
+ * @internal
946
956
  */
947
957
  _getStream(content) {
948
958
  let contentStream;
949
-
950
959
  if (content._resolvedValue) {
951
960
  // pass string or buffer content as a stream
952
961
  contentStream = new PassThrough();
953
-
954
962
  setImmediate(() => {
955
963
  try {
956
964
  contentStream.end(content._resolvedValue);
957
- } catch (err) {
958
- contentStream.emit('error', err);
965
+ }
966
+ catch (_err) {
967
+ contentStream.emit('error', _err);
959
968
  }
960
969
  });
961
-
962
970
  return contentStream;
963
- } else if (typeof content.pipe === 'function') {
971
+ }
972
+ if (typeof content.pipe === 'function') {
964
973
  // assume as stream
965
974
  return content;
966
- } else if (content && typeof content.path === 'string' && !content.href) {
967
- if (this.disableFileAccess) {
975
+ }
976
+ if (content && typeof content.path === 'string' && !content.href) {
977
+ if (this._accessDisabled('disableFileAccess')) {
968
978
  contentStream = new PassThrough();
969
- 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
+ });
970
984
  return contentStream;
971
985
  }
972
986
  // read file
973
987
  return fs.createReadStream(content.path);
974
- } else if (content && typeof content.href === 'string') {
975
- if (this.disableUrlAccess) {
988
+ }
989
+ if (content && typeof content.href === 'string') {
990
+ if (this._accessDisabled('disableUrlAccess')) {
976
991
  contentStream = new PassThrough();
977
- 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
+ });
978
997
  return contentStream;
979
998
  }
980
- // fetch URL
981
- return nmfetch(content.href, { headers: content.httpHeaders });
982
- } else {
983
- // pass string or buffer content as a stream
984
- contentStream = new PassThrough();
985
-
986
- setImmediate(() => {
987
- try {
988
- contentStream.end(content || '');
989
- } catch (err) {
990
- contentStream.emit('error', err);
991
- }
992
- });
993
- return contentStream;
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 });
994
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;
995
1015
  }
996
-
997
1016
  /**
998
1017
  * Parses addresses. Takes in a single address or an array or an
999
1018
  * array of address arrays (eg. To: [[first group], [second group],...])
1000
1019
  *
1001
- * @param {Mixed} addresses Addresses to be parsed
1002
- * @return {Array} An array of address objects
1020
+ * @param addresses Addresses to be parsed
1021
+ * @return An array of address objects
1022
+ * @internal
1003
1023
  */
1004
1024
  _parseAddresses(addresses) {
1005
- return [].concat.apply(
1006
- [],
1007
- [].concat(addresses).map(address => {
1008
- // eslint-disable-line prefer-spread
1009
- if (address && address.address) {
1010
- address.address = this._normalizeAddress(address.address);
1011
- address.name = address.name || '';
1012
- return [address];
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;
1013
1057
  }
1014
- return addressparser(address);
1015
- })
1016
- );
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
+ });
1017
1117
  }
1018
-
1019
1118
  /**
1020
1119
  * Normalizes a header key, uses Camel-Case form, except for uppercase MIME-
1021
1120
  *
1022
- * @param {String} key Key to be normalized
1023
- * @return {String} key in Camel-Case form
1121
+ * @param key Key to be normalized
1122
+ * @return key in Camel-Case form
1123
+ * @internal
1024
1124
  */
1025
1125
  _normalizeHeaderKey(key) {
1026
1126
  key = (key || '')
1027
1127
  .toString()
1028
1128
  // no newlines in keys
1029
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, '')
1030
1133
  .trim()
1031
1134
  .toLowerCase()
1032
1135
  // use uppercase words, except MIME
1033
1136
  .replace(/^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, c => c.toUpperCase())
1034
1137
  // special case
1035
1138
  .replace(/^Content-Features$/i, 'Content-features');
1036
-
1037
1139
  return key;
1038
1140
  }
1039
-
1040
1141
  /**
1041
1142
  * Checks if the content type is multipart and defines boundary if needed.
1042
1143
  * Doesn't return anything, modifies object argument instead.
1043
1144
  *
1044
- * @param {Object} structured Parsed header value for 'Content-Type' key
1145
+ * @param structured Parsed header value for 'Content-Type' key
1146
+ * @internal
1045
1147
  */
1046
1148
  _handleContentType(structured) {
1047
1149
  this.contentType = structured.value.trim().toLowerCase();
1048
-
1049
1150
  this.multipart = /^multipart\//i.test(this.contentType) ? this.contentType.substr(this.contentType.indexOf('/') + 1) : false;
1050
-
1051
1151
  if (this.multipart) {
1052
- this.boundary = structured.params.boundary = structured.params.boundary || this.boundary || this._generateBoundary();
1053
- } 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 {
1054
1168
  this.boundary = false;
1055
1169
  }
1056
1170
  }
1057
-
1058
1171
  /**
1059
1172
  * Generates a multipart boundary value
1060
1173
  *
1061
- * @return {String} boundary value
1174
+ * @return boundary value
1175
+ * @internal
1062
1176
  */
1063
1177
  _generateBoundary() {
1064
- 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;
1065
1181
  }
1066
-
1067
1182
  /**
1068
1183
  * Encodes a header value for use in the generated rfc2822 email.
1069
1184
  *
1070
- * @param {String} key Header key
1071
- * @param {String} value Header value
1185
+ * @param key Header key
1186
+ * @param value Header value
1187
+ * @internal
1072
1188
  */
1073
1189
  _encodeHeaderValue(key, value) {
1074
1190
  key = this._normalizeHeaderKey(key);
1075
-
1076
1191
  switch (key) {
1077
1192
  // Structured headers
1078
1193
  case 'From':
@@ -1082,172 +1197,206 @@ class MimeNode {
1082
1197
  case 'Bcc':
1083
1198
  case 'Reply-To':
1084
1199
  return this._convertAddresses(this._parseAddresses(value));
1085
-
1086
1200
  // values enclosed in <>
1087
1201
  case 'Message-ID':
1088
1202
  case 'In-Reply-To':
1089
1203
  case 'Content-Id':
1090
- value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
1091
-
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, '');
1092
1211
  if (value.charAt(0) !== '<') {
1093
1212
  value = '<' + value;
1094
1213
  }
1095
-
1096
1214
  if (value.charAt(value.length - 1) !== '>') {
1097
1215
  value = value + '>';
1098
1216
  }
1099
1217
  return value;
1100
-
1101
1218
  // space separated list of values enclosed in <>
1102
1219
  case 'References':
1103
1220
  value = [].concat
1104
- .apply(
1105
- [],
1106
- [].concat(value || '').map(elm => {
1107
- // eslint-disable-line prefer-spread
1108
- elm = (elm || '')
1109
- .toString()
1110
- .replace(/\r?\n|\r/g, ' ')
1111
- .trim();
1112
- return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
1113
- })
1114
- )
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
+ }))
1115
1229
  .map(elm => {
1116
- if (elm.charAt(0) !== '<') {
1117
- elm = '<' + elm;
1118
- }
1119
- if (elm.charAt(elm.length - 1) !== '>') {
1120
- elm = elm + '>';
1121
- }
1122
- return elm;
1123
- });
1124
-
1230
+ if (elm.charAt(0) !== '<') {
1231
+ elm = '<' + elm;
1232
+ }
1233
+ if (elm.charAt(elm.length - 1) !== '>') {
1234
+ elm = elm + '>';
1235
+ }
1236
+ return elm;
1237
+ });
1125
1238
  return value.join(' ').trim();
1126
-
1127
1239
  case 'Date':
1128
1240
  if (Object.prototype.toString.call(value) === '[object Date]') {
1129
1241
  return value.toUTCString().replace(/GMT/, '+0000');
1130
1242
  }
1131
-
1132
1243
  value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
1133
- return this._encodeWords(value);
1134
-
1244
+ return this._encodeHeaderText(value);
1135
1245
  case 'Content-Type':
1136
1246
  case 'Content-Disposition':
1137
1247
  // if it includes a filename then it is already encoded
1138
1248
  return (value || '').toString().replace(/\r?\n|\r/g, ' ');
1139
-
1140
1249
  default:
1141
1250
  value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
1142
- // encodeWords only encodes if needed, otherwise the original string is returned
1143
- return this._encodeWords(value);
1251
+ return this._encodeHeaderText(value);
1144
1252
  }
1145
1253
  }
1146
-
1147
1254
  /**
1148
1255
  * Rebuilds address object using punycode and other adjustments
1149
1256
  *
1150
- * @param {Array} addresses An array of address objects
1151
- * @param {Array} [uniqueList] An array to be populated with addresses
1152
- * @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
1153
1262
  */
1154
- _convertAddresses(addresses, uniqueList) {
1155
- let values = [];
1156
-
1263
+ _convertAddresses(addresses, uniqueList, seenAddresses) {
1264
+ const values = [];
1157
1265
  uniqueList = uniqueList || [];
1158
-
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
+ }
1159
1276
  [].concat(addresses || []).forEach(address => {
1160
1277
  if (address.address) {
1161
1278
  address.address = this._normalizeAddress(address.address);
1162
-
1163
1279
  if (!address.name) {
1164
- values.push(address.address.indexOf(' ') >= 0 ? `<${address.address}>` : `${address.address}`);
1165
- } 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 {
1166
1287
  values.push(`${this._encodeAddressName(address.name)} <${address.address}>`);
1167
1288
  }
1168
-
1169
- if (address.address) {
1170
- if (!uniqueList.filter(a => a.address === address.address).length) {
1171
- uniqueList.push(address);
1172
- }
1289
+ if (!seenAddresses.has(address.address)) {
1290
+ seenAddresses.add(address.address);
1291
+ uniqueList.push(address);
1173
1292
  }
1174
- } else if (address.group) {
1175
- 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();
1176
1296
  values.push(`${this._encodeAddressName(address.name)}:${groupListAddresses};`);
1177
1297
  }
1178
1298
  });
1179
-
1180
1299
  return values.join(', ');
1181
1300
  }
1182
-
1183
1301
  /**
1184
1302
  * Normalizes an email address
1185
1303
  *
1186
- * @param {Array} address An array of address objects
1187
- * @return {String} address string
1304
+ * @param address An array of address objects
1305
+ * @return address string
1306
+ * @internal
1188
1307
  */
1189
1308
  _normalizeAddress(address) {
1190
1309
  address = (address || '')
1191
1310
  .toString()
1192
- .replace(/[\x00-\x1F<>]+/g, ' ') // remove unallowed characters
1311
+ .replace(/[\x00-\x1F\x7F<>]+/g, ' ') // remove unallowed characters
1193
1312
  .trim();
1194
-
1195
- let lastAt = address.lastIndexOf('@');
1196
- if (lastAt < 0) {
1197
- // Bare username
1313
+ if (!address) {
1314
+ // callers use an empty value to detect a missing address
1198
1315
  return address;
1199
1316
  }
1200
-
1201
- let user = address.substr(0, lastAt);
1202
- let domain = address.substr(lastAt + 1);
1203
-
1204
- // Usernames are not touched and are kept as is even if these include unicode
1205
- // Domains are punycoded by default
1206
- // 'jõgeva.ee' will be converted to 'xn--jgeva-dua.ee'
1207
- // non-unicode domains are left as is
1208
-
1209
- let encodedDomain;
1210
-
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);
1211
1334
  try {
1212
- encodedDomain = punycode.toASCII(domain.toLowerCase());
1213
- } catch (err) {
1214
- // keep as is?
1335
+ encodedDomain = normalizeDomain(domain.toLowerCase(), smtputf8);
1215
1336
  }
1216
-
1217
- if (user.indexOf(' ') >= 0) {
1218
- if (user.charAt(0) !== '"') {
1219
- user = '"' + user;
1220
- }
1221
- if (user.substr(-1) !== '"') {
1222
- user = user + '"';
1223
- }
1337
+ catch (_err) {
1338
+ // keep domain as supplied
1224
1339
  }
1225
-
1226
- 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);
1227
1360
  }
1228
-
1229
1361
  /**
1230
1362
  * If needed, mime encodes the name part
1231
1363
  *
1232
- * @param {String} name Name part of an address
1233
- * @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
1234
1367
  */
1235
1368
  _encodeAddressName(name) {
1236
- if (!/^[\w ']*$/.test(name)) {
1369
+ if (!/^[\w ]*$/.test(name)) {
1237
1370
  if (/^[\x20-\x7e]*$/.test(name)) {
1238
- return '"' + name.replace(/([\\"])/g, '\\$1') + '"';
1239
- } else {
1371
+ return mimeFuncs.quoteString(name);
1372
+ }
1373
+ else {
1240
1374
  return mimeFuncs.encodeWord(name, this._getTextEncoding(name), 52);
1241
1375
  }
1242
1376
  }
1243
1377
  return name;
1244
1378
  }
1245
-
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
+ }
1246
1394
  /**
1247
1395
  * If needed, mime encodes the name part
1248
1396
  *
1249
- * @param {String} name Name part of an address
1250
- * @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
1251
1400
  */
1252
1401
  _encodeWords(value) {
1253
1402
  // set encodeAll parameter to true even though it is against the recommendation of RFC2047,
@@ -1255,51 +1404,50 @@ class MimeNode {
1255
1404
  // but some clients (eg. Zimbra) do not handle it properly and remove surrounding whitespace
1256
1405
  return mimeFuncs.encodeWords(value, this._getTextEncoding(value), 52, true);
1257
1406
  }
1258
-
1259
1407
  /**
1260
1408
  * Detects best mime encoding for a text value
1261
1409
  *
1262
- * @param {String} value Value to check for
1263
- * @return {String} either 'Q' or 'B'
1410
+ * @param value Value to check for
1411
+ * @return either 'Q' or 'B'
1412
+ * @internal
1264
1413
  */
1265
1414
  _getTextEncoding(value) {
1266
1415
  value = (value || '').toString();
1267
-
1268
- let encoding = this.textEncoding;
1269
- let latinLen;
1270
- let nonLatinLen;
1271
-
1272
- if (!encoding) {
1273
- // count latin alphabet symbols and 8-bit range symbols + control symbols
1274
- // if there are more latin characters, then use quoted-printable
1275
- // encoding, otherwise use base64
1276
- nonLatinLen = (value.match(/[\x00-\x08\x0B\x0C\x0E-\x1F\u0080-\uFFFF]/g) || []).length; // eslint-disable-line no-control-regex
1277
- latinLen = (value.match(/[a-z]/gi) || []).length;
1278
- // if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
1279
- encoding = nonLatinLen < latinLen ? 'Q' : 'B';
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
+ }
1280
1432
  }
1281
- return encoding;
1433
+ // if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
1434
+ return nonLatinLen < latinLen ? 'Q' : 'B';
1282
1435
  }
1283
-
1284
1436
  /**
1285
1437
  * Generates a message id
1286
1438
  *
1287
- * @return {String} Random Message-ID value
1439
+ * @return Random Message-ID value
1440
+ * @internal
1288
1441
  */
1289
1442
  _generateMessageId() {
1290
- return (
1291
- '<' +
1443
+ return ('<' +
1292
1444
  [2, 2, 2, 6].reduce(
1293
- // crux to generate UUID-like random strings
1294
- (prev, len) => prev + '-' + crypto.randomBytes(len).toString('hex'),
1295
- crypto.randomBytes(4).toString('hex')
1296
- ) +
1445
+ // crux to generate UUID-like random strings
1446
+ (prev, len) => prev + '-' + crypto.randomBytes(len).toString('hex'), crypto.randomBytes(4).toString('hex')) +
1297
1447
  '@' +
1298
1448
  // try to use the domain of the FROM address or fallback to server hostname
1299
1449
  (this.getEnvelope().from || this.hostname || 'localhost').split('@').pop() +
1300
- '>'
1301
- );
1450
+ '>');
1302
1451
  }
1303
1452
  }
1304
-
1305
- module.exports = MimeNode;
1453
+ export default MimeNode;