Working with vCard contacts

Before the release of Thunderbird 102, contacts in Thunderbird’s address books only supported a fixed set of properties. All these properties where accessible through ContactProperties, extensions could store additional custom properties.

Since Thunderbird 102, contact details are stored as vCards and the former fixed properties are now referred to as legacy properties. Those legacy properties are mapped for read/write access to the best matching vCard property and are still accessible through ContactProperties.

The vCard is exposed in the vCard property and changing it will update the contact according to the newly set vCard string.

Important

Accessing contact details through legacy properties is deprecated. Newly added fields in the address book UI (e.g. the Timezone information), are not accessible through legacy properties, but only through the vCard.

When updating a contact and providing values for the vCard property as well as for legacy properties, the specified legacy properties are ignored.

Updating legacy properties

A vCard can store multiple values for each type and legacy properties point to the first entry of the associated type. Deleting the one which is currently exposed through a legacy property only deletes that single entry, not all entries. Consider a contact being updated and some of its legacy properties are cleared as follows:

await messenger.contacts.update(id, {
  "PrimaryEmail" : null,
  "HomePhone" : null
})

If the vCard had multiple email addresses or multiple home numbers, each next entry will now be exposed through the associated legacy property. This can lead to unexpected results, when setting SecondEmail on a contact which does not yet have any email entries:

await messenger.contacts.update(id, {
  "SecondEmail" : "[email protected]",
})

let { properties } = await messenger.contacts.get(id);
console.log(properties);

The console output will include PrimaryEmail: user@inter.net, but no value for SecondEmail, simply because PrimaryEmail points to the first email address stored in the vCard.

Updating the vCard property

Instead of parsing or manipulating the vCard string manually, we recommend to use the same library that Thunderbird itself is using for parsing. Add the ical.js file from the linked repository to your add-on and load it into your background. One way is to add it to the manifest as follows:

"background": {
  "scripts": [
    "ical.js",
    "background.js"
  ]
},

In the background.js script one can now parse vCard strings as follows:

// Get JSON representation of the vCard data.
let vCardObj = ICAL.parse("BEGIN:VCARD\r\nVERSION:4.0\r\nN:LastName;FirstName;;;\r\nEMAIL;PREF=1:[email protected]\r\nEND:VCARD\r\n");
let [ component, jCard ] = vCardObj;

/* ICAL.parse() return value:
 *
 * Array(3)
 *  0: "vcard"     // Name of the component.
 *  1: Array(4)    // Array of entries.
 *     0: Array(4) ["version", {}, "text", "4.0" ]
 *     1: Array(4) [ "n", {}, "text", [ "", "first", "", "", "" ] ]
 *     2: Array(4) [ "email", { pref: "1" }, "text", "[email protected]"]
 *  2: Array []    // Array of subcomponents, should be empty for vCard, used
 *                 // by vCalendar, which has vEvent subcomponents.
 */

// Manipulate the jCard object.
if (component == "vcard") {
  let email = jCard.find(e => e[0] == "email");
  if (email) {
    email[3] = "[email protected]"
  }
}

// Update the contact.
messenger.contacts.update(id, {vCard: ICAL.stringify(vCardObj)});

The ical library also supports manipulating the data on a higher level, using the Component class:

// Get JSON representation of the vCard data (jCal).
var vCard = new ICAL.Component(ICAL.parse("BEGIN:VCARD\r\nVERSION:4.0\r\nN:LastName;FirstName;;;\r\nEMAIL;PREF=1:[email protected]\r\nEND:VCARD\r\n"));

// Add an entry.
vCard.addPropertyWithValue("email", "[email protected]");

/* Other useful methods:
 *
 *  vCard.getFirstProperty("email")
 *  vCard.getFirstPropertyValue("email")
 *
 *  vCard.getAllProperties("email")
 *  vCard.removeAllProperties("email")
 *
 *  let emailEntry = new ICAL.Property(["email", { pref: "1" }, "text", "[email protected]"]);
 *  vCard.addProperty(emailEntry)
 *  vCard.addPropertyWithValue("email", "[email protected]")
 *
 *  vCard.removeProperty(emailEntry)
 */

// Update an entry.
let email = vCard.getAllProperties("email").find(e => e.jCal[3] == "[email protected]");
if (email) {
  // Option 1: Manipulate the existing jCal entry (Array(4), [name, options, type, value])
  email.jCal[3] = "[email protected]";
  // Option 2: Remove the existing entry and add a new one (changes order of entries)
  vCard.removeProperty(email);
  vCard.addProperty(new ICAL.Property(["email", {}, "text", "[email protected]"]);
}

// Update the contact.
messenger.contacts.update(id, {vCard: vCard.toString()});