From e363427d0e3024c081d0f044ccf5577b9038d42e Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:52:08 +0100 Subject: [PATCH] fix: decode escaped colons in imported vcard notes #1438 --- lib/__tests__/vcard.test.ts | 28 ++++++++++++++++++++++++++++ lib/vcard.ts | 10 +++++----- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/lib/__tests__/vcard.test.ts b/lib/__tests__/vcard.test.ts index e9af44004..b996a0d74 100644 --- a/lib/__tests__/vcard.test.ts +++ b/lib/__tests__/vcard.test.ts @@ -220,6 +220,34 @@ describe("parseVCard", () => { ); }); + it("decodes the escaped colons Google Contacts writes (#1438)", () => { + const vcf = [ + "BEGIN:VCARD", + "VERSION:3.0", + "FN:Test Example", + "NOTE:Reference\\: fictional value\\nSecond line\\, with comma\\; and semicolon", + "END:VCARD", + ].join("\r\n"); + + const result = parseVCard(vcf); + expect(result[0].notes?.n0?.note).toBe( + "Reference: fictional value\nSecond line, with comma; and semicolon" + ); + }); + + it("keeps an escaped backslash literal when an escapable character follows", () => { + const vcf = [ + "BEGIN:VCARD", + "VERSION:3.0", + "FN:Paths", + "NOTE:C:\\\\new\\\\:dir\\\\\\:x\\\\\\\\", + "END:VCARD", + ].join("\r\n"); + + const result = parseVCard(vcf); + expect(result[0].notes?.n0?.note).toBe("C:\\new\\:dir\\:x\\\\"); + }); + it("allows group kind without name or email", () => { const vcf = [ "BEGIN:VCARD", diff --git a/lib/vcard.ts b/lib/vcard.ts index 869038040..ea664a802 100644 --- a/lib/vcard.ts +++ b/lib/vcard.ts @@ -204,12 +204,12 @@ function decodeQuotedPrintable(input: string, charset?: string): string { } } +// One pass, so an escaped backslash can't pair up with the next character. +// \: isn't a vCard escape, but Google Contacts writes it. function decodeValue(raw: string): string { - return raw - .replace(/\\n/gi, "\n") - .replace(/\\,/g, ",") - .replace(/\\;/g, ";") - .replace(/\\\\/g, "\\"); + return raw.replace(/\\([\\,;:nN])/g, (_, c: string) => + c === "n" || c === "N" ? "\n" : c + ); } function encodeValue(val: string): string {