diff options
Diffstat (limited to 'comm/mail/test/browser/attachment')
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--
|