summaryrefslogtreecommitdiffstats
path: root/comm/mail/test/browser/attachment
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mail/test/browser/attachment
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mail/test/browser/attachment')
-rw-r--r--comm/mail/test/browser/attachment/browser.ini18
-rw-r--r--comm/mail/test/browser/attachment/browser_attachment.js764
-rw-r--r--comm/mail/test/browser/attachment/browser_attachmentEvents.js494
-rw-r--r--comm/mail/test/browser/attachment/browser_attachmentIcon.js254
-rw-r--r--comm/mail/test/browser/attachment/browser_attachmentInPlainMsg.js51
-rw-r--r--comm/mail/test/browser/attachment/browser_attachmentMenus.js565
-rw-r--r--comm/mail/test/browser/attachment/browser_attachmentSize.js421
-rw-r--r--comm/mail/test/browser/attachment/browser_openAttachment.js738
-rw-r--r--comm/mail/test/browser/attachment/data/attachment.txt1
-rw-r--r--comm/mail/test/browser/attachment/data/bug1358565.eml62
10 files changed, 3368 insertions, 0 deletions
diff --git a/comm/mail/test/browser/attachment/browser.ini b/comm/mail/test/browser/attachment/browser.ini
new file mode 100644
index 0000000000..357098a517
--- /dev/null
+++ b/comm/mail/test/browser/attachment/browser.ini
@@ -0,0 +1,18 @@
+[DEFAULT]
+prefs =
+ mail.provider.suppress_dialog_on_startup=true
+ mail.spotlight.firstRunDone=true
+ mail.winsearch.firstRunDone=true
+ mailnews.start_page.override_url=about:blank
+ mailnews.start_page.url=about:blank
+ datareporting.policy.dataSubmissionPolicyBypassNotification=true
+subsuite = thunderbird
+support-files = data/**
+
+[browser_attachment.js]
+[browser_attachmentEvents.js]
+[browser_attachmentInPlainMsg.js]
+[browser_attachmentMenus.js]
+[browser_attachmentSize.js]
+[browser_attachmentIcon.js]
+[browser_openAttachment.js]
diff --git a/comm/mail/test/browser/attachment/browser_attachment.js b/comm/mail/test/browser/attachment/browser_attachment.js
new file mode 100644
index 0000000000..c24a15eff2
--- /dev/null
+++ b/comm/mail/test/browser/attachment/browser_attachment.js
@@ -0,0 +1,764 @@
+/* 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/. */
+
+/**
+ * Checks various attachments display correctly
+ */
+
+"use strict";
+
+var { close_compose_window, open_compose_with_forward } = ChromeUtils.import(
+ "resource://testing-common/mozmill/ComposeHelpers.jsm"
+);
+var {
+ add_message_to_folder,
+ assert_attachment_list_focused,
+ assert_message_pane_focused,
+ assert_selected_and_displayed,
+ be_in_folder,
+ close_popup,
+ create_folder,
+ create_message,
+ get_about_message,
+ mc,
+ msgGen,
+ plan_to_wait_for_folder_events,
+ select_click_row,
+ select_none,
+ wait_for_folder_events,
+ wait_for_message_display_completion,
+ wait_for_popup_to_open,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { SyntheticPartLeaf, SyntheticPartMultiMixed } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+
+var {
+ async_plan_for_new_window,
+ close_window,
+ plan_for_modal_dialog,
+ wait_for_modal_dialog,
+} = ChromeUtils.import("resource://testing-common/mozmill/WindowHelpers.jsm");
+
+var folder;
+var messages;
+
+var textAttachment =
+ "One of these days... people like me will rise up and overthrow you, and " +
+ "the end of tyranny by the homeostatic machine will have arrived. The day " +
+ "of human values and compassion and simple warmth will return, and when " +
+ "that happens someone like myself who has gone through an ordeal and who " +
+ "genuinely needs hot coffee to pick him up and keep him functioning when " +
+ "he has to function will get the hot coffee whether he happens to have a " +
+ "poscred readily available or not.";
+
+var binaryAttachment = textAttachment;
+
+add_setup(async function () {
+ folder = await create_folder("AttachmentA");
+
+ var attachedMessage = msgGen.makeMessage({
+ body: { body: "I'm an attached email!" },
+ attachments: [
+ { body: textAttachment, filename: "inner attachment.txt", format: "" },
+ ],
+ });
+
+ // create some messages that have various types of attachments
+ messages = [
+ // no attachment
+ {},
+ // text attachment
+ {
+ attachments: [{ body: textAttachment, filename: "ubik.txt", format: "" }],
+ },
+ // binary attachment; filename has 9 "1"s, which should be just within the
+ // limit for showing the original name
+ {
+ attachments: [
+ {
+ body: binaryAttachment,
+ contentType: "application/octet-stream",
+ filename: "ubik-111111111.xxyyzz",
+ format: "",
+ },
+ ],
+ },
+ // multiple attachments
+ {
+ attachments: [
+ { body: textAttachment, filename: "ubik.txt", format: "" },
+ {
+ body: binaryAttachment,
+ contentType: "application/octet-stream",
+ filename: "ubik.xxyyzz",
+ format: "",
+ },
+ ],
+ },
+ // attachment with a long name; the attachment bar should crop this
+ {
+ attachments: [
+ {
+ body: textAttachment,
+ filename:
+ "this-is-a-file-with-an-extremely-long-name-" +
+ "that-seems-to-go-on-forever-seriously-you-" +
+ "would-not-believe-how-long-this-name-is-it-" +
+ "surely-exceeds-the-maximum-filename-length-" +
+ "for-most-filesystems.txt",
+ format: "",
+ },
+ ],
+ },
+ // a message with a text attachment and an email attachment, which in turn
+ // has its own text attachment
+ {
+ bodyPart: new SyntheticPartMultiMixed([
+ new SyntheticPartLeaf("I'm a message!"),
+ new SyntheticPartLeaf(textAttachment, {
+ filename: "outer attachment.txt",
+ contentType: "text/plain",
+ format: "",
+ }),
+ attachedMessage,
+ ]),
+ },
+ // evilly-named attachment; spaces should be collapsed and trimmed on the
+ // ends
+ {
+ attachments: [
+ {
+ body: textAttachment,
+ contentType: "application/octet-stream",
+ filename: " ubik .txt .evil ",
+ sanitizedFilename: "ubik .txt .evil",
+ format: "",
+ },
+ ],
+ },
+ // another evilly-named attachment; filename has 10 "_"s, which should be
+ // just enough to trigger the sanitizer
+ {
+ attachments: [
+ {
+ body: textAttachment,
+ contentType: "application/octet-stream",
+ filename: "ubik.txt__________.evil",
+ sanitizedFilename: "ubik.txt_…_.evil",
+ format: "",
+ },
+ ],
+ },
+ // No texdir change in the filename please.
+ {
+ attachments: [
+ {
+ body: textAttachment,
+ filename: "ABC\u202EE.txt.zip",
+ sanitizedFilename: "ABC.E.txt.zip",
+ },
+ ],
+ },
+ ];
+
+ // Add another evilly-named attachment for Windows tests, to ensure that
+ // trailing periods are stripped.
+ if ("@mozilla.org/windows-registry-key;1" in Cc) {
+ messages.push({
+ attachments: [
+ {
+ body: textAttachment,
+ contentType: "application/octet-stream",
+ filename: "ubik.evil. . . . . . . . . ....",
+ sanitizedFilename: "ubik.evil",
+ format: "",
+ },
+ ],
+ });
+ }
+
+ for (let i = 0; i < messages.length; i++) {
+ await add_message_to_folder([folder], create_message(messages[i]));
+ }
+});
+
+/**
+ * Set the pref to ensure that the attachments pane starts out (un)expanded
+ *
+ * @param expand true if the attachment pane should start out expanded,
+ * false otherwise
+ */
+function ensure_starts_expanded(expand) {
+ Services.prefs.setBoolPref(
+ "mailnews.attachments.display.start_expanded",
+ expand
+ );
+}
+
+add_task(async function test_attachment_view_collapsed() {
+ await be_in_folder(folder);
+
+ select_click_row(0);
+ assert_selected_and_displayed(0);
+
+ if (
+ !get_about_message().document.getElementById("attachmentView").collapsed
+ ) {
+ throw new Error("Attachment pane expanded when it shouldn't be!");
+ }
+});
+
+add_task(async function test_attachment_view_expanded() {
+ await be_in_folder(folder);
+
+ for (let i = 1; i < messages.length; i++) {
+ select_click_row(i);
+ assert_selected_and_displayed(i);
+
+ if (
+ get_about_message().document.getElementById("attachmentView").collapsed
+ ) {
+ throw new Error(
+ "Attachment pane collapsed (on message #" + i + " when it shouldn't be!"
+ );
+ }
+ }
+});
+
+add_task(async function test_attachment_name_sanitization() {
+ await be_in_folder(folder);
+
+ let aboutMessage = get_about_message();
+ let attachmentList = aboutMessage.document.getElementById("attachmentList");
+
+ for (let i = 0; i < messages.length; i++) {
+ if ("attachments" in messages[i]) {
+ select_click_row(i);
+ assert_selected_and_displayed(i);
+
+ let attachments = messages[i].attachments;
+ if (messages[i].attachments.length == 1) {
+ Assert.equal(
+ aboutMessage.document.getElementById("attachmentName").value,
+ attachments[0].sanitizedFilename || attachments[0].filename
+ );
+ }
+
+ for (let j = 0; j < attachments.length; j++) {
+ Assert.equal(
+ attachmentList.getItemAtIndex(j).getAttribute("name"),
+ attachments[j].sanitizedFilename || attachments[j].filename
+ );
+ }
+ }
+ }
+});
+
+add_task(async function test_long_attachment_name() {
+ await be_in_folder(folder);
+
+ select_click_row(4);
+ assert_selected_and_displayed(4);
+
+ let aboutMessage = get_about_message();
+ let messagepaneBox = aboutMessage.document.getElementById("messagepanebox");
+ let attachmentBar = aboutMessage.document.getElementById("attachmentBar");
+
+ Assert.ok(
+ messagepaneBox.getBoundingClientRect().width >=
+ attachmentBar.getBoundingClientRect().width,
+ "Attachment bar has expanded off the edge of the window!"
+ );
+});
+
+/**
+ * Make sure that, when opening attached messages, we only show the attachments
+ * "beneath" the attached message (as opposed to all attachments for the root
+ * message).
+ */
+add_task(async function test_attached_message_attachments() {
+ await be_in_folder(folder);
+
+ select_click_row(5);
+ assert_selected_and_displayed(5);
+
+ // Make sure we have the expected number of attachments in the root message:
+ // an outer text attachment, an attached email, and an inner text attachment.
+ let aboutMessage = get_about_message();
+ Assert.equal(
+ aboutMessage.document.getElementById("attachmentList").itemCount,
+ 3
+ );
+
+ // Open the attached email.
+ let newWindowPromise = async_plan_for_new_window("mail:messageWindow");
+ aboutMessage.document
+ .getElementById("attachmentList")
+ .getItemAtIndex(1)
+ .attachment.open();
+ let msgc = await newWindowPromise;
+ wait_for_message_display_completion(msgc, true);
+
+ // Make sure we have the expected number of attachments in the attached
+ // message: just an inner text attachment.
+ Assert.equal(
+ msgc.window.document.getElementById("attachmentList").itemCount,
+ 1
+ );
+
+ close_window(msgc);
+}).skip();
+
+add_task(async function test_attachment_name_click() {
+ await be_in_folder(folder);
+
+ select_click_row(1);
+ assert_selected_and_displayed(1);
+
+ let aboutMessage = get_about_message();
+ let attachmentList = aboutMessage.document.getElementById("attachmentList");
+
+ Assert.ok(
+ attachmentList.collapsed,
+ "Attachment list should start out collapsed!"
+ );
+
+ // Ensure the open dialog appears when clicking on the attachment name and
+ // that the attachment list doesn't expand.
+ plan_for_modal_dialog("unknownContentTypeWindow", function () {});
+ EventUtils.synthesizeMouseAtCenter(
+ aboutMessage.document.getElementById("attachmentName"),
+ { clickCount: 1 },
+ aboutMessage
+ );
+ wait_for_modal_dialog("unknownContentTypeWindow");
+ Assert.ok(
+ attachmentList.collapsed,
+ "Attachment list should not expand when clicking on attachmentName!"
+ );
+});
+
+/**
+ * Test that right-clicking on a particular element opens the expected context
+ * menu.
+ *
+ * @param elementId the id of the element to right click on
+ * @param contextMenuId the id of the context menu that should appear
+ */
+async function subtest_attachment_right_click(elementId, contextMenuId) {
+ let aboutMessage = get_about_message();
+ let element = aboutMessage.document.getElementById(elementId);
+ let contextMenu = aboutMessage.document.getElementById(contextMenuId);
+
+ let shownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ element,
+ { type: "contextmenu" },
+ aboutMessage
+ );
+ await shownPromise;
+ let hiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.hidePopup();
+ await hiddenPromise;
+ await new Promise(resolve => requestAnimationFrame(resolve));
+}
+
+add_task(async function test_attachment_right_click_single() {
+ await be_in_folder(folder);
+
+ select_click_row(1);
+ assert_selected_and_displayed(1);
+
+ await subtest_attachment_right_click(
+ "attachmentIcon",
+ "attachmentItemContext"
+ );
+ await subtest_attachment_right_click(
+ "attachmentCount",
+ "attachmentItemContext"
+ );
+ await subtest_attachment_right_click(
+ "attachmentName",
+ "attachmentItemContext"
+ );
+ await subtest_attachment_right_click(
+ "attachmentSize",
+ "attachmentItemContext"
+ );
+
+ await subtest_attachment_right_click(
+ "attachmentToggle",
+ "attachment-toolbar-context-menu"
+ );
+ await subtest_attachment_right_click(
+ "attachmentSaveAllSingle",
+ "attachment-toolbar-context-menu"
+ );
+ await subtest_attachment_right_click(
+ "attachmentBar",
+ "attachment-toolbar-context-menu"
+ );
+});
+
+add_task(async function test_attachment_right_click_multiple() {
+ await be_in_folder(folder);
+
+ select_click_row(3);
+ assert_selected_and_displayed(3);
+
+ await subtest_attachment_right_click(
+ "attachmentIcon",
+ "attachmentListContext"
+ );
+ await subtest_attachment_right_click(
+ "attachmentCount",
+ "attachmentListContext"
+ );
+ await subtest_attachment_right_click(
+ "attachmentSize",
+ "attachmentListContext"
+ );
+
+ await subtest_attachment_right_click(
+ "attachmentToggle",
+ "attachment-toolbar-context-menu"
+ );
+ await subtest_attachment_right_click(
+ "attachmentSaveAllMultiple",
+ "attachment-toolbar-context-menu"
+ );
+ await subtest_attachment_right_click(
+ "attachmentBar",
+ "attachment-toolbar-context-menu"
+ );
+});
+
+/**
+ * Test that clicking on various elements in the attachment bar toggles the
+ * attachment list.
+ *
+ * @param elementId the id of the element to click
+ */
+function subtest_attachment_list_toggle(elementId) {
+ let aboutMessage = get_about_message();
+ let attachmentList = aboutMessage.document.getElementById("attachmentList");
+ let element = aboutMessage.document.getElementById(elementId);
+
+ EventUtils.synthesizeMouseAtCenter(element, { clickCount: 1 }, aboutMessage);
+ Assert.ok(
+ !attachmentList.collapsed,
+ `Attachment list should be expanded after clicking ${elementId}!`
+ );
+ assert_attachment_list_focused();
+
+ EventUtils.synthesizeMouseAtCenter(element, { clickCount: 1 }, aboutMessage);
+ Assert.ok(
+ attachmentList.collapsed,
+ `Attachment list should be collapsed after clicking ${elementId} again!`
+ );
+ assert_message_pane_focused();
+}
+
+add_task(async function test_attachment_list_expansion() {
+ await be_in_folder(folder);
+
+ select_click_row(1);
+ assert_selected_and_displayed(1);
+
+ let aboutMessage = get_about_message();
+ Assert.ok(
+ aboutMessage.document.getElementById("attachmentList").collapsed,
+ "Attachment list should start out collapsed!"
+ );
+
+ subtest_attachment_list_toggle("attachmentToggle");
+ subtest_attachment_list_toggle("attachmentIcon");
+ subtest_attachment_list_toggle("attachmentCount");
+ subtest_attachment_list_toggle("attachmentSize");
+ subtest_attachment_list_toggle("attachmentBar");
+
+ // Ensure that clicking the "Save All" button doesn't expand the attachment
+ // list.
+ let dm = aboutMessage.document.querySelector(
+ "#attachmentSaveAllSingle .toolbarbutton-menubutton-dropmarker"
+ );
+ EventUtils.synthesizeMouseAtCenter(dm, { clickCount: 1 }, aboutMessage);
+ Assert.ok(
+ aboutMessage.document.getElementById("attachmentList").collapsed,
+ "Attachment list should be collapsed after clicking save button!"
+ );
+}).skip();
+
+add_task(async function test_attachment_list_starts_expanded() {
+ ensure_starts_expanded(true);
+ await be_in_folder(folder);
+
+ select_click_row(2);
+ assert_selected_and_displayed(2);
+
+ Assert.ok(
+ !get_about_message().document.getElementById("attachmentList").collapsed,
+ "Attachment list should start out expanded!"
+ );
+});
+
+add_task(async function test_selected_attachments_are_cleared() {
+ ensure_starts_expanded(false);
+ await be_in_folder(folder);
+ // First, select the message with two attachments.
+ select_click_row(3);
+
+ // Expand the attachment list.
+ let aboutMessage = get_about_message();
+ EventUtils.synthesizeMouseAtCenter(
+ aboutMessage.document.getElementById("attachmentToggle"),
+ { clickCount: 1 },
+ aboutMessage
+ );
+
+ // Select both the attachments.
+ let attachmentList = aboutMessage.document.getElementById("attachmentList");
+ Assert.equal(
+ attachmentList.selectedItems.length,
+ 1,
+ "On first load the first item should be selected"
+ );
+
+ // We can just click on the first element, but the second one needs a
+ // ctrl-click (or cmd-click for those Mac-heads among us).
+ EventUtils.synthesizeMouseAtCenter(
+ attachmentList.children[0],
+ { clickCount: 1 },
+ aboutMessage
+ );
+ EventUtils.synthesizeMouse(
+ attachmentList.children[1],
+ 5,
+ 5,
+ { accelKey: true },
+ aboutMessage
+ );
+
+ Assert.equal(
+ attachmentList.selectedItems.length,
+ 2,
+ "We had the wrong number of selected items after selecting some!"
+ );
+
+ // Switch to the message with one attachment, and make sure there are no
+ // selected attachments.
+ select_click_row(2);
+
+ // Expand the attachment list again.
+ EventUtils.synthesizeMouseAtCenter(
+ aboutMessage.document.getElementById("attachmentToggle"),
+ { clickCount: 1 },
+ aboutMessage
+ );
+
+ Assert.equal(
+ attachmentList.selectedItems.length,
+ 1,
+ "After loading a new message the first item should be selected"
+ );
+});
+
+add_task(async function test_select_all_attachments_key() {
+ await be_in_folder(folder);
+
+ // First, select the message with two attachments.
+ select_none();
+ select_click_row(3);
+
+ // Expand the attachment list.
+ let aboutMessage = get_about_message();
+ EventUtils.synthesizeMouseAtCenter(
+ aboutMessage.document.getElementById("attachmentToggle"),
+ { clickCount: 1 },
+ aboutMessage
+ );
+
+ let attachmentList = aboutMessage.document.getElementById("attachmentList");
+ attachmentList.focus();
+ EventUtils.synthesizeKey("a", { accelKey: true }, aboutMessage);
+ Assert.equal(
+ attachmentList.selectedItems.length,
+ 2,
+ "Should have selected all attachments!"
+ );
+});
+
+add_task(async function test_delete_attachment_key() {
+ await be_in_folder(folder);
+
+ // First, select the message with two attachments.
+ select_none();
+ select_click_row(3);
+
+ // Expand the attachment list.
+ assert_selected_and_displayed(3);
+ let aboutMessage = get_about_message();
+ if (aboutMessage.document.getElementById("attachmentList").collapsed) {
+ EventUtils.synthesizeMouseAtCenter(
+ aboutMessage.document.getElementById("attachmentToggle"),
+ { clickCount: 1 },
+ aboutMessage
+ );
+ }
+ let firstAttachment =
+ aboutMessage.document.getElementById("attachmentList").firstElementChild;
+ EventUtils.synthesizeMouseAtCenter(
+ firstAttachment,
+ { clickCount: 1 },
+ aboutMessage
+ );
+
+ // Try deleting with the delete key
+ let dialogPromise = BrowserTestUtils.promiseAlertDialog("cancel");
+ firstAttachment.focus();
+ EventUtils.synthesizeKey("VK_DELETE", {}, aboutMessage);
+ await dialogPromise;
+
+ // Try deleting with the shift-delete key combo.
+ dialogPromise = BrowserTestUtils.promiseAlertDialog("cancel");
+ firstAttachment.focus();
+ EventUtils.synthesizeKey("VK_DELETE", { shiftKey: true }, aboutMessage);
+ await dialogPromise;
+}).skip();
+
+add_task(async function test_attachments_compose_menu() {
+ await be_in_folder(folder);
+
+ // First, select the message with two attachments.
+ select_none();
+ select_click_row(3);
+
+ let cwc = open_compose_with_forward();
+ let attachment = cwc.window.document.getElementById("attachmentBucket");
+
+ // On Linux and OSX, focus events don't seem to be sent to child elements properly if
+ // the parent window is not focused. This causes some random oranges for us.
+ // We use the force_focus function to "cheat" a bit, and trigger the function
+ // that focusing normally would fire. We do normal focusing for Windows.
+ function force_focus(aId) {
+ let element = cwc.window.document.getElementById(aId);
+ element.focus();
+
+ if (["linux", "macosx"].includes(AppConstants.platform)) {
+ // First, call the window's default controller's function.
+ cwc.window.defaultController.isCommandEnabled("cmd_delete");
+
+ // Walk up the DOM tree and call isCommandEnabled on the first controller
+ // that supports "cmd_delete".
+ while (element != cwc.window.document) {
+ // NOTE: html elements (like body) don't have controllers.
+ let numControllers = element.controllers?.getControllerCount() || 0;
+ for (let i = 0; numControllers; i++) {
+ let currController = element.controllers.getControllerAt(i);
+ if (currController.supportsCommand("cmd_delete")) {
+ currController.isCommandEnabled("cmd_delete");
+ return;
+ }
+ }
+ element = element.parentNode;
+ }
+ }
+ }
+
+ // Click on a portion of the attachmentBucket to focus on it. The last
+ // attachment should be selected since we don't handle any action on an empty
+ // bucket, and we always ensure that the last attached file is visible.
+ force_focus("attachmentBucket");
+
+ Assert.equal(
+ "Remove Attachment",
+ cwc.window.document.getElementById("cmd_delete").getAttribute("label"),
+ "attachmentBucket with last attachment is focused!"
+ );
+
+ // We opened a message with 2 attachments, so index 1 should be focused.
+ Assert.equal(attachment.selectedIndex, 1, "Last attachment is focused!");
+
+ // Select 1 attachment, and
+ // focus the subject to see the label change and to execute isCommandEnabled
+ attachment.selectedIndex = 0;
+ force_focus("msgSubject");
+ Assert.equal(
+ "Delete",
+ cwc.window.document.getElementById("cmd_delete").getAttribute("label"),
+ "attachmentBucket is not focused!"
+ );
+
+ // Focus back to the attachmentBucket
+ force_focus("attachmentBucket");
+ Assert.equal(
+ "Remove Attachment",
+ cwc.window.document.getElementById("cmd_delete").getAttribute("label"),
+ "Only 1 attachment is selected!"
+ );
+
+ // Select multiple attachments, and focus the identity for the same purpose
+ attachment.selectAll();
+ force_focus("msgIdentity");
+ Assert.equal(
+ "Delete",
+ cwc.window.document.getElementById("cmd_delete").getAttribute("label"),
+ "attachmentBucket is not focused!"
+ );
+
+ // Focus back to the attachmentBucket
+ force_focus("attachmentBucket");
+ Assert.equal(
+ "Remove Attachments",
+ cwc.window.document.getElementById("cmd_delete").getAttribute("label"),
+ "Multiple attachments are selected!"
+ );
+
+ close_compose_window(cwc);
+});
+
+add_task(async function test_delete_from_toolbar() {
+ await be_in_folder(folder);
+
+ // First, select the message with two attachments.
+ select_none();
+ select_click_row(3);
+
+ // Expand the attachment list.
+ assert_selected_and_displayed(3);
+ let aboutMessage = get_about_message();
+ if (aboutMessage.document.getElementById("attachmentList").collapsed) {
+ EventUtils.synthesizeMouseAtCenter(
+ aboutMessage.document.getElementById("attachmentToggle"),
+ { clickCount: 1 },
+ aboutMessage
+ );
+ }
+
+ let firstAttachment =
+ aboutMessage.document.getElementById("attachmentList").firstElementChild;
+ EventUtils.synthesizeMouseAtCenter(
+ firstAttachment,
+ { clickCount: 1 },
+ aboutMessage
+ );
+
+ // Make sure clicking the "Delete" toolbar button with an attachment focused
+ // deletes the *message*.
+ plan_to_wait_for_folder_events("DeleteOrMoveMsgCompleted");
+ EventUtils.synthesizeMouseAtCenter(
+ aboutMessage.document.getElementById("hdrTrashButton"),
+ { clickCount: 1 },
+ aboutMessage
+ );
+ wait_for_folder_events();
+}).skip();
+
+registerCleanupFunction(() => {
+ // Remove created folders.
+ folder.deleteSelf(null);
+});
diff --git a/comm/mail/test/browser/attachment/browser_attachmentEvents.js b/comm/mail/test/browser/attachment/browser_attachmentEvents.js
new file mode 100644
index 0000000000..beb7034a86
--- /dev/null
+++ b/comm/mail/test/browser/attachment/browser_attachmentEvents.js
@@ -0,0 +1,494 @@
+/* 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/. */
+
+/**
+ * Ensures that attachment events are fired properly
+ */
+
+/* eslint-disable @microsoft/sdl/no-insecure-url */
+
+"use strict";
+
+var { select_attachments } = ChromeUtils.import(
+ "resource://testing-common/mozmill/AttachmentHelpers.jsm"
+);
+var { mc } = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var utils = ChromeUtils.import("resource://testing-common/mozmill/utils.jsm");
+var { add_attachments, close_compose_window, open_compose_new_mail } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { gMockPromptService } = ChromeUtils.import(
+ "resource://testing-common/mozmill/PromptHelpers.jsm"
+);
+
+var kAttachmentsAdded = "attachments-added";
+var kAttachmentsRemoved = "attachments-removed";
+var kAttachmentRenamed = "attachment-renamed";
+
+/**
+ * Test that the attachments-added event is fired when we add a single
+ * attachment.
+ */
+add_task(function test_attachments_added_on_single() {
+ // Prepare to listen for attachments-added
+ let eventCount = 0;
+ let lastEvent;
+ let listener = function (event) {
+ eventCount++;
+ lastEvent = event;
+ };
+
+ // Open up the compose window
+ let cw = open_compose_new_mail(mc);
+ cw.window.document
+ .getElementById("attachmentBucket")
+ .addEventListener(kAttachmentsAdded, listener);
+
+ // Attach a single file
+ add_attachments(cw, "http://www.example.com/1", 0, false);
+
+ // Make sure we only saw the event once
+ Assert.equal(1, eventCount);
+
+ // Make sure that we were passed the right subject
+ let subjects = lastEvent.detail;
+ Assert.equal(1, subjects.length);
+ Assert.equal("http://www.example.com/1", subjects[0].url);
+
+ // Make sure that we can get that event again if we
+ // attach more files.
+ add_attachments(cw, "http://www.example.com/2", 0, false);
+ Assert.equal(2, eventCount);
+ subjects = lastEvent.detail;
+ Assert.equal("http://www.example.com/2", subjects[0].url);
+
+ // And check that we don't receive the event if we try to attach a file
+ // that's already attached.
+ add_attachments(cw, "http://www.example.com/2", null, false);
+ Assert.equal(2, eventCount);
+
+ cw.window.document
+ .getElementById("attachmentBucket")
+ .removeEventListener(kAttachmentsAdded, listener);
+ close_compose_window(cw);
+});
+
+/**
+ * Test that the attachments-added event is fired when we add a series
+ * of files all at once.
+ */
+add_task(function test_attachments_added_on_multiple() {
+ // Prepare to listen for attachments-added
+ let eventCount = 0;
+ let lastEvent;
+ let listener = function (event) {
+ eventCount++;
+ lastEvent = event;
+ };
+
+ // Prepare the attachments - we store the names in attachmentNames to
+ // make sure that we observed the right event subjects later on.
+ let attachmentUrls = ["http://www.example.com/1", "http://www.example.com/2"];
+
+ // Open the compose window and add the attachments
+ let cw = open_compose_new_mail(mc);
+ cw.window.document
+ .getElementById("attachmentBucket")
+ .addEventListener(kAttachmentsAdded, listener);
+
+ add_attachments(cw, attachmentUrls, null, false);
+
+ // Make sure we only saw a single attachments-added for this group
+ // of files.
+ Assert.equal(1, eventCount);
+
+ // Now make sure we got passed the right subjects for the event
+ let subjects = lastEvent.detail;
+ Assert.equal(2, subjects.length);
+
+ for (let attachment of subjects) {
+ Assert.ok(attachmentUrls.includes(attachment.url));
+ }
+
+ // Close the compose window - let's try again with 3 attachments.
+ cw.window.document
+ .getElementById("attachmentBucket")
+ .removeEventListener(kAttachmentsAdded, listener);
+ close_compose_window(cw);
+
+ attachmentUrls = [
+ "http://www.example.com/1",
+ "http://www.example.com/2",
+ "http://www.example.com/3",
+ ];
+
+ // Open the compose window and attach the files, and ensure that we saw
+ // the attachments-added event
+ cw = open_compose_new_mail(mc);
+ cw.window.document
+ .getElementById("attachmentBucket")
+ .addEventListener(kAttachmentsAdded, listener);
+
+ add_attachments(cw, attachmentUrls, null, false);
+ Assert.equal(2, eventCount);
+
+ // Make sure that we got the right subjects back
+ subjects = lastEvent.detail;
+ Assert.equal(3, subjects.length);
+
+ for (let attachment of subjects) {
+ Assert.ok(attachmentUrls.includes(attachment.url));
+ }
+
+ // Make sure we don't fire the event again if we try to attach the same
+ // files.
+ add_attachments(cw, attachmentUrls, null, false);
+ Assert.equal(2, eventCount);
+
+ cw.window.document
+ .getElementById("attachmentBucket")
+ .removeEventListener(kAttachmentsAdded, listener);
+ close_compose_window(cw);
+});
+
+/**
+ * Test that the attachments-removed event is fired when removing a
+ * single file.
+ */
+add_task(function test_attachments_removed_on_single() {
+ // Prepare to listen for attachments-removed
+ let eventCount = 0;
+ let lastEvent;
+ let listener = function (event) {
+ eventCount++;
+ lastEvent = event;
+ };
+
+ // Open up the compose window, attach a file...
+ let cw = open_compose_new_mail(mc);
+ cw.window.document
+ .getElementById("attachmentBucket")
+ .addEventListener(kAttachmentsRemoved, listener);
+
+ add_attachments(cw, "http://www.example.com/1");
+
+ // Now select that attachment and delete it
+ select_attachments(cw, 0);
+ // We need to hold a reference to removedAttachment here because
+ // the delete routine nulls it out from the attachmentitem.
+ cw.window.goDoCommand("cmd_delete");
+ // Make sure we saw the event
+ Assert.equal(1, eventCount);
+ // And make sure we were passed the right attachment item as the
+ // subject.
+ let subjects = lastEvent.detail;
+ Assert.equal(1, subjects.length);
+ Assert.equal(subjects[0].url, "http://www.example.com/1");
+
+ // Ok, let's attach it again, and remove it again to ensure that
+ // we still see the event.
+ add_attachments(cw, "http://www.example.com/2");
+ select_attachments(cw, 0);
+ cw.window.goDoCommand("cmd_delete");
+
+ Assert.equal(2, eventCount);
+ subjects = lastEvent.detail;
+ Assert.equal(1, subjects.length);
+ Assert.equal(subjects[0].url, "http://www.example.com/2");
+
+ cw.window.document
+ .getElementById("attachmentBucket")
+ .removeEventListener(kAttachmentsRemoved, listener);
+ close_compose_window(cw);
+});
+
+/**
+ * Test that the attachments-removed event is fired when removing multiple
+ * files all at once.
+ */
+add_task(function test_attachments_removed_on_multiple() {
+ // Prepare to listen for attachments-removed
+ let eventCount = 0;
+ let lastEvent;
+ let listener = function (event) {
+ eventCount++;
+ lastEvent = event;
+ };
+
+ // Open up the compose window and attach some files...
+ let cw = open_compose_new_mail(mc);
+ cw.window.document
+ .getElementById("attachmentBucket")
+ .addEventListener(kAttachmentsRemoved, listener);
+
+ add_attachments(cw, [
+ "http://www.example.com/1",
+ "http://www.example.com/2",
+ "http://www.example.com/3",
+ ]);
+
+ // Select all three attachments, and remove them.
+ let removedAttachmentItems = select_attachments(cw, 0, 2);
+
+ let removedAttachmentUrls = removedAttachmentItems.map(
+ aAttachment => aAttachment.attachment.url
+ );
+
+ cw.window.goDoCommand("cmd_delete");
+
+ // We should have seen the attachments-removed event exactly once.
+ Assert.equal(1, eventCount);
+
+ // Now let's make sure we got passed back the right attachment items
+ // as the event subject
+ let subjects = lastEvent.detail;
+ Assert.equal(3, subjects.length);
+
+ for (let attachment of subjects) {
+ Assert.ok(removedAttachmentUrls.includes(attachment.url));
+ }
+
+ // Ok, let's attach and remove some again to ensure that we still see the event.
+ add_attachments(cw, ["http://www.example.com/1", "http://www.example.com/2"]);
+
+ select_attachments(cw, 0, 1);
+ cw.window.goDoCommand("cmd_delete");
+ Assert.equal(2, eventCount);
+
+ cw.window.document
+ .getElementById("attachmentBucket")
+ .removeEventListener(kAttachmentsRemoved, listener);
+ close_compose_window(cw);
+});
+
+/**
+ * Test that we don't see the attachments-removed event if no attachments
+ * are selected when hitting "Delete"
+ */
+add_task(function test_no_attachments_removed_on_none() {
+ // Prepare to listen for attachments-removed
+ let eventCount = 0;
+ let listener = function (event) {
+ eventCount++;
+ };
+
+ // Open the compose window and add some attachments.
+ let cw = open_compose_new_mail(mc);
+ cw.window.document
+ .getElementById("attachmentBucket")
+ .addEventListener(kAttachmentsRemoved, listener);
+
+ add_attachments(cw, [
+ "http://www.example.com/1",
+ "http://www.example.com/2",
+ "http://www.example.com/3",
+ ]);
+
+ // Choose no attachments
+ cw.window.document.getElementById("attachmentBucket").clearSelection();
+ // Run the delete command
+ cw.window.goDoCommand("cmd_delete");
+ // Make sure we didn't see the attachments_removed event.
+ Assert.equal(0, eventCount);
+ cw.window.document
+ .getElementById("attachmentBucket")
+ .removeEventListener(kAttachmentsRemoved, listener);
+
+ close_compose_window(cw);
+});
+
+/**
+ * Test that we see the attachment-renamed event when an attachments
+ * name is changed.
+ */
+add_task(function test_attachment_renamed() {
+ // Here's what we'll rename some files to.
+ const kRenameTo1 = "Renamed-1";
+ const kRenameTo2 = "Renamed-2";
+ const kRenameTo3 = "Renamed-3";
+
+ // Prepare to listen for attachment-renamed
+ let eventCount = 0;
+ let lastEvent;
+ let listener = function (event) {
+ eventCount++;
+ lastEvent = event;
+ };
+
+ // Renaming a file brings up a Prompt, so we'll mock the Prompt Service
+ gMockPromptService.reset();
+ gMockPromptService.register();
+ // The inoutValue is used to set the attachment name
+ gMockPromptService.inoutValue = kRenameTo1;
+ gMockPromptService.returnValue = true;
+
+ // Open up the compose window, attach some files, choose the first
+ // attachment, and choose to rename it.
+ let cw = open_compose_new_mail(mc);
+ cw.window.document
+ .getElementById("attachmentBucket")
+ .addEventListener(kAttachmentRenamed, listener);
+
+ add_attachments(cw, [
+ "http://www.example.com/1",
+ "http://www.example.com/2",
+ "http://www.example.com/3",
+ ]);
+
+ select_attachments(cw, 0);
+ Assert.equal(0, eventCount);
+ cw.window.goDoCommand("cmd_renameAttachment");
+
+ // Wait until we saw the attachment-renamed event.
+ utils.waitFor(function () {
+ return eventCount == 1;
+ });
+
+ // Ensure that the event mentions the right attachment
+ let renamedAttachment1 = lastEvent.target.attachment;
+ let originalAttachment1 = lastEvent.detail;
+ Assert.ok(renamedAttachment1 instanceof Ci.nsIMsgAttachment);
+ Assert.equal(kRenameTo1, renamedAttachment1.name);
+ Assert.ok(renamedAttachment1.url.includes("http://www.example.com/1"));
+ Assert.equal("www.example.com/1", originalAttachment1.name);
+
+ // Ok, let's try renaming the same attachment.
+ gMockPromptService.reset();
+ gMockPromptService.inoutValue = kRenameTo2;
+ gMockPromptService.returnValue = true;
+
+ select_attachments(cw, 0);
+ Assert.equal(1, eventCount);
+ cw.window.goDoCommand("cmd_renameAttachment");
+
+ // Wait until we saw the attachment-renamed event.
+ utils.waitFor(function () {
+ return eventCount == 2;
+ });
+
+ let renamedAttachment2 = lastEvent.target.attachment;
+ let originalAttachment2 = lastEvent.detail;
+ Assert.ok(renamedAttachment2 instanceof Ci.nsIMsgAttachment);
+ Assert.equal(kRenameTo2, renamedAttachment2.name);
+ Assert.ok(renamedAttachment2.url.includes("http://www.example.com/1"));
+ Assert.equal(kRenameTo1, originalAttachment2.name);
+
+ // Ok, let's rename another attachment
+ gMockPromptService.reset();
+ gMockPromptService.inoutValue = kRenameTo3;
+ gMockPromptService.returnValue = true;
+
+ // We'll select the second attachment this time.
+ select_attachments(cw, 1);
+ Assert.equal(2, eventCount);
+ cw.window.goDoCommand("cmd_renameAttachment");
+
+ // Wait until we saw the attachment-renamed event.
+ utils.waitFor(function () {
+ return eventCount == 3;
+ });
+
+ // Ensure that the event mentions the right attachment
+ let renamedAttachment3 = lastEvent.target.attachment;
+ let originalAttachment3 = lastEvent.detail;
+ Assert.ok(renamedAttachment3 instanceof Ci.nsIMsgAttachment);
+ Assert.equal(kRenameTo3, renamedAttachment3.name);
+ Assert.ok(renamedAttachment3.url.includes("http://www.example.com/2"));
+ Assert.equal("www.example.com/2", originalAttachment3.name);
+
+ // Unregister the Mock Prompt service, and remove our observer.
+ cw.window.document
+ .getElementById("attachmentBucket")
+ .removeEventListener(kAttachmentRenamed, listener);
+
+ close_compose_window(cw);
+ gMockPromptService.unregister();
+});
+
+/**
+ * Test that the attachment-renamed event is not fired if we set the
+ * filename to be blank.
+ */
+add_task(function test_no_attachment_renamed_on_blank() {
+ // Prepare to listen for attachment-renamed
+ let eventCount = 0;
+ let listener = function (event) {
+ eventCount++;
+ };
+
+ // Register the Mock Prompt Service to return the empty string when
+ // prompted.
+ gMockPromptService.reset();
+ gMockPromptService.register();
+ gMockPromptService.inoutValue = "";
+ gMockPromptService.returnValue = true;
+
+ // Open the compose window, attach some files, select one, and chooes to
+ // rename it.
+ let cw = open_compose_new_mail(mc);
+ cw.window.document
+ .getElementById("attachmentBucket")
+ .addEventListener(kAttachmentRenamed, listener);
+
+ add_attachments(cw, [
+ "http://www.example.com/1",
+ "http://www.example.com/2",
+ "http://www.example.com/3",
+ ]);
+
+ select_attachments(cw, 0);
+ cw.window.goDoCommand("cmd_renameAttachment");
+
+ // Ensure that we didn't see the attachment-renamed event.
+ Assert.equal(0, eventCount);
+ cw.window.document
+ .getElementById("attachmentBucket")
+ .removeEventListener(kAttachmentRenamed, listener);
+ close_compose_window(cw);
+ gMockPromptService.unregister();
+});
+
+/**
+ * Test that toggling attachments pane works.
+ */
+add_task(function test_attachments_pane_toggle() {
+ // Open the compose window.
+ let cw = open_compose_new_mail(mc);
+
+ // Use the hotkey to try to toggle attachmentsArea open.
+ let opts =
+ AppConstants.platform == "macosx"
+ ? { metaKey: true, shiftKey: true }
+ : { ctrlKey: true, shiftKey: true };
+ EventUtils.synthesizeKey("m", opts, cw.window);
+ let attachmentArea = cw.window.document.getElementById("attachmentArea");
+
+ // Since we don't have any uploaded attachment, assert that the box remains
+ // closed.
+ utils.waitFor(() => !attachmentArea.open);
+ Assert.ok(!attachmentArea.open);
+
+ // Add an attachment. This should automatically open the box.
+ add_attachments(cw, ["http://www.example.com/1"]);
+ Assert.ok(attachmentArea.open);
+
+ // Press again, should toggle to closed.
+ EventUtils.synthesizeKey("m", opts, cw.window);
+ utils.waitFor(() => !attachmentArea.open);
+ Assert.ok(!attachmentArea.open);
+
+ // Press again, should toggle to open.
+ EventUtils.synthesizeKey("m", opts, cw.window);
+ utils.waitFor(() => attachmentArea.open);
+ Assert.ok(attachmentArea.open);
+
+ close_compose_window(cw);
+});
+
+registerCleanupFunction(() => {
+ // Some tests that open new windows don't return focus to the main window
+ // in a way that satisfies mochitest, and the test times out.
+ Services.focus.focusedWindow = window;
+});
diff --git a/comm/mail/test/browser/attachment/browser_attachmentIcon.js b/comm/mail/test/browser/attachment/browser_attachmentIcon.js
new file mode 100644
index 0000000000..ec39497a3b
--- /dev/null
+++ b/comm/mail/test/browser/attachment/browser_attachmentIcon.js
@@ -0,0 +1,254 @@
+/* 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";
+
+var folder;
+var messenger;
+
+var {
+ create_body_part,
+ create_deleted_attachment,
+ create_detached_attachment,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/AttachmentHelpers.jsm"
+);
+var {
+ add_message_to_folder,
+ be_in_folder,
+ create_folder,
+ create_message,
+ get_about_message,
+ mc,
+ msgGen,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+var { SyntheticPartLeaf, SyntheticPartMultiMixed } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+
+var textAttachment =
+ "Can't make the frug contest, Helen; stomach's upset. I'll fix you, " +
+ "Ubik! Ubik drops you back in the thick of things fast. Taken as " +
+ "directed, Ubik speeds relief to head and stomach. Remember: Ubik is " +
+ "only seconds away. Avoid prolonged use.";
+
+var binaryAttachment = textAttachment;
+
+var imageAttachment =
+ "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwS" +
+ "FlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA" +
+ "A5SURBVCiRY/z//z8DKYCJJNXkaGBgYGD4D8NQ5zUgiTVAxeBqSLaBkVRPM0KtIhrQ3km0jwe" +
+ "SNQAAlmAY+71EgFoAAAAASUVORK5CYII=";
+var imageSize = 188;
+
+var vcardAttachment =
+ "YmVnaW46dmNhcmQNCmZuOkppbSBCb2INCm46Qm9iO0ppbQ0KZW1haWw7aW50ZXJuZXQ6Zm9v" +
+ "QGJhci5jb20NCnZlcnNpb246Mi4xDQplbmQ6dmNhcmQNCg0K";
+var vcardSize = 90;
+
+var detachedName = "./attachment.txt";
+var missingName = "./nonexistent.txt";
+var deletedName = "deleted.txt";
+
+// Create some messages that have various types of attachments.
+var messages = [
+ {
+ name: "text_attachment",
+ attachments: [
+ {
+ body: textAttachment,
+ filename: "ubik.txt",
+ format: "",
+ icon: "moz-icon://ubik.txt?size=16&contentType=text/plain",
+ },
+ ],
+ },
+ {
+ name: "binary_attachment",
+ attachments: [
+ {
+ body: binaryAttachment,
+ contentType: "application/x-ubik",
+ filename: "ubik",
+ format: "",
+ icon: "moz-icon://ubik?size=16&contentType=application/x-ubik",
+ },
+ ],
+ },
+ {
+ name: "image_attachment",
+ attachments: [
+ {
+ body: imageAttachment,
+ contentType: "image/png",
+ filename: "lines.png",
+ encoding: "base64",
+ format: "",
+ icon: "moz-icon://lines.png?size=16&contentType=image/png",
+ },
+ ],
+ },
+ {
+ name: "detached_attachment",
+ bodyPart: null,
+ attachments: [
+ {
+ icon: "moz-icon://attachment.txt?size=16&contentType=text/plain",
+ },
+ ],
+ },
+ {
+ name: "detached_attachment_with_missing_file",
+ bodyPart: null,
+ attachments: [
+ {
+ icon: "moz-icon://nonexistent.txt?size=16&contentType=text/plain",
+ },
+ ],
+ },
+ {
+ name: "deleted_attachment",
+ bodyPart: null,
+ attachments: [
+ {
+ icon: "chrome://messenger/skin/icons/attachment-deleted.svg",
+ },
+ ],
+ },
+ {
+ name: "multiple_attachments",
+ attachments: [
+ {
+ body: textAttachment,
+ filename: "ubik.txt",
+ format: "",
+ icon: "moz-icon://ubik.txt?size=16&contentType=text/plain",
+ },
+ {
+ body: binaryAttachment,
+ contentType: "application/x-ubik",
+ filename: "ubik",
+ format: "",
+ icon: "moz-icon://ubik?size=16&contentType=application/x-ubik",
+ },
+ ],
+ },
+ // vCards should be included in the attachment list.
+ {
+ name: "multiple_attachments_one_vcard",
+ attachments: [
+ {
+ body: textAttachment,
+ filename: "ubik.txt",
+ format: "",
+ icon: "moz-icon://ubik.txt?size=16&contentType=text/plain",
+ },
+ {
+ body: vcardAttachment,
+ contentType: "text/vcard",
+ filename: "ubik.vcf",
+ encoding: "base64",
+ format: "",
+ icon: "moz-icon://ubik.vcf?size=16&contentType=text/vcard",
+ },
+ ],
+ },
+];
+
+add_setup(async function () {
+ messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+
+ // Set up our detached/deleted attachments.
+ var detachedFile = new FileUtils.File(
+ getTestFilePath(`data/${detachedName}`)
+ );
+ var detached = create_body_part("Here is a file", [
+ create_detached_attachment(detachedFile, "text/plain"),
+ ]);
+
+ var missingFile = new FileUtils.File(getTestFilePath(`data/${missingName}`));
+ var missing = create_body_part(
+ "Here is a file (but you deleted the external file, you silly oaf!)",
+ [create_detached_attachment(missingFile, "text/plain")]
+ );
+
+ var deleted = create_body_part("Here is a file that you deleted", [
+ create_deleted_attachment(deletedName, "text/plain"),
+ ]);
+
+ folder = await create_folder("AttachmentIcons");
+ for (let i = 0; i < messages.length; i++) {
+ switch (messages[i].name) {
+ case "detached_attachment":
+ messages[i].bodyPart = detached;
+ break;
+ case "detached_attachment_with_missing_file":
+ messages[i].bodyPart = missing;
+ break;
+ case "deleted_attachment":
+ messages[i].bodyPart = deleted;
+ break;
+ }
+
+ await add_message_to_folder([folder], create_message(messages[i]));
+ }
+});
+
+/**
+ * Make sure that the attachment's icon is what we expect.
+ *
+ * @param index the attachment's index, starting at 0
+ * @param expectedSize the URL of the expected icon of the attachment
+ */
+function check_attachment_icon(index, expectedIcon) {
+ let win = get_about_message();
+ let list = win.document.getElementById("attachmentList");
+ let node = list.querySelectorAll("richlistitem.attachmentItem")[index];
+
+ Assert.equal(
+ node.querySelector("img.attachmentcell-icon").src,
+ expectedIcon,
+ `Icon should be correct for attachment #${index}`
+ );
+}
+
+/**
+ * Make sure that the individual icons are as expected.
+ *
+ * @param index the index of the message to check in the thread pane
+ */
+async function help_test_attachment_icon(index) {
+ await be_in_folder(folder);
+ select_click_row(index);
+ info(`Testing message ${index}: ${messages[index].name}`);
+ let attachments = messages[index].attachments;
+
+ let win = get_about_message();
+ win.toggleAttachmentList(true);
+
+ let attachmentList = win.document.getElementById("attachmentList");
+ await TestUtils.waitForCondition(
+ () => !attachmentList.collapsed,
+ "Attachment list is shown"
+ );
+
+ for (let i = 0; i < attachments.length; i++) {
+ check_attachment_icon(i, attachments[i].icon);
+ }
+}
+
+add_task(async function test_attachment_icons() {
+ for (let i = 0; i < messages.length; i++) {
+ await help_test_attachment_icon(i);
+ }
+});
+
+registerCleanupFunction(() => {
+ // Remove created folders.
+ folder.deleteSelf(null);
+});
diff --git a/comm/mail/test/browser/attachment/browser_attachmentInPlainMsg.js b/comm/mail/test/browser/attachment/browser_attachmentInPlainMsg.js
new file mode 100644
index 0000000000..fb8ff5eb6c
--- /dev/null
+++ b/comm/mail/test/browser/attachment/browser_attachmentInPlainMsg.js
@@ -0,0 +1,51 @@
+/* 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";
+
+var { get_about_message, open_message_from_file } = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { close_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+/**
+ * Bug 1358565
+ * Check that a non-empty image is shown as attachment and is detected as non-empty
+ * when message is viewed as plain text.
+ */
+add_task(async function test_attachment_not_empty() {
+ Services.prefs.setBoolPref("mailnews.display.prefer_plaintext", true);
+
+ let file = new FileUtils.File(getTestFilePath("data/bug1358565.eml"));
+
+ let msgc = await open_message_from_file(file);
+ let aboutMessage = get_about_message(msgc.window);
+
+ EventUtils.synthesizeMouseAtCenter(
+ aboutMessage.document.getElementById("attachmentToggle"),
+ {},
+ aboutMessage
+ );
+ Assert.equal(
+ aboutMessage.document.getElementById("attachmentList").itemCount,
+ 1
+ );
+
+ let attachmentElem = aboutMessage.document
+ .getElementById("attachmentList")
+ .getItemAtIndex(0);
+ Assert.equal(attachmentElem.attachment.contentType, "image/jpeg");
+ Assert.equal(attachmentElem.attachment.name, "bug.png");
+ Assert.ok(attachmentElem.attachment.hasFile);
+ Assert.ok(
+ !(await attachmentElem.attachment.isEmpty()),
+ "Attachment incorrectly determined empty"
+ );
+
+ close_window(msgc);
+
+ Services.prefs.clearUserPref("mailnews.display.prefer_plaintext");
+});
diff --git a/comm/mail/test/browser/attachment/browser_attachmentMenus.js b/comm/mail/test/browser/attachment/browser_attachmentMenus.js
new file mode 100644
index 0000000000..cc24e6ccc9
--- /dev/null
+++ b/comm/mail/test/browser/attachment/browser_attachmentMenus.js
@@ -0,0 +1,565 @@
+/* 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";
+
+var folder;
+var messenger;
+var epsilon;
+
+var {
+ create_body_part,
+ create_deleted_attachment,
+ create_detached_attachment,
+ create_enclosure_attachment,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/AttachmentHelpers.jsm"
+);
+var {
+ add_message_to_folder,
+ be_in_folder,
+ close_popup,
+ create_folder,
+ create_message,
+ get_about_message,
+ mc,
+ select_click_row,
+ wait_for_popup_to_open,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+var aboutMessage = get_about_message();
+
+var textAttachment =
+ "Can't make the frug contest, Helen; stomach's upset. I'll fix you, " +
+ "Ubik! Ubik drops you back in the thick of things fast. Taken as " +
+ "directed, Ubik speeds relief to head and stomach. Remember: Ubik is " +
+ "only seconds away. Avoid prolonged use.";
+
+var detachedName = "./attachment.txt";
+var missingName = "./nonexistent.txt";
+var deletedName = "deleted.txt";
+
+// create some messages that have various types of attachments
+var messages = [
+ {
+ name: "regular_attachment",
+ attachments: [{ body: textAttachment, filename: "ubik.txt", format: "" }],
+ menuStates: [{ open: true, save: true, detach: true, delete_: true }],
+ allMenuStates: { open: true, save: true, detach: true, delete_: true },
+ },
+ {
+ name: "detached_attachment",
+ bodyPart: null,
+ menuStates: [{ open: true, save: true, detach: false, delete_: false }],
+ allMenuStates: { open: true, save: true, detach: false, delete_: false },
+ },
+ {
+ name: "detached_attachment_with_missing_file",
+ bodyPart: null,
+ menuStates: [{ open: false, save: false, detach: false, delete_: false }],
+ allMenuStates: { open: false, save: false, detach: false, delete_: false },
+ },
+ {
+ name: "deleted_attachment",
+ bodyPart: null,
+ menuStates: [{ open: false, save: false, detach: false, delete_: false }],
+ allMenuStates: { open: false, save: false, detach: false, delete_: false },
+ },
+ {
+ name: "multiple_attachments",
+ attachments: [
+ { body: textAttachment, filename: "ubik.txt", format: "" },
+ { body: textAttachment, filename: "ubik2.txt", format: "" },
+ ],
+ menuStates: [
+ { open: true, save: true, detach: true, delete_: true },
+ { open: true, save: true, detach: true, delete_: true },
+ ],
+ allMenuStates: { open: true, save: true, detach: true, delete_: true },
+ },
+ {
+ name: "multiple_attachments_one_detached",
+ bodyPart: null,
+ attachments: [{ body: textAttachment, filename: "ubik.txt", format: "" }],
+ menuStates: [
+ { open: true, save: true, detach: false, delete_: false },
+ { open: true, save: true, detach: true, delete_: true },
+ ],
+ allMenuStates: { open: true, save: true, detach: true, delete_: true },
+ },
+ {
+ name: "multiple_attachments_one_detached_with_missing_file",
+ bodyPart: null,
+ attachments: [{ body: textAttachment, filename: "ubik.txt", format: "" }],
+ menuStates: [
+ { open: false, save: false, detach: false, delete_: false },
+ { open: true, save: true, detach: true, delete_: true },
+ ],
+ allMenuStates: { open: true, save: true, detach: true, delete_: true },
+ },
+ {
+ name: "multiple_attachments_one_deleted",
+ bodyPart: null,
+ attachments: [{ body: textAttachment, filename: "ubik.txt", format: "" }],
+ menuStates: [
+ { open: false, save: false, detach: false, delete_: false },
+ { open: true, save: true, detach: true, delete_: true },
+ ],
+ allMenuStates: { open: true, save: true, detach: true, delete_: true },
+ },
+ {
+ name: "multiple_attachments_all_detached",
+ bodyPart: null,
+ menuStates: [
+ { open: true, save: true, detach: false, delete_: false },
+ { open: true, save: true, detach: false, delete_: false },
+ ],
+ allMenuStates: { open: true, save: true, detach: false, delete_: false },
+ },
+ {
+ name: "multiple_attachments_all_detached_with_missing_files",
+ bodyPart: null,
+ menuStates: [
+ { open: false, save: false, detach: false, delete_: false },
+ { open: false, save: false, detach: false, delete_: false },
+ ],
+ allMenuStates: { open: false, save: false, detach: false, delete_: false },
+ },
+ {
+ name: "multiple_attachments_all_deleted",
+ bodyPart: null,
+ menuStates: [
+ { open: false, save: false, detach: false, delete_: false },
+ { open: false, save: false, detach: false, delete_: false },
+ ],
+ allMenuStates: { open: false, save: false, detach: false, delete_: false },
+ },
+ {
+ name: "link_enclosure_valid",
+ bodyPart: null,
+ menuStates: [{ open: true, save: true, detach: false, delete_: false }],
+ allMenuStates: { open: true, save: true, detach: false, delete_: false },
+ },
+ {
+ name: "link_enclosure_invalid",
+ bodyPart: null,
+ menuStates: [{ open: false, save: false, detach: false, delete_: false }],
+ allMenuStates: { open: false, save: false, detach: false, delete_: false },
+ },
+ {
+ name: "link_multiple_enclosures",
+ bodyPart: null,
+ menuStates: [
+ { open: true, save: true, detach: false, delete_: false },
+ { open: true, save: true, detach: false, delete_: false },
+ ],
+ allMenuStates: { open: true, save: true, detach: false, delete_: false },
+ },
+ {
+ name: "link_multiple_enclosures_one_invalid",
+ bodyPart: null,
+ menuStates: [
+ { open: true, save: true, detach: false, delete_: false },
+ { open: false, save: false, detach: false, delete_: false },
+ ],
+ allMenuStates: { open: true, save: true, detach: false, delete_: false },
+ },
+ {
+ name: "link_multiple_enclosures_all_invalid",
+ bodyPart: null,
+ menuStates: [
+ { open: false, save: false, detach: false, delete_: false },
+ { open: false, save: false, detach: false, delete_: false },
+ ],
+ allMenuStates: { open: false, save: false, detach: false, delete_: false },
+ },
+];
+
+add_setup(async function () {
+ messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+
+ /* Today's gory details (thanks to Jonathan Protzenko): libmime somehow
+ * counts the trailing newline for an attachment MIME part. Most of the time,
+ * assuming attachment has N bytes (no matter what's inside, newlines or
+ * not), libmime will return N + 1 bytes. On Linux and Mac, this always
+ * holds. However, on Windows, if the attachment is not encoded (that is, is
+ * inline text), libmime will return N + 2 bytes.
+ */
+ epsilon = "@mozilla.org/windows-registry-key;1" in Cc ? 2 : 1;
+
+ // set up our detached/deleted attachments
+ var detachedFile = new FileUtils.File(
+ getTestFilePath(`data/${detachedName}`)
+ );
+ var detached = create_body_part("Here is a file", [
+ create_detached_attachment(detachedFile, "text/plain"),
+ ]);
+ var multiple_detached = create_body_part("Here are some files", [
+ create_detached_attachment(detachedFile, "text/plain"),
+ create_detached_attachment(detachedFile, "text/plain"),
+ ]);
+
+ var missingFile = new FileUtils.File(getTestFilePath(`data/${missingName}`));
+ var missing = create_body_part(
+ "Here is a file (but you deleted the external file, you silly oaf!)",
+ [create_detached_attachment(missingFile, "text/plain")]
+ );
+ var multiple_missing = create_body_part(
+ "Here are some files (but you deleted the external files, you silly oaf!)",
+ [
+ create_detached_attachment(missingFile, "text/plain"),
+ create_detached_attachment(missingFile, "text/plain"),
+ ]
+ );
+
+ var deleted = create_body_part("Here is a file that you deleted", [
+ create_deleted_attachment(deletedName, "text/plain"),
+ ]);
+ var multiple_deleted = create_body_part(
+ "Here are some files that you deleted",
+ [
+ create_deleted_attachment(deletedName, "text/plain"),
+ create_deleted_attachment(deletedName, "text/plain"),
+ ]
+ );
+
+ var enclosure_valid_url = create_body_part("My blog has the best enclosure", [
+ create_enclosure_attachment(
+ "purr.mp3",
+ "audio/mpeg",
+ "https://example.com",
+ 12345678
+ ),
+ ]);
+ var enclosure_invalid_url = create_body_part(
+ "My blog has the best enclosure with a dead link",
+ [
+ create_enclosure_attachment(
+ "meow.mp3",
+ "audio/mpeg",
+ "https://example.com/invalid"
+ ),
+ ]
+ );
+ var multiple_enclosures = create_body_part(
+ "My blog has the best 2 cat sound enclosures",
+ [
+ create_enclosure_attachment(
+ "purr.mp3",
+ "audio/mpeg",
+ "https://example.com",
+ 1234567
+ ),
+ create_enclosure_attachment(
+ "meow.mp3",
+ "audio/mpeg",
+ "https://example.com",
+ 987654321
+ ),
+ ]
+ );
+ var multiple_enclosures_one_link_invalid = create_body_part(
+ "My blog has the best 2 cat sound enclosures but one is invalid",
+ [
+ create_enclosure_attachment(
+ "purr.mp3",
+ "audio/mpeg",
+ "https://example.com",
+ 1234567
+ ),
+ create_enclosure_attachment(
+ "meow.mp3",
+ "audio/mpeg",
+ "https://example.com/invalid"
+ ),
+ ]
+ );
+ var multiple_enclosures_all_links_invalid = create_body_part(
+ "My blog has 2 enclosures with 2 bad links",
+ [
+ create_enclosure_attachment(
+ "purr.mp3",
+ "audio/mpeg",
+ "https://example.com/invalid"
+ ),
+ create_enclosure_attachment(
+ "meow.mp3",
+ "audio/mpeg",
+ "https://example.com/invalid"
+ ),
+ ]
+ );
+
+ folder = await create_folder("AttachmentMenusA");
+ for (let i = 0; i < messages.length; i++) {
+ // First, add any missing info to the message object.
+ switch (messages[i].name) {
+ case "detached_attachment":
+ case "multiple_attachments_one_detached":
+ messages[i].bodyPart = detached;
+ break;
+ case "multiple_attachments_all_detached":
+ messages[i].bodyPart = multiple_detached;
+ break;
+ case "detached_attachment_with_missing_file":
+ case "multiple_attachments_one_detached_with_missing_file":
+ messages[i].bodyPart = missing;
+ break;
+ case "multiple_attachments_all_detached_with_missing_files":
+ messages[i].bodyPart = multiple_missing;
+ break;
+ case "deleted_attachment":
+ case "multiple_attachments_one_deleted":
+ messages[i].bodyPart = deleted;
+ break;
+ case "multiple_attachments_all_deleted":
+ messages[i].bodyPart = multiple_deleted;
+ break;
+ case "link_enclosure_valid":
+ messages[i].bodyPart = enclosure_valid_url;
+ break;
+ case "link_enclosure_invalid":
+ messages[i].bodyPart = enclosure_invalid_url;
+ break;
+ case "link_multiple_enclosures":
+ messages[i].bodyPart = multiple_enclosures;
+ break;
+ case "link_multiple_enclosures_one_invalid":
+ messages[i].bodyPart = multiple_enclosures_one_link_invalid;
+ break;
+ case "link_multiple_enclosures_all_invalid":
+ messages[i].bodyPart = multiple_enclosures_all_links_invalid;
+ break;
+ }
+
+ await add_message_to_folder([folder], create_message(messages[i]));
+ }
+});
+
+/**
+ * Ensure that the specified element is visible/hidden
+ *
+ * @param id the id of the element to check
+ * @param visible true if the element should be visible, false otherwise
+ */
+function assert_shown(id, visible) {
+ Assert.notEqual(
+ aboutMessage.document.getElementById(id).hidden,
+ visible,
+ `"${id}" should be ${visible ? "visible" : "hidden"}`
+ );
+}
+
+/**
+ * Ensure that the specified element is enabled/disabled
+ *
+ * @param id the id of the element to check
+ * @param enabled true if the element should be enabled, false otherwise
+ */
+function assert_enabled(id, enabled) {
+ Assert.notEqual(
+ aboutMessage.document.getElementById(id).disabled,
+ enabled,
+ `"${id}" should be ${enabled ? "enabled" : "disabled"}`
+ );
+}
+
+/**
+ * Check that the menu states in the "save" toolbar button are correct.
+ *
+ * @param expected a dictionary containing the expected states
+ */
+async function check_toolbar_menu_states_single(expected) {
+ assert_shown("attachmentSaveAllSingle", true);
+ assert_shown("attachmentSaveAllMultiple", false);
+
+ if (expected.save === false) {
+ assert_enabled("attachmentSaveAllSingle", false);
+ } else {
+ assert_enabled("attachmentSaveAllSingle", true);
+ let dm = aboutMessage.document.querySelector(
+ "#attachmentSaveAllSingle .toolbarbutton-menubutton-dropmarker"
+ );
+ EventUtils.synthesizeMouseAtCenter(dm, { clickCount: 1 }, aboutMessage);
+ await wait_for_popup_to_open(
+ aboutMessage.document.getElementById("attachmentSaveAllSingleMenu")
+ );
+
+ try {
+ assert_enabled("button-openAttachment", expected.open);
+ assert_enabled("button-saveAttachment", expected.save);
+ assert_enabled("button-detachAttachment", expected.detach);
+ assert_enabled("button-deleteAttachment", expected.delete_);
+ } finally {
+ await close_popup(
+ aboutMessage,
+ aboutMessage.document.getElementById("attachmentSaveAllSingleMenu")
+ );
+ }
+ }
+}
+
+/**
+ * Check that the menu states in the "save all" toolbar button are correct.
+ *
+ * @param expected a dictionary containing the expected states
+ */
+async function check_toolbar_menu_states_multiple(expected) {
+ assert_shown("attachmentSaveAllSingle", false);
+ assert_shown("attachmentSaveAllMultiple", true);
+
+ if (expected.save === false) {
+ assert_enabled("attachmentSaveAllMultiple", false);
+ } else {
+ assert_enabled("attachmentSaveAllMultiple", true);
+ let dm = aboutMessage.document.querySelector(
+ "#attachmentSaveAllMultiple .toolbarbutton-menubutton-dropmarker"
+ );
+ EventUtils.synthesizeMouseAtCenter(dm, { clickCount: 1 }, aboutMessage);
+ await wait_for_popup_to_open(
+ aboutMessage.document.getElementById("attachmentSaveAllMultipleMenu")
+ );
+
+ try {
+ assert_enabled("button-openAllAttachments", expected.open);
+ assert_enabled("button-saveAllAttachments", expected.save);
+ assert_enabled("button-detachAllAttachments", expected.detach);
+ assert_enabled("button-deleteAllAttachments", expected.delete_);
+ } finally {
+ await close_popup(
+ mc,
+ aboutMessage.document.getElementById("attachmentSaveAllMultipleMenu")
+ );
+ }
+ }
+}
+
+/**
+ * Check that the menu states in the single item context menu are correct
+ *
+ * @param expected a dictionary containing the expected states
+ */
+async function check_menu_states_single(index, expected) {
+ let attachmentList = aboutMessage.document.getElementById("attachmentList");
+ let node = attachmentList.getItemAtIndex(index);
+
+ let contextMenu = aboutMessage.document.getElementById(
+ "attachmentItemContext"
+ );
+ let shownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ attachmentList.selectItem(node);
+ EventUtils.synthesizeMouseAtCenter(
+ node,
+ { type: "contextmenu" },
+ aboutMessage
+ );
+ await shownPromise;
+
+ try {
+ assert_shown("context-openAttachment", true);
+ assert_shown("context-saveAttachment", true);
+ assert_shown("context-menu-separator", true);
+ assert_shown("context-detachAttachment", true);
+ assert_shown("context-deleteAttachment", true);
+
+ assert_enabled("context-openAttachment", expected.open);
+ assert_enabled("context-saveAttachment", expected.save);
+ assert_enabled("context-detachAttachment", expected.detach);
+ assert_enabled("context-deleteAttachment", expected.delete_);
+ } finally {
+ let hiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await hiddenPromise;
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ }
+}
+
+/**
+ * Check that the menu states in the all items context menu are correct
+ *
+ * @param expected a dictionary containing the expected states
+ */
+async function check_menu_states_all(expected) {
+ // Using a rightClick here is unsafe, because we need to hit the empty area
+ // beside the attachment items and that seems to be different per platform.
+ // Using DOM methods to open the popup works fine.
+ let contextMenu = aboutMessage.document.getElementById(
+ "attachmentListContext"
+ );
+ let shownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ aboutMessage.document
+ .getElementById("attachmentListContext")
+ .openPopup(aboutMessage.document.getElementById("attachmentList"));
+ await shownPromise;
+
+ try {
+ assert_shown("context-openAllAttachments", true);
+ assert_shown("context-saveAllAttachments", true);
+ assert_shown("context-menu-separator-all", true);
+ assert_shown("context-detachAllAttachments", true);
+ assert_shown("context-deleteAllAttachments", true);
+
+ assert_enabled("context-openAllAttachments", expected.open);
+ assert_enabled("context-saveAllAttachments", expected.save);
+ assert_enabled("context-detachAllAttachments", expected.detach);
+ assert_enabled("context-deleteAllAttachments", expected.delete_);
+ } finally {
+ await close_popup(
+ aboutMessage,
+ aboutMessage.document.getElementById("attachmentListContext")
+ );
+ }
+}
+
+async function help_test_attachment_menus(index) {
+ await be_in_folder(folder);
+ select_click_row(index);
+ let expectedStates = messages[index].menuStates;
+
+ let aboutMessage = get_about_message();
+ aboutMessage.toggleAttachmentList(true);
+
+ for (let attachment of aboutMessage.currentAttachments) {
+ // Ensure all attachments are resolved; other than external they already
+ // should be.
+ await attachment.isEmpty();
+ }
+
+ if (expectedStates.length == 1) {
+ await check_toolbar_menu_states_single(messages[index].allMenuStates);
+ } else {
+ await check_toolbar_menu_states_multiple(messages[index].allMenuStates);
+ }
+
+ await check_menu_states_all(messages[index].allMenuStates);
+ for (let i = 0; i < expectedStates.length; i++) {
+ await check_menu_states_single(i, expectedStates[i]);
+ }
+}
+
+// Generate a test for each message in |messages|.
+for (let i = 0; i < messages.length; i++) {
+ add_task(function () {
+ return help_test_attachment_menus(i);
+ });
+}
+
+add_task(() => {
+ Assert.report(
+ false,
+ undefined,
+ undefined,
+ "Test ran to completion successfully"
+ );
+});
+
+registerCleanupFunction(() => {
+ // Remove created folders.
+ folder.deleteSelf(null);
+});
diff --git a/comm/mail/test/browser/attachment/browser_attachmentSize.js b/comm/mail/test/browser/attachment/browser_attachmentSize.js
new file mode 100644
index 0000000000..f4980c46ad
--- /dev/null
+++ b/comm/mail/test/browser/attachment/browser_attachmentSize.js
@@ -0,0 +1,421 @@
+/* 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";
+
+var folder;
+var messenger;
+var epsilon;
+
+var {
+ create_body_part,
+ create_deleted_attachment,
+ create_detached_attachment,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/AttachmentHelpers.jsm"
+);
+var {
+ add_message_to_folder,
+ be_in_folder,
+ create_folder,
+ create_message,
+ get_about_message,
+ mc,
+ msgGen,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+var { SyntheticPartLeaf, SyntheticPartMultiMixed } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+
+var textAttachment =
+ "Can't make the frug contest, Helen; stomach's upset. I'll fix you, " +
+ "Ubik! Ubik drops you back in the thick of things fast. Taken as " +
+ "directed, Ubik speeds relief to head and stomach. Remember: Ubik is " +
+ "only seconds away. Avoid prolonged use.";
+
+var binaryAttachment = textAttachment;
+
+var imageAttachment =
+ "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwS" +
+ "FlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA" +
+ "A5SURBVCiRY/z//z8DKYCJJNXkaGBgYGD4D8NQ5zUgiTVAxeBqSLaBkVRPM0KtIhrQ3km0jwe" +
+ "SNQAAlmAY+71EgFoAAAAASUVORK5CYII=";
+var imageSize = 188;
+
+var vcardAttachment =
+ "YmVnaW46dmNhcmQNCmZuOkppbSBCb2INCm46Qm9iO0ppbQ0KZW1haWw7aW50ZXJuZXQ6Zm9v" +
+ "QGJhci5jb20NCnZlcnNpb246Mi4xDQplbmQ6dmNhcmQNCg0K";
+var vcardSize = 90;
+
+var detachedName = "./attachment.txt";
+var missingName = "./nonexistent.txt";
+var deletedName = "deleted.txt";
+
+// create some messages that have various types of attachments
+var messages = [
+ {
+ name: "text_attachment",
+ attachments: [{ body: textAttachment, filename: "ubik.txt", format: "" }],
+ attachmentSizes: [textAttachment.length],
+ attachmentTotalSize: { size: textAttachment.length, exact: true },
+ },
+ {
+ name: "binary_attachment",
+ attachments: [
+ {
+ body: binaryAttachment,
+ contentType: "application/x-ubik",
+ filename: "ubik",
+ format: "",
+ },
+ ],
+ attachmentSizes: [binaryAttachment.length],
+ attachmentTotalSize: { size: binaryAttachment.length, exact: true },
+ },
+ {
+ name: "image_attachment",
+ attachments: [
+ {
+ body: imageAttachment,
+ contentType: "image/png",
+ filename: "lines.png",
+ encoding: "base64",
+ format: "",
+ },
+ ],
+ attachmentSizes: [imageSize],
+ attachmentTotalSize: { size: imageSize, exact: true },
+ },
+ {
+ name: "detached_attachment",
+ bodyPart: null,
+ // Sizes filled in on message creation.
+ attachmentSizes: [null],
+ attachmentTotalSize: { size: 0, exact: true },
+ },
+ {
+ name: "detached_attachment_with_missing_file",
+ bodyPart: null,
+ attachmentSizes: [-1],
+ attachmentTotalSize: { size: 0, exact: false },
+ },
+ {
+ name: "deleted_attachment",
+ bodyPart: null,
+ attachmentSizes: [-1],
+ attachmentTotalSize: { size: 0, exact: true },
+ },
+ {
+ name: "multiple_attachments",
+ attachments: [
+ { body: textAttachment, filename: "ubik.txt", format: "" },
+ {
+ body: binaryAttachment,
+ contentType: "application/x-ubik",
+ filename: "ubik",
+ format: "",
+ },
+ ],
+ attachmentSizes: [textAttachment.length, binaryAttachment.length],
+ attachmentTotalSize: {
+ size: textAttachment.length + binaryAttachment.length,
+ exact: true,
+ },
+ },
+ // vCards should be included in the attachment list.
+ {
+ name: "multiple_attachments_one_vcard",
+ attachments: [
+ { body: textAttachment, filename: "ubik.txt", format: "" },
+ {
+ body: vcardAttachment,
+ contentType: "text/vcard",
+ filename: "ubik.vcf",
+ encoding: "base64",
+ format: "",
+ },
+ ],
+ attachmentSizes: [textAttachment.length, vcardSize],
+ attachmentTotalSize: {
+ size: textAttachment.length + vcardSize,
+ exact: true,
+ },
+ },
+ {
+ name: "multiple_attachments_one_detached",
+ bodyPart: null,
+ attachments: [{ body: textAttachment, filename: "ubik.txt", format: "" }],
+ attachmentSizes: [null, textAttachment.length],
+ attachmentTotalSize: { size: textAttachment.length, exact: true },
+ },
+ {
+ name: "multiple_attachments_one_detached_with_missing_file",
+ bodyPart: null,
+ attachments: [{ body: textAttachment, filename: "ubik.txt", format: "" }],
+ attachmentSizes: [-1, textAttachment.length],
+ attachmentTotalSize: { size: textAttachment.length, exact: false },
+ },
+ {
+ name: "multiple_attachments_one_deleted",
+ bodyPart: null,
+ attachments: [{ body: textAttachment, filename: "ubik.txt", format: "" }],
+ attachmentSizes: [-1, textAttachment.length],
+ attachmentTotalSize: { size: textAttachment.length, exact: true },
+ },
+ // this is an attached message that itself has an attachment
+ {
+ name: "attached_message_with_attachment",
+ bodyPart: null,
+ attachmentSizes: [-1, textAttachment.length],
+ attachmentTotalSize: { size: 0, exact: true },
+ },
+];
+
+add_setup(async function () {
+ messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+
+ /* Today's gory details (thanks to Jonathan Protzenko): libmime somehow
+ * counts the trailing newline for an attachment MIME part. Most of the time,
+ * assuming attachment has N bytes (no matter what's inside, newlines or
+ * not), libmime will return N + 1 bytes. On Linux and Mac, this always
+ * holds. However, on Windows, if the attachment is not encoded (that is, is
+ * inline text), libmime will return N + 2 bytes.
+ */
+ epsilon = "@mozilla.org/windows-registry-key;1" in Cc ? 4 : 2;
+
+ // set up our detached/deleted attachments
+ var detachedFile = new FileUtils.File(
+ getTestFilePath(`data/${detachedName}`)
+ );
+ var detached = create_body_part("Here is a file", [
+ create_detached_attachment(detachedFile, "text/plain"),
+ ]);
+
+ var missingFile = new FileUtils.File(getTestFilePath(`data/${missingName}`));
+ var missing = create_body_part(
+ "Here is a file (but you deleted the external file, you silly oaf!)",
+ [create_detached_attachment(missingFile, "text/plain")]
+ );
+
+ var deleted = create_body_part("Here is a file that you deleted", [
+ create_deleted_attachment(deletedName, "text/plain"),
+ ]);
+
+ var attachedMessage = msgGen.makeMessage({
+ body: { body: textAttachment },
+ attachments: [{ body: textAttachment, filename: "ubik.txt", format: "" }],
+ });
+
+ /* Much like the above comment, libmime counts bytes differently on Windows,
+ * where it counts newlines (\r\n) as 2 bytes. Mac and Linux treats them as
+ * 1 byte.
+ */
+ var attachedMessageLength;
+ if (epsilon == 4) {
+ // Windows
+ attachedMessageLength = attachedMessage.toMessageString().length;
+ } else {
+ // Mac/Linux
+ attachedMessageLength = attachedMessage
+ .toMessageString()
+ .replace(/\r\n/g, "\n").length;
+ }
+
+ folder = await create_folder("AttachmentSizeA");
+ for (let i = 0; i < messages.length; i++) {
+ // First, add any missing info to the message object.
+ switch (messages[i].name) {
+ case "detached_attachment":
+ case "multiple_attachments_one_detached":
+ messages[i].bodyPart = detached;
+ messages[i].attachmentSizes[0] = detachedFile.fileSize;
+ messages[i].attachmentTotalSize.size += detachedFile.fileSize;
+ break;
+ case "detached_attachment_with_missing_file":
+ case "multiple_attachments_one_detached_with_missing_file":
+ messages[i].bodyPart = missing;
+ break;
+ case "deleted_attachment":
+ case "multiple_attachments_one_deleted":
+ messages[i].bodyPart = deleted;
+ break;
+ case "attached_message_with_attachment":
+ messages[i].bodyPart = new SyntheticPartMultiMixed([
+ new SyntheticPartLeaf("I am text!", { contentType: "text/plain" }),
+ attachedMessage,
+ ]);
+ messages[i].attachmentSizes[0] = attachedMessageLength;
+ messages[i].attachmentTotalSize.size += attachedMessageLength;
+ break;
+ }
+
+ await add_message_to_folder([folder], create_message(messages[i]));
+ }
+});
+
+/**
+ * Make sure that the attachment's size is what we expect
+ *
+ * @param index the attachment's index, starting at 0
+ * @param expectedSize the expected size of the attachment, in bytes
+ */
+function check_attachment_size(index, expectedSize) {
+ let win = get_about_message();
+ let list = win.document.getElementById("attachmentList");
+ let node = list.querySelectorAll("richlistitem.attachmentItem")[index];
+
+ // First, let's check that the attachment size is correct
+ let size = node.attachment.size;
+ Assert.ok(
+ Math.abs(size - expectedSize) <= epsilon,
+ `Attachment "${node.attachment.name}" size should be within ${epsilon} ` +
+ `of ${expectedSize} (actual: ${size})`
+ );
+
+ // Next, make sure that the formatted size in the label is correct
+ Assert.equal(
+ node.getAttribute("size"),
+ messenger.formatFileSize(size),
+ `Attachment "${node.attachment.name}" displayed size should match`
+ );
+}
+
+/**
+ * Make sure that the attachment's size is not displayed
+ *
+ * @param index the attachment's index, starting at 0
+ */
+function check_no_attachment_size(index) {
+ let win = get_about_message();
+ let list = win.document.getElementById("attachmentList");
+ let node = list.querySelectorAll("richlistitem.attachmentItem")[index];
+
+ Assert.equal(
+ node.attachment.size,
+ -1,
+ `Attachment "${node.attachment.name}" should have a size of -1`
+ );
+
+ // If there's no size, the size attribute is the zero-width space.
+ let nodeSize = node.getAttribute("size");
+ Assert.equal(
+ nodeSize,
+ "",
+ `Attachment "${node.attachment.name}" size should not be displayed`
+ );
+}
+
+/**
+ * Make sure that the total size of all attachments is what we expect.
+ *
+ * @param count the expected number of attachments
+ * @param expectedSize the expected size in bytes of all the attachments
+ * @param exact true if the size of all attachments is known, false otherwise
+ */
+function check_total_attachment_size(count, expectedSize, exact) {
+ let win = get_about_message();
+ let list = win.document.getElementById("attachmentList");
+ let nodes = list.querySelectorAll("richlistitem.attachmentItem");
+ let sizeNode = win.document.getElementById("attachmentSize");
+
+ Assert.equal(
+ nodes.length,
+ count,
+ "Should have the expected number of attachments"
+ );
+
+ let lastPartID;
+ let size = 0;
+ for (let i = 0; i < nodes.length; i++) {
+ let attachment = nodes[i].attachment;
+ if (!lastPartID || attachment.partID.indexOf(lastPartID) != 0) {
+ lastPartID = attachment.partID;
+ let currSize = attachment.size;
+ if (currSize > 0 && !isNaN(currSize)) {
+ size += Number(currSize);
+ }
+ }
+ }
+
+ Assert.ok(
+ Math.abs(size - expectedSize) <= epsilon * count,
+ `Total attachments size should be within ${epsilon * count} ` +
+ `of ${expectedSize} (actual: ${size})`
+ );
+
+ // Next, make sure that the formatted size in the label is correct
+ let formattedSize = sizeNode.getAttribute("value");
+ let expectedFormattedSize = messenger.formatFileSize(size);
+ let messengerBundle = mc.window.document.getElementById("bundle_messenger");
+
+ if (!exact) {
+ if (size == 0) {
+ expectedFormattedSize = messengerBundle.getString(
+ "attachmentSizeUnknown"
+ );
+ } else {
+ expectedFormattedSize = messengerBundle.getFormattedString(
+ "attachmentSizeAtLeast",
+ [expectedFormattedSize]
+ );
+ }
+ }
+ Assert.equal(
+ formattedSize,
+ expectedFormattedSize,
+ "Displayed attachments total size should match"
+ );
+}
+
+/**
+ * Make sure that the individual and total attachment sizes for this message
+ * are as expected
+ *
+ * @param index the index of the message to check in the thread pane
+ */
+async function help_test_attachment_size(index) {
+ await be_in_folder(folder);
+ select_click_row(index);
+ info(`Testing message ${index}: ${messages[index].name}`);
+ let expectedSizes = messages[index].attachmentSizes;
+
+ let aboutMessage = get_about_message();
+ aboutMessage.toggleAttachmentList(true);
+
+ let attachmentList = aboutMessage.document.getElementById("attachmentList");
+ await TestUtils.waitForCondition(
+ () => !attachmentList.collapsed,
+ "Attachment list is shown"
+ );
+
+ for (let i = 0; i < expectedSizes.length; i++) {
+ if (expectedSizes[i] == -1) {
+ check_no_attachment_size(i);
+ } else {
+ check_attachment_size(i, expectedSizes[i]);
+ }
+ }
+
+ let totalSize = messages[index].attachmentTotalSize;
+ check_total_attachment_size(
+ expectedSizes.length,
+ totalSize.size,
+ totalSize.exact
+ );
+}
+
+add_task(async function test_attachment_sizes() {
+ for (let i = 0; i < messages.length; i++) {
+ await help_test_attachment_size(i);
+ }
+});
+
+registerCleanupFunction(() => {
+ // Remove created folders.
+ folder.deleteSelf(null);
+});
diff --git a/comm/mail/test/browser/attachment/browser_openAttachment.js b/comm/mail/test/browser/attachment/browser_openAttachment.js
new file mode 100644
index 0000000000..737144b3c6
--- /dev/null
+++ b/comm/mail/test/browser/attachment/browser_openAttachment.js
@@ -0,0 +1,738 @@
+/* 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/. */
+
+const {
+ add_message_to_folder,
+ be_in_folder,
+ create_folder,
+ create_message,
+ get_about_message,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+let aboutMessage = get_about_message();
+
+const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+const handlerService = Cc[
+ "@mozilla.org/uriloader/handler-service;1"
+].getService(Ci.nsIHandlerService);
+
+const { MockFilePicker } = SpecialPowers;
+MockFilePicker.init(window);
+
+// At the time of writing, this pref was set to true on nightly channels only.
+// The behaviour is slightly different when it is false.
+const IMPROVEMENTS_PREF_SET = Services.prefs.getBoolPref(
+ "browser.download.improvements_to_download_panel",
+ true
+);
+
+let tmpD;
+let savePath;
+let homeDirectory;
+
+let folder;
+
+let mockedHandlerApp;
+let mockedHandlers = new Set();
+
+function getNsIFileFromPath(path) {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(path);
+ return file;
+}
+
+add_setup(async function () {
+ folder = await create_folder("OpenAttachment");
+ await be_in_folder(folder);
+
+ // @see logic for tmpD in msgHdrView.js
+ tmpD = PathUtils.join(
+ Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ "pid-" + Services.appinfo.processID
+ );
+
+ savePath = await IOUtils.createUniqueDirectory(tmpD, "saveDestination");
+ Services.prefs.setStringPref("browser.download.dir", savePath);
+
+ homeDirectory = await IOUtils.createUniqueDirectory(tmpD, "homeDirectory");
+
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setBoolPref("browser.download.useDownloadDir", true);
+ Services.prefs.setIntPref("security.dialog_enable_delay", 0);
+
+ let mockedExecutable = FileUtils.getFile("TmpD", ["mockedExecutable"]);
+ if (!mockedExecutable.exists()) {
+ mockedExecutable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o755);
+ }
+
+ mockedHandlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ mockedHandlerApp.executable = mockedExecutable;
+ mockedHandlerApp.detailedDescription = "Mocked handler app";
+ registerCleanupFunction(() => {
+ if (mockedExecutable.exists()) {
+ mockedExecutable.remove(true);
+ }
+ });
+});
+
+registerCleanupFunction(async function () {
+ MockFilePicker.cleanup();
+
+ await IOUtils.remove(savePath, { recursive: true });
+ await IOUtils.remove(homeDirectory, { recursive: true });
+
+ Services.prefs.clearUserPref("browser.download.dir");
+ Services.prefs.clearUserPref("browser.download.folderList");
+ Services.prefs.clearUserPref("browser.download.useDownloadDir");
+ Services.prefs.clearUserPref("security.dialog.dialog_enable_delay");
+
+ for (let type of mockedHandlers) {
+ let handlerInfo = mimeService.getFromTypeAndExtension(type, null);
+ if (handlerService.exists(handlerInfo)) {
+ handlerService.remove(handlerInfo);
+ }
+ }
+
+ // Remove created folders.
+ folder.deleteSelf(null);
+
+ Services.focus.focusedWindow = window;
+});
+
+function createMockedHandler(type, preferredAction, alwaysAskBeforeHandling) {
+ info(`Creating handler for ${type}`);
+
+ let handlerInfo = mimeService.getFromTypeAndExtension(type, null);
+ handlerInfo.preferredAction = preferredAction;
+ handlerInfo.alwaysAskBeforeHandling = alwaysAskBeforeHandling;
+
+ handlerInfo.description = mockedHandlerApp.detailedDescription;
+ handlerInfo.possibleApplicationHandlers.appendElement(mockedHandlerApp);
+ handlerInfo.hasDefaultHandler = true;
+ handlerInfo.preferredApplicationHandler = mockedHandlerApp;
+
+ handlerService.store(handlerInfo);
+ mockedHandlers.add(type);
+}
+
+let messageIndex = -1;
+async function createAndLoadMessage(
+ type,
+ { filename, isDetached = false } = {}
+) {
+ messageIndex++;
+
+ if (!filename) {
+ filename = `attachment${messageIndex}.test${messageIndex}`;
+ }
+
+ let attachment = {
+ contentType: type,
+ body: `${type}Attachment`,
+ filename,
+ };
+
+ // Allow for generation of messages with detached attachments.
+ if (isDetached) {
+ // Generate a file with content to represent the attachment.
+ let attachmentFile = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+ attachmentFile.initWithPath(homeDirectory);
+ attachmentFile.append(filename);
+ if (!attachmentFile.exists()) {
+ attachmentFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o755);
+ await IOUtils.writeUTF8(attachmentFile.path, "some file content");
+ }
+
+ let fileHandler = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+
+ // Append relevant Thunderbird headers to indicate a detached file.
+ attachment.extraHeaders = {
+ "X-Mozilla-External-Attachment-URL":
+ fileHandler.getURLSpecFromActualFile(attachmentFile),
+ "X-Mozilla-Altered":
+ 'AttachmentDetached; date="Mon Apr 04 13:59:42 2022"',
+ };
+ }
+
+ await add_message_to_folder(
+ [folder],
+ create_message({
+ subject: `${type} attachment`,
+ body: {
+ body: "I'm an attached email!",
+ },
+ attachments: [attachment],
+ })
+ );
+ select_click_row(messageIndex);
+}
+
+async function singleClickAttachmentAndWaitForDialog(
+ { mode = "save", rememberExpected = true, remember } = {},
+ button = "cancel"
+) {
+ let dialogPromise = BrowserTestUtils.promiseAlertDialog(
+ undefined,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ {
+ async callback(dialogWindow) {
+ await new Promise(resolve => dialogWindow.setTimeout(resolve));
+ await new Promise(resolve => dialogWindow.setTimeout(resolve));
+
+ let dialogDocument = dialogWindow.document;
+ let rememberChoice = dialogDocument.getElementById("rememberChoice");
+ Assert.equal(
+ dialogDocument.getElementById("mode").selectedItem.id,
+ mode,
+ "correct action is selected"
+ );
+ Assert.equal(
+ rememberChoice.checked,
+ rememberExpected,
+ "remember choice checkbox checked/not checked as expected"
+ );
+ if (remember !== undefined && remember != rememberExpected) {
+ EventUtils.synthesizeMouseAtCenter(rememberChoice, {}, dialogWindow);
+ Assert.equal(
+ rememberChoice.checked,
+ remember,
+ "remember choice checkbox changed"
+ );
+ }
+
+ dialogDocument.querySelector("dialog").getButton(button).click();
+ },
+ }
+ );
+
+ info(aboutMessage.document.getElementById("attachmentName").value);
+ EventUtils.synthesizeMouseAtCenter(
+ aboutMessage.document.getElementById("attachmentName"),
+ {},
+ aboutMessage
+ );
+ await dialogPromise;
+}
+
+async function singleClickAttachment() {
+ info(aboutMessage.document.getElementById("attachmentName").value);
+ EventUtils.synthesizeMouseAtCenter(
+ aboutMessage.document.getElementById("attachmentName"),
+ {},
+ aboutMessage
+ );
+}
+
+// Other test boilerplate should initialize a message with attachment; here we
+// verify that it was created and return an nsIFile handle to it.
+async function verifyAndFetchSavedAttachment(parentPath = savePath, leafName) {
+ let expectedFile = getNsIFileFromPath(parentPath);
+ if (leafName) {
+ expectedFile.append(leafName);
+ } else {
+ expectedFile.append(`attachment${messageIndex}.test${messageIndex}`);
+ }
+ await TestUtils.waitForCondition(
+ () => expectedFile.exists(),
+ `attachment was not saved to ${expectedFile.path}`
+ );
+ Assert.ok(expectedFile.exists(), `${expectedFile.path} exists`);
+
+ // Wait a moment in case the file is still locked for writing.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 250));
+
+ return expectedFile;
+}
+
+function checkHandler(type, preferredAction, alwaysAskBeforeHandling) {
+ let handlerInfo = mimeService.getFromTypeAndExtension(type, null);
+ Assert.equal(
+ handlerInfo.preferredAction,
+ preferredAction,
+ `preferredAction of ${type}`
+ );
+ Assert.equal(
+ handlerInfo.alwaysAskBeforeHandling,
+ alwaysAskBeforeHandling,
+ `alwaysAskBeforeHandling of ${type}`
+ );
+}
+
+function promiseFileOpened() {
+ let __openFile = aboutMessage.AttachmentInfo.prototype._openFile;
+ return new Promise(resolve => {
+ aboutMessage.AttachmentInfo.prototype._openFile = function (
+ mimeInfo,
+ file
+ ) {
+ aboutMessage.AttachmentInfo.prototype._openFile = __openFile;
+ resolve({ mimeInfo, file });
+ };
+ });
+}
+
+/**
+ * Check that the directory for saving is correct.
+ * If not, we're gonna have a bad time.
+ */
+add_task(async function sanityCheck() {
+ Assert.equal(
+ await Downloads.getPreferredDownloadsDirectory(),
+ savePath,
+ "sanity check: correct downloads directory"
+ );
+});
+
+// First, check content types we have no saved information about.
+
+/**
+ * Open a content type we've never seen before. Save, and remember the action.
+ */
+add_task(async function noHandler() {
+ await createAndLoadMessage("test/foo");
+ await singleClickAttachmentAndWaitForDialog(
+ { rememberExpected: false, remember: true },
+ "accept"
+ );
+ let file = await verifyAndFetchSavedAttachment();
+ file.remove(false);
+ checkHandler("test/foo", Ci.nsIHandlerInfo.saveToDisk, false);
+});
+
+/**
+ * Open a content type we've never seen before. Save, and DON'T remember the
+ * action (except that we do remember it, but also remember to ask next time).
+ */
+add_task(async function noHandlerNoSave() {
+ await createAndLoadMessage("test/bar");
+ await singleClickAttachmentAndWaitForDialog(
+ { rememberExpected: false, remember: false },
+ "accept"
+ );
+ let file = await verifyAndFetchSavedAttachment();
+ file.remove(false);
+ checkHandler("test/bar", Ci.nsIHandlerInfo.saveToDisk, true);
+});
+
+/**
+ * The application/octet-stream type is handled weirdly. Check that opening it
+ * still behaves in a useful way.
+ */
+add_task(async function applicationOctetStream() {
+ await createAndLoadMessage("application/octet-stream");
+ await singleClickAttachmentAndWaitForDialog(
+ { rememberExpected: false },
+ "accept"
+ );
+ let file = await verifyAndFetchSavedAttachment();
+ file.remove(false);
+});
+
+// Now we'll test the various states that handler info objects might be in.
+// There's two fields: preferredAction and alwaysAskBeforeHandling. If the
+// latter is true, we MUST get a prompt. Check that first.
+
+/**
+ * Open a content type set to save to disk, but always ask.
+ */
+add_task(async function saveToDiskAlwaysAsk() {
+ createMockedHandler(
+ "test/saveToDisk-true",
+ Ci.nsIHandlerInfo.saveToDisk,
+ true
+ );
+ await createAndLoadMessage("test/saveToDisk-true");
+ await singleClickAttachmentAndWaitForDialog(
+ { rememberExpected: false },
+ "accept"
+ );
+ let file = await verifyAndFetchSavedAttachment();
+ file.remove(false);
+ checkHandler("test/saveToDisk-true", Ci.nsIHandlerInfo.saveToDisk, true);
+});
+
+/**
+ * Open a content type set to save to disk, but always ask, and with no
+ * default download directory.
+ */
+add_task(async function saveToDiskAlwaysAskPromptLocation() {
+ Services.prefs.setBoolPref("browser.download.useDownloadDir", false);
+
+ createMockedHandler(
+ "test/saveToDisk-true",
+ Ci.nsIHandlerInfo.saveToDisk,
+ true
+ );
+ await createAndLoadMessage("test/saveToDisk-true");
+
+ let expectedFile = getNsIFileFromPath(tmpD);
+ expectedFile.append(`attachment${messageIndex}.test${messageIndex}`);
+ MockFilePicker.showCallback = function (instance) {
+ Assert.equal(instance.defaultString, expectedFile.leafName);
+ Assert.equal(instance.defaultExtension, `test${messageIndex}`);
+ };
+ MockFilePicker.setFiles([expectedFile]);
+ MockFilePicker.returnValue = Ci.nsIFilePicker.returnOK;
+
+ await singleClickAttachmentAndWaitForDialog(
+ { rememberExpected: false },
+ "accept"
+ );
+ let file = await verifyAndFetchSavedAttachment(tmpD);
+ file.remove(false);
+ Assert.ok(MockFilePicker.shown, "file picker was shown");
+
+ MockFilePicker.reset();
+ Services.prefs.setBoolPref("browser.download.useDownloadDir", true);
+});
+
+/**
+ * Open a content type set to always ask in both fields.
+ */
+add_task(async function alwaysAskAlwaysAsk() {
+ createMockedHandler("test/alwaysAsk-true", Ci.nsIHandlerInfo.alwaysAsk, true);
+ await createAndLoadMessage("test/alwaysAsk-true");
+ await singleClickAttachmentAndWaitForDialog({
+ mode: IMPROVEMENTS_PREF_SET ? "save" : "open",
+ rememberExpected: false,
+ });
+});
+
+/**
+ * Open a content type set to use helper app, but always ask.
+ */
+add_task(async function useHelperAppAlwaysAsk() {
+ createMockedHandler(
+ "test/useHelperApp-true",
+ Ci.nsIHandlerInfo.useHelperApp,
+ true
+ );
+ await createAndLoadMessage("test/useHelperApp-true");
+ await singleClickAttachmentAndWaitForDialog({
+ mode: "open",
+ rememberExpected: false,
+ });
+});
+
+/*
+ * Open a detached attachment with content type set to use helper app, but
+ * always ask.
+ */
+add_task(async function detachedUseHelperAppAlwaysAsk() {
+ const mimeType = "test/useHelperApp-true";
+ let openedPromise = promiseFileOpened();
+
+ createMockedHandler(mimeType, Ci.nsIHandlerInfo.useHelperApp, true);
+
+ // Generate an email with detached attachment.
+ await createAndLoadMessage(mimeType, { isDetached: true });
+ await singleClickAttachmentAndWaitForDialog(
+ { mode: "open", rememberExpected: false },
+ "accept"
+ );
+
+ let expectedPath = PathUtils.join(
+ homeDirectory,
+ `attachment${messageIndex}.test${messageIndex}`
+ );
+
+ let { file } = await openedPromise;
+ Assert.equal(
+ file.path,
+ expectedPath,
+ "opened file should match attachment path"
+ );
+
+ file.remove(false);
+});
+
+/**
+ * Open a content type set to use the system default app, but always ask.
+ */
+add_task(async function useSystemDefaultAlwaysAsk() {
+ createMockedHandler(
+ "test/useSystemDefault-true",
+ Ci.nsIHandlerInfo.useSystemDefault,
+ true
+ );
+ await createAndLoadMessage("test/useSystemDefault-true");
+ // Would be mode: "open" on all platforms except our handler isn't real.
+ await singleClickAttachmentAndWaitForDialog({
+ mode: AppConstants.platform == "win" ? "open" : "save",
+ rememberExpected: false,
+ });
+});
+
+// Check what happens with alwaysAskBeforeHandling set to false. We can't test
+// the actions that would result in an external app opening the file.
+
+/**
+ * Open a content type set to save to disk without asking.
+ */
+add_task(async function saveToDisk() {
+ createMockedHandler("test/saveToDisk-false", saveToDisk, false);
+ await createAndLoadMessage("test/saveToDisk-false");
+ await singleClickAttachment();
+ let file = await verifyAndFetchSavedAttachment();
+ file.remove(false);
+});
+
+/**
+ * Open a content type set to save to disk without asking, and with no
+ * default download directory.
+ */
+add_task(async function saveToDiskPromptLocation() {
+ Services.prefs.setBoolPref("browser.download.useDownloadDir", false);
+
+ createMockedHandler(
+ "test/saveToDisk-true",
+ Ci.nsIHandlerInfo.saveToDisk,
+ false
+ );
+ await createAndLoadMessage("test/saveToDisk-false");
+
+ let expectedFile = getNsIFileFromPath(tmpD);
+ expectedFile.append(`attachment${messageIndex}.test${messageIndex}`);
+ MockFilePicker.showCallback = function (instance) {
+ Assert.equal(instance.defaultString, expectedFile.leafName);
+ Assert.equal(instance.defaultExtension, `test${messageIndex}`);
+ };
+ MockFilePicker.setFiles([expectedFile]);
+ MockFilePicker.returnValue = Ci.nsIFilePicker.returnOK;
+
+ await singleClickAttachment();
+ let file = await verifyAndFetchSavedAttachment(tmpD);
+ file.remove(false);
+ Assert.ok(MockFilePicker.shown, "file picker was shown");
+
+ MockFilePicker.reset();
+ Services.prefs.setBoolPref("browser.download.useDownloadDir", true);
+});
+
+/**
+ * Open a content type set to always ask without asking (weird but plausible).
+ * Check the action is saved and the "do this automatically" checkbox works.
+ */
+add_task(async function alwaysAskRemember() {
+ createMockedHandler(
+ "test/alwaysAsk-false",
+ Ci.nsIHandlerInfo.alwaysAsk,
+ false
+ );
+ await createAndLoadMessage("test/alwaysAsk-false");
+ await singleClickAttachmentAndWaitForDialog(undefined, "accept");
+ let file = await verifyAndFetchSavedAttachment();
+ file.remove(false);
+ checkHandler("test/alwaysAsk-false", Ci.nsIHandlerInfo.saveToDisk, false);
+}).__skipMe = !IMPROVEMENTS_PREF_SET;
+
+/**
+ * Open a content type set to always ask without asking (weird but plausible).
+ * Check the action is saved and the unticked "do this automatically" leaves
+ * alwaysAskBeforeHandling set.
+ */
+add_task(async function alwaysAskForget() {
+ createMockedHandler(
+ "test/alwaysAsk-false",
+ Ci.nsIHandlerInfo.alwaysAsk,
+ false
+ );
+ await createAndLoadMessage("test/alwaysAsk-false");
+ await singleClickAttachmentAndWaitForDialog({ remember: false }, "accept");
+ let file = await verifyAndFetchSavedAttachment();
+ file.remove(false);
+ checkHandler("test/alwaysAsk-false", Ci.nsIHandlerInfo.saveToDisk, true);
+}).__skipMe = !IMPROVEMENTS_PREF_SET;
+
+/**
+ * Open a content type set to use helper app.
+ */
+add_task(async function useHelperApp() {
+ let openedPromise = promiseFileOpened();
+
+ createMockedHandler(
+ "test/useHelperApp-false",
+ Ci.nsIHandlerInfo.useHelperApp,
+ false
+ );
+ await createAndLoadMessage("test/useHelperApp-false");
+ await singleClickAttachment();
+ let attachmentFile = await verifyAndFetchSavedAttachment(tmpD);
+
+ let { file } = await openedPromise;
+ Assert.ok(file.path);
+
+ // In the temp dir, files should be read-only.
+ if (AppConstants.platform != "win") {
+ let fileInfo = await IOUtils.stat(file.path);
+ Assert.equal(
+ fileInfo.permissions,
+ 0o400,
+ `file ${file.path} should be read-only`
+ );
+ }
+ attachmentFile.permissions = 0o755;
+ attachmentFile.remove(false);
+});
+
+/*
+ * Open a detached attachment with content type set to use helper app.
+ */
+add_task(async function detachedUseHelperApp() {
+ const mimeType = "test/useHelperApp-false";
+ let openedPromise = promiseFileOpened();
+
+ createMockedHandler(mimeType, Ci.nsIHandlerInfo.useHelperApp, false);
+
+ // Generate an email with detached attachment.
+ await createAndLoadMessage(mimeType, { isDetached: true });
+ await singleClickAttachment();
+
+ let expectedPath = PathUtils.join(
+ homeDirectory,
+ `attachment${messageIndex}.test${messageIndex}`
+ );
+
+ let { file } = await openedPromise;
+ Assert.equal(
+ file.path,
+ expectedPath,
+ "opened file should match attachment path"
+ );
+
+ file.remove(false);
+});
+
+/**
+ * Open a content type set to use the system default app.
+ */
+add_task(async function useSystemDefault() {
+ let openedPromise = promiseFileOpened();
+
+ createMockedHandler(
+ "test/useSystemDefault-false",
+ Ci.nsIHandlerInfo.useSystemDefault,
+ false
+ );
+ await createAndLoadMessage("test/useSystemDefault-false");
+ await singleClickAttachment();
+ let attachmentFile = await verifyAndFetchSavedAttachment(tmpD);
+ let { file } = await openedPromise;
+ Assert.ok(file.path);
+
+ // In the temp dir, files should be read-only.
+ if (AppConstants.platform != "win") {
+ let fileInfo = await IOUtils.stat(file.path);
+ Assert.equal(
+ fileInfo.permissions,
+ 0o400,
+ `file ${file.path} should be read-only`
+ );
+ }
+ attachmentFile.permissions = 0o755;
+ attachmentFile.remove(false);
+});
+
+/*
+ * Open a detached attachment with content type set to use the system default
+ * app.
+ */
+add_task(async function detachedUseSystemDefault() {
+ const mimeType = "test/useSystemDefault-false";
+ let openedPromise = promiseFileOpened();
+
+ createMockedHandler(mimeType, Ci.nsIHandlerInfo.useSystemDefault, false);
+
+ // Generate an email with detached attachment.
+ await createAndLoadMessage(mimeType, { isDetached: true });
+ await singleClickAttachment();
+
+ let expectedPath = PathUtils.join(
+ homeDirectory,
+ `attachment${messageIndex}.test${messageIndex}`
+ );
+
+ let { file } = await openedPromise;
+ Assert.equal(
+ file.path,
+ expectedPath,
+ "opened file should match attachment path"
+ );
+
+ file.remove(false);
+});
+
+/**
+ * Save an attachment with characters that are illegal in a file name.
+ * Check the characters are sanitized.
+ */
+add_task(async function filenameSanitisedSave() {
+ createMockedHandler("test/bar", Ci.nsIHandlerInfo.saveToDisk, false);
+
+ // Colon, slash and backslash are escaped on all platforms.
+ // Backslash is double-escaped here because of the message generator.
+ await createAndLoadMessage("test/bar", { filename: "f:i\\\\le/123.bar" });
+ await singleClickAttachment();
+ let file = await verifyAndFetchSavedAttachment(undefined, "f i_le_123.bar");
+ file.remove(false);
+
+ // Asterisk, question mark, pipe and angle brackets are escaped on Windows.
+ await createAndLoadMessage("test/bar", { filename: "f*i?|le<123>.bar" });
+ await singleClickAttachment();
+ file = await verifyAndFetchSavedAttachment(undefined, "f i le 123 .bar");
+ file.remove(false);
+});
+
+/**
+ * Open an attachment with characters that are illegal in a file name.
+ * Check the characters are sanitized.
+ */
+add_task(async function filenameSanitisedOpen() {
+ createMockedHandler("test/bar", Ci.nsIHandlerInfo.useHelperApp, false);
+
+ let openedPromise = promiseFileOpened();
+
+ // Colon, slash and backslash are escaped on all platforms.
+ // Backslash is double-escaped here because of the message generator.
+ await createAndLoadMessage("test/bar", { filename: "f:i\\\\le/123.bar" });
+ await singleClickAttachment();
+ let { file } = await openedPromise;
+ let attachmentFile = await verifyAndFetchSavedAttachment(
+ tmpD,
+ "f i_le_123.bar"
+ );
+ Assert.equal(file.leafName, "f i_le_123.bar");
+ // In the temp dir, files should be read-only.
+ if (AppConstants.platform != "win") {
+ let fileInfo = await IOUtils.stat(file.path);
+ Assert.equal(
+ fileInfo.permissions,
+ 0o400,
+ `file ${file.path} should be read-only`
+ );
+ }
+ attachmentFile.permissions = 0o755;
+ attachmentFile.remove(false);
+
+ openedPromise = promiseFileOpened();
+
+ // Asterisk, question mark, pipe and angle brackets are escaped on Windows.
+ await createAndLoadMessage("test/bar", { filename: "f*i?|le<123>.bar" });
+ await singleClickAttachment();
+ ({ file } = await openedPromise);
+ attachmentFile = await verifyAndFetchSavedAttachment(tmpD, "f i le 123 .bar");
+ Assert.equal(file.leafName, "f i le 123 .bar");
+ attachmentFile.permissions = 0o755;
+ attachmentFile.remove(false);
+});
diff --git a/comm/mail/test/browser/attachment/data/attachment.txt b/comm/mail/test/browser/attachment/data/attachment.txt
new file mode 100644
index 0000000000..385b5b2c95
--- /dev/null
+++ b/comm/mail/test/browser/attachment/data/attachment.txt
@@ -0,0 +1 @@
+This is a test attachment! It sure is exciting!
diff --git a/comm/mail/test/browser/attachment/data/bug1358565.eml b/comm/mail/test/browser/attachment/data/bug1358565.eml
new file mode 100644
index 0000000000..a2cf644898
--- /dev/null
+++ b/comm/mail/test/browser/attachment/data/bug1358565.eml
@@ -0,0 +1,62 @@
+Date: Thu, 22 Feb 2017 10:00:00 -0300
+From: <nobody@example.com>
+MIME-Version: 1.0
+To: <nobody2@example.com>
+Content-Type: multipart/alternative; boundary="------alternative"
+Subject: thunderbird bug
+
+
+--------alternative
+Content-Type: text/plain;
+ charset=US-ASCII;
+ format=flowed
+Content-Transfer-Encoding: 7bit
+
+text plain part.
+
+--------alternative
+Content-Type: multipart/related;
+ boundary="------related";
+ type="text/html"
+
+--------related
+Content-Type: text/html;
+ charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+
+<html><body>HTML part<br><img src="cid:bug.png"></body></html>
+--------related
+Content-Disposition: inline;
+ filename=bug.png
+Content-Transfer-Encoding: base64
+Content-Type: image/jpeg;
+ name="bug.png"
+Content-Id: <bug.png>
+
+iVBORw0KGgoAAAANSUhEUgAAAHYAAABNCAYAAABzGpB/AAAABGdBTUEAALGPC/xhBQAAAAFzUkdC
+AK7OHOkAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAS/SURBVHja7Zy/TexAEIdPJKQkhDQAJRBQAhk5
+GQWQ0QARBSBRAVRAAQRkpERIFEAJfvqeZOnOzHj/2l4fv5+00ruz18/M55kd787eppP2UhuZQGAl
+gZUEVhJYSWAFVib4o2BPTk66zWaz056fn2U5gZUEVhJYSWAFVmAFVpYTWElgJYGVBFZgBVZgZTmB
+lVYP9v39vbu9ve1OT0+7w8PDnT585loc57y/pM/Pz+7h4aG7urrqjo+Pd+xycXHR3dzcdK+vr93P
+z09bYL++vrrz8/Nf5401zs8FbN3T9fV1kSH4m4bX5P8pEbDOzs6ibQL0l5eXXUDGeR8fH9ODfXx8
+7A4ODpKg9o1+9/f3ewcWz8M7c2xCo2/vvYuABUruzW+3VLgtgyXsDsNtTiNELwLWMgQeSIjFyP05
+QLu8vPw15pYkZK2CxcvGoHLs7u6ue3p6+g+HRujlO6sf4/KsYIfhl39jWMbaMQHZC9t8H+rfOli8
+zAPKeBsSkGO8fTKw23COjo6ibno70aKPdcOxcFoEixd6IfX7+7tqKJ8M7DbgnMwWuJbnxnpta2C9
+EExGnPMKE4I7OdiSiQov8eI9d21gPW+NAeDp7e1tGbAkSaWyEqoYY7YG1npXJSEqlTdmTwo2ZVz1
+hHda114TWMKm9Tfwfamw8axg8bQaYnzOCfEtgbXCMB5cQyRds4KtEYZ7WUlUaJxtCaw1w8T7Z7U5
+3znBlhqx9PotgbWyVxKfVYKtuWy3drC1s2GBbQCslzjVlMAuABYDC6zACqzA7gFYa0FAYPcA7Jon
+KLzkabVZccxEfa4xaaEVoynA0r/W606N6dbSBycLLBURNcR1cqYrp7gnqxgvBuxUCwCrniu2Vndi
+pitzvSv1XmKuCUSrYqKGvIK4SVd3SsdZbz2WspuQvFWh2NKa2CEhBqznVaXh2AvDk4Mlm80VACwP
+iY0EHoicUtaxvzEGLBUSVt/c6omQt85SQZG7yuMVl6eAsVaFeDBSvdbz/pTwboXjkrHWq06cDWwO
+XA9qagTwrpNyP6G66Fiw3rppDlyvzGb2KsXeAKFXFI6PPSSpY5IXjmnsHRrzXI4NEzD6TFGl2Ff2
+hyoVOT4Mv4vXFW97HQbjeF8szmev3LR0bLRgDGFtF68TdvFo68GsUVcc2tZBDRMPAOu1wOFh5rNV
+28R33vg96cwTN5W7Z6cG1N7zQg9NqNGf69TaCeAVoKVu71h0704JXPrFvNqERIjPhdtDzZ1S9OCy
+LbLGhqxF54oxbCgkWvt7ct85Pc+1ZrHG2nBLSu1tlITblC2UnDvcQjkp2BTPwVgYwxrDgEnYrQnU
+Ajy28ZrvOT7lPViA+03PlneSNY/VSC0OVppGucuCAtuwStZ7BbZhMeZa4VtgVy4ru44tRhfYRuVN
+TsQWowtso7KmKFPWeQW2QXlrsSkLCgLbYAj2piVTfvJAYCuJbLV0MxavN95PFKTu4BPYyhMJTA0C
+IaUEldDrLdYPFwUEdiGw1nsnoEmGho3XmdB8Mh6cszteYCcGW7p8l/uTBwLbKFi8vKQYTmAriYyV
+8JqyVOf9JF9K9iuwM0OmCKFfrvNgc4zW/7Zi1QgiDHs6NMgEAisJrCSwksBK8foHNXX2LMmMFTgA
+AAAASUVORK5CYII=
+--------related--
+
+--------alternative--