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:
Родитель
aa4062195b
Коммит
646ba07646
|
@ -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);
|
||||
|
|
Загрузка…
Ссылка в новой задаче