Bug 1637923 - Convert JS address book directory to a JS class. r=mkmelin
authorGeoff Lankow <geoff@darktrojan.net>
Fri, 08 May 2020 12:14:08 +1200
changeset 39153 34122197a6a4cfdc121496238d6f23c859ca6dc7
parent 39152 ced94145e95c92495ccab66e4d5ac45517bcf009
child 39154 6101367410aa1d14da490e96bfb06e1cbce96257
push id402
push userclokep@gmail.com
push dateMon, 29 Jun 2020 20:48:04 +0000
reviewersmkmelin
bugs1637923
Bug 1637923 - Convert JS address book directory to a JS class. r=mkmelin
mail/components/extensions/parent/ext-addressBook.js
mailnews/addrbook/jsaddrbook/AddrBookDirectory.jsm
mailnews/addrbook/jsaddrbook/AddrBookManager.jsm
mailnews/addrbook/test/unit/test_jsaddrbook_inner.js
mailnews/addrbook/test/unit/xpcshell.ini
--- a/mail/components/extensions/parent/ext-addressBook.js
+++ b/mail/components/extensions/parent/ext-addressBook.js
@@ -523,46 +523,58 @@ this.addressBook = class extends Extensi
       contacts: {
         list(parentId) {
           let parentNode = addressBookCache.findAddressBookById(parentId);
           return addressBookCache.convert(
             addressBookCache.getContacts(parentNode),
             false
           );
         },
-        quickSearch(parentId, searchString) {
+        async quickSearch(parentId, searchString) {
           const {
             getSearchTokens,
             getModelQuery,
             generateQueryURI,
           } = ChromeUtils.import("resource:///modules/ABQueryUtils.jsm");
 
           let searchWords = getSearchTokens(searchString);
           if (searchWords.length == 0) {
             return [];
           }
           let searchFormat = getModelQuery(
             "mail.addr_book.quicksearchquery.format"
           );
+          let searchQuery = generateQueryURI(searchFormat, searchWords);
 
-          let results = [];
           let booksToSearch;
           if (parentId == null) {
             booksToSearch = [...addressBookCache.addressBooks.values()];
           } else {
             booksToSearch = [addressBookCache.findAddressBookById(parentId)];
           }
+
+          let results = [];
+          let promises = [];
           for (let book of booksToSearch) {
-            let searchURI =
-              book.item.URI + generateQueryURI(searchFormat, searchWords);
-            for (let contact of MailServices.ab.getDirectory(searchURI)
-              .childCards) {
-              results.push(addressBookCache.findContactById(contact.UID, book));
-            }
+            promises.push(
+              new Promise(resolve => {
+                book.item.search(searchQuery, {
+                  onSearchFinished(result, errorMsg) {
+                    resolve();
+                  },
+                  onSearchFoundCard(contact) {
+                    results.push(
+                      addressBookCache.findContactById(contact.UID, book)
+                    );
+                  },
+                });
+              })
+            );
           }
+          await Promise.all(promises);
 
           return addressBookCache.convert(results, false);
         },
         get(id) {
           return addressBookCache.convert(
             addressBookCache.findContactById(id),
             false
           );
--- a/mailnews/addrbook/jsaddrbook/AddrBookDirectory.jsm
+++ b/mailnews/addrbook/jsaddrbook/AddrBookDirectory.jsm
@@ -40,72 +40,25 @@ ChromeUtils.defineModuleGetter(
   "resource:///modules/AddrBookMailingList.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "newUID",
   "resource:///modules/AddrBookUtils.jsm"
 );
 
-/* This is where the address book manager creates an nsIAbDirectory. We want
- * to do things differently depending on whether or not the directory is a
- * mailing list, so we do this by abusing javascript prototypes.
- * A non-list directory has bookPrototype, a list directory has a
- * AddrBookMailingList prototype, ultimately created by getting the owner
- * directory and calling childNodes on it. This will make more sense and be
- * a lot neater once we stop using one XPCOM interface for two jobs. */
-
-function AddrBookDirectory() {}
-AddrBookDirectory.prototype = {
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIAbDirectory]),
-  classID: Components.ID("{e96ee804-0bd3-472f-81a6-8a9d65277ad3}"),
-
-  _query: null,
-
-  init(uri) {
-    this._uri = uri;
-
-    let index = uri.indexOf("?");
-    if (index >= 0) {
-      this._query = uri.substring(index + 1);
-      uri = uri.substring(0, index);
-    }
-    if (/\/MailList\d+$/.test(uri)) {
-      let parent = MailServices.ab.getDirectory(
-        uri.substring(0, uri.lastIndexOf("/"))
-      );
-      for (let list of parent.childNodes) {
-        list.QueryInterface(Ci.nsIAbDirectory);
-        if (list.URI == uri) {
-          this.__proto__ = list;
-          return;
-        }
-      }
-      throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
-    }
-
-    let fileName = uri.substring("jsaddrbook://".length);
-    if (fileName.includes("/")) {
-      fileName = fileName.substring(0, fileName.indexOf("/"));
-    }
-    this.__proto__ =
-      directories.get(fileName) || new AddrBookDirectoryInner(fileName);
-  },
-};
-
 // Keep track of all database connections, and close them at shutdown, since
 // nothing else ever tells us to close them.
 
 var connections = new Map();
 Services.obs.addObserver(() => {
   for (let connection of connections.values()) {
     connection.asyncClose();
   }
   connections.clear();
-  directories.clear();
 }, "quit-application");
 
 // Close a connection on demand. This serves as an escape hatch from C++ code.
 
 Services.obs.addObserver(async file => {
   file.QueryInterface(Ci.nsIFile);
   await closeConnectionTo(file);
   Services.obs.notifyObservers(file, "addrbook-close-ab-complete");
@@ -149,113 +102,116 @@ function openConnectionTo(file) {
   }
   return connection;
 }
 
 /**
  * Closes the SQLite connection to `file` and removes it from the cache.
  */
 function closeConnectionTo(file) {
-  directories.delete(file.leafName);
   let connection = connections.get(file.path);
   if (connection) {
     return new Promise(resolve => {
       connection.asyncClose({
         complete() {
           resolve();
         },
       });
       connections.delete(file.path);
     });
   }
   return Promise.resolve();
 }
 
-// One AddrBookDirectoryInner exists for each address book, multiple
-// AddrBookDirectory objects (e.g. queries) can use it as their prototype.
-
-var directories = new Map();
-
-/**
- * Prototype for nsIAbDirectory objects that aren't mailing lists.
- *
- * @implements {nsIAbDirectory}
- */
-function AddrBookDirectoryInner(fileName) {
-  for (let child of Services.prefs.getChildList("ldap_2.servers.")) {
-    if (
-      child.endsWith(".filename") &&
-      Services.prefs.getStringPref(child) == fileName
-    ) {
-      this.dirPrefId = child.substring(0, child.length - ".filename".length);
-      break;
-    }
-  }
-  if (!this.dirPrefId) {
-    throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+class AddrBookDirectory {
+  constructor() {
+    this._uid = null;
+    this._nextCardId = null;
+    this._nextListId = null;
   }
 
-  // Make sure we always have a file. If a file is not created, the
-  // filename may be accidentally reused.
-  let file = FileUtils.getFile("ProfD", [fileName]);
-  if (!file.exists()) {
-    file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+  init(uri) {
+    this._uri = uri;
+
+    let index = uri.indexOf("?");
+    if (index >= 0) {
+      throw new Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+    }
+    if (/\/MailList\d+$/.test(uri)) {
+      throw new Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+    }
+
+    let fileName = uri.substring("jsaddrbook://".length);
+    if (fileName.includes("/")) {
+      fileName = fileName.substring(0, fileName.indexOf("/"));
+    }
+
+    for (let child of Services.prefs.getChildList("ldap_2.servers.")) {
+      if (
+        child.endsWith(".filename") &&
+        Services.prefs.getStringPref(child) == fileName
+      ) {
+        this.dirPrefId = child.substring(0, child.length - ".filename".length);
+        break;
+      }
+    }
+    if (!this.dirPrefId) {
+      throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+    }
+
+    // Make sure we always have a file. If a file is not created, the
+    // filename may be accidentally reused.
+    let file = FileUtils.getFile("ProfD", [fileName]);
+    if (!file.exists()) {
+      file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+    }
+
+    this._fileName = fileName;
   }
 
-  directories.set(fileName, this);
-  this._inner = this;
-  this._fileName = fileName;
-}
-AddrBookDirectoryInner.prototype = {
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIAbDirectory]),
-  classID: Components.ID("{e96ee804-0bd3-472f-81a6-8a9d65277ad3}"),
-
-  _uid: null,
-  _nextCardId: null,
-  _nextListId: null,
   get _prefBranch() {
     if (!this.dirPrefId) {
       throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
     }
     return Services.prefs.getBranch(`${this.dirPrefId}.`);
-  },
+  }
   get _dbConnection() {
     let file = FileUtils.getFile("ProfD", [this.fileName]);
     let connection = openConnectionTo(file);
 
-    Object.defineProperty(this._inner, "_dbConnection", {
+    Object.defineProperty(this, "_dbConnection", {
       enumerable: true,
       value: connection,
       writable: false,
     });
     return connection;
-  },
+  }
   get _lists() {
     let listCache = new Map();
     let selectStatement = this._dbConnection.createStatement(
       "SELECT uid, localId, name, nickName, description FROM lists"
     );
     while (selectStatement.executeStep()) {
       listCache.set(selectStatement.row.uid, {
         uid: selectStatement.row.uid,
         localId: selectStatement.row.localId,
         name: selectStatement.row.name,
         nickName: selectStatement.row.nickName,
         description: selectStatement.row.description,
       });
     }
     selectStatement.finalize();
 
-    Object.defineProperty(this._inner, "_lists", {
+    Object.defineProperty(this, "_lists", {
       enumerable: true,
       value: listCache,
       writable: false,
     });
     return listCache;
-  },
+  }
   get _cards() {
     let cardCache = new Map();
     let cardStatement = this._dbConnection.createStatement(
       "SELECT uid, localId FROM cards"
     );
     while (cardStatement.executeStep()) {
       cardCache.set(cardStatement.row.uid, {
         uid: cardStatement.row.uid,
@@ -273,84 +229,84 @@ AddrBookDirectoryInner.prototype = {
         card.properties.set(
           propertiesStatement.row.name,
           propertiesStatement.row.value
         );
       }
     }
     propertiesStatement.finalize();
 
-    Object.defineProperty(this._inner, "_cards", {
+    Object.defineProperty(this, "_cards", {
       enumerable: true,
       value: cardCache,
       writable: false,
     });
     return cardCache;
-  },
+  }
 
   _getNextCardId() {
-    if (this._inner._nextCardId === null) {
+    if (this._nextCardId === null) {
       let value = 0;
       let selectStatement = this._dbConnection.createStatement(
         "SELECT MAX(localId) AS localId FROM cards"
       );
       if (selectStatement.executeStep()) {
         value = selectStatement.row.localId;
       }
-      this._inner._nextCardId = value;
+      this._nextCardId = value;
       selectStatement.finalize();
     }
-    this._inner._nextCardId++;
-    return this._inner._nextCardId.toString();
-  },
+    this._nextCardId++;
+    return this._nextCardId.toString();
+  }
   _getNextListId() {
-    if (this._inner._nextListId === null) {
+    if (this._nextListId === null) {
       let value = 0;
       let selectStatement = this._dbConnection.createStatement(
         "SELECT MAX(localId) AS localId FROM lists"
       );
       if (selectStatement.executeStep()) {
         value = selectStatement.row.localId;
       }
-      this._inner._nextListId = value;
+      this._nextListId = value;
       selectStatement.finalize();
     }
-    this._inner._nextListId++;
-    return this._inner._nextListId.toString();
-  },
+    this._nextListId++;
+    return this._nextListId.toString();
+  }
   _getCard({ uid, localId = null }) {
     let card = new AddrBookCard();
     card.directoryId = this.uuid;
     card._uid = uid;
     card.localId = localId;
     card._properties = this._loadCardProperties(uid);
     return card.QueryInterface(Ci.nsIAbCard);
-  },
+  }
   _loadCardProperties(uid) {
-    if (this._inner.hasOwnProperty("_cards")) {
-      let cachedCard = this._inner._cards.get(uid);
+    if (this.hasOwnProperty("_cards")) {
+      let cachedCard = this._cards.get(uid);
       if (cachedCard) {
         return new Map(cachedCard.properties);
       }
     }
     let properties = new Map();
     let propertyStatement = this._dbConnection.createStatement(
       "SELECT name, value FROM properties WHERE card = :card"
     );
     propertyStatement.params.card = uid;
     while (propertyStatement.executeStep()) {
       properties.set(propertyStatement.row.name, propertyStatement.row.value);
     }
     propertyStatement.finalize();
     return properties;
-  },
+  }
   _saveCardProperties(card) {
     let cachedCard;
-    if (this._inner.hasOwnProperty("_cards")) {
-      cachedCard = this._inner._cards.get(card.UID);
+    if (this.hasOwnProperty("_cards")) {
+      cachedCard = this._cards.get(card.UID);
       cachedCard.properties.clear();
     }
 
     this._dbConnection.beginTransaction();
     let deleteStatement = this._dbConnection.createStatement(
       "DELETE FROM properties WHERE card = :card"
     );
     deleteStatement.params.card = card.UID;
@@ -369,17 +325,17 @@ AddrBookDirectoryInner.prototype = {
         if (cachedCard) {
           cachedCard.properties.set(name, value);
         }
       }
     }
     this._dbConnection.commitTransaction();
     deleteStatement.finalize();
     insertStatement.finalize();
-  },
+  }
   _saveList(list) {
     // Ensure list cache exists.
     this._lists;
 
     let replaceStatement = this._dbConnection.createStatement(
       "REPLACE INTO lists (uid, localId, name, nickName, description) " +
         "VALUES (:uid, :localId, :name, :nickName, :description)"
     );
@@ -393,17 +349,17 @@ AddrBookDirectoryInner.prototype = {
 
     this._lists.set(list._uid, {
       uid: list._uid,
       localId: list._localId,
       name: list._name,
       nickName: list._nickName,
       description: list._description,
     });
-  },
+  }
   async _bulkAddCards(cards) {
     let usedUIDs = new Set();
     let cardStatement = this._dbConnection.createStatement(
       "INSERT INTO cards (uid, localId) VALUES (:uid, :localId)"
     );
     let propertiesStatement = this._dbConnection.createStatement(
       "INSERT INTO properties VALUES (:card, :name, :value)"
     );
@@ -419,23 +375,23 @@ AddrBookDirectoryInner.prototype = {
       usedUIDs.add(uid);
       let localId = this._getNextCardId();
       let cardParams = cardArray.newBindingParams();
       cardParams.bindByName("uid", uid);
       cardParams.bindByName("localId", localId);
       cardArray.addParams(cardParams);
 
       let cachedCard;
-      if (this._inner.hasOwnProperty("_cards")) {
+      if (this.hasOwnProperty("_cards")) {
         cachedCard = {
           uid,
           localId,
           properties: new Map(),
         };
-        this._inner._cards.set(uid, cachedCard);
+        this._cards.set(uid, cachedCard);
       }
 
       for (let { name, value } of fixIterator(
         card.properties,
         Ci.nsIProperty
       )) {
         if (
           [
@@ -501,82 +457,82 @@ AddrBookDirectoryInner.prototype = {
         });
         propertiesStatement.finalize();
       }
       this._dbConnection.commitTransaction();
     } catch (ex) {
       this._dbConnection.rollbackTransaction();
       throw ex;
     }
-  },
+  }
 
   /* nsIAbDirectory */
 
   get readOnly() {
     return false;
-  },
+  }
   get isRemote() {
     return false;
-  },
+  }
   get isSecure() {
     return false;
-  },
+  }
   get propertiesChromeURI() {
     return "chrome://messenger/content/addressbook/abAddressBookNameDialog.xhtml";
-  },
+  }
   get dirName() {
     return this.getLocalizedStringValue("description", "");
-  },
+  }
   set dirName(value) {
     let oldValue = this.dirName;
     this.setLocalizedStringValue("description", value);
     MailServices.ab.notifyItemPropertyChanged(this, "DirName", oldValue, value);
     Services.obs.notifyObservers(this, "addrbook-directory-updated", "DirName");
-  },
+  }
   get dirType() {
     return 101;
-  },
+  }
   get fileName() {
     return this._fileName;
-  },
+  }
   get UID() {
     if (!this._uid) {
       if (this._prefBranch.getPrefType("uid") == Services.prefs.PREF_STRING) {
-        this._inner._uid = this._prefBranch.getStringPref("uid");
+        this._uid = this._prefBranch.getStringPref("uid");
       } else {
-        this._inner._uid = newUID();
-        this._prefBranch.setStringPref("uid", this._inner._uid);
+        this._uid = newUID();
+        this._prefBranch.setStringPref("uid", this._uid);
       }
     }
-    return this._inner._uid;
-  },
+    return this._uid;
+  }
   get URI() {
     return this._uri;
-  },
+  }
   get position() {
     return this._prefBranch.getIntPref("position", 1);
-  },
+  }
   get uuid() {
     return `${this.dirPrefId}&${this.dirName}`;
-  },
+  }
   get childNodes() {
     let lists = Array.from(
       this._lists.values(),
       list =>
         new AddrBookMailingList(
           list.uid,
           this,
           list.localId,
           list.name,
           list.nickName,
           list.description
         ).asDirectory
     );
     return new SimpleEnumerator(lists);
-  },
+  }
   get childCards() {
     let results = Array.from(
       this._lists.values(),
       list =>
         new AddrBookMailingList(
           list.uid,
           this,
           list.localId,
@@ -683,23 +639,23 @@ AddrBookDirectoryInner.prototype = {
           }
           return false;
         };
 
         return matches(this._processedQuery);
       }, this);
     }
     return new SimpleEnumerator(results);
-  },
+  }
   get isQuery() {
     return !!this._query;
-  },
+  }
   get supportsMailingLists() {
     return true;
-  },
+  }
 
   search(query, listener) {
     if (!listener) {
       return;
     }
     if (!query) {
       listener.onSearchFinished(
         Ci.nsIAbDirectoryQueryResultListener.queryResultStopped,
@@ -823,54 +779,54 @@ AddrBookDirectoryInner.prototype = {
 
     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)
     );
-  },
+  }
   getCardFromProperty(property, value, caseSensitive) {
     let sql = caseSensitive
       ? "SELECT card FROM properties WHERE name = :name AND value = :value LIMIT 1"
       : "SELECT card FROM properties WHERE name = :name AND LOWER(value) = LOWER(:value) LIMIT 1";
     let selectStatement = this._dbConnection.createStatement(sql);
     selectStatement.params.name = property;
     selectStatement.params.value = value;
     let result = null;
     if (selectStatement.executeStep()) {
       result = this._getCard({ uid: selectStatement.row.card });
     }
     selectStatement.finalize();
     return result;
-  },
+  }
   getCardsFromProperty(property, value, caseSensitive) {
     let sql = caseSensitive
       ? "SELECT card FROM properties WHERE name = :name AND value = :value"
       : "SELECT card FROM properties WHERE name = :name AND LOWER(value) = LOWER(:value)";
     let selectStatement = this._dbConnection.createStatement(sql);
     selectStatement.params.name = property;
     selectStatement.params.value = value;
     let results = [];
     while (selectStatement.executeStep()) {
       results.push(this._getCard({ uid: selectStatement.row.card }));
     }
     selectStatement.finalize();
     return new SimpleEnumerator(results);
-  },
+  }
   deleteDirectory(directory) {
     let list = this._lists.get(directory.UID);
     list = new AddrBookMailingList(
       list.uid,
       this,
       list.localId,
       list.name,
       list.nickName,
@@ -879,49 +835,49 @@ AddrBookDirectoryInner.prototype = {
 
     let deleteListStatement = this._dbConnection.createStatement(
       "DELETE FROM lists WHERE uid = :uid"
     );
     deleteListStatement.params.uid = directory.UID;
     deleteListStatement.execute();
     deleteListStatement.finalize();
 
-    if (this._inner.hasOwnProperty("_lists")) {
-      this._inner._lists.delete(directory.UID);
+    if (this.hasOwnProperty("_lists")) {
+      this._lists.delete(directory.UID);
     }
 
     this._dbConnection.executeSimpleSQL(
       "DELETE FROM list_cards WHERE list NOT IN (SELECT DISTINCT uid FROM lists)"
     );
     MailServices.ab.notifyDirectoryItemDeleted(this, list.asCard);
     MailServices.ab.notifyDirectoryItemDeleted(list.asDirectory, list.asCard);
     MailServices.ab.notifyDirectoryDeleted(this, directory);
     Services.obs.notifyObservers(
       list.asDirectory,
       "addrbook-list-deleted",
       this.UID
     );
-  },
+  }
   hasCard(card) {
     return this._lists.has(card.UID) || this._cards.has(card.UID);
-  },
+  }
   hasDirectory(dir) {
     return this._lists.has(dir.UID);
-  },
+  }
   hasMailListWithName(name) {
     for (let list of this._lists.values()) {
       if (list.name.toLowerCase() == name.toLowerCase()) {
         return true;
       }
     }
     return false;
-  },
+  }
   addCard(card) {
     return this.dropCard(card, false);
-  },
+  }
   modifyCard(card) {
     let oldProperties = this._loadCardProperties(card.UID);
     let newProperties = new Map();
     for (let { name, value } of fixIterator(card.properties, Ci.nsIProperty)) {
       newProperties.set(name, value);
     }
     this._saveCardProperties(card);
     for (let [name, oldValue] of oldProperties.entries()) {
@@ -939,32 +895,32 @@ AddrBookDirectoryInner.prototype = {
           card,
           name,
           oldValue,
           newValue
         );
       }
     }
     Services.obs.notifyObservers(card, "addrbook-contact-updated", this.UID);
-  },
+  }
   deleteCards(cards) {
     if (cards === null) {
       throw Components.Exception("", Cr.NS_ERROR_INVALID_POINTER);
     }
 
     let deleteCardStatement = this._dbConnection.createStatement(
       "DELETE FROM cards WHERE uid = :uid"
     );
     for (let card of cards) {
       deleteCardStatement.params.uid = card.UID;
       deleteCardStatement.execute();
       deleteCardStatement.reset();
 
-      if (this._inner.hasOwnProperty("_cards")) {
-        this._inner._cards.delete(card.UID);
+      if (this.hasOwnProperty("_cards")) {
+        this._cards.delete(card.UID);
       }
     }
     this._dbConnection.executeSimpleSQL(
       "DELETE FROM properties WHERE card NOT IN (SELECT DISTINCT uid FROM cards)"
     );
     for (let card of cards) {
       MailServices.ab.notifyDirectoryItemDeleted(this, card);
       Services.obs.notifyObservers(card, "addrbook-contact-deleted", this.UID);
@@ -972,17 +928,17 @@ AddrBookDirectoryInner.prototype = {
 
     // We could just delete all non-existent cards from list_cards, but a
     // notification should be fired for each one. Let the list handle that.
     for (let list of this.childNodes) {
       list.deleteCards(cards);
     }
 
     deleteCardStatement.finalize();
-  },
+  }
   dropCard(card, needToCopyCard) {
     if (!card.UID) {
       throw new Error("Card must have a UID to be added to this directory.");
     }
 
     let newCard = new AddrBookCard();
     newCard.directoryId = this.uuid;
     newCard.localId = this._getNextCardId();
@@ -991,18 +947,18 @@ AddrBookDirectoryInner.prototype = {
     let insertStatement = this._dbConnection.createStatement(
       "INSERT INTO cards (uid, localId) VALUES (:uid, :localId)"
     );
     insertStatement.params.uid = newCard.UID;
     insertStatement.params.localId = newCard.localId;
     insertStatement.execute();
     insertStatement.finalize();
 
-    if (this._inner.hasOwnProperty("_cards")) {
-      this._inner._cards.set(newCard._uid, {
+    if (this.hasOwnProperty("_cards")) {
+      this._cards.set(newCard._uid, {
         uid: newCard._uid,
         localId: newCard.localId,
         properties: new Map(),
       });
     }
 
     for (let { name, value } of fixIterator(card.properties, Ci.nsIProperty)) {
       if (
@@ -1021,23 +977,23 @@ AddrBookDirectoryInner.prototype = {
       newCard.setProperty(name, value);
     }
     this._saveCardProperties(newCard);
 
     MailServices.ab.notifyDirectoryItemAdded(this, newCard);
     Services.obs.notifyObservers(newCard, "addrbook-contact-created", this.UID);
 
     return newCard;
-  },
+  }
   useForAutocomplete(identityKey) {
     return (
       Services.prefs.getBoolPref("mail.enable_autocomplete") &&
       this.getBoolValue("enable_autocomplete", true)
     );
-  },
+  }
   addMailList(list) {
     if (!list.isMailList) {
       throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
     }
 
     // Check if the new name is empty.
     if (!list.dirName) {
       throw new Components.Exception(
@@ -1078,53 +1034,59 @@ AddrBookDirectoryInner.prototype = {
     MailServices.ab.notifyDirectoryItemAdded(this, newList.asCard);
     MailServices.ab.notifyDirectoryItemAdded(this, newListDirectory);
     Services.obs.notifyObservers(
       newList.asDirectory,
       "addrbook-list-created",
       this.UID
     );
     return newListDirectory;
-  },
+  }
   editMailListToDatabase(listCard) {
     // Deliberately not implemented, this isn't a mailing list.
     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
-  },
+  }
   copyMailList(srcList) {
     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
-  },
+  }
   getIntValue(name, defaultValue) {
     return this._prefBranch.getIntPref(name, defaultValue);
-  },
+  }
   getBoolValue(name, defaultValue) {
     return this._prefBranch.getBoolPref(name, defaultValue);
-  },
+  }
   getStringValue(name, defaultValue) {
     return this._prefBranch.getStringPref(name, defaultValue);
-  },
+  }
   getLocalizedStringValue(name, defaultValue) {
     if (this._prefBranch.getPrefType(name) == Ci.nsIPrefBranch.PREF_INVALID) {
       return defaultValue;
     }
     return this._prefBranch.getComplexValue(name, Ci.nsIPrefLocalizedString)
       .data;
-  },
+  }
   setIntValue(name, value) {
     this._prefBranch.setIntPref(name, value);
-  },
+  }
   setBoolValue(name, value) {
     this._prefBranch.setBoolPref(name, value);
-  },
+  }
   setStringValue(name, value) {
     this._prefBranch.setStringPref(name, value);
-  },
+  }
   setLocalizedStringValue(name, value) {
     let valueLocal = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(
       Ci.nsIPrefLocalizedString
     );
     valueLocal.data = value;
     this._prefBranch.setComplexValue(
       name,
       Ci.nsIPrefLocalizedString,
       valueLocal
     );
-  },
-};
+  }
+}
+AddrBookDirectory.prototype.QueryInterface = ChromeUtils.generateQI([
+  Ci.nsIAbDirectory,
+]);
+AddrBookDirectory.prototype.classID = Components.ID(
+  "{e96ee804-0bd3-472f-81a6-8a9d65277ad3}"
+);
--- a/mailnews/addrbook/jsaddrbook/AddrBookManager.jsm
+++ b/mailnews/addrbook/jsaddrbook/AddrBookManager.jsm
@@ -188,18 +188,28 @@ AddrBookManager.prototype = {
     if (store.has(uri)) {
       return store.get(uri);
     }
 
     let uriParts = URI_REGEXP.exec(uri);
     if (!uriParts) {
       throw Components.Exception("", Cr.NS_ERROR_MALFORMED_URI);
     }
-    let [, scheme, , tail] = uriParts;
+    let [, scheme, fileName, tail] = uriParts;
     if (tail && types.includes(scheme)) {
+      if (scheme == "jsaddrbook" && tail.startsWith("/MailList")) {
+        let parent = this.getDirectory(`${scheme}://${fileName}`);
+        for (let list of parent.childNodes) {
+          list.QueryInterface(Ci.nsIAbDirectory);
+          if (list.URI == uri) {
+            return list;
+          }
+        }
+        throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+      }
       // `tail` could either point to a mailing list or a query.
       // Both of these will be handled differently in future.
       return createDirectoryObject(uri);
     }
     throw Components.Exception("", Cr.NS_ERROR_FAILURE);
   },
   getDirectoryFromId(dirPrefId) {
     ensureInitialized();
deleted file mode 100644
--- a/mailnews/addrbook/test/unit/test_jsaddrbook_inner.js
+++ /dev/null
@@ -1,69 +0,0 @@
-/* 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 { AddrBookDirectory } = ChromeUtils.import(
-  "resource:///modules/AddrBookDirectory.jsm"
-);
-
-add_task(async () => {
-  let a = new AddrBookDirectory();
-  a.init("jsaddrbook://abook.sqlite");
-
-  let b = new AddrBookDirectory();
-  b.init("jsaddrbook://abook.sqlite/?fakeQuery");
-
-  // Different objects, same prototype.
-  notEqual(a, b);
-  equal(a.__proto__, b.__proto__);
-
-  let inner = a.__proto__;
-  equal(inner._fileName, "abook.sqlite");
-
-  // URI should be cached on the outer object.
-  ok(a.hasOwnProperty("_uri"));
-  ok(b.hasOwnProperty("_uri"));
-  ok(!inner.hasOwnProperty("_uri"));
-  equal(a._uri, "jsaddrbook://abook.sqlite");
-  equal(b._uri, "jsaddrbook://abook.sqlite/?fakeQuery");
-
-  // Query should be cached on the outer object.
-  ok(!a.hasOwnProperty("_query"));
-  ok(b.hasOwnProperty("_query"));
-  ok(!inner.hasOwnProperty("_query"));
-  ok(!a.isQuery);
-  ok(b.isQuery);
-  equal(b._query, "fakeQuery");
-
-  // UID should be cached on the inner object.
-  a.UID;
-  ok(!a.hasOwnProperty("_uid"));
-  ok(!b.hasOwnProperty("_uid"));
-  ok(inner.hasOwnProperty("_uid"));
-  equal(a.UID, b.UID);
-
-  // Database connection should be created on first access, and shared.
-  ok(!a.hasOwnProperty("_dbConnection"));
-  ok(!b.hasOwnProperty("_dbConnection"));
-  ok(!inner.hasOwnProperty("_dbConnection"));
-  ok(inner.__proto__.hasOwnProperty("_dbConnection"));
-
-  a._dbConnection;
-  ok(!a.hasOwnProperty("_dbConnection"));
-  ok(!b.hasOwnProperty("_dbConnection"));
-  ok(inner.hasOwnProperty("_dbConnection"));
-
-  // Calling _getNextCardId should increment a shared value.
-  equal(a._getNextCardId(), 1);
-  equal(a._getNextCardId(), 2);
-  equal(b._getNextCardId(), 3);
-  equal(a._getNextCardId(), 4);
-
-  // Calling _getNextListId should increment a shared value.
-  equal(b._getNextListId(), 1);
-  equal(b._getNextListId(), 2);
-  equal(a._getNextListId(), 3);
-  equal(b._getNextListId(), 4);
-});
--- a/mailnews/addrbook/test/unit/xpcshell.ini
+++ b/mailnews/addrbook/test/unit/xpcshell.ini
@@ -11,17 +11,16 @@ tags = addrbook
 [test_bug1522453.js]
 [test_cardForEmail.js]
 [test_collection.js]
 [test_collection_2.js]
 [test_db_enumerator.js]
 [test_delete_book.js]
 [test_export.js]
 [test_jsaddrbook.js]
-[test_jsaddrbook_inner.js]
 [test_ldap1.js]
 [test_ldap2.js]
 [test_ldapOffline.js]
 [test_ldapReplication.js]
 skip-if = debug # Fails for unknown reasons.
 [test_mailList1.js]
 [test_notifications.js]
 [test_nsAbAutoCompleteMyDomain.js]