Bug 511131 - "Advanced search uses constrained online search scope, but is actually searching offline". r=Standard8,sr=bienvenu
authorKent James <kent@caspia.com>
Fri, 04 Sep 2009 17:30:07 -0700
changeset 3542 f83b9d5dddd1333a7ec6b62ff8cd168bcb0b6f25
parent 3541 dde243bf059e4198a7799cf1717af76de3da007c
child 3543 b4c1ff77f3b2c5633d88dae46bb13da336fb321a
push idunknown
push userunknown
push dateunknown
reviewersStandard8, bienvenu
bugs511131
Bug 511131 - "Advanced search uses constrained online search scope, but is actually searching offline". r=Standard8,sr=bienvenu
mail/base/content/SearchDialog.js
mail/base/content/SearchDialog.xul
mail/locales/en-US/chrome/messenger/SearchDialog.dtd
mailnews/base/content/virtualFolderProperties.js
mailnews/base/search/public/nsMsgSearchAdapter.h
mailnews/base/search/public/nsMsgSearchCore.idl
mailnews/base/search/src/nsMsgLocalSearch.cpp
mailnews/base/search/src/nsMsgSearchAdapter.cpp
mailnews/base/search/src/nsMsgSearchTerm.cpp
mailnews/base/src/searchSpec.js
mailnews/news/src/nsNntpIncomingServer.cpp
--- a/mail/base/content/SearchDialog.js
+++ b/mail/base/content/SearchDialog.js
@@ -55,16 +55,19 @@ var gSearchBundle;
 
 // Datasource search listener -- made global as it has to be registered
 // and unregistered in different functions.
 var gDataSourceSearchListener;
 var gViewSearchListener;
 
 var gSearchStopButton;
 
+// Should we try to search online?
+var gSearchOnline = false;
+
 // Controller object for search results thread pane
 var nsSearchResultsController =
 {
     supportsCommand: function(command)
     {
         switch(command) {
         case "cmd_delete":
         case "cmd_shiftDelete":
@@ -343,19 +346,36 @@ function selectFolder(folder)
 
 function updateSearchFolderPicker(folderURI)
 {
     SetFolderPicker(folderURI, gFolderPicker.id);
 
     // use the URI to get the real folder
     gCurrentFolder = GetMsgFolderFromUri(folderURI);
 
-    var searchLocalSystem = document.getElementById("checkSearchLocalSystem");
-    if (searchLocalSystem)
-        searchLocalSystem.disabled = gCurrentFolder.server.searchScope == nsMsgSearchScope.offlineMail;
+    var searchOnline = document.getElementById("checkSearchOnline");
+    if (searchOnline)
+    {
+      // We will clear and disable the search online checkbox if we are offline, or
+      // if the folder does not support online search.
+
+      // Anything greater than 0 is an online server like IMAP or news.
+      if (gCurrentFolder.server.offlineSupportLevel &&
+          !Components.classes["@mozilla.org/network/io-service;1"]
+                             .getService(Components.interfaces.nsIIOService)
+                             .offline)
+      {
+        searchOnline.disabled = false;
+      }
+      else
+      {
+        searchOnline.checked = false;
+        searchOnline.disabled = true;
+      }
+    }
     setSearchScope(GetScopeForFolder(gCurrentFolder));
 }
 
 function updateSearchLocalSystem()
 {
   setSearchScope(GetScopeForFolder(gCurrentFolder));
 }
 
@@ -387,16 +407,17 @@ function onEnterInSearchTerm()
 
 function onSearch()
 {
   let viewWrapper = gFolderDisplay.view;
   let searchTerms = getSearchTerms();
 
   viewWrapper.beginViewUpdate();
   viewWrapper.search.userTerms = searchTerms.length ? searchTerms : null;
+  viewWrapper.search.onlineSearch = gSearchOnline;
   viewWrapper.searchFolders = getSearchFolders();
   viewWrapper.endViewUpdate();
 }
 
 /**
  * Get the current set of search terms, returning them as a list.  We filter out
  *  dangerous and insane predicates.
  */
@@ -481,21 +502,79 @@ function AddSubFoldersToURI(folder)
           returnString += '|';
         returnString += subFoldersString;
       }
     }
   }
   return returnString;
 }
 
-
+/**
+ * Determine the proper search scope to use for a folder, so that the user is
+ *  presented with a correct list of search capabilities. The user may manually
+ *  request on online search for certain server types. To determine if the
+ *  folder body may be searched, we ignore whether autosync is enabled,
+ *  figuring that after the user manually syncs, they would still expect that
+ *  body searches would work.
+ *  
+ * The available search capabilities also depend on whether the user is
+ *  currently online or offline. Although that is also checked by the server,
+ *  we do it ourselves because we have a more complex response to offline
+ *  than the server's searchScope attribute provides.
+ *
+ * This method only works for real folders.
+ */
 function GetScopeForFolder(folder)
 {
-  var searchLocalSystem = document.getElementById("checkSearchLocalSystem");
-  return searchLocalSystem && searchLocalSystem.checked ? nsMsgSearchScope.offlineMail : folder.server.searchScope;
+  let searchOnline = document.getElementById("checkSearchOnline");
+  if (searchOnline && searchOnline.checked)
+  {
+    gSearchOnline = true;
+    return folder.server.searchScope;
+  }
+  gSearchOnline = false;
+
+  // We are going to search offline. The proper search scope may depend on
+  // whether we have the body and/or junk available or not.
+  let localType;
+  try
+  {
+    localType = folder.server.localStoreType;
+  }
+  catch (e) {} // On error, we'll just assume the default mailbox type
+
+  let hasBody = folder.getFlag(Components.interfaces.nsMsgFolderFlags.Offline);
+  let nsMsgSearchScope = Components.interfaces.nsMsgSearchScope;
+  switch (localType)
+  {
+    case "news":
+      // News has four offline scopes, depending on whether junk and body
+      // are available.
+      let hasJunk = 
+        folder.getInheritedStringProperty("dobayes.mailnews@mozilla.org#junk")
+               == "true";
+      if (hasJunk && hasBody)
+        return nsMsgSearchScope.localNewsJunkBody;
+      if (hasJunk) // and no body
+        return nsMsgSearchScope.localNewsJunk;
+      if (hasBody) // and no junk
+        return nsMsgSearchScope.localNewsBody;
+      // We don't have offline message bodies or junk processing.
+      return nsMsgSearchScope.localNews;
+
+    case "imap":
+      // Junk is always enabled for imap, so the offline scope only depends on
+      // whether the body is available.
+      if (!hasBody)
+        return nsMsgSearchScope.onlineManual;
+        // fall through to default
+    default:
+      return nsMsgSearchScope.offlineMail;
+  }
+
 }
 
 var nsMsgViewSortType = Components.interfaces.nsMsgViewSortType;
 var nsMsgViewSortOrder = Components.interfaces.nsMsgViewSortOrder;
 var nsMsgViewFlagsType = Components.interfaces.nsMsgViewFlagsType;
 var nsMsgViewCommandType = Components.interfaces.nsMsgViewCommandType;
 
 function goUpdateSearchItems(commandset)
@@ -583,11 +662,12 @@ function saveAsVirtualFolder()
       searchFolderURIs += '|' + subFolderURIs;
   }
 
   var dialog = window.openDialog("chrome://messenger/content/virtualFolderProperties.xul", "",
                                  "chrome,titlebar,modal,centerscreen",
                                  {folder: window.arguments[0].folder,
                                   searchTerms: toXPCOMArray(getSearchTerms(),
                                                             Components.interfaces.nsISupportsArray),
-                                  searchFolderURIs: searchFolderURIs});
+                                  searchFolderURIs: searchFolderURIs,
+                                  searchOnline: document.getElementById("checkSearchOnline").checked});
 }
 
--- a/mail/base/content/SearchDialog.xul
+++ b/mail/base/content/SearchDialog.xul
@@ -118,16 +118,22 @@
          <button id="search-button" oncommand="onSearchButton(event);" default="true"/>
         </hbox>
 
          <hbox align="center">
            <checkbox id="checkSearchSubFolders" label="&searchSubfolders.label;" checked="true" accesskey="&searchSubfolders.accesskey;"/>
            <spacer flex="10"/>
            <button label="&resetButton.label;" oncommand="onResetSearch(event);" accesskey="&resetButton.accesskey;"/>
          </hbox>
+         <hbox align="center">
+           <checkbox id="checkSearchOnline"
+                     label="&searchOnline.label;"
+                     accesskey="&searchOnline.accesskey;"
+                     oncommand="updateSearchLocalSystem();"/>
+         </hbox>
       </vbox>
 
       <hbox flex="1">
         <vbox id="searchTermListBox" flex="1"/>
       </hbox>
     </vbox>
 
     <splitter id="gray_horizontal_splitter" collapse="after" persist="state"/>
--- a/mail/locales/en-US/chrome/messenger/SearchDialog.dtd
+++ b/mail/locales/en-US/chrome/messenger/SearchDialog.dtd
@@ -1,13 +1,15 @@
 <!-- for SearchDialog.xul -->
 <!ENTITY searchHeading.label         "Search for messages in:">
 <!ENTITY searchHeading.accesskey     "h">
 <!ENTITY searchSubfolders.label      "Search subfolders">
 <!ENTITY searchSubfolders.accesskey  "e">
+<!ENTITY searchOnline.label          "Search online">
+<!ENTITY searchOnline.accesskey      "l">
 <!ENTITY resetButton.label           "Clear">
 <!ENTITY resetButton.accesskey       "C">
 <!ENTITY openButton.label            "Open">
 <!ENTITY openButton.accesskey        "n">
 <!ENTITY deleteButton.label          "Delete">
 <!ENTITY deleteButton.accesskey      "D">
 <!ENTITY searchDialogTitle.label     "Search Messages">
 <!ENTITY results.label               "Results">
--- a/mailnews/base/content/virtualFolderProperties.js
+++ b/mailnews/base/content/virtualFolderProperties.js
@@ -93,16 +93,18 @@ function onLoad()
       document.getElementById("name").value = arguments.newFolderName;
     if (arguments.searchFolderURIs)
       gSearchFolderURIs = arguments.searchFolderURIs;
 
     setupSearchRows(gSearchTermSession.searchTerms);
     doEnabling(); // we only need to disable/enable the OK button for new virtual folders
   }
 
+  if (typeof arguments.searchOnline != "undefined")
+    document.getElementById('searchOnline').checked = arguments.searchOnline;
   updateOnlineSearchState();
   doSetOKCancel(onOK, onCancel);
 }
 
 function setupSearchRows(aSearchTerms)
 {
   if (aSearchTerms && aSearchTerms.Count() > 0)
     initializeSearchRows(nsMsgSearchScope.offlineMail, aSearchTerms); // load the search terms for the folder
--- a/mailnews/base/search/public/nsMsgSearchAdapter.h
+++ b/mailnews/base/search/public/nsMsgSearchAdapter.h
@@ -197,33 +197,41 @@ protected:
   // object makes cleanup of these tables (at shutdown-time) automagic.
 
   nsCOMPtr<nsIMsgSearchValidityTable> m_offlineMailTable;
   nsCOMPtr<nsIMsgSearchValidityTable> m_offlineMailFilterTable;
   nsCOMPtr<nsIMsgSearchValidityTable> m_onlineMailTable;
   nsCOMPtr<nsIMsgSearchValidityTable> m_onlineMailFilterTable;
   nsCOMPtr<nsIMsgSearchValidityTable> m_onlineManualFilterTable;
 
-  nsCOMPtr<nsIMsgSearchValidityTable> m_newsTable;
-  nsCOMPtr<nsIMsgSearchValidityTable> m_localNewsTable; // used for local news searching or offline news searching...
+  nsCOMPtr<nsIMsgSearchValidityTable> m_newsTable;      // online news
+
+  // Local news tables, used for local news searching or offline.
+  nsCOMPtr<nsIMsgSearchValidityTable> m_localNewsTable;         // base table
+  nsCOMPtr<nsIMsgSearchValidityTable> m_localNewsJunkTable;     // base + junk
+  nsCOMPtr<nsIMsgSearchValidityTable> m_localNewsBodyTable;     // base + body
+  nsCOMPtr<nsIMsgSearchValidityTable> m_localNewsJunkBodyTable; // base + junk + body
   nsCOMPtr<nsIMsgSearchValidityTable> m_ldapTable;
   nsCOMPtr<nsIMsgSearchValidityTable> m_ldapAndTable;
   nsCOMPtr<nsIMsgSearchValidityTable> m_localABTable;
   nsCOMPtr<nsIMsgSearchValidityTable> m_localABAndTable;
   nsCOMPtr<nsIMsgSearchValidityTable> m_newsFilterTable;
 
   nsresult NewTable (nsIMsgSearchValidityTable **);
 
   nsresult InitOfflineMailTable();
   nsresult InitOfflineMailFilterTable();
   nsresult InitOnlineMailTable();
   nsresult InitOnlineMailFilterTable();
   nsresult InitOnlineManualFilterTable();
   nsresult InitNewsTable();
   nsresult InitLocalNewsTable();
+  nsresult InitLocalNewsJunkTable();
+  nsresult InitLocalNewsBodyTable();
+  nsresult InitLocalNewsJunkBodyTable();
   nsresult InitNewsFilterTable();
 
   //set the custom headers in the table, changes whenever "mailnews.customHeaders" pref changes.
   nsresult SetOtherHeadersInTable(nsIMsgSearchValidityTable *table, const char *customHeaders);
 
   nsresult InitLdapTable();
   nsresult InitLdapAndTable();
   nsresult InitLocalABTable();
--- a/mailnews/base/search/public/nsMsgSearchCore.idl
+++ b/mailnews/base/search/public/nsMsgSearchCore.idl
@@ -48,27 +48,34 @@ interface nsIMsgDBHdr;
 
 [scriptable, uuid(5fe70a74-304e-11d3-9be1-00a0c900d445)]
 
 interface nsMsgSearchScope {
   const nsMsgSearchScopeValue offlineMail = 0;
   const nsMsgSearchScopeValue offlineMailFilter = 1;
   const nsMsgSearchScopeValue onlineMail = 2;
   const nsMsgSearchScopeValue onlineMailFilter = 3;
+  /// offline news, base table, no body or junk
   const nsMsgSearchScopeValue localNews = 4;
   const nsMsgSearchScopeValue news = 5;
   const nsMsgSearchScopeValue newsEx = 6;
   const nsMsgSearchScopeValue LDAP = 7;
   const nsMsgSearchScopeValue LocalAB = 8;
   const nsMsgSearchScopeValue allSearchableGroups = 9;
   const nsMsgSearchScopeValue newsFilter = 10;
   const nsMsgSearchScopeValue LocalABAnd = 11;
   const nsMsgSearchScopeValue LDAPAnd = 12;
   // IMAP and NEWS, searched using local headers
   const nsMsgSearchScopeValue onlineManual = 13;
+  /// local news + junk
+  const nsMsgSearchScopeValue localNewsJunk = 14;
+  /// local news + body
+  const nsMsgSearchScopeValue localNewsBody = 15;
+  /// local news + junk + body
+  const nsMsgSearchScopeValue localNewsJunkBody = 16;
 };
 
 typedef long nsMsgSearchAttribValue;
 
 /**
  * Definitions of search attribute types. The numerical order
  * from here will also be used to determine the order that the
  * attributes display in the filter editor.
--- a/mailnews/base/search/src/nsMsgLocalSearch.cpp
+++ b/mailnews/base/search/src/nsMsgLocalSearch.cpp
@@ -873,81 +873,173 @@ nsresult nsMsgSearchOfflineNews::OpenSum
   return err;
 }
 
 nsresult nsMsgSearchOfflineNews::ValidateTerms ()
 {
   return nsMsgSearchOfflineMail::ValidateTerms ();
 }
 
+// local helper functions to set subsets of the validity table
 
-//-----------------------------------------------------------------------------
+nsresult SetJunk(nsIMsgSearchValidityTable* aTable)
+{
+  NS_ENSURE_ARG_POINTER(aTable);
+
+  aTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Is, 1);
+  aTable->SetEnabled(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Is, 1);
+  aTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Isnt, 1);
+  aTable->SetEnabled(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Isnt, 1);
+
+  aTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsGreaterThan, 1);
+  aTable->SetEnabled(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsGreaterThan, 1);
+  aTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsLessThan, 1);
+  aTable->SetEnabled(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsLessThan, 1);
+  aTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::Is, 1);
+  aTable->SetEnabled(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::Is, 1);
+
+  aTable->SetAvailable(nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Is, 1);
+  aTable->SetEnabled(nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Is, 1);
+  aTable->SetAvailable(nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Isnt, 1);
+  aTable->SetEnabled(nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Isnt, 1);
+
+  return NS_OK;
+}
+
+nsresult SetBody(nsIMsgSearchValidityTable* aTable)
+{
+  NS_ENSURE_ARG_POINTER(aTable);
+  aTable->SetAvailable (nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Body, nsMsgSearchOp::Is, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Body, nsMsgSearchOp::Is, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Body, nsMsgSearchOp::Isnt, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Body, nsMsgSearchOp::Isnt, 1);
+
+  return NS_OK;
+}
+
+// set the base validity table values for local news
+nsresult SetLocalNews(nsIMsgSearchValidityTable* aTable)
+{
+  NS_ENSURE_ARG_POINTER(aTable);
+
+  aTable->SetAvailable (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1);
+
+  aTable->SetAvailable (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
+
+  aTable->SetAvailable (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
+
+  aTable->SetAvailable (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan,  1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is,  1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1);
+
+  aTable->SetAvailable (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1);
+
+  aTable->SetAvailable (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1);
+
+  aTable->SetAvailable (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
+  aTable->SetAvailable (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+  aTable->SetEnabled   (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
+  return NS_OK;
+
+}
+
 nsresult nsMsgSearchValidityManager::InitLocalNewsTable()
 {
-  NS_ASSERTION (nsnull == m_localNewsTable, "already have local news validty table");
-  nsresult err = NewTable (getter_AddRefs(m_localNewsTable));
-
-  if (NS_SUCCEEDED(err))
-  {
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1);
-
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1);
-
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::Body, nsMsgSearchOp::Is, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::Body, nsMsgSearchOp::Is, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::Body, nsMsgSearchOp::Isnt, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::Body, nsMsgSearchOp::Isnt, 1);
-
+  NS_ASSERTION (nsnull == m_localNewsTable, "already have local news validity table");
+  nsresult rv = NewTable(getter_AddRefs(m_localNewsTable));
+  NS_ENSURE_SUCCESS(rv, rv);
+  return SetLocalNews(m_localNewsTable);
+}
 
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1);
-
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan,  1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is,  1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1);
-
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1);
-
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1);
-    m_localNewsTable->SetAvailable (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
-    m_localNewsTable->SetEnabled   (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1);
-
-
-  }
-
-  return err;
+nsresult nsMsgSearchValidityManager::InitLocalNewsBodyTable()
+{
+  NS_ASSERTION (nsnull == m_localNewsBodyTable, "already have local news+body validity table");
+  nsresult rv = NewTable(getter_AddRefs(m_localNewsBodyTable));
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = SetLocalNews(m_localNewsBodyTable);
+  NS_ENSURE_SUCCESS(rv, rv);
+  return SetBody(m_localNewsBodyTable);
 }
 
+nsresult nsMsgSearchValidityManager::InitLocalNewsJunkTable()
+{
+  NS_ASSERTION (nsnull == m_localNewsJunkTable, "already have local news+junk validity table");
+  nsresult rv = NewTable(getter_AddRefs(m_localNewsJunkTable));
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = SetLocalNews(m_localNewsJunkTable);
+  NS_ENSURE_SUCCESS(rv, rv);
+  return SetJunk(m_localNewsJunkTable);
+}
 
+nsresult nsMsgSearchValidityManager::InitLocalNewsJunkBodyTable()
+{
+  NS_ASSERTION (nsnull == m_localNewsJunkBodyTable, "already have local news+junk+body validity table");
+  nsresult rv = NewTable(getter_AddRefs(m_localNewsJunkBodyTable));
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = SetLocalNews(m_localNewsJunkBodyTable);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = SetJunk(m_localNewsJunkBodyTable);
+  NS_ENSURE_SUCCESS(rv, rv);
+  return SetBody(m_localNewsJunkBodyTable);
+}
--- a/mailnews/base/search/src/nsMsgSearchAdapter.cpp
+++ b/mailnews/base/search/src/nsMsgSearchAdapter.cpp
@@ -1044,16 +1044,38 @@ NS_IMETHODIMP nsMsgSearchValidityManager
     break;
   case nsMsgSearchScope::localNews:
     if (!m_localNewsTable)
       rv = InitLocalNewsTable();
     if (m_localNewsTable)
       rv = SetOtherHeadersInTable(m_localNewsTable, customHeaders.get());
     *ppOutTable = m_localNewsTable;
     break;
+  case nsMsgSearchScope::localNewsJunk:
+    if (!m_localNewsJunkTable)
+      rv = InitLocalNewsJunkTable();
+    if (m_localNewsJunkTable)
+      rv = SetOtherHeadersInTable(m_localNewsJunkTable, customHeaders.get());
+    *ppOutTable = m_localNewsJunkTable;
+    break;
+  case nsMsgSearchScope::localNewsBody:
+    if (!m_localNewsBodyTable)
+      rv = InitLocalNewsBodyTable();
+    if (m_localNewsBodyTable)
+      rv = SetOtherHeadersInTable(m_localNewsBodyTable, customHeaders.get());
+    *ppOutTable = m_localNewsBodyTable;
+    break;
+  case nsMsgSearchScope::localNewsJunkBody:
+    if (!m_localNewsJunkBodyTable)
+      rv = InitLocalNewsJunkBodyTable();
+    if (m_localNewsJunkBodyTable)
+      rv = SetOtherHeadersInTable(m_localNewsJunkBodyTable, customHeaders.get());
+    *ppOutTable = m_localNewsJunkBodyTable;
+    break;
+
   case nsMsgSearchScope::onlineManual:
     if (!m_onlineManualFilterTable)
       rv = InitOnlineManualFilterTable();
     if (m_onlineManualFilterTable)
       rv = SetOtherHeadersInTable(m_onlineManualFilterTable, customHeaders.get());
     *ppOutTable = m_onlineManualFilterTable;
     break;
   case nsMsgSearchScope::LDAP:
--- a/mailnews/base/search/src/nsMsgSearchTerm.cpp
+++ b/mailnews/base/search/src/nsMsgSearchTerm.cpp
@@ -1941,31 +1941,35 @@ nsresult nsMsgSearchScopeTerm::Initializ
   nsresult err = NS_OK;
 
   switch (m_attribute)
   {
     case nsMsgSearchScope::onlineMail:
         m_adapter = new nsMsgSearchOnlineMail (this, termList);
       break;
     case nsMsgSearchScope::offlineMail:
+    case nsMsgSearchScope::onlineManual:
         m_adapter = new nsMsgSearchOfflineMail (this, termList);
       break;
     case nsMsgSearchScope::newsEx:
       NS_ASSERTION(PR_FALSE, "not supporting newsEx yet");
       break;
     case nsMsgSearchScope::news:
           m_adapter = new nsMsgSearchNews (this, termList);
         break;
     case nsMsgSearchScope::allSearchableGroups:
       NS_ASSERTION(PR_FALSE, "not supporting allSearchableGroups yet");
       break;
     case nsMsgSearchScope::LDAP:
       NS_ASSERTION(PR_FALSE, "not supporting LDAP yet");
       break;
     case nsMsgSearchScope::localNews:
+    case nsMsgSearchScope::localNewsJunk:
+    case nsMsgSearchScope::localNewsBody:
+    case nsMsgSearchScope::localNewsJunkBody:
       m_adapter = new nsMsgSearchOfflineNews (this, termList);
       break;
     default:
       NS_ASSERTION(PR_FALSE, "invalid scope");
       err = NS_ERROR_FAILURE;
   }
 
   if (m_adapter)
--- a/mailnews/base/src/searchSpec.js
+++ b/mailnews/base/src/searchSpec.js
@@ -375,52 +375,62 @@ SearchSpec.prototype = {
     //  MatchHdr.
     if (this.owner.isSynthetic) {
       // We don't want to pass in a folder, and we don't want to use the
       //  allSearchableGroups scope, so we cheat and use AddDirectoryScopeTerm.
       session.addDirectoryScopeTerm(nsMsgSearchScope.offlineMail);
       return;
     }
 
-    // We are filtering if we have mail view terms or user terms.  When
-    //  filtering, we bias towards offline search.  The only time we would use
-    //  an online search when filtering is if one of the constraints uses the
-    //  body attribute and the folder is not marked for offline access.
-    // We are not filtering if we only have virtual folder terms, in which case
-    //  we honor the onlineSearch attribute.  This means that we use the
-    //  folder's server's searchScope if the folder is not explicitly marked
-    //  offline.
-    // For further discussion on this choice of logic, please read from:
-    //  https://bugzilla.mozilla.org/show_bug.cgi?id=474701#c73
-    let filtering = this._virtualFolderTerms == null &&
-                    this._viewTerms == null;
-
     let ioService = Cc["@mozilla.org/network/io-service;1"]
                       .getService(Ci.nsIIOService);
+    let validityManager = Cc['@mozilla.org/mail/search/validityManager;1']
+                            .getService(Ci.nsIMsgSearchValidityManager);
     for each (let [, folder] in Iterator(this.owner._underlyingFolders)) {
       // we do not need to check isServer here because _underlyingFolders
       //  filtered it out when it was initialized.
 
       let scope;
-      let folderIsOffline = (folder instanceof nsIMsgLocalMailFolder) ||
-                            (folder.flags & nsMsgFolderFlags.Offline) ||
-                            ioService.offline;
-      // To restate the above logic into simpler rules, the scope is definitely
-      //  offline if:
-      // - Folders available offline always use offline search.
-      // - If we are filtering and don't have a body term, use offline search.
-      // - If we are not filtering and our virtual folder is not marked for
-      //   onlineSearch.
-      if (folderIsOffline ||
-          (filtering && !haveBodyTerm) ||
-          (!filtering && !this.onlineSearch))
-        scope = nsMsgSearchScope.offlineMail;
-      // Otherwise, it's up to the folder's sever's searchScope.
-      else
-        scope = folder.server.searchScope;
+      let serverScope = folder.server.searchScope;
+      // If we're offline, or this is a local folder, or there's no separate
+      //  online scope, use server scope.
+      if (ioService.offline || (serverScope == nsMsgSearchScope.offlineMail) ||
+                               (folder instanceof nsIMsgLocalMailFolder))
+        scope = serverScope;
+      else {
+        // we need to test the validity in online and offline tables
+        let onlineValidityTable = validityManager.getTable(serverScope);
+
+        let offlineScope;
+        if (folder.flags & nsMsgFolderFlags.Offline)
+          offlineScope = nsMsgSearchScope.offlineMail;
+        else
+          // The onlineManual table is used for local search when there is no
+          //  body available.
+          offlineScope = nsMsgSearchScope.onlineManual;
+
+        let offlineValidityTable = validityManager.getTable(offlineScope);
+        let offlineAvailable = true;
+        let onlineAvailable = true;
+        for each (let term in fixIterator(session.searchTerms,
+                                          nsIMsgSearchTerm)) {
+          if (!offlineValidityTable.getAvailable(term.attrib, term.op))
+            offlineAvailable = false;
+          if (!onlineValidityTable.getAvailable(term.attrib, term.op))
+            onlineAvailable = false;
+        }
+        // If both scopes work, honor the onlineSearch request
+        if (onlineAvailable && offlineAvailable)
+          scope = this.onlineSearch ? serverScope : offlineScope;
+        // If only one works, use it. Otherwise, default to offline
+        else if (onlineAvailable)
+          scope = serverScope;
+        else
+          scope = offlineScope;
+      }
       session.addScopeTerm(scope, folder);
     }
   },
 
   prettyStringOfSearchTerms: function(aSearchTerms) {
     if (aSearchTerms == null)
       return '      (none)\n';
 
--- a/mailnews/news/src/nsNntpIncomingServer.cpp
+++ b/mailnews/news/src/nsNntpIncomingServer.cpp
@@ -2112,17 +2112,19 @@ nsNntpIncomingServer::GetFilterScope(nsM
 }
 
 NS_IMETHODIMP
 nsNntpIncomingServer::GetSearchScope(nsMsgSearchScopeValue *searchScope)
 {
    NS_ENSURE_ARG_POINTER(searchScope);
 
    if (WeAreOffline()) {
-     *searchScope = nsMsgSearchScope::localNews;
+     // This value is set to the localNewsBody scope to be compatible with
+     // the legacy default value.
+     *searchScope = nsMsgSearchScope::localNewsBody;
    }
    else {
      *searchScope = nsMsgSearchScope::news;
    }
    return NS_OK;
 }
 
 NS_IMETHODIMP