summaryrefslogtreecommitdiffstats
path: root/comm/mail/test/browser/attachment/browser_openAttachment.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/test/browser/attachment/browser_openAttachment.js')
-rw-r--r--comm/mail/test/browser/attachment/browser_openAttachment.js738
1 files changed, 738 insertions, 0 deletions
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);
+});