@mavogel/awscdk-rootmail 0.1.24 → 0.1.26

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 (252) hide show
  1. package/.jsii +9 -9
  2. package/lib/rootmail.js +1 -1
  3. package/lib/ses-receive.js +1 -1
  4. package/lib/ses-receive.ops-santa-handler.js +2 -2
  5. package/node_modules/@aws-sdk/checksums/package.json +3 -3
  6. package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-cjs/index.js +9 -7
  7. package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-es/schemas/schemas_0.js +11 -6
  8. package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/commands/DescribeDeliveriesCommand.d.ts +3 -0
  9. package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/commands/DescribeDeliveryDestinationsCommand.d.ts +3 -0
  10. package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/commands/DescribeDeliverySourcesCommand.d.ts +3 -0
  11. package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/commands/PutResourcePolicyCommand.d.ts +1 -1
  12. package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/models/models_0.d.ts +27 -1
  13. package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/ts3.4/models/models_0.d.ts +3 -0
  14. package/node_modules/@aws-sdk/client-cloudwatch-logs/package.json +4 -4
  15. package/node_modules/@aws-sdk/client-route-53/dist-cjs/index.js +1 -1
  16. package/node_modules/@aws-sdk/client-route-53/package.json +4 -4
  17. package/node_modules/@aws-sdk/client-s3/dist-cjs/index.js +1 -1
  18. package/node_modules/@aws-sdk/client-s3/package.json +7 -7
  19. package/node_modules/@aws-sdk/client-ses/dist-cjs/index.js +1 -1
  20. package/node_modules/@aws-sdk/client-ses/package.json +4 -4
  21. package/node_modules/@aws-sdk/client-ssm/dist-cjs/index.js +1 -1
  22. package/node_modules/@aws-sdk/client-ssm/package.json +4 -4
  23. package/node_modules/@aws-sdk/core/dist-cjs/submodules/protocols/index.js +94 -1589
  24. package/node_modules/@aws-sdk/core/dist-es/submodules/protocols/index.js +1 -7
  25. package/node_modules/@aws-sdk/core/dist-es/submodules/protocols/json/AwsJsonRpcProtocol.js +1 -3
  26. package/node_modules/@aws-sdk/core/dist-es/submodules/protocols/json/AwsRestJsonProtocol.js +1 -3
  27. package/node_modules/@aws-sdk/core/dist-types/submodules/protocols/index.d.ts +2 -8
  28. package/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/AwsJson1_0Protocol.d.ts +1 -2
  29. package/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/AwsJson1_1Protocol.d.ts +1 -2
  30. package/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/AwsJsonRpcProtocol.d.ts +2 -3
  31. package/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/AwsRestJsonProtocol.d.ts +2 -3
  32. package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/index.d.ts +10 -10
  33. package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/AwsJson1_0Protocol.d.ts +1 -2
  34. package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/AwsJson1_1Protocol.d.ts +1 -2
  35. package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/AwsJsonRpcProtocol.d.ts +2 -3
  36. package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/AwsRestJsonProtocol.d.ts +2 -3
  37. package/node_modules/@aws-sdk/core/package.json +2 -2
  38. package/node_modules/@aws-sdk/credential-provider-env/package.json +3 -3
  39. package/node_modules/@aws-sdk/credential-provider-http/package.json +3 -3
  40. package/node_modules/@aws-sdk/credential-provider-ini/package.json +10 -10
  41. package/node_modules/@aws-sdk/credential-provider-login/package.json +4 -4
  42. package/node_modules/@aws-sdk/credential-provider-node/package.json +8 -8
  43. package/node_modules/@aws-sdk/credential-provider-process/package.json +3 -3
  44. package/node_modules/@aws-sdk/credential-provider-sso/package.json +5 -5
  45. package/node_modules/@aws-sdk/credential-provider-web-identity/package.json +4 -4
  46. package/node_modules/@aws-sdk/middleware-sdk-s3/package.json +3 -3
  47. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/cognito-identity/index.js +1 -1
  48. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/signin/index.js +1 -1
  49. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso/index.js +1 -1
  50. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso-oidc/index.js +1 -1
  51. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sts/index.js +1 -1
  52. package/node_modules/@aws-sdk/nested-clients/package.json +3 -3
  53. package/node_modules/@aws-sdk/token-providers/dist-cjs/index.js +37 -19
  54. package/node_modules/@aws-sdk/token-providers/dist-es/fromSso.js +37 -19
  55. package/node_modules/@aws-sdk/token-providers/package.json +4 -4
  56. package/node_modules/@smithy/core/dist-cjs/submodules/protocols/index.js +1695 -3
  57. package/node_modules/@smithy/core/dist-es/submodules/protocols/ConfigurableSerdeContext.js +6 -0
  58. package/node_modules/@smithy/core/dist-es/submodules/protocols/UnionSerde.js +26 -0
  59. package/node_modules/@smithy/core/dist-es/submodules/protocols/common.js +3 -0
  60. package/node_modules/@smithy/core/dist-es/submodules/protocols/index.js +8 -0
  61. package/node_modules/@smithy/core/dist-es/submodules/protocols/json/SmithyRpcV2JsonProtocol.js +102 -0
  62. package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/codec-v1/JsonShapeDeserializer.js +8 -1
  63. package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/codec-v1/JsonShapeSerializer.js +8 -1
  64. package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/codec-v2/JsonShapeDeserializer2.js +13 -2
  65. package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/codec-v2/JsonShapeSerializer2.js +11 -1
  66. package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/parseJsonBody.js +16 -1
  67. package/node_modules/@smithy/core/dist-es/submodules/protocols/writeKey.js +3 -0
  68. package/node_modules/@smithy/core/dist-types/submodules/protocols/ConfigurableSerdeContext.d.ts +8 -0
  69. package/node_modules/@smithy/core/dist-types/submodules/protocols/UnionSerde.d.ts +24 -0
  70. package/node_modules/@smithy/core/dist-types/submodules/protocols/common.d.ts +2 -0
  71. package/node_modules/@smithy/core/dist-types/submodules/protocols/index.d.ts +9 -0
  72. package/node_modules/@smithy/core/dist-types/submodules/protocols/json/JsonSettings.d.ts +13 -0
  73. package/node_modules/@smithy/core/dist-types/submodules/protocols/json/SmithyRpcV2JsonProtocol.d.ts +28 -0
  74. package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/parseJsonBody.d.ts +16 -0
  75. package/node_modules/@smithy/core/dist-types/submodules/protocols/writeKey.d.ts +4 -0
  76. package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/ConfigurableSerdeContext.d.ts +8 -0
  77. package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/UnionSerde.d.ts +24 -0
  78. package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/common.d.ts +2 -0
  79. package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/index.d.ts +9 -0
  80. package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/JsonSettings.d.ts +13 -0
  81. package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/SmithyRpcV2JsonProtocol.d.ts +28 -0
  82. package/node_modules/{@aws-sdk → @smithy}/core/dist-types/ts3.4/submodules/protocols/json/codec-v1/JsonCodec.d.ts +15 -11
  83. package/node_modules/{@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json → @smithy/core/dist-types/ts3.4/submodules/protocols/json/codec-v1}/JsonSettings.d.ts +7 -4
  84. package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/codec-v1/JsonShapeDeserializer.d.ts +14 -0
  85. package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/codec-v1/JsonShapeSerializer.d.ts +29 -0
  86. package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/codec-v2/JsonBytesStringAdapter.d.ts +51 -0
  87. package/node_modules/{@aws-sdk → @smithy}/core/dist-types/ts3.4/submodules/protocols/json/codec-v2/JsonCodec2.d.ts +13 -9
  88. package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/codec-v2/JsonShapeDeserializer2.d.ts +30 -0
  89. package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/codec-v2/JsonShapeSerializer2.d.ts +60 -0
  90. package/node_modules/{@aws-sdk → @smithy}/core/dist-types/ts3.4/submodules/protocols/json/detectBufferParsing.d.ts +1 -1
  91. package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/jsonReplacer.d.ts +21 -0
  92. package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/jsonReviver.d.ts +15 -0
  93. package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/needsReviver.d.ts +11 -0
  94. package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/parseJsonBody.d.ts +38 -0
  95. package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/writeKey.d.ts +4 -0
  96. package/node_modules/@smithy/core/package.json +2 -1
  97. package/node_modules/@zone-eu/mailsplit/README.md +43 -54
  98. package/node_modules/@zone-eu/mailsplit/lib/headers.js +10 -4
  99. package/node_modules/@zone-eu/mailsplit/lib/message-splitter.js +5 -4
  100. package/node_modules/@zone-eu/mailsplit/lib/mime-node.d.ts +4 -1
  101. package/node_modules/@zone-eu/mailsplit/lib/mime-node.js +30 -5
  102. package/node_modules/@zone-eu/mailsplit/lib/types.d.ts +3 -1
  103. package/node_modules/@zone-eu/mailsplit/package.json +22 -17
  104. package/node_modules/libbase64/CHANGELOG.md +20 -0
  105. package/node_modules/libbase64/README.md +40 -13
  106. package/node_modules/libbase64/dist/cjs/libbase64.d.ts +80 -0
  107. package/node_modules/libbase64/dist/cjs/libbase64.js +310 -0
  108. package/node_modules/libbase64/dist/cjs/package.json +3 -0
  109. package/node_modules/libbase64/dist/esm/libbase64.d.ts +80 -0
  110. package/node_modules/libbase64/{lib → dist/esm}/libbase64.js +74 -117
  111. package/node_modules/libbase64/dist/esm/package.json +3 -0
  112. package/node_modules/libbase64/package.json +59 -13
  113. package/node_modules/libmime/CHANGELOG.md +38 -0
  114. package/node_modules/libmime/README.md +55 -26
  115. package/node_modules/libmime/SECURITY.md +152 -0
  116. package/node_modules/libmime/dist/cjs/charset.d.ts +43 -0
  117. package/node_modules/libmime/dist/cjs/charset.js +135 -0
  118. package/node_modules/libmime/dist/cjs/charsets.d.ts +2 -0
  119. package/node_modules/libmime/dist/cjs/charsets.js +213 -0
  120. package/node_modules/libmime/dist/cjs/get-charset-name.d.ts +1 -0
  121. package/node_modules/libmime/dist/cjs/get-charset-name.js +224 -0
  122. package/node_modules/libmime/dist/cjs/libmime.d.ts +218 -0
  123. package/node_modules/libmime/dist/cjs/libmime.js +1027 -0
  124. package/node_modules/libmime/dist/cjs/mimetypes.d.ts +7 -0
  125. package/node_modules/libmime/dist/cjs/mimetypes.js +2072 -0
  126. package/node_modules/libmime/dist/cjs/package.json +3 -0
  127. package/node_modules/libmime/dist/cjs/types.d.ts +59 -0
  128. package/node_modules/libmime/dist/cjs/types.js +4 -0
  129. package/node_modules/libmime/dist/esm/charset.d.ts +43 -0
  130. package/node_modules/libmime/dist/esm/charset.js +125 -0
  131. package/node_modules/libmime/dist/esm/charsets.d.ts +2 -0
  132. package/node_modules/libmime/{lib → dist/esm}/charsets.js +3 -5
  133. package/node_modules/libmime/dist/esm/get-charset-name.d.ts +1 -0
  134. package/node_modules/libmime/{lib → dist/esm}/get-charset-name.js +1 -8
  135. package/node_modules/libmime/dist/esm/libmime.d.ts +219 -0
  136. package/node_modules/libmime/{lib → dist/esm}/libmime.js +275 -334
  137. package/node_modules/libmime/dist/esm/mimetypes.d.ts +7 -0
  138. package/node_modules/libmime/dist/esm/mimetypes.js +2068 -0
  139. package/node_modules/libmime/dist/esm/package.json +3 -0
  140. package/node_modules/libmime/dist/esm/types.d.ts +59 -0
  141. package/node_modules/libmime/dist/esm/types.js +3 -0
  142. package/node_modules/libmime/package.json +60 -15
  143. package/node_modules/libqp/CHANGELOG.md +18 -0
  144. package/node_modules/libqp/README.md +50 -19
  145. package/node_modules/libqp/dist/cjs/libqp.d.ts +74 -0
  146. package/node_modules/libqp/dist/cjs/libqp.js +378 -0
  147. package/node_modules/libqp/dist/cjs/package.json +3 -0
  148. package/node_modules/libqp/dist/esm/libqp.d.ts +74 -0
  149. package/node_modules/libqp/dist/esm/libqp.js +369 -0
  150. package/node_modules/libqp/dist/esm/package.json +3 -0
  151. package/node_modules/libqp/lib/libqp.js +4 -383
  152. package/node_modules/libqp/lib/package.json +3 -0
  153. package/node_modules/libqp/package.json +62 -6
  154. package/node_modules/mailparser/CHANGELOG.md +56 -0
  155. package/node_modules/mailparser/README.md +6 -0
  156. package/node_modules/mailparser/dist/cjs/index.d.ts +10 -0
  157. package/node_modules/mailparser/dist/cjs/index.js +12 -0
  158. package/node_modules/mailparser/dist/cjs/mail-parser.d.ts +98 -0
  159. package/node_modules/mailparser/dist/cjs/mail-parser.js +1419 -0
  160. package/node_modules/mailparser/dist/cjs/package.json +3 -0
  161. package/node_modules/mailparser/dist/cjs/simple-parser.d.ts +17 -0
  162. package/node_modules/mailparser/dist/cjs/simple-parser.js +133 -0
  163. package/node_modules/mailparser/dist/cjs/stream-hash.d.ts +24 -0
  164. package/node_modules/mailparser/dist/cjs/stream-hash.js +33 -0
  165. package/node_modules/mailparser/dist/cjs/tlds.cjs +8 -0
  166. package/node_modules/mailparser/dist/cjs/types.d.ts +309 -0
  167. package/node_modules/mailparser/dist/cjs/types.js +2 -0
  168. package/node_modules/mailparser/dist/esm/index.d.ts +10 -0
  169. package/node_modules/mailparser/dist/esm/index.js +4 -0
  170. package/node_modules/mailparser/dist/esm/mail-parser.d.ts +98 -0
  171. package/node_modules/mailparser/{lib → dist/esm}/mail-parser.js +302 -368
  172. package/node_modules/mailparser/dist/esm/package.json +3 -0
  173. package/node_modules/mailparser/dist/esm/simple-parser.d.ts +17 -0
  174. package/node_modules/mailparser/{lib → dist/esm}/simple-parser.js +45 -68
  175. package/node_modules/mailparser/dist/esm/stream-hash.d.ts +24 -0
  176. package/node_modules/mailparser/{lib → dist/esm}/stream-hash.js +7 -9
  177. package/node_modules/mailparser/dist/esm/tlds.cjs +8 -0
  178. package/node_modules/mailparser/dist/esm/types.d.ts +309 -0
  179. package/node_modules/mailparser/dist/esm/types.js +1 -0
  180. package/node_modules/mailparser/package.json +88 -17
  181. package/node_modules/nodemailer/CHANGELOG.md +44 -0
  182. package/node_modules/nodemailer/dist/cjs/addressparser/index.js +695 -267
  183. package/node_modules/nodemailer/dist/cjs/mailer/mail-message.js +4 -1
  184. package/node_modules/nodemailer/dist/cjs/mime-funcs/index.js +8 -2
  185. package/node_modules/nodemailer/dist/cjs/mime-node/index.js +48 -16
  186. package/node_modules/nodemailer/dist/cjs/package-info.d.ts +1 -1
  187. package/node_modules/nodemailer/dist/cjs/package-info.js +1 -1
  188. package/node_modules/nodemailer/dist/cjs/shared/url.js +22 -5
  189. package/node_modules/nodemailer/dist/esm/addressparser/index.js +695 -267
  190. package/node_modules/nodemailer/dist/esm/mailer/mail-message.js +4 -1
  191. package/node_modules/nodemailer/dist/esm/mime-funcs/index.js +8 -2
  192. package/node_modules/nodemailer/dist/esm/mime-node/index.js +48 -16
  193. package/node_modules/nodemailer/dist/esm/package-info.d.ts +1 -1
  194. package/node_modules/nodemailer/dist/esm/package-info.js +1 -1
  195. package/node_modules/nodemailer/dist/esm/shared/url.js +22 -5
  196. package/node_modules/nodemailer/package.json +1 -1
  197. package/package.json +9 -9
  198. package/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/codec-v1/JsonSettings.d.ts +0 -7
  199. package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/codec-v1/JsonSettings.d.ts +0 -4
  200. package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/codec-v1/JsonShapeDeserializer.d.ts +0 -13
  201. package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/codec-v1/JsonShapeSerializer.d.ts +0 -18
  202. package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/codec-v2/JsonBytesStringAdapter.d.ts +0 -27
  203. package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/codec-v2/JsonShapeDeserializer2.d.ts +0 -15
  204. package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/codec-v2/JsonShapeSerializer2.d.ts +0 -39
  205. package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/jsonReplacer.d.ts +0 -7
  206. package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/jsonReviver.d.ts +0 -7
  207. package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/needsReviver.d.ts +0 -2
  208. package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/parseJsonBody.d.ts +0 -14
  209. package/node_modules/libbase64/.eslintrc +0 -11
  210. package/node_modules/libbase64/.github/FUNDING.yml +0 -4
  211. package/node_modules/libbase64/.github/VULNERABILITY_REPORT.yml +0 -75
  212. package/node_modules/libbase64/.github/workflows/release.yaml +0 -39
  213. package/node_modules/libbase64/.github/workflows/test.yml +0 -21
  214. package/node_modules/libbase64/.ncurc.js +0 -13
  215. package/node_modules/libbase64/.prettierrc.js +0 -8
  216. package/node_modules/libmime/lib/charset.js +0 -131
  217. package/node_modules/libmime/lib/mimetypes.js +0 -2049
  218. package/node_modules/libqp/.eslintrc +0 -15
  219. package/node_modules/libqp/.github/FUNDING.yml +0 -4
  220. package/node_modules/libqp/.github/VULNERABILITY_REPORT.yml +0 -75
  221. package/node_modules/libqp/.github/workflows/release.yaml +0 -39
  222. package/node_modules/libqp/.github/workflows/test.yml +0 -21
  223. package/node_modules/libqp/.prettierrc.js +0 -8
  224. package/node_modules/libqp/test/libqp-test.js +0 -366
  225. package/node_modules/mailparser/.gitattributes +0 -1
  226. package/node_modules/mailparser/.prettierignore +0 -3
  227. package/node_modules/mailparser/.prettierrc.js +0 -10
  228. package/node_modules/mailparser/.release-please-manifest.json +0 -3
  229. package/node_modules/mailparser/eslint.config.js +0 -33
  230. package/node_modules/mailparser/index.js +0 -9
  231. package/node_modules/mailparser/release-please-config.json +0 -10
  232. /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/JsonSettings.js +0 -0
  233. /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/codec-v1/JsonCodec.js +0 -0
  234. /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/codec-v1/JsonSettings.js +0 -0
  235. /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/codec-v2/JsonBytesStringAdapter.js +0 -0
  236. /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/codec-v2/JsonCodec2.js +0 -0
  237. /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/detectBufferParsing.js +0 -0
  238. /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/jsonReplacer.js +0 -0
  239. /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/jsonReviver.js +0 -0
  240. /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/needsReviver.js +0 -0
  241. /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/codec-v1/JsonCodec.d.ts +0 -0
  242. /package/node_modules/{@aws-sdk/core/dist-types/submodules/protocols/json → @smithy/core/dist-types/submodules/protocols/json/codec-v1}/JsonSettings.d.ts +0 -0
  243. /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/codec-v1/JsonShapeDeserializer.d.ts +0 -0
  244. /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/codec-v1/JsonShapeSerializer.d.ts +0 -0
  245. /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/codec-v2/JsonBytesStringAdapter.d.ts +0 -0
  246. /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/codec-v2/JsonCodec2.d.ts +0 -0
  247. /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/codec-v2/JsonShapeDeserializer2.d.ts +0 -0
  248. /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/codec-v2/JsonShapeSerializer2.d.ts +0 -0
  249. /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/detectBufferParsing.d.ts +0 -0
  250. /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/jsonReplacer.d.ts +0 -0
  251. /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/jsonReviver.d.ts +0 -0
  252. /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/needsReviver.d.ts +0 -0
@@ -1,3 +1,97 @@
1
+ /* eslint-disable no-bitwise -- the kind of a token is a set of bit flags packed into one slot of a typed array */
2
+ /** The token is an operator, a text run otherwise */
3
+ const TOKEN_OPERATOR = 1;
4
+ /** The next character continues the same run without whitespace in between */
5
+ const TOKEN_NO_BREAK = 2;
6
+ /** The text run holds characters its value leaves out or changes, so it is no plain slice */
7
+ const TOKEN_DIRTY = 4;
8
+ /** A backslash escapes the next character in the text run, inside a quoted string or a comment */
9
+ const TOKEN_ESCAPES = 8;
10
+ /** The text run opens on a character an escaping backslash in front of it has escaped */
11
+ const TOKEN_STARTS_ESCAPED = 16;
12
+ /**
13
+ * The tokens of an address field, kept as offsets into it.
14
+ *
15
+ * Every token used to be an object of its own, an operator for every ',' and a value string
16
+ * for every run of text, and a list of one letter names, 'a,a,a,...', took some 65 times its
17
+ * size in tokens before any address was read. A token is now three numbers in one typed
18
+ * array, its kind and the range of the field it covers, and its value is sliced off the field
19
+ * when it is read. The few text runs whose value differs from their slice, as they hold an
20
+ * escaping backslash, a line break or a control character, are rebuilt once and kept aside.
21
+ */
22
+ class TokenList {
23
+ constructor(str) {
24
+ this.str = str;
25
+ this.slots = new Int32Array(48);
26
+ this.length = 0;
27
+ this.cleaned = new Map();
28
+ }
29
+ /**
30
+ * Appends a token
31
+ *
32
+ * @param kind Kind and flags of the token
33
+ * @param start Offset of its first character
34
+ * @param end Offset just past its last character
35
+ * @return Index of the token
36
+ */
37
+ push(kind, start, end) {
38
+ if ((this.length + 1) * 3 > this.slots.length) {
39
+ const grown = new Int32Array(this.slots.length * 2);
40
+ grown.set(this.slots);
41
+ this.slots = grown;
42
+ }
43
+ const at = this.length * 3;
44
+ this.slots[at] = kind;
45
+ this.slots[at + 1] = start;
46
+ this.slots[at + 2] = end;
47
+ return this.length++;
48
+ }
49
+ /**
50
+ * The character of an operator token, an empty string for a text run
51
+ */
52
+ operatorAt(index) {
53
+ const at = index * 3;
54
+ return this.slots[at] & TOKEN_OPERATOR ? this.str.charAt(this.slots[at + 1]) : '';
55
+ }
56
+ /**
57
+ * Whether a token is an operator
58
+ */
59
+ isOperator(index) {
60
+ return (this.slots[index * 3] & TOKEN_OPERATOR) !== 0;
61
+ }
62
+ /**
63
+ * Whether the next character continues the run of a token without whitespace in between
64
+ */
65
+ noBreak(index) {
66
+ return (this.slots[index * 3] & TOKEN_NO_BREAK) !== 0;
67
+ }
68
+ /**
69
+ * Value of a token
70
+ */
71
+ value(index) {
72
+ const cleaned = this.cleaned.get(index);
73
+ if (cleaned !== undefined) {
74
+ return cleaned;
75
+ }
76
+ return this.str.slice(this.slots[index * 3 + 1], this.slots[index * 3 + 2]);
77
+ }
78
+ }
79
+ /**
80
+ * A domain literal that ends an address. dtext includes '@' (RFC 5322 3.4.1), so the '@'
81
+ * in front of the literal is the one that splits the domain off, not the last one: reading
82
+ * 'a@[x@evil.com]' at its last '@' made it an address at evil.com.
83
+ */
84
+ const DOMAIN_LITERAL_TAIL = /@(\[[^[\]\\]*\])$/;
85
+ /**
86
+ * Offset of the '@' that splits the domain off an address, or -1 when there is none
87
+ *
88
+ * @param address Address to split
89
+ * @return Offset of the '@'
90
+ */
91
+ function _domainAt(address) {
92
+ const literal = address.charAt(address.length - 1) === ']' ? DOMAIN_LITERAL_TAIL.exec(address) : null;
93
+ return literal ? address.length - literal[1].length - 1 : address.lastIndexOf('@');
94
+ }
1
95
  /**
2
96
  * Restores the quoting of a local part that was read out of a quoted string.
3
97
  *
@@ -16,7 +110,7 @@
16
110
  * @return Address with the local part as a quoted-string
17
111
  */
18
112
  function _quoteLocalPart(address) {
19
- const lastAt = address.lastIndexOf('@');
113
+ const lastAt = _domainAt(address);
20
114
  if (lastAt < 0) {
21
115
  // no domain to split off, nothing can be misrouted
22
116
  return address;
@@ -42,6 +136,23 @@ const QUOTED_LOCAL_ADDR = /^("(?:[^"\\]|\\[\s\S])*"@\S+)(?:\s+([\s\S]+))?$/;
42
136
  * One run holding a single '@' and no whitespace, the shape an addr-spec has to have.
43
137
  */
44
138
  const ADDR_SPEC = /^[^@\s]+@[^@\s]+$/;
139
+ /**
140
+ * The same with a domain literal for the domain, which may hold an '@' of its own.
141
+ */
142
+ const LITERAL_ADDR_SPEC = /^[^@\s]+@\[[^[\]\\\s]*\]$/;
143
+ /**
144
+ * The '@' and domain that follow a quoted local part in the same run.
145
+ */
146
+ const QUOTED_LOCAL_DOMAIN = /^@(?:[^@\s"]+|\[[^[\]\\\s]*\])$/;
147
+ /**
148
+ * Whether a whitespace free run reads as an addr-spec under the strict grammar
149
+ *
150
+ * @param value Run to check
151
+ * @return true when it does
152
+ */
153
+ function _isAddrSpec(value) {
154
+ return ADDR_SPEC.test(value) || LITERAL_ADDR_SPEC.test(value);
155
+ }
45
156
  /**
46
157
  * The looser reading applied once the strict one finds nothing, which tolerates the
47
158
  * further '@' that a domain should not have but malformed headers carry anyway.
@@ -108,6 +219,62 @@ function _indexOfAt(text, from, to) {
108
219
  }
109
220
  return -1;
110
221
  }
222
+ /**
223
+ * A quoted-string a value opens with.
224
+ */
225
+ const LEADING_QUOTED_STRING = /^"(?:[^"\\]|\\[\s\S])*"/;
226
+ /**
227
+ * Drops the whitespace on either side of the '@' at an offset.
228
+ *
229
+ * obs-local-part and obs-domain allow CFWS around the '@' (RFC 5322 4.4), so 'user @ good.com'
230
+ * names user@good.com. Kept, the whitespace went into the envelope as '"user "@ good.com'.
231
+ * The scan is by index, a pattern such as /\s+@/ backtracks over a long whitespace run that
232
+ * no '@' follows.
233
+ *
234
+ * @param value Value holding the '@'
235
+ * @param at Offset of the '@', or -1
236
+ * @return The value with the whitespace beside that '@' removed
237
+ */
238
+ function _joinAtSpacing(value, at) {
239
+ if (at < 0) {
240
+ return value;
241
+ }
242
+ let start = at;
243
+ while (start > 0 && _isSpaceCode(value.charCodeAt(start - 1))) {
244
+ start--;
245
+ }
246
+ let end = at + 1;
247
+ while (end < value.length && _isSpaceCode(value.charCodeAt(end))) {
248
+ end++;
249
+ }
250
+ return start === at && end === at + 1 ? value : value.slice(0, start) + '@' + value.slice(end);
251
+ }
252
+ /**
253
+ * Drops the whitespace beside the '@' that splits the domain off a value. That is the '@'
254
+ * right after a quoted local part the value opens with, or else the only '@' it holds outside
255
+ * any quoted string. With several it is not clear which one the whitespace belongs to, and
256
+ * '<x@evil.com @good.com>' is left to the addr-spec recovery to read as it always has.
257
+ *
258
+ * @param value Value to clean up
259
+ * @return The value with the whitespace beside that '@' removed
260
+ */
261
+ function _collapseAtSpacing(value) {
262
+ const quoted = LEADING_QUOTED_STRING.exec(value);
263
+ if (quoted) {
264
+ let at = quoted[0].length;
265
+ while (at < value.length && _isSpaceCode(value.charCodeAt(at))) {
266
+ at++;
267
+ }
268
+ if (value.charAt(at) === '@' && value.indexOf('@', at + 1) < 0) {
269
+ return _joinAtSpacing(value, at);
270
+ }
271
+ }
272
+ const at = value.indexOf('@');
273
+ if (at < 0 || value.indexOf('@', at + 1) >= 0 || value.lastIndexOf('"') > at) {
274
+ return value;
275
+ }
276
+ return _joinAtSpacing(value, at);
277
+ }
111
278
  /**
112
279
  * Finds the offset LOOSE_TEXT_ADDR matches at, or -1 when it does not match at all.
113
280
  *
@@ -197,6 +364,7 @@ function _looseAddressStart(text) {
197
364
  * @param data Collected address parts, mutated in place
198
365
  */
199
366
  function _recoverAddrSpec(data) {
367
+ data.address = _collapseAtSpacing(data.address);
200
368
  if (!HAS_WHITESPACE.test(data.address)) {
201
369
  return;
202
370
  }
@@ -223,7 +391,7 @@ function _recoverAddrSpec(data) {
223
391
  return;
224
392
  }
225
393
  const parts = data.address.split(/\s+/);
226
- let addrIndex = parts.findIndex(part => ADDR_SPEC.test(part));
394
+ let addrIndex = parts.findIndex(_isAddrSpec);
227
395
  if (addrIndex < 0) {
228
396
  addrIndex = parts.findIndex(part => LOOSE_ADDR_SPEC.test(part));
229
397
  }
@@ -250,6 +418,15 @@ function _recoverAddrSpec(data) {
250
418
  * A comment is folding whitespace, so it is read as a space that splits two atoms and as
251
419
  * nothing next to an '@', the same rule the token walk applies outside the brackets. Quoted
252
420
  * strings and domain literals are copied through untouched, a '(' in there is not a comment.
421
+ * An unexpected '<' outside them drops everything in front of it, comments included.
422
+ *
423
+ * RFC 822 also let an angle-addr name the relays to go through in front of the mailbox,
424
+ * '<@relay1,@relay2:user@example.com>', which RFC 5322 still parses as obs-route. The route
425
+ * is not part of the mailbox, and folded into the local part it made
426
+ * '"@relay1,@relay2:user"@example.com' a recipient that does not exist. A local part can not
427
+ * start with '@', so contents that do, up to the first ':' outside a domain literal and in
428
+ * front of any quoted string, are a route and are dropped, as long as a mailbox with an '@'
429
+ * of its own follows.
253
430
  *
254
431
  * @param address Contents of the angle brackets
255
432
  * @return The address with the comments removed, and the text of the comments
@@ -263,6 +440,16 @@ function _stripAddressComments(address) {
263
440
  // carried along rather than read back off the growing result, which would flatten it on
264
441
  // every comment (see lastChars in _handleAddress)
265
442
  let lastChar = '';
443
+ // A comment that just closed stands for a space, written once the next character shows
444
+ // whether one is wanted: not beside an '@', and not on top of whitespace that follows.
445
+ // Deciding on the spot saw only the character right after the ')', so a second comment
446
+ // in a row hid the '@' behind it and 'a(x)(y)@b' read as 'a @b'
447
+ let pendingSpace = false;
448
+ // first character of the result and whether a quoted string has opened since, which
449
+ // together tell a route from a local part
450
+ let firstChar = '';
451
+ let quoteSeen = false;
452
+ let routeEnd = -1;
266
453
  for (let i = 0, len = address.length; i < len; i++) {
267
454
  const chr = address.charAt(i);
268
455
  if (depth) {
@@ -276,16 +463,20 @@ function _stripAddressComments(address) {
276
463
  else if (chr === ')' && !--depth) {
277
464
  comments.push(comment.trim());
278
465
  comment = '';
279
- if (lastChar !== '@' && address.charAt(i + 1) !== '@') {
280
- result += ' ';
281
- lastChar = ' ';
282
- }
466
+ pendingSpace = lastChar !== '@';
283
467
  }
284
468
  else {
285
469
  comment += chr;
286
470
  }
287
471
  continue;
288
472
  }
473
+ if (pendingSpace && chr !== '(') {
474
+ pendingSpace = false;
475
+ if (chr !== '@' && chr !== '<' && !_isSpaceCode(address.charCodeAt(i))) {
476
+ result += ' ';
477
+ lastChar = ' ';
478
+ }
479
+ }
289
480
  if (closer) {
290
481
  if (chr === '\\' && closer === '"' && i < len - 1) {
291
482
  result += chr + address.charAt(++i);
@@ -298,6 +489,10 @@ function _stripAddressComments(address) {
298
489
  }
299
490
  else if (chr === '"') {
300
491
  closer = '"';
492
+ quoteSeen = true;
493
+ }
494
+ else if (chr === ':' && routeEnd < 0 && firstChar === '@' && !quoteSeen) {
495
+ routeEnd = result.length + 1;
301
496
  }
302
497
  else if (chr === '[') {
303
498
  closer = ']';
@@ -306,8 +501,27 @@ function _stripAddressComments(address) {
306
501
  depth = 1;
307
502
  continue;
308
503
  }
504
+ else if (chr === '<') {
505
+ // Apple Mail drops everything between an unexpected '<' and the address after it.
506
+ // Read here rather than off the raw brackets so that a '<' inside a quoted string,
507
+ // a comment or a domain literal stays where it is: '<"a<b"@victim.com>' cut at
508
+ // the first '<' left the address 'b"@victim.com'
509
+ result = '';
510
+ lastChar = '';
511
+ comments.length = 0;
512
+ firstChar = '';
513
+ quoteSeen = false;
514
+ routeEnd = -1;
515
+ continue;
516
+ }
309
517
  result += chr;
310
518
  lastChar = chr;
519
+ if (!firstChar && !_isSpaceCode(address.charCodeAt(i))) {
520
+ firstChar = chr;
521
+ }
522
+ }
523
+ if (routeEnd >= 0 && result.indexOf('@', routeEnd) >= 0) {
524
+ result = result.slice(routeEnd);
311
525
  }
312
526
  if (depth) {
313
527
  // an unterminated comment runs to the end of the address
@@ -316,36 +530,41 @@ function _stripAddressComments(address) {
316
530
  return { address: result.trim(), comments: comments.filter(text => text) };
317
531
  }
318
532
  /**
319
- * Converts tokens for a single address into an address object
533
+ * Walks the tokens of one entry of an address list and sorts them into the runs of display
534
+ * text, angle-addr and comment they belong to
320
535
  *
321
- * @param tokens Tokens object
322
- * @param depth Current recursion depth for nested group protection
323
- * @return Address object
536
+ * @param tokens Tokens of the whole address list
537
+ * @param from Index of the first token of the entry
538
+ * @param to Index just past the last token of the entry
539
+ * @return The collected runs, and whether the display text came out of a comment
324
540
  */
325
- function _handleAddress(tokens, depth) {
326
- let isGroup = false;
541
+ function _collectAddressParts(tokens, from, to) {
327
542
  let state = 'text';
328
- const addresses = [];
329
543
  const data = {
330
544
  address: [],
331
545
  comment: [],
332
- group: [],
333
546
  text: [],
334
- textWasQuoted: []
547
+ textWasQuoted: [],
548
+ quotedLocalAt: []
335
549
  };
336
550
  let insideQuotes = false;
551
+ // whether the last text run holds nothing but quoted text so far, which is what a quoted
552
+ // local part is until the '@' after it joins the run
553
+ let lastTextPureQuoted = false;
337
554
  // Last character of the run each state is currently accumulating. Reading it back off
338
555
  // the accumulator with slice(-1) makes the engine flatten the whole growing string on
339
556
  // every token, which is quadratic over an address built from many comment-joined atoms
340
557
  // (GHSA-prgh-xp8r-p3m5). A run only ever grows by the token appended below, so the
341
558
  // character is carried along instead of re-read.
342
- const lastChars = { address: '', comment: '', group: '', text: '' };
559
+ const lastChars = { address: '', comment: '', text: '' };
343
560
  // 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) {
561
+ for (let i = from; i < to; i++) {
562
+ const value = tokens.value(i);
563
+ // the operator in front of this token, if there is one, and whether it runs on into it
564
+ const prevOperator = i > from ? tokens.operatorAt(i - 1) : '';
565
+ const prevNoBreak = i > from && tokens.noBreak(i - 1);
566
+ if (tokens.isOperator(i)) {
567
+ switch (value) {
349
568
  case '<':
350
569
  state = 'address';
351
570
  insideQuotes = false;
@@ -354,11 +573,6 @@ function _handleAddress(tokens, depth) {
354
573
  state = 'comment';
355
574
  insideQuotes = false;
356
575
  break;
357
- case ':':
358
- state = 'group';
359
- isGroup = true;
360
- insideQuotes = false;
361
- break;
362
576
  case '"':
363
577
  insideQuotes = !insideQuotes;
364
578
  state = 'text';
@@ -369,7 +583,7 @@ function _handleAddress(tokens, depth) {
369
583
  break;
370
584
  }
371
585
  }
372
- else if (token.value) {
586
+ else {
373
587
  // An empty quoted string is dropped by the tokenizer, leaving no text token of its
374
588
  // own for textWasQuoted to be recorded on, so the pair of quote operators right in
375
589
  // front of this token is all that is left of it and the run it opens carries the
@@ -377,181 +591,200 @@ function _handleAddress(tokens, depth) {
377
591
  // the quotes never go back on, and the value is no longer an addr-spec a trailing
378
592
  // comment can be peeled off of. It only ever opens a run: a run that already holds
379
593
  // 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
- }
594
+ const opensAfterEmptyQuotedString = prevOperator === '"' && prevNoBreak && i > from + 1 && tokens.operatorAt(i - 2) === '"';
391
595
  // A comment is folding whitespace. It may sit inside an addr-spec, on either side
392
596
  // of the '@', but it cannot join two atoms into one: gluing across it would read
393
597
  // 'user@example.com(x)evil.com' as the single domain 'example.comevil.com' and
394
598
  // deliver to a domain the sender never named.
395
599
  const parts = data[state];
396
- const joins = prevToken &&
397
- prevToken.noBreak &&
398
- parts.length &&
399
- (prevToken.value !== ')' || lastChars[state] === '@' || token.value.charAt(0) === '@');
600
+ const joins = prevNoBreak && parts.length && (prevOperator !== ')' || lastChars[state] === '@' || value.charAt(0) === '@');
400
601
  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);
602
+ const last = data[state].length - 1;
603
+ if (state === 'text' && !insideQuotes) {
604
+ if (lastTextPureQuoted && value.charAt(0) === '@') {
605
+ data.quotedLocalAt[last] = data.text[last].length;
606
+ }
607
+ lastTextPureQuoted = false;
404
608
  }
609
+ data[state][last] += value;
610
+ lastChars[state] = value.charAt(value.length - 1);
405
611
  if (state === 'text' && insideQuotes) {
406
- data.textWasQuoted[data.textWasQuoted.length - 1] = true;
612
+ data.textWasQuoted[last] = true;
407
613
  }
408
614
  }
409
615
  else {
410
- data[state].push(token.value);
411
- lastChars[state] = token.value.charAt(token.value.length - 1);
616
+ data[state].push(value);
617
+ lastChars[state] = value.charAt(value.length - 1);
412
618
  if (state === 'text') {
413
619
  data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
620
+ // an empty quoted string leaves no text of its own, so the '@' opens the run
621
+ data.quotedLocalAt.push(opensAfterEmptyQuotedString && value.charAt(0) === '@' ? 0 : -1);
622
+ lastTextPureQuoted = insideQuotes;
414
623
  }
415
624
  }
416
625
  }
417
626
  }
418
- // If there is no text but a comment, replace the two
627
+ // If there is no text but a comment, replace the two. A comment is never part of a
628
+ // mailbox, so text that came out of one only ever names it: reading an address out of it
629
+ // made '(a@evil.com)' a recipient where no conformant reading has a mailbox at all
630
+ let textFromComment = false;
419
631
  if (!data.text.length && data.comment.length) {
420
632
  data.text = data.comment;
421
633
  data.comment = [];
634
+ textFromComment = true;
422
635
  }
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
- });
636
+ return { data, textFromComment };
637
+ }
638
+ /**
639
+ * Name of a group, from the tokens in front of its ':'. A comment names it when no text does,
640
+ * as it names a mailbox.
641
+ *
642
+ * @param tokens Tokens of the whole address list
643
+ * @param from Index of the first token in front of the ':'
644
+ * @param to Index of the ':'
645
+ * @return Name of the group
646
+ */
647
+ function _groupName(tokens, from, to) {
648
+ // http://tools.ietf.org/html/rfc2822#appendix-A.1.3
649
+ return _collectAddressParts(tokens, from, to).data.text.join(' ');
650
+ }
651
+ /**
652
+ * Converts tokens for a single address into an address object
653
+ *
654
+ * @param tokens Tokens of the whole address list
655
+ * @param from Index of the first token of this address
656
+ * @param to Index just past the last token of this address
657
+ * @return Address object
658
+ */
659
+ function _handleAddress(tokens, from, to) {
660
+ const collected = _collectAddressParts(tokens, from, to);
661
+ const data = collected.data;
662
+ let textFromComment = collected.textFromComment;
663
+ // Comments come out of the angle-addr before anything asks whether one was found, so
664
+ // that brackets holding nothing but a comment read the same as empty ones
665
+ const addressComments = [];
666
+ const addressParts = [];
667
+ for (const part of data.address) {
668
+ const stripped = _stripAddressComments(part);
669
+ for (const comment of stripped.comments) {
670
+ addressComments.push(comment);
671
+ }
672
+ if (stripped.address) {
673
+ addressParts.push(stripped.address);
438
674
  }
439
- addresses.push({
440
- name: data.text || '',
441
- group: groupMembers
442
- });
443
675
  }
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);
676
+ data.address = addressParts;
677
+ // If no address was found, try to detect one from regular text
678
+ if (!data.address.length && data.text.length && !textFromComment) {
679
+ // Whitespace beside the '@' is CFWS, so 'user @ good.com' is read as one run. Both
680
+ // tiers read the collapsed runs, and only a run an address comes out of is replaced
681
+ // by its collapsed form, the display text keeps its spacing
682
+ const candidates = data.text.map((part) => _collapseAtSpacing(part));
683
+ // The first addr-spec in the text is the mailbox, as it is for a conformant parser
684
+ // reading as far as it can. Taking the last one let a comment part two of them and
685
+ // pick the second, 'user@good.com (x) evil@evil.com' went to evil.com, while the
686
+ // same two without the comment, one run, went to good.com
687
+ for (let i = 0; i < data.text.length; i++) {
688
+ // Security: Do not extract email addresses from quoted strings.
689
+ // RFC 5321 allows @ inside quoted local-parts like "user@domain"@example.com.
690
+ // Extracting emails from quoted text leads to misrouting vulnerabilities.
691
+ // A run that is a quoted local part with its '@' and domain is an addr-spec of
692
+ // its own, with the quoting put back on, and takes its turn like any other
693
+ const local = data.quotedLocalAt[i];
694
+ const quotedLocal = data.textWasQuoted[i] && local >= 0 && QUOTED_LOCAL_DOMAIN.test(data.text[i].slice(local))
695
+ ? _quoteLocalPart(data.text[i])
696
+ : '';
697
+ if (quotedLocal || (!data.textWasQuoted[i] && _isAddrSpec(candidates[i]))) {
698
+ data.address = [quotedLocal || candidates[i]];
699
+ data.text.splice(i, 1);
700
+ data.textWasQuoted.splice(i, 1);
701
+ data.quotedLocalAt.splice(i, 1);
702
+ break;
460
703
  }
461
704
  }
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;
705
+ // Try a looser regex match if strict match found nothing
706
+ if (!data.address.length) {
707
+ let extracted = false;
708
+ for (let i = 0; i < data.text.length; i++) {
709
+ // Security: Do not extract email addresses from quoted strings
710
+ if (!data.textWasQuoted[i]) {
711
+ // the strict tier found nothing, so it spliced nothing and the indexes still line up
712
+ const part = candidates[i];
713
+ let remainder = data.text[i];
714
+ const at = _looseAddressStart(part);
715
+ if (at >= 0) {
716
+ LOOSE_TEXT_ADDR.lastIndex = at;
717
+ const match = LOOSE_TEXT_ADDR.exec(part);
718
+ if (match) {
719
+ data.address = [match[0].trim()];
720
+ extracted = true;
721
+ remainder = part.slice(0, at) + ' ' + part.slice(at + match[0].length);
496
722
  }
497
723
  }
724
+ data.text[i] = remainder.trim();
725
+ if (extracted) {
726
+ break;
727
+ }
498
728
  }
499
729
  }
500
730
  }
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
- }
731
+ }
732
+ // If there's still no text but a comment exists, replace the two
733
+ if (!data.text.length && data.comment.length) {
734
+ data.text = data.comment;
735
+ data.comment = [];
736
+ }
737
+ // Keep only the first address occurrence, push others to regular text
738
+ if (data.address.length > 1) {
739
+ data.text = data.text.concat(data.address.splice(1));
740
+ }
741
+ // An address is only taken from unquoted text, so anything left in the text at this
742
+ // point that still has to serve as the address carries its quoting in this flag
743
+ const addressFromQuotedText = !data.address.length && data.textWasQuoted.some(wasQuoted => wasQuoted);
744
+ // Join values with spaces
745
+ data.text = data.text.join(' ');
746
+ data.address = data.address.join(' ');
747
+ if (addressFromQuotedText && data.text.indexOf('@') >= 0) {
748
+ // A quoted run with no '@' in it is a display name and nothing else, so it stays
749
+ // where it is. Moving it over anyway made a comment the mailbox was read from:
750
+ // '"Display Name" <(a comment)>' handed on 'Display Name' as the address with the
751
+ // comment as the name, and a quoted name holding a ',' or a ';' became an address
752
+ // that reads as two recipients once a consumer writes it back into a header.
753
+ //
754
+ // The mailbox is still sitting in the text, so it moves over here and is quoted
755
+ // before the recovery below rather than after it. Anything else the text holds
756
+ // came along with it: a comment ends the domain but leaves the atoms behind it in
757
+ // the same text, and '"user"@example.com(x)evil.com' was handed on as the address
758
+ // 'user@example.com evil.com', a second domain riding into the envelope recipient
759
+ // on a value that is no addr-spec at all (GHSA-g57g-f23g-4646). Putting the quotes
760
+ // back first is what lets the recovery tell the whitespace an addr-spec may carry
761
+ // from the wreckage trailing one, as only a quoted local part may hold whitespace
762
+ // The quotes are gone at this point, so the '@' the domain splits off at is the
763
+ // last one, not the single one _collapseAtSpacing insists on
764
+ data.address = _quoteLocalPart(_joinAtSpacing(data.text, _domainAt(data.text)));
765
+ data.text = '';
766
+ }
767
+ _recoverAddrSpec(data);
768
+ // a comment names the mailbox only when nothing else does, as one outside the brackets
769
+ if (!data.text && addressComments.length) {
770
+ data.text = addressComments.join(' ');
771
+ textFromComment = true;
772
+ }
773
+ const address = {
774
+ address: data.address || (textFromComment ? '' : data.text) || '',
775
+ name: data.text || data.address || ''
776
+ };
777
+ if (address.address === address.name) {
778
+ if (/@/.test(address.address || '')) {
779
+ address.name = '';
780
+ }
781
+ else {
782
+ address.address = '';
550
783
  }
551
- addresses.push(address);
552
784
  }
553
- return addresses;
785
+ return address;
554
786
  }
787
+ /* eslint-disable no-bitwise -- see TokenList */
555
788
  /**
556
789
  * Creates a Tokenizer object for tokenizing address field strings
557
790
  *
@@ -563,11 +796,16 @@ class Tokenizer {
563
796
  this.str = (str || '').toString();
564
797
  this.operatorCurrent = '';
565
798
  this.operatorExpecting = '';
566
- this.node = null;
799
+ this.node = -1;
567
800
  this.escaped = false;
568
801
  this.inDomainLiteral = false;
569
802
  this.afterAt = false;
570
- this.list = [];
803
+ this.inGroup = false;
804
+ this.innerQuoted = false;
805
+ this.innerComment = 0;
806
+ this.innerLiteral = false;
807
+ this.innerEscaped = false;
808
+ this.list = new TokenList(this.str);
571
809
  /**
572
810
  * Operator tokens and which tokens are expected to end the sequence
573
811
  */
@@ -576,7 +814,9 @@ class Tokenizer {
576
814
  '(': ')',
577
815
  '<': '>',
578
816
  ',': '',
579
- ':': ';',
817
+ // A group is read in the same pass as the rest of the list, so its ':' only marks
818
+ // where the name ends and its members are tokenized like any other address
819
+ ':': '',
580
820
  // Semicolons are not a legal delimiter per the RFC2822 grammar other
581
821
  // than for terminating a group, but they are also not valid for any
582
822
  // other use in this context. Given that some mail clients have
@@ -592,27 +832,96 @@ class Tokenizer {
592
832
  * @return An array of operator|text tokens
593
833
  */
594
834
  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
- });
835
+ const str = this.str;
836
+ const len = str.length;
837
+ for (let i = 0; i < len; i++) {
838
+ this.checkChar(str.charAt(i), i);
839
+ }
840
+ this._closeText(len);
841
+ // Compacted in place: a text run is trimmed to its value and one left empty is
842
+ // dropped, and the value of a run that is no plain slice is rebuilt and kept aside
843
+ const list = this.list;
844
+ const slots = list.slots;
845
+ let kept = 0;
846
+ for (let i = 0, count = list.length; i < count; i++) {
847
+ const kind = slots[i * 3];
848
+ let start = slots[i * 3 + 1];
849
+ let end = slots[i * 3 + 2];
850
+ if (kind & TOKEN_DIRTY) {
851
+ const value = this._cleanValue(start, end, kind).trim();
852
+ if (!value) {
853
+ continue;
854
+ }
855
+ list.cleaned.set(kept, value);
856
+ }
857
+ else if (!(kind & TOKEN_OPERATOR)) {
858
+ while (start < end && _isSpaceCode(str.charCodeAt(start))) {
859
+ start++;
860
+ }
861
+ while (end > start && _isSpaceCode(str.charCodeAt(end - 1))) {
862
+ end--;
863
+ }
864
+ if (start === end) {
865
+ continue;
866
+ }
867
+ }
868
+ slots[kept * 3] = kind;
869
+ slots[kept * 3 + 1] = start;
870
+ slots[kept * 3 + 2] = end;
871
+ kept++;
872
+ }
873
+ list.length = kept;
607
874
  return list;
608
875
  }
876
+ /**
877
+ * Ends the text run being collected, if there is one
878
+ *
879
+ * @param end Offset just past its last character
880
+ */
881
+ _closeText(end) {
882
+ if (this.node >= 0) {
883
+ this.list.slots[this.node * 3 + 2] = end;
884
+ this.node = -1;
885
+ }
886
+ }
887
+ /**
888
+ * Rebuilds the value of a text run that is no plain slice of the field, applying the
889
+ * rules checkChar collects characters by: an escaping backslash is dropped, a line break
890
+ * reads as a space and any other control character is left out
891
+ *
892
+ * @param start Offset of the first character of the run
893
+ * @param end Offset just past its last character
894
+ * @param kind Kind and flags of the run
895
+ * @return Value of the run
896
+ */
897
+ _cleanValue(start, end, kind) {
898
+ const str = this.str;
899
+ const escapes = (kind & TOKEN_ESCAPES) !== 0;
900
+ let escaped = (kind & TOKEN_STARTS_ESCAPED) !== 0;
901
+ let value = '';
902
+ for (let i = start; i < end; i++) {
903
+ const chr = str.charAt(i);
904
+ if (escapes && !escaped && chr === '\\') {
905
+ escaped = true;
906
+ continue;
907
+ }
908
+ escaped = false;
909
+ if (chr === '\n') {
910
+ value += ' ';
911
+ }
912
+ else if (chr.charCodeAt(0) >= 0x21 || chr === ' ' || chr === '\t') {
913
+ value += chr;
914
+ }
915
+ }
916
+ return value;
917
+ }
609
918
  /**
610
919
  * Checks if a character is an operator or text and acts accordingly
611
920
  *
612
921
  * @param chr Character from the address field
613
- * @param nextChr Character following chr, null at the end of the field
922
+ * @param i Offset of the character in the field
614
923
  */
615
- checkChar(chr, nextChr) {
924
+ checkChar(chr, i) {
616
925
  // Track RFC 5322 domain-literals ("[" *dtext "]"). The ":" of an IPv6
617
926
  // address-literal (user@[IPv6:2001:db8::1]) is dtext and must not be treated as
618
927
  // the group delimiter while inside the brackets. That is the only operator the
@@ -633,39 +942,67 @@ class Tokenizer {
633
942
  this.inDomainLiteral = false;
634
943
  }
635
944
  }
945
+ // The closer of an angle-addr is only read outside the quoted strings, comments and
946
+ // domain literals it holds. Looking for nothing but the closer let a '>' inside a
947
+ // quoted local part end the angle-addr, '<"a@evil.com>"@good.com>' was read as an
948
+ // address at evil.com. The characters are still collected as they are, the angle-addr
949
+ // is read again later
950
+ let nested = false;
951
+ if (this.operatorExpecting === '>') {
952
+ nested = this._trackInner(chr);
953
+ if (!nested && chr === ';' && this.inGroup) {
954
+ // The ';' of a group ends an angle-addr left open inside it, as it always did
955
+ // when a group was cut out of the field at its ';' and parsed on its own. Read
956
+ // as text instead, an unclosed '<' swallowed the end of the group and every
957
+ // recipient after it
958
+ this._closeText(i);
959
+ this.operatorExpecting = '';
960
+ this._resetInner();
961
+ }
962
+ }
963
+ else if (this.operatorExpecting === ')' && !this.escaped) {
964
+ // Comments nest (RFC 5322 3.2.2). Ending one at the first ')' let the ')' of a
965
+ // nested comment close the outer one and released the rest of it, an address
966
+ // included, into the header
967
+ if (chr === '(') {
968
+ this.innerComment++;
969
+ }
970
+ else if (chr === ')' && this.innerComment) {
971
+ this.innerComment--;
972
+ nested = true;
973
+ }
974
+ }
636
975
  if (this.escaped) {
637
976
  // ignore next condition blocks
638
977
  }
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;
978
+ else if (chr === this.operatorExpecting && !nested) {
979
+ const nextChr = this.str.charAt(i + 1);
980
+ this._closeText(i);
981
+ this.list.push(nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr) ? TOKEN_OPERATOR | TOKEN_NO_BREAK : TOKEN_OPERATOR, i, i + 1);
649
982
  if (chr !== ')') {
650
983
  // a comment is folding whitespace, it does not part the "@" from the "["
651
984
  this.afterAt = false;
652
985
  }
653
986
  this.operatorExpecting = '';
654
987
  this.escaped = false;
988
+ this._resetInner();
655
989
  return;
656
990
  }
657
991
  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;
992
+ this._closeText(i);
993
+ this.list.push(TOKEN_OPERATOR, i, i + 1);
994
+ if (chr === ':') {
995
+ this.inGroup = true;
996
+ }
997
+ else if (chr === ';') {
998
+ this.inGroup = false;
999
+ }
664
1000
  if (chr !== '(') {
665
1001
  this.afterAt = false;
666
1002
  }
667
1003
  this.operatorExpecting = this.operators[chr];
668
1004
  this.escaped = false;
1005
+ this._resetInner();
669
1006
  return;
670
1007
  }
671
1008
  else if (['"', "'", ')'].includes(this.operatorExpecting) && chr === '\\') {
@@ -674,104 +1011,195 @@ class Tokenizer {
674
1011
  // only in quoted strings let an escaped parenthesis close the comment, which
675
1012
  // released the rest of it, an address included, into the header
676
1013
  this.escaped = true;
1014
+ if (this.node >= 0) {
1015
+ // the backslash is left out of the value
1016
+ this.list.slots[this.node * 3] |= TOKEN_DIRTY;
1017
+ }
677
1018
  return;
678
1019
  }
679
- if (!this.node) {
680
- this.node = {
681
- type: 'text',
682
- value: ''
683
- };
684
- this.list.push(this.node);
1020
+ if (this.node < 0) {
1021
+ this.node = this.list.push((this.operatorExpecting === '"' || this.operatorExpecting === ')' ? TOKEN_ESCAPES : 0) |
1022
+ (this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
685
1023
  }
686
1024
  if (chr === '\n') {
687
1025
  // Convert newlines to spaces. Carriage return is ignored as \r and \n usually
688
1026
  // go together anyway and there already is a WS for \n. Lone \r means something is fishy.
689
1027
  chr = ' ';
1028
+ this.list.slots[this.node * 3] |= TOKEN_DIRTY;
690
1029
  }
691
- if (chr.charCodeAt(0) >= 0x21 || [' ', '\t'].includes(chr)) {
692
- // skip command bytes
693
- this.node.value += chr;
1030
+ if (chr.charCodeAt(0) >= 0x21 || chr === ' ' || chr === '\t') {
1031
+ // the character is part of the value, which is read off the field later
694
1032
  // text inside a quoted string or a comment is not part of an addr-spec. Tracked
695
1033
  // as it goes rather than read back off the value (see lastChars in _handleAddress)
696
1034
  if (!this.operatorExpecting && chr !== ' ' && chr !== '\t') {
697
1035
  this.afterAt = chr === '@';
698
1036
  }
699
1037
  }
1038
+ else {
1039
+ // skip command bytes
1040
+ this.list.slots[this.node * 3] |= TOKEN_DIRTY;
1041
+ }
700
1042
  this.escaped = false;
701
1043
  }
1044
+ /**
1045
+ * Tracks the quoted strings, comments and domain literals inside an angle-addr, which its
1046
+ * closer must not be read from. A backslash escapes the next character inside a quoted
1047
+ * string and a comment. A domain literal opens right after an "@" and ends at the "]".
1048
+ * dtext holds the list separators as well, and inside the brackets they never split the
1049
+ * list anyway.
1050
+ *
1051
+ * @param chr Character from the address field
1052
+ * @return true while the character sits inside one of them, the one that closes it included
1053
+ */
1054
+ _trackInner(chr) {
1055
+ if (this.innerEscaped) {
1056
+ this.innerEscaped = false;
1057
+ return true;
1058
+ }
1059
+ if (this.innerQuoted) {
1060
+ if (chr === '\\') {
1061
+ this.innerEscaped = true;
1062
+ }
1063
+ else if (chr === '"') {
1064
+ this.innerQuoted = false;
1065
+ }
1066
+ return true;
1067
+ }
1068
+ if (this.innerComment) {
1069
+ if (chr === '\\') {
1070
+ this.innerEscaped = true;
1071
+ }
1072
+ else if (chr === '(') {
1073
+ this.innerComment++;
1074
+ }
1075
+ else if (chr === ')') {
1076
+ this.innerComment--;
1077
+ }
1078
+ return true;
1079
+ }
1080
+ if (this.innerLiteral) {
1081
+ if (chr === ']') {
1082
+ this.innerLiteral = false;
1083
+ }
1084
+ return true;
1085
+ }
1086
+ if (chr === '"') {
1087
+ this.innerQuoted = true;
1088
+ return true;
1089
+ }
1090
+ if (chr === '(') {
1091
+ this.innerComment = 1;
1092
+ return true;
1093
+ }
1094
+ if (chr === '[' && this.afterAt) {
1095
+ this.innerLiteral = true;
1096
+ return true;
1097
+ }
1098
+ // afterAt is otherwise only kept outside the operators and is cleared when one opens
1099
+ // or ends, so inside an angle-addr it serves the literal tracked here
1100
+ if (!_isSpaceCode(chr.charCodeAt(0))) {
1101
+ this.afterAt = chr === '@';
1102
+ }
1103
+ return false;
1104
+ }
1105
+ /**
1106
+ * Forgets the nesting collected for the operator that just opened or ended
1107
+ */
1108
+ _resetInner() {
1109
+ this.innerQuoted = false;
1110
+ this.innerComment = 0;
1111
+ this.innerLiteral = false;
1112
+ this.innerEscaped = false;
1113
+ }
702
1114
  }
1115
+ /* eslint-enable no-bitwise */
703
1116
  /**
704
- * Maximum recursion depth for parsing nested groups.
705
- * RFC 5322 doesn't allow nested groups, so this is a safeguard against
706
- * malicious input that could cause stack overflow.
1117
+ * Folds the fragments an unquoted display name holding a comma is split into back together.
1118
+ *
1119
+ * "Joe Foo, PhD <joe@example.com>" is split on the comma into
1120
+ * [{name:"Joe Foo", address:""}, {name:"PhD", address:"joe@example.com"}]. A name-only entry
1121
+ * followed by an entry with both name and address is recombined. Walked back to front so
1122
+ * that a run of fragments folds into one entry in a single pass. Splicing each fragment out
1123
+ * of the list instead would cost O(n^2).
1124
+ *
1125
+ * @param list Parsed entries, in order
1126
+ * @return The entries with the fragments folded in
707
1127
  */
708
- const MAX_NESTED_GROUP_DEPTH = 50;
1128
+ function _mergeNameFragments(list) {
1129
+ const merged = [];
1130
+ for (let i = list.length - 1; i >= 0; i--) {
1131
+ const current = list[i];
1132
+ const next = merged.length ? merged[merged.length - 1] : null;
1133
+ if (next && current.address === '' && current.name && !current.group && next.address && next.name) {
1134
+ next.name = current.name + ', ' + next.name;
1135
+ }
1136
+ else {
1137
+ merged.push(current);
1138
+ }
1139
+ }
1140
+ return merged.reverse();
1141
+ }
709
1142
  /**
710
- * Parses an address list, recursing into the groups it holds.
1143
+ * Parses an address list, groups included, in a single pass over its tokens.
711
1144
  *
712
- * The depth is threaded through the calls rather than carried in an options object, so a
713
- * caller supplied one can not seed it and lift the recursion limit: `{ _depth: -1e9 }`
714
- * bought a billion levels of nesting and turned the guard below into the stack overflow it
715
- * is there to prevent.
1145
+ * A group used to be cut out of the field at the ';' that ends it and parsed again as an
1146
+ * address list of its own, recursively, so every level of nesting parsed what was left of
1147
+ * the field once more and the depth had to be capped. RFC 5322 does not nest groups at all,
1148
+ * and the nested ones were flattened anyway. The tokens are now walked once: a ':' opens a
1149
+ * group, its members are read as they come, a ';' closes it. A ':' inside an open group
1150
+ * stands for a group nested in it, whose name is dropped and whose members belong to the
1151
+ * outer one, which is how they were flattened before. The first ';' ends the group, nested
1152
+ * or not, as it did.
716
1153
  *
717
1154
  * @param str Address field
718
- * @param depth Current recursion depth for nested group protection
719
1155
  * @return An array of address objects
720
1156
  */
721
- function _parseAddressList(str, depth) {
722
- // Prevent stack overflow from deeply nested groups (DoS protection)
723
- if (depth > MAX_NESTED_GROUP_DEPTH) {
724
- return [];
725
- }
726
- const tokenizer = new Tokenizer(str);
727
- const tokens = tokenizer.tokenize();
728
- const addresses = [];
729
- let address = [];
1157
+ function _parseAddressList(str) {
1158
+ const tokens = new Tokenizer(str).tokenize();
730
1159
  const parsedAddresses = [];
731
- tokens.forEach(token => {
732
- if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
733
- if (address.length) {
734
- addresses.push(address);
1160
+ let group = null;
1161
+ let members = [];
1162
+ // Every address is handed over as the range of the token list it covers, as soon as the
1163
+ // separator after it is seen. Copying the tokens of each address into an array of its own,
1164
+ // and collecting those before handling any, held a second structure as large as the token
1165
+ // list for the whole parse, and an array that receives a single push is allocated room for
1166
+ // seventeen: a list of one letter names took some 80 times its size in that alone
1167
+ let from = 0;
1168
+ for (let i = 0, len = tokens.length; i <= len; i++) {
1169
+ const operator = i < len ? tokens.operatorAt(i) : '';
1170
+ if (i < len && operator !== ',' && operator !== ';' && operator !== ':') {
1171
+ continue;
1172
+ }
1173
+ if (operator === ':') {
1174
+ if (!group) {
1175
+ group = { name: _groupName(tokens, from, i), group: [] };
1176
+ members = [];
735
1177
  }
736
- address = [];
1178
+ from = i + 1;
1179
+ continue;
737
1180
  }
738
- else {
739
- address.push(token);
740
- }
741
- });
742
- if (address.length) {
743
- addresses.push(address);
744
- }
745
- addresses.forEach(addr => {
746
- const handled = _handleAddress(addr, depth);
747
- // Appended in place. Rebuilding the accumulator with concat() would copy every
748
- // entry collected so far on each address, making a flat list cost O(n^2).
749
- for (let i = 0; i < handled.length; i++) {
750
- parsedAddresses.push(handled[i]);
751
- }
752
- });
753
- // Merge fragments produced when unquoted display names contain commas.
754
- // "Joe Foo, PhD <joe@example.com>" is split on the comma into
755
- // [{name:"Joe Foo", address:""}, {name:"PhD", address:"joe@example.com"}].
756
- // Recombine: a name-only entry followed by an entry with both name and address.
757
- // Walked back to front so that a run of fragments folds into one entry in a single
758
- // pass. Splicing each fragment out of the list instead would cost O(n^2).
759
- const mergedAddresses = [];
760
- for (let i = parsedAddresses.length - 1; i >= 0; i--) {
761
- const current = parsedAddresses[i];
762
- const next = mergedAddresses.length ? mergedAddresses[mergedAddresses.length - 1] : null;
763
- if (next && current.address === '' && current.name && !current.group && next.address && next.name) {
764
- next.name = current.name + ', ' + next.name;
1181
+ if (i > from) {
1182
+ // Appended in place. Rebuilding the accumulator with concat() would copy every
1183
+ // entry collected so far on each address, making a flat list cost O(n^2).
1184
+ const address = _handleAddress(tokens, from, i);
1185
+ if (group) {
1186
+ members.push(address);
1187
+ }
1188
+ else {
1189
+ parsedAddresses.push(address);
1190
+ }
765
1191
  }
766
- else {
767
- mergedAddresses.push(current);
1192
+ from = i + 1;
1193
+ if (group && (operator === ';' || i === len)) {
1194
+ group.group = _mergeNameFragments(members);
1195
+ parsedAddresses.push(group);
1196
+ group = null;
768
1197
  }
769
1198
  }
770
- mergedAddresses.reverse();
771
- return mergedAddresses;
1199
+ return _mergeNameFragments(parsedAddresses);
772
1200
  }
773
1201
  function addressparser(str, options) {
774
- const parsedAddresses = _parseAddressList(str, 0);
1202
+ const parsedAddresses = _parseAddressList(str);
775
1203
  if (options?.flatten) {
776
1204
  const flatAddresses = [];
777
1205
  const walkAddressList = (list) => {