From bdd79648b0bbc63253fecbde67c62253bbefa21d Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 26 Sep 2026 15:48:27 +0200 Subject: [PATCH] fix: stop a quoted sender name from adding a recipient to a draft `formatRecipient` writes a quote inside a display name as `\"`, but the parsers that read recipient strings back took every `"` as the end of the quoted name. A sender named `Support", ceo@corp.example, "x` came back from the draft stash (or the dirty check) as three recipients, one of them an address the user never chose. The splitter and its helpers now skip escaped characters inside quotes. --- lib/__tests__/email-composer-utils.test.ts | 13 +++++++++++++ lib/email-composer-utils.ts | 14 +++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/__tests__/email-composer-utils.test.ts b/lib/__tests__/email-composer-utils.test.ts index 3b0502b28..117614a79 100644 --- a/lib/__tests__/email-composer-utils.test.ts +++ b/lib/__tests__/email-composer-utils.test.ts @@ -375,6 +375,19 @@ describe("parseRecipientList / formatRecipientList", () => { it("parses an empty string to an empty array", () => { expect(parseRecipientList("")).toEqual([]); }); + + it("keeps a display name with quotes as one recipient (no injected To)", () => { + // A sender named `Support", ceo@corp.example, "x` used to come back from + // the draft stash as three recipients, one of them the injected ceo. + const list = [{ name: 'Support", ceo@corp.example, "x', email: "help@vendor.example" }]; + const serialized = formatRecipientList(list); + expect(parseRecipientList(serialized)).toEqual(list); + }); + + it("round-trips backslashes in a quoted name", () => { + const list = [{ name: 'a\\b, "c"', email: "x@example.com" }, { email: "y@example.com" }]; + expect(parseRecipientList(formatRecipientList(list))).toEqual(list); + }); }); describe("splitPastedRecipients", () => { diff --git a/lib/email-composer-utils.ts b/lib/email-composer-utils.ts index de98389da..c69aa6d93 100644 --- a/lib/email-composer-utils.ts +++ b/lib/email-composer-utils.ts @@ -254,7 +254,8 @@ function angleRunCloses(value: string, from: number): boolean { let inQuotes = false; for (let i = from + 1; i < value.length; i++) { const ch = value[i]; - if (ch === '"') inQuotes = !inQuotes; + if (inQuotes && ch === '\\') i++; + else if (ch === '"') inQuotes = !inQuotes; else if (inQuotes) continue; else if (ch === '>') return true; else if (ch === '<') return false; @@ -280,7 +281,13 @@ export function splitRecipients(value: string, separators = ','): string[] { let inGroup = false; for (let i = 0; i < value.length; i++) { const ch = value[i]; - if (ch === '"') { + if (inQuotes && ch === '\\' && i + 1 < value.length) { + // A quoted-pair: formatRecipient writes a `"` in a display name as + // `\"`. Taking it as the closing quote let a sender's name like + // `Support", ceo@corp.example, "x` split into an extra recipient. + current += ch + value[i + 1]; + i++; + } else if (ch === '"') { inQuotes = !inQuotes; current += ch; } else if (ch === '<' && !inQuotes) { @@ -353,7 +360,8 @@ function findTopLevelColon(value: string): number { let inAngle = false; for (let i = 0; i < value.length; i++) { const ch = value[i]; - if (ch === '"') inQuotes = !inQuotes; + if (inQuotes && ch === '\\') i++; + else if (ch === '"') inQuotes = !inQuotes; else if (ch === '<' && !inQuotes) inAngle = true; else if (ch === '>' && !inQuotes) inAngle = false; else if (ch === ':' && !inQuotes && !inAngle) return i;