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.
This commit is contained in:
Linus Rath
2026-09-26 15:48:27 +02:00
parent 34fb9d247a
commit bdd79648b0
2 changed files with 24 additions and 3 deletions
@@ -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", () => {
+11 -3
View File
@@ -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;