summaryrefslogtreecommitdiffstats
path: root/content
diff options
context:
space:
mode:
Diffstat (limited to 'content')
-rw-r--r--content/api/BootstrapLoader/CHANGELOG.md75
-rw-r--r--content/api/BootstrapLoader/README.md1
-rw-r--r--content/api/BootstrapLoader/implementation.js917
-rw-r--r--content/api/BootstrapLoader/schema.json61
-rw-r--r--content/bootstrap.js55
-rw-r--r--content/includes/calendarsync.js421
-rw-r--r--content/includes/contactsync.js542
-rw-r--r--content/includes/network.js1459
-rw-r--r--content/includes/sync.js1504
-rw-r--r--content/includes/tasksync.js211
-rw-r--r--content/includes/tools.js529
-rw-r--r--content/includes/wbxmltools.js877
-rw-r--r--content/includes/xmltools.js155
-rw-r--r--content/locales.js3
-rw-r--r--content/manager/createAccount.js244
-rw-r--r--content/manager/createAccount.xhtml97
-rw-r--r--content/manager/editAccountOverlay.js56
-rw-r--r--content/manager/editAccountOverlay.xhtml154
-rw-r--r--content/provider.js729
-rw-r--r--content/skin/365.svg12
-rw-r--r--content/skin/365_16.pngbin0 -> 445 bytes
-rw-r--r--content/skin/365_32.pngbin0 -> 720 bytes
-rw-r--r--content/skin/365_48.pngbin0 -> 982 bytes
-rw-r--r--content/skin/eas16.pngbin0 -> 664 bytes
-rw-r--r--content/skin/eas32.pngbin0 -> 1859 bytes
-rw-r--r--content/skin/eas64.pngbin0 -> 24072 bytes
-rw-r--r--content/skin/eas_300.pngbin0 -> 47367 bytes
-rw-r--r--content/skin/fieldset.css6
-rw-r--r--content/skin/sponsors/netcup.pngbin0 -> 26849 bytes
-rw-r--r--content/skin/sponsors/nethinks.pngbin0 -> 23750 bytes
-rw-r--r--content/timezonedata/Aliases.csv115
-rw-r--r--content/timezonedata/README4
-rw-r--r--content/timezonedata/WindowsTimezone.csv514
33 files changed, 8741 insertions, 0 deletions
diff --git a/content/api/BootstrapLoader/CHANGELOG.md b/content/api/BootstrapLoader/CHANGELOG.md
new file mode 100644
index 0000000..5006ecf
--- /dev/null
+++ b/content/api/BootstrapLoader/CHANGELOG.md
@@ -0,0 +1,75 @@
+Version: 1.21
+-------------
+- Explicitly set hasAddonManagerEventListeners flag to false on uninstall
+
+Version: 1.20
+-------------
+- hard fork BootstrapLoader v1.19 implementation and continue to serve it for
+ Thunderbird 111 and older
+- BootstrapLoader v1.20 has removed a lot of unnecessary code used for backward
+ compatibility
+
+Version: 1.19
+-------------
+- fix race condition which could prevent the AOM tab to be monkey patched correctly
+
+Version: 1.18
+-------------
+- be precise on which revision the wrench symbol should be displayed, instead of
+ the options button
+
+Version: 1.17
+-------------
+- fix "ownerDoc.getElementById() is undefined" bug
+
+Version: 1.16
+-------------
+- fix "tab.browser is undefined" bug
+
+Version 1.15
+------------
+- clear cache only if add-on is uninstalled/updated, not on app shutdown
+
+Version 1.14
+------------
+- fix for TB90 ("view-loaded" event) and TB78.10 (wrench icon for options)
+
+Version 1.13
+------------
+- removed notifyTools and move it into its own NotifyTools API
+
+Version 1.12
+------------
+- add support for notifyExperiment and onNotifyBackground
+
+Version 1.11
+------------
+- add openOptionsDialog()
+
+Version 1.10
+------------
+- fix for 68
+
+Version 1.7
+-----------
+- fix for beta 87
+
+Version 1.6
+-----------
+- add support for options button/menu in add-on manager and fix 68 double menu entry
+
+Version 1.5
+-----------
+- fix for e10s
+
+Version 1.4
+-----------
+- add registerOptionsPage
+
+Version 1.3
+-----------
+- flush cache
+
+Version 1.2
+-----------
+- add support for resource urls
diff --git a/content/api/BootstrapLoader/README.md b/content/api/BootstrapLoader/README.md
new file mode 100644
index 0000000..7e8fe2a
--- /dev/null
+++ b/content/api/BootstrapLoader/README.md
@@ -0,0 +1 @@
+Usage description can be found in the [wiki](https://github.com/thundernest/addon-developer-support/wiki/Using-the-BootstrapLoader-API-to-convert-a-Legacy-Bootstrap-WebExtension-into-a-MailExtension-for-Thunderbird-78).
diff --git a/content/api/BootstrapLoader/implementation.js b/content/api/BootstrapLoader/implementation.js
new file mode 100644
index 0000000..03e6b76
--- /dev/null
+++ b/content/api/BootstrapLoader/implementation.js
@@ -0,0 +1,917 @@
+/*
+ * This file is provided by the addon-developer-support repository at
+ * https://github.com/thundernest/addon-developer-support
+ *
+ * Version: 1.21
+ *
+ * Author: John Bieling (john@thunderbird.net)
+ *
+ * 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/.
+ */
+
+// Get various parts of the WebExtension framework that we need.
+var { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
+var { ExtensionSupport } = ChromeUtils.import("resource:///modules/ExtensionSupport.jsm");
+var { AddonManager } = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+function getThunderbirdVersion() {
+ let parts = Services.appinfo.version.split(".");
+ return {
+ major: parseInt(parts[0]),
+ minor: parseInt(parts[1]),
+ revision: parts.length > 2 ? parseInt(parts[2]) : 0,
+ }
+}
+
+function getMessenger(context) {
+ let apis = ["storage", "runtime", "extension", "i18n"];
+
+ function getStorage() {
+ let localstorage = null;
+ try {
+ localstorage = context.apiCan.findAPIPath("storage");
+ localstorage.local.get = (...args) =>
+ localstorage.local.callMethodInParentProcess("get", args);
+ localstorage.local.set = (...args) =>
+ localstorage.local.callMethodInParentProcess("set", args);
+ localstorage.local.remove = (...args) =>
+ localstorage.local.callMethodInParentProcess("remove", args);
+ localstorage.local.clear = (...args) =>
+ localstorage.local.callMethodInParentProcess("clear", args);
+ } catch (e) {
+ console.info("Storage permission is missing");
+ }
+ return localstorage;
+ }
+
+ let messenger = {};
+ for (let api of apis) {
+ switch (api) {
+ case "storage":
+ XPCOMUtils.defineLazyGetter(messenger, "storage", () =>
+ getStorage()
+ );
+ break;
+
+ default:
+ XPCOMUtils.defineLazyGetter(messenger, api, () =>
+ context.apiCan.findAPIPath(api)
+ );
+ }
+ }
+ return messenger;
+}
+
+var BootstrapLoader_102 = class extends ExtensionCommon.ExtensionAPI {
+ getCards(e) {
+ // This gets triggered by real events but also manually by providing the outer window.
+ // The event is attached to the outer browser, get the inner one.
+ let doc;
+
+ // 78,86, and 87+ need special handholding. *Yeah*.
+ if (getThunderbirdVersion().major < 86) {
+ let ownerDoc = e.document || e.target.ownerDocument;
+ doc = ownerDoc.getElementById("html-view-browser").contentDocument;
+ } else if (getThunderbirdVersion().major < 87) {
+ let ownerDoc = e.document || e.target;
+ doc = ownerDoc.getElementById("html-view-browser").contentDocument;
+ } else {
+ doc = e.document || e.target;
+ }
+ return doc.querySelectorAll("addon-card");
+ }
+
+ // Add pref entry to 68
+ add68PrefsEntry(event) {
+ let id = this.menu_addonPrefs_id + "_" + this.uniqueRandomID;
+
+ // Get the best size of the icon (16px or bigger)
+ let iconSizes = this.extension.manifest.icons
+ ? Object.keys(this.extension.manifest.icons)
+ : [];
+ iconSizes.sort((a, b) => a - b);
+ let bestSize = iconSizes.filter(e => parseInt(e) >= 16).shift();
+ let icon = bestSize ? this.extension.manifest.icons[bestSize] : "";
+
+ let name = this.extension.manifest.name;
+ let entry = icon
+ ? event.target.ownerGlobal.MozXULElement.parseXULToFragment(
+ `<menuitem class="menuitem-iconic" id="${id}" image="${icon}" label="${name}" />`)
+ : event.target.ownerGlobal.MozXULElement.parseXULToFragment(
+ `<menuitem id="${id}" label="${name}" />`);
+
+ event.target.appendChild(entry);
+ let noPrefsElem = event.target.querySelector('[disabled="true"]');
+ // using collapse could be undone by core, so we use display none
+ // noPrefsElem.setAttribute("collapsed", "true");
+ noPrefsElem.style.display = "none";
+ event.target.ownerGlobal.document.getElementById(id).addEventListener("command", this);
+ }
+
+ // Event handler for the addon manager, to update the state of the options button.
+ handleEvent(e) {
+ switch (e.type) {
+ // 68 add-on options menu showing
+ case "popupshowing": {
+ this.add68PrefsEntry(e);
+ }
+ break;
+
+ // 78/88 add-on options menu/button click
+ case "click": {
+ e.preventDefault();
+ e.stopPropagation();
+ let BL = {}
+ BL.extension = this.extension;
+ BL.messenger = getMessenger(this.context);
+ let w = Services.wm.getMostRecentWindow("mail:3pane");
+ w.openDialog(this.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", BL);
+ }
+ break;
+
+ // 68 add-on options menu command
+ case "command": {
+ let BL = {}
+ BL.extension = this.extension;
+ BL.messenger = getMessenger(this.context);
+ e.target.ownerGlobal.openDialog(this.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", BL);
+ }
+ break;
+
+ // update, ViewChanged and manual call for add-on manager options overlay
+ default: {
+ let cards = this.getCards(e);
+ for (let card of cards) {
+ // Setup either the options entry in the menu or the button
+ if (card.addon.id == this.extension.id) {
+ let optionsMenu =
+ (getThunderbirdVersion().major > 78 && getThunderbirdVersion().major < 88) ||
+ (getThunderbirdVersion().major == 78 && getThunderbirdVersion().minor < 10) ||
+ (getThunderbirdVersion().major == 78 && getThunderbirdVersion().minor == 10 && getThunderbirdVersion().revision < 2);
+ if (optionsMenu) {
+ // Options menu in 78.0-78.10 and 79-87
+ let addonOptionsLegacyEntry = card.querySelector(".extension-options-legacy");
+ if (card.addon.isActive && !addonOptionsLegacyEntry) {
+ let addonOptionsEntry = card.querySelector("addon-options panel-list panel-item[action='preferences']");
+ addonOptionsLegacyEntry = card.ownerDocument.createElement("panel-item");
+ addonOptionsLegacyEntry.setAttribute("data-l10n-id", "preferences-addon-button");
+ addonOptionsLegacyEntry.classList.add("extension-options-legacy");
+ addonOptionsEntry.parentNode.insertBefore(
+ addonOptionsLegacyEntry,
+ addonOptionsEntry
+ );
+ card.querySelector(".extension-options-legacy").addEventListener("click", this);
+ } else if (!card.addon.isActive && addonOptionsLegacyEntry) {
+ addonOptionsLegacyEntry.remove();
+ }
+ } else {
+ // Add-on button in 88
+ let addonOptionsButton = card.querySelector(".extension-options-button2");
+ if (card.addon.isActive && !addonOptionsButton) {
+ addonOptionsButton = card.ownerDocument.createElement("button");
+ addonOptionsButton.classList.add("extension-options-button2");
+ addonOptionsButton.style["min-width"] = "auto";
+ addonOptionsButton.style["min-height"] = "auto";
+ addonOptionsButton.style["width"] = "24px";
+ addonOptionsButton.style["height"] = "24px";
+ addonOptionsButton.style["margin"] = "0";
+ addonOptionsButton.style["margin-inline-start"] = "8px";
+ addonOptionsButton.style["-moz-context-properties"] = "fill";
+ addonOptionsButton.style["fill"] = "currentColor";
+ addonOptionsButton.style["background-image"] = "url('chrome://messenger/skin/icons/developer.svg')";
+ addonOptionsButton.style["background-repeat"] = "no-repeat";
+ addonOptionsButton.style["background-position"] = "center center";
+ addonOptionsButton.style["padding"] = "1px";
+ addonOptionsButton.style["display"] = "flex";
+ addonOptionsButton.style["justify-content"] = "flex-end";
+ card.optionsButton.parentNode.insertBefore(
+ addonOptionsButton,
+ card.optionsButton
+ );
+ card.querySelector(".extension-options-button2").addEventListener("click", this);
+ } else if (!card.addon.isActive && addonOptionsButton) {
+ addonOptionsButton.remove();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Some tab/add-on-manager related functions
+ getTabMail(window) {
+ return window.document.getElementById("tabmail");
+ }
+
+ // returns the outer browser, not the nested browser of the add-on manager
+ // events must be attached to the outer browser
+ getAddonManagerFromTab(tab) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ let win = tab.browser.contentWindow;
+ if (win && win.location.href == "about:addons") {
+ return win;
+ }
+ }
+ }
+
+ getAddonManagerFromWindow(window) {
+ let tabMail = this.getTabMail(window);
+ for (let tab of tabMail.tabInfo) {
+ let managerWindow = this.getAddonManagerFromTab(tab);
+ if (managerWindow) {
+ return managerWindow;
+ }
+ }
+ }
+
+ async getAddonManagerFromWindowWaitForLoad(window) {
+ let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane");
+
+ let tabMail = this.getTabMail(window);
+ for (let tab of tabMail.tabInfo) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ // Instead of registering a load observer, wait until its loaded. Not nice,
+ // but gets aroud a lot of edge cases.
+ while (!tab.pageLoaded) {
+ await new Promise(r => setTimeout(r, 150));
+ }
+ let managerWindow = this.getAddonManagerFromTab(tab);
+ if (managerWindow) {
+ return managerWindow;
+ }
+ }
+ }
+ }
+
+ setupAddonManager(managerWindow, forceLoad = false) {
+ if (!managerWindow) {
+ return;
+ }
+ if (
+ managerWindow &&
+ managerWindow[this.uniqueRandomID] &&
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners
+ ) {
+ return;
+ }
+ managerWindow.document.addEventListener("ViewChanged", this);
+ managerWindow.document.addEventListener("update", this);
+ managerWindow.document.addEventListener("view-loaded", this);
+ managerWindow[this.uniqueRandomID] = {};
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = true;
+ if (forceLoad) {
+ this.handleEvent(managerWindow);
+ }
+ }
+
+ getAPI(context) {
+ this.uniqueRandomID = "AddOnNS" + context.extension.instanceId;
+ this.menu_addonPrefs_id = "addonPrefs";
+
+
+ this.pathToBootstrapScript = null;
+ this.pathToOptionsPage = null;
+ this.chromeHandle = null;
+ this.chromeData = null;
+ this.resourceData = null;
+ this.bootstrappedObj = {};
+
+ // make the extension object and the messenger object available inside
+ // the bootstrapped scope
+ this.bootstrappedObj.extension = context.extension;
+ this.bootstrappedObj.messenger = getMessenger(this.context);
+
+ this.BOOTSTRAP_REASONS = {
+ APP_STARTUP: 1,
+ APP_SHUTDOWN: 2,
+ ADDON_ENABLE: 3,
+ ADDON_DISABLE: 4,
+ ADDON_INSTALL: 5,
+ ADDON_UNINSTALL: 6, // not supported
+ ADDON_UPGRADE: 7,
+ ADDON_DOWNGRADE: 8,
+ };
+
+ const aomStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"].getService(Ci.amIAddonManagerStartup);
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+
+ let self = this;
+
+ // TabMonitor to detect opening of tabs, to setup the options button in the add-on manager.
+ this.tabMonitor = {
+ onTabTitleChanged(tab) { },
+ onTabClosing(tab) { },
+ onTabPersist(tab) { },
+ onTabRestored(tab) { },
+ onTabSwitched(aNewTab, aOldTab) { },
+ async onTabOpened(tab) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane");
+ // Instead of registering a load observer, wait until its loaded. Not nice,
+ // but gets aroud a lot of edge cases.
+ while (!tab.pageLoaded) {
+ await new Promise(r => setTimeout(r, 150));
+ }
+ self.setupAddonManager(self.getAddonManagerFromTab(tab));
+ }
+ },
+ };
+
+ return {
+ BootstrapLoader: {
+
+ registerOptionsPage(optionsUrl) {
+ self.pathToOptionsPage = optionsUrl.startsWith("chrome://")
+ ? optionsUrl
+ : context.extension.rootURI.resolve(optionsUrl);
+ },
+
+ openOptionsDialog(windowId) {
+ let window = context.extension.windowManager.get(windowId, context).window
+ let BL = {}
+ BL.extension = self.extension;
+ BL.messenger = getMessenger(self.context);
+ window.openDialog(self.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", BL);
+ },
+
+ registerChromeUrl(data) {
+ let chromeData = [];
+ let resourceData = [];
+ for (let entry of data) {
+ if (entry[0] == "resource") resourceData.push(entry);
+ else chromeData.push(entry)
+ }
+
+ if (chromeData.length > 0) {
+ const manifestURI = Services.io.newURI(
+ "manifest.json",
+ null,
+ context.extension.rootURI
+ );
+ self.chromeHandle = aomStartup.registerChrome(manifestURI, chromeData);
+ }
+
+ for (let res of resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ let uri = Services.io.newURI(
+ res[2],
+ null,
+ context.extension.rootURI
+ );
+ resProto.setSubstitutionWithFlags(
+ res[1],
+ uri,
+ resProto.ALLOW_CONTENT_ACCESS
+ );
+ }
+
+ self.chromeData = chromeData;
+ self.resourceData = resourceData;
+ },
+
+ registerBootstrapScript: async function (aPath) {
+ self.pathToBootstrapScript = aPath.startsWith("chrome://")
+ ? aPath
+ : context.extension.rootURI.resolve(aPath);
+
+ // Get the addon object belonging to this extension.
+ let addon = await AddonManager.getAddonByID(context.extension.id);
+ //make the addon globally available in the bootstrapped scope
+ self.bootstrappedObj.addon = addon;
+
+ // add BOOTSTRAP_REASONS to scope
+ for (let reason of Object.keys(self.BOOTSTRAP_REASONS)) {
+ self.bootstrappedObj[reason] = self.BOOTSTRAP_REASONS[reason];
+ }
+
+ // Load registered bootstrap scripts and execute its startup() function.
+ try {
+ if (self.pathToBootstrapScript) Services.scriptloader.loadSubScript(self.pathToBootstrapScript, self.bootstrappedObj, "UTF-8");
+ if (self.bootstrappedObj.startup) self.bootstrappedObj.startup.call(self.bootstrappedObj, self.extension.addonData, self.BOOTSTRAP_REASONS[self.extension.startupReason]);
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+
+ // Register window listener for main TB window
+ if (self.pathToOptionsPage) {
+ ExtensionSupport.registerWindowListener("injectListener_" + self.uniqueRandomID, {
+ chromeURLs: [
+ "chrome://messenger/content/messenger.xul",
+ "chrome://messenger/content/messenger.xhtml",
+ ],
+ async onLoadWindow(window) {
+ if (getThunderbirdVersion().major < 78) {
+ let element_addonPrefs = window.document.getElementById(self.menu_addonPrefs_id);
+ element_addonPrefs.addEventListener("popupshowing", self);
+ } else {
+ // Add a tabmonitor, to be able to setup the options button/menu in the add-on manager.
+ self.getTabMail(window).registerTabMonitor(self.tabMonitor);
+ window[self.uniqueRandomID] = {};
+ window[self.uniqueRandomID].hasTabMonitor = true;
+ // Setup the options button/menu in the add-on manager, if it is already open.
+ let managerWindow = await self.getAddonManagerFromWindowWaitForLoad(window);
+ self.setupAddonManager(managerWindow, true);
+ }
+ },
+
+ onUnloadWindow(window) {
+ }
+ });
+ }
+ }
+ }
+ };
+ }
+
+ onShutdown(isAppShutdown) {
+ if (isAppShutdown) {
+ return; // the application gets unloaded anyway
+ }
+
+ //remove our entry in the add-on options menu
+ if (this.pathToOptionsPage) {
+ for (let window of Services.wm.getEnumerator("mail:3pane")) {
+ if (getThunderbirdVersion().major < 78) {
+ let element_addonPrefs = window.document.getElementById(this.menu_addonPrefs_id);
+ element_addonPrefs.removeEventListener("popupshowing", this);
+ // Remove our entry.
+ let entry = window.document.getElementById(this.menu_addonPrefs_id + "_" + this.uniqueRandomID);
+ if (entry) entry.remove();
+ // Do we have to unhide the noPrefsElement?
+ if (element_addonPrefs.children.length == 1) {
+ let noPrefsElem = element_addonPrefs.querySelector('[disabled="true"]');
+ noPrefsElem.style.display = "inline";
+ }
+ } else {
+ // Remove event listener for addon manager view changes
+ let managerWindow = this.getAddonManagerFromWindow(window);
+ if (
+ managerWindow &&
+ managerWindow[this.uniqueRandomID] &&
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners
+ ) {
+ managerWindow.document.removeEventListener("ViewChanged", this);
+ managerWindow.document.removeEventListener("update", this);
+ managerWindow.document.removeEventListener("view-loaded", this);
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = false;
+
+ let cards = this.getCards(managerWindow);
+ if (getThunderbirdVersion().major < 88) {
+ // Remove options menu in 78-87
+ for (let card of cards) {
+ let addonOptionsLegacyEntry = card.querySelector(".extension-options-legacy");
+ if (addonOptionsLegacyEntry) addonOptionsLegacyEntry.remove();
+ }
+ } else {
+ // Remove options button in 88
+ for (let card of cards) {
+ if (card.addon.id == this.extension.id) {
+ let addonOptionsButton = card.querySelector(".extension-options-button2");
+ if (addonOptionsButton) addonOptionsButton.remove();
+ break;
+ }
+ }
+ }
+ }
+
+ // Remove tabmonitor
+ if (window[this.uniqueRandomID].hasTabMonitor) {
+ this.getTabMail(window).unregisterTabMonitor(this.tabMonitor);
+ window[this.uniqueRandomID].hasTabMonitor = false;
+ }
+
+ }
+ }
+ // Stop listening for new windows.
+ ExtensionSupport.unregisterWindowListener("injectListener_" + this.uniqueRandomID);
+ }
+
+ // Execute registered shutdown()
+ try {
+ if (this.bootstrappedObj.shutdown) {
+ this.bootstrappedObj.shutdown(
+ this.extension.addonData,
+ isAppShutdown
+ ? this.BOOTSTRAP_REASONS.APP_SHUTDOWN
+ : this.BOOTSTRAP_REASONS.ADDON_DISABLE);
+ }
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+
+ if (this.resourceData) {
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+ for (let res of this.resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ resProto.setSubstitution(
+ res[1],
+ null,
+ );
+ }
+ }
+
+ if (this.chromeHandle) {
+ this.chromeHandle.destruct();
+ this.chromeHandle = null;
+ }
+ // Flush all caches
+ Services.obs.notifyObservers(null, "startupcache-invalidate");
+ console.log("BootstrapLoader for " + this.extension.id + " unloaded!");
+ }
+};
+
+// Removed all extra code for backward compatibility for better maintainability.
+var BootstrapLoader_115 = class extends ExtensionCommon.ExtensionAPI {
+ getCards(e) {
+ // This gets triggered by real events but also manually by providing the outer window.
+ // The event is attached to the outer browser, get the inner one.
+ let doc = e.document || e.target;
+ return doc.querySelectorAll("addon-card");
+ }
+
+ // Event handler for the addon manager, to update the state of the options button.
+ handleEvent(e) {
+ switch (e.type) {
+ case "click": {
+ e.preventDefault();
+ e.stopPropagation();
+ let BL = {}
+ BL.extension = this.extension;
+ BL.messenger = getMessenger(this.context);
+ let w = Services.wm.getMostRecentWindow("mail:3pane");
+ w.openDialog(
+ this.pathToOptionsPage,
+ "AddonOptions",
+ "chrome,resizable,centerscreen",
+ BL
+ );
+ }
+ break;
+
+
+ // update, ViewChanged and manual call for add-on manager options overlay
+ default: {
+ let cards = this.getCards(e);
+ for (let card of cards) {
+ // Setup either the options entry in the menu or the button
+ if (card.addon.id == this.extension.id) {
+ // Add-on button
+ let addonOptionsButton = card.querySelector(
+ ".windowlistener-options-button"
+ );
+ if (card.addon.isActive && !addonOptionsButton) {
+ let origAddonOptionsButton = card.querySelector(".extension-options-button")
+ origAddonOptionsButton.setAttribute("hidden", "true");
+
+ addonOptionsButton = card.ownerDocument.createElement("button");
+ addonOptionsButton.classList.add("windowlistener-options-button");
+ addonOptionsButton.classList.add("extension-options-button");
+ card.optionsButton.parentNode.insertBefore(
+ addonOptionsButton,
+ card.optionsButton
+ );
+ card
+ .querySelector(".windowlistener-options-button")
+ .addEventListener("click", this);
+ } else if (!card.addon.isActive && addonOptionsButton) {
+ addonOptionsButton.remove();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Some tab/add-on-manager related functions
+ getTabMail(window) {
+ return window.document.getElementById("tabmail");
+ }
+
+ // returns the outer browser, not the nested browser of the add-on manager
+ // events must be attached to the outer browser
+ getAddonManagerFromTab(tab) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ let win = tab.browser.contentWindow;
+ if (win && win.location.href == "about:addons") {
+ return win;
+ }
+ }
+ }
+
+ getAddonManagerFromWindow(window) {
+ let tabMail = this.getTabMail(window);
+ for (let tab of tabMail.tabInfo) {
+ let managerWindow = this.getAddonManagerFromTab(tab);
+ if (managerWindow) {
+ return managerWindow;
+ }
+ }
+ }
+
+ async getAddonManagerFromWindowWaitForLoad(window) {
+ let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane");
+
+ let tabMail = this.getTabMail(window);
+ for (let tab of tabMail.tabInfo) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ // Instead of registering a load observer, wait until its loaded. Not nice,
+ // but gets aroud a lot of edge cases.
+ while (!tab.pageLoaded) {
+ await new Promise(r => setTimeout(r, 150));
+ }
+ let managerWindow = this.getAddonManagerFromTab(tab);
+ if (managerWindow) {
+ return managerWindow;
+ }
+ }
+ }
+ }
+
+ setupAddonManager(managerWindow, forceLoad = false) {
+ if (!managerWindow) {
+ return;
+ }
+ if (!this.pathToOptionsPage) {
+ return;
+ }
+ if (
+ managerWindow &&
+ managerWindow[this.uniqueRandomID] &&
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners
+ ) {
+ return;
+ }
+
+ managerWindow.document.addEventListener("ViewChanged", this);
+ managerWindow.document.addEventListener("update", this);
+ managerWindow.document.addEventListener("view-loaded", this);
+ managerWindow[this.uniqueRandomID] = {};
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = true;
+ if (forceLoad) {
+ this.handleEvent(managerWindow);
+ }
+ }
+
+ getAPI(context) {
+ this.uniqueRandomID = "AddOnNS" + context.extension.instanceId;
+ this.menu_addonPrefs_id = "addonPrefs";
+
+
+ this.pathToBootstrapScript = null;
+ this.pathToOptionsPage = null;
+ this.chromeHandle = null;
+ this.chromeData = null;
+ this.resourceData = null;
+ this.bootstrappedObj = {};
+
+ // make the extension object and the messenger object available inside
+ // the bootstrapped scope
+ this.bootstrappedObj.extension = context.extension;
+ this.bootstrappedObj.messenger = getMessenger(this.context);
+
+ this.BOOTSTRAP_REASONS = {
+ APP_STARTUP: 1,
+ APP_SHUTDOWN: 2,
+ ADDON_ENABLE: 3,
+ ADDON_DISABLE: 4,
+ ADDON_INSTALL: 5,
+ ADDON_UNINSTALL: 6, // not supported
+ ADDON_UPGRADE: 7,
+ ADDON_DOWNGRADE: 8,
+ };
+
+ const aomStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"].getService(Ci.amIAddonManagerStartup);
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+
+ let self = this;
+
+ // TabMonitor to detect opening of tabs, to setup the options button in the add-on manager.
+ this.tabMonitor = {
+ onTabTitleChanged(tab) { },
+ onTabClosing(tab) { },
+ onTabPersist(tab) { },
+ onTabRestored(tab) { },
+ onTabSwitched(aNewTab, aOldTab) { },
+ async onTabOpened(tab) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane");
+ // Instead of registering a load observer, wait until its loaded. Not nice,
+ // but gets aroud a lot of edge cases.
+ while (!tab.pageLoaded) {
+ await new Promise(r => setTimeout(r, 150));
+ }
+ self.setupAddonManager(self.getAddonManagerFromTab(tab));
+ }
+ },
+ };
+
+ return {
+ BootstrapLoader: {
+
+ registerOptionsPage(optionsUrl) {
+ self.pathToOptionsPage = optionsUrl.startsWith("chrome://")
+ ? optionsUrl
+ : context.extension.rootURI.resolve(optionsUrl);
+ },
+
+ openOptionsDialog(windowId) {
+ let window = context.extension.windowManager.get(windowId, context).window
+ let BL = {}
+ BL.extension = self.extension;
+ BL.messenger = getMessenger(self.context);
+ window.openDialog(self.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", BL);
+ },
+
+ registerChromeUrl(data) {
+ let chromeData = [];
+ let resourceData = [];
+ for (let entry of data) {
+ if (entry[0] == "resource") resourceData.push(entry);
+ else chromeData.push(entry)
+ }
+
+ if (chromeData.length > 0) {
+ const manifestURI = Services.io.newURI(
+ "manifest.json",
+ null,
+ context.extension.rootURI
+ );
+ self.chromeHandle = aomStartup.registerChrome(manifestURI, chromeData);
+ }
+
+ for (let res of resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ let uri = Services.io.newURI(
+ res[2],
+ null,
+ context.extension.rootURI
+ );
+ resProto.setSubstitutionWithFlags(
+ res[1],
+ uri,
+ resProto.ALLOW_CONTENT_ACCESS
+ );
+ }
+
+ self.chromeData = chromeData;
+ self.resourceData = resourceData;
+ },
+
+ registerBootstrapScript: async function (aPath) {
+ self.pathToBootstrapScript = aPath.startsWith("chrome://")
+ ? aPath
+ : context.extension.rootURI.resolve(aPath);
+
+ // Get the addon object belonging to this extension.
+ let addon = await AddonManager.getAddonByID(context.extension.id);
+ //make the addon globally available in the bootstrapped scope
+ self.bootstrappedObj.addon = addon;
+
+ // add BOOTSTRAP_REASONS to scope
+ for (let reason of Object.keys(self.BOOTSTRAP_REASONS)) {
+ self.bootstrappedObj[reason] = self.BOOTSTRAP_REASONS[reason];
+ }
+
+ // Load registered bootstrap scripts and execute its startup() function.
+ try {
+ if (self.pathToBootstrapScript) Services.scriptloader.loadSubScript(self.pathToBootstrapScript, self.bootstrappedObj, "UTF-8");
+ if (self.bootstrappedObj.startup) self.bootstrappedObj.startup.call(self.bootstrappedObj, self.extension.addonData, self.BOOTSTRAP_REASONS[self.extension.startupReason]);
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+
+ // Register window listener for main TB window
+ if (self.pathToOptionsPage) {
+ ExtensionSupport.registerWindowListener("injectListener_" + self.uniqueRandomID, {
+ chromeURLs: [
+ "chrome://messenger/content/messenger.xul",
+ "chrome://messenger/content/messenger.xhtml",
+ ],
+ async onLoadWindow(window) {
+ if (getThunderbirdVersion().major < 78) {
+ let element_addonPrefs = window.document.getElementById(self.menu_addonPrefs_id);
+ element_addonPrefs.addEventListener("popupshowing", self);
+ } else {
+ // Add a tabmonitor, to be able to setup the options button/menu in the add-on manager.
+ self.getTabMail(window).registerTabMonitor(self.tabMonitor);
+ window[self.uniqueRandomID] = {};
+ window[self.uniqueRandomID].hasTabMonitor = true;
+ // Setup the options button/menu in the add-on manager, if it is already open.
+ let managerWindow = await self.getAddonManagerFromWindowWaitForLoad(window);
+ self.setupAddonManager(managerWindow, true);
+ }
+ },
+
+ onUnloadWindow(window) {
+ }
+ });
+ }
+ }
+ }
+ };
+ }
+
+ onShutdown(isAppShutdown) {
+ if (isAppShutdown) {
+ return; // the application gets unloaded anyway
+ }
+
+ //remove our entry in the add-on options menu
+ if (this.pathToOptionsPage) {
+ for (let window of Services.wm.getEnumerator("mail:3pane")) {
+ if (getThunderbirdVersion().major < 78) {
+ let element_addonPrefs = window.document.getElementById(this.menu_addonPrefs_id);
+ element_addonPrefs.removeEventListener("popupshowing", this);
+ // Remove our entry.
+ let entry = window.document.getElementById(this.menu_addonPrefs_id + "_" + this.uniqueRandomID);
+ if (entry) entry.remove();
+ // Do we have to unhide the noPrefsElement?
+ if (element_addonPrefs.children.length == 1) {
+ let noPrefsElem = element_addonPrefs.querySelector('[disabled="true"]');
+ noPrefsElem.style.display = "inline";
+ }
+ } else {
+ // Remove event listener for addon manager view changes
+ let managerWindow = this.getAddonManagerFromWindow(window);
+ if (
+ managerWindow &&
+ managerWindow[this.uniqueRandomID] &&
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners
+ ) {
+ managerWindow.document.removeEventListener("ViewChanged", this);
+ managerWindow.document.removeEventListener("update", this);
+ managerWindow.document.removeEventListener("view-loaded", this);
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = false;
+
+ let cards = this.getCards(managerWindow);
+ if (getThunderbirdVersion().major < 88) {
+ // Remove options menu in 78-87
+ for (let card of cards) {
+ let addonOptionsLegacyEntry = card.querySelector(".extension-options-legacy");
+ if (addonOptionsLegacyEntry) addonOptionsLegacyEntry.remove();
+ }
+ } else {
+ // Remove options button in 88
+ for (let card of cards) {
+ if (card.addon.id == this.extension.id) {
+ let addonOptionsButton = card.querySelector(".extension-options-button2");
+ if (addonOptionsButton) addonOptionsButton.remove();
+ break;
+ }
+ }
+ }
+ }
+
+ // Remove tabmonitor
+ if (window[this.uniqueRandomID].hasTabMonitor) {
+ this.getTabMail(window).unregisterTabMonitor(this.tabMonitor);
+ window[this.uniqueRandomID].hasTabMonitor = false;
+ }
+
+ }
+ }
+ // Stop listening for new windows.
+ ExtensionSupport.unregisterWindowListener("injectListener_" + this.uniqueRandomID);
+ }
+
+ // Execute registered shutdown()
+ try {
+ if (this.bootstrappedObj.shutdown) {
+ this.bootstrappedObj.shutdown(
+ this.extension.addonData,
+ isAppShutdown
+ ? this.BOOTSTRAP_REASONS.APP_SHUTDOWN
+ : this.BOOTSTRAP_REASONS.ADDON_DISABLE);
+ }
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+
+ if (this.resourceData) {
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+ for (let res of this.resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ resProto.setSubstitution(
+ res[1],
+ null,
+ );
+ }
+ }
+
+ if (this.chromeHandle) {
+ this.chromeHandle.destruct();
+ this.chromeHandle = null;
+ }
+ // Flush all caches
+ Services.obs.notifyObservers(null, "startupcache-invalidate");
+ console.log("BootstrapLoader for " + this.extension.id + " unloaded!");
+ }
+};
+
+var BootstrapLoader = getThunderbirdVersion().major < 111
+ ? BootstrapLoader_102
+ : BootstrapLoader_115;
diff --git a/content/api/BootstrapLoader/schema.json b/content/api/BootstrapLoader/schema.json
new file mode 100644
index 0000000..fe48fb6
--- /dev/null
+++ b/content/api/BootstrapLoader/schema.json
@@ -0,0 +1,61 @@
+[
+ {
+ "namespace": "BootstrapLoader",
+ "functions": [
+ {
+ "name": "registerOptionsPage",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "aPath",
+ "type": "string",
+ "description": "Path to the options page, which should be made accessible in the (legacy) Add-On Options menu."
+ }
+ ]
+ },
+ {
+ "name": "openOptionsDialog",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "windowId",
+ "type": "integer",
+ "description": "Id of the window the dialog should be opened from."
+ }
+ ]
+ },
+ {
+ "name": "registerChromeUrl",
+ "type": "function",
+ "description": "Register folders which should be available as chrome:// urls (as defined in the legacy chrome.manifest)",
+ "async": true,
+ "parameters": [
+ {
+ "name": "chromeData",
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "description": "Array of ChromeData Arrays."
+ }
+ ]
+ },
+ {
+ "name": "registerBootstrapScript",
+ "type": "function",
+ "description": "Register a bootstrap.js style script",
+ "async": true,
+ "parameters": [
+ {
+ "name": "aPath",
+ "type": "string",
+ "description": "Either the chrome:// path to the script or its relative location from the root of the extension,"
+ }
+ ]
+ }
+ ]
+ }
+] \ No newline at end of file
diff --git a/content/bootstrap.js b/content/bootstrap.js
new file mode 100644
index 0000000..5fb33c8
--- /dev/null
+++ b/content/bootstrap.js
@@ -0,0 +1,55 @@
+/*
+ * 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/.
+ */
+
+// no need to create namespace, we are in a sandbox
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+let onInitDoneObserver = {
+ observe: async function (aSubject, aTopic, aData) {
+ let valid = false;
+ try {
+ var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+ valid = TbSync.enabled;
+ } catch (e) {
+ // If this fails, TbSync is not loaded yet and we will get the notification later again.
+ }
+
+ //load this provider add-on into TbSync
+ if (valid) {
+ await TbSync.providers.loadProvider(extension, "eas", "chrome://eas4tbsync/content/provider.js");
+ }
+ }
+}
+
+function startup(data, reason) {
+ // Possible reasons: APP_STARTUP, ADDON_ENABLE, ADDON_INSTALL, ADDON_UPGRADE, or ADDON_DOWNGRADE.
+
+ Services.obs.addObserver(onInitDoneObserver, "tbsync.observer.initialized", false);
+
+ // Did we miss the observer?
+ onInitDoneObserver.observe();
+}
+
+function shutdown(data, reason) {
+ // Possible reasons: APP_STARTUP, ADDON_ENABLE, ADDON_INSTALL, ADDON_UPGRADE, or ADDON_DOWNGRADE.
+
+ // When the application is shutting down we normally don't have to clean up.
+ if (reason == APP_SHUTDOWN) {
+ return;
+ }
+
+ Services.obs.removeObserver(onInitDoneObserver, "tbsync.observer.initialized");
+ //unload this provider add-on from TbSync
+ try {
+ var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+ TbSync.providers.unloadProvider("eas");
+ } catch (e) {
+ //if this fails, TbSync has been unloaded already and has unloaded this addon as well
+ }
+}
diff --git a/content/includes/calendarsync.js b/content/includes/calendarsync.js
new file mode 100644
index 0000000..7592060
--- /dev/null
+++ b/content/includes/calendarsync.js
@@ -0,0 +1,421 @@
+/*
+ * 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, {
+ CalAlarm: "resource:///modules/CalAlarm.jsm",
+ CalAttachment: "resource:///modules/CalAttachment.jsm",
+ CalAttendee: "resource:///modules/CalAttendee.jsm",
+ CalEvent: "resource:///modules/CalEvent.jsm",
+ CalTodo: "resource:///modules/CalTodo.jsm",
+});
+
+const cal = TbSync.lightning.cal;
+const ICAL = TbSync.lightning.ICAL;
+
+var Calendar = {
+
+ // --------------------------------------------------------------------------- //
+ // Read WBXML and set Thunderbird item
+ // --------------------------------------------------------------------------- //
+ setThunderbirdItemFromWbxml: function (tbItem, data, id, syncdata, mode = "standard") {
+
+ let item = tbItem instanceof TbSync.lightning.TbItem ? tbItem.nativeItem : tbItem;
+
+ let asversion = syncdata.accountData.getAccountProperty("asversion");
+ item.id = id;
+ eas.sync.setItemSubject(item, syncdata, data);
+ if (TbSync.prefs.getIntPref("log.userdatalevel") > 2) TbSync.dump("Processing " + mode + " calendar item", item.title + " (" + id + ")");
+
+ eas.sync.setItemLocation(item, syncdata, data);
+ eas.sync.setItemCategories(item, syncdata, data);
+ eas.sync.setItemBody(item, syncdata, data);
+
+ //timezone
+ let stdOffset = eas.defaultTimezoneInfo.std.offset;
+ let dstOffset = eas.defaultTimezoneInfo.dst.offset;
+ let easTZ = new eas.tools.TimeZoneDataStructure();
+ if (data.TimeZone) {
+ if (data.TimeZone == "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") {
+ TbSync.dump("Recieve TZ", "No timezone data received, using local default timezone.");
+ } else {
+ //load timezone struct into EAS TimeZone object
+ easTZ.easTimeZone64 = data.TimeZone;
+ if (TbSync.prefs.getIntPref("log.userdatalevel") > 2) TbSync.dump("Recieve TZ", item.title + easTZ.toString());
+ stdOffset = easTZ.utcOffset;
+ dstOffset = easTZ.daylightBias + easTZ.utcOffset;
+ }
+ }
+ let timezone = eas.tools.guessTimezoneByStdDstOffset(stdOffset, dstOffset, easTZ.standardName);
+
+ if (data.StartTime) {
+ let utc = cal.createDateTime(data.StartTime); //format "19800101T000000Z" - UTC
+ item.startDate = utc.getInTimezone(timezone);
+ if (data.AllDayEvent && data.AllDayEvent == "1") {
+ item.startDate.timezone = (cal.dtz && cal.dtz.floating) ? cal.dtz.floating : cal.floating();
+ item.startDate.isDate = true;
+ }
+ }
+
+ if (data.EndTime) {
+ let utc = cal.createDateTime(data.EndTime);
+ item.endDate = utc.getInTimezone(timezone);
+ if (data.AllDayEvent && data.AllDayEvent == "1") {
+ item.endDate.timezone = (cal.dtz && cal.dtz.floating) ? cal.dtz.floating : cal.floating();
+ item.endDate.isDate = true;
+ }
+ }
+
+ //stamp time cannot be set and it is not needed, an updated version is only send to the server, if there was a change, so stamp will be updated
+
+
+ //EAS Reminder
+ item.clearAlarms();
+ if (data.Reminder && data.StartTime) {
+ let alarm = new CalAlarm();
+ alarm.related = Components.interfaces.calIAlarm.ALARM_RELATED_START;
+ alarm.offset = cal.createDuration();
+ alarm.offset.inSeconds = (0-parseInt(data.Reminder)*60);
+ alarm.action ="DISPLAY";
+ item.addAlarm(alarm);
+
+ let alarmData = cal.alarms.calculateAlarmDate(item, alarm);
+ let startDate = cal.createDateTime(data.StartTime);
+ let nowDate = eas.tools.getNowUTC();
+ if (startDate.compare(nowDate) < 0) {
+ // Mark alarm as ACK if in the past.
+ item.alarmLastAck = nowDate;
+ }
+ }
+
+ eas.sync.mapEasPropertyToThunderbird ("BusyStatus", "TRANSP", data, item);
+ eas.sync.mapEasPropertyToThunderbird ("Sensitivity", "CLASS", data, item);
+
+ if (data.ResponseType) {
+ //store original EAS value
+ item.setProperty("X-EAS-ResponseType", eas.xmltools.checkString(data.ResponseType, "0")); //some server send empty ResponseType ???
+ }
+
+ //Attendees - remove all Attendees and re-add the ones from XML
+ item.removeAllAttendees();
+ if (data.Attendees && data.Attendees.Attendee) {
+ let att = [];
+ if (Array.isArray(data.Attendees.Attendee)) att = data.Attendees.Attendee;
+ else att.push(data.Attendees.Attendee);
+ for (let i = 0; i < att.length; i++) {
+ if (att[i].Email && eas.tools.isString(att[i].Email) && att[i].Name) { //req.
+
+ let attendee = new CalAttendee();
+
+ //is this attendee the local EAS user?
+ let isSelf = (att[i].Email == syncdata.accountData.getAccountProperty("user"));
+
+ attendee["id"] = cal.email.prependMailTo(att[i].Email);
+ attendee["commonName"] = att[i].Name;
+ //default is "FALSE", only if THIS attendee isSelf, use ResponseRequested (we cannot respond for other attendee) - ResponseType is not send back to the server, it is just a local information
+ attendee["rsvp"] = (isSelf && data.ResponseRequested) ? "TRUE" : "FALSE";
+
+ //not supported in 2.5
+ switch (att[i].AttendeeType) {
+ case "1": //required
+ attendee["role"] = "REQ-PARTICIPANT";
+ attendee["userType"] = "INDIVIDUAL";
+ break;
+ case "2": //optional
+ attendee["role"] = "OPT-PARTICIPANT";
+ attendee["userType"] = "INDIVIDUAL";
+ break;
+ default : //resource or unknown
+ attendee["role"] = "NON-PARTICIPANT";
+ attendee["userType"] = "RESOURCE";
+ break;
+ }
+
+ //not supported in 2.5 - if attendeeStatus is missing, check if this isSelf and there is a ResponseType
+ if (att[i].AttendeeStatus)
+ attendee["participationStatus"] = eas.sync.MAP_EAS2TB.ATTENDEESTATUS[att[i].AttendeeStatus];
+ else if (isSelf && data.ResponseType)
+ attendee["participationStatus"] = eas.sync.MAP_EAS2TB.ATTENDEESTATUS[data.ResponseType];
+ else
+ attendee["participationStatus"] = "NEEDS-ACTION";
+
+ // status : [NEEDS-ACTION, ACCEPTED, DECLINED, TENTATIVE, DELEGATED, COMPLETED, IN-PROCESS]
+ // rolemap : [REQ-PARTICIPANT, OPT-PARTICIPANT, NON-PARTICIPANT, CHAIR]
+ // typemap : [INDIVIDUAL, GROUP, RESOURCE, ROOM]
+
+ // Add attendee to event
+ item.addAttendee(attendee);
+ } else {
+ TbSync.eventlog.add("info", syncdata, "Attendee without required name and/or email found. Skipped.");
+ }
+ }
+ }
+
+ if (data.OrganizerName && data.OrganizerEmail && eas.tools.isString(data.OrganizerEmail)) {
+ //Organizer
+ let organizer = new CalAttendee();
+ organizer.id = cal.email.prependMailTo(data.OrganizerEmail);
+ organizer.commonName = data.OrganizerName;
+ organizer.rsvp = "FALSE";
+ organizer.role = "CHAIR";
+ organizer.userType = null;
+ organizer.participationStatus = "ACCEPTED";
+ organizer.isOrganizer = true;
+ item.organizer = organizer;
+ }
+
+ eas.sync.setItemRecurrence(item, syncdata, data, timezone);
+
+ // BusyStatus is always representing the status of the current user in terms of availability.
+ // It has nothing to do with the status of a meeting. The user could be just the organizer, but does not need to attend, so he would be free.
+ // The correct map is between BusyStatus and TRANSP (show time as avail, busy, unset)
+ // A new event always sets TRANSP to busy, so unset is indeed a good way to store Tentiative
+ // However:
+ // - EAS Meetingstatus only knows ACTIVE or CANCELLED, but not CONFIRMED or TENTATIVE
+ // - TB STATUS has UNSET, CONFIRMED, TENTATIVE, CANCELLED
+ // -> Special case: User sets BusyStatus to TENTIATIVE -> TRANSP is unset and also set STATUS to TENTATIVE
+ // The TB STATUS is the correct map for EAS Meetingstatus and should be unset, if it is not a meeting EXCEPT if set to TENTATIVE
+ let tbStatus = (data.BusyStatus && data.BusyStatus == "1" ? "TENTATIVE" : null);
+
+ if (data.MeetingStatus) {
+ //store original EAS value
+ item.setProperty("X-EAS-MeetingStatus", data.MeetingStatus);
+ //bitwise representation for Meeting, Received, Cancelled:
+ let M = data.MeetingStatus & 0x1;
+ let R = data.MeetingStatus & 0x2;
+ let C = data.MeetingStatus & 0x4;
+
+ // We can map M+C to TB STATUS (TENTATIVE, CONFIRMED, CANCELLED, unset).
+ if (M) {
+ if (C) tbStatus = "CANCELLED";
+ else if (!tbStatus) tbStatus = "CONFIRMED"; // do not override "TENTIATIVE"
+ }
+
+ //we can also use the R information, to update our fallbackOrganizerName
+ if (!R && data.OrganizerName) syncdata.target.calendar.setProperty("fallbackOrganizerName", data.OrganizerName);
+ }
+
+ if (tbStatus) item.setProperty("STATUS", tbStatus)
+ else item.deleteProperty("STATUS");
+
+ //TODO: attachements (needs EAS 16.0!)
+ },
+
+
+
+
+
+
+
+
+
+ // --------------------------------------------------------------------------- //
+ //read TB event and return its data as WBXML
+ // --------------------------------------------------------------------------- //
+ getWbxmlFromThunderbirdItem: async function (tbItem, syncdata, isException = false) {
+ let item = tbItem instanceof TbSync.lightning.TbItem ? tbItem.nativeItem : tbItem;
+
+ 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();
+
+ /*
+ * We do not use ghosting, that means, if we do not include a value in CHANGE, it is removed from the server.
+ * However, this does not seem to work on all fields. Furthermore, we need to include any (empty) container to blank its childs.
+ */
+
+ //Order of tags taken from https://msdn.microsoft.com/en-us/library/dn338917(v=exchg.80).aspx
+
+ //timezone
+ if (!isException) {
+ let easTZ = new eas.tools.TimeZoneDataStructure();
+
+ //if there is no end and no start (or both are floating) use default timezone info
+ let tzInfo = null;
+ if (item.startDate && item.startDate.timezone.tzid != "floating") tzInfo = eas.tools.getTimezoneInfo(item.startDate.timezone);
+ else if (item.endDate && item.endDate.timezone.tzid != "floating") tzInfo = eas.tools.getTimezoneInfo(item.endDate.timezone);
+ if (!tzInfo) tzInfo = eas.defaultTimezoneInfo;
+
+ easTZ.utcOffset = tzInfo.std.offset;
+ easTZ.standardBias = 0;
+ easTZ.daylightBias = tzInfo.dst.offset - tzInfo.std.offset;
+
+ easTZ.standardName = eas.ianaToWindowsTimezoneMap.hasOwnProperty(tzInfo.std.displayname) ? eas.ianaToWindowsTimezoneMap[tzInfo.std.displayname] : tzInfo.std.displayname;
+ easTZ.daylightName = eas.ianaToWindowsTimezoneMap.hasOwnProperty(tzInfo.dst.displayname) ? eas.ianaToWindowsTimezoneMap[tzInfo.dst.displayname] : tzInfo.dst.displayname;
+
+ if (tzInfo.std.switchdate && tzInfo.dst.switchdate) {
+ easTZ.standardDate.wMonth = tzInfo.std.switchdate.month;
+ easTZ.standardDate.wDay = tzInfo.std.switchdate.weekOfMonth;
+ easTZ.standardDate.wDayOfWeek = tzInfo.std.switchdate.dayOfWeek;
+ easTZ.standardDate.wHour = tzInfo.std.switchdate.hour;
+ easTZ.standardDate.wMinute = tzInfo.std.switchdate.minute;
+ easTZ.standardDate.wSecond = tzInfo.std.switchdate.second;
+
+ easTZ.daylightDate.wMonth = tzInfo.dst.switchdate.month;
+ easTZ.daylightDate.wDay = tzInfo.dst.switchdate.weekOfMonth;
+ easTZ.daylightDate.wDayOfWeek = tzInfo.dst.switchdate.dayOfWeek;
+ easTZ.daylightDate.wHour = tzInfo.dst.switchdate.hour;
+ easTZ.daylightDate.wMinute = tzInfo.dst.switchdate.minute;
+ easTZ.daylightDate.wSecond = tzInfo.dst.switchdate.second;
+ }
+
+ wbxml.atag("TimeZone", easTZ.easTimeZone64);
+ if (TbSync.prefs.getIntPref("log.userdatalevel") > 2) TbSync.dump("Send TZ", item.title + easTZ.toString());
+ }
+
+ //AllDayEvent (for simplicity, we always send a value)
+ wbxml.atag("AllDayEvent", (item.startDate && item.startDate.isDate && item.endDate && item.endDate.isDate) ? "1" : "0");
+
+ //Body
+ wbxml.append(eas.sync.getItemBody(item, syncdata));
+
+ //BusyStatus (Free, Tentative, Busy) is taken from TRANSP (busy, free, unset=tentative)
+ //However if STATUS is set to TENTATIVE, overide TRANSP and set BusyStatus to TENTATIVE
+ if (item.hasProperty("STATUS") && item.getProperty("STATUS") == "TENTATIVE") {
+ wbxml.atag("BusyStatus","1");
+ } else {
+ wbxml.atag("BusyStatus", eas.sync.mapThunderbirdPropertyToEas("TRANSP", "BusyStatus", item));
+ }
+
+ //Organizer
+ if (!isException) {
+ if (item.organizer && item.organizer.commonName) wbxml.atag("OrganizerName", item.organizer.commonName);
+ if (item.organizer && item.organizer.id) wbxml.atag("OrganizerEmail", cal.email.removeMailTo(item.organizer.id));
+ }
+
+ //DtStamp in UTC
+ wbxml.atag("DtStamp", item.stampTime ? eas.tools.getIsoUtcString(item.stampTime) : eas.tools.dateToBasicISOString(nowDate));
+
+ //EndTime in UTC
+ wbxml.atag("EndTime", item.endDate ? eas.tools.getIsoUtcString(item.endDate) : eas.tools.dateToBasicISOString(nowDate));
+
+ //Location
+ wbxml.atag("Location", (item.hasProperty("location")) ? item.getProperty("location") : "");
+
+ //EAS Reminder (TB getAlarms) - at least with zpush blanking by omitting works, horde does not work
+ let alarms = item.getAlarms({});
+ if (alarms.length>0) {
+
+ let reminder = -1;
+ if (alarms[0].offset !== null) {
+ reminder = 0 - alarms[0].offset.inSeconds/60;
+ } else if (item.startDate) {
+ let timeDiff =item.startDate.getInTimezone(eas.utcTimezone).subtractDate(alarms[0].alarmDate.getInTimezone(eas.utcTimezone));
+ reminder = timeDiff.inSeconds/60;
+ TbSync.eventlog.add("info", syncdata, "Converting absolute alarm to relative alarm (not supported).", item.icalString);
+ }
+ if (reminder >= 0) wbxml.atag("Reminder", reminder.toString());
+ else TbSync.eventlog.add("info", syncdata, "Droping alarm after start date (not supported).", item.icalString);
+
+ }
+
+ //Sensitivity (CLASS)
+ wbxml.atag("Sensitivity", eas.sync.mapThunderbirdPropertyToEas("CLASS", "Sensitivity", item));
+
+ //Subject (obmitting these, should remove them from the server - that does not work reliably, so we send blanks)
+ wbxml.atag("Subject", (item.title) ? item.title : "");
+
+ //StartTime in UTC
+ wbxml.atag("StartTime", item.startDate ? eas.tools.getIsoUtcString(item.startDate) : eas.tools.dateToBasicISOString(nowDate));
+
+ //UID (limit to 300)
+ //each TB event has an ID, which is used as EAS serverId - however there is a second UID in the ApplicationData
+ //since we do not have two different IDs to use, we use the same ID
+ if (!isException) { //docs say it would be allowed in exception in 2.5, but it does not work, if present
+ wbxml.atag("UID", item.id);
+ }
+ //IMPORTANT in EAS v16 it is no longer allowed to send a UID
+ //Only allowed in exceptions in v2.5
+
+
+ //EAS MeetingStatus
+ // 0 (000) The event is an appointment, which has no attendees.
+ // 1 (001) The event is a meeting and the user is the meeting organizer.
+ // 3 (011) This event is a meeting, and the user is not the meeting organizer; the meeting was received from someone else.
+ // 5 (101) The meeting has been canceled and the user was the meeting organizer.
+ // 7 (111) The meeting has been canceled. The user was not the meeting organizer; the meeting was received from someone else
+
+ //there are 3 fields; Meeting, Owner, Cancelled
+ //M can be reconstructed from #of attendees (looking at the old value is not wise, since it could have been changed)
+ //C can be reconstucted from TB STATUS
+ //O can be reconstructed by looking at the original value, or (if not present) by comparing EAS ownerID with TB ownerID
+
+ let attendees = item.getAttendees();
+ //if (!(isException && asversion == "2.5")) { //MeetingStatus is not supported in exceptions in EAS 2.5
+ if (!isException) { //Exchange 2010 does not seem to support MeetingStatus at all in exceptions
+ if (attendees.length == 0) wbxml.atag("MeetingStatus", "0");
+ else {
+ //get owner information
+ let isReceived = false;
+ if (item.hasProperty("X-EAS-MEETINGSTATUS")) isReceived = item.getProperty("X-EAS-MEETINGSTATUS") & 0x2;
+ else isReceived = (item.organizer && item.organizer.id && cal.email.removeMailTo(item.organizer.id) != syncdata.accountData.getAccountProperty("user"));
+
+ //either 1,3,5 or 7
+ if (item.hasProperty("STATUS") && item.getProperty("STATUS") == "CANCELLED") {
+ //either 5 or 7
+ wbxml.atag("MeetingStatus", (isReceived ? "7" : "5"));
+ } else {
+ //either 1 or 3
+ wbxml.atag("MeetingStatus", (isReceived ? "3" : "1"));
+ }
+ }
+ }
+
+ //Attendees
+ let TB_responseType = null;
+ if (!(isException && asversion == "2.5")) { //attendees are not supported in exceptions in EAS 2.5
+ if (attendees.length > 0) { //We should use it instead of countAttendees.value
+ wbxml.otag("Attendees");
+ for (let attendee of attendees) {
+ wbxml.otag("Attendee");
+ wbxml.atag("Email", cal.email.removeMailTo(attendee.id));
+ wbxml.atag("Name", (attendee.commonName ? attendee.commonName : cal.email.removeMailTo(attendee.id).split("@")[0]));
+ if (asversion != "2.5") {
+ //it's pointless to send AttendeeStatus,
+ // - if we are the owner of a meeting, TB does not have an option to actually set the attendee status (on behalf of an attendee) in the UI
+ // - if we are an attendee (of an invite) we cannot and should not set status of other attendees and or own status must be send through a MeetingResponse
+ // -> all changes of attendee status are send from the server to us, either via ResponseType or via AttendeeStatus
+ //wbxml.atag("AttendeeStatus", eas.sync.MAP_TB2EAS.ATTENDEESTATUS[attendee.participationStatus]);
+
+ if (attendee.userType == "RESOURCE" || attendee.userType == "ROOM" || attendee.role == "NON-PARTICIPANT") wbxml.atag("AttendeeType","3");
+ else if (attendee.role == "REQ-PARTICIPANT" || attendee.role == "CHAIR") wbxml.atag("AttendeeType","1");
+ else wbxml.atag("AttendeeType","2"); //leftovers are optional
+ }
+ wbxml.ctag();
+ }
+ wbxml.ctag();
+ } else {
+ wbxml.atag("Attendees");
+ }
+ }
+
+ //Categories (see https://github.com/jobisoft/TbSync/pull/35#issuecomment-359286374)
+ if (!isException) {
+ wbxml.append(eas.sync.getItemCategories(item, syncdata));
+ }
+
+ //recurrent events (implemented by Chris Allan)
+ if (!isException) {
+ wbxml.append(await eas.sync.getItemRecurrence(item, syncdata));
+ }
+
+
+ //---------------------------
+
+ //TP PRIORITY (9=LOW, 5=NORMAL, 1=HIGH) not mapable to EAS Event
+ //TODO: attachements (needs EAS 16.0!)
+
+ //https://dxr.mozilla.org/comm-central/source/calendar/base/public/calIAlarm.idl
+ //TbSync.dump("ALARM ("+i+")", [, alarms[i].related, alarms[i].repeat, alarms[i].repeatOffset, alarms[i].repeatDate, alarms[i].action].join("|"));
+
+ return wbxml.getBytes();
+ }
+}
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);
diff --git a/content/includes/network.js b/content/includes/network.js
new file mode 100644
index 0000000..ba2abcd
--- /dev/null
+++ b/content/includes/network.js
@@ -0,0 +1,1459 @@
+/*
+ * 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 { OAuth2 } = ChromeUtils.import("resource:///modules/OAuth2.jsm");
+
+var network = {
+
+ getEasURL: function(accountData) {
+ let protocol = (accountData.getAccountProperty("https")) ? "https://" : "http://";
+ let h = protocol + accountData.getAccountProperty("host");
+ while (h.endsWith("/")) { h = h.slice(0,-1); }
+
+ if (h.endsWith("Microsoft-Server-ActiveSync")) return h;
+ return h + "/Microsoft-Server-ActiveSync";
+ },
+
+ getAuthData: function(accountData) {
+ let authData = {
+ // This is the host for the password manager, which could be different from
+ // the actual host property of the account. For EAS we want to couple the password
+ // with the ACCOUNT and not any sort of url, which could change via autodiscover
+ // at any time.
+ get host() {
+ return "TbSync#" + accountData.accountID;
+ },
+
+ get user() {
+ return accountData.getAccountProperty("user");
+ },
+
+ get password() {
+ return TbSync.passwordManager.getLoginInfo(this.host, "TbSync/EAS", this.user);
+ },
+
+ updateLoginData: function(newUsername, newPassword) {
+ let oldUsername = this.user;
+ TbSync.passwordManager.updateLoginInfo(this.host, "TbSync/EAS", oldUsername, newUsername, newPassword);
+ // Also update the username of this account. Add dedicated username setter?
+ accountData.setAccountProperty("user", newUsername);
+ },
+
+ removeLoginData: function() {
+ TbSync.passwordManager.removeLoginInfos(this.host, "TbSync/EAS");
+ }
+ };
+ return authData;
+ },
+
+ // prepare and patch OAuth2 object
+ getOAuthObj: function(configObject = null) {
+ let accountname, user, host, accountID, servertype;
+
+ let accountData = (configObject && configObject.hasOwnProperty("accountData")) ? configObject.accountData : null;
+ if (accountData) {
+ accountname = accountData.getAccountProperty("accountname");
+ user = accountData.getAccountProperty("user");
+ host = accountData.getAccountProperty("host");
+ servertype = accountData.getAccountProperty("servertype");
+ accountID = accountData.accountID;
+ } else {
+ accountname = (configObject && configObject.hasOwnProperty("accountname")) ? configObject.accountname : "";
+ user = (configObject && configObject.hasOwnProperty("user")) ? configObject.user : "";
+ host = (configObject && configObject.hasOwnProperty("host")) ? configObject.host : "";
+ servertype = (configObject && configObject.hasOwnProperty("servertype")) ? configObject.servertype : "";
+ accountID = "";
+ }
+
+ if (!["office365"].includes(servertype))
+ return null;
+
+ let config = {};
+ let customID = eas.Base.getCustomeOauthClientID();
+ switch (host) {
+ case "outlook.office365.com":
+ case "eas.outlook.com":
+ config = {
+ auth_uri : "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
+ token_uri : "https://login.microsoftonline.com/common/oauth2/v2.0/token",
+ redirect_uri : "https://login.microsoftonline.com/common/oauth2/nativeclient",
+ client_id : customID != "" ? customID : "2980deeb-7460-4723-864a-f9b0f10cd992",
+ }
+ break;
+
+ default:
+ return null;
+ }
+
+ switch (host) {
+ case "outlook.office365.com":
+ config.scope = "offline_access https://outlook.office.com/.default";
+ break;
+ case "eas.outlook.com":
+ config.scope = "offline_access https://outlook.office.com/EAS.AccessAsUser.All";
+ break;
+ }
+
+ let oauth = new OAuth2(config.scope, {
+ authorizationEndpoint: config.auth_uri,
+ tokenEndpoint: config.token_uri,
+ clientId: config.client_id,
+ clientSecret: config.client_secret
+ });
+ oauth.requestWindowFeatures = "chrome,private,centerscreen,width=500,height=750";
+
+ // The v2 redirection endpoint differs from the default and needs manual override
+ oauth.redirectionEndpoint = config.redirect_uri;
+
+ oauth.extraAuthParams = [
+ // removed in beta 1.14.1, according to
+ // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#default-and-consent
+ // prompt = consent will always ask for admin consent, even if it was granted
+ //["prompt", "consent"],
+ ["login_hint", user],
+ ];
+
+ if (accountname) {
+ oauth.requestWindowTitle = "TbSync account <" + accountname + "> requests authorization.";
+ } else {
+ oauth.requestWindowTitle = "A TbSync account requests authorization.";
+ }
+
+
+
+
+ /* Adding custom methods to the oauth object */
+
+ oauth.asyncConnect = async function(rv) {
+ let self = this;
+ rv.error = "";
+ rv.tokens = "";
+
+ // If multiple resources need to authenticate they will all end here, even though they
+ // might share the same token. Due to the async nature, each process will refresh
+ // "its own" token again, which is not needed. We force clear the token here and each
+ // final connect process will actually check the acccessToken and abort the refresh,
+ // if it is already there, generated by some other process.
+ if (self.accessToken) self.accessToken = "";
+
+ try {
+ await new Promise(function(resolve, reject) {
+ // refresh = false will do nothing and resolve immediately, if an accessToken
+ // exists already, which must have been generated by another process, as
+ // we cleared it beforehand.
+ self.connect(resolve, reject, /* with UI */ true, /* refresh */ false);
+ });
+ rv.tokens = self.tokens;
+ return true;
+ } catch (e) {
+ rv.error = eas.tools.isString(e) ? e : JSON.stringify(e);
+ }
+
+ try {
+ switch (JSON.parse(rv.error).error) {
+ case "invalid_grant":
+ self.accessToken = "";
+ self.refreshToken = "";
+ return true;
+
+ case "cancelled":
+ rv.error = "OAuthAbortError";
+ break;
+
+ default:
+ rv.error = "OAuthServerError::"+rv.error;
+ break;
+ }
+ } catch (e) {
+ rv.error = "OAuthServerError::"+rv.error;
+ Components.utils.reportError(e);
+ }
+ return false;
+ };
+
+ oauth.isExpired = function() {
+ const OAUTH_GRACE_TIME = 30 * 1000;
+ return (this.tokenExpires - OAUTH_GRACE_TIME < new Date().getTime());
+ };
+
+ const OAUTHVALUES = [
+ ["access", "", "accessToken"],
+ ["refresh", "", "refreshToken"],
+ ["expires", Number.MAX_VALUE, "tokenExpires"],
+ ];
+
+ // returns a JSON string containing all the oauth values
+ Object.defineProperty(oauth, "tokens", {
+ get: function() {
+ let tokensObj = {};
+ for (let oauthValue of OAUTHVALUES) {
+ // use the system value or if not defined the default
+ tokensObj[oauthValue[0]] = this[oauthValue[2]] || oauthValue[1];
+ }
+ return JSON.stringify(tokensObj);
+ },
+ enumerable: true,
+ });
+
+ if (accountData) {
+ // authData allows us to access the password manager values belonging to this account/calendar
+ // simply by authdata.username and authdata.password
+ oauth.authData = TbSync.providers.eas.network.getAuthData(accountData);
+
+ oauth.parseAndSanitizeTokenString = function(tokenString) {
+ let _tokensObj = {};
+ try {
+ _tokensObj = JSON.parse(tokenString);
+ } catch (e) {}
+
+ let tokensObj = {};
+ for (let oauthValue of OAUTHVALUES) {
+ // use the provided value or if not defined the default
+ tokensObj[oauthValue[0]] = (_tokensObj && _tokensObj.hasOwnProperty(oauthValue[0]))
+ ? _tokensObj[oauthValue[0]]
+ : oauthValue[1];
+ }
+ return tokensObj;
+ };
+
+ // Define getter/setter to act on the password manager password value belonging to this account/calendar
+ for (let oauthValue of OAUTHVALUES) {
+ Object.defineProperty(oauth, oauthValue[2], {
+ get: function() {
+ return this.parseAndSanitizeTokenString(this.authData.password)[oauthValue[0]];
+ },
+ set: function(val) {
+ let tokens = this.parseAndSanitizeTokenString(this.authData.password);
+ let valueChanged = (val != tokens[oauthValue[0]])
+ if (valueChanged) {
+ tokens[oauthValue[0]] = val;
+ this.authData.updateLoginData(this.authData.user, JSON.stringify(tokens));
+ }
+ },
+ enumerable: true,
+ });
+ }
+ }
+
+ return oauth;
+ },
+
+ getOAuthValue: function(currentTokenString, type = "access") {
+ try {
+ let tokens = JSON.parse(currentTokenString);
+ if (tokens.hasOwnProperty(type))
+ return tokens[type];
+ } catch (e) {
+ //NOOP
+ }
+ return "";
+ },
+
+ sendRequest: async function (wbxml, command, syncData, allowSoftFail = false) {
+ let ALLOWED_RETRIES = {
+ PasswordPrompt : 3,
+ NetworkError : 1,
+ }
+
+ let rv = {};
+ let oauthData = eas.network.getOAuthObj({ accountData: syncData.accountData });
+ let syncState = syncData.getSyncState().state;
+
+ for (;;) {
+
+ if (rv.errorType) {
+ let retry = false;
+
+ if (ALLOWED_RETRIES[rv.errorType] > 0) {
+ ALLOWED_RETRIES[rv.errorType]--;
+
+
+ switch (rv.errorType) {
+
+ case "PasswordPrompt":
+ {
+
+ if (oauthData) {
+ oauthData.accessToken = "";
+ retry = true;
+ } else {
+ let authData = eas.network.getAuthData(syncData.accountData);
+ syncData.setSyncState("passwordprompt");
+ let promptData = {
+ windowID: "auth:" + syncData.accountData.accountID,
+ accountname: syncData.accountData.getAccountProperty("accountname"),
+ usernameLocked: syncData.accountData.isConnected(),
+ username: authData.user
+ }
+ let credentials = await TbSync.passwordManager.asyncPasswordPrompt(promptData, eas.openWindows);
+ if (credentials) {
+ retry = true;
+ authData.updateLoginData(credentials.username, credentials.password);
+ }
+ }
+ }
+ break;
+
+ case "NetworkError":
+ {
+ // Could not connect to server. Can we rerun autodiscover?
+ // Note: Autodiscover is currently not supported by OAuth
+ if (syncData.accountData.getAccountProperty( "servertype") == "auto" && !oauthData) {
+ let errorcode = await eas.network.updateServerConnectionViaAutodiscover(syncData);
+ console.log("ERR: " + errorcode);
+ if (errorcode == 200) {
+ // autodiscover succeeded, retry with new data
+ retry = true;
+ } else if (errorcode == 401) {
+ // manipulate rv to run password prompt
+ ALLOWED_RETRIES[rv.errorType]++;
+ rv.errorType = "PasswordPrompt";
+ rv.errorObj = eas.sync.finish("error", "401");
+ continue; // with the next loop, skip connection to the server
+ }
+ }
+ }
+ break;
+
+ }
+ }
+
+ if (!retry) throw rv.errorObj;
+ }
+
+ // check OAuth situation before connecting
+ if (oauthData && (!oauthData.accessToken || oauthData.isExpired())) {
+ syncData.setSyncState("oauthprompt");
+ let _rv = {}
+ if (!(await oauthData.asyncConnect(_rv))) {
+ throw eas.sync.finish("error", _rv.error);
+ }
+ }
+
+ // Return to original syncstate
+ if (syncState != syncData.getSyncState().state) {
+ syncData.setSyncState(syncState);
+ }
+ rv = await this.sendRequestPromise(wbxml, command, syncData, allowSoftFail);
+
+ if (rv.errorType) {
+ // make sure, there is a valid ALLOWED_RETRIES setting for the returned error
+ if (rv.errorType && !ALLOWED_RETRIES.hasOwnProperty(rv.errorType)) {
+ ALLOWED_RETRIES[rv.errorType] = 1;
+ }
+ } else {
+ return rv;
+ }
+ }
+ },
+
+ sendRequestPromise: function (wbxml, command, syncData, allowSoftFail = false) {
+ let msg = "Sending data <" + syncData.getSyncState().state + "> for " + syncData.accountData.getAccountProperty("accountname");
+ if (syncData.currentFolderData) msg += " (" + syncData.currentFolderData.getFolderProperty("foldername") + ")";
+ syncData.request = eas.network.logXML(wbxml, msg);
+ syncData.response = "";
+
+ let connection = eas.network.getAuthData(syncData.accountData);
+ let userAgent = syncData.accountData.getAccountProperty("useragent"); //plus calendar.useragent.extra = Lightning/5.4.5.2
+ let deviceType = syncData.accountData.getAccountProperty("devicetype");
+ let deviceId = syncData.accountData.getAccountProperty("deviceId");
+
+ TbSync.dump("Sending (EAS v"+syncData.accountData.getAccountProperty("asversion") +")", "POST " + eas.network.getEasURL(syncData.accountData) + '?Cmd=' + command + '&User=' + encodeURIComponent(connection.user) + '&DeviceType=' +deviceType + '&DeviceId=' + deviceId, true);
+
+ const textEncoder = new TextEncoder();
+ let encoded = textEncoder.encode(wbxml);
+ // console.log("wbxml: " + wbxml);
+ // console.log("byte array: " + encoded);
+ // console.log("length :" + wbxml.length + " vs " + encoded.byteLength + " vs " + encoded.length);
+
+ return new Promise(function(resolve,reject) {
+ // Create request handler - API changed with TB60 to new XMKHttpRequest()
+ syncData.req = new XMLHttpRequest();
+ syncData.req.mozBackgroundRequest = true;
+ syncData.req.open("POST", eas.network.getEasURL(syncData.accountData) + '?Cmd=' + command + '&User=' + encodeURIComponent(connection.user) + '&DeviceType=' +encodeURIComponent(deviceType) + '&DeviceId=' + deviceId, true);
+ syncData.req.overrideMimeType("text/plain");
+ syncData.req.setRequestHeader("User-Agent", userAgent);
+ syncData.req.setRequestHeader("Content-Type", "application/vnd.ms-sync.wbxml");
+ if (connection.password) {
+ if (eas.network.getOAuthObj({ accountData: syncData.accountData })) {
+ syncData.req.setRequestHeader("Authorization", 'Bearer ' + eas.network.getOAuthValue(connection.password, "access"));
+ } else {
+ syncData.req.setRequestHeader("Authorization", 'Basic ' + TbSync.tools.b64encode(connection.user + ':' + connection.password));
+ }
+ }
+
+ if (syncData.accountData.getAccountProperty("asversion") == "2.5") {
+ syncData.req.setRequestHeader("MS-ASProtocolVersion", "2.5");
+ } else {
+ syncData.req.setRequestHeader("MS-ASProtocolVersion", "14.0");
+ }
+ syncData.req.setRequestHeader("Content-Length", encoded.length);
+ if (syncData.accountData.getAccountProperty("provision")) {
+ syncData.req.setRequestHeader("X-MS-PolicyKey", syncData.accountData.getAccountProperty("policykey"));
+ TbSync.dump("PolicyKey used", syncData.accountData.getAccountProperty("policykey"));
+ }
+
+ syncData.req.timeout = eas.Base.getConnectionTimeout();
+
+ syncData.req.ontimeout = function () {
+ if (allowSoftFail) {
+ resolve("");
+ } else {
+ reject(eas.sync.finish("error", "timeout"));
+ }
+ };
+
+ syncData.req.onerror = function () {
+ if (allowSoftFail) {
+ resolve("");
+ } else {
+ let error = TbSync.network.createTCPErrorFromFailedXHR(syncData.req) || "networkerror";
+ let rv = {};
+ rv.errorObj = eas.sync.finish("error", error);
+ rv.errorType = "NetworkError";
+ resolve(rv);
+ }
+ };
+
+ syncData.req.onload = function() {
+ let response = syncData.req.responseText;
+ switch(syncData.req.status) {
+
+ case 200: //OK
+ let msg = "Receiving data <" + syncData.getSyncState().state + "> for " + syncData.accountData.getAccountProperty("accountname");
+ if (syncData.currentFolderData) msg += " (" + syncData.currentFolderData.getFolderProperty("foldername") + ")";
+ syncData.response = eas.network.logXML(response, msg);
+
+ //What to do on error? IS this an error? Yes!
+ if (!allowSoftFail && response.length !== 0 && response.substr(0, 4) !== String.fromCharCode(0x03, 0x01, 0x6A, 0x00)) {
+ TbSync.dump("Recieved Data", "Expecting WBXML but got junk (request status = " + syncData.req.status + ", ready state = " + syncData.req.readyState + "\n>>>>>>>>>>\n" + response + "\n<<<<<<<<<<\n");
+ reject(eas.sync.finish("warning", "invalid"));
+ } else {
+ resolve(response);
+ }
+ break;
+
+ case 401: // AuthError
+ case 403: // Forbiddden (some servers send forbidden on AuthError, like Freenet)
+ let rv = {};
+ rv.errorObj = eas.sync.finish("error", "401");
+ rv.errorType = "PasswordPrompt";
+ resolve(rv);
+ break;
+
+ case 449: // Request for new provision (enable it if needed)
+ //enable provision
+ syncData.accountData.setAccountProperty("provision", true);
+ syncData.accountData.resetAccountProperty("policykey");
+ reject(eas.sync.finish("resyncAccount", syncData.req.status));
+ break;
+
+ case 451: // Redirect - update host and login manager
+ let header = syncData.req.getResponseHeader("X-MS-Location");
+ let newHost = header.slice(header.indexOf("://") + 3, header.indexOf("/M"));
+
+ TbSync.dump("redirect (451)", "header: " + header + ", oldHost: " +syncData.accountData.getAccountProperty("host") + ", newHost: " + newHost);
+
+ syncData.accountData.setAccountProperty("host", newHost);
+ reject(eas.sync.finish("resyncAccount", syncData.req.status));
+ break;
+
+ default:
+ if (allowSoftFail) {
+ resolve("");
+ } else {
+ reject(eas.sync.finish("error", "httperror::" + syncData.req.status));
+ }
+ }
+ };
+
+ syncData.req.send(encoded);
+
+ });
+ },
+
+
+
+
+
+
+
+
+
+
+ // RESPONSE EVALUATION
+
+ logXML : function (wbxml, what) {
+ let rawxml = eas.wbxmltools.convert2xml(wbxml);
+ let xml = null;
+ if (rawxml) {
+ xml = rawxml.split('><').join('>\n<');
+ }
+
+ //include xml in log, if userdatalevel 2 or greater
+ if (TbSync.prefs.getIntPref("log.userdatalevel") > 1) {
+
+ //log raw wbxml if userdatalevel is 3 or greater
+ if (TbSync.prefs.getIntPref("log.userdatalevel") > 2) {
+ let charcodes = [];
+ for (let i=0; i< wbxml.length; i++) charcodes.push(wbxml.charCodeAt(i).toString(16));
+ let bytestring = charcodes.join(" ");
+ TbSync.dump("WBXML: " + what, "\n" + bytestring);
+ }
+
+ if (xml) {
+ //raw xml is save xml with all special chars in user data encoded by encodeURIComponent - KEEP that in order to be able to analyze logged XML
+ //let xml = decodeURIComponent(rawxml.split('><').join('>\n<'));
+ TbSync.dump("XML: " + what, "\n" + xml);
+ } else {
+ TbSync.dump("XML: " + what, "\nFailed to convert WBXML to XML!\n");
+ }
+ }
+
+ return xml;
+ },
+
+ //returns false on parse error and null on empty response (if allowed)
+ getDataFromResponse: function (wbxml, allowEmptyResponse = !eas.flags.allowEmptyResponse) {
+ //check for empty wbxml
+ if (wbxml.length === 0) {
+ if (allowEmptyResponse) return null;
+ else throw eas.sync.finish("warning", "empty-response");
+ }
+
+ //convert to save xml (all special chars in user data encoded by encodeURIComponent) and check for parse errors
+ let xml = eas.wbxmltools.convert2xml(wbxml);
+ if (xml === false) {
+ throw eas.sync.finish("warning", "wbxml-parse-error");
+ }
+
+ //retrieve data and check for empty data (all returned data fields are already decoded by decodeURIComponent)
+ let wbxmlData = eas.xmltools.getDataFromXMLString(xml);
+ if (wbxmlData === null) {
+ if (allowEmptyResponse) return null;
+ else throw eas.sync.finish("warning", "response-contains-no-data");
+ }
+
+ //debug
+ eas.xmltools.printXmlData(wbxmlData, false); //do not include ApplicationData in log
+ return wbxmlData;
+ },
+
+ updateSynckey: function (syncData, wbxmlData) {
+ let synckey = eas.xmltools.getWbxmlDataField(wbxmlData,"Sync.Collections.Collection.SyncKey");
+
+ if (synckey) {
+ // This COULD be a cause of problems...
+ syncData.synckey = synckey;
+ syncData.currentFolderData.setFolderProperty("synckey", synckey);
+ } else {
+ throw eas.sync.finish("error", "wbxmlmissingfield::Sync.Collections.Collection.SyncKey");
+ }
+ },
+
+ checkStatus : function (syncData, wbxmlData, path, rootpath="", allowSoftFail = false) {
+ //path is relative to wbxmlData
+ //rootpath is the absolute path and must be specified, if wbxml is not the root node and thus path is not the rootpath
+ let status = eas.xmltools.getWbxmlDataField(wbxmlData,path);
+ let fullpath = (rootpath=="") ? path : rootpath;
+ let elements = fullpath.split(".");
+ let type = elements[0];
+
+ //check if fallback to main class status: the answer could just be a "Sync.Status" instead of a "Sync.Collections.Collections.Status"
+ if (status === false) {
+ let mainStatus = eas.xmltools.getWbxmlDataField(wbxmlData, type + "." + elements[elements.length-1]);
+ if (mainStatus === false) {
+ //both possible status fields are missing, abort
+ throw eas.sync.finish("warning", "wbxmlmissingfield::" + fullpath, "Request:\n" + syncData.request + "\n\nResponse:\n" + syncData.response);
+ } else {
+ //the alternative status could be extracted
+ status = mainStatus;
+ fullpath = type + "." + elements[elements.length-1];
+ }
+ }
+
+ //check if all is fine (not bad)
+ if (status == "1") {
+ return "";
+ }
+
+ TbSync.dump("wbxml status check", type + ": " + fullpath + " = " + status);
+
+ //handle errrors based on type
+ let statusType = type+"."+status;
+ switch (statusType) {
+ case "Sync.3": /*
+ MUST return to SyncKey element value of 0 for the collection. The client SHOULD either delete any items that were added
+ since the last successful Sync or the client MUST add those items back to the server after completing the full resynchronization
+ */
+ TbSync.eventlog.add("warning", syncData.eventLogInfo, "Forced Folder Resync", "Request:\n" + syncData.request + "\n\nResponse:\n" + syncData.response);
+ syncData.currentFolderData.remove();
+ throw eas.sync.finish("resyncFolder", statusType);
+
+ case "Sync.4": //Malformed request
+ case "Sync.5": //Temporary server issues or invalid item
+ case "Sync.6": //Invalid item
+ case "Sync.8": //Object not found
+ if (allowSoftFail) return statusType;
+ throw eas.sync.finish("warning", statusType, "Request:\n" + syncData.request + "\n\nResponse:\n" + syncData.response);
+
+ case "Sync.7": //The client has changed an item for which the conflict policy indicates that the server's changes take precedence.
+ case "Sync.9": //User account could be out of disk space, also send if no write permission (TODO)
+ return "";
+
+ case "FolderDelete.3": // special system folder - fatal error
+ case "FolderDelete.6": // error on server
+ throw eas.sync.finish("warning", statusType, "Request:\n" + syncData.request + "\n\nResponse:\n" + syncData.response);
+
+ case "FolderDelete.4": // folder does not exist - resync ( we allow delete only if folder is not subscribed )
+ case "FolderDelete.9": // invalid synchronization key - resync
+ case "FolderSync.9": // invalid synchronization key - resync
+ case "Sync.12": // folder hierarchy changed
+ {
+ let folders = syncData.accountData.getAllFoldersIncludingCache();
+ for (let folder of folders) {
+ folder.remove();
+ }
+ // reset account
+ eas.Base.onEnableAccount(syncData.accountData);
+ throw eas.sync.finish("resyncAccount", statusType, "Request:\n" + syncData.request + "\n\nResponse:\n" + syncData.response);
+ }
+ }
+
+ //handle global error (https://msdn.microsoft.com/en-us/library/ee218647(v=exchg.80).aspx)
+ let descriptions = {};
+ switch(status) {
+ case "101": //invalid content
+ case "102": //invalid wbxml
+ case "103": //invalid xml
+ throw eas.sync.finish("error", "global." + status, "Request:\n" + syncData.request + "\n\nResponse:\n" + syncData.response);
+
+ case "109": descriptions["109"]="DeviceTypeMissingOrInvalid";
+ case "112": descriptions["112"]="ActiveDirectoryAccessDenied";
+ case "126": descriptions["126"]="UserDisabledForSync";
+ case "127": descriptions["127"]="UserOnNewMailboxCannotSync";
+ case "128": descriptions["128"]="UserOnLegacyMailboxCannotSync";
+ case "129": descriptions["129"]="DeviceIsBlockedForThisUser";
+ case "130": descriptions["120"]="AccessDenied";
+ case "131": descriptions["131"]="AccountDisabled";
+ throw eas.sync.finish("error", "global.clientdenied"+ "::" + status + "::" + descriptions[status]);
+
+ case "110": //server error - abort and disable autoSync for 30 minutes
+ {
+ let noAutosyncUntil = 30 * 60000 + Date.now();
+ let humanDate = new Date(noAutosyncUntil).toUTCString();
+ syncData.accountData.setAccountProperty("noAutosyncUntil", noAutosyncUntil);
+ throw eas.sync.finish("error", "global." + status, "AutoSync disabled until: " + humanDate + " \n\nRequest:\n" + syncData.request + "\n\nResponse:\n" + syncData.response);
+
+/* // reset account
+ * let folders = syncData.accountData.getAllFoldersIncludingCache();
+ * for (let folder of folders) {
+ * folder.remove();
+ * }
+ * // reset account
+ * eas.Base.onEnableAccount(syncData.accountData);
+ * throw eas.sync.finish("resyncAccount", statusType, "Request:\n" + syncData.request + "\n\nResponse:\n" + syncData.response);
+ */
+ }
+
+ case "141": // The device is not provisionable
+ case "142": // DeviceNotProvisioned
+ case "143": // PolicyRefresh
+ case "144": // InvalidPolicyKey
+ //enable provision
+ syncData.accountData.setAccountProperty("provision", true);
+ syncData.accountData.resetAccountProperty("policykey");
+ throw eas.sync.finish("resyncAccount", statusType);
+
+ default:
+ if (allowSoftFail) return statusType;
+ throw eas.sync.finish("error", statusType, "Request:\n" + syncData.request + "\n\nResponse:\n" + syncData.response);
+
+ }
+ },
+
+
+
+
+
+
+
+
+
+
+ // WBXML COMM STUFF
+
+ setDeviceInformation: async function (syncData) {
+ if (syncData.accountData.getAccountProperty("asversion") == "2.5" || !syncData.accountData.getAccountProperty("allowedEasCommands").split(",").includes("Settings")) {
+ return;
+ }
+
+ syncData.setSyncState("prepare.request.setdeviceinfo");
+
+ let wbxml = wbxmltools.createWBXML();
+ wbxml.switchpage("Settings");
+ wbxml.otag("Settings");
+ wbxml.otag("DeviceInformation");
+ wbxml.otag("Set");
+ wbxml.atag("Model", "Computer");
+ wbxml.atag("FriendlyName", "TbSync on Device " + syncData.accountData.getAccountProperty("deviceId").substring(4));
+ wbxml.atag("OS", Services.appinfo.OS);
+ wbxml.atag("UserAgent", syncData.accountData.getAccountProperty("useragent"));
+ wbxml.ctag();
+ wbxml.ctag();
+ wbxml.ctag();
+
+ syncData.setSyncState("send.request.setdeviceinfo");
+ let response = await eas.network.sendRequest(wbxml.getBytes(), "Settings", syncData);
+
+ syncData.setSyncState("eval.response.setdeviceinfo");
+ let wbxmlData = eas.network.getDataFromResponse(response);
+
+ eas.network.checkStatus(syncData, wbxmlData,"Settings.Status");
+ },
+
+ getPolicykey: async function (syncData) {
+ //build WBXML to request provision
+ syncData.setSyncState("prepare.request.provision");
+ let wbxml = eas.wbxmltools.createWBXML();
+ wbxml.switchpage("Provision");
+ wbxml.otag("Provision");
+ wbxml.otag("Policies");
+ wbxml.otag("Policy");
+ wbxml.atag("PolicyType", (syncData.accountData.getAccountProperty("asversion") == "2.5") ? "MS-WAP-Provisioning-XML" : "MS-EAS-Provisioning-WBXML" );
+ wbxml.ctag();
+ wbxml.ctag();
+ wbxml.ctag();
+
+ for (let loop=0; loop < 2; loop++) {
+ syncData.setSyncState("send.request.provision");
+ let response = await eas.network.sendRequest(wbxml.getBytes(), "Provision", syncData);
+
+ syncData.setSyncState("eval.response.provision");
+ let wbxmlData = eas.network.getDataFromResponse(response);
+ let policyStatus = eas.xmltools.getWbxmlDataField(wbxmlData, "Provision.Policies.Policy.Status");
+ let provisionStatus = eas.xmltools.getWbxmlDataField(wbxmlData, "Provision.Status");
+ if (provisionStatus === false) {
+ throw eas.sync.finish("error", "wbxmlmissingfield::Provision.Status");
+ } else if (provisionStatus != "1") {
+ //dump policy status as well
+ if (policyStatus) TbSync.dump("PolicyKey","Received policy status: " + policyStatus);
+ throw eas.sync.finish("error", "provision::" + provisionStatus);
+ }
+
+ //reaching this point: provision status was ok
+ let policykey = eas.xmltools.getWbxmlDataField(wbxmlData,"Provision.Policies.Policy.PolicyKey");
+ switch (policyStatus) {
+ case false:
+ throw eas.sync.finish("error", "wbxmlmissingfield::Provision.Policies.Policy.Status");
+
+ case "2":
+ //server does not have a policy for this device: disable provisioning
+ syncData.accountData.setAccountProperty("provision", false)
+ syncData.accountData.resetAccountProperty("policykey");
+ throw eas.sync.finish("resyncAccount", "NoPolicyForThisDevice");
+
+ case "1":
+ if (policykey === false) {
+ throw eas.sync.finish("error", "wbxmlmissingfield::Provision.Policies.Policy.PolicyKey");
+ }
+ TbSync.dump("PolicyKey","Received policykey (" + loop + "): " + policykey);
+ syncData.accountData.setAccountProperty("policykey", policykey);
+ break;
+
+ default:
+ throw eas.sync.finish("error", "policy." + policyStatus);
+ }
+
+ //build WBXML to acknowledge provision
+ syncData.setSyncState("prepare.request.provision");
+ wbxml = eas.wbxmltools.createWBXML();
+ wbxml.switchpage("Provision");
+ wbxml.otag("Provision");
+ wbxml.otag("Policies");
+ wbxml.otag("Policy");
+ wbxml.atag("PolicyType",(syncData.accountData.getAccountProperty("asversion") == "2.5") ? "MS-WAP-Provisioning-XML" : "MS-EAS-Provisioning-WBXML" );
+ wbxml.atag("PolicyKey", policykey);
+ wbxml.atag("Status", "1");
+ wbxml.ctag();
+ wbxml.ctag();
+ wbxml.ctag();
+
+ //this wbxml will be used by Send at the top of this loop
+ }
+ },
+
+ getSynckey: async function (syncData) {
+ syncData.setSyncState("prepare.request.synckey");
+ //build WBXML to request a new syncKey
+ let wbxml = eas.wbxmltools.createWBXML();
+ wbxml.otag("Sync");
+ wbxml.otag("Collections");
+ wbxml.otag("Collection");
+ if (syncData.accountData.getAccountProperty("asversion") == "2.5") wbxml.atag("Class", syncData.type);
+ wbxml.atag("SyncKey","0");
+ wbxml.atag("CollectionId", syncData.currentFolderData.getFolderProperty("serverID"));
+ wbxml.ctag();
+ wbxml.ctag();
+ wbxml.ctag();
+
+ syncData.setSyncState("send.request.synckey");
+ let response = await eas.network.sendRequest(wbxml.getBytes(), "Sync", syncData);
+
+ syncData.setSyncState("eval.response.synckey");
+ // get data from wbxml response
+ let wbxmlData = eas.network.getDataFromResponse(response);
+ //check status
+ eas.network.checkStatus(syncData, wbxmlData,"Sync.Collections.Collection.Status");
+ //update synckey
+ eas.network.updateSynckey(syncData, wbxmlData);
+ },
+
+ getItemEstimate: async function (syncData) {
+ syncData.progressData.reset();
+
+ if (!syncData.accountData.getAccountProperty("allowedEasCommands").split(",").includes("GetItemEstimate")) {
+ return; //do not throw, this is optional
+ }
+
+ syncData.setSyncState("prepare.request.estimate");
+
+ // BUILD WBXML
+ let wbxml = eas.wbxmltools.createWBXML();
+ wbxml.switchpage("GetItemEstimate");
+ wbxml.otag("GetItemEstimate");
+ wbxml.otag("Collections");
+ wbxml.otag("Collection");
+ if (syncData.accountData.getAccountProperty("asversion") == "2.5") { //got this order for 2.5 directly from Microsoft support
+ wbxml.atag("Class", syncData.type); //only 2.5
+ wbxml.atag("CollectionId", syncData.currentFolderData.getFolderProperty("serverID"));
+ wbxml.switchpage("AirSync");
+ // required !
+ // https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-ascmd/ffbefa62-e315-40b9-9cc6-f8d74b5f65d4
+ if (syncData.type == "Calendar") wbxml.atag("FilterType", syncData.currentFolderData.accountData.getAccountProperty("synclimit"));
+ else wbxml.atag("FilterType", "0"); // we may filter incomplete tasks
+
+ wbxml.atag("SyncKey", syncData.synckey);
+ wbxml.switchpage("GetItemEstimate");
+ } else { //14.0
+ wbxml.switchpage("AirSync");
+ wbxml.atag("SyncKey", syncData.synckey);
+ wbxml.switchpage("GetItemEstimate");
+ wbxml.atag("CollectionId", syncData.currentFolderData.getFolderProperty("serverID"));
+ wbxml.switchpage("AirSync");
+ wbxml.otag("Options");
+ // optional
+ if (syncData.type == "Calendar") wbxml.atag("FilterType", syncData.currentFolderData.accountData.getAccountProperty("synclimit"));
+ wbxml.atag("Class", syncData.type);
+ wbxml.ctag();
+ wbxml.switchpage("GetItemEstimate");
+ }
+ wbxml.ctag();
+ wbxml.ctag();
+ wbxml.ctag();
+
+ //SEND REQUEST
+ syncData.setSyncState("send.request.estimate");
+ let response = await eas.network.sendRequest(wbxml.getBytes(), "GetItemEstimate", syncData, /* allowSoftFail */ true);
+
+ //VALIDATE RESPONSE
+ syncData.setSyncState("eval.response.estimate");
+
+ // get data from wbxml response, some servers send empty response if there are no changes, which is not an error
+ let wbxmlData = eas.network.getDataFromResponse(response, eas.flags.allowEmptyResponse);
+ if (wbxmlData === null) return;
+
+ let status = eas.xmltools.getWbxmlDataField(wbxmlData, "GetItemEstimate.Response.Status");
+ let estimate = eas.xmltools.getWbxmlDataField(wbxmlData, "GetItemEstimate.Response.Collection.Estimate");
+
+ if (status && status == "1") { //do not throw on error, with EAS v2.5 I get error 2 for tasks and calendars ???
+ syncData.progressData.reset(0, estimate);
+ }
+ },
+
+ getUserInfo: async function (syncData) {
+ if (!syncData.accountData.getAccountProperty("allowedEasCommands").split(",").includes("Settings")) {
+ return;
+ }
+
+ syncData.setSyncState("prepare.request.getuserinfo");
+
+ let wbxml = eas.wbxmltools.createWBXML();
+ wbxml.switchpage("Settings");
+ wbxml.otag("Settings");
+ wbxml.otag("UserInformation");
+ wbxml.atag("Get");
+ wbxml.ctag();
+ wbxml.ctag();
+
+ syncData.setSyncState("send.request.getuserinfo");
+ let response = await eas.network.sendRequest(wbxml.getBytes(), "Settings", syncData);
+
+
+ syncData.setSyncState("eval.response.getuserinfo");
+ let wbxmlData = eas.network.getDataFromResponse(response);
+
+ eas.network.checkStatus(syncData, wbxmlData,"Settings.Status");
+ },
+
+
+
+
+
+
+
+
+
+
+ // SEARCH
+
+ getSearchResults: async function (accountData, currentQuery) {
+
+ let _wbxml = eas.wbxmltools.createWBXML();
+ _wbxml.switchpage("Search");
+ _wbxml.otag("Search");
+ _wbxml.otag("Store");
+ _wbxml.atag("Name", "GAL");
+ _wbxml.atag("Query", currentQuery);
+ _wbxml.otag("Options");
+ _wbxml.atag("Range", "0-99"); //Z-Push needs a Range
+ //Not valid for GAL: https://msdn.microsoft.com/en-us/library/gg675461(v=exchg.80).aspx
+ //_wbxml.atag("DeepTraversal");
+ //_wbxml.atag("RebuildResults");
+ _wbxml.ctag();
+ _wbxml.ctag();
+ _wbxml.ctag();
+
+ let wbxml = _wbxml.getBytes();
+
+ eas.network.logXML(wbxml, "Send (GAL Search)");
+ let command = "Search";
+
+ let authData = eas.network.getAuthData(accountData);
+ let oauthData = eas.network.getOAuthObj({ accountData });
+ let userAgent = accountData.getAccountProperty("useragent"); //plus calendar.useragent.extra = Lightning/5.4.5.2
+ let deviceType = accountData.getAccountProperty("devicetype");
+ let deviceId = accountData.getAccountProperty("deviceId");
+
+ TbSync.dump("Sending (EAS v" + accountData.getAccountProperty("asversion") +")", "POST " + eas.network.getEasURL(accountData) + '?Cmd=' + command + '&User=' + encodeURIComponent(authData.user) + '&DeviceType=' +deviceType + '&DeviceId=' + deviceId, true);
+
+ for (let i=0; i < 2; i++) {
+ // check OAuth situation before connecting
+ if (oauthData && (!oauthData.accessToken || oauthData.isExpired())) {
+ let _rv = {}
+ if (!(await oauthData.asyncConnect(_rv))) {
+ throw eas.sync.finish("error", _rv.error);
+ }
+ }
+
+ try {
+ let response = await new Promise(function(resolve, reject) {
+ // Create request handler - API changed with TB60 to new XMKHttpRequest()
+ let req = new XMLHttpRequest();
+ req.mozBackgroundRequest = true;
+ req.open("POST", eas.network.getEasURL(accountData) + '?Cmd=' + command + '&User=' + encodeURIComponent(authData.user) + '&DeviceType=' +encodeURIComponent(deviceType) + '&DeviceId=' + deviceId, true);
+ req.overrideMimeType("text/plain");
+ req.setRequestHeader("User-Agent", userAgent);
+ req.setRequestHeader("Content-Type", "application/vnd.ms-sync.wbxml");
+
+ if (authData.password) {
+ if (eas.network.getOAuthObj({ accountData })) {
+ req.setRequestHeader("Authorization", 'Bearer ' + eas.network.getOAuthValue(authData.password, "access"));
+ } else {
+ req.setRequestHeader("Authorization", 'Basic ' + TbSync.tools.b64encode(authData.user + ':' + authData.password));
+ }
+ }
+
+ if (accountData.getAccountProperty("asversion") == "2.5") {
+ req.setRequestHeader("MS-ASProtocolVersion", "2.5");
+ } else {
+ req.setRequestHeader("MS-ASProtocolVersion", "14.0");
+ }
+ req.setRequestHeader("Content-Length", wbxml.length);
+ if (accountData.getAccountProperty("provision")) {
+ req.setRequestHeader("X-MS-PolicyKey", accountData.getAccountProperty("policykey"));
+ TbSync.dump("PolicyKey used", accountData.getAccountProperty("policykey"));
+ }
+
+ req.timeout = eas.Base.getConnectionTimeout();
+
+ req.ontimeout = function () {
+ reject("GAL Search timeout");
+ };
+
+ req.onerror = function () {
+ reject("GAL Search Error");
+ };
+
+ req.onload = function() {
+ let response = req.responseText;
+
+ switch(req.status) {
+
+ case 200: //OK
+ eas.network.logXML(response, "Received (GAL Search");
+
+ //What to do on error? IS this an error? Yes!
+ if (response.length !== 0 && response.substr(0, 4) !== String.fromCharCode(0x03, 0x01, 0x6A, 0x00)) {
+ TbSync.dump("Recieved Data", "Expecting WBXML but got junk (request status = " + req.status + ", ready state = " + req.readyState + "\n>>>>>>>>>>\n" + response + "\n<<<<<<<<<<\n");
+ reject("GAL Search Response Invalid");
+ } else {
+ resolve(response);
+ }
+ break;
+
+ case 401: // bad auth
+ resolve("401");
+ break;
+
+ default:
+ reject("GAL Search Failed: " + req.status);
+ }
+ };
+
+ req.send(wbxml);
+
+ });
+
+ if (response === "401") {
+ // try to recover from bad auth via token refresh
+ if (oauthData) {
+ oauthData.accessToken = "";
+ continue;
+ }
+ }
+
+ return response;
+ } catch (e) {
+ Components.utils.reportError(e);
+ return;
+ }
+ }
+ },
+
+
+
+
+
+
+
+
+
+
+ // OPTIONS
+
+ getServerOptions: async function (syncData) {
+ syncData.setSyncState("prepare.request.options");
+ let authData = eas.network.getAuthData(syncData.accountData);
+
+ let userAgent = syncData.accountData.getAccountProperty("useragent"); //plus calendar.useragent.extra = Lightning/5.4.5.2
+ TbSync.dump("Sending", "OPTIONS " + eas.network.getEasURL(syncData.accountData));
+
+ let allowedRetries = 5;
+ let retry;
+ let oauthData = eas.network.getOAuthObj({ accountData: syncData.accountData });
+
+ do {
+ retry = false;
+
+ // Check OAuth situation before connecting
+ if (oauthData && (!oauthData.accessToken || oauthData.isExpired())) {
+ let _rv = {};
+ syncData.setSyncState("oauthprompt");
+ if (!(await oauthData.asyncConnect(_rv))) {
+ throw eas.sync.finish("error", _rv.error);
+ }
+ }
+
+ let result = await new Promise(function(resolve,reject) {
+ syncData.req = new XMLHttpRequest();
+ syncData.req.mozBackgroundRequest = true;
+ syncData.req.open("OPTIONS", eas.network.getEasURL(syncData.accountData), true);
+ syncData.req.overrideMimeType("text/plain");
+ syncData.req.setRequestHeader("User-Agent", userAgent);
+ if (authData.password) {
+ if (eas.network.getOAuthObj({ accountData: syncData.accountData })) {
+ syncData.req.setRequestHeader("Authorization", 'Bearer ' + eas.network.getOAuthValue(authData.password, "access"));
+ } else {
+ syncData.req.setRequestHeader("Authorization", 'Basic ' + TbSync.tools.b64encode(authData.user + ':' + authData.password));
+ }
+ }
+ syncData.req.timeout = eas.Base.getConnectionTimeout();
+
+ syncData.req.ontimeout = function () {
+ resolve();
+ };
+
+ syncData.req.onerror = function () {
+ let responseData = {};
+ responseData["MS-ASProtocolVersions"] = syncData.req.getResponseHeader("MS-ASProtocolVersions");
+ responseData["MS-ASProtocolCommands"] = syncData.req.getResponseHeader("MS-ASProtocolCommands");
+
+ TbSync.dump("EAS OPTIONS with response (status: "+syncData.req.status+")", "\n" +
+ "responseText: " + syncData.req.responseText + "\n" +
+ "responseHeader(MS-ASProtocolVersions): " + responseData["MS-ASProtocolVersions"]+"\n" +
+ "responseHeader(MS-ASProtocolCommands): " + responseData["MS-ASProtocolCommands"]);
+ resolve();
+ };
+
+ syncData.req.onload = function() {
+ syncData.setSyncState("eval.request.options");
+ let responseData = {};
+
+ switch(syncData.req.status) {
+ case 401: // AuthError
+ let rv = {};
+ rv.errorObj = eas.sync.finish("error", "401");
+ rv.errorType = "PasswordPrompt";
+ resolve(rv);
+ break;
+
+ case 200:
+ responseData["MS-ASProtocolVersions"] = syncData.req.getResponseHeader("MS-ASProtocolVersions");
+ responseData["MS-ASProtocolCommands"] = syncData.req.getResponseHeader("MS-ASProtocolCommands");
+
+ TbSync.dump("EAS OPTIONS with response (status: 200)", "\n" +
+ "responseText: " + syncData.req.responseText + "\n" +
+ "responseHeader(MS-ASProtocolVersions): " + responseData["MS-ASProtocolVersions"]+"\n" +
+ "responseHeader(MS-ASProtocolCommands): " + responseData["MS-ASProtocolCommands"]);
+
+ if (responseData && responseData["MS-ASProtocolCommands"] && responseData["MS-ASProtocolVersions"]) {
+ syncData.accountData.setAccountProperty("allowedEasCommands", responseData["MS-ASProtocolCommands"]);
+ syncData.accountData.setAccountProperty("allowedEasVersions", responseData["MS-ASProtocolVersions"]);
+ syncData.accountData.setAccountProperty("lastEasOptionsUpdate", Date.now());
+ }
+ resolve();
+ break;
+
+ default:
+ resolve();
+ break;
+
+ }
+ };
+
+ syncData.setSyncState("send.request.options");
+ syncData.req.send();
+
+ });
+
+ if (result && result.hasOwnProperty("errorType") && result.errorType == "PasswordPrompt") {
+ if (allowedRetries > 0) {
+ if (oauthData) {
+ oauthData.accessToken = "";
+ retry = true;
+ } else {
+ syncData.setSyncState("passwordprompt");
+ let authData = eas.network.getAuthData(syncData.accountData);
+ let promptData = {
+ windowID: "auth:" + syncData.accountData.accountID,
+ accountname: syncData.accountData.getAccountProperty("accountname"),
+ usernameLocked: syncData.accountData.isConnected(),
+ username: authData.user
+ }
+ let credentials = await TbSync.passwordManager.asyncPasswordPrompt(promptData, eas.openWindows);
+ if (credentials) {
+ authData.updateLoginData(credentials.username, credentials.password);
+ retry = true;
+ }
+ }
+ }
+
+ if (!retry) {
+ throw result.errorObj;
+ }
+ }
+
+ allowedRetries--;
+ } while (retry);
+ },
+
+
+
+
+
+
+
+
+
+
+ // AUTODISCOVER
+
+ updateServerConnectionViaAutodiscover: async function (syncData) {
+ syncData.setSyncState("prepare.request.autodiscover");
+ let authData = eas.network.getAuthData(syncData.accountData);
+
+ syncData.setSyncState("send.request.autodiscover");
+ let result = await eas.network.getServerConnectionViaAutodiscover(authData.user, authData.password, 30*1000);
+
+ syncData.setSyncState("eval.response.autodiscover");
+ if (result.errorcode == 200) {
+ //update account
+ syncData.accountData.setAccountProperty("host", eas.network.stripAutodiscoverUrl(result.server));
+ syncData.accountData.setAccountProperty("user", result.user);
+ syncData.accountData.setAccountProperty("https", (result.server.substring(0,5) == "https"));
+ }
+
+ return result.errorcode;
+ },
+
+ stripAutodiscoverUrl: function(url) {
+ let u = url;
+ while (u.endsWith("/")) { u = u.slice(0,-1); }
+ if (u.endsWith("/Microsoft-Server-ActiveSync")) u=u.slice(0, -28);
+ else TbSync.dump("Received non-standard EAS url via autodiscover:", url);
+
+ return u.split("//")[1]; //cut off protocol
+ },
+
+ getServerConnectionViaAutodiscover : async function (user, password, maxtimeout) {
+ let urls = [];
+ let parts = user.split("@");
+
+ urls.push({"url":"http://autodiscover."+parts[1]+"/autodiscover/autodiscover.xml", "user":user});
+ urls.push({"url":"http://"+parts[1]+"/autodiscover/autodiscover.xml", "user":user});
+ urls.push({"url":"http://autodiscover."+parts[1]+"/Autodiscover/Autodiscover.xml", "user":user});
+ urls.push({"url":"http://"+parts[1]+"/Autodiscover/Autodiscover.xml", "user":user});
+
+ urls.push({"url":"https://autodiscover."+parts[1]+"/autodiscover/autodiscover.xml", "user":user});
+ urls.push({"url":"https://"+parts[1]+"/autodiscover/autodiscover.xml", "user":user});
+ urls.push({"url":"https://autodiscover."+parts[1]+"/Autodiscover/Autodiscover.xml", "user":user});
+ urls.push({"url":"https://"+parts[1]+"/Autodiscover/Autodiscover.xml", "user":user});
+
+ let requests = [];
+ let responses = []; //array of objects {url, error, server}
+
+ for (let i=0; i< urls.length; i++) {
+ await TbSync.tools.sleep(200);
+ requests.push( eas.network.getServerConnectionViaAutodiscoverRedirectWrapper(urls[i].url, urls[i].user, password, maxtimeout) );
+ }
+
+ try {
+ responses = await Promise.all(requests);
+ } catch (e) {
+ responses.push(e.result); //this is actually a success, see return value of getServerConnectionViaAutodiscoverRedirectWrapper()
+ }
+
+ let result;
+ let log = [];
+ for (let r=0; r < responses.length; r++) {
+ log.push("* "+responses[r].url+" @ " + responses[r].user +" : " + (responses[r].server ? responses[r].server : responses[r].error));
+
+ if (responses[r].server) {
+ result = {"server": responses[r].server, "user": responses[r].user, "error": "", "errorcode": 200};
+ break;
+ }
+
+ if (responses[r].error == 403 || responses[r].error == 401) {
+ //we could still find a valid server, so just store this state
+ result = {"server": "", "user": responses[r].user, "errorcode": responses[r].error, "error": TbSync.getString("status." + responses[r].error, "eas")};
+ }
+ }
+
+ //this is only reached on fail, if no result defined yet, use general error
+ if (!result) {
+ result = {"server": "", "user": user, "error": TbSync.getString("autodiscover.Failed","eas").replace("##user##", user), "errorcode": 503};
+ }
+
+ TbSync.eventlog.add("error", new TbSync.EventLogInfo("eas"), result.error, log.join("\n"));
+ return result;
+ },
+
+ getServerConnectionViaAutodiscoverRedirectWrapper : async function (url, user, password, maxtimeout) {
+ //using HEAD to find URL redirects until response URL no longer changes
+ // * XHR should follow redirects transparently, but that does not always work, POST data could get lost, so we
+ // * need to find the actual POST candidates (example: outlook.de accounts)
+ let result = {};
+ let method = "HEAD";
+ let connection = { url, user };
+
+ do {
+ await TbSync.tools.sleep(200);
+ result = await eas.network.getServerConnectionViaAutodiscoverRequest(method, connection, password, maxtimeout);
+ method = "";
+
+ if (result.error == "redirect found") {
+ TbSync.dump("EAS autodiscover URL redirect", "\n" + connection.url + " @ " + connection.user + " => \n" + result.url + " @ " + result.user);
+ connection.url = result.url;
+ connection.user = result.user;
+ method = "HEAD";
+ } else if (result.error == "POST candidate found") {
+ method = "POST";
+ }
+
+ } while (method);
+
+ //invert reject and resolve, so we exit the promise group on success right away
+ if (result.server) {
+ let e = new Error("Not an error (early exit from promise group)");
+ e.result = result;
+ throw e;
+ } else {
+ return result;
+ }
+ },
+
+ getServerConnectionViaAutodiscoverRequest: function (method, connection, password, maxtimeout) {
+ TbSync.dump("Querry EAS autodiscover URL", connection.url + " @ " + connection.user);
+
+ return new Promise(function(resolve,reject) {
+
+ let xml = '<?xml version="1.0" encoding="utf-8"?>\r\n';
+ xml += '<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/mobilesync/requestschema/2006">\r\n';
+ xml += '<Request>\r\n';
+ xml += '<EMailAddress>' + connection.user + '</EMailAddress>\r\n';
+ xml += '<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006</AcceptableResponseSchema>\r\n';
+ xml += '</Request>\r\n';
+ xml += '</Autodiscover>\r\n';
+
+ let userAgent = eas.prefs.getCharPref("clientID.useragent"); //plus calendar.useragent.extra = Lightning/5.4.5.2
+
+ // Create request handler - API changed with TB60 to new XMKHttpRequest()
+ let req = new XMLHttpRequest();
+ req.mozBackgroundRequest = true;
+ req.open(method, connection.url, true);
+ req.timeout = maxtimeout;
+ req.setRequestHeader("User-Agent", userAgent);
+
+ let secure = (connection.url.substring(0,8).toLowerCase() == "https://");
+
+ if (method == "POST") {
+ req.setRequestHeader("Content-Length", xml.length);
+ req.setRequestHeader("Content-Type", "text/xml");
+ if (secure && password) {
+ // OAUTH accounts cannot authenticate against the standard discovery services
+ // updateServerConnectionViaAutodiscover() is not passing them on
+ req.setRequestHeader("Authorization", "Basic " + TbSync.tools.b64encode(connection.user + ":" + password));
+ }
+ }
+
+ req.ontimeout = function () {
+ TbSync.dump("EAS autodiscover with timeout", "\n" + connection.url + " => \n" + req.responseURL);
+ resolve({"url":req.responseURL, "error":"timeout", "server":"", "user":connection.user});
+ };
+
+ req.onerror = function () {
+ let error = TbSync.network.createTCPErrorFromFailedXHR(req);
+ if (!error) error = req.responseText;
+ TbSync.dump("EAS autodiscover with error ("+error+")", "\n" + connection.url + " => \n" + req.responseURL);
+ resolve({"url":req.responseURL, "error":error, "server":"", "user":connection.user});
+ };
+
+ req.onload = function() {
+ //initiate rerun on redirects
+ if (req.responseURL != connection.url) {
+ resolve({"url":req.responseURL, "error":"redirect found", "server":"", "user":connection.user});
+ return;
+ }
+
+ //initiate rerun on HEAD request without redirect (rerun and do a POST on this)
+ if (method == "HEAD") {
+ resolve({"url":req.responseURL, "error":"POST candidate found", "server":"", "user":connection.user});
+ return;
+ }
+
+ //ignore POST without autherization (we just do them to get redirect information)
+ if (!secure) {
+ resolve({"url":req.responseURL, "error":"unsecure POST", "server":"", "user":connection.user});
+ return;
+ }
+
+ //evaluate secure POST requests which have not been redirected
+ TbSync.dump("EAS autodiscover POST with status (" + req.status + ")", "\n" + connection.url + " => \n" + req.responseURL + "\n[" + req.responseText + "]");
+
+ if (req.status === 200) {
+ let data = null;
+ // getDataFromXMLString may throw an error which cannot be catched outside onload,
+ // because we are in an async callback of the surrounding Promise
+ // Alternatively we could just return the responseText and do any data analysis outside of the Promise
+ try {
+ data = eas.xmltools.getDataFromXMLString(req.responseText);
+ } catch (e) {
+ resolve({"url":req.responseURL, "error":"bad response", "server":"", "user":connection.user});
+ return;
+ }
+
+ if (!(data === null) && data.Autodiscover && data.Autodiscover.Response && data.Autodiscover.Response.Action) {
+ // "Redirect" or "Settings" are possible
+ if (data.Autodiscover.Response.Action.Redirect) {
+ // redirect, start again with new user
+ let newuser = action.Redirect;
+ resolve({"url":req.responseURL, "error":"redirect found", "server":"", "user":newuser});
+
+ } else if (data.Autodiscover.Response.Action.Settings) {
+ // get server settings
+ let server = eas.xmltools.nodeAsArray(data.Autodiscover.Response.Action.Settings.Server);
+
+ for (let count = 0; count < server.length; count++) {
+ if (server[count].Type == "MobileSync" && server[count].Url) {
+ resolve({"url":req.responseURL, "error":"", "server":server[count].Url, "user":connection.user});
+ return;
+ }
+ }
+ }
+ } else {
+ resolve({"url":req.responseURL, "error":"invalid", "server":"", "user":connection.user});
+ }
+ } else {
+ resolve({"url":req.responseURL, "error":req.status, "server":"", "user":connection.user});
+ }
+ };
+
+ if (method == "HEAD") req.send();
+ else req.send(xml);
+
+ });
+ },
+
+ getServerConnectionViaAutodiscoverV2JsonRequest: function (url, maxtimeout) {
+ TbSync.dump("Querry EAS autodiscover V2 URL", url);
+
+ return new Promise(function(resolve,reject) {
+
+ let userAgent = eas.prefs.getCharPref("clientID.useragent"); //plus calendar.useragent.extra = Lightning/5.4.5.2
+
+ // Create request handler - API changed with TB60 to new XMKHttpRequest()
+ let req = new XMLHttpRequest();
+ req.mozBackgroundRequest = true;
+ req.open("GET", url, true);
+ req.timeout = maxtimeout;
+ req.setRequestHeader("User-Agent", userAgent);
+
+ req.ontimeout = function () {
+ TbSync.dump("EAS autodiscover V2 with timeout", "\n" + url + " => \n" + req.responseURL);
+ resolve({"url":req.responseURL, "error":"timeout", "server":""});
+ };
+
+ req.onerror = function () {
+ let error = TbSync.network.createTCPErrorFromFailedXHR(req);
+ if (!error) error = req.responseText;
+ TbSync.dump("EAS autodiscover V2 with error ("+error+")", "\n" + url + " => \n" + req.responseURL);
+ resolve({"url":req.responseURL, "error":error, "server":""});
+ };
+
+ req.onload = function() {
+ if (req.status === 200) {
+ let data = JSON.parse(req.responseText);
+
+ if (data && data.Url) {
+ resolve({"url":req.responseURL, "error":"", "server": eas.network.stripAutodiscoverUrl(data.Url)});
+ } else {
+ resolve({"url":req.responseURL, "error":"invalid", "server":""});
+ }
+ return;
+ }
+
+ resolve({"url":req.responseURL, "error":req.status, "server":""});
+ };
+
+ req.send();
+ });
+ }
+}
diff --git a/content/includes/sync.js b/content/includes/sync.js
new file mode 100644
index 0000000..451b661
--- /dev/null
+++ b/content/includes/sync.js
@@ -0,0 +1,1504 @@
+/*
+ * 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, {
+ CalRecurrenceInfo: "resource:///modules/CalRecurrenceInfo.jsm",
+});
+
+// - https://dxr.mozilla.org/comm-central/source/calendar/base/public/calIEvent.idl
+// - https://dxr.mozilla.org/comm-central/source/calendar/base/public/calIItemBase.idl
+// - https://dxr.mozilla.org/comm-central/source/calendar/base/public/calICalendar.idl
+// - https://dxr.mozilla.org/comm-central/source/calendar/base/modules/calAsyncUtils.jsm
+
+// https://msdn.microsoft.com/en-us/library/dd299454(v=exchg.80).aspx
+
+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/EAS: Unknown status <"+aStatus+">");
+ status = TbSync.StatusData.ERROR;
+ break;
+ }
+
+ let e = new Error();
+ e.name = "eas4tbsync";
+ e.message = status.toUpperCase() + ": " + msg.toString() + " (" + details.toString() + ")";
+ e.statusData = new TbSync.StatusData(status, msg.toString(), details.toString());
+ return e;
+ },
+
+
+ resetFolderSyncInfo: function (folderData) {
+ folderData.resetFolderProperty("synckey");
+ folderData.resetFolderProperty("lastsynctime");
+ },
+
+
+ // update folders avail on server and handle added, removed and renamed
+ // folders
+ folderList: async function(syncData) {
+ //should we recheck options/commands? Always check, if we have no info about asversion!
+ if (syncData.accountData.getAccountProperty("asversion", "") == "" || (Date.now() - syncData.accountData.getAccountProperty("lastEasOptionsUpdate")) > 86400000 ) {
+ await eas.network.getServerOptions(syncData);
+ }
+
+ //only update the actual used asversion, if we are currently not connected or it has not yet been set
+ if (syncData.accountData.getAccountProperty("asversion", "") == "" || !syncData.accountData.isConnected()) {
+ //eval the currently in the UI selected EAS version
+ let asversionselected = syncData.accountData.getAccountProperty("asversionselected");
+ let allowedVersionsString = syncData.accountData.getAccountProperty("allowedEasVersions").trim();
+ let allowedVersionsArray = allowedVersionsString.split(",");
+
+ if (asversionselected == "auto") {
+ if (allowedVersionsArray.includes("14.0")) syncData.accountData.setAccountProperty("asversion", "14.0");
+ else if (allowedVersionsArray.includes("2.5")) syncData.accountData.setAccountProperty("asversion", "2.5");
+ else if (allowedVersionsString == "") {
+ throw eas.sync.finish("error", "InvalidServerOptions");
+ } else {
+ throw eas.sync.finish("error", "nosupportedeasversion::"+allowedVersionsArray.join(", "));
+ }
+ } else if (allowedVersionsString != "" && !allowedVersionsArray.includes(asversionselected)) {
+ throw eas.sync.finish("error", "notsupportedeasversion::"+asversionselected+"::"+allowedVersionsArray.join(", "));
+ } else {
+ //just use the value set by the user
+ syncData.accountData.setAccountProperty("asversion", asversionselected);
+ }
+ }
+
+ //do we need to get a new policy key?
+ if (syncData.accountData.getAccountProperty("provision") && syncData.accountData.getAccountProperty("policykey") == "0") {
+ await eas.network.getPolicykey(syncData);
+ }
+
+ //set device info
+ await eas.network.setDeviceInformation(syncData);
+
+ syncData.setSyncState("prepare.request.folders");
+ let foldersynckey = syncData.accountData.getAccountProperty("foldersynckey");
+
+ //build WBXML to request foldersync
+ let wbxml = eas.wbxmltools.createWBXML();
+ wbxml.switchpage("FolderHierarchy");
+ wbxml.otag("FolderSync");
+ wbxml.atag("SyncKey", foldersynckey);
+ wbxml.ctag();
+
+ syncData.setSyncState("send.request.folders");
+ let response = await eas.network.sendRequest(wbxml.getBytes(), "FolderSync", syncData);
+
+ syncData.setSyncState("eval.response.folders");
+ let wbxmlData = eas.network.getDataFromResponse(response);
+ eas.network.checkStatus(syncData, wbxmlData,"FolderSync.Status");
+
+ let synckey = eas.xmltools.getWbxmlDataField(wbxmlData,"FolderSync.SyncKey");
+ if (synckey) {
+ syncData.accountData.setAccountProperty("foldersynckey", synckey);
+ } else {
+ throw eas.sync.finish("error", "wbxmlmissingfield::FolderSync.SyncKey");
+ }
+
+ // If we reach this point, wbxmlData contains FolderSync node,
+ // so the next "if" will not fail with an javascript error, no need
+ // to use save getWbxmlDataField function.
+
+ // Are there any changes in folder hierarchy?
+ if (wbxmlData.FolderSync.Changes) {
+ // Looking for additions.
+ let add = eas.xmltools.nodeAsArray(wbxmlData.FolderSync.Changes.Add);
+ for (let count = 0; count < add.length; count++) {
+ // Only add allowed folder types to DB (include trash(4), so we can find trashed folders.
+ if (!["9","14","8","13","7","15", "4"].includes(add[count].Type))
+ continue;
+
+ let existingFolder = syncData.accountData.getFolder("serverID", add[count].ServerId);
+ if (existingFolder) {
+ // Server has send us an ADD for a folder we alreay have, treat as update.
+ existingFolder.setFolderProperty("foldername", add[count].DisplayName);
+ existingFolder.setFolderProperty("type", add[count].Type);
+ existingFolder.setFolderProperty("parentID", add[count].ParentId);
+ } else {
+ // Create folder obj for new folder settings.
+ let newFolder = syncData.accountData.createNewFolder();
+ switch (add[count].Type) {
+ case "9": // contact
+ case "14":
+ newFolder.setFolderProperty("targetType", "addressbook");
+ break;
+ case "8": // event
+ case "13":
+ newFolder.setFolderProperty("targetType", "calendar");
+ break;
+ case "7": // todo
+ case "15":
+ newFolder.setFolderProperty("targetType", "calendar");
+ break;
+ default:
+ newFolder.setFolderProperty("targetType", "unknown type ("+add[count].Type+")");
+ break;
+
+ }
+
+ newFolder.setFolderProperty("serverID", add[count].ServerId);
+ newFolder.setFolderProperty("foldername", add[count].DisplayName);
+ newFolder.setFolderProperty("type", add[count].Type);
+ newFolder.setFolderProperty("parentID", add[count].ParentId);
+
+ // Do we have a cached folder?
+ let cachedFolderData = syncData.accountData.getFolderFromCache("serverID", add[count].ServerId);
+ if (cachedFolderData) {
+ // Copy fields from cache which we want to re-use.
+ newFolder.setFolderProperty("targetColor", cachedFolderData.getFolderProperty("targetColor"));
+ newFolder.setFolderProperty("targetName", cachedFolderData.getFolderProperty("targetName"));
+ newFolder.setFolderProperty("downloadonly", cachedFolderData.getFolderProperty("downloadonly"));
+ }
+ }
+ }
+
+ // Looking for updates.
+ let update = eas.xmltools.nodeAsArray(wbxmlData.FolderSync.Changes.Update);
+ for (let count = 0; count < update.length; count++) {
+ let existingFolder = syncData.accountData.getFolder("serverID", update[count].ServerId);
+ if (existingFolder) {
+ // Update folder.
+ existingFolder.setFolderProperty("foldername", update[count].DisplayName);
+ existingFolder.setFolderProperty("type", update[count].Type);
+ existingFolder.setFolderProperty("parentID", update[count].ParentId);
+ }
+ }
+
+ // Looking for deletes. Do not delete the targets,
+ // but keep them as stale/unconnected elements.
+ let del = eas.xmltools.nodeAsArray(wbxmlData.FolderSync.Changes.Delete);
+ for (let count = 0; count < del.length; count++) {
+ let existingFolder = syncData.accountData.getFolder("serverID", del[count].ServerId);
+ if (existingFolder) {
+ existingFolder.remove("[deleted on server]");
+ }
+ }
+ }
+ },
+
+
+
+
+
+ deleteFolder: async function (syncData) {
+ if (!syncData.currentFolderData) {
+ return;
+ }
+
+ if (!syncData.accountData.getAccountProperty("allowedEasCommands").split(",").includes("FolderDelete")) {
+ throw eas.sync.finish("error", "notsupported::FolderDelete");
+ }
+
+ syncData.setSyncState("prepare.request.deletefolder");
+ let foldersynckey = syncData.accountData.getAccountProperty("foldersynckey");
+
+ //request foldersync
+ let wbxml = eas.wbxmltools.createWBXML();
+ wbxml.switchpage("FolderHierarchy");
+ wbxml.otag("FolderDelete");
+ wbxml.atag("SyncKey", foldersynckey);
+ wbxml.atag("ServerId", syncData.currentFolderData.getFolderProperty("serverID"));
+ wbxml.ctag();
+
+ syncData.setSyncState("send.request.deletefolder");
+ let response = await eas.network.sendRequest(wbxml.getBytes(), "FolderDelete", syncData);
+
+ syncData.setSyncState("eval.response.deletefolder");
+ let wbxmlData = eas.network.getDataFromResponse(response);
+
+ eas.network.checkStatus(syncData, wbxmlData,"FolderDelete.Status");
+
+ let synckey = eas.xmltools.getWbxmlDataField(wbxmlData,"FolderDelete.SyncKey");
+ if (synckey) {
+ syncData.accountData.setAccountProperty("foldersynckey", synckey);
+ syncData.currentFolderData.remove();
+ } else {
+ throw eas.sync.finish("error", "wbxmlmissingfield::FolderDelete.SyncKey");
+ }
+ },
+
+
+
+
+
+ singleFolder: async function (syncData) {
+ // add target to syncData
+ try {
+ // accessing the target for the first time will check if it is avail and if not will create it (if possible)
+ syncData.target = await syncData.currentFolderData.targetData.getTarget();
+ } catch (e) {
+ Components.utils.reportError(e);
+ throw eas.sync.finish("warning", e.message);
+ }
+
+ //get syncData type, which is also used in WBXML for the CLASS element
+ syncData.type = null;
+ switch (syncData.currentFolderData.getFolderProperty("type")) {
+ case "9": //contact
+ case "14":
+ syncData.type = "Contacts";
+ break;
+ case "8": //event
+ case "13":
+ syncData.type = "Calendar";
+ break;
+ case "7": //todo
+ case "15":
+ syncData.type = "Tasks";
+ break;
+ default:
+ throw eas.sync.finish("info", "skipped");
+ break;
+ }
+
+ syncData.setSyncState("preparing");
+
+ //get synckey if needed
+ syncData.synckey = syncData.currentFolderData.getFolderProperty("synckey");
+ if (syncData.synckey == "") {
+ await eas.network.getSynckey(syncData);
+ }
+
+ //sync folder
+ syncData.timeOfLastSync = syncData.currentFolderData.getFolderProperty( "lastsynctime") / 1000;
+ syncData.timeOfThisSync = (Date.now() / 1000) - 1;
+
+ let lightningBatch = false;
+ let lightningReadOnly = "";
+ let error = null;
+
+ // We ned to intercept any throw error, because lightning needs a few operations after sync finished
+ try {
+ switch (syncData.type) {
+ case "Contacts":
+ await eas.sync.easFolder(syncData);
+ break;
+
+ case "Calendar":
+ case "Tasks":
+ //save current value of readOnly (or take it from the setting)
+ lightningReadOnly = syncData.target.calendar.getProperty("readOnly") || syncData.currentFolderData.getFolderProperty( "downloadonly");
+ syncData.target.calendar.setProperty("readOnly", false);
+
+ lightningBatch = true;
+ syncData.target.calendar.startBatch();
+
+ await eas.sync.easFolder(syncData);
+ break;
+ }
+ } catch (report) {
+ error = report;
+ }
+
+ if (lightningBatch) {
+ syncData.target.calendar.endBatch();
+ syncData.target.calendar.setProperty("readOnly", lightningReadOnly);
+ }
+
+ if (error) throw error;
+ },
+
+
+
+
+
+
+
+
+
+
+ // ---------------------------------------------------------------------------
+ // MAIN FUNCTIONS TO SYNC AN EAS FOLDER
+ // ---------------------------------------------------------------------------
+
+ easFolder: async function (syncData) {
+ syncData.progressData.reset();
+
+ if (syncData.currentFolderData.getFolderProperty("downloadonly")) {
+ await eas.sync.revertLocalChanges(syncData);
+ }
+
+ await eas.network.getItemEstimate(syncData);
+ await eas.sync.requestRemoteChanges(syncData);
+
+ if (!syncData.currentFolderData.getFolderProperty("downloadonly")) {
+ let sendChanges = await eas.sync.sendLocalChanges(syncData);
+ if (sendChanges) {
+ // This is ugly as hell, but Microsoft sometimes sets the state of the
+ // remote account to "changed" after we have send a local change (even
+ // though it has acked the change) and this will cause the server to
+ // send a change request with our next sync. Because we follow the
+ // "server wins" policy, this will overwrite any additional local change
+ // we have done in the meantime. This is stupid, but we wait 2s and
+ // hope it is enough to catch this second ack of the local change.
+ let timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
+ await new Promise(function(resolve, reject) {
+ let event = {
+ notify: function(timer) {
+ resolve();
+ }
+ }
+ timer.initWithCallback(event, 2000, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
+ });
+ await eas.sync.requestRemoteChanges(syncData);
+ }
+ }
+ },
+
+
+ requestRemoteChanges: async function (syncData) {
+ do {
+ syncData.setSyncState("prepare.request.remotechanges");
+ syncData.request = "";
+ syncData.response = "";
+
+ // BUILD WBXML
+ let wbxml = eas.wbxmltools.createWBXML();
+ wbxml.otag("Sync");
+ wbxml.otag("Collections");
+ wbxml.otag("Collection");
+ if (syncData.accountData.getAccountProperty("asversion") == "2.5") wbxml.atag("Class", syncData.type);
+ wbxml.atag("SyncKey", syncData.synckey);
+ wbxml.atag("CollectionId", syncData.currentFolderData.getFolderProperty("serverID"));
+ wbxml.atag("DeletesAsMoves");
+ wbxml.atag("GetChanges");
+ wbxml.atag("WindowSize", eas.prefs.getIntPref("maxitems").toString());
+
+ if (syncData.accountData.getAccountProperty("asversion") != "2.5") {
+ wbxml.otag("Options");
+ if (syncData.type == "Calendar") wbxml.atag("FilterType", syncData.currentFolderData.accountData.getAccountProperty("synclimit"));
+ wbxml.atag("Class", syncData.type);
+ wbxml.switchpage("AirSyncBase");
+ wbxml.otag("BodyPreference");
+ wbxml.atag("Type", "1");
+ wbxml.ctag();
+ wbxml.switchpage("AirSync");
+ wbxml.ctag();
+ } else if (syncData.type == "Calendar") { //in 2.5 we only send it to filter Calendar
+ wbxml.otag("Options");
+ wbxml.atag("FilterType", syncData.currentFolderData.accountData.getAccountProperty("synclimit"));
+ wbxml.ctag();
+ }
+
+ wbxml.ctag();
+ wbxml.ctag();
+ wbxml.ctag();
+
+ //SEND REQUEST
+ syncData.setSyncState("send.request.remotechanges");
+ let response = await eas.network.sendRequest(wbxml.getBytes(), "Sync", syncData);
+
+ //VALIDATE RESPONSE
+ // get data from wbxml response, some servers send empty response if there are no changes, which is not an error
+ let wbxmlData = eas.network.getDataFromResponse(response, eas.flags.allowEmptyResponse);
+ if (wbxmlData === null) return;
+
+ //check status, throw on error
+ eas.network.checkStatus(syncData, wbxmlData,"Sync.Collections.Collection.Status");
+
+ //PROCESS COMMANDS
+ await eas.sync.processCommands(wbxmlData, syncData);
+
+ //Update count in UI
+ syncData.setSyncState("eval.response.remotechanges");
+
+ //update synckey
+ eas.network.updateSynckey(syncData, wbxmlData);
+
+ if (!eas.xmltools.hasWbxmlDataField(wbxmlData,"Sync.Collections.Collection.MoreAvailable")) {
+ //Feedback from users: They want to see the final count
+ await TbSync.tools.sleep(100);
+ return;
+ }
+ } while (true);
+
+ },
+
+
+ sendLocalChanges: async function (syncData) {
+ let maxnumbertosend = eas.prefs.getIntPref("maxitems");
+ syncData.progressData.reset(0, syncData.target.getItemsFromChangeLog().length);
+
+ //keep track of failed items
+ syncData.failedItems = [];
+
+ let done = false;
+ let numberOfItemsToSend = maxnumbertosend;
+ let sendChanges = false;
+ do {
+ syncData.setSyncState("prepare.request.localchanges");
+ syncData.request = "";
+ syncData.response = "";
+
+ //get changed items from ChangeLog
+ let changes = syncData.target.getItemsFromChangeLog(numberOfItemsToSend);
+ //console.log("chnages", changes);
+ let c=0;
+ let e=0;
+
+ //keep track of send items during this request
+ let changedItems = [];
+ let addedItems = {};
+ let sendItems = [];
+
+ // BUILD WBXML
+ let wbxml = eas.wbxmltools.createWBXML();
+ wbxml.otag("Sync");
+ wbxml.otag("Collections");
+ wbxml.otag("Collection");
+ if (syncData.accountData.getAccountProperty("asversion") == "2.5") wbxml.atag("Class", syncData.type);
+ wbxml.atag("SyncKey", syncData.synckey);
+ wbxml.atag("CollectionId", syncData.currentFolderData.getFolderProperty("serverID"));
+ wbxml.otag("Commands");
+
+ for (let i=0; i<changes.length; i++) if (!syncData.failedItems.includes(changes[i].itemId)) {
+ //TbSync.dump("CHANGES",(i+1) + "/" + changes.length + " ("+changes[i].status+"," + changes[i].itemId + ")");
+ let item = null;
+ switch (changes[i].status) {
+
+ case "added_by_user":
+ item = await syncData.target.getItem(changes[i].itemId);
+ if (item) {
+ //filter out bad object types for this folder
+ if (syncData.type == "Contacts" && item.isMailList) {
+ // Mailing lists are not supported, this is not an error
+ TbSync.eventlog.add("warning", syncData.eventLogInfo, "MailingListNotSupportedItemSkipped");
+ syncData.target.removeItemFromChangeLog(changes[i].itemId);
+ } else if (syncData.type == eas.sync.getEasItemType(item)) {
+ //create a temp clientId, to cope with too long or invalid clientIds (for EAS)
+ let clientId = Date.now() + "-" + c;
+ addedItems[clientId] = changes[i].itemId;
+ sendItems.push({type: changes[i].status, id: changes[i].itemId});
+
+ wbxml.otag("Add");
+ wbxml.atag("ClientId", clientId); //Our temp clientId will get replaced by an id generated by the server
+ wbxml.otag("ApplicationData");
+ wbxml.switchpage(syncData.type);
+
+/*wbxml.atag("TimeZone", "xP///0UAdQByAG8AcABlAC8AQgBlAHIAbABpAG4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAFAAIAAAAAAAAAAAAAAEUAdQByAG8AcABlAC8AQgBlAHIAbABpAG4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAFAAEAAAAAAAAAxP///w==");
+wbxml.atag("AllDayEvent", "0");
+wbxml.switchpage("AirSyncBase");
+wbxml.otag("Body");
+ wbxml.atag("Type", "1");
+ wbxml.atag("EstimatedDataSize", "0");
+ wbxml.atag("Data");
+wbxml.ctag();
+
+wbxml.switchpage(syncData.type);
+wbxml.atag("BusyStatus", "2");
+wbxml.atag("OrganizerName", "REDACTED.REDACTED");
+wbxml.atag("OrganizerEmail", "REDACTED.REDACTED@REDACTED");
+wbxml.atag("DtStamp", "20190131T091024Z");
+wbxml.atag("EndTime", "20180906T083000Z");
+wbxml.atag("Location");
+wbxml.atag("Reminder", "5");
+wbxml.atag("Sensitivity", "0");
+wbxml.atag("Subject", "SE-CN weekly sync");
+wbxml.atag("StartTime", "20180906T080000Z");
+wbxml.atag("UID", "1D51E503-9DFE-4A46-A6C2-9129E5E00C1D");
+wbxml.atag("MeetingStatus", "3");
+wbxml.otag("Attendees");
+ wbxml.otag("Attendee");
+ wbxml.atag("Email", "REDACTED.REDACTED@REDACTED");
+ wbxml.atag("Name", "REDACTED.REDACTED");
+ wbxml.atag("AttendeeType", "1");
+ wbxml.ctag();
+wbxml.ctag();
+wbxml.atag("Categories");
+wbxml.otag("Recurrence");
+ wbxml.atag("Type", "1");
+ wbxml.atag("DayOfWeek", "16");
+ wbxml.atag("Interval", "1");
+wbxml.ctag();
+wbxml.otag("Exceptions");
+ wbxml.otag("Exception");
+ wbxml.atag("ExceptionStartTime", "20181227T090000Z");
+ wbxml.atag("Deleted", "1");
+ wbxml.ctag();
+wbxml.ctag();*/
+
+ wbxml.append(await eas.sync.getWbxmlFromThunderbirdItem(item, syncData));
+ wbxml.switchpage("AirSync");
+ wbxml.ctag();
+ wbxml.ctag();
+ c++;
+ } else {
+ eas.sync.updateFailedItems(syncData, "forbidden" + eas.sync.getEasItemType(item) +"ItemIn" + syncData.type + "Folder", item.primaryKey, item.toString());
+ e++;
+ }
+ } else {
+ syncData.target.removeItemFromChangeLog(changes[i].itemId);
+ }
+ break;
+
+ case "modified_by_user":
+ item = await syncData.target.getItem(changes[i].itemId);
+ if (item) {
+ //filter out bad object types for this folder
+ if (syncData.type == eas.sync.getEasItemType(item)) {
+ wbxml.otag("Change");
+ wbxml.atag("ServerId", changes[i].itemId);
+ wbxml.otag("ApplicationData");
+ wbxml.switchpage(syncData.type);
+ wbxml.append(await eas.sync.getWbxmlFromThunderbirdItem(item, syncData));
+ wbxml.switchpage("AirSync");
+ wbxml.ctag();
+ wbxml.ctag();
+ changedItems.push(changes[i].itemId);
+ sendItems.push({type: changes[i].status, id: changes[i].itemId});
+ c++;
+ } else {
+ eas.sync.updateFailedItems(syncData, "forbidden" + eas.sync.getEasItemType(item) +"ItemIn" + syncData.type + "Folder", item.primaryKey, item.toString());
+ e++;
+ }
+ } else {
+ syncData.target.removeItemFromChangeLog(changes[i].itemId);
+ }
+ break;
+
+ case "deleted_by_user":
+ wbxml.otag("Delete");
+ wbxml.atag("ServerId", changes[i].itemId);
+ wbxml.ctag();
+ changedItems.push(changes[i].itemId);
+ sendItems.push({type: changes[i].status, id: changes[i].itemId});
+ c++;
+ break;
+ }
+ }
+
+ wbxml.ctag(); //Commands
+ wbxml.ctag(); //Collection
+ wbxml.ctag(); //Collections
+ wbxml.ctag(); //Sync
+
+
+ if (c > 0) { //if there was at least one actual local change, send request
+ sendChanges = true;
+ //SEND REQUEST & VALIDATE RESPONSE
+ syncData.setSyncState("send.request.localchanges");
+ let response = await eas.network.sendRequest(wbxml.getBytes(), "Sync", syncData);
+
+ syncData.setSyncState("eval.response.localchanges");
+
+ //get data from wbxml response
+ let wbxmlData = eas.network.getDataFromResponse(response);
+
+ //check status and manually handle error states which support softfails
+ let errorcause = eas.network.checkStatus(syncData, wbxmlData, "Sync.Collections.Collection.Status", "", true);
+ switch (errorcause) {
+ case "":
+ break;
+
+ case "Sync.4": //Malformed request
+ case "Sync.6": //Invalid item
+ //some servers send a global error - to catch this, we reduce the number of items we send to the server
+ if (sendItems.length == 1) {
+ //the request contained only one item, so we know which one failed
+ if (sendItems[0].type == "deleted_by_user") {
+ //we failed to delete an item, discard and place message in log
+ syncData.target.removeItemFromChangeLog(sendItems[0].id);
+ TbSync.eventlog.add("warning", syncData.eventLogInfo, "ErrorOnDelete::"+sendItems[0].id);
+ } else {
+ let foundItem = await syncData.target.getItem(sendItems[0].id);
+ if (foundItem) {
+ eas.sync.updateFailedItems(syncData, errorcause, foundItem.primaryKey, foundItem.toString());
+ } else {
+ //should not happen
+ syncData.target.removeItemFromChangeLog(sendItems[0].id);
+ }
+ }
+ syncData.progressData.inc();
+ //restore numberOfItemsToSend
+ numberOfItemsToSend = maxnumbertosend;
+ } else if (sendItems.length > 1) {
+ //reduce further
+ numberOfItemsToSend = Math.min(1, Math.round(sendItems.length / 5));
+ } else {
+ //sendItems.length == 0 ??? recheck but this time let it handle all cases
+ eas.network.checkStatus(syncData, wbxmlData, "Sync.Collections.Collection.Status");
+ }
+ break;
+
+ default:
+ //recheck but this time let it handle all cases
+ eas.network.checkStatus(syncData, wbxmlData, "Sync.Collections.Collection.Status");
+ }
+
+ await TbSync.tools.sleep(10, true);
+
+ if (errorcause == "") {
+ //PROCESS RESPONSE
+ await eas.sync.processResponses(wbxmlData, syncData, addedItems, changedItems);
+
+ //PROCESS COMMANDS
+ await eas.sync.processCommands(wbxmlData, syncData);
+
+ //remove all items from changelog that did not fail
+ for (let a=0; a < changedItems.length; a++) {
+ syncData.target.removeItemFromChangeLog(changedItems[a]);
+ syncData.progressData.inc();
+ }
+
+ //update synckey
+ eas.network.updateSynckey(syncData, wbxmlData);
+ }
+
+ } else if (e==0) { //if there was no local change and also no error (which will not happen twice) finish
+
+ done = true;
+
+ }
+
+ } while (!done);
+
+ //was there an error?
+ if (syncData.failedItems.length > 0) {
+ throw eas.sync.finish("warning", "ServerRejectedSomeItems::" + syncData.failedItems.length);
+ }
+ return sendChanges;
+ },
+
+
+
+
+ revertLocalChanges: async function (syncData) {
+ let maxnumbertosend = eas.prefs.getIntPref("maxitems");
+ syncData.progressData.reset(0, syncData.target.getItemsFromChangeLog().length);
+ if (syncData.progressData.todo == 0) {
+ return;
+ }
+
+ let viaItemOperations = (syncData.accountData.getAccountProperty("allowedEasCommands").split(",").includes("ItemOperations"));
+
+ //get changed items from ChangeLog
+ do {
+ syncData.setSyncState("prepare.request.revertlocalchanges");
+ let changes = syncData.target.getItemsFromChangeLog(maxnumbertosend);
+ let c=0;
+ syncData.request = "";
+ syncData.response = "";
+
+ // BUILD WBXML
+ let wbxml = eas.wbxmltools.createWBXML();
+ if (viaItemOperations) {
+ wbxml.switchpage("ItemOperations");
+ wbxml.otag("ItemOperations");
+ } else {
+ wbxml.otag("Sync");
+ wbxml.otag("Collections");
+ wbxml.otag("Collection");
+ if (syncData.accountData.getAccountProperty("asversion") == "2.5") wbxml.atag("Class", syncData.type);
+ wbxml.atag("SyncKey", syncData.synckey);
+ wbxml.atag("CollectionId", syncData.currentFolderData.getFolderProperty("serverID"));
+ wbxml.otag("Commands");
+ }
+
+ for (let i=0; i<changes.length; i++) {
+ let item = null;
+ let ServerId = changes[i].itemId;
+ let foundItem = await syncData.target.getItem(ServerId);
+
+ switch (changes[i].status) {
+ case "added_by_user": //remove
+ if (foundItem) {
+ await syncData.target.deleteItem(foundItem);
+ }
+ break;
+
+ case "modified_by_user":
+ if (foundItem) { //delete item so it can be replaced with a fresh copy, the changelog entry will be changed from modified to deleted
+ await syncData.target.deleteItem(foundItem);
+ }
+ case "deleted_by_user":
+ if (viaItemOperations) {
+ wbxml.otag("Fetch");
+ wbxml.atag("Store", "Mailbox");
+ wbxml.switchpage("AirSync");
+ wbxml.atag("CollectionId", syncData.currentFolderData.getFolderProperty("serverID"));
+ wbxml.atag("ServerId", ServerId);
+ wbxml.switchpage("ItemOperations");
+ wbxml.otag("Options");
+ wbxml.switchpage("AirSyncBase");
+ wbxml.otag("BodyPreference");
+ wbxml.atag("Type","1");
+ wbxml.ctag();
+ wbxml.switchpage("ItemOperations");
+ wbxml.ctag();
+ wbxml.ctag();
+ } else {
+ wbxml.otag("Fetch");
+ wbxml.atag("ServerId", ServerId);
+ wbxml.ctag();
+ }
+ c++;
+ break;
+ }
+ }
+
+ if (viaItemOperations) {
+ wbxml.ctag(); //ItemOperations
+ } else {
+ wbxml.ctag(); //Commands
+ wbxml.ctag(); //Collection
+ wbxml.ctag(); //Collections
+ wbxml.ctag(); //Sync
+ }
+
+ if (c > 0) { //if there was at least one actual local change, send request
+ let error = false;
+ let wbxmlData = "";
+
+ //SEND REQUEST & VALIDATE RESPONSE
+ try {
+ syncData.setSyncState("send.request.revertlocalchanges");
+ let response = await eas.network.sendRequest(wbxml.getBytes(), (viaItemOperations) ? "ItemOperations" : "Sync", syncData);
+
+ syncData.setSyncState("eval.response.revertlocalchanges");
+
+ //get data from wbxml response
+ wbxmlData = eas.network.getDataFromResponse(response);
+ } catch (e) {
+ //we do not handle errors, IF there was an error, wbxmlData is empty and will trigger the fallback
+ }
+
+ let fetchPath = (viaItemOperations) ? "ItemOperations.Response.Fetch" : "Sync.Collections.Collection.Responses.Fetch";
+ if (eas.xmltools.hasWbxmlDataField(wbxmlData, fetchPath)) {
+
+ //looking for additions
+ let add = eas.xmltools.nodeAsArray(eas.xmltools.getWbxmlDataField(wbxmlData, fetchPath));
+ for (let count = 0; count < add.length; count++) {
+ await TbSync.tools.sleep(10, true);
+
+ let ServerId = add[count].ServerId;
+ let data = (viaItemOperations) ? add[count].Properties : add[count].ApplicationData;
+
+ if (data && ServerId) {
+ let foundItem = await syncData.target.getItem(ServerId);
+ if (!foundItem) { //do NOT add, if an item with that ServerId was found
+ let newItem = eas.sync.createItem(syncData);
+ try {
+ eas.sync[syncData.type].setThunderbirdItemFromWbxml(newItem, data, ServerId, syncData);
+ await syncData.target.addItem(newItem);
+ } catch (e) {
+ eas.xmltools.printXmlData(add[count], true); //include application data in log
+ TbSync.eventlog.add("warning", syncData.eventLogInfo, "BadItemSkipped::JavaScriptError", newItem.toString());
+ throw e; // unable to add item to Thunderbird - fatal error
+ }
+ } else {
+ //should not happen, since we deleted that item beforehand
+ syncData.target.removeItemFromChangeLog(ServerId);
+ }
+ syncData.progressData.inc();
+ } else {
+ error = true;
+ break;
+ }
+ }
+ } else {
+ error = true;
+ }
+
+ if (error) {
+ //if ItemOperations.Fetch fails, fall back to Sync.Fetch, if that fails, fall back to resync
+ if (viaItemOperations) {
+ viaItemOperations = false;
+ TbSync.eventlog.add("info", syncData.eventLogInfo, "Server returned error during ItemOperations.Fetch, falling back to Sync.Fetch.");
+ } else {
+ await eas.sync.revertLocalChangesViaResync(syncData);
+ return;
+ }
+ }
+
+ } else { //if there was no more local change we need to revert, return
+
+ return;
+
+ }
+
+ } while (true);
+
+ },
+
+ revertLocalChangesViaResync: async function (syncData) {
+ TbSync.eventlog.add("info", syncData.eventLogInfo, "Server does not support ItemOperations.Fetch and/or Sync.Fetch, must revert via resync.");
+ let changes = syncData.target.getItemsFromChangeLog();
+
+ syncData.progressData.reset(0, changes.length);
+ syncData.setSyncState("prepare.request.revertlocalchanges");
+
+ //remove all changes, so we can get them fresh from the server
+ for (let i=0; i<changes.length; i++) {
+ let item = null;
+ let ServerId = changes[i].itemId;
+ syncData.target.removeItemFromChangeLog(ServerId);
+ let foundItem = await syncData.target.getItem(ServerId);
+ if (foundItem) { //delete item with that ServerId
+ await syncData.target.deleteItem(foundItem);
+ }
+ syncData.progressData.inc();
+ }
+
+ //This will resync all missing items fresh from the server
+ TbSync.eventlog.add("info", syncData.eventLogInfo, "RevertViaFolderResync");
+ eas.sync.resetFolderSyncInfo(syncData.currentFolderData);
+ throw eas.sync.finish("resyncFolder", "RevertViaFolderResync");
+ },
+
+
+
+
+ // ---------------------------------------------------------------------------
+ // SUB FUNCTIONS CALLED BY MAIN FUNCTION
+ // ---------------------------------------------------------------------------
+
+ processCommands: async function (wbxmlData, syncData) {
+ //any commands for us to work on? If we reach this point, Sync.Collections.Collection is valid,
+ //no need to use the save getWbxmlDataField function
+ if (wbxmlData.Sync.Collections.Collection.Commands) {
+
+ //looking for additions
+ let add = eas.xmltools.nodeAsArray(wbxmlData.Sync.Collections.Collection.Commands.Add);
+ for (let count = 0; count < add.length; count++) {
+ await TbSync.tools.sleep(10, true);
+
+ let ServerId = add[count].ServerId;
+ let data = add[count].ApplicationData;
+
+ let foundItem = await syncData.target.getItem(ServerId);
+ if (!foundItem) {
+ //do NOT add, if an item with that ServerId was found
+ let newItem = eas.sync.createItem(syncData);
+ try {
+ eas.sync[syncData.type].setThunderbirdItemFromWbxml(newItem, data, ServerId, syncData);
+ await syncData.target.addItem(newItem);
+ } catch (e) {
+ eas.xmltools.printXmlData(add[count], true); //include application data in log
+ TbSync.eventlog.add("warning", syncData.eventLogInfo, "BadItemSkipped::JavaScriptError", newItem.toString());
+ throw e; // unable to add item to Thunderbird - fatal error
+ }
+ } else {
+ TbSync.eventlog.add("info", syncData.eventLogInfo, "Add request, but element exists already, skipped.", ServerId);
+ }
+ syncData.progressData.inc();
+ }
+
+ //looking for changes
+ let upd = eas.xmltools.nodeAsArray(wbxmlData.Sync.Collections.Collection.Commands.Change);
+ //inject custom change object for debug
+ //upd = JSON.parse('[{"ServerId":"2tjoanTeS0CJ3QTsq5vdNQAAAAABDdrY6Gp03ktAid0E7Kub3TUAAAoZy4A1","ApplicationData":{"DtStamp":"20171109T142149Z"}}]');
+ for (let count = 0; count < upd.length; count++) {
+ await TbSync.tools.sleep(10, true);
+
+ let ServerId = upd[count].ServerId;
+ let data = upd[count].ApplicationData;
+
+ syncData.progressData.inc();
+ let foundItem = await syncData.target.getItem(ServerId);
+ if (foundItem) { //only update, if an item with that ServerId was found
+
+ let keys = Object.keys(data);
+ //replace by smart merge
+ if (keys.length == 1 && keys[0] == "DtStamp") TbSync.dump("DtStampOnly", keys); //ignore DtStamp updates (fix with smart merge)
+ else {
+
+ if (foundItem.changelogStatus !== null) {
+ TbSync.eventlog.add("info", syncData.eventLogInfo, "Change request from server, but also local modifications, server wins!", ServerId);
+ foundItem.changelogStatus = null;
+ }
+
+ let newItem = foundItem.clone();
+ try {
+ eas.sync[syncData.type].setThunderbirdItemFromWbxml(newItem, data, ServerId, syncData);
+ await syncData.target.modifyItem(newItem, foundItem);
+ } catch (e) {
+ TbSync.eventlog.add("warning", syncData.eventLogInfo, "BadItemSkipped::JavaScriptError", newItem.toString());
+ eas.xmltools.printXmlData(upd[count], true); //include application data in log
+ throw e; // unable to mod item to Thunderbird - fatal error
+ }
+ }
+
+ }
+ }
+
+ //looking for deletes
+ let del = eas.xmltools.nodeAsArray(wbxmlData.Sync.Collections.Collection.Commands.Delete).concat(eas.xmltools.nodeAsArray(wbxmlData.Sync.Collections.Collection.Commands.SoftDelete));
+ for (let count = 0; count < del.length; count++) {
+ await TbSync.tools.sleep(10, true);
+
+ let ServerId = del[count].ServerId;
+
+ let foundItem = await syncData.target.getItem(ServerId);
+ if (foundItem) { //delete item with that ServerId
+ await syncData.target.deleteItem(foundItem);
+ }
+ syncData.progressData.inc();
+ }
+
+ }
+ },
+
+
+ updateFailedItems: function (syncData, cause, id, data) {
+ //something is wrong with this item, move it to the end of changelog and go on
+ if (!syncData.failedItems.includes(id)) {
+ //the extra parameter true will re-add the item to the end of the changelog
+ syncData.target.removeItemFromChangeLog(id, true);
+ syncData.failedItems.push(id);
+ TbSync.eventlog.add("info", syncData.eventLogInfo, "BadItemSkipped::" + TbSync.getString("status." + cause ,"eas"), "\n\nRequest:\n" + syncData.request + "\n\nResponse:\n" + syncData.response + "\n\nElement:\n" + data);
+ }
+ },
+
+
+ processResponses: async function (wbxmlData, syncData, addedItems, changedItems) {
+ //any responses for us to work on? If we reach this point, Sync.Collections.Collection is valid,
+ //no need to use the save getWbxmlDataField function
+ if (wbxmlData.Sync.Collections.Collection.Responses) {
+
+ //looking for additions (Add node contains, status, old ClientId and new ServerId)
+ let add = eas.xmltools.nodeAsArray(wbxmlData.Sync.Collections.Collection.Responses.Add);
+ for (let count = 0; count < add.length; count++) {
+ await TbSync.tools.sleep(10, true);
+
+ //get the true Thunderbird UID of this added item (we created a temp clientId during add)
+ add[count].ClientId = addedItems[add[count].ClientId];
+
+ //look for an item identfied by ClientId and update its id to the new id received from the server
+ let foundItem = await syncData.target.getItem(add[count].ClientId);
+ if (foundItem) {
+
+ //Check status, stop sync if bad, allow soft fail
+ let errorcause = eas.network.checkStatus(syncData, add[count],"Status","Sync.Collections.Collection.Responses.Add["+count+"].Status", true);
+ if (errorcause !== "") {
+ //something is wrong with this item, move it to the end of changelog and go on
+ eas.sync.updateFailedItems(syncData, errorcause, foundItem.primaryKey, foundItem.toString());
+ } else {
+ let newItem = foundItem.clone();
+ newItem.primaryKey = add[count].ServerId;
+ syncData.target.removeItemFromChangeLog(add[count].ClientId);
+ await syncData.target.modifyItem(newItem, foundItem);
+ syncData.progressData.inc();
+ }
+
+ }
+ }
+
+ //looking for modifications
+ let upd = eas.xmltools.nodeAsArray(wbxmlData.Sync.Collections.Collection.Responses.Change);
+ for (let count = 0; count < upd.length; count++) {
+ let foundItem = await syncData.target.getItem(upd[count].ServerId);
+ if (foundItem) {
+
+ //Check status, stop sync if bad, allow soft fail
+ let errorcause = eas.network.checkStatus(syncData, upd[count],"Status","Sync.Collections.Collection.Responses.Change["+count+"].Status", true);
+ if (errorcause !== "") {
+ //something is wrong with this item, move it to the end of changelog and go on
+ eas.sync.updateFailedItems(syncData, errorcause, foundItem.primaryKey, foundItem.toString());
+ //also remove from changedItems
+ let p = changedItems.indexOf(upd[count].ServerId);
+ if (p>-1) changedItems.splice(p,1);
+ }
+
+ }
+ }
+
+ //looking for deletions
+ let del = eas.xmltools.nodeAsArray(wbxmlData.Sync.Collections.Collection.Responses.Delete);
+ for (let count = 0; count < del.length; count++) {
+ //What can we do about failed deletes? SyncLog
+ eas.network.checkStatus(syncData, del[count],"Status","Sync.Collections.Collection.Responses.Delete["+count+"].Status", true);
+ }
+
+ }
+ },
+
+
+
+
+
+
+
+
+
+
+ // ---------------------------------------------------------------------------
+ // HELPER FUNCTIONS AND DEFINITIONS
+ // ---------------------------------------------------------------------------
+
+ MAP_EAS2TB : {
+ //EAS Importance: 0 = LOW | 1 = NORMAL | 2 = HIGH
+ Importance : { "0":"9", "1":"5", "2":"1"}, //to PRIORITY
+ //EAS Sensitivity : 0 = Normal | 1 = Personal | 2 = Private | 3 = Confidential
+ Sensitivity : { "0":"PUBLIC", "1":"PRIVATE", "2":"PRIVATE", "3":"CONFIDENTIAL"}, //to CLASS
+ //EAS BusyStatus: 0 = Free | 1 = Tentative | 2 = Busy | 3 = Work | 4 = Elsewhere
+ BusyStatus : {"0":"TRANSPARENT", "1":"unset", "2":"OPAQUE", "3":"OPAQUE", "4":"OPAQUE"}, //to TRANSP
+ //EAS AttendeeStatus: 0 =Response unknown (but needed) | 2 = Tentative | 3 = Accept | 4 = Decline | 5 = Not responded (and not needed) || 1 = Organizer in ResponseType
+ ATTENDEESTATUS : {"0": "NEEDS-ACTION", "1":"Orga", "2":"TENTATIVE", "3":"ACCEPTED", "4":"DECLINED", "5":"ACCEPTED"},
+ },
+
+ MAP_TB2EAS : {
+ //TB PRIORITY: 9 = LOW | 5 = NORMAL | 1 = HIGH
+ PRIORITY : { "9":"0", "5":"1", "1":"2","unset":"1"}, //to Importance
+ //TB CLASS: PUBLIC, PRIVATE, CONFIDENTIAL)
+ CLASS : { "PUBLIC":"0", "PRIVATE":"2", "CONFIDENTIAL":"3", "unset":"0"}, //to Sensitivity
+ //TB TRANSP : free = TRANSPARENT, busy = OPAQUE)
+ TRANSP : {"TRANSPARENT":"0", "unset":"1", "OPAQUE":"2"}, // to BusyStatus
+ //TB STATUS: NEEDS-ACTION, ACCEPTED, DECLINED, TENTATIVE, (DELEGATED, COMPLETED, IN-PROCESS - for todo)
+ ATTENDEESTATUS : {"NEEDS-ACTION":"0", "ACCEPTED":"3", "DECLINED":"4", "TENTATIVE":"2", "DELEGATED":"5","COMPLETED":"5", "IN-PROCESS":"5"},
+ },
+
+ mapEasPropertyToThunderbird : function (easProp, tbProp, data, item) {
+ if (data[easProp]) {
+ //store original EAS value
+ let easPropValue = eas.xmltools.checkString(data[easProp]);
+ item.setProperty("X-EAS-" + easProp, easPropValue);
+ //map EAS value to TB value (use setCalItemProperty if there is one option which can unset/delete the property)
+ eas.tools.setCalItemProperty(item, tbProp, eas.sync.MAP_EAS2TB[easProp][easPropValue]);
+ }
+ },
+
+ mapThunderbirdPropertyToEas: function (tbProp, easProp, item) {
+ if (item.hasProperty("X-EAS-" + easProp) && eas.tools.getCalItemProperty(item, tbProp) == eas.sync.MAP_EAS2TB[easProp][item.getProperty("X-EAS-" + easProp)]) {
+ //we can use our stored EAS value, because it still maps to the current TB value
+ return item.getProperty("X-EAS-" + easProp);
+ } else {
+ return eas.sync.MAP_TB2EAS[tbProp][eas.tools.getCalItemProperty(item, tbProp)];
+ }
+ },
+
+ getEasItemType(aItem) {
+ if (aItem instanceof TbSync.addressbook.AbItem) {
+ return "Contacts";
+ } else if (aItem instanceof TbSync.lightning.TbItem) {
+ return aItem.isTodo ? "Tasks" : "Calendar";
+ } else {
+ throw "Unknown aItem.";
+ }
+ },
+
+ createItem(syncData) {
+ switch (syncData.type) {
+ case "Contacts":
+ return syncData.target.createNewCard();
+ break;
+
+ case "Tasks":
+ return syncData.target.createNewTodo();
+ break;
+
+ case "Calendar":
+ return syncData.target.createNewEvent();
+ break;
+
+ default:
+ throw "Unknown item type <" + syncData.type + ">";
+ }
+ },
+
+ async getWbxmlFromThunderbirdItem(item, syncData, isException = false) {
+ try {
+ let wbxml = await eas.sync[syncData.type].getWbxmlFromThunderbirdItem(item, syncData, isException);
+ return wbxml;
+ } catch (e) {
+ TbSync.eventlog.add("warning", syncData.eventLogInfo, "BadItemSkipped::JavaScriptError", item.toString());
+ throw e; // unable to read item from Thunderbird - fatal error
+ }
+ },
+
+
+
+
+
+
+
+ // ---------------------------------------------------------------------------
+ // LIGHTNING HELPER FUNCTIONS AND DEFINITIONS
+ // These functions are needed only by tasks and events, so they
+ // are placed here, even though they are not type independent,
+ // but I did not want to add another "lightning" sub layer.
+ //
+ // The item in these functions is a native lightning item.
+ // ---------------------------------------------------------------------------
+
+ setItemSubject: function (item, syncData, data) {
+ if (data.Subject) item.title = eas.xmltools.checkString(data.Subject);
+ },
+
+ setItemLocation: function (item, syncData, data) {
+ if (data.Location) item.setProperty("location", eas.xmltools.checkString(data.Location));
+ },
+
+
+ setItemCategories: function (item, syncData, data) {
+ if (data.Categories && data.Categories.Category) {
+ let cats = [];
+ if (Array.isArray(data.Categories.Category)) cats = data.Categories.Category;
+ else cats.push(data.Categories.Category);
+ item.setCategories(cats);
+ }
+ },
+
+ getItemCategories: function (item, syncData) {
+ let asversion = syncData.accountData.getAccountProperty("asversion");
+ let wbxml = eas.wbxmltools.createWBXML("", syncData.type); //init wbxml with "" and not with precodes, also activate type codePage (Calendar, Tasks, Contacts etc)
+
+ //to properly "blank" categories, we need to always include the container
+ let categories = item.getCategories({});
+ if (categories.length > 0) {
+ wbxml.otag("Categories");
+ for (let i=0; i<categories.length; i++) wbxml.atag("Category", categories[i]);
+ wbxml.ctag();
+ } else {
+ wbxml.atag("Categories");
+ }
+ return wbxml.getBytes();
+ },
+
+
+ setItemBody: function (item, syncData, data) {
+ let asversion = syncData.accountData.getAccountProperty("asversion");
+ if (asversion == "2.5") {
+ if (data.Body) item.setProperty("description", eas.xmltools.checkString(data.Body));
+ } else {
+ if (data.Body && /* data.Body.EstimatedDataSize > 0 && */ data.Body.Data) item.setProperty("description", eas.xmltools.checkString(data.Body.Data)); //EstimatedDataSize is optional
+ }
+ },
+
+ getItemBody: function (item, syncData) {
+ let asversion = syncData.accountData.getAccountProperty("asversion");
+ let wbxml = eas.wbxmltools.createWBXML("", syncData.type); //init wbxml with "" and not with precodes, also activate type codePage (Calendar, Tasks, Contacts etc)
+
+ let description = (item.hasProperty("description")) ? item.getProperty("description") : "";
+ 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();
+ //does not work with horde at the moment, does not work with task, does not work with exceptions
+ //if (syncData.accountData.getAccountProperty("horde") == "0") wbxml.atag("NativeBodyType", "1");
+
+ //return to code page of this type
+ wbxml.switchpage(syncData.type);
+ }
+ return wbxml.getBytes();
+ },
+
+ //item is a native lightning item
+ setItemRecurrence: function (item, syncData, data, timezone) {
+ if (data.Recurrence) {
+ item.recurrenceInfo = new CalRecurrenceInfo();
+ item.recurrenceInfo.item = item;
+ let recRule = TbSync.lightning.cal.createRecurrenceRule();
+ switch (data.Recurrence.Type) {
+ case "0":
+ recRule.type = "DAILY";
+ break;
+ case "1":
+ recRule.type = "WEEKLY";
+ break;
+ case "2":
+ case "3":
+ recRule.type = "MONTHLY";
+ break;
+ case "5":
+ case "6":
+ recRule.type = "YEARLY";
+ break;
+ }
+
+ if (data.Recurrence.CalendarType) {
+ // TODO
+ }
+ if (data.Recurrence.DayOfMonth) {
+ recRule.setComponent("BYMONTHDAY", [data.Recurrence.DayOfMonth]);
+ }
+ if (data.Recurrence.DayOfWeek) {
+ let DOW = data.Recurrence.DayOfWeek;
+ if (DOW == 127 && (recRule.type == "MONTHLY" || recRule.type == "YEARLY")) {
+ recRule.setComponent("BYMONTHDAY", [-1]);
+ }
+ else {
+ let days = [];
+ for (let i = 0; i < 7; ++i) {
+ if (DOW & 1 << i) days.push(i + 1);
+ }
+ if (data.Recurrence.WeekOfMonth) {
+ for (let i = 0; i < days.length; ++i) {
+ if (data.Recurrence.WeekOfMonth == 5) {
+ days[i] = -1 * (days[i] + 8);
+ }
+ else {
+ days[i] += 8 * (data.Recurrence.WeekOfMonth - 0);
+ }
+ }
+ }
+ recRule.setComponent("BYDAY", days);
+ }
+ }
+ if (data.Recurrence.FirstDayOfWeek) {
+ //recRule.setComponent("WKST", [data.Recurrence.FirstDayOfWeek]); // WKST is not a valid component
+ //recRule.weekStart = data.Recurrence.FirstDayOfWeek; // - (NS_ERROR_NOT_IMPLEMENTED) [calIRecurrenceRule.weekStart]
+ TbSync.eventlog.add("info", syncData.eventLogInfo, "FirstDayOfWeek tag ignored (not supported).", item.icalString);
+ }
+
+ if (data.Recurrence.Interval) {
+ recRule.interval = data.Recurrence.Interval;
+ }
+ if (data.Recurrence.IsLeapMonth) {
+ // TODO
+ }
+ if (data.Recurrence.MonthOfYear) {
+ recRule.setComponent("BYMONTH", [data.Recurrence.MonthOfYear]);
+ }
+ if (data.Recurrence.Occurrences) {
+ recRule.count = data.Recurrence.Occurrences;
+ }
+ if (data.Recurrence.Until) {
+ //time string could be in compact/basic or extended form of ISO 8601,
+ //cal.createDateTime only supports compact/basic, our own method takes both styles
+ recRule.untilDate = eas.tools.createDateTime(data.Recurrence.Until);
+ }
+ if (data.Recurrence.Start) {
+ TbSync.eventlog.add("info", syncData.eventLogInfo, "Start tag in recurring task is ignored, recurrence will start with first entry.", item.icalString);
+ }
+
+ item.recurrenceInfo.insertRecurrenceItemAt(recRule, 0);
+
+ if (data.Exceptions && syncData.type == "Calendar") { // only events, tasks cannot have exceptions
+ // Exception could be an object or an array of objects
+ let exceptions = [].concat(data.Exceptions.Exception);
+ for (let exception of exceptions) {
+ //exception.ExceptionStartTime is in UTC, but the Recurrence Object is in local timezone
+ let dateTime = TbSync.lightning.cal.createDateTime(exception.ExceptionStartTime).getInTimezone(timezone);
+ if (data.AllDayEvent == "1") {
+ dateTime.isDate = true;
+ // Pass to replacement event unless overriden
+ if (!exception.AllDayEvent) {
+ exception.AllDayEvent = "1";
+ }
+ }
+ if (exception.Deleted == "1") {
+ item.recurrenceInfo.removeOccurrenceAt(dateTime);
+ }
+ else {
+ let replacement = item.recurrenceInfo.getOccurrenceFor(dateTime);
+ // replacement is a native lightning item, so we can access its id via .id
+ eas.sync[syncData.type].setThunderbirdItemFromWbxml(replacement, exception, replacement.id, syncData, "recurrence");
+ // Reminders should carry over from parent, but setThunderbirdItemFromWbxml clears all alarms
+ if (!exception.Reminder && item.getAlarms({}).length) {
+ replacement.addAlarm(item.getAlarms({})[0]);
+ }
+ // Removing a reminder requires EAS 16.0
+ item.recurrenceInfo.modifyException(replacement, true);
+ }
+ }
+ }
+ }
+ },
+
+ getItemRecurrence: async function (item, syncData, localStartDate = null) {
+ let asversion = syncData.accountData.getAccountProperty("asversion");
+ let wbxml = eas.wbxmltools.createWBXML("", syncData.type); //init wbxml with "" and not with precodes, also activate type codePage (Calendar, Tasks etc)
+
+ if (item.recurrenceInfo && (syncData.type == "Calendar" || syncData.type == "Tasks")) {
+ let deleted = [];
+ let hasRecurrence = false;
+ let startDate = (syncData.type == "Calendar") ? item.startDate : item.entryDate;
+
+ for (let recRule of item.recurrenceInfo.getRecurrenceItems({})) {
+ if (recRule.date) {
+ if (recRule.isNegative) {
+ // EXDATE
+ deleted.push(recRule);
+ }
+ else {
+ // RDATE
+ TbSync.eventlog.add("info", syncData.eventLogInfo, "Ignoring RDATE rule (not supported)", recRule.icalString);
+ }
+ continue;
+ }
+ if (recRule.isNegative) {
+ // EXRULE
+ TbSync.eventlog.add("info", syncData.eventLogInfo, "Ignoring EXRULE rule (not supported)", recRule.icalString);
+ continue;
+ }
+
+ // RRULE
+ wbxml.otag("Recurrence");
+ hasRecurrence = true;
+
+ let type = 0;
+ let monthDays = recRule.getComponent("BYMONTHDAY", {});
+ let weekDays = recRule.getComponent("BYDAY", {});
+ let months = recRule.getComponent("BYMONTH", {});
+ let weeks = [];
+
+ // Unpack 1MO style days
+ for (let i = 0; i < weekDays.length; ++i) {
+ if (weekDays[i] > 8) {
+ weeks[i] = Math.floor(weekDays[i] / 8);
+ weekDays[i] = weekDays[i] % 8;
+ }
+ else if (weekDays[i] < -8) {
+ // EAS only supports last week as a special value, treat
+ // all as last week or assume every month has 5 weeks?
+ // Change to last week
+ //weeks[i] = 5;
+ // Assumes 5 weeks per month for week <= -2
+ weeks[i] = 6 - Math.floor(-weekDays[i] / 8);
+ weekDays[i] = -weekDays[i] % 8;
+ }
+ }
+ if (monthDays[0] && monthDays[0] == -1) {
+ weeks = [5];
+ weekDays = [1, 2, 3, 4, 5, 6, 7]; // 127
+ monthDays[0] = null;
+ }
+ // Type
+ if (recRule.type == "WEEKLY") {
+ type = 1;
+ if (!weekDays.length) {
+ weekDays = [startDate.weekday + 1];
+ }
+ }
+ else if (recRule.type == "MONTHLY" && weeks.length) {
+ type = 3;
+ }
+ else if (recRule.type == "MONTHLY") {
+ type = 2;
+ if (!monthDays.length) {
+ monthDays = [startDate.day];
+ }
+ }
+ else if (recRule.type == "YEARLY" && weeks.length) {
+ type = 6;
+ }
+ else if (recRule.type == "YEARLY") {
+ type = 5;
+ if (!monthDays.length) {
+ monthDays = [startDate.day];
+ }
+ if (!months.length) {
+ months = [startDate.month + 1];
+ }
+ }
+ wbxml.atag("Type", type.toString());
+
+ //Tasks need a Start tag, but we cannot allow a start date different from the start of the main item (thunderbird does not support that)
+ if (localStartDate) wbxml.atag("Start", localStartDate);
+
+ // TODO: CalendarType: 14.0 and up
+ // DayOfMonth
+ if (monthDays[0]) {
+ // TODO: Multiple days of month - multiple Recurrence tags?
+ wbxml.atag("DayOfMonth", monthDays[0].toString());
+ }
+ // DayOfWeek
+ if (weekDays.length) {
+ let bitfield = 0;
+ for (let day of weekDays) {
+ bitfield |= 1 << (day - 1);
+ }
+ wbxml.atag("DayOfWeek", bitfield.toString());
+ }
+ // FirstDayOfWeek: 14.1 and up
+ //wbxml.atag("FirstDayOfWeek", recRule.weekStart); - (NS_ERROR_NOT_IMPLEMENTED) [calIRecurrenceRule.weekStart]
+ // Interval
+ wbxml.atag("Interval", recRule.interval.toString());
+ // TODO: IsLeapMonth: 14.0 and up
+ // MonthOfYear
+ if (months.length) {
+ wbxml.atag("MonthOfYear", months[0].toString());
+ }
+ // Occurrences
+ if (recRule.isByCount) {
+ wbxml.atag("Occurrences", recRule.count.toString());
+ }
+ // Until
+ else if (recRule.untilDate != null) {
+ //Events need the Until data in compact form, Tasks in the basic form
+ wbxml.atag("Until", eas.tools.getIsoUtcString(recRule.untilDate, (syncData.type == "Tasks")));
+ }
+ // WeekOfMonth
+ if (weeks.length) {
+ wbxml.atag("WeekOfMonth", weeks[0].toString());
+ }
+ wbxml.ctag();
+ }
+
+ if (syncData.type == "Calendar" && hasRecurrence) { //Exceptions only allowed in Calendar and only if a valid Recurrence was added
+ let modifiedIds = item.recurrenceInfo.getExceptionIds({});
+ if (deleted.length || modifiedIds.length) {
+ wbxml.otag("Exceptions");
+ for (let exception of deleted) {
+ wbxml.otag("Exception");
+ wbxml.atag("ExceptionStartTime", eas.tools.getIsoUtcString(exception.date));
+ wbxml.atag("Deleted", "1");
+ //Docs say it is allowed, but if present, it does not work
+ //if (asversion == "2.5") {
+ // wbxml.atag("UID", item.id); //item.id is not valid, use UID or primaryKey
+ //}
+ wbxml.ctag();
+ }
+ for (let exceptionId of modifiedIds) {
+ let replacement = item.recurrenceInfo.getExceptionFor(exceptionId);
+ wbxml.otag("Exception");
+ wbxml.atag("ExceptionStartTime", eas.tools.getIsoUtcString(exceptionId));
+ wbxml.append(await eas.sync.getWbxmlFromThunderbirdItem(replacement, syncData, true));
+ wbxml.ctag();
+ }
+ wbxml.ctag();
+ }
+ }
+ }
+
+ return wbxml.getBytes();
+ }
+
+}
diff --git a/content/includes/tasksync.js b/content/includes/tasksync.js
new file mode 100644
index 0000000..e703152
--- /dev/null
+++ b/content/includes/tasksync.js
@@ -0,0 +1,211 @@
+/*
+ * 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, {
+ CalAlarm: "resource:///modules/CalAlarm.jsm",
+ CalAttachment: "resource:///modules/CalAttachment.jsm",
+ CalAttendee: "resource:///modules/CalAttendee.jsm",
+ CalEvent: "resource:///modules/CalEvent.jsm",
+ CalTodo: "resource:///modules/CalTodo.jsm",
+});
+
+var Tasks = {
+
+ // --------------------------------------------------------------------------- //
+ // Read WBXML and set Thunderbird item
+ // --------------------------------------------------------------------------- //
+ setThunderbirdItemFromWbxml: function (tbItem, data, id, syncdata, mode = "standard") {
+
+ let item = tbItem instanceof TbSync.lightning.TbItem ? tbItem.nativeItem : tbItem;
+
+ let asversion = syncdata.accountData.getAccountProperty("asversion");
+ item.id = id;
+ eas.sync.setItemSubject(item, syncdata, data);
+ if (TbSync.prefs.getIntPref("log.userdatalevel") > 2) TbSync.dump("Processing " + mode + " task item", item.title + " (" + id + ")");
+
+ eas.sync.setItemBody(item, syncdata, data);
+ eas.sync.setItemCategories(item, syncdata, data);
+ eas.sync.setItemRecurrence(item, syncdata, data);
+
+ let dueDate = null;
+ if (data.DueDate && data.UtcDueDate) {
+ //extract offset from EAS data
+ let DueDate = new Date(data.DueDate);
+ let UtcDueDate = new Date(data.UtcDueDate);
+ let offset = (UtcDueDate.getTime() - DueDate.getTime())/60000;
+
+ //timezone is identified by its offset
+ let utc = cal.createDateTime(eas.tools.dateToBasicISOString(UtcDueDate)); //format "19800101T000000Z" - UTC
+ dueDate = utc.getInTimezone(eas.tools.guessTimezoneByCurrentOffset(offset, utc));
+ item.dueDate = dueDate;
+ }
+
+ if (data.StartDate && data.UtcStartDate) {
+ //extract offset from EAS data
+ let StartDate = new Date(data.StartDate);
+ let UtcStartDate = new Date(data.UtcStartDate);
+ let offset = (UtcStartDate.getTime() - StartDate.getTime())/60000;
+
+ //timezone is identified by its offset
+ let utc = cal.createDateTime(eas.tools.dateToBasicISOString(UtcStartDate)); //format "19800101T000000Z" - UTC
+ item.entryDate = utc.getInTimezone(eas.tools.guessTimezoneByCurrentOffset(offset, utc));
+ } else {
+ //there is no start date? if this is a recurring item, we MUST add an entryDate, otherwise Thunderbird will not display the recurring items
+ if (data.Recurrence) {
+ if (dueDate) {
+ item.entryDate = dueDate;
+ TbSync.eventlog.add("info", syncdata, "Copy task dueData to task startDate, because Thunderbird needs a startDate for recurring items.", item.icalString);
+ } else {
+ TbSync.eventlog.add("info", syncdata, "Task without startDate and without dueDate but with recurrence info is not supported by Thunderbird. Recurrence will be lost.", item.icalString);
+ }
+ }
+ }
+
+ eas.sync.mapEasPropertyToThunderbird ("Sensitivity", "CLASS", data, item);
+ eas.sync.mapEasPropertyToThunderbird ("Importance", "PRIORITY", data, item);
+
+ let msTodoCompat = eas.prefs.getBoolPref("msTodoCompat");
+
+ item.clearAlarms();
+ if (data.ReminderSet && data.ReminderTime) {
+ let UtcAlarmDate = eas.tools.createDateTime(data.ReminderTime);
+ let alarm = new CalAlarm();
+ alarm.action = "DISPLAY";
+
+ if (msTodoCompat)
+ {
+ // Microsoft To-Do only uses due dates (no start dates) an doesn't have a time part in the due date
+ // dirty hack: Use the reminder date as due date and set a reminder exactly to the due date
+ // drawback: only works if due date and reminder is set to the same day - this could maybe checked here but I don't know how
+ item.entryDate = UtcAlarmDate;
+ item.dueDate = UtcAlarmDate;
+ alarm.related = Components.interfaces.calIAlarm.ALARM_RELATED_START;
+ alarm.offset = TbSync.lightning.cal.createDuration();
+ alarm.offset.inSeconds = 0;
+ }
+ else if (data.UtcStartDate)
+ {
+ let UtcDate = eas.tools.createDateTime(data.UtcStartDate);
+ alarm.related = Components.interfaces.calIAlarm.ALARM_RELATED_START;
+ alarm.offset = UtcAlarmDate.subtractDate(UtcDate);
+ }
+ else
+ {
+ // Alternative solution for Microsoft To-Do:
+ // alarm correctly set but because time part of due date is always "0:00", all tasks for today are shown as overdue
+ alarm.related = Components.interfaces.calIAlarm.ALARM_RELATED_ABSOLUTE;
+ alarm.alarmDate = UtcAlarmDate;
+ }
+ item.addAlarm(alarm);
+ }
+
+ //status/percentage cannot be mapped
+ if (data.Complete) {
+ if (data.Complete == "0") {
+ item.isCompleted = false;
+ } else {
+ item.isCompleted = true;
+ if (data.DateCompleted) item.completedDate = eas.tools.createDateTime(data.DateCompleted);
+ }
+ }
+ },
+
+/*
+ Regenerate: After complete, the completed task is removed from the series and stored as an new entry. The series starts an week (as set) after complete date with one less occurence
+
+ */
+
+
+
+
+
+
+
+ // --------------------------------------------------------------------------- //
+ //read TB event and return its data as WBXML
+ // --------------------------------------------------------------------------- //
+ getWbxmlFromThunderbirdItem: async function (tbItem, syncdata) {
+ let item = tbItem instanceof TbSync.lightning.TbItem ? tbItem.nativeItem : tbItem;
+
+ let asversion = syncdata.accountData.getAccountProperty("asversion");
+ let wbxml = eas.wbxmltools.createWBXML("", syncdata.type); //init wbxml with "" and not with precodes, and set initial codepage
+
+ //Order of tags taken from: https://msdn.microsoft.com/en-us/library/dn338924(v=exchg.80).aspx
+
+ //Subject
+ wbxml.atag("Subject", (item.title) ? item.title : "");
+
+ //Body
+ wbxml.append(eas.sync.getItemBody(item, syncdata));
+
+ //Importance
+ wbxml.atag("Importance", eas.sync.mapThunderbirdPropertyToEas("PRIORITY", "Importance", item));
+
+ //tasks is using extended ISO 8601 (2019-01-18T00:00:00.000Z) instead of basic (20190118T000000Z),
+ //eas.tools.getIsoUtcString returns extended if true as second parameter is present
+
+ // TB will enforce a StartDate if it has a recurrence
+ let localStartDate = null;
+ if (item.entryDate) {
+ wbxml.atag("UtcStartDate", eas.tools.getIsoUtcString(item.entryDate, true));
+ //to fake the local time as UTC, getIsoUtcString needs the third parameter to be true
+ localStartDate = eas.tools.getIsoUtcString(item.entryDate, true, true);
+ wbxml.atag("StartDate", localStartDate);
+ }
+
+ // Tasks without DueDate are breaking O365 - use StartDate as DueDate
+ if (item.entryDate || item.dueDate) {
+ wbxml.atag("UtcDueDate", eas.tools.getIsoUtcString(item.dueDate ? item.dueDate : item.entryDate, true));
+ //to fake the local time as UTC, getIsoUtcString needs the third parameter to be true
+ wbxml.atag("DueDate", eas.tools.getIsoUtcString(item.dueDate ? item.dueDate : item.entryDate, true, true));
+ }
+
+ //Categories
+ wbxml.append(eas.sync.getItemCategories(item, syncdata));
+
+ //Recurrence (only if localStartDate has been set)
+ if (localStartDate) wbxml.append(await eas.sync.getItemRecurrence(item, syncdata, localStartDate));
+
+ //Complete
+ if (item.isCompleted) {
+ wbxml.atag("Complete", "1");
+ wbxml.atag("DateCompleted", eas.tools.getIsoUtcString(item.completedDate, true));
+ } else {
+ wbxml.atag("Complete", "0");
+ }
+
+ //Sensitivity
+ wbxml.atag("Sensitivity", eas.sync.mapThunderbirdPropertyToEas("CLASS", "Sensitivity", item));
+
+ //ReminderTime and ReminderSet
+ let alarms = item.getAlarms({});
+ if (alarms.length>0 && (item.entryDate || item.dueDate)) {
+ let reminderTime;
+ if (alarms[0].offset) {
+ //create Date obj from entryDate by converting item.entryDate to an extended UTC ISO string, which can be parsed by Date
+ //if entryDate is missing, the startDate of this object is set to its dueDate
+ let UtcDate = new Date(eas.tools.getIsoUtcString(item.entryDate ? item.entryDate : item.dueDate, true));
+ //add offset
+ UtcDate.setSeconds(UtcDate.getSeconds() + alarms[0].offset.inSeconds);
+ reminderTime = UtcDate.toISOString();
+ } else {
+ reminderTime = eas.tools.getIsoUtcString(alarms[0].alarmDate, true);
+ }
+ wbxml.atag("ReminderTime", reminderTime);
+ wbxml.atag("ReminderSet", "1");
+ } else {
+ wbxml.atag("ReminderSet", "0");
+ }
+
+ return wbxml.getBytes();
+ },
+}
diff --git a/content/includes/tools.js b/content/includes/tools.js
new file mode 100644
index 0000000..a2298b9
--- /dev/null
+++ b/content/includes/tools.js
@@ -0,0 +1,529 @@
+/*
+ * 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 tools = {
+
+ setCalItemProperty: function (item, prop, value) {
+ if (value == "unset") item.deleteProperty(prop);
+ else item.setProperty(prop, value);
+ },
+
+ getCalItemProperty: function (item, prop) {
+ if (item.hasProperty(prop)) return item.getProperty(prop);
+ else return "unset";
+ },
+
+ isString: function (s) {
+ return (typeof s == 'string' || s instanceof String);
+ },
+
+ getIdentityKey: function (email) {
+ for (let account of MailServices.accounts.accounts) {
+ if (account.defaultIdentity && account.defaultIdentity.email == email) return account.defaultIdentity.key;
+ }
+ return "";
+ },
+
+ parentIsTrash: function (folderData) {
+ let parentID = folderData.getFolderProperty("parentID");
+ if (parentID == "0") return false;
+
+ let parentFolder = folderData.accountData.getFolder("serverID", parentID);
+ if (parentFolder && parentFolder.getFolderProperty("type") == "4") return true;
+
+ return false;
+ },
+
+ getNewDeviceId: function () {
+ //taken from https://jsfiddle.net/briguy37/2MVFd/
+ let d = new Date().getTime();
+ let uuid = 'xxxxxxxxxxxxxxxxyxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ let r = (d + Math.random()*16)%16 | 0;
+ d = Math.floor(d/16);
+ return (c=='x' ? r : (r&0x3|0x8)).toString(16);
+ });
+ return "MZTB" + uuid;
+ },
+
+ 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
+ return eas.tools.getUriFromDirectoryId(aCard.directoryId);
+ } 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;
+ }
+ },
+
+ //read file from within the XPI package
+ fetchFile: function (aURL, returnType = "Array") {
+ return new Promise((resolve, reject) => {
+ let uri = Services.io.newURI(aURL);
+ let channel = Services.io.newChannelFromURI(uri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Components.interfaces.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT,
+ Components.interfaces.nsIContentPolicy.TYPE_OTHER);
+
+ NetUtil.asyncFetch(channel, (inputStream, status) => {
+ if (!Components.isSuccessCode(status)) {
+ reject(status);
+ return;
+ }
+
+ try {
+ let data = NetUtil.readInputStreamToString(inputStream, inputStream.available());
+ if (returnType == "Array") {
+ resolve(data.replace("\r","").split("\n"))
+ } else {
+ resolve(data);
+ }
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ });
+ },
+
+
+
+
+
+
+
+
+
+
+ // TIMEZONE STUFF
+
+ TimeZoneDataStructure : class {
+ constructor() {
+ this.buf = new DataView(new ArrayBuffer(172));
+ }
+
+/*
+ Buffer structure:
+ @000 utcOffset (4x8bit as 1xLONG)
+
+ @004 standardName (64x8bit as 32xWCHAR)
+ @068 standardDate (16x8 as 1xSYSTEMTIME)
+ @084 standardBias (4x8bit as 1xLONG)
+
+ @088 daylightName (64x8bit as 32xWCHAR)
+ @152 daylightDate (16x8 as 1xSTRUCT)
+ @168 daylightBias (4x8bit as 1xLONG)
+*/
+
+ set easTimeZone64 (b64) {
+ //clear buffer
+ for (let i=0; i<172; i++) this.buf.setUint8(i, 0);
+ //load content into buffer
+ let content = (b64 == "") ? "" : atob(b64);
+ for (let i=0; i<content.length; i++) this.buf.setUint8(i, content.charCodeAt(i));
+ }
+
+ get easTimeZone64 () {
+ let content = "";
+ for (let i=0; i<172; i++) content += String.fromCharCode(this.buf.getUint8(i));
+ return (btoa(content));
+ }
+
+ getstr (byteoffset) {
+ let str = "";
+ //walk thru the buffer in 32 steps of 16bit (wchars)
+ for (let i=0;i<32;i++) {
+ let cc = this.buf.getUint16(byteoffset+i*2, true);
+ if (cc == 0) break;
+ str += String.fromCharCode(cc);
+ }
+ return str;
+ }
+
+ setstr (byteoffset, str) {
+ //clear first
+ for (let i=0;i<32;i++) this.buf.setUint16(byteoffset+i*2, 0);
+ //walk thru the buffer in steps of 16bit (wchars)
+ for (let i=0;i<str.length && i<32; i++) this.buf.setUint16(byteoffset+i*2, str.charCodeAt(i), true);
+ }
+
+ getsystemtime (buf, offset) {
+ let systemtime = {
+ get wYear () { return buf.getUint16(offset + 0, true); },
+ get wMonth () { return buf.getUint16(offset + 2, true); },
+ get wDayOfWeek () { return buf.getUint16(offset + 4, true); },
+ get wDay () { return buf.getUint16(offset + 6, true); },
+ get wHour () { return buf.getUint16(offset + 8, true); },
+ get wMinute () { return buf.getUint16(offset + 10, true); },
+ get wSecond () { return buf.getUint16(offset + 12, true); },
+ get wMilliseconds () { return buf.getUint16(offset + 14, true); },
+ toString() { return [this.wYear, this.wMonth, this.wDay].join("-") + ", " + this.wDayOfWeek + ", " + [this.wHour,this.wMinute,this.wSecond].join(":") + "." + this.wMilliseconds},
+
+ set wYear (v) { buf.setUint16(offset + 0, v, true); },
+ set wMonth (v) { buf.setUint16(offset + 2, v, true); },
+ set wDayOfWeek (v) { buf.setUint16(offset + 4, v, true); },
+ set wDay (v) { buf.setUint16(offset + 6, v, true); },
+ set wHour (v) { buf.setUint16(offset + 8, v, true); },
+ set wMinute (v) { buf.setUint16(offset + 10, v, true); },
+ set wSecond (v) { buf.setUint16(offset + 12, v, true); },
+ set wMilliseconds (v) { buf.setUint16(offset + 14, v, true); },
+ };
+ return systemtime;
+ }
+
+ get standardDate () {return this.getsystemtime (this.buf, 68); }
+ get daylightDate () {return this.getsystemtime (this.buf, 152); }
+
+ get utcOffset () { return this.buf.getInt32(0, true); }
+ set utcOffset (v) { this.buf.setInt32(0, v, true); }
+
+ get standardBias () { return this.buf.getInt32(84, true); }
+ set standardBias (v) { this.buf.setInt32(84, v, true); }
+ get daylightBias () { return this.buf.getInt32(168, true); }
+ set daylightBias (v) { this.buf.setInt32(168, v, true); }
+
+ get standardName () {return this.getstr(4); }
+ set standardName (v) {return this.setstr(4, v); }
+ get daylightName () {return this.getstr(88); }
+ set daylightName (v) {return this.setstr(88, v); }
+
+ toString () { return ["",
+ "utcOffset: "+ this.utcOffset,
+ "standardName: "+ this.standardName,
+ "standardDate: "+ this.standardDate.toString(),
+ "standardBias: "+ this.standardBias,
+ "daylightName: "+ this.daylightName,
+ "daylightDate: "+ this.daylightDate.toString(),
+ "daylightBias: "+ this.daylightBias].join("\n"); }
+ },
+
+
+ //Date has a toISOString method, which returns the Date obj as extended ISO 8601,
+ //however EAS MS-ASCAL uses compact/basic ISO 8601,
+ dateToBasicISOString : function (date) {
+ function pad(number) {
+ if (number < 10) {
+ return '0' + number;
+ }
+ return number.toString();
+ }
+
+ return pad(date.getUTCFullYear()) +
+ pad(date.getUTCMonth() + 1) +
+ pad(date.getUTCDate()) +
+ 'T' +
+ pad(date.getUTCHours()) +
+ pad(date.getUTCMinutes()) +
+ pad(date.getUTCSeconds()) +
+ 'Z';
+ },
+
+
+ //Save replacement for cal.createDateTime, which accepts compact/basic and also extended ISO 8601,
+ //cal.createDateTime only supports compact/basic
+ createDateTime: function(str) {
+ let datestring = str;
+ if (str.indexOf("-") == 4) {
+ //this looks like extended ISO 8601
+ let tempDate = new Date(str);
+ datestring = eas.tools.dateToBasicISOString(tempDate);
+ }
+ return TbSync.lightning.cal.createDateTime(datestring);
+ },
+
+
+ // Convert TB date to UTC and return it as basic or extended ISO 8601 String
+ getIsoUtcString: function(origdate, requireExtendedISO = false, fakeUTC = false) {
+ let date = origdate.clone();
+ //floating timezone cannot be converted to UTC (cause they float) - we have to overwrite it with the local timezone
+ if (date.timezone.tzid == "floating") date.timezone = eas.defaultTimezoneInfo.timezone;
+ //to get the UTC string we could use icalString (which does not work on allDayEvents, or calculate it from nativeTime)
+ date.isDate = 0;
+ let UTC = date.getInTimezone(eas.utcTimezone);
+ if (fakeUTC) UTC = date.clone();
+
+ function pad(number) {
+ if (number < 10) {
+ return '0' + number;
+ }
+ return number;
+ }
+
+ if (requireExtendedISO) {
+ return UTC.year +
+ "-" + pad(UTC.month + 1 ) +
+ "-" + pad(UTC.day) +
+ "T" + pad(UTC.hour) +
+ ":" + pad(UTC.minute) +
+ ":" + pad(UTC.second) +
+ "." + "000" +
+ "Z";
+ } else {
+ return UTC.icalString;
+ }
+ },
+
+ getNowUTC : function() {
+ return TbSync.lightning.cal.dtz.jsDateToDateTime(new Date()).getInTimezone(TbSync.lightning.cal.dtz.UTC);
+ },
+
+ //guess the IANA timezone (used by TB) based on the current offset (standard or daylight)
+ guessTimezoneByCurrentOffset: function(curOffset, utcDateTime) {
+ //if we only now the current offset and the current date, we need to actually try each TZ.
+ let tzService = TbSync.lightning.cal.timezoneService;
+
+ //first try default tz
+ let test = utcDateTime.getInTimezone(eas.defaultTimezoneInfo.timezone);
+ TbSync.dump("Matching TZ via current offset: " + test.timezone.tzid + " @ " + curOffset, test.timezoneOffset/-60);
+ if (test.timezoneOffset/-60 == curOffset) return test.timezone;
+
+ //second try UTC
+ test = utcDateTime.getInTimezone(eas.utcTimezone);
+ TbSync.dump("Matching TZ via current offset: " + test.timezone.tzid + " @ " + curOffset, test.timezoneOffset/-60);
+ if (test.timezoneOffset/-60 == curOffset) return test.timezone;
+
+ //third try all others
+ for (let timezoneId of tzService.timezoneIds) {
+ let test = utcDateTime.getInTimezone(tzService.getTimezone(timezoneId));
+ TbSync.dump("Matching TZ via current offset: " + test.timezone.tzid + " @ " + curOffset, test.timezoneOffset/-60);
+ if (test.timezoneOffset/-60 == curOffset) return test.timezone;
+ }
+
+ //return default TZ as fallback
+ return eas.defaultTimezoneInfo.timezone;
+ },
+
+
+ //guess the IANA timezone (used by TB) based on stdandard offset, daylight offset and standard name
+ guessTimezoneByStdDstOffset: function(stdOffset, dstOffset, stdName = "") {
+
+ //get a list of all zones
+ //alternativly use cal.fromRFC3339 - but this is only doing this:
+ //https://dxr.mozilla.org/comm-central/source/calendar/base/modules/calProviderUtils.jsm
+
+ //cache timezone data on first attempt
+ if (eas.cachedTimezoneData === null) {
+ eas.cachedTimezoneData = {};
+ eas.cachedTimezoneData.iana = {};
+ eas.cachedTimezoneData.abbreviations = {};
+ eas.cachedTimezoneData.stdOffset = {};
+ eas.cachedTimezoneData.bothOffsets = {};
+
+ let tzService = TbSync.lightning.cal.timezoneService;
+
+ //cache timezones data from internal IANA data
+ for (let timezoneId of tzService.timezoneIds) {
+ let timezone = tzService.getTimezone(timezoneId);
+ let tzInfo = eas.tools.getTimezoneInfo(timezone);
+
+ eas.cachedTimezoneData.bothOffsets[tzInfo.std.offset+":"+tzInfo.dst.offset] = timezone;
+ eas.cachedTimezoneData.stdOffset[tzInfo.std.offset] = timezone;
+
+ eas.cachedTimezoneData.abbreviations[tzInfo.std.abbreviation] = timezoneId;
+ eas.cachedTimezoneData.iana[timezoneId] = tzInfo;
+
+ //TbSync.dump("TZ ("+ tzInfo.std.id + " :: " + tzInfo.dst.id + " :: " + tzInfo.std.displayname + " :: " + tzInfo.dst.displayname + " :: " + tzInfo.std.offset + " :: " + tzInfo.dst.offset + ")", tzService.getTimezone(id));
+ }
+
+ //make sure, that UTC timezone is there
+ eas.cachedTimezoneData.bothOffsets["0:0"] = eas.utcTimezone;
+
+ //multiple TZ share the same offset and abbreviation, make sure the default timezone is present
+ eas.cachedTimezoneData.abbreviations[eas.defaultTimezoneInfo.std.abbreviation] = eas.defaultTimezoneInfo.std.id;
+ eas.cachedTimezoneData.bothOffsets[eas.defaultTimezoneInfo.std.offset+":"+eas.defaultTimezoneInfo.dst.offset] = eas.defaultTimezoneInfo.timezone;
+ eas.cachedTimezoneData.stdOffset[eas.defaultTimezoneInfo.std.offset] = eas.defaultTimezoneInfo.timezone;
+
+ }
+
+ /*
+ 1. Try to find name in Windows names and map to IANA -> if found, does the stdOffset match? -> if so, done
+ 2. Try to parse our own format, split name and test each chunk for IANA -> if found, does the stdOffset match? -> if so, done
+ 3. Try if one of the chunks matches international code -> if found, does the stdOffset match? -> if so, done
+ 4. Fallback: Use just the offsets */
+
+
+ //check for windows timezone name
+ if (eas.windowsToIanaTimezoneMap[stdName] && eas.cachedTimezoneData.iana[eas.windowsToIanaTimezoneMap[stdName]] && eas.cachedTimezoneData.iana[eas.windowsToIanaTimezoneMap[stdName]].std.offset == stdOffset ) {
+ //the windows timezone maps multiple IANA zones to one (Berlin*, Rome, Bruessel)
+ //check the windowsZoneName of the default TZ and of the winning, if they match, use default TZ
+ //so Rome could win, even Berlin is the default IANA zone
+ if (eas.defaultTimezoneInfo.std.windowsZoneName && eas.windowsToIanaTimezoneMap[stdName] != eas.defaultTimezoneInfo.std.id && eas.cachedTimezoneData.iana[eas.windowsToIanaTimezoneMap[stdName]].std.offset == eas.defaultTimezoneInfo.std.offset && stdName == eas.defaultTimezoneInfo.std.windowsZoneName) {
+ TbSync.dump("Timezone matched via windows timezone name ("+stdName+") with default TZ overtake", eas.windowsToIanaTimezoneMap[stdName] + " -> " + eas.defaultTimezoneInfo.std.id);
+ return eas.defaultTimezoneInfo.timezone;
+ }
+
+ TbSync.dump("Timezone matched via windows timezone name ("+stdName+")", eas.windowsToIanaTimezoneMap[stdName]);
+ return eas.cachedTimezoneData.iana[eas.windowsToIanaTimezoneMap[stdName]].timezone;
+ }
+
+ let parts = stdName.replace(/[;,()\[\]]/g," ").split(" ");
+ for (let i = 0; i < parts.length; i++) {
+ //check for IANA
+ if (eas.cachedTimezoneData.iana[parts[i]] && eas.cachedTimezoneData.iana[parts[i]].std.offset == stdOffset) {
+ TbSync.dump("Timezone matched via IANA", parts[i]);
+ return eas.cachedTimezoneData.iana[parts[i]].timezone;
+ }
+
+ //check for international abbreviation for standard period (CET, CAT, ...)
+ if (eas.cachedTimezoneData.abbreviations[parts[i]] && eas.cachedTimezoneData.iana[eas.cachedTimezoneData.abbreviations[parts[i]]] && eas.cachedTimezoneData.iana[eas.cachedTimezoneData.abbreviations[parts[i]]].std.offset == stdOffset) {
+ TbSync.dump("Timezone matched via international abbreviation (" + parts[i] +")", eas.cachedTimezoneData.abbreviations[parts[i]]);
+ return eas.cachedTimezoneData.iana[eas.cachedTimezoneData.abbreviations[parts[i]]].timezone;
+ }
+ }
+
+ //fallback to zone based on stdOffset and dstOffset, if we have that cached
+ if (eas.cachedTimezoneData.bothOffsets[stdOffset+":"+dstOffset]) {
+ TbSync.dump("Timezone matched via both offsets (std:" + stdOffset +", dst:" + dstOffset + ")", eas.cachedTimezoneData.bothOffsets[stdOffset+":"+dstOffset].tzid);
+ return eas.cachedTimezoneData.bothOffsets[stdOffset+":"+dstOffset];
+ }
+
+ //fallback to zone based on stdOffset only, if we have that cached
+ if (eas.cachedTimezoneData.stdOffset[stdOffset]) {
+ TbSync.dump("Timezone matched via std offset (" + stdOffset +")", eas.cachedTimezoneData.stdOffset[stdOffset].tzid);
+ return eas.cachedTimezoneData.stdOffset[stdOffset];
+ }
+
+ //return default timezone, if everything else fails
+ TbSync.dump("Timezone could not be matched via offsets (std:" + stdOffset +", dst:" + dstOffset + "), using default timezone", eas.defaultTimezoneInfo.std.id);
+ return eas.defaultTimezoneInfo.timezone;
+ },
+
+
+ //extract standard and daylight timezone data
+ getTimezoneInfo: function (timezone) {
+ let tzInfo = {};
+
+ tzInfo.std = eas.tools.getTimezoneInfoObject(timezone, "standard");
+ tzInfo.dst = eas.tools.getTimezoneInfoObject(timezone, "daylight");
+
+ if (tzInfo.dst === null) tzInfo.dst = tzInfo.std;
+
+ tzInfo.timezone = timezone;
+ return tzInfo;
+ },
+
+
+ //get timezone info for standard/daylight
+ getTimezoneInfoObject: function (timezone, standardOrDaylight) {
+
+ //handle UTC
+ if (timezone.isUTC) {
+ let obj = {}
+ obj.id = "UTC";
+ obj.offset = 0;
+ obj.abbreviation = "UTC";
+ obj.displayname = "Coordinated Universal Time (UTC)";
+ return obj;
+ }
+
+ //we could parse the icalstring by ourself, but I wanted to use ICAL.parse - TODO try catch
+ let info = TbSync.lightning.ICAL.parse("BEGIN:VCALENDAR\r\n" + timezone.icalComponent.toString() + "\r\nEND:VCALENDAR");
+ let comp = new TbSync.lightning.ICAL.Component(info);
+ let vtimezone =comp.getFirstSubcomponent("vtimezone");
+ let id = vtimezone.getFirstPropertyValue("tzid").toString();
+ let zone = vtimezone.getFirstSubcomponent(standardOrDaylight);
+
+ if (zone) {
+ let obj = {};
+ obj.id = id;
+
+ //get offset
+ let utcOffset = zone.getFirstPropertyValue("tzoffsetto").toString();
+ let o = parseInt(utcOffset.replace(":","")); //-330 = - 3h 30min
+ let h = Math.floor(o / 100); //-3 -> -180min
+ let m = o - (h*100) //-330 - -300 = -30
+ obj.offset = -1*((h*60) + m);
+
+ //get international abbreviation (CEST, CET, CAT ... )
+ obj.abbreviation = "";
+ try {
+ obj.abbreviation = zone.getFirstPropertyValue("tzname").toString();
+ } catch(e) {
+ TbSync.dump("Failed TZ", timezone.icalComponent.toString());
+ }
+
+ //get displayname
+ obj.displayname = /*"("+utcOffset+") " +*/ obj.id;// + ", " + obj.abbreviation;
+
+ //get DST switch date
+ let rrule = zone.getFirstPropertyValue("rrule");
+ let dtstart = zone.getFirstPropertyValue("dtstart");
+ if (rrule && dtstart) {
+ /*
+
+ THE switchdate PART OF THE OBJECT IS MICROSOFT SPECIFIC, EVERYTHING ELSE IS THUNDERBIRD GENERIC, I LET IT SIT HERE ANYHOW
+
+ https://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx
+
+ To select the correct day in the month, set the wYear member to zero, the wHour and wMinute members to
+ the transition time, the wDayOfWeek member to the appropriate weekday, and the wDay member to indicate
+ the occurrence of the day of the week within the month (1 to 5, where 5 indicates the final occurrence during the
+ month if that day of the week does not occur 5 times).
+
+ Using this notation, specify 02:00 on the first Sunday in April as follows:
+ wHour = 2, wMonth = 4, wDayOfWeek = 0, wDay = 1.
+ Specify 02:00 on the last Thursday in October as follows:
+ wHour = 2, wMonth = 10, wDayOfWeek = 4, wDay = 5.
+
+ So we have to parse the RRULE to exract wDay
+ RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 */
+
+ let parts =rrule.toString().split(";");
+ let rules = {};
+ for (let i = 0; i< parts.length; i++) {
+ let sub = parts[i].split("=");
+ if (sub.length == 2) rules[sub[0]] = sub[1];
+ }
+
+ if (rules.FREQ == "YEARLY" && rules.BYDAY && rules.BYMONTH && rules.BYDAY.length > 2) {
+ obj.switchdate = {};
+ obj.switchdate.month = parseInt(rules.BYMONTH);
+
+ let days = ["SU","MO","TU","WE","TH","FR","SA"];
+ obj.switchdate.dayOfWeek = days.indexOf(rules.BYDAY.substring(rules.BYDAY.length-2));
+ obj.switchdate.weekOfMonth = parseInt(rules.BYDAY.substring(0, rules.BYDAY.length-2));
+ if (obj.switchdate.weekOfMonth<0 || obj.switchdate.weekOfMonth>5) obj.switchdate.weekOfMonth = 5;
+
+ //get switch time from dtstart
+ let dttime = eas.tools.createDateTime(dtstart.toString());
+ obj.switchdate.hour = dttime.hour;
+ obj.switchdate.minute = dttime.minute;
+ obj.switchdate.second = dttime.second;
+ }
+ }
+
+ return obj;
+ }
+ return null;
+ },
+}
+
+//TODO: Invites
+/*
+ cal.itip.checkAndSendOrigial = cal.itip.checkAndSend;
+ cal.itip.checkAndSend = function(aOpType, aItem, aOriginalItem) {
+ //if this item is added_by_user, do not call checkAndSend yet, because the UID is wrong, we need to sync first to get the correct ID - TODO
+ TbSync.dump("cal.checkAndSend", aOpType);
+ cal.itip.checkAndSendOrigial(aOpType, aItem, aOriginalItem);
+ }
+*/
diff --git a/content/includes/wbxmltools.js b/content/includes/wbxmltools.js
new file mode 100644
index 0000000..ae0e38c
--- /dev/null
+++ b/content/includes/wbxmltools.js
@@ -0,0 +1,877 @@
+/*
+ * 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 wbxmltools = {
+
+ // Convert a WBXML (WAP Binary XML) to plain XML - returns save xml with all special chars in the user data encoded by encodeURIComponent
+ convert2xml: function (wbxml) {
+
+ let num = 4; //skip the 4 first bytes which are mostly 0x03 (WBXML Version 1.3), 0x01 (unknown public identifier), 0x6A (utf-8), 0x00 (Length of string table)
+
+ //the main code page will be set to the the first codepage used
+ let mainCodePage = null;
+
+ let tagStack = [];
+ let xml = "";
+ let codepage = 0;
+
+ while (num < wbxml.length) {
+ let data = wbxml.substr(num, 1).charCodeAt(0);
+ let token = data & 0x3F; //removes content bit(6) and attribute bit(7)
+ let tokenHasContent = ((data & 0x40) != 0); //checks if content bit is set
+ let tokenHasAttributes = ((data & 0x80) != 0); //checks if attribute bit is set
+
+ switch(token) {
+ case 0x00: // switch of codepage (new codepage is next byte)
+ num = num + 1;
+ codepage = (wbxml.substr(num, 1)).charCodeAt(0) & 0xFF;
+ break;
+
+ case 0x01: // Indicates the end of an attribute list or the end of an element
+ // tagStack contains a list of opened tags, which await to be closed
+ xml = xml + tagStack.pop();
+ break;
+
+ case 0x02: // A character entity. Followed by a mb_u_int32 encoding the character entity number.
+ TbSync.dump("wbxml", "Encoded character entity has not yet been implemented. Sorry.");
+ return false;
+ break;
+
+ case 0x03: // Inline string followed by a termstr. (0x00)
+ let termpos = wbxml.indexOf(String.fromCharCode(0x00), num);
+ //encode all special chars in the user data by encodeURIComponent which does not encode the apostrophe, so we need to do that by hand
+ xml = xml + encodeURIComponent(wbxml.substring(num + 1, termpos)).replace(/'/g, "%27");
+ num = termpos;
+ break;
+
+ case 0x04: // An unknown tag or attribute name. Followed by an mb_u_int32 that encodes an offset into the string table.
+ case 0x40: // Inline string document-type-specific extension token. Token is followed by a termstr.
+ case 0x41: // Inline string document-type-specific extension token. Token is followed by a termstr.
+ case 0x42: // Inline string document-type-specific extension token. Token is followed by a termstr.
+ case 0x43: // Processing instruction.
+ case 0x44: // Unknown tag, with content.
+ case 0x80: // Inline integer document-type-specific extension token. Token is followed by a mb_uint_32.
+ case 0x81: // Inline integer document-type-specific extension token. Token is followed by a mb_uint_32.
+ case 0x82: // Inline integer document-type-specific extension token. Token is followed by a mb_uint_32.
+ case 0x83: // String table reference. Followed by a mb_u_int32 encoding a byte offset from the beginning of the string table.
+ case 0x84: // Unknown tag, with attributes.
+ case 0xC0: // Single-byte document-type-specific extension token.
+ case 0xC1: // Single-byte document-type-specific extension token.
+ case 0xC2: // Single-byte document-type-specific extension token.
+ case 0xC3: // Opaque document-type-specific data.
+ case 0xC4: // Unknown tag, with content and attributes.
+ TbSync.dump("wbxml", "Global token <" + token + "> has not yet been implemented. Sorry.");
+ return false;
+ break;
+
+ default:
+ // if this code page is not the mainCodePage (or mainCodePage is not yet set = very first tag), add codePageTag with current codepage
+ let codePageTag = (codepage != mainCodePage) ? " xmlns='" + this.getNamespace(codepage) + "'" : "";
+
+ // if no mainCodePage has been defined yet, use the current codepage, which is either the initialized/default value of codepage or a value set by SWITCH_PAGE
+ if (mainCodePage === null) mainCodePage = codepage;
+
+ if (!tokenHasContent) {
+ xml = xml + "<" + this. getCodepageToken(codepage, token) + codePageTag + "/>";
+ } else {
+ xml = xml + "<" +this. getCodepageToken(codepage, token) + codePageTag +">";
+ //add the closing tag to the stack, so it can get properly closed later
+ tagStack.push("</" +this. getCodepageToken(codepage, token) + ">");
+ }
+
+ if (this.isUnknownToken(codepage, token)) {
+ TbSync.eventlog.add("warning", null, "WBXML: Unknown token <" + token + "> for codepage <"+codepage+">.");
+ }
+ }
+ num = num + 1;
+ }
+ return (xml == "") ? "" : '<?xml version="1.0"?>' + xml;
+ },
+
+ isUnknownToken: function (codepage, token) {
+ if (this.codepages[codepage] && token in this.codepages[codepage]) return false;
+ else return true;
+ },
+
+ getNamespace: function (codepage) {
+ return (this.namespaces[codepage]) ? this.namespaces[codepage] : "UnknownCodePage" + codepage ;
+ },
+
+ getCodepageToken: function (codepage, token) {
+ return this.isUnknownToken(codepage, token) ? "Unknown." + codepage + "." + token : this.codepages[codepage][token];
+ },
+
+ // This returns a wbxml object, which allows to add tags (using names), switch codepages, or open and close tags, it is also possible to append pure (binary) wbxml
+ // If no wbxmlstring is present, default to the "init" string ( WBXML Version 1.3, unknown public identifier, UTF-8, Length of string table)
+ createWBXML: function (wbxmlstring = String.fromCharCode(0x03, 0x01, 0x6A, 0x00), initialCodepage = "") {
+ let wbxml = {
+ _codepage : 0,
+ _wbxml : wbxmlstring,
+
+ append : function (wbxmlstring) {
+ this._wbxml = this._wbxml + wbxmlstring;
+ },
+
+ // adding a string content tag as <tagname>contentstring</tagname>
+ atag : function (tokenname, content = "") {
+ //check if tokenname is in current codepage
+ if ((this._codepage in wbxmltools.codepages2) == false) throw "[wbxmltools] Unknown codepage <"+this._codepage+">";
+ if ((tokenname in wbxmltools.codepages2[this._codepage]) == false) throw "[wbxmltools] Unknown tokenname <"+tokenname+"> for codepage <"+wbxmltools.namespaces[this._codepage]+">";
+
+ if (content == "") {
+ //empty, just add token
+ this._wbxml += String.fromCharCode(wbxmltools.codepages2[this._codepage][tokenname]);
+ } else {
+ //not empty,add token with enabled content bit and also add inlinestringidentifier
+ this._wbxml += String.fromCharCode(wbxmltools.codepages2[this._codepage][tokenname] | 0x40, 0x03);
+ //add content
+ for (let i=0; i< content.length; i++) this._wbxml += String.fromCharCode(content.charCodeAt(i));
+ //add string termination and tag close
+ this._wbxml += String.fromCharCode(0x00, 0x01);
+ }
+ },
+
+ switchpage : function (name) {
+ let codepage = wbxmltools.namespaces.indexOf(name);
+ if (codepage == -1) throw "[wbxmltools] Unknown codepage <"+ name +">";
+ this._codepage = codepage;
+ this._wbxml += String.fromCharCode(0x00, codepage);
+ },
+
+ ctag : function () {
+ this._wbxml += String.fromCharCode(0x01);
+ },
+
+ //opentag is assumed to add a token with content, otherwise use addtag
+ otag : function (tokenname) {
+ this._wbxml += String.fromCharCode(wbxmltools.codepages2[this._codepage][tokenname] | 0x40);
+ },
+
+ getCharCodes : function () {
+ let value = "";
+ for (let i=0; i<this._wbxml.length; i++) value += ("00" + this._wbxml.charCodeAt(i).toString(16)).substr(-2) + " ";
+ return value;
+ },
+
+ getBytes : function () {
+ return this._wbxml;
+ }
+ };
+ if (initialCodepage) wbxml._codepage = wbxmltools.namespaces.indexOf(initialCodepage);
+ return wbxml;
+ },
+
+
+
+
+
+ codepages2 : [],
+
+ buildCodepages2 : function () {
+ for (let i=0; i<this.codepages.length; i++) {
+ let inverted = {};
+ for (let token in this.codepages[i]) {
+ inverted[this.codepages[i][token]] = token;
+ }
+ this.codepages2.push(inverted);
+ }
+ },
+
+
+
+
+
+ codepages : [
+ // Code Page 0: AirSync
+ {
+ 0x05: 'Sync',
+ 0x06: 'Responses',
+ 0x07: 'Add',
+ 0x08: 'Change',
+ 0x09: 'Delete',
+ 0x0A: 'Fetch',
+ 0x0B: 'SyncKey',
+ 0x0C: 'ClientId',
+ 0x0D: 'ServerId',
+ 0x0E: 'Status',
+ 0x0F: 'Collection',
+ 0x10: 'Class',
+ 0x12: 'CollectionId',
+ 0x13: 'GetChanges',
+ 0x14: 'MoreAvailable',
+ 0x15: 'WindowSize',
+ 0x16: 'Commands',
+ 0x17: 'Options',
+ 0x18: 'FilterType',
+ 0x1B: 'Conflict',
+ 0x1C: 'Collections',
+ 0x1D: 'ApplicationData',
+ 0x1E: 'DeletesAsMoves',
+ 0x20: 'Supported',
+ 0x21: 'SoftDelete',
+ 0x22: 'MIMESupport',
+ 0x23: 'MIMETruncation',
+ 0x24: 'Wait',
+ 0x25: 'Limit',
+ 0x26: 'Partial',
+ 0x27: 'ConversationMode',
+ 0x28: 'MaxItems',
+ 0x29: 'HeartbeatInterval'
+ },
+ // Code Page 1: Contacts
+ {
+ 0x05: 'Anniversary',
+ 0x06: 'AssistantName',
+ 0x07: 'AssistantPhoneNumber',
+ 0x08: 'Birthday',
+ 0x09: 'Body',
+ 0x0A: 'BodySize',
+ 0x0B: 'BodyTruncated',
+ 0x0C: 'Business2PhoneNumber',
+ 0x0D: 'BusinessAddressCity',
+ 0x0E: 'BusinessAddressCountry',
+ 0x0F: 'BusinessAddressPostalCode',
+ 0x10: 'BusinessAddressState',
+ 0x11: 'BusinessAddressStreet',
+ 0x12: 'BusinessFaxNumber',
+ 0x13: 'BusinessPhoneNumber',
+ 0x14: 'CarPhoneNumber',
+ 0x15: 'Categories',
+ 0x16: 'Category',
+ 0x17: 'Children',
+ 0x18: 'Child',
+ 0x19: 'CompanyName',
+ 0x1A: 'Department',
+ 0x1B: 'Email1Address',
+ 0x1C: 'Email2Address',
+ 0x1D: 'Email3Address',
+ 0x1E: 'FileAs',
+ 0x1F: 'FirstName',
+ 0x20: 'Home2PhoneNumber',
+ 0x21: 'HomeAddressCity',
+ 0x22: 'HomeAddressCountry',
+ 0x23: 'HomeAddressPostalCode',
+ 0x24: 'HomeAddressState',
+ 0x25: 'HomeAddressStreet',
+ 0x26: 'HomeFaxNumber',
+ 0x27: 'HomePhoneNumber',
+ 0x28: 'JobTitle',
+ 0x29: 'LastName',
+ 0x2A: 'MiddleName',
+ 0x2B: 'MobilePhoneNumber',
+ 0x2C: 'OfficeLocation',
+ 0x2D: 'OtherAddressCity',
+ 0x2E: 'OtherAddressCountry',
+ 0x2F: 'OtherAddressPostalCode',
+ 0x30: 'OtherAddressState',
+ 0x31: 'OtherAddressStreet',
+ 0x32: 'PagerNumber',
+ 0x33: 'RadioPhoneNumber',
+ 0x34: 'Spouse',
+ 0x35: 'Suffix',
+ 0x36: 'Title',
+ 0x37: 'WebPage',
+ 0x38: 'YomiCompanyName',
+ 0x39: 'YomiFirstName',
+ 0x3A: 'YomiLastName',
+ 0x3B: 'CompressedRTF',
+ 0x3C: 'Picture',
+ 0x3D: 'Alias',
+ 0x3E: 'WeightedRank'
+ },
+ // Code Page 2: Email
+ {
+ 0x05: 'Attachment',
+ 0x06: 'Attachments',
+ 0x07: 'AttName',
+ 0x08: 'AttSize',
+ 0x09: 'Att0Id',
+ 0x0a: 'AttMethod',
+ 0x0b: 'AttRemoved',
+ 0x0c: 'Body',
+ 0x0d: 'BodySize',
+ 0x0e: 'BodyTruncated',
+ 0x0f: 'DateReceived',
+ 0x10: 'DisplayName',
+ 0x11: 'DisplayTo',
+ 0x12: 'Importance',
+ 0x13: 'MessageClass',
+ 0x14: 'Subject',
+ 0x15: 'Read',
+ 0x16: 'To',
+ 0x17: 'Cc',
+ 0x18: 'From',
+ 0x19: 'ReplyTo',
+ 0x1a: 'AllDayEvent',
+ 0x1b: 'Categories',
+ 0x1c: 'Category',
+ 0x1d: 'DTStamp',
+ 0x1e: 'EndTime',
+ 0x1f: 'InstanceType',
+ 0x20: 'BusyStatus',
+ 0x21: 'Location',
+ 0x22: 'MeetingRequest',
+ 0x23: 'Organizer',
+ 0x24: 'RecurrenceId',
+ 0x25: 'Reminder',
+ 0x26: 'ResponseRequested',
+ 0x27: 'Recurrences',
+ 0x28: 'Recurrence',
+ 0x29: 'Recurrence_Type',
+ 0x2a: 'Recurrence_Until',
+ 0x2b: 'Recurrence_Occurrences',
+ 0x2c: 'Recurrence_Interval',
+ 0x2d: 'Recurrence_DayOfWeek',
+ 0x2e: 'Recurrence_DayOfMonth',
+ 0x2f: 'Recurrence_WeekOfMonth',
+ 0x30: 'Recurrence_MonthOfYear',
+ 0x31: 'StartTime',
+ 0x32: 'Sensitivity',
+ 0x33: 'TimeZone',
+ 0x34: 'GlobalObjId',
+ 0x35: 'ThreadTopic',
+ 0x36: 'MIMEData',
+ 0x37: 'MIMETruncated',
+ 0x38: 'MIMESize',
+ 0x39: 'InternetCPID',
+ 0x3a: 'Flag',
+ 0x3b: 'Status',
+ 0x3c: 'ContentClass',
+ 0x3d: 'FlagType',
+ 0x3e: 'CompleteTime',
+ 0x3f: 'DisallowNewTimeProposal'
+ },
+ // Code Page 3: AirNotify (WBXML code page 3 is no longer in use)
+ {},
+ // Code Page 4: Calendar
+ {
+ 0x05: 'TimeZone',
+ 0x06: 'AllDayEvent',
+ 0x07: 'Attendees',
+ 0x08: 'Attendee',
+ 0x09: 'Email',
+ 0x0a: 'Name',
+ 0x0b: 'Body',
+ 0x0c: 'BodyTruncated',
+ 0x0d: 'BusyStatus',
+ 0x0e: 'Categories',
+ 0x0f: 'Category',
+ 0x10: 'CompressedRTF',
+ 0x11: 'DtStamp',
+ 0x12: 'EndTime',
+ 0x13: 'Exception',
+ 0x14: 'Exceptions',
+ 0x15: 'Deleted',
+ 0x16: 'ExceptionStartTime',
+ 0x17: 'Location',
+ 0x18: 'MeetingStatus',
+ 0x19: 'OrganizerEmail',
+ 0x1a: 'OrganizerName',
+ 0x1b: 'Recurrence',
+ 0x1c: 'Type',
+ 0x1d: 'Until',
+ 0x1e: 'Occurrences',
+ 0x1f: 'Interval',
+ 0x20: 'DayOfWeek',
+ 0x21: 'DayOfMonth',
+ 0x22: 'WeekOfMonth',
+ 0x23: 'MonthOfYear',
+ 0x24: 'Reminder',
+ 0x25: 'Sensitivity',
+ 0x26: 'Subject',
+ 0x27: 'StartTime',
+ 0x28: 'UID',
+ 0x29: 'AttendeeStatus',
+ 0x2a: 'AttendeeType',
+ 0x2b: 'Attachment',
+ 0x2c: 'Attachments',
+ 0x2d: 'AttName',
+ 0x2e: 'AttSize',
+ 0x2f: 'AttOid',
+ 0x30: 'AttMethod',
+ 0x31: 'AttRemoved',
+ 0x32: 'DisplayName',
+ 0x33: 'DisallowNewTimeProposal',
+ 0x34: 'ResponseRequested',
+ 0x35: 'AppointmentReplyTime',
+ 0x36: 'ResponseType',
+ 0x37: 'CalendarType',
+ 0x38: 'IsLeapMonth',
+ 0x39: 'FirstDayOfWeek',
+ 0x3a: 'OnlineMeetingConfLink',
+ 0x3b: 'OnlineMeetingExternalLink'
+ },
+ // Code Page 5: Move
+ {
+ 0x05: 'MoveItems',
+ 0x06: 'Move',
+ 0x07: 'SrcMsgId',
+ 0x08: 'SrcFldId',
+ 0x09: 'DstFldId',
+ 0x0A: 'Response',
+ 0x0B: 'Status',
+ 0x0C: 'DstMsgId'
+ },
+ // Code Page 6: GetItemEstimate
+ {
+ 0x05: 'GetItemEstimate',
+ 0x06: 'Version',
+ 0x07: 'Collections',
+ 0x08: 'Collection',
+ 0x09: 'Class',
+ 0x0A: 'CollectionId',
+ 0x0B: 'DateTime',
+ 0x0C: 'Estimate',
+ 0x0D: 'Response',
+ 0x0E: 'Status'
+ },
+ // Code Page 7: FolderHierarchy
+ {
+ 0x07: 'DisplayName',
+ 0x08: 'ServerId',
+ 0x09: 'ParentId',
+ 0x0A: 'Type',
+ 0x0C: 'Status',
+ 0x0E: 'Changes',
+ 0x0F: 'Add',
+ 0x10: 'Delete',
+ 0x11: 'Update',
+ 0x12: 'SyncKey',
+ 0x13: 'FolderCreate',
+ 0x14: 'FolderDelete',
+ 0x15: 'FolderUpdate',
+ 0x16: 'FolderSync',
+ 0x17: 'Count'
+ },
+ // Code Page 8: MeetingResponse
+ {
+ 0x05: 'CalendarId',
+ 0x06: 'CollectionId',
+ 0x07: 'MeetingResponse',
+ 0x08: 'RequestId',
+ 0x09: 'Request',
+ 0x0a: 'Result',
+ 0x0b: 'Status',
+ 0x0c: 'UserResponse',
+ 0x0e: 'InstanceId'
+ },
+ // Code Page 9: Tasks
+ {
+ 0x05: 'Body',
+ 0x06: 'BodySize',
+ 0x07: 'BodyTruncated',
+ 0x08: 'Categories',
+ 0x09: 'Category',
+ 0x0A: 'Complete',
+ 0x0B: 'DateCompleted',
+ 0x0C: 'DueDate',
+ 0x0D: 'UtcDueDate',
+ 0x0E: 'Importance',
+ 0x0F: 'Recurrence',
+ 0x10: 'Type',
+ 0x11: 'Start',
+ 0x12: 'Until',
+ 0x13: 'Occurrences',
+ 0x14: 'Interval',
+ 0x15: 'DayOfMonth',
+ 0x16: 'DayOfWeek',
+ 0x17: 'WeekOfMonth',
+ 0x18: 'MonthOfYear',
+ 0x19: 'Regenerate',
+ 0x1A: 'DeadOccur',
+ 0x1B: 'ReminderSet',
+ 0x1C: 'ReminderTime',
+ 0x1D: 'Sensitivity',
+ 0x1E: 'StartDate',
+ 0x1F: 'UtcStartDate',
+ 0x20: 'Subject',
+ 0x22: 'OrdinalDate',
+ 0x23: 'SubOrdinalDate',
+ 0x24: 'CalendarType',
+ 0x25: 'IsLeapMonth',
+ 0x26: 'FirstDayOfWeek'
+ },
+ // Code Page 10: ResolveRecipients
+ {
+ 0x05: 'ResolveRecipients',
+ 0x06: 'Response',
+ 0x07: 'Status',
+ 0x08: 'Type',
+ 0x09: 'Recipient',
+ 0x0a: 'DisplayName',
+ 0x0b: 'EmailAddress',
+ 0x0c: 'Certificates',
+ 0x0d: 'Certificate',
+ 0x0e: 'MiniCertificate',
+ 0x0f: 'Options',
+ 0x10: 'To',
+ 0x11: 'CertificateRetrieval',
+ 0x12: 'RecipientCount',
+ 0x13: 'MaxCertificates',
+ 0x14: 'MaxAmbiguousRecipients',
+ 0x15: 'CertificateCount',
+ 0x16: 'Availability',
+ 0x17: 'StartTime',
+ 0x18: 'EndTime',
+ 0x19: 'MergedFreeBusy',
+ 0x1a: 'Picture',
+ 0x1b: 'MaxSize',
+ 0x1c: 'Data',
+ 0x1d: 'MaxPictures'
+ },
+ // Code Page 11: ValidateCert
+ {
+ 0x05: 'ValidateCert',
+ 0x06: 'Certificates',
+ 0x07: 'Certificate',
+ 0x08: 'CertificateChain',
+ 0x09: 'CheckCRL',
+ 0x0a: 'Status'
+ },
+ // Code Page 12: Contacts2
+ {
+ 0x05: 'CustomerId',
+ 0x06: 'GovernmentId',
+ 0x07: 'IMAddress',
+ 0x08: 'IMAddress2',
+ 0x09: 'IMAddress3',
+ 0x0a: 'ManagerName',
+ 0x0b: 'CompanyMainPhone',
+ 0x0c: 'AccountName',
+ 0x0d: 'NickName',
+ 0x0e: 'MMS'
+ },
+ // Code Page 13: Ping
+ {
+ 0x05: 'Ping',
+ 0x06: 'AutdState',
+ //(Not used)
+ 0x07: 'Status',
+ 0x08: 'HeartbeatInterval',
+ 0x09: 'Folders',
+ 0x0A: 'Folder',
+ 0x0B: 'Id',
+ 0x0C: 'Class',
+ 0x0D: 'MaxFolders'
+ },
+ // Code Page 14: Provision
+ {
+ 0x05: 'Provision',
+ 0x06: 'Policies',
+ 0x07: 'Policy',
+ 0x08: 'PolicyType',
+ 0x09: 'PolicyKey',
+ 0x0A: 'Data',
+ 0x0B: 'Status',
+ 0x0C: 'RemoteWipe',
+ 0x0D: 'EASProvisionDoc',
+ 0x0E: 'DevicePasswordEnabled',
+ 0x0F: 'AlphanumericDevicePasswordRequired',
+ 0x10: 'DeviceEncryptionEnabled',
+ 0x10: 'RequireStorageCardEncryption',
+ 0x11: 'PasswordRecoveryEnabled',
+ 0x13: 'AttachmentsEnabled',
+ 0x14: 'MinDevicePasswordLength',
+ 0x15: 'MaxInactivityTimeDeviceLock',
+ 0x16: 'MaxDevicePasswordFailedAttempts',
+ 0x17: 'MaxAttachmentSize',
+ 0x18: 'AllowSimpleDevicePassword',
+ 0x19: 'DevicePasswordExpiration',
+ 0x1A: 'DevicePasswordHistory',
+ 0x1B: 'AllowStorageCard',
+ 0x1C: 'AllowCamera',
+ 0x1D: 'RequireDeviceEncryption',
+ 0x1E: 'AllowUnsignedApplications',
+ 0x1F: 'AllowUnsignedInstallationPackages',
+ 0x20: 'MinDevicePasswordComplexCharacters',
+ 0x21: 'AllowWiFi',
+ 0x22: 'AllowTextMessaging',
+ 0x23: 'AllowPOPIMAPEmail',
+ 0x24: 'AllowBluetooth',
+ 0x25: 'AllowIrDA',
+ 0x26: 'RequireManualSyncWhenRoaming',
+ 0x27: 'AllowDesktopSync',
+ 0x28: 'MaxCalendarAgeFilter',
+ 0x29: 'AllowHTMLEmail',
+ 0x2A: 'MaxEmailAgeFilter',
+ 0x2B: 'MaxEmailBodyTruncationSize',
+ 0x2C: 'MaxEmailHTMLBodyTruncationSize',
+ 0x2D: 'RequireSignedSMIMEMessages',
+ 0x2E: 'RequireEncryptedSMIMEMessages',
+ 0x2F: 'RequireSignedSMIMEAlgorithm',
+ 0x30: 'RequireEncryptionSMIMEAlgorithm',
+ 0x31: 'AllowSMIMEEncryptionAlgorithmNegotiation',
+ 0x32: 'AllowSMIMESoftCerts',
+ 0x33: 'AllowBrowser',
+ 0x34: 'AllowConsumerEmail',
+ 0x35: 'AllowRemoteDesktop',
+ 0x36: 'AllowInternetSharing',
+ 0x37: 'UnapprovedInROMApplicationList',
+ 0x38: 'ApplicationName',
+ 0x39: 'ApprovedApplicationList',
+ 0x3A: 'Hash'
+ },
+ // Code Page 15: Search
+ {
+ 0x05: 'Search',
+ 0x06: 'Stores',
+ 0x07: 'Store',
+ 0x08: 'Name',
+ 0x09: 'Query',
+ 0x0a: 'Options',
+ 0x0b: 'Range',
+ 0x0c: 'Status',
+ 0x0d: 'Response',
+ 0x0e: 'Result',
+ 0x0f: 'Properties',
+ 0x10: 'Total',
+ 0x11: 'EqualTo',
+ 0x12: 'Value',
+ 0x13: 'And',
+ 0x14: 'Or',
+ 0x15: 'FreeText',
+ 0x17: 'DeepTraversal',
+ 0x18: 'LongId',
+ 0x19: 'RebuildResults',
+ 0x1a: 'LessThan',
+ 0x1b: 'GreaterThan',
+ 0x1c: 'Schema',
+ 0x1d: 'Supported',
+ 0x1e: 'UserName',
+ 0x1f: 'Password',
+ 0x20: 'ConversationId',
+ 0x21: 'Picture',
+ 0x22: 'MaxSize',
+ 0x23: 'MaxPictures'
+ },
+ // Code Page 16: GAL
+ {
+ 0x05: 'DisplayName',
+ 0x06: 'Phone',
+ 0x07: 'Office',
+ 0x08: 'Title',
+ 0x09: 'Company',
+ 0x0a: 'Alias',
+ 0x0b: 'FirstName',
+ 0x0c: 'LastName',
+ 0x0d: 'HomePhone',
+ 0x0e: 'MobilePhone',
+ 0x0f: 'EmailAddress',
+ 0x10: 'Picture',
+ 0x11: 'Status',
+ 0x12: 'Data'
+ },
+ // Code Page 17: AirSyncBase
+ {
+ 0x05: 'BodyPreference',
+ 0x06: 'Type',
+ 0x07: 'TruncationSize',
+ 0x08: 'AllOrNone',
+ 0x0A: 'Body',
+ 0x0B: 'Data',
+ 0x0C: 'EstimatedDataSize',
+ 0x0D: 'Truncated',
+ 0x0E: 'Attachments',
+ 0x0F: 'Attachment',
+ 0x10: 'DisplayName',
+ 0x11: 'FileReference',
+ 0x12: 'Method',
+ 0x13: 'ContentId',
+ 0x14: 'ContentLocation',
+ 0x15: 'IsInline',
+ 0x16: 'NativeBodyType',
+ 0x17: 'ContentType',
+ 0x18: 'Preview',
+ 0x19: 'BodyPartPreference',
+ 0x1A: 'BodyPart',
+ 0x1B: 'Status'
+ },
+ // Code Page 18: Settings
+ {
+ 0x05: 'Settings',
+ 0x06: 'Status',
+ 0x07: 'Get',
+ 0x08: 'Set',
+ 0x09: 'Oof',
+ 0x0A: 'OofState',
+ 0x0B: 'StartTime',
+ 0x0C: 'EndTime',
+ 0x0D: 'OofMessage',
+ 0x0E: 'AppliesToInternal',
+ 0x0F: 'AppliesToExternalKnown',
+ 0x10: 'AppliesToExternalUnknown',
+ 0x11: 'Enabled',
+ 0x12: 'ReplyMessage',
+ 0x13: 'BodyType',
+ 0x14: 'DevicePassword',
+ 0x15: 'Password',
+ 0x16: 'DeviceInformation',
+ 0x17: 'Model',
+ 0x18: 'IMEI',
+ 0x19: 'FriendlyName',
+ 0x1A: 'OS',
+ 0x1B: 'OSLanguage',
+ 0x1C: 'PhoneNumber',
+ 0x1D: 'UserInformation',
+ 0x1E: 'EmailAddresses',
+ 0x1F: 'SMTPAddress',
+ 0x20: 'UserAgent',
+ 0x21: 'EnableOutboundSMS',
+ 0x22: 'MobileOperator',
+ 0x23: 'PrimarySmtpAddress',
+ 0x24: 'Accounts',
+ 0x25: 'Account',
+ 0x26: 'AccountId',
+ 0x27: 'AccountName',
+ 0x28: 'UserDisplayName',
+ 0x29: 'SendDisabled',
+ 0x2B: 'RightsManagementInformation'
+ },
+ // Code Page 19: DocumentLibrary
+ {
+ 0x05: 'LinkId',
+ 0x06: 'DisplayName',
+ 0x07: 'IsFolder',
+ 0x08: 'CreationDate',
+ 0x09: 'LastModifiedDate',
+ 0x0a: 'IsHidden',
+ 0x0b: 'ContentLength',
+ 0x0c: 'ContentType'
+ },
+ // Code Page 20: ItemOperations
+ {
+ 0x05: 'ItemOperations',
+ 0x06: 'Fetch',
+ 0x07: 'Store',
+ 0x08: 'Options',
+ 0x09: 'Range',
+ 0x0A: 'Total',
+ 0x0B: 'Properties',
+ 0x0C: 'Data',
+ 0x0D: 'Status',
+ 0x0E: 'Response',
+ 0x0F: 'Version',
+ 0x10: 'Schema',
+ 0x11: 'Part',
+ 0x12: 'EmptyFolderContents',
+ 0x13: 'DeleteSubFolders',
+ 0x14: 'UserName',
+ 0x15: 'Password',
+ 0x16: 'Move',
+ 0x17: 'DstFldId',
+ 0x18: 'ConversationId',
+ 0x19: 'MoveAlways'
+ },
+ // Code Page 21: ComposeMail
+ {
+ 0x05: 'SendMail',
+ 0x06: 'SmartForward',
+ 0x07: 'SmartReply',
+ 0x08: 'SaveInSentItems',
+ 0x09: 'ReplaceMime',
+ 0x0b: 'Source',
+ 0x0c: 'FolderId',
+ 0x0d: 'ItemId',
+ 0x0e: 'LongId',
+ 0x0f: 'InstanceId',
+ 0x10: 'Mime',
+ 0x11: 'ClientId',
+ 0x12: 'Status',
+ 0x13: 'AccountId',
+ 0x15: 'Forwardees',
+ 0x16: 'Forwardee',
+ 0x17: 'ForwardeeName',
+ 0x18: 'ForwardeeEmail'
+ },
+ // Code Page 22: Email2
+ {
+ 0x05: 'UmCallerID',
+ 0x06: 'UmUserNotes',
+ 0x07: 'UmAttDuration',
+ 0x08: 'UmAttOrder',
+ 0x09: 'ConversationId',
+ 0x0a: 'ConversationIndex',
+ 0x0b: 'LastVerbExecuted',
+ 0x0c: 'LastVerbExecutionTime',
+ 0x0d: 'ReceivedAsBcc',
+ 0x0e: 'Sender',
+ 0x0f: 'CalendarType',
+ 0x10: 'IsLeapMonth',
+ 0x11: 'AccountId',
+ 0x12: 'FirstDayOfWeek',
+ 0x13: 'MeetingMessageType',
+ 0x15: 'IsDraft',
+ 0x16: 'Bcc',
+ 0x17: 'Send'
+ },
+ // Code Page 23: Notes
+ {
+ 0x05: 'Subject',
+ 0x06: 'MessageClass',
+ 0x07: 'LastModifiedDate',
+ 0x08: 'Categories',
+ 0x09: 'Category'
+ },
+ // Code Page 24: RightsManagement
+ {
+ 0x05: 'RightsManagementSupport',
+ 0x06: 'RightsManagementTemplates',
+ 0x07: 'RightsManagementTemplate',
+ 0x08: 'RightsManagementLicense',
+ 0x09: 'EditAllowed',
+ 0x0a: 'ReplyAllowed',
+ 0x0b: 'ReplyAllAllowed',
+ 0x0c: 'ForwardAllowed',
+ 0x0d: 'ModifyRecipientsAllowed',
+ 0x0e: 'ExtractAllowed',
+ 0x0f: 'PrintAllowed',
+ 0x10: 'ExportAllowed',
+ 0x11: 'ProgrammaticAccessAllowed',
+ 0x12: 'Owner',
+ 0x13: 'ContentExpiryDate',
+ 0x14: 'TemplateID',
+ 0x15: 'TemplateName',
+ 0x16: 'TemplateDescription',
+ 0x17: 'ContentOwner',
+ 0x18: 'RemoveRightsManagementDistribution'
+ }
+ ],
+
+ namespaces : [
+ 'AirSync',
+ 'Contacts',
+ 'Email',
+ 'AirNotify',
+ 'Calendar',
+ 'Move',
+ 'GetItemEstimate',
+ 'FolderHierarchy',
+ 'MeetingResponse',
+ 'Tasks',
+ 'ResolveRecipients',
+ 'ValidateCert',
+ 'Contacts2',
+ 'Ping',
+ 'Provision',
+ 'Search',
+ 'Gal',
+ 'AirSyncBase',
+ 'Settings',
+ 'DocumentLibrary',
+ 'ItemOperations',
+ 'ComposeMail',
+ 'Email2',
+ 'Notes',
+ 'RightsManagement'
+ ]
+
+};
+
+wbxmltools.buildCodepages2();
diff --git a/content/includes/xmltools.js b/content/includes/xmltools.js
new file mode 100644
index 0000000..68b69d0
--- /dev/null
+++ b/content/includes/xmltools.js
@@ -0,0 +1,155 @@
+/*
+ * 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 xmltools = {
+
+ isString : function (obj) {
+ return (Object.prototype.toString.call(obj) === '[object String]');
+ },
+
+ checkString : function(d, fallback = "") {
+ if (this.isString(d)) return d;
+ else return fallback;
+ },
+
+ nodeAsArray : function (node) {
+ let a = [];
+ if (node) {
+ //return, if already an array
+ if (node instanceof Array) return node;
+
+ //else push node into an array
+ a.push(node);
+ }
+ return a;
+ },
+
+ hasWbxmlDataField: function(wbxmlData, path) {
+ if (wbxmlData) {
+ let pathElements = path.split(".");
+ let data = wbxmlData;
+ for (let x = 0; x < pathElements.length; x++) {
+ if (data[pathElements[x]]) data = data[pathElements[x]];
+ else return false;
+ }
+ return true;
+ }
+ return false;
+ },
+
+ getWbxmlDataField: function(wbxmlData,path) {
+ if (wbxmlData) {
+ let pathElements = path.split(".");
+ let data = wbxmlData;
+ let valid = true;
+ for (let x = 0; valid && x < pathElements.length; x++) {
+ if (data[pathElements[x]]) data = data[pathElements[x]];
+ else valid = false;
+ }
+ if (valid) return data;
+ }
+ return false
+ },
+
+ //print content of xml data object (if debug output enabled)
+ printXmlData : function (data, printApplicationData) {
+ if (TbSync.prefs.getIntPref("log.userdatalevel") > 1 || (TbSync.prefs.getIntPref("log.userdatalevel") == 1 && printApplicationData)) {
+ let dump = JSON.stringify(data);
+ TbSync.dump("Extracted XML data", "\n" + dump);
+ }
+ },
+
+ getDataFromXMLString: function (str) {
+ let data = null;
+ let xml = "";
+ if (str == "") return data;
+
+ let oParser = (Services.vc.compare(Services.appinfo.platformVersion, "61.*") >= 0) ? new DOMParser() : Components.classes["@mozilla.org/xmlextras/domparser;1"].createInstance(Components.interfaces.nsIDOMParser);
+ try {
+ xml = oParser.parseFromString(str, "application/xml");
+ } catch (e) {
+ //however, domparser does not throw an error, it returns an error document
+ //https://developer.mozilla.org/de/docs/Web/API/DOMParser
+ //just in case
+ throw eas.sync.finish("error", "malformed-xml");
+ }
+
+ //check if xml is error document
+ if (xml.documentElement.nodeName == "parsererror") {
+ TbSync.dump("BAD XML", "The above XML and WBXML could not be parsed correctly, something is wrong.");
+ throw eas.sync.finish("error", "malformed-xml");
+ }
+
+ try {
+ data = this.getDataFromXML(xml);
+ } catch (e) {
+ throw eas.sync.finish("error", "mailformed-data");
+ }
+
+ return data;
+ },
+
+ //create data object from XML node
+ getDataFromXML : function (nodes) {
+
+ /*
+ * The passed nodes value could be an entire document in a single node (type 9) or a
+ * single element node (type 1) as returned by getElementById. It could however also
+ * be an array of nodes as returned by getElementsByTagName or a nodeList as returned
+ * by childNodes. In that case node.length is defined.
+ */
+
+ // create the return object
+ let obj = {};
+ let nodeList = [];
+ let multiplicity = {};
+
+ if (nodes.length === undefined) nodeList.push(nodes);
+ else nodeList = nodes;
+
+ // nodelist contains all childs, if two childs have the same name, we cannot add the chils as an object, but as an array of objects
+ for (let node of nodeList) {
+ if (node.nodeType == 1 || node.nodeType == 3) {
+ if (!multiplicity.hasOwnProperty(node.nodeName)) multiplicity[node.nodeName] = 0;
+ multiplicity[node.nodeName]++;
+ //if this nodeName has multiplicity > 1, prepare obj (but only once)
+ if (multiplicity[node.nodeName]==2) obj[node.nodeName] = [];
+ }
+ }
+
+ // process nodes
+ for (let node of nodeList) {
+ switch (node.nodeType) {
+ case 9:
+ //document node, dive directly and process all children
+ if (node.hasChildNodes) obj = this.getDataFromXML(node.childNodes);
+ break;
+ case 1:
+ //element node
+ if (node.hasChildNodes) {
+ //if this is an element with only one text child, do not dive, but get text childs value
+ let o;
+ if (node.childNodes.length == 1 && node.childNodes.item(0).nodeType==3) {
+ //the passed xml is a save xml with all special chars in the user data encoded by encodeURIComponent
+ o = decodeURIComponent(node.childNodes.item(0).nodeValue);
+ } else {
+ o = this.getDataFromXML(node.childNodes);
+ }
+ //check, if we can add the object directly, or if we have to push it into an array
+ if (multiplicity[node.nodeName]>1) obj[node.nodeName].push(o)
+ else obj[node.nodeName] = o;
+ }
+ break;
+ }
+ }
+ return obj;
+ }
+
+};
diff --git a/content/locales.js b/content/locales.js
new file mode 100644
index 0000000..7bc8b43
--- /dev/null
+++ b/content/locales.js
@@ -0,0 +1,3 @@
+var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+
+TbSync.localizeOnLoad(window, "eas");
diff --git a/content/manager/createAccount.js b/content/manager/createAccount.js
new file mode 100644
index 0000000..72c266b
--- /dev/null
+++ b/content/manager/createAccount.js
@@ -0,0 +1,244 @@
+/*
+ * 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 { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+
+const eas = TbSync.providers.eas;
+
+var tbSyncEasNewAccount = {
+
+ startTime: 0,
+ maxTimeout: 30,
+ validating: false,
+
+ onClose: function () {
+ //disallow closing of wizard while validating
+ return !this.validating;
+ },
+
+ onCancel: function (event) {
+ //disallow closing of wizard while validating
+ if (this.validating) {
+ event.preventDefault();
+ }
+ },
+
+ onLoad: function () {
+ this.providerData = new TbSync.ProviderData("eas");
+
+ this.elementName = document.getElementById('tbsync.newaccount.name');
+ this.elementUser = document.getElementById('tbsync.newaccount.user');
+ this.elementUrl = document.getElementById('tbsync.newaccount.url');
+ this.elementPass = document.getElementById('tbsync.newaccount.password');
+ this.elementServertype = document.getElementById('tbsync.newaccount.servertype');
+
+ document.getElementById("tbsync.newaccount.wizard").getButton("back").hidden = true;
+ this.onUserDropdown();
+
+ document.getElementById("tbsync.error").hidden = true;
+ document.getElementById("tbsync.spinner").hidden = true;
+
+ document.addEventListener("wizardfinish", tbSyncEasNewAccount.onFinish.bind(this));
+ document.addEventListener("wizardcancel", tbSyncEasNewAccount.onCancel.bind(this));
+ // bug https://bugzilla.mozilla.org/show_bug.cgi?id=1618252
+ document.getElementById('tbsync.newaccount.wizard')._adjustWizardHeader();
+ },
+
+ onUnload: function () {
+ },
+
+ onUserTextInput: function () {
+ document.getElementById("tbsync.error").hidden = true;
+ switch (this.elementServertype.value) {
+ case "select":
+ document.getElementById("tbsync.newaccount.wizard").getButton("finish").disabled = true;
+ break;
+
+ case "auto":
+ document.getElementById("tbsync.newaccount.wizard").getButton("finish").disabled = (this.elementName.value.trim() == "" || this.elementUser.value == "" || this.elementPass.value == "");
+ break;
+
+ case "office365":
+ document.getElementById("tbsync.newaccount.wizard").getButton("finish").disabled = (this.elementName.value.trim() == "" || this.elementUser.value == "");
+ break;
+
+ case "custom":
+ default:
+ document.getElementById("tbsync.newaccount.wizard").getButton("finish").disabled = (this.elementName.value.trim() == "" || this.elementUser.value == "" || this.elementPass.value == "" || this.elementUrl.value.trim() == "");
+ break;
+ }
+ },
+
+ onUserDropdown: function () {
+ if (this.elementServertype) {
+ switch (this.elementServertype.value) {
+ case "select":
+ document.getElementById('tbsync.newaccount.user.box').hidden = true;
+ document.getElementById('tbsync.newaccount.url.box').hidden = true;
+ document.getElementById('tbsync.newaccount.password.box').hidden = true;
+ document.getElementById("tbsync.newaccount.wizard").getButton("finish").label = TbSync.getString("newaccount.add_custom","eas");
+ break;
+
+ case "auto":
+ document.getElementById('tbsync.newaccount.user.box').hidden = false;
+ document.getElementById('tbsync.newaccount.url.box').hidden = true;
+ document.getElementById('tbsync.newaccount.password.box').hidden = false;
+ document.getElementById("tbsync.newaccount.wizard").getButton("finish").label = TbSync.getString("newaccount.add_auto","eas");
+ break;
+
+ case "office365":
+ document.getElementById('tbsync.newaccount.user.box').hidden = false;
+ document.getElementById('tbsync.newaccount.url.box').hidden = true;
+ document.getElementById('tbsync.newaccount.password.box').hidden = true;
+ document.getElementById("tbsync.newaccount.wizard").getButton("finish").label = TbSync.getString("newaccount.add_custom","eas");
+ break;
+
+ case "custom":
+ default:
+ document.getElementById('tbsync.newaccount.user.box').hidden = false;
+ document.getElementById('tbsync.newaccount.url.box').hidden = false;
+ document.getElementById('tbsync.newaccount.password.box').hidden = false;
+ document.getElementById("tbsync.newaccount.wizard").getButton("finish").label = TbSync.getString("newaccount.add_custom","eas");
+ break;
+ }
+ this.onUserTextInput();
+ //document.getElementById("tbsync.newaccount.name").focus();
+ }
+ },
+
+ onFinish: function (event) {
+ if (document.getElementById("tbsync.newaccount.wizard").getButton("finish").disabled == false) {
+ //initiate validation of server connection
+ this.validate();
+ }
+ event.preventDefault();
+ },
+
+ validate: async function () {
+ let user = this.elementUser.value;
+ let servertype = this.elementServertype.value;
+ let accountname = this.elementName.value.trim();
+
+ let url = (servertype == "custom") ?this.elementUrl.value.trim() : "";
+ let password = (servertype == "auto" || servertype == "custom") ? this.elementPass.value : "";
+
+ if ((servertype == "auto" || servertype == "office365") && user.split("@").length != 2) {
+ alert(TbSync.getString("autodiscover.NeedEmail","eas"))
+ return;
+ }
+
+ this.validating = true;
+ let error = "";
+
+ //document.getElementById("tbsync.newaccount.wizard").canRewind = false;
+ document.getElementById("tbsync.error").hidden = true;
+ document.getElementById("tbsync.newaccount.wizard").getButton("cancel").disabled = true;
+ document.getElementById("tbsync.newaccount.wizard").getButton("finish").disabled = true;
+ document.getElementById("tbsync.newaccount.name").disabled = true;
+ document.getElementById("tbsync.newaccount.user").disabled = true;
+ document.getElementById("tbsync.newaccount.password").disabled = true;
+ document.getElementById("tbsync.newaccount.servertype").disabled = true;
+
+ tbSyncEasNewAccount.startTime = Date.now();
+ tbSyncEasNewAccount.updateAutodiscoverStatus();
+ document.getElementById("tbsync.spinner").hidden = false;
+
+ //do autodiscover
+ if (servertype == "office365" || servertype == "auto") {
+ let updateTimer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
+ updateTimer.initWithCallback({notify : function () {tbSyncEasNewAccount.updateAutodiscoverStatus()}}, 1000, 3);
+
+ if (servertype == "office365") {
+ let v2 = await eas.network.getServerConnectionViaAutodiscoverV2JsonRequest("https://autodiscover-s.outlook.com/autodiscover/autodiscover.json?Email="+encodeURIComponent(user)+"&Protocol=ActiveSync");
+ let oauthData = eas.network.getOAuthObj({ host: v2.server, user, accountname, servertype });
+ if (oauthData) {
+ // ask for token
+ document.getElementById("tbsync.spinner").hidden = true;
+ let _rv = {};
+ if (await oauthData.asyncConnect(_rv)) {
+ password = _rv.tokens;
+ } else {
+ error = TbSync.getString("status." + _rv.error, "eas");
+ }
+ document.getElementById("tbsync.spinner").hidden = false;
+ url=v2.server;
+ } else {
+ error = TbSync.getString("status.404", "eas");
+ }
+ } else {
+ let result = await eas.network.getServerConnectionViaAutodiscover(user, password, tbSyncEasNewAccount.maxTimeout*1000);
+ if (result.server) {
+ user = result.user;
+ url = result.server;
+ } else {
+ error = result.error; // is a localized string
+ }
+ }
+
+ updateTimer.cancel();
+ }
+
+ //now validate the information
+ if (!error) {
+ if (!password) error = TbSync.getString("status.401", "eas");
+ }
+
+ //add if valid
+ if (!error) {
+ tbSyncEasNewAccount.addAccount(user, password, servertype, accountname, url);
+ }
+
+ //end validation
+ document.getElementById("tbsync.newaccount.name").disabled = false;
+ document.getElementById("tbsync.newaccount.user").disabled = false;
+ document.getElementById("tbsync.newaccount.password").disabled = false;
+ document.getElementById("tbsync.newaccount.servertype").disabled = false;
+ document.getElementById("tbsync.newaccount.wizard").getButton("cancel").disabled = false;
+ document.getElementById("tbsync.newaccount.wizard").getButton("finish").disabled = false;
+ document.getElementById("tbsync.spinner").hidden = true;
+ //document.getElementById("tbsync.newaccount.wizard").canRewind = true;
+
+ this.validating = false;
+
+ //close wizard, if done
+ if (!error) {
+ document.getElementById("tbsync.newaccount.wizard").cancel();
+ } else {
+ document.getElementById("tbsync.error.message").textContent = error;
+ document.getElementById("tbsync.error").hidden = false;
+ }
+ },
+
+ updateAutodiscoverStatus: function () {
+ let offset = Math.round(((Date.now() - tbSyncEasNewAccount.startTime)/1000));
+ let timeout = (offset>2) ? " (" + (tbSyncEasNewAccount.maxTimeout - offset) + ")" : "";
+
+ document.getElementById('tbsync.newaccount.autodiscoverstatus').value = TbSync.getString("autodiscover.Querying","eas") + timeout;
+ },
+
+ addAccount (user, password, servertype, accountname, url) {
+ let newAccountEntry = this.providerData.getDefaultAccountEntries();
+ newAccountEntry.user = user;
+ newAccountEntry.servertype = servertype;
+
+ if (url) {
+ //if no protocoll is given, prepend "https://"
+ if (url.substring(0,4) != "http" || url.indexOf("://") == -1) url = "https://" + url.split("://").join("/");
+ newAccountEntry.host = eas.network.stripAutodiscoverUrl(url);
+ newAccountEntry.https = (url.substring(0,5) == "https");
+ }
+
+ // Add the new account.
+ let newAccountData = this.providerData.addAccount(accountname, newAccountEntry);
+ eas.network.getAuthData(newAccountData).updateLoginData(user, password);
+
+ window.close();
+ }
+};
diff --git a/content/manager/createAccount.xhtml b/content/manager/createAccount.xhtml
new file mode 100644
index 0000000..73acf13
--- /dev/null
+++ b/content/manager/createAccount.xhtml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="tbSyncEasNewAccount.onLoad();"
+ onunload="tbSyncEasNewAccount.onUnload();"
+ onclose="return tbSyncEasNewAccount.onClose()" >
+
+ <linkset>
+ <html:link rel="localization" href="toolkit/global/wizard.ftl"/>
+ </linkset>
+
+ <script type="application/javascript" src="chrome://eas4tbsync/content/manager/createAccount.js"/>
+ <script type="application/javascript" src="chrome://eas4tbsync/content/locales.js"/>
+
+ <wizard
+ title="__EAS4TBSYNCMSG_add.title__"
+ id="tbsync.newaccount.wizard">
+
+ <wizardpage onFirstPage="true" label="__EAS4TBSYNCMSG_add.shortdescription__">
+ <description style="width: 350px">__EAS4TBSYNCMSG_add.description__</description>
+
+ <richlistbox height="250" id="tbsync.newaccount.servertype" seltype="single" style="margin-top:1ex" onselect="tbSyncEasNewAccount.onUserDropdown();">
+ <richlistitem value="auto" style="padding: 4px">
+ <vbox><image src="chrome://eas4tbsync/content/skin/eas32.png" style="margin:1ex" /></vbox>
+ <vbox flex="1">
+ <label class="header" value="__EAS4TBSYNCMSG_servertype.auto__" />
+ <description>__EAS4TBSYNCMSG_servertype.description.auto__</description>
+ </vbox>
+ </richlistitem>
+
+ <richlistitem value="custom" style="padding: 4px">
+ <vbox><image src="chrome://eas4tbsync/content/skin/eas32.png" style="margin:1ex" /></vbox>
+ <vbox flex="1">
+ <label class="header" value="__EAS4TBSYNCMSG_servertype.custom__" />
+ <description>__EAS4TBSYNCMSG_servertype.description.custom__</description>
+ </vbox>
+ </richlistitem>
+
+ <richlistitem value="office365" style="padding: 4px">
+ <vbox><image src="chrome://eas4tbsync/content/skin/365_32.png" style="margin:1ex" /></vbox>
+ <vbox flex="1">
+ <label class="header" value="__EAS4TBSYNCMSG_servertype.office365__" />
+ <description>__EAS4TBSYNCMSG_servertype.description.office365__</description>
+ </vbox>
+ </richlistitem>
+ </richlistbox>
+
+ <html:table style="margin-top:1em">
+ <html:tr>
+ <html:td width="33%"><vbox pack="center"><label value="__EAS4TBSYNCMSG_add.name__" /></vbox></html:td>
+ <html:td width="67%"><html:input id="tbsync.newaccount.name" oninput="tbSyncEasNewAccount.onUserTextInput();"/></html:td>
+ </html:tr>
+ <html:tr id="tbsync.newaccount.user.box">
+ <html:td><vbox pack="center"><label value="__EAS4TBSYNCMSG_add.user__" /></vbox></html:td>
+ <html:td><html:input id="tbsync.newaccount.user" oninput="tbSyncEasNewAccount.onUserTextInput();"/></html:td>
+ </html:tr>
+ <html:tr id="tbsync.newaccount.password.box">
+ <html:td><vbox pack="center"><label value="__EAS4TBSYNCMSG_add.password__" /></vbox></html:td>
+ <html:td><html:input id="tbsync.newaccount.password" type="password" oninput="tbSyncEasNewAccount.onUserTextInput();"/></html:td>
+ </html:tr>
+ <html:tr id="tbsync.newaccount.url.box">
+ <html:td><vbox pack="center"><label value="__EAS4TBSYNCMSG_add.url__" /></vbox></html:td>
+ <html:td><html:input id="tbsync.newaccount.url" oninput="tbSyncEasNewAccount.onUserTextInput();" tooltiptext="__EAS4TBSYNCMSG_add.urldescription__"/></html:td>
+ </html:tr>
+
+ <!--html:tr style="height:40px; margin-top:1ex;margin-bottom:1ex;">
+ <html:td><vbox pack="center"></vbox></html:td>
+ <html:td><vbox pack="center"></vbox></html:td>
+ </html:tr-->
+ </html:table>
+
+ <vbox flex="1">
+ </vbox>
+ <hbox id="tbsync.spinner">
+ <image src="chrome://tbsync/content/skin/spinner.gif" style="margin-left:1em" width="16" height="16"/>
+ <label id='tbsync.newaccount.autodiscoverstatus' value="" />
+ </hbox>
+
+ <vbox id="tbsync.error" style="width: 450px;">
+ <description id="tbsync.error.message" flex="1" style="font-weight: bold;"></description>
+ <vbox>
+ <button
+ id="tbsync.error.link"
+ label="__EAS4TBSYNCMSG_manager.ShowEventLog__"
+ oncommand="TbSync.eventlog.open();"/>
+ </vbox>
+ </vbox>
+
+
+ </wizardpage>
+
+ </wizard>
+
+</window>
diff --git a/content/manager/editAccountOverlay.js b/content/manager/editAccountOverlay.js
new file mode 100644
index 0000000..7fdc18f
--- /dev/null
+++ b/content/manager/editAccountOverlay.js
@@ -0,0 +1,56 @@
+/*
+ * 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";
+
+const eas = TbSync.providers.eas;
+
+var tbSyncEditAccountOverlay = {
+
+ onload: function (window, accountData) {
+ this.accountData = accountData;
+
+ // special treatment for configuration label, which is a permanent setting and will not change by switching modes
+ let configlabel = window.document.getElementById("tbsync.accountsettings.label.config");
+ if (configlabel) {
+ configlabel.setAttribute("value", TbSync.getString("config.custom", "eas"));
+ }
+ },
+
+ stripHost: function (document) {
+ let host = document.getElementById('tbsync.AccountPropertys.pref.host').value;
+ if (host.indexOf("https://") == 0) {
+ host = host.replace("https://","");
+ document.getElementById('tbsync.AccountPropertys.pref.https').checked = true;
+ this.accountData.setAccountProperty("https", true);
+ } else if (host.indexOf("http://") == 0) {
+ host = host.replace("http://","");
+ document.getElementById('tbsync.AccountPropertys.pref.https').checked = false;
+ this.accountData.setAccountProperty("https", false);
+ }
+
+ while (host.endsWith("/")) { host = host.slice(0,-1); }
+ document.getElementById('tbsync.AccountPropertys.pref.host').value = host
+ this.accountData.setAccountProperty("host", host);
+ },
+
+ deleteFolder: function() {
+ let folderList = document.getElementById("tbsync.accountsettings.folderlist");
+ if (folderList.selectedItem !== null && !folderList.disabled) {
+ let folderData = folderList.selectedItem.folderData;
+
+ //only trashed folders can be purged (for example O365 does not show deleted folders but also does not allow to purge them)
+ if (!eas.tools.parentIsTrash(folderData)) return;
+
+ if (folderData.getFolderProperty("selected")) window.alert(TbSync.getString("deletefolder.notallowed::" + folderData.getFolderProperty("foldername"), "eas"));
+ else if (window.confirm(TbSync.getString("deletefolder.confirm::" + folderData.getFolderProperty("foldername"), "eas"))) {
+ folderData.sync({syncList: false, syncJob: "deletefolder"});
+ }
+ }
+ }
+};
diff --git a/content/manager/editAccountOverlay.xhtml b/content/manager/editAccountOverlay.xhtml
new file mode 100644
index 0000000..dc37731
--- /dev/null
+++ b/content/manager/editAccountOverlay.xhtml
@@ -0,0 +1,154 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://tbsync/content/skin/fix_dropdown_1534697.css" type="text/css"?>
+
+<overlay
+ id="tbSyncAccountConfig"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <script type="application/javascript" src="chrome://eas4tbsync/content/manager/editAccountOverlay.js"/>
+ <script type="application/javascript" src="chrome://eas4tbsync/content/locales.js"/>
+
+ <menuitem
+ id="TbSync.eas.FolderListContextMenuDelete"
+ label="__EAS4TBSYNCMSG_pref.ShowTrashedFolders__"
+ class="menuitem-iconic"
+ image="chrome://tbsync/content/skin/trash16.png"
+ appendto="tbsync.accountsettings.FolderListContextMenu"
+ oncommand="tbSyncEditAccountOverlay.deleteFolder();"/>
+
+ <tab id="manager.tabs.accountsettings" label="__EAS4TBSYNCMSG_manager.tabs.accountsettings__" appendto="manager.tabs" />
+ <tab id="manager.tabs.syncsettings" label="__EAS4TBSYNCMSG_manager.tabs.syncsettings__" appendto="manager.tabs" />
+
+ <tabpanel id="manager.tabpanels.accountsettings" appendto="manager.tabpanels" flex="1" orient="vertical"><!-- ACCOUNT SETTINGS -->
+ <vbox flex="1">
+ <label class="header lockIfConnected" style="margin-left:0; margin-bottom:1ex;" value="" id="tbsync.accountsettings.label.config" />
+ <html:table flex="1">
+ <html:tr>
+ <html:td style="margin-right:1ex;">
+ <vbox pack="center">
+ <label style="text-align:left" control="tbsync.accountsettings.pref.accountname" value="__EAS4TBSYNCMSG_pref.AccountName__" />
+ </vbox>
+ </html:td>
+ <html:td width="100%">
+ <html:input id="tbsync.accountsettings.pref.accountname" />
+ </html:td>
+ </html:tr>
+
+ <html:tr>
+ <html:td style="margin-right:1ex;" >
+ <vbox pack="center">
+ <label class="lockIfConnected" style="text-align:left" control="tbsync.accountsettings.pref.user" value="__EAS4TBSYNCMSG_pref.UserName__" />
+ </vbox>
+ </html:td>
+ <html:td>
+ <html:input class="lockIfConnected" tooltiptext="__EAS4TBSYNCMSG_pref.UserNameDescription__" id="tbsync.accountsettings.pref.user" />
+ </html:td>
+ </html:tr>
+
+ <html:tr>
+ <html:td style="margin-right:1ex;" >
+ <vbox pack="center">
+ <label class="lockIfConnected" style="text-align:left" control="tbsync.accountsettings.pref.host" value="__EAS4TBSYNCMSG_pref.ServerName__" />
+ </vbox>
+ </html:td>
+ <html:td>
+ <html:input class="lockIfConnected" tooltiptext="__EAS4TBSYNCMSG_pref.ServerNameDescription__" id="tbsync.accountsettings.pref.host" onblur="tbSyncEditAccountOverlay.stripHost(document);" />
+ </html:td>
+ </html:tr>
+
+ <html:tr>
+ <html:td style="margin-right:1ex;" >
+ <hbox/>
+ </html:td>
+ <html:td>
+ <checkbox class="lockIfConnected" style="margin-bottom:1ex" id="tbsync.accountsettings.pref.https" label="__EAS4TBSYNCMSG_pref.usehttps__" />
+ </html:td>
+ </html:tr>
+
+ <html:tr>
+ <html:td style="margin-right:1ex;" >
+ <vbox id="asversion.hook" pack="center">
+ <label class="lockIfConnected" style="text-align:left" control="tbsync.accountsettings.pref.asversionselected" value="__EAS4TBSYNCMSG_pref.ActiveSyncVersion__" />
+ </vbox>
+ </html:td>
+ <html:td>
+ <menulist flex="0" id="tbsync.accountsettings.pref.asversionselected" class="lockIfConnected">
+ <menupopup id="asversion.popup">
+ <menuitem label="__EAS4TBSYNCMSG_pref.autodetect__" value="auto" />
+ <menuitem label="v2.5" value="2.5" />
+ <menuitem label="v14.0" value="14.0" />
+ </menupopup>
+ </menulist>
+ </html:td>
+ </html:tr>
+
+ <html:tr>
+ <html:td style="margin-right:1ex;" >
+ <label class="lockIfConnected" style="text-align:left" value="__EAS4TBSYNCMSG_pref.DeviceId__" />
+ </html:td>
+ <html:td>
+ <label value="" disabled="true" id="tbsync.accountsettings.pref.deviceId" />
+ </html:td>
+ </html:tr>
+ </html:table>
+
+ <vbox flex="1" />
+ <vbox class="showIfConnected">
+ <hbox>
+ <vbox pack="center"><image src="chrome://tbsync/content/skin/info16.png" /></vbox>
+ <description flex="1">__EAS4TBSYNCMSG_manager.lockedsettings.description__</description>
+ </hbox>
+ </vbox>
+
+ </vbox>
+ </tabpanel>
+
+ <tabpanel id="manager.tabpanels.syncsettings" appendto="manager.tabpanels" flex="1" orient="vertical"><!-- SYNC SETTINGS -->
+ <vbox flex="1">
+ <label style="margin-left:0; margin-bottom: 1ex;" class="header lockIfConnected" value="__EAS4TBSYNCMSG_pref.generaloptions__"/>
+ <vbox>
+ <checkbox class="lockIfConnected" id="tbsync.accountsettings.pref.provision" label="__EAS4TBSYNCMSG_pref.provision__" />
+ </vbox>
+
+ <label style="margin-left:0; margin-bottom: 1ex; margin-top: 3ex" class="header lockIfConnected" value="__EAS4TBSYNCMSG_pref.contactoptions__"/>
+ <vbox>
+ <checkbox class="lockIfConnected" id="tbsync.accountsettings.pref.displayoverride" label="__EAS4TBSYNCMSG_pref.displayoverride__" />
+ <hbox align="center">
+ <description id="separator.hook" class="lockIfConnected" flex="0">__EAS4TBSYNCMSG_pref.seperator.description__</description>
+ <menulist id="tbsync.accountsettings.pref.seperator" class="lockIfConnected">
+ <menupopup id="separator.popup">
+ <menuitem label="__EAS4TBSYNCMSG_pref.seperator.linebreak__" value="10" />
+ <menuitem label="__EAS4TBSYNCMSG_pref.seperator.comma__" value="44" />
+ </menupopup>
+ </menulist>
+ </hbox>
+ </vbox>
+
+ <label style="margin-left:0; margin-bottom: 1ex; margin-top: 3ex" class="header lockIfConnected" value="__EAS4TBSYNCMSG_pref.calendaroptions__"/>
+ <vbox>
+ <hbox align="center">
+ <description id="synclimit.hook" class="lockIfConnected" flex="0">__EAS4TBSYNCMSG_pref.synclimit.description__</description>
+ <menulist id="tbsync.accountsettings.pref.synclimit" class="lockIfConnected">
+ <menupopup id="synclimit.popup">
+ <menuitem label="__EAS4TBSYNCMSG_pref.synclimit.all__" value="0" />
+ <menuitem label="__EAS4TBSYNCMSG_pref.synclimit.2weeks__" value="4" />
+ <menuitem label="__EAS4TBSYNCMSG_pref.synclimit.1month__" value="5" />
+ <menuitem label="__EAS4TBSYNCMSG_pref.synclimit.3month__" value="6" />
+ <menuitem label="__EAS4TBSYNCMSG_pref.synclimit.6month__" value="7" />
+ </menupopup>
+ </menulist>
+ </hbox>
+ </vbox>
+
+
+ <vbox flex="1" />
+ <vbox class="showIfConnected">
+ <hbox>
+ <vbox pack="center"><image src="chrome://tbsync/content/skin/info16.png" /></vbox>
+ <description flex="1">__EAS4TBSYNCMSG_manager.lockedsettings.description__</description>
+ </hbox>
+ </vbox>
+ </vbox>
+ </tabpanel>
+
+</overlay>
diff --git a/content/provider.js b/content/provider.js
new file mode 100644
index 0000000..de5af36
--- /dev/null
+++ b/content/provider.js
@@ -0,0 +1,729 @@
+/*
+ * 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";
+
+// Every object in here will be loaded into TbSync.providers.<providername>.
+const eas = TbSync.providers.eas;
+
+eas.prefs = Services.prefs.getBranch("extensions.eas4tbsync.");
+
+//use flags instead of strings to avoid errors due to spelling errors
+eas.flags = Object.freeze({
+ allowEmptyResponse: true,
+});
+
+eas.windowsToIanaTimezoneMap = {};
+eas.ianaToWindowsTimezoneMap = {};
+eas.cachedTimezoneData = null;
+eas.defaultTimezoneInfo = null;
+eas.defaultTimezone = null;
+eas.utcTimezone = null;
+
+
+/**
+ * Implementing the TbSync interface for external provider extensions.
+ */
+var Base = class {
+ /**
+ * Called during load of external provider extension to init provider.
+ */
+ static async load() {
+ // Set default prefs
+ let branch = Services.prefs.getDefaultBranch("extensions.eas4tbsync.");
+ branch.setIntPref("timeout", 90000);
+ branch.setIntPref("maxitems", 50);
+ branch.setBoolPref("msTodoCompat", false);
+ branch.setCharPref("clientID.type", "TbSync");
+ branch.setCharPref("clientID.useragent", "Thunderbird ActiveSync");
+ branch.setCharPref("oauth.clientID", "");
+
+ eas.defaultTimezone = null;
+ eas.utcTimezone = null;
+ eas.defaultTimezoneInfo = null;
+ eas.windowsToIanaTimezoneMap = {};
+ eas.openWindows = {};
+
+ try {
+ // Create a basic error info (no accountname or foldername, just the provider)
+ let eventLogInfo = new TbSync.EventLogInfo("eas");
+
+ //get timezone info of default timezone (old cal. without dtz are depricated)
+ eas.defaultTimezone = (TbSync.lightning.cal.dtz && TbSync.lightning.cal.dtz.defaultTimezone) ? TbSync.lightning.cal.dtz.defaultTimezone : TbSync.lightning.cal.calendarDefaultTimezone();
+ eas.utcTimezone = (TbSync.lightning.cal.dtz && TbSync.lightning.cal.dtz.UTC) ? TbSync.lightning.cal.dtz.UTC : TbSync.lightning.cal.UTC();
+ if (eas.defaultTimezone && eas.defaultTimezone.icalComponent) {
+ TbSync.eventlog.add("info", eventLogInfo, "Default timezone has been found.");
+ } else {
+ TbSync.eventlog.add("info", eventLogInfo, "Default timezone is not defined, using UTC!");
+ eas.defaultTimezone = eas.utcTimezone;
+ }
+
+ eas.defaultTimezoneInfo = eas.tools.getTimezoneInfo(eas.defaultTimezone);
+ if (!eas.defaultTimezoneInfo) {
+ TbSync.eventlog.add("info", eventLogInfo, "Could not create defaultTimezoneInfo");
+ }
+
+ //get windows timezone data from CSV
+ let aliasData = await eas.tools.fetchFile("chrome://eas4tbsync/content/timezonedata/Aliases.csv");
+ let aliasNames = {};
+ for (let i = 0; i<aliasData.length; i++) {
+ let lData = aliasData[i].split(",");
+ if (lData.length<2) continue;
+ aliasNames[lData[0].toString().trim()] = lData[1].toString().trim().split(" ");
+ }
+
+ let csvData = await eas.tools.fetchFile("chrome://eas4tbsync/content/timezonedata/WindowsTimezone.csv");
+ for (let i = 0; i<csvData.length; i++) {
+ let lData = csvData[i].split(",");
+ if (lData.length<3) continue;
+
+ let windowsZoneName = lData[0].toString().trim();
+ let zoneType = lData[1].toString().trim();
+ let ianaZoneName = lData[2].toString().trim();
+
+ if (zoneType == "001") eas.windowsToIanaTimezoneMap[windowsZoneName] = ianaZoneName;
+ if (ianaZoneName == eas.defaultTimezoneInfo.std.id) eas.defaultTimezoneInfo.std.windowsZoneName = windowsZoneName;
+
+ // build the revers map as well, which is many-to-one, grap iana aliases from the csvData and from the aliasData
+ // 1. multiple iana zones map to the same windows zone
+ let ianaZones = ianaZoneName.split(" ");
+ for (let ianaZone of ianaZones) {
+ eas.ianaToWindowsTimezoneMap[ianaZone] = windowsZoneName;
+ if (aliasNames.hasOwnProperty(ianaZone)) {
+ for (let aliasName of aliasNames[ianaZone]) {
+ // 2. multiple iana zonescan be an alias to a main iana zone
+ eas.ianaToWindowsTimezoneMap[aliasName] = windowsZoneName;
+ }
+ }
+ }
+ }
+
+ let tzService = TbSync.lightning.cal.timezoneService;
+ for (let timezoneId of tzService.timezoneIds) {
+ if (!eas.ianaToWindowsTimezoneMap[timezoneId]) {
+ TbSync.eventlog.add("info", eventLogInfo, "The IANA timezone <"+timezoneId+"> cannot be mapped to any Exchange timezone.");
+ }
+ }
+
+ //If an EAS calendar is currently NOT associated with an email identity, try to associate,
+ //but do not change any explicitly set association
+ // - A) find email identity and associate (which sets organizer to that user identity)
+ // - B) overwrite default organizer with current best guess
+ //TODO: Do this after email accounts changed, not only on restart?
+ let providerData = new TbSync.ProviderData("eas");
+ let folders = providerData.getFolders({"selected": true, "type": ["8","13"]});
+ for (let folder of folders) {
+ let manager = TbSync.lightning.cal.manager;
+ let calendar = manager.getCalendarById(folder.getFolderProperty("target"));
+ if (calendar && calendar.getProperty("imip.identity.key") == "") {
+ //is there an email identity for this eas account?
+ let authData = eas.network.getAuthData(folder.accountData);
+
+ let key = eas.tools.getIdentityKey(authData.user);
+ if (key === "") { //TODO: Do this even after manually switching to NONE, not only on restart?
+ //set transient calendar organizer settings based on current best guess and
+ calendar.setProperty("organizerId", TbSync.lightning.cal.email.prependMailTo(authData.user));
+ calendar.setProperty("organizerCN", calendar.getProperty("fallbackOrganizerName"));
+ } else {
+ //force switch to found identity
+ calendar.setProperty("imip.identity.key", key);
+ }
+ }
+ }
+ } catch(e) {
+ Components.utils.reportError(e);
+ }
+ }
+
+
+ /**
+ * Called during unload of external provider extension to unload provider.
+ */
+ static async unload() {
+ // Close all open windows of this provider.
+ for (let id in eas.openWindows) {
+ if (eas.openWindows.hasOwnProperty(id)) {
+ try {
+ eas.openWindows[id].close();
+ } catch(e) {
+ //NOOP
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Returns string for the name of provider for the add account menu.
+ */
+ static getProviderName() {
+ return "Exchange ActiveSync";
+ }
+
+
+ /**
+ * Returns version of the TbSync API this provider is using
+ */
+ static getApiVersion() { return "2.5"; }
+
+
+ /**
+ * Returns location of a provider icon.
+ */
+ static getProviderIcon(size, accountData = null) {
+ let base = (accountData && accountData.getAccountProperty("servertype") == "office365") ? "365_" : "eas";
+
+ switch (size) {
+ case 16:
+ return "chrome://eas4tbsync/content/skin/" + base + "16.png";
+ case 32:
+ return "chrome://eas4tbsync/content/skin/" + base + "32.png";
+ default :
+ return "chrome://eas4tbsync/content/skin/" + base + "64.png";
+ }
+ }
+
+
+ /**
+ * Returns a list of sponsors, they will be sorted by the index
+ */
+ static getSponsors() {
+ return {
+ "Schiessl, Michael 1" : {name: "Michael Schiessl", description: "Tine 2.0", icon: "", link: "" },
+ "Schiessl, Michael 2" : {name: "Michael Schiessl", description: " Exchange 2007", icon: "", link: "" },
+ "netcup GmbH" : {name: "netcup GmbH", description : "SOGo", icon: "chrome://eas4tbsync/content/skin/sponsors/netcup.png", link: "http://www.netcup.de/" },
+ "nethinks GmbH" : {name: "nethinks GmbH", description : "Zarafa", icon: "chrome://eas4tbsync/content/skin/sponsors/nethinks.png", link: "http://www.nethinks.com/" },
+ "Jau, Stephan" : {name: "Stephan Jau", description: "Horde", icon: "", link: "" },
+ "Zavar " : {name: "Zavar", description: "Zoho", icon: "", link: "" },
+ };
+ }
+
+
+ /**
+ * Returns the url of a page with details about contributors (used in the manager UI)
+ */
+ static getContributorsUrl() {
+ return "https://github.com/jobisoft/EAS-4-TbSync/blob/master/CONTRIBUTORS.md";
+ }
+
+
+ /**
+ * Returns the email address of the maintainer (used for bug reports).
+ */
+ static getMaintainerEmail() {
+ return "john.bieling@gmx.de";
+ }
+
+
+ /**
+ * Returns URL of the new account window.
+ *
+ * The URL will be opened via openDialog(), when the user wants to create a
+ * new account of this provider.
+ */
+ static getCreateAccountWindowUrl() {
+ return "chrome://eas4tbsync/content/manager/createAccount.xhtml";
+ }
+
+
+ /**
+ * Returns overlay XUL URL of the edit account dialog
+ * (chrome://tbsync/content/manager/editAccount.xhtml)
+ */
+ static getEditAccountOverlayUrl() {
+ return "chrome://eas4tbsync/content/manager/editAccountOverlay.xhtml";
+ }
+
+
+ /**
+ * Return object which contains all possible fields of a row in the
+ * accounts database with the default value if not yet stored in the
+ * database.
+ */
+ static getDefaultAccountEntries() {
+ let row = {
+ "policykey" : "0",
+ "foldersynckey" : "0",
+ "deviceId" : eas.tools.getNewDeviceId(),
+ "asversionselected" : "auto",
+ "asversion" : "",
+ "host" : "",
+ "user" : "",
+ "servertype" : "",
+ "seperator" : "10",
+ "https" : true,
+ "provision" : false,
+ "displayoverride" : false,
+ "lastEasOptionsUpdate":"0",
+ "allowedEasVersions": "",
+ "allowedEasCommands": "",
+ "useragent": eas.prefs.getCharPref("clientID.useragent"),
+ "devicetype": eas.prefs.getCharPref("clientID.type"),
+ "synclimit" : "7",
+ };
+ return row;
+ }
+
+
+ /**
+ * Return object which contains all possible fields of a row in the folder
+ * database with the default value if not yet stored in the database.
+ */
+ static getDefaultFolderEntries() {
+ let folder = {
+ "type" : "",
+ "synckey" : "",
+ "target" : "",
+ "targetColor" : "",
+ "targetName" : "",
+ "parentID" : "0",
+ "serverID" : "",
+ };
+ return folder;
+ }
+
+
+ /**
+ * Is called everytime an account of this provider is enabled in the
+ * manager UI.
+ */
+ static onEnableAccount(accountData) {
+ accountData.resetAccountProperty("policykey");
+ accountData.resetAccountProperty("foldersynckey");
+ accountData.resetAccountProperty("lastEasOptionsUpdate");
+ accountData.resetAccountProperty("lastsynctime");
+ }
+
+
+ /**
+ * Is called everytime an account of this provider is disabled in the
+ * manager UI.
+ *
+ * @param accountData [in] AccountData
+ */
+ static onDisableAccount(accountData) {
+ }
+
+
+ /**
+ * Is called everytime an account of this provider is deleted in the
+ * manager UI.
+ */
+ static onDeleteAccount(accountData) {
+ eas.network.getAuthData(accountData).removeLoginData();
+ }
+
+
+ /**
+ * Returns all folders of the account, sorted in the desired order.
+ * The most simple implementation is to return accountData.getAllFolders();
+ */
+ static getSortedFolders(accountData) {
+ let allowedTypesOrder = ["9","14","8","13","7","15"];
+
+ function getIdChain (aServerID) {
+ let serverID = aServerID;
+ let chain = [];
+ let folder;
+ let rootType = "";
+
+ // create sort string so that child folders are directly below their parent folders
+ do {
+ folder = accountData.getFolder("serverID", serverID);
+ if (folder) {
+ chain.unshift(folder.getFolderProperty("foldername"));
+ serverID = folder.getFolderProperty("parentID");
+ rootType = folder.getFolderProperty("type");
+ }
+ } while (folder && serverID != "0")
+
+ // different folder types are grouped and trashed folders at the end
+ let pos = allowedTypesOrder.indexOf(rootType);
+ chain.unshift(pos == -1 ? "ZZZ" : pos.toString().padStart(3,"0"));
+
+ return chain.join(".");
+ };
+
+ let toBeSorted = [];
+ let folders = accountData.getAllFolders();
+ for (let f of folders) {
+ if (!allowedTypesOrder.includes(f.getFolderProperty("type"))) {
+ continue;
+ }
+ toBeSorted.push({"key": getIdChain(f.getFolderProperty("serverID")), "folder": f});
+ }
+
+ //sort
+ toBeSorted.sort(function(a,b) {
+ return a.key > b.key;
+ });
+
+ let sortedFolders = [];
+ for (let sortObj of toBeSorted) {
+ sortedFolders.push(sortObj.folder);
+ }
+ return sortedFolders;
+ }
+
+
+ /**
+ * Return the connection timeout for an active sync, so TbSync can append
+ * a countdown to the connection timeout, while waiting for an answer from
+ * the server. Only syncstates which start with "send." will trigger this.
+ */
+ static getConnectionTimeout(accountData) {
+ return eas.prefs.getIntPref("timeout");
+ }
+
+
+ /**
+ * Is called if TbSync needs to synchronize the folder list.
+ */
+ static async syncFolderList(syncData, syncJob, syncRunNr) {
+ // Recommendation: Put the actual function call inside a try catch, to
+ // ensure returning a proper StatusData object, regardless of what
+ // happens inside that function. You may also throw custom errors
+ // in that function, which have the StatusData obj attached, which
+ // should be returned.
+
+ try {
+ await eas.sync.folderList(syncData);
+ } catch (e) {
+ if (e.name == "eas4tbsync") {
+ return e.statusData;
+ } else {
+ Components.utils.reportError(e);
+ // re-throw any other error and let TbSync handle it
+ throw (e);
+ }
+ }
+
+ // Fall through, if there was no error.
+ return new TbSync.StatusData();
+ }
+
+
+ /**
+ * Is called if TbSync needs to synchronize a folder.
+ */
+ static async syncFolder(syncData, syncJob, syncRunNr) {
+ // Recommendation: Put the actual function call inside a try catch, to
+ // ensure returning a proper StatusData object, regardless of what
+ // happens inside that function. You may also throw custom errors
+ // in that function, which have the StatusData obj attached, which
+ // should be returned.
+
+ try {
+ switch (syncJob) {
+ case "deletefolder":
+ await eas.sync.deleteFolder(syncData);
+ break;
+ default:
+ await eas.sync.singleFolder(syncData);
+ }
+ } catch (e) {
+ if (e.name == "eas4tbsync") {
+ return e.statusData;
+ } else {
+ Components.utils.reportError(e);
+ // re-throw any other error and let TbSync handle it
+ throw (e);
+ }
+ }
+
+ // Fall through, if there was no error.
+ return new TbSync.StatusData();
+ }
+
+
+ /**
+ * Return the custom OAuth2 ClientID.
+ */
+ static getCustomeOauthClientID() {
+ return eas.prefs.getCharPref("oauth.clientID");
+ }
+}
+
+
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// * TargetData implementation
+// * Using TbSyncs advanced address book TargetData
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+var TargetData_addressbook = class extends TbSync.addressbook.AdvancedTargetData {
+ constructor(folderData) {
+ super(folderData);
+ }
+
+ get primaryKeyField() {
+ return "X-EAS-SERVERID";
+ }
+
+ generatePrimaryKey() {
+ return TbSync.generateUUID();
+ }
+
+ // enable or disable changelog
+ get logUserChanges() {
+ return true;
+ }
+
+ directoryObserver(aTopic) {
+ switch (aTopic) {
+ case "addrbook-removed":
+ case "addrbook-updated":
+ //Services.console.logStringMessage("["+ aTopic + "] " + this.folderData.getFolderProperty("foldername"));
+ break;
+ }
+ }
+
+ cardObserver(aTopic, abCardItem) {
+ switch (aTopic) {
+ case "addrbook-contact-updated":
+ case "addrbook-contact-removed":
+ //Services.console.logStringMessage("["+ aTopic + "] " + abCardItem.getProperty("DisplayName"));
+ break;
+
+ case "addrbook-contact-created":
+ {
+ //Services.console.logStringMessage("["+ aTopic + "] "+ abCardItem.getProperty("DisplayName")+">");
+ break;
+ }
+ }
+ }
+
+ listObserver(aTopic, abListItem, abListMember) {
+ switch (aTopic) {
+ case "addrbook-list-member-added":
+ case "addrbook-list-member-removed":
+ //Services.console.logStringMessage("["+ aTopic + "] MemberName: " + abListMember.getProperty("DisplayName"));
+ break;
+
+ case "addrbook-list-removed":
+ case "addrbook-list-updated":
+ //Services.console.logStringMessage("["+ aTopic + "] ListName: " + abListItem.getProperty("ListName"));
+ break;
+
+ case "addrbook-list-created":
+ //Services.console.logStringMessage("["+ aTopic + "] ListName: "+abListItem.getProperty("ListName")+">");
+ break;
+ }
+ }
+
+ async createAddressbook(newname) {
+ // https://searchfox.org/comm-central/source/mailnews/addrbook/src/nsDirPrefs.h
+ let dirPrefId = MailServices.ab.newAddressBook(newname, "", 101);
+ let directory = MailServices.ab.getDirectoryFromId(dirPrefId);
+
+ eas.sync.resetFolderSyncInfo(this.folderData);
+
+ if (directory && directory instanceof Components.interfaces.nsIAbDirectory && directory.dirPrefId == dirPrefId) {
+ directory.setStringValue("tbSyncIcon", "eas" + (this.folderData.accountData.getAccountProperty("servertype") == "office365" ? "_365" : ""));
+ return directory;
+ }
+ return null;
+ }
+}
+
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// * TargetData implementation
+// * Using TbSyncs advanced calendar TargetData
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+var TargetData_calendar = class extends TbSync.lightning.AdvancedTargetData {
+ constructor(folderData) {
+ super(folderData);
+ }
+
+ // The calendar target does not support a custom primaryKeyField, because
+ // the lightning implementation only allows to search for items via UID.
+ // Like the addressbook target, the calendar target item element has a
+ // primaryKey getter/setter which - however - only works on the UID.
+
+ // enable or disable changelog
+ get logUserChanges() {
+ return true;
+ }
+
+ calendarObserver(aTopic, tbCalendar, aPropertyName, aPropertyValue, aOldPropertyValue) {
+ switch (aTopic) {
+ case "onCalendarPropertyChanged":
+ //Services.console.logStringMessage("["+ aTopic + "] " + tbCalendar.calendar.name + " : " + aPropertyName);
+ break;
+
+ case "onCalendarDeleted":
+ case "onCalendarPropertyDeleted":
+ //Services.console.logStringMessage("["+ aTopic + "] " +tbCalendar.calendar.name);
+ break;
+ }
+ }
+
+ itemObserver(aTopic, tbItem, tbOldItem) {
+ switch (aTopic) {
+ case "onAddItem":
+ case "onModifyItem":
+ case "onDeleteItem":
+ //Services.console.logStringMessage("["+ aTopic + "] " + tbItem.nativeItem.title);
+ break;
+ }
+ }
+
+ async createCalendar(newname) {
+ let calManager = TbSync.lightning.cal.manager;
+
+ //Create the new standard calendar with a unique name
+ let newCalendar = calManager.createCalendar("storage", Services.io.newURI("moz-storage-calendar://"));
+ newCalendar.id = TbSync.lightning.cal.getUUID();
+ newCalendar.name = newname;
+
+ eas.sync.resetFolderSyncInfo(this.folderData);
+
+ newCalendar.setProperty("color", this.folderData.getFolderProperty("targetColor"));
+ newCalendar.setProperty("relaxedMode", true); //sometimes we get "generation too old for modifyItem", check can be disabled with relaxedMode
+ // removed in TB78, as it seems to not fully enable the calendar, if present before registering
+ // https://searchfox.org/comm-central/source/calendar/base/content/calendar-management.js#385
+ //newCalendar.setProperty("calendar-main-in-composite",true);
+ newCalendar.setProperty("readOnly", this.folderData.getFolderProperty("downloadonly"));
+
+ switch (this.folderData.getFolderProperty("type")) {
+ case "8": //event
+ case "13":
+ newCalendar.setProperty("capabilities.tasks.supported", false);
+ newCalendar.setProperty("capabilities.events.supported", true);
+ break;
+ case "7": //todo
+ case "15":
+ newCalendar.setProperty("capabilities.tasks.supported", true);
+ newCalendar.setProperty("capabilities.events.supported", false);
+ break;
+ default:
+ newCalendar.setProperty("capabilities.tasks.supported", false);
+ newCalendar.setProperty("capabilities.events.supported", false);
+ }
+
+ calManager.registerCalendar(newCalendar);
+
+ let authData = eas.network.getAuthData(this.folderData.accountData);
+
+ //is there an email identity we can associate this calendar to?
+ //getIdentityKey returns "" if none found, which removes any association
+ let key = eas.tools.getIdentityKey(authData.user);
+ newCalendar.setProperty("fallbackOrganizerName", newCalendar.getProperty("organizerCN"));
+ newCalendar.setProperty("imip.identity.key", key);
+ if (key === "") {
+ //there is no matching email identity - use current default value as best guess and remove association
+ //use current best guess
+ newCalendar.setProperty("organizerCN", newCalendar.getProperty("fallbackOrganizerName"));
+ newCalendar.setProperty("organizerId", TbSync.lightning.cal.email.prependMailTo(authData.user));
+ }
+
+ return newCalendar;
+ }
+}
+
+
+
+
+
+/**
+ * This provider is implementing the StandardFolderList class instead of
+ * the FolderList class.
+ */
+var StandardFolderList = class {
+ /**
+ * Is called before the context menu of the folderlist is shown, allows to
+ * show/hide custom menu options based on selected folder. During an active
+ * sync, folderData will be null.
+ */
+ static onContextMenuShowing(window, folderData) {
+ let hideContextMenuDelete = true;
+ if (folderData !== null) {
+ //if a folder in trash is selected, also show ContextMenuDelete (but only if FolderDelete is allowed)
+ if (eas.tools.parentIsTrash(folderData) && folderData.accountData.getAccountProperty("allowedEasCommands").split(",").includes("FolderDelete")) {
+ hideContextMenuDelete = false;
+ window.document.getElementById("TbSync.eas.FolderListContextMenuDelete").label = TbSync.getString("deletefolder.menuentry::" + folderData.getFolderProperty("foldername"), "eas");
+ }
+ }
+ window.document.getElementById("TbSync.eas.FolderListContextMenuDelete").hidden = hideContextMenuDelete;
+ }
+
+
+ /**
+ * Return the icon used in the folderlist to represent the different folder
+ * types.
+ */
+ static getTypeImage(folderData) {
+ let src = "";
+ switch (folderData.getFolderProperty("type")) {
+ case "9":
+ case "14":
+ src = "contacts16.png";
+ break;
+ case "8":
+ case "13":
+ src = "calendar16.png";
+ break;
+ case "7":
+ case "15":
+ src = "todo16.png";
+ break;
+ }
+ return "chrome://tbsync/content/skin/" + src;
+ }
+
+
+ /**
+ * Return the name of the folder shown in the folderlist.
+ */
+ static getFolderDisplayName(folderData) {
+ let folderName = folderData.getFolderProperty("foldername");
+ if (eas.tools.parentIsTrash(folderData)) folderName = TbSync.getString("recyclebin", "eas") + " | " + folderName;
+ return folderName;
+ }
+
+
+ /**
+ * Return the attributes for the ACL RO (readonly menu element per folder.
+ * (label, disabled, hidden, style, ...)
+ *
+ * Return a list of attributes and their values If both (RO+RW) do
+ * not return any attributes, the ACL menu is not displayed at all.
+ */
+ static getAttributesRoAcl(folderData) {
+ return {
+ label: TbSync.getString("acl.readonly", "eas"),
+ };
+ }
+
+
+ /**
+ * Return the attributes for the ACL RW (readwrite) menu element per folder.
+ * (label, disabled, hidden, style, ...)
+ *
+ * Return a list of attributes and their values. If both (RO+RW) do
+ * not return any attributes, the ACL menu is not displayed at all.
+ */
+ static getAttributesRwAcl(folderData) {
+ return {
+ label: TbSync.getString("acl.readwrite", "eas"),
+ }
+ }
+}
+
+Services.scriptloader.loadSubScript("chrome://eas4tbsync/content/includes/network.js", this, "UTF-8");
+Services.scriptloader.loadSubScript("chrome://eas4tbsync/content/includes/wbxmltools.js", this, "UTF-8");
+Services.scriptloader.loadSubScript("chrome://eas4tbsync/content/includes/xmltools.js", this, "UTF-8");
+Services.scriptloader.loadSubScript("chrome://eas4tbsync/content/includes/tools.js", this, "UTF-8");
+Services.scriptloader.loadSubScript("chrome://eas4tbsync/content/includes/sync.js", this, "UTF-8");
+Services.scriptloader.loadSubScript("chrome://eas4tbsync/content/includes/contactsync.js", this.sync, "UTF-8");
+Services.scriptloader.loadSubScript("chrome://eas4tbsync/content/includes/calendarsync.js", this.sync, "UTF-8");
+Services.scriptloader.loadSubScript("chrome://eas4tbsync/content/includes/tasksync.js", this.sync, "UTF-8");
diff --git a/content/skin/365.svg b/content/skin/365.svg
new file mode 100644
index 0000000..e2e9273
--- /dev/null
+++ b/content/skin/365.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 53.59 64.356" width="53.589577" height="64.356148">
+ <g transform="translate(-216.07358,-549.28882)">
+ <g transform="matrix(1.8232952,0,0,1.8232952,-597.71681,-124.12247)">
+ <g transform="translate(0,-91.137241)">
+ <g fill="#eb3c00" transform="matrix(0.74069815,0,0,0.74069815,98.5698,-8.2505871)">
+ <path d="m469.87,671.03,0-28.52,25.229-9.3238,13.711,4.3877,0,38.392-13.711,4.133-25.229-9.0691,25.229,3.0361,0-33.201-16.454,3.8392,0,22.487z"/>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/content/skin/365_16.png b/content/skin/365_16.png
new file mode 100644
index 0000000..2dda541
--- /dev/null
+++ b/content/skin/365_16.png
Binary files differ
diff --git a/content/skin/365_32.png b/content/skin/365_32.png
new file mode 100644
index 0000000..8022230
--- /dev/null
+++ b/content/skin/365_32.png
Binary files differ
diff --git a/content/skin/365_48.png b/content/skin/365_48.png
new file mode 100644
index 0000000..84a664f
--- /dev/null
+++ b/content/skin/365_48.png
Binary files differ
diff --git a/content/skin/eas16.png b/content/skin/eas16.png
new file mode 100644
index 0000000..072d5f0
--- /dev/null
+++ b/content/skin/eas16.png
Binary files differ
diff --git a/content/skin/eas32.png b/content/skin/eas32.png
new file mode 100644
index 0000000..f85ff8f
--- /dev/null
+++ b/content/skin/eas32.png
Binary files differ
diff --git a/content/skin/eas64.png b/content/skin/eas64.png
new file mode 100644
index 0000000..266afa1
--- /dev/null
+++ b/content/skin/eas64.png
Binary files differ
diff --git a/content/skin/eas_300.png b/content/skin/eas_300.png
new file mode 100644
index 0000000..0aaec3a
--- /dev/null
+++ b/content/skin/eas_300.png
Binary files differ
diff --git a/content/skin/fieldset.css b/content/skin/fieldset.css
new file mode 100644
index 0000000..6a4c0e0
--- /dev/null
+++ b/content/skin/fieldset.css
@@ -0,0 +1,6 @@
+
+vbox[OS=WINNT] legend.insideTab {
+ padding-right: 5px;
+ padding-left: 5px;
+ margin-bottom:5px;
+}
diff --git a/content/skin/sponsors/netcup.png b/content/skin/sponsors/netcup.png
new file mode 100644
index 0000000..c4cff7f
--- /dev/null
+++ b/content/skin/sponsors/netcup.png
Binary files differ
diff --git a/content/skin/sponsors/nethinks.png b/content/skin/sponsors/nethinks.png
new file mode 100644
index 0000000..82356fc
--- /dev/null
+++ b/content/skin/sponsors/nethinks.png
Binary files differ
diff --git a/content/timezonedata/Aliases.csv b/content/timezonedata/Aliases.csv
new file mode 100644
index 0000000..5047c5a
--- /dev/null
+++ b/content/timezonedata/Aliases.csv
@@ -0,0 +1,115 @@
+Africa/Abidjan,Iceland Africa/Timbuktu Africa/Accra Africa/Bamako Africa/Banjul Africa/Conakry Africa/Dakar Africa/Freetown Africa/Lome Africa/Nouakchott Africa/Ouagadougou Atlantic/Reykjavik Atlantic/St_Helena
+Africa/Cairo,Egypt
+Africa/Johannesburg,Africa/Maseru Africa/Mbabane
+Africa/Lagos,Africa/Bangui Africa/Brazzaville Africa/Douala Africa/Kinshasa Africa/Libreville Africa/Luanda Africa/Malabo Africa/Niamey Africa/Porto-Novo
+Africa/Maputo,Africa/Blantyre Africa/Bujumbura Africa/Gaborone Africa/Harare Africa/Kigali Africa/Lubumbashi Africa/Lusaka
+Africa/Nairobi,Africa/Asmara Africa/Addis_Ababa Africa/Dar_es_Salaam Africa/Djibouti Africa/Kampala Africa/Mogadishu Indian/Antananarivo Indian/Comoro Indian/Mayotte Africa/Asmera
+Africa/Tripoli,Libya
+America/Adak,America/Atka US/Aleutian
+America/Anchorage,US/Alaska
+America/Argentina/Buenos_Aires,America/Buenos_Aires
+America/Argentina/Catamarca,America/Catamarca America/Argentina/ComodRivadavia
+America/Argentina/Cordoba,America/Cordoba America/Rosario
+America/Argentina/Jujuy,America/Jujuy
+America/Argentina/Mendoza,America/Mendoza
+America/Chicago,US/Central
+America/Denver,America/Shiprock Navajo US/Mountain
+America/Detroit,US/Michigan
+America/Edmonton,Canada/Mountain
+America/Halifax,Canada/Atlantic
+America/Havana,Cuba
+America/Indiana/Indianapolis,America/Fort_Wayne America/Indianapolis US/East-Indiana
+America/Indiana/Knox,America/Knox_IN US/Indiana-Starke
+America/Jamaica,Jamaica
+America/Kentucky/Louisville,America/Louisville
+America/Los_Angeles,US/Pacific US/Pacific-New
+America/Manaus,Brazil/West
+America/Mazatlan,Mexico/BajaSur
+America/Mexico_City,Mexico/General
+America/New_York,US/Eastern
+America/Noronha,Brazil/DeNoronha
+America/Nuuk,America/Godthab
+America/Panama,America/Atikokan America/Coral_Harbour America/Cayman
+America/Phoenix,US/Arizona America/Creston
+America/Puerto_Rico,America/Virgin America/Anguilla America/Antigua America/Aruba America/Curacao America/Blanc-Sablon America/Dominica America/Grenada America/Guadeloupe America/Kralendijk America/Lower_Princes America/Marigot America/Montserrat America/Port_of_Spain America/St_Barthelemy America/St_Kitts America/St_Lucia America/St_Thomas America/St_Vincent America/Tortola
+America/Regina,Canada/East-Saskatchewan Canada/Saskatchewan
+America/Rio_Branco,America/Porto_Acre Brazil/Acre
+America/Santiago,Chile/Continental
+America/Sao_Paulo,Brazil/East
+America/St_Johns,Canada/Newfoundland
+America/Tijuana,America/Ensenada Mexico/BajaNorte America/Santa_Isabel
+America/Toronto,Canada/Eastern America/Montreal America/Nassau
+America/Vancouver,Canada/Pacific
+America/Whitehorse,Canada/Yukon
+America/Winnipeg,Canada/Central
+Asia/Ashgabat,Asia/Ashkhabad
+Asia/Bangkok,Asia/Phnom_Penh Asia/Vientiane Indian/Christmas
+Asia/Dhaka,Asia/Dacca
+Asia/Dubai,Asia/Muscat Indian/Mahe Indian/Reunion
+Asia/Ho_Chi_Minh,Asia/Saigon
+Asia/Hong_Kong,Hongkong
+Asia/Jerusalem,Asia/Tel_Aviv Israel
+Asia/Kathmandu,Asia/Katmandu
+Asia/Kolkata,Asia/Calcutta
+Asia/Kuching,Asia/Brunei
+Asia/Macau,Asia/Macao
+Asia/Makassar,Asia/Ujung_Pandang
+Asia/Nicosia,Europe/Nicosia
+Asia/Qatar,Asia/Bahrain
+Asia/Riyadh,Antarctica/Syowa Asia/Aden Asia/Kuwait
+Asia/Seoul,ROK
+Asia/Shanghai,Asia/Chongqing Asia/Chungking Asia/Harbin PRC
+Asia/Singapore,Singapore Asia/Kuala_Lumpur
+Asia/Taipei,ROC
+Asia/Tehran,Iran
+Asia/Thimphu,Asia/Thimbu
+Asia/Tokyo,Japan
+Asia/Ulaanbaatar,Asia/Ulan_Bator
+Asia/Urumqi,Asia/Kashgar Antarctica/Vostok
+Asia/Yangon,Asia/Rangoon Indian/Cocos
+Atlantic/Canary,WET
+Atlantic/Faroe,Atlantic/Faeroe
+Australia/Adelaide,Australia/South
+Australia/Brisbane,Australia/Queensland
+Australia/Broken_Hill,Australia/Yancowinna
+Australia/Darwin,Australia/North
+Australia/Hobart,Australia/Tasmania Australia/Currie
+Australia/Lord_Howe,Australia/LHI
+Australia/Melbourne,Australia/Victoria
+Australia/Perth,Australia/West
+Australia/Sydney,Australia/ACT Australia/Canberra Australia/NSW
+Etc/GMT+10,HST
+Etc/GMT+5,EST
+Etc/GMT+7,MST
+Etc/UTC,Etc/GMT+0 Etc/GMT-0 Etc/GMT0 Etc/Greenwich GMT GMT+0 GMT-0 GMT0 Greenwich Etc/UCT Etc/Universal Etc/Zulu UCT UTC Universal Zulu Etc/GMT
+Europe/Belgrade,Europe/Ljubljana Europe/Podgorica Europe/Sarajevo Europe/Skopje Europe/Zagreb
+Europe/Berlin,Atlantic/Jan_Mayen Arctic/Longyearbyen Europe/Copenhagen Europe/Oslo Europe/Stockholm
+Europe/Berlin,MET
+Europe/Brussels,Europe/Amsterdam Europe/Luxembourg
+Europe/Bucharest,EET
+Europe/Chisinau,Europe/Tiraspol
+Europe/Dublin,Eire
+Europe/Helsinki,Europe/Mariehamn
+Europe/Istanbul,Asia/Istanbul Turkey
+Europe/Kyiv,Europe/Kiev
+Europe/Lisbon,Portugal
+Europe/London,Europe/Belfast GB GB-Eire Europe/Jersey Europe/Guernsey Europe/Isle_of_Man
+Europe/Moscow,W-SU
+Europe/Paris,CET
+Europe/Paris,Europe/Monaco
+Europe/Prague,Europe/Bratislava
+Europe/Rome,Europe/Vatican Europe/San_Marino
+Europe/Warsaw,Poland
+Europe/Zurich,Europe/Busingen Europe/Vaduz
+Indian/Maldives,Indian/Kerguelen
+Pacific/Auckland,Antarctica/South_Pole NZ Antarctica/McMurdo
+Pacific/Chatham,NZ-CHAT
+Pacific/Easter,Chile/EasterIsland
+Pacific/Guadalcanal,Pacific/Pohnpei Pacific/Ponape
+Pacific/Guam,Pacific/Saipan
+Pacific/Honolulu,US/Hawaii Pacific/Johnston
+Pacific/Kanton,Pacific/Enderbury
+Pacific/Kwajalein,Kwajalein
+Pacific/Pago_Pago,Pacific/Samoa US/Samoa Pacific/Midway
+Pacific/Port_Moresby,Pacific/Chuuk Pacific/Yap Antarctica/DumontDUrville Pacific/Truk
+Pacific/Tarawa,Pacific/Funafuti Pacific/Majuro Pacific/Wake Pacific/Wallis
diff --git a/content/timezonedata/README b/content/timezonedata/README
new file mode 100644
index 0000000..dcf5176
--- /dev/null
+++ b/content/timezonedata/README
@@ -0,0 +1,4 @@
+Taken from TimeZoneConverter:
+https://github.com/mj1856/TimeZoneConverter/blob/master/src/TimeZoneConverter/Data/Mapping.csv.gz
+
+ 12 Jan 2022
diff --git a/content/timezonedata/WindowsTimezone.csv b/content/timezonedata/WindowsTimezone.csv
new file mode 100644
index 0000000..8e1ec5f
--- /dev/null
+++ b/content/timezonedata/WindowsTimezone.csv
@@ -0,0 +1,514 @@
+AUS Central Standard Time,001,Australia/Darwin
+AUS Central Standard Time,AU,Australia/Darwin
+AUS Eastern Standard Time,001,Australia/Sydney
+AUS Eastern Standard Time,AU,Australia/Sydney Australia/Melbourne
+Afghanistan Standard Time,001,Asia/Kabul
+Afghanistan Standard Time,AF,Asia/Kabul
+Alaskan Standard Time,001,America/Anchorage
+Alaskan Standard Time,US,America/Anchorage America/Juneau America/Metlakatla America/Nome America/Sitka America/Yakutat
+Aleutian Standard Time,001,America/Adak
+Aleutian Standard Time,US,America/Adak
+Altai Standard Time,001,Asia/Barnaul
+Altai Standard Time,RU,Asia/Barnaul
+Arab Standard Time,001,Asia/Riyadh
+Arab Standard Time,BH,Asia/Bahrain
+Arab Standard Time,KW,Asia/Kuwait
+Arab Standard Time,QA,Asia/Qatar
+Arab Standard Time,SA,Asia/Riyadh
+Arab Standard Time,YE,Asia/Aden
+Arabian Standard Time,001,Asia/Dubai
+Arabian Standard Time,AE,Asia/Dubai
+Arabian Standard Time,OM,Asia/Muscat
+Arabian Standard Time,ZZ,Etc/GMT-4
+Arabic Standard Time,001,Asia/Baghdad
+Arabic Standard Time,IQ,Asia/Baghdad
+Argentina Standard Time,001,America/Buenos_Aires
+Argentina Standard Time,AR,America/Buenos_Aires America/Argentina/La_Rioja America/Argentina/Rio_Gallegos America/Argentina/Salta America/Argentina/San_Juan America/Argentina/San_Luis America/Argentina/Tucuman America/Argentina/Ushuaia America/Catamarca America/Cordoba America/Jujuy America/Mendoza
+Astrakhan Standard Time,001,Europe/Astrakhan
+Astrakhan Standard Time,RU,Europe/Astrakhan Europe/Ulyanovsk
+Atlantic Standard Time,001,America/Halifax
+Atlantic Standard Time,BM,Atlantic/Bermuda
+Atlantic Standard Time,CA,America/Halifax America/Glace_Bay America/Goose_Bay America/Moncton
+Atlantic Standard Time,GL,America/Thule
+Aus Central W. Standard Time,001,Australia/Eucla
+Aus Central W. Standard Time,AU,Australia/Eucla
+Azerbaijan Standard Time,001,Asia/Baku
+Azerbaijan Standard Time,AZ,Asia/Baku
+Azores Standard Time,001,Atlantic/Azores
+Azores Standard Time,GL,America/Scoresbysund
+Azores Standard Time,PT,Atlantic/Azores
+Bahia Standard Time,001,America/Bahia
+Bahia Standard Time,BR,America/Bahia
+Bangladesh Standard Time,001,Asia/Dhaka
+Bangladesh Standard Time,BD,Asia/Dhaka
+Bangladesh Standard Time,BT,Asia/Thimphu
+Belarus Standard Time,001,Europe/Minsk
+Belarus Standard Time,BY,Europe/Minsk
+Bougainville Standard Time,001,Pacific/Bougainville
+Bougainville Standard Time,PG,Pacific/Bougainville
+Canada Central Standard Time,001,America/Regina
+Canada Central Standard Time,CA,America/Regina America/Swift_Current
+Cape Verde Standard Time,001,Atlantic/Cape_Verde
+Cape Verde Standard Time,CV,Atlantic/Cape_Verde
+Cape Verde Standard Time,ZZ,Etc/GMT+1
+Caucasus Standard Time,001,Asia/Yerevan
+Caucasus Standard Time,AM,Asia/Yerevan
+Cen. Australia Standard Time,001,Australia/Adelaide
+Cen. Australia Standard Time,AU,Australia/Adelaide Australia/Broken_Hill
+Central America Standard Time,001,America/Guatemala
+Central America Standard Time,BZ,America/Belize
+Central America Standard Time,CR,America/Costa_Rica
+Central America Standard Time,EC,Pacific/Galapagos
+Central America Standard Time,GT,America/Guatemala
+Central America Standard Time,HN,America/Tegucigalpa
+Central America Standard Time,NI,America/Managua
+Central America Standard Time,SV,America/El_Salvador
+Central America Standard Time,ZZ,Etc/GMT+6
+Central Asia Standard Time,001,Asia/Almaty
+Central Asia Standard Time,AQ,Antarctica/Vostok
+Central Asia Standard Time,CN,Asia/Urumqi
+Central Asia Standard Time,DG,Indian/Chagos
+Central Asia Standard Time,IO,Indian/Chagos
+Central Asia Standard Time,KG,Asia/Bishkek
+Central Asia Standard Time,KZ,Asia/Almaty Asia/Qostanay
+Central Asia Standard Time,ZZ,Etc/GMT-6
+Central Brazilian Standard Time,001,America/Cuiaba
+Central Brazilian Standard Time,BR,America/Cuiaba America/Campo_Grande
+Central Europe Standard Time,001,Europe/Budapest
+Central Europe Standard Time,AL,Europe/Tirane
+Central Europe Standard Time,CZ,Europe/Prague
+Central Europe Standard Time,HU,Europe/Budapest
+Central Europe Standard Time,ME,Europe/Podgorica
+Central Europe Standard Time,RS,Europe/Belgrade
+Central Europe Standard Time,SI,Europe/Ljubljana
+Central Europe Standard Time,SK,Europe/Bratislava
+Central Europe Standard Time,XK,Europe/Belgrade
+Central European Standard Time,001,Europe/Warsaw
+Central European Standard Time,BA,Europe/Sarajevo
+Central European Standard Time,HR,Europe/Zagreb
+Central European Standard Time,MK,Europe/Skopje
+Central European Standard Time,PL,Europe/Warsaw
+Central Pacific Standard Time,001,Pacific/Guadalcanal
+Central Pacific Standard Time,AQ,Antarctica/Casey
+Central Pacific Standard Time,FM,Pacific/Ponape Pacific/Kosrae
+Central Pacific Standard Time,NC,Pacific/Noumea
+Central Pacific Standard Time,SB,Pacific/Guadalcanal
+Central Pacific Standard Time,VU,Pacific/Efate
+Central Pacific Standard Time,ZZ,Etc/GMT-11
+Central Standard Time (Mexico),001,America/Mexico_City
+Central Standard Time (Mexico),MX,America/Mexico_City America/Bahia_Banderas America/Merida America/Monterrey
+Central Standard Time,001,America/Chicago
+Central Standard Time,CA,America/Winnipeg America/Rainy_River America/Rankin_Inlet America/Resolute
+Central Standard Time,MX,America/Matamoros
+Central Standard Time,US,America/Chicago America/Indiana/Knox America/Indiana/Tell_City America/Menominee America/North_Dakota/Beulah America/North_Dakota/Center America/North_Dakota/New_Salem
+Central Standard Time,ZZ,CST6CDT
+Chatham Islands Standard Time,001,Pacific/Chatham
+Chatham Islands Standard Time,NZ,Pacific/Chatham
+China Standard Time,001,Asia/Shanghai
+China Standard Time,CN,Asia/Shanghai
+China Standard Time,HK,Asia/Hong_Kong
+China Standard Time,MO,Asia/Macau
+Cuba Standard Time,001,America/Havana
+Cuba Standard Time,CU,America/Havana
+Dateline Standard Time,001,Etc/GMT+12
+Dateline Standard Time,ZZ,Etc/GMT+12
+E. Africa Standard Time,001,Africa/Nairobi
+E. Africa Standard Time,AQ,Antarctica/Syowa
+E. Africa Standard Time,DJ,Africa/Djibouti
+E. Africa Standard Time,ER,Africa/Asmera
+E. Africa Standard Time,ET,Africa/Addis_Ababa
+E. Africa Standard Time,KE,Africa/Nairobi
+E. Africa Standard Time,KM,Indian/Comoro
+E. Africa Standard Time,MG,Indian/Antananarivo
+E. Africa Standard Time,SO,Africa/Mogadishu
+E. Africa Standard Time,TZ,Africa/Dar_es_Salaam
+E. Africa Standard Time,UG,Africa/Kampala
+E. Africa Standard Time,YT,Indian/Mayotte
+E. Africa Standard Time,ZZ,Etc/GMT-3
+E. Australia Standard Time,001,Australia/Brisbane
+E. Australia Standard Time,AU,Australia/Brisbane Australia/Lindeman
+E. Europe Standard Time,001,Europe/Chisinau
+E. Europe Standard Time,MD,Europe/Chisinau
+E. South America Standard Time,001,America/Sao_Paulo
+E. South America Standard Time,BR,America/Sao_Paulo
+Easter Island Standard Time,001,Pacific/Easter
+Easter Island Standard Time,CL,Pacific/Easter
+Eastern Standard Time (Mexico),001,America/Cancun
+Eastern Standard Time (Mexico),MX,America/Cancun
+Eastern Standard Time,001,America/New_York
+Eastern Standard Time,BS,America/Nassau
+Eastern Standard Time,CA,America/Toronto America/Iqaluit America/Montreal America/Nipigon America/Pangnirtung America/Thunder_Bay
+Eastern Standard Time,US,America/New_York America/Detroit America/Indiana/Petersburg America/Indiana/Vincennes America/Indiana/Winamac America/Kentucky/Monticello America/Louisville
+Eastern Standard Time,ZZ,EST5EDT
+Egypt Standard Time,001,Africa/Cairo
+Egypt Standard Time,EG,Africa/Cairo
+Ekaterinburg Standard Time,001,Asia/Yekaterinburg
+Ekaterinburg Standard Time,RU,Asia/Yekaterinburg
+FLE Standard Time,001,Europe/Kiev
+FLE Standard Time,AX,Europe/Mariehamn
+FLE Standard Time,BG,Europe/Sofia
+FLE Standard Time,EE,Europe/Tallinn
+FLE Standard Time,FI,Europe/Helsinki
+FLE Standard Time,LT,Europe/Vilnius
+FLE Standard Time,LV,Europe/Riga
+FLE Standard Time,UA,Europe/Kiev Europe/Uzhgorod Europe/Zaporozhye
+Fiji Standard Time,001,Pacific/Fiji
+Fiji Standard Time,FJ,Pacific/Fiji
+GMT Standard Time,001,Europe/London
+GMT Standard Time,ES,Atlantic/Canary
+GMT Standard Time,FO,Atlantic/Faeroe
+GMT Standard Time,GB,Europe/London
+GMT Standard Time,GG,Europe/Guernsey
+GMT Standard Time,IC,Atlantic/Canary
+GMT Standard Time,IE,Europe/Dublin
+GMT Standard Time,IM,Europe/Isle_of_Man
+GMT Standard Time,JE,Europe/Jersey
+GMT Standard Time,PT,Europe/Lisbon Atlantic/Madeira
+GTB Standard Time,001,Europe/Bucharest
+GTB Standard Time,CY,Asia/Nicosia Asia/Famagusta
+GTB Standard Time,GR,Europe/Athens
+GTB Standard Time,RO,Europe/Bucharest
+Georgian Standard Time,001,Asia/Tbilisi
+Georgian Standard Time,GE,Asia/Tbilisi
+Greenland Standard Time,001,America/Godthab
+Greenland Standard Time,GL,America/Godthab
+Greenwich Standard Time,001,Atlantic/Reykjavik
+Greenwich Standard Time,AC,Atlantic/St_Helena
+Greenwich Standard Time,BF,Africa/Ouagadougou
+Greenwich Standard Time,CI,Africa/Abidjan
+Greenwich Standard Time,GH,Africa/Accra
+Greenwich Standard Time,GL,America/Danmarkshavn
+Greenwich Standard Time,GM,Africa/Banjul
+Greenwich Standard Time,GN,Africa/Conakry
+Greenwich Standard Time,GW,Africa/Bissau
+Greenwich Standard Time,IS,Atlantic/Reykjavik
+Greenwich Standard Time,LR,Africa/Monrovia
+Greenwich Standard Time,ML,Africa/Bamako
+Greenwich Standard Time,MR,Africa/Nouakchott
+Greenwich Standard Time,SH,Atlantic/St_Helena
+Greenwich Standard Time,SL,Africa/Freetown
+Greenwich Standard Time,SN,Africa/Dakar
+Greenwich Standard Time,TA,Atlantic/St_Helena
+Greenwich Standard Time,TG,Africa/Lome
+Haiti Standard Time,001,America/Port-au-Prince
+Haiti Standard Time,HT,America/Port-au-Prince
+Hawaiian Standard Time,001,Pacific/Honolulu
+Hawaiian Standard Time,CK,Pacific/Rarotonga
+Hawaiian Standard Time,PF,Pacific/Tahiti
+Hawaiian Standard Time,UM,Pacific/Johnston
+Hawaiian Standard Time,US,Pacific/Honolulu
+Hawaiian Standard Time,ZZ,Etc/GMT+10
+India Standard Time,001,Asia/Calcutta
+India Standard Time,IN,Asia/Calcutta
+Iran Standard Time,001,Asia/Tehran
+Iran Standard Time,IR,Asia/Tehran
+Israel Standard Time,001,Asia/Jerusalem
+Israel Standard Time,IL,Asia/Jerusalem
+Jordan Standard Time,001,Asia/Amman
+Jordan Standard Time,JO,Asia/Amman
+Kaliningrad Standard Time,001,Europe/Kaliningrad
+Kaliningrad Standard Time,RU,Europe/Kaliningrad
+Korea Standard Time,001,Asia/Seoul
+Korea Standard Time,KR,Asia/Seoul
+Libya Standard Time,001,Africa/Tripoli
+Libya Standard Time,LY,Africa/Tripoli
+Line Islands Standard Time,001,Pacific/Kiritimati
+Line Islands Standard Time,KI,Pacific/Kiritimati
+Line Islands Standard Time,ZZ,Etc/GMT-14
+Lord Howe Standard Time,001,Australia/Lord_Howe
+Lord Howe Standard Time,AU,Australia/Lord_Howe
+Magadan Standard Time,001,Asia/Magadan
+Magadan Standard Time,RU,Asia/Magadan
+Magallanes Standard Time,001,America/Punta_Arenas
+Magallanes Standard Time,CL,America/Punta_Arenas
+Marquesas Standard Time,001,Pacific/Marquesas
+Marquesas Standard Time,PF,Pacific/Marquesas
+Mauritius Standard Time,001,Indian/Mauritius
+Mauritius Standard Time,MU,Indian/Mauritius
+Mauritius Standard Time,RE,Indian/Reunion
+Mauritius Standard Time,SC,Indian/Mahe
+Middle East Standard Time,001,Asia/Beirut
+Middle East Standard Time,LB,Asia/Beirut
+Montevideo Standard Time,001,America/Montevideo
+Montevideo Standard Time,UY,America/Montevideo
+Morocco Standard Time,001,Africa/Casablanca
+Morocco Standard Time,EH,Africa/El_Aaiun
+Morocco Standard Time,MA,Africa/Casablanca
+Mountain Standard Time (Mexico),001,America/Chihuahua
+Mountain Standard Time (Mexico),MX,America/Chihuahua America/Mazatlan
+Mountain Standard Time,001,America/Denver
+Mountain Standard Time,CA,America/Edmonton America/Cambridge_Bay America/Inuvik America/Yellowknife
+Mountain Standard Time,MX,America/Ojinaga
+Mountain Standard Time,US,America/Denver America/Boise
+Mountain Standard Time,ZZ,MST7MDT
+Myanmar Standard Time,001,Asia/Rangoon
+Myanmar Standard Time,CC,Indian/Cocos
+Myanmar Standard Time,MM,Asia/Rangoon
+N. Central Asia Standard Time,001,Asia/Novosibirsk
+N. Central Asia Standard Time,RU,Asia/Novosibirsk
+Namibia Standard Time,001,Africa/Windhoek
+Namibia Standard Time,NA,Africa/Windhoek
+Nepal Standard Time,001,Asia/Katmandu
+Nepal Standard Time,NP,Asia/Katmandu
+New Zealand Standard Time,001,Pacific/Auckland
+New Zealand Standard Time,AQ,Antarctica/McMurdo
+New Zealand Standard Time,NZ,Pacific/Auckland
+Newfoundland Standard Time,001,America/St_Johns
+Newfoundland Standard Time,CA,America/St_Johns
+Norfolk Standard Time,001,Pacific/Norfolk
+Norfolk Standard Time,NF,Pacific/Norfolk
+North Asia East Standard Time,001,Asia/Irkutsk
+North Asia East Standard Time,RU,Asia/Irkutsk
+North Asia Standard Time,001,Asia/Krasnoyarsk
+North Asia Standard Time,RU,Asia/Krasnoyarsk Asia/Novokuznetsk
+North Korea Standard Time,001,Asia/Pyongyang
+North Korea Standard Time,KP,Asia/Pyongyang
+Omsk Standard Time,001,Asia/Omsk
+Omsk Standard Time,RU,Asia/Omsk
+Pacific SA Standard Time,001,America/Santiago
+Pacific SA Standard Time,CL,America/Santiago
+Pacific Standard Time (Mexico),001,America/Tijuana
+Pacific Standard Time (Mexico),MX,America/Tijuana America/Santa_Isabel
+Pacific Standard Time,001,America/Los_Angeles
+Pacific Standard Time,CA,America/Vancouver
+Pacific Standard Time,US,America/Los_Angeles
+Pacific Standard Time,ZZ,PST8PDT
+Pakistan Standard Time,001,Asia/Karachi
+Pakistan Standard Time,PK,Asia/Karachi
+Paraguay Standard Time,001,America/Asuncion
+Paraguay Standard Time,PY,America/Asuncion
+Qyzylorda Standard Time,001,Asia/Qyzylorda
+Qyzylorda Standard Time,KZ,Asia/Qyzylorda
+Romance Standard Time,001,Europe/Paris
+Romance Standard Time,BE,Europe/Brussels
+Romance Standard Time,DK,Europe/Copenhagen
+Romance Standard Time,EA,Africa/Ceuta
+Romance Standard Time,ES,Europe/Madrid Africa/Ceuta
+Romance Standard Time,FR,Europe/Paris
+Russia Time Zone 10,001,Asia/Srednekolymsk
+Russia Time Zone 10,RU,Asia/Srednekolymsk
+Russia Time Zone 11,001,Asia/Kamchatka
+Russia Time Zone 11,RU,Asia/Kamchatka Asia/Anadyr
+Russia Time Zone 3,001,Europe/Samara
+Russia Time Zone 3,RU,Europe/Samara
+Russian Standard Time,001,Europe/Moscow
+Russian Standard Time,RU,Europe/Moscow Europe/Kirov
+Russian Standard Time,UA,Europe/Simferopol
+SA Eastern Standard Time,001,America/Cayenne
+SA Eastern Standard Time,AQ,Antarctica/Rothera Antarctica/Palmer
+SA Eastern Standard Time,BR,America/Fortaleza America/Belem America/Maceio America/Recife America/Santarem
+SA Eastern Standard Time,FK,Atlantic/Stanley
+SA Eastern Standard Time,GF,America/Cayenne
+SA Eastern Standard Time,SR,America/Paramaribo
+SA Eastern Standard Time,ZZ,Etc/GMT+3
+SA Pacific Standard Time,001,America/Bogota
+SA Pacific Standard Time,BR,America/Rio_Branco America/Eirunepe
+SA Pacific Standard Time,CA,America/Coral_Harbour
+SA Pacific Standard Time,CO,America/Bogota
+SA Pacific Standard Time,EC,America/Guayaquil
+SA Pacific Standard Time,JM,America/Jamaica
+SA Pacific Standard Time,KY,America/Cayman
+SA Pacific Standard Time,PA,America/Panama
+SA Pacific Standard Time,PE,America/Lima
+SA Pacific Standard Time,ZZ,Etc/GMT+5
+SA Western Standard Time,001,America/La_Paz
+SA Western Standard Time,AG,America/Antigua
+SA Western Standard Time,AI,America/Anguilla
+SA Western Standard Time,AW,America/Aruba
+SA Western Standard Time,BB,America/Barbados
+SA Western Standard Time,BL,America/St_Barthelemy
+SA Western Standard Time,BO,America/La_Paz
+SA Western Standard Time,BQ,America/Kralendijk
+SA Western Standard Time,BR,America/Manaus America/Boa_Vista America/Porto_Velho
+SA Western Standard Time,CA,America/Blanc-Sablon
+SA Western Standard Time,CW,America/Curacao
+SA Western Standard Time,DM,America/Dominica
+SA Western Standard Time,DO,America/Santo_Domingo
+SA Western Standard Time,GD,America/Grenada
+SA Western Standard Time,GP,America/Guadeloupe
+SA Western Standard Time,GY,America/Guyana
+SA Western Standard Time,KN,America/St_Kitts
+SA Western Standard Time,LC,America/St_Lucia
+SA Western Standard Time,MF,America/Marigot
+SA Western Standard Time,MQ,America/Martinique
+SA Western Standard Time,MS,America/Montserrat
+SA Western Standard Time,PR,America/Puerto_Rico
+SA Western Standard Time,SX,America/Lower_Princes
+SA Western Standard Time,TT,America/Port_of_Spain
+SA Western Standard Time,VC,America/St_Vincent
+SA Western Standard Time,VG,America/Tortola
+SA Western Standard Time,VI,America/St_Thomas
+SA Western Standard Time,ZZ,Etc/GMT+4
+SE Asia Standard Time,001,Asia/Bangkok
+SE Asia Standard Time,AQ,Antarctica/Davis
+SE Asia Standard Time,CX,Indian/Christmas
+SE Asia Standard Time,ID,Asia/Jakarta Asia/Pontianak
+SE Asia Standard Time,KH,Asia/Phnom_Penh
+SE Asia Standard Time,LA,Asia/Vientiane
+SE Asia Standard Time,TH,Asia/Bangkok
+SE Asia Standard Time,VN,Asia/Saigon
+SE Asia Standard Time,ZZ,Etc/GMT-7
+Saint Pierre Standard Time,001,America/Miquelon
+Saint Pierre Standard Time,PM,America/Miquelon
+Sakhalin Standard Time,001,Asia/Sakhalin
+Sakhalin Standard Time,RU,Asia/Sakhalin
+Samoa Standard Time,001,Pacific/Apia
+Samoa Standard Time,WS,Pacific/Apia
+Sao Tome Standard Time,001,Africa/Sao_Tome
+Sao Tome Standard Time,ST,Africa/Sao_Tome
+Saratov Standard Time,001,Europe/Saratov
+Saratov Standard Time,RU,Europe/Saratov
+Singapore Standard Time,001,Asia/Singapore
+Singapore Standard Time,BN,Asia/Brunei
+Singapore Standard Time,ID,Asia/Makassar
+Singapore Standard Time,MY,Asia/Kuala_Lumpur Asia/Kuching
+Singapore Standard Time,PH,Asia/Manila
+Singapore Standard Time,SG,Asia/Singapore
+Singapore Standard Time,ZZ,Etc/GMT-8
+South Africa Standard Time,001,Africa/Johannesburg
+South Africa Standard Time,BI,Africa/Bujumbura
+South Africa Standard Time,BW,Africa/Gaborone
+South Africa Standard Time,CD,Africa/Lubumbashi
+South Africa Standard Time,LS,Africa/Maseru
+South Africa Standard Time,MW,Africa/Blantyre
+South Africa Standard Time,MZ,Africa/Maputo
+South Africa Standard Time,RW,Africa/Kigali
+South Africa Standard Time,SZ,Africa/Mbabane
+South Africa Standard Time,ZA,Africa/Johannesburg
+South Africa Standard Time,ZM,Africa/Lusaka
+South Africa Standard Time,ZW,Africa/Harare
+South Africa Standard Time,ZZ,Etc/GMT-2
+South Sudan Standard Time,001,Africa/Juba
+South Sudan Standard Time,SS,Africa/Juba
+Sri Lanka Standard Time,001,Asia/Colombo
+Sri Lanka Standard Time,LK,Asia/Colombo
+Sudan Standard Time,001,Africa/Khartoum
+Sudan Standard Time,SD,Africa/Khartoum
+Syria Standard Time,001,Asia/Damascus
+Syria Standard Time,SY,Asia/Damascus
+Taipei Standard Time,001,Asia/Taipei
+Taipei Standard Time,TW,Asia/Taipei
+Tasmania Standard Time,001,Australia/Hobart
+Tasmania Standard Time,AU,Australia/Hobart Australia/Currie Antarctica/Macquarie
+Tocantins Standard Time,001,America/Araguaina
+Tocantins Standard Time,BR,America/Araguaina
+Tokyo Standard Time,001,Asia/Tokyo
+Tokyo Standard Time,ID,Asia/Jayapura
+Tokyo Standard Time,JP,Asia/Tokyo
+Tokyo Standard Time,PW,Pacific/Palau
+Tokyo Standard Time,TL,Asia/Dili
+Tokyo Standard Time,ZZ,Etc/GMT-9
+Tomsk Standard Time,001,Asia/Tomsk
+Tomsk Standard Time,RU,Asia/Tomsk
+Tonga Standard Time,001,Pacific/Tongatapu
+Tonga Standard Time,TO,Pacific/Tongatapu
+Transbaikal Standard Time,001,Asia/Chita
+Transbaikal Standard Time,RU,Asia/Chita
+Turkey Standard Time,001,Europe/Istanbul
+Turkey Standard Time,TR,Europe/Istanbul
+Turks And Caicos Standard Time,001,America/Grand_Turk
+Turks And Caicos Standard Time,TC,America/Grand_Turk
+US Eastern Standard Time,001,America/Indianapolis
+US Eastern Standard Time,US,America/Indianapolis America/Indiana/Marengo America/Indiana/Vevay
+US Mountain Standard Time,001,America/Phoenix
+US Mountain Standard Time,CA,America/Creston America/Dawson_Creek America/Fort_Nelson
+US Mountain Standard Time,MX,America/Hermosillo
+US Mountain Standard Time,US,America/Phoenix
+US Mountain Standard Time,ZZ,Etc/GMT+7
+UTC+12,001,Etc/GMT-12
+UTC+12,KI,Pacific/Tarawa
+UTC+12,MH,Pacific/Majuro Pacific/Kwajalein
+UTC+12,NR,Pacific/Nauru
+UTC+12,TV,Pacific/Funafuti
+UTC+12,UM,Pacific/Wake
+UTC+12,WF,Pacific/Wallis
+UTC+12,ZZ,Etc/GMT-12
+UTC+13,001,Etc/GMT-13
+UTC+13,KI,Pacific/Enderbury
+UTC+13,TK,Pacific/Fakaofo
+UTC+13,ZZ,Etc/GMT-13
+UTC,001,Etc/UTC
+UTC,ZZ,Etc/UTC Etc/GMT
+UTC-02,001,Etc/GMT+2
+UTC-02,BR,America/Noronha
+UTC-02,GS,Atlantic/South_Georgia
+UTC-02,ZZ,Etc/GMT+2
+UTC-08,001,Etc/GMT+8
+UTC-08,PN,Pacific/Pitcairn
+UTC-08,ZZ,Etc/GMT+8
+UTC-09,001,Etc/GMT+9
+UTC-09,PF,Pacific/Gambier
+UTC-09,ZZ,Etc/GMT+9
+UTC-11,001,Etc/GMT+11
+UTC-11,AS,Pacific/Pago_Pago
+UTC-11,NU,Pacific/Niue
+UTC-11,UM,Pacific/Midway
+UTC-11,ZZ,Etc/GMT+11
+Ulaanbaatar Standard Time,001,Asia/Ulaanbaatar
+Ulaanbaatar Standard Time,MN,Asia/Ulaanbaatar Asia/Choibalsan
+Venezuela Standard Time,001,America/Caracas
+Venezuela Standard Time,VE,America/Caracas
+Vladivostok Standard Time,001,Asia/Vladivostok
+Vladivostok Standard Time,RU,Asia/Vladivostok Asia/Ust-Nera
+Volgograd Standard Time,001,Europe/Volgograd
+Volgograd Standard Time,RU,Europe/Volgograd
+W. Australia Standard Time,001,Australia/Perth
+W. Australia Standard Time,AU,Australia/Perth
+W. Central Africa Standard Time,001,Africa/Lagos
+W. Central Africa Standard Time,AO,Africa/Luanda
+W. Central Africa Standard Time,BJ,Africa/Porto-Novo
+W. Central Africa Standard Time,CD,Africa/Kinshasa
+W. Central Africa Standard Time,CF,Africa/Bangui
+W. Central Africa Standard Time,CG,Africa/Brazzaville
+W. Central Africa Standard Time,CM,Africa/Douala
+W. Central Africa Standard Time,DZ,Africa/Algiers
+W. Central Africa Standard Time,GA,Africa/Libreville
+W. Central Africa Standard Time,GQ,Africa/Malabo
+W. Central Africa Standard Time,NE,Africa/Niamey
+W. Central Africa Standard Time,NG,Africa/Lagos
+W. Central Africa Standard Time,TD,Africa/Ndjamena
+W. Central Africa Standard Time,TN,Africa/Tunis
+W. Central Africa Standard Time,ZZ,Etc/GMT-1
+W. Europe Standard Time,001,Europe/Berlin
+W. Europe Standard Time,AD,Europe/Andorra
+W. Europe Standard Time,AT,Europe/Vienna
+W. Europe Standard Time,CH,Europe/Zurich
+W. Europe Standard Time,DE,Europe/Berlin Europe/Busingen
+W. Europe Standard Time,GI,Europe/Gibraltar
+W. Europe Standard Time,IT,Europe/Rome
+W. Europe Standard Time,LI,Europe/Vaduz
+W. Europe Standard Time,LU,Europe/Luxembourg
+W. Europe Standard Time,MC,Europe/Monaco
+W. Europe Standard Time,MT,Europe/Malta
+W. Europe Standard Time,NL,Europe/Amsterdam
+W. Europe Standard Time,NO,Europe/Oslo
+W. Europe Standard Time,SE,Europe/Stockholm
+W. Europe Standard Time,SJ,Arctic/Longyearbyen
+W. Europe Standard Time,SM,Europe/San_Marino
+W. Europe Standard Time,VA,Europe/Vatican
+W. Mongolia Standard Time,001,Asia/Hovd
+W. Mongolia Standard Time,MN,Asia/Hovd
+West Asia Standard Time,001,Asia/Tashkent
+West Asia Standard Time,AQ,Antarctica/Mawson
+West Asia Standard Time,KZ,Asia/Oral Asia/Aqtau Asia/Aqtobe Asia/Atyrau
+West Asia Standard Time,MV,Indian/Maldives
+West Asia Standard Time,TF,Indian/Kerguelen
+West Asia Standard Time,TJ,Asia/Dushanbe
+West Asia Standard Time,TM,Asia/Ashgabat
+West Asia Standard Time,UZ,Asia/Tashkent Asia/Samarkand
+West Asia Standard Time,ZZ,Etc/GMT-5
+West Bank Standard Time,001,Asia/Hebron
+West Bank Standard Time,PS,Asia/Hebron Asia/Gaza
+West Pacific Standard Time,001,Pacific/Port_Moresby
+West Pacific Standard Time,AQ,Antarctica/DumontDUrville
+West Pacific Standard Time,FM,Pacific/Truk
+West Pacific Standard Time,GU,Pacific/Guam
+West Pacific Standard Time,MP,Pacific/Saipan
+West Pacific Standard Time,PG,Pacific/Port_Moresby
+West Pacific Standard Time,ZZ,Etc/GMT-10
+Yakutsk Standard Time,001,Asia/Yakutsk
+Yakutsk Standard Time,RU,Asia/Yakutsk Asia/Khandyga
+Yukon Standard Time,001,America/Whitehorse
+Yukon Standard Time,CA,America/Whitehorse America/Dawson
+Kamchatka Standard Time,001,Asia/Kamchatka
+Mid-Atlantic Standard Time,001,Etc/GMT+2