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