@agentcomms/resend 0.26.0 → 0.26.1

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.
@@ -1,4 +1,4 @@
1
- import { C as unknown, S as union, b as strictObject, c as boolean, g as object, h as number, i as _enum, l as discriminatedUnion, m as looseObject, n as datetime, p as literal$1, s as array, x as string, y as record } from "./iso-USuVU_sk.mjs";
1
+ import { C as unknown, S as union, b as strictObject, c as boolean, g as object, h as number, i as _enum, l as discriminatedUnion, m as looseObject, n as datetime, p as literal$1, s as array, x as string, y as record } from "./iso-5-9R47LC.mjs";
2
2
  import { access, chmod, constants, link, lstat, mkdir, open, readFile, readdir, readlink, realpath, rename, rm, stat, unlink, utimes } from "node:fs/promises";
3
3
  import path, { basename, delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
4
4
  import { domainToASCII, fileURLToPath, pathToFileURL } from "node:url";
@@ -13,7 +13,7 @@ import { gunzipSync } from "node:zlib";
13
13
  //#endregion
14
14
  //#region src/version.ts
15
15
  /** The package version, read from package.json at build time. */
16
- const VERSION$2 = "0.26.0";
16
+ const VERSION$2 = "0.26.1";
17
17
  //#endregion
18
18
  //#region src/api/routes.ts
19
19
  /**
@@ -202,7 +202,7 @@ const RESEND_CALLER = Object.freeze({
202
202
  packageName: PACKAGE_NAME
203
203
  });
204
204
  //#endregion
205
- //#region ../core/dist/update-check-C3j1VxDg.mjs
205
+ //#region ../core/dist/update-check-CfybSI56.mjs
206
206
  /**
207
207
  * Every failure a user or an agent can act on carries a specific, stable code from this registry, a message, an
208
208
  * optional one-line hint and structured `details`. The code decides the process exit status; agents and skills branch
@@ -5581,8 +5581,8 @@ async function mergeIntoJsonConfig(path, client, name, entry) {
5581
5581
  }
5582
5582
  /** Starts the server exactly as a client would, and completes the handshake. */
5583
5583
  async function verifyEntry(entry, product) {
5584
- const { Client } = await import("./dist-C7DG2LdL-l0mj9_dH.mjs");
5585
- const { StdioClientTransport } = await import("./stdio-CyjIgrch-59zcjSEt.mjs");
5584
+ const { Client } = await import("./dist-C7DG2LdL-6QTVBgXp.mjs");
5585
+ const { StdioClientTransport } = await import("./stdio-CRbkOCqe-BpzkpnG6.mjs");
5586
5586
  const client = new Client({
5587
5587
  name: `${product.binary}-install-check`,
5588
5588
  version: product.version
@@ -12996,7 +12996,7 @@ function folding(platform) {
12996
12996
  return platform === "win32" ? (text) => text.toLowerCase() : (text) => text;
12997
12997
  }
12998
12998
  /** The package version, read from package.json at build time. */
12999
- const VERSION = "0.26.0";
12999
+ const VERSION = "0.26.1";
13000
13000
  /**
13001
13001
  * The command a person is told to run for one of this suite's own CLIs (design 2026-10-04, D1 and D5; CUE-403).
13002
13002
  *
@@ -17191,7 +17191,7 @@ function serverPruneChange(core, env, request, own) {
17191
17191
  };
17192
17192
  const { packageName, version } = ownProduct(request.channel, own) ?? {
17193
17193
  packageName: serverFacts(request.channel).packageName,
17194
- version: "0.26.0"
17194
+ version: "0.26.1"
17195
17195
  };
17196
17196
  const product = {
17197
17197
  packageName,
@@ -19339,7 +19339,7 @@ function terminalUpdateHooks(core, env, options = {}) {
19339
19339
  } };
19340
19340
  }
19341
19341
  //#endregion
19342
- //#region ../core/dist/tool-arguments-C8Oy4uOx.mjs
19342
+ //#region ../core/dist/tool-arguments-CJ4RQpx4.mjs
19343
19343
  const APPLIED = Symbol.for("agentcomms.strictToolArguments");
19344
19344
  const TARGET = "draft-2020-12";
19345
19345
  /**
@@ -19728,7 +19728,7 @@ function spoken(words, joiner) {
19728
19728
  return `${words.slice(0, -1).join(", ")} ${joiner} ${words.at(-1)}`;
19729
19729
  }
19730
19730
  //#endregion
19731
- //#region ../../node_modules/.pnpm/postal-mime@4.0.5/node_modules/postal-mime/dist/esm/decode-strings.js
19731
+ //#region ../../node_modules/.pnpm/postal-mime@4.1.4/node_modules/postal-mime/dist/esm/decode-strings.js
19732
19732
  const textEncoder = new TextEncoder();
19733
19733
  const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
19734
19734
  const base64Lookup = /* @__PURE__ */ new Uint8Array(256);
@@ -19827,20 +19827,51 @@ const ENCODING_LABELS = new Set([
19827
19827
  "cseuckr csksc56011987 euc-kr iso-ir-149 korean ks_c_5601-1987 ks_c_5601-1989 ksc5601 ksc_5601",
19828
19828
  "windows-949",
19829
19829
  "csiso2022kr hz-gb-2312 iso-2022-cn iso-2022-cn-ext iso-2022-kr replacement",
19830
- "unicodefffe utf-16be",
19831
- "csunicode iso-10646-ucs-2 ucs-2 unicode unicodefeff utf-16 utf-16le",
19832
19830
  "x-user-defined"
19833
19831
  ].join(" ").split(" "));
19832
+ const REPLACEMENT_LABELS = /* @__PURE__ */ new Set([
19833
+ "csiso2022kr",
19834
+ "hz-gb-2312",
19835
+ "iso-2022-cn",
19836
+ "iso-2022-cn-ext",
19837
+ "iso-2022-kr",
19838
+ "replacement"
19839
+ ]);
19840
+ const replacementDecoder = {
19841
+ encoding: "replacement",
19842
+ decode: (input) => input && input.byteLength ? "�" : ""
19843
+ };
19844
+ const UTF16_LABELS = /* @__PURE__ */ new Map([
19845
+ ["utf-16", "utf-16"],
19846
+ ["utf16", "utf-16"],
19847
+ ["utf_16", "utf-16"],
19848
+ ["utf-16le", "utf-16le"],
19849
+ ["utf-16be", "utf-16be"]
19850
+ ]);
19851
+ /**
19852
+ * The label of the Encoding Standard a charset resolves to, as written or through the
19853
+ * alias tables, or null when it names no encoding the standard lists
19854
+ */
19855
+ function resolveLabel(charset) {
19856
+ charset = (charset || "utf8").trim().toLowerCase();
19857
+ if (ENCODING_LABELS.has(charset)) return charset;
19858
+ const utf16 = UTF16_LABELS.get(charset);
19859
+ if (utf16) return utf16;
19860
+ const normalized = normalizeCharset(charset);
19861
+ const codePage = normalized.match(codePagePattern);
19862
+ const alias = codePage && codePageAliases.get(codePage[1]) || charsetAliases.get(normalized) || normalized;
19863
+ return ENCODING_LABELS.has(alias) ? alias : null;
19864
+ }
19834
19865
  const decoders = /* @__PURE__ */ new Map();
19835
- function tryDecoder(charset) {
19836
- if (!ENCODING_LABELS.has(charset)) return null;
19837
- let decoder = decoders.get(charset);
19866
+ function tryDecoder(label) {
19867
+ if (REPLACEMENT_LABELS.has(label)) return replacementDecoder;
19868
+ let decoder = decoders.get(label);
19838
19869
  if (decoder === void 0) {
19839
19870
  decoder = null;
19840
19871
  try {
19841
- decoder = new TextDecoder(charset);
19872
+ decoder = new TextDecoder(label);
19842
19873
  } catch {}
19843
- decoders.set(charset, decoder);
19874
+ decoders.set(label, decoder);
19844
19875
  }
19845
19876
  return decoder;
19846
19877
  }
@@ -19849,16 +19880,21 @@ const utf8Decoder = new TextDecoder();
19849
19880
  * Finds the decoder for a charset label, or null when the runtime has no encoding for it
19850
19881
  */
19851
19882
  function findDecoder(charset) {
19852
- charset = (charset || "utf8").trim().toLowerCase();
19853
- const decoder = tryDecoder(charset);
19854
- if (decoder) return decoder;
19855
- const normalized = normalizeCharset(charset);
19856
- const codePage = normalized.match(codePagePattern);
19857
- return tryDecoder(codePage && codePageAliases.get(codePage[1]) || charsetAliases.get(normalized) || normalized);
19883
+ const label = resolveLabel(charset);
19884
+ return label ? tryDecoder(label) : null;
19858
19885
  }
19859
19886
  function getDecoder(charset) {
19860
19887
  return findDecoder(charset) || tryDecoder("windows-1252") || utf8Decoder;
19861
19888
  }
19889
+ const UTF16_UNORDERED_LABELS = /* @__PURE__ */ new Set(["utf-16"]);
19890
+ /**
19891
+ * The byte order a UTF-16 byte order mark at the start of the bytes names, or null without one
19892
+ */
19893
+ function bomByteOrder(bytes) {
19894
+ if (bytes[0] === 254 && bytes[1] === 255) return "utf-16be";
19895
+ if (bytes[0] === 255 && bytes[1] === 254) return "utf-16le";
19896
+ return null;
19897
+ }
19862
19898
  /**
19863
19899
  * Whether the bytes are well formed UTF-8. A scan rather than a fatal TextDecoder, since
19864
19900
  * the exception one throws costs more than the decode itself and a message decides how
@@ -19912,8 +19948,14 @@ function decodeUtf8OrWindows1252(bytes, utf8 = utf8Decoder) {
19912
19948
  * @return Decoded text
19913
19949
  */
19914
19950
  function decodeCharset(bytes, charset) {
19915
- const decoder = findDecoder(charset);
19916
- return decoder ? decoder.decode(bytes) : decodeUtf8OrWindows1252(bytes);
19951
+ const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
19952
+ const label = resolveLabel(charset);
19953
+ let decoder = label ? tryDecoder(label) : null;
19954
+ if (label && UTF16_UNORDERED_LABELS.has(label)) {
19955
+ const order = bomByteOrder(view);
19956
+ decoder = order && tryDecoder(order) || decoder;
19957
+ }
19958
+ return decoder ? decoder.decode(view) : decodeUtf8OrWindows1252(view);
19917
19959
  }
19918
19960
  /**
19919
19961
  * Numeric value of an ASCII hex digit
@@ -20033,7 +20075,7 @@ function splitEncodedWords(str) {
20033
20075
  function canJoinWords(left, right) {
20034
20076
  const encoding = left.encoding.toUpperCase();
20035
20077
  if (left.charset !== right.charset || encoding !== right.encoding.toUpperCase()) return false;
20036
- if (encoding === "B") return left.encodedText.length % 4 === 0 && !/=$/.test(left.encodedText);
20078
+ if (encoding === "B") return left.encodedText.length % 4 === 0 && !left.padded;
20037
20079
  return true;
20038
20080
  }
20039
20081
  /**
@@ -20060,13 +20102,15 @@ function renderTokens(tokens, joinWords) {
20060
20102
  }
20061
20103
  if (pending && joinWords && canJoinWords(pending, token)) {
20062
20104
  pending.encodedText += token.encodedText;
20105
+ pending.padded = token.encodedText.endsWith("=");
20063
20106
  continue;
20064
20107
  }
20065
20108
  if (pending) result += decodeWord(pending.charset, pending.encoding, pending.encodedText);
20066
20109
  pending = {
20067
20110
  charset: token.charset,
20068
20111
  encoding: token.encoding,
20069
- encodedText: token.encodedText
20112
+ encodedText: token.encodedText,
20113
+ padded: token.encodedText.endsWith("=")
20070
20114
  };
20071
20115
  }
20072
20116
  if (pending) result += decodeWord(pending.charset, pending.encoding, pending.encodedText);
@@ -20085,7 +20129,86 @@ function decodeWords(str) {
20085
20129
  }
20086
20130
  new TextDecoder("utf-8", { ignoreBOM: true });
20087
20131
  //#endregion
20088
- //#region ../../node_modules/.pnpm/postal-mime@4.0.5/node_modules/postal-mime/dist/esm/address-parser.js
20132
+ //#region ../../node_modules/.pnpm/postal-mime@4.1.4/node_modules/postal-mime/dist/esm/text-format.js
20133
+ const hexByte = (value) => value.toString(16).toUpperCase().padStart(2, "0");
20134
+ Array.from({ length: 256 }, (_, byte) => "%" + hexByte(byte));
20135
+ //#endregion
20136
+ //#region ../../node_modules/.pnpm/postal-mime@4.1.4/node_modules/postal-mime/dist/esm/address-parser.js
20137
+ /** The token is an operator, a text run otherwise */
20138
+ const TOKEN_OPERATOR = 1;
20139
+ /** The next character continues the same run without whitespace in between */
20140
+ const TOKEN_NO_BREAK = 2;
20141
+ /** The text run holds characters its value leaves out or changes, so it is no plain slice */
20142
+ const TOKEN_DIRTY = 4;
20143
+ /** A backslash escapes the next character in the text run, inside a quoted string or a comment */
20144
+ const TOKEN_ESCAPES = 8;
20145
+ /** The text run opens on a character an escaping backslash in front of it has escaped */
20146
+ const TOKEN_STARTS_ESCAPED = 16;
20147
+ /**
20148
+ * The tokens of an address field, kept as offsets into it.
20149
+ *
20150
+ * Every token used to be an object of its own, an operator for every ',' and a value string
20151
+ * for every run of text, and a list of one letter names, 'a,a,a,...', took some 65 times its
20152
+ * size in tokens before any address was read. A token is now three numbers in one typed
20153
+ * array, its kind and the range of the field it covers, and its value is sliced off the field
20154
+ * when it is read. The few text runs whose value differs from their slice, as they hold an
20155
+ * escaping backslash, a line break or a control character, are rebuilt once and kept aside.
20156
+ */
20157
+ var TokenList = class {
20158
+ constructor(str) {
20159
+ this.str = str;
20160
+ this.slots = /* @__PURE__ */ new Int32Array(48);
20161
+ this.length = 0;
20162
+ this.cleaned = /* @__PURE__ */ new Map();
20163
+ }
20164
+ /**
20165
+ * Appends a token
20166
+ *
20167
+ * @param kind Kind and flags of the token
20168
+ * @param start Offset of its first character
20169
+ * @param end Offset just past its last character
20170
+ * @return Index of the token
20171
+ */
20172
+ push(kind, start, end) {
20173
+ if ((this.length + 1) * 3 > this.slots.length) {
20174
+ const grown = new Int32Array(this.slots.length * 2);
20175
+ grown.set(this.slots);
20176
+ this.slots = grown;
20177
+ }
20178
+ const at = this.length * 3;
20179
+ this.slots[at] = kind;
20180
+ this.slots[at + 1] = start;
20181
+ this.slots[at + 2] = end;
20182
+ return this.length++;
20183
+ }
20184
+ /**
20185
+ * The character of an operator token, an empty string for a text run
20186
+ */
20187
+ operatorAt(index) {
20188
+ const at = index * 3;
20189
+ return this.slots[at] & TOKEN_OPERATOR ? this.str.charAt(this.slots[at + 1]) : "";
20190
+ }
20191
+ /**
20192
+ * Whether a token is an operator
20193
+ */
20194
+ isOperator(index) {
20195
+ return (this.slots[index * 3] & TOKEN_OPERATOR) !== 0;
20196
+ }
20197
+ /**
20198
+ * Whether the next character continues the run of a token without whitespace in between
20199
+ */
20200
+ noBreak(index) {
20201
+ return (this.slots[index * 3] & TOKEN_NO_BREAK) !== 0;
20202
+ }
20203
+ /**
20204
+ * Value of a token
20205
+ */
20206
+ value(index) {
20207
+ const cleaned = this.cleaned.get(index);
20208
+ if (cleaned !== void 0) return cleaned;
20209
+ return this.str.slice(this.slots[index * 3 + 1], this.slots[index * 3 + 2]);
20210
+ }
20211
+ };
20089
20212
  /**
20090
20213
  * Any whitespace, the test for an angle-addr that needs recovering
20091
20214
  */
@@ -20100,6 +20223,23 @@ const QUOTED_LOCAL_ADDR = /^("(?:[^"\\]|\\[\s\S])*"@\S+)(?:\s+([\s\S]+))?$/;
20100
20223
  */
20101
20224
  const ADDR_SPEC = /^[^@\s]+@[^@\s]+$/;
20102
20225
  /**
20226
+ * The same with a domain literal for the domain, which may hold an '@' of its own
20227
+ */
20228
+ const LITERAL_ADDR_SPEC = /^[^@\s]+@\[[^[\]\\\s]*\]$/;
20229
+ /**
20230
+ * The '@' and domain that follow a quoted local part in the same run
20231
+ */
20232
+ const QUOTED_LOCAL_DOMAIN = /^@(?:[^@\s"]+|\[[^[\]\\\s]*\])$/;
20233
+ /**
20234
+ * Whether a whitespace free run reads as an addr-spec under the strict grammar
20235
+ *
20236
+ * @param value Run to check
20237
+ * @return true when it does
20238
+ */
20239
+ function isAddrSpec(value) {
20240
+ return ADDR_SPEC.test(value) || LITERAL_ADDR_SPEC.test(value);
20241
+ }
20242
+ /**
20103
20243
  * The looser reading applied once the strict one finds nothing, which tolerates the
20104
20244
  * further '@' that a domain should not have but malformed headers carry anyway
20105
20245
  */
@@ -20110,6 +20250,22 @@ const LOOSE_ADDR_SPEC = /^[^@\s]+@\S+$/;
20110
20250
  */
20111
20251
  const LOOSE_TEXT_ADDR = /\s*\b[^@\s]+@[^\s]+\b\s*/y;
20112
20252
  /**
20253
+ * A domain literal that ends an address. dtext includes '@' (RFC 5322 3.4.1), so the '@'
20254
+ * in front of the literal is the one that splits the domain off, not the last one: reading
20255
+ * 'a@[x@evil.com]' at its last '@' made it an address at evil.com.
20256
+ */
20257
+ const DOMAIN_LITERAL_TAIL = /@(\[[^[\]\\]*\])$/;
20258
+ /**
20259
+ * Offset of the '@' that splits the domain off an address, or -1 when there is none
20260
+ *
20261
+ * @param address Address to split
20262
+ * @return Offset of the '@'
20263
+ */
20264
+ function domainAt(address) {
20265
+ const literal = address.charAt(address.length - 1) === "]" ? DOMAIN_LITERAL_TAIL.exec(address) : null;
20266
+ return literal ? address.length - literal[1].length - 1 : address.lastIndexOf("@");
20267
+ }
20268
+ /**
20113
20269
  * Restores the quoting of a local part that was read out of a quoted string.
20114
20270
  *
20115
20271
  * RFC 5321 allows '@' inside a quoted local part, so handing '"user@evil.com"@good.com'
@@ -20122,7 +20278,7 @@ const LOOSE_TEXT_ADDR = /\s*\b[^@\s]+@[^\s]+\b\s*/y;
20122
20278
  * @return Address with the local part as a quoted-string
20123
20279
  */
20124
20280
  function quoteLocalPart(address) {
20125
- const lastAt = address.lastIndexOf("@");
20281
+ const lastAt = domainAt(address);
20126
20282
  if (lastAt < 0) return address;
20127
20283
  const user = address.substring(0, lastAt);
20128
20284
  if (/^[^\s"(),:;<>@[\\\]]+$/.test(user) || /^"(?:[^"\\]|\\[\s\S])*"$/.test(user)) return address;
@@ -20159,6 +20315,50 @@ function indexOfAt(text, from, to) {
20159
20315
  return -1;
20160
20316
  }
20161
20317
  /**
20318
+ * A quoted-string a value opens with
20319
+ */
20320
+ const LEADING_QUOTED_STRING = /^"(?:[^"\\]|\\[\s\S])*"/;
20321
+ /**
20322
+ * Drops the whitespace on either side of the '@' at an offset.
20323
+ *
20324
+ * obs-local-part and obs-domain allow CFWS around the '@' (RFC 5322 4.4), so 'user @ good.com'
20325
+ * names user@good.com. Kept, the whitespace became part of the address as '"user "@ good.com'.
20326
+ * The scan is by index, a pattern such as /\s+@/ backtracks over a long whitespace run that
20327
+ * no '@' follows.
20328
+ *
20329
+ * @param value Value holding the '@'
20330
+ * @param at Offset of the '@', or -1
20331
+ * @return The value with the whitespace beside that '@' removed
20332
+ */
20333
+ function joinAtSpacing(value, at) {
20334
+ if (at < 0) return value;
20335
+ let start = at;
20336
+ while (start > 0 && isSpaceCode(value.charCodeAt(start - 1))) start--;
20337
+ let end = at + 1;
20338
+ while (end < value.length && isSpaceCode(value.charCodeAt(end))) end++;
20339
+ return start === at && end === at + 1 ? value : value.slice(0, start) + "@" + value.slice(end);
20340
+ }
20341
+ /**
20342
+ * Drops the whitespace beside the '@' that splits the domain off a value. That is the '@'
20343
+ * right after a quoted local part the value opens with, or else the only '@' it holds outside
20344
+ * any quoted string. With several it is not clear which one the whitespace belongs to, and
20345
+ * '<x@evil.com @good.com>' is left to the addr-spec recovery to read as it always has.
20346
+ *
20347
+ * @param value Value to clean up
20348
+ * @return The value with the whitespace beside that '@' removed
20349
+ */
20350
+ function collapseAtSpacing(value) {
20351
+ const quoted = LEADING_QUOTED_STRING.exec(value);
20352
+ if (quoted) {
20353
+ let at = quoted[0].length;
20354
+ while (at < value.length && isSpaceCode(value.charCodeAt(at))) at++;
20355
+ if (value.charAt(at) === "@" && value.indexOf("@", at + 1) < 0) return joinAtSpacing(value, at);
20356
+ }
20357
+ const at = value.indexOf("@");
20358
+ if (at < 0 || value.indexOf("@", at + 1) >= 0 || value.lastIndexOf("\"") > at) return value;
20359
+ return joinAtSpacing(value, at);
20360
+ }
20361
+ /**
20162
20362
  * Finds the offset LOOSE_TEXT_ADDR matches at, or -1 when it does not match at all.
20163
20363
  *
20164
20364
  * Letting the pattern search for itself is quadratic: '[^@\s]+' is retried from every
@@ -20229,6 +20429,7 @@ function looseAddressStart(text) {
20229
20429
  * @return The address and the display text, with any leftovers moved to the text
20230
20430
  */
20231
20431
  function recoverAddrSpec(address, text) {
20432
+ address = collapseAtSpacing(address);
20232
20433
  if (!HAS_WHITESPACE.test(address)) return {
20233
20434
  address,
20234
20435
  text
@@ -20248,7 +20449,7 @@ function recoverAddrSpec(address, text) {
20248
20449
  text
20249
20450
  };
20250
20451
  const parts = address.split(/\s+/);
20251
- let addrIndex = parts.findIndex((part) => ADDR_SPEC.test(part));
20452
+ let addrIndex = parts.findIndex(isAddrSpec);
20252
20453
  if (addrIndex < 0) addrIndex = parts.findIndex((part) => LOOSE_ADDR_SPEC.test(part));
20253
20454
  if (addrIndex < 0) return {
20254
20455
  address,
@@ -20271,7 +20472,16 @@ function recoverAddrSpec(address, text) {
20271
20472
  * consumer stripping the comment reads as 'user@example.comevil.com'. A comment is folding
20272
20473
  * whitespace, so it is read as a space that splits two atoms and as nothing next to an '@',
20273
20474
  * the same rule the token walk applies outside the brackets. Quoted strings and domain
20274
- * literals are copied through untouched, a '(' in there is not a comment.
20475
+ * literals are copied through untouched, a '(' in there is not a comment. An unexpected '<'
20476
+ * outside them drops everything in front of it, comments included.
20477
+ *
20478
+ * RFC 822 also let an angle-addr name the relays to go through in front of the mailbox,
20479
+ * '<@relay1,@relay2:user@example.com>', which RFC 5322 still parses as obs-route. The route
20480
+ * is not part of the mailbox, and folded into the local part it made
20481
+ * '"@relay1,@relay2:user"@example.com' a recipient that does not exist. A local part can not
20482
+ * start with '@', so contents that do, up to the first ':' outside a domain literal and in
20483
+ * front of any quoted string, are a route and are dropped, as long as a mailbox with an '@'
20484
+ * of its own follows.
20275
20485
  *
20276
20486
  * @param address Contents of the angle brackets
20277
20487
  * @return The address with the comments removed, and the text of the comments
@@ -20283,6 +20493,10 @@ function stripAddressComments(address) {
20283
20493
  let depth = 0;
20284
20494
  let closer = "";
20285
20495
  let lastChar = "";
20496
+ let pendingSpace = false;
20497
+ let firstChar = "";
20498
+ let quoteSeen = false;
20499
+ let routeEnd = -1;
20286
20500
  for (let i = 0, len = address.length; i < len; i++) {
20287
20501
  const chr = address.charAt(i);
20288
20502
  if (depth) {
@@ -20293,13 +20507,17 @@ function stripAddressComments(address) {
20293
20507
  } else if (chr === ")" && !--depth) {
20294
20508
  comments.push(comment.trim());
20295
20509
  comment = "";
20296
- if (lastChar !== "@" && address.charAt(i + 1) !== "@") {
20297
- result += " ";
20298
- lastChar = " ";
20299
- }
20510
+ pendingSpace = lastChar !== "@";
20300
20511
  } else comment += chr;
20301
20512
  continue;
20302
20513
  }
20514
+ if (pendingSpace && chr !== "(") {
20515
+ pendingSpace = false;
20516
+ if (chr !== "@" && chr !== "<" && !isSpaceCode(address.charCodeAt(i))) {
20517
+ result += " ";
20518
+ lastChar = " ";
20519
+ }
20520
+ }
20303
20521
  if (closer) {
20304
20522
  if (chr === "\\" && closer === "\"" && i < len - 1) {
20305
20523
  result += chr + address.charAt(++i);
@@ -20307,15 +20525,28 @@ function stripAddressComments(address) {
20307
20525
  continue;
20308
20526
  }
20309
20527
  if (chr === closer) closer = "";
20310
- } else if (chr === "\"") closer = "\"";
20528
+ } else if (chr === "\"") {
20529
+ closer = "\"";
20530
+ quoteSeen = true;
20531
+ } else if (chr === ":" && routeEnd < 0 && firstChar === "@" && !quoteSeen) routeEnd = result.length + 1;
20311
20532
  else if (chr === "[") closer = "]";
20312
20533
  else if (chr === "(") {
20313
20534
  depth = 1;
20314
20535
  continue;
20536
+ } else if (chr === "<") {
20537
+ result = "";
20538
+ lastChar = "";
20539
+ comments.length = 0;
20540
+ firstChar = "";
20541
+ quoteSeen = false;
20542
+ routeEnd = -1;
20543
+ continue;
20315
20544
  }
20316
20545
  result += chr;
20317
20546
  lastChar = chr;
20547
+ if (!firstChar && !isSpaceCode(address.charCodeAt(i))) firstChar = chr;
20318
20548
  }
20549
+ if (routeEnd >= 0 && result.indexOf("@", routeEnd) >= 0) result = result.slice(routeEnd);
20319
20550
  if (depth) comments.push(comment.trim());
20320
20551
  return {
20321
20552
  address: result.trim(),
@@ -20323,62 +20554,36 @@ function stripAddressComments(address) {
20323
20554
  };
20324
20555
  }
20325
20556
  /**
20326
- * Checks whether text holds an angle bracket address, ie. `<` and `>` around a run
20327
- * without other angle brackets that has an '@' with at least one character on each side.
20328
- *
20329
- * Same answer as `/<[^<>]+@[^<>]+>/.test(text)`, which backtracks quadratically on a `<`
20330
- * followed by a long run of '@' and no closing `>`.
20331
- *
20332
- * @param text Text to check
20333
- * @return true if an angle bracket address is present
20334
- */
20335
- function hasAngleAddress(text) {
20336
- let start = -1;
20337
- let at = -1;
20338
- for (let i = 0; i < text.length; i++) {
20339
- const chr = text.charAt(i);
20340
- if (chr === "<") {
20341
- start = i + 1;
20342
- at = -1;
20343
- } else if (chr === ">") {
20344
- if (at >= 0 && at < i - 1) return true;
20345
- start = -1;
20346
- at = -1;
20347
- } else if (chr === "@" && start >= 0 && at < 0 && i > start) at = i;
20348
- }
20349
- return false;
20350
- }
20351
- /**
20352
- * Converts tokens for a single address into an address object
20557
+ * Walks the tokens of one entry of an address list and sorts them into the runs of display
20558
+ * text, angle-addr and comment they belong to
20353
20559
  *
20354
- * @param tokens Tokens object
20355
- * @param depth Current recursion depth for nested group protection
20356
- * @return Address objects
20560
+ * @param tokens Tokens of the whole address list
20561
+ * @param from Index of the first token of the entry
20562
+ * @param to Index just past the last token of the entry
20563
+ * @return The collected runs, and whether the display text came out of a comment
20357
20564
  */
20358
- function _handleAddress(tokens, depth) {
20359
- let isGroup = false;
20565
+ function collectAddressParts(tokens, from, to) {
20360
20566
  let state = "text";
20361
- let addresses = [];
20362
20567
  let data = {
20363
20568
  address: [],
20364
20569
  comment: [],
20365
- group: [],
20366
20570
  text: [],
20367
- textWasQuoted: []
20571
+ textWasQuoted: [],
20572
+ quotedLocalAt: []
20368
20573
  };
20574
+ let lastTextPureQuoted = false;
20369
20575
  let i;
20370
- let len;
20371
20576
  let insideQuotes = false;
20372
20577
  const lastChars = {
20373
20578
  address: "",
20374
20579
  comment: "",
20375
- group: "",
20376
20580
  text: ""
20377
20581
  };
20378
- for (i = 0, len = tokens.length; i < len; i++) {
20379
- let token = tokens[i];
20380
- let prevToken = i ? tokens[i - 1] : null;
20381
- if (token.type === "operator") switch (token.value) {
20582
+ for (i = from; i < to; i++) {
20583
+ const value = tokens.value(i);
20584
+ const prevOperator = i > from ? tokens.operatorAt(i - 1) : "";
20585
+ const prevNoBreak = i > from && tokens.noBreak(i - 1);
20586
+ if (tokens.isOperator(i)) switch (value) {
20382
20587
  case "<":
20383
20588
  state = "address";
20384
20589
  insideQuotes = false;
@@ -20387,11 +20592,6 @@ function _handleAddress(tokens, depth) {
20387
20592
  state = "comment";
20388
20593
  insideQuotes = false;
20389
20594
  break;
20390
- case ":":
20391
- state = "group";
20392
- isGroup = true;
20393
- insideQuotes = false;
20394
- break;
20395
20595
  case "\"":
20396
20596
  insideQuotes = !insideQuotes;
20397
20597
  state = "text";
@@ -20400,108 +20600,132 @@ function _handleAddress(tokens, depth) {
20400
20600
  state = "text";
20401
20601
  insideQuotes = false;
20402
20602
  }
20403
- else if (token.value) {
20404
- const prevPrevToken = i > 1 ? tokens[i - 2] : null;
20405
- const opensAfterEmptyQuotedString = prevToken?.type === "operator" && prevToken.value === "\"" && !!prevToken.noBreak && prevPrevToken?.type === "operator" && prevPrevToken.value === "\"";
20406
- if (state === "address") token.value = token.value.replace(/^[^<]*<\s*/, "");
20407
- if (prevToken && prevToken.noBreak && data[state].length && (prevToken.value !== ")" || lastChars[state] === "@" || token.value.charAt(0) === "@")) {
20408
- data[state][data[state].length - 1] += token.value;
20409
- if (state === "text" && insideQuotes) data.textWasQuoted[data.textWasQuoted.length - 1] = true;
20603
+ else {
20604
+ const opensAfterEmptyQuotedString = prevOperator === "\"" && prevNoBreak && i > from + 1 && tokens.operatorAt(i - 2) === "\"";
20605
+ if (prevNoBreak && data[state].length && (prevOperator !== ")" || lastChars[state] === "@" || value.charAt(0) === "@")) {
20606
+ const last = data[state].length - 1;
20607
+ if (state === "text" && !insideQuotes) {
20608
+ if (lastTextPureQuoted && value.charAt(0) === "@") data.quotedLocalAt[last] = data.text[last].length;
20609
+ lastTextPureQuoted = false;
20610
+ }
20611
+ data[state][last] += value;
20612
+ if (state === "text" && insideQuotes) data.textWasQuoted[last] = true;
20410
20613
  } else {
20411
- data[state].push(token.value);
20412
- if (state === "text") data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
20614
+ data[state].push(value);
20615
+ if (state === "text") {
20616
+ data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
20617
+ data.quotedLocalAt.push(opensAfterEmptyQuotedString && value.charAt(0) === "@" ? 0 : -1);
20618
+ lastTextPureQuoted = insideQuotes;
20619
+ }
20413
20620
  }
20414
- lastChars[state] = token.value.charAt(token.value.length - 1);
20621
+ lastChars[state] = value.charAt(value.length - 1);
20415
20622
  }
20416
20623
  }
20624
+ let textFromComment = false;
20417
20625
  if (!data.text.length && data.comment.length) {
20418
20626
  data.text = data.comment;
20419
20627
  data.comment = [];
20628
+ textFromComment = true;
20420
20629
  }
20421
- if (isGroup) {
20422
- const text = data.text.join(" ");
20423
- let groupMembers = [];
20424
- if (data.group.length) parseAddressList$1(data.group.join(","), depth + 1).forEach((member) => {
20425
- if (member.group) for (const nestedMember of member.group) groupMembers.push(nestedMember);
20426
- else groupMembers.push(member);
20427
- });
20428
- addresses.push({
20429
- name: decodeWords(text),
20430
- group: groupMembers
20431
- });
20432
- } else {
20433
- const addressComments = [];
20434
- const addressParts = [];
20435
- for (const part of data.address) {
20436
- if (part.indexOf("(") < 0) {
20437
- addressParts.push(part);
20438
- continue;
20439
- }
20440
- const stripped = stripAddressComments(part);
20441
- for (const comment of stripped.comments) addressComments.push(comment);
20442
- if (stripped.address) addressParts.push(stripped.address);
20443
- }
20444
- data.address = addressParts;
20445
- if (!data.address.length && data.text.length) {
20446
- for (i = data.text.length - 1; i >= 0; i--) if (!data.textWasQuoted[i] && ADDR_SPEC.test(data.text[i])) {
20447
- data.address = data.text.splice(i, 1);
20630
+ return {
20631
+ data,
20632
+ textFromComment
20633
+ };
20634
+ }
20635
+ /**
20636
+ * Name of a group, from the tokens in front of its ':'. A comment names it when no text
20637
+ * does, as it names a mailbox.
20638
+ *
20639
+ * @param tokens Tokens of the whole address list
20640
+ * @param from Index of the first token in front of the ':'
20641
+ * @param to Index of the ':'
20642
+ * @return Decoded name of the group
20643
+ */
20644
+ function groupName(tokens, from, to) {
20645
+ return decodeWords(collectAddressParts(tokens, from, to).data.text.join(" "));
20646
+ }
20647
+ /**
20648
+ * Converts tokens for a single address into an address object
20649
+ *
20650
+ * @param tokens Tokens of the whole address list
20651
+ * @param from Index of the first token of this address
20652
+ * @param to Index just past the last token of this address
20653
+ * @return The address object
20654
+ */
20655
+ function _handleAddress(tokens, from, to) {
20656
+ const collected = collectAddressParts(tokens, from, to);
20657
+ const data = collected.data;
20658
+ let textFromComment = collected.textFromComment;
20659
+ let i;
20660
+ const addressComments = [];
20661
+ const addressParts = [];
20662
+ for (const part of data.address) {
20663
+ const stripped = stripAddressComments(part);
20664
+ for (const comment of stripped.comments) addressComments.push(comment);
20665
+ if (stripped.address) addressParts.push(stripped.address);
20666
+ }
20667
+ data.address = addressParts;
20668
+ if (!data.address.length && data.text.length && !textFromComment) {
20669
+ const candidates = data.text.map((part) => collapseAtSpacing(part));
20670
+ for (i = 0; i < data.text.length; i++) {
20671
+ const local = data.quotedLocalAt[i];
20672
+ const quotedLocal = data.textWasQuoted[i] && local >= 0 && QUOTED_LOCAL_DOMAIN.test(data.text[i].slice(local)) ? quoteLocalPart(data.text[i]) : "";
20673
+ if (quotedLocal || !data.textWasQuoted[i] && isAddrSpec(candidates[i])) {
20674
+ data.address = [quotedLocal || candidates[i]];
20675
+ data.text.splice(i, 1);
20448
20676
  data.textWasQuoted.splice(i, 1);
20677
+ data.quotedLocalAt.splice(i, 1);
20449
20678
  break;
20450
20679
  }
20451
- if (!data.address.length) {
20452
- for (i = data.text.length - 1; i >= 0; i--) if (!data.textWasQuoted[i]) {
20453
- const part = data.text[i];
20454
- const start = looseAddressStart(part);
20455
- if (start >= 0) {
20456
- LOOSE_TEXT_ADDR.lastIndex = start;
20457
- const match = LOOSE_TEXT_ADDR.exec(part);
20458
- if (match) {
20459
- data.address = [match[0].trim()];
20460
- data.text[i] = (part.slice(0, start) + " " + part.slice(start + match[0].length)).trim();
20461
- break;
20462
- }
20680
+ }
20681
+ if (!data.address.length) {
20682
+ for (i = 0; i < data.text.length; i++) if (!data.textWasQuoted[i]) {
20683
+ const part = candidates[i];
20684
+ const start = looseAddressStart(part);
20685
+ if (start >= 0) {
20686
+ LOOSE_TEXT_ADDR.lastIndex = start;
20687
+ const match = LOOSE_TEXT_ADDR.exec(part);
20688
+ if (match) {
20689
+ data.address = [match[0].trim()];
20690
+ data.text[i] = (part.slice(0, start) + " " + part.slice(start + match[0].length)).trim();
20691
+ break;
20463
20692
  }
20464
- data.text[i] = part.trim();
20465
20693
  }
20694
+ data.text[i] = data.text[i].trim();
20466
20695
  }
20467
20696
  }
20468
- if (!data.text.length && data.comment.length) {
20469
- data.text = data.comment;
20470
- data.comment = [];
20471
- }
20472
- if (data.address.length > 1) data.text = data.text.concat(data.address.splice(1));
20473
- const addressFromQuotedText = !data.address.length && data.textWasQuoted.some((wasQuoted) => wasQuoted);
20474
- let text = data.text.join(" ");
20475
- let addressValue = data.address.join(" ");
20476
- if (!addressValue && isEncodedWordsOnly(text.trim())) {
20477
- const decodedText = decodeWords(text);
20478
- if (!addressFromQuotedText && hasAngleAddress(decodedText)) {
20479
- const parsedSubAddresses = parseAddressList$1(decodedText, depth + 1);
20480
- if (parsedSubAddresses.length) return parsedSubAddresses;
20481
- }
20482
- return [{
20483
- address: "",
20484
- name: decodedText
20485
- }];
20486
- }
20487
- if (addressFromQuotedText && text.indexOf("@") >= 0) {
20488
- addressValue = quoteLocalPart(text);
20489
- text = "";
20490
- }
20491
- ({address: addressValue, text} = recoverAddrSpec(addressValue, text));
20492
- if (!text && addressComments.length) text = addressComments.join(" ");
20493
- const rawName = text || addressValue || "";
20494
- const address = {
20495
- address: addressValue || text || "",
20496
- name: decodeWords(rawName)
20497
- };
20498
- if (address.address === rawName || address.address === address.name) {
20499
- if (address.address.match(/@/)) address.name = "";
20500
- else address.address = "";
20501
- }
20502
- addresses.push(address);
20503
20697
  }
20504
- return addresses;
20698
+ if (!data.text.length && data.comment.length) {
20699
+ data.text = data.comment;
20700
+ data.comment = [];
20701
+ }
20702
+ if (data.address.length > 1) data.text = data.text.concat(data.address.splice(1));
20703
+ const addressFromQuotedText = !data.address.length && data.textWasQuoted.some((wasQuoted) => wasQuoted);
20704
+ let text = data.text.join(" ");
20705
+ let addressValue = data.address.join(" ");
20706
+ if (!addressValue && isEncodedWordsOnly(text.trim())) return {
20707
+ address: "",
20708
+ name: decodeWords(text)
20709
+ };
20710
+ if (addressFromQuotedText && text.indexOf("@") >= 0) {
20711
+ addressValue = quoteLocalPart(joinAtSpacing(text, domainAt(text)));
20712
+ text = "";
20713
+ }
20714
+ ({address: addressValue, text} = recoverAddrSpec(addressValue, text));
20715
+ if (!text && addressComments.length) {
20716
+ text = addressComments.join(" ");
20717
+ textFromComment = true;
20718
+ }
20719
+ const rawName = text || addressValue || "";
20720
+ const address = {
20721
+ address: addressValue || (textFromComment ? "" : text) || "",
20722
+ name: decodeWords(rawName)
20723
+ };
20724
+ if (address.address === rawName || address.address === address.name) {
20725
+ if (address.address.match(/@/)) address.name = "";
20726
+ else address.address = "";
20727
+ }
20728
+ return address;
20505
20729
  }
20506
20730
  /**
20507
20731
  * Tokenizer for address field strings
@@ -20514,17 +20738,22 @@ var Tokenizer$2 = class {
20514
20738
  this.str = (str || "").toString();
20515
20739
  this.operatorCurrent = "";
20516
20740
  this.operatorExpecting = "";
20517
- this.node = null;
20741
+ this.node = -1;
20518
20742
  this.escaped = false;
20519
20743
  this.inDomainLiteral = false;
20520
20744
  this.afterAt = false;
20521
- this.list = [];
20745
+ this.inGroup = false;
20746
+ this.innerQuoted = false;
20747
+ this.innerComment = 0;
20748
+ this.innerLiteral = false;
20749
+ this.innerEscaped = false;
20750
+ this.list = new TokenList(this.str);
20522
20751
  this.operators = {
20523
20752
  "\"": "\"",
20524
20753
  "(": ")",
20525
20754
  "<": ">",
20526
20755
  ",": "",
20527
- ":": ";",
20756
+ ":": "",
20528
20757
  ";": ""
20529
20758
  };
20530
20759
  }
@@ -20534,115 +20763,259 @@ var Tokenizer$2 = class {
20534
20763
  * @return An array of operator|text tokens
20535
20764
  */
20536
20765
  tokenize() {
20537
- let list = [];
20538
- for (let i = 0, len = this.str.length; i < len; i++) {
20539
- let chr = this.str.charAt(i);
20540
- let nextChr = i < len - 1 ? this.str.charAt(i + 1) : null;
20541
- this.checkChar(chr, nextChr);
20542
- }
20543
- this.list.forEach((node) => {
20544
- node.value = (node.value || "").toString().trim();
20545
- if (node.value) list.push(node);
20546
- });
20766
+ const str = this.str;
20767
+ const len = str.length;
20768
+ for (let i = 0; i < len; i++) this.checkChar(str.charAt(i), i);
20769
+ this.closeText(len);
20770
+ const list = this.list;
20771
+ const slots = list.slots;
20772
+ let kept = 0;
20773
+ for (let i = 0, count = list.length; i < count; i++) {
20774
+ const kind = slots[i * 3];
20775
+ let start = slots[i * 3 + 1];
20776
+ let end = slots[i * 3 + 2];
20777
+ if (kind & TOKEN_DIRTY) {
20778
+ const value = this.cleanValue(start, end, kind).trim();
20779
+ if (!value) continue;
20780
+ list.cleaned.set(kept, value);
20781
+ } else if (!(kind & TOKEN_OPERATOR)) {
20782
+ while (start < end && isSpaceCode(str.charCodeAt(start))) start++;
20783
+ while (end > start && isSpaceCode(str.charCodeAt(end - 1))) end--;
20784
+ if (start === end) continue;
20785
+ }
20786
+ slots[kept * 3] = kind;
20787
+ slots[kept * 3 + 1] = start;
20788
+ slots[kept * 3 + 2] = end;
20789
+ kept++;
20790
+ }
20791
+ list.length = kept;
20547
20792
  return list;
20548
20793
  }
20549
20794
  /**
20795
+ * Ends the text run being collected, if there is one
20796
+ *
20797
+ * @param end Offset just past its last character
20798
+ */
20799
+ closeText(end) {
20800
+ if (this.node >= 0) {
20801
+ this.list.slots[this.node * 3 + 2] = end;
20802
+ this.node = -1;
20803
+ }
20804
+ }
20805
+ /**
20806
+ * Rebuilds the value of a text run that is no plain slice of the field, applying the
20807
+ * rules checkChar collects characters by: an escaping backslash is dropped, a line break
20808
+ * reads as a space and any other control character is left out
20809
+ *
20810
+ * @param start Offset of the first character of the run
20811
+ * @param end Offset just past its last character
20812
+ * @param kind Kind and flags of the run
20813
+ * @return Value of the run
20814
+ */
20815
+ cleanValue(start, end, kind) {
20816
+ const str = this.str;
20817
+ const escapes = (kind & TOKEN_ESCAPES) !== 0;
20818
+ let escaped = (kind & TOKEN_STARTS_ESCAPED) !== 0;
20819
+ let value = "";
20820
+ for (let i = start; i < end; i++) {
20821
+ const chr = str.charAt(i);
20822
+ if (escapes && !escaped && chr === "\\") {
20823
+ escaped = true;
20824
+ continue;
20825
+ }
20826
+ escaped = false;
20827
+ if (chr === "\n") value += " ";
20828
+ else if (chr.charCodeAt(0) >= 33 || chr === " " || chr === " ") value += chr;
20829
+ }
20830
+ return value;
20831
+ }
20832
+ /**
20550
20833
  * Checks if a character is an operator or text and acts accordingly
20551
20834
  *
20552
20835
  * @param chr Character from the address field
20553
- * @param nextChr The character that follows, or null at the end of the field
20836
+ * @param i Offset of the character in the field
20554
20837
  */
20555
- checkChar(chr, nextChr) {
20838
+ checkChar(chr, i) {
20556
20839
  if (!this.escaped && !this.operatorExpecting) {
20557
20840
  if (!this.inDomainLiteral && chr === "[" && this.afterAt) this.inDomainLiteral = true;
20558
20841
  else if (this.inDomainLiteral && (chr === "]" || chr === "," || chr === ";")) this.inDomainLiteral = false;
20559
20842
  }
20560
- if (this.escaped) {} else if (chr === this.operatorExpecting) {
20561
- this.node = {
20562
- type: "operator",
20563
- value: chr
20564
- };
20565
- if (nextChr && ![
20843
+ let nested = false;
20844
+ if (this.operatorExpecting === ">") {
20845
+ nested = this.trackInner(chr);
20846
+ if (!nested && chr === ";" && this.inGroup) {
20847
+ this.closeText(i);
20848
+ this.operatorExpecting = "";
20849
+ this.resetInner();
20850
+ }
20851
+ } else if (this.operatorExpecting === ")" && !this.escaped) {
20852
+ if (chr === "(") this.innerComment++;
20853
+ else if (chr === ")" && this.innerComment) {
20854
+ this.innerComment--;
20855
+ nested = true;
20856
+ }
20857
+ }
20858
+ if (this.escaped) {} else if (chr === this.operatorExpecting && !nested) {
20859
+ const nextChr = this.str.charAt(i + 1);
20860
+ this.closeText(i);
20861
+ this.list.push(nextChr && ![
20566
20862
  " ",
20567
20863
  " ",
20568
20864
  "\r",
20569
20865
  "\n",
20570
20866
  ",",
20571
20867
  ";"
20572
- ].includes(nextChr)) this.node.noBreak = true;
20573
- this.list.push(this.node);
20574
- this.node = null;
20868
+ ].includes(nextChr) ? 3 : TOKEN_OPERATOR, i, i + 1);
20575
20869
  if (chr !== ")") this.afterAt = false;
20576
20870
  this.operatorExpecting = "";
20577
20871
  this.escaped = false;
20872
+ this.resetInner();
20578
20873
  return;
20579
20874
  } else if (!this.operatorExpecting && !(this.inDomainLiteral && chr === ":") && chr in this.operators) {
20580
- this.node = {
20581
- type: "operator",
20582
- value: chr
20583
- };
20584
- this.list.push(this.node);
20585
- this.node = null;
20875
+ this.closeText(i);
20876
+ this.list.push(TOKEN_OPERATOR, i, i + 1);
20877
+ if (chr === ":") this.inGroup = true;
20878
+ else if (chr === ";") this.inGroup = false;
20586
20879
  if (chr !== "(") this.afterAt = false;
20587
20880
  this.operatorExpecting = this.operators[chr];
20588
20881
  this.escaped = false;
20882
+ this.resetInner();
20589
20883
  return;
20590
20884
  } else if ((this.operatorExpecting === "\"" || this.operatorExpecting === ")") && chr === "\\") {
20591
20885
  this.escaped = true;
20886
+ if (this.node >= 0) this.list.slots[this.node * 3] |= TOKEN_DIRTY;
20592
20887
  return;
20593
20888
  }
20594
- if (!this.node) {
20595
- this.node = {
20596
- type: "text",
20597
- value: ""
20598
- };
20599
- this.list.push(this.node);
20889
+ if (this.node < 0) this.node = this.list.push((this.operatorExpecting === "\"" || this.operatorExpecting === ")" ? TOKEN_ESCAPES : 0) | (this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
20890
+ if (chr === "\n") {
20891
+ chr = " ";
20892
+ this.list.slots[this.node * 3] |= TOKEN_DIRTY;
20600
20893
  }
20601
- if (chr === "\n") chr = " ";
20602
- if (chr.charCodeAt(0) >= 33 || [" ", " "].includes(chr)) {
20603
- this.node.value += chr;
20894
+ if (chr.charCodeAt(0) >= 33 || chr === " " || chr === " ") {
20604
20895
  if (!this.operatorExpecting && chr !== " " && chr !== " ") this.afterAt = chr === "@";
20605
- }
20896
+ } else this.list.slots[this.node * 3] |= TOKEN_DIRTY;
20606
20897
  this.escaped = false;
20607
20898
  }
20899
+ /**
20900
+ * Tracks the quoted strings, comments and domain literals inside an angle-addr, which its
20901
+ * closer must not be read from. A backslash escapes the next character inside a quoted
20902
+ * string and a comment. A domain literal opens right after an "@" and ends at the "]".
20903
+ * dtext holds the list separators as well, and inside the brackets they never split the
20904
+ * list anyway.
20905
+ *
20906
+ * @param chr Character from the address field
20907
+ * @return true while the character sits inside one of them, the one that closes it included
20908
+ */
20909
+ trackInner(chr) {
20910
+ if (this.innerEscaped) {
20911
+ this.innerEscaped = false;
20912
+ return true;
20913
+ }
20914
+ if (this.innerQuoted) {
20915
+ if (chr === "\\") this.innerEscaped = true;
20916
+ else if (chr === "\"") this.innerQuoted = false;
20917
+ return true;
20918
+ }
20919
+ if (this.innerComment) {
20920
+ if (chr === "\\") this.innerEscaped = true;
20921
+ else if (chr === "(") this.innerComment++;
20922
+ else if (chr === ")") this.innerComment--;
20923
+ return true;
20924
+ }
20925
+ if (this.innerLiteral) {
20926
+ if (chr === "]") this.innerLiteral = false;
20927
+ return true;
20928
+ }
20929
+ if (chr === "\"") {
20930
+ this.innerQuoted = true;
20931
+ return true;
20932
+ }
20933
+ if (chr === "(") {
20934
+ this.innerComment = 1;
20935
+ return true;
20936
+ }
20937
+ if (chr === "[" && this.afterAt) {
20938
+ this.innerLiteral = true;
20939
+ return true;
20940
+ }
20941
+ if (!isSpaceCode(chr.charCodeAt(0))) this.afterAt = chr === "@";
20942
+ return false;
20943
+ }
20944
+ /**
20945
+ * Forgets the nesting collected for the operator that just opened or ended
20946
+ */
20947
+ resetInner() {
20948
+ this.innerQuoted = false;
20949
+ this.innerComment = 0;
20950
+ this.innerLiteral = false;
20951
+ this.innerEscaped = false;
20952
+ }
20608
20953
  };
20609
20954
  /**
20610
- * Maximum recursion depth for parsing nested groups.
20611
- * RFC 5322 doesn't allow nested groups, so this is a safeguard against
20612
- * malicious input that could cause stack overflow.
20955
+ * Folds the fragments an unquoted display name holding a comma is split into back together.
20956
+ *
20957
+ * "Joe Foo, PhD <joe@example.com>" is split on the comma into
20958
+ * [{name:"Joe Foo", address:""}, {name:"PhD", address:"joe@example.com"}]. A name-only entry
20959
+ * followed by an entry with both name and address is recombined. Walked back to front so
20960
+ * that a run of fragments folds into one entry in a single pass. Splicing each fragment out
20961
+ * of the list instead would be quadratic.
20962
+ *
20963
+ * @param list Parsed entries, in order
20964
+ * @return The entries with the fragments folded in
20613
20965
  */
20614
- const MAX_NESTED_GROUP_DEPTH = 50;
20966
+ function mergeNameFragments(list) {
20967
+ const merged = [];
20968
+ for (let i = list.length - 1; i >= 0; i--) {
20969
+ const current = list[i];
20970
+ const next = merged.length ? merged[merged.length - 1] : null;
20971
+ if (next && current.address === "" && current.name && !current.group && next.address && next.name) next.name = current.name + ", " + next.name;
20972
+ else merged.push(current);
20973
+ }
20974
+ return merged.reverse();
20975
+ }
20615
20976
  /**
20616
- * Parses an address list, recursing into groups. The depth is threaded through the
20617
- * calls rather than taken from an option, so a caller supplied options object can not
20618
- * seed it and lift the recursion limit.
20977
+ * Parses an address list, groups included, in a single pass over its tokens.
20978
+ *
20979
+ * A group used to be cut out of the field at the ';' that ends it and parsed again as an
20980
+ * address list of its own, recursively, so every level of nesting parsed what was left of
20981
+ * the field once more. RFC 5322 does not nest groups at all, and the nested ones were
20982
+ * flattened anyway. The tokens are now walked once: a ':' opens a group, its members are
20983
+ * read as they come, a ';' closes it. A ':' inside an open group stands for a group nested
20984
+ * in it, whose name is dropped and whose members belong to the outer one, and the first ';'
20985
+ * ends the group, nested or not, as before.
20619
20986
  *
20620
20987
  * @param str Address field
20621
- * @param depth Current recursion depth for nested group protection
20622
20988
  * @return An array of address objects
20623
20989
  */
20624
- function parseAddressList$1(str, depth) {
20625
- if (depth > MAX_NESTED_GROUP_DEPTH) return [];
20626
- let tokens = new Tokenizer$2(str).tokenize();
20627
- let addresses = [];
20628
- let address = [];
20990
+ function parseAddressList$1(str) {
20991
+ const tokens = new Tokenizer$2(str).tokenize();
20629
20992
  const parsedAddresses = [];
20630
- tokens.forEach((token) => {
20631
- if (token.type === "operator" && (token.value === "," || token.value === ";")) {
20632
- if (address.length) addresses.push(address);
20633
- address = [];
20634
- } else address.push(token);
20635
- });
20636
- if (address.length) addresses.push(address);
20637
- for (const tokens of addresses) for (const address of _handleAddress(tokens, depth)) parsedAddresses.push(address);
20638
- const mergedAddresses = [];
20639
- for (let i = parsedAddresses.length - 1; i >= 0; i--) {
20640
- const current = parsedAddresses[i];
20641
- const next = mergedAddresses.length ? mergedAddresses[mergedAddresses.length - 1] : null;
20642
- if (next && current.address === "" && current.name && !current.group && next.address && next.name) next.name = current.name + ", " + next.name;
20643
- else mergedAddresses.push(current);
20993
+ let group = null;
20994
+ let members = [];
20995
+ let from = 0;
20996
+ for (let i = 0, len = tokens.length; i <= len; i++) {
20997
+ const operator = i < len ? tokens.operatorAt(i) : "";
20998
+ if (i < len && operator !== "," && operator !== ";" && operator !== ":") continue;
20999
+ if (operator === ":") {
21000
+ if (!group) {
21001
+ group = {
21002
+ name: groupName(tokens, from, i),
21003
+ group: []
21004
+ };
21005
+ members = [];
21006
+ }
21007
+ from = i + 1;
21008
+ continue;
21009
+ }
21010
+ if (i > from) (group ? members : parsedAddresses).push(_handleAddress(tokens, from, i));
21011
+ from = i + 1;
21012
+ if (group && (operator === ";" || i === len)) {
21013
+ group.group = mergeNameFragments(members);
21014
+ parsedAddresses.push(group);
21015
+ group = null;
21016
+ }
20644
21017
  }
20645
- return mergedAddresses.reverse();
21018
+ return mergeNameFragments(parsedAddresses);
20646
21019
  }
20647
21020
  /**
20648
21021
  * Parses structured e-mail addresses from an address field
@@ -20660,7 +21033,7 @@ function parseAddressList$1(str, depth) {
20660
21033
  * @return An array of address objects
20661
21034
  */
20662
21035
  function addressParser(str, options) {
20663
- const parsedAddresses = parseAddressList$1(str, 0);
21036
+ const parsedAddresses = parseAddressList$1(str);
20664
21037
  if (options && options.flatten) {
20665
21038
  let addresses = [];
20666
21039
  let walkAddressList = (list) => {