@nocobase/plugin-workflow-mailer 2.3.0-beta.9 → 2.4.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/dist/externalVersion.js +6 -6
  2. package/dist/node_modules/joi/dist/joi-browser.min.js +1 -1
  3. package/dist/node_modules/joi/lib/index.js +1 -1
  4. package/dist/node_modules/joi/lib/messages.js +35 -6
  5. package/dist/node_modules/joi/lib/types/keys.js +18 -1
  6. package/dist/node_modules/joi/lib/types/link.js +18 -2
  7. package/dist/node_modules/joi/lib/types/string.js +2 -2
  8. package/dist/node_modules/joi/package.json +1 -1
  9. package/dist/node_modules/nodemailer/dist/cjs/addressparser/index.d.ts +59 -0
  10. package/dist/node_modules/nodemailer/dist/cjs/addressparser/index.js +790 -0
  11. package/dist/node_modules/nodemailer/dist/cjs/base64/index.d.ts +36 -0
  12. package/dist/node_modules/nodemailer/dist/cjs/base64/index.js +119 -0
  13. package/dist/node_modules/nodemailer/dist/cjs/dkim/index.d.ts +52 -0
  14. package/dist/node_modules/nodemailer/dist/cjs/dkim/index.js +228 -0
  15. package/dist/node_modules/nodemailer/dist/cjs/dkim/message-parser.d.ts +48 -0
  16. package/dist/node_modules/nodemailer/dist/cjs/dkim/message-parser.js +180 -0
  17. package/dist/node_modules/nodemailer/dist/cjs/dkim/relaxed-body.d.ts +36 -0
  18. package/dist/node_modules/nodemailer/dist/cjs/dkim/relaxed-body.js +148 -0
  19. package/dist/node_modules/nodemailer/dist/cjs/dkim/sign.d.ts +52 -0
  20. package/dist/node_modules/nodemailer/dist/cjs/dkim/sign.js +165 -0
  21. package/dist/node_modules/nodemailer/dist/cjs/errors.d.ts +96 -0
  22. package/dist/node_modules/nodemailer/dist/cjs/errors.js +70 -0
  23. package/dist/node_modules/nodemailer/dist/cjs/fetch/cookies.d.ts +107 -0
  24. package/dist/node_modules/nodemailer/dist/cjs/fetch/cookies.js +282 -0
  25. package/dist/node_modules/nodemailer/dist/cjs/fetch/index.d.ts +65 -0
  26. package/dist/node_modules/nodemailer/dist/cjs/fetch/index.js +384 -0
  27. package/dist/node_modules/nodemailer/dist/cjs/json-transport/index.d.ts +64 -0
  28. package/dist/node_modules/nodemailer/dist/cjs/json-transport/index.js +99 -0
  29. package/dist/node_modules/nodemailer/dist/cjs/mail-composer/index.d.ts +174 -0
  30. package/dist/node_modules/nodemailer/dist/cjs/mail-composer/index.js +613 -0
  31. package/dist/node_modules/nodemailer/dist/cjs/mailer/index.d.ts +237 -0
  32. package/dist/node_modules/nodemailer/dist/cjs/mailer/index.js +415 -0
  33. package/dist/node_modules/nodemailer/dist/cjs/mailer/mail-message.d.ts +97 -0
  34. package/dist/node_modules/nodemailer/dist/cjs/mailer/mail-message.js +342 -0
  35. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/index.d.ts +161 -0
  36. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/index.js +787 -0
  37. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/mime-types.d.ts +2 -0
  38. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/mime-types.js +2103 -0
  39. package/dist/node_modules/nodemailer/dist/cjs/mime-node/index.d.ts +333 -0
  40. package/dist/node_modules/nodemailer/dist/cjs/mime-node/index.js +1493 -0
  41. package/dist/node_modules/nodemailer/dist/cjs/mime-node/last-newline.d.ts +5 -0
  42. package/dist/node_modules/nodemailer/dist/cjs/mime-node/last-newline.js +32 -0
  43. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-unix.d.ts +9 -0
  44. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-unix.js +40 -0
  45. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-windows.d.ts +10 -0
  46. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-windows.js +47 -0
  47. package/dist/node_modules/nodemailer/dist/cjs/nodemailer.d.ts +124 -0
  48. package/dist/node_modules/nodemailer/dist/cjs/nodemailer.js +1 -0
  49. package/dist/node_modules/nodemailer/dist/cjs/package-info.d.ts +3 -0
  50. package/dist/node_modules/nodemailer/dist/cjs/package-info.js +7 -0
  51. package/dist/node_modules/nodemailer/dist/cjs/package.json +3 -0
  52. package/dist/node_modules/nodemailer/dist/cjs/punycode/index.d.ts +73 -0
  53. package/dist/node_modules/nodemailer/{lib → dist/cjs}/punycode/index.js +63 -112
  54. package/dist/node_modules/nodemailer/dist/cjs/qp/index.d.ts +32 -0
  55. package/dist/node_modules/nodemailer/dist/cjs/qp/index.js +207 -0
  56. package/dist/node_modules/nodemailer/dist/cjs/sendmail-transport/index.d.ts +69 -0
  57. package/dist/node_modules/nodemailer/dist/cjs/sendmail-transport/index.js +210 -0
  58. package/dist/node_modules/nodemailer/dist/cjs/ses-transport/index.d.ts +112 -0
  59. package/dist/node_modules/nodemailer/dist/cjs/ses-transport/index.js +252 -0
  60. package/dist/node_modules/nodemailer/dist/cjs/shared/index.d.ts +260 -0
  61. package/dist/node_modules/nodemailer/dist/cjs/shared/index.js +680 -0
  62. package/dist/node_modules/nodemailer/dist/cjs/shared/objects.d.ts +23 -0
  63. package/dist/node_modules/nodemailer/dist/cjs/shared/objects.js +43 -0
  64. package/dist/node_modules/nodemailer/dist/cjs/shared/url.d.ts +21 -0
  65. package/dist/node_modules/nodemailer/dist/cjs/shared/url.js +255 -0
  66. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/data-stream.d.ts +15 -0
  67. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/data-stream.js +108 -0
  68. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.d.ts +47 -0
  69. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js +189 -0
  70. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/index.d.ts +416 -0
  71. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/index.js +1837 -0
  72. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/index.d.ts +133 -0
  73. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/index.js +569 -0
  74. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.d.ts +62 -0
  75. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.js +257 -0
  76. package/dist/node_modules/nodemailer/dist/cjs/smtp-transport/index.d.ts +156 -0
  77. package/dist/node_modules/nodemailer/dist/cjs/smtp-transport/index.js +371 -0
  78. package/dist/node_modules/nodemailer/dist/cjs/stream-transport/index.d.ts +65 -0
  79. package/dist/node_modules/nodemailer/dist/cjs/stream-transport/index.js +146 -0
  80. package/dist/node_modules/nodemailer/dist/cjs/well-known/index.d.ts +35 -0
  81. package/dist/node_modules/nodemailer/dist/cjs/well-known/index.js +41 -0
  82. package/dist/node_modules/nodemailer/dist/cjs/well-known/services.d.ts +2 -0
  83. package/dist/node_modules/nodemailer/dist/cjs/well-known/services.js +674 -0
  84. package/dist/node_modules/nodemailer/dist/cjs/xoauth2/index.d.ts +201 -0
  85. package/dist/node_modules/nodemailer/dist/cjs/xoauth2/index.js +426 -0
  86. package/dist/node_modules/nodemailer/dist/esm/addressparser/index.d.ts +59 -0
  87. package/dist/node_modules/nodemailer/dist/esm/addressparser/index.js +786 -0
  88. package/dist/node_modules/nodemailer/dist/esm/base64/index.d.ts +36 -0
  89. package/dist/node_modules/nodemailer/{lib → dist/esm}/base64/index.js +39 -68
  90. package/dist/node_modules/nodemailer/dist/esm/dkim/index.d.ts +52 -0
  91. package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/index.js +62 -92
  92. package/dist/node_modules/nodemailer/dist/esm/dkim/message-parser.d.ts +48 -0
  93. package/dist/node_modules/nodemailer/dist/esm/dkim/message-parser.js +176 -0
  94. package/dist/node_modules/nodemailer/dist/esm/dkim/relaxed-body.d.ts +36 -0
  95. package/dist/node_modules/nodemailer/dist/esm/dkim/relaxed-body.js +141 -0
  96. package/dist/node_modules/nodemailer/dist/esm/dkim/sign.d.ts +52 -0
  97. package/dist/node_modules/nodemailer/dist/esm/dkim/sign.js +125 -0
  98. package/dist/node_modules/nodemailer/dist/esm/errors.d.ts +96 -0
  99. package/dist/node_modules/nodemailer/dist/esm/errors.js +67 -0
  100. package/dist/node_modules/nodemailer/dist/esm/fetch/cookies.d.ts +107 -0
  101. package/dist/node_modules/nodemailer/dist/esm/fetch/cookies.js +242 -0
  102. package/dist/node_modules/nodemailer/dist/esm/fetch/index.d.ts +65 -0
  103. package/dist/node_modules/nodemailer/dist/esm/fetch/index.js +344 -0
  104. package/dist/node_modules/nodemailer/dist/esm/json-transport/index.d.ts +64 -0
  105. package/dist/node_modules/nodemailer/dist/esm/json-transport/index.js +62 -0
  106. package/dist/node_modules/nodemailer/dist/esm/mail-composer/index.d.ts +174 -0
  107. package/dist/node_modules/nodemailer/{lib → dist/esm}/mail-composer/index.js +283 -275
  108. package/dist/node_modules/nodemailer/dist/esm/mailer/index.d.ts +237 -0
  109. package/dist/node_modules/nodemailer/{lib → dist/esm}/mailer/index.js +151 -205
  110. package/dist/node_modules/nodemailer/dist/esm/mailer/mail-message.d.ts +97 -0
  111. package/dist/node_modules/nodemailer/{lib → dist/esm}/mailer/mail-message.js +111 -125
  112. package/dist/node_modules/nodemailer/dist/esm/mime-funcs/index.d.ts +161 -0
  113. package/dist/node_modules/nodemailer/dist/esm/mime-funcs/index.js +738 -0
  114. package/dist/node_modules/nodemailer/dist/esm/mime-funcs/mime-types.d.ts +2 -0
  115. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-funcs/mime-types.js +43 -49
  116. package/dist/node_modules/nodemailer/dist/esm/mime-node/index.d.ts +333 -0
  117. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/index.js +576 -437
  118. package/dist/node_modules/nodemailer/dist/esm/mime-node/last-newline.d.ts +5 -0
  119. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/last-newline.js +4 -10
  120. package/dist/node_modules/nodemailer/dist/esm/mime-node/le-unix.d.ts +9 -0
  121. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/le-unix.js +7 -15
  122. package/dist/node_modules/nodemailer/dist/esm/mime-node/le-windows.d.ts +10 -0
  123. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/le-windows.js +6 -16
  124. package/dist/node_modules/nodemailer/dist/esm/nodemailer.d.ts +124 -0
  125. package/dist/node_modules/nodemailer/dist/esm/nodemailer.js +166 -0
  126. package/dist/node_modules/nodemailer/dist/esm/package-info.d.ts +3 -0
  127. package/dist/node_modules/nodemailer/dist/esm/package-info.js +4 -0
  128. package/dist/node_modules/nodemailer/dist/esm/package.json +3 -0
  129. package/dist/node_modules/nodemailer/dist/esm/punycode/index.d.ts +73 -0
  130. package/dist/node_modules/nodemailer/dist/esm/punycode/index.js +405 -0
  131. package/dist/node_modules/nodemailer/dist/esm/qp/index.d.ts +32 -0
  132. package/dist/node_modules/nodemailer/{lib → dist/esm}/qp/index.js +59 -77
  133. package/dist/node_modules/nodemailer/dist/esm/sendmail-transport/index.d.ts +69 -0
  134. package/dist/node_modules/nodemailer/dist/esm/sendmail-transport/index.js +170 -0
  135. package/dist/node_modules/nodemailer/dist/esm/ses-transport/index.d.ts +112 -0
  136. package/dist/node_modules/nodemailer/dist/esm/ses-transport/index.js +212 -0
  137. package/dist/node_modules/nodemailer/dist/esm/shared/index.d.ts +260 -0
  138. package/dist/node_modules/nodemailer/dist/esm/shared/index.js +631 -0
  139. package/dist/node_modules/nodemailer/dist/esm/shared/objects.d.ts +23 -0
  140. package/dist/node_modules/nodemailer/dist/esm/shared/objects.js +38 -0
  141. package/dist/node_modules/nodemailer/dist/esm/shared/url.d.ts +21 -0
  142. package/dist/node_modules/nodemailer/dist/esm/shared/url.js +214 -0
  143. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/data-stream.d.ts +15 -0
  144. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/data-stream.js +103 -0
  145. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.d.ts +47 -0
  146. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.js +149 -0
  147. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/index.d.ts +416 -0
  148. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/index.js +1797 -0
  149. package/dist/node_modules/nodemailer/dist/esm/smtp-pool/index.d.ts +133 -0
  150. package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-pool/index.js +195 -314
  151. package/dist/node_modules/nodemailer/dist/esm/smtp-pool/pool-resource.d.ts +62 -0
  152. package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-pool/pool-resource.js +70 -107
  153. package/dist/node_modules/nodemailer/dist/esm/smtp-transport/index.d.ts +156 -0
  154. package/dist/node_modules/nodemailer/dist/esm/smtp-transport/index.js +331 -0
  155. package/dist/node_modules/nodemailer/dist/esm/stream-transport/index.d.ts +65 -0
  156. package/dist/node_modules/nodemailer/dist/esm/stream-transport/index.js +106 -0
  157. package/dist/node_modules/nodemailer/dist/esm/well-known/index.d.ts +35 -0
  158. package/dist/node_modules/nodemailer/{lib → dist/esm}/well-known/index.js +12 -23
  159. package/dist/node_modules/nodemailer/dist/esm/well-known/services.d.ts +2 -0
  160. package/dist/node_modules/nodemailer/dist/esm/well-known/services.js +671 -0
  161. package/dist/node_modules/nodemailer/dist/esm/xoauth2/index.d.ts +201 -0
  162. package/dist/node_modules/nodemailer/dist/esm/xoauth2/index.js +386 -0
  163. package/dist/node_modules/nodemailer/dist/well-known/services.json +640 -0
  164. package/dist/node_modules/nodemailer/package.json +1 -1
  165. package/package.json +4 -4
  166. package/dist/node_modules/nodemailer/.gitattributes +0 -6
  167. package/dist/node_modules/nodemailer/.ncurc.js +0 -7
  168. package/dist/node_modules/nodemailer/.prettierrc.js +0 -8
  169. package/dist/node_modules/nodemailer/lib/addressparser/index.js +0 -313
  170. package/dist/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
  171. package/dist/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
  172. package/dist/node_modules/nodemailer/lib/dkim/sign.js +0 -117
  173. package/dist/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
  174. package/dist/node_modules/nodemailer/lib/fetch/index.js +0 -274
  175. package/dist/node_modules/nodemailer/lib/json-transport/index.js +0 -82
  176. package/dist/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
  177. package/dist/node_modules/nodemailer/lib/nodemailer.js +0 -1
  178. package/dist/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
  179. package/dist/node_modules/nodemailer/lib/ses-transport/index.js +0 -349
  180. package/dist/node_modules/nodemailer/lib/shared/index.js +0 -688
  181. package/dist/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
  182. package/dist/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
  183. package/dist/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1825
  184. package/dist/node_modules/nodemailer/lib/smtp-transport/index.js +0 -416
  185. package/dist/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
  186. package/dist/node_modules/nodemailer/lib/well-known/services.json +0 -343
  187. package/dist/node_modules/nodemailer/lib/xoauth2/index.js +0 -376
@@ -0,0 +1,786 @@
1
+ /**
2
+ * Restores the quoting of a local part that was read out of a quoted string.
3
+ *
4
+ * RFC 5321 allows '@' inside a quoted local part, so handing '"user@evil.com"@good.com'
5
+ * on as the bare 'user@evil.com@good.com' leaves it to the consumer which '@' splits the
6
+ * domain off. Getting that wrong is a misrouting vector, so the quotes go back on. The
7
+ * same holds for the other specials: a ',' or a ';' that loses its quotes reads as a
8
+ * recipient separator once the consumer puts the address back into a header.
9
+ *
10
+ * This module has no dependencies so that it can ship on its own, which is why the two
11
+ * grammar tests below are spelled out here instead of shared with src/mime-node. Keeping
12
+ * only what is ambiguous quoted is deliberate, mime-node applies the stricter RFC 5321
13
+ * dot-atom rule on top of this when it emits an address.
14
+ *
15
+ * @param address Address with an unquoted local part
16
+ * @return Address with the local part as a quoted-string
17
+ */
18
+ function _quoteLocalPart(address) {
19
+ const lastAt = address.lastIndexOf('@');
20
+ if (lastAt < 0) {
21
+ // no domain to split off, nothing can be misrouted
22
+ return address;
23
+ }
24
+ const user = address.substr(0, lastAt);
25
+ if (/^[^\s"(),:;<>@[\\\]]+$/.test(user) || /^"(?:[^"\\]|\\[\s\S])*"$/.test(user)) {
26
+ // a local part that carries no special reads the same with or without the quotes,
27
+ // and one that is already a complete quoted-string needs nothing either
28
+ return address;
29
+ }
30
+ return '"' + user.replace(/["\\]/g, '\\$&') + '"@' + address.substr(lastAt + 1);
31
+ }
32
+ /**
33
+ * Reached for every parsed address, so it is built once rather than per call.
34
+ */
35
+ const HAS_WHITESPACE = /\s/;
36
+ /**
37
+ * An addr-spec that carries its whitespace legally, inside a quoted local part. The
38
+ * optional tail is the malformed shape: a real mailbox with wreckage trailing it.
39
+ */
40
+ const QUOTED_LOCAL_ADDR = /^("(?:[^"\\]|\\[\s\S])*"@\S+)(?:\s+([\s\S]+))?$/;
41
+ /**
42
+ * One run holding a single '@' and no whitespace, the shape an addr-spec has to have.
43
+ */
44
+ const ADDR_SPEC = /^[^@\s]+@[^@\s]+$/;
45
+ /**
46
+ * The looser reading applied once the strict one finds nothing, which tolerates the
47
+ * further '@' that a domain should not have but malformed headers carry anyway.
48
+ */
49
+ const LOOSE_ADDR_SPEC = /^[^@\s]+@\S+$/;
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
+ }
178
+ /**
179
+ * Recovers the addr-spec from an angle-addr that came back holding unquoted whitespace.
180
+ *
181
+ * A malformed header can put more than a mailbox between the angle brackets, most often
182
+ * because the generator wrote the recipient twice: '<user@example.com user@example.com>'
183
+ * or '<example.com user@example.com>'. Whitespace is not addr-spec, so the whole run can
184
+ * never be a mailbox anyone could deliver to, and passing it on as the address loses the
185
+ * recipient that is sitting right there in the header.
186
+ *
187
+ * The run that still reads as an addr-spec is kept and whatever is left over becomes
188
+ * display text rather than being dropped. Candidates are read strictly first and then
189
+ * under the looser grammar, the same two tiers the unquoted-text branch below applies to
190
+ * the same problem, so that '<a@b@c.com junk>' and a bare 'a@b@c.com junk' agree on the
191
+ * recipient. When several runs qualify the first wins, which is what that branch's looser
192
+ * tier does within a token.
193
+ *
194
+ * A quoted local part is left alone: RFC 5321 allows whitespace inside it, so
195
+ * '<"user name"@example.com>' is well formed and means exactly what it says.
196
+ *
197
+ * @param data Collected address parts, mutated in place
198
+ */
199
+ function _recoverAddrSpec(data) {
200
+ if (!HAS_WHITESPACE.test(data.address)) {
201
+ return;
202
+ }
203
+ let address;
204
+ let rest;
205
+ const quoted = data.address.match(QUOTED_LOCAL_ADDR);
206
+ if (quoted) {
207
+ if (!quoted[2]) {
208
+ // the whitespace sits inside the quoted local part, this is a well formed mailbox
209
+ return;
210
+ }
211
+ // a real mailbox with wreckage trailing it, so peel the addr-spec off whole rather
212
+ // than splitting into the quotes
213
+ address = quoted[1];
214
+ rest = [quoted[2]];
215
+ }
216
+ else {
217
+ if (data.address.indexOf('"') >= 0) {
218
+ // Splitting on whitespace loses track of where the quoted string starts and ends,
219
+ // and this module does not take addresses out of quoted strings: the run picked out
220
+ // of '<junk "user@evil.com b"@good.com>' would be an address from the domain the
221
+ // quotes were hiding. Every well formed shape was already handled above, so what is
222
+ // left is wreckage either way and the original is the honest answer
223
+ return;
224
+ }
225
+ const parts = data.address.split(/\s+/);
226
+ let addrIndex = parts.findIndex(part => ADDR_SPEC.test(part));
227
+ if (addrIndex < 0) {
228
+ addrIndex = parts.findIndex(part => LOOSE_ADDR_SPEC.test(part));
229
+ }
230
+ if (addrIndex < 0) {
231
+ // nothing in there reads as an address, there is no better answer than the original
232
+ return;
233
+ }
234
+ address = parts.splice(addrIndex, 1)[0];
235
+ rest = parts;
236
+ }
237
+ data.address = address;
238
+ data.text = [data.text]
239
+ .concat(rest)
240
+ .filter(part => part)
241
+ .join(' ');
242
+ }
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
+ }
318
+ /**
319
+ * Converts tokens for a single address into an address object
320
+ *
321
+ * @param tokens Tokens object
322
+ * @param depth Current recursion depth for nested group protection
323
+ * @return Address object
324
+ */
325
+ function _handleAddress(tokens, depth) {
326
+ let isGroup = false;
327
+ let state = 'text';
328
+ const addresses = [];
329
+ const data = {
330
+ address: [],
331
+ comment: [],
332
+ group: [],
333
+ text: [],
334
+ textWasQuoted: []
335
+ };
336
+ let insideQuotes = false;
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: '' };
343
+ // Filter out <addresses>, (comments) and regular text
344
+ for (let i = 0, len = tokens.length; i < len; i++) {
345
+ const token = tokens[i];
346
+ const prevToken = i ? tokens[i - 1] : null;
347
+ if (token.type === 'operator') {
348
+ switch (token.value) {
349
+ case '<':
350
+ state = 'address';
351
+ insideQuotes = false;
352
+ break;
353
+ case '(':
354
+ state = 'comment';
355
+ insideQuotes = false;
356
+ break;
357
+ case ':':
358
+ state = 'group';
359
+ isGroup = true;
360
+ insideQuotes = false;
361
+ break;
362
+ case '"':
363
+ insideQuotes = !insideQuotes;
364
+ state = 'text';
365
+ break;
366
+ default:
367
+ state = 'text';
368
+ insideQuotes = false;
369
+ break;
370
+ }
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 === '"';
386
+ if (state === 'address') {
387
+ // Handle unquoted name that includes a "<".
388
+ // Apple Mail truncates everything between an unexpected < and an address.
389
+ token.value = token.value.replace(/^[^<]*<\s*/, '');
390
+ }
391
+ // A comment is folding whitespace. It may sit inside an addr-spec, on either side
392
+ // of the '@', but it cannot join two atoms into one: gluing across it would read
393
+ // 'user@example.com(x)evil.com' as the single domain 'example.comevil.com' and
394
+ // deliver to a domain the sender never named.
395
+ const parts = data[state];
396
+ const joins = prevToken &&
397
+ prevToken.noBreak &&
398
+ parts.length &&
399
+ (prevToken.value !== ')' || lastChars[state] === '@' || token.value.charAt(0) === '@');
400
+ if (joins) {
401
+ data[state][data[state].length - 1] += token.value;
402
+ if (token.value) {
403
+ lastChars[state] = token.value.charAt(token.value.length - 1);
404
+ }
405
+ if (state === 'text' && insideQuotes) {
406
+ data.textWasQuoted[data.textWasQuoted.length - 1] = true;
407
+ }
408
+ }
409
+ else {
410
+ data[state].push(token.value);
411
+ lastChars[state] = token.value.charAt(token.value.length - 1);
412
+ if (state === 'text') {
413
+ data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
414
+ }
415
+ }
416
+ }
417
+ }
418
+ // If there is no text but a comment, replace the two
419
+ if (!data.text.length && data.comment.length) {
420
+ data.text = data.comment;
421
+ data.comment = [];
422
+ }
423
+ if (isGroup) {
424
+ // http://tools.ietf.org/html/rfc2822#appendix-A.1.3
425
+ data.text = data.text.join(' ');
426
+ // Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting)
427
+ let groupMembers = [];
428
+ if (data.group.length) {
429
+ const parsedGroup = _parseAddressList(data.group.join(','), depth + 1);
430
+ parsedGroup.forEach(member => {
431
+ if (member.group) {
432
+ groupMembers = groupMembers.concat(member.group);
433
+ }
434
+ else {
435
+ groupMembers.push(member);
436
+ }
437
+ });
438
+ }
439
+ addresses.push({
440
+ name: data.text || '',
441
+ group: groupMembers
442
+ });
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;
463
+ // If no address was found, try to detect one from regular text
464
+ if (!data.address.length && data.text.length) {
465
+ for (let i = data.text.length - 1; i >= 0; i--) {
466
+ // Security: Do not extract email addresses from quoted strings.
467
+ // RFC 5321 allows @ inside quoted local-parts like "user@domain"@example.com.
468
+ // Extracting emails from quoted text leads to misrouting vulnerabilities.
469
+ if (!data.textWasQuoted[i] && ADDR_SPEC.test(data.text[i])) {
470
+ data.address = data.text.splice(i, 1);
471
+ data.textWasQuoted.splice(i, 1);
472
+ break;
473
+ }
474
+ }
475
+ // Try a looser regex match if strict match found nothing
476
+ if (!data.address.length) {
477
+ let extracted = false;
478
+ for (let i = data.text.length - 1; i >= 0; i--) {
479
+ // Security: Do not extract email addresses from quoted strings
480
+ if (!data.textWasQuoted[i]) {
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();
494
+ if (extracted) {
495
+ break;
496
+ }
497
+ }
498
+ }
499
+ }
500
+ }
501
+ // If there's still no text but a comment exists, replace the two
502
+ if (!data.text.length && data.comment.length) {
503
+ data.text = data.comment;
504
+ data.comment = [];
505
+ }
506
+ // Keep only the first address occurrence, push others to regular text
507
+ if (data.address.length > 1) {
508
+ data.text = data.text.concat(data.address.splice(1));
509
+ }
510
+ // An address is only taken from unquoted text, so anything left in the text at this
511
+ // point that still has to serve as the address carries its quoting in this flag
512
+ const addressFromQuotedText = !data.address.length && data.textWasQuoted.some(wasQuoted => wasQuoted);
513
+ // Join values with spaces
514
+ data.text = data.text.join(' ');
515
+ data.address = data.address.join(' ');
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
+ }
534
+ _recoverAddrSpec(data);
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
+ }
539
+ const address = {
540
+ address: data.address || data.text || '',
541
+ name: data.text || data.address || ''
542
+ };
543
+ if (address.address === address.name) {
544
+ if (/@/.test(address.address || '')) {
545
+ address.name = '';
546
+ }
547
+ else {
548
+ address.address = '';
549
+ }
550
+ }
551
+ addresses.push(address);
552
+ }
553
+ return addresses;
554
+ }
555
+ /**
556
+ * Creates a Tokenizer object for tokenizing address field strings
557
+ *
558
+ * @constructor
559
+ * @param str Address field string
560
+ */
561
+ class Tokenizer {
562
+ constructor(str) {
563
+ this.str = (str || '').toString();
564
+ this.operatorCurrent = '';
565
+ this.operatorExpecting = '';
566
+ this.node = null;
567
+ this.escaped = false;
568
+ this.inDomainLiteral = false;
569
+ this.afterAt = false;
570
+ this.list = [];
571
+ /**
572
+ * Operator tokens and which tokens are expected to end the sequence
573
+ */
574
+ this.operators = {
575
+ '"': '"',
576
+ '(': ')',
577
+ '<': '>',
578
+ ',': '',
579
+ ':': ';',
580
+ // Semicolons are not a legal delimiter per the RFC2822 grammar other
581
+ // than for terminating a group, but they are also not valid for any
582
+ // other use in this context. Given that some mail clients have
583
+ // historically allowed the semicolon as a delimiter equivalent to the
584
+ // comma in their UI, it makes sense to treat them the same as a comma
585
+ // when used outside of a group.
586
+ ';': ''
587
+ };
588
+ }
589
+ /**
590
+ * Tokenizes the original input string
591
+ *
592
+ * @return An array of operator|text tokens
593
+ */
594
+ tokenize() {
595
+ const list = [];
596
+ for (let i = 0, len = this.str.length; i < len; i++) {
597
+ const chr = this.str.charAt(i);
598
+ const nextChr = i < len - 1 ? this.str.charAt(i + 1) : null;
599
+ this.checkChar(chr, nextChr);
600
+ }
601
+ this.list.forEach(node => {
602
+ node.value = (node.value || '').toString().trim();
603
+ if (node.value) {
604
+ list.push(node);
605
+ }
606
+ });
607
+ return list;
608
+ }
609
+ /**
610
+ * Checks if a character is an operator or text and acts accordingly
611
+ *
612
+ * @param chr Character from the address field
613
+ * @param nextChr Character following chr, null at the end of the field
614
+ */
615
+ checkChar(chr, nextChr) {
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.
628
+ if (!this.escaped && !this.operatorExpecting) {
629
+ if (!this.inDomainLiteral && chr === '[' && this.afterAt) {
630
+ this.inDomainLiteral = true;
631
+ }
632
+ else if (this.inDomainLiteral && (chr === ']' || chr === ',' || chr === ';')) {
633
+ this.inDomainLiteral = false;
634
+ }
635
+ }
636
+ if (this.escaped) {
637
+ // ignore next condition blocks
638
+ }
639
+ else if (chr === this.operatorExpecting) {
640
+ this.node = {
641
+ type: 'operator',
642
+ value: chr
643
+ };
644
+ if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) {
645
+ this.node.noBreak = true;
646
+ }
647
+ this.list.push(this.node);
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
+ }
653
+ this.operatorExpecting = '';
654
+ this.escaped = false;
655
+ return;
656
+ }
657
+ else if (!this.operatorExpecting && !(this.inDomainLiteral && chr === ':') && chr in this.operators) {
658
+ this.node = {
659
+ type: 'operator',
660
+ value: chr
661
+ };
662
+ this.list.push(this.node);
663
+ this.node = null;
664
+ if (chr !== '(') {
665
+ this.afterAt = false;
666
+ }
667
+ this.operatorExpecting = this.operators[chr];
668
+ this.escaped = false;
669
+ return;
670
+ }
671
+ else if (['"', "'"].includes(this.operatorExpecting) && chr === '\\') {
672
+ this.escaped = true;
673
+ return;
674
+ }
675
+ if (!this.node) {
676
+ this.node = {
677
+ type: 'text',
678
+ value: ''
679
+ };
680
+ this.list.push(this.node);
681
+ }
682
+ if (chr === '\n') {
683
+ // Convert newlines to spaces. Carriage return is ignored as \r and \n usually
684
+ // go together anyway and there already is a WS for \n. Lone \r means something is fishy.
685
+ chr = ' ';
686
+ }
687
+ if (chr.charCodeAt(0) >= 0x21 || [' ', '\t'].includes(chr)) {
688
+ // skip command bytes
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
+ }
695
+ }
696
+ this.escaped = false;
697
+ }
698
+ }
699
+ /**
700
+ * Maximum recursion depth for parsing nested groups.
701
+ * RFC 5322 doesn't allow nested groups, so this is a safeguard against
702
+ * malicious input that could cause stack overflow.
703
+ */
704
+ const MAX_NESTED_GROUP_DEPTH = 50;
705
+ /**
706
+ * Parses an address list, recursing into the groups it holds.
707
+ *
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.
712
+ *
713
+ * @param str Address field
714
+ * @param depth Current recursion depth for nested group protection
715
+ * @return An array of address objects
716
+ */
717
+ function _parseAddressList(str, depth) {
718
+ // Prevent stack overflow from deeply nested groups (DoS protection)
719
+ if (depth > MAX_NESTED_GROUP_DEPTH) {
720
+ return [];
721
+ }
722
+ const tokenizer = new Tokenizer(str);
723
+ const tokens = tokenizer.tokenize();
724
+ const addresses = [];
725
+ let address = [];
726
+ const parsedAddresses = [];
727
+ tokens.forEach(token => {
728
+ if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
729
+ if (address.length) {
730
+ addresses.push(address);
731
+ }
732
+ address = [];
733
+ }
734
+ else {
735
+ address.push(token);
736
+ }
737
+ });
738
+ if (address.length) {
739
+ addresses.push(address);
740
+ }
741
+ addresses.forEach(addr => {
742
+ const handled = _handleAddress(addr, depth);
743
+ // Appended in place. Rebuilding the accumulator with concat() would copy every
744
+ // entry collected so far on each address, making a flat list cost O(n^2).
745
+ for (let i = 0; i < handled.length; i++) {
746
+ parsedAddresses.push(handled[i]);
747
+ }
748
+ });
749
+ // Merge fragments produced when unquoted display names contain commas.
750
+ // "Joe Foo, PhD <joe@example.com>" is split on the comma into
751
+ // [{name:"Joe Foo", address:""}, {name:"PhD", address:"joe@example.com"}].
752
+ // Recombine: a name-only entry followed by an entry with both name and address.
753
+ // Walked back to front so that a run of fragments folds into one entry in a single
754
+ // pass. Splicing each fragment out of the list instead would cost O(n^2).
755
+ const mergedAddresses = [];
756
+ for (let i = parsedAddresses.length - 1; i >= 0; i--) {
757
+ const current = parsedAddresses[i];
758
+ const next = mergedAddresses.length ? mergedAddresses[mergedAddresses.length - 1] : null;
759
+ if (next && current.address === '' && current.name && !current.group && next.address && next.name) {
760
+ next.name = current.name + ', ' + next.name;
761
+ }
762
+ else {
763
+ mergedAddresses.push(current);
764
+ }
765
+ }
766
+ mergedAddresses.reverse();
767
+ return mergedAddresses;
768
+ }
769
+ function addressparser(str, options) {
770
+ const parsedAddresses = _parseAddressList(str, 0);
771
+ if (options?.flatten) {
772
+ const flatAddresses = [];
773
+ const walkAddressList = (list) => {
774
+ list.forEach(entry => {
775
+ if (entry.group) {
776
+ return walkAddressList(entry.group);
777
+ }
778
+ flatAddresses.push(entry);
779
+ });
780
+ };
781
+ walkAddressList(parsedAddresses);
782
+ return flatAddresses;
783
+ }
784
+ return parsedAddresses;
785
+ }
786
+ export default addressparser;