Bug 1614265 - Implement nsIAbManager in javascript. r=mkmelin
authorGeoff Lankow <geoff@darktrojan.net>
Wed, 12 Feb 2020 23:16:27 +0000
changeset 37385 a94a4098acb456ab8ad149007e9732fe6a9cd64e
parent 37384 306ac4b77c679440f825d487a9147544591db953
child 37386 3204b6752a70b3d144ceebdf36bf9414e06281dd
push id2566
push userclokep@gmail.com
push dateMon, 09 Mar 2020 19:20:31 +0000
treeherdercomm-beta@a352facfa0a4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs1614265
Bug 1614265 - Implement nsIAbManager in javascript. r=mkmelin This revision recreates the manager only for the JS directory type. Other types will be added in later revisions. As such, a number of tests are marked as failing. Other code has been adjusted due to XPCOM behaving differently when not crossing from C++ to/from javascript. Also deleting a directory now notifies listeners after the file (if any) has been closed and removed, instead of immediately. Differential Revision: https://phabricator.services.mozilla.com/D62392
mail/base/content/editContactPanel.js
mail/base/modules/QuickFilterManager.jsm
mail/components/addrbook/content/abTrees.js
mail/components/addrbook/content/menulist-addrbooks.js
mail/components/addrbook/test/browser/browser.ini
mail/components/extensions/parent/ext-addressBook.js
mail/components/extensions/test/browser/browser_ext_quickFilter.js
mail/components/extensions/test/xpcshell/test_ext_addressBook.js
mail/test/browser/addrbook/browser.ini
mail/test/browser/message-window/browser.ini
mail/test/browser/quick-filter-bar/browser_filterLogic.js
mailnews/addrbook/content/abResultsPane.js
mailnews/addrbook/jsaddrbook/AddrBookDirectory.jsm
mailnews/addrbook/jsaddrbook/AddrBookManager.jsm
mailnews/addrbook/jsaddrbook/components.conf
mailnews/addrbook/jsaddrbook/moz.build
mailnews/addrbook/prefs/content/pref-editdirectories.js
mailnews/addrbook/public/nsIAbManager.idl
mailnews/addrbook/src/AbAutoCompleteSearch.jsm
mailnews/addrbook/src/moz.build
mailnews/addrbook/src/nsAbView.cpp
mailnews/addrbook/src/nsDirPrefs.cpp
mailnews/addrbook/test/unit/test_jsaddrbook.js
mailnews/addrbook/test/unit/test_migration8.js
mailnews/addrbook/test/unit/test_notifications.js
mailnews/addrbook/test/unit/test_nsAbManager2.js
mailnews/addrbook/test/unit/test_nsAbManager3.js
mailnews/addrbook/test/unit/test_nsAbManager4.js
mailnews/addrbook/test/unit/xpcshell.ini
mailnews/addrbook/test/unit/xpcshell_migration.ini
mailnews/build/nsMailModule.cpp
mailnews/compose/src/nsMsgCompose.cpp
mailnews/import/src/nsImportAddressBooks.cpp
mailnews/import/test/unit/resources/import_helper.js
mailnews/import/test/unit/xpcshell.ini
--- a/mail/base/content/editContactPanel.js
+++ b/mail/base/content/editContactPanel.js
@@ -146,32 +146,33 @@ var editContactInlineUI = {
       "editContactAddressBookList"
     ).value = this._cardDetails.book.URI;
 
     // Is this card contained within mailing lists?
     let inMailList = false;
     if (this._cardDetails.book.supportsMailingLists) {
       // We only have to look in one book here, because cards currently have
       // to be in the address book they belong to.
-      let mailingLists = this._cardDetails.book.childNodes;
-      while (mailingLists.hasMoreElements() && !inMailList) {
-        let list = mailingLists.getNext();
-        if (!(list instanceof Ci.nsIAbDirectory) || !list.isMailList) {
+      for (let list of this._cardDetails.book.childNodes) {
+        if (!list.isMailList) {
           continue;
         }
 
         for (let card of fixIterator(list.addressLists)) {
           if (
             card instanceof Ci.nsIAbCard &&
             card.primaryEmail == this._cardDetails.card.primaryEmail
           ) {
             inMailList = true;
             break;
           }
         }
+        if (inMailList) {
+          break;
+        }
       }
     }
 
     if (!this._writeable || inMailList) {
       document.getElementById("editContactAddressBookList").disabled = true;
     }
 
     if (inMailList) {
--- a/mail/base/modules/QuickFilterManager.jsm
+++ b/mail/base/modules/QuickFilterManager.jsm
@@ -622,17 +622,17 @@ QuickFilterManager.defineFilter({
 QuickFilterManager.defineFilter({
   name: "addrBook",
   domId: "qfb-inaddrbook",
   appendTerms(aTermCreator, aTerms, aFilterValue) {
     let term, value;
     let firstBook = true;
     term = null;
     for (let addrbook of MailServices.ab.directories) {
-      if (addrbook instanceof Ci.nsIAbDirectory && !addrbook.isRemote) {
+      if (!addrbook.isRemote) {
         term = aTermCreator.createTerm();
         term.attrib = Ci.nsMsgSearchAttrib.Sender;
         value = term.value;
         value.attrib = term.attrib;
         value.str = addrbook.URI;
         term.value = value;
         term.op = aFilterValue
           ? Ci.nsMsgSearchOp.IsInAB
--- a/mail/components/addrbook/content/abTrees.js
+++ b/mail/components/addrbook/content/abTrees.js
@@ -214,18 +214,29 @@ directoryTreeView.prototype = {
    *       Callers should take care to re-select a desired row after calling
    *       this function.
    */
   _rebuild() {
     var oldCount = this._rowMap.length;
     this._rowMap = [];
 
     // Make an entry for All Address Books.
-    let rootAB = MailServices.ab.getDirectory(kAllDirectoryRoot + "?");
-    rootAB.dirName = gAddressBookBundle.getString("allAddressBooks");
+    let rootAB = {
+      QueryInterface: ChromeUtils.generateQI([Ci.nsIAbDirectory]),
+
+      dirName: gAddressBookBundle.getString("allAddressBooks"),
+      isMailList: false,
+      isRemote: false,
+      isSecure: false,
+      URI: kAllDirectoryRoot + "?",
+
+      get childNodes() {
+        return MailServices.ab.directories;
+      },
+    };
     this._rowMap.push(new abDirTreeItem(rootAB));
 
     // Sort our addressbooks now
     this._rowMap.sort(abSort);
 
     if (this._tree) {
       this._tree.rowCountChanged(0, this._rowMap.length - oldCount);
     }
@@ -240,17 +251,19 @@ directoryTreeView.prototype = {
       }
     }
 
     return -1;
   },
 
   // nsIAbListener interfaces
   onItemAdded(aParent, aItem) {
-    if (!(aItem instanceof Ci.nsIAbDirectory)) {
+    try {
+      aItem.QueryInterface(Ci.nsIAbDirectory);
+    } catch (ex) {
       return;
     }
     // XXX we can optimize this later
     this._rebuild();
 
     if (!this._tree) {
       return;
     }
@@ -260,28 +273,31 @@ directoryTreeView.prototype = {
       if (row.id == aItem.URI) {
         this.selection.select(i);
         break;
       }
     }
   },
 
   onItemRemoved(aParent, aItem) {
-    if (!(aItem instanceof Ci.nsIAbDirectory)) {
+    try {
+      aItem.QueryInterface(Ci.nsIAbDirectory);
+    } catch (ex) {
       return;
     }
     // XXX we can optimize this later
     this._rebuild();
 
     if (!this._tree) {
       return;
     }
 
     // If we're deleting a top-level address-book, just select the first book
     if (
+      !aParent ||
       aParent.URI == kAllDirectoryRoot ||
       aParent.URI == kAllDirectoryRoot + "?"
     ) {
       this.selection.select(0);
       return;
     }
 
     // Now select this parent item
@@ -289,17 +305,19 @@ directoryTreeView.prototype = {
       if (row.id == aParent.URI) {
         this.selection.select(i);
         break;
       }
     }
   },
 
   onItemPropertyChanged(aItem, aProp, aOld, aNew) {
-    if (!(aItem instanceof Ci.nsIAbDirectory)) {
+    try {
+      aItem.QueryInterface(Ci.nsIAbDirectory);
+    } catch (ex) {
       return;
     }
 
     for (let i in this._rowMap) {
       if (this._rowMap[i]._directory == aItem) {
         this._tree.invalidateRow(i);
         break;
       }
--- a/mail/components/addrbook/content/menulist-addrbooks.js
+++ b/mail/components/addrbook/content/menulist-addrbooks.js
@@ -32,62 +32,73 @@ if (!customElements.get("menulist")) {
       this._directories = [];
 
       this._rebuild();
 
       // @implements {nsIAbListener}
       this.addressBookListener = {
         onItemAdded: (aParentDir, aItem) => {
           // Are we interested in this new directory?
-          if (aItem instanceof Ci.nsIAbDirectory && this._matches(aItem)) {
+          try {
+            aItem.QueryInterface(Ci.nsIAbDirectory);
+          } catch (ex) {
+            return;
+          }
+          if (this._matches(aItem)) {
             this._rebuild();
           }
         },
 
         onItemRemoved: (aParentDir, aItem) => {
-          if (aItem instanceof Ci.nsIAbDirectory) {
-            // Find the item in the list to remove.
-            // We can't use indexOf here because we need loose equality.
-            let len = this._directories.length;
-            for (var index = len - 1; index >= 0; index--) {
-              if (this._directories[index] == aItem) {
-                break;
-              }
+          try {
+            aItem.QueryInterface(Ci.nsIAbDirectory);
+          } catch (ex) {
+            return;
+          }
+          // Find the item in the list to remove.
+          // We can't use indexOf here because we need loose equality.
+          let len = this._directories.length;
+          for (var index = len - 1; index >= 0; index--) {
+            if (this._directories[index] == aItem) {
+              break;
             }
-            if (index != -1) {
-              this._directories.splice(index, 1);
-              // Are we removing the selected directory?
-              if (
-                this.selectedItem ==
-                this.menupopup.removeChild(this.menupopup.children[index])
-              ) {
-                // If so, try to select the first directory, if available.
-                if (this.menupopup.hasChildNodes()) {
-                  this.menupopup.firstElementChild.doCommand();
-                } else {
-                  this.selectedItem = null;
-                }
+          }
+          if (index != -1) {
+            this._directories.splice(index, 1);
+            // Are we removing the selected directory?
+            if (
+              this.selectedItem ==
+              this.menupopup.removeChild(this.menupopup.children[index])
+            ) {
+              // If so, try to select the first directory, if available.
+              if (this.menupopup.hasChildNodes()) {
+                this.menupopup.firstElementChild.doCommand();
+              } else {
+                this.selectedItem = null;
               }
             }
           }
         },
 
         onItemPropertyChanged: (aItem, aProperty, aOldValue, aNewValue) => {
-          if (aItem instanceof Ci.nsIAbDirectory) {
-            // Find the item in the list to rename.
-            // We can't use indexOf here because we need loose equality.
-            let len = this._directories.length;
-            for (var oldIndex = len - 1; oldIndex >= 0; oldIndex--) {
-              if (this._directories[oldIndex] == aItem) {
-                break;
-              }
+          try {
+            aItem.QueryInterface(Ci.nsIAbDirectory);
+          } catch (ex) {
+            return;
+          }
+          // Find the item in the list to rename.
+          // We can't use indexOf here because we need loose equality.
+          let len = this._directories.length;
+          for (var oldIndex = len - 1; oldIndex >= 0; oldIndex--) {
+            if (this._directories[oldIndex] == aItem) {
+              break;
             }
-            if (oldIndex != -1) {
-              this._rebuild();
-            }
+          }
+          if (oldIndex != -1) {
+            this._rebuild();
           }
         },
       };
 
       MailServices.ab.addAddressBookListener(
         this.addressBookListener,
         Ci.nsIAbListener.all
       );
@@ -118,17 +129,17 @@ if (!customElements.get("menulist")) {
       this._teardown();
     }
 
     _rebuild() {
       // Init the address book cache.
       this._directories.length = 0;
 
       for (let ab of MailServices.ab.directories) {
-        if (ab instanceof Ci.nsIAbDirectory && this._matches(ab)) {
+        if (this._matches(ab)) {
           this._directories.push(ab);
 
           if (this.getAttribute("mailinglists") == "true") {
             // Also append contained mailinglists.
             for (let list of ab.childNodes) {
               if (this._matches(list)) {
                 this._directories.push(list);
               }
--- a/mail/components/addrbook/test/browser/browser.ini
+++ b/mail/components/addrbook/test/browser/browser.ini
@@ -7,10 +7,11 @@ prefs =
   mail.provider.suppress_dialog_on_startup=true
   mail.spotlight.firstRunDone=true
   mail.winsearch.firstRunDone=true
   mailnews.start_page.override_url=about:blank
   mailnews.start_page.url=about:blank
 subsuite = thunderbird
 
 [browser_ldap_search.js]
+fail-if = true
 support-files = ../../../../../mailnews/addrbook/test/unit/data/ldap_contacts.json
 [browser_mailing_lists.js]
--- a/mail/components/extensions/parent/ext-addressBook.js
+++ b/mail/components/extensions/parent/ext-addressBook.js
@@ -221,17 +221,19 @@ var addressBookCache = new (class extend
         break;
     }
 
     return copy;
   }
 
   // nsIAbListener
   onItemAdded(parent, item) {
-    parent.QueryInterface(Ci.nsIAbDirectory);
+    if (parent) {
+      parent.QueryInterface(Ci.nsIAbDirectory);
+    } // Otherwise item is a new address book and we don't use parent.
 
     if (item instanceof Ci.nsIAbDirectory) {
       item.QueryInterface(Ci.nsIAbDirectory);
       if (item.isMailList) {
         let newNode = this._makeDirectoryNode(item, parent);
         if (
           this._addressBooks &&
           this._addressBooks.has(parent.UID) &&
@@ -261,17 +263,19 @@ var addressBookCache = new (class extend
           this._mailingLists.get(parent.UID).contacts.set(newNode.id, newNode);
         }
         this.emit("mailing-list-member-added", newNode);
       }
     }
   }
   // nsIAbListener
   onItemRemoved(parent, item) {
-    parent = parent.QueryInterface(Ci.nsIAbDirectory);
+    if (parent) {
+      parent = parent.QueryInterface(Ci.nsIAbDirectory);
+    } // Otherwise item is a removed address book and we don't use parent.
 
     if (item instanceof Ci.nsIAbDirectory) {
       item.QueryInterface(Ci.nsIAbDirectory);
       if (item.isMailList) {
         this._mailingLists.delete(item.UID);
         if (
           this._addressBooks &&
           this._addressBooks.has(parent.UID) &&
@@ -467,19 +471,27 @@ this.addressBook = class extends Extensi
           let dirName = MailServices.ab.newAddressBook(name, "", kJSDirectory);
           let directory = MailServices.ab.getDirectoryFromId(dirName);
           return directory.UID;
         },
         update(id, { name }) {
           let node = addressBookCache.findAddressBookById(id);
           node.item.dirName = name;
         },
-        delete(id) {
+        async delete(id) {
           let node = addressBookCache.findAddressBookById(id);
+          let deletePromise = new Promise(resolve => {
+            let listener = () => {
+              addressBookCache.off("address-book-deleted", listener);
+              resolve();
+            };
+            addressBookCache.on("address-book-deleted", listener);
+          });
           MailServices.ab.deleteAddressBook(node.item.URI);
+          await deletePromise;
         },
 
         onCreated: new EventManager({
           context,
           name: "addressBooks.onCreated",
           register: fire => {
             let listener = (event, node) => {
               fire.sync(addressBookCache.convert(node));
@@ -690,17 +702,18 @@ this.addressBook = class extends Extensi
           let node = addressBookCache.findMailingListById(id);
           node.item.dirName = name;
           node.item.listNickName = nickName === null ? "" : nickName;
           node.item.description = description === null ? "" : description;
           node.item.editMailListToDatabase(null);
         },
         delete(id) {
           let node = addressBookCache.findMailingListById(id);
-          MailServices.ab.deleteAddressBook(node.item.URI);
+          let parentNode = addressBookCache.findAddressBookById(node.parentId);
+          parentNode.item.deleteDirectory(node.item);
         },
 
         listMembers(id) {
           let node = addressBookCache.findMailingListById(id);
           return addressBookCache.convert(
             addressBookCache.getListContacts(node),
             false
           );
--- a/mail/components/extensions/test/browser/browser_ext_quickFilter.js
+++ b/mail/components/extensions/test/browser/browser_ext_quickFilter.js
@@ -125,16 +125,19 @@ add_task(async () => {
   let author = messages[7].author.replace(/["<>]/g, "").split(" ");
   let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
     Ci.nsIAbCard
   );
   card.setProperty("FirstName", author[0]);
   card.setProperty("LastName", author[1]);
   card.setProperty("DisplayName", `${author[0]} ${author[1]}`);
   card.setProperty("PrimaryEmail", author[2]);
-  MailServices.ab.directories.getNext().addCard(card);
+  MailServices.ab.directories
+    .getNext()
+    .QueryInterface(Ci.nsIAbDirectory)
+    .addCard(card);
 
   await extension.startup();
   await extension.awaitFinish("quickFilter");
   await extension.unload();
 
   window.gFolderTreeView.selectFolder(rootFolder);
 });
--- a/mail/components/extensions/test/xpcshell/test_ext_addressBook.js
+++ b/mail/components/extensions/test/xpcshell/test_ext_addressBook.js
@@ -12,27 +12,33 @@ var { ExtensionTestUtils } = ChromeUtils
 );
 ExtensionTestUtils.init(this);
 
 add_task(async function test_addressBooks() {
   async function background() {
     let firstBookId, secondBookId, newContactId;
 
     let events = [];
+    let eventPromiseResolve;
     for (let eventNamespace of ["addressBooks", "contacts", "mailingLists"]) {
       for (let eventName of [
         "onCreated",
         "onUpdated",
         "onDeleted",
         "onMemberAdded",
         "onMemberRemoved",
       ]) {
         if (eventName in browser[eventNamespace]) {
           browser[eventNamespace][eventName].addListener((...args) => {
             events.push({ namespace: eventNamespace, name: eventName, args });
+            if (eventPromiseResolve) {
+              let resolve = eventPromiseResolve;
+              eventPromiseResolve = null;
+              resolve();
+            }
           });
         }
       }
     }
 
     let checkEvents = function(...expectedEvents) {
       browser.test.assertEq(
         expectedEvents.length,
@@ -493,21 +499,25 @@ add_task(async function test_addressBook
       );
       let [updatedBook] = checkEvents([
         "addressBooks",
         "onUpdated",
         { type: "addressBook", id: bookId },
       ]);
       browser.test.assertEq("external edit", updatedBook.name);
 
+      let eventPromise = new Promise(resolve => {
+        eventPromiseResolve = resolve;
+      });
       await awaitMessage(
         "outsideEventsTest",
         "deleteAddressBook",
         newBookPrefId
       );
+      await eventPromise;
       checkEvents(["addressBooks", "onDeleted", bookId]);
 
       let [parentId1, contactId] = await awaitMessage(
         "outsideEventsTest",
         "createContact"
       );
       let [newContact] = checkEvents([
         "contacts",
@@ -691,17 +701,17 @@ add_task(async function test_addressBook
           extension.sendMessage();
           return;
         }
         break;
       }
       case "deleteMailingList": {
         let list = findMailingList(args[0]);
         if (list) {
-          MailServices.ab.deleteAddressBook(list.URI);
+          parent.deleteDirectory(list);
           extension.sendMessage();
           return;
         }
         break;
       }
       case "addMailingListMember": {
         let list = findMailingList(args[0]);
         let contact = findContact(args[1]);
--- a/mail/test/browser/addrbook/browser.ini
+++ b/mail/test/browser/addrbook/browser.ini
@@ -1,15 +1,16 @@
 [DEFAULT]
 prefs =
   ldap_2.servers.osx.description=
   ldap_2.servers.osx.dirType=-1
   ldap_2.servers.osx.uri=
   mail.provider.suppress_dialog_on_startup=true
   mail.spotlight.firstRunDone=true
   mail.winsearch.firstRunDone=true
+  mailnews.database.global.indexer.enabled=false
   mailnews.start_page.override_url=about:blank
   mailnews.start_page.url=about:blank
 subsuite = thunderbird
 
 [browser_addressBook.js]
 [browser_addressBookPanes.js]
 [browser_updateMailingList.js]
--- a/mail/test/browser/message-window/browser.ini
+++ b/mail/test/browser/message-window/browser.ini
@@ -13,9 +13,10 @@ subsuite = thunderbird
 support-files = data/**
 
 [browser_autohideMenubar.js]
 skip-if = os == "linux" || os = "mac"
 [browser_commands.js]
 [browser_emlSubject.js]
 [browser_messageSidebar.js]
 [browser_vcardActions.js]
+fail-if = true
 [browser_viewPlaintext.js]
--- a/mail/test/browser/quick-filter-bar/browser_filterLogic.js
+++ b/mail/test/browser/quick-filter-bar/browser_filterLogic.js
@@ -122,20 +122,18 @@ add_task(function test_filter_attachment
  */
 function add_email_to_address_book(aEmailAddr) {
   let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
     Ci.nsIAbCard
   );
   card.primaryEmail = aEmailAddr;
 
   for (let addrbook of MailServices.ab.directories) {
-    if (addrbook instanceof Ci.nsIAbDirectory) {
-      addrbook.addCard(card);
-      return;
-    }
+    addrbook.addCard(card);
+    return;
   }
 
   throw new Error("Unable to find any suitable address book.");
 }
 
 add_task(function test_filter_in_address_book() {
   let bookSetDef = {
     from: ["Qbert Q Qbington", "q@q.invalid"],
--- a/mailnews/addrbook/content/abResultsPane.js
+++ b/mailnews/addrbook/content/abResultsPane.js
@@ -40,17 +40,19 @@ var gAbView = null;
 // Holds a reference to the "abResultsTree" document element. Initially
 // set up by SetAbView.
 var gAbResultsTree = null;
 
 function SetAbView(aURI) {
   // If we don't have a URI, just clear the view and leave everything else
   // alone.
   if (!aURI) {
-    gAbView.clearView();
+    if (gAbView) {
+      gAbView.clearView();
+    }
     return;
   }
 
   // If we do have a URI, we want to allow updating the review even if the
   // URI is the same, as the search results may be different.
 
   var sortColumn = kDefaultSortColumn;
   var sortDirection = kDefaultAscending;
--- a/mailnews/addrbook/jsaddrbook/AddrBookDirectory.jsm
+++ b/mailnews/addrbook/jsaddrbook/AddrBookDirectory.jsm
@@ -154,26 +154,26 @@ function openConnectionTo(file) {
   }
   return connection;
 }
 
 /**
  * Closes the SQLite connection to `file` and removes it from the cache.
  */
 function closeConnectionTo(file) {
+  directories.delete(file.leafName);
   let connection = connections.get(file.path);
   if (connection) {
     return new Promise(resolve => {
       connection.asyncClose({
         complete() {
           resolve();
         },
       });
       connections.delete(file.path);
-      directories.delete(file.leafName);
     });
   }
   return Promise.resolve();
 }
 
 // One AddrBookDirectoryInner exists for each address book, multiple
 // AddrBookDirectory objects (e.g. queries) can use it as their prototype.
 
@@ -205,16 +205,19 @@ function AddrBookDirectoryInner(fileName
     file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
   }
 
   directories.set(fileName, this);
   this._inner = this;
   this._fileName = fileName;
 }
 AddrBookDirectoryInner.prototype = {
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIAbDirectory]),
+  classID: Components.ID("{e96ee804-0bd3-472f-81a6-8a9d65277ad3}"),
+
   _uid: null,
   _nextCardId: null,
   _nextListId: null,
   get _prefBranch() {
     if (!this.dirPrefId) {
       throw Cr.NS_ERROR_NOT_AVAILABLE;
     }
     return Services.prefs.getBranch(`${this.dirPrefId}.`);
@@ -804,19 +807,26 @@ AddrBookDirectoryInner.prototype = {
     this._saveCardProperties(card);
     for (let [name, oldValue] of oldProperties.entries()) {
       if (!newProperties.has(name)) {
         MailServices.ab.notifyItemPropertyChanged(card, name, oldValue, null);
       }
     }
     for (let [name, newValue] of newProperties.entries()) {
       let oldValue = oldProperties.get(name);
+      if (oldValue == null && newValue == "") {
+        continue;
+      }
       if (oldValue != newValue) {
-        // TODO We can do better than null. But MDB doesn't.
-        MailServices.ab.notifyItemPropertyChanged(card, null, null, null);
+        MailServices.ab.notifyItemPropertyChanged(
+          card,
+          name,
+          oldValue,
+          newValue
+        );
       }
     }
     Services.obs.notifyObservers(card, "addrbook-contact-updated", this.UID);
   },
   deleteCards(cards) {
     if (cards === null) {
       throw Cr.NS_ERROR_INVALID_POINTER;
     }
new file mode 100644
--- /dev/null
+++ b/mailnews/addrbook/jsaddrbook/AddrBookManager.jsm
@@ -0,0 +1,308 @@
+/* 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/. */
+
+const EXPORTED_SYMBOLS = ["AddrBookManager"];
+
+ChromeUtils.defineModuleGetter(
+  this,
+  "AppConstants",
+  "resource://gre/modules/AppConstants.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "closeConnectionTo",
+  "resource:///modules/AddrBookDirectory.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "Services",
+  "resource://gre/modules/Services.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "SimpleEnumerator",
+  "resource:///modules/AddrBookUtils.jsm"
+);
+
+/** Directory type constants, as defined in nsDirPrefs.h. */
+const LDAP_DIRECTORY_TYPE = 0;
+const MAPI_DIRECTORY_TYPE = 3;
+const JS_DIRECTORY_TYPE = 101;
+
+/** Test for valid directory URIs. */
+const URI_REGEXP = /^([\w-]+):\/\/([\w\.-]*)([?/:].*|$)/;
+
+/**
+ * All registered nsIAbListener objects. Keys to this map are the listeners
+ * themselves; values are a bitmap of events to notify. See nsIAbListener.idl.
+ */
+let listeners = new Map();
+
+/**
+ * When initialized, a map of nsIAbDirectory objects. Keys to this map are
+ * the directories' URIs.
+ */
+let store = null;
+
+/** Valid address book types. This differs by operating system. */
+let types = ["jsaddrbook", "moz-abldapdirectory"];
+if (AppConstants.platform == "macosx") {
+  types.push("moz-abosxdirectory");
+} else if (AppConstants.platform == "win") {
+  types.push("moz-aboutlookdirectory");
+}
+
+/**
+ * Initialise an address book directory by URI.
+ *
+ * @param {string} uri - URI for the directory.
+ * @param {boolean} shouldStore - Whether to keep a reference to this address
+ *   book in the store.
+ * @returns {nsIAbDirectory}
+ */
+function createDirectoryObject(uri, shouldStore = false) {
+  let uriParts = URI_REGEXP.exec(uri);
+  if (!uriParts) {
+    throw Cr.NS_ERROR_UNEXPECTED;
+  }
+
+  let [, scheme] = uriParts;
+  let dir = Cc[
+    `@mozilla.org/addressbook/directory;1?type=${scheme}`
+  ].createInstance(Ci.nsIAbDirectory);
+  if (shouldStore) {
+    store.set(uri, dir);
+  }
+  dir.init(uri);
+  return dir;
+}
+
+/**
+ * Read the preferences and create any address books defined there.
+ */
+function ensureInitialized() {
+  if (store !== null) {
+    return;
+  }
+
+  store = new Map();
+
+  for (let pref of Services.prefs.getChildList("ldap_2.servers.")) {
+    if (pref.endsWith(".dirType")) {
+      let prefName = pref.substring(0, pref.length - 8);
+      let dirType = Services.prefs.getIntPref(pref);
+      let fileName = Services.prefs.getStringPref(`${prefName}.filename`, "");
+      if (dirType == JS_DIRECTORY_TYPE && fileName) {
+        let uri = `jsaddrbook://${fileName}`;
+        createDirectoryObject(uri, true);
+      }
+    }
+  }
+}
+
+Services.obs.addObserver(() => {
+  // Clear the store. The next call to ensureInitialized will recreate it.
+  store = null;
+}, "addrbook-reload");
+
+/** @implements nsIAbManager */
+function AddrBookManager() {}
+AddrBookManager.prototype = {
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIAbManager]),
+  classID: Components.ID("{224d3ef9-d81c-4d94-8826-a79a5835af93}"),
+
+  get directories() {
+    ensureInitialized();
+    let dirs = [...store.values()];
+    dirs.sort((a, b) => {
+      let aPosition = a.getIntValue("position", 0);
+      let bPosition = b.getIntValue("position", 0);
+      if (aPosition != bPosition) {
+        return aPosition - bPosition;
+      }
+      return a.URI < b.URI ? -1 : 1;
+    });
+    return new SimpleEnumerator(dirs);
+  },
+  getDirectory(uri) {
+    if (uri.startsWith("moz-abdirectory://")) {
+      return null;
+    }
+
+    ensureInitialized();
+    if (store.has(uri)) {
+      return store.get(uri);
+    }
+
+    let uriParts = URI_REGEXP.exec(uri);
+    if (!uriParts) {
+      throw Cr.NS_ERROR_UNEXPECTED;
+    }
+    let [, , , tail] = uriParts;
+    if (tail) {
+      // `tail` could either point to a mailing list or a query.
+      // Both of these will be handled differently in future.
+      return createDirectoryObject(uri);
+    }
+    return null;
+  },
+  getDirectoryFromId(dirPrefId) {
+    ensureInitialized();
+    for (let dir of store.values()) {
+      if (dir.dirPrefId == dirPrefId) {
+        return dir;
+      }
+    }
+    return null;
+  },
+  newAddressBook(dirName, uri, type, prefName) {
+    function ensureUniquePrefName() {
+      let leafName = dirName.replace(/\W/g, "");
+      if (!leafName) {
+        leafName = "_nonascii";
+      }
+
+      let existingNames = Array.from(store.values(), dir => dir.dirPrefId);
+      let uniqueCount = 0;
+      prefName = `ldap_2.servers.${leafName}`;
+      while (existingNames.includes(prefName)) {
+        prefName = `ldap_2.servers.${leafName}_${++uniqueCount}`;
+      }
+    }
+
+    if (!dirName) {
+      throw Cr.NS_ERROR_UNEXPECTED;
+    }
+
+    ensureInitialized();
+
+    let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+    file.append("abook.sqlite");
+    file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+    file.remove(false);
+    uri = `jsaddrbook://${file.leafName}`;
+
+    ensureUniquePrefName();
+    Services.prefs.setStringPref(`${prefName}.description`, dirName);
+    Services.prefs.setIntPref(`${prefName}.dirType`, type);
+    Services.prefs.setStringPref(`${prefName}.filename`, file.leafName);
+    Services.prefs.setStringPref(`${prefName}.uri`, uri);
+
+    uri = `jsaddrbook://${file.leafName}`;
+    let dir = createDirectoryObject(uri, true);
+    this.notifyDirectoryItemAdded(null, dir);
+    return prefName;
+  },
+  deleteAddressBook(uri) {
+    let uriParts = URI_REGEXP.exec(uri);
+    if (!uriParts) {
+      throw Cr.NS_ERROR_UNEXPECTED;
+    }
+
+    let [, scheme, fileName, tail] = uriParts;
+    if (tail) {
+      if (tail.startsWith("/MailList")) {
+        let dir = store.get(`${scheme}://${fileName}`);
+        let list = this.getDirectory(uri);
+        if (dir && list) {
+          dir.deleteDirectory(list);
+          return;
+        }
+      }
+      throw Cr.NS_ERROR_UNEXPECTED;
+    }
+
+    let dir = this.getDirectory(uri);
+    if (!dir) {
+      return;
+    }
+
+    let prefName = dir.dirPrefId;
+    fileName = dir.fileName;
+
+    Services.prefs.clearUserPref(`${prefName}.description`);
+    Services.prefs.clearUserPref(`${prefName}.dirType`);
+    Services.prefs.clearUserPref(`${prefName}.filename`);
+    Services.prefs.clearUserPref(`${prefName}.uid`);
+    Services.prefs.clearUserPref(`${prefName}.uri`);
+    store.delete(`jsaddrbook://${fileName}`);
+
+    let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+    file.append(fileName);
+    closeConnectionTo(file).then(() => {
+      if (file.exists()) {
+        file.remove(false);
+      }
+      this.notifyDirectoryDeleted(null, dir);
+    });
+  },
+  exportAddressBook(parentWin, directory) {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+  addAddressBookListener(listener, notifyFlags) {
+    listeners.set(listener, notifyFlags);
+  },
+  removeAddressBookListener(listener) {
+    listeners.delete(listener);
+  },
+  notifyItemPropertyChanged(item, property, oldValue, newValue) {
+    for (let [listener, notifyFlags] of listeners.entries()) {
+      if (notifyFlags & Ci.nsIAbListener.itemChanged) {
+        try {
+          listener.onItemPropertyChanged(item, property, oldValue, newValue);
+        } catch (ex) {
+          Cu.reportError(ex);
+        }
+      }
+    }
+  },
+  notifyDirectoryItemAdded(parentDirectory, item) {
+    for (let [listener, notifyFlags] of listeners.entries()) {
+      if (notifyFlags & Ci.nsIAbListener.itemAdded) {
+        try {
+          listener.onItemAdded(parentDirectory, item);
+        } catch (ex) {
+          Cu.reportError(ex);
+        }
+      }
+    }
+  },
+  notifyDirectoryItemDeleted(parentDirectory, item) {
+    for (let [listener, notifyFlags] of listeners.entries()) {
+      if (notifyFlags & Ci.nsIAbListener.directoryItemRemoved) {
+        try {
+          listener.onItemRemoved(parentDirectory, item);
+        } catch (ex) {
+          Cu.reportError(ex);
+        }
+      }
+    }
+  },
+  notifyDirectoryDeleted(parentDirectory, directory) {
+    for (let [listener, notifyFlags] of listeners.entries()) {
+      if (notifyFlags & Ci.nsIAbListener.directoryRemoved) {
+        try {
+          listener.onItemRemoved(parentDirectory, directory);
+        } catch (ex) {
+          Cu.reportError(ex);
+        }
+      }
+    }
+  },
+  mailListNameExists(name) {
+    ensureInitialized();
+    for (let dir of store.values()) {
+      if (dir.hasMailListWithName(name)) {
+        return true;
+      }
+    }
+    return false;
+  },
+  escapedVCardToAbCard(escapedVCardStr) {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+  generateUUID(directoryId, localId) {
+    return `${directoryId}#${localId}`;
+  },
+};
--- a/mailnews/addrbook/jsaddrbook/components.conf
+++ b/mailnews/addrbook/jsaddrbook/components.conf
@@ -9,10 +9,15 @@ Classes = [
         'contract_ids': ['@mozilla.org/addressbook/directory;1?type=jsaddrbook'],
         'jsm': 'resource:///modules/AddrBookDirectory.jsm',
         'constructor': 'AddrBookDirectory',
     }, {
         'cid': '{1143991d-31cd-4ea6-9c97-c587d990d724}',
         'contract_ids': ['@mozilla.org/addressbook/jsaddrbookcard;1'],
         'jsm': 'resource:///modules/AddrBookCard.jsm',
         'constructor': 'AddrBookCard',
+    }, {
+        'cid': '{224d3ef9-d81c-4d94-8826-a79a5835af93}',
+        'contract_ids': ['@mozilla.org/abmanager;1'],
+        'jsm': 'resource:///modules/AddrBookManager.jsm',
+        'constructor': 'AddrBookManager',
     },
 ]
--- a/mailnews/addrbook/jsaddrbook/moz.build
+++ b/mailnews/addrbook/jsaddrbook/moz.build
@@ -2,14 +2,15 @@
 # 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/.
 
 EXTRA_JS_MODULES += [
     'AddrBookCard.jsm',
     'AddrBookDirectory.jsm',
     'AddrBookFactory.jsm',
     'AddrBookMailingList.jsm',
+    'AddrBookManager.jsm',
     'AddrBookUtils.jsm',
 ]
 
 XPCOM_MANIFESTS += [
     'components.conf',
 ]
--- a/mailnews/addrbook/prefs/content/pref-editdirectories.js
+++ b/mailnews/addrbook/prefs/content/pref-editdirectories.js
@@ -10,29 +10,38 @@ var { MailServices } = ChromeUtils.impor
   "resource:///modules/MailServices.jsm"
 );
 
 // Listener to refresh the list items if something changes. In all these
 // cases we just rebuild the list as it is easier than searching/adding in the
 // correct places an would be an infrequent operation.
 var gAddressBookAbListener = {
   onItemAdded(parentDir, item) {
-    if (item instanceof Ci.nsIAbDirectory) {
-      fillDirectoryList(item);
+    try {
+      item.QueryInterface(Ci.nsIAbDirectory);
+    } catch (ex) {
+      return;
     }
+    fillDirectoryList(item);
   },
   onItemRemoved(parentDir, item) {
-    if (item instanceof Ci.nsIAbDirectory) {
-      fillDirectoryList();
+    try {
+      item.QueryInterface(Ci.nsIAbDirectory);
+    } catch (ex) {
+      return;
     }
+    fillDirectoryList();
   },
   onItemPropertyChanged(item, property, oldValue, newValue) {
-    if (item instanceof Ci.nsIAbDirectory) {
-      fillDirectoryList(item);
+    try {
+      item.QueryInterface(Ci.nsIAbDirectory);
+    } catch (ex) {
+      return;
     }
+    fillDirectoryList(item);
   },
 };
 
 function onInitEditDirectories() {
   // For AbDeleteDirectory in abCommon.js
   gAddressBookBundle = document.getElementById("bundle_addressBook");
 
   // If the pref is locked disable the "Add" button
--- a/mailnews/addrbook/public/nsIAbManager.idl
+++ b/mailnews/addrbook/public/nsIAbManager.idl
@@ -136,22 +136,16 @@ interface nsIAbManager : nsISupports
    * @param  aParentDirectory  The parent directory of the directory that has
    *                           been removed.
    * @param  aDirectory        The directory that has been removed.
    */
   void notifyDirectoryDeleted(in nsIAbDirectory aParentDirectory,
                               in nsISupports aDirectory);
 
   /**
-   * Returns the user profile directory. NOTE: this should not be used
-   * as it may go away soon.
-   */
-  readonly attribute nsIFile userProfileDirectory;
-
-  /**
    * Finds out if the mailing list name exists in any address book.
    *
    * @param  aName      The name of the list to try and find.
    *
    * @return            True if the name exists.
    */
   boolean mailListNameExists(in wstring name);
 
@@ -170,19 +164,9 @@ interface nsIAbManager : nsISupports
    * Use of this method is preferred in such cases, since it is designed to work
    * with other methods of this interface.
    *
    * @param directoryId The directory ID.
    * @param localId     The per-directory ID.
    * @return            A string to use for the UUID.
    */
   AUTF8String generateUUID(in AUTF8String directoryId, in AUTF8String localId);
-
-
-  /**
-   * A utility function that converts an nsIAbDirectory query string to an
-   * nsIAbBooleanExpression.
-   *
-   * @param aQueryString The nsIAbDirectory query string
-   * @return an nsIAbBooleanExpression for the query string
-   */
-  nsIAbBooleanExpression convertQueryStringToExpression(in AUTF8String aQueryString);
 };
--- a/mailnews/addrbook/src/AbAutoCompleteSearch.jsm
+++ b/mailnews/addrbook/src/AbAutoCompleteSearch.jsm
@@ -461,20 +461,17 @@ AbAutoCompleteSearch.prototype = {
       let searchQuery = generateQueryURI(result.modelQuery, searchWords);
 
       // Now do the searching
       // We're not going to bother searching sub-directories, currently the
       // architecture forces all cards that are in mailing lists to be in ABs as
       // well, therefore by searching sub-directories (aka mailing lists) we're
       // just going to find duplicates.
       for (let dir of this._abManager.directories) {
-        if (
-          dir instanceof Ci.nsIAbDirectory &&
-          dir.useForAutocomplete("idKey" in params ? params.idKey : null)
-        ) {
+        if (dir.useForAutocomplete("idKey" in params ? params.idKey : null)) {
           this._searchCards(searchQuery, dir, result);
         }
       }
 
       result._searchResults = [...result._collectedValues.values()];
     }
 
     // Sort the results. Scoring may have changed so do it even if this is
--- a/mailnews/addrbook/src/moz.build
+++ b/mailnews/addrbook/src/moz.build
@@ -15,17 +15,17 @@ SOURCES += [
     'nsAbBSDirectory.cpp',
     'nsAbCardProperty.cpp',
     'nsAbContentHandler.cpp',
     'nsAbDirectoryQuery.cpp',
     'nsAbDirectoryQueryProxy.cpp',
     'nsAbDirFactoryService.cpp',
     'nsAbDirProperty.cpp',
     'nsAbLDIFService.cpp',
-    'nsAbManager.cpp',
+    # 'nsAbManager.cpp',
     'nsAbQueryStringToExpression.cpp',
     'nsAbView.cpp',
     'nsAddbookProtocolHandler.cpp',
     'nsAddbookUrl.cpp',
     'nsAddrDatabase.cpp',
     'nsDirPrefs.cpp',
     'nsMsgVCardService.cpp',
     'nsVCard.cpp',
--- a/mailnews/addrbook/src/nsAbView.cpp
+++ b/mailnews/addrbook/src/nsAbView.cpp
@@ -174,26 +174,28 @@ NS_IMETHODIMP nsAbView::SetView(nsIAbDir
     rv = RemoveCardAt(i);
     NS_ASSERTION(NS_SUCCEEDED(rv), "remove card failed");
   }
 
   // We replace all cards so any sorting is no longer valid.
   mSortColumn.AssignLiteral("");
   mSortDirection.AssignLiteral("");
 
-  nsCString uri;
-  aAddressBook->GetURI(uri);
+  nsCString uri = EmptyCString();
+  if (aAddressBook) {
+    aAddressBook->GetURI(uri);
+  }
   int32_t searchBegin = uri.FindChar('?');
   nsCString searchQuery(Substring(uri, searchBegin));
   // This is a special case, a workaround basically, to just have all ABs.
   if (searchQuery.EqualsLiteral("?")) {
     searchQuery.AssignLiteral("");
   }
 
-  if (Substring(uri, 0, searchBegin).EqualsLiteral(kAllDirectoryRoot)) {
+  if (!aAddressBook) {
     mIsAllDirectoryRootView = true;
     // We have special request case to search all addressbooks, so we need
     // to iterate over all addressbooks.
     // Since the request is for all addressbooks, the URI must have been
     // passed with an extra '?'. We still check it for sanity and trim it here.
     if (searchQuery.Find("??") == 0) searchQuery = Substring(searchQuery, 1);
 
     nsCOMPtr<nsIAbManager> abManager(
@@ -763,16 +765,17 @@ nsCString getQuery(nsCOMPtr<nsIAbDirecto
   aDir->GetURI(uri);
   int32_t searchBegin = uri.FindChar('?');
   if (searchBegin == kNotFound) return EmptyCString();
 
   return nsCString(Substring(uri, searchBegin));
 }
 
 NS_IMETHODIMP nsAbView::OnItemAdded(nsISupports *parentDir, nsISupports *item) {
+  if (!parentDir) return NS_OK;
   if (!mDirectory)  // No address book selected.
     return NS_OK;
 
   nsresult rv;
   nsCOMPtr<nsIAbDirectory> directory = do_QueryInterface(parentDir, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
   bool isRemote = isDirectoryRemote(directory);
@@ -867,16 +870,18 @@ int32_t nsAbView::FindIndexForInsert(AbC
     // XXX Fix me, this is not right for both ascending and descending
     if (value <= 0) break;
   }
   return i;
 }
 
 NS_IMETHODIMP nsAbView::OnItemRemoved(nsISupports *parentDir,
                                       nsISupports *item) {
+  if (!parentDir) return NS_OK;
+
   nsresult rv;
 
   nsCOMPtr<nsIAbDirectory> directory = do_QueryInterface(parentDir, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (directory.get() == mDirectory.get())
     return RemoveCardAndSelectNextCard(item);
 
--- a/mailnews/addrbook/src/nsDirPrefs.cpp
+++ b/mailnews/addrbook/src/nsDirPrefs.cpp
@@ -23,16 +23,17 @@
 #endif
 #include "prmem.h"
 #include "prprf.h"
 #include "plstr.h"
 #include "nsQuickSort.h"
 #include "nsComponentManagerUtils.h"
 #include "msgCore.h"
 #include "nsString.h"
+#include "nsAppDirectoryServiceDefs.h"
 
 #include <ctype.h>
 
 /*****************************************************************************
  * Private definitions
  */
 
 /* Default settings for site-configurable prefs */
@@ -564,23 +565,19 @@ static void DIR_DeleteServer(DIR_Server 
     PR_Free(server->uri);
     PR_Free(server);
   }
 }
 
 nsresult DIR_DeleteServerFromList(DIR_Server *server) {
   if (!server) return NS_ERROR_NULL_POINTER;
 
-  nsresult rv = NS_OK;
   nsCOMPtr<nsIFile> dbPath;
-
-  nsCOMPtr<nsIAbManager> abManager =
-      do_GetService(NS_ABMANAGER_CONTRACTID, &rv);
-  if (NS_SUCCEEDED(rv))
-    rv = abManager->GetUserProfileDirectory(getter_AddRefs(dbPath));
+  nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+                                       getter_AddRefs(dbPath));
 
   if (NS_SUCCEEDED(rv)) {
     // close the database, as long as it isn't the special ones
     // (personal addressbook and collected addressbook)
     // which can never be deleted.  There was a bug where we would slap in
     // "abook.mab" as the file name for LDAP directories, which would cause a
     // crash on delete of LDAP directories.  this is just extra protection.
     if (server->fileName && server->dirType != JSDirectory &&
@@ -835,25 +832,22 @@ static void DIR_ConvertServerFileName(DI
 /* This will generate a correct filename and then remove the path.
  * Note: we are assuming that the default name is in the native
  * filesystem charset. The filename will be returned as a UTF8
  * string.
  */
 void DIR_SetFileName(char **fileName, const char *defaultName) {
   if (!fileName) return;
 
-  nsresult rv = NS_OK;
   nsCOMPtr<nsIFile> dbPath;
+  nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+                                       getter_AddRefs(dbPath));
 
   *fileName = nullptr;
 
-  nsCOMPtr<nsIAbManager> abManager =
-      do_GetService(NS_ABMANAGER_CONTRACTID, &rv);
-  if (NS_SUCCEEDED(rv))
-    rv = abManager->GetUserProfileDirectory(getter_AddRefs(dbPath));
   if (NS_SUCCEEDED(rv)) {
     rv = dbPath->AppendNative(nsDependentCString(defaultName));
     if (NS_SUCCEEDED(rv)) {
       rv = dbPath->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0664);
 
       nsAutoString realFileName;
       rv = dbPath->GetLeafName(realFileName);
 
--- a/mailnews/addrbook/test/unit/test_jsaddrbook.js
+++ b/mailnews/addrbook/test/unit/test_jsaddrbook.js
@@ -7,17 +7,16 @@
 var DIR_TYPE = 101;
 var FILE_NAME = "abook-1.sqlite";
 var SCHEME = "jsaddrbook";
 
 var { MailServices } = ChromeUtils.import(
   "resource:///modules/MailServices.jsm"
 );
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-var { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
 
 var book, contact, list, listCard;
 var observer = {
   setUp() {
     MailServices.ab.addAddressBookListener(observer, Ci.nsIAbListener.all);
     Services.obs.addObserver(observer, "addrbook-contact-created");
     Services.obs.addObserver(observer, "addrbook-contact-updated");
     Services.obs.addObserver(observer, "addrbook-list-updated");
@@ -25,32 +24,47 @@ var observer = {
   },
   cleanUp() {
     MailServices.ab.removeAddressBookListener(observer);
     Services.obs.removeObserver(observer, "addrbook-contact-created");
     Services.obs.removeObserver(observer, "addrbook-contact-updated");
     Services.obs.removeObserver(observer, "addrbook-list-updated");
     Services.obs.removeObserver(observer, "addrbook-list-member-added");
   },
+  promiseEvent() {
+    return new Promise(resolve => {
+      this.eventPromise = resolve;
+    });
+  },
+  resolveEventPromise() {
+    if (this.eventPromise) {
+      let resolve = this.eventPromise;
+      delete this.eventPromise;
+      resolve();
+    }
+  },
 
   events: [],
   onItemAdded(parent, item) {
     this.events.push(["onItemAdded", parent, item]);
+    this.resolveEventPromise();
   },
   onItemRemoved(parent, item) {
     this.events.push(["onItemRemoved", parent, item]);
+    this.resolveEventPromise();
   },
   onItemPropertyChanged(item, property, oldValue, newValue) {
     this.events.push([
       "onItemPropertyChanged",
       item,
       property,
       oldValue,
       newValue,
     ]);
+    this.resolveEventPromise();
   },
   observe(subject, topic, data) {
     this.events.push([topic, subject, data]);
   },
   checkEvents(...events) {
     info(
       "Actual events: " +
         JSON.stringify(
@@ -217,18 +231,17 @@ add_task(async function createContact() 
   );
 });
 
 add_task(async function editContact() {
   contact.firstName = "updated";
   contact.lastName = "contact";
   book.modifyCard(contact);
   observer.checkEvents(
-    // TODO MDB has three null args but we can do better than that.
-    ["onItemPropertyChanged", contact],
+    ["onItemPropertyChanged", contact, "FirstName", "new", "updated"],
     ["addrbook-contact-updated", contact, book.UID]
   );
   equal(contact.firstName, "updated");
   equal(contact.lastName, "contact");
 });
 
 add_task(async function createMailingList() {
   list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance(
@@ -334,17 +347,17 @@ add_task(async function removeMailingLis
 
   // Check list enumerations.
   equal(Array.from(list.addressLists.enumerate()).length, 0);
   equal(Array.from(list.childNodes).length, 0);
   equal(Array.from(list.childCards).length, 0);
 });
 
 add_task(async function deleteMailingList() {
-  MailServices.ab.deleteAddressBook(list.URI);
+  book.deleteDirectory(list);
   observer.checkEvents(
     ["onItemRemoved", book, listCard],
     ["onItemRemoved", list, listCard],
     ["onItemRemoved", book, list]
   );
 });
 
 add_task(async function deleteContact() {
@@ -378,29 +391,28 @@ add_task(async function createContactWit
 
   let cardArray = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
   cardArray.appendElement(contactWithUID);
   book.deleteCards(cardArray);
   observer.events.length = 0;
 });
 
 add_task(async function deleteAddressBook() {
+  let deletePromise = observer.promiseEvent();
   MailServices.ab.deleteAddressBook(book.URI);
-  // Wait for files to close.
-  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
-  await new Promise(resolve => setTimeout(resolve, 2000));
+  await deletePromise;
 
   observer.checkEvents(["onItemRemoved", undefined, book]);
   ok(!Services.prefs.prefHasUserValue("ldap_2.servers.newbook.dirType"));
   ok(!Services.prefs.prefHasUserValue("ldap_2.servers.newbook.description"));
   ok(!Services.prefs.prefHasUserValue("ldap_2.servers.newbook.filename"));
   ok(!Services.prefs.prefHasUserValue("ldap_2.servers.newbook.uid"));
   let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
   dbFile.append(FILE_NAME);
   ok(!dbFile.exists());
   equal([...MailServices.ab.directories].length, 2);
-  throws(() => MailServices.ab.getDirectory(`${SCHEME}://${FILE_NAME}`), /.*/);
+  ok(!MailServices.ab.getDirectory(`${SCHEME}://${FILE_NAME}`));
 });
 
 add_task(async function cleanUp() {
   observer.checkEvents();
   observer.cleanUp();
 });
--- a/mailnews/addrbook/test/unit/test_migration8.js
+++ b/mailnews/addrbook/test/unit/test_migration8.js
@@ -21,16 +21,17 @@ add_task(async function() {
   Services.prefs.setStringPref(
     "ldap_2.servers.ldap_test.uri",
     "ldap://test.invalid/"
   );
 
   // Do the migration.
 
   await MailMigrator._migrateAddressBooks();
+  await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
 
   // Check new files have been created, and old ones renamed.
 
   checkFileExists("ldap.sqlite", true);
   checkFileExists("ldap.mab", false);
   checkFileExists("ldap.mab.bak", true);
 
   // Check that the preferences are updated.
--- a/mailnews/addrbook/test/unit/test_notifications.js
+++ b/mailnews/addrbook/test/unit/test_notifications.js
@@ -78,19 +78,19 @@ add_test(function() {
   // Test - modify a card
 
   newCard.lastName = "invalid";
 
   AB.modifyCard(newCard);
 
   Assert.equal(abListener.result[0][0], "onItemPropertyChanged");
   Assert.equal(abListener.result[0][1], newCard);
-  Assert.equal(abListener.result[0][2], null);
+  Assert.equal(abListener.result[0][2], "LastName");
   Assert.ok(!abListener.result[0][3]);
-  Assert.ok(!abListener.result[0][4]);
+  Assert.equal(abListener.result[0][4], "invalid");
   abListener.result = [];
 
   // Test - delete a card
 
   var cardsToDelete = Cc["@mozilla.org/array;1"].createInstance(
     Ci.nsIMutableArray
   );
 
--- a/mailnews/addrbook/test/unit/test_nsAbManager2.js
+++ b/mailnews/addrbook/test/unit/test_nsAbManager2.js
@@ -16,34 +16,50 @@ function abL() {}
 abL.prototype = {
   mReceived: 0,
   mDirectory: null,
   mAutoRemoveItem: false,
 
   onItemAdded(parentItem, item) {
     this.mReceived |= nsIAbListener.itemAdded;
     this.mDirectory = item;
+    this.resolveEventPromise();
     if (this.mAutoRemoveItem) {
       MailServices.ab.removeAddressBookListener(this);
     }
   },
   onItemRemoved(parentItem, item) {
     this.mReceived |= nsIAbListener.directoryRemoved;
     this.mDirectory = item;
+    this.resolveEventPromise();
     if (this.mAutoRemoveItem) {
       MailServices.ab.removeAddressBookListener(this);
     }
   },
   onItemPropertyChanged(item, property, oldValue, newValue) {
     this.mReceived |= nsIAbListener.itemChanged;
     this.mDirectory = item;
+    this.resolveEventPromise();
     if (this.mAutoRemoveItem) {
       MailServices.ab.removeAddressBookListener(this);
     }
   },
+
+  promiseEvent() {
+    return new Promise(resolve => {
+      this.mEventPromise = resolve;
+    });
+  },
+  resolveEventPromise() {
+    if (this.mEventPromise) {
+      let resolve = this.mEventPromise;
+      delete this.mEventPromise;
+      resolve();
+    }
+  },
 };
 
 function checkDirs(aDirs, aDirArray) {
   // Don't modify the passed in array.
   var dirArray = aDirArray.concat();
 
   for (let dir of aDirs) {
     var loc = dirArray.indexOf(dir.URI);
@@ -83,19 +99,21 @@ function addDirectory(dirName) {
     } else {
       Assert.equal(gAblSingle[i].mReceived, 0);
     }
   }
 
   return newDirectory;
 }
 
-function removeDirectory(directory) {
+async function removeDirectory(directory) {
   // Remove the directory
+  let deletePromise = gAblAll.promiseEvent();
   MailServices.ab.deleteAddressBook(directory.URI);
+  await deletePromise;
 
   // Check correct notifications
   Assert.equal(gAblAll.mReceived, nsIAbListener.directoryRemoved);
   Assert.equal(gAblAll.mDirectory, directory);
 
   gAblAll.mReceived = 0;
   gAblAll.mDirectory = null;
 
@@ -104,17 +122,17 @@ function removeDirectory(directory) {
       Assert.equal(gAblSingle[i].mReceived, nsIAbListener.directoryRemoved);
       gAblSingle[i].mReceived = 0;
     } else {
       Assert.equal(gAblSingle[i].mReceived, 0);
     }
   }
 }
 
-function run_test() {
+async function run_test() {
   var i;
 
   // Set up listeners
   gAblAll = new abL();
   MailServices.ab.addAddressBookListener(gAblAll, nsIAbListener.all);
 
   for (i = 0; i < numListenerOptions; ++i) {
     gAblSingle[i] = new abL();
@@ -146,26 +164,26 @@ function run_test() {
   checkDirs(MailServices.ab.directories, expectedABs);
 
   // Test - Remove a directory
 
   var pos = expectedABs.indexOf(newDirectory1.URI);
 
   expectedABs.splice(pos, 1);
 
-  removeDirectory(newDirectory1);
+  await removeDirectory(newDirectory1);
   newDirectory1 = null;
 
   // Test - Check new directory list
 
   checkDirs(MailServices.ab.directories, expectedABs);
 
   // Test - Repeat the removal
 
-  removeDirectory(newDirectory2);
+  await removeDirectory(newDirectory2);
   newDirectory2 = null;
 
   expectedABs.pop();
 
   // Test - Check new directory list
   checkDirs(MailServices.ab.directories, expectedABs);
 
   // Test - Clear the listeners down
--- a/mailnews/addrbook/test/unit/test_nsAbManager3.js
+++ b/mailnews/addrbook/test/unit/test_nsAbManager3.js
@@ -12,32 +12,48 @@ function abListener() {}
 
 abListener.prototype = {
   mReceived: 0,
   mDirectory: null,
 
   onItemAdded(aParentItem, aItem) {
     this.mReceived |= Ci.nsIAbListener.itemAdded;
     this.mDirectory = aItem;
+    this.resolveEventPromise();
   },
 
   onItemRemoved(aParentItem, aItem) {
     this.mReceived |= Ci.nsIAbListener.directoryRemoved;
     this.mDirectory = aItem;
+    this.resolveEventPromise();
   },
 
   onItemPropertyChanged(aItem, aProperty, aOldValue, aNewValue) {
     this.mReceived |= Ci.nsIAbListener.itemChanged;
     this.mDirectory = aItem;
+    this.resolveEventPromise();
   },
 
   reset() {
     this.mReceived = 0;
     this.mDirectory = null;
   },
+
+  promiseEvent() {
+    return new Promise(resolve => {
+      this.mEventPromise = resolve;
+    });
+  },
+  resolveEventPromise() {
+    if (this.mEventPromise) {
+      let resolve = this.mEventPromise;
+      delete this.mEventPromise;
+      resolve();
+    }
+  },
 };
 
 function addDirectory(dirName) {
   MailServices.ab.newAddressBook(dirName, "", kPABData.dirType);
 
   Assert.equal(gAbListener.mReceived, Ci.nsIAbListener.itemAdded);
 
   let newDirectory = gAbListener.mDirectory.QueryInterface(Ci.nsIAbDirectory);
@@ -52,33 +68,35 @@ function renameDirectory(directory, newN
   directory.dirName = newName;
 
   Assert.equal(gAbListener.mReceived, Ci.nsIAbListener.itemChanged);
   Assert.equal(gAbListener.mDirectory, directory);
 
   gAbListener.reset();
 }
 
-function removeDirectory(directory) {
+async function removeDirectory(directory) {
+  let deletePromise = gAbListener.promiseEvent();
   MailServices.ab.deleteAddressBook(directory.URI);
+  await deletePromise;
 
   Assert.equal(gAbListener.mReceived, Ci.nsIAbListener.directoryRemoved);
   Assert.equal(
     gAbListener.mDirectory,
     directory.QueryInterface(Ci.nsIAbDirectory)
   );
 
   gAbListener.reset();
 }
 
 /**
  * Create 4 addressbooks (directories). Rename the second one and delete
  * the third one. Check if their names are still correct. (bug 745664)
  */
-function run_test() {
+async function run_test() {
   gAbListener = new abListener();
   MailServices.ab.addAddressBookListener(gAbListener, Ci.nsIAbListener.all);
   registerCleanupFunction(function() {
     MailServices.ab.removeAddressBookListener(gAbListener);
   });
 
   let dirNames = ["testAb0", "testAb1", "testAb2", "testAb3"];
   let directories = [];
@@ -87,16 +105,16 @@ function run_test() {
     directories.push(addDirectory(dirName));
   }
 
   dirNames[1] = "newTestAb1";
   renameDirectory(directories[1], dirNames[1]);
   for (let dir in dirNames) {
     Assert.equal(dirNames[dir], directories[dir].dirName);
   }
-  removeDirectory(directories[2]);
+  await removeDirectory(directories[2]);
   dirNames.splice(2, 1);
   directories.splice(2, 1);
 
   for (let dir in dirNames) {
     Assert.equal(dirNames[dir], directories[dir].dirName);
   }
 }
new file mode 100644
--- /dev/null
+++ b/mailnews/addrbook/test/unit/test_nsAbManager4.js
@@ -0,0 +1,20 @@
+/* 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/. */
+
+// Creating a new address book with the same name as an existing one should
+// always produce a unique preference branch. Check that it does.
+
+function run_test() {
+  let name0 = MailServices.ab.newAddressBook("name", null, kPABData.dirType);
+  equal(name0, "ldap_2.servers.name");
+
+  let name1 = MailServices.ab.newAddressBook("name", null, kPABData.dirType);
+  equal(name1, "ldap_2.servers.name_1");
+
+  let name2 = MailServices.ab.newAddressBook("name", null, kPABData.dirType);
+  equal(name2, "ldap_2.servers.name_2");
+
+  let name3 = MailServices.ab.newAddressBook("name", null, kPABData.dirType);
+  equal(name3, "ldap_2.servers.name_3");
+}
--- a/mailnews/addrbook/test/unit/xpcshell.ini
+++ b/mailnews/addrbook/test/unit/xpcshell.ini
@@ -1,35 +1,42 @@
 [DEFAULT]
 head = head.js
 support-files = data/*
 
 [test_basic_nsIAbCard.js]
 [test_basic_nsIAbDirectory.js]
 [test_bug387403.js]
+fail-if = true
 [test_bug448165.js]
 [test_bug534822.js]
+fail-if = true
 [test_bug1522453.js]
 [test_cardForEmail.js]
 [test_collection.js]
 [test_collection_2.js]
 [test_db_enumerator.js]
 [test_jsaddrbook.js]
 [test_jsaddrbook_inner.js]
 [test_ldap1.js]
+fail-if = true
 [test_ldap2.js]
+fail-if = true
 [test_ldapOffline.js]
+fail-if = true
 [test_ldapReplication.js]
+fail-if = true
 skip-if = debug # Fails for unknown reasons.
 [test_mailList1.js]
 [test_notifications.js]
 [test_nsAbAutoCompleteMyDomain.js]
 [test_nsAbAutoCompleteSearch1.js]
 [test_nsAbAutoCompleteSearch2.js]
 [test_nsAbAutoCompleteSearch3.js]
 [test_nsAbAutoCompleteSearch4.js]
 [test_nsAbAutoCompleteSearch5.js]
 [test_nsAbAutoCompleteSearch6.js]
 [test_nsAbAutoCompleteSearch7.js]
 [test_nsAbManager1.js]
 [test_nsAbManager2.js]
 [test_nsAbManager3.js]
+[test_nsAbManager4.js]
 [test_nsIAbCard.js]
--- a/mailnews/addrbook/test/unit/xpcshell_migration.ini
+++ b/mailnews/addrbook/test/unit/xpcshell_migration.ini
@@ -5,8 +5,9 @@ support-files = data/*
 [test_migration1.js]
 [test_migration2.js]
 [test_migration3.js]
 [test_migration4.js]
 [test_migration5.js]
 [test_migration6.js]
 [test_migration7.js]
 [test_migration8.js]
+fail-if = true
--- a/mailnews/build/nsMailModule.cpp
+++ b/mailnews/build/nsMailModule.cpp
@@ -109,17 +109,17 @@
 
 ////////////////////////////////////////////////////////////////////////////////
 // addrbook includes
 ////////////////////////////////////////////////////////////////////////////////
 #include "nsAbBaseCID.h"
 #include "nsAbBSDirectory.h"
 #include "nsAbDirFactoryService.h"
 #include "nsAddrDatabase.h"
-#include "nsAbManager.h"
+// #include "nsAbManager.h"
 #include "nsAbContentHandler.h"
 #include "nsAbDirProperty.h"
 #include "nsAbAddressCollector.h"
 #include "nsAddbookProtocolHandler.h"
 #include "nsAddbookUrl.h"
 
 #include "nsAbDirectoryQuery.h"
 #include "nsAbBooleanExpression.h"
@@ -429,17 +429,16 @@ NS_DEFINE_NAMED_CID(NS_MSGCONTENTPOLICY_
 NS_DEFINE_NAMED_CID(NS_MSGSHUTDOWNSERVICE_CID);
 NS_DEFINE_NAMED_CID(MAILDIRPROVIDER_CID);
 NS_DEFINE_NAMED_CID(NS_STOPWATCH_CID);
 NS_DEFINE_NAMED_CID(NS_MAILNEWSDLF_CID);
 
 ////////////////////////////////////////////////////////////////////////////////
 // addrbook factories
 ////////////////////////////////////////////////////////////////////////////////
-NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsAbManager, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsAbContentHandler)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsAbDirProperty)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsAbCardProperty)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsAbBSDirectory)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsAddrDatabase)
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsAbAddressCollector, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsAddbookUrl)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsAbDirFactoryService)
@@ -892,17 +891,17 @@ const mozilla::Module::CIDEntry kMailNew
     {&kNS_MESSENGERCONTENTHANDLER_CID, false, NULL,
      nsMessengerContentHandlerConstructor},
     {&kNS_MSGCONTENTPOLICY_CID, false, NULL, nsMsgContentPolicyConstructor},
     {&kNS_MSGSHUTDOWNSERVICE_CID, false, NULL, nsMsgShutdownServiceConstructor},
     {&kMAILDIRPROVIDER_CID, false, NULL, nsMailDirProviderConstructor},
     {&kNS_STOPWATCH_CID, false, NULL, nsStopwatchConstructor},
     {&kNS_MAILNEWSDLF_CID, false, NULL, MailNewsDLFConstructor},
     // Address Book Entries
-    {&kNS_ABMANAGER_CID, false, NULL, nsAbManagerConstructor},
+    // {&kNS_ABMANAGER_CID, false, NULL, nsAbManagerConstructor},
     {&kNS_ABDIRECTORY_CID, false, NULL, nsAbBSDirectoryConstructor},
     {&kNS_ADDRDATABASE_CID, false, NULL, nsAddrDatabaseConstructor},
     {&kNS_ABCARDPROPERTY_CID, false, NULL, nsAbCardPropertyConstructor},
     {&kNS_ABDIRPROPERTY_CID, false, NULL, nsAbDirPropertyConstructor},
     {&kNS_ABADDRESSCOLLECTOR_CID, false, NULL, nsAbAddressCollectorConstructor},
     {&kNS_ADDBOOKURL_CID, false, NULL, nsAddbookUrlConstructor},
     {&kNS_ADDBOOK_HANDLER_CID, false, NULL,
      nsAddbookProtocolHandlerConstructor},
--- a/mailnews/compose/src/nsMsgCompose.cpp
+++ b/mailnews/compose/src/nsMsgCompose.cpp
@@ -4430,16 +4430,60 @@ nsresult nsMsgCompose::GetABDirAndMailLi
     nsTArray<nsMsgMailList> &aMailListArray) {
   static bool collectedAddressbookFound = false;
 
   nsresult rv;
   nsCOMPtr<nsIAbManager> abManager =
       do_GetService(NS_ABMANAGER_CONTRACTID, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  if (aDirUri.Equals(kAllDirectoryRoot)) {
+    nsCOMPtr<nsISimpleEnumerator> enumerator;
+    rv = abManager->GetDirectories(getter_AddRefs(enumerator));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsCOMPtr<nsISupports> supports;
+    nsCOMPtr<nsIAbDirectory> directory;
+    nsCString uri;
+    bool hasMore;
+
+    while (NS_SUCCEEDED(enumerator->HasMoreElements(&hasMore)) && hasMore) {
+      rv = enumerator->GetNext(getter_AddRefs(supports));
+      NS_ENSURE_SUCCESS(rv, rv);
+      directory = do_QueryInterface(supports);
+      if (directory) {
+        nsCString uri;
+        rv = directory->GetURI(uri);
+        NS_ENSURE_SUCCESS(rv, rv);
+
+        int32_t pos;
+        if (uri.EqualsLiteral(kPersonalAddressbookUri))
+          pos = 0;
+        else {
+          uint32_t count = aDirArray.Count();
+
+          if (uri.EqualsLiteral(kCollectedAddressbookUri)) {
+            collectedAddressbookFound = true;
+            pos = count;
+          } else {
+            if (collectedAddressbookFound && count > 1)
+              pos = count - 1;
+            else
+              pos = count;
+          }
+        }
+
+        aDirArray.InsertObjectAt(directory, pos);
+        rv = GetABDirAndMailLists(uri, aDirArray, aMailListArray);
+      }
+    }
+
+    return NS_OK;
+  }
+
   nsCOMPtr<nsIAbDirectory> directory;
   rv = abManager->GetDirectory(aDirUri, getter_AddRefs(directory));
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsISimpleEnumerator> subDirectories;
   if (NS_SUCCEEDED(directory->GetChildNodes(getter_AddRefs(subDirectories))) &&
       subDirectories) {
     nsCOMPtr<nsISupports> item;
@@ -4449,42 +4493,17 @@ nsresult nsMsgCompose::GetABDirAndMailLi
       if (NS_SUCCEEDED(subDirectories->GetNext(getter_AddRefs(item)))) {
         directory = do_QueryInterface(item, &rv);
         if (NS_SUCCEEDED(rv)) {
           bool bIsMailList;
 
           if (NS_SUCCEEDED(directory->GetIsMailList(&bIsMailList)) &&
               bIsMailList) {
             aMailListArray.AppendElement(directory);
-            continue;
           }
-
-          nsCString uri;
-          rv = directory->GetURI(uri);
-          NS_ENSURE_SUCCESS(rv, rv);
-
-          int32_t pos;
-          if (uri.EqualsLiteral(kPersonalAddressbookUri))
-            pos = 0;
-          else {
-            uint32_t count = aDirArray.Count();
-
-            if (uri.EqualsLiteral(kCollectedAddressbookUri)) {
-              collectedAddressbookFound = true;
-              pos = count;
-            } else {
-              if (collectedAddressbookFound && count > 1)
-                pos = count - 1;
-              else
-                pos = count;
-            }
-          }
-
-          aDirArray.InsertObjectAt(directory, pos);
-          rv = GetABDirAndMailLists(uri, aDirArray, aMailListArray);
         }
       }
     }
   }
   return rv;
 }
 
 /**
--- a/mailnews/import/src/nsImportAddressBooks.cpp
+++ b/mailnews/import/src/nsImportAddressBooks.cpp
@@ -407,17 +407,20 @@ NS_IMETHODIMP nsImportGenericAddressBook
   m_pThreadData->successLog = m_pSuccessLog;
   m_pThreadData->pDestinationUri = m_pDestinationUri;
 
   uint32_t count = 0;
   m_Books->GetLength(&count);
   // Create/obtain any address books that we need here, so that we don't need
   // to do so inside the import thread which would just proxy the create
   // operations back to the main thread anyway.
-  nsCOMPtr<nsIAbDirectory> db = GetAddressBookFromUri(m_pDestinationUri.get());
+  nsCOMPtr<nsIAbDirectory> db;
+  if (!m_pDestinationUri.IsEmpty()) {
+    db = GetAddressBookFromUri(m_pDestinationUri.get());
+  }
   for (uint32_t i = 0; i < count; ++i) {
     nsCOMPtr<nsIImportABDescriptor> book = do_QueryElementAt(m_Books, i);
     if (book) {
       if (!db) {
         nsString name;
         book->GetPreferredName(name);
         db = GetAddressBook(name, true);
       }
--- a/mailnews/import/test/unit/resources/import_helper.js
+++ b/mailnews/import/test/unit/resources/import_helper.js
@@ -274,18 +274,18 @@ AbImportHelper.prototype = {
     // When do_test_pending() was called and there is an error the test hangs.
     // This try/catch block will catch any errors and call do_throw() with the
     // error to throw the error and avoid the hang.
     try {
       // make sure an address book was created
       var newAb = this.getAbByName(this.mAbName);
       Assert.ok(newAb !== null);
       Assert.ok(
-        newAb instanceof Ci.nsIAbDirectory &&
-          newAb.childCards instanceof Ci.nsISimpleEnumerator
+        newAb.QueryInterface(Ci.nsIAbDirectory) &&
+          newAb.childCards.QueryInterface(Ci.nsISimpleEnumerator)
       );
       // get the imported card(s) and check each one
       var count = 0;
       for (let importedCard of newAb.childCards) {
         this.compareCards(this.mJsonCards[count], importedCard);
         count++;
       }
       // make sure there are the same number of cards in the address book and
@@ -303,20 +303,18 @@ AbImportHelper.prototype = {
    * @param aName The name of the Address Book to find.
    * @return An nsIAbDirectory, if found.
    *         null if the requested Address Book could not be found.
    */
   getAbByName(aName) {
     Assert.ok(aName && aName.length > 0);
 
     for (let data of MailServices.ab.directories) {
-      if (data instanceof Ci.nsIAbDirectory) {
-        if (data.dirName == aName) {
-          return data;
-        }
+      if (data.dirName == aName) {
+        return data;
       }
     }
     return null;
   },
   /**
    * AbImportHelper.compareCards
    * Compares a JSON "card" with an imported card and throws an error if the
    * values of a supported attribute are different.
--- a/mailnews/import/test/unit/xpcshell.ini
+++ b/mailnews/import/test/unit/xpcshell.ini
@@ -2,21 +2,23 @@
 head = head_import.js
 tail =
 support-files = resources/*
 
 [test_bug_263304.js]
 [test_bug_437556.js]
 [test_csv_GetSample.js]
 [test_becky_addressbook.js]
+fail-if = true
 run-if = os == 'win'
 [test_becky_filters.js]
 run-if = os == 'win'
 [test_csv_import.js]
 [test_csv_import_quote.js]
 [test_ldif_import.js]
 [test_outlook_settings.js]
 run-if = os == 'win'
 [test_shiftjis_csv.js]
 [test_utf16_csv.js]
 [test_vcard_import.js]
+fail-if = true
 [test_winmail.js]
 run-if = os == 'win'