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.
- package/CHANGELOG.md +26 -0
- package/README.md +7 -3
- package/SECURITY.md +7 -7
- package/dist/cjs/addressparser/index.d.ts +47 -0
- package/dist/cjs/addressparser/index.js +508 -0
- package/dist/cjs/base64/index.d.ts +40 -0
- package/{lib → dist/cjs}/base64/index.js +31 -53
- package/dist/cjs/dkim/index.d.ts +40 -0
- package/dist/cjs/dkim/index.js +211 -0
- package/dist/cjs/dkim/message-parser.d.ts +41 -0
- package/dist/cjs/dkim/message-parser.js +147 -0
- package/dist/cjs/dkim/relaxed-body.d.ts +46 -0
- package/dist/cjs/dkim/relaxed-body.js +143 -0
- package/dist/cjs/dkim/sign.d.ts +49 -0
- package/dist/cjs/dkim/sign.js +148 -0
- package/dist/cjs/errors.d.ts +95 -0
- package/dist/cjs/errors.js +70 -0
- package/dist/cjs/fetch/cookies.d.ts +94 -0
- package/dist/cjs/fetch/cookies.js +268 -0
- package/dist/cjs/fetch/index.d.ts +51 -0
- package/dist/cjs/fetch/index.js +409 -0
- package/dist/cjs/json-transport/index.d.ts +54 -0
- package/dist/cjs/json-transport/index.js +97 -0
- package/dist/cjs/mail-composer/index.d.ts +224 -0
- package/dist/cjs/mail-composer/index.js +602 -0
- package/dist/cjs/mailer/index.d.ts +226 -0
- package/dist/cjs/mailer/index.js +401 -0
- package/dist/cjs/mailer/mail-message.d.ts +94 -0
- package/dist/cjs/mailer/mail-message.js +339 -0
- package/dist/cjs/mime-funcs/index.d.ts +157 -0
- package/dist/cjs/mime-funcs/index.js +670 -0
- package/dist/cjs/mime-funcs/mime-types.d.ts +2 -0
- package/dist/cjs/mime-funcs/mime-types.js +2103 -0
- package/dist/cjs/mime-node/index.d.ts +484 -0
- package/dist/cjs/mime-node/index.js +1422 -0
- package/dist/cjs/mime-node/last-newline.d.ts +7 -0
- package/dist/cjs/mime-node/last-newline.js +30 -0
- package/dist/cjs/mime-node/le-unix.d.ts +13 -0
- package/dist/cjs/mime-node/le-unix.js +39 -0
- package/dist/cjs/mime-node/le-windows.d.ts +14 -0
- package/dist/cjs/mime-node/le-windows.js +46 -0
- package/dist/cjs/nodemailer.d.ts +102 -0
- package/dist/cjs/nodemailer.js +207 -0
- package/dist/cjs/package-info.d.ts +3 -0
- package/dist/cjs/package-info.js +7 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/punycode/index.d.ts +73 -0
- package/{lib → dist/cjs}/punycode/index.js +61 -111
- package/dist/cjs/qp/index.d.ts +33 -0
- package/{lib → dist/cjs}/qp/index.js +30 -64
- package/dist/cjs/sendmail-transport/index.d.ts +69 -0
- package/dist/cjs/sendmail-transport/index.js +210 -0
- package/dist/cjs/ses-transport/index.d.ts +75 -0
- package/dist/cjs/ses-transport/index.js +248 -0
- package/dist/cjs/shared/index.d.ts +230 -0
- package/dist/cjs/shared/index.js +665 -0
- package/dist/cjs/shared/objects.d.ts +23 -0
- package/dist/cjs/shared/objects.js +43 -0
- package/dist/cjs/shared/url.d.ts +21 -0
- package/dist/cjs/shared/url.js +255 -0
- package/dist/cjs/smtp-connection/data-stream.d.ts +22 -0
- package/dist/cjs/smtp-connection/data-stream.js +98 -0
- package/dist/cjs/smtp-connection/http-proxy-client.d.ts +39 -0
- package/dist/cjs/smtp-connection/http-proxy-client.js +177 -0
- package/dist/cjs/smtp-connection/index.d.ts +719 -0
- package/dist/cjs/smtp-connection/index.js +1676 -0
- package/dist/cjs/smtp-pool/index.d.ts +163 -0
- package/dist/cjs/smtp-pool/index.js +530 -0
- package/dist/cjs/smtp-pool/pool-resource.d.ts +64 -0
- package/dist/cjs/smtp-pool/pool-resource.js +261 -0
- package/dist/cjs/smtp-transport/index.d.ts +143 -0
- package/dist/cjs/smtp-transport/index.js +379 -0
- package/dist/cjs/stream-transport/index.d.ts +63 -0
- package/dist/cjs/stream-transport/index.js +146 -0
- package/dist/cjs/well-known/index.d.ts +35 -0
- package/dist/cjs/well-known/index.js +41 -0
- package/dist/cjs/well-known/services.d.ts +2 -0
- package/dist/cjs/well-known/services.js +666 -0
- package/dist/cjs/xoauth2/index.d.ts +191 -0
- package/dist/cjs/xoauth2/index.js +414 -0
- package/dist/esm/addressparser/index.d.ts +47 -0
- package/{lib → dist/esm}/addressparser/index.js +49 -102
- package/dist/esm/base64/index.d.ts +40 -0
- package/dist/esm/base64/index.js +110 -0
- package/dist/esm/dkim/index.d.ts +40 -0
- package/{lib → dist/esm}/dkim/index.js +24 -70
- package/dist/esm/dkim/message-parser.d.ts +41 -0
- package/{lib → dist/esm}/dkim/message-parser.js +28 -40
- package/dist/esm/dkim/relaxed-body.d.ts +46 -0
- package/dist/esm/dkim/relaxed-body.js +135 -0
- package/dist/esm/dkim/sign.d.ts +49 -0
- package/{lib → dist/esm}/dkim/sign.js +32 -45
- package/dist/esm/errors.d.ts +95 -0
- package/{lib → dist/esm}/errors.js +26 -18
- package/dist/esm/fetch/cookies.d.ts +94 -0
- package/{lib → dist/esm}/fetch/cookies.js +75 -121
- package/dist/esm/fetch/index.d.ts +51 -0
- package/{lib → dist/esm}/fetch/index.js +46 -88
- package/dist/esm/json-transport/index.d.ts +54 -0
- package/{lib → dist/esm}/json-transport/index.js +17 -39
- package/dist/esm/mail-composer/index.d.ts +224 -0
- package/{lib → dist/esm}/mail-composer/index.js +187 -264
- package/dist/esm/mailer/index.d.ts +226 -0
- package/{lib → dist/esm}/mailer/index.js +109 -229
- package/dist/esm/mailer/mail-message.d.ts +94 -0
- package/{lib → dist/esm}/mailer/mail-message.js +59 -114
- package/dist/esm/mime-funcs/index.d.ts +157 -0
- package/dist/esm/mime-funcs/index.js +621 -0
- package/dist/esm/mime-funcs/mime-types.d.ts +2 -0
- package/{lib → dist/esm}/mime-funcs/mime-types.js +29 -42
- package/dist/esm/mime-node/index.d.ts +484 -0
- package/{lib → dist/esm}/mime-node/index.js +177 -361
- package/dist/esm/mime-node/last-newline.d.ts +7 -0
- package/{lib → dist/esm}/mime-node/last-newline.js +2 -10
- package/dist/esm/mime-node/le-unix.d.ts +13 -0
- package/{lib → dist/esm}/mime-node/le-unix.js +5 -11
- package/dist/esm/mime-node/le-windows.d.ts +14 -0
- package/{lib → dist/esm}/mime-node/le-windows.js +5 -13
- package/dist/esm/nodemailer.d.ts +102 -0
- package/{lib → dist/esm}/nodemailer.js +65 -69
- package/dist/esm/package-info.d.ts +3 -0
- package/dist/esm/package-info.js +4 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/punycode/index.d.ts +73 -0
- package/dist/esm/punycode/index.js +404 -0
- package/dist/esm/qp/index.d.ts +33 -0
- package/dist/esm/qp/index.js +190 -0
- package/dist/esm/sendmail-transport/index.d.ts +69 -0
- package/{lib → dist/esm}/sendmail-transport/index.js +51 -101
- package/dist/esm/ses-transport/index.d.ts +75 -0
- package/dist/esm/ses-transport/index.js +208 -0
- package/dist/esm/shared/index.d.ts +230 -0
- package/{lib → dist/esm}/shared/index.js +137 -268
- package/dist/esm/shared/objects.d.ts +23 -0
- package/{lib → dist/esm}/shared/objects.js +13 -17
- package/dist/esm/shared/url.d.ts +21 -0
- package/dist/esm/shared/url.js +214 -0
- package/dist/esm/smtp-connection/data-stream.d.ts +22 -0
- package/{lib → dist/esm}/smtp-connection/data-stream.js +14 -26
- package/dist/esm/smtp-connection/http-proxy-client.d.ts +39 -0
- package/{lib → dist/esm}/smtp-connection/http-proxy-client.js +27 -64
- package/dist/esm/smtp-connection/index.d.ts +719 -0
- package/{lib → dist/esm}/smtp-connection/index.js +332 -713
- package/dist/esm/smtp-pool/index.d.ts +163 -0
- package/{lib → dist/esm}/smtp-pool/index.js +120 -271
- package/dist/esm/smtp-pool/pool-resource.d.ts +64 -0
- package/{lib → dist/esm}/smtp-pool/pool-resource.js +36 -72
- package/dist/esm/smtp-transport/index.d.ts +143 -0
- package/{lib → dist/esm}/smtp-transport/index.js +58 -148
- package/dist/esm/stream-transport/index.d.ts +63 -0
- package/{lib → dist/esm}/stream-transport/index.js +36 -74
- package/dist/esm/well-known/index.d.ts +35 -0
- package/{lib → dist/esm}/well-known/index.js +5 -16
- package/dist/esm/well-known/services.d.ts +2 -0
- package/dist/esm/well-known/services.js +663 -0
- package/dist/esm/xoauth2/index.d.ts +191 -0
- package/{lib → dist/esm}/xoauth2/index.js +101 -175
- package/package.json +139 -14
- package/.gitattributes +0 -6
- package/.ncurc.js +0 -9
- package/.prettierignore +0 -8
- package/.prettierrc +0 -12
- package/.prettierrc.js +0 -10
- package/.release-please-config.json +0 -9
- package/CLAUDE.md +0 -56
- package/CODE_OF_CONDUCT.md +0 -76
- package/eslint.config.js +0 -88
- package/lib/dkim/relaxed-body.js +0 -154
- package/lib/mime-funcs/index.js +0 -671
- package/lib/ses-transport/index.js +0 -266
- package/lib/shared/url.js +0 -151
- /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
|
|
23
|
+
#### Nodemailer fails to load or throws a SyntaxError
|
|
20
24
|
|
|
21
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
|
15
|
-
| <
|
|
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
|
|
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
|
|
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
|
|
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 });
|