summaryrefslogtreecommitdiffstats
path: root/content/modules/addressbook.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--content/modules/addressbook.js1149
1 files changed, 1149 insertions, 0 deletions
diff --git a/content/modules/addressbook.js b/content/modules/addressbook.js
new file mode 100644
index 0000000..d239a95
--- /dev/null
+++ b/content/modules/addressbook.js
@@ -0,0 +1,1149 @@
+/*
+ * 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;
+
+ }
+ }
+ },
+
+}