diff options
Diffstat (limited to '')
-rw-r--r-- | comm/mailnews/addrbook/modules/VCardUtils.jsm | 973 |
1 files changed, 973 insertions, 0 deletions
diff --git a/comm/mailnews/addrbook/modules/VCardUtils.jsm b/comm/mailnews/addrbook/modules/VCardUtils.jsm new file mode 100644 index 0000000000..a3ff0f5e14 --- /dev/null +++ b/comm/mailnews/addrbook/modules/VCardUtils.jsm @@ -0,0 +1,973 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +const EXPORTED_SYMBOLS = [ + "VCardService", + "VCardMimeConverter", + "VCardProperties", + "VCardPropertyEntry", + "VCardUtils", + "BANISHED_PROPERTIES", +]; + +const { ICAL } = ChromeUtils.import("resource:///modules/calendar/Ical.jsm"); + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + AddrBookCard: "resource:///modules/AddrBookCard.jsm", +}); + +/** + * Utilities for working with vCard data. This file uses ICAL.js as parser and + * formatter to avoid reinventing the wheel. + * + * @see RFC 6350. + */ + +var VCardUtils = { + _decodeQuotedPrintable(value) { + let bytes = []; + for (let b = 0; b < value.length; b++) { + if (value[b] == "=") { + bytes.push(parseInt(value.substr(b + 1, 2), 16)); + b += 2; + } else { + bytes.push(value.charCodeAt(b)); + } + } + return new TextDecoder().decode(new Uint8Array(bytes)); + }, + _parse(vProps) { + let vPropMap = new Map(); + for (let index = 0; index < vProps.length; index++) { + let { name, params, value } = vProps[index]; + + // Work out which type in typeMap, if any, this property belongs to. + + // To make the next piece easier, the type param must always be an array + // of lower-case strings. + let type = params.type || []; + if (type) { + if (Array.isArray(type)) { + type = type.map(t => t.toLowerCase()); + } else { + type = [type.toLowerCase()]; + } + } + + // Special cases for address and telephone types. + if (name == "adr") { + name = type.includes("home") ? "adr.home" : "adr.work"; + } + if (name == "tel") { + name = "tel.work"; + for (let t of type) { + if (["home", "work", "cell", "pager", "fax"].includes(t)) { + name = `tel.${t}`; + break; + } + } + } + // Preserve URL if no URL with type work is given take for `url.work` the URL without any type. + if (name == "url") { + name = type.includes("home") ? "url.home" : name; + name = type.includes("work") ? "url.work" : name; + } + + // Special treatment for `url`, which is not in the typeMap. + if (!(name in typeMap) && name != "url") { + continue; + } + + // The preference param is 1-100, lower numbers indicate higher + // preference. If not specified, the value is least preferred. + let pref = parseInt(params.pref, 10) || 101; + + if (!vPropMap.has(name)) { + vPropMap.set(name, []); + } + vPropMap.get(name).push({ index, pref, value }); + } + + // If no URL with type is specified assume its the Work Web Page (WebPage 1). + if (vPropMap.has("url") && !vPropMap.has("url.work")) { + vPropMap.set("url.work", vPropMap.get("url")); + } + // AbCard only supports Work Web Page or Home Web Page. Get rid of the URL without type. + vPropMap.delete("url"); + + for (let props of vPropMap.values()) { + // Sort the properties by preference, or by the order they appeared. + props.sort((a, b) => { + if (a.pref == b.pref) { + return a.index - b.index; + } + return a.pref - b.pref; + }); + } + return vPropMap; + }, + /** + * ICAL.js's parser only supports vCard 3.0 and 4.0. To maintain + * interoperability with other applications, here we convert vCard 2.1 + * cards into a "good-enough" mimic of vCard 4.0 so that the parser will + * read it without throwing an error. + * + * @param {string} vCard + * @returns {string} + */ + translateVCard21(vCard) { + if (!/\bVERSION:2.1\b/i.test(vCard)) { + return vCard; + } + + // Convert known type parameters to valid vCard 4.0, ignore unknown ones. + vCard = vCard.replace(/\n(([A-Z]+)(;[\w-]*)+):/gi, (match, key) => { + let parts = key.split(";"); + let newParts = [parts[0]]; + for (let i = 1; i < parts.length; i++) { + if (parts[i] == "") { + continue; + } + if ( + ["HOME", "WORK", "FAX", "PAGER", "CELL"].includes( + parts[i].toUpperCase() + ) + ) { + newParts.push(`TYPE=${parts[i]}`); + } else if (parts[i].toUpperCase() == "PREF") { + newParts.push("PREF=1"); + } else if (parts[i].toUpperCase() == "QUOTED-PRINTABLE") { + newParts.push("ENCODING=QUOTED-PRINTABLE"); + } + } + return "\n" + newParts.join(";") + ":"; + }); + + // Join quoted-printable wrapped lines together. This regular expression + // only matches lines that are quoted-printable and end with `=`. + let quotedNewLineRegExp = /(;ENCODING=QUOTED-PRINTABLE[;:][^\r\n]*)=\r?\n/i; + while (vCard.match(quotedNewLineRegExp)) { + vCard = vCard.replace(quotedNewLineRegExp, "$1"); + } + + // Strip the version. + return vCard.replace(/(\r?\n)VERSION:2.1\r?\n/i, "$1"); + }, + /** + * Return a new AddrBookCard from the provided vCard string. + * + * @param {string} vCard - The vCard string. + * @param {string} [uid] - An optional UID to be used for the new card, + * overriding any UID specified in the vCard string. + * @returns {AddrBookCard} + */ + vCardToAbCard(vCard, uid) { + vCard = this.translateVCard21(vCard); + + let abCard = new lazy.AddrBookCard(); + abCard.setProperty("_vCard", vCard); + + let vCardUID = abCard.vCardProperties.getFirstValue("uid"); + if (uid || vCardUID) { + abCard.UID = uid || vCardUID; + if (abCard.UID != vCardUID) { + abCard.vCardProperties.clearValues("uid"); + abCard.vCardProperties.addValue("uid", abCard.UID); + } + } + + return abCard; + }, + abCardToVCard(abCard, version) { + if (abCard.supportsVCard && abCard.getProperty("_vCard")) { + return abCard.vCardProperties.toVCard(); + } + + // Collect all of the AB card properties into a Map. + let abProps = new Map( + Array.from(abCard.properties, p => [p.name, p.value]) + ); + abProps.set("UID", abCard.UID); + + return this.propertyMapToVCard(abProps, version); + }, + propertyMapToVCard(abProps, version = "4.0") { + let vProps = [["version", {}, "text", version]]; + + // Add the properties to the vCard. + for (let vPropName of Object.keys(typeMap)) { + for (let vProp of typeMap[vPropName].fromAbCard(abProps, vPropName)) { + if (vProp[3] !== null && vProp[3] !== undefined && vProp[3] !== "") { + vProps.push(vProp); + } + } + } + + // If there's only one address or telephone number, don't specify type. + let adrProps = vProps.filter(p => p[0] == "adr"); + if (adrProps.length == 1) { + delete adrProps[0][1].type; + } + let telProps = vProps.filter(p => p[0] == "tel"); + if (telProps.length == 1) { + delete telProps[0][1].type; + } + + if (abProps.has("UID")) { + vProps.push(["uid", {}, "text", abProps.get("UID")]); + } + return ICAL.stringify(["vcard", vProps]); + }, +}; + +function VCardService() {} +VCardService.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIMsgVCardService"]), + classID: Components.ID("{e2e0f615-bc5a-4441-a16b-a26e75949376}"), + + vCardToAbCard(vCard) { + return vCard ? VCardUtils.vCardToAbCard(vCard) : null; + }, + escapedVCardToAbCard(vCard) { + return vCard ? VCardUtils.vCardToAbCard(decodeURIComponent(vCard)) : null; + }, + abCardToEscapedVCard(abCard) { + return abCard ? encodeURIComponent(VCardUtils.abCardToVCard(abCard)) : null; + }, +}; + +function VCardMimeConverter() {} +VCardMimeConverter.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsISimpleMimeConverter"]), + classID: Components.ID("{dafab386-bd4c-4238-bb48-228fbc98ba29}"), + + mailChannel: null, + uri: null, + convertToHTML(contentType, data) { + function escapeHTML(template, ...parts) { + let arr = []; + for (let i = 0; i < parts.length; i++) { + arr.push(template[i]); + arr.push( + parts[i] + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(/</g, "<") + .replace(/>/g, ">") + ); + } + arr.push(template[template.length - 1]); + return arr.join(""); + } + + let abCard; + try { + abCard = VCardUtils.vCardToAbCard(data); + } catch (e) { + // We were given invalid vcard data. + return ""; + } + + let escapedVCard = encodeURIComponent(data); + + let propertiesTable = `<table class="moz-vcard-properties-table">`; + propertiesTable += escapeHTML`<tr><td class="moz-vcard-title-property">${abCard.displayName}`; + if (abCard.primaryEmail) { + propertiesTable += escapeHTML` <<a href="mailto:${abCard.primaryEmail}" private>${abCard.primaryEmail}</a>>`; + } + propertiesTable += `</td></tr>`; + for (let propName of ["JobTitle", "Department", "Company"]) { + let propValue = abCard.getProperty(propName, ""); + if (propValue) { + propertiesTable += escapeHTML`<tr><td class="moz-vcard-property">${propValue}</td></tr>`; + } + } + propertiesTable += `</table>`; + + // VCardChild.jsm and VCardParent.jsm handle clicking on this link. + return `<html> + <body> + <table class="moz-vcard-table"> + <tr> + <td valign="top"><a class="moz-vcard-badge" href="data:text/vcard,${escapedVCard}"></a></td> + <td> + ${propertiesTable} + </td> + </tr> + </table> + </body> + </html>`; + }, +}; + +const BANISHED_PROPERTIES = [ + "UID", + "PrimaryEmail", + "SecondEmail", + "DisplayName", + "NickName", + "Notes", + "Company", + "Department", + "JobTitle", + "BirthDay", + "BirthMonth", + "BirthYear", + "AnniversaryDay", + "AnniversaryMonth", + "AnniversaryYear", + "LastName", + "FirstName", + "AdditionalNames", + "NamePrefix", + "NameSuffix", + "HomePOBox", + "HomeAddress2", + "HomeAddress", + "HomeCity", + "HomeState", + "HomeZipCode", + "HomeCountry", + "WorkPOBox", + "WorkAddress2", + "WorkAddress", + "WorkCity", + "WorkState", + "WorkZipCode", + "WorkCountry", + "HomePhone", + "WorkPhone", + "FaxNumber", + "PagerNumber", + "CellularNumber", + "WebPage1", + "WebPage2", + "Custom1", + "Custom2", + "Custom3", + "Custom4", +]; + +/** Helper functions for typeMap. */ + +function singleTextProperty( + abPropName, + vPropName, + vPropParams = {}, + vPropType = "text" +) { + return { + /** + * Formats nsIAbCard properties into an array for use by ICAL.js. + * + * @param {Map} map - A map of address book properties to map. + * @yields {Array} - Values in a jCard array for use with ICAL.js. + */ + *fromAbCard(map) { + yield [vPropName, { ...vPropParams }, vPropType, map.get(abPropName)]; + }, + /** + * Parses a vCard value into properties usable by nsIAbCard. + * + * @param {string} value - vCard string to map to an address book card property. + * @yields {string[]} - Any number of key, value pairs to set on the nsIAbCard. + */ + *toAbCard(value) { + if (typeof value != "string") { + console.warn(`Unexpected value for ${vPropName}: ${value}`); + return; + } + yield [abPropName, value]; + }, + }; +} +function dateProperty(abCardPrefix, vPropName) { + return { + *fromAbCard(map) { + let year = map.get(`${abCardPrefix}Year`); + let month = map.get(`${abCardPrefix}Month`); + let day = map.get(`${abCardPrefix}Day`); + + if (!year && !month && !day) { + return; + } + + let dateValue = new ICAL.VCardTime({}, null, "date"); + // Set the properties directly instead of using the VCardTime + // constructor argument, which causes null values to become 0. + dateValue.year = year ? Number(year) : null; + dateValue.month = month ? Number(month) : null; + dateValue.day = day ? Number(day) : null; + + yield [vPropName, {}, "date", dateValue.toString()]; + }, + *toAbCard(value) { + 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) { + console.error(ex); + } + }, + }; +} +function multiTextProperty(abPropNames, vPropName, vPropParams = {}) { + return { + *fromAbCard(map) { + if (abPropNames.every(name => !map.has(name))) { + return; + } + let vPropValues = abPropNames.map(name => map.get(name) || ""); + if (vPropValues.some(Boolean)) { + yield [vPropName, { ...vPropParams }, "text", vPropValues]; + } + }, + *toAbCard(value) { + if (Array.isArray(value)) { + for (let abPropName of abPropNames) { + let valuePart = value.shift(); + if (abPropName && valuePart) { + yield [ + abPropName, + Array.isArray(valuePart) ? valuePart.join(" ") : valuePart, + ]; + } + } + } else if (typeof value == "string") { + // Only one value was given. + yield [abPropNames[0], value]; + } else { + console.warn(`Unexpected value for ${vPropName}: ${value}`); + } + }, + }; +} + +/** + * Properties we support for conversion between nsIAbCard and vCard. + * + * Keys correspond to vCard property keys, with the type appended where more + * than one type is supported (e.g. work and home). + * + * Values are objects with toAbCard and fromAbCard functions which convert + * property values in each direction. See the docs on the object returned by + * singleTextProperty. + */ +var typeMap = { + fn: singleTextProperty("DisplayName", "fn"), + email: { + *fromAbCard(map) { + yield ["email", { pref: "1" }, "text", map.get("PrimaryEmail")]; + yield ["email", {}, "text", map.get("SecondEmail")]; + }, + toAbCard: singleTextProperty("PrimaryEmail", "email", { pref: "1" }) + .toAbCard, + }, + nickname: singleTextProperty("NickName", "nickname"), + note: singleTextProperty("Notes", "note"), + org: multiTextProperty(["Company", "Department"], "org"), + title: singleTextProperty("JobTitle", "title"), + bday: dateProperty("Birth", "bday"), + anniversary: dateProperty("Anniversary", "anniversary"), + n: multiTextProperty( + ["LastName", "FirstName", "AdditionalNames", "NamePrefix", "NameSuffix"], + "n" + ), + "adr.home": multiTextProperty( + [ + "HomePOBox", + "HomeAddress2", + "HomeAddress", + "HomeCity", + "HomeState", + "HomeZipCode", + "HomeCountry", + ], + "adr", + { type: "home" } + ), + "adr.work": multiTextProperty( + [ + "WorkPOBox", + "WorkAddress2", + "WorkAddress", + "WorkCity", + "WorkState", + "WorkZipCode", + "WorkCountry", + ], + "adr", + { type: "work" } + ), + "tel.home": singleTextProperty("HomePhone", "tel", { type: "home" }), + "tel.work": singleTextProperty("WorkPhone", "tel", { type: "work" }), + "tel.fax": singleTextProperty("FaxNumber", "tel", { type: "fax" }), + "tel.pager": singleTextProperty("PagerNumber", "tel", { type: "pager" }), + "tel.cell": singleTextProperty("CellularNumber", "tel", { type: "cell" }), + "url.work": singleTextProperty("WebPage1", "url", { type: "work" }, "url"), + "url.home": singleTextProperty("WebPage2", "url", { type: "home" }, "url"), + "x-custom1": singleTextProperty("Custom1", "x-custom1"), + "x-custom2": singleTextProperty("Custom2", "x-custom2"), + "x-custom3": singleTextProperty("Custom3", "x-custom3"), + "x-custom4": singleTextProperty("Custom4", "x-custom4"), +}; + +/** + * Any value that can be represented in a vCard. A value can be a boolean, + * number, string, or an array, depending on the data. A top-level array might + * contain primitives and/or second-level arrays of primitives. + * + * @see ICAL.design + * @see RFC6350 + * + * @typedef {boolean|number|string|vCardValue[]} vCardValue + */ + +/** + * Represents a single entry in a vCard ("contentline" in RFC6350 terms). + * The name, params, type and value are as returned by ICAL. + */ +class VCardPropertyEntry { + #name = null; + #params = null; + #type = null; + #value = null; + _original = null; + + /** + * @param {string} name + * @param {object} params + * @param {string} type + * @param {vCardValue} value + */ + constructor(name, params, type, value) { + this.#name = name; + this.#params = params; + this.#type = type; + if (params.encoding?.toUpperCase() == "QUOTED-PRINTABLE") { + if (Array.isArray(value)) { + value = value.map(VCardUtils._decodeQuotedPrintable); + } else { + value = VCardUtils._decodeQuotedPrintable(value); + } + delete params.encoding; + delete params.charset; + } + this.#value = value; + this._original = this; + } + + /** + * @type {string} + */ + get name() { + return this.#name; + } + + /** + * @type {object} + */ + get params() { + return this.#params; + } + + /** + * @type {string} + */ + get type() { + return this.#type; + } + set type(type) { + this.#type = type; + } + + /** + * @type {vCardValue} + */ + get value() { + return this.#value; + } + set value(value) { + this.#value = value; + } + + /** + * Clone this object. + * + * @returns {VCardPropertyEntry} + */ + clone() { + let cloneValue; + if (Array.isArray(this.#value)) { + cloneValue = this.#value.map(v => (Array.isArray(v) ? v.slice() : v)); + } else { + cloneValue = this.#value; + } + + let clone = new VCardPropertyEntry( + this.#name, + { ...this.#params }, + this.#type, + cloneValue + ); + clone._original = this; + return clone; + } + + /** + * @param {VCardPropertyEntry} other + */ + equals(other) { + if (other.constructor.name != "VCardPropertyEntry") { + return false; + } + return this._original == other._original; + } +} + +/** + * Represents an entire vCard as a collection of `VCardPropertyEntry` objects. + */ +class VCardProperties { + /** + * All of the vCard entries in this object. + * + * @type {VCardPropertyEntry[]} + */ + entries = []; + + /** + * @param {?string} version - The version of vCard to use. Valid values are + * "3.0" and "4.0". If unspecified, vCard 3.0 will be used. + */ + constructor(version) { + if (version) { + if (!["3.0", "4.0"].includes(version)) { + throw new Error(`Unsupported vCard version: ${version}`); + } + this.addEntry(new VCardPropertyEntry("version", {}, "text", version)); + } + } + + /** + * Parse a vCard into a VCardProperties object. + * + * @param {string} vCard + * @returns {VCardProperties} + */ + static fromVCard(vCard, { isGoogleCardDAV = false } = {}) { + vCard = VCardUtils.translateVCard21(vCard); + + let rv = new VCardProperties(); + let [, properties] = ICAL.parse(vCard); + for (let property of properties) { + let [name, params, type, value] = property; + if (property.length > 4) { + // The jCal format stores multiple values as the 4th...nth items. + // VCardPropertyEntry has only one place for a value, so store an + // array instead. This applies to CATEGORIES and NICKNAME types in + // vCard 4 and also NOTE in vCard 3. + value = property.slice(3); + } + if (isGoogleCardDAV) { + // Google escapes the characters \r : , ; and \ unnecessarily, in + // violation of RFC6350. Removing the escaping at this point means no + // other code requires a special case for it. + if (Array.isArray(value)) { + value = value.map(v => v.replace(/\\r/g, "\r").replace(/\\:/g, ":")); + } else { + value = value.replace(/\\r/g, "\r").replace(/\\:/g, ":"); + if (["phone-number", "uri"].includes(type)) { + value = value.replace(/\\([,;\\])/g, "$1"); + } + } + } + rv.addEntry(new VCardPropertyEntry(name, params, type, value)); + } + return rv; + } + + /** + * Parse a Map of Address Book properties into a VCardProperties object. + * + * @param {Map<string, string>} propertyMap + * @param {string} [version="4.0"] + * @returns {VCardProperties} + */ + static fromPropertyMap(propertyMap, version = "4.0") { + let rv = new VCardProperties(version); + + for (let vPropName of Object.keys(typeMap)) { + for (let vProp of typeMap[vPropName].fromAbCard(propertyMap, vPropName)) { + if (vProp[3] !== null && vProp[3] !== undefined && vProp[3] !== "") { + rv.addEntry(new VCardPropertyEntry(...vProp)); + } + } + } + + return rv; + } + + /** + * Used to determine the default value type when adding values. + * Either `ICAL.design.vcard` for (vCard 4.0) or `ICAL.design.vcard3` (3.0). + * + * @type {ICAL.design.designSet} + */ + designSet = ICAL.design.vcard3; + + /** + * Add an entry to this object. + * + * @param {VCardPropertyEntry} entry - The entry to add. + * @returns {boolean} - If the entry was added. + */ + addEntry(entry) { + if (entry.constructor.name != "VCardPropertyEntry") { + throw new Error("Not a VCardPropertyEntry"); + } + + if (this.entries.find(e => e.equals(entry))) { + return false; + } + + if (entry.name == "version") { + if (entry.value == "3.0") { + this.designSet = ICAL.design.vcard3; + } else if (entry.value == "4.0") { + this.designSet = ICAL.design.vcard; + } else { + throw new Error(`Unsupported vCard version: ${entry.value}`); + } + // Version must be the first entry, so clear out any existing values + // and add it to the start of the collection. + this.clearValues("version"); + this.entries.unshift(entry); + return true; + } + + this.entries.push(entry); + return true; + } + + /** + * Add an entry to this object by name and value. + * + * @param {string} name + * @param {string} value + * @returns {VCardPropertyEntry} + */ + addValue(name, value) { + for (let entry of this.getAllEntries(name)) { + if (entry.value == value) { + return entry; + } + } + + let newEntry = new VCardPropertyEntry( + name, + {}, + this.designSet.property[name].defaultType, + value + ); + this.entries.push(newEntry); + return newEntry; + } + + /** + * Remove an entry from this object. + * + * @param {VCardPropertyEntry} entry - The entry to remove. + * @returns {boolean} - If an entry was found and removed. + */ + removeEntry(entry) { + if (entry.constructor.name != "VCardPropertyEntry") { + throw new Error("Not a VCardPropertyEntry"); + } + + let index = this.entries.findIndex(e => e.equals(entry)); + if (index >= 0) { + this.entries.splice(index, 1); + return true; + } + return false; + } + + /** + * Remove entries from this object by name and value. All entries matching + * the name and value will be removed. + * + * @param {string} name + * @param {string} value + */ + removeValue(name, value) { + for (let entry of this.getAllEntries(name)) { + if (entry.value == value) { + this.removeEntry(entry); + } + } + } + + /** + * Remove entries from this object by name. All entries matching the name + * will be removed. + * + * @param {string} name + */ + clearValues(name) { + for (let entry of this.getAllEntries(name)) { + this.removeEntry(entry); + } + } + + /** + * Get the first value matching the given name, or null if no entry matches. + * + * @param {string} name + * @returns {?vCardValue} + */ + getFirstValue(name) { + let entry = this.entries.find(e => e.name == name); + if (entry) { + return entry.value; + } + return null; + } + + /** + * Get all values matching the given name. + * + * @param {string} name + * @returns {vCardValue[]} + */ + getAllValues(name) { + return this.getAllEntries(name).map(e => e.value); + } + + /** + * Get all values matching the given name, sorted in order of preference. + * Preference is determined by the `pref` parameter if it exists, then by + * the position in `entries`. + * + * @param {string} name + * @returns {vCardValue[]} + */ + getAllValuesSorted(name) { + return this.getAllEntriesSorted(name).map(e => e.value); + } + + /** + * Get the first entry matching the given name, or null if no entry matches. + * + * @param {string} name + * @returns {?VCardPropertyEntry} + */ + getFirstEntry(name) { + return this.entries.find(e => e.name == name) ?? null; + } + + /** + * Get all entries matching the given name. + * + * @param {string} name + * @returns {VCardPropertyEntry[]} + */ + getAllEntries(name) { + return this.entries.filter(e => e.name == name); + } + + /** + * Get all entries matching the given name, sorted in order of preference. + * Preference is determined by the `pref` parameter if it exists, then by + * the position in `entries`. + * + * @param {string} name + * @returns {VCardPropertyEntry[]} + */ + getAllEntriesSorted(name) { + let nextPref = 101; + let entries = this.getAllEntries(name).map(e => { + return { entry: e, pref: e.params.pref || nextPref++ }; + }); + entries.sort((a, b) => a.pref - b.pref); + return entries.map(e => e.entry); + } + + /** + * Get all entries matching the given group. + * + * @param {string} group + * @returns {VCardPropertyEntry[]} + */ + getGroupedEntries(group) { + return this.entries.filter(e => e.params.group == group); + } + + /** + * Clone this object. + * + * @returns {VCardProperties} + */ + clone() { + let copy = new VCardProperties(); + copy.entries = this.entries.map(e => e.clone()); + return copy; + } + + /** + * Get a Map of Address Book properties from this object. + * + * @returns {Map<string, string>} propertyMap + */ + toPropertyMap() { + let vPropMap = VCardUtils._parse(this.entries.map(e => e.clone())); + let propertyMap = new Map(); + + for (let [name, props] of vPropMap) { + // Store the value(s) on the abCard. + for (let [abPropName, abPropValue] of typeMap[name].toAbCard( + props[0].value + )) { + if (abPropValue) { + propertyMap.set(abPropName, abPropValue); + } + } + // Special case for email, which can also have a second preference. + if (name == "email" && props.length > 1) { + propertyMap.set("SecondEmail", props[1].value); + } + } + + return propertyMap; + } + + /** + * Serialize this object into a vCard. + * + * @returns {string} vCard + */ + toVCard() { + let jCal = this.entries.map(e => { + if (Array.isArray(e.value)) { + let design = this.designSet.property[e.name]; + if (design.multiValue == "," && !design.structuredValue) { + // The jCal format stores multiple values as the 4th...nth items, + // but VCardPropertyEntry stores them as an array. This applies to + // CATEGORIES and NICKNAME types in vCard 4 and also NOTE in vCard 3. + return [e.name, e.params, e.type, ...e.value]; + } + } + return [e.name, e.params, e.type, e.value]; + }); + return ICAL.stringify(["vcard", jCal]); + } +} |