fix: decode escaped colons in imported vcard notes

This commit is contained in:
Linus Rath
2026-10-29 19:52:28 +01:00
parent cd6724f754
commit 9d547b9d16
2 changed files with 31 additions and 5 deletions
+26
View File
@@ -74,6 +74,32 @@ describe('parseVCard', () => {
expect(note).toBe('line one\nline two, still going');
});
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 [card] = parseVCard(vcf);
expect(Object.values(card.notes ?? {})[0]?.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 [card] = parseVCard(vcf);
expect(Object.values(card.notes ?? {})[0]?.note).toBe('C:\\new\\:dir\\:x\\\\');
});
it('parses CATEGORIES into keywords', () => {
const vcf = 'BEGIN:VCARD\r\nVERSION:3.0\r\nFN:Tagged\r\nEMAIL:t@x.com\r\nCATEGORIES:Friends,VIP\r\nEND:VCARD';
const [card] = parseVCard(vcf);
+5 -5
View File
@@ -252,12 +252,12 @@ function decodeQuotedPrintable(input: string, charset?: string): string {
return decodeBytes(bytes, charset);
}
// 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 {