Bug 1499617 - Folder tab (3-pane) WebExtensions API; r=mkmelin
authorGeoff Lankow <geoff@darktrojan.net>
Sun, 30 Dec 2018 20:32:40 +1300
changeset 34079 71c8bc1307b83cf886b172ea2a788931bad50472
parent 34078 4b4bc20aa6deba71751a8e0043d9c07b8faeae00
child 34080 4331140b9077f55277917f2a0d8f2d2f9e839e70
push id389
push userclokep@gmail.com
push dateMon, 18 Mar 2019 19:01:53 +0000
reviewersmkmelin
bugs1499617
Bug 1499617 - Folder tab (3-pane) WebExtensions API; r=mkmelin
mail/components/extensions/ext-mail.json
mail/components/extensions/jar.mn
mail/components/extensions/parent/.eslintrc.js
mail/components/extensions/parent/ext-mail.js
mail/components/extensions/parent/ext-mailTabs.js
mail/components/extensions/schemas/mailTabs.json
mail/components/extensions/schemas/tabs.json
mailnews/base/util/MailServices.jsm
--- a/mail/components/extensions/ext-mail.json
+++ b/mail/components/extensions/ext-mail.json
@@ -45,16 +45,25 @@
     ]
   },
   "legacy": {
     "url": "chrome://messenger/content/parent/ext-legacy.js",
     "schema": "chrome://messenger/content/schemas/legacy.json",
     "scopes": ["addon_parent"],
     "manifest": ["legacy"]
   },
+  "mailTabs": {
+    "url": "chrome://messenger/content/parent/ext-mailTabs.js",
+    "schema": "chrome://messenger/content/schemas/mailTabs.json",
+    "scopes": ["addon_parent"],
+    "manifest": ["mailTabs"],
+    "paths": [
+      ["mailTabs"]
+    ]
+  },
   "pkcs11": {
     "url": "chrome://messenger/content/parent/ext-pkcs11.js",
     "schema": "chrome://messenger/content/schemas/pkcs11.json",
     "scopes": ["addon_parent"],
     "paths": [
       ["pkcs11"]
     ]
   },
--- a/mail/components/extensions/jar.mn
+++ b/mail/components/extensions/jar.mn
@@ -11,21 +11,23 @@ messenger.jar:
 
     content/messenger/parent/ext-addressBook.js    (parent/ext-addressBook.js)
     content/messenger/parent/ext-browserAction.js  (parent/ext-browserAction.js)
     content/messenger/parent/ext-cloudFile.js      (parent/ext-cloudFile.js)
     content/messenger/parent/ext-commands.js       (parent/ext-commands.js)
     content/messenger/parent/ext-composeAction.js  (parent/ext-composeAction.js)
     content/messenger/parent/ext-legacy.js         (parent/ext-legacy.js)
     content/messenger/parent/ext-mail.js           (parent/ext-mail.js)
+    content/messenger/parent/ext-mailTabs.js       (parent/ext-mailTabs.js)
     content/messenger/parent/ext-pkcs11.js         (../../../../browser/components/extensions/parent/ext-pkcs11.js)
     content/messenger/parent/ext-tabs.js           (parent/ext-tabs.js)
     content/messenger/parent/ext-windows.js        (parent/ext-windows.js)
 
     content/messenger/schemas/addressBook.json     (schemas/addressBook.json)
     content/messenger/schemas/browserAction.json   (schemas/browserAction.json)
     content/messenger/schemas/cloudFile.json       (schemas/cloudFile.json)
     content/messenger/schemas/commands.json        (schemas/commands.json)
     content/messenger/schemas/composeAction.json   (schemas/composeAction.json)
     content/messenger/schemas/legacy.json          (schemas/legacy.json)
+    content/messenger/schemas/mailTabs.json        (schemas/mailTabs.json)
     content/messenger/schemas/pkcs11.json          (../../../../browser/components/extensions/schemas/pkcs11.json)
     content/messenger/schemas/tabs.json            (schemas/tabs.json)
     content/messenger/schemas/windows.json         (schemas/windows.json)
--- a/mail/components/extensions/parent/.eslintrc.js
+++ b/mail/components/extensions/parent/.eslintrc.js
@@ -32,16 +32,20 @@ module.exports = {
     "isPrivateCookieStoreId": true,
     "isValidCookieStoreId": true,
 
     // These are defined in ext-mail.js.
     "ExtensionError": true,
     "Tab": true,
     "Window": true,
     "WindowEventManager": true,
+    "convertFolder": true,
+    "convertMessage": true,
+    "folderPathToURI": true,
+    "folderURIToPath": true,
     "getTabBrowser": true,
     "makeWidgetId": true,
     "tabGetSender": true,
     "tabTracker": true,
     "windowTracker": true,
 
     // ext-browserAction.js
     "browserActionFor": true,
--- a/mail/components/extensions/parent/ext-mail.js
+++ b/mail/components/extensions/parent/ext-mail.js
@@ -1,12 +1,14 @@
 /* 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/. */
 
+ChromeUtils.defineModuleGetter(this, "MailServices", "resource:///modules/MailServices.jsm");
+
 var {
   ExtensionError,
 } = ExtensionUtils;
 
 var {
   defineLazyGetter,
 } = ExtensionCommon;
 
@@ -499,19 +501,31 @@ class TabTracker extends TabTrackerBase 
 tabTracker = new TabTracker();
 windowTracker = new WindowTracker();
 Object.assign(global, { tabTracker, windowTracker });
 
 /**
  * Extension-specific wrapper around a Thunderbird tab.
  */
 class Tab extends TabBase {
-  /** Removes some useless properties from a tab object. */
+  /** Returns true if this tab is a 3-pane tab. */
+  get isMail3Pane() {
+    return this.nativeTab.mode.type == "folder";
+  }
+
+  /** Overrides the matches function to enable querying for 3-pane tabs. */
+  matches(queryInfo, context) {
+    let result = super.matches(queryInfo, context);
+    return result && (!queryInfo.isMail3Pane || this.isMail3Pane);
+  }
+
+  /** Adds the isMail3Pane property and removes some useless properties from a tab object. */
   convert(fallback) {
     let result = super.convert(fallback);
+    result.isMail3Pane = this.isMail3Pane;
 
     // These properties are not useful to Thunderbird extensions and are not returned.
     for (let key of [
       "attention",
       "audible",
       "discarded",
       "hidden",
       "incognito",
@@ -983,8 +997,84 @@ class WindowManager extends WindowManage
 }
 
 extensions.on("startup", (type, extension) => { // eslint-disable-line mozilla/balanced-listeners
   defineLazyGetter(extension, "tabManager",
                    () => new TabManager(extension));
   defineLazyGetter(extension, "windowManager",
                    () => new WindowManager(extension));
 });
+
+/**
+ * The following functions turn nsIMsgFolder references into more human-friendly forms.
+ * A folder can be referenced with the account key, and the path to the folder in that account.
+ */
+
+/**
+ * Convert a folder URI to a human-friendly path.
+ * @return {String}
+ */
+function folderURIToPath(uri) {
+  let path = Services.io.newURI(uri).filePath;
+  return path.split("/").map(decodeURIComponent).join("/");
+}
+
+/**
+ * Convert a human-friendly path to a folder URI. This function does not assume that the
+ * folder referenced exists.
+ * @return {String}
+ */
+function folderPathToURI(accountId, path) {
+  let rootURI = MailServices.accounts.getAccount(accountId).incomingServer.rootFolder.URI;
+  if (path == "/") {
+    return rootURI;
+  }
+  return rootURI + path.split("/").map(encodeURIComponent).join("/");
+}
+
+/**
+ * Converts an nsIMsgFolder to a simple object for use in messages.
+ * @return {Object}
+ */
+function convertFolder(folder, accountId) {
+  if (!folder) {
+    return null;
+  }
+  if (!accountId) {
+    let server = folder.server;
+    let account = MailServices.accounts.FindAccountForServer(server);
+    accountId = account.key;
+  }
+  return {
+    accountId,
+    name: folder.prettyName,
+    path: folderURIToPath(folder.URI),
+  };
+}
+
+/**
+ * Converts an nsIMsgHdr to a simle object for use in messages.
+ * This function WILL change as the API develops.
+ * @return {Object}
+ */
+function convertMessage(msgHdr) {
+  if (!msgHdr) {
+    return null;
+  }
+
+  return {
+    messageId: msgHdr.messageId,
+    read: msgHdr.isRead,
+    flagged: msgHdr.isFlagged,
+    ccList: msgHdr.ccList,
+    bccList: msgHdr.bccList,
+    author: msgHdr.mime2DecodedAuthor,
+    subject: msgHdr.mime2DecodedSubject,
+    recipients: msgHdr.mime2DecodedRecipients,
+  };
+}
+
+Object.assign(global, {
+  convertFolder,
+  convertMessage,
+  folderPathToURI,
+  folderURIToPath,
+});
new file mode 100644
--- /dev/null
+++ b/mail/components/extensions/parent/ext-mailTabs.js
@@ -0,0 +1,344 @@
+/* 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/. */
+
+ChromeUtils.defineModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+ChromeUtils.defineModuleGetter(this, "MailServices", "resource:///modules/MailServices.jsm");
+ChromeUtils.defineModuleGetter(this, "QuickFilterManager",
+                               "resource:///modules/QuickFilterManager.jsm");
+
+const LAYOUTS = ["standard", "wide", "vertical"];
+// From nsIMsgDBView.idl
+const SORT_TYPE_MAP = new Map(
+  Object.keys(Ci.nsMsgViewSortType).map(key => [Ci.nsMsgViewSortType[key], key])
+);
+const SORT_ORDER_MAP = new Map(
+  Object.keys(Ci.nsMsgViewSortOrder).map(key => [Ci.nsMsgViewSortOrder[key], key])
+);
+
+/**
+ * Converts a mail tab to a simle object for use in messages.
+ * @return {Object}
+ */
+function convertMailTab(tab, context) {
+  let mailTabObject = {
+    id: tab.id,
+    windowId: tab.windowId,
+    active: tab.active,
+    sortType: null,
+    sortOrder: null,
+    layout: LAYOUTS[Services.prefs.getIntPref("mail.pane_config.dynamic")],
+    folderPaneVisible: null,
+    messagePaneVisible: null,
+  };
+
+  let nativeTab = tab.nativeTab;
+  let { folderDisplay } = nativeTab;
+  if (folderDisplay.view.displayedFolder) {
+    let { folderPaneVisible, messagePaneVisible } = nativeTab.mode.persistTab(nativeTab);
+    mailTabObject.sortType = SORT_TYPE_MAP.get(folderDisplay.view.primarySortType);
+    mailTabObject.sortOrder = SORT_ORDER_MAP.get(folderDisplay.view.primarySortOrder);
+    mailTabObject.folderPaneVisible = folderPaneVisible;
+    mailTabObject.messagePaneVisible = messagePaneVisible;
+  }
+  if (context.extension.hasPermission("accountsRead")) {
+    mailTabObject.displayedFolder = convertFolder(folderDisplay.displayedFolder);
+  }
+  return mailTabObject;
+}
+
+/**
+ * Listens for changes in the UI to fire events.
+ */
+var uiListener = new class extends EventEmitter {
+  constructor() {
+    super();
+    this.listenerCount = 0;
+    this.handleSelect = this.handleSelect.bind(this);
+    this.lastSelected = new WeakMap();
+  }
+
+  handleSelect(event) {
+    let tab = tabTracker.activeTab;
+    if (event.target.id == "folderTree") {
+      let folder = tab.folderDisplay.displayedFolder;
+      if (this.lastSelected.get(tab) == folder) {
+        return;
+      }
+      this.lastSelected.set(tab, folder);
+      this.emit("folder-changed", tab, folder);
+      return;
+    }
+    if (event.target.id == "threadTree") {
+      this.emit("messages-changed", tab, tab.folderDisplay.view.dbView.getSelectedMsgHdrs());
+    }
+  }
+
+  addListenersToWindow(window) {
+    window.addEventListener("select", uiListener.handleSelect);
+  }
+  removeListenersFromWindow(window) {
+    window.removeEventListener("select", uiListener.handleSelect);
+  }
+  incrementListeners() {
+    this.listenerCount++;
+    if (this.listenerCount == 1) {
+      for (let window of windowTracker.browserWindows()) {
+        this.addListenersToWindow(window);
+      }
+      windowTracker.addOpenListener(this.addListenersToWindow);
+    }
+  }
+  decrementListeners() {
+    this.listenerCount--;
+    if (this.listenerCount == 0) {
+      for (let window of windowTracker.browserWindows()) {
+        this.removeListenersFromWindow(window);
+      }
+      windowTracker.removeOpenListener(this.addListenersToWindow);
+      this.lastSelected = new WeakMap();
+    }
+  }
+};
+
+class PermissionedEventManager extends EventManager {
+  constructor({ permission, context, name, register }) {
+    super({ context, name, register });
+    this.permission = permission;
+  }
+  addListener(callback) {
+    let { extension } = this.context;
+    if (!extension.hasPermission(this.permission)) {
+      throw new ExtensionError(
+        `The "${this.permission}" permission is required to use ${this.name}.`
+      );
+    }
+    return super.addListener(callback);
+  }
+}
+
+this.mailTabs = class extends ExtensionAPI {
+  getAPI(context) {
+    let { extension } = context;
+    let { tabManager } = extension;
+
+    /**
+     * Gets the tab for the given tab id, or the active tab if the id is null.
+     *
+     * @param {?Integer} tabId          The tab id to get
+     * @return {Tab}                    The matching tab, or the active tab
+     */
+    function getTabOrActive(tabId) {
+      let tab;
+      if (tabId) {
+        tab = tabManager.get(tabId);
+      } else {
+        tab = tabManager.wrapTab(tabTracker.activeTab);
+        tabId = tab.id;
+      }
+
+      if (tab && tab.isMail3Pane) {
+        return tab;
+      }
+      throw new ExtensionError(`Invalid mail tab ID: ${tabId}`);
+    }
+
+    return {
+      mailTabs: {
+        async getAll() {
+          return Array.from(tabManager.query({
+            // All of these are needed for tabManager to return every tab we want.
+            "currentWindow": null,
+            "index": null,
+            "isMail3Pane": true,
+            "lastFocusedWindow": null,
+            "screen": null,
+            "windowId": null,
+            "windowType": null,
+          }, context), (tab) => convertMailTab(tab, context));
+        },
+
+        async getCurrent() {
+          let tab = tabManager.wrapTab(tabTracker.activeTab);
+          if (!tab || !tab.isMail3Pane) {
+            return null;
+          }
+          return convertMailTab(tab, context);
+        },
+
+        async update(tabId, args) {
+          let tab = getTabOrActive(tabId);
+          let window = tab.window;
+
+          let {
+            displayedFolder,
+            layout,
+            folderPaneVisible,
+            messagePaneVisible,
+            sortOrder,
+            sortType,
+          } = args;
+
+          if (displayedFolder && extension.hasPermission("accountsRead")) {
+            let uri = folderPathToURI(displayedFolder.accountId, displayedFolder.path);
+            if (tab.active) {
+              let treeView = Cu.getGlobalForObject(tab.nativeTab).gFolderTreeView;
+              let folder = MailServices.folderLookup.getFolderForURL(uri);
+              if (folder) {
+                treeView.selectFolder(folder);
+              } else {
+                throw new ExtensionError(
+                  `Folder "${displayedFolder.path}" for account ` +
+                  `"${displayedFolder.accountId}" not found.`
+                );
+              }
+            } else {
+              tab.nativeTab.folderDisplay.showFolderUri(uri);
+            }
+          }
+
+          if (sortType && sortType in Ci.nsMsgViewSortType &&
+              sortOrder && sortOrder in Ci.nsMsgViewSortOrder) {
+            tab.nativeTab.folderDisplay.view.sort(Ci.nsMsgViewSortType[sortType],
+                                                  Ci.nsMsgViewSortOrder[sortOrder]);
+          }
+
+          // Layout applies to all folder tabs.
+          if (layout) {
+            Services.prefs.setIntPref("mail.pane_config.dynamic", LAYOUTS.indexOf(layout));
+          }
+
+          if (typeof folderPaneVisible == "boolean") {
+            if (tab.active) {
+              let document = window.document;
+              let folderPaneSplitter = document.getElementById("folderpane_splitter");
+              folderPaneSplitter.setAttribute("state", folderPaneVisible ? "open" : "collapsed");
+            } else {
+              tab.nativeTab.folderDisplay.folderPaneVisible = folderPaneVisible;
+            }
+          }
+
+          if (typeof messagePaneVisible == "boolean") {
+            if (tab.active) {
+              if (messagePaneVisible == window.IsMessagePaneCollapsed()) {
+                window.MsgToggleMessagePane();
+              }
+            } else {
+              tab.nativeTab.messageDisplay._visible = messagePaneVisible;
+              if (!messagePaneVisible) {
+                // Prevent the messagePane from showing if a message is selected.
+                tab.nativeTab.folderDisplay._aboutToSelectMessage = true;
+              }
+            }
+          }
+        },
+
+        async getSelectedMessages(tabId) {
+          if (!extension.hasPermission("messagesRead")) {
+            throw new ExtensionError(
+              `The "messagesRead" permission is required to use mailTabs.getSelectedMessages.`
+            );
+          }
+
+          let tab = getTabOrActive(tabId);
+          let { folderDisplay } = tab.nativeTab;
+          return [...folderDisplay.view.dbView.getSelectedMsgHdrs()].map(convertMessage);
+        },
+
+        async setQuickFilter(tabId, state) {
+          let tab = getTabOrActive(tabId);
+          let nativeTab = tab.nativeTab;
+          let window = Cu.getGlobalForObject(nativeTab);
+
+          let filterer;
+          if (tab.active) {
+            filterer = window.QuickFilterBarMuxer.activeFilterer;
+          } else {
+            filterer = nativeTab._ext.quickFilter;
+          }
+          filterer.clear();
+
+          filterer.visible = (state.show !== false);
+          for (let s of ["unread", "starred", "contact", "attachment"]) {
+            let key = (s == "contact") ? "addrBook" : s;
+            let value = state[s];
+            if (value === null) {
+              delete filterer.filterValues[key];
+            } else {
+              filterer.filterValues[key] = value;
+            }
+          }
+
+          if (state.tags) {
+            filterer.filterValues.tags = {
+              mode: "OR",
+              tags: {},
+            };
+            for (let tag of MailServices.tags.getAllTags({})) {
+              filterer.filterValues.tags[tag.key] = null;
+            }
+            if (typeof state.tags == "object") {
+              filterer.filterValues.tags.mode = (state.tags.mode == "any") ? "OR" : "AND";
+              for (let [key, value] of Object.entries(state.tags.tags)) {
+                filterer.filterValues.tags.tags[key] = value;
+              }
+            }
+          }
+          if (state.text) {
+            filterer.filterValues.text = {
+              states: {
+                recipients: state.text.recipients || false,
+                sender: state.text.sender || false,
+                subject: state.text.subject || false,
+                body: state.text.body || false,
+              },
+              text: state.text.text,
+            };
+          }
+
+          if (tab.active) {
+            window.QuickFilterBarMuxer.deferredUpdateSearch();
+            window.QuickFilterBarMuxer.reflectFiltererState(filterer, window.gFolderDisplay);
+          }
+          // Inactive tabs are updated when they become active, except the search doesn't. :(
+        },
+
+        onDisplayedFolderChanged: new PermissionedEventManager({
+          permission: "accountsRead",
+          context,
+          name: "mailTabs.onDisplayedFolderChanged",
+          register: (fire) => {
+            let listener = (event, tab, folder) => {
+              fire.sync(tabTracker.getId(tab), convertFolder(folder));
+            };
+
+            uiListener.on("folder-changed", listener);
+            uiListener.incrementListeners();
+            return () => {
+              uiListener.off("folder-changed", listener);
+              uiListener.decrementListeners();
+            };
+          },
+        }).api(),
+
+        onSelectedMessagesChanged: new PermissionedEventManager({
+          permission: "messagesRead",
+          context,
+          name: "mailTabs.onSelectedMessagesChanged",
+          register: (fire) => {
+            let listener = (event, tab, messages) => {
+              fire.sync(tabTracker.getId(tab), messages.map(convertMessage));
+            };
+
+            uiListener.on("messages-changed", listener);
+            uiListener.incrementListeners();
+            return () => {
+              uiListener.off("messages-changed", listener);
+              uiListener.decrementListeners();
+            };
+          },
+        }).api(),
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mail/components/extensions/schemas/mailTabs.json
@@ -0,0 +1,282 @@
+[
+  {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "OptionalPermission",
+        "choices": [
+          {
+            "type": "string",
+            "enum": [
+              "mailTabs"
+            ]
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "namespace": "mailTabs",
+    "permissions": [
+      "mailTabs"
+    ],
+    "types": [
+      {
+        "id": "QuickFilterTagsDetail",
+        "type": "object",
+        "properties": {
+          "tags": {
+            "type": "object",
+            "description": "Object keys are tags to filter on, values are <code>true</code> if the message must have the tag, or <code>false</code> if it must not have the tag. For a list of available tags, call the :ref:`messages.listTags` method.",
+            "patternProperties": {
+              ".*": {
+                "type": "boolean"
+              }
+            }
+          },
+          "mode": {
+            "type": "string",
+            "description": "Whether all of the tag filters must apply, or any of them.",
+            "enum": [
+              "all",
+              "any"
+            ]
+          }
+        }
+      },
+      {
+        "id": "QuickFilterTextDetail",
+        "type": "object",
+        "properties": {
+          "text": {
+            "type": "string",
+            "description": "String to match against the <var>recipients</var>, <var>sender</var>, <var>subject</var>, or <var>body</var>."
+          },
+          "recipients": {
+            "type": "boolean",
+            "description": "Shows messages where <var>text</var> matches the recipients.",
+            "optional": true
+          },
+          "sender": {
+            "type": "boolean",
+            "description": "Shows messages where <var>text</var> matches the sender.",
+            "optional": true
+          },
+          "subject": {
+            "type": "boolean",
+            "description": "Shows messages where <var>text</var> matches the subject.",
+            "optional": true
+          },
+          "body": {
+            "type": "boolean",
+            "description": "Shows messages where <var>text</var> matches the message body.",
+            "optional": true
+          }
+        }
+      }
+    ],
+    "functions": [
+      {
+        "name": "getAll",
+        "type": "function",
+        "description": "Returns an array of all mail tabs in all windows.",
+        "async": true,
+        "parameters": []
+      },
+      {
+        "name": "getCurrent",
+        "type": "function",
+        "description": "Returns the current mail tab in the most recent window, or throws an exception if the current tab is not a mail tab.",
+        "async": true,
+        "parameters": []
+      },
+      {
+        "name": "update",
+        "type": "function",
+        "description": "Modifies the properties of a mail tab. Properties that are not specified in <var>updateProperties</var> are not modified.",
+        "async": true,
+        "parameters": [
+          {
+            "name": "tabId",
+            "type": "integer",
+            "description": "Defaults to the selected tab of the current window.",
+            "optional": true,
+            "minimum": 1
+          },
+          {
+            "name": "updateProperties",
+            "type": "object",
+            "properties": {
+              "displayedFolder": {
+                "type": "object",
+                "description": "Sets the folder displayed in the tab. The extension must have an accounts permission to do this.",
+                "optional": true,
+                "properties": {
+                  "accountId": {
+                    "type": "string"
+                  },
+                  "name": {
+                    "type": "string",
+                    "optional": true
+                  },
+                  "path": {
+                    "type": "string"
+                  }
+                }
+              },
+              "sortType": {
+                "type": "string",
+                "description": "Sorts the list of messages. <var>sortOrder</var> must also be given.",
+                "optional": true,
+                "enum": [
+                  "byNone",
+                  "byDate",
+                  "bySubject",
+                  "byAuthor",
+                  "byId",
+                  "byThread",
+                  "byPriority",
+                  "byStatus",
+                  "bySize",
+                  "byFlagged",
+                  "byUnread",
+                  "byRecipient",
+                  "byLocation",
+                  "byTags",
+                  "byJunkStatus",
+                  "byAttachments",
+                  "byAccount",
+                  "byCustom",
+                  "byReceived",
+                  "byCorrespondent"
+                ]
+              },
+              "sortOrder": {
+                "type": "string",
+                "description": "Sorts the list of messages. <var>sortType</var> must also be given.",
+                "optional": true,
+                "enum": [
+                  "none",
+                  "ascending",
+                  "descending"
+                ]
+              },
+              "layout": {
+                "type": "string",
+                "description": "Sets the arrangement of the folder pane, message list pane, and message display pane. Note that setting this applies it to all mail tabs.",
+                "optional": true,
+                "enum": [
+                  "standard",
+                  "wide",
+                  "vertical"
+                ]
+              },
+              "folderPaneVisible": {
+                "type": "boolean",
+                "description": "Shows or hides the folder pane.",
+                "optional": true
+              },
+              "messagePaneVisible": {
+                "type": "boolean",
+                "description": "Shows or hides the message display pane.",
+                "optional": true
+              }
+            }
+          }
+        ]
+      },
+      {
+        "name": "getSelectedMessages",
+        "type": "function",
+        "description": "Lists the selected messages in the current folder. A messages permission is required to do this.",
+        "async": true,
+        "parameters": [
+          {
+            "name": "tabId",
+            "type": "integer",
+            "description": "Defaults to the selected tab of the current window.",
+            "optional": true,
+            "minimum": 1
+          }
+        ]
+      },
+      {
+        "name": "setQuickFilter",
+        "type": "function",
+        "description": "Sets the Quick Filter user interface based on the options specified.",
+        "async": true,
+        "parameters": [
+          {
+            "name": "tabId",
+            "type": "integer",
+            "description": "Defaults to the selected tab of the current window.",
+            "optional": true,
+            "minimum": 1
+          },
+          {
+            "name": "properties",
+            "type": "object",
+            "properties": {
+              "show": {
+                "type": "boolean",
+                "description": "Shows or hides the Quick Filter bar.",
+                "optional": true
+              },
+              "unread": {
+                "type": "boolean",
+                "description": "Shows only unread messages.",
+                "optional": true
+              },
+              "starred": {
+                "type": "boolean",
+                "description": "Shows only starred messages.",
+                "optional": true
+              },
+              "contact": {
+                "type": "boolean",
+                "description": "Shows only messages from people in the address book.",
+                "optional": true
+              },
+              "tags": {
+                "optional": true,
+                "choices": [
+                  {
+                    "type": "boolean"
+                  },
+                  {
+                    "$ref": "QuickFilterTagsDetail"
+                  }
+                ],
+                "description": "Shows only messages with tags on them."
+              },
+              "attachment": {
+                "type": "boolean",
+                "description": "Shows only messages with attachments.",
+                "optional": true
+              },
+              "text": {
+                "$ref": "QuickFilterTextDetail",
+                "description": "Shows only messages matching the supplied text.",
+                "optional": true
+              }
+            }
+          }
+        ]
+      }
+    ],
+    "events": [
+      {
+        "name": "onDisplayedFolderChanged",
+        "type": "function",
+        "description": "Fired when the displayed folder changes in any mail tab.",
+        "parameters": []
+      },
+      {
+        "name": "onSelectedMessagesChanged",
+        "type": "function",
+        "description": "Fired when the selected messages change in any mail tab.",
+        "parameters": []
+      }
+    ]
+  }
+]
--- a/mail/components/extensions/schemas/tabs.json
+++ b/mail/components/extensions/schemas/tabs.json
@@ -33,17 +33,18 @@
           "selected": {"type": "boolean", "description": "Whether the tab is selected.", "deprecated": "Please use $(ref:tabs.Tab.highlighted).", "unsupported": true},
           "highlighted": {"type": "boolean", "description": "Whether the tab is highlighted. Works as an alias of active"},
           "active": {"type": "boolean", "description": "Whether the tab is active in its window. (Does not necessarily mean the window is focused.)"},
           "url": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL the tab is displaying. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
           "title": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The title of the tab. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
           "favIconUrl": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL of the tab's favicon. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission. It may also be an empty string if the tab is loading."},
           "status": {"type": "string", "optional": true, "description": "Either <em>loading</em> or <em>complete</em>."},
           "width": {"type": "integer", "optional": true, "description": "The width of the tab in pixels."},
-          "height": {"type": "integer", "optional": true, "description": "The height of the tab in pixels."}
+          "height": {"type": "integer", "optional": true, "description": "The height of the tab in pixels."},
+          "isMail3Pane": {"type": "boolean", "optional": true, "description": "Whether the tab is a 3-pane tab."}
         }
       },
       {
         "id": "TabStatus",
         "type": "string",
         "enum": ["loading", "complete"],
         "description": "Whether the tabs have completed loading."
       },
@@ -221,16 +222,21 @@
         "type": "function",
         "description": "Gets all tabs that have the specified properties, or all tabs if no properties are specified.",
         "async": "callback",
         "parameters": [
           {
             "type": "object",
             "name": "queryInfo",
             "properties": {
+              "isMail3Pane": {
+                "type": "boolean",
+                "optional": true,
+                "description": "Whether the tab is a Thunderbird 3-pane tab."
+              },
               "active": {
                 "type": "boolean",
                 "optional": true,
                 "description": "Whether the tabs are active in their windows."
               },
               "highlighted": {
                 "type": "boolean",
                 "optional": true,
--- a/mailnews/base/util/MailServices.jsm
+++ b/mailnews/base/util/MailServices.jsm
@@ -66,8 +66,12 @@ XPCOMUtils.defineLazyServiceGetter(MailS
 
 XPCOMUtils.defineLazyServiceGetter(MailServices, "junk",
                                    "@mozilla.org/messenger/filter-plugin;1?name=bayesianfilter",
                                    "nsIJunkMailPlugin");
 
 XPCOMUtils.defineLazyServiceGetter(MailServices, "newMailNotification",
                                    "@mozilla.org/newMailNotificationService;1",
                                    "mozINewMailNotificationService");
+
+XPCOMUtils.defineLazyServiceGetter(MailServices, "folderLookup",
+                                   "@mozilla.org/mail/folder-lookup;1",
+                                   "nsIFolderLookupService");