Bug 1087233 - Create about:downloads to migrate to Downloads.jsm. try: -t none -u mozmill draft
authorHiroyuki Ikezoe <hiikezoe@mozilla-japan.org>
Thu, 22 Jan 2015 09:25:09 +0900
changeset 27853 1e520530a01e05ee95c0d2d2b57079ce9530cdc3
parent 27846 f4503d2596a421afc5ecd3f7a6144cc8575cf6d1
push id2123
push userhiikezoe@mozilla-japan.org
push dateThu, 22 Jan 2015 00:25:18 +0000
treeherdertry-comm-central@1e520530a01e [default view] [failures only]
bugs1087233
Bug 1087233 - Create about:downloads to migrate to Downloads.jsm. try: -t none -u mozmill
mail/base/content/mailCore.js
mail/components/aboutRedirector.js
mail/components/downloads/content/aboutDownloads.css
mail/components/downloads/content/aboutDownloads.js
mail/components/downloads/content/aboutDownloads.xul
mail/components/downloads/content/download.xml
mail/components/downloads/jar.mn
mail/components/downloads/moz.build
mail/components/mailComponents.manifest
mail/components/moz.build
mail/confvars.sh
mail/installer/package-manifest.in
mail/locales/en-US/chrome/messenger/aboutDownloads.dtd
mail/locales/jar.mn
mail/test/mozmill/downloads/test-about-downloads.js
mail/test/mozmill/mozmilltests.list
mailnews/base/src/nsMessenger.cpp
--- a/mail/base/content/mailCore.js
+++ b/mail/base/content/mailCore.js
@@ -503,19 +503,20 @@ function openIMAccountWizard()
     return;
   }
 #endif
   window.openDialog(kUrl, kName, kFeatures);
 }
 
 function openSavedFilesWnd()
 {
-  Components.classes['@mozilla.org/download-manager-ui;1']
-            .getService(Components.interfaces.nsIDownloadManagerUI)
-            .show(window);
+  let tabmail = document.getElementById("tabmail");
+  tabmail.openTab("chromeTab",
+                  { chromePage: "about:downloads",
+                    clickHandler: "specialTabs.aboutClickHandler(event);" });
 }
 
 function SetBusyCursor(window, enable)
 {
     // setCursor() is only available for chrome windows.
     // However one of our frames is the start page which
     // is a non-chrome window, so check if this window has a
     // setCursor method
--- a/mail/components/aboutRedirector.js
+++ b/mail/components/aboutRedirector.js
@@ -19,16 +19,18 @@ AboutRedirector.prototype = {
   _redirMap: {
     "rights": {url: "chrome://messenger/content/aboutRights.xhtml",
                flags: (Ci.nsIAboutModule.ALLOW_SCRIPT |
                        Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT)},
     "support": {url: "chrome://messenger/content/about-support/aboutSupport.xhtml",
                 flags: Ci.nsIAboutModule.ALLOW_SCRIPT},
     "preferences": {url: "chrome://messenger/content/preferences/aboutPreferences.xul",
                     flags: Ci.nsIAboutModule.ALLOW_SCRIPT},
+    "downloads": {url: "chrome://messenger/content/downloads/aboutDownloads.xul",
+                  flags: Ci.nsIAboutModule.ALLOW_SCRIPT},
   },
 
   /**
    * Gets the module name from the given URI.
    */
   _getModuleName: function AboutRedirector__getModuleName(aURI) {
     // Strip out the first ? or #, and anything following it
     let name = (/[^?#]+/.exec(aURI.path))[0];
new file mode 100644
--- /dev/null
+++ b/mail/components/downloads/content/aboutDownloads.css
@@ -0,0 +1,26 @@
+/* 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/. */
+
+richlistitem.download {
+  -moz-binding: url('chrome://messenger/content/downloads/download.xml#download');
+}
+
+#msgDownloadsRichListBox {
+  /** The default listbox appearance comes with an unwanted margin. **/
+  margin: 0;
+  display: -moz-box;
+}
+
+#msgDownloadsRichListBox > richlistitem.download {
+  height: 5em;
+  padding: 5px 8px;
+}
+
+#msgDownloadsListEmptyDescription {
+  display: none;
+}
+
+#msgDownloadsRichListBox:empty + #msgDownloadsListEmptyDescription {
+  display: -moz-box;
+}
new file mode 100644
--- /dev/null
+++ b/mail/components/downloads/content/aboutDownloads.js
@@ -0,0 +1,325 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* 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/. */
+
+"use strict";
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", "resource://gre/modules/DownloadUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+
+const DownloadsView = {
+  init() {
+    window.controllers.insertControllerAt(0, this);
+    this.listElement = document.getElementById("msgDownloadsRichListBox");
+
+    this.items = new Map();
+
+    Downloads.getList(Downloads.ALL)
+             .then(list => list.addView(this))
+             .then(null, Cu.reportError);
+
+    window.addEventListener("unload", aEvent => {
+      Downloads.getList(Downloads.ALL)
+               .then(list => list.removeView(this))
+               .then(null, Cu.reportError);
+      window.controllers.removeController(this);
+    });
+  },
+
+  insertOrMoveItem(aItem) {
+    let compare = (a, b) => {
+      // active downloads always before stopped downloads
+      if (a.stopped != b.stopped) {
+        return b.stopped ? -1 : 1
+      }
+      // most recent downloads first
+      return b.startTime - a.startTime;
+    };
+
+    let at = this.listElement.firstChild;
+    while (at && compare(aItem.download, at.download) > 0) {
+      at = at.nextElementSibling;
+    }
+    this.listElement.insertBefore(aItem.element, at);
+  },
+
+  onDownloadAdded(aDownload) {
+    let isPurgedFromDisk = download => {
+      if (!download.succeeded) {
+        return false;
+      }
+      let targetFile = Cc["@mozilla.org/file/local;1"]
+                        .createInstance(Ci.nsIFile);
+      targetFile.initWithPath(download.target.path);
+      return !targetFile.exists();
+    }
+    if (isPurgedFromDisk(aDownload)) {
+      Downloads.getList(Downloads.ALL)
+               .then(list => list.remove(aDownload))
+      return;
+    }
+
+    let item = new DownloadItem(aDownload);
+    this.items.set(aDownload, item);
+    this.insertOrMoveItem(item);
+  },
+
+  onDownloadChanged(aDownload) {
+    let item = this.items.get(aDownload);
+    if (!item) {
+      Cu.reportError("No DownloadItem found for download");
+      return;
+    }
+
+    if (item.stateChanged) {
+      this.insertOrMoveItem(item);
+    }
+
+    item.onDownloadChanged();
+  },
+
+  onDownloadRemoved(aDownload) {
+    let item = this.items.get(aDownload);
+    if (!item) {
+      Cu.reportError("No DownloadItem found for download");
+      return;
+    }
+
+    this.items.delete(aDownload);
+    this.listElement.removeChild(item.element);
+  },
+
+  onDownloadContextMenu(aEvent) {
+    let element = this.listElement.selectedItem;
+    if (!element) {
+      return;
+    }
+
+    this.updateCommands();
+  },
+
+  clearDownloads() {
+    Downloads.getList(Downloads.ALL)
+             .then(list => list.removeFinished())
+             .then(null, Cu.reportError);
+  },
+
+  supportsCommand(aCommand) {
+    if (!(this.commands.some(command => command == aCommand)) &&
+        !(DownloadItem.prototype.supportsCommand(aCommand))) {
+      return false;
+    }
+    return true;
+  },
+
+  isCommandEnabled(aCommand) {
+    if (aCommand == "msgDownloadsCmd_clearDownloads") {
+        return true;
+    }
+
+    let element = this.listElement.selectedItem;
+    if (element) {
+      return element.downloadItem.isCommandEnabled(aCommand);
+    }
+
+    return false;
+  },
+
+  doCommand(aCommand) {
+    if (aCommand == "msgDownloadsCmd_clearDownloads") {
+        this.clearDownloads();
+        return;
+    }
+
+    if (this.listElement.selectedItems.length == 0) {
+      return;
+    }
+
+    for (let [, element] in Iterator(this.listElement.selectedItems)) {
+      element.downloadItem.doCommand(aCommand);
+    }
+  },
+
+  onEvent() { },
+
+  updateCommands() {
+    this.commands.forEach(goUpdateCommand);
+    DownloadItem.prototype.commands.forEach(goUpdateCommand);
+  },
+
+  commands: [
+    "msgDownloadsCmd_clearDownloads",
+  ]
+};
+
+function DownloadItem(aDownload) {
+  this._download = aDownload;
+  this._updateFromDownload();
+
+  if (aDownload._unknownProperties && aDownload._unknownProperties.sender) {
+    this._sender = aDownload._unknownProperties.sender;
+  } else {
+    this._sender = "";
+  }
+  this._fileName = this._htmlEscape(OS.Path.basename(aDownload.target.path));
+  this._iconUrl = "moz-icon://" + this._fileName + "?size=32";
+  this._startDate = this._htmlEscape(DownloadUtils.getReadableDates(aDownload.startTime)[0]);
+  this._filePath = aDownload.target.path;
+}
+
+const kDownloadStatePropertyNames = [
+  "stopped",
+  "succeeded",
+  "canceled",
+  "error",
+  "startTime"
+];
+
+DownloadItem.prototype = {
+  _htmlEscape(s) {
+    s = s.replace(/&/g, "&amp;");
+    s = s.replace(/>/g, "&gt;");
+    s = s.replace(/</g, "&lt;");
+    s = s.replace(/"/g, "&quot;");
+    s = s.replace(/'/g, "&apos;");
+    return s;
+  },
+
+  _updateFromDownload() {
+    this._state = {};
+    for (let name of kDownloadStatePropertyNames) {
+      this._state[name] = this._download[name];
+    }
+  },
+
+  get stateChanged() {
+    for (let name of kDownloadStatePropertyNames) {
+      if (this._state[name] != this._download[name]) {
+        return true;
+      }
+    }
+    return false;
+  },
+
+  get download() this._download,
+
+  get element() {
+    if (!this._element) {
+      this._element = this.createElement();
+    }
+
+    return this._element;
+  },
+
+  createElement() {
+    let element = document.createElement("richlistitem");
+    element.classList.add("download");
+
+    // launch the download if double clicked
+    element.addEventListener("dblclick", aEvent => this.launch());
+
+    // set download as an expando property for the context menu
+    element.download = this.download;
+    element.downloadItem = this;
+
+    this.updateElement(element);
+
+    return element;
+  },
+
+  updateElement(aElement) {
+    aElement.setAttribute("image", this.iconUrl);
+    aElement.setAttribute("size", this.size);
+    aElement.setAttribute("displayName", this.fileName);
+    aElement.setAttribute("sender", this.sender);
+    aElement.setAttribute("startDate", this.startDate);
+  },
+
+  launch() {
+    if (this.download.succeeded) {
+      this.download.launch().then(null, Cu.reportError);
+    }
+  },
+
+  remove() {
+    Downloads.getList(Downloads.ALL)
+             .then(list => list.remove(this.download))
+             .then(() => this.download.finalize(true))
+             .then(null, Cu.reportError);
+  },
+
+  show() {
+    if (this.download.succeeded) {
+      let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+      file.initWithPath(this._filePath);
+      file.reveal();
+    }
+  },
+
+  onDownloadChanged() {
+    this._updateFromDownload();
+    this.updateElement(this.element);
+  },
+
+  get fileName() this._fileName,
+
+  get iconUrl() this._iconUrl,
+
+  get sender() this._sender,
+
+  get size() {
+    let bytes;
+    if (this.download.succeeded || this.download.hasProgress) {
+      bytes = this.download.target.size;
+    } else {
+      bytes = this.download.currentBytes;
+    }
+    return DownloadUtils.convertByteUnits(bytes).join("");
+  },
+
+  get startDate() this._startDate,
+
+  supportsCommand(aCommand) {
+    return this.commands.some(command => command == aCommand);
+  },
+
+  isCommandEnabled(aCommand) {
+    switch (aCommand) {
+      case "msgDownloadsCmd_open":
+      case "msgDownloadsCmd_show":
+        return this.download.succeeded;
+      case "msgDownloadsCmd_remove":
+        return true;
+    }
+    return false;
+  },
+
+  doCommand(aCommand) {
+    switch (aCommand) {
+      case "msgDownloadsCmd_open":
+        this.launch();
+        break;
+      case "msgDownloadsCmd_show":
+        this.show();
+        break;
+      case "msgDownloadsCmd_remove":
+        this.remove();
+        break;
+    }
+  },
+
+  commands: [
+    "msgDownloadsCmd_remove",
+    "msgDownloadsCmd_open",
+    "msgDownloadsCmd_show",
+  ]
+};
new file mode 100644
--- /dev/null
+++ b/mail/components/downloads/content/aboutDownloads.xul
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet href="chrome://global/skin/"?>
+<?xml-stylesheet href="chrome://messenger/content/downloads/aboutDownloads.css" type="text/css"?>
+<!-- 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/. -->
+
+<!DOCTYPE window [
+<!ENTITY % aboutDownloadsDTD SYSTEM "chrome://messenger/locale/aboutDownloads.dtd">
+%aboutDownloadsDTD;
+]>
+
+<window id="aboutDownloads"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        title="&aboutDownloads.title;"
+        onload="DownloadsView.init();">
+  <script type="application/javascript"
+          src="chrome://global/content/globalOverlay.js"/>
+  <script type="application/javascript"
+          src="chrome://messenger/content/downloads/aboutDownloads.js"/>
+
+  <stack flex="1">
+    <richlistbox id="msgDownloadsRichListBox"
+                 flex="1"
+                 seltype="multiple"
+                 context="msgDownloadsContextMenu"
+                 oncontextmenu="DownloadsView.onDownloadContextMenu(event);"/>
+    <description id="msgDownloadsListEmptyDescription"
+                 value="&aboutDownloads.empty;"
+                 mousethrough="always"/>
+  </stack>
+
+  <commandset id="msgDownloadCommands"
+              commandupdater="true"
+              events="focus,select,contextmenu">
+    <command id="msgDownloadsCmd_open"
+             oncommand="goDoCommand('msgDownloadsCmd_open')"/>
+    <command id="msgDownloadsCmd_show"
+             oncommand="goDoCommand('msgDownloadsCmd_show')"/>
+    <command id="msgDownloadsCmd_remove"
+             oncommand="goDoCommand('msgDownloadsCmd_remove')"/>
+    <command id="msgDownloadsCmd_clearDownloads"
+             oncommand="goDoCommand('msgDownloadsCmd_clearDownloads')"/>
+  </commandset>
+
+  <menupopup id="msgDownloadsContextMenu">
+    <menuitem command="msgDownloadsCmd_remove"
+              class="msgDownloadRemoveFromHistoryMenuItem"
+              label="&cmd.removeFromHistory.label;"
+              accesskey="&cmd.removeFromHistory.accesskey;"/>
+    <menuitem command="msgDownloadsCmd_open"
+              label="&cmd.open.label;"
+              accesskey="&cmd.open.accesskey;"/>
+    <menuitem command="msgDownloadsCmd_show"
+              class="msgDownloadShowMenuItem"
+#ifdef XP_MACOSX
+              label="&cmd.showMac.label;"
+              accesskey="&cmd.showMac.accesskey;"
+#else
+              label="&cmd.show.label;"
+              accesskey="&cmd.show.accesskey;"
+#endif
+              />
+    <menuitem command="msgDownloadsCmd_clearDownloads"
+              label="&cmd.clearDownloads.label;"
+              accesskey="&cmd.clearDownloads.accesskey;"/>
+  </menupopup>
+</window>
new file mode 100644
--- /dev/null
+++ b/mail/components/downloads/content/download.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0"?>
+<!-- -*- Mode: HTML; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- -->
+<!-- vim: set ts=2 et sw=2 tw=80: -->
+
+<!-- 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/. -->
+
+<bindings id="downloadBindings"
+          xmlns="http://www.mozilla.org/xbl"
+          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+          xmlns:xbl="http://www.mozilla.org/xbl">
+
+  <binding id="download"
+           extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+
+    <content orient="horizontal" align="center">
+      <xul:image class="fileTypeIcon"
+                 validate="always"
+                 xbl:inherits="src=image"/>
+      <xul:vbox pack="center" flex="1">
+        <xul:description class="fileName"
+                         crop="center"
+                         xbl:inherits="value=displayName,tooltiptext=displayName"/>
+        <xul:description class="size"
+                         xbl:inherits="value=size,tooltiptext=size"/>
+        <xul:description class="startDate"
+                         crop="end"
+                         xbl:inherits="value=startDate,tooltiptext=startDate"/>
+      </xul:vbox>
+    <xul:description class="sender"
+                     xbl:inherits="value=sender,tooltiptext=sender"/>
+    </content>
+  </binding>
+
+</bindings>
new file mode 100644
--- /dev/null
+++ b/mail/components/downloads/jar.mn
@@ -0,0 +1,9 @@
+# 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/.
+
+messenger.jar:
+    content/messenger/downloads/download.xml           (content/download.xml)
+    content/messenger/downloads/aboutDownloads.js      (content/aboutDownloads.js)
+*   content/messenger/downloads/aboutDownloads.xul     (content/aboutDownloads.xul)
+    content/messenger/downloads/aboutDownloads.css     (content/aboutDownloads.css)
new file mode 100644
--- /dev/null
+++ b/mail/components/downloads/moz.build
@@ -0,0 +1,5 @@
+# vim: set filetype=python:
+# 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/.
+JAR_MANIFESTS += ['jar.mn']
--- a/mail/components/mailComponents.manifest
+++ b/mail/components/mailComponents.manifest
@@ -1,19 +1,28 @@
 component {8cc51368-6aa0-43e8-b762-bde9b9fd828c} aboutRedirector.js
 # Each addition here should be coupled with a corresponding addition in
 # aboutRedirector.js.
 contract @mozilla.org/network/protocol/about;1?what=rights {8cc51368-6aa0-43e8-b762-bde9b9fd828c}
 contract @mozilla.org/network/protocol/about;1?what=support {8cc51368-6aa0-43e8-b762-bde9b9fd828c}
 contract @mozilla.org/network/protocol/about;1?what=preferences {8cc51368-6aa0-43e8-b762-bde9b9fd828c}
+contract @mozilla.org/network/protocol/about;1?what=downloads {8cc51368-6aa0-43e8-b762-bde9b9fd828c}
 
 component {44346520-c5d2-44e5-a1ec-034e04d7fac4} nsMailDefaultHandler.js
 contract @mozilla.org/mail/clh;1 {44346520-c5d2-44e5-a1ec-034e04d7fac4}
 category command-line-handler x-default @mozilla.org/mail/clh;1
 category command-line-validator b-default @mozilla.org/mail/clh;1
 
 component {1c73f03a-b817-4640-b984-18c3478a9ae3} mailContentHandler.js
 contract @mozilla.org/uriloader/content-handler;1?type=text/html {1c73f03a-b817-4640-b984-18c3478a9ae3}
 contract @mozilla.org/uriloader/content-handler;1?type=text/plain {1c73f03a-b817-4640-b984-18c3478a9ae3}
 
 component {eb239c82-fac9-431e-98d7-11cacd0f71b8} mailGlue.js
 contract @mozilla.org/mail/mailglue;1 {eb239c82-fac9-431e-98d7-11cacd0f71b8}
 category app-startup MailGlue service,@mozilla.org/mail/mailglue;1
+
+component {a93f0d6f-02a3-4486-a662-8f49b8c1de48} DownloadsStartup.js
+contract @mozilla.org/mail/downloadsstartup;1 {a93f0d6f-02a3-4486-a662-8f49b8c1de48}
+category profile-after-change DownloadsStartup @mozilla.org/mail/downloadsstartup;1
+
+component {a93f0d6f-02a3-4486-a662-8f49b8c1de48} DownloadsStartup.js
+contract @mozilla.org/mail/downloadsstartup;1 {a93f0d6f-02a3-4486-a662-8f49b8c1de48}
+category profile-after-change DownloadsStartup @mozilla.org/mail/downloadsstartup;1
--- a/mail/components/moz.build
+++ b/mail/components/moz.build
@@ -4,16 +4,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # Only Mac and Windows have search integration components, but we include at
 # least one module from search/ on all platforms
 DIRS += [
     'compose',
     'cloudfile',
     'devtools',
+    'downloads',
     'preferences',
     'addrbook',
     'migration',
     'activity',
     'search',
     'about-support',
     'wintaskbar',
     'newmailaccount',
@@ -30,16 +31,17 @@ DIRS += ['build']
 XPIDL_SOURCES += [
     'nsIMailGlue.idl',
 ]
 
 XPIDL_MODULE = 'mailcompsbase'
 
 EXTRA_COMPONENTS += [
     'aboutRedirector.js',
+    'DownloadsStartup.js',
     'mailComponents.manifest',
     'mailContentHandler.js',
     'mailGlue.js',
     'nsMailDefaultHandler.js',
 ]
 
 EXTRA_JS_MODULES += [
     'appIdleManager.js',
--- a/mail/confvars.sh
+++ b/mail/confvars.sh
@@ -38,8 +38,9 @@ MOZ_APP_ID={3550f703-e582-4d05-9a08-453d
 # If more than one ID is needed, then you should use a comma separated list
 # of values.
 ACCEPTED_MAR_CHANNEL_IDS=thunderbird-comm-central
 # The MAR_CHANNEL_ID must not contain the following 3 characters: ",\t "
 MAR_CHANNEL_ID=thunderbird-comm-central
 # Enable generational GC on desktop.
 JSGC_GENERATIONAL=1
 MOZ_PROFILE_MIGRATOR=1
+MOZ_JSDOWNLOADS=1
--- a/mail/installer/package-manifest.in
+++ b/mail/installer/package-manifest.in
@@ -366,21 +366,22 @@
 @RESPATH@/components/loginmgr.xpt
 @RESPATH@/components/nsLoginInfo.js
 @RESPATH@/components/nsLoginManager.js
 @RESPATH@/components/nsLoginManagerPrompter.js
 @RESPATH@/components/passwordmgr.manifest
 @RESPATH@/components/storage-json.js
 @RESPATH@/components/crypto-SDR.js
 
+; download progress for jsdownloads
+@RESPATH@/components/DownloadsStartup.js
+
 ; download progress
 @RESPATH@/components/nsHelperAppDlg.js
 @RESPATH@/components/nsHelperAppDlg.manifest
-@RESPATH@/components/nsDownloadManagerUI.js
-@RESPATH@/components/nsDownloadManagerUI.manifest
 @RESPATH@/components/downloads.xpt
 
 ; Protocol/Content handling
 @RESPATH@/components/nsContentDispatchChooser.js
 @RESPATH@/components/nsContentDispatchChooser.manifest
 @RESPATH@/components/nsHandlerService.js
 @RESPATH@/components/nsHandlerService.manifest
 @RESPATH@/components/nsWebHandlerApp.js
new file mode 100644
--- /dev/null
+++ b/mail/locales/en-US/chrome/messenger/aboutDownloads.dtd
@@ -0,0 +1,21 @@
+<!-- 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/. -->
+
+<!ENTITY aboutDownloads.title                      "Saved Files">
+<!ENTITY aboutDownloads.empty                      "No Saved Files">
+<!-- LOCALIZATION NOTE (cmd.show.label, cmd.show.accesskey, cmd.showMac.label,
+     cmd.showMac.accesskey):
+     The show and showMac commands are never shown together, thus they can share
+     the same access key (though the two access keys can also be different).
+     -->
+<!ENTITY cmd.show.label                            "Open Containing Folder">
+<!ENTITY cmd.show.accesskey                        "F">
+<!ENTITY cmd.showMac.label                         "Show In Finder">
+<!ENTITY cmd.showMac.accesskey                     "F">
+<!ENTITY cmd.open.label                            "Open">
+<!ENTITY cmd.open.accesskey                        "O">
+<!ENTITY cmd.removeFromHistory.label               "Remove From History">
+<!ENTITY cmd.removeFromHistory.accesskey           "e">
+<!ENTITY cmd.clearDownloads.label                  "Clear Downloads">
+<!ENTITY cmd.clearDownloads.accesskey              "D">
--- a/mail/locales/jar.mn
+++ b/mail/locales/jar.mn
@@ -5,16 +5,17 @@
 
 #filter substitution
 
 @AB_CD@.jar:
 % locale messenger @AB_CD@ %locale/@AB_CD@/messenger/
 % override chrome://mozapps/locale/downloads/settingsChange.dtd chrome://messenger/locale/downloads/settingsChange.dtd
 % override chrome://global/locale/netError.dtd chrome://messenger/locale/netError.dtd
   locale/@AB_CD@/messenger/aboutDialog.dtd                              (%chrome/messenger/aboutDialog.dtd)
+  locale/@AB_CD@/messenger/aboutDownloads.dtd                           (%chrome/messenger/aboutDownloads.dtd)
   locale/@AB_CD@/messenger/aboutRights.dtd                              (%chrome/messenger/aboutRights.dtd)
   locale/@AB_CD@/messenger/aboutRights.properties                       (%chrome/messenger/aboutRights.properties)
   locale/@AB_CD@/messenger/aboutSupportMail.dtd                         (%chrome/messenger/aboutSupportMail.dtd)
   locale/@AB_CD@/messenger/aboutSupportMail.properties                  (%chrome/messenger/aboutSupportMail.properties)
   locale/@AB_CD@/messenger/telemetry.properties                         (%chrome/messenger/telemetry.properties)
   locale/@AB_CD@/messenger/accountCreation.dtd                          (%chrome/messenger/accountCreation.dtd)
   locale/@AB_CD@/messenger/accountCreation.properties                   (%chrome/messenger/accountCreation.properties)
   locale/@AB_CD@/messenger/accountCreationModel.properties              (%chrome/messenger/accountCreationModel.properties)
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/downloads/test-about-downloads.js
@@ -0,0 +1,296 @@
+/* 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/. */
+
+/**
+ * Test about:downloads.
+ */
+
+var MODULE_NAME = 'test-about-download';
+
+var RELATIVE_ROOT = "../shared-modules";
+var MODULE_REQUIRES = [ 'attachment-helpers',
+                        'content-tab-helpers',
+                        'dom-helpers',
+                        'folder-display-helpers',
+                        'prompt-helpers',
+                        'window-helpers' ];
+
+var mozmill = {}; Components.utils.import('resource://mozmill/modules/mozmill.js', mozmill);
+var elementslib = {}; Components.utils.import('resource://mozmill/modules/elementslib.js', elementslib);
+var downloads = {}; Components.utils.import("resource://gre/modules/Downloads.jsm", downloads);
+
+var ah;
+
+var downloadsTab;
+
+var attachmentFileNames = [
+  "Attachment#1.txt",
+  "Attachment#2.txt",
+  "Attachment#3.txt",
+];
+
+const downloadsView = {
+  init() {
+    this.items = new Map();
+    this.removedItems = [];
+  },
+
+  get count() {
+    return this.items.size;
+  },
+
+  onDownloadAdded(aDownload) {
+    this.items.set(aDownload, aDownload.target.path);
+  },
+
+  onDownloadChanged(aDownload) {
+  },
+
+  onDownloadRemoved(aDownload) {
+    this.removedItems.push(aDownload.target.path);
+    this.items.delete(aDownload);
+  },
+
+  waitForFinish() {
+    for (let download of this.items.keys()) {
+      let succeededPromise = download.whenSucceeded();
+      yield succeededPromise;
+    }
+  }
+};
+
+function prepare_messages() {
+  let folder = create_folder("about:downloads");
+  let msgSet = make_new_sets_in_folder(folder, [
+    {
+      count: 1,
+      attachments: [{
+        filename: attachmentFileNames[0],
+        body: "Body"
+      }]
+    },
+    {
+      count: 1,
+      attachments: [{
+        filename: attachmentFileNames[1],
+        body: "Body"
+      }]
+    },
+    {
+      count: 1,
+      attachments: [{
+        filename: attachmentFileNames[2],
+        body: "Body"
+      }]
+    }
+  ]);
+  be_in_folder(folder);
+}
+
+function prepare_downloads_view() {
+  let success = false;
+  downloads.Downloads.getList(downloads.Downloads.ALL)
+                     .then(list => list.addView(downloadsView))
+                     .then(() => success = true, Cu.reportError);
+  mc.waitFor(() => success, "Timeout waiting for attaching our download view.");
+}
+
+function setupModule(module) {
+  let fdh = collector.getModule("folder-display-helpers");
+  fdh.installInto(module);
+  let wh = collector.getModule('window-helpers');
+  wh.installInto(module);
+  let cth = collector.getModule("content-tab-helpers");
+  cth.installInto(module);
+  let dh = collector.getModule('dom-helpers');
+  dh.installInto(module);
+  ah = collector.getModule('attachment-helpers');
+  ah.installInto(module);
+  ah.gMockFilePickReg.register();
+
+  prepare_messages();
+  prepare_downloads_view();
+}
+
+function setupTest(test) {
+  downloadsView.init();
+}
+
+function open_about_downloads() {
+  let preCount = mc.tabmail.tabContainer.childNodes.length;
+  let newTab = mc.tabmail.openTab("chromeTab", { chromePage: "about:downloads",
+                                                 clickHandler: "specialTabs.aboutClickHandler(event);" });
+  mc.waitFor(() => mc.tabmail.tabContainer.childNodes.length == preCount + 1,
+             "Timeout waiting for about:downloads tab");
+
+  wait_for_browser_load(newTab.browser, "about:downloads");
+  // We append new tabs at the end, so check the last one.
+  let expectedNewTab = mc.tabmail.tabInfo[preCount];
+  return expectedNewTab;
+}
+
+/**
+ * Test that there is no file in the list at first.
+ */
+function test_empty_list() {
+  downloadsTab = open_about_downloads();
+
+  switch_tab(downloadsTab);
+
+  let empty = content_tab_e(downloadsTab, "msgDownloadsListEmptyDescription");
+  assert_false(empty.hidden, "msgDownloadsListEmptyDescription is not visible");
+}
+
+function save_attachment_files() {
+  switch_tab(0);
+
+  let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+
+  let length = attachmentFileNames.length;
+  for (let i = 0; i < length; i++) {
+    let file = profileDir.clone();
+    file.append(attachmentFileNames[i]);
+    select_click_row(i);
+    gMockFilePicker.returnFiles = [ file ];
+    mc.click(mc.eid("attachmentSaveAllSingle",
+                    {"class": "toolbarbutton-menubutton-button"}));
+  }
+}
+
+/**
+ * Test that all downloaded files are showed up in the list.
+ */
+function test_save_attachment_files_in_list() {
+  save_attachment_files();
+
+  mc.tabmail.switchToTab(downloadsTab);
+  let list = content_tab_e(downloadsTab, "msgDownloadsRichListBox");
+
+  let length = attachmentFileNames.length;
+  mc.waitFor(() => downloadsView.count == length,
+             "Timeout waiting for saving three attachment files.");
+
+  assert_equals(length, list.childNodes.length);
+  assert_equals(downloadsView.count, list.childNodes.length);
+
+  let actualNames = [];
+  let child = list.firstChild;
+  while (child) {
+    actualNames.push(child.getAttribute("displayName"));
+    child = child.nextSibling;
+  }
+  actualNames.sort();
+
+  for (let i = 0; i < length; i++) {
+    assert_equals(attachmentFileNames[i], actualNames[i]);
+  }
+}
+
+/**
+ * Test that 'remove' in context menu removes surely the target file from
+ * the list.
+ */
+function test_remove_file() {
+  test_save_attachment_files_in_list();
+
+  let list = content_tab_e(downloadsTab, "msgDownloadsRichListBox");
+  let firstElement = list.firstChild;
+  let removingFileName = firstElement.getAttribute("displayName");
+
+  // select first element
+  mc.click(new elementslib.Elem(firstElement));
+  mc.rightClick(new elementslib.Elem(firstElement));
+
+  let contextMenu = content_tab_e(downloadsTab, "msgDownloadsContextMenu");
+  wait_for_popup_to_open(contextMenu);
+  mc.click_menus_in_sequence(contextMenu, [
+                               { command: "msgDownloadsCmd_remove" }
+                             ]);
+  mc.waitFor(() => downloadsView.count == 2,
+             "Timeout waiting for removing a saved attachment file.");
+
+  child = list.firstChild;
+  while (child) {
+    assert_not_equals(removingFileName, child.getAttribute("displayName"));
+    child = child.nextSibling;
+  }
+}
+
+/**
+ * Test that removing multiple files surely removes the files.
+ */
+function test_remove_multiple_files() {
+  test_save_attachment_files_in_list();
+
+  let list = content_tab_e(downloadsTab, "msgDownloadsRichListBox");
+  let firstElement = list.firstChild;
+  let secondElement = firstElement.nextSibling;
+  let removingFileNames = [];
+
+  removingFileNames.push(firstElement.getAttribute("displayName"));
+  removingFileNames.push(secondElement.getAttribute("displayName"));
+
+  // select two elements
+  mc.click(new elementslib.Elem(firstElement));
+  list.selectItemRange(firstElement, secondElement);
+  mc.rightClick(new elementslib.Elem(firstElement));
+
+  let contextMenu = content_tab_e(downloadsTab, "msgDownloadsContextMenu");
+  wait_for_popup_to_open(contextMenu);
+  mc.click_menus_in_sequence(contextMenu, [
+                               { command: "msgDownloadsCmd_remove" }
+                             ]);
+  mc.waitFor(() => downloadsView.count == 1,
+             "Timeout waiting for removing two saved attachment files.");
+
+  child = list.firstChild;
+  while (child) {
+    for (let [, name] in Iterator(removingFileNames)) {
+      assert_not_equals(name, child.getAttribute("displayName"));
+    }
+    child = child.nextSibling;
+  }
+}
+
+/**
+ * Test that 'clearDownloads" in context menu purges all downloaded files.
+ */
+function test_clear_all_files() {
+  test_save_attachment_files_in_list();
+  downloadsView.waitForFinish();
+
+  mc.click(content_tab_eid(downloadsTab, "msgDownloadsRichListBox"));
+  mc.rightClick(content_tab_eid(downloadsTab, "msgDownloadsRichListBox"));
+
+  let contextMenu = content_tab_e(downloadsTab, "msgDownloadsContextMenu");
+  wait_for_popup_to_open(contextMenu);
+  mc.click_menus_in_sequence(contextMenu, [
+                               { command: "msgDownloadsCmd_clearDownloads" }
+                             ]);
+  mc.waitFor(() => downloadsView.count == 0,
+             "Timeout waiting for clearing all saved attachment files.");
+
+  let empty = content_tab_e(downloadsTab, "msgDownloadsListEmptyDescription");
+  assert_false(empty.hidden, "msgDownloadsListEmptyDescription is not visible");
+}
+
+function teardownTest() {
+  downloads.Downloads.getList(downloads.Downloads.ALL)
+                     .then(function(list) {
+                       for (let download of downloadsView.items.keys()) {
+                         list.remove(download);
+                       }
+                     })
+                     .then(null, Cu.reportError);
+  mc.waitFor(() => downloadsView.count == 0,
+             "Timeout waiting for clearing all saved attachment files.");
+  let empty = content_tab_e(downloadsTab, "msgDownloadsListEmptyDescription");
+  mc.waitFor(() => empty.hidden == false,
+             "Timeout waiting for msgDownloadsListEmptyDescription is visible.");
+}
+
+function teardownModule(module) {
+  close_tab(downloadsTab);
+  ah.gMockFilePickReg.unregister();
+}
--- a/mail/test/mozmill/mozmilltests.list
+++ b/mail/test/mozmill/mozmilltests.list
@@ -2,16 +2,17 @@ account
 addrbook
 attachment
 cloudfile
 composition
 content-policy
 content-tabs
 cookies
 crypto
+downloads
 folder-display
 folder-pane
 folder-tree-modes
 folder-widget
 im
 instrumentation
 junk-commands
 keyboard
--- a/mailnews/base/src/nsMessenger.cpp
+++ b/mailnews/base/src/nsMessenger.cpp
@@ -1720,16 +1720,20 @@ nsresult nsSaveMsgListener::InitializeDo
     mimeService->GetFromTypeAndExtension(m_contentType, EmptyCString(), getter_AddRefs(mimeinfo));
     
     // create a download progress window
     // We don't want to show the progress dialog if the download is really small.
     // but what is a small download? Well that's kind of arbitrary
     // so make an arbitrary decision based on the content length of the
     // attachment -- show it if less than half of the download has completed
 
+    // Set saveToDisk explicitly to avoid launching the saved file.
+    // See http://hg.mozilla.org/mozilla-central/file/814a6f071472/toolkit/components/jsdownloads/src/DownloadLegacy.js#l164
+    mimeinfo->SetPreferredAction(nsIHandlerInfo::saveToDisk);
+
     // When we don't allow warnings, also don't show progress, as this
     //  is an environment (typically filters) where we don't want
     //  interruption.
     bool allowProgress = true;
     if (m_saveAllAttachmentsState)
       allowProgress = !m_saveAllAttachmentsState->m_withoutWarning;
     if (allowProgress && mMaxProgress != -1 &&
         mMaxProgress > aBytesDownloaded * 2)