diff options
Diffstat (limited to 'content/provider.js')
-rw-r--r-- | content/provider.js | 729 |
1 files changed, 729 insertions, 0 deletions
diff --git a/content/provider.js b/content/provider.js new file mode 100644 index 0000000..de5af36 --- /dev/null +++ b/content/provider.js @@ -0,0 +1,729 @@ +/* + * This file is part of EAS-4-TbSync. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +// Every object in here will be loaded into TbSync.providers.<providername>. +const eas = TbSync.providers.eas; + +eas.prefs = Services.prefs.getBranch("extensions.eas4tbsync."); + +//use flags instead of strings to avoid errors due to spelling errors +eas.flags = Object.freeze({ + allowEmptyResponse: true, +}); + +eas.windowsToIanaTimezoneMap = {}; +eas.ianaToWindowsTimezoneMap = {}; +eas.cachedTimezoneData = null; +eas.defaultTimezoneInfo = null; +eas.defaultTimezone = null; +eas.utcTimezone = null; + + +/** + * Implementing the TbSync interface for external provider extensions. + */ +var Base = class { + /** + * Called during load of external provider extension to init provider. + */ + static async load() { + // Set default prefs + let branch = Services.prefs.getDefaultBranch("extensions.eas4tbsync."); + branch.setIntPref("timeout", 90000); + branch.setIntPref("maxitems", 50); + branch.setBoolPref("msTodoCompat", false); + branch.setCharPref("clientID.type", "TbSync"); + branch.setCharPref("clientID.useragent", "Thunderbird ActiveSync"); + branch.setCharPref("oauth.clientID", ""); + + eas.defaultTimezone = null; + eas.utcTimezone = null; + eas.defaultTimezoneInfo = null; + eas.windowsToIanaTimezoneMap = {}; + eas.openWindows = {}; + + try { + // Create a basic error info (no accountname or foldername, just the provider) + let eventLogInfo = new TbSync.EventLogInfo("eas"); + + //get timezone info of default timezone (old cal. without dtz are depricated) + eas.defaultTimezone = (TbSync.lightning.cal.dtz && TbSync.lightning.cal.dtz.defaultTimezone) ? TbSync.lightning.cal.dtz.defaultTimezone : TbSync.lightning.cal.calendarDefaultTimezone(); + eas.utcTimezone = (TbSync.lightning.cal.dtz && TbSync.lightning.cal.dtz.UTC) ? TbSync.lightning.cal.dtz.UTC : TbSync.lightning.cal.UTC(); + if (eas.defaultTimezone && eas.defaultTimezone.icalComponent) { + TbSync.eventlog.add("info", eventLogInfo, "Default timezone has been found."); + } else { + TbSync.eventlog.add("info", eventLogInfo, "Default timezone is not defined, using UTC!"); + eas.defaultTimezone = eas.utcTimezone; + } + + eas.defaultTimezoneInfo = eas.tools.getTimezoneInfo(eas.defaultTimezone); + if (!eas.defaultTimezoneInfo) { + TbSync.eventlog.add("info", eventLogInfo, "Could not create defaultTimezoneInfo"); + } + + //get windows timezone data from CSV + let aliasData = await eas.tools.fetchFile("chrome://eas4tbsync/content/timezonedata/Aliases.csv"); + let aliasNames = {}; + for (let i = 0; i<aliasData.length; i++) { + let lData = aliasData[i].split(","); + if (lData.length<2) continue; + aliasNames[lData[0].toString().trim()] = lData[1].toString().trim().split(" "); + } + + let csvData = await eas.tools.fetchFile("chrome://eas4tbsync/content/timezonedata/WindowsTimezone.csv"); + for (let i = 0; i<csvData.length; i++) { + let lData = csvData[i].split(","); + if (lData.length<3) continue; + + let windowsZoneName = lData[0].toString().trim(); + let zoneType = lData[1].toString().trim(); + let ianaZoneName = lData[2].toString().trim(); + + if (zoneType == "001") eas.windowsToIanaTimezoneMap[windowsZoneName] = ianaZoneName; + if (ianaZoneName == eas.defaultTimezoneInfo.std.id) eas.defaultTimezoneInfo.std.windowsZoneName = windowsZoneName; + + // build the revers map as well, which is many-to-one, grap iana aliases from the csvData and from the aliasData + // 1. multiple iana zones map to the same windows zone + let ianaZones = ianaZoneName.split(" "); + for (let ianaZone of ianaZones) { + eas.ianaToWindowsTimezoneMap[ianaZone] = windowsZoneName; + if (aliasNames.hasOwnProperty(ianaZone)) { + for (let aliasName of aliasNames[ianaZone]) { + // 2. multiple iana zonescan be an alias to a main iana zone + eas.ianaToWindowsTimezoneMap[aliasName] = windowsZoneName; + } + } + } + } + + let tzService = TbSync.lightning.cal.timezoneService; + for (let timezoneId of tzService.timezoneIds) { + if (!eas.ianaToWindowsTimezoneMap[timezoneId]) { + TbSync.eventlog.add("info", eventLogInfo, "The IANA timezone <"+timezoneId+"> cannot be mapped to any Exchange timezone."); + } + } + + //If an EAS calendar is currently NOT associated with an email identity, try to associate, + //but do not change any explicitly set association + // - A) find email identity and associate (which sets organizer to that user identity) + // - B) overwrite default organizer with current best guess + //TODO: Do this after email accounts changed, not only on restart? + let providerData = new TbSync.ProviderData("eas"); + let folders = providerData.getFolders({"selected": true, "type": ["8","13"]}); + for (let folder of folders) { + let manager = TbSync.lightning.cal.manager; + let calendar = manager.getCalendarById(folder.getFolderProperty("target")); + if (calendar && calendar.getProperty("imip.identity.key") == "") { + //is there an email identity for this eas account? + let authData = eas.network.getAuthData(folder.accountData); + + let key = eas.tools.getIdentityKey(authData.user); + if (key === "") { //TODO: Do this even after manually switching to NONE, not only on restart? + //set transient calendar organizer settings based on current best guess and + calendar.setProperty("organizerId", TbSync.lightning.cal.email.prependMailTo(authData.user)); + calendar.setProperty("organizerCN", calendar.getProperty("fallbackOrganizerName")); + } else { + //force switch to found identity + calendar.setProperty("imip.identity.key", key); + } + } + } + } catch(e) { + Components.utils.reportError(e); + } + } + + + /** + * Called during unload of external provider extension to unload provider. + */ + static async unload() { + // Close all open windows of this provider. + for (let id in eas.openWindows) { + if (eas.openWindows.hasOwnProperty(id)) { + try { + eas.openWindows[id].close(); + } catch(e) { + //NOOP + } + } + } + } + + + /** + * Returns string for the name of provider for the add account menu. + */ + static getProviderName() { + return "Exchange ActiveSync"; + } + + + /** + * Returns version of the TbSync API this provider is using + */ + static getApiVersion() { return "2.5"; } + + + /** + * Returns location of a provider icon. + */ + static getProviderIcon(size, accountData = null) { + let base = (accountData && accountData.getAccountProperty("servertype") == "office365") ? "365_" : "eas"; + + switch (size) { + case 16: + return "chrome://eas4tbsync/content/skin/" + base + "16.png"; + case 32: + return "chrome://eas4tbsync/content/skin/" + base + "32.png"; + default : + return "chrome://eas4tbsync/content/skin/" + base + "64.png"; + } + } + + + /** + * Returns a list of sponsors, they will be sorted by the index + */ + static getSponsors() { + return { + "Schiessl, Michael 1" : {name: "Michael Schiessl", description: "Tine 2.0", icon: "", link: "" }, + "Schiessl, Michael 2" : {name: "Michael Schiessl", description: " Exchange 2007", icon: "", link: "" }, + "netcup GmbH" : {name: "netcup GmbH", description : "SOGo", icon: "chrome://eas4tbsync/content/skin/sponsors/netcup.png", link: "http://www.netcup.de/" }, + "nethinks GmbH" : {name: "nethinks GmbH", description : "Zarafa", icon: "chrome://eas4tbsync/content/skin/sponsors/nethinks.png", link: "http://www.nethinks.com/" }, + "Jau, Stephan" : {name: "Stephan Jau", description: "Horde", icon: "", link: "" }, + "Zavar " : {name: "Zavar", description: "Zoho", icon: "", link: "" }, + }; + } + + + /** + * Returns the url of a page with details about contributors (used in the manager UI) + */ + static getContributorsUrl() { + return "https://github.com/jobisoft/EAS-4-TbSync/blob/master/CONTRIBUTORS.md"; + } + + + /** + * Returns the email address of the maintainer (used for bug reports). + */ + static getMaintainerEmail() { + return "john.bieling@gmx.de"; + } + + + /** + * Returns URL of the new account window. + * + * The URL will be opened via openDialog(), when the user wants to create a + * new account of this provider. + */ + static getCreateAccountWindowUrl() { + return "chrome://eas4tbsync/content/manager/createAccount.xhtml"; + } + + + /** + * Returns overlay XUL URL of the edit account dialog + * (chrome://tbsync/content/manager/editAccount.xhtml) + */ + static getEditAccountOverlayUrl() { + return "chrome://eas4tbsync/content/manager/editAccountOverlay.xhtml"; + } + + + /** + * Return object which contains all possible fields of a row in the + * accounts database with the default value if not yet stored in the + * database. + */ + static getDefaultAccountEntries() { + let row = { + "policykey" : "0", + "foldersynckey" : "0", + "deviceId" : eas.tools.getNewDeviceId(), + "asversionselected" : "auto", + "asversion" : "", + "host" : "", + "user" : "", + "servertype" : "", + "seperator" : "10", + "https" : true, + "provision" : false, + "displayoverride" : false, + "lastEasOptionsUpdate":"0", + "allowedEasVersions": "", + "allowedEasCommands": "", + "useragent": eas.prefs.getCharPref("clientID.useragent"), + "devicetype": eas.prefs.getCharPref("clientID.type"), + "synclimit" : "7", + }; + return row; + } + + + /** + * Return object which contains all possible fields of a row in the folder + * database with the default value if not yet stored in the database. + */ + static getDefaultFolderEntries() { + let folder = { + "type" : "", + "synckey" : "", + "target" : "", + "targetColor" : "", + "targetName" : "", + "parentID" : "0", + "serverID" : "", + }; + return folder; + } + + + /** + * Is called everytime an account of this provider is enabled in the + * manager UI. + */ + static onEnableAccount(accountData) { + accountData.resetAccountProperty("policykey"); + accountData.resetAccountProperty("foldersynckey"); + accountData.resetAccountProperty("lastEasOptionsUpdate"); + accountData.resetAccountProperty("lastsynctime"); + } + + + /** + * Is called everytime an account of this provider is disabled in the + * manager UI. + * + * @param accountData [in] AccountData + */ + static onDisableAccount(accountData) { + } + + + /** + * Is called everytime an account of this provider is deleted in the + * manager UI. + */ + static onDeleteAccount(accountData) { + eas.network.getAuthData(accountData).removeLoginData(); + } + + + /** + * Returns all folders of the account, sorted in the desired order. + * The most simple implementation is to return accountData.getAllFolders(); + */ + static getSortedFolders(accountData) { + let allowedTypesOrder = ["9","14","8","13","7","15"]; + + function getIdChain (aServerID) { + let serverID = aServerID; + let chain = []; + let folder; + let rootType = ""; + + // create sort string so that child folders are directly below their parent folders + do { + folder = accountData.getFolder("serverID", serverID); + if (folder) { + chain.unshift(folder.getFolderProperty("foldername")); + serverID = folder.getFolderProperty("parentID"); + rootType = folder.getFolderProperty("type"); + } + } while (folder && serverID != "0") + + // different folder types are grouped and trashed folders at the end + let pos = allowedTypesOrder.indexOf(rootType); + chain.unshift(pos == -1 ? "ZZZ" : pos.toString().padStart(3,"0")); + + return chain.join("."); + }; + + let toBeSorted = []; + let folders = accountData.getAllFolders(); + for (let f of folders) { + if (!allowedTypesOrder.includes(f.getFolderProperty("type"))) { + continue; + } + toBeSorted.push({"key": getIdChain(f.getFolderProperty("serverID")), "folder": f}); + } + + //sort + toBeSorted.sort(function(a,b) { + return a.key > b.key; + }); + + let sortedFolders = []; + for (let sortObj of toBeSorted) { + sortedFolders.push(sortObj.folder); + } + return sortedFolders; + } + + + /** + * Return the connection timeout for an active sync, so TbSync can append + * a countdown to the connection timeout, while waiting for an answer from + * the server. Only syncstates which start with "send." will trigger this. + */ + static getConnectionTimeout(accountData) { + return eas.prefs.getIntPref("timeout"); + } + + + /** + * Is called if TbSync needs to synchronize the folder list. + */ + static async syncFolderList(syncData, syncJob, syncRunNr) { + // Recommendation: Put the actual function call inside a try catch, to + // ensure returning a proper StatusData object, regardless of what + // happens inside that function. You may also throw custom errors + // in that function, which have the StatusData obj attached, which + // should be returned. + + try { + await eas.sync.folderList(syncData); + } catch (e) { + if (e.name == "eas4tbsync") { + return e.statusData; + } else { + Components.utils.reportError(e); + // re-throw any other error and let TbSync handle it + throw (e); + } + } + + // Fall through, if there was no error. + return new TbSync.StatusData(); + } + + + /** + * Is called if TbSync needs to synchronize a folder. + */ + static async syncFolder(syncData, syncJob, syncRunNr) { + // Recommendation: Put the actual function call inside a try catch, to + // ensure returning a proper StatusData object, regardless of what + // happens inside that function. You may also throw custom errors + // in that function, which have the StatusData obj attached, which + // should be returned. + + try { + switch (syncJob) { + case "deletefolder": + await eas.sync.deleteFolder(syncData); + break; + default: + await eas.sync.singleFolder(syncData); + } + } catch (e) { + if (e.name == "eas4tbsync") { + return e.statusData; + } else { + Components.utils.reportError(e); + // re-throw any other error and let TbSync handle it + throw (e); + } + } + + // Fall through, if there was no error. + return new TbSync.StatusData(); + } + + + /** + * Return the custom OAuth2 ClientID. + */ + static getCustomeOauthClientID() { + return eas.prefs.getCharPref("oauth.clientID"); + } +} + + + + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +// * TargetData implementation +// * Using TbSyncs advanced address book TargetData +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +var TargetData_addressbook = class extends TbSync.addressbook.AdvancedTargetData { + constructor(folderData) { + super(folderData); + } + + get primaryKeyField() { + return "X-EAS-SERVERID"; + } + + generatePrimaryKey() { + return TbSync.generateUUID(); + } + + // enable or disable changelog + get logUserChanges() { + return true; + } + + directoryObserver(aTopic) { + switch (aTopic) { + case "addrbook-removed": + case "addrbook-updated": + //Services.console.logStringMessage("["+ aTopic + "] " + this.folderData.getFolderProperty("foldername")); + break; + } + } + + cardObserver(aTopic, abCardItem) { + switch (aTopic) { + case "addrbook-contact-updated": + case "addrbook-contact-removed": + //Services.console.logStringMessage("["+ aTopic + "] " + abCardItem.getProperty("DisplayName")); + break; + + case "addrbook-contact-created": + { + //Services.console.logStringMessage("["+ aTopic + "] "+ abCardItem.getProperty("DisplayName")+">"); + break; + } + } + } + + listObserver(aTopic, abListItem, abListMember) { + switch (aTopic) { + case "addrbook-list-member-added": + case "addrbook-list-member-removed": + //Services.console.logStringMessage("["+ aTopic + "] MemberName: " + abListMember.getProperty("DisplayName")); + break; + + case "addrbook-list-removed": + case "addrbook-list-updated": + //Services.console.logStringMessage("["+ aTopic + "] ListName: " + abListItem.getProperty("ListName")); + break; + + case "addrbook-list-created": + //Services.console.logStringMessage("["+ aTopic + "] ListName: "+abListItem.getProperty("ListName")+">"); + break; + } + } + + async createAddressbook(newname) { + // https://searchfox.org/comm-central/source/mailnews/addrbook/src/nsDirPrefs.h + let dirPrefId = MailServices.ab.newAddressBook(newname, "", 101); + let directory = MailServices.ab.getDirectoryFromId(dirPrefId); + + eas.sync.resetFolderSyncInfo(this.folderData); + + if (directory && directory instanceof Components.interfaces.nsIAbDirectory && directory.dirPrefId == dirPrefId) { + directory.setStringValue("tbSyncIcon", "eas" + (this.folderData.accountData.getAccountProperty("servertype") == "office365" ? "_365" : "")); + return directory; + } + return null; + } +} + + + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +// * TargetData implementation +// * Using TbSyncs advanced calendar TargetData +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +var TargetData_calendar = class extends TbSync.lightning.AdvancedTargetData { + constructor(folderData) { + super(folderData); + } + + // The calendar target does not support a custom primaryKeyField, because + // the lightning implementation only allows to search for items via UID. + // Like the addressbook target, the calendar target item element has a + // primaryKey getter/setter which - however - only works on the UID. + + // enable or disable changelog + get logUserChanges() { + return true; + } + + calendarObserver(aTopic, tbCalendar, aPropertyName, aPropertyValue, aOldPropertyValue) { + switch (aTopic) { + case "onCalendarPropertyChanged": + //Services.console.logStringMessage("["+ aTopic + "] " + tbCalendar.calendar.name + " : " + aPropertyName); + break; + + case "onCalendarDeleted": + case "onCalendarPropertyDeleted": + //Services.console.logStringMessage("["+ aTopic + "] " +tbCalendar.calendar.name); + break; + } + } + + itemObserver(aTopic, tbItem, tbOldItem) { + switch (aTopic) { + case "onAddItem": + case "onModifyItem": + case "onDeleteItem": + //Services.console.logStringMessage("["+ aTopic + "] " + tbItem.nativeItem.title); + break; + } + } + + async createCalendar(newname) { + let calManager = TbSync.lightning.cal.manager; + + //Create the new standard calendar with a unique name + let newCalendar = calManager.createCalendar("storage", Services.io.newURI("moz-storage-calendar://")); + newCalendar.id = TbSync.lightning.cal.getUUID(); + newCalendar.name = newname; + + eas.sync.resetFolderSyncInfo(this.folderData); + + newCalendar.setProperty("color", this.folderData.getFolderProperty("targetColor")); + newCalendar.setProperty("relaxedMode", true); //sometimes we get "generation too old for modifyItem", check can be disabled with relaxedMode + // removed in TB78, as it seems to not fully enable the calendar, if present before registering + // https://searchfox.org/comm-central/source/calendar/base/content/calendar-management.js#385 + //newCalendar.setProperty("calendar-main-in-composite",true); + newCalendar.setProperty("readOnly", this.folderData.getFolderProperty("downloadonly")); + + switch (this.folderData.getFolderProperty("type")) { + case "8": //event + case "13": + newCalendar.setProperty("capabilities.tasks.supported", false); + newCalendar.setProperty("capabilities.events.supported", true); + break; + case "7": //todo + case "15": + newCalendar.setProperty("capabilities.tasks.supported", true); + newCalendar.setProperty("capabilities.events.supported", false); + break; + default: + newCalendar.setProperty("capabilities.tasks.supported", false); + newCalendar.setProperty("capabilities.events.supported", false); + } + + calManager.registerCalendar(newCalendar); + + let authData = eas.network.getAuthData(this.folderData.accountData); + + //is there an email identity we can associate this calendar to? + //getIdentityKey returns "" if none found, which removes any association + let key = eas.tools.getIdentityKey(authData.user); + newCalendar.setProperty("fallbackOrganizerName", newCalendar.getProperty("organizerCN")); + newCalendar.setProperty("imip.identity.key", key); + if (key === "") { + //there is no matching email identity - use current default value as best guess and remove association + //use current best guess + newCalendar.setProperty("organizerCN", newCalendar.getProperty("fallbackOrganizerName")); + newCalendar.setProperty("organizerId", TbSync.lightning.cal.email.prependMailTo(authData.user)); + } + + return newCalendar; + } +} + + + + + +/** + * This provider is implementing the StandardFolderList class instead of + * the FolderList class. + */ +var StandardFolderList = class { + /** + * Is called before the context menu of the folderlist is shown, allows to + * show/hide custom menu options based on selected folder. During an active + * sync, folderData will be null. + */ + static onContextMenuShowing(window, folderData) { + let hideContextMenuDelete = true; + if (folderData !== null) { + //if a folder in trash is selected, also show ContextMenuDelete (but only if FolderDelete is allowed) + if (eas.tools.parentIsTrash(folderData) && folderData.accountData.getAccountProperty("allowedEasCommands").split(",").includes("FolderDelete")) { + hideContextMenuDelete = false; + window.document.getElementById("TbSync.eas.FolderListContextMenuDelete").label = TbSync.getString("deletefolder.menuentry::" + folderData.getFolderProperty("foldername"), "eas"); + } + } + window.document.getElementById("TbSync.eas.FolderListContextMenuDelete").hidden = hideContextMenuDelete; + } + + + /** + * Return the icon used in the folderlist to represent the different folder + * types. + */ + static getTypeImage(folderData) { + let src = ""; + switch (folderData.getFolderProperty("type")) { + case "9": + case "14": + src = "contacts16.png"; + break; + case "8": + case "13": + src = "calendar16.png"; + break; + case "7": + case "15": + src = "todo16.png"; + break; + } + return "chrome://tbsync/content/skin/" + src; + } + + + /** + * Return the name of the folder shown in the folderlist. + */ + static getFolderDisplayName(folderData) { + let folderName = folderData.getFolderProperty("foldername"); + if (eas.tools.parentIsTrash(folderData)) folderName = TbSync.getString("recyclebin", "eas") + " | " + folderName; + return folderName; + } + + + /** + * Return the attributes for the ACL RO (readonly menu element per folder. + * (label, disabled, hidden, style, ...) + * + * Return a list of attributes and their values If both (RO+RW) do + * not return any attributes, the ACL menu is not displayed at all. + */ + static getAttributesRoAcl(folderData) { + return { + label: TbSync.getString("acl.readonly", "eas"), + }; + } + + + /** + * Return the attributes for the ACL RW (readwrite) menu element per folder. + * (label, disabled, hidden, style, ...) + * + * Return a list of attributes and their values. If both (RO+RW) do + * not return any attributes, the ACL menu is not displayed at all. + */ + static getAttributesRwAcl(folderData) { + return { + label: TbSync.getString("acl.readwrite", "eas"), + } + } +} + +Services.scriptloader.loadSubScript("chrome://eas4tbsync/content/includes/network.js", this, "UTF-8"); +Services.scriptloader.loadSubScript("chrome://eas4tbsync/content/includes/wbxmltools.js", this, "UTF-8"); +Services.scriptloader.loadSubScript("chrome://eas4tbsync/content/includes/xmltools.js", this, "UTF-8"); +Services.scriptloader.loadSubScript("chrome://eas4tbsync/content/includes/tools.js", this, "UTF-8"); +Services.scriptloader.loadSubScript("chrome://eas4tbsync/content/includes/sync.js", this, "UTF-8"); +Services.scriptloader.loadSubScript("chrome://eas4tbsync/content/includes/contactsync.js", this.sync, "UTF-8"); +Services.scriptloader.loadSubScript("chrome://eas4tbsync/content/includes/calendarsync.js", this.sync, "UTF-8"); +Services.scriptloader.loadSubScript("chrome://eas4tbsync/content/includes/tasksync.js", this.sync, "UTF-8"); |