summaryrefslogtreecommitdiffstats
path: root/unused
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--unused/_mbo16.pngbin0 -> 349 bytes
-rw-r--r--unused/_mbo32.pngbin0 -> 675 bytes
-rw-r--r--unused/_mbo48.pngbin0 -> 1040 bytes
-rw-r--r--unused/abUI.js487
-rw-r--r--unused/iconfinder_17_939744_16.pngbin0 -> 327 bytes
-rw-r--r--unused/iconfinder_17_939744_32.pngbin0 -> 619 bytes
-rw-r--r--unused/iconfinder_17_939744_64.pngbin0 -> 1217 bytes
-rw-r--r--unused/iconfinder_Apple_1298725_16.pngbin0 -> 436 bytes
-rw-r--r--unused/iconfinder_Apple_1298725_32.pngbin0 -> 826 bytes
-rw-r--r--unused/iconfinder_Apple_1298725_64.pngbin0 -> 1712 bytes
-rw-r--r--unused/mbo48_2.pngbin0 -> 940 bytes
-rw-r--r--unused/orig_sync.js908
-rw-r--r--unused/vcard/LICENSE21
-rw-r--r--unused/vcard/SOURCE1
-rw-r--r--unused/vcard/vcard.js305
15 files changed, 1722 insertions, 0 deletions
diff --git a/unused/_mbo16.png b/unused/_mbo16.png
new file mode 100644
index 0000000..a0ecec5
--- /dev/null
+++ b/unused/_mbo16.png
Binary files differ
diff --git a/unused/_mbo32.png b/unused/_mbo32.png
new file mode 100644
index 0000000..a08c4b4
--- /dev/null
+++ b/unused/_mbo32.png
Binary files differ
diff --git a/unused/_mbo48.png b/unused/_mbo48.png
new file mode 100644
index 0000000..128f729
--- /dev/null
+++ b/unused/_mbo48.png
Binary files differ
diff --git a/unused/abUI.js b/unused/abUI.js
new file mode 100644
index 0000000..22008bc
--- /dev/null
+++ b/unused/abUI.js
@@ -0,0 +1,487 @@
+/*
+ * This file is part of DAV-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 ui = {
+
+ getUriFromDirectoryId: function(ownerId) {
+ let directories = MailServices.ab.directories;
+ for (let directory of directories) {
+ if (directory instanceof Components.interfaces.nsIAbDirectory) {
+ if (ownerId.startsWith(directory.dirPrefId)) return directory.URI;
+ }
+ }
+ return null;
+ },
+
+
+ //function to get correct uri of current card for global book as well for mailLists
+ getSelectedUri : function(aUri, aCard) {
+ if (aUri == "moz-abdirectory://?") {
+ //get parent via card owner
+ let ownerId = aCard.directoryId;
+ return dav.ui.getUriFromDirectoryId(ownerId);
+ } else if (MailServices.ab.getDirectory(aUri).isMailList) {
+ //MailList suck, we have to cut the url to get the parent
+ return aUri.substring(0, aUri.lastIndexOf("/"))
+ } else {
+ return aUri;
+ }
+ },
+
+
+
+ //* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ //* Functions to handle advanced UI elements of AB
+ //* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+ updatePref: function(aDocument, icon, toggle = false) {
+ if (toggle) {
+ if (icon.parentNode.meta.includes("PREF")) icon.parentNode.meta = icon.parentNode.meta.filter(e => e != "PREF");
+ else icon.parentNode.meta.push("PREF");
+
+ icon.parentNode.updateFunction (aDocument);
+ }
+
+ if (icon.parentNode.meta.includes("PREF")) {
+ icon.setAttribute("src", "chrome://dav4tbsync/content/skin/type.pref.png");
+ } else {
+ icon.setAttribute("src", "chrome://dav4tbsync/content/skin/type.nopref.png");
+ }
+ },
+
+ updateType: function(aDocument, button, newvalue = null) {
+ if (newvalue) {
+ //we declare allowedValues to be non-overlapping -> remove all allowed values and just add the newvalue
+ button.parentNode.meta = button.parentNode.meta.filter(value => -1 == button.allowedValues.indexOf(value));
+ if (button.allowedValues.includes(newvalue)) {
+ //hardcoded sort order: HOME/WORK always before other types
+ if (["HOME","WORK"].includes(newvalue)) button.parentNode.meta.unshift(newvalue);
+ else button.parentNode.meta.push(newvalue);
+ }
+
+ button.parentNode.updateFunction (aDocument);
+ }
+
+ let intersection = button.parentNode.meta.filter(value => -1 !== button.allowedValues.indexOf(value));
+ let buttonType = (intersection.length > 0) ? intersection[0].toLowerCase() : button.otherIcon;
+ button.setAttribute("image","chrome://dav4tbsync/content/skin/type."+buttonType+"10.png");
+ },
+
+ dragdrop: {
+ handleEvent(event) {
+ //only allow to drag the elements which are valid drag targets
+ if (event.target.getAttribute("dragtarget") != "true") {
+ event.stopPropagation();
+ return;
+ }
+
+ let outerbox = event.currentTarget;
+ let richlistitem = outerbox.parentNode;
+
+ switch (event.type) {
+ case "dragenter":
+ case "dragover":
+ let dropIndex = richlistitem.parentNode.getIndexOfItem(richlistitem);
+ let dragIndex = richlistitem.parentNode.getIndexOfItem(richlistitem.ownerDocument.getElementById(event.dataTransfer.getData("id")));
+
+ let centerY = event.currentTarget.clientHeight / 2;
+ let insertBefore = (event.offsetY < centerY);
+ let moveNeeded = !(dropIndex == dragIndex || (dropIndex+1 == dragIndex && !insertBefore) || (dropIndex-1 == dragIndex && insertBefore));
+
+ if (moveNeeded) {
+ if (insertBefore) {
+ richlistitem.parentNode.insertBefore(richlistitem.parentNode.getItemAtIndex(dragIndex), richlistitem);
+ } else {
+ richlistitem.parentNode.insertBefore(richlistitem.parentNode.getItemAtIndex(dragIndex), richlistitem.nextSibling);
+ }
+ }
+
+ event.preventDefault();
+ break;
+
+ case "drop":
+ event.preventDefault();
+ case "dragleave":
+ break;
+
+ case "dragstart":
+ event.currentTarget.style["background-color"] = "#eeeeee";
+ event.dataTransfer.setData("id", richlistitem.id);
+ break;
+
+ case "dragend":
+ event.currentTarget.style["background-color"] = "transparent";
+ outerbox.updateFunction(outerbox.ownerDocument);
+ break;
+
+ default:
+ return undefined;
+ }
+ },
+ },
+
+ //* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ //* Functions to handle multiple email addresses in AB (UI)
+ //* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+ getNewEmailDetailsRow: function (aWindow, aItemData) {
+ let emailType = "other";
+ if (aItemData.meta.includes("HOME")) emailType = "home";
+ else if (aItemData.meta.includes("WORK")) emailType = "work";
+
+ //first column
+ let vbox = aWindow.document.createXULElement("vbox");
+ vbox.setAttribute("class","CardViewText");
+ vbox.setAttribute("style","margin-right:1ex; margin-bottom:0px;");
+ let image = aWindow.document.createXULElement("image");
+ image.setAttribute("width","10");
+ image.setAttribute("height","10");
+ image.setAttribute("src", "chrome://dav4tbsync/content/skin/type."+emailType+"10.png");
+ vbox.appendChild(image);
+
+ //second column
+ let description = aWindow.document.createXULElement("description");
+ description.setAttribute("class","plain");
+ let namespace = aWindow.document.lookupNamespaceURI("html");
+ let a = aWindow.document.createElementNS(namespace, "a");
+ a.setAttribute("href", "mailto:" + aItemData.value);
+ a.textContent = aItemData.value;
+ description.appendChild(a);
+
+ if (aItemData.meta.includes("PREF")) {
+ let pref = aWindow.document.createXULElement("image");
+ pref.setAttribute("style", "margin-left:1ex;");
+ pref.setAttribute("width", "11");
+ pref.setAttribute("height", "10");
+ pref.setAttribute("src", "chrome://dav4tbsync/content/skin/type.nopref.png");
+ description.appendChild(pref);
+ }
+
+ //row
+ let row = aWindow.document.createElement("tr");
+ let cell1 = aWindow.document.createElement("td");
+ let cell2 = aWindow.document.createElement("td");
+ cell2.setAttribute("width","100%");
+
+ cell1.appendChild(vbox);
+ cell2.appendChild(description);
+
+ row.appendChild(cell1);
+ row.appendChild(cell2);
+ return row;
+ },
+
+ getNewEmailListItem: function (aDocument, aItemData) {
+ //hbox
+ let outerhbox = aDocument.createXULElement("hbox");
+ outerhbox.setAttribute("dragtarget", "true");
+ outerhbox.setAttribute("flex", "1");
+ outerhbox.setAttribute("align", "center");
+ outerhbox.setAttribute("style", "padding:0; margin:0");
+ outerhbox.updateFunction = dav.ui.updateEmails;
+ outerhbox.meta = aItemData.meta;
+
+ outerhbox.addEventListener("dragenter", dav.ui.dragdrop);
+ outerhbox.addEventListener("dragover", dav.ui.dragdrop);
+ outerhbox.addEventListener("dragleave", dav.ui.dragdrop);
+ outerhbox.addEventListener("dragstart", dav.ui.dragdrop);
+ outerhbox.addEventListener("dragend", dav.ui.dragdrop);
+ outerhbox.addEventListener("drop", dav.ui.dragdrop);
+
+ outerhbox.style["background-image"] = "url('chrome://dav4tbsync/content/skin/dragdrop.png')";
+ outerhbox.style["background-position"] = "right";
+ outerhbox.style["background-repeat"] = "no-repeat";
+
+ //button
+ let button = aDocument.createXULElement("button");
+ button.allowedValues = ["HOME", "WORK"];
+ button.otherIcon = "other";
+ button.setAttribute("type", "menu");
+ button.setAttribute("style", "width: 35px; min-width: 35px; margin: 0; padding:0");
+ button.appendChild(aDocument.getElementById("DavMenuTemplate").children[0].cloneNode(true));
+ outerhbox.appendChild(button);
+
+ //email box
+ let emailbox = aDocument.createXULElement("hbox");
+ emailbox.setAttribute("flex", "1");
+ let email = aDocument.createElement("input");
+ email.setAttribute("style", "width: 240px");
+ email.setAttribute("value", aItemData.value);
+ email.addEventListener("change", function(e) {dav.ui.updateEmails(aDocument)});
+ email.addEventListener("keydown", function(e) {if (e.key == "Enter") {e.stopPropagation(); e.preventDefault(); if (e.target.value != "") { dav.ui.addEmailEntry(e.target.ownerDocument); }}});
+ emailbox.appendChild(email);
+ outerhbox.appendChild(emailbox);
+
+ //image
+ let image = aDocument.createXULElement("image");
+ image.setAttribute("width", "11");
+ image.setAttribute("height", "10");
+ image.setAttribute("style", "margin:2px 20px 2px 1ex");
+ image.addEventListener("click", function(e) { dav.ui.updatePref(aDocument, e.target, true); });
+ outerhbox.appendChild(image);
+
+ //richlistitem
+ let richlistitem = aDocument.createXULElement("richlistitem");
+ richlistitem.setAttribute("id", "entry_" + TbSync.generateUUID());
+ richlistitem.setAttribute("style", "padding:0;margin:0;");
+ richlistitem.appendChild(outerhbox);
+
+ return richlistitem;
+ },
+
+ getEmailListItemElement: function(item, element) {
+ switch (element) {
+ case "dataContainer":
+ return item.children[0];
+ case "button":
+ return item.children[0].children[0];
+ case "email":
+ return item.children[0].children[1].children[0];
+ case "pref":
+ return item.children[0].children[2];
+ default:
+ return null;
+ }
+ },
+
+ addEmailEntry: function(aDocument) {
+ let list = aDocument.getElementById("X-DAV-EmailAddressList");
+ let data = {value: "", meta: ["HOME"]};
+ let item = list.appendChild(dav.ui.getNewEmailListItem(aDocument, data));
+ list.ensureElementIsVisible(item);
+
+ dav.ui.updateType(aDocument, dav.ui.getEmailListItemElement(item, "button"));
+ dav.ui.updatePref(aDocument, dav.ui.getEmailListItemElement(item, "pref"));
+
+ dav.ui.getEmailListItemElement(item, "email").focus();
+ },
+
+
+ //if any setting changed, we need to update Primary and Secondary Email Fields
+ updateEmails: function(aDocument) {
+ let list = aDocument.getElementById("X-DAV-EmailAddressList");
+
+ let emails = [];
+ for (let i=0; i < list.children.length; i++) {
+ let item = list.children[i];
+ let email = dav.ui.getEmailListItemElement(item, "email").value.trim();
+ if (email != "") {
+ let json = {};
+ json.meta = dav.ui.getEmailListItemElement(item, "dataContainer").meta;
+ json.value = email;
+ emails.push(json);
+ }
+ }
+ aDocument.getElementById("X-DAV-JSON-Emails").value = JSON.stringify(emails);
+
+ //now update all other TB email fields based on the new JSON data
+ let emailData = dav.tools.getEmailsFromJSON(aDocument.getElementById("X-DAV-JSON-Emails").value);
+ for (let field in emailData) {
+ if (emailData.hasOwnProperty(field)) {
+ aDocument.getElementById(field).value = emailData[field].join(", ");
+ }
+ }
+ },
+
+
+
+
+ //* * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ //* Functions to handle multiple phone numbers in AB (UI)
+ //* * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+ getNewPhoneDetailsRow: function (aWindow, aItemData) {
+ let phoneType1 = "";
+ if (aItemData.meta.includes("HOME")) phoneType1 = "home";
+ else if (aItemData.meta.includes("WORK")) phoneType1 = "work";
+
+ let phoneType2 = "";
+ if (aItemData.meta.includes("CELL")) phoneType2 = "cell";
+ else if (aItemData.meta.includes("FAX")) phoneType2 = "fax";
+ else if (aItemData.meta.includes("PAGER")) phoneType2 = "pager";
+ else if (aItemData.meta.includes("CAR")) phoneType2 = "car";
+ else if (aItemData.meta.includes("VIDEO")) phoneType2 = "video";
+ else if (aItemData.meta.includes("VOICE")) phoneType2 = "voice";
+
+ //first column
+ let vbox = aWindow.document.createXULElement("hbox");
+ vbox.setAttribute("pack","end");
+ vbox.setAttribute("class","CardViewText");
+ vbox.setAttribute("style","margin-bottom:0px;");
+ if (phoneType1) {
+ let image = aWindow.document.createXULElement("image");
+ image.setAttribute("style","margin-right:1ex;");
+ image.setAttribute("width","10");
+ image.setAttribute("height","10");
+ image.setAttribute("src", "chrome://dav4tbsync/content/skin/type."+phoneType1+"10.png");
+ vbox.appendChild(image);
+ }
+ if (phoneType2) {
+ let image = aWindow.document.createXULElement("image");
+ image.setAttribute("style","margin-right:1ex;");
+ image.setAttribute("width","10");
+ image.setAttribute("height","10");
+ image.setAttribute("src", "chrome://dav4tbsync/content/skin/type."+phoneType2+"10.png");
+ vbox.appendChild(image);
+ }
+
+ //second column
+ let description = aWindow.document.createXULElement("description");
+ description.setAttribute("class","plain");
+ description.setAttribute("style","-moz-user-select: text;");
+ description.textContent = aItemData.value;
+
+ if (aItemData.meta.includes("PREF")) {
+ let pref = aWindow.document.createXULElement("image");
+ pref.setAttribute("style", "margin-left:1ex;");
+ pref.setAttribute("width", "11");
+ pref.setAttribute("height", "10");
+ pref.setAttribute("src", "chrome://dav4tbsync/content/skin/type.nopref.png");
+ description.appendChild(pref);
+ }
+
+ //row
+ let row = aWindow.document.createElement("tr");
+ let cell1 = aWindow.document.createElement("td");
+ let cell2 = aWindow.document.createElement("td");
+ cell2.setAttribute("width","100%");
+
+ cell1.appendChild(vbox);
+ cell2.appendChild(description);
+
+ row.appendChild(cell1);
+ row.appendChild(cell2);
+ return row;
+ },
+
+ getNewPhoneListItem: function (aDocument, aItemData) {
+ //hbox
+ let outerhbox = aDocument.createXULElement("hbox");
+ outerhbox.setAttribute("dragtarget", "true");
+ outerhbox.setAttribute("flex", "1");
+ outerhbox.setAttribute("align", "center");
+ outerhbox.setAttribute("style", "padding:0; margin:0");
+ outerhbox.updateFunction = dav.ui.updatePhoneNumbers;
+ outerhbox.meta = aItemData.meta;
+
+ outerhbox.addEventListener("dragenter", dav.ui.dragdrop);
+ outerhbox.addEventListener("dragover", dav.ui.dragdrop);
+ outerhbox.addEventListener("dragleave", dav.ui.dragdrop);
+ outerhbox.addEventListener("dragstart", dav.ui.dragdrop);
+ outerhbox.addEventListener("dragend", dav.ui.dragdrop);
+ outerhbox.addEventListener("drop", dav.ui.dragdrop);
+
+ outerhbox.style["background-image"] = "url('chrome://dav4tbsync/content/skin/dragdrop.png')";
+ outerhbox.style["background-position"] = "right";
+ outerhbox.style["background-repeat"] = "no-repeat";
+
+ //button1
+ let button1 = aDocument.createXULElement("button");
+ button1.allowedValues = ["HOME", "WORK"];
+ button1.otherIcon = "none";
+ button1.setAttribute("type", "menu");
+ button1.setAttribute("style", "width: 35px; min-width: 35px; margin: 0; padding:0");
+ button1.appendChild(aDocument.getElementById("DavMenuTemplate").children[1].cloneNode(true));
+ outerhbox.appendChild(button1);
+
+ //button2
+ let button2 = aDocument.createXULElement("button");
+ button2.allowedValues = ["CELL", "FAX", "PAGER", "CAR", "VIDEO", "VOICE"] ; //same order as in getNewPhoneDetailsRow
+ button2.otherIcon = "none";
+ button2.setAttribute("type", "menu");
+ button2.setAttribute("class", "plain");
+ button2.setAttribute("style", "width: 35px; min-width: 35px; margin: 0;");
+ button2.appendChild(aDocument.getElementById("DavMenuTemplate").children[2].cloneNode(true));
+ outerhbox.appendChild(button2);
+
+ //phone box
+ let phonebox = aDocument.createXULElement("hbox");
+ phonebox.setAttribute("flex", "1");
+ let phone = aDocument.createElement("input");
+ phone.setAttribute("style", "width: 205px");
+ phone.setAttribute("value", aItemData.value);
+ phone.addEventListener("change", function(e) {dav.ui.updatePhoneNumbers(aDocument)});
+ phone.addEventListener("keydown", function(e) {if (e.key == "Enter") {e.stopPropagation(); e.preventDefault(); if (e.target.value != "") { dav.ui.addPhoneEntry(e.target.ownerDocument); }}});
+ phonebox.appendChild(phone);
+ outerhbox.appendChild(phonebox);
+
+ //image
+ let image = aDocument.createXULElement("image");
+ image.setAttribute("width", "11");
+ image.setAttribute("height", "10");
+ image.setAttribute("style", "margin:2px 20px 2px 1ex");
+ image.addEventListener("click", function(e) { dav.ui.updatePref(aDocument, e.target, true); });
+ outerhbox.appendChild(image);
+
+ //richlistitem
+ let richlistitem = aDocument.createXULElement("richlistitem");
+ richlistitem.setAttribute("id", "entry_" + TbSync.generateUUID());
+ richlistitem.setAttribute("style", "padding:0;margin:0;");
+ richlistitem.appendChild(outerhbox);
+
+ return richlistitem;
+ },
+
+ updatePhoneNumbers: function(aDocument) {
+ let list = aDocument.getElementById("X-DAV-PhoneNumberList");
+
+ let phones = [];
+ for (let i=0; i < list.children.length; i++) {
+ let item = list.children[i];
+ let phone = dav.ui.getPhoneListItemElement(item, "phone").value.trim();
+ if (phone != "") {
+ let json = {};
+ json.meta = dav.ui.getPhoneListItemElement(item, "dataContainer").meta;
+ json.value = phone;
+ phones.push(json);
+ }
+ }
+ aDocument.getElementById("X-DAV-JSON-Phones").value = JSON.stringify(phones);
+
+ //now update all other TB number fields based on the new JSON data
+ let phoneData = dav.tools.getPhoneNumbersFromJSON(aDocument.getElementById("X-DAV-JSON-Phones").value);
+ for (let field in phoneData) {
+ if (phoneData.hasOwnProperty(field)) {
+ aDocument.getElementById(field).value = phoneData[field].join(", ");
+ }
+ }
+ },
+
+ addPhoneEntry: function(aDocument) {
+ let list = aDocument.getElementById("X-DAV-PhoneNumberList");
+ let data = {value: "", meta: ["VOICE"]};
+ let item = list.appendChild(dav.ui.getNewPhoneListItem(aDocument, data));
+ list.ensureElementIsVisible(item);
+
+ dav.ui.updateType(aDocument, dav.ui.getPhoneListItemElement(item, "button1"));
+ dav.ui.updateType(aDocument, dav.ui.getPhoneListItemElement(item, "button2"));
+ dav.ui.updatePref(aDocument, dav.ui.getPhoneListItemElement(item, "pref"));
+
+ dav.ui.getPhoneListItemElement(item, "phone").focus();
+ },
+
+ getPhoneListItemElement: function(item, element) {
+ switch (element) {
+ case "dataContainer":
+ return item.children[0];
+ case "button1":
+ return item.children[0].children[0];
+ case "button2":
+ return item.children[0].children[1];
+ case "phone":
+ return item.children[0].children[2].children[0];
+ case "pref":
+ return item.children[0].children[3];
+ default:
+ return null;
+ }
+ },
+
+}
diff --git a/unused/iconfinder_17_939744_16.png b/unused/iconfinder_17_939744_16.png
new file mode 100644
index 0000000..00eab4b
--- /dev/null
+++ b/unused/iconfinder_17_939744_16.png
Binary files differ
diff --git a/unused/iconfinder_17_939744_32.png b/unused/iconfinder_17_939744_32.png
new file mode 100644
index 0000000..baacddc
--- /dev/null
+++ b/unused/iconfinder_17_939744_32.png
Binary files differ
diff --git a/unused/iconfinder_17_939744_64.png b/unused/iconfinder_17_939744_64.png
new file mode 100644
index 0000000..b6a227b
--- /dev/null
+++ b/unused/iconfinder_17_939744_64.png
Binary files differ
diff --git a/unused/iconfinder_Apple_1298725_16.png b/unused/iconfinder_Apple_1298725_16.png
new file mode 100644
index 0000000..fd5fa79
--- /dev/null
+++ b/unused/iconfinder_Apple_1298725_16.png
Binary files differ
diff --git a/unused/iconfinder_Apple_1298725_32.png b/unused/iconfinder_Apple_1298725_32.png
new file mode 100644
index 0000000..0815a20
--- /dev/null
+++ b/unused/iconfinder_Apple_1298725_32.png
Binary files differ
diff --git a/unused/iconfinder_Apple_1298725_64.png b/unused/iconfinder_Apple_1298725_64.png
new file mode 100644
index 0000000..2d05773
--- /dev/null
+++ b/unused/iconfinder_Apple_1298725_64.png
Binary files differ
diff --git a/unused/mbo48_2.png b/unused/mbo48_2.png
new file mode 100644
index 0000000..0379947
--- /dev/null
+++ b/unused/mbo48_2.png
Binary files differ
diff --git a/unused/orig_sync.js b/unused/orig_sync.js
new file mode 100644
index 0000000..ac654f8
--- /dev/null
+++ b/unused/orig_sync.js
@@ -0,0 +1,908 @@
+/*
+/*
+ * This file is part of DAV-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 sync = {
+
+ finish: function (aStatus = "", msg = "", details = "") {
+ let status = TbSync.StatusData.SUCCESS
+ switch (aStatus) {
+
+ case "":
+ case "ok":
+ status = TbSync.StatusData.SUCCESS;
+ break;
+
+ case "info":
+ status = TbSync.StatusData.INFO;
+ break;
+
+ case "resyncAccount":
+ status = TbSync.StatusData.ACCOUNT_RERUN;
+ break;
+
+ case "resyncFolder":
+ status = TbSync.StatusData.FOLDER_RERUN;
+ break;
+
+ case "warning":
+ status = TbSync.StatusData.WARNING;
+ break;
+
+ case "error":
+ status = TbSync.StatusData.ERROR;
+ break;
+
+ default:
+ console.log("TbSync/DAV: Unknown status <"+aStatus+">");
+ status = TbSync.StatusData.ERROR;
+ break;
+ }
+
+ let e = new Error();
+ e.name = "dav4tbsync";
+ e.message = status.toUpperCase() + ": " + msg.toString() + " (" + details.toString() + ")";
+ e.statusData = new TbSync.StatusData(status, msg.toString(), details.toString());
+ return e;
+ },
+
+ prefSettings: Services.prefs.getBranch("extensions.dav4tbsync."),
+
+ ns: {
+ d: "DAV:",
+ cal: "urn:ietf:params:xml:ns:caldav" ,
+ card: "urn:ietf:params:xml:ns:carddav" ,
+ cs: "http://calendarserver.org/ns/",
+ s: "http://sabredav.org/ns",
+ apple: "http://apple.com/ns/ical/"
+ },
+
+ serviceproviders: {
+ "fruux" : {revision: 1, icon: "fruux", caldav: "https://dav.fruux.com", carddav: "https://dav.fruux.com"},
+ "mbo" : {revision: 1, icon: "mbo", caldav: "caldav6764://mailbox.org", carddav: "carddav6764://mailbox.org"},
+ "icloud" : {revision: 1, icon: "icloud", caldav: "https://caldav.icloud.com", carddav: "https://contacts.icloud.com"},
+ "gmx.net" : {revision: 1, icon: "gmx", caldav: "caldav6764://gmx.net", carddav: "carddav6764://gmx.net"},
+ "gmx.com" : {revision: 1, icon: "gmx", caldav: "caldav6764://gmx.com", carddav: "carddav6764://gmx.com"},
+ "posteo" : {revision: 1, icon: "posteo", caldav: "https://posteo.de:8443", carddav: "posteo.de:8843"},
+ "web.de" : {revision: 1, icon: "web", caldav: "caldav6764://web.de", carddav: "carddav6764://web.de"},
+ "yahoo" : {revision: 1, icon: "yahoo", caldav: "caldav6764://yahoo.com", carddav: "carddav6764://yahoo.com"},
+ },
+
+ onChange(abItem) {
+ if (!this._syncOnChangeTimers)
+ this._syncOnChangeTimers = {};
+
+ this._syncOnChangeTimers[abItem.abDirectory.UID] = {};
+ this._syncOnChangeTimers[abItem.abDirectory.UID].timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
+ this._syncOnChangeTimers[abItem.abDirectory.UID].event = {
+ notify: function(timer) {
+ // if account is syncing, re-schedule
+ // if folder got synced after the start time (due to re-scheduling) abort
+ console.log("DONE: "+ abItem.abDirectory.UID);
+ }
+ }
+
+ this._syncOnChangeTimers[abItem.abDirectory.UID].timer.initWithCallback(
+ this._syncOnChangeTimers[abItem.abDirectory.UID].event,
+ 2000,
+ Components.interfaces.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ resetFolderSyncInfo : function (folderData) {
+ folderData.resetFolderProperty("ctag");
+ folderData.resetFolderProperty("token");
+ folderData.setFolderProperty("createdWithProviderVersion", folderData.accountData.providerData.getVersion());
+ },
+
+ folderList: async function (syncData) {
+ //Method description: http://sabre.io/dav/building-a-caldav-client/
+ //get all folders currently known
+ let folderTypes = ["caldav", "carddav", "ics"];
+ let unhandledFolders = {};
+ for (let type of folderTypes) {
+ unhandledFolders[type] = [];
+ }
+
+
+ let folders = syncData.accountData.getAllFolders();
+ for (let folder of folders) {
+ //just in case
+ if (!unhandledFolders.hasOwnProperty(folder.getFolderProperty("type"))) {
+ unhandledFolders[folder.getFolderProperty("type")] = [];
+ }
+ unhandledFolders[folder.getFolderProperty("type")].push(folder);
+ }
+
+ // refresh urls of service provider, if they have been updated
+ let serviceprovider = syncData.accountData.getAccountProperty("serviceprovider");
+ let serviceproviderRevision = syncData.accountData.getAccountProperty("serviceproviderRevision");
+ if (dav.sync.serviceproviders.hasOwnProperty(serviceprovider) && serviceproviderRevision != dav.sync.serviceproviders[serviceprovider].revision) {
+ TbSync.eventlog.add("info", syncData.eventLogInfo, "updatingServiceProvider", serviceprovider);
+ syncData.accountData.setAccountProperty("serviceproviderRevision", dav.sync.serviceproviders[serviceprovider].revision);
+ syncData.accountData.resetAccountProperty("calDavPrincipal");
+ syncData.accountData.resetAccountProperty("cardDavPrincipal");
+ syncData.accountData.setAccountProperty("calDavHost", dav.sync.serviceproviders[serviceprovider].caldav);
+ syncData.accountData.setAccountProperty("cardDavHost", dav.sync.serviceproviders[serviceprovider].carddav);
+ }
+
+ let davjobs = {
+ cal : {server: syncData.accountData.getAccountProperty("calDavHost")},
+ card : {server: syncData.accountData.getAccountProperty("cardDavHost")},
+ };
+
+ for (let job in davjobs) {
+ if (!davjobs[job].server) continue;
+
+ // SOGo needs some special handling for shared addressbooks. We detect it by having SOGo/dav in the url.
+ let isSogo = davjobs[job].server.includes("/SOGo/dav");
+
+ //sync states are only printed while the account state is "syncing" to inform user about sync process (it is not stored in DB, just in syncData)
+ //example state "getfolders" to get folder information from server
+ //if you send a request to a server and thus have to wait for answer, use a "send." syncstate, which will give visual feedback to the user,
+ //that we are waiting for an answer with timeout countdown
+
+ let home = [];
+ let own = [];
+
+ // migration code for http setting, we might keep it as a fallback, if user removed the http:// scheme from the url in the settings
+ if (!dav.network.startsWithScheme(davjobs[job].server)) {
+ davjobs[job].server = "http" + (syncData.accountData.getAccountProperty("https") ? "s" : "") + "://" + davjobs[job].server;
+ syncData.accountData.setAccountProperty(job + "DavHost", davjobs[job].server);
+ }
+
+ //add connection to syncData
+ syncData.connectionData = new dav.network.ConnectionData(syncData);
+
+ //only do that, if a new calendar has been enabled
+ TbSync.network.resetContainerForUser(syncData.connectionData.username);
+
+ syncData.setSyncState("send.getfolders");
+ let principal = syncData.accountData.getAccountProperty(job + "DavPrincipal"); // defaults to null
+ if (principal === null) {
+
+ let response = await dav.network.sendRequest("<d:propfind "+dav.tools.xmlns(["d"])+"><d:prop><d:current-user-principal /></d:prop></d:propfind>", davjobs[job].server , "PROPFIND", syncData.connectionData, {"Depth": "0", "Prefer": "return=minimal"});
+ syncData.setSyncState("eval.folders");
+
+ // keep track of permanent redirects for the server URL
+ if (response && response.permanentlyRedirectedUrl) {
+ syncData.accountData.setAccountProperty(job + "DavHost", response.permanentlyRedirectedUrl)
+ }
+
+ // store dav options send by server
+ if (response && response.davOptions) {
+ syncData.accountData.setAccountProperty(job + "DavOptions", response.davOptions.split(",").map(e => e.trim()));
+ }
+
+ // allow 404 because iCloud sends it on valid answer (yeah!)
+ if (response && response.multi) {
+ principal = dav.tools.getNodeTextContentFromMultiResponse(response, [["d","prop"], ["d","current-user-principal"], ["d","href"]], null, ["200","404"]);
+ }
+ }
+
+ //principal now contains something like "/remote.php/carddav/principals/john.bieling/"
+ //principal can also be an absolute url
+ // -> get home/root of storage
+ if (principal !== null) {
+ syncData.setSyncState("send.getfolders");
+
+ let options = syncData.accountData.getAccountProperty(job + "DavOptions");
+
+ let homeset = (job == "cal")
+ ? "calendar-home-set"
+ : "addressbook-home-set";
+
+ let request = "<d:propfind "+dav.tools.xmlns(["d", job, "cs"])+"><d:prop><"+job+":" + homeset + " />"
+ + (job == "cal" && options.includes("calendar-proxy") ? "<cs:calendar-proxy-write-for /><cs:calendar-proxy-read-for />" : "")
+ + "<d:group-membership />"
+ + "</d:prop></d:propfind>";
+
+ let response = await dav.network.sendRequest(request, principal, "PROPFIND", syncData.connectionData, {"Depth": "0", "Prefer": "return=minimal"});
+ syncData.setSyncState("eval.folders");
+
+ // keep track of permanent redirects for the principal URL
+ if (response && response.permanentlyRedirectedUrl) {
+ principal = response.permanentlyRedirectedUrl;
+ }
+
+ own = dav.tools.getNodesTextContentFromMultiResponse(response, [["d","prop"], [job, homeset ], ["d","href"]], principal);
+ home = own.concat(dav.tools.getNodesTextContentFromMultiResponse(response, [["d","prop"], ["cs", "calendar-proxy-read-for" ], ["d","href"]], principal));
+ home = home.concat(dav.tools.getNodesTextContentFromMultiResponse(response, [["d","prop"], ["cs", "calendar-proxy-write-for" ], ["d","href"]], principal));
+
+ //Any groups we need to find? Only diving one level at the moment,
+ let g = dav.tools.getNodesTextContentFromMultiResponse(response, [["d","prop"], ["d", "group-membership" ], ["d","href"]], principal);
+ for (let gc=0; gc < g.length; gc++) {
+ //SOGo reports a 403 if I request the provided resource, also since we do not dive, remove the request for group-membership
+ response = await dav.network.sendRequest(request.replace("<d:group-membership />",""), g[gc], "PROPFIND", syncData.connectionData, {"Depth": "0", "Prefer": "return=minimal"}, {softfail: [403, 404]});
+ if (response && response.softerror) {
+ continue;
+ }
+ home = home.concat(dav.tools.getNodesTextContentFromMultiResponse(response, [["d","prop"], [job, homeset ], ["d","href"]], g[gc]));
+ }
+
+ //calendar-proxy and group-membership could have returned the same values, make the homeset unique
+ home = home.filter((v,i,a) => a.indexOf(v) == i);
+ } else {
+ // do not throw here, but log the error and skip this server
+ TbSync.eventlog.add("error", syncData.eventLogInfo, job+"davservernotfound", davjobs[job].server);
+ }
+
+ //home now contains something like /remote.php/caldav/calendars/john.bieling/
+ // -> get all resources
+ if (home.length > 0) {
+ // the used principal returned valid resources, store/update it
+ // as the principal is being used as a starting point, it must be stored as absolute url
+ syncData.accountData.setAccountProperty(job + "DavPrincipal", dav.network.startsWithScheme(principal)
+ ? principal
+ : "http" + (syncData.connectionData.https ? "s" : "") + "://" + syncData.connectionData.fqdn + principal);
+
+ for (let h=0; h < home.length; h++) {
+ syncData.setSyncState("send.getfolders");
+ let request = (job == "cal")
+ ? "<d:propfind "+dav.tools.xmlns(["d","apple","cs"])+"><d:prop><d:current-user-privilege-set/><d:resourcetype /><d:displayname /><apple:calendar-color/><cs:source/></d:prop></d:propfind>"
+ : "<d:propfind "+dav.tools.xmlns(["d"])+"><d:prop><d:current-user-privilege-set/><d:resourcetype /><d:displayname /></d:prop></d:propfind>";
+
+ //some servers report to have calendar-proxy-read but return a 404 when that gets actually queried
+ let response = await dav.network.sendRequest(request, home[h], "PROPFIND", syncData.connectionData, {"Depth": "1", "Prefer": "return=minimal"}, {softfail: [403, 404]});
+ if (response && response.softerror) {
+ continue;
+ }
+
+ for (let r=0; r < response.multi.length; r++) {
+ if (response.multi[r].status != "200") continue;
+
+ let resourcetype = null;
+ //is this a result with a valid recourcetype? (the node must be present)
+ switch (job) {
+ case "card":
+ if (dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["d","resourcetype"], ["card", "addressbook"]]) !== null) resourcetype = "carddav";
+ break;
+
+ case "cal":
+ if (dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["d","resourcetype"], ["cal", "calendar"]]) !== null) resourcetype = "caldav";
+ else if (dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["d","resourcetype"], ["cs", "subscribed"]]) !== null) resourcetype = "ics";
+ break;
+ }
+ if (resourcetype === null) continue;
+
+ //get ACL (grant read rights per default, if it is SOGo, as they do not send that permission)
+ let acl = isSogo ? 0x1 : 0;
+
+ let privilegNode = dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["d","current-user-privilege-set"]]);
+ if (privilegNode) {
+ if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "all").length > 0) {
+ acl = 0xF; //read=1, mod=2, create=4, delete=8
+ } else {
+ // check for individual write permissions
+ if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "write").length > 0) {
+ acl = 0xF;
+ } else {
+ if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "write-content").length > 0) acl |= 0x2;
+ if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "bind").length > 0) acl |= 0x4;
+ if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "unbind").length > 0) acl |= 0x8;
+ }
+
+ // check for read permission (implying read if any write is given)
+ if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "read").length > 0 || acl != 0) acl |= 0x1;
+ }
+ }
+
+ //ignore this resource, if no read access
+ if ((acl & 0x1) == 0) continue;
+
+ let href = response.multi[r].href;
+ if (resourcetype == "ics") href = dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["cs","source"], ["d","href"]]).textContent;
+
+ let name_node = dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["d","displayname"]]);
+ let name = TbSync.getString("defaultname." + ((job == "cal") ? "calendar" : "contacts") , "dav");
+ if (name_node != null) {
+ name = name_node.textContent;
+ }
+ let color = dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["apple","calendar-color"]]);
+
+ //remove found folder from list of unhandled folders
+ unhandledFolders[resourcetype] = unhandledFolders[resourcetype].filter(item => item.getFolderProperty("href") !== href);
+
+
+ // interaction with TbSync
+ // do we have a folder for that href?
+ let folderData = syncData.accountData.getFolder("href", href);
+ if (!folderData) {
+ // create a new folder entry
+ folderData = syncData.accountData.createNewFolder();
+ // this MUST be set to either "addressbook" or "calendar" to use the standard target support, or any other value, which
+ // requires a corresponding targets implementation by this provider
+ folderData.setFolderProperty("targetType", (job == "card") ? "addressbook" : "calendar");
+
+ folderData.setFolderProperty("href", href);
+ folderData.setFolderProperty("foldername", name);
+ folderData.setFolderProperty("type", resourcetype);
+ folderData.setFolderProperty("shared", !own.includes(home[h]));
+ folderData.setFolderProperty("acl", acl.toString());
+ folderData.setFolderProperty("downloadonly", (acl == 0x1)); //if any write access is granted, setup as writeable
+
+ //we assume the folder has the same fqdn as the homeset, otherwise href must contain the full URL and the fqdn is ignored
+ folderData.setFolderProperty("fqdn", syncData.connectionData.fqdn);
+ folderData.setFolderProperty("https", syncData.connectionData.https);
+
+ //do we have a cached folder?
+ let cachedFolderData = syncData.accountData.getFolderFromCache("href", href);
+ if (cachedFolderData) {
+ // copy fields from cache which we want to re-use
+ folderData.setFolderProperty("targetColor", cachedFolderData.getFolderProperty("targetColor"));
+ folderData.setFolderProperty("targetName", cachedFolderData.getFolderProperty("targetName"));
+ //if we have only READ access, do not restore cached value for downloadonly
+ if (acl > 0x1) folderData.setFolderProperty("downloadonly", cachedFolderData.getFolderProperty("downloadonly"));
+ }
+ } else {
+ //Update name & color
+ folderData.setFolderProperty("foldername", name);
+ folderData.setFolderProperty("fqdn", syncData.connectionData.fqdn);
+ folderData.setFolderProperty("https", syncData.connectionData.https);
+ folderData.setFolderProperty("acl", acl);
+ //if the acl changed from RW to RO we need to update the downloadonly setting
+ if (acl == 0x1) {
+ folderData.setFolderProperty("downloadonly", true);
+ }
+ }
+
+ // Update color from server.
+ if (color && job == "cal") {
+ color = color.textContent.substring(0,7);
+ folderData.setFolderProperty("targetColor", color);
+
+ // Do we have to update the calendar?
+ if (folderData.targetData && folderData.targetData.hasTarget()) {
+ try {
+ let targetCal = await folderData.targetData.getTarget();
+ targetCal.calendar.setProperty("color", color);
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+ }
+ }
+ }
+ }
+ } else {
+ //home was not found - connection error? - do not delete unhandled folders
+ switch (job) {
+ case "card":
+ unhandledFolders.carddav = [];
+ break;
+
+ case "cal":
+ unhandledFolders.caldav = [];
+ unhandledFolders.ics = [];
+ break;
+ }
+ //reset stored principal
+ syncData.accountData.resetAccountProperty(job + "DavPrincipal");
+ }
+ }
+
+ // Remove unhandled old folders, (because they no longer exist on the server).
+ // Do not delete the targets, but keep them as stale/unconnected elements.
+ for (let type of folderTypes) {
+ for (let folder of unhandledFolders[type]) {
+ folder.remove("[deleted on server]");
+ }
+ }
+ },
+
+
+
+
+
+
+ folder: async function (syncData) {
+ // add connection data to syncData
+ syncData.connectionData = new dav.network.ConnectionData(syncData);
+
+ // add target to syncData
+ let hadTarget;
+ try {
+ // accessing the target for the first time will check if it is avail and if not will create it (if possible)
+ hadTarget = syncData.currentFolderData.targetData.hasTarget();
+ syncData.target = await syncData.currentFolderData.targetData.getTarget();
+ } catch (e) {
+ Components.utils.reportError(e);
+ throw dav.sync.finish("warning", e.message);
+ }
+
+ switch (syncData.currentFolderData.getFolderProperty("type")) {
+ case "carddav":
+ {
+ await dav.sync.singleFolder(syncData);
+ }
+ break;
+
+ case "caldav":
+ case "ics":
+ {
+ // update downloadonly - we do not use TbCalendar (syncData.target) but the underlying lightning calendar obj
+ if (syncData.currentFolderData.getFolderProperty("downloadonly")) syncData.target.calendar.setProperty("readOnly", true);
+
+ // update username of calendar
+ syncData.target.calendar.setProperty("username", syncData.connectionData.username);
+
+ //init sync via lightning
+ if (hadTarget) syncData.target.calendar.refresh();
+
+ throw dav.sync.finish("ok", "managed-by-thunderbird");
+ }
+ break;
+
+ default:
+ {
+ throw dav.sync.finish("warning", "notsupported");
+ }
+ break;
+ }
+ },
+
+
+ singleFolder: async function (syncData) {
+ let downloadonly = syncData.currentFolderData.getFolderProperty("downloadonly");
+
+ // we have to abort sync of this folder, if it is contact, has groupSync enabled and gContactSync is enabled
+ let syncGroups = syncData.accountData.getAccountProperty("syncGroups");
+ let gContactSync = await AddonManager.getAddonByID("gContactSync@pirules.net") ;
+ let contactSync = (syncData.currentFolderData.getFolderProperty("type") == "carddav");
+ if (syncGroups && contactSync && gContactSync && gContactSync.isActive) {
+ throw dav.sync.finish("warning", "gContactSync");
+ }
+
+ await dav.sync.remoteChanges(syncData);
+ let numOfLocalChanges = await dav.sync.localChanges(syncData);
+
+ //revert all local changes on permission error by doing a clean sync
+ if (numOfLocalChanges < 0) {
+ dav.sync.resetFolderSyncInfo(syncData.currentFolderData);
+ await dav.sync.remoteChanges(syncData);
+
+ if (!downloadonly) throw dav.sync.finish("info", "info.restored");
+ } else if (numOfLocalChanges > 0){
+ //we will get back our own changes and can store etags and vcards and also get a clean ctag/token
+ await dav.sync.remoteChanges(syncData);
+ }
+ },
+
+
+
+
+
+
+
+
+
+
+ remoteChanges: async function (syncData) {
+ //Do we have a sync token? No? -> Initial Sync (or WebDAV sync not supported) / Yes? -> Get updates only (token only present if WebDAV sync is suported)
+ let token = syncData.currentFolderData.getFolderProperty("token");
+ if (token) {
+ //update via token sync
+ let tokenSyncSucceeded = await dav.sync.remoteChangesByTOKEN(syncData);
+ if (tokenSyncSucceeded) return;
+
+ //token sync failed, reset ctag and token and do a full sync
+ dav.sync.resetFolderSyncInfo(syncData.currentFolderData);
+ }
+
+ //Either token sync did not work or there is no token (initial sync)
+ //loop until ctag is the same before and after polling data (sane start condition)
+ let maxloops = 20;
+ for (let i=0; i <= maxloops; i++) {
+ if (i == maxloops)
+ throw dav.sync.finish("warning", "could-not-get-stable-ctag");
+
+ let ctagChanged = await dav.sync.remoteChangesByCTAG(syncData);
+ if (!ctagChanged) break;
+ }
+ },
+
+ remoteChangesByTOKEN: async function (syncData) {
+ syncData.progressData.reset();
+
+ let token = syncData.currentFolderData.getFolderProperty("token");
+ syncData.setSyncState("send.request.remotechanges");
+ let cards = await dav.network.sendRequest("<d:sync-collection "+dav.tools.xmlns(["d"])+"><d:sync-token>"+token+"</d:sync-token><d:sync-level>1</d:sync-level><d:prop><d:getetag/></d:prop></d:sync-collection>", syncData.currentFolderData.getFolderProperty("href"), "REPORT", syncData.connectionData, {}, {softfail: [415,403,409]});
+
+ //EteSync throws 409 because it does not support sync-token
+ //Sabre\DAV\Exception\ReportNotSupported - Unsupported media type - returned by fruux if synctoken is 0 (empty book), 415 & 403
+ //https://github.com/sabre-io/dav/issues/1075
+ //Sabre\DAV\Exception\InvalidSyncToken (403)
+ if (cards && cards.softerror) {
+ //token sync failed, reset ctag and do a full sync
+ return false;
+ }
+
+ let tokenNode = dav.tools.evaluateNode(cards.node, [["d", "sync-token"]]);
+ if (tokenNode === null) {
+ //token sync failed, reset ctag and do a full sync
+ return false;
+ }
+
+ let vCardsDeletedOnServer = [];
+ let vCardsChangedOnServer = {};
+
+ let localDeletes = syncData.target.getDeletedItemsFromChangeLog();
+
+ let cardsFound = 0;
+ for (let c=0; c < cards.multi.length; c++) {
+ let id = cards.multi[c].href;
+ if (id !==null) {
+ //valid
+ let card = await syncData.target.getItemFromProperty("X-DAV-HREF", id);
+ if (cards.multi[c].status == "200") {
+ //MOD or ADD
+ let etag = dav.tools.evaluateNode(cards.multi[c].node, [["d","prop"], ["d","getetag"]]);
+ if (!card) {
+ //if the user deleted this card (not yet send to server), do not add it again
+ if (!localDeletes.includes(id)) {
+ cardsFound++;
+ vCardsChangedOnServer[id] = "ADD";
+ }
+ } else if (etag.textContent != card.getProperty("X-DAV-ETAG")) {
+ cardsFound++;
+ vCardsChangedOnServer[id] = "MOD";
+ }
+ } else if (cards.multi[c].responsestatus == "404" && card) {
+ //DEL
+ cardsFound++;
+ vCardsDeletedOnServer.push(card);
+ } else {
+ //We received something, that is not a DEL, MOD or ADD
+ TbSync.eventlog.add("warning", syncData.eventLogInfo, "Unknown XML", JSON.stringify(cards.multi[c]));
+ }
+ }
+ }
+
+ // reset sync process
+ syncData.progressData.reset(0, cardsFound);
+
+ //download all cards added to vCardsChangedOnServer and process changes
+ await dav.sync.multiget(syncData, vCardsChangedOnServer);
+
+ //delete all contacts added to vCardsDeletedOnServer
+ await dav.sync.deleteContacts (syncData, vCardsDeletedOnServer);
+
+ //update token
+ syncData.currentFolderData.setFolderProperty("token", tokenNode.textContent);
+
+ return true;
+ },
+
+ remoteChangesByCTAG: async function (syncData) {
+ syncData.progressData.reset();
+
+ //Request ctag and token
+ syncData.setSyncState("send.request.remotechanges");
+ let response = await dav.network.sendRequest("<d:propfind "+dav.tools.xmlns(["d", "cs"])+"><d:prop><cs:getctag /><d:sync-token /></d:prop></d:propfind>", syncData.currentFolderData.getFolderProperty("href"), "PROPFIND", syncData.connectionData, {"Depth": "0"});
+
+ syncData.setSyncState("eval.response.remotechanges");
+ let ctag = dav.tools.getNodeTextContentFromMultiResponse(response, [["d","prop"], ["cs", "getctag"]], syncData.currentFolderData.getFolderProperty("href"));
+ let token = dav.tools.getNodeTextContentFromMultiResponse(response, [["d","prop"], ["d", "sync-token"]], syncData.currentFolderData.getFolderProperty("href"));
+
+ let localDeletes = syncData.target.getDeletedItemsFromChangeLog();
+
+ //if CTAG changed, we need to sync everything and compare
+ if (ctag === null || ctag != syncData.currentFolderData.getFolderProperty("ctag")) {
+ let vCardsFoundOnServer = [];
+ let vCardsChangedOnServer = {};
+
+ //get etags of all cards on server and find the changed cards
+ syncData.setSyncState("send.request.remotechanges");
+ let cards = await dav.network.sendRequest("<d:propfind "+dav.tools.xmlns(["d"])+"><d:prop><d:getetag /></d:prop></d:propfind>", syncData.currentFolderData.getFolderProperty("href"), "PROPFIND", syncData.connectionData, {"Depth": "1", "Prefer": "return=minimal"});
+
+ //to test other impl
+ //let cards = await dav.network.sendRequest("<d:propfind "+dav.tools.xmlns(["d"])+"><d:prop><d:getetag /></d:prop></d:propfind>", syncData.currentFolderData.getFolderProperty("href"), "PROPFIND", syncData.connectionData, {"Depth": "1", "Prefer": "return=minimal"}, {softfail: []}, false);
+
+ //this is the same request, but includes getcontenttype, do we need it? icloud send contacts without
+ //let cards = await dav.network.sendRequest("<d:propfind "+dav.tools.xmlns(["d"])+"><d:prop><d:getetag /><d:getcontenttype /></d:prop></d:propfind>", syncData.currentFolderData.getFolderProperty("href"), "PROPFIND", syncData.connectionData, {"Depth": "1", "Prefer": "return=minimal"});
+
+ //play with filters and limits for synology
+ /*
+ let additional = "<card:limit><card:nresults>10</card:nresults></card:limit>";
+ additional += "<card:filter test='anyof'>";
+ additional += "<card:prop-filter name='FN'>";
+ additional += "<card:text-match negate-condition='yes' match-type='equals'>bogusxy</card:text-match>";
+ additional += "</card:prop-filter>";
+ additional += "</card:filter>";*/
+
+ //addressbook-query does not work on older servers (zimbra)
+ //let cards = await dav.network.sendRequest("<card:addressbook-query "+dav.tools.xmlns(["d", "card"])+"><d:prop><d:getetag /></d:prop></card:addressbook-query>", syncData.currentFolderData.getFolderProperty("href"), "REPORT", syncData.connectionData, {"Depth": "1", "Prefer": "return=minimal"});
+
+ syncData.setSyncState("eval.response.remotechanges");
+ let cardsFound = 0;
+ for (let c=0; cards.multi && c < cards.multi.length; c++) {
+ let id = cards.multi[c].href;
+ if (id == syncData.currentFolderData.getFolderProperty("href")) {
+ //some servers (Radicale) report the folder itself and a querry to that would return everything again
+ continue;
+ }
+ let etag = dav.tools.evaluateNode(cards.multi[c].node, [["d","prop"], ["d","getetag"]]);
+
+ //ctype is currently not used, because iCloud does not send one and sabre/dav documentation is not checking ctype
+ //let ctype = dav.tools.evaluateNode(cards.multi[c].node, [["d","prop"], ["d","getcontenttype"]]);
+
+ if (cards.multi[c].status == "200" && etag !== null && id !== null /* && ctype !== null */) { //we do not actually check the content of ctype - but why do we request it? iCloud seems to send cards without ctype
+ vCardsFoundOnServer.push(id);
+ let card = await syncData.target.getItemFromProperty("X-DAV-HREF", id);
+ if (!card) {
+ //if the user deleted this card (not yet send to server), do not add it again
+ if (!localDeletes.includes(id)) {
+ cardsFound++;
+ vCardsChangedOnServer[id] = "ADD";
+ }
+ } else if (etag.textContent != card.getProperty("X-DAV-ETAG")) {
+ cardsFound++;
+ vCardsChangedOnServer[id] = "MOD";
+ }
+ }
+ }
+
+ //FIND DELETES: loop over current addressbook and check each local card if it still exists on the server
+ let vCardsDeletedOnServer = [];
+ let localAdditions = syncData.target.getAddedItemsFromChangeLog();
+ let allItems = syncData.target.getAllItems()
+ for (let card of allItems) {
+ let id = card.getProperty("X-DAV-HREF");
+ if (id && !vCardsFoundOnServer.includes(id) && !localAdditions.includes(id)) {
+ //delete request from server
+ cardsFound++;
+ vCardsDeletedOnServer.push(card);
+ }
+ }
+
+ // reset sync process
+ syncData.progressData.reset(0, cardsFound);
+
+ //download all cards added to vCardsChangedOnServer and process changes
+ await dav.sync.multiget(syncData, vCardsChangedOnServer);
+
+ //delete all contacts added to vCardsDeletedOnServer
+ await dav.sync.deleteContacts (syncData, vCardsDeletedOnServer);
+
+ //update ctag and token (if there is one)
+ if (ctag === null) return false; //if server does not support ctag, "it did not change"
+ syncData.currentFolderData.setFolderProperty("ctag", ctag);
+ if (token) syncData.currentFolderData.setFolderProperty("token", token);
+
+ //ctag did change
+ return true;
+ } else {
+
+ //ctag did not change
+ return false;
+ }
+
+ },
+
+
+
+ multiget: async function (syncData, vCardsChangedOnServer) {
+ //keep track of found mailing lists and its members
+ syncData.foundMailingListsDuringDownSync = {};
+
+ //download all changed cards and process changes
+ let cards2catch = Object.keys(vCardsChangedOnServer);
+ let maxitems = dav.sync.prefSettings.getIntPref("maxitems");
+
+ for (let i=0; i < cards2catch.length; i+=maxitems) {
+ let request = dav.tools.getMultiGetRequest(cards2catch.slice(i, i+maxitems));
+ if (request) {
+ syncData.setSyncState("send.request.remotechanges");
+ let cards = await dav.network.sendRequest(request, syncData.currentFolderData.getFolderProperty("href"), "REPORT", syncData.connectionData, {"Depth": "1"});
+
+ syncData.setSyncState("eval.response.remotechanges");
+ for (let c=0; c < cards.multi.length; c++) {
+ syncData.progressData.inc();
+ let id = cards.multi[c].href;
+ let etag = dav.tools.evaluateNode(cards.multi[c].node, [["d","prop"], ["d","getetag"]]);
+ let data = dav.tools.evaluateNode(cards.multi[c].node, [["d","prop"], ["card","address-data"]]);
+
+ if (cards.multi[c].status == "200" && etag !== null && data !== null && id !== null && vCardsChangedOnServer.hasOwnProperty(id)) {
+ switch (vCardsChangedOnServer[id]) {
+ case "ADD":
+ await dav.tools.addContact (syncData, id, data, etag);
+ break;
+
+ case "MOD":
+ await dav.tools.modifyContact (syncData, id, data, etag);
+ break;
+ }
+ //Feedback from users: They want to see the individual count
+ syncData.setSyncState("eval.response.remotechanges");
+ await TbSync.tools.sleep(100);
+ } else {
+ TbSync.dump("Skipped Card", [id, cards.multi[c].status == "200", etag !== null, data !== null, id !== null, vCardsChangedOnServer.hasOwnProperty(id)].join(", "));
+ }
+ }
+ }
+ }
+ // Feedback from users: They want to see the final count.
+ syncData.setSyncState("eval.response.remotechanges");
+ await TbSync.tools.sleep(200);
+
+ // On down sync, mailinglists need to be done at the very end so all member data is avail.
+ if (syncData.accountData.getAccountProperty("syncGroups")) {
+ let l=0;
+ for (let listID in syncData.foundMailingListsDuringDownSync) {
+ if (syncData.foundMailingListsDuringDownSync.hasOwnProperty(listID)) {
+ l++;
+
+ let list = await syncData.target.getItemFromProperty("X-DAV-HREF", listID);
+ if (!list.isMailList)
+ continue;
+
+ //CardInfo contains the name and the X-DAV-UID list of the members
+ let vCardInfo = dav.tools.getGroupInfoFromCardData(syncData.foundMailingListsDuringDownSync[listID].vCardData, syncData.target);
+ let oCardInfo = dav.tools.getGroupInfoFromCardData(syncData.foundMailingListsDuringDownSync[listID].oCardData, syncData.target);
+
+ // Smart merge: oCardInfo contains the state during last sync, vCardInfo is the current state.
+ // By comparing we can learn, which member was deleted by the server (in old but not in new),
+ // and which one was added (in new but not in old)
+ let removedMembers = oCardInfo.members.filter(e => !vCardInfo.members.includes(e));
+ let newMembers = vCardInfo.members.filter(e => !oCardInfo.members.includes(e));
+
+ // Check that all new members have an email address (fix for bug 1522453)
+ let m=0;
+ for (let member of newMembers) {
+ let card = await syncData.target.getItemFromProperty("X-DAV-UID", member);
+ if (card) {
+ let email = card.getProperty("PrimaryEmail");
+ if (!email) {
+ let email = Date.now() + "." + l + "." + m + "@bug1522453";
+ card.setProperty("PrimaryEmail", email);
+ syncData.target.modifyItem(card);
+ }
+ } else {
+ TbSync.dump("Member not found: " + member);
+ }
+ m++;
+ }
+
+ // if any of the to-be-removed members are not members of the local list, they are skipt
+ // if any of the to-be-added members are already members of the local list, they are skipt
+ list.removeListMembers("X-DAV-UID", removedMembers);
+ list.addListMembers("X-DAV-UID", newMembers);
+ syncData.target.modifyItem(list);
+ }
+ }
+ }
+ },
+
+ deleteContacts: async function (syncData, cards2delete) {
+ let maxitems = dav.sync.prefSettings.getIntPref("maxitems");
+
+ // try to show a progress based on maxitens during delete and not delete all at once
+ for (let i=0; i < cards2delete.length; i+=maxitems) {
+ //get size of next block
+ let remain = (cards2delete.length - i);
+ let chunk = Math.min(remain, maxitems);
+
+ syncData.progressData.inc(chunk);
+ syncData.setSyncState("eval.response.remotechanges");
+ await TbSync.tools.sleep(200); //we want the user to see, that deletes are happening
+
+ for (let j=0; j < chunk; j++) {
+ syncData.target.deleteItem(cards2delete[i+j]);
+ }
+ }
+ },
+
+
+
+
+ localChanges: async function (syncData) {
+ //define how many entries can be send in one request
+ let maxitems = dav.sync.prefSettings.getIntPref("maxitems");
+
+ let downloadonly = syncData.currentFolderData.getFolderProperty("downloadonly");
+
+ let permissionErrors = 0;
+ let permissionError = { //keep track of permission errors - preset with downloadonly status to skip sync in that case
+ "added_by_user": downloadonly,
+ "modified_by_user": downloadonly,
+ "deleted_by_user": downloadonly
+ };
+
+ let syncGroups = syncData.accountData.getAccountProperty("syncGroups");
+
+ //access changelog to get local modifications (done and todo are used for UI to display progress)
+ syncData.progressData.reset(0, syncData.target.getItemsFromChangeLog().length);
+
+ do {
+ syncData.setSyncState("prepare.request.localchanges");
+
+ //get changed items from ChangeLog
+ let changes = syncData.target.getItemsFromChangeLog(maxitems);
+ if (changes.length == 0)
+ break;
+
+ for (let i=0; i < changes.length; i++) {
+ switch (changes[i].status) {
+ case "added_by_user":
+ case "modified_by_user":
+ {
+ let isAdding = (changes[i].status == "added_by_user");
+ if (!permissionError[changes[i].status]) { //if this operation failed already, do not retry
+
+ let card = await syncData.target.getItem(changes[i].itemId);
+ if (card) {
+ if (card.isMailList && !syncGroups) {
+ // Conditionally break out of the switch early, but do
+ // execute the cleanup code below the switch. A continue would
+ // miss that.
+ break;
+ }
+
+ let vcard = card.isMailList
+ ? dav.tools.getVCardFromThunderbirdListCard(syncData, card, isAdding)
+ : dav.tools.getVCardFromThunderbirdContactCard(syncData, card, isAdding);
+ let headers = {"Content-Type": "text/vcard; charset=utf-8"};
+ //if (!isAdding) headers["If-Match"] = vcard.etag;
+
+ syncData.setSyncState("send.request.localchanges");
+ if (isAdding || vcard.modified) {
+ let response = await dav.network.sendRequest(vcard.data, card.getProperty("X-DAV-HREF"), "PUT", syncData.connectionData, headers, {softfail: [403,405]});
+
+ syncData.setSyncState("eval.response.localchanges");
+ if (response && response.softerror) {
+ permissionError[changes[i].status] = true;
+ TbSync.eventlog.add("warning", syncData.eventLogInfo, "missing-permission::" + TbSync.getString(isAdding ? "acl.add" : "acl.modify", "dav"));
+ }
+ }
+ } else {
+ TbSync.eventlog.add("warning", syncData.eventLogInfo, "cardnotfoundbutinchangelog::" + changes[i].itemId + "/" + changes[i].status);
+ }
+ }
+
+ if (permissionError[changes[i].status]) {
+ //we where not allowed to add or modify that card, remove it, we will get a fresh copy on the following revert
+ let card = await syncData.target.getItem(changes[i].itemId);
+ if (card) syncData.target.deleteItem(card);
+ permissionErrors++;
+ }
+ }
+ break;
+
+ case "deleted_by_user":
+ {
+ if (!permissionError[changes[i].status]) { //if this operation failed already, do not retry
+ syncData.setSyncState("send.request.localchanges");
+ let response = await dav.network.sendRequest("", changes[i].itemId , "DELETE", syncData.connectionData, {}, {softfail: [403, 404, 405]});
+
+ syncData.setSyncState("eval.response.localchanges");
+ if (response && response.softerror) {
+ if (response.softerror != 404) { //we cannot do anything about a 404 on delete, the card has been deleted here and is not avail on server
+ permissionError[changes[i].status] = true;
+ TbSync.eventlog.add("warning", syncData.eventLogInfo, "missing-permission::" + TbSync.getString("acl.delete", "dav"));
+ }
+ }
+ }
+
+ if (permissionError[changes[i].status]) {
+ permissionErrors++;
+ }
+ }
+ break;
+ }
+
+ syncData.target.removeItemFromChangeLog(changes[i].itemId);
+ syncData.progressData.inc(); //UI feedback
+ }
+
+
+ } while (true);
+
+ //return number of modified cards or the number of permission errors (negativ)
+ return (permissionErrors > 0 ? 0 - permissionErrors : syncData.progressData.done);
+ },
+}
diff --git a/unused/vcard/LICENSE b/unused/vcard/LICENSE
new file mode 100644
index 0000000..fa9dba1
--- /dev/null
+++ b/unused/vcard/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Aleksandr Kitov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/unused/vcard/SOURCE b/unused/vcard/SOURCE
new file mode 100644
index 0000000..21d88d2
--- /dev/null
+++ b/unused/vcard/SOURCE
@@ -0,0 +1 @@
+https://github.com/Heymdall/vcard/releases/tag/v0.2.7 \ No newline at end of file
diff --git a/unused/vcard/vcard.js b/unused/vcard/vcard.js
new file mode 100644
index 0000000..d1b9757
--- /dev/null
+++ b/unused/vcard/vcard.js
@@ -0,0 +1,305 @@
+(function (namespace) {
+ var PREFIX = 'BEGIN:VCARD',
+ POSTFIX = 'END:VCARD';
+
+ /**
+ * Return json representation of vCard
+ * @param {string} string raw vCard
+ * @returns {*}
+ */
+ function parse(string) {
+ var result = {},
+ lines = string.split(/\r\n|\r|\n/),
+ count = lines.length,
+ pieces,
+ key,
+ value,
+ meta,
+ namespace;
+
+ for (var i = 0; i < count; i++) {
+ if (lines[i] == '') {
+ continue;
+ }
+ if (lines[i].toUpperCase() == PREFIX || lines[i].toUpperCase() == POSTFIX) {
+ continue;
+ }
+ var data = lines[i];
+
+ /**
+ * Check that next line continues current
+ * @param {number} i
+ * @returns {boolean}
+ */
+ var isValueContinued = function (i) {
+ return i + 1 < count && (lines[i + 1][0] == ' ' || lines[i + 1][0] == '\t');
+ };
+ // handle multiline properties (i.e. photo).
+ // next line should start with space or tab character
+ if (isValueContinued(i)) {
+ while (isValueContinued(i)) {
+ data += lines[i + 1].trim();
+ i++;
+ }
+ }
+
+ pieces = data.split(':');
+ key = pieces.shift();
+ value = pieces.join(':');
+ namespace = false;
+ meta = {};
+
+ // meta fields in property
+ if (key.match(/;/)) {
+ key = key
+ .replace(/\\;/g, 'ΩΩΩ')
+ .replace(/\\,/, ',');
+ var metaArr = key.split(';').map(function (item) {
+ return item.replace(/ΩΩΩ/g, ';');
+ });
+ key = metaArr.shift();
+ metaArr.forEach(function (item) {
+ var arr = item.split('=');
+ arr[0] = arr[0].toLowerCase();
+ if (arr[0].length === 0) {
+ return;
+ }
+ if (arr.length>1) {
+ //removing boundary quotes and splitting up values, if send as list - upperCase for hitory reasons
+ let metavalue = arr[1].replace (/(^")|("$)/g, '').toUpperCase().split(",");
+ if (meta[arr[0]]) {
+ meta[arr[0]].push(...metavalue);
+ } else {
+ meta[arr[0]] = metavalue;
+ }
+ }
+ });
+ }
+
+ // values with \n
+ value = value
+ .replace(/\\r/g, '')
+ .replace(/\\n/g, '\n');
+
+ value = tryToSplit(value);
+
+ // Grouped properties
+ if (key.match(/\./)) {
+ var arr = key.split('.');
+ key = arr[1];
+ namespace = arr[0];
+ }
+
+ var newValue = {
+ value: value
+ };
+ if (Object.keys(meta).length) {
+ newValue.meta = meta;
+ }
+ if (namespace) {
+ newValue.namespace = namespace;
+ }
+
+ if (key.indexOf('X-') !== 0) {
+ key = key.toLowerCase();
+ }
+
+ if (typeof result[key] === 'undefined') {
+ result[key] = [newValue];
+ } else {
+ result[key].push(newValue);
+ }
+
+ }
+
+ return result;
+ }
+
+ var HAS_SEMICOLON_SEPARATOR = /[^\\];|^;/,
+ HAS_COMMA_SEPARATOR = /[^\\],|^,/;
+ /**
+ * Split value by "," or ";" and remove escape sequences for this separators
+ * @param {string} value
+ * @returns {string|string[]
+ */
+ function tryToSplit(value) {
+ if (value.match(HAS_SEMICOLON_SEPARATOR)) {
+ value = value.replace(/\\,/g, ',');
+ return splitValue(value, ';');
+ } else if (value.match(HAS_COMMA_SEPARATOR)) {
+ value = value.replace(/\\;/g, ';');
+ return splitValue(value, ',');
+ } else {
+ return value
+ .replace(/\\,/g, ',')
+ .replace(/\\;/g, ';');
+ }
+ }
+ /**
+ * Split vcard field value by separator
+ * @param {string|string[]} value
+ * @param {string} separator
+ * @returns {string|string[]}
+ */
+ function splitValue(value, separator) {
+ var separatorRegexp = new RegExp(separator);
+ var escapedSeparatorRegexp = new RegExp('\\\\' + separator, 'g');
+ // easiest way, replace it with really rare character sequence
+ value = value.replace(escapedSeparatorRegexp, 'ΩΩΩ');
+ if (value.match(separatorRegexp)) {
+ value = value.split(separator);
+
+ value = value.map(function (item) {
+ return item.replace(/ΩΩΩ/g, separator);
+ });
+ } else {
+ value = value.replace(/ΩΩΩ/g, separator);
+ }
+ return value;
+ }
+
+ var guid = (function() {
+ function s4() {
+ return Math.floor((1 + Math.random()) * 0x10000)
+ .toString(16)
+ .substring(1);
+ }
+ return function() {
+ return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
+ s4() + '-' + s4() + s4() + s4();
+ };
+ })();
+
+ var COMMA_SEPARATED_FIELDS = ['nickname', 'related', 'categories', 'pid'];
+
+ /**
+ * Generate vCard representation af object
+ * @param {*} data
+ * @param {obj}
+ * member "simpleType" returns all types joined by , instead of multiple TYPE= entries
+ * member "addRequired" determine if generator should add required properties (version and uid)
+ * @returns {string}
+ */
+ function generate(data, options = {simpleType: true}) {
+ var lines = [PREFIX],
+ line = '';
+
+ if (options.addRequired && !data.version) {
+ data.version = [{value: '3.0'}];
+ }
+ if (options.addRequired && !data.uid) {
+ data.uid = [{value: guid()}];
+ }
+
+ var escapeCharacters = function (v) {
+ if (typeof v === 'undefined') {
+ return '';
+ }
+ return v
+ .replace(/\r\n|\r|\n/g, '\\n')
+ .replace(/;/g, '\\;')
+ .replace(/,/g, '\\,')
+ };
+
+ var escapeTypeCharacters = function(v) {
+ if (typeof v === 'undefined') {
+ return '';
+ }
+ return v
+ .replace(/\r\n|\r|\n/g, '\\n')
+ .replace(/;/g, '\\;')
+ };
+
+ Object.keys(data).forEach(function (key) {
+ if (!data[key] || typeof data[key].forEach !== 'function') {
+ return;
+ }
+ data[key].forEach(function (value) {
+ // ignore empty values
+ if (typeof value.value === 'undefined' || value.value == '') {
+ return;
+ }
+ // ignore empty array values
+ if (value.value instanceof Array) {
+ var empty = true;
+ for (var i = 0; i < value.value.length; i++) {
+ if (typeof value.value[i] !== 'undefined' && value.value[i] != '') {
+ empty = false;
+ break;
+ }
+ }
+ if (empty) {
+ return;
+ }
+ }
+ line = '';
+
+ // add namespace if exists
+ if (value.namespace) {
+ line += value.namespace + '.';
+ }
+ line += key.indexOf('X-') === 0 ? key : key.toUpperCase();
+
+ // add meta properties
+ if (typeof value.meta === 'object') {
+ Object.keys(value.meta).forEach(function (metaKey) {
+ // values of meta tags must be an array
+ if (typeof value.meta[metaKey].forEach !== 'function') {
+ return;
+ }
+ //join meta types so we get TYPE=a,b,c instead of TYPE=a;TYPE=b;TYPE=c
+ let metaArr = (options.simpleType && metaKey.toUpperCase() === 'TYPE') ? [value.meta[metaKey].join(",")] : value.meta[metaKey];
+ metaArr.forEach(function (metaValue) {
+ if (metaKey.length > 0) {
+ if (metaKey.toUpperCase() === 'TYPE') {
+ // Do not escape the comma when it is the type property. This breaks a lot.
+ line += ';' + escapeCharacters(metaKey.toUpperCase()) + '=' + escapeTypeCharacters(metaValue);
+ } else {
+ line += ';' + escapeCharacters(metaKey.toUpperCase()) + '=' + escapeCharacters(metaValue);
+ }
+ }
+ });
+ });
+ }
+
+ line += ':';
+
+
+
+ if (typeof value.value === 'string') {
+ line += escapeCharacters(value.value);
+ } else {
+ // list-values
+ var separator = COMMA_SEPARATED_FIELDS.indexOf(key) !== -1
+ ? ','
+ : ';';
+ line += value.value.map(function (item) {
+ return escapeCharacters(item);
+ }).join(separator);
+ }
+
+ // line-length limit. Content lines
+ // SHOULD be folded to a maximum width of 75 octets, excluding the line break.
+ if (line.length > 75) {
+ var firstChunk = line.substr(0, 75),
+ least = line.substr(75);
+ var splitted = least.match(/.{1,74}/g);
+ lines.push(firstChunk);
+ splitted.forEach(function (chunk) {
+ lines.push(' ' + chunk);
+ });
+ } else {
+ lines.push(line);
+ }
+ });
+ });
+
+ lines.push(POSTFIX);
+ return lines.join('\r\n');
+ }
+
+ namespace.vCard = {
+ parse: parse,
+ generate: generate
+ };
+})(this);