/* * This file is part of 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, { AddrBookCard: "resource:///modules/AddrBookCard.jsm" }); var addressbook = { _notifications: [ "addrbook-directory-updated", "addrbook-directory-deleted", "addrbook-contact-created", "addrbook-contact-updated", "addrbook-contact-deleted", "addrbook-list-member-added", "addrbook-list-member-removed", "addrbook-list-deleted", "addrbook-list-updated", "addrbook-list-created" ], load : async function () { for (let topic of this._notifications) { Services.obs.addObserver(this.addressbookObserver, topic); } }, unload : async function () { for (let topic of this._notifications) { Services.obs.removeObserver(this.addressbookObserver, topic); } }, getStringValue : function (ab, value, fallback) { try { return ab.getStringValue(value, fallback); } catch (e) { return fallback; } }, searchDirectory: function (uri, search) { return new Promise((resolve, reject) => { let listener = { cards : [], onSearchFinished(aResult, aErrorMsg) { resolve(this.cards); }, onSearchFoundCard(aCard) { this.cards.push(aCard.QueryInterface(Components.interfaces.nsIAbCard)); } } let result = MailServices.ab.getDirectory(uri).search(search, "", listener); }); }, // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // * AdvancedTargetData, an extended TargetData implementation, providers // * can use this as their own TargetData by extending it and just // * defining the extra methods // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * AdvancedTargetData : class { constructor(folderData) { this._folderData = folderData; this._targetObj = null; } // Check, if the target exists and return true/false. hasTarget() { let target = this._folderData.getFolderProperty("target"); let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target); return directory ? true : false; } // Returns the target obj, which TbSync should return as the target. It can // be whatever you want and is returned by FolderData.targetData.getTarget(). // If the target does not exist, it should be created. Throw a simple Error, if that // failed. async getTarget() { let target = this._folderData.getFolderProperty("target"); let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target); if (!directory) { // create a new addressbook and store its UID in folderData directory = await TbSync.addressbook.prepareAndCreateAddressbook(this._folderData); if (!directory) throw new Error("notargets"); } if (!this._targetObj || this._targetObj.UID != directory.UID) this._targetObj = new TbSync.addressbook.AbDirectory(directory, this._folderData); return this._targetObj; } /** * Removes the target from the local storage. If it does not exist, return * silently. A call to ``hasTarget()`` should return false, after this has * been executed. * */ removeTarget() { let target = this._folderData.getFolderProperty("target"); let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target); try { if (directory) { MailServices.ab.deleteAddressBook(directory.URI); } } catch (e) {} TbSync.db.clearChangeLog(target); this._folderData.resetFolderProperty("target"); } /** * Disconnects the target in the local storage from this TargetData, but * does not delete it, so it becomes a stale "left over" . A call * to ``hasTarget()`` should return false, after this has been executed. * */ disconnectTarget() { let target = this._folderData.getFolderProperty("target"); let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target); if (directory) { let changes = TbSync.db.getItemsFromChangeLog(target, 0, "_by_user"); if (changes.length > 0) { this.targetName = this.targetName + " (*)"; } directory.setStringValue("tbSyncIcon", "orphaned"); directory.setStringValue("tbSyncProvider", "orphaned"); directory.setStringValue("tbSyncAccountID", ""); } TbSync.db.clearChangeLog(target); this._folderData.resetFolderProperty("target"); } set targetName(newName) { let target = this._folderData.getFolderProperty("target"); let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target); if (directory) { directory.dirName = newName; } else { throw new Error("notargets"); } } get targetName() { let target = this._folderData.getFolderProperty("target"); let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target); if (directory) { return directory.dirName; } else { throw new Error("notargets"); } } setReadOnly(value) { } // * * * * * * * * * * * * * * * * * // * AdvancedTargetData extension * // * * * * * * * * * * * * * * * * * get isAdvancedAddressbookTargetData() { return true; } get folderData() { return this._folderData; } // define a card property, which should be used for the changelog // basically your primary key for the abItem properties // UID will be used, if nothing specified get primaryKeyField() { return "UID"; } generatePrimaryKey() { return TbSync.generateUUID(); } // enable or disable changelog get logUserChanges() { return true; } directoryObserver(aTopic) { switch (aTopic) { case "addrbook-directory-deleted": case "addrbook-directory-updated": //Services.console.logStringMessage("["+ aTopic + "] " + folderData.getFolderProperty("foldername")); break; } } cardObserver(aTopic, abCardItem) { switch (aTopic) { case "addrbook-contact-updated": case "addrbook-contact-deleted": 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-deleted": case "addrbook-list-updated": //Services.console.logStringMessage("["+ aTopic + "] ListName: " + abListItem.getProperty("ListName")); break; case "addrbook-list-created": //Services.console.logStringMessage("["+ aTopic + "] Created new X-DAV-UID for List <"+abListItem.getProperty("ListName")+">"); break; } } // replace this with your own implementation to create the actual addressbook, // when this class is extended 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); return directory; } }, // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // * AbItem and AbDirectory Classes // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * AbItem : class { constructor(abDirectory, item) { if (!abDirectory) throw new Error("AbItem::constructor is missing its first parameter!"); if (!item) throw new Error("AbItem::constructor is missing its second parameter!"); this._abDirectory = abDirectory; this._card = null; this._tempListDirectory = null; this._tempProperties = null; this._isMailList = false; if (item instanceof Components.interfaces.nsIAbDirectory) { this._tempListDirectory = item; this._isMailList = true; this._tempProperties = {}; } else { this._card = item; this._isMailList = item.isMailList; } } get abDirectory() { return this._abDirectory; } get isMailList() { return this._isMailList; } get nativeItem() { return this._card; } get UID() { if (this._tempListDirectory) return this._tempListDirectory.UID; return this._card.UID; } get primaryKey() { //use UID as fallback let key = this._abDirectory.primaryKeyField; return key ? this.getProperty(key) : this.UID; } set primaryKey(value) { //use UID as fallback let key = this._abDirectory.primaryKeyField; if (key) this.setProperty(key, value) else throw ("TbSync.addressbook.AbItem.set primaryKey: UID is used as primaryKeyField but changing the UID of an item is currently not supported. Please use a custom primaryKeyField."); } clone() { //no real clone ... this is just here to match the calendar target return new TbSync.addressbook.AbItem(this._abDirectory, this._card); } toString() { return this._card.displayName + " (" + this._card.firstName + ", " + this._card.lastName + ") <"+this._card.primaryEmail+">"; } // mailinglist aware method to get properties of cards // mailinglist properties cannot be stored in mailinglists themselves, so we store them in changelog getProperty(property, fallback = "") { if (property == "UID") return this.UID; if (this._isMailList) { const directListProperties = { ListName: "dirName", ListNickName: "listNickName", ListDescription: "description" }; let value; if (directListProperties.hasOwnProperty(property)) { try { let mailListDirectory = this._tempListDirectory || MailServices.ab.getDirectory(this._card.mailListURI); //this._card.asDirectory value = mailListDirectory[directListProperties[property]]; } catch (e) { // list does not exists } } else { value = this._tempProperties ? this._tempProperties[property] : TbSync.db.getItemStatusFromChangeLog(this._abDirectory.UID + "#" + this.UID, property); } return value || fallback; } else { return this._card.getProperty(property, fallback); } } // mailinglist aware method to set properties of cards // mailinglist properties cannot be stored in mailinglists themselves, so we store them in changelog // while the list has not been added, we keep all props in an object (UID changes on adding) setProperty(property, value) { // UID cannot be changed (currently) if (property == "UID") { throw ("TbSync.addressbook.AbItem.setProperty: UID cannot be changed currently."); return; } if (this._isMailList) { const directListProperties = { ListName: "dirName", ListNickName: "listNickName", ListDescription: "description" }; if (directListProperties.hasOwnProperty(property)) { try { let mailListDirectory = this._tempListDirectory || MailServices.ab.getDirectory(this._card.mailListURI); mailListDirectory[directListProperties[property]] = value; } catch (e) { // list does not exists } } else { if (this._tempProperties) { this._tempProperties[property] = value; } else { TbSync.db.addItemToChangeLog(this._abDirectory.UID + "#" + this.UID, property, value); } } } else { this._card.setProperty(property, value); } } deleteProperty(property) { if (this._isMailList) { if (this._tempProperties) { delete this._tempProperties[property]; } else { TbSync.db.removeItemFromChangeLog(this._abDirectory.UID + "#" + this.UID, property); } } else { this._card.deleteProperty(property); } } get changelogData() { return TbSync.db.getItemDataFromChangeLog(this._abDirectory.UID, this.primaryKey); } get changelogStatus() { return TbSync.db.getItemStatusFromChangeLog(this._abDirectory.UID, this.primaryKey); } set changelogStatus(status) { let value = this.primaryKey; if (value) { if (!status) { TbSync.db.removeItemFromChangeLog(this._abDirectory.UID, value); return; } if (this._abDirectory.logUserChanges || status.endsWith("_by_server")) { TbSync.db.addItemToChangeLog(this._abDirectory.UID, value, status); } } } // get the property given from all members and return it as an array (that property better be uniqe) getMembersPropertyList(property) { let members = []; if (this._card && this._card.isMailList) { // get mailListDirectory let mailListDirectory = MailServices.ab.getDirectory(this._card.mailListURI); for (let member of mailListDirectory.childCards) { let prop = member.getProperty(property, ""); if (prop) members.push(prop); } } return members; } addListMembers(property, candidates) { if (this._card && this._card.isMailList) { let members = this.getMembersPropertyList(property); let mailListDirectory = MailServices.ab.getDirectory(this._card.mailListURI); for (let candidate of candidates) { if (members.includes(candidate)) continue; let card = this._abDirectory._directory.getCardFromProperty(property, candidate, true); if (card) mailListDirectory.addCard(card); } } } removeListMembers(property, candidates) { if (this._card && this._card.isMailList) { let members = this.getMembersPropertyList(property); let mailListDirectory = MailServices.ab.getDirectory(this._card.mailListURI); let cardsToRemove = []; for (let candidate of candidates) { if (!members.includes(candidate)) continue; let card = this._abDirectory._directory.getCardFromProperty(property, candidate, true); if (card) cardsToRemove.push(card); } if (cardsToRemove.length > 0) mailListDirectory.deleteCards(cardsToRemove); } } addPhoto(photo, data, extension = "jpg", url = "") { let dest = []; let card = this._card; let bookUID = this.abDirectory.UID; // TbSync storage must be set as last let book64 = btoa(bookUID); let photo64 = btoa(photo); let photoName64 = book64 + "_" + photo64 + "." + extension; dest.push(["Photos", photoName64]); // I no longer see a reason for this // dest.push(["TbSync","Photos", book64, photo64]); let filePath = ""; for (let i=0; i < dest.length; i++) { let file = FileUtils.getFile("ProfD", dest[i]); let foStream = Components.classes["@mozilla.org/network/file-output-stream;1"].createInstance(Components.interfaces.nsIFileOutputStream); foStream.init(file, 0x02 | 0x08 | 0x20, 0x180, 0); // write, create, truncate let binary = ""; try { binary = atob(data.split(" ").join("")); } catch (e) { console.log("Failed to decode base64 string:", data); } foStream.write(binary, binary.length); foStream.close(); filePath = 'file:///' + file.path.replace(/\\/g, '\/').replace(/^\s*\/?/, '').replace(/\ /g, '%20'); } card.setProperty("PhotoName", photoName64); card.setProperty("PhotoType", url ? "web" : "file"); card.setProperty("PhotoURI", url ? url : filePath); return filePath; } getPhoto() { let card = this._card; let photo = card.getProperty("PhotoName", ""); let data = ""; if (photo) { try { let file = FileUtils.getFile("ProfD", ["Photos", photo]); let fiStream = Components.classes["@mozilla.org/network/file-input-stream;1"].createInstance(Components.interfaces.nsIFileInputStream); fiStream.init(file, -1, -1, false); let bstream = Components.classes["@mozilla.org/binaryinputstream;1"].createInstance(Components.interfaces.nsIBinaryInputStream); bstream.setInputStream(fiStream); data = btoa(bstream.readBytes(bstream.available())); fiStream.close(); } catch (e) {} } return data; } }, AbDirectory : class { constructor(directory, folderData) { this._directory = directory; this._folderData = folderData; } get directory() { return this._directory; } get logUserChanges() { return this._folderData.targetData.logUserChanges; } get primaryKeyField() { return this._folderData.targetData.primaryKeyField; } get UID() { return this._directory.UID; } get URI() { return this._directory.URI; } createNewCard() { let card = new AddrBookCard(); return new TbSync.addressbook.AbItem(this, card); } createNewList() { let listDirectory = Components.classes["@mozilla.org/addressbook/directoryproperty;1"].createInstance(Components.interfaces.nsIAbDirectory); listDirectory.isMailList = true; return new TbSync.addressbook.AbItem(this, listDirectory); } async addItem(abItem, pretagChangelogWithByServerEntry = true) { if (this.primaryKeyField && !abItem.getProperty(this.primaryKeyField)) { abItem.setProperty(this.primaryKeyField, this._folderData.targetData.generatePrimaryKey()); //Services.console.logStringMessage("[AbDirectory::addItem] Generated primary key!"); } if (pretagChangelogWithByServerEntry) { abItem.changelogStatus = "added_by_server"; } if (abItem.isMailList && abItem._tempListDirectory) { let list = this._directory.addMailList(abItem._tempListDirectory); // the list has been added and we can now get the corresponding card via its UID let found = await this.getItemFromProperty("UID", list.UID); // clone and clear temporary properties let props = {...abItem._tempProperties}; abItem._tempListDirectory = null; abItem._tempProperties = null; // store temporary properties for (const [property, value] of Object.entries(props)) { found.setProperty(property, value); } abItem._card = found._card; } else if (!abItem.isMailList) { this._directory.addCard(abItem._card); } else { throw new Error("Cannot re-add a list to a directory."); } } modifyItem(abItem, pretagChangelogWithByServerEntry = true) { // only add entry if the current entry does not start with _by_user let status = abItem.changelogStatus ? abItem.changelogStatus : ""; if (pretagChangelogWithByServerEntry && !status.endsWith("_by_user")) { abItem.changelogStatus = "modified_by_server"; } if (abItem.isMailList) { // get mailListDirectory let mailListDirectory = MailServices.ab.getDirectory(abItem._card.mailListURI); // store mailListDirectory.editMailListToDatabase(abItem._card); } else { this._directory.modifyCard(abItem._card); } } deleteItem(abItem, pretagChangelogWithByServerEntry = true) { if (pretagChangelogWithByServerEntry) { abItem.changelogStatus = "deleted_by_server"; } this._directory.deleteCards([abItem._card]); } async getItem(searchId) { //use UID as fallback let key = this.primaryKeyField ? this.primaryKeyField : "UID"; return await this.getItemFromProperty(key, searchId); } async getItemFromProperty(property, value) { // try to use the standard card method first let card = this._directory.getCardFromProperty(property, value, true); if (card) { return new TbSync.addressbook.AbItem(this, card); } // search for list cards // we cannot search for the prop directly, because for mailinglists // they are not part of the card (expect UID) but stored in a custom storage let searchList = "(IsMailList,=,TRUE)"; let foundCards = await TbSync.addressbook.searchDirectory(this._directory.URI, "(or" + searchList+")"); for (let aCard of foundCards) { let card = new TbSync.addressbook.AbItem(this, aCard); //does this list card have the req prop? if (card.getProperty(property) == value) { return card; } } return null; } getAllItems () { let rv = []; for (let card of this._directory.childCards) { rv.push(new TbSync.addressbook.AbItem( this._directory, card )); } return rv; } getAddedItemsFromChangeLog(maxitems = 0) { return TbSync.db.getItemsFromChangeLog(this._directory.UID, maxitems, "added_by_user").map(item => item.itemId); } getModifiedItemsFromChangeLog(maxitems = 0) { return TbSync.db.getItemsFromChangeLog(this._directory.UID, maxitems, "modified_by_user").map(item => item.itemId); } getDeletedItemsFromChangeLog(maxitems = 0) { return TbSync.db.getItemsFromChangeLog(this._directory.UID, maxitems, "deleted_by_user").map(item => item.itemId); } getItemsFromChangeLog(maxitems = 0) { // Document what this returns return TbSync.db.getItemsFromChangeLog(this._directory.UID, maxitems, "_by_user"); } removeItemFromChangeLog(id, moveToEndInsteadOfDelete = false) { TbSync.db.removeItemFromChangeLog(this._directory.UID, id, moveToEndInsteadOfDelete); } clearChangelog() { TbSync.db.clearChangeLog(this._directory.UID); } }, // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // * Internal Functions // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * prepareAndCreateAddressbook: async function (folderData) { let target = folderData.getFolderProperty("target"); let provider = folderData.accountData.getAccountProperty("provider"); // Get cached or new unique name for new address book let cachedName = folderData.getFolderProperty("targetName"); let newname = cachedName == "" ? folderData.accountData.getAccountProperty("accountname") + " (" + folderData.getFolderProperty("foldername")+ ")" : cachedName; //Create the new book with the unique name let directory = await folderData.targetData.createAddressbook(newname); if (directory && directory instanceof Components.interfaces.nsIAbDirectory) { directory.setStringValue("tbSyncProvider", provider); directory.setStringValue("tbSyncAccountID", folderData.accountData.accountID); // Prevent gContactSync to inject its stuff into New/EditCard dialogs // https://github.com/jdgeenen/gcontactsync/pull/127 directory.setStringValue("gContactSyncSkipped", "true"); folderData.setFolderProperty("target", directory.UID); folderData.setFolderProperty("targetName", directory.dirName); //notify about new created address book Services.obs.notifyObservers(null, 'tbsync.observer.addressbook.created', null) return directory; } return null; }, getFolderFromDirectoryUID: function(bookUID) { let folders = TbSync.db.findFolders({"target": bookUID}); if (folders.length == 1) { let accountData = new TbSync.AccountData(folders[0].accountID); return new TbSync.FolderData(accountData, folders[0].folderID); } return null; }, getDirectoryFromDirectoryUID: function(UID) { if (!UID) return null; for (let directory of MailServices.ab.directories) { if (directory instanceof Components.interfaces.nsIAbDirectory) { if (directory.UID == UID) return directory; } } return null; }, getListInfoFromListUID: async function(UID) { for (let directory of MailServices.ab.directories) { if (directory instanceof Components.interfaces.nsIAbDirectory && !directory.isRemote) { let searchList = "(IsMailList,=,TRUE)"; let foundCards = await TbSync.addressbook.searchDirectory(directory.URI, "(and" + searchList+")"); for (let listCard of foundCards) { //return after first found card if (listCard.UID == UID) return {directory, listCard}; } } } throw new Error("List with UID <" + UID + "> does not exists"); }, // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // * Addressbook Observer and Listener // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * addressbookObserver: { observe: async function (aSubject, aTopic, aData) { switch (aTopic) { // we do not need addrbook-created case "addrbook-directory-updated": case "addrbook-directory-deleted": { //aSubject: nsIAbDirectory (we can get URI and UID directly from the object, but the directory no longer exists) aSubject.QueryInterface(Components.interfaces.nsIAbDirectory); let bookUID = aSubject.UID; let folderData = TbSync.addressbook.getFolderFromDirectoryUID(bookUID); if (folderData && folderData.targetData && folderData.targetData.isAdvancedAddressbookTargetData) { switch(aTopic) { case "addrbook-directory-updated": { //update name of target (if changed) folderData.setFolderProperty("targetName", aSubject.dirName); //update settings window, if open Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", folderData.accountID); } break; case "addrbook-directory-deleted": { //delete any pending changelog of the deleted book TbSync.db.clearChangeLog(bookUID); //unselect book if deleted by user and update settings window, if open if (folderData.getFolderProperty("selected")) { folderData.setFolderProperty("selected", false); //update settings window, if open Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", folderData.accountID); } folderData.resetFolderProperty("target"); } break; } folderData.targetData.directoryObserver(aTopic); } } break; case "addrbook-contact-created": case "addrbook-contact-updated": case "addrbook-contact-deleted": { //aSubject: nsIAbCard aSubject.QueryInterface(Components.interfaces.nsIAbCard); //aData: 128-bit unique identifier for the parent directory let bookUID = aData; let folderData = TbSync.addressbook.getFolderFromDirectoryUID(bookUID); if (folderData && folderData.targetData && folderData.targetData.isAdvancedAddressbookTargetData) { let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(bookUID); let abDirectory = new TbSync.addressbook.AbDirectory(directory, folderData); let abItem = new TbSync.addressbook.AbItem(abDirectory, aSubject); let itemStatus = abItem.changelogStatus || ""; // during create the following can happen // card has no primary key // another process could try to mod // -> we need to identify this card with an always available ID and block any other MODS until we free it again // -> store creation type if (aTopic == "addrbook-contact-created" && itemStatus == "") { // add this new card to changelog to keep track of it TbSync.db.addItemToChangeLog(bookUID, aSubject.UID + "#DelayedUserCreation", Date.now()); // new cards must get a NEW(!) primaryKey first if (abDirectory.primaryKeyField) { console.log("New primary Key generated!"); abItem.setProperty(abDirectory.primaryKeyField, folderData.targetData.generatePrimaryKey()); } // special case: do not add "modified_by_server" abDirectory.modifyItem(abItem, /*pretagChangelogWithByServerEntry */ false); // We will see this card again as updated but delayed created return; } // during follow up MODs we can identify this card via let delayedUserCreation = TbSync.db.getItemStatusFromChangeLog(bookUID, aSubject.UID + "#DelayedUserCreation"); // if we reach this point and if we have adelayedUserCreation, // we can remove the delayedUserCreation marker and can // continue to process this event as an addrbook-contact-created let bTopic = aTopic; if (delayedUserCreation) { let age = Date.now() - delayedUserCreation; if (age < 1500) { bTopic = "addrbook-contact-created"; } else { TbSync.db.removeItemFromChangeLog(bookUID, aSubject.UID + "#DelayedUserCreation"); } } // if this card was created by us, it will be in the log // we want to ignore any MOD for a freeze time, because // gContactSync modifies our(!) contacts (GoogleID) after we added them, so they get // turned into "modified_by_user" and will be send back to the server. if (itemStatus && itemStatus.endsWith("_by_server")) { let age = Date.now() - abItem.changelogData.timestamp; if (age < 1500) { // during freeze, local modifications are not possible return; } else { // remove blocking entry from changelog after freeze time is over (1.5s), // and continue evaluating this event abItem.changelogStatus = ""; } } // From here on, we only process user changes as server changes are self freezed // update changelog based on old status switch (bTopic) { case "addrbook-contact-created": { switch (itemStatus) { case "added_by_user": // late create notification break; case "modified_by_user": // late create notification abItem.changelogStatus = "added_by_user"; break; case "deleted_by_user": // unprocessed delete for this card, undo the delete (moved out and back in) abItem.changelogStatus = "modified_by_user"; break; default: // new card abItem.changelogStatus = "added_by_user"; } } break; case "addrbook-contact-updated": { switch (itemStatus) { case "added_by_user": // unprocessed add for this card, keep status break; case "modified_by_user": // double notification, keep status break; case "deleted_by_user": // race? unprocessed delete for this card, moved out and back in and modified default: abItem.changelogStatus = "modified_by_user"; break; } } break; case "addrbook-contact-deleted": { switch (itemStatus) { case "added_by_user": // unprocessed add for this card, revert abItem.changelogStatus = ""; return; case "deleted_by_user": // double notification break; case "modified_by_user": // unprocessed mod for this card default: abItem.changelogStatus = "deleted_by_user"; break; } } break; } if (abDirectory.logUserChanges) TbSync.core.setTargetModified(folderData); // notify observers only if status changed if (itemStatus != abItem.changelogStatus) { folderData.targetData.cardObserver(bTopic, abItem); } return; } } break; case "addrbook-list-created": case "addrbook-list-deleted": { //aSubject: nsIAbDirectory aSubject.QueryInterface(Components.interfaces.nsIAbDirectory); //aData: 128-bit unique identifier for the parent directory let bookUID = aData; let folderData = TbSync.addressbook.getFolderFromDirectoryUID(bookUID); if (folderData && folderData.targetData && folderData.targetData.isAdvancedAddressbookTargetData) { let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(bookUID); let abDirectory = new TbSync.addressbook.AbDirectory(directory, folderData); let abItem = new TbSync.addressbook.AbItem(abDirectory, aSubject); let itemStatus = abItem.changelogStatus; if (itemStatus && itemStatus.endsWith("_by_server")) { //we caused this, ignore abItem.changelogStatus = ""; return; } // update changelog based on old status switch (aTopic) { case "addrbook-list-created": { if (abDirectory.primaryKeyField) { // Since we do not need to update a list, to make custom properties persistent, we do not need to use delayedUserCreation as with contacts. abItem.setProperty(abDirectory.primaryKeyField, folderData.targetData.generatePrimaryKey()); } switch (itemStatus) { case "added_by_user": // double notification, which is probably impossible, keep status break; case "modified_by_user": // late create notification abItem.changelogStatus = "added_by_user"; break; case "deleted_by_user": // unprocessed delete for this card, undo the delete (moved out and back in) abItem.changelogStatus = "modified_by_user"; break; default: // new list abItem.changelogStatus = "added_by_user"; break; } } break; case "addrbook-list-deleted": { switch (itemStatus) { case "added_by_user": // unprocessed add for this card, revert abItem.changelogStatus = ""; return; case "modified_by_user": // unprocessed mod for this card case "deleted_by_user": // double notification default: abItem.changelogStatus = "deleted_by_user"; break; } //remove properties of this ML stored in changelog TbSync.db.clearChangeLog(abDirectory.UID + "#" + abItem.UID); } break; } if (abDirectory.logUserChanges) TbSync.core.setTargetModified(folderData); folderData.targetData.listObserver(aTopic, abItem, null); } } break; case "addrbook-list-updated": { // aSubject: nsIAbDirectory aSubject.QueryInterface(Components.interfaces.nsIAbDirectory); // get the card representation of this list, including its parent directory let listInfo = await TbSync.addressbook.getListInfoFromListUID(aSubject.UID); let bookUID = listInfo.directory.UID; let folderData = TbSync.addressbook.getFolderFromDirectoryUID(bookUID); if (folderData && folderData.targetData && folderData.targetData.isAdvancedAddressbookTargetData) { let abDirectory = new TbSync.addressbook.AbDirectory(listInfo.directory, folderData); let abItem = new TbSync.addressbook.AbItem(abDirectory, listInfo.listCard); let itemStatus = abItem.changelogStatus; if (itemStatus && itemStatus.endsWith("_by_server")) { //we caused this, ignore abItem.changelogStatus = ""; return; } // update changelog based on old status switch (aTopic) { case "addrbook-list-updated": { switch (itemStatus) { case "added_by_user": // unprocessed add for this card, keep status break; case "modified_by_user": // double notification, keep status break; case "deleted_by_user": // race? unprocessed delete for this card, moved out and back in and modified default: abItem.changelogStatus = "modified_by_user"; break; } } break; } if (abDirectory.logUserChanges) TbSync.core.setTargetModified(folderData); folderData.targetData.listObserver(aTopic, abItem, null); } } break; // unknown, if called for programmatically added members as well, probably not case "addrbook-list-member-added": //exclude contact without Email - notification is wrongly send case "addrbook-list-member-removed": { //aSubject: nsIAbCard of Member aSubject.QueryInterface(Components.interfaces.nsIAbCard); //aData: 128-bit unique identifier for the list let listInfo = await TbSync.addressbook.getListInfoFromListUID(aData); let bookUID = listInfo.directory.UID; let folderData = TbSync.addressbook.getFolderFromDirectoryUID(bookUID); if (folderData && folderData.targetData && folderData.targetData.isAdvancedAddressbookTargetData) { let abDirectory = new TbSync.addressbook.AbDirectory(listInfo.directory, folderData); let abItem = new TbSync.addressbook.AbItem(abDirectory, listInfo.listCard); let abMember = new TbSync.addressbook.AbItem(abDirectory, aSubject); if (abDirectory.logUserChanges) TbSync.core.setTargetModified(folderData); folderData.targetData.listObserver(aTopic, abItem, abMember); // removed, added members cause the list to be changed let mailListDirectory = MailServices.ab.getDirectory(listInfo.listCard.mailListURI); TbSync.addressbook.addressbookObserver.observe(mailListDirectory, "addrbook-list-updated", null); return; } } break; } } }, }