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
This commit is contained in:
John Bieling 2022-08-25 20:40:20 +10:00
Родитель aa4062195b
Коммит 646ba07646
16 изменённых файлов: 1112 добавлений и 197 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -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,

Просмотреть файл

@ -51,6 +51,7 @@ module.exports = {
traverseSubfolders: true,
convertMailIdentity: true,
convertMessage: true,
convertMessageOrAttachedMessage: true,
folderPathToURI: true,
folderURIToPath: true,
getNormalWindowReady: true,

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -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":

Просмотреть файл

@ -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

Просмотреть файл

@ -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": [

Просмотреть файл

@ -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",

Просмотреть файл

@ -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: <literalinclude>includes/messages/getTrash.js<lang>JavaScript</lang></literalinclude>",
"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: <literalinclude>includes/messages/getTrash.js<lang>JavaScript</lang></literalinclude>",
"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"

Просмотреть файл

@ -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]

Просмотреть файл

@ -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 <bruce@wayne-enterprises.com>",
ccList: ["Robin <damian@wayne-enterprises.com>"],
subject: "Attached message with attachments",
attachments: 4,
size: 9754,
external: true,
read: null,
recipients: ["Heinz <mueller@example.com>"],
date: 958796995000,
body:
"This message has one normal attachment and one email attachment",
},
openExternalAttachedMessage: {
headerMessageId: "sample-attached.eml@mime.sample",
author: "Superman <clark.kent@dailyplanet.com>",
ccList: ["Jimmy <jimmy.Olsen@dailyplanet.com>"],
subject: "Test message",
attachments: 3,
size: platform.os == "win" ? 6947 : 6825,
external: true,
read: null,
recipients: ["Heinz Müller <mueller@examples.com>"],
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();
});

Просмотреть файл

@ -0,0 +1,186 @@
Message-ID: <sample.eml@mime.sample>
Date: Fri, 20 May 2000 00:29:55 -0400
To: Heinz <mueller@example.com>
Cc: Robin <damian@wayne-enterprises.com>
From: Batman <bruce@wayne-enterprises.com>
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
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>
<body>
<p>This message has one normal attachment and one email attachment,
which itself has 3 attachments.<br>
</p>
</body>
</html>
--------------49CVLb1N6p6Spdka4qq7Naeg
Content-Type: message/rfc822; charset=UTF-8; name="sample02.eml"
Content-Disposition: attachment; filename="sample02.eml"
Content-Transfer-Encoding: 7bit
Message-ID: <sample-attached.eml@mime.sample>
From: Superman <clark.kent@dailyplanet.com>
To: =?iso-8859-1?Q?Heinz_M=FCller?= <mueller@examples.com>
Cc: Jimmy <jimmy.Olsen@dailyplanet.com>
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--

Просмотреть файл

@ -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"
);

Просмотреть файл

@ -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<nsIURI> 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<nsIMsgDBHdr> 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);
}
}
}
}
}

Просмотреть файл

@ -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;

Просмотреть файл

@ -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<nsIURI> 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<nsIStreamListener> aStreamListener =
do_QueryInterface(aConsumer, &rv);
if (NS_SUCCEEDED(rv) && aStreamListener) {
nsCOMPtr<nsIChannel> aChannel;
nsCOMPtr<nsILoadGroup> aLoadGroup;
nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(uri, &rv);
if (NS_SUCCEEDED(rv) && mailnewsUrl)
mailnewsUrl->GetLoadGroup(getter_AddRefs(aLoadGroup));
nsCOMPtr<nsILoadInfo> 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);