Bug 545955 - The quick filter bar. This replaces the previous quick search widget. r+sr=bienvenu, ui-review=clarkbw
authorAndrew Sutherland <asutherland@asutherland.org>
Wed, 21 Apr 2010 21:52:20 -0700
changeset 5504 1e333d47863da0324273d7bc97e8f62ad7bf1455
parent 5503 7d1528f6c1715c4a4ccabb4254d1de3f659032e9
child 5505 c341ceea13c51506fb62bbefa749073b82d524d7
push id4257
push userbugmail@asutherland.org
push dateThu, 22 Apr 2010 04:52:41 +0000
treeherdercomm-central@1e333d47863d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs545955
Bug 545955 - The quick filter bar. This replaces the previous quick search widget. r+sr=bienvenu, ui-review=clarkbw
mail/base/content/extraCustomizeItems.xul
mail/base/content/folderDisplay.js
mail/base/content/mailTabs.js
mail/base/content/mailWindowOverlay.xul
mail/base/content/messenger.xul
mail/base/content/msgMail3PaneWindow.js
mail/base/content/quickFilterBar.css
mail/base/content/quickFilterBar.js
mail/base/content/quickFilterBar.xul
mail/base/content/search.xml
mail/base/content/searchBar.js
mail/base/content/tabmail.xml
mail/base/jar.mn
mail/base/modules/Makefile.in
mail/base/modules/dbViewWrapper.js
mail/base/modules/quickFilterManager.js
mail/base/modules/quickSearchManager.js
mail/base/modules/searchSpec.js
mail/base/test/unit/resources/viewWrapperTestUtils.js
mail/base/test/unit/test_viewWrapper_realFolder.js
mail/base/test/unit/test_viewWrapper_virtualFolder.js
mail/installer/removed-files.in
mail/locales/en-US/chrome/messenger/messenger.dtd
mail/locales/en-US/chrome/messenger/quickFilterBar.dtd
mail/locales/en-US/chrome/messenger/quickSearch.properties
mail/locales/jar.mn
mail/test/mozmill/folder-display/test-quick-search.js
mail/test/mozmill/mozmilltests.list
mail/test/mozmill/quick-filter-bar/test-display-issues.js
mail/test/mozmill/quick-filter-bar/test-filter-logic.js
mail/test/mozmill/quick-filter-bar/test-keyboard-interface.js
mail/test/mozmill/quick-filter-bar/test-sticky-filter-logic.js
mail/test/mozmill/quick-filter-bar/test-toggle-bar.js
mail/test/mozmill/shared-modules/test-folder-display-helpers.js
mail/test/mozmill/shared-modules/test-quick-filter-bar-helper.js
mail/test/mozmill/shared-modules/test-window-helpers.js
mail/themes/gnomestripe/jar.mn
mail/themes/gnomestripe/mail/icons/black_pin.png
mail/themes/gnomestripe/mail/icons/filter.png
mail/themes/gnomestripe/mail/icons/filterbar.png
mail/themes/gnomestripe/mail/icons/red_pin.png
mail/themes/gnomestripe/mail/quickFilterBar.css
mail/themes/gnomestripe/mail/searchBox.css
mail/themes/pinstripe/jar.mn
mail/themes/pinstripe/mail/icons/black_pin.png
mail/themes/pinstripe/mail/icons/filter.png
mail/themes/pinstripe/mail/icons/red_pin.png
mail/themes/pinstripe/mail/icons/tag1616.png
mail/themes/pinstripe/mail/quickFilterBar.css
mail/themes/qute/jar.mn
mail/themes/qute/mail/icons/filter.png
mail/themes/qute/mail/icons/xp-pin-grey.png
mail/themes/qute/mail/icons/xp-pin-red.png
mail/themes/qute/mail/quickFilterBar.css
mailnews/base/public/nsIMsgDBView.idl
mailnews/base/search/public/nsIMsgSearchSession.idl
mailnews/base/search/src/nsMsgFilterService.cpp
mailnews/base/search/src/nsMsgSearchSession.cpp
mailnews/base/search/src/nsMsgSearchSession.h
mailnews/base/src/nsMsgDBView.cpp
mailnews/base/src/nsMsgPurgeService.cpp
mailnews/base/src/nsMsgQuickSearchDBView.cpp
mailnews/base/src/nsMsgQuickSearchDBView.h
mailnews/base/src/nsMsgSearchDBView.cpp
mailnews/base/src/nsMsgSearchDBView.h
mailnews/base/src/nsMsgSpecialViews.cpp
mailnews/base/src/nsMsgSpecialViews.h
mailnews/base/util/errUtils.js
mailnews/db/gloda/components/glautocomp.js
mailnews/news/src/nsNewsDownloader.cpp
--- a/mail/base/content/extraCustomizeItems.xul
+++ b/mail/base/content/extraCustomizeItems.xul
@@ -34,16 +34,18 @@
  - use your version of this file under the terms of the MPL, indicate your
  - and other provisions required by the GPL or the LGPL. If you do not delete
  - the provisions above, a recipient may use your version of this file under
  - the terms of any one of the MPL, the GPL or the LGPL.
  -
  - ***** END LICENSE BLOCK *****
  -->
 
+<?xml-stylesheet href="chrome://global/skin/textbox.css" type="text/css"?>
+
 <!DOCTYPE overlay [
   <!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd">
   %messengerDTD;
   <!ENTITY % msgViewPickerDTD SYSTEM "chrome://messenger/locale/msgViewPickerOverlay.dtd" >
   %msgViewPickerDTD;
   <!ENTITY % msgFolderPickerDTD SYSTEM "chrome://messenger/locale/msgFolderPickerOverlay.dtd" >
   %msgFolderPickerDTD;
   <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
@@ -61,22 +63,39 @@
     <!-- gloda search widget; provides global (message) searching.  -->
     <toolbaritem id="gloda-search" insertafter="button-stop"
                  title="&glodaSearch.title;"
                  align="center"
                  flex="1"
                  class="chromeclass-toolbar-additional">
         <textbox id="searchInput" 
                  flex="1"
-                 searchCriteria="true"
                  type="glodacomplete"
                  searchbutton="true"
                  autocompletesearch="gloda"
                  autocompletepopup="PopupGlodaAutocomplete"
+                 autocompletesearchparam="global"
+                 enablehistory="false"
+                 timeout="200"
+                 emptytext=""
+                 emptytextbase="&searchAllMessages.label.base;"
+                 keyLabelNonMac="&searchAllMessages.keyLabel.nonmac;"
+                 keyLabelMac="&searchAllMessages.keyLabel.mac;"
                  >
+          <!-- Mimic the search/clear buttons of the standard search textbox,
+               but adjust for the reality that clear doesn't make much sense
+               since gloda results only show in a tab and the idiom for closing
+               tabs is closing the tab.  Our binding does process escape to
+               clear the box, if people want to clear it that way...
+               -->
+          <hbox>
+            <image class="textbox-search-icon"
+                   onclick="document.getElementById('searchInput').doSearch();"
+                   />
+          </hbox>
         </textbox>
     </toolbaritem>
 
     <toolbarbutton id="button-compact" class="toolbarbutton-1"
                    insertafter="button-mark"
                    label="&compactButton.label;"
                    tooltiptext="&compactButton.tooltip;"
                    oncommand="goDoCommand('button_compact');"
--- a/mail/base/content/folderDisplay.js
+++ b/mail/base/content/folderDisplay.js
@@ -41,16 +41,79 @@ Components.utils.import("resource:///mod
 Components.utils.import("resource:///modules/jsTreeSelection.js");
 
 var gFolderDisplay = null;
 var gMessageDisplay = null;
 
 var nsMsgFolderFlags = Components.interfaces.nsMsgFolderFlags;
 
 /**
+ * Maintains a list of listeners for all FolderDisplayWidget instances in this
+ *  window.  The assumption is that because of our multiplexed tab
+ *  implementation all consumers are effectively going to care about all such
+ *  tabs.
+ *
+ * We are not just a global list so that we can add brains about efficiently
+ *  building lists, provide try-wrapper convenience, etc.
+ */
+let FolderDisplayListenerManager = {
+  _listeners: [],
+
+  /**
+   * Register a listener that implements one or more of the methods defined on
+   *  |IDBViewWrapperListener|.  Note that a change from those interface
+   *  signatures is that the first argument is always a reference to the
+   *  FolderDisplayWidget generating the notification.
+   *
+   * We additionally support the following notifications:
+   * - onMakeActive.  Invoked when makeActive is called on the
+   *   FolderDisplayWidget.  The second argument (after the folder display) is
+   *   aWasInactive.
+   *
+   * - onActiveCreatedView.  onCreatedView deferred to when the tab is actually
+   *   made active.
+   *
+   * - onActiveAllMessagesLoaded.  onAllMessagesLoaded deferred to when the
+   *   tab is actually made active.  Use this if the actions you need to take
+   *   are based on the folder display actually being visible, such as updating
+   *   some UI widget, etc.
+   *
+   */
+  registerListener: function FDLM_registerListener(aListener) {
+    this._listeners.push(aListener);
+  },
+
+  /**
+   * Unregister a previously registered event listener.
+   */
+  unregisterListener: function FDLM_unregisterListener(aListener) {
+    let idx = this._listeners.indexOf(idx);
+    if (idx >= 0) {
+      this._listeners.splice(idx, 1);
+    }
+  },
+
+  /**
+   * For use by FolderDisplayWidget to trigger listener invocation.
+   */
+  _fireListeners: function FDBLM__fireListeners(aEventName, aArgs) {
+    for each (let [, listener] in Iterator(this._listeners)) {
+      if (aEventName in listener) {
+        try {
+          listener[aEventName].apply(listener, aArgs);
+        }
+        catch(ex) {
+          Components.utils.reportError(ex);
+        }
+      }
+    }
+  },
+};
+
+/**
  * Abstraction for a widget that (roughly speaking) displays the contents of
  *  folders.  The widget belongs to a tab and has a lifetime as long as the tab
  *  that contains it.  This class is strictly concerned with the UI aspects of
  *  this; the DBViewWrapper class handles the view details (and is exposed on
  *  the 'view' attribute.)
  *
  * The search window subclasses this into the SearchFolderDisplayWidget rather
  *  than us attempting to generalize everything excessively.  This is because
@@ -774,58 +837,71 @@ FolderDisplayWidget.prototype = {
    *  nice to the future. Loading a folder is a blocking operation that is going
    *  to make us unresponsive and accordingly make it very hard for the user to
    *  change tabs.
    */
   onFolderLoading: function(aFolderLoading) {
     if (this._tabInfo)
       document.getElementById("tabmail").setTabBusy(this._tabInfo,
                                                     aFolderLoading);
+
+    FolderDisplayListenerManager._fireListeners("onFolderLoading",
+                                                [this, aFolderLoading]);
   },
 
   /**
    * The view wrapper tells us when a search is active, and we mark the tab as
    *  thinking so the user knows something is happening.  'Searching' in this
    *  case is more than just a user-initiated search.  Virtual folders / saved
    *  searches, mail views, plus the more obvious quick search are all based off
    *  of searches and we will receive a notification for them.
    */
   onSearching: function(aIsSearching) {
     if (this._tabInfo) {
       let searchBundle = document.getElementById("bundle_search");
       document.getElementById("tabmail").setTabThinking(
         this._tabInfo,
         aIsSearching && searchBundle.getString("searchingMessage"));
     }
+
+    FolderDisplayListenerManager._fireListeners("onSearching",
+                                                [this, aIsSearching]);
   },
 
   /**
    * Things we do on creating a view:
    * - notify the observer service so that custom column handler providers can
    *   add their custom columns to our view.
    */
   onCreatedView: function FolderDisplayWidget_onCreatedView() {
     // All of our messages are not displayed if the view was just created.  We
     //  will get an onAllMessagesLoaded nearly immediately if this is a local
     //  folder where view creation is synonymous with having all messages.
     this._allMessagesLoaded = false;
     this.messageDisplay.onCreatedView();
+
+    FolderDisplayListenerManager._fireListeners("onCreatedView",
+                                                [this]);
+
     this._notifyWhenActive(this._activeCreatedView);
   },
   _activeCreatedView: function() {
     gDBView = this.view.dbView;
 
     // A change in view may result in changes to sorts, the view menu, etc.
     // Do this before we 'reroot' the dbview.
     this._updateThreadDisplay();
 
     // this creates a new selection object for the view.
     if (this.treeBox)
       this.treeBox.view = this.view.dbView;
 
+    FolderDisplayListenerManager._fireListeners("onActiveCreatedView",
+                                                [this]);
+
     let ObserverService =
       Components.classes["@mozilla.org/observer-service;1"]
                 .getService(Components.interfaces.nsIObserverService);
     // The data payload used to be viewType + ":" + viewFlags.  We no longer
     //  do this because we already have the implied contract that gDBView is
     //  valid at the time we generate the notification.  In such a case, you
     //  can easily get that information from the gDBView.  (The documentation
     //  on creating a custom column assumes gDBView.)
@@ -845,16 +921,19 @@ FolderDisplayWidget.prototype = {
       // any selections
       if (aFolderIsComingBack && !this._aboutToSelectMessage)
         this._saveSelection();
       else
         this._clearSavedSelection();
       gDBView = null;
     }
 
+    FolderDisplayListenerManager._fireListeners("onDestroyingView",
+                                                [this, aFolderIsComingBack]);
+
     // if we have no view, no messages could be loaded.
     this._allMessagesLoaded = false;
 
     // but the actual tree view selection (based on view indicies) is a goner no
     //  matter what, make everyone forget.
     this.view.dbView.selection = null;
     this._savedFirstVisibleRow = null;
     this._nextViewIndexAfterDelete = null;
@@ -874,16 +953,19 @@ FolderDisplayWidget.prototype = {
    * Restore persisted information about what columns to display for the folder.
    *  If we have no persisted information, we leave/set _savedColumnStates null.
    *  The column states will be set to default values in onDisplayingFolder in
    *  that case.
    */
   onLoadingFolder: function FolderDisplayWidget_onLoadingFolder(aDbFolderInfo) {
     this._savedColumnStates =
       this._depersistColumnStatesFromDbFolderInfo(aDbFolderInfo);
+
+    FolderDisplayListenerManager._fireListeners("onLoadingFolder",
+                                                [this, aDbFolderInfo]);
   },
 
   /**
    * We are entering the folder for display:
    * - set the header cache size.
    * - Setup the columns if we did not already depersist in |onLoadingFolder|.
    */
   onDisplayingFolder: function FolderDisplayWidget_onDisplayingFolder() {
@@ -895,31 +977,32 @@ FolderDisplayWidget.prototype = {
     // makeActive will restore the folder state
     if (!this._savedColumnStates) {
       // get the default for this folder
       this._savedColumnStates = this._getDefaultColumnsForCurrentFolder();
       // and save it so it doesn't wiggle if the inbox/prototype changes
       this._persistColumnStates(this._savedColumnStates);
     }
 
-    // update the quick-search relative to whether it's incoming/outgoing
-    let searchInput = document.getElementById("searchInput");
-    if (searchInput)
-      searchInput.folderChanged(this.view.isOutgoingFolder)
+    FolderDisplayListenerManager._fireListeners("onDisplayingFolder",
+                                                [this]);
 
     if (this.active)
       this.makeActive();
   },
 
   /**
    * Notification from DBViewWrapper that it is closing the folder.  This can
    *  happen for reasons other than our own 'close' method closing the view.
    *  For example, user deletion of the folder or underlying folder closes it.
    */
   onLeavingFolder: function FolderDisplayWidget_onLeavingFolder() {
+    FolderDisplayListenerManager._fireListeners("onLeavingFolder",
+                                                [this]);
+
     // Keep the msgWindow's openFolder up-to-date; it powers nsMessenger's
     //  concept of history so that it can bring you back to the actual folder
     //  you were looking at, rather than just the underlying folder.
     if (this._active)
       msgWindow.openFolder = null;
   },
 
   /**
@@ -937,20 +1020,27 @@ FolderDisplayWidget.prototype = {
    *  shown up.  For a real folder, this happens when the folder is entered.
    *  For a virtual folder, this happens when the search completes.
    *
    * What we do:
    * - Any scrolling required!
    */
   onAllMessagesLoaded: function FolderDisplayWidget_onAllMessagesLoaded() {
     this._allMessagesLoaded = true;
+
+    FolderDisplayListenerManager._fireListeners("onAllMessagesLoaded",
+                                                [this]);
+
     this._notifyWhenActive(this._activeAllMessagesLoaded);
   },
   _activeAllMessagesLoaded:
       function FolderDisplayWidget__activeAllMessagesLoaded() {
+    FolderDisplayListenerManager._fireListeners("onActiveAllMessagesLoaded",
+                                                [this]);
+
     // - if a selectMessage's coming up, get out of here
     if (this._aboutToSelectMessage)
       return;
 
     // - restore selection
     // Attempt to restore the selection (if we saved it because the view was
     //  being destroyed or otherwise manipulated in a fashion that the normal
     //  nsTreeSelection would be unable to handle.)
@@ -1033,25 +1123,31 @@ FolderDisplayWidget.prototype = {
   /**
    * Just the sort or threading was changed, without changing other things.  We
    *  will not get this notification if the view was re-created, for example.
    */
   onSortChanged: function FolderDisplayWidget_onSortChanged() {
     if (this.active)
       UpdateSortIndicators(this.view.primarySortType,
                            this.view.primarySortOrder);
+
+    FolderDisplayListenerManager._fireListeners("onSortChanged",
+                                                [this]);
   },
 
   /**
    * Messages (that may have been displayed) have been removed; this may impact
    * our message selection. We might know it's coming; if we do then
    * this._nextViewIndexAfterDelete should know what view index to select next.
    * For the imap mark-as-deleted we won't know beforehand.
    */
   onMessagesRemoved: function FolderDisplayWidget_onMessagesRemoved() {
+    FolderDisplayListenerManager._fireListeners("onMessagesRemoved",
+                                                [this]);
+
     // - we saw this coming
     let rowCount = this.view.dbView.rowCount;
     if (!this._massMoveActive && (this._nextViewIndexAfterDelete != null)) {
       // adjust the index if it is after the last row...
       // (this can happen if the "mail.delete_matches_sort_order" pref is not
       //  set and the message is the last message in the view.)
       if (this._nextViewIndexAfterDelete >= rowCount)
         this._nextViewIndexAfterDelete = rowCount - 1;
@@ -1103,24 +1199,28 @@ FolderDisplayWidget.prototype = {
   /**
    * Messages were not actually removed, but we were expecting that they would
    *  be.  Clean-up what onMessagesRemoved would have cleaned up, namely the
    *  next view index to select.
    */
   onMessageRemovalFailed:
       function FolderDisplayWidget_onMessageRemovalFailed() {
     this._nextViewIndexAfterDelete = null;
+    FolderDisplayListenerManager._fireListeners("onMessagesRemovalFailed",
+                                                [this]);
   },
 
   /**
    * Update the status bar to reflect our exciting message counts.
    */
   onMessageCountsChanged: function FolderDisplayWidget_onMessageCountsChaned() {
     if (this.active)
       UpdateStatusMessageCounts(this.displayedFolder);
+    FolderDisplayListenerManager._fireListeners("onMessageCountsChanged",
+                                                [this]);
   },
   //@}
   /* ===== End IDBViewWrapperListener ===== */
 
   /*   ==================================   */
   /* ===== nsIMsgDBViewCommandUpdater ===== */
   /*   ==================================   */
 
@@ -1450,16 +1550,19 @@ FolderDisplayWidget.prototype = {
     this._active = true;
     this._runNotificationsPendingActivation();
 
     // Make sure we get rid of this._fakeTreeSelection, whether we use it below
     // or not.
     let fakeTreeSelection = this._fakeTreeSelection;
     this._fakeTreeSelection = null;
 
+    FolderDisplayListenerManager._fireListeners("onMakeActive",
+                                                [this, aWasInactive]);
+
     // -- UI
 
     // We're going to set this to true if we've already caused a
     // selectionChanged event, so that the message display doesn't cause
     // another, or if a select message is coming up shortly.
     let dontReloadMessage = this._aboutToSelectMessage;
     // thread pane if we have a db view
     if (this.view.dbView) {
--- a/mail/base/content/mailTabs.js
+++ b/mail/base/content/mailTabs.js
@@ -115,59 +115,40 @@ let mailTabType = {
         let windowToInheritFrom = null;
         if (window.opener &&
             (window.opener.document.documentElement.getAttribute("windowtype") ==
              "mail:3pane"))
           windowToInheritFrom = window.opener;
         else
           windowToInheritFrom = FindOther3PaneWindow();
 
-        if (windowToInheritFrom) {
-          let searchInputToInheritFrom =
-            windowToInheritFrom.document.getElementById("searchInput");
-          if (searchInputToInheritFrom) {
-            let searchInput = document.getElementById("searchInput");
-            if (searchInput)
-              // This should set the aTab.searchMode as well
-              searchInput.searchMode = searchInputToInheritFrom.searchMode;
-          }
-        }
         aTab.folderDisplay.makeActive();
       },
       /**
        * @param aArgs.folder The nsIMsgFolder to display.
        * @param [aArgs.msgHdr] Optional message header to display.
        * @param [aArgs.folderPaneVisible] Whether the folder pane should be
        *            visible. If this isn't specified, the current or first tab's
        *            current state is used.
        * @param [aArgs.messagePaneVisible] Whether the message pane should be
        *            visible. If this isn't specified, the current or first tab's
        *            current state is used.
-       * @param [aArgs.searchMode] The search mode for this tab. If this isn't
-       *            specified, the current or first tab's current mode is used.
        * @param [aArgs.forceSelectMessage] Whether we should consider dropping
        *            filters to select the message. This has no effect if
        *            aArgs.msgHdr isn't specified. Defaults to false.
        */
       openTab: function(aTab, aArgs) {
         // persistence and restoreTab wants to know if we are the magic first tab
         aTab.firstTab = false;
 
         // Get a tab that we can initialize our user preferences from.
         // (We don't want to assume that our immediate predecessor was a
         //  "folder" tab.)
         let modelTab = document.getElementById("tabmail")
                          .getTabInfoForCurrentOrFirstModeInstance(aTab.mode);
-        let searchInput = document.getElementById("searchInput");
-
-        if ("searchMode" in aArgs)
-          aTab.searchMode = aArgs.searchMode;
-        else if (searchInput)
-          aTab.searchMode = searchInput.searchMode;
-        aTab.searchInputValue = "";
 
         // - figure out whether to show the folder pane
         let folderPaneShouldBeVisible;
         // explicitly told to us?
         if ("folderPaneVisible" in aArgs)
           folderPaneShouldBeVisible = aArgs.folderPaneVisible;
         // inherit from the previous tab (if we've got one)
         else if (modelTab)
@@ -233,18 +214,16 @@ let mailTabType = {
           let retval = {
             folderURI: aTab.folderDisplay.displayedFolder.URI,
             // if the folder pane is active, then we need to look at
             // whether the box is collapsed
             folderPaneVisible: aTab.folderDisplay.folderPaneVisible,
             messagePaneVisible: aTab.messageDisplay.visible,
             firstTab: aTab.firstTab
           };
-          if ("searchMode" in aTab)
-            retval.searchMode = aTab.searchMode;
           return retval;
         } catch (e) {
           logException(e);
           return null;
         }
       },
       restoreTab: function(aTabmail, aPersistedState) {
       try {
@@ -277,33 +256,40 @@ let mailTabType = {
               if (!gMessageDisplay._active)
                 gMessageDisplay._visible = aPersistedState.messagePaneVisible;
             }
 
             if (!("dontRestoreFirstTab" in aPersistedState &&
                   aPersistedState.dontRestoreFirstTab))
               gFolderTreeView.selectFolder(folder);
 
-            // This should be after selectFolder, so that onDisplayingFolder
-            // there doesn't clobber this.
-            if ("searchMode" in aPersistedState) {
-              let searchInput = document.getElementById("searchInput");
-              if (searchInput)
-                searchInput.searchMode = aPersistedState.searchMode;
+            // We need to manually trigger the tab monitor restore trigger
+            // for this tab.  In theory this should be in tabmail, but the
+            // special nature of the first tab will last exactly long as this
+            // implementation right here so it does not particularly matter
+            // and is a bit more honest, if ugly, to do it here.
+            let tabmail = document.getElementById("tabmail");
+            let restoreState = tabmail._restoringTabState;
+            let tab = tabmail.tabInfo[0];
+            for each (let [, tabMonitor] in Iterator(tabmail.tabMonitors)) {
+              if (("onTabRestored" in tabMonitor) &&
+                  (tabMonitor.monitorName in restoreState.ext)) {
+                tabMonitor.onTabRestored(tab,
+                                         restoreState.ext[tabMonitor.monitorName],
+                                         true);
+              }
             }
           }
           else {
             let tabArgs = {
               folder: folder,
               folderPaneVisible: folderPaneVisible,
               messagePaneVisible: aPersistedState.messagePaneVisible,
               background: true
             };
-            if ("searchMode" in aPersistedState)
-              tabArgs.searchMode = aPersistedState.searchMode;
             aTabmail.openTab("folder", tabArgs);
           }
         }
       } catch (e) {
         logException(e);
       }
       },
       onTitleChanged: function(aTab, aTabNode) {
--- a/mail/base/content/mailWindowOverlay.xul
+++ b/mail/base/content/mailWindowOverlay.xul
@@ -1047,17 +1047,17 @@
                       label="&selectFlaggedCmd.label;"
                       accesskey="&selectFlaggedCmd.accesskey;"
                       command="cmd_selectFlagged"/>
           </menupopup>
         </menu>
         <menuseparator id="editMenuAfterSelectSeparator"/>
         <menu id="menu_find" label="&findMenu.label;" accesskey="&findMenu.accesskey;">
           <menupopup id="menu_FindPopup">
-            <menuitem id="menu_findCmd" label="&findCmd.label;" key="key_find" accesskey="&findCmd.accesskey;" command="cmd_find"/>
+            <menuitem id="menu_findCmd" label="&findCmd.label;" key="key_findAgain" accesskey="&findCmd.accesskey;" command="cmd_find"/>
             <menuitem id="menu_findAgainCmd" label="&findAgainCmd.label;" key="key_findAgain" accesskey="&findAgainCmd.accesskey;" command="cmd_findAgain"/>
             <menuseparator id="editMenuAfterFindSeparator"/>
             <menuitem id="searchMailCmd" label="&searchMailCmd.label;"
                       key="key_searchMail"
                       accesskey="&searchMailCmd.accesskey;"
                       command="cmd_search"/>
             <menuitem id="searchAddressesCmd" label="&searchAddressesCmd.label;"
                       accesskey="&searchAddressesCmd.accesskey;"
--- a/mail/base/content/messenger.xul
+++ b/mail/base/content/messenger.xul
@@ -45,16 +45,17 @@
 
 <?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
 <?xul-overlay href="chrome://messenger/content/msgHdrViewOverlay.xul"?>
 <?xul-overlay href="chrome://messenger/content/mailWindowOverlay.xul"?>
 <?xul-overlay href="chrome://messenger/content/extraCustomizeItems.xul"?>
 <?xul-overlay href="chrome://messenger/content/mailOverlay.xul"?>
 <?xul-overlay href="chrome://messenger/content/editContactOverlay.xul"?>
 <?xul-overlay href="chrome://messenger/content/specialTabs.xul"?>
+<?xul-overlay href="chrome://messenger/content/quickFilterBar.xul"?>
 
 <!DOCTYPE window [
 <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
 %brandDTD;
 <!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd" >
 %messengerDTD;
 <!ENTITY % customizeToolbarDTD SYSTEM "chrome://global/locale/customizeToolbar.dtd">
 %customizeToolbarDTD;
@@ -236,17 +237,22 @@
 
   <toolbox id="mail-toolbox" class="toolbox-top">
   </toolbox>
   <!-- XXX This extension point (tabmail-container) is only temporary!
        Horizontal space shouldn't be wasted if it isn't absolutely critical.
        A mechanism for adding sidebar panes will be added in bug 476154. -->
   <hbox id="tabmail-container" flex="1">
     <tabmail id="tabmail" flex="1" panelcontainer="tabpanelcontainer">
-      <hbox id="tabmail-buttons"/>
+      <hbox>
+        <!-- extensions can put stuff in here -->
+        <hbox id="tabmail-buttons"/>
+        <!-- extensions must not put stuff in here -->
+        <hbox id="thunderbird-private-tabmail-buttons"/>
+      </hbox>
       <tabpanels id="tabpanelcontainer" flex="1" class="plain" selectedIndex="0">
         <!-- mailContent is the container used for the "wide" layout. Normally,
              all it contains is the "messengerBox" box.  However, in "wide" mode
              the message pane and its splitter transplant themselves into the box
              (respectively, messagepanebox and threadpane-splitter).  This gives us
              the folder pane next to the thread view, with the message pane/reader
              beneath both of them. -->
         <box id="mailContent" orient="vertical" flex="1">
--- a/mail/base/content/msgMail3PaneWindow.js
+++ b/mail/base/content/msgMail3PaneWindow.js
@@ -38,22 +38,22 @@
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
+Components.utils.import("resource://gre/modules/folderUtils.jsm");
 Components.utils.import("resource:///modules/activity/activityModules.js");
-Components.utils.import("resource:///modules/errUtils.js");
-Components.utils.import("resource:///modules/folderUtils.jsm");
-Components.utils.import("resource:///modules/IOUtils.js");
 Components.utils.import("resource:///modules/jsTreeSelection.js");
 Components.utils.import("resource:///modules/MailConsts.js");
+Components.utils.import("resource:///modules/errUtils.js");
+Components.utils.import("resource:///modules/IOUtils.js");
 Components.utils.import("resource:///modules/mailnewsMigrator.js");
 Components.utils.import("resource:///modules/sessionStoreManager.js");
 
 /* This is where functions related to the 3 pane window are kept */
 
 // from MailNewsTypes.h
 const nsMsgKey_None = 0xFFFFFFFF;
 const nsMsgViewIndex_None = 0xFFFFFFFF;
@@ -314,17 +314,17 @@ function OnLoadMessenger()
   //  specialTabs.openSpecialTabsOnStartup below.
   let tabmail = document.getElementById('tabmail');
   if (tabmail)
   {
     // mailTabType is defined in mailWindowOverlay.js
     tabmail.registerTabType(mailTabType);
     // glodaFacetTab* in glodaFacetTab.js
     tabmail.registerTabType(glodaFacetTabType);
-    tabmail.registerTabMonitor(QuickSearchTabMonitor);
+    tabmail.registerTabMonitor(GlodaSearchBoxTabMonitor);
     tabmail.registerTabMonitor(statusMessageCountsMonitor);
     tabmail.openFirstTab();
   }
 
   // verifyAccounts returns true if the callback won't be called
   // We also don't want the account wizard to open if any sort of account exists
   if (verifyAccounts(LoadPostAccountWizard, false, AutoConfigWizard))
     LoadPostAccountWizard();
@@ -876,17 +876,21 @@ function ClearMessagePane()
 {
   // hide the message header view AND the message pane...
   HideMessageHeaderPane();
   gMessageNotificationBar.clearMsgNotifications();
   ClearPendingReadTimer();
   try {
     // This can fail because cloning imap URI's can fail if the username
     // has been cleared by docshell/base/nsDefaultURIFixup.cpp.
-    GetMessagePaneFrame().location.href = "about:blank";
+    let messagePane = GetMessagePaneFrame();
+    // If we don't do this check, no one else does and we do a non-trivial
+    // amount of work.  So do the check.
+    if (messagePane.location.href != "about:blank")
+      messagePane.location.href = "about:blank";
   } catch(ex) {
       logException(ex, false, "error clearing message pane");
   }
 }
 
 /**
  * When right-clicks happen, we do not want to corrupt the underlying
  * selection.  The right-click is a transient selection.  So, unless the
new file mode 100644
--- /dev/null
+++ b/mail/base/content/quickFilterBar.css
@@ -0,0 +1,20 @@
+#qfb-results-label {
+  text-align: end;
+  visibility: hidden;
+}
+
+#qfb-filter-bar-spacer {
+  min-width: 4px !important;
+}
+
+.qfb-tag-button[inverted] {
+  text-decoration: line-through;
+}
+
+#quick-filter-bar-main-bar {
+  overflow: hidden;
+}
+
+#quick-filter-bar-collapsible-buttons[shrink="true"] toolbarbutton label.toolbarbutton-text {
+  display: none;
+}
new file mode 100644
--- /dev/null
+++ b/mail/base/content/quickFilterBar.js
@@ -0,0 +1,626 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Mail Client.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Andrew Sutherland <asutherland@asutherland.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+Components.utils.import("resource:///modules/iteratorUtils.jsm");
+Components.utils.import("resource:///modules/errUtils.js");
+
+Components.utils.import("resource:///modules/searchSpec.js");
+
+Components.utils.import("resource:///modules/quickFilterManager.js");
+
+
+////////////////////////////////////////////////////////////////////////////////
+//// Proper Code
+
+/**
+ * There is only one message filter bar widget; the muxer deals with tab
+ *  changes and directing modifications to and reflecting the state of the
+ *  actual filterer objects.
+ */
+let QuickFilterBarMuxer = {
+  _init: function QFBM__init() {
+    // -- folder display hookup
+    FolderDisplayListenerManager.registerListener(this);
+
+    // -- tab monitor hookup
+    this.tabmail = document.getElementById("tabmail");
+    this.tabmail.registerTabMonitor(this);
+    // We may be registering after the first tab was opened, in which case
+    //  we should generate a synthetic notification to ourselves.
+    if (this.tabmail.currentTabInfo)
+      this.onTabOpened(this.tabmail.currentTabInfo, true, null);
+
+    // -- window hookups
+    let dis = this;
+    window.addEventListener("resize", function() {
+                              dis.onWindowResize();
+                            }, false);
+
+    this._bindUI();
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  // FolderDisplayListener
+
+  /**
+   * Decide whether to display the filter bar toggle button whenever a folder
+   *  display is made active.  makeActive is what triggers the display of
+   *  account central so this is the perfect spot to do so.
+   */
+  onMakeActive: function QFBM_onMakeActive(aFolderDisplay) {
+    let tab = aFolderDisplay._tabInfo;
+    let appropriate = ("quickFilter" in tab._ext) &&
+                        aFolderDisplay.displayedFolder &&
+                        !aFolderDisplay.displayedFolder.isServer;
+    document.getElementById("qfb-show-filter-bar").style.visibility =
+      (appropriate ? "visible" : "hidden");
+  },
+
+  /**
+   * Clear out our state when notified the user has changed folders and re-apply
+   *  search constraints if we are in sticky mode.  It is important that we
+   *  re-apply search constraints here in onLoadingFolder as this is the only
+   *  notification we receive where we have a chance to avoid creating a view
+   *  just to nuke it and re-create it with our new search constraints shortly
+   *  afterwards.
+   */
+  onLoadingFolder: function QFBM_onFolderChanged(aFolderDisplay,
+                                                 aIsOutbound) {
+    let tab = aFolderDisplay._tabInfo;
+    let filterer = ("quickFilter" in tab._ext) ? tab._ext.quickFilter : null;
+    if (!filterer)
+      return;
+
+    // check if there actually was a change (notification might not be for us)
+    if (tab.folderDisplay.displayedFolder != filterer.displayedFolder) {
+      // perform state propagation to a new filter state
+      tab._ext.quickFilter = filterer = new QuickFilterState(filterer);
+      this.updateSearch();
+      this.reflectFiltererState(filterer, tab.folderDisplay);
+    }
+  },
+
+  /**
+   * Once the view is fully populated:
+   * - Invoke postFilterProcess on all filter definitions that expose such a
+   *   method.  If they return a value, cram it in their state and (assuming
+   *   this is the current tab), call their reflectInDOM method so they can
+   *   update their state.
+   * - Update UI to reflect some/no matches.
+   */
+  onActiveAllMessagesLoaded:
+      function QFBM_onFolderDisplayAllMessagesLoaded(aFolderDisplay) {
+    let filterer = this.maybeActiveFilterer;
+    if (!filterer)
+      return;
+
+    let filtering = aFolderDisplay.view.search.userTerms != null;
+
+    // - postFilterProcess everyone who cares
+    // This may need to be converted into an asynchronous process at some point.
+    for each (let [, filterDef] in Iterator(QuickFilterManager.filterDefs)) {
+      if ("postFilterProcess" in filterDef) {
+        let preState = (filterDef.name in filterer.filterValues) ?
+          filterer.filterValues[filterDef.name] : null;
+        let [newState, update, treatAsUserAction] =
+          filterDef.postFilterProcess(preState, aFolderDisplay.view, filtering);
+        filterer.setFilterValue(filterDef.name, newState, !treatAsUserAction);
+        if (update) {
+          if (aFolderDisplay._tabInfo == this.tabmail.currentTabInfo &&
+              ("reflectInDOM" in filterDef)) {
+            let domNode = document.getElementById(filterDef.domId);
+            // We are passing update as a super-secret data propagation channel
+            //  exclusively for one-off cases like the text filter gloda upsell.
+            filterDef.reflectInDOM(domNode, newState, document, this, update);
+          }
+        }
+      }
+    }
+
+    // - Update match status.
+    this.reflectFiltererResults(filterer, aFolderDisplay);
+  },
+
+  /**
+   * If we're searching, update the filter results.  (If we stop searching,
+   *  we're going to end up in the onFolderDisplayAllMessagesLoaded
+   *  notification.  Mayhaps we should lose that vector and just use this one.)
+   */
+  onSearching: function QFBM_onSearching(
+      aFolderDisplay, aIsSearching) {
+    // we only care if we just started searching and we are active
+    if (!aIsSearching || !aFolderDisplay.active)
+      return;
+
+    // - Update match status.
+    this.reflectFiltererResults(this.activeFilterer, aFolderDisplay);
+  },
+
+
+  //////////////////////////////////////////////////////////////////////////////
+  // UI State Manipulation
+
+  /**
+   * Add appropriate event handlers to the DOM elements.  We do this rather
+   *  than requiring lots of boilerplate "oncommand" junk on the nodes.
+   *
+   * We hook up the following:
+   * - "command" event listener.
+   * - reflect filter state
+   */
+  _bindUI: function QFBM__bindUI() {
+    for each (let [, filterDef] in Iterator(QuickFilterManager.filterDefs)) {
+      let domNode = document.getElementById(filterDef.domId);
+      // the loop let binding does not latch, at least in 1.9.2
+      let latchedFilterDef = filterDef;
+
+      let handler;
+      if (!("onCommand" in filterDef)) {
+        handler = function(aEvent) {
+          try {
+            let postValue = domNode.checked ? true : null;
+            QuickFilterBarMuxer.activeFilterer.setFilterValue(
+              latchedFilterDef.name, postValue);
+            QuickFilterBarMuxer.deferredUpdateSearch();
+          }
+          catch (ex) {
+            logException(ex);
+          }
+        };
+      }
+      else {
+        handler = function(aEvent) {
+          let filterValues = QuickFilterBarMuxer.activeFilterer.filterValues;
+          let preValue = (latchedFilterDef.name in filterValues) ?
+                           filterValues[latchedFilterDef.name] : null;
+          let [postValue, update] =
+            latchedFilterDef.onCommand(preValue, domNode, aEvent, document);
+          QuickFilterBarMuxer.activeFilterer.setFilterValue(
+            latchedFilterDef.name, postValue, !update);
+          if (update)
+            QuickFilterBarMuxer.deferredUpdateSearch();
+        };
+      }
+      domNode.addEventListener("command", handler, false);
+
+      if ("domBindExtra" in filterDef)
+        filterDef.domBindExtra(document, this, domNode);
+    }
+  },
+
+  /**
+   * Update the UI to reflect the state of the filterer constraints.
+   *
+   * @param aFilterer The active filterer.
+   * @param aFolderDisplay The active FolderDisplayWidget.
+   * @param [aFilterName] If only a single filter needs to be updated, name it.
+   */
+  reflectFiltererState: function QFBM_reflectFiltererState(aFilterer,
+                                                           aFolderDisplay,
+                                                           aFilterName) {
+    // If we aren't visible then there is no need to update the widgets.
+    if (aFilterer.visible) {
+      let filterValues = aFilterer.filterValues;
+      for each (let [, filterDef] in
+                Iterator(QuickFilterManager.filterDefs)) {
+        // If we only need to update one state, check and skip as appropriate.
+        if (aFilterName && filterDef.name != aFilterName)
+          continue;
+
+        let domNode = document.getElementById(filterDef.domId);
+        let value = (filterDef.name in filterValues) ?
+          filterValues[filterDef.name] : null;
+        if (!("reflectInDOM" in filterDef))
+          domNode.checked = Boolean(value);
+        else
+          filterDef.reflectInDOM(domNode, value, document, this);
+      }
+    }
+
+    this.reflectFiltererResults(aFilterer, aFolderDisplay);
+
+    document.getElementById("quick-filter-bar").collapsed =
+      !aFilterer.visible;
+    document.getElementById("qfb-show-filter-bar").checked = aFilterer.visible;
+  },
+
+  /**
+   * Update the UI to reflect the state of the folderDisplay in terms of
+   *  filtering.  This is expected to be called by |reflectFiltererState| and
+   *  when something happens event-wise in terms of search.
+   *
+   * We can have one of four states:
+   * - No filter is active; no attributes exposed for CSS to do anything.
+   * - A filter is active and we are still searching; filterActive=searching.
+   * - A filter is active, completed searching, and we have results;
+   *   filterActive=matches.
+   * - A filter is active, completed searching, and we have no results;
+   *   filterActive=nomatches.
+   */
+  reflectFiltererResults: function QFBM_reflectFiltererResults(aFilterer,
+                                                               aFolderDisplay) {
+    let view = aFolderDisplay.view;
+    let threadPane = document.getElementById("threadTree");
+    let qfb = document.getElementById("quick-filter-bar");
+
+    // bail early if the view is in the process of being created
+    if (!view.dbView)
+      return;
+
+    // no filter active
+    if (!view.search || !view.search.userTerms) {
+      threadPane.removeAttribute("filterActive");
+      qfb.removeAttribute("filterActive");
+    }
+    // filter active, still searching
+    else if (view.searching) {
+      // Do not set this immediately; wait a bit and then only set this if we
+      //  still are in this same state (and we are still the active tab...)
+      setTimeout(function() {
+        if (!view.searching ||
+            (QuickFilterBarMuxer.maybeActiveFilterer != aFilterer))
+          return;
+        threadPane.setAttribute("filterActive", "searching");
+        qfb.setAttribute("filterActive", "searching");
+      }, 500);
+    }
+    // filter completed, results
+    else if (view.dbView.numMsgsInView) {
+      // some matches
+      threadPane.setAttribute("filterActive", "matches");
+      qfb.setAttribute("filterActive", "matches");
+    }
+    // filter completed, no results
+    else {
+      // no matches! :(
+      threadPane.setAttribute("filterActive", "nomatches");
+      qfb.setAttribute("filterActive", "nomatches");
+    }
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  // Resizing regimen
+
+  /**
+   * Are the button labels currently collapsed?
+   */
+  _buttonLabelsCollapsed: false,
+
+  /**
+   * The minimum width the bar must be before we can un-collapse the button
+   *  labels.
+   */
+  _minExpandedBarWidth: null,
+
+  /**
+   * Our general strategy is this:
+   * - All collapsible buttons are set to not flex and live in the
+   *   "quick-filter-bar-collapsible-buttons" hbox.  This provides us with a
+   *   nice minimum size.
+   * - All flexy widgets have some minimum size configured.
+   * - When the bar starts to overflow we save off the (minimum) size of the bar
+   *   so that once it gets large enough again we can restore the buttons.
+   *
+   * This method handles the overflow case where we transition to collapsed
+   * buttons.  Our onWindowResize logic handles detecting when it is time to
+   * un-collapse.
+   */
+  onOverflow: function QFBM_onOverflow() {
+    // if we are already collapsed, there is nothing more to do.
+    if (this._buttonLabelsCollapsed)
+      return;
+
+    let quickFilterBarBox =
+      document.getElementById("quick-filter-bar-main-bar");
+    let collapsibleButtonBox =
+      document.getElementById("quick-filter-bar-collapsible-buttons");
+    // the scroll width is the actual size it wants to be...
+    this._minExpandedBarWidth = quickFilterBarBox.scrollWidth;
+    this._buttonLabelsCollapsed = true;
+
+    collapsibleButtonBox.setAttribute("shrink", "true");
+  },
+
+  /**
+   * Counterpart to |onOverflow| un-collapses the buttons once the quick filter
+   *  bar gets wide enough to support the desired minimum widget of the bar when
+   *  the buttons are not collapsed.
+   */
+  onWindowResize: function QFB_onWindowResize() {
+    // nothing to do here if the buttons are not collapsed
+    if (!this._buttonLabelsCollapsed)
+      return;
+
+    let quickFilterBarBox =
+      document.getElementById("quick-filter-bar-main-bar");
+    // the client width is how big it actually is (thanks to overflow:hidden)
+    if (quickFilterBarBox.clientWidth < this._minExpandedBarWidth)
+      return;
+
+    this._buttonLabelsCollapsed = false;
+    this._minExpandedBarWidth = null;
+
+    let collapsibleButtonBox =
+      document.getElementById("quick-filter-bar-collapsible-buttons");
+    collapsibleButtonBox.removeAttribute("shrink");
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  // Tab Monitor Interaction
+
+  monitorName: "quickFilter",
+
+  onTabTitleChanged: function QFBM_onTabTitleChanged(aTab) {
+    // nop
+  },
+
+  /**
+   * Whenever an appropriate new tab is opened, initialize its quick filter
+   *  state.
+   */
+  onTabOpened: function QFBM_onTabOpened(aTab, aFirstTab, aOldTab) {
+    if (aTab.mode.name == "folder" ||
+        aTab.mode.name == "glodaList") {
+      let modelTab =
+        this.tabmail.getTabInfoForCurrentOrFirstModeInstance(aTab.mode);
+      let oldFilterer = (modelTab && ("quickFilter" in modelTab._ext)) ?
+                          modelTab._ext.quickFilter : undefined;
+      aTab._ext.quickFilter = new QuickFilterState(oldFilterer);
+      this.updateSearch(aTab);
+    }
+  },
+
+  onTabRestored: function QFBM_onTabRestored(aTab, aState, aFirstTab) {
+    let filterer = aTab._ext.quickFilter = new QuickFilterState(null, aState);
+    this.updateSearch(aTab);
+    if (aTab == this.tabmail.currentTabInfo)
+      this.reflectFiltererState(filterer, aTab.folderDisplay);
+  },
+
+  onTabPersist: function QFBM_onTabPersist(aTab) {
+    let filterer = ("quickFilter" in aTab._ext) ? aTab._ext.quickFilter : null;
+    if (filterer)
+      return filterer.persistToObj();
+    return null;
+  },
+
+  /**
+   * On tab switch we need to:
+   * - Restore state for already existing state
+   * - Create state if it's a new (to us) tab
+   */
+  onTabSwitched: function QFBM_onTabSwitched(aTab, aOldTab) {
+    // (Note: we used to explicitly handle the possibility that the user had
+    // typed something but an ontimeout had not yet fired in the textbox.
+    // We are bailing on that because it adds complexity without much functional
+    // gain.  Our UI will be consistent when we switch back to the tab, which
+    // is good enough.)
+
+    let filterer = this.maybeActiveFilterer;
+    if (filterer)
+      this.reflectFiltererState(filterer, aTab.folderDisplay);
+    else // this only happens for tabs we are not legal on
+      document.getElementById("qfb-show-filter-bar").style.visibility = "hidden";
+  },
+
+  supportsCommand: function QFBM_supportsCommand(aCommand, aTab) {
+    // we are not active on tab types we do not support (message tabs)
+    if (!("quickFilter" in aTab._ext))
+      return null;
+
+    if (aCommand == "cmd_stop" || aCommand == "cmd_find" ||
+        aCommand == "cmd_toggleQuickFilterBar")
+      return true;
+    else
+      return null;
+  },
+  isCommandEnabled: function QFBM_isCommandEnabled(aCommand, aTab) {
+    // we are not active on tab types we do not support (message tabs)
+    if (!("quickFilter" in aTab._ext))
+      return null;
+
+    if (aCommand == "cmd_stop" || aCommand == "cmd_find" ||
+        aCommand == "cmd_toggleQuickFilterBar")
+      return true;
+    else
+      return null;
+  },
+  doCommand: function QFBM_doCommand(aCommand, aTab) {
+    // we are not active on tab types we do not support (message tabs)
+    if (!("quickFilter" in aTab._ext))
+      return null;
+
+    if (aCommand == "cmd_stop") {
+      QuickFilterBarMuxer.cmdEscapeFilterStack();
+      return true;
+    }
+    else if (aCommand == "cmd_find") {
+      let textWidget = document.getElementById(
+                         QuickFilterManager.textBoxDomId);
+      // if it's not already focused, then focus/select it
+      if (document.commandDispatcher.focusedElement != textWidget.inputField) {
+        QuickFilterBarMuxer._showFilterBar(true);
+        textWidget.select();
+        return true;
+      }
+    }
+    else if (aCommand == "cmd_toggleQuickFilterBar") {
+      this._showFilterBar(!this.activeFilterer.visible);
+      return true;
+    }
+    return null;
+  },
+
+  get maybeActiveFilterer() {
+    if ("quickFilter" in this.tabmail.currentTabInfo._ext)
+      return this.tabmail.currentTabInfo._ext.quickFilter;
+    return null;
+  },
+
+  get activeFilterer() {
+    if ("quickFilter" in this.tabmail.currentTabInfo._ext)
+      return this.tabmail.currentTabInfo._ext.quickFilter;
+    throw errorWithDebug("There is no active filterer but we want one.");
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  // Event Handling Support
+
+  /**
+   * Retrieve the current filter state value (presumably an object) for mutation
+   *  purposes.  This causes the filter to be the last touched filter for escape
+   *  undo-ish purposes.
+   */
+  getFilterValueForMutation: function QFBM_getFilterValueForMutation(aName) {
+    return this.activeFilterer.getFilterValue(aName);
+  },
+
+  /**
+   * Set the filter state for the given named filter to the given value.  This
+   *  causes the filter to be the last touched filter for escape undo-ish
+   *  purposes.
+   *
+   * @param aName Filter name.
+   * @param aValue The new filter state.
+   */
+  setFilterValue: function QFBM_setFilterValue(aName, aValue) {
+    this.activeFilterer.setFilterValue(aName, aValue);
+  },
+
+  /**
+   * For UI responsiveness purposes, defer the actual initiation of the search
+   *  until after the button click handling has completed and had the ability
+   *  to paint such.
+   */
+  deferredUpdateSearch: function QFBM_deferredUpdateSearch() {
+    setTimeout(this._deferredInvocUpdateSearch, 10);
+  },
+
+  /**
+   * The actual helper function to call updateSearch for deferredUpdateSearch
+   *  that makes 'this' relevant.
+   */
+  _deferredInvocUpdateSearch: function QFBM__deferredInvocUpdateSearch() {
+    QuickFilterBarMuxer.updateSearch();
+  },
+
+  /**
+   * Update the user terms part of the search definition to reflect the active
+   *  filterer's current state.
+   */
+  updateSearch: function QFBM_updateSearch(aTab) {
+    let tab = aTab || this.tabmail.currentTabInfo;
+    // bail if things don't really exist yet
+    if (!tab.folderDisplay.view.search)
+      return;
+
+    let filterer = tab._ext.quickFilter;
+    filterer.displayedFolder = tab.folderDisplay.displayedFolder;
+
+    let [terms, listeners] =
+      filterer.createSearchTerms(tab.folderDisplay.view.search.session);
+
+    for each (let [, [listener, filterDef]] in Iterator(listeners)) {
+      // it registers itself with the search session.
+      new QuickFilterSearchListener(
+        tab.folderDisplay, filterer, filterDef,
+        listener, QuickFilterBarMuxer);
+    }
+    tab.folderDisplay.view.search.userTerms = terms;
+    // Uncomment to know what the search state is when we (try and) update it.
+    //dump(tab.folderDisplay.view.search.prettyString());
+  },
+
+  _showFilterBar: function QFBM__showFilterBar(aShow) {
+    this.activeFilterer.visible = aShow;
+    if (!aShow) {
+      this.activeFilterer.clear();
+      this.updateSearch();
+    }
+    this.reflectFiltererState(this.activeFilterer,
+                              this.tabmail.currentTabInfo.folderDisplay);
+  },
+
+  /**
+   * Invoked when the user chooses the popup from the gloda search box.
+   */
+  cmdGlodaSearchDownSell: function QFBM_cmdGlodaSearchDownSell(aEvent) {
+    aEvent.stopPropagation();
+    this._showFilterBar(true);
+    let textWidget = document.getElementById(
+                       QuickFilterManager.textBoxDomId);
+    textWidget.select();
+  },
+
+  /**
+   * User explicitly closed the filter bar.
+   */
+  cmdClose: function QFBM_cmdClose(aEvent) {
+    this._showFilterBar(false);
+  },
+
+  /**
+   * User hit the escape key; do our undo-ish thing keeping in mind that this
+   *  may be invoked in situations where the filter bar is not legal / enabled.
+   */
+  cmdEscapeFilterStack: function QFBM_cmdEscapeFilterStack() {
+    let filterer = this.maybeActiveFilterer;
+    if (!filterer || !filterer.visible)
+      return;
+
+    // update the search if we were relaxing something
+    if (filterer.userHitEscape()) {
+      this.updateSearch();
+      this.reflectFiltererState(filterer,
+                                this.tabmail.currentTabInfo.folderDisplay);
+    }
+    // close the filter since there was nothing left to relax
+    else {
+      this.cmdClose();
+    }
+  },
+
+  _testHelperResetFilterState: function QFBM_resetFilterState() {
+    let filterer = this.maybeActiveFilterer;
+    if (!filterer)
+      return;
+    let tab = this.tabmail.currentTabInfo;
+    tab._ext.quickFilter = filterer = new QuickFilterState();
+    this.updateSearch();
+    this.reflectFiltererState(filterer, tab.folderDisplay);
+  },
+};
+addEventListener("load", function() QuickFilterBarMuxer._init(), false);
new file mode 100644
--- /dev/null
+++ b/mail/base/content/quickFilterBar.xul
@@ -0,0 +1,184 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- ***** BEGIN LICENSE BLOCK *****
+  -   Version: MPL 1.1/GPL 2.0/LGPL 2.1
+  -
+  - The contents of this file are subject to the Mozilla Public License Version
+  - 1.1 (the "License"); you may not use this file except in compliance with
+  - the License. You may obtain a copy of the License at
+  - http://www.mozilla.org/MPL/
+  - 
+  - Software distributed under the License is distributed on an "AS IS" basis,
+  - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+  - for the specific language governing rights and limitations under the
+  - License.
+  -
+  - The Original Code is Thunderbird Mail Client.
+  -
+  - The Initial Developer of the Original Code is the Mozilla Foundation.
+  - Portions created by the Initial Developer are Copyright (C) 2010
+  - the Initial Developer. All Rights Reserved.
+  -
+  - Contributor(s):
+  -   Andrew Sutherland <asutherland@asutherland.org>
+  -
+  - Alternatively, the contents of this file may be used under the terms of
+  - either the GNU General Public License Version 2 or later (the "GPL"), or
+  - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+  - in which case the provisions of the GPL or the LGPL are applicable instead
+  - of those above. If you wish to allow use of your version of this file only
+  - under the terms of either the GPL or the LGPL, and not to allow others to
+  - use your version of this file under the terms of the MPL, indicate your
+  - decision by deleting the provisions above and replace them with the notice
+  - and other provisions required by the GPL or the LGPL. If you do not delete
+  - the provisions above, a recipient may use your version of this file under
+  - the terms of any one of the MPL, the GPL or the LGPL.
+  - 
+  - ***** END LICENSE BLOCK ***** -->
+
+<?xml-stylesheet href="chrome://messenger/skin/quickFilterBar.css" type="text/css"?>
+<!DOCTYPE quickFilterBar SYSTEM "chrome://messenger/locale/quickFilterBar.dtd">
+<overlay id="quickFilterBar-overlay"
+         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script src="quickFilterBar.js" type="application/javascript" />
+
+  <commandset id="mailViewMenuItems">
+    <command id="cmd_toggleQuickFilterBar"
+             oncommand="goDoCommand('cmd_toggleQuickFilterBar');"
+             />
+  </commandset>
+
+  <menupopup id="view_toolbars_popup">
+    <!-- gets disabled but stays visible when not legal -->
+    <menuitem id="view_toolbars_popup_quickFilterBar"
+              insertbefore="viewMenuBeforeCustomizeMailToolbarsSeparator"
+              label="&quickFilterBar.toggleBarVisibility.menu.label;"
+              accesskey="&quickFilterBar.toggleBarVisibility.menu.accesskey;"
+              command="cmd_toggleQuickFilterBar"
+              observes="qfb-show-filter-bar"
+              key="key_find"
+              />
+  </menupopup>
+
+  <hbox id="thunderbird-private-tabmail-buttons">
+    <!-- gets hidden but keeps space allocation when not legal-->
+    <toolbarbutton id="qfb-show-filter-bar" type="checkbox"
+                   command="cmd_toggleQuickFilterBar"
+                   tooltiptext="&quickFilterBar.toggleBarVisibility.button.tooltip;"
+                   />
+  </hbox>
+
+  <popupset id="mainPopupSet">
+    <!-- Using an actual panel resulted in serious focus problems on linux,
+         even with noautofocus=true.  It would appear the window manager does
+         not buy into what we are selling in that case, whereas I presume the
+         specialization that precludes tooltips from ever having focus prevents
+         that problem from happening. -->
+    <tooltip id="qfb-text-search-upsell"
+             level="parent"
+             style="background-color: #ffeeee;"
+           >
+      <vbox>
+        <label id="qfb-upsell-line-one"
+               class="header"
+               fmt="&quickFilterBar.glodaUpsell.continueSearch;"
+               value=""
+               />
+        <label id="qfb-upsell-line-two"
+               fmt="&quickFilterBar.glodaUpsell.pressEnterAndCurrent;"
+               value=""
+               />
+      </vbox>
+    </tooltip>
+  </popupset>
+
+
+  <vbox id="threadContentArea">
+    <!--
+      - The message filter bar is not a XBL binding at this time in order to
+      - make it easier for people to overlay given that we are likely leaving
+      - a lot of visible space on the table.
+      -
+      - This may need to change for drag-and-drop or other reasons.
+      -->
+    <vbox id="quick-filter-bar" insertbefore="threadTree">
+      <hbox id="quick-filter-bar-main-bar" align="center"
+            onoverflow="QuickFilterBarMuxer.onOverflow();"
+            >
+        <toolbarbutton id="qfb-sticky" type="checkbox"
+                       crop="none" minwidth="16"
+                       tooltiptext="&quickFilterBar.sticky.tooltip;"
+                       />
+        <label id="qfb-filter-label"
+               value="&quickFilterBar.barLabel.label;"/>
+        <!-- Extensions, put your labeled buttons in the following box. -->
+        <hbox id="quick-filter-bar-collapsible-buttons">
+          <toolbarbutton id="qfb-unread" type="checkbox"
+                         crop="none" minwidth="16"
+                         label="&quickFilterBar.unread.label;"
+                         tooltiptext="&quickFilterBar.unread.tooltip;"
+                         />
+          <toolbarbutton id="qfb-starred" type="checkbox"
+                         crop="none" minwidth="16"
+                         label="&quickFilterBar.starred.label;"
+                         tooltiptext="&quickFilterBar.starred.tooltip;"
+                         />
+          <toolbarbutton id="qfb-inaddrbook" type="checkbox"
+                         crop="none" minwidth="16"
+                         label="&quickFilterBar.inaddrbook.label;"
+                         tooltiptext="&quickFilterBar.inaddrbook.tooltip;"
+                         />
+          <toolbarbutton id="qfb-tags" type="checkbox"
+                         crop="none" minwidth="16"
+                         label="&quickFilterBar.tags.label;"
+                         tooltiptext="&quickFilterBar.tags.tooltip;"
+                         />
+          <toolbarbutton id="qfb-attachment" type="checkbox"
+                         crop="none" minwidth="16"
+                         label="&quickFilterBar.attachment.label;"
+                         tooltiptext="&quickFilterBar.attachment.tooltip;"/>
+        </hbox>
+        <toolbarspacer id="qfb-filter-bar-spacer" flex="200" align="end"/>
+        <!-- I added a minwidth and end text-align because otherwise the change
+             in dimensions causes ugly flex rearrangement of the textbox. -->
+        <label id="qfb-results-label"
+               minwidth="&quickFilterBar.resultsLabel.minWidth;"
+               value=""
+               somefmtstring="&quickFilterBar.resultsLabel.some.formatString;"
+               noresultsstring="&quickFilterBar.resultsLabel.none;"
+               />
+        <textbox id="qfb-qs-textbox" flex="1"
+                 type="search"
+                 emptytext=""
+                 emptytextbase="&quickFilterBar.textbox.emptyText.base;"
+                 keyLabelNonMac="&quickFilterBar.textbox.emptyText.keyLabel.nonmac;"
+                 keyLabelMac="&quickFilterBar.textbox.emptyText.keyLabel.mac;"
+                 timeout="500"
+                 width="320"
+                 minwidth="280">
+        </textbox>
+      </hbox>
+      <hbox id="quick-filter-bar-expando">
+        <arrowscrollbox id="quick-filter-bar-tab-bar"
+                        orient="horizontal"
+                        collapsed="true"
+                        flex="2">
+        </arrowscrollbox>
+        <hbox id="quick-filter-bar-filter-text-bar"
+              collapsed="true"
+              pack="end"
+              align="center"
+              flex="1">
+          <label id="qfb-qs-label" value="Filter message by:"/>
+          <toolbarbutton id="qfb-qs-sender" type="checkbox"
+                         label="&quickFilterBar.textFilter.sender.label;" />
+          <toolbarbutton id="qfb-qs-recipients" type="checkbox"
+                         label="&quickFilterBar.textFilter.recipients.label;" />
+          <toolbarbutton id="qfb-qs-subject" type="checkbox"
+                         label="&quickFilterBar.textFilter.subject.label;" />
+          <toolbarbutton id="qfb-qs-body" type="checkbox"
+                         label="&quickFilterBar.textFilter.body.label;" />
+        </hbox>
+      </hbox>
+    </vbox>
+  </vbox>
+</overlay>
--- a/mail/base/content/search.xml
+++ b/mail/base/content/search.xml
@@ -53,159 +53,59 @@
 
   <!--
     - The glodaSearch binding implements a gloda-backed search mechanism.  The
     -  actual search logic comes from the glodaFacet tab mode in the
     -  glodaFacetTabType definition.  This binding serves as a means to display
     -  and alter the current search query if a "glodaFacet" tab is displayed,
     -  or enter a search query and spawn a new "glodaFacet" tab if one is
     -  currently not displayed.
+    -
+    - This widget used to have many weird implementation nuances.  Now we are
+    -  just a little bit of extra stuff on top of the toolkit autocomplete
+    -  implementation.  Our deviations are:
+    -  - We collapse ourselves when gloda is disabled; we track the state.
+    -  -
     -->
-  <binding id="glodaSearch" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
+  <binding id="glodaSearch"
+           extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
     <resources>
       <stylesheet src="chrome://messenger/skin/searchBox.css"/>
     </resources>
 
-    <content>
-      <xul:button anonid="quick-search-button" class="quick-search-button" type="menu">
-        <xul:menupopup anonid="quick-search-menupopup"
-                   class="quick-search-menupopup"
-                   persist="value"
-                   onpopupshowing="this.parentNode.parentNode.updatePopup();"
-                   popupalign="topleft"
-                   popupanchor="bottomleft">
-          <xul:menuitem anonid="searchGlobalMenu"
-                    class="quick-search-menu"
-                    value="global"
-                    glodaOnly="true"
-                    label="&searchAllMessages.label;"
-                    type="radio"
-                    oncommand="this.parentNode.parentNode.parentNode.changeMode(this)"/>
-          <xul:menuseparator quicksearchOnly="true"
-                             glodaOnly="true"/>
-        </xul:menupopup>
-      </xul:button>
-      <xul:hbox class="quick-search-textbox textbox-input-box" flex="1">
-        <html:input class="textbox-input" flex="1" anonid="input" allowevents="true"
-                    xbl:inherits="onfocus,onblur,oninput,value,type,maxlength,disabled,size,readonly,tabindex,accesskey"/>
-      </xul:hbox>
-      <xul:toolbarbutton anonid="quick-search-clearbutton" xbl:inherits=""
-                         disabled="true" class="quick-search-clearbutton"
-                         onclick="this.parentNode.value = ''; this.parentNode.doSearch(); this.parentNode.select(); return false;"/>
-                         <!--XXX update search if not global-->
-
-    </content>
     <handlers>
-      <handler event="input">
-        <![CDATA[
-        try {
-          if (this.searchMode != "global") { // it's a quick search
-            let dis = this;
-            clearTimeout(this.timeoutHandler);
-            this.timeoutHandler = setTimeout(this.onTimeout, this.timeout, dis);
-          }
-        } catch (e) {
-          logException(e);
-        }
-        ]]>
-      </handler>
-      <!-- For the next two, we need to get in on the bubbling phase, as
-           otherwise we'll be doing searches when autocomplete results are
-           being selected. -->
-      <handler event="keypress" keycode="VK_ENTER"
-        phase="bubbling" action="return this.doSearch();"/>
-      <handler event="keypress" keycode="VK_RETURN"
-        phase="bubbling">
-        <![CDATA[
-          try {
-            this.doSearch();
-          } catch (e) {
-            logException(e);
-          }
-          return true;
-        ]]>
-      </handler>
-      <handler event="input">
-        <![CDATA[
-          if (!this.value)
-            this.clearButtonHidden = true;
-          else
-            this.clearButtonHidden = false;
-        ]]></handler>
-      <handler event="keypress" keycode="VK_DOWN" modifiers="alt"
-               phase="capturing" action="return this.openmenupopup();"/>
-      <handler event="keypress" keycode="VK_UP"   modifiers="alt"
-               phase="capturing" action="return this.openmenupopup();"/>
-      <handler event="keypress" keycode="VK_F4" phase="capturing"><![CDATA[
-        if (window.navigator.oscpu.substring(0, 3).toLowerCase() != "mac")
-          return this.openmenupopup();
+      <handler event="drop" phase="capturing"><![CDATA[
+        nsDragAndDrop.drop(event, this.searchInputDNDObserver);
       ]]></handler>
 
-      <handler event="keypress" keycode="VK_UP" modifiers="control"
-               phase="capturing">
-        <![CDATA[
-        try {
-          var menuPopupValue = this.menupopup.getAttribute('value');
-          var menuItem =
-            this.menupopup.getElementsByAttribute('value', this.searchMode)[0];
-          if (menuItem != this.menupopup.firstChild) {
-            let previousMenuItem = menuItem.previousSibling;
-            while (! previousMenuItem.hasAttribute("value") &&
-                   previousMenuItem != this.menupopup.firstChild)
-              previousMenuItem = previousMenuItem.previousSibling;
-
-            previousMenuItem.setAttribute("checked", "true");
-            menuItem.removeAttribute("checked");
-            this.searchMode = previousMenuItem.getAttribute('value');
-            this.menupopup.setAttribute('value', this.searchMode);
-          }
-          return false;
-        } catch (e) {
-          logException(e);
-        }
-        ]]></handler>
-
-      <handler event="keypress" keycode="VK_DOWN" modifiers="control"
-               phase="capturing">
-        <![CDATA[
-        try {
-          var menuPopupValue = this.menupopup.getAttribute('value');
-          var menuItem =
-            this.menupopup.getElementsByAttribute('value', this.searchMode)[0];
-          if (menuItem != this.menupopup.lastChild) {
-            let nextMenuItem = menuItem.nextSibling;
-            while (! nextMenuItem.hasAttribute("value") &&
-                   nextMenuItem != this.menupopup.lastChild)
-              nextMenuItem = nextMenuItem.nextSibling;
-            nextMenuItem.setAttribute("checked", "true");
-            menuItem.removeAttribute("checked");
-            this.searchMode = nextMenuItem.getAttribute('value');
-            this.menupopup.setAttribute('value', this.searchMode);
-          }
-          return false;
-        } catch (e) {
-          logException(e);
-        }
-        ]]></handler>
-
-
-      <handler event="drop" phase="capturing">
-        nsDragAndDrop.drop(event, this.searchInputDNDObserver);
-      </handler>
+      <handler event="keypress" keycode="VK_RETURN"><![CDATA[
+        this.doSearch();
+        event.preventDefault();
+        event.stopPropagation();
+      ]]></handler>
+      <handler event="keypress" keycode="VK_ESCAPE"><![CDATA[
+        this.clearSearch();
+        event.preventDefault();
+        event.stopPropagation();
+      ]]></handler>
     </handlers>
 
     <implementation implements="nsIObserver">
       <constructor><![CDATA[
         const Cc = Components.classes;
         const Ci = Components.interfaces;
         const Cu = Components.utils;
         Cu.import("resource:///modules/errUtils.js");
         try {
-          Cu.import("resource:///modules/StringBundle.js");
-          Cu.import("resource:///modules/quickSearchManager.js");
+          this.setAttribute(
+            "emptytext",
+            this.getAttribute("emptytextbase")
+                 .replace("#1", this.getAttribute(
+                                  Application.platformIsMac ?
+                                  "keyLabelMac" : "keyLabelNonMac")));
 
           var prefBranch =
               Components.classes['@mozilla.org/preferences-service;1'].
               getService(Components.interfaces.nsIPrefBranch2);
           this.glodaCompleter =
             Components.classes["@mozilla.org/autocomplete/search;1?name=gloda"]
                       .getService()
                       .wrappedJSObject;
@@ -227,58 +127,19 @@
             // care, since we don't want to register observers in that scope.
 
             prefBranch.addObserver("mailnews.database.global.indexer.enabled",
                                    this._prefObserver, false);
             observerSvc.addObserver(this, "autocomplete-did-enter-text", false);
             gSearchInputObserversRegistered = true;
           }
 
-          this.quickSearchStrings =
-            new StringBundle(
-              "chrome://messenger/locale/quickSearch.properties");
-
-          let quickSearchModes = QuickSearchManager.getSearchModes();
-          for (let i = 0; i < quickSearchModes.length; i++) {
-            let searchMode = quickSearchModes[i];
-            let value = searchMode["value"];
-            let label = searchMode["label"];
-            let menuitem = document.createElement("menuitem");
-            menuitem.setAttribute("value", value.toString());
-            menuitem.setAttribute("label", label);
-            menuitem.setAttribute("type", "radio");
-            menuitem.setAttribute("quicksearchOnly", "true");
-            menuitem.setAttribute("oncommand",
-              "this.parentNode.parentNode.parentNode.changeMode(this)");
-            this.menupopup.appendChild(menuitem);
-          }
-
-          let separator = document.createElement("menuseparator");
-          separator.setAttribute("quicksearchOnly", "true");
-          this.menupopup.appendChild(separator);
-
-          let saveAsVF = document.createElement("menuitem");
-          saveAsVF.setAttribute("anonid",
-                                "quick-search-save-as-virtual-folder");
-          saveAsVF.setAttribute("label",
-            this.quickSearchStrings.get("saveAsVirtualFolder.label"));
-          saveAsVF.setAttribute("quicksearchOnly", "true");
-          saveAsVF.setAttribute("oncommand",
-            "gFolderTreeController.newVirtualFolder(\
-              this.parentNode.parentNode.parentNode.value,\
-              gFolderDisplay.view.search.session.searchTerms);");
-          this.menupopup.appendChild(saveAsVF);
-
-          this.updateSaveItem();
-          this.input = "";
           this.glodaEnabled =
             prefBranch.getBoolPref("mailnews.database.global.indexer.enabled");
-          this.searchMode = this.glodaEnabled ? "global" :
-            quickSearchModes[QuickSearchConstants.kQuickSearchFromOrSubject]
-              .value.toString();
+          this.collapsed = !this.glodaEnabled;
         } catch (e) {
           logException(e, true);
         }
       ]]></constructor>
 
       <destructor>
         <![CDATA[
           var prefBranch =
@@ -298,122 +159,46 @@
         {
           if (topic == "nsPref:changed") {
             subject.QueryInterface(Components.interfaces.nsIPrefBranch);
             switch (data) {
             case "mailnews.database.global.indexer.enabled":
               this.inputSearch.glodaEnabled =
                 gPrefBranch.getBoolPref(
                   "mailnews.database.global.indexer.enabled");
-              let quickSearchModes = QuickSearchManager.getSearchModes();
-              this.inputSearch.searchMode = this.inputSearch.glodaEnabled ?
-                "global" :
-                quickSearchModes[QuickSearchConstants.kQuickSearchFromOrSubject]
-                  .value.toString();
+              this.inputSearch.collapsed = !this.inputSearch.glodaEnabled;
               break;
             }
           }
         },
 
         QueryInterface: function(aIID)
         {
           if (aIID.equals(Components.interfaces.nsIObserver) ||
               aIID.equals(Components.interfaces.nsISupports))
             return this;
           throw Components.results.NS_NOINTERFACE;
         }
         });
       </field>
-      <field name="timeout">200</field>
       <field name="glodaCompleter">null</field>
-      <field name="ignoreClick">false</field>
-      <field name="quickSearchStrings">null</field>
-      <field name="mQuickSearchMode">null</field>
-      <field name="timeoutHandler">null</field>
-      <property name="searchMode">
-        <getter><![CDATA[
-          return this.mQuickSearchMode;
-        ]]></getter>
-        <setter><![CDATA[
-          this.mQuickSearchMode = val;
-          this.menupopup.setAttribute('value', val);
-          let tabmail = document.getElementById('tabmail');
-          if (tabmail) { /* if not in the customize toolbar */
-            let currentTabInfo = tabmail.currentTabInfo;
-            this.menupopup.getElementsByAttribute('value', this.searchMode)[0]
-                          .setAttribute('checked', 'true');
-            if (currentTabInfo)
-             currentTabInfo.searchMode = this.searchMode;
-            this.updateEmptyText();
-          }
-          this.setAttribute("autocompletesearchparam", val)
-        ]]></setter>
-      </property>
-      <field name="_glodaEnabled"/>
-      <property name="glodaEnabled">
-        <getter><![CDATA[
-          return this._glodaEnabled
-        ]]></getter>
-        <setter><![CDATA[
-          try {
-            this.showGlodaItems(val); this._glodaEnabled = val;
-          } catch(e) {
-            logException(e);
-          }
-        ]]></setter>
-      </property>
-      <property name="autocompletePopup" readonly="true">
-        <getter><![CDATA[
-          return document.getElementById(
-                   this.getAttribute('autocompletepopup'));
-        ]]></getter>
-      </property>
-      <property name="showingSearchCriteria">
-        <getter><![CDATA[
-          return this.getAttribute('searchCriteria') == 'true';
-        ]]></getter>
-        <setter><![CDATA[
-          this.setAttribute('searchCriteria', val); return val;
-        ]]></setter>
-      </property>
       <property name="menupopup" readonly="true">
         <getter><![CDATA[
           return document.getAnonymousElementByAttribute(
                    this, 'anonid', 'quick-search-menupopup');
         ]]></getter>
       </property>
-      <property name="saveAsVirtualFolder" readonly="true">
-        <getter><![CDATA[
-          return document.getAnonymousElementByAttribute(
-                   this, 'anonid', 'quick-search-save-as-virtual-folder');
-        ]]></getter>
-      </property>
-      <property name="clearButtonHidden">
-        <getter><![CDATA[
-          return document.getAnonymousElementByAttribute(
-                            this, 'anonid', 'quick-search-clearbutton')
-                         .getAttribute('clearButtonHidden') == 'true';
-        ]]></getter>
-        <setter><![CDATA[
-          document.getAnonymousElementByAttribute(
-                     this, 'anonid', 'quick-search-clearbutton')
-                  .setAttribute('clearButtonHidden', val);
-          return val;
-        ]]></setter>
-      </property>
 
       <property name="state">
         <getter><![CDATA[
           return {
-            'mode': this.searchMode,
             'string': this.value
           };
         ]]></getter>
         <setter><![CDATA[
-          this.searchMode = val['mode'];
           this.value = val['string'];
         ]]></setter>
       </property>
 
       // DND Observer
       <field name="searchInputDNDObserver" readonly="true"><![CDATA[
       ({
         inputSearch: this,
@@ -435,64 +220,24 @@
         getSupportedFlavours: function () {
           var flavourSet = new FlavourSet();
           flavourSet.appendFlavour("text/unicode");
           return flavourSet;
         }
       })
       ]]></field>
 
-      <method name="onTimeout">
-        <parameter name="dis"/>
-        <body><![CDATA[
-        try {
-          dis.doSearch();
-        } catch (e) {
-          logException(e);
-        }
-        ]]>
-        </body>
-      </method>
-      <method name="updatePopup">
-        <body><![CDATA[
-        try {
-          // disable the create virtual folder menu item if the current radio
-          // value is set to Find in message since you can't really  create a VF from find
-          // in message
-          if (this.searchMode == "global" || this.value == "")
-            this.saveAsVirtualFolder.setAttribute('disabled', 'true');
-          else
-            this.saveAsVirtualFolder.removeAttribute('disabled');
-
-          //let hideQuickSearchModes = this.searchMode == "global" ? "true" : "false";
-          //for each (let child in this.menupopup.childNodes) {
-          //  if (child.hasAttribute("quicksearch")) {
-          //    child.setAttribute("collapsed", hideQuickSearchModes)
-          //  }
-          //}
-        } catch (e) {
-          logException(e);
-        }
-        ]]>
-        </body>
-      </method>
-      <method name="build">
-        <body><![CDATA[
-
-        ]]></body>
-      </method>
-
       <method name="observe">
-      <parameter name="aSubject"/>
-      <parameter name="aTopic"/>
-      <parameter name="aData"/>
+        <parameter name="aSubject"/>
+        <parameter name="aTopic"/>
+        <parameter name="aData"/>
         <body><![CDATA[
         try {
           if (aTopic == "autocomplete-did-enter-text") {
-            let selectedIndex = this.autocompletePopup.selectedIndex;
+            let selectedIndex = this.popup.selectedIndex;
             let curResult = this.glodaCompleter.curResult;
             if (! curResult)
               return; // autocomplete didn't even finish.
             let row = curResult.getObjectAt(selectedIndex);
             if (row == null)
               return;
             let theQuery = Gloda.newQuery(Gloda.NOUN_MESSAGE);
             let tabmail = document.getElementById("tabmail");
@@ -513,172 +258,50 @@
             }
           }
         } catch (e) {
           logException(e);
         }
         ]]></body>
       </method>
 
-      <method name="updateEmptyText">
-        <body><![CDATA[
-        try {
-          // extract the label value from the menu item
-          let menuItem = this.menupopup.getElementsByAttribute('value',
-                          this.searchMode)[0];
-          this.emptyText = menuItem.getAttribute('label');
-        } catch (e) {
-          logException(e);
-        }
-        ]]></body>
-      </method>
-
-      <method name="updateSaveItem">
-        <body><![CDATA[
-          let disabled = true;
-          this.saveAsVirtualFolder.setAttribute("disabled", disabled)
-        ]]></body>
-      </method>
-
-
       <method name="doSearch">
         <body><![CDATA[
           try {
-            if (this.searchMode == 'global') // faceted search
-            {
-              if (this.value) {
-                let tabmail = document.getElementById("tabmail");
-                // If the current tab is a gloda search tab, reset the value
-                //  to the initial search value.  Otherwise, clear it.  This
-                //  is the value that 3is going to be saved with the current
-                //  tab when we switch back to it next.
-                let searchString = this.value;
+            if (this.value) {
+              let tabmail = document.getElementById("tabmail");
+              // If the current tab is a gloda search tab, reset the value
+              //  to the initial search value.  Otherwise, clear it.  This
+              //  is the value that 3is going to be saved with the current
+              //  tab when we switch back to it next.
+              let searchString = this.value;
 
-                if (tabmail.currentTabInfo.mode.name == "glodaFacet") {
-                  // we'd rather reuse the existing tab (and somehow do something
-                  // smart with any preexisting facet choices, but that's a
-                  // bit hard right now, so doing the cheap thing and closing
-                  // this tab and starting over
-                  tabmail.closeTab();
-                }
-                this.value = ''; // clear our value, to avoid persistence
-                if (gFolderDisplay.view) {
-                  // if we started out from a gFolderDisplay.view,
-                  // remove existing (non-global) filter from view, to avoid persistence
-                  gFolderDisplay.view.search.userTerms = null;
-                }
-                tabmail.openTab("glodaFacet", {
-                  searcher: new GlodaMsgSearcher(null, searchString)
-                });
+              if (tabmail.currentTabInfo.mode.name == "glodaFacet") {
+                // we'd rather reuse the existing tab (and somehow do something
+                // smart with any preexisting facet choices, but that's a
+                // bit hard right now, so doing the cheap thing and closing
+                // this tab and starting over
+                tabmail.closeTab();
               }
-            } else { // quick search
-              if (!gFolderDisplay.view)
-                return;
-              if (! this.value)
-                gFolderDisplay.view.search.userTerms = null
-              else
-                gFolderDisplay.view.search.quickSearch(Number(this.searchMode), this.value);
+              this.value = ''; // clear our value, to avoid persistence
+              tabmail.openTab("glodaFacet", {
+                searcher: new GlodaMsgSearcher(null, searchString)
+              });
             }
           } catch (e) {
             logException(e);
           }
         ]]>
         </body>
       </method>
-
-      <method name="changeMode">
-      <parameter name="aMenuItem" />
+      <method name="clearSearch">
         <body><![CDATA[
-          var oldSearchMode = this.searchMode;
-          this.searchMode = aMenuItem.value;
-          if (oldSearchMode != this.searchMode) // the search mode just changed so we need to redo the quick search
-            this.doSearch();
+          this.value = "";
         ]]></body>
       </method>
-
-      <method name="openmenupopup">
-        <body>
-          <![CDATA[
-          try {
-            this.menupopup.click();
-          } catch (e) {
-            logException(e);
-          }
-          return false;
-          ]]>
-        </body>
-      </method>
-
-
-      <!--If switching from an "incoming" (Inbox, etc.) type of mail folder,-->
-      <!--to an "outbound" (Sent, Drafts etc.)  type, and the current search-->
-      <!--type contains 'Sender', then switch it to the equivalent-->
-      <!--'Recipient' search type by default. Vice versa when switching from-->
-      <!--outbound to incoming folder type.-->
-      <!--@param isOutboundFolder  Bool-->
-      <!--       true:  switch from an incoming to an outgoing folder-->
-      <!--       false: switch from an outgoing to an incoming folder-->
-      <method name="folderChanged">
-        <parameter name="isOutboundFolder" />
-        <body>
-          <![CDATA[
-            let newSearchMode = null;
-            if (isOutboundFolder) {
-              if (this.searchMode == QuickSearchConstants.kQuickSearchFromOrSubject)
-                this.searchMode = QuickSearchConstants.kQuickSearchRecipientOrSubject;
-              else if (this.searchMode == QuickSearchConstants.kQuickSearchFrom)
-                this.searchMode = QuickSearchConstants.kQuickSearchRecipient;
-            } else {
-              if (this.searchMode == QuickSearchConstants.kQuickSearchRecipientOrSubject)
-                this.searchMode = QuickSearchConstants.kQuickSearchFromOrSubject;
-              else if (this.searchMode == QuickSearchConstants.kQuickSearchRecipient)
-                this.searchMode = QuickSearchConstants.kQuickSearchFrom;
-            }
-          ]]>
-        </body>
-      </method>
-      <!--
-        Called by the TabMonitor to make the quick search menu only show options that are relevant for a particular tab.
-        In particular, in faceted search results, no real mode choices are available.
-
-         @param showQSItems  Bool
-            true:  show quick search-relevant menu items
-            false: don't show quick search-relevant menu items
-      -->
-      <method name="showQuickSearchItems">
-        <parameter name="showQSItems" />
-        <body>
-          <![CDATA[
-            for (let i = 0; i < this.menupopup.childNodes.length; i++) {
-              let menuitem = this.menupopup.childNodes[i];
-              if (menuitem.getAttribute('quicksearchOnly') == 'true')
-                menuitem.collapsed = ! showQSItems;
-            }
-          ]]>
-        </body>
-      </method>
-
-      <method name="showGlodaItems">
-        <parameter name="aEnable"/>
-        <body><![CDATA[
-        try {
-          for (let i = 0; i < this.menupopup.childNodes.length; i++) {
-            let menuitem = this.menupopup.childNodes[i];
-            if (menuitem.getAttribute('glodaOnly') == 'true') {
-              menuitem.collapsed = ! aEnable;
-              menuitem.hidden = ! aEnable;
-            }
-          }
-        } catch (e) {
-          logException(e);
-        }
-        ]]>
-        </body>
-      </method>
-
     </implementation>
   </binding>
 
   <binding id="searchBarDropMarker">
     <resources>
       <stylesheet src="chrome://messenger/skin/searchBox.css"/>
     </resources>
     <content popup="_child">
--- a/mail/base/content/searchBar.js
+++ b/mail/base/content/searchBar.js
@@ -35,63 +35,64 @@
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
-Components.utils.import("resource:///modules/quickSearchManager.js");
 Components.utils.import("resource:///modules/StringBundle.js");
 
-
 var gSearchBundle;
 
 var gStatusBar = document.getElementById('statusbar-icon');
 
 var gGlodaCompleteStrings = new StringBundle("chrome://messenger/locale/glodaComplete.properties");
 
 /* see the constructor of searchbar in search.xml's constructor for details */
 var gSearchInputObserversRegistered = false;
 
 /**
  * The quicksearch widget is a UI widget (the #searchInput textbox) which is
  * outside of the mailTabType's display panel, but acts as though it were within
  * it..  This means we need to use a tab monitor so that we can appropriately
  * update the contents of the textbox.
- * 
+ *
  * Every time a tab is changed, we save the state of the text box and restore
  *  its previous value for the tab we are switching to, as well as whether this
  *  value is a change to the currently-used value (if it is a faceted search) tab.
  *  The behaviour rationale for this is that the searchInput is like the
  *  URL bar.  When you are on a glodaSearch tab, we need to show you your
  *  current value, including any "uncommitted" (you haven't hit enter yet)
  *  changes.
  *
  *  In addition, we want to disable the quick-search modes when a tab is
  *  being displayed that lacks quick search abilities (but we'll leave the
  *  faceted search as it's always available).
  */
 
-var QuickSearchTabMonitor = {
+var GlodaSearchBoxTabMonitor = {
+  monitorName: "glodaSearchBox",
+
   onTabTitleChanged: function() {
   },
 
+  onTabOpened: function GSBTM_onTabOpened(aTab, aFirstTab, aOldTab) {
+    aTab._ext.glodaSearchBox = {
+      value: "",
+    };
+  },
+
   onTabSwitched: function (aTab, aOldTab) {
     let searchInput = document.getElementById("searchInput");
     if (!searchInput) // customized out of the way
       return;
 
-    searchInput.showQuickSearchItems(aTab.mode.tabType != glodaFacetTabType)
     // save the current search field value
     if (aOldTab) {
-      aOldTab.searchInputValue = searchInput.value;
-      // XXX search.xml also updates this, so this shouldn't be necessary
-      aOldTab.searchMode = searchInput.searchMode;
+      aOldTab._ext.glodaSearchBox.value = searchInput.value;
     }
     // load (or clear if there is none) the persisted search field value
-    searchInput.value = aTab.searchInputValue || "";
-    if (aTab.searchMode)
-      searchInput.searchMode = aTab.searchMode;
+    searchInput.value = aTab._ext.glodaSearchBox.value || "";
   }
 };
 
--- a/mail/base/content/tabmail.xml
+++ b/mail/base/content/tabmail.xml
@@ -216,26 +216,57 @@
     -     the window.
     - * getBrowser(aTab): This function should return the browser element for
     -     your tab if there is one (return null or don't define this function
     -     otherwise). It is used for some toolkit functions that require a
     -     global "getBrowser" function, e.g. ZoomManager.
     -
     - Tab monitoring code is expected to be used for widgets on the screen
     -  outside of the tab box that need to update themselves as the active tab
-    -  changes.  This is primarily intended to be used for the ThunderBar; if
-    -  you are not the ThunderBar and this sounds appealing to you, please
-    -  solicit discussion on your needs on the mozilla.dev.apps.thunderbird
-    -  newsgroup.
+    -  changes.
     - Tab monitoring code (un)registers itself via (un)registerTabMonitor.
+    -  The following attributes should be provided on the monitor object:
+    - * monitorName: A string value naming the tab monitor/extension.  This is
+    -     the canonical name for the tab monitor for all persistence purposes.
+    -     If the tab monitor wants to store data in the tab info object and its
+    -     name is FOO it should store it in 'tabInfo._ext.FOO'.  This is the
+    -     only place the tab monitor should store information on the tab info
+    -     object.  The FOO attribute will not be automatically created; it is
+    -     up to the code.  The _ext attribute will be there, reliably, however.
+    -     The name is also used when persisting state, but the tab monitor
+    -     does not need to do anything in that case; the name is automatically
+    -     used in the course of wrapping the object.
     -  The following functions should be provided on the monitor object:
     - * onTabTitleChanged(aTab): Called when the tab's title changes.
     - * onTabSwitched(aTab, aOldTab): Called when a new tab is made active.  If
     -     this is the first tab ever, aOldTab will be null, otherwise aOldTab
     -     will be the previously active tab.
+    - * onTabOpened(aTab, aIsFirstTab, aWasCurrentTab): Called when a new tab is
+    -     opened.  This method is invoked after the tab mode's openTab method
+    -     is invoked.  This method is invoked before the tab monitor
+    -     onTabSwitched method in the case where it will be invoked.  (It is
+    -     not invoked if the tab is opened in the background.)
+    - * onTabClosing(aTab): Called when a tab is being closed.  This method is
+    -     is invoked before the call to the tab mode's closeTab function.
+    - * onTabPersist(aTab): Return a JSON-representable object to persist for
+    -     the tab.  Return null if you do not have anything to persist.
+    - * onTabRestored(aTab, aState, aIsFirstTab): Called when a tab is being
+    -     restored and there is data previously persisted by the tab monitor.
+    -     This method is called instead of invoking onTabOpened.  This is done
+    -     because the restoreTab method (potentially) uses the tabmail openTab
+    -     API to effect restoration.  (Note: the first opened tab is special;
+    -     it will produce an onTabOpened notification potentially followed by
+    -     an onTabRestored notification.)
+    - Tab monitor code is also allowed to hook into the command processing
+    -  logic.  We support the standard supportsCommand/isCommandEnabled/
+    -  doCommand functions but with a twist to indicate when other tab monitors
+    -  and the actual tab itself should get a chance to process: supportsCommand
+    -  and isCommandEnabled should return null when they are not handling the
+    -  case.  doCommand should return true if it handled the case, null
+    -  otherwise.
     -->
   <binding id="tabmail">
     <resources>
       <stylesheet src="chrome://messenger/content/tabmail.css"/>
       <stylesheet src="chrome://messenger/skin/tabmail.css"/>
     </resources>
     <content>
       <xul:tabbox anonid="tabbox" flex="1" eventnode="document" xbl:inherits="handleCtrlPageUpDown"
@@ -277,16 +308,17 @@
         <!-- Remember, user of this binding, you need to provide tabpanels!  -->
         <children includes="tabpanels"/>
       </xul:tabbox>
     </content>
 
     <implementation implements="nsIController">
       <constructor>
         window.controllers.insertControllerAt(0, this);
+        this._restoringTabState = null;
       </constructor>
       <destructor>
         window.controllers.removeController(this);
       </destructor>
       <field name="currentTabInfo">
         null
       </field>
       <!-- Temporary field that only has a non-null value during a call to
@@ -395,29 +427,30 @@
           // From the moment of creation, our XBL binding already has a visible
           //  tab.  We need to create a tab information structure for this tab.
           //  In the process we also generate a synthetic tab title changed
           //  event to ensure we have an accurate title.  We assume the tab
           //  contents will set themselves up correctly.
           if (this.tabInfo.length == 0) {
             let firstTab = {mode: this.defaultTabMode, busy: false,
                             canClose: false,
-                            searchMode: 'global'};
+                            _ext: {}};
             firstTab.mode.tabs.push(firstTab);
 
             this.tabInfo[0] = this.currentTabInfo = firstTab;
 
             let tabOpenFirstFunc = firstTab.mode.openFirstTab ||
                                    firstTab.mode.tabType.openFirstTab;
             tabOpenFirstFunc.call(firstTab.mode.tabType, firstTab);
             this.setTabTitle(null);
 
-            if (this.tabMonitors.length) {
-              for each (let [i, tabMonitor] in Iterator(this.tabMonitors))
-                tabMonitor.onTabSwitched(firstTab, null);
+            for each (let [i, tabMonitor] in Iterator(this.tabMonitors)) {
+              if ("onTabOpened" in tabMonitor)
+                tabMonitor.onTabOpened(firstTab, true);
+              tabMonitor.onTabSwitched(firstTab, null);
             }
           }
         ]]></body>
       </method>
       <method name="openTab">
         <parameter name="aTabModeName"/>
         <parameter name="aArgs"/>
         <body>
@@ -450,17 +483,17 @@
               return;
             }
           }
 
           if (!background)
             // we need to save the state before it gets corrupted
             this.saveCurrentTabState();
 
-          let tab = {mode: tabMode, busy: false, canClose: true};
+          let tab = {mode: tabMode, busy: false, canClose: true, _ext: {}};
           tabMode.tabs.push(tab);
 
           var t = document.createElementNS(
             "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
             "tab");
           tab.tabNode = t;
           t.setAttribute("crop", "end");
           t.maxWidth = this.tabContainer.mTabMaxWidth;
@@ -503,18 +536,26 @@
               this.panelContainer.selectedPanel = tab.panel;
           }
           else if (!background)
             this.panelContainer.selectedPanel = tab.mode.tabType.panel;
 
           let tabOpenFunc = tab.mode.openTab || tab.mode.tabType.openTab;
           tabOpenFunc.apply(tab.mode.tabType, [tab, aArgs]);
 
-          if (!background && this.tabMonitors.length) {
-            for each (let [i, tabMonitor] in Iterator(this.tabMonitors))
+          let restoreState = this._restoringTabState;
+          for each (let [i, tabMonitor] in Iterator(this.tabMonitors)) {
+            if (("onTabRestored" in tabMonitor) && restoreState &&
+                (tabMonitor.monitorName in restoreState.ext))
+              tabMonitor.onTabRestored(tab,
+                                       restoreState.ext[tabMonitor.monitorName],
+                                       false);
+            else if ("onTabOpened" in tabMonitor)
+              tabMonitor.onTabOpened(tab, false, oldTab);
+            if (!background)
               tabMonitor.onTabSwitched(tab, oldTab);
           }
           
           // clear _mostRecentTabInfo; we only needed it during the call to
           //  openTab.
           this._mostRecentTabInfo = null;
           
           t.setAttribute("label", tab.title);
@@ -599,16 +640,21 @@
         <parameter name="aOptTabIndexNodeOrInfo"/>
         <body>
           <![CDATA[
             let [iTab, tab, tabNode] =
               this._getTabContextForTabbyThing(aOptTabIndexNodeOrInfo, true);
 
             if (!tab.canClose)
               return;
+
+            for each (let [i, tabMonitor] in Iterator(this.tabMonitors)) {
+              if ("onTabClosing" in tabMonitor)
+                tabMonitor.onTabClosing(tab);
+            }
             
             let closeFunc = tab.mode.closeTab || tab.mode.tabType.closeTab;
             closeFunc.call(tab.mode.tabType, tab);
              
             this.tabInfo.splice(iTab, 1);
             tab.mode.tabs.splice(tab.mode.tabs.indexOf(tab), 1);
             this.tabContainer.removeChild(tabNode);
             if (this.tabContainer.selectedIndex == -1)
@@ -700,19 +746,30 @@
               continue;
             let tabState = persistFunc.call(tab.mode.tabType, tab);
             // If there is a non-null tab-state, then persisting succeeded and
             //  we should store it.  We store the tab's persisted state in its
             //  own distinct object rather than mixing things up in a dictionary
             //  to avoid bugs and because we may eventually let extensions store
             //  per-tab information in the persisted state.
             if (tabState != null) {
+              let ext = {};
+
+              for each (let [i, tabMonitor] in Iterator(this.tabMonitors)) {
+                if ("onTabPersist" in tabMonitor) {
+                  let monState = tabMonitor.onTabPersist(tab);
+                  if (monState !== null)
+                    ext[tabMonitor.monitorName] = monState;
+                }
+              }
+
               tabs.push({
                 mode: tab.mode.name,
                 state: tabState,
+                ext: ext,
               });
               // Mark this persisted tab as selected
               if (iTab == this.tabContainer.selectedIndex)
                 state.selectedIndex = tabs.length - 1;
             }
           }
 
           return state;
@@ -741,17 +798,22 @@
               continue;
 
             // The first tab is a folder tab (we know that from our
             // implementation). Tell the folder tab to back off if necessary.
             // XXX find a better way to do this.
             if (tabState.state.firstTab && aDontRestoreFirstTab)
               tabState.state.dontRestoreFirstTab = aDontRestoreFirstTab;
 
+            // normalize the state to have an ext attribute if it does not.
+            if (!("ext" in tabState))
+              tabState.ext = {};
+            this._restoringTabState = tabState;
             restoreFunc.call(mode.tabType, this, tabState.state);
+            this._restoringTabState = null;
 
             // If this persisted tab was the selected one, then mark the newest
             //  tab as the guy to select.
             if (iTab == aPersistedState.selectedIndex)
               indexToSelect = this.tabInfo.length - 1;
           }
           if (indexToSelect != null && !aDontRestoreFirstTab)
             this.tabContainer.selectedIndex = indexToSelect;
@@ -816,20 +878,18 @@
                 this.tabInfo[this.tabContainer.selectedIndex];
 
               this.panelContainer.selectedPanel = tab.panel ||
                                                   tab.mode.tabType.panel;
 
               let showTabFunc = tab.mode.showTab || tab.mode.tabType.showTab;
               showTabFunc.call(tab.mode.tabType, tab);
 
-              if (this.tabMonitors.length) {
-                for each (let [i, tabMonitor] in Iterator(this.tabMonitors))
-                  tabMonitor.onTabSwitched(tab, oldTab);
-              }
+              for each (let [i, tabMonitor] in Iterator(this.tabMonitors))
+                tabMonitor.onTabSwitched(tab, oldTab);
 
               // always update the cursor status when we switch tabs
               SetBusyCursor(window, tab.busy);
               // active tabs should not have the wasBusy attribute
               this.tabContainer.selectedItem.removeAttribute("wasBusy");
 
               // update the thinking status when we switch tabs
               this._setActiveThinkingState(tab.thinking);
@@ -883,20 +943,18 @@
               let tabNode =
                 this.tabContainer.childNodes[iTab];
 
               let titleChangeFunc = tab.mode.onTitleChanged ||
                                     tab.mode.tabType.onTitleChanged;
               if (titleChangeFunc)
                 titleChangeFunc.call(tab.mode.tabType, tab, tabNode);
 
-              if (this.tabMonitors.length) {
-                for each (let [, tabMonitor] in Iterator(this.tabMonitors))
-                  tabMonitor.onTabTitleChanged(tab);
-              }
+              for each (let [, tabMonitor] in Iterator(this.tabMonitors))
+                tabMonitor.onTabTitleChanged(tab);
 
               tabNode.setAttribute("label", tab.title);
 
               // Update the window title if we're the displayed tab.
               if (iTab == this.tabContainer.selectedIndex)
                 this.setDocumentTitle(tab);
             }
           ]]>
@@ -1007,69 +1065,93 @@
             closeTabItem.setAttribute("disabled", tab.canClose ? "false" : "true");
             return true;
           ]]>
         </body>
       </method>
       <method name="supportsCommand">
         <parameter name="aCommand"/>
         <body>
-           <![CDATA[
-             let tab = this.currentTabInfo;
+          <![CDATA[
+            let tab = this.currentTabInfo;
 
-             // This can happen if we're starting up and haven't got a tab
-             // loaded yet.
-             if (!tab)
-               return false;
+            // This can happen if we're starting up and haven't got a tab
+            // loaded yet.
+            if (!tab)
+              return false;
 
-             let supportsCommandFunc = tab.mode.supportsCommand ||
-                                       tab.mode.tabType.supportsCommand;
-             if (supportsCommandFunc)
-               return supportsCommandFunc.call(tab.mode.tabType, aCommand, tab);
+            for each (let [, tabMonitor] in Iterator(this.tabMonitors)) {
+              if ("supportsCommand" in tabMonitor) {
+                let result = tabMonitor.supportsCommand(aCommand, tab);
+                if (result !== null)
+                  return result;
+              }
+            }
 
-             return false;
-           ]]>
+            let supportsCommandFunc = tab.mode.supportsCommand ||
+                                      tab.mode.tabType.supportsCommand;
+            if (supportsCommandFunc)
+              return supportsCommandFunc.call(tab.mode.tabType, aCommand, tab);
+
+            return false;
+          ]]>
         </body>
       </method>
       <method name="isCommandEnabled">
         <parameter name="aCommand"/>
         <body>
-           <![CDATA[
-             let tab = this.currentTabInfo;
+          <![CDATA[
+            let tab = this.currentTabInfo;
 
-             // This can happen if we're starting up and haven't got a tab
-             // loaded yet.
-             if (!tab)
-               return false;
+            // This can happen if we're starting up and haven't got a tab
+            // loaded yet.
+            if (!tab)
+              return false;
 
-             let isCommandEnabledFunc = tab.mode.isCommandEnabled ||
-                                        tab.mode.tabType.isCommandEnabled;
-             if (isCommandEnabledFunc)
-               return isCommandEnabledFunc.call(tab.mode.tabType, aCommand, tab);
+            for each (let [, tabMonitor] in Iterator(this.tabMonitors)) {
+              if ("isCommandEnabled" in tabMonitor) {
+                let result = tabMonitor.isCommandEnabled(aCommand, tab);
+                if (result !== null)
+                  return result;
+              }
+            }
 
-             return false;
-           ]]>
+            let isCommandEnabledFunc = tab.mode.isCommandEnabled ||
+                                       tab.mode.tabType.isCommandEnabled;
+            if (isCommandEnabledFunc)
+              return isCommandEnabledFunc.call(tab.mode.tabType, aCommand, tab);
+
+            return false;
+          ]]>
         </body>
       </method>
       <method name="doCommand">
         <parameter name="aCommand"/>
         <body>
-           <![CDATA[
-             let tab = this.currentTabInfo;
+          <![CDATA[
+            let tab = this.currentTabInfo;
+
+            // This can happen if we're starting up and haven't got a tab
+            // loaded yet.
+            if (!tab)
+              return;
 
-             // This can happen if we're starting up and haven't got a tab
-             // loaded yet.
-             if (!tab)
-               return;
+            for each (let [, tabMonitor] in Iterator(this.tabMonitors)) {
+              if ("doCommand" in tabMonitor) {
+                let result = tabMonitor.doCommand(aCommand, tab);
+                if (result === true)
+                  return;
+              }
+            }
 
-             let doCommandFunc = tab.mode.doCommand ||
-                                 tab.mode.tabType.doCommand;
-             if (doCommandFunc)
-               doCommandFunc.call(tab.mode.tabType, aCommand, tab);
-           ]]>
+            let doCommandFunc = tab.mode.doCommand ||
+                                tab.mode.tabType.doCommand;
+            if (doCommandFunc)
+              doCommandFunc.call(tab.mode.tabType, aCommand, tab);
+          ]]>
         </body>
       </method>
       <method name="onEvent">
         <parameter name="aEvent"/>
         <body>
            <![CDATA[
              let tab = this.currentTabInfo;
 
@@ -1114,18 +1196,17 @@
   </binding>
 
   <binding id="tabmail-tab" display="xul:box"
            extends="chrome://global/content/bindings/tabbox.xml#tab">
     <content closetabtext="&closeTab.label;">
       <xul:hbox class="tab-image-left" xbl:inherits="selected"/>
       <xul:hbox class="tab-image-middle box-inherit" align="center"
                 xbl:inherits="dir,pack,orient,selected" flex="1">
-        <xul:image class="tab-icon-image"
-                   xbl:inherits="validate,src=image,src"/>
+        <xul:image class="tab-icon-image" xbl:inherits="validate,src=image"/>
         <xul:label class="tab-text"
                   xbl:inherits="value=label,accesskey,crop,disabled"
                   crop="right" flex="1"/>
       </xul:hbox>
       <xul:toolbarbutton anonid="close-button" class="tab-close-button" tabindex="-1"/>
       <xul:hbox class="tab-image-right" xbl:inherits="selected"/>
     </content>
 
@@ -1625,28 +1706,23 @@
         <body><![CDATA[
           var menuItem = aEvent.target.mCorrespondingMenuitem;
           if (menuItem) {
             var attrName = aEvent.attrName;
             switch (attrName) {
               case "label":
               case "crop":
               case "busy":
+              case "image":
               case "selected":
                 if (aEvent.attrChange == aEvent.REMOVAL)
                   menuItem.removeAttribute(attrName);
                 else
                   menuItem.setAttribute(attrName, aEvent.newValue);
             }
-            if (attrName == "busy") {
-              // With busy status changes the tab icon
-              let style = window.getComputedStyle(aEvent.target, null);
-              menuItem.style.listStyleImage = style.listStyleImage;
-              menuItem.style.MozImageRegion = style.MozImageRegion;
-            }
           }
         ]]></body>
       </method>
 
       <method name="_tabOnTabClose">
         <parameter name="aEvent"/>
         <body><![CDATA[
           var menuItem = aEvent.target.mCorrespondingMenuitem;
@@ -1706,21 +1782,17 @@
           var menuItem = document.createElementNS(
             "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", 
             "menuitem");
 
           menuItem.setAttribute("class", "menuitem-iconic alltabs-item");
 
           menuItem.setAttribute("label", aTab.label);
           menuItem.setAttribute("crop", aTab.getAttribute("crop"));
-
-          // Get the tabicon and set it here
-          let style = window.getComputedStyle(aTab, null);
-          menuItem.style.listStyleImage = style.listStyleImage;
-          menuItem.style.MozImageRegion = style.MozImageRegion;
+          menuItem.setAttribute("image", aTab.getAttribute("image"));
 
           if (aTab.hasAttribute("busy"))
             menuItem.setAttribute("busy", aTab.getAttribute("busy"));
           if (aTab.selected)
             menuItem.setAttribute("selected", "true");
 
           // Keep some attributes of the menuitem in sync with its
           // corresponding tab (e.g. the tab label)
--- a/mail/base/jar.mn
+++ b/mail/base/jar.mn
@@ -101,16 +101,19 @@ messenger.jar:
     content/messenger/glodaFacetTab.js              (content/glodaFacetTab.js)
     content/messenger/glodaFacetViewWrapper.xul     (content/glodaFacetViewWrapper.xul)
     content/messenger/glodaFacetView.xhtml          (content/glodaFacetView.xhtml)
     content/messenger/glodaFacetView.js             (content/glodaFacetView.js)
     content/messenger/glodaFacetView.css            (content/glodaFacetView.css)
     content/messenger/glodaFacetBindings.css        (content/glodaFacetBindings.css)
     content/messenger/glodaFacetBindings.xml        (content/glodaFacetBindings.xml)
     content/messenger/glodaFacetVis.js              (content/glodaFacetVis.js)
+    content/messenger/quickFilterBar.xul            (content/quickFilterBar.xul)
+    content/messenger/quickFilterBar.js             (content/quickFilterBar.js)
+    content/messenger/quickFilterBar.css            (content/quickFilterBar.css)
     content/messenger/downloadsOverlay.xul          (content/downloadsOverlay.xul)
 # the following files are mail-specific overrides
 *+  content/messenger/license.html                  (/mozilla/toolkit/content/license.html)
 % override chrome://global/content/license.html chrome://messenger/content/license.html
 
 comm.jar:
 % content communicator %content/communicator/ xpcnativewrappers=yes
 *  content/communicator/contentAreaClick.js         (content/contentAreaClick.js)
--- a/mail/base/modules/Makefile.in
+++ b/mail/base/modules/Makefile.in
@@ -44,16 +44,16 @@ VPATH   = @srcdir@
 include $(DEPTH)/config/autoconf.mk
 
 EXTRA_JS_MODULES = \
   MailConsts.js \
   MailUtils.js \
   attachmentChecker.js \
   dbViewWrapper.js \
   mailViewManager.js \
-  quickSearchManager.js \
+  quickFilterManager.js \
   searchSpec.js \
   MsgHdrSyntheticView.js \
   sessionStoreManager.js \
   mailMigrator.js \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
--- a/mail/base/modules/dbViewWrapper.js
+++ b/mail/base/modules/dbViewWrapper.js
@@ -1083,21 +1083,26 @@ DBViewWrapper.prototype = {
     let [sortType, sortOrder, sortCustomCol] =
       this._getSortDetails(this._sort.length-1);
     let outCount = {};
     // when the underlying folder is a single real folder (virtual or no), we
     //  tell the view about the underlying folder.
     if (this.isSingleFolder) {
       dbView.open(this._underlyingFolders[0], sortType, sortOrder, viewFlags,
                   outCount);
-      // but if it's a virtual folder, we need to tell the db view about the
-      //  the display (virtual) folder so it can store all the view-specific
+      // If there are any search terms, we need to tell the db view about the
+      //  the display (/virtual) folder so it can store all the view-specific
       //  data there (things like the active mail view and such that go in
-      //  dbFolderInfo.)
-      if (this.isVirtual)
+      //  dbFolderInfo.)  This also goes for cases where the quick search is
+      //  active; the C++ code explicitly nulls out the view folder for no
+      //  good/documented reason, so we need to set it again if we want changes
+      //  made with the quick filter applied.  (We don't just change the C++
+      //  code because there could be SeaMonkey fallout.)  See bug 502767 for
+      //  info about the quick-search part of the problem.
+      if (this.search.hasSearchTerms)
         dbView.viewFolder = this.displayedFolder;
     }
     // when we're dealing with a multi-folder virtual folder, we just tell the
     //  db view about the display folder.  (It gets its own XFVF view, so it
     //  knows what to do.)
     // and for a synthetic folder, displayedFolder is null anyways
     else {
       dbView.open(this.displayedFolder, sortType, sortOrder, viewFlags,
new file mode 100644
--- /dev/null
+++ b/mail/base/modules/quickFilterManager.js
@@ -0,0 +1,1344 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Email Client.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Andrew Sutherland <asutherland@asutherland.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const EXPORTED_SYMBOLS = ["QuickFilterState", "QuickFilterManager",
+                          "MessageTextFilter", "QuickFilterSearchListener"];
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/PluralForm.jsm");
+
+Cu.import("resource:///modules/searchSpec.js");
+Cu.import("resource:///modules/iteratorUtils.jsm");
+Cu.import("resource:///modules/errUtils.js");
+
+const Application = Cc["@mozilla.org/steel/application;1"]
+                      .getService(Ci.steelIApplication);
+
+const FocusManager = Cc["@mozilla.org/focus-manager;1"]
+                       .getService(Ci.nsIFocusManager);
+
+const nsMsgSearchAttrib = Components.interfaces.nsMsgSearchAttrib;
+const nsMsgMessageFlags = Components.interfaces.nsMsgMessageFlags;
+const nsMsgSearchOp = Components.interfaces.nsMsgSearchOp;
+
+// XXX we need to know whether the gloda indexer is enabled for upsell reasons,
+// but this should really just be exposed on the main Gloda public interface.
+Cu.import("resource://app/modules/gloda/indexer.js");
+// we need to be able to create gloda message searcher instances for upsells:
+Cu.import("resource://app/modules/gloda/msg_search.js");
+
+
+/**
+ * Shallow object copy.
+ */
+function shallowObjCopy(obj) {
+  let newObj = {};
+  for each (let [key, value] in Iterator(obj)) {
+    newObj[key] = value;
+  }
+  return newObj;
+}
+
+/**
+ * Should the filter be visible when there's no previous state to propagate it
+ *  from?  The idea is that when session persistence is working this should only
+ *  ever affect the first time Thunderbird is started up.  Although opening
+ *  additional 3-panes will likely trigger this unless we go out of our way to
+ *  implement propagation across those boundaries (and we're not).
+ */
+const FILTER_VISIBILITY_DEFAULT = true;
+
+/**
+ * Represents the state of a quick filter bar.  This mainly decorates the
+ *  manipulation of the filter states with support of tracking the filter most
+ *  recently manipulated so we can maintain a very limited undo stack of sorts.
+ */
+function QuickFilterState(aTemplateState, aJsonedState) {
+  if (aJsonedState) {
+    this.filterValues = aJsonedState.filterValues;
+    this.visible = aJsonedState.visible;
+  }
+  else if (aTemplateState) {
+    this.filterValues = QuickFilterManager.propagateValues(
+                          aTemplateState.filterValues);
+    this.visible = aTemplateState.visible;
+  }
+  else {
+    this.filterValues = QuickFilterManager.getDefaultValues();
+    this.visible = FILTER_VISIBILITY_DEFAULT;
+  }
+  this._lastFilterAttr = null;
+}
+QuickFilterState.prototype = {
+  /**
+   * Maps filter names to their current states.  We rely on QuickFilterManager
+   *  to do most of the interesting manipulation of this value.
+   */
+  filterValues: null,
+  /**
+   * Is the filter bar visible?  Always inherited from the template regardless
+   *  of stickyness.
+   */
+  visible: null,
+
+  /**
+   * Get a filter state and update lastFilterAttr appropriately.  This is
+   *  intended for use when the filter state is a rich object whose state
+   *  cannot be updated just by clobbering as provided by |setFilterValue|.
+   *
+   * @param aName The name of the filter we are retrieving.
+   * @param [aNoChange=false] Is this actually a change for the purposes of
+   *     lastFilterAttr purposes?
+   */
+  getFilterValue: function MFS_getFilterValue(aName, aNoChange) {
+    if (!aNoChange)
+      this._lastFilterAttr = aName;
+    return this.filterValues[aName];
+  },
+
+  /**
+   * Set a filter state and update lastFilterAttr appropriately.
+   *
+   * @param aName The name of the filter we are setting.
+   * @param aValue The value to set; null/undefined implies deletion.
+   * @param [aNoChange=false] Is this actually a change for the purposes of
+   *     lastFilterAttr purposes?
+   */
+  setFilterValue: function MFS_setFilterValue(aName, aValue, aNoChange) {
+    if (aValue == null) {
+      delete this.filterValues[aName];
+      return;
+    }
+
+    this.filterValues[aName] = aValue;
+    if (!aNoChange)
+      this._lastFilterAttr = aName;
+  },
+
+  /**
+   * Track the last filter that was affirmatively applied.  If you hit escape
+   *  and this value is non-null, we clear the referenced filter constraint.
+   *  If you hit escape and the value is null, we clear all filters.
+   */
+  _lastFilterAttr: null,
+
+  /**
+   * The user hit escape; based on _lastFilterAttr and whether there are any
+   *  applied filters, change our constraints.  First press clears the last
+   *  added constraint (if any), second press (or if no last constraint) clears
+   *  the state entirely.
+   *
+   * @return true if we relaxed the state, false if there was nothing to relax.
+   */
+  userHitEscape: function MFS_userHitEscape() {
+    if (this._lastFilterAttr) {
+      QuickFilterManager.clearFilterValue(this._lastFilterAttr,
+                                            this.filterValues);
+      this._lastFilterAttr = null;
+      return true;
+    }
+
+    return QuickFilterManager.clearAllFilterValues(this.filterValues);
+  },
+
+  /**
+   * Clear the state without going through any undo-ish steps like
+   *  |userHitEscape| tries to do.
+   */
+  clear: function MFS_clear() {
+    QuickFilterManager.clearAllFilterValues(this.filterValues);
+  },
+
+  /**
+   * Create the search terms appropriate to the current filter states.
+   */
+  createSearchTerms: function MFS_createSearchTerms(aTermCreator) {
+    return QuickFilterManager.createSearchTerms(this.filterValues,
+                                                aTermCreator);
+  },
+
+  persistToObj: function MFS_persistToObj() {
+    return {
+      filterValues: this.filterValues,
+      visible: this.visible,
+    };
+  },
+};
+
+/**
+ * An nsIMsgSearchNotify listener wrapper to facilitate faceting of messages
+ *  being returned by a search.  We have to use a listener because the
+ *  nsMsgDBView includes presentation logic and unless we force all of its
+ *  results to be fully expanded (and dummy headers ignored), we can't get
+ *  at all the messages reliably.
+ *
+ * We need to provide a wrapper so that:
+ * - We can provide better error handling support.
+ * - We can provide better GC support.
+ * - We can ensure the right life-cycle stuff happens (unregister ourselves as
+ *   a listener, namely.)
+ *
+ * It is nice that we have a wrapper so that:
+ * - We can provide context to the thing we are calling that it does not need
+ *  to maintain.
+ *
+ * The listener should implement the following methods:
+ *
+ * - function onSearchStart(aCurState) returning aScratch.
+ *   This function should initialize the scratch object that will be passed to
+ *    onSearchMessage and onSearchDone.  This is an attempt to provide a
+ *    friendly API that provides debugging support by dumping the state of
+ *    said object when things go wrong.
+ *
+ * - function onSearchMessage(aScratch, aMsgHdr, aFolder)
+ *   Processes messages reported as search hits.  Its only context is the
+ *    object you returned from onSearchStart.  Take the hint and try and keep
+ *    this method efficient!  We will catch all exceptions for you and report
+ *    errors.  We will also handle forcing GCs as appropriate.
+ *
+ * - function onSearchDone(aCurState, aScratch, aSuccess) returning
+ *    [new state for your filter, should call reflectInDOM, should treat the
+ *     state as if it is a result of user action].
+ *   This ends up looking exactly the same as the postFilterProcess handler
+ *
+ * @param aFolderDisplay The folder display we are working in service of.
+ * @param aFilterer The QuickFilterState instance.
+ * @param aListener The thing on which we invoke methods.
+ */
+function QuickFilterSearchListener(aFolderDisplay, aFilterer, aFilterDef,
+                                   aListener, aMuxer) {
+  this.folderDisplay = aFolderDisplay;
+  this.filterer = aFilterer;
+  this.filterDef = aFilterDef;
+  this.listener = aListener;
+  this.muxer = aMuxer;
+  this.folderDisplay = aFolderDisplay;
+
+  this.session = aFolderDisplay.view.search.session;
+
+  this.scratch = null;
+  this.count = 0;
+  this.started = false;
+
+  this.session.registerListener(this,
+                                Ci.nsIMsgSearchSession.allNotifications);
+}
+QuickFilterSearchListener.prototype = {
+  onNewSearch: function QuickFilterSearchListener_onNewSearch() {
+    this.started = true;
+    let curState = (this.filterDef.name in this.filterer.filterValues) ?
+                     this.filterer.filterValues[this.filterDef.name] : null;
+    this.scratch = this.listener.onSearchStart(curState);
+  },
+
+  onSearchHit: function QuickFilterSearchListener_onSearchHit(aMsgHdr,
+                                                              aFolder) {
+    // GC sanity demands that we trigger a GC if we have seen a large number
+    //  of headers.  Because we are driven by the search mechanism which likes
+    //  to time-slice when it has a lot of messages on its plate, it is
+    //  conceivable something else may trigger a GC for us.  Unfortunately,
+    //  we can't guarantee it, as XPConnect does not inform memory pressure,
+    //  so it's us to stop-gap it.
+    this.count++;
+    if (!(this.count % 4096))
+      Cu.forceGC();
+
+    try {
+      this.listener.onSearchMessage(this.scratch, aMsgHdr, aFolder);
+    }
+    catch (ex) {
+      logException(ex);
+      logObject(this.scratch, "scratch object");
+    }
+  },
+
+  onSearchDone: function QuickFilterSearchListener_onSearchDone(aStatus) {
+    // it's possible we will see the tail end of an existing search. ignore.
+    if (!this.started)
+      return;
+
+    this.session.unregisterListener(this);
+
+    let curState = (this.filterDef.name in this.filterer.filterValues) ?
+                     this.filterer.filterValues[this.filterDef.name] : null;
+    let [newState, update, treatAsUserAction] =
+      this.listener.onSearchDone(curState, this.scratch, aStatus);
+
+    this.filterer.setFilterValue(this.filterDef.name, newState,
+                                 !treatAsUserAction);
+    if (update && this.folderDisplay.active) {
+     this.muxer.reflectFiltererState(this.filterer, this.folderDisplay,
+                                     this.filterDef.name);
+    }
+  },
+};
+
+/**
+ * Extensible mechanism for defining filters for the quick filter bar.  This
+ * is the spiritual successor to the mailViewManager and quickSearchManager.
+ *
+ * The manager includes and requires UI-relevant metadata for use by its
+ * counterparts in quickFilterBar.js.  New filters are expected to contribute
+ * DOM nodes to the overlay and tell us about them using their id during
+ * registration.
+ *
+ * We support two types of filtery things.
+ * - Filters via defineFilter.
+ * - Text filters via defineTextFilter.  These always take the filter text as
+ *   a parameter.
+ *
+ * If you are an adventurous extension developer and want to add a magic
+ * text filter that does the whole "from:bob to:jim subject:shoes" what you
+ * will want to do is register a normal filter and collapse the normal text
+ * filter text-box.  You add your own text box, etc.
+ */
+let QuickFilterManager = {
+  /**
+   * List of filter definitions, potentially prioritized.
+   */
+  filterDefs: [],
+  /**
+   * Keys are filter definition names, values are the filter defs.
+   */
+  filterDefsByName: {},
+  /**
+   * The DOM id of the text widget that should get focused when the user hits
+   *  control-f or the equivalent.  This is here so it can get clobbered.
+   */
+  textBoxDomId: null,
+
+  /**
+   * Define a new filter.
+   *
+   * Filter states must always be JSON serializable.  A state of undefined means
+   * that we are not persisting any state for your filter.
+   *
+   * @param {String} aFilterDef.name The name of your filter.  This is the name
+   *     of the attribute we cram your state into the state dictionary as, so
+   *     the key thing is that it doesn't conflict with other id's.
+   * @param {String} aFilterDef.domId The id of the DOM node that you have
+   *     overlayed into the quick filter bar.
+   * @param {function(aTermCreator, aTerms, aState)} aFilterDef.appendTerms
+   *     The function to invoke to contribute your terms to the list of
+   *     search terms in aTerms.  Your function will not be invoked if you do
+   *     not have any currently persisted state (as is the case if null or
+   *     undefined was set).  If you have nothing to add, then don't do
+   *     anything.  If you do add terms, the first term you add needs to have
+   *     the booleanAnd flag set to true.  You may optionally return a listener
+   *     that complies with the documentation on QuickFilterSearchListener if
+   *     you want to process all of the messages returned by the filter; doing
+   *     so is not cheap, so don't do that lightly.  (Tag faceting uses this.)
+   * @param {function()} [aFilterDef.getDefaults] Function that returns the
+   *     default state for the filter.  If the function is not defined or the
+   *     returned value is === undefined, no state is set.
+   * @param {function(aTemplState, aSticky)} [aFilterDef.propagateState] A
+   *     function that takes the state from another QuickFilterState instance
+   *     for this definition and propagates it to a new state which it returns.
+   *     You would use this to keep the 'sticky' bits of state that you want to
+   *     persist between folder changes and when new tabs are opened.  The
+   *     aSticky argument tells you if the user wants all the filters still
+   *     applied or not.  When false, the idea is you might keep things like
+   *     which text fields to filter on, but not the text to filter.  When true,
+   *     you would keep the text to filter on too.  Return undefined if you do
+   *     not want any state stored in the new filter state.  If you do not
+   *     define this function and aSticky would be true, we will propagate your
+   *     state verbatim; accordingly functions using rich object state must
+   *     implement this method.
+   * @param {function(aState)} [aFilterDef.clearState] Function to reset the
+   *     the filter's value for the given state, returning a tuple of the new
+   *     state and a boolean flag indicating whether there was actually state to
+   *     clear.  This is used when the user decides to reset the state of the
+   *     filter bar or (just one specific filter).  If omitted, we just delete
+   *     the filter state entirely, so you only need to define this if you have
+   *     some sticky meta-state you want to maintain.  Return undefined for the
+   *     state value if you do not need any state kept around.
+   * @param {function(aDocument, aMuxer, aNode)} [aFilterDef.domBindExtra]
+   *     Function invoked at initial UI binding of the quick filter bar after
+   *     we add a command listener to whatever is identified by domId.  If you
+   *     have additional widgets to hook up, this is where you do it.  aDocument
+   *     and aMuxer are provided to assist in this endeavor.  Use aMuxer's
+   *     getFilterValueForMutation/setFilterValue/updateSearch methods from any
+   *     event handlers you register.
+   * @param {function(aState, aNode, aEvent, aDocument)} [aFilterDef.onCommand]
+   *     If omitted, the default handler assumes your widget has a "checked"
+   *     state that should set your state value to true when checked and delete
+   *     the state when unchecked.  Implement this function if that is not what
+   *     you need.  The function should return a tuple of [new state, should
+   *     update the search] as its result.
+   * @param {function(aDomNode, aFilterValue, aDoc, aMuxer)}
+   *     [aFilterDef.reflectInDOM]
+   *     If omitted, we assume the widget referenced by domId has a checked
+   *     attribute and assign the filter value coerced to a boolean to the
+   *     checked attribute.  Otherwise we call your function and it's up to you
+   *     to reflect your state.  aDomNode is the node referred to by domId.
+   *     This function will be called when the tab changes, folder changes, or
+   *     if we called postFilterProcess and you returned a value !== undefined.
+   * @param {function(aState, aViewWrapper, aFiltering)}
+   *     [aFilterDef.postFilterProcess]
+   *     Invoked after all of the message headers for the view have been
+   *     displayed, allowing your code to perform some kind of faceting or other
+   *     clever logic.  Return a tuple of [new state, should call reflectInDOM,
+   *     should treat as if the user modified the state].  We call this _even
+   *     when there is no filter_ applied.  We tell you what's happening via
+   *     aFiltering; true means we have applied some terms, false means not.
+   *     It's vitally important that you do not just facet things willy nilly
+   *     unless there is expected user payoff and they opted in.  Our tagging UI
+   *     only facets when the user clicked the tag facet.  If you write an
+   *     extension that provides really sweet visualizations or something like
+   *     that and the user installs you knowing what's what, that is also cool,
+   *     we just can't do it in core for now.
+   */
+  defineFilter: function MFM_defineFilter(aFilterDef) {
+    this.filterDefs.push(aFilterDef);
+    this.filterDefsByName[aFilterDef.name] = aFilterDef;
+  },
+
+  /**
+   * Remove a filter from existence by name.  This is for extensions to disable
+   *  existing filters and not a dynamic jetpack-like lifecycle.  It falls to
+   *  the code calling killFilter to deal with the DOM nodes themselves for now.
+   *
+   * @param aName The name of the filter to kill.
+   */
+  killFilter: function MFM_killFilter(aName) {
+    let filterDef = this.filterDefsByName[aName];
+    this.filterDefs.splice(this.filterDefs.indexOf(aName), 1);
+    delete this.filterDefsByName[aName];
+  },
+
+  /**
+   * Propagate values from an existing state into a new state based on
+   *  propagation rules.  For use by QuickFilterState.
+   *
+   * @param aTemplValues A set of existing filterValues.
+   * @return The new filterValues state.
+   */
+  propagateValues: function MFM_propagateValues(aTemplValues) {
+    let values = {};
+    let sticky = ("sticky" in aTemplValues) ? aTemplValues.sticky : false;
+
+    for each (let [, filterDef] in Iterator(this.filterDefs)) {
+      if ("propagateState" in filterDef) {
+        let curValue = (filterDef.name in aTemplValues) ?
+                         aTemplValues[filterDef.name] : undefined;
+        let newValue = filterDef.propagateState(curValue, sticky);
+        if (newValue !== undefined)
+          values[filterDef.name] = newValue;
+      }
+      // always propagate the value if sticky and there was no handler
+      else if (sticky) {
+        if (filterDef.name in aTemplValues)
+          values[filterDef.name] = aTemplValues[filterDef.name];
+      }
+    }
+
+    return values;
+  },
+  /**
+   * Get the set of default filterValues for the current set of defined filters.
+   *
+   * @return Thew new filterValues state.
+   */
+  getDefaultValues: function MFM_getDefaultValues() {
+    let values = {};
+    for each (let [, filterDef] in Iterator(this.filterDefs)) {
+      if ("getDefaults" in filterDef) {
+        let newValue = filterDef.getDefaults();
+        if (newValue !== undefined)
+          values[filterDef.name] = newValue;
+      }
+    }
+    return values;
+  },
+
+  /**
+   * Reset the state of a single filter given the provided values.
+   *
+   * @return true if we actually cleared some state, false if there was nothing
+   *     to clear.
+   */
+  clearFilterValue: function MFM_clearFilterValue(aFilterName, aValues) {
+    let filterDef = this.filterDefsByName[aFilterName];
+    if (!("clearState" in filterDef)) {
+      if (aFilterName in aValues) {
+        delete aValues[aFilterName];
+        return true;
+      }
+      return false;
+    }
+
+    let curValue = (aFilterName in aValues) ?
+                     aValues[aFilterName] : undefined;
+    // Yes, we want to call it to clear its state even if it has no state.
+    let [newValue, didClear] = filterDef.clearState(curValue);
+    if (newValue !== undefined)
+      aValues[aFilterName] = newValue;
+    else
+      delete aValues[aFilterName];
+    return didClear;
+  },
+
+  /**
+   * Reset the state of all filters given the provided values.
+   *
+   * @return true if we actually cleared something, false if there was nothing
+   *     to clear.
+   */
+  clearAllFilterValues: function MFM_clearFilterValues(aFilterValues) {
+    let didClearSomething = false;
+    for each (let [, filterDef] in Iterator(this.filterDefs)) {
+      if (this.clearFilterValue(filterDef.name, aFilterValues))
+        didClearSomething = true;
+    }
+    return didClearSomething;
+  },
+
+  /**
+   * Populate and return a list of search terms given the provided state.
+   *
+   * We only invoke appendTerms on filters that have state in aFilterValues,
+   * as per the contract.
+   */
+  createSearchTerms: function MFM_createSearchTerms(aFilterValues,
+                                                    aTermCreator) {
+    let searchTerms = [], listeners = [];
+    for each (let [filterName, filterValue] in Iterator(aFilterValues)) {
+      let filterDef = this.filterDefsByName[filterName];
+      try {
+        let listener =
+          filterDef.appendTerms(aTermCreator, searchTerms, filterValue);
+        if (listener)
+          listeners.push([listener, filterDef]);
+      }
+      catch(ex) {
+        logException(ex);
+      }
+    }
+    return searchTerms.length ? [searchTerms, listeners] : [null, listeners];
+  }
+};
+
+/**
+ * Meta-filter, just handles whether or not things are sticky.
+ */
+QuickFilterManager.defineFilter({
+  name: "sticky",
+  domId: "qfb-sticky",
+  appendTerms: function(aTermCreator, aTerms, aFilterValue) {
+  },
+  /**
+   * This should not cause an update, otherwise default logic.
+   */
+  onCommand: function(aState, aNode, aEvent, aDocument) {
+    let checked = aNode.checked ? true : null;
+    return [checked, false];
+  },
+});
+
+/**
+ * true: must be unread, false: must be read.
+ */
+QuickFilterManager.defineFilter({
+  name: "unread",
+  domId: "qfb-unread",
+  appendTerms: function(aTermCreator, aTerms, aFilterValue) {
+    let term, value;
+    term = aTermCreator.createTerm();
+    term.attrib = nsMsgSearchAttrib.MsgStatus;
+    value = term.value;
+    value.attrib = term.attrib;
+    value.status = nsMsgMessageFlags.Read;
+    term.value = value;
+    term.op = aFilterValue ? nsMsgSearchOp.Isnt : nsMsgSearchOp.Is;
+    term.booleanAnd = true;
+    aTerms.push(term);
+  }
+});
+
+/**
+ * true: must be starred, false: must not be starred.
+ */
+QuickFilterManager.defineFilter({
+  name: "starred",
+  domId: "qfb-starred",
+  appendTerms: function(aTermCreator, aTerms, aFilterValue) {
+    let term, value;
+    term = aTermCreator.createTerm();
+    term.attrib = nsMsgSearchAttrib.MsgStatus;
+    value = term.value;
+    value.attrib = term.attrib;
+    value.status = nsMsgMessageFlags.Marked;
+    term.value = value;
+    term.op = aFilterValue ? nsMsgSearchOp.Is : nsMsgSearchOp.Isnt;
+    term.booleanAnd = true;
+    aTerms.push(term);
+  }
+});
+
+/**
+ * true: sender must be in a local address book, false: sender must not be.
+ */
+QuickFilterManager.defineFilter({
+  name: "addrBook",
+  domId: "qfb-inaddrbook",
+  appendTerms: function(aTermCreator, aTerms, aFilterValue) {
+    let term, value;
+    let enumerator = Components.classes["@mozilla.org/abmanager;1"]
+                               .getService(Components.interfaces.nsIAbManager)
+                               .directories;
+    let firstBook = true;
+    term = null;
+    while (enumerator.hasMoreElements()) {
+      let addrbook = enumerator.getNext();
+      if (addrbook instanceof Components.interfaces.nsIAbDirectory &&
+          !addrbook.isRemote) {
+        term = aTermCreator.createTerm();
+        term.attrib = Components.interfaces.nsMsgSearchAttrib.Sender;
+        value = term.value;
+        value.attrib = term.attrib;
+        value.str = addrbook.URI;
+        term.value = value;
+        term.op = aFilterValue ? nsMsgSearchOp.IsInAB : nsMsgSearchOp.IsntInAB;
+        // It's an AND if we're the first book (so the boolean affects the
+        //  group as a whole.)
+        // It's the negation of whether we're filtering otherwise; demorgans.
+        term.booleanAnd = firstBook || !aFilterValue;
+        term.beginsGrouping = firstBook;
+        aTerms.push(term);
+        firstBook = false;
+      }
+    }
+    if (term)
+      term.endsGrouping = true;
+  }
+});
+
+/**
+ * It's a tag filter that sorta facets! Stealing gloda's thunder! Woo!
+ *
+ * Filter on message tags?  Meanings:
+ * - true: Yes, must have at least one tag on it.
+ * - false: No, no tags on it!
+ * - dictionary where keys are tag keys and values are tri-state with null
+ *    meaning don't constraint, true meaning yes should be present, false
+ *    meaning no, don't be present
+ */
+let TagFacetingFilter = {
+  name: "tags",
+  domId: "qfb-tags",
+
+  /**
+   * @return true if the constaint is only on has tags/does not have tags,
+   *     false if there are specific tag constraints in play.
+   */
+  isSimple: function(aFilterValue) {
+    // it's the simple case if the value is just a boolean
+    if (typeof(aFilterValue) != "object")
+      return true;
+    // but also if the object contains no true values
+    let simpleCase = true;
+    for each (let [key, value] in Iterator(aFilterValue)) {
+      if (value !== null) {
+        simpleCase = false;
+        break;
+      }
+    }
+    return simpleCase;
+  },
+
+  /**
+   * Because we support both inclusion and exclusion we can produce up to two
+   *  groups.  One group for inclusion, one group for exclusion.  To get listed
+   *  you only need to include one of the tags marked for inclusion, but you
+   *  must not have any of the tags marked for exclusion.
+   */
+  appendTerms: function TFF_appendTerms(aTermCreator, aTerms, aFilterValue) {
+    let term, value;
+
+    if (aFilterValue == null)
+      return null;
+
+    // just the true/false case
+    if (this.isSimple(aFilterValue)) {
+      term = aTermCreator.createTerm();
+      term.attrib = Components.interfaces.nsMsgSearchAttrib.Keywords;
+      value = term.value;
+      value.str = "";
+      term.value = value;
+      term.op = aFilterValue ?
+                  Components.interfaces.nsMsgSearchOp.IsntEmpty :
+                  Components.interfaces.nsMsgSearchOp.IsEmpty;
+      term.booleanAnd = true;
+      aTerms.push(term);
+
+      // we need to perform faceting if the value is literally true.
+      if (aFilterValue === true)
+        return this;
+    }
+    else {
+      let firstIncludeClause = true, firstExcludeClause = true;
+      let lastIncludeTerm = null;
+      term = null;
+
+      let excludeTerms = [];
+
+      for each (let [key, shouldFilter] in Iterator(aFilterValue)) {
+        if (shouldFilter !== null) {
+          term = aTermCreator.createTerm();
+          term.attrib = Components.interfaces.nsMsgSearchAttrib.Keywords;
+          value = term.value;
+          value.attrib = term.attrib;
+          value.str = key;
+          term.value = value;
+          if (shouldFilter) {
+            term.op = nsMsgSearchOp.Contains;
+            // AND for the group, but OR inside the group
+            term.booleanAnd = firstIncludeClause;
+            term.beginsGrouping = firstIncludeClause;
+            aTerms.push(term);
+            firstIncludeClause = false;
+            lastIncludeTerm = term;
+          }
+          else {
+            term.op = nsMsgSearchOp.DoesntContain;
+            // you need to not include all of the tags marked excluded.
+            term.booleanAnd = true;
+            term.beginsGrouping = firstExcludeClause;
+            excludeTerms.push(term);
+            firstExcludeClause = false;
+          }
+        }
+      }
+      if (lastIncludeTerm)
+        lastIncludeTerm.endsGrouping = true;
+
+      // if we have any exclude terms:
+      // - we might need to add a "has a tag" clause if there were no explicit
+      //   inclusions.
+      // - extend the exclusions list in.
+      if (excludeTerms.length) {
+        // (we need to add has a tag)
+        if (!lastIncludeTerm) {
+          term = aTermCreator.createTerm();
+          term.attrib = Components.interfaces.nsMsgSearchAttrib.Keywords;
+          value = term.value;
+          value.str = "";
+          term.value = value;
+          term.op = Components.interfaces.nsMsgSearchOp.IsntEmpty;
+          term.booleanAnd = true;
+          aTerms.push(term);
+        }
+
+        // (extend in the exclusions)
+        excludeTerms[excludeTerms.length-1].endsGrouping = true;
+        aTerms.push.apply(aTerms, excludeTerms);
+      }
+    }
+
+    return null;
+  },
+
+  onSearchStart: function(aCurState) {
+    // this becomes aKeywordMap; we want to start with an empty one
+    return {};
+  },
+  onSearchMessage: function(aKeywordMap, aMsgHdr, aFolder) {
+    let keywords = aMsgHdr.getStringProperty("keywords");
+    let keywordList = keywords.split(' ');
+    for (let iKeyword = 0; iKeyword < keywordList.length; iKeyword++) {
+      let keyword = keywordList[iKeyword];
+      aKeywordMap[keyword] = null;
+    }
+  },
+  onSearchDone: function(aCurState, aKeywordMap, aStatus) {
+    // we are an async operation; if the user turned off the tag facet already,
+    //  then leave that state intact...
+    if (aCurState == null)
+      return [null, false, false];
+
+    // only propagate things that are actually tags though!
+    let outKeyMap = {};
+    let tagService = Cc["@mozilla.org/messenger/tagservice;1"]
+                       .getService(Ci.nsIMsgTagService);
+    let tags = tagService.getAllTags({});
+    let tagCount = tags.length;
+    for (let iTag=0; iTag < tagCount; iTag++) {
+      let tag = tags[iTag];
+
+      if (tag.key in aKeywordMap)
+        outKeyMap[tag.key] = aKeywordMap[tag.key];
+    }
+
+    return [outKeyMap, true, false];
+  },
+
+  /**
+   * We need to clone our state if it's an object to avoid bad sharing.
+   */
+  propagateState: function(aOld, aSticky) {
+    // stay disabled when disabled
+    if (aOld == null)
+      return null;
+    if (this.isSimple(aOld))
+      return aOld ? true : false; // could be an object, need to convert.
+    return shallowObjCopy(aOld);
+  },
+
+  /**
+   * Default behaviour but:
+   * - We collapse our expando if we get unchecked.
+   * - We want to initiate a faceting pass if we just got checked.
+   */
+  onCommand: function(aState, aNode, aEvent, aDocument) {
+    let checked = aNode.checked ? true : null;
+    if (!checked)
+      aDocument.getElementById("quick-filter-bar-tab-bar").collapsed = true;
+
+    // return ourselves if we just got checked to have
+    //  onSearchStart/onSearchMessage/onSearchDone get to do their thing.
+    return [checked, true];
+  },
+
+  reflectInDOM: function TFF_reflectInDOM(aNode, aFilterValue,
+                                          aDocument, aMuxer) {
+    aNode.checked = aFilterValue ? true : false;
+
+    if ((aFilterValue != null) &&
+        (typeof(aFilterValue) == "object"))
+      this._populateTagBar(aFilterValue, aDocument, aMuxer);
+    else
+      aDocument.getElementById("quick-filter-bar-tab-bar").collapsed = true;
+  },
+
+  _populateTagBar: function TFF__populateTagMenu(aState, aDocument, aMuxer) {
+    let tagbar = aDocument.getElementById("quick-filter-bar-tab-bar");
+    let keywordMap = aState;
+
+    function commandHandler(aEvent) {
+      let tagKey = aEvent.target.getAttribute("value");
+      let state = aMuxer.getFilterValueForMutation(TagFacetingFilter.name);
+      state[tagKey] = aEvent.target.checked ? true : null;
+      aEvent.target.removeAttribute("inverted");
+      aMuxer.updateSearch();
+    };
+
+    function rightClickHandler(aEvent) {
+      // Only do something if this is a right-click, otherwise commandHandler
+      //  will pick up on it.
+      if (aEvent.button == 2) {
+        // we need to toggle the checked state ourselves
+        aEvent.target.checked = !aEvent.target.checked;
+
+        let tagKey = aEvent.target.getAttribute("value");
+        let state = aMuxer.getFilterValueForMutation(TagFacetingFilter.name);
+        state[tagKey] = aEvent.target.checked ? false : null;
+        if (aEvent.target.checked)
+          aEvent.target.setAttribute("inverted", "true");
+        else
+          aEvent.target.removeAttribute("inverted");
+        aMuxer.updateSearch();
+        aEvent.stopPropagation();
+        aEvent.preventDefault();
+      }
+    }
+
+    // -- nuke existing exposed tags
+    while (tagbar.lastChild)
+      tagbar.removeChild(tagbar.lastChild);
+
+    let addCount = 0;
+
+    // -- create an element for each tag
+    let tagService = Components.classes["@mozilla.org/messenger/tagservice;1"]
+                           .getService(Components.interfaces.nsIMsgTagService);
+    let tags = tagService.getAllTags({});
+    let tagCount = tags.length;
+    for (let iTag=0; iTag < tagCount; iTag++) {
+      let tag = tags[iTag];
+
+      if (tag.key in keywordMap) {
+        addCount++;
+
+        // Keep in mind that the XBL does not get built for dynamically created
+        //  elements such as these until they get displayed, which definitely
+        //  means not before we append it into the tree.
+        let button = aDocument.createElement("toolbarbutton");
+
+        button.setAttribute("id", "qfb-tag-" + tag.key);
+        button.addEventListener("command", commandHandler, false);
+        button.addEventListener("click", rightClickHandler, false);
+        button.setAttribute("type", "checkbox");
+        if (keywordMap[tag.key] !== null) {
+          button.setAttribute("checked", "true");
+          if (!keywordMap[tag.key])
+            button.setAttribute("inverted", "true");
+        }
+        button.setAttribute("label", tag.tag);
+        button.setAttribute("value", tag.key);
+        let color = tag.color;
+        // everybody always gets to be an qfb-tag-button.
+        if (color)
+          button.setAttribute("class", "qfb-tag-button lc-" + color.substr(1));
+        else
+          button.setAttribute("class", "qfb-tag-button");
+        tagbar.appendChild(button);
+      }
+    }
+
+    tagbar.collapsed = !addCount;
+  },
+};
+QuickFilterManager.defineFilter(TagFacetingFilter);
+
+/**
+ * true: must have attachment, false: must not have attachment.
+ */
+QuickFilterManager.defineFilter({
+  name: "attachment",
+  domId: "qfb-attachment",
+  appendTerms: function(aTermCreator, aTerms, aFilterValue) {
+    let term, value;
+    term = aTermCreator.createTerm();
+    term.attrib = Components.interfaces.nsMsgSearchAttrib.MsgStatus;
+    value = term.value;
+    value.attrib = term.attrib;
+    value.status = Components.interfaces.nsMsgMessageFlags.Attachment;
+    term.value = value;
+    term.op = aFilterValue ? nsMsgSearchOp.Is : nsMsgSearchOp.Isnt;
+    term.booleanAnd = true;
+    aTerms.push(term);
+  }
+});
+
+/**
+ * The traditional quick-search text filter now with added gloda upsell!  We
+ * are mildly extensible in case someone wants to add more specific text filter
+ * criteria to toggle, but otherwise are intended to be taken out of the
+ * picture entirely by extensions implementing more featureful text searches.
+ *
+ * Our state looks like {text: "", states: {a: true, b: false}} where a and b
+ * are text filters.
+ */
+let MessageTextFilter = {
+  name: "text",
+  domId: "qfb-qs-textbox",
+  /**
+   * Parse the string into terms/phrases by finding matching double-quotes.  If
+   * we find a quote that doesn't have a friend, we assume the user was going
+   * to put a quote at the end of the string.  (This is important because we
+   * update using a timer and this results in stable behavior.)
+   *
+   * This code is cloned from gloda's msg_search.js and known good (enough :).
+   * I did change the friendless quote situation, though.
+   *
+   * @param aSearchString The phrase to parse up.
+   * @return A list of terms.
+   */
+  _parseSearchString: function MTF__parseSearchString(aSearchString) {
+    aSearchString = aSearchString.trim();
+    let terms = [];
+
+    /*
+     * Add the term as long as the trim on the way in didn't obliterate it.
+     *
+     * In the future this might have other helper logic; it did once before.
+     */
+    function addTerm(aTerm) {
+      if (aTerm)
+        terms.push(aTerm);
+    }
+
+    while (aSearchString) {
+      if (aSearchString[0] == '"') {
+        let endIndex = aSearchString.indexOf(aSearchString[0], 1);
+        // treat a quote without a friend as making a phrase containing the
+        // rest of the string...
+        if (endIndex == -1) {
+          endIndex = aSearchString.length;
+        }
+
+        addTerm(aSearchString.substring(1, endIndex).trim());
+        aSearchString = aSearchString.substring(endIndex + 1);
+        continue;
+      }
+
+      let spaceIndex = aSearchString.indexOf(" ");
+      if (spaceIndex == -1) {
+        addTerm(aSearchString);
+        break;
+      }
+
+      addTerm(aSearchString.substring(0, spaceIndex));
+      aSearchString = aSearchString.substring(spaceIndex+1);
+    }
+
+    return terms;
+  },
+
+  /**
+   * For each search phrase, build a group that contains all our active text
+   *  filters OR'ed together.  So if the user queries for 'foo bar' with
+   *  sender and recipient enabled, we build:
+   * ("foo" sender OR "foo" recipient) AND ("bar" sender OR "bar" recipient)
+   */
+  appendTerms: function(aTermCreator, aTerms, aFilterValue) {
+    let term, value;
+
+    if (aFilterValue.text) {
+      let phrases = this._parseSearchString(aFilterValue.text);
+      for each (let [, phrase] in Iterator(phrases)) {
+        let firstClause = true;
+        term = null;
+        for each (let [tfName, tfValue] in Iterator(aFilterValue.states)) {
+          if (!tfValue)
+            continue;
+          let tfDef = this.textFilterDefs[tfName];
+
+          term = aTermCreator.createTerm();
+          term.attrib = tfDef.attrib;
+          value = term.value;
+          value.attrib = tfDef.attrib;
+          value.str = phrase;
+          term.value = value;
+          term.op = nsMsgSearchOp.Contains;
+          // AND for the group, but OR inside the group
+          term.booleanAnd = firstClause;
+          term.beginsGrouping = firstClause;
+          aTerms.push(term);
+          firstClause = false;
+        }
+        if (term)
+          term.endsGrouping = true;
+      }
+    }
+  },
+  getDefaults: function() {
+    let states = {};
+    for each (let [name, value] in Iterator(this._defaultStates)) {
+      states[name] = value;
+    }
+    return {
+      text: null,
+      states: states,
+    };
+  },
+  propagateState: function(aOld, aSticky) {
+    return {
+      text: aSticky ? aOld.text : null,
+      states: shallowObjCopy(aOld.states),
+    };
+  },
+  clearState: function(aState) {
+    let hadState = (aState.text && aState.text != "");
+    aState.text = null;
+    return [aState, hadState];
+  },
+
+  /**
+   * We need to create and bind our expando-bar toggle buttons.  We also need to
+   *  add a special down keypress handler that escapes the textbox into the
+   *  thread pane.
+   */
+  domBindExtra: function MessageTextFilter_domBind(aDocument, aMuxer, aNode) {
+    // -- platform-dependent emptytext setup
+    aNode.setAttribute(
+      "emptytext",
+      aNode.getAttribute("emptytextbase")
+           .replace("#1", aNode.getAttribute(Application.platformIsMac ?
+                                             "keyLabelMac" : "keyLabelNonMac")));
+
+    // -- Keypresses for focus transferral and upsell
+    aNode.addEventListener("keypress", function(aEvent) {
+      // - Down key into the thread pane
+      if (aEvent.keyCode == aEvent.DOM_VK_DOWN) {
+        let threadPane = aDocument.getElementById("threadTree");
+        // focusing does not actually select the row...
+        threadPane.focus();
+        // ...so explicitly select the current index.
+        threadPane.view.selection.select(threadPane.currentIndex);
+        return false;
+      }
+      // - Enter when upsell is actively proposed...
+      else if (aEvent.keyCode == aEvent.DOM_VK_ENTER) {
+      }
+      return true;
+    }, false);
+
+    // -- Blurring kills upsell.
+    aNode.addEventListener("blur", function(aEvent) {
+      let panel = aDocument.getElementById("qfb-text-search-upsell");
+      if ((FocusManager.activeWindow != aDocument.defaultView ||
+           aDocument.commandDispatcher.focusedElement != aNode.inputField) &&
+          panel.state == "open") {
+        panel.hidePopup();
+      }
+    }, true);
+
+    // -- Expando Buttons!
+    function commandHandler(aEvent) {
+      let state = aMuxer.getFilterValueForMutation(MessageTextFilter.name);
+      let filterDef = MessageTextFilter.textFilterDefsByDomId[aEvent.target.id];
+      state.states[filterDef.name] = aEvent.target.checked;
+      aMuxer.updateSearch();
+    }
+
+    for each (let [, textFilter] in Iterator(this.textFilterDefs)) {
+      aDocument.getElementById(textFilter.domId).addEventListener(
+        "command", commandHandler, false);
+    }
+  },
+
+  onCommand: function(aState, aNode, aEvent, aDocument) {
+    let text = aNode.value.length ? aNode.value : null;
+    if (text == aState.text) {
+      let upsell = aDocument.getElementById("qfb-text-search-upsell");
+      if (upsell.state == "open") {
+        upsell.hidePopup();
+        let tabmail = aDocument.getElementById("tabmail");
+        tabmail.openTab("glodaFacet", {
+                          searcher: new GlodaMsgSearcher(null, aState.text)
+                        });
+      }
+      return [aState, false];
+    }
+
+    aState.text = text;
+    aDocument.getElementById("quick-filter-bar-filter-text-bar").collapsed =
+      (text == null);
+    return [aState, true];
+  },
+
+  reflectInDOM: function MessageTextFilter_reflectInDOM(aNode, aFilterValue,
+                                                        aDocument, aMuxer,
+                                                        aFromPFP) {
+    if (aFromPFP == "nosale") {
+      let panel = aDocument.getElementById("qfb-text-search-upsell");
+      if (panel.state != "closed")
+        panel.hidePopup();
+      return;
+    }
+    if (aFromPFP == "upsell") {
+      let panel = aDocument.getElementById("qfb-text-search-upsell");
+      let line1 = aDocument.getElementById("qfb-upsell-line-one");
+      let line2 = aDocument.getElementById("qfb-upsell-line-two");
+      line1.value = line1.getAttribute("fmt").replace("#1", aFilterValue.text);
+      line2.value = line2.getAttribute("fmt").replace("#1", aFilterValue.text);
+
+      if (panel.state == "closed" &&
+          aDocument.commandDispatcher.focusedElement == aNode.inputField) {
+        let filterBar = aDocument.getElementById("quick-filter-bar");
+        //panel.sizeTo(filterBar.clientWidth - 20, filterBar.clientHeight - 20);
+        panel.openPopup(filterBar, "after_end", -7, 7, false, true);
+      }
+      return;
+    }
+
+    // Make sure we have no visible upsell on state change while our textbox
+    //  retains focus.
+    let panel = aDocument.getElementById("qfb-text-search-upsell");
+    if (panel.state != "closed")
+      panel.hidePopup();
+
+    // Update the text
+    aNode.value = aFilterValue.text;
+
+    // Update our expando buttons
+    let states = aFilterValue.states;
+    for each (let [, textFilter] in Iterator(this.textFilterDefs)) {
+      aDocument.getElementById(textFilter.domId).checked =
+        states[textFilter.name];
+    }
+
+    // Show the expando?
+    aDocument.getElementById("quick-filter-bar-filter-text-bar").collapsed =
+      (aFilterValue.text == null);
+  },
+
+  /**
+   * In order to do our upsell we need to know when we are not getting any
+   *  results.
+   */
+  postFilterProcess: function MessageTextFilter_postFilterProcess(aState,
+                                                                  aViewWrapper,
+                                                                  aFiltering) {
+    // If we're not filtering, not filtering on text, there are results, or
+    //  gloda is not enabled so upselling makes no sense, then bail.
+    // (Currently we always return "nosale" to make sure our panel is closed;
+    //  this might be overkill but unless it becomes a performance problem, it
+    //  keeps us safe from weird stuff.)
+    if (!aFiltering || !aState.text || aViewWrapper.dbView.numMsgsInView ||
+        !GlodaIndexer.enabled)
+      return [aState, "nosale", false];
+
+    // since we're filtering, filtering on text, and there are no results, tell
+    //  the upsell code to get bizzay
+    return [aState, "upsell", false];
+  },
+
+  /** maps text filter names to whether they are enabled by default (bool)  */
+  _defaultStates: {},
+  /** maps text filter name to text filter def */
+  textFilterDefs: {},
+  /** maps dom id to text filter def */
+  textFilterDefsByDomId: {},
+  defineTextFilter: function MessageTextFilter_defineTextFilter(aTextDef) {
+    this.textFilterDefs[aTextDef.name] = aTextDef;
+    this.textFilterDefsByDomId[aTextDef.domId] = aTextDef;
+    if (aTextDef.defaultState)
+      this._defaultStates[aTextDef.name] = true;
+  },
+};
+// Note that we definitely want this filter defined AFTER the cheap message
+// status filters, so don't reorder this invocation willy nilly.
+QuickFilterManager.defineFilter(MessageTextFilter);
+QuickFilterManager.textBoxDomId = "qfb-qs-textbox";
+
+MessageTextFilter.defineTextFilter({
+  name: "sender",
+  domId: "qfb-qs-sender",
+  attrib: nsMsgSearchAttrib.Sender,
+  defaultState: true,
+});
+MessageTextFilter.defineTextFilter({
+  name: "recipients",
+  domId: "qfb-qs-recipients",
+  attrib: nsMsgSearchAttrib.ToOrCC,
+  defaultState: true,
+});
+MessageTextFilter.defineTextFilter({
+  name: "subject",
+  domId: "qfb-qs-subject",
+  attrib: nsMsgSearchAttrib.Subject,
+  defaultState: true,
+});
+MessageTextFilter.defineTextFilter({
+  name: "body",
+  domId: "qfb-qs-body",
+  attrib: nsMsgSearchAttrib.Body,
+  defaultState: false,
+});
+
+/**
+ * We need to be parameterized by folder/muxer to provide update notifications
+ * and this is the cleanest way given the current FolderDisplayWidget assumption
+ * that everyone knows the window they are in already.
+ */
+function ResultsLabelFolderDisplayListener(aMuxer) {
+  this.muxer = aMuxer;
+}
+ResultsLabelFolderDisplayListener.prototype = {
+  _update: function ResultsLabelFolderDisplayListener__update(aFolderDisplay) {
+    let filterer = aFolderDisplay._tabInfo._ext.quickFilter;
+    if (!filterer)
+      return;
+    let oldCount = ("results" in filterer.filterValues) ?
+                     filterer.filterValues.results : null;
+    // (we only display the tally when the filter is active; don't change that)
+    if (oldCount == null)
+      return;
+    let newCount = aFolderDisplay.view.dbView.numMsgsInView;
+    if (oldCount == newCount)
+      return;
+    filterer.setFilterValue("results", newCount, true);
+    if (aFolderDisplay.active)
+      this.muxer.reflectFiltererState(filterer, aFolderDisplay, "results");
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// FolderDisplayListener
+
+  // We want to make sure that anything that would change the count of displayed
+  //  messages causes us to update our dislayed value.
+
+  onMessageCountsChanged: function(aFolderDisplay) {
+    this._update(aFolderDisplay);
+  },
+
+  onMessagesRemoved: function(aFolderDisplay) {
+    this._update(aFolderDisplay);
+  },
+};
+
+/**
+ * The results label says whether there were any matches and, if so, how many.
+ */
+QuickFilterManager.defineFilter({
+  name: "results",
+  domId: "qfb-results-label",
+  appendTerms: function(aTermCreator, aTerms, aFilterValue) {
+  },
+  /**
+   * Hook us up as a folder display listener so we can get information on when
+   * the counts change.
+   */
+  domBindExtra: function MessageTextFilter_domBind(aDocument, aMuxer, aNode) {
+    aDocument.defaultView.FolderDisplayListenerManager.registerListener(
+      new ResultsLabelFolderDisplayListener(aMuxer));
+  },
+  reflectInDOM: function MessageTextFilter_reflectInDOM(aNode, aFilterValue,
+                                                        aDocument) {
+    if (aFilterValue == null) {
+      aNode.value = "";
+      aNode.style.visibility = "hidden";
+    }
+    else if (aFilterValue == 0) {
+      aNode.value = aNode.getAttribute("noresultsstring");
+      aNode.style.visibility = "visible";
+    }
+    else {
+      let fmtstring = aNode.getAttribute("somefmtstring");
+
+      aNode.value = PluralForm.get(aFilterValue, fmtstring)
+                              .replace("#1", aFilterValue.toString());
+      aNode.style.visibility = "visible";
+    }
+  },
+  /**
+   * We slightly abuse the filtering hook to figure out how many messages there
+   *  are and whether a filter is active.  What makes this reasonable is that
+   *  a more complicated widget that visualized the results as a timeline would
+   *  definitely want to be hooked up like this.  (Although they would want
+   *  to implement propagateState since the state they store would be pretty
+   *  expensive.)
+   */
+  postFilterProcess: function TFF_postFilterProcess(aState, aViewWrapper,
+                                                    aFiltering) {
+    return [aFiltering ? aViewWrapper.dbView.numMsgsInView : null, true, false];
+  },
+});
deleted file mode 100644
--- a/mail/base/modules/quickSearchManager.js
+++ /dev/null
@@ -1,262 +0,0 @@
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Mozilla Communicator client code, released
- * March 31, 1998.
- *
- * The Initial Developer of the Original Code is
- * Netscape Communications Corporation.
- * Portions created by the Initial Developer are Copyright (C) 1998-1999
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *   Seth Spitzer <sspitzer@netscape.com>
- *   Scott MacGregor <mscott@mozilla.org>
- *   David Bienvenu <bienvenu@nventure.com>
- *   Andrew Sutherland <asutherland@asutherland.org>
- *   Thomas Düllmann <bugzilla2009@duellmann24.net>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either of the GNU General Public License Version 2 or later (the "GPL"),
- * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-/*
- * This file mimics mailViewManager.js.  It shares the same idiom of creating
- *  lists of search terms, and so is really quite similar.  Except the term
- *  Manager is all wrong; however, we keep it for parallel construction and
- *  ease of searching.
- */
-
-const EXPORTED_SYMBOLS = ['QuickSearchManager', 'QuickSearchConstants'];
-
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
-
-Cu.import("resource:///modules/errUtils.js");
-
-try {
-  Cu.import("resource:///modules/StringBundle.js");
-} catch (e) {
-  logException(e);
-}
-
-const nsMsgSearchScope = Ci.nsMsgSearchScope;
-const nsMsgSearchAttrib = Ci.nsMsgSearchAttrib;
-const nsMsgSearchOp = Ci.nsMsgSearchOp;
-
-/**
- * Constants originally found in searchBar.js bundled together into a single
- *  name-space contribution.
- *
- * These constants are used by quick-search-menupopup.  The state of
- *  quick-search-menupopup is persisted to localstore.rdf, so new values need
- *  new constants.
- */
-var QuickSearchConstants = {
-  kQuickSearchSubjectFromOrRecipient: 0,
-  kQuickSearchFromOrSubject: 1,
-  kQuickSearchRecipientOrSubject: 2,
-  kQuickSearchSubject: 3,
-  kQuickSearchFrom: 4,
-  kQuickSearchRecipient: 5,
-  kQuickSearchBody: 6,
-  kQuickSearchEntireMessage: 7
-};
-const kQuickSearchCount = 8;
-
-var QuickSearchLabels = null; // populated dynamically from properties files
-
-/**
- * All quick search logic that takes us from a search string (and search mode)
- *  to a set of search terms goes in here.  Check out FolderDisplayWidget for
- *  display concerns involving views, or DBViewWrapper and SearchSpec for the
- *  actual nsIMsgDBView-related logic.
- */
-var QuickSearchManager = {
-
-  _modeLabels: {},
-
-  /**
-   * Populate an associative array containing the labels from a properties file
-   */
-  loadLabels: function QuickSearchManager_loadLabels() {
-    const quickSearchStrings =
-      new StringBundle("chrome://messenger/locale/quickSearch.properties");
-    this._modeLabels[QuickSearchConstants.kQuickSearchSubject] =
-      quickSearchStrings.get("searchSubject.label");
-    this._modeLabels[QuickSearchConstants.kQuickSearchFrom] =
-      quickSearchStrings.get("searchFrom.label");
-    this._modeLabels[QuickSearchConstants.kQuickSearchFromOrSubject] =
-      quickSearchStrings.get("searchFromOrSubject.label");
-    this._modeLabels[QuickSearchConstants.kQuickSearchRecipient] =
-      quickSearchStrings.get("searchRecipient.label");
-    this._modeLabels[QuickSearchConstants.kQuickSearchRecipientOrSubject] =
-      quickSearchStrings.get("searchRecipientOrSubject.label");
-    this._modeLabels[QuickSearchConstants.kQuickSearchBody] =
-      quickSearchStrings.get("searchMsgBody.label");
-    this._modeLabels[QuickSearchConstants.kQuickSearchSubjectFromOrRecipient] =
-      quickSearchStrings.get("searchSubjectFromOrRecipient.label");
-    this._modeLabels[QuickSearchConstants.kQuickSearchEntireMessage] =
-      quickSearchStrings.get("searchEntireMessage.label");
-
-  },
-
-  /**
-   * Create the structure that the UI needs to fully describe a quick search
-   * mode.
-   *
-   * @return a list of array objects mapping 'value' to the constant specified
-   * in QuickSearchConstants, and 'label' to a localized string.
-   */
-  getSearchModes: function QuickSearchManager_getSearchModes() {
-    let modes = [];
-    for (let i = 0; i < kQuickSearchCount; i++)
-      modes.push({'value': i, 'label': this._modeLabels[i]});
-    return modes;
-  },
-
-  /**
-   * Create the search terms for the given quick-search configuration.  This is
-   *  intended to basically be directly used in the service of the UI without
-   *  pre-processing.  If you want to add extra logic, probably add it in here
-   *  (with appropriate refactoring.)
-   * Callers should strongly consider using DBViewWrapper's search attribute
-   *  (which is a SearchSpec)'s quickSearch method which in turn calls us.  The
-   *  DBViewWrapper may in turn be embedded in a FolderDisplayWidget.  So an
-   *  example usage might be:
-   *
-   * gFolderDisplay.view.search.quickSearch(
-   *   QuickSearchConstants.kQuickSearchSubject, "foo|bar");
-   *
-   * @param aTermCreator A nsIMsgSearchSession or other interface with a
-   *     createTerm method.
-   * @param aSearchMode One of the QuickSearchConstants.kQuickSearch* search
-   *     mode constants specifying what parts of the message to search on.
-   * @param aSearchString The search string, consisting of sub-strings delimited
-   *     by '|' to be OR-ed together.  Given the string "foo" we search for
-   *     messages containing "foo".  Given the string "foo|bar", we search for
-   *     messages containing "foo" or "bar".
-   * @return a list of nsIMsgSearch term instances representing the search as
-   *     defined by the arguments.
-   */
-  createSearchTerms: function QuickSearchManager_createSearchTerms(
-      aTermCreator, aSearchMode, aSearchString) {
-    let searchTerms = [];
-    let termList = aSearchString.split("|");
-    for (var i = 0; i < termList.length; i ++)
-    {
-      // if the term is empty, skip it
-      if (termList[i] == "")
-        continue;
-
-      // create, fill, and append the terms for subject etc.
-      let term;
-      let value;
-
-      // if our search criteria is subject or subject|from etc. then add a term for
-      // the subject
-      if (aSearchMode == QuickSearchConstants.kQuickSearchSubject ||
-          aSearchMode == QuickSearchConstants.kQuickSearchFromOrSubject ||
-          aSearchMode == QuickSearchConstants.kQuickSearchRecipientOrSubject ||
-          aSearchMode == QuickSearchConstants.kQuickSearchSubjectFromOrRecipient ||
-          aSearchMode == QuickSearchConstants.kQuickSearchEntireMessage)
-      {
-        term = aTermCreator.createTerm();
-        value = term.value;
-        value.str = termList[i];
-        term.value = value;
-        term.attrib = nsMsgSearchAttrib.Subject;
-        term.op = nsMsgSearchOp.Contains;
-        term.booleanAnd = false;
-        searchTerms.push(term);
-      }
-
-      // create, fill, and append a term for the body
-      if (aSearchMode == QuickSearchConstants.kQuickSearchBody ||
-          aSearchMode == QuickSearchConstants.kQuickSearchEntireMessage)
-      {
-        // what do we do for news and imap users that aren't configured for offline use?
-        // in these cases the body search will never return any matches. Should we try to
-        // see if body is a valid search scope in this particular case before doing the search?
-        // should we switch back to a subject/from search behind the scenes?
-        term = aTermCreator.createTerm();
-        value = term.value;
-        value.str = termList[i];
-        term.value = value;
-        term.attrib = nsMsgSearchAttrib.Body;
-        term.op = nsMsgSearchOp.Contains;
-        term.booleanAnd = false;
-        searchTerms.push(term);
-      }
-
-      // create, fill, and append a term for from
-      if (aSearchMode == QuickSearchConstants.kQuickSearchFrom ||
-          aSearchMode == QuickSearchConstants.kQuickSearchFromOrSubject)
-      {
-        term = aTermCreator.createTerm();
-        value = term.value;
-        value.str = termList[i];
-        term.value = value;
-        term.attrib = nsMsgSearchAttrib.Sender;
-        term.op = nsMsgSearchOp.Contains;
-        term.booleanAnd = false;
-        searchTerms.push(term);
-      }
-
-      // create, fill, and append a term for the recipient
-      // XXX: need to include bcc here
-      if (aSearchMode == QuickSearchConstants.kQuickSearchRecipient ||
-          aSearchMode == QuickSearchConstants.kQuickSearchRecipientOrSubject)
-      {
-        term = aTermCreator.createTerm();
-        value = term.value;
-        value.str = termList[i];
-        term.value = value;
-        term.attrib = nsMsgSearchAttrib.ToOrCC;
-        term.op = nsMsgSearchOp.Contains;
-        term.booleanAnd = false;
-        searchTerms.push(term);
-      }
-
-      // create, fill, and append the AllAddresses term
-      if (aSearchMode == QuickSearchConstants.kQuickSearchSubjectFromOrRecipient ||
-          aSearchMode == QuickSearchConstants.kQuickSearchEntireMessage)
-      {
-        term = aTermCreator.createTerm();
-        value = term.value;
-        value.str = termList[i];
-        term.value = value;
-        term.attrib = nsMsgSearchAttrib.AllAddresses;
-        term.op = nsMsgSearchOp.Contains;
-        term.booleanAnd = false;
-        searchTerms.push(term);
-      }
-    }
-
-    return searchTerms.length ? searchTerms : null;
-  }
-};
-
-QuickSearchManager.loadLabels();
--- a/mail/base/modules/searchSpec.js
+++ b/mail/base/modules/searchSpec.js
@@ -38,17 +38,16 @@
 const EXPORTED_SYMBOLS = ['SearchSpec'];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource:///modules/iteratorUtils.jsm");
-Cu.import("resource:///modules/quickSearchManager.js");
 
 const nsMsgSearchScope = Ci.nsMsgSearchScope;
 const nsIMsgSearchTerm = Ci.nsIMsgSearchTerm;
 const nsIMsgLocalMailFolder = Ci.nsIMsgLocalMailFolder;
 const nsMsgFolderFlags = Ci.nsMsgFolderFlags;
 const nsMsgSearchAttrib = Ci.nsMsgSearchAttrib;
 
 const NS_MSG_SEARCH_INTERRUPTED = 0x00550002;
@@ -131,19 +130,22 @@ SearchSpec.prototype = {
 
       if (this.owner.isSynthetic) {
         this.owner._syntheticView.search(new FilteringSyntheticListener(this));
       }
       else {
         if (!this._sessionListener)
           this._sessionListener = new SearchSpecListener(this);
 
-        this.session.registerListener(aDBView);
+        this.session.registerListener(aDBView,
+                                      Ci.nsIMsgSearchSession.allNotifications);
         aDBView.searchSession = this._session;
-        this._session.registerListener(this._sessionListener);
+        this._session.registerListener(this._sessionListener,
+                                       Ci.nsIMsgSearchSession.onNewSearch |
+                                       Ci.nsIMsgSearchSession.onSearchDone);
         this._listenersRegistered = true;
 
         this.owner.searching = true;
         this.session.search(this.owner.listener.msgWindow);
       }
     }
     // if it's synthetic but we have no search terms, hook the output of the
     //  synthetic view directly up to the search nsIMsgDBView
@@ -180,17 +182,18 @@ SearchSpec.prototype = {
 
   /**
    * Given a list of terms, mutate them so that they form a single boolean
    *  group.
    *
    * @param aTerms The search terms
    * @param aCloneTerms Do we need to clone the terms?
    */
-  _groupifyTerms: function SearchSpec__groupifyTerms(aTerms, aCloneTerms) {
+  _flattenGroupifyTerms: function SearchSpec__flattenGroupifyTerms(aTerms,
+                                                                   aCloneTerms){
     let iTerm = 0, term;
     let outTerms = aCloneTerms ? [] : aTerms;
     for (term in fixIterator(aTerms, Ci.nsIMsgSearchTerm)) {
       if (aCloneTerms) {
         let cloneTerm = this.session.createTerm();
         cloneTerm.value = term.value;
         cloneTerm.attrib = term.attrib;
         cloneTerm.arbitraryHeader = term.arbitraryHeader;
@@ -199,36 +202,100 @@ SearchSpec.prototype = {
         cloneTerm.op = term.op;
         cloneTerm.booleanAnd = term.booleanAnd;
         cloneTerm.matchAll = term.matchAll;
         term = cloneTerm;
         outTerms.push(term);
       }
       if (iTerm == 0) {
         term.beginsGrouping = true;
+        term.endsGrouping = false;
         term.booleanAnd = true;
       }
+      else {
+        term.beginsGrouping = false;
+        term.endsGrouping = false;
+      }
       iTerm++;
     }
     if (term)
       term.endsGrouping = true;
 
     return outTerms;
   },
 
   /**
+   * Normalize the provided list of terms so that all of the 'groups' in it are
+   *  ANDed together.  If any OR clauses are detected outside of a group, we
+   *  defer to |_flattenGroupifyTerms| to force the terms to be bundled up into
+   *  a single group, maintaining the booleanAnd state of terms.
+   *
+   * This particular logic is desired because it allows the quick filter bar to
+   *  produce interesting and useful filters.
+   *
+   * @param aTerms The search terms
+   * @param aCloneTerms Do we need to clone the terms?
+   */
+  _groupifyTerms: function SearchSpec__groupifyTerms(aTerms, aCloneTerms) {
+    let term;
+    let outTerms = aCloneTerms ? [] : aTerms;
+    let inGroup = false;
+    for (term in fixIterator(aTerms, Components.interfaces.nsIMsgSearchTerm)) {
+      // If we're in a group, all that is forbidden is the creation of new
+      // groups.
+      if (inGroup) {
+        if (term.beginsGrouping) // forbidden!
+          return this._flattenGroupifyTerms(aTerms, aCloneTerms);
+        else if (term.endsGrouping)
+          inGroup = false;
+      }
+      // If we're not in a group, the boolean must be AND.  It's okay for a group
+      // to start.
+      else {
+        // If it's not an AND then it needs to be in a group and we use the other
+        //  function to take care of it.  (This function can't back up...)
+        if (!term.booleanAnd)
+          return this._flattenGroupifyTerms(aTerms, aCloneTerms);
+
+        inGroup = term.beginsGrouping;
+      }
+
+      if (aCloneTerms) {
+        let cloneTerm = this.session.createTerm();
+        cloneTerm.attrib = term.attrib;
+        cloneTerm.value = term.value;
+        cloneTerm.arbitraryHeader = term.arbitraryHeader;
+        cloneTerm.hdrProperty = term.hdrProperty;
+        cloneTerm.customId = term.customId;
+        cloneTerm.op = term.op;
+        cloneTerm.booleanAnd = term.booleanAnd;
+        cloneTerm.matchAll = term.matchAll;
+        cloneTerm.beginsGrouping = term.beginsGrouping;
+        cloneTerm.endsGrouping = term.endsGrouping;
+        term = cloneTerm;
+        outTerms.push(term);
+      }
+    }
+
+    return outTerms;
+  },
+
+  /**
    * Set search terms that are defined by the 'view', which translates to that
    *  weird combo-box that lets you view your unread messages, messages by tag,
    *  messages that aren't deleted, etc.
    *
    * @param aViewTerms The list of terms.  We take ownership and mutate it.
    */
   set viewTerms(aViewTerms) {
     if (aViewTerms)
       this._viewTerms = this._groupifyTerms(aViewTerms);
+    // if they are nulling out already null values, do not apply view changes!
+    else if (this._viewTerms === null)
+      return;
     else
       this._viewTerms = null;
     this.owner._applyViewChanges();
   },
   /**
    * @return the view terms currently in effect.  Do not mutate this.
    */
   get viewTerms() {
@@ -242,16 +309,19 @@ SearchSpec.prototype = {
    *     do not mutate yours.
    */
   set virtualFolderTerms(aVirtualFolderTerms) {
     if (aVirtualFolderTerms)
       // we need to clone virtual folder terms because they are pulled from a
       //  persistent location rather than created on demand
       this._virtualFolderTerms = this._groupifyTerms(aVirtualFolderTerms,
                                                      true);
+    // if they are nulling out already null values, do not apply view changes!
+    else if (this._virtualFolderTerms === null)
+      return;
     else
       this._virtualFolderTerms = null;
     this.owner._applyViewChanges();
   },
   /**
    * @return the Virtual folder terms currently in effect.  Do not mutate this.
    */
   get virtualFolderTerms() {
@@ -263,44 +333,30 @@ SearchSpec.prototype = {
    *  augmented with the 'context' search terms potentially provided by
    *  viewTerms and virtualFolderTerms.
    *
    * @param aUserTerms The list of terms.  We take ownership and mutate it.
    */
   set userTerms(aUserTerms) {
     if (aUserTerms)
       this._userTerms = this._groupifyTerms(aUserTerms);
+    // if they are nulling out already null values, do not apply view changes!
+    else if (this._userTerms === null)
+      return;
     else
       this._userTerms = null;
     this.owner._applyViewChanges();
   },
   /**
    * @return the user terms currently in effect as set via the |userTerms|
    *     attribute or via the |quickSearch| method.  Do not mutate this.
    */
   get userTerms() {
     return this._userTerms;
   },
-  /**
-   * Apply a quick-search for the given search mode using the given search
-   *  string.  All of the hard work is done by
-   *  QuickSearchManager.createSearchTerms; we mainly just assign the result to
-   *  our userTerms property.
-   *
-   * @param aSearchMode One of the QuickSearchConstants.kQuickSearch* search
-   *     mode constants specifying what parts of the message to search on.
-   * @param aSearchString The search string, consisting of sub-strings delimited
-   *     by '|' to be OR-ed together.  Given the string "foo" we search for
-   *     messages containing "foo".  Given the string "foo|bar", we search for
-   *     messages containing "foo" or "bar".
-   */
-  quickSearch: function SearchSpec_quickSearch(aSearchMode, aSearchString) {
-    this.userTerms = QuickSearchManager.createSearchTerms(
-      this.session, aSearchMode, aSearchString);
-  },
 
   clear: function SearchSpec_clear() {
     if (this.hasSearchTerms) {
       this._viewTerms = null;
       this._virtualFolderTerms = null;
       this._userTerms = null;
       this.owner._applyViewChanges();
     }
@@ -473,75 +529,38 @@ SearchSpec.prototype = {
     for each (let [, folder] in Iterator(this.owner._underlyingFolders)) {
       s += '      ' + folder.prettyName + '\n';
     }
     return s;
   },
 };
 
 /**
- * An nsIMsgSearchNotify listener for searches, primarily to keep the UI
- *  up-to-date.  The db view itself always gets added as a listener and does
- *  the heavy lifting.
- *
- * The one notable thing we do is help single-folder virtual folders out by
- *  tracking and updating their total and unread message counts.  Our logic
- *  is simple and is not clever enough to deal with the user reading messages
- *  as they are displayed.  However, this is not a major issue for single-folder
- *  searches because they should complete very quickly.  (Note: I am documenting
- *  reality here, not implementing and rationalizing.)
+ * A simple nsIMsgSearchNotify listener that only listens for search start/stop
+ *  so that it can tell the DBViewWrapper when the search has completed.
  */
 function SearchSpecListener(aSearchSpec) {
   this.searchSpec = aSearchSpec;
 }
 SearchSpecListener.prototype = {
   onNewSearch: function SearchSpecListener_onNewSearch() {
     // searching should already be true by the time this happens.  if it's not,
     //  it means some code is poking at the search session.  bad!
     if (!this.searchSpec.owner.searching) {
-      dump("Search originated from unknown initiator! Confusion!\n");
+      Cu.reportErrror("Search originated from unknown initiator! Confusion!");
       this.searchSpec.owner.searching = true;
     }
-
-    // we track total/unread messages to help out single-folder virtual folders
-    this.totalMessages = 0;
-    this.unreadMessages = 0;
   },
 
   onSearchHit: function SearchSpecListener_onSearchHit(aMsgHdr, aFolder) {
-    this.totalMessages++;
-    if (!aMsgHdr.isRead)
-      this.unreadMessages++;
+    // this method is never invoked!
   },
 
   onSearchDone: function SearchSpecListener_onSearchDone(aStatus) {
-    let viewWrapper = this.searchSpec.owner;
-    let folder = viewWrapper.displayedFolder;
-
-    // Save message counts if it's a virtual folder and there are no additional
-    //  constraints contaminating the virtual folder results.
-    // The old code did this every time, for both cross-folder (multi-folder)
-    //  virtual folders and single-folder backed virtual folders.  However,
-    //  nsMsgXFVirtualFolderDBView already does this (and does a better job of
-    //  it), so we only do this for single virtual folders.
-    if (viewWrapper.isVirtual && viewWrapper.isSingleFolder) {
-      let msgDatabase = folder.msgDatabase;
-      if (msgDatabase) {
-        let dbFolderInfo = msgDatabase.dBFolderInfo;
-        dbFolderInfo.numUnreadMessages = this.unreadMessages;
-        dbFolderInfo.numMessages = this.totalMessages;
-        // passing true compels it to use the new message counts we just set.
-        // this call also flushes to the folder cache.
-        folder.updateSummaryTotals(true);
-        const MSG_DB_LARGE_COMMIT = 1;
-        msgDatabase.Commit(MSG_DB_LARGE_COMMIT);
-      }
-    }
-
-    viewWrapper.searching = false;
+    this.searchSpec.owner.searching = false;
   },
 };
 
 /**
  * Pretend to implement the nsIMsgSearchNotify interface, checking all matches
  *  we are given against the search session on the search spec.  If they pass,
  *  relay them to the underlying db view, otherwise quietly eat them.
  * This is what allows us to use mail-views and quick searches against
--- a/mail/base/test/unit/resources/viewWrapperTestUtils.js
+++ b/mail/base/test/unit/resources/viewWrapperTestUtils.js
@@ -32,17 +32,16 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 Components.utils.import("resource:///modules/dbViewWrapper.js");
 Components.utils.import("resource:///modules/mailViewManager.js");
-Components.utils.import("resource:///modules/quickSearchManager.js");
 Components.utils.import("resource:///modules/virtualFolderWrapper.js");
 
 /**
  * Do initialization for xpcshell-tests; not used by
  *  test-folder-display-helpers.js, our friendly mozmill test helper.
  */
 function initViewWrapperTestUtils(aInjectionConfig) {
   gMessageGenerator = new MessageGenerator();
@@ -238,22 +237,16 @@ function async_view_open(aViewWrapper, a
 }
 
 function async_view_set_mail_view(aViewWrapper, aMailViewIndex, aData) {
   aViewWrapper.listener.pendingLoad = true;
   aViewWrapper.setMailView(aMailViewIndex, aData);
   return false;
 }
 
-function async_view_quick_search(aViewWrapper, aSearchMode, aSearchString) {
-  aViewWrapper.listener.pendingLoad = true;
-  aViewWrapper.search.quickSearch(aSearchMode, aSearchString);
-  return false;
-}
-
 function async_view_refresh(aViewWrapper) {
   aViewWrapper.listener.pendingLoad = true;
   aViewWrapper.refresh();
   return false;
 }
 
 function async_view_group_by_sort(aViewWrapper, aGroupBySort) {
   aViewWrapper.listener.pendingLoad = true;
--- a/mail/base/test/unit/test_viewWrapper_realFolder.js
+++ b/mail/base/test/unit/test_viewWrapper_realFolder.js
@@ -470,179 +470,16 @@ function test_real_folder_mail_views_cus
   yield async_view_set_mail_view(viewWrapper, "Has Attachments");
   verify_messages_in_view(setAttach, viewWrapper);
 
   let [setMoreAttach, setMoreNoAttach] =
     make_new_sets_in_folder(folder, [attachSetDef, noAttachSetDef]);
   verify_messages_in_view([setAttach, setMoreAttach], viewWrapper);
 }
 
-/* ===== Real Folder, Quick Search ===== */
-
-/*
- * Quick-search testing is aligned according to the UI exposure, even though
- *  this is arguably some redundancy from a coverage perspective.
- *
- * The message generator does not generate strings containing "foo", "bar", or
- *  "baz" on its own, so it is okay to use these as test things ourselves.
- */
-
-function test_real_folder_quick_search_subject () {
-  let viewWrapper = make_view_wrapper();
-
-  // add a "foo" set and a "bar" set
-  let [folder, setFoo, setBar] = make_folder_with_sets([
-    {subject: "foo is a word"}, {subject: "bar is also a word"}]);
-  yield async_view_open(viewWrapper, folder);
-
-  // quick-search on 'foo'
-  yield async_view_quick_search(viewWrapper, QuickSearchConstants.kQuickSearchSubject,
-                                 "foo");
-  verify_messages_in_view(setFoo, viewWrapper);
-
-  // quick-search on 'foo' or 'bar'
-  yield async_view_quick_search(viewWrapper, QuickSearchConstants.kQuickSearchSubject,
-                                 "foo|bar");
-  verify_messages_in_view([setFoo, setBar], viewWrapper);
-
-  // add a "bar foo" set (matches) and a "nopers" set (should not match)
-  let [setFooBar, setNopers] = make_new_sets_in_folder(folder, [
-    {subject: "bar foo"}, {subject: "nopers"}]);
-  verify_messages_in_view([setFoo, setBar, setFooBar], viewWrapper);
-}
-
-function test_real_folder_quick_search_subject_or_from () {
-  let viewWrapper = make_view_wrapper();
-
-  let whoFoo = make_person_with_word_in_name("foo");
-  let whoBar = make_person_with_word_in_address("bar");
-  let [folder, subjFoo, fromFoo, subjBar, fromBar, setNopers] =
-    make_folder_with_sets([{subject: "foo"}, {from: whoFoo},
-                           {subject: "bar"}, {from: whoBar},
-                           {}]);
-  yield async_view_open(viewWrapper, folder);
-
-  // search on "foo"
-  yield async_view_quick_search(viewWrapper, QuickSearchConstants.kQuickSearchFromOrSubject,
-                                 "foo");
-  verify_messages_in_view([subjFoo, fromFoo], viewWrapper);
-
-  // search on "foo" or "bar"
-  yield async_view_quick_search(viewWrapper, QuickSearchConstants.kQuickSearchFromOrSubject,
-                                 "foo|bar");
-  verify_messages_in_view([subjFoo, fromFoo, subjBar, fromBar], viewWrapper);
-}
-
-function test_real_folder_quick_search_to_or_cc () {
-  let viewWrapper = make_view_wrapper();
-
-  let whoFoo = make_person_with_word_in_name("foo");
-  let whoBar = make_person_with_word_in_address("bar");
-  let [folder, toFoo, ccFoo, toBar, ccBar, setNopers] =
-    make_folder_with_sets([{to: [whoFoo]}, {cc: [whoFoo]},
-                           {to: [whoBar]}, {cc: [whoBar]},
-                           {}]);
-  yield async_view_open(viewWrapper, folder);
-
-  // search on "foo"
-  yield async_view_quick_search(viewWrapper, QuickSearchConstants.kQuickSearchRecipient,
-                                 "foo");
-  verify_messages_in_view([toFoo, ccFoo], viewWrapper);
-
-  // search on "foo" or "bar"
-  yield async_view_quick_search(viewWrapper, QuickSearchConstants.kQuickSearchRecipient,
-                                 "foo|bar");
-  verify_messages_in_view([toFoo, ccFoo, toBar, ccBar], viewWrapper);
-}
-
-function test_real_folder_quick_search_subject_to_or_cc () {
-  let viewWrapper = make_view_wrapper();
-
-  let whoFoo = make_person_with_word_in_name("foo");
-  let whoBar = make_person_with_word_in_address("bar");
-  let [folder, subjFoo, toFoo, ccFoo, subjBar, toBar, ccBar, setNopers] =
-    make_folder_with_sets([{subject: "foo"}, {to: [whoFoo]}, {cc: [whoFoo]},
-                           {subject: "bar"}, {to: [whoBar]}, {cc: [whoBar]},
-                           {}]);
-  yield async_view_open(viewWrapper, folder);
-
-  // search on "foo"
-  yield async_view_quick_search(viewWrapper,
-    QuickSearchConstants.kQuickSearchRecipientOrSubject, "foo");
-  verify_messages_in_view([subjFoo, toFoo, ccFoo], viewWrapper);
-
-  // search on "foo" or "bar"
-  yield async_view_quick_search(viewWrapper,
-    QuickSearchConstants.kQuickSearchRecipientOrSubject, "foo|bar");
-  verify_messages_in_view([subjFoo, toFoo, ccFoo, subjBar, toBar, ccBar],
-                          viewWrapper);
-}
-
-/**
- * The UI says "entire message", but we search the body.
- */
-function test_real_folder_quick_search_body () {
-  let viewWrapper = make_view_wrapper();
-
-  let [folder, bodyFoo, bodyBar] =
-    make_folder_with_sets([{body: {body: "foo"}},
-                           {body: {body: "bar"}},
-                           {}]);
-  yield async_view_open(viewWrapper, folder);
-
-  // search on "foo"
-  yield async_view_quick_search(viewWrapper, QuickSearchConstants.kQuickSearchBody,
-                                 "foo");
-  verify_messages_in_view(bodyFoo, viewWrapper);
-
-  // search on "bar"
-  yield async_view_quick_search(viewWrapper, QuickSearchConstants.kQuickSearchBody,
-                                 "foo|bar");
-  verify_messages_in_view([bodyFoo, bodyBar], viewWrapper);
-}
-
-/* ===== Real Folder, Mail View and Quick Search Together */
-
-/*
- * No need to be exhaustive here, just prove the machinery works with a mail
- *  view and a quick search at the same time.
- */
-
-/**
- * We'll use the unread mail view with a subject quick search.
- */
-function test_real_folder_mail_view_and_quick_search() {
-  let viewWrapper = make_view_wrapper();
-
-  let [folder, fooOne, fooTwo, noneOne] = make_folder_with_sets([
-    {subject: "foo 1"}, {subject: "foo 2"}, {}]);
-
-  // everything is unread to start with
-  yield async_view_open(viewWrapper, folder);
-  yield async_view_set_mail_view(viewWrapper, MailViewConstants.kViewItemUnread);
-  yield async_view_quick_search(viewWrapper, QuickSearchConstants.kQuickSearchSubject,
-                          "foo");
-  verify_messages_in_view([fooOne, fooTwo], viewWrapper);
-
-  // add some more things (unread!), make sure they appear. #2
-  let [fooThree, noneTwo] = make_new_sets_in_folder(folder, [
-    {subject: "foo 3"}, {}]);
-  verify_messages_in_view([fooOne, fooTwo, fooThree], viewWrapper);
-
-  // make some things read, make sure they disappear. #3 (after refresh)
-  fooTwo.setRead(true);
-  yield async_view_refresh(viewWrapper); // refresh to get the messages to disappear
-  verify_messages_in_view([fooOne, fooThree], viewWrapper);
-
-  // make those things un-read again. #2
-  fooTwo.setRead(false);
-  yield async_view_refresh(viewWrapper); // QUICKSEARCH-VIEW-LIMITATION-REMOVE
-  verify_messages_in_view([fooOne, fooTwo, fooThree], viewWrapper);
-}
-
 /* ===== Real Folder, Special Views ===== */
 
 function test_real_folder_special_views_threads_with_unread() {
   let viewWrapper = make_view_wrapper();
   let folder = make_empty_folder();
 
   // create two maximally nested threads and add them to the folder.
   const count = 10;
@@ -743,24 +580,16 @@ var tests = [
   test_real_folder_mail_views_tags,
   test_real_folder_mail_views_not_deleted,
   // - mail views: test the custom views
   test_real_folder_mail_views_custom_people_i_know,
   test_real_folder_mail_views_custom_recent_mail,
   test_real_folder_mail_views_custom_last_5_days,
   test_real_folder_mail_views_custom_not_junk,
   test_real_folder_mail_views_custom_has_attachments,
-  // - quick search
-  test_real_folder_quick_search_subject,
-  test_real_folder_quick_search_subject_or_from,
-  test_real_folder_quick_search_to_or_cc,
-  test_real_folder_quick_search_subject_to_or_cc,
-  test_real_folder_quick_search_body,
-  // - mail views with quick search
-  test_real_folder_mail_view_and_quick_search,
   // - special views
   test_real_folder_special_views_threads_with_unread,
   test_real_folder_special_views_persist,
   // (we cannot test the watched threads with unread case in local folders)
   test_real_folder_mark_read_on_exit,
 ];
 
 function run_test() {
--- a/mail/base/test/unit/test_viewWrapper_virtualFolder.js
+++ b/mail/base/test/unit/test_viewWrapper_virtualFolder.js
@@ -352,127 +352,16 @@ function test_virtual_folder_mail_views_
   // make those things un-read again.
   fooTwo.setRead(false);
   // I thought this was a quick search limitation, but XFVF needs it to, at
   //  least for the unread case.
   yield async_view_refresh(viewWrapper);
   verify_messages_in_view([fooOne, fooTwo, fooThree], viewWrapper);
 }
 
-
-/* ===== Virtual Folder, Quick Search  ===== */
-
-/*
- * We do not need to test all of the quick search permutations, realFolder
- *  already did that.  We just need to make sure that quick search works
- *  with single-folder and multi-folder virtual folders.  Additionally, we
- *  need to make sure that it works for simple and complex virtual folders.
- * We handle the single and multi folder cases with the same test code, just
- *  parameterized over the number of folders.  We use 4 folders for the multi
- *  folder case to encourage the time slicing logic to split itself up.
- */
-
-function test_virtual_folder_param_quick_search_simple(aNumFolders) {
-  let viewWrapper = make_view_wrapper();
-
-  // venn it up.  virtual folder search on "foo", actual search will be on "bar"
-  let [folders, subjFooBar, subjFoo, subjBar, nopers] =
-    make_folders_with_sets(aNumFolders,
-      [{subject: "foo bar"}, {subject: "foo"}, {subject: "bar"}, {}]);
-  let virt = make_virtual_folder(folders, {subject: "foo"});
-  yield async_view_open(viewWrapper, virt);
-  verify_messages_in_view([subjFooBar, subjFoo], viewWrapper);
-
-  yield async_view_quick_search(viewWrapper,
-                                QuickSearchConstants.kQuickSearchSubject,
-                                "bar");
-  verify_messages_in_view([subjFooBar], viewWrapper);
-}
-
-function test_virtual_folder_param_quick_search_complex(aNumFolders) {
-  let viewWrapper = make_view_wrapper();
-
-  let whoBaz = make_person_with_word_in_address("baz");
-
-  // virtual folder is on "foo" and "baz"
-  // quick search is on "bar"
-  let [folders, fooBarBaz, fooBar, fooBaz, foo, barBaz, bar, baz, nopers] =
-    make_folders_with_sets(aNumFolders,
-      [{subject: "foo bar", from: whoBaz}, {subject: "foo bar"},
-       {subject: "foo", from: whoBaz}, {subject: "foo"},
-       {subject: "bar", from: whoBaz}, {subject: "bar"},
-       {from: whoBaz}, {}]);
-  let virt = make_virtual_folder(folders, {subject: "foo", from: "baz"}, true);
-  yield async_view_open(viewWrapper, virt);
-  verify_messages_in_view([fooBarBaz, fooBaz], viewWrapper);
-
-  yield async_view_quick_search(viewWrapper,
-                                QuickSearchConstants.kQuickSearchSubject,
-                                "bar");
-  verify_messages_in_view([fooBarBaz], viewWrapper);
-}
-
-/* ===== Virtual Folder, Mail View and Quick Search ===== */
-
-/**
- * Test the complex intersection of all three search clauses for result
- *  retrieval and that deletion correctly removes rows.  Check that clones
- *  end up operating the same too.
- */
-function test_virtual_folder_param_mail_view_and_quick_search(aNumFolders) {
-  let viewWrapper = make_view_wrapper();
-
-  // virtual folder search on "foo"
-  // quick search on "bar"
-  let [folders, fooBarOne, fooBarTwo, foo, bar, nopers] =
-    make_folders_with_sets(aNumFolders,
-      [{subject: "foo bar 1"}, {subject: "foo bar 2"}, {subject: "foo"},
-       {subject: "bar"}, {}]);
-  let virt = make_virtual_folder(folders, {subject: "foo"});
-  yield async_view_open(viewWrapper, virt);
-  // mailview on unread
-  yield async_view_set_mail_view(viewWrapper, MailViewConstants.kViewItemUnread);
-  yield async_view_quick_search(viewWrapper,
-                                QuickSearchConstants.kQuickSearchSubject,
-                                "bar");
-  verify_messages_in_view([fooBarOne, fooBarTwo], viewWrapper);
-
-  // clone and make sure the clone has the same results before/after refresh
-  let clonedWrapper = clone_view_wrapper(viewWrapper);
-  verify_messages_in_view([fooBarOne, fooBarTwo], clonedWrapper);
-  yield async_view_refresh(clonedWrapper);
-  verify_messages_in_view([fooBarOne, fooBarTwo], clonedWrapper);
-
-  // - Mark a set read so only one set remains.
-  fooBarTwo.setRead(true);
-  // We need to refresh to actually see the change (views do not make things
-  //  disappear out from under the user for attribute changes.)
-  yield async_view_refresh(viewWrapper);
-  verify_messages_in_view(fooBarOne, viewWrapper);
-  // (and make sure the clone sees this change too)
-  yield async_view_refresh(clonedWrapper);
-  verify_messages_in_view(fooBarOne, clonedWrapper);
-
-  // Make another clone to make sure the deleted notification shows up even
-  //  without a refresh triggering the creation of a new search session.
-  let doubleClone = clone_view_wrapper(clonedWrapper);
-
-  // - Delete some messages
-  // This should result in both views getting a messages removed notification.
-  gMockViewWrapperListener.messagesRemovedEventCount = 0;
-  yield async_delete_messages(fooBarOne);
-  // (thrice for each folder because we have three views listening)
-  do_check_eq(gMockViewWrapperListener.messagesRemovedEventCount,
-              aNumFolders * 3);
-
-  verify_messages_in_view([], viewWrapper);
-  verify_messages_in_view([], clonedWrapper);
-  verify_messages_in_view([], doubleClone);
-}
-
 var tests = [
   // -- single-folder backed virtual folder
   test_virtual_folder_single_load_no_pred,
   test_virtual_folder_single_load_simple_pred,
   test_virtual_folder_single_load_complex_pred,
   test_virtual_folder_single_load_after_load,
   // -- multi-folder backed virtual folder
   test_virtual_folder_multi_load_no_pred,
@@ -485,18 +374,13 @@ var tests = [
   // -- mixture of single-backed and multi-backed
   test_virtual_folder_combo_load_after_load,
   // -- ignore things we should ignore
   test_virtual_folder_filters_out_servers,
   // -- rare/edge cases!
   test_virtual_folder_underlying_folder_deleted,
   // -- mail views (parameterized)
   parameterizeTest(test_virtual_folder_mail_views_unread, [1, 4]),
-  // -- quick search (parameterized for single and multi folder cases)
-  parameterizeTest(test_virtual_folder_param_quick_search_simple, [1, 4]),
-  parameterizeTest(test_virtual_folder_param_quick_search_complex, [1, 4]),
-  // -- mail view with quick search
-  parameterizeTest(test_virtual_folder_param_mail_view_and_quick_search,[1, 4]),
 ];
 
 function run_test() {
   async_run_tests(tests);
 }
--- a/mail/installer/removed-files.in
+++ b/mail/installer/removed-files.in
@@ -213,16 +213,17 @@ isp/sl/gmail.rdf
 isp/sv-SE/gmail.rdf
 isp/tr/gmail.rdf
 isp/uk/gmail.rdf
 isp/zh-CN/gmail.rdf
 isp/zh-TW/gmail.rdf
 license.html
 LICENSE.txt
 modules/JSON.jsm
+modules/quickSearchManager.js
 #ifndef MOZ_IPC
 mozilla-runtime@BIN_SUFFIX@
 #endif
 #ifdef XP_MACOSX
 ../Plug-Ins/PrintPDE.plugin/Contents/Info.plist
 ../Plug-Ins/PrintPDE.plugin/Contents/MacOS/PrintPDE
 ../Plug-Ins/PrintPDE.plugin/Contents/Resources/English.lproj/Localizable.strings
 ../Plug-Ins/PrintPDE.plugin/Contents/Resources/English.lproj/PrintPDE.nib/classes.nib
--- a/mail/locales/en-US/chrome/messenger/messenger.dtd
+++ b/mail/locales/en-US/chrome/messenger/messenger.dtd
@@ -692,18 +692,45 @@ you can use these alternative items. Oth
 <!ENTITY reportPhishingError1.label "This message doesn't appear to be a scam.">
 
 <!-- MDN Bar -->
 <!ENTITY mdnBarMessage.label "The sender of this message has asked to be notified when you read this message. Do you wish to notify the sender?">
 <!ENTITY mdnBarIgnoreButton.label "Ignore Request">
 <!ENTITY mdnBarSendButton.label "Send Receipt">
 
 <!-- Quick Search Bar -->
+<!-- LOCALIZATION NOTE (quickSearchCmd.key):
+     This is actually the key used for the global message search box; we have
+     not changed 
+     -->
 <!ENTITY quickSearchCmd.key "k">
-<!ENTITY searchAllMessages.label "Search all messages">
+<!-- LOCALIZATION NOTE (searchAllMessages.label.base):
+     This is the base of the empty text for the global search box.  We replace
+     #1 with the contents of the appropriate
+     searchAllMessages.keyLabel.* value for the platform.
+     The goal is to convey to the user that typing in the box will allow them
+     to search for messages globally and that there is a hotkey they can press
+     to get to the box faster.  If the global indexer is disabled, the search
+     box will be collapsed and the user will never see this message.
+     -->
+<!ENTITY searchAllMessages.label.base "Search all messages… #1">
+<!-- LOCALIZATION NOTE (searchAllMessages.keyLabel.nonmac):
+     The description of the key-binding to get into the box on windows and
+     linux (which use the control key).  We use the keybinding for cmd_find
+     used by the find-in-message mechanism, so that's the letter to indicate
+     if you don't use 'f' for your localization.
+     -->
+<!ENTITY searchAllMessages.keyLabel.nonmac "&lt;Ctrl+K&gt;">
+<!-- LOCALIZATION NOTE (searchAllMessages.keyLabel.mac):
+     The description of the key-binding to get into the box on mac systems.
+     We use the keybinding for cmd_find used by the find-in-message mechanism,
+     so that's the letter to indicate  if you don't use 'f' for your
+     localization.
+     -->
+<!ENTITY searchAllMessages.keyLabel.mac "&lt;&#2318;+F&gt;">
 
 <!-- Message Header Context Menu -->
 <!ENTITY AddToAddressBook.label "Add to Address Book…">
 <!ENTITY AddToAddressBook.accesskey "B">
 <!ENTITY AddDirectlyToAddressBook.label "Add to Address Book">
 <!ENTITY AddDirectlyToAddressBook.accesskey "B">
 <!ENTITY EditContact.label "Edit Contact…">
 <!ENTITY EditContact.accesskey "E">
new file mode 100644
--- /dev/null
+++ b/mail/locales/en-US/chrome/messenger/quickFilterBar.dtd
@@ -0,0 +1,227 @@
+<!-- LOCALIZATION NOTE (quickFilterBar.toggleBarVisibility.menu.label):
+     The label to display for the "View... Toolbars..." menu item that controls
+     whether the quick filter bar is visible.
+     -->
+<!ENTITY quickFilterBar.toggleBarVisibility.menu.label
+         "Quick Filter Bar">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.toggleBarVisibility.menu.accesskey):
+     The access key for the "View... Toolbars..." menu item label that controls
+     whether the quick filter bar is visible.
+     -->
+<!ENTITY quickFilterBar.toggleBarVisibility.menu.accesskey
+         "Q">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.toggleBarVisibility.button.tooltip):
+     The tooltip to display when hovering over the button on the tab bar that
+     toggles the visibility of the quick filter bar.
+     -->
+<!ENTITY quickFilterBar.toggleBarVisibility.button.tooltip
+         "Toggle the quick filter bar.">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.sticky.tooltip):
+     The tooltip to display when the user hovers over the sticky button
+     (currently displayed as a push-pin).  When active, the sticky button
+     causes the current filter settings to be retained when the user changes
+     folders or opens new tabs.  (When inactive, only the state of the text
+     filters are propagated between folder changes and when opening new tabs.)
+     -->
+<!ENTITY quickFilterBar.sticky.tooltip
+         "Keep filters applied when switching folders?">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.barLabel.label):
+     The text to display on the quick filter bar, labeling it.  This should
+     ideally be as short as possible.
+     -->
+<!ENTITY quickFilterBar.barLabel.label
+         "Quick Filter:">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.unread.label):
+     The label for the filter button that causes us to filter results to only
+     include unread messages.
+     -->
+<!ENTITY quickFilterBar.unread.label
+         "Unread">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.unread.tooltip):
+     The tooltip for the filter button that causes us to filter results to only
+     include unread messages.
+     -->
+<!ENTITY quickFilterBar.unread.tooltip
+         "Show only unread messages.">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.starred.label):
+     The label for the filter button that causes us to filter results to only
+     include messages that have been starred/flagged.
+     -->
+<!ENTITY quickFilterBar.starred.label
+         "Starred">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.starred.tooltip):
+     The tooltip for the filter button that causes us to filter results to only
+     include messages that have been starred/flagged.
+     -->
+<!ENTITY quickFilterBar.starred.tooltip
+         "Show only starred messages.">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.inaddrbook.label):
+     The label for the filter button that causes us to filter results to only
+     include messages from contacts in one of the user's non-remote address
+     books.
+     -->
+<!ENTITY quickFilterBar.inaddrbook.label
+         "Contact">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.inaddrbook.tooltip):
+     The tooltip for the filter button that causes us to filter results to only
+     include messages from contacts in one of the user's non-remote address
+     books.
+     -->
+<!ENTITY quickFilterBar.inaddrbook.tooltip
+         "Show only messages from people in your address book.">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.tags.label):
+     The label for the filter button that causes us to filter results to only
+     include messages with at least one tag on them.
+     -->
+<!ENTITY quickFilterBar.tags.label
+         "Tags">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.tags.tooltip):
+     The tooltip for the filter button that causes us to filter results to only
+     include messages with at least one tag on them.
+     -->
+<!ENTITY quickFilterBar.tags.tooltip
+         "Show only messages with tags on them.">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.attachment.label):
+     The label for the filter button that causes us to filter results to only
+     include messages with attachments.
+     -->
+<!ENTITY quickFilterBar.attachment.label
+         "Attachment">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.attachment.tooltip):
+     The tooltip for the filter button that causes us to filter results to only
+     include messages with attachments.
+     -->
+<!ENTITY quickFilterBar.attachment.tooltip
+         "Show only messages with attachments.">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.resultsLabel.some.formatString):
+     This is used to populate the results box; it either displays the
+     number of messages found using this string, that there are no messages
+     (using quickFilterBar.resultsLabel.none), or the box is hidden.
+     This is a pluralizable string used to express the number of messages in
+     the results.  We replace the '#1' with the number of messages, otherwise
+     see the following URL For more information:
+     https://developer.mozilla.org/En/Localization_and_Plurals
+     -->
+<!ENTITY quickFilterBar.resultsLabel.some.formatString
+         "#1 message;#1 messages">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.resultsLabel.none):
+     The contents of the results box when there is a filter active but there
+     are no messages matching the filter.
+     -->
+<!ENTITY quickFilterBar.resultsLabel.none
+         "No results">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.resultsLabel.minWidth):
+     The minimum width, in pixels, of the results label.  Please size this
+     so that a 3 or 4 digit number of messages in the results can be displayed
+     without growing the size of the box.  You can tell this has been
+     accomplished if adding a filter constraint that changes the displayed
+     string to your "no results" string does not result in any changes to the
+     size of the text box to the label's right.  (If your string for
+     "no results" is longer than the "#### messages" case, then size for that.
+     -->
+<!ENTITY quickFilterBar.resultsLabel.minWidth
+         "100">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.textbox.emptyText.base):
+     This is the base of the empty text for the text search box.  We replace
+     #1 with the contents of the appropriate
+     quickFilterBar.textbox.emptyText.keyLabel.* value for the platform.
+     The goal is to convey to the user that typing in the box will filter
+     the messages and that there is a hotkey they can press to get to the
+     box faster.
+     -->
+<!ENTITY quickFilterBar.textbox.emptyText.base
+         "Filter these messages... #1">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.textbox.emptyText.keyLabel.nonmac):
+     The description of the key-binding to get into the box on windows and
+     linux (which use the control key).  We use the keybinding for cmd_find
+     used by the find-in-message mechanism, so that's the letter to indicate
+     if you don't use 'f' for your localization.
+     -->
+<!ENTITY quickFilterBar.textbox.emptyText.keyLabel.nonmac
+         "&lt;Ctrl+F&gt;">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.textbox.emptyText.keyLabel.mac):
+     The description of the key-binding to get into the box on mac systems.
+     We use the keybinding for cmd_find used by the find-in-message mechanism,
+     so that's the letter to indicate  if you don't use 'f' for your
+     localization.
+     -->
+<!ENTITY quickFilterBar.textbox.emptyText.keyLabel.mac
+         "&lt;&#2318;+F&gt;">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.textbox.idealWidth):
+     The number of pixels for the ideal width of the quick filter box textbox.
+     Choose this value so that the emptyText fits nicely with a little bit of
+     extra whitespace.
+     -->
+<!ENTITY quickFilterBar.textbox.idealWidth
+         "320">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.textbox.minWidth):
+     The minimum width of the quick filter textbox in pixels.  This is the size
+     which we should refuse to flex below.  When we hit this size, the buttons
+     with labels will have their labels collapsed.
+     -->
+<!ENTITY quickFilterBar.textbox.minWidth
+         "280">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.textFilter.sender.label):
+     The button label that toggles whether the text filter searches the message
+     sender for the string.
+     -->
+<!ENTITY quickFilterBar.textFilter.sender.label
+         "Sender">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.textFilter.recipients.label):
+     The button label that toggles whether the text filter searches the message
+     recipients (to, cc) for the string.
+     -->
+<!ENTITY quickFilterBar.textFilter.recipients.label
+         "Recipients">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.textFilter.subject.label):
+     The button label that toggles whether the text filter searches the message
+     subject for the string.
+     -->
+<!ENTITY quickFilterBar.textFilter.subject.label
+         "Subject">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.textFilter.body.label):
+     The button label that toggles whether the text filter searches the message
+     body for the string.
+     -->
+<!ENTITY quickFilterBar.textFilter.body.label
+         "Body">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.glodaUpsell.continueSearch):
+     The first line of the panel popup that tells the user we found no matches
+     but we can convert to a global search for them.
+     -->
+<!ENTITY quickFilterBar.glodaUpsell.continueSearch
+         "Continue this search across all folders">
+
+<!-- LOCALIZATION NOTE (quickFilterBar.glodaUpsell.pressEnterAndCurrent):
+     The second line of the panel popup that tells the user we found no matches.
+     This line will have #1 replaced with what the user has typed so far.
+     -->
+<!ENTITY quickFilterBar.glodaUpsell.pressEnterAndCurrent
+         "Press 'Enter' again to continue your search for: #1">
deleted file mode 100644
--- a/mail/locales/en-US/chrome/messenger/quickSearch.properties
+++ /dev/null
@@ -1,9 +0,0 @@
-searchSubject.label=Subject filter
-searchFrom.label=From filter
-searchFromOrSubject.label=Subject or From filter
-searchRecipient.label=To or Cc filter
-searchRecipientOrSubject.label=Subject, To, or Cc filter
-searchMsgBody.label=Message body filter
-saveAsVirtualFolder.label=Save search as virtual folder
-searchSubjectFromOrRecipient.label=Subject, From, or Recipient filter
-searchEntireMessage.label=Entire message filter
--- a/mail/locales/jar.mn
+++ b/mail/locales/jar.mn
@@ -90,24 +90,24 @@
   locale/@AB_CD@/messenger/fieldMapImport.dtd                           (%chrome/messenger/fieldMapImport.dtd)
   locale/@AB_CD@/messenger/textImportMsgs.properties                    (%chrome/messenger/textImportMsgs.properties)
   locale/@AB_CD@/messenger/appleMailImportMsgs.properties               (%chrome/messenger/appleMailImportMsgs.properties)
   locale/@AB_CD@/messenger/comm4xMailImportMsgs.properties              (%chrome/messenger/comm4xMailImportMsgs.properties)
   locale/@AB_CD@/messenger/eudoraImportMsgs.properties                  (%chrome/messenger/eudoraImportMsgs.properties)
   locale/@AB_CD@/messenger/oeImportMsgs.properties                      (%chrome/messenger/oeImportMsgs.properties)
   locale/@AB_CD@/messenger/outlookImportMsgs.properties                 (%chrome/messenger/outlookImportMsgs.properties)
   locale/@AB_CD@/messenger/shutdownWindow.properties                    (%chrome/messenger/shutdownWindow.properties)
-  locale/@AB_CD@/messenger/quickSearch.properties                       (%chrome/messenger/quickSearch.properties)
   locale/@AB_CD@/messenger/featureConfigurator.dtd                      (%chrome/messenger/featureConfigurator.dtd)
   locale/@AB_CD@/messenger/configEditorOverlay.dtd                      (%chrome/messenger/configEditorOverlay.dtd)
   locale/@AB_CD@/messenger/gloda.properties                             (%chrome/messenger/gloda.properties)
   locale/@AB_CD@/messenger/glodaComplete.properties                     (%chrome/messenger/glodaComplete.properties)
   locale/@AB_CD@/messenger/templateUtils.properties                     (%chrome/messenger/templateUtils.properties)
   locale/@AB_CD@/messenger/glodaFacetView.properties                    (%chrome/messenger/glodaFacetView.properties)
   locale/@AB_CD@/messenger/glodaFacetView.dtd                           (%chrome/messenger/glodaFacetView.dtd)
+  locale/@AB_CD@/messenger/quickFilterBar.dtd                           (%chrome/messenger/quickFilterBar.dtd)
   locale/@AB_CD@/messenger/addressbook/abMainWindow.dtd                 (%chrome/messenger/addressbook/abMainWindow.dtd)
   locale/@AB_CD@/messenger/addressbook/abNewCardDialog.dtd              (%chrome/messenger/addressbook/abNewCardDialog.dtd)
   locale/@AB_CD@/messenger/addressbook/abContactsPanel.dtd              (%chrome/messenger/addressbook/abContactsPanel.dtd)
   locale/@AB_CD@/messenger/addressbook/abAddressBookNameDialog.dtd      (%chrome/messenger/addressbook/abAddressBookNameDialog.dtd)
   locale/@AB_CD@/messenger/addressbook/abCardOverlay.dtd                (%chrome/messenger/addressbook/abCardOverlay.dtd)
   locale/@AB_CD@/messenger/addressbook/abCardViewOverlay.dtd            (%chrome/messenger/addressbook/abCardViewOverlay.dtd)
   locale/@AB_CD@/messenger/addressbook/abDirTreeOverlay.dtd             (%chrome/messenger/addressbook/abDirTreeOverlay.dtd)
   locale/@AB_CD@/messenger/addressbook/abResultsPaneOverlay.dtd         (%chrome/messenger/addressbook/abResultsPaneOverlay.dtd)
deleted file mode 100644
--- a/mail/test/mozmill/folder-display/test-quick-search.js
+++ /dev/null
@@ -1,277 +0,0 @@
-/* ***** BEGIN LICENSE BLOCK *****
- *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Thunderbird Mail Client.
- *
- * The Initial Developer of the Original Code is
- * Mozilla Messaging, Inc.
- * Portions created by the Initial Developer are Copyright (C) 2009
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *   Andrew Sutherland <asutherland@asutherland.org>
- *   Siddharth Agarwal <sid.bugzilla@gmail.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-var MODULE_NAME = 'test-quick-search';
-
-var RELATIVE_ROOT = '../shared-modules';
-var MODULE_REQUIRES = ['folder-display-helpers', 'window-helpers',
-                       'search-window-helpers'];
-
-Cu.import("resource:///modules/quickSearchManager.js");
-Cu.import("resource:///modules/sessionStoreManager.js");
-
-var folder;
-var setFoo, setBar;
-
-function setupModule(module) {
-  let fdh = collector.getModule('folder-display-helpers');
-  fdh.installInto(module);
-  let wh = collector.getModule('window-helpers');
-  wh.installInto(module);
-  let sh = collector.getModule('search-window-helpers');
-  sh.installInto(module);
-
-  folder = create_folder("QuickSearch");
-  [setFoo, setBar] =
-    make_new_sets_in_folder(folder, [{subject: "foo", count: 1},
-                                     {subject: "bar", count: 1}]);
-}
-
-function _assert_quick_search_mode(aController, aMode)
-{
-  let searchInput = aController.e("searchInput");
-  let actualMode = searchInput.searchMode;
-  if (actualMode != aMode)
-    throw new Error("The search mode is supposed to be " + aMode +
-                    ", but is actually " + actualMode);
-
-  // Check whether the menupopup has the correct value selected
-  let menupopupMode = searchInput.menupopup.getAttribute("value");
-  if (menupopupMode != aMode)
-    throw new Error("The search menupopup's mode is supposed to be " + aMode +
-                    ", but is actually " + menupopupMode);
-
-  // Also check the empty text
-  let expectedEmptyText = searchInput.menupopup.getElementsByAttribute("value",
-                              aMode)[0].getAttribute("label");
-  if (expectedEmptyText != searchInput.emptyText)
-    throw new Error("The search empty text is supposed to be " +
-                    expectedEmptyText + ", but is actually " +
-                    searchInput.emptyText);
-}
-
-function _open_3pane_window()
-{
-  let windowWatcher = Cc["@mozilla.org/embedcomp/window-watcher;1"]
-                        .getService(Ci.nsIWindowWatcher);
-  windowWatcher.openWindow(null,
-      "chrome://messenger/content/", "",
-      "all,chrome,dialog=no,status,toolbar", null);
-}
-
-/**
- *
- */
-function test_save_quick_search() {
-  be_in_folder(folder);
-
-  // - We want to do a from or subject search
-  mc.e("searchInput").searchMode =
-    QuickSearchConstants.kQuickSearchFromOrSubject.toString();
-
-  // - Type something in the quick search box.
-  mc.type(mc.eid("searchInput"), "foo");
-
-  mc.keypress(mc.eid("searchInput"), "VK_RETURN", {});
-  wait_for_all_messages_to_load();
-
-  // - Click the "Save Search as a Folder" button
-  // This will create a virtual folder properties dialog...
-  // (label: "New Saved Search Folder", source: virtualFolderProperties.xul
-  //  no windowtype, id: "virtualFolderPropertiesDialog")
-  plan_for_modal_dialog("mailnews:virtualFolderProperties",
-                        subtest_save_search);
-  mc.e("searchInput").saveAsVirtualFolder.doCommand();
-  wait_for_modal_dialog("mailnews:virtualFolderProperties");
-}
-
-/**
- * Save the search, making sure that the "subject OR from" constraints are
- *  there.
- */
-function subtest_save_search(savc) {
-  // - make sure our constraint propagated
-  // this should be an "OR" constraint
-  savc.assertValue(savc.eid("booleanAndGroup"), "or");
-
-  // first constraint is on "Subject"=0 and should be "foo"
-  let searchAttr0 = savc.eid("searchAttr0");
-  savc.assertNode(searchAttr0);
-  savc.assertValue(searchAttr0, "0");
-
-  let searchVal0 = savc.aid("searchVal0", {crazyDeck: 0});
-  savc.assertNode(searchVal0);
-  savc.assertValue(searchVal0, "foo");
-
-  // second constraint is on "From"=1 and should be "foo" as well
-  let searchAttr1 = savc.eid("searchAttr1");
-  savc.assertNode(searchAttr1);
-  savc.assertValue(searchAttr1, "1");
-
-  let searchVal1 = savc.aid("searchVal1", {crazyDeck: 0});
-  savc.assertNode(searchVal1);
-  savc.assertValue(searchVal1, "foo");
-
-  // - Make sure the name mangling is as expected
-  savc.assertValue(savc.eid("name"), "QuickSearch-foo");
-
-  // - save it!
-  // this will close the dialog, which wait_for_modal_dialog is making sure
-  //  happens.
-  savc.window.onOK();
-}
-
-/**
- * Make sure the folder showed up with the right name, and that displaying it
- *  has the right contents.
- */
-function test_verify_saved_search() {
-  let savedFolder = folder.findSubFolder("QuickSearch-foo");
-  if (savedFolder == null)
-    throw new Error("Saved folder did not show up.");
-
-  be_in_folder(savedFolder);
-  assert_messages_in_view(setFoo);
-}
-
-/**
- * Test that when a new 3-pane window is opened from the original 3-pane, the
- * search mode is persisted.
- */
-function test_search_mode_persistence_new_3pane_from_original_3pane()
-{
-  plan_for_new_window("mail:3pane");
-  mc.window.MsgOpenNewWindowForFolder(null, -1);
-  let mc2 = wait_for_new_window("mail:3pane");
-
-  _assert_quick_search_mode(mc2,
-      QuickSearchConstants.kQuickSearchFromOrSubject.toString());
-
-  mc2.window.close();
-}
-
-/**
- * Test that the window.close() above doesn't cause session persistence to come
- * into play -- i.e. if we now change the search mode and open a window again,
- * the new window has the new search mode and not the old one.
- */
-function test_search_mode_persistence_new_3pane_from_original_3pane_again()
-{
-  mc.e("searchInput").searchMode =
-    QuickSearchConstants.kQuickSearchBody.toString();
-  plan_for_new_window("mail:3pane");
-  mc.window.MsgOpenNewWindowForFolder(null, -1);
-  let mc2 = wait_for_new_window("mail:3pane");
-
-  _assert_quick_search_mode(mc2,
-      QuickSearchConstants.kQuickSearchBody.toString());
-
-  mc2.window.close();
-}
-
-/**
- * Test that when a new 3-pane window is opened independently of the original
- * 3-pane, the search mode is persisted from the original 3-pane.
- */
-function test_search_mode_persistence_new_3pane_independently()
-{
-  mc.e("searchInput").searchMode =
-    QuickSearchConstants.kQuickSearchFromOrSubject.toString();
-  plan_for_new_window("mail:3pane");
-  _open_3pane_window();
-  let mc2 = wait_for_new_window("mail:3pane");
-
-  _assert_quick_search_mode(mc2,
-      QuickSearchConstants.kQuickSearchFromOrSubject.toString());
-
-  mc2.window.close();
-}
-
-/**
- * Test that the window.close() above doesn't cause session persistence to come
- * into play -- i.e. if we now change the search mode and open a window again,
- * the new window has the new search mode and not the old one.
- */
-function test_search_mode_persistence_new_3pane_independently_again()
-{
-  mc.e("searchInput").searchMode =
-    QuickSearchConstants.kQuickSearchBody.toString();
-  plan_for_new_window("mail:3pane");
-  _open_3pane_window();
-  let mc2 = wait_for_new_window("mail:3pane");
-
-  _assert_quick_search_mode(mc2,
-      QuickSearchConstants.kQuickSearchBody.toString());
-
-  mc2.window.close();
-}
-
-/**
- * Test that persistence works properly if the "Subject, To or Cc" filter is
- * selected.
- */
-function test_search_mode_persistence_subject_to_cc_filter()
-{
-  mc.e("searchInput").searchMode =
-    QuickSearchConstants.kQuickSearchRecipientOrSubject.toString();
-
-  // Make sure we have a different window open, so that we don't start shutting
-  // down just because the last window was closed.
-  let swc = open_search_window();
-
-  plan_for_window_close(mc);
-  mc.window.close();
-  wait_for_window_close();
-
-  // XXX we force the session store manager to think it's not initialized
-  // so that it'll load the session file and restore the state of the last
-  // open window 3pane window.
-  sessionStoreManager._initialized = false;
-
-  plan_for_new_window("mail:3pane");
-  _open_3pane_window();
-  mc = wait_for_new_window("mail:3pane");
-
-  // We don't need the search window any more
-  plan_for_window_close(swc);
-  swc.window.close();
-  wait_for_window_close();
-
-  _assert_quick_search_mode(mc,
-      QuickSearchConstants.kQuickSearchRecipientOrSubject.toString());
-}
--- a/mail/test/mozmill/mozmilltests.list
+++ b/mail/test/mozmill/mozmilltests.list
@@ -4,9 +4,10 @@ search-window
 cookies
 content-policy
 content-tabs
 message-header
 folder-pane
 session-store
 pref-window
 folder-tree-modes
+quick-filter-bar
 migration
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/quick-filter-bar/test-display-issues.js
@@ -0,0 +1,118 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Mail Client.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Andrew Sutherland <asutherland@asutherland.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Test things of a visual nature.
+ */
+
+var MODULE_NAME = 'test-keyboard-interface';
+
+const RELATIVE_ROOT = '../shared-modules';
+
+var MODULE_REQUIRES = ['folder-display-helpers', 'window-helpers',
+                       'quick-filter-bar-helper'];
+
+var folder;
+var setUnstarred, setStarred;
+
+function setupModule(module) {
+  let fdh = collector.getModule('folder-display-helpers');
+  fdh.installInto(module);
+  let wh = collector.getModule('window-helpers');
+  wh.installInto(module);
+  let qfb = collector.getModule('quick-filter-bar-helper');
+  qfb.installInto(module);
+
+  folder = create_folder("QuickFilterBarDisplayIssues");
+  be_in_folder(folder);
+}
+
+/**
+ * When the window gets too narrow the collapsible button labels need to get
+ *  gone.  Then they need to come back when we get large enough again.
+ *
+ * Because the mozmill window sizing is weird and confusing, we force our size
+ *  in both cases but do save/restore around our test.
+ */
+function test_buttons_collapse_and_expand() {
+  assert_quick_filter_bar_visible(true); // precondition
+
+  try {
+    let qfbCollapsy = mc.e("quick-filter-bar-collapsible-buttons");
+    let qfbExemplarButton = mc.e("qfb-unread"); // (arbitrary labeled button)
+    let qfbExemplarLabel = mc.window
+                             .document.getAnonymousNodes(qfbExemplarButton)[1];
+
+    function assertCollapsed() {
+      // The bar should be shrunken and the button should be the same size as its
+      // image!
+      if (qfbCollapsy.getAttribute("shrink") != "true")
+        throw new Error("The collapsy bar should be shrunk!");
+      if (qfbExemplarLabel.clientWidth != 0)
+        throw new Error("The exemplar label should be collapsed!");
+    }
+    function assertExpanded() {
+      // The bar should not be shrunken and the button should be smaller than its
+      // label!
+      if (qfbCollapsy.hasAttribute("shrink"))
+        throw new Error("The collapsy bar should not be shrunk!");
+      if (qfbExemplarLabel.clientWidth == 0)
+        throw new Error("The exemplar label should not be collapsed!");
+    }
+
+    // -- GIANT!
+    mc.window.resizeTo(1200, 600);
+    // spin the event loop once
+    mc.sleep(0);
+    assertExpanded();
+
+    // -- tiny.
+    mc.window.resizeTo(600, 600);
+    // spin the event loop once
+    mc.sleep(0);
+    assertCollapsed();
+
+    // -- GIANT again!
+    mc.window.resizeTo(1200, 600);
+    // spin the event loop once
+    mc.sleep(0);
+    assertExpanded();
+  }
+  finally {
+    // restore window to nominal dimensions; saving was not working out
+    mc.window.resizeTo(1024, 768);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/quick-filter-bar/test-filter-logic.js
@@ -0,0 +1,344 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Mail Client.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Andrew Sutherland <asutherland@asutherland.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Verify that we are constructing the filters that we expect and that they
+ * are hooked up to the right buttons.
+ */
+
+var MODULE_NAME = 'test-filter-logic';
+
+const RELATIVE_ROOT = '../shared-modules';
+
+var MODULE_REQUIRES = ['folder-display-helpers', 'window-helpers',
+                       'quick-filter-bar-helper'];
+
+function setupModule(module) {
+  let fdh = collector.getModule('folder-display-helpers');
+  fdh.installInto(module);
+  let wh = collector.getModule('window-helpers');
+  wh.installInto(module);
+  let qfb = collector.getModule('quick-filter-bar-helper');
+  qfb.installInto(module);
+}
+
+function test_filter_unread() {
+  let folder = create_folder("QuickFilterBarFilterUnread");
+  let [unread, read] = make_new_sets_in_folder(folder,
+    [{count: 1}, {count: 1}]);
+  read.setRead(true);
+
+  be_in_folder(folder);
+  toggle_boolean_constraints("unread");
+  assert_messages_in_view(unread);
+}
+
+function test_filter_starred() {
+  let folder = create_folder("QuickFilterBarFilterStarred");
+  let [unstarred, starred] = make_new_sets_in_folder(folder,
+    [{count: 1}, {count: 1}]);
+  starred.setStarred(true);
+
+  be_in_folder(folder);
+  toggle_boolean_constraints("starred");
+  assert_messages_in_view(starred);
+}
+
+function test_filter_simple_intersection_unread_and_starred() {
+  let folder = create_folder("QuickFilterBarFilterUnreadAndStarred");
+  let [unreadUnstarred, readUnstarred, unreadStarred, readStarred] =
+    make_new_sets_in_folder(folder,
+      [{count: 1}, {count: 1}, {count: 1}, {count: 1}]);
+  readUnstarred.setRead(true);
+  unreadStarred.setStarred(true);
+  readStarred.setRead(true);
+  readStarred.setStarred(true);
+
+  be_in_folder(folder);
+  toggle_boolean_constraints("unread", "starred");
+
+  assert_messages_in_view(unreadStarred);
+}
+
+function test_filter_attachments() {
+  let attachSetDef = {
+    count: 1,
+    attachments: [{filename: 'foo.png',
+                   contentType: 'image/png',
+                   encoding: 'base64', charset: null,
+                   body: 'YWJj\n', format: null}],
+  };
+  let noAttachSetDef = {
+    count: 1,
+  };
+
+
+  let folder = create_folder("QuickFilterBarFilterAttachments");
+  let [setNoAttach, setAttach] = make_new_sets_in_folder(folder,
+    [noAttachSetDef, attachSetDef]);
+
+  be_in_folder(folder);
+  toggle_boolean_constraints("attachments");
+
+  assert_messages_in_view(setAttach);
+}
+
+/**
+ * Create a card for the given e-mail address, adding it to the first address
+ * book we can find.
+ */
+function add_email_to_address_book(aEmailAddr) {
+  let card = Cc["@mozilla.org/addressbook/cardproperty;1"]
+               .createInstance(Ci.nsIAbCard);
+  card.primaryEmail = aEmailAddr;
+
+  let enumerator = Cc["@mozilla.org/abmanager;1"]
+                     .getService(Ci.nsIAbManager)
+                     .directories;
+  while (enumerator.hasMoreElements()) {
+    let addrbook = enumerator.getNext();
+    if (addrbook instanceof Components.interfaces.nsIAbMDBDirectory &&
+        addrbook instanceof Components.interfaces.nsIAbDirectory) {
+      addrbook.addCard(card);
+      return;
+    }
+  }
+
+  throw new Error("Unable to find any suitable address book.");
+}
+
+function test_filter_in_address_book() {
+  let bookSetDef = {
+    from: ["Qbert Q Qbington", "q@q.q"],
+    count: 1
+  };
+  add_email_to_address_book(bookSetDef.from[1]);
+  let folder = create_folder("MesssageFilterBarInAddressBook");
+  let [setBook, setNoBook] = make_new_sets_in_folder(folder,
+                               [bookSetDef, {count: 1}]);
+  be_in_folder(folder);
+  toggle_boolean_constraints("addrbook");
+  assert_messages_in_view(setBook);
+}
+
+function test_filter_tags() {
+  let folder = create_folder("QuickFilterBarTags");
+  const tagA = "$label1", tagB = "$label2", tagC = "$label3";
+  let [setNoTag, setTagA, setTagB, setTagAB, setTagC] = make_new_sets_in_folder(
+    folder,
+    [{count: 1}, {count: 1}, {count: 1}, {count: 1}, {count: 1}]);
+  setTagA.addTag(tagA);
+  setTagB.addTag(tagB);
+  setTagAB.addTag(tagA);
+  setTagAB.addTag(tagB);
+  setTagC.addTag(tagC);
+
+  be_in_folder(folder);
+  toggle_boolean_constraints("tags"); // must have a tag
+  assert_messages_in_view([setTagA, setTagB, setTagAB, setTagC]);
+
+  toggle_tag_constraints(tagA); // must have tag A
+  assert_messages_in_view([setTagA, setTagAB]);
+
+  toggle_tag_constraints(tagB); // must have tag A OR tag B
+  assert_messages_in_view([setTagA, setTagB, setTagAB]);
+
+  toggle_tag_constraints(tagA); // must have tag B
+  assert_messages_in_view([setTagB, setTagAB]);
+
+  toggle_tag_constraints(tagB); // have have a tag
+  assert_messages_in_view([setTagA, setTagB, setTagAB, setTagC]);
+
+  toggle_boolean_constraints("tags"); // no constraints
+  assert_messages_in_view([setNoTag, setTagA, setTagB, setTagAB, setTagC]);
+
+  // If we have filtered to a specific tag and we disable the tag filter
+  // entirely, make sure that when we turn it back on we are just back to "any
+  // tag".
+  toggle_boolean_constraints("tags");
+  toggle_tag_constraints(tagC);
+  assert_messages_in_view(setTagC);
+
+  toggle_boolean_constraints("tags"); // no constraints
+  toggle_boolean_constraints("tags"); // should be any tag (not tagC!)
+  assert_messages_in_view([setTagA, setTagB, setTagAB, setTagC]);
+}
+
+function test_filter_text_single_word_and_predicates() {
+  let folder = create_folder("QuickFilterBarTextSingleWord");
+  let whoFoo = ["zabba", "foo@madeup.nul"];
+  let [setInert, setSenderFoo, setRecipientsFoo, setSubjectFoo, setBodyFoo] =
+    make_new_sets_in_folder(folder, [
+      {count: 1}, {count:1, from: whoFoo}, {count: 1, to: [whoFoo]},
+      {count: 1, subject: "foo"}, {count: 1, body: {body: "foo"}}]);
+  be_in_folder(folder);
+
+  // by default, sender/recipients/subject are selected
+  assert_text_constraints_checked("sender", "recipients", "subject");
+
+  // con defaults, por favor
+  set_filter_text("foo");
+  assert_messages_in_view([setSenderFoo, setRecipientsFoo, setSubjectFoo]);
+  // note: we sequence the changes in the list so there is always at least one
+  //  dude selected.  selecting down to nothing has potential UI implications
+  //  we don't want this test to get affected by.
+  // sender only
+  toggle_text_constraints("recipients", "subject");
+  assert_messages_in_view(setSenderFoo);
+  // recipients only
+  toggle_text_constraints("recipients", "sender");
+  assert_messages_in_view(setRecipientsFoo);
+  // subject only
+  toggle_text_constraints("subject", "recipients");
+  assert_messages_in_view(setSubjectFoo);
+  // body only
+  toggle_text_constraints("body", "subject");
+  assert_messages_in_view(setBodyFoo);
+  // everybody
+  toggle_text_constraints("sender", "recipients", "subject");
+  assert_messages_in_view([setSenderFoo, setRecipientsFoo, setSubjectFoo,
+                          setBodyFoo]);
+
+  // sanity check non-matching
+  set_filter_text("notgonnamatchevercauseisayso");
+  assert_messages_in_view([]);
+  // disable body, still should get nothing
+  toggle_text_constraints("body");
+  assert_messages_in_view([]);
+
+  // (we are leaving with the defaults once again active)
+  assert_text_constraints_checked("sender", "recipients", "subject");
+}
+
+/**
+ * Verify that the multi-word logic is actually splitting the words into
+ *  different terms and that the terms can match in different predicates.
+ *  This means that given "foo bar" we should be able to match "bar foo" in
+ *  a subject and "foo" in the sender and "bar" in the recipient.  And that
+ *  constitutes sufficient positive coverage, although we also want to make
+ *  sure that just a single term match is insufficient.
+ */
+function test_filter_text_multi_word() {
+  let folder = create_folder("QuickFilterBarTextMultiWord");
+
+  let whoFoo = ["foo", "zabba@madeup.nul"];
+  let whoBar = ["zabba", "bar@madeup.nul"];
+  let [setInert, setPeepMatch, setSubjReverse, setSubjectJustFoo] =
+    make_new_sets_in_folder(folder, [
+      {count: 1}, {count:1, from: whoFoo, to: [whoBar]},
+      {count: 1, subject: "bar foo"}, {count: 1, from: whoFoo}]);
+  be_in_folder(folder);
+
+  // (precondition)
+  assert_text_constraints_checked("sender", "recipients", "subject");
+
+  set_filter_text("foo bar");
+  assert_messages_in_view([setPeepMatch, setSubjReverse]);
+}
+
+/**
+ * Make sure that when dropping all constraints on toggle off or changing
+ *  folders that we persist/propagate the state of the
+ *  sender/recipients/subject/body toggle buttons.
+ */
+function test_filter_text_constraints_propagate() {
+  let whoFoo = ["foo", "zabba@madeup.nul"];
+  let whoBar = ["zabba", "bar@madeup.nul"];
+
+  let folderOne = create_folder("QuickFilterBarTextPropagate1");
+  let [setSubjFoo, setWhoFoo] = make_new_sets_in_folder(folderOne,
+    [{count: 1, subject: "foo"}, {count: 1, from: whoFoo}]);
+  let folderTwo = create_folder("QuickFilterBarTextPropagate2");
+  let [setSubjBar, setWhoBar] = make_new_sets_in_folder(folderTwo,
+    [{count: 1, subject: "bar"}, {count: 1, from: whoBar}]);
+
+  be_in_folder(folderOne);
+  set_filter_text("foo");
+  // (precondition)
+  assert_text_constraints_checked("sender", "recipients", "subject");
+  assert_messages_in_view([setSubjFoo, setWhoFoo]);
+
+  // -- drop subject, close bar to reset, make sure it sticks
+  toggle_text_constraints("subject");
+  assert_messages_in_view([setWhoFoo]);
+
+  toggle_quick_filter_bar();
+  toggle_quick_filter_bar();
+
+  set_filter_text("foo");
+  assert_messages_in_view([setWhoFoo]);
+  assert_text_constraints_checked("sender", "recipients");
+
+  // -- now change folders and make sure the settings stick
+  be_in_folder(folderTwo);
+  set_filter_text("bar");
+  assert_messages_in_view([setWhoBar]);
+  assert_text_constraints_checked("sender", "recipients");
+}
+
+/**
+ * Here is what the results label does:
+ * - No filter active: results label is not visible.
+ * - Filter active, messages: it says the number of messages.
+ * - Filter active, no messages: it says there are no messages.
+ *
+ * Additional nuances:
+ * - The count needs to update as the user deletes messages or what not.
+ */
+function test_results_label() {
+  let folder = create_folder("QuickFilterBarResultsLabel");
+  let [setImmortal, setMortal, setGoldfish] = make_new_sets_in_folder(folder,
+    [{count: 1}, {count: 1}, {count: 1}]);
+
+  be_in_folder(folder);
+
+  // no filter, the label should not be visible
+  if (mc.e("qfb-results-label").visible)
+    throw new Error("results label should not be visible, yo! mad impropah!");
+
+  toggle_boolean_constraints("unread");
+  assert_messages_in_view([setImmortal, setMortal, setGoldfish]);
+  assert_results_label_count(3);
+
+  delete_message_set(setGoldfish);
+  assert_results_label_count(2);
+
+  delete_message_set(setMortal);
+  assert_results_label_count(1);
+
+  delete_message_set(setImmortal);
+  assert_results_label_count(0);
+}
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/quick-filter-bar/test-keyboard-interface.js
@@ -0,0 +1,169 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Mail Client.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Andrew Sutherland <asutherland@asutherland.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Tests keyboard stuff that doesn't fall under some other test's heading.
+ * Namely, control-f toggling the bar into existence happens in
+ * test-toggle-bar.js, but we test that repeatedly hitting control-f toggles
+ * between our search text box and the find-in-message text box.
+ */
+
+var MODULE_NAME = 'test-keyboard-interface';
+
+const RELATIVE_ROOT = '../shared-modules';
+
+var MODULE_REQUIRES = ['folder-display-helpers', 'window-helpers',
+                       'quick-filter-bar-helper'];
+
+var folder;
+var setUnstarred, setStarred;
+
+function setupModule(module) {
+  let fdh = collector.getModule('folder-display-helpers');
+  fdh.installInto(module);
+  let wh = collector.getModule('window-helpers');
+  wh.installInto(module);
+  let qfb = collector.getModule('quick-filter-bar-helper');
+  qfb.installInto(module);
+
+  folder = create_folder("QuickFilterBarKeyboardInterface");
+  // we need a message so we can select it so we can find in message
+  make_new_sets_in_folder(folder, [{count: 1}]);
+  be_in_folder(folder);
+}
+
+/**
+ * The rules for pressing escape:
+ * - If there are any applied constraints:
+ *   - If there is a 'most recent' constraint, it is relaxed and the 'most
+ *     recent' field gets cleared, so that if escape gets hit again...
+ *   - If there is no 'most recent' constraint, all constraints are cleared.
+ * - If there are no applied constraints, we close the filter bar.
+ *
+ * We test these rules two ways:
+ * 1) With the focus in the thread pane.
+ * 2) With our focus in our text-box.
+ */
+function test_escape_rules() {
+  assert_quick_filter_bar_visible(true); // (precondition)
+
+  // the common logic for each bit...
+  function legwork() {
+    // apply two...
+    toggle_boolean_constraints("unread", "starred", "addrbook");
+    assert_constraints_expressed({unread: true, starred: true, addrbook: true});
+    assert_quick_filter_bar_visible(true);
+
+    // hit escape, should clear addrbook
+    mc.keypress(null, "VK_ESCAPE", {});
+    assert_quick_filter_bar_visible(true);
+    assert_constraints_expressed({unread: true, starred: true});
+
+    // hit escape, should clear both remaining ones
+    mc.keypress(null, "VK_ESCAPE", {});
+    assert_quick_filter_bar_visible(true);
+    assert_constraints_expressed({});
+
+    // hit escape, bar should disappear
+    mc.keypress(null, "VK_ESCAPE", {});
+    assert_quick_filter_bar_visible(false);
+
+    // bring the bar back for the next dude
+    toggle_quick_filter_bar();
+  }
+
+  // 1) focus in the thread pane
+  mc.e("threadTree").focus();
+  legwork();
+
+  // 2) focus in the text box
+  mc.e("qfb-qs-textbox").focus();
+  legwork();
+}
+
+/**
+ * It's fairly important that the gloda search widget eats escape when people
+ * press escape in there.  Because gloda is disabled by default, we need to
+ * viciously uncollapse it ourselves and then cleanup afterwards...
+ */
+function test_escape_does_not_reach_us_from_gloda_search() {
+  let glodaSearchWidget = mc.e("searchInput");
+  try {
+    // uncollapse and focus the gloda search widget
+    glodaSearchWidget.collapsed = false;
+    glodaSearchWidget.focus();
+
+    mc.keypress(null, "VK_ESCAPE", {});
+
+    assert_quick_filter_bar_visible(true);
+  }
+  finally {
+    glodaSearchWidget.collapsed = true;
+  }
+
+}
+
+/**
+ * Control-f jumps to the quickfilter text box when not in it.  when in it, it
+ * allows the normal find-in-message command to execute, which should jump it
+ * to the find-in-message textbox thing.
+ */
+function test_control_f_toggles_between_textboxes() {
+  let dispatcha = mc.window.document.commandDispatcher;
+
+  // focus explicitly on the thread pane so we know where the focus is.
+  mc.e("threadTree").focus();
+  // select a message so we can find in message
+  select_click_row(0);
+
+  // hit control-f to get in the quick filter box
+  mc.keypress(null, "f", {accelKey: true});
+  if (dispatcha.focusedElement != mc.e("qfb-qs-textbox").inputField)
+    throw new Error("control-f did not focus quick filter textbox");
+
+  // hit control-f to get in the find-in-message box
+  mc.keypress(null, "f", {accelKey: true});
+  if (dispatcha.focusedElement != mc.e("FindToolbar")._findField.inputField)
+    throw new Error("control-f did not focus toolbar textbox. focused elem: " +
+                    dispatcha.focusedElement + " expected elem: " +
+                    mc.e("FindToolbar")._findField.inputField + " last dude: " +
+                    mc.e("qfb-qs-textbox").inputField);
+
+  // secret bonus test!  hit escape and make sure it only closes the
+  //  find-in-message bar.
+  mc.keypress(null, "VK_ESCAPE", {});
+  assert_quick_filter_bar_visible(true);
+}
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/quick-filter-bar/test-sticky-filter-logic.js
@@ -0,0 +1,147 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Mail Client.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Andrew Sutherland <asutherland@asutherland.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Sticky logic only needs to test the general sticky logic plus any filters
+ *  with custom propagateState implementations (currently: tags, text filter.)
+ */
+
+var MODULE_NAME = 'test-filter-logic';
+
+const RELATIVE_ROOT = '../shared-modules';
+
+var MODULE_REQUIRES = ['folder-display-helpers', 'window-helpers',
+                       'quick-filter-bar-helper'];
+
+function setupModule(module) {
+  let fdh = collector.getModule('folder-display-helpers');
+  fdh.installInto(module);
+  let wh = collector.getModule('window-helpers');
+  wh.installInto(module);
+  let qfb = collector.getModule('quick-filter-bar-helper');
+  qfb.installInto(module);
+}
+
+/**
+ * Persist the current settings through folder change and inherit into a new tab.
+ */
+function test_sticky_basics() {
+  let folderOne = create_folder("QuickFilterBarStickyBasics1");
+  let [unreadOne, readOne] = make_new_sets_in_folder(folderOne,
+    [{count: 1}, {count: 1}]);
+  readOne.setRead(true);
+
+  let folderTwo = create_folder("QuickFilterBarStickyBasics2");
+  let [unreadTwo, readTwo] = make_new_sets_in_folder(folderTwo,
+    [{count: 1}, {count: 1}]);
+  readTwo.setRead(true);
+
+  // -- setup
+  let tabA = be_in_folder(folderOne);
+  toggle_boolean_constraints("sticky", "unread");
+  assert_messages_in_view(unreadOne);
+
+  // -- change folders
+  be_in_folder(folderTwo);
+  assert_constraints_expressed({sticky: true, unread: true});
+  assert_messages_in_view(unreadTwo);
+
+  // -- inherit into a new folder
+  let tabB = open_folder_in_new_tab(folderOne);
+  assert_constraints_expressed({sticky: true, unread: true});
+  assert_messages_in_view(unreadOne);
+
+  close_tab(tabB);
+}
+
+/**
+ * The semantics of sticky tags are not obvious; there were decisions involved:
+ * - If the user has the tag facet enabled but not explicitly filtered on
+ *   specific tags then we propagate just "true" to cause the faceting to
+ *   run in the new folder.  In other words, the list of displayed tags should
+ *   change.
+ * - If the user has filtered on specific tags, then we do and must propagate
+ *   the list of tags.
+ *
+ * We only need to do folder changes from here on out since the logic is
+ *  identical (and tested to be identical in |test_sticky_basics|).
+ */
+function test_sticky_tags() {
+  let folderOne = create_folder("QuickFilterBarStickyTags1");
+  let folderTwo = create_folder("QuickFilterBarStickyTags2");
+  const tagA = "$label1", tagB = "$label2", tagC = "$label3";
+  let [setNoTag1, setTagA1, setTagB1] = make_new_sets_in_folder(
+    folderOne, [{count: 1}, {count: 1}, {count: 1}]);
+  let [setNoTag2, setTagA2, setTagC2] = make_new_sets_in_folder(
+    folderTwo, [{count: 1}, {count: 1}, {count: 1}]);
+  setTagA1.addTag(tagA);
+  setTagB1.addTag(tagB);
+  setTagA2.addTag(tagA);
+  setTagC2.addTag(tagC);
+
+  be_in_folder(folderOne);
+  toggle_boolean_constraints("sticky", "tags");
+  assert_tag_constraints_visible(tagA, tagB);
+  assert_messages_in_view([setTagA1, setTagB1]);
+
+  // -- re-facet when we change folders since constraint was just true
+  be_in_folder(folderTwo);
+  assert_tag_constraints_visible(tagA, tagC);
+  assert_messages_in_view([setTagA2, setTagC2]);
+
+  // -- do not re-facet since tag A was selected
+  toggle_tag_constraints(tagA);
+  be_in_folder(folderOne);
+  assert_tag_constraints_visible(tagA, tagC);
+  assert_messages_in_view([setTagA1]);
+}
+
+/**
+ * All we are testing propagating is the text value; the text states are always
+ *  propagated and that is tested in test-filter-logic.js by
+ *  |test_filter_text_constraints_propagate|.
+ */
+function test_sticky_text() {
+  let folderOne = create_folder("QuickFilterBarStickyText1");
+  let folderTwo = create_folder("QuickFilterBarStickyText2");
+
+  be_in_folder(folderOne);
+  toggle_boolean_constraints("sticky");
+  set_filter_text("foo");
+
+  be_in_folder(folderTwo);
+  assert_filter_text("foo");
+}
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/quick-filter-bar/test-toggle-bar.js
@@ -0,0 +1,116 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Mail Client.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Andrew Sutherland <asutherland@asutherland.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Test that the message filter bar toggles into and out of existence and
+ * states are updated as appropriate.
+ */
+
+var MODULE_NAME = 'test-toggle-bar';
+
+const RELATIVE_ROOT = '../shared-modules';
+
+var MODULE_REQUIRES = ['folder-display-helpers', 'window-helpers',
+                       'quick-filter-bar-helper'];
+
+var folder;
+var setUnstarred, setStarred;
+
+function setupModule(module) {
+  let fdh = collector.getModule('folder-display-helpers');
+  fdh.installInto(module);
+  let wh = collector.getModule('window-helpers');
+  wh.installInto(module);
+  let qfb = collector.getModule('quick-filter-bar-helper');
+  qfb.installInto(module);
+
+  folder = create_folder("QuickFilterBarToggleBar");
+  [setUnstarred, setStarred] = make_new_sets_in_folder(folder, [
+                                 {count: 1}, {count: 1}]);
+  setStarred.setStarred(true);
+}
+
+function test_filter_button_hidden_on_account_central() {
+  be_in_folder(folder.rootFolder);
+  assert_quick_filter_button_visible(false);
+}
+
+function test_visible_by_default() {
+  be_in_folder(folder);
+  assert_quick_filter_button_visible(true);
+  assert_quick_filter_bar_visible(true);
+}
+
+function test_direct_toggle() {
+  assert_quick_filter_bar_visible(true);
+  toggle_quick_filter_bar();
+  assert_quick_filter_bar_visible(false);
+  toggle_quick_filter_bar();
+  assert_quick_filter_bar_visible(true);
+}
+
+function test_control_f_triggers_display() {
+  // hide it
+  toggle_quick_filter_bar();
+  assert_quick_filter_bar_visible(false);
+
+  // focus explicitly on the thread pane so we know where the focus is.
+  mc.e("threadTree").focus();
+
+  // hit control-f
+  mc.keypress(null, "f", {accelKey: true});
+
+  // now we should be visible again!
+  assert_quick_filter_bar_visible(true);
+}
+
+function test_constraints_disappear_when_collapsed() {
+  // set some constraints
+  toggle_boolean_constraints("starred");
+  assert_constraints_expressed({starred: true});
+  assert_messages_in_view(setStarred);
+
+  // collapse, now we should see them all again!
+  toggle_quick_filter_bar();
+  assert_messages_in_view([setUnstarred, setStarred]);
+
+  // uncollapse, we should still see them all!
+  toggle_quick_filter_bar();
+  assert_messages_in_view([setUnstarred, setStarred]);
+
+  // there better be no constraints left!
+  assert_constraints_expressed({});
+}
--- a/mail/test/mozmill/shared-modules/test-folder-display-helpers.js
+++ b/mail/test/mozmill/shared-modules/test-folder-display-helpers.js
@@ -63,34 +63,34 @@ Cu.import('resource:///modules/MailConst
 Cu.import('resource:///modules/mailViewManager.js');
 
 /**
  * List of keys not to export via installInto; values do not matter, we just
  *  use true.
  */
 const DO_NOT_EXPORT = {
   // magic globals
-  MODULE_NAME: true, DO_NOT_EXPORT: true,
+  MODULE_NAME: true, DO_NOT_EXPORT: true, installInto: true,
   // imported modules
   elib: true, mozmill: true, controller: true, frame: true, os: true,
   // convenience constants
   Ci: true, Cc: true, Cu: true, Cr: true,
   // useful constants (we do export MailViewConstants)
   nsMsgViewIndex_None: true, MailConsts: true,
   // utility functions
   MailUtils: true, MailViewManager: true,
   // internal setup functions
   setupModule: true, setupAccountStuff: true,
   // we export this separately
   teardownImporter: true,
   // internal setup flags
   initialized: false,
   // other libraries we use
   testHelperModule: true,
-  windowHelper: true
+  windowHelper: true,
 };
 
 const EXPORT_VIA_GETTER_SETTER = {
   // These should be getters and setters instead of direct property accesses so
   // that setting them reflects across scopes.
   mc: true,
 };
 
@@ -180,31 +180,36 @@ function setupModule() {
   testHelperModule.gMessageGenerator = msgGen;
   testHelperModule.gMessageScenarioFactory =
     new testHelperModule.MessageScenarioFactory(msgGen);
 
   make_new_sets_in_folders = make_new_sets_in_folder =
     testHelperModule.make_new_sets_in_folders;
   add_sets_to_folders = testHelperModule.add_sets_to_folders;
 
+  delete_message_set = testHelperModule.async_delete_messages;
+
   // use window-helper's augment_controller method to get our extra good stuff
   //  we need.
   windowHelper = collector.getModule('window-helpers');
   mc = windowHelper.wait_for_existing_window("mail:3pane");
   windowHelper.augment_controller(mc);
 
   setupAccountStuff();
 }
 
 /**
  * Install this module into the provided module.
  */
 function installInto(module) {
   setupModule();
 
+  // force the window to be a nice size we all can love.
+  mc.window.resizeTo(1024, 768);
+
   // now copy everything into the module they provided to us...
   let us = collector.getModule('folder-display-helpers');
   let self = this;
   for each (let [key, value] in Iterator(us)) {
     if (key in EXPORT_VIA_GETTER_SETTER) {
       // The value of |key| changes between iterations, so it's important to
       // capture the right key in a local variable.
       let thisKey = key;
@@ -2445,16 +2450,17 @@ function throw_and_dump_view_state(aMess
   testHelperModule.dump_view_state(aController.folderDisplay.view);
   throw new Error(aMessage);
 }
 
 /** exported from messageInjection */
 var make_new_sets_in_folders;
 var make_new_sets_in_folder;
 var add_sets_to_folders;
+var delete_message_set;
 
 /**
  * Load a file in its own 'module'.
  *
  * @param aPath A path relative to the comm-central source path.
  *
  * @return An object that serves as the global scope for the loaded file.
  */
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/shared-modules/test-quick-filter-bar-helper.js
@@ -0,0 +1,292 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Mail Client.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Andrew Sutherland <asutherland@asutherland.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var MODULE_NAME = 'quick-filter-bar-helper';
+
+const RELATIVE_ROOT = '../shared-modules';
+
+var MODULE_REQUIRES = ['folder-display-helpers'];
+
+var initialized = false;
+function setupModule(module) {
+  if (initialized)
+    return;
+
+  let fdh = collector.getModule('folder-display-helpers');
+  fdh.installInto(module);
+
+  initialized = true;
+}
+
+var EXPORT = [
+  'assert_quick_filter_button_visible',
+  'assert_quick_filter_bar_visible',
+  'toggle_quick_filter_bar',
+  'assert_constraints_expressed',
+  'toggle_boolean_constraints',
+  'toggle_tag_constraints',
+  'assert_tag_constraints_visible',
+  'assert_tag_constraints_checked',
+  'toggle_text_constraints',
+  'assert_text_constraints_checked',
+  'set_filter_text',
+  'assert_filter_text',
+  'assert_results_label_count',
+  'clear_constraints',
+];
+
+var backstage = this;
+
+function installInto(module) {
+  setupModule(backstage);
+  for each (let [, name] in Iterator(EXPORT)) {
+    module[name] = backstage[name];
+  }
+  // disable the deferred search processing!
+  mc.window.QuickFilterBarMuxer.deferredUpdateSearch =
+    mc.window.QuickFilterBarMuxer.updateSearch;
+
+  module.__teardownTest__ = _afterEveryTest;
+  _afterEveryTest.__name__ = "teardownTest";
+}
+
+function _afterEveryTest() {
+  clear_constraints();
+  // make it visible if it's not
+  if (mc.e("quick-filter-bar").collapsed) {
+    toggle_quick_filter_bar();
+  }
+}
+
+/**
+ * Maps names to bar DOM ids to simplify checking.
+ */
+const nameToBarDomId = {
+  sticky: "qfb-sticky",
+  unread: "qfb-unread",
+  starred: "qfb-starred",
+  addrbook: "qfb-inaddrbook",
+  tags: "qfb-tags",
+  attachments: "qfb-attachment",
+};
+
+function assert_quick_filter_button_visible(aVisible) {
+  if (mc.e("qfb-show-filter-bar").style.visibility !=
+      (aVisible ? "visible" : "hidden")) {
+    throw new Error("Quick filter bar button should be " +
+                    (aVisible ? "visible" : "collapsed"));
+  }
+}
+
+function assert_quick_filter_bar_visible(aVisible) {
+  if (mc.e("quick-filter-bar").collapsed != !aVisible) {
+    throw new Error("Quick filter bar should be " +
+                    (aVisible ? "visible" : "collapsed"));
+  }
+}
+
+/**
+ * Toggle the state of the message filter bar as if by a mouse click.
+ */
+function toggle_quick_filter_bar() {
+  mc.click(mc.eid("qfb-show-filter-bar"));
+  wait_for_all_messages_to_load();
+}
+
+/**
+ * Assert that the state of the constraints visually expressed by the bar is
+ * consistent with the passed-in constraints.  This method does not verify
+ * that the search constraints are in effect.  Check that elsewhere.
+ */
+function assert_constraints_expressed(aConstraints) {
+  for each (let [name, domId] in Iterator(nameToBarDomId)) {
+    let expectedValue = (name in aConstraints) ? aConstraints[name] : false;
+    let domNode = mc.e(domId);
+    if (domNode.checked !== expectedValue) {
+      throw new Error(name + "'s checked state should be " + expectedValue);
+    }
+  }
+}
+
+/**
+ * Toggle the given filter buttons by name (from nameToBarDomId); variable
+ * argument magic enabled.
+ */
+function toggle_boolean_constraints() {
+  for (let iArg = 0; iArg < arguments.length; iArg++) {
+    mc.click(mc.eid(nameToBarDomId[arguments[iArg]]));
+  }
+  wait_for_all_messages_to_load(mc);
+}
+
+/**
+ * Toggle the tag faceting buttons by tag key.  Wait for messages after.
+ */
+function toggle_tag_constraints() {
+  for (let iArg = 0; iArg < arguments.length; iArg++) {
+    mc.click(mc.eid("qfb-tag-" + arguments[iArg]));
+  }
+  wait_for_all_messages_to_load(mc);
+}
+
+/**
+ * Verify that tag buttons exist for exactly the given set of tag keys in the
+ *  provided variable argument list.  Ordering is significant.
+ */
+function assert_tag_constraints_visible() {
+  // the stupid bar should be visible if any arguments are specified
+  if (arguments.length && mc.e("quick-filter-bar-tab-bar").collapsed)
+    throw new Error("The tag bar should not be collapsed!");
+
+  let kids = mc.e("quick-filter-bar-tab-bar").childNodes;
+  // this is bad error reporting in here for now.
+  if (kids.length != arguments.length)
+    throw new Error("Mismatch in expected tag count and actual. " +
+                    "Expected " + arguments.length +
+                    " actual " + kids.length);
+  for (let iArg = 0; iArg < arguments.length; iArg++) {
+    let nodeId = "qfb-tag-" + arguments[iArg];
+    if (nodeId != kids[iArg].id)
+      throw new Error("Mismatch at tag " + iArg + " expected " + nodeId +
+                      " but got " + kids[iArg].id);
+  }
+}
+
+/**
+ * Verify that only the buttons corresponding to the provided tag keys are
+ * checked.
+ */
+function assert_tag_constraints_checked() {
+  let expected = {};
+  for (let iArg = 0; iArg < arguments.length; iArg++) {
+    let nodeId = "qfb-tag-" + arguments[iArg];
+    expected[nodeId] = true;
+  }
+
+  let kids = mc.e("quick-filter-bar-tab-bar").childNodes;
+  for (let iNode = 0; iNode < kids.length; iNode++) {
+    let node = kids[iNode];
+    if (node.checked != (node.id in expected))
+      throw new Error("node " + node.id + " should " +
+                      ((node.id in expected) ? "be " : "not be ") + "checked.");
+  }
+}
+
+const nameToTextDomId = {
+  sender: "qfb-qs-sender",
+  recipients: "qfb-qs-recipients",
+  subject: "qfb-qs-subject",
+  body: "qfb-qs-body",
+};
+
+function toggle_text_constraints() {
+  for (let iArg = 0; iArg < arguments.length; iArg++) {
+    mc.click(mc.eid(nameToTextDomId[arguments[iArg]]));
+  }
+  wait_for_all_messages_to_load(mc);
+}
+
+/**
+ * Assert that the text constraint buttons are checked.  Variable-argument
+ *  support where the arguments are one of sender/recipients/subject/body.
+ */
+function assert_text_constraints_checked() {
+  let expected = {};
+  for (let iArg = 0; iArg < arguments.length; iArg++) {
+    let nodeId = nameToTextDomId[arguments[iArg]];
+    expected[nodeId] = true;
+  }
+
+  let kids = mc.e("quick-filter-bar-filter-text-bar").childNodes;
+  for (let iNode = 0; iNode < kids.length; iNode++) {
+    let node = kids[iNode];
+    if (node.tagName == "label")
+      continue;
+    if (node.checked != (node.id in expected))
+      throw new Error("node " + node.id + " should " +
+                      ((node.id in expected) ? "be " : "not be ") + "checked.");
+  }
+}
+
+/**
+ * Set the text in the text filter box, trigger it like enter was pressed, then
+ *  wait for all messages to load.
+ */
+function set_filter_text(aText) {
+  // We're not testing the reliability of the textbox widget; just poke our text
+  // in and trigger the command logic.
+  let textbox = mc.e("qfb-qs-textbox");
+  textbox.value = aText;
+  textbox.doCommand();
+  wait_for_all_messages_to_load(mc);
+}
+
+function assert_filter_text(aText) {
+  let textbox = mc.e("qfb-qs-textbox");
+  if (textbox.value != aText)
+    throw new Error("Expected text filter value of '" + aText + "' but got '" +
+                    textbox.value);
+}
+
+/**
+ * Assert that the results label is telling us there are aCount messages
+ *  using the appropriate string.
+ */
+function assert_results_label_count(aCount) {
+  let resultsLabel = mc.e("qfb-results-label");
+  if (aCount == 0) {
+    if (resultsLabel.value != resultsLabel.getAttribute("noresultsstring"))
+      throw new Error("results label should be displaying the no messages case");
+  }
+  else {
+    let s = resultsLabel.value;
+    s = s.substring(0, s.indexOf(" "));
+    if (parseInt(s) !== aCount)
+     throw new Error("Result count is displaying " + s + " but should show " +
+                     aCount);
+  }
+}
+
+/**
+ * Clear active constraints via any means necessary; state cleanup for testing,
+ *  not to be used as part of a test.  Unlike normal clearing, this will kill
+ *  the sticky bit.
+ *
+ * This is automatically called by the test teardown helper.
+ */
+function clear_constraints() {
+  mc.window.QuickFilterBarMuxer._testHelperResetFilterState();
+}
--- a/mail/test/mozmill/shared-modules/test-window-helpers.js
+++ b/mail/test/mozmill/shared-modules/test-window-helpers.js
@@ -596,16 +596,25 @@ var AugmentEverybodyWith = {
      * @return an elementlib.Elem for the element with the given id on the
      *  window's document.
      */
     eid: function _get_elementid_by_id_helper(aId, aQuery) {
       return new elib.Elem(this.e(aId, aQuery));
     },
 
     /**
+     * Wait for an element with the given id to show up.
+     *
+     * @param aId The DOM id of the element you want to wait to show up.
+     */
+    ewait: function _wait_for_element_by_id_helper(aId) {
+      this.waitForElement(new elib.ID(this.window.document, aId));
+    },
+
+    /**
      * Find an element in the anonymous subtree of an element in the document
      *  identified by its id.  You would use this to dig into XBL bindings that
      *  are not doing what you want.  For example, jerks that don't focus right.
      *
      * Examples:
      *  // by class of the node
      *  a("searchVal0", {class: "search-value-textbox"});
      *  // when the thing is vaguely deck-like
--- a/mail/themes/gnomestripe/jar.mn
+++ b/mail/themes/gnomestripe/jar.mn
@@ -35,16 +35,17 @@ classic.jar:
   skin/classic/messenger/icons/exclude.png                    (mail/icons/exclude.png)
   skin/classic/messenger/icons/exclude-selected.png           (mail/icons/exclude-selected.png)
   skin/classic/messenger/icons/zoomout.png                    (mail/icons/zoomout.png)
   skin/classic/messenger/icons/zoomout-hover.png              (mail/icons/zoomout-hover.png)
   skin/classic/messenger/dialogs.css                          (mail/dialogs.css)
   skin/classic/messenger/newmailalert.css                     (mail/newmailalert.css)
   skin/classic/messenger/tabmail.css                          (mail/tabmail.css)
   skin/classic/messenger/editContactOverlay.css               (mail/editContactOverlay.css)
+  skin/classic/messenger/quickFilterBar.css                   (mail/quickFilterBar.css)
   skin/classic/messenger/starred48.png                        (mail/starred48.png)
   skin/classic/messenger/contactStarred.png                   (mail/contactStarred.png)
   skin/classic/messenger/starContact.png                      (mail/starContact.png)
   skin/classic/messenger/activity/activity.css                (mail/activity/activity.css)
   skin/classic/messenger/activity/buttons.png                 (mail/activity/buttons.png)
   skin/classic/messenger/activity/defaultProcessIcon.png      (mail/activity/defaultProcessIcon.png)
   skin/classic/messenger/activity/defaultEventIcon.png        (mail/activity/defaultEventIcon.png)
   skin/classic/messenger/activity/defaultWarningIcon.png      (mail/activity/defaultWarningIcon.png)
@@ -144,16 +145,20 @@ classic.jar:
   skin/classic/messenger/icons/remote-blocked.png             (mail/icons/remote-blocked.png)
   skin/classic/messenger/icons/phishing.png                   (mail/icons/phishing.png)
   skin/classic/messenger/icons/junk.png                       (mail/icons/junk.png)
   skin/classic/messenger/icons/check.gif                      (mail/icons/check.gif)
   skin/classic/messenger/icons/notchecked.gif                 (mail/icons/notchecked.gif)
   skin/classic/messenger/icons/online.png                     (mail/icons/online.png)
   skin/classic/messenger/icons/offline.png                    (mail/icons/offline.png)
   skin/classic/messenger/icons/row.png                        (mail/icons/row.png)
+  skin/classic/messenger/icons/black_pin.png                  (mail/icons/black_pin.png)
+  skin/classic/messenger/icons/filter.png                     (mail/icons/filter.png)
+  skin/classic/messenger/icons/filterbar.png                  (mail/icons/filterbar.png)
+  skin/classic/messenger/icons/red_pin.png                    (mail/icons/red_pin.png)
 % skin communicator classic/1.0 %skin/classic/communicator/
   skin/classic/communicator/communicator.css                      (mail/communicator.css)
   skin/classic/communicator/icons/smileys/smiley-smile.png        (mail/icons/smiley-smile.png)
   skin/classic/communicator/icons/smileys/smiley-frown.png        (mail/icons/smiley-frown.png)
   skin/classic/communicator/icons/smileys/smiley-wink.png         (mail/icons/smiley-wink.png)
   skin/classic/communicator/icons/smileys/smiley-tongue-out.png  (mail/icons/smiley-tongue-out.png)
   skin/classic/communicator/icons/smileys/smiley-laughing.png (mail/icons/smiley-laughing.png)
   skin/classic/communicator/icons/smileys/smiley-embarassed.png  (mail/icons/smiley-embarassed.png)
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e7051bc25668dc31ffc475549c5c31ff6455be77
GIT binary patch
literal 602
zc$@)R0;T<lP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00001b5ch_0Itp)
z=>Px#32;bRa{vGh*8l(w*8xH(n|J^K00(qQO+^RV10D|vGMZjuVE_OD>`6pHR5;6(
zlfR4FU>L{0d3{ZLDyS%LMOx_)`~lM8l!_pSbTQYZ|A1qc(lr5x4jm7&b?9Hvu3fsk
z;LvPWQ~Cou+$~uIa};f2Oq!(2U6CW#J3sJ_-wzM(_j!aUif|QKmP-JBan4=b0Er6|
zLQVl3;}(#sVM?h{EEX%2(ieLPF2n71yWH>hNfbp#vMe9oN^mi@QLR?XjIo4in&|a<
zcPOQwZjuOt!GH;Za7rnCC`l4=90%oc8I48*^Z6XQu5ScE_z{Mo5r*M60A~QsIOiS!
zBqT|40DJ)uESJj~rIhA!IpVr5*6TIu^?H&q_8^nVbS%qy2A~1p{ct$^0|3Hqjj}A?
zCxjS<LV=~z=|nc0MYGuir4+iZZ%xzu9t6P?&iN*`1<tt{hT&t&vR0nwA(cwO_k9?K
zf%EfoFQ3mp-3`a?0q5KV@L{{%LJ$N@CKEW0gKoD=E0xMeRaNi)R{#KE80xld+q2nh
zvsf&CGREvqr*lv$l@621<ak#?u4(79>$>UHYW3B1-J0+FC*$$hZnatmjIlPQ^!2|Q
z<8I;NL*D^-et8e0(dd;ZilbVsmYz<hzk?vSr)ipVtso9nRgXncJn=m55rCJProBZJ
oMSDL*QH~Tvc@squ_L?A$ze*_iFu_aY*Z=?k07*qoM6N<$f`tMG{r~^~
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7ed2d204bd64aecb67948c374d66f0ee035bdc48
GIT binary patch
literal 426
zc$@*M0agBqP)<h;3K|Lk000e1NJLTq000sI000aK1^@s6ZxZdi0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUzOi4sRRCwBqlCes}KoEv^_YMRJ<N<_$
zK7dKULQoK*SXjo!Lh%WF0E+{!I7LwrJ3+xte2HM8mL`oHC|(*-u1sN}NY*bJBpkU*
z7JmMjoy?#4Cy`of;kxc5d@QA$7eai42Kgm<I}F1QQR7J4wx{5G@I+$^fmdRO_>{N{
zuORkSCDa5R@CcS@(E0rfQ)c21U<({o2`S#_fld@fx>!K;9l8Lkb*w4yiG_ZtZz-Rl
zamR6FrBJqP8tk$nwK=FiIPEZNSMWYNnJ5;>m!UPpPU=`|@?Z<GY2q#x&WL;5&62N!
z9{Fvp&!7KC4dP9(0Zj15{Q@Ee+{W5YvB~#+9mlayN{KX0g=JZPLS+Z>ZD=?D7>kJA
z73-eo=_E;nX__Jk0;xt!dw{?VJO&O2i&?W_7{B~)ZP;OFGtfEMW1{O(JNgk|0PtLo
UX_aH9bpQYW07*qoM6N<$f-20h<NyEw
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b67b78340e5ca561f8f118b97d25df8bb3297f3c
GIT binary patch
literal 2741
zc$@*X3QF~fP)<h;3K|Lk000e1NJLTq002+`000mO1^@s6fl2tm00004b3#c}2nYxW
zd<bNS00009a7bBm000fw000fw0YWI7cmMzZ8FWQhbW?9;ba!ELWdL_~cP?peYja~^
zaAhuUa%Y?FJQ@H13NlGVK~z|U)tGru6lWU7pRap*1{gq&;Zh<ox}4$>B(4q$9uO%F
z9*M;4W<_>$SewMSMnu*m)?|}48;><v-PJ@*;S~Zz4LLMgC6EY-D3`c`cRWCJ80PHh
znZCaLLm?jMnyOf}`&La=SM~Sw`+UE8`~BV?LMg?Aikf1=8uO^~z!Llq1bI*ZYl_K0
zhGnY=VHy-!dd8SXrTf*cTJ`oIhSmO4rPK&eLI`tMq{<(Q78RX(AZG6oCX>lW=dVlB
zQ17M1i<e)!{r&?3`2BXxhQX;zpSN`$@tS|!KhKy)OYcXwYSr6=Sk9+;_LI5({rg7~
zLVzosS7>QjiA#{hQFnvb($<(xK=xBgwThy^>lGnMQkNu3yF9%0_3Z4~=AYx=WHK>2
zU+yiIA+L-cJtpwX+0%;IY_5RH<`)(g+I!Y9Kj)$M8#TqmSECJ?8y>rydH7M2uZH6m
z-LIBmwI5|=X6d7%1`vRPQi_Q1NHTfiB!4YoKfbGwl5ZlyqO>v5gYZbq5G2GVB6e_`
zAvh>FUsTu=B_&%&-meFf$;9~RHJcI=<Co>k$O(LOWC~_ZpUDnOPM%2^b>q!zH9Z&m
z5AMw-z(N3V0M39y27wWPhJqd;APj(L0<<VpUGw;)sg?KeFehf@5|&EGq17istABWS
zO)(j4%%f-ji+3U`iZLQQlCfB>L9Y)4App-i5fTzg><)WsZf-7HT3XsiU!_`|RtKlc
zp<cXnK@dd|563*>_sF2ZoUgB+q3+1x>JL9$pYg&AFE!i`yrhJr{NW=;pzd%T>JA@;
z*XzaOnVD#9Yn!!V#fs3Pq9RK#zyp>0aw5Va_r%8~5uH{GuPDZt&7JVk`G61tQ5468
z8p5IFI0zn3Ow+OY>|GV5nX__oD*>QpD>GBWafJ-)8?Mnsg@(q=^ba5MDg(j+stxh!
z#o7}GpK1MJ2WKaQu>iC~mZX=BdGy3B^a#PyKQDpR)(ub<Jn!ns4@nYX?Y7-@<EpAs
zN-4Q`>4M;P3Nt;DC+I}$zc(Alqy<DpMqyM+YCvVx!K#%jS7sCx6!ZzP2j0-c<ouMB
z6ns~E2!dNcTx>j!A3p|{;6lHCfn104S|k7q(EAG0f`qsvqEH6y76I*65&eQf(cIFA
zy80gw9v%rvA-HT2&}o-2WJo;AJNZQbP*Y480+gi8Iz2iy*EVS6^wz-mj9u5^0RR&O
z-@rsBWkw4(dd^kDs3(udJ(QS}!D_gV?x-P3sf%~kHZ(LSUauE+yA3v5H{5O~nwpzc
zm&<je@9G0Q9xnv|Ns_2X@&tLkLO)rSq2B261NslZ__Xu@mB^KEzFBm~>-<yYCFYMx
zO+`&@Ep%EfCQO)!#-;`s!VF*-7B-um7X+cRN5=qwA}h%tz{z$eE_b_e!R&@Cd(maF
zoU_?%osuHsREHDo-ELek3t$M~<Xy=CV9cW`!?OF^kDElQ+Ck-upz@EP(n(Nn4OIRU
zsE)NzEYE>@_5!LM*Df}?sj8OTzKOE3GB&th(Dz?{^%YZh<Op0YCwR9T4UNtCX8!>a
z)jz7btgMXf3!o%PR<Gzq%)mi<hG8~yoNi50;?Mv~w;2}8_48xkz=6n`oE1oceG3*W
z2)rF$d|Z6~u;C+6T~!IIts6s!CZnpV3I>A#zaBFdhray|zJ7js6>@Vy!CZYWK$p|i
z(rvY&+bQ7ORUU4S0E^WM7w@XI+Z{)3b{o1JZk)TyL-2ZVt*Z+zr>mvMfFjE?ue5Fb
z>{Rs(fhrbI=SHa3La6qYp!}zxVk3xO4iqI6jbCO9Zrl0AV}2+9<8}yV&Ys+ymNstj
zU*BFq%w`Kd+_VK7HhzT8&TB9&e~XMy8^7rEnYQhH0u-gMJ388x;Gke)9FrE1Hg;S9
z!w`f7g~H?Y^g<jwcrapP;{rg`<vm)R54$QPI3zzIF$tAbm1t@{hH>M@;fv2JV6}GR
zvBy%ed-pC7mc``Blc3k@qq@4-0N_RsbU2+W>+262W8)LZuM8Tv+yWXKj!A-(f8Q+%
z0pC_vW{*r6#m0qcV0XH3=-V2FcevK{7%=8h8NhG8-_E^vy0++**fGm|Ks`V0j`)Ex
z323Ou@W`U`X=5J!;&uQ_mlfnD4;}V&M*2h@&%2O6Zyp$i1t9>X6k4qg6DLg4oj-qW
z#*(G4&3*6P)mv}Ydw9pP3l95?z`%ZihEM|t!@%qHqRV1NWJG^-UhROzVn%3a7={fS
z?ps$^HwD0Jg@uIy8ZCKg%G4>S_<RpKI<Ax}it?1x<-)WlrhymS*s^&Gq92MzMtVBR
z%RfU)OUvrglJd@8fWOaszGCgV)l*NNY+I;OB~Ddny#jev(c(9b0>HB6%QBmq8W&Mj
zO_r$ILRG9;{kPS3oWiOiR}hAq58~?sE(su7fT{o}7*-EekqCf&eN~XhBfOeEF5RcC
ztp$`)aGVwz&GjKcmSsqi2#(Vtb8?o?y7eEt4B!@k+1ax$my~RuUQ>JME900nKZC)5
zh=?d$y?W(Vh}JIn`};$bq%Z)W)TQTnMyt_iaQ5tZ^S{<_oc)LA=N(x0!Me$cqCim;
zj87krtjx#pN$DqOINGqbw4{7VPu|`!X5q`P?FF#6ug$$<dZ!Y=y~dUsL5%&PCi#E~
z1o6#)sxTl72LcB`{16Z}O0;(#8VR88cBReg7<uW^d3ZeTUU&?1{ftvpwHKmB!@*{E
zB=?nl&YWkfHf-AP#J&UjDl#W#>LVf|?tmyOGFnfxLc_8r0f6V-@Ja;NuXz;!wrncS
znmv1V0vL7u#EFwKrazH`Z9BK2;Yh>UPfN=FbaT#Mbdc8Qg0lP#X&liEg!RfXTEF`-
zq5116g5VNC*a!p<n5qRZwlA&VcAMMUPQc-?!)~|2X6uI4di`g!b;EAA!r`#v^qEs|
zyWN(%d6@s){DW?XD`(H1-F!z!I~YO`5fOEx6{uht7Q1%s<{do08UWmG9z4$j0HUL#
zBl4fi^W^0}EBNZQjd5|Y8B?cC#rB=sQD0Yo2Rz`W3j8I4Qgx8qdIIN<7V^BMp;;Df
zzm>d}m}7g>cSgp|4*{iGhV_l_OKW#Hw>3347o<EoLaWiRH~JsB@mva3RUpd>T3U{a
zPN#F{y?iWQy!7B3rZ=YT*s){x@ZrP#l9GmM{rvpU+S-Ql@^Y8QBb0CZc-tNT;CTpO
z6xn>uta<bG<><KDuCv>1pa4XV2k*cCcU-z~xlee&FZ?ZByG{2D%lW?|i?$8Mxk_)3
zswpNuj7I+#hS8?VqIID$k2c+|oiJf+JSAL{s;ats<sb|Zp^#D4)zvNcS_c3N7Ay#3
z7_vf=#Yv(l1#3C(l+(?>yY=I(yKY{WJLjp@02TxXhiI(U?wfBpD5Y!4KK;yeS3eK(
v@2vl)c=5#-IZ+gEnK5~um+m&W_r!ky;64ABYKoaw00000NkvXXu0mjfs!c8s
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8f06471545ca860cf8da01cf0d2226740ee48f93
GIT binary patch
literal 633
zc$@)w0*3vGP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004b3#c}2nYxW
zd<bNS00009a7bBm000fw000fw0YWI7cmMzZ8FWQhbW?9;ba!ELWdL_~cP?peYja~^
zaAhuUa%Y?FJQ@H10rp8mK~y-6mD5ja6G0dT@Mm^*x7$Q(17+O}i&UsYFN)og)Jtg#
zg6+W%;K7p@@wBCBi+J$ZQpkb_KY?d2-V9bzOlAFxDHTdkDuS4lkc7rv$826NMnbc-
z?E`Zf-XF~SFpP+>H!foafSN-T^jz+V6g4aZ>i}jB8twsH#temX&m0&y)d$=>65ujs
z$eeo@GnwSn<fIfbHsLZhdMLnU%;1c9XGcbo*5DxjTrOiOlj&0=>FGfT%K^FstZTsV
z1=Eyvg8)g>gq2R?d#QxSg@P})+aG=dUI@GcumNDhArc*c0l370>jYdIR+QK|%i`M5
z5Pk@OL_7{&E~DCL(924Ni0DNdxYB8eb=T-JW^uqft*ZL5Sd1S}Byh&Ip-K{-udV$m
z?d-f4z_df;N4DS)eG7r~7a<y<@8g7`z$e0!%}sn#WKm6z&2|s>038It!)B`mp=l_*
zd5sE}AY~odzdZdYyEv!)7XW||cvcO==Etoqf4g4)x_x!FX^)Th8-_8eC`!H?i39+=
zssZsz5WGdLo(fv6<$ONhv~9az*Yz=3mhb+nG3qb;UFZV<H#*y1x}CkFs_IfIm5LXO
z#oaIrPp+=ye(noICA&B`qpIq%5aK+5TdOO%`yiqtpZNu6Vt&E7M@0KS;jsAwqLc9!
Ts!6F{00000NkvXXu0mjfqMZ^m
new file mode 100644
--- /dev/null
+++ b/mail/themes/gnomestripe/mail/quickFilterBar.css
@@ -0,0 +1,117 @@
+@import url("chrome://messenger/content/quickFilterBar.css");
+
+/* :::: Filter Tab Bar Button :::: */
+
+#qfb-show-filter-bar {
+  list-style-image: url("chrome://messenger/skin/icons/filter.png");
+  -moz-border-start: 2px solid;
+  -moz-border-end: none;
+  -moz-border-left-colors: rgba(0,0,0,0.25) rgba(255,255,255,0.15);
+  -moz-border-right-colors: rgba(0,0,0,0.25) rgba(255,255,255,0.15);
+  margin: 0;
+  padding: 0 4px;
+}
+
+#qfb-show-filter-bar:hover {
+  background-color: rgba(0,0,0,0.10);
+}
+
+#qfb-show-filter-bar:hover:active,
+#qfb-show-filter-bar[checked] {
+  background-color: rgba(0,0,0,0.20);
+}
+
+#qfb-show-filter-bar > .toolbarbutton-icon {
+  padding: 0 3px;
+}
+
+#qfb-show-filter-bar > .toolbarbutton-text {
+  display: none;
+}
+
+/* :::: Filter Bar :::: */
+
+#threadTree[filterActive="matches"] {
+  outline: 1px solid #4e9a06;
+}
+
+#threadTree[filterActive="nomatches"] {
+  outline: 1px solid #cc0000;
+}
+
+/* :::: Filter Buttons :::: */
+
+#quick-filter-bar #qfb-sticky,
+#quick-filter-bar #qfb-sticky[checked] {
+  background: none;
+  border-width: 0;
+  -moz-border-image: none;
+}
+
+#qfb-sticky {
+  list-style-image: url("chrome://messenger/skin/icons/black_pin.png");
+}
+
+#qfb-sticky[checked] {
+  list-style-image: url("chrome://messenger/skin/icons/red_pin.png");
+}
+
+/* we use both IDs so we are more precise than the other # toolbarbutton rules */
+#quick-filter-bar #qfb-sticky:hover {
+  text-shadow: none;
+  background: none;
+  border: 1px solid transparent;
+  -moz-border-radius: 0;
+  border-width: 0;
+}
+
+
+#qfb-closebutton {
+  list-style-image: url("chrome://global/skin/icons/closeSidebar.png");
+  -moz-image-region: rect(0px, 14px, 14px, 0px);
+}
+
+#qfb-unread {
+  list-style-image: url("chrome://messenger/skin/icons/filterbar.png");
+  -moz-image-region: rect(0px, 16px, 16px, 0px);
+}
+
+#qfb-starred {
+  list-style-image: url("chrome://messenger/skin/icons/filterbar.png");
+  -moz-image-region: rect(0px, 32px, 16px, 16px);
+}
+
+.qfb-starred-nostar {
+  list-style-image: url("chrome://messenger/skin/starContact.png");
+  -moz-image-region:rect(0px 32px 16px 16px);
+}
+
+#qfb-inaddrbook {
+  list-style-image: url("chrome://messenger/skin/icons/filterbar.png");
+  -moz-image-region: rect(0px, 48px, 16px, 32px);
+}
+
+#qfb-tags {
+  list-style-image: url("chrome://messenger/skin/icons/filterbar.png");
+  -moz-image-region: rect(0px, 64px, 16px, 48px);
+}
+
+#qfb-attachment {
+  list-style-image: url("chrome://messenger/skin/icons/filterbar.png");
+  -moz-image-region: rect(0px, 80px, 16px, 64px);
+}
+
+#qfb-results-label {
+  color: GrayText;
+}
+
+#quick-filter-bar[filterActive="matches"] #qfb-results-label {
+  color: #4e9a06;
+}
+
+#quick-filter-bar[filterActive="nomatches"] #qfb-results-label {
+  color: #cc0000;
+}
+
+#qfb-qs-textbox {
+}
--- a/mail/themes/gnomestripe/mail/searchBox.css
+++ b/mail/themes/gnomestripe/mail/searchBox.css
@@ -69,8 +69,21 @@
   -moz-appearance: none;
   padding: 0px !important;
   border: none !important;
 }
 
 .quick-search-clearbutton > .toolbarbutton-icon {
   -moz-margin-end: 0px; /* override toolkit's default value */
 }
+
+/* things from xul.css that only exist if Thunderbird did not define 
+ * AUTOCOMPLETE_OLD_STYLE 
+ */
+
+.autocomplete-history-dropmarker {
+  display: none;
+}
+
+.autocomplete-history-dropmarker[enablehistory="true"] {
+  display: -moz-box;
+  -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#history-dropmarker");
+}
--- a/mail/themes/pinstripe/jar.mn
+++ b/mail/themes/pinstripe/jar.mn
@@ -28,16 +28,17 @@ classic.jar:
   skin/classic/messenger/folderMenus.css                         (mail/folderMenus.css)
   skin/classic/messenger/folderPane.css                          (mail/folderPane.css)
   skin/classic/messenger/subscribe.css                           (mail/subscribe.css)
   skin/classic/messenger/virtualFolderListDialog.css             (mail/virtualFolderListDialog.css)
   skin/classic/messenger/searchDialog.css                        (mail/searchDialog.css)
   skin/classic/messenger/filterDialog.css                        (mail/filterDialog.css)
   skin/classic/messenger/tabmail.css                             (mail/tabmail.css)
   skin/classic/messenger/editContactOverlay.css                  (mail/editContactOverlay.css)
+  skin/classic/messenger/quickFilterBar.css                      (mail/quickFilterBar.css)
   skin/classic/messenger/starred48.png                           (mail/starred48.png)
   skin/classic/messenger/starIcons.png                           (mail/starIcons.png)
   skin/classic/messenger/activity/activity.css                   (mail/activity/activity.css)
   skin/classic/messenger/activity/buttons.png                    (mail/activity/buttons.png)  
   skin/classic/messenger/activity/defaultProcessIcon.png         (mail/activity/defaultProcessIcon.png)
   skin/classic/messenger/activity/defaultEventIcon.png           (mail/activity/defaultEventIcon.png)
   skin/classic/messenger/activity/defaultWarningIcon.png         (mail/activity/defaultWarningIcon.png)
   skin/classic/messenger/activity/undoIcon.png                   (mail/activity/undoIcon.png)
@@ -212,16 +213,20 @@ classic.jar:
   skin/classic/messenger/icons/zoomout-hover.png                 (mail/icons/zoomout-hover.png)
   skin/classic/messenger/icons/timeline.png                      (mail/icons/timeline.png)
   skin/classic/messenger/icons/timeline-inverted.png             (mail/icons/timeline-inverted.png)
   skin/classic/messenger/icons/empty-search-results.png          (mail/icons/empty-search-results.png)
   skin/classic/messenger/icons/symbolic-forward.png              (mail/icons/symbolic-forward.png)
   skin/classic/messenger/icons/symbolic-reply.png                (mail/icons/symbolic-reply.png)
   skin/classic/messenger/icons/symbolic-replyall.png             (mail/icons/symbolic-replyall.png)
   skin/classic/messenger/icons/symbolic-replylist.png            (mail/icons/symbolic-replylist.png)
+  skin/classic/messenger/icons/black_pin.png                     (mail/icons/black_pin.png)
+  skin/classic/messenger/icons/filter.png                        (mail/icons/filter.png)
+  skin/classic/messenger/icons/red_pin.png                       (mail/icons/red_pin.png)
+  skin/classic/messenger/icons/tag1616.png                       (mail/icons/tag1616.png)
   skin/classic/messenger/tabs/alltabs-box-bkgnd-icon.png              (mail/tabs/alltabs-box-bkgnd-icon.png)
   skin/classic/messenger/tabs/alltabs-box-overflow-bkgnd-animate.png  (mail/tabs/alltabs-box-overflow-bkgnd-animate.png)
   skin/classic/messenger/tabs/newtab.png                              (mail/tabs/newtab.png)
   skin/classic/messenger/tabs/tab-arrow-end.png                       (mail/tabs/tab-arrow-end.png)
   skin/classic/messenger/tabs/tab-arrow-start.png                     (mail/tabs/tab-arrow-start.png)
   skin/classic/messenger/tabs/tab-bkgnd.png                           (mail/tabs/tab-bkgnd.png)
   skin/classic/messenger/tabs/tabDragIndicator.png                    (mail/tabs/tabDragIndicator.png)
   skin/classic/messenger/tabs/tabbrowser-tabs-bkgnd.png               (mail/tabs/tabbrowser-tabs-bkgnd.png)
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..fe73f1ce1539637ccac2555c503e2636f1054a25
GIT binary patch
literal 688
zc$@*S0#E&kP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!Qb|NXRCwB4Q#*?jQ4~HinIw~BAP*62
z0xp3C8;Le4Vj8Wor3e8*-9{`d76ff9`~|YIot@aY2)fHQvXzCqjVqR$CdOz|BoLAi
z^M1_ueV1IBc;Im7p2zo{#~q_sEQ)EGvRp1@u~_ij!2N<@7~cefQzny%_DQSNlG$vg
zay-uyQ&xgN8^)bbDD)BMG440SC$8%%FfB}`SgQ|uUd3XuN8xZ-#^bRJ27{YOIMI~_
z0s&QlAB188IS9vYG#Y&vkH^KfZC%mQ-x`Uut^pum3#>a}H^~BK8I49{ybb@>d_LEJ
zYo`_qh9`O;{eEA1y`CBn?AHPQ1~Ko0!5~p8(9h8GH2@@j1>+`|?CEssAvsA0nbF?G
zR4VnDm9Z*-|Agxv^z<J9^ci}52etsduK?gR?lzN<ERje=1+uz!yS)M5T~v3<R#-J#
z;8eaVG|M_ez$TcVCzDBW97iSBYBedB%N*z*#O&*2HAH>kr1@Zm!{H%%7Os=KZ!{Xx
z?RLrTV!(T-@C!iHTB(+vDw9{JE=~_@Mzh(JN~I$Ce4f=ArBdl6oldKlsPil^Dkk5G
zeq&(20C>4tt#WvM#2gd~h5KmfLn4v5iufPg6!T0@n~z05PiPzM?1Fs``#J6pu$}E(
zF835bp?bak35k}@Q!^mif3gSX9r(8KeaIMW<7Fn3DWUvSr_(vsEtvX8(Npz3)g$Pc
z2B)1r8fI_}AeT_h3)&2nrkvSqHiR<@jX=3S2WN~neA8w_gN((B9n9nww)OU}00RKQ
Wdx3ldApk`H0000<MNUMnLSTZAM=vx0
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7ed2d204bd64aecb67948c374d66f0ee035bdc48
GIT binary patch
literal 426
zc$@*M0agBqP)<h;3K|Lk000e1NJLTq000sI000aK1^@s6ZxZdi0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUzOi4sRRCwBqlCes}KoEv^_YMRJ<N<_$
zK7dKULQoK*SXjo!Lh%WF0E+{!I7LwrJ3+xte2HM8mL`oHC|(*-u1sN}NY*bJBpkU*
z7JmMjoy?#4Cy`of;kxc5d@QA$7eai42Kgm<I}F1QQR7J4wx{5G@I+$^fmdRO_>{N{
zuORkSCDa5R@CcS@(E0rfQ)c21U<({o2`S#_fld@fx>!K;9l8Lkb*w4yiG_ZtZz-Rl
zamR6FrBJqP8tk$nwK=FiIPEZNSMWYNnJ5;>m!UPpPU=`|@?Z<GY2q#x&WL;5&62N!
z9{Fvp&!7KC4dP9(0Zj15{Q@Ee+{W5YvB~#+9mlayN{KX0g=JZPLS+Z>ZD=?D7>kJA
z73-eo=_E;nX__Jk0;xt!dw{?VJO&O2i&?W_7{B~)ZP;OFGtfEMW1{O(JNgk|0PtLo
UX_aH9bpQYW07*qoM6N<$f-20h<NyEw
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..431f0902cf0420d2f261915d7371a402b92f6e75
GIT binary patch
literal 900
zc$@)*1AF|5P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU#CP_p=RCwA<Q%i3XR}}u{&b2*$h7j9{
z@uN+IDhNVIh)1DF5fUsAVZlSJ3PgyC6;-9KSeGENL9mLDKth5Q`~XxIu%c?kLnKr<
zMNv%TD0bX9fpP5d+<9EiwN2A=b>_^R*FEPuXEccb5Y&JGNy`p-zIMWd%q_sbYXR$I
zflZt$<S=^nEQ+sRVeaDs0=f=KdXE1{4v_Ya6%3wO6(ab__N4{wHfO-5bP9z%djQvk
zijtkvrEUW0Cw+}QvWAj}>){BNR#&l9`!^ek7)a-H*n8|4l_3Dn2bczRI$_-eGAea`
z-%6$quMPC0Moz6*42DE3(U%B>VsQt;p<ue5Q=61S%I^It4X5vmRxEzJo`|F7OEiT*
zI21y}Fiy9HSk32iH^C{=bL$l0stUeRC&4;!rsfFL8Vy(kBxlsLra`!FIx{$Ub+ZvZ
zf|WV}9WW7zJRticoiSVQ`EfE$md(aWwcXBa+LG0|VeAvGgKsliur(29m@V#yPjwED
zV5QMGI#(_i2&2g$*E?!H5Banu@f?m5401Lq9P#j*T<*KAh5^jH1E?|#rt$aeEM6=w
z;&Zc=W<3BEF`*uSL#;Jk*L#^vr6voB1W+mg=4YV457VZJr~iC_*=)`0z)jX0pRT0V
zn<>+D1uqKu{2RlzUD!6qKoW0+jXN~T{&cD_oXMVxj|?yPq+`8NavBigeF=<zTI<$y
z`LczzH52*5=68!_{9dg>J9rR(Rjculb3dL+?%MT-Y`4J_i0a}5mAL)D_Zthz*dkKt
z=}I(;XXP?Nj{9-jZ1K{eu`#n+D*d`XH+RKdUWTYtI?To<CNQ{tbR)skl<oW8Z+{lw
zzZ9<bgnB*s%iHODJ9q3jkQo{(mFMTLyS9Bus<rk!@Z!u26b{?)zapllo-Z(Vl>RaP
zv+viHE}Bff4~N4))ERrcRH;19X0yAQrd6~X1c#4})%+?$G#0h8YHL#y7tnwHJkV-&
z?9FELn&)|iXf%3NyG3BAITSDb6r_L%ahNzpele0!rK5MHGl&B|Z4c*M8y_F{bX_lg
a5nurK-Csp>kAjc@0000<MNUMnLSTZ&u${dC
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4950ed6661a71ebd2684d81ecde66e3a524a2c35
GIT binary patch
literal 826
zc$@(`1I7G_P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!+(|@1RCwBiQ(0>hQ561WGLt<MjaFJj
zp+N-S)QAf%D42&X6p?6aTdRUUKwk`QPl7+d7xxDj)Vfqm5v*cSk=kHm^+g4(MSZZC
zrJ7kYGnwnX(?+TyJ#e_^o^!sl-pk#-a~I%wVNOK4DoNpHhY$b+gq}=N742ERP}phP
zwx;Vk6h($yR-kDrDwPTZ+pY?+m~<(TSl%8Cg<W8cIF5s1=q*D-!@b`oCU#Jjfxn2C
zq{x+3iIttA*XKs8Rvij5psE_prqJR%Shsdf>%3U3&oV9lKLPxHZ%T?r;Wz}DO#j5e
zg9jdd{``e_y&?tkuq+#bC}Pvb4NY}*^KO}@75pOr&7@C>1n=IxANlxUZ2y<9Ux`RT
ztObteAo5j2!T}_c$=3S%1vgF86n_WMHH~J1+xxCv9W95C92rlnSixJC<pu~**LyF2
zBKy7siC&tUnp+wg8txetGcZ+wu7T!pqs8Nm+@_7|J%LaVx^9548o2trgi}v#oP3~T
z=xqT(zkpO~d(+}Y3vbX$0z7euI~Z1hMyn}KPSOFY5Mbi^3mKPlJm$y3fG>;_{VHC4
zm_#VxLr>4{ws<_A;a`uAJ}b*ITN*T)m97;5k;mPffopFBL?t#e2UUx1$THkn(azwd
zTN@w$<n?5;*}hV#l-RO)%Mw8l=)fG@&;7u;VTWdxz_aBW6%Zwl<84u_S~?$w<sg0N
z(7@R<r?+{i-OS~3nL@F+CYjvY==1p?s|H5KRM?yd%eDcnfTOKZtX@_J)v%FHrw1=y
zIG>_edFDz(d-v^ol+PE5p~>WFZj5X`mUWrQhE^ub#(bDB{zH-7J$nYef6o(D`AMFP
zd~}(~Ovh}@2eVM5tGoL#11NIYWhOJ-Y+&{n_*y%ion3?U7m|FTNSI7@;b|Ywv>#Uk
z)lTmWb|}wnZ|}IqY`wiVy6E|XGJC=n<;wKWw0;RN03?}maj!Kp^Z)<=07*qoM6N<$
Ef)ogT^8f$<
new file mode 100644
--- /dev/null
+++ b/mail/themes/pinstripe/mail/quickFilterBar.css
@@ -0,0 +1,206 @@
+@import url("chrome://messenger/content/quickFilterBar.css");
+
+/* :::: Filter Tab Bar Button :::: */
+
+#qfb-show-filter-bar {
+  list-style-image: url("chrome://messenger/skin/icons/filter.png");
+  -moz-border-start: 2px solid;
+  -moz-border-end: none;
+  -moz-border-left-colors: rgba(0,0,0,0.25) rgba(255,255,255,0.15);
+  -moz-border-right-colors: rgba(0,0,0,0.25) rgba(255,255,255,0.15);
+  margin: 0;
+  padding: 0 4px;
+}
+
+#qfb-show-filter-bar:hover {
+  background-color: rgba(0,0,0,0.10);
+}
+
+#qfb-show-filter-bar:hover:active,
+#qfb-show-filter-bar[checked] {
+  background-color: rgba(0,0,0,0.20);
+}
+
+#qfb-show-filter-bar > .toolbarbutton-icon {
+  padding: 0 3px;
+}
+
+#qfb-show-filter-bar > .toolbarbutton-text {
+  display: none;
+}
+
+/* :::: Filter Bar :::: */
+
+#quick-filter-bar {
+  height: 25px;
+}
+
+#quick-filter-bar-main-bar {
+  background: -moz-linear-gradient(top, #eaeaea, #d1d1d1);
+  border-bottom: 1px solid #bebebe;
+}
+
+#quick-filter-bar-expando {
+  border-top: 1px solid #f9faf9;
+  background: #eaeaea;
+}
+
+#quick-filter-bar[filterActive="matches"] {
+}
+
+#threadTree[filterActive="matches"] {
+  background: -moz-repeating-linear-gradient(top, #ecf3fe 0, #ecf3fe 18px, white 18px, white 36px);
+}
+
+#quick-filter-bar[filterActive="nomatches"] {
+  background: -moz-linear-gradient(top, #ffcccc, #fd929d);
+}
+
+#threadTree[filterActive="nomatches"] {
+  background: -moz-repeating-linear-gradient(top, #fff0f4, #fff0f4 18px, white 18px, white 36px);
+}
+
+#qfb-filter-label {
+  color: #6b6b6b;
+  font-weight: bold;
+}
+
+/* :::: Filter Buttons :::: */
+
+#qfb-unread,
+#qfb-starred,
+#qfb-inaddrbook,
+#qfb-tags,
+#qfb-attachment {
+  color: #2b2b2b;
+}
+
+#qfb-unread,
+#qfb-starred,
+#qfb-inaddrbook,
+#qfb-tags,
+#qfb-attachment,
+#quick-filter-bar-expando toolbarbutton {
+  margin-top: 3pt;
+  margin-bottom: 3pt;
+  text-shadow: #e8e8e8 0 1px;
+  height: 17px;
+  font-weight: bold;
+  padding: 1px 7px;
+  -moz-border-radius: 10px;
+  border: 1px inset transparent;
+}
+
+#quick-filter-bar toolbarbutton[checked="true"],
+#quick-filter-bar toolbarbutton[checked="true"]:hover {
+  background: -moz-linear-gradient(top, #888 0px, #888 1px, #939393 1px, #8c8c8c 16px, #8e8e8e 16px, #8e8e8e 17px);
+  border-bottom-color: rgba(231, 231, 231, 0.85);
+  border-top-color: rgba(53, 53, 53, 0.4);
+  -moz-box-shadow: inset 0 0 1px #888;
+}
+
+#quick-filter-bar toolbarbutton:active,
+#quick-filter-bar toolbarbutton[checked="true"]:active,
+#quick-filter-bar toolbarbutton:active:hover {
+  background: -moz-linear-gradient(top, #686868 0px, #686868 1px, #737373 1px, #6d6d6d 16px, #717171 16px, #717171 17px);
+  border-bottom-color: rgba(231, 231, 231, 0.85);
+  border-top-color: rgba(53, 53, 53, 0.75);
+  border-left-color: rgba(53, 53, 53, 0.5);
+  border-right-color: rgba(53, 53, 53, 0.5);
+}
+
+#quick-filter-bar toolbarbutton:hover {
+  background: -moz-linear-gradient(top, #959595, #8b8b8b);
+  border-style: solid;
+  border-bottom-color: transparent;
+  border-top-color: #959595;
+}
+
+#quick-filter-bar toolbarbutton > .toolbarbutton-icon,
+#quick-filter-bar toolbarbutton > .toolbarbutton-text {
+  padding: 0;
+  margin: 0;
+}
+
+#quick-filter-bar toolbarbutton[checked="true"] > .toolbarbutton-text,
+#quick-filter-bar toolbarbutton:hover > .toolbarbutton-text {
+  color: #fff;
+  text-shadow: #535353 0 1px;
+}
+
+#quick-filter-bar #qfb-sticky,
+#quick-filter-bar #qfb-sticky[checked] {
+  background: none;
+  border-width: 0;
+  -moz-border-image: none;
+}
+
+#qfb-sticky {
+  list-style-image: url("chrome://messenger/skin/icons/black_pin.png");
+}
+
+#qfb-sticky[checked] {
+  list-style-image: url("chrome://messenger/skin/icons/red_pin.png");
+}
+
+/* we use both IDs so we are more precise than the other # toolbarbutton rules */
+#quick-filter-bar #qfb-sticky:hover {
+  text-shadow: none;
+  background: none;
+  border: 1px solid transparent;
+  -moz-border-radius: 0;
+  border-width: 0;
+}
+
+#qfb-unread {
+  list-style-image: url("chrome://messenger/skin/icons/readcol.png");
+}
+
+#qfb-unread > .toolbarbutton-icon {
+  /* 9x9 icon so pad with (2 * 2px) border */
+  border: 2px solid transparent;
+}
+
+#qfb-unread[checked] {
+  list-style-image: url("chrome://messenger/skin/icons/unreadmail.png");
+}
+
+#qfb-unread[checked] > .toolbarbutton-icon {
+  /* 13x13 icon */
+  border: none;
+}
+
+#qfb-starred {
+  list-style-image: url("chrome://messenger/skin/icons/flagcol.png");
+}
+
+#qfb-starred[checked] {
+  list-style-image: url("chrome://messenger/skin/icons/flaggedmail.png");
+}
+
+#qfb-inaddrbook {
+  list-style-image: url("chrome://messenger/skin/addressbook/icons/abcard.png");
+}
+
+#qfb-tags {
+  list-style-image: url("chrome://messenger/skin/icons/tag1616.png");
+}
+
+#qfb-tags[disabled] {
+  -moz-image-region: rect(48px, 384px, 72px, 360px) !important;
+}
+
+#qfb-attachment {
+  list-style-image: url("chrome://messenger/skin/icons/attachment.png");
+}
+
+#quick-filter-bar[filterActive="matches"] #qfb-results-label {
+  color: green;
+}
+
+#quick-filter-bar[filterActive="nomatches"] #qfb-results-label {
+  color: #f66;
+}
+
+#qfb-qs-textbox {
+}
--- a/mail/themes/qute/jar.mn
+++ b/mail/themes/qute/jar.mn
@@ -73,16 +73,17 @@ classic.jar:
   skin/classic/messenger/dialogs.css                          (mail/dialogs.css)
   skin/classic/messenger/multimessageview.css                 (mail/multimessageview.css)
   skin/classic/messenger/glodaFacetView.css                   (mail/glodaFacetView.css)
   skin/classic/messenger/icons/exclude.png                    (mail/icons/exclude.png)
   skin/classic/messenger/icons/exclude-selected.png           (mail/icons/exclude-selected.png)
   skin/classic/messenger/newmailalert.css                     (mail/newmailalert.css)
   skin/classic/messenger/tabmail.css                          (mail/tabmail.css)
   skin/classic/messenger/editContactOverlay.css               (mail/editContactOverlay.css)
+  skin/classic/messenger/quickFilterBar.css                   (mail/quickFilterBar.css)
   skin/classic/messenger/starred48.png                        (mail/starred48.png)
   skin/classic/messenger/contactStarred.png                   (mail/contactStarred.png)
   skin/classic/messenger/starContact.png                      (mail/starContact.png)
   skin/classic/messenger/activity/activity.css                   (mail/activity/activity.css)
   skin/classic/messenger/activity/buttons.png                    (mail/activity/buttons.png)
   skin/classic/messenger/activity/defaultProcessIcon.png         (mail/activity/defaultProcessIcon.png)
   skin/classic/messenger/activity/defaultEventIcon.png           (mail/activity/defaultEventIcon.png)
   skin/classic/messenger/activity/defaultWarningIcon.png         (mail/activity/defaultWarningIcon.png)
@@ -233,16 +234,19 @@ classic.jar:
   skin/classic/messenger/icons/arrow/arrow-right-dim.png      (mail/icons/arrow/arrow-right-dim.png)
   skin/classic/messenger/icons/arrow/arrow-up-dim.png         (mail/icons/arrow/arrow-up-dim.png)
   skin/classic/messenger/icons/arrow/arrow-down-dim.png       (mail/icons/arrow/arrow-down-dim.png)
   skin/classic/messenger/icons/timeline.png                   (mail/icons/timeline.png)
   skin/classic/messenger/icons/timeline-inverted.png          (mail/icons/timeline-inverted.png)
   skin/classic/messenger/icons/empty-search-results.png       (mail/icons/empty-search-results.png)
   skin/classic/messenger/icons/arrow/foldercycler-arrow-left.png        (mail/icons/arrow/foldercycler-arrow-left.png)
   skin/classic/messenger/icons/arrow/foldercycler-arrow-right.png       (mail/icons/arrow/foldercycler-arrow-right.png)
+  skin/classic/messenger/icons/filter.png                     (mail/icons/filter.png)
+  skin/classic/messenger/icons/xp-pin-grey.png                (mail/icons/xp-pin-grey.png)
+  skin/classic/messenger/icons/xp-pin-red.png                 (mail/icons/xp-pin-red.png)
   skin/classic/messenger/tagbg.png                            (mail/tagbg.png)
 % skin messenger-newsblog classic/1.0 %skin/classic/messenger-newsblog/ os=WINNT osversion<6
 % skin messenger-newsblog classic/1.0 %skin/classic/messenger-newsblog/ os!=WINNT
   skin/classic/messenger-newsblog/feed-subscriptions.css      (mail/newsblog/feed-subscriptions.css)
   skin/classic/messenger-newsblog/icons/rss-feed.png          (mail/newsblog/rss-feed.png)
   skin/classic/messenger-newsblog/icons/server-rss.png        (mail/newsblog/server-rss.png)
 #ifdef XP_WIN
 % skin communicator classic/1.0 %skin/classic/aero/communicator/ os=WINNT osversion>=6
@@ -299,16 +303,17 @@ classic.jar:
   skin/classic/aero/messenger/glodaFacetView.css                   (mail/glodaFacetView.css)
   skin/classic/aero/messenger/icons/exclude.png                    (mail/icons/exclude.png)
   skin/classic/aero/messenger/icons/exclude-selected.png           (mail/icons/exclude-selected.png)
   skin/classic/aero/messenger/icons/zoomout.png                    (mail/icons/zoomout.png)
   skin/classic/aero/messenger/icons/zoomout-hover.png              (mail/icons/zoomout-hover.png)
   skin/classic/aero/messenger/newmailalert.css                     (mail/newmailalert.css)
   skin/classic/aero/messenger/tabmail.css                          (mail/tabmail.css)
   skin/classic/aero/messenger/editContactOverlay.css               (mail/editContactOverlay.css)
+  skin/classic/aero/messenger/quickFilterBar.css                   (mail/quickFilterBar.css)
   skin/classic/aero/messenger/starred48.png                        (mail/starred48.png)
   skin/classic/aero/messenger/contactStarred.png                   (mail/contactStarred.png)
   skin/classic/aero/messenger/starContact.png                      (mail/starContact.png)
   skin/classic/aero/messenger/activity/activity.css                   (mail/activity/activity.css)
   skin/classic/aero/messenger/activity/buttons.png                    (mail/activity/buttons.png)
   skin/classic/aero/messenger/activity/defaultProcessIcon.png         (mail/activity/defaultProcessIcon-aero.png)
   skin/classic/aero/messenger/activity/defaultEventIcon.png           (mail/activity/defaultEventIcon.png)
   skin/classic/aero/messenger/activity/defaultWarningIcon.png         (mail/activity/defaultWarningIcon-aero.png)
@@ -458,15 +463,18 @@ classic.jar:
   skin/classic/aero/messenger/icons/arrow/arrow-up.png             (mail/icons/arrow/arrow-up.png)
   skin/classic/aero/messenger/icons/arrow/arrow-down.png           (mail/icons/arrow/arrow-down.png)
   skin/classic/aero/messenger/icons/arrow/arrow-left-dim.png       (mail/icons/arrow/arrow-left-dim.png)
   skin/classic/aero/messenger/icons/arrow/arrow-right-dim.png      (mail/icons/arrow/arrow-right-dim.png)
   skin/classic/aero/messenger/icons/arrow/arrow-up-dim.png         (mail/icons/arrow/arrow-up-dim.png)
   skin/classic/aero/messenger/icons/arrow/arrow-down-dim.png       (mail/icons/arrow/arrow-down-dim.png)
   skin/classic/aero/messenger/icons/arrow/foldercycler-arrow-left.png        (mail/icons/arrow/foldercycler-arrow-left.png)
   skin/classic/aero/messenger/icons/arrow/foldercycler-arrow-right.png       (mail/icons/arrow/foldercycler-arrow-right.png)
+  skin/classic/aero/messenger/icons/filter.png                     (mail/icons/filter.png)
+  skin/classic/aero/messenger/icons/xp-pin-grey.png                (mail/icons/xp-pin-grey.png)
+  skin/classic/aero/messenger/icons/xp-pin-red.png                 (mail/icons/xp-pin-red.png)
   skin/classic/aero/messenger/tagbg.png                            (mail/tagbg.png)
 % skin messenger-newsblog classic/1.0 %skin/classic/aero/messenger-newsblog/ os=WINNT osversion>=6
   skin/classic/aero/messenger-newsblog/feed-subscriptions.css      (mail/newsblog/feed-subscriptions.css)
   skin/classic/aero/messenger-newsblog/icons/rss-feed.png          (mail/newsblog/rss-feed-aero.png)
   skin/classic/aero/messenger-newsblog/icons/server-rss.png        (mail/newsblog/server-rss-aero.png)
 
 #endif
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7ed2d204bd64aecb67948c374d66f0ee035bdc48
GIT binary patch
literal 426
zc$@*M0agBqP)<h;3K|Lk000e1NJLTq000sI000aK1^@s6ZxZdi0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUzOi4sRRCwBqlCes}KoEv^_YMRJ<N<_$
zK7dKULQoK*SXjo!Lh%WF0E+{!I7LwrJ3+xte2HM8mL`oHC|(*-u1sN}NY*bJBpkU*
z7JmMjoy?#4Cy`of;kxc5d@QA$7eai42Kgm<I}F1QQR7J4wx{5G@I+$^fmdRO_>{N{
zuORkSCDa5R@CcS@(E0rfQ)c21U<({o2`S#_fld@fx>!K;9l8Lkb*w4yiG_ZtZz-Rl
zamR6FrBJqP8tk$nwK=FiIPEZNSMWYNnJ5;>m!UPpPU=`|@?Z<GY2q#x&WL;5&62N!
z9{Fvp&!7KC4dP9(0Zj15{Q@Ee+{W5YvB~#+9mlayN{KX0g=JZPLS+Z>ZD=?D7>kJA
z73-eo=_E;nX__Jk0;xt!dw{?VJO&O2i&?W_7{B~)ZP;OFGtfEMW1{O(JNgk|0PtLo
UX_aH9bpQYW07*qoM6N<$f-20h<NyEw
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4210219c707ca22f7f3abe88e55f3016957fa3b5
GIT binary patch
literal 659
zc$@)~0&M+>P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!HAzH4RCwB)QcH+ZVH7@IA27a~2EkEj
z#6{Rff^ieL@v%z~f?NcGOU1pL*+q+xfgnK$+PP6$DrgwF$y7|m2P%e&6M>J$d|~Fj
z-p(Bm9Fb<256(T8!+##%cMiaQh|un#vEc@=b^|h*?2*Z2dLflcxxrxY-E=zr4!4Cy
zqdAR4BJt&N`4{oWZnvLdn0uRCmRhYoO{deJX#{qq>-9SAa5&!Flz7Dv$JGV`fpfFj
zZ0z^@5Rb<}B9Q=2(W_Rgy~$*<!e>h~YPSFfjj&iO&uuoF89PHVnFOs?3p$;S(c>ve
zlE;lkqk{gjR4S#5#o|Ybq9)g-a5|l5*!fqXP@qz&6v@!&cDr;gm!k)R0X-g%X)M6_
zA6~EboWtQ9Zd0IJt=1(z9$@m<#bWUj^>~Ph9Hmmp(17`T4%iij!=ZS+UQ43UXl%7w
z4K@@2IaXq^SRH@KL?WSMu~>4K%Vor3z|ay11c0JKCX<13xy)?oVq1d^NalkHwMwN@
z5(<Sz92w9<C=`NnwE{<n_dkTg;n&+55PLR5jYgy2C<BPaVraM9SIG+bF|gTetS3C$
ze!RCptycTe>-8E)4YJuRNJQfIoleJZFc_Y=-R_qJK|Hv22fLWfW*5z7bBr1s<n#G3
zp7bkRUlPS!;mS>THz&BUZ_Lg8@5l%)mn%mv@cDe37*DU)KlA(jpR87^8`pUNcY#W!
tGI%_mw+e;gG29h=KL3Qr<B9$WFaX>qD1&c2Ef4?z002ovPDHLkV1nHrEs+2K
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a487949ca7e7f9033807ff5517778fc4edb154da
GIT binary patch
literal 792
zc$@(k1LypSP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00001b5ch_0Itp)
z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2igM|
z5-2M}Kd`+300NjvL_t(I%YBmDP7`4ih1dW8Gi_m7C@l$CryL3iMh%iwBLp!R^vVF2
zya5m3D-h%1LauxS6EBT-CU{IRL{v->42R00M`_E@X**00GusOxf|9Pgy_5ZAue}!o
zfa&RJ9E$*0oOjrOQ?~65hWg?s3yU!bqQOu){dhE)e5zR1B(eGk>Etjc8ktBW?hmHZ
z!!4XMQK?kkbal<kMA^CkVhj?|jj4G2!L3YYn1sN7xeRWaE}Jo4Zvz1USHj`mvEAL-
z@oe^HjR@~cC7`VhlG|PRDMY#;#DPuZAR^8gtFV7J@D~c>qnXSIIL9Z$KvOV?i-7=2
z#o~oju{g8Y(h_w^l9{j7wtkuBED>$8lK{66H6hDI{qV5a;q&2YAb^6dBPs;$bai!1
z=z51x6c{}rV=kWPoXckKS(3EQk1g~K4JB7xF86nrE0$4|^>gKNa8H&U=iA!gUs{5^
zybQrO^u4THT3u`3^teA%oX&MlbaX_bc*^U2nDF_W%(9$^n&z1b1jhW42(Y#Ws48|+
zdx(8o!D?^!J3~>vaN9opMoyIEmW3*SM0@*&oAWW`azIeULZb(-BpIr=IkFt;d;(CC
zofV`b5Nz9y9V$u{{r%X1gpC6o`vd*hpSbIgn>-G(th18{01SXxt-eiry)oV0jIEqz
zQ)9z|U-0MMp-@l%rOS_m5ZCP^3jzSb;qYGoW2g~nX~S?GZ11#gd6XYt`+8@za`~BA
zC^V_6dd=x{4&-vV2oa_K0{{SLtg2P3i`z=$>pj2krOo*w2MYOoeqPhGtg5QF0W|2k
z{_#w}5u^qoGGHvni2n3AHx0x1tZ7=JwY7E9G|iM$FXzXDr?M{u48wR4jYeZc6#oqp
W3pDkIWX<&e0000<MNUMnLSTY6C}h<D
new file mode 100644
--- /dev/null
+++ b/mail/themes/qute/mail/quickFilterBar.css
@@ -0,0 +1,169 @@
+@import url("chrome://messenger/content/quickFilterBar.css");
+
+/* :::: Filter Tab Bar Button :::: */
+
+#qfb-show-filter-bar {
+  -moz-appearance: none;
+  list-style-image: url("chrome://messenger/skin/icons/filter.png");
+  background: -moz-linear-gradient(top, #e3e3e3 0px, #e3e3e3 9px, #dedede 9px, #dedede 10px, #d9d9d9 10px, #e5e5e5 100%);
+  border-right: 2px solid;
+  border-top: 2px solid;
+  border-left: 2px solid;
+  -moz-border-top-colors: threedshadow #f2f2f2;
+  -moz-border-left-colors: threedshadow #f2f2f2;
+  -moz-border-right-colors: threedshadow #f2f2f2;
+  border-bottom: none;
+  -moz-border-radius: 4px 4px 0pt 0pt;
+  margin: 2px 2px 3px 0;
+  padding: 0 !important;
+  -moz-padding-start: 4px !important;
+  -moz-padding-end: 4px !important;
+}
+
+#qfb-show-filter-bar:hover {
+  background: -moz-linear-gradient(top, #f6f6f6 0px, #ededed 9px, #e4e4e4 9px, #e4e4e4 10px, #d6d6d6 10px, #e4e4e4 100%);
+}
+
+#qfb-show-filter-bar:hover:active,
+#qfb-show-filter-bar[checked],
+#qfb-show-filter-bar[checked]:hover {
+  -moz-border-top-colors: ThreeDDarkShadow #bdbdbd;
+  -moz-border-left-colors: ThreeDDarkShadow #bdbdbd;
+  -moz-border-right-colors: ThreeDDarkShadow #bdbdbd;
+  background: #cbcbcb;
+  padding: 0 !important;
+  -moz-padding-start: 4px !important;
+  -moz-padding-end: 4px !important;
+}
+
+#qfb-show-filter-bar > .toolbarbutton-icon {
+  padding: 0 3px;
+}
+
+#qfb-show-filter-bar > .toolbarbutton-text {
+  display: none;
+}
+
+/* :::: Filter Bar :::: */
+
+#quick-filter-bar-main-bar {
+  background: -moz-linear-gradient(top, #f2f7fd 0, #e9f2fc 12px, #e3eefb 12px, #e3eefb 100%);
+  border-left: 2px solid;
+  -moz-border-left-colors: #9196a2 #fff;
+  border-top: 1px solid #fff;
+  border-bottom: 2px solid;
+  -moz-border-bottom-colors: #9196a2 #fff;
+}
+
+#quick-filter-bar-expando {
+  border-left: 2px solid;
+  -moz-border-left-colors: #9196a2 #fff;
+  background: #f2f6fb;  
+}
+
+#quick-filter-bar-tab-bar,
+#quick-filter-bar-filter-text-bar {
+  border-top: 1px solid #fff;
+  border-bottom: 2px solid;
+  -moz-border-bottom-colors: #9196a2 #fff;
+}
+
+#quick-filter-bar[filterActive="searching"] {
+  
+}
+#threadTree[filterActive="searching"] {
+  background-color: #ffffcc;
+}
+
+#quick-filter-bar[filterActive="matches"] {
+  
+}
+#threadTree[filterActive="matches"] {
+  background-color: #f2f9fc;
+}
+
+#quick-filter-bar[filterActive="nomatches"] {
+  
+}
+#threadTree[filterActive="nomatches"] {
+  background: -moz-repeating-linear-gradient(top left -45deg, #fff0f4, #fff0f4 5px, white 5px, white 10px);
+}
+
+/* :::: Filter Buttons :::: */
+
+#quick-filter-bar-main-bar toolbarbutton {
+  -moz-margin-start: 1px;
+  -moz-margin-end: 1px;
+}
+
+/* keep that hideous outline focus ring from showing on the checked buttons */
+#quick-filter-bar-main-bar toolbarbutton:focus {
+  outline: none;
+}
+
+#qfb-filter-label {
+  color: GrayText;
+}
+
+#qfb-sticky {
+  list-style-image: url("chrome://messenger/skin/icons/xp-pin-grey.png");
+}
+
+#qfb-sticky[checked] {
+  list-style-image: url("chrome://messenger/skin/icons/xp-pin-red.png");
+}
+
+#qfb-unread {
+  list-style-image: url("chrome://messenger/skin/icons/readmail.png");
+}
+
+#qfb-unread[checked] {
+  list-style-image: url("chrome://messenger/skin/icons/unreadmail.png");
+}
+
+#qfb-starred {
+  list-style-image: url("chrome://messenger/skin/icons/flag-empty.png");
+}
+
+#qfb-starred[checked] {
+  list-style-image: url("chrome://messenger/skin/icons/flag-col.png");
+}
+
+.qfb-starred-nostar {
+  list-style-image: url("chrome://messenger/skin/starContact.png");
+  -moz-image-region:rect(0px 32px 16px 16px);
+}
+
+#qfb-inaddrbook {
+  list-style-image: url("chrome://messenger/skin/addressbook/icons/abcard.png");
+}
+
+#qfb-tags {
+  list-style-image: url("chrome://messenger/skin/icons/mail-toolbar-small.png");
+  -moz-image-region: rect(0px 256px 16px 240px);
+}
+
+#qfb-tags[disabled] {
+  -moz-image-region: rect(48px, 384px, 72px, 360px) !important;
+}
+
+#qfb-attachment {
+  list-style-image: url("chrome://messenger/skin/icons/attachment-col.png");
+}
+
+#quick-filter-bar[filterActive="matches"] #qfb-results-label {
+  color: green;
+}
+
+#quick-filter-bar[filterActive="nomatches"] #qfb-results-label {
+  color: #f66;
+}
+
+#quick-filter-bar-expando toolbarbutton,
+#quick-filter-bar-expando toolbarbutton[checked] {
+  -moz-padding-end: 6px !important;
+  padding-top: 1px;
+  padding-bottom: 1px;
+  -moz-margin-start: 1px;
+  -moz-margin-end: 1px;
+}
--- a/mailnews/base/public/nsIMsgDBView.idl
+++ b/mailnews/base/public/nsIMsgDBView.idl
@@ -241,17 +241,17 @@ interface nsMsgNavigationType
   const nsMsgNavigationTypeValue previousFlagged = 19;
   const nsMsgNavigationTypeValue firstNew = 20;
   const nsMsgNavigationTypeValue editUndo = 21;
   const nsMsgNavigationTypeValue editRedo = 22;
   const nsMsgNavigationTypeValue toggleSubthreadKilled = 23;
 };
 
 
-[scriptable, uuid(7b6f1f8f-f27a-409b-955b-ab40d46194e1)]
+[scriptable, uuid(73b3c502-ac7b-4c3f-9265-8804f7ac11fe)]
 interface nsIMsgDBView : nsISupports
 {
   void open(in nsIMsgFolder folder, in nsMsgViewSortTypeValue sortType, in nsMsgViewSortOrderValue sortOrder, in nsMsgViewFlagsTypeValue viewFlags, out long count);
   void openWithHdrs(in nsISimpleEnumerator aHeaders, in nsMsgViewSortTypeValue aSortType, 
                       in nsMsgViewSortOrderValue aSortOrder, 
                       in nsMsgViewFlagsTypeValue aViewFlags, out long aCount);
   void close();
 
@@ -364,16 +364,22 @@ interface nsIMsgDBView : nsISupports
    *  "mail.operate_on_msgs_in_collapsed_threads" preference is enabled, then
    *  any collapsed thread roots that are selected will also conceptually have
    *  all of the messages in that thread selected.
    */
   readonly attribute unsigned long numSelected;
   readonly attribute nsMsgViewIndex msgToSelectAfterDelete; 
   readonly attribute nsMsgViewIndex currentlyDisplayedMessage; 
 
+  /**
+   * Number of messages in view, including messages in collapsed threads.
+   * Not currently implemented for threads with unread or watched threads
+   * with unread.
+   */
+  readonly attribute long numMsgsInView;
   // used by "go to folder" feature
   // and "remember last selected message" feature
   // if key is not found, we don't select.
   void selectMsgByKey(in nsMsgKey key);
 
   void selectFolderMsgByKey(in nsIMsgFolder aFolder, in nsMsgKey aKey);
   // we'll suppress displaying messages if the message pane is collapsed
   attribute boolean suppressMsgDisplay;
--- a/mailnews/base/search/public/nsIMsgSearchSession.idl
+++ b/mailnews/base/search/public/nsIMsgSearchSession.idl
@@ -47,101 +47,125 @@ interface nsIMsgHdr;
 interface nsIMsgDatabase;
 
 //////////////////////////////////////////////////////////////////////////////
 // The Msg Search Session is an interface designed to make constructing
 // searches easier. Clients typically build up search terms, and then run
 // the search
 //////////////////////////////////////////////////////////////////////////////
 
-[scriptable, uuid(a819050a-0302-11d3-a50a-0060b0fc04b7)]
+[scriptable, uuid(86c508e5-bf38-44d5-9af9-87a84754321f)]
 interface nsIMsgSearchSession :  nsISupports {
 
 /**
  * add a search term to the search session
  *
  * @param   attrib        search attribute (e.g. nsMsgSearchAttrib::Subject)
  * @param   op            search operator (e.g. nsMsgSearchOp::Contains)
  * @param   value         search value (e.g. "Dogbert", see nsIMsgSearchValue)
  * @param   BooleanAND    set to true if associated boolean operator is AND
  * @param   customString  if attrib > nsMsgSearchAttrib::OtherHeader,
  *                            a user defined arbitrary header
  *                        if attrib == nsMsgSearchAttrib::Custom, the custom id
  *                        otherwise ignored
  */
-    void addSearchTerm(in nsMsgSearchAttribValue attrib,
-                       in nsMsgSearchOpValue op,
-                       in nsIMsgSearchValue value,
-                       in boolean BooleanAND,
-                       in string customString);
+  void addSearchTerm(in nsMsgSearchAttribValue attrib,
+                     in nsMsgSearchOpValue op,
+                     in nsIMsgSearchValue value,
+                     in boolean BooleanAND,
+                     in string customString);
 
-    readonly attribute nsISupportsArray searchTerms;
+  readonly attribute nsISupportsArray searchTerms;
 
   nsIMsgSearchTerm createTerm ();
-    void appendTerm(in nsIMsgSearchTerm term);
+  void appendTerm(in nsIMsgSearchTerm term);
+
+  /**
+   * @name Search notification flags
+   * These flags determine which notifications will be sent.
+   * @{
+   */
+  /// search started notification
+  const long onNewSearch = 0x1;
+
+  /// search finished notification
+  const long onSearchDone = 0x2;
 
-  void registerListener (in nsIMsgSearchNotify listener);
+  /// search hit notification
+  const long onSearchHit = 0x4;
+
+  const long allNotifications = 0x7;
+  /** @} */
+
+  /**
+   * Add a listener to get notified of search starts, stops, and hits.
+   *
+   * @param aListener listener
+   * @param aNotifyFlags which notifications to send. Defaults to all
+   */
+  void registerListener (in nsIMsgSearchNotify aListener,
+                         [optional] in long aNotifyFlags);
   void unregisterListener (in nsIMsgSearchNotify listener);
 
-    readonly attribute unsigned long numSearchTerms;
+  readonly attribute unsigned long numSearchTerms;
 
   readonly attribute nsIMsgSearchAdapter runningAdapter;
 
-    void getNthSearchTerm(in long whichTerm,
-                          in nsMsgSearchAttribValue attrib,
-                          in nsMsgSearchOpValue op,
-                          in nsIMsgSearchValue value); // wrong, should be out
+  void getNthSearchTerm(in long whichTerm,
+                        in nsMsgSearchAttribValue attrib,
+                        in nsMsgSearchOpValue op,
+                        in nsIMsgSearchValue value); // wrong, should be out
 
-    long countSearchScopes();
+  long countSearchScopes();
 
-    void getNthSearchScope(in long which,out nsMsgSearchScopeValue scopeId, out nsIMsgFolder folder);
+  void getNthSearchScope(in long which,out nsMsgSearchScopeValue scopeId, out nsIMsgFolder folder);
 
   /* add a scope (e.g. a mail folder) to the search */
-    void addScopeTerm(in nsMsgSearchScopeValue scope,
-                      in nsIMsgFolder folder);
+  void addScopeTerm(in nsMsgSearchScopeValue scope,
+                    in nsIMsgFolder folder);
 
-    void addDirectoryScopeTerm(in nsMsgSearchScopeValue scope);
+  void addDirectoryScopeTerm(in nsMsgSearchScopeValue scope);
 
-    void clearScopes();
+  void clearScopes();
 
-    /* Call this function everytime the scope changes! It informs the FE if
-       the current scope support custom header use. FEs should not display the
-       custom header dialog if custom headers are not supported */
-    [noscript] boolean ScopeUsesCustomHeaders(in nsMsgSearchScopeValue scope,
-                                   /* could be a folder or server based on scope */
-                                   in voidPtr selection,
-                                   in boolean forFilters);
+  /* Call this function everytime the scope changes! It informs the FE if
+     the current scope support custom header use. FEs should not display the
+     custom header dialog if custom headers are not supported */
+  [noscript] boolean ScopeUsesCustomHeaders(in nsMsgSearchScopeValue scope,
+                                 /* could be a folder or server based on scope */
+                                 in voidPtr selection,
+                                 in boolean forFilters);
 
-    /* use this to determine if your attribute is a string attrib */
-    boolean IsStringAttribute(in nsMsgSearchAttribValue attrib);
+  /* use this to determine if your attribute is a string attrib */
+  boolean IsStringAttribute(in nsMsgSearchAttribValue attrib);
 
-    /* add all scopes of a given type to the search */
-    void AddAllScopes(in nsMsgSearchScopeValue attrib);
+  /* add all scopes of a given type to the search */
+  void AddAllScopes(in nsMsgSearchScopeValue attrib);
 
-    void search(in nsIMsgWindow aWindow);
-    void interruptSearch();
+  void search(in nsIMsgWindow aWindow);
+  void interruptSearch();
 
   // these two methods are used when the search session is using
   // a timer to do local search, and the search adapter needs
   // to run a url (e.g., to reparse a local folder) and wants to
   // pause the timer while running the url. This will fail if the
   // current adapter is not using a timer.
-    void pauseSearch();
+  void pauseSearch();
   void resumeSearch();
 
-    [noscript] readonly attribute voidPtr searchParam;
-    readonly attribute nsMsgSearchType searchType;
+  [noscript] readonly attribute voidPtr searchParam;
+  readonly attribute nsMsgSearchType searchType;
 
-    [noscript] nsMsgSearchType SetSearchParam(in nsMsgSearchType type,
-                                              in voidPtr param);
+  [noscript] nsMsgSearchType SetSearchParam(in nsMsgSearchType type,
+                                            in voidPtr param);
 
   [noscript] void AddResultElement(in nsMsgResultElement element);
   boolean MatchHdr(in nsIMsgDBHdr aMsgHdr, in nsIMsgDatabase aDatabase);
 
   void addSearchHit(in nsIMsgDBHdr header, in nsIMsgFolder folder);
 
-    readonly attribute long numResults;
+  readonly attribute long numResults;
   attribute nsIMsgWindow window;
 
-    /* these longs are all actually of type nsMsgSearchBooleanOp */
-    const long BooleanOR=0;
-    const long BooleanAND=1;
+  /* these longs are all actually of type nsMsgSearchBooleanOp */
+  const long BooleanOR=0;
+  const long BooleanAND=1;
 };
--- a/mailnews/base/search/src/nsMsgFilterService.cpp
+++ b/mailnews/base/search/src/nsMsgFilterService.cpp
@@ -367,17 +367,18 @@ nsresult nsMsgFilterAfterTheFact::RunNex
   for (PRUint32 termIndex = 0; termIndex < termCount; termIndex++)
   {
     nsCOMPtr <nsIMsgSearchTerm> term;
     rv = searchTerms->QueryElementAt(termIndex, NS_GET_IID(nsIMsgSearchTerm), getter_AddRefs(term));
     NS_ENSURE_SUCCESS(rv, rv);
     rv = m_searchSession->AppendTerm(term);
     NS_ENSURE_SUCCESS(rv, rv);
   }
-  m_searchSession->RegisterListener(this);
+  m_searchSession->RegisterListener(this,
+                                    nsIMsgSearchSession::allNotifications);
 
   rv = m_searchSession->AddScopeTerm(searchScope, m_curFolder);
   NS_ENSURE_SUCCESS(rv, rv);
   m_nextAction = 0;
   // it's possible that this error handling will need to be rearranged when mscott lands the UI for
   // doing filters based on sender in PAB, because we can't do that for IMAP. I believe appending the
   // search term will fail, or the Search itself will fail synchronously. In that case, we'll
   // have to ignore the filter, I believe. Ultimately, we'd like to re-work the search backend
--- a/mailnews/base/search/src/nsMsgSearchSession.cpp
+++ b/mailnews/base/search/src/nsMsgSearchSession.cpp
@@ -128,28 +128,43 @@ nsMsgSearchSession::CreateTerm(nsIMsgSea
     NS_ENSURE_TRUE(term, NS_ERROR_OUT_OF_MEMORY);
 
     *aResult = static_cast<nsIMsgSearchTerm*>(term);
     NS_ADDREF(*aResult);
     return NS_OK;
 }
 
 /* void RegisterListener (in nsIMsgSearchNotify listener); */
-NS_IMETHODIMP nsMsgSearchSession::RegisterListener(nsIMsgSearchNotify *aListener)
+NS_IMETHODIMP nsMsgSearchSession::RegisterListener(nsIMsgSearchNotify *aListener,
+                                                   PRInt32 aNotifyFlags)
 {
   NS_ENSURE_ARG_POINTER(aListener);
   m_listenerList.AppendElement(aListener);
+  m_listenerFlagList.AppendElement(aNotifyFlags);
   return NS_OK;
 }
 
 /* void UnregisterListener (in nsIMsgSearchNotify listener); */
 NS_IMETHODIMP nsMsgSearchSession::UnregisterListener(nsIMsgSearchNotify *aListener)
 {
   NS_ENSURE_ARG_POINTER(aListener);
-  m_listenerList.RemoveElement(aListener);
+  PRInt32 listenerIndex = m_listenerList.IndexOf(aListener);
+  if (listenerIndex != -1)
+  {
+    m_listenerList.RemoveElementAt(listenerIndex);
+    m_listenerFlagList.RemoveElementAt(listenerIndex);
+
+    // Adjust our iterator if it is active.
+    // Removal of something at a higher index than the iterator does not affect
+    // it; we only care if the the index we were pointing at gets shifted down,
+    // in which case we also want to shift down.
+    if (m_iListener != -1 && listenerIndex <= m_iListener)
+      m_iListener--;
+  }
+
   return NS_OK;
 }
 
 /* readonly attribute long numSearchTerms; */
 NS_IMETHODIMP nsMsgSearchSession::GetNumSearchTerms(PRUint32 *aNumSearchTerms)
 {
   NS_ENSURE_ARG(aNumSearchTerms);
   return m_termList->Count(aNumSearchTerms);
@@ -253,23 +268,26 @@ nsMsgSearchSession::AddAllScopes(nsMsgSe
 }
 
 /* void Search (); */
 NS_IMETHODIMP nsMsgSearchSession::Search(nsIMsgWindow *aWindow)
 {
   nsresult rv = Initialize();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  nsTObserverArray<nsCOMPtr<nsIMsgSearchNotify> >::ForwardIterator iter(m_listenerList);
   nsCOMPtr<nsIMsgSearchNotify> listener;
-  while (iter.HasMore())
+  m_iListener = 0;
+  while (m_iListener != -1 && m_iListener < m_listenerList.Length())
   {
-    listener = iter.GetNext();
-    listener->OnNewSearch();
+    listener = m_listenerList[m_iListener];
+    PRInt32 listenerFlags = m_listenerFlagList[m_iListener++];
+    if (!listenerFlags || (listenerFlags & nsIMsgSearchSession::onNewSearch))
+      listener->OnNewSearch();
   }
+  m_iListener = -1;
 
   m_msgWindowWeak = do_GetWeakReference(aWindow);
 
   return BeginSearching();
 }
 
 /* void InterruptSearch (); */
 NS_IMETHODIMP nsMsgSearchSession::InterruptSearch()
@@ -568,40 +586,45 @@ NS_IMETHODIMP nsMsgSearchSession::GetRun
   }
   *aSearchAdapter = nsnull;
   return NS_OK;
 }
 
 NS_IMETHODIMP nsMsgSearchSession::AddSearchHit(nsIMsgDBHdr *aHeader,
                                                nsIMsgFolder *aFolder)
 {
-  nsTObserverArray<nsCOMPtr<nsIMsgSearchNotify> >::ForwardIterator iter(m_listenerList);
   nsCOMPtr<nsIMsgSearchNotify> listener;
-  while (iter.HasMore())
+  m_iListener = 0;
+  while (m_iListener != -1 && m_iListener < m_listenerList.Length())
   {
-    listener = iter.GetNext();
-    listener->OnSearchHit(aHeader, aFolder);
+    listener = m_listenerList[m_iListener];
+    PRInt32 listenerFlags = m_listenerFlagList[m_iListener++];
+    if (!listenerFlags || (listenerFlags & nsIMsgSearchSession::onSearchHit))
+      listener->OnSearchHit(aHeader, aFolder);
   }
+  m_iListener = -1;
   return NS_OK;
 }
 
 nsresult nsMsgSearchSession::NotifyListenersDone(nsresult aStatus)
 {
   // need to stabilize "this" in case one of the listeners releases the last
   // reference to us.
   nsRefPtr <nsIMsgSearchSession> kungFuDeathGrip(this);
 
-  nsTObserverArray<nsCOMPtr<nsIMsgSearchNotify> >::ForwardIterator iter(m_listenerList);
   nsCOMPtr<nsIMsgSearchNotify> listener;
-
-  while (iter.HasMore())
+  m_iListener = 0;
+  while (m_iListener != -1 && m_iListener < m_listenerList.Length())
   {
-    listener = iter.GetNext();
-    listener->OnSearchDone(aStatus);
+    listener = m_listenerList[m_iListener];
+    PRInt32 listenerFlags = m_listenerFlagList[m_iListener++];
+    if (!listenerFlags || (listenerFlags & nsIMsgSearchSession::onSearchDone))
+      listener->OnSearchDone(aStatus);
   }
+  m_iListener = -1;
   return NS_OK;
 }
 
 
 NS_IMETHODIMP nsMsgSearchSession::AddResultElement (nsMsgResultElement *element)
 {
   NS_ASSERTION(element, "no null elements");
 
--- a/mailnews/base/search/src/nsMsgSearchSession.h
+++ b/mailnews/base/search/src/nsMsgSearchSession.h
@@ -77,17 +77,36 @@ protected:
   nsresult SearchWOUrls ();
   nsresult GetNextUrl();
   nsresult NotifyListenersDone(nsresult status);
   void EnableFolderNotifications(PRBool aEnable);
   void ReleaseFolderDBRef();
 
   nsMsgSearchScopeTermArray m_scopeList;
   nsCOMPtr <nsISupportsArray> m_termList;
-  nsTObserverArray<nsCOMPtr<nsIMsgSearchNotify> > m_listenerList;
+
+  nsTArray<nsCOMPtr<nsIMsgSearchNotify> > m_listenerList;
+  nsTArray<PRInt32> m_listenerFlagList;
+  /**
+   * Iterator index for m_listenerList/m_listenerFlagList.  We used to use an
+   * nsTObserverArray for m_listenerList but its auto-adjusting iterator was
+   * not helping us keep our m_listenerFlagList iterator correct.
+   *
+   * We are making the simplifying assumption that our notifications are
+   * non-reentrant.  In the exceptional case that it turns out they are
+   * reentrant, we assume that this is the result of canceling a search while
+   * the session is active and initiating a new one.  In that case, we assume
+   * the outer iteration can safely be abandoned.
+   *
+   * This value is defined to be the index of the next listener we will process.
+   * This allows us to use the sentinel value of -1 to convey that no iteration
+   * is in progress (and the iteration process to abort if the value transitions
+   * to -1, which we always set on conclusion of our loop).
+   */
+  PRInt32 m_iListener;
 
   nsMsgResultArray m_resultList;
 
   void DestroyTermList ();
   void DestroyScopeList ();
   void DestroyResultList ();
 
   static void TimerCallback(nsITimer *aTimer, void *aClosure);
--- a/mailnews/base/src/nsMsgDBView.cpp
+++ b/mailnews/base/src/nsMsgDBView.cpp
@@ -6754,16 +6754,22 @@ nsMsgDBView::GetNumSelected(PRUint32 *aN
       ExpansionDelta(selection[i], &collapsedCount);
       numSelectedIncludingCollapsed += collapsedCount;
     }
   }
   *aNumSelected = numSelectedIncludingCollapsed;
   return rv;
 }
 
+NS_IMETHODIMP nsMsgDBView::GetNumMsgsInView(PRInt32 *aNumMsgs)
+{
+  NS_ENSURE_ARG_POINTER(aNumMsgs);
+  return (m_folder) ? m_folder->GetTotalMessages(PR_FALSE, aNumMsgs) :
+                    NS_ERROR_FAILURE;
+}
 /**
  * @note For the IMAP delete model, this applies to both deleting and 
  *       undeleting a message.
  */
 NS_IMETHODIMP
 nsMsgDBView::GetMsgToSelectAfterDelete(nsMsgViewIndex *msgToSelectAfterDelete)
 {
   NS_ENSURE_ARG_POINTER(msgToSelectAfterDelete);
--- a/mailnews/base/src/nsMsgPurgeService.cpp
+++ b/mailnews/base/src/nsMsgPurgeService.cpp
@@ -391,17 +391,18 @@ nsresult nsMsgPurgeService::PerformPurge
   return rv;
 }
 
 nsresult nsMsgPurgeService::SearchFolderToPurge(nsIMsgFolder *folder, PRInt32 purgeInterval)
 {
   nsresult rv;
   mSearchSession = do_CreateInstance(NS_MSGSEARCHSESSION_CONTRACTID, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
-  mSearchSession->RegisterListener(this);
+  mSearchSession->RegisterListener(this,
+                                   nsIMsgSearchSession::allNotifications);
 
   // update the time we attempted to purge this folder
   char dateBuf[100];
   dateBuf[0] = '\0';
   PRExplodedTime exploded;
   PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded);
   PR_FormatTimeUSEnglish(dateBuf, sizeof(dateBuf), "%a %b %d %H:%M:%S %Y", &exploded);
   folder->SetStringProperty("curJunkFolderLastPurgeTime", nsDependentCString(dateBuf));
--- a/mailnews/base/src/nsMsgQuickSearchDBView.cpp
+++ b/mailnews/base/src/nsMsgQuickSearchDBView.cpp
@@ -51,18 +51,17 @@
 
 nsMsgQuickSearchDBView::nsMsgQuickSearchDBView()
 {
   m_usingCachedHits = PR_FALSE;
   m_cacheEmpty = PR_TRUE;
 }
 
 nsMsgQuickSearchDBView::~nsMsgQuickSearchDBView()
-{	
- /* destructor code */
+{
 }
 
 NS_IMPL_ISUPPORTS_INHERITED2(nsMsgQuickSearchDBView, nsMsgDBView, nsIMsgDBView, nsIMsgSearchNotify)
 
 NS_IMETHODIMP nsMsgQuickSearchDBView::Open(nsIMsgFolder *folder, nsMsgViewSortTypeValue sortType, nsMsgViewSortOrderValue sortOrder, nsMsgViewFlagsTypeValue viewFlags, PRInt32 *pCount)
 {
   nsresult rv = nsMsgDBView::Open(folder, sortType, sortOrder, viewFlags, pCount);
   NS_ENSURE_SUCCESS(rv, rv);
@@ -89,16 +88,30 @@ nsMsgQuickSearchDBView::CloneDBView(nsIM
 
   nsresult rv = CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
   NS_ENSURE_SUCCESS(rv, rv);
 
   NS_IF_ADDREF(*_retval = newMsgDBView);
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::CopyDBView(nsMsgDBView *aNewMsgDBView,
+                                   nsIMessenger *aMessengerInstance,
+                                   nsIMsgWindow *aMsgWindow,
+                                   nsIMsgDBViewCommandUpdater *aCmdUpdater)
+{
+  nsMsgThreadedDBView::CopyDBView(aNewMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
+  nsMsgQuickSearchDBView* newMsgDBView = (nsMsgQuickSearchDBView *) aNewMsgDBView;
+
+  // now copy all of our private member data
+  newMsgDBView->m_origKeys = m_origKeys;
+  return NS_OK;
+}
+
 nsresult nsMsgQuickSearchDBView::DeleteMessages(nsIMsgWindow *window, nsMsgViewIndex *indices, PRInt32 numIndices, PRBool deleteStorage)
 {
   for (nsMsgViewIndex i = 0; i < (nsMsgViewIndex) numIndices; i++) 
   {
     nsCOMPtr<nsIMsgDBHdr> msgHdr; 
     (void) GetMsgHdrForViewIndex(indices[i],getter_AddRefs(msgHdr));
     if (msgHdr)
       RememberDeletedMsgHdr(msgHdr);
@@ -187,17 +200,19 @@ nsresult nsMsgQuickSearchDBView::OnNewHe
   return NS_OK;
 }
 
 NS_IMETHODIMP nsMsgQuickSearchDBView::OnHdrFlagsChanged(nsIMsgDBHdr *aHdrChanged, PRUint32 aOldFlags, 
                                        PRUint32 aNewFlags, nsIDBChangeListener *aInstigator)
 {
   nsresult rv = nsMsgGroupView::OnHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags, aInstigator);
 
-  if (m_viewFolder && (aOldFlags & nsMsgMessageFlags::Read) != (aNewFlags & nsMsgMessageFlags::Read))
+  if (m_viewFolder &&
+      (m_viewFolder != m_folder) &&
+      (aOldFlags & nsMsgMessageFlags::Read) != (aNewFlags & nsMsgMessageFlags::Read))
   {
     // if we're displaying a single folder virtual folder for an imap folder,
     // the search criteria might be on message body, and we might not have the
     // message body offline, in which case we can't tell if the message 
     // matched or not. But if the unread flag changed, we need to update the
     // unread counts. Normally, VirtualFolderChangeListener::OnHdrFlagsChanged will
     // handle this, but it won't work for body criteria when we don't have the
     // body offline.
@@ -313,17 +328,20 @@ nsMsgQuickSearchDBView::OnSearchHit(nsIM
     return AddHdr(aMsgHdr); 
   else
     return NS_OK;
 }
 
 NS_IMETHODIMP
 nsMsgQuickSearchDBView::OnSearchDone(nsresult status)
 {
-  if (m_viewFolder)
+  // We're a single-folder virtual folder if viewFolder != folder, and that is
+  // the only case in which we want to be messing about with a results cache
+  // or unread counts.
+  if (m_viewFolder && m_viewFolder != m_folder)
   {
     nsTArray<nsMsgKey> keyArray;
     nsCString searchUri;
     m_viewFolder->GetURI(searchUri);
     PRUint32 count = m_hdrHits.Count();
     // build up message keys.
     PRUint32 i;
     for (i = 0; i < count; i++)
@@ -344,23 +362,41 @@ nsMsgQuickSearchDBView::OnSearchDone(nsr
       for (i = 0; i < numBadHits; i++)
       {
         m_db->GetMsgHdrForKey(staleHits[i], getter_AddRefs(hdrDeleted));
         if (hdrDeleted)
           OnHdrDeleted(hdrDeleted, nsMsgKey_None, 0, this);
       }
       delete [] staleHits;
     }
+    nsCOMPtr<nsIMsgDatabase> virtDatabase;
+    nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+    nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo), getter_AddRefs(virtDatabase));
+    NS_ENSURE_SUCCESS(rv, rv);
+    PRUint32 numUnread = 0;
+    PRUint32 numTotal = m_origKeys.Length();
+
+    for (i = 0; i < m_origKeys.Length(); i++)
+    {
+      PRBool isRead;
+      m_db->IsRead(m_origKeys[i], &isRead);
+      if (!isRead)
+        numUnread++;
+    }
+    dbFolderInfo->SetNumUnreadMessages(numUnread);
+    dbFolderInfo->SetNumMessages(numTotal);
+    m_viewFolder->UpdateSummaryTotals(true); // force update from db.
+    virtDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
   }
   if (m_sortType != nsMsgViewSortType::byThread)//we do not find levels for the results.
   {
     m_sortValid = PR_FALSE;       //sort the results 
     Sort(m_sortType, m_sortOrder);
   }
-  if (m_viewFolder)
+  if (m_viewFolder && (m_viewFolder != m_folder))
     SetMRUTimeForFolder(m_viewFolder);
 
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsMsgQuickSearchDBView::OnNewSearch()
@@ -832,8 +868,31 @@ NS_IMETHODIMP nsMsgQuickSearchDBView::Se
   return rv;
 }
 
 nsresult 
 nsMsgQuickSearchDBView::GetMessageEnumerator(nsISimpleEnumerator **enumerator)
 {
   return GetViewEnumerator(enumerator);
 }
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::OnHdrDeleted(nsIMsgDBHdr *aHdrDeleted,
+                                     nsMsgKey aParentKey,
+                                     PRInt32 aFlags,
+                                     nsIDBChangeListener *aInstigator)
+{
+  NS_ENSURE_ARG_POINTER(aHdrDeleted);
+  nsMsgKey msgKey;
+  aHdrDeleted->GetMessageKey(&msgKey);
+  PRInt32 keyIndex = m_origKeys.BinaryIndexOf(msgKey);
+  if (keyIndex != -1)
+    m_origKeys.RemoveElementAt(keyIndex);
+  return nsMsgThreadedDBView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags,
+                                           aInstigator);
+}
+
+NS_IMETHODIMP nsMsgQuickSearchDBView::GetNumMsgsInView(PRInt32 *aNumMsgs)
+{
+  NS_ENSURE_ARG_POINTER(aNumMsgs);
+  *aNumMsgs = m_origKeys.Length();
+  return NS_OK;
+}
--- a/mailnews/base/src/nsMsgQuickSearchDBView.h
+++ b/mailnews/base/src/nsMsgQuickSearchDBView.h
@@ -63,25 +63,32 @@ public:
                           nsMsgViewSortTypeValue aSortType, 
                           nsMsgViewSortOrderValue aSortOrder, 
                           nsMsgViewFlagsTypeValue aViewFlags, 
                           PRInt32 *aCount);
   NS_IMETHOD CloneDBView(nsIMessenger *aMessengerInstance,
                          nsIMsgWindow *aMsgWindow,
                          nsIMsgDBViewCommandUpdater *aCommandUpdater,
                          nsIMsgDBView **_retval);
+  NS_IMETHOD CopyDBView(nsMsgDBView *aNewMsgDBView,
+                        nsIMessenger *aMessengerInstance,
+                        nsIMsgWindow *aMsgWindow,
+                        nsIMsgDBViewCommandUpdater *aCmdUpdater);
   NS_IMETHOD DoCommand(nsMsgViewCommandTypeValue aCommand);
   NS_IMETHOD GetViewType(nsMsgViewTypeValue *aViewType);
   NS_IMETHOD SetViewFlags(nsMsgViewFlagsTypeValue aViewFlags);
   NS_IMETHOD SetSearchSession(nsIMsgSearchSession *aSearchSession);
   NS_IMETHOD GetSearchSession(nsIMsgSearchSession* *aSearchSession);
   NS_IMETHOD OnHdrFlagsChanged(nsIMsgDBHdr *aHdrChanged, PRUint32 aOldFlags, 
                          PRUint32 aNewFlags, nsIDBChangeListener *aInstigator);
   NS_IMETHOD OnHdrPropertyChanged(nsIMsgDBHdr *aHdrToChange, PRBool aPreChange, PRUint32 *aStatus, 
                                  nsIDBChangeListener * aInstigator);
+  NS_IMETHOD OnHdrDeleted(nsIMsgDBHdr *aHdrDeleted, nsMsgKey aParentKey,
+                          PRInt32 aFlags, nsIDBChangeListener *aInstigator);
+  NS_IMETHOD GetNumMsgsInView(PRInt32 *aNumMsgs);
 
 protected:
   nsWeakPtr m_searchSession;
   nsTArray<nsMsgKey> m_origKeys;
   PRBool    m_usingCachedHits;
   PRBool    m_cacheEmpty;
   nsCOMArray <nsIMsgDBHdr> m_hdrHits;
   virtual nsresult AddHdr(nsIMsgDBHdr *msgHdr, nsMsgViewIndex *resultIndex = nsnull);
--- a/mailnews/base/src/nsMsgSearchDBView.cpp
+++ b/mailnews/base/src/nsMsgSearchDBView.cpp
@@ -1433,17 +1433,17 @@ nsresult nsMsgSearchDBView::GetThreadCon
 
   // if not threaded, use the real thread. 
   nsCOMPtr<nsIMsgDatabase> msgDB;
   nsresult rv = GetDBForHeader(msgHdr, getter_AddRefs(msgDB));
   NS_ENSURE_SUCCESS(rv, rv);
   return msgDB->GetThreadContainingMsgHdr(msgHdr, pThread);
 }
 
-nsresult 
+nsresult
 nsMsgSearchDBView::ListIdsInThread(nsIMsgThread *threadHdr, 
                                    nsMsgViewIndex startOfThreadViewIndex, 
                                    PRUint32 *pNumListed)
 {
   NS_ENSURE_ARG(threadHdr);
   // these children ids should be in thread order.
   PRUint32 i;
   nsMsgViewIndex viewIndex = startOfThreadViewIndex + 1;
@@ -1481,8 +1481,15 @@ nsMsgSearchDBView::ListIdsInThread(nsIMs
                   level);
       (*pNumListed)++;
       viewIndex++;
     }
   }
   return NS_OK;
 }
 
+NS_IMETHODIMP nsMsgSearchDBView::GetNumMsgsInView(PRInt32 *aNumMsgs)
+{
+  NS_ENSURE_ARG_POINTER(aNumMsgs);
+  *aNumMsgs = m_hdrsTable.Count();
+  return NS_OK;
+}
+
--- a/mailnews/base/src/nsMsgSearchDBView.h
+++ b/mailnews/base/src/nsMsgSearchDBView.h
@@ -80,16 +80,17 @@ public:
                           nsMsgViewSortTypeValue aSortType,
                           nsMsgViewSortOrderValue aSortOrder, 
                           nsMsgViewFlagsTypeValue aViewFlags,
                           PRInt32 *aCount);
   NS_IMETHOD OnHdrDeleted(nsIMsgDBHdr *aHdrDeleted, nsMsgKey aParentKey, 
                           PRInt32 aFlags, nsIDBChangeListener *aInstigator);
   NS_IMETHOD OnHdrFlagsChanged(nsIMsgDBHdr *aHdrChanged, PRUint32 aOldFlags,
                                PRUint32 aNewFlags, nsIDBChangeListener *aInstigator);
+  NS_IMETHODIMP GetNumMsgsInView(PRInt32 *aNumMsgs);
   // override to get location
   NS_IMETHOD GetCellText(PRInt32 aRow, nsITreeColumn* aCol, nsAString& aValue);
   virtual nsresult GetMsgHdrForViewIndex(nsMsgViewIndex index, nsIMsgDBHdr **msgHdr);
   virtual nsresult OnNewHeader(nsIMsgDBHdr *newHdr, nsMsgKey parentKey, PRBool ensureListed);
   NS_IMETHOD GetFolderForViewIndex(nsMsgViewIndex index, nsIMsgFolder **folder);
 
   NS_IMETHOD OnAnnouncerGoingAway(nsIDBChangeAnnouncer *instigator);
 
--- a/mailnews/base/src/nsMsgSpecialViews.cpp
+++ b/mailnews/base/src/nsMsgSpecialViews.cpp
@@ -52,17 +52,17 @@ NS_IMETHODIMP nsMsgThreadsWithUnreadDBVi
 {
     NS_ENSURE_ARG_POINTER(aViewType);
     *aViewType = nsMsgViewType::eShowThreadsWithUnread;
     return NS_OK;
 }
 
 PRBool nsMsgThreadsWithUnreadDBView::WantsThisThread(nsIMsgThread *threadHdr)
 {
-	if (threadHdr)
+  if (threadHdr)
   {
     PRUint32 numNewChildren;
 
     threadHdr->GetNumUnreadChildren(&numNewChildren);
     if (numNewChildren > 0) 
       return PR_TRUE;
   }
   return PR_FALSE;
@@ -104,16 +104,21 @@ nsMsgThreadsWithUnreadDBView::CloneDBVie
 
   nsresult rv = CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
   NS_ENSURE_SUCCESS(rv,rv);
 
   NS_IF_ADDREF(*_retval = newMsgDBView);
   return NS_OK;
 }
 
+NS_IMETHODIMP nsMsgThreadsWithUnreadDBView::GetNumMsgsInView(PRInt32 *aNumMsgs)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
 NS_IMETHODIMP nsMsgWatchedThreadsWithUnreadDBView::GetViewType(nsMsgViewTypeValue *aViewType)
 {
     NS_ENSURE_ARG_POINTER(aViewType);
     *aViewType = nsMsgViewType::eShowWatchedThreadsWithUnread;
     return NS_OK;
 }
 
 PRBool nsMsgWatchedThreadsWithUnreadDBView::WantsThisThread(nsIMsgThread *threadHdr)
@@ -170,8 +175,14 @@ nsMsgWatchedThreadsWithUnreadDBView::Clo
     return NS_ERROR_OUT_OF_MEMORY;
 
   nsresult rv = CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
   NS_ENSURE_SUCCESS(rv,rv);
 
   NS_IF_ADDREF(*_retval = newMsgDBView);
   return NS_OK;
 }
+
+NS_IMETHODIMP
+nsMsgWatchedThreadsWithUnreadDBView::GetNumMsgsInView(PRInt32 *aNumMsgs)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
--- a/mailnews/base/src/nsMsgSpecialViews.h
+++ b/mailnews/base/src/nsMsgSpecialViews.h
@@ -38,35 +38,37 @@
 #ifndef _nsMsgSpecialViews_H_
 #define _nsMsgSpecialViews_H_
 
 #include "nsMsgThreadedDBView.h"
 
 class nsMsgThreadsWithUnreadDBView : public nsMsgThreadedDBView
 {
 public:
-	nsMsgThreadsWithUnreadDBView();
-	virtual ~nsMsgThreadsWithUnreadDBView();
-	virtual const char * GetViewName(void) {return "ThreadsWithUnreadView"; }
-    NS_IMETHOD CloneDBView(nsIMessenger *aMessengerInstance, nsIMsgWindow *aMsgWindow, nsIMsgDBViewCommandUpdater *aCommandUpdater, nsIMsgDBView **_retval);
-    NS_IMETHOD GetViewType(nsMsgViewTypeValue *aViewType);
+  nsMsgThreadsWithUnreadDBView();
+  virtual ~nsMsgThreadsWithUnreadDBView();
+  virtual const char * GetViewName(void) {return "ThreadsWithUnreadView"; }
+  NS_IMETHOD CloneDBView(nsIMessenger *aMessengerInstance, nsIMsgWindow *aMsgWindow, nsIMsgDBViewCommandUpdater *aCommandUpdater, nsIMsgDBView **_retval);
+  NS_IMETHOD GetViewType(nsMsgViewTypeValue *aViewType);
+  NS_IMETHOD GetNumMsgsInView(PRInt32 *aNumMsgs);
 
-	virtual PRBool		WantsThisThread(nsIMsgThread *threadHdr);
+virtual PRBool WantsThisThread(nsIMsgThread *threadHdr);
 protected:
   virtual nsresult AddMsgToThreadNotInView(nsIMsgThread *threadHdr, nsIMsgDBHdr *msgHdr, PRBool ensureListed);
 
 };
 
 class nsMsgWatchedThreadsWithUnreadDBView : public nsMsgThreadedDBView
 {
 public:
-    NS_IMETHOD GetViewType(nsMsgViewTypeValue *aViewType);
-    NS_IMETHOD CloneDBView(nsIMessenger *aMessengerInstance, nsIMsgWindow *aMsgWindow, nsIMsgDBViewCommandUpdater *aCommandUpdater, nsIMsgDBView **_retval);
-	virtual const char * GetViewName(void) {return "WatchedThreadsWithUnreadView"; }
-	virtual PRBool		WantsThisThread(nsIMsgThread *threadHdr);
+  NS_IMETHOD GetViewType(nsMsgViewTypeValue *aViewType);
+  NS_IMETHOD CloneDBView(nsIMessenger *aMessengerInstance, nsIMsgWindow *aMsgWindow, nsIMsgDBViewCommandUpdater *aCommandUpdater, nsIMsgDBView **_retval);
+  NS_IMETHOD GetNumMsgsInView(PRInt32 *aNumMsgs);
+  virtual const char * GetViewName(void) {return "WatchedThreadsWithUnreadView"; }
+  virtual PRBool WantsThisThread(nsIMsgThread *threadHdr);
 protected:
   virtual nsresult AddMsgToThreadNotInView(nsIMsgThread *threadHdr, nsIMsgDBHdr *msgHdr, PRBool ensureListed);
 
 };
 #ifdef DOING_CACHELESS_VIEW
 // This view will initially be used for cacheless IMAP.
 class nsMsgCachelessView : public nsMsgDBView
 {
--- a/mailnews/base/util/errUtils.js
+++ b/mailnews/base/util/errUtils.js
@@ -35,17 +35,18 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 /**
  * This file contains helper methods for debugging -- things like logging
  * exception objects, dumping DOM nodes, Events, and generic object dumps.
  */
 
-const EXPORTED_SYMBOLS = ["logObject", "logException", "logElement", "logEvent"];
+const EXPORTED_SYMBOLS = ["logObject", "logException", "logElement", "logEvent",
+                          "errorWithDebug"];
 
 /**
  * Report on an object to stdout.
  * @param aObj  the object to be dumped
  * @param aName the name of the object, for informational purposes
  */
 function logObject(aObj, aName) {
   dump("Dumping Object: " + aName + "\n");
@@ -76,16 +77,34 @@ function logElement(aElement) {
 /**
  * Log an DOM event to stdout.
  * @param aEvent the DOM event object to dump
  */
 function logEvent(aEvent) {
   stringifier.dumpEvent(aEvent);
 }
 
+/**
+ * Dump the current stack and return an Error suitable for throwing.  We return
+ *  the new Error so that your code can use a "throw" statement which makes it
+ *  obvious to syntactic analysis that there is an exit occuring at that point.
+ *
+ * Example:
+ *   throw errorWithDebug("I did not expect this!");
+ *
+ * @param aString The message payload for the exception.
+ */
+function errorWithDebug(aString) {
+  dump("PROBLEM: " + aString + "\n");
+  dump("CURRENT STACK (and throwing):\n");
+  // skip this frame.
+  dump(stringifier.getStack(1));
+  return new Error(aString);
+}
+
 function Stringifier() {};
 
 Stringifier.prototype = {
   dumpObj: function (o, name) {
     this._reset();
     this._append(this.objectTreeAsString(o, true, true, 0));
     dump(this._asString());
   },
@@ -307,11 +326,11 @@ Stringifier.prototype = {
                 ];
     for (let i in names) {
       if (names[i] in event)
         this._append(names[i] + ": " + event[names[i]] + "\n");
     }
     this._append("-------------------------------------\n");
     return this._asString();
   }
-}
+};
 
 var stringifier = new Stringifier();
--- a/mailnews/db/gloda/components/glautocomp.js
+++ b/mailnews/db/gloda/components/glautocomp.js
@@ -94,45 +94,54 @@ ResultRowMulti.prototype = {
     }
   },
   onItemsModified: function(aItems) {
   },
   onItemsRemoved: function(aItems) {
   },
   onQueryCompleted: function() {
   }
-}
+};
 
 function nsAutoCompleteGlodaResult(aListener, aCompleter, aString) {
   this.listener = aListener;
   this.completer = aCompleter;
   this.searchString = aString;
   this._results = [];
   this._pendingCount = 0;
   this._problem = false;
+  // Track whether we have reported anything to the complete controller so
+  //  that we know not to send notifications to it during calls to addRows
+  //  prior to that point.
+  this._initiallyReported = false;
 
   this.wrappedJSObject = this;
 }
 nsAutoCompleteGlodaResult.prototype = {
   getObjectAt: function(aIndex) {
     return this._results[aIndex];
   },
   markPending: function ACGR_markPending(aCompleter) {
     this._pendingCount++;
   },
   markCompleted: function ACGR_markCompleted(aCompleter) {
     if (--this._pendingCount == 0) {
       this.listener.onSearchResult(this.completer, this);
     }
   },
+  announceYourself: function ACGR_announceYourself() {
+    this._initiallyReported = true;
+    this.listener.onSearchResult(this.completer, this);
+  },
   addRows: function ACGR_addRows(aRows) {
     if (!aRows.length)
       return;
     this._results.push.apply(this._results, aRows);
-    this.listener.onSearchResult(this.completer, this);
+    if (this._initiallyReported)
+      this.listener.onSearchResult(this.completer, this);
   },
   // ==== nsIAutoCompleteResult
   searchString: null,
   get searchResult() {
     if (this._problem)
       return Ci.nsIAutoCompleteResult.RESULT_FAILURE;
     if (this._results.length)
       return (!this._pendingCount) ? Ci.nsIAutoCompleteResult.RESULT_SUCCESS
@@ -374,17 +383,17 @@ ContactTagCompleter.prototype = {
   complete: function ContactTagCompleter_complete(aResult, aString) {
     // now is not the best time to do this; have onFreeTagAdded use a timer.
     if (this._suffixTreeDirty)
       this._buildSuffixTree();
 
     if (aString.length < 2)
       return false; // no async mechanism that will add new rows
 
-    tags = this._suffixTree.findMatches(aString.toLowerCase());
+    let tags = this._suffixTree.findMatches(aString.toLowerCase());
     let rows = [];
     for each (let [iTag, tag] in Iterator(tags)) {
       let query = Gloda.newQuery(Gloda.NOUN_CONTACT);
       query.freeTags(tag);
       let resRow = new ResultRowMulti(Gloda.NOUN_CONTACT, "tag", tag.name,
                                       query);
       rows.push(resRow);
     }
@@ -411,17 +420,17 @@ MessageTagCompleter.prototype = {
     }
     this._suffixTree = new MultiSuffixTree(tagNames, tags);
     this._suffixTreeDirty = false;
   },
   complete: function MessageTagCompleter_complete(aResult, aString) {
     if (aString.length < 2)
       return false;
 
-    tags = this._suffixTree.findMatches(aString.toLowerCase());
+    let tags = this._suffixTree.findMatches(aString.toLowerCase());
     let rows = [];
     for each (let [, tag] in Iterator(tags)) {
       let resRow = new ResultRowSingle(tag, "tag", tag.tag, TagNoun.id);
       rows.push(resRow);
     }
     aResult.addRows(rows);
 
     return false; // no async mechanism that will add new rows
@@ -537,17 +546,17 @@ nsAutoCompleteGloda.prototype = {
             result.markPending(completer);
         }
       //} else {
       //   It'd be nice to do autocomplete in the quicksearch modes based
       //   on the specific values for that mode in the current view.
       //   But we don't do that yet.
       }
 
-      aListener.onSearchResult(this, result);
+      result.announceYourself();
     } catch (e) {
       logException(e);
     }
   },
 
   stopSearch: function() {
   }
 };
--- a/mailnews/news/src/nsNewsDownloader.cpp
+++ b/mailnews/news/src/nsNewsDownloader.cpp
@@ -524,17 +524,18 @@ nsresult DownloadMatchingNewsArticlesToN
   m_newsDB = newsDB;
   m_searchSession = searchSession;
 
   m_keysToDownload.Clear();
   nsresult rv;
   NS_ENSURE_ARG(searchSession);
   NS_ENSURE_ARG(folder);
 
-  searchSession->RegisterListener(this);
+  searchSession->RegisterListener(this,
+                                  nsIMsgSearchSession::allNotifications);
   rv = searchSession->AddScopeTerm(nsMsgSearchScope::localNews, folder);
   return searchSession->Search(m_window);
 }
 
 nsresult nsMsgDownloadAllNewsgroups::ProcessNextGroup()
 {
   nsresult rv = NS_OK;
   PRBool done = PR_FALSE;