nodemailer 9.1.1 → 10.0.0

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 (172) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +7 -3
  3. package/SECURITY.md +7 -7
  4. package/dist/cjs/addressparser/index.d.ts +47 -0
  5. package/dist/cjs/addressparser/index.js +508 -0
  6. package/dist/cjs/base64/index.d.ts +40 -0
  7. package/{lib → dist/cjs}/base64/index.js +31 -53
  8. package/dist/cjs/dkim/index.d.ts +40 -0
  9. package/dist/cjs/dkim/index.js +211 -0
  10. package/dist/cjs/dkim/message-parser.d.ts +41 -0
  11. package/dist/cjs/dkim/message-parser.js +147 -0
  12. package/dist/cjs/dkim/relaxed-body.d.ts +46 -0
  13. package/dist/cjs/dkim/relaxed-body.js +143 -0
  14. package/dist/cjs/dkim/sign.d.ts +49 -0
  15. package/dist/cjs/dkim/sign.js +148 -0
  16. package/dist/cjs/errors.d.ts +95 -0
  17. package/dist/cjs/errors.js +70 -0
  18. package/dist/cjs/fetch/cookies.d.ts +94 -0
  19. package/dist/cjs/fetch/cookies.js +268 -0
  20. package/dist/cjs/fetch/index.d.ts +51 -0
  21. package/dist/cjs/fetch/index.js +409 -0
  22. package/dist/cjs/json-transport/index.d.ts +54 -0
  23. package/dist/cjs/json-transport/index.js +97 -0
  24. package/dist/cjs/mail-composer/index.d.ts +224 -0
  25. package/dist/cjs/mail-composer/index.js +602 -0
  26. package/dist/cjs/mailer/index.d.ts +226 -0
  27. package/dist/cjs/mailer/index.js +401 -0
  28. package/dist/cjs/mailer/mail-message.d.ts +94 -0
  29. package/dist/cjs/mailer/mail-message.js +339 -0
  30. package/dist/cjs/mime-funcs/index.d.ts +157 -0
  31. package/dist/cjs/mime-funcs/index.js +670 -0
  32. package/dist/cjs/mime-funcs/mime-types.d.ts +2 -0
  33. package/dist/cjs/mime-funcs/mime-types.js +2103 -0
  34. package/dist/cjs/mime-node/index.d.ts +484 -0
  35. package/dist/cjs/mime-node/index.js +1422 -0
  36. package/dist/cjs/mime-node/last-newline.d.ts +7 -0
  37. package/dist/cjs/mime-node/last-newline.js +30 -0
  38. package/dist/cjs/mime-node/le-unix.d.ts +13 -0
  39. package/dist/cjs/mime-node/le-unix.js +39 -0
  40. package/dist/cjs/mime-node/le-windows.d.ts +14 -0
  41. package/dist/cjs/mime-node/le-windows.js +46 -0
  42. package/dist/cjs/nodemailer.d.ts +102 -0
  43. package/dist/cjs/nodemailer.js +207 -0
  44. package/dist/cjs/package-info.d.ts +3 -0
  45. package/dist/cjs/package-info.js +7 -0
  46. package/dist/cjs/package.json +3 -0
  47. package/dist/cjs/punycode/index.d.ts +73 -0
  48. package/{lib → dist/cjs}/punycode/index.js +61 -111
  49. package/dist/cjs/qp/index.d.ts +33 -0
  50. package/{lib → dist/cjs}/qp/index.js +30 -64
  51. package/dist/cjs/sendmail-transport/index.d.ts +69 -0
  52. package/dist/cjs/sendmail-transport/index.js +210 -0
  53. package/dist/cjs/ses-transport/index.d.ts +75 -0
  54. package/dist/cjs/ses-transport/index.js +248 -0
  55. package/dist/cjs/shared/index.d.ts +230 -0
  56. package/dist/cjs/shared/index.js +665 -0
  57. package/dist/cjs/shared/objects.d.ts +23 -0
  58. package/dist/cjs/shared/objects.js +43 -0
  59. package/dist/cjs/shared/url.d.ts +21 -0
  60. package/dist/cjs/shared/url.js +255 -0
  61. package/dist/cjs/smtp-connection/data-stream.d.ts +22 -0
  62. package/dist/cjs/smtp-connection/data-stream.js +98 -0
  63. package/dist/cjs/smtp-connection/http-proxy-client.d.ts +39 -0
  64. package/dist/cjs/smtp-connection/http-proxy-client.js +177 -0
  65. package/dist/cjs/smtp-connection/index.d.ts +719 -0
  66. package/dist/cjs/smtp-connection/index.js +1676 -0
  67. package/dist/cjs/smtp-pool/index.d.ts +163 -0
  68. package/dist/cjs/smtp-pool/index.js +530 -0
  69. package/dist/cjs/smtp-pool/pool-resource.d.ts +64 -0
  70. package/dist/cjs/smtp-pool/pool-resource.js +261 -0
  71. package/dist/cjs/smtp-transport/index.d.ts +143 -0
  72. package/dist/cjs/smtp-transport/index.js +379 -0
  73. package/dist/cjs/stream-transport/index.d.ts +63 -0
  74. package/dist/cjs/stream-transport/index.js +146 -0
  75. package/dist/cjs/well-known/index.d.ts +35 -0
  76. package/dist/cjs/well-known/index.js +41 -0
  77. package/dist/cjs/well-known/services.d.ts +2 -0
  78. package/dist/cjs/well-known/services.js +666 -0
  79. package/dist/cjs/xoauth2/index.d.ts +191 -0
  80. package/dist/cjs/xoauth2/index.js +414 -0
  81. package/dist/esm/addressparser/index.d.ts +47 -0
  82. package/{lib → dist/esm}/addressparser/index.js +49 -102
  83. package/dist/esm/base64/index.d.ts +40 -0
  84. package/dist/esm/base64/index.js +110 -0
  85. package/dist/esm/dkim/index.d.ts +40 -0
  86. package/{lib → dist/esm}/dkim/index.js +24 -70
  87. package/dist/esm/dkim/message-parser.d.ts +41 -0
  88. package/{lib → dist/esm}/dkim/message-parser.js +28 -40
  89. package/dist/esm/dkim/relaxed-body.d.ts +46 -0
  90. package/dist/esm/dkim/relaxed-body.js +135 -0
  91. package/dist/esm/dkim/sign.d.ts +49 -0
  92. package/{lib → dist/esm}/dkim/sign.js +32 -45
  93. package/dist/esm/errors.d.ts +95 -0
  94. package/{lib → dist/esm}/errors.js +26 -18
  95. package/dist/esm/fetch/cookies.d.ts +94 -0
  96. package/{lib → dist/esm}/fetch/cookies.js +75 -121
  97. package/dist/esm/fetch/index.d.ts +51 -0
  98. package/{lib → dist/esm}/fetch/index.js +46 -88
  99. package/dist/esm/json-transport/index.d.ts +54 -0
  100. package/{lib → dist/esm}/json-transport/index.js +17 -39
  101. package/dist/esm/mail-composer/index.d.ts +224 -0
  102. package/{lib → dist/esm}/mail-composer/index.js +187 -264
  103. package/dist/esm/mailer/index.d.ts +226 -0
  104. package/{lib → dist/esm}/mailer/index.js +109 -229
  105. package/dist/esm/mailer/mail-message.d.ts +94 -0
  106. package/{lib → dist/esm}/mailer/mail-message.js +59 -114
  107. package/dist/esm/mime-funcs/index.d.ts +157 -0
  108. package/dist/esm/mime-funcs/index.js +621 -0
  109. package/dist/esm/mime-funcs/mime-types.d.ts +2 -0
  110. package/{lib → dist/esm}/mime-funcs/mime-types.js +29 -42
  111. package/dist/esm/mime-node/index.d.ts +484 -0
  112. package/{lib → dist/esm}/mime-node/index.js +177 -361
  113. package/dist/esm/mime-node/last-newline.d.ts +7 -0
  114. package/{lib → dist/esm}/mime-node/last-newline.js +2 -10
  115. package/dist/esm/mime-node/le-unix.d.ts +13 -0
  116. package/{lib → dist/esm}/mime-node/le-unix.js +5 -11
  117. package/dist/esm/mime-node/le-windows.d.ts +14 -0
  118. package/{lib → dist/esm}/mime-node/le-windows.js +5 -13
  119. package/dist/esm/nodemailer.d.ts +102 -0
  120. package/{lib → dist/esm}/nodemailer.js +65 -69
  121. package/dist/esm/package-info.d.ts +3 -0
  122. package/dist/esm/package-info.js +4 -0
  123. package/dist/esm/package.json +3 -0
  124. package/dist/esm/punycode/index.d.ts +73 -0
  125. package/dist/esm/punycode/index.js +404 -0
  126. package/dist/esm/qp/index.d.ts +33 -0
  127. package/dist/esm/qp/index.js +190 -0
  128. package/dist/esm/sendmail-transport/index.d.ts +69 -0
  129. package/{lib → dist/esm}/sendmail-transport/index.js +51 -101
  130. package/dist/esm/ses-transport/index.d.ts +75 -0
  131. package/dist/esm/ses-transport/index.js +208 -0
  132. package/dist/esm/shared/index.d.ts +230 -0
  133. package/{lib → dist/esm}/shared/index.js +137 -268
  134. package/dist/esm/shared/objects.d.ts +23 -0
  135. package/{lib → dist/esm}/shared/objects.js +13 -17
  136. package/dist/esm/shared/url.d.ts +21 -0
  137. package/dist/esm/shared/url.js +214 -0
  138. package/dist/esm/smtp-connection/data-stream.d.ts +22 -0
  139. package/{lib → dist/esm}/smtp-connection/data-stream.js +14 -26
  140. package/dist/esm/smtp-connection/http-proxy-client.d.ts +39 -0
  141. package/{lib → dist/esm}/smtp-connection/http-proxy-client.js +27 -64
  142. package/dist/esm/smtp-connection/index.d.ts +719 -0
  143. package/{lib → dist/esm}/smtp-connection/index.js +332 -713
  144. package/dist/esm/smtp-pool/index.d.ts +163 -0
  145. package/{lib → dist/esm}/smtp-pool/index.js +120 -271
  146. package/dist/esm/smtp-pool/pool-resource.d.ts +64 -0
  147. package/{lib → dist/esm}/smtp-pool/pool-resource.js +36 -72
  148. package/dist/esm/smtp-transport/index.d.ts +143 -0
  149. package/{lib → dist/esm}/smtp-transport/index.js +58 -148
  150. package/dist/esm/stream-transport/index.d.ts +63 -0
  151. package/{lib → dist/esm}/stream-transport/index.js +36 -74
  152. package/dist/esm/well-known/index.d.ts +35 -0
  153. package/{lib → dist/esm}/well-known/index.js +5 -16
  154. package/dist/esm/well-known/services.d.ts +2 -0
  155. package/dist/esm/well-known/services.js +663 -0
  156. package/dist/esm/xoauth2/index.d.ts +191 -0
  157. package/{lib → dist/esm}/xoauth2/index.js +101 -175
  158. package/package.json +139 -14
  159. package/.gitattributes +0 -6
  160. package/.ncurc.js +0 -9
  161. package/.prettierignore +0 -8
  162. package/.prettierrc +0 -12
  163. package/.prettierrc.js +0 -10
  164. package/.release-please-config.json +0 -9
  165. package/CLAUDE.md +0 -56
  166. package/CODE_OF_CONDUCT.md +0 -76
  167. package/eslint.config.js +0 -88
  168. package/lib/dkim/relaxed-body.js +0 -154
  169. package/lib/mime-funcs/index.js +0 -671
  170. package/lib/ses-transport/index.js +0 -266
  171. package/lib/shared/url.js +0 -151
  172. /package/{lib → dist}/well-known/services.json +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [10.0.0](https://github.com/nodemailer/nodemailer/compare/v9.1.1...v10.0.0) (2026-09-03)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * Node.js 20 or newer is required. The Node.js 6 syntax compatibility check and the .npmignore file are gone.
9
+
10
+ ### Features
11
+
12
+ * keep the @types/nodemailer type layout working ([1cc5356](https://github.com/nodemailer/nodemailer/commit/1cc535627e1bb672214b09fa4cc503567275fd7f))
13
+ * migrate to TypeScript with ES module and CommonJS builds ([f7cbf83](https://github.com/nodemailer/nodemailer/commit/f7cbf8384e0ea6055efcc514afb8bc070ac3a2b1))
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * apply the other keys of a configuration object next to its url ([29610f9](https://github.com/nodemailer/nodemailer/commit/29610f9fe04dbd8af04c4a3362f3b68d293592da))
19
+ * **dkim:** canonicalize raw messages the way verifiers do ([2c84b11](https://github.com/nodemailer/nodemailer/commit/2c84b117d4d138751e9ffa8607f3796771bb5e66))
20
+ * keep a transporter assignable to the plain Transporter type ([8bf55fb](https://github.com/nodemailer/nodemailer/commit/8bf55fb177cf155b24d6bb86a7cd86670146d551))
21
+ * **shared:** keep a colon in the user name of a connection or proxy url ([6acf4b6](https://github.com/nodemailer/nodemailer/commit/6acf4b67d919971f516f58940113c9b8598b2254))
22
+ * **shared:** refuse URL hosts the legacy parser would truncate ([17a5068](https://github.com/nodemailer/nodemailer/commit/17a5068f06e62fc04e1d5545ed70d4248bef258e))
23
+ * **shared:** resolve hostnames when the runtime has no interface table ([8b03240](https://github.com/nodemailer/nodemailer/commit/8b032407135ea05ae377e623a7021705a4a139d0))
24
+ * **smtp-connection:** clear the timers of a connection dropped before the greeting ([01dcaa0](https://github.com/nodemailer/nodemailer/commit/01dcaa05b212aafc33747041027fb8c66497e0eb))
25
+ * **smtp-connection:** keep an incomplete server reply out of lastServerResponse ([1a6e427](https://github.com/nodemailer/nodemailer/commit/1a6e4271adb808509adf11832ad1adfdfc05c31b))
26
+ * **smtp-pool:** free the pool slot when the proxy socket can not be opened ([204a344](https://github.com/nodemailer/nodemailer/commit/204a344f7e4068ff41385195ee394a223ebadd84))
27
+ * **well-known:** keep nodemailer/lib/well-known/services.json available ([367730c](https://github.com/nodemailer/nodemailer/commit/367730cce85bc78c7804405c2cc7d89d93aafdd1))
28
+
3
29
  ## [9.1.1](https://github.com/nodemailer/nodemailer/compare/v9.1.0...v9.1.1) (2026-09-01)
4
30
 
5
31
 
package/README.md CHANGED
@@ -10,15 +10,19 @@ See [nodemailer.com](https://nodemailer.com/) for documentation and terms.
10
10
 
11
11
  > Nodemailer is developed by the team behind **[EmailEngine](https://emailengine.app/?utm_source=nodemailer-readme&utm_medium=readme&utm_campaign=oss-docs)**, a self-hosted email API that turns any Gmail, Microsoft 365, or IMAP account into a REST endpoint, with managed OAuth2, webhooks for incoming mail, and built-in [sending](https://emailengine.app/sending-emails?utm_source=nodemailer-readme&utm_medium=readme&utm_campaign=oss-docs). If you would rather call an HTTP API than maintain IMAP and SMTP connections yourself, that is what it is for.
12
12
 
13
+ ## Supported runtimes
14
+
15
+ Nodemailer targets Node.js 20 and later. The ES module build also runs on [Bun](https://bun.sh/) (tested against the latest release) and on [Cloudflare Workers](https://developers.cloudflare.com/workers/) with the `nodejs_compat` compatibility flag. On Workers only the SMTP based transports apply, `sendmail` needs a child process, and the runtime does not allow turning certificate validation off, so `tls.rejectUnauthorized: false` fails there with an error.
16
+
13
17
  ## Having an issue?
14
18
 
15
19
  #### First review the docs
16
20
 
17
21
  Documentation for Nodemailer can be found at [nodemailer.com](https://nodemailer.com/about/).
18
22
 
19
- #### Nodemailer throws a SyntaxError for "..."
23
+ #### Nodemailer fails to load or throws a SyntaxError
20
24
 
21
- You are using an older Node.js version than v6.0. Upgrade Node.js to get support for the spread operator. Nodemailer supports all Node.js versions starting from Node.js@v6.0.0.
25
+ Nodemailer 10 and later require Node.js 20 or newer. The package ships both ES module and CommonJS builds, so `import nodemailer from 'nodemailer'` and `const nodemailer = require('nodemailer')` both work. If you need to run on an older Node.js version, stay on the 9.x line.
22
26
 
23
27
  #### I'm having issues with Gmail
24
28
 
@@ -68,7 +72,7 @@ let configOptions = {
68
72
 
69
73
  #### I have an issue with TypeScript types
70
74
 
71
- Nodemailer has official support for Node.js only. For anything related to TypeScript, you need to directly contact the authors of the [type definitions](https://www.npmjs.com/package/@types/nodemailer).
75
+ Nodemailer 10 and later are written in TypeScript and ship their own type definitions, so `@types/nodemailer` is no longer needed and should be removed from your project to avoid conflicting declarations. The type names follow the layout of the old definitions, so references such as `Mail.Options`, `SMTPTransport.Options` or `Transporter<SMTPTransport.SentMessageInfo>` keep compiling, and the most used types (`SendMailOptions`, `Transporter`, `SentMessageInfo`, `Attachment`, `Address`) are exported from the package root. For older Nodemailer versions, the community maintained [type definitions](https://www.npmjs.com/package/@types/nodemailer) still apply.
72
76
 
73
77
  #### I have a different problem
74
78
 
package/SECURITY.md CHANGED
@@ -6,13 +6,13 @@ reports seriously and aim to respond quickly.
6
6
  ## Supported Versions
7
7
 
8
8
  Security fixes are released only against the latest major version. We do not
9
- backport patches to older majors upgrading to the current release line is the
10
- supported way to receive security updates.
9
+ backport patches to older majors, so upgrading to the current release line is
10
+ the supported way to receive security updates.
11
11
 
12
12
  | Version | Supported |
13
13
  | ------- | ------------------ |
14
- | 9.x | :white_check_mark: |
15
- | < 9.0 | :x: |
14
+ | 10.x | :white_check_mark: |
15
+ | < 10.0 | :x: |
16
16
 
17
17
  If you are on an older major, please upgrade. See the migration notes at
18
18
  <https://nodemailer.com/> before updating.
@@ -41,7 +41,7 @@ When reporting, please include as much of the following as you can:
41
41
  - Any suggested remediation, if you have one.
42
42
 
43
43
  Nodemailer is maintained by a single person, so there is no guaranteed response
44
- time sometimes reports are handled within hours, sometimes they take longer.
44
+ time: sometimes reports are handled within hours, sometimes they take longer.
45
45
  Accepted issues are fixed in a new release and coordinated through a GitHub
46
46
  Security Advisory, and reporters who wish to be named are credited.
47
47
 
@@ -49,12 +49,12 @@ Security Advisory, and reporters who wish to be named are credited.
49
49
 
50
50
  We track and disclose vulnerabilities through GitHub Security Advisories. We do
51
51
  not request or manage CVE identifiers ourselves. If you need a CVE assigned for a
52
- reported issue, please request one yourself for example, through GitHub's own
52
+ reported issue, please request one yourself, for example through GitHub's own
53
53
  CVE request flow on the published advisory, or another CNA.
54
54
 
55
55
  ## Scope
56
56
 
57
- In scope: the `nodemailer` package source in this repository message and MIME
57
+ In scope: the `nodemailer` package source in this repository: message and MIME
58
58
  generation, SMTP/LMTP client behaviour, address parsing, header handling, DKIM
59
59
  signing, and the bundled transports.
60
60
 
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Options for addressparser
3
+ */
4
+ export interface AddressParserOptions {
5
+ /** Flatten groups into a single list of mailboxes */
6
+ flatten?: boolean;
7
+ /** Internal recursion depth counter (do not set manually) */
8
+ _depth?: number;
9
+ }
10
+ /**
11
+ * A single mailbox. Either value may be an empty string when the input did not carry it
12
+ */
13
+ export interface MailboxAddress {
14
+ name: string;
15
+ address: string;
16
+ group?: undefined;
17
+ }
18
+ /**
19
+ * An address group. RFC 5322 does not allow nested groups, so any nesting is flattened
20
+ * into `group`
21
+ */
22
+ export interface GroupAddress {
23
+ name: string;
24
+ group: Address[];
25
+ address?: undefined;
26
+ }
27
+ /**
28
+ * A parsed address entry, either a mailbox or a group
29
+ */
30
+ export type Address = MailboxAddress | GroupAddress;
31
+ /**
32
+ * Parses structured e-mail addresses from an address field
33
+ *
34
+ * Example:
35
+ *
36
+ * 'Name <address@domain>'
37
+ *
38
+ * will be converted to
39
+ *
40
+ * [{name: 'Name', address: 'address@domain'}]
41
+ *
42
+ * @param str Address field
43
+ * @param options Optional options object
44
+ * @param options._depth Internal recursion depth counter (do not set manually)
45
+ * @return An array of address objects
46
+ */
47
+ export default function addressparser(str?: string | null, options?: AddressParserOptions): Address[];
@@ -0,0 +1,508 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = addressparser;
4
+ /**
5
+ * Restores the quoting of a local part that was read out of a quoted string.
6
+ *
7
+ * RFC 5321 allows '@' inside a quoted local part, so handing '"user@evil.com"@good.com'
8
+ * on as the bare 'user@evil.com@good.com' leaves it to the consumer which '@' splits the
9
+ * domain off. Getting that wrong is a misrouting vector, so the quotes go back on. The
10
+ * same holds for the other specials: a ',' or a ';' that loses its quotes reads as a
11
+ * recipient separator once the consumer puts the address back into a header.
12
+ *
13
+ * This module has no dependencies so that it can ship on its own, which is why the two
14
+ * grammar tests below are spelled out here instead of shared with src/mime-node. Keeping
15
+ * only what is ambiguous quoted is deliberate, mime-node applies the stricter RFC 5321
16
+ * dot-atom rule on top of this when it emits an address.
17
+ *
18
+ * @param address Address with an unquoted local part
19
+ * @return Address with the local part as a quoted-string
20
+ */
21
+ function _quoteLocalPart(address) {
22
+ const lastAt = address.lastIndexOf('@');
23
+ if (lastAt < 0) {
24
+ // no domain to split off, nothing can be misrouted
25
+ return address;
26
+ }
27
+ const user = address.substr(0, lastAt);
28
+ if (/^[^\s"(),:;<>@[\\\]]+$/.test(user) || /^"(?:[^"\\]|\\[\s\S])*"$/.test(user)) {
29
+ // a local part that carries no special reads the same with or without the quotes,
30
+ // and one that is already a complete quoted-string needs nothing either
31
+ return address;
32
+ }
33
+ return '"' + user.replace(/["\\]/g, '\\$&') + '"@' + address.substr(lastAt + 1);
34
+ }
35
+ /**
36
+ * Reached for every parsed address, so it is built once rather than per call.
37
+ */
38
+ const HAS_WHITESPACE = /\s/;
39
+ /**
40
+ * An addr-spec that carries its whitespace legally, inside a quoted local part. The
41
+ * optional tail is the malformed shape: a real mailbox with wreckage trailing it.
42
+ */
43
+ const QUOTED_LOCAL_ADDR = /^("(?:[^"\\]|\\[\s\S])*"@\S+)(?:\s+([\s\S]+))?$/;
44
+ /**
45
+ * One run holding a single '@' and no whitespace, the shape an addr-spec has to have.
46
+ */
47
+ const ADDR_SPEC = /^[^@\s]+@[^@\s]+$/;
48
+ /**
49
+ * The looser reading applied once the strict one finds nothing, which tolerates the
50
+ * further '@' that a domain should not have but malformed headers carry anyway.
51
+ */
52
+ const LOOSE_ADDR_SPEC = /^[^@\s]+@\S+$/;
53
+ /**
54
+ * Recovers the addr-spec from an angle-addr that came back holding unquoted whitespace.
55
+ *
56
+ * A malformed header can put more than a mailbox between the angle brackets, most often
57
+ * because the generator wrote the recipient twice: '<user@example.com user@example.com>'
58
+ * or '<example.com user@example.com>'. Whitespace is not addr-spec, so the whole run can
59
+ * never be a mailbox anyone could deliver to, and passing it on as the address loses the
60
+ * recipient that is sitting right there in the header.
61
+ *
62
+ * The run that still reads as an addr-spec is kept and whatever is left over becomes
63
+ * display text rather than being dropped. Candidates are read strictly first and then
64
+ * under the looser grammar, the same two tiers the unquoted-text branch below applies to
65
+ * the same problem, so that '<a@b@c.com junk>' and a bare 'a@b@c.com junk' agree on the
66
+ * recipient. When several runs qualify the first wins, which is what that branch's looser
67
+ * tier does within a token.
68
+ *
69
+ * A quoted local part is left alone: RFC 5321 allows whitespace inside it, so
70
+ * '<"user name"@example.com>' is well formed and means exactly what it says.
71
+ *
72
+ * @param data Collected address parts, mutated in place
73
+ */
74
+ function _recoverAddrSpec(data) {
75
+ if (!HAS_WHITESPACE.test(data.address)) {
76
+ return;
77
+ }
78
+ let address;
79
+ let rest;
80
+ const quoted = data.address.match(QUOTED_LOCAL_ADDR);
81
+ if (quoted) {
82
+ if (!quoted[2]) {
83
+ // the whitespace sits inside the quoted local part, this is a well formed mailbox
84
+ return;
85
+ }
86
+ // a real mailbox with wreckage trailing it, so peel the addr-spec off whole rather
87
+ // than splitting into the quotes
88
+ address = quoted[1];
89
+ rest = [quoted[2]];
90
+ }
91
+ else {
92
+ if (data.address.indexOf('"') >= 0) {
93
+ // Splitting on whitespace loses track of where the quoted string starts and ends,
94
+ // and this module does not take addresses out of quoted strings: the run picked out
95
+ // of '<junk "user@evil.com b"@good.com>' would be an address from the domain the
96
+ // quotes were hiding. Every well formed shape was already handled above, so what is
97
+ // left is wreckage either way and the original is the honest answer
98
+ return;
99
+ }
100
+ const parts = data.address.split(/\s+/);
101
+ let addrIndex = parts.findIndex(part => ADDR_SPEC.test(part));
102
+ if (addrIndex < 0) {
103
+ addrIndex = parts.findIndex(part => LOOSE_ADDR_SPEC.test(part));
104
+ }
105
+ if (addrIndex < 0) {
106
+ // nothing in there reads as an address, there is no better answer than the original
107
+ return;
108
+ }
109
+ address = parts.splice(addrIndex, 1)[0];
110
+ rest = parts;
111
+ }
112
+ data.address = address;
113
+ data.text = [data.text]
114
+ .concat(rest)
115
+ .filter(part => part)
116
+ .join(' ');
117
+ }
118
+ /**
119
+ * Converts tokens for a single address into an address object
120
+ *
121
+ * @param tokens Tokens object
122
+ * @param depth Current recursion depth for nested group protection
123
+ * @return Address object
124
+ */
125
+ function _handleAddress(tokens, depth) {
126
+ let isGroup = false;
127
+ let state = 'text';
128
+ const addresses = [];
129
+ const data = {
130
+ address: [],
131
+ comment: [],
132
+ group: [],
133
+ text: [],
134
+ textWasQuoted: []
135
+ };
136
+ let insideQuotes = false;
137
+ // Filter out <addresses>, (comments) and regular text
138
+ for (let i = 0, len = tokens.length; i < len; i++) {
139
+ const token = tokens[i];
140
+ const prevToken = i ? tokens[i - 1] : null;
141
+ if (token.type === 'operator') {
142
+ switch (token.value) {
143
+ case '<':
144
+ state = 'address';
145
+ insideQuotes = false;
146
+ break;
147
+ case '(':
148
+ state = 'comment';
149
+ insideQuotes = false;
150
+ break;
151
+ case ':':
152
+ state = 'group';
153
+ isGroup = true;
154
+ insideQuotes = false;
155
+ break;
156
+ case '"':
157
+ insideQuotes = !insideQuotes;
158
+ state = 'text';
159
+ break;
160
+ default:
161
+ state = 'text';
162
+ insideQuotes = false;
163
+ break;
164
+ }
165
+ }
166
+ else if (token.value) {
167
+ if (state === 'address') {
168
+ // Handle unquoted name that includes a "<".
169
+ // Apple Mail truncates everything between an unexpected < and an address.
170
+ token.value = token.value.replace(/^[^<]*<\s*/, '');
171
+ }
172
+ // A comment is folding whitespace. It may sit inside an addr-spec, on either side
173
+ // of the '@', but it cannot join two atoms into one: gluing across it would read
174
+ // 'user@example.com(x)evil.com' as the single domain 'example.comevil.com' and
175
+ // deliver to a domain the sender never named.
176
+ const parts = data[state];
177
+ const joins = prevToken &&
178
+ prevToken.noBreak &&
179
+ parts.length &&
180
+ (prevToken.value !== ')' || parts[parts.length - 1].slice(-1) === '@' || token.value.charAt(0) === '@');
181
+ if (joins) {
182
+ data[state][data[state].length - 1] += token.value;
183
+ if (state === 'text' && insideQuotes) {
184
+ data.textWasQuoted[data.textWasQuoted.length - 1] = true;
185
+ }
186
+ }
187
+ else {
188
+ data[state].push(token.value);
189
+ if (state === 'text') {
190
+ data.textWasQuoted.push(insideQuotes);
191
+ }
192
+ }
193
+ }
194
+ }
195
+ // If there is no text but a comment, replace the two
196
+ if (!data.text.length && data.comment.length) {
197
+ data.text = data.comment;
198
+ data.comment = [];
199
+ }
200
+ if (isGroup) {
201
+ // http://tools.ietf.org/html/rfc2822#appendix-A.1.3
202
+ data.text = data.text.join(' ');
203
+ // Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting)
204
+ let groupMembers = [];
205
+ if (data.group.length) {
206
+ const parsedGroup = addressparser(data.group.join(','), { _depth: depth + 1 });
207
+ parsedGroup.forEach(member => {
208
+ if (member.group) {
209
+ groupMembers = groupMembers.concat(member.group);
210
+ }
211
+ else {
212
+ groupMembers.push(member);
213
+ }
214
+ });
215
+ }
216
+ addresses.push({
217
+ name: data.text || '',
218
+ group: groupMembers
219
+ });
220
+ }
221
+ else {
222
+ // If no address was found, try to detect one from regular text
223
+ if (!data.address.length && data.text.length) {
224
+ for (let i = data.text.length - 1; i >= 0; i--) {
225
+ // Security: Do not extract email addresses from quoted strings.
226
+ // RFC 5321 allows @ inside quoted local-parts like "user@domain"@example.com.
227
+ // Extracting emails from quoted text leads to misrouting vulnerabilities.
228
+ if (!data.textWasQuoted[i] && ADDR_SPEC.test(data.text[i])) {
229
+ data.address = data.text.splice(i, 1);
230
+ data.textWasQuoted.splice(i, 1);
231
+ break;
232
+ }
233
+ }
234
+ // Try a looser regex match if strict match found nothing
235
+ if (!data.address.length) {
236
+ let extracted = false;
237
+ for (let i = data.text.length - 1; i >= 0; i--) {
238
+ // Security: Do not extract email addresses from quoted strings
239
+ if (!data.textWasQuoted[i]) {
240
+ data.text[i] = data.text[i]
241
+ .replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, (match) => {
242
+ if (!extracted) {
243
+ data.address = [match.trim()];
244
+ extracted = true;
245
+ return ' ';
246
+ }
247
+ return match;
248
+ })
249
+ .trim();
250
+ if (extracted) {
251
+ break;
252
+ }
253
+ }
254
+ }
255
+ }
256
+ }
257
+ // If there's still no text but a comment exists, replace the two
258
+ if (!data.text.length && data.comment.length) {
259
+ data.text = data.comment;
260
+ data.comment = [];
261
+ }
262
+ // Keep only the first address occurrence, push others to regular text
263
+ if (data.address.length > 1) {
264
+ data.text = data.text.concat(data.address.splice(1));
265
+ }
266
+ // An address is only taken from unquoted text, so anything left in the text at this
267
+ // point that still has to serve as the address carries its quoting in this flag
268
+ const addressFromQuotedText = !data.address.length && data.textWasQuoted.some(wasQuoted => wasQuoted);
269
+ // Join values with spaces
270
+ data.text = data.text.join(' ');
271
+ data.address = data.address.join(' ');
272
+ _recoverAddrSpec(data);
273
+ const address = {
274
+ address: data.address || data.text || '',
275
+ name: data.text || data.address || ''
276
+ };
277
+ if (address.address === address.name) {
278
+ if (/@/.test(address.address || '')) {
279
+ address.name = '';
280
+ }
281
+ else {
282
+ address.address = '';
283
+ }
284
+ }
285
+ if (addressFromQuotedText && address.address) {
286
+ address.address = _quoteLocalPart(address.address);
287
+ }
288
+ addresses.push(address);
289
+ }
290
+ return addresses;
291
+ }
292
+ /**
293
+ * Creates a Tokenizer object for tokenizing address field strings
294
+ *
295
+ * @constructor
296
+ * @param str Address field string
297
+ */
298
+ class Tokenizer {
299
+ constructor(str) {
300
+ this.str = (str || '').toString();
301
+ this.operatorCurrent = '';
302
+ this.operatorExpecting = '';
303
+ this.node = null;
304
+ this.escaped = false;
305
+ this.inDomainLiteral = false;
306
+ this.list = [];
307
+ /**
308
+ * Operator tokens and which tokens are expected to end the sequence
309
+ */
310
+ this.operators = {
311
+ '"': '"',
312
+ '(': ')',
313
+ '<': '>',
314
+ ',': '',
315
+ ':': ';',
316
+ // Semicolons are not a legal delimiter per the RFC2822 grammar other
317
+ // than for terminating a group, but they are also not valid for any
318
+ // other use in this context. Given that some mail clients have
319
+ // historically allowed the semicolon as a delimiter equivalent to the
320
+ // comma in their UI, it makes sense to treat them the same as a comma
321
+ // when used outside of a group.
322
+ ';': ''
323
+ };
324
+ }
325
+ /**
326
+ * Tokenizes the original input string
327
+ *
328
+ * @return An array of operator|text tokens
329
+ */
330
+ tokenize() {
331
+ const list = [];
332
+ for (let i = 0, len = this.str.length; i < len; i++) {
333
+ const chr = this.str.charAt(i);
334
+ const nextChr = i < len - 1 ? this.str.charAt(i + 1) : null;
335
+ this.checkChar(chr, nextChr);
336
+ }
337
+ this.list.forEach(node => {
338
+ node.value = (node.value || '').toString().trim();
339
+ if (node.value) {
340
+ list.push(node);
341
+ }
342
+ });
343
+ return list;
344
+ }
345
+ /**
346
+ * Checks if a character is an operator or text and acts accordingly
347
+ *
348
+ * @param chr Character from the address field
349
+ */
350
+ checkChar(chr, nextChr) {
351
+ // Track RFC 5322 domain-literals ("[" *dtext "]"). Operator characters such
352
+ // as the ":" of an IPv6 address-literal (user@[IPv6:2001:db8::1]) are dtext
353
+ // and must not be treated as the group delimiter while inside the brackets.
354
+ // Quoted strings and comments are handled separately via operatorExpecting,
355
+ // so only enter this state when no operator is open. The list separators ","
356
+ // and ";" are the exception: they always end the literal (and split the
357
+ // address list) so that an unclosed "[" cannot swallow later recipients.
358
+ if (!this.escaped && !this.operatorExpecting) {
359
+ if (!this.inDomainLiteral && chr === '[') {
360
+ this.inDomainLiteral = true;
361
+ }
362
+ else if (this.inDomainLiteral && (chr === ']' || chr === ',' || chr === ';')) {
363
+ this.inDomainLiteral = false;
364
+ }
365
+ }
366
+ if (this.escaped) {
367
+ // ignore next condition blocks
368
+ }
369
+ else if (chr === this.operatorExpecting) {
370
+ this.node = {
371
+ type: 'operator',
372
+ value: chr
373
+ };
374
+ if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) {
375
+ this.node.noBreak = true;
376
+ }
377
+ this.list.push(this.node);
378
+ this.node = null;
379
+ this.operatorExpecting = '';
380
+ this.escaped = false;
381
+ return;
382
+ }
383
+ else if (!this.operatorExpecting && !this.inDomainLiteral && chr in this.operators) {
384
+ this.node = {
385
+ type: 'operator',
386
+ value: chr
387
+ };
388
+ this.list.push(this.node);
389
+ this.node = null;
390
+ this.operatorExpecting = this.operators[chr];
391
+ this.escaped = false;
392
+ return;
393
+ }
394
+ else if (['"', "'"].includes(this.operatorExpecting) && chr === '\\') {
395
+ this.escaped = true;
396
+ return;
397
+ }
398
+ if (!this.node) {
399
+ this.node = {
400
+ type: 'text',
401
+ value: ''
402
+ };
403
+ this.list.push(this.node);
404
+ }
405
+ if (chr === '\n') {
406
+ // Convert newlines to spaces. Carriage return is ignored as \r and \n usually
407
+ // go together anyway and there already is a WS for \n. Lone \r means something is fishy.
408
+ chr = ' ';
409
+ }
410
+ if (chr.charCodeAt(0) >= 0x21 || [' ', '\t'].includes(chr)) {
411
+ // skip command bytes
412
+ this.node.value += chr;
413
+ }
414
+ this.escaped = false;
415
+ }
416
+ }
417
+ /**
418
+ * Maximum recursion depth for parsing nested groups.
419
+ * RFC 5322 doesn't allow nested groups, so this is a safeguard against
420
+ * malicious input that could cause stack overflow.
421
+ */
422
+ const MAX_NESTED_GROUP_DEPTH = 50;
423
+ /**
424
+ * Parses structured e-mail addresses from an address field
425
+ *
426
+ * Example:
427
+ *
428
+ * 'Name <address@domain>'
429
+ *
430
+ * will be converted to
431
+ *
432
+ * [{name: 'Name', address: 'address@domain'}]
433
+ *
434
+ * @param str Address field
435
+ * @param options Optional options object
436
+ * @param options._depth Internal recursion depth counter (do not set manually)
437
+ * @return An array of address objects
438
+ */
439
+ function addressparser(str, options) {
440
+ options = options || {};
441
+ const depth = options._depth || 0;
442
+ // Prevent stack overflow from deeply nested groups (DoS protection)
443
+ if (depth > MAX_NESTED_GROUP_DEPTH) {
444
+ return [];
445
+ }
446
+ const tokenizer = new Tokenizer(str);
447
+ const tokens = tokenizer.tokenize();
448
+ const addresses = [];
449
+ let address = [];
450
+ let parsedAddresses = [];
451
+ tokens.forEach(token => {
452
+ if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
453
+ if (address.length) {
454
+ addresses.push(address);
455
+ }
456
+ address = [];
457
+ }
458
+ else {
459
+ address.push(token);
460
+ }
461
+ });
462
+ if (address.length) {
463
+ addresses.push(address);
464
+ }
465
+ addresses.forEach(addr => {
466
+ const handled = _handleAddress(addr, depth);
467
+ // Appended in place. Rebuilding the accumulator with concat() would copy every
468
+ // entry collected so far on each address, making a flat list cost O(n^2).
469
+ for (let i = 0; i < handled.length; i++) {
470
+ parsedAddresses.push(handled[i]);
471
+ }
472
+ });
473
+ // Merge fragments produced when unquoted display names contain commas.
474
+ // "Joe Foo, PhD <joe@example.com>" is split on the comma into
475
+ // [{name:"Joe Foo", address:""}, {name:"PhD", address:"joe@example.com"}].
476
+ // Recombine: a name-only entry followed by an entry with both name and address.
477
+ // Walked back to front so that a run of fragments folds into one entry in a single
478
+ // pass. Splicing each fragment out of the list instead would cost O(n^2).
479
+ const mergedAddresses = [];
480
+ for (let i = parsedAddresses.length - 1; i >= 0; i--) {
481
+ const current = parsedAddresses[i];
482
+ const next = mergedAddresses.length ? mergedAddresses[mergedAddresses.length - 1] : null;
483
+ if (next && current.address === '' && current.name && !current.group && next.address && next.name) {
484
+ next.name = current.name + ', ' + next.name;
485
+ }
486
+ else {
487
+ mergedAddresses.push(current);
488
+ }
489
+ }
490
+ mergedAddresses.reverse();
491
+ parsedAddresses = mergedAddresses;
492
+ if (options.flatten) {
493
+ const flatAddresses = [];
494
+ const walkAddressList = (list) => {
495
+ list.forEach(entry => {
496
+ if (entry.group) {
497
+ return walkAddressList(entry.group);
498
+ }
499
+ flatAddresses.push(entry);
500
+ });
501
+ };
502
+ walkAddressList(parsedAddresses);
503
+ return flatAddresses;
504
+ }
505
+ return parsedAddresses;
506
+ }
507
+ module.exports = exports.default;
508
+ Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });