/*
 * 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 { ExtensionParent } = ChromeUtils.importESModule(
    "resource://gre/modules/ExtensionParent.sys.mjs"
);
var { MailServices } = ChromeUtils.importESModule(
    "resource:///modules/MailServices.sys.mjs"
);

var tbsyncExtension = ExtensionParent.GlobalManager.getExtension(
    "tbsync@jobisoft.de"
);
var { TbSync } = ChromeUtils.importESModule(
    `chrome://tbsync/content/tbsync.sys.mjs?${tbsyncExtension.manifest.version}`
);

// 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" : ""));
            directory.setStringValue("tbSyncRevision", "2");
            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");
