diff --git a/mail/components/addrbook/content/aboutAddressBook.js b/mail/components/addrbook/content/aboutAddressBook.js index 97a0946572..d0c62c121f 100644 --- a/mail/components/addrbook/content/aboutAddressBook.js +++ b/mail/components/addrbook/content/aboutAddressBook.js @@ -3032,7 +3032,12 @@ var detailsPane = { list.replaceChildren(); let formatDate = function(date) { - date = ICAL.VCardTime.fromDateAndOrTimeString(date); + try { + date = ICAL.VCardTime.fromDateAndOrTimeString(date); + } catch (ex) { + Cu.reportError(ex); + return ""; + } if (date.year && date.month && date.day) { return new Services.intl.DateTimeFormat( Services.locale.appLocalesAsBCP47, diff --git a/mail/components/addrbook/content/vcard-edit/special-date.js b/mail/components/addrbook/content/vcard-edit/special-date.js index 9d3a830870..1c7a081122 100644 --- a/mail/components/addrbook/content/vcard-edit/special-date.js +++ b/mail/components/addrbook/content/vcard-edit/special-date.js @@ -135,15 +135,20 @@ class VCardSpecialDateComponent extends HTMLElement { return; } // Default value is date-and-or-time. - let dateValue = ICAL.VCardTime.fromDateAndOrTimeString( - this.vCardPropertyEntry.value || "", - "date-and-or-time" - ); + let dateValue; + try { + dateValue = ICAL.VCardTime.fromDateAndOrTimeString( + this.vCardPropertyEntry.value || "", + "date-and-or-time" + ); + } catch (ex) { + Cu.reportError(ex); + } // Always set the month first since that controls the available days. - this.month.value = dateValue.month || ""; + this.month.value = dateValue?.month || ""; this.fillDayOptions(); - this.day.value = dateValue.day || ""; - this.year.value = dateValue.year || ""; + this.day.value = dateValue?.day || ""; + this.year.value = dateValue?.year || ""; } fromUIToVCardPropertyEntry() { diff --git a/mailnews/addrbook/modules/VCardUtils.jsm b/mailnews/addrbook/modules/VCardUtils.jsm index ef4b3f346b..49500264a1 100644 --- a/mailnews/addrbook/modules/VCardUtils.jsm +++ b/mailnews/addrbook/modules/VCardUtils.jsm @@ -403,10 +403,14 @@ function dateProperty(abCardPrefix, vPropName) { yield [vPropName, {}, "date", dateValue.toString()]; }, *toAbCard(value) { - let dateValue = ICAL.VCardTime.fromDateAndOrTimeString(value); - yield [`${abCardPrefix}Year`, String(dateValue.year)]; - yield [`${abCardPrefix}Month`, String(dateValue.month)]; - yield [`${abCardPrefix}Day`, String(dateValue.day)]; + try { + let dateValue = ICAL.VCardTime.fromDateAndOrTimeString(value); + yield [`${abCardPrefix}Year`, String(dateValue.year ?? "")]; + yield [`${abCardPrefix}Month`, String(dateValue.month ?? "")]; + yield [`${abCardPrefix}Day`, String(dateValue.day ?? "")]; + } catch (ex) { + Cu.reportError(ex); + } }, }; } diff --git a/mailnews/addrbook/test/unit/test_vCard.js b/mailnews/addrbook/test/unit/test_vCard.js index d472da4163..328be1c8cd 100644 --- a/mailnews/addrbook/test/unit/test_vCard.js +++ b/mailnews/addrbook/test/unit/test_vCard.js @@ -2,10 +2,294 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ -let { VCardUtils } = ChromeUtils.import("resource:///modules/VCardUtils.jsm"); +let { VCardProperties, VCardUtils } = ChromeUtils.import( + "resource:///modules/VCardUtils.jsm" +); const ANY_UID = "UID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; +add_task(function testVCardToPropertyMap() { + function check(vCardLine, expectedProps) { + let vCard = `BEGIN:VCARD\r\n${vCardLine}\r\nEND:VCARD\r\n`; + info(vCard); + let properties = VCardProperties.fromVCard(vCard).toPropertyMap(); + // Check that every property in expectedProps is present in `properties`. + // No other property can be present unless it is in `propWhitelist`. + for (let [name, value] of properties) { + if (name in expectedProps) { + Assert.equal(value, expectedProps[name], `expected ${name}`); + delete expectedProps[name]; + } else { + Assert.ok(false, `card should not have property '${name}'`); + } + } + + for (let name of Object.keys(expectedProps)) { + Assert.ok(false, `expected ${name} not found`); + } + } + + // Name + check("N:Last;First", { FirstName: "First", LastName: "Last" }); + check("N:Last;First;;;", { FirstName: "First", LastName: "Last" }); + check("N:Last;First;Middle;Prefix;Suffix", { + FirstName: "First", + LastName: "Last", + AdditionalNames: "Middle", + NamePrefix: "Prefix", + NameSuffix: "Suffix", + }); + check("N:Stevenson;John;Philip,Paul;Dr.;Jr.,M.D.,A.C.P.", { + FirstName: "John", + LastName: "Stevenson", + AdditionalNames: "Philip Paul", + NamePrefix: "Dr.", + NameSuffix: "Jr. M.D. A.C.P.", + }); + + // Address + check( + "ADR:PO Box 3.14;Apartment 4;123 Main Street;Any Town;CA;91921-1234;U.S.A.", + { + WorkPOBox: "PO Box 3.14", + WorkAddress2: "Apartment 4", + WorkAddress: "123 Main Street", + WorkCity: "Any Town", + WorkState: "CA", + WorkZipCode: "91921-1234", + WorkCountry: "U.S.A.", + } + ); + check( + "ADR;TYPE=work:PO Box 3.14;Apartment 4;123 Main Street;Any Town;CA;91921-1234;U.S.A.", + { + WorkPOBox: "PO Box 3.14", + WorkAddress2: "Apartment 4", + WorkAddress: "123 Main Street", + WorkCity: "Any Town", + WorkState: "CA", + WorkZipCode: "91921-1234", + WorkCountry: "U.S.A.", + } + ); + check( + "ADR;TYPE=home:PO Box 3.14;Apartment 4;123 Main Street;Any Town;CA;91921-1234;U.S.A.", + { + HomePOBox: "PO Box 3.14", + HomeAddress2: "Apartment 4", + HomeAddress: "123 Main Street", + HomeCity: "Any Town", + HomeState: "CA", + HomeZipCode: "91921-1234", + HomeCountry: "U.S.A.", + } + ); + + // Phone + check("TEL:11-2358-13-21", { WorkPhone: "11-2358-13-21" }); + check("TEL;TYPE=work:11-2358-13-21", { WorkPhone: "11-2358-13-21" }); + check("TEL;TYPE=home:11-2358-13-21", { HomePhone: "11-2358-13-21" }); + check("TEL;TYPE=cell:11-2358-13-21", { CellularNumber: "11-2358-13-21" }); + check("TEL;TYPE=pager:11-2358-13-21", { PagerNumber: "11-2358-13-21" }); + check("TEL;TYPE=fax:11-2358-13-21", { FaxNumber: "11-2358-13-21" }); + + check("TEL;TYPE=work;PREF:11-2358-13-21", { WorkPhone: "11-2358-13-21" }); + check("TEL;TYPE=work,cell:11-2358-13-21", { WorkPhone: "11-2358-13-21" }); + check("TEL;TYPE=work;TYPE=cell:11-2358-13-21", { + WorkPhone: "11-2358-13-21", + }); + check("TEL;TYPE=work;VALUE=TEXT:11-2358-13-21", { + WorkPhone: "11-2358-13-21", + }); + check("TEL;TYPE=home;VALUE=TEXT:011-2358-13-21", { + HomePhone: "011-2358-13-21", + }); + check( + "TEL;TYPE=work;VALUE=TEXT:11-2358-13-21\r\nTEL;TYPE=home;VALUE=TEXT:011-2358-13-21", + { + WorkPhone: "11-2358-13-21", + HomePhone: "011-2358-13-21", + } + ); + check("TEL;TYPE=cell:11-2358-13-21\r\nTEL;TYPE=cell:011-2358-13-21", { + CellularNumber: "11-2358-13-21", + }); + check("TEL;TYPE=cell;PREF=1:11-2358-13-21\r\nTEL;TYPE=cell:011-2358-13-21", { + CellularNumber: "11-2358-13-21", + }); + check("TEL;TYPE=cell:11-2358-13-21\r\nTEL;TYPE=cell;PREF=1:011-2358-13-21", { + CellularNumber: "011-2358-13-21", + }); + + // Birthday + check("BDAY;VALUE=DATE:19830403", { + BirthDay: "3", + BirthMonth: "4", + BirthYear: "1983", + }); + check("BDAY:--0415", { BirthDay: "15", BirthMonth: "4" }); + check("BDAY:2001", { BirthYear: "2001" }); + check("BDAY:2006-06", { BirthYear: "2006", BirthMonth: "6" }); + check("BDAY:--12", { BirthMonth: "12" }); + check("BDAY:---30", { BirthDay: "30" }); + // These are error cases, testing that it doesn't throw. + check("BDAY;VALUE=DATE:NaN-NaN-NaN", {}); + check("BDAY;VALUE=TEXT:07/07/1949", {}); + + // Anniversary + check("ANNIVERSARY;VALUE=DATE:20041207", { + AnniversaryDay: "7", + AnniversaryMonth: "12", + AnniversaryYear: "2004", + }); + + // Organization: any number of values is valid here. + check("ORG:Acme Widgets, Inc.", { + Company: "Acme Widgets, Inc.", + }); + check("ORG:Acme Widgets, Inc.;Manufacturing", { + Company: "Acme Widgets, Inc.", + Department: "Manufacturing", + }); + check("ORG:Acme Widgets, Inc.;Manufacturing;Thingamies", { + Company: "Acme Widgets, Inc.", + Department: "Manufacturing", + }); + + // URL + // If no type is given assume its WebPage1 (work). + check("URL:https://www.thunderbird.net/", { + WebPage1: "https://www.thunderbird.net/", + }); + + check("URL;TYPE=work:https://developer.thunderbird.net/", { + WebPage1: "https://developer.thunderbird.net/", + }); + + check("URL;TYPE=home:https://addons.thunderbird.net/", { + WebPage2: "https://addons.thunderbird.net/", + }); + + check( + formatVCard` + URL;TYPE=home:https://addons.thunderbird.net/ + URL;TYPE=work:https://developer.thunderbird.net/`, + { + WebPage1: "https://developer.thunderbird.net/", + WebPage2: "https://addons.thunderbird.net/", + } + ); + + // If a URL without a type is given and a Work Web Page do not import the URL without type. + check( + formatVCard` + URL:https://www.thunderbird.net/ + URL;TYPE=home:https://addons.thunderbird.net/ + URL;TYPE=work:https://developer.thunderbird.net/`, + { + WebPage1: "https://developer.thunderbird.net/", + WebPage2: "https://addons.thunderbird.net/", + } + ); + // Email: just to be difficult, email is stored by priority, not type. + check("EMAIL:first@invalid", { PrimaryEmail: "first@invalid" }); + check("EMAIL;PREF=1:first@invalid", { PrimaryEmail: "first@invalid" }); + + check("EMAIL;PREF=1:first@invalid\r\nEMAIL:second@invalid", { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + }); + check("EMAIL:second@invalid\r\nEMAIL;PREF=1:first@invalid", { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + }); + + check("EMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=2:second@invalid", { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + }); + check("EMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=1:first@invalid", { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + }); + + check( + "EMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=3:third@invalid", + { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + } + ); + check( + "EMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=3:third@invalid\r\nEMAIL;PREF=1:first@invalid", + { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + } + ); + check( + "EMAIL;PREF=3:third@invalid\r\nEMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=2:second@invalid", + { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + } + ); + check( + "EMAIL;PREF=3:third@invalid\r\nEMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=1:first@invalid", + { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + } + ); + check( + "EMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=3:third@invalid", + { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + } + ); + check( + "EMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=3:third@invalid\r\nEMAIL;PREF=2:second@invalid", + { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + } + ); + + // Group-prefixed properties. + check( + formatVCard` + item1.EMAIL:first@invalid + item1.X-ABLabel:First`, + { + PrimaryEmail: "first@invalid", + } + ); + check( + formatVCard` + item1.EMAIL:first@invalid + item1.X-ABLabel:First + item2.EMAIL:second@invalid + item2.X-ABLabel:Second`, + { PrimaryEmail: "first@invalid", SecondEmail: "second@invalid" } + ); + check( + formatVCard` + foo-bar.EMAIL:first@invalid + foo-bar.X-ABLabel:First + EMAIL:second@invalid`, + { PrimaryEmail: "first@invalid", SecondEmail: "second@invalid" } + ); + check( + formatVCard` + EMAIL:first@invalid + abc.EMAIL:second@invalid + abc.X-ABLabel:Second`, + { PrimaryEmail: "first@invalid", SecondEmail: "second@invalid" } + ); + check("xyz.TEL:11-2358-13-21", { WorkPhone: "11-2358-13-21" }); +}); + add_task(function testAbCardToVCard() { function check(abCardProps, ...expectedLines) { let abCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(