From 646ba076467d965daf28906dcb22fd3a8a27f7b2 Mon Sep 17 00:00:00 2001 From: John Bieling Date: Thu, 25 Aug 2022 20:40:20 +1000 Subject: [PATCH] Bug 1644038 - Fix messageDisplay API, tabs API and messages API to properly handle external emails. r=darktrojan Differential Revision: https://phabricator.services.mozilla.com/D148120 --HG-- extra : amend_source : a060617f34f94498ab264741e81d63afb772b8ea --- mail/base/content/mailWindowOverlay.js | 6 +- mail/base/content/msgHdrView.js | 11 +- .../components/extensions/parent/.eslintrc.js | 1 + mail/components/extensions/parent/ext-mail.js | 256 ++++++++++-- .../extensions/parent/ext-messageDisplay.js | 40 +- .../extensions/parent/ext-messages.js | 336 +++++++++------- .../extensions/schemas/compose.json | 2 +- .../extensions/schemas/messageDisplay.json | 6 +- .../extensions/schemas/messages.json | 19 +- .../extensions/test/browser/browser.ini | 2 + .../browser/browser_ext_message_external.js | 366 ++++++++++++++++++ .../messages/attachedMessageSample.eml | 186 +++++++++ .../test/xpcshell/test_ext_messages.js | 11 +- mailnews/base/src/nsMessenger.cpp | 13 +- mailnews/db/gloda/modules/MimeMessage.jsm | 19 +- mailnews/imap/src/nsImapService.cpp | 35 ++ 16 files changed, 1112 insertions(+), 197 deletions(-) create mode 100644 mail/components/extensions/test/browser/browser_ext_message_external.js create mode 100644 mail/components/extensions/test/browser/messages/attachedMessageSample.eml diff --git a/mail/base/content/mailWindowOverlay.js b/mail/base/content/mailWindowOverlay.js index be69f67db6..554844f17f 100644 --- a/mail/base/content/mailWindowOverlay.js +++ b/mail/base/content/mailWindowOverlay.js @@ -3917,13 +3917,17 @@ function OnMsgParsed(aUrl) { } function OnMsgLoaded(aUrl) { - if (!aUrl || gMessageDisplay.isDummy) { + if (!aUrl) { return; } var msgHdr = gMessageDisplay.displayedMessage; window.dispatchEvent(new CustomEvent("MsgLoaded", { detail: msgHdr })); + if (gMessageDisplay.isDummy) { + return; + } + let win = location.href == "about:message" ? window.browsingContext.topChromeWindow diff --git a/mail/base/content/msgHdrView.js b/mail/base/content/msgHdrView.js index b8cb8d3499..d589a2f478 100644 --- a/mail/base/content/msgHdrView.js +++ b/mail/base/content/msgHdrView.js @@ -3071,6 +3071,12 @@ function nsDummyMsgHeader() {} nsDummyMsgHeader.prototype = { mProperties: [], + getProperty(aProperty) { + return this.getStringProperty(aProperty); + }, + setProperty(aProperty, aVal) { + return this.setStringProperty(aProperty, aVal); + }, getStringProperty(aProperty) { if (aProperty in this.mProperties) { return this.mProperties[aProperty]; @@ -3091,7 +3097,6 @@ nsDummyMsgHeader.prototype = { }, markHasAttachments(hasAttachments) {}, messageSize: 0, - recipients: null, author: null, get mime2DecodedAuthor() { return this.author; @@ -3100,6 +3105,10 @@ nsDummyMsgHeader.prototype = { get mime2DecodedSubject() { return this.subject; }, + recipients: null, + get mime2DecodedRecipients() { + return this.recipients; + }, ccList: null, listPost: null, messageId: null, diff --git a/mail/components/extensions/parent/.eslintrc.js b/mail/components/extensions/parent/.eslintrc.js index c16692a524..f5037c2680 100644 --- a/mail/components/extensions/parent/.eslintrc.js +++ b/mail/components/extensions/parent/.eslintrc.js @@ -51,6 +51,7 @@ module.exports = { traverseSubfolders: true, convertMailIdentity: true, convertMessage: true, + convertMessageOrAttachedMessage: true, folderPathToURI: true, folderURIToPath: true, getNormalWindowReady: true, diff --git a/mail/components/extensions/parent/ext-mail.js b/mail/components/extensions/parent/ext-mail.js index f1b62cc83f..8b6e1ae40e 100644 --- a/mail/components/extensions/parent/ext-mail.js +++ b/mail/components/extensions/parent/ext-mail.js @@ -30,6 +30,7 @@ XPCOMUtils.defineLazyPreferenceGetter( "extensions.webextensions.messagesPerPage", 100 ); +XPCOMUtils.defineLazyGlobalGetters(this, ["IOUtils", "PathUtils"]); const COMPOSE_WINDOW_URI = "chrome://messenger/content/messengercompose/messengercompose.xhtml"; @@ -154,7 +155,12 @@ function MsgHdrToRawMessage(msgHdr) { let messenger = Cc["@mozilla.org/messenger;1"].createInstance( Ci.nsIMessenger ); - let msgUri = msgHdr.folder.generateMessageURI(msgHdr.messageKey); + // Messages opened from file or attachments do not have a folder property, but + // have their url stored as a string property. + let msgUri = msgHdr.folder + ? msgHdr.folder.generateMessageURI(msgHdr.messageKey) + : msgHdr.getStringProperty("dummyMsgUrl"); + let service = messenger.messageServiceFromURI(msgUri); return new Promise((resolve, reject) => { let streamlistener = { @@ -1739,6 +1745,96 @@ class FolderManager { } } +/** + * Check if the provided fileUrl points to a valid file. + */ +function isValidFileURL(fileUrl) { + if (!fileUrl) { + return false; + } + try { + return Services.io + .newURI(fileUrl) + .QueryInterface(Ci.nsIFileURL) + .file?.exists(); + } catch (ex) { + return false; + } +} + +/** + * Checks if the provided nsIMsgHdr is a dummy message header of an attached message. + */ +function isAttachedMessage(msgHdr) { + try { + return ( + !msgHdr.folder && + new URL(msgHdr.getStringProperty("dummyMsgUrl")).searchParams.has("part") + ); + } catch (ex) { + return false; + } +} + +/** + * Asynchronous wrapper for convertMessage(), which first checks if the passed + * nsIMsgHdr belongs to an attached message and saves it as a temporary file (if + * not yet done) and uses that for further processing. + */ +async function convertMessageOrAttachedMessage(msgHdr, extension) { + try { + if (isAttachedMessage(msgHdr)) { + let dummyMsgUrl = msgHdr.getStringProperty("dummyMsgUrl"); + let emlFileUrl = messageTracker._attachedMessageUrls.get(dummyMsgUrl); + let rawBinaryString = await MsgHdrToRawMessage(msgHdr); + + if (!isValidFileURL(emlFileUrl)) { + let pathEmlFile = await IOUtils.createUniqueFile( + PathUtils.tempDir, + encodeURIComponent(msgHdr.messageId) + ".eml", + 0o600 + ); + + let emlFile = Cc["@mozilla.org/file/local;1"].createInstance( + Ci.nsIFile + ); + emlFile.initWithPath(pathEmlFile); + let extAppLauncher = Cc[ + "@mozilla.org/uriloader/external-helper-app-service;1" + ].getService(Ci.nsPIExternalAppLauncher); + extAppLauncher.deleteTemporaryFileOnExit(emlFile); + + let buffer = new Uint8Array(rawBinaryString.length); + // rawBinaryString should be a sequence of chars where each char *should* + // use only the lowest 8 bytes. Each charcode value (0...255) is to be + // added to an Uint8Array. Since charCodeAt() may return 16bit values, mask + // them to 8bit. This should not be necessary, but does not harm. + for (let i = 0; i < rawBinaryString.length; i++) { + buffer[i] = rawBinaryString.charCodeAt(i) & 0xff; + } + await IOUtils.write(pathEmlFile, buffer); + + // Attach x-message-display type to make MsgHdrToRawMessage() work with + // the file url. + emlFileUrl = Services.io + .newFileURI(emlFile) + .mutate() + .setQuery("type=application/x-message-display") + .finalize().spec; + messageTracker._attachedMessageUrls.set(dummyMsgUrl, emlFileUrl); + } + + msgHdr.setStringProperty("dummyMsgUrl", emlFileUrl); + //FIXME : msgHdr of attached message has size of main message + msgHdr.setStringProperty("dummyMsgSize", rawBinaryString.length); + } + } catch (ex) { + Cu.reportError(ex); + } + + return convertMessage(msgHdr, extension); +} + /** * Converts an nsIMsgHdr to a simple object for use in messages. * This function WILL change as the API develops. @@ -1749,14 +1845,27 @@ function convertMessage(msgHdr, extension) { return null; } + if (isAttachedMessage(msgHdr)) { + throw new Error( + "Attached messages need to be converted through convertMessageOrAttachedMessage()." + ); + } + let composeFields = Cc[ "@mozilla.org/messengercompose/composefields;1" ].createInstance(Ci.nsIMsgCompFields); + let junkScore = parseInt(msgHdr.getProperty("junkscore"), 10) || 0; + let tags = (msgHdr.getProperty("keywords") || "") + .split(" ") + .filter(MailServices.tags.isValidKey); + + let external = !msgHdr.folder; + let size = msgHdr.getStringProperty("dummyMsgSize") || msgHdr.messageSize; let messageObject = { id: messageTracker.getId(msgHdr), - date: new Date(msgHdr.dateInSeconds * 1000), + date: new Date(Math.round(msgHdr.date / 1000)), author: msgHdr.mime2DecodedAuthor, recipients: composeFields.splitRecipients( msgHdr.mime2DecodedRecipients, @@ -1767,21 +1876,23 @@ function convertMessage(msgHdr, extension) { subject: msgHdr.mime2DecodedSubject, read: msgHdr.isRead, headersOnly: !!(msgHdr.flags & Ci.nsMsgMessageFlags.Partial), - flagged: msgHdr.isFlagged, + flagged: !!msgHdr.isFlagged, junk: junkScore >= gJunkThreshold, junkScore, headerMessageId: msgHdr.messageId, - size: msgHdr.messageSize, + size, + tags, + external, }; // convertMessage can be called without providing an extension, if the info is // needed for multiple extensions. The caller has to ensure that the folder info // is not forwarded to extensions, which do not have the required permission. - if (!extension || extension.hasPermission("accountsRead")) { + if ( + msgHdr.folder && + (!extension || extension.hasPermission("accountsRead")) + ) { messageObject.folder = convertFolder(msgHdr.folder); } - let tags = msgHdr.getProperty("keywords"); - tags = tags ? tags.split(" ") : []; - messageObject.tags = tags.filter(MailServices.tags.isValidKey); return messageObject; } @@ -1800,6 +1911,8 @@ var messageTracker = new (class extends EventEmitter { this._messageIds = new Map(); this._listenerCount = 0; this._pendingKeyChanges = new Map(); + this._attachedMessageUrls = new Map(); + this._dummyMessageHeaders = new Map(); // nsIObserver Services.obs.addObserver(this, "xpcom-shutdown"); @@ -1831,23 +1944,25 @@ var messageTracker = new (class extends EventEmitter { } /** - * Maps the provided message identifiers to the given messageTracker id. + * Maps the provided message identifier to the given messageTracker id. */ - _set(id, folderURI, messageKey) { - let hash = JSON.stringify([folderURI, messageKey]); + _set(id, msgIdentifier, msgHdr) { + let hash = JSON.stringify(msgIdentifier); this._messageIds.set(hash, id); - this._messages.set(id, { - folderURI, - messageKey, - }); + this._messages.set(id, msgIdentifier); + // Keep track of dummy message headers, which do not have a folderURI property + // and cannot be retrieved later. + if (msgHdr && !msgHdr.folderURI) { + this._dummyMessageHeaders.set(id, msgHdr); + } } /** - * Lookup the messageTracker id for the given message identifiers, return null + * Lookup the messageTracker id for the given message identifier, return null * if not known. */ - _get(folderURI, messageKey) { - let hash = JSON.stringify([folderURI, messageKey]); + _get(msgIdentifier) { + let hash = JSON.stringify(msgIdentifier); if (this._messageIds.has(hash)) { return this._messageIds.get(hash); } @@ -1855,13 +1970,14 @@ var messageTracker = new (class extends EventEmitter { } /** - * Removes the provided message identifiers from the messageTracker. + * Removes the provided message identifier from the messageTracker. */ - _remove(folderURI, messageKey) { - let hash = JSON.stringify([folderURI, messageKey]); - let id = this._get(folderURI, messageKey); + _remove(msgIdentifier) { + let hash = JSON.stringify(msgIdentifier); + let id = this._get(msgIdentifier); this._messages.delete(id); this._messageIds.delete(hash); + this._dummyMessageHeaders.delete(id); } /** @@ -1869,36 +1985,83 @@ var messageTracker = new (class extends EventEmitter { * @return {int} The messageTracker id of the message */ getId(msgHdr) { - let id = this._get(msgHdr.folder.URI, msgHdr.messageKey); + let msgIdentifier = msgHdr.folder + ? { folderURI: msgHdr.folder.URI, messageKey: msgHdr.messageKey } + : { + dummyMsgUrl: msgHdr.getStringProperty("dummyMsgUrl"), + dummyMsgLastModifiedTime: msgHdr.getUint32Property( + "dummyMsgLastModifiedTime" + ), + }; + + let id = this._get(msgIdentifier); if (id) { return id; } id = this._nextId++; - this._set(id, msgHdr.folder.URI, msgHdr.messageKey); + this._set(id, msgIdentifier, msgHdr); return id; } + /** + * Check if the provided msgIdentifier belongs to a modified file message. + * + * @param {*} msgIdentifier - the msgIdentifier object of the message + * @returns {Boolean} + */ + isModifiedFileMsg(msgIdentifier) { + try { + let file = Services.io + .newURI(msgIdentifier.dummyMsgUrl) + .QueryInterface(Ci.nsIFileURL).file; + if (!file?.exists()) { + throw new Error("File does not exist"); + } + if ( + msgIdentifier.dummyMsgLastModifiedTime && + !!(file.lastModifiedTime ^ msgIdentifier.dummyMsgLastModifiedTime) + ) { + throw new Error("File has been modified"); + } + } catch (ex) { + Cu.reportError(ex); + return true; + } + return false; + } + /** * Retrieves a message from the messageTracker. If the message no longer, * exists it is removed from the messageTracker. * @return {nsIMsgHdr} The identifier of the message */ getMessage(id) { - let value = this._messages.get(id); - if (!value) { + let msgIdentifier = this._messages.get(id); + if (!msgIdentifier) { return null; } - let folder = MailServices.folderLookup.getFolderForURL(value.folderURI); - if (folder) { - let msgHdr = folder.msgDatabase.GetMsgHdrForKey(value.messageKey); - if (msgHdr) { + if (msgIdentifier.folderURI) { + let folder = MailServices.folderLookup.getFolderForURL( + msgIdentifier.folderURI + ); + if (folder) { + let msgHdr = folder.msgDatabase.GetMsgHdrForKey( + msgIdentifier.messageKey + ); + if (msgHdr) { + return msgHdr; + } + } + } else { + let msgHdr = this._dummyMessageHeaders.get(id); + if (msgHdr && !this.isModifiedFileMsg(msgIdentifier)) { return msgHdr; } } - this._remove(value.folderURI, value.messageKey); + this._remove(msgIdentifier); return null; } @@ -2025,10 +2188,16 @@ var messageTracker = new (class extends EventEmitter { this._pendingKeyChanges.set(newKey, oldKey); // Swap tracker entries. - let oldId = this._get(newMsgHdr.folder.URI, oldKey); - let newId = this._get(newMsgHdr.folder.URI, newKey); - this._set(oldId, newMsgHdr.folder.URI, newKey); - this._set(newId, newMsgHdr.folder.URI, oldKey); + let oldId = this._get({ + folderURI: newMsgHdr.folder.URI, + messageKey: oldKey, + }); + let newId = this._get({ + folderURI: newMsgHdr.folder.URI, + messageKey: newKey, + }); + this._set(oldId, { folderURI: newMsgHdr.folder.URI, messageKey: newKey }); + this._set(newId, { folderURI: newMsgHdr.folder.URI, messageKey: oldKey }); } } @@ -2043,10 +2212,16 @@ var messageTracker = new (class extends EventEmitter { data = JSON.parse(data); if (data && data.folderURI && data.oldMessageKey && data.newMessageKey) { - let id = this._get(data.folderURI, data.oldMessageKey); + let id = this._get({ + folderURI: data.folderURI, + messageKey: data.oldMessageKey, + }); if (id) { // Replace tracker entries. - this._set(id, data.folderURI, data.newMessageKey); + this._set(id, { + folderURI: data.folderURI, + messageKey: data.newMessageKey, + }); } } } else if (topic == "xpcom-shutdown") { @@ -2226,6 +2401,13 @@ class MessageManager { return convertMessage(msgHdr, this.extension); } + // Streaming attached messages does not fully work. Use this async wrapper for + // convert(), which reads the content of attached messages, stores them as temp + // files and uses those for later message processing. + convertMessageOrAttachedMessage(msgHdr) { + return convertMessageOrAttachedMessage(msgHdr, this.extension); + } + get(id) { return messageTracker.getMessage(id); } diff --git a/mail/components/extensions/parent/ext-messageDisplay.js b/mail/components/extensions/parent/ext-messageDisplay.js index cde7bfcff1..1152381643 100644 --- a/mail/components/extensions/parent/ext-messageDisplay.js +++ b/mail/components/extensions/parent/ext-messageDisplay.js @@ -5,7 +5,7 @@ var { MailConsts } = ChromeUtils.import("resource:///modules/MailConsts.jsm"); var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); -function getDisplayedMessages(tab, extension) { +async function getDisplayedMessages(tab, extension) { let displayedMessages; if (tab instanceof TabmailTab) { @@ -22,7 +22,7 @@ function getDisplayedMessages(tab, extension) { let result = []; for (let msg of displayedMessages) { - let hdr = convertMessage(msg, extension); + let hdr = await convertMessageOrAttachedMessage(msg, extension); if (hdr) { result.push(hdr); } @@ -91,9 +91,11 @@ this.messageDisplay = class extends ExtensionAPI { let listener = { handleEvent(event) { let win = windowManager.wrapWindow(event.target); - fire.async( - tabManager.convert(win.activeTab.nativeTab), - convertMessage(event.detail, extension) + let tab = tabManager.convert(win.activeTab.nativeTab); + convertMessageOrAttachedMessage(event.detail, extension).then( + msg => { + fire.async(tab, msg); + } ); }, }; @@ -112,8 +114,9 @@ this.messageDisplay = class extends ExtensionAPI { handleEvent(event) { let win = windowManager.wrapWindow(event.target); let tab = tabManager.convert(win.activeTab.nativeTab); - let msgs = getDisplayedMessages(win.activeTab, extension); - fire.async(tab, msgs); + getDisplayedMessages(win.activeTab, extension).then(msgs => { + fire.async(tab, msgs); + }); }, }; @@ -139,13 +142,34 @@ this.messageDisplay = class extends ExtensionAPI { displayedMessage = tab.nativeTab.gMessageDisplay.displayedMessage; } - return convertMessage(displayedMessage, extension); + return convertMessageOrAttachedMessage(displayedMessage, extension); }, async getDisplayedMessages(tabId) { return getDisplayedMessages(tabManager.get(tabId), extension); }, async open(properties) { let msgHdr = getMsgHdr(properties); + if (!msgHdr.folder) { + // If this is a temporary file of an attached message, open the original + // url instead. + let msgUrl = msgHdr.getStringProperty("dummyMsgUrl"); + let attachedMessage = Array.from( + messageTracker._attachedMessageUrls.entries() + ).find(e => e[1] == msgUrl); + if (attachedMessage) { + msgUrl = attachedMessage[0]; + } + + let window = await getNormalWindowReady(context); + let msgWindow = window.openDialog( + "chrome://messenger/content/messageWindow.xhtml", + "_blank", + "all,chrome,dialog=no,status,toolbar", + Services.io.newURI(msgUrl) + ); + return tabManager.convert(msgWindow); + } + let tab; switch (properties.location || getDefaultMessageOpenLocation()) { case "tab": diff --git a/mail/components/extensions/parent/ext-messages.js b/mail/components/extensions/parent/ext-messages.js index 0ba3bb64bf..317d2bf476 100644 --- a/mail/components/extensions/parent/ext-messages.js +++ b/mail/components/extensions/parent/ext-messages.js @@ -84,7 +84,7 @@ function convertAttachment(attachment) { async function getAttachments(msgHdr) { // Use jsmime based MimeParser to read NNTP messages, which are not // supported by MsgHdrToMimeMessage. No encryption support! - if (msgHdr.folder.server.type == "nntp") { + if (msgHdr.folder?.server.type == "nntp") { let raw = await MsgHdrToRawMessage(msgHdr); let mimeMsg = MimeParser.extractMimeMsg(raw, { includeAttachments: true, @@ -110,14 +110,14 @@ this.messages = class extends ExtensionAPI { function collectMessagesInFolders(messageIds) { let folderMap = new DefaultMap(() => new Set()); - for (let id of messageIds) { - let msgHdr = messageTracker.getMessage(id); + for (let messageId of messageIds) { + let msgHdr = messageTracker.getMessage(messageId); if (!msgHdr) { - continue; + throw new ExtensionError(`Message not found: ${messageId}.`); } - let sourceSet = folderMap.get(msgHdr.folder); - sourceSet.add(msgHdr); + let msgHeaderSet = folderMap.get(msgHdr.folder); + msgHeaderSet.add(msgHdr); } return folderMap; @@ -138,55 +138,99 @@ this.messages = class extends ExtensionAPI { let destinationFolder = MailServices.folderLookup.getFolderForURL( destinationURI ); - let folderMap = collectMessagesInFolders(messageIds); - let promises = []; - for (let [sourceFolder, sourceSet] of folderMap.entries()) { - if (sourceFolder == destinationFolder) { - continue; - } - - let messages = [...sourceSet]; - promises.push( - new Promise((resolve, reject) => { - MailServices.copy.copyMessages( - sourceFolder, - messages, - destinationFolder, - isMove, - { - OnStartCopy() {}, - OnProgress(progress, progressMax) {}, - SetMessageKey(key) {}, - GetMessageId(messageId) {}, - OnStopCopy(status) { - if (status == Cr.NS_OK) { - resolve(); - } else { - reject(status); - } - }, - }, - /* msgWindow */ null, - /* allowUndo */ true - ); - }) - ); - } try { + let promises = []; + let folderMap = collectMessagesInFolders(messageIds); + for (let [sourceFolder, msgHeaderSet] of folderMap.entries()) { + if (sourceFolder == destinationFolder) { + continue; + } + let msgHeaders = [...msgHeaderSet]; + + // Special handling for external messages. + if (!sourceFolder) { + if (isMove) { + throw new ExtensionError( + `Operation not permitted for external messages` + ); + } + + for (let msgHdr of msgHeaders) { + let fileUrl = msgHdr.getStringProperty("dummyMsgUrl"); + let file = Services.io + .newURI(fileUrl) + .QueryInterface(Ci.nsIFileURL).file; + promises.push( + new Promise((resolve, reject) => { + MailServices.copy.copyFileMessage( + file, + destinationFolder, + /* msgToReplace */ null, + /* isDraftOrTemplate */ false, + /* aMsgFlags */ Ci.nsMsgMessageFlags.Read, + /* aMsgKeywords */ "", + { + OnStartCopy() {}, + OnProgress(progress, progressMax) {}, + SetMessageKey(key) {}, + GetMessageId(messageId) {}, + OnStopCopy(status) { + if (status == Cr.NS_OK) { + resolve(); + } else { + reject(status); + } + }, + }, + /* msgWindow */ null + ); + }) + ); + } + continue; + } + + // Since the archiver falls back to copy if delete is not supported, + // lets do that here as well. + promises.push( + new Promise((resolve, reject) => { + MailServices.copy.copyMessages( + sourceFolder, + msgHeaders, + destinationFolder, + isMove && sourceFolder.canDeleteMessages, + { + OnStartCopy() {}, + OnProgress(progress, progressMax) {}, + SetMessageKey(key) {}, + GetMessageId(messageId) {}, + OnStopCopy(status) { + if (status == Cr.NS_OK) { + resolve(); + } else { + reject(status); + } + }, + }, + /* msgWindow */ null, + /* allowUndo */ true + ); + }) + ); + } await Promise.all(promises); } catch (ex) { Cu.reportError(ex); - if (isMove) { - throw new ExtensionError(`Unexpected error moving messages: ${ex}`); - } - throw new ExtensionError(`Unexpected error copying messages: ${ex}`); + throw new ExtensionError( + `Error ${isMove ? "moving" : "copying"} message: ${ex.message}` + ); } } async function getMimeMessage(msgHdr) { // Use jsmime based MimeParser to read NNTP messages, which are not // supported by MsgHdrToMimeMessage. No encryption support! - if (msgHdr.folder.server.type == "nntp") { + if (msgHdr.folder?.server.type == "nntp") { try { let raw = await MsgHdrToRawMessage(msgHdr); let mimeMsg = MimeParser.extractMimeMsg(raw, { @@ -331,13 +375,23 @@ this.messages = class extends ExtensionAPI { return messageListTracker.getNextPage(messageList); }, async get(messageId) { - return convertMessage( - messageTracker.getMessage(messageId), - context.extension - ); + let msgHdr = messageTracker.getMessage(messageId); + if (!msgHdr) { + throw new ExtensionError(`Message not found: ${messageId}.`); + } + let messageHeader = convertMessage(msgHdr, context.extension); + if (messageHeader.id != messageId) { + throw new Error( + "Unexpected Error: Returned message does not equal requested message." + ); + } + return messageHeader; }, async getFull(messageId) { let msgHdr = messageTracker.getMessage(messageId); + if (!msgHdr) { + throw new ExtensionError(`Message not found: ${messageId}.`); + } let mimeMsg = await getMimeMessage(msgHdr); if (!mimeMsg) { throw new ExtensionError(`Error reading message ${messageId}`); @@ -350,6 +404,9 @@ this.messages = class extends ExtensionAPI { }, async getRaw(messageId) { let msgHdr = messageTracker.getMessage(messageId); + if (!msgHdr) { + throw new ExtensionError(`Message not found: ${messageId}.`); + } return MsgHdrToRawMessage(msgHdr).catch(() => { throw new ExtensionError(`Error reading message ${messageId}`); }); @@ -369,7 +426,7 @@ this.messages = class extends ExtensionAPI { // Use jsmime based MimeParser to read NNTP messages, which are not // supported by MsgHdrToMimeMessage. No encryption support! - if (msgHdr.folder.server.type == "nntp") { + if (msgHdr.folder?.server.type == "nntp") { let raw = await MsgHdrToRawMessage(msgHdr); let attachment = MimeParser.extractMimeMsg(raw, { includeAttachments: true, @@ -845,43 +902,53 @@ this.messages = class extends ExtensionAPI { return messageListTracker.getNextPage(messageList); }, async update(messageId, newProperties) { - let msgHdr = messageTracker.getMessage(messageId); - if (!msgHdr) { - return; - } - let msgs = [msgHdr]; + try { + let msgHdr = messageTracker.getMessage(messageId); + if (!msgHdr) { + throw new ExtensionError(`Message not found: ${messageId}.`); + } + if (!msgHdr.folder) { + throw new ExtensionError( + `Operation not permitted for external messages` + ); + } - if (newProperties.read !== null) { - msgHdr.folder.markMessagesRead(msgs, newProperties.read); - } - if (newProperties.flagged !== null) { - msgHdr.folder.markMessagesFlagged(msgs, newProperties.flagged); - } - if (newProperties.junk !== null) { - let score = newProperties.junk - ? Ci.nsIJunkMailPlugin.IS_SPAM_SCORE - : Ci.nsIJunkMailPlugin.IS_HAM_SCORE; - msgHdr.folder.setJunkScoreForMessages(msgs, score); - // nsIFolderListener::OnFolderEvent is notified about changes through - // setJunkScoreForMessages(), but does not provide the actual message. - // nsIMsgFolderListener::msgsJunkStatusChanged is notified only by - // nsMsgDBView::ApplyCommandToIndices(). Since it only works on - // selected messages, we cannot use it here. - // Notify msgsJunkStatusChanged() manually. - MailServices.mfn.notifyMsgsJunkStatusChanged(msgs); - } - if (Array.isArray(newProperties.tags)) { - let currentTags = msgHdr.getStringProperty("keywords").split(" "); + let msgs = [msgHdr]; + if (newProperties.read !== null) { + msgHdr.folder.markMessagesRead(msgs, newProperties.read); + } + if (newProperties.flagged !== null) { + msgHdr.folder.markMessagesFlagged(msgs, newProperties.flagged); + } + if (newProperties.junk !== null) { + let score = newProperties.junk + ? Ci.nsIJunkMailPlugin.IS_SPAM_SCORE + : Ci.nsIJunkMailPlugin.IS_HAM_SCORE; + msgHdr.folder.setJunkScoreForMessages(msgs, score); + // nsIFolderListener::OnFolderEvent is notified about changes through + // setJunkScoreForMessages(), but does not provide the actual message. + // nsIMsgFolderListener::msgsJunkStatusChanged is notified only by + // nsMsgDBView::ApplyCommandToIndices(). Since it only works on + // selected messages, we cannot use it here. + // Notify msgsJunkStatusChanged() manually. + MailServices.mfn.notifyMsgsJunkStatusChanged(msgs); + } + if (Array.isArray(newProperties.tags)) { + let currentTags = msgHdr.getStringProperty("keywords").split(" "); - for (let { key: tagKey } of MailServices.tags.getAllTags()) { - if (newProperties.tags.includes(tagKey)) { - if (!currentTags.includes(tagKey)) { - msgHdr.folder.addKeywordsToMessages(msgs, tagKey); + for (let { key: tagKey } of MailServices.tags.getAllTags()) { + if (newProperties.tags.includes(tagKey)) { + if (!currentTags.includes(tagKey)) { + msgHdr.folder.addKeywordsToMessages(msgs, tagKey); + } + } else if (currentTags.includes(tagKey)) { + msgHdr.folder.removeKeywordsFromMessages(msgs, tagKey); } - } else if (currentTags.includes(tagKey)) { - msgHdr.folder.removeKeywordsFromMessages(msgs, tagKey); } } + } catch (ex) { + Cu.reportError(ex); + throw new ExtensionError(`Error updating message: ${ex.message}`); } }, async move(messageIds, destination) { @@ -891,65 +958,72 @@ this.messages = class extends ExtensionAPI { return moveOrCopyMessages(messageIds, destination, false); }, async delete(messageIds, skipTrash) { - let folderMap = collectMessagesInFolders(messageIds); - for (let sourceFolder of folderMap.keys()) { - if (!sourceFolder.canDeleteMessages) { - throw new ExtensionError( - `Unable to delete messages in "${sourceFolder.prettyName}"` + try { + let promises = []; + let folderMap = collectMessagesInFolders(messageIds); + for (let [sourceFolder, msgHeaderSet] of folderMap.entries()) { + if (!sourceFolder) { + throw new ExtensionError( + `Operation not permitted for external messages` + ); + } + if (!sourceFolder.canDeleteMessages) { + throw new ExtensionError( + `Messages in "${sourceFolder.prettyName}" cannot be deleted` + ); + } + promises.push( + new Promise((resolve, reject) => { + sourceFolder.deleteMessages( + [...msgHeaderSet], + /* msgWindow */ null, + /* deleteStorage */ skipTrash, + /* isMove */ false, + { + OnStartCopy() {}, + OnProgress(progress, progressMax) {}, + SetMessageKey(key) {}, + GetMessageId(messageId) {}, + OnStopCopy(status) { + if (status == Cr.NS_OK) { + resolve(); + } else { + reject(status); + } + }, + }, + /* allowUndo */ true + ); + }) ); } - } - let promises = []; - for (let [sourceFolder, sourceSet] of folderMap.entries()) { - promises.push( - new Promise((resolve, reject) => { - sourceFolder.deleteMessages( - [...sourceSet], - /* msgWindow */ null, - /* deleteStorage */ skipTrash, - /* isMove */ false, - { - OnStartCopy() {}, - OnProgress(progress, progressMax) {}, - SetMessageKey(key) {}, - GetMessageId(messageId) {}, - OnStopCopy(status) { - if (status == Cr.NS_OK) { - resolve(); - } else { - reject(status); - } - }, - }, - /* allowUndo */ true - ); - }) - ); - } - try { await Promise.all(promises); } catch (ex) { Cu.reportError(ex); - throw new ExtensionError( - `Unexpected error deleting messages: ${ex}` - ); + throw new ExtensionError(`Error deleting message: ${ex.message}`); } }, async archive(messageIds) { - let messages = []; - for (let id of messageIds) { - let msgHdr = messageTracker.getMessage(id); - if (!msgHdr) { - continue; + try { + let messages = []; + let folderMap = collectMessagesInFolders(messageIds); + for (let [sourceFolder, msgHeaderSet] of folderMap.entries()) { + if (!sourceFolder) { + throw new ExtensionError( + `Operation not permitted for external messages` + ); + } + messages.push(...msgHeaderSet); } - messages.push(msgHdr); + await new Promise(resolve => { + let archiver = new MessageArchiver(); + archiver.oncomplete = resolve; + archiver.archiveMessages(messages); + }); + } catch (ex) { + Cu.reportError(ex); + throw new ExtensionError(`Error archiving message: ${ex.message}`); } - - return new Promise(resolve => { - let archiver = new MessageArchiver(); - archiver.oncomplete = resolve; - archiver.archiveMessages(messages); - }); }, async listTags() { return MailServices.tags diff --git a/mail/components/extensions/schemas/compose.json b/mail/components/extensions/schemas/compose.json index 14f105d6ba..a7a70995c0 100644 --- a/mail/components/extensions/schemas/compose.json +++ b/mail/components/extensions/schemas/compose.json @@ -105,7 +105,7 @@ "overrideDefaultFcc": { "type": "boolean", "optional": true, - "description": "Indicates whether the default fcc setting (defined by the used identity) is being overridden for this message. Setting ``false`` will clear the override. Setting ``true`` will throw an exception, if ``overrideDefaultFccFolder`` is not set as well." + "description": "Indicates whether the default fcc setting (defined by the used identity) is being overridden for this message. Setting ``false`` will clear the override. Setting ``true`` will throw an ``ExtensionError``, if ``overrideDefaultFccFolder`` is not set as well." }, "overrideDefaultFccFolder": { "choices": [ diff --git a/mail/components/extensions/schemas/messageDisplay.json b/mail/components/extensions/schemas/messageDisplay.json index 518d663883..3ca9c9d885 100644 --- a/mail/components/extensions/schemas/messageDisplay.json +++ b/mail/components/extensions/schemas/messageDisplay.json @@ -111,12 +111,12 @@ "type": "integer", "optional": true, "minimum": 1, - "description": "The id of a message to be opened. Will throw, if the provided ``messageId`` is unknown or invalid." + "description": "The id of a message to be opened. Will throw an ``ExtensionError``, if the provided ``messageId`` is unknown or invalid." }, "headerMessageId": { "type": "string", "optional": true, - "description": "The headerMessageId of a message to be opened. Will throw, if the provided ``headerMessageId`` is unknown or invalid." + "description": "The headerMessageId of a message to be opened. Will throw an ``ExtensionError``, if the provided ``headerMessageId`` is unknown or invalid. Not supported for external messages." }, "location": { "type": "string", @@ -125,7 +125,7 @@ "window" ], "optional": true, - "description": "Where to open the message. If not specified, the users preference is honoured." + "description": "Where to open the message. If not specified, the users preference is honoured. Ignored for external messages, which are always opened in a new window." }, "active": { "type": "boolean", diff --git a/mail/components/extensions/schemas/messages.json b/mail/components/extensions/schemas/messages.json index 4614ade3df..0fcbb0f148 100644 --- a/mail/components/extensions/schemas/messages.json +++ b/mail/components/extensions/schemas/messages.json @@ -49,6 +49,10 @@ "date": { "$ref": "extensionTypes.Date" }, + "external": { + "type": "boolean", + "description": "Whether this message is a real message or an external message (opened from a file or from an attachment)." + }, "flagged": { "type": "boolean" }, @@ -70,16 +74,19 @@ "minimum": 1 }, "junk": { - "description": "Not populated for news/nntp messages.", + "description": "Whether the message has been marked as junk. Always ``false`` for news/nntp messages and external messages.", "type": "boolean" }, "junkScore": { "type": "integer", + "description": "The junk score associated with the message. Always ``0`` for news/nntp messages and external messages.", "minimum": 0, "maximum": 100 }, "read": { - "type": "boolean" + "type": "boolean", + "optional": true, + "description": "Whether the message has been marked as read. Not available for external or attached messages." }, "recipients": { "description": "The To recipients. Not populated for news/nntp messages.", @@ -628,7 +635,7 @@ { "name": "update", "type": "function", - "description": "Marks or unmarks a message as junk, read, flagged, or tagged.", + "description": "Marks or unmarks a message as junk, read, flagged, or tagged. Updating external messages will throw an ``ExtensionError``.", "async": true, "parameters": [ { @@ -645,7 +652,7 @@ { "name": "move", "type": "function", - "description": "Moves messages to a specified folder.", + "description": "Moves messages to a specified folder. If the messages cannot be removed from the source folder, they will be copied instead of moved. Moving external messages will throw an ``ExtensionError``.", "async": true, "permissions": [ "accountsRead", @@ -697,7 +704,7 @@ { "name": "delete", "type": "function", - "description": "Deletes messages permanently, or moves them to the trash folder (honoring the account's deletion behavior settings). The ``skipTrash`` parameter allows immediate permanent deletion, bypassing the trash folder.\n**Note**: Consider using :ref:`messages.move` to manually move messages to the account's trash folder, instead of requesting the overly powerful permission to actually delete messages. The account's trash folder can be extracted as follows: includes/messages/getTrash.jsJavaScript", + "description": "Deletes messages permanently, or moves them to the trash folder (honoring the account's deletion behavior settings). Deleting external messages will throw an ``ExtensionError``. The ``skipTrash`` parameter allows immediate permanent deletion, bypassing the trash folder.\n**Note**: Consider using :ref:`messages.move` to manually move messages to the account's trash folder, instead of requesting the overly powerful permission to actually delete messages. The account's trash folder can be extracted as follows: includes/messages/getTrash.jsJavaScript", "async": true, "permissions": [ "messagesDelete" @@ -723,7 +730,7 @@ { "name": "archive", "type": "function", - "description": "Archives messages using the current settings.", + "description": "Archives messages using the current settings. Archiving external messages will throw an ``ExtensionError``.", "async": true, "permissions": [ "messagesMove" diff --git a/mail/components/extensions/test/browser/browser.ini b/mail/components/extensions/test/browser/browser.ini index e0ac7f2ae7..ebebb6d338 100644 --- a/mail/components/extensions/test/browser/browser.ini +++ b/mail/components/extensions/test/browser/browser.ini @@ -59,6 +59,8 @@ tags = contextmenu tags = contextmenu [browser_ext_menus_replace_menu_context.js] tags = contextmenu +[browser_ext_message_external.js] +support-files = messages/attachedMessageSample.eml [browser_ext_messageDisplay.js] [browser_ext_messageDisplayAction.js] [browser_ext_messageDisplayAction_properties.js] diff --git a/mail/components/extensions/test/browser/browser_ext_message_external.js b/mail/components/extensions/test/browser/browser_ext_message_external.js new file mode 100644 index 0000000000..0beb776df8 --- /dev/null +++ b/mail/components/extensions/test/browser/browser_ext_message_external.js @@ -0,0 +1,366 @@ +/* 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/. */ + +var gAccount; +var gFolder; + +add_setup(() => { + gAccount = createAccount(); + let rootFolder = gAccount.incomingServer.rootFolder; + rootFolder.createSubfolder("test0", null); + gFolder = rootFolder.getChildNamed("test0"); + createMessages(gFolder, 5); +}); + +add_task(async function testExternalMessage() { + // Copy eml file into the profile folder, where we can delete it during the test. + let profileDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + profileDir.initWithPath(PathUtils.profileDir); + let messageFile = new FileUtils.File( + getTestFilePath("messages/attachedMessageSample.eml") + ); + messageFile.copyTo(profileDir, "attachedMessageSample.eml"); + + let files = { + "background.js": async () => { + const platform = await browser.runtime.getPlatformInfo(); + const emlData = { + openExternalFileMessage: { + headerMessageId: "sample.eml@mime.sample", + author: "Batman ", + ccList: ["Robin "], + subject: "Attached message with attachments", + attachments: 4, + size: 9754, + external: true, + read: null, + recipients: ["Heinz "], + date: 958796995000, + body: + "This message has one normal attachment and one email attachment", + }, + openExternalAttachedMessage: { + headerMessageId: "sample-attached.eml@mime.sample", + author: "Superman ", + ccList: ["Jimmy "], + subject: "Test message", + attachments: 3, + size: platform.os == "win" ? 6947 : 6825, + external: true, + read: null, + recipients: ["Heinz Müller "], + date: 958606367000, + body: "Die Hasen und die Frösche", + }, + }; + + let [{ displayedFolder }] = await browser.mailTabs.query({ + active: true, + currentWindow: true, + }); + + let foundMessages = []; + + // Open an external file, either from file or via API. + async function openAndVerifyExternalMessage(actionOrMessageId, expected) { + let windowPromise = window.waitForEvent("windows.onCreated"); + let messagePromise = window.waitForEvent( + "messageDisplay.onMessageDisplayed" + ); + + let returnedMsgTab; + if (Number.isInteger(actionOrMessageId)) { + returnedMsgTab = await browser.messageDisplay.open({ + messageId: actionOrMessageId, + }); + } else { + await window.sendMessage(actionOrMessageId); + } + let [msgWindow] = await windowPromise; + let [openedMsgTab, message] = await messagePromise; + + browser.test.assertEq( + openedMsgTab.windowId, + msgWindow.id, + "The opened tab should belong to the correct window" + ); + + if (Number.isInteger(actionOrMessageId)) { + browser.test.assertEq( + returnedMsgTab.windowId, + msgWindow.id, + "The returned tab should belong to the correct window" + ); + } + + // Test the received message and the re-queried message. + for (let msg of [message, await browser.messages.get(message.id)]) { + browser.test.assertEq( + message.id, + msg.id, + "`The opened message should be correct." + ); + browser.test.assertEq( + expected.author, + msg.author, + "The author should be correct" + ); + browser.test.assertEq( + expected.headerMessageId, + msg.headerMessageId, + "The headerMessageId should be correct" + ); + browser.test.assertEq( + expected.subject, + msg.subject, + "The subject should be correct" + ); + browser.test.assertEq( + expected.size, + msg.size, + "The size should be correct" + ); + browser.test.assertEq( + expected.external, + msg.external, + "The external flag should be correct" + ); + browser.test.assertEq( + expected.date, + msg.date.getTime(), + "The date should be correct" + ); + window.assertDeepEqual( + expected.recipients, + msg.recipients, + "The recipients should be correct" + ); + window.assertDeepEqual( + expected.ccList, + msg.ccList, + "The carbon copy recipients should be correct" + ); + } + + let raw = await browser.messages.getRaw(message.id); + browser.test.assertTrue( + raw.startsWith(`Message-ID: <${expected.headerMessageId}>`), + "Raw msg should be correct" + ); + + let full = await browser.messages.getFull(message.id); + browser.test.assertTrue( + full.headers["message-id"].includes(`<${expected.headerMessageId}>`), + "Message-ID of full msg should be correct" + ); + browser.test.assertTrue( + full.parts[0].parts[0].body.includes(expected.body), + "Body of full msg should be correct" + ); + + let attachments = await browser.messages.listAttachments(message.id); + browser.test.assertEq( + expected.attachments, + attachments.length, + "Should find the correct number of attachments" + ); + browser.windows.remove(msgWindow.id); + return message; + } + + for (let action of [ + "openExternalFileMessage", + "openExternalAttachedMessage", + ]) { + let expected = emlData[action]; + + // Open the external message file and check its details. + let extMsgOpenByFile = await openAndVerifyExternalMessage( + action, + expected + ); + + // Open the external message via API and check its details. + await openAndVerifyExternalMessage(extMsgOpenByFile.id, expected); + + // Open the external message file again and check if it returns the same id. + let extMsgOpenByFileAgain = await openAndVerifyExternalMessage( + action, + expected + ); + browser.test.assertEq( + extMsgOpenByFile.id, + extMsgOpenByFileAgain.id, + "Should return the same messageId when opened again" + ); + + // Test copying a file message into Thunderbird. + let { messages: messagesBeforeCopy } = await browser.messages.list( + displayedFolder + ); + await browser.messages.copy([extMsgOpenByFile.id], displayedFolder); + let { messages: messagesAfterCopy } = await browser.messages.list( + displayedFolder + ); + browser.test.assertEq( + messagesBeforeCopy.length + 1, + messagesAfterCopy.length, + "The file message should have been copied into the current folder" + ); + let { messages } = await browser.messages.query({ + folder: displayedFolder, + headerMessageId: expected.headerMessageId, + }); + browser.test.assertTrue( + messages.length == 1, + "A query should find the new copied file message in the current folder" + ); + + // All other operations should fail. + await browser.test.assertRejects( + browser.messages.update(extMsgOpenByFile.id, {}), + `Error updating message: Operation not permitted for external messages`, + "Updating external messages should throw." + ); + + await browser.test.assertRejects( + browser.messages.delete([extMsgOpenByFile.id]), + `Error deleting message: Operation not permitted for external messages`, + "Deleting external messages should throw." + ); + + await browser.test.assertRejects( + browser.messages.archive([extMsgOpenByFile.id]), + `Error archiving message: Operation not permitted for external messages`, + "Archiving external messages should throw." + ); + + await browser.test.assertRejects( + browser.messages.move([extMsgOpenByFile.id], displayedFolder), + `Error moving message: Operation not permitted for external messages`, + "Moving external messages should throw." + ); + + foundMessages[action] = extMsgOpenByFile.id; + } + + // Delete the local eml file to trigger access errors. + let messageId = foundMessages.openExternalFileMessage; + await window.sendMessage(`deleteExternalMessage`); + + await browser.test.assertRejects( + browser.messages.update(messageId, {}), + `Error updating message: Message not found: ${messageId}.`, + "Updating a missing message should throw." + ); + + await browser.test.assertRejects( + browser.messages.delete([messageId]), + `Error deleting message: Message not found: ${messageId}.`, + "Deleting a missing message should throw." + ); + + await browser.test.assertRejects( + browser.messages.archive([messageId]), + `Error archiving message: Message not found: ${messageId}.`, + "Archiving a missing message should throw." + ); + + await browser.test.assertRejects( + browser.messages.move([messageId], displayedFolder), + `Error moving message: Message not found: ${messageId}.`, + "Moving a missing message should throw." + ); + + await browser.test.assertRejects( + browser.messages.copy([messageId], displayedFolder), + `Error copying message: Message not found: ${messageId}.`, + "Copying a missing message should throw." + ); + + await browser.test.assertRejects( + browser.messageDisplay.open({ messageId }), + `Unknown or invalid messageId: ${messageId}.`, + "Opening a missing message should throw." + ); + + browser.test.notifyPass("finished"); + }, + "utils.js": await getUtilsJS(), + }; + let extension = ExtensionTestUtils.loadExtension({ + files, + manifest: { + background: { scripts: ["utils.js", "background.js"] }, + permissions: [ + "accountsRead", + "messagesRead", + "messagesMove", + "messagesDelete", + ], + }, + }); + + window.gFolderTreeView.selectFolder(gFolder); + window.gFolderDisplay.selectViewIndex(0); + + extension.onMessage("openExternalFileMessage", async () => { + let messagePath = PathUtils.join( + PathUtils.profileDir, + "attachedMessageSample.eml" + ); + let messageFile = new FileUtils.File(messagePath); + let url = Services.io + .newFileURI(messageFile) + .mutate() + .setQuery("type=application/x-message-display") + .finalize(); + + window.openDialog( + "chrome://messenger/content/messageWindow.xhtml", + "_blank", + "all,chrome,dialog=no,status,toolbar", + url + ); + extension.sendMessage(); + }); + + extension.onMessage("openExternalAttachedMessage", async () => { + let messagePath = PathUtils.join( + PathUtils.profileDir, + "attachedMessageSample.eml" + ); + let messageFile = new FileUtils.File(messagePath); + let url = Services.io + .newFileURI(messageFile) + .mutate() + .setScheme("mailbox") + .setQuery( + "number=0&part=1.2&filename=sample02.eml&type=application/x-message-display&filename=sample02.eml" + ) + .finalize(); + + window.openDialog( + "chrome://messenger/content/messageWindow.xhtml", + "_blank", + "all,chrome,dialog=no,status,toolbar", + url + ); + extension.sendMessage(); + }); + + extension.onMessage("deleteExternalMessage", async () => { + let messagePath = PathUtils.join( + PathUtils.profileDir, + "attachedMessageSample.eml" + ); + let messageFile = new FileUtils.File(messagePath); + messageFile.remove(false); + extension.sendMessage(); + }); + + await extension.startup(); + await extension.awaitFinish("finished"); + await extension.unload(); +}); diff --git a/mail/components/extensions/test/browser/messages/attachedMessageSample.eml b/mail/components/extensions/test/browser/messages/attachedMessageSample.eml new file mode 100644 index 0000000000..0575e8542c --- /dev/null +++ b/mail/components/extensions/test/browser/messages/attachedMessageSample.eml @@ -0,0 +1,186 @@ +Message-ID: +Date: Fri, 20 May 2000 00:29:55 -0400 +To: Heinz +Cc: Robin +From: Batman +Subject: Attached message with attachments +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------49CVLb1N6p6Spdka4qq7Naeg" + +This is a multi-part message in MIME format. +--------------49CVLb1N6p6Spdka4qq7Naeg +Content-Type: text/html; charset=UTF-8 +Content-Transfer-Encoding: 7bit + + + + + + + +

This message has one normal attachment and one email attachment, + which itself has 3 attachments.
+

+ + +--------------49CVLb1N6p6Spdka4qq7Naeg +Content-Type: message/rfc822; charset=UTF-8; name="sample02.eml" +Content-Disposition: attachment; filename="sample02.eml" +Content-Transfer-Encoding: 7bit + +Message-ID: +From: Superman +To: =?iso-8859-1?Q?Heinz_M=FCller?= +Cc: Jimmy +Subject: Test message +Date: Wed, 17 May 2000 19:32:47 -0400 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_0002_01BFC036.AE309650" +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 + +This is a multi-part message in MIME format. + +------=_NextPart_000_0002_01BFC036.AE309650 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + +Die Hasen und die Fr=F6sche=20 +=20 + +------=_NextPart_000_0002_01BFC036.AE309650 +Content-Type: image/png; + name="blueball1.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="blueball2.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgAAAAA +CCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQ +MYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO +5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaMvee1 +5++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAADBMg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbssceb +L5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18P +yqqWUw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/MjRxm +T6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1CSYoOiMOS +GwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B +1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD +/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZz +O7wAAAAASUVORK5CYII= + +------=_NextPart_000_0002_01BFC036.AE309650 +Content-Type: image/png; + name="greenball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAAAEAAAGAAAIQAA +CAAAMQAAQgAAUgAAWgAASgAIYwAIcwAIewAQjAAIawAAOQAAYwAQlAAQnAAhpQAQpQAhrQBCvRhj +xjFjxjlSxiEpzgAYvQAQrQAYrQAhvQCU1mOt1nuE1lJK3hgh1gAYxgAYtQAAKQBCzhDO55Te563G +55SU52NS5yEh3gAYzgBS3iGc52vW75y974yE71JC7xCt73ul3nNa7ykh5wAY1gAx5wBS7yFr7zlK +7xgp5wAp7wAx7wAIhAAQtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAp1fnZAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAFtSURBVHicddJtV8IgFAdwD2zIgMEE1+NcqdsoK+m5tCyz7/+ZiLmHsyzvq53zO/cy ++N9ery1bVe9PWQA9z4MQ+H8Yoj7GASZ95IHfaBGmLOSchyIgyOu22mgQSjUcDuNYcoGjLiLK1cHh +0fHJaTKKOcMItgYxT89OzsfjyTTLC8UF0c2ZNmKquJhczq6ub+YmSVUYRF59GeDastu7+9nD41Nm +kiJ2jc2J3kAWZ9Pr55fH18XSmRuKUTXUaqHy7O19tfr4NFle/w3YDrWRUIlZrL/W86XJkyJVG9Ea +EjIx2XyZmZJGioeUaL+2AY8TY8omR6nkLKhu70zjUKVJXsp3quS2DVSJWNh3zzJKCyexI0ZxBP3a +fE0ElyqOlZJyw8r3BE2SFiJCyxA434SCkg65RhdeQBljQtCg39LWrA90RDDG1EWrYUO23hMANUKR +Rl61E529cR++D2G5LK002dr/qrcfu9u0V3bxn/XdhR/NYeeN0ggsLAAAACV0RVh0Q29tbWVudABj +bGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII= + +------=_NextPart_000_0002_01BFC036.AE309650 +Content-Type: image/png +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="redball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAVAAAa +AAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACHAAB9AAB0 +AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABM +AAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHfhITm +f3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5PlrKzpmZntZWXvJSXXAADB +AACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2 +AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABH +AAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABPAAASAAAC +AABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADIAADTAADNAACzAACDAABuAAAe +AAB+AADAAACkAACNAAB/AABpAABQAAAwAACRAACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACs +AACvAACtAACmAACJAAB6AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABV +AACOAACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAM +AAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAD8LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu +MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iK +iUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ +29ja2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2YnOAj+ +d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/uXLzVJ2q +m6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqV +tWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZw +HBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/joOyYed5 +QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms1y9evXid7QZacgOxmSxktNzd +tSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5 +IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== + +------=_NextPart_000_0002_01BFC036.AE309650-- +--------------49CVLb1N6p6Spdka4qq7Naeg +Content-Type: image/png; +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="yellowball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgA +AAAACCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQ +MZQAGFIQMYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYY +QsYQMaUAACHO5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9K +e+8YOaUYSsaMvee15++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADB +Mg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAGI +SURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbsscebL5xznTsh +5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18PyqqW +Uw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/M +jRxmT6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1C +SYoOiMOSGwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIom +H3NCKX0lnI+B1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0N +xW62p+lT+Yi747sD/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBi +eSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII= + +--------------49CVLb1N6p6Spdka4qq7Naeg-- diff --git a/mail/components/extensions/test/xpcshell/test_ext_messages.js b/mail/components/extensions/test/xpcshell/test_ext_messages.js index d737a2a33a..0f244c8b04 100644 --- a/mail/components/extensions/test/xpcshell/test_ext_messages.js +++ b/mail/components/extensions/test/xpcshell/test_ext_messages.js @@ -273,6 +273,7 @@ add_task( message = await browser.messages.get(message.id); browser.test.assertFalse(message.flagged); browser.test.assertFalse(message.read); + browser.test.assertFalse(message.external); browser.test.assertFalse(message.junk); browser.test.assertEq(0, message.junkScore); browser.test.assertEq(0, message.tags.length); @@ -606,9 +607,11 @@ add_task( browser.test.log(JSON.stringify(messages)); // 101, 109, 103, 110, 105 // Move a non-existent message. - await browser.messages.move([9999], testFolder1); - await checkMessagesInFolder(["Red", "Blue", "Happy"], testFolder1); - browser.test.log(JSON.stringify(messages)); // 101, 109, 103, 110, 105 + await browser.test.assertRejects( + browser.messages.move([9999], testFolder1), + /Error moving message/, + "something should happen" + ); // Move to a non-existent folder. await browser.test.assertRejects( @@ -616,7 +619,7 @@ add_task( accountId, path: "/missing", }), - /Unexpected error moving messages/, + /Error moving message/, "something should happen" ); diff --git a/mailnews/base/src/nsMessenger.cpp b/mailnews/base/src/nsMessenger.cpp index c1f746569f..9893043348 100644 --- a/mailnews/base/src/nsMessenger.cpp +++ b/mailnews/base/src/nsMessenger.cpp @@ -537,6 +537,7 @@ nsMessenger::LoadURL(mozIDOMWindowProxy* aWin, const nsACString& aURL) { bool loadingFromFile = false; bool getDummyMsgHdr = false; int64_t fileSize; + int64_t lastModifiedTime = 0; if (StringBeginsWith(uriString, u"file:"_ns)) { nsCOMPtr fileUri; @@ -548,6 +549,7 @@ nsMessenger::LoadURL(mozIDOMWindowProxy* aWin, const nsACString& aURL) { rv = fileUrl->GetFile(getter_AddRefs(file)); NS_ENSURE_SUCCESS(rv, rv); file->GetFileSize(&fileSize); + file->GetLastModifiedTime(&lastModifiedTime); uriString.Replace(0, 5, u"mailbox:"_ns); uriString.AppendLiteral(u"&number=0"); loadingFromFile = true; @@ -596,8 +598,15 @@ nsMessenger::LoadURL(mozIDOMWindowProxy* aWin, const nsACString& aURL) { if (headerSink) { nsCOMPtr dummyHeader; headerSink->GetDummyMsgHeader(getter_AddRefs(dummyHeader)); - if (dummyHeader && loadingFromFile) - dummyHeader->SetMessageSize((uint32_t)fileSize); + if (dummyHeader) { + dummyHeader->SetUint32Property("dummyMsgLastModifiedTime", + (uint32_t)lastModifiedTime); + dummyHeader->SetStringProperty("dummyMsgUrl", + PromiseFlatCString(aURL).get()); + if (loadingFromFile) { + dummyHeader->SetMessageSize((uint32_t)fileSize); + } + } } } } diff --git a/mailnews/db/gloda/modules/MimeMessage.jsm b/mailnews/db/gloda/modules/MimeMessage.jsm index 16ec05d1b1..5a9ba45ae7 100644 --- a/mailnews/db/gloda/modules/MimeMessage.jsm +++ b/mailnews/db/gloda/modules/MimeMessage.jsm @@ -56,7 +56,12 @@ var shutdownCleanupObserver = { function CallbackStreamListener(aMsgHdr, aCallbackThis, aCallback) { this._msgHdr = aMsgHdr; - let hdrURI = aMsgHdr.folder.getUriForMsg(aMsgHdr); + // Messages opened from file or attachments do not have a folder property, but + // have their url stored as a string property. + let hdrURI = aMsgHdr.folder + ? aMsgHdr.folder.getUriForMsg(aMsgHdr) + : aMsgHdr.getStringProperty("dummyMsgUrl"); + this._request = null; this._stream = null; if (aCallback === undefined) { @@ -77,7 +82,11 @@ CallbackStreamListener.prototype = { this._request = aRequest; }, onStopRequest(aRequest, aStatusCode) { - let msgURI = this._msgHdr.folder.getUriForMsg(this._msgHdr); + // Messages opened from file or attachments do not have a folder property, + // but have their url stored as a string property. + let msgURI = this._msgHdr.folder + ? this._msgHdr.folder.getUriForMsg(this._msgHdr) + : this._msgHdr.getStringProperty("dummyMsgUrl"); delete activeStreamListeners[msgURI]; aRequest.QueryInterface(Ci.nsIChannel); @@ -182,8 +191,12 @@ function MsgHdrToMimeMessage( shutdownCleanupObserver.ensureInitialized(); let requireOffline = !aAllowDownload; + // Messages opened from file or attachments do not have a folder property, but + // have their url stored as a string property. + let msgURI = aMsgHdr.folder + ? aMsgHdr.folder.getUriForMsg(aMsgHdr) + : aMsgHdr.getStringProperty("dummyMsgUrl"); - let msgURI = aMsgHdr.folder.getUriForMsg(aMsgHdr); let msgService = gMessenger.messageServiceFromURI(msgURI); MsgHdrToMimeMessage.OPTION_TUNNEL = aOptions; diff --git a/mailnews/imap/src/nsImapService.cpp b/mailnews/imap/src/nsImapService.cpp index d245650b3d..0afa944f2e 100644 --- a/mailnews/imap/src/nsImapService.cpp +++ b/mailnews/imap/src/nsImapService.cpp @@ -1049,6 +1049,41 @@ NS_IMETHODIMP nsImapService::StreamMessage( nsAutoCString mimePart; nsAutoCString folderURI; nsMsgKey key; + nsAutoCString messageURI(aMessageURI); + + int32_t typeIndex = messageURI.Find("&type=application/x-message-display"); + if (typeIndex != kNotFound) { + // This happens with forward inline of a message/rfc822 attachment opened in + // a standalone msg window. + // So, just cut to the chase and call AsyncOpen on a channel. + nsCOMPtr uri; + messageURI.Cut(typeIndex, + sizeof("&type=application/x-message-display") - 1); + nsresult rv = NS_NewURI(getter_AddRefs(uri), messageURI.get()); + NS_ENSURE_SUCCESS(rv, rv); + if (aURL) NS_IF_ADDREF(*aURL = uri); + nsCOMPtr aStreamListener = + do_QueryInterface(aConsumer, &rv); + if (NS_SUCCEEDED(rv) && aStreamListener) { + nsCOMPtr aChannel; + nsCOMPtr aLoadGroup; + nsCOMPtr mailnewsUrl = do_QueryInterface(uri, &rv); + if (NS_SUCCEEDED(rv) && mailnewsUrl) + mailnewsUrl->GetLoadGroup(getter_AddRefs(aLoadGroup)); + + nsCOMPtr loadInfo = new mozilla::net::LoadInfo( + nsContentUtils::GetSystemPrincipal(), nullptr, nullptr, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + rv = NewChannel(uri, loadInfo, getter_AddRefs(aChannel)); + NS_ENSURE_SUCCESS(rv, rv); + + // now try to open the channel passing in our display consumer as the + // listener + rv = aChannel->AsyncOpen(aStreamListener); + return rv; + } + } nsresult rv = DecomposeImapURI(aMessageURI, getter_AddRefs(folder), msgKey); NS_ENSURE_SUCCESS(rv, rv);