Version in base suite: 4.7-1~deb12u1 Base version: eas4tbsync_4.7-1~deb12u1 Target version: eas4tbsync_4.11-1~deb12u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/e/eas4tbsync/eas4tbsync_4.7-1~deb12u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/e/eas4tbsync/eas4tbsync_4.11-1~deb12u1.dsc _locales/ja/messages.json | 22 background.js | 24 content/api/BootstrapLoader/implementation.js | 742 --------------- content/bootstrap.js | 2 content/includes/calendarsync.js | 1 content/includes/contactsync.js | 25 content/includes/network.js | 1271 +++++++++++++------------- content/includes/sync.js | 1 content/includes/tasksync.js | 1 content/includes/tools.js | 10 content/includes/wbxmltools.js | 3 content/includes/xmltools.js | 3 content/manager/createAccount.js | 20 content/manager/createAccount.xhtml | 2 content/provider.js | 8 debian/changelog | 23 debian/control | 7 debian/copyright | 4 manifest.json | 6 19 files changed, 783 insertions(+), 1392 deletions(-) diff -Nru eas4tbsync-4.7/_locales/ja/messages.json eas4tbsync-4.11/_locales/ja/messages.json --- eas4tbsync-4.7/_locales/ja/messages.json 2023-08-18 14:51:43.000000000 +0000 +++ eas4tbsync-4.11/_locales/ja/messages.json 2024-08-19 18:22:14.000000000 +0000 @@ -60,13 +60,13 @@ "message": "自宅電話番号 3:" }, "abCard.header.messaging": { - "message": "Messaging:" + "message": "メッセージング:" }, "abCard.header.otheraddress": { "message": "その他のアドレス (EAS)" }, "abCard.header.othernumbers": { - "message": "Additional numbers:" + "message": "追加の電話番号:" }, "abCard.header.people": { "message": "People:" @@ -279,7 +279,7 @@ "message": "ユーザーが見つかりません (HTTP エラー 404)。" }, "status.449": { - "message": "Server requests provisioning (HTTP Error 449)." + "message": "サーバーがプロビジョニングを要求しています (HTTP エラー 449)。" }, "status.500": { "message": "不明なサーバー エラー (HTTP エラー 500)。" @@ -441,7 +441,7 @@ "message": "Sync failed. Server responded with status <##replace.1##>." }, "status.wbxmlmissingfield": { - "message": "ActiveSync protocol violation: Mandatory field <##replace.1##> is missing from server response." + "message": "ActiveSync プロトコル違反: 必須フィールド <##replace.1##> がサーバー応答から見つかりません。" }, "syncstate.accountdone": { "message": "Finished account" @@ -513,7 +513,7 @@ "message": "Requesting remote changes" }, "syncstate.prepare.request.revertlocalchanges": { - "message": "Collecting local changes" + "message": "ローカルの変更を収集中" }, "syncstate.prepare.request.setdeviceinfo": { "message": "デバイス情報を送信中" @@ -534,13 +534,13 @@ "message": "Waiting for change estimate" }, "syncstate.send.request.folders": { - "message": "Waiting for folder list update" + "message": "フォルダリストの更新を待機中" }, "syncstate.send.request.localchanges": { - "message": "Waiting for acknowledgment of local changes" + "message": "ローカルの変更の承認を待機中" }, "syncstate.send.request.localdeletes": { - "message": "Waiting for acknowledgment of local deletes" + "message": "ローカルの削除の承認を待機中" }, "syncstate.send.request.options": { "message": "Waiting for server options" @@ -549,7 +549,7 @@ "message": "Waiting for provision" }, "syncstate.send.request.remotechanges": { - "message": "Waiting for remote changes" + "message": "リモートの変更を待機中" }, "syncstate.send.request.revertlocalchanges": { "message": "Waiting for most recent versions" @@ -558,9 +558,9 @@ "message": "デバイス情報を送信中" }, "syncstate.send.request.synckey": { - "message": "Waiting for SyncKey" + "message": "同期キーを待機中" }, "syncstate.syncing": { - "message": "Initialize synchronization" + "message": "同期を初期化中" } } diff -Nru eas4tbsync-4.7/background.js eas4tbsync-4.11/background.js --- eas4tbsync-4.7/background.js 2023-08-18 14:51:43.000000000 +0000 +++ eas4tbsync-4.11/background.js 2024-08-19 18:22:14.000000000 +0000 @@ -1,26 +1,6 @@ -function isCompatible(version) { - let [ major, minor , patch ] = version.split(".").map(e => parseInt(e,10)); - return ( - major > 102 || - (major == 102 && minor > 3) || - (major == 102 && minor == 3 && patch > 2) - ); -} - async function main() { - let { version } = await browser.runtime.getBrowserInfo(); - if (isCompatible(version)) { - await messenger.BootstrapLoader.registerChromeUrl([ ["content", "eas4tbsync", "content/"] ]); - await messenger.BootstrapLoader.registerBootstrapScript("chrome://eas4tbsync/content/bootstrap.js"); - } else { - let manifest = browser.runtime.getManifest(); - browser.notifications.create({ - type: "basic", - iconUrl: browser.runtime.getURL("content/skin/eas32.png"), - title: `${manifest.name}`, - message: "Please update Thunderbird to at least 102.3.3 to be able to use this provider.", - }); - } + await messenger.BootstrapLoader.registerChromeUrl([ ["content", "eas4tbsync", "content/"] ]); + await messenger.BootstrapLoader.registerBootstrapScript("chrome://eas4tbsync/content/bootstrap.js"); } main(); diff -Nru eas4tbsync-4.7/content/api/BootstrapLoader/implementation.js eas4tbsync-4.11/content/api/BootstrapLoader/implementation.js --- eas4tbsync-4.7/content/api/BootstrapLoader/implementation.js 2023-08-18 14:51:43.000000000 +0000 +++ eas4tbsync-4.11/content/api/BootstrapLoader/implementation.js 2024-08-19 18:22:14.000000000 +0000 @@ -15,16 +15,6 @@ 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"]; @@ -51,13 +41,13 @@ for (let api of apis) { switch (api) { case "storage": - XPCOMUtils.defineLazyGetter(messenger, "storage", () => + ChromeUtils.defineLazyGetter(messenger, "storage", () => getStorage() ); break; default: - XPCOMUtils.defineLazyGetter(messenger, api, () => + ChromeUtils.defineLazyGetter(messenger, api, () => context.apiCan.findAPIPath(api) ); } @@ -65,607 +55,16 @@ 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( - ``) - : event.target.ownerGlobal.MozXULElement.parseXULToFragment( - ``); - - 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); - } - } - +var BootstrapLoader = class extends ExtensionCommon.ExtensionAPI { 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 @@ -711,27 +110,10 @@ 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) + chromeData.push(entry) } if (chromeData.length > 0) { @@ -743,22 +125,7 @@ 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) { @@ -768,6 +135,7 @@ // Get the addon object belonging to this extension. let addon = await AddonManager.getAddonByID(context.extension.id); + console.log(addon.id); //make the addon globally available in the bootstrapped scope self.bootstrappedObj.addon = addon; @@ -784,32 +152,6 @@ 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) { - } - }); - } } } }; @@ -820,63 +162,6 @@ 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 { @@ -891,17 +176,6 @@ 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; @@ -910,8 +184,4 @@ Services.obs.notifyObservers(null, "startupcache-invalidate"); console.log("BootstrapLoader for " + this.extension.id + " unloaded!"); } -}; - -var BootstrapLoader = getThunderbirdVersion().major < 111 - ? BootstrapLoader_102 - : BootstrapLoader_115; +}; \ No newline at end of file diff -Nru eas4tbsync-4.7/content/bootstrap.js eas4tbsync-4.11/content/bootstrap.js --- eas4tbsync-4.7/content/bootstrap.js 2023-08-18 14:51:43.000000000 +0000 +++ eas4tbsync-4.11/content/bootstrap.js 2024-08-19 18:22:14.000000000 +0000 @@ -8,8 +8,6 @@ // 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; diff -Nru eas4tbsync-4.7/content/includes/calendarsync.js eas4tbsync-4.11/content/includes/calendarsync.js --- eas4tbsync-4.7/content/includes/calendarsync.js 2023-08-18 14:51:43.000000000 +0000 +++ eas4tbsync-4.11/content/includes/calendarsync.js 2024-08-19 18:22:14.000000000 +0000 @@ -18,6 +18,7 @@ CalTodo: "resource:///modules/CalTodo.jsm", }); +var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm"); const cal = TbSync.lightning.cal; const ICAL = TbSync.lightning.ICAL; diff -Nru eas4tbsync-4.7/content/includes/contactsync.js eas4tbsync-4.11/content/includes/contactsync.js --- eas4tbsync-4.7/content/includes/contactsync.js 2023-08-18 14:51:43.000000000 +0000 +++ eas4tbsync-4.11/content/includes/contactsync.js 2024-08-19 18:22:14.000000000 +0000 @@ -21,6 +21,9 @@ VCardUtils: "resource:///modules/VCardUtils.jsm", }); +var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm"); +var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); + const eas = TbSync.providers.eas; var Contacts = { @@ -45,7 +48,7 @@ org : 2 }, - map_EAS_properties_to_vCard : { + map_EAS_properties_to_vCard: revision => ({ FileAs: {item: "fn", type: "text", params: {}}, /* DisplayName */ Birthday: {item: "bday", type: "date", params: {}}, @@ -72,8 +75,8 @@ // "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 */ + CompanyName: {item: "org", type: "text", params: {}, index: revision < 2 ? 1 : 0}, /* Company */ + Department: {item: "org", type: "text", params: {}, index: revision < 2 ? 0 : 1}, /* Department */ JobTitle: { item: "title", type: "text", params: {} }, /* JobTitle */ MobilePhoneNumber: { item: "tel", type: "text", params: {type: "cell" }}, @@ -115,7 +118,7 @@ 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: {} }, @@ -235,6 +238,9 @@ // --------------------------------------------------------------------------- // setThunderbirdItemFromWbxml: function (abItem, data, id, syncdata, mode = "standard") { let asversion = syncdata.accountData.getAccountProperty("asversion"); + let revision = parseInt(syncdata.target._directory.getStringValue("tbSyncRevision","1"), 10); + let map_EAS_properties_to_vCard = this.map_EAS_properties_to_vCard(revision); + 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 @@ -251,7 +257,7 @@ 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 vCard_property = (set == 0) ? map_EAS_properties_to_vCard[EAS_property] : this.map_EAS_properties_to_vCard_set2[EAS_property]; let value; switch (EAS_property) { case "Notes": @@ -399,6 +405,9 @@ // --------------------------------------------------------------------------- // getWbxmlFromThunderbirdItem: async function (abItem, syncdata, isException = false) { let asversion = syncdata.accountData.getAccountProperty("asversion"); + let revision = parseInt(syncdata.target._directory.getStringValue("tbSyncRevision","1"), 10); + let map_EAS_properties_to_vCard = this.map_EAS_properties_to_vCard(revision); + let wbxml = eas.wbxmltools.createWBXML("", syncdata.type); //init wbxml with "" and not with precodes, and set initial codepage let nowDate = new Date(); @@ -411,7 +420,7 @@ // 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 vCard_property = map_EAS_properties_to_vCard[EAS_property]; let value; switch (EAS_property) { case "Notes": @@ -512,7 +521,7 @@ } // 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"]); + let description = this.getValue(vCardProperties, map_EAS_properties_to_vCard["Notes"]); if (asversion == "2.5") { wbxml.atag("Body", description); } else { @@ -538,5 +547,5 @@ } } -Contacts.EAS_properties = Object.keys(Contacts.map_EAS_properties_to_vCard); +Contacts.EAS_properties = Object.keys(Contacts.map_EAS_properties_to_vCard()); Contacts.EAS_properties2 = Object.keys(Contacts.map_EAS_properties_to_vCard_set2); diff -Nru eas4tbsync-4.7/content/includes/network.js eas4tbsync-4.11/content/includes/network.js --- eas4tbsync-4.7/content/includes/network.js 2023-08-18 14:51:43.000000000 +0000 +++ eas4tbsync-4.11/content/includes/network.js 2024-08-19 18:22:14.000000000 +0000 @@ -5,29 +5,81 @@ * 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"; + +"use strict"; var { OAuth2 } = ChromeUtils.import("resource:///modules/OAuth2.jsm"); +var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm"); + +var containers = []; +var sandboxes = {}; + +function resetContainerWithId(id) { + Services.clearData.deleteDataFromOriginAttributesPattern({ userContextId: id }); +} + +function getContainerIdForContainerName(containerName) { + // Define the allowed range of container ids to be used + // TbSync is using 10000 - 19999 + // Lightning is using 20000 - 29999 + // Cardbook is using 30000 - 39999 + let min = 10000; + let max = 19999; + + //reset if adding an entry will exceed allowed range + if (containers.length > (max - min) && containers.indexOf(containerName) == -1) { + for (let i = 0; i < containers.length; i++) { + resetContainerWithId(i + min); + } + containers = []; + } + + let idx = containers.indexOf(containerName); + return (idx == -1) ? containers.push(containerName) - 1 + min : (idx + min); +} + +function getSandBoxedXHR({ user, accountname }, uri, containerReset = false) { + // The content principal used for the sandbox honours CORS. A server redirect + // to a different server may cause CORS violations. We implemented code to + // catch such redirects and re-run the request with the correct sandbox. If + // that becomes an issue, we need to make sandboxing optional. + // return new XMLHttpRequest({ mozAnon: false }); + + let containerName = `${accountname}::${user}@${uri.scheme}://${uri.hostPort}`; + + let userContextId = getContainerIdForContainerName(containerName); + if (containerReset) { + resetContainerWithId(userContextId); + } + if (!sandboxes.hasOwnProperty(containerName)) { + console.log("Creating sandbox for <" + containerName + ">"); + let principal = Services.scriptSecurityManager.createContentPrincipal(uri, { userContextId }); + sandboxes[containerName] = Components.utils.Sandbox(principal, { + wantXrays: true, + wantGlobalProperties: ["XMLHttpRequest"], + }); + } + return new sandboxes[containerName].XMLHttpRequest({ mozAnon: false }); +} + +var network = { -var network = { - - getEasURL: function(accountData) { + getEasURL: function (accountData) { let protocol = (accountData.getAccountProperty("https")) ? "https://" : "http://"; - let h = protocol + accountData.getAccountProperty("host"); - while (h.endsWith("/")) { h = h.slice(0,-1); } + 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"; + return h + "/Microsoft-Server-ActiveSync"; }, - - getAuthData: function(accountData) { + + 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() { + get host() { return "TbSync#" + accountData.accountID; }, @@ -39,213 +91,244 @@ return TbSync.passwordManager.getLoginInfo(this.host, "TbSync/EAS", this.user); }, - updateLoginData: function(newUsername, newPassword) { + get accountname() { + return accountData.getAccountProperty("accountname"); + }, + + updateLoginData: async function (newUsername, newPassword) { let oldUsername = this.user; - TbSync.passwordManager.updateLoginInfo(this.host, "TbSync/EAS", oldUsername, newUsername, newPassword); + await 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"); + 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 = ""; - } + }, + + getContextData: function (configObject = null) { + let contextData = {} + contextData.accountData = (configObject && configObject.hasOwnProperty("accountData")) ? configObject.accountData : null; + + if (contextData.accountData) { + contextData.accountname = contextData.accountData.getAccountProperty("accountname"); + contextData.user = contextData.accountData.getAccountProperty("user"); + contextData.host = contextData.accountData.getAccountProperty("host"); + contextData.servertype = contextData.accountData.getAccountProperty("servertype"); + contextData.accountID = contextData.accountData.accountID; + } else { + contextData.accountname = (configObject && configObject.hasOwnProperty("accountname")) ? configObject.accountname : ""; + contextData.user = (configObject && configObject.hasOwnProperty("user")) ? configObject.user : ""; + contextData.host = (configObject && configObject.hasOwnProperty("host")) ? configObject.host : ""; + contextData.servertype = (configObject && configObject.hasOwnProperty("servertype")) ? configObject.servertype : ""; + contextData.accountID = ""; + } - if (!["office365"].includes(servertype)) - return null; + return contextData; + }, - 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; - } + // prepare and patch OAuth2 object + getOAuthObj: function (configObject = null) { + let { + accountData, + accountname, + user, + host, + accountID, + servertype + } = this.getContextData(configObject); + + 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; - 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; - } + 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); + let oauth = new OAuth2(config.scope, { + authorizationEndpoint: config.auth_uri, + tokenEndpoint: config.token_uri, + clientId: config.client_id, + clientSecret: config.client_secret }); - 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.requestWindowFeatures = "chrome,private,centerscreen,width=500,height=750"; - oauth.parseAndSanitizeTokenString = function(tokenString) { - let _tokensObj = {}; - try { - _tokensObj = JSON.parse(tokenString); - } catch (e) {} + // 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 */ - 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)); + oauth.asyncConnect = async function (rv) { + rv.error = ""; + + // 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 (oauth.getToken("accessToken")) { + await oauth.setToken("accessToken", ""); + } + + + try { + // 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. + await oauth.connect(/* with UI */ true, /* refresh */ false); + await oauth.setToken("accessToken", oauth.accessToken); + await oauth.setToken("refreshToken", oauth.refreshToken); + await oauth.setToken("tokenExpires", oauth.tokenExpires); + rv.tokens = oauth.tokens; + return true; + } catch (e) { + await oauth.setToken("accessToken", oauth.accessToken); + await oauth.setToken("refreshToken", oauth.refreshToken); + await oauth.setToken("tokenExpires", oauth.tokenExpires); + rv.error = eas.tools.isString(e) ? e : JSON.stringify(e); + } + + try { + switch (JSON.parse(rv.error).error) { + case "invalid_grant": + await oauth.setToken("accessToken", ""); + await oauth.setToken("refreshToken", ""); + rv.tokens = oauth.tokens; + 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); } - }, - enumerable: true, + rv.tokens = oauth.tokens; + return false; + }; + + oauth.isExpired = function () { + const OAUTH_GRACE_TIME = 30 * 1000; + return (oauth.getToken("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, }); - } - } - - return oauth; - }, - getOAuthValue: function(currentTokenString, type = "access") { + 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; + }; + + oauth.getToken = function (tokenName) { + let oauthValue = OAUTHVALUES.find(e => e[2] == tokenName); + return oauth.parseAndSanitizeTokenString(oauth.authData.password)[oauthValue[0]]; + }; + + oauth.setToken = async function (tokenName, val) { + oauth[tokenName] = val; + + let oauthValue = OAUTHVALUES.find(e => e[2] == tokenName); + let tokens = oauth.parseAndSanitizeTokenString(oauth.authData.password); + let valueChanged = (val != tokens[oauthValue[0]]) + if (valueChanged) { + tokens[oauthValue[0]] = val; + await oauth.authData.updateLoginData(oauth.authData.user, JSON.stringify(tokens)); + } + }; + } else { + oauth.getToken = function (tokenName) { + return oauth[tokenName]; + }; + + oauth.setToken = async function (tokenName, val) { + oauth[tokenName] = val; + }; + } + + return oauth; + }, + + getOAuthValue: function (currentTokenString, type = "access") { try { let tokens = JSON.parse(currentTokenString); if (tokens.hasOwnProperty(type)) @@ -258,91 +341,91 @@ sendRequest: async function (wbxml, command, syncData, allowSoftFail = false) { let ALLOWED_RETRIES = { - PasswordPrompt : 3, - NetworkError : 1, + PasswordPrompt: 3, + NetworkError: 1, } - + let rv = {}; let oauthData = eas.network.getOAuthObj({ accountData: syncData.accountData }); - let syncState = syncData.getSyncState().state; - - for (;;) { + let syncState = syncData.getSyncState().state; + + for (; ;) { - if (rv.errorType) { + 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); + + case "PasswordPrompt": + { + + if (oauthData) { + await oauthData.setToken("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; + await authData.updateLoginData(credentials.username, credentials.password); + } } } - } - break; - + 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 + { + // 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; - + break; + } - } - + } + if (!retry) throw rv.errorObj; } - + // check OAuth situation before connecting - if (oauthData && (!oauthData.accessToken || oauthData.isExpired())) { - syncData.setSyncState("oauthprompt"); + if (oauthData && (!oauthData.getToken("accessToken") || oauthData.isExpired())) { + syncData.setSyncState("oauthprompt"); let _rv = {} if (!(await oauthData.asyncConnect(_rv))) { - throw eas.sync.finish("error", _rv.error); + 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)) { @@ -351,7 +434,7 @@ } else { return rv; } - } + } }, sendRequestPromise: function (wbxml, command, syncData, allowSoftFail = false) { @@ -365,19 +448,20 @@ 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); + 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); + 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(); + + let contextData = eas.network.getContextData({ accountData: syncData.accountData }); + let uri = Services.io.newURI(eas.network.getEasURL(syncData.accountData) + '?Cmd=' + command + '&User=' + encodeURIComponent(connection.user) + '&DeviceType=' + encodeURIComponent(deviceType) + '&DeviceId=' + deviceId); + return new Promise(function (resolve, reject) { + syncData.req = getSandBoxedXHR(contextData, uri); 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.open("POST", uri.spec, true); syncData.req.overrideMimeType("text/plain"); syncData.req.setRequestHeader("User-Agent", userAgent); syncData.req.setRequestHeader("Content-Type", "application/vnd.ms-sync.wbxml"); @@ -388,7 +472,7 @@ 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 { @@ -422,9 +506,9 @@ } }; - syncData.req.onload = function() { + syncData.req.onload = function () { let response = syncData.req.responseText; - switch(syncData.req.status) { + switch (syncData.req.status) { case 200: //OK let msg = "Receiving data <" + syncData.getSyncState().state + "> for " + syncData.accountData.getAccountProperty("accountname"); @@ -459,12 +543,12 @@ 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); + 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(""); @@ -475,7 +559,7 @@ }; syncData.req.send(encoded); - + }); }, @@ -489,21 +573,21 @@ // RESPONSE EVALUATION - - logXML : function (wbxml, what) { + + logXML: function (wbxml, what) { let rawxml = eas.wbxmltools.convert2xml(wbxml); let xml = null; - if (rawxml) { + 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)); + 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); } @@ -516,12 +600,12 @@ 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) { + getDataFromResponse: function (wbxml, allowEmptyResponse = !eas.flags.allowEmptyResponse) { //check for empty wbxml if (wbxml.length === 0) { if (allowEmptyResponse) return null; @@ -533,21 +617,21 @@ 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"); + let synckey = eas.xmltools.getWbxmlDataField(wbxmlData, "Sync.Collections.Collection.SyncKey"); if (synckey) { // This COULD be a cause of problems... @@ -558,24 +642,24 @@ } }, - checkStatus : function (syncData, wbxmlData, path, rootpath="", allowSoftFail = false) { + 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 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]); + 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]; + fullpath = type + "." + elements[elements.length - 1]; } } @@ -587,7 +671,7 @@ TbSync.dump("wbxml status check", type + ": " + fullpath + " = " + status); //handle errrors based on type - let statusType = type+"."+status; + 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 @@ -596,7 +680,7 @@ 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 @@ -620,49 +704,49 @@ 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) { + 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 "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); - */ - } - + { + 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 @@ -671,14 +755,14 @@ 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); - } + } }, - + @@ -689,25 +773,25 @@ // WBXML COMM STUFF - - setDeviceInformation: async function (syncData) { + + 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.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"); @@ -716,24 +800,24 @@ syncData.setSyncState("eval.response.setdeviceinfo"); let wbxmlData = eas.network.getDataFromResponse(response); - eas.network.checkStatus(syncData, wbxmlData,"Settings.Status"); + eas.network.checkStatus(syncData, wbxmlData, "Settings.Status"); }, - getPolicykey: async function (syncData) { + getPolicykey: async function (syncData) { //build WBXML to request provision - syncData.setSyncState("prepare.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.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"); + 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"); @@ -744,12 +828,12 @@ 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); + 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"); + let policykey = eas.xmltools.getWbxmlDataField(wbxmlData, "Provision.Policies.Policy.PolicyKey"); switch (policyStatus) { case false: throw eas.sync.finish("error", "wbxmlmissingfield::Provision.Policies.Policy.Status"); @@ -763,8 +847,8 @@ case "1": if (policykey === false) { throw eas.sync.finish("error", "wbxmlmissingfield::Provision.Policies.Policy.PolicyKey"); - } - TbSync.dump("PolicyKey","Received policykey (" + loop + "): " + policykey); + } + TbSync.dump("PolicyKey", "Received policykey (" + loop + "): " + policykey); syncData.accountData.setAccountProperty("policykey", policykey); break; @@ -773,19 +857,19 @@ } //build WBXML to acknowledge provision - syncData.setSyncState("prepare.request.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.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 } }, @@ -795,15 +879,15 @@ //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.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); @@ -811,52 +895,52 @@ // get data from wbxml response let wbxmlData = eas.network.getDataFromResponse(response); //check status - eas.network.checkStatus(syncData, wbxmlData,"Sync.Collections.Collection.Status"); + eas.network.checkStatus(syncData, wbxmlData, "Sync.Collections.Collection.Status"); //update synckey eas.network.updateSynckey(syncData, wbxmlData); }, - getItemEstimate: async function (syncData) { + 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.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 @@ -878,7 +962,7 @@ } }, - getUserInfo: async function (syncData) { + getUserInfo: async function (syncData) { if (!syncData.accountData.getAccountProperty("allowedEasCommands").split(",").includes("Settings")) { return; } @@ -888,9 +972,9 @@ let wbxml = eas.wbxmltools.createWBXML(); wbxml.switchpage("Settings"); wbxml.otag("Settings"); - wbxml.otag("UserInformation"); - wbxml.atag("Get"); - wbxml.ctag(); + wbxml.otag("UserInformation"); + wbxml.atag("Get"); + wbxml.ctag(); wbxml.ctag(); syncData.setSyncState("send.request.getuserinfo"); @@ -900,12 +984,12 @@ syncData.setSyncState("eval.response.getuserinfo"); let wbxmlData = eas.network.getDataFromResponse(response); - eas.network.checkStatus(syncData, wbxmlData,"Settings.Status"); + eas.network.checkStatus(syncData, wbxmlData, "Settings.Status"); }, - + @@ -919,50 +1003,51 @@ 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.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++) { + 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())) { + if (oauthData && (!oauthData.getToken("accessToken") || oauthData.isExpired())) { let _rv = {} if (!(await oauthData.asyncConnect(_rv))) { - throw eas.sync.finish("error", _rv.error); + 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(); + let contextData = eas.network.getContextData({ accountData }); + let uri = Services.io.newURI(eas.network.getEasURL(accountData) + '?Cmd=' + command + '&User=' + encodeURIComponent(authData.user) + '&DeviceType=' + encodeURIComponent(deviceType) + '&DeviceId=' + deviceId); + let response = await new Promise(function (resolve, reject) { + let req = getSandBoxedXHR(contextData, uri); req.mozBackgroundRequest = true; - req.open("POST", eas.network.getEasURL(accountData) + '?Cmd=' + command + '&User=' + encodeURIComponent(authData.user) + '&DeviceType=' +encodeURIComponent(deviceType) + '&DeviceId=' + deviceId, true); + req.open("POST", uri.spec, 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")); @@ -970,7 +1055,7 @@ req.setRequestHeader("Authorization", 'Basic ' + TbSync.tools.b64encode(authData.user + ':' + authData.password)); } } - + if (accountData.getAccountProperty("asversion") == "2.5") { req.setRequestHeader("MS-ASProtocolVersion", "2.5"); } else { @@ -992,10 +1077,10 @@ reject("GAL Search Error"); }; - req.onload = function() { + req.onload = function () { let response = req.responseText; - - switch(req.status) { + + switch (req.status) { case 200: //OK eas.network.logXML(response, "Received (GAL Search"); @@ -1008,27 +1093,27 @@ resolve(response); } break; - + case 401: // bad auth - resolve("401"); + 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 = ""; + await oauthData.setToken("accessToken", ""); continue; } - } + } return response; } catch (e) { @@ -1047,37 +1132,39 @@ - // OPTIONS + // OPTIONS - getServerOptions: async function (syncData) { + 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())) { + if (oauthData && (!oauthData.getToken("accessToken") || oauthData.isExpired())) { let _rv = {}; - syncData.setSyncState("oauthprompt"); + 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(); + + let contextData = eas.network.getContextData({ accountData: syncData.accountData }); + let uri = Services.io.newURI(eas.network.getEasURL(syncData.accountData)); + let result = await new Promise(function (resolve, reject) { + syncData.req = getSandBoxedXHR(contextData, uri); syncData.req.mozBackgroundRequest = true; - syncData.req.open("OPTIONS", eas.network.getEasURL(syncData.accountData), true); + syncData.req.open("OPTIONS", uri.spec, true); syncData.req.overrideMimeType("text/plain"); - syncData.req.setRequestHeader("User-Agent", userAgent); + 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")); @@ -1093,21 +1180,21 @@ syncData.req.onerror = function () { let responseData = {}; - responseData["MS-ASProtocolVersions"] = syncData.req.getResponseHeader("MS-ASProtocolVersions"); - responseData["MS-ASProtocolCommands"] = syncData.req.getResponseHeader("MS-ASProtocolCommands"); + 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"]); + 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.req.onload = function () { syncData.setSyncState("eval.request.options"); let responseData = {}; - switch(syncData.req.status) { + switch (syncData.req.status) { case 401: // AuthError let rv = {}; rv.errorObj = eas.sync.finish("error", "401"); @@ -1116,39 +1203,39 @@ break; case 200: - responseData["MS-ASProtocolVersions"] = syncData.req.getResponseHeader("MS-ASProtocolVersions"); - responseData["MS-ASProtocolCommands"] = syncData.req.getResponseHeader("MS-ASProtocolCommands"); + 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" + + TbSync.dump("EAS OPTIONS with response (status: 200)", "\n" + "responseText: " + syncData.req.responseText + "\n" + - "responseHeader(MS-ASProtocolVersions): " + responseData["MS-ASProtocolVersions"]+"\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(); + 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(); + 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; + await oauthData.setToken("accessToken", ""); + retry = true; } else { syncData.setSyncState("passwordprompt"); let authData = eas.network.getAuthData(syncData.accountData); @@ -1160,8 +1247,8 @@ } let credentials = await TbSync.passwordManager.asyncPasswordPrompt(promptData, eas.openWindows); if (credentials) { - authData.updateLoginData(credentials.username, credentials.password); - retry = true; + await authData.updateLoginData(credentials.username, credentials.password); + retry = true; } } } @@ -1170,7 +1257,7 @@ throw result.errorObj; } } - + allowedRetries--; } while (retry); }, @@ -1185,111 +1272,112 @@ // 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); + let result = await eas.network.getServerConnectionViaAutodiscover(authData.accountname, 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("host", eas.network.stripAutodiscoverUrl(result.server)); syncData.accountData.setAccountProperty("user", result.user); - syncData.accountData.setAccountProperty("https", (result.server.substring(0,5) == "https")); + syncData.accountData.setAccountProperty("https", (result.server.substring(0, 5) == "https")); } return result.errorcode; }, - - stripAutodiscoverUrl: function(url) { + + 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); + 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) { + getServerConnectionViaAutodiscover: async function (accountname, 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}); - + + 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 }); + + 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 }); + let requests = []; let responses = []; //array of objects {url, error, server} - - for (let i=0; i< urls.length; i++) { + + 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) ); + requests.push(eas.network.getServerConnectionViaAutodiscoverRedirectWrapper( + accountname, + urls[i].url, + urls[i].user, + password, + maxtimeout + )); } try { - responses = await Promise.all(requests); + 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)); + 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}; + 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")}; + 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}; + 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; + 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) + + getServerConnectionViaAutodiscoverRedirectWrapper: async function (accountname, url, user, password, maxtimeout) { let result = {}; - let method = "HEAD"; - let connection = { url, user }; - - do { + let method = "POST"; + let connection = { accountname, 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); + 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)"); @@ -1298,13 +1386,13 @@ } 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) { - + + return new Promise(function (resolve, reject) { + let xml = '\r\n'; xml += '\r\n'; xml += '\r\n'; @@ -1312,18 +1400,17 @@ xml += 'http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006\r\n'; xml += '\r\n'; xml += '\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(); + let userAgent = eas.prefs.getCharPref("clientID.useragent"); //plus calendar.useragent.extra = Lightning/5.4.5.2 + let uri = Services.io.newURI(connection.url); + let req = getSandBoxedXHR(connection, uri); req.mozBackgroundRequest = true; - req.open(method, connection.url, true); + req.open(method, uri.spec, true); req.timeout = maxtimeout; req.setRequestHeader("User-Agent", userAgent); - - let secure = (connection.url.substring(0,8).toLowerCase() == "https://"); - + + let secure = (connection.url.substring(0, 8).toLowerCase() == "https://"); + if (method == "POST") { req.setRequestHeader("Content-Length", xml.length); req.setRequestHeader("Content-Type", "text/xml"); @@ -1331,43 +1418,44 @@ // 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}); + 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}); + // CORS violations can happen on redirects. They come back as NS_ERROR_DOM_BAD_URI. + if (error == "network::NS_ERROR_DOM_BAD_URI" && req.channel.URI.spec != uri.spec) { + resolve({ "url": req.channel.URI.spec, "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}); + 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; } //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}); + 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 + "]"); - + 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, @@ -1376,16 +1464,16 @@ try { data = eas.xmltools.getDataFromXMLString(req.responseText); } catch (e) { - resolve({"url":req.responseURL, "error":"bad response", "server":"", "user":connection.user}); + 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}); + resolve({ "url": req.responseURL, "error": "redirect found", "server": "", "user": newuser }); } else if (data.Autodiscover.Response.Action.Settings) { // get server settings @@ -1393,67 +1481,64 @@ 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}); + resolve({ "url": req.responseURL, "error": "", "server": server[count].Url, "user": connection.user }); return; } } } } else { - resolve({"url":req.responseURL, "error":"invalid", "server":"", "user":connection.user}); + resolve({ "url": req.responseURL, "error": "invalid", "server": "", "user": connection.user }); } } else { - resolve({"url":req.responseURL, "error":req.status, "server":"", "user":connection.user}); + resolve({ "url": req.responseURL, "error": req.status, "server": "", "user": connection.user }); } }; - - if (method == "HEAD") req.send(); - else req.send(xml); - + + req.send(xml); }); }, - - getServerConnectionViaAutodiscoverV2JsonRequest: function (url, maxtimeout) { + + getServerConnectionViaAutodiscoverV2JsonRequest: function (accountname, user, 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(); + return new Promise(function (resolve, reject) { + + let userAgent = eas.prefs.getCharPref("clientID.useragent"); //plus calendar.useragent.extra = Lightning/5.4.5.2 + let uri = Services.io.newURI(url); + let req = getSandBoxedXHR({ accountname, user }, uri); req.mozBackgroundRequest = true; - req.open("GET", url, true); + req.open("GET", uri.spec, 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":""}); + 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":""}); + TbSync.dump("EAS autodiscover V2 with error (" + error + ")", "\n" + url + " => \n" + req.responseURL); + resolve({ "url": req.responseURL, "error": error, "server": "" }); }; - req.onload = function() { + 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)}); + resolve({ "url": req.responseURL, "error": "", "server": eas.network.stripAutodiscoverUrl(data.Url) }); } else { - resolve({"url":req.responseURL, "error":"invalid", "server":""}); + resolve({ "url": req.responseURL, "error": "invalid", "server": "" }); } return; } - - resolve({"url":req.responseURL, "error":req.status, "server":""}); + + resolve({ "url": req.responseURL, "error": req.status, "server": "" }); }; - - req.send(); + + req.send(); }); - } + } } diff -Nru eas4tbsync-4.7/content/includes/sync.js eas4tbsync-4.11/content/includes/sync.js --- eas4tbsync-4.7/content/includes/sync.js 2023-08-18 14:51:43.000000000 +0000 +++ eas4tbsync-4.11/content/includes/sync.js 2024-08-19 18:22:14.000000000 +0000 @@ -9,6 +9,7 @@ "use strict"; var { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm"); XPCOMUtils.defineLazyModuleGetters(this, { CalRecurrenceInfo: "resource:///modules/CalRecurrenceInfo.jsm", diff -Nru eas4tbsync-4.7/content/includes/tasksync.js eas4tbsync-4.11/content/includes/tasksync.js --- eas4tbsync-4.7/content/includes/tasksync.js 2023-08-18 14:51:43.000000000 +0000 +++ eas4tbsync-4.11/content/includes/tasksync.js 2024-08-19 18:22:14.000000000 +0000 @@ -9,6 +9,7 @@ "use strict"; var { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm"); XPCOMUtils.defineLazyModuleGetters(this, { CalAlarm: "resource:///modules/CalAlarm.jsm", diff -Nru eas4tbsync-4.7/content/includes/tools.js eas4tbsync-4.11/content/includes/tools.js --- eas4tbsync-4.7/content/includes/tools.js 2023-08-18 14:51:43.000000000 +0000 +++ eas4tbsync-4.11/content/includes/tools.js 2024-08-19 18:22:14.000000000 +0000 @@ -6,7 +6,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; +"use strict"; + +var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm"); +var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); +var { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); var tools = { @@ -435,7 +441,7 @@ 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); diff -Nru eas4tbsync-4.7/content/includes/wbxmltools.js eas4tbsync-4.11/content/includes/wbxmltools.js --- eas4tbsync-4.7/content/includes/wbxmltools.js 2023-08-18 14:51:43.000000000 +0000 +++ eas4tbsync-4.11/content/includes/wbxmltools.js 2024-08-19 18:22:14.000000000 +0000 @@ -6,8 +6,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; +"use strict"; +var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm"); 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 diff -Nru eas4tbsync-4.7/content/includes/xmltools.js eas4tbsync-4.11/content/includes/xmltools.js --- eas4tbsync-4.7/content/includes/xmltools.js 2023-08-18 14:51:43.000000000 +0000 +++ eas4tbsync-4.11/content/includes/xmltools.js 2024-08-19 18:22:14.000000000 +0000 @@ -6,8 +6,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; +"use strict"; +var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm"); var xmltools = { isString : function (obj) { diff -Nru eas4tbsync-4.7/content/manager/createAccount.js eas4tbsync-4.11/content/manager/createAccount.js --- eas4tbsync-4.7/content/manager/createAccount.js 2023-08-18 14:51:43.000000000 +0000 +++ eas4tbsync-4.11/content/manager/createAccount.js 2024-08-19 18:22:14.000000000 +0000 @@ -156,7 +156,11 @@ 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 v2 = await eas.network.getServerConnectionViaAutodiscoverV2JsonRequest( + accountname, + user, + "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 @@ -173,7 +177,12 @@ error = TbSync.getString("status.404", "eas"); } } else { - let result = await eas.network.getServerConnectionViaAutodiscover(user, password, tbSyncEasNewAccount.maxTimeout*1000); + let result = await eas.network.getServerConnectionViaAutodiscover( + accountname, + user, + password, + tbSyncEasNewAccount.maxTimeout*1000 + ); if (result.server) { user = result.user; url = result.server; @@ -192,7 +201,7 @@ //add if valid if (!error) { - tbSyncEasNewAccount.addAccount(user, password, servertype, accountname, url); + await tbSyncEasNewAccount.addAccount(user, password, servertype, accountname, url); } //end validation @@ -214,6 +223,7 @@ document.getElementById("tbsync.error.message").textContent = error; document.getElementById("tbsync.error").hidden = false; } + window.sizeToContent(); }, updateAutodiscoverStatus: function () { @@ -223,7 +233,7 @@ document.getElementById('tbsync.newaccount.autodiscoverstatus').value = TbSync.getString("autodiscover.Querying","eas") + timeout; }, - addAccount (user, password, servertype, accountname, url) { + async addAccount (user, password, servertype, accountname, url) { let newAccountEntry = this.providerData.getDefaultAccountEntries(); newAccountEntry.user = user; newAccountEntry.servertype = servertype; @@ -237,7 +247,7 @@ // Add the new account. let newAccountData = this.providerData.addAccount(accountname, newAccountEntry); - eas.network.getAuthData(newAccountData).updateLoginData(user, password); + await eas.network.getAuthData(newAccountData).updateLoginData(user, password); window.close(); } diff -Nru eas4tbsync-4.7/content/manager/createAccount.xhtml eas4tbsync-4.11/content/manager/createAccount.xhtml --- eas4tbsync-4.7/content/manager/createAccount.xhtml 2023-08-18 14:51:43.000000000 +0000 +++ eas4tbsync-4.11/content/manager/createAccount.xhtml 2024-08-19 18:22:14.000000000 +0000 @@ -79,7 +79,7 @@