@nocobase/plugin-workflow-mailer 2.3.0-beta.13 → 2.3.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/externalVersion.js +6 -6
- package/dist/node_modules/joi/package.json +1 -1
- package/dist/node_modules/nodemailer/dist/cjs/addressparser/index.d.ts +59 -0
- package/dist/node_modules/nodemailer/dist/cjs/addressparser/index.js +790 -0
- package/dist/node_modules/nodemailer/dist/cjs/base64/index.d.ts +36 -0
- package/dist/node_modules/nodemailer/dist/cjs/base64/index.js +119 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/index.d.ts +52 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/index.js +228 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/message-parser.d.ts +48 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/message-parser.js +180 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/relaxed-body.d.ts +36 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/relaxed-body.js +148 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/sign.d.ts +52 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/sign.js +165 -0
- package/dist/node_modules/nodemailer/dist/cjs/errors.d.ts +96 -0
- package/dist/node_modules/nodemailer/dist/cjs/errors.js +70 -0
- package/dist/node_modules/nodemailer/dist/cjs/fetch/cookies.d.ts +107 -0
- package/dist/node_modules/nodemailer/dist/cjs/fetch/cookies.js +282 -0
- package/dist/node_modules/nodemailer/dist/cjs/fetch/index.d.ts +65 -0
- package/dist/node_modules/nodemailer/dist/cjs/fetch/index.js +384 -0
- package/dist/node_modules/nodemailer/dist/cjs/json-transport/index.d.ts +64 -0
- package/dist/node_modules/nodemailer/dist/cjs/json-transport/index.js +99 -0
- package/dist/node_modules/nodemailer/dist/cjs/mail-composer/index.d.ts +174 -0
- package/dist/node_modules/nodemailer/dist/cjs/mail-composer/index.js +613 -0
- package/dist/node_modules/nodemailer/dist/cjs/mailer/index.d.ts +237 -0
- package/dist/node_modules/nodemailer/dist/cjs/mailer/index.js +415 -0
- package/dist/node_modules/nodemailer/dist/cjs/mailer/mail-message.d.ts +97 -0
- package/dist/node_modules/nodemailer/dist/cjs/mailer/mail-message.js +342 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/index.d.ts +161 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/index.js +787 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/mime-types.d.ts +2 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/mime-types.js +2103 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/index.d.ts +333 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/index.js +1493 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/last-newline.d.ts +5 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/last-newline.js +32 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-unix.d.ts +9 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-unix.js +40 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-windows.d.ts +10 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-windows.js +47 -0
- package/dist/node_modules/nodemailer/dist/cjs/nodemailer.d.ts +124 -0
- package/dist/node_modules/nodemailer/dist/cjs/nodemailer.js +1 -0
- package/dist/node_modules/nodemailer/dist/cjs/package-info.d.ts +3 -0
- package/dist/node_modules/nodemailer/dist/cjs/package-info.js +7 -0
- package/dist/node_modules/nodemailer/dist/cjs/package.json +3 -0
- package/dist/node_modules/nodemailer/dist/cjs/punycode/index.d.ts +73 -0
- package/dist/node_modules/nodemailer/{lib → dist/cjs}/punycode/index.js +62 -111
- package/dist/node_modules/nodemailer/dist/cjs/qp/index.d.ts +32 -0
- package/dist/node_modules/nodemailer/dist/cjs/qp/index.js +207 -0
- package/dist/node_modules/nodemailer/dist/cjs/sendmail-transport/index.d.ts +69 -0
- package/dist/node_modules/nodemailer/dist/cjs/sendmail-transport/index.js +210 -0
- package/dist/node_modules/nodemailer/dist/cjs/ses-transport/index.d.ts +112 -0
- package/dist/node_modules/nodemailer/dist/cjs/ses-transport/index.js +252 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/index.d.ts +260 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/index.js +680 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/objects.d.ts +23 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/objects.js +43 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/url.d.ts +21 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/url.js +255 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/data-stream.d.ts +15 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/data-stream.js +108 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.d.ts +47 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js +189 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/index.d.ts +416 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/index.js +1837 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/index.d.ts +133 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/index.js +569 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.d.ts +62 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.js +257 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-transport/index.d.ts +156 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-transport/index.js +371 -0
- package/dist/node_modules/nodemailer/dist/cjs/stream-transport/index.d.ts +65 -0
- package/dist/node_modules/nodemailer/dist/cjs/stream-transport/index.js +146 -0
- package/dist/node_modules/nodemailer/dist/cjs/well-known/index.d.ts +35 -0
- package/dist/node_modules/nodemailer/dist/cjs/well-known/index.js +41 -0
- package/dist/node_modules/nodemailer/dist/cjs/well-known/services.d.ts +2 -0
- package/dist/node_modules/nodemailer/dist/cjs/well-known/services.js +674 -0
- package/dist/node_modules/nodemailer/dist/cjs/xoauth2/index.d.ts +201 -0
- package/dist/node_modules/nodemailer/dist/cjs/xoauth2/index.js +426 -0
- package/dist/node_modules/nodemailer/dist/esm/addressparser/index.d.ts +59 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/addressparser/index.js +361 -131
- package/dist/node_modules/nodemailer/dist/esm/base64/index.d.ts +36 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/base64/index.js +31 -56
- package/dist/node_modules/nodemailer/dist/esm/dkim/index.d.ts +52 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/index.js +49 -78
- package/dist/node_modules/nodemailer/dist/esm/dkim/message-parser.d.ts +48 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/message-parser.js +64 -42
- package/dist/node_modules/nodemailer/dist/esm/dkim/relaxed-body.d.ts +36 -0
- package/dist/node_modules/nodemailer/dist/esm/dkim/relaxed-body.js +141 -0
- package/dist/node_modules/nodemailer/dist/esm/dkim/sign.d.ts +52 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/sign.js +50 -46
- package/dist/node_modules/nodemailer/dist/esm/errors.d.ts +96 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/errors.js +26 -18
- package/dist/node_modules/nodemailer/dist/esm/fetch/cookies.d.ts +107 -0
- package/dist/node_modules/nodemailer/dist/esm/fetch/cookies.js +242 -0
- package/dist/node_modules/nodemailer/dist/esm/fetch/index.d.ts +65 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/fetch/index.js +80 -147
- package/dist/node_modules/nodemailer/dist/esm/json-transport/index.d.ts +64 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/json-transport/index.js +19 -39
- package/dist/node_modules/nodemailer/dist/esm/mail-composer/index.d.ts +174 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mail-composer/index.js +199 -265
- package/dist/node_modules/nodemailer/dist/esm/mailer/index.d.ts +237 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mailer/index.js +128 -234
- package/dist/node_modules/nodemailer/dist/esm/mailer/mail-message.d.ts +97 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mailer/mail-message.js +62 -114
- package/dist/node_modules/nodemailer/dist/esm/mime-funcs/index.d.ts +161 -0
- package/dist/node_modules/nodemailer/dist/esm/mime-funcs/index.js +738 -0
- package/dist/node_modules/nodemailer/dist/esm/mime-funcs/mime-types.d.ts +2 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-funcs/mime-types.js +29 -42
- package/dist/node_modules/nodemailer/dist/esm/mime-node/index.d.ts +333 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/index.js +259 -372
- package/dist/node_modules/nodemailer/dist/esm/mime-node/last-newline.d.ts +5 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/last-newline.js +4 -10
- package/dist/node_modules/nodemailer/dist/esm/mime-node/le-unix.d.ts +9 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/le-unix.js +6 -11
- package/dist/node_modules/nodemailer/dist/esm/mime-node/le-windows.d.ts +10 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/le-windows.js +6 -13
- package/dist/node_modules/nodemailer/dist/esm/nodemailer.d.ts +124 -0
- package/dist/node_modules/nodemailer/dist/esm/nodemailer.js +166 -0
- package/dist/node_modules/nodemailer/dist/esm/package-info.d.ts +3 -0
- package/dist/node_modules/nodemailer/dist/esm/package-info.js +4 -0
- package/dist/node_modules/nodemailer/dist/esm/package.json +3 -0
- package/dist/node_modules/nodemailer/dist/esm/punycode/index.d.ts +73 -0
- package/dist/node_modules/nodemailer/dist/esm/punycode/index.js +405 -0
- package/dist/node_modules/nodemailer/dist/esm/qp/index.d.ts +32 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/qp/index.js +38 -67
- package/dist/node_modules/nodemailer/dist/esm/sendmail-transport/index.d.ts +69 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/sendmail-transport/index.js +51 -101
- package/dist/node_modules/nodemailer/dist/esm/ses-transport/index.d.ts +112 -0
- package/dist/node_modules/nodemailer/dist/esm/ses-transport/index.js +212 -0
- package/dist/node_modules/nodemailer/dist/esm/shared/index.d.ts +260 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/shared/index.js +156 -272
- package/dist/node_modules/nodemailer/dist/esm/shared/objects.d.ts +23 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/shared/objects.js +13 -17
- package/dist/node_modules/nodemailer/dist/esm/shared/url.d.ts +21 -0
- package/dist/node_modules/nodemailer/dist/esm/shared/url.js +214 -0
- package/dist/node_modules/nodemailer/dist/esm/smtp-connection/data-stream.d.ts +15 -0
- package/dist/node_modules/nodemailer/dist/esm/smtp-connection/data-stream.js +103 -0
- package/dist/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.d.ts +47 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-connection/http-proxy-client.js +49 -74
- package/dist/node_modules/nodemailer/dist/esm/smtp-connection/index.d.ts +416 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-connection/index.js +574 -794
- package/dist/node_modules/nodemailer/dist/esm/smtp-pool/index.d.ts +133 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-pool/index.js +172 -284
- package/dist/node_modules/nodemailer/dist/esm/smtp-pool/pool-resource.d.ts +62 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-pool/pool-resource.js +57 -97
- package/dist/node_modules/nodemailer/dist/esm/smtp-transport/index.d.ts +156 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-transport/index.js +81 -179
- package/dist/node_modules/nodemailer/dist/esm/stream-transport/index.d.ts +65 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/stream-transport/index.js +36 -74
- package/dist/node_modules/nodemailer/dist/esm/well-known/index.d.ts +35 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/well-known/index.js +5 -16
- package/dist/node_modules/nodemailer/dist/esm/well-known/services.d.ts +2 -0
- package/dist/node_modules/nodemailer/dist/esm/well-known/services.js +671 -0
- package/dist/node_modules/nodemailer/dist/esm/xoauth2/index.d.ts +201 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/xoauth2/index.js +125 -187
- package/dist/node_modules/nodemailer/{lib → dist}/well-known/services.json +9 -2
- package/dist/node_modules/nodemailer/package.json +1 -1
- package/package.json +3 -3
- package/dist/node_modules/nodemailer/.gitattributes +0 -6
- package/dist/node_modules/nodemailer/.ncurc.js +0 -9
- package/dist/node_modules/nodemailer/.prettierignore +0 -8
- package/dist/node_modules/nodemailer/.prettierrc +0 -12
- package/dist/node_modules/nodemailer/.prettierrc.js +0 -10
- package/dist/node_modules/nodemailer/.release-please-config.json +0 -9
- package/dist/node_modules/nodemailer/eslint.config.js +0 -88
- package/dist/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
- package/dist/node_modules/nodemailer/lib/fetch/cookies.js +0 -276
- package/dist/node_modules/nodemailer/lib/mime-funcs/index.js +0 -671
- package/dist/node_modules/nodemailer/lib/nodemailer.js +0 -1
- package/dist/node_modules/nodemailer/lib/ses-transport/index.js +0 -266
- package/dist/node_modules/nodemailer/lib/shared/url.js +0 -151
- package/dist/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -105
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Restores the quoting of a local part that was read out of a quoted string.
|
|
5
3
|
*
|
|
@@ -10,12 +8,12 @@
|
|
|
10
8
|
* recipient separator once the consumer puts the address back into a header.
|
|
11
9
|
*
|
|
12
10
|
* This module has no dependencies so that it can ship on its own, which is why the two
|
|
13
|
-
* grammar tests below are spelled out here instead of shared with
|
|
11
|
+
* grammar tests below are spelled out here instead of shared with src/mime-node. Keeping
|
|
14
12
|
* only what is ambiguous quoted is deliberate, mime-node applies the stricter RFC 5321
|
|
15
13
|
* dot-atom rule on top of this when it emits an address.
|
|
16
14
|
*
|
|
17
|
-
* @param
|
|
18
|
-
* @return
|
|
15
|
+
* @param address Address with an unquoted local part
|
|
16
|
+
* @return Address with the local part as a quoted-string
|
|
19
17
|
*/
|
|
20
18
|
function _quoteLocalPart(address) {
|
|
21
19
|
const lastAt = address.lastIndexOf('@');
|
|
@@ -23,39 +21,160 @@ function _quoteLocalPart(address) {
|
|
|
23
21
|
// no domain to split off, nothing can be misrouted
|
|
24
22
|
return address;
|
|
25
23
|
}
|
|
26
|
-
|
|
27
24
|
const user = address.substr(0, lastAt);
|
|
28
25
|
if (/^[^\s"(),:;<>@[\\\]]+$/.test(user) || /^"(?:[^"\\]|\\[\s\S])*"$/.test(user)) {
|
|
29
26
|
// a local part that carries no special reads the same with or without the quotes,
|
|
30
27
|
// and one that is already a complete quoted-string needs nothing either
|
|
31
28
|
return address;
|
|
32
29
|
}
|
|
33
|
-
|
|
34
30
|
return '"' + user.replace(/["\\]/g, '\\$&') + '"@' + address.substr(lastAt + 1);
|
|
35
31
|
}
|
|
36
|
-
|
|
37
32
|
/**
|
|
38
33
|
* Reached for every parsed address, so it is built once rather than per call.
|
|
39
34
|
*/
|
|
40
35
|
const HAS_WHITESPACE = /\s/;
|
|
41
|
-
|
|
42
36
|
/**
|
|
43
37
|
* An addr-spec that carries its whitespace legally, inside a quoted local part. The
|
|
44
38
|
* optional tail is the malformed shape: a real mailbox with wreckage trailing it.
|
|
45
39
|
*/
|
|
46
40
|
const QUOTED_LOCAL_ADDR = /^("(?:[^"\\]|\\[\s\S])*"@\S+)(?:\s+([\s\S]+))?$/;
|
|
47
|
-
|
|
48
41
|
/**
|
|
49
42
|
* One run holding a single '@' and no whitespace, the shape an addr-spec has to have.
|
|
50
43
|
*/
|
|
51
44
|
const ADDR_SPEC = /^[^@\s]+@[^@\s]+$/;
|
|
52
|
-
|
|
53
45
|
/**
|
|
54
46
|
* The looser reading applied once the strict one finds nothing, which tolerates the
|
|
55
47
|
* further '@' that a domain should not have but malformed headers carry anyway.
|
|
56
48
|
*/
|
|
57
49
|
const LOOSE_ADDR_SPEC = /^[^@\s]+@\S+$/;
|
|
58
|
-
|
|
50
|
+
/**
|
|
51
|
+
* An addr-spec sitting inside free text, together with the whitespace around it. Sticky
|
|
52
|
+
* on purpose: it is run at the one offset _looseAddressStart picks rather than being let
|
|
53
|
+
* loose to search, see there.
|
|
54
|
+
*/
|
|
55
|
+
const LOOSE_TEXT_ADDR = /\s*\b[^@\s]+@[^\s]+\b\s*/y;
|
|
56
|
+
/**
|
|
57
|
+
* The characters JS `\s` matches, which the scan below has to agree with to land on the
|
|
58
|
+
* same match the pattern would.
|
|
59
|
+
*/
|
|
60
|
+
function _isSpaceCode(code) {
|
|
61
|
+
return (code === 0x20 ||
|
|
62
|
+
(code >= 0x09 && code <= 0x0d) ||
|
|
63
|
+
code === 0xa0 ||
|
|
64
|
+
code === 0x1680 ||
|
|
65
|
+
(code >= 0x2000 && code <= 0x200a) ||
|
|
66
|
+
code === 0x2028 ||
|
|
67
|
+
code === 0x2029 ||
|
|
68
|
+
code === 0x202f ||
|
|
69
|
+
code === 0x205f ||
|
|
70
|
+
code === 0x3000 ||
|
|
71
|
+
code === 0xfeff);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The characters JS `\w` matches without the unicode flag, the set the `\b` in
|
|
75
|
+
* LOOSE_TEXT_ADDR is read against. charCodeAt off either end of the string gives NaN,
|
|
76
|
+
* which compares false throughout, so out of range reads as the non-word the pattern
|
|
77
|
+
* treats them as.
|
|
78
|
+
*/
|
|
79
|
+
function _isWordCode(code) {
|
|
80
|
+
return (code >= 0x30 && code <= 0x39) || (code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a) || code === 0x5f;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Whether `\b` holds at an offset
|
|
84
|
+
*/
|
|
85
|
+
function _isBoundary(text, at) {
|
|
86
|
+
return _isWordCode(text.charCodeAt(at - 1)) !== _isWordCode(text.charCodeAt(at));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Offset of the first '@' in `text` between `from` and `to`, or -1 when the range holds none.
|
|
90
|
+
*
|
|
91
|
+
* indexOf would scan on to the end of the value, and the value here is a whole header. The
|
|
92
|
+
* walk below steps one whitespace delimited run at a time and only ever uses a '@' that sits
|
|
93
|
+
* inside the run it is on, so an unbounded probe rescans everything behind that run once per
|
|
94
|
+
* run and grows with the square of the header: 400KB of free text carrying no '@' took a
|
|
95
|
+
* quarter of a second. GHSA-v53p-9fqp-m79j took the pattern search out of this walk and left
|
|
96
|
+
* the probe unbounded behind it.
|
|
97
|
+
*
|
|
98
|
+
* @param text Text to look in
|
|
99
|
+
* @param from Offset to start at
|
|
100
|
+
* @param to Offset to stop before
|
|
101
|
+
* @return Offset of the '@', or -1
|
|
102
|
+
*/
|
|
103
|
+
function _indexOfAt(text, from, to) {
|
|
104
|
+
for (let i = from; i < to; i++) {
|
|
105
|
+
if (text.charCodeAt(i) === 0x40) {
|
|
106
|
+
return i;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return -1;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Finds the offset LOOSE_TEXT_ADDR matches at, or -1 when it does not match at all.
|
|
113
|
+
*
|
|
114
|
+
* Letting the pattern search for itself is quadratic: '[^@\s]+' is retried from every
|
|
115
|
+
* offset and rescans the run to the next '@' each time, so 140KB of header holding no
|
|
116
|
+
* usable '@' blocks the event loop for about ten seconds (GHSA-v53p-9fqp-m79j). The search is also unnecessary.
|
|
117
|
+
* '[^@\s]+' crosses neither whitespace nor a '@', so a match can only begin at the head of
|
|
118
|
+
* a whitespace delimited run or just past a '@' inside one, and '[^\s]+\b' gives characters
|
|
119
|
+
* back until it lands on a boundary, so the only end it can take in that run is the last
|
|
120
|
+
* boundary in it. Both are found in one pass, and the pattern is then run at that single
|
|
121
|
+
* offset.
|
|
122
|
+
*
|
|
123
|
+
* @param text Free text to look in
|
|
124
|
+
* @return Offset to match at, or -1
|
|
125
|
+
*/
|
|
126
|
+
function _looseAddressStart(text) {
|
|
127
|
+
const len = text.length;
|
|
128
|
+
let pos = 0;
|
|
129
|
+
while (pos < len) {
|
|
130
|
+
while (pos < len && _isSpaceCode(text.charCodeAt(pos))) {
|
|
131
|
+
pos++;
|
|
132
|
+
}
|
|
133
|
+
if (pos >= len) {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
const runStart = pos;
|
|
137
|
+
let runEnd = pos;
|
|
138
|
+
while (runEnd < len && !_isSpaceCode(text.charCodeAt(runEnd))) {
|
|
139
|
+
runEnd++;
|
|
140
|
+
}
|
|
141
|
+
let at = _indexOfAt(text, runStart, runEnd);
|
|
142
|
+
if (at >= 0) {
|
|
143
|
+
let lastBoundary = -1;
|
|
144
|
+
for (let k = runEnd; k > runStart; k--) {
|
|
145
|
+
if (_isBoundary(text, k)) {
|
|
146
|
+
lastBoundary = k;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
let atomStart = runStart;
|
|
151
|
+
while (lastBoundary >= 0 && at >= 0) {
|
|
152
|
+
// '[^@\s]+' has to cover a character before the '@' and '[^\s]+' one after it,
|
|
153
|
+
// and the boundary that ends the match has to sit past both
|
|
154
|
+
if (at > atomStart && runEnd > at + 1 && lastBoundary > at + 1) {
|
|
155
|
+
for (let start = atomStart; start < at; start++) {
|
|
156
|
+
if (_isBoundary(text, start)) {
|
|
157
|
+
if (start > runStart) {
|
|
158
|
+
return start;
|
|
159
|
+
}
|
|
160
|
+
// the leading '\s*' is greedy, so a match that begins at the run
|
|
161
|
+
// takes the whitespace in front of it along
|
|
162
|
+
let padded = runStart;
|
|
163
|
+
while (padded > 0 && _isSpaceCode(text.charCodeAt(padded - 1))) {
|
|
164
|
+
padded--;
|
|
165
|
+
}
|
|
166
|
+
return padded;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
atomStart = at + 1;
|
|
171
|
+
at = _indexOfAt(text, atomStart, runEnd);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
pos = runEnd;
|
|
175
|
+
}
|
|
176
|
+
return -1;
|
|
177
|
+
}
|
|
59
178
|
/**
|
|
60
179
|
* Recovers the addr-spec from an angle-addr that came back holding unquoted whitespace.
|
|
61
180
|
*
|
|
@@ -75,28 +194,26 @@ const LOOSE_ADDR_SPEC = /^[^@\s]+@\S+$/;
|
|
|
75
194
|
* A quoted local part is left alone: RFC 5321 allows whitespace inside it, so
|
|
76
195
|
* '<"user name"@example.com>' is well formed and means exactly what it says.
|
|
77
196
|
*
|
|
78
|
-
* @param
|
|
197
|
+
* @param data Collected address parts, mutated in place
|
|
79
198
|
*/
|
|
80
199
|
function _recoverAddrSpec(data) {
|
|
81
200
|
if (!HAS_WHITESPACE.test(data.address)) {
|
|
82
201
|
return;
|
|
83
202
|
}
|
|
84
|
-
|
|
85
203
|
let address;
|
|
86
204
|
let rest;
|
|
87
|
-
|
|
88
205
|
const quoted = data.address.match(QUOTED_LOCAL_ADDR);
|
|
89
206
|
if (quoted) {
|
|
90
207
|
if (!quoted[2]) {
|
|
91
208
|
// the whitespace sits inside the quoted local part, this is a well formed mailbox
|
|
92
209
|
return;
|
|
93
210
|
}
|
|
94
|
-
|
|
95
211
|
// a real mailbox with wreckage trailing it, so peel the addr-spec off whole rather
|
|
96
212
|
// than splitting into the quotes
|
|
97
213
|
address = quoted[1];
|
|
98
214
|
rest = [quoted[2]];
|
|
99
|
-
}
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
100
217
|
if (data.address.indexOf('"') >= 0) {
|
|
101
218
|
// Splitting on whitespace loses track of where the quoted string starts and ends,
|
|
102
219
|
// and this module does not take addresses out of quoted strings: the run picked out
|
|
@@ -105,36 +222,105 @@ function _recoverAddrSpec(data) {
|
|
|
105
222
|
// left is wreckage either way and the original is the honest answer
|
|
106
223
|
return;
|
|
107
224
|
}
|
|
108
|
-
|
|
109
225
|
const parts = data.address.split(/\s+/);
|
|
110
|
-
|
|
111
226
|
let addrIndex = parts.findIndex(part => ADDR_SPEC.test(part));
|
|
112
227
|
if (addrIndex < 0) {
|
|
113
228
|
addrIndex = parts.findIndex(part => LOOSE_ADDR_SPEC.test(part));
|
|
114
229
|
}
|
|
115
|
-
|
|
116
230
|
if (addrIndex < 0) {
|
|
117
231
|
// nothing in there reads as an address, there is no better answer than the original
|
|
118
232
|
return;
|
|
119
233
|
}
|
|
120
|
-
|
|
121
234
|
address = parts.splice(addrIndex, 1)[0];
|
|
122
235
|
rest = parts;
|
|
123
236
|
}
|
|
124
|
-
|
|
125
237
|
data.address = address;
|
|
126
238
|
data.text = [data.text]
|
|
127
239
|
.concat(rest)
|
|
128
240
|
.filter(part => part)
|
|
129
241
|
.join(' ');
|
|
130
242
|
}
|
|
131
|
-
|
|
243
|
+
/**
|
|
244
|
+
* Takes the RFC 5322 comments out of the contents of an angle-addr.
|
|
245
|
+
*
|
|
246
|
+
* The tokenizer tracks a single open operator, so once '<' is open a '(' is plain text and
|
|
247
|
+
* a comment inside the brackets reached the address verbatim: 'Name <user@example.com(x)evil.com>'
|
|
248
|
+
* put 'user@example.com(x)evil.com' into the envelope, a value that is no mailbox and that a
|
|
249
|
+
* receiver stripping the comment reads as 'user@example.comevil.com' (GHSA-g73g-hqqh-jr95).
|
|
250
|
+
* A comment is folding whitespace, so it is read as a space that splits two atoms and as
|
|
251
|
+
* nothing next to an '@', the same rule the token walk applies outside the brackets. Quoted
|
|
252
|
+
* strings and domain literals are copied through untouched, a '(' in there is not a comment.
|
|
253
|
+
*
|
|
254
|
+
* @param address Contents of the angle brackets
|
|
255
|
+
* @return The address with the comments removed, and the text of the comments
|
|
256
|
+
*/
|
|
257
|
+
function _stripAddressComments(address) {
|
|
258
|
+
const comments = [];
|
|
259
|
+
let result = '';
|
|
260
|
+
let comment = '';
|
|
261
|
+
let depth = 0;
|
|
262
|
+
let closer = '';
|
|
263
|
+
// carried along rather than read back off the growing result, which would flatten it on
|
|
264
|
+
// every comment (see lastChars in _handleAddress)
|
|
265
|
+
let lastChar = '';
|
|
266
|
+
for (let i = 0, len = address.length; i < len; i++) {
|
|
267
|
+
const chr = address.charAt(i);
|
|
268
|
+
if (depth) {
|
|
269
|
+
if (chr === '\\' && i < len - 1) {
|
|
270
|
+
comment += address.charAt(++i);
|
|
271
|
+
}
|
|
272
|
+
else if (chr === '(') {
|
|
273
|
+
depth++;
|
|
274
|
+
comment += chr;
|
|
275
|
+
}
|
|
276
|
+
else if (chr === ')' && !--depth) {
|
|
277
|
+
comments.push(comment.trim());
|
|
278
|
+
comment = '';
|
|
279
|
+
if (lastChar !== '@' && address.charAt(i + 1) !== '@') {
|
|
280
|
+
result += ' ';
|
|
281
|
+
lastChar = ' ';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
comment += chr;
|
|
286
|
+
}
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (closer) {
|
|
290
|
+
if (chr === '\\' && closer === '"' && i < len - 1) {
|
|
291
|
+
result += chr + address.charAt(++i);
|
|
292
|
+
lastChar = address.charAt(i);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (chr === closer) {
|
|
296
|
+
closer = '';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else if (chr === '"') {
|
|
300
|
+
closer = '"';
|
|
301
|
+
}
|
|
302
|
+
else if (chr === '[') {
|
|
303
|
+
closer = ']';
|
|
304
|
+
}
|
|
305
|
+
else if (chr === '(') {
|
|
306
|
+
depth = 1;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
result += chr;
|
|
310
|
+
lastChar = chr;
|
|
311
|
+
}
|
|
312
|
+
if (depth) {
|
|
313
|
+
// an unterminated comment runs to the end of the address
|
|
314
|
+
comments.push(comment.trim());
|
|
315
|
+
}
|
|
316
|
+
return { address: result.trim(), comments: comments.filter(text => text) };
|
|
317
|
+
}
|
|
132
318
|
/**
|
|
133
319
|
* Converts tokens for a single address into an address object
|
|
134
320
|
*
|
|
135
|
-
* @param
|
|
136
|
-
* @param
|
|
137
|
-
* @return
|
|
321
|
+
* @param tokens Tokens object
|
|
322
|
+
* @param depth Current recursion depth for nested group protection
|
|
323
|
+
* @return Address object
|
|
138
324
|
*/
|
|
139
325
|
function _handleAddress(tokens, depth) {
|
|
140
326
|
let isGroup = false;
|
|
@@ -148,7 +334,12 @@ function _handleAddress(tokens, depth) {
|
|
|
148
334
|
textWasQuoted: []
|
|
149
335
|
};
|
|
150
336
|
let insideQuotes = false;
|
|
151
|
-
|
|
337
|
+
// Last character of the run each state is currently accumulating. Reading it back off
|
|
338
|
+
// the accumulator with slice(-1) makes the engine flatten the whole growing string on
|
|
339
|
+
// every token, which is quadratic over an address built from many comment-joined atoms
|
|
340
|
+
// (GHSA-prgh-xp8r-p3m5). A run only ever grows by the token appended below, so the
|
|
341
|
+
// character is carried along instead of re-read.
|
|
342
|
+
const lastChars = { address: '', comment: '', group: '', text: '' };
|
|
152
343
|
// Filter out <addresses>, (comments) and regular text
|
|
153
344
|
for (let i = 0, len = tokens.length; i < len; i++) {
|
|
154
345
|
const token = tokens[i];
|
|
@@ -177,66 +368,98 @@ function _handleAddress(tokens, depth) {
|
|
|
177
368
|
insideQuotes = false;
|
|
178
369
|
break;
|
|
179
370
|
}
|
|
180
|
-
}
|
|
371
|
+
}
|
|
372
|
+
else if (token.value) {
|
|
373
|
+
// An empty quoted string is dropped by the tokenizer, leaving no text token of its
|
|
374
|
+
// own for textWasQuoted to be recorded on, so the pair of quote operators right in
|
|
375
|
+
// front of this token is all that is left of it and the run it opens carries the
|
|
376
|
+
// quoting instead. Without this '""@example.com' reads as the bare '@example.com',
|
|
377
|
+
// the quotes never go back on, and the value is no longer an addr-spec a trailing
|
|
378
|
+
// comment can be peeled off of. It only ever opens a run: a run that already holds
|
|
379
|
+
// material collected outside the quotes is not a quoted string, whatever follows it
|
|
380
|
+
const prevPrevToken = i > 1 ? tokens[i - 2] : null;
|
|
381
|
+
const opensAfterEmptyQuotedString = prevToken?.type === 'operator' &&
|
|
382
|
+
prevToken.value === '"' &&
|
|
383
|
+
!!prevToken.noBreak &&
|
|
384
|
+
prevPrevToken?.type === 'operator' &&
|
|
385
|
+
prevPrevToken.value === '"';
|
|
181
386
|
if (state === 'address') {
|
|
182
387
|
// Handle unquoted name that includes a "<".
|
|
183
388
|
// Apple Mail truncates everything between an unexpected < and an address.
|
|
184
389
|
token.value = token.value.replace(/^[^<]*<\s*/, '');
|
|
185
390
|
}
|
|
186
|
-
|
|
187
391
|
// A comment is folding whitespace. It may sit inside an addr-spec, on either side
|
|
188
392
|
// of the '@', but it cannot join two atoms into one: gluing across it would read
|
|
189
393
|
// 'user@example.com(x)evil.com' as the single domain 'example.comevil.com' and
|
|
190
394
|
// deliver to a domain the sender never named.
|
|
191
395
|
const parts = data[state];
|
|
192
|
-
const joins =
|
|
193
|
-
prevToken &&
|
|
396
|
+
const joins = prevToken &&
|
|
194
397
|
prevToken.noBreak &&
|
|
195
398
|
parts.length &&
|
|
196
|
-
(prevToken.value !== ')' ||
|
|
197
|
-
|
|
399
|
+
(prevToken.value !== ')' || lastChars[state] === '@' || token.value.charAt(0) === '@');
|
|
198
400
|
if (joins) {
|
|
199
401
|
data[state][data[state].length - 1] += token.value;
|
|
402
|
+
if (token.value) {
|
|
403
|
+
lastChars[state] = token.value.charAt(token.value.length - 1);
|
|
404
|
+
}
|
|
200
405
|
if (state === 'text' && insideQuotes) {
|
|
201
406
|
data.textWasQuoted[data.textWasQuoted.length - 1] = true;
|
|
202
407
|
}
|
|
203
|
-
}
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
204
410
|
data[state].push(token.value);
|
|
411
|
+
lastChars[state] = token.value.charAt(token.value.length - 1);
|
|
205
412
|
if (state === 'text') {
|
|
206
|
-
data.textWasQuoted.push(insideQuotes);
|
|
413
|
+
data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
|
|
207
414
|
}
|
|
208
415
|
}
|
|
209
416
|
}
|
|
210
417
|
}
|
|
211
|
-
|
|
212
418
|
// If there is no text but a comment, replace the two
|
|
213
419
|
if (!data.text.length && data.comment.length) {
|
|
214
420
|
data.text = data.comment;
|
|
215
421
|
data.comment = [];
|
|
216
422
|
}
|
|
217
|
-
|
|
218
423
|
if (isGroup) {
|
|
219
424
|
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
|
|
220
425
|
data.text = data.text.join(' ');
|
|
221
|
-
|
|
222
426
|
// Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting)
|
|
223
427
|
let groupMembers = [];
|
|
224
428
|
if (data.group.length) {
|
|
225
|
-
const parsedGroup =
|
|
429
|
+
const parsedGroup = _parseAddressList(data.group.join(','), depth + 1);
|
|
226
430
|
parsedGroup.forEach(member => {
|
|
227
431
|
if (member.group) {
|
|
228
432
|
groupMembers = groupMembers.concat(member.group);
|
|
229
|
-
}
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
230
435
|
groupMembers.push(member);
|
|
231
436
|
}
|
|
232
437
|
});
|
|
233
438
|
}
|
|
234
|
-
|
|
235
439
|
addresses.push({
|
|
236
440
|
name: data.text || '',
|
|
237
441
|
group: groupMembers
|
|
238
442
|
});
|
|
239
|
-
}
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
// Comments come out of the angle-addr before anything asks whether one was found, so
|
|
446
|
+
// that brackets holding nothing but a comment read the same as empty ones
|
|
447
|
+
const addressComments = [];
|
|
448
|
+
const addressParts = [];
|
|
449
|
+
for (const part of data.address) {
|
|
450
|
+
if (part.indexOf('(') < 0) {
|
|
451
|
+
addressParts.push(part);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const stripped = _stripAddressComments(part);
|
|
455
|
+
for (const comment of stripped.comments) {
|
|
456
|
+
addressComments.push(comment);
|
|
457
|
+
}
|
|
458
|
+
if (stripped.address) {
|
|
459
|
+
addressParts.push(stripped.address);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
data.address = addressParts;
|
|
240
463
|
// If no address was found, try to detect one from regular text
|
|
241
464
|
if (!data.address.length && data.text.length) {
|
|
242
465
|
for (let i = data.text.length - 1; i >= 0; i--) {
|
|
@@ -249,23 +472,25 @@ function _handleAddress(tokens, depth) {
|
|
|
249
472
|
break;
|
|
250
473
|
}
|
|
251
474
|
}
|
|
252
|
-
|
|
253
475
|
// Try a looser regex match if strict match found nothing
|
|
254
476
|
if (!data.address.length) {
|
|
255
477
|
let extracted = false;
|
|
256
478
|
for (let i = data.text.length - 1; i >= 0; i--) {
|
|
257
479
|
// Security: Do not extract email addresses from quoted strings
|
|
258
480
|
if (!data.textWasQuoted[i]) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
481
|
+
const part = data.text[i];
|
|
482
|
+
let remainder = part;
|
|
483
|
+
const at = _looseAddressStart(part);
|
|
484
|
+
if (at >= 0) {
|
|
485
|
+
LOOSE_TEXT_ADDR.lastIndex = at;
|
|
486
|
+
const match = LOOSE_TEXT_ADDR.exec(part);
|
|
487
|
+
if (match) {
|
|
488
|
+
data.address = [match[0].trim()];
|
|
489
|
+
extracted = true;
|
|
490
|
+
remainder = part.slice(0, at) + ' ' + part.slice(at + match[0].length);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
data.text[i] = remainder.trim();
|
|
269
494
|
if (extracted) {
|
|
270
495
|
break;
|
|
271
496
|
}
|
|
@@ -273,56 +498,65 @@ function _handleAddress(tokens, depth) {
|
|
|
273
498
|
}
|
|
274
499
|
}
|
|
275
500
|
}
|
|
276
|
-
|
|
277
501
|
// If there's still no text but a comment exists, replace the two
|
|
278
502
|
if (!data.text.length && data.comment.length) {
|
|
279
503
|
data.text = data.comment;
|
|
280
504
|
data.comment = [];
|
|
281
505
|
}
|
|
282
|
-
|
|
283
506
|
// Keep only the first address occurrence, push others to regular text
|
|
284
507
|
if (data.address.length > 1) {
|
|
285
508
|
data.text = data.text.concat(data.address.splice(1));
|
|
286
509
|
}
|
|
287
|
-
|
|
288
510
|
// An address is only taken from unquoted text, so anything left in the text at this
|
|
289
511
|
// point that still has to serve as the address carries its quoting in this flag
|
|
290
512
|
const addressFromQuotedText = !data.address.length && data.textWasQuoted.some(wasQuoted => wasQuoted);
|
|
291
|
-
|
|
292
513
|
// Join values with spaces
|
|
293
514
|
data.text = data.text.join(' ');
|
|
294
515
|
data.address = data.address.join(' ');
|
|
295
|
-
|
|
516
|
+
if (addressFromQuotedText && data.text.indexOf('@') >= 0) {
|
|
517
|
+
// A quoted run with no '@' in it is a display name and nothing else, so it stays
|
|
518
|
+
// where it is. Moving it over anyway made a comment the mailbox was read from:
|
|
519
|
+
// '"Display Name" <(a comment)>' handed on 'Display Name' as the address with the
|
|
520
|
+
// comment as the name, and a quoted name holding a ',' or a ';' became an address
|
|
521
|
+
// that reads as two recipients once a consumer writes it back into a header.
|
|
522
|
+
//
|
|
523
|
+
// The mailbox is still sitting in the text, so it moves over here and is quoted
|
|
524
|
+
// before the recovery below rather than after it. Anything else the text holds
|
|
525
|
+
// came along with it: a comment ends the domain but leaves the atoms behind it in
|
|
526
|
+
// the same text, and '"user"@example.com(x)evil.com' was handed on as the address
|
|
527
|
+
// 'user@example.com evil.com', a second domain riding into the envelope recipient
|
|
528
|
+
// on a value that is no addr-spec at all (GHSA-g57g-f23g-4646). Putting the quotes
|
|
529
|
+
// back first is what lets the recovery tell the whitespace an addr-spec may carry
|
|
530
|
+
// from the wreckage trailing one, as only a quoted local part may hold whitespace
|
|
531
|
+
data.address = _quoteLocalPart(data.text);
|
|
532
|
+
data.text = '';
|
|
533
|
+
}
|
|
296
534
|
_recoverAddrSpec(data);
|
|
297
|
-
|
|
535
|
+
// a comment names the mailbox only when nothing else does, as one outside the brackets
|
|
536
|
+
if (!data.text && addressComments.length) {
|
|
537
|
+
data.text = addressComments.join(' ');
|
|
538
|
+
}
|
|
298
539
|
const address = {
|
|
299
540
|
address: data.address || data.text || '',
|
|
300
541
|
name: data.text || data.address || ''
|
|
301
542
|
};
|
|
302
|
-
|
|
303
543
|
if (address.address === address.name) {
|
|
304
544
|
if (/@/.test(address.address || '')) {
|
|
305
545
|
address.name = '';
|
|
306
|
-
}
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
307
548
|
address.address = '';
|
|
308
549
|
}
|
|
309
550
|
}
|
|
310
|
-
|
|
311
|
-
if (addressFromQuotedText && address.address) {
|
|
312
|
-
address.address = _quoteLocalPart(address.address);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
551
|
addresses.push(address);
|
|
316
552
|
}
|
|
317
|
-
|
|
318
553
|
return addresses;
|
|
319
554
|
}
|
|
320
|
-
|
|
321
555
|
/**
|
|
322
556
|
* Creates a Tokenizer object for tokenizing address field strings
|
|
323
557
|
*
|
|
324
558
|
* @constructor
|
|
325
|
-
* @param
|
|
559
|
+
* @param str Address field string
|
|
326
560
|
*/
|
|
327
561
|
class Tokenizer {
|
|
328
562
|
constructor(str) {
|
|
@@ -332,7 +566,7 @@ class Tokenizer {
|
|
|
332
566
|
this.node = null;
|
|
333
567
|
this.escaped = false;
|
|
334
568
|
this.inDomainLiteral = false;
|
|
335
|
-
|
|
569
|
+
this.afterAt = false;
|
|
336
570
|
this.list = [];
|
|
337
571
|
/**
|
|
338
572
|
* Operator tokens and which tokens are expected to end the sequence
|
|
@@ -352,85 +586,92 @@ class Tokenizer {
|
|
|
352
586
|
';': ''
|
|
353
587
|
};
|
|
354
588
|
}
|
|
355
|
-
|
|
356
589
|
/**
|
|
357
590
|
* Tokenizes the original input string
|
|
358
591
|
*
|
|
359
|
-
* @return
|
|
592
|
+
* @return An array of operator|text tokens
|
|
360
593
|
*/
|
|
361
594
|
tokenize() {
|
|
362
595
|
const list = [];
|
|
363
|
-
|
|
364
596
|
for (let i = 0, len = this.str.length; i < len; i++) {
|
|
365
597
|
const chr = this.str.charAt(i);
|
|
366
598
|
const nextChr = i < len - 1 ? this.str.charAt(i + 1) : null;
|
|
367
599
|
this.checkChar(chr, nextChr);
|
|
368
600
|
}
|
|
369
|
-
|
|
370
601
|
this.list.forEach(node => {
|
|
371
602
|
node.value = (node.value || '').toString().trim();
|
|
372
603
|
if (node.value) {
|
|
373
604
|
list.push(node);
|
|
374
605
|
}
|
|
375
606
|
});
|
|
376
|
-
|
|
377
607
|
return list;
|
|
378
608
|
}
|
|
379
|
-
|
|
380
609
|
/**
|
|
381
610
|
* Checks if a character is an operator or text and acts accordingly
|
|
382
611
|
*
|
|
383
|
-
* @param
|
|
612
|
+
* @param chr Character from the address field
|
|
613
|
+
* @param nextChr Character following chr, null at the end of the field
|
|
384
614
|
*/
|
|
385
615
|
checkChar(chr, nextChr) {
|
|
386
|
-
// Track RFC 5322 domain-literals ("[" *dtext "]").
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
616
|
+
// Track RFC 5322 domain-literals ("[" *dtext "]"). The ":" of an IPv6
|
|
617
|
+
// address-literal (user@[IPv6:2001:db8::1]) is dtext and must not be treated as
|
|
618
|
+
// the group delimiter while inside the brackets. That is the only operator the
|
|
619
|
+
// literal hides: letting it hide the others as well meant a "[" could turn the
|
|
620
|
+
// comment, quoted string or angle-addr after it into text and pick a different
|
|
621
|
+
// mailbox, '[ ( ] <a@victim.com> ) <b@evil.com>' yielded a@victim.com where
|
|
622
|
+
// RFC 5322 reads b@evil.com. A "[" also only opens a literal right after the "@"
|
|
623
|
+
// of an addr-spec, so the group delimiter of a display name holding one stays
|
|
624
|
+
// intact. Quoted strings and comments are handled separately via
|
|
625
|
+
// operatorExpecting, so only enter this state when no operator is open. The list
|
|
626
|
+
// separators "," and ";" always end the literal (and split the address list) so
|
|
627
|
+
// that an unclosed "[" cannot swallow later recipients.
|
|
393
628
|
if (!this.escaped && !this.operatorExpecting) {
|
|
394
|
-
if (!this.inDomainLiteral && chr === '[') {
|
|
629
|
+
if (!this.inDomainLiteral && chr === '[' && this.afterAt) {
|
|
395
630
|
this.inDomainLiteral = true;
|
|
396
|
-
}
|
|
631
|
+
}
|
|
632
|
+
else if (this.inDomainLiteral && (chr === ']' || chr === ',' || chr === ';')) {
|
|
397
633
|
this.inDomainLiteral = false;
|
|
398
634
|
}
|
|
399
635
|
}
|
|
400
|
-
|
|
401
636
|
if (this.escaped) {
|
|
402
637
|
// ignore next condition blocks
|
|
403
|
-
}
|
|
638
|
+
}
|
|
639
|
+
else if (chr === this.operatorExpecting) {
|
|
404
640
|
this.node = {
|
|
405
641
|
type: 'operator',
|
|
406
642
|
value: chr
|
|
407
643
|
};
|
|
408
|
-
|
|
409
644
|
if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) {
|
|
410
645
|
this.node.noBreak = true;
|
|
411
646
|
}
|
|
412
|
-
|
|
413
647
|
this.list.push(this.node);
|
|
414
648
|
this.node = null;
|
|
649
|
+
if (chr !== ')') {
|
|
650
|
+
// a comment is folding whitespace, it does not part the "@" from the "["
|
|
651
|
+
this.afterAt = false;
|
|
652
|
+
}
|
|
415
653
|
this.operatorExpecting = '';
|
|
416
654
|
this.escaped = false;
|
|
417
|
-
|
|
418
655
|
return;
|
|
419
|
-
}
|
|
656
|
+
}
|
|
657
|
+
else if (!this.operatorExpecting && !(this.inDomainLiteral && chr === ':') && chr in this.operators) {
|
|
420
658
|
this.node = {
|
|
421
659
|
type: 'operator',
|
|
422
660
|
value: chr
|
|
423
661
|
};
|
|
424
662
|
this.list.push(this.node);
|
|
425
663
|
this.node = null;
|
|
664
|
+
if (chr !== '(') {
|
|
665
|
+
this.afterAt = false;
|
|
666
|
+
}
|
|
426
667
|
this.operatorExpecting = this.operators[chr];
|
|
427
668
|
this.escaped = false;
|
|
428
669
|
return;
|
|
429
|
-
}
|
|
670
|
+
}
|
|
671
|
+
else if (['"', "'"].includes(this.operatorExpecting) && chr === '\\') {
|
|
430
672
|
this.escaped = true;
|
|
431
673
|
return;
|
|
432
674
|
}
|
|
433
|
-
|
|
434
675
|
if (!this.node) {
|
|
435
676
|
this.node = {
|
|
436
677
|
type: 'text',
|
|
@@ -438,76 +679,65 @@ class Tokenizer {
|
|
|
438
679
|
};
|
|
439
680
|
this.list.push(this.node);
|
|
440
681
|
}
|
|
441
|
-
|
|
442
682
|
if (chr === '\n') {
|
|
443
683
|
// Convert newlines to spaces. Carriage return is ignored as \r and \n usually
|
|
444
684
|
// go together anyway and there already is a WS for \n. Lone \r means something is fishy.
|
|
445
685
|
chr = ' ';
|
|
446
686
|
}
|
|
447
|
-
|
|
448
687
|
if (chr.charCodeAt(0) >= 0x21 || [' ', '\t'].includes(chr)) {
|
|
449
688
|
// skip command bytes
|
|
450
689
|
this.node.value += chr;
|
|
690
|
+
// text inside a quoted string or a comment is not part of an addr-spec. Tracked
|
|
691
|
+
// as it goes rather than read back off the value (see lastChars in _handleAddress)
|
|
692
|
+
if (!this.operatorExpecting && chr !== ' ' && chr !== '\t') {
|
|
693
|
+
this.afterAt = chr === '@';
|
|
694
|
+
}
|
|
451
695
|
}
|
|
452
|
-
|
|
453
696
|
this.escaped = false;
|
|
454
697
|
}
|
|
455
698
|
}
|
|
456
|
-
|
|
457
699
|
/**
|
|
458
700
|
* Maximum recursion depth for parsing nested groups.
|
|
459
701
|
* RFC 5322 doesn't allow nested groups, so this is a safeguard against
|
|
460
702
|
* malicious input that could cause stack overflow.
|
|
461
703
|
*/
|
|
462
704
|
const MAX_NESTED_GROUP_DEPTH = 50;
|
|
463
|
-
|
|
464
705
|
/**
|
|
465
|
-
* Parses
|
|
466
|
-
*
|
|
467
|
-
* Example:
|
|
468
|
-
*
|
|
469
|
-
* 'Name <address@domain>'
|
|
470
|
-
*
|
|
471
|
-
* will be converted to
|
|
706
|
+
* Parses an address list, recursing into the groups it holds.
|
|
472
707
|
*
|
|
473
|
-
*
|
|
708
|
+
* The depth is threaded through the calls rather than carried in an options object, so a
|
|
709
|
+
* caller supplied one can not seed it and lift the recursion limit: `{ _depth: -1e9 }`
|
|
710
|
+
* bought a billion levels of nesting and turned the guard below into the stack overflow it
|
|
711
|
+
* is there to prevent.
|
|
474
712
|
*
|
|
475
|
-
* @param
|
|
476
|
-
* @param
|
|
477
|
-
* @
|
|
478
|
-
* @return {Array} An array of address objects
|
|
713
|
+
* @param str Address field
|
|
714
|
+
* @param depth Current recursion depth for nested group protection
|
|
715
|
+
* @return An array of address objects
|
|
479
716
|
*/
|
|
480
|
-
function
|
|
481
|
-
options = options || {};
|
|
482
|
-
const depth = options._depth || 0;
|
|
483
|
-
|
|
717
|
+
function _parseAddressList(str, depth) {
|
|
484
718
|
// Prevent stack overflow from deeply nested groups (DoS protection)
|
|
485
719
|
if (depth > MAX_NESTED_GROUP_DEPTH) {
|
|
486
720
|
return [];
|
|
487
721
|
}
|
|
488
|
-
|
|
489
722
|
const tokenizer = new Tokenizer(str);
|
|
490
723
|
const tokens = tokenizer.tokenize();
|
|
491
|
-
|
|
492
724
|
const addresses = [];
|
|
493
725
|
let address = [];
|
|
494
|
-
|
|
495
|
-
|
|
726
|
+
const parsedAddresses = [];
|
|
496
727
|
tokens.forEach(token => {
|
|
497
728
|
if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
|
|
498
729
|
if (address.length) {
|
|
499
730
|
addresses.push(address);
|
|
500
731
|
}
|
|
501
732
|
address = [];
|
|
502
|
-
}
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
503
735
|
address.push(token);
|
|
504
736
|
}
|
|
505
737
|
});
|
|
506
|
-
|
|
507
738
|
if (address.length) {
|
|
508
739
|
addresses.push(address);
|
|
509
740
|
}
|
|
510
|
-
|
|
511
741
|
addresses.forEach(addr => {
|
|
512
742
|
const handled = _handleAddress(addr, depth);
|
|
513
743
|
// Appended in place. Rebuilding the accumulator with concat() would copy every
|
|
@@ -516,7 +746,6 @@ function addressparser(str, options) {
|
|
|
516
746
|
parsedAddresses.push(handled[i]);
|
|
517
747
|
}
|
|
518
748
|
});
|
|
519
|
-
|
|
520
749
|
// Merge fragments produced when unquoted display names contain commas.
|
|
521
750
|
// "Joe Foo, PhD <joe@example.com>" is split on the comma into
|
|
522
751
|
// [{name:"Joe Foo", address:""}, {name:"PhD", address:"joe@example.com"}].
|
|
@@ -529,16 +758,19 @@ function addressparser(str, options) {
|
|
|
529
758
|
const next = mergedAddresses.length ? mergedAddresses[mergedAddresses.length - 1] : null;
|
|
530
759
|
if (next && current.address === '' && current.name && !current.group && next.address && next.name) {
|
|
531
760
|
next.name = current.name + ', ' + next.name;
|
|
532
|
-
}
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
533
763
|
mergedAddresses.push(current);
|
|
534
764
|
}
|
|
535
765
|
}
|
|
536
766
|
mergedAddresses.reverse();
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
767
|
+
return mergedAddresses;
|
|
768
|
+
}
|
|
769
|
+
function addressparser(str, options) {
|
|
770
|
+
const parsedAddresses = _parseAddressList(str, 0);
|
|
771
|
+
if (options?.flatten) {
|
|
540
772
|
const flatAddresses = [];
|
|
541
|
-
const walkAddressList = list => {
|
|
773
|
+
const walkAddressList = (list) => {
|
|
542
774
|
list.forEach(entry => {
|
|
543
775
|
if (entry.group) {
|
|
544
776
|
return walkAddressList(entry.group);
|
|
@@ -549,8 +781,6 @@ function addressparser(str, options) {
|
|
|
549
781
|
walkAddressList(parsedAddresses);
|
|
550
782
|
return flatAddresses;
|
|
551
783
|
}
|
|
552
|
-
|
|
553
784
|
return parsedAddresses;
|
|
554
785
|
}
|
|
555
|
-
|
|
556
|
-
module.exports = addressparser;
|
|
786
|
+
export default addressparser;
|