/* * This file is part of EAS-4-TbSync. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; var { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetters(this, { CalRecurrenceInfo: "resource:///modules/CalRecurrenceInfo.jsm", }); // - https://dxr.mozilla.org/comm-central/source/calendar/base/public/calIEvent.idl // - https://dxr.mozilla.org/comm-central/source/calendar/base/public/calIItemBase.idl // - https://dxr.mozilla.org/comm-central/source/calendar/base/public/calICalendar.idl // - https://dxr.mozilla.org/comm-central/source/calendar/base/modules/calAsyncUtils.jsm // https://msdn.microsoft.com/en-us/library/dd299454(v=exchg.80).aspx var sync = { finish: function (aStatus = "", msg = "", details = "") { let status = TbSync.StatusData.SUCCESS switch (aStatus) { case "": case "ok": status = TbSync.StatusData.SUCCESS; break; case "info": status = TbSync.StatusData.INFO; break; case "resyncAccount": status = TbSync.StatusData.ACCOUNT_RERUN; break; case "resyncFolder": status = TbSync.StatusData.FOLDER_RERUN; break; case "warning": status = TbSync.StatusData.WARNING; break; case "error": status = TbSync.StatusData.ERROR; break; default: console.log("TbSync/EAS: Unknown status <"+aStatus+">"); status = TbSync.StatusData.ERROR; break; } let e = new Error(); e.name = "eas4tbsync"; e.message = status.toUpperCase() + ": " + msg.toString() + " (" + details.toString() + ")"; e.statusData = new TbSync.StatusData(status, msg.toString(), details.toString()); return e; }, resetFolderSyncInfo: function (folderData) { folderData.resetFolderProperty("synckey"); folderData.resetFolderProperty("lastsynctime"); }, // update folders avail on server and handle added, removed and renamed // folders folderList: async function(syncData) { //should we recheck options/commands? Always check, if we have no info about asversion! if (syncData.accountData.getAccountProperty("asversion", "") == "" || (Date.now() - syncData.accountData.getAccountProperty("lastEasOptionsUpdate")) > 86400000 ) { await eas.network.getServerOptions(syncData); } //only update the actual used asversion, if we are currently not connected or it has not yet been set if (syncData.accountData.getAccountProperty("asversion", "") == "" || !syncData.accountData.isConnected()) { //eval the currently in the UI selected EAS version let asversionselected = syncData.accountData.getAccountProperty("asversionselected"); let allowedVersionsString = syncData.accountData.getAccountProperty("allowedEasVersions").trim(); let allowedVersionsArray = allowedVersionsString.split(","); if (asversionselected == "auto") { if (allowedVersionsArray.includes("14.0")) syncData.accountData.setAccountProperty("asversion", "14.0"); else if (allowedVersionsArray.includes("2.5")) syncData.accountData.setAccountProperty("asversion", "2.5"); else if (allowedVersionsString == "") { throw eas.sync.finish("error", "InvalidServerOptions"); } else { throw eas.sync.finish("error", "nosupportedeasversion::"+allowedVersionsArray.join(", ")); } } else if (allowedVersionsString != "" && !allowedVersionsArray.includes(asversionselected)) { throw eas.sync.finish("error", "notsupportedeasversion::"+asversionselected+"::"+allowedVersionsArray.join(", ")); } else { //just use the value set by the user syncData.accountData.setAccountProperty("asversion", asversionselected); } } //do we need to get a new policy key? if (syncData.accountData.getAccountProperty("provision") && syncData.accountData.getAccountProperty("policykey") == "0") { await eas.network.getPolicykey(syncData); } //set device info await eas.network.setDeviceInformation(syncData); syncData.setSyncState("prepare.request.folders"); let foldersynckey = syncData.accountData.getAccountProperty("foldersynckey"); //build WBXML to request foldersync let wbxml = eas.wbxmltools.createWBXML(); wbxml.switchpage("FolderHierarchy"); wbxml.otag("FolderSync"); wbxml.atag("SyncKey", foldersynckey); wbxml.ctag(); syncData.setSyncState("send.request.folders"); let response = await eas.network.sendRequest(wbxml.getBytes(), "FolderSync", syncData); syncData.setSyncState("eval.response.folders"); let wbxmlData = eas.network.getDataFromResponse(response); eas.network.checkStatus(syncData, wbxmlData,"FolderSync.Status"); let synckey = eas.xmltools.getWbxmlDataField(wbxmlData,"FolderSync.SyncKey"); if (synckey) { syncData.accountData.setAccountProperty("foldersynckey", synckey); } else { throw eas.sync.finish("error", "wbxmlmissingfield::FolderSync.SyncKey"); } // If we reach this point, wbxmlData contains FolderSync node, // so the next "if" will not fail with an javascript error, no need // to use save getWbxmlDataField function. // Are there any changes in folder hierarchy? if (wbxmlData.FolderSync.Changes) { // Looking for additions. let add = eas.xmltools.nodeAsArray(wbxmlData.FolderSync.Changes.Add); for (let count = 0; count < add.length; count++) { // Only add allowed folder types to DB (include trash(4), so we can find trashed folders. if (!["9","14","8","13","7","15", "4"].includes(add[count].Type)) continue; let existingFolder = syncData.accountData.getFolder("serverID", add[count].ServerId); if (existingFolder) { // Server has send us an ADD for a folder we alreay have, treat as update. existingFolder.setFolderProperty("foldername", add[count].DisplayName); existingFolder.setFolderProperty("type", add[count].Type); existingFolder.setFolderProperty("parentID", add[count].ParentId); } else { // Create folder obj for new folder settings. let newFolder = syncData.accountData.createNewFolder(); switch (add[count].Type) { case "9": // contact case "14": newFolder.setFolderProperty("targetType", "addressbook"); break; case "8": // event case "13": newFolder.setFolderProperty("targetType", "calendar"); break; case "7": // todo case "15": newFolder.setFolderProperty("targetType", "calendar"); break; default: newFolder.setFolderProperty("targetType", "unknown type ("+add[count].Type+")"); break; } newFolder.setFolderProperty("serverID", add[count].ServerId); newFolder.setFolderProperty("foldername", add[count].DisplayName); newFolder.setFolderProperty("type", add[count].Type); newFolder.setFolderProperty("parentID", add[count].ParentId); // Do we have a cached folder? let cachedFolderData = syncData.accountData.getFolderFromCache("serverID", add[count].ServerId); if (cachedFolderData) { // Copy fields from cache which we want to re-use. newFolder.setFolderProperty("targetColor", cachedFolderData.getFolderProperty("targetColor")); newFolder.setFolderProperty("targetName", cachedFolderData.getFolderProperty("targetName")); newFolder.setFolderProperty("downloadonly", cachedFolderData.getFolderProperty("downloadonly")); } } } // Looking for updates. let update = eas.xmltools.nodeAsArray(wbxmlData.FolderSync.Changes.Update); for (let count = 0; count < update.length; count++) { let existingFolder = syncData.accountData.getFolder("serverID", update[count].ServerId); if (existingFolder) { // Update folder. existingFolder.setFolderProperty("foldername", update[count].DisplayName); existingFolder.setFolderProperty("type", update[count].Type); existingFolder.setFolderProperty("parentID", update[count].ParentId); } } // Looking for deletes. Do not delete the targets, // but keep them as stale/unconnected elements. let del = eas.xmltools.nodeAsArray(wbxmlData.FolderSync.Changes.Delete); for (let count = 0; count < del.length; count++) { let existingFolder = syncData.accountData.getFolder("serverID", del[count].ServerId); if (existingFolder) { existingFolder.remove("[deleted on server]"); } } } }, deleteFolder: async function (syncData) { if (!syncData.currentFolderData) { return; } if (!syncData.accountData.getAccountProperty("allowedEasCommands").split(",").includes("FolderDelete")) { throw eas.sync.finish("error", "notsupported::FolderDelete"); } syncData.setSyncState("prepare.request.deletefolder"); let foldersynckey = syncData.accountData.getAccountProperty("foldersynckey"); //request foldersync let wbxml = eas.wbxmltools.createWBXML(); wbxml.switchpage("FolderHierarchy"); wbxml.otag("FolderDelete"); wbxml.atag("SyncKey", foldersynckey); wbxml.atag("ServerId", syncData.currentFolderData.getFolderProperty("serverID")); wbxml.ctag(); syncData.setSyncState("send.request.deletefolder"); let response = await eas.network.sendRequest(wbxml.getBytes(), "FolderDelete", syncData); syncData.setSyncState("eval.response.deletefolder"); let wbxmlData = eas.network.getDataFromResponse(response); eas.network.checkStatus(syncData, wbxmlData,"FolderDelete.Status"); let synckey = eas.xmltools.getWbxmlDataField(wbxmlData,"FolderDelete.SyncKey"); if (synckey) { syncData.accountData.setAccountProperty("foldersynckey", synckey); syncData.currentFolderData.remove(); } else { throw eas.sync.finish("error", "wbxmlmissingfield::FolderDelete.SyncKey"); } }, singleFolder: async function (syncData) { // add target to syncData try { // accessing the target for the first time will check if it is avail and if not will create it (if possible) syncData.target = await syncData.currentFolderData.targetData.getTarget(); } catch (e) { Components.utils.reportError(e); throw eas.sync.finish("warning", e.message); } //get syncData type, which is also used in WBXML for the CLASS element syncData.type = null; switch (syncData.currentFolderData.getFolderProperty("type")) { case "9": //contact case "14": syncData.type = "Contacts"; break; case "8": //event case "13": syncData.type = "Calendar"; break; case "7": //todo case "15": syncData.type = "Tasks"; break; default: throw eas.sync.finish("info", "skipped"); break; } syncData.setSyncState("preparing"); //get synckey if needed syncData.synckey = syncData.currentFolderData.getFolderProperty("synckey"); if (syncData.synckey == "") { await eas.network.getSynckey(syncData); } //sync folder syncData.timeOfLastSync = syncData.currentFolderData.getFolderProperty( "lastsynctime") / 1000; syncData.timeOfThisSync = (Date.now() / 1000) - 1; let lightningBatch = false; let lightningReadOnly = ""; let error = null; // We ned to intercept any throw error, because lightning needs a few operations after sync finished try { switch (syncData.type) { case "Contacts": await eas.sync.easFolder(syncData); break; case "Calendar": case "Tasks": //save current value of readOnly (or take it from the setting) lightningReadOnly = syncData.target.calendar.getProperty("readOnly") || syncData.currentFolderData.getFolderProperty( "downloadonly"); syncData.target.calendar.setProperty("readOnly", false); lightningBatch = true; syncData.target.calendar.startBatch(); await eas.sync.easFolder(syncData); break; } } catch (report) { error = report; } if (lightningBatch) { syncData.target.calendar.endBatch(); syncData.target.calendar.setProperty("readOnly", lightningReadOnly); } if (error) throw error; }, // --------------------------------------------------------------------------- // MAIN FUNCTIONS TO SYNC AN EAS FOLDER // --------------------------------------------------------------------------- easFolder: async function (syncData) { syncData.progressData.reset(); if (syncData.currentFolderData.getFolderProperty("downloadonly")) { await eas.sync.revertLocalChanges(syncData); } await eas.network.getItemEstimate(syncData); await eas.sync.requestRemoteChanges(syncData); if (!syncData.currentFolderData.getFolderProperty("downloadonly")) { let sendChanges = await eas.sync.sendLocalChanges(syncData); if (sendChanges) { // This is ugly as hell, but Microsoft sometimes sets the state of the // remote account to "changed" after we have send a local change (even // though it has acked the change) and this will cause the server to // send a change request with our next sync. Because we follow the // "server wins" policy, this will overwrite any additional local change // we have done in the meantime. This is stupid, but we wait 2s and // hope it is enough to catch this second ack of the local change. let timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer); await new Promise(function(resolve, reject) { let event = { notify: function(timer) { resolve(); } } timer.initWithCallback(event, 2000, Components.interfaces.nsITimer.TYPE_ONE_SHOT); }); await eas.sync.requestRemoteChanges(syncData); } } }, requestRemoteChanges: async function (syncData) { do { syncData.setSyncState("prepare.request.remotechanges"); syncData.request = ""; syncData.response = ""; // BUILD WBXML let wbxml = eas.wbxmltools.createWBXML(); wbxml.otag("Sync"); wbxml.otag("Collections"); wbxml.otag("Collection"); if (syncData.accountData.getAccountProperty("asversion") == "2.5") wbxml.atag("Class", syncData.type); wbxml.atag("SyncKey", syncData.synckey); wbxml.atag("CollectionId", syncData.currentFolderData.getFolderProperty("serverID")); wbxml.atag("DeletesAsMoves"); wbxml.atag("GetChanges"); wbxml.atag("WindowSize", eas.prefs.getIntPref("maxitems").toString()); if (syncData.accountData.getAccountProperty("asversion") != "2.5") { wbxml.otag("Options"); if (syncData.type == "Calendar") wbxml.atag("FilterType", syncData.currentFolderData.accountData.getAccountProperty("synclimit")); wbxml.atag("Class", syncData.type); wbxml.switchpage("AirSyncBase"); wbxml.otag("BodyPreference"); wbxml.atag("Type", "1"); wbxml.ctag(); wbxml.switchpage("AirSync"); wbxml.ctag(); } else if (syncData.type == "Calendar") { //in 2.5 we only send it to filter Calendar wbxml.otag("Options"); wbxml.atag("FilterType", syncData.currentFolderData.accountData.getAccountProperty("synclimit")); wbxml.ctag(); } wbxml.ctag(); wbxml.ctag(); wbxml.ctag(); //SEND REQUEST syncData.setSyncState("send.request.remotechanges"); let response = await eas.network.sendRequest(wbxml.getBytes(), "Sync", syncData); //VALIDATE RESPONSE // get data from wbxml response, some servers send empty response if there are no changes, which is not an error let wbxmlData = eas.network.getDataFromResponse(response, eas.flags.allowEmptyResponse); if (wbxmlData === null) return; //check status, throw on error eas.network.checkStatus(syncData, wbxmlData,"Sync.Collections.Collection.Status"); //PROCESS COMMANDS await eas.sync.processCommands(wbxmlData, syncData); //Update count in UI syncData.setSyncState("eval.response.remotechanges"); //update synckey eas.network.updateSynckey(syncData, wbxmlData); if (!eas.xmltools.hasWbxmlDataField(wbxmlData,"Sync.Collections.Collection.MoreAvailable")) { //Feedback from users: They want to see the final count await TbSync.tools.sleep(100); return; } } while (true); }, sendLocalChanges: async function (syncData) { let maxnumbertosend = eas.prefs.getIntPref("maxitems"); syncData.progressData.reset(0, syncData.target.getItemsFromChangeLog().length); //keep track of failed items syncData.failedItems = []; let done = false; let numberOfItemsToSend = maxnumbertosend; let sendChanges = false; do { syncData.setSyncState("prepare.request.localchanges"); syncData.request = ""; syncData.response = ""; //get changed items from ChangeLog let changes = syncData.target.getItemsFromChangeLog(numberOfItemsToSend); //console.log("chnages", changes); let c=0; let e=0; //keep track of send items during this request let changedItems = []; let addedItems = {}; let sendItems = []; // BUILD WBXML let wbxml = eas.wbxmltools.createWBXML(); wbxml.otag("Sync"); wbxml.otag("Collections"); wbxml.otag("Collection"); if (syncData.accountData.getAccountProperty("asversion") == "2.5") wbxml.atag("Class", syncData.type); wbxml.atag("SyncKey", syncData.synckey); wbxml.atag("CollectionId", syncData.currentFolderData.getFolderProperty("serverID")); wbxml.otag("Commands"); for (let i=0; i 0) { //if there was at least one actual local change, send request sendChanges = true; //SEND REQUEST & VALIDATE RESPONSE syncData.setSyncState("send.request.localchanges"); let response = await eas.network.sendRequest(wbxml.getBytes(), "Sync", syncData); syncData.setSyncState("eval.response.localchanges"); //get data from wbxml response let wbxmlData = eas.network.getDataFromResponse(response); //check status and manually handle error states which support softfails let errorcause = eas.network.checkStatus(syncData, wbxmlData, "Sync.Collections.Collection.Status", "", true); switch (errorcause) { case "": break; case "Sync.4": //Malformed request case "Sync.6": //Invalid item //some servers send a global error - to catch this, we reduce the number of items we send to the server if (sendItems.length == 1) { //the request contained only one item, so we know which one failed if (sendItems[0].type == "deleted_by_user") { //we failed to delete an item, discard and place message in log syncData.target.removeItemFromChangeLog(sendItems[0].id); TbSync.eventlog.add("warning", syncData.eventLogInfo, "ErrorOnDelete::"+sendItems[0].id); } else { let foundItem = await syncData.target.getItem(sendItems[0].id); if (foundItem) { eas.sync.updateFailedItems(syncData, errorcause, foundItem.primaryKey, foundItem.toString()); } else { //should not happen syncData.target.removeItemFromChangeLog(sendItems[0].id); } } syncData.progressData.inc(); //restore numberOfItemsToSend numberOfItemsToSend = maxnumbertosend; } else if (sendItems.length > 1) { //reduce further numberOfItemsToSend = Math.min(1, Math.round(sendItems.length / 5)); } else { //sendItems.length == 0 ??? recheck but this time let it handle all cases eas.network.checkStatus(syncData, wbxmlData, "Sync.Collections.Collection.Status"); } break; default: //recheck but this time let it handle all cases eas.network.checkStatus(syncData, wbxmlData, "Sync.Collections.Collection.Status"); } await TbSync.tools.sleep(10, true); if (errorcause == "") { //PROCESS RESPONSE await eas.sync.processResponses(wbxmlData, syncData, addedItems, changedItems); //PROCESS COMMANDS await eas.sync.processCommands(wbxmlData, syncData); //remove all items from changelog that did not fail for (let a=0; a < changedItems.length; a++) { syncData.target.removeItemFromChangeLog(changedItems[a]); syncData.progressData.inc(); } //update synckey eas.network.updateSynckey(syncData, wbxmlData); } } else if (e==0) { //if there was no local change and also no error (which will not happen twice) finish done = true; } } while (!done); //was there an error? if (syncData.failedItems.length > 0) { throw eas.sync.finish("warning", "ServerRejectedSomeItems::" + syncData.failedItems.length); } return sendChanges; }, revertLocalChanges: async function (syncData) { let maxnumbertosend = eas.prefs.getIntPref("maxitems"); syncData.progressData.reset(0, syncData.target.getItemsFromChangeLog().length); if (syncData.progressData.todo == 0) { return; } let viaItemOperations = (syncData.accountData.getAccountProperty("allowedEasCommands").split(",").includes("ItemOperations")); //get changed items from ChangeLog do { syncData.setSyncState("prepare.request.revertlocalchanges"); let changes = syncData.target.getItemsFromChangeLog(maxnumbertosend); let c=0; syncData.request = ""; syncData.response = ""; // BUILD WBXML let wbxml = eas.wbxmltools.createWBXML(); if (viaItemOperations) { wbxml.switchpage("ItemOperations"); wbxml.otag("ItemOperations"); } else { wbxml.otag("Sync"); wbxml.otag("Collections"); wbxml.otag("Collection"); if (syncData.accountData.getAccountProperty("asversion") == "2.5") wbxml.atag("Class", syncData.type); wbxml.atag("SyncKey", syncData.synckey); wbxml.atag("CollectionId", syncData.currentFolderData.getFolderProperty("serverID")); wbxml.otag("Commands"); } for (let i=0; i 0) { //if there was at least one actual local change, send request let error = false; let wbxmlData = ""; //SEND REQUEST & VALIDATE RESPONSE try { syncData.setSyncState("send.request.revertlocalchanges"); let response = await eas.network.sendRequest(wbxml.getBytes(), (viaItemOperations) ? "ItemOperations" : "Sync", syncData); syncData.setSyncState("eval.response.revertlocalchanges"); //get data from wbxml response wbxmlData = eas.network.getDataFromResponse(response); } catch (e) { //we do not handle errors, IF there was an error, wbxmlData is empty and will trigger the fallback } let fetchPath = (viaItemOperations) ? "ItemOperations.Response.Fetch" : "Sync.Collections.Collection.Responses.Fetch"; if (eas.xmltools.hasWbxmlDataField(wbxmlData, fetchPath)) { //looking for additions let add = eas.xmltools.nodeAsArray(eas.xmltools.getWbxmlDataField(wbxmlData, fetchPath)); for (let count = 0; count < add.length; count++) { await TbSync.tools.sleep(10, true); let ServerId = add[count].ServerId; let data = (viaItemOperations) ? add[count].Properties : add[count].ApplicationData; if (data && ServerId) { let foundItem = await syncData.target.getItem(ServerId); if (!foundItem) { //do NOT add, if an item with that ServerId was found let newItem = eas.sync.createItem(syncData); try { eas.sync[syncData.type].setThunderbirdItemFromWbxml(newItem, data, ServerId, syncData); await syncData.target.addItem(newItem); } catch (e) { eas.xmltools.printXmlData(add[count], true); //include application data in log TbSync.eventlog.add("warning", syncData.eventLogInfo, "BadItemSkipped::JavaScriptError", newItem.toString()); throw e; // unable to add item to Thunderbird - fatal error } } else { //should not happen, since we deleted that item beforehand syncData.target.removeItemFromChangeLog(ServerId); } syncData.progressData.inc(); } else { error = true; break; } } } else { error = true; } if (error) { //if ItemOperations.Fetch fails, fall back to Sync.Fetch, if that fails, fall back to resync if (viaItemOperations) { viaItemOperations = false; TbSync.eventlog.add("info", syncData.eventLogInfo, "Server returned error during ItemOperations.Fetch, falling back to Sync.Fetch."); } else { await eas.sync.revertLocalChangesViaResync(syncData); return; } } } else { //if there was no more local change we need to revert, return return; } } while (true); }, revertLocalChangesViaResync: async function (syncData) { TbSync.eventlog.add("info", syncData.eventLogInfo, "Server does not support ItemOperations.Fetch and/or Sync.Fetch, must revert via resync."); let changes = syncData.target.getItemsFromChangeLog(); syncData.progressData.reset(0, changes.length); syncData.setSyncState("prepare.request.revertlocalchanges"); //remove all changes, so we can get them fresh from the server for (let i=0; i-1) changedItems.splice(p,1); } } } //looking for deletions let del = eas.xmltools.nodeAsArray(wbxmlData.Sync.Collections.Collection.Responses.Delete); for (let count = 0; count < del.length; count++) { //What can we do about failed deletes? SyncLog eas.network.checkStatus(syncData, del[count],"Status","Sync.Collections.Collection.Responses.Delete["+count+"].Status", true); } } }, // --------------------------------------------------------------------------- // HELPER FUNCTIONS AND DEFINITIONS // --------------------------------------------------------------------------- MAP_EAS2TB : { //EAS Importance: 0 = LOW | 1 = NORMAL | 2 = HIGH Importance : { "0":"9", "1":"5", "2":"1"}, //to PRIORITY //EAS Sensitivity : 0 = Normal | 1 = Personal | 2 = Private | 3 = Confidential Sensitivity : { "0":"PUBLIC", "1":"PRIVATE", "2":"PRIVATE", "3":"CONFIDENTIAL"}, //to CLASS //EAS BusyStatus: 0 = Free | 1 = Tentative | 2 = Busy | 3 = Work | 4 = Elsewhere BusyStatus : {"0":"TRANSPARENT", "1":"unset", "2":"OPAQUE", "3":"OPAQUE", "4":"OPAQUE"}, //to TRANSP //EAS AttendeeStatus: 0 =Response unknown (but needed) | 2 = Tentative | 3 = Accept | 4 = Decline | 5 = Not responded (and not needed) || 1 = Organizer in ResponseType ATTENDEESTATUS : {"0": "NEEDS-ACTION", "1":"Orga", "2":"TENTATIVE", "3":"ACCEPTED", "4":"DECLINED", "5":"ACCEPTED"}, }, MAP_TB2EAS : { //TB PRIORITY: 9 = LOW | 5 = NORMAL | 1 = HIGH PRIORITY : { "9":"0", "5":"1", "1":"2","unset":"1"}, //to Importance //TB CLASS: PUBLIC, PRIVATE, CONFIDENTIAL) CLASS : { "PUBLIC":"0", "PRIVATE":"2", "CONFIDENTIAL":"3", "unset":"0"}, //to Sensitivity //TB TRANSP : free = TRANSPARENT, busy = OPAQUE) TRANSP : {"TRANSPARENT":"0", "unset":"1", "OPAQUE":"2"}, // to BusyStatus //TB STATUS: NEEDS-ACTION, ACCEPTED, DECLINED, TENTATIVE, (DELEGATED, COMPLETED, IN-PROCESS - for todo) ATTENDEESTATUS : {"NEEDS-ACTION":"0", "ACCEPTED":"3", "DECLINED":"4", "TENTATIVE":"2", "DELEGATED":"5","COMPLETED":"5", "IN-PROCESS":"5"}, }, mapEasPropertyToThunderbird : function (easProp, tbProp, data, item) { if (data[easProp]) { //store original EAS value let easPropValue = eas.xmltools.checkString(data[easProp]); item.setProperty("X-EAS-" + easProp, easPropValue); //map EAS value to TB value (use setCalItemProperty if there is one option which can unset/delete the property) eas.tools.setCalItemProperty(item, tbProp, eas.sync.MAP_EAS2TB[easProp][easPropValue]); } }, mapThunderbirdPropertyToEas: function (tbProp, easProp, item) { if (item.hasProperty("X-EAS-" + easProp) && eas.tools.getCalItemProperty(item, tbProp) == eas.sync.MAP_EAS2TB[easProp][item.getProperty("X-EAS-" + easProp)]) { //we can use our stored EAS value, because it still maps to the current TB value return item.getProperty("X-EAS-" + easProp); } else { return eas.sync.MAP_TB2EAS[tbProp][eas.tools.getCalItemProperty(item, tbProp)]; } }, getEasItemType(aItem) { if (aItem instanceof TbSync.addressbook.AbItem) { return "Contacts"; } else if (aItem instanceof TbSync.lightning.TbItem) { return aItem.isTodo ? "Tasks" : "Calendar"; } else { throw "Unknown aItem."; } }, createItem(syncData) { switch (syncData.type) { case "Contacts": return syncData.target.createNewCard(); break; case "Tasks": return syncData.target.createNewTodo(); break; case "Calendar": return syncData.target.createNewEvent(); break; default: throw "Unknown item type <" + syncData.type + ">"; } }, async getWbxmlFromThunderbirdItem(item, syncData, isException = false) { try { let wbxml = await eas.sync[syncData.type].getWbxmlFromThunderbirdItem(item, syncData, isException); return wbxml; } catch (e) { TbSync.eventlog.add("warning", syncData.eventLogInfo, "BadItemSkipped::JavaScriptError", item.toString()); throw e; // unable to read item from Thunderbird - fatal error } }, // --------------------------------------------------------------------------- // LIGHTNING HELPER FUNCTIONS AND DEFINITIONS // These functions are needed only by tasks and events, so they // are placed here, even though they are not type independent, // but I did not want to add another "lightning" sub layer. // // The item in these functions is a native lightning item. // --------------------------------------------------------------------------- setItemSubject: function (item, syncData, data) { if (data.Subject) item.title = eas.xmltools.checkString(data.Subject); }, setItemLocation: function (item, syncData, data) { if (data.Location) item.setProperty("location", eas.xmltools.checkString(data.Location)); }, setItemCategories: function (item, syncData, data) { if (data.Categories && data.Categories.Category) { let cats = []; if (Array.isArray(data.Categories.Category)) cats = data.Categories.Category; else cats.push(data.Categories.Category); item.setCategories(cats); } }, getItemCategories: function (item, syncData) { let asversion = syncData.accountData.getAccountProperty("asversion"); let wbxml = eas.wbxmltools.createWBXML("", syncData.type); //init wbxml with "" and not with precodes, also activate type codePage (Calendar, Tasks, Contacts etc) //to properly "blank" categories, we need to always include the container let categories = item.getCategories({}); if (categories.length > 0) { wbxml.otag("Categories"); for (let i=0; i 0 && */ data.Body.Data) item.setProperty("description", eas.xmltools.checkString(data.Body.Data)); //EstimatedDataSize is optional } }, getItemBody: function (item, syncData) { let asversion = syncData.accountData.getAccountProperty("asversion"); let wbxml = eas.wbxmltools.createWBXML("", syncData.type); //init wbxml with "" and not with precodes, also activate type codePage (Calendar, Tasks, Contacts etc) let description = (item.hasProperty("description")) ? item.getProperty("description") : ""; if (asversion == "2.5") { wbxml.atag("Body", description); } else { wbxml.switchpage("AirSyncBase"); wbxml.otag("Body"); wbxml.atag("Type", "1"); wbxml.atag("EstimatedDataSize", "" + description.length); wbxml.atag("Data", description); wbxml.ctag(); //does not work with horde at the moment, does not work with task, does not work with exceptions //if (syncData.accountData.getAccountProperty("horde") == "0") wbxml.atag("NativeBodyType", "1"); //return to code page of this type wbxml.switchpage(syncData.type); } return wbxml.getBytes(); }, //item is a native lightning item setItemRecurrence: function (item, syncData, data, timezone) { if (data.Recurrence) { item.recurrenceInfo = new CalRecurrenceInfo(); item.recurrenceInfo.item = item; let recRule = TbSync.lightning.cal.createRecurrenceRule(); switch (data.Recurrence.Type) { case "0": recRule.type = "DAILY"; break; case "1": recRule.type = "WEEKLY"; break; case "2": case "3": recRule.type = "MONTHLY"; break; case "5": case "6": recRule.type = "YEARLY"; break; } if (data.Recurrence.CalendarType) { // TODO } if (data.Recurrence.DayOfMonth) { recRule.setComponent("BYMONTHDAY", [data.Recurrence.DayOfMonth]); } if (data.Recurrence.DayOfWeek) { let DOW = data.Recurrence.DayOfWeek; if (DOW == 127 && (recRule.type == "MONTHLY" || recRule.type == "YEARLY")) { recRule.setComponent("BYMONTHDAY", [-1]); } else { let days = []; for (let i = 0; i < 7; ++i) { if (DOW & 1 << i) days.push(i + 1); } if (data.Recurrence.WeekOfMonth) { for (let i = 0; i < days.length; ++i) { if (data.Recurrence.WeekOfMonth == 5) { days[i] = -1 * (days[i] + 8); } else { days[i] += 8 * (data.Recurrence.WeekOfMonth - 0); } } } recRule.setComponent("BYDAY", days); } } if (data.Recurrence.FirstDayOfWeek) { //recRule.setComponent("WKST", [data.Recurrence.FirstDayOfWeek]); // WKST is not a valid component //recRule.weekStart = data.Recurrence.FirstDayOfWeek; // - (NS_ERROR_NOT_IMPLEMENTED) [calIRecurrenceRule.weekStart] TbSync.eventlog.add("info", syncData.eventLogInfo, "FirstDayOfWeek tag ignored (not supported).", item.icalString); } if (data.Recurrence.Interval) { recRule.interval = data.Recurrence.Interval; } if (data.Recurrence.IsLeapMonth) { // TODO } if (data.Recurrence.MonthOfYear) { recRule.setComponent("BYMONTH", [data.Recurrence.MonthOfYear]); } if (data.Recurrence.Occurrences) { recRule.count = data.Recurrence.Occurrences; } if (data.Recurrence.Until) { //time string could be in compact/basic or extended form of ISO 8601, //cal.createDateTime only supports compact/basic, our own method takes both styles recRule.untilDate = eas.tools.createDateTime(data.Recurrence.Until); } if (data.Recurrence.Start) { TbSync.eventlog.add("info", syncData.eventLogInfo, "Start tag in recurring task is ignored, recurrence will start with first entry.", item.icalString); } item.recurrenceInfo.insertRecurrenceItemAt(recRule, 0); if (data.Exceptions && syncData.type == "Calendar") { // only events, tasks cannot have exceptions // Exception could be an object or an array of objects let exceptions = [].concat(data.Exceptions.Exception); for (let exception of exceptions) { //exception.ExceptionStartTime is in UTC, but the Recurrence Object is in local timezone let dateTime = TbSync.lightning.cal.createDateTime(exception.ExceptionStartTime).getInTimezone(timezone); if (data.AllDayEvent == "1") { dateTime.isDate = true; // Pass to replacement event unless overriden if (!exception.AllDayEvent) { exception.AllDayEvent = "1"; } } if (exception.Deleted == "1") { item.recurrenceInfo.removeOccurrenceAt(dateTime); } else { let replacement = item.recurrenceInfo.getOccurrenceFor(dateTime); // replacement is a native lightning item, so we can access its id via .id eas.sync[syncData.type].setThunderbirdItemFromWbxml(replacement, exception, replacement.id, syncData, "recurrence"); // Reminders should carry over from parent, but setThunderbirdItemFromWbxml clears all alarms if (!exception.Reminder && item.getAlarms({}).length) { replacement.addAlarm(item.getAlarms({})[0]); } // Removing a reminder requires EAS 16.0 item.recurrenceInfo.modifyException(replacement, true); } } } } }, getItemRecurrence: async function (item, syncData, localStartDate = null) { let asversion = syncData.accountData.getAccountProperty("asversion"); let wbxml = eas.wbxmltools.createWBXML("", syncData.type); //init wbxml with "" and not with precodes, also activate type codePage (Calendar, Tasks etc) if (item.recurrenceInfo && (syncData.type == "Calendar" || syncData.type == "Tasks")) { let deleted = []; let hasRecurrence = false; let startDate = (syncData.type == "Calendar") ? item.startDate : item.entryDate; for (let recRule of item.recurrenceInfo.getRecurrenceItems({})) { if (recRule.date) { if (recRule.isNegative) { // EXDATE deleted.push(recRule); } else { // RDATE TbSync.eventlog.add("info", syncData.eventLogInfo, "Ignoring RDATE rule (not supported)", recRule.icalString); } continue; } if (recRule.isNegative) { // EXRULE TbSync.eventlog.add("info", syncData.eventLogInfo, "Ignoring EXRULE rule (not supported)", recRule.icalString); continue; } // RRULE wbxml.otag("Recurrence"); hasRecurrence = true; let type = 0; let monthDays = recRule.getComponent("BYMONTHDAY", {}); let weekDays = recRule.getComponent("BYDAY", {}); let months = recRule.getComponent("BYMONTH", {}); let weeks = []; // Unpack 1MO style days for (let i = 0; i < weekDays.length; ++i) { if (weekDays[i] > 8) { weeks[i] = Math.floor(weekDays[i] / 8); weekDays[i] = weekDays[i] % 8; } else if (weekDays[i] < -8) { // EAS only supports last week as a special value, treat // all as last week or assume every month has 5 weeks? // Change to last week //weeks[i] = 5; // Assumes 5 weeks per month for week <= -2 weeks[i] = 6 - Math.floor(-weekDays[i] / 8); weekDays[i] = -weekDays[i] % 8; } } if (monthDays[0] && monthDays[0] == -1) { weeks = [5]; weekDays = [1, 2, 3, 4, 5, 6, 7]; // 127 monthDays[0] = null; } // Type if (recRule.type == "WEEKLY") { type = 1; if (!weekDays.length) { weekDays = [startDate.weekday + 1]; } } else if (recRule.type == "MONTHLY" && weeks.length) { type = 3; } else if (recRule.type == "MONTHLY") { type = 2; if (!monthDays.length) { monthDays = [startDate.day]; } } else if (recRule.type == "YEARLY" && weeks.length) { type = 6; } else if (recRule.type == "YEARLY") { type = 5; if (!monthDays.length) { monthDays = [startDate.day]; } if (!months.length) { months = [startDate.month + 1]; } } wbxml.atag("Type", type.toString()); //Tasks need a Start tag, but we cannot allow a start date different from the start of the main item (thunderbird does not support that) if (localStartDate) wbxml.atag("Start", localStartDate); // TODO: CalendarType: 14.0 and up // DayOfMonth if (monthDays[0]) { // TODO: Multiple days of month - multiple Recurrence tags? wbxml.atag("DayOfMonth", monthDays[0].toString()); } // DayOfWeek if (weekDays.length) { let bitfield = 0; for (let day of weekDays) { bitfield |= 1 << (day - 1); } wbxml.atag("DayOfWeek", bitfield.toString()); } // FirstDayOfWeek: 14.1 and up //wbxml.atag("FirstDayOfWeek", recRule.weekStart); - (NS_ERROR_NOT_IMPLEMENTED) [calIRecurrenceRule.weekStart] // Interval wbxml.atag("Interval", recRule.interval.toString()); // TODO: IsLeapMonth: 14.0 and up // MonthOfYear if (months.length) { wbxml.atag("MonthOfYear", months[0].toString()); } // Occurrences if (recRule.isByCount) { wbxml.atag("Occurrences", recRule.count.toString()); } // Until else if (recRule.untilDate != null) { //Events need the Until data in compact form, Tasks in the basic form wbxml.atag("Until", eas.tools.getIsoUtcString(recRule.untilDate, (syncData.type == "Tasks"))); } // WeekOfMonth if (weeks.length) { wbxml.atag("WeekOfMonth", weeks[0].toString()); } wbxml.ctag(); } if (syncData.type == "Calendar" && hasRecurrence) { //Exceptions only allowed in Calendar and only if a valid Recurrence was added let modifiedIds = item.recurrenceInfo.getExceptionIds({}); if (deleted.length || modifiedIds.length) { wbxml.otag("Exceptions"); for (let exception of deleted) { wbxml.otag("Exception"); wbxml.atag("ExceptionStartTime", eas.tools.getIsoUtcString(exception.date)); wbxml.atag("Deleted", "1"); //Docs say it is allowed, but if present, it does not work //if (asversion == "2.5") { // wbxml.atag("UID", item.id); //item.id is not valid, use UID or primaryKey //} wbxml.ctag(); } for (let exceptionId of modifiedIds) { let replacement = item.recurrenceInfo.getExceptionFor(exceptionId); wbxml.otag("Exception"); wbxml.atag("ExceptionStartTime", eas.tools.getIsoUtcString(exceptionId)); wbxml.append(await eas.sync.getWbxmlFromThunderbirdItem(replacement, syncData, true)); wbxml.ctag(); } wbxml.ctag(); } } } return wbxml.getBytes(); } }