summaryrefslogtreecommitdiffstats
path: root/content/includes/contactsync.js
diff options
context:
space:
mode:
Diffstat (limited to 'content/includes/contactsync.js')
-rw-r--r--content/includes/contactsync.js542
1 files changed, 542 insertions, 0 deletions
diff --git a/content/includes/contactsync.js b/content/includes/contactsync.js
new file mode 100644
index 0000000..23d6535
--- /dev/null
+++ b/content/includes/contactsync.js
@@ -0,0 +1,542 @@
+/*
+ * This file is part of EAS-4-TbSync.
+ *
+ * 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/.
+ */
+
+ "use strict";
+
+ var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ newUID: "resource:///modules/AddrBookUtils.jsm",
+ AddrBookCard: "resource:///modules/AddrBookCard.jsm",
+ BANISHED_PROPERTIES: "resource:///modules/VCardUtils.jsm",
+ VCardProperties: "resource:///modules/VCardUtils.jsm",
+ VCardPropertyEntry: "resource:///modules/VCardUtils.jsm",
+ VCardUtils: "resource:///modules/VCardUtils.jsm",
+});
+
+const eas = TbSync.providers.eas;
+
+var Contacts = {
+
+ // Remove if migration code is removed.
+ arrayFromString: function (stringValue) {
+ let arrayValue = [];
+ if (stringValue.trim().length>0) arrayValue = stringValue.trim().split("\u001A").filter(String);
+ return arrayValue;
+ },
+
+ /* The following TB properties are not synced to the server:
+ - only one WebPage
+ - more than 3 emails
+ - more than one fax, pager, mobile, work, home
+ - position (in org)
+ */
+
+ vcard_array_fields : {
+ n : 5,
+ adr : 7,
+ org : 2
+ },
+
+ map_EAS_properties_to_vCard : {
+ FileAs: {item: "fn", type: "text", params: {}}, /* DisplayName */
+
+ Birthday: {item: "bday", type: "date", params: {}},
+ Anniversary: {item: "anniversary", type: "date", params: {}},
+
+ LastName: {item: "n", type: "text", params: {}, index: 0},
+ FirstName: {item: "n", type: "text", params: {}, index: 1},
+ MiddleName: {item: "n", type: "text", params: {}, index: 2},
+ Title: {item: "n", type: "text", params: {}, index: 3},
+ Suffix: {item: "n", type: "text", params: {}, index: 4},
+
+ Notes: {item: "note", type: "text", params: {}},
+
+ // What should we do with Email 4+?
+ // EAS does not have the concept of home/work for emails. Define matchAll
+ // to not use params for finding the correct entry. They will come back as
+ // "other".
+ Email1Address: {item: "email", type: "text", entry: 0, matchAll: true, params: {}},
+ Email2Address: {item: "email", type: "text", entry: 1, matchAll: true, params: {}},
+ Email3Address: {item: "email", type: "text", entry: 2, matchAll: true, params: {}},
+
+ // EAS does not have the concept of home/work for WebPage. Define matchAll
+ // to not use params for finding the correct entry. It will come back as
+ // "other".
+ WebPage: {item: "url", type: "text", matchAll: true, params: {}},
+
+ CompanyName: {item: "org", type: "text", params: {}, index: 1}, /* Company */
+ Department: {item: "org", type: "text", params: {}, index: 0}, /* Department */
+ JobTitle: { item: "title", type: "text", params: {} }, /* JobTitle */
+
+ MobilePhoneNumber: { item: "tel", type: "text", params: {type: "cell" }},
+ PagerNumber: { item: "tel", type: "text", params: {type: "pager" }},
+ HomeFaxNumber: { item: "tel", type: "text", params: {type: "fax" }},
+ // If home phone is defined, use that, otherwise use unspecified phone
+ // Note: This must be exclusive (no other field may use home/unspecified)
+ // except if entry is specified.
+ HomePhoneNumber: { item: "tel", type: "text", params: {type: "home"}, fallbackParams: [{}]},
+ BusinessPhoneNumber: { item: "tel", type: "text", params: {type: "work"}},
+ Home2PhoneNumber: { item: "tel", type: "text", params: {type: "home"}, entry: 1 },
+ Business2PhoneNumber: { item: "tel", type: "text", params: {type: "work"}, entry: 1 },
+
+ HomeAddressStreet: {item: "adr", type: "text", params: {type: "home"}, index: 2}, // needs special handling
+ HomeAddressCity: {item: "adr", type: "text", params: {type: "home"}, index: 3},
+ HomeAddressState: {item: "adr", type: "text", params: {type: "home"}, index: 4},
+ HomeAddressPostalCode: {item: "adr", type: "text", params: {type: "home"}, index: 5},
+ HomeAddressCountry: {item: "adr", type: "text", params: {type: "home"}, index: 6},
+
+ BusinessAddressStreet: {item: "adr", type: "text", params: {type: "work"}, index: 2}, // needs special handling
+ BusinessAddressCity: {item: "adr", type: "text", params: {type: "work"}, index: 3},
+ BusinessAddressState: {item: "adr", type: "text", params: {type: "work"}, index: 4},
+ BusinessAddressPostalCode: {item: "adr", type: "text", params: {type: "work"}, index: 5},
+ BusinessAddressCountry: {item: "adr", type: "text", params: {type: "work"}, index: 6},
+
+ OtherAddressStreet: {item: "adr", type: "text", params: {}, index: 2}, // needs special handling
+ OtherAddressCity: {item: "adr", type: "text", params: {}, index: 3},
+ OtherAddressState: {item: "adr", type: "text", params: {}, index: 4},
+ OtherAddressPostalCode: {item: "adr", type: "text", params: {}, index: 5},
+ OtherAddressCountry: {item: "adr", type: "text", params: {}, index: 6},
+
+ // Misusing this EAS field, so that "Custom1" is saved to the server.
+ OfficeLocation: {item: "x-custom1", type: "text", params: {}},
+
+ Picture: {item: "photo", params: {}, type: "uri"},
+
+ // TB shows them as undefined, but showing them might be better, than not. Use a prefix.
+ AssistantPhoneNumber: { item: "tel", type: "text", params: {type: "Assistant"}, prefix: true},
+ CarPhoneNumber: { item: "tel", type: "text", params: {type: "Car"}, prefix: true},
+ RadioPhoneNumber: { item: "tel", type: "text", params: {type: "Radio"}, prefix: true},
+ BusinessFaxNumber: { item: "tel", type: "text", params: {type: "WorkFax"}, prefix: true},
+ },
+
+ map_EAS_properties_to_vCard_set2 : {
+ NickName: {item: "nickname", type: "text", params: {} },
+ // Misusing these EAS fields, so that "Custom2,3,4" is saved to the server.
+ CustomerId: {item: "x-custom2", type: "text", params: {}},
+ GovernmentId: {item: "x-custom3", type: "text", params: {}},
+ AccountName: {item: "x-custom4", type: "text", params: {}},
+
+ IMAddress: {item: "impp", type: "text", params: {} },
+ IMAddress2: {item: "impp", type: "text", params: {}, entry: 1 },
+ IMAddress3: {item: "impp", type: "text", params: {}, entry: 2 },
+
+ CompanyMainPhone: { item: "tel", type: "text", params: {type: "Company"}, prefix: true},
+ },
+
+ // There are currently no TB fields for these values, TbSync will store (and
+ // resend) them, but will not allow to view/edit.
+ unused_EAS_properties: [
+ "Alias", //pseudo field
+ "WeightedRank", //pseudo field
+ "YomiCompanyName", //japanese phonetic equivalent
+ "YomiFirstName", //japanese phonetic equivalent
+ "YomiLastName", //japanese phonetic equivalent
+ "CompressedRTF",
+ "MMS",
+ // Former custom EAS fields, no longer added to UI after 102.
+ "ManagerName",
+ "AssistantName",
+ "Spouse",
+ ],
+
+ // Normalize a parameters entry, to be able to find matching existing
+ // entries. If we want to be less restrictive, we need to check if all
+ // the requested values exist. But we should be the only one who sets
+ // the vCard props, so it should be safe. Except someone moves a contact.
+ // Should we prevent that via a vendor id in the vcard?
+ normalizeParameters: function (unordered) {
+ return JSON.stringify(
+ Object.keys(unordered).map(e => `${e}`.toLowerCase()).sort().reduce(
+ (obj, key) => {
+ obj[key] = `${unordered[key]}`.toLowerCase();
+ return obj;
+ },
+ {}
+ )
+ );
+ },
+
+ getValue: function (vCardProperties, vCard_property) {
+ let parameters = [vCard_property.params];
+ if (vCard_property.fallbackParams) {
+ parameters.push(...vCard_property.fallbackParams);
+ }
+ let entries;
+ for (let normalizedParams of parameters.map(this.normalizeParameters)) {
+ // If no params set, do not filter, otherwise filter for exact match.
+ entries = vCardProperties.getAllEntries(vCard_property.item)
+ .filter(e => vCard_property.matchAll || normalizedParams == this.normalizeParameters(e.params));
+ if (entries.length > 0) {
+ break;
+ }
+ }
+
+ // Which entry should we take?
+ let entryNr = vCard_property.entry || 0;
+ if (entries[entryNr]) {
+ let value;
+ if (vCard_property.item == "org" && !Array.isArray(entries[entryNr].value)) {
+ // The org field sometimes comes back as a string (then it is Company),
+ // even though it should be an array [Department,Company]
+ value = vCard_property.index == 1 ? entries[entryNr].value : "";
+ } else if (this.vcard_array_fields[vCard_property.item]) {
+ if (!Array.isArray(entries[entryNr].value)) {
+ // If the returned value is a single string, return it only
+ // when index 0 is requested, otherwise return nothing.
+ value = vCard_property.index == 0 ? entries[entryNr].value : "";
+ } else {
+ value = entries[entryNr].value[vCard_property.index];
+ }
+ } else {
+ value = entries[entryNr].value;
+ }
+
+ if (value) {
+ if (vCard_property.prefix && value.startsWith(`${vCard_property.params.type}: `)) {
+ return value.substring(`${vCard_property.params.type}: `.length);
+ }
+ return value;
+ }
+ }
+ return "";
+ },
+
+ /**
+ * Reads a DOM File and returns a Promise for its dataUrl.
+ *
+ * @param {File} file
+ * @returns {string}
+ */
+ getDataUrl(file) {
+ return new Promise((resolve, reject) => {
+ var reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onload = function() {
+ resolve(reader.result);
+ };
+ reader.onerror = function(error) {
+ resolve("");
+ };
+ });
+ },
+
+
+
+ // --------------------------------------------------------------------------- //
+ // Read WBXML and set Thunderbird item
+ // --------------------------------------------------------------------------- //
+ setThunderbirdItemFromWbxml: function (abItem, data, id, syncdata, mode = "standard") {
+ let asversion = syncdata.accountData.getAccountProperty("asversion");
+ if (TbSync.prefs.getIntPref("log.userdatalevel") > 2) TbSync.dump("Processing " + mode + " contact item", id);
+
+ // Make sure we are dealing with a vCard, so we can update the card just
+ // by updating its vCardProperties.
+ if (!abItem._card.supportsVCard) {
+ // This is an older card??
+ throw new Error("It looks like you are trying to sync a TB91 sync state. Does not work.");
+ }
+ let vCardProperties = abItem._card.vCardProperties
+ abItem.primaryKey = id;
+
+ // Loop over all known EAS properties (two EAS sets Contacts and Contacts2).
+ for (let set=0; set < 2; set++) {
+ let properties = (set == 0) ? this.EAS_properties : this.EAS_properties2;
+
+ for (let EAS_property of properties) {
+ let vCard_property = (set == 0) ? this.map_EAS_properties_to_vCard[EAS_property] : this.map_EAS_properties_to_vCard_set2[EAS_property];
+ let value;
+ switch (EAS_property) {
+ case "Notes":
+ if (asversion == "2.5") {
+ value = eas.xmltools.checkString(data.Body);
+ } else if (data.Body && data.Body.Data) {
+ value = eas.xmltools.checkString(data.Body.Data);
+ }
+ break;
+
+ default:
+ value = eas.xmltools.checkString(data[EAS_property]);
+ }
+
+ let normalizedParams = this.normalizeParameters(vCard_property.params)
+ let entries = vCardProperties.getAllEntries(vCard_property.item)
+ .filter(e => vCard_property.matchAll || normalizedParams == this.normalizeParameters(e.params));
+ // Which entry should we update? Add empty entries, if the requested entry number
+ // does not yet exist.
+ let entryNr = vCard_property.entry || 0;
+ while (entries.length <= entryNr) {
+ let newEntry = new VCardPropertyEntry(
+ vCard_property.item,
+ vCard_property.params,
+ vCard_property.type,
+ this.vcard_array_fields[vCard_property.item]
+ ? new Array(this.vcard_array_fields[vCard_property.item]).fill("")
+ : ""
+ );
+ vCardProperties.addEntry(newEntry);
+ entries = vCardProperties.getAllEntries(vCard_property.item);
+ entryNr = entries.length - 1;
+ }
+
+ // Is this property part of the send data?
+ if (value) {
+ // Do we need to manipulate the value?
+ switch (EAS_property) {
+ case "Picture":
+ value = `data:image/jpeg;base64,${eas.xmltools.nodeAsArray(data.Picture)[0]}`; //Kerio sends Picture as container
+ break;
+
+ case "Birthday":
+ case "Anniversary":
+ let dateObj = new Date(value);
+ value = dateObj.toISOString().substr(0, 10);
+ break;
+
+ case "Email1Address":
+ case "Email2Address":
+ case "Email3Address":
+ let parsedInput = MailServices.headerParser.makeFromDisplayAddress(value);
+ let fixedValue = (parsedInput && parsedInput[0] && parsedInput[0].email) ? parsedInput[0].email : value;
+ if (fixedValue != value) {
+ if (TbSync.prefs.getIntPref("log.userdatalevel") > 2) TbSync.dump("Parsing email display string via RFC 2231 and RFC 2047 ("+EAS_property+")", value + " -> " + fixedValue);
+ value = fixedValue;
+ }
+ break;
+
+ case "HomeAddressStreet":
+ case "BusinessAddressStreet":
+ case "OtherAddressStreet":
+ // Thunderbird accepts an array in the vCardProperty of the 2nd index of the adr field.
+ let seperator = String.fromCharCode(syncdata.accountData.getAccountProperty("seperator")); // options are 44 (,) or 10 (\n)
+ value = value.split(seperator);
+ break;
+ }
+
+ // Add a typePrefix for fields unknown to TB (better: TB should use the type itself).
+ if (vCard_property.prefix && !value.startsWith(`${vCard_property.params.type}: `)) {
+ value = `${vCard_property.params.type}: ${value}`;
+ }
+
+ // Is this an array value?
+ if (this.vcard_array_fields[vCard_property.item]) {
+ // Make sure this is an array.
+ if (!Array.isArray(entries[entryNr].value)) {
+ let arr = new Array(this.vcard_array_fields[vCard_property.item]).fill("");
+ arr[0] = entries[entryNr].value;
+ entries[entryNr].value = arr;
+ }
+ entries[entryNr].value[vCard_property.index] = value;
+ } else {
+ entries[entryNr].value = value;
+ }
+ } else {
+ if (this.vcard_array_fields[vCard_property.item]) {
+ // Make sure this is an array.
+ if (!Array.isArray(entries[entryNr].value)) {
+ let arr = new Array(this.vcard_array_fields[vCard_property.item]).fill("");
+ arr[0] = entries[entryNr].value;
+ entries[entryNr].value = arr;
+ }
+ entries[entryNr].value[vCard_property.index] = "";
+ } else {
+ entries[entryNr].value = "";
+ }
+ }
+ }
+ }
+
+ // Take care of categories.
+ if (data["Categories"] && data["Categories"]["Category"]) {
+ let categories = Array.isArray(data["Categories"]["Category"])
+ ? data["Categories"]["Category"]
+ : [data["Categories"]["Category"]];
+ vCardProperties.clearValues("categories");
+ vCardProperties.addValue("categories", categories);
+ // Migration code, remove once no longer needed.
+ abItem.setProperty("Categories", "");
+ }
+
+ // Take care of children, stored in contacts property bag.
+ if (data["Children"] && data["Children"]["Child"]) {
+ let children = Array.isArray(data["Children"]["Child"])
+ ? data["Children"]["Child"]
+ : [data["Children"]["Child"]];
+ abItem.setProperty("Children", JSON.stringify(children));
+ }
+
+ // Take care of un-mappable EAS options, which are stored in the contacts
+ // property bag.
+ for (let i=0; i < this.unused_EAS_properties.length; i++) {
+ if (data[this.unused_EAS_properties[i]]) abItem.setProperty("EAS-" + this.unused_EAS_properties[i], data[this.unused_EAS_properties[i]]);
+ }
+
+ // Remove all entries, which are marked for deletion.
+ vCardProperties.entries = vCardProperties.entries.filter(e => Array.isArray(e.value) ? e.value.some(a => a != "") : e.value != "");
+
+ // Further manipulations (a few getters are still usable \o/).
+ if (syncdata.accountData.getAccountProperty("displayoverride")) {
+ abItem._card.displayName = abItem._card.firstName + " " + abItem._card.lastName;
+ if (abItem._card.displayName == " " ) {
+ let company = (vCardProperties.getFirstValue("org") || [""])[0];
+ abItem._card.displayName = company || abItem._card.primaryEmail
+ }
+ }
+ },
+
+
+
+
+ // --------------------------------------------------------------------------- //
+ //read TB event and return its data as WBXML
+ // --------------------------------------------------------------------------- //
+ getWbxmlFromThunderbirdItem: async function (abItem, syncdata, isException = false) {
+ let asversion = syncdata.accountData.getAccountProperty("asversion");
+ let wbxml = eas.wbxmltools.createWBXML("", syncdata.type); //init wbxml with "" and not with precodes, and set initial codepage
+ let nowDate = new Date();
+
+ // Make sure we are dealing with a vCard, so we can access its vCardProperties.
+ if (!abItem._card.supportsVCard) {
+ throw new Error("It looks like you are trying to sync a TB91 sync state. Does not work.");
+ }
+ let vCardProperties = abItem._card.vCardProperties
+
+ // Loop over all known EAS properties (send empty value if not set).
+ for (let EAS_property of this.EAS_properties) {
+ // Some props need special handling.
+ let vCard_property = this.map_EAS_properties_to_vCard[EAS_property];
+ let value;
+ switch (EAS_property) {
+ case "Notes":
+ // Needs to be done later, because we have to switch the code page.
+ continue;
+
+ case "Picture": {
+ let photoUrl = abItem._card.photoURL;
+ if (!photoUrl) {
+ continue;
+ }
+ if (photoUrl.startsWith("file://")) {
+ let realPhotoFile = Services.io.newURI(photoUrl).QueryInterface(Ci.nsIFileURL).file;
+ let photoFile = await File.createFromNsIFile(realPhotoFile);
+ photoUrl = await this.getDataUrl(photoFile);
+ }
+ if (photoUrl.startsWith("data:image/")) {
+ let parts = photoUrl.split(",");
+ parts.shift();
+ value = parts.join(",");
+ }
+ }
+ break;
+
+ case "Birthday":
+ case "Anniversary": {
+ let raw = this.getValue(vCardProperties, vCard_property);
+ if (raw) {
+ let dateObj = new Date(raw);
+ value = dateObj.toISOString();
+ }
+ }
+ break;
+
+ case "HomeAddressStreet":
+ case "BusinessAddressStreet":
+ case "OtherAddressStreet": {
+ let raw = this.getValue(vCardProperties, vCard_property);
+ try {
+ if (raw) {
+ // We either get a single string or an array for the
+ // street adr field from Thunderbird.
+ if (!Array.isArray(raw)) {
+ raw = [raw];
+ }
+ let seperator = String.fromCharCode(syncdata.accountData.getAccountProperty("seperator")); // options are 44 (,) or 10 (\n)
+ value = raw.join(seperator);
+ }
+ } catch (ex) {
+ throw new Error(`Failed to eval value: <${JSON.stringify(raw)}> @ ${JSON.stringify(vCard_property)}`);
+ }
+ }
+ break;
+
+ default: {
+ value = this.getValue(vCardProperties, vCard_property);
+ }
+ }
+
+ if (value) {
+ wbxml.atag(EAS_property, value);
+ }
+ }
+
+ // Take care of un-mappable EAS option.
+ for (let i=0; i < this.unused_EAS_properties.length; i++) {
+ let value = abItem.getProperty("EAS-" + this.unused_EAS_properties[i], "");
+ if (value) wbxml.atag(this.unused_EAS_properties[i], value);
+ }
+
+ // Take care of categories.
+ let categories = vCardProperties.getFirstValue("categories");
+ let categoriesProperty = abItem.getProperty("Categories", "");
+ if (categoriesProperty) {
+ // Migration code, remove once no longer needed.
+ abItem.setProperty("Categories", "");
+ categories = this.arrayFromString(categoriesProperty);
+ }
+ if (categories) {
+ wbxml.otag("Categories");
+ for (let category of categories) wbxml.atag("Category", category);
+ wbxml.ctag();
+ }
+
+ // Take care of children, stored in contacts property bag.
+ let childrenProperty = abItem.getProperty("Children", "");
+ if (childrenProperty) {
+ let children = [];
+ try {
+ children = JSON.parse(childrenProperty);
+ } catch(ex) {
+ // Migration code, remove once no longer needed.
+ children = this.arrayFromString(childrenProperty);
+ }
+ wbxml.otag("Children");
+ for (let child of children) wbxml.atag("Child", child);
+ wbxml.ctag();
+ }
+
+ // Take care of notes - SWITCHING TO AirSyncBase (if 2.5, we still need Contact group here!)
+ let description = this.getValue(vCardProperties, this.map_EAS_properties_to_vCard["Notes"]);
+ if (asversion == "2.5") {
+ wbxml.atag("Body", description);
+ } else {
+ wbxml.switchpage("AirSyncBase");
+ wbxml.otag("Body");
+ wbxml.atag("Type", "1");
+ wbxml.atag("EstimatedDataSize", "" + description.length);
+ wbxml.atag("Data", description);
+ wbxml.ctag();
+ }
+
+ // Take care of Contacts2 group - SWITCHING TO CONTACTS2
+ wbxml.switchpage("Contacts2");
+
+ // Loop over all known TB properties of EAS group Contacts2 (send empty value if not set).
+ for (let EAS_property of this.EAS_properties2) {
+ let vCard_property = this.map_EAS_properties_to_vCard_set2[EAS_property];
+ let value = this.getValue(vCardProperties, vCard_property);
+ if (value) wbxml.atag(EAS_property, value);
+ }
+
+ return wbxml.getBytes();
+ }
+}
+
+Contacts.EAS_properties = Object.keys(Contacts.map_EAS_properties_to_vCard);
+Contacts.EAS_properties2 = Object.keys(Contacts.map_EAS_properties_to_vCard_set2);