Bug 1633620 - Stop using URLs to search address books. r=mkmelin
authorGeoff Lankow <geoff@darktrojan.net>
Mon, 06 Apr 2020 21:52:53 +1200
changeset 38958 9eda2be91b4ed91bf64cb4571e1d70c360146225
parent 38957 69e91e0bc92ce7e94f97a3069e2949bbca3c840f
child 38959 a34c59e873c92c1d65c9dd3715d260698083d887
push id401
push userclokep@gmail.com
push dateMon, 01 Jun 2020 20:41:59 +0000
reviewersmkmelin
bugs1633620
Bug 1633620 - Stop using URLs to search address books. r=mkmelin
mail/components/addrbook/content/abCommon.js
mail/components/addrbook/content/abContactsPanel.js
mail/components/addrbook/content/abContactsPanel.xhtml
mail/components/addrbook/content/addressbook.js
mail/components/addrbook/content/addressbook.xhtml
mail/components/addrbook/test/browser/browser.ini
mail/components/addrbook/test/browser/browser_contact_tree.js
mail/components/addrbook/test/browser/browser_mailing_lists.js
mail/components/addrbook/test/browser/browser_search.js
mail/test/browser/addrbook/browser_addressBook.js
mailnews/addrbook/content/abDragDrop.js
mailnews/addrbook/content/abResultsPane.js
mailnews/addrbook/content/abView.js
mailnews/addrbook/jsaddrbook/AddrBookDirectory.jsm
mailnews/addrbook/public/nsIAbDirectory.idl
mailnews/addrbook/src/AbAutoCompleteSearch.jsm
mailnews/addrbook/src/nsAbDirProperty.cpp
mailnews/addrbook/src/nsAbLDAPDirectory.cpp
mailnews/addrbook/src/nsAbLDAPDirectory.h
mailnews/addrbook/src/nsAbOSXDirectory.h
mailnews/addrbook/src/nsAbOSXDirectory.mm
mailnews/addrbook/test/unit/head.js
mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js
mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch2.js
mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch3.js
mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js
mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch5.js
mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js
mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch7.js
mailnews/addrbook/test/unit/test_search.js
mailnews/addrbook/test/unit/xpcshell.ini
mailnews/jar.mn
--- a/mail/components/addrbook/content/abCommon.js
+++ b/mail/components/addrbook/content/abCommon.js
@@ -520,17 +520,17 @@ function AbDelete() {
       let cardArray = Cc["@mozilla.org/array;1"].createInstance(
         Ci.nsIMutableArray
       );
       cardArray.appendElement(cards[i]);
       if (directory) {
         directory.deleteCards(cardArray);
       }
     }
-    SetAbView(kAllDirectoryRoot + "?");
+    SetAbView();
   } else {
     // Delete cards from address books or mailing lists.
     gAbView.deleteSelectedCards();
   }
 }
 
 function AbNewCard() {
   goNewCardDialog(getSelectedDirectoryURI());
--- a/mail/components/addrbook/content/abContactsPanel.js
+++ b/mail/components/addrbook/content/abContactsPanel.js
@@ -219,26 +219,27 @@ function onEnterInSearchBar() {
     // Get model query from pref. We don't want the query starting with "?"
     // as we have to prefix "?and" to this format.
     /* eslint-disable no-global-assign */
     gQueryURIFormat = getModelQuery("mail.addr_book.quicksearchquery.format");
     /* eslint-enable no-global-assign */
   }
 
   let searchURI = getSelectedDirectoryURI();
+  let searchQuery;
   let searchInput = document.getElementById("peopleSearchInput");
 
   // Use helper method to split up search query to multi-word search
   // query against multiple fields.
   if (searchInput) {
     let searchWords = getSearchTokens(searchInput.value);
-    searchURI += generateQueryURI(gQueryURIFormat, searchWords);
+    searchQuery = generateQueryURI(gQueryURIFormat, searchWords);
   }
 
-  SetAbView(searchURI);
+  SetAbView(searchURI, searchQuery);
 }
 
 /**
  * Open a menupopup as a context menu
  *
  * @param aContextMenuID The ID of a menupopup to be shown as context menu
  * @param aEvent         The event which triggered this.
  * @param positionArray  An optional array containing the parameters for openPopup() method;
--- a/mail/components/addrbook/content/abContactsPanel.xhtml
+++ b/mail/components/addrbook/content/abContactsPanel.xhtml
@@ -25,16 +25,18 @@
   <script src="chrome://global/content/globalOverlay.js"/>
   <script src="chrome://global/content/editMenuOverlay.js"/>
   <script src="chrome://communicator/content/utilityOverlay.js"/>
   <script src="chrome://messenger/content/addressbook/addressbook.js"/>
   <script src="chrome://messenger/content/addressbook/abDragDrop.js"/>
   <script src="chrome://messenger/content/addressbook/abCommon.js"/>
   <script src="chrome://messenger/content/addressbook/abResultsPane.js"/>
   <script src="chrome://messenger/content/addressbook/abContactsPanel.js"/>
+  <script src="chrome://messenger/content/jsTreeView.js"/>
+  <script src="chrome://messenger/content/addressbook/abView.js"/>
 
   <commandset id="CommandUpdate_AddressBook"
               commandupdater="true"
               events="focus,addrbook-select"
               oncommandupdate="CommandUpdate_AddressBook()">
     <command id="cmd_addrTo" oncommand="addSelectedAddresses('addr_to')" disabled="true"/>
     <command id="cmd_addrCc" oncommand="addSelectedAddresses('addr_cc')" disabled="true"/>
     <command id="cmd_addrBcc" oncommand="addSelectedAddresses('addr_bcc')" disabled="true"/>
--- a/mail/components/addrbook/content/addressbook.js
+++ b/mail/components/addrbook/content/addressbook.js
@@ -140,16 +140,19 @@ function OnUnloadAddressBook() {
   CloseAbView();
 }
 
 var gAddressBookAbViewListener = {
   onSelectionChanged() {
     ResultsPaneSelectionChanged();
   },
   onCountChanged(total) {
+    // For some unknown reason the tree needs this before the changes show up.
+    // The view is already gAbView but setting it again works.
+    gAbResultsTree.view = gAbView;
     SetStatusText(total);
     window.dispatchEvent(new CustomEvent("countchange"));
   },
 };
 
 function GetAbViewListener() {
   return gAddressBookAbViewListener;
 }
@@ -732,45 +735,46 @@ function onEnterInSearchBar() {
   ClearCardViewPane();
   if (!gQueryURIFormat) {
     // Get model query from pref. We don't want the query starting with "?"
     // as we have to prefix "?and" to this format.
     gQueryURIFormat = getModelQuery("mail.addr_book.quicksearchquery.format");
   }
 
   let searchURI = getSelectedDirectoryURI();
+  let searchQuery;
   if (!searchURI) {
     return;
   }
 
   /*
    XXX todo, handle the case where the LDAP url
    already has a query, like
    moz-abldapdirectory://nsdirectory.netscape.com:389/ou=People,dc=netscape,dc=com?(or(Department,=,Applications))
   */
   let searchInput = document.getElementById("peopleSearchInput");
   // Use helper method to split up search query to multi-word search
   // query against multiple fields.
   if (searchInput) {
     let searchWords = getSearchTokens(searchInput.value);
-    searchURI += generateQueryURI(gQueryURIFormat, searchWords);
+    searchQuery = generateQueryURI(gQueryURIFormat, searchWords);
   }
 
   if (searchURI == kAllDirectoryRoot) {
     searchURI += "?";
   }
 
   document
     .getElementById("localResultsOnlyMessage")
     .setAttribute(
       "hidden",
       !gDirectoryTreeView.hasRemoteAB || searchURI != kAllDirectoryRoot + "?"
     );
 
-  SetAbView(searchURI);
+  SetAbView(searchURI, searchQuery);
 
   // XXX todo
   // this works for synchronous searches of local addressbooks,
   // but not for LDAP searches
   SelectFirstCard();
 }
 
 function SwitchPaneFocus(event) {
--- a/mail/components/addrbook/content/addressbook.xhtml
+++ b/mail/components/addrbook/content/addressbook.xhtml
@@ -41,16 +41,17 @@
 
   <linkset>
     <html:link rel="localization" href="toolkit/global/textActions.ftl"/>
   </linkset>
 
 <script src="chrome://messenger/content/button-menu-button.js"/>
 <script src="chrome://messenger/content/jsTreeView.js"/>
 <script src="chrome://messenger/content/addressbook/abTrees.js"/>
+<script src="chrome://messenger/content/addressbook/abView.js"/>
 <script src="chrome://messenger/content/accountUtils.js"/>
 <script src="chrome://messenger/content/mailCore.js"/>
 <script src="chrome://messenger/content/addressbook/addressbook.js"/>
 <script src="chrome://messenger/content/addressbook/map-list.js"/>
 <script src="chrome://messenger/content/addressbook/abCommon.js"/>
 <script src="chrome://communicator/content/contentAreaClick.js"/>
 <script src="chrome://global/content/printUtils.js"/>
 <script src="chrome://messenger/content/msgPrintEngine.js"/>
--- a/mail/components/addrbook/test/browser/browser.ini
+++ b/mail/components/addrbook/test/browser/browser.ini
@@ -4,13 +4,14 @@ 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
 tags = addrbook
 
+[browser_contact_tree.js]
 [browser_directory_tree.js]
 [browser_ldap_search.js]
 support-files = ../../../../../mailnews/addrbook/test/unit/data/ldap_contacts.json
 [browser_mailing_lists.js]
 [browser_search.js]
new file mode 100644
--- /dev/null
+++ b/mail/components/addrbook/test/browser/browser_contact_tree.js
@@ -0,0 +1,240 @@
+/* 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/. */
+
+var { toXPCOMArray } = ChromeUtils.import(
+  "resource:///modules/iteratorUtils.jsm"
+);
+var { mailTestUtils } = ChromeUtils.import(
+  "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+
+function createAddressBook(name) {
+  let dirPrefId = MailServices.ab.newAddressBook(name, "", 101);
+  return MailServices.ab.getDirectoryFromId(dirPrefId);
+}
+
+function createContact(firstName, lastName) {
+  let contact = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+    Ci.nsIAbCard
+  );
+  contact.displayName = `${firstName} ${lastName}`;
+  contact.firstName = firstName;
+  contact.lastName = lastName;
+  contact.primaryEmail = `${firstName}.${lastName}@invalid`;
+  return contact;
+}
+
+function createMailingList(name) {
+  let list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance(
+    Ci.nsIAbDirectory
+  );
+  list.isMailList = true;
+  list.dirName = name;
+  return list;
+}
+
+var observer = {
+  topics: [
+    "addrbook-directory-created",
+    "addrbook-directory-updated",
+    "addrbook-directory-deleted",
+    "addrbook-contact-created",
+    "addrbook-contact-updated",
+    "addrbook-contact-deleted",
+    "addrbook-list-created",
+    "addrbook-list-updated",
+    "addrbook-list-deleted",
+    "addrbook-list-member-added",
+    "addrbook-list-member-removed",
+  ],
+  setUp() {
+    for (let topic of this.topics) {
+      Services.obs.addObserver(observer, topic);
+    }
+  },
+  cleanUp() {
+    for (let topic of this.topics) {
+      Services.obs.removeObserver(observer, topic);
+    }
+  },
+  promiseNotification() {
+    return new Promise(resolve => {
+      this.notificationPromise = resolve;
+    });
+  },
+  resolveNotificationPromise() {
+    if (this.notificationPromise) {
+      let resolve = this.notificationPromise;
+      delete this.notificationPromise;
+      resolve();
+    }
+  },
+
+  notifications: [],
+  observe(subject, topic, data) {
+    info([topic, subject, data]);
+    this.notifications.push([topic, subject, data]);
+    this.resolveNotificationPromise();
+  },
+};
+
+add_task(async () => {
+  function openRootDirectory() {
+    mailTestUtils.treeClick(EventUtils, abWindow, abDirTree, 0, 0, {});
+  }
+
+  function openDirectory(directory) {
+    for (let i = 0; i < abDirTree.view.rowCount; i++) {
+      abDirTree.changeOpenState(i, true);
+    }
+
+    let row = abWindow.gDirectoryTreeView.getIndexForId(directory.URI);
+    mailTestUtils.treeClick(EventUtils, abWindow, abDirTree, row, 0, {});
+  }
+
+  function deleteRowWithPrompt(row) {
+    let promptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+    mailTestUtils.treeClick(EventUtils, abWindow, abContactTree, row, 0, {});
+    EventUtils.synthesizeKey("VK_DELETE", {}, abWindow);
+    return promptPromise;
+  }
+
+  function checkRows(...expectedCards) {
+    abContactTree.view.QueryInterface(Ci.nsIAbView);
+    Assert.equal(
+      abContactTree.view.rowCount,
+      expectedCards.length,
+      "rowCount correct"
+    );
+    for (let i = 0; i < expectedCards.length; i++) {
+      if (expectedCards[i].isMailList) {
+        Assert.equal(
+          abContactTree.view.getCardFromRow(i).displayName,
+          expectedCards[i].dirName
+        );
+      } else {
+        Assert.equal(
+          abContactTree.view.getCardFromRow(i).displayName,
+          expectedCards[i].displayName
+        );
+      }
+    }
+  }
+
+  let bookA = createAddressBook("book A");
+  let contactA1 = bookA.addCard(createContact("contact", "A1"));
+  let bookB = createAddressBook("book B");
+  let contactB1 = bookB.addCard(createContact("contact", "B1"));
+
+  let abWindow = await openAddressBookWindow();
+  let abDirTree = abWindow.GetDirTree();
+  let abContactTree = abWindow.document.getElementById("abResultsTree");
+
+  observer.setUp();
+
+  openRootDirectory();
+  checkRows(contactA1, contactB1);
+
+  // While in bookA, add a contact and list. Check that they show up.
+  openDirectory(bookA);
+  checkRows(contactA1);
+  let contactA2 = bookA.addCard(createContact("contact", "A2")); // Add A2.
+  checkRows(contactA1, contactA2);
+  let listC = bookA.addMailList(createMailingList("list C")); // Add C.
+  // Adding a mailing list changes the view. Go back to where we were.
+  openDirectory(bookA);
+  checkRows(contactA1, contactA2, listC);
+  listC.addCard(contactA1);
+  checkRows(contactA1, contactA2, listC);
+
+  openRootDirectory();
+  checkRows(contactA1, contactA2, contactB1, listC);
+
+  // While in listC, add a member and remove a member. Check that they show up
+  // or disappear as appropriate.
+  openDirectory(listC);
+  checkRows(contactA1);
+  listC.addCard(contactA2);
+  checkRows(contactA1, contactA2);
+  await deleteRowWithPrompt(0);
+  checkRows(contactA2);
+
+  openRootDirectory();
+  checkRows(contactA1, contactA2, contactB1, listC);
+
+  // While in bookA, delete a contact. Check it disappears.
+  openDirectory(bookA);
+  checkRows(contactA1, contactA2, listC);
+  await deleteRowWithPrompt(0); // Delete A1.
+  checkRows(contactA2, listC);
+  // Now do some things in an unrelated book. Check nothing changes here.
+  let contactB2 = bookB.addCard(createContact("contact", "B2")); // Add B2.
+  checkRows(contactA2, listC);
+  let listD = bookB.addMailList(createMailingList("list D")); // Add D.
+  // Adding a mailing list changes the view. Go back to where we were.
+  openDirectory(bookA);
+  checkRows(contactA2, listC);
+  listD.addCard(contactB1);
+  checkRows(contactA2, listC);
+
+  openRootDirectory();
+  checkRows(contactA2, contactB1, contactB2, listC, listD);
+
+  // While in listC, do some things in an unrelated list. Check nothing
+  // changes here.
+  openDirectory(listC);
+  checkRows(contactA2);
+  listD.addCard(contactB2);
+  checkRows(contactA2);
+  listD.deleteCards(toXPCOMArray([contactB1], Ci.nsIMutableArray));
+  checkRows(contactA2);
+  bookB.deleteCards(toXPCOMArray([contactB1], Ci.nsIMutableArray));
+  checkRows(contactA2);
+
+  openRootDirectory();
+  checkRows(contactA2, contactB2, listC, listD);
+
+  // While in bookA, do some things in an unrelated book. Check nothing
+  // changes here.
+  openDirectory(bookA);
+  checkRows(contactA2, listC);
+  bookB.deleteDirectory(listD); // Delete D.
+  // Removing a mailing list changes the view. Go back to where we were.
+  openDirectory(bookA);
+  checkRows(contactA2, listC);
+  await deleteRowWithPrompt(1); // Delete C.
+  checkRows(contactA2);
+
+  // While in "All Address Books", make some changes and check that things
+  // appear or disappear as appropriate.
+  openRootDirectory();
+  checkRows(contactA2, contactB2);
+  let listE = bookB.addMailList(createMailingList("list E")); // Add E.
+  // Adding a mailing list changes the view. Go back to where we were.
+  openRootDirectory();
+  checkRows(contactA2, contactB2, listE);
+  listE.addCard(contactB2);
+  checkRows(contactA2, contactB2, listE);
+  listE.deleteCards(toXPCOMArray([contactB2], Ci.nsIMutableArray));
+  checkRows(contactA2, contactB2, listE);
+  bookB.deleteDirectory(listE); // Delete E.
+  // Removing a mailing list changes the view. Go back to where we were.
+  openRootDirectory();
+  checkRows(contactA2, contactB2);
+  await deleteRowWithPrompt(1);
+  checkRows(contactA2);
+  bookA.deleteCards(toXPCOMArray([contactA2], Ci.nsIMutableArray));
+  checkRows();
+
+  abWindow.close();
+
+  let deletePromise = observer.promiseNotification();
+  MailServices.ab.deleteAddressBook(bookA.URI);
+  await deletePromise;
+  deletePromise = observer.promiseNotification();
+  MailServices.ab.deleteAddressBook(bookB.URI);
+  await deletePromise;
+
+  observer.cleanUp();
+});
--- a/mail/components/addrbook/test/browser/browser_mailing_lists.js
+++ b/mail/components/addrbook/test/browser/browser_mailing_lists.js
@@ -280,19 +280,22 @@ add_task(async () => {
   );
 
   is(
     global.dirTree.view.getCellText(2, global.dirTree.columns[0]),
     inputs.abName,
     `address book ("${inputs.abName}") is displayed in the address book list`
   );
 
-  // Expand the tree to reveal the mailing list.
-  global.dirTreeClick(2, 1);
-  EventUtils.sendKey("RETURN", global.abWindow);
+  // Expand the tree to reveal the mailing list. It might already be expanded
+  // if earlier tests ran (which is a bug), so check first.
+  if (global.dirTree.view.rowCount == 4) {
+    global.dirTreeClick(2, 1);
+    EventUtils.sendKey("RETURN", global.abWindow);
+  }
 
   is(
     global.dirTree.view.getCellText(3, global.dirTree.columns[0]),
     inputs.mlName,
     `mailing list ("${inputs.mlName}") is displayed in the address book list`
   );
 
   // Open the mailing list dialog, the callback above interacts with it.
--- a/mail/components/addrbook/test/browser/browser_search.js
+++ b/mail/components/addrbook/test/browser/browser_search.js
@@ -30,17 +30,18 @@ add_task(async () => {
       }
     });
   }
 
   function checkRows(...expectedCards) {
     is(resultsTree.view.rowCount, expectedCards.length, "rowCount correct");
     for (let i = 0; i < expectedCards.length; i++) {
       is(
-        resultsTree.view.getCardFromRow(i).displayName,
+        resultsTree.view.QueryInterface(Ci.nsIAbView).getCardFromRow(i)
+          .displayName,
         expectedCards[i].displayName,
         `row ${i} has the right contact`
       );
     }
   }
 
   let personalBook = MailServices.ab.getDirectoryFromId("ldap_2.servers.pab");
   let historyBook = MailServices.ab.getDirectoryFromId(
--- a/mail/test/browser/addrbook/browser_addressBook.js
+++ b/mail/test/browser/addrbook/browser_addressBook.js
@@ -258,16 +258,17 @@ add_task(function test_deleting_contacts
   );
   confirmMultiple = confirmMultiple.replace(/.*;/, "").replace("#1", "3");
 
   // Add some contacts to the address book
   load_contacts_into_address_book(addrBook1, toDelete);
   select_address_book(addrBook1);
 
   let totalEntries = abController.window.gAbView.rowCount;
+  Assert.equal(totalEntries, 4);
 
   // Set the mock prompt to return false, so that the
   // contact should not be deleted.
   gMockPromptService.returnValue = false;
 
   // Now attempt to delete the contact
   select_contacts(toDelete);
   abController.keypress(null, "VK_DELETE", {});
--- a/mailnews/addrbook/content/abDragDrop.js
+++ b/mailnews/addrbook/content/abDragDrop.js
@@ -342,17 +342,17 @@ var abDirTreeObserver = {
     } else {
       cardsTransferredText = PluralForm.get(
         numrows,
         gAddressBookBundle.getFormattedString("contactsCopied", [numrows])
       );
     }
 
     if (srcURI == kAllDirectoryRoot + "?") {
-      SetAbView(srcURI);
+      SetAbView();
     }
 
     document.getElementById("statusText").label = cardsTransferredText;
   },
 
   onToggleOpenState() {},
 
   onCycleHeader(colID, elt) {},
--- a/mailnews/addrbook/content/abResultsPane.js
+++ b/mailnews/addrbook/content/abResultsPane.js
@@ -1,15 +1,16 @@
 /* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 ; js-indent-level: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 /* import-globals-from ../../../mail/components/addrbook/content/abCommon.js */
+/* import-globals-from abView.js */
 /* globals GetAbViewListener */
 // gCurFrame is SeaMonkey-only */
 /* globals gCurFrame */
 
 /**
  * Use of items in this file require:
  *
  * getSelectedDirectoryURI()
@@ -36,17 +37,17 @@ var kCardsOnly = 4;
 // Global Variables
 
 // gAbView holds an object with an nsIAbView interface
 var gAbView = null;
 // Holds a reference to the "abResultsTree" document element. Initially
 // set up by SetAbView.
 var gAbResultsTree = null;
 
-function SetAbView(aURI) {
+function SetAbView(aURI, aSearchQuery) {
   // If we don't have a URI, just clear the view and leave everything else
   // alone.
   if (!aURI) {
     if (gAbView) {
       gAbView.clearView();
     }
     return;
   }
@@ -70,55 +71,35 @@ function SetAbView(aURI) {
       sortColumn = gAbResultsTree.getAttribute("sortCol");
     }
     var sortColumnNode = document.getElementById(sortColumn);
     if (sortColumnNode && sortColumnNode.hasAttribute("sortDirection")) {
       sortDirection = sortColumnNode.getAttribute("sortDirection");
     }
   }
 
-  var directory = GetDirectoryFromURI(aURI);
-  if (!directory && aURI.startsWith("moz-abdirectory://?")) {
-    // This is an obsolete reference to the root directory, which isn't a thing
-    // any more. Fortunately all we need is a way to get the URI to gAbView, so
-    // we can pretend we have a real directory.
-    directory = {
-      QueryInterface: ChromeUtils.generateQI([Ci.nsIAbDirectory]),
-      get URI() {
-        return aURI;
-      },
-    };
-  }
-
-  if (!gAbView) {
-    gAbView = Cc["@mozilla.org/addressbook/abview;1"].createInstance(
-      Ci.nsIAbView
-    );
-  }
-
-  var actualSortColumn = gAbView.setView(
-    directory,
+  gAbView = gAbResultsTree.view = new ABView(
+    GetDirectoryFromURI(aURI),
+    aSearchQuery,
     GetAbViewListener(),
     sortColumn,
     sortDirection
-  );
-
-  gAbResultsTree.view = gAbView.QueryInterface(Ci.nsITreeView);
+  ).QueryInterface(Ci.nsITreeView);
   window.dispatchEvent(new CustomEvent("viewchange"));
 
-  UpdateSortIndicators(actualSortColumn, sortDirection);
+  UpdateSortIndicators(sortColumn, sortDirection);
 
   // If the selected address book is LDAP and the search box is empty,
   // inform the user of the empty results pane.
   let abResultsTree = document.getElementById("abResultsTree");
   let cardViewOuterBox = document.getElementById("CardViewOuterBox");
   let blankResultsPaneMessageBox = document.getElementById(
     "blankResultsPaneMessageBox"
   );
-  if (aURI.startsWith("moz-abldapdirectory://") && !aURI.includes("?")) {
+  if (aURI.startsWith("moz-abldapdirectory://") && !aSearchQuery) {
     if (abResultsTree) {
       abResultsTree.hidden = true;
     }
     if (cardViewOuterBox) {
       cardViewOuterBox.hidden = true;
     }
     if (blankResultsPaneMessageBox) {
       blankResultsPaneMessageBox.hidden = false;
@@ -132,19 +113,17 @@ function SetAbView(aURI) {
     }
     if (blankResultsPaneMessageBox) {
       blankResultsPaneMessageBox.hidden = true;
     }
   }
 }
 
 function CloseAbView() {
-  if (gAbView) {
-    gAbView.clearView();
-  }
+  gAbView = gAbResultsTree.view = null;
 }
 
 function GetOneOrMoreCardsSelected() {
   return gAbView && gAbView.selection.getRangeCount() > 0;
 }
 
 function GetSelectedAddresses() {
   return GetAddressesForCards(GetSelectedAbCards());
@@ -237,17 +216,17 @@ function GetSelectedAbCards() {
       gCurFrame.getAttribute("src") == abPanelUrl &&
       document.commandDispatcher.focusedWindow ==
         gCurFrame.contentDocument.defaultView
     ) {
       abView = gCurFrame.contentDocument.defaultView.gAbView;
     }
   }
 
-  if (!abView) {
+  if (!abView || !abView.selection) {
     return [];
   }
 
   let cards = [];
   var count = abView.selection.getRangeCount();
   for (let i = 0; i < count; ++i) {
     let start = {};
     let end = {};
@@ -382,16 +361,21 @@ function UpdateSortIndicators(colID, sor
 
   // remove the sort indicator from all the columns
   // except the one we are sorted by
   var currCol = gAbResultsTree.firstElementChild.firstElementChild;
   while (currCol) {
     if (currCol != sortedColumn && currCol.localName == "treecol") {
       currCol.removeAttribute("sortDirection");
     }
+    // Change the column header's border colour to force it to redraw.
+    // Otherwise redrawing doesn't happen until something else causes it to.
+    currCol.style.borderColor = currCol.style.borderColor
+      ? null
+      : "transparent";
     currCol = currCol.nextElementSibling;
   }
 }
 
 function InvalidateResultsPane() {
   if (gAbResultsTree) {
     gAbResultsTree.invalidate();
   }
@@ -421,16 +405,18 @@ var ResultsPaneController = {
         return true;
       case "cmd_delete":
       case "button_delete": {
         let numSelected;
         let enabled = false;
         if (gAbView && gAbView.selection) {
           if (gAbView.directory) {
             enabled = !gAbView.directory.readOnly;
+          } else {
+            enabled = true;
           }
           numSelected = gAbView.selection.count;
         } else {
           numSelected = 0;
         }
         enabled = enabled && numSelected > 0;
 
         let labelAttr = null;
new file mode 100644
--- /dev/null
+++ b/mailnews/addrbook/content/abView.js
@@ -0,0 +1,234 @@
+/* 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/. */
+
+/* globals MailServices, PROTO_TREE_VIEW, Services */
+
+var { toXPCOMArray } = ChromeUtils.import(
+  "resource:///modules/iteratorUtils.jsm"
+);
+
+function ABView(directory, searchQuery, listener, sortColumn, sortDirection) {
+  this.__proto__.__proto__ = new PROTO_TREE_VIEW();
+  this.directory = directory;
+  this.listener = listener;
+
+  let directories = directory ? [directory] : MailServices.ab.directories;
+  if (searchQuery) {
+    searchQuery = searchQuery.replace(/^\?+/, "");
+    for (let dir of directories) {
+      dir.search(searchQuery, this);
+    }
+  } else {
+    for (let dir of directories) {
+      for (let card of dir.childCards) {
+        this._rowMap.push(new abViewCard(card));
+      }
+    }
+    if (this.listener) {
+      this.listener.onCountChanged(this.rowCount);
+    }
+  }
+  this.sortBy(sortColumn, sortDirection);
+}
+ABView.prototype = {
+  QueryInterface: ChromeUtils.generateQI([
+    Ci.nsITreeView,
+    Ci.nsIAbView,
+    Ci.nsIAbDirSearchListener,
+    Ci.nsIObserver,
+    Ci.nsISupportsWeakReference,
+  ]),
+
+  _notifications: [
+    "addrbook-contact-created",
+    "addrbook-contact-deleted",
+    "addrbook-list-member-added",
+    "addrbook-list-member-removed",
+  ],
+
+  // nsITreeView
+
+  selectionChanged() {
+    if (this.listener) {
+      this.listener.onSelectionChanged();
+    }
+  },
+  setTree(tree) {
+    this.tree = tree;
+    for (let topic of this._notifications) {
+      if (tree) {
+        Services.obs.addObserver(this, topic, true);
+      } else {
+        Services.obs.removeObserver(this, topic);
+      }
+    }
+  },
+
+  // nsIAbView
+
+  deleteSelectedCards() {
+    let directoryMap = new Map();
+    for (let i = 0; i < this.selection.getRangeCount(); i++) {
+      let start = {},
+        finish = {};
+      this.selection.getRangeAt(i, start, finish);
+      for (let j = start.value; j <= finish.value; j++) {
+        let card = this.getCardFromRow(j);
+        let directoryId = card.directoryId.split("&")[0];
+        let cardSet = directoryMap.get(directoryId);
+        if (!cardSet) {
+          cardSet = new Set();
+          directoryMap.set(directoryId, cardSet);
+        }
+        cardSet.add(card);
+      }
+    }
+
+    for (let [directoryId, cardSet] of directoryMap) {
+      let directory;
+      if (this.directory && this.directory.isMailList) {
+        // Removes cards from the list instead of deleting them.
+        directory = this.directory;
+      } else {
+        directory = MailServices.ab.getDirectoryFromId(directoryId);
+      }
+
+      cardSet = [...cardSet];
+      directory.deleteCards(
+        toXPCOMArray(
+          cardSet.filter(card => !card.isMailList),
+          Ci.nsIMutableArray
+        )
+      );
+      for (let card of cardSet.filter(card => card.isMailList)) {
+        MailServices.ab.deleteAddressBook(card.mailListURI);
+      }
+    }
+  },
+
+  getCardFromRow(row) {
+    return this._rowMap[row] ? this._rowMap[row].card : null;
+  },
+
+  sortColumn: "",
+  sortDirection: "",
+  sortBy(sortColumn, sortDirection, resort) {
+    if (sortColumn == this.sortColumn && !resort) {
+      if (sortDirection == this.sortDirection) {
+        return;
+      }
+      this._rowMap.reverse();
+    } else {
+      this._rowMap.sort((a, b) => {
+        let aText = a.getText(sortColumn);
+        let bText = b.getText(sortColumn);
+        if (aText == bText) {
+          return 0;
+        }
+        return aText < bText ? -1 : 1;
+      });
+    }
+
+    if (this.tree) {
+      this.tree.invalidate();
+    }
+    this.sortColumn = sortColumn;
+    this.sortDirection = sortDirection;
+  },
+
+  // nsIAbDirSearchListener
+
+  onSearchFoundCard(card) {
+    this._rowMap.push(new abViewCard(card));
+  },
+  onSearchFinished(result, errorMsg) {
+    this.sortBy(this.sortColumn, this.sortDirection, true);
+    if (this.listener) {
+      this.listener.onCountChanged(this.rowCount);
+    }
+  },
+
+  // nsIObserver
+
+  observe(subject, topic, data) {
+    if (this.directory && this.directory.UID != data) {
+      // How did we get here?
+      return;
+    }
+
+    switch (topic) {
+      case "addrbook-list-member-added":
+        if (!this.directory) {
+          break;
+        }
+      // Falls through.
+      case "addrbook-contact-created":
+        this._rowMap.push(new abViewCard(subject));
+        if (this.listener) {
+          this.listener.onCountChanged(this.rowCount);
+        }
+        break;
+      case "addrbook-list-member-removed":
+        if (!this.directory) {
+          break;
+        }
+      // Falls through.
+      case "addrbook-contact-deleted":
+        for (let i = this._rowMap.length - 1; i >= 0; i--) {
+          if (this._rowMap[i].card.equals(subject)) {
+            this._rowMap.splice(i, 1);
+          }
+        }
+        if (this.listener) {
+          this.listener.onCountChanged(this.rowCount);
+        }
+        break;
+    }
+  },
+};
+
+function abViewCard(card) {
+  this.card = card;
+}
+abViewCard.prototype = {
+  getText(columnID) {
+    try {
+      switch (columnID) {
+        case "addrbook": {
+          let { directoryId } = this.card;
+          return directoryId.substring(directoryId.indexOf("&") + 1);
+        }
+        case "GeneratedName":
+          return this.card.generateName(
+            Services.prefs.getIntPref("mail.addr_book.lastnamefirst", 0)
+          );
+        case "_PhoneticName":
+          return this.card.generatePhoneticName(true);
+        case "ChatName":
+          return this.card.isMailList ? "" : this.card.generateChatName();
+        default:
+          return this.card.isMailList
+            ? ""
+            : this.card.getPropertyAsAString(columnID);
+      }
+    } catch (ex) {
+      return "";
+    }
+  },
+  get id() {
+    return this.card.UID;
+  },
+  get open() {
+    return false;
+  },
+  get level() {
+    return 0;
+  },
+  get children() {
+    return [];
+  },
+  getProperties() {
+    return this.card.isMailList ? "MailList" : "";
+  },
+};
--- a/mailnews/addrbook/jsaddrbook/AddrBookDirectory.jsm
+++ b/mailnews/addrbook/jsaddrbook/AddrBookDirectory.jsm
@@ -691,16 +691,149 @@ AddrBookDirectoryInner.prototype = {
   },
   get isQuery() {
     return !!this._query;
   },
   get supportsMailingLists() {
     return true;
   },
 
+  search(query, listener) {
+    if (!listener) {
+      return;
+    }
+    if (!query) {
+      listener.onSearchFinished(
+        Ci.nsIAbDirectoryQueryResultListener.queryResultStopped,
+        "No query specified."
+      );
+      return;
+    }
+    if (query[0] == "?") {
+      query = query.substring(1);
+    }
+
+    let results = Array.from(
+      this._lists.values(),
+      list =>
+        new AddrBookMailingList(
+          list.uid,
+          this,
+          list.localId,
+          list.name,
+          list.nickName,
+          list.description
+        ).asCard
+    ).concat(Array.from(this._cards.values(), card => this._getCard(card)));
+
+    // Process the query string into a tree of conditions to match.
+    let lispRegexp = /^\((and|or|not|([^\)]*)(\)+))/;
+    let index = 0;
+    let rootQuery = { children: [], op: "or" };
+    let currentQuery = rootQuery;
+
+    while (true) {
+      let match = lispRegexp.exec(query.substring(index));
+      if (!match) {
+        break;
+      }
+      index += match[0].length;
+
+      if (["and", "or", "not"].includes(match[1])) {
+        // For the opening bracket, step down a level.
+        let child = {
+          parent: currentQuery,
+          children: [],
+          op: match[1],
+        };
+        currentQuery.children.push(child);
+        currentQuery = child;
+      } else {
+        currentQuery.children.push(match[2]);
+
+        // For each closing bracket except the first, step up a level.
+        for (let i = match[3].length - 1; i > 0; i--) {
+          currentQuery = currentQuery.parent;
+        }
+      }
+    }
+
+    results = results.filter(card => {
+      let properties;
+      if (card.isMailList) {
+        properties = new Map([
+          ["DisplayName", card.displayName],
+          ["NickName", card.getProperty("NickName")],
+          ["Notes", card.getProperty("Notes")],
+        ]);
+      } else {
+        properties = this._loadCardProperties(card.UID);
+      }
+      let matches = b => {
+        if (typeof b == "string") {
+          let [name, condition, value] = b.split(",");
+          if (name == "IsMailList" && condition == "=") {
+            return card.isMailList == (value == "TRUE");
+          }
+
+          if (!properties.has(name)) {
+            return condition == "!ex";
+          }
+          if (condition == "ex") {
+            return true;
+          }
+
+          value = decodeURIComponent(value).toLowerCase();
+          let cardValue = properties.get(name).toLowerCase();
+          switch (condition) {
+            case "=":
+              return cardValue == value;
+            case "!=":
+              return cardValue != value;
+            case "lt":
+              return cardValue < value;
+            case "gt":
+              return cardValue > value;
+            case "bw":
+              return cardValue.startsWith(value);
+            case "ew":
+              return cardValue.endsWith(value);
+            case "c":
+              return cardValue.includes(value);
+            case "!c":
+              return !cardValue.includes(value);
+            case "~=":
+            case "regex":
+            default:
+              return false;
+          }
+        }
+        if (b.op == "or") {
+          return b.children.some(bb => matches(bb));
+        }
+        if (b.op == "and") {
+          return b.children.every(bb => matches(bb));
+        }
+        if (b.op == "not") {
+          return !matches(b.children[0]);
+        }
+        return false;
+      };
+
+      return matches(rootQuery);
+    }, this);
+
+    for (let card of results) {
+      listener.onSearchFoundCard(card);
+    }
+    listener.onSearchFinished(
+      Ci.nsIAbDirectoryQueryResultListener.queryResultComplete,
+      ""
+    );
+  },
   generateName(generateFormat, bundle) {
     return this.dirName;
   },
   cardForEmailAddress(emailAddress) {
     return (
       this.getCardFromProperty("PrimaryEmail", emailAddress, false) ||
       this.getCardFromProperty("SecondEmail", emailAddress, false)
     );
--- a/mailnews/addrbook/public/nsIAbDirectory.idl
+++ b/mailnews/addrbook/public/nsIAbDirectory.idl
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
 
 #include "nsISupports.idl"
 
 interface nsIAbCard;
+interface nsIAbDirSearchListener;
 interface nsIArray;
 interface nsIMutableArray;
 interface nsISimpleEnumerator;
 
 /* moz-abdirectory:// is the URI to access nsAbBSDirectory,
  * which is the root directory for all types of address books
  * this is used to get all address book directories. */
 
@@ -189,16 +190,33 @@ interface nsIAbDirectory : nsISupports {
 
   /**
    * Get the cards associated with the directory. This will return the cards
    * associated with the mailing lists too.
    */
   readonly attribute nsISimpleEnumerator childCards;
 
   /**
+   * Searches the directory for cards matching query.
+   *
+   * The query takes the form:
+   * (BOOL1(FIELD1,OP1,VALUE1)..(FIELDn,OPn,VALUEn)(BOOL2(FIELD1,OP1,VALUE1)...)...)
+   *
+   * BOOLn   A boolean operator joining subsequent terms delimited by ().
+   *         For possible values see CreateBooleanExpression().
+   * FIELDn  An addressbook card data field.
+   * OPn     An operator for the search term.
+   *         For possible values see CreateBooleanConditionString().
+   * VALUEn  The value to be matched in the FIELDn via the OPn operator.
+   *         The value must be URL encoded by the caller, if it contains any
+   *         special characters including '(' and ')'.
+   */
+  void search(in AString query, in nsIAbDirSearchListener listener);
+
+  /**
    * Returns true if this directory represents a query - i.e. the rdf resource
    * was something like moz-abmdbdirectory://abook.mab?....
    */
   readonly attribute boolean isQuery;
 
   /**
    * Initializes a directory, pointing to a particular
    * URI
--- a/mailnews/addrbook/src/AbAutoCompleteSearch.jsm
+++ b/mailnews/addrbook/src/AbAutoCompleteSearch.jsm
@@ -193,63 +193,58 @@ AbAutoCompleteSearch.prototype = {
    * a mailing list) then the function will add a result for each email address
    * that exists.
    *
    * @param searchQuery  The boolean search query to use.
    * @param directory    An nsIAbDirectory to search.
    * @param result       The result element to append results to.
    */
   _searchCards(searchQuery, directory, result) {
-    let childCards;
-    try {
-      childCards = this._abManager.getDirectory(directory.URI + searchQuery)
-        .childCards;
-    } catch (e) {
-      Cu.reportError(
-        "Error running addressbook query '" + searchQuery + "': " + e
-      );
-      return;
-    }
-
     // Cache this values to save going through xpconnect each time
-    var commentColumn = this._commentColumn == 1 ? directory.dirName : "";
+    let commentColumn = this._commentColumn == 1 ? directory.dirName : "";
 
-    // Now iterate through all the cards.
-    while (childCards.hasMoreElements()) {
-      var card = childCards.getNext();
-      if (card instanceof Ci.nsIAbCard) {
-        if (card.isMailList) {
-          this._addToResult(commentColumn, directory, card, "", true, result);
-        } else {
-          let email = card.primaryEmail;
-          if (email) {
-            this._addToResult(
-              commentColumn,
-              directory,
-              card,
-              email,
-              true,
-              result
-            );
+    if (searchQuery[0] == "?") {
+      searchQuery = searchQuery.substring(1);
+    }
+    return new Promise(resolve => {
+      this._abManager.getDirectory(directory.URI).search(searchQuery, {
+        onSearchFoundCard: card => {
+          if (card.isMailList) {
+            this._addToResult(commentColumn, directory, card, "", true, result);
+          } else {
+            let email = card.primaryEmail;
+            if (email) {
+              this._addToResult(
+                commentColumn,
+                directory,
+                card,
+                email,
+                true,
+                result
+              );
+            }
+
+            email = card.getProperty("SecondEmail", "");
+            if (email) {
+              this._addToResult(
+                commentColumn,
+                directory,
+                card,
+                email,
+                false,
+                result
+              );
+            }
           }
-
-          email = card.getProperty("SecondEmail", "");
-          if (email) {
-            this._addToResult(
-              commentColumn,
-              directory,
-              card,
-              email,
-              false,
-              result
-            );
-          }
-        }
-      }
-    }
+        },
+        onSearchFinished(result, errorMsg) {
+          resolve();
+        },
+      });
+    });
   },
 
   /**
    * Checks the parent card and email address of an autocomplete results entry
    * from a previous result against the search parameters to see if that entry
    * should still be included in the narrowed-down result.
    *
    * @param aCard        The card to check.
@@ -374,17 +369,17 @@ AbAutoCompleteSearch.prototype = {
   /**
    * Starts a search based on the given parameters.
    *
    * @see nsIAutoCompleteSearch for parameter details.
    *
    * It is expected that aSearchParam contains the identity (if any) to use
    * for determining if an address book should be autocompleted against.
    */
-  startSearch(aSearchString, aSearchParam, aPreviousResult, aListener) {
+  async startSearch(aSearchString, aSearchParam, aPreviousResult, aListener) {
     let params = aSearchParam ? JSON.parse(aSearchParam) : {};
     var result = new nsAbAutoCompleteResult(aSearchString);
     if ("type" in params && !this.applicableHeaders.has(params.type)) {
       result.searchResult = ACR.RESULT_IGNORED;
       aListener.onSearchResult(this, result);
       return;
     }
 
@@ -465,17 +460,17 @@ AbAutoCompleteSearch.prototype = {
 
       // 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.useForAutocomplete("idKey" in params ? params.idKey : null)) {
-          this._searchCards(searchQuery, dir, result);
+          await this._searchCards(searchQuery, dir, result);
         }
       }
 
       result._searchResults = [...result._collectedValues.values()];
     }
 
     // Sort the results. Scoring may have changed so do it even if this is
     // just filtered previous results.
--- a/mailnews/addrbook/src/nsAbDirProperty.cpp
+++ b/mailnews/addrbook/src/nsAbDirProperty.cpp
@@ -557,8 +557,13 @@ NS_IMETHODIMP nsAbDirProperty::SetLocali
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = locStr->SetData(NS_ConvertUTF8toUTF16(aValue));
   NS_ENSURE_SUCCESS(rv, rv);
 
   return m_DirectoryPrefs->SetComplexValue(
       aName, NS_GET_IID(nsIPrefLocalizedString), locStr);
 }
+
+NS_IMETHODIMP nsAbDirProperty::Search(const nsAString &query,
+                                      nsIAbDirSearchListener *listener) {
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
--- a/mailnews/addrbook/src/nsAbLDAPDirectory.cpp
+++ b/mailnews/addrbook/src/nsAbLDAPDirectory.cpp
@@ -129,20 +129,16 @@ NS_IMETHODIMP nsAbLDAPDirectory::GetChil
         do_CreateInstance(NS_ABJSDIRECTORY_CONTRACTID, &rv);
     NS_ENSURE_SUCCESS(rv, rv);
 
     rv = directory->Init(localDirectoryURI.get());
     NS_ENSURE_SUCCESS(rv, rv);
 
     rv = directory->GetChildCards(result);
   } else {
-    // Start the search
-    rv = StartSearch();
-    NS_ENSURE_SUCCESS(rv, rv);
-
     rv = NS_NewEmptyEnumerator(result);
   }
 
   NS_ENSURE_SUCCESS(rv, rv);
   return rv;
 }
 
 NS_IMETHODIMP nsAbLDAPDirectory::GetIsQuery(bool *aResult) {
@@ -243,70 +239,92 @@ NS_IMETHODIMP nsAbLDAPDirectory::SetLDAP
         static_cast<nsIAbDirectory *>(this), "IsSecure",
         (newIsNotSecure ? trueString : falseString),
         (newIsNotSecure ? falseString : trueString));
   }
 
   return NS_OK;
 }
 
-nsresult nsAbLDAPDirectory::StartSearch() {
-  if (!mIsQueryURI || mQueryString.IsEmpty()) return NS_OK;
+NS_IMETHODIMP nsAbLDAPDirectory::Search(const nsAString &query,
+                                        nsIAbDirSearchListener *listener) {
+  // When offline, get the child cards from the local, replicated directory.
+  bool offline;
+  nsCOMPtr<nsIIOService> ioService = mozilla::services::GetIOService();
+  NS_ENSURE_TRUE(ioService, NS_ERROR_UNEXPECTED);
+  nsresult rv = ioService->GetOffline(&offline);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (offline) {
+    nsCString fileName;
+    rv = GetReplicationFileName(fileName);
+    NS_ENSURE_SUCCESS(rv, rv);
 
-  nsresult rv = Initiate();
+    // If there is no fileName, bail out now.
+    if (fileName.IsEmpty()) {
+      listener->OnSearchFinished(1, EmptyString());
+      return NS_OK;
+    }
+
+    // Get the local directory.
+    nsAutoCString localDirectoryURI(NS_LITERAL_CSTRING(kJSDirectoryRoot));
+    localDirectoryURI.Append(fileName);
+
+    nsCOMPtr<nsIAbDirectory> directory =
+        do_CreateInstance(NS_ABJSDIRECTORY_CONTRACTID, &rv);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    rv = directory->Init(localDirectoryURI.get());
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // Perform the query.
+    return directory->Search(query, listener);
+  }
+
+  rv = Initiate();
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = StopSearch();
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsIAbDirectoryQueryArguments> arguments =
       do_CreateInstance(NS_ABDIRECTORYQUERYARGUMENTS_CONTRACTID, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsIAbBooleanExpression> expression;
-  rv = nsAbQueryStringToExpression::Convert(mQueryString,
+  rv = nsAbQueryStringToExpression::Convert(NS_ConvertUTF16toUTF8(query),
                                             getter_AddRefs(expression));
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = arguments->SetExpression(expression);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = arguments->SetQuerySubDirectories(true);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // Get the max hits to return
+  // Get the max hits to return.
   int32_t maxHits;
   rv = GetMaxHits(&maxHits);
   if (NS_FAILED(rv)) maxHits = kDefaultMaxHits;
 
-  // get the appropriate ldap attribute map, and pass it in via the
-  // TypeSpecificArgument
+  // Get the appropriate ldap attribute map, and pass it in via the
+  // TypeSpecificArgument.
   nsCOMPtr<nsIAbLDAPAttributeMap> attrMap;
   rv = GetAttributeMap(getter_AddRefs(attrMap));
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = arguments->SetTypeSpecificArg(attrMap);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  if (!mDirectoryQuery) {
-    mDirectoryQuery =
-        do_CreateInstance(NS_ABLDAPDIRECTORYQUERY_CONTRACTID, &rv);
-    NS_ENSURE_SUCCESS(rv, rv);
-  }
-
-  // Perform the query
-  rv = mDirectoryQuery->DoQuery(this, arguments, this, maxHits, 0, &mContext);
+  mDirectoryQuery = do_CreateInstance(NS_ABLDAPDIRECTORYQUERY_CONTRACTID, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // Enter lock
-  MutexAutoLock lock(mLock);
-  mPerformingQuery = true;
-  mCache.Clear();
-
-  return rv;
+  // Perform the query.
+  return mDirectoryQuery->DoQuery(this, arguments, listener, maxHits, 0,
+                                  &mContext);
 }
 
 nsresult nsAbLDAPDirectory::StopSearch() {
   nsresult rv = Initiate();
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Enter lock
   {
--- a/mailnews/addrbook/src/nsAbLDAPDirectory.h
+++ b/mailnews/addrbook/src/nsAbLDAPDirectory.h
@@ -28,16 +28,18 @@ class nsAbLDAPDirectory : public nsAbDir
   NS_IMETHOD Init(const char *aUri) override;
 
   // nsIAbDirectory methods
   NS_IMETHOD GetPropertiesChromeURI(nsACString &aResult) override;
   NS_IMETHOD GetURI(nsACString &aURI) override;
   NS_IMETHOD GetChildNodes(nsISimpleEnumerator **result) override;
   NS_IMETHOD GetChildCards(nsISimpleEnumerator **result) override;
   NS_IMETHOD GetIsQuery(bool *aResult) override;
+  NS_IMETHOD Search(const nsAString &query,
+                    nsIAbDirSearchListener *listener) override;
   NS_IMETHOD HasCard(nsIAbCard *cards, bool *hasCard) override;
   NS_IMETHOD GetSupportsMailingLists(bool *aSupportsMailingsLists) override;
   NS_IMETHOD GetReadOnly(bool *aReadOnly) override;
   NS_IMETHOD GetIsRemote(bool *aIsRemote) override;
   NS_IMETHOD GetIsSecure(bool *aIsRemote) override;
   NS_IMETHOD UseForAutocomplete(const nsACString &aIdentityKey,
                                 bool *aResult) override;
   NS_IMETHOD AddCard(nsIAbCard *aChildCard, nsIAbCard **aAddedCard) override;
@@ -46,17 +48,16 @@ class nsAbLDAPDirectory : public nsAbDir
 
   NS_DECL_NSIABLDAPDIRECTORY
   NS_DECL_NSIABDIRSEARCHLISTENER
 
  protected:
   virtual ~nsAbLDAPDirectory();
   nsresult Initiate();
 
-  nsresult StartSearch();
   nsresult StopSearch();
 
   bool mPerformingQuery;
   int32_t mContext;
   int32_t mMaxHits;
 
   nsInterfaceHashtable<nsISupportsHashKey, nsIAbCard> mCache;
 
--- a/mailnews/addrbook/src/nsAbOSXDirectory.h
+++ b/mailnews/addrbook/src/nsAbOSXDirectory.h
@@ -74,16 +74,18 @@ class nsAbOSXDirectory final : public ns
   NS_IMETHOD GetCardFromProperty(const char *aProperty,
                                  const nsACString &aValue, bool caseSensitive,
                                  nsIAbCard **aResult) override;
   NS_IMETHOD GetCardsFromProperty(const char *aProperty,
                                   const nsACString &aValue, bool aCaseSensitive,
                                   nsISimpleEnumerator **aResult) override;
   NS_IMETHOD CardForEmailAddress(const nsACString &aEmailAddress,
                                  nsIAbCard **aResult) override;
+  NS_IMETHOD Search(const nsAString &query,
+                    nsIAbDirSearchListener *listener) override;
 
   // nsIAbOSXDirectory
   nsresult AssertChildNodes() override;
   nsresult AssertDirectory(nsIAbManager *aManager,
                            nsIAbDirectory *aDirectory) override;
   nsresult AssertCard(nsIAbManager *aManager, nsIAbCard *aCard) override;
   nsresult UnassertCard(nsIAbManager *aManager, nsIAbCard *aCard,
                         nsIMutableArray *aCardList) override;
@@ -96,18 +98,16 @@ class nsAbOSXDirectory final : public ns
 
   nsresult GetCardByUri(const nsACString &aUri,
                         nsIAbOSXCard **aResult) override;
 
   nsresult GetRootOSXDirectory(nsIAbOSXDirectory **aResult);
 
  private:
   ~nsAbOSXDirectory();
-  nsresult FallbackSearch(nsIAbBooleanExpression *aExpression,
-                          nsISimpleEnumerator **aCards);
 
   // This is a list of nsIAbCards, kept separate from m_AddressList because:
   // - nsIAbDirectory items that are mailing lists, must keep a list of
   //   nsIAbCards in m_AddressList, however
   // - nsIAbDirectory items that are address books, must keep a list of
   //   nsIAbDirectory (i.e. mailing lists) in m_AddressList, AND no nsIAbCards.
   //
   // This wasn't too bad for mork, as that just gets a list from its database,
--- a/mailnews/addrbook/src/nsAbOSXDirectory.mm
+++ b/mailnews/addrbook/src/nsAbOSXDirectory.mm
@@ -211,218 +211,16 @@ static nsresult Sync(NSString *aUid) {
 
   if (!inserted && !updated && !deleted) {
     // XXX This is supposed to mean "everything was updated", but we get
     //     this whenever something has changed, so not sure what to do.
   }
 }
 @end
 
-static nsresult MapConditionString(nsIAbBooleanConditionString *aCondition, bool aNegate,
-                                   bool &aCanHandle, ABSearchElement **aResult) {
-  aCanHandle = false;
-
-  nsAbBooleanConditionType conditionType = 0;
-  nsresult rv = aCondition->GetCondition(&conditionType);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  ABSearchComparison comparison;
-  switch (conditionType) {
-    case nsIAbBooleanConditionTypes::Contains: {
-      if (!aNegate) {
-        comparison = kABContainsSubString;
-        aCanHandle = true;
-      }
-      break;
-    }
-    case nsIAbBooleanConditionTypes::DoesNotContain: {
-      if (aNegate) {
-        comparison = kABContainsSubString;
-        aCanHandle = true;
-      }
-      break;
-    }
-    case nsIAbBooleanConditionTypes::Is: {
-      comparison = aNegate ? kABNotEqual : kABEqual;
-      aCanHandle = true;
-      break;
-    }
-    case nsIAbBooleanConditionTypes::IsNot: {
-      comparison = aNegate ? kABEqual : kABNotEqual;
-      aCanHandle = true;
-      break;
-    }
-    case nsIAbBooleanConditionTypes::BeginsWith: {
-      if (!aNegate) {
-        comparison = kABPrefixMatch;
-        aCanHandle = true;
-      }
-      break;
-    }
-    case nsIAbBooleanConditionTypes::EndsWith: {
-      // comparison = kABSuffixMatch;
-      break;
-    }
-    case nsIAbBooleanConditionTypes::LessThan: {
-      comparison = aNegate ? kABGreaterThanOrEqual : kABLessThan;
-      aCanHandle = true;
-      break;
-    }
-    case nsIAbBooleanConditionTypes::GreaterThan: {
-      comparison = aNegate ? kABLessThanOrEqual : kABGreaterThan;
-      aCanHandle = true;
-      break;
-    }
-  }
-
-  if (!aCanHandle) return NS_OK;
-
-  nsCString name;
-  rv = aCondition->GetName(getter_Copies(name));
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  nsString value;
-  rv = aCondition->GetValue(getter_Copies(value));
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  uint32_t length = value.Length();
-
-  uint32_t i;
-  for (i = 0; i < nsAbOSXUtils::kPropertyMapSize; ++i) {
-    if (name.Equals(nsAbOSXUtils::kPropertyMap[i].mPropertyName)) {
-      *aResult = [ABPerson
-          searchElementForProperty:nsAbOSXUtils::kPropertyMap[i].mOSXProperty
-                             label:nsAbOSXUtils::kPropertyMap[i].mOSXLabel
-                               key:nsAbOSXUtils::kPropertyMap[i].mOSXKey
-                             value:[NSString stringWithCharacters:reinterpret_cast<const unichar *>(
-                                                                      value.get())
-                                                           length:length]
-                        comparison:comparison];
-
-      return NS_OK;
-    }
-  }
-
-  if (name.EqualsLiteral("DisplayName") && comparison == kABContainsSubString) {
-    ABSearchElement *first = [ABPerson
-        searchElementForProperty:kABFirstNameProperty
-                           label:nil
-                             key:nil
-                           value:[NSString stringWithCharacters:reinterpret_cast<const unichar *>(
-                                                                    value.get())
-                                                         length:length]
-                      comparison:comparison];
-    ABSearchElement *second = [ABPerson
-        searchElementForProperty:kABLastNameProperty
-                           label:nil
-                             key:nil
-                           value:[NSString stringWithCharacters:reinterpret_cast<const unichar *>(
-                                                                    value.get())
-                                                         length:length]
-                      comparison:comparison];
-    ABSearchElement *third = [ABGroup
-        searchElementForProperty:kABGroupNameProperty
-                           label:nil
-                             key:nil
-                           value:[NSString stringWithCharacters:reinterpret_cast<const unichar *>(
-                                                                    value.get())
-                                                         length:length]
-                      comparison:comparison];
-
-    *aResult = [ABSearchElement
-        searchElementForConjunction:kABSearchOr
-                           children:[NSArray arrayWithObjects:first, second, third, nil]];
-
-    return NS_OK;
-  }
-
-  aCanHandle = false;
-
-  return NS_OK;
-}
-
-static nsresult BuildSearchElements(nsIAbBooleanExpression *aExpression, bool &aCanHandle,
-                                    ABSearchElement **aResult) {
-  aCanHandle = true;
-
-  nsCOMPtr<nsIArray> expressions;
-  nsresult rv = aExpression->GetExpressions(getter_AddRefs(expressions));
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  nsAbBooleanOperationType operation;
-  rv = aExpression->GetOperation(&operation);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  uint32_t count;
-  rv = expressions->GetLength(&count);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  NS_ASSERTION(count > 1 && operation != nsIAbBooleanOperationTypes::NOT,
-               "This doesn't make sense!");
-
-  NSMutableArray *array = nullptr;
-  if (count > 1) array = [[NSMutableArray alloc] init];
-
-  uint32_t i;
-  nsCOMPtr<nsIAbBooleanConditionString> condition;
-  nsCOMPtr<nsIAbBooleanExpression> subExpression;
-  for (i = 0; i < count; ++i) {
-    ABSearchElement *element = nullptr;
-
-    condition = do_QueryElementAt(expressions, i);
-    if (condition) {
-      rv = MapConditionString(condition, operation == nsIAbBooleanOperationTypes::NOT, aCanHandle,
-                              &element);
-      if (NS_FAILED(rv)) break;
-    } else {
-      subExpression = do_QueryElementAt(expressions, i);
-      if (subExpression) {
-        rv = BuildSearchElements(subExpression, aCanHandle, &element);
-        if (NS_FAILED(rv)) break;
-      }
-    }
-
-    if (!aCanHandle) {
-      // remember to free the array when returning early
-      [array release];
-      return NS_OK;
-    }
-
-    if (element) {
-      if (array)
-        [array addObject:element];
-      else
-        *aResult = element;
-    }
-  }
-
-  if (array) {
-    if (NS_SUCCEEDED(rv)) {
-      ABSearchConjunction conjunction =
-          operation == nsIAbBooleanOperationTypes::AND ? kABSearchAnd : kABSearchOr;
-      *aResult = [ABSearchElement searchElementForConjunction:conjunction children:array];
-    }
-    [array release];
-  }
-
-  return rv;
-}
-
-static bool Search(nsIAbBooleanExpression *aExpression, NSArray **aResult) {
-  bool canHandle = false;
-  ABSearchElement *searchElement;
-  nsresult rv = BuildSearchElements(aExpression, canHandle, &searchElement);
-  NS_ENSURE_SUCCESS(rv, false);
-
-  if (canHandle)
-    *aResult = [[ABAddressBook sharedAddressBook] recordsMatchingSearchElement:searchElement];
-
-  return canHandle;
-}
-
 static uint32_t sObserverCount = 0;
 static ABChangedMonitor *sObserver = nullptr;
 
 nsAbOSXDirectory::nsAbOSXDirectory() {}
 
 nsAbOSXDirectory::~nsAbOSXDirectory() {
   if (--sObserverCount == 0) {
     [[NSNotificationCenter defaultCenter] removeObserver:sObserver];
@@ -832,61 +630,18 @@ nsAbOSXDirectory::GetChildNodes(nsISimpl
 }
 
 NS_IMETHODIMP
 nsAbOSXDirectory::GetChildCards(nsISimpleEnumerator **aCards) {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
 
   NS_ENSURE_ARG_POINTER(aCards);
 
-  nsresult rv;
-  NSArray *cards;
-  if (mIsQueryURI) {
-    nsCOMPtr<nsIAbBooleanExpression> expression;
-    rv = nsAbQueryStringToExpression::Convert(mQueryString, getter_AddRefs(expression));
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    bool canHandle = !m_IsMailList && Search(expression, &cards);
-    if (!canHandle) return FallbackSearch(expression, aCards);
-
-    if (!mCardList)
-      mCardList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
-    else
-      mCardList->Clear();
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    // The uuid for initializing cards
-    nsAutoCString ourUuid;
-    GetUuid(ourUuid);
-
-    // Fill the results array and update the card list
-    unsigned int nbCards = [cards count];
-
-    unsigned int i;
-    nsCOMPtr<nsIAbCard> card;
-    nsCOMPtr<nsIAbOSXDirectory> rootOSXDirectory;
-    rv = GetRootOSXDirectory(getter_AddRefs(rootOSXDirectory));
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    for (i = 0; i < nbCards; ++i) {
-      rv = GetCard([cards objectAtIndex:i], getter_AddRefs(card), rootOSXDirectory);
-
-      if (NS_FAILED(rv)) rv = CreateCard([cards objectAtIndex:i], getter_AddRefs(card));
-
-      NS_ENSURE_SUCCESS(rv, rv);
-      card->SetDirectoryId(ourUuid);
-
-      mCardList->AppendElement(card);
-    }
-
-    return NS_NewArrayEnumerator(aCards, mCardList, NS_GET_IID(nsIAbCard));
-  }
-
   // Not a search, so just return the appropriate list of items.
-  return m_IsMailList ? NS_NewArrayEnumerator(aCards, m_AddressList, NS_GET_IID(nsIAbDirectory))
+  return m_IsMailList ? NS_NewArrayEnumerator(aCards, m_AddressList, NS_GET_IID(nsIAbCard))
                       : NS_NewArrayEnumerator(aCards, mCardList, NS_GET_IID(nsIAbCard));
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
 NS_IMETHODIMP
 nsAbOSXDirectory::GetIsQuery(bool *aResult) {
   NS_ENSURE_ARG_POINTER(aResult);
@@ -1087,69 +842,53 @@ nsAbOSXDirectory::OnSearchFoundCard(nsIA
 
   nsAutoCString ourUuid;
   GetUuid(ourUuid);
   aCard->SetDirectoryId(ourUuid);
 
   return NS_OK;
 }
 
-nsresult nsAbOSXDirectory::FallbackSearch(nsIAbBooleanExpression *aExpression,
-                                          nsISimpleEnumerator **aCards) {
+NS_IMETHODIMP
+nsAbOSXDirectory::Search(const nsAString &query, nsIAbDirSearchListener *listener) {
   nsresult rv;
 
-  if (mCardList)
-    rv = mCardList->Clear();
-  else
-    mCardList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+  nsCOMPtr<nsIAbBooleanExpression> expression;
+  rv = nsAbQueryStringToExpression::Convert(NS_ConvertUTF16toUTF8(query),
+                                            getter_AddRefs(expression));
   NS_ENSURE_SUCCESS(rv, rv);
 
-  if (m_AddressList) {
-    m_AddressList->Clear();
-  } else {
-    m_AddressList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
-    NS_ENSURE_SUCCESS(rv, rv);
-  }
-
   nsCOMPtr<nsIAbDirectoryQueryArguments> arguments =
       do_CreateInstance(NS_ABDIRECTORYQUERYARGUMENTS_CONTRACTID, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  rv = arguments->SetExpression(aExpression);
+  rv = arguments->SetExpression(expression);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Don't search the subdirectories. If the current directory is a mailing
   // list, it won't have any subdirectories. If the current directory is an
   // addressbook, searching both it and the subdirectories (the mailing
   // lists), will yield duplicate results because every entry in a mailing
   // list will be an entry in the parent addressbook.
   rv = arguments->SetQuerySubDirectories(false);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // Get the directory without the query
-  nsCOMPtr<nsIAbManager> abManager = do_GetService(NS_ABMANAGER_CONTRACTID, &rv);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  nsCOMPtr<nsIAbDirectory> directory;
-  rv = abManager->GetDirectory(mURINoQuery, getter_AddRefs(directory));
-  NS_ENSURE_SUCCESS(rv, rv);
-
   // Initiate the proxy query with the no query directory
   nsCOMPtr<nsIAbDirectoryQueryProxy> queryProxy =
       do_CreateInstance(NS_ABDIRECTORYQUERYPROXY_CONTRACTID, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = queryProxy->Initiate();
   NS_ENSURE_SUCCESS(rv, rv);
 
   int32_t context = 0;
-  rv = queryProxy->DoQuery(directory, arguments, this, -1, 0, &context);
+  rv = queryProxy->DoQuery(this, arguments, listener, -1, 0, &context);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  return NS_NewArrayEnumerator(aCards, m_AddressList, NS_GET_IID(nsIAbDirectory));
+  return NS_OK;
 }
 
 nsresult nsAbOSXDirectory::DeleteUid(const nsACString &aUid) {
   if (!m_AddressList) return NS_ERROR_NULL_POINTER;
 
   nsresult rv;
   nsCOMPtr<nsIAbManager> abManager = do_GetService(NS_ABMANAGER_CONTRACTID, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
--- a/mailnews/addrbook/test/unit/head.js
+++ b/mailnews/addrbook/test/unit/head.js
@@ -26,8 +26,27 @@ function promiseDirectoryRemoved() {
       },
     };
     MailServices.ab.addAddressBookListener(
       observer,
       Ci.nsIAbListener.directoryRemoved
     );
   });
 }
+
+function acObserver() {}
+acObserver.prototype = {
+  _search: null,
+  _result: null,
+  _resolve: null,
+
+  onSearchResult(aSearch, aResult) {
+    this._search = aSearch;
+    this._result = aResult;
+    this._resolve();
+  },
+
+  waitForResult() {
+    return new Promise(resolve => {
+      this._resolve = resolve;
+    });
+  },
+};
--- a/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js
+++ b/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js
@@ -96,28 +96,16 @@ var inputs = [
   lastNames,
   displayNames,
   nickNames,
   emails,
   lists,
   bothNames,
 ];
 
-function acObserver() {}
-
-acObserver.prototype = {
-  _search: null,
-  _result: null,
-
-  onSearchResult(aSearch, aResult) {
-    this._search = aSearch;
-    this._result = aResult;
-  },
-};
-
 var PAB_CARD_DATA = [
   {
     FirstName: "firs",
     LastName: "lastn",
     DisplayName: "d",
     NickName: "ni",
     PrimaryEmail: "ema@foo.invalid",
     PreferDisplayName: true,
@@ -233,17 +221,17 @@ function setupAddressBookData(aDirURI, a
     list.isMailList = true;
     for (var prop in ld) {
       list[prop] = ld[prop];
     }
     ab.addMailList(list);
   });
 }
 
-function run_test() {
+add_task(async () => {
   // Set up addresses for in the personal address book.
   setupAddressBookData(kPABData.URI, PAB_CARD_DATA, PAB_LIST_DATA);
   // ... and collected addresses address book.
   setupAddressBookData(kCABData.URI, CAB_CARD_DATA, CAB_LIST_DATA);
 
   // Test - Create a new search component
 
   var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
@@ -257,124 +245,144 @@ function run_test() {
   // Test - Check disabling of autocomplete
 
   Services.prefs.setBoolPref("mail.enable_autocomplete", false);
 
   let param = JSON.stringify({ type: "addr_to" });
   let paramNews = JSON.stringify({ type: "addr_newsgroups" });
   let paramFollowup = JSON.stringify({ type: "addr_followup" });
 
+  let resultPromise = obs.waitForResult();
   acs.startSearch("abc", param, null, obs);
+  await resultPromise;
 
   Assert.equal(obs._search, acs);
   Assert.equal(obs._result.searchString, "abc");
   Assert.equal(obs._result.searchResult, ACR.RESULT_NOMATCH);
   Assert.equal(obs._result.errorDescription, null);
   Assert.equal(obs._result.matchCount, 0);
 
   // Test - Check Enabling of autocomplete, but with empty string.
 
   Services.prefs.setBoolPref("mail.enable_autocomplete", true);
 
+  resultPromise = obs.waitForResult();
   acs.startSearch(null, param, null, obs);
+  await resultPromise;
 
   Assert.equal(obs._search, acs);
   Assert.equal(obs._result.searchString, null);
   Assert.equal(obs._result.searchResult, ACR.RESULT_IGNORED);
   Assert.equal(obs._result.errorDescription, null);
   Assert.equal(obs._result.matchCount, 0);
   Assert.equal(obs._result.defaultIndex, -1);
 
   // Test - Check ignoring result with comma
 
+  resultPromise = obs.waitForResult();
   acs.startSearch("a,b", param, null, obs);
+  await resultPromise;
 
   Assert.equal(obs._search, acs);
   Assert.equal(obs._result.searchString, "a,b");
   Assert.equal(obs._result.searchResult, ACR.RESULT_IGNORED);
   Assert.equal(obs._result.errorDescription, null);
   Assert.equal(obs._result.matchCount, 0);
   Assert.equal(obs._result.defaultIndex, -1);
 
   // Test - No matches
 
+  resultPromise = obs.waitForResult();
   acs.startSearch("asjdkljdgfjglkfg", param, null, obs);
+  await resultPromise;
 
   Assert.equal(obs._search, acs);
   Assert.equal(obs._result.searchString, "asjdkljdgfjglkfg");
   Assert.equal(obs._result.searchResult, ACR.RESULT_NOMATCH);
   Assert.equal(obs._result.errorDescription, null);
   Assert.equal(obs._result.matchCount, 0);
   Assert.equal(obs._result.defaultIndex, -1);
 
   // Test - Matches
 
   // Basic quick-check
+  resultPromise = obs.waitForResult();
   acs.startSearch("email", param, null, obs);
+  await resultPromise;
 
   Assert.equal(obs._search, acs);
   Assert.equal(obs._result.searchString, "email");
   Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
   Assert.equal(obs._result.errorDescription, null);
   Assert.equal(obs._result.matchCount, 2);
   Assert.equal(obs._result.defaultIndex, 0);
 
   Assert.equal(obs._result.getValueAt(0), "dis <email@foo.invalid>");
   Assert.equal(obs._result.getLabelAt(0), "dis <email@foo.invalid>");
   Assert.equal(obs._result.getCommentAt(0), "");
   Assert.equal(obs._result.getStyleAt(0), "local-abook");
   Assert.equal(obs._result.getImageAt(0), "");
 
   // quick-check that nothing is found for addr_newsgroups
+  resultPromise = obsNews.waitForResult();
   acs.startSearch("email", paramNews, null, obsNews);
+  await resultPromise;
   Assert.ok(obsNews._result == null || obsNews._result.matchCount == 0);
 
   // quick-check that nothing is found for  addr_followup
+  resultPromise = obsFollowup.waitForResult();
   acs.startSearch("a@b", paramFollowup, null, obsFollowup);
+  await resultPromise;
   Assert.ok(obsFollowup._result == null || obsFollowup._result.matchCount == 0);
 
   // Now quick-check with the address book name in the comment column.
   Services.prefs.setIntPref("mail.autoComplete.commentColumn", 1);
 
+  resultPromise = obs.waitForResult();
   acs.startSearch("email", param, null, obs);
+  await resultPromise;
 
   Assert.equal(obs._search, acs);
   Assert.equal(obs._result.searchString, "email");
   Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
   Assert.equal(obs._result.errorDescription, null);
   Assert.equal(obs._result.matchCount, 2);
   Assert.equal(obs._result.defaultIndex, 0);
 
   Assert.equal(obs._result.getValueAt(0), "dis <email@foo.invalid>");
   Assert.equal(obs._result.getLabelAt(0), "dis <email@foo.invalid>");
   Assert.equal(obs._result.getCommentAt(0), kPABData.dirName);
   Assert.equal(obs._result.getStyleAt(0), "local-abook");
   Assert.equal(obs._result.getImageAt(0), "");
 
   // Check input with different case
+  resultPromise = obs.waitForResult();
   acs.startSearch("EMAIL", param, null, obs);
+  await resultPromise;
 
   Assert.equal(obs._search, acs);
   Assert.equal(obs._result.searchString, "EMAIL");
   Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
   Assert.equal(obs._result.errorDescription, null);
   Assert.equal(obs._result.matchCount, 2);
   Assert.equal(obs._result.defaultIndex, 0);
 
   Assert.equal(obs._result.getValueAt(0), "dis <email@foo.invalid>");
   Assert.equal(obs._result.getLabelAt(0), "dis <email@foo.invalid>");
   Assert.equal(obs._result.getCommentAt(0), kPABData.dirName);
   Assert.equal(obs._result.getStyleAt(0), "local-abook");
   Assert.equal(obs._result.getImageAt(0), "");
 
   // Now check multiple matches
-  function checkInputItem(element, index, array) {
+  async function checkInputItem(element, index) {
     let prevRes = obs._result;
     print("Search #" + index + ": search=" + element.search);
+    resultPromise = obs.waitForResult();
     acs.startSearch(element.search, param, prevRes, obs);
+    await resultPromise;
 
     for (let i = 0; i < obs._result.matchCount; i++) {
       print("... got " + i + ": " + obs._result.getValueAt(i));
     }
 
     for (let i = 0; i < element.expected.length; i++) {
       print(
         "... expected " +
@@ -405,22 +413,23 @@ function run_test() {
       Assert.equal(
         obs._result.getCommentAt(i),
         results[element.expected[i]].dirName
       );
       Assert.equal(obs._result.getStyleAt(i), "local-abook");
       Assert.equal(obs._result.getImageAt(i), "");
     }
   }
-  function checkInputSet(element, index, array) {
-    element.forEach(checkInputItem);
+
+  for (let inputSet of inputs) {
+    for (let i = 0; i < inputSet.length; i++) {
+      await checkInputItem(inputSet[i], i);
+    }
   }
 
-  inputs.forEach(checkInputSet);
-
   // Test - Popularity Index
   print("Checking by popularity index:");
   let pab = MailServices.ab.getDirectory(kPABData.URI);
 
   for (let card of pab.childCards) {
     if (card.isMailList) {
       continue;
     }
@@ -450,10 +459,12 @@ function run_test() {
     { search: "d", expected: [1, 4, 2, 3, 0, 5, 9] },
     { search: "di", expected: [1, 4, 2, 3, 5] },
     { search: "dis", expected: [4, 2, 3, 5] },
     { search: "disp", expected: [4, 3, 5] },
     { search: "displ", expected: [4, 5] },
     { search: "displa", expected: [5] },
   ];
 
-  popularitySearch.forEach(checkInputItem);
-}
+  for (let i = 0; i < popularitySearch.length; i++) {
+    await checkInputItem(popularitySearch[i], i);
+  }
+});
--- a/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch2.js
+++ b/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch2.js
@@ -108,29 +108,17 @@ var firstNames = [
 
 var lastNames = [
   { search: "la", expected: [1, 2] },
   { search: "las", expected: [2] },
 ];
 
 var inputs = [firstNames, lastNames];
 
-function acObserver() {}
-
-acObserver.prototype = {
-  _search: null,
-  _result: null,
-
-  onSearchResult(aSearch, aResult) {
-    this._search = aSearch;
-    this._result = aResult;
-  },
-};
-
-function run_test() {
+add_task(async () => {
   // Test - Create a new search component
 
   var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
     Ci.nsIAutoCompleteSearch
   );
 
   var obs = new acObserver();
 
@@ -150,23 +138,25 @@ function run_test() {
       comment: results[i].dirName,
       card: createCard(i + 1, 0),
     });
   }
 
   // Test - Matches
 
   // Now check multiple matches
-  function checkInputItem(element, index, array) {
+  async function checkInputItem(element, index) {
+    let resultPromise = obs.waitForResult();
     acs.startSearch(
       element.search,
       JSON.stringify({ type: "addr_to", idKey: "" }),
       lastResult,
       obs
     );
+    await resultPromise;
 
     Assert.equal(obs._search, acs);
     Assert.equal(obs._result.searchString, element.search);
     Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
     Assert.equal(obs._result.errorDescription, null);
     Assert.equal(obs._result.matchCount, element.expected.length);
 
     for (let i = 0; i < element.expected.length; ++i) {
@@ -181,14 +171,15 @@ function run_test() {
       Assert.equal(
         obs._result.getCommentAt(i),
         results[element.expected[i]].dirName
       );
       Assert.equal(obs._result.getStyleAt(i), "local-abook");
       Assert.equal(obs._result.getImageAt(i), "");
     }
   }
-  function checkInputSet(element, index, array) {
-    element.forEach(checkInputItem);
+
+  for (let inputSet of inputs) {
+    for (let i = 0; i < inputSet.length; i++) {
+      await checkInputItem(inputSet[i], i);
+    }
   }
-
-  inputs.forEach(checkInputSet);
-}
+});
--- a/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch3.js
+++ b/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch3.js
@@ -75,29 +75,17 @@ var cards = [
 ];
 
 var duplicates = [
   { search: "test", expected: [1, 2] },
   { search: "first", expected: [6, 5, 3] },
   { search: "(bracket)", expected: [7, 8] },
 ];
 
-function acObserver() {}
-
-acObserver.prototype = {
-  _search: null,
-  _result: null,
-
-  onSearchResult(aSearch, aResult) {
-    this._search = aSearch;
-    this._result = aResult;
-  },
-};
-
-function run_test() {
+add_task(async () => {
   // We set up the cards for this test manually as it is easier to set the
   // popularity index and we don't need many.
 
   // Ensure all the directories are initialised.
   MailServices.ab.directories;
 
   let ab = MailServices.ab.getDirectory(kPABData.URI);
 
@@ -119,24 +107,26 @@ function run_test() {
   // Test - duplicate elements
 
   var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
     Ci.nsIAutoCompleteSearch
   );
 
   var obs = new acObserver();
 
-  function checkInputItem(element, index, array) {
+  async function checkInputItem(element, index) {
     print("Search #" + index + ": search=" + element.search);
+    let resultPromise = obs.waitForResult();
     acs.startSearch(
       element.search,
       JSON.stringify({ type: "addr_to" }),
       null,
       obs
     );
+    await resultPromise;
 
     for (let i = 0; i < obs._result.matchCount; i++) {
       print("... got " + i + ": " + obs._result.getValueAt(i));
     }
 
     for (let i = 0; i < element.expected.length; i++) {
       print(
         "... expected " +
@@ -163,10 +153,12 @@ function run_test() {
       obs._result.QueryInterface(Ci.nsIAbAutoCompleteResult);
       Assert.equal(
         obs._result.getCardAt(i).firstName,
         cards[element.expected[i]].firstName
       );
     }
   }
 
-  duplicates.forEach(checkInputItem);
-}
+  for (let i = 0; i < duplicates.length; i++) {
+    await checkInputItem(duplicates[i], i);
+  }
+});
--- a/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js
+++ b/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js
@@ -135,29 +135,17 @@ var reductionExpectedResults = [
     "boo2@test.invalid",
     "sortbasic <foo_b@test.invalid>",
     "sortbasic <foo_a@test.invalid>",
   ],
   ["boo1@test.invalid", "boo2@test.invalid"],
   ["boo2@test.invalid"],
 ];
 
-function acObserver() {}
-
-acObserver.prototype = {
-  _search: null,
-  _result: null,
-
-  onSearchResult(aSearch, aResult) {
-    this._search = aSearch;
-    this._result = aResult;
-  },
-};
-
-function run_test() {
+add_task(async () => {
   // We set up the cards for this test manually as it is easier to set the
   // popularity index and we don't need many.
 
   // Ensure all the directories are initialised.
   MailServices.ab.directories;
 
   let ab = MailServices.ab.getDirectory(kPABData.URI);
 
@@ -184,24 +172,26 @@ function run_test() {
   var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
     Ci.nsIAutoCompleteSearch
   );
 
   var obs = new acObserver();
 
   print("Checking Initial Searches");
 
-  function checkSearch(element, index, array) {
+  async function checkSearch(element, index) {
     print("Search #" + index + ": search=" + element);
+    let resultPromise = obs.waitForResult();
     acs.startSearch(
       element,
       JSON.stringify({ type: "addr_to", idKey: "" }),
       null,
       obs
     );
+    await resultPromise;
 
     for (let i = 0; i < obs._result.matchCount; i++) {
       print("... got " + i + ": " + obs._result.getValueAt(i));
     }
 
     Assert.equal(obs._search, acs);
     Assert.equal(obs._result.searchString, element);
     Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
@@ -213,29 +203,33 @@ function run_test() {
       Assert.equal(obs._result.getLabelAt(i), expectedResults[index][i]);
       Assert.equal(obs._result.getCommentAt(i), "");
       Assert.equal(obs._result.getStyleAt(i), "local-abook");
       Assert.equal(obs._result.getImageAt(i), "");
       obs._result.QueryInterface(Ci.nsIAbAutoCompleteResult);
     }
   }
 
-  searches.forEach(checkSearch);
+  for (let i = 0; i < searches.length; i++) {
+    await checkSearch(searches[i], i);
+  }
 
   print("Checking Reduction of Search Results");
 
   var lastResult = null;
 
-  function checkReductionSearch(element, index, array) {
+  async function checkReductionSearch(element, index) {
+    let resultPromise = obs.waitForResult();
     acs.startSearch(
       element,
       JSON.stringify({ type: "addr_to", idKey: "" }),
       lastResult,
       obs
     );
+    await resultPromise;
 
     Assert.equal(obs._search, acs);
     Assert.equal(obs._result.searchString, element);
     Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
     Assert.equal(obs._result.errorDescription, null);
     Assert.equal(
       obs._result.matchCount,
       reductionExpectedResults[index].length
@@ -252,10 +246,13 @@ function run_test() {
       );
       Assert.equal(obs._result.getCommentAt(i), "");
       Assert.equal(obs._result.getStyleAt(i), "local-abook");
       Assert.equal(obs._result.getImageAt(i), "");
       obs._result.QueryInterface(Ci.nsIAbAutoCompleteResult);
     }
     lastResult = obs._result;
   }
-  reductionSearches.forEach(checkReductionSearch);
-}
+
+  for (let i = 0; i < reductionSearches.length; i++) {
+    await checkReductionSearch(reductionSearches[i], i);
+  }
+});
--- a/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch5.js
+++ b/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch5.js
@@ -32,53 +32,43 @@ var lastNames = [
   { search: "la", expected: [4, 0, 2, 3] },
   { search: "las", expected: [4, 0, 3] },
   { search: "last", expected: [4, 0] },
   { search: "lastn", expected: [0] },
 ];
 
 var inputs = [firstNames, lastNames];
 
-function acObserver() {}
-
-acObserver.prototype = {
-  _search: null,
-  _result: null,
-
-  onSearchResult(aSearch, aResult) {
-    this._search = aSearch;
-    this._result = aResult;
-  },
-};
-
-function run_test() {
+add_task(async () => {
   loadABFile("../../../data/tb2hexpopularity", kPABData.fileName);
 
   // Test - Create a new search component
 
   let acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
     Ci.nsIAutoCompleteSearch
   );
 
   let obs = new acObserver();
 
   // Ensure we've got the comment column set up for extra checking.
   Services.prefs.setIntPref("mail.autoComplete.commentColumn", 1);
 
   // Test - Matches
 
   // Now check multiple matches
-  function checkInputItem(element, index, array) {
+  async function checkInputItem(element, index) {
     print("Search #" + index + ": search=" + element.search);
+    let resultPromise = obs.waitForResult();
     acs.startSearch(
       element.search,
       JSON.stringify({ type: "addr_to" }),
       null,
       obs
     );
+    await resultPromise;
 
     for (let i = 0; i < obs._result.matchCount; i++) {
       print("... got " + i + ": " + obs._result.getValueAt(i));
     }
 
     for (let i = 0; i < element.expected.length; i++) {
       print(
         "... expected " +
@@ -116,14 +106,15 @@ function run_test() {
         let result = obs._result.QueryInterface(Ci.nsIAbAutoCompleteResult);
         Assert.equal(
           result.getCardAt(i).getProperty("PopularityIndex", -1),
           10
         );
       }
     }
   }
-  function checkInputSet(element, index, array) {
-    element.forEach(checkInputItem);
+
+  for (let inputSet of inputs) {
+    for (let i = 0; i < inputSet.length; i++) {
+      await checkInputItem(inputSet[i], i);
+    }
   }
-
-  inputs.forEach(checkInputSet);
-}
+});
--- a/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js
+++ b/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js
@@ -163,29 +163,17 @@ var inputs = [
   { search: "sh", expected: [15, 14, 2, 16, 10, 7] },
   { search: "st", expected: [3, 8] },
   { search: "paul mary", expected: [11, 12] },
   { search: '"paul mary"', expected: [11] },
   { search: '"iron man" mr "exp dev"', expected: [13] },
   { search: "short", expected: [14] },
 ];
 
-function acObserver() {}
-
-acObserver.prototype = {
-  _search: null,
-  _result: null,
-
-  onSearchResult(aSearch, aResult) {
-    this._search = aSearch;
-    this._result = aResult;
-  },
-};
-
-function run_test() {
+add_task(async () => {
   // We set up the cards for this test manually as it is easier to set the
   // popularity index and we don't need many.
 
   // Ensure all the directories are initialised.
   MailServices.ab.directories;
 
   let ab = MailServices.ab.getDirectory(kPABData.URI);
 
@@ -211,24 +199,26 @@ function run_test() {
   // Test - duplicate elements
 
   var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
     Ci.nsIAutoCompleteSearch
   );
 
   var obs = new acObserver();
 
-  function checkInputItem(element, index, array) {
+  async function checkInputItem(element, index) {
     print("Search #" + index + ": search=" + element.search);
+    let resultPromise = obs.waitForResult();
     acs.startSearch(
       element.search,
       JSON.stringify({ type: "addr_to" }),
       null,
       obs
     );
+    await resultPromise;
 
     for (let i = 0; i < obs._result.matchCount; i++) {
       print("... got " + i + ": " + obs._result.getValueAt(i));
     }
 
     for (let i = 0; i < element.expected.length; i++) {
       print(
         "... expected " +
@@ -247,10 +237,12 @@ function run_test() {
     Assert.equal(obs._result.matchCount, element.expected.length);
 
     for (let i = 0; i < element.expected.length; ++i) {
       Assert.equal(obs._result.getValueAt(i), cards[element.expected[i]].value);
       Assert.equal(obs._result.getLabelAt(i), cards[element.expected[i]].value);
     }
   }
 
-  inputs.forEach(checkInputItem);
-}
+  for (let i = 0; i < inputs.length; i++) {
+    await checkInputItem(inputs[i], i);
+  }
+});
--- a/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch7.js
+++ b/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch7.js
@@ -22,28 +22,16 @@ var results = [
 var inputs = [
   [
     { search: "t", expected: [2, 3, 0, 1, 4] },
     { search: "tom", expected: [0, 1, 2, 3, 4] },
     { search: "tomek", expected: [4] },
   ],
 ];
 
-function acObserver() {}
-
-acObserver.prototype = {
-  _search: null,
-  _result: null,
-
-  onSearchResult(aSearch, aResult) {
-    this._search = aSearch;
-    this._result = aResult;
-  },
-};
-
 var PAB_CARD_DATA = [
   {
     FirstName: "Tomas",
     LastName: "Doe",
     DisplayName: "Tomas Doe",
     NickName: "tom",
     PrimaryEmail: "tomez.doe@foo.invalid",
     SecondEmail: "tomez.doe@foo2.invalid",
@@ -97,35 +85,37 @@ function setupAddressBookData(aDirURI, a
     list.isMailList = true;
     for (var prop in ld) {
       list[prop] = ld[prop];
     }
     ab.addMailList(list);
   });
 }
 
-function run_test() {
+add_task(async () => {
   // Set up addresses for in the personal address book.
   setupAddressBookData(kPABData.URI, PAB_CARD_DATA, []);
 
   // Test - Create a new search component
 
   var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
     Ci.nsIAutoCompleteSearch
   );
 
   var obs = new acObserver();
 
   let param = JSON.stringify({ type: "addr_to" });
 
   // Now check multiple matches
-  function checkInputItem(element, index, array) {
+  async function checkInputItem(element, index) {
     let prevRes = obs._result;
     print("Search #" + index + ": search=" + element.search);
+    let resultPromise = obs.waitForResult();
     acs.startSearch(element.search, param, prevRes, obs);
+    await resultPromise;
 
     for (let i = 0; i < obs._result.matchCount; i++) {
       print("... got " + i + ": " + obs._result.getValueAt(i));
     }
     for (let i = 0; i < element.expected.length; i++) {
       print(
         "... expected " +
           i +
@@ -152,14 +142,15 @@ function run_test() {
         obs._result.getLabelAt(i),
         results[element.expected[i]].email
       );
       Assert.equal(obs._result.getCommentAt(i), "");
       Assert.equal(obs._result.getStyleAt(i), "local-abook");
       Assert.equal(obs._result.getImageAt(i), "");
     }
   }
-  function checkInputSet(element, index, array) {
-    element.forEach(checkInputItem);
+
+  for (let inputSet of inputs) {
+    for (let i = 0; i < inputSet.length; i++) {
+      await checkInputItem(inputSet[i], i);
+    }
   }
-
-  inputs.forEach(checkInputSet);
-}
+});
new file mode 100644
--- /dev/null
+++ b/mailnews/addrbook/test/unit/test_search.js
@@ -0,0 +1,63 @@
+/* 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";
+
+const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+const { getModelQuery, generateQueryURI } = ChromeUtils.import(
+  "resource:///modules/ABQueryUtils.jsm"
+);
+
+const jsonFile = do_get_file("data/ldap_contacts.json");
+
+add_task(async () => {
+  let contents = await OS.File.read(jsonFile.path);
+  let contacts = await JSON.parse(new TextDecoder().decode(contents));
+
+  let dirPrefId = MailServices.ab.newAddressBook("new book", "", 101);
+  let book = MailServices.ab.getDirectoryFromId(dirPrefId);
+
+  for (let [name, { attributes }] of Object.entries(contacts)) {
+    let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+      Ci.nsIAbCard
+    );
+    card.displayName = attributes.cn;
+    card.firstName = attributes.givenName;
+    card.lastName = attributes.sn;
+    card.primaryEmail = attributes.mail;
+    contacts[name] = book.addCard(card);
+  }
+
+  let doSearch = async function(searchString, ...expectedContacts) {
+    let foundCards = await new Promise(resolve => {
+      let listener = {
+        cards: [],
+        onSearchFoundCard(card) {
+          this.cards.push(card);
+        },
+        onSearchFinished(result, errorMessage) {
+          resolve(this.cards);
+        },
+      };
+      book.search(searchString, listener);
+    });
+
+    Assert.equal(foundCards.length, expectedContacts.length);
+    for (let name of expectedContacts) {
+      Assert.ok(foundCards.find(c => c.equals(contacts[name])));
+    }
+  };
+
+  await doSearch("(DisplayName,c,watson)", "john", "mary");
+
+  let modelQuery = getModelQuery("mail.addr_book.autocompletequery.format");
+  await doSearch(
+    generateQueryURI(modelQuery, ["holmes"]),
+    "eurus",
+    "mycroft",
+    "sherlock"
+  );
+  await doSearch(generateQueryURI(modelQuery, ["adler"]), "irene");
+  await doSearch(generateQueryURI(modelQuery, ["redbeard"]));
+});
--- a/mailnews/addrbook/test/unit/xpcshell.ini
+++ b/mailnews/addrbook/test/unit/xpcshell.ini
@@ -33,8 +33,9 @@ skip-if = debug # Fails for unknown reas
 [test_nsAbAutoCompleteSearch6.js]
 [test_nsAbAutoCompleteSearch7.js]
 [test_nsAbManager1.js]
 [test_nsAbManager2.js]
 [test_nsAbManager3.js]
 [test_nsAbManager4.js]
 [test_nsAbManager5.js]
 [test_nsIAbCard.js]
+[test_search.js]
--- a/mailnews/jar.mn
+++ b/mailnews/jar.mn
@@ -7,16 +7,17 @@ messenger.jar:
     content/messenger/addressbook/pref-directory-add.xhtml                     (addrbook/prefs/content/pref-directory-add.xhtml)
     content/messenger/addressbook/pref-editdirectories.js                      (addrbook/prefs/content/pref-editdirectories.js)
     content/messenger/addressbook/pref-editdirectories.xhtml                   (addrbook/prefs/content/pref-editdirectories.xhtml)
     content/messenger/addressbook/abAddressBookNameDialog.js                   (addrbook/content/abAddressBookNameDialog.js)
     content/messenger/addressbook/abAddressBookNameDialog.xhtml                (addrbook/content/abAddressBookNameDialog.xhtml)
     content/messenger/addressbook/abResultsPane.js                             (addrbook/content/abResultsPane.js)
     content/messenger/addressbook/abDragDrop.js                                (addrbook/content/abDragDrop.js)
     content/messenger/addressbook/abMailListDialog.js                          (addrbook/content/abMailListDialog.js)
+    content/messenger/addressbook/abView.js                                    (addrbook/content/abView.js)
     content/messenger/addressbook/map-list.js                                  (addrbook/content/map-list.js)
     content/messagebody/addressbook/print.css                                  (addrbook/content/print.css)
 *   content/messenger/AccountManager.xhtml                                     (base/prefs/content/AccountManager.xhtml)
     content/messenger/AccountManager.js                                        (base/prefs/content/AccountManager.js)
     content/messenger/am-main.xhtml                                            (base/prefs/content/am-main.xhtml)
     content/messenger/am-main.js                                               (base/prefs/content/am-main.js)
 #ifdef MOZ_SUITE
     content/messenger/am-help.js                                               (base/prefs/content/am-help.js)