/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

Components.utils.import("resource:///modules/folderUtils.jsm");
Components.utils.import("resource:///modules/iteratorUtils.jsm");
Components.utils.import("resource:///modules/mailServices.js");
Components.utils.import("resource:///modules/MailUtils.js");
Components.utils.import("resource:///modules/IOUtils.js");
Components.utils.import("resource://gre/modules/Services.jsm");

var kDefaultMode = "all";

var nsMsgFolderFlags = Components.interfaces.nsMsgFolderFlags;

/**
 * This file contains the controls and functions for the folder pane.
 * The following definitions will be useful to know:
 *
 * gFolderTreeView - the controller for the folder tree.
 * ftvItem  - folder tree view item, representing a row in the tree
 * mode - folder view type, e.g., all folders, favorite folders, MRU...
 */

/**
 * An interface that needs to be implemented in order to add a new view to the
 * folder tree. For default behavior, it is recommended that implementers
 * subclass this interface instead of relying on duck typing.
 *
 * For implementation examples, see |gFolderTreeView._modes|. For how to
 * register this mode with |gFolderTreeView|, see
 * |gFolderTreeView.registerFolderTreeMode|.
 */
var IFolderTreeMode = {
  /**
   * Generates the folder map for this mode.
   *
   * @param aFolderTreeView The gFolderTreeView for which this mode is being
   *     activated.
   *
   * @returns An array containing ftvItem instances representing the top-level
   *     folders in this view.
   */
  generateMap: function IFolderTreeMode_generateMap(aFolderTreeView) {
    return null;
  },

  /**
   * Given an nsIMsgFolder, returns its parent in the map. The default behaviour
   * is to return the folder's actual parent (aFolder.parent). Folder tree modes
   * may decide to override it.
   *
   * If the parent isn't easily computable given just the folder, you may
   * consider generating the entire ftvItem tree at once and using a map from
   * folders to ftvItems.
   *
   * @returns an nsIMsgFolder representing the parent of the folder in the view,
   *     or null if the folder is a top-level folder in the map. It is expected
   *     that the returned parent will have the given folder as one of its
   *     children.
   * @note This function need not guarantee that either the folder or its parent
   *       is actually in the view.
   */
  getParentOfFolder: function IFolderTreeMode_getParentOfFolder(aFolder) {
    return aFolder.parent;
  },

  /**
   * Given an nsIMsgDBHdr, returns the folder it is considered to be contained
   * in, in this mode. This is usually just the physical folder it is contained
   * in (aMsgHdr.folder), but some modes may decide to override this. For
   * example, combined views like Smart Folders return the smart inbox for any
   * messages in any inbox.
   *
   * The folder returned doesn't need to be in the view.

   * @returns The folder the message header is considered to be contained in, in
   *     this mode. The returned folder may or may not actually be in the view
   *     -- however, given a valid nsIMsgDBHdr, it is expected that a) a
   *     non-null folder is returned, and that b) the folder that is returned
   *     actually does contain the message header.
   */
  getFolderForMsgHdr: function IFolderTreeMode_getFolderForMsgHdr(aMsgHdr) {
    return aMsgHdr.folder;
  },

  /**
   * Notified when a folder is added. The default behavior is to add it as a
   * child of the parent item, but some views may decide to override this. For
   * example, combined views like Smart Folders add any new inbox as a child of
   * the smart inbox.
   *
   * @param aParent The parent of the folder that was added.
   * @param aFolder The folder that was added.
   */
  onFolderAdded: function IFolderTreeMode_onFolderAdded(aParent, aFolder) {
    gFolderTreeView.addFolder(aParent, aFolder);
  },

  /**
   * Notified when a folder int property is changed.
   *
   * Returns true if the event was processed inside the function and no further
   * default handling should be done in the caller. Otherwise false.
   *
   * @param aItem      The folder with a change.
   * @param aProperty  The changed property string.
   * @param aOld       The old value of the property.
   * @param aNew       The new value of the property.
   */
  handleChangedIntProperty: function(aItem, aProperty, aOld, aNew) {
    return false;
  }
};

/**
 * This is our controller for the folder-tree. It includes our nsITreeView
 * implementation, as well as other control functions.
 */
var gFolderTreeView = {
  messengerBundle: null,

  /**
   * Called when the window is initially loaded.  This function initializes the
   * folder-pane to the view last shown before the application was closed.
   */
  load: function ftv_load(aTree, aJSONFile) {
    const Cc = Components.classes;
    const Ci = Components.interfaces;
    this._treeElement = aTree;
    this.messengerBundle = document.getElementById("bundle_messenger");

    // 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
      // to migrate
      let modeIndex = Services.prefs.getIntPref("mail.ui.folderpane.view");
      this._mode = this._modeNames[modeIndex];
      Services.prefs.deleteBranch("mail.ui.folderpane");
    } catch(ex) {
      // This is ok.  If we've already migrated we'll end up here
    }

    if (aJSONFile) {
      // Parse our persistent-open-state json file
      let data = IOUtils.loadFileToString(aJSONFile);
      if (data) {
        try {
          this._persistOpenMap = JSON.parse(data);
        } catch (x) {
          Components.utils.reportError(
            gFolderTreeView.messengerBundle
                           .getFormattedString("failedToReadFile", [aJSONFile, x]));
        }
      }
    }

    // Load our data
    this._updateCompactState(this.mode);
    this._selectModeInSelector(this.mode);
    this._rebuild();
    // And actually draw the tree
    aTree.view = this;

    this.toggleCols(true);
    gFolderStatsHelpers.init();

    // Add this listener so that we can update the tree when things change
    MailServices.mailSession.AddFolderListener(this, Ci.nsIFolderListener.all);
  },

  /**
   * Called when the window is being torn down.  Here we undo everything we did
   * onload.  That means removing our listener and serializing our JSON.
   */
  unload: function ftv_unload(aJSONFile) {
    const Cc = Components.classes;
    const Ci = Components.interfaces;

    // Remove our listener
    MailServices.mailSession.RemoveFolderListener(this);

    if (aJSONFile) {
      // Write out our json file...
      let data = JSON.stringify(this._persistOpenMap);
      IOUtils.saveStringToFile(aJSONFile, data);
    }
  },

  /**
   * Extensions can use this function to add a new mode to the folder pane.
   *
   * @param aCommonName  an internal name to identify this mode. Must be unique
   * @param aMode An implementation of |IFolderTreeMode| for this mode.
   * @param aDisplayName  a localized name for this mode
   */
  registerFolderTreeMode: function ftv_registerFolderTreeMode(aCommonName,
                                                              aMode,
                                                              aDisplayName) {
    this._modeNames.push(aCommonName);
    this._modes[aCommonName] = aMode;
    this._modeDisplayNames[aCommonName] = aDisplayName;
  },

  /**
   * Unregisters a previously registered mode. Since common-names must be unique
   * this is all that need be provided to unregister.
   * @param aCommonName  the common-name with which the mode was previously
   *                     registered
   */
  unregisterFolderTreeMode: function ftv_unregisterFolderTreeMode(aCommonName) {
    this._modeNames.splice(this._modeNames.indexOf(aCommonName), 1);
    delete this._modes[aCommonName];
    delete this._modeDisplayNames[aCommonName];
    if (this._mode == aCommonName)
      this.mode = kDefaultMode;
  },

  /**
   * Retrieves a specific mode object
   * @param aCommonName  the common-name with which the mode was previously
   *                     registered
   */
  getFolderTreeMode: function ftv_getFolderTreeMode(aCommonName) {
    return this._modes[aCommonName];
  },

  /**
   * Called to move to the next/prev folder-mode in the list
   *
   * @param aForward  whether or not we should move forward in the list
   */
  cycleMode: function ftv_cycleMode(aForward) {
    let index = this._modeNames.indexOf(this.mode);
    let offset = aForward ? 1 : this._modeNames.length - 1;
    index = (index + offset) % this._modeNames.length;

    this.mode = this._modeNames[index];
  },

  /**
   * If the hidden pref is set, then double-clicking on a folder should open it
   *
   * @param event  the double-click event
   */
  onDoubleClick: function ftv_onDoubleClick(aEvent) {
    if (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)
      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);
    if (row in gFolderTreeView._rowMap)
      return gFolderTreeView._rowMap[row]._folder;
    return null;
  },

  /**
   * Toggle displaying the headers of columns in the folder pane.
   * @param aSetup  Set to true if the columns should be set up according
   *                to the pref, not toggle them.
   */
  toggleCols: function(aSetup = false) {
    let hide = Services.prefs.getBoolPref("mail.folderpane.showColumns");
    if (aSetup)
      hide = !hide;
    this._treeElement.setAttribute("hidecolumnpicker", hide ? "true" : "false");
    for (let columnName of ["folderNameCol", "folderUnreadCol",
                            "folderTotalCol", "folderSizeCol"])
    {
      let column = document.getElementById(columnName);
      if (hide) {
        column.setAttribute("hideheader", "true");
        column.removeAttribute("label");
        if (columnName != "folderNameCol") {
          if (!aSetup) {
            // If user hides the columns store their visible state in a special attribute
            // that is persisted by XUL.
            let state = column.getAttribute("hidden");
            column.setAttribute("hiddeninactive", state);
          }
          column.setAttribute("hidden", "true");
        }
      } else {
        column.setAttribute("label", column.getAttribute("label2"));
        column.setAttribute("hideheader", "false");
        if (!aSetup) {
          // If user unhides the columns restore their visible state
          // from our special attribute.
          if (column.hasAttribute("hiddeninactive")) {
            let state = column.getAttribute("hiddeninactive");
            column.setAttribute("hidden", state);
          } else if (columnName == "folderTotalCol") {
            // If there was no hiddeninactive attribute set, that means this is
            // our first showing of the folder pane columns. Show the TotalCol
            // as a sample so the user notices what is happening.
            column.setAttribute("hidden", "false");
          }
        }
      }
    }

    if (!aSetup)
      Services.prefs.setBoolPref("mail.folderpane.showColumns", !hide);
  },

  /**
   * Toggles the compact view of the current mode.
   *
   * @param aCompact  Boolean telling whether compact view should be enabled.
   */
  toggleCompact: function(aCompact) {
    let targetMode = this.fullMode(this.baseMode(), aCompact);
    this.mode = targetMode;
  },

  /**
   * Toggles the folder mode, but tries to keep the "compact" variant the same
   * as the previous mode.
   *
   * @param aMode  The base name of the new mode selected.
   */
  toggleMode: function(aMode) {
    // Take the base name and add compact variant according to the state of the
    // "Compact" checkbox in the UI.
    let userMode = this.fullMode(aMode,
        document.getElementById("appmenu_compactFolderView").hasAttribute("checked"));

    // Some combinations of user selection and "Compact view" checkbox are not supported.
    // In that case fall back to a version of this mode that exists.
    if (!(userMode in this._modes)) {
      let baseMode = this.baseMode(aMode);
      if (baseMode in this._modes)
        userMode = baseMode;
      else
        userMode = this.fullMode(baseMode, true);
    }

    this.mode = userMode;
  },

  /**
   * Update state of checkboxes according to currently selected mode.
   * Synchronize the state of our 2 "compact" menuitems and decide if they
   * should be disabled.
   *
   * @param aMode  The current folder mode.
   */
  _updateCompactState: function(aMode) {
    let checked = aMode.endsWith("_compact");
    let menuitem = document.getElementById("menu_compactFolderView");
    let appmenuitem = document.getElementById("appmenu_compactFolderView");
    if (checked) {
      menuitem.setAttribute("checked", "true");
      appmenuitem.setAttribute("checked", "true");
    } else {
      menuitem.removeAttribute("checked");
      appmenuitem.removeAttribute("checked");
    }
    let baseMode = this.baseMode(aMode);
    let compactToggleable = (baseMode in this._modes) &&
                            (this.fullMode(baseMode, true) in this._modes);
    menuitem.disabled = !compactToggleable;
    appmenuitem.disabled = !compactToggleable;
  },

  /**
   * A string representation for the current display-mode.  Each value here must
   * correspond to an entry in _modes
   */
  _mode: null,
  get mode() {
    if (!this._mode) {
      this._mode = this._treeElement.getAttribute("mode");
      // this can happen when an extension is removed
      if (!(this._mode in this._modes))
        this._mode = kDefaultMode;
    }
    return this._mode;
  },

  /**
   * @param aMode  The final name of the mode to switch to.
   */
  set mode(aMode) {
    // Ignore unknown modes.
    if (!(aMode in this._modes))
      return;

    this._mode = aMode;
    this._updateCompactState(this._mode);

    this._selectModeInSelector(this._mode);

    // Store current mode and actually build the folder pane.
    this._treeElement.setAttribute("mode", this._mode);
    this._rebuild();
  },

  /**
   * Name of the mode without the _compact suffix, used e.g. in the menulists.
   *
   * @param aMode  If set, construct the base name from this mode name instead
   *               of the currently active one.
   */
  baseMode: function(aMode) {
    if (!aMode)
      aMode = this.mode;

    return aMode.replace(/_compact$/, "");
  },

  /**
   * Name of the mode including the _compact suffix if appropriate.
   *
   * @param aMode  If set, construct the base name from this mode name instead
   *               of the currently active one.
   * @param aCOmpact  Bool value whether to force adding the suffix or not.
   */
  fullMode: function(aMode, aCompact) {
    if (!aMode)
      aMode = this.mode;
    if (aCompact == undefined)
      aCompact = aMode.endsWith("_compact");

    return this.baseMode(aMode) + (aCompact ? "_compact" : "");
  },

  _initFolderModeSelector: function() {
    // Populate the mode selector menulist on the toolbar.
    let fullModes = [];
    let compactModes = [];
    for (let mode of this._modeNames) {
      let array = mode.endsWith("_compact") ? compactModes : fullModes;
      array.push(mode);
    }

    let modeSelector = document.getElementById("folderpane-mode-selector").firstChild;
    // Can't use modeSelector.removeAllItems() here as it would remove the menupopup too, with its attributes.
    while (modeSelector.menupopup.hasChildNodes())
      modeSelector.menupopup.lastChild.remove();

    let currentMode = this.mode;
    let parent = this;

    function appendMode(aMode) {
      let name;
      if (aMode in parent._modeDisplayNames) {
        name = parent._modeDisplayNames[aMode];
      } else {
        let key = "folderPaneModeHeader_" + aMode;
        name = parent.messengerBundle.getString(key);
      }
      let item = modeSelector.appendItem(name, aMode);
      item.setAttribute("type", "radio");
      if (aMode == currentMode)
        item.setAttribute("checked", "true");
      else
        item.setAttribute("checked", "false");
    }

    for (let mode of fullModes)
      appendMode(mode);

    if ((fullModes.length > 0) && (compactModes.length > 0))
      modeSelector.menupopup.appendChild(document.createElement("menuseparator"));

    for (let mode of compactModes)
      appendMode(mode);
  },

  _selectModeInSelector: function(aMode) {
    // Show the mode in the mode selector, if it is on a toolbar.
    let modeSelector = document.getElementById("folderpane-mode-selector");
    if (modeSelector) {
      if (!modeSelector.querySelector('[value="' + aMode + '"]'))
        this._initFolderModeSelector();
      modeSelector.firstChild.value = aMode;
    }
  },

  /**
   * Selects a given nsIMsgFolder in the tree.  This function will also ensure
   * that the folder is actually being displayed (that is, that none of its
   * ancestors are collapsed.
   *
   * @param aFolder  the nsIMsgFolder to select
   * @param [aForceSelect] Whether we should switch to the default mode to
   *      select the folder in case we didn't find the folder in the current
   *      view. Defaults to false.
   * @returns true if the folder selection was successful, false if it failed
   *     (probably because the folder isn't in the view at all)
   */
  selectFolder: function ftv_selectFolder(aFolder, aForceSelect = false) {
    // "this" inside the nested function refers to the function...
    // Also note that openIfNot is recursive.
    let tree = this;
    let folderTreeMode = this._modes[this._mode];
    function openIfNot(aFolderToOpen) {
      let index = tree.getIndexOfFolder(aFolderToOpen);
      if (index != null) {
        if (!tree._rowMap[index].open)
          tree._toggleRow(index, false);
        return true;
      }

      // not found, so open the parent
      let parent = folderTreeMode.getParentOfFolder(aFolderToOpen);
      if (parent && openIfNot(parent)) {
        // now our parent is open, so we can open ourselves
        index = tree.getIndexOfFolder(aFolderToOpen);
        if (index != null) {
          tree._toggleRow(index, false);
          return true;
        }
      }

      // No way we can find the folder now.
      return false;
    }
    let parent = folderTreeMode.getParentOfFolder(aFolder);
    if (parent)
      openIfNot(parent);

    let folderIndex = tree.getIndexOfFolder(aFolder);
    if (folderIndex == null) {
      if (aForceSelect) {
        // Switch to the default mode. The assumption here is that the default
        // mode can display every folder
        this.mode = kDefaultMode;
        // We don't want to get stuck in an infinite recursion, so pass in false
        return this.selectFolder(aFolder, false);
      }

      return false;
    }

    this.selection.select(folderIndex);
    this._treeElement.treeBoxObject.ensureRowIsVisible(folderIndex);
    return true;
  },

  /**
   * Returns the index of a folder in the current display.
   *
   * @param aFolder  the folder whose index should be returned.
   * @returns The index of the folder in the view (a number).
   * @note If the folder is not in the display (perhaps because one of its
   *       anscetors is collapsed), this function returns null.
   */
  getIndexOfFolder: function ftv_getIndexOfFolder(aFolder) {
    for (let [iRow, row] of this._rowMap.entries()) {
      if (row.id == aFolder.URI)
        return iRow;
    }
    return null;
  },

  /**
   * Returns the folder for an index in the current display.
   *
   * @param aIndex the index for which the folder should be returned.
   * @note If the index is out of bounds, this function returns null.
   */
  getFolderForIndex: function ftv_getFolderForIndex(aIndex) {
    if (aIndex < 0 || aIndex >= this._rowMap.length)
      return null;
    return this._rowMap[aIndex]._folder;
  },

  /**
   * Returns the parent of a folder in the current view. This may be, but is not
   * necessarily, the actual parent of the folder (aFolder.parent). In
   * particular, in the smart view, special folders are usually children of the
   * smart folder of that kind.
   *
   * @param aFolder The folder to get the parent of.
   * @returns The parent of the folder, or null if the parent wasn't found.
   * @note This function does not guarantee that either the folder or its parent
   *       is actually in the view.
   */
  getParentOfFolder: function ftv_getParentOfFolder(aFolder) {
    return this._modes[this._mode].getParentOfFolder(aFolder);
  },

  /**
   * Given an nsIMsgDBHdr, returns the folder it is considered to be contained
   * in, in the current mode. This is usually, but not necessarily, the actual
   * folder the message is in (aMsgHdr.folder). For more details, see
   * |IFolderTreeMode.getFolderForMsgHdr|.
   */
  getFolderForMsgHdr: function ftv_getFolderForMsgHdr(aMsgHdr) {
    return this._modes[this._mode].getFolderForMsgHdr(aMsgHdr);
  },

  /**
   * Returns the |ftvItem| for an index in the current display. Intended for use
   * by folder tree mode implementers.
   *
   * @param aIndex The index for which the ftvItem should be returned.
   * @note If the index is out of bounds, this function returns null.
   */
  getFTVItemForIndex: function ftv_getFTVItemForIndex(aIndex) {
    return this._rowMap[aIndex];
  },

  /**
   * Returns an array of nsIMsgFolders corresponding to the current selection
   * in the tree
   */
  getSelectedFolders: function ftv_getSelectedFolders() {
    let selection = this.selection;
    if (!selection)
      return [];

    let folderArray = [];
    let rangeCount = selection.getRangeCount();
    for (let i = 0; i < rangeCount; i++) {
      let startIndex = {};
      let endIndex = {};
      selection.getRangeAt(i, startIndex, endIndex);
      for (let j = startIndex.value; j <= endIndex.value; j++) {
        if (j < this._rowMap.length)
          folderArray.push(this._rowMap[j]._folder);
      }
    }
    return folderArray;
  },

  /**
   * Adds a new child |ftvItem| to the given parent |ftvItem|. Intended for use
   * by folder tree mode implementers.
   *
   * @param aParentItem The parent ftvItem. It is assumed that this is visible
   *     in the view.
   * @param aParentIndex The index of the parent ftvItem in the view.
   * @param aItem The item to add.
   */
  addChildItem: function ftv_addChildItem(aParentItem, aParentIndex, aItem) {
    this._addChildToView(aParentItem, aParentIndex, aItem);
  },

  // ****************** Start of nsITreeView implementation **************** //

  get rowCount() {
    return this._rowMap.length;
  },

  /**
   * drag drop interfaces
   */
  canDrop: function ftv_canDrop(aRow, aOrientation) {
    const Cc = Components.classes;
    const Ci = Components.interfaces;
    let targetFolder = gFolderTreeView._rowMap[aRow]._folder;
    if (!targetFolder)
      return false;
    let dt = this._currentTransfer;
    let types = dt.mozTypesAt(0);
    if (Array.indexOf(types, "text/x-moz-message") != -1) {
      if (aOrientation != Ci.nsITreeView.DROP_ON)
        return false;
      // Don't allow drop onto server itself.
      if (targetFolder.isServer)
        return false;
      // Don't allow drop into a folder that cannot take messages.
      if (!targetFolder.canFileMessages)
        return false;
      let messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
      for (let i = 0; i < dt.mozItemCount; i++) {
        let msgHdr = messenger.msgHdrFromURI(dt.mozGetDataAt("text/x-moz-message", i));
        // Don't allow drop onto original folder.
        if (msgHdr.folder == targetFolder)
          return false;
      }
      return true;
    }
    else if (Array.indexOf(types, "text/x-moz-folder") != -1) {
      if (aOrientation != Ci.nsITreeView.DROP_ON)
        return false;
      // If cannot create subfolders then don't allow drop here.
      if (!targetFolder.canCreateSubfolders)
        return false;
      for (let i = 0; i < dt.mozItemCount; i++) {
        let folder = dt.mozGetDataAt("text/x-moz-folder", i)
                       .QueryInterface(Ci.nsIMsgFolder);
        // Don't allow to drop on itself.
        if (targetFolder == folder)
          return false;
        // Don't copy within same server.
        if ((folder.server == targetFolder.server) &&
             (dt.dropEffect == 'copy'))
          return false;
        // Don't allow immediate child to be dropped onto its parent.
        if (targetFolder == folder.parent)
          return false;
        // Don't allow dragging of virtual folders across accounts.
        if ((folder.flags & nsMsgFolderFlags.Virtual) &&
            folder.server != targetFolder.server)
          return false;
        // Don't allow parent to be dropped on its ancestors.
        if (folder.isAncestorOf(targetFolder))
          return false;
        // If there is a folder that can't be renamed, don't allow it to be
        // dropped if it is not to "Local Folders" or is to the same account.
        if (!folder.canRename && (targetFolder.server.type != "none" ||
                                  folder.server == targetFolder.server))
          return false;
      }
      return true;
    }
    else if (Array.indexOf(types, "text/x-moz-newsfolder") != -1) {
      // Don't allow dragging onto element.
      if (aOrientation == Ci.nsITreeView.DROP_ON)
        return false;
      // Don't allow drop onto server itself.
      if (targetFolder.isServer)
        return false;
      for (let i = 0; i < dt.mozItemCount; i++) {
        let folder = dt.mozGetDataAt("text/x-moz-newsfolder", i)
                       .QueryInterface(Ci.nsIMsgFolder);
        // Don't allow dragging newsgroup to other account.
        if (targetFolder.rootFolder != folder.rootFolder)
          return false;
        // Don't allow dragging newsgroup to before/after itself.
        if (targetFolder == folder)
          return false;
        // Don't allow dragging newsgroup to before item after or
        // after item before.
        let row = aRow + aOrientation;
        if (row in gFolderTreeView._rowMap &&
            (gFolderTreeView._rowMap[row]._folder == folder))
          return false;
      }
      return true;
    }
    // Allow subscribing to feeds by dragging an url to a feed account.
    else if (targetFolder.server.type == "rss" && dt.mozItemCount == 1)
      return FeedUtils.getFeedUriFromDataTransfer(dt) ? true : false;
    else if (Array.indexOf(types, "application/x-moz-file") != -1) {
      if (aOrientation != Ci.nsITreeView.DROP_ON)
        return false;
      // Don't allow drop onto server itself.
      if (targetFolder.isServer)
        return false;
      // Don't allow drop into a folder that cannot take messages.
      if (!targetFolder.canFileMessages)
        return false;
      for (let i = 0; i < dt.mozItemCount; i++) {
        let extFile = dt.mozGetDataAt("application/x-moz-file", i)
                        .QueryInterface(Ci.nsILocalFile);
        return extFile.isFile();
      }
    }
    return false;
  },
  drop: function ftv_drop(aRow, aOrientation) {
    const Cc = Components.classes;
    const Ci = Components.interfaces;
    let targetFolder = gFolderTreeView._rowMap[aRow]._folder;

    let dt = this._currentTransfer;
    let count = dt.mozItemCount;
    let cs = MailServices.copy;

    // This is a potential rss feed.  A link image as well as link text url
    // should be handled; try to extract a url from non moz apps as well.
    let feedUri = targetFolder.server.type == "rss" && count == 1 ?
                    FeedUtils.getFeedUriFromDataTransfer(dt) : null;

    // we only support drag of a single flavor at a time.
    let types = dt.mozTypesAt(0);
    if (Array.indexOf(types, "text/x-moz-folder") != -1) {
      for (let i = 0; i < count; i++) {
        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;
      }
      let newsFolder = targetFolder.rootFolder
                                   .QueryInterface(Ci.nsIMsgNewsFolder);
      // When moving down, want to insert first one last.
      // When moving up, want to insert first one first.
      let i = (aOrientation == 1) ? folders.length - 1 : 0;
      while (i >= 0 && i < folders.length) {
        let folder = folders[i];
        if (folder) {
          newsFolder.moveFolder(folder, targetFolder, aOrientation);
          this.selection.toggleSelect(this.getIndexOfFolder(folder));
        }
        i -= aOrientation;
      }
    }
    else if (Array.indexOf(types, "text/x-moz-message") != -1) {
      let array = Cc["@mozilla.org/array;1"]
                    .createInstance(Ci.nsIMutableArray);
      let sourceFolder;
      let messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
      for (let i = 0; i < count; i++) {
        let msgHdr = messenger.msgHdrFromURI(dt.mozGetDataAt("text/x-moz-message", i));
        if (!i)
          sourceFolder = msgHdr.folder;
        array.appendElement(msgHdr, false);
      }
      let prefBranch = Services.prefs.getBranch("mail.");
      let isMove = Cc["@mozilla.org/widget/dragservice;1"]
                      .getService(Ci.nsIDragService).getCurrentSession()
                      .dragAction == Ci.nsIDragService.DRAGDROP_ACTION_MOVE;
      if (!sourceFolder.canDeleteMessages)
        isMove = false;

      prefBranch.setCharPref("last_msg_movecopy_target_uri", targetFolder.URI);
      prefBranch.setBoolPref("last_msg_movecopy_was_move", isMove);
      // ### ugh, so this won't work with cross-folder views. We would
      // really need to partition the messages by folder.
      cs.CopyMessages(sourceFolder, array, targetFolder, isMove, null,
                        msgWindow, true);
    }
    else if (feedUri) {
      Cc["@mozilla.org/newsblog-feed-downloader;1"]
         .getService(Ci.nsINewsBlogFeedDownloader)
         .subscribeToFeed(feedUri.spec, targetFolder, msgWindow);
    }
    else if (Array.indexOf(types, "application/x-moz-file") != -1) {
      for (let i = 0; i < count; i++) {
        let extFile = dt.mozGetDataAt("application/x-moz-file", i)
                        .QueryInterface(Ci.nsILocalFile);
        if (extFile.isFile()) {
          let len = extFile.leafName.length;
          if (len > 4 && extFile.leafName.toLowerCase().endsWith(".eml"))
            cs.CopyFileMessage(extFile, targetFolder, null, false, 1, "", null, msgWindow);
        }
      }
    }
  },

  _onDragStart: function ftv_dragStart(aEvent) {
    // Ugh, this is ugly but necessary
    let view = gFolderTreeView;

    if (aEvent.originalTarget.localName != "treechildren")
      return;

    let folders = view.getSelectedFolders();
    folders = folders.filter(function(f) { return !f.isServer; });
    for (let i in folders) {
      let flavor = folders[i].server.type == "nntp" ? "text/x-moz-newsfolder" :
                                                      "text/x-moz-folder";
      aEvent.dataTransfer.mozSetDataAt(flavor, folders[i], i);
    }
    aEvent.dataTransfer.effectAllowed = "copyMove";
    aEvent.dataTransfer.addElement(aEvent.originalTarget);
    return;
  },

  _onDragOver: function ftv_onDragOver(aEvent) {
    this._currentTransfer = aEvent.dataTransfer;
  },

  /**
   * CSS files will cue off of these.  Note that we reach into the rowMap's
   * items so that custom data-displays can define their own properties
   */
  getCellProperties: function ftv_getCellProperties(aRow, aCol) {
    return this._rowMap[aRow].getProperties(aCol);
  },

  /**
   * The actual text to display in the tree
   */
  getCellText: function ftv_getCellText(aRow, aCol) {
    if ((aCol.id == "folderNameCol") ||
        (aCol.id == "folderUnreadCol") ||
        (aCol.id == "folderTotalCol") ||
        (aCol.id == "folderSizeCol"))
      return this._rowMap[aRow].getText(aCol.id);
    return "";
  },

  /**
   * For feed folders get, cache, and return a favicon. Otherwise return "" to
   * let css set the image per nsITreeView requirements.
   */
  getImageSrc: function(aRow, aCol) {
    if (aCol.id != "folderNameCol")
      return "";

    let rowItem = gFolderTreeView._rowMap[aRow];
    let folder = rowItem._folder;
    if (folder.server.type != "rss" || folder.isServer)
      return "";

    let favicon = this.getFolderCacheProperty(folder, "favicon");
    if (favicon != null)
      return favicon;

    let callback = (iconUrl => {
      this.setFolderCacheProperty(folder, "favicon", iconUrl);
      this._tree.invalidateRow(aRow);
    });

    // Cache empty string initiallly to return default while getting favicon,
    // so as to never return here. Alternatively, a blank image could be cached.
    this.setFolderCacheProperty(folder, "favicon", "");

    // On startup, allow the ui to paint first before spawning potentially
    // many requests for favicons, even though they are async.
    setTimeout(() => {
      FeedUtils.getFavicon(folder, null, favicon, window, callback);
    }, 0);
  },

  /**
   * The ftvItems take care of assigning this when created.
   */
  getLevel: function ftv_getLevel(aIndex) {
    return this._rowMap[aIndex].level;
  },

  /**
   * The ftvItems take care of assigning this when building children lists
   */
  getServerNameAdded: function ftv_getServerNameAdded(aIndex) {
    return this._rowMap[aIndex].addServerName;
  },

  /**
   * This is easy since the ftv items assigned the _parent property when making
   * the child lists
   */
  getParentIndex: function ftv_getParentIndex(aIndex) {
    return this._rowMap.indexOf(this._rowMap[aIndex]._parent);
  },

  /**
   * This is duplicative for our normal ftv views, but custom data-displays may
   * want to do something special here
   */
  getRowProperties: function ftv_getRowProperties(aRow) {
    return this._rowMap[aRow].getProperties();
  },

  /**
   * Check whether there are any more rows with our level before the next row
   * at our parent's level
   */
  hasNextSibling: function ftv_hasNextSibling(aIndex, aNextIndex) {
    var currentLevel = this._rowMap[aIndex].level;
    for (var i = aNextIndex + 1; i < this._rowMap.length; i++) {
      if (this._rowMap[i].level == currentLevel)
        return true;
      if (this._rowMap[i].level < currentLevel)
        return false;
    }
    return false;
  },

  /**
   * All folders are containers, so we can drag drop messages to them.
   */
  isContainer: function ftv_isContainer(aIndex) {
    return true;
  },

  isContainerEmpty: function ftv_isContainerEmpty(aIndex) {
    // If the folder has no children, the container is empty.
    return !this._rowMap[aIndex].children.length;
  },

  /**
   * Just look at the ftvItem here
   */
  isContainerOpen: function ftv_isContainerOpen(aIndex) {
    return this._rowMap[aIndex].open;
  },
  getSummarizedCounts: function(aIndex, aColName) {
    return this._rowMap[aIndex]._summarizedCounts.get(aColName);
  },
  isEditable: function ftv_isEditable(aRow, aCol) {
    // We don't support editing rows in the tree yet.  We may want to later as
    // an easier way to rename folders.
    return false;
  },
  isSeparator: function ftv_isSeparator(aIndex) {
    // There are no separators in our trees
    return false;
  },
  isSorted: function ftv_isSorted() {
    // We do our own customized sorting
    return false;
  },
  setTree: function ftv_setTree(aTree) {
    this._tree = aTree;
  },

  /**
   * Opens or closes a folder with children.  The logic here is a bit hairy, so
   * be very careful about changing anything.
   */
  toggleOpenState: function ftv_toggleOpenState(aIndex) {
    this._toggleRow(aIndex, true);
  },

  recursivelyAddToMap: function ftv_recursivelyAddToMap(aChild, aNewIndex) {
    // When we add sub-children, we're going to need to increase our index
    // for the next add item at our own level.
    let count = 0;
    if (aChild.children.length && aChild.open) {
      for (let [i, child] in Iterator(this._rowMap[aNewIndex].children)) {
        count++;
        let index = Number(aNewIndex) + Number(i) + 1;
        this._rowMap.splice(index, 0, child);

        let kidsAdded = this.recursivelyAddToMap(child, index);
        count += kidsAdded;
        // Somehow the aNewIndex turns into a string without this.
        aNewIndex = Number(aNewIndex) + kidsAdded;
      }
    }
    return count;
  },

  _toggleRow: function toggleRow(aIndex, aExpandServer)
  {
    // Ok, this is a bit tricky.
    this._rowMap[aIndex].open = !this._rowMap[aIndex].open;
    if (!this._rowMap[aIndex].open) {
      // We're closing the current container.  Remove the children

      // Note that we can't simply splice out children.length, because some of
      // them might have children too.  Find out how many items we're actually
      // going to splice
      let count = 0;
      let i = aIndex + 1;
      let row = this._rowMap[i];
      while (row && row.level > this._rowMap[aIndex].level) {
        count++;
        row = this._rowMap[++i];
      }
      this._rowMap.splice(aIndex + 1, count);

      // Remove us from the persist map
      this._persistItemClosed(this._rowMap[aIndex].id);

      // Notify the tree of changes
      if (this._tree) {
        this._tree.rowCountChanged(aIndex + 1, (-1) * count);
        this._tree.invalidateRow(aIndex);
      }
    } else {
      // We're opening the container.  Add the children to our map

      // Note that these children may have been open when we were last closed,
      // and if they are, we also have to add those grandchildren to the map
      let oldCount = this._rowMap.length;
      this.recursivelyAddToMap(this._rowMap[aIndex], aIndex);

      // Add this folder to the persist map
      this._persistItemOpen(this._rowMap[aIndex].id);

      // Notify the tree of changes
      if (this._tree) {
        this._tree.rowCountChanged(aIndex + 1, this._rowMap.length - oldCount);
        this._tree.invalidateRow(aIndex);
      }
      // if this was a server that was expanded, let it update its counts
      let folder = this._rowMap[aIndex]._folder;
      if (aExpandServer) {
        if (folder.isServer)
          folder.server.performExpand(msgWindow);
        else if (folder instanceof Components.interfaces.nsIMsgImapMailFolder)
          folder.performExpand(msgWindow);
      }
    }
  },

  _subFoldersWithStringProperty: function ftv_subFoldersWithStringProperty(folder, folders, aFolderName, deep)
  {
    for (let child in fixIterator(folder.subFolders, Components.interfaces.nsIMsgFolder)) {
      // if the folder selection is based on a string propery, use that
      if (aFolderName == getSmartFolderName(child)) {
        folders.push(child);
        // Add sub-folders if requested.
        if (deep)
          this.addSubFolders(child, folders);
      }
      else
        // if this folder doesn't have a property set, check Its children
        this._subFoldersWithStringProperty(child, folders, aFolderName, deep);
    }
  },

  _allFoldersWithStringProperty: function ftv_getAllFoldersWithProperty(accounts, aFolderName, deep)
  {
    let folders = [];
    for (let acct of accounts) {
      let folder = acct.incomingServer.rootFolder;
      this._subFoldersWithStringProperty(folder, folders, aFolderName, deep);
    }
    return folders;
  },

  _allFoldersWithFlag: function ftv_getAllFolders(accounts, aFolderFlag, deep)
  {
    let folders = [];
    for (let acct of accounts) {
      let foldersWithFlag = acct.incomingServer.rootFolder.getFoldersWithFlags(aFolderFlag);
      if (foldersWithFlag.length > 0) {
        for (let folderWithFlag in fixIterator(foldersWithFlag,
                                               Components.interfaces.nsIMsgFolder)) {
          folders.push(folderWithFlag);
          // Add sub-folders of Sent and Archive to the result.
          if (deep && (aFolderFlag & (nsMsgFolderFlags.SentMail | nsMsgFolderFlags.Archive)))
            this.addSubFolders(folderWithFlag, folders);
        }
      }
    }
    return folders;
  },

  /**
   * get folders by flag or property based on the value of flag
   */
  _allSmartFolders: function ftv_allSmartFolders(accounts, flag, folderName, deep) {
    return flag ?
      gFolderTreeView._allFoldersWithFlag(accounts, flag, deep) :
      gFolderTreeView._allFoldersWithStringProperty(accounts, folderName, deep);
  },

  /**
   * Add a smart folder for folders with the passed flag set. But if there's
   * only one folder with the flag set, just put it at the top level.
   *
   * @param map array to add folder item to.
   * @param accounts array of accounts.
   * @param smartRootFolder root folder of the smart folders server
   * @param flag folder flag to create smart folders for
   * @param folderName name to give smart folder
   * @param position optional place to put folder item in map. If not specified,
   *                 folder item will be appended at the end of map.
   * @returns The smart folder's ftvItem if one was added, null otherwise.
   */
  _addSmartFoldersForFlag: function ftv_addSFForFlag(map, accounts, smartRootFolder,
                                                     flag, folderName, position)
  {
    // If there's only one subFolder, just put it at the root.
    let subFolders = gFolderTreeView._allSmartFolders(accounts, flag, folderName, false);
    if (flag && subFolders.length == 1) {
      let folderItem = new ftvItem(subFolders[0]);
      folderItem._level = 0;
      if (flag & nsMsgFolderFlags.Inbox)
        folderItem.__defineGetter__("children", () => []);
      if (position == undefined)
        map.push(folderItem);
      else
        map[position] = folderItem;
      // No smart folder was added
      return null;
    }

    let smartFolder;
    try {
      let folderUri = smartRootFolder.URI + "/" + encodeURI(folderName);
      smartFolder = smartRootFolder.getChildWithURI(folderUri, false, true);
    } catch (ex) {
        smartFolder = null;
    };
    if (!smartFolder) {
      let searchFolders = gFolderTreeView._allSmartFolders(accounts, flag, folderName, true);
      let searchFolderURIs = "";
      for (let searchFolder of searchFolders) {
        if (searchFolderURIs.length)
          searchFolderURIs += '|';
        searchFolderURIs +=  searchFolder.URI;
      }
      if (!searchFolderURIs.length)
        return null;
      smartFolder = gFolderTreeView._createVFFolder(folderName, smartRootFolder,
                                                    searchFolderURIs, flag);
    }

    let smartFolderItem = new ftvItem(smartFolder);
    smartFolderItem._level = 0;
    if (position == undefined)
      map.push(smartFolderItem);
    else
      map[position] = smartFolderItem;
    // Add the actual special folders as sub-folders of the saved search.
    // By setting _children directly, we bypass the normal calculation
    // of subfolders.
    smartFolderItem._children = subFolders.map(f => new ftvItem(f));

    let prevChild = null;
    // Each child is a level one below the smartFolder
    for (let child of smartFolderItem._children) {
      child._level = smartFolderItem._level + 1;
      child._parent = smartFolderItem;
      // don't show sub-folders of the inbox, but I think Archives/Sent, etc
      // should have the sub-folders.
      if (flag & nsMsgFolderFlags.Inbox)
        child.__defineGetter__("children", () => []);
      // If we have consecutive children with the same server, then both
      // should display as folder - server.
      if (prevChild && (child._folder.server == prevChild._folder.server)) {
        child.addServerName = true;
        prevChild.addServerName = true;
        prevChild.useServerNameOnly = false;
      }
      else if (flag)
        child.useServerNameOnly = true;
      else
        child.addServerName = true;
      prevChild = child;
    }
    // new custom folders from addons may contain lots of children, sort them
    if (flag == 0)
      sortFolderItems(smartFolderItem._children);
    return smartFolderItem;
  },
  _createVFFolder: function ftv_createVFFolder(newName, parentFolder,
                                               searchFolderURIs, folderFlag)
  {
    let newFolder;
    try {
      if (parentFolder instanceof(Components.interfaces.nsIMsgLocalMailFolder))
        newFolder = parentFolder.createLocalSubfolder(newName);
      else
        newFolder = parentFolder.addSubfolder(newName);
      newFolder.setFlag(nsMsgFolderFlags.Virtual);
      // provide a way to make the top level folder just a container, not
      // a search folder
      let type = this._modes["smart"].getSmartFolderTypeByName(newName);
      if (type[3]) { // isSearch
        let vfdb = newFolder.msgDatabase;
        let 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", "ALL");
        dbFolderInfo.setCharProperty("searchFolderUri", searchFolderURIs);
        dbFolderInfo.setUint32Property("searchFolderFlag", folderFlag);
        dbFolderInfo.setBooleanProperty("searchOnline", true);
        vfdb.summaryValid = true;
        vfdb.Close(true);
      }
      parentFolder.NotifyItemAdded(newFolder);
      MailServices.accounts.saveVirtualFolders();
    }
    catch(e) {
       throw(e);
       dump ("Exception : creating virtual folder \n");
    }
    return newFolder;
  },

  // We don't implement any of these at the moment
  performAction: function ftv_performAction(aAction) {},
  performActionOnCell: function ftv_performActionOnCell(aAction, aRow, aCol) {},
  performActionOnRow: function ftv_performActionOnRow(aAction, aRow) {},
  selectionChanged: function ftv_selectionChanged() {},
  setCellText: function ftv_setCellText(aRow, aCol, aValue) {},
  setCellValue: function ftv_setCellValue(aRow, aCol, aValue) {},
  getCellValue: function ftv_getCellValue(aRow, aCol) {},
  getColumnProperties: function ftv_getColumnProperties(aCol) { return ""; },
  getProgressMode: function ftv_getProgressMode(aRow, aCol) {},
  cycleCell: function ftv_cycleCell(aRow, aCol) {},
  cycleHeader: function ftv_cycleHeader(aCol) {},

  // ****************** End of nsITreeView implementation **************** //

  //
  // WARNING: Everything below this point is considered private.  Touch at your
  //          own risk.

  /**
   * This is an array of all possible modes for the folder tree. You should not
   * modify this directly, but rather use registerFolderTreeMode.
   *
   * Internally each mode is defined separatelly. But in the UI we currently expose
   * only the "base" name (see baseMode()) of the mode plus a "Compact view" option
   * The internal name of the mode to use is then constructed from the base name
   * and "_compact" suffix if compact view is selected. See bug 978592.
   *
   */
  _modeNames: ["all", "unread", "unread_compact", "favorite", "favorite_compact", "recent_compact", "smart"],
  _modeDisplayNames: {},

  /**
   * This is a javascript map of which folders we had open, so that we can
   * persist their state over-time.  It is designed to be used as a JSON object.
   */
  _persistOpenMap: {},
  _notPersistedModes: ["unread", "unread_compact", "favorite", "favorite_compact", "recent_compact"],

  /**
   * Iterate over the persistent list and open the items (folders) stored in it.
   */
  _restoreOpenStates: function ftv__persistOpenStates() {
    let mode = this.mode;
    // Remove any saved state of modes where open state should not be persisted.
    // This is mostly for migration from older profiles that may have the info stored.
    if (this._notPersistedModes.includes(mode)) {
      delete this._persistOpenMap[mode];
    }

    let curLevel = 0;
    let tree = this;
    let map = tree._persistOpenMap[mode]; // may be undefined
    function openLevel() {
      let goOn = false;
      // We can't use a js iterator because we're changing the array as we go.
      // So fallback on old trick of going backwards from the end, which
      // doesn't care when you add things at the end.
      for (let i = tree._rowMap.length - 1; i >= 0; i--) {
        let row = tree._rowMap[i];
        if (row.level != curLevel)
          continue;

        // The initial state of all rows is closed, so toggle those we want open.
        if (!map || map.includes(row.id)) {
          tree._toggleRow(i, false);
          goOn = true;
        }
      }

      // If we opened up any new kids, we need to check their level as well.
      curLevel++;
      if (goOn)
        openLevel();
    }
    openLevel();
  },

  /**
   * Remove the item from the persistent list, meaning the item should
   * be persisted as closed in the tree.
   *
   * @param aItemId  The URI of the folder item.
   */
  _persistItemClosed: function ftv_unpersistItem(aItemId) {
    let mode = this.mode;
    if (this._notPersistedModes.includes(mode))
      return;

    // If the whole mode is not in the map yet,
    // we can silently ignore the folder removal.
    if (!this._persistOpenMap[mode])
      return;

    let persistMapIndex = this._persistOpenMap[mode].indexOf(aItemId);
    if (persistMapIndex != -1)
      this._persistOpenMap[mode].splice(persistMapIndex, 1);
  },

  /**
   * Add the item from the persistent list, meaning the item should
   * be persisted as open (expanded) in the tree.
   *
   * @param aItemId  The URI of the folder item.
   */
  _persistItemOpen: function ftv_persistItem(aItemId) {
    let mode = this.mode;
    if (this._notPersistedModes.includes(mode))
      return;

    if (!this._persistOpenMap[mode])
      this._persistOpenMap[mode] = [];

    if (!this._persistOpenMap[mode].includes(aItemId))
      this._persistOpenMap[mode].push(aItemId);
  },

  _tree: null,
  selection: null,
  /**
   * An array of ftvItems, where each item corresponds to a row in the tree
   */
  _rowMap: null,

  /**
   * Completely discards the current tree and rebuilds it based on current
   * settings
   */
  _rebuild: function ftv__rebuild() {
    let newRowMap;
    try {
      newRowMap = this._modes[this.mode].generateMap(this);
    } catch(ex) {
      Services.console.logStringMessage("generator " + this.mode + " failed with exception: " + ex);
      this.mode = kDefaultMode;
      newRowMap = this._modes[this.mode].generateMap(this);
    }
    let selectedFolders = this.getSelectedFolders();
    if (this.selection)
      this.selection.clearSelection();
    // There's a chance the call to the map generator altered this._rowMap, so
    // evaluate oldCount after calling it rather than before
    let oldCount = this._rowMap ? this._rowMap.length : null;
    this._rowMap = newRowMap;

    this._treeElement.dispatchEvent(new Event("mapRebuild",
      { bubbles: true, cancelable: false }));

    if (this._tree)
    {
      if (oldCount !== null)
          this._tree.rowCountChanged(0, this._rowMap.length - oldCount);
      this._tree.invalidate();
    }
    this._restoreOpenStates();
    // restore selection.
    for (let folder of selectedFolders) {
      if (folder) {
        let index = this.getIndexOfFolder(folder);
        if (index != null)
          this.selection.toggleSelect(index);
      }
    }
  },

  _sortedAccounts: function ftv_getSortedAccounts() {
    let accounts = allAccountsSorted(true);

    // Don't show deferred pop accounts.
    accounts = accounts.filter(function isNotDeferred(a) {
      let server = a.incomingServer;
      return !(server instanceof Components.interfaces.nsIPop3IncomingServer &&
               server.deferredToAccount);
    });

    return accounts;
  },

  /*
   * Session cache keyed by folder url, for properties intended to survive
   * a _rowMap rebuild and avoid expensive requeries. Not for persistence
   * across restarts; _persistOpenMap could be used for that.
   */
  _cache: {},

  /**
   * Update a folder property in the session cache.
   *
   * @param  nsIMsgFolder aFolder   - folder.
   * @param  string aProperty       - property, currently in "favicon".
   * @param  aValue                 - string or object value.
   */
  setFolderCacheProperty: function(aFolder, aProperty, aValue) {
    if (!aFolder || !aProperty)
      return;

    if (!this._cache[aFolder.URI])
      this._cache[aFolder.URI] = {};

    this._cache[aFolder.URI][aProperty] = aValue;
  },

  /**
   * Get a folder property from the session cache.
   *
   * @param  nsIMsgFolder aFolder   - folder.
   * @param  string aProperty       - property key.
   * @return value or null          - null indicates uninitialized.
   */
  getFolderCacheProperty: function(aFolder, aProperty) {
    if (!aFolder || !aProperty)
      return null;

    if (!(aFolder.URI in this._cache))
      return null;

    return this._cache[aFolder.URI][aProperty];
  },

  /**
   * Contains the set of modes registered with the folder tree, initially those
   * included by default. This is a map from names of modes to their
   * implementations of |IFolderTreeMode|.
   */
  _modes: {
    /**
     * The all mode returns all folders, arranged in a hierarchy
     */
    all: {
      __proto__: IFolderTreeMode,

      generateMap: function ftv_all_generateMap(ftv) {
        let accounts = gFolderTreeView._sortedAccounts();
        // force each root folder to do its local subfolder discovery.
        MailUtils.discoverFolders();

        return accounts.map(acct => new ftvItem(acct.incomingServer.rootFolder));
      }
    },

    /**
     * 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.
     * It also includes parent folders of the Unread folders so the hierarchy
     * shown.
     */
    unread: {
      __proto__: IFolderTreeMode,

      generateMap: function ftv_unread_generateMap(ftv) {
        let filterUnread = function filterUnread(aFolder) {
          let currentFolder = gFolderTreeView.getSelectedFolders()[0];
          return ((aFolder.getNumUnread(true) > 0) ||
                  (aFolder == currentFolder));
        }

        let accounts = gFolderTreeView._sortedAccounts();
        // Force each root folder to do its local subfolder discovery.
        MailUtils.discoverFolders();

        let unreadRootFolders = [];
        for (let acct of accounts) {
          let rootFolder = acct.incomingServer.rootFolder;
          // Add rootFolders of accounts that contain at least one Favorite folder.
          if (rootFolder.getNumUnread(true) > 0)
            unreadRootFolders.push(new ftvItem(rootFolder, filterUnread));
        }

        return unreadRootFolders;
      },

      handleChangedIntProperty: function(aItem, aProperty, aOld, aNew) {
        // We want to rebuild only if we have a newly unread folder
        // and we didn't already have the folder.
        if (aProperty == "TotalUnreadMessages" && aOld == 0 && aNew > 0 &&
            gFolderTreeView.getIndexOfFolder(aItem) == null) {
          gFolderTreeView._rebuild();
          return true;
        }
        return false;
      }
    },

    /**
     * A variant of the 'unread' mode above. This does not include the parent folders
     * and the unread folders are shown in a flat list with no hierarchy.
     */
    unread_compact: {
      __proto__: IFolderTreeMode,

      generateMap: function(ftv) {
        let map = [];
        let currentFolder = gFolderTreeView.getSelectedFolders()[0];
        for (let folder of ftv._enumerateFolders) {
          if ((!folder.isServer && folder.getNumUnread(false) > 0) ||
              (folder == currentFolder))
            map.push(new ftvItem(folder));
        }

        // There are no children in this view!
        for (let folder of map) {
          folder.__defineGetter__("children", () => []);
          folder.addServerName = true;
        }
        sortFolderItems(map);
        return map;
      },

      getParentOfFolder: function(aFolder) {
        // This is a flat view, so no folders have parents.
        return null;
      },

      handleChangedIntProperty: function(aItem, aProperty, aOld, aNew) {
        // We want to rebuild only if we have a newly unread folder
        // and we didn't already have the folder.
        if (aProperty == "TotalUnreadMessages" && aOld == 0 && aNew > 0 &&
            gFolderTreeView.getIndexOfFolder(aItem) == null) {
          gFolderTreeView._rebuild();
          return true;
        }
        return false;
      }
    },

    /**
     * The favorites mode returns all folders whose flags are set to include
     * the favorite flag.
     * It also includes parent folders of the Unread folders so the hierarchy
     * shown.
     */
    favorite: {
      __proto__: IFolderTreeMode,

      generateMap: function ftv_favorite_generateMap(ftv) {
        let accounts = gFolderTreeView._sortedAccounts();
        // Force each root folder to do its local subfolder discovery.
        MailUtils.discoverFolders();

        let favRootFolders = [];
        let filterFavorite = function filterFavorite(aFolder) {
          return aFolder.getFolderWithFlags(nsMsgFolderFlags.Favorite) != null;
        }
        for (let acct of accounts) {
          let rootFolder = acct.incomingServer.rootFolder;
          // Add rootFolders of accounts that contain at least one Favorite folder.
          if (filterFavorite(rootFolder))
            favRootFolders.push(new ftvItem(rootFolder, filterFavorite));
        }

        return favRootFolders;
      },

      handleChangedIntProperty: function(aItem, aProperty, aOld, aNew) {
        // We want to rebuild if the favorite status of a folder changed.
        if (aProperty == "FolderFlag" &&
            ((aOld & Components.interfaces.nsMsgFolderFlags.Favorite) !=
            (aNew & Components.interfaces.nsMsgFolderFlags.Favorite))) {
          gFolderTreeView._rebuild();
          return true;
        }
        return false;
      }
    },

    /**
     * A variant of the 'favorite' mode above. This does not include the parent folders
     * and the unread folders are shown in a compact list with no hierarchy.
     */
    favorite_compact: {
      __proto__: IFolderTreeMode,

      generateMap: function(ftv) {
        let faves = [];
        for (let folder of ftv._enumerateFolders) {
          if (folder.getFlag(nsMsgFolderFlags.Favorite))
            faves.push(new ftvItem(folder));
        }

        // We want to display the account name alongside folders that have
        // duplicated folder names.
        let uniqueNames = new Set(); // set of folder names seen at least once
        let dupeNames = new Set(); // set of folders seen at least twice
        for (let item of faves) {
          let name = item._folder.abbreviatedName.toLocaleLowerCase();
          if (uniqueNames.has(name)) {
            if (!dupeNames.has(name))
              dupeNames.add(name);
          } else {
            uniqueNames.add(name);
          }
        }

        // There are no children in this view!
        for (let item of faves) {
          let name = item._folder.abbreviatedName.toLocaleLowerCase();
          item.__defineGetter__("children", () => []);
          item.addServerName = dupeNames.has(name);
        }
        sortFolderItems(faves);
        return faves;
      },

      getParentOfFolder: function(aFolder) {
        // This is a flat view, so no folders have parents.
        return null;
      },

      handleChangedIntProperty: function(aItem, aProperty, aOld, aNew) {
        // We want to rebuild if the favorite status of a folder changed.
        if (aProperty == "FolderFlag" &&
            ((aOld & Components.interfaces.nsMsgFolderFlags.Favorite) !=
            (aNew & Components.interfaces.nsMsgFolderFlags.Favorite))) {
          gFolderTreeView._rebuild();
          return true;
        }
        return false;
      }
    },

    recent_compact: {
      __proto__: IFolderTreeMode,

      generateMap: function(ftv) {
        const MAXRECENT = 15;

        // Get 15 (MAXRECENT) most recently accessed folders.
        let recentFolders = getMostRecentFolders(ftv._enumerateFolders,
                                                 MAXRECENT,
                                                 "MRUTime",
                                                 null);

        // Sort the folder names alphabetically.
        recentFolders.sort(function rf_sort(a, b){
          let aLabel = a.prettyName;
          let bLabel = b.prettyName;
          if (aLabel == bLabel) {
            aLabel = a.server.prettyName;
            bLabel = b.server.prettyName;
          }
          return folderNameCompare(aLabel, bLabel);
        });

        let items = recentFolders.map(f => new ftvItem(f));

        // There are no children in this view!
        // And we want to display the account name to distinguish folders w/
        // the same name.
        for (let folder of items) {
          folder.__defineGetter__("children", () => []);
          folder.addServerName = true;
        }

        return items;
      },

      getParentOfFolder: function(aFolder) {
        // This is a flat view, so no folders have parents.
        return null;
      }
    },

    /**
     * The smart folder mode combines special folders of a particular type
     * across accounts into a single cross-folder saved search.
     */
    smart: {
      __proto__: IFolderTreeMode,

      /**
       * The smart server. This will create the server if it doesn't exist.
       */
      get _smartServer() {
        let smartServer;
        try {
          smartServer = MailServices.accounts.FindServer("nobody", "smart mailboxes", "none");
        }
        catch (ex) {
          smartServer = MailServices.accounts.createIncomingServer("nobody", "smart mailboxes", "none");
          // We don't want the "smart" server/account leaking out into the ui in
          // other places, so set it as hidden.
          smartServer.hidden = true;
          let account = MailServices.accounts.createAccount();
          account.incomingServer = smartServer;
        }
        delete this._smartServer;
        return this._smartServer = smartServer;
      },

      /**
       * A list of [flag, name, isDeep, isSearch] for smart folders. isDeep ==
       * false means that subfolders are displayed as subfolders of the account,
       * not of the smart folder. This list is expected to be constant through a
       * session.
       */
      _flagNameList: [
        [nsMsgFolderFlags.Inbox, "Inbox", false, true],
        [nsMsgFolderFlags.Drafts, "Drafts", false, true],
        [nsMsgFolderFlags.SentMail, "Sent", true, true],
        [nsMsgFolderFlags.Trash, "Trash", true, true],
        [nsMsgFolderFlags.Templates, "Templates", false, true],
        [nsMsgFolderFlags.Archive, "Archives", true, true],
        [nsMsgFolderFlags.Junk, "Junk", false, true],
        [nsMsgFolderFlags.Queue, "Outbox", true, true]
      ],

      /**
       * support for addons to add special folder types, this must be called
       * prior to onload.
       *
       * @param aFolderName  name of the folder
       * @param isDeep  include subfolders
       * @param folderOptions  object with searchStr and searchOnline options, or null
       */
      addSmartFolderType: function ftv_addSmartFolderType(aFolderName, isDeep, isSearchFolder) {
        this._flagNameList.push([0, aFolderName, isDeep, isSearchFolder]);
      },

      /**
       * Returns an array of 4 elements describing the smart folder
       * if the given folder is a special folder, else returns null.
       */
      getSmartFolderTypeByName: function ftv_smart__getSmartFolderType(aName) {
        for (let type of this._flagNameList) {
          if (type[1] == aName)
            return type;
        }
        return null;
      },
      /**
       * check to see if a folder is a smart folder
       */
      isSmartFolder: function ftv_smart__isSmartFolder(aFolder) {
        if (aFolder.flags & this._allSmartFlags)
            return true;
        // Also check the folder name itself, as containers do not
        // have the smartFolderName property.  We check all folders here, since
        // a "real" folder might be marked as a child of a smart folder.
        let smartFolderName = getSmartFolderName(aFolder);
        return smartFolderName && this.getSmartFolderTypeByName(smartFolderName) ||
            this.getSmartFolderTypeByName(aFolder.name);
      },

      /**
       * All the flags above, bitwise ORed.
       */
      get _allSmartFlags() {
        delete this._allSmartFlags;
        return this._allSmartFlags = this._flagNameList.reduce(
          (res, [flag,, isDeep,]) => res | flag, 0);
      },

      /**
       * All the "shallow" flags above (isDeep set to false), bitwise ORed.
       */
      get _allShallowFlags() {
        delete this._allShallowFlags;
        return this._allShallowFlags = this._flagNameList.reduce(
          (res, [flag,, isDeep,]) => isDeep ? res : (res | flag), 0);
      },

      /**
       * Returns an array of 4 elements describing the smart folder
       * if the given folder is a special folder, else returns null.
       */
      _getSmartFolderType: function ftv_smart__getSmartFolderType(aFolder) {
        let smartFolderName = getSmartFolderName(aFolder);
        for (let type of this._flagNameList) {
          if (smartFolderName) {
            if (type[1] == smartFolderName)
              return type;
            continue;
          }
          if (aFolder.flags & type[0])
            return type;
        }
        return null;
      },

      /**
       * Returns the smart folder with the given name.
       */
      _getSmartFolderNamed: function ftv_smart__getSmartFolderNamed(aName) {
        let smartRoot = this._smartServer.rootFolder;
        return smartRoot.getChildWithURI(smartRoot.URI + "/" + encodeURI(aName), false,
                                         true);
      },

      generateMap: function ftv_smart_generateMap(ftv) {
        let map = [];
        let accounts = gFolderTreeView._sortedAccounts();
        let smartServer = this._smartServer;
        smartServer.prettyName = gFolderTreeView.messengerBundle
                                                .getString("unifiedAccountName");
        smartServer.canHaveFilters = false;

        let smartRoot = smartServer.rootFolder;
        let smartChildren = [];
        for (let [flag, name,,] of this._flagNameList) {
          gFolderTreeView._addSmartFoldersForFlag(smartChildren, accounts,
                                                  smartRoot, flag, name);
        }

        sortFolderItems(smartChildren);
        for (let smartChild of smartChildren)
          map.push(smartChild);

        MailUtils.discoverFolders();

        for (let acct of accounts)
          map.push(new ftv_SmartItem(acct.incomingServer.rootFolder));

        return map;
      },

      /**
       * Returns the parent of a folder in the view.
       *
       * - The smart mailboxes are all top-level, so there's no parent.
       * - For one of the special folders, it is the smart folder of that kind
       *   if we're showing it (this happens when there's more than one folder
       *   of the kind). Otherwise it's a top-level folder, so there isn't a
       *   parent.
       * - For a child of a "shallow" special folder (see |_flagNameList| for
       *   the definition), it is the account.
       * - Otherwise it is simply the folder's actual parent.
       */
      getParentOfFolder: function ftv_smart_getParentOfFolder(aFolder) {
        let smartServer = this._smartServer;
        if (aFolder.server == smartServer)
          // This is a smart mailbox
          return null;

        let smartType = this._getSmartFolderType(aFolder);
        if (smartType) {
          // This is a special folder
          let smartFolder = this._getSmartFolderNamed(smartType[1]);
          if (smartFolder &&
              gFolderTreeView.getIndexOfFolder(smartFolder) != null)
            return smartFolder;

          return null;
        }

        let parent = aFolder.parent;
        if (parent && parent.isSpecialFolder(this._allShallowFlags, false)) {
          // Child of a shallow special folder
          return aFolder.server.rootFolder;
        }

        return parent;
      },

      /**
       * For a folder of a particular type foo, this returns the smart folder of
       * that type (if it's displayed). Otherwise this returns the folder the
       * message is in.
       */
      getFolderForMsgHdr: function ftv_smart_getFolderForMsgHdr(aMsgHdr) {
        let folder = aMsgHdr.folder;

        let smartType = this._getSmartFolderType(folder);
        if (smartType) {
          let smartFolder = this._getSmartFolderNamed(smartType[1]);
          if (smartFolder &&
              gFolderTreeView.getIndexOfFolder(smartFolder) != null)
            return smartFolder;
        }
        return folder;
      },

      /**
       * Handles the case of a new folder being added.
       *
       * - If a new special folder is added, we need to add it as a child of the
       *   corresponding smart folder.
       * - If the parent is a shallow special folder, we need to add it as a
       *   top-level folder in its account.
       * - Otherwise, we need to add it as a child of its parent (as normal).
       */
      onFolderAdded: function ftv_smart_onFolderAdded(aParent, aFolder) {
        if (aFolder.flags & this._allSmartFlags) {
          // add as child of corresponding smart folder
          let smartServer = this._smartServer;
          let smartRoot = smartServer.rootFolder;
          // In theory, a folder can have multiple flags set, so we need to
          // check each flag separately.
          for (let [flag, name,,] of this._flagNameList) {
            if (aFolder.flags & flag)
              gFolderTreeView._addSmartSubFolder(aFolder, smartRoot, name, flag);
          }
        }
        else if (aParent.isSpecialFolder(this._allShallowFlags, false)) {
          // add as a child of the account
          let rootIndex = gFolderTreeView.getIndexOfFolder(
            aFolder.server.rootFolder);
          let root = gFolderTreeView._rowMap[rootIndex];
          if (!root)
            return;

          let newChild = new ftv_SmartItem(aFolder);
          root.children.push(newChild);
          newChild._level = root._level + 1;
          newChild._parent = root;
          sortFolderItems(root._children);

          gFolderTreeView._addChildToView(root, rootIndex, newChild);
        }
        else {
          // add as normal
          gFolderTreeView.addFolder(aParent, aFolder);
        }
      }
    }
  },

  /**
   * This is a helper attribute that simply returns a flat list of all folders
   */
  get _enumerateFolders() {
    const Cc = Components.classes;
    const Ci = Components.interfaces;
    let folders = [];

    for (let server in fixIterator(MailServices.accounts.allServers, Ci.nsIMsgIncomingServer)) {
      // Skip deferred accounts
      if (server instanceof Ci.nsIPop3IncomingServer &&
          server.deferredToAccount)
        continue;

      let rootFolder = server.rootFolder;
      folders.push(rootFolder);
      this.addSubFolders(rootFolder, folders);
    }
    return folders;
  },

  /**
   * This is a recursive function to add all subfolders to the array. It
   * assumes that the passed in folder itself has already been added.
   *
   * @param aFolder  the folder whose subfolders should be added
   * @param folders  the array to add the folders to.
   */
  addSubFolders : function ftv_addSubFolders (folder, folders) {
    for (let f in fixIterator(folder.subFolders, Components.interfaces.nsIMsgFolder)) {
      folders.push(f);
      this.addSubFolders(f, folders);
    }
  },

  /**
   * This updates the rowmap and invalidates the right row(s) in the tree
   */
  _addChildToView: function ftl_addChildToView(aParent, aParentIndex, aNewChild) {
    if (aParent.open) {
      let newChildIndex;
      let newChildNum = aParent._children.indexOf(aNewChild);
      // only child - go right after our parent
      if (newChildNum == 0) {
        newChildIndex = Number(aParentIndex) + 1
      }
      // if we're not the last child, insert ourselves before the next child.
      else if (newChildNum < aParent._children.length - 1) {
        newChildIndex = this.getIndexOfFolder(aParent._children[Number(newChildNum) + 1]._folder);
      }
      // otherwise, go after the last child
      else {
        let lastChild = aParent._children[newChildNum - 1];
        let lastChildIndex = this.getIndexOfFolder(lastChild._folder);
        newChildIndex = Number(lastChildIndex) + 1;
        while (newChildIndex < this.rowCount &&
               this._rowMap[newChildIndex].level > this._rowMap[lastChildIndex].level)
          newChildIndex++;
      }
      this._rowMap.splice(newChildIndex, 0, aNewChild);
      this._tree.rowCountChanged(newChildIndex, 1);
    } else {
      this._tree.invalidateRow(aParentIndex);
    }
  },
  _addSmartSubFolder: function ftl_addSmartSubFolder(aItem, aSmartRoot, aName, aFlag) {
    let smartFolder = aSmartRoot.getChildWithURI(aSmartRoot.URI + "/" + encodeURI(aName),
                                                 false, true);
    let parent = null;
    let parentIndex = -1;
    let newChild;
    let newChildIndex = 0;
    if (!smartFolder || this.getIndexOfFolder(smartFolder) == null) {
      newChild = new ftv_SmartItem(aItem);
      newChild._level = 0;
      while (newChildIndex < this.rowCount) {
        if (this._rowMap[newChildIndex]._folder.flags & aFlag) {
          // This type of folder seems to already exist, so replace the row
          // with a smartFolder.
          this._addSmartFoldersForFlag(this._rowMap, this._sortedAccounts(),
                                       aSmartRoot, aFlag, aName, newChildIndex);
          return;
        }
        if (this._rowMap[newChildIndex]._folder.isServer)
          break;
        newChildIndex++;
      }
    } else {
      parentIndex = this.getIndexOfFolder(smartFolder);
      parent = this._rowMap[parentIndex];
      if (!parent)
         return;

      newChild = new ftv_SmartItem(aItem);
      parent.children.push(newChild);
      newChild._level = parent._level + 1;
      newChild._parent = parent;
      sortFolderItems(parent._children);
      newChild.useServerNameOnly = true;
    }
    if (aItem.flags & nsMsgFolderFlags.Inbox)
      newChild.__defineGetter__("children", () => []);
    if (parent)
      this._addChildToView(parent, parentIndex, newChild);
    else {
      this._rowMap.splice(newChildIndex, 0, newChild);
      this._tree.rowCountChanged(newChildIndex, 1);
    }
  },
  /**
   * This is our implementation of nsIMsgFolderListener to watch for changes
   */
  OnItemAdded: function ftl_add(aParentItem, aItem) {
    // Ignore this item if it's not a folder, or we knew about it.
    if (!(aItem instanceof Components.interfaces.nsIMsgFolder) ||
        this.getIndexOfFolder(aItem) != null)
      return;

    // if no parent, this is an account, so let's rebuild.
    if (!aParentItem) {
      if (!aItem.server.hidden) // ignore hidden server items
        this._rebuild();
      return;
    }
    this._modes[this._mode].onFolderAdded(
      aParentItem.QueryInterface(Components.interfaces.nsIMsgFolder), aItem);
  },
  addFolder: function ftl_add_folder(aParentItem, aItem)
  {
    // This intentionally adds any new folder even if it would not pass the
    // _filterFunction. The idea is that the user can add new folders even
    // in modes like "unread" or "favorite" and could wonder why they
    // are not appearing (forgetting they do not meet the criteria of the view).
    // The folders will be hidden properly next time the view is rebuilt.
    let parentIndex = this.getIndexOfFolder(aParentItem);
    let parent = this._rowMap[parentIndex];
    if (!parent)
       return;

    // Getting these children might have triggered our parent to build its
    // array just now, in which case the added item will already exist
    let children = parent.children;
    var newChild;
    for (let child of children) {
      if (child._folder == aItem) {
        newChild = child;
        break;
      }
    }
    if (!newChild) {
      newChild = new ftvItem(aItem);
      parent.children.push(newChild);
      newChild._level = parent._level + 1;
      newChild._parent = parent;
      sortFolderItems(parent._children);
    }
    // If the parent is open, add the new child into the folder pane.
    // Otherwise, just invalidate the parent row. Note that this code doesn't
    // get called for the smart folder case.
    if (!parent.open) {
      // Special case adding a special folder when the parent is collapsed.
      // Expand the parent so the user can see the special child.
      // Expanding the parent is sufficient to add the folder to the view,
      // because either we knew about it, or we will have added a child item
      // for it above.
      if (newChild._folder.flags & nsMsgFolderFlags.SpecialUse) {
        this._toggleRow(parentIndex, false);
        return;
      }
    }
    this._addChildToView(parent, parentIndex, newChild);
  },

  OnItemRemoved: function ftl_remove(aRDFParentItem, aItem) {
    if (!(aItem instanceof Components.interfaces.nsIMsgFolder))
      return;

    this._persistItemClosed(aItem.URI);

    let index = this.getIndexOfFolder(aItem);
    if (index == null)
      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;
    while (walker < this.rowCount &&
           this._rowMap[walker].level > this._rowMap[index].level) {
      walker++;
      kidCount++;
    }
    this._rowMap.splice(index, kidCount);
    this._tree.rowCountChanged(index, -1 * kidCount);
    this._tree.invalidateRow(index);
  },

  OnItemPropertyChanged: function(aItem, aProperty, aOld, aNew) {},
  OnItemIntPropertyChanged: function(aItem, aProperty, aOld, aNew) {
    // First try mode specific handling of the changed property.
    if (this._modes[this.mode].handleChangedIntProperty(aItem, aProperty, aOld, aNew))
      return;

    if (aItem instanceof Components.interfaces.nsIMsgFolder) {
      let index = this.getIndexOfFolder(aItem);
      let folder = aItem;
      let folderTreeMode = this._modes[this._mode];
      // look for first visible ancestor
      while (index == null) {
        folder = folderTreeMode.getParentOfFolder(folder);
        if (!folder)
          break;
        index = this.getIndexOfFolder(folder);
      }
      if (index != null)
        this._tree.invalidateRow(index);
    }
  },

  OnItemBoolPropertyChanged: function(aItem, aProperty, aOld, aNew) {
    let index = this.getIndexOfFolder(aItem);
    if (index != null)
      this._tree.invalidateRow(index);
  },
  OnItemUnicharPropertyChanged: function(aItem, aProperty, aOld, aNew) {},
  OnItemPropertyFlagChanged: function(aItem, aProperty, aOld, aNew) {},
  OnItemEvent: function(aFolder, aEvent) {
    let index = this.getIndexOfFolder(aFolder);
    if (index != null)
      this._tree.invalidateRow(index);
  }
};

/**
 * The ftvItem object represents a single row in the tree view. Because I'm lazy
 * I'm just going to define the expected interface here.  You are free to return
 * an alternative object in a _mapGenerator, provided that it matches this
 * interface:
 *
 * id (attribute) - a unique string for this object. Must persist over sessions
 * text (attribute) - the text to display in the tree
 * level (attribute) - the level in the tree to display the item at
 * open (rw, attribute) - whether or not this container is open
 * children (attribute) - an array of child items also conforming to this spec
 * getProperties (function) - a call from getRowProperties or getCellProperties
 *                            for this item will be passed into this function
 * command (function) - this function will be called when the item is double-
 *                      clicked
 */

/**
 * The ftvItem constructor takes these arguments:
 *
 * @param aFolder        The folder attached to this row in the tree.
 * @param aFolderFilter  When showing children folders of this one,
 *                       only show those that pass this filter function.
 *                       If unset, show all subfolders.
 */
function ftvItem(aFolder, aFolderFilter) {
  this._folder = aFolder;
  this._level = 0;
  this._parent = null;
  this._folderFilter = aFolderFilter;
  // The map contains message counts for each folder column.
  // Each key is a column name (ID) from the folder tree.
  // Value is an array of the format "[value_for_folder, value_for_all_its_subfolders]".
  this._summarizedCounts = new Map();
}

ftvItem.prototype = {
  open: false,
  addServerName: false,
  useServerNameOnly: false,

  get id() {
    return this._folder.URI;
  },
  get text() {
    return this.getText("folderNameCol");
  },

  getText(aColName) {
    // Only show counts / total size of subtree if the pref is set,
    // we are in "All folders" mode and this folder row is not expanded.
    gFolderStatsHelpers.sumSubfolders = gFolderStatsHelpers.sumSubfoldersPref &&
                          (gFolderTreeView.mode == kDefaultMode) &&
                          this._folder.hasSubFolders && !this.open;

    this._summarizedCounts.delete(aColName);
    switch (aColName) {
      case "folderNameCol":
        let text;
        if (this.useServerNameOnly)
          text = this._folder.server.prettyName;
        else {
          text = this._folder.abbreviatedName;
          if (this.addServerName)
            text = gFolderTreeView.messengerBundle.getFormattedString(
              "folderWithAccount", [text, this._folder.server.prettyName]);
        }

        // If the unread column is shown, we don't need to add the count
        // to the name.
        if (!document.getElementById("folderUnreadCol").hidden)
          return text;

        let unread = this._folder.getNumUnread(false);
        let totalUnread = gFolderStatsHelpers.sumSubfolders ?
                          this._folder.getNumUnread(true) : unread;
        this._summarizedCounts.set(aColName, [unread, totalUnread - unread]);
        if (totalUnread > 0) {
          text = gFolderTreeView.messengerBundle
            .getFormattedString("folderWithUnreadMsgs",
                                [text, gFolderStatsHelpers.addSummarizedPrefix(totalUnread,
                                       unread != totalUnread)]);
        }
        return text;

      case "folderUnreadCol":
        let folderUnread = this._folder.getNumUnread(false);
        let subfoldersUnread = gFolderStatsHelpers.sumSubfolders ?
                               this._folder.getNumUnread(true) : folderUnread;
        this._summarizedCounts.set(aColName, [folderUnread,
                                              subfoldersUnread - folderUnread]);
        return gFolderStatsHelpers
                 .fixNum(subfoldersUnread, folderUnread != subfoldersUnread);

      case "folderTotalCol":
        let folderTotal = this._folder.getTotalMessages(false);
        let subfoldersTotal = gFolderStatsHelpers.sumSubfolders ?
                              this._folder.getTotalMessages(true) : folderTotal;
        this._summarizedCounts.set(aColName, [folderTotal,
                                              subfoldersTotal - folderTotal]);
        return gFolderStatsHelpers
                 .fixNum(subfoldersTotal, folderTotal != subfoldersTotal);

      case "folderSizeCol":
        let thisFolderSize = gFolderStatsHelpers.getFolderSize(this._folder);
        let subfoldersSize = gFolderStatsHelpers.sumSubfolders ?
                             gFolderStatsHelpers.getSubfoldersSize(this._folder) : 0;

        if (subfoldersSize == gFolderStatsHelpers.kUnknownSize ||
            thisFolderSize == gFolderStatsHelpers.kUnknownSize)
          return gFolderStatsHelpers.kUnknownSize;

        let totalSize = thisFolderSize + subfoldersSize;
        if (totalSize == 0)
          return "";

        let [totalText, folderUnit] = gFolderStatsHelpers.formatFolderSize(totalSize);
        let folderText = (subfoldersSize == 0) ? totalText :
                         gFolderStatsHelpers.formatFolderSize(thisFolderSize, folderUnit)[0];
        let subfoldersText = (subfoldersSize == 0) ? "" :
                             gFolderStatsHelpers.formatFolderSize(subfoldersSize, folderUnit)[0];
        this._summarizedCounts.set(aColName, [folderText, subfoldersText]);
        return gFolderStatsHelpers
                 .addSummarizedPrefix(totalText, totalSize != thisFolderSize);

      default:
        return "";
    }
  },

  get level() {
    return this._level;
  },

  getProperties: function (aColumn) {
    if (aColumn && aColumn.id != "folderNameCol")
      return "";

    // From folderUtils.jsm
    let properties = getFolderProperties(this._folder, this.open);
    if (this._folder.getFlag(nsMsgFolderFlags.Virtual)) {
      properties += " specialFolder-Smart";
      // a second possibility for customized smart folders
      properties += " specialFolder-" + this._folder.name.replace(/\s+/g, "");
    }
    // if there is a smartFolder name property, add it
    let smartFolderName = getSmartFolderName(this._folder);
    if (smartFolderName) {
      properties += " specialFolder-" + smartFolderName.replace(/\s+/g, "");
    }

    if (this._folder.server.type == "rss" && !this._folder.isServer &&
        FeedUtils.getFeedUrlsInFolder(this._folder))
      properties += " isFeedFolder-true";

    return properties;
  },

  command: function fti_command() {
    if (!Services.prefs.getBoolPref("mailnews.reuse_thread_window2"))
      MsgOpenNewWindowForFolder(this._folder.URI, -1 /* key */);
  },

  _children: null,
  get children() {
    const Ci = Components.interfaces;
    // We're caching our child list to save perf.
    if (!this._children) {
      let iter;
      try {
        iter = fixIterator(this._folder.subFolders, Ci.nsIMsgFolder);
      } catch (ex) {
        Services.console.logStringMessage("Discovering children for " + this._folder.URI +
                                          " failed with " + "exception: " + ex);
        iter = [];
      }
      this._children = [];
      // Out of all children, only keep those that match the _folderFilter
      // and those that contain such children.
      for (let folder in iter) {
        if (!this._folderFilter || this._folderFilter(folder)) {
          this._children.push(new ftvItem(folder, this._folderFilter));
        }
      }
      sortFolderItems(this._children);
      // Each child is a level one below us
      for (let child of this._children) {
        child._level = this._level + 1;
        child._parent = this;
      }
    }
    return this._children;
  }
};

/**
 * This handles the invocation of most commmands dealing with folders, based off
 * of the current selection, or a passed in folder.
 */
var gFolderTreeController = {
  /**
   * Opens the dialog to create a new sub-folder, and creates it if the user
   * accepts
   *
   * @param aParent (optional)  the parent for the new subfolder
   */
  newFolder: function ftc_newFolder(aParent) {
    let folder = aParent || gFolderTreeView.getSelectedFolders()[0];

    // Make sure we actually can create subfolders
    if (!folder.canCreateSubfolders) {
      // Check if we can create them at the root
      let rootMsgFolder = folder.server.rootMsgFolder;
      if (rootMsgFolder.canCreateSubfolders)
        folder = rootMsgFolder;
      else // just use the default account
        folder = GetDefaultAccountRootFolder();
    }

    if (!folder)
      return;

    let dualUseFolders = true;
    if (folder.server instanceof Components.interfaces.nsIImapIncomingServer)
      dualUseFolders = folder.server.dualUseFolders;

    function newFolderCallback(aName, aFolder) {
      // createSubfolder can throw an exception, causing the newFolder dialog
      // to not close and wait for another input.
      // TODO: Rewrite this logic and also move the opening of alert dialogs from
      // nsMsgLocalMailFolder::CreateSubfolderInternal to here (bug 831190#c16).
      if (aName)
        aFolder.createSubfolder(aName, msgWindow);
    }

    window.openDialog("chrome://messenger/content/newFolderDialog.xul",
                      "",
                      "chrome,modal,resizable=no,centerscreen",
                      {folder: folder, dualUseFolders: dualUseFolders,
                       okCallback: newFolderCallback});
  },

  /**
   * Opens the dialog to edit the properties for a folder
   *
   * @param aTabID  (optional) the tab to show in the dialog
   * @param aFolder (optional) the folder to edit, if not the selected one
   */
  editFolder: function ftc_editFolder(aTabID, aFolder) {
    let folder = aFolder || gFolderTreeView.getSelectedFolders()[0];

    // If this is actually a server, send it off to that controller
    if (folder.isServer) {
      MsgAccountManager(null, folder.server);
      return;
    }

    if (folder.flags & nsMsgFolderFlags.Virtual) {
      this.editVirtualFolder(folder);
      return;
    }

    let title = gFolderTreeView.messengerBundle
                               .getString("folderProperties");

    //xxx useless param
    function editFolderCallback(aNewName, aOldName, aUri) {
      if (aNewName != aOldName)
        folder.rename(aNewName, msgWindow);
    }

    //xxx useless param
    function rebuildSummary(aFolder) {
      // folder is already introduced in our containing function and is
      //  lexically captured and available to us.
      if (folder.locked) {
        folder.throwAlertMsg("operationFailedFolderBusy", msgWindow);
        return;
      }
      if (folder.supportsOffline) {
        // Remove the offline store, if any.
        let offlineStore = folder.filePath;
        // XXX todo: figure out how to delete a maildir directory async. This
        // delete causes main thread lockup for large maildir folders.
        if (offlineStore.exists())
          offlineStore.remove(true);
      }
      gFolderDisplay.view.close();

      // Send a notification that we are triggering a database rebuild.
      MailServices.mfn.notifyItemEvent(folder, "FolderReindexTriggered", null);

      folder.msgDatabase.summaryValid = false;

      var msgDB = folder.msgDatabase;
      msgDB.summaryValid = false;
      try {
        folder.closeAndBackupFolderDB("");
      }
      catch(e) {
        // In a failure, proceed anyway since we're dealing with problems
        folder.ForceDBClosed();
      }
      folder.updateFolder(msgWindow);
      gFolderDisplay.show(folder);
    }

    window.openDialog("chrome://messenger/content/folderProps.xul",
                      "",
                      "chrome,modal,centerscreen",
                      {folder: folder, serverType: folder.server.type,
                       msgWindow: msgWindow, title: title,
                       okCallback: editFolderCallback,
                       tabID: aTabID, name: folder.prettyName,
                       rebuildSummaryCallback: rebuildSummary});
  },

  /**
   * Opens the dialog to rename a particular folder, and does the renaming if
   * the user clicks OK in that dialog
   *
   * @param aFolder (optional)  the folder to rename, if different than the
   *                            currently selected one
   */
  renameFolder: function ftc_rename(aFolder) {
    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._tree.view.selection.clearSelection();

      // Actually do the rename
      folder.rename(aName, msgWindow);
    }
    window.openDialog("chrome://messenger/content/renameFolderDialog.xul",
                      "",
                      "chrome,modal,centerscreen",
                      {preselectedURI: folder.URI,
                       okCallback: renameCallback, name: folder.prettyName});
  },

  /**
   * Deletes a folder from its parent. Also handles unsubscribe from newsgroups
   * if the selected folder/s happen to be nntp.
   *
   * @param aFolder (optional) the folder to delete, if not the selected one
   */
  deleteFolder: function ftc_delete(aFolder) {
    const Ci = Components.interfaces;
    let folders = aFolder ? [aFolder] : gFolderTreeView.getSelectedFolders();
    let folder = folders[0];

    // For newsgroups, "delete" means "unsubscribe".
    if (folder.server.type == "nntp" && !folder.getFlag(nsMsgFolderFlags.Virtual)) {
      MsgUnsubscribe(folders);
      return;
    }

    var canDelete = (folder.isSpecialFolder(nsMsgFolderFlags.Junk, false)) ?
      CanRenameDeleteJunkMail(folder.URI) : folder.deletable;

    if (!canDelete)
      throw new Error("Can't delete folder: " + folder.name);

    if (folder.flags & nsMsgFolderFlags.Virtual) {
      let confirmation = gFolderTreeView.messengerBundle
                                        .getString("confirmSavedSearchDeleteMessage");
      let title = gFolderTreeView.messengerBundle
                                 .getString("confirmSavedSearchTitle");
      if (Services.prompt
            .confirmEx(window, title, confirmation,
                       Services.prompt.STD_YES_NO_BUTTONS + Services.prompt.BUTTON_POS_1_DEFAULT,
                       "", "", "", "", {}) != 0) /* the yes button is in position 0 */
        return;
    }

    let array = toXPCOMArray([folder], Ci.nsIMutableArray);
    folder.parent.deleteSubFolders(array, msgWindow);
  },

  /**
   * Prompts the user to confirm and empties the trash for the selected folder.
   * The folder and its children are only emptied if it has the proper Trash flag.
   *
   * @param aFolder (optional)  the trash folder to empty
   * @note Calling this function on a non-trash folder will result in strange
   *       behavior!
   */
  emptyTrash: function ftc_emptyTrash(aFolder) {
    let folder = aFolder || gFolderTreeView.getSelectedFolders()[0];

    if (!folder)
      return;

    if (!this._checkConfirmationPrompt("emptyTrash", folder))
      return;

      // Check if this is a top-level smart folder. If so, we're going
      // to empty all the trash folders.
      if (folder.server.hostName == "smart mailboxes" &&
          folder.parent.isServer) {
        let subFolders = gFolderTreeView
                           ._allFoldersWithFlag(gFolderTreeView._sortedAccounts(),
                            nsMsgFolderFlags.Trash, false);
        for (let trash of subFolders)
          trash.emptyTrash(msgWindow, null);
      }
      else {
        folder.emptyTrash(msgWindow, null);
      }
  },

  /**
   * Deletes everything (folders and messages) in the selected folder.
   * The folder is only emptied if it has the proper Junk flag.
   *
   * @param aFolder (optional)  the folder to empty
   */
  emptyJunk: function ftc_emptyJunk(aFolder) {
    const Ci = Components.interfaces;
    let folder = aFolder || gFolderTreeView.getSelectedFolders()[0];

    if (!folder || !folder.getFlag(nsMsgFolderFlags.Junk))
      return;

    if (!this._checkConfirmationPrompt("emptyJunk", folder))
      return;

    // Delete any subfolders this folder might have
    let iter = folder.subFolders;
    while (iter.hasMoreElements())
      folder.propagateDelete(iter.getNext(), true, msgWindow);

    // Now delete the messages
    let messages = Array.from(fixIterator(folder.messages));
    let children = toXPCOMArray(messages, Ci.nsIMutableArray);
    folder.deleteMessages(children, msgWindow, true, false, null, false);
  },

  /**
   * Compacts either particular folder/s, or selected folders.
   *
   * @param aFolders (optional) the folders to compact, if different than the
   *                            currently selected ones
   */
  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;

      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.
   *
   * @param aFolders (optional) the folders for whose accounts we should compact
   *                            all folders, if different than the currently
   *                            selected ones
   */
  compactAllFoldersForAccount: function ftc_compactAllFoldersOfAccount(aFolders) {
    let folders = aFolders || gFolderTreeView.getSelectedFolders();
    for (let i = 0; i < folders.length; i++) {
      folders[i].compactAll(null, msgWindow, folders[i].server.type == "imap" ||
                                             folders[i].server.type == "nntp");
    }
  },

  /**
   * Opens the dialog to create a new virtual folder
   *
   * @param aName - the default name for the new folder
   * @param aSearchTerms - the search terms associated with the folder
   * @param aParent - the folder to run the search terms on
   */
  newVirtualFolder: function ftc_newVFolder(aName, aSearchTerms, aParent) {
    let folder = aParent || gFolderTreeView.getSelectedFolders()[0] ||
                 GetDefaultAccountRootFolder();
    if (!folder)
      return;

    let name = folder.prettyName;
    if (aName)
      name += "-" + aName;

    window.openDialog("chrome://messenger/content/virtualFolderProperties.xul",
                      "",
                      "chrome,modal,centerscreen",
                      {folder: folder, searchTerms: aSearchTerms,
                       newFolderName: name});
  },

  editVirtualFolder: function ftc_editVirtualFolder(aFolder) {
    let folder = aFolder || gFolderTreeView.getSelectedFolders()[0];

    //xxx should pass the folder object
    function editVirtualCallback(aURI) {
      // we need to reload the folder if it is the currently loaded folder...
      if (gFolderDisplay.displayedFolder &&
          aURI == gFolderDisplay.displayedFolder.URI)
        FolderPaneSelectionChange();
    }
    window.openDialog("chrome://messenger/content/virtualFolderProperties.xul",
                      "",
                      "chrome,modal,centerscreen",
                      {folder: folder, editExistingFolder: true,
                       onOKCallback: editVirtualCallback,
                       msgWindow: msgWindow});
  },

  /**
   * Opens a search window with the given folder, or the selected one if none
   * is given.
   *
   * @param [aFolder] the folder to open the search window for, if different
   *                  from the selected one
   */
  searchMessages: function ftc_searchMessages(aFolder) {
    MsgSearchMessages(aFolder || gFolderTreeView.getSelectedFolders()[0]);
  },

  /**
   * Prompts for confirmation, if the user hasn't already chosen the "don't ask
   * again" option.
   *
   * @param aCommand  the command to prompt for
   * @param aFolder   The folder for which the confirmation is requested.
   */
  _checkConfirmationPrompt: function ftc_confirm(aCommand, aFolder) {
    // If no folder was specified, reject the operation.
    if (!aFolder)
      return false;

    let showPrompt = true;
    try {
      showPrompt = !Services.prefs.getBoolPref("mailnews." + aCommand + ".dontAskAgain");
    } catch (ex) {}

    if (showPrompt) {
      let checkbox = {value:false};
      let title = gFolderTreeView.messengerBundle
        .getFormattedString(aCommand + "FolderTitle", [aFolder.prettyName]);
      let msg = gFolderTreeView.messengerBundle.getString(aCommand + "FolderMessage");
      let ok = Services.prompt.confirmEx(window,
                                         title,
                                         msg,
                                         Services.prompt.STD_YES_NO_BUTTONS,
                                         null, null, null,
                                         gFolderTreeView.messengerBundle.getString(aCommand + "DontAsk"),
                                         checkbox) == 0;
      if (checkbox.value)
        Services.prefs.setBoolPref("mailnews." + aCommand + ".dontAskAgain", true);
      if (!ok)
        return false;
    }
    return true;
  },

  get _tree() {
    let tree = document.getElementById("folderTree");
    delete this._tree;
    return this._tree = tree;
  }
};

/**
 * Constructor for ftv_SmartItem. This is a top level item in the "smart"
 * (a.k.a. "Unified") folder mode.
 */
function ftv_SmartItem(aFolder)
{
  ftvItem.call(this, aFolder); // call super constructor
  this._level = 0;
}

ftv_SmartItem.prototype = {
  __proto__: ftvItem.prototype,
  get children() {
    const Ci = Components.interfaces;
    let smartMode = gFolderTreeView.getFolderTreeMode("smart");

    // We're caching our child list to save perf.
    if (!this._children) {
      this._children = [];
      let iter = fixIterator(this._folder.subFolders, Ci.nsIMsgFolder);
      for (let folder in iter) {
        if (!smartMode.isSmartFolder(folder)) {
          this._children.push(new ftv_SmartItem(folder));
        }
        else if (folder.flags & nsMsgFolderFlags.Inbox) {
          let subIter = fixIterator(folder.subFolders, Ci.nsIMsgFolder);
          for (let subfolder in subIter) {
            if (!smartMode.isSmartFolder(subfolder))
              this._children.push(new ftv_SmartItem(subfolder));
          }
        }
      }
      sortFolderItems(this._children);
      // Each child is a level one below us
      for (let child of this._children) {
        child._level = this._level + 1;
        child._parent = this;
      }
    }
    return this._children;
  }
}

/**
 * Sorts the passed in array of folder items using the folder sort key
 *
 * @param aFolders - the array of ftvItems to sort.
 */
function sortFolderItems (aFtvItems) {
  function sorter(a, b) {
    return a._folder.compareSortKeys(b._folder);
  }
  aFtvItems.sort(sorter);
}

/**
 * An extension wishing to set a folderpane tree property must use
 * gFolderTreeView.setFolderCacheProperty(). Due to severe perf and memory
 * issues, direct access by nsITreeView methods to any call which opens a
 * folder's msgDatabase is disallowed.
 *
 * Example:
 *   gFolderTreeView.setFolderCacheProperty(folder, // nsIMsgFolder
 *                                          "smartFolderName",
 *                                          "My Smart Folder");
 * Note: for css styling using nsITreeView pseudo elements, the name property
 * is returned with all spaces removed, eg |specialFolder-MySmartFolder|.
 *
 * @param nsIMsgFolder aFolder  - The folder.
 * @return property || null     - Cached property value, or null if not set.
 */
function getSmartFolderName(aFolder) {
  return gFolderTreeView.getFolderCacheProperty(aFolder, "smartFolderName");
}

function setSmartFolderName(aFolder, aName) {
  gFolderTreeView.setFolderCacheProperty(aFolder, "smartFolderName", aName);
}

var gFolderStatsHelpers = {
    kUnknownSize: "-",
    sumSubfoldersPref: false,
    sumSubfolders: false,
    sizeUnits: "",
    kiloUnit: "KB",
    megaUnit: "MB",

    init: function() {
      // We cache these values because the cells in the folder pane columns
      // using these helpers can be redrawn often.
      this.sumSubfoldersPref = Services.prefs.getBoolPref("mail.folderpane.sumSubfolders");
      this.sizeUnits = Services.prefs.getCharPref("mail.folderpane.sizeUnits");
      this.kiloUnit = gFolderTreeView.messengerBundle.getString("kiloByteAbbreviation2");
      this.megaUnit = gFolderTreeView.messengerBundle.getString("megaByteAbbreviation2");
    },

    /**
     * Add a prefix to denote the value is actually a sum of all the subfolders.
     * The prefix is useful as this sum may not always be the exact sum of individual
     * folders when they are shown expanded (due to rounding to a unit).
     * E.g. folder1 600bytes -> 1KB, folder2 700bytes -> 1KB
     * summarized at parent folder: 1300bytes -> 1KB
     *
     * @param aValue                  The value to be displayed.
     * @param aSubfoldersContributed  Boolean indicating whether subfolders
     *                                contributed to the accumulated total value.
     */
    addSummarizedPrefix: function(aValue, aSubfoldersContributed) {
      if (!this.sumSubfolders)
        return aValue;

      if (!aSubfoldersContributed)
        return aValue;

      return gFolderTreeView.messengerBundle
        .getFormattedString("folderSummarizedSymbolValue", [aValue]);
    },

    /**
     * nsIMsgFolder uses -1 as a magic number to mean "I don't know". In those
     * cases we indicate it to the user. The user has to open the folder
     * so that the property is initialized from the DB.
     *
     * @param aNumber                 The number to translate for the user.
     * @param aSubfoldersContributed  Boolean indicating whether subfolders
     *                                contributed to the accumulated total value.
     */
    fixNum: function(aNumber, aSubfoldersContributed) {
      if (aNumber < 0)
        return this.kUnknownSize;

      return (aNumber == 0 ? "" : this.addSummarizedPrefix(aNumber,
                                                           aSubfoldersContributed));
    },

    /**
     * Get the size of the specified folder.
     *
     * @param aFolder  The nsIMsgFolder to analyze.
     */
    getFolderSize: function(aFolder) {
      let folderSize = 0;
      try {
        folderSize = aFolder.sizeOnDisk;
        if (folderSize < 0)
          return this.kUnknownSize;
      } catch(ex) {
        return this.kUnknownSize;
      }
      return folderSize;
    },

    /**
     * Get the total size of all subfolders of the specified folder.
     *
     * @param aFolder  The nsIMsgFolder to analyze.
     */
    getSubfoldersSize: function(aFolder) {
      let folderSize = 0;
      if (aFolder.hasSubFolders) {
        let subFolders = aFolder.subFolders;
        while (subFolders.hasMoreElements()) {
          let subFolder = subFolders.getNext()
            .QueryInterface(Components.interfaces.nsIMsgFolder);
          let subSize = this.getFolderSize(subFolder);
          let subSubSize = this.getSubfoldersSize(subFolder);
          if (subSize == this.kUnknownSize || subSubSize == this.kUnknownSize)
            return subSize;

          folderSize += subSize + subSubSize;
        }
      }
      return folderSize;
    },

    /**
     * Format the given folder size into a string with an appropriate unit.
     *
     * @param aSize  The size in bytes to format.
     * @param aUnit  Optional unit to use for the format.
     *               Possible values are "KB" or "MB".
     * @return       An array with 2 values. First is the resulting formatted strings.
     *               The second one is the final unit used to format the string.
     */
    formatFolderSize: function(aSize, aUnit = gFolderStatsHelpers.sizeUnits) {
      let size = Math.round(aSize / 1024);
      let unit = gFolderStatsHelpers.kiloUnit;
      // If size is non-zero try to show it in a unit that fits in 3 digits,
      // but if user specified a fixed unit, use that.
      if (aUnit != "KB" && (size > 999 || aUnit == "MB")) {
        size = Math.round(size / 1024);
        unit = gFolderStatsHelpers.megaUnit;
        aUnit = "MB";
      }
      // This needs to be updated if the "%.*f" placeholder string
      // in "*ByteAbbreviation2" in messenger.properties changes.
      return [unit.replace("%.*f", size).replace(" ",""), aUnit];
    }
};
