Bug 474701 - gloda global search on toolbar, folder display refactoring mega-bug. folder display layer v11. r/sr=bienvenu.
authorAndrew Sutherland <asutherland@asutherland.org>
Fri, 12 Jun 2009 14:50:52 -0700
changeset 2825 b0e37b312b54b10e1fa62feb8b0f576df93d015b
parent 2824 f6e6df3e2f7de2ccd277207a97696e626907879c
child 2826 ecdca1bb4d6eca3a6480e22eeecf91aa26617101
push idunknown
push userunknown
push dateunknown
bugs474701
Bug 474701 - gloda global search on toolbar, folder display refactoring mega-bug. folder display layer v11. r/sr=bienvenu.
mail/base/content/SearchDialog.js
mail/base/content/SearchDialog.xul
mail/base/content/commandglue.js
mail/base/content/extraCustomizeItems.xul
mail/base/content/folderDisplay.js
mail/base/content/folderPane.js
mail/base/content/mail-offline.js
mail/base/content/mail3PaneWindowCommands.js
mail/base/content/mailContextMenus.js
mail/base/content/mailWindow.js
mail/base/content/mailWindowOverlay.js
mail/base/content/mailWindowOverlay.xul
mail/base/content/messageDisplay.js
mail/base/content/messageWindow.js
mail/base/content/messageWindow.xul
mail/base/content/messenger.xul
mail/base/content/msgHdrViewOverlay.js
mail/base/content/msgHdrViewOverlay.xul
mail/base/content/msgMail3PaneWindow.js
mail/base/content/msgViewNavigation.js
mail/base/content/phishingDetector.js
mail/base/content/search.xml
mail/base/content/searchBar.js
mail/base/content/selectionsummaries.js
mail/base/content/tabmail.xml
mail/base/content/threadPane.js
mail/base/content/widgetglue.js
mail/base/jar.mn
mail/components/compose/content/MsgComposeCommands.js
mail/components/compose/jar.mn
mail/extensions/mailviews/content/msgViewPickerOverlay.js
mail/test/mozmill/folder-display/test-deletion-with-multiple-displays.js
mail/test/mozmill/folder-display/test-mail-views.js
mail/test/mozmill/folder-display/test-message-window.js
mail/test/mozmill/folder-display/test-right-click-middle-click.js
mail/test/mozmill/folder-display/test-selection.js
mail/test/mozmill/folder-display/test-summarization.js
mail/test/mozmill/folder-display/test-tabs-simple.js
mail/test/mozmill/runtest.py
mail/test/mozmill/search-window/test-search-window.js
mail/test/mozmill/shared-modules/test-folder-display-helpers.js
mail/test/mozmill/shared-modules/test-window-helpers.js
mail/themes/gnomestripe/mail/messenger.css
mail/themes/gnomestripe/mail/tabmail.css
mail/themes/pinstripe/mail/tabmail.css
mail/themes/qute/mail/tabmail.css
mailnews/base/public/nsIMsgDBView.idl
mailnews/base/resources/content/msgViewNavigation.js
mailnews/base/resources/content/virtualFolderProperties.js
mailnews/base/resources/content/virtualFolderProperties.xul
mailnews/base/search/src/nsMsgSearchAdapter.cpp
mailnews/base/src/dbViewWrapper.js
mailnews/base/src/nsMsgDBView.cpp
mailnews/base/src/nsMsgSearchDBView.cpp
mailnews/base/src/nsMsgSearchDBView.h
mailnews/base/src/nsMsgXFVirtualFolderDBView.cpp
mailnews/base/src/nsMsgXFVirtualFolderDBView.h
mailnews/base/src/quickSearchManager.js
mailnews/base/src/searchSpec.js
mailnews/base/src/virtualFolderWrapper.js
mailnews/base/test/unit/test_jsTreeSelection.js
mailnews/base/test/unit/test_viewWrapper_logic.js
mailnews/base/test/unit/test_viewWrapper_realFolder.js
mailnews/base/test/unit/test_viewWrapper_virtualFolder.js
mailnews/base/util/Makefile.in
mailnews/base/util/jsTreeSelection.js
mailnews/base/util/traceHelper.js
mailnews/jar.mn
mailnews/test/resources/viewWrapperTestUtils.js
suite/mailnews/jar.mn
suite/mailnews/msgViewNavigation.js
--- a/mail/base/content/SearchDialog.js
+++ b/mail/base/content/SearchDialog.js
@@ -1,10 +1,9 @@
-/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
- * ***** BEGIN LICENSE BLOCK *****
+/* ***** 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,
@@ -17,49 +16,47 @@
  *
  * 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):
  *   Håkan Waara <hwaara@chello.se>
+ *   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 ***** */
 
-var searchSessionContractID = "@mozilla.org/messenger/searchSession;1";
-var gSearchView;
-var gSearchSession;
 var gCurrentFolder;
 
-var nsIMsgFolder = Components.interfaces.nsIMsgFolder;
+var gFolderDisplay;
+// Although we don't display messages, we have a message display object to
+//  simplify our code.  It's just always disabled.
+var gMessageDisplay;
+
 var nsIMsgWindow = Components.interfaces.nsIMsgWindow;
-var nsIMsgRDFDataSource = Components.interfaces.nsIMsgRDFDataSource;
-var nsMsgSearchScope = Components.interfaces.nsMsgSearchScope;
 
-var gFolderDatasource;
 var gFolderPicker;
 var gStatusFeedback;
 var gTimelineEnabled = false;
 var gMessengerBundle = null;
 var RDF;
 var gSearchBundle;
-var gNextMessageViewIndexAfterDelete = -2;
 
 // Datasource search listener -- made global as it has to be registered
 // and unregistered in different functions.
 var gDataSourceSearchListener;
 var gViewSearchListener;
 
 var gSearchStopButton;
 
@@ -84,34 +81,35 @@ var nsSearchResultsController =
     },
 
     // this controller only handles commands
     // that rely on items being selected in
     // the search results pane.
     isCommandEnabled: function(command)
     {
         var enabled = true;
-        
-        switch (command) { 
+
+        switch (command) {
           case "goto_folder_button":
             if (GetNumSelectedMessages() != 1)
               enabled = false;
             break;
           case "cmd_delete":
           case "cmd_shiftDelete":
           case "button_delete":
             // this assumes that advanced searches don't cross accounts
-            if (GetNumSelectedMessages() <= 0 || isNewsURI(gSearchView.getURIForViewIndex(0)))
+            if (GetNumSelectedMessages() <= 0 ||
+                isNewsURI(gFolderDisplay.view.dbView.getURIForViewIndex(0)))
               enabled = false;
             break;
           case "saveas_vf_button":
               // need someway to see if there are any search criteria...
               return true;
           case "cmd_selectAll":
-            return GetDBView() != null;              
+            return true;
           default:
             if (GetNumSelectedMessages() <= 0)
               enabled = false;
             break;
         }
 
         return enabled;
     },
@@ -137,230 +135,218 @@ var nsSearchResultsController =
 
         case "saveas_vf_button":
             saveAsVirtualFolder();
             return true;
 
         case "cmd_selectAll":
             // move the focus to the search results pane
             GetThreadTree().focus();
-            GetDBView().doCommand(nsMsgViewCommandType.selectAll)
+            gFolderDisplay.doCommand(nsMsgViewCommandType.selectAll);
             return true;
-                            
+
         default:
             return false;
         }
 
     },
 
     onEvent: function(event)
     {
     }
 }
 
 function UpdateMailSearch(caller)
 {
-  //dump("XXX update mail-search " + caller + "\n");
   document.commandDispatcher.updateCommands('mail-search');
 }
+/**
+ * FolderDisplayWidget currently calls this function when the command updater
+ *  notification for updateCommandStatus is called.  We don't have a toolbar,
+ *  but our 'mail-search' command set serves the same purpose.
+ */
+var UpdateMailToolbar = UpdateMailSearch;
+
+/**
+ * No-op clear message pane function for FolderDisplayWidget.
+ */
+function ClearMessagePane() {
+}
 
 function SetAdvancedSearchStatusText(aNumHits)
 {
-  var statusMsg;
-  // 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]);
-  }
-
-  gStatusFeedback.showStatusString(statusMsg);
 }
 
-// nsIMsgSearchNotify object
-var gSearchNotificationListener =
-{
-    onSearchHit: function(header, folder)
-    {
-        // XXX TODO
-        // update status text?
-    },
-
-    onSearchDone: function(status)
-    {
-        gSearchStopButton.setAttribute("label", gSearchBundle.getString("labelForSearchButton"));
-        gSearchStopButton.setAttribute("accesskey", gSearchBundle.getString("labelForSearchButton.accesskey"));
-        gStatusFeedback._stopMeteors();
-        SetAdvancedSearchStatusText(gSearchView.QueryInterface(Components.interfaces.nsITreeView).rowCount);
-    },
-
-    onNewSearch: function()
-    {
-      gSearchStopButton.setAttribute("label", gSearchBundle.getString("labelForStopButton"));
-      gSearchStopButton.setAttribute("accesskey", gSearchBundle.getString("labelForStopButton.accesskey"));
-      UpdateMailSearch("new-search");	
-      gStatusFeedback._startMeteors();
-      gStatusFeedback.showStatusString(gSearchBundle.getString("searchingMessage"));
-    }
+/**
+ * Subclass the FolderDisplayWidget to deal with UI specific to the search
+ *  window.
+ */
+function SearchFolderDisplayWidget(aMessageDisplay) {
+  FolderDisplayWidget.call(this, /* no tab info */ null, aMessageDisplay);
 }
 
-// the folderListener object
-var gFolderListener = {
-    OnItemAdded: function(parentItem, item) {},
+SearchFolderDisplayWidget.prototype = {
+  __proto__: FolderDisplayWidget.prototype,
+
+  /// folder display will want to show the thread pane; we need do nothing
+  _showThreadPane: function () {},
 
-    OnItemRemoved: function(parentItem, item){},
+  onSearching: function SearchFolderDisplayWidget_onSearch(aIsSearching) {
+    if (aIsSearching) {
+      // Search button becomes the "stop" button
+      gSearchStopButton.setAttribute(
+        "label", gSearchBundle.getString("labelForStopButton"));
+      gSearchStopButton.setAttribute(
+        "accesskey", gSearchBundle.getString("labelForStopButton.accesskey"));
 
-    OnItemPropertyChanged: function(item, property, oldValue, newValue) {},
-
-    OnItemIntPropertyChanged: function(item, property, oldValue, newValue) {},
-
-    OnItemBoolPropertyChanged: function(item, property, oldValue, newValue) {},
-
-    OnItemUnicharPropertyChanged: function(item, property, oldValue, newValue){},
-    OnItemPropertyFlagChanged: function(item, property, oldFlag, newFlag) {},
+      // update our toolbar equivalent
+      UpdateMailSearch("new-search");
+      // spin the meteors
+      gStatusFeedback._startMeteors();
+      // tell the user that we're searching
+      gStatusFeedback.showStatusString(
+        gSearchBundle.getString("searchingMessage"));
+    }
+    else {
+      // Stop button resumes being the "search" button
+      gSearchStopButton.setAttribute(
+        "label", gSearchBundle.getString("labelForSearchButton"));
+      gSearchStopButton.setAttribute(
+        "accesskey", gSearchBundle.getString("labelForSearchButton.accesskey"));
 
-    OnItemEvent: function(folder, event) {
-        var eventType = event.toString();
-        
-        if (eventType == "DeleteOrMoveMsgCompleted") {
-            HandleDeleteOrMoveMessageCompleted(folder);
-        }     
-        else if (eventType == "DeleteOrMoveMsgFailed") {
-            HandleDeleteOrMoveMessageFailed(folder);
-        }
+      // update our toolbar equivalent
+      UpdateMailSearch("done-search");
+      // stop spining the meteors
+      gStatusFeedback._stopMeteors();
+      // set the result test
+      this.updateStatusResultText();
     }
-}
+  },
 
-function HideSearchColumn(id)
-{
-  var col = document.getElementById(id);
-  if (col) {
-    col.setAttribute("hidden","true");
-    col.setAttribute("ignoreincolumnpicker","true");
-  }
-}
+  /**
+   * If messages were removed, we might have lost some search results and so
+   *  should update our search result text.  Also, defer to our super-class.
+   */
+  onMessagesRemoved: function SearchFolderDisplayWidget_onMessagesRemoved() {
+    // result text is only for when we are not searching
+    if (!this.view.searching)
+      this.updateStatusResultText();
+    this.__proto__.__proto__.onMessagesRemoved.call(this);
+  },
 
-function ShowSearchColumn(id)
-{
-  var col = document.getElementById(id);
-  if (col) {
-    col.removeAttribute("hidden");
-    col.removeAttribute("ignoreincolumnpicker");
-  }
-}
+  updateStatusResultText: function() {
+    let statusMsg, rowCount = this.view.dbView.rowCount;
+    // if there are no hits, it means no matches were found in the search.
+    if (rowCount == 0)
+      statusMsg = gSearchBundle.getString("searchFailureMessage");
+    else if (rowCount == 1)
+      statusMsg = gSearchBundle.getString("searchSuccessMessage");
+    else
+      statusMsg = gSearchBundle.getFormattedString("searchSuccessMessages",
+                                                   [rowCount]);
+
+    gStatusFeedback.showStatusString(statusMsg);
+  },
+};
+
 
 function searchOnLoad()
 {
   initializeSearchWidgets();
   initializeSearchWindowWidgets();
   messenger = Components.classes["@mozilla.org/messenger;1"]
                         .createInstance(Components.interfaces.nsIMessenger);
 
   gSearchBundle = document.getElementById("bundle_search");
   gSearchStopButton.setAttribute("label", gSearchBundle.getString("labelForSearchButton"));
   gSearchStopButton.setAttribute("accesskey", gSearchBundle.getString("labelForSearchButton.accesskey"));
   gMessengerBundle = document.getElementById("bundle_messenger");
-  setupDatasource();
-  setupSearchListener();
+
+  gMessageDisplay = new NeverVisisbleMessageDisplayWidget();
+  gFolderDisplay = new SearchFolderDisplayWidget(gMessageDisplay);
+  gFolderDisplay.messenger = messenger;
+  gFolderDisplay.msgWindow = msgWindow;
+  gFolderDisplay.tree = document.getElementById("threadTree");
+  gFolderDisplay.treeBox = gFolderDisplay.tree.boxObject.QueryInterface(
+                             Components.interfaces.nsITreeBoxObject);
+  gFolderDisplay.view.openSearchView();
+  gFolderDisplay.makeActive();
+
+  gFolderDisplay.setVisibleColumns({subjectCol: true,
+                                    senderCol: true,
+                                    dateCol: true,
+                                    locationCol: true,
+                                    });
 
   if (window.arguments && window.arguments[0])
       selectFolder(window.arguments[0].folder);
 
+  // trigger searchTermOverlay.js to create the first criterion
   onMore(null);
+  // make sure all the buttons are configured
   UpdateMailSearch("onload");
-  
-  // hide and remove these columns from the column picker.  you can't thread search results
-  HideSearchColumn("threadCol"); // since you can't thread search results
-  HideSearchColumn("totalCol"); // since you can't thread search results
-  HideSearchColumn("unreadCol"); // since you can't thread search results
-  HideSearchColumn("unreadButtonColHeader");
-  HideSearchColumn("statusCol");
-  HideSearchColumn("flaggedCol");
-  HideSearchColumn("idCol");
-  HideSearchColumn("junkStatusCol");
-  HideSearchColumn("accountCol");
-  
-  // we want to show the location column for search
-  ShowSearchColumn("locationCol");
 }
 
 function searchOnUnload()
 {
-    // unregister listeners
-    gSearchSession.unregisterListener(gViewSearchListener);
-    gSearchSession.unregisterListener(gSearchNotificationListener);
+  gFolderDisplay.close();
+  top.controllers.removeController(nsSearchResultsController);
 
-    Components.classes["@mozilla.org/messenger/services/session;1"]
-              .getService(Components.interfaces.nsIMsgMailSession)
-              .RemoveFolderListener(gFolderListener);
-	
-    if (gSearchView) {
-	gSearchView.close();
-	gSearchView = null;
-    }
-
-    top.controllers.removeController(nsSearchResultsController);
-
-    // release this early because msgWindow holds a weak reference
-    msgWindow.rootDocShell = null;
+  // release this early because msgWindow holds a weak reference
+  msgWindow.rootDocShell = null;
 }
 
 function initializeSearchWindowWidgets()
 {
     gFolderPicker = document.getElementById("searchableFolders");
     gSearchStopButton = document.getElementById("search-button");
     hideMatchAllItem();
-    
+
     msgWindow = Components.classes["@mozilla.org/messenger/msgwindow;1"]
                           .createInstance(nsIMsgWindow);
     msgWindow.domWindow = window;
     msgWindow.rootDocShell.appType = Components.interfaces.nsIDocShell.APP_TYPE_MAIL;
 
     gStatusFeedback = new nsMsgStatusFeedback();
     msgWindow.statusFeedback = gStatusFeedback;
 
     // functionality to enable/disable buttons using nsSearchResultsController
     // depending of whether items are selected in the search results thread pane.
     top.controllers.insertControllerAt(0, nsSearchResultsController);
 }
 
 
 function onSearchStop() {
-    gSearchSession.interruptSearch();
+  gFolderDisplay.view.search.session.interruptSearch();
 }
 
 function onResetSearch(event) {
-    onReset(event);
-    
-    var tree = GetThreadTree();
-    tree.treeBoxObject.view = null;
-    gStatusFeedback.showStatusString("");
+  onReset(event);
+  gFolderDisplay.view.search.clear();
+
+  gStatusFeedback.showStatusString("");
 }
 
-function selectFolder(folder) 
+function selectFolder(folder)
 {
     var folderURI;
 
     // if we can't search messages on this folder, just select the first one
     if (!folder || !folder.server.canSearchMessages ||
         (folder.flags & Components.interfaces.nsMsgFolderFlags.Virtual)) {
         // find first item in our folder picker menu list
         folderURI = gFolderPicker.firstChild.tree.builderView.getResourceAtIndex(0).Value;
     } else {
         folderURI = folder.URI;
     }
     updateSearchFolderPicker(folderURI);
 }
 
-function updateSearchFolderPicker(folderURI) 
-{ 
+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;
@@ -384,80 +370,96 @@ function onChooseFolder(event) {
     }
 }
 
 function onEnterInSearchTerm()
 {
   // on enter
   // if not searching, start the search
   // if searching, stop and then start again
-  if (gSearchStopButton.getAttribute("label") == gSearchBundle.getString("labelForSearchButton")) { 
-     onSearch(); 
+  if (gSearchStopButton.getAttribute("label") == gSearchBundle.getString("labelForSearchButton")) {
+     onSearch();
   }
   else {
      onSearchStop();
      onSearch();
   }
 }
 
 function onSearch()
 {
-    // set the view.  do this on every search, to
-    // allow the tree to reset itself
-    var treeView = gSearchView.QueryInterface(Components.interfaces.nsITreeView);
-    if (treeView)
-    {
-      var tree = GetThreadTree();
-      tree.treeBoxObject.view = treeView;
-    }
-
-    gSearchSession.clearScopes();
-    // tell the search session what the new scope is
-    if (!gCurrentFolder.isServer && !gCurrentFolder.noSelect)
-        gSearchSession.addScopeTerm(GetScopeForFolder(gCurrentFolder),
-                                    gCurrentFolder);
+  let viewWrapper = gFolderDisplay.view;
+  let searchTerms = getSearchTerms();
 
-    var searchSubfolders = document.getElementById("checkSearchSubFolders").checked;
-    if (gCurrentFolder && (searchSubfolders || gCurrentFolder.isServer || gCurrentFolder.noSelect))
-    {
-        AddSubFolders(gCurrentFolder);
-    }
-    // reflect the search widgets back into the search session
-    saveSearchTerms(gSearchSession.searchTerms, gSearchSession);
-
-    try
-    {
-      gSearchSession.search(msgWindow);
-    }
-    catch(ex)
-    {
-       dump("Search Exception\n");
-    }
-    // refresh the tree after the search starts, because initiating the
-    // search will cause the datasource to clear itself
+  viewWrapper.beginViewUpdate();
+  viewWrapper.search.userTerms = searchTerms.length ? searchTerms : null;
+  viewWrapper.searchFolders = getSearchFolders();
+  viewWrapper.endViewUpdate();
 }
 
-function AddSubFolders(folder) {
+/**
+ * Get the current set of search terms, returning them as a list.  We filter out
+ *  dangerous and insane predicates.
+ */
+function getSearchTerms() {
+  let termCreator = gFolderDisplay.view.search.session;
+
+  let searchTerms = [];
+  // searchTermOverlay stores wrapper objects in its gSearchTerms array.  Pluck
+  //  them.
+  for (let iTerm = 0; iTerm < gSearchTerms.length; iTerm++) {
+    let termWrapper = gSearchTerms[iTerm].obj;
+    let realTerm = termCreator.createTerm();
+    termWrapper.saveTo(realTerm);
+    // A header search of "" is illegal for IMAP and will cause us to
+    //  explode.  You don't want that and I don't want that.  So let's check
+    //  if the bloody term is a subject search on a blank string, and if it
+    //  is, let's secretly not add the term.  Everyone wins!
+    if ((realTerm.attrib != Components.interfaces.nsMsgSearchAttrib.Subject) ||
+        (realTerm.value.str != ""))
+      searchTerms.push(realTerm);
+  }
+
+  return searchTerms;
+}
+
+/**
+ * @return the list of folders the search should cover.
+ */
+function getSearchFolders() {
+  let searchFolders = [];
+
+  if (!gCurrentFolder.isServer && !gCurrentFolder.noSelect)
+    searchFolders.push(gCurrentFolder);
+
+  var searchSubfolders =
+    document.getElementById("checkSearchSubFolders").checked;
+  if (gCurrentFolder &&
+      (searchSubfolders || gCurrentFolder.isServer || gCurrentFolder.noSelect))
+    AddSubFolders(gCurrentFolder, searchFolders);
+
+  return searchFolders;
+}
+
+function AddSubFolders(folder, outFolders) {
   var subFolders = folder.subFolders;
-  while (subFolders.hasMoreElements())
-  {
+  while (subFolders.hasMoreElements()) {
     var nextFolder =
       subFolders.getNext().QueryInterface(Components.interfaces.nsIMsgFolder);
 
-    if (!(nextFolder.flags & Components.interfaces.nsMsgFolderFlags.Virtual))
-    {
+    if (!(nextFolder.flags & Components.interfaces.nsMsgFolderFlags.Virtual)) {
       if (!nextFolder.noSelect)
-        gSearchSession.addScopeTerm(GetScopeForFolder(nextFolder), nextFolder);
+        outFolders.push(nextFolder);
 
-      AddSubFolders(nextFolder);
+      AddSubFolders(nextFolder, outFolders);
     }
   }
 }
 
-function AddSubFoldersToURI(folder) 
+function AddSubFoldersToURI(folder)
 {
   var returnString = "";
 
   var subFolders = folder.subFolders;
 
   while (subFolders.hasMoreElements())
   {
     var nextFolder =
@@ -479,17 +481,17 @@ function AddSubFoldersToURI(folder)
         returnString += subFoldersString;
       }
     }
   }
   return returnString;
 }
 
 
-function GetScopeForFolder(folder) 
+function GetScopeForFolder(folder)
 {
   var searchLocalSystem = document.getElementById("checkSearchLocalSystem");
   return searchLocalSystem && searchLocalSystem.checked ? nsMsgSearchScope.offlineMail : folder.server.searchScope;
 }
 
 var nsMsgViewSortType = Components.interfaces.nsMsgViewSortType;
 var nsMsgViewSortOrder = Components.interfaces.nsMsgViewSortOrder;
 var nsMsgViewFlagsType = Components.interfaces.nsMsgViewFlagsType;
@@ -502,287 +504,89 @@ function goUpdateSearchItems(commandset)
     var commandID = commandset.childNodes[i].getAttribute("id");
     if (commandID)
     {
       goUpdateCommand(commandID);
     }
   }
 }
 
-function nsMsgSearchCommandUpdater()
-{}
-
-nsMsgSearchCommandUpdater.prototype =
-{
-  updateCommandStatus : function()
-  {
-    // the back end is smart and is only telling us to update command status
-    // when the # of items in the selection has actually changed.
-    document.commandDispatcher.updateCommands('mail-search');
-  },
-  displayMessageChanged : function(aFolder, aSubject, aKeywords)
-  {
-  },
-
-  updateNextMessageAfterDelete : function()
-  {
-    SetNextMessageAfterDelete();
-  },
-
-  summarizeSelection : function() {return false},
-
-  QueryInterface : function(iid)
-  {
-    if (iid.equals(Components.interfaces.nsIMsgDBViewCommandUpdater) ||
-        iid.equals(Components.interfaces.nsISupports))
-      return this;
-
-    throw Components.results.NS_NOINTERFACE;
-  }
-}
-
-function setupDatasource() {
-    gSearchView = Components.classes["@mozilla.org/messenger/msgdbview;1?type=search"].createInstance(Components.interfaces.nsIMsgDBView);
-    var count = new Object;
-    var cmdupdator = new nsMsgSearchCommandUpdater();
-
-    gSearchView.init(messenger, msgWindow, cmdupdator);
-    gSearchView.open(null, nsMsgViewSortType.byId, nsMsgViewSortOrder.ascending, nsMsgViewFlagsType.kNone, count);
-
-    // the thread pane needs to use the search datasource (to get the
-    // actual list of messages) and the message datasource (to get any
-    // attributes about each message)
-    gSearchSession = Components.classes[searchSessionContractID].createInstance(Components.interfaces.nsIMsgSearchSession);
-
-    var nsIFolderListener = Components.interfaces.nsIFolderListener;
-    var notifyFlags = nsIFolderListener.event;
-    Components.classes["@mozilla.org/messenger/services/session;1"]
-              .getService(Components.interfaces.nsIMsgMailSession)
-              .AddFolderListener(gFolderListener, notifyFlags);
-
-    // the datasource is a listener on the search results
-    gViewSearchListener = gSearchView.QueryInterface(Components.interfaces.nsIMsgSearchNotify);
-    gSearchSession.registerListener(gViewSearchListener);
-}
-
-
-function setupSearchListener()
-{
-    // Setup the javascript object as a listener on the search results
-    gSearchSession.registerListener(gSearchNotificationListener);
-}
-
-// stuff after this is implemented to make the thread pane work
-function GetFolderDatasource()
-{
-    if (!gFolderDatasource)
-        gFolderDatasource = Components.classes["@mozilla.org/rdf/datasource;1?name=mailnewsfolders"]
-                                      .getService(Components.interfaces.nsIRDFDataSource);
-    return gFolderDatasource;
-}
-
-// used to determine if we should try to load a message
-function IsThreadAndMessagePaneSplitterCollapsed()
-{
-    return true;
-}
-
 // used to toggle functionality for Search/Stop button.
 function onSearchButton(event)
 {
     if (event.target.label == gSearchBundle.getString("labelForSearchButton"))
         onSearch();
     else
         onSearchStop();
 }
 
 // threadPane.js will be needing this, too
 function GetNumSelectedMessages()
 {
-   try {
-       return gSearchView.numSelected;
-   }
-   catch (ex) {
-       return 0;
-   }
-}
-
-function GetDBView()
-{
-    return gSearchView;
+  return gFolderDisplay.treeSelection.count;
 }
 
 function MsgDeleteSelectedMessages(aCommandType)
 {
     // we don't delete news messages, we just return in that case
-    if (isNewsURI(gSearchView.getURIForViewIndex(0))) 
+    if (gFolderDisplay.selectedMessageIsNews)
         return;
 
     // if mail messages delete
-    SetNextMessageAfterDelete();
-    gSearchView.doCommand(aCommandType);
-}
-
-function SetNextMessageAfterDelete()
-{
-  gNextMessageViewIndexAfterDelete = gSearchView.msgToSelectAfterDelete;
-}
-
-function HandleDeleteOrMoveMessageFailed(folder)
-{
-  gNextMessageViewIndexAfterDelete = -2;
-}
-
-function HandleDeleteOrMoveMessageCompleted(folder)
-{
-  var treeView = gSearchView.QueryInterface(Components.interfaces.nsITreeView);
-  var treeSelection = treeView.selection;
-  var viewSize = treeView.rowCount;
-
-  if (gNextMessageViewIndexAfterDelete == -2) {
-    // a move or delete can cause our selection can change underneath us.
-    // this can happen when the user
-    // deletes message from the stand alone msg window
-    // or the three pane
-    if (!treeSelection) {
-      // this can happen if you open the search window
-      // and before you do any searches
-      // and you do delete from another mail window
-      return;
-    }
-    else if (treeSelection.count == 0) {
-      // this can happen if you double clicked a message
-      // in the thread pane, and deleted it from the stand alone msg window
-      // see bug #185147
-      treeSelection.clearSelection();
-
-      UpdateMailSearch("delete from another view, 0 rows now selected");
-    }
-    else if (treeSelection.count == 1) {
-      // this can happen if you had two messages selected
-      // in the search results pane, and you deleted one of them from another view
-      // (like the view in the stand alone msg window or the three pane)
-      // since one item is selected, we should load it.
-      var startIndex = {};
-      var endIndex = {};
-      treeSelection.getRangeAt(0, startIndex, endIndex);
-        
-      // select the selected item, so we'll load it
-      treeSelection.select(startIndex.value); 
-      treeView.selectionChanged();
-
-      EnsureRowInThreadTreeIsVisible(startIndex.value); 
-      UpdateMailSearch("delete from another view, 1 row now selected");
-    }
-    else {
-      // this can happen if you have more than 2 messages selected
-      // in the search results pane, and you deleted one of them from another view
-      // (like the view in the stand alone msg window or the three pane)
-      // since multiple messages are still selected, do nothing.
-    }
-  }
-  else {
-    if (gNextMessageViewIndexAfterDelete != nsMsgViewIndex_None && gNextMessageViewIndexAfterDelete >= viewSize) 
-    {
-      if (viewSize > 0)
-        gNextMessageViewIndexAfterDelete = viewSize - 1;
-      else
-      {           
-        gNextMessageViewIndexAfterDelete = nsMsgViewIndex_None;
-
-        // there is nothing to select since viewSize is 0
-        treeSelection.clearSelection();
-
-        UpdateMailSearch("delete from current view, 0 rows left");
-      }
-    }
-
-    // if we are about to set the selection with a new element then DON'T clear
-    // the selection then add the next message to select. This just generates
-    // an extra round of command updating notifications that we are trying to
-    // optimize away.
-    if (gNextMessageViewIndexAfterDelete != nsMsgViewIndex_None) 
-    {
-      treeSelection.select(gNextMessageViewIndexAfterDelete);
-      // since gNextMessageViewIndexAfterDelete probably has the same value
-      // as the last index we had selected, the tree isn't generating a new
-      // selectionChanged notification for the tree view. So we aren't loading the 
-      // next message. to fix this, force the selection changed update.
-      if (treeView)
-        treeView.selectionChanged();
-
-      EnsureRowInThreadTreeIsVisible(gNextMessageViewIndexAfterDelete); 
-
-      // XXX TODO
-      // I think there is a bug in the suppression code above.
-      // what if I have two rows selected, and I hit delete, 
-      // and so we load the next row.
-      // what if I have commands that only enable where 
-      // exactly one row is selected?
-      UpdateMailSearch("delete from current view, at least one row selected");
-    }
-  }
-
-  // default value after delete/move/copy is over
-  gNextMessageViewIndexAfterDelete = -2;
-
-  // something might have been deleted, so update the status text
-  SetAdvancedSearchStatusText(viewSize);
+    gFolderDisplay.hintAboutToDeleteMessages();
+    gFolderDisplay.doCommand(aCommandType);
 }
 
 function MoveMessageInSearch(destFolder)
 {
-    try {
-        // get the msg folder we're moving messages into
-        // if the id (uri) is not set, use file-uri which is set for
-        // "File Here"
-        var destUri = destFolder.getAttribute('id');
-        if (destUri.length == 0) { 
-          destUri = destFolder.getAttribute('file-uri')
-        }
+  // Get the msg folder we're moving messages into.
+  // If the id (uri) is not set, use file-uri which is set for
+  // "File Here".
+  let destUri = destFolder.getAttribute('id');
+  if (destUri.length == 0)
+    destUri = destFolder.getAttribute('file-uri');
 
-        var destMsgFolder = GetMsgFolderFromUri(destUri).QueryInterface(Components.interfaces.nsIMsgFolder);
+  let destMsgFolder = GetMsgFolderFromUri(destUri).QueryInterface(
+                        Components.interfaces.nsIMsgFolder);
 
-        // we don't move news messages, we copy them
-        if (isNewsURI(gSearchView.getURIForViewIndex(0))) {
-          gSearchView.doCommandWithFolder(nsMsgViewCommandType.copyMessages, destMsgFolder);
-        }
-        else {
-            SetNextMessageAfterDelete();
-            gSearchView.doCommandWithFolder(nsMsgViewCommandType.moveMessages, destMsgFolder);
-        } 
-    }
-    catch (ex) {
-        dump("MsgMoveMessage failed: " + ex + "\n");
-    }   
+  // we don't move news messages, we copy them
+  if (gFolderDisplay.selectedMessageIsNews) {
+    gFolderDisplay.doCommandWithFolder(nsMsgViewCommandType.copyMessages,
+                                       destMsgFolder);
+  }
+  else {
+    gFolderDisplay.hintAboutToDeleteMessages();
+    gFolderDisplay.doCommandWithFolder(nsMsgViewCommandType.moveMessages,
+                                       destMsgFolder);
+  }
 }
 
 function GoToFolder()
 {
-  var hdr = gSearchView.hdrForFirstSelectedMessage;
-  MsgOpenNewWindowForFolder(hdr.folder.URI, hdr.messageKey);
+  MsgOpenNewWindowForFolder(gFolderDisplay.selectedMessage);
 }
 
 function BeginDragThreadPane(event)
 {
     // no search pane dnd yet
     return false;
 }
 
 function saveAsVirtualFolder()
 {
-  searchFolderURIs = window.arguments[0].folder.URI;
+  var searchFolderURIs = window.arguments[0].folder.URI;
 
   var searchSubfolders = document.getElementById("checkSearchSubFolders").checked;
   if (gCurrentFolder && (searchSubfolders || gCurrentFolder.isServer || gCurrentFolder.noSelect))
   {
     var subFolderURIs = AddSubFoldersToURI(gCurrentFolder);
     if (subFolderURIs.length > 0)
       searchFolderURIs += '|' + subFolderURIs;
   }
 
   var dialog = window.openDialog("chrome://messenger/content/virtualFolderProperties.xul", "",
                                  "chrome,titlebar,modal,centerscreen",
-                                 {folder:window.arguments[0].folder,
-                                  searchTerms:gSearchSession.searchTerms,
+                                 {folder: window.arguments[0].folder,
+                                  searchTerms: toXPCOMArray(getSearchTerms(),
+                                                            Components.interfaces.nsISupportsArray),
                                   searchFolderURIs: searchFolderURIs});
 }
 
--- a/mail/base/content/SearchDialog.xul
+++ b/mail/base/content/SearchDialog.xul
@@ -53,16 +53,18 @@
         style="width: 52em; height: 34em;"
         persist="screenX screenY width height sizemode">
 
   <stringbundle id="bundle_search" src="chrome://messenger/locale/search.properties"/>
   <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
   <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
 
   <script type="application/x-javascript" src="chrome://messenger/content/mailWindow.js"/>
+  <script type="application/x-javascript" src="chrome://messenger/content/folderDisplay.js"/>
+  <script type="application/x-javascript" src="chrome://messenger/content/messageDisplay.js"/>
   <script type="application/x-javascript" src="chrome://messenger/content/threadPane.js"/>
   <script type="application/x-javascript" src="chrome://messenger/content/msgMail3PaneWindow.js"/>
   <script type="application/x-javascript" src="chrome://global/content/globalOverlay.js"/>
   <script type="application/x-javascript" src="chrome://messenger/content/mailCommands.js"/>
   <script type="application/x-javascript" src="chrome://messenger/content/mailWindowOverlay.js"/>
   <script type="application/x-javascript" src="chrome://messenger/content/commandglue.js"/>
   <script type="application/x-javascript" src="chrome://messenger/content/SearchDialog.js"/>
   <script type="application/x-javascript" src="chrome://messenger/content/messengerdnd.js"/>
--- a/mail/base/content/commandglue.js
+++ b/mail/base/content/commandglue.js
@@ -1,485 +1,111 @@
-# -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is Mozilla Communicator client code, released
-# March 31, 1998.
-#
-# The Initial Developer of the Original Code is
-# Netscape Communications Corporation.
-# Portions created by the Initial Developer are Copyright (C) 1998-1999
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#   Jan Varga <varga@nixcorp.com>
-#   HÃ¥kan Waara (hwaara@chello.se)
-#   David Bienvenu (bienvenu@nventure.com)
-#   Jeremy Morton (bugzilla@game-point.net)
-#
-# 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 *****
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Communicator client code, released
+ * March 31, 1998.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1998-1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Jan Varga <varga@nixcorp.com>
+ *   HÃ¥kan Waara (hwaara@chello.se)
+ *   David Bienvenu (bienvenu@nventure.com)
+ *   Jeremy Morton (bugzilla@game-point.net)
+ *   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 ***** */
+
 /*
  * Command-specific code. This stuff should be called by the widgets
  */
 
 Components.utils.import("resource://gre/modules/iteratorUtils.jsm");
 
 //NOTE: gMessengerBundle and gBrandBundle must be defined and set
 //      for this Overlay to work properly
 
-var gFolderJustSwitched = false;
-var gVirtualFolderTerms;
-var gXFVirtualFolderTerms;
-var gCurrentVirtualFolderUri;
-var gPrevFolderFlags;
-var gPrevSelectedFolder;
 var gMsgFolderSelected;
 
-function setTitleFromFolder(msgfolder, subject)
-{
-    var wintype = document.documentElement.getAttribute('windowtype');
-    var title; 
-
-    // If we are showing the mail:3pane. Never include the subject of the selected
-    // message in the title. ("Inbox - My Mail - Mozilla Thunderbird")
-    // If we are a stand alone message window, we should show the Subject
-    // and the product but not the account name: "Re: New window Title - Mozilla Thunderbird"
-
-    if (wintype == "mail:messageWindow")  
-      title = subject ? subject : "";
-    else if (msgfolder)
-    {
-      title = msgfolder.prettyName;
-
-      if (!msgfolder.isServer)
-      {
-        var server = msgfolder.server;
-        var middle;
-        var end;
-        if (server.type == "nntp") {
-            // <folder> on <hostname>
-            middle = gMessengerBundle.getString("titleNewsPreHost");
-            end = server.hostName;
-        }
-        else {
-          // <folder> - <server.prettyName>
-          middle = "-";
-          end = server.prettyName;
-        }
-        if (middle) title += " " + middle;
-        if (end) title += " " + end;
-      }
-    }
-
-#ifndef XP_MACOSX
-    title += " - " + gBrandBundle.getString("brandFullName");
-#endif
-    document.title = title;
-}
-
 function UpdateMailToolbar(caller)
 {
   //dump("XXX update mail-toolbar " + caller + "\n");
   document.commandDispatcher.updateCommands('mail-toolbar');
 
   // hook for extra toolbar items
   var observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
   observerService.notifyObservers(window, "mail:updateToolbarItems", null);
 }
 
-/**
- * @param   folder                - If viewFolder is a single folder saved
-                                  - search, this folder is the scope of the
-                                  - saved search, the real, underlying folder.
-                                  - Otherwise, it's the same as the viewFolder.
- * @param   viewFolder            - nsIMsgFolder selected in the folder pane.
-                                  - Will be the same as folder, except if
-                                  - it's a single folder saved search.
- * @param   viewType              - nsMsgViewType (see nsIMsgDBView.idl)
- * @param   viewFlags             - nsMsgViewFlagsType (see nsIMsgDBView.idl)
- * @param   sortType              - nsMsgViewSortType (see nsIMsgDBView.idl)
- * @param   sortOrder             - nsMsgViewSortOrder (see nsIMsgDBView.idl)
- **/
-function ChangeFolder(folder, viewFolder, viewType, viewFlags, sortType, sortOrder)
-{
-  if (folder.URI == gCurrentLoadingFolderURI)
-    return;
-
-  // hook for extra toolbar items
-  var observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
-  observerService.notifyObservers(window, "mail:setupToolbarItems", folder.URI);
-
-  try {
-      setTitleFromFolder(viewFolder, null);
-  } catch (ex) {
-      dump("error setting title: " + ex + "\n");
-  }
-
-  //if it's a server, clear the threadpane and don't bother trying to load.
-  if(folder.isServer)
-  {
-    msgWindow.openFolder = null;
-    ClearThreadPane();
-    UpdateStatusQuota(null);
-    // Load AccountCentral page here.
-    ShowAccountCentral();
-    return;
-  }
-  else
-  {
-    if (folder.server.displayStartupPage)
-    {
-      gDisplayStartupPage = true;
-      folder.server.displayStartupPage = false;
-    }
-  }
-
-  // If the user clicks on msgfolder, time to display thread pane and message pane.
-  ShowThreadPane();
-
-  gCurrentLoadingFolderURI = folder.URI;
-  gNextMessageAfterDelete = null; // forget what message to select, if any
-
-  gCurrentFolderToReroot = folder.URI;
-  gCurrentLoadingFolderViewFlags = viewFlags;
-  gCurrentLoadingFolderViewType = viewType;
-  gCurrentLoadingFolderSortType = sortType;
-  gCurrentLoadingFolderSortOrder = sortOrder;
-
-  var showMessagesAfterLoading;
-  try {
-    var server = folder.server;
-    if (gPrefBranch.getBoolPref("mail.password_protect_local_cache"))
-    {
-      showMessagesAfterLoading = server.passwordPromptRequired;
-      // servers w/o passwords (like local mail) will always be non-authenticated.
-      // So we need to use the account manager for that case.
-    }
-    else
-      showMessagesAfterLoading = false;
-  }
-  catch (ex) {
-    showMessagesAfterLoading = false;
-  }
-
-  if (viewType != nsMsgViewType.eShowVirtualFolderResults && (folder.manyHeadersToDownload || showMessagesAfterLoading))
-  {
-    gRerootOnFolderLoad = true;
-    try
-    {
-      ClearThreadPane();
-      SetBusyCursor(window, true);
-      folder.startFolderLoading();
-      folder.updateFolder(msgWindow);
-    }
-    catch(ex)
-    {
-      SetBusyCursor(window, false);
-      dump("Error loading with many headers to download: " + ex + "\n");
-    }
-  }
-  else
-  {
-    if (viewType != nsMsgViewType.eShowVirtualFolderResults)
-      SetBusyCursor(window, true);
-    RerootFolder(folder.URI, folder, viewType, viewFlags, sortType, sortOrder);
-    gRerootOnFolderLoad = false;
-    folder.startFolderLoading();
-
-    //Need to do this after rerooting folder.  Otherwise possibility of receiving folder loaded
-    //notification before folder has actually changed.
-    if (viewType != nsMsgViewType.eShowVirtualFolderResults)
-      folder.updateFolder(msgWindow);
-  }
-}
-
 function isNewsURI(uri)
 {
     if (!uri || uri[0] != 'n') {
         return false;
     }
     else {
         return ((uri.substring(0,6) == "news:/") || (uri.substring(0,14) == "news-message:/"));
     }
 }
 
-function UpdateColumnsForView(folder, viewType)
-{
-  const nsMsgFolderFlags = Components.interfaces.nsMsgFolderFlags;
-  // if this is the drafts, sent, or send later folder,
-  // we show "Recipient" instead of "Author"
-  SetSentFolderColumns(IsSpecialFolder(folder, nsMsgFolderFlags.SentMail | nsMsgFolderFlags.Drafts | nsMsgFolderFlags.Queue, true));
-  ShowLocationColumn(viewType == nsMsgViewType.eShowVirtualFolderResults);
-  // Only show 'Received' column for e-mails.  For newsgroup messages, the 'Date' header is as reliable as an e-mail's
-  // 'Received' header, as it is replaced with the news server's (more reliable) date.
-  UpdateReceivedColumn(folder);
-}
-
-function RerootFolder(uri, newFolder, viewType, viewFlags, sortType, sortOrder)
-{
-  viewDebug("In reroot folder, sortType = " +  sortType + "viewType = " + viewType + "\n");
-
-  if (sortType == 0)
-  {
-    try
-    {
-      var dbFolderInfo = newFolder.msgDatabase.dBFolderInfo;
-      sortType = dbFolderInfo.sortType;
-      sortOrder = dbFolderInfo.sortOrder;
-      viewFlags = dbFolderInfo.viewFlags;
-      viewType = dbFolderInfo.viewType;
-      dbFolderInfo = null;
-    }
-    catch(ex)
-    {
-      dump("invalid db in RerootFolder: " + ex + "\n");
-    }
-  }
-
-  // workaround for #39655
-  gFolderJustSwitched = true;
-
-  ClearThreadPaneSelection();
-
-  //Clear the new messages of the old folder
-  var oldFolder = gPrevSelectedFolder;
-  if (oldFolder) {
-    oldFolder.clearNewMessages();
-    oldFolder.hasNewMessages = false;
-  }
-
-  //Set the window's new open folder.
-  msgWindow.openFolder = newFolder;
-
-  //the new folder being selected should have its biff state get cleared.
-  if(newFolder)
-  {
-    newFolder.biffState =
-          Components.interfaces.nsIMsgFolder.nsMsgBiffState_NoMail;
-  }
-
-  //Clear out the thread pane so that we can sort it with the new sort id without taking any time.
-  // folder.setAttribute('ref', "");
-
-  // null this out, so we don't try sort.
-  if (gDBView) {
-    gDBView.close();
-    gDBView = null;
-  }
-
-  // cancel the pending mark as read timer
-  ClearPendingReadTimer();
-
-  UpdateColumnsForView(newFolder, viewType);
-  // now create the db view, which will sort it.
-  CreateDBView(newFolder, viewType, viewFlags, sortType, sortOrder);
-
-  if (oldFolder)
-  {
-    /* we don't null out the db reference for inbox because inbox is like the "main" folder
-               and performance outweighs footprint*/
-    if (!IsSpecialFolder(oldFolder, Components.interfaces.nsMsgFolderFlags.Inbox, false))
-    {
-      if (oldFolder.URI != newFolder.URI)
-        oldFolder.msgDatabase = null;
-    }
-  }
-  // that should have initialized gDBView, now re-root the thread pane
-  RerootThreadPane();
-  SetUpToolbarButtons(uri);
-  UpdateStatusMessageCounts(gMsgFolderSelected);
-  
-  // hook for extra toolbar items
-  var observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
-  observerService.notifyObservers(window, "mail:updateToolbarItems", null);
-  // this is to kick off cross-folder searches for virtual folders.
-  if (gSearchSession && !gVirtualFolderTerms) // another var might be better...
-  {
-    viewDebug("doing a xf folder search in rerootFolder\n");
-    gCurrentLoadingFolderURI = ""
-    ViewChangeByFolder(newFolder);
-    gPreQuickSearchView = null; // don't remember the cross folder search
-    ScrollToMessageAfterFolderLoad(newFolder);
-  }
-}
-
 function SwitchView(command)
 {
   // when switching thread views, we might be coming out of quick search
   // or a message view.
   // first set view picker to all
-  if (gCurrentViewValue != kViewItemAll)
-    ViewChangeByValue(kViewItemAll);
+  if (gFolderDisplay.view.mailViewIndex != kViewItemAll)
+    gFolderDisplay.view.setMailView(kViewItemAll);
 
-  // clear the QS text, if we need to
-  ClearQSIfNecessary();
-  
-  var oldSortType, oldSortOrder, viewFlags, viewType, db;
-  // now switch views
-  if (gDBView) 
-  {
-    oldSortType = gDBView.sortType;
-    oldSortOrder = gDBView.sortOrder;
-    viewFlags = gDBView.viewFlags;
-    viewType = gDBView.viewType;
-    db = gDBView.db;
-    gDBView.close();
-    gDBView = null; 
-  }
-  else
-  {
-    oldSortType = nsMsgViewSortType.byThread;
-    oldSortOrder = nsMsgViewSortOrder.ascending;
-    viewFlags = gCurViewFlags;
-    viewType = nsMsgViewType.eShowAllThreads;
-    db = null;
-  }
   switch(command)
   {
     // "All" threads and "Unread" threads don't change threading state
     case "cmd_viewAllMsgs":
-      viewType = nsMsgViewType.eShowAllThreads;
-      viewFlags = viewFlags & ~nsMsgViewFlagsType.kUnreadOnly;
+      gFolderDisplay.view.showUnreadOnly = false;
       break;
     case "cmd_viewUnreadMsgs":
-      viewType = nsMsgViewType.eShowAllThreads;
-      viewFlags = viewFlags | nsMsgViewFlagsType.kUnreadOnly;
+      gFolderDisplay.view.showUnreadOnly = true;
       break;
     // "Threads with Unread" and "Watched Threads with Unread" force threading
     case "cmd_viewWatchedThreadsWithUnread":
-      viewType = nsMsgViewType.eShowWatchedThreadsWithUnread;
-      viewFlags |= nsMsgViewFlagsType.kThreadedDisplay;
+      gFolderDisplay.view.specialViewWatchedThreadsWithUnread = true;
       break;
     case "cmd_viewThreadsWithUnread":
-      viewType = nsMsgViewType.eShowThreadsWithUnread;
-      viewFlags |= nsMsgViewFlagsType.kThreadedDisplay;
+      gFolderDisplay.view.specialViewThreadsWithUnread = true;
       break;
     // "Ignored Threads" toggles 'ignored' inclusion --
     //   but it also resets 'With Unread' views to 'All'
     case "cmd_viewIgnoredThreads":
-      viewType = nsMsgViewType.eShowAllThreads;
-      if (viewFlags & nsMsgViewFlagsType.kShowIgnored)
-        viewFlags = viewFlags & ~nsMsgViewFlagsType.kShowIgnored;
-      else
-        viewFlags = viewFlags | nsMsgViewFlagsType.kShowIgnored;
+      gFolderDisplay.view.showIgnored = !gFolderDisplay.view.showIgnored;
       break;
   }
-
-  if (db && viewType == nsMsgViewType.eShowVirtualFolderResults)
-  {
-      db.dBFolderInfo.viewFlags = viewFlags;
-      gMsgFolderSelected = null;
-      msgWindow.openFolder = null;
-      FolderPaneSelectionChange();
-      LoadCurrentlyDisplayedMessage();
-  }
-  else
-  {
-    CreateDBView(msgWindow.openFolder, viewType, viewFlags, oldSortType,
-                 oldSortOrder);
-  RerootThreadPane();
-  }
-}
-
-function SetSentFolderColumns(isSentFolder)
-{
-  var tree = GetThreadTree();
-
-  var lastFolderSent = tree.getAttribute("lastfoldersent") == "true";
-  if (isSentFolder != lastFolderSent)
-  {
-    var senderColumn = document.getElementById("senderCol");
-    var recipientColumn = document.getElementById("recipientCol");
-    
-    var saveHidden = senderColumn.getAttribute("hidden");
-    senderColumn.setAttribute("hidden", senderColumn.getAttribute("swappedhidden"));
-    senderColumn.setAttribute("swappedhidden", saveHidden);
-
-    saveHidden = recipientColumn.getAttribute("hidden");
-    recipientColumn.setAttribute("hidden", recipientColumn.getAttribute("swappedhidden"));
-    recipientColumn.setAttribute("swappedhidden", saveHidden);
-  }
-
-  if(isSentFolder)
-    tree.setAttribute("lastfoldersent", "true");
-  else
-    tree.setAttribute("lastfoldersent", "false");
-}
-
-function ShowLocationColumn(show)
-{
-  var col = document.getElementById("locationCol");
-  if (col) {
-    if (show) {
-      col.removeAttribute("hidden");
-      col.removeAttribute("ignoreincolumnpicker");
-    }
-    else {
-      col.setAttribute("hidden","true");
-      col.setAttribute("ignoreincolumnpicker","true");
-    }
-  }
-}
-
-function UpdateReceivedColumn(newFolder)
-{
-  // Only show 'Received' column for e-mails.  For newsgroup messages, the 'Date' header is as reliable as an e-mail's
-  // 'Received' header, as it is replaced with the news server's (more reliable) date.
-  var receivedColumn = document.getElementById("receivedCol");
-
-  const nsMsgFolderFlags = Components.interfaces.nsMsgFolderFlags;
-  var newFolderShowsRcvd = (newFolder.flags & nsMsgFolderFlags.Mail) &&
-    !(newFolder.flags & (nsMsgFolderFlags.Queue | nsMsgFolderFlags.Drafts |
-                         nsMsgFolderFlags.Templates |
-                         nsMsgFolderFlags.SentMail));
-    
-  var tempHidden = receivedColumn.getAttribute("temphidden") == "true";
-  var isHidden = receivedColumn.getAttribute("hidden") == "true";
-  
-  if (!newFolderShowsRcvd && !isHidden)
-  {
-    // Record state & hide
-    receivedColumn.setAttribute("temphidden", "true");
-    receivedColumn.setAttribute("hidden", "true");
-  }
-  else if (newFolderShowsRcvd && tempHidden && isHidden)
-  {
-    receivedColumn.setAttribute("hidden", "false");
-  }
-  
-  if (newFolderShowsRcvd)
-  {
-    receivedColumn.removeAttribute("ignoreincolumnpicker");
-    receivedColumn.removeAttribute("temphidden");
-  }
-  else
-    receivedColumn.setAttribute("ignoreincolumnpicker", "true");
 }
 
 function SetNewsFolderColumns()
 {
   var sizeColumn = document.getElementById("sizeCol");
 
   if (gDBView.usingLines) {
      sizeColumn.setAttribute("label",gMessengerBundle.getString("linesColumnHeader"));
@@ -494,17 +120,17 @@ function UpdateStatusMessageCounts(folde
   var unreadElement = GetUnreadCountElement();
   var totalElement = GetTotalCountElement();
   if(folder && unreadElement && totalElement)
   {
     var numSelected = GetNumSelectedMessages();
 
     var numUnread = (numSelected > 1) ?
             gMessengerBundle.getFormattedString("selectedMsgStatus",
-                                                [numSelected]) :    
+                                                [numSelected]) :
             gMessengerBundle.getFormattedString("unreadMsgStatus",
                                                 [ folder.getNumUnread(false)]);
     var numTotal =
             gMessengerBundle.getFormattedString("totalMsgStatus",
                                                 [folder.getTotalMessages(false)]);
 
     unreadElement.setAttribute("label", numUnread);
     totalElement.setAttribute("label", numTotal);
@@ -637,19 +263,18 @@ function ConvertSortTypeToColumnID(sortK
     case nsMsgViewSortType.byAttachments:
       columnID = "attachmentCol";
       break;
     case nsMsgViewSortType.byCustom:
 
       //TODO: either change try() catch to if (property exists) or restore the getColumnHandler() check
       try //getColumnHandler throws an errror when the ID is not handled
       {
-        columnID = gDBView.db.dBFolderInfo.getProperty('customSortCol');
+        columnID = gDBView.curCustomColumn;
       }
-
       catch (err) { //error - means no handler
         dump("ConvertSortTypeToColumnID: custom sort key but no handler for column '" + columnID + "'\n");
         columnID = "dateCol";
       }
 
       break;
     default:
       dump("unsupported sort key: " + sortKey + "\n");
@@ -665,314 +290,86 @@ var nsMsgViewFlagsType = Components.inte
 var nsMsgViewCommandType = Components.interfaces.nsMsgViewCommandType;
 var nsMsgViewType = Components.interfaces.nsMsgViewType;
 var nsMsgNavigationType = Components.interfaces.nsMsgNavigationType;
 
 var gDBView = null;
 var gCurViewFlags;
 var gCurSortType;
 
-// CreateDBView is called when we have a thread pane. CreateBareDBView is called when there is no
-// tree associated with the view. CreateDBView will call into CreateBareDBView...
-
-function CreateBareDBView(originalView, msgFolder, viewType, viewFlags, sortType, sortOrder)
-{
-  var dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=";
-  // hack to turn this into an integer, if it was a string
-  // it would be a string if it came from localStore.rdf
-  viewType = viewType - 0;
-
-  switch (viewType) {
-      case nsMsgViewType.eShowQuickSearchResults:
-          dbviewContractId += "quicksearch";
-          break;
-      case nsMsgViewType.eShowThreadsWithUnread:
-          dbviewContractId += "threadswithunread";
-          break;
-      case nsMsgViewType.eShowWatchedThreadsWithUnread:
-          dbviewContractId += "watchedthreadswithunread";
-          break;
-      case nsMsgViewType.eShowVirtualFolderResults:
-          dbviewContractId += "xfvf";
-          break;
-      case nsMsgViewType.eShowSearch:
-          dbviewContractId += "search";
-          break;
-      case nsMsgViewType.eShowAllThreads:
-      default:
-          if (sortType == nsMsgViewSortType.byThread || sortType == nsMsgViewSortType.byId
-            || sortType == nsMsgViewSortType.byNone)
-            viewFlags &= ~nsMsgViewFlagsType.kGroupBySort;
-
-          if (viewFlags & nsMsgViewFlagsType.kGroupBySort)
-            dbviewContractId += "group";
-          else
-            dbviewContractId += "threaded";
-          break;
-  }
-
-  //  dump ("contract id = " + dbviewContractId + "original view = " + originalView + "\n");
-  if (!originalView)
-    gDBView = Components.classes[dbviewContractId].createInstance(Components.interfaces.nsIMsgDBView);
-
-  gCurViewFlags = viewFlags;
-  var count = new Object;
-  if (!gThreadPaneCommandUpdater)
-    gThreadPaneCommandUpdater = new nsMsgDBViewCommandUpdater();
-
-  gCurSortType = sortType;
-
-  if (!originalView) {
-    gDBView.init(messenger, msgWindow, gThreadPaneCommandUpdater);
-    gDBView.open(msgFolder, gCurSortType, sortOrder, viewFlags, count);
-    if (viewType == nsMsgViewType.eShowVirtualFolderResults)
-    {
-      // the view is a listener on the search results
-      gViewSearchListener = gDBView.QueryInterface(Components.interfaces.nsIMsgSearchNotify);
-      gSearchSession.registerListener(gViewSearchListener);
-    }
-  } 
-  else {
-    gDBView = originalView.cloneDBView(messenger, msgWindow, gThreadPaneCommandUpdater);
-  }
-}
-
-function CreateDBView(msgFolder, viewType, viewFlags, sortType, sortOrder)
-{
-  // call the inner create method
-  CreateBareDBView(null, msgFolder, viewType, viewFlags, sortType, sortOrder);
-
-  // now do tree specific work
-
-  // based on the collapsed state of the thread pane/message pane splitter,
-  // suppress message display if appropriate.
-  gDBView.suppressMsgDisplay = IsMessagePaneCollapsed();
-
-  UpdateSortIndicators(gCurSortType, sortOrder);
-  var ObserverService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
-  ObserverService.notifyObservers(msgFolder, "MsgCreateDBView", viewType + ":" + viewFlags);
-}
 
 function ChangeMessagePaneVisibility(now_hidden)
 {
   // we also have to hide the File/Attachments menuitem
   var node = document.getElementById("fileAttachmentMenu");
   if (node)
     node.hidden = now_hidden;
 
-  if (gDBView) {
-    // the collapsed state is the state after we released the mouse 
-    // so we take it as it is
-    gDBView.suppressMsgDisplay = now_hidden;
-  }
+  gMessageDisplay.visible = !now_hidden;
+
   var event = document.createEvent('Events');
   if (now_hidden) {
     event.initEvent('messagepane-hide', false, true);
   }
   else {
     event.initEvent('messagepane-unhide', false, true);
   }
   document.getElementById("messengerWindow").dispatchEvent(event);
 }
 
 function OnMouseUpThreadAndMessagePaneSplitter()
 {
-  // the collapsed state is the state after we released the mouse 
-  // so we take it as it is
+  // The collapsed state is the state after we released the mouse,
+  // so we take it as it is.
   ChangeMessagePaneVisibility(IsMessagePaneCollapsed());
 }
 
+/**
+ * Our multiplexed tabbing model ends up sending synthetic folder pane
+ *  selection change notifications.  We want to ignore these because the
+ *  user may explicitly re-select a folder intentionally, and we want to
+ *  be able to know that.  So we filter out the synthetics here.
+ * The tabbing logic sets this global to help us out.
+ */
+var gIgnoreSyntheticFolderPaneSelectionChange = false;
 function FolderPaneSelectionChange()
 {
-    var folderSelection = gFolderTreeView.selection;
-
-    // This prevents a folder from being loaded in the case that the user
-    // has right-clicked on a folder different from the one that was
-    // originally highlighted.  On a right-click, the highlight (selection)
-    // of a row will be different from the value of currentIndex, thus if
-    // the currentIndex is not selected, it means the user right-clicked
-    // and we don't want to load the contents of the folder.
-    if (!folderSelection.isSelected(folderSelection.currentIndex))
-      return;
-
-    gVirtualFolderTerms = null;
-    gXFVirtualFolderTerms = null;
-
-    var folders = GetSelectedMsgFolders();
-    if (folders.length == 1)
-    {
-        var msgFolder = folders[0];
-        var uriToLoad = msgFolder.URI;
+  if (gIgnoreSyntheticFolderPaneSelectionChange) {
+    gIgnoreSyntheticFolderPaneSelectionChange = false;
+    return;
+  }
 
-        if (msgFolder == gMsgFolderSelected)
-           return;
-        // If msgFolder turns out to be a single folder saved search, a virtual folder,
-        // realFolder will get set to the underlying folder the
-        // saved search is based on.
-        var realFolder = msgFolder;
-        gPrevSelectedFolder = gMsgFolderSelected;
-        gMsgFolderSelected = msgFolder;
-        var folderFlags = msgFolder.flags;
-        // If this is same folder, and we're not showing a virtual folder
-        // then do nothing.
-        const nsMsgFolderFlags = Components.interfaces.nsMsgFolderFlags;
-        if (msgFolder == msgWindow.openFolder &&
-            !(folderFlags & nsMsgFolderFlags.Virtual) &&
-            !(gPrevFolderFlags & nsMsgFolderFlags.Virtual))
-        {
-            return;
-        }
-        else
-        {
-            const outFolderFlagMask = nsMsgFolderFlags.SentMail |
-              nsMsgFolderFlags.Drafts | nsMsgFolderFlags.Queue |
-              nsMsgFolderFlags.Templates;
-            if (IsSpecialFolder(gMsgFolderSelected, outFolderFlagMask, true))
-            {
-              if (!gPrevSelectedFolder ||
-                  !IsSpecialFolder(gPrevSelectedFolder, outFolderFlagMask, true))
-                onSearchFolderTypeChanged(true);
-            }
-            else
-            {
-              if (!gPrevSelectedFolder ||
-                  IsSpecialFolder(gPrevSelectedFolder, outFolderFlagMask, true))
-                onSearchFolderTypeChanged(false);
-            }
+  let folderSelection = gFolderTreeView.selection;
 
-            OnLeavingFolder(gPrevSelectedFolder);  // mark all read in last folder
-            var sortType = 0;
-            var sortOrder = 0;
-            var viewFlags = 0;
-            var viewType = 0;
-            gDefaultSearchViewTerms = null;
-            gVirtualFolderTerms = null;
-            gXFVirtualFolderTerms = null;
-            gPrevFolderFlags = folderFlags;
-            gCurrentVirtualFolderUri = null;
-            // don't get the db if this folder is a server
-            // we're going to be display account central
-            if (!(msgFolder.isServer)) 
-            {
-              try 
-              {
-                var msgDatabase = msgFolder.msgDatabase;
-                if (msgDatabase)
-                {
-                  var dbFolderInfo = msgDatabase.dBFolderInfo;
-                  sortType = dbFolderInfo.sortType;
-                  sortOrder = dbFolderInfo.sortOrder;
-                  viewFlags = dbFolderInfo.viewFlags;
-                  if (folderFlags & Components.interfaces.nsMsgFolderFlags.Virtual)
-                  {
-                    viewType = nsMsgViewType.eShowQuickSearchResults;
-                    var searchTermString = dbFolderInfo.getCharProperty("searchStr");
-                    var searchOnline = dbFolderInfo.getBooleanProperty("searchOnline", false);
-                    // trick the view code into updating the real folder...
-                    gCurrentVirtualFolderUri = uriToLoad;
-                    viewDebug("uriToLoad = " + uriToLoad + "\n");
-                    var srchFolderUri = dbFolderInfo.getCharProperty("searchFolderUri");
-                    var srchFolderUriArray = srchFolderUri.split('|');
-                    // cross folder search
-                    var filterService = Components.classes["@mozilla.org/messenger/services/filters;1"].getService(Components.interfaces.nsIMsgFilterService);
-                    var filterList = filterService.getTempFilterList(msgFolder);
-                    var tempFilter = filterList.createFilter("temp");
-                    filterList.parseCondition(tempFilter, searchTermString);
-                    if (srchFolderUriArray.length > 1)
-                    {
-                      viewType = nsMsgViewType.eShowVirtualFolderResults;
-                      gXFVirtualFolderTerms = CreateGroupedSearchTerms(tempFilter.searchTerms);
-                      setupXFVirtualFolderSearch(srchFolderUriArray, gXFVirtualFolderTerms, searchOnline);
-                      // need to set things up so that reroot folder issues the search
-                    }
-                    else
-                    {
-                      gSearchSession = null;
-                      uriToLoad = srchFolderUri;
-                      // we need to load the db for the actual folder so that many hdrs to download
-                      // will return false...
-                      realFolder = GetMsgFolderFromUri(uriToLoad);
-                      msgDatabase = realFolder.msgDatabase;
-                      gVirtualFolderTerms = CreateGroupedSearchTerms(tempFilter.searchTerms);
-                    }
-                  }
-                  else
-                  {
-                    gSearchSession = null;
-                    viewFlags = dbFolderInfo.viewFlags;
-                    viewType = dbFolderInfo.viewType;
-                  }
-                  msgDatabase = null;
-                  dbFolderInfo = null;
-                }
-              }
-              catch (ex)
-              {
-                dump("failed to get view & sort values.  ex = " + ex +"\n");
-              }
-            }
-            if (gDBView && gDBView.viewType == nsMsgViewType.eShowQuickSearchResults)
-            {
-              if (gPreQuickSearchView) //close cached view before quick search
-              {
-                gPreQuickSearchView.close();
-                gPreQuickSearchView = null;  
-              }
-              ClearQSIfNecessary();
-            }
-            ClearMessagePane();
+  // This prevents a folder from being loaded in the case that the user
+  // has right-clicked on a folder different from the one that was
+  // originally highlighted.  On a right-click, the highlight (selection)
+  // of a row will be different from the value of currentIndex, thus if
+  // the currentIndex is not selected, it means the user right-clicked
+  // and we don't want to load the contents of the folder.
+  if (!folderSelection.isSelected(folderSelection.currentIndex))
+    return;
 
-            if (gXFVirtualFolderTerms)
-              viewType = nsMsgViewType.eShowVirtualFolderResults;
-            else if (gSearchEmailAddress || gVirtualFolderTerms)
-              viewType = nsMsgViewType.eShowQuickSearchResults;
-            else if (viewType == nsMsgViewType.eShowQuickSearchResults)
-              viewType = nsMsgViewType.eShowAllThreads;  //override viewType - we don't want to start w/ quick search
-            ChangeFolder(realFolder, msgFolder, viewType, viewFlags, sortType, sortOrder);
-            if (gVirtualFolderTerms)
-              gDBView.viewFolder = msgFolder;
-        }
-        document.getElementById('tabmail').setTabTitle(null);
-    }
-    else
-    {
-      msgWindow.openFolder = null;
-      ClearThreadPane();
-    }
-
-    var startpageenabled = gPrefBranch.getBoolPref("mailnews.start_page.enabled");
-    if (gDisplayStartupPage && startpageenabled)
-    {
-      loadStartPage();
-      gDisplayStartupPage = false;
-    }
-    UpdateMailToolbar("FolderPaneSelectionChange");
-}
-
-function ClearThreadPane()
-{
-  if (gDBView) {
-    gDBView.close();
-    gDBView = null; 
-  }
+  let folders = GetSelectedMsgFolders();
+  gFolderDisplay.show(folders.length ? folders[0] : null);
 }
 
 function IsSpecialFolder(msgFolder, flags, checkAncestors)
 {
-    if (!msgFolder) 
+    if (!msgFolder)
         return false;
     else if ((msgFolder.flags & flags) == 0)
     {
       var parentMsgFolder = msgFolder.parentMsgFolder;
 
       return (parentMsgFolder && checkAncestors) ? IsSpecialFolder(parentMsgFolder, flags, true) : false;
     }
     else {
         // the user can set their INBOX to be their SENT folder.
-        // in that case, we want this folder to act like an INBOX, 
+        // in that case, we want this folder to act like an INBOX,
         // and not a SENT folder
         const nsMsgFolderFlags = Components.interfaces.nsMsgFolderFlags;
         return !((flags & nsMsgFolderFlags.SentMail) &&
                  (msgFolder.flags & nsMsgFolderFlags.Inbox));
     }
 }
 
 function Undo()
@@ -980,166 +377,10 @@ function Undo()
     messenger.undo(msgWindow);
 }
 
 function Redo()
 {
     messenger.redo(msgWindow);
 }
 
-function getSearchTermString(searchTerms)
-{
-  var searchIndex;
-  var condition = "";
-  var count = searchTerms.Count();
-  for (searchIndex = 0; searchIndex < count; )
-  {
-    var term = searchTerms.QueryElementAt(searchIndex++, Components.interfaces.nsIMsgSearchTerm);
-    
-    if (condition.length > 1)
-      condition += ' ';
-    
-    if (term.matchAll)
-    {
-        condition = "ALL";
-        break;
-    }
-    condition += (term.booleanAnd) ? "AND (" : "OR (";
-    condition += term.termAsString + ')';
-  }
-  return condition;
-}
-
-function  CreateVirtualFolder(newName, parentFolder, searchFolderURIs, searchTerms, searchOnline)
-{
-  // ### need to make sure view/folder doesn't exist.
-  if (searchFolderURIs && (searchFolderURIs != "") && newName && (newName != "")) 
-  {
-    try
-    {
-      var newFolder = parentFolder.addSubfolder(newName);
-      newFolder.prettyName = newName;
-      newFolder.setFlag(Components.interfaces.nsMsgFolderFlags.Virtual);
-      var vfdb = newFolder.msgDatabase;
-      var searchTermString = getSearchTermString(searchTerms);
-      var dbFolderInfo = vfdb.dBFolderInfo;
-      // set the view string as a property of the db folder info
-      // set the original folder name as well.
-      dbFolderInfo.setCharProperty("searchStr", searchTermString);
-      dbFolderInfo.setCharProperty("searchFolderUri", searchFolderURIs);
-      dbFolderInfo.setBooleanProperty("searchOnline", searchOnline);
-
-      vfdb.summaryValid = true;
-      vfdb.Close(true);
-      parentFolder.NotifyItemAdded(newFolder);
-      var accountManager = Components.classes["@mozilla.org/messenger/account-manager;1"].getService(Components.interfaces.nsIMsgAccountManager);
-      accountManager.saveVirtualFolders();
-    }
-    catch(e)
-    {
-      throw(e); // so that the dialog does not automatically close
-      dump ("Exception : creating virtual folder \n");
-    }
-  }
-  else 
-  {
-    dump("no name or nothing selected\n");
-  }   
-}
-
-var gSearchSession;
-
-var nsMsgSearchScope = Components.interfaces.nsMsgSearchScope;
-
 var gMessengerBundle = null;
 
-// Datasource search listener -- made global as it has to be registered
-// and unregistered in different functions.
-var gViewSearchListener;
-
-function GetScopeForFolder(folder) 
-{
-  return folder.server.searchScope;
-}
-
-function setupXFVirtualFolderSearch(folderUrisToSearch, searchTerms, searchOnline)
-{
-  const Ci = Components.interfaces;
-  var count = new Object;
-  var i;
-
-  gSearchSession = Components.classes["@mozilla.org/messenger/searchSession;1"]
-                             .createInstance(Ci.nsIMsgSearchSession);
-
-  for (i in folderUrisToSearch)
-    {
-      var realFolder = GetMsgFolderFromUri(folderUrisToSearch[i]);
-      if (!realFolder.isServer)
-        gSearchSession.addScopeTerm(!searchOnline ? nsMsgSearchScope.offlineMail : GetScopeForFolder(realFolder), realFolder);
-    }
-
-    var termsArray = searchTerms.QueryInterface(Components.interfaces.nsISupportsArray);
-    const nsIMsgSearchTerm = Components.interfaces.nsIMsgSearchTerm;
-    for each (var term in fixIterator(termsArray, nsIMsgSearchTerm)) {
-      gSearchSession.appendTerm(term);
-    }
-}
-
-function CreateGroupedSearchTerms(searchTermsArray)
-{
-  const Ci = Components.interfaces;
-  var searchSession = gSearchSession;
-  if (!searchSession) {
-    searchSession = Components.classes["@mozilla.org/messenger/searchSession;1"]
-                              .createInstance(Ci.nsIMsgSearchSession);
-  }
-
-  // create a temporary isupports array to store our search terms
-  // since we will be modifying the terms so they work with quick search
-  var searchTermsArrayForQS = Components.classes["@mozilla.org/supports-array;1"].createInstance(Components.interfaces.nsISupportsArray);
-  
-  var numEntries = searchTermsArray.Count();
-  for (var i = 0; i < numEntries; i++) {
-    var searchTerm = searchTermsArray.GetElementAt(i).QueryInterface(Components.interfaces.nsIMsgSearchTerm); 
-
-    // clone the term, since we might be modifying it
-    var searchTermForQS = searchSession.createTerm();
-    searchTermForQS.value = searchTerm.value;
-    searchTermForQS.attrib = searchTerm.attrib;
-    searchTermForQS.arbitraryHeader = searchTerm.arbitraryHeader
-    searchTermForQS.op = searchTerm.op;
-
-    // mark the first node as a group
-    if (i == 0)
-      searchTermForQS.beginsGrouping = true;
-    else if (i == numEntries - 1)
-      searchTermForQS.endsGrouping = true;
-
-    // turn the first term to true to work with quick search...
-    searchTermForQS.booleanAnd = i ? searchTerm.booleanAnd : true; 
-    
-    searchTermsArrayForQS.AppendElement(searchTermForQS);
-  }
-  return searchTermsArrayForQS;
-}
-
-function OnLeavingFolder(aFolder)
-{
-  try
-  {
-    // Mark all messages of aFolder as read:
-    // We can't use the command controller, because it is already tuned in to the
-    // new folder, so we just mimic its behaviour wrt goDoCommand('cmd_markAllRead').
-    if (gDBView && gPrefBranch.getBoolPref("mailnews.mark_message_read." + aFolder.server.type))
-    {
-      gDBView.doCommand(nsMsgViewCommandType.markAllRead);
-    }
-  }
-  catch(e){/* ignore */}
-}
-
-var gViewDebug = false;
-
-function viewDebug(str)
-{
-  if (gViewDebug)
-    dump(str);
-}
--- a/mail/base/content/extraCustomizeItems.xul
+++ b/mail/base/content/extraCustomizeItems.xul
@@ -1,46 +1,48 @@
 <?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 Mozilla Communicator client code, released
-# March 31, 1998.
-#
-# The Initial Developer of the Original Code is
-# Netscape Communications Corporation.
-# Portions created by the Initial Developer are Copyright (C) 1998-1999
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#   Karsten Düsterloh <mnyromyr@tprac.de>
-#   Simon Paquet <bugzilla@babylonsounds.com>
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# 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 *****
+<!--
+ - ***** BEGIN LICENSE BLOCK *****
+ - Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ -
+ - The contents of this file are subject to the Mozilla Public License Version
+ - 1.1 (the "License"); you may not use this file except in compliance with
+ - the License. You may obtain a copy of the License at
+ - http://www.mozilla.org/MPL/
+ -
+ - Software distributed under the License is distributed on an "AS IS" basis,
+ - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ - for the specific language governing rights and limitations under the
+ - License.
+ -
+ - The Original Code is Mozilla Communicator client code, released
+ - March 31, 1998.
+ -
+ - The Initial Developer of the Original Code is
+ - Netscape Communications Corporation.
+ - Portions created by the Initial Developer are Copyright (C) 1998-1999
+ - the Initial Developer. All Rights Reserved.
+ -
+ - Contributor(s):
+ -   Karsten Düsterloh <mnyromyr@tprac.de>
+ -   Simon Paquet <bugzilla@babylonsounds.com>
+ -
+ - Alternatively, the contents of this file may be used under the terms of
+ - either the GNU General Public License Version 2 or later (the "GPL"), or
+ - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ - in which case the provisions of the GPL or the LGPL are applicable instead
+ - of those above. If you wish to allow use of your version of this file only
+ - under the terms of either the GPL or the LGPL, and not to allow others to
+ - use your version of this file under the terms of the MPL, indicate your
+ - 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 *****
+ -->
 
 <!DOCTYPE overlay [
   <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
   %globalDTD;
   <!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd">
   %messengerDTD;
   <!ENTITY % msgViewPickerDTD SYSTEM "chrome://messenger/locale/msgViewPickerOverlay.dtd" >
   %msgViewPickerDTD;
@@ -152,20 +154,26 @@
                  title="&mailViewsToolbarItem.title;"
                  align="center"
                  class="chromeclass-toolbar-additional">
       <label id="viewPickerLabel"
              value="&viewPicker.label;"
              control="viewPicker"
              accesskey="&viewPicker.accesskey;"/>
       <menulist id="viewPicker" oncommand="ViewChangeByMenuitem(event.target);">
-        <menupopup id="viewPickerPopup" onpopupshowing="RefreshViewPopup(this, false);">
-          <menuitem id="viewPickerAll" value="0" label="&viewAll.label;"/>
-          <menuitem id="viewPickerUnread" value="1" label="&viewUnread.label;"/>
-          <menuitem id="viewPickerNotDeleted" value="3" label="&viewNotDeleted.label;"/>
+        <menupopup id="viewPickerPopup" onpopupshowing="RefreshViewPopup(this);">
+          <menuitem id="viewPickerAll" value="0"
+                    label="&viewAll.label;"
+                    type="radio"/>
+          <menuitem id="viewPickerUnread" value="1"
+                    label="&viewUnread.label;"
+                    type="radio"/>
+          <menuitem id="viewPickerNotDeleted" value="3"
+                    label="&viewNotDeleted.label;"
+                    type="radio"/>
           <menuseparator id="afterViewPickerUnreadSeparator"/>
           <menu id="viewPickerTags" label="&viewTags.label;">
             <menupopup id="viewPickerTagsPopup"
                        class="menulist-menupopup"
                        onpopupshowing="RefreshTagsPopup(this, true);"/>
           </menu>
           <menu id="viewPickerCustomViews" label="&viewCustomViews.label;">
             <menupopup id="viewPickerCustomViewsPopup"
new file mode 100644
--- /dev/null
+++ b/mail/base/content/folderDisplay.js
@@ -0,0 +1,2073 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Mail Client.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Andrew Sutherland <asutherland@asutherland.org>
+ *   David Bienvenu <bienvenu@nventure.com>
+ *   Siddharth Agarwal <sid.bugzilla@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+Components.utils.import("resource://app/modules/dbViewWrapper.js");
+
+var gFolderDisplay = null;
+var gMessageDisplay = null;
+
+var nsMsgFolderFlags = Components.interfaces.nsMsgFolderFlags;
+
+/**
+ * Abstraction for a widget that (roughly speaking) displays the contents of
+ *  folders.  The widget belongs to a tab and has a lifetime as long as the tab
+ *  that contains it.  This class is strictly concerned with the UI aspects of
+ *  this; the DBViewWrapper class handles the view details (and is exposed on
+ *  the 'view' attribute.)
+ *
+ * The search window subclasses this into the SearchFolderDisplayWidget rather
+ *  than us attempting to generalize everything excessively.  This is because
+ *  we hate the search window and don't want to clutter up this code for it.
+ * The standalone message display window also subclasses us; we do not hate it,
+ *  but it's not invited to our birthday party either.
+ * For reasons of simplicity and the original order of implementation, this
+ *  class does alter its behavior slightly for the benefit of the standalone
+ *  message window.  If no tab info is provided, we avoid touching tabmail
+ *  (which is good, because it won't exist!)  And now we guard against treeBox
+ *  manipulations...
+ */
+function FolderDisplayWidget(aTabInfo, aMessageDisplayWidget) {
+  this._tabInfo = aTabInfo;
+
+  /// If the folder does not get handled by the DBViewWrapper, stash it here.
+  ///  ex: when isServer is true.
+  this._nonViewFolder = null;
+
+  this.view = new DBViewWrapper(this);
+  this.messageDisplay = aMessageDisplayWidget;
+  this.messageDisplay.folderDisplay = this;
+
+  /**
+   * The XUL tree node, as retrieved by getDocumentElementById.  The caller is
+   *  responsible for setting this.
+   */
+  this.tree = null;
+  /**
+   * The nsITreeBoxObject on the XUL tree node, accessible from this.tree as
+   *  this.tree.boxObject and QueryInterfaced as such.  The caller is
+   *  responsible for setting this.
+   */
+  this.treeBox = null;
+
+  /**
+   * The nsIMsgWindow corresponding to the window that holds us.  There is only
+   *  one of these per tab.  The caller is responsible for setting this.
+   */
+  this.msgWindow = null;
+  /**
+   * The nsIMessenger instance that corresponds to our tab/window.  We do not
+   *  use this ourselves, but are responsible for using it to update the
+   *  global |messenger| object so that our tab maintains its own undo and
+   *  navigation history.  At some point we might touch it for those reasons.
+   */
+  this.messenger = null;
+  this.threadPaneCommandUpdater = this;
+
+  /**
+   * Flag to expose whether all messages are loaded or not.  Set by
+   *  onAllMessagesLoaded.
+   */
+  this._allMessagesLoaded = false;
+
+  /**
+   * Save the top row displayed when we go inactive, restore when we go active,
+   *  nuke it when we destroy the view.
+   */
+  this._savedFirstVisibleRow = null;
+  /** the next view index to select once the delete completes */
+  this._nextViewIndexAfterDelete = null;
+  /**
+   * Track when a mass move is in effect (we get told by hintMassMoveStarting,
+   *  and hintMassMoveCompleted) so that we can avoid deletion-triggered
+   *  moving to _nextViewIndexAfterDelete until the mass move completes.
+   */
+  this._massMoveActive = false;
+
+  /**
+   * Used by pushNavigation to queue a navigation request for when we enter the
+   *  next folder; onAllMessagesLoaded is the one that processes it.
+   */
+  this._pendingNavigation = null;
+
+  this._active = false;
+  /**
+   * A list of methods to call on 'this' object when we are next made active.
+   *  This list is populated by calls to |_notifyWhenActive| when we are
+   *  not active at the moment.
+   */
+  this._notificationsPendingActivation = [];
+
+  let dummyDOMNode = document.getElementById('mail-toolbox');
+  /**
+   * Create a fake tree box object for if/when this folder is in the background.
+   * We need to give it a bogus DOM object to send events to, so we choose the
+   *  mail-toolbox, who is hopefully unlikely to take offense.
+   */
+  this._fakeTreeBox = dummyDOMNode ?
+                        new FakeTreeBoxObject(dummyDOMNode.boxObject) : null;
+
+  this._mostRecentSelectionCounts = [];
+  this._mostRecentCurrentIndices = [];
+}
+FolderDisplayWidget.prototype = {
+  /**
+   * @return the currently displayed folder.  This is just proxied from the
+   *     view wrapper.
+   */
+  get displayedFolder() {
+    return this._nonViewFolder || this.view.displayedFolder;
+  },
+
+  /**
+   * @return the nsITreeSelection object for our tree view if there is one,
+   *     null otherwise.
+   */
+  get treeSelection() {
+    if (this.view.dbView)
+      return this.view.dbView.selection;
+    else
+      return null;
+  },
+
+  /**
+   * Number of headers to tell the message database to cache when we enter a
+   *  folder.  This value is being propagated from legacy code which provided
+   *  no explanation for its choice.
+   *
+   * We definitely want the header cache size to be larger than the number of
+   *  rows that can be displayed on screen simultaneously.
+   */
+  PERF_HEADER_CACHE_SIZE: 100,
+
+  /**
+   * An optional list where each item is an object with the following
+   *  attributes sufficient to re-establish the selected items even in the face
+   *  of folder renaming.
+   * - messageId: The value of the message's message-id header.
+   *
+   * That's right, we only save the message-id header value.  This is arguably
+   *  overkill and ambiguous in the face of duplicate messages, but it's the
+   *  most persistent/reliable thing we have without gloda.
+   * Using the view index was ruled out because it is hardly stable.  Using the
+   *  message key alone is insufficient for cross-folder searches.  Using a
+   *  folder identifier and message key is insufficent for local folders in the
+   *  face of compaction, let alone complexities where the folder name may
+   *  change due to renaming/moving.  Which means we eventually need to fall
+   *  back to message-id anyways.  Feel free to add in lots of complexity if
+   *  you actually write unit tests for all the many possible cases.
+   * Additional justification is that selection saving/restoration should not
+   *  happen all that frequently.  A nice freebie is that message-id is
+   *  definitely persistable.
+   */
+  _savedSelection: null,
+
+  /**
+   * Save the current view selection for when we the view is getting destroyed
+   *  or otherwise re-ordered in such a way that the nsITreeSelection will lose
+   *  track of things (because it just has a naive view-index 'view' of the
+   *  world.)  We just save each message's message-id header.  This is overkill
+   *  and ambiguous in the face of duplicate messages (and expensive to
+   *  restore), but is also the most reliable option for this use case.
+   */
+  _saveSelection: function FolderDisplayWidget_saveSelection() {
+    this._savedSelection = [{messageId: msgHdr.messageId} for each
+                              ([, msgHdr] in Iterator(this.selectedMessages))];
+  },
+
+  /**
+   * Clear the saved selection.
+   */
+  _clearSavedSelection: function FolderDisplayWidget_clearSavedSelection() {
+    this._savedSelection = null;
+  },
+
+  /**
+   * Restore the view selection if we have a saved selection.  We must be
+   *  active!
+   *
+   * @return true if we were able to restore the selection and there was
+   *     a selection, false if there was no selection (anymore).
+   */
+  _restoreSelection: function FolderDisplayWidget_restoreSelection() {
+    if (!this._savedSelection || !this._active)
+      return false;
+
+    // translate message IDs back to messages.  this is O(s(m+n)) where:
+    // - s is the number of messages saved in the selection
+    // - m is the number of messages in the view (from findIndexOfMsgHdr)
+    // - n is the number of messages in the underlying folders (from
+    //   DBViewWrapper.getMsgHdrForMessageID).
+    // which ends up being O(sn)
+    var msgHdr;
+    let messages =
+      [msgHdr for each
+        ([, savedInfo] in Iterator(this._savedSelection)) if
+        ((msgHdr = this.view.getMsgHdrForMessageID(savedInfo.messageId)))];
+
+    this.selectMessages(messages, true);
+    this._savedSelection = null;
+
+    return this.selectedCount != 0;
+  },
+
+  /**
+   * Maps column ids to functions that test whether the column is legal for
+   *  display for the folder.  Each function should expect a DBViewWrapper
+   *  instance as its argument.  The intent is that the various helper
+   *  properties like isMailFolder/isIncomingFolder/isOutgoingFolder allow the
+   *  constraint to be expressed concisely.  If a helper does not exist, add
+   *  one! (If doing so is out of reach, than access viewWrapper.displayedFolder
+   *  to get at the nsIMsgFolder.)
+   * If a column does not have a function, it is assumed to be legal for display
+   *  in all cases.
+   */
+  COLUMN_LEGALITY_TESTERS: {
+    // Only show 'Received' column for e-mails.  For newsgroup messages, the
+    // 'Date' header is as reliable as an e-mail's 'Received' header, as it is
+    // replaced with the news server's (more reliable) date.
+    receivedCol: function (viewWrapper) {
+      return viewWrapper.isMailFolder && !viewWrapper.isOutgoingFolder;
+    },
+    // senderCol = From.  You only care in incoming folders.
+    senderCol: function (viewWrapper) {
+      return viewWrapper.isIncomingFolder;
+    },
+    // recipient = To. You only care in outgoing folders.
+    recipientCol: function (viewWrapper) {
+      return viewWrapper.isOutgoingFolder;
+    },
+    // Only show the location column for non-single-folder results
+    locationCol: function(viewWrapper) {
+      return !viewWrapper.isSingleFolder;
+    },
+  },
+
+  /**
+   * If we determine that a column is illegal but was displayed, use this
+   *  mapping to find suggested legal alternatives.  This basically exists
+   *  just to flip-flop between senderCol and recipientCol.
+   */
+  COLUMN_LEGAL_ALTERNATIVES: {
+    // swap between sender and recipient
+    senderCol: ["recipientCol"],
+    recipientCol: ["senderCol"],
+    // if we nuke received, put back date...
+    receivedCol: ["dateCol"],
+  },
+
+  /**
+   * Columns to display whenever we can.  This is currently a bit of a hack to
+   *  always show the location column when relevant.  Arguably, it would be
+   *  better to use this as a default for a folder you've never been in and
+   *  the rest of the time we restore the last column set for that folder from
+   *  properties.
+   */
+  COLUMNS_DISPLAY_WHEN_POSSIBLE: ["locationCol"],
+
+  /**
+   * Update the displayed columns so that:
+   * - Only legal columns (per COLUMN_LEGALITY_TESTERS) are displayed.
+   * - Alternatives to now-illegal columns may be displayed.
+   */
+  updateColumns: function() {
+    // Keep a list of columns we might want to make show up if they are not
+    //  illegal.
+    let legalize = this.COLUMNS_DISPLAY_WHEN_POSSIBLE.concat();
+
+    // figure out who is illegal and make them go away
+    for (let [colId, legalityFunc] in Iterator(this.COLUMN_LEGALITY_TESTERS)) {
+      let column = document.getElementById(colId);
+      // The search window does not have all the columns we know about, bail in
+      //  such cases.
+      if (!column)
+        continue;
+      let legal = legalityFunc(this.view);
+
+      if (!legal) {
+        let isHidden = column.getAttribute("hidden") == "true";
+        // If it wasn't hidden, consider making its alternatives visible in the
+        //  next pass.
+        if (!isHidden && (colId in this.COLUMN_LEGAL_ALTERNATIVES))
+          legalize = legalize.concat(this.COLUMN_LEGAL_ALTERNATIVES[colId]);
+        // but definitely hide the heck out of it right now
+        column.setAttribute("hidden", true);
+        column.setAttribute("ignoreincolumnpicker", true);
+      }
+      else {
+        column.removeAttribute("ignoreincolumnpicker");
+      }
+    }
+    // If we have any columns we should consider making visible because they are
+    //  alternatives to columns that became illegal, uh, do that.
+    for each (let [, colId] in Iterator(legalize)) {
+      let column = document.getElementById(colId);
+      let isLegal = column.getAttribute("ignoreincolumnpicker") != "true";
+      let isHidden = column.getAttribute("hidden") == "true";
+      if (isLegal && isHidden)
+        column.removeAttribute("hidden");
+    }
+  },
+
+  /**
+   * @param aColumnMap an object where the attribute names are column ids and
+   *     the values are a boolean indicating whether the column should be
+   *     visible or not.  If a column is not in the map, it is assumed that it
+   *     should be hidden.
+   */
+  setVisibleColumns: function(aColumnMap) {
+    let cols = document.getElementById("threadCols");
+    let colChildren = cols.children;
+
+    // because the splitter correspondence can be confusing and tricky, let's
+    //  build a list of the nodes ordered by their ordinal
+    let ordinalOrdered = [], iKid;
+    for (iKid = 0; iKid < colChildren.length; iKid++)
+      ordinalOrdered.push(null);
+
+    for (iKid = 0; iKid < colChildren.length; iKid++) {
+      let colChild = colChildren[iKid];
+      let ordinal = colChild.getAttribute("ordinal") - 1;
+      ordinalOrdered[ordinal] = colChild;
+    }
+
+    function twiddleAround(index, makeHidden) {
+      if (index + 1 < ordinalOrdered.length) {
+        let nexty = ordinalOrdered[index+1];
+        if (nexty) {
+          let isHidden = nexty.getAttribute("hidden") == "true";
+          if (isHidden != makeHidden) {
+            nexty.setAttribute("hidden", true);
+            return;
+          }
+        }
+      }
+      if (index - 1 > 0) {
+        let prevy = ordinalOrdered[index-1];
+        if (prevy) {
+          let isHidden = prevy.getAttribute("hidden") == "true";
+          if (isHidden != makeHidden) {
+            prevy.setAttribute("hidden", true);
+            return;
+          }
+        }
+      }
+    }
+
+    for (iKid = 0; iKid < ordinalOrdered.length; iKid++) {
+      let colChild = ordinalOrdered[iKid];
+      if (colChild == null)
+        continue;
+
+      if (colChild.tagName == "treecol") {
+        if (colChild.id in aColumnMap) {
+          // only need to do something if currently hidden
+          if (colChild.getAttribute("hidden") == "true") {
+            colChild.removeAttribute("hidden");
+            twiddleAround(iKid, false);
+          }
+        }
+        else {
+          // only need to do something if currently visible
+          if (colChild.getAttribute("hidden") != "true") {
+            colChild.setAttribute("hidden", true);
+            twiddleAround(iKid, true);
+          }
+        }
+      }
+    }
+  },
+
+  _savedColumnStates: null,
+
+  /**
+   * For now, just save the visible columns into a dictionary for use in a
+   *  subsequent call to setVisibleColumns.  This does not do anything about
+   *  re-arranging columns.
+   */
+  saveColumnStates: function() {
+    // In the actual nsITreeColumn, the index property indicates the column
+    //  number.  This column number is a 0-based index with no gaps; it only
+    //  increments the number each time it sees a column.
+    // However, this is subservient to the 'ordinal' property which
+    //  defines the _apparent content sequence_ provided by GetNextSibling.
+    //  The underlying content ordering is still the same, which is how
+    //  restoreNaturalOrder can reset things to their XUL definition sequence.
+    //  The 'ordinal' stuff works because nsBoxFrame::RelayoutChildAtOrdinal
+    //  messes with the sibling relationship.
+    // Ordinals are 1-based.  restoreNaturalOrder apparently is dumb and does
+    //  not know this, although the ordering is relative so it doesn't actually
+    //  matter.  The annoying splitters do have ordinals, and live between
+    //  tree columns.  The splitters adjacent to a tree column do not need to
+    //  have any 'ordinal' relationship, although it would appear user activity
+    //  tends to move them around in a predictable fashion with oddness involved
+    //  at the edges.
+    // Changes to the ordinal attribute should take immediate effect in terms of
+    //  sibling relationship, but will merely invalidate the columns rather than
+    //  cause a re-computaiton of column relationships every time.
+    // restoreNaturalOrder invalidates the tree when it is done re-ordering; I'm
+    //  not sure that's entirely necessary...
+
+    let visibleMap = {};
+
+    let cols = document.getElementById("threadCols");
+    let colChildren = cols.children;
+    for (let iKid = 0; iKid < colChildren.length; iKid++) {
+      let colChild = colChildren[iKid];
+      if (colChild.getAttribute("hidden") != "true")
+        visibleMap[colChild.id] = true;
+    }
+
+    this._savedColumnStates = visibleMap;
+  },
+
+  /**
+   * Restores the visible columns saved by saveColumnStates.  Some day, in the
+   *  future we might do something about positions and the like.  But we don't
+   *  currently.
+   */
+  restoreColumnStates: function () {
+    if (this._savedColumnStates) {
+      this.setVisibleColumns(this._savedColumnStates);
+      this._savedColumnStates = null;
+    }
+  },
+
+  showFolderUri: function FolderDisplayWidget_showFolderUri(aFolderURI) {
+    return this.show(GetMsgFolderFromUri(aFolderURI));
+  },
+
+  /**
+   * Invoked by showFolder when it turns out the folder is in fact a server.
+   */
+  _showServer: function FolderDisplayWidget__showServer() {
+    // currently nothing to do.  makeActive handles everything for us (because
+    //  what is displayed needs to be re-asserted each time we are activated
+    //  too.)
+  },
+
+  /**
+   * Select a folder for display.
+   *
+   * @param aFolder The nsIMsgDBFolder to display.
+   */
+  show: function FolderDisplayWidget_show(aFolder) {
+    if (aFolder == null) {
+      this._nonViewFolder = null;
+      this.view.close();
+    }
+    else if (aFolder instanceof Components.interfaces.nsIMsgFolder) {
+      if (aFolder.isServer) {
+        this._nonViewFolder = aFolder;
+        this._showServer();
+        this.view.close();
+        // A server is fully loaded immediately, for now.  (When we have the
+        //  account summary, we might want to change this to wait for the page
+        //  load to complete.)
+        this._allMessagesLoaded = true;
+      }
+      else {
+        this._nonViewFolder = null;
+        this.view.open(aFolder);
+      }
+    }
+    // it must be a synthetic view
+    else {
+      this.view.openSynthetic(aFolder);
+    }
+    if (this._active)
+      this.makeActive();
+
+    if (this._tabInfo)
+      document.getElementById('tabmail').setTabTitle(this._tabInfo);
+  },
+
+  /**
+   * Clone an existing view wrapper as the basis for our display.
+   */
+  cloneView: function FolderDisplayWidget_cloneView(aViewWrapper) {
+    this.view = aViewWrapper.clone(this);
+    // generate a view created notification; this will cause us to do the right
+    //  thing in terms of associating the view with the tree and such.
+    this.onCreatedView();
+    if (this._active)
+      this.makeActive();
+  },
+
+  /**
+   * Close resources associated with the currently displayed folder because you
+   *  no longer care about this FolderDisplayWidget.
+   */
+  close: function FolderDisplayWidget_close() {
+    // Mark ourselves as inactive without doing any of the hard work of becoming
+    //  inactive.  This saves us from trying to update things as they go away.
+    this._active = false;
+    // Tell the message display to close itself too.  We do this before we do
+    //  anything else because closing the view could theoretically propagate
+    //  down to the message display and we don't want it doing anything it
+    //  doesn't have to do.
+    this.messageDisplay._close();
+
+    this.view.close();
+    this.messenger.setWindow(null, null);
+    this.messenger = null;
+    this._fakeTreeBox = null;
+  },
+
+  /*   ===============================   */
+  /* ===== IDBViewWrapper Listener ===== */
+  /*   ===============================   */
+
+  /**
+   * @return true if the mail view picker is visible.  This affects whether the
+   *     DBViewWrapper will actually use the persisted mail view or not.
+   */
+  get shouldUseMailViews() {
+    return ViewPickerBinding.isVisible;
+  },
+
+  /**
+   * Let the viewWrapper know if we should defer message display because we
+   *  want the user to connect to the server first so password authentication
+   *  can occur.
+   *
+   * @return true if the folder should be shown immediately, false if we should
+   *     wait for updateFolder to complete.
+   */
+  get shouldDeferMessageDisplayUntilAfterServerConnect() {
+    let passwordPromptRequired = false;
+
+    if (gPrefBranch.getBoolPref("mail.password_protect_local_cache"))
+      passwordPromptRequired =
+        this.view.displayedFolder.server.passwordPromptRequired;
+
+    return passwordPromptRequired;
+  },
+
+  /**
+   * Let the viewWrapper know if it should mark the messages read when leaving
+   *  the provided folder.
+   *
+   * @return true if the preference is set for the folder's server type.
+   */
+  shouldMarkMessagesReadOnLeavingFolder:
+    function FolderDisplayWidget_crazyMarkOnReadChecker (aMsgFolder) {
+      return gPrefBranch.getBoolPref("mailnews.mark_message_read." +
+                                     aMsgFolder.server.type);
+  },
+
+  /**
+   * The view wrapper tells us when it starts loading a folder, and we set the
+   *  cursor busy.  Setting the cursor busy on a per-tab basis is us being
+   *  nice to the future. Loading a folder is a blocking operation that is going
+   *  to make us unresponsive and accordingly make it very hard for the user to
+   *  change tabs.
+   */
+  onFolderLoading: function(aFolderLoading) {
+    if (this._tabInfo)
+      document.getElementById("tabmail").setTabBusy(this._tabInfo,
+                                                    aFolderLoading);
+  },
+
+  /**
+   * 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)
+      document.getElementById("tabmail").setTabThinking(
+        this._tabInfo,
+        aIsSearching && gSearchBundle.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() {
+    // All of our messages are not displayed if the view was just created.  We
+    //  will get an onAllMessagesLoaded nearly immediately if this is a local
+    //  folder where view creation is synonymous with having all messages.
+    this._allMessagesLoaded = false;
+    this.messageDisplay.onCreatedView();
+    this._notifyWhenActive(this._activeCreatedView);
+  },
+  _activeCreatedView: function() {
+    gDBView = this.view.dbView;
+
+    // A change in view may result in changes to sorts, the view menu, etc.
+    // Do this before we 'reroot' the dbview.
+    this._updateThreadDisplay();
+
+    // this creates a new selection object for the view.
+    if (this.treeBox)
+      this.treeBox.view = this.view.dbView;
+
+    let ObserverService =
+      Components.classes["@mozilla.org/observer-service;1"]
+                .getService(Components.interfaces.nsIObserverService);
+    // The data payload used to be viewType + ":" + viewFlags.  We no longer
+    //  do this because we already have the implied contract that gDBView is
+    //  valid at the time we generate the notification.  In such a case, you
+    //  can easily get that information from the gDBView.  (The documentation
+    //  on creating a custom column assumes gDBView.)
+    ObserverService.notifyObservers(this.displayedFolder,
+                                    "MsgCreateDBView", "");
+  },
+
+  /**
+   * If our view is being destroyed and it is coming back, we want to save the
+   *  current selection so we can restore it when the view comes back.
+   */
+  onDestroyingView: function FolderDisplayWidget_onDestroyingView(
+      aFolderIsComingBack) {
+    // try and persist the selection's content if we can
+    if (this._active) {
+      if (aFolderIsComingBack)
+        this._saveSelection();
+      else
+        this._clearSavedSelection();
+      gDBView = null;
+    }
+
+    // if we have no view, no messages could be loaded.
+    this._allMessagesLoaded = false;
+
+    // but the actual tree view selection (based on view indicies) is a goner no
+    //  matter what, make everyone forget.
+    this.view.dbView.selection = null;
+    this._savedFirstVisibleRow = null;
+    this._nextViewIndexAfterDelete = null;
+    // although the move may still be active, its relation to the view is moot.
+    this._massMoveActive = false;
+
+    // Anything pending needs to get cleared out; the new view and its related
+    //  events will re-schedule anything required or simply run it when it
+    //  has its initial call to makeActive compelled.
+    this._notificationsPendingActivation = [];
+
+    // and the message display needs to forget
+    this.messageDisplay.onDestroyingView(aFolderIsComingBack);
+  },
+  /**
+   * We are entering the folder for display, set the header cache size.
+   */
+  onDisplayingFolder: function FolderDisplayWidget_onDisplayingFolder() {
+    let msgDatabase = this.view.displayedFolder.msgDatabase;
+    if (msgDatabase) {
+      msgDatabase.resetHdrCacheSize(this.PERF_HEADER_CACHE_SIZE);
+    }
+
+    // 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();
+  },
+
+  /**
+   * Notification from DBViewWrapper that it is closing the folder.  This can
+   *  happen for reasons other than our own 'close' method closing the view.
+   *  For example, user deletion of the folder or underlying folder closes it.
+   */
+  onLeavingFolder: function FolderDisplayWidget_onLeavingFolder() {
+    // Keep the msgWindow's openFolder up-to-date; it powers nsMessenger's
+    //  concept of history so that it can bring you back to the actual folder
+    //  you were looking at, rather than just the underlying folder.
+    if (this._active)
+      msgWindow.openFolder = null;
+  },
+
+  /**
+   * Indictes whether we are done loading the messages that should be in this
+   *  folder.  This is being surfaced for testing purposes, but could be useful
+   *  to other code as well.  But don't poll this property; ask for an event
+   *  that you can hook.
+   */
+  get allMessagesLoaded FolderDisplayWidget_get_allMessagesLoaded() {
+    return this._allMessagesLoaded;
+  },
+
+  /**
+   * Things to do once all the messages that should show up in a folder have
+   *  shown up.  For a real folder, this happens when the folder is entered.
+   *  For a virtual folder, this happens when the search completes.
+   *
+   * What we do:
+   * - Any scrolling required!
+   */
+  onAllMessagesLoaded: function() {
+    this._allMessagesLoaded = true;
+    this._notifyWhenActive(this._activeAllMessagesLoaded);
+  },
+  _activeAllMessagesLoaded: function() {
+    // - restore selection
+    // Attempt to restore the selection (if we saved it because the view was
+    //  being destroyed or otherwise manipulated in a fashion that the normal
+    //  nsTreeSelection would be unable to handle.)
+    if (this._restoreSelection()) {
+      this.ensureRowIsVisible(this.view.dbView.viewIndexForFirstSelectedMsg);
+      return;
+    }
+
+    // - pending navigation from pushNavigation (probably spacebar triggered)
+    if (this._pendingNavigation) {
+      // Move it to a local and clear the state in case something bad happens.
+      //  (We don't want to swallow the exception.)
+      let pendingNavigation = this._pendingNavigation;
+      this._pendingNavigation = null;
+      this.navigate.apply(this, pendingNavigation);
+      return;
+    }
+
+    // - new messages
+    // if configured to scroll to new messages, try that
+    if (gPrefBranch.getBoolPref("mailnews.scroll_to_new_message") &&
+        this.navigate(nsMsgNavigationType.firstNew, /* select */ false))
+      return;
+
+    // - last selected message
+    // if configured to load the last selected message (this is currently more
+    //  persistent than our saveSelection/restoreSelection stuff), and the view
+    //  is backed by a single underlying folder (the only way having just a
+    //  message key works out), try that
+    if (gPrefBranch.getBoolPref("mailnews.remember_selected_message") &&
+        this.view.isSingleFolder) {
+      // use the displayed folder; nsMsgDBView goes to the effort to save the
+      //  state to the viewFolder, so this is the correct course of action.
+      let lastLoadedMessageKey = this.view.displayedFolder.lastMessageLoaded;
+      if (lastLoadedMessageKey != nsMsgKey_None) {
+        this.view.dbView.selectMsgByKey(lastLoadedMessageKey);
+        // The message key may not be present in the view for a variety of
+        //  reasons.  Beyond message deletion, it simply may not match the
+        //  active mail view or quick search, for example.
+        if (this.view.dbView.numSelected) {
+          this.ensureRowIsVisible(
+            this.view.dbView.viewIndexForFirstSelectedMsg);
+          return;
+        }
+      }
+    }
+
+    // - towards the newest messages, but don't select
+    if (this.view.isSortedAscending && this.view.sortImpliesTemporalOrdering &&
+      this.navigate(nsMsgNavigationType.lastMessage, /* select */ false))
+      return;
+
+    // - to the top, the coliseum
+    this.ensureRowIsVisible(0);
+  },
+
+  /**
+   * The DBViewWrapper tells us when someone (possibly the wrapper itself)
+   *  changes the active mail view so that we can kick the UI to update.
+   */
+  onMailViewChanged: function FolderDisplayWidget_onMailViewChanged() {
+    // only do this if we're currently active.  no need to queue it because we
+    //  always update the mail view whenever we are made active.
+    if (this.active) {
+      let event = document.createEvent("datacontainerevents");
+      // you cannot cancel a view change!
+      event.initEvent("MailViewChanged", false, false);
+      //event.setData("folderDisplay", this);
+      window.dispatchEvent(event);
+    }
+  },
+
+  /**
+   * Just the sort or threading was changed, without changing other things.  We
+   *  will not get this notification if the view was re-created, for example.
+   */
+  onSortChanged: function FolderDisplayWidget_onSortChanged() {
+    if (this.active)
+      UpdateSortIndicators(this.view.primarySortType,
+                           this.view.primarySortOrder);
+  },
+
+  /**
+   * Messages (that may have been displayed) have been removed; this may impact
+   *  our message selection.  If we saw this coming, then
+   *  this._nextViewIndexAfterDelete should know what view index we should
+   *  select next.  If we didn't see this coming, the cause is likely an
+   *  explicit deletion in another tab/window.
+   * Because the nsMsgDBView is on top of things, it will already have called
+   *  summarizeSelection as a result of the changes to the message display.
+   *  So our job here is really just to try and potentially improve on the
+   *  default selection logic.
+   */
+  onMessagesRemoved: function FolderDisplayWidget_onMessagesRemoved() {
+    // - we saw this coming
+    let rowCount = this.view.dbView.rowCount;
+    if (!this._massMoveActive && (this._nextViewIndexAfterDelete != null)) {
+      // adjust the index if it is after the last row...
+      // (this can happen if the "mail.delete_matches_sort_order" pref is not
+      //  set and the message is the last message in the view.)
+      if (this._nextViewIndexAfterDelete >= rowCount)
+        this._nextViewIndexAfterDelete = rowCount - 1;
+      // just select the index and get on with our lives
+      this.selectViewIndex(this._nextViewIndexAfterDelete);
+      this._nextViewIndexAfterDelete = null;
+      return;
+    }
+
+    // - surprise!
+    // A deletion happened to our folder.
+    let treeSelection = this.treeSelection;
+    // we can't fix the selection if we have no selection
+    if (!treeSelection)
+      return;
+
+    // For reasons unknown (but theoretically knowable), sometimes the selection
+    //  object will be invalid.  At least, I've reliably seen a selection of
+    //  [0, 0] with 0 rows.  If that happens, we need to fix up the selection
+    //  here.
+    if (rowCount == 0 && treeSelection.count)
+      // nsTreeSelection does't generate an event if we use clearRange, so use
+      //  that to avoid spurious events, given that we are going to definitely
+      //  trigger a change notification below.
+      treeSelection.clearRange(0, 0);
+
+    // Check if we now no longer have a selection, but we had exactly one
+    //  message selected previously.  If we did, then try and do some
+    //  'persistence of having a thing selected'.
+    if (treeSelection.count == 0 &&
+        this._mostRecentSelectionCounts.length > 1 &&
+        this._mostRecentSelectionCounts[1] == 1 &&
+        this._mostRecentCurrentIndices[1] != -1) {
+      let targetIndex = this._mostRecentCurrentIndices[1];
+      if (targetIndex >= rowCount)
+        targetIndex = rowCount - 1;
+      this.selectViewIndex(targetIndex);
+      return;
+    }
+
+    // Otherwise, just tell the view that things have changed so it can update
+    //  itself to the new state of things.
+    // tell the view that things have changed so it can update itself suitably.
+    if (this.view.dbView)
+      this.view.dbView.selectionChanged();
+  },
+
+  /**
+   * Messages were not actually removed, but we were expecting that they would
+   *  be.  Clean-up what onMessagesRemoved would have cleaned up, namely the
+   *  next view index to select.
+   */
+  onMessageRemovalFailed:
+      function FolderDisplayWidget_onMessageRemovalFailed() {
+    this._nextViewIndexAfterDelete = null;
+  },
+
+  /**
+   * Update the status bar to reflect our exciting message counts.
+   */
+  onMessageCountsChanged: function FolderDisplayWidget_onMessageCountsChaned() {
+    if (this.active)
+      UpdateStatusMessageCounts(this.displayedFolder);
+  },
+
+  /* ===== End IDBViewWrapperListener ===== */
+
+  /*   ==================================   */
+  /* ===== nsIMsgDBViewCommandUpdater ===== */
+  /*   ==================================   */
+
+  /**
+   * This gets called when the selection changes AND !suppressCommandUpdating
+   *  AND (we're not removing a row OR we are now out of rows).
+   * In response, we update the toolbar.
+   */
+  updateCommandStatus: function FolderDisplayWidget_updateCommandStatus() {
+    UpdateMailToolbar("FolderDisplayWidget command updater notification");
+  },
+
+  /**
+   * This gets called by nsMsgDBView::UpdateDisplayMessage following a call
+   *  to nsIMessenger.OpenURL to kick off message display OR (UDM gets called)
+   *  by nsMsgDBView::SelectionChanged in lieu of loading the message because
+   *  mSupressMsgDisplay.
+   * In other words, we get notified immediately after the process of displaying
+   *  a message triggered by the nsMsgDBView happens.  We get some arguments
+   *  that are display optimizations for historical reasons (as usual).
+   *
+   * Things this makes us want to do:
+   * - Set the tab title, perhaps.  (If we are a message display.)
+   * - Update message counts, because things might have changed, why not.
+   * - Update some toolbar buttons, why not.
+   *
+   * @param aFolder The display/view folder, as opposed to the backing folder.
+   * @param aSubject The subject with "Re: " if it's got one, which makes it
+   *     notably different from just directly accessing the message header's
+   *     subject.
+   * @param aKeywords The keywords, which roughly translates to message tags.
+   */
+  displayMessageChanged: function FolderDisplayWidget_displayMessageChanged(
+      aFolder, aSubject, aKeywords) {
+    UpdateMailToolbar("FolderDisplayWidget displayed message changed");
+    let viewIndex = this.view.dbView.currentlyDisplayedMessage;
+    let msgHdr = (viewIndex != nsMsgViewIndex_None) ?
+                   this.view.dbView.getMsgHdrAt(viewIndex) : null;
+    this.messageDisplay.onDisplayingMessage(msgHdr);
+
+    // Although deletes should now be so fast that the user has no time to do
+    //  anything, treat the user explicitly choosing to display a different
+    //  message as invalidating the choice we automatically made for them when
+    //  they initiated the message delete / move. (bug 243532)
+    // Note: legacy code used to check whether the message being displayed was
+    //  the one being deleted, so it didn't erroneously clear the next message
+    //  to display (bug 183394).  This is not a problem for us because we hook
+    //  our notification when the message load is initiated, rather than when
+    //  the message completes loading.
+    this._nextViewIndexAfterDelete = null;
+  },
+
+  /**
+   * This gets called as a hint that the currently selected message is junk and
+   *  said junked message is going to be moved out of the current folder.  The
+   *  legacy behaviour is to retrieve the msgToSelectAfterDelete attribute off
+   *  the db view, stashing it for benefit of the code that gets called when a
+   *  message move/deletion is completed so that we can trigger its display.
+   */
+  updateNextMessageAfterDelete:
+      function FolderDisplayWidget_updateNextMessageAfterDelete() {
+    this.hintAboutToDeleteMessages();
+  },
+
+  /**
+   * The most recent currentIndexes on the selection (from the last time
+   *  summarizeSelection got called).  We use this in onMessagesRemoved if
+   *  we get an unexpected notification.
+   * We keep a maximum of 2 entries in this list.
+   */
+  _mostRecentCurrentIndices: undefined, // initialized in constructor
+  /**
+   * The most recent counts on the selection (from the last time
+   *  summarizeSelection got called).  We use this in onMessagesRemoved if
+   *  we get an unexpected notification.
+   * We keep a maximum of 2 entries in this list.
+   */
+  _mostRecentSelectionCounts: undefined, // initialized in constructor
+
+  /**
+   * Always called by the db view when the selection changes in
+   *  SelectionChanged.  This event will come after the notification to
+   *  displayMessageChanged (if one happens), and before the notification to
+   *  updateCommandStatus (if one happens).
+   */
+  summarizeSelection: function FolderDisplayWidget_summarizeSelection() {
+    // save the current index off in case the selection gets deleted out from
+    //  under us and we want to have persistence of actually-having-something
+    //  selected.
+    let treeSelection = this.treeSelection;
+    if (treeSelection) {
+      this._mostRecentCurrentIndices.unshift(treeSelection.currentIndex);
+      this._mostRecentCurrentIndices.splice(2);
+      this._mostRecentSelectionCounts.unshift(treeSelection.count);
+      this._mostRecentSelectionCounts.splice(2);
+    }
+    return this.messageDisplay.onSelectedMessagesChanged();
+  },
+
+  /* ===== End nsIMsgDBViewCommandUpdater ===== */
+
+  /* ===== Hints from the command infrastructure ===== */
+
+  /**
+   * doCommand helps us out by telling us when it is telling the view to delete
+   *  some messages.  Ideally it should go through us / the DB View Wrapper to
+   *  kick off the delete in the first place, but that's a thread I don't want
+   *  to pull on right now.
+   * We use this hint to figure out the next message to display once the
+   *  deletion completes.  We do this before the deletion happens because the
+   *  selection is probably going away (except in the IMAP delete model), and it
+   *  might be too late to figure this out after the deletion happens.
+   * Our automated complement (that calls us) is updateNextMessageAfterDelete.
+   */
+  hintAboutToDeleteMessages:
+      function FolderDisplayWidget_hintAboutToDeleteMessages() {
+    // If there is a right click going on, then the possibilities are:
+    // 1) The user right-clicked in the selection.  In this case, the selection
+    //    is maintained.  This holds true for one or multiple messages.
+    // 2) The user right-clicked outside the selection.  In this case, the
+    //    selection, but not the current index, reflects the single message
+    //    the user right-clicked on.
+    // We want to treat case #1 as if a right-click was not involved and we
+    //  want to ignore case #2 by bailing because our existing selection (or
+    //  lack thereof) we want maintained.
+//    if (gRightMouseButtonDown && gRightMouseButtonChangedSelection)
+//      return;
+
+    // save the value, even if it is nsMsgViewIndex_None.
+    this._nextViewIndexAfterDelete = this.view.dbView.msgToSelectAfterDelete;
+  },
+
+  /**
+   * The archive code tells us when it is starting to archive messages.  This
+   *  is different from hinting about deletion because it will also tell us
+   *  when it has completed its mass move.
+   * The UI goal is that we do not immediately jump beyond the selected messages
+   *  to the next message until all of the selected messages have been
+   *  processed (moved).  Ideally we would also do this when deleting messages
+   *  from a multiple-folder backed message view, but we don't know when the
+   *  last job completes in that case (whereas in this case we do because of the
+   *  call to hintMassMoveCompleted.)
+   */
+  hintMassMoveStarting:
+      function FolderDisplayWidget_hintMassMoveStarting() {
+    this.hintAboutToDeleteMessages();
+    this._massMoveActive = true;
+  },
+
+  /**
+   * The archival has completed, we can finally let onMessagseRemoved run to
+   *  completion.
+   */
+  hintMassMoveCompleted:
+      function FolderDisplayWidget_hintMassMoveCompleted() {
+    this._massMoveActive = false;
+    this.onMessagesRemoved();
+  },
+
+  /**
+   * When a right-click on the thread pane is going to alter our selection, we
+   *  get this notification (currently from |ChangeSelectionWithoutContentLoad|
+   *  in msgMail3PaneWindow.js), which lets us save our state.
+   * This ends one of two ways: we get made inactive because a new tab popped up
+   *  or we get a call to |hintRightClickSelectionPerturbationDone|.
+   *
+   * Ideally, we could just save off our current nsITreeSelection and restore it
+   *  when this is all over.  This assumption would rely on the underlying view
+   *  not having any changes to its rows before we restore the selection.  I am
+   *  not confident we can rule out background processes making changes, plus
+   *  the right-click itself may mutate the view (although we could try and get
+   *  it to restore the selection before it gets to the mutation part).  Our
+   *  only way to resolve this would be to create a 'tee' like fake selection
+   *  that would proxy view change notifications to both sets of selections.
+   *  That is hard.
+   * So we just use the existing _saveSelection/_restoreSelection mechanism
+   *  which is potentially very costly.
+   */
+  hintRightClickPerturbingSelection:
+      function FolderDisplayWidget_hintRightClickPerturbingSelect() {
+    this._saveSelection();
+  },
+
+  /**
+   * When a right-click on the thread pane altered our selection (which we
+   *  should have received a call to |hintRightClickPerturbingSelection| for),
+   *  we should receive this notification from
+   *  |RestoreSelectionWithoutContentLoad| when it wants to put things back.
+   */
+  hintRightClickSelectionPerturbationDone:
+      function FolderDisplayWidget_hintRightClickSelectionPerturbationDone() {
+    this._restoreSelection();
+  },
+
+  /* ===== End hints from the command infrastructure ==== */
+
+  _updateThreadDisplay: function FolderDisplayWidget__updateThreadDisplay() {
+    if (this.active) {
+      if (this.view.dbView) {
+        this.updateColumns();
+        UpdateSortIndicators(this.view.dbView.sortType,
+                             this.view.dbView.sortOrder);
+        SetNewsFolderColumns();
+      }
+    }
+  },
+
+  /**
+   * Update the UI display apart from the thread tree because the folder being
+   *  displayed has changed.  This can be the result of changing the folder in
+   *  this FolderDisplayWidget, or because this FolderDisplayWidget is being
+   *  made active.  _updateThreadDisplay handles the parts of the thread tree
+   *  that need updating.
+   */
+  _updateContextDisplay: function FolderDisplayWidget__updateContextDisplay() {
+    if (this.active) {
+      UpdateMailToolbar("FolderDisplayWidget updating context");
+      UpdateStatusQuota(this.displayedFolder);
+      UpdateStatusMessageCounts(this.displayedFolder);
+
+      // - mail view combo-box.
+      this.onMailViewChanged();
+    }
+  },
+
+  /**
+   * Run the provided notification function right now if we are 'active' (the
+   *  currently displayed tab), otherwise queue it to be run when we become
+   *  active.  We do this because our tabbing model uses multiplexed (reused)
+   *  widgets, and extensions likewise depend on these global/singleton things.
+   * If the requested notification function is already queued, it will not be
+   *  added a second time, and the original call ordering will be maintained.
+   *  If a new call ordering is required, the list of notifications should
+   *  probably be reset by the 'big bang' event (new view creation?).
+   */
+  _notifyWhenActive: function (aNotificationFunc) {
+    if (this._active) {
+      aNotificationFunc.call(this);
+    }
+    else {
+      if (this._notificationsPendingActivation.indexOf(aNotificationFunc) == -1)
+        this._notificationsPendingActivation.push(aNotificationFunc);
+    }
+  },
+
+  /**
+   * Some notifications cannot run while the FolderDisplayWidget is inactive
+   *  (presumbly because it is in a background tab).  We accumulate those in
+   *  _notificationsPendingActivation and then this method runs them when we
+   *  become active again.
+   */
+  _runNotificationsPendingActivation:
+      function FolderDisplayWidget__runNotificationsPendingActivation() {
+    if (!this._notificationsPendingActivation.length)
+      return;
+
+    let pendingNotifications = this._notificationsPendingActivation;
+    this._notificationsPendingActivation = [];
+    for each (let [, notif] in Iterator(pendingNotifications)) {
+      notif.call(this);
+    }
+  },
+
+  get active() {
+    return this._active;
+  },
+
+  /**
+   * Make this FolderDisplayWidget the 'active' widget by updating globals and
+   *  linking us up to the UI widgets.  This is intended for use by the tabbing
+   *  logic.
+   */
+  makeActive: function FolderDisplayWidget_makeActive(aWasInactive) {
+    let wasInactive = !this._active;
+
+    // -- globals
+    // update per-tab globals that we own
+    gFolderDisplay = this;
+    gMessageDisplay = this.messageDisplay;
+    gDBView = this.view.dbView;
+    messenger = this.messenger;
+
+    // update singleton globals' state
+    msgWindow.openFolder = this.view.displayedFolder;
+
+    this._active = true;
+    this._runNotificationsPendingActivation();
+
+    // -- UI
+
+    // thread pane if we have a db view
+    if (this.view.dbView) {
+      // Make sure said thread pane is visible.  If we do this after we re-root
+      //  the tree, the thread pane may not actually replace the account central
+      //  pane.  Concerning...
+      this._showThreadPane();
+
+      // some things only need to happen if we are transitioning from inactive
+      //  to active
+      if (wasInactive) {
+        // Setting the 'view' attribute on treeBox results in the following
+        //  effective calls, noting that in makeInactive we made sure to null
+        //  out its view so that it won't try and clean up any views or their
+        //  selections.  (The actual actions happen in nsTreeBodyFrame::SetView)
+        // - this.view.dbView.selection.tree = this.treeBox
+        // - this.view.dbView.setTree(this.treeBox)
+        // - this.treeBox.view = this.view.dbView (in nsTreeBodyObject::SetView)
+        if (this.treeBox) {
+          this.treeBox.view = this.view.dbView;
+          if (this._savedFirstVisibleRow != null)
+            this.treeBox.scrollToRow(this._savedFirstVisibleRow);
+
+          this.restoreColumnStates();
+        }
+
+        // restore the quick search widget
+        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();
+          }
+        }
+      }
+
+      // the tab mode knows whether we are folder or message display, which
+      //  impacts the legal modes
+      if (this._tabInfo)
+        mailTabType._setPaneStates(this._tabInfo.mode.legalPanes,
+          {folder: !this._tabInfo.folderPaneCollapsed,
+           message: !this._tabInfo.messagePaneCollapsed});
+
+      // update the columns and such that live inside the thread pane
+      this._updateThreadDisplay();
+
+      this.messageDisplay.makeActive();
+    }
+    // account central stuff when we don't have a dbview
+    else {
+      this._showAccountCentral();
+      if (this._tabInfo)
+        mailTabType._setPaneStates(this._tabInfo.mode.legalPanes,
+                                   {folder: !this._tabInfo.folderPaneCollapsed,
+                                    message: false});
+    }
+
+    this._updateContextDisplay();
+  },
+
+  /**
+   * Cause the displayDeck to display the thread pane.
+   */
+  _showThreadPane: function FolderDisplayWidget__showThreadPane() {
+    document.getElementById("displayDeck").selectedPanel =
+      document.getElementById("threadPaneBox");
+  },
+
+  /**
+   * Cause the displayDeck to display the (preference configurable) account
+   *  central page.
+   */
+  _showAccountCentral: function FolderDisplayWidget__showAccountCentral() {
+    var accountBox = document.getElementById("accountCentralBox");
+    document.getElementById("displayDeck").selectedPanel = accountBox;
+    var prefName = "mailnews.account_central_page.url";
+    // oh yeah, 'pref' is a global all right.
+    var acctCentralPage =
+      pref.getComplexValue(prefName,
+                           Components.interfaces.nsIPrefLocalizedString).data;
+    window.frames["accountCentralPane"].location.href = acctCentralPage;
+  },
+
+  /**
+   * Call this when the tab using us is being hidden.
+   */
+  makeInactive: function FolderDisplayWidget_makeInactive() {
+    this._active = false;
+    // save the folder pane's state always
+    this.folderPaneCollapsed =
+      document.getElementById("folderPaneBox").collapsed;
+
+    if (this.view.dbView) {
+      if (this.treeBox)
+        this._savedFirstVisibleRow = this.treeBox.getFirstVisibleRow();
+
+      // save column states
+      this.saveColumnStates();
+
+      // save the message pane's state only when it is potentially visible
+      this.messagePaneCollapsed =
+        document.getElementById("messagepanebox").collapsed;
+
+      // save the actual quick-search query text
+      let searchInput = document.getElementById("searchInput");
+      if (searchInput) {
+        this._savedQuickSearch = {
+          text: searchInput.showingSearchCriteria ? null : searchInput.value,
+          searchMode: searchInput.searchMode
+        };
+      }
+
+      // save off the tree selection object.  the nsTreeBodyFrame will make the
+      //  view forget about it when our view is removed, so it's up to us to
+      //  save it.
+      let treeViewSelection = this.view.dbView.selection;
+      // make the tree forget about the view right now so we can tell the db
+      //  view about its selection object so it can try and keep it up-to-date
+      //  even while hidden in the background
+      if (this.treeBox)
+        this.treeBox.view = null;
+      // (and tell the db view about its selection again...)
+      this.view.dbView.selection = treeViewSelection;
+
+      // hook the dbview up to the fake tree box
+      this._fakeTreeBox.view = this.view.dbView;
+      this.view.dbView.setTree(this._fakeTreeBox);
+      treeViewSelection.tree = this._fakeTreeBox;
+    }
+
+    this.messageDisplay.makeInactive();
+  },
+
+  /**
+   * @return true if there is a db view and the command is enabled on the view.
+   *  This function hides some of the XPCOM-odditities of the getCommandStatus
+   *  call.
+   */
+  getCommandStatus: function FolderDisplayWidget_getCommandStatus(
+      aCommandType, aEnabledObj, aCheckStatusObj) {
+    // no view means not enabled
+    if (!this.view.dbView)
+      return false;
+    let enabledObj = {}, checkStatusObj = {};
+    this.view.dbView.getCommandStatus(aCommandType, enabledObj, checkStatusObj);
+    return enabledObj.value;
+  },
+
+  /**
+   * Make code cleaner by allowing peoples to call doCommand on us rather than
+   *  having to do folderDisplayWidget.view.dbView.doCommand.
+   *
+   * @param aCommandName The command name to invoke.
+   */
+  doCommand: function FolderDisplayWidget_doCommand(aCommandName) {
+    return this.view.dbView && this.view.dbView.doCommand(aCommandName);
+  },
+
+  /**
+   * Make code cleaner by allowing peoples to call doCommandWithFolder on us
+   *  rather than having to do:
+   *  folderDisplayWidget.view.dbView.doCommandWithFolder.
+   *
+   * @param aCommandName The command name to invoke.
+   * @param aFolder The folder context for the command.
+   */
+  doCommandWithFolder: function FolderDisplayWidget_doCommandWithFolder(
+      aCommandName, aFolder) {
+    return this.view.dbView &&
+           this.view.dbView.doCommandWithFolder(aCommandName, aFolder);
+  },
+
+
+
+  /**
+   * Navigate using nsMsgNavigationType rules and ensuring the resulting row is
+   *  visible.  This is trickier than it used to be because we now support
+   *  treating collapsed threads as the set of all the messages in the collapsed
+   *  thread rather than just the root message in that thread.
+   *
+   * @param aNavType {nsMsgNavigationType} navigation command.
+   * @param aSelect {Boolean} should we select the message if we find one?
+   *     Defaults to true if omitted.
+   *
+   * @return true if the navigation constraint matched anything, false if not.
+   *     We will have navigated if true, we will have done nothing if false.
+   */
+  navigate: function FolderDisplayWidget_navigate(aNavType, aSelect) {
+    if (aSelect === undefined)
+      aSelect = true;
+    let resultKeyObj = {}, resultIndexObj = {}, threadIndexObj = {};
+
+    let summarizeSelection =
+      gPrefBranch.getBoolPref("mail.operate_on_msgs_in_collapsed_threads");
+
+    let treeSelection = this.treeSelection; // potentially magic getter
+    let currentIndex = treeSelection ? treeSelection.currentIndex : 0;
+
+    let viewIndex;
+    // if we're doing next unread, and a collapsed thread is selected, and
+    // the top level message is unread, just set the result manually to
+    // the top level message, without using viewNavigate.
+    if (summarizeSelection &&
+        aNavType == nsMsgNavigationType.nextUnreadMessage &&
+        currentIndex != -1 &&
+        this.view.isCollapsedThreadAtIndex(currentIndex) &&
+        !(this.view.dbView.getFlagsAt(currentIndex) &
+          nsMsgMessageFlags.Read)) {
+      viewIndex = currentIndex;
+    }
+    else {
+      // always 'wrap' because the start index is relative to the selection.
+      // (keep in mind that many forms of navigation do not care about the
+      //  starting position or 'wrap' at all; for example, firstNew just finds
+      //  the first new message.)
+      // allegedly this does tree-expansion for us.
+      this.view.dbView.viewNavigate(aNavType, resultKeyObj, resultIndexObj,
+                                    threadIndexObj, true);
+      viewIndex = resultIndexObj.value;
+    }
+
+    if (viewIndex == nsMsgViewIndex_None)
+      return false;
+
+    // - Expand if required.
+    // (The nsMsgDBView isn't really aware of the varying semantics of
+    //  collapsed threads, so viewNavigate might tell us about the root message
+    //  and leave it collapsed, not realizing that it needs to be expanded.)
+    if (summarizeSelection &&
+        this.view.isCollapsedThreadAtIndex(viewIndex))
+      this.view.dbView.toggleOpenState(viewIndex);
+
+    if (aSelect)
+      this.selectViewIndex(viewIndex);
+    else
+      this.ensureRowIsVisible(viewIndex);
+    return true;
+  },
+
+  /**
+   * Push a call to |navigate| to be what we do once we successfully open the
+   *  next folder.  This is intended to be used by cross-folder navigation
+   *  code.  It should call this method before triggering the folder change.
+   */
+  pushNavigation: function FolderDisplayWidget_navigate(aNavType, aSelect) {
+    this._pendingNavigation = [aNavType, aSelect];
+  },
+
+  /**
+   * @return true if we are able to navigate using the given navigation type at
+   *  this time.
+   */
+  navigateStatus: function FolderDisplayWidget_navigateStatus(aNavType) {
+    if (!this.view.dbView)
+      return false;
+    return this.view.dbView.navigateStatus(aNavType);
+  },
+
+  /**
+   * @returns the message header for the first selected message, or null if
+   *  there is no selected message.
+   *
+   * If the user has right-clicked on a message, this method will return that
+   *  message and not the 'current index' (the dude with the dotted selection
+   *  rectangle around him.)  If you instead always want the currently
+   *  displayed message (which is not impacted by right-clicking), then you
+   *  would want to access the displayedMessage property on the
+   *  MessageDisplayWidget.  You can get to that via the messageDisplay
+   *  attribute on this object or (potentially) via the gMessageDisplay object.
+   */
+  get selectedMessage FolderDisplayWidget_get_selectedMessage() {
+    // there are inconsistencies in hdrForFirstSelectedMessage between
+    //  nsMsgDBView and nsMsgSearchDBView in whether they use currentIndex,
+    //  do it ourselves.  (nsMsgDBView does not use currentIndex, search does.)
+    let treeSelection = this.treeSelection;
+    if (!treeSelection || !treeSelection.count)
+      return null;
+    let minObj = {}, maxObj = {};
+    treeSelection.getRangeAt(0, minObj, maxObj);
+    return this.view.dbView.getMsgHdrAt(minObj.value);
+  },
+
+  /**
+   * @return true if there is a selected message and it's an RSS feed message.
+   */
+  get selectedMessageIsFeed FolderDisplayWidget_get_selectedMessageIsFeed() {
+    let message = this.selectedMessage;
+    return Boolean(message && message.folder &&
+                   message.folder.server.type == 'rss');
+  },
+
+  /**
+   * @return true if there is a selected message and it's an IMAP message.
+   */
+  get selectedMessageIsImap FolderDisplayWidget_get_selectedMessageIsImap() {
+    let message = this.selectedMessage;
+    return Boolean(message && message.folder &&
+                   message.folder.flags & nsMsgFolderFlags.ImapBox);
+  },
+
+  /**
+   * @return true if there is a selected message and it's a news message.  It
+   *  would be great if messages knew this about themselves, but they don't.
+   */
+  get selectedMessageIsNews FolderDisplayWidget_get_selectedMessageIsNews() {
+    let message = this.selectedMessage;
+    return Boolean(message && message.folder &&
+                   (message.folder.flags & nsMsgFolderFlags.Newsgroup));
+  },
+
+  /**
+   * @return true if there is a selected message and it's an external message,
+   *  meaning it is loaded from an .eml file on disk or is an rfc822 attachment
+   *  on a message.
+   */
+  get selectedMessageIsExternal
+      FolderDisplayWidget_get_selectedMessageIsExternal() {
+    let message = this.selectedMessage;
+    // Dummy messages currently lack a folder.  This is not a great heuristic.
+    // I have annotated msgHdrViewOverlay.js which provides the dummy header to
+    //  express this implementation dependency.
+    // (Currently, since external mails can only be opened in standalone windows
+    //  which subclass us, we could always return false, and have the subclass
+    //  return true using its own heuristics.  But since we are moving to a tab
+    //  model more heavily, at some point the 3-pane will need this.)
+    return Boolean(message && !message.folder);
+  },
+
+  /**
+   * @return the number of selected messages.  If the
+   *  "mail.operate_on_msgs_in_collapsed_threads" preference is enabled, then
+   *  any collapsed thread roots that are selected will also conceptually have
+   *  all of the messages in that thread selected.
+   */
+  get selectedCount FolderDisplayWidget_get_selectedCount() {
+    if (!this.view.dbView)
+      return 0;
+    return this.view.dbView.numSelected;
+  },
+
+  /**
+   * Provides a list of the view indices that are selected which is *not* the
+   *  same as the rows of the selected messages.  When the
+   *  "mail.operate_on_msgs_in_collapsed_threads" preference is enabled,
+   *  messages may be selected but not visible (because the thread root is
+   *  selected.)
+   * You probably want to use the |selectedMessages| attribute instead of this
+   *  one.  (Or selectedMessageUris in some rare cases.)
+   *
+   * If the user has right-clicked on a message, this will return that message
+   *  and not the selection prior to the right-click.
+   *
+   * @return a list of the view indices that are currently selected
+   */
+  get selectedIndices FolderDisplayWidget_get_selectedIndices() {
+    if (!this.view.dbView)
+      return [];
+
+    return this.view.dbView.getIndicesForSelection({});
+  },
+
+  /**
+   * Provides a list of the message headers for the currently selected messages.
+   *  If the "mail.operate_on_msgs_in_collapsed_threads" preference is enabled,
+   *  then any collapsed thread roots that are selected will also (conceptually)
+   *  have all of the messages in that thread selected and they will be included
+   *  in the returned list.
+   *
+   * If the user has right-clicked on a message, this will return that message
+   *  (and any collapsed children if so enabled) and not the selection prior to
+   *  the right-click.
+   *
+   * @return a list of the message headers for the currently selected messages.
+   *     If there are no selected messages, the result is an empty list.
+   */
+  get selectedMessages FolderDisplayWidget_get_selectedMessages() {
+    if (!this.view.dbView)
+      return [];
+    // getMsgHdrsForSelection returns an nsIMutableArray.  We want our callers
+    //  to have a user-friendly JS array and not have to worry about
+    //  QueryInterfacing the values (or needing to know to use fixIterator).
+    return [msgHdr for each
+              (msgHdr in fixIterator(
+                          this.view.dbView.getMsgHdrsForSelection().enumerate(),
+                          Components.interfaces.nsIMsgDBHdr))];
+  },
+
+  /**
+   * @return a list of the URIs for the currently selected messages or null
+   *     (instead of a list) if there are no selected messages.  Do not
+   *     pass around URIs unless you have a good reason.  Legacy code is an
+   *     ok reason.
+   *
+   * If the user has right-clicked on a message, this will return that message's
+   *  URI and not the selection prior to the right-click.
+   */
+  get selectedMessageUris FolderDisplayWidget_get_selectedMessageUris() {
+    if (!this.view.dbView)
+      return null;
+
+    let messageArray = this.view.dbView.getURIsForSelection({});
+    return messageArray.length ? messageArray : null;
+  },
+
+  /**
+   * Clear the tree selection, making sure the message pane is cleared and
+   *  the context display (toolbars, etc.) are updated.
+   */
+  clearSelection: function FolderDisplayWidget_clearSelection() {
+    let treeSelection = this.treeSelection; // potentially magic getter
+    if (!treeSelection)
+      return;
+    treeSelection.clearSelection();
+    this.messageDisplay.clearDisplay();
+    this._updateContextDisplay();
+  },
+
+  /**
+   * Select a message for display by header.  If the view is active, attempt
+   *  to select the message right now.  If the view is not active or we were
+   *  unable to find it, update our saved selection to want to display the
+   *  message.  Threads are expanded to find the header.
+   *
+   * @param aMsgHdr The message header to select for display.
+   */
+  selectMessage: function FolderDisplayWidget_selectMessage(aMsgHdr,
+      aForceNotification) {
+    if (this.active) {
+      let viewIndex = this.view.dbView.findIndexOfMsgHdr(aMsgHdr, true);
+      if (viewIndex != nsMsgViewIndex_None) {
+        this.selectViewIndex(viewIndex);
+        return;
+      }
+    }
+    this._savedSelection = [{messageId: aMsgHdr.messageId}];
+    // queue the selection to be restored once we become active if we are not
+    //  active.
+    if (!this.active)
+      this._notifyWhenActive(this._restoreSelection);
+  },
+
+  /**
+   * Select all of the provided nsIMsgDBHdrs in the aMessages array, expanding
+   *  threads as required.  If the view is not active, or we were not able to
+   *  find all of the messages, update our saved selection to want to display
+   *  the messages.  The messages will then be selected when we are made active
+   *  or all messages in the folder complete loading.  This is to accomodate the
+   *  use-case where we are backed by an in-progress search and no
+   *
+   * @param aMessages An array of nsIMsgDBHdr instances.
+   * @param aDoNotNeedToFindAll If true (can be omitted and left undefined), we
+   *     do not attempt to save the selection for future use.  This is intended
+   *     for use by the _restoreSelection call which is the end-of-the-line for
+   *     restoring the selection.  (Once it gets called all of our messages
+   *     should have already been loaded.)
+   */
+  selectMessages: function FolderDisplayWidget_selectMessages(
+    aMessages, aDoNotNeedToFindAll) {
+    if (this.active && this.treeSelection) {
+      let foundAll = true;
+      let treeSelection = this.treeSelection;
+      let dbView = this.view.dbView;
+      let minRow = null, maxRow = null;
+
+      treeSelection.selectEventsSuppressed = true;
+      treeSelection.clearSelection();
+
+      for each (let [, msgHdr] in Iterator(aMessages)) {
+        let viewIndex = dbView.findIndexOfMsgHdr(msgHdr, true);
+        if (viewIndex != nsMsgViewIndex_None) {
+          if (minRow == null || viewIndex < minRow)
+            minRow = viewIndex;
+          if (maxRow == null || viewIndex > maxRow )
+            maxRow = viewIndex;
+          // nsTreeSelection is actually very clever about doing this
+          //  efficiently.
+          treeSelection.rangedSelect(viewIndex, viewIndex, true);
+        }
+        else {
+          foundAll = false;
+        }
+
+        // make sure the selection is as visible as possible
+        if (minRow != null)
+          this.ensureRowRangeIsVisible(minRow, maxRow);
+      }
+
+      treeSelection.selectEventsSuppressed = false;
+
+      if (!aDoNotNeedToFindAll || foundAll)
+        return;
+    }
+    this._savedSelection = [{messageId: msgHdr.messageId} for each
+                            ([, msgHdr] in Iterator(aMessages))];
+    if (!this.active)
+      this._notifyWhenActive(this._restoreSelection);
+  },
+
+  /**
+   * Select the message at view index.
+   *
+   * @param aViewIndex The view index to select.  This will be bounds-checked
+   *     and if it is outside the bounds, we will clear the selection and
+   *     bail.
+   */
+  selectViewIndex: function FolderDisplayWidget_selectViewIndex(aViewIndex) {
+    let treeSelection = this.treeSelection;
+    // if we have no selection, we can't select something
+    if (!treeSelection)
+      return;
+    let rowCount = this.view.dbView.rowCount;
+    if ((aViewIndex == nsMsgViewIndex_None) ||
+        (aViewIndex < 0) || (aViewIndex >= rowCount)) {
+      this.clearSelection();
+      return;
+    }
+
+    // Check whether the index is already selected/current.  This can be the
+    //  case when we are here as the result of a deletion.  Assuming
+    //  nsMsgDBView::NoteChange ran and was not suppressing change
+    //  notifications, then it's very possible the selection is already where
+    //  we want it to go.  However, in that case, nsMsgDBView::SelectionChanged
+    //  bailed without doing anything because m_deletingRows...
+    // So we want to generate a change notification if that is the case. (And
+    //  we still want to call ensureRowIsVisible, as there may be padding
+    //  required.)
+    if ((treeSelection.count == 1) &&
+        ((treeSelection.currentIndex == aViewIndex) ||
+         treeSelection.isSelected(aViewIndex))) {
+      // Make sure the index we just selected is also the current index.
+      //  This can happen when the tree selection adjusts itself as a result of
+      //  changes to the tree as a result of deletion.  This will not trigger
+      //  a notification.
+      treeSelection.select(aViewIndex);
+      this.view.dbView.selectionChanged();
+    }
+    // Previous code was concerned about avoiding updating commands on the
+    //  assumption that only the selection count mattered.  We no longer
+    //  make this assumption.
+    // Things that may surprise you about the call to treeSelection.select:
+    // 1) This ends up calling the onselect method defined on the XUL 'tree'
+    //    tag.  For the 3pane this is the ThreadPaneSelectionChanged method in
+    //    threadPane.js.  That code checks a global to see if it is dealing
+    //    with a right-click, and ignores it if so.
+    else {
+      treeSelection.select(aViewIndex);
+    }
+
+    this.ensureRowIsVisible(aViewIndex);
+  },
+
+  /**
+   * For every selected message in the display that is part of a (displayed)
+   *  thread and is not the root message, de-select it and ensure that the
+   *  root message of the thread is selected.
+   * This is primarily intended to be used when collapsing visible threads.
+   *
+   * We do nothing if we are not in a threaded display mode.
+   */
+  selectSelectedThreadRoots:
+      function FolderDisplayWidget_selectSelectedThreadRoots() {
+    if (!this.view.showThreaded)
+      return;
+
+    // There are basically two implementation strategies available to us:
+    // 1) For each selected view index with a level > 0, keep walking 'up'
+    //    (numerically smaller) until we find a message with level 0.
+    //    The inefficiency here is the potentially large number of JS calls
+    //    into XPCOM space that will be required.
+    // 2) Ask for the thread that each view index belongs to, use that to
+    //    efficiently retrieve the thread root, then find the root using
+    //    the message header.  The inefficiency here is that the view
+    //    currently does a linear scan, albeit a relatively efficient one.
+    // And the winner is... option 2, because the code is simpler because we
+    //  can reuse selectMessages to do most of the work.
+    let selectedIndices = this.selectedIndices;
+    let newSelectedMessages = [];
+    let dbView = this.view.dbView;
+    for each (let [, index] in Iterator(selectedIndices)) {
+      let thread = dbView.getThreadContainingIndex(index);
+      // We use getChildHdrAt instead of getRootHdr because getRootHdr has
+      //  a useless out-param and just calls getChildHdrAt anyways.
+      newSelectedMessages.push(thread.getChildHdrAt(0));
+    }
+    this.selectMessages(newSelectedMessages);
+  },
+
+  /**
+   * Number of padding messages before the 'focused' message when it is at the
+   *  top of the thread pane.
+   */
+  TOP_VIEW_PADDING: 1,
+  /**
+   * Number of padding messages after the 'focused' message when it is at the
+   *  bottom of the thread pane and lip padding does not apply.
+   */
+  BOTTOM_VIEW_PADDING: 1,
+
+  /**
+   * Ensure the given view index is visible, preferably with some padding.
+   * By padding, we mean that the index will not be the first or last message
+   *  displayed, but rather have messages on either side.
+   * If we get near the end of the list of messages, we 'snap' to the last page
+   *  of messages.  The intent is that we later implement a
+   * We have the concept of a 'lip' when we are at the end of the message
+   *  display.  If we are near the end of the display, we want to show an
+   *  empty row (at the bottom) so the user knows they are at the end.  Also,
+   *  if a message shows up that is new and things are sorted ascending, this
+   *  turns out to be useful.
+   */
+  ensureRowIsVisible: function FolderDisplayWidget_ensureRowIsVisible(
+      aViewIndex, aBounced) {
+    // Dealing with the tree view layout is a nightmare, let's just always make
+    //  sure we re-schedule ourselves.  The most particular rationale here is
+    //  that the message pane may be toggling its state and it's much simpler
+    //  and reliable if we ensure that all of FolderDisplayWidget's state
+    //  change logic gets to run to completion before we run ourselves.
+    if (!aBounced) {
+      let dis = this;
+      window.setTimeout(function() {
+          dis.ensureRowIsVisible(aViewIndex, true);
+        }, 0);
+    }
+
+    let treeBox = this.treeBox;
+    if (!treeBox)
+      return;
+
+    // try and trigger a reflow...
+    treeBox.height;
+
+    let maxIndex = this.view.dbView.rowCount - 1;
+
+    let first = treeBox.getFirstVisibleRow();
+    // Assume the bottom row is half-visible and should generally be ignored.
+    // (We could actually do the legwork to see if there is a partial one...)
+    const halfVisible = 1;
+    let last  = treeBox.getLastVisibleRow() - halfVisible;
+    let span = treeBox.getPageLength() - halfVisible;
+
+    let target;
+    // If the index is near the end, try and latch on to the bottom.
+    if (aViewIndex + span - this.TOP_VIEW_PADDING > maxIndex)
+      target = maxIndex - span;
+    // If the index is after the last visible guy (with padding), move down
+    //  so that the target index is padded in 1 from the bottom.
+    else if (aViewIndex >= last - this.BOTTOM_VIEW_PADDING)
+      target = Math.min(maxIndex, aViewIndex + this.BOTTOM_VIEW_PADDING) -
+                 span;
+    // If the index is before the first visible guy (with padding), move up
+    else if (aViewIndex <= first + this.TOP_VIEW_PADDING)  // move up
+      target = Math.max(0, aViewIndex - this.TOP_VIEW_PADDING);
+    else // it is already visible
+      return;
+
+    // this sets the first visible row
+    treeBox.scrollToRow(target);
+  },
+
+  /**
+   * Ensure that the given range of rows is visible maximally visible in the
+   *  thread pane.  If the range is larger than the number of rows that can be
+   *  displayed in the thread pane, we bias towards showing the min row (with
+   *  padding).
+   *
+   * @param aMinRow The numerically smallest row index defining the start of
+   *     the inclusive range.
+   * @param aMaxRow The numberically largest row index defining the end of the
+   *     inclusive range.
+   */
+  ensureRowRangeIsVisible:
+      function FolderDisplayWidget_ensureRowRangeIsVisible(aMinRow, aMaxRow,
+                                                           aBounced) {
+    // Dealing with the tree view layout is a nightmare, let's just always make
+    //  sure we re-schedule ourselves.  The most particular rationale here is
+    //  that the message pane may be toggling its state and it's much simpler
+    //  and reliable if we ensure that all of FolderDisplayWidget's state
+    //  change logic gets to run to completion before we run ourselves.
+    if (!aBounced) {
+      let dis = this;
+      window.setTimeout(function() {
+          dis.ensureRowRangeIsVisible(aMinRow, aMaxRow, true);
+        }, 0);
+    }
+
+    let treeBox = this.treeBox;
+    if (!treeBox)
+      return;
+    let first = treeBox.getFirstVisibleRow();
+    const halfVisible = 1;
+    let last  = treeBox.getLastVisibleRow() - halfVisible;
+    let span = treeBox.getPageLength() - halfVisible;
+
+    // bail if the range is already visible with padding constraints handled
+    if ((first + this.TOP_VIEW_PADDING <= aMinRow) &&
+        (last - this.BOTTOM_VIEW_PADDING >= aMaxRow))
+      return;
+
+    let target;
+    // if the range is bigger than we can fit, optimize position for the min row
+    //  with padding to make it obvious the range doesn't extend above the row.
+    if (aMaxRow - aMinRow > span)
+      target = Math.max(0, aMinRow - this.TOP_VIEW_PADDING);
+    // So the range must fit, and it's a question of how we want to position it.
+    // For now, the answer is we try and center it, why not.
+    else {
+      let rowSpan = aMaxRow - aMinRow + 1;
+      let halfSpare = parseInt((span - rowSpan - this.TOP_VIEW_PADDING -
+                                this.BOTTOM_VIEW_PADDING) / 2);
+      target = aMinRow - halfSpare - this.TOP_VIEW_PADDING;
+    }
+    treeBox.scrollToRow(target);
+  },
+
+  /**
+   * Ensure that the selection is visible to the extent possible.
+   */
+  ensureSelectionIsVisible:
+      function FolderDisplayWidget_ensureSelectionIsVisible() {
+    let treeSelection = this.treeSelection; // potentially magic getter
+    if (!treeSelection || !treeSelection.count)
+      return;
+
+    let minRow = null, maxRow = null;
+
+    let rangeCount = treeSelection.getRangeCount();
+    for (let iRange = 0; iRange < rangeCount; iRange++) {
+      let rangeMinObj = {}, rangeMaxObj = {};
+      treeSelection.getRangeAt(iRange, rangeMinObj, rangeMaxObj);
+      let rangeMin = rangeMinObj.value, rangeMax = rangeMaxObj.value;
+      if (minRow == null || rangeMin < minRow)
+        minRow = rangeMin;
+      if (maxRow == null || rangeMax > maxRow )
+        maxRow = rangeMax;
+    }
+
+    this.ensureRowRangeIsVisible(minRow, maxRow);
+  }
+};
+
+/**
+ * Implement a fake nsITreeBoxObject so that we can keep the view
+ *  nsITreeSelection selections 'live' when they are in the background.  We need
+ *  to do this because nsTreeSelection changes its behaviour (and gets ornery)
+ *  if it does not have a box object.
+ * This does not need to exist once we abandon multiplexed tabbing.
+ *
+ * Sometimes, nsTreeSelection tries to turn us into an nsIBoxObject and then in
+ *  turn get the associated element, and then create DOM events on that.  We
+ *  can't really stop that, but we can use misdirection to tell it about a box
+ *  object that we don't care about.  That way it gets the bogus events,
+ *  effectively blackholing them.
+ */
+function FakeTreeBoxObject(aDummyBoxObject) {
+  this.dummyBoxObject = aDummyBoxObject.QueryInterface(
+                          Components.interfaces.nsIBoxObject);
+  this.view = null;
+}
+FakeTreeBoxObject.prototype = {
+  view: null,
+  /**
+   * No need to actually invalidate, as when we re-root the view this will
+   *  happen.
+   */
+  invalidate: function FakeTreeBoxObject_invalidate() {
+    // NOP
+  },
+  invalidateRange: function FakeTreeBoxObject_invalidateRange() {
+    // NOP
+  },
+  invalidateRow: function FakeTreeBoxObject_invalidateRow() {
+    // NOP
+  },
+  beginUpdateBatch: function FakeTreeBoxObject_beginUpdateBatch() {
+
+  },
+  endUpdateBatch: function FakeTreeBoxObject_endUpdateBatch() {
+
+  },
+  rowCountChanged: function FakeTreeBoxObject_rowCountChanged() {
+
+  },
+  /**
+   * Sleight of hand!  If someone asks us about an nsIBoxObject, we tell them
+   *  about a real box object that is just a dummy and is never used for
+   *  anything.
+   */
+  QueryInterface: function FakeTreeBoxObject_QueryInterface(aIID) {
+    if (aIID.equals(Components.interfaces.nsIBoxObject))
+      return this.dummyBoxObject;
+    if (!aIID.equals(Components.interfaces.nsISupports) &&
+        !aIID.equals(Components.interfaces.nsITreeBoxObject))
+      throw Components.results.NS_ERROR_NO_INTERFACE;
+    return this;
+  }
+};
+/*
+ * Provide attribute and function implementations that complain very loudly if
+ *  they are used.  Now, XPConnect will return an error to callers if we don't
+ *  implement part of the interface signature, but this is unlikely to provide
+ *  the visibility we desire.  In fact, since it is a simple nsresult error,
+ *  it may make things completely crazy.  So this way we can yell via dump,
+ *  throw an exception, etc.
+ */
+function FTBO_stubOutAttributes(aObj, aAttribNames) {
+  for (let [, attrName] in Iterator(aAttribNames)) {
+    let myAttrName = attrName;
+    aObj.__defineGetter__(attrName,
+      function() {
+        let msg = "Read access to stubbed attribute " + myAttrName;
+        dump(msg + "\n");
+        debugger;
+        throw new Error(msg);
+      });
+    aObj.__defineSetter__(attrName,
+      function() {
+        let msg = "Write access to stubbed attribute " + myAttrName;
+        dump(msg + "\n");
+        debugger;
+        throw new Error(msg);
+      });
+  }
+}
+function FTBO_stubOutMethods(aObj, aMethodNames) {
+  for (let [, methodName] in Iterator(aMethodNames)) {
+    let myMethodName = methodName;
+    aObj[myMethodName] = function() {
+      let msg = "Call to stubbed method " + myMethodName;
+      dump(msg + "\n");
+      debugger;
+      throw new Error(msg);
+    };
+  }
+}
+FTBO_stubOutAttributes(FakeTreeBoxObject.prototype, [
+  "columns",
+  "focused",
+  "treeBody",
+  "rowHeight",
+  "rowWidth",
+  "horizontalPosition",
+  "selectionRegion",
+  ]);
+FTBO_stubOutMethods(FakeTreeBoxObject.prototype, [
+  "getFirstVisibleRow",
+  "getLastVisibleRow",
+  "getPageLength",
+  "ensureRowIsVisible",
+  "ensureCellIsVisible",
+  "scrollToRow",
+  "scrollByLines",
+  "scrollByPages",
+  "scrollToCell",
+  "scrollToColumn",
+  "scrollToHorizontalPosition",
+  "invalidateColumn",
+  "invalidateRow",
+  "invalidateCell",
+  "invalidateColumnRange",
+  "getRowAt",
+  "getCellAt",
+  "getCoordsForCellItem",
+  "isCellCropped",
+  "clearStyleAndImageCaches",
+  ]);
\ No newline at end of file
--- a/mail/base/content/folderPane.js
+++ b/mail/base/content/folderPane.js
@@ -58,17 +58,17 @@ let gFolderTreeView = {
   load: function ftv_load(aTree, aJSONFile) {
     const Cc = Components.classes;
     const Ci = Components.interfaces;
     this._treeElement = aTree;
 
     let smartName = document.getElementById("bundle_messenger")
                             .getString("folderPaneHeader_smart");
     this.registerMode("smart", this._smartFoldersGenerator, smartName);
-    // the folder pane can be used for other trees which may not have these elements. 
+    // the folder pane can be used for other trees which may not have these elements.
     if (document.getElementById("folderpane_splitter"))
       document.getElementById("folderpane_splitter").collapsed = false;
     if (document.getElementById("folderPaneBox"))
       document.getElementById("folderPaneBox").collapsed = false;
 
     try {
       // Normally our tree takes care of keeping the last selected by itself.
       // However older versions of TB stored this in a preference, which we need
@@ -105,17 +105,17 @@ let gFolderTreeView = {
                          .createInstance(Ci.nsIFileInputStream);
         let sstream = Cc["@mozilla.org/scriptableinputstream;1"]
                          .createInstance(Ci.nsIScriptableInputStream);
         fstream.init(file, -1, 0, 0);
         sstream.init(fstream);
 
         while (sstream.available())
           data += sstream.read(4096);
-        
+
         sstream.close();
         fstream.close();
         let JSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
         this._persistOpenMap = JSON.decode(data);
       }
     }
 
     // Load our data
@@ -211,17 +211,17 @@ let gFolderTreeView = {
         aEvent.button != 0 || aEvent.originalTarget.localName == "twisty" ||
         aEvent.originalTarget.localName == "slider" ||
         aEvent.originalTarget.localName == "scrollbarbutton")
       return;
 
     let row = gFolderTreeView._treeElement.treeBoxObject.getRowAt(aEvent.clientX,
                                                                   aEvent.clientY);
     let folderItem = gFolderTreeView._rowMap[row];
-    if (folderItem)                                                   
+    if (folderItem)
       folderItem.command();
 
     // Don't let the double-click toggle the open state of the folder here
     aEvent.stopPropagation();
   },
 
   getFolderAtCoords: function ftv_getFolderAtCoords(aX, aY) {
     let row = gFolderTreeView._treeElement.treeBoxObject.getRowAt(aX, aY);
@@ -265,17 +265,17 @@ let gFolderTreeView = {
    * that the folder is actually being displayed (that is, that none of its
    * ancestors are collapsed.
    *
    * @param aFolder  the nsIMsgFolder to select
    */
   selectFolder: function ftv_selectFolder(aFolder) {
     // "this" inside the nested function refers to the function...
     // Also note that openIfNot is recursive.
-    let tree = this; 
+    let tree = this;
     function openIfNot(aFolderToOpen) {
       let index = tree.getIndexOfFolder(aFolderToOpen);
       if (index) {
         if (!tree._rowMap[index].open)
           tree._toggleRow(index, false);
         return;
       }
 
@@ -444,17 +444,17 @@ let gFolderTreeView = {
         let folders = new Array;
         folders.push(dt.mozGetDataAt("text/x-moz-folder", i)
                        .QueryInterface(Ci.nsIMsgFolder));
         let array = toXPCOMArray(folders, Ci.nsIMutableArray);
         cs.CopyFolders(array, targetFolder,
                       (folders[0].server == targetFolder.server), null,
                        msgWindow);
       }
-    } 
+    }
     else if (Array.indexOf(types, "text/x-moz-newsfolder") != -1) {
       // Start by getting folders into order.
       let folders = new Array;
       for (let i = 0; i < count; i++) {
         let folder = dt.mozGetDataAt("text/x-moz-newsfolder", i)
                        .QueryInterface(Ci.nsIMsgFolder);
         folders[this.getIndexOfFolder(folder)] = folder;
       }
@@ -1027,17 +1027,17 @@ let gFolderTreeView = {
           acct.incomingServer.rootFolder.subFolders;
         }
         catch (ex) {
           Components.classes["@mozilla.org/consoleservice;1"]
                     .getService(Components.interfaces.nsIConsoleService)
                     .logStringMessage("Discovering folders for account failed with exception: " + ex);
         }
       }
-        
+
       return [new ftvItem(acct.incomingServer.rootFolder)
               for each (acct in accounts)];
     },
 
     /**
      * The unread mode returns all folders that are not root-folders and that
      * have unread items.  Also always keep the currently selected folder
      * so it doesn't disappear under the user.
@@ -1129,17 +1129,17 @@ let gFolderTreeView = {
 
       for each (let folder in ftv._enumerateFolders)
         addIfRecent(folder);
 
       recentFolders.sort(sorter);
 
       let items = [new ftvItem(f) for each (f in recentFolders)];
 
-      // There are no children in this view! 
+      // There are no children in this view!
       // And we want to display the account name to distinguish folders w/
       // the same name.
       for each (let folder in items) {
         folder.__defineGetter__("children", function() []);
         folder.addServerName = true;
       }
 
       return items;
@@ -1253,17 +1253,17 @@ let gFolderTreeView = {
 
   OnItemRemoved: function ftl_remove(aRDFParentItem, aItem) {
     if (!(aItem instanceof Components.interfaces.nsIMsgFolder))
       return;
 
     let persistMapIndex = this._persistOpenMap[this.mode].indexOf(aItem.URI);
     if (persistMapIndex != -1)
       this._persistOpenMap[this.mode].splice(persistMapIndex, 1);
-    
+
     let index = this.getIndexOfFolder(aItem);
     if (!index)
       return;
     // forget our parent's children; they'll get rebuilt
     if (aRDFParentItem)
       this._rowMap[index]._parent._children = null;
     let kidCount = 1;
     let walker = Number(index) + 1;
@@ -1465,23 +1465,16 @@ let gFolderTreeController = {
       msgDB.summaryValid = false;
       try {
         folder.closeAndBackupFolderDB("");
       }
       catch(e) {
         // In a failure, proceed anyway since we're dealing with problems
         folder.ForceDBClosed();
       }
-      // these lines will cause the thread pane to get reloaded when the
-      // download/reparse is finished. Only do this if the selected folder is
-      // loaded (i.e., not thru the context menu on a non-loaded folder).
-      if (folder == GetLoadedMsgFolder()) {
-        gRerootOnFolderLoad = true;
-        gCurrentFolderToReroot = folder.URI;
-      }
       folder.updateFolder(msgWindow);
     }
 
     window.openDialog("chrome://messenger/content/folderProps.xul", "",
                       "chrome,centerscreen,titlebar,modal",
                       {folder: folder, serverType: folder.server.type,
                        msgWindow: msgWindow, title: title,
                        okCallback: editFolderCallback,
@@ -1500,17 +1493,16 @@ let gFolderTreeController = {
     let folder = aFolder || gFolderTreeView.getSelectedFolders()[0];
 
     //xxx no need for uri now
     let controller = this;
     function renameCallback(aName, aUri) {
       if (aUri != folder.URI)
         Components.utils.reportError("got back a different folder to rename!");
 
-      controller._resetThreadPane();
       controller._tree.view.selection.clearSelection();
 
       // Actually do the rename
       folder.rename(aName, msgWindow);
     }
     window.openDialog("chrome://messenger/content/renameFolderDialog.xul",
                       "newFolder", "chrome,titlebar,modal",
                       {preselectedURI: folder.URI,
@@ -1559,21 +1551,19 @@ let gFolderTreeController = {
     }
 
     if (folder.flags & FLAGS.Virtual) {
       let confirmation = bundle.getString("confirmSavedSearchDeleteMessage");
       let title = bundle.getString("confirmSavedSearchTitle");
       let IPS = Components.interfaces.nsIPromptService;
       if (Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
             .getService(IPS)
-            .confirmEx(window, title, confirmation, IPS.STD_YES_NO_BUTTONS + IPS.BUTTON_POS_1_DEFAULT, 
+            .confirmEx(window, title, confirmation, IPS.STD_YES_NO_BUTTONS + IPS.BUTTON_POS_1_DEFAULT,
                        "", "", "", "", {}) != 0) /* the yes button is in position 0 */
         return;
-      if (gCurrentVirtualFolderUri == folder.URI)
-        gCurrentVirtualFolderUri = null;
     }
 
     let array = toXPCOMArray([folder], Ci.nsIMutableArray);
     folder.parent.deleteSubFolders(array, msgWindow);
   },
 
   /**
    * Prompts the user to confirm and empties the trash for the selected folder
@@ -1621,20 +1611,16 @@ let gFolderTreeController = {
    */
   compactFolders: function ftc_compactFolders(aFolders) {
     let folders = aFolders || gFolderTreeView.getSelectedFolders();
     for (let i = 0; i < folders.length; i++) {
       // Can't compact folders that have just been compacted.
       if (folders[i].server.type != "imap" && !folders[i].expungedBytes)
         continue;
 
-      // Reset thread pane for non-imap folders if the folder is selected.
-      if (gDBView && gDBView.msgFolder == folders[i] && folders[i].server.type != "imap")
-        this._resetThreadPane();
-
       folders[i].compact(null, msgWindow);
     }
   },
 
   /**
    * Compacts all folders for accounts that the given folders belong
    * to, or all folders for accounts of the currently selected folders.
    *
@@ -1686,29 +1672,16 @@ let gFolderTreeController = {
     window.openDialog("chrome://messenger/content/virtualFolderProperties.xul",
                       "", "chrome,titlebar,modal,centerscreen",
                       {folder: folder, editExistingFolder: true,
                        onOKCallback: editVirtualCallback,
                        msgWindow: msgWindow});
   },
 
   /**
-   * For certain folder commands, the thread pane needs to be invalidated, this
-   * takes care of doing so.
-   */
-  _resetThreadPane: function ftc_resetThreadPane() {
-    if (gDBView)
-      gCurrentlyDisplayedMessage = gDBView.currentlyDisplayedMessage;
-
-    ClearThreadPaneSelection();
-    ClearThreadPane();
-    ClearMessagePane();
-  },
-
-  /**
    * Prompts for confirmation, if the user hasn't already chosen the "don't ask
    * again" option.
    *
    * @param aCommand - the command to prompt for
    */
   _checkConfirmationPrompt: function ftc_confirm(aCommand) {
     const Cc = Components.classes;
     const Ci = Components.interfaces;
--- a/mail/base/content/mail-offline.js
+++ b/mail/base/content/mail-offline.js
@@ -259,12 +259,11 @@ var MailOfflineMgr = {
     }    
   },   
 
   /**
    * private helper method called whenever we detect a change to the offline state
    */ 
   mailOfflineStateChanged: function (aGoingOffline)
   {
-    gFolderJustSwitched = true;
     this.updateOfflineUI(aGoingOffline);
   }
 };
--- a/mail/base/content/mail3PaneWindowCommands.js
+++ b/mail/base/content/mail3PaneWindowCommands.js
@@ -1,48 +1,47 @@
-# -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is Mozilla Communicator client code, released
-# March 31, 1998.
-#
-# The Initial Developer of the Original Code is
-# Netscape Communications Corporation.
-# Portions created by the Initial Developer are Copyright (C) 1998-2000
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#   Jan Varga <varga@nixcorp.com>
-#   HÃ¥kan Waara <hwaara@gmail.com>
-#   Magnus Melin <mkmelin+mozilla@iki.fi>
-#
-# 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 *****
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Communicator client code, released
+ * March 31, 1998.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1998-2000
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Jan Varga <varga@nixcorp.com>
+ *   HÃ¥kan Waara <hwaara@gmail.com>
+ *   Magnus Melin <mkmelin+mozilla@iki.fi>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
 
 var gMessengerBundle = document.getElementById("bundle_messenger");
 
 // Controller object for folder pane
 var FolderPaneController =
 {
   supportsCommand: function(command)
   {
@@ -103,17 +102,17 @@ var FolderPaneController =
     if (!this.isCommandEnabled(command)) return;
 
     switch ( command )
     {
       case "cmd_delete":
       case "cmd_shiftDelete":
       case "button_delete":
       case "cmd_deleteFolder":
-        gFolderTreeController.deleteFolder(); 
+        gFolderTreeController.deleteFolder();
         break;
     }
   },
 
   onEvent: function(event)
   {
   }
 };
@@ -237,17 +236,17 @@ var DefaultController =
       case "cmd_downloadFlagged":
       case "cmd_downloadSelected":
       case "cmd_synchronizeOffline":
         return MailOfflineMgr.isOnline();
 
       case "cmd_watchThread":
       case "cmd_killThread":
       case "cmd_killSubthread":
-        return(isNewsURI(GetFirstSelectedMessage()));
+        return(gFolderDisplay.selectedMessageIsNews);
 
       default:
         return false;
     }
   },
 
   isCommandEnabled: function(command)
   {
@@ -257,45 +256,37 @@ var DefaultController =
 
     switch ( command )
     {
       case "cmd_delete":
         UpdateDeleteCommand();
         // fall through
       case "button_delete":
         UpdateDeleteToolbarButton();
-        if (gDBView)
-          gDBView.getCommandStatus(nsMsgViewCommandType.deleteMsg, enabled, checkStatus);
-        return enabled.value;
+        return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.deleteMsg);
       case "cmd_shiftDelete":
-        if (gDBView)
-          gDBView.getCommandStatus(nsMsgViewCommandType.deleteNoTrash, enabled, checkStatus);
-        return enabled.value;
+        return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.deleteNoTrash);
       case "cmd_deleteFolder":
         var folders = gFolderTreeView.getSelectedFolders();
         if (folders.length == 1) {
           var folder = folders[0];
           if (folder.server.type == "nntp")
             return false; // Just disable the command for news.
           else
             return CanDeleteFolder(folder);
         }
         return false;
       case "button_junk":
         UpdateJunkToolbarButton();
-        if (gDBView)
-          gDBView.getCommandStatus(nsMsgViewCommandType.junk, enabled, checkStatus);
-        return enabled.value;
+        return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.junk);
       case "cmd_killThread":
       case "cmd_killSubthread":
         return GetNumSelectedMessages() > 0;
       case "cmd_watchThread":
-        if (gDBView)
-          gDBView.getCommandStatus(nsMsgViewCommandType.toggleThreadWatched, enabled, checkStatus);
-        return enabled.value;
+        return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.toggleThreadWatched);
       case "cmd_createFilterFromPopup":
       case "cmd_createFilterFromMenu":
         var loadedFolder = GetLoadedMsgFolder();
         if (!(loadedFolder && loadedFolder.server.canHaveFilters))
           return false;   // else fall thru
       case "cmd_saveAsFile":
       case "cmd_saveAsTemplate":
         if (GetNumSelectedMessages() > 1)
@@ -323,30 +314,21 @@ var DefaultController =
         {
           var whichText = "valueMessage";
           if (GetNumSelectedMessages() > 1)
             whichText = "valueSelection";
           goSetMenuValue(command, whichText);
           goSetAccessKey(command, whichText + "AccessKey");
         }
         if (GetNumSelectedMessages() > 0)
-        {
-          if (gDBView)
-          {
-            gDBView.getCommandStatus(nsMsgViewCommandType.cmdRequiringMsgBody, enabled, checkStatus);
-            return enabled.value;
-          }
-        }
+          return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.cmdRequiringMsgBody);
         return false;
       case "cmd_printpreview":
-        if ( GetNumSelectedMessages() == 1 && gDBView)
-        {
-           gDBView.getCommandStatus(nsMsgViewCommandType.cmdRequiringMsgBody, enabled, checkStatus);
-           return enabled.value;
-        }
+        if (GetNumSelectedMessages() == 1)
+          return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.cmdRequiringMsgBody);
         return false;
       case "cmd_printSetup":
         return true;
       case "cmd_markAsFlagged":
       case "button_file":
       case "cmd_file":
       case "cmd_archive":
         return (GetNumSelectedMessages() > 0 );
@@ -355,33 +337,27 @@ var DefaultController =
         let folder = GetLoadedMsgFolder();
         return GetNumSelectedMessages() > 0 && folder &&
           !(IsSpecialFolder(folder, Components.interfaces.nsMsgFolderFlags.Archive,
                             true));
       }
       case "cmd_markAsJunk":
       case "cmd_markAsNotJunk":
         // can't do news on junk yet.
-        return (GetNumSelectedMessages() > 0 && !isNewsURI(GetFirstSelectedMessage()));
+        return (GetNumSelectedMessages() > 0 && !gFolderDisplay.selectedMessageIsNews);
       case "cmd_recalculateJunkScore":
         if (GetNumSelectedMessages() > 0)
-          gDBView.getCommandStatus(nsMsgViewCommandType.runJunkControls, enabled, checkStatus);
-        return enabled.value;
+          return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.runJunkControls);
+        return false;
       case "cmd_applyFilters":
-        if (gDBView)
-          gDBView.getCommandStatus(nsMsgViewCommandType.applyFilters, enabled, checkStatus);
-        return enabled.value;
+        return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.applyFilters);
       case "cmd_runJunkControls":
-        if (gDBView)
-          gDBView.getCommandStatus(nsMsgViewCommandType.runJunkControls, enabled, checkStatus);
-        return enabled.value;
+        return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.runJunkControls);
       case "cmd_deleteJunk":
-        if (gDBView)
-          gDBView.getCommandStatus(nsMsgViewCommandType.deleteJunk, enabled, checkStatus);
-        return enabled.value;
+        return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.deleteJunk);
       case "button_mark":
       case "cmd_tag":
       case "cmd_markAsRead":
       case "cmd_markThreadAsRead":
         return GetNumSelectedMessages() > 0;
       case "button_previous":
       case "button_next":
         return IsViewNavigationItemEnabled();
@@ -413,28 +389,27 @@ var DefaultController =
       case "cmd_selectAll":
       case "cmd_selectFlagged":
         return gDBView != null;
       // these are enabled on when we are in threaded mode
       case "cmd_selectThread":
         if (GetNumSelectedMessages() <= 0) return false;
       case "cmd_expandAllThreads":
       case "cmd_collapseAllThreads":
-        return gDBView && (gDBView.viewFlags & nsMsgViewFlagsType.kThreadedDisplay);
+        return gFolderDisplay.view.showThreaded;
       case "cmd_nextFlaggedMsg":
       case "cmd_previousFlaggedMsg":
         return IsViewNavigationItemEnabled();
       case "cmd_viewAllMsgs":
       case "cmd_viewUnreadMsgs":
       case "cmd_viewIgnoredThreads":
         return gDBView;
       case "cmd_viewThreadsWithUnread":
       case "cmd_viewWatchedThreadsWithUnread":
-        return gDBView && !(GetSelectedMsgFolders()[0].flags & 
-                            Components.interfaces.nsMsgFolderFlags.Virtual);
+        return !gFolderDisplay.view.isVirtual;
       case "cmd_stop":
         return true;
       case "cmd_undo":
       case "cmd_redo":
           return SetupUndoRedoCommand(command);
       case "cmd_renameFolder":
       {
         let folders = gFolderTreeView.getSelectedFolders();
@@ -564,36 +539,41 @@ var DefaultController =
         break;
       case "cmd_createFilterFromPopup":
         break;// This does nothing because the createfilter is invoked from the popupnode oncommand.
       case "button_delete":
       case "cmd_delete":
          // if the user deletes a message before its mark as read timer goes off, we should mark it as read
          // this ensures that we clear the biff indicator from the system tray when the user deletes the new message
         MarkSelectedMessagesRead(true);
-        SetNextMessageAfterDelete();
-        gDBView.doCommand(nsMsgViewCommandType.deleteMsg);
+        // If this is a right-click triggered delete, then do not hint about
+        //  the deletion.  Note: The code that swaps the selection back in will
+        //  take care of ensuring that this deletion does not make the saved
+        //  selection incorrect.
+        if (!gRightMouseButtonSavedSelection)
+          gFolderDisplay.hintAboutToDeleteMessages();
+        gFolderDisplay.doCommand(nsMsgViewCommandType.deleteMsg);
         break;
       case "cmd_shiftDelete":
         MarkSelectedMessagesRead(true);
-        SetNextMessageAfterDelete();
-        gDBView.doCommand(nsMsgViewCommandType.deleteNoTrash);
+        gFolderDisplay.hintAboutToDeleteMessages();
+        gFolderDisplay.doCommand(nsMsgViewCommandType.deleteNoTrash);
         break;
       case "cmd_deleteFolder":
         gFolderTreeController.deleteFolder();
         break;
       case "cmd_killThread":
         /* kill thread kills the thread and then does a next unread */
         GoNextMessage(nsMsgNavigationType.toggleThreadKilled, true);
         break;
       case "cmd_killSubthread":
         GoNextMessage(nsMsgNavigationType.toggleSubthreadKilled, true);
         break;
       case "cmd_watchThread":
-        gDBView.doCommand(nsMsgViewCommandType.toggleThreadWatched);
+        gFolderDisplay.doCommand(nsMsgViewCommandType.toggleThreadWatched);
         break;
       case "button_next":
       case "cmd_nextUnreadMsg":
         GoNextMessage(nsMsgNavigationType.nextUnreadMessage, true);
         break;
       case "cmd_nextUnreadThread":
         GoNextMessage(nsMsgNavigationType.nextUnreadThread, true);
         break;
@@ -634,20 +614,23 @@ var DefaultController =
         break;
       case "cmd_undo":
         messenger.undo(msgWindow);
         break;
       case "cmd_redo":
         messenger.redo(msgWindow);
         break;
       case "cmd_expandAllThreads":
-        gDBView.doCommand(nsMsgViewCommandType.expandAll);
+        gFolderDisplay.doCommand(nsMsgViewCommandType.expandAll);
+        gFolderDisplay.ensureSelectionIsVisible();
         break;
       case "cmd_collapseAllThreads":
-        gDBView.doCommand(nsMsgViewCommandType.collapseAll);
+        gFolderDisplay.selectSelectedThreadRoots();
+        gFolderDisplay.doCommand(nsMsgViewCommandType.collapseAll);
+        gFolderDisplay.ensureSelectionIsVisible();
         break;
       case "cmd_renameFolder":
         gFolderTreeController.renameFolder();
         return;
       case "cmd_sendUnsentMsgs":
         // if offline, prompt for sendUnsentMessages
         if (MailOfflineMgr.isOnline())
           SendUnsentMessages();
@@ -668,17 +651,17 @@ var DefaultController =
         return;
       case "cmd_saveAsFile":
         MsgSaveAsFile();
         return;
       case "cmd_saveAsTemplate":
         MsgSaveAsTemplate();
         return;
       case "cmd_viewPageSource":
-        ViewPageSource(GetSelectedMessages());
+        ViewPageSource(gFolderDisplay.selectedMessageUris);
         return;
       case "cmd_setFolderCharset":
         gFolderTreeController.editFolder();
         return;
       case "cmd_reload":
         ReloadMessage();
         return;
       case "cmd_find":
@@ -709,20 +692,20 @@ var DefaultController =
         MsgSearchMessages();
         return;
       case "button_mark":
       case "cmd_markAsRead":
         MsgMarkMsgAsRead();
         return;
       case "cmd_markThreadAsRead":
         ClearPendingReadTimer();
-        gDBView.doCommand(nsMsgViewCommandType.markThreadRead);
+        gFolderDisplay.doCommand(nsMsgViewCommandType.markThreadRead);
         return;
       case "cmd_markAllRead":
-        gDBView.doCommand(nsMsgViewCommandType.markAllRead);
+        gFolderDisplay.doCommand(nsMsgViewCommandType.markAllRead);
         return;
       case "button_junk":
         MsgJunk();
         return;
       case "cmd_stop":
         msgWindow.StopUrls();
         return;
       case "cmd_markAsFlagged":
@@ -754,20 +737,20 @@ var DefaultController =
         return;
       case "cmd_compactFolder":
         gFolderTreeController.compactAllFoldersForAccount();
         return;
       case "button_compact":
         gFolderTreeController.compactFolders();
         return;
       case "cmd_downloadFlagged":
-          gDBView.doCommand(nsMsgViewCommandType.downloadFlaggedForOffline);
+          gFolderDisplay.doCommand(nsMsgViewCommandType.downloadFlaggedForOffline);
           break;
       case "cmd_downloadSelected":
-          gDBView.doCommand(nsMsgViewCommandType.downloadSelectedForOffline);
+          gFolderDisplay.doCommand(nsMsgViewCommandType.downloadSelectedForOffline);
           break;
       case "cmd_synchronizeOffline":
           MsgSynchronizeOffline();
           break;
       case "cmd_settingsOffline":
           MailOfflineMgr.openOfflineAccountSettings();
           break;
       case "cmd_moveToFolderAgain":
@@ -776,27 +759,23 @@ var DefaultController =
             MsgMoveMessage(GetMsgFolderFromUri(folderId));
           else
             MsgCopyMessage(GetMsgFolderFromUri(folderId));
           break;
       case "cmd_selectAll":
           // move the focus so the user can delete the newly selected messages, not the folder
           SetFocusThreadPane();
           // if in threaded mode, the view will expand all before selecting all
-          gDBView.doCommand(nsMsgViewCommandType.selectAll)
-          if (gDBView.numSelected != 1) {
-              setTitleFromFolder(gDBView.msgFolder,null);
-              ClearMessagePane();
-          }
+          gFolderDisplay.doCommand(nsMsgViewCommandType.selectAll);
           break;
       case "cmd_selectThread":
-          gDBView.doCommand(nsMsgViewCommandType.selectThread);
+          gFolderDisplay.doCommand(nsMsgViewCommandType.selectThread);
           break;
       case "cmd_selectFlagged":
-        gDBView.doCommand(nsMsgViewCommandType.selectFlagged);
+        gFolderDisplay.doCommand(nsMsgViewCommandType.selectFlagged);
         break;
       case "cmd_fullZoomReduce":
         ZoomManager.reduce();
         break;
       case "cmd_fullZoomEnlarge":
         ZoomManager.enlarge();
         break;
       case "cmd_fullZoomReset":
--- a/mail/base/content/mailContextMenus.js
+++ b/mail/base/content/mailContextMenus.js
@@ -1,113 +1,77 @@
-# -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-#
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is Mozilla Communicator client code, released
-# March 31, 1998.
-#
-# The Initial Developer of the Original Code is
-# Netscape Communications Corporation.
-# Portions created by the Initial Developer are Copyright (C) 2000
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#   Jan Varga <varga@nixcorp.com>
-#   Hakan Waara <hwaara@chello.se>
-#   Markus Hossner <markushossner@gmx.de>
-#   Magnus Melin <mkmelin+mozilla@iki.fi>
-#
-# 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 *****
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Communicator client code, released
+ * March 31, 1998.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2000
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Jan Varga <varga@nixcorp.com>
+ *   Hakan Waara <hwaara@chello.se>
+ *   Markus Hossner <markushossner@gmx.de>
+ *   Magnus Melin <mkmelin+mozilla@iki.fi>
+ *
+ * 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 ***** */
 
 //NOTE: gMessengerBundle must be defined and set or this Overlay won't work
 
 Components.utils.import("resource://gre/modules/PluralForm.jsm");
 
 const mailtolength = 7;
 
 /**
  * Function to change the highlighted row back to the row that is currently
  * outline/dotted without loading the contents of either rows. This is
  * triggered when the context menu for a given row is hidden/closed
  * (onpopuphiding).
  * @param tree the tree element to restore selection for
  */
 function RestoreSelectionWithoutContentLoad(tree)
 {
-    if (!tree)
-      return;
-
-    // If a delete or move command had been issued, then we should
-    // reset gRightMouseButtonDown and gThreadPaneDeleteOrMoveOccurred
-    // and return (see bug 142065).
-    if(gThreadPaneDeleteOrMoveOccurred)
-    {
-      gRightMouseButtonDown = false;
-      gThreadPaneDeleteOrMoveOccurred = false;
-      return;
-    }
-
-    var treeSelection = tree.view.selection;
-
-    // make sure that currentIndex is valid so that we don't try to restore
-    // a selection of an invalid row.
-    if((!treeSelection.isSelected(treeSelection.currentIndex)) &&
-       (treeSelection.currentIndex >= 0))
-    {
-        treeSelection.selectEventsSuppressed = true;
-        treeSelection.select(treeSelection.currentIndex);
-        treeSelection.selectEventsSuppressed = false;
+  if (gRightMouseButtonSavedSelection) {
+    let view = gRightMouseButtonSavedSelection.view;
+    // restore the selection
+    let transientSelection = gRightMouseButtonSavedSelection.transientSelection;
+    let realSelection = gRightMouseButtonSavedSelection.realSelection;
+    view.selection = realSelection;
+    // replay any calls to adjustSelection, this handles suppression.
+    transientSelection.replayAdjustSelectionLog(realSelection);
+    gRightMouseButtonSavedSelection = null;
 
-        // Keep track of which row in the thread pane is currently selected.
-        // This is currently only needed when deleting messages.  See
-        // declaration of var in msgMail3PaneWindow.js.
-        if(tree.id == "threadTree")
-          gThreadPaneCurrentSelectedIndex = treeSelection.currentIndex;
-    }
-    else if(treeSelection.currentIndex < 0)
-        // Clear the selection in the case of when a folder has just been
-        // loaded where the message pane does not have a message loaded yet.
-        // When right-clicking a message in this case and dismissing the
-        // popup menu (by either executing a menu command or clicking
-        // somewhere else),  the selection needs to be cleared.
-        // However, if the 'Delete Message' or 'Move To' menu item has been
-        // selected, DO NOT clear the selection, else it will prevent the
-        // tree view from refreshing.
-        treeSelection.clearSelection();
-
-    // Need to reset gRightMouseButtonDown to false here because
-    // TreeOnMouseDown() is only called on a mousedown, not on a key down.
-    // So resetting it here allows the loading of messages in the messagepane
-    // when navigating via the keyboard or the toolbar buttons *after*
-    // the context menu has been dismissed.
-    gRightMouseButtonDown = false;
+    if (tree)
+      tree.treeBoxObject.invalidate();
+  }
 }
 
 /**
  * Function to clear out the global nsContextMenu, and in the case when we
  * were a threadpane context menu, restore the selection so that a right-click
  * on a non-selected row doesn't move the selection.
  * @param event the onpopuphiding event
  */
@@ -149,18 +113,18 @@ function fillMailContextMenu(event)
 
   var numSelected = GetNumSelectedMessages();
   if (numSelected == 0)
     return false; // Don't show the context menu if no items are selected.
 
   var inThreadPane = popupNodeIsInThreadPane();
   gContextMenu = new nsContextMenu(event.target);
 
-  var selectedMessage = GetFirstSelectedMessage();
-  var isNewsgroup = IsNewsMessage(selectedMessage);
+  var selectedMessage = gFolderDisplay.selectedMessage;
+  var isNewsgroup = gFolderDisplay.selectedMessageIsNews;
 
   // Clear the global var used to keep track if a 'Delete Message' or 'Move
   // To' command has been triggered via the thread pane context menu.
   gThreadPaneDeleteOrMoveOccurred = false;
 
   // Don't show mail items for links/images, just show related items.
   var hideMailItems = !inThreadPane &&
                       (gContextMenu.onImage || gContextMenu.onLink);
@@ -227,21 +191,20 @@ function fillMailContextMenu(event)
 
   ShowMenuItem("paneContext-afterMove", !inThreadPane);
 
   ShowMenuItem("mailContext-tags", !hideMailItems && msgFolder);
 
   ShowMenuItem("mailContext-mark", !hideMailItems && msgFolder);
 
   setSingleSelection("mailContext-saveAs");
-#ifdef XP_MACOSX
-  ShowMenuItem("mailContext-printpreview", false);
-#else
-  setSingleSelection("mailContext-printpreview");
-#endif
+  if (gPlatformOSX)
+    ShowMenuItem("mailContext-printpreview", false);
+  else
+    setSingleSelection("mailContext-printpreview");
 
   ShowMenuItem("mailContext-print", !hideMailItems);
 
   ShowMenuItem("mailContext-delete", !hideMailItems && (isNewsgroup || canMove));
   // This function is needed for the case where a folder is just loaded (while
   // there isn't a message loaded in the message pane), a right-click is done
   // in the thread pane.  This function will disable enable the 'Delete
   // Message' menu item.
@@ -390,21 +353,19 @@ function OpenMessageForMessageId(message
 
 function OpenMessageByHeader(messageHeader, openInNewWindow)
 {
   var folder    = messageHeader.folder;
   var folderURI = folder.URI;
 
   if (openInNewWindow)
   {
-    var messageURI = folder.getUriForMsg(messageHeader);
-
     window.openDialog("chrome://messenger/content/messageWindow.xul",
                       "_blank", "all,chrome,dialog=no,status,toolbar",
-                      messageURI, folderURI, null);
+                      messageHeader);
   }
   else
   {
     if (msgWindow.openFolder != folderURI)
       gFolderTreeView.selectFolder(folder);
 
     var tree = null;
     var wintype = document.documentElement.getAttribute('windowtype');
@@ -728,17 +689,17 @@ function SetMenuItemLabel(id, label)
 }
 
 // helper function used by shouldShowSeparator
 function hasAVisibleNextSibling(aNode)
 {
   var sibling = aNode.nextSibling;
   while (sibling)
   {
-    if (sibling.getAttribute("hidden") != "true" 
+    if (sibling.getAttribute("hidden") != "true"
         && sibling.localName != "menuseparator")
       return true;
     sibling = sibling.nextSibling;
   }
   return false;
 }
 
 function IsMenuItemShowing(menuID)
@@ -770,24 +731,24 @@ function composeEmailTo ()
   params.type = Components.interfaces.nsIMsgCompType.New;
   params.format = Components.interfaces.nsIMsgCompFormat.Default;
   params.identity = accountManager.getFirstIdentityForServer(GetLoadedMsgFolder().server);
   params.composeFields = fields;
   msgComposeService.OpenComposeWindowWithParams(null, params);
 }
 
 // Extracts email address from url string
-function getEmail (url) 
+function getEmail (url)
 {
   var qmark = url.indexOf( "?" );
   var addresses;
 
-  if ( qmark > mailtolength ) 
+  if ( qmark > mailtolength )
       addresses = url.substring( mailtolength, qmark );
-  else 
+  else
      addresses = url.substr( mailtolength );
   // Let's try to unescape it using a character set
   try {
     var characterSet = gContextMenu.target.ownerDocument.characterSet;
     const textToSubURI = Components.classes["@mozilla.org/intl/texttosuburi;1"]
                                  .getService(Components.interfaces.nsITextToSubURI);
     addresses = textToSubURI.unEscapeURIForUI(characterSet, addresses);
   }
--- a/mail/base/content/mailWindow.js
+++ b/mail/base/content/mailWindow.js
@@ -1,47 +1,46 @@
-# -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is Mozilla Communicator client code, released
-# March 31, 1998.
-#
-# The Initial Developer of the Original Code is
-# Netscape Communications Corporation.
-# Portions created by the Initial Developer are Copyright (C) 1998-1999
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#   Jan Varga <varga@nixcorp.com>
-#   HÃ¥kan Waara <hwaara@gmail.com>
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
+/** ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Communicator client code, released
+ * March 31, 1998.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1998-1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Jan Varga <varga@nixcorp.com>
+ *   HÃ¥kan Waara <hwaara@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
 
 Components.utils.import("resource://app/modules/appIdleManager.js");
 
 //This file stores variables common to mail windows
 var messenger;
 var pref;
 var statusFeedback;
 var msgWindow;
@@ -54,40 +53,45 @@ var gBrandBundle;
 
 Components.utils.import("resource://app/modules/gloda/log4moz.js");
 
 var gContextMenu;
 var gMailWindowLog = Log4Moz.getConfiguredLogger("mailWindow", Log4Moz.Level.Debug, Log4Moz.Level.Debug, Log4Moz.Level.Debug);
 var gAccountCentralLoaded = true;
 
 
+/**
+ * Indicate whether we are running on Mac OS X.  Our code is currently littered
+ *  with #ifdef/#ifndef XP_MACOSX's that do not need to exist in js code.  Use
+ *  of preprocessing makes error line numbers misleading, complicates
+ *  development because preprocessed files can't be symlinked when using
+ *  --enable-chrome-format=symlink, etc.
+ */
+var gPlatformOSX =
+  (window.navigator.oscpu.substring(0, 3).toLowerCase() == "mac");
+
+/**
+ * Called by messageWindow.xul:onunload,  the 'single message display window'.
+ *
+ * Also called by messenger.xul:onunload's (the 3-pane window inside of tabs
+ *  window) unload function, OnUnloadMessenger.
+ */
 function OnMailWindowUnload()
 {
   MailOfflineMgr.uninit();
   ClearPendingReadTimer();
 
-  var searchSession = GetSearchSession();
-  if (searchSession)
-  {
-    removeGlobalListeners();
-    if (gPreQuickSearchView)     //close the cached pre quick search view
-      gPreQuickSearchView.close();
-  }
-
-  var dbview = GetDBView();
-  if (dbview) {
-    dbview.close();
-  }
+  // all dbview closing is handled by OnUnloadMessenger for the 3-pane (it closes
+  //  the tabs which close their views) and OnUnloadMessageWindow for the
+  //  standalone message window.
 
   var mailSession = Components.classes["@mozilla.org/messenger/services/session;1"]
                               .getService(Components.interfaces.nsIMsgMailSession);
-  mailSession.RemoveFolderListener(folderListener);
-
   mailSession.RemoveMsgWindow(msgWindow);
-  messenger.setWindow(null, null);
+  // the tabs have the FolderDisplayWidget close their 'messenger' instances for us
 
   msgWindow.closeWindow();
 
   window.MsgStatusFeedback.unload();
   Components.classes["@mozilla.org/activity-manager;1"]
             .getService(Components.interfaces.nsIActivityManager)
             .removeListener(window.MsgStatusFeedback);
 }
@@ -98,17 +102,17 @@ function CreateMailWindowGlobals()
   messenger = Components.classes["@mozilla.org/messenger;1"]
                         .createInstance(Components.interfaces.nsIMessenger);
 
   pref = Components.classes["@mozilla.org/preferences-service;1"]
           .getService(Components.interfaces.nsIPrefBranch2);
 
   window.addEventListener("blur", appIdleManager.onBlur, false);
   window.addEventListener("focus", appIdleManager.onFocus, false);
-  
+
   //Create windows status feedback
   // set the JS implementation of status feedback before creating the c++ one..
   window.MsgStatusFeedback = new nsMsgStatusFeedback();
   // double register the status feedback object as the xul browser window implementation
   window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
         .getInterface(Components.interfaces.nsIWebNavigation)
         .QueryInterface(Components.interfaces.nsIDocShellTreeItem).treeOwner
         .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
@@ -156,17 +160,17 @@ function InitMsgWindow()
 }
 
 // We're going to implement our status feedback for the mail window in JS now.
 // the following contains the implementation of our status feedback object
 
 function nsMsgStatusFeedback()
 {
   this._statusText = document.getElementById("statusText");
-  this._progressBar = document.getElementById("statusbar-icon"); 
+  this._progressBar = document.getElementById("statusbar-icon");
   this._progressBarContainer = document.getElementById("statusbar-progresspanel");
   this._throbber = document.getElementById("navigator-throbber");
   this._stopCmd = document.getElementById("cmd_stop");
   this._activeProcesses = new Array();
 }
 
 nsMsgStatusFeedback.prototype =
 {
@@ -351,17 +355,17 @@ nsMsgStatusFeedback.prototype =
         this._progressBarContainer.removeAttribute('collapsed');
         this._progressBarVisible = true;
       }
     }
     else {
       // Stop the bar spinning as we're not doing anything now.
       this._progressBar.setAttribute("mode", "determined");
       this._progressBar.value = 0;
-      this._progressBar.label = ""; 
+      this._progressBar.label = "";
 
       if (this._progressBarVisible) {
         this._progressBarContainer.collapsed = true;
         this._progressBarVisible = false;
       }
     }
   },
 
@@ -437,25 +441,26 @@ nsMsgWindowCommands.prototype =
 
   selectFolder: function(folderUri)
   {
     gFolderTreeView.selectFolder(GetMsgFolderFromUri(folderUri));
   },
 
   selectMessage: function(messageUri)
   {
-    SelectMessage(messageUri);
+    let msgHdr = messenger.msgHdrFromURI(messageUri);
+    gFolderDisplay.selectMessage(msgHdr);
   },
 
   clearMsgPane: function()
   {
-    if (gDBView)
-      setTitleFromFolder(gDBView.msgFolder,null);
-    else
-      setTitleFromFolder(null,null);
+    // This call happens as part of a display decision made by the nsMsgDBView
+    //  instance.  Strictly speaking, we don't want this.  I think davida's
+    //  patch will change this, so we can figure it out after that lands if
+    //  there are issues.
     ClearMessagePane();
   }
 }
 
 /**
  * @returns the pref name to use for fetching the start page url. Every time the application version changes,
  * return "mailnews.start_page.override_url". If this is the first time the application has been
  * launched, return "mailnews.start_page.welcome_url". Otherwise return "mailnews.start_page.url".
@@ -475,17 +480,16 @@ function startPageUrlPref()
 }
 
 /**
  * Loads the mail start page.
  */
 function loadStartPage()
 {
   gMessageNotificationBar.clearMsgNotifications();
-  ClearThreadPaneSelection();
   let startpage = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
                             .getService(Components.interfaces.nsIURLFormatter)
                             .formatURLPref(startPageUrlPref());
   if (startpage)
   {
     try {
       let urifixup = Components.classes["@mozilla.org/docshell/urifixup;1"]
                                .getService(Components.interfaces.nsIURIFixup);
@@ -498,67 +502,50 @@ function loadStartPage()
     }
   }
   else
   {
     GetMessagePaneFrame().location.href = "about:blank";
   }
 }
 
-// When the ThreadPane is hidden via the displayDeck, we should collapse the
-// elements that are only meaningful to the thread pane. When AccountCentral is
-// shown via the displayDeck, we need to switch the displayDeck to show the
-// accountCentralBox, and load the iframe in the AccountCentral box with
-// corresponding page.
-function ShowAccountCentral()
-{
-  var accountBox = document.getElementById("accountCentralBox");
-  document.getElementById("displayDeck").selectedPanel = accountBox;
-  var prefName = "mailnews.account_central_page.url";
-  var acctCentralPage = pref.getComplexValue(prefName,
-                                             Components.interfaces.nsIPrefLocalizedString).data;
-  window.frames["accountCentralPane"].location.href = acctCentralPage;
-}
-
-function ShowThreadPane()
-{
-  document.getElementById("displayDeck").selectedPanel =
-    document.getElementById("threadPaneBox");
-}
-
 function ShowingThreadPane()
 {
   var threadPaneSplitter = document.getElementById("threadpane-splitter");
   threadPaneSplitter.collapsed = false;
   GetMessagePane().collapsed = (threadPaneSplitter.getAttribute("state") == "collapsed");
   // XXX We need to force the tree to refresh its new height
   // so that it will correctly scroll to the newest message
   GetThreadTree().boxObject.height;
   document.getElementById("key_toggleMessagePane").removeAttribute("disabled");
 }
 
 function HidingThreadPane()
 {
-  ClearThreadPane();
   GetUnreadCountElement().hidden = true;
   GetTotalCountElement().hidden = true;
   GetMessagePane().collapsed = true;
   document.getElementById("threadpane-splitter").collapsed = true;
   document.getElementById("key_toggleMessagePane").setAttribute("disabled", "true");
 }
 
 // The zoom manager, view source and possibly some other functions still rely
 // on the getBrowser function.
 function getBrowser()
 {
   let tabmail = document.getElementById('tabmail');
   return tabmail ? tabmail.getBrowserForSelectedTab() :
                    document.getElementById("messagepane");
 }
 
+// When the ThreadPane is hidden via the displayDeck, we should collapse the
+// elements that are only meaningful to the thread pane. When AccountCentral is
+// shown via the displayDeck, we need to switch the displayDeck to show the
+// accountCentralBox, and load the iframe in the AccountCentral box with
+// corresponding page.
 var gCurrentDisplayDeckId = "";
 function ObserveDisplayDeckChange(event)
 {
   var selectedPanel = document.getElementById("displayDeck").selectedPanel;
   var nowSelected = selectedPanel ? selectedPanel.id : null;
   // onselect fires for every mouse click inside the deck, so ObserveDisplayDeckChange is getting called every time we click
   // on a message in the thread pane. Only show / Hide elements if the selected deck is actually changing.
   if (nowSelected != gCurrentDisplayDeckId)
@@ -579,33 +566,24 @@ function ObserveDisplayDeckChange(event)
   }
 }
 
 // Given the server, open the twisty and the set the selection
 // on inbox of that server.
 // prompt if offline.
 function OpenInboxForServer(server)
 {
-  ShowThreadPane();
   gFolderTreeView.selectFolder(GetInboxFolder(server));
 
   if (MailOfflineMgr.isOnline() || MailOfflineMgr.getNewMail()) {
     if (server.type != "imap")
       GetMessagesForInboxOnServer(server);
   }
 }
 
-function GetSearchSession()
-{
-  if (("gSearchSession" in top) && gSearchSession)
-    return gSearchSession;
-  else
-    return null;
-}
-
 /** Update state of zoom type (text vs. full) menu item. */
 function UpdateFullZoomMenu() {
   var menuItem = document.getElementById("menu_fullZoomToggle");
   menuItem.setAttribute("checked", !ZoomManager.useFullZoom);
 }
 
 /**
  * This class implements nsIBadCertListener2.  Its job is to prevent "bad cert"
--- a/mail/base/content/mailWindowOverlay.js
+++ b/mail/base/content/mailWindowOverlay.js
@@ -1,55 +1,54 @@
-# -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is Mozilla Communicator client code, released
-# March 31, 1998.
-#
-# The Initial Developer of the Original Code is
-# Netscape Communications Corporation.
-# Portions created by the Initial Developer are Copyright (C) 1998-1999
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#   timeless
-#   slucy@objectivesw.co.uk
-#   HÃ¥kan Waara <hwaara@chello.se>
-#   Jan Varga <varga@nixcorp.com>
-#   Seth Spitzer <sspitzer@netscape.com>
-#   David Bienvenu <bienvenu@nventure.com>
-#   Karsten Düsterloh <mnyromyr@tprac.de>
-#   Christopher Thomas <cst@yecc.com>
-#   Jeremy Morton <bugzilla@game-point.net>
-#   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 *****
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Communicator client code, released
+ * March 31, 1998.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1998-1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   timeless
+ *   slucy@objectivesw.co.uk
+ *   HÃ¥kan Waara <hwaara@chello.se>
+ *   Jan Varga <varga@nixcorp.com>
+ *   Seth Spitzer <sspitzer@netscape.com>
+ *   David Bienvenu <bienvenu@nventure.com>
+ *   Karsten Düsterloh <mnyromyr@tprac.de>
+ *   Christopher Thomas <cst@yecc.com>
+ *   Jeremy Morton <bugzilla@game-point.net>
+ *   Andrew Sutherland <asutherland@asutherland.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
 
 const ADDR_DB_LARGE_COMMIT       = 1;
 
 const kClassicMailLayout = 0;
 const kWideMailLayout = 1;
 const kVerticalMailLayout = 2;
 
 // Per message header flags to keep track of whether the user is allowing remote
@@ -153,17 +152,17 @@ function InitEditMessagesMenu()
 
 function InitGoMessagesMenu()
 {
   document.commandDispatcher.updateCommands('create-menu-go');
 }
 
 function view_init()
 {
-  var isFeed = IsFeedItem();
+  var isFeed = gFolderDisplay.selectedMessageIsFeed;
 
   if (!gMessengerBundle)
     gMessengerBundle = document.getElementById("bundle_messenger");
 
   var messagePaneMenuItem = document.getElementById("menu_showMessage");
   if (!messagePaneMenuItem.hidden) { // Hidden in the standalone msg window.
     messagePaneMenuItem.setAttribute("checked", !IsMessagePaneCollapsed());
     messagePaneMenuItem.disabled = gAccountCentralLoaded;
@@ -232,105 +231,106 @@ function InitViewFolderViewsMenu(event)
 
 function setSortByMenuItemCheckState(id, value)
 {
   var menuitem = document.getElementById(id);
   if (menuitem)
     menuitem.setAttribute("checked", value);
 }
 
+/**
+ * Called when showing the menu_viewSortPopup menupopup, so it should always
+ * be up-to-date.
+ */
 function InitViewSortByMenu()
 {
-  var sortType = gDBView.sortType;
+  var sortType = gFolderDisplay.view.primarySortType;
 
   setSortByMenuItemCheckState("sortByDateMenuitem", (sortType == nsMsgViewSortType.byDate));
   setSortByMenuItemCheckState("sortByReceivedMenuitem", (sortType == nsMsgViewSortType.byReceived));
   setSortByMenuItemCheckState("sortByFlagMenuitem", (sortType == nsMsgViewSortType.byFlagged));
   setSortByMenuItemCheckState("sortByOrderReceivedMenuitem", (sortType == nsMsgViewSortType.byId));
   setSortByMenuItemCheckState("sortByPriorityMenuitem", (sortType == nsMsgViewSortType.byPriority));
   setSortByMenuItemCheckState("sortBySizeMenuitem", (sortType == nsMsgViewSortType.bySize));
   setSortByMenuItemCheckState("sortByStatusMenuitem", (sortType == nsMsgViewSortType.byStatus));
   setSortByMenuItemCheckState("sortBySubjectMenuitem", (sortType == nsMsgViewSortType.bySubject));
   setSortByMenuItemCheckState("sortByUnreadMenuitem", (sortType == nsMsgViewSortType.byUnread));
   setSortByMenuItemCheckState("sortByTagsMenuitem", (sortType == nsMsgViewSortType.byTags));
   setSortByMenuItemCheckState("sortByJunkStatusMenuitem", (sortType == nsMsgViewSortType.byJunkStatus));
   setSortByMenuItemCheckState("sortByFromMenuitem", (sortType == nsMsgViewSortType.byAuthor));
   setSortByMenuItemCheckState("sortByRecipientMenuitem", (sortType == nsMsgViewSortType.byRecipient));
   setSortByMenuItemCheckState("sortByAttachmentsMenuitem", (sortType == nsMsgViewSortType.byAttachments));
 
-  var sortOrder = gDBView.sortOrder;
+  var sortOrder = gFolderDisplay.view.primarySortOrder;
   var sortTypeSupportsGrouping = (sortType == nsMsgViewSortType.byAuthor ||
       sortType == nsMsgViewSortType.byDate || sortType == nsMsgViewSortType.byReceived ||
       sortType == nsMsgViewSortType.byPriority ||
       sortType == nsMsgViewSortType.bySubject || sortType == nsMsgViewSortType.byTags ||
       sortType == nsMsgViewSortType.byRecipient || sortType == nsMsgViewSortType.byAccount ||
       sortType == nsMsgViewSortType.byStatus || sortType == nsMsgViewSortType.byFlagged ||
       sortType == nsMsgViewSortType.byAttachments);
 
   setSortByMenuItemCheckState("sortAscending", (sortOrder == nsMsgViewSortOrder.ascending));
   setSortByMenuItemCheckState("sortDescending", (sortOrder == nsMsgViewSortOrder.descending));
 
-  var grouped = ((gDBView.viewFlags & nsMsgViewFlagsType.kGroupBySort) != 0);
-  var threaded = ((gDBView.viewFlags & nsMsgViewFlagsType.kThreadedDisplay) != 0 && !grouped);
+  var grouped = gFolderDisplay.view.showGroupedBySort;
+  var threaded = gFolderDisplay.view.showThreaded;
   var sortThreadedMenuItem = document.getElementById("sortThreaded");
   var sortUnthreadedMenuItem = document.getElementById("sortUnthreaded");
 
   sortThreadedMenuItem.setAttribute("checked", threaded);
   sortUnthreadedMenuItem.setAttribute("checked", !threaded && !grouped);
 
   var groupBySortOrderMenuItem = document.getElementById("groupBySort");
 
   groupBySortOrderMenuItem.setAttribute("disabled", !sortTypeSupportsGrouping);
   groupBySortOrderMenuItem.setAttribute("checked", grouped);
 }
 
 function InitViewMessagesMenu()
 {
-  var viewFlags = (gDBView) ? gDBView.viewFlags : 0;
-  var viewType = (gDBView) ? gDBView.viewType : 0;
-
   document.getElementById("viewAllMessagesMenuItem").setAttribute("checked",
-    (viewFlags & nsMsgViewFlagsType.kUnreadOnly) == 0 &&
-    (viewType == nsMsgViewType.eShowAllThreads));
+    !gFolderDisplay.view.showUnreadOnly &&
+    !gFolderDisplay.view.specialView);
 
   document.getElementById("viewUnreadMessagesMenuItem").setAttribute("checked",
-    (viewFlags & nsMsgViewFlagsType.kUnreadOnly) != 0);
+    gFolderDisplay.view.showUnreadOnly);
 
   document.getElementById("viewThreadsWithUnreadMenuItem").setAttribute("checked",
-    viewType == nsMsgViewType.eShowThreadsWithUnread);
+    gFolderDisplay.view.specialViewThreadsWithUnread);
 
   document.getElementById("viewWatchedThreadsWithUnreadMenuItem").setAttribute("checked",
-    viewType == nsMsgViewType.eShowWatchedThreadsWithUnread);
+    gFolderDisplay.view.specialViewWatchedThreadsWithUnread);
 
   document.getElementById("viewIgnoredThreadsMenuItem").setAttribute("checked",
-    (viewFlags & nsMsgViewFlagsType.kShowIgnored) != 0);
+    gFolderDisplay.view.showIgnored);
 }
 
 function InitMessageMenu()
 {
-  var selectedMsg = GetFirstSelectedMessage();
-  var isNews = IsNewsMessage(selectedMsg);
-  var isFeed = IsFeedItem();
+  var selectedMsg = gFolderDisplay.selectedMessage;
+  var isNews = gFolderDisplay.selectedMessageIsNews;
+  var isFeed = gFolderDisplay.selectedMessageIsFeed;
 
   // We show reply to Newsgroups only for news messages.
   document.getElementById("replyNewsgroupMainMenu").hidden = !isNews;
 
   // For mail messages we say reply. For news we say ReplyToSender.
   document.getElementById("replyMainMenu").hidden = isNews;
   document.getElementById("replySenderMainMenu").hidden = !isNews;
 
   // We only kill and watch threads for news.
   document.getElementById("threadItemsSeparator").hidden = !isNews;
   document.getElementById("killThread").hidden = !isNews;
   document.getElementById("killSubthread").hidden = !isNews;
   document.getElementById("watchThread").hidden = !isNews;
 
   // Disable the move and copy menus if there are no messages selected.
   // Disable the move menu if we can't delete msgs from the folder.
-  var msgFolder = GetLoadedMsgFolder();
+  var msgFolder = gFolderDisplay.displayedFolder;
   var enableMenuItem = selectedMsg && msgFolder && msgFolder.canDeleteMessages;
   document.getElementById("moveMenu").disabled = !enableMenuItem;
 
   // Also disable copy when no folder is loaded (like for .eml files).
   document.getElementById("copyMenu").disabled = !(selectedMsg && msgFolder);
 
   initMoveToFolderAgainMenu(document.getElementById("moveToFolderAgain"));
 
@@ -411,17 +411,17 @@ function InitViewHeadersMenu()
     menuitem.setAttribute("checked", "true");
 }
 
 function InitViewBodyMenu()
 {
   var html_as = 0;
   var prefer_plaintext = false;
   var disallow_classes = 0;
-  var isFeed = IsFeedItem();
+  var isFeed = gFolderDisplay.selectedMessageIsFeed;
   const defaultIDs = ["bodyAllowHTML",
                       "bodySanitized",
                       "bodyAsPlaintext"];
   const rssIDs = ["bodyFeedSummaryAllowHTML",
                   "bodyFeedSummarySanitized",
                   "bodyFeedSummaryAsPlaintext"];
   var menuIDs = isFeed ? rssIDs : defaultIDs;
   try
@@ -465,45 +465,27 @@ function InitViewBodyMenu()
   if (isFeed) {
     AllowHTML_menuitem.hidden = !gShowFeedSummary;
     Sanitized_menuitem.hidden = !gShowFeedSummary;
     AsPlaintext_menuitem.hidden = !gShowFeedSummary;
     document.getElementById("viewFeedSummarySeparator").hidden = !gShowFeedSummary;
   }
 }
 
-function IsNewsMessage(messageUri)
-{
-  return (/^news-message:/.test(messageUri));
-}
-
-function IsImapMessage(messageUri)
-{
-  return (/^imap-message:/.test(messageUri));
-}
-
-function IsFeedItem()
-{
-  return (GetFirstSelectedMessage() &&
-          ((gMsgFolderSelected &&
-            gMsgFolderSelected.server.type == 'rss') ||
-           'content-base' in currentHeaderData));
-}
-
 function SetMenuItemLabel(menuItemId, customLabel)
 {
   var menuItem = document.getElementById(menuItemId);
   if (menuItem)
     menuItem.setAttribute('label', customLabel);
 }
 
 function RemoveAllMessageTags()
 {
-  var selectedMsgUris = GetSelectedMessages();
-  if (!selectedMsgUris.length)
+  var selectedMessages = gFolderDisplay.selectedMessages;
+  if (!selectedMessages.length)
     return;
 
   var messages = Components.classes["@mozilla.org/array;1"]
                            .createInstance(Components.interfaces.nsIMutableArray);
   var tagService = Components.classes["@mozilla.org/messenger/tagservice;1"]
                              .getService(Components.interfaces.nsIMsgTagService);
   var tagArray = tagService.getAllTags({});
 
@@ -518,19 +500,19 @@ function RemoveAllMessageTags()
   var prevHdrFolder = null;
   // this crudely handles cross-folder virtual folders with selected messages
   // that spans folders, by coalescing consecutive messages in the selection
   // that happen to be in the same folder. nsMsgSearchDBView does this better,
   // but nsIMsgDBView doesn't handle commands with arguments, and untag takes a
   // key argument. Furthermore, we only delete legacy labels and known tags,
   // keeping other keywords like (non)junk intact.
 
-  for (var i = 0; i < selectedMsgUris.length; ++i)
+  for (var i = 0; i < selectedMessages.length; ++i)
   {
-    var msgHdr = messenger.msgHdrFromURI(selectedMsgUris[i]);
+    var msgHdr = selectedMessages[i];
     msgHdr.label = 0; // remove legacy label
     if (prevHdrFolder != msgHdr.folder)
     {
       if (prevHdrFolder)
         prevHdrFolder.removeKeywordsFromMessages(messages, allKeys);
       messages.clear();
       prevHdrFolder = msgHdr.folder;
     }
@@ -542,17 +524,17 @@ function RemoveAllMessageTags()
 }
 
 function ToggleMessageTagKey(index)
 {
   if (GetNumSelectedMessages() < 1)
     return;
   // set the tag state based upon that of the first selected message,
   // just like we do for markAsRead etc.
-  var msgHdr = gDBView.hdrForFirstSelectedMessage;
+  var msgHdr = gFolderDisplay.selectedMessage;
   var tagService = Components.classes["@mozilla.org/messenger/tagservice;1"]
                              .getService(Components.interfaces.nsIMsgTagService);
   var tagArray = tagService.getAllTags({});
   for (var i = 0; i < tagArray.length; ++i)
   {
     var key = tagArray[i].key;
     if (!--index)
     {
@@ -575,27 +557,27 @@ function ToggleMessageTagMenu(target)
 }
 
 function ToggleMessageTag(key, addKey)
 {
   var messages = Components.classes["@mozilla.org/array;1"]
                            .createInstance(Components.interfaces.nsIMutableArray);
   var msg = Components.classes["@mozilla.org/array;1"]
                       .createInstance(Components.interfaces.nsIMutableArray);
-  var selectedMsgUris = GetSelectedMessages();
+  var selectedMessages = gFolderDisplay.selectedMessages;
   var toggler = addKey ? "addKeywordsToMessages" : "removeKeywordsFromMessages";
   var prevHdrFolder = null;
   // this crudely handles cross-folder virtual folders with selected messages
   // that spans folders, by coalescing consecutive msgs in the selection
   // that happen to be in the same folder. nsMsgSearchDBView does this
   // better, but nsIMsgDBView doesn't handle commands with arguments,
   // and (un)tag takes a key argument.
-  for (var i = 0; i < selectedMsgUris.length; ++i)
+  for (var i = 0; i < selectedMessages.length; ++i)
   {
-    var msgHdr = messenger.msgHdrFromURI(selectedMsgUris[i]);
+    var msgHdr = selectedMessages[i];
     if (msgHdr.label)
     {
       // Since we touch all these messages anyway, migrate the label now.
       // If we don't, the thread tree won't always show the correct tag state,
       // because resetting a label doesn't update the tree anymore...
       msg.clear();
       msg.appendElement(msgHdr, false);
       msgHdr.folder.addKeywordsToMessages(msg, "$label" + msgHdr.label);
@@ -643,17 +625,17 @@ function AddTagCallback(name, color)
 function SetMessageTagLabel(menuitem, index, name)
 {
   // if a <key> is defined for this tag, use its key as the accesskey
   // (the key for the tag at index n needs to have the id key_tag<n>)
   var shortcutkey = document.getElementById("key_tag" + index);
   var accesskey = shortcutkey ? shortcutkey.getAttribute("key") : "";
   if (accesskey)
     menuitem.setAttribute("accesskey", accesskey);
-  var label = gMessengerBundle.getFormattedString("mailnews.tags.format", 
+  var label = gMessengerBundle.getFormattedString("mailnews.tags.format",
                                                   [accesskey, name]);
   menuitem.setAttribute("label", label);
 }
 
 function InitMessageTags(menuPopup)
 {
   var tagService = Components.classes["@mozilla.org/messenger/tagservice;1"]
                              .getService(Components.interfaces.nsIMsgTagService);
@@ -668,17 +650,17 @@ function InitMessageTags(menuPopup)
   // hide double menuseparator
   menuseparator.previousSibling.hidden = !tagCount;
 
   // create label and accesskey for the static remove item
   var tagRemoveLabel = gMessengerBundle.getString("mailnews.tags.remove");
   SetMessageTagLabel(menuPopup.firstChild, 0, tagRemoveLabel);
 
   // now rebuild the list
-  var msgHdr = gDBView.hdrForFirstSelectedMessage;
+  var msgHdr = gFolderDisplay.selectedMessage;
   var curKeys = msgHdr.getStringProperty("keywords");
   if (msgHdr.label)
     curKeys += " $label" + msgHdr.label;
 
   for (var i = 0; i < tagCount; ++i)
   {
     var taginfo = tagArray[i];
     // TODO we want to either remove or "check" the tags that already exist
@@ -686,17 +668,17 @@ function InitMessageTags(menuPopup)
     SetMessageTagLabel(newMenuItem, i + 1, taginfo.tag);
     newMenuItem.setAttribute("value", taginfo.key);
     newMenuItem.setAttribute("type", "checkbox");
     var removeKey = (" " + curKeys + " ").indexOf(" " + taginfo.key + " ") > -1;
     newMenuItem.setAttribute('checked', removeKey);
     newMenuItem.setAttribute('oncommand', 'ToggleMessageTagMenu(event.target);');
     var color = taginfo.color;
     if (color)
-      newMenuItem.setAttribute("class", "lc-" + color.substr(1));    
+      newMenuItem.setAttribute("class", "lc-" + color.substr(1));
     menuPopup.insertBefore(newMenuItem, menuseparator);
   }
 }
 
 function backToolbarMenu_init(menuPopup)
 {
   populateHistoryMenu(menuPopup, true);
 }
@@ -722,38 +704,43 @@ function populateHistoryMenu(menuPopup, 
   var numEntries = new Object;
   var historyEntries = new Object;
   messenger.getNavigateHistory(curPos, numEntries, historyEntries);
   curPos.value = curPos.value * 2;
   navDebug("curPos = " + curPos.value + " numEntries = " + numEntries.value + "\n");
   var historyArray = historyEntries.value;
   var folder;
   var newMenuItem;
-  if (GetLoadedMessage())
+  if (gFolderDisplay.selectedMessage)
   {
     if (!isBackMenu)
       curPos.value += 2;
     else
       curPos.value -= 2;
   }
   // For populating the back menu, we want the most recently visited
   // messages first in the menu. So we go backward from curPos to 0.
   // For the forward menu, we want to go forward from curPos to the end.
   var relPos = 0;
   for (var i = curPos.value; (isBackMenu) ? i >= 0 : i < historyArray.length; i += ((isBackMenu) ? -2 : 2))
   {
     navDebug("history[" + i + "] = " + historyArray[i] + "\n");
     navDebug("history[" + i + "] = " + historyArray[i + 1] + "\n");
-    folder = GetMsgFolderFromUri(historyArray[i + 1])
+    folder = GetMsgFolderFromUri(historyArray[i + 1]);
     navDebug("folder URI = " + folder.URI + "pretty name " + folder.prettyName + "\n");
     var menuText = "";
 
+    // If the message was not being displayed via the current folder, prepend
+    //  the folder name.  We do not need to check underlying folders for
+    //  virtual folders because 'folder' is the display folder, not the
+    //  underlying one.
+    if (folder != gFolderDisplay.displayedFolder)
+      menuText = folder.prettyName + " - ";
+
     var msgHdr = messenger.msgHdrFromURI(historyArray[i]);
-    if (!IsCurrentLoadedFolder(folder))
-      menuText = folder.prettyName + " - ";
 
     var subject = "";
     if (msgHdr.flags & Components.interfaces.nsMsgMessageFlags.HasRe)
       subject = "Re: ";
     if (msgHdr.mime2DecodedSubject)
       subject += msgHdr.mime2DecodedSubject;
     if (subject)
       menuText += subject + " - ";
@@ -766,27 +753,38 @@ function populateHistoryMenu(menuPopup, 
     newMenuItem.folder = folder;
     newMenuItem.setAttribute('oncommand', 'NavigateToUri(event.target); event.stopPropagation();');
     menuPopup.appendChild(newMenuItem);
     if (! (relPos % 20))
       break;
   }
 }
 
+/**
+ * This is triggered by the history navigation menu options, as created by
+ *  populateHistoryMenu above.
+ */
 function NavigateToUri(target)
 {
   var historyIndex = target.getAttribute('value');
   var msgUri = messenger.getMsgUriAtNavigatePos(historyIndex);
   var folder = target.folder;
   var msgHdr = messenger.msgHdrFromURI(msgUri);
   navDebug("navigating from " + messenger.navigatePos + " by " + historyIndex + " to " + msgUri + "\n");
 
   // this "- 0" seems to ensure that historyIndex is treated as an int, not a string.
   messenger.navigatePos += (historyIndex - 0);
-  LoadNavigatedToMessage(msgHdr, folder, folder.URI);
+
+  if (gFolderDisplay.displayedFolder != folder) {
+    if (gFolderTreeView)
+      gFolderTreeView.selectFolder(folder);
+    else
+      gFolderDisplay.show(folder);
+  }
+  gFolderDisplay.selectMessage(msgHdr);
 }
 
 function forwardToolbarMenu_init(menuPopup)
 {
   populateHistoryMenu(menuPopup, false);
 }
 
 function InitMessageMark()
@@ -804,33 +802,33 @@ function UpdateJunkToolbarButton()
 {
   var junkButtonDeck = document.getElementById("junk-deck");
   if (junkButtonDeck)
     junkButtonDeck.selectedIndex = SelectedMessagesAreJunk() ? 1 : 0;
 }
 
 function UpdateReplyButtons()
 {
-  let msgHdr = messenger.msgHdrFromURI(GetLoadedMessage());
+  let msgHdr = gFolderDisplay.selectedMessage;
 
   let myEmail = getIdentityForHeader(msgHdr).email;
   let recipients = msgHdr.recipients + "," + msgHdr.ccList;
 
   // If my email address isn't in the to or cc list, then I've been bcc-ed.
   let imBcced = recipients.indexOf(myEmail) == -1;
 
   // Now, let's get the number of unique recipients
   let hdrParser = Components.classes["@mozilla.org/messenger/headerparser;1"]
                             .getService(Components.interfaces.nsIMsgHeaderParser);
   let uniqueRecipients = hdrParser.removeDuplicateAddresses(recipients, {});
   let numAddresses = hdrParser.parseHeadersWithArray(uniqueRecipients, {}, {}, {});
 
   // If I've been bcc-ed, then add 1 to the number of addresses to compensate.
   if (imBcced)
-    numAddresses++
+    numAddresses++;
 
   // By default, ReplyAll if there is more than 1 person to reply to.
   let showReplyAll = numAddresses > 1;
 
   // And ReplyToList if there is a List-Post header.
   let showReplyList = currentHeaderData["list-post"];
 
   // Get the server type.
@@ -913,57 +911,57 @@ function UpdateDeleteToolbarButton()
       GetNumSelectedMessages() == 0)
     deleteButtonDeck.selectedIndex = 0;
   else
     deleteButtonDeck.selectedIndex = SelectedMessagesAreDeleted() ? 1 : 0;
 }
 function UpdateDeleteCommand()
 {
   var value = "value";
-  var uri = GetFirstSelectedMessage();
-  if (IsNewsMessage(uri))
+  if (gFolderDisplay.selectedMessageIsNews)
     value += "News";
   else if (SelectedMessagesAreDeleted())
     value += "IMAPDeleted";
   if (GetNumSelectedMessages() < 2)
     value += "Message";
   else
     value += "Messages";
   goSetMenuValue("cmd_delete", value);
   goSetAccessKey("cmd_delete", value + "AccessKey");
 }
 
 function SelectedMessagesAreDeleted()
 {
-  return gDBView && gDBView.numSelected &&
-         (gDBView.hdrForFirstSelectedMessage.flags &
+  let firstSelectedMessage = gFolderDisplay.selectedMessage;
+  return firstSelectedMessage &&
+         (firstSelectedMessage.flags &
           Components.interfaces.nsMsgMessageFlags.IMAPDeleted);
 }
 
 function SelectedMessagesAreJunk()
 {
   try {
-    var junkScore = gDBView.hdrForFirstSelectedMessage.getStringProperty("junkscore");
+    var junkScore = gFolderDisplay.selectedMessage.getStringProperty("junkscore");
     return (junkScore != "") && (junkScore != "0");
   }
   catch (ex) {
     return false;
   }
 }
 
 function SelectedMessagesAreRead()
 {
-  return gDBView && gDBView.numSelected &&
-         gDBView.hdrForFirstSelectedMessage.isRead;
+  let firstSelectedMessage = gFolderDisplay.selectedMessage;
+  return firstSelectedMessage && firstSelectedMessage.isRead;
 }
 
 function SelectedMessagesAreFlagged()
 {
-  return gDBView && gDBView.numSelected &&
-         gDBView.hdrForFirstSelectedMessage.isFlagged;
+  let firstSelectedMessage = gFolderDisplay.selectedMessage;
+  return firstSelectedMessage && firstSelectedMessage.isFlagged;
 }
 
 function GetFirstSelectedMsgFolder()
 {
   var selectedFolders = GetSelectedMsgFolders();
   return (selectedFolders.length > 0) ? selectedFolders[0] : null;
 }
 
@@ -1092,20 +1090,20 @@ function MsgGetNextNMessages()
   if (MailOfflineMgr.isOnline() || MailOfflineMgr.getNewMail())
     GetNextNMessages(GetFirstSelectedMsgFolder());
 }
 
 function MsgDeleteMessage(reallyDelete, fromToolbar)
 {
   // If from the toolbar, return right away if this is a news message
   // only allow cancel from the menu:  "Edit | Cancel / Delete Message".
-  if (fromToolbar && isNewsURI(GetLoadedMsgFolder().URI))
+  if (fromToolbar && gFolderDisplay.view.isNewsFolder)
     return;
 
-  SetNextMessageAfterDelete();
+  gFolderDisplay.hintAboutToDeleteMessages();
   if (reallyDelete)
     gDBView.doCommand(nsMsgViewCommandType.deleteNoTrash);
   else
     gDBView.doCommand(nsMsgViewCommandType.deleteMsg);
 }
 
 /**
  * Copies the selected messages to the destination folder
@@ -1124,17 +1122,17 @@ function MsgCopyMessage(aDestFolder)
  */
 function MsgMoveMessage(aDestFolder)
 {
   // We don't move news messages, we copy them.
   if (isNewsURI(gDBView.msgFolder.URI))
     gDBView.doCommandWithFolder(nsMsgViewCommandType.copyMessages, aDestFolder);
   else
   {
-    SetNextMessageAfterDelete();
+    gFolderDisplay.hintAboutToDeleteMessages();
     gDBView.doCommandWithFolder(nsMsgViewCommandType.moveMessages, aDestFolder);
   }
   pref.setCharPref("mail.last_msg_movecopy_target_uri", aDestFolder.URI);
   pref.setBoolPref("mail.last_msg_movecopy_was_move", true);
 }
 
 /**
  * Calls the ComposeMessage function with the desired type, and proper default
@@ -1142,32 +1140,34 @@ function MsgMoveMessage(aDestFolder)
  *
  * @param aCompType  the nsIMsgCompType to pass to the function
  * @param aEvent (optional) the event that triggered the call
  */
 function composeMsgByType(aCompType, aEvent) {
   if (aEvent && aEvent.shiftKey) {
     ComposeMessage(aCompType,
                    Components.interfaces.nsIMsgCompFormat.OppositeOfDefault,
-                   GetFirstSelectedMsgFolder(), GetSelectedMessages());
+                   GetFirstSelectedMsgFolder(),
+                   gFolderDisplay.selectedMessageUris);
   }
   else {
     ComposeMessage(aCompType, Components.interfaces.nsIMsgCompFormat.Default,
-                   GetFirstSelectedMsgFolder(), GetSelectedMessages());
+                   GetFirstSelectedMsgFolder(),
+                   gFolderDisplay.selectedMessageUris);
   }
 }
 
 function MsgNewMessage(event)
 {
   composeMsgByType(Components.interfaces.nsIMsgCompType.New, event);
 }
 
 function MsgReplyMessage(event)
 {
-  var loadedFolder = GetLoadedMsgFolder();
+  var loadedFolder = gFolderDisplay.displayedFolder;
   if (loadedFolder)
   {
     var server = loadedFolder.server;
     if(server && server.type == "nntp")
     {
       MsgReplyGroup(event);
       return;
     }
@@ -1202,50 +1202,47 @@ function BatchMessageMover()
   this._batches = {};
   this._currentKey = null;
 }
 
 BatchMessageMover.prototype = {
 
   archiveSelectedMessages: function()
   {
-    // subtle:
-    SetNextMessageAfterDelete(); // we're just pretending
-    this.messageToSelectAfterWereDone = gNextMessageViewIndexAfterDelete;
-    gNextMessageViewIndexAfterDelete = -2;
-
-    let selectedMsgUris = GetSelectedMessages();
-    if (!selectedMsgUris.length)
+    gFolderDisplay.hintMassMoveStarting();
+
+    let selectedMessages = gFolderDisplay.selectedMessages;
+    if (!selectedMessages.length)
       return;
-    
+
     let messages = Components.classes["@mozilla.org/array;1"]
                              .createInstance(Components.interfaces.nsIMutableArray);
-    
-    for (let i = 0; i < selectedMsgUris.length; ++i)
+
+    for (let i = 0; i < selectedMessages.length; ++i)
     {
-      let msgHdr = messenger.msgHdrFromURI(selectedMsgUris[i]);
-  
+      let msgHdr = selectedMessages[i];
+
       let rootFolder = msgHdr.folder.server.rootFolder;
-  
+
       let msgDate = new Date(msgHdr.date / 1000);  // convert date to JS date object
       let msgYear = msgDate.getFullYear().toString();
       let monthFolderName = msgDate.toLocaleFormat("%Y-%m")
       let dstFolderName = monthFolderName;
 
       let copyBatchKey = msgHdr.folder.URI + '\000' + dstFolderName;
       if (! (copyBatchKey in this._batches)) {
         this._batches[copyBatchKey] = [msgHdr.folder, msgYear, dstFolderName];
       }
       this._batches[copyBatchKey].push(msgHdr);
     }
     // Now we launch the code that will iterate over all of the message copies
     // one in turn
     this.processNextBatch();
   },
-  
+
   processNextBatch: function()
   {
     for (let key in this._batches)
     {
       this._currentKey = key;
       let batch = this._batches[key];
       let srcFolder = batch[0];
       let msgYear = batch[1];
@@ -1256,18 +1253,18 @@ BatchMessageMover.prototype = {
       // rss servers don't have an identity so we special case the archives URI
       let archiveFolderUri = (srcFolder.server.type == 'rss')
         ? srcFolder.server.serverURI + "/Archives"
         : getIdentityForHeader(msgs[0], Ci.nsIMsgCompType
                                         .ReplyAll).archiveFolder;
 
       let archiveFolder = GetMsgFolderFromUri(archiveFolderUri, false);
       let granularity = archiveFolder.server.archiveGranularity;
-      // for imap folders, we need to create the sub-folders asynchronously, 
-      // so we chain the urls using the listener called back from 
+      // for imap folders, we need to create the sub-folders asynchronously,
+      // so we chain the urls using the listener called back from
       // createStorageIfMissing. For local, creatStorageIfMissing is
       // synchronous.
       let isImap = archiveFolder.server.type == "imap";
       if (!archiveFolder.parent) {
         archiveFolder.createStorageIfMissing(this);
         if (isImap)
           return;
       }
@@ -1349,28 +1346,20 @@ BatchMessageMover.prototype = {
       this._currentKey = null;
 
       // is there a safe way to test whether this._batches is empty?
       let empty = true;
       for (let key in this._batches) {
         empty = false;
       }
 
-      if (empty)
-      {
-        // we're just going to select the message now
-        var treeView = gDBView.QueryInterface(Components.interfaces.nsITreeView);
-        var treeSelection = treeView.selection;
-        treeSelection.select(this.messageToSelectAfterWereDone);
-        treeView.selectionChanged();
-      }
-      else
-      {
+      if (!empty)
         this.processNextBatch();
-      }
+      else // this will select the appropriate next message
+        gFolderDisplay.hintMassMoveCompleted();
     }
   },
   QueryInterface: function(iid) {
     if (!iid.equals(Components.interfaces.nsIUrlListener) &&
       !iid.equals(Components.interfaces.nsIMsgCopyServiceListener) &&
       !iid.equals(Components.interfaces.nsISupports))
       throw Components.results.NS_ERROR_NO_INTERFACE;
     return this;
@@ -1415,28 +1404,26 @@ function MsgForwardAsInline(event)
 
 function MsgEditMessageAsNew()
 {
   composeMsgByType(Components.interfaces.nsIMsgCompType.Template);
 }
 
 function MsgComposeDraftMessage()
 {
-  var loadedFolder = GetLoadedMsgFolder();
-  var messageArray = GetSelectedMessages();
-
   ComposeMessage(Components.interfaces.nsIMsgCompType.Draft,
                  Components.interfaces.nsIMsgCompFormat.Default,
-                 loadedFolder, messageArray);
+                 gFolderDisplay.displayedFolder,
+                 gFolderDisplay.selectedMessageUris);
 }
 
 function MsgCreateFilter()
 {
   // retrieve Sender direct from selected message's headers
-  var msgHdr = gDBView.hdrForFirstSelectedMessage;
+  var msgHdr = gFolderDisplay.selectedMessage;
   var headerParser = Components.classes["@mozilla.org/messenger/headerparser;1"]
                                .getService(Components.interfaces.nsIMsgHeaderParser);
   var emailAddress = headerParser.extractHeaderAddressMailboxes(msgHdr.author);
   if (emailAddress)
     top.MsgFilters(emailAddress, null);
 }
 
 function MsgNewFolder(callBackFunctionName)
@@ -1552,24 +1539,24 @@ function ToggleFavoriteFolderFlag()
 {
   var folder = GetFirstSelectedMsgFolder();
   folder.toggleFlag(Components.interfaces.nsMsgFolderFlags.Favorite);
 }
 
 function MsgSaveAsFile()
 {
   if (GetNumSelectedMessages() == 1)
-    SaveAsFile(GetFirstSelectedMessage());
+    SaveAsFile(gFolderDisplay.selectedMessageUris[0]);
 }
 
 function MsgSaveAsTemplate()
 {
-  var folder = GetLoadedMsgFolder();
   if (GetNumSelectedMessages() == 1)
-    SaveAsTemplate(GetFirstSelectedMessage(), folder);
+    SaveAsTemplate(gFolderDisplay.selectedMessageUris[0],
+                   gFolderDisplay.displayedFolder);
 }
 
 function CreateToolbarTooltip(document, event)
 {
   event.stopPropagation();
   var tn = document.tooltipNode;
   if (tn.localName != "tab")
     return false; // Not a tab, so cancel the tooltip.
@@ -1580,315 +1567,337 @@ function CreateToolbarTooltip(document, 
   if (tn.hasAttribute("label")) {
     event.target.setAttribute("label", tn.getAttribute("label"));
     return true;
   }
   return false;
 }
 
 /**
- * mailTabType provides both "folder" and "message" tab modes. Under the
- * previous TabOwner framework, their logic was separated into two 'classes'
- * which called common helper methods and had similar boilerplate logic.
+ * Displays message "folder"s, mail "message"s, and "glodaSearch" 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.
+ *
+ * The tab info objects (as tabmail's currentTabInfo/tabInfo fields contain)
+ *  have the following attributes specific to our implementation:
+ *
+ *
+ * @property {string} uriToOpen
+ * @property {nsIMsgFolder} msgSelectedFolder Preserves gMsgFolderSelected
+ *     global.
+ * @property {nsIMsgDBView} dbView The database view to use with the thread tree
+ *     when this tab is displayed.  The value will be assigned to the global
+ *     gDBView in the process.
+ * @property {nsIMessenger} messenger Used to preserve "messenger" global value.
+ *     The messenger object is the keeper of the 'undo' state and navigation
+ *     history, which is why we do this.
+ *
+ * @property {boolean} folderPaneCollapsed In "folder" mode, has the user
+ *     intentionally collapsed the folder pane.
+ * @property {boolean} messagePaneCollapsed In "folder" or "glodaSearch" mode,
+ *     has the user intentionally collapsed the message pane.
+ *
+ * @property {nsIMsgDBHdr} hdr In "message" mode, the header of the message
+ *     being displayed.
+ * @property {nsIMsgSearchSession} searchSession Used to preserve gSearchSession
+ *     global value.
+ *
  */
 let mailTabType = {
   name: "mail",
   panelId: "mailContent",
   modes: {
+    /**
+     * The folder view displays the contents of an nsIMsgDBFolder, with the
+     *  folder pane (potentially), thread pane (always), and message pane
+     *  (potentially) displayed.
+     *
+     * The actual nsMsgDBView can be any of the following types of things:
+     *  - A single folder.
+     *    - A quicksearch on a single folder.
+     *  - A virtual folder potentially containing messages from multiple
+     *    folders. (eShowVirtualFolderResults)
+     */
     folder: {
       isDefault: true,
       type: "folder",
-      openTab: function(aTab, aFolderUri) {
-        aTab.uriToOpen = aFolderUri;
-
-        this.openTab(aTab); // call superclass logic
+      /// The set of panes that are legal to be displayed in this mode
+      legalPanes: {
+        folder: true,
+        thread: true,
+        message: true
+      },
+      openFirstTab: function(aTab) {
+        this.openTab(aTab, true, new MessagePaneDisplayWidget());
+        aTab.folderDisplay.makeActive();
       },
-      showTab: function(aTab) {
-        this.folderAndThreadPaneVisible = true;
-        ClearMessagePane();
-
-        this.showTab(aTab);
+      /**
+       * @param aFolder The nsIMsgFolder to display.
+       * @param aMsgHdr Optional message header to display.
+       */
+      openTab: function(aTab, aFolder, aMsgHdr) {
+        // Get a tab that we can initialize our user preferences from.
+        // (We don't want to assume that our immediate predecessor was a
+        //  "folder" tab.)
+        let modelTab = document.getElementById("tabmail")
+                         .getTabInfoForCurrentOrFirstModeInstance(aTab.mode);
+        // copy its state
+        aTab.folderPaneCollapsed = modelTab.folderPaneCollapsed;
+        aTab.messagePaneCollapsed = modelTab.messagePaneCollapsed;
+
+        this.openTab(aTab, false,  new MessagePaneDisplayWidget());
+
+        // Clear selection, because context clicking on a folder and opening in a
+        // new tab needs to have SelectFolder think the selection has changed.
+        // We also need to clear these globals to subvert the code that prevents
+        // folder loads when things haven't changed.
+        var folderTree = document.getElementById("folderTree");
+        folderTree.view.selection.clearSelection();
+        folderTree.view.selection.currentIndex = -1;
+
+        aTab.folderDisplay.makeActive();
+
+        // selecting the folder effectively calls
+        //  aTab.folderDisplay.show(aFolder)
+        gFolderTreeView.selectFolder(aFolder);
+        if(aMsgHdr)
+          aTab.folderDisplay.selectMessage(aMsgHdr);
       },
       onTitleChanged: function(aTab, aTabNode) {
-        if (!gMsgFolderSelected) {
+        if (!aTab.folderDisplay || !aTab.folderDisplay.displayedFolder) {
           // Don't show "undefined" as title when there is no account.
           aTab.title = " ";
           return;
         }
-        aTab.title = gMsgFolderSelected.prettyName;
-        if (!gMsgFolderSelected.isServer && this._getNumberOfRealAccounts() > 1)
-          aTab.title += " - " + gMsgFolderSelected.server.prettyName;
-
-        // The user may have changed folders, triggering our onTitleChanged callback.
+        // The user may have changed folders, triggering our onTitleChanged
+        // callback.
+        let folder = aTab.folderDisplay.displayedFolder;
+        aTab.title = folder.prettyName;
+        if (!folder.isServer && this._getNumberOfRealAccounts() > 1)
+          aTab.title += " - " + folder.server.prettyName;
+
         // Update the appropriate attributes on the tab.
-        aTabNode.setAttribute('SpecialFolder', getSpecialFolderString(gMsgFolderSelected));
-        aTabNode.setAttribute('ServerType', gMsgFolderSelected.server.type);
-        aTabNode.setAttribute('IsServer', gMsgFolderSelected.isServer);
-        aTabNode.setAttribute('IsSecure', gMsgFolderSelected.server.isSecure);
+        aTabNode.setAttribute('SpecialFolder',
+                              getSpecialFolderString(folder));
+        aTabNode.setAttribute('ServerType', folder.server.type);
+        aTabNode.setAttribute('IsServer', folder.isServer);
+        aTabNode.setAttribute('IsSecure', folder.server.isSecure);
       }
     },
+    /**
+     * The message view displays a single message.  In this view, the folder
+     *  pane and thread pane are forced hidden and only the message pane is
+     *  displayed.
+     */
     message: {
       type: "message",
-      openTab: function(aTab, aFolderUri, aMsgHdr) {
-        aTab.uriToOpen = aFolderUri;
-        aTab.hdr = aMsgHdr;
-
+      /// The set of panes that are legal to be displayed in this mode
+      legalPanes: {
+        folder: false,
+        thread: false,
+        message: true
+      },
+      openTab: function(aTab, aMsgHdr, aViewWrapperToClone) {
+        aTab.mode.onTitleChanged.call(this, aTab, null, aMsgHdr);
+
+        this.openTab(aTab, false, new MessageTabDisplayWidget());
+
+        if (aViewWrapperToClone)
+          aTab.folderDisplay.cloneView(aViewWrapperToClone);
+        else
+          aTab.folderDisplay.show(aMsgHdr.folder);
+
+        aTab.folderDisplay.selectMessage(aMsgHdr);
+
+        // we only want to make it active after setting up the view and the message
+        //  to avoid generating bogus summarization events.
+        aTab.folderDisplay.makeActive();
+      },
+      onTitleChanged: function(aTab, aTabNode, aMsgHdr) {
+        if (aMsgHdr == null)
+          aMsgHdr = aTab.folderDisplay.selectedMessage;
         aTab.title = "";
-        if (aTab.hdr.flags & Components.interfaces.nsMsgMessageFlags.HasRe)
+        if (aMsgHdr.flags & Components.interfaces.nsMsgMessageFlags.HasRe)
           aTab.title = "Re: ";
-        if (aTab.hdr.mime2DecodedSubject)
-          aTab.title += aTab.hdr.mime2DecodedSubject;
-
-        aTab.title += " - " + aTab.hdr.folder.prettyName;
+        if (aMsgHdr.mime2DecodedSubject)
+          aTab.title += aMsgHdr.mime2DecodedSubject;
+
+        aTab.title += " - " + aMsgHdr.folder.prettyName;
         if (this._getNumberOfRealAccounts() > 1)
-          aTab.title += " - " + aTab.hdr.folder.server.prettyName;
-
-        // let's try hiding the thread pane and folder pane
-        this.folderAndThreadPaneVisible = false;
-
-        this.openTab(aTab); // call superclass logic
-
-        gCurrentlyDisplayedMessage = nsMsgViewIndex_None;
-        ClearThreadPaneSelection();
-        setTimeout(gDBView.selectFolderMsgByKey, 0, aTab.hdr.folder,
-                   aTab.hdr.messageKey);
-      },
-      showTab: function(aTab) {
-        this.folderAndThreadPaneVisible = false;
-        this.showTab(aTab);
+          aTab.title += " - " + aMsgHdr.folder.server.prettyName;
       }
     }
   },
 
   _getNumberOfRealAccounts : function() {
     let mgr = Components.classes["@mozilla.org/messenger/account-manager;1"]
                         .getService(Components.interfaces.nsIMsgAccountManager);
     let accountCount = mgr.accounts.Count();
     // If we have an account, we also always have a "Local Folders" account.
     return accountCount > 0 ? (accountCount - 1) : 0;
   },
 
   /**
-   * Create the new tab's state, which engenders some side effects.  Part of our
-   *  contract is that we leave the tab in the selected state.
+   * Common tab opening code shared by the various tab modes.
    */
-  openTab: function(aTab) {
+  openTab: function(aTab, aIsFirstTab, aMessageDisplay) {
     // Set the messagepane as the primary browser for content.
     document.getElementById("messagepane").setAttribute("type",
                                                         "content-primary");
 
-    ClearThreadPaneSelection();
-
-    // Each tab gets its own messenger instance; I assume this is so each one
-    // gets its own undo/redo stack?
-    messenger = Components.classes["@mozilla.org/messenger;1"]
-                          .createInstance(Components.interfaces.nsIMessenger);
-    messenger.setWindow(window, msgWindow);
-    aTab.messenger = messenger;
-
-    aTab.msgSelectedFolder = gMsgFolderSelected;
-
-    // Clear selection, because context clicking on a folder and opening in a
-    // new tab needs to have SelectFolder think the selection has changed.
-    // We also need to clear these globals to subvert the code that prevents
-    // folder loads when things haven't changed.
-    var folderTree = document.getElementById("folderTree");
-    folderTree.view.selection.clearSelection();
-    folderTree.view.selection.currentIndex = -1;
-    gMsgFolderSelected = null;
-    msgWindow.openFolder = null;
-
-    // Clear thread pane selection - otherwise, the tree tries to impose the
-    // the current selection on the new view.
-    gDBView = null; // clear gDBView so we won't try to close it.
-    gFolderTreeView.selectFolder(GetMsgFolderFromUri(aTab.uriToOpen));
-    aTab.dbView = gDBView;
+    aTab.messageDisplay = aMessageDisplay;
+    aTab.folderDisplay = new FolderDisplayWidget(aTab, aTab.messageDisplay);
+    aTab.folderDisplay.msgWindow = msgWindow;
+    aTab.folderDisplay.tree = document.getElementById("threadTree");
+    aTab.folderDisplay.treeBox = aTab.folderDisplay.tree.boxObject.QueryInterface(
+                                   Components.interfaces.nsITreeBoxObject);
+
+    if (aIsFirstTab) {
+      aTab.folderDisplay.messenger = messenger;
+    }
+    else {
+      // Each tab gets its own messenger instance; this provides each tab with
+      // its own undo/redo stack and back/forward navigation history.
+      messenger = Components.classes["@mozilla.org/messenger;1"]
+                            .createInstance(Components.interfaces.nsIMessenger);
+      messenger.setWindow(window, msgWindow);
+      aTab.folderDisplay.messenger = messenger;
+    }
   },
 
   closeTab: function(aTab) {
-    if (aTab.dbView)
-      aTab.dbView.close();
-    if (aTab.messenger)
-      aTab.messenger.setWindow(null, null);
+    aTab.folderDisplay.close();
   },
 
   saveTabState: function(aTab) {
-    aTab.messenger = messenger;
-    aTab.dbView = gDBView;
-    aTab.searchSession = gSearchSession;
-    aTab.msgSelectedFolder = gMsgFolderSelected;
-    if (!gDBView)
-      return;
-
-    aTab.firstVisibleRow = document.getElementById("threadTree").treeBoxObject.getFirstVisibleRow();
-
-    if (gDBView.currentlyDisplayedMessage != nsMsgViewIndex_None)
-    {
-      try // there may not be a selected message.
-      {
-        var curMsgHdr = gDBView.hdrForFirstSelectedMessage;
-        aTab.selectedMsgId = curMsgHdr.messageId;
-      }
-      catch (ex) {}
-    }
-    else
-    {
-      aTab.selectedMsgId = null;
-      aTab.msgSelectedFolder = gDBView.msgFolder;
-    }
-    if (aTab.msgSelectedFolder)
-      aTab.mailView = GetMailViewForFolder(aTab.msgSelectedFolder);
-
     // Now let other tabs have a primary browser if they want.
     document.getElementById("messagepane").setAttribute("type",
                                                         "content-targetable");
+
+    aTab.folderDisplay.makeInactive();
   },
 
-  _displayFolderAndThreadPane: function(show) {
-    let collapse = !show;
+  /**
+   * Some panes simply are illegal in certain views, and some panes are legal
+   *  but the user may have collapsed/hidden them.  If that was not enough, we
+   *  have three different layouts that are possible, each of which requires a
+   *  slightly different DOM configuration, and accordingly for us to poke at
+   *  different DOM nodes.  Things are made somewhat simpler by our decision
+   *  that all tabs share the same layout.
+   * This method takes the legal states and current display states and attempts
+   *  to apply the appropriate logic to make it all work out.  This method is
+   *  not in charge of figuring out or preserving display states.
+   *
+   * We take a dictionary of desired visibility booleans as our argument because
+   * it is both readable and scalable.
+   *
+   * @param aLegalStates A dictionary where each key and value indicates whether
+   *     the pane in question (key) is legal to be displayed in this mode.  If
+   *     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.
+   *     - 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) {
     let layout = pref.getIntPref("mail.pane_config.dynamic");
     if (layout == kWidePaneConfig)
     {
-      document.getElementById("messengerBox").collapsed = collapse;
-      // If opening a standalone message, need to give the messagepanebox flex.
-      if (collapse)
+      // in the "wide" configuration, the #messengerBox is left holding the
+      //  folder pane and thread pane, and the message pane has migrated to be
+      //  its sibling (under #mailContent).
+      // Accordingly, if both the folder and thread panes are illegal, we
+      //  want to collapse the #messengerBox and make sure the #messagepanebox
+      //  fills up the screen.  (For example, when in "message" mode.)
+      let collapseMessengerBox = !aLegalStates.folder && !aLegalStates.thread;
+      document.getElementById("messengerBox").collapsed = collapseMessengerBox;
+      if (collapseMessengerBox)
         document.getElementById("messagepanebox").flex = 1;
     }
 
-    if (layout == kVerticalPaneConfig)
-      document.getElementById("threadTree").collapsed = collapse;
-    else
-      document.getElementById("displayDeck").collapsed = collapse;
-
-    let fpSplitter = document.getElementById("folderpane_splitter");
-    if (show && fpSplitter.getAttribute("state") == "collapsed")
-    {
-      // If we're showing everything but the folder pane splitter was
-      // previously collapsed, then keep it collapsed.
-      // However, in the case of it being collapsed from showing a
-      // a full message in a tab, then uncollapse just the splitter.
-      if (fpSplitter.getAttribute("substate"))
-        fpSplitter.collapsed = false;
-    }
+    // -- folder pane
+    // collapse the splitter when not legal
+    document.getElementById("folderpane_splitter").collapsed =
+      !aLegalStates.folder;
+    // collapse the folder pane when not visible
+    document.getElementById("folderPaneBox").collapsed =
+      !aLegalStates.folder || !aVisibleStates.folder;
+
+    // -- thread pane
+    // in a vertical view, the threadContentArea sits in the #threadPaneBox
+    //  next to the message pane and its splitter.
+    if (layout == kVerticalMailLayout)
+      document.getElementById("threadContentArea").collapsed =
+        !aLegalStates.thread;
+    // whereas in the default view, the displayDeck is the one next to the
+    //  message pane and its splitter
     else
-    {
-      fpSplitter.collapsed = collapse;
-      document.getElementById("folderPaneBox").collapsed = collapse;
-    }
-
-    let tpSplitter = document.getElementById("threadpane-splitter");
-    if (show && tpSplitter.getAttribute("state") == "collapsed")
-    {
-      // If we're showing everything but the thread pane splitter was
-      // previously collapsed, then keep it collapsed.
-      // However, in the case of it being collapsed from showing a
-      // a full message in a tab, then uncollapse just the splitter.
-      tpSplitter.collapsed = tpSplitter.getAttribute("substate") ? false : true;
-      document.getElementById("messagepanebox").collapsed = true;
-    }
-    else
-    {
-      tpSplitter.collapsed = collapse;
-      document.getElementById("messagepanebox").collapsed = false;
-    }
-    // Need to call ChangeMessagePaneVisibility() a little later after the
-    // message was selected so that the tab state data gets the message key
-    // saved off, which happens below in showTab().
-    setTimeout(ChangeMessagePaneVisibility, 0, IsMessagePaneCollapsed());
-
+      document.getElementById("displayDeck").collapsed =
+        !aLegalStates.thread;
+
+    // the threadpane-splitter collapses the message pane (arguably a misnomer),
+    //  but it only needs to exist when the thread-pane is legal
+    document.getElementById("threadpane-splitter").collapsed =
+      !aLegalStates.thread;
+
+    // Some things do not make sense if the thread pane is not legal.
+    // (This is likely an example of something that should be using the command
+    //  mechanism to update the UI elements as to the state of what the user
+    //  is looking at, rather than home-brewing it in here.)
     try {
-      document.getElementById("search-container").collapsed = collapse;
+      // you can't quick-search if you don't have a collection of messages
+      document.getElementById("search-container").collapsed =
+        !aLegalStates.thread;
     } catch (ex) {}
     try {
-      document.getElementById("mailviews-container").collapsed = collapse;
+      // views only work on the thread pane; no thread pane, no views
+      document.getElementById("mailviews-container").collapsed =
+        !aLegalStates.thread;
     } catch (ex) {}
-  },
-
-  _folderAndThreadPaneVisible: true,
-  get folderAndThreadPaneVisible() { return this._folderAndThreadPaneVisible; },
-  set folderAndThreadPaneVisible(aDesiredVisible) {
-    if (aDesiredVisible != this._folderAndThreadPaneVisible) {
-      this._displayFolderAndThreadPane(aDesiredVisible);
-      this._folderAndThreadPaneVisible = aDesiredVisible;
-    }
+
+    // -- message pane
+    // the message pane can only be collapsed when the thread pane is legal
+    document.getElementById("messagepanebox").collapsed =
+      aLegalStates.thread && !aVisibleStates.message;
+
+    // -- 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");
 
-    // restore globals
-    messenger = aTab.messenger;
-    gDBView = aTab.dbView;
-    gSearchSession = aTab.searchSession;
-
-    // restore selection in folder pane;
-    let folderToSelect = gDBView ? gDBView.msgFolder : aTab.msgSelectedFolder;
-    // restore view state if we had one
-    var row = gFolderTreeView.getIndexOfFolder(folderToSelect);
-
-    var treeBoxObj = document.getElementById("folderTree").treeBoxObject;
-    var folderTreeSelection = treeBoxObj.view.selection;
-    // make sure that row.value is valid so that it doesn't mess up
-    // the call to ensureRowIsVisible().
-    if ((row >= 0) && !folderTreeSelection.isSelected(row))
-    {
-      gMsgFolderSelected = folderToSelect;
-      folderTreeSelection.selectEventsSuppressed = true;
-      folderTreeSelection.select(row);
-      treeBoxObj.ensureRowIsVisible(row);
-      folderTreeSelection.selectEventsSuppressed = false;
-    }
-    if (gDBView)
-    {
-      UpdateColumnsForView(gMsgFolderSelected, gDBView.viewType);
-      UpdateSortIndicators(gDBView.sortType, gDBView.sortOrder);
-      // This sets the thread pane tree's view to the gDBView view.
-      RerootThreadPane();
-      // Only refresh the view picker if the views toolbar is visible.
-      if (document.getElementById("mailviews-container")) 
-        UpdateViewPickerByValue(aTab.mailView);
-
-      // We need to restore the selection to what it was when we switched away
-      // from this tab. We need to remember the selected keys, instead of the
-      // selected indices, since the view might have changed. But maybe the
-      // selectedIndices adjust as items are added/removed from the (hidden)
-      // view.
-      try
-      {
-        if (aTab.selectedMsgId && aTab.msgSelectedFolder)
-        {
-          // We clear the selection in order to generate an event when we
-          // re-select our message.
-          ClearThreadPaneSelection();
-
-          var msgDB = aTab.msgSelectedFolder.msgDatabase;
-          var msgHdr = msgDB.getMsgHdrForMessageID(aTab.selectedMsgId);
-          setTimeout(gDBView.selectFolderMsgByKey, 0, aTab.msgSelectedFolder,
-                     msgHdr.messageKey);
-        }
-        // We do not clear the selection if there was more than one message
-        // displayed.  this leaves our selection intact. there was originally
-        // some claim that the selection might lose synchronization with the
-        // view, but this is unsubstantiated.  said comment came from the
-        // original code that stored information on the selected rows, but
-        // then failed to do anything with it, probably because there is no
-        // existing API call that accomplishes it.
+    aTab.folderDisplay.makeActive();
+
+    // - restore folder pane/tree selection
+    if (aTab.folderDisplay.displayedFolder) {
+      // but don't generate any events while doing so!
+      gFolderTreeView.selection.selectEventsSuppressed = true;
+      try {
+        gFolderTreeView.selectFolder(aTab.folderDisplay.displayedFolder);
       }
-      catch (ex) {dump(ex);}
-
-      document.getElementById("threadTree").treeBoxObject.scrollToRow(aTab.firstVisibleRow);
-      ShowThreadPane();
-    }
-    else if (gMsgFolderSelected.isServer)
-    {
-      UpdateStatusQuota(null);
-      // Load AccountCentral page here.
-      ShowAccountCentral();
+      finally {
+        gIgnoreSyntheticFolderPaneSelectionChange = true;
+        gFolderTreeView.selection.selectEventsSuppressed = false;
+      }
     }
   },
 
   supportsCommand: function(aTab, aCommand) {
     return DefaultController.supportsCommand(aCommand);
   },
 
   isCommandEnabled: function(aTab, aCommand) {
@@ -1912,90 +1921,64 @@ let mailTabType = {
 function MsgOpenNewWindowForFolder(folderURI, msgKeyToSelect)
 {
   if (folderURI) {
     window.openDialog("chrome://messenger/content/", "_blank",
                       "chrome,all,dialog=no", folderURI, msgKeyToSelect);
     return;
   }
 
-  // Use gFolderTreeView.getSelectedFolders() to find out which folder to open
-  // instead of GetLoadedMsgFolder().URI. This is required because on a
-  // right-click, the currentIndex value will be different from the actual row
-  // that is highlighted. gFolderTreeView.getSelectedFolders() will return the
-  // folder that is highlighted.
+  // If there is a right-click happening, gFolderTreeView.getSelectedFolders()
+  // will tell us about it (while the selection's currentIndex would reflect
+  // the node that was selected/displayed before the right-click.)
   let selectedFolders = gFolderTreeView.getSelectedFolders();
   for (let i = 0; i < selectedFolders.length; i++) {
     window.openDialog("chrome://messenger/content/", "_blank",
                       "chrome,all,dialog=no",
                       selectedFolders[i].URI, msgKeyToSelect);
   }
 }
 
-function MsgOpenNewTabForFolder(folderURI, msgKeyToSelect)
+/**
+ * UI-triggered command to open the currently selected folder(s) in new tabs.
+ */
+function MsgOpenNewTabForFolder()
 {
-  // XXX implement the usage of msgKeyToSelect...
-  if (folderURI) {
-    document.getElementById("tabmail").openTab("folder", folderURI);
-    return;
-  }
-
-  // Use gFolderTreeView.getSelectedFolders() to find out which folder to open
-  // instead of GetLoadedMsgFolder().URI. This is required because on a
-  // right-click, the currentIndex value will be different from the actual row
-  // that is highlighted. gFolderTreeView.getSelectedFolders() will return the
-  // folder that is highlighted.
+  // If there is a right-click happening, gFolderTreeView.getSelectedFolders()
+  // will tell us about it (while the selection's currentIndex would reflect
+  // the node that was selected/displayed before the right-click.)
   let selectedFolders = gFolderTreeView.getSelectedFolders();
   for (let i = 0; i < selectedFolders.length; i++) {
-    // Set up the first tab, which was previously invisible.
-    // This assumes the first tab is always a 3-pane ui, which
-    // may not be right, especially if we have the ability
-    // to persist your tab setup.
-    document.getElementById("tabmail").openTab("folder", selectedFolders[i].URI);
+    document.getElementById("tabmail").openTab("folder", selectedFolders[i]);
   }
 }
 
-function MsgOpenNewTabForMessage(messageKey, folderUri)
+/**
+ * UI-triggered command to open the currently selected message in a new tab.
+ */
+function MsgOpenNewTabForMessage()
 {
-  var hdr;
-
-  // messageKey can be 0 for an actual message (first message in the folder)  
-  if (folderUri)
-  {
-    hdr = GetMsgFolderFromUri(folderUri).GetMessageHeader(messageKey);
-  }
-  else
-  {
-    hdr = gDBView.hdrForFirstSelectedMessage;
-    // Use the header's folder - this will open a msg in a virtual folder view
-    // in its real folder, which is needed if the msg wouldn't be in a new
-    // view with the same terms - e.g., it's read and the view is unread only.
-    // If we cloned the view, we wouldn't have to do this.
-    folderUri = hdr.folder.URI;
-  }
-  
-  // Fix it so we won't try to load the previously loaded message.
-  hdr.folder.lastMessageLoaded = nsMsgKey_None;
-
-  document.getElementById('tabmail').openTab("message", folderUri, hdr);
+  if (!gFolderDisplay.selectedMessage)
+    return;
+  document.getElementById('tabmail').openTab("message",
+                                             gFolderDisplay.selectedMessage,
+                                             gFolderDisplay.view);
 }
 
 function MsgOpenSelectedMessages()
 {
   // Toggle message body (rss summary) and content-base url in message
   // pane per pref, otherwise open summary or web page in new window.
-  if (IsFeedItem() && GetFeedOpenHandler() == 2) {
+  if (gFolderDisplay.selectedMessageIsFeed && GetFeedOpenHandler() == 2) {
     FeedSetContentViewToggle();
     return;
   }
 
-  var dbView = GetDBView();
-
-  var indices = GetSelectedIndices(dbView);
-  var numMessages = indices.length;
+  let selectedMessages = gFolderDisplay.selectedMessages;
+  var numMessages = selectedMessages.length;
 
   var windowReuse = gPrefBranch.getBoolPref("mailnews.reuse_message_window");
   // This is a radio type button pref, currently with only 2 buttons.
   // We need to keep the pref type as 'bool' for backwards compatibility
   // with 4.x migrated prefs.  For future radio button(s), please use another
   // pref (either 'bool' or 'int' type) to describe it.
   //
   // windowReuse values: false, true
@@ -2013,55 +1996,37 @@ function MsgOpenSelectedMessages()
     var text = gMessengerBundle.getFormattedString("openWindowWarningText", [numMessages]);
     var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
                                   .getService(Components.interfaces.nsIPromptService);
     if (!promptService.confirm(window, title, text))
       return;
   }
 
   for (var i = 0; i < numMessages; i++) {
-    MsgOpenNewWindowForMessage(dbView.getURIForViewIndex(indices[i]),
-                               dbView.getFolderForViewIndex(indices[i]).URI);
+    MsgOpenNewWindowForMessage(selectedMessages[i]);
   }
 }
 
 function MsgOpenSelectedMessageInExistingWindow()
 {
   var windowID = GetWindowByWindowType("mail:messageWindow");
   if (!windowID)
     return false;
 
-  try {
-    var messageURI = gDBView.URIForFirstSelectedMessage;
-    var msgHdr = gDBView.hdrForFirstSelectedMessage;
-
-    // Reset the window's message uri and folder uri vars, and
-    // update the command handlers to what's going to be used.
-    // This has to be done before the call to CreateView().
-    windowID.gCurrentMessageUri = messageURI;
-    windowID.gCurrentFolderUri = msgHdr.folder.URI;
-    windowID.UpdateMailToolbar('MsgOpenExistingWindowForMessage');
-
-    // Even if the folder uri's match, we can't use the existing view
-    // (msgHdr.folder.URI == windowID.gCurrentFolderUri)
-    // - the reason is quick search and mail views. See bug #187673.
-    //
-    // For the sake of simplicity, let's always call CreateView(gDBView);
-    // which will clone gDBView.
-    windowID.CreateView(gDBView);
-    windowID.LoadMessageByMsgKey(msgHdr.messageKey);
-
-    // bring existing window to front
-    windowID.focus();
-    return true;
-  }
-  catch (ex) {
-    dump("reusing existing standalone message window failed: " + ex + "\n");
-  }
-  return false;
+  var msgHdr = gFolderDisplay.selectedMessage;
+
+  // (future work: perhaps make the window have a method we can call to do this)
+  // make the window's folder clone our view
+  windowID.gFolderDisplay.cloneView(gFolderDisplay.view);
+  // boss the window into showing our message
+  windowID.gFolderDisplay.selectMessage(msgHdr);
+
+  // bring existing window to front
+  windowID.focus();
+  return true;
 }
 
 function MsgOpenFromFile()
 {
   const nsIFilePicker = Components.interfaces.nsIFilePicker;
   var fp = Components.classes["@mozilla.org/filepicker;1"]
                      .createInstance(nsIFilePicker);
 
@@ -2086,62 +2051,53 @@ function MsgOpenFromFile()
     dump("filePicker.chooseInputFile threw an exception\n");
     return;
   }
 
   var uri = fp.fileURL.QueryInterface(Components.interfaces.nsIURL);
   uri.query = "type=application/x-message-display";
 
   window.openDialog("chrome://messenger/content/messageWindow.xul", "_blank",
-                    "all,chrome,dialog=no,status,toolbar", uri, null, null);
+                    "all,chrome,dialog=no,status,toolbar", uri);
 }
 
-function MsgOpenNewWindowForMessage(messageUri, folderUri)
+function MsgOpenNewWindowForMessage(aMsgHdr)
 {
-  if (!messageUri)
-    // Use GetFirstSelectedMessage() to find out which message to open
-    // instead of gDBView.getURIForViewIndex(currentIndex). This is
-    // required because on a right-click, the currentIndex value will be
-    // different from the actual row that is highlighted.
-    // GetFirstSelectedMessage() will return the message that is
-    // highlighted.
-    messageUri = GetFirstSelectedMessage();
-
-    if (!folderUri)
-      // Use GetSelectedMsgFolders() to find out which message to open
-      // instead of gDBView.getURIForViewIndex(currentIndex).  This is
-      // required because on a right-click, the currentIndex value will be
-      // different from the actual row that is highlighted.
-      // GetSelectedMsgFolders() will return the message that is
-      // highlighted.
-      folderUri = GetSelectedMsgFolders()[0].URI;
-
-  // be sure to pass in the current view....
-  if (messageUri && folderUri) {
+  // no message header provided?  get the selected message (this will give us
+  //  the right-click selected message if that's what is going down.)
+  if (!aMsgHdr)
+    aMsgHdr = gFolderDisplay.selectedMessage;
+
+  // (there might not have been a selected message, so check...)
+  if (aMsgHdr)
+    // we also need to tell the window about our current view so that it can
+    //  clone it.  This enables advancing through the messages, etc.
     window.openDialog("chrome://messenger/content/messageWindow.xul", "_blank",
                       "all,chrome,dialog=no,status,toolbar",
-                      messageUri, folderUri, gDBView);
-  }
+                      aMsgHdr, gFolderDisplay.view);
 }
 
 function MsgJunk()
 {
   MsgJunkMailInfo(true);
   JunkSelectedMessages(!SelectedMessagesAreJunk());
 }
 
 
 function UpdateJunkButton()
 {
-  let hdr = msgHdrForCurrentMessage();
-  if (!hdr) // .eml file
+  // The junk message should slave off the selected message, as the preview pane
+  //  may not be visible
+  let hdr = gFolderDisplay.selectedMessage;
+  // But only the message display knows if we are dealing with a dummy.
+  if (gMessageDisplay.isDummy) // .eml file
     return;
   let junkScore = hdr.getStringProperty("junkscore");
   let hideJunk = (junkScore != "") && (junkScore != "0");
-  if (isNewsURI(hdr.folder.URI))
+  if (gFolderDisplay.selectedMessageIsNews)
     hideJunk = true;
   // which DOM node is the current junk button in the
   // message reader depends on whether it's the collapsed or
   // expanded header
   let buttonBox = document.getElementById(gCollapsedHeaderViewMode ?
                      "collapsedButtonBox" : "expandedButtonBox");
   buttonBox.getButton('hdrJunkButton').disabled = hideJunk;
 }
@@ -2155,17 +2111,17 @@ function MsgMarkAsFlagged()
 {
   MarkSelectedMessagesFlagged(!SelectedMessagesAreFlagged());
 }
 
 function MsgMarkReadByDate()
 {
   window.openDialog("chrome://messenger/content/markByDate.xul","",
                     "chrome,modal,titlebar,centerscreen",
-                    GetLoadedMsgFolder());
+                    gFolderDisplay.displayedFolder);
 }
 
 function MsgMarkAllRead()
 {
   var folder = GetSelectedMsgFolders()[0];
 
   if (folder)
     folder.markAllMessagesRead(msgWindow);
@@ -2175,17 +2131,17 @@ function MsgFilters(emailAddress, folder
 {
   if (!folder)
   {
     // Try to determine the folder from the selected message.
     if (gDBView)
     {
       try
       {
-        var msgHdr = gDBView.hdrForFirstSelectedMessage;
+        var msgHdr = gFolderDisplay.selectedMessage;
         var accountKey = msgHdr.accountKey;
         if (accountKey.length > 0)
         {
           var account = accountManager.getAccount(accountKey);
           if (account)
           {
             var server = account.incomingServer;
             if (server)
@@ -2267,43 +2223,30 @@ function MsgApplyFilters()
       newFilterIndex++;
     }
   }
   filterService.applyFiltersToFolders(tempFilterList, selectedFolders, msgWindow);
 }
 
 function MsgApplyFiltersToSelection()
 {
-  var filterService = Components.classes["@mozilla.org/messenger/services/filters;1"]
-                                .getService(Components.interfaces.nsIMsgFilterService);
-
-  var folder = gDBView.msgFolder;
-  var indices = GetSelectedIndices(gDBView);
-  if (indices && indices.length)
-  {
-    var selectedMsgs = Components.classes["@mozilla.org/array;1"]
-                                 .createInstance(Components.interfaces.nsIMutableArray);
-    for (var i = 0; i < indices.length; i++)
-    {
-      try
-      {
-        // Getting the URI will tell us if the item is real or a dummy header
-        var uri = gDBView.getURIForViewIndex(indices[i]);
-        if (uri)
-        {
-          var msgHdr = folder.GetMessageHeader(gDBView.getKeyAt(indices[i]));
-          if (msgHdr)
-            selectedMsgs.appendElement(msgHdr, false);
-        }
-      } catch (ex) {}
-    }
+  // bail if we're dealing with a dummy header
+  if (gMessageDisplay.isDummy)
+    return;
+
+  var selectedMessages = gFolderDisplay.selectedMessages;
+  if (selectedMessages.length) {
+    var filterService =
+      Components.classes["@mozilla.org/messenger/services/filters;1"]
+                .getService(Components.interfaces.nsIMsgFilterService);
 
     filterService.applyFilters(Components.interfaces.nsMsgFilterType.Manual,
-                               selectedMsgs,
-                               folder,
+                               toXPCOMArray(selectedMessages,
+                                            Components.interfaces.nsIMutableArray),
+                               gFolderDisplay.displayedFolder,
                                msgWindow);
   }
 }
 
 function ChangeMailLayout(newLayout)
 {
   gPrefBranch.setIntPref("mail.pane_config.dynamic", newLayout);
 }
@@ -2381,18 +2324,18 @@ function ToggleInlineAttachment(target)
   var viewAttachmentInline = !pref.getBoolPref("mail.inline_attachments");
   pref.setBoolPref("mail.inline_attachments", viewAttachmentInline)
   target.setAttribute("checked", viewAttachmentInline ? "true" : "false");
   ReloadMessage();
 }
 
 function PrintEnginePrintInternal(doPrintPreview, msgType)
 {
-  var messageList = GetSelectedMessages();
-  if (messageList.length == 0) {
+  var messageList = gFolderDisplay.selectedMessageUris;
+  if (!messageList) {
     dump("PrintEnginePrintInternal(): No messages selected.\n");
     return;
   }
 
   window.openDialog("chrome://messenger/content/msgPrintEngine.xul", "",
                     "chrome,dialog=no,all,centerscreen",
                     messageList.length, messageList, statusFeedback,
                     doPrintPreview, msgType, window);
@@ -2670,17 +2613,17 @@ function CommandUpdate_UndoRedo()
   EnableMenuItem("menu_undo", SetupUndoRedoCommand("cmd_undo"));
   EnableMenuItem("menu_redo", SetupUndoRedoCommand("cmd_redo"));
 }
 
 function SetupUndoRedoCommand(command)
 {
   // If we have selected a server, and are viewing account central
   // there is no loaded folder.
-  var loadedFolder = GetLoadedMsgFolder();
+  var loadedFolder = gFolderDisplay.displayedFolder;
   if (!loadedFolder || !loadedFolder.server.canUndoDeleteOnServer)
     return false;
 
   var canUndoOrRedo;
   var txnType;
   if (command == "cmd_undo")
   {
     canUndoOrRedo = messenger.canUndo();
@@ -2689,41 +2632,57 @@ function SetupUndoRedoCommand(command)
   else
   {
     canUndoOrRedo = messenger.canRedo();
     txnType = messenger.getRedoTransactionType();
   }
 
   if (canUndoOrRedo)
   {
-    var commands = 
+    var commands =
       ['valueDefault', 'valueDeleteMsg', 'valueMoveMsg', 'valueCopyMsg', 'valueUnmarkAllMsgs'];
     goSetMenuValue(command, commands[txnType]);
   }
   else
   {
     goSetMenuValue(command, 'valueDefault');
   }
   return canUndoOrRedo;
 }
 
+/**
+ * Triggered by the global JunkStatusChanged notification, we handle updating
+ *  the message display if our displayed message might have had its junk status
+ *  change.  This primarily entails updating the notification bar (that thing
+ *  that appears above the message and says "this message might be junk") and
+ *  (potentially) reloading the message because junk status affects the form of
+ *  HTML display used (sanitized vs not).
+ * When our tab implementation is no longer multiplexed (reusing the same
+ *  display widget), this must be moved into the MessageDisplayWidget or
+ *  otherwise be scoped to the tab.
+ */
 function HandleJunkStatusChanged(folder)
 {
+  // We have nothing to do (and should bail) if:
+  // - There is no currently displayed message.
+  // - The displayed message is an .eml file from disk or an attachment.
+  // - The folder that has had a junk change is not backing the display folder.
+
   // This might be the stand alone window, open to a message that was
   // and attachment (or on disk), in which case, we want to ignore it.
-  var loadedMessage = GetLoadedMessage();
-  if (!loadedMessage || (/type=application\/x-message-display/.test(loadedMessage)) ||
-      !IsCurrentLoadedFolder(folder))
+  if (!gMessageDisplay.displayedMessage ||
+      gMessageDisplay.isDummy ||
+      gFolderDisplay.displayedFolder != folder)
     return;
 
   // If multiple message are selected and we change the junk status
   // we don't want to show the junk bar (since the message pane is blank).
   var msgHdr = null;
   if (GetNumSelectedMessages() == 1)
-    msgHdr = messenger.msgHdrFromURI(loadedMessage);
+    msgHdr = gMessageDisplay.displayedMessage;
   var junkBarWasDisplayed = gMessageNotificationBar.isFlagSet(kMsgNotificationJunkBar);
   gMessageNotificationBar.setJunkMsg(msgHdr);
 
   // Only reload message if junk bar display state has changed.
   if (msgHdr && junkBarWasDisplayed != gMessageNotificationBar.isFlagSet(kMsgNotificationJunkBar))
   {
     // We may be forcing junk mail to be rendered with sanitized html.
     // In that scenario, we want to reload the message if the status has just
@@ -2764,17 +2723,17 @@ var gMessageNotificationBar =
   mMsgNotificationBar: document.getElementById('msgNotificationBar'),
 
   setJunkMsg: function(aMsgHdr)
   {
     var isJunk = false;
 
     if (aMsgHdr)
     {
-      var junkScore = aMsgHdr.getStringProperty("junkscore"); 
+      var junkScore = aMsgHdr.getStringProperty("junkscore");
       isJunk = ((junkScore != "") && (junkScore != "0"));
     }
 
     this.updateMsgNotificationBar(kMsgNotificationJunkBar, isJunk);
 
     goUpdateCommand('button_junk');
   },
 
@@ -2834,34 +2793,25 @@ function LoadMsgWithRemoteContent()
   // we want to get the msg hdr for the currently selected message
   // change the "remoteContentBar" property on it
   // then reload the message
 
   setMsgHdrPropertyAndReload("remoteContentPolicy", kAllowRemoteContent);
 }
 
 /**
- * Returns the msg hdr associated with the current loaded message.
- */
-function msgHdrForCurrentMessage()
-{
-  var msgURI = GetLoadedMessage();
-  return (msgURI && !(/type=application\/x-message-display/.test(msgURI))) ? messenger.msgHdrFromURI(msgURI) : null;
-}
-
-/**
  *  Reloads the message after adjusting the remote content policy for the sender.
- *  Iterate through the local address books looking for a card with the same e-mail address as the 
+ *  Iterate through the local address books looking for a card with the same e-mail address as the
  *  sender of the current loaded message. If we find a card, update the allow remote content field.
  *  If we can't find a card, prompt the user with a new AB card dialog, pre-selecting the remote content field.
  */
 function allowRemoteContentForSender()
 {
   // get the sender of the msg hdr
-  var msgHdr = msgHdrForCurrentMessage();
+  var msgHdr = gMessageDisplay.displayedMessage;
   if (!msgHdr)
     return;
 
   var headerParser = Components.classes["@mozilla.org/messenger/headerparser;1"]
                                .getService(Components.interfaces.nsIMsgHeaderParser);
   var names = {};
   var addresses = {};
   var fullNames = {};
@@ -2925,17 +2875,17 @@ function IgnorePhishingWarning()
   // This property is used to supress the phishing bar for the message.
   setMsgHdrPropertyAndReload("notAPhishMessage", 1);
 }
 
 function setMsgHdrPropertyAndReload(aProperty, aValue)
 {
   // we want to get the msg hdr for the currently selected message
   // change the appropiate property on it then reload the message
-  var msgHdr = msgHdrForCurrentMessage();
+  var msgHdr = gMessageDisplay.displayedMessage;
   if (msgHdr)
   {
     msgHdr.setUint32Property(aProperty, aValue);
     ReloadMessage();
   }
 }
 
 /**
@@ -2955,40 +2905,40 @@ function ClearPendingReadTimer()
 {
   if (gMarkViewedMessageAsReadTimer)
   {
     clearTimeout(gMarkViewedMessageAsReadTimer);
     gMarkViewedMessageAsReadTimer = null;
   }
 }
 
-// this is called when layout is actually finished rendering a 
+// this is called when layout is actually finished rendering a
 // mail message. OnMsgLoaded is called when libmime is done parsing the message
 function OnMsgParsed(aUrl)
 {
   // If rss feed (has 'content-base' header), show summary or load web
   // page per pref; earliest we have content DOM is here (onMsgParsed).
   FeedSetContentView();
 
   // browser doesn't do this, but I thought it could be a useful thing to test out...
-  // If the find bar is visible and we just loaded a new message, re-run 
+  // If the find bar is visible and we just loaded a new message, re-run
   // the find command. This means the new message will get highlighted and
   // we'll scroll to the first word in the message that matches the find text.
   var findBar = document.getElementById("FindToolbar");
   if (!findBar.hidden)
     findBar.onFindAgainCommand(false);
 
   // Run the phishing detector on the message if it hasn't been marked as not
   // a scam already.
-  var msgHdr = msgHdrForCurrentMessage();
+  var msgHdr = gMessageDisplay.displayedMessage;
   if (msgHdr && !msgHdr.getUint32Property("notAPhishMessage"))
     gPhishingDetector.analyzeMsgForPhishingURLs(aUrl);
 
   // notify anyone (e.g., extensions) who's interested in when a message is loaded.
-  var msgURI = GetLoadedMessage();
+  var msgURI = gFolderDisplay.selectedMessageUris[0];
   var observerService = Components.classes["@mozilla.org/observer-service;1"]
                                   .getService(Components.interfaces.nsIObserverService);
   observerService.notifyObservers(msgWindow.msgHeaderSink, "MsgMsgDisplayed", msgURI);
 
   // scale any overflowing images
   var doc = document.getElementById("messagepane").contentDocument;
   var imgs = doc.getElementsByTagName("img");
   for each (var img in imgs)
@@ -3010,33 +2960,25 @@ function OnMsgLoaded(aUrl)
 
   // nsIMsgMailNewsUrl.folder throws an error when opening .eml files.
   var folder;
   try {
     folder = aUrl.folder;
   }
   catch (ex) {}
 
-  var msgURI = GetLoadedMessage();
-
-  if (!folder || !msgURI)
+  var msgHdr = gMessageDisplay.displayedMessage;
+  gMessageDisplay.messageLoading = false;
+  gMessageDisplay.messageLoaded = true;
+
+  if (!folder || !msgHdr)
     return;
 
-  // If we are in the middle of a delete or move operation, make sure that
-  // if the user clicks on another message then that message stays selected
-  // and the selection does not "snap back" to the message chosen by
-  // SetNextMessageAfterDelete() when the operation completes (bug 243532).
-  // But the just loaded message might be getting deleted, if the user
-  // deletes it before the message is loaded (bug 183394).
   var wintype = document.documentElement.getAttribute('windowtype');
-  if (wintype == "mail:messageWindow" ||
-      GetThreadTree().view.selection.currentIndex != gSelectedIndexWhenDeleting)
-    gNextMessageViewIndexAfterDelete = -2;
-
-  var msgHdr = msgHdrForCurrentMessage();
+
   gMessageNotificationBar.setJunkMsg(msgHdr);
 
   goUpdateCommand('button_delete');
 
   var markReadAutoMode = gPrefBranch.getBoolPref("mailnews.mark_message_read.auto");
 
   // We just finished loading a message. If messages are to be marked as read
   // automatically, set a timer to mark the message is read after n seconds
@@ -3062,17 +3004,17 @@ function OnMsgLoaded(aUrl)
     {
       MarkMessageAsRead(msgHdr);
     }
   }
 
   // See if MDN was requested but has not been sent.
   HandleMDNResponse(aUrl);
 
-  if (!IsImapMessage(msgURI))
+  if (!gFolderDisplay.selectedMessageIsImap)
     return;
 
   var imapServer = folder.server.QueryInterface(Components.interfaces.nsIImapIncomingServer);
   if (imapServer.storeReadMailInPFC)
   {
     // Look in read mail PFC for msg with same msg id - if we find one,
     // don't put this message in the read mail pfc.
     var outputPFC = imapServer.GetReadMailPFC(true);
@@ -3100,26 +3042,25 @@ function OnMsgLoaded(aUrl)
  * no need to check it either.
  */
 function HandleMDNResponse(aUrl)
 {
   if (!aUrl)
     return;
 
   var msgFolder = aUrl.folder;
-  var msgURI = GetLoadedMessage();
-  if (!msgFolder || !msgURI || IsNewsMessage(msgURI))
+  var msgHdr = gFolderDisplay.selectedMessage;
+  if (!msgFolder || !msgHdr || gFolderDisplay.selectedMessageIsNews)
     return;
 
   // if the message is marked as junk, do NOT attempt to process a return receipt
   // in order to better protect the user
   if (SelectedMessagesAreJunk())
     return;
 
-  var msgHdr = messenger.msgHdrFromURI(msgURI);
   var mimeHdr;
 
   try {
     mimeHdr = aUrl.mimeHeaders;
   } catch (ex) {
     return;
   }
 
@@ -3159,30 +3100,26 @@ function HandleMDNResponse(aUrl)
   msgHdr.OrFlags(Components.interfaces.nsMsgMessageFlags.MDNReportSent);
 
   // Commit db changes.
   var msgdb = msgFolder.msgDatabase;
   if (msgdb)
     msgdb.Commit(ADDR_DB_LARGE_COMMIT);
 }
 
-function QuickSearchFocus() 
+function QuickSearchFocus()
 {
   var quickSearchTextBox = document.getElementById('searchInput');
   if (quickSearchTextBox)
     quickSearchTextBox.focus();
 }
 
 function MsgSearchMessages()
 {
-  var preselectedFolder = null;
-  if ("GetFirstSelectedMsgFolder" in window)
-    preselectedFolder = GetFirstSelectedMsgFolder();
-
-  var args = { folder: preselectedFolder };
+  var args = { folder: gFolderDisplay.displayedFolder };
   OpenOrFocusWindow(args, "mailnews:search", "chrome://messenger/content/SearchDialog.xul");
 }
 
 function MsgJunkMailInfo(aCheckFirstUse)
 {
   if (aCheckFirstUse) {
     if (!pref.getBoolPref("mailnews.ui.junk.firstuse"))
       return;
@@ -3206,17 +3143,17 @@ function MsgJunkMailInfo(aCheckFirstUse)
                       "centerscreen,resizeable=no,titlebar,chrome,modal", null);
 }
 
 function MsgSearchAddresses()
 {
   var args = { directory: null };
   OpenOrFocusWindow(args, "mailnews:absearch", "chrome://messenger/content/ABSearchDialog.xul");
 }
- 
+
 function MsgFilterList(args)
 {
   OpenOrFocusWindow(args, "mailnews:filterlist", "chrome://messenger/content/FilterListDialog.xul");
 }
 
 function GetWindowByWindowType(windowType)
 {
   var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1']
@@ -3244,17 +3181,17 @@ function FeedSetContentViewToggle()
   gShowFeedSummaryToggle = true;
   FeedSetContentView(gShowFeedSummary ? 0 : 1);
 }
 
 // Check message format
 function FeedCheckContentFormat()
 {
   // Not an rss message
-  if (!IsFeedItem())
+  if (!gFolderDisplay.selectedMessageIsFeed)
     return false;
 
   var contentWindowDoc = window.top.content.document;
 
   // Thunderbird 2 rss messages with 'Show article summary' not selected,
   // ie message body constructed to show web page in an iframe, can't show
   // a summary - notify user.
   var rssIframe = contentWindowDoc.getElementById('_mailrssiframe');
--- a/mail/base/content/mailWindowOverlay.xul
+++ b/mail/base/content/mailWindowOverlay.xul
@@ -57,16 +57,18 @@
   %brandDTD;
 ]>
 
 <overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
 <script type="application/x-javascript" src="chrome://messenger/content/mailCommands.js"/>
 <script type="application/x-javascript" src="chrome://messenger/content/junkCommands.js"/>
 <script type="application/x-javascript" src="chrome://messenger/content/mailWindowOverlay.js"/>
+<script type="application/x-javascript" src="chrome://messenger/content/messageDisplay.js"/>
+<script type="application/x-javascript" src="chrome://messenger/content/folderDisplay.js"/>
 <script type="application/x-javascript" src="chrome://messenger-newsblog/content/newsblogOverlay.js"/>
 <script type="application/x-javascript" src="chrome://messenger/content/mail-offline.js"/>
 <script type="application/x-javascript" src="chrome://global/content/printUtils.js"/>
 <script type="application/x-javascript" src="chrome://messenger/content/msgViewPickerOverlay.js"/>
 <script type="application/x-javascript" src="chrome://global/content/viewZoomOverlay.js"/>
 
 <stringbundleset id="stringbundleset">
   <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
@@ -722,17 +724,17 @@
               oncommand="MsgGetMessage();"/>
     <menuitem id="folderPaneContext-openNewWindow"
               label="&folderContextOpenNewWindow.label;"
               accesskey="&folderContextOpenNewWindow.accesskey;"
               oncommand="MsgOpenNewWindowForFolder(null,-1);"/>
     <menuitem id="folderPaneContext-openNewTab"
               label="&folderContextOpenNewTab.label;"
               accesskey="&folderContextOpenNewTab.accesskey;"
-              oncommand="MsgOpenNewTabForFolder(null,-1);"/>
+              oncommand="MsgOpenNewTabForFolder();"/>
     <menuitem id="folderPaneContext-searchMessages"
               label="&folderContextSearchMessages.label;"
               accesskey="&folderContextSearchMessages.accesskey;"
               command="cmd_search"/>
     <menuitem id="folderPaneContext-subscribe"
               label="&folderContextSubscribe.label;"
               accesskey="&folderContextSubscribe.accesskey;"
               oncommand="MsgSubscribe();"/>
new file mode 100644
--- /dev/null
+++ b/mail/base/content/messageDisplay.js
@@ -0,0 +1,460 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Mail Client.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Andrew Sutherland <asutherland@asutherland.org>
+ *   David Bienvenu <bienvenu@nventure.com>
+ *   Siddharth Agarwal <sid.bugzilla@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Base abstraction for message display along the line of FolderDisplayWidget,
+ *  but for message display.  This really only exists to keep
+ *  FolderDisplayWidget manageable and free from taking on responsibility for
+ *  the (different) horrors of message display.  The reality of the situation
+ *  is that FolderDisplayWidget still has a lot to do with message display,
+ *  and so we are just where helper logic gets to live, but the FDW still
+ *  may deal with some things internally.
+ * You should not use this class directly, but rather its friendly subclasses
+ *  that live later in this file.
+ */
+function MessageDisplayWidget() {
+}
+MessageDisplayWidget.prototype = {
+  _active: false,
+  get active MessageDisplayWidget_get_active() {
+    return this._active;
+  },
+
+  /**
+   * Track whether the single message display pane is desired to be displayed
+   *  (it is actually displayed when active, does't matter when not), or
+   *  otherwise the multiple message display pane is desired to be displayed.
+   */
+  _singleMessageDisplay: null,
+  get singleMessageDisplay MessageDisplayWidget_get_singleMessageDisplay() {
+    // when null, assume single message display
+    return this._singleMessageDisplay != false;
+  },
+  set singleMessageDisplay MessageDisplayWidget_set_singleMessageDisplay(
+      aSingleDisplay) {
+    if (this._singleMessageDisplay != aSingleDisplay) {
+      this._singleMessageDisplay = aSingleDisplay;
+      if (this._active)
+        this._updateActiveMessagePane();
+    }
+  },
+
+  /**
+   * Set pane visibility based on this.singleMessageDisplay.
+   */
+  _updateActiveMessagePane: function MessageDisplayWidget_updateMessagePane() {
+    // _singleMessageDisplay can be null, so use the property (getter)
+    document.getElementById("singlemessage").hidden =
+      !this.singleMessageDisplay;
+    document.getElementById("multimessage").hidden =
+      this.singleMessageDisplay;
+  },
+
+  /**
+   * Cleanup the MessageDisplayWidget in preparation for going away.  Called by
+   *  FolderDisplayWidget's close method.
+   */
+  _close: function MessageDisplayWidget_close() {
+    // mark ourselves inactive without doing any work.
+    this._active = false;
+  },
+
+  /**
+   * The FolderDisplayWidget that owns us.
+   */
+  folderDisplay: null,
+  /**
+   * The currently displayed message's nsIMsgDBHdr.  null if there's no message.
+   */
+  displayedMessage: null,
+
+  clearDisplay: function MessageDisplayWidget_clearDisplay() {
+    this.displayedMessage = null;
+    this.messageLoading = false;
+    this.messageLoaded = false;
+    ClearPendingReadTimer();
+    ClearMessagePane();
+  },
+
+  onCreatedView: function MessageDisplayWidget_onCreatedView() {
+    // we need to compel setting this because nsMsgSearchDBView defaults it on
+    this.folderDisplay.view.dbView.suppressMsgDisplay = !this.visible;
+  },
+
+  /**
+   * FolderDisplayWidget tells us when it is killing the view, which means our
+   *  displayed message is no longer valid.
+   */
+  onDestroyingView: function MessageDisplayWidget_onDestroyingView(
+      aFolderIsComingBack) {
+    this.displayedMessage = null;
+    // The only time we want to do anything is when the folder is not coming
+    //  back.  If it is coming back, we can handle things when it shows up.
+    if (!aFolderIsComingBack && this._active) {
+      // and in this case, we just want to clear things.
+      this.clearDisplay();
+      this.singleMessageDisplay = true;
+    }
+  },
+
+  /**
+   * FolderDisplayWidget tells us when a message is being displayed.
+   */
+  onDisplayingMessage: function MessageDisplayWidget_onDisplayingMessage(
+      aMsgHdr) {
+    this.displayedMessage = aMsgHdr;
+    this.messageLoading = true;
+    this.messageLoaded = false;
+  },
+
+  /**
+   * The maximum number of messages to summarize at any given time.  If there
+   *  are more messages than this, we don't summarize, and instead give a blank
+   *  window pane.  Arguably something that says "there are two many messages"
+   *  would be a better idea.
+   */
+  MAX_MESSAGES_TO_SUMMARIZE: 100,
+
+  /**
+   * FolderDisplayWidget tells us when the set of selected messages has changed.
+   *  FDW is doing this because an nsMsgDBView/subclass called
+   *  summarizeSelection.  Although the call is purely an outgrowth of the
+   *  introduction of folder summaries, it also provides a means for us to
+   *  completely replace the nsMsgDBView logic.  Namely, we get first bat at
+   *  taking an action as a result of the selection change, and we can cause the
+   *  nsMsgDBView to not do anything (by returning true).
+   * This notification will come prior to an onDisplayingMessage notification,
+   *  and we will only get that notification if we return false and the
+   *  nsMsgDBView logic wanted to display a message (read: there is exactly
+   *  one message displayed and it wasn't already displayed.)
+   *
+   * The prime responsibilities of this function are:
+   * - To make sure the right message pane (single or multi) is displayed.
+   * - To kick off a multi-message or thread summarization if multiple messages
+   *   are selected.
+   * - To throttle the rate at which we perform summarizations in case the
+   *   selection is being updated frequently.  This could be as a result of the
+   *   user holding down shift and using their keyboard to expand the selection,
+   *   use of the archive mechanism, or other.
+   * - To clear the message pane if no messages are selected.  This used to be
+   *   triggered by nsMsgDBView::SelectionChanged but is now our responsibility.
+   *
+   * In the event that the controlling preference for message summarization is
+   *  not enabled (mail.operate_on_msgs_in_collapsed_threads), and there is a
+   *  multi-selection, we just clear the display.
+   *
+   * @return true if we handled the selection notification and the nsMsgDBView
+   *  should do nothing, false if we did not and the nsMsgDBView should use its
+   *  logic to display a message.
+   */
+  onSelectedMessagesChanged:
+      function MessageDisplayWidget_onSelectedMessagesChanged() {
+    // If we are not active, we should not be touching things.  pretend we are
+    //  handling whatever is happening so the nsMsgDBView doesn't try and do
+    //  anything.  makeActive will trigger a fake SelectionChanged notification
+    //  when we switch, which should put everything in its right place.
+    if (!this.active)
+      return true;
+
+    let selectedCount = this.folderDisplay.selectedCount;
+
+    if (selectedCount == 0) {
+      // davida, put your folder summary stuff here.
+      this.clearDisplay();
+      this.singleMessageDisplay = true;
+    }
+    else if (selectedCount == 1) {
+      // the display of the message is happening without us
+      this.singleMessageDisplay = true;
+
+      // This is the only case we don't handle and want the nsMsgDBView to
+      //  take care of.
+      return false;
+    }
+    // we have a limit on the number of messages and if the pref is enabled
+    else if ((selectedCount < this.MAX_MESSAGES_TO_SUMMARIZE) &&
+        gPrefBranch.getBoolPref("mail.operate_on_msgs_in_collapsed_threads")) {
+      // _showSummary is responsible for handling the "don't resummarize too
+      //  often" logic, as well as updating singleMessageDisplay.
+      this._showSummary();
+    }
+    // and so we clear things
+    else {
+      this.clearDisplay();
+      this.singleMessageDisplay = true;
+    }
+
+    return true;
+  },
+
+  /**
+   * If we are already summarized and we get a new request to summarize, require
+   *  that the selection has been stable for at least this many milliseconds
+   *  before resummarizing.
+   * Unit tests know about this variable and poke at it, so don't change the name
+   *  without making sure you update the unit tests.  (Not that you would commit
+   *  code without first running all tests yourself...)
+   */
+  SUMMARIZATION_SELECTION_STABILITY_INTERVAL_MS: 100,
+  /**
+   * The timeout ID resulting from the call to window.setTimeout that we are
+   *  using to require the selection be 'stabilized' before re-summarizing.
+   */
+  _summaryStabilityTimeout: null,
+
+  /**
+   * Updates message summaries with care to throttle the summarization when the
+   *  selection is rapidly changing.  We require that either we have not
+   *  summarized anything 'recently', or that the selection has been stable for
+   *  SUMMARIZATION_SELECTION_STABILITY_INTERVAL_MS ms before we update the
+   *  summary.  'Recently' for our purposes means that it has been at least
+   *  SUMMARIZATION_SELECTION_STABILITY_INTERVAL_MS ms since our last summary.
+   *
+   * Example event sequences (assuming a 100ms stability interval):
+   * - User selects a collapsed thread => we summarize the selection and set a
+   *    100ms timer set to call _clearSummaryTimer.
+   * - User extends the selection 50ms later => the timer has not elapsed so we
+   *    reset it to 100ms to call _showSummary.
+   * - User extends the selection yet again 50ms later => timer has not elapsed
+   *    so we reset it to 100ms to call _showSummary.
+   * - 100ms later, the timer elapses => we call _showSummary which updates the
+   *    summary.
+   * - 2 seconds later, the user selects some other messages => we summarize the
+   *    select and set a 100ms timer set to call _clearSummaryTimer.
+   * - 100ms later, _clearSummaryTimer clears _summaryStabilityTimeout so that
+   *    the next selection change will immediately summarize.
+   *
+   * @param aIsCallback Is this a callback to ourselves?  Callers should not set
+   *     this, leaving it undefined.
+   */
+  _showSummary: function MessageDisplayWidget_showSummary(aIsCallback) {
+    // note: if we are in this function, we already know that summaries are
+    //  enabled.
+
+    // If this is not a callback from the timeout and we currently have a
+    //  timeout, that means that we need to wait for the selection to stabilize.
+    //  The fact that we are getting called means the selection has just changed
+    //  yet again and is not stable, so reset the timer for the full duration.
+    if (!aIsCallback && this._summaryStabilityTimeout != null) {
+      clearTimeout(this._summaryStabilityTimeout);
+      this._summaryStabilityTimeout =
+        setTimeout(this._wrapShowSummary,
+                   this.SUMMARIZATION_SELECTION_STABILITY_INTERVAL_MS,
+                   this);
+      return;
+    }
+
+    // Bail if our selection count has stabilized outside an acceptable range.
+    let selectedCount = this.folderDisplay.selectedCount;
+    if (selectedCount < 2 || selectedCount > this.MAX_MESSAGES_TO_SUMMARIZE)
+      return;
+
+    // Setup a timeout call to _clearSummaryTimer so that we don't try and
+    //  summarize again within 100ms of now.  Do this before calling
+    //  the summarization logic in case it throws an exception.
+    this._summaryStabilityTimeout =
+      setTimeout(this._clearSummaryTimer,
+                 this.SUMMARIZATION_SELECTION_STABILITY_INTERVAL_MS,
+                 this);
+
+    // figure out if we're looking at one thread or more than one thread
+    let selectedMessages = this.folderDisplay.selectedMessages;
+    if (selectedMessages[0].threadId != selectedMessages[1].threadId)
+      summarizeMultipleSelection(selectedMessages);
+    else
+      summarizeThread(selectedMessages);
+    this.singleMessageDisplay = false;
+  },
+  _wrapShowSummary: function MessageDisplayWidget__wrapShowSummary(aThis) {
+    aThis._showSummary(true);
+  },
+  /**
+   * Just clears the _summaryStabilityTimeout attribute so we can use it as a
+   *  means of checking if we are allowed to display the summary immediately.
+   */
+  _clearSummaryTimer: function MessageDisplayWidget__clearSummaryTimer(aThis) {
+    aThis._summaryStabilityTimeout = null;
+  },
+
+  /**
+   * Called by the FolderDisplayWidget when it is being made active again and
+   *  it's time for us to step up and re-display or clear the message as
+   *  demanded by our multiplexed tab implementation.
+   */
+  makeActive: function MessageDisplayWidget_makeActive() {
+    let wasInactive = !this._active;
+    this._active = true;
+
+    if (wasInactive) {
+      // (see our usage below)
+      let preDisplayedMessage = this.displayedMessage;
+      // Force a synthetic selection changed event.  This will propagate through
+      //  to a call to onSelectedMessagesChanged who will handle making sure the
+      //  right message pane is in use, etc.
+      this.folderDisplay.view.dbView.selectionChanged();
+      // The one potential problem is that the message view only triggers message
+      //  streaming if it doesn't think the message is already displayed.  In that
+      //  case we need to force a re-display by calling reloadMessage.  We can
+      //  detect that case by seeing if our displayedMessage value changes its
+      //  value during this call (because we will receive a onDisplayingMessage
+      //  notification).  If we should be displaying a single message but the
+      //  value does not change, we need to force a re-display.
+      if (this.singleMessageDisplay && this.displayedMessage &&
+          (this.displayedMessage == preDisplayedMessage))
+        this.folderDisplay.view.dbView.reloadMessage();
+    }
+
+    this._updateActiveMessagePane();
+  },
+
+  /**
+   * Called by the FolderDisplayWidget when it is being made inactive or no
+   *  longer requires messages to be displayed.
+   */
+  makeInactive: function MessageDisplayWidget_makeInactive() {
+    this._active = false;
+  }
+};
+
+/**
+ * Display widget abstraction for the 3-pane message view's preview pane/message
+ *  pane. Like the DisplayWidget, it is multiplexed.
+ */
+function MessagePaneDisplayWidget() {
+  MessageDisplayWidget.call(this);
+  this._visible = true;
+}
+MessagePaneDisplayWidget.prototype = {
+  __proto__: MessageDisplayWidget.prototype,
+
+  get visible MessageDisplayWidget_get_visible() {
+    return this._visible;
+  },
+  /**
+   * Tell us whether the message pane is visible or not; this should reflect
+   *  reality and does not define reality.  (Setting this to false does not
+   *  hide the message pane, it merely makes us think it is hidden.)
+   */
+  set visible MessageDisplayWidget_set_visible(aVisible) {
+    // Ignore this if we are inactive.  We don't want to get faked out by things
+    //  happening after our tab has closed up shop.
+    if (!this._active)
+      return;
+
+    // no-op if it's the same
+    if (aVisible == this._visible)
+      return;
+
+    this._visible = aVisible;
+    // Update suppression.  If we were not visible and now are visible, the db
+    //  view itself will handle triggering the message display for us if the
+    //  message was not currently being displayed...
+    let dbView = this.folderDisplay.view.dbView;
+    if (dbView) {
+      let treeSelection = this.folderDisplay.treeSelection;
+      // flag if we need to force the redisplay manually...
+      let needToReloadMessage = treeSelection.count &&
+        dbView.currentlyDisplayedMessage == treeSelection.currentIndex;
+      dbView.suppressMsgDisplay = !this._visible;
+      if (needToReloadMessage)
+        dbView.reloadMessage();
+    }
+    // But if we are no longer visible, it's on us to clear the display.
+    if (!aVisible)
+      this.clearDisplay();
+  },
+};
+
+/**
+ * Message display widget for the "message" tab that is the tab-based equivalent
+ *  of the standalone message window.
+ */
+function MessageTabDisplayWidget() {
+  MessageDisplayWidget.call(this);
+}
+MessageTabDisplayWidget.prototype = {
+  __proto__: MessageDisplayWidget.prototype,
+
+  /**
+   * The message tab always has a visible message pane.
+   */
+  get visible() {
+    return true;
+  },
+  set visible(aIgnored) {
+  },
+
+  onSelectedMessagesChanged:
+      function MessageTabDisplayWidget_onSelectedMessagesChanged() {
+    this.__proto__.__proto__.onSelectedMessagesChanged.apply(this, arguments);
+    if (!this.closing)
+      document.getElementById('tabmail').setTabTitle(this.folderDisplay._tabInfo);
+  },
+
+  /**
+   * A message tab should never ever be blank.  Close the tab if we become
+   *  blank.
+   */
+  clearDisplay: function MessageTabDisplayWidget_clearDisplay() {
+    if (!this.closing) {
+      this.closing = true;
+      document.getElementById('tabmail').closeTab(this.folderDisplay._tabInfo);
+    }
+  }
+};
+
+/**
+ * The search dialog has no message preview pane, and so wants a message
+ *  display widget that is never visible.  No one other than the search
+ *  dialog should use this because the search dialog is bad UI.
+ */
+function NeverVisisbleMessageDisplayWidget() {
+  MessageDisplayWidget.call(this);
+}
+NeverVisisbleMessageDisplayWidget.prototype = {
+  __proto__: MessageDisplayWidget.prototype,
+  get visible() {
+    return false;
+  },
+  onSelectedMessagesChanged: function() {
+    return false;
+  },
+  _updateActiveMessagePane: function() {
+  },
+};
\ No newline at end of file
--- a/mail/base/content/messageWindow.js
+++ b/mail/base/content/messageWindow.js
@@ -1,129 +1,242 @@
-# -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is Mozilla Communicator client code, released
-# March 31, 1998.
-#
-# The Initial Developer of the Original Code is
-# Netscape Communications Corporation.
-# Portions created by the Initial Developer are Copyright (C) 1998-1999
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#
-# 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 *****
+/** ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Communicator client code, released
+ * March 31, 1998.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1998-1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * 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 is where functions related to the standalone message window are kept */
 
+Components.utils.import("resource://app/modules/jsTreeSelection.js");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
 // from MailNewsTypes.h
 const nsMsgKey_None = 0xFFFFFFFF;
 const nsMsgViewIndex_None = 0xFFFFFFFF;
 
 /* globals for a particular window */
 
-var gCurrentMessageUri;
-var gCurrentFolderUri;
-var gThreadPaneCommandUpdater = null;
-var gNextMessageViewIndexAfterDelete = -2;
-var gCurrentFolderToRerootForStandAlone;
-var gRerootOnFolderLoadForStandAlone = false;
-var gNextMessageAfterLoad = null;
-var gMessageToLoad = nsMsgKey_None;
+/// we have no tree view; let people know that.
+var gFolderTreeView = null;
+
+var gFolderDisplay;
+var gMessageDisplay;
+
+/**
+ * We subclass FolderDisplayWidget:
+ * - Because it assumes some thread-pane things that do not apply to us and we
+ *    want to no-op those things out.
+ * - To intercept queries about the selected message so that we can do the
+ *    .eml file thing.  We were originally trying to avoid involving the
+ *    nsMsgDBView, but that might not be important anymore. (future work)
+ */
+function StandaloneFolderDisplayWidget(aMessageDisplayWidget) {
+  FolderDisplayWidget.call(this, null, aMessageDisplayWidget);
+  // do not set the actual treeBox variable or our superclass might try and do
+  //  weird rooting things we don't want to have to think about right now.
+  this._magicTreeSelection = new JSTreeSelection(this.treeBox);
+}
+StandaloneFolderDisplayWidget.prototype = {
+  __proto__: FolderDisplayWidget.prototype,
+
+  /**
+   * If we have a displayed message, then we've got 1 message, otherwise 0.
+   */
+  get selectedCount() {
+    return this.messageDisplay.displayedMessage ? 1 : 0;
+  },
+
+  /**
+   * If we have a selected message, it's the one displayed!  This is more
+   *  straight-forward than having you trace through the tree selection and
+   *  db view logic.
+   */
+  get selectedMessage() {
+    return this.messageDisplay.displayedMessage;
+  },
 
-// the folderListener object
-var folderListener = {
-  OnItemAdded: function(parentItem, item) {},
+  /**
+   * If we have a selected message, it's the one displayed!  This is more
+   *  straight-forward than having you trace through the tree selection and
+   *  db view logic.
+   */
+  get selectedMessages() {
+    return this.messageDisplay.displayedMessage ?
+             [this.messageDisplay.displayedMessage] : [];
+  },
 
-  OnItemRemoved: function(parentItem, item) {},
-  OnItemPropertyChanged: function(item, property, oldValue, newValue) {},
-  OnItemIntPropertyChanged: function(item, property, oldValue, newValue) {
-    if (item.URI == gCurrentFolderUri) {
-      if (property.toString() == "TotalMessages" || property.toString() == "TotalUnreadMessages") {
-        UpdateStandAloneMessageCounts();
-      }
+  /**
+   * We never have a real treeview, so we always want to tell the view about
+   *  the fake tree box so it will actually do something in NoteChange.
+   */
+  onCreatedView:
+      function StandaloneMessageDisplayWidget_onCreatedView() {
+    this._fakeTreeBox.view = this.view.dbView;
+    // only if we're not dealing with a dummy message (from .eml file /
+    //  attachment should we try and hook up the selection object.)  Otherwise
+    //  the view will not operate in stand alone message mode.
+    // XXX the sequencing here may break re-using a message window that is
+    //  showing an .eml file to go to a real message, at least in terms of
+    //  having the selection object properly associated with the tree.
+    if (!this.messageDisplay.isDummy) {
+      this.view.dbView.setTree(this._fakeTreeBox);
+      this.view.dbView.selection = this._magicTreeSelection;
     }
+    this.__proto__.__proto__.onCreatedView.call(this);
+  },
+
+  _superSelectedMessageUrisGetter:
+    FolderDisplayWidget.prototype.__lookupGetter__('selectedMessageUris'),
+  /**
+   * Check with the message display widget to see if it has a dummy; if so, just
+   *  return the dummy's URI, as the nsMsgDBView logic that our superclass uses
+   *  falls down in that case.
+   */
+  get selectedMessageUris() {
+    if (this.messageDisplay.displayedUri)
+      return [this.messageDisplay.displayedUri];
+    return this._superSelectedMessageUrisGetter.call(this);
   },
-  OnItemBoolPropertyChanged: function(item, property, oldValue, newValue) {},
-  OnItemUnicharPropertyChanged: function(item, property, oldValue, newValue){},
-  OnItemPropertyFlagChanged: function(item, property, oldFlag, newFlag) {},
+
+  /// folder display will want to show the thread pane; we need do nothing
+  _showThreadPane: function () {},
+  _showAccountCentral: function () {},
+
+  _updateThreadDisplay: function () {},
+
+  onMessageCountsChanged:
+      function StandaloneFolderDisplayWidget_onMessageCountsChaned() {
+    UpdateStatusMessageCounts();
+  },
+};
+
 
-  OnItemEvent: function(folder, event) {
-    var eventType = event.toString();
+/**
+ * Display widget abstraction for a standalone message display.  Right now
+ *  I think this means the standalone message window, and not the 'message in a
+ *  tab' thing, which is really just a perverted configuration of the 3-pane
+ *  format.
+ */
+function StandaloneMessageDisplayWidget() {
+  MessageDisplayWidget.call(this);
+  /**
+   * Indicate whether the message being displayed is a 'dummy' because it is
+   *  backed not by an nsIMsgDBHdr but instead by a file on disk or an
+   *  attachment on some mail message.
+   */
+  this.isDummy = false;
+  /**
+   * When displaying a dummy message, this is the URI of the message that we are
+   *  displaying.  If we are not displaying a dummy message, this is null.
+   */
+  this.displayedUri = null;
+}
+StandaloneMessageDisplayWidget.prototype = {
+  __proto__: MessageDisplayWidget.prototype,
 
-    if (eventType == "DeleteOrMoveMsgCompleted")
-      HandleDeleteOrMoveMsgCompleted(folder);
-    else if (eventType == "DeleteOrMoveMsgFailed")
-      HandleDeleteOrMoveMsgFailed(folder);
-    else if (eventType == "FolderLoaded") {
-      if (folder) {
-        var uri = folder.URI;
-        if (uri == gCurrentFolderToRerootForStandAlone) {
-          gCurrentFolderToRerootForStandAlone = null;
-          folder.endFolderLoading();
-          if (gRerootOnFolderLoadForStandAlone) {
-            RerootFolderForStandAlone(uri);
-          }
-        }
-      }
+  /**
+   * The message pane is a standalone display widget is always visible.
+   */
+  get visible() {
+    return true;
+  },
+  set visible(aIgnored) {
+  },
+
+  /**
+   * Display the external message (from disk or attachment) named by the URI.
+   */
+  displayExternalMessage:
+      function StandaloneMessageDisplayWidget_displayExternalMessage(aUri) {
+    this.isDummy = true;
+    this.displayedUri = aUri;
+    this.onDisplayingMessage(messageHeaderSink.dummyMsgHeader);
+    UpdateMailToolbar("external message display");
+    // null out the selection on the view so it operates in stand alone mode
+    this.folderDisplay.view.dbView.selection = null;
+    this.folderDisplay.view.dbView.loadMessageByUrl(aUri);
+  },
+
+  clearDisplay: function () {
+    window.close();
+  },
+  _updateActiveMessagePane: function() {
+    // no-op.  the message pane is always visible.
+  },
+
+  onDisplayingMessage:
+      function StandaloneMessageDisplayWidget_onDisplayingMessage(aMsgHdr) {
+    this.__proto__.__proto__.onDisplayingMessage.call(this, aMsgHdr);
+
+    // - set the window title to the message subject (and maybe the app name)
+    let title = aMsgHdr.mime2DecodedSubject;
+    if (!gPlatformOSX)
+      title += " - " + gBrandBundle.getString("brandFullName");
+    document.title = title;
+
+    this.isDummy = aMsgHdr.folder == null;
+    if (!this.isDummy)
+      this.displayedUri = null;
+  },
+
+  onSelectedMessagesChanged: function () {
+    if (this.folderDisplay.treeSelection.count == 0) {
+      window.close();
+      return true;
     }
-    else if (eventType == "JunkStatusChanged") {
-      HandleJunkStatusChanged(folder);
-    }
-  }
-}
+    return false;
+  },
+};
 
 var messagepaneObserver = {
 
   canHandleMultipleItems: false,
 
   onDrop: function (aEvent, aData, aDragSession)
   {
     var sourceUri = aData.data;
-    if (sourceUri != gCurrentMessageUri)
+    if (!gFolderDisplay.selectedMessaage ||
+        sourceUri != gFolderDisplay.selectedMessageUris[0])
     {
-      var msgHdr = GetMsgHdrFromUri(sourceUri);
-
-      // Reset the window's message uri and folder uri vars, and
-      // update the command handlers to what's going to be used.
-      // This has to be done before the call to CreateView().
-      gCurrentMessageUri = sourceUri;
-      gCurrentFolderUri = msgHdr.folder.URI;
-      UpdateMailToolbar('onDrop');
-
-      // even if the folder uri's match, we can't use the existing view
-      // (msgHdr.folder.URI == windowID.gCurrentFolderUri)
-      // the reason is quick search and mail views.
-      // see bug #187673
-      CreateView(aDragSession.sourceNode.ownerDocument.defaultView.gDBView);
-      LoadMessageByMsgKey(msgHdr.messageKey);
+      var msgHdr = messenger.msgHdrFromURI(sourceUri);
+      let originGlobal = aDragSession.sourceNode.ownerDocument.defaultValue;
+      gFolderDisplay.cloneView(originGlobal.gFolderDisplay.view);
+      gFolderDisplay.selectMessage(msgHdr);
     }
   },
 
   onDragOver: function (aEvent, aFlavour, aDragSession)
   {
     var messagepanebox = document.getElementById("messagepanebox");
     messagepanebox.setAttribute("dragover", "true");
   },
@@ -144,295 +257,144 @@ var messagepaneObserver = {
   getSupportedFlavours: function ()
   {
     var flavourSet = new FlavourSet();
     flavourSet.appendFlavour("text/x-moz-message");
     return flavourSet;
   }
 };
 
-function nsMsgDBViewCommandUpdater()
-{}
-
-function UpdateStandAloneMessageCounts()
+function UpdateStatusMessageCounts()
 {
   // hook for extra toolbar items
   var observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
   observerService.notifyObservers(window, "mail:updateStandAloneMessageCounts", "");
 }
 
-nsMsgDBViewCommandUpdater.prototype =
-{
-  updateCommandStatus : function()
-    {
-      // the back end is smart and is only telling us to update command status
-      // when the # of items in the selection has actually changed.
-      UpdateMailToolbar("dbview, std alone window");
-    },
-
-  displayMessageChanged : function(aFolder, aSubject, aKeywords)
-  {
-    setTitleFromFolder(aFolder, aSubject);
-    ClearPendingReadTimer(); // we are loading / selecting a new message so kill the mark as read timer for the currently viewed message
-    gCurrentMessageUri = gDBView.URIForFirstSelectedMessage;
-    UpdateStandAloneMessageCounts();
-    goUpdateCommand("button_delete");
-    goUpdateCommand("button_junk");
-    goUpdateCommand("button_goBack");
-    goUpdateCommand("button_goForward");
-    goUpdateCommand("button_reply");
-    goUpdateCommand("button_replyall");
-    goUpdateCommand("button_replylist");
-  },
-
-  updateNextMessageAfterDelete : function()
-  {
-    SetNextMessageAfterDelete();
-  },
-
-  summarizeSelection : function() {return false},
-
-  QueryInterface : function(iid)
-  {
-    if (iid.equals(Components.interfaces.nsIMsgDBViewCommandUpdater) ||
-        iid.equals(Components.interfaces.nsISupports))
-      return this;
-
-    throw Components.results.NS_NOINTERFACE;
-  }
-}
-
-function HandleDeleteOrMoveMsgCompleted(folder)
-{
-  if ((folder.URI == gCurrentFolderUri))
-  {
-    gDBView.onDeleteCompleted(true);
-    if (gNextMessageViewIndexAfterDelete != nsMsgViewIndex_None)
-    {
-      var nextMstKey = gDBView.getKeyAt(gNextMessageViewIndexAfterDelete);
-
-      if (pref.getBoolPref("mail.close_message_window.on_delete")) {
-        // Tell the main window to select the next message since we
-        // won't be viewing it automatically in the standalone window.
-        var treeView = window.opener.document.getElementById("threadTree").view;
-        if (gDBView.removeRowOnMoveOrDelete && gNextMessageViewIndexAfterDelete >= 0) {
-          gDBView.suppressCommandUpdating = true;
-          treeView.selection.select(gNextMessageViewIndexAfterDelete);
-          treeView.selectionChanged();
-
-          window.opener.EnsureRowInThreadTreeIsVisible(gNextMessageViewIndexAfterDelete);
-          gDBView.suppressCommandUpdating = false;
-        }
-
-        window.close();
-      }
-      else if (nextMstKey != nsMsgKey_None) {
-        LoadMessageByViewIndex(gNextMessageViewIndexAfterDelete);
-      }
-      else {
-        window.close();
-      }
-    }
-    else
-    {
-      // close the stand alone window because there are no more messages in the folder
-      window.close();
-    }
-  }
-}
-
-function HandleDeleteOrMoveMsgFailed(folder)
-{
-  gDBView.onDeleteCompleted(false);
-}
-
-function IsCurrentLoadedFolder(folder)
-{
-  return (folder.URI == gCurrentFolderUri);
-}
-
-
 // we won't show the window until the onload() handler is finished
 // so we do this trick (suggested by hyatt / blaker)
 function OnLoadMessageWindow()
 {
   setTimeout(delayedOnLoadMessageWindow, 0); // when debugging, set this to 5000, so you can see what happens after the window comes up.
 }
 
 function delayedOnLoadMessageWindow()
 {
   HideMenus();
   ShowMenus();
   MailOfflineMgr.init();
   CreateMailWindowGlobals();
   verifyAccounts(null);
 
+  /**
+   * Create a message listener so that we can update the title once the message
+   *  finishes streaming when it's a dummy.
+   */
+  gMessageListeners.push({
+    onStartHeaders: function () {},
+    onEndHeaders: function() {
+      if (gMessageDisplay.isDummy)
+        gMessageDisplay.onDisplayingMessage(messageHeaderSink.dummyMsgHeader);
+      UpdateMailToolbar(".eml/message from attachment finished loading");
+    },
+    onEndAttachments: function () {},
+  });
+
   InitMsgWindow();
 
   messenger.setWindow(window, msgWindow);
   // FIX ME - later we will be able to use onload from the overlay
   OnLoadMsgHeaderPane();
 
-  var nsIFolderListener = Components.interfaces.nsIFolderListener;
-  var notifyFlags = nsIFolderListener.removed | nsIFolderListener.event | nsIFolderListener.intPropertyChanged;
-  Components.classes["@mozilla.org/messenger/services/session;1"]
-            .getService(Components.interfaces.nsIMsgMailSession)
-            .AddFolderListener(folderListener, notifyFlags);
-
-  var originalView = null;
-  var folder = null;
-  var messageUri;
-  var loadCustomMessage = false;       //set to true when either loading a message/rfc822 attachment or a .eml file
-  if (window.arguments)
-  {
-    if (window.arguments[0])
-    {
-      try
-      {
-        messageUri = window.arguments[0];
-        if (messageUri instanceof Components.interfaces.nsIURI)
-        {
-          loadCustomMessage = /type=application\/x-message-display/.test(messageUri.spec);
-          gCurrentMessageUri = messageUri.spec;
-          if (messageUri.folder && messageUri instanceof Components.interfaces.nsIMsgMailNewsUrl)
-            folder = messageUri.folder;
-        }
-      }
-      catch(ex)
-      {
-        folder = null;
-        dump("## ex=" + ex + "\n");
-      }
-
-      if (!gCurrentMessageUri)
-        gCurrentMessageUri = window.arguments[0];
-    }
-    else
-      gCurrentMessageUri = null;
-
-    if (window.arguments[1])
-      gCurrentFolderUri = window.arguments[1];
-    else
-      gCurrentFolderUri = folder ? folder.URI : null;
-
-    if (window.arguments[2])
-      originalView = window.arguments[2];
-
-  }
-
-  CreateView(originalView);
-
   gPhishingDetector.init();
 
   // initialize the customizeDone method on the customizeable toolbar
   var toolbox = document.getElementById("mail-toolbox");
   toolbox.customizeDone = function(aEvent) { MailToolboxCustomizeDone(aEvent, "CustomizeMailToolbar"); };
 
   var toolbarset = document.getElementById('customToolbars');
   toolbox.toolbarset = toolbarset;
 
-  setTimeout(OnLoadMessageWindowDelayed, 0, loadCustomMessage);
+  SetupCommandUpdateHandlers();
 
-  SetupCommandUpdateHandlers();
+  gMessageDisplay = new StandaloneMessageDisplayWidget();
+  gFolderDisplay = new StandaloneFolderDisplayWidget(gMessageDisplay);
+  gFolderDisplay.msgWindow = msgWindow;
+  gFolderDisplay.messenger = messenger;
+
+  setTimeout(actuallyLoadMessage, 0);
 }
 
-function OnLoadMessageWindowDelayed(loadCustomMessage)
-{
-  if (loadCustomMessage)
-  {
-    gDBView.suppressMsgDisplay = false;
-    gDBView.loadMessageByUrl(gCurrentMessageUri);
-  }
-  else
+function actuallyLoadMessage() {
+  /*
+   * Our actual use cases that drive the arguments we take are:
+   * 1) Displaying a message from disk or that was an attachment on a message.
+   *    Such messages have no (real) message header and must come in the form of
+   *    a URI.  (The message display code creates a 'dummy' header.)
+   * 2) Displaying a message that has a header available, either as a result of
+   *    the user selecting a message in another window to spawn us or through
+   *    some indirection like displaying a message by message-id.  (The
+   *    newsgroup UI exposes this, as well as the spotlight/vista indexers.)
+   *
+   * We clone views when possible for:
+   * - Consistency of navigation within the message display.  Users would find
+   *   it odd if they showed a message from a cross-folder view but ended up
+   *   navigating around the message's actual folder.
+   * - Efficiency.  It's faster to clone a view than open a new one.
+   *
+   * Our argument idioms for the use cases are thus:
+   * 1) [A Message URI] where the URI is an nsIURL corresponding to a message
+   *     on disk or that is an attachment part on another message.
+   * 2) [A Message header, (optional) the origin DBViewWraper]
+   *
+   * Our original set of arguments, in case these get passed in and you're
+   *  wondering why we explode, was:
+   *   0: A message URI, string or nsIURI.
+   *   1: A folder URI.  If arg 0 was an nsIURI, it may have had a folder attribute.
+   *   2: The nsIMsgDBView used to open us.
+   */
+  if (window.arguments && window.arguments.length)
   {
-    var msgKey = extractMsgKeyFromURI(gCurrentMessageUri);
-    var viewIndex = gDBView.findIndexFromKey(msgKey, true);
-    // the message may not appear in the view if loaded from a search dialog
-    if (viewIndex != nsMsgViewIndex_None)
-      LoadMessageByViewIndex(viewIndex);
-    else
-      messenger.openURL(gCurrentMessageUri);
+    // message header?
+    if (window.arguments[0] instanceof Components.interfaces.nsIMsgDBHdr) {
+      let msgHdr = window.arguments[0];
+      let originViewWrapper = window.arguments.length > 1 ?
+        window.arguments[1] : null;
+      if (originViewWrapper)
+        gFolderDisplay.cloneView(originViewWrapper);
+      else
+        gFolderDisplay.show(msgHdr.folder);
+      gFolderDisplay.selectMessage(msgHdr);
+    }
+    // it must be a URI for a message lacking a backing header
+    else {
+      // Here's how this goes.  nsMessenger::LoadURL checks out the URL we
+      //  pass it, and if it sees that the URI starts with "file:" or contains
+      //  "type=application/x-message-display" then it knows it needs to
+      //  create a dummy header.  It gets the 'dummyMsgHeader' property from
+      //  the js message header sink.
+      // Additionally, nsMessenger::MsgHdrFromURI checks the URI we pass it
+      //  and if it meets either of those same constraints (assuming it has a
+      //  msgWindow), it will retrieve the header sink off the msgWindow, get
+      //  the dummy header, and return that.
+      // so...
+      // - create a search view for the standalone dude
+      gFolderDisplay.view.openSearchView();
+      // - load the message
+      let messageURI = window.arguments[0];
+      if (messageURI instanceof Components.interfaces.nsIURI)
+        messageURI = messageURI.spec;
+      gMessageDisplay.displayExternalMessage(messageURI);
+    }
   }
-  gNextMessageViewIndexAfterDelete = gDBView.msgToSelectAfterDelete;
-  UpdateStandAloneMessageCounts();
+
+  gFolderDisplay.makeActive();
 
   // set focus to the message pane
   window.content.focus();
-
-  // since we just changed the pane with focus we need to update the toolbar to reflect this
-  // XXX TODO
-  // can we optimize
-  // and just update cmd_delete and button_delete?
-  UpdateMailToolbar("focus");
-}
-
-function CreateView(originalView)
-{
-  var msgFolder = GetLoadedMsgFolder();
-
-  // extract the sort type, the sort order,
-  var sortType;
-  var sortOrder;
-  var viewFlags;
-  var viewType;
-
-  if (originalView)
-  {
-    viewType = originalView.viewType;
-    viewFlags = originalView.viewFlags;
-    sortType = originalView.sortType;
-    sortOrder = originalView.sortOrder;
-  }
-  else if (msgFolder)
-  {
-    var msgDatabase = msgFolder.msgDatabase;
-    if (msgDatabase)
-    {
-      var dbFolderInfo = msgDatabase.dBFolderInfo;
-      sortType = dbFolderInfo.sortType;
-      sortOrder = dbFolderInfo.sortOrder;
-      viewFlags = dbFolderInfo.viewFlags;
-      viewType = dbFolderInfo.viewType;
-      msgDatabase = null;
-      dbFolderInfo = null;
-   }
-  }
-  else
-  {
-    // this is a hack to make opening a stand-alone msg window on a
-    // .eml file work. We use a search view since its much more tolerant
-    // of not having a folder.
-    viewType = nsMsgViewType.eShowSearch;
-  }
-
-  // create a db view
-  CreateBareDBView(originalView, msgFolder, viewType, viewFlags, sortType, sortOrder);
-
-  var uri;
-  if (gCurrentMessageUri)
-    uri = gCurrentMessageUri;
-  else if (gCurrentFolderUri)
-    uri = gCurrentFolderUri;
-  else
-    uri = null;
-
-  SetUpToolbarButtons(uri);
-
-  // hook for extra toolbar items
-  var observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
-  observerService.notifyObservers(window, "mail:setupToolbarItems", uri);
-}
-
-function extractMsgKeyFromURI()
-{
-  var msgKey = -1;
-  var msgHdr =   messenger.msgHdrFromURI(gCurrentMessageUri);
-  if (msgHdr)
-    msgKey = msgHdr.messageKey;
-  return msgKey;
 }
 
 function ShowMenus()
 {
   var openMail3Pane_menuitem = document.getElementById('tasksMenuMail');
   if (openMail3Pane_menuitem)
     openMail3Pane_menuitem.removeAttribute("hidden");
 }
@@ -529,256 +491,108 @@ function HideMenus()
   var menuFileClose = document.getElementById('menu_close');
   var menuFileQuit = document.getElementById('menu_FileQuitItem');
   if (menuFileClose && menuFileQuit)
     menuFileQuit.parentNode.replaceChild(menuFileClose, menuFileQuit);
 }
 
 function OnUnloadMessageWindow()
 {
+  gFolderDisplay.close();
   UnloadCommandUpdateHandlers();
   // FIX ME - later we will be able to use onunload from the overlay
   OnUnloadMsgHeaderPane();
   gPhishingDetector.shutdown();
   OnMailWindowUnload();
 }
 
 function GetSelectedMsgFolders()
 {
-  var folderArray = [];
-  var msgFolder = GetLoadedMsgFolder();
-  if (msgFolder)
-    folderArray[0] = msgFolder;
-
-  return folderArray;
-}
-
-function GetFirstSelectedMessage()
-{
-  return GetLoadedMessage();
+  if (gFolderDisplay.displayedFolder)
+    return [gFolderDisplay.displayedFolder];
+  return [];
 }
 
 function GetNumSelectedMessages()
 {
-  if (gCurrentMessageUri)
-    return 1;
-  else
-    return 0;
-}
-
-function GetSelectedMessages()
-{
-  var messageArray = new Array(1);
-  var message = GetLoadedMessage();
-  if (message)
-    messageArray[0] = message;
-
-  return messageArray;
-}
-
-function GetSelectedIndices(dbView)
-{
-  try {
-    return dbView.getIndicesForSelection({});
-  }
-  catch (ex) {
-    dump("ex = " + ex + "\n");
-    return null;
-  }
-}
-
-function GetLoadedMsgFolder()
-{
-  return (gCurrentFolderUri) ? GetMsgFolderFromUri(gCurrentFolderUri) : null;
-}
-
-function GetSelectedFolderURI()
-{
-  return gCurrentFolderUri;
-}
-
-function GetLoadedMessage()
-{
-  return gCurrentMessageUri;
-}
-
-//Clear everything related to the current message. called after load start page.
-function ClearMessageSelection()
-{
-  gCurrentMessageUri = null;
-  gCurrentFolderUri = null;
-  UpdateMailToolbar("clear msg, std alone window");
-}
-
-function SetNextMessageAfterDelete()
-{
-  gNextMessageViewIndexAfterDelete = gDBView.msgToSelectAfterDelete;
-}
-
-function SelectFolder(folderUri)
-{
-  if (folderUri == gCurrentFolderUri)
-    return;
-
-  var msgfolder = GetMsgFolderFromUri(folderUri)
-  if (!msgfolder || msgfolder.isServer)
-    return;
-
-  // close old folder view
-  var dbview = GetDBView();
-  if (dbview)
-    dbview.close();
-
-  gCurrentFolderToRerootForStandAlone = folderUri;
-  msgWindow.openFolder = msgfolder;
-
-  if (msgfolder.manyHeadersToDownload)
-  {
-    gRerootOnFolderLoadForStandAlone = true;
-    try
-    {
-      // accessing the db causes the folder loaded notification to get sent
-      // for local folders.
-      var db = msgfolder.msgDatabase;
-      msgfolder.startFolderLoading();
-      msgfolder.updateFolder(msgWindow);
-    }
-    catch(ex)
-    {
-      dump("Error loading with many headers to download: " + ex + "\n");
-    }
-  }
-  else
-  {
-    RerootFolderForStandAlone(folderUri);
-    gRerootOnFolderLoadForStandAlone = false;
-    msgfolder.startFolderLoading();
-
-    //Need to do this after rerooting folder.  Otherwise possibility of receiving folder loaded
-    //notification before folder has actually changed.
-    msgfolder.updateFolder(msgWindow);
-  }
-}
-
-function RerootFolderForStandAlone(uri)
-{
-  gCurrentFolderUri = uri;
-
-  // create new folder view
-  CreateView(null);
-
-  if (gMessageToLoad != nsMsgKey_None)
-  {
-    LoadMessageByMsgKey(gMessageToLoad);
-    gMessageToLoad = nsMsgKey_None;
-  }
-  // now do the work to load the appropriate message
-  else if (gNextMessageAfterLoad) {
-    var type = gNextMessageAfterLoad;
-    gNextMessageAfterLoad = null;
-    LoadMessageByNavigationType(type);
-  }
-
-  SetUpToolbarButtons(gCurrentFolderUri);
-
-  UpdateMailToolbar("reroot folder in stand alone window");
-
-  // hook for extra toolbar items
-  var observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
-  observerService.notifyObservers(window, "mail:setupToolbarItems", uri);
-}
-
-function GetMsgHdrFromUri(messageUri)
-{
-  return messenger.msgHdrFromURI(messageUri);
-}
-
-function SelectMessage(messageUri)
-{
-  var msgHdr = GetMsgHdrFromUri(messageUri);
-  LoadMessageByMsgKey(msgHdr.messageKey);
+  return gFolderDisplay.treeSelection.count;
 }
 
 function ReloadMessage()
 {
-  gDBView.reloadMessage();
+  gFolderDisplay.view.dbView.reloadMessage();
 }
 
 function MsgDeleteMessageFromMessageWindow(reallyDelete, fromToolbar)
 {
   // if from the toolbar, return right away if this is a news message
   // only allow cancel from the menu:  "Edit | Cancel / Delete Message"
-  if (fromToolbar)
-  {
-    if (isNewsURI(gCurrentFolderUri))
-    {
-        // if news, don't delete
-        return;
-    }
-  }
+  if (fromToolbar && gDisplayFolder.view.isNewsFolder)
+      return;
 
-  // before we delete
-  SetNextMessageAfterDelete();
+  gFolderDisplay.hintAboutToDeleteMessages();
 
   if (reallyDelete)
-      gDBView.doCommand(nsMsgViewCommandType.deleteNoTrash);
+    gFolderDisplay.doCommand(nsMsgViewCommandType.deleteNoTrash);
   else
-      gDBView.doCommand(nsMsgViewCommandType.deleteMsg);
+    gFolderDisplay.doCommand(nsMsgViewCommandType.deleteMsg);
 }
 
 // MessageWindowController object (handles commands when one of the trees does not have focus)
 var MessageWindowController =
 {
    supportsCommand: function(command)
   {
     switch ( command )
     {
+      // external messages cannot be deleted, mutated, or subjected to filtering
       case "cmd_delete":
-      case "cmd_undo":
-      case "cmd_redo":
       case "cmd_killThread":
       case "cmd_killSubthread":
       case "cmd_watchThread":
       case "button_delete":
       case "button_junk":
       case "cmd_shiftDelete":
-      case "cmd_saveAsFile":
-      case "cmd_saveAsTemplate":
-      case "cmd_viewPageSource":
-      case "cmd_getMsgsForAuthAccounts":
       case "cmd_tag":
       case "button_mark":
       case "cmd_markAsRead":
       case "cmd_markAllRead":
       case "cmd_markThreadAsRead":
       case "cmd_markReadByDate":
       case "cmd_markAsFlagged":
-      case "button_file":
-      case "cmd_file":
       case "cmd_markAsJunk":
       case "cmd_markAsNotJunk":
       case "cmd_recalculateJunkScore":
       case "cmd_applyFiltersToSelection":
       case "cmd_applyFilters":
       case "cmd_runJunkControls":
       case "cmd_deleteJunk":
+        return !gMessageDisplay.isDummy;
+      case "cmd_undo":
+      case "cmd_redo":
+      case "cmd_saveAsFile":
+      case "cmd_saveAsTemplate":
+      case "cmd_viewPageSource":
+      case "cmd_getMsgsForAuthAccounts":
+      case "button_file":
+      case "cmd_file":
       case "cmd_nextMsg":
       case "button_next":
       case "button_previous":
       case "cmd_nextUnreadMsg":
       case "cmd_nextFlaggedMsg":
       case "cmd_nextUnreadThread":
       case "cmd_previousMsg":
       case "cmd_previousUnreadMsg":
       case "cmd_previousFlaggedMsg":
       case "cmd_goForward":
       case "cmd_goBack":
       case "button_goForward":
       case "button_goBack":
-        return !(gDBView.keyForFirstSelectedMessage == nsMsgKey_None);
+        return gFolderDisplay.selectedMessage != null;
 
       case "cmd_reply":
       case "button_reply":
       case "cmd_replySender":
       case "cmd_replyGroup":
       case "cmd_replyall":
       case "button_replyall":
       case "cmd_replylist":
@@ -817,42 +631,45 @@ var MessageWindowController =
         return MailOfflineMgr.isOnline();
       default:
         return false;
     }
   },
 
   isCommandEnabled: function(command)
   {
+    let loadedFolder;
     switch ( command )
     {
       case "cmd_createFilterFromPopup":
       case "cmd_createFilterFromMenu":
-        var loadedFolder = GetLoadedMsgFolder();
+        loadedFolder = gFolderDisplay.displayedFolder;
         if (!(loadedFolder && loadedFolder.server.canHaveFilters))
           return false;
       case "cmd_delete":
         UpdateDeleteCommand();
         // fall through
       case "button_delete":
         UpdateDeleteToolbarButton();
         // fall through
       case "cmd_shiftDelete":
-        var loadedFolder = GetLoadedMsgFolder();
-        return gCurrentMessageUri && loadedFolder && (loadedFolder.canDeleteMessages || isNewsURI(gCurrentFolderUri));
+        return gFolderDisplay.selectedMessage &&
+               gFolderDisplay.displayedFolder &&
+               (gFolderDisplay.displayedFolder.canDeleteMessages ||
+                gFolderDisplay.view.isNewsFolder);
       case "button_junk":
         UpdateJunkToolbarButton();
         // fall through
       case "cmd_markAsJunk":
       case "cmd_markAsNotJunk":
       case "cmd_recalculateJunkScore":
         // can't do junk on news yet
-        return (!isNewsURI(gCurrentFolderUri));
+        return (!gFolderDisplay.view.isNewsFolder);
       case "button_archive":
-        var folder = GetLoadedMsgFolder();
+        var folder = gFolderDisplay.displayedFolder;
         return folder &&
           !(IsSpecialFolder(folder, Components.interfaces.nsMsgFolderFlags.Archive,
                             true));
       case "cmd_archive":
       case "cmd_reply":
       case "button_reply":
       case "cmd_replySender":
       case "cmd_replyGroup":
@@ -878,17 +695,17 @@ var MessageWindowController =
       case "cmd_markAsRead":
       case "cmd_markAllRead":
       case "cmd_markThreadAsRead":
       case "cmd_markReadByDate":
         return(true);
       case "cmd_markAsFlagged":
       case "button_file":
       case "cmd_file":
-        return ( gCurrentMessageUri != null);
+        return ( gFolderDisplay.selectedMessage != null);
       case "cmd_printSetup":
         return true;
       case "cmd_getNewMessages":
       case "button_getNewMessages":
       case "cmd_getMsgsForAuthAccounts":
         // GetMsgs should always be enabled, see bugs 89404 and 111102.
         return true;
       case "cmd_getNextNMessages":
@@ -917,30 +734,29 @@ var MessageWindowController =
       case "cmd_fullZoomEnlarge":
       case "cmd_fullZoomReset":
       case "cmd_fullZoomToggle":
         return true;
       case "button_goForward":
       case "button_goBack":
       case "cmd_goForward":
       case "cmd_goBack":
-        return gDBView &&
-            gDBView.navigateStatus((command == "cmd_goBack" ||
-                                    command == "button_goBack")
-                                    ? nsMsgNavigationType.back : nsMsgNavigationType.forward);
+        return gFolderDisplay.navigateStatus(
+          (command == "cmd_goBack" || command == "button_goBack") ?
+            nsMsgNavigationType.back : nsMsgNavigationType.forward);
       case "cmd_search":
-        var loadedFolder = GetLoadedMsgFolder();
+        loadedFol