diff --git a/src/lib/__tests__/recipients.test.ts b/src/lib/__tests__/recipients.test.ts index 168efc25..d6790a7c 100644 --- a/src/lib/__tests__/recipients.test.ts +++ b/src/lib/__tests__/recipients.test.ts @@ -38,6 +38,23 @@ describe('splitRecipients', () => { it('keeps RFC 5322 groups as one entry', () => { expect(splitRecipients('Team: a@x.com, b@y.com;, c@z.com')).toEqual(['Team: a@x.com, b@y.com;', 'c@z.com']); }); + + it('does not take an escaped quote in a display name as the closing one', () => { + expect(splitRecipients('"Support\\", ceo@corp.example, \\"x" , b@y.com')) + .toEqual(['"Support\\", ceo@corp.example, \\"x" ', 'b@y.com']); + }); + + it('does not take an escaped quote as closing when an unclosed angle bracket looks ahead', () => { + expect(splitRecipients('John b" ')).toEqual(['John b" ']); + }); +}); + +describe('splitPastedRecipients with an escaped quote (composer paste)', () => { + it('adds no extra recipient from inside a display name', () => { + const { valid, invalid } = splitPastedRecipients('"Support\\", ceo@corp.example, \\"x" , b@y.com'); + expect(valid.map((r) => r.email)).toEqual(['s@x.com', 'b@y.com']); + expect(invalid).toEqual([]); + }); }); describe('parseRecipient', () => { diff --git a/src/lib/recipients.ts b/src/lib/recipients.ts index 21848ab2..f95dd8e6 100644 --- a/src/lib/recipients.ts +++ b/src/lib/recipients.ts @@ -58,7 +58,9 @@ 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; + // A quoted-pair (`\"`) never closes the quote. + if (inQuotes && ch === '\\') i++; + else if (ch === '"') inQuotes = !inQuotes; else if (inQuotes) continue; else if (ch === '>') return true; else if (ch === '<') return false; @@ -84,7 +86,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: a `"` in a display name is written `\"`. Taking it as + // the closing quote let a name like `Support", ceo@corp.example, "x` + // split into an extra recipient (webmail lib/email-composer-utils.ts). + current += ch + value[i + 1]; + i++; + } else if (ch === '"') { inQuotes = !inQuotes; current += ch; } else if (ch === '<' && !inQuotes) {