Import the gloda-facet patch with davida's quicksearch changes and CSS pulled in. gloda-facet
authorAndrew Sutherland <asutherland@asutherland.org>
Tue, 01 Sep 2009 11:26:31 -0700
branchgloda-facet
changeset 3619 7e2abfed8c928253eaaca01be5313f01cf7050ea
parent 3618 fc30b12098c11b17f2970e7e47d5bf1bfbed9920
child 3620 9e9d511e378042e5cfdc333dd5ffb582b752d951
push id2927
push userbugmail@asutherland.org
push dateThu, 10 Sep 2009 01:15:56 +0000
treeherdercomm-central@0b161ed73eb6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
Import the gloda-facet patch with davida's quicksearch changes and CSS pulled in.
mail/base/content/extraCustomizeItems.xul
mail/base/content/folderDisplay.js
mail/base/content/glodaFacetBindings.css
mail/base/content/glodaFacetBindings.xml
mail/base/content/glodaFacetTab.js
mail/base/content/glodaFacetView.css
mail/base/content/glodaFacetView.js
mail/base/content/glodaFacetView.xhtml
mail/base/content/glodaFacetViewWrapper.xul
mail/base/content/glodaFacetVis.js
mail/base/content/mailWindowOverlay.js
mail/base/content/messenger.css
mail/base/content/messenger.xul
mail/base/content/msgMail3PaneWindow.js
mail/base/content/search.xml
mail/base/content/searchBar.js
mail/base/content/tabmail.xml
mail/base/jar.mn
mail/locales/en-US/chrome/messenger/gloda.properties
mail/locales/en-US/chrome/messenger/glodaFacetView.dtd
mail/locales/en-US/chrome/messenger/glodaFacetView.properties
mail/locales/en-US/chrome/messenger/messenger.dtd
mail/locales/en-US/chrome/messenger/messenger.properties
mail/locales/en-US/chrome/messenger/quickSearch.properties
mail/locales/en-US/chrome/messenger/search.properties
mail/locales/jar.mn
mail/themes/pinstripe/mail/searchBox.css
mailnews/base/src/dbViewWrapper.js
mailnews/base/src/quickSearchManager.js
mailnews/base/util/errUtils.js
mailnews/db/gloda/components/glautocomp.js
mailnews/db/gloda/content/glodacomplete.css
mailnews/db/gloda/content/glodacomplete.xml
mailnews/db/gloda/modules/collection.js
mailnews/db/gloda/modules/datamodel.js
mailnews/db/gloda/modules/datastore.js
mailnews/db/gloda/modules/dbview.js
mailnews/db/gloda/modules/explattr.js
mailnews/db/gloda/modules/facet.js
mailnews/db/gloda/modules/fundattr.js
mailnews/db/gloda/modules/gloda.js
mailnews/db/gloda/modules/index_ab.js
mailnews/db/gloda/modules/indexer.js
mailnews/db/gloda/modules/mimeTypeCategories.js
mailnews/db/gloda/modules/msg_search.js
mailnews/db/gloda/modules/noun_freetag.js
mailnews/db/gloda/modules/noun_mimetype.js
mailnews/db/gloda/modules/noun_tag.js
mailnews/db/gloda/modules/query.js
mailnews/db/gloda/modules/utils.js
mailnews/db/gloda/test/unit/resources/glodaTestHelper.js
mailnews/db/gloda/test/unit/test_query_core.js
--- a/mail/base/content/extraCustomizeItems.xul
+++ b/mail/base/content/extraCustomizeItems.xul
@@ -47,32 +47,43 @@
   <!ENTITY % msgViewPickerDTD SYSTEM "chrome://messenger/locale/msgViewPickerOverlay.dtd" >
   %msgViewPickerDTD;
   <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
   %brandDTD;
 ]>
 
 <overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
+  <popupset>
+    <panel type="glodacomplete-richlistbox" chromedir="ltr"
+           id="PopupGlodaAutocomplete" noautofocus="true" />
+  </popupset>
+
   <toolbarpalette id="MailToolbarPalette">
     <!-- gloda search widget; provides global (message) searching.  -->
     <toolbaritem id="gloda-search" insertafter="search-container"
                  title="&glodaSearch.title;"
                  align="center"
                  class="chromeclass-toolbar-additional">
-      <textbox id="glodaSearchInput" flex="1" type="search"
-               searchbutton="true"
-               emptytext="&glodaSearchBar.emptyText;"/>
+        <textbox id="searchInput" flex="1"
+                 chromedir="ltr"
+                 searchCriteria="true"
+                 type="glodacomplete"
+                 searchbutton="true"
+                 autocompletesearch="gloda"
+                 autocompletepopup="PopupGlodaAutocomplete"
+                 >
+        </textbox>
     </toolbaritem>
 
     <toolbaritem id="search-container" insertafter="button-stop"
                  title="&searchItem.title;"
                  align="center"
                  class="chromeclass-toolbar-additional">
-      <textbox id="searchInput" timeout="800" flex="1"
+      <textbox id="old_searchInput" timeout="800" flex="1"
                onfocus="onSearchInputFocus(event);"
                onclick="onSearchInputClick(event);"
                onmousedown="onSearchInputMousedown(event);"
                onblur="onSearchInputBlur(event);"
                oncommand="onEnterInSearchBar();"
                onkeypress="onSearchKeyPress();"
                chromedir="&locale.dir;">
         <button id="quick-search-button" type="menu" chromedir="&locale.dir;">
--- a/mail/base/content/folderDisplay.js
+++ b/mail/base/content/folderDisplay.js
@@ -778,22 +778,22 @@ FolderDisplayWidget.prototype = {
   /**
    * 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) {
-    // getDocumentElements() sets gSearchBundle
-    getDocumentElements();
-    if (this._tabInfo)
+    if (this._tabInfo) {
+      let searchBundle = document.getElementById("bundle_search");
       document.getElementById("tabmail").setTabThinking(
         this._tabInfo,
-        aIsSearching && gSearchBundle.getString("searchingMessage"));
+        aIsSearching && searchBundle.getString("searchingMessage"));
+    }
   },
 
   /**
    * 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() {
@@ -887,18 +887,16 @@ 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);
     }
 
-    // the quick-search gets nuked when we show a new folder
-    ClearQSIfNecessary();
     // update the quick-search relative to whether it's incoming/outgoing
     onSearchFolderTypeChanged(this.view.isOutgoingFolder);
 
     if (this.active)
       this.makeActive();
   },
 
   /**
@@ -1433,19 +1431,16 @@ FolderDisplayWidget.prototype = {
         let searchInput = document.getElementById("searchInput");
         if (searchInput && this._savedQuickSearch) {
           searchInput.searchMode = this._savedQuickSearch.searchMode;
           if (this._savedQuickSearch.text) {
             searchInput.value = this._savedQuickSearch.text;
             searchInput.showingSearchCriteria = false;
             searchInput.clearButtonHidden = false;
           }
-          else {
-            searchInput.setSearchCriteriaText();
-          }
         }
       }
 
       // Always restore the column state if we have persisted state.  We restore
       //  state on folder entry, in which case we were probably not inactive.
       this._restoreColumnStates();
 
       // the tab mode knows whether we are folder or message display, which
new file mode 100644
--- /dev/null
+++ b/mail/base/content/glodaFacetBindings.css
@@ -0,0 +1,31 @@
+#query-explanation {
+  -moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#query-explanation');
+}
+
+.facets {
+  -moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#facets');
+}
+
+.facetious[type="discrete"] {
+  -moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#facet-discrete');
+}
+
+.facetious[type="boolean"] {
+  -moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#facet-boolean');
+}
+
+.facetious[type="boolean-filtered"] {
+  -moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#facet-boolean-filtered');
+}
+
+.facetious[type="date"] {
+  -moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#facet-date');
+}
+
+.results[type="message"] {
+  -moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#results-message');
+}
+
+.message {
+  -moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#result-message');
+}
new file mode 100644
--- /dev/null
+++ b/mail/base/content/glodaFacetBindings.xml
@@ -0,0 +1,1029 @@
+<?xml version="1.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 Global Database.
+  -
+  - 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>
+  -   David Ascher <dascher@mozillamessaging.com>
+  -   Bryan Clark <clarkbw@gnome.org>
+  -
+  - 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 ***** -->
+
+<bindings id="glodaFacetBindings"
+          xmlns="http://www.mozilla.org/xbl"
+          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+          xmlns:html="http://www.w3.org/1999/xhtml"
+          xmlns:xbl="http://www.mozilla.org/xbl"
+          xmlns:svg="http://www.w3.org/2000/svg">
+
+<!-- ===== Constraints ===== -->
+
+<binding id="query-explanation">
+  <content>
+  </content>
+  <implementation>
+    <!-- Indicate that we are based on a fulltext search-->
+    <method name="setFulltext">
+      <parameter name="aMsgSearcher" />
+      <body><![CDATA[
+        while (this.lastChild)
+          this.removeChild(this.lastChild);
+
+        let dis = this;
+        function spanify(aText, aClass) {
+          let span = document.createElement("span");
+          span.setAttribute("class", aClass);
+          span.textContent = aText;
+          dis.appendChild(span);
+          return span;
+        }
+
+        let labelFormat = glodaFacetStrings.get(
+          "glodaFacetView.constraints.query.fulltext.label");
+        let [labelPre, labelPost] = labelFormat.split("#1");
+        // trim both; we rely on CSS to handle the spacing
+        labelPre = labelPre.trim();
+        labelPost = labelPost.trim();
+
+        if (labelPre)
+          spanify(labelPre, "explanation-fulltext-label");
+
+        let criteriaText = glodaFacetStrings.get(
+          "glodaFacetView.constraints.query.fulltext." +
+          (aMsgSearcher.andTerms ? "and" : "or") + "JoinWord");
+        for each (let [iTerm, term] in Iterator(aMsgSearcher.fulltextTerms)) {
+          if (iTerm)
+            spanify(criteriaText, "explanation-fulltext-criteria");
+          spanify(term, "explanation-fulltext-term");
+        }
+
+        if (labelPost)
+          spanify(labelPost, "explanation-fulltext-label");
+
+        if (aMsgSearcher.fulltextTerms.length > 1) {
+          spanify(
+            glodaFacetStrings.get("glodaFacetView.constraints.query.fulltext." +
+              (aMsgSearcher.andTerms ? "changeToOrLabel" : "changeToAndLabel")),
+            "explanation-change-label")
+          .onclick = function() {
+            FacetContext.toggleFulltextCriteria();
+          };
+        }
+      ]]></body>
+    </method>
+    <method name="setQuery">
+      <parameter name="aMsgQuery" />
+      <body><![CDATA[
+      try {
+        while (this.lastChild)
+          this.removeChild(this.lastChild);
+
+        let dis = this;
+        function spanify(aText, aClass) {
+          let span = document.createElement("span");
+          span.setAttribute("class", aClass);
+          span.textContent = aText;
+          dis.appendChild(span);
+          return span;
+        }
+
+        let label = glodaFacetStrings.get(
+          "glodaFacetView.constraints.query.initial");
+        spanify(label, "explanation-query-label");
+
+        let constraintStrings = [];
+        for each (let constraint in aMsgQuery._constraints) {
+          if (constraint[0] != 1) return; // no idea what this is about
+          if (constraint[1].attributeName == 'involves') {
+            let involvesLabel = glodaFacetStrings.get(
+              "glodaFacetView.constraints.query.involves.label");
+            involvesLabel = involvesLabel.replace("#1", constraint[2].value)
+            spanify(involvesLabel, "explanation-query-involves");
+          } else if (constraint[1].attributeName == 'tag') {
+            let tagLabel = glodaFacetStrings.get(
+              "glodaFacetView.constraints.query.tagged.label");
+            let tag = constraint[2];
+            let _msgTagService = Components.classes["@mozilla.org/messenger/tagservice;1"].
+                                      getService(Components.interfaces.nsIMsgTagService);
+
+            let tagNode = document.createElement("span");
+            let colorClass = "blc-" + _msgTagService.getColorForKey(tag.key).substr(1);
+            tagNode.setAttribute("class", "message-tag tag " + colorClass);
+            tagNode.textContent = tag.tag;
+            spanify(tagLabel, "explanation-query-tagged");
+            this.appendChild(tagNode);
+          }
+        }
+        label = label + constraintStrings.join(', '); // XXX l10n?
+      } catch (e) {
+        logException(e);
+      }
+      ]]></body>
+    </method>
+  </implementation>
+</binding>
+
+<!-- ===== Facets ===== -->
+
+<binding id="facets">
+  <content>
+  </content>
+  <implementation>
+    <method name="clearFacets">
+      <body><![CDATA[
+        while (this.lastChild != null)
+          this.removeChild(this.lastChild);
+      ]]></body>
+    </method>
+    <method name="addFacet">
+      <parameter name="aType" />
+      <parameter name="aAttrDef" />
+      <parameter name="aArgs" />
+      <body><![CDATA[
+        let facets = this;
+
+        let facet = document.createElement("div");
+        facet.attrDef = aAttrDef;
+        facet.nounDef = aAttrDef.objectNounDef;
+        for each (let [key, value] in Iterator(aArgs)) {
+          facet[key] = value;
+        }
+        facet.setAttribute("class", "facetious");
+        facet.setAttribute("type", aType);
+        facet.setAttribute("name", aAttrDef.attributeName);
+        facets.appendChild(facet);
+
+        return facet;
+      ]]></body>
+    </method>
+  </implementation>
+</binding>
+
+<binding id="facet-base">
+  <implementation>
+    <method name="brushItems">
+      <body><![CDATA[
+      ]]></body>
+    </method>
+    <method name="clearBrushedItems">
+      <body><![CDATA[
+      ]]></body>
+    </method>
+  </implementation>
+</binding>
+
+<!--
+  - A boolean facet; we presume orderedGroups contains at most two entries, and
+  -  that their group values consist of true or false.
+  -
+  - The implication of this UI is that you only want to filter to true values
+  -  and would never want to filter out the false values.  Consistent with this
+  -  assumption we disable the UI if there are no true values.
+  -
+  - This depressingly
+  -->
+<binding id="facet-boolean"
+         extends="chrome://messenger/content/glodaFacetBindings.xml#facet-base">
+  <content>
+    <html:span anonid="bubble" class="facet-checkbox-bubble">
+      <html:input anonid="checkbox" type="checkbox" />
+      <html:span anonid="label" class="facet-checkbox-label" />
+      <html:span anonid="count" class="facet-checkbox-count" />
+    </html:span>
+  </content>
+  <implementation>
+    <constructor><![CDATA[
+      this.bubble = document.getAnonymousElementByAttribute(this, "anonid",
+                                                            "bubble");
+      this.checkbox = document.getAnonymousElementByAttribute(this, "anonid",
+                                                              "checkbox");
+      this.labelNode = document.getAnonymousElementByAttribute(this, "anonid",
+                                                               "label");
+      this.countNode = document.getAnonymousElementByAttribute(this, "anonid",
+                                                               "count");
+
+      let dis = this;
+      this.bubble.addEventListener("click", function (event) {
+        return dis.bubbleClicked(event);
+      }, true);
+
+      this.extraSetup();
+
+      if ("faceter" in this)
+        this.build(true);
+    ]]></constructor>
+    <field name="canUpdate" readonly="true">true</field>
+    <property name="disabled">
+      <getter><![CDATA[
+        return this.getAttribute("disabled") == "true";
+      ]]></getter>
+      <setter><![CDATA[
+        if (val) {
+          this.setAttribute("disabled", "true");
+          this.checkbox.setAttribute("disabled", true);
+        }
+        else {
+          this.removeAttribute("disabled");
+          this.checkbox.removeAttribute("disabled");
+        }
+      ]]></setter>
+    </property>
+    <property name="checked">
+      <getter><![CDATA[
+        return this.getAttribute("checked") == "true";
+      ]]></getter>
+      <setter><![CDATA[
+        if (this.checked == val)
+          return;
+        this.checkbox.checked = val;
+        if (val) {
+          // the XBL inherits magic appears to fail if we explicitly check the
+          //  box itself rather than via our click handler, presumably because
+          //  we unshadow something.  So manually apply changes ourselves.
+          this.setAttribute("checked", "true");
+          this.checkbox.setAttribute("checked", "true");
+          if (!this.disabled)
+            FacetContext.addFacetConstraint(this.faceter, this.attrDef, true,
+                                            this.trueGroups);
+        }
+        else {
+          this.removeAttribute("checked");
+          this.checkbox.removeAttribute("checked");
+          if (!this.disabled)
+            FacetContext.removeFacetConstraint(this.faceter);
+        }
+        this.checkStateChanged();
+      ]]></setter>
+    </property>
+    <method name="extraSetup">
+      <body><![CDATA[
+      ]]></body>
+    </method>
+    <method name="checkStateChanged">
+      <body><![CDATA[
+      ]]></body>
+    </method>
+    <method name="build">
+      <parameter name="aFirstTime" />
+      <body><![CDATA[
+        if (aFirstTime) {
+          this.labelNode.textContent = this.attrDef.strings.facetLabel;
+          this.checkbox.setAttribute("aria-label",
+                                     this.attrDef.strings.facetLabel);
+        }
+
+        // If we do not currently have a constraint applied and there is only
+        //  one (or no) group, then: disable us, but reflect the underlying
+        //  state of the data (checked or non-checked)
+        if (!this.faceter.constraint && (this.orderedGroups.length <= 1)){
+          this.disabled = true;
+          let count = 0;
+          if (this.orderedGroups.length) {
+            // true case?
+            if (this.orderedGroups[0][0]) {
+              count = this.orderedGroups[0][1].length;
+              this.checked = true;
+            }
+            else {
+              this.checked = false;
+            }
+          }
+          this.countNode.textContent = count.toLocaleString();
+          return;
+        }
+        // if we were disabled checked before, clear ourselves out
+        if (this.disabled && this.checked)
+          this.checked = false;
+        this.disabled = false;
+
+        // if we are here, we have our 2 groups, find true...
+        // (note: it is possible to get jerked around by null values
+        //  currently, so leave a reasonable failure case)
+        this.trueValues = [];
+        this.trueGroups = [true];
+        for each (let [, groupPair] in Iterator(this.orderedGroups)) {
+          if (groupPair[0] == true)
+            this.trueValues = groupPair[1];
+        }
+
+        this.countNode.textContent = this.trueValues.length.toLocaleString();
+      ]]></body>
+    </method>
+    <method name="bubbleClicked">
+      <parameter name="event" />
+      <body><![CDATA[
+      if (!this.disabled)
+        this.checked = !this.checked;
+      event.stopPropagation();
+      ]]></body>
+    </method>
+  </implementation>
+</binding>
+
+<!--
+  - A check-box with filter-box front-end to a standard discrete faceter.  If
+  -  there are no non-null values, we disable the UI.  If there are non-null
+  -  values the checkbox is enabled and the filter is hidden.  Once you check
+  -  the box we apply the facet and show the filtering mechanism.
+  -->
+<binding id="facet-boolean-filtered"
+         extends="chrome://messenger/content/glodaFacetBindings.xml#facet-boolean">
+  <content>
+    <html:span anonid="bubble" class="facet-checkbox-bubble">
+      <html:input anonid="checkbox" type="checkbox"
+                  xbl:inherits="checked,disabled"/>
+      <html:span anonid="label" class="facet-checkbox-label" />
+      <html:span anonid="count" class="facet-checkbox-count" />
+    </html:span>
+    <html:select anonid="filter" class="facet-filter-list" />
+  </content>
+  <implementation>
+    <method name="extraSetup">
+      <body><![CDATA[
+        this.filterNode = document.getAnonymousElementByAttribute(
+                            this, "anonid", "filter");
+        this.groupDisplayProperty = this.getAttribute("groupDisplayProperty");
+
+        let dis = this;
+        this.filterNode.addEventListener("change", function(event) {
+          return dis.filterChanged(event);
+        }, false);
+
+        this.selectedValue = "all";
+      ]]></body>
+    </method>
+    <method name="build">
+      <parameter name="aFirstTime" />
+      <body><![CDATA[
+        if (aFirstTime) {
+          this.labelNode.textContent = this.attrDef.strings.facetLabel;
+          this.checkbox.setAttribute("aria-label",
+                                     this.attrDef.strings.facetLabel);
+        }
+
+        // Only update count if anything other than "all" is selected.
+        //  Otherwise we lose the set of attachment types in our select box,
+        //  and that makes us sad.  We do want to update on "all" though
+        //  because other facets may further reduce the number of attachments
+        //  we see.  (Or if this is not just being used for attachments, it
+        //  still holds.)
+        if (this.selectedValue != "all") {
+          let count = 0;
+          for each (let [, groupPair] in Iterator(this.orderedGroups)) {
+            if (groupPair[0] != null)
+              count += groupPair[1].length;
+          }
+          this.countNode.textContent = count.toLocaleString();
+          return;
+        }
+
+        while (this.filterNode.lastChild)
+          this.filterNode.removeChild(this.filterNode.lastChild);
+        let allNode = document.createElement("option");
+        allNode.textContent =
+          glodaFacetStrings.get("glodaFacetView.facets.filter." +
+                                this.attrDef.attributeName + ".allLabel");
+        allNode.setAttribute("value", "all");
+        if (this.selectedValue == "all")
+          allNode.setAttribute("selected", "selected");
+        this.filterNode.appendChild(allNode);
+
+        // if we are here, we have our 2 groups, find true...
+        // (note: it is possible to get jerked around by null values
+        //  currently, so leave a reasonable failure case)
+        // empty true groups is for the checkbox
+        this.trueGroups = [];
+        // the real true groups is the actual true values for our explicit
+        //  filtering
+        this.realTrueGroups = [];
+        this.trueValues = [];
+        this.falseValues = [];
+        let selectNodes = [];
+        for each (let [, groupPair] in Iterator(this.orderedGroups)) {
+          if (groupPair[0] == null)
+            this.falseValues.push.apply(this.falseValues, groupPair[1]);
+          else {
+            this.trueValues.push.apply(this.trueValues, groupPair[1]);
+
+            let groupValue = groupPair[0];
+            let selNode = document.createElement("option");
+            selNode.textContent = groupValue[this.groupDisplayProperty];
+            selNode.setAttribute("value", this.realTrueGroups.length);
+            if (this.selectedValue == groupValue.category)
+              selNode.setAttribute("selected", "selected");
+            selectNodes.push(selNode);
+
+            this.realTrueGroups.push(groupValue);
+          }
+        }
+        selectNodes.sort(function(a, b) {
+          return a.textContent.localeCompare(b.textContent);
+        })
+        for each (let [, selNode] in Iterator(selectNodes)) {
+          this.filterNode.appendChild(selNode);
+        }
+
+        this.disabled = !this.trueValues.length;
+
+        this.countNode.textContent = this.trueValues.length.toLocaleString();
+      ]]></body>
+    </method>
+    <method name="checkStateChanged">
+      <body><![CDATA[
+        // if they un-check us, revert our value to all.
+        if (!this.checked)
+          this.selectedValue = "all";
+      ]]></body>
+    </method>
+    <method name="filterChanged">
+      <parameter name="event" />
+      <body><![CDATA[
+        if (!this.checked)
+          return;
+        if (this.filterNode.value == "all") {
+          this.selectedValue = "all";
+          FacetContext.addFacetConstraint(this.faceter, this.attrDef, true,
+                                          this.trueGroups, false, true);
+        }
+        else {
+          let groupValue = this.realTrueGroups[parseInt(this.filterNode.value)];
+          this.selectedValue = groupValue.category;
+          FacetContext.addFacetConstraint(this.faceter, this.attrDef, true,
+                                          [groupValue], false, true);
+        }
+      ]]></body>
+    </method>
+  </implementation>
+</binding>
+
+<binding id="facet-discrete">
+  <content>
+  <!-- without this explicit div here, the sibling selectors used to span
+       included-label/included and excluded-label/excluded fail to apply.
+       so. magic! (this is why our binding node's class is facetious. -->
+  <html:div class="facet">
+    <html:h2 anonid="name"></html:h2>
+    <html:ul anonid="included" class="facet-included barry"></html:ul>
+    <html:ul anonid="excluded" class="facet-excluded barry"></html:ul>
+    <html:ul anonid="candidates" class="barry"></html:ul>
+  </html:div>
+  </content>
+  <implementation>
+    <constructor><![CDATA[
+      if ("faceter" in this)
+        this.build(true);
+    ]]></constructor>
+    <field name="canUpdate" readonly="true">false</field>
+    <method name="lockSize">
+      <body><![CDATA[
+        return;
+        if (this.style.paddingBottom != "0px") {
+          let $this = $(this);
+          let height = ($this.height() + 60) + "px";
+          let width = $this.width() + "px";
+          this.style.minWidth = width;
+          this.style.minHeight = height;
+          this.style.maxWidth = width;
+          this.style.maxHeight = height;
+          this.style.paddingBottom = "0px";
+        }
+      ]]></body>
+    </method>
+    <method name="build">
+      <parameter name="aFirstTime" />
+      <body><![CDATA[
+        // -- Header Building
+        let nameNode = document.getAnonymousElementByAttribute(this, "anonid",
+                                                               "name");
+        nameNode.textContent = this.attrDef.strings.facetLabel;
+        nameNode.setAttribute("title", this.attrDef.strings.facetTooltip);
+
+
+        this.inclList = document.getAnonymousElementByAttribute(this, "anonid",
+                                                                "included");
+        this.exclList = document.getAnonymousElementByAttribute(this, "anonid",
+                                                                "excluded");
+
+        this.inclList.setAttribute("state", "empty");
+        this.exclList.setAttribute("state", "empty");
+
+        // -- House-cleaning
+        // -- All/Top mode decision
+        // to simplify our logic, we always need an other group sentinel even
+        //  if we are operating in "all" mode.
+        this.otherGroupObject = {};
+        this.modes = ["all"];
+        if (this.maxDisplayRows >= this.orderedGroups.length) {
+          this.mode = "all";
+        }
+        else {
+          // top mode must be used
+          this.modes.push("top");
+          this.mode = "top";
+          this.topGroups = FacetUtils.makeTopGroups(this.attrDef,
+                                                    this.orderedGroups,
+                                                    this.maxDisplayRows,
+                                                    this.otherGroupObject);
+        }
+
+        // -- Row Building
+        this.buildRows();
+
+      ]]></body>
+    </method>
+    <method name="buildRows">
+      <body><![CDATA[
+        let nounDef = this.nounDef;
+        let useGroups = (this.mode == "all") ? this.orderedGroups
+                                             : this.topGroups;
+
+        // -- empty all of our display buckets...
+        let root = document.getAnonymousElementByAttribute(this, "anonid",
+                                                           "candidates");
+        while (root.lastChild)
+          root.removeChild(root.lastChild);
+
+        let inclList = this.inclList, exclList = this.exclList;
+        while (inclList.lastChild)
+          inclList.removeChild(inclList.lastChild);
+        while (exclList.lastChild)
+          exclList.removeChild(exclList.lastChild);
+
+        // for our bar-graph thing we need to find the largest group size
+        let maxGroupSize = 0;
+        for each (let [, groupPair] in Iterator(useGroups)) {
+          maxGroupSize = Math.max(maxGroupSize, groupPair[1].length);
+        }
+
+        for each (let [, groupPair] in Iterator(useGroups)) {
+          let [groupValue, groupItems] = groupPair;
+          let li = document.createElement("li");
+          li.setAttribute("class", "bar");
+          li.groupValue = groupValue;
+          li.groupItems = groupItems;
+
+          let label = document.createElement("a");
+          label.setAttribute("class", "bar-link");
+          // The null value is a special indicator for 'none'
+          if (groupValue == null) {
+            label.textContent =
+              glodaFacetStrings.get("glodaFacetView.facets.noneLabel");
+          }
+          // Top mode uses a sentinel value for the 'other' case
+          else if (groupValue == this.otherGroupObject) {
+            label.textContent = glodaFacetStrings.get(
+                                  "glodaFacetView.facets.mode.top.otherLabel",
+                                  [this.otherGroupObject.count.toLocaleString(),
+                                   ]);
+          }
+          // Otherwise stringify the group object
+          else {
+            // XXX do a better job of truncation
+            label.textContent = ("userVisibleString" in nounDef) ?
+                                  nounDef.userVisibleString(groupValue) :
+                                  groupValue.toLocaleString().substring(0, 80);
+          }
+
+          li.appendChild(label);
+
+          let barSpan = document.createElement("span");
+          let percent = Math.floor(100 * groupItems.length / maxGroupSize);
+          barSpan.setAttribute("class", "bar-percent");
+          barSpan.setAttribute("style", "width: " + percent +  "%");
+          barSpan.textContent = percent + "%";
+          li.appendChild(barSpan);
+
+          let excludeSpan = document.createElement("span");
+          excludeSpan.setAttribute("class", "bar-exclude");
+          li.appendChild(excludeSpan);
+
+          root.appendChild(li);
+        }
+      ]]></body>
+    </method>
+    <method name="brushItems">
+      <parameter name="aItems" />
+      <body><![CDATA[
+      ]]></body>
+    </method>
+    <method name="clearBrushedItems">
+      <body><![CDATA[
+      ]]></body>
+    </method>
+    <method name="afterListVisible">
+      <parameter name="aInclude" />
+      <parameter name="aCallback" />
+      <body><![CDATA[
+        let listNode = aInclude ? this.inclList : this.exclList;
+
+        // if there are already things displayed, no need
+        if (listNode.childElementCount) {
+          aCallback();
+          return;
+        }
+
+        listNode.setAttribute("state", "some");
+        $(listNode)
+          .hide()
+          .slideDown("fast", aCallback);
+      ]]></body>
+    </method>
+    <method name="barClicked">
+      <parameter name="aBarNode" />
+      <parameter name="aInclude" />
+      <body><![CDATA[
+        let groupValue = aBarNode.groupValue;
+        let groupItems = aBarNode.groupItems;
+
+        // The 'other' object has special interaction semantics given that it is
+        //  in fact an aggregation of a bunch of other groups.
+        // Clicking on it inclusively results in excluding all of the displayed
+        //  top groups.  This allows us to keep displaying the facet while also
+        //  having stable behavior if the user starts excluding single groups.
+        //  (Our rule is that we only have one active constraint per attribute
+        //  at a time, so if we treated the other inclusion as an inclusion, the
+        //  subsequent exclude would knock the inclusion off the chart.
+        // Clicking on it exclusively results in excluding all of the groups in
+        //  the 'other' group.  We do this for the same rationale as in the
+        //  inclusion case; we need subsequent excludes to not do weird things
+        //  (or complicate the implementation).
+        // (If we start allowing multiple inclusion, we will keep these
+        //  semantics; we will simply remove things from the inclusion set when
+        //  excluded.  Easy peasy.)
+        if (groupValue == this.otherGroupObject) {
+          if (aInclude) {
+            // build up the list of non-other group values.  since other is
+            //  always last, this is not so hard.
+            let groupValues = [];
+            for (let iGroup = 0; iGroup < this.topGroups.length - 1; iGroup++) {
+              groupValues.push(this.topGroups[iGroup][0]);
+            }
+
+            FacetContext.addFacetConstraint(this.faceter, this.attrDef, false,
+                                            groupValues);
+            return;
+          }
+          // exclusive case, exclude all the values in the group... we already
+          //  know who they are, though from the sentinel object.
+          FacetContext.addFacetConstraint(this.faceter, this.attrDef, false,
+                                          this.otherGroupObject.groupValues);
+          return;
+        }
+
+        // When clicking on something we want to animate it flying up into the
+        //  appropriate list (included/excluded).  This depends on the list
+        //  being visible, which may also need animation.
+        let dis = this;
+        this.afterListVisible(aInclude, function() {
+          // figure out our origin location prior to adding the target or it
+          //  will shift us down.
+          let $barNode = $(aBarNode);
+          let origin = $barNode.offset();
+
+          // clone the node into its target location
+          let targetNode = aBarNode.cloneNode(true);
+          targetNode.groupValue = groupValue;
+          targetNode.groupItems = groupItems;
+          targetNode.setAttribute(aInclude ? "included" : "excluded", "true");
+          targetNode.removeAttribute(aInclude ? "excluded" : "included")
+
+          let targetParent = aInclude ? dis.inclList : dis.exclList;
+          targetParent.appendChild(targetNode);
+
+          // create a flying clone
+          let flyingNode = aBarNode.cloneNode(true);
+
+          // animate the flying clone... flying!
+          let $targetNode = $(targetNode);
+          let dest = $targetNode.offset();
+
+          let $flyingNode = $(flyingNode)
+            .appendTo("body")
+            .css("position", "absolute")
+            .css("width", $barNode.width())
+            .css("height", $barNode.height())
+            .css("top", origin.top)
+            .css("left", origin.left)
+            .css("zIndex", 1000)
+            .animate({
+                top: dest.top,
+                left: dest.left
+              },
+              // have a velocity of 1 pixel every 4ms
+              Math.abs(dest.top - origin.top) * 6, function() {
+                $barNode.remove();
+                $targetNode.show();
+                $flyingNode.remove();
+
+                setTimeout(function() {
+                    FacetContext.addFacetConstraint(dis.faceter, dis.attrDef,
+                                                    aInclude, [groupValue]);
+                  }, 50);
+              });
+
+          // hide the target (cloned) node
+          $(targetNode).hide();
+
+          // hide the original node and remove its JS properties
+          $(aBarNode).css("visibility", "hidden");
+          delete aBarNode.groupValue;
+          delete aBarNode.groupItems;
+        });
+
+      ]]></body>
+    </method>
+    <method name="barHovered">
+      <parameter name="aBarNode" />
+      <parameter name="aInclude" />
+      <body><![CDATA[
+        let groupValue = aBarNode.groupValue;
+        let groupItems = aBarNode.groupItems;
+
+        FacetContext.hoverFacet(this.faceter, this.attrDef, groupValue, groupItems);
+      ]]></body>
+    </method>
+    <!-- HoverGone! HoverGone!
+         We know it's gone, but where has it gone? -->
+    <method name="barHoverGone">
+      <parameter name="aBarNode" />
+      <parameter name="aInclude" />
+      <body><![CDATA[
+        let groupValue = aBarNode.groupValue;
+        let groupItems = aBarNode.groupItems;
+
+        FacetContext.unhoverFacet(this.faceter, this.attrDef, groupValue, groupItems);
+      ]]></body>
+    </method>
+  </implementation>
+  <handlers>
+    <handler event="click"><![CDATA[
+      // we dispatch based on the class of the thing we clicked on.
+      // there are other ways we could accomplish this, but they all sorta suck.
+      let nodeClasses = event.originalTarget.getAttribute("class");
+      if (!nodeClasses)
+        return;
+
+      let nodeClass = nodeClasses.split(" ")[0];
+      if (nodeClass == "bar-exclude")
+        this.barClicked(event.originalTarget.parentNode, false);
+      else if (nodeClass == "bar-link")
+        this.barClicked(event.originalTarget.parentNode, true);
+    ]]></handler>
+    <handler event="mouseover"><![CDATA[
+      // we dispatch based on the class of the thing we clicked on.
+      // there are other ways we could accomplish this, but they all sorta suck.
+      let nodeClasses = event.originalTarget.getAttribute("class");
+      if (!nodeClasses)
+        return;
+
+      let nodeClass = nodeClasses.split(" ")[0];
+      if (nodeClass == "bar-link")
+        this.barHovered(event.originalTarget.parentNode, true);
+    ]]></handler>
+    <handler event="mouseout"><![CDATA[
+      // we dispatch based on the class of the thing we clicked on.
+      // there are other ways we could accomplish this, but they all sorta suck.
+      let nodeClasses = event.originalTarget.getAttribute("class");
+      if (!nodeClasses)
+        return;
+
+      let nodeClass = nodeClasses.split(" ")[0];
+      if (nodeClass == "bar-link")
+        this.barHoverGone(event.originalTarget.parentNode, true);
+    ]]></handler>
+  </handlers>
+</binding>
+
+<binding id="facet-date">
+  <content>
+    <html:div class="facet">
+      <html:h2 anonid="name"></html:h2>
+      <!-- we need to do this because of something protovis is doing where
+           it attempts to re-interpret the text and the html namespace no
+           longer exists in that context. -->
+      <html:div anonid="canvas" class="date-vis-frame"></html:div>
+    </html:div>
+  </content>
+  <implementation>
+    <constructor><![CDATA[
+      this.canvasNode = document.getAnonymousElementByAttribute(
+                                   this, "anonid", "canvas");
+      this.vis = null;
+      if ("faceter" in this)
+        this.build(true);
+    ]]></constructor>
+    <field name="canUpdate" readonly="true">true</field>
+    <method name="build">
+      <parameter name="aFirstTime" />
+      <body><![CDATA[
+        if (!this.vis) {
+          this.vis = new DateFacetVis(this, this.canvasNode);
+          this.vis.build();
+        }
+        else {
+          while (this.canvasNode.lastChild)
+            this.canvasNode.removeChild(this.canvasNode.lastChild);
+          this.vis.rebuild();
+        }
+      ]]></body>
+    </method>
+    <method name="brushItems">
+      <parameter name="aItems" />
+      <body><![CDATA[
+        this.vis.hoverItems(aItems);
+      ]]></body>
+    </method>
+    <method name="clearBrushedItems">
+      <body><![CDATA[
+        this.vis.clearHover();
+      ]]></body>
+    </method>
+  </implementation>
+</binding>
+
+<!-- ===== Results ===== -->
+
+<binding id="results-message">
+  <content>
+    <html:div class="results-message-header">
+      <html:h2 class="results-message-count" anonid="count"></html:h2>
+      <html:span class="results-message-showall" anonid="showall"
+            onclick="FacetContext.showActiveSetInTab()"></html:span>
+    </html:div>
+    <html:div class="messages" anonid="messages">
+    </html:div>
+  </content>
+  <implementation>
+    <method name="setMessages">
+      <parameter name="aMessages" />
+      <body><![CDATA[
+        // -- Count
+        let countNode = document.getAnonymousElementByAttribute(
+                          this, "anonid", "count");
+        let messagesPlural = glodaFacetStrings.get(
+          "glodaFacetView.results.message.countLabelMessagePlurals");
+        let countLabel = glodaFacetStrings.get(
+          "glodaFacetView.results.message.countLabel");
+        // display set
+        countLabel = countLabel.replace("#1",
+          aMessages.length.toLocaleString());
+        countLabel = countLabel.replace("#2",
+          PluralForm.get(aMessages.length, messagesPlural));
+        // active set
+        countLabel = countLabel.replace("#3",
+          FacetContext.activeSet.length.toLocaleString());
+        countLabel = countLabel.replace("#4",
+          PluralForm.get(FacetContext.activeSet.length, messagesPlural));
+        countNode.textContent = countLabel;
+
+        // -- Show All
+        let showNode = document.getAnonymousElementByAttribute(
+                          this, "anonid", "showall");
+        showNode.textContent = glodaFacetStrings.get(
+          "glodaFacetView.results.message.showAllInList.label");
+        showNode.setAttribute("title",
+          glodaFacetStrings.get(
+            "glodaFacetView.results.message.showAllInList.tooltip"));
+
+        let messagesNode = document.getAnonymousElementByAttribute(
+                             this, "anonid", "messages");
+        while (messagesNode.lastChild)
+          messagesNode.removeChild(messagesNode.lastChild);
+
+        // -- Messages
+        for each (let [, message] in Iterator(aMessages)) {
+          let msgNode = document.createElement("message");
+          msgNode.message = message;
+          msgNode.setAttribute("class", "message");
+          messagesNode.appendChild(msgNode);
+        }
+      ]]></body>
+    </method>
+  </implementation>
+</binding>
+
+<binding id="result-message">
+  <content>
+    <html:div class="message-header">
+      <html:div class="message-meta">
+        <html:div anonid="oldestMessageDate" class="message-oldestMessageDate"></html:div>
+        <html:div anonid="attachments" class="message-attachments"></html:div>
+      </html:div>
+      <html:div anonid="subject" class="message-subject"></html:div>
+      <html:div class="message-addressing">
+        <html:span anonid="author" class="message-author"></html:span>
+        <html:span anonid="writes" class="message-writes"></html:span>
+        <html:div anonid="recipients" class="message-recipients"/>
+        <html:span anonid="date" class="message-date"></html:span>
+      </html:div>
+      <html:div anonid="tags" class="message-tags"></html:div>
+    </html:div>
+    <html:pre anonid="snippet" class="message-body"></html:pre>
+  </content>
+  <implementation>
+    <constructor><![CDATA[
+      this.build();
+    ]]></constructor>
+    <method name="build">
+      <body><![CDATA[
+        let message = this.message;
+        let dis = this;
+        function anonElem(aAnonId) {
+          return document.getAnonymousElementByAttribute(dis, "anonid",
+                                                         aAnonId);
+        }
+
+        // -- eventify
+        this.onclick = function(aEvent) {
+          FacetContext.showConversationInTab(message,
+                                             aEvent.button == 1);
+        }
+
+        // -- l10n poking
+        anonElem("writes").textContent =
+          glodaFacetStrings.get("glodaFacetView.result.message.writesLabel");
+
+        // -- Content Poking
+        anonElem("subject").textContent = message.subject;
+        anonElem("author").textContent = message.from.toLocaleString();
+        anonElem("date").textContent = message.date.toLocaleString();
+
+        // - Recipients
+        let recipientsNode = anonElem("recipients");
+        if (message.recipients) {
+          for each (let [, recip] in Iterator(message.recipients)) {
+            let recipNode = document.createElement("span");
+            recipNode.setAttribute("class", "message-recipient");
+            recipNode.textContent = recip.toLocaleString();
+            recipientsNode.appendChild(recipNode);
+          }
+        }
+        // - Tags
+        let tagsNode = anonElem("tags");
+        if ("tags" in message && message.tags.length) {
+          let _msgTagService = Components.classes["@mozilla.org/messenger/tagservice;1"].
+                                    getService(Components.interfaces.nsIMsgTagService);
+          for each (let [, tag] in Iterator(message.tags)) {
+            let tagNode = document.createElement("span");
+            let colorClass = "blc-" + _msgTagService.getColorForKey(tag.key).substr(1);
+            tagNode.setAttribute("class", "message-tag tag " + colorClass);
+            tagNode.textContent = tag.tag;
+            tagsNode.appendChild(tagNode);
+          }
+        }
+
+        // - Body
+        if (message.indexedBodyText) {
+          let bodyText = message.indexedBodyText;
+          // start assuming it's just one line that we want to show
+          let idxNewline = -1;
+          for (let newlineCount = 0; newlineCount < 5; newlineCount++) {
+            let idxNextNewline = bodyText.indexOf("\n", idxNewline+1);
+            if (idxNextNewline == -1)
+              break;
+            idxNewline = idxNextNewline;
+          }
+          if (idxNewline > -1)
+            anonElem("snippet").textContent = bodyText.substring(0, idxNewline);
+          else
+            anonElem("snippet").textContent = bodyText;
+        }
+
+        // - Misc attributes
+        if (!message.read)
+          this.setAttribute("unread", "true");
+      ]]></body>
+    </method>
+  </implementation>
+</binding>
+
+</bindings>
new file mode 100644
--- /dev/null
+++ b/mail/base/content/glodaFacetTab.js
@@ -0,0 +1,106 @@
+Components.utils.import("resource://app/modules/StringBundle.js");
+
+Components.utils.import("resource://app/modules/gloda/facet.js");
+// needed by search.xml to use us
+Components.utils.import("resource://app/modules/gloda/msg_search.js");
+
+var glodaFacetTabType = {
+  name: "glodaFacet",
+  perTabPanel: "iframe",
+  strings:
+    new StringBundle("chrome://messenger/locale/glodaFacetView.properties"),
+  modes: {
+    glodaFacet: {
+      // this is what get exposed on the tab for icon purposes
+      type: "glodaSearch"
+    }
+  },
+  openTab: function glodaFacetTabType_openTab(aTab, aArgs) {
+    // we have no browser until our XUL document loads
+    aTab.browser = null;
+
+    if ("query" in aArgs) {
+      aTab.query = aArgs.query;
+      aTab.collection = aTab.query.getCollection();
+
+      aTab.title = this.strings.get("glodaFacetView.tab.query.label");
+      aTab.searchString = null;
+    }
+    else if ("searcher" in aArgs) {
+      aTab.searcher = aArgs.searcher;
+      aTab.collection = aTab.searcher.getCollection();
+      aTab.query = aTab.searcher.query;
+
+      let searchString = aTab.searcher.searchString;
+      aTab.title = aTab.glodaSearchInputValue = aTab.searchString =
+        searchString;
+    }
+    else if ("collection" in aArgs) {
+      aTab.collection = aArgs.collection;
+
+      aTab.title = this.strings.get("glodaFacetView.tab.query.label");
+      aTab.searchString = null;
+    }
+
+    function xulLoadHandler() {
+      aTab.panel.contentWindow.removeEventListener("load", xulLoadHandler,
+                                                   false);
+      aTab.panel.contentWindow.tab = aTab;
+      aTab.browser = aTab.panel.contentDocument.getElementById("browser");
+      aTab.browser.setAttribute("src",
+        "chrome://messenger/content/glodaFacetView.xhtml");
+    }
+
+    aTab.panel.contentWindow.addEventListener("load", xulLoadHandler, false);
+    aTab.panel.setAttribute("src",
+      "chrome://messenger/content/glodaFacetViewWrapper.xul");
+  },
+  closeTab: function glodaFacetTabType_closeTab(aTab) {
+  },
+  saveTabState: function glodaFacetTabType_saveTabState(aTab) {
+    // nothing to do; we are not multiplexed
+  },
+  showTab: function glodaFacetTabType_showTab(aTab) {
+    // nothing to do; we are not multiplexed
+  },
+  getBrowser: function(aTab) {
+    return aTab.browser;
+  }
+};
+
+/**
+ * The glodaSearch tab mode has a UI widget outside of the mailTabType's
+ *  display panel, the #glodaSearchInput textbox.  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 glodaSearch) tab.
+ *  The behaviour rationale for this is that the glodaSearchInput 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.  It's not entirely clear that imitating this behaviour on
+ *  non-glodaSearch tabs makes a lot of sense, but it is consistent, so we do
+ *  so.  The counter-example to this choice is the search box in firefox, but
+ *  it never updates when you switch tabs, so it is arguably less of a fit.
+ */
+var glodaFacetTabMonitor = {
+  onTabTitleChanged: function() {},
+  onTabSwitched: function glodaFacetTabMonitor_onTabSwitch(aTab, aOldTab) {
+    let inputNode = document.getElementById("glodaSearchInput");
+    if (!inputNode)
+      return;
+
+    // save the current search field value
+    if (aOldTab)
+      aOldTab.glodaSearchInputValue = inputNode.value;
+    // load (or clear if there is none) the persisted search field value
+    inputNode.value = aTab.glodaSearchInputValue || "";
+
+    // If the mode is glodaSearch and the search is unchanged, then we want to
+    //  set the icon state of the input box to be the 'clear' icon.
+    if (aTab.mode.name == "glodaFacet") {
+      if (aTab.searchString == aTab.glodaSearchInputValue)
+        inputNode._searchIcons.selectedIndex = 1;
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mail/base/content/glodaFacetView.css
@@ -0,0 +1,491 @@
+/* ***** 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 Global Database.
+ *
+ * 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>
+ *   David Ascher <dascher@mozillamessaging.com>
+ *   Bryan Clark <clarkbw@gnome.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 ***** */
+
+body {
+  background: #ffffff;
+  font-family: sans-serif;
+  padding: 0;
+  margin: 0;
+  height: 100%;
+  width: 100%;
+  font-family: sans-serif;
+}
+
+/* ===== Query Explanation ===== */
+
+#query-explanation {
+  display: block;
+  position: absolute;
+  top: 0;
+  left: 20.5em;
+  height: 2.5em;
+  font-size: 110%;
+  margin-left: 0;
+  padding: 2px;
+  padding-left: 0;
+  padding-top: 1em;
+  font-size: medium;
+}
+
+.explanation-fulltext-label {
+  font-size: 120%;
+  color: #3465a4;
+  margin: 0 0.1em;
+}
+
+.explanation-fulltext-term {
+  font-size: large;
+  color: black;
+  margin: 0 0.1em;
+}
+
+.explanation-fulltext-criteria {
+  font-size: medium;
+  color: #888;
+  margin: 0 0.1em;
+}
+
+.explanation-change-label {
+  font-size: 80%;
+  color: black;
+  margin: 0 0.1em;
+}
+
+.explanation-query-label {
+  margin-top: 1ex;
+}
+
+.explanation-query-label,
+.explanation-query-involves,
+.explanation-query-tagged {
+  color: #3465a4;
+  margin-right: 0.5ex;
+}
+
+/* ===== Facets ===== */
+
+h1, h2, h3 {
+  color: #3465a4;
+}
+
+#filter-header-label {
+  font-size: 120%;
+  margin: 0;
+  margin-top: 1em;
+}
+
+.explanation-change-label {
+  margin-left: 1em;
+  font-size: 80%;
+  border: 1px solid grey;
+  -moz-border-radius: 4px;
+  padding: 3px;
+}
+
+.explanation-change-label:hover {
+  cursor: pointer;
+  background-color: #aed5ff;
+}
+
+.facetious[uninitialized] {
+  display: none;
+}
+
+.facets-sidebar {
+  display: block;
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  width: 20em;
+  height: 100%;
+  background-color: #eeeeee;
+  padding: 4px;
+  padding-left: 1em;
+  font-size: 90%;
+}
+
+.facetious {
+  display: inline-block;
+  padding: 2px;
+}
+
+.facet > h2 {
+  display: inline;
+  margin: 0;
+  font-size: medium;
+}
+
+.facet-included[state="empty"],
+.facet-excluded[state="empty"] {
+  display: none;
+}
+
+#facet-date {
+  position: fixed;
+  bottom: -4px;
+  left: 20em;
+  padding: 0px;
+  background-color: rgba(255,255,255,0.6);
+
+  display: block;
+}
+
+/* === Boolean Facet === */
+
+.facetious[type="boolean"][disabled] {
+  color: #888888;
+}
+
+.facet-checkbox-bubble {
+  padding: 2px;
+  padding-right: 6px;
+  border: solid transparent 1px;
+  cursor: pointer;
+  color: #333;
+  font-size: 90%;
+}
+
+
+.facetious[type="boolean"][disabled] > .facet-checkbox-bubble,
+.facetious[type="boolean-filtered"][disabled] > .facet-checkbox-bubble {
+  cursor: default;
+  color: #777;
+}
+
+.facetious[type="boolean"]:not([disabled]):hover > .facet-checkbox-bubble,
+.facetious[type="boolean-filtered"]:not([disabled]):hover > .facet-checkbox-bubble {
+  background-color: #aed5ff;
+  border: solid #3465a4 1px;
+  -moz-border-radius: 4px;
+}
+
+
+
+.facet-checkbox-label {
+}
+
+.facet-checkbox-count {
+}
+.facet-checkbox-count:before {
+  content: " (";
+}
+.facet-checkbox-count:after {
+  content: ")";
+}
+
+/* === Boolean Filtered === */
+
+.facetious[type="boolean-filtered"]:not([checked]) > .facet-filter-list {
+  display: none
+}
+
+.facet-filter-list {
+  display: block;
+}
+
+/* === Discrete Facet === */
+
+/*
+ * Our basis for the bar-chart comes from:
+ *  http://www.alistapart.com/articles/accessibledatavisualization/
+ * Thank you, Wilson Miner.
+ */
+
+.barry {
+  border-top: 1px solid #EEE;
+  margin: 0;
+  padding: 4px;
+}
+
+.facet-modes {
+  font-size: 80%;
+  cursor: pointer;
+  color: #888888;
+}
+
+.facet-modes:before {
+  font-weight: normal;
+  content: "( ";
+}
+
+.facet-mode:not(:first-child):before {
+  font-weight: normal;
+  content: " | ";
+}
+
+.facet-modes:after {
+  font-weight: normal;
+  content: " )";
+}
+
+.facet-mode[selected] {
+  font-weight: bold;
+}
+
+.bar {
+  position: relative;
+  display: block;
+  border-bottom: 1px solid #EEE;
+  _zoom: 1;
+  cursor: pointer;
+  font-size: 80%;
+}
+
+.bar[included] {
+  background-color: #efffef;
+}
+
+.bar[excluded] {
+  text-decoration: line-through;
+}
+
+
+.bar:hover {
+  background: #e8e8e8;
+}
+
+.bar-link {
+  color: #2D7BB2;
+  text-decoration: none;
+  display: block;
+  padding: 0.3em 2em 0.3em 0.5em;
+  position: relative;
+  z-index: 2;
+}
+
+.bar:hover > .bar-link {
+  color: #333;
+}
+
+.bar-percent {
+  display: block;
+  position: absolute;
+  top: 0;
+  left: 0;
+  height: 100%;
+  background: #e9f2f5;
+  text-indent: -9999px;
+  overflow: hidden;
+  line-height: 1.6em;
+}
+
+.bar:hover > .bar-percent {
+  background: #ceeaf5;
+}
+
+.bar-exclude {
+  display: block;
+  visibility: hidden;
+  background-image: url("chrome://messenger/skin/icons/thread-ignored.png");
+  position: absolute;
+  top: 0.3em;
+  right: 2px;
+  width: 16px;
+  height: 16px;
+  z-index: 3;
+}
+
+.bar:hover > .bar-exclude {
+  visibility: visible;
+}
+
+.bar-exclude:hover {
+  background-color: #ffffcc;
+}
+
+/* ===== Results ===== */
+
+.results {
+  margin-left: 20.5em;
+  margin-top: 3em;
+}
+
+.results-message-header {
+  background-color: #dcddde;
+  border-top: 2px solid #ccc;
+  padding: 2px;
+}
+
+.results-message-count {
+  display: inline;
+  margin: 0;
+  font-size: medium;
+}
+
+.results-message-showall {
+  margin-left: 1em;
+  cursor: pointer;
+  font-size: 80%;
+}
+
+/* ===== Messages ===== */
+
+.message {
+  display: block;
+  font-family: sans-serif;
+  font-size: 80%;
+  padding-top: 3px;
+  padding-bottom: 3px;
+  margin-right: 1em;
+  border: 1px solid transparent;
+  -moz-border-radius: 3px;
+  border-bottom: 1px solid #ddd;
+  display: block;
+  color: #555;
+  background-color: #ffffff;
+}
+
+.message:hover {
+  border-color: grey;
+  background: #efefef;
+  cursor: pointer;
+}
+
+.message:focus {
+  border: 1px dotted #111;
+  padding: 1em 0px;
+}
+.message[unread="true"]:focus {
+  border: 1px dotted #111;
+  padding: 1em 0px;
+}
+.message:focus:last-child {
+  border: 1px dotted #111;
+  padding: 1em 0px;
+}
+.message:focus:first-child {
+  border: 1px dotted #111;
+  padding: 1em 0px;
+}
+.message:last-child {
+  border-bottom: 1px solid transparent;
+}
+.message:last-child:hover {
+  border-bottom: 1px solid;
+}
+
+
+.message {
+  display: block;
+  padding: 0.2em 0em;
+  padding-right: 1em;
+}
+
+.message-header,
+.message-body {
+  margin-left: 24px;
+  font-size: 95%;
+}
+
+.message-header {
+  margin-bottom: 0.5em;
+}
+.message-meta {
+  float: right;
+  padding-left: 2em;
+  text-align: right;
+  color: #999;
+  font-size: 90%;
+}
+.message-attachments {
+  padding-right: 18px;
+  background: url("chrome://messenger/skin/icons/attachment.png") transparent no-repeat center right;
+  display: none;
+}
+.message-attachments[count] {
+  display: inline;
+}
+.message-attachments:before {
+  content: "(";
+}
+.message-attachments:after {
+  content: ")";
+}
+.message-writes {
+  font-size: 90%;
+  color: #777;
+}
+.message-date {
+  color: #999; font-size: 90%; }
+.message-date:before {
+  content: "\2014  ";
+}
+
+.message-recipients {
+  display: inline;
+  color: #222;
+}
+.message-recipient:first-child:before {
+  content: "";
+}
+.message-recipient:after {
+  content: ", ";
+}
+.message-recipient:last-child:after {
+  content: "";
+}
+
+.message-subject {
+  white-space: nowrap;
+  overflow: hidden;
+  font-size: 115%;
+  font-weight: bold;
+  color: #555;
+}
+.message-body {
+  color: #555;
+  padding-left: 1em;
+  font-family: monospace;
+  font-size: 110%;
+}
+
+.message-tag {
+  -moz-margin-start: 0px;
+  background-image: url("chrome://messenger/skin/tagbg.png");
+  color: black;
+  -moz-border-radius: 2px;
+  padding: 1px 3px;
+  margin-right: 3px;
+}
+.message-folder {
+  background-color: #faf0b8;
+  border: 1px solid #ede4af;
+}
+
new file mode 100644
--- /dev/null
+++ b/mail/base/content/glodaFacetView.js
@@ -0,0 +1,489 @@
+/* ***** 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 Global Database.
+ *
+ * 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>
+ *
+ * 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 ***** */
+
+/*
+ * This file provides the global context for the faceting environment.  In the
+ *  Model View Controller (paradigm), we are the view and the XBL widgets are
+ *  the the view and controller.
+ *
+ * Because much of the work related to faceting is not UI-specific, we try and
+ *  push as much of it into mailnews/db/gloda/facet.js.  In some cases we may
+ *  get it wrong and it may eventually want to migrate.
+ */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://app/modules/gloda/log4moz.js");
+Cu.import("resource://app/modules/StringBundle.js");
+Cu.import("resource://app/modules/PluralForm.jsm");
+Cu.import("resource://app/modules/errUtils.js");
+
+Cu.import("resource://app/modules/gloda/public.js");
+Cu.import("resource://app/modules/gloda/facet.js");
+
+const glodaFacetStrings =
+  new StringBundle("chrome://messenger/locale/glodaFacetView.properties");
+
+/**
+ *
+ */
+function ActiveConstraint(aFaceter, aAttrDef, aInclusive, aGroupValues,
+                          aRanged) {
+  this.faceter = aFaceter;
+  this.attrDef = aAttrDef;
+  this.inclusive = aInclusive;
+  this.ranged = Boolean(aRanged);
+  this.groupValues = aGroupValues;
+
+  this._makeQuery();
+}
+ActiveConstraint.prototype = {
+  _makeQuery: function() {
+    // have the faceter make the query and the invert decision for us if it
+    //  implements the makeQuery method.
+    if ("makeQuery" in this.faceter) {
+      [this.query, this.invertQuery] = this.faceter.makeQuery(this.groupValues,
+                                                              this.inclusive);
+      return;
+    }
+
+    let query = this.query = Gloda.newQuery(Gloda.NOUN_MESSAGE);
+    let constraintFunc;
+    // If the facet definition references a queryHelper defined by the noun
+    //  type, use that instead of the standard constraint function.
+    if ("queryHelper" in this.attrDef.facet)
+      constraintFunc = query[this.attrDef.boundName +
+                             this.attrDef.facet.queryHelper];
+    else
+      constraintFunc = query[this.ranged ? (this.attrDef.boundName + "Range")
+                                         : this.attrDef.boundName];
+    constraintFunc.apply(query, this.groupValues);
+
+    this.invertQuery = !this.inclusive;
+  },
+  /**
+   * Adjust the constraint given the incoming faceting constraint desired.
+   *  Mainly, if the inclusive flag is the same as what we already have, we
+   *  just append the new values to the existing set of values.  If it is not
+   *  the same, we replace them.
+   */
+  adjust: function(aInclusive, aGroupValues) {
+    if (aInclusive == this.inclusive) {
+      this.groupValues = this.groupValues.concat(aGroupValues);
+      this._makeQuery();
+      return;
+    }
+
+    this.inclusive = aInclusive;
+    this.groupValues = aGroupValues;
+    this._makeQuery();
+  },
+  /**
+   * Replace the existing constraints with the new constraint.
+   */
+  replace: function(aInclusive, aGroupValues) {
+    this.inclusive = aInclusive;
+    this.groupValues = aGroupValues;
+    this._makeQuery();
+  },
+  /**
+   * Filter the items against our constraint.
+   */
+  sieve: function(aItems) {
+    let query = this.query;
+    let expectedResult = !this.invertQuery;
+    let outItems = [];
+    for each (let [, item] in Iterator(aItems)) {
+      if (query.test(item) == expectedResult)
+        outItems.push(item);
+    }
+    return outItems;
+  }
+};
+
+var FacetContext = {
+  facetDriver: new FacetDriver(Gloda.lookupNounDef("message"),
+                               window),
+
+  /**
+   * The root collection which our active set is a subset of.  We hold onto this
+   *  for garbage collection reasons, although the tab that owns us should also
+   *  be holding on.
+   */
+  _collection: null,
+  set collection(aCollection) {
+    this._collection = aCollection;
+  },
+  get collection() {
+    return this._collection;
+  },
+
+  /**
+   * List of the current working set
+   */
+  _activeSet: null,
+  get activeSet() {
+    return this._activeSet;
+  },
+
+  initialBuild: function() {
+    let queryExplanation = document.getElementById("query-explanation");
+    if (this.searcher)
+      queryExplanation.setFulltext(this.searcher);
+    else
+      queryExplanation.setQuery(this.collection.query);
+    // we like to sort them so should clone the list
+    this.faceters = this.facetDriver.faceters.concat();
+
+    this.everFaceted = false;
+
+    this.build(this._collection.items);
+  },
+
+  build: function(aNewSet) {
+    this._activeSet = aNewSet;
+    this.facetDriver.go(this._activeSet, this.facetingCompleted, this);
+  },
+
+  /**
+   * Attempt to figure out a reasonable number of rows to limit each facet to
+   *  display.  While the number will ordinarily be dominated by the maximum
+   *  number of rows we believe the user can easily scan, this may also be
+   *  impacted by layout concerns (since we want to avoid scrolling).
+   */
+  planLayout: function() {
+    // XXX arbitrary!
+    this.maxDisplayRows = 8;
+    this.maxMessagesToShow = 8;
+  },
+
+  _groupCountComparator: function(a, b) {
+    return b.groupCount - a.groupCount;
+  },
+  /**
+   * Tells the UI about all the facets when notified by the |facetDriver| when
+   *  it is done faceting everything.
+   */
+  facetingCompleted: function() {
+    this.planLayout();
+
+    let uiFacets = document.getElementById("facets");
+
+    if (!this.everFaceted) {
+      this.everFaceted = true;
+      this.faceters.sort(this._groupCountComparator);
+      for each (let [, faceter] in Iterator(this.faceters)) {
+        let attrName = faceter.attrDef.attributeName;
+        let explicitBinding = document.getElementById("facet-" + attrName);
+
+        if (explicitBinding) {
+          explicitBinding.faceter = faceter;
+          explicitBinding.attrDef = faceter.attrDef;
+          explicitBinding.nounDef = faceter.attrDef.objectNounDef;
+          explicitBinding.orderedGroups = faceter.orderedGroups;
+          // explicit booleans should always be displayed for consistency
+          if (faceter.groupCount >= 1 ||
+              faceter.type == "boolean") {
+            explicitBinding.build(true);
+            explicitBinding.removeAttribute("uninitialized");
+          }
+          faceter.xblNode = explicitBinding;
+          continue;
+        }
+
+        // ignore facets that do not vary!
+        if (faceter.groupCount <= 1) {
+          faceter.xblNode = null;
+          continue;
+        }
+
+        faceter.xblNode = uiFacets.addFacet(faceter.type, faceter.attrDef, {
+          faceter: faceter,
+          orderedGroups: faceter.orderedGroups,
+          maxDisplayRows: this.maxDisplayRows,
+        });
+      }
+    }
+    else {
+      for each (let [, faceter] in Iterator(this.faceters)) {
+        // Do not bother with un-displayed facets, or that are locked by a
+        //  constraint.  But do bother if the widget can be updated without
+        //  losing important data.
+        if (!faceter.xblNode ||
+            (faceter.constraint && !faceter.xblNode.canUpdate))
+          continue;
+
+        // hide things that have 0/1 groups now and are not constrained and not
+        //  boolean
+        if (faceter.groupCount <= 1 && !faceter.constraint &&
+            (faceter.type != "boolean"))
+          $(faceter.xblNode).hide();
+        // otherwise, update
+        else {
+          faceter.xblNode.orderedGroups = faceter.orderedGroups;
+          faceter.xblNode.build(false);
+          $(faceter.xblNode).show();
+        }
+      }
+    }
+
+    let results = document.getElementById("results");
+    let numMessageToShow = Math.min(this.maxMessagesToShow,
+                                    this._activeSet.length);
+    results.setMessages(this._activeSet.slice(0, numMessageToShow));
+  },
+
+  _HOVER_STABILITY_DURATION_MS: 100,
+  _brushedFacet: null,
+  _brushedGroup: null,
+  _brushedItems: null,
+  _brushTimeout: null,
+  hoverFacet: function(aFaceter, aAttrDef, aGroupValue, aGroupItems) {
+    // bail if we are already brushing this item
+    if (this._brushedFacet == aFaceter && this._brushedGroup == aGroupValue)
+      return;
+
+    this._brushedFacet = aFaceter;
+    this._brushedGroup = aGroupValue;
+    this._brushedItems = aGroupItems;
+
+    if (this._brushTimeout != null)
+      clearTimeout(this._brushTimeout);
+    this._brushTimeout = setTimeout(this._timeoutHoverWrapper,
+                                    this._HOVER_STABILITY_DURATION_MS, this);
+
+  },
+  _timeoutHover: function() {
+    this._brushTimeout = null;
+    for each (let [, faceter] in Iterator(this.faceters)) {
+      if (faceter == this._brushedFacet || !faceter.xblNode)
+        continue;
+
+      if (this._brushedItems != null)
+        faceter.xblNode.brushItems(this._brushedItems);
+      else
+        faceter.xblNode.clearBrushedItems();
+    }
+  },
+  _timeoutHoverWrapper: function(aThis) {
+    aThis._timeoutHover();
+  },
+  unhoverFacet: function(aFaceter, aAttrDef, aGroupValue, aGroupItems) {
+    // have we already brushed from some other source already?  ignore then.
+    if (this._brushedFacet != aFaceter || this._brushedGroup != aGroupValue)
+      return;
+
+    // reuse hover facet to null everyone out
+    this.hoverFacet(null, null, null, null);
+  },
+
+  /**
+   * Maps attribute names to their corresponding |ActiveConstraint|, if they
+   *  have one.
+   */
+  _activeConstraints: {},
+  /**
+   * Called by facets when the user does some clicking and wants to impose a new
+   *  constraint.
+   *
+   * @param aFaceter
+   * @param aAttrDef
+   * @param {Boolean} aInclusive
+   * @param aGroupValues
+   * @param aRanged Is it a ranged constraint?  (Currently only for dates)
+   * @param aNukeExisting Do we need to replace the existing constraint and
+   *     re-sieve everything?  This currently only happens for dates, where
+   *     our display allows a click to actually make our range more generic
+   *     than it currently is.  (But this only matters if we already have
+   *     a date constraint applied.)
+   */
+  addFacetConstraint: function(aFaceter, aAttrDef, aInclusive, aGroupValues,
+                               aRanged, aNukeExisting) {
+    let attrName = aAttrDef.attributeName;
+
+    let constraint;
+    let needToSieveAll = false;
+    if (attrName in this._activeConstraints) {
+      constraint = this._activeConstraints[attrName];
+
+      needToSieveAll = true;
+      if (aNukeExisting)
+        constraint.replace(aInclusive, aGroupValues);
+      else
+        constraint.adjust(aInclusive, aGroupValues);
+    }
+    else {
+      constraint = this._activeConstraints[attrName] =
+        new ActiveConstraint(aFaceter, aAttrDef, aInclusive, aGroupValues,
+                             aRanged);
+    }
+    aFaceter.constraint = constraint;
+
+    // Given our current implementation, we can only be further constraining our
+    //  active set, so we can just sieve the existing active set with the
+    //  (potentially updated) constraint.  In some cases, it would be much
+    //  cheaper to use the facet's knowledge about the items in the groups, but
+    //  for now let's keep a single code-path for how we refine the active set.
+    this.build(needToSieveAll ? this._sieveAll()
+                              : constraint.sieve(this.activeSet));
+  },
+
+  removeFacetConstraint: function(aFaceter) {
+    let attrName = aFaceter.attrDef.attributeName;
+    delete this._activeConstraints[attrName];
+    aFaceter.constraint = null;
+
+    // we definitely need to re-sieve everybody in this case...
+    this.build(this._sieveAll());
+  },
+
+  /**
+   * Sieve the items from the underlying collection against all constraints,
+   *  returning the value.
+   */
+  _sieveAll: function() {
+    let items = this.collection.items;
+
+    for each (let [, constraint] in Iterator(this._activeConstraints)) {
+      items = constraint.sieve(items);
+    }
+
+    return items;
+  },
+
+  toggleFulltextCriteria: function() {
+    this.tab.searcher.andTerms = !this.tab.searcher.andTerms;
+    this.collection = this.tab.searcher.getCollection(this);
+  },
+
+  /**
+   * Show the active message set in a glodaList tab, closing the current tab.
+   */
+  showActiveSetInTab: function() {
+    let tabmail = this.rootWin.document.getElementById("tabmail");
+    tabmail.openTab("glodaList", {
+      collection: Gloda.explicitCollection(Gloda.NOUN_MESSAGE, this.activeSet),
+      title: this.tab.title
+    });
+    tabmail.closeTab(this.tab);
+  },
+
+  /**
+   * Show the conversation in a new glodaList tab.
+   *
+   * @param {GlodaConversation} aConversation The conversation to show.
+   * @param {Boolean} [aBackground] Whether it should be in the background.
+   */
+  showConversationInTab: function(aMessage, aBackground) {
+    let tabmail = this.rootWin.document.getElementById("tabmail");
+    tabmail.openTab("glodaList", {
+      conversation: aMessage.conversation,
+      message: aMessage,
+      title: aMessage.conversation.subject,
+      background: aBackground
+    });
+  },
+
+  /**
+   * Show the message in a new tab.
+   *
+   * @param {GlodaMessage} aMessage The message to show.
+   * @param {Boolean} [aBackground] Whether it should be in the background.
+   */
+  showMessageInTab: function(aMessage, aBackground) {
+    let tabmail = this.rootWin.document.getElementById("tabmail");
+    let msgHdr = aMessage.folderMessage;
+    if (!msgHdr)
+      throw new Error("Unable to translate gloda message to message header.");
+    tabmail.openTab("message", {
+      msgHdr: msgHdr,
+      background: aBackground
+    });
+  },
+
+  onItemsAdded: function(aItems, aCollection) {
+  },
+  onItemsModified: function(aItems, aCollection) {
+  },
+  onItemsRemoved: function(aItems, aCollection) {
+  },
+  onQueryCompleted: function(aCollection) {
+    this.initialBuild();
+  }
+};
+
+/**
+ * addEventListener betrayals compel us to establish our link with the
+ *  outside world from inside.  NeilAway suggests the problem might have
+ *  been the registration of the listener prior to initiating the load.  Which
+ *  is odd considering it works for the XUL case, but I could see how that might
+ *  differ.  Anywho, this works for now and is a delightful reference to boot.
+ */
+function reachOutAndTouchFrame() {
+  let us = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                 .getInterface(Ci.nsIWebNavigation)
+                 .QueryInterface(Ci.nsIDocShellTreeItem);
+
+  FacetContext.rootWin = us.rootTreeItem
+                    .QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIDOMWindow);
+
+  let parentWin = us.parent
+                    .QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIDOMWindow);
+  let aTab = FacetContext.tab = parentWin.tab;
+  parentWin.tab = null;
+
+  // we need to hook the context up as a listener in all cases since
+  //  removal notifications are required.
+  if ("searcher" in aTab) {
+    FacetContext.searcher = aTab.searcher;
+    aTab.searcher.listener = FacetContext;
+  }
+  else {
+    FacetContext.searcher = null;
+    aTab.collection.listener = FacetContext;
+  }
+  FacetContext.collection = aTab.collection;
+
+  // if it has already completed, we need to prod things
+  if (aTab.query.completed)
+    FacetContext.initialBuild();
+}
new file mode 100644
--- /dev/null
+++ b/mail/base/content/glodaFacetView.xhtml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % aboutDTD SYSTEM "chrome://global/locale/about.dtd" >
+%aboutDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+%globalDTD;
+<!ENTITY % facetViewDTD SYSTEM "chrome://messenger/locale/glodaFacetView.dtd">
+%facetViewDTD;
+]>
+<html xmlns="http://www.w3.org/1999/xhtml"
+    xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+    version="-//W3C//DTD XHTML 1.1//EN" xml:lang="en">
+<head>
+  <!-- XBL bindings CSS -->
+  <link rel="stylesheet"
+      href="chrome://messenger/content/glodaFacetBindings.css"
+      type="text/css"></link>
+  <link rel="stylesheet" media="screen" type="text/css"
+        href="chrome://messenger/skin/tagColors.css"/>
+  <!-- Themes -->
+  <link rel="stylesheet"
+      href="chrome://messenger/content/glodaFacetView.css"
+      type="text/css"></link>
+  <!-- Global Context -->
+  <script type="application/javascript;version=1.8"
+      src="chrome://messenger/content/glodaFacetView.js"></script>
+  <!-- Libs -->
+  <script type="application/javascript;version=1.8"
+      src="chrome://messenger/content/jquery.js"></script>
+  <script type="application/javascript;version=1.8"
+      src="chrome://messenger/content/protovis-r2.6-modded.js"></script>
+  <!-- Facet Binding Stuff that doesn't belong in XBL -->
+  <script type="application/javascript;version=1.8"
+      src="chrome://messenger/content/glodaFacetVis.js"></script>
+</head>
+<body onload="reachOutAndTouchFrame()">
+  <div class="facets facets-sidebar" id="facets">
+    <h1 id="filter-header-label">&glodaFacetView.filters.label;</h1>
+    <div id="facet-fromMe" class="facetious" type="boolean" attr="fromMe"
+         uninitialized="true" />
+    <div id="facet-toMe" class="facetious" type="boolean" attr="toMe"
+         uninitialized="true" />
+    <div id="facet-star" class="facetious" type="boolean" attr="star"
+         uninitialized="true"/><br />
+    <div id="facet-attachmentTypes" class="facetious" type="boolean-filtered"
+         attr="attachmentTypes"
+         groupDisplayProperty="categoryLabel"
+         uninitialized="true"/>
+  </div>
+  <div id="query-explanation" />
+  <div class="results" id="results" type="message" />
+  <div id="facet-date" class="facetious" type="date" />
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/mail/base/content/glodaFacetViewWrapper.xul
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<window id="window" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript"
+      src="chrome://global/content/viewZoomOverlay.js"/>
+  <script type="application/javascript;version=1.8"><![CDATA[
+    function getBrowser() {
+        return document.getElementById('browser');
+    }
+  ]]></script>
+  <commandset id="selectEditMenuItems">
+    <command id="cmd_fullZoomReduce" oncommand="ZoomManager.reduce();"/>
+    <command id="cmd_fullZoomEnlarge" oncommand="ZoomManager.enlarge();"/>
+    <command id="cmd_fullZoomReset" oncommand="ZoomManager.reset();"/>
+  </commandset>
+  <keyset>
+    <!--move to locale-->
+    <key id="key_fullZoomEnlarge" key="+"
+         command="cmd_fullZoomEnlarge" modifiers="accel"/>
+    <key id="key_fullZoomEnlarge2" key="="
+         command="cmd_fullZoomEnlarge" modifiers="accel"/>
+    <key id="key_fullZoomReduce" key="-"
+         command="cmd_fullZoomReduce" modifiers="accel"/>
+    <key id="key_fullZoomReset" key="0"
+         command="cmd_fullZoomReset" modifiers="accel"/>
+  </keyset>
+  <browser id="browser"
+           flex="1" />
+</window>
new file mode 100644
--- /dev/null
+++ b/mail/base/content/glodaFacetVis.js
@@ -0,0 +1,407 @@
+/****** 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 Global Database.
+ *
+ * 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>
+ *
+ * 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 ***** */
+
+/*
+ * Facet visualizations that would be awkward in XBL.  Allegedly because the
+ *  interaciton idiom of a protovis-based visualization is entirely different
+ *  from XBL, but also a lot because of the lack of good syntax highlighting.
+ */
+
+/**
+ * A date facet visualization abstraction.
+ */
+function DateFacetVis(aBinding, aCanvasNode) {
+  this.binding = aBinding;
+  this.canvasNode = aCanvasNode;
+
+  this.faceter = aBinding.faceter;
+  this.attrDef = this.faceter.attrDef;
+}
+DateFacetVis.prototype = {
+  build: function() {
+    this.allowedSpace = document.documentElement.clientWidth -
+                        this.canvasNode.getBoundingClientRect().left;
+    this.render();
+  },
+  rebuild: function() {
+    this.render();
+  },
+
+  _MIN_BAR_SIZE_PX: 9,
+  _BAR_SPACING_PX: 1,
+
+  _MAX_BAR_SIZE_PX: 15,
+
+  _AXIS_FONT: "10px sans-serif",
+  _AXIS_HEIGHT_NO_LABEL_PX: 6,
+  _AXIS_HEIGHT_WITH_LABEL_PX: 14,
+  _AXIS_VERT_SPACING_PX: 1,
+  _AXIS_HORIZ_MIN_SPACING_PX: 4,
+
+  _MAX_DAY_COUNT_LABEL_DISPLAY: 10,
+
+  /**
+   * Figure out how to chunk things given the linear space in pixels.  In an
+   *  ideal world we would not use pixels, avoiding tying ourselves to assumed
+   *  pixel densities, but we do not live there.  Reality wants crisp graphics
+   *  and does not have enough pixels that you can ignore the pixel coordinate
+   *  space and have things still look sharp (and good).
+   *
+   * Because of our love of sharpness, we will potentially under-use the space
+   *  allocated to us.
+   *
+   * @param aPixels The number of linear content pixels we have to work with.
+   *     You are in charge of the borders and such, so you subtract that off
+   *     before you pass it in.
+   * @return An object with attributes:
+   */
+  makeIdealScaleGivenSpace: function(aPixels) {
+    let facet = this.faceter;
+    // build a scale and have it grow the edges based on the span
+    let scale = pv.Scales.dateTime(facet.oldest, facet.newest);
+
+    const Span = pv.Scales.DateTimeScale.Span;
+    const MS_MIN = 60*1000, MS_HOUR = 60*MS_MIN, MS_DAY = 24*MS_HOUR,
+          MS_WEEK = 7*MS_DAY, MS_MONTHISH = 31*MS_DAY, MS_YEARISH = 366*MS_DAY;
+    const roughMap = {};
+    roughMap[Span.DAYS] = MS_DAY;
+    roughMap[Span.WEEKS] = MS_WEEK;
+    // we overestimate since we want to slightly underestimate pixel usage
+    //  in enoughPix's rough estimate
+    roughMap[Span.MONTHS] = MS_MONTHISH;
+    roughMap[Span.YEARS] = MS_YEARISH;
+
+    const minBarPix = this._MIN_BAR_SIZE_PX + this._BAR_SPACING_PX;
+
+    let delta = facet.newest.valueOf() - facet.oldest.valueOf();
+    let span, rules, barPixBudget;
+    // evil side-effect land
+    function enoughPix(aSpan) {
+      span = aSpan;
+      // do a rough guestimate before doing something potentially expensive...
+      barPixBudget = Math.floor(aPixels / (delta / roughMap[span]));
+      if (barPixBudget < (minBarPix + 1))
+        return false;
+
+      rules = scale.ruleValues(span);
+      // + 0 because we want to over-estimate slightly for niceness rounding
+      //  reasons
+      barPixBudget = Math.floor(aPixels / (rules.length + 0));
+      delta = scale.max().valueOf() - scale.min().valueOf();
+      return barPixBudget > minBarPix;
+    }
+
+    // day is our smallest unit
+    const ALLOWED_SPANS = [Span.DAYS, Span.WEEKS, Span.MONTHS, Span.YEARS];
+    for each (let [, trySpan] in Iterator(ALLOWED_SPANS)) {
+      if (enoughPix(trySpan)) {
+        // do the equivalent of nice() for our chosen span
+        scale.min(scale.round(scale.min(), trySpan, false));
+        scale.max(scale.round(scale.max(), trySpan, true));
+        // try again for paranoia, but mainly for the side-effect...
+        if (enoughPix(trySpan))
+          break;
+      }
+    }
+
+    // - Figure out our labeling strategy
+    // normalize the symbols into an explicit ordering
+    let spandex = ALLOWED_SPANS.indexOf(span);
+    // from least-specific to most-specific
+    let labelTiers = [];
+    // add year spans in all cases, although whether we draw bars depends on if
+    //  we are in year mode or not
+    labelTiers.push({
+      rules: (span == Span.YEARS) ? rules : scale.ruleValues(Span.YEARS, true),
+      label: ["%Y", "%y", null], // we should not hit the null case...
+      boost: (span == Span.YEARS),
+      noFringe: (span == Span.YEARS)
+    });
+    // add month spans if we are days or weeks...
+    if (spandex < 2) {
+      labelTiers.push({
+        rules: scale.ruleValues(Span.MONTHS, true),
+        // try to use the full month, falling back to the short month
+        label: ["%B", "%b", null],
+        boost: false
+      });
+    }
+    // add week spans if our granularity is days...
+    if (span == Span.DAYS) {
+      let numDays = delta / MS_DAY;
+
+      // find out how many days we are talking about and add days if it's small
+      //  enough, display both the date and the day of the week
+      if (numDays <= this._MAX_DAY_COUNT_LABEL_DISPLAY) {
+        labelTiers.push({
+          rules: rules,
+          label: ["%d", null],
+          boost: true, noFringe: true
+        });
+        labelTiers.push({
+          rules: rules,
+          label: ["%a", null],
+          boost: true, noFringe: true
+        });
+      }
+      // show the weeks since we're at greater than a day time-scale
+      else {
+        labelTiers.push({
+          rules: scale.ruleValues(Span.WEEKS, true),
+          // labeling weeks is nonsensical; no one understands ISO weeks
+          //  numbers.
+          label: [null],
+          boost: false
+        });
+      }
+    }
+
+    return {
+      scale: scale, span: span, rules: rules, barPixBudget: barPixBudget,
+      labelTiers: labelTiers
+    };
+  },
+
+  render: function() {
+    let {scale: scale, span: span, rules: rules, barPixBudget: barPixBudget,
+         labelTiers: labelTiers} =
+      this.makeIdealScaleGivenSpace(this.allowedSpace);
+
+    barPixBudget = Math.floor(barPixBudget);
+
+    let minBarPix = this._MIN_BAR_SIZE_PX + this._BAR_SPACING_PX;
+    let maxBarPix = this._MAX_BAR_SIZE_PX + this._BAR_SPACING_PX;
+
+    let barPix = Math.max(minBarPix, Math.min(maxBarPix, barPixBudget));
+    let width = barPix * (rules.length - 1);
+
+    let totalAxisLabelHeight = 0;
+
+    // we need to do some font-metric calculations, so create a canvas...
+    let fontMetricCanvas = document.createElement("canvas");
+    let ctx = fontMetricCanvas.getContext("2d");
+
+    // do the labeling logic,
+    for each (let [, labelTier] in Iterator(labelTiers)) {
+      let labelRules = labelTier.rules;
+      let perLabelBudget = width / (labelRules.length - 1);
+      for each (let [, labelFormat] in Iterator(labelTier.label)) {
+        let maxWidth = 0;
+        let displayValues = [];
+        for (let iRule = 0; iRule < labelRules.length - 1; iRule++) {
+          // is this at the either edge of the display?  in that case, it might
+          //  be partial...
+          let fringe = (labelRules.length > 2) &&
+                       ((iRule == 0) || (iRule == labelRules.length - 2));
+          let labelStartDate = labelRules[iRule];
+          let labelEndDate = labelRules[iRule + 1];
+          let labelText = labelFormat ?
+                            labelStartDate.toLocaleFormat(labelFormat) : null;
+          let labelStartNorm = Math.max(0, scale.normalize(labelStartDate));
+          let labelEndNorm = Math.min(1, scale.normalize(labelEndDate));
+          let labelBudget = (labelEndNorm - labelStartNorm) * width;
+          if (labelText) {
+            let labelWidth = ctx.measureText(labelText).width;
+            // discard labels at the fringe who don't fit in our budget
+            if (fringe && !labelTier.noFringe && labelWidth > labelBudget)
+              labelText = null;
+            else
+              maxWidth = Math.max(labelWidth, maxWidth);
+          }
+
+          displayValues.push([labelStartNorm, labelEndNorm, labelText,
+                              labelStartDate, labelEndDate]);
+        }
+        // there needs to be space between the labels.  (we may be over-padding
+        //  here if there is only one label with the maximum width...)
+        maxWidth += this._AXIS_HORIZ_MIN_SPACING_PX;
+
+        if (labelTier.boost && (maxWidth > perLabelBudget)) {
+          // we only boost labels that are the same span as the bins, so rules
+          //  === labelRules at this point.  (and barPix === perLabelBudget)
+          barPix = perLabelBudget = maxWidth;
+          width = barPix * (labelRules.length - 1);
+        }
+        if (maxWidth <= perLabelBudget) {
+          labelTier.displayValues = displayValues;
+          labelTier.displayLabel = labelFormat != null;
+          labelTier.vertHeight = labelFormat ? this._AXIS_HEIGHT_WITH_LABEL_PX
+                                             : this._AXIS_HEIGHT_NO_LABEL_PX;
+          labelTier.vertOffset = totalAxisLabelHeight;
+          totalAxisLabelHeight += labelTier.vertHeight +
+                                  this._AXIS_VERT_SPACING_PX;
+
+          break;
+        }
+      }
+    }
+
+    let barWidth = barPix - this._BAR_SPACING_PX;
+    let barSpacing = this._BAR_SPACING_PX;
+
+    width = barPix * (rules.length - 1);
+    // we ideally want this to be the same size as the max rows translates to...
+    let height = 100;
+    let ch = height - totalAxisLabelHeight;
+
+    let [bins, maxBinSize] = this.binBySpan(scale, span, rules);
+
+    // build empty bins for our hot bins
+    this.emptyBins = [0 for each (bin in bins)];
+
+    let binScale = maxBinSize ? (ch / maxBinSize) : 1;
+
+    let vis = this.vis = new pv.Panel().canvas(this.canvasNode)
+      // dimensions
+      .width(width).height(height)
+      // margins
+      .bottom(totalAxisLabelHeight);
+
+    let faceter = this.faceter;
+    // bin bars...
+    vis.add(pv.Bar)
+      .data(bins)
+      .bottom(0)
+      .height(function (d) Math.floor(d.items.length * binScale))
+      .width(function() barWidth)
+      .left(function() this.index * barPix)
+      .fillStyle("#e9f2f5")
+      .event("mouseover", function(d) this.fillStyle("#ceeaf5"))
+      .event("mouseout", function(d) this.fillStyle("#e9f2f5"))
+      .event("click", function(d)
+        FacetContext.addFacetConstraint(faceter, faceter.attrDef, true,
+                                        [[d.startDate, d.endDate]],
+                                        true, true));
+
+    this.hotBars = vis.add(pv.Bar)
+      .data(this.emptyBins)
+      .bottom(0)
+      .height(function (d) Math.floor(d * binScale))
+      .width(function() barWidth)
+      .left(function() this.index * barPix)
+      .fillStyle("#ceeaf5");
+
+    for each (let [, labelTier] in Iterator(labelTiers)) {
+      let labelBar = vis.add(pv.Bar)
+        .data(labelTier.displayValues)
+        .bottom(-totalAxisLabelHeight + labelTier.vertOffset)
+        .height(labelTier.vertHeight)
+        .left(function(d) Math.floor(width * d[0]))
+        .width(function(d)
+                 Math.floor(width * d[1]) - Math.floor(width * d[0]) - 1)
+        .fillStyle("#dddddd")
+        .event("mouseover", function(d) this.fillStyle("#ceeaf5"))
+        .event("mouseout", function(d) this.fillStyle("#dddddd"))
+        .event("click", function(d)
+          FacetContext.addFacetConstraint(faceter, faceter.attrDef, true,
+                                          [[d[3], d[4]]], true, true));
+
+      if (labelTier.displayLabel) {
+        labelBar.anchor("top").add(pv.Label)
+          .font(this._AXIS_FONT)
+          .textAlign("center")
+          .textBaseline("top")
+          .textStyle("#888888")
+          .text(function(d) d[2]);
+      }
+    }
+
+
+    vis.render();
+  },
+
+  hoverItems: function(aItems) {
+    let itemToBin = this.itemToBin;
+    let bins = this.emptyBins.concat();
+    for each (let [, item] in Iterator(aItems)) {
+      if (item.id in itemToBin)
+        bins[itemToBin[item.id]]++;
+    }
+    this.hotBars.data(bins);
+    this.vis.render();
+  },
+
+  clearHover: function() {
+    this.hotBars.data(this.emptyBins);
+    this.vis.render();
+  },
+
+  /**
+   * Bin items at the given span granularity with the set of rules generated
+   *  for the given span.  This could equally as well be done as a pre-built
+   *  array of buckets with a linear scan of items and a calculation of what
+   *  bucket they should be placed in.
+   */
+  binBySpan: function(aScale, aSpan, aRules, aItems) {
+    let bins = [];
+    let maxBinSize = 0;
+    let binCount = aRules.length - 1;
+    let itemToBin = this.itemToBin = {};
+
+    // We used to break this out by case, but that was a lot of code, and it was
+    //  somewhat ridiculous.  So now we just do the simple, if somewhat more
+    //  expensive thing.  Reviewer, feel free to thank me.
+    // We do a pass through the rules, mapping each rounded rule to a bin.  We
+    //  then do a pass through all of the items, rounding them down and using
+    //  that to perform a lookup against the map.  We could special-case the
+    //  rounding, but I doubt it's worth it.
+    let binMap = {};
+    for (let iRule = 0; iRule < binCount; iRule++) {
+      let binStartDate = aRules[iRule], binEndDate = aRules[iRule+1];
+      binMap[binStartDate.valueOf().toString()] = iRule;
+      bins.push({items: [],
+                 startDate: binStartDate,
+                 endDate: binEndDate});
+    }
+    let attrKey = this.attrDef.boundName;
+    for each (let [, item] in Iterator(this.faceter.validItems)) {
+      let val = item[attrKey];
+      // round it to the rule...
+      val = aScale.round(val, aSpan, false);
+      // which we can then map...
+      let itemBin = binMap[val.valueOf().toString()];
+      itemToBin[item.id] = itemBin;
+      bins[itemBin].items.push(item);
+    }
+    for each (let [, bin] in Iterator(bins)) {
+      maxBinSize = Math.max(bin.items.length, maxBinSize);
+    }
+
+    return [bins, maxBinSize];
+  }
+
+};
--- a/mail/base/content/mailWindowOverlay.js
+++ b/mail/base/content/mailWindowOverlay.js
@@ -1674,17 +1674,17 @@ function CreateToolbarTooltip(document, 
   if (tn.hasAttribute("label")) {
     event.target.setAttribute("label", tn.getAttribute("label"));
     return true;
   }
   return false;
 }
 
 /**
- * Displays message "folder"s, mail "message"s, and "glodaSearch" results.  The
+ * Displays message "folder"s, mail "message"s, and "glodaList" results.  The
  *  commonality is that they all use the "mailContent" panel's folder tree,
  *  thread tree, and message pane objects.  This happens for historical reasons,
  *  likely involving the fact that prior to the introduction of this
  *  abstraction, everything was always stored in global objects.  For the 3.0
  *  release cycle we considered avoiding this 'multiplexed' style of operation
  *  but decided against moving to making each tab be indepdendent because of
  *  presumed complexity.
  *
@@ -1949,75 +1949,83 @@ let mailTabType = {
           aTab.title += " - " + aMsgHdr.folder.server.prettyName;
       },
       getBrowser: function(aTab) {
         // Message tabs always use the messagepane browser.
         return document.getElementById("messagepane");
       }
     },
     /**
-     * The glodaSearch view displays a gloda-backed nsMsgDBView with only the
+     * The glodaList view displays a gloda-backed nsMsgDBView with only the
      *  thread pane and (potentially) the message pane displayed; the folder
      *  pane is forced hidden.
      */
-    glodaSearch: {
+    glodaList: {
       type: "glodaSearch",
       /// The set of panes that are legal to be displayed in this mode
       legalPanes: {
         folder: false,
         thread: true,
         message: true,
-        glodaFacets: false,
       },
+      /**
+       * The default set of columns to show.  This really should just be for
+       *  boot-strapping and should be persisted after that...
+       */
       desiredColumnStates: {
         flaggedCol: {
           visible: true,
         },
         subjectCol: {
           visible: true,
         },
         senderCol: {
           visible: true,
         },
         dateCol: {
           visible: true,
         },
       },
       /**
-       * Open a new tab whose view is backed by a gloda search.
+       * Open a new folder-display-style tab showing the contents of a gloda
+       *  query/collection.  You must pass one of 'query'/'collection'/
+       *  'conversation'
        *
-       * @param searchString
-       * @param facetString
-       *     - everything:
-       *     - subject:
-       *     - involves:
-       *     - to:
-       *     - from:
-       *     - body:
-       * @param location Either a GlodaFolder or the string "everywhere".
+       * @param {GlodaQuery} [aArgs.query] An un-triggered gloda query to use.
+       *     Alternatively, if you already have a collection, you can pass that
+       *     instead as 'collection'.
+       * @param {GlodaCollection} [aArgs.collection] A gloda collection to
+       *     display.
+       * @param {GlodaConversation} [aArgs.conversation] A conversation whose
+       *     messages you want to display.
+       * @param {GlodaMessage} [aArgs.message] The message to select in the
+       *     conversation, if provided.
+       * @param aArgs.title The title to give to the tab.  If this is not user
+       *     content (a search string, a message subject, etc.), make sure you
+       *     are using a localized string.
        *
        * XXX This needs to handle opening in the background
        */
       openTab: function(aTab, aArgs) {
-        // make sure the search string bundle is loaded
-        getDocumentElements();
-        aTab.title = gSearchBundle.getFormattedString("glodaSearchTabTitle",
-                                                      [aArgs.searchString]);
-        aTab.glodaSearchInputValue = aArgs.searchString;
-        aTab.searchString = aArgs.searchString;
-        aTab.facetString = aArgs.facetString;
-
-        aTab.glodaSynView = new GlodaSyntheticSearchView(aArgs.searchString,
-                                                         aArgs.facetString,
-                                                         aArgs.location);
+        aTab.glodaSynView = new GlodaSyntheticView(aArgs);
+        aTab.title = aArgs.title;
 
         this.openTab(aTab, false, new MessagePaneDisplayWidget());
         aTab.folderDisplay.show(aTab.glodaSynView);
+        // XXX persist column states in preferences or session store or other
         aTab.folderDisplay.setColumnStates(aTab.mode.desiredColumnStates);
-        aTab.folderDisplay.makeActive();
+
+        let background = ("background" in aArgs) && aArgs.background;
+        if (!background)
+          aTab.folderDisplay.makeActive();
+        if ("message" in aArgs) {
+          let hdr = aArgs.message.folderMessage;
+          if (hdr)
+            aTab.folderDisplay.selectMessage(hdr);
+        }
       },
       getBrowser: function(aTab) {
         // If we are currently a thread summary, we want to select the multi
         // message browser rather than the message pane.
         return gMessageDisplay.singleMessageDisplay ?
                document.getElementById("messagepane") :
                document.getElementById("multimessage");
       }
@@ -2161,17 +2169,16 @@ let mailTabType = {
    *     the value is true, then the pane is legal.  Omitted pane keys imply
    *     that the pane is illegal.  Keys are:
    *     - folder: The folder (tree) pane.
    *     - thread: The thread pane.
    *     - accountCentral: While it's in a deck with the thread pane, this
    *        is distinct from the thread pane because some other things depend
    *        on whether it's actually the thread pane we are showing.
    *     - message: The message pane.  Required/assumed to be true for now.
-   *     - glodaFacets: The gloda search facets pane.
    * @param aVisibleStates A dictionary where each value indicates whether the
    *     pane should be 'visible' (not collapsed).  Only panes that are governed
    *     by splitters are options here.  Keys are:
    *     - folder: The folder (tree) pane.
    *     - message: The message pane.
    */
   _setPaneStates: function mailTabType_setPaneStates(aLegalStates,
                                                      aVisibleStates) {
@@ -2248,20 +2255,16 @@ let mailTabType = {
 
     // we are responsible for updating the keybinding; view_init takes care of
     //  updating the menu item (on demand)
     let messagePaneToggleKey = document.getElementById("key_toggleMessagePane");
     if (aLegalStates.thread)
       messagePaneToggleKey.removeAttribute("disabled");
     else
       messagePaneToggleKey.setAttribute("disabled", "true");
-
-    // -- gloda facets
-    document.getElementById("glodaSearchFacets").hidden =
-      !aLegalStates.glodaFacets;
   },
 
   showTab: function(aTab) {
     // Set the messagepane as the primary browser for content.
     document.getElementById("messagepane").setAttribute("type",
                                                         "content-primary");
 
     aTab.folderDisplay.makeActive();
@@ -2294,52 +2297,16 @@ let mailTabType = {
   doCommand: function(aTab, aCommand) {
     DefaultController.doCommand(aCommand, aTab);
   },
 
   onEvent: function(aTab, aEvent) {
     DefaultController.onEvent(aEvent);
   }
 };
-/**
- * The glodaSearch tab mode has a UI widget outside of the mailTabType's
- *  display panel, the #glodaSearchInput textbox.  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 glodaSearch) tab.
- *  The behaviour rationale for this is that the glodaSearchInput 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.  It's not entirely clear that imitating this behaviour on
- *  non-glodaSearch tabs makes a lot of sense, but it is consistent, so we do
- *  so.  The counter-example to this choice is the search box in firefox, but
- *  it never updates when you switch tabs, so it is arguably less of a fit.
- */
-var glodaSearchTabMonitor = {
-  onTabTitleChanged: function() {},
-  onTabSwitched: function glodaSearchTabMonitor_onTabSwitch(aTab, aOldTab) {
-    let inputNode = document.getElementById("glodaSearchInput");
-    if (!inputNode)
-      return;
-
-    // save the current search field value
-    if (aOldTab)
-      aOldTab.glodaSearchInputValue = inputNode.value;
-    // load (or clear if there is none) the persisted search field value
-    inputNode.value = aTab.glodaSearchInputValue || "";
-
-    // If the mode is glodaSearch and the search is unchanged, then we want to
-    //  set the icon state of the input box to be the 'clear' icon.
-    if (aTab.mode.name == "glodaSearch") {
-      if (aTab.searchString == aTab.glodaSearchInputValue)
-        inputNode._searchIcons.selectedIndex = 1;
-    }
-  }
-};
 
 
 function MsgOpenNewWindowForFolder(folderURI, msgKeyToSelect)
 {
   if (folderURI) {
     window.openDialog("chrome://messenger/content/", "_blank",
                       "chrome,all,dialog=no", folderURI, msgKeyToSelect);
     return;
--- a/mail/base/content/messenger.css
+++ b/mail/base/content/messenger.css
@@ -159,29 +159,25 @@ searchterm {
 .ruleactiontarget[type="replytomessage"] {
   -moz-binding: url("chrome://messenger/content/searchWidgets.xml#ruleactiontarget-replyto");
 }
 
 dummy.usesMailWidgets {
   -moz-binding: url("chrome://messenger/content/mailWidgets.xml#dummy");
 }
 
-#glodaSearchInput {
+#searchInput {
   -moz-binding: url("chrome://messenger/content/search.xml#glodaSearch");
 }
 
-#glodaSearchFacets {
-  -moz-binding: url("chrome://messenger/content/search.xml#glodaFacets");
-}
-
-#searchInput {
+#oldsearchInput {
   -moz-binding: url("chrome://messenger/content/search.xml#searchbar");
 }
 
-#quick-search-button {
+.quick-search-button {
   -moz-binding: url("chrome://messenger/content/search.xml#searchBarDropMarker");
   cursor: default;
   -moz-user-focus: none;
 }
 
 .quick-search-clearbutton{
   cursor: default;
   -moz-user-focus: none;
--- a/mail/base/content/messenger.xul
+++ b/mail/base/content/messenger.xul
@@ -35,16 +35,18 @@
 # 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/mailWindow1.css" type="text/css"?>
+<?xml-stylesheet href="chrome://gloda/content/glodacomplete.css" type="text/css"?>
+
 
 <?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"?>
@@ -85,22 +87,23 @@
 <script type="application/x-javascript" src="chrome://messenger/content/widgetglue.js"/>
 <script type="application/x-javascript" src="chrome://messenger/content/commandglue.js"/>
 <script type="application/x-javascript" src="chrome://messenger/content/shareglue.js"/>
 <script type="application/x-javascript" src="chrome://messenger/content/msgViewNavigation.js"/>
 <script type="application/x-javascript" src="chrome://messenger/content/mailWindow.js"/>
 <script type="application/x-javascript" src="chrome://messenger/content/selectionsummaries.js"/>
 <script type="application/x-javascript" src="chrome://messenger/content/msgMail3PaneWindow.js"/>
 <script type="application/x-javascript" src="chrome://messenger/content/specialTabs.js"/>
+<script type="application/x-javascript" src="chrome://messenger/content/glodaFacetTab.js"/>
+<script type="application/x-javascript" src="chrome://messenger/content/searchBar.js"/>
 <script type="application/x-javascript" src="chrome://messenger/content/mail3PaneWindowCommands.js"/>
 <script type="application/x-javascript" src="chrome://global/content/contentAreaUtils.js"/>
 <script type="application/x-javascript" src="chrome://communicator/content/nsContextMenu.js"/>
 <script type="application/x-javascript" src="chrome://messenger/content/mailContextMenus.js"/>
 <script type="application/x-javascript" src="chrome://messenger/content/accountUtils.js"/>
-<script type="application/x-javascript" src="chrome://messenger/content/searchBar.js"/>
 <script type="application/x-javascript" src="chrome://messenger/content/folderPane.js"/>
 <script type="application/x-javascript" src="chrome://messenger/content/phishingDetector.js"/>
 <script type="application/x-javascript" src="chrome://communicator/content/contentAreaClick.js"/>
 <script type="application/x-javascript" src="chrome://global/content/nsDragAndDrop.js"/>
 <script type="application/x-javascript" src="chrome://messenger/content/editContactOverlay.js"/>
 
 
 <!-- move needed functions into a single js file -->
@@ -339,26 +342,16 @@
                         <treecol id="totalCol" persist="width" flex="1" hidden="true"
                                  label="&totalColumn.label;" tooltiptext="&totalColumn.tooltip;"/>
                         <splitter class="tree-splitter"/>
                         <treecol id="locationCol" persist="width" flex="1" hidden="true"
                                  label="&locationColumn.label;" tooltiptext="&locationColumn.tooltip;"/>
                         <splitter class="tree-splitter"/>
                         <treecol id="idCol" persist="width" flex="1" hidden="true"
                                  label="&idColumn.label;" tooltiptext="&idColumn.tooltip;"/>
-                        <splitter class="tree-splitter"/>
-                        <treecol id="glodaWhyCol" persist="width" flex="1"
-                                 hidden="true" ignoreincolumnpicker="true"
-                                 label="&glodaWhyColumn.label;"
-                                 tooltiptext="&glodaWhyColumn.tooltip;"/>
-                        <splitter class="tree-splitter"/>
-                        <treecol id="glodaScoreCol" persist="width" flex="1"
-                                 hidden="true" ignoreincolumnpicker="true"
-                                 label="&glodaScoreColumn.label;"
-                                 tooltiptext="&glodaScoreColumn.tooltip;"/>
                       </treecols>
                     <treechildren ondraggesture="ThreadPaneOnDragStart(event);"
                                   ondragover="ThreadPaneOnDragOver(event);"
                                   ondrop="ThreadPaneOnDrop(event);"/>
                   </tree>
                  </vbox>
                 </hbox>
                 <!-- extensions may overlay in additional panels; don't assume that there are only 2! -->
--- a/mail/base/content/msgMail3PaneWindow.js
+++ b/mail/base/content/msgMail3PaneWindow.js
@@ -274,18 +274,21 @@ function OnLoadMessenger()
   // Do this before LoadPostAccountWizard since that code selects the first
   //  folder for display, and we want gFolderDisplay setup and ready to handle
   //  that event chain.
   // Also, we definitely need to register the tab type prior to the call to
   //  specialTabs.openSpecialTabsOnStartup below.
   let tabmail = document.getElementById('tabmail');
   if (tabmail)
   {
+    // mailTabType is defined in mailWindowOverlay.js
     tabmail.registerTabType(mailTabType);
-    tabmail.registerTabMonitor(glodaSearchTabMonitor);
+    // glodaFacetTab* in glodaFacetTab.js
+    tabmail.registerTabType(glodaFacetTabType);
+    tabmail.registerTabMonitor(glodaFacetTabMonitor);
     tabmail.registerTabMonitor(QuickSearchTabMonitor);
     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))
--- a/mail/base/content/search.xml
+++ b/mail/base/content/search.xml
@@ -18,16 +18,18 @@
   -
   - 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):
   -   Scott MacGregor <mscott@mozilla.org>
+  -   Andrew Sutherland <asutherland@asutherland.org>
+  -   David Ascher <dascher@mozillamessaging.com>
   -
   - 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
@@ -47,180 +49,308 @@
 <bindings id="SearchBindings"
    xmlns="http://www.mozilla.org/xbl"
    xmlns:html="http://www.w3.org/1999/xhtml"
    xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
    xmlns:xbl="http://www.mozilla.org/xbl">
 
   <!--
     - The glodaSearch binding implements a gloda-backed search mechanism.  The
-    -  actual search logic comes from the glodaSearch tab mode in the
-    -  mailTabType definition.  This binding serves as a means to display and 
-    -  alter the current search query if a "glodaSearch" tab is displayed, or
-    -  enter a search query and spawn a new "glodaSearch" tab if one is
-    -  currently not displayed.  The "glodaFacets" binding also is used to
-    -  display/modify the parameters of the search when on a "glodaSearch" tab.
+    -  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.
     -->
-  <binding id="glodaSearch" extends="chrome://global/content/bindings/textbox.xml#search-textbox">
+  <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" chromedir="&locale.dir;">
+        <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"
+                    label="&searchEverywhere.label;"
+                    type="radio"
+                    oncommand="this.parentNode.parentNode.parentNode.changeMode(this)"/>
+          <xul:menuseparator/>
+        </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.select(); return false;" 
+                         chromedir="&locale.dir;"/>
+                         <!--XXX update search if not global-->
+
+    </content>
     <handlers>
-      <handler event="command"><![CDATA[
-        if (this.value) {
-          let searchString = 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 is going to be saved with the current
-          //  tab when we switch back to it next.
-          if (tabmail.currentTabInfo.mode.name == "glodaSearch")
-            this.value = tabmail.currentTabInfo.searchString;
-          else
-            this.value = "";
-          // open a new tab with our dude
-          tabmail.openTab("glodaSearch", {searchString: searchString,
-              facetString: "everything", locationString: "everywhere"});
+      <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>
+        ]]>
+      </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" action="try {this.doSearch();} catch (e) { logException(e);} return true;"/>
+      <handler event="input">
+        <![CDATA[ 
+          if (!this.value) 
+            this.clearButtonHidden = true;
+          else 
+            this.clearButtonHidden = false;
+        ]]></handler>
     </handlers>
-  </binding>
 
-  <!--
-    - The glodaFacets binding is used to display additional search constraints
-    -  on a "glodaSearch" tab's gloda-backed search.  Because we live in the
-    -  "mailContent" panel reused by the "glodaSearch" tab mode, we are always
-    -  present, even when we should not be displayed (namely for "folder" and
-    -  "message" tab modes).  We leave it up to the mailTabType and glodaSearch
-    -  tab mode to ensure that we are shown/hidden at the right times.  (We
-    -  could do this ourselves as a tabmail tab monitor, but it is more
-    -  intuitive to have our behaviour/relationship made explicit.)
-    -->
-  <binding id="glodaFacets">
-    <resources>
-      <stylesheet src="chrome://messenger/skin/searchBox.css"/>
-    </resources>
-    <content orientation="horizontal" hidden="true">
-      <xul:label control="glodaFacetType" value="&glodaSearchBar.facet.label;"/>
-      <xul:menulist id="glodaFacetType">
-        <xul:menupopup>
-          <xul:menuitem label="&glodaSearchFacet.everything.label;"
-                        value="everything"/>
-          <xul:menuitem label="&glodaSearchFacet.subject.label;"
-                        value="subject"/>
-          <xul:menuitem label="&glodaSearchFacet.involves.label;"
-                        value="involves"/>
-          <xul:menuitem label="&glodaSearchFacet.to.label;"
-                        value="to"/>
-          <xul:menuitem label="&glodaSearchFacet.from.label;"
-                        value="from"/>
-          <xul:menuitem label="&glodaSearchFacet.body.label;"
-                        value="body"/>
-        </xul:menupopup>
-      </xul:menulist>
-      <xul:label control="glodaFacetLocation"
-                 value="&glodaSearchBar.location.label;"/>
-      <xul:menulist id="glodaFacetLocation" anonid="glodaFacetLocation">
-        <xul:menupopup>
-          <xul:menuitem label="&glodaSearchFacet.everywhere.label;" value="everywhere"/>
-          <xul:menuitem anonid="currentFolder" label="" value="currentFolder" hidden="true"/>
-          <xul:menu label="&glodaSearchFacet.folder.label;">
-            <xul:menupopup type="folder"/>
-          </xul:menu>
-        </xul:menupopup>
-      </xul:menulist>
-    </content>
+    <implementation implements="nsIObserver">
+      <constructor><![CDATA[
+        this.build();
+      ]]></constructor>
+      <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');
 
-    <implementation>
-      <constructor>
-        <![CDATA[
-          this._facetTypeNode =
-            document.getAnonymousElementByAttribute(this, "id",
-                                                    "glodaFacetType");
-          this._facetLocationNode =
-            document.getAnonymousElementByAttribute(this, "id",
-                                                    "glodaFacetLocation");
-          this._currentFolderNode =
-            document.getAnonymousElementByAttribute(this, "anonid",
-                                                    "currentFolder");
+          //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);
+        }
         ]]>
-      </constructor>
-      <method name="updateStateFromCurrentTab">
+        </body>
+      </method>
+      <method name="build">
         <body><![CDATA[
-        /**
-         * Update our display state to match the state of the current tab.
-         */
-          let tabmail = document.getElementById("tabmail");
-          let tabInfo = tabMail.currentTabInfo;
+        const Cu = Components.utils;
+        Cu.import("resource://gre/modules/errUtils.js");
+        try {
+          Cu.import("resource://app/modules/StringBundle.js");
+          Cu.import("resource://app/modules/quickSearchManager.js");
+  
+          this.glodaCompleter =
+            Components.classes["@mozilla.org/autocomplete/search;1?name=gloda"].
+                      getService(). //Components.interfaces.nsIAutoCompleteSearch)
+                      wrappedJSObject;
+  
+          var observerSvc = Components.classes["@mozilla.org/observer-service;1"]
+                            .getService(Components.interfaces.nsIObserverService);
+          observerSvc.addObserver(this, "autocomplete-did-enter-text", false);
+  
+          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", String(value));
+            menuitem.setAttribute("label", label);
+            menuitem.setAttribute("type", "radio");
+            menuitem.setAttribute("quicksearch", "true");
+            menuitem.setAttribute("oncommand", "this.parentNode.parentNode.parentNode.changeMode(this)");
+            this.menupopup.appendChild(menuitem);
+          }
+          
+          let separator = document.createElement("menuseparator");
+          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("oncommand",
+                                "gFolderTreeController.newVirtualFolder(this.parentNode.parentNode.parentNode.value,\
+                                gFolderDisplay.view.search.session.searchTerms);");
 
-          this._facetTypeNode.value = tabInfo.facetString;
-          if (typeof(tabInfo.location) == "string")
-            this._facetLocationNode.value = tabInfo.location;
+          this.menupopup.appendChild(saveAsVF);
+          this.updateSaveItem();
+        } catch (e) {
+          logException(e);
+        }
         ]]></body>
       </method>
-      <method name="setLocationFacetToFolder">
+
+      <method name="observe">
+      <parameter name="aSubject"/>
+      <parameter name="aTopic"/>
+      <parameter name="aData"/>
         <body><![CDATA[
-        /**
-         * Update our display state to match the state of the current tab.
-         */
-          let tabmail = document.getElementById("tabmail");
-          let tabInfo = tabMail.currentTabInfo;
+        try {
+          if (aTopic == "autocomplete-did-enter-text") {
+            let selectedIndex = this.autocompletePopup.selectedIndex;
+            let row = this.glodaCompleter.curResult.getObjectAt(selectedIndex);
+            if (row == null) {
+              this.applyConstraints();
+              return;
+            }
+            let theQuery = Gloda.newQuery(Gloda.NOUN_MESSAGE);
+            let tabmail = document.getElementById("tabmail");
+            if (row.fullText) {
+              tabmail.openTab("glodaFacet", {
+                searcher: new GlodaMsgSearcher(null, row.item, row.andTerms)
+              });
+            } else {
+              if (row.nounDef.name == "tag") {
+                theQuery = theQuery.tags(row.item);
+              } else if (row.nounDef.name == "identity") {
+                theQuery = theQuery.involves(row.item); 
+              }
+              tabmail.openTab("glodaFacet", {
+                query: theQuery
+              });
+            }
 
-          this._facetTypeNode.value = tabInfo.facetString;
-          if (typeof(tabInfo.location) == "string")
-            this._facetLocationNode.value = tabInfo.location;
+            this.applyConstraints();
+          }
+        } catch (e) {
+          logException(e);
+        }
         ]]></body>
+      </method>
 
+      <method name="applyConstraints">
+        <body><![CDATA[
+          ]]>
+        </body>
       </method>
-    </implementation>
+
+      <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>
+
 
-    <handlers>
-      <handler event="command" phase="bubble"><![CDATA[
-        // Have widgetNode be our immediate child, with nodes being a list of
-        //  the descendent nodes between eventNode and the actual target.
-        // This allows us to know which of our widgets actually got clicked on,
-        //  plus makes subsequent processing easier.  (Alternatively, we could
-        //  register a command listener on each of our widgets, but that is
-        //  arguably just as ugly.) 
-        let nodes = [];
-        let widgetNode = event.originalTarget;
-        while (widgetNode.parentNode != this) {
-          nodes.unshift(widgetNode);
-          widgetNode = widgetNode.parentNode;
-        }
-
-        // -- Type Facet
-        if (widgetNode == this._facetTypeNode) {
-
+      <method name="doSearch">
+        <body><![CDATA[
+      try {
+        dump("doing search, value = " + this.value + "\n");
+        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 is 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")
+              this.value = tabmail.currentTabInfo.searchString;
+            else
+              this.value = "";
+            // open a new tab with our dude
+            tabmail.openTab("glodaFacet", {
+              searcher: new GlodaMsgSearcher(null, searchString)
+            });
+          }
+        } else { // quick search
+          if (! this.value)
+            gFolderDisplay.view.search.userTerms = null
+          else
+            gFolderDisplay.view.search.quickSearch(Number(this.searchMode), this.value);
         }
-        // -- Location Facet
-        else if (widgetNode == this._facetLocationNode) {
-          if (event.originalTarget._folder) {
-            let folder = event.originalTarget._folder;
-            this._currentFolderNode.label =
-              [node._folder.prettiestName
-               for each ([, node] in Iterator(nodes))
-               if (node._folder)].join("/");
-            this._currentFolderNode.hidden = false;
-            this._facetLocationNode.selectedItem = this._currentFolderNode;
-          }
-        }
+      } catch (e) {
+        logException(e);
+      }
+          ]]>
+        </body>
+      </method>
 
-        dump("Selected folder: " + event.originalTarget._folder + "\n");
-        dump("event: " + event + "\n");
-        dump("target: " + event.originalTarget + "\n");
-        dump("event target id: " + event.originalTarget.id + "\n");
-        dump("event tag: " + event.originalTarget.tagName + "\n");
-      ]]></handler>
-    </handlers>
+      <method name="changeMode">
+      <parameter name="aMenuItem" />
+        <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();
+        ]]></body>
+      </method>
+      <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" onget="return this.mQuickSearchMode;"
+                onset="this.mQuickSearchMode = val;
+                       dump('val = ' + val + ' \n');
+                       dump('typeof val = ' + typeof(val) + ' \n');
+                       this.menupopup.setAttribute('value', val);
+                       this.updateEmptyText(); "/>
 
+      <property name="autocompletePopup" readonly="true" onget="return document.getElementById(this.getAttribute('autocompletepopup'))"/>
+
+
+      <property name="showingSearchCriteria" onget="return this.getAttribute('searchCriteria') == 'true';"
+                onset="this.setAttribute('searchCriteria', val); return val;"/>
+      <property name="menupopup" readonly="true" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'quick-search-menupopup')"/>
+      <property name="saveAsVirtualFolder" readonly="true" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'quick-search-save-as-virtual-folder')"/>
+      <property name="clearButtonHidden" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'quick-search-clearbutton').getAttribute('clearButtonHidden') == 'true';"
+                onset="document.getAnonymousElementByAttribute(this, 'anonid', 'quick-search-clearbutton').setAttribute('clearButtonHidden', val); return val;"/>
+
+    </implementation>
   </binding>
 
-
   <binding id="searchbar" extends="chrome://global/content/bindings/textbox.xml#timed-textbox">
     <resources>
       <stylesheet src="chrome://messenger/skin/searchBox.css"/>
     </resources>
     <content> 
       <children/>
       <xul:hbox class="quick-search-textbox textbox-input-box" flex="1">
         <html:input class="textbox-input" flex="1" anonid="input" allowevents="true"
@@ -253,27 +383,21 @@
             // Error condition, something went wrong - try and recover from it.
             this.mQuickSearchMode =
               QuickSearchConstants.kQuickSearchFromOrSubject.toString();
 
             selectedMenuItem =
               qsmenu.getElementsByAttribute('value', this.searchMode)[0];
           }
           selectedMenuItem.setAttribute('checked', 'true');
-
-          this.setSearchCriteriaText();
         ]]>
       </constructor>
-
       <property name="showingSearchCriteria" onget="return this.getAttribute('searchCriteria') == 'true';"
                 onset="this.setAttribute('searchCriteria', val); return val;"/>
 
-      <property name="clearButtonHidden" onget="return document.getElementById('quick-search-clearbutton').getAttribute('clearButtonHidden') == 'true';"
-                onset="document.getElementById('quick-search-clearbutton').setAttribute('clearButtonHidden', val); return val;"/>
-
       <field name="mQuickSearchMode">null</field>
 
       // DND Observer
       <field name="searchInputDNDObserver" readonly="true"><![CDATA[
       ({
         inputSearch: this,
 
         onDrop: function (aEvent, aXferData, aDragSession)
@@ -295,40 +419,45 @@
       })
       ]]></field>
 
       <property name="searchMode" onget="return this.mQuickSearchMode;"
                 onset="this.mQuickSearchMode = val; document.getElementById('quick-search-menupopup').setAttribute('value', val);"/>
 
       <method name="setSearchCriteriaText">
         <body><![CDATA[
+    try {
           this.showingSearchCriteria = true;
 
           let qsmenu = document.getElementById('quick-search-menupopup');
 
           // extract the label value from the menu item
           let menuItem = qsmenu.getElementsByAttribute('value',
                                                        this.searchMode)[0];
-
+          logElement(menuItem);
           if (typeof menuItem == "undefined") {
             // Error condition, something went wrong - try and recover from it.
             this.mQuickSearchMode =
               QuickSearchConstants.kQuickSearchFromOrSubject.toString();
 
             let selectedMenuItem =
               qsmenu.getElementsByAttribute('value', this.searchMode)[0];
 
             selectedMenuItem.setAttribute('checked', 'true');
 
             this.inputField.value = selectedMenuItem.getAttribute('label');
           }
-          else
+          else {
             this.inputField.value = menuItem.getAttribute('label');
+          }
 
          this.clearButtonHidden = true;
+    } catch (e) {
+      logException(e);
+    }
         ]]></body>
       </method>
 
       <method name="openmenupopup">
         <body>
           <![CDATA[
             document.getElementById('quick-search-menupopup').click();
             return false;
--- a/mail/base/content/searchBar.js
+++ b/mail/base/content/searchBar.js
@@ -18,16 +18,18 @@
  * 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>
+ *   David Ascher <dascher@mozillamessaging.com>
  *
  * 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
@@ -35,224 +37,63 @@
  * 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://app/modules/quickSearchManager.js");
 
-var gSearchBundle;
-var gStatusBar = null;
-var gIgnoreFocus = false;
-var gIgnoreClick = false;
 
-// change/add constants in QuickSearchConstants in quickSearchManager.js first
-// ideally these should go away in favor of everyone using QuickSearchConstants
-const kQuickSearchSubject = QuickSearchConstants.kQuickSearchSubject;
-const kQuickSearchFrom = QuickSearchConstants.kQuickSearchFrom;
-const kQuickSearchFromOrSubject =
-  QuickSearchConstants.kQuickSearchFromOrSubject;
-const kQuickSearchBody = QuickSearchConstants.kQuickSearchBody;
-const kQuickSearchRecipient = QuickSearchConstants.kQuickSearchRecipient;
-const kQuickSearchRecipientOrSubject =
-  QuickSearchConstants.kQuickSearchRecipientOrSubject;
+var gSearchBundle;
 
+var gStatusBar = null;
 
 /**
  * We are exclusively concerned with disabling the quick-search box when a
  *  tab is being displayed that lacks quick search abilities.
  */
+
 var QuickSearchTabMonitor = {
   onTabTitleChanged: function() {
   },
 
   onTabSwitched: function (aTab, aOldTab) {
     let searchInput = document.getElementById("searchInput");
 
     if (searchInput) {
-      let newTabEligible = aTab.mode.tabType == mailTabType;
+      let newTabEligible = ((aTab.mode.tabType == mailTabType) ||
+                            (aTab.mode.tabType == glodaFacetTabType));
       searchInput.disabled = !newTabEligible;
       if (!newTabEligible)
         searchInput.value = "";
     }
-  },
+  }
 };
 
 
+// XXX never called?
 function SetQSStatusText(aNumHits)
 {
   var statusMsg;
+  gSearchBundle = document.getElementById("bundle_search");
   // if there are no hits, it means no matches were found in the search.
   if (aNumHits == 0)
     statusMsg = gSearchBundle.getString("searchFailureMessage");
   else
   {
     if (aNumHits == 1)
       statusMsg = gSearchBundle.getString("searchSuccessMessage");
     else
       statusMsg = gSearchBundle.getFormattedString("searchSuccessMessages", [aNumHits]);
   }
 
   statusFeedback.showStatusString(statusMsg);
 }
 
-function getDocumentElements()
-{
-  gSearchBundle = document.getElementById("bundle_search");
-  gStatusBar = document.getElementById('statusbar-icon');
-  GetSearchInput();
-}
-
-function onEnterInSearchBar()
-{
-  if (!gSearchInput)
-    return;
-
-  // nothing changes while showing the criteria
-  if (gSearchInput.showingSearchCriteria)
-    return;
-
-  if (!gSearchInput || gSearchInput.value == "")
-     gFolderDisplay.view.search.userTerms = null;
-  else
-     gFolderDisplay.view.search.quickSearch(gSearchInput.searchMode,
-                                            gSearchInput.value);
-}
-
-
-function onSearchKeyPress()
-{
-  if (gSearchInput.showingSearchCriteria)
-    gSearchInput.showingSearchCriteria = false;
-}
-
-function onSearchInputFocus(event)
-{
-  GetSearchInput();
-  // search bar has focus, ...clear the showing search criteria flag
-  if (gSearchInput.showingSearchCriteria)
-  {
-    gSearchInput.value = "";
-    gSearchInput.showingSearchCriteria = false;
-  }
-
-  if (gIgnoreFocus) // got focus via mouse click, don't need to anything else
-    gIgnoreFocus = false;
-  else
-    gSearchInput.select();
-}
-
-function onSearchInputMousedown(event)
-{
-  GetSearchInput();
-  if (gSearchInput.hasAttribute("focused"))
-    // If the search input is focused already, ignore the click so that
-    // onSearchInputBlur does nothing.
-    gIgnoreClick = true;
-  else
-  {
-    gIgnoreFocus = true;
-    gIgnoreClick = false;
-  }
-}
-
-function onSearchInputClick(event)
-{
-  if (!gIgnoreClick)
-    // Triggers onSearchInputBlur(), but focus returns to field.
-    gSearchInput.select();
-}
-
-function onSearchInputBlur(event)
-{
-  // If we're doing something else, don't process the blur.
-  if (gIgnoreClick)
-    return;
-
-  if (!gSearchInput.value)
-    gSearchInput.showingSearchCriteria = true;
-
-  if (gSearchInput.showingSearchCriteria)
-    gSearchInput.setSearchCriteriaText();
-}
-
-function onClearSearch()
-{
-  // If we're not showing search criteria, then we need to clear up.
-  if (!gSearchInput.showingSearchCriteria)
-  {
-    Search("");
-    // Hide the clear button
-    gSearchInput.clearButtonHidden = true;
-    gIgnoreClick = true;
-    gSearchInput.select();
-    gIgnoreClick = false;
-  }
-}
-
-// called from commandglue.js in cases where the view is being changed and QS
-// needs to be cleared.
-function ClearQSIfNecessary()
-{
-  if (!gSearchInput || gSearchInput.showingSearchCriteria)
-    return;
-  gSearchInput.setSearchCriteriaText();
-}
-
-function Search(str)
-{
-  if (gSearchInput.showingSearchCriteria && str != "")
-    return;
-
-  gSearchInput.value = str;  //on input does not get fired for some reason
-  onEnterInSearchBar();
-}
-
-// helper methods for the quick search drop down menu
-function changeQuickSearchMode(aMenuItem)
-{
-  // extract the label and set the search input to match it
-  var oldSearchMode = gSearchInput.searchMode;
-  gSearchInput.searchMode = aMenuItem.value;
-
-  if (gSearchInput.value == "" || gSearchInput.showingSearchCriteria)
-  {
-    gSearchInput.showingSearchCriteria = true;
-    if (gSearchInput.value) //
-      gSearchInput.setSearchCriteriaText();
-  }
-
-  // if the search box is empty, set showing search criteria to true so it shows up when focus moves out of the box
-  if (!gSearchInput.value)
-    gSearchInput.showingSearchCriteria = true;
-  else if (gSearchInput.showingSearchCriteria) // if we are showing criteria text and the box isn't empty, change the criteria text
-    gSearchInput.setSearchCriteriaText();
-  else if (oldSearchMode != gSearchInput.searchMode) // the search mode just changed so we need to redo the quick search
-    onEnterInSearchBar();
-}
-
-function saveViewAsVirtualFolder()
-{
-  gFolderTreeController.newVirtualFolder(gSearchInput.value,
-                                         gFolderDisplay.view.search.session.searchTerms);
-}
-
-function InitQuickSearchPopup()
-{
-  // 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
-
-  GetSearchInput();
-  if (!gSearchInput ||gSearchInput.value == "" || gSearchInput.showingSearchCriteria)
-    document.getElementById('quickSearchSaveAsVirtualFolder').setAttribute('disabled', 'true');
-  else
-    document.getElementById('quickSearchSaveAsVirtualFolder').removeAttribute('disabled');
-}
 
 /**
  * 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
@@ -267,29 +108,29 @@ function onSearchFolderTypeChanged(isOut
 
   GetSearchInput();
 
   if (!gSearchInput)
     return;
 
   if (isOutboundFolder)
   {
-    if (gSearchInput.searchMode == kQuickSearchFromOrSubject)
-      newSearchType = kQuickSearchRecipientOrSubject;
-    else if (gSearchInput.searchMode == kQuickSearchFrom)
-      newSearchType = kQuickSearchRecipient;
+    if (gSearchInput.searchMode == QuickSearchConstants.kQuickSearchFromOrSubject)
+      newSearchType = QuickSearchConstants.kQuickSearchRecipientOrSubject;
+    else if (gSearchInput.searchMode == QuickSearchConstants.kQuickSearchFrom)
+      newSearchType = QuickSearchConstants.kQuickSearchRecipient;
     else
       return;
   }
   else
   {
-    if (gSearchInput.searchMode == kQuickSearchRecipientOrSubject)
-      newSearchType = kQuickSearchFromOrSubject;
-    else if (gSearchInput.searchMode == kQuickSearchRecipient)
-      newSearchType = kQuickSearchFrom;
+    if (gSearchInput.searchMode == QuickSearchConstants.kQuickSearchRecipientOrSubject)
+      newSearchType = QuickSearchConstants.kQuickSearchFromOrSubject;
+    else if (gSearchInput.searchMode == QuickSearchConstants.kQuickSearchRecipient)
+      newSearchType = QuickSearchConstants.kQuickSearchFrom;
     else
       return;
   }
   var newMenuItem = quickSearchMenu.getElementsByAttribute('value', newSearchType).item(0);
   if (newMenuItem)
   {
     // If a menu item is already checked, need to uncheck it first:
     var checked = quickSearchMenu.getElementsByAttribute('checked', 'true').item(0);
--- a/mail/base/content/tabmail.xml
+++ b/mail/base/content/tabmail.xml
@@ -412,16 +412,19 @@
           }
         ]]></body>
       </method>
       <method name="openTab">
         <parameter name="aTabModeName"/>
         <parameter name="aArgs"/>
         <body>
             <![CDATA[
+        try {
+          if (!(aTabModeName in this.tabModes))
+            throw new Error("No such tab mode: " + aTabModeName);
           let tabMode = this.tabModes[aTabModeName];
           // if we are already at our limit for this mode, show an existing one
           if (tabMode.tabs.length == tabMode.maxTabs) {
             let desiredTab = tabMode.tabs[0];
             this.tabContainer.selectedIndex = this.tabInfo.indexOf(desiredTab);
             return;
           }
 
@@ -518,16 +521,17 @@
           t.setAttribute('type', tab.mode.type);
 
           if (!background)
             // Update the toolbar status - we don't need to do menus as they
             // do themselves when we open them.
             UpdateMailToolbar("tabmail");
 
           return tab;
+          } catch (e) { logException(e);}
         ]]></body>
       </method>
       <method name="selectTabByMode">
         <parameter name="aTabModeName"/>
         <body><![CDATA[
           let tabMode = this.tabModes[aTabModeName];
           if (tabMode.tabs.length) {
             let desiredTab = tabMode.tabs[0];
--- a/mail/base/jar.mn
+++ b/mail/base/jar.mn
@@ -55,17 +55,17 @@ messenger.jar:
 *   content/messenger/msgPrintEngine.xul            (content/msgPrintEngine.xul)
     content/messenger/searchBar.js                  (content/searchBar.js)
     content/messenger/phishingDetector.js           (content/phishingDetector.js)
 *   content/messenger/mail-offline.js               (content/mail-offline.js)
     content/messenger/about-footer.png              (content/about-footer.png)
     content/messenger/aboutDialog.css               (content/aboutDialog.css)
 *   content/messenger/credits.xhtml                 (content/credits.xhtml)
     content/messenger/messenger.css                 (content/messenger.css)
-*   content/messenger/search.xml                    (content/search.xml)
+    content/messenger/search.xml                    (content/search.xml)
     content/messenger/tabmail.xml                   (content/tabmail.xml)
     content/messenger/tabmail.css                   (content/tabmail.css)
 *   content/messenger/newmailalert.xul              (content/newmailalert.xul)
     content/messenger/newmailalert.js               (content/newmailalert.js)
     content/messenger/newTagDialog.xul              (content/newTagDialog.xul)
     content/messenger/newTagDialog.js               (content/newTagDialog.js)
 *   content/messenger/viewSourceOverlay.xul         (content/viewSourceOverlay.xul)
 *   content/messenger/configEditorOverlay.xul       (content/configEditorOverlay.xul)
@@ -75,15 +75,23 @@ messenger.jar:
 #ifdef XP_MACOSX
     content/messenger/macMenuOverlay.xul            (content/macMenuOverlay.xul)
 #endif
 *   content/messenger/baseMenuOverlay.xul           (content/baseMenuOverlay.xul)
     content/messenger/selectionsummaries.js         (content/selectionsummaries.js)
     content/messenger/multimessageview.css          (content/multimessageview.css)
     content/messenger/sharedsummary.css             (content/sharedsummary.css)
     content/messenger/multimessageview.xhtml        (content/multimessageview.xhtml)
+    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)
 
 comm.jar:
 % content communicator %content/communicator/ xpcnativewrappers=yes
 *  content/communicator/contentAreaClick.js         (content/contentAreaClick.js)
 *  content/communicator/utilityOverlay.xul          (content/utilityOverlay.xul)
 *  content/communicator/utilityOverlay.js           (content/utilityOverlay.js)
 *  content/communicator/nsContextMenu.js            (content/nsContextMenu.js)
new file mode 100644
--- /dev/null
+++ b/mail/locales/en-US/chrome/messenger/gloda.properties
@@ -0,0 +1,151 @@
+# ***** 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 Global Database
+#
+# 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>
+#
+# 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 *****
+
+# LOCALIZATION NOTE (*.facetLabel): These are the labels used to label the facet
+#  displays in the global search facet display mechanism.  Like thread pane
+#  column headers these should be relatively short but can take advantage of
+#  attributes with longer displays to be slightly longer.  For example, the
+#  conversation attribute is larger than most attributes, so can have a slightly
+#  longer label.  You can and should use the facetTooltip to provide a better
+#  and longer explanation of what the facet attribute is.
+
+# LOCALIZATION NOTE (*.facetTooltip): These are the tooltips that will be
+#  displayed if you hover over the facet's label.  These should be longer than
+#  the facetLabel and reduce confusion about what the facet is, but do not
+#  need to be a book on the subject.
+
+
+# LOCALIZATION NOTE (gloda.message.attr.folder.*): Stores the message folder in
+#  which the message is stored.
+gloda.message.attr.folder.facetLabel=Mail Folder
+gloda.message.attr.folder.facetTooltip=The folder where the message is stored.
+
+# LOCALIZATION NOTE (gloda.message.attr.conversation.*): Stores the conversation
+#  the message belongs to.  The conversation is currently expressed using the
+#  subject of the message that initiated it.
+gloda.message.attr.conversation.facetLabel=Conversation Subject
+gloda.message.attr.conversation.facetTooltip=The subject of the conversation the message belongs to.
+
+# LOCALIZATION NOTE (gloda.message.attr.fromMe.*): Stores everyone involved
+#  with the message.  This means from/to/cc/bcc.
+gloda.message.attr.fromMe.facetLabel=From Me
+gloda.message.attr.fromMe.facetTooltip=Messages where I was the author.
+
+# LOCALIZATION NOTE (gloda.message.attr.toMe.*): Stores everyone involved
+#  with the message.  This means from/to/cc/bcc.
+gloda.message.attr.toMe.facetLabel=To Me
+gloda.message.attr.toMe.facetTooltip=Messages where I was on the To or Cc lines.
+
+
+# LOCALIZATION NOTE (gloda.message.attr.involves.*): Stores everyone involved
+#  with the message.  This means from/to/cc/bcc.
+gloda.message.attr.involves.facetLabel=People
+gloda.message.attr.involves.facetTooltip=People explicitly involved in the message by being listed on the From, To, Cc, or Bcc lines.
+
+# LOCALIZATION NOTE (gloda.message.attr.date.*): Stores the date of the message.
+#  Thunderbird normally stores the date the message claims it was composed
+#  according to the "Date" header.  This is not the same as when the message
+#  was sent or when it was eventually received by the user.  In the future we
+#  may change this to be one of the other dates, but not anytime soon.
+gloda.message.attr.date.facetLabel=Date
+gloda.message.attr.date.facetTooltip=The date the message claims it was authored.
+
+# LOCALIZATION NOTE (gloda.message.attr.attachmentTypes.*): Stores the list of
+#  MIME types (ex: image/png, text/plain) of real attachments (not just part of
+#  the message content but explicitly named attachments) on the message.
+#  Although we hope to be able to provide localized human-readable explanations
+#  of the MIME type (ex: "PowerPoint document"), I don't know if that is going
+#  to happen.
+gloda.message.attr.attachmentTypes.facetLabel=Attachments
+gloda.message.attr.attachmentTypes.facetTooltip=The type of attachments found on the message, if any.
+
+# LOCALIZATION NOTE (gloda.message.attr.mailing-list.*): Stores the mailing
+#  lists detected in the message.  This will normally be the e-mail address of
+#  the mailing list and only be detected in messages received from the mailing
+#  list.  Extensions may contribute additional detected mailing-list-like
+#  things.
+gloda.message.attr.mailing-list.facetLabel=Mail List Involved
+gloda.message.attr.mailing-list.facetTooltip=If the message was sent via a mailing list, the e-mail address associated with the mailing list.
+
+# LOCALIZATION NOTE (gloda.message.attr.tag.*): Stores the tags applied to the
+#  message.  Notably, gmail's labels are not currently exposed via IMAP and we
+#  do not do anything clever with gmail, so this is indepdendent of gmail
+#  labels.  This may change in the future, but it's a safe bet it's not
+#  happening on Thunderbird's side prior to 3.0.
+gloda.message.attr.tag.facetLabel=Tags
+gloda.message.attr.tag.facetTooltip=Tags applied to the message.
+
+# LOCALIZATION NOTE (gloda.message.attr.tag.*): Stores whether the message is
+#  starred or not, as indicated by a pretty star icon.  In the past, the icon
+#  used to be a flag.  The IMAP terminology continues to be "flagged".
+gloda.message.attr.star.facetLabel=Starred
+gloda.message.attr.star.facetTooltip=Is the message starred / flagged?
+
+# LOCALIZATION NOTE (gloda.message.attr.read.*): Stores whether the user has
+#  read the message or not.
+gloda.message.attr.read.facetLabel=Read
+gloda.message.attr.read.facetTooltip=Is the message marked read, or is it unread?
+
+# LOCALIZATION NOTE (gloda.message.attr.repliedTo.*): Stores whether we believe
+#  the user has ever replied to the message.  We normally show a little icon in
+#  the thread pane when this is the case.
+gloda.message.attr.repliedTo.facetLabel=Replied To
+gloda.message.attr.repliedTo.facetTooltip=Has this message been replied to?
+
+# LOCALIZATION NOTE (gloda.message.attr.forwarded.*): Stores whether we believe
+#  the user has ever forwarded the message.  We normally show a little icon in
+#  the thread pane when this is the case.
+gloda.message.attr.forwarded.facetLabel=Forwarded
+gloda.message.attr.forwarded.facetTooltip=Has this message been forwarded to anyone?
+
+# LOCALIZATION NOTE (gloda.mimetype.category.*.label): Map categories of MIME
+#  types defined in mimeTypeCategories.js to labels.
+# LOCALIZATION NOTE (gloda.mimetype.category.archives.label): Archive is
+#  referring to things like zip files, tar files, tar.gz files, etc.
+gloda.mimetype.category.archives.label=Archives
+gloda.mimetype.category.documents.label=Documents
+gloda.mimetype.category.images.label=Images
+# LOCALIZATION NOTE (gloda.mimetype.category.media.label): Media is meant to
+#  encompass both audio and video.  This is because video and audio streams are
+#  frequently stored in the same type of container and we cannot rely on the
+#  sending e-mail client to have been clever enough to figure out what was
+#  really in the file.  So we group them together.
+gloda.mimetype.category.media.label=Media (Audio, Video)
+gloda.mimetype.category.pdf.label=PDF Files
+# LOCALIZATION NOTE (gloda.mimetype.category.other.label): Other is the category
+#  for MIME types that we don't really know what it is.
+gloda.mimetype.category.other.label=Other
new file mode 100644
--- /dev/null
+++ b/mail/locales/en-US/chrome/messenger/glodaFacetView.dtd
@@ -0,0 +1,4 @@
+<!-- LOCALIZATION NOTE (glodaFacetView.filters.label): Label at the top of the
+     faceting sidebar.  Serves as a header both for the checkboxes under it as
+     well for labeled facets with multiple options. -->
+<!ENTITY glodaFacetView.filters.label "Filters">
new file mode 100644
--- /dev/null
+++ b/mail/locales/en-US/chrome/messenger/glodaFacetView.properties
@@ -0,0 +1,115 @@
+# ***** 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 Global Database
+#
+# 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>
+#
+# 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 *****
+
+# LOCALIZATION NOTE (glodaFacetView.tab.query.label): The title to display for
+#  tabs that are based on a gloda (global database) query or collection rather
+#  than a user search.  In the case of a user search, we just display the
+#  search string they entered.  At some point we might try and explain what
+#  the query/collection is an automatic fashion, but not today.
+glodaFacetView.tab.query.label=Search
+
+# LOCALIZATION NOTE(glodaFacetView.constraints.query.fulltext.label):
+#  The label to display to describe when our base query was a fulltext search
+#  across messages.  The value is displayed following the label.
+glodaFacetView.constraints.query.fulltext.label=Searching for #1
+glodaFacetView.constraints.query.fulltext.andJoinWord=and
+glodaFacetView.constraints.query.fulltext.orJoinWord=or
+glodaFacetView.constraints.query.fulltext.changeToAndLabel=require all terms instead
+glodaFacetView.constraints.query.fulltext.changeToOrLabel=require any of the terms instead
+
+# LOCALIZATION NOTE(glodaFacetView.constraints.query.initial):
+#  The label to display to describe when our base query is not a full-text
+#  search.  Additional labels are appended describing each constraint.
+glodaFacetView.constraints.query.initial=Searching for messages
+
+# LOCALIZATION NOTE(glodaFacetView.constraints.query.involves.label):
+#  The label to display to describe when our base query was on messages
+#  involving a given contact from the address book.  The value is displayed
+#  where the #1 is.
+glodaFacetView.constraints.query.involves.label=involving #1
+
+# LOCALIZATION NOTE(glodaFacetView.constraints.query.contact.label):
+#  The label to display to describe when our base query was on messages
+#  tagged with a specific tag.  The tag is displayed following the label.
+glodaFacetView.constraints.query.tagged.label=tagged:
+
+
+# LOCALIZATION NOTE (glodaFacetView.facets.mode.top.otherLabel): The label to
+#  use for the "other" category where we lump everything that is not one of the
+#  top groups.  Includes an argument which is the number of groups that are
+#  collapsed into this group.
+glodaFacetView.facets.mode.top.otherLabel=Other (%S)
+
+# LOCALIZATION NOTE (glodaFacetView.facets.noneLabel): The text to display when
+#  a facet needs to indicate that an attribute omitted a value or was otherwise
+#  empty.
+glodaFacetView.facets.noneLabel=None
+
+# LOCALIZATION NOTE (glodaFacetView.facets.filter.attachmentTypes.allLabel):
+#  The label to use when all types of attachments are being displayed.
+glodaFacetView.facets.filter.attachmentTypes.allLabel=Any Kind
+
+# LOCALIZATION NOTE (glodaFacetView.result.message.writesLabel): Used in the
+#  faceted search message display to delinate the author of a message and the
+#  recipients.  An example usage is  "Alice writes Bob, Chuck, Don".
+glodaFacetView.result.message.writesLabel=writes
+
+# LOCALIZATION NOTE(glodaFacetView.results.message.countLabel): Displays the
+#  number of messages displayed in the result area out of the number of
+#  messages in the active set (the set of messages remaining after the
+#  application of the facet constraints.)  It takes the following arguments,
+#  you do not have to use all of them.
+# #1: The number of messages displayed in the result area.
+# #2: The pluralized form of "messages" (or whatever you provide in
+#      glodaFacetView.results.message.countLabelMessagePlurals) as it
+#      applies to #1 (the number of messages in the result area.)
+# #3: The number of messages in the active set.
+# #4: The pluralized form of "messages" as it applies to #3.
+glodaFacetView.results.message.countLabel=Top #1 #2 out of #3
+# LOCALIZATION NOTE(glodaFacetView.results.message.countLabelMessagePlurals):
+#  The plural forms of "messages" or whatever you choose.  See
+#  https://developer.mozilla.org/en/Localization_and_Plurals for details on
+#  how this stuff works.
+glodaFacetView.results.message.countLabelMessagePlurals=message;messages
+# LOCALIZATION NOTE(glodaFacetView.results.message.showAllInList.label): The
+#  label for the button/link that causes us to display all of the messages in
+#  the active set in a new thread pane display tab, closing the current faceting
+#  tab.
+glodaFacetView.results.message.showAllInList.label=Show all as list
+# LOCALIZATION NOTE(glodaFacetView.results.message.showAllInList.tooltip): The
+#  tooltip to display when hovering over the showAllInList label.
+glodaFacetView.results.message.showAllInList.tooltip=Show all of the messages in the active set in a new tab, closing this tab.
--- a/mail/locales/en-US/chrome/messenger/messenger.dtd
+++ b/mail/locales/en-US/chrome/messenger/messenger.dtd
@@ -561,38 +561,16 @@ you can use these alternative items. Oth
 <!ENTITY folderContextSettings.accesskey "e">
 
 <!-- Search Bar -->
 <!ENTITY SearchNameOrEmail.label "Name or Email contains:">
 <!ENTITY SearchNameOrEmail.accesskey "N">
 
 <!-- Gloda Search Bar -->
 <!ENTITY glodaSearchBar.emptyText "Search messages…">
-<!ENTITY glodaSearchBar.facet.label "Search:">
-<!-- LOCALIZATION NOTE (glodaSearchFacet.*) labels specify search constraints.
-       "everything" searches over subject, involves, body, and attachments.
-       "subject" searches only messages subjects.
-       "involves" searches only message to/from/cc/bcc.
-       "to" searches only message to/cc.
-       "from" searches only message using "from" (the message author).
-       "body" searches only message bodies, which does not include message
-           attachment names or the content of the attachments.  Message body
-           basically means a message part with a content-type of text/*.
-    -->
-<!ENTITY glodaSearchFacet.everything.label "Everything">
-<!ENTITY glodaSearchFacet.subject.label "Subject">
-<!ENTITY glodaSearchFacet.involves.label "Involves (from,to,cc,bcc)">
-<!ENTITY glodaSearchFacet.to.label "To, CC">
-<!ENTITY glodaSearchFacet.from.label "From">
-<!ENTITY glodaSearchFacet.body.label "Message Text">
-<!ENTITY glodaSearchFacet.attachmentNames.label "Attachment Names">
-
-<!ENTITY glodaSearchBar.location.label "Location:">
-<!ENTITY glodaSearchFacet.everywhere.label "All Folders">
-<!ENTITY glodaSearchFacet.folder.label "In a specific folder…">
 
 <!-- Quick Search Menu Bar -->
 <!ENTITY searchSubjectMenu.label "Subject">
 <!ENTITY searchFromMenu.label "From">
 <!ENTITY searchSubjectOrFromMenu.label "Subject or From">
 <!ENTITY searchRecipient.label "To or Cc">
 <!ENTITY searchSubjectOrRecipientMenu.label "Subject, To or Cc">
 <!ENTITY searchMessageBody.label "Entire Message">
@@ -613,25 +591,16 @@ you can use these alternative items. Oth
 <!ENTITY unreadColumn.label "Unread">
 <!ENTITY totalColumn.label "Total">
 <!ENTITY readColumn.label "Read">
 <!ENTITY receivedColumn.label "Received">
 <!ENTITY starredColumn.label "Starred">
 <!ENTITY locationColumn.label "Location">
 <!ENTITY idColumn.label "Order Received">
 <!ENTITY attachmentColumn.label "Attachments">
-<!-- LOCALIZATION NOTE (glodaWhyColumn.label): explains why a message is
-     present in the gloda search results.  The values can be found in
-     messenger.properties with a prefix of "glodaSearch_results_why_". -->
-<!ENTITY glodaWhyColumn.label "Why">
-<!-- LOCALIZATION NOTE (glodaScoreColumn.label): provides the numerical
-     score assigned to the message as a result of the search.  The column
-     primarily exists to be sorted by, and its contents will probably have
-     little meaning for most users. -->
-<!ENTITY glodaScoreColumn.label "Score">
 
 <!-- Thread Pane Tooltips -->
 <!ENTITY columnChooser.tooltip "Click to select columns to display">
 <!ENTITY threadColumn.tooltip "Click to display message threads">
 <!ENTITY fromColumn.tooltip "Click to sort by from">
 <!ENTITY recipientColumn.tooltip "Click to sort by recipient">
 <!ENTITY subjectColumn.tooltip "Click to sort by subject">
 <!ENTITY dateColumn.tooltip "Click to sort by date">
@@ -644,18 +613,16 @@ you can use these alternative items. Oth
 <!ENTITY unreadColumn.tooltip "Number of unread messages in thread">
 <!ENTITY totalColumn.tooltip "Total number of messages in thread">
 <!ENTITY readColumn.tooltip "Click to sort by read">
 <!ENTITY receivedColumn.tooltip "Click to sort by date received">
 <!ENTITY starredColumn.tooltip "Click to sort by star">
 <!ENTITY locationColumn.tooltip "Click to sort by location">
 <!ENTITY idColumn.tooltip "Click to sort by order received">
 <!ENTITY attachmentColumn.tooltip "Click to sort by attachments">
-<!ENTITY glodaWhyColumn.tooltip "Click to sort by why the message is in your search results">
-<!ENTITY glodaScoreColumn.tooltip "Click to sort by the search score">
 
 <!-- Thread Pane Context Menu -->
 <!ENTITY contextOpenNewWindow.label "Open Message in New Window">
 <!ENTITY contextOpenNewWindow.accesskey "W">
 <!ENTITY contextOpenNewTab.label "Open Message in New Tab">
 <!ENTITY contextOpenNewTab.accesskey "T">
 <!ENTITY contextEditAsNew.label "Edit As New…">
 <!ENTITY contextEditAsNew.accesskey "E">
@@ -698,16 +665,17 @@ you can use these alternative items. Oth
 <!ENTITY removePhishingBarButton1.label "Ignore Warning">
 <!ENTITY reportPhishingError1.label "This message doesn't appear to be a scam.">
 
 <!-- Message Header Button Box (to show hidden email addresses) -->
 <!ENTITY more.label "more">
 
 <!-- Quick Search Bar -->
 <!ENTITY quickSearchCmd.key "k">
+<!ENTITY searchEverywhere.label "Search everywhere">
 
 <!-- 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">
--- a/mail/locales/en-US/chrome/messenger/messenger.properties
+++ b/mail/locales/en-US/chrome/messenger/messenger.properties
@@ -493,28 +493,16 @@ headerFieldYou=You
 # Shown when content tabs are being loaded.
 loadingTab=Loading…
 
 applyToCollapsedMsgsTitle=Confirm Delete of Messages in Collapsed Thread(s)
 applyToCollapsedMsgs=Warning - this will delete messages in collapsed thread(s)
 applyToCollapsedAlwaysAskCheckbox=Always ask me before deleting messages in collapsed threads
 applyNowButton=Apply
 
-#LOCALIZATION NOTE (glodaSearch_results_why_*):  These strings populate the
-# glodaWhyColumn when performing a gloda-backed search, explaining why a
-# specific result is present in the search.  Because of how the message grouping
-# mechanism works, this value is both the value in the "Why" column as well as
-# the header for the group (when sorting/grouping on the why column.)  Short
-# strings are preferable so the why column can be understood without taking up
-# too much space.
-glodaSearch_results_why_contact=Contact
-glodaSearch_results_why_subject=Subject
-glodaSearch_results_why_body=Body
-glodaSearch_results_why_attachment=Attachment
-
 mailServerLoginFailedTitle=Login Failed
 # LOCALIZATION NOTE (mailServerLoginFailedTitle): Insert "%S" in your
 # translation where you wish to display the hostname of the server to which
 # login failed.
 mailServerLoginFailed=Login to server %S failed.
 mailServerLoginFailedRetryButton=&Retry
 mailServerLoginFailedEnterNewPasswordButton=&Enter New Password
 
new file mode 100644
--- /dev/null
+++ b/mail/locales/en-US/chrome/messenger/quickSearch.properties
@@ -0,0 +1,8 @@
+searchEverywhere.label=Search everywhere
+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
+searchBody.label=Entire message filter
+saveAsVirtualFolder.label=Save search as virtual folder
\ No newline at end of file
--- a/mail/locales/en-US/chrome/messenger/search.properties
+++ b/mail/locales/en-US/chrome/messenger/search.properties
@@ -16,12 +16,8 @@ searchSuccessMessages=%S matches found
 searchFailureMessage=No matches found
 labelForStopButton=Stop
 labelForSearchButton=Search
 labelForStopButton.accesskey=S
 labelForSearchButton.accesskey=S
 
 moreButtonTooltipText=Add a new rule
 lessButtonTooltipText=Remove this rule
-
-# LOCALIZATION NOTE (glodaSearchTabTitle): The title to use for global database
-#  search tabs.  Include "%S" where you want the search string to be inserted.
-glodaSearchTabTitle=Search: %S
--- a/mail/locales/jar.mn
+++ b/mail/locales/jar.mn
@@ -90,16 +90,20 @@
   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/configEditorOverlay.dtd                      (%chrome/messenger/configEditorOverlay.dtd)
+  locale/@AB_CD@/messenger/quickSearch.properties                       (%chrome/messenger/quickSearch.properties)
+  locale/@AB_CD@/messenger/gloda.properties                             (%chrome/messenger/gloda.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/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)
--- a/mail/themes/pinstripe/mail/searchBox.css
+++ b/mail/themes/pinstripe/mail/searchBox.css
@@ -32,21 +32,17 @@
 # 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 *****
 */
 
-#searchInput[searchCriteria="true"]  {
-  color: grey;
-}
-
-#quick-search-button {
+.quick-search-button {
   -moz-margin-start: -10px;
   -moz-margin-end: 4px;
   margin-bottom: 0;
   margin-top: 0;
 }
 
 .quick-search-button-image {
   padding: 0px;
--- a/mailnews/base/src/dbViewWrapper.js
+++ b/mailnews/base/src/dbViewWrapper.js
@@ -1815,17 +1815,17 @@ DBViewWrapper.prototype = {
     this._mailViewData = aData;
 
     // - update the search terms
     // (this triggers a view update if we are not in a batch)
     this.search.viewTerms = mailViewDef.makeTerms(this.search.session,
                                                   aData);
 
     // - persist the view to the folder.
-    if (!aDoNotPersist) {
+    if (!aDoNotPersist && this.displayedFolder) {
       let msgDatabase = this.displayedFolder.msgDatabase;
       if (msgDatabase) {
         let dbFolderInfo = msgDatabase.dBFolderInfo;
         dbFolderInfo.setUint32Property(MailViewConstants.kViewCurrent,
                                        this._mailViewIndex);
         // _mailViewData attempts to be sane and be the tag name, as opposed to
         //  magic-value ":"-prefixed value historically stored on disk.  Because
         //  we want to be forwards and backwards compatible, we put this back on
@@ -1896,16 +1896,18 @@ DBViewWrapper.prototype = {
    * @param aMessageId The message-id of the message you want.
    * @return The first nsIMsgDBHdr found in any of the underlying folders with
    *     the given message header, null if none are found.  The fact that we
    *     return something does not guarantee that it is actually visible in the
    *     view.  (The search may be filtering it out.)
    */
   getMsgHdrForMessageID: function DBViewWrapper_getMsgHdrForMessageID(
       aMessageId) {
+    if (this._syntheticView)
+      return this._syntheticView.getMsgHdrForMessageID(aMessageId);
     if (!this._underlyingFolders)
       return null;
     for (let [, folder] in Iterator(this._underlyingFolders)) {
       let msgHdr = folder.msgDatabase.getMsgHdrForMessageID(aMessageId);
       if (msgHdr)
         return msgHdr;
     }
     return null;
--- a/mailnews/base/src/quickSearchManager.js
+++ b/mailnews/base/src/quickSearchManager.js
@@ -48,45 +48,85 @@
 
 EXPORTED_SYMBOLS = ['QuickSearchManager', 'QuickSearchConstants'];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
+Cu.import("resource://app/modules/errUtils.js");
+
+try {
+  Cu.import("resource://app/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 = {
   kQuickSearchSubject: 0,
   kQuickSearchFrom: 1,
   kQuickSearchFromOrSubject: 2,
-  kQuickSearchBody: 3,
-  // there used to be a kQuickSearchHighlight = 4, apparently removed
-  kQuickSearchRecipient: 5,
-  kQuickSearchRecipientOrSubject: 6,
+  kQuickSearchRecipient: 3,
+  kQuickSearchRecipientOrSubject: 4,
+  kQuickSearchBody: 5
 };
+const kQuickSearchCount = 6;
+
+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("searchBody.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
@@ -178,9 +218,11 @@ var QuickSearchManager = {
         term.op = nsMsgSearchOp.Contains;
         term.booleanAnd = false;
         searchTerms.push(term);
       }
     }
 
     return searchTerms.length ? searchTerms : null;
   }
-};
\ No newline at end of file
+};
+
+QuickSearchManager.loadLabels();
\ No newline at end of file
--- a/mailnews/base/util/errUtils.js
+++ b/mailnews/base/util/errUtils.js
@@ -86,16 +86,17 @@ function Stringifier() {};
 Stringifier.prototype = {
   dumpObj: function (o, name) {
     this._reset();
     this._append(this.objectTreeAsString(o, true, true, 0));
     dump(this._asString());
   },
 
   dumpDOM: function(node, level, recursive) {
+    this._reset();
     let s = this.DOMNodeAsString(node, level, recursive);
     dump(s);
   },
 
   dumpEvent: function(event) {
     dump(this.eventAsString(event));
   },
 
@@ -196,17 +197,17 @@ Stringifier.prototype = {
                 s += pfx + tee + i + " (" + t + ") " + o[i].length + " chars\n";
               else
                 s += pfx + tee + i + " (" + t + ") '" + o[i] + "'\n";
               break;
             default:
               s += pfx + tee + i + " (" + t + ") " + o[i] + "\n";
           }
         } catch (ex) {
-          s += pfx + tee + i + " (exception) " + ex + "\n";
+          s += pfx + tee + " (exception) " + ex + "\n";
         }
         if (!compress)
           s += pfx + "|\n";
       }
     }
     s += pfx + "*\n";
     return s;
   },
@@ -214,22 +215,20 @@ Stringifier.prototype = {
   _repeatStr: function (str, aCount) {
     let res = "";
     while (--aCount >= 0)
       res += str;
     return res;
   },
 
   DOMNodeAsString: function(node, level, recursive) {
-    this._reset();
     if (level === undefined)
       level = 0
     if (recursive === undefined)
       recursive = true;
-
     this._append(this._repeatStr(" ", 2*level) + "<" + node.nodeName + "\n");
 
     if (node.nodeType == 3) {
         this._append(this._repeatStr(" ", (2*level) + 4) + node.nodeValue + "'\n");
     }
     else {
       if (node.attributes) {
         for (let i = 0; i < node.attributes.length; i++) {
@@ -239,17 +238,17 @@ Stringifier.prototype = {
         }
       }
       if (node.childNodes.length == 0) {
         this._append(this._repeatStr(" ", (2*level)) + "/>\n");
       }
       else if (recursive) {
         this._append(this._repeatStr(" ", (2*level)) + ">\n");
         for (let i = 0; i < node.childNodes.length; i++) {
-          this.dumpDOM(node.childNodes[i], level + 1);
+          this._append(this.DOMNodeAsString(node.childNodes[i], level + 1));
         }
         this._append(this._repeatStr(" ", 2*level) + "</" + node.nodeName + ">\n");
       }
     }
     return this._asString();
   },
 
   eventAsString: function (event) {
--- a/mailnews/db/gloda/components/glautocomp.js
+++ b/mailnews/db/gloda/components/glautocomp.js
@@ -37,53 +37,64 @@
  * ***** END LICENSE BLOCK ***** */
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-var LOG = null;
+Cu.import("resource://gre/modules/errUtils.js");
 
 var Gloda = null;
 var GlodaUtils = null;
 var MultiSuffixTree = null;
 var TagNoun = null;
 var FreeTagNoun = null;
 
+function ResultRowFullText(aItem, words, typeForStyle, andTerms) {
+  this.item = aItem;
+  this.words = words;
+  this.andTerms = andTerms;
+  this.typeForStyle = "gloda-fulltext-" + typeForStyle;
+}
+ResultRowFullText.prototype = {
+  multi: false,
+  fullText: true
+};
+
 function ResultRowSingle(aItem, aCriteriaType, aCriteria, aExplicitNounID) {
   this.nounID = aExplicitNounID || aItem.NOUN_ID;
   this.nounDef = Gloda._nounIDToDef[this.nounID];
   this.criteriaType = aCriteriaType;
   this.criteria = aCriteria;
   this.item = aItem;
+  this.typeForStyle = "gloda-single-" + this.nounDef.name;
 }
 ResultRowSingle.prototype = {
-  multi: false
+  multi: false,
+  fullText: false
 };
 
 function ResultRowMulti(aNounID, aCriteriaType, aCriteria, aQuery) {
   this.nounID = aNounID;
   this.nounDef = Gloda._nounIDToDef[aNounID];
   this.criteriaType = aCriteriaType;
   this.criteria = aCriteria;
   this.collection = aQuery.getCollection(this);
   this.collection.becomeExplicit();
   this.renderer = null;
 }
 ResultRowMulti.prototype = {
   multi: true,
+  typeForStyle: "gloda-multi",
+  fullText: false,
   onItemsAdded: function(aItems) {
-    LOG.debug("RRM onItemsAdded: " + aItems.length + ": " + aItems);
     if (this.renderer) {
-      LOG.debug("RRM rendering...");
       for each (let [iItem, item] in Iterator(aItems)) {
-        LOG.debug("RRM ..." + item);
         this.renderer.renderItem(item);
       }
     }
   },
   onItemsModified: function(aItems) {
   },
   onItemsRemoved: function(aItems) {
   },
@@ -105,25 +116,22 @@ 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) {
-      LOG.debug("Notifying completion.");
       this.listener.onSearchResult(this.completer, this);
     }
   },
   addRows: function ACGR_addRows(aRows) {
     if (!aRows.length)
       return;
-    LOG.debug("Adding " + aRows.length + " rows (" + this._pendingCount +
-              " jobs still pending)");
     this._results.push.apply(this._results, aRows); 
     this.listener.onSearchResult(this.completer, this);
   },
   // ==== nsIAutoCompleteResult
   searchString: null,
   get searchResult() {
     if (this._problem)
       return Ci.nsIAutoCompleteResult.RESULT_FAILURE;
@@ -152,36 +160,33 @@ nsAutoCompleteGlodaResult.prototype = {
     if (thing.value) // identity
       return thing.contact.name;
     else
       return thing.name || thing.subject;
   },
   // rich uses this to be the "type"
   getStyleAt: function(aIndex) {
     let row = this._results[aIndex];
-    if (row.multi)
-      return "gloda-multi";
-    else
-      return "gloda-single-" + row.nounDef.name;
+    return row.typeForStyle;
   },
   // rich uses this to be the icon
   getImageAt: function(aIndex) {
     let thing = this._results[aIndex];
     if (!thing.value)
       return null;
 
     let md5hash = GlodaUtils.md5HashString(thing.value);
     let gravURL = "http://www.gravatar.com/avatar/" + md5hash +
                                 "?d=identicon&s=32&r=g";
     return gravURL;
   },
   removeValueAt: function() {},
 
   _stop: function() {
-  },
+  }
 };
 
 const MAX_POPULAR_CONTACTS = 200;
 
 /**
  * Complete contacts/identities based on name/email.  Instant phase is based on
  *  a suffix-tree built of popular contacts/identities.  Delayed phase relies
  *  on a LIKE search of all known contacts.
@@ -197,17 +202,16 @@ ContactIdentityCompleter.prototype = {
   _popularitySorter: function(a, b){ return b.popularity - a.popularity; },
   complete: function ContactIdentityCompleter_complete(aResult, aString) {
     if (aString.length < 3)
       return false;
 
     let matches;
     if (this.suffixTree) {
       matches = this.suffixTree.findMatches(aString.toLowerCase());
-      LOG.debug("CIC: Suffix Tree found " + matches.length + " matches.")
     }
     else
       matches = [];
 
     // let's filter out duplicates due to identity/contact double-hits by
     //  establishing a map based on the contact id for these guys.
     // let's also favor identities as we do it, because that gets us the
     //  most accurate gravat, potentially
@@ -226,23 +230,21 @@ ContactIdentityCompleter.prototype = {
 
     let rows = [new ResultRowSingle(match, "text", aResult.searchString)
                 for each ([iMatch, match] in Iterator(matches))];
     aResult.addRows(rows);
 
     // - match against database contacts / identities
     let pending = {contactToThing: contactToThing, pendingCount: 2};
     
-    LOG.debug("CIC: issuing contact LIKE query");
     let contactQuery = Gloda.newQuery(Gloda.NOUN_CONTACT);
     contactQuery.nameLike(contactQuery.WILD, aString, contactQuery.WILD);
     pending.contactColl = contactQuery.getCollection(this, aResult);
     pending.contactColl.becomeExplicit();
 
-    LOG.debug("CIC: issuing identity LIKE query");
     let identityQuery = Gloda.newQuery(Gloda.NOUN_IDENTITY);
     identityQuery.kind("email").valueLike(identityQuery.WILD, aString,
         identityQuery.WILD);
     pending.identityColl = identityQuery.getCollection(this, aResult);
     pending.identityColl.becomeExplicit();
     
     aResult._contactCompleterPending = pending;
 
@@ -252,17 +254,16 @@ ContactIdentityCompleter.prototype = {
   },
   onItemsModified: function(aItems, aCollection) {
   },
   onItemsRemoved: function(aItems, aCollection) {
   },
   onQueryCompleted: function(aCollection) {
     // handle the initial setup case...
     if (aCollection.data == null) {
-      LOG.debug("CIC: Initial query found " + aCollection.items.length);
       // cheat and explicitly add our own contact...
       if (!(Gloda.myContact.id in this.contactCollection._idMap))
         this.contactCollection._onItemsAdded([Gloda.myContact]);
         
       // the set of identities owned by the contacts is automatically loaded as part
       //  of the contact loading...
       // (but only if we actually have any contacts)
       this.identityCollection =
@@ -285,18 +286,16 @@ ContactIdentityCompleter.prototype = {
       //  passed through to concat.  identityMails will likewise be undefined.
       this.suffixTree = new MultiSuffixTree(contactNames.concat(identityMails),
         this.contactCollection.items.concat(this.identityCollection &&
           this.identityCollection.items));
       
       return;
     }
     
-    LOG.debug("CIC: LIKE query found " + aCollection.items.length);
-    
     // handle the completion case
     let result = aCollection.data;
     let pending = result._contactCompleterPending;
     
     if (--pending.pendingCount == 0) {
       let possibleDudes = [];
       
       let contactToThing = pending.contactToThing;
@@ -332,19 +331,16 @@ ContactIdentityCompleter.prototype = {
       result.markCompleted(this);
       
       // the collections no longer care about the result, make it clear.
       delete pending.identityColl.data;
       delete pending.contactColl.data;
       // the result object no longer needs us or our data
       delete result._contactCompleterPending;
     }
-    else {
-      LOG.debug("ignoring... pending is still: " + pending.pendingCount);
-    }
   }
 };
 
 /**
  * Complete tags that are used on contacts.
  */
 function ContactTagCompleter() {
   FreeTagNoun.populateKnownFreeTags();
@@ -352,39 +348,35 @@ function ContactTagCompleter() {
   FreeTagNoun.addListener(this);
 }
 ContactTagCompleter.prototype = {
   _buildSuffixTree: function() {
     let tagNames = [], tags = [];
     for (let [tagName, tag] in Iterator(FreeTagNoun.knownFreeTags)) {
       tagNames.push(tagName.toLowerCase());
       tags.push(tag);
-      LOG.debug("contact tag: " + tagName);
     }
     this._suffixTree = new MultiSuffixTree(tagNames, tags);
     this._suffixTreeDirty = false;
   },
   onFreeTagAdded: function(aTag) {
     this._suffixTreeDirty = true;
   },
   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
     
-    LOG.debug("Completing on contact tags...");
-    
     tags = this._suffixTree.findMatches(aString.toLowerCase());
     let rows = [];
     for each (let [iTag, tag] in Iterator(tags)) {
       let query = Gloda.newQuery(Gloda.NOUN_CONTACT);
-      LOG.debug("  checking for contact tag: " + tag.name);
       query.freeTags(tag);
       let resRow = new ResultRowMulti(Gloda.NOUN_CONTACT, "tag", tag.name,
                                       query);
       rows.push(resRow);
     }
     aResult.addRows(rows);
     
     return false; // no async mechanism that will add new rows
@@ -400,107 +392,121 @@ function MessageTagCompleter() {
 MessageTagCompleter.prototype = {
   _buildSuffixTree: function MessageTagCompleter__buildSufficeTree() {
     let tagNames = [], tags = [];
     let tagArray = TagNoun.getAllTags();
     for (let iTag = 0; iTag < tagArray.length; iTag++) {
       let tag = tagArray[iTag];
       tagNames.push(tag.tag.toLowerCase());
       tags.push(tag);
-      LOG.debug("message tag: " + tag.tag);
     }
     this._suffixTree = new MultiSuffixTree(tagNames, tags);
     this._suffixTreeDirty = false;
   },
   complete: function MessageTagCompleter_complete(aResult, aString) {
     if (aString.length < 2)
       return false;
     
-    LOG.debug("Completing on message tags...");
-    
     tags = this._suffixTree.findMatches(aString.toLowerCase());
     let rows = [];
     for each (let [, tag] in Iterator(tags)) {
-      LOG.debug(" found message tag: " + tag.tag);
       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
   }
 };
 
+/**
+ * Complete with helpful hints about full-text search
+ */
+function FullTextCompleter() {
+}
+FullTextCompleter.prototype = {
+  complete: function FullTextCompleter_complete(aResult, aString) {
+    if (aString.length < 2)
+      return false;
+    let rows = [];
+    let words = aString.trim().replace(/\s+/g, ' ').split(' ');
+    let numWords = words.length;
+    if (numWords == 1) {
+      let resRow = new ResultRowFullText(aString, words, "single", false);
+      rows.push(resRow);
+    } else {
+      let resRow = new ResultRowFullText(aString, words, "all", true);
+      rows.push(resRow);
+      resRow = new ResultRowFullText(aString, words, "any", false);
+      rows.push(resRow);
+    }
+    aResult.addRows(rows);
+    return false; // no async mechanism that will add new rows
+  }
+};
+
 function nsAutoCompleteGloda() {
   this.wrappedJSObject = this;
-
-  // set up our awesome globals!
-  if (Gloda === null) {
-    let loadNS = {};
-
-    Cu.import("resource://app/modules/gloda/public.js", loadNS);
-    Gloda = loadNS.Gloda;
-
-    Cu.import("resource://app/modules/gloda/utils.js", loadNS);
-    GlodaUtils = loadNS.GlodaUtils;
-    Cu.import("resource://app/modules/gloda/suffixtree.js", loadNS);
-    MultiSuffixTree = loadNS.MultiSuffixTree;
-    Cu.import("resource://app/modules/gloda/noun_tag.js", loadNS);
-    TagNoun = loadNS.TagNoun;
-    Cu.import("resource://app/modules/gloda/noun_freetag.js", loadNS);
-    FreeTagNoun = loadNS.FreeTagNoun;
-
-    Cu.import("resource://app/modules/gloda/log4moz.js", loadNS);
-    LOG = loadNS["Log4Moz"].repository.getLogger("gloda.autocomp");
-  }
+  try {
+    // set up our awesome globals!
+    if (Gloda === null) {
+      let loadNS = {};
+      Cu.import("resource://app/modules/gloda/public.js", loadNS);
+      Gloda = loadNS.Gloda;
+  
+      Cu.import("resource://app/modules/gloda/utils.js", loadNS);
+      GlodaUtils = loadNS.GlodaUtils;
+      Cu.import("resource://app/modules/gloda/suffixtree.js", loadNS);
+      MultiSuffixTree = loadNS.MultiSuffixTree;
+      Cu.import("resource://app/modules/gloda/noun_tag.js", loadNS);
+      TagNoun = loadNS.TagNoun;
+      Cu.import("resource://app/modules/gloda/noun_freetag.js", loadNS);
+      FreeTagNoun = loadNS.FreeTagNoun;
+  
+      Cu.import("resource://app/modules/gloda/log4moz.js", loadNS);
+      LOG = loadNS["Log4Moz"].repository.getLogger("gloda.autocomp");
+    }
 
-  LOG.debug("initializing completers");
-
-  this.completers = [];
-  
-  this.curResult = null;
+    this.completers = [];
+    this.curResult = null;
 
-dump("init CIC\n");
-  LOG.debug("initializing ContactIdentityCompleter");
-  try {
-  this.completers.push(new ContactIdentityCompleter());
-  } catch (ex) {dump("CICEX: " + ex.fileName + ":" + ex.lineNumber + ": " + ex);}
-dump("init CTC\n");
-  LOG.debug("initializing ContactTagCompleter");
-  this.completers.push(new ContactTagCompleter());
-dump("init MTC\n");
-  LOG.debug("initializing MessageTagCompleter");
-  try {
-  this.completers.push(new MessageTagCompleter());
-  } catch (ex) {dump("MTCEX: " + ex.fileName + ":" + ex.lineNumber + ": " + ex);}
-  
-  LOG.debug("initialized completers");
+    this.completers.push(new FullTextCompleter());
+    this.completers.push(new ContactIdentityCompleter());
+    this.completers.push(new ContactTagCompleter());
+    this.completers.push(new MessageTagCompleter());
+  } catch (e) {
+    logException(e);
+  }
 }
 
 nsAutoCompleteGloda.prototype = {
   classDescription: "AutoCompleteGloda",
   contractID: "@mozilla.org/autocomplete/search;1?name=gloda",
-  classID: Components.ID("{3bbe4d77-3f70-4252-9500-bc00c26f476c}"),
+  classID: Components.ID("{3bbe4d77-3f70-4252-9500-bc00c26f476d}"),
   QueryInterface: XPCOMUtils.generateQI([
       Components.interfaces.nsIAutoCompleteSearch]),
 
   startSearch: function(aString, aParam, aResult, aListener) {
-    let result = new nsAutoCompleteGlodaResult(aListener, this, aString);
-    // save this for hacky access to the search.  I somewhat suspect we simply
-    //  should not be using the formal autocomplete mechanism at all.
-    this.curResult = result;
-    
-    for each (let [iCompleter, completer] in Iterator(this.completers)) {
-      // they will return true if they have something pending.
-      if (completer.complete(result, aString))
-        result.markPending(completer);
+    try {
+      let result = new nsAutoCompleteGlodaResult(aListener, this, aString);
+      // save this for hacky access to the search.  I somewhat suspect we simply
+      //  should not be using the formal autocomplete mechanism at all.
+      this.curResult = result;
+      
+      for each (let [iCompleter, completer] in Iterator(this.completers)) {
+        // they will return true if they have something pending.
+        if (completer.complete(result, aString))
+          result.markPending(completer);
+      }
+      
+      aListener.onSearchResult(this, result);
+    } catch (e) {
+      logException(e);
     }
-    
-    aListener.onSearchResult(this, result);
   },
 
   stopSearch: function() {
-  },
+  }
 };
 
 function NSGetModule(compMgr, fileSpec) {
   return XPCOMUtils.generateModule([nsAutoCompleteGloda]);
 }
--- a/mailnews/db/gloda/content/glodacomplete.css
+++ b/mailnews/db/gloda/content/glodacomplete.css
@@ -1,37 +1,62 @@
 textbox[type="glodacomplete"] {
   -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete");
+  width: 400px;
 }
 
 panel[type="glodacomplete-richlistbox"] {
   -moz-binding: url("chrome://gloda/content/glodacomplete.xml#glodacomplete-rich-result-popup");
 }
 
 .autocomplete-richlistbox {
   -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistbox");
   -moz-user-focus: ignore;
   -moz-appearance: none;
 }
 
 .autocomplete-richlistbox > scrollbox {
   overflow-x: hidden !important;
 }
 
+.parameters {
+  font-style: italic;
+  margin-left: 1em;
+}
+.picture {
+  margin: 1ex;
+  margin-right: 1em;
+}
+
 .autocomplete-richlistitem[type="gloda-single-tag"] {
   -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-single-tag-item");
   overflow: -moz-hidden-unscrollable;
 }
 
 .autocomplete-richlistitem[type="gloda-single-identity"] {
   -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-single-identity-item");
   -moz-box-orient: vertical;
   overflow: -moz-hidden-unscrollable;
 }
 
+.autocomplete-richlistitem[type="gloda-fulltext-single"] {
+  -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-fulltext-single-item");
+  overflow: -moz-hidden-unscrollable;
+}
+
+.autocomplete-richlistitem[type="gloda-fulltext-any"] {
+  -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-fulltext-any-item");
+  overflow: -moz-hidden-unscrollable;
+}
+
+.autocomplete-richlistitem[type="gloda-fulltext-all"] {
+  -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-fulltext-all-item");
+  overflow: -moz-hidden-unscrollable;
+}
+
 richlistitem[type="gloda-contact-chunk"] {
   -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-contact-chunk");
   -moz-box-orient: vertical;
   overflow: -moz-hidden-unscrollable;
 }
 
 .autocomplete-richlistitem[type="gloda-multi"] {
   -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-multi-item");
--- a/mailnews/db/gloda/content/glodacomplete.xml
+++ b/mailnews/db/gloda/content/glodacomplete.xml
@@ -355,27 +355,134 @@
             return "tag " + this.row.item.tag;
           ]]>
         </getter>
       </property>
       
       <method name="_adjustAcItem">
         <body>
           <![CDATA[
-          this._explanation.value = "messages tagged " + this.row.item.tag;
+          this._explanation.value = "messages tagged " + this.row.item.tag; // XXX l10n
+          ]]>
+        </body>
+      </method>
+    </implementation>
+  </binding>
+
+
+  <binding id="gloda-fulltext-single-item" extends="chrome://gloda/content/glodacomplete.xml#glodacomplete-base-richlistitem">
+    <content orient="vertical">
+      <xul:description anonid="explanation"/>
+      <xul:description anonid="parameters"/>
+    </content>
+    <implementation implements="nsIDOMXULSelectControlItemElement">
+      <constructor>
+        <![CDATA[
+            this._explanation = document.getAnonymousElementByAttribute(this, "anonid", "explanation");
+
+            this._adjustAcItem();
+          ]]>
+      </constructor>
+      
+      <property name="label" readonly="true">
+        <getter>
+          <![CDATA[
+            return "full text search: " + this.row.item;
+          ]]>
+        </getter>
+      </property>
+      
+      <method name="_adjustAcItem">
+        <body>
+          <![CDATA[
+          try {
+            this._explanation.value = "messages mentioning " + this.row.item; // XXX l10n
+            } catch (e) {
+            logException(e);
+            }
+          ]]>
+        </body>
+      </method>
+    </implementation>
+  </binding>
+
+
+
+  <binding id="gloda-fulltext-any-item" extends="chrome://gloda/content/glodacomplete.xml#glodacomplete-base-richlistitem">
+    <content orient="vertical">
+      <xul:description anonid="explanation"/>
+      <xul:description anonid="parameters" class="parameters"/>
+    </content>
+    <implementation implements="nsIDOMXULSelectControlItemElement">
+      <constructor>
+        <![CDATA[
+            this._explanation = document.getAnonymousElementByAttribute(this, "anonid", "explanation");
+            this._parameters = document.getAnonymousElementByAttribute(this, "anonid", "parameters");
+
+            this._adjustAcItem();
+          ]]>
+      </constructor>
+      
+      <property name="label" readonly="true">
+        <getter>
+          <![CDATA[
+            return "full text search: " + this.row.item;
+          ]]>
+        </getter>
+      </property>
+      
+      <method name="_adjustAcItem">
+        <body>
+          <![CDATA[
+          this._explanation.value = "messages with ANY of: "
+          this._parameters.value = this.row.words.join(", "); // XXX l10n
+          ]]>
+        </body>
+      </method>
+    </implementation>
+  </binding>
+
+  <binding id="gloda-fulltext-all-item" extends="chrome://gloda/content/glodacomplete.xml#glodacomplete-base-richlistitem">
+    <content orient="vertical">
+      <xul:description anonid="explanation"/>
+      <xul:description anonid="parameters" class="parameters"/>
+    </content>
+    <implementation implements="nsIDOMXULSelectControlItemElement">
+      <constructor>
+        <![CDATA[
+            this._explanation = document.getAnonymousElementByAttribute(this, "anonid", "explanation");
+            this._parameters = document.getAnonymousElementByAttribute(this, "anonid", "parameters");
+
+            this._adjustAcItem();
+          ]]>
+      </constructor>
+      
+      <property name="label" readonly="true">
+        <getter>
+          <![CDATA[
+            return "full text search: " + this.row.item;
+          ]]>
+        </getter>
+      </property>
+      
+      <method name="_adjustAcItem">
+        <body>
+          <![CDATA[
+          this._explanation.value = "messages with ALL of: "
+          this._parameters.value = this.row.words.join(", "); // XXX l10n
           ]]>
         </body>
       </method>
     </implementation>
   </binding>
 
   <binding id="gloda-single-identity-item" extends="chrome://gloda/content/glodacomplete.xml#glodacomplete-base-richlistitem">
     <content>
       <xul:hbox>
-	    <xul:image anonid="picture"/>
+	    <xul:image anonid="picture" class="picture"/>
 	    <xul:vbox>
 	      <xul:hbox>
 	        <xul:hbox anonid="name-box" class="ac-title" flex="1"
 	                  onunderflow="_doUnderflow('_name');">
 	          <xul:description anonid="name" class="ac-normal-text ac-comment"
 	                           xbl:inherits="selected"/>
 	        </xul:hbox>
 	        <xul:label anonid="name-overflow-ellipsis" xbl:inherits="selected"
--- a/mailnews/db/gloda/modules/collection.js
+++ b/mailnews/db/gloda/modules/collection.js
@@ -592,16 +592,17 @@ GlodaCollection.prototype = {
       catch (ex) {
         LOG.error("caught exception from listener in onItemsRemoved: " +
             ex.fileName + ":" + ex.lineNumber + ": " + ex);
       }
     }
   },
 
   _onQueryCompleted: function gloda_coll_onQueryCompleted() {
+    this.query.completed = true;
     if (this._listener && this._listener.onQueryCompleted)
       this._listener.onQueryCompleted(this);
   }
 };
 
 /**
  * Create an LRU cache collection for the given noun with the given size.
  * @constructor
--- a/mailnews/db/gloda/modules/datamodel.js
+++ b/mailnews/db/gloda/modules/datamodel.js
@@ -201,16 +201,20 @@ GlodaConversation.prototype = {
     let query = new GlodaMessage.prototype.NOUN_DEF.queryClass();
     query.conversation(this._id).orderBy("date");
     return query.getCollection(aListener, aData);
   },
 
   toString: function gloda_conversation_toString() {
     return "Conversation:" + this._id;
   },
+
+  toLocaleString: function gloda_conversation_toLocaleString() {
+    return this._subject;
+  }
 };
 
 function GlodaFolder(aDatastore, aID, aURI, aDirtyStatus, aPrettyName,
                      aIndexingPriority) {
   this._datastore = aDatastore;
   this._id = aID;
   this._uri = aURI;
   this._dirtyStatus = aDirtyStatus;
@@ -260,24 +264,35 @@ GlodaFolder.prototype = {
       this._datastore.updateFolderDirtyStatus(this);
     }
   },
   get name() { return this._prettyName; },
   toString: function gloda_folder_toString() {
     return "Folder:" + this._id;
   },
 
+  toLocaleString: function gloda_folder_toLocaleString() {
+    let xpcomFolder = this.getXPCOMFolder(this.kActivityFolderOnlyNoData);
+    if (!xpcomFolder)
+      return this._prettyName;
+    return xpcomFolder.prettiestName +
+      " (" + xpcomFolder.rootFolder.prettiestName + ")";
+  },
+
   get indexingPriority() {
     return this._indexingPriority;
   },
 
   /** We are going to index this folder. */
   kActivityIndexing: 0,
   /** Asking for the folder to perform header retrievals. */
   kActivityHeaderRetrieval: 1,
+  /** We only want the folder for its metadata but are not going to open it. */
+  kActivityFolderOnlyNoData: 2,
+
 
   /** Is this folder known to be actively used for indexing? */
   _activeIndexing: false,
   /** Get our indexing status. */
   get indexing() {
     return this._activeIndexing;
   },
   /**
@@ -319,16 +334,19 @@ GlodaFolder.prototype = {
         //  that independently and only for header retrieval.
         this.indexing = true;
         break;
       case this.kActivityHeaderRetrieval:
         if (this._activeHeaderRetrievalLastStamp === 0)
           this._datastore.markFolderLive(this);
         this._activeHeaderRetrievalLastStamp = Date.now();
         break;
+      case this.kActivityFolderOnlyNoData:
+        // we don't have to do anything here.
+        break;
     }
 
     return this._xpcomFolder;
   },
 
   /**
    * How many milliseconds must a folder have not had any header retrieval
    *  activity before it's okay to lose the database reference?
@@ -623,16 +641,22 @@ GlodaIdentity.prototype = {
   get uniqueValue() {
     return this._kind + "@" + this._value;
   },
 
   toString: function gloda_identity_toString() {
     return "Identity:" + this._kind + ":" + this._value;
   },
 
+  toLocaleString: function gloda_identity_toLocaleString() {
+    if (this.contact.name == this.value)
+      return this.value;
+    return this.contact.name + " : " + this.value;
+  },
+
   get abCard() {
     // for our purposes, the address book only speaks email
     if (this._kind != "email")
       return false;
     let card = GlodaUtils.getCardForEmail(this._value);
     if (card)
       this._hasAddressBookCard = true;
     return card;
--- a/mailnews/db/gloda/modules/datastore.js
+++ b/mailnews/db/gloda/modules/datastore.js
@@ -548,17 +548,17 @@ var GlodaDatastore = {
   kConstraintIn: 1,
   kConstraintRanges: 2,
   kConstraintEquals: 3,
   kConstraintStringLike: 4,
   kConstraintFulltext: 5,
 
   /* ******************* SCHEMA ******************* */
 
-  _schemaVersion: 12,
+  _schemaVersion: 13,
   _schema: {
     tables: {
 
       // ----- Messages
       folderLocations: {
         columns: [
           ["id", "INTEGER PRIMARY KEY"],
           ["folderURI", "TEXT NOT NULL"],
@@ -983,16 +983,19 @@ var GlodaDatastore = {
 
     aDBConnection.schemaVersion = this._schemaVersion;
   },
 
   /**
    * Create a table for a noun, replete with data binding.
    */
   createNounTable: function gloda_ds_createTableIfNotExists(aNounDef) {
+    // give it a _jsonText attribute if appropriate...
+    if (aNounDef.allowsArbitraryAttrs)
+      aNounDef.schema.columns.push(['jsonAttributes', 'STRING', '_jsonText']);
     // check if the table exists
     if (!this.asyncConnection.tableExists(aNounDef.tableName)) {
       // it doesn't! create it (and its potentially many variants)
       try {
         this._createTableSchema(this.asyncConnection, aNounDef.tableName,
                                 aNounDef.schema);
       }
       catch (ex) {
@@ -1023,17 +1026,25 @@ var GlodaDatastore = {
    */
   _migrate: function gloda_ds_migrate(aDBService, aDBFile, aDBConnection,
                                       aCurVersion, aNewVersion) {
 
     // version 12:
     // - notability column added
     // version 13:
     // - we are adding a new fulltext index column. blow away!
-    if (aCurVersion < 13) {
+    // - note that I screwed up and failed to mark the schema change; apparently
+    //   no database will claim to be version 13...
+    // version 14:
+    // - new attributes: forwarded, repliedTo, bcc, recipients
+    // - altered fromMeTo and fromMeCc to fromMe
+    // - altered toMe and ccMe to just be toMe
+    // - exposes bcc to cc-related attributes
+    // - MIME type DB schema overhaul
+    if (aCurVersion < 14) {
       aDBConnection.close();
       aDBFile.remove(false);
       this._log.warn("Global database has been purged due to schema change.");
       return this._createDB(aDBService, aDBFile);
     }
 
     aDBConnection.schemaVersion = aNewVersion;
 
@@ -2094,17 +2105,17 @@ var GlodaDatastore = {
       date = null;
     else
       date = new Date(aRow.getInt64(4) / 1000);
     if (aRow.getTypeOfIndex(7) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
       jsonText = undefined;
     else
       jsonText = aRow.getString(7);
     // only queryFromQuery queries will have these columns
-    if (aRow.numEntries == 14) {
+    if (aRow.numEntries >= 14) {
       if (aRow.getTypeOfIndex(9) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
         subject = undefined;
       else
         subject = aRow.getString(9);
       if (aRow.getTypeOfIndex(10) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
         indexedBodyText = undefined;
       else
         indexedBodyText = aRow.getString(10);
@@ -2975,17 +2986,17 @@ var GlodaDatastore = {
       // we don't want to overwrite the existing listener or its data, but this
       //  does raise the question about what should happen if we get passed in
       //  a different listener and/or data.
       if (aListenerData !== undefined)
         collection.data = aListenerData;
     }
     if (aListenerData) {
       if (collection.dataStack)
-        collection.dataStack.push(aListenerData)
+        collection.dataStack.push(aListenerData);
       else
         collection.dataStack = [aListenerData];
     }
 
     statement.executeAsync(new QueryFromQueryCallback(statement, aNounDef,
       collection));
     statement.finalize();
     return collection;
--- a/mailnews/db/gloda/modules/dbview.js
+++ b/mailnews/db/gloda/modules/dbview.js
@@ -35,158 +35,122 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 /*
  * This file is charged with providing you a way to have a pretty gloda-backed
  *  nsIMsgDBView.
  */
 
-EXPORTED_SYMBOLS = ["GlodaSyntheticSearchView", "GlodaViewFactory"];
+EXPORTED_SYMBOLS = ["GlodaSyntheticView"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://app/modules/gloda/log4moz.js");
 
 Cu.import("resource://app/modules/gloda/public.js");
 Cu.import("resource://app/modules/gloda/msg_search.js");
 
-function GlodaScoreColumn(aSearcher) {
-  this.searcher = aSearcher;
-}
-GlodaScoreColumn.prototype = {
-  id: "glodaScoreCol",
-  bindToView: function (aDBView) {
-    this.dbView = aDBView;
-  },
+/**
+ * Create a synthetic view suitable for passing to |FolderDisplayWidget.show|.
+ * You must pass a query, collection, or conversation in.
+ *
+ * @param {GlodaQuery} [aArgs.query] A gloda query to run.
+ * @param {GlodaCollection} [aArgs.collection] An already-populated collection
+ *     to display.  Do not call getCollection on a query and hand us that.  We
+ *     will not register ourselves as a listener and things will not work.
+ * @param {GlodaConversation} [aArgs.conversation] A conversation whose messages
+ *     you want to display.
+ */
+function GlodaSyntheticView(aArgs) {
+  if ("query" in aArgs) {
+    this.query = aArgs.query;
+    this.collection = this.query.getCollection(this);
+    this.completed = false;
+  }
+  else if ("collection" in aArgs) {
+    this.query = null;
+    this.collection = aArgs.collection;
+    this.completed = true;
+  }
+  else if ("conversation" in aArgs) {
+    this.collection = aArgs.conversation.getMessagesCollection(this);
+    this.query = this.collection.query;
+    this.completed = false;
+  }
+  else {
+    throw new Error("You need to pass a query or collection");
+  }
 
-  getCellText: function(row, col) {
-    let folder = this.dbView.getFolderForViewIndex(row);
-    let key = this.dbView.getKeyAt(row);
-    return "" + this.searcher.scoresByUriAndKey[folder.URI + "-" + key];
-  },
-  getSortLongForRow:   function(hdr) {
-    return this.searcher.scoresByUriAndKey[
-      hdr.folder.URI + "-" + hdr.messageKey] || 0;
-  },
-  isString: function() {
-    return false;
-  },
-
-  getCellProperties:   function(row, col, props){},
-  getRowProperties:    function(row, props){},
-  getImageSrc:         function(row, col) {return null;},
-  getSortStringForRow: function(hdr) {
-    return null;
-  },
-};
-
-function GlodaWhyColumn(aSearcher) {
-  this.searcher = aSearcher;
+  this.customColumns = [];
 }
-GlodaWhyColumn.prototype = {
-  id: "glodaWhyCol",
-  bindToView: function (aDBView) {
-    this.dbView = aDBView;
-  },
-
-  getCellText: function(row, col) {
-    let folder = this.dbView.getFolderForViewIndex(row);
-    let key = this.dbView.getKeyAt(row);
-    return this.searcher.whysByUriAndKey[folder.URI + "-" + key] || "";
-  },
-  getSortStringForRow: function(hdr) {
-    return this.searcher.whysByUriAndKey[hdr.folder.URI + "-" + hdr.messageKey]
-      || "";
-  },
-  isString: function() {
-    return true;
-  },
-
-  getCellProperties:   function(row, col, props){},
-  getRowProperties:    function(row, props){},
-  getImageSrc:         function(row, col) {return null;},
-  getSortLongForRow:   function(hdr) {return 0;}
-};
-
-function GlodaSyntheticSearchView(aSearchString, aFacetString, aLocation) {
-  this.searcher = new GlodaMsgSearcher(this, aSearchString.split(" "));
-
-  this._whyColumn = new GlodaWhyColumn(this.searcher);
-  this._scoreColumn = new GlodaScoreColumn(this.searcher);
-
-  this.customColumns = [this._whyColumn, this._scoreColumn];
-
-  this.collection = null;
-  this._whyMap = {};
-  this._scoreMap = {};
-
-  this.searchString = aSearchString;
-  this.facetString = aFacetString;
-  this.location = aLocation;
-}
-GlodaSyntheticSearchView.prototype = {
-  defaultSort: [["glodaScoreCol", Ci.nsMsgViewSortOrder.descending]],
+GlodaSyntheticView.prototype = {
+  defaultSort: [["dateCol", Ci.nsMsgViewSortOrder.descending]],
 
   /**
    * Request the search be performed and notification provided to
    *  aSearchListener.  If results are already available, they should
    *  be provided to aSearchListener without re-performing the search.
    */
   search: function(aSearchListener, aCompletionCallback) {
     this.searchListener = aSearchListener;
     this.completionCallback = aCompletionCallback;
 
     this.searchListener.onNewSearch();
-    if (this.collection) {
+    if (this.completed) {
       this.reportResults(this.collection.items);
       // we're not really aborting, but it closes things out nicely
       this.abortSearch();
       return;
     }
-
-    this.collection = this.searcher.go();
   },
 
   abortSearch: function() {
     if (this.searchListener)
       this.searchListener.onSearchDone(Cr.NS_OK);
     if (this.completionCallback)
       this.completionCallback();
     this.searchListener = null;
     this.completionCallback = null;
   },
 
   reportResults: function(aItems) {
     for each (let [, item] in Iterator(aItems)) {
       let hdr = item.folderMessage;
-      this.searchListener.onSearchHit(hdr, hdr.folder);
+      if (hdr)
+        this.searchListener.onSearchHit(hdr, hdr.folder);
     }
   },
 
+  /**
+   * Helper function used by |DBViewWrapper.getMsgHdrForMessageID| since there
+   *  are no actual backing folders for it to check.
+   */
+  getMsgHdrForMessageID: function(aMessageId) {
+    for each (let [, item] in this.collection.items) {
+      if (item.messageId == aMessageId) {
+        let hdr = item.folderMessage;
+        if (hdr)
+          return hdr;
+      }
+    }
+    return null;
+  },
+
   // --- collection listener
   onItemsAdded: function(aItems, aCollection) {
     if (this.searchListener)
       this.reportResults(aItems);
   },
   onItemsModified: function(aItems, aCollection) {
   },
   onItemsRemoved: function(aItems, aCollection) {
   },
   onQueryCompleted: function(aCollection) {
+    this.completed = true;
     this.searchListener.onSearchDone(Cr.NS_OK);
     if (this.completionCallback)
       this.completionCallback();
   },
 };
-
-var GlodaViewFactory = {
-  kFacetEverything: "everything",
-  kFacetSubject: "subject",
-  kFacetBody: "body",
-  kFacetAttachments: "attachments",
-  kFacetInvolves: "involves",
-  kFacetTo: "to",
-  kFacetFrom: "from",
-};
--- a/mailnews/db/gloda/modules/explattr.js
+++ b/mailnews/db/gloda/modules/explattr.js
@@ -45,34 +45,36 @@
 EXPORTED_SYMBOLS = ['GlodaExplicitAttr'];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://app/modules/gloda/log4moz.js");
+Cu.import("resource://app/modules/StringBundle.js");
 
 Cu.import("resource://app/modules/gloda/utils.js");
 Cu.import("resource://app/modules/gloda/gloda.js");
 Cu.import("resource://app/modules/gloda/noun_tag.js");
 
 
+const nsMsgMessageFlags_Replied = Ci.nsMsgMessageFlags.Replied;
+const nsMsgMessageFlags_Forwarded = Ci.nsMsgMessageFlags.Forwarded;
+
 const EXT_BUILTIN = "built-in";
-const FA_TAG = "TAG";
-const FA_STAR = "STAR";
-const FA_READ = "READ";
 
 /**
  * @namespace Explicit attribute provider.  Indexes/defines attributes that are
  *  explicitly a result of user action.  This dubiously includes marking a
  *  message as read.
  */
 var GlodaExplicitAttr = {
   providerName: "gloda.explattr",
+  strings: new StringBundle("chrome://messenger/locale/gloda.properties"),
   _log: null,
   _msgTagService: null,
 
   init: function gloda_explattr_init() {
     this._log =  Log4Moz.repository.getLogger("gloda.explattr");
 
     this._msgTagService = Cc["@mozilla.org/messenger/tagservice;1"].
                           getService(Ci.nsIMsgTagService);
@@ -88,72 +90,101 @@ var GlodaExplicitAttr = {
 
   /** Boost for starred messages. */
   NOTABILITY_STARRED: 16,
   /** Boost for tagged messages, first tag. */
   NOTABILITY_TAGGED_FIRST: 8,
   /** Boost for tagged messages, each additional tag. */
   NOTABILITY_TAGGED_ADDL: 1,
 
-  _attrTag: null,
-  _attrStar: null,
-  _attrRead: null,
-
   defineAttributes: function() {
     // Tag
     this._attrTag = Gloda.defineAttribute({
                         provider: this,
                         extensionName: Gloda.BUILT_IN,
                         attributeType: Gloda.kAttrExplicit,
                         attributeName: "tag",
                         bindName: "tags",
                         singular: false,
+                        facet: true,
                         subjectNouns: [Gloda.NOUN_MESSAGE],
                         objectNoun: Gloda.NOUN_TAG,
                         parameterNoun: null,
                         // Property change notifications that we care about:
                         propertyChanges: ["keywords"],
                         }); // not-tested
 
     // Star
     this._attrStar = Gloda.defineAttribute({
                         provider: this,
                         extensionName: Gloda.BUILT_IN,
                         attributeType: Gloda.kAttrExplicit,
                         attributeName: "star",
                         bindName: "starred",
                         singular: true,
+                        facet: true,
                         subjectNouns: [Gloda.NOUN_MESSAGE],
                         objectNoun: Gloda.NOUN_BOOLEAN,
                         parameterNoun: null,
                         }); // tested-by: test_attributes_explicit
     // Read/Unread
     this._attrRead = Gloda.defineAttribute({
                         provider: this,
                         extensionName: Gloda.BUILT_IN,
                         attributeType: Gloda.kAttrExplicit,
                         attributeName: "read",
                         singular: true,
                         subjectNouns: [Gloda.NOUN_MESSAGE],
                         objectNoun: Gloda.NOUN_BOOLEAN,
                         parameterNoun: null,
                         }); // tested-by: test_attributes_explicit
 
+    /**
+     * Has this message been replied to by the user.
+     */
+    this._attrRepliedTo = Gloda.defineAttribute({
+      provider: this,
+      extensionName: Gloda.BUILT_IN,
+      attributeType: Gloda.kAttrExplicit,
+      attributeName: "repliedTo",
+      singular: true,
+      subjectNouns: [Gloda.NOUN_MESSAGE],
+      objectNoun: Gloda.NOUN_BOOLEAN,
+      parameterNoun: null,
+    }); // tested-by: test_attributes_explicit
+
+    /**
+     * Has this user forwarded this message to someone.
+     */
+    this._attrForwarded = Gloda.defineAttribute({
+      provider: this,
+      extensionName: Gloda.BUILT_IN,
+      attributeType: Gloda.kAttrExplicit,
+      attributeName: "forwarded",
+      singular: true,
+      subjectNouns: [Gloda.NOUN_MESSAGE],
+      objectNoun: Gloda.NOUN_BOOLEAN,
+      parameterNoun: null,
+    }); // tested-by: test_attributes_explicit
   },
 
   process: function Gloda_explattr_process(aGlodaMessage, aRawReps, aIsNew,
                                            aCallbackHandle) {
     let aMsgHdr = aRawReps.header;
 
     aGlodaMessage.starred = aMsgHdr.isFlagged;
     if (aGlodaMessage.starred)
       aGlodaMessage.notability += this.NOTABILITY_STARRED;
 
     aGlodaMessage.read = aMsgHdr.isRead;
 
+    let flags = aMsgHdr.flags;
+    aGlodaMessage.repliedTo = Boolean(flags & nsMsgMessageFlags_Replied);
+    aGlodaMessage.forwarded = Boolean(flags & nsMsgMessageFlags_Forwarded);
+
     let tags = aGlodaMessage.tags = [];
 
     // -- Tag
     // build a map of the keywords
     let keywords = aMsgHdr.getStringProperty("keywords");
     let keywordList = keywords.split(' ');
     let keywordMap = {};
     for (let iKeyword = 0; iKeyword < keywordList.length; iKeyword++) {
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/facet.js
@@ -0,0 +1,595 @@
+/* ***** 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 Global Database.
+ *
+ * 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>
+ *
+ * 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 ***** */
+
+/*
+ * This file provides faceting logic.
+ */
+
+let EXPORTED_SYMBOLS = ["FacetDriver", "FacetUtils"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://app/modules/gloda/public.js");
+
+/**
+ * Decides the appropriate faceters for the noun type and drives the faceting
+ *  process.  This class and the faceters are intended to be reusable so that
+ *  you only need one instance per faceting session.  (Although each faceting
+ *  pass is accordingly destructive to previous results.)
+ *
+ * Our strategy for faceting is to process one attribute at a time across all
+ *  the items in the provided set.  The alternative would be to iterate over
+ *  the items and then iterate over the attributes on each item.  While both
+ *  approaches have caching downsides
+ */
+function FacetDriver(aNounDef, aWindow) {
+  this.nounDef = aNounDef;
+  this._window = aWindow;
+
+  this._makeFaceters();
+}
+FacetDriver.prototype = {
+  /**
+   * Populate |this.faceters| with a set of faceters appropriate to the noun
+   *  definition associated with this instance.
+   */
+  _makeFaceters: function() {
+    let faceters = this.faceters = [];
+    for each (let [, attrDef] in Iterator(this.nounDef.attribsByBoundName)) {
+      // ignore attributes that do not want to be faceted
+      if (!attrDef.facet)
+        continue;
+
+      let facetType = attrDef.facet.type;
+
+      if (attrDef.singular) {
+        if (facetType == "date")
+          faceters.push(new DateFaceter(attrDef));
+        else
+          faceters.push(new DiscreteFaceter(attrDef));
+      }
+      else {
+        if (facetType == "nonempty?")
+          faceters.push(new NonEmptySetFaceter(attrDef));
+        else
+          faceters.push(new DiscreteSetFaceter(attrDef));
+      }
+    }
+  },
+  /**
+   * Asynchronously facet the provided items, calling the provided callback when
+   *  completed.
+   */
+  go: function FacetDriver_go(aItems, aCallback, aCallbackThis) {
+    this.items = aItems;
+    this.callback = aCallback;
+    this.callbackThis = aCallbackThis;
+
+    this._nextFaceter = 0;
+    this._drive();
+  },
+
+  _MAX_FACETING_TIMESLICE_MS: 100,
+  _FACETING_YIELD_DURATION_MS: 0,
+  _driveWrapper: function(aThis) {
+    aThis._drive();
+  },
+  _drive: function() {
+    let start = Date.now();
+
+    while (this._nextFaceter < this.faceters.length) {
+      let faceter = this.faceters[this._nextFaceter++];
+      // for now we facet in one go, but the long-term plan allows for them to
+      //  be generators.
+      faceter.facetItems(this.items);
+
+      let delta = Date.now() - start;
+      if (delta > this._MAX_FACETING_TIMESLICE_MS) {
+        this._window.setTimeout(this._driveWrapper,
+                                this._FACETING_YIELD_DURATION_MS,
+                                this);
+        return;
+      }
+    }
+
+    // we only get here once we are done with the faceters
+    this.callback.call(this.callbackThis);
+  }
+};
+
+var FacetUtils = {
+  _groupSizeComparator: function(a, b) {
+    return b[1].length - a[1].length;
+  },
+
+  /**
+   * Given a list where each entry is a tuple of [group object, list of items
+   *  belonging to that group], produce a new list of the top grouped items plus
+   *  an "other" category.
+   *
+   * @param aAttrDef The attribute for the facet we are working with.
+   * @param aGroups The list of groups built for the facet.
+   * @param aMaxCount The number of result rows you want back.  We will provide
+   *     the aMaxCount-1 rows plus an "other" row which we put last.
+   * @param aOtherSentinel An object to use as the other sentinel object and the
+   *     count of the number of groups that went into the other group.  The
+   *     count is found in the 'count' attribute of this object.
+   */
+  makeTopGroups: function FacetUtils_makeTopGroups(aAttrDef, aGroups,
+                                                   aMaxCount, aOtherSentinel) {
+    let nounDef = aAttrDef.objectNounDef;
+    let realGroupsToUse = aMaxCount - 1;
+
+    let orderedBySize = aGroups.concat();
+    orderedBySize.sort(this._groupSizeComparator);
+
+    // - get the real groups to use and order them by the attribute comparator
+    let outGroups = orderedBySize.slice(0, realGroupsToUse);
+    let comparator = nounDef.comparator;
+    function comparatorHelper(a, b) {
+      return comparator(a[0], b[0]);
+    }
+    outGroups.sort(comparatorHelper);
+
+    // - build the 'other' group
+    let otherItems, otherGroupValues;
+    // If the attribute is singular, we can just concatenate everybody together
+    if (aAttrDef.singular) {
+      // Since concat can take multiple arrays, build a list of all of the items
+      //  except for the first one who we will issue the concat call against.
+      //  (We could also just use an empty array as the base.)
+      let iGroup = realGroupsToUse;
+      otherGroupValues = [orderedBySize[iGroup][0]];
+      let firstItemList = orderedBySize[iGroup++][1];
+      let otherItemLists = [];
+      for (; iGroup < orderedBySize.length; iGroup++) {
+        otherGroupValues.push(orderedBySize[iGroup][0]);
+        otherItemLists.push(orderedBySize[iGroup][1]);
+      }
+
+      otherItems = firstItemList.concat.apply(firstItemList, otherItemLists);
+    }
+    // For non-singular attributes, we need to uniqify the contents.  If we
+    //  naively concatenated all the items, we might end up with duplicates.
+    else {
+      let idsSeen = {};
+      otherItems = [];
+      otherGroupValues = [];
+      for (let iGroup = realGroupsToUse; iGroup < orderedBySize.length;
+           iGroup++) {
+        otherGroupValues.push(orderedBySize[iGroup][0]);
+        for each (let [, item] in Iterator(orderedBySize[iGroup][1])) {
+          if (!(item.id in idsSeen)) {
+            idsSeen[item.id] = true;
+            otherItems.push(item);
+          }
+        }
+      }
+    }
+
+    aOtherSentinel.count = orderedBySize.length - realGroupsToUse;
+    aOtherSentinel.groupValues = otherGroupValues;
+    outGroups.push([aOtherSentinel, otherItems]);
+
+    return outGroups;
+  }
+};
+
+/**
+ * Facet discrete things like message authors, boolean values, etc.  Only
+ *  appropriate for use on singular values.  Use |DiscreteSetFaceter| for
+ *  non-singular values.
+ */
+function DiscreteFaceter(aAttrDef) {
+  this.attrDef = aAttrDef;
+}
+DiscreteFaceter.prototype = {
+  type: "discrete",
+  /**
+   * Facet the given set of items, deferring to the appropriate helper method
+   */
+  facetItems: function(aItems) {
+    if (this.attrDef.objectNounDef.isPrimitive)
+      return this.facetPrimitiveItems(aItems);
+    else
+      return this.facetComplexItems(aItems);
+  },
+  /**
+   * Facet an attribute whose value is primitive, meaning that it is a raw
+   *  numeric value or string, rather than a complex object.
+   */
+  facetPrimitiveItems: function(aItems) {
+    let attrKey = this.attrDef.boundName;
+    let nounDef = this.attrDef.objectNounDef;
+
+    let valStrToVal = {};
+    let groups = this.groups = {};
+    this.groupCount = 0;
+
+    for each (let [, item] in Iterator(aItems)) {
+      let val = (attrKey in item) ? item[attrKey] : null;
+      if (val in groups)
+        groups[val].push(item);
+      else {
+        groups[val] = [item];
+        valStrToVal[val] = val;
+        this.groupCount++;
+      }
+    }
+
+    let orderedGroups = [[valStrToVal[key], items] for each
+                         ([key, items] in Iterator(groups))];
+    let comparator = nounDef.comparator;
+    function comparatorHelper(a, b) {
+      return comparator(a[0], b[0]);
+    }
+    orderedGroups.sort(comparatorHelper);
+    this.orderedGroups = orderedGroups;
+  },
+  /**
+   * Facet an attribute whose value is a complex object that can be identified
+   *  by its 'id' attribute.  This is the case where the value is itself a noun
+   *  instance.
+   */
+  facetComplexItems: function(aItems) {
+    let attrKey = this.attrDef.boundName;
+    let nounDef = this.attrDef.objectNounDef;
+    let idAttr = this.attrDef.facet.groupIdAttr;
+
+    let groups = this.groups = {};
+    let groupMap = this.groupMap = {};
+    this.groupCount = 0;
+
+    for each (let [, item] in Iterator(aItems)) {
+      let val = (attrKey in item) ? item[attrKey] : null;
+      let valId = (val == null) ? null : val[idAttr];
+      if (valId in groupMap) {
+        groups[valId].push(item);
+      }
+      else {
+        groupMap[valId] = val;
+        groups[valId] = [item];
+        this.groupCount++;
+      }
+    }
+
+    let orderedGroups = [[groupMap[key], items] for each
+                         ([key, items] in Iterator(groups))];
+    let comparator = nounDef.comparator;
+    function comparatorHelper(a, b) {
+      return comparator(a[0], b[0]);
+    }
+    orderedGroups.sort(comparatorHelper);
+    this.orderedGroups = orderedGroups;
+  },
+};
+
+/**
+ * Facet sets of discrete items.  For example, tags applied to messages.
+ *
+ * The main differences between us and |DiscreteFaceter| are:
+ * - The empty set is notable.
+ * - Specific set configurations could be interesting, but are not low-hanging
+ *    fruit.
+ */
+function DiscreteSetFaceter(aAttrDef) {
+  this.attrDef = aAttrDef;
+}
+DiscreteSetFaceter.prototype = {
+  type: "discrete",
+  /**
+   * Facet the given set of items, deferring to the appropriate helper method
+   */
+  facetItems: function(aItems) {
+    if (this.attrDef.objectNounDef.isPrimitive)
+      return this.facetPrimitiveItems(aItems);
+    else
+      return this.facetComplexItems(aItems);
+  },
+  /**
+   * Facet an attribute whose value is primitive, meaning that it is a raw
+   *  numeric value or string, rather than a complex object.
+   */
+  facetPrimitiveItems: function(aItems) {
+    let attrKey = this.attrDef.boundName;
+    let nounDef = this.attrDef.objectNounDef;
+
+    let groups = this.groups = {};
+    let valStrToVal = {};
+    this.groupCount = 0;
+
+    for each (let [, item] in Iterator(aItems)) {
+      let vals = (attrKey in item) ? item[attrKey] : null;
+      if (vals == null || vals.length == 0) {
+        vals = [null];
+      }
+      for each (let [, val] in Iterator(vals)) {
+        if (val in groups)
+          groups[val].push(item);
+        else {
+          groups[val] = [item];
+          valStrToVal[val] = val;
+          this.groupCount++;
+        }
+      }
+    }
+
+    let orderedGroups = [[valStrToVal[key], items] for each
+                         ([key, items] in Iterator(groups))];
+    let comparator = nounDef.comparator;
+    function comparatorHelper(a, b) {
+      return comparator(a[0], b[0]);
+    }
+    orderedGroups.sort(comparatorHelper);
+    this.orderedGroups = orderedGroups;
+  },
+  /**
+   * Facet an attribute whose value is a complex object that can be identified
+   *  by its 'id' attribute.  This is the case where the value is itself a noun
+   *  instance.
+   */
+  facetComplexItems: function(aItems) {
+    let attrKey = this.attrDef.boundName;
+    let nounDef = this.attrDef.objectNounDef;
+    let idAttr = this.attrDef.facet.groupIdAttr;
+
+    let groups = this.groups = {};
+    let groupMap = this.groupMap = {};
+    this.groupCount = 0;
+
+    for each (let [, item] in Iterator(aItems)) {
+      let vals = (attrKey in item) ? item[attrKey] : null;
+      if (vals == null || vals.length == 0) {
+        vals = [null];
+      }
+      for each (let [, val] in Iterator(vals)) {
+        let valId = (val == null) ? null : val[idAttr];
+        if (valId in groupMap) {
+          groups[valId].push(item);
+        }
+        else {
+          groupMap[valId] = val;
+          groups[valId] = [item];
+          this.groupCount++;
+        }
+      }
+    }
+
+    let orderedGroups = [[groupMap[key], items] for each
+                         ([key, items] in Iterator(groups))];
+    let comparator = nounDef.comparator;
+    function comparatorHelper(a, b) {
+      return comparator(a[0], b[0]);
+    }
+    orderedGroups.sort(comparatorHelper);
+    this.orderedGroups = orderedGroups;
+  },
+};
+
+/**
+ * Given a non-singular attribute, facet it as if it were a boolean based on
+ *  whether there is anything in the list (set).
+ */
+function NonEmptySetFaceter(aAttrDef) {
+  this.attrDef = aAttrDef;
+}
+NonEmptySetFaceter.prototype = {
+  type: "boolean",
+  /**
+   * Facet the given set of items, deferring to the appropriate helper method
+   */
+  facetItems: function(aItems) {
+    let attrKey = this.attrDef.boundName;
+    let nounDef = this.attrDef.objectNounDef;
+
+    let trueValues = [];
+    let falseValues = [];
+
+    let groups = this.groups = {};
+    this.groupCount = 0;
+
+    for each (let [, item] in Iterator(aItems)) {
+      let vals = (attrKey in item) ? item[attrKey] : null;
+      if (vals == null || vals.length == 0)
+        falseValues.push(item);
+      else
+        trueValues.push(item);
+    }
+
+    this.orderedGroups = [];
+    if (trueValues.length)
+      this.orderedGroups.push([true, trueValues]);
+    if (falseValues.length)
+      this.orderedGroups.push([false, falseValues]);
+    this.groupCount = this.orderedGroups.length;
+  },
+  makeQuery: function(aGroupValues, aInclusive) {
+    let query = this.query = Gloda.newQuery(Gloda.NOUN_MESSAGE);
+
+    let constraintFunc = query[this.attrDef.boundName];
+    constraintFunc.call(query);
+
+    // Our query is always for non-empty lists (at this time), so we want to
+    //  invert if they're excluding 'true' or including 'false', which means !=.
+    let invert = aGroupValues[0] != aInclusive;
+
+    return [query, invert];
+  }
+};
+
+
+/**
+ * Facet dates.  We build a hierarchical nested structure of year, month, and
+ *  day nesting levels.  This decision was made speculatively in the hopes that
+ *  it would allow us to do clustered analysis and that there might be a benefit
+ *  for that.  For example, if you search for "Christmas", we might notice
+ *  clusters of messages around December of each year.  We could then present
+ *  these in a list as likely candidates, rather than a graphical timeline.
+ *  Alternately, it could be used to inform a non-linear visualization.  As it
+ *  stands (as of this writing), it's just a complicating factor.
+ */
+function DateFaceter(aAttrDef) {
+  this.attrDef = aAttrDef;
+}
+DateFaceter.prototype = {
+  type: "date",
+  /**
+   *
+   */
+  facetItems: function(aItems) {
+    let attrKey = this.attrDef.boundName;
+    let nounDef = this.attrDef.objectNounDef;
+
+    let years = this.years = {_subCount: 0};
+    // generally track the time range
+    let oldest = null, newest = null;
+
+    let validItems = this.validItems = [];
+
+    // just cheat and put us at the front...
+    this.groupCount = aItems.length ? 1000 : 0;
+    this.orderedGroups = null;
+
+    /** The number of items with a null/missing attribute. */
+    this.missing = 0;
+
+    /**
+     * The number of items with a date that is unreasonably far in the past or
+     *  in the future.  Old-wise, we are concerned about incorrectly formatted
+     *  messages (spam) that end up placed around the UNIX epoch.  New-wise,
+     *  we are concerned about messages that can't be explained by users who
+     *  don't know how to set their clocks (both the current user and people
+     *  sending them mail), mainly meaning spam.
+     * We want to avoid having our clever time-scale logic being made useless by
+     *  these unreasonable messages.
+     */
+    this.unreasonable = 0;
+    // feb 1, 1970
+    let tooOld = new Date(1970, 1, 1);
+    // 3 days from now
+    let tooNew = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000);
+
+    for each (let [, item] in Iterator(aItems)) {
+      let val = (attrKey in item) ? item[attrKey] : null;
+      // -- missing
+      if (val == null) {
+        this.missing++;
+        continue;
+      }
+
+      // -- unreasonable
+      if (val < tooOld || val > tooNew) {
+        this.unreasonable++;
+        continue;
+      }
+
+      this.validItems.push(item);
+
+      // -- time range
+      if (oldest == null)
+        oldest = newest = val;
+      else if (val < oldest)
+        oldest = val;
+      else if (val > newest)
+        newest = val;
+
+      // -- bucket
+      // - year
+      let year, valYear = val.getYear();
+      if (valYear in years) {
+        year = years[valYear];
+        year._dateCount++;
+      }
+      else {
+        year = years[valYear] = {
+          _dateCount: 1,
+          _subCount: 0
+        };
+        years._subCount++;
+      }
+
+      // - month
+      let month, valMonth = val.getMonth();
+      if (valMonth in year) {
+        month = year[valMonth];
+        month._dateCount++;
+      }
+      else {
+        month = year[valMonth] = {
+          _dateCount: 1,
+          _subCount: 0
+        };
+        year._subCount++;
+      }
+
+      // - day
+      let valDate = val.getDate();
+      if (valDate in month) {
+        month[valDate].push(item);
+      }
+      else {
+        month[valDate] = [item];
+      }
+    }
+
+    this.oldest = oldest;
+    this.newest = newest;
+  },
+
+  _unionMonth: function(aMonthObj) {
+    let dayItemLists = [];
+    for each (let [key, dayItemList] in Iterator(aMonthObj)) {
+      if (typeof(key) == "string" && key[0] == '_')
+        continue;
+      dayItemLists.push(dayItemList);
+    }
+    return Array.concat.apply([], dayItemLists);
+  },
+
+  _unionYear: function(aYearObj) {
+    let monthItemLists = [];
+    for each (let [key, monthObj] in Iterator(aYearObj)) {
+      if (typeof(key) == "string" && key[0] == '_')
+        continue;
+      monthItemLists.push(this._unionMonth(monthObj));
+    }
+    return Array.concat.apply([], monthItemLists);
+  }
+};
--- a/mailnews/db/gloda/modules/fundattr.js
+++ b/mailnews/db/gloda/modules/fundattr.js
@@ -38,16 +38,17 @@
 EXPORTED_SYMBOLS = ['GlodaFundAttr'];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://app/modules/gloda/log4moz.js");
+Cu.import("resource://app/modules/StringBundle.js");
 
 Cu.import("resource://app/modules/gloda/utils.js");
 Cu.import("resource://app/modules/gloda/gloda.js");
 Cu.import("resource://app/modules/gloda/datastore.js");
 
 Cu.import("resource://app/modules/gloda/noun_mimetype.js");
 
 
@@ -55,57 +56,49 @@ Cu.import("resource://app/modules/gloda/
  * @namespace The Gloda Fundamental Attribute provider is a special attribute
  *  provider; it provides attributes that the rest of the providers should be
  *  able to assume exist.  Also, it may end up accessing things at a lower level
  *  than most extension providers should do.  In summary, don't mimic this code
  *  unless you won't complain when your code breaks.
  */
 var GlodaFundAttr = {
   providerName: "gloda.fundattr",
+  strings: new StringBundle("chrome://messenger/locale/gloda.properties"),
   _log: null,
 
   init: function gloda_explattr_init() {
     this._log =  Log4Moz.repository.getLogger("gloda.fundattr");
 
     try {
       this.defineAttributes();
     }
     catch (ex) {
       this._log.error("Error in init: " + ex);
       throw ex;
     }
   },
 
   POPULARITY_FROM_ME_TO: 10,
   POPULARITY_FROM_ME_CC: 4,
+  POPULARITY_FROM_ME_BCC: 3,
   POPULARITY_TO_ME: 5,
   POPULARITY_CC_ME: 1,
+  POPULARITY_BCC_ME: 1,
 
   /** Boost for messages 'I' sent */
   NOTABILITY_FROM_ME: 10,
   /** Boost for messages involving 'me'. */
   NOTABILITY_INVOLVING_ME: 1,
   /** Boost for message from someone in 'my' address book. */
   NOTABILITY_FROM_IN_ADDR_BOOK: 10,
   /** Boost for the first person involved in my address book. */
   NOTABILITY_INVOLVING_ADDR_BOOK_FIRST: 8,
   /** Boost for each additional person involved in my address book. */
   NOTABILITY_INVOLVING_ADDR_BOOK_ADDL: 2,
 
-  _attrConvSubject: null,
-  _attrFolder: null,
-  _attrBody: null,
-  _attrFrom: null,
-  _attrFromMe: null,
-  _attrTo: null,
-  _attrToMe: null,
-  _attrCc: null,
-  _attrCcMe: null,
-  _attrDate: null,
-
   defineAttributes: function() {
     /* ***** Conversations ***** */
     // conversation: subjectMatches
     this._attrConvSubject = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrDerived,
       attributeName: "subjectMatches",
@@ -119,16 +112,17 @@ var GlodaFundAttr = {
     /* ***** Messages ***** */
     // folder
     this._attrFolder = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrFundamental,
       attributeName: "folder",
       singular: true,
+      facet: true,
       special: Gloda.kSpecialColumn,
       specialColumnName: "folderID",
       subjectNouns: [Gloda.NOUN_MESSAGE],
       objectNoun: Gloda.NOUN_FOLDER,
       }); // tested-by: test_attributes_fundamental
     this._attrFolder = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
@@ -267,92 +261,118 @@ var GlodaFundAttr = {
                         provider: this,
                         extensionName: Gloda.BUILT_IN,
                         attributeType: Gloda.kAttrFundamental,
                         attributeName: "cc",
                         singular: false,
                         subjectNouns: [Gloda.NOUN_MESSAGE],
                         objectNoun: Gloda.NOUN_IDENTITY,
                         }); // not-tested
+    /**
+     * Bcc'ed recipients; only makes sense for sent messages.
+     */
+    this._attrBcc = Gloda.defineAttribute({
+      provider: this,
+      extensionName: Gloda.BUILT_IN,
+      attributeType: Gloda.kAttrFundamental,
+      attributeName: "bcc",
+      singular: false,
+      facet: true,
+      subjectNouns: [Gloda.NOUN_MESSAGE],
+      objectNoun: Gloda.NOUN_IDENTITY,
+    }); // not-tested
 
     // Date.  now lives on the row.
     this._attrDate = Gloda.defineAttribute({
                         provider: this,
                         extensionName: Gloda.BUILT_IN,
                         attributeType: Gloda.kAttrFundamental,
                         attributeName: "date",
                         singular: true,
+                        facet: {
+                          type: "date",
+                        },
                         special: Gloda.kSpecialColumn,
                         specialColumnName: "date",
                         subjectNouns: [Gloda.NOUN_MESSAGE],
                         objectNoun: Gloda.NOUN_DATE,
                         }); // tested-by: test_attributes_fundamental
 
     // Attachment MIME Types
     this._attrAttachmentTypes = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrFundamental,
       attributeName: "attachmentTypes",
       singular: false,
+      facet: {
+        type: "default",
+        // This will group the MIME types by their category.
+        groupIdAttr: "category",
+        queryHelper: "Category",
+      },
       subjectNouns: [Gloda.NOUN_MESSAGE],
       objectNoun: Gloda.NOUN_MIME_TYPE,
       });
 
     // --- Optimization
-    // Involves.  Means any of from/to/cc.  The queries get ugly enough without
-    //   this that it seems to justify the cost, especially given the frequent
-    //   use case.  (In fact, post-filtering for the specific from/to/cc is
-    //   probably justifiable rather than losing this attribute...)
+    /**
+     * Involves means any of from/to/cc/bcc.  The queries get ugly enough
+     *  without this that it seems to justify the cost, especially given the
+     *  frequent use case.  (In fact, post-filtering for the specific from/to/cc
+     *  is probably justifiable rather than losing this attribute...)
+     */
     this._attrInvolves = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrOptimization,
       attributeName: "involves",
       singular: false,
+      facet: true,
       subjectNouns: [Gloda.NOUN_MESSAGE],
       objectNoun: Gloda.NOUN_IDENTITY,
       }); // not-tested
 
-    // From Me To
-    this._attrFromMeTo = Gloda.defineAttribute({
+    /**
+     * Any of to/cc/bcc.
+     */
+    this._attrRecipients = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrOptimization,
-      attributeName: "fromMeTo",
+      attributeName: "recipients",
       singular: false,
       subjectNouns: [Gloda.NOUN_MESSAGE],
+      objectNoun: Gloda.NOUN_IDENTITY,
+      }); // not-tested
+
+    // From Me (To/Cc/Bcc)
+    this._attrFromMe = Gloda.defineAttribute({
+      provider: this,
+      extensionName: Gloda.BUILT_IN,
+      attributeType: Gloda.kAttrOptimization,
+      attributeName: "fromMe",
+      singular: false,
+      // The interesting thing to a facet is whether the message is from me.
+      facet: {
+        type: "nonempty?"
+      },
+      subjectNouns: [Gloda.NOUN_MESSAGE],
       objectNoun: Gloda.NOUN_PARAM_IDENTITY,
       }); // not-tested
-    // From Me Cc
-    this._attrFromMeCc = Gloda.defineAttribute({
-      provider: this,
-      extensionName: Gloda.BUILT_IN,
-      attributeType: Gloda.kAttrOptimization,
-      attributeName: "fromMeCc",
-      singular: false,
-      subjectNouns: [Gloda.NOUN_MESSAGE],
-      objectNoun: Gloda.NOUN_PARAM_IDENTITY,
-      }); // not-tested
-    // To Me
+    // To/Cc/Bcc Me
     this._attrToMe = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrFundamental,
       attributeName: "toMe",
-      singular: false,
-      subjectNouns: [Gloda.NOUN_MESSAGE],
-      objectNoun: Gloda.NOUN_PARAM_IDENTITY,
-      }); // not-tested
-    // Cc Me
-    this._attrCcMe = Gloda.defineAttribute({
-      provider: this,
-      extensionName: Gloda.BUILT_IN,
-      attributeType: Gloda.kAttrFundamental,
-      attributeName: "ccMe",
+      // The interesting thing to a facet is whether the message is to me.
+      facet: {
+        type: "nonempty?"
+      },
       singular: false,
       subjectNouns: [Gloda.NOUN_MESSAGE],
       objectNoun: Gloda.NOUN_PARAM_IDENTITY,
       }); // not-tested
 
 
     // -- Mailing List
     // Non-singular, but a hard call.  Namely, it is obvious that a message can
@@ -372,21 +392,24 @@ var GlodaFundAttr = {
     //  by the user.
     this._attrList = Gloda.defineAttribute({
                         provider: this,
                         extensionName: Gloda.BUILT_IN,
                         attributeType: Gloda.kAttrFundamental,
                         attributeName: "mailing-list",
                         bindName: "mailingLists",
                         singular: false,
+                        facet: true,
                         subjectNouns: [Gloda.NOUN_MESSAGE],
                         objectNoun: Gloda.NOUN_IDENTITY,
                         }); // not-tested, not-implemented
   },
 
+  RE_LIST_POST: /<mailto:([^>]+)>/,
+
   /**
    *
    * Specializations:
    * - Mailing Lists.  Replies to a message on a mailing list frequently only
    *   have the list-serve as the 'to', so we try to generate a synthetic 'to'
    *   based on the author of the parent message when possible.  (The 'possible'
    *   part is that we may not have a copy of the parent message at the time of
    *   processing.)
@@ -408,33 +431,47 @@ var GlodaFundAttr = {
       author = aMsgHdr.getStringProperty("replyTo");
     }
     catch (ex) {
     }
     */
     if (author == null || author == "")
       author = aMsgHdr.mime2DecodedAuthor;
 
-    let [authorIdentities, toIdentities, ccIdentities] =
+    let normalizedListPost = "";
+    if (aMimeMsg.has("list-post")) {
+      let match = this.RE_LIST_POST.exec(aMimeMsg.get("list-post"));
+      if (match)
+        normalizedListPost = "<" + match[1] + ">";
+    }
+
+    let [authorIdentities, toIdentities, ccIdentities, bccIdentities,
+         listIdentities] =
       yield aCallbackHandle.pushAndGo(
         Gloda.getOrCreateMailIdentities(aCallbackHandle,
                                         author, aMsgHdr.mime2DecodedRecipients,
-                                        aMsgHdr.ccList));
+                                        aMsgHdr.ccList, aMsgHdr.bccList,
+                                        normalizedListPost));
 
     if (authorIdentities.length == 0) {
       this._log.error("Message with subject '" + aMsgHdr.mime2DecodedSubject +
                       "' somehow lacks a valid author.  Bailing.");
       return; // being a generator, this generates an exception; we like.
     }
     let authorIdentity = authorIdentities[0];
     aGlodaMessage.from = authorIdentity;
 
-    // -- To, Cc
+    // -- To, Cc, Bcc
     aGlodaMessage.to = toIdentities;
     aGlodaMessage.cc = ccIdentities;
+    aGlodaMessage.bcc = bccIdentities;
+
+    // -- Mailing List
+    if (listIdentities.length)
+      aGlodaMessage.mailingLists = listIdentities;
 
     // -- Attachments
     let attachmentTypes = [];
     for each (let [, attachment] in Iterator(aMimeMsg.allAttachments)) {
       // We don't care about would-be attachments that are not user-intended
       //  attachments but rather artifacts of the message content.
       // We also want to avoid dealing with obviously bogus mime types.
       //  (If you don't have a "/", you are probably bogus.)
@@ -455,24 +492,24 @@ var GlodaFundAttr = {
     yield Gloda.kWorkDone;
   },
 
   optimize: function gloda_fundattr_optimize(aGlodaMessage, aRawReps,
       aIsNew, aCallbackHandle) {
 
     let aMsgHdr = aRawReps.header;
 
+    // for simplicity this is used for both involves and recipients
     let involvesIdentities = {};
     let involves = aGlodaMessage.involves || [];
+    let recipients = aGlodaMessage.recipients || [];
 
-    // me specialization optimizations
+    // 'me' specialization optimizations
     let toMe = aGlodaMessage.toMe || [];
-    let fromMeTo = aGlodaMessage.fromMeTo || [];
-    let ccMe = aGlodaMessage.ccMe || [];
-    let fromMeCc = aGlodaMessage.fromMeCc || [];
+    let fromMe = aGlodaMessage.fromMe || [];
 
     let myIdentities = Gloda.myIdentities; // needless optimization?
     let authorIdentity = aGlodaMessage.from;
     let isFromMe = authorIdentity.id in myIdentities;
 
     // The fulltext search column for the author.  We want to have in here:
     // - The e-mail address and display name as enclosed on the message.
     // - The name per the address book card for this e-mail address, if we have
@@ -495,16 +532,17 @@ var GlodaFundAttr = {
     involves.push(authorIdentity);
     involvesIdentities[authorIdentity.id] = true;
 
     let involvedAddrBookCount = 0;
 
     for each (let [,toIdentity] in Iterator(aGlodaMessage.to)) {
       if (!(toIdentity.id in involvesIdentities)) {
         involves.push(toIdentity);
+        recipients.push(toIdentity);
         involvesIdentities[toIdentity.id] = true;
         let toCard = toIdentity.abCard;
         if (toCard) {
           involvedAddrBookCount++;
           // @testpoint gloda.noun.message.attr.recipientsMatch
           aGlodaMessage._indexRecipients += ' ' + toCard.displayName;
         }
       }
@@ -512,63 +550,89 @@ var GlodaFundAttr = {
       // optimization attribute to-me ('I' am the parameter)
       if (toIdentity.id in myIdentities) {
         toMe.push([toIdentity, authorIdentity]);
         if (aIsNew)
           authorIdentity.contact.popularity += this.POPULARITY_TO_ME;
       }
       // optimization attribute from-me-to ('I' am the parameter)
       if (isFromMe) {
-        fromMeTo.push([authorIdentity, toIdentity]);
+        fromMe.push([authorIdentity, toIdentity]);
         // also, popularity
         if (aIsNew)
           toIdentity.contact.popularity += this.POPULARITY_FROM_ME_TO;
       }
     }
     for each (let [,ccIdentity] in Iterator(aGlodaMessage.cc)) {
       if (!(ccIdentity.id in involvesIdentities)) {
         involves.push(ccIdentity);
+        recipients.push(ccIdentity);
         involvesIdentities[ccIdentity.id] = true;
         let ccCard = ccIdentity.abCard;
         if (ccCard) {
           involvedAddrBookCount++;
           // @testpoint gloda.noun.message.attr.recipientsMatch
           aGlodaMessage._indexRecipients += ' ' + ccCard.displayName;
         }
       }
       // optimization attribute cc-me ('I' am the parameter)
       if (ccIdentity.id in myIdentities) {
-        ccMe.push([ccIdentity, authorIdentity]);
+        toMe.push([ccIdentity, authorIdentity]);
         if (aIsNew)
           authorIdentity.contact.popularity += this.POPULARITY_CC_ME;
       }
       // optimization attribute from-me-to ('I' am the parameter)
       if (isFromMe) {
-        fromMeCc.push([authorIdentity, ccIdentity]);
+        fromMe.push([authorIdentity, ccIdentity]);
         // also, popularity
         if (aIsNew)
           ccIdentity.contact.popularity += this.POPULARITY_FROM_ME_CC;
       }
     }
+    // just treat bcc like cc; the intent is the same although the exact
+    //  semantics differ.
+    for each (let [,bccIdentity] in Iterator(aGlodaMessage.bcc)) {
+      if (!(bccIdentity.id in involvesIdentities)) {
+        involves.push(bccIdentity);
+        recipients.push(bccIdentity);
+        involvesIdentities[bccIdentity.id] = true;
+        let bccCard = bccIdentity.abCard;
+        if (bccCard) {
+          involvedAddrBookCount++;
+          // @testpoint gloda.noun.message.attr.recipientsMatch
+          aGlodaMessage._indexRecipients += ' ' + bccCard.displayName;
+        }
+      }
+      // optimization attribute cc-me ('I' am the parameter)
+      if (bccIdentity.id in myIdentities) {
+        toMe.push([bccIdentity, authorIdentity]);
+        if (aIsNew)
+          authorIdentity.contact.popularity += this.POPULARITY_BCC_ME;
+      }
+      // optimization attribute from-me-to ('I' am the parameter)
+      if (isFromMe) {
+        fromMe.push([authorIdentity, bccIdentity]);
+        // also, popularity
+        if (aIsNew)
+          bccIdentity.contact.popularity += this.POPULARITY_FROM_ME_BCC;
+      }
+    }
 
     if (involvedAddrBookCount)
       aGlodaMessage.notability += this.NOTABILITY_INVOLVING_ADDR_BOOK_FIRST +
         (involvedAddrBookCount - 1) * this.NOTABILITY_INVOLVING_ADDR_BOOK_ADDL;
 
     aGlodaMessage.involves = involves;
+    aGlodaMessage.recipients = recipients;
     if (toMe.length) {
       aGlodaMessage.toMe = toMe;
       aGlodaMessage.notability += this.NOTABILITY_INVOLVING_ME;
     }
-    if (fromMeTo.length)
-      aGlodaMessage.fromMeTo = fromMeTo;
-    if (ccMe.length)
-      aGlodaMessage.ccMe = ccMe;
-    if (fromMeCc.length)
-      aGlodaMessage.fromMeCc = fromMeCc;
+    if (fromMe.length)
+      aGlodaMessage.fromMe = fromMe;
 
     if (aRawReps.bodyLines &&
         this.contentWhittle({}, aRawReps.bodyLines, aRawReps.content)) {
       // we were going to do something here?
     }
 
     yield Gloda.kWorkDone;
   },
--- a/mailnews/db/gloda/modules/gloda.js
+++ b/mailnews/db/gloda/modules/gloda.js
@@ -346,20 +346,26 @@ var Gloda = {
    * If the identities need to be created, they will also result in the
    *  creation of a gloda contact.  If a display name was provided with the
    *  e-mail address, it will become the name of the gloda contact.  If a
    *  display name was not provided, the e-mail address will also serve as the
    *  contact name.
    * This method uses the indexer's callback handle mechanism, and does not
    *  obey traditional return semantics.
    *
+   * We normalize all e-mail addresses to be lowercase as a normative measure.
+   *
+   * @param aCallbackHandle The GlodaIndexer callback handle (or equivalent)
+   *   that you are operating under.
    * @param ... One or more strings.  Each string can contain zero or more
    *   e-mail addresses with display name.  If more than one address is given,
    *   they should be comma-delimited.  For example
-   *   '"Bob Smith" <bob@smith.com>' is an address with display name.
+   *   '"Bob Smith" <bob@smith.com>' is an address with display name.  Mime
+   *   header decoding is performed, but is ignorant of any folder-level
+   *   character set overrides.
    * @returns via the callback handle mechanism, a list containing one sub-list
    *   for each string argument passed.  Each sub-list containts zero or more
    *   GlodaIdentity instances corresponding to the addresses provided.
    */
   getOrCreateMailIdentities:
       function gloda_ns_getOrCreateMailIdentities(aCallbackHandle) {
     let addresses = {};
     let resultLists = [];
@@ -369,17 +375,17 @@ var Gloda = {
       let aMailAddresses = arguments[iArg];
       let parsed = GlodaUtils.parseMailAddresses(aMailAddresses);
 
       let resultList = [];
       resultLists.push(resultList);
 
       let identities = [];
       for (let iAddress = 0; iAddress < parsed.count; iAddress++) {
-        let address = parsed.addresses[iAddress];
+        let address = parsed.addresses[iAddress].toLowerCase();
         if (address in addresses)
           addresses[address].push(resultList);
         else
           addresses[address] = [parsed.names[iAddress], resultList];
       }
     }
 
     let addressList = [address for (address in addresses)];
@@ -508,29 +514,29 @@ var Gloda = {
       if (!fallbackName)
         fallbackName = msgIdentity.email;
 
       let emailAddress = msgIdentity.email;
       let replyTo = msgIdentity.replyTo;
 
       // find the identities if they exist, flag to create them if they don't
       if (emailAddress) {
-        parsed = GlodaUtils.parseMailAddresses(emailAddress);
+        let parsed = GlodaUtils.parseMailAddresses(emailAddress);
         if (!(parsed.addresses[0] in myEmailAddresses)) {
           let identity = GlodaDatastore.getIdentity("email",
                                                     parsed.addresses[0]);
           if (identity)
             existingIdentities.push(identity);
           else
             identitiesToCreate.push(parsed.addresses[0]);
           myEmailAddresses[parsed.addresses[0]] = true;
         }
       }
       if (replyTo) {
-        parsed = GlodaUtils.parseMailAddresses(replyTo);
+        let parsed = GlodaUtils.parseMailAddresses(replyTo);
         if (!(parsed.addresses[0] in myEmailAddresses)) {
           let identity = GlodaDatastore.getIdentity("email",
                                                     parsed.addresses[0]);
           if (identity)
             existingIdentities.push(identity);
           else
             identitiesToCreate.push(parsed.addresses[0]);
           myEmailAddresses[parsed.addresses[0]] = true;
@@ -775,50 +781,58 @@ var Gloda = {
 
   _managedToJSON: function gloda_ns_managedToJSON(aItem) {
     return aItem.id;
   },
 
   /**
    * Define a noun.  Takes a dictionary with the following keys/values:
    *
-   * @param name The name of the noun.  This is not a display name (anything
-   *     being displayed needs to be localized, after all), but simply the
-   *     canonical name for debugging purposes and for people to pass to
+   * @param aNounDef.name The name of the noun.  This is not a display name
+   *     (anything being displayed needs to be localized, after all), but simply
+   *     the canonical name for debugging purposes and for people to pass to
    *     lookupNoun.  The suggested convention is lower-case-dash-delimited,
    *     with names being singular (since it's a single noun we are referring
    *     to.)
-   * @param class The 'class' to which an instance of the noun will belong (aka
-   *     will pass an instanceof test).
-   * @param allowsArbitraryAttrs Is this a 'first class noun'/can it be a subject, AKA can
-   *     this noun have attributes stored on it that relate it to other things?
-   *     For example, a message is first-class; we store attributes of
-   *     messages.  A date is not first-class now, nor is it likely to be; we
-   *     will not store attributes about a date, although dates will be the
-   *     objects of other subjects.  (For example: we might associate a date
-   *     with a calendar event, but the date is an attribute of the calendar
-   *     event and not vice versa.)
-   * @param usesParameter A boolean indicating whether this noun requires use
-   *     of the 'parameter' BLOB storage field on the attribute bindings in the
-   *     database to persist itself.  Use of parameters should be limited
-   *     to a reasonable number of values (16-32 is okay, more than that is
-   *     pushing it and 256 should be considered an absolute upper bound)
-   *     because of the database organization.  When false, your toParamAndValue
-   *     function is expected to return null for the parameter and likewise your
-   *     fromParamAndValue should expect ignore and generally ignore the
-   *     argument.
-   * @param toParamAndValue A function that takes an instantiated noun
+   * @param aNounDef.class The 'class' to which an instance of the noun will
+   *     belong (aka will pass an instanceof test).  You may also provide this
+   *     as 'clazz' if the keyword makes your IDE angry.
+   * @param aNounDef.allowsArbitraryAttrs Is this a 'first class noun'/can it be
+   *     a subject, AKA can this noun have attributes stored on it that relate
+   *     it to other things?  For example, a message is first-class; we store
+   *     attributes of messages.  A date is not first-class now, nor is it
+   *     likely to be; we will not store attributes about a date, although dates
+   *     will be the objects of other subjects.  (For example: we might
+   *     associate a date with a calendar event, but the date is an attribute of
+   *     the calendar event and not vice versa.)
+   * @param aNounDef.usesParameter A boolean indicating whether this noun
+   *     requires use of the 'parameter' BLOB storage field on the attribute
+   *     bindings in the database to persist itself.  Use of parameters should
+   *     be limited to a reasonable number of values (16-32 is okay, more than
+   *     that is pushing it and 256 should be considered an absolute upper
+   *     bound) because of the database organization.  When false, your
+   *     toParamAndValue function is expected to return null for the parameter
+   *     and likewise your fromParamAndValue should expect ignore and generally
+   *     ignore the argument.
+   * @param aNounDef.toParamAndValue A function that takes an instantiated noun
    *     instance and returns a 2-element list of [parameter, value] where
    *     parameter may only be non-null if you passed a usesParameter of true.
    *     Parameter may be of any type (BLOB), and value must be numeric (pass
    *     0 if you don't need the value).
    *
-   * @param schema Unsupported mechanism by which you can define a table that
-   *     corresponds to this noun.  The table will be created if it does not
-   *     exist.
+   * @param aNounDef.isPrimitive True when the noun instance is a raw numeric
+   *     value/string/boolean.  False when the instance is an object.  When
+   *     false, it is assumed the attribute that serves as a unique identifier
+   *     for the value is "id" unless 'idAttr' is provided.
+   * @param [aNounDef.idAttr="id"] For non-primitive nouns, this is the
+   *     attribute on the object that uniquely identifies it.
+   *
+   * @param aNounDef.schema Unsupported mechanism by which you can define a
+   *     table that corresponds to this noun.  The table will be created if it
+   *     does not exist.
    *     - name The table name; don't conflict with other things!
    *     - columns A list of [column name, sqlite type] tuples.  You should
    *       always include a definition like ["id", "INTEGER PRIMARY KEY"] for
    *       now (and it should be the first column name too.)  If you care about
    *       how the attributes are poked into your object (for example, you want
    *       underscores used for some of them because the attributes should be
    *       immutable), then you can include a third string that is the name of
    *       the attribute to use.
@@ -832,16 +846,25 @@ var Gloda = {
       aNounID = this._nextNounID++;
     aNounDef.id = aNounID;
 
     // Let people whose editors get angry about illegal attribute names use
     //  clazz instead of class.
     if (aNounDef.clazz)
       aNounDef.class = aNounDef.clazz;
 
+    if (!("idAttr" in aNounDef))
+      aNounDef.idAttr = "id";
+    if (!("comparator" in aNounDef)) {
+      aNounDef.comparator = function() {
+        throw new Error("Noun type '" + aNounDef.name +
+                        "' lacks a real comparator.");
+      };
+    }
+
     // We allow nouns to have data tables associated with them where we do all
     //  the legwork.  The schema attribute is the gateway to this magical world
     //  of functionality.  Said door is officially unsupported.
     if (aNounDef.schema) {
       if (aNounDef.schema.name)
         aNounDef.tableName = "ext_" + aNounDef.schema.name;
       else
         aNounDef.tableName = "ext_" + aNounDef.name;
@@ -1002,75 +1025,159 @@ var Gloda = {
    *  own noun_*.js files.  There are some trade-offs to be made, and I think
    *  we can deal with those once we start to integrate lightning/calendar and
    *  our noun space gets large and more heterogeneous.
    */
   _initAttributes: function gloda_ns_initAttributes() {
     this.defineNoun({
       name: "bool",
       clazz: Boolean, allowsArbitraryAttrs: false,
+      isPrimitive: true,
+      // favor true before false
+      comparator: function gloda_bool_comparator(a, b) {
+        if (a == null) {
+          if (b == null)
+            return 0;
+          else
+            return 1;
+        }
+        else if (b == null) {
+          return -1;
+        }
+        return b - a;
+      },
       toParamAndValue: function(aBool) {
         return [null, aBool ? 1 : 0];
       }}, this.NOUN_BOOLEAN);
     this.defineNoun({
       name: "number",
       clazz: Number, allowsArbitraryAttrs: false, continuous: true,
+      isPrimitive: true,
+      comparator: function gloda_number_comparator(a, b) {
+        if (a == null) {
+          if (b == null)
+            return 0;
+          else
+            return 1;
+        }
+        else if (b == null) {
+          return -1;
+        }
+        return a - b;
+      },
       toParamAndValue: function(aNum) {
         return [null, aNum];
       }}, this.NOUN_NUMBER);
     this.defineNoun({
       name: "string",
       clazz: String, allowsArbitraryAttrs: false,
+      isPrimitive: true,
+      comparator: function gloda_string_comparator(a, b) {
+        if (a == null) {
+          if (b == null)
+            return 0;
+          else
+            return 1;
+        }
+        else if (b == null) {
+          return -1;
+        }
+        return a.localeCompare(b);
+      },
       toParamAndValue: function(aString) {
         return [null, aString];
       }}, this.NOUN_STRING);
     this.defineNoun({
       name: "date",
       clazz: Date, allowsArbitraryAttrs: false, continuous: true,
+      isPrimitive: true,
+      comparator: function gloda_data_comparator(a, b) {
+        if (a == null) {
+          if (b == null)
+            return 0;
+          else
+            return 1;
+        }
+        else if (b == null) {
+          return -1;
+        }
+        return a - b;
+      },
       toParamAndValue: function(aDate) {
         return [null, aDate.valueOf() * 1000];
       }}, this.NOUN_DATE);
     this.defineNoun({
       name: "fulltext",
       clazz: String, allowsArbitraryAttrs: false, continuous: false,
+      isPrimitive: true,
+      comparator: function gloda_fulltext_comparator(a, b) {
+        throw new Error("Fulltext nouns are not comparable!");
+      },
       // as noted on NOUN_FULLTEXT, we just pass the string around.  it never
       //  hits the database, so it's okay.
       toParamAndValue: function(aString) {
         return [null, aString];
       }}, this.NOUN_FULLTEXT);
 
     this.defineNoun({
       name: "folder",
       clazz: GlodaFolder,
       allowsArbitraryAttrs: false,
+      isPrimitive: false,
+      comparator: function gloda_folder_comparator(a, b) {
+        if (a == null) {
+          if (b == null)
+            return 0;
+          else
+            return 1;
+        }
+        else if (b == null) {
+          return -1;
+        }
+        return a.name.localeCompare(b.name);
+      },
       toParamAndValue: function(aFolderOrGlodaFolder) {
         if (aFolderOrGlodaFolder instanceof GlodaFolder)
           return [null, aFolderOrGlodaFolder.id];
         else
           return [null, GlodaDatastore._mapFolder(aFolderOrGlodaFolder).id];
       }}, this.NOUN_FOLDER);
     this.defineNoun({
       name: "conversation",
       clazz: GlodaConversation,
       allowsArbitraryAttrs: false,
+      isPrimitive: false,
       cache: true, cacheCost: 512,
       tableName: "conversations",
       attrTableName: "messageAttributes", attrIDColumnName: "conversationID",
       datastore: GlodaDatastore,
       objFromRow: GlodaDatastore._conversationFromRow,
+      comparator: function gloda_conversation_comparator(a, b) {
+        if (a == null) {
+          if (b == null)
+            return 0;
+          else
+            return 1;
+        }
+        else if (b == null) {
+          return -1;
+        }
+        return a.subject.localeCompare(b.subject);
+      },
       toParamAndValue: function(aConversation) {
         if (aConversation instanceof GlodaConversation)
           return [null, aConversation.id];
         else // assume they're just passing the id directly
           return [null, aConversation];
       }}, this.NOUN_CONVERSATION);
     this.defineNoun({
       name: "message",
       clazz: GlodaMessage,
       allowsArbitraryAttrs: true,
+      isPrimitive: false,
       cache: true, cacheCost: 2048,
       tableName: "messages",
       // we will always have a fulltext row, even for messages where we don't
       //  have the body available.  this is because we want the subject indexed.
       dbQueryJoinMagic:
         " INNER JOIN messagesText ON messages.id = messagesText.rowid",
       attrTableName: "messageAttributes", attrIDColumnName: "messageID",
       datastore: GlodaDatastore, objFromRow: GlodaDatastore._messageFromRow,
@@ -1084,52 +1191,98 @@ var Gloda = {
           return [null, aMessage.id];
         else // assume they're just passing the id directly
           return [null, aMessage];
       }}, this.NOUN_MESSAGE);
     this.defineNoun({
       name: "contact",
       clazz: GlodaContact,
       allowsArbitraryAttrs: true,
+      isPrimitive: false,
       cache: true, cacheCost: 128,
       tableName: "contacts",
       attrTableName: "contactAttributes", attrIDColumnName: "contactID",
       datastore: GlodaDatastore, objFromRow: GlodaDatastore._contactFromRow,
       dbAttribAdjuster: GlodaDatastore.adjustAttributes,
       objInsert: GlodaDatastore.insertContact,
       objUpdate: GlodaDatastore.updateContact,
+      comparator: function gloda_contact_comparator(a, b) {
+        if (a == null) {
+          if (b == null)
+            return 0;
+          else
+            return 1;
+        }
+        else if (b == null) {
+          return -1;
+        }
+        return a.name.localeCompare(b.name);
+      },
       toParamAndValue: function(aContact) {
         if (aContact instanceof GlodaContact)
           return [null, aContact.id];
         else // assume they're just passing the id directly
           return [null, aContact];
       }}, this.NOUN_CONTACT);
     this.defineNoun({
       name: "identity",
       clazz: GlodaIdentity,
       allowsArbitraryAttrs: false,
+      isPrimitive: false,
       cache: true, cacheCost: 128,
       usesUniqueValue: true,
       tableName: "identities",
       datastore: GlodaDatastore, objFromRow: GlodaDatastore._identityFromRow,
+      comparator: function gloda_identity_comparator(a, b) {
+        if (a == null) {
+          if (b == null)
+            return 0;
+          else
+            return 1;
+        }
+        else if (b == null) {
+          return -1;
+        }
+        return a.contact.name.localeCompare(b.contact.name);
+      },
       toParamAndValue: function(aIdentity) {
         if (aIdentity instanceof GlodaIdentity)
           return [null, aIdentity.id];
         else // assume they're just passing the id directly
           return [null, aIdentity];
       }}, this.NOUN_IDENTITY);
 
     // parameterized identity is just two identities; we store the first one
     //  (whose value set must be very constrainted, like the 'me' identities)
     //  as the parameter, the second (which does not need to be constrained)
     //  as the value.
     this.defineNoun({
       name: "parameterized-identity",
       clazz: null,
       allowsArbitraryAttrs: false,
+      comparator: function gloda_fulltext_comparator(a, b) {
+        if (a == null) {
+          if (b == null)
+            return 0;
+          else
+            return 1;
+        }
+        else if (b == null) {
+          return -1;
+        }
+        // First sort by the first identity in the tuple
+        // Since our general use-case is for the first guy to be "me", we only
+        //  compare the identity value, not the name.
+        let fic = a[0].value.localeCompare(b[0].value);
+        if (fic)
+          return fic;
+        // Next compare the second identity in the tuple, but use the contact
+        //  this time to be consistent with our identity comparator.
+        return a[1].contact.name.localeCompare(b[1].contact.name);
+      },
       computeDelta: function(aCurValues, aOldValues) {
         let oldMap = {};
         for each (let [, tupe] in Iterator(aOldValues)) {
           let [originIdentity, targetIdentity] = tupe;
           let targets = oldMap[originIdentity];
           if (targets === undefined)
             targets = oldMap[originIdentity] = {};
           targets[targetIdentity] = true;
@@ -1264,53 +1417,76 @@ var Gloda = {
           }
           this._constraints.push(constraint);
           return this;
         };
 
         aSubjectNounDef.queryClass.prototype[aAttrDef.boundName + "Like"] =
           likeConstrainer;
       }
+
+      // - Custom helpers provided by the noun type...
+      if ("queryHelpers" in objectNounDef) {
+        for each (let [name, helper] in Iterator(objectNounDef.queryHelpers)) {
+          // we need a new closure...
+          let helperFunc = helper;
+          aSubjectNounDef.queryClass.prototype[aAttrDef.boundName + name] =
+            function() {
+              return helperFunc.call(this, aAttrDef, arguments);
+            };
+        }
+      }
     }
   },
 
   /**
+   * Names of attribute-specific localized strings and the JS attribute they are
+   *  exposed as in the attribute's "strings" attribute (if the provider has a
+   *  string bundle exposed on its "strings" attribute).  They are rooted at
+   *  "gloda.SUBJECT-NOUN-NAME.attr.ATTR-NAME.*".
+   */
+  _ATTR_LOCALIZED_STRINGS: {
+    facetLabel: "facetLabel",
+    facetTooltip: "facetTooltip",
+  },
+  /**
    * Define an attribute and all its meta-data.  Takes a single dictionary as
    *  its argument, with the following required properties:
    *
-   * @param provider The object instance providing a 'process' method.
-   * @param extensionName The name of the extension providing these attributes.
-   * @param attributeType The type of attribute, one of the values from the
-   *     kAttr* enumeration.
-   * @param attributeName The name of the attribute, which also doubles as the
-   *     bound property name if you pass 'bind' a value of true.  You are
+   * @param aAttrDef.provider The object instance providing a 'process' method.
+   * @param aAttrDef.extensionName The name of the extension providing these
+   *     attributes.
+   * @param aAttrDef.attributeType The type of attribute, one of the values from
+   *     the kAttr* enumeration.
+   * @param aAttrDef.attributeName The name of the attribute, which also doubles
+   *     as the bound property name if you pass 'bind' a value of true.  You are
    *     responsible for avoiding collisions, which presumably will mean
    *     checking/updating a wiki page in the future, or just prefixing your
    *     attribute name with your extension name or something like that.
-   * @param bind Should this attribute be 'bound' as a convenience attribute
-   *     on the subject's object (true/false)?  For example, with an
+   * @param aAttrDef.bind Should this attribute be 'bound' as a convenience
+   *     attribute on the subject's object (true/false)?  For example, with an
    *     attributeName of "foo" and passing true for 'bind' with a subject noun
-   *     of NOUN_MESSAGE, GlodaMessage instances will expose a "foo" getter
-   *     that returns the value of the attribute.  If 'singular' is true, this
-   *     means an instance of the object class corresponding to the noun type or
-   *     null if the attribute does not exist.  If 'singular' is false, this
-   *     means a list of instances of the object class corresponding to the noun
-   *     type, where the list may be empty if no instances of the attribute are
+   *     of NOUN_MESSAGE, GlodaMessage instances will expose a "foo" getter that
+   *     returns the value of the attribute.  If 'singular' is true, this means
+   *     an instance of the object class corresponding to the noun type or null
+   *     if the attribute does not exist.  If 'singular' is false, this means a
+   *     list of instances of the object class corresponding to the noun type,
+   *     where the list may be empty if no instances of the attribute are
    *     present.
-   * @param bindName Optional override of attributeName for purposes of the
-   *     binding property's name.
-   * @param singular Is the attribute going to happen at most once (true),
-   *     or potentially multiple times (false).  This affects whether
-   *     the binding  returns a list or just a single item (which is null when
+   * @param aAttrDef.bindName Optional override of attributeName for purposes of
+   *     the binding property's name.
+   * @param aAttrDef.singular Is the attribute going to happen at most once
+   *     (true), or potentially multiple times (false).  This affects whether
+   *     the binding returns a list or just a single item (which is null when
    *     the attribute is not present).
-   * @param subjectNouns A list of object types (NOUNs) that this attribute can
-   *     be set on.  Each element in the list should be one of the NOUN_*
-   *     constants or a dynamically registered noun type.
-   * @param objectNoun The object type (one of the NOUN_* constants or a
-   *     dynamically registered noun types) that is the 'object' in the
+   * @param aAttrDef.subjectNouns A list of object types (NOUNs) that this
+   *     attribute can be set on.  Each element in the list should be one of the
+   *     NOUN_* constants or a dynamically registered noun type.
+   * @param aAttrDef.objectNoun The object type (one of the NOUN_* constants or
+   *     a dynamically registered noun types) that is the 'object' in the
    *     traditional RDF triple.  More pragmatically, in the database row used
    *     to represent an attribute, we store the subject (ex: message ID),
    *     attribute ID, and an integer which is the integer representation of the
    *     'object' whose type you are defining right here.
    */
   defineAttribute: function gloda_ns_defineAttribute(aAttrDef) {
     // ensure required properties exist on aAttrDef
     if (!("provider" in aAttrDef) ||
@@ -1333,16 +1509,17 @@ var Gloda = {
     // - first time we've seen a provider init logic
     if (!(aAttrDef.provider.providerName in this._attrProviders)) {
       this._attrProviders[aAttrDef.provider.providerName] = [];
       if (aAttrDef.provider.contentWhittle)
         whittlerRegistry.registerWhittler(aAttrDef.provider);
     }
 
     let compoundName = aAttrDef.extensionName + ":" + aAttrDef.attributeName;
+    // -- Database Definition
     let attrDBDef;
     if (compoundName in GlodaDatastore._attributeDBDefs) {
       // the existence of the GlodaAttributeDBDef means that either it has
       //  already been fully defined, or has been loaded from the database but
       //  not yet 'bound' to a provider (and had important meta-info that
       //  doesn't go in the db copied over)
       attrDBDef = GlodaDatastore._attributeDBDefs[compoundName];
     }
@@ -1368,16 +1545,62 @@ var Gloda = {
     if ("bindName" in aAttrDef)
       aAttrDef.boundName = aAttrDef.bindName;
     else
       aAttrDef.boundName = aAttrDef.attributeName;
 
     aAttrDef.objectNounDef = this._nounIDToDef[aAttrDef.objectNoun];
     aAttrDef.objectNounDef.objectNounOfAttributes.push(aAttrDef);
 
+    // No facet attribute means no facet desired; set an explicit null so that
+    //  code can check without doing an "in" check.
+    if (!("facet" in aAttrDef))
+      aAttrDef.facet = null;
+    // Promote "true" facet values to the defaults.  Where attributes have
+    //  specified values, make sure we fill in any missing defaults.
+    else {
+      if (aAttrDef.facet == true) {
+        aAttrDef.facet = {
+          type: "default",
+          groupIdAttr: aAttrDef.objectNounDef.idAttr
+        };
+      }
+      else {
+        if (!("groupIdAttr" in aAttrDef.facet))
+          aAttrDef.facet.groupIdAttr = aAttrDef.objectNounDef.idAttr;
+      }
+    }
+
+    // -- L10n.
+    // If the provider has a string bundle, populate a "strings" attribute with
+    //  our standard attribute strings that can be UI exposed.
+    if ("strings" in aAttrDef.provider) {
+      let bundle = aAttrDef.provider.strings;
+      let attrStrings = aAttrDef.strings = {};
+      // we use the first subject the attribute applies to as the basis of
+      //  where to get the string from.  Mainly because we currently don't have
+      //  any attributes with multiple subjects nor a use-case where we expose
+      //  multiple noun types via the UI.  (Just messages right now.)
+      let canonicalSubject = this._nounIDToDef[aAttrDef.subjectNouns[0]];
+      let propRoot = "gloda." + canonicalSubject.name + ".attr." +
+                       aAttrDef.attributeName + ".";
+      for each (let [propName, attrName] in
+                Iterator(this._ATTR_LOCALIZED_STRINGS)) {
+        try {
+          attrStrings[attrName] = bundle.get(propRoot + propName);
+        }
+        catch (ex) {
+          // do nothing.  nsIStringBundle throws exceptions because it is a
+          //  standard nsresult type of API and our helper buddy does nothing
+          //  to help us.  (StringBundle.js, that is.)
+        }
+      }
+    }
+
+    // -- Subject Noun Binding
     for (let iSubject = 0; iSubject < aAttrDef.subjectNouns.length;
            iSubject++) {
       let subjectType = aAttrDef.subjectNouns[iSubject];
       let subjectNounDef = this._nounIDToDef[subjectType];
       this._bindAttribute(aAttrDef, subjectNounDef);
 
       // update the provider maps...
       if (this._attrProviderOrderByNoun[subjectType]
@@ -1751,18 +1974,22 @@ var Gloda = {
    *
    * @param aItems The non-empty list of items to score.
    * @param aContext A noun-specific dictionary that we just pass to the funcs.
    * @param aExtraScoreFuncs A list of extra scoring functions to apply.
    * @returns A list of integer scores equal in length to aItems.
    */
   scoreNounItems: function gloda_ns_grokNounItem(aItems, aContext,
                                                  aExtraScoreFuncs) {
+    let scores = [];
+    // bail if there is nothing to score
+    if (!aItems.length)
+      return scores;
+
     let itemNounDef = aItems[0].NOUN_DEF;
-    let scores = [];
     if (aExtraScoreFuncs == null)
       aExtraScoreFuncs = [];
 
     for each (let [, item] in Iterator(aItems)) {
       let score = 0;
       let attrProviders = this._attrProviderOrderByNoun[itemNounDef.id];
       for (let iProvider = 0; iProvider < attrProviders.length; iProvider++) {
         let provider = attrProviders[iProvider];
--- a/mailnews/db/gloda/modules/index_ab.js
+++ b/mailnews/db/gloda/modules/index_ab.js
@@ -1,16 +1,16 @@
 /* ***** 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 Global Database.
  *
  * The Initial Developer of the Original Code is
@@ -27,17 +27,17 @@
  * 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 ***** */
 
 EXPORTED_SYMBOLS = ['GlodaABIndexer', 'GlodaABAttrs'];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
@@ -56,58 +56,59 @@ Cu.import("resource://app/modules/gloda/
 
 var GlodaABIndexer = {
   _log: null,
 
   name: "ab_indexer",
   enable: function() {
     if (this._log == null)
       this._log =  Log4Moz.repository.getLogger("gloda.ab_indexer");
-  
+
     let abManager = Cc["@mozilla.org/abmanager;1"].getService(Ci.nsIAbManager);
     abManager.addAddressBookListener(this, Ci.nsIAbListener.itemChanged);
   },
-  
+
   disable: function() {
     let abManager = Cc["@mozilla.org/abmanager;1"].getService(Ci.nsIAbManager);
     abManager.removeAddressBookListener(this);
   },
 
   get workers() {
     return [["ab-card", this._worker_index_card]];
   },
-  
+
   _worker_index_card: function(aJob, aCallbackHandle) {
     let card = aJob.id;
-    
+
     if (card.primaryEmail) {
       // load the identity
       let query = Gloda.newQuery(Gloda.NOUN_IDENTITY);
       query.kind("email");
-      query.value(card.primaryEmail);
+      // we currently normalize all e-mail addresses to be lowercase
+      query.value(card.primaryEmail.toLowerCase());
       let identityCollection = query.getCollection(aCallbackHandle);
       yield Gloda.kWorkAsync;
-      
+
       if (identityCollection.items.length) {
         let identity = identityCollection.items[0];
-  
+
         this._log.debug("Found identity, processing card.");
         yield aCallbackHandle.pushAndGo(
             Gloda.grokNounItem(identity.contact, card, false, false,
                                aCallbackHandle));
         this._log.debug("Done processing card.");
       }
     }
-    
+
     yield GlodaIndexer.kWorkDone;
   },
-  
+
   initialSweep: function() {
   },
-  
+
   /* ------ nsIAbListener ------ */
   onItemAdded: function ab_indexer_onItemAdded(aParentDir, aItem) {
   },
   onItemRemoved: function ab_indexer_onItemRemoved(aParentDir, aItem) {
   },
   onItemPropertyChanged: function ab_indexer_onItemPropertyChanged(aItem,
       aProperty, aOldValue, aNewValue) {
     if (aProperty == null && aItem instanceof Ci.nsIAbCard) {
@@ -122,26 +123,26 @@ var GlodaABIndexer = {
 GlodaIndexer.registerIndexer(GlodaABIndexer);
 
 var GlodaABAttrs = {
   providerName: "gloda.ab_attr",
   _log: null,
 
   init: function() {
     this._log =  Log4Moz.repository.getLogger("gloda.abattrs");
-    
+
     try {
       this.defineAttributes();
     }
     catch (ex) {
       this._log.error("Error in init: " + ex);
       throw ex;
     }
   },
-  
+
   defineAttributes: function() {
     /* ***** Contacts ***** */
     this._attrIdentityContact = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrDerived,
       attributeName: "identities",
       singular: false,
@@ -190,17 +191,17 @@ var GlodaABAttrs = {
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrDerived,
       attributeName: "contact",
       singular: true,
       special: Gloda.kSpecialColumnParent,
       specialColumnName: "contactID", // the column in the db
       idStorageAttributeName: "_contactID",
-      valueStorageAttributeName: "_contact", 
+      valueStorageAttributeName: "_contact",
       subjectNouns: [Gloda.NOUN_IDENTITY],
       objectNoun: Gloda.NOUN_CONTACT,
       }); // tested-by: test_attributes_fundamental
     this._attrIdentityKind = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrFundamental,
       attributeName: "kind",
@@ -240,39 +241,39 @@ var GlodaABAttrs = {
                         }); // not-tested
     // we need to find any existing bound freetag attributes, and use them to
     //  populate to FreeTagNoun's understanding
     for (let freeTagName in this._attrFreeTag.parameterBindings) {
       this._log.debug("Telling FreeTagNoun about: " + freeTagName);
       FreeTagNoun.getFreeTag(freeTagName);
     }
   },
-  
+
   process: function(aContact, aCard, aIsNew, aCallbackHandle) {
     if (aContact.NOUN_ID != Gloda.NOUN_CONTACT) {
       this._log.warning("Somehow got a non-contact: " + aContact);
       return; // this will produce an exception; we like.
     }
-    
+
     // update the name
     if (aCard.displayName && aCard.displayName != aContact.name)
       aContact.name = aCard.displayName;
-  
+
     aContact.freeTags = [];
-    
+
     let tags = null;
     try {
       tags = aCard.getProperty("Categories", null);
     } catch (ex) {
       this._log.error("Problem accessing property: " + ex);
     }
     if (tags) {
       for each (let [iTagName, tagName] in Iterator(tags.split(","))) {
         tagName = tagName.trim();
         if (tagName) {
           aContact.freeTags.push(FreeTagNoun.getFreeTag(tagName));
         }
       }
     }
-    
+
     yield Gloda.kWorkDone;
   }
 };
--- a/mailnews/db/gloda/modules/indexer.js
+++ b/mailnews/db/gloda/modules/indexer.js
@@ -346,16 +346,22 @@ var GlodaIndexer = {
   _INDEX_IDLE_ADJUSTMENT_TIME: 5000,
 
   /**
    * The time delay in milliseconds before we should schedule our initial sweep.
    */
   _INITIAL_SWEEP_DELAY: 10000,
 
   /**
+   * How many milliseconds in the future should we schedule indexing to start
+   *  when turning on indexing (and it was not previously active).
+   */
+  _INDEX_KICKOFF_DELAY: 200,
+
+  /**
    * The time interval, in milliseconds, of pause between indexing batches.  The
    *  maximum processor consumption is determined by this constant and the
    *  active |_cpuTargetIndexTime|.
    *
    * For current constants, that puts us at 50% while the user is active and 83%
    *  when idle.
    */
   _INDEX_INTERVAL: 32,
@@ -681,17 +687,17 @@ var GlodaIndexer = {
    */
   set indexing(aShouldIndex) {
     if (!this._indexingDesired && aShouldIndex) {
       this._indexingDesired = true;
       if (this.enabled && !this._indexingActive && !this._suppressIndexing) {
         this._log.info("+++ Indexing Queue Processing Commencing");
         this._indexingActive = true;
         this._timer.initWithCallback(this._wrapCallbackDriver,
-                                     this._INDEX_INTERVAL,
+                                     this._INDEX_KICKOFF_DELAY,
                                      Ci.nsITimer.TYPE_ONE_SHOT);
       }
     }
   },
 
   _suppressIndexing: false,
   /**
    * Set whether or not indexing should be suppressed.  This is to allow us to
@@ -705,17 +711,17 @@ var GlodaIndexer = {
 
     // re-start processing if we are no longer suppressing, there is work yet
     //  to do, and the indexing process had actually stopped.
     if (!this._suppressIndexing && this._indexingDesired &&
         !this._indexingActive) {
         this._log.info("+++ Indexing Queue Processing Resuming");
         this._indexingActive = true;
         this._timer.initWithCallback(this._wrapCallbackDriver,
-                                     this._INDEX_INTERVAL,
+                                     this._INDEX_KICKOFF_DELAY,
                                      Ci.nsITimer.TYPE_ONE_SHOT);
     }
   },
 
   /**
    * Our timer-driven callback to schedule our first initial indexing sweep.
    *  Because it is invoked by an nsITimer it operates without the benefit of
    *  a 'this' context and must use GlodaIndexer instead of this.
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/mimeTypeCategories.js
@@ -0,0 +1,200 @@
+/*
+ * This file wants to be a data file of some sort.  It might do better as a real
+ *  raw JSON file.  It is trying to be one right now, but it obviously is not.
+ */
+
+let EXPORTED_SYMBOLS = ['MimeCategoryMapping'];
+
+/**
+ * Input data structure to allow us to build a fast mapping from mime type to
+ *  category name.  The keys in MimeCategoryMapping are the top-level
+ *  categories.  Each value can either be a list of MIME types or a nested
+ *  object which recursively defines sub-categories.  We currently do not use
+ *  the sub-categories.  They are just there to try and organize the MIME types
+ *  a little and open the door to future enhancements.
+ *
+ * Do _not_ add additional top-level categories unless you have added
+ *  corresponding entries to gloda.properties under the
+ *  "gloda.mimetype.category" branch and are making sure localizers are aware
+ *  of the change and have time to localize it.
+ *
+ * Entries with wildcards in them are part of a fallback strategy by the
+ *  |mimeTypeNoun| and do not actually use regular expressions or anything like
+ *  that.  Everything is a straight string lookup.  Given "foo/bar" we look for
+ *  "foo/bar", then "foo/*", and finally "*".
+ */
+let MimeCategoryMapping = {
+  archives: [
+    "application/java-archive",
+    "application/x-java-archive",
+    "application/x-jar",
+    "application/x-java-jnlp-file",
+
+    "application/mac-binhex40",
+    "application/vnd.ms-cab-compressed",
+
+    "application/x-arc",
+    "application/x-arj",
+    "application/x-compress",
+    "application/x-compressed-tar",
+    "application/x-cpio",
+    "application/x-cpio-compressed",
+    "application/x-deb",
+
+    "application/x-bittorrent",
+
+    "application/x-rar",
+    "application/x-rar-compressed",
+    "application/x-7z-compressed",
+    "application/zip",
+    "application/x-zip-compressed",
+    "application/x-zip",
+
+    "application/x-bzip",
+    "application/x-bzip-compressed-tar",
+    "application/x-bzip2",
+    "application/x-gzip",
+    "application/x-tar",
+    "application/x-tar-gz",
+    "application/x-tarz",
+  ],
+  documents: {
+    database: [
+      "application/vnd.ms-access",
+      "application/x-msaccess",
+      "application/msaccess",
+      "application/vnd.msaccess",
+      "application/x-msaccess",
+      "application/mdb",
+      "application/x-mdb",
+
+      "application/vnd.oasis.opendocument.database",
+
+    ],
+    graphics: [
+      "application/postscript",
+      "application/x-bzpostscript",
+      "application/x-dvi",
+      "application/x-gzdvi",
+
+      "application/illustrator",
+
+      "application/vnd.corel-draw",
+      "application/cdr",
+      "application/coreldraw",
+      "application/x-cdr",
+      "application/x-coreldraw",
+      "image/cdr",
+      "image/x-cdr",
+      "zz-application/zz-winassoc-cdr",
+
+      "application/vnd.oasis.opendocument.graphics",
+      "application/vnd.oasis.opendocument.graphics-template",
+      "application/vnd.oasis.opendocument.image",
+
+      "application/x-dia-diagram",
+    ],
+    presentation: [
+      "application/vnd.ms-powerpoint.presentation.macroenabled.12",
+      "application/vnd.ms-powerpoint.template.macroenabled.12",
+      "application/vnd.ms-powerpoint",
+      "application/powerpoint",
+      "application/mspowerpoint",
+      "application/x-mspowerpoint",
+      "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+      "application/vnd.openxmlformats-officedocument.presentationml.template",
+
+      "application/vnd.oasis.opendocument.presentation",
+      "application/vnd.oasis.opendocument.presentation-template"
+    ],
+    spreadsheet: [
+      "application/vnd.lotus-1-2-3",
+      "application/x-lotus123",
+      "application/x-123",
+      "application/lotus123",
+      "application/wk1",
+
+      "application/x-quattropro",
+
+      "application/vnd.ms-excel.sheet.binary.macroenabled.12",
+      "application/vnd.ms-excel.sheet.macroenabled.12",
+      "application/vnd.ms-excel.template.macroenabled.12",
+      "application/vnd.ms-excel",
+      "application/msexcel",
+      "application/x-msexcel",
+      "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+      "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
+
+      "application/vnd.oasis.opendocument.formula",
+      "application/vnd.oasis.opendocument.formula-template",
+      "application/vnd.oasis.opendocument.chart",
+      "application/vnd.oasis.opendocument.chart-template",
+      "application/vnd.oasis.opendocument.spreadsheet",
+      "application/vnd.oasis.opendocument.spreadsheet-template",
+
+      "application/x-gnumeric",
+    ],
+    wordProcessor: [
+      "application/msword",
+      "application/vnd.ms-word",
+      "application/x-msword",
+      "application/msword-template",
+      "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+      "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
+      "application/vnd.ms-word.document.macroenabled.12",
+      "application/vnd.ms-word.template.macroenabled.12",
+      "application/x-mswrite",
+      "application/x-pocket-word",
+
+      "application/rtf",
+      "text/rtf",
+
+
+      "application/vnd.oasis.opendocument.text",
+      "application/vnd.oasis.opendocument.text-master",
+      "application/vnd.oasis.opendocument.text-template",
+      "application/vnd.oasis.opendocument.text-web",
+
+      "application/vnd.wordperfect",
+
+      "application/x-abiword",
+      "application/x-amipro",
+    ],
+    suite: [
+      "application/vnd.ms-works"
+    ],
+  },
+  images: [
+    "image/*"
+  ],
+  media: {
+    audio: [
+      "audio/*",
+    ],
+    video: [
+      "video/*",
+    ],
+    container: [
+      "application/ogg",
+
+      "application/smil",
+      "application/vnd.ms-asf",
+      "application/vnd.rn-realmedia",
+      "application/x-matroska",
+      "application/x-quicktime-media-link",
+      "application/x-quicktimeplayer",
+    ]
+  },
+  other: [
+    "*"
+  ],
+  pdf: [
+    "application/pdf",
+    "application/x-pdf",
+    "image/pdf",
+    "file/pdf",
+
+    "application/x-bzpdf",
+    "application/x-gzpdf",
+  ],
+}
--- a/mailnews/db/gloda/modules/msg_search.js
+++ b/mailnews/db/gloda/modules/msg_search.js
@@ -83,17 +83,17 @@ const OFFSET_FUZZSCORE_SQL_SNIPPET =
 const FUZZSCORE_SQL_SNIPPET =
   "(" + OFFSET_FUZZSCORE_SQL_SNIPPET + " + notability)";
 
 const DASCORE_SQL_SNIPPET =
   "((" + FUZZSCORE_SQL_SNIPPET + " * " + FUZZSCORE_TIMESTAMP_FACTOR +
     ") + date)";
 
 const FULLTEXT_QUERY_EXPLICIT_SQL =
-  "SELECT messages.*, offsets(messagesText) AS osets " +
+  "SELECT messages.*, messagesText.*, offsets(messagesText) AS osets " +
     "FROM messages, messagesText WHERE messagesText MATCH ?" +
     " AND messages.id == messagesText.docid";
 
 
 function identityFunc(x) {
   return x;
 }
 
@@ -178,55 +178,106 @@ function scoreOffsets(aMessage, aContext
     score += Math.min(termIncidence.map(oneLessMaxZero).reduce(reduceSum, 0),
                       COLUMN_MULTIPLE_MATCH_LIMIT[iColumn]) *
              COLUMN_MULTIPLE_MATCH_SCORES[iColumn];
   }
 
   return score;
 }
 
+/**
+ * The searcher basically looks like a query, but is specialized for fulltext
+ *  search against messages.  Most of the explicit specialization involves
+ *  crafting a SQL query that attempts to order the matches by likelihood that
+ *  the user was looking for it.  This is based on full-text matches combined
+ *  with an explicit (generic) interest score value placed on the message at
+ *  indexing time.  This is followed by using the more generic gloda scoring
+ *  mechanism to explicitly score the messages given the search context in
+ *  addition to the more generic score adjusting rules.
+ */
+function GlodaMsgSearcher(aListener, aSearchString, aAndTerms) {
+  this.listener = aListener;
 
-function GlodaMsgSearcher(aViewWrapper, aFulltextTerms) {
-  this.viewWrapper = aViewWrapper;
-
-  this.fulltextTerms = aFulltextTerms;
+  this.searchString = aSearchString;
+  this.fulltextTerms = this.parseSearchString(aSearchString);
+  this.andTerms = (aAndTerms != null) ? aAndTerms : true;
 
   this.query = null;
   this.collection = null;
 
-  this.scoresByUriAndKey = {};
-  this.whysByUriAndKey = {};
+  this.scoresById = {};
 }
 GlodaMsgSearcher.prototype = {
   /**
    * Number of messages to retrieve initially.
    */
-  retrievalLimit: 100,
+  retrievalLimit: 400,
+
+  parseSearchString: function GlodaMsgSearcher_parseSearchString(aSearchString) {
+    aSearchString = aSearchString.trim();
+    let terms = [];
+    while (aSearchString) {
+      if (aSearchString[0] == '"') {
+        let endIndex = aSearchString.indexOf(aSearchString[0], 1);
+        // eat the quote if it has no friend
+        if (endIndex == -1) {
+          aSearchString = aSearchString.substring(1);
+          continue;
+        }
+
+        terms.push(aSearchString.substring(1, endIndex).trim());
+        aSearchString = aSearchString.substring(endIndex + 1);
+        continue;
+      }
+
+      let spaceIndex = aSearchString.indexOf(" ");
+      if (spaceIndex == -1) {
+        terms.push(aSearchString);
+        break;
+      }
+
+      terms.push(aSearchString.substring(0, spaceIndex));
+      aSearchString = aSearchString.substring(spaceIndex+1);
+    }
+
+    return terms;
+  },
 
   buildFulltextQuery: function GlodaMsgSearcher_buildFulltextQuery() {
     let query = Gloda.newQuery(Gloda.NOUN_MESSAGE, {
       noMagic: true,
       explicitSQL: FULLTEXT_QUERY_EXPLICIT_SQL,
-      // osets is 0-based column number 9 (volatile to column changes)
-      // dascore becomes 0-based column number 10
+      // osets is 0-based column number 14 (volatile to column changes)
+      // dascore becomes 0-based column number 15
       outerWrapColumns: [DASCORE_SQL_SNIPPET + " AS dascore"],
       // save the offset column for extra analysis
-      stashColumns: [9]
+      stashColumns: [14]
     });
 
-    query.fulltextMatches(this.fulltextTerms.join(" "));
+    let fulltextQueryString;
+    if (this.andTerms)
+      fulltextQueryString = '"' + this.fulltextTerms.join('" "') + '"';
+    else
+      fulltextQueryString = '"' + this.fulltextTerms.join('" OR "') + '"';
+
+    query.fulltextMatches(fulltextQueryString);
     query.orderBy('-dascore');
     query.limit(this.retrievalLimit);
 
     return query;
   },
 
-  go: function GlodaMsgSearcher_go() {
+  getCollection: function GlodaMsgSearcher_getCollection(
+      aListenerOverride, aData) {
+    if (aListenerOverride)
+      this.listener = aListenerOverride;
+
     this.query = this.buildFulltextQuery();
-    this.collection = this.query.getCollection(this);
+    this.collection = this.query.getCollection(this, aData);
+    this.completed = false;
 
     return this.collection;
   },
 
   onItemsAdded: function GlodaMsgSearcher_onItemsAdded(aItems, aCollection) {
     let scores = Gloda.scoreNounItems(
       aItems,
       {
@@ -234,23 +285,30 @@ GlodaMsgSearcher.prototype = {
         stashedColumns: aCollection.stashedColumns
       },
       [scoreOffsets]);
     let actualItems = [];
     for (let i = 0; i < aItems.length; i++) {
       let item = aItems[i];
       let score = scores[i];
 
-      let hdr = item.folderMessage;
-      if (hdr) {
-        this.scoresByUriAndKey[hdr.folder.URI + "-" + hdr.messageKey] = score;
-        actualItems.push(item);
-      }
+      this.scoresById[item.id] = score;
     }
 
-    this.viewWrapper.onItemsAdded(actualItems, aCollection);
+    if (this.listener)
+      this.listener.onItemsAdded(actualItems, aCollection);
+  },
+  onItemsModified: function GlodaMsgSearcher_onItemsModified(aItems,
+                                                             aCollection) {
+    if (this.listener)
+      this.listener.onItemsModified(aItems, aCollection);
   },
-  onItemsModified: function GlodaMsgSearcher_onItemsModified() {},
-  onItemsRemoved: function GlodaMsgSearcher_onItemsRemoved() {},
+  onItemsRemoved: function GlodaMsgSearcher_onItemsRemoved(aItems,
+                                                           aCollection) {
+    if (this.listener)
+      this.listener.onItemsRemoved(aItems, aCollection);
+  },
   onQueryCompleted: function GlodaMsgSearcher_onQueryCompleted(aCollection) {
-    this.viewWrapper.onQueryCompleted(aCollection);
+    this.completed = true;
+    if (this.listener)
+      this.listener.onQueryCompleted(aCollection);
   },
-};
\ No newline at end of file
+};
--- a/mailnews/db/gloda/modules/noun_freetag.js
+++ b/mailnews/db/gloda/modules/noun_freetag.js
@@ -1,16 +1,16 @@
 /* ***** 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 Global Database.
  *
  * The Initial Developer of the Original Code is
@@ -27,17 +27,17 @@
  * 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 ***** */
 
 EXPORTED_SYMBOLS = ['FreeTag', 'FreeTagNoun'];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
@@ -59,54 +59,67 @@ FreeTag.prototype = {
 /**
  * @namespace Tag noun provider.  Since the tag unique value is stored as a
  *  parameter, we are an odd case and semantically confused.
  */
 var FreeTagNoun = {
   _log: Log4Moz.repository.getLogger("gloda.noun.freetag"),
 
   name: "freetag",
-  class: FreeTag,
+  clazz: FreeTag,
   allowsArbitraryAttrs: false,
   usesParameter: true,
-  
+
   _listeners: [],
   addListener: function(aListener) {
     this._listeners.push(aListener);
   },
   removeListener: function(aListener) {
     let index = this._listeners.indexOf(aListener);
     if (index >=0)
       this._listeners.splice(index, 1);
   },
-  
+
   populateKnownFreeTags: function() {
     for each (let [,attr] in Iterator(this.objectNounOfAttributes)) {
       let attrDB = attr.dbDef;
       for (let param in attrDB.parameterBindings) {
         this.getFreeTag(param);
       }
     }
   },
-  
+
   knownFreeTags: {},
   getFreeTag: function(aTagName) {
     let tag = this.knownFreeTags[aTagName];
     if (!tag) {
       tag = this.knownFreeTags[aTagName] = new FreeTag(aTagName);
       for each (let [iListener, listener] in Iterator(this._listeners))
         listener.onFreeTagAdded(tag);
     }
     return tag;
   },
 
+  comparator: function gloda_noun_freetag_comparator(a, b) {
+    if (a == null) {
+      if (b == null)
+        return 0;
+      else
+        return 1;
+    }
+    else if (b == null) {
+      return -1;
+    }
+    return a.name.localeCompare(b.name);
+  },
+
   toParamAndValue: function gloda_noun_freetag_toParamAndValue(aTag) {
     return [aTag.name, null];
   },
-  
+
   toJSON: function gloda_noun_freetag_toJSON(aTag) {
     return aTag.name;
   },
   fromJSON: function gloda_noun_freetag_fromJSON(aTagName) {
     return this.getFreeTag(aTagName);
   },
 };
 
--- a/mailnews/db/gloda/modules/noun_mimetype.js
+++ b/mailnews/db/gloda/modules/noun_mimetype.js
@@ -38,33 +38,37 @@
 EXPORTED_SYMBOLS = ['MimeType', 'MimeTypeNoun'];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://app/modules/gloda/log4moz.js");
+Cu.import("resource://app/modules/StringBundle.js");
 
 const LOG = Log4Moz.repository.getLogger("gloda.noun.mimetype");
 
 Cu.import("resource://app/modules/gloda/gloda.js");
 
+let CategoryStringMap = {};
+
 /**
  * Mime type abstraction that exists primarily so we can map mime types to
  *  integer id's.
  *
  * Instances of this class should only be retrieved via |MimeTypeNoun|; no one
  *  should ever create an instance directly.
  */
-function MimeType(aID, aType, aSubType, aFullType) {
+function MimeType(aID, aType, aSubType, aFullType, aCategory) {
   this._id = aID;
   this._type = aType;
   this._subType = aSubType;
   this._fullType = aFullType;
+  this._category = aCategory;
 }
 
 MimeType.prototype = {
   /**
    * The integer id we have associated with the mime type.  This is stable for
    *  the lifetime of the database, which means that anything in the Gloda
    *  database can use this without fear.  Things not persisted in the database
    *  should use the actual string mime type, retrieval via |fullType|.
@@ -73,142 +77,315 @@ MimeType.prototype = {
   /**
    * The first part of the MIME type; "text/plain" gets you "text".
    */
   get type() { return this._type; },
   set fullType(aFullType) {
     if (!this._fullType) {
       this._fullType = aFullType;
       [this._type, this._subType] = this._fullType.split("/");
+      this._category =
+        MimeTypeNoun._getCategoryForMimeType(aFullType, this._type);
     }
   },
   /**
    * If the |fullType| is "text/plain", subType is "plain".
    */
   get subType() { return this._subType; },
   /**
    * The full MIME type; "text/plain" returns "text/plain".
    */
   get fullType() { return this._fullType; },
   toString: function () {
     return this.fullType;
+  },
+
+  /**
+   * @return the category we believe this mime type belongs to.  This category
+   *     name should never be shown directly to the user.  Instead, use
+   *     |categoryLabel| to get the localized name for the category.  The
+   *     category mapping comes from mimeTypesCategories.js.
+   */
+  get category() {
+    return this._category;
+  },
+  /**
+   * @return The localized label for the category from gloda.properties in the
+   *     "gloda.mimetype.category.CATEGORY.label" definition using the value
+   *     from |category|.
+   */
+  get categoryLabel() {
+    return CategoryStringMap[this._category];
   }
 };
 
 /**
- * @namespace Mime type noun provider.
+ * Mime type noun provider.
  *
  * The set of MIME Types is sufficiently limited that we can keep them all in
  *  memory.  In theory it is also sufficiently limited that we could use the
  *  parameter mechanism in the database.  However, it is more efficient, for
  *  both space and performance reasons, to store the specific mime type as a
  *  value.  For future-proofing reasons, we opt to use a database table to
  *  persist the mapping rather than a hard-coded list.  A preferences file or
  *  other text file would arguably suffice, but for consistency reasons, the
  *  database is not a bad thing.
  */
 var MimeTypeNoun = {
   name: "mime-type",
   clazz: MimeType, // gloda supports clazz as well as class
   allowsArbitraryAttrs: false,
 
+  _strings: new StringBundle("chrome://messenger/locale/gloda.properties"),
+
   // note! update test_noun_mimetype if you change our internals!
   _mimeTypes: {},
   _mimeTypesByID: {},
-  TYPE_BLOCK_SIZE: 8096, // bet you were expecting a power of 2!
-    // (we can fix this next time we bump the database schema version and have
-    //  to resort to blowing the database away.)
+  TYPE_BLOCK_SIZE: 16384,
   _mimeTypeHighID: {},
+  _mimeTypeRangeDummyObjects: {},
   _highID: 0,
 
   // we now use the exciting 'schema' mechanism of defineNoun to get our table
   //  created for us, plus some helper methods that we simply don't use.
   schema: {
     name: 'mimeTypes',
     columns: [['id', 'INTEGER PRIMARY KEY', '_id'],
               ['mimeType', 'TEXT', 'fullType']],
   },
 
   _init: function() {
     LOG.debug("loading MIME types");
+    this._loadCategoryMapping();
     this._loadMimeTypes();
   },
 
-  _loadMimeTypes: function() {
+  /**
+   * A map from MIME type to category name.
+   */
+  _mimeTypeToCategory: {},
+  /**
+   * Load the contents of mimeTypeCategories.js and populate
+   */
+  _loadCategoryMapping: function MimeTypeNoun__loadCategoryMapping() {
+    let mimecatNS = {};
+    Cu.import("resource://app/modules/gloda/mimeTypeCategories.js",
+              mimecatNS);
+    let mcm = mimecatNS.MimeCategoryMapping;
+
+    let mimeTypeToCategory = this._mimeTypeToCategory;
+
+    function procMapObj(aSubTree, aCategories) {
+      for each (let [key, value] in Iterator(aSubTree)) {
+        // Add this category to our nested categories list.  Use concat since
+        //  the list will be long-lived and each list needs to be distinct.
+        let categories = aCategories.concat();
+        categories.push(key);
+
+        if (categories.length == 1) {
+          CategoryStringMap[key] =
+            MimeTypeNoun._strings.get(
+              "gloda.mimetype.category." + key + ".label");
+        }
+
+        // Is it an array?  (We do not have isArray in 1.9.1 and since it comes
+        //  from another JS module, it has its own Array global, so instanceof
+        //  fails us.)  If it is, just process this depth
+        if ("length" in value) {
+          for each (let [, mimeTypeStr] in Iterator(value)) {
+            mimeTypeToCategory[mimeTypeStr] = categories;
+          }
+        }
+        // it's yet another sub-tree branch
+        else {
+          procMapObj(value, categories);
+        }
+      }
+    }
+
+    procMapObj(mimecatNS.MimeCategoryMapping, []);
+  },
+
+  /**
+   * Lookup the category associated with a MIME type given its full type and
+   *  type.  (So, "foo/bar" and "foo" for "foo/bar".)
+   */
+  _getCategoryForMimeType:
+      function MimeTypeNoun__getCategoryForMimeType(aFullType, aType) {
+    if (aFullType in this._mimeTypeToCategory)
+      return this._mimeTypeToCategory[aFullType][0];
+    let wildType = aType + "/*";
+    if (wildType in this._mimeTypeToCategory)
+      return this._mimeTypeToCategory[wildType][0];
+    return this._mimeTypeToCategory["*"][0];
+  },
+
+  /**
+   * In order to allow the gloda query mechanism to avoid hitting the database,
+   *  we need to either define the noun type as cachable and have a super-large
+   *  cache or simply have a collection with every MIME type in it that stays
+   *  alive forever.
+   * This is that collection.  It is initialized by |_loadMimeTypes|.  As new
+   *  MIME types are created, we add them to the collection.
+   */
+  _universalCollection: null,
+
+  /**
+   * Kick off a query of all the mime types in our database, leaving
+   *  |_processMimeTypes| to actually do the legwork.
+   */
+  _loadMimeTypes: function MimeTypeNoun__loadMimeTypes() {
     // get all the existing mime types!
     let query = Gloda.newQuery(this.id);
     let nullFunc = function() {};
-    query.getCollection({
-      onItemsAdded: nullFunc, onItemsModified: nullFunc, onItemsRemoved: null,
+    this._universalCollection = query.getCollection({
+      onItemsAdded: nullFunc, onItemsModified: nullFunc,
+      onItemsRemoved: nullFunc,
       onQueryCompleted: function (aCollection) {
         MimeTypeNoun._processMimeTypes(aCollection.items);
       }
-    }, null).becomeExplicit();
+    }, null);
   },
 
-  _processMimeTypes: function(aMimeTypes) {
+  /**
+   * For the benefit of our Category queryHelper, we need dummy ranged objects
+   *  that cover the numerical address space allocated to the category.  We
+   *  can't use a real object for the upper-bound because the upper-bound is
+   *  constantly growing and there is the chance the query might get persisted,
+   *  which means these values need to be long-lived.  Unfortunately, our
+   *  solution to this problem (dummy objects) complicates the second case,
+   *  should it ever occur.  (Because the dummy objects cannot be persisted
+   *  on their own... but there are other issues that will come up that we will
+   *  just have to deal with then.)
+   */
+  _createCategoryDummies: function (aId, aCategory) {
+    let blockBottom = aId - (aId % this.TYPE_BLOCK_SIZE);
+    let blockTop = blockBottom + this.TYPE_BLOCK_SIZE - 1;
+    this._mimeTypeRangeDummyObjects[aCategory] = [
+      new MimeType(blockBottom, "!category-dummy!", aCategory,
+                   "!category-dummy!/" + aCategory, aCategory),
+      new MimeType(blockTop, "!category-dummy!", aCategory,
+                   "!category-dummy!/" + aCategory, aCategory)
+    ];
+  },
+
+  _processMimeTypes: function MimeTypeNoun__processMimeTypes(aMimeTypes) {
     for each (let [, mimeType] in Iterator(aMimeTypes)) {
       if (mimeType.id > this._highID)
         this._highID = mimeType.id;
       this._mimeTypes[mimeType] = mimeType;
       this._mimeTypesByID[mimeType.id] = mimeType;
 
       let typeBlock = mimeType.id - (mimeType.id % this.TYPE_BLOCK_SIZE);
-      let blockHighID = this._mimeTypeHighID[mimeType.type];
+      let blockHighID = (mimeType.category in this._mimeTypeHighID) ?
+                          this._mimeTypeHighID[mimeType.category] : undefined;
+      // create the dummy range objects
+      if (blockHighID === undefined)
+        this._createCategoryDummies(mimeType.id, mimeType.category);
       if ((blockHighID === undefined) || mimeType.id > blockHighID)
-        this._mimeTypeHighID[mimeType.type] = mimeType.id;
+        this._mimeTypeHighID[mimeType.category] = mimeType.id;
     }
   },
 
-  _addNewMimeType: function(aMimeTypeName) {
+  _addNewMimeType: function MimeTypeNoun__addNewMimeType(aMimeTypeName) {
     let [typeName, subTypeName] = aMimeTypeName.split("/");
+    let category = this._getCategoryForMimeType(aMimeTypeName, typeName);
 
-    if (!(typeName in this._mimeTypeHighID)) {
+    if (!(category in this._mimeTypeHighID)) {
       let nextID = this._highID - (this._highID % this.TYPE_BLOCK_SIZE) +
         this.TYPE_BLOCK_SIZE;
-      this._mimeTypeHighID[typeName] = nextID;
+      this._mimeTypeHighID[category] = nextID;
+      this._createCategoryDummies(nextID, category);
     }
 
-    let nextID = ++this._mimeTypeHighID[typeName];
+    let nextID = ++this._mimeTypeHighID[category];
 
-    let mimeType = new MimeType(nextID, typeName, subTypeName, aMimeTypeName);
+    let mimeType = new MimeType(nextID, typeName, subTypeName, aMimeTypeName,
+                                category);
     if (mimeType.id > this._highID)
       this._highID = mimeType.id;
 
     this._mimeTypes[aMimeTypeName] = mimeType;
     this._mimeTypesByID[nextID] = mimeType;
 
-    // as great as the gloda extension mechanisms are, we don't think it makes
+    // As great as the gloda extension mechanisms are, we don't think it makes
     //  a lot of sense to use them in this case.  So we directly trigger object
     //  insertion without any of the grokNounItem stuff.
     this.objInsert.call(this.datastore, mimeType);
+    // Since we bypass grokNounItem and its fun, we need to explicitly add the
+    //  new MIME-type to _universalCollection ourselves.  Don't try this at
+    //  home, kids.
+    this._universalCollection._onItemsAdded([mimeType]);
 
     return mimeType;
   },
 
   /**
    * Map a mime type to a |MimeType| instance, creating it if necessary.
    *
    * @param aMimeTypeName The mime type.  It may optionally include parameters
    *     (which will be ignored).  A mime type is of the form "type/subtype".
    *     A type with parameters would look like 'type/subtype; param="value"'.
    */
-  getMimeType: function(aMimeTypeName) {
+  getMimeType: function MimeTypeNoun_getMimeType(aMimeTypeName) {
     // first, lose any parameters
     let semiIndex = aMimeTypeName.indexOf(";");
     if (semiIndex >= 0)
       aMimeTypeName = aMimeTypeName.substring(0, semiIndex);
     aMimeTypeName = aMimeTypeName.trim().toLowerCase();
 
     if (aMimeTypeName in this._mimeTypes)
       return this._mimeTypes[aMimeTypeName];
     else
       return this._addNewMimeType(aMimeTypeName);
   },
 
+  /**
+   * Query helpers contribute additional functions to the query object for the
+   *  attributes that use the noun type.  For example, we define Category, so
+   *  for the "attachmentTypes" attribute, "attachmentTypesCategory" would be
+   *  exposed.
+   */
+  queryHelpers: {
+    /**
+     * Query for MIME type categories based on one or more MIME type objects
+     *  passed in.  We want the range to span the entire block allocated to the
+     *  category.
+     *
+     * @param aAttrDef The attribute that is using us.
+     * @param aArguments The actual arguments object that
+     */
+    Category: function(aAttrDef, aArguments) {
+      let rangePairs = [];
+      // If there are no arguments then we want to fall back to the 'in'
+      //  constraint which matches on any attachment.
+      if (aArguments.length == 0)
+        return this._inConstraintHelper(aAttrDef, []);
+
+      for (let iArg = 0; iArg < aArguments.length; iArg++) {
+        let arg = aArguments[iArg];
+        rangePairs.push(MimeTypeNoun._mimeTypeRangeDummyObjects[arg.category]);
+      }
+      return this._rangedConstraintHelper(aAttrDef, rangePairs);
+    }
+  },
+
+  comparator: function gloda_noun_mimeType_comparator(a, b) {
+    if (a == null) {
+      if (b == null)
+        return 0;
+      else
+        return 1;
+    }
+    else if (b == null) {
+      return -1;
+    }
+    return a.fullType.localeCompare(b.fullType);
+  },
+
   toParamAndValue: function gloda_noun_mimeType_toParamAndValue(aMimeType) {
     return [null, aMimeType.id];
   },
   toJSON: function gloda_noun_mimeType_toJSON(aMimeType) {
     return aMimeType.id;
   },
   fromJSON: function gloda_noun_mimeType_fromJSON(aMimeTypeID) {
     return this._mimeTypesByID[aMimeTypeID];
--- a/mailnews/db/gloda/modules/noun_tag.js
+++ b/mailnews/db/gloda/modules/noun_tag.js
@@ -1,16 +1,16 @@
 /* ***** 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 Global Database.
  *
  * The Initial Developer of the Original Code is
@@ -27,60 +27,77 @@
  * 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 ***** */
 
 EXPORTED_SYMBOLS = ['TagNoun'];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://app/modules/gloda/gloda.js");
 
 /**
  * @namespace Tag noun provider.
  */
 var TagNoun = {
   name: "tag",
-  class: Ci.nsIMsgTag,
+  clazz: Ci.nsIMsgTag,
   usesParameter: true,
   allowsArbitraryAttrs: false,
+  idAttr: "key",
   _msgTagService: null,
   _tagMap: null,
-  
+
   _init: function () {
     this._msgTagService = Cc["@mozilla.org/messenger/tagservice;1"].
                           getService(Ci.nsIMsgTagService);
     this._updateTagMap();
   },
-  
+
   getAllTags: function gloda_noun_tag_getAllTags() {
     return this._msgTagService.getAllTags({});
   },
-  
+
   _updateTagMap: function gloda_noun_tag_updateTagMap() {
     this._tagMap = {};
     let tagArray = this._msgTagService.getAllTags({});
     for (let iTag = 0; iTag < tagArray.length; iTag++) {
       let tag = tagArray[iTag];
       this._tagMap[tag.key] = tag;
     }
   },
-  
+
+  comparator: function gloda_noun_tag_comparator(a, b) {
+    if (a == null) {
+      if (b == null)
+        return 0;
+      else
+        return 1;
+    }
+    else if (b == null) {
+      return -1;
+    }
+    return a.tag.localeCompare(b.tag);
+  },
+  userVisibleString: function gloda_noun_tag_userVisibleString(aTag) {
+    return aTag.tag;
+  },
+
   // we cannot be an attribute value
-  
+
   toParamAndValue: function gloda_noun_tag_toParamAndValue(aTag) {
     return [aTag.key, null];
   },
   toJSON: function gloda_noun_tag_toJSON(aTag) {
     return aTag.key;
   },
   fromJSON: function gloda_noun_tag_fromJSON(aTagKey, aIgnored) {
     let tag = this._tagMap[aTagKey];
--- a/mailnews/db/gloda/modules/query.js
+++ b/mailnews/db/gloda/modules/query.js
@@ -130,16 +130,17 @@ GlodaQueryClass.prototype = {
    * Return a collection asynchronously populated by this collection.  You must
    *  provide a listener to receive notifications from the collection as it
    *  receives updates.  The listener object should implement onItemsAdded,
    *  onItemsModified, and onItemsRemoved methods, all of which take a single
    *  argument which is the list of items which have been added, modified, or
    *  removed respectively.
    */
   getCollection: function gloda_query_getCollection(aListener, aData) {
+    this.completed = false;
     return this._nounDef.datastore.queryFromQuery(this, aListener, aData);
   },
 
   /**
    * Test whether the given first-class noun instance satisfies this query.
    *
    * @testpoint gloda.query.test
    */
@@ -152,16 +153,17 @@ GlodaQueryClass.prototype = {
       let curQuery = unionQueries[iUnion];
 
       // assume success until a specific (or) constraint proves us wrong
       let querySatisfied = true;
       for (let iConstraint = 0; iConstraint < curQuery._constraints.length;
            iConstraint++) {
         let constraint = curQuery._constraints[iConstraint];
         let [constraintType, attrDef] = constraint;
+        let boundName = attrDef ? attrDef.boundName : "id";
         let constraintValues = constraint.slice(2);
 
         if (constraintType === GlodaDatastore.kConstraintIdIn) {
           if (constraintValues.indexOf(aObj.id) == -1) {
             querySatisfied = false;
             break;
           }
         }
@@ -171,20 +173,28 @@ GlodaQueryClass.prototype = {
           let objectNounDef = attrDef.objectNounDef;
 
           // if they provide an equals comparator, use that.
           // (note: the next case has better optimization possibilities than
           //  this mechanism, but of course has higher initialization costs or
           //  code complexity costs...)
           if (objectNounDef.equals) {
             let testValues;
-            if (attrDef.singular)
-              testValues = [aObj[attrDef.boundName]];
+            if (!(boundName in aObj))
+              testValues = [];
+            else if (attrDef.singular)
+              testValues = [aObj[boundName]];
             else
-              testValues = aObj[attrDef.boundName];
+              testValues = aObj[boundName];
+
+            // If there are no constraints, then we are just testing for there
+            //  being a value.  Succeed (continue) in that case.
+            if (constraintValues.length == 0 && testValues.length &&
+                testValues[0] != null)
+              continue;
 
             let foundMatch = false;
             for each (let [,testValue] in Iterator(testValues)) {
               for each (let [,value] in Iterator(constraintValues)) {
                 if (objectNounDef.equals(testValue, value)) {
                   foundMatch = true;
                   break;
                 }
@@ -199,20 +209,28 @@ GlodaQueryClass.prototype = {
           }
           // otherwise, we need to convert everyone to their param/value form
           //  in order to test for equality
           else {
             // let's just do the simple, obvious thing for now.  which is
             //  what we did in the prior case but exploding values using
             //  toParamAndValue, and then comparing.
             let testValues;
-            if (attrDef.singular)
-              testValues = [aObj[attrDef.boundName]];
+            if (!(boundName in aObj))
+              testValues = [];
+            else if (attrDef.singular)
+              testValues = [aObj[boundName]];
             else
-              testValues = aObj[attrDef.boundName];
+              testValues = aObj[boundName];
+
+            // If there are no constraints, then we are just testing for there
+            //  being a value.  Succeed (continue) in that case.
+            if (constraintValues.length == 0 && testValues.length &&
+                testValues[0] != null)
+              continue;
 
             let foundMatch = false;
             for each (let [,testValue] in Iterator(testValues)) {
               let [aParam, aValue] = objectNounDef.toParamAndValue(testValue);
               for each (let [,value] in Iterator(constraintValues)) {
                 let [bParam, bValue] = objectNounDef.toParamAndValue(value);
                 if (aParam == bParam && aValue == bValue) {
                   foundMatch = true;
@@ -228,20 +246,22 @@ GlodaQueryClass.prototype = {
             }
           }
         }
         // @testpoint gloda.query.test.kConstraintRanges
         else if (constraintType === GlodaDatastore.kConstraintRanges) {
           let objectNounDef = attrDef.objectNounDef;
 
           let testValues;
-          if (attrDef.singular)
-            testValues = [aObj[attrDef.boundName]];
+          if (!(boundName in aObj))
+              testValues = [];
+          else if (attrDef.singular)
+            testValues = [aObj[boundName]];
           else
-            testValues = aObj[attrDef.boundName];
+            testValues = aObj[boundName];
 
           let foundMatch = false;
           for each (let [,testValue] in Iterator(testValues)) {
             let [tParam, tValue] = objectNounDef.toParamAndValue(testValue);
             for each (let [,rangeTuple] in Iterator(constraintValues)) {
               let [lowerRValue, upperRValue] = rangeTuple;
               if (lowerRValue == null) {
                 let [upperParam, upperValue] =
@@ -277,17 +297,17 @@ GlodaQueryClass.prototype = {
           if (!foundMatch) {
             querySatisfied = false;
             break;
           }
         }
         // @testpoint gloda.query.test.kConstraintStringLike
         else if (constraintType === GlodaDatastore.kConstraintStringLike) {
           let curIndex = 0;
-          let value = aObj[attrDef.boundName];
+          let value = (boundName in aObj) ? aObj[boundName] : "";
           // the attribute must be singular, we don't support arrays of strings.
           for each (let [iValuePart, valuePart] in Iterator(constraintValues)) {
             if (typeof valuePart == "string") {
               let index = value.indexOf(valuePart);
               // if curIndex is null, we just need any match
               // if it's not null, it must match the offset of our found match
               if (curIndex === null) {
                 if (index == -1)
@@ -327,16 +347,47 @@ GlodaQueryClass.prototype = {
           break;
       }
 
       if (querySatisfied)
         return true;
     }
     return false;
   },
+
+  /**
+   * Helper code for noun definitions of queryHelpers that want to build a
+   *  traditional in/equals constraint.  The goal is to let them build a range
+   *  without having to know how we structure |_constraints|.
+   *
+   * @protected
+   */
+  _inConstraintHelper:
+      function gloda_query__discreteConstraintHelper(aAttrDef, aValues) {
+    let constraint =
+      [GlodaDatastore.kConstraintIn, aAttrDef].concat(aValues);
+    this._constraints.push(constraint);
+    return this;
+  },
+
+  /**
+   * Helper code for noun definitions of queryHelpers that want to build a
+   *  range.  The goal is to let them build a range without having to know how
+   *  we structure |_constraints| or requiring them to mark themselves as
+   *  continuous to get a "Range".
+   *
+   * @protected
+   */
+  _rangedConstraintHelper:
+      function gloda_query__rangedConstraintHelper(aAttrDef, aRanges) {
+    let constraint =
+      [GlodaDatastore.kConstraintRanges, aAttrDef].concat(aRanges);
+    this._constraints.push(constraint);
+    return this;
+  }
 };
 
 /**
  * @class A query that never matches anything.
  *
  * Collections corresponding to this query are intentionally frozen in time and
  *  do not want to be notified of any updates.  We need the collection to be
  *  registered with the collection manager so that the noun instances in the
--- a/mailnews/db/gloda/modules/utils.js
+++ b/mailnews/db/gloda/modules/utils.js
@@ -59,16 +59,19 @@ var GlodaUtils = {
 
   _headerParser: null,
 
   /**
    * Parses an RFC 2822 list of e-mail addresses and returns an object with
    *  4 attributes, as described below.  We will use the example of the user
    *  passing an argument of '"Bob Smith" <bob@company.com>'.
    *
+   * This method (by way of nsIMsgHeaderParser) takes care of decoding mime
+   *  headers, but is not aware of folder-level character set overrides.
+   *
    * count: the number of addresses parsed. (ex: 1)
    * addresses: a list of e-mail addresses (ex: ["bob@company.com"])
    * names: a list of names (ex: ["Bob Smith"])
    * fullAddresses: aka the list of name and e-mail together (ex: ['"Bob Smith"
    *  <bob@company.com>']).
    *
    * This method is a convenience wrapper around nsIMsgHeaderParser.
    */
--- a/mailnews/db/gloda/test/unit/resources/glodaTestHelper.js
+++ b/mailnews/db/gloda/test/unit/resources/glodaTestHelper.js
@@ -33,16 +33,18 @@
  * 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 ***** */
 
 // -- Pull in the POP3 fake-server / local account helper code
 load("../../test_mailnewslocal/unit/head_maillocal.js");
 
+Components.utils.import("resource://app/modules/errUtils.js");
+
 /**
  * Create a 'me' identity of "me@localhost" for the benefit of Gloda.  At the
  *  time of this writing, Gloda only initializes Gloda.myIdentities and
  *  Gloda.myContact at startup with no event-driven updates.  As such, this
  *  function needs to be called prior to gloda startup.
  */
 function createMeIdentity() {
   var acctMgr = Cc["@mozilla.org/messenger/account-manager;1"]
@@ -870,19 +872,21 @@ QueryExpectationListener.prototype = {
                   "") + "\n");
           }
           throw ex;
         }
       }
       this.nextIndex++;
 
       // make sure the query's test method agrees with the database about this
-      if (!aCollection.query.test(item))
+      if (!aCollection.query.test(item)) {
+        logObject(item);
         do_throw("Query test returned false when it should have been true on " +
                  "extracted: " + glodaStringRep + " item: " + item);
+      }
     }
   },
   onItemsModified: function query_expectation_onItemsModified(aItems,
       aCollection) {
   },
   onItemsRemoved: function query_expectation_onItemsRemoved(aItems,
       aCollection) {
   },
--- a/mailnews/db/gloda/test/unit/test_query_core.js
+++ b/mailnews/db/gloda/test/unit/test_query_core.js
@@ -54,17 +54,18 @@ var WidgetProvider = {
 };
 
 var WidgetNoun;
 function setup_test_noun_and_attributes() {
   // --- noun
   WidgetNoun = Gloda.defineNoun({
     name: "widget",
     clazz: Widget,
-    allowArbitraryAttrs: true,
+    allowsArbitraryAttrs: true,
+    //cache: true, cacheCost: 32,
     schema: {
       columns: [['id', 'INTEGER PRIMARY KEY'],
                 ['intCol', 'NUMBER', 'inum'],
                 ['dateCol', 'NUMBER', 'date'],
                 ['strCol', 'STRING', 'str'],
                 ['notabilityCol', 'NUMBER', 'notability'],
                 ['textOne', 'STRING', 'text1'],
                 ['textTwo', 'STRING', 'text2']],
@@ -77,60 +78,60 @@ function setup_test_noun_and_attributes(
   });
 
   EXT_NAME = "test";
 
   // --- special (on-row) attributes
   Gloda.defineAttribute({
     provider: WidgetProvider, extensionName: EXT_NAME,
     attributeType: Gloda.kAttrFundamental,
-    attributeName: "intCol",
+    attributeName: "inum",
     singular: true,
     special: Gloda.kSpecialColumn,
     specialColumnName: "intCol",
     subjectNouns: [WidgetNoun.id],
     objectNoun: Gloda.NOUN_NUMBER
   });
   Gloda.defineAttribute({
     provider: WidgetProvider, extensionName: EXT_NAME,
     attributeType: Gloda.kAttrFundamental,
-    attributeName: "dateCol",
+    attributeName: "date",
     singular: true,
     special: Gloda.kSpecialColumn,
     specialColumnName: "dateCol",
     subjectNouns: [WidgetNoun.id],
     objectNoun: Gloda.NOUN_DATE
   });
   Gloda.defineAttribute({
     provider: WidgetProvider, extensionName: EXT_NAME,
     attributeType: Gloda.kAttrFundamental,
-    attributeName: "strCol",
+    attributeName: "str",
     singular: true,
     special: Gloda.kSpecialString,
     specialColumnName: "strCol",
     subjectNouns: [WidgetNoun.id],
     objectNoun: Gloda.NOUN_STRING
   });
 
 
   // --- fulltext attributes
   Gloda.defineAttribute({
     provider: WidgetProvider, extensionName: EXT_NAME,
     attributeType: Gloda.kAttrFundamental,
-    attributeName: "fulltextOne",
+    attributeName: "text1",
     singular: true,
     special: Gloda.kSpecialFulltext,
     specialColumnName: "fulltextOne",
     subjectNouns: [WidgetNoun.id],
     objectNoun: Gloda.NOUN_FULLTEXT
   });
   Gloda.defineAttribute({
     provider: WidgetProvider, extensionName: EXT_NAME,
     attributeType: Gloda.kAttrFundamental,
-    attributeName: "fulltextTwo",
+    attributeName: "text2",
     singular: true,
     special: Gloda.kSpecialFulltext,
     specialColumnName: "fulltextTwo",
     subjectNouns: [WidgetNoun.id],
     objectNoun: Gloda.NOUN_FULLTEXT
   });
   Gloda.defineAttribute({
     provider: WidgetProvider, extensionName: EXT_NAME,
@@ -148,16 +149,25 @@ function setup_test_noun_and_attributes(
     provider: WidgetProvider, extensionName: EXT_NAME,
     attributeType: Gloda.kAttrFundamental,
     attributeName: "singleIntAttr",
     singular: true,
     subjectNouns: [WidgetNoun.id],
     objectNoun: Gloda.NOUN_NUMBER
   });
 
+  Gloda.defineAttribute({
+    provider: WidgetProvider, extensionName: EXT_NAME,
+    attributeType: Gloda.kAttrFundamental,
+    attributeName: "multiIntAttr",
+    singular: false,
+    subjectNouns: [WidgetNoun.id],
+    objectNoun: Gloda.NOUN_NUMBER
+  });
+
   next_test();
 }
 
 /* ===== Tests ===== */
 
 ALPHABET = "abcdefghijklmnopqrstuvwxyz";
 function test_lots_of_string_constraints() {
   let stringConstraints = [];
@@ -169,21 +179,67 @@ function test_lots_of_string_constraints
                            ALPHABET[i % ALPHABET.length] +
                            // throw in something that will explode if not quoted
                            // (and use an uneven number of things so if we fail
                            // to quote it won't get quietly eaten.)
                            "'" + '"');
   }
 
   let query = Gloda.newQuery(WidgetNoun.id);
-  query.strCol.apply(query, stringConstraints);
+  query.str.apply(query, stringConstraints);
 
   queryExpect(query, []);
 }
 
+/* === Query === */
+
+/**
+ * Use a counter so that each test can have its own unique value for intCol so
+ *  that it can use that as a constraint.  Otherwise we would need to purge
+ *  between every test.  That's not an unreasonable alternative, but this works.
+ * Every test should increment this before using it.
+ */
+var testUnique = 100;
+
+/**
+ * Widgets with multiIntAttr populated with one or more values.
+ */
+var nonSingularWidgets;
+/**
+ * Widgets with multiIntAttr unpopulated.
+ */
+var singularWidgets;
+
+function setup_non_singular_values() {
+  testUnique++;
+  let origin = new Date("2007/01/01");
+  nonSingularWidgets = [
+    new Widget(testUnique, origin, "ns1", 0, "", ""),
+    new Widget(testUnique, origin, "ns2", 0, "", ""),
+  ];
+  singularWidgets = [
+    new Widget(testUnique, origin, "s1", 0, "", ""),
+    new Widget(testUnique, origin, "s2", 0, "", ""),
+  ];
+  nonSingularWidgets[0].multiIntAttr = [1, 2];
+  nonSingularWidgets[1].multiIntAttr = [3];
+  singularWidgets[0].multiIntAttr = [];
+  // and don't bother setting it on singularWidgets[1]
+
+  runOnIndexingComplete(next_test);
+  GenericIndexer.indexNewObjects(nonSingularWidgets.concat(singularWidgets));
+}
+
+function test_query_has_value_for_non_singular() {
+  let query = Gloda.newQuery(WidgetNoun.id);
+  query.inum(testUnique);
+  query.multiIntAttr();
+  queryExpect(query, nonSingularWidgets);
+}
+
 /* === Search === */
 /*
  * The conceit of our search is that more recent messages are better than older
  *  messages.  But at the same time, we care about some messages more than
  *  others (in general), and we care about messages that match search terms
  *  more strongly too.  So we introduce a general 'score' heuristic which we
  *  then apply to message timestamps to make them appear more recent.  We
  *  then order by this 'date score' hybrid, which we dub "dascore".  Such a
@@ -306,16 +362,18 @@ function test_search_ranking_idiom_score
 }
 
 
 /* ===== Driver ===== */
 
 var tests = [
   setup_test_noun_and_attributes,
   test_lots_of_string_constraints,
+  setup_non_singular_values,
+  test_query_has_value_for_non_singular,
   setup_search_ranking_idiom,
   test_search_ranking_idiom_offsets,
   test_search_ranking_idiom_score,
 ];
 
 function run_test() {
   // use mbox injection so we get multiple folders...
   injectMessagesUsing(INJECT_MBOX);