1 import { parseISO } from 'date-fns';
3 import { ImportContactError } from '../../lib/contacts/errors/ImportContactError';
5 extractContactImportCategories,
9 naiveExtractPropertyValue,
10 } from '../../lib/contacts/helpers/import';
11 import { fromVCardProperties, getVCardProperties } from '../../lib/contacts/properties';
12 import { extractVcards } from '../../lib/contacts/vcard';
13 import { toCRLF } from '../../lib/helpers/string';
14 import type { ContactMetadata, EncryptedContact, ImportedContact } from '../../lib/interfaces/contacts';
15 import type { VCardContact, VCardProperty } from '../../lib/interfaces/contacts/VCard';
17 const excludeUids = (contact: VCardContact | ImportContactError) => {
18 if (contact instanceof ImportContactError) {
21 const properties = getVCardProperties(contact).map(({ uid, ...property }) => property);
22 return fromVCardProperties(properties as VCardProperty[]);
25 describe('import', () => {
26 describe('extract vcards', () => {
27 it('should keep the line separator used in the vcard', () => {
28 const vcardsPlain = `BEGIN:VCARD
36 const vcardsCRLF = toCRLF(vcardsPlain);
37 expect(extractVcards(vcardsPlain)).toEqual([
38 'BEGIN:VCARD\nVERSION:4.0\nFN:One\nEND:VCARD',
39 'BEGIN:VCARD\nVERSION:4.0\nFN:Two\nEND:VCARD',
41 expect(extractVcards(vcardsCRLF)).toEqual([
42 'BEGIN:VCARD\r\nVERSION:4.0\r\nFN:One\r\nEND:VCARD',
43 'BEGIN:VCARD\r\nVERSION:4.0\r\nFN:Two\r\nEND:VCARD',
47 it('extracts vcards separated by empty lines', () => {
48 const vcardsPlain = `BEGIN:VCARD
58 expect(extractVcards(vcardsPlain)).toEqual([
59 'BEGIN:VCARD\nVERSION:4.0\nFN:One\nEND:VCARD',
60 'BEGIN:VCARD\nVERSION:4.0\nFN:Two\nEND:VCARD',
65 describe('naiveExtractPropertyValue', () => {
66 const vcard = `BEGIN:VCARD
68 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
72 EMAIL;PID=1.1:jdoe@example.com
73 CLIENTPIDMAP:1;urn:uuid:53e374d9-337e-4727-8803-a1e9c14e0556
74 NOTE:This is a long descrip
79 it('should return undefined when the property is not present', () => {
80 expect(naiveExtractPropertyValue(vcard, 'CATEGORIES')).toBeUndefined();
83 it('should return the empty string when the property has no value', () => {
84 expect(naiveExtractPropertyValue(vcard, 'BDAY')).toEqual('');
87 it('should extract values as expected with and without parameters', () => {
88 expect(naiveExtractPropertyValue(vcard, 'UID')).toEqual('urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1');
89 expect(naiveExtractPropertyValue(vcard, 'email')).toEqual('jdoe@example.com');
90 expect(naiveExtractPropertyValue(vcard, 'version')).toEqual('4.0');
93 it('should extract folded values', () => {
94 expect(naiveExtractPropertyValue(vcard, 'NOTE')).toEqual(
95 'This is a long description that exists on a long line.'
100 describe('getContactId', () => {
101 it('should retrieve FN value whenever present', () => {
103 getContactId(`BEGIN:VCARD
105 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
108 EMAIL;PID=1.1:jdoe@example.com
109 CLIENTPIDMAP:1;urn:uuid:53e374d9-337e-4727-8803-a1e9c14e0556
113 getContactId(`BEGIN:VCARD
116 N:Perreault;Simon;;;ing. jr,M.Sc.
118 ANNIVERSARY:20090808T1430-0500
122 ORG;TYPE=work:Viagenie
123 ADR;TYPE=work:;Suite D2-630;2875 Laurier;
124 Quebec;QC;G1V 2M2;Canada
125 TEL;VALUE=uri;TYPE="work,voice";PREF=1:tel:+1-418-656-9254;ext=102
126 TEL;VALUE=uri;TYPE="work,cell,voice,video,text":tel:+1-418-262-6501
127 EMAIL;TYPE=work:simon.perreault@viagenie.ca
128 GEO;TYPE=work:geo:46.772673,-71.282945
129 KEY;TYPE=work;VALUE=uri:
130 http://www.viagenie.ca/simon.perreault/simon.asc
132 URL;TYPE=home:http://nomis80.org
134 ).toEqual('Simon Perreault');
137 it('should retrieve FN when multiple lines are present', () => {
139 getContactId(`BEGIN:VCARD
141 UID:urn:uuid:4fbe8971-0bc3
149 EMAIL;PID=1.1:jdoe@example.com
150 CLIENTPIDMAP:1;urn:uuid:53e374d9-337e-4727-8803-a1e9c14e0556
152 ).toEqual('Johnnie Doe');
155 it('should crop FN when too long', () => {
157 getContactId(`BEGIN:VCARD
159 UID:urn:uuid:4fbe8971-0bc3
162 FN;PID=1.1:This contact has a very loo
163 ong name, but that's a pity since no
164 one will remember such a long one
166 EMAIL;PID=1.1:jdoe@example.com
167 CLIENTPIDMAP:1;urn:uuid:53e374d9-337e-4727-8803-a1e9c14e0556
169 ).toEqual('This contact has a very looong name, bu…');
173 describe('extractCategories', () => {
174 it('should pick the right contactEmail ids for a contact with categories', () => {
180 Email: 'one@email.test',
184 Email: 'two@email.test',
188 Email: 'three@email.test',
191 } as ContactMetadata;
192 const encryptedContact = {
194 { email: 'one@email.test', group: 'item1' },
195 { email: 'two@email.test', group: 'item2' },
196 { email: 'three@email.test', group: 'item3' },
199 { name: 'dogs', group: 'item1' },
200 { name: 'cats', group: 'item2' },
201 { name: 'cats', group: 'item3' },
202 { name: 'pets', group: 'item2' },
205 } as EncryptedContact;
207 { name: 'dogs', contactEmailIDs: ['one'] },
208 { name: 'cats', contactEmailIDs: ['two', 'three'] },
209 { name: 'pets', contactEmailIDs: ['two'] },
210 { name: 'all', contactEmailIDs: ['one', 'two', 'three'] },
212 expect(extractContactImportCategories(contact, encryptedContact)).toEqual(result);
216 it('should pick the right contactEmail ids for a contact with categories', () => {
222 Email: 'one@email.test',
226 Email: 'two@email.test',
230 Email: 'three@email.test',
233 } as ContactMetadata;
234 const encryptedContact = {
236 { email: 'one@email.test', group: 'item1' },
237 { email: 'two@email.test', group: 'item2' },
238 { email: 'three@email.test', group: 'item3' },
241 } as unknown as EncryptedContact;
242 expect(extractContactImportCategories(contact, encryptedContact)).toEqual([]);
245 describe('getImportCategories', () => {
246 it('should combine contact email ids and contact ids from different contacts', () => {
247 const contacts: ImportedContact[] = [
249 contactID: 'contact1',
250 contactEmailIDs: ['contactemail1-1', 'contactemail1-2'],
252 { name: 'cats', contactEmailIDs: ['contactemail1-1', 'contactemail1-2'] },
253 { name: 'dogs', contactEmailIDs: ['contactemail1-1'] },
258 contactID: 'contact2',
260 categories: [{ name: 'dogs' }, { name: 'birds' }],
263 contactID: 'contact3',
264 contactEmailIDs: ['contactemail3-1', 'contactemail3-2'],
267 { name: 'dogs', contactEmailIDs: ['contactemail3-1'] },
268 { name: 'pets', contactEmailIDs: ['contactemail3-2'] },
276 contactIDs: ['contact1'],
281 contactEmailIDs: ['contactemail1-1', 'contactemail3-1'],
285 { name: 'pets', contactEmailIDs: ['contactemail3-2'], contactIDs: [], totalContacts: 1 },
287 expect(getImportCategories(contacts)).toEqual(result);
291 describe('getSupportedContacts', () => {
292 const getExpectedProperties = (withLineBreaks = false): VCardContact => {
294 fn: [{ field: 'fn', value: 'Name', params: { pref: '1' }, uid: '' }],
295 version: { field: 'version', value: '4.0', uid: '' },
302 streetAddress: withLineBreaks ? 'street with line breaks' : 'street',
311 org: [{ field: 'org', value: { organizationalName: 'company' }, uid: '' }],
312 bday: { field: 'bday', value: { date: parseISO('1999-01-01') }, uid: '' },
313 note: [{ field: 'note', value: 'Notes', uid: '' }],
315 { field: 'email', value: 'email1@protonmail.com', params: { pref: '1' }, group: 'item1', uid: '' },
317 tel: [{ field: 'tel', value: '00 00000000', params: { pref: '1' }, uid: '' }],
318 title: [{ field: 'title', value: 'title', uid: '' }],
322 it('should import normal vCard correctly', () => {
323 const vCard = `BEGIN:VCARD
325 ADR:;;street;city;;00000;FR
329 TEL;PREF=1:00 00000000
332 ITEM1.EMAIL;PREF=1:email1@protonmail.com
335 const expected = getExpectedProperties();
337 const contact = getSupportedContact(vCard);
339 expect(excludeUids(contact)).toEqual(excludeUids(expected));
342 it('should import vCard with address containing \\r\\n correctly', () => {
343 const vCard = `BEGIN:VCARD
345 ADR:;;street\\r\\nwith\\r\\nline\\r\\nbreaks;city;;00000;FR
349 TEL;PREF=1:00 00000000
352 ITEM1.EMAIL;PREF=1:email1@protonmail.com
355 const expected = getExpectedProperties(true);
357 const contact = getSupportedContact(vCard);
359 expect(excludeUids(contact)).toEqual(excludeUids(expected));
362 it('should import vCard with address containing \\\\n correctly', () => {
363 const vCard = `BEGIN:VCARD
365 ADR:;;street\\nwith\\nline\\nbreaks;city;;00000;FR
369 TEL;PREF=1:00 00000000
372 ITEM1.EMAIL;PREF=1:email1@protonmail.com
375 const expected = getExpectedProperties(true);
377 const contact = getSupportedContact(vCard);
379 expect(excludeUids(contact)).toEqual(excludeUids(expected));
383 it('should import BDAY and ANNIVERSARY', () => {
384 const vCard = `BEGIN:VCARD
391 const expected: VCardContact = {
392 fn: [{ field: 'fn', value: 'Name', uid: '' }],
393 version: { field: 'version', value: '4.0', uid: '' },
394 bday: { field: 'bday', value: { date: parseISO('1999-01-01') }, uid: '' },
395 anniversary: { field: 'anniversary', value: { date: parseISO('1999-01-01') }, uid: '' },
398 const contact = getSupportedContact(vCard);
400 expect(excludeUids(contact)).toEqual(excludeUids(expected));
403 it('should import BDAY and ANNIVERSARY with text format', () => {
404 const vCard = `BEGIN:VCARD
407 BDAY;VALUE=text:bidet
408 ANNIVERSARY;VALUE=text:annie
411 const expected: VCardContact = {
412 fn: [{ field: 'fn', value: 'Name', uid: '' }],
413 version: { field: 'version', value: '4.0', uid: '' },
414 bday: { field: 'bday', value: { text: 'bidet' }, params: { type: 'text' }, uid: '' },
415 anniversary: { field: 'anniversary', value: { text: 'annie' }, params: { type: 'text' }, uid: '' },
418 const contact = getSupportedContact(vCard);
420 expect(excludeUids(contact)).toEqual(excludeUids(expected));
423 it('should import the contact using email as FN when no FN specified', () => {
424 const vCard = `BEGIN:VCARD
426 ITEM1.EMAIL;PREF=1:email1@protonmail.com
429 const expected: VCardContact = {
430 fn: [{ field: 'fn', value: 'email1@protonmail.com', uid: '' }],
431 version: { field: 'version', value: '4.0', uid: '' },
432 email: [{ field: 'email', value: 'email1@protonmail.com', params: { pref: '1' }, group: 'item1', uid: '' }],
435 const contact = getSupportedContact(vCard);
437 expect(excludeUids(contact)).toEqual(excludeUids(expected));
440 it(`should not import a contact with a missing FN for which we can't find an alternative`, () => {
441 const vCard = `BEGIN:VCARD
443 CATEGORIES:MISSING_FN,MISSING_EMAIL
447 expect(() => getSupportedContact(vCard)).toThrowError('Missing FN property');
450 it(`should not import a contact with version 2.1`, () => {
451 const vCard = `BEGIN:VCARD
455 ORG:Bubba Gump Shrimp Co.
457 TEL;WORK;VOICE:(111) 555-1212
458 TEL;HOME;VOICE:(404) 555-1212
459 ADR;WORK;PREF:;;100 Waters Edge;Baytown;LA;30314;United States of America
460 LABEL;WORK;PREF;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:100 Waters Edge=0D=
461 =0ABaytown\\, LA 30314=0D=0AUnited States of America
462 ADR;HOME:;;42 Plantation St.;Baytown;LA;30314;United States of America
463 LABEL;HOME;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:42 Plantation St.=0D=0A=
464 Baytown, LA 30314=0D=0AUnited States of America
465 PHOTO;GIF:http://www.example.com/dir_photos/my_photo.gif
466 EMAIL:forrestgump@example.com
469 expect(() => getSupportedContact(vCard)).toThrowError('vCard versions < 3.0 not supported');