Bug 1698886 - Make messages.listAttachments() return a MessageHeader for attached messages. r=mkmelin
Differential Revision: https://phabricator.services.mozilla.com/D156580 --HG-- extra : amend_source : 09101c3e57bbbe967faaa411835c99e391cbdbc2
This commit is contained in:
Родитель
df9cdb819c
Коммит
20b508c7ee
|
@ -44,14 +44,12 @@ module.exports = {
|
|||
TabmailTab: true,
|
||||
Window: true,
|
||||
TabmailWindow: true,
|
||||
MsgHdrToRawMessage: true,
|
||||
clickModifiersFromEvent: true,
|
||||
convertFolder: true,
|
||||
convertAccount: true,
|
||||
traverseSubfolders: true,
|
||||
convertMailIdentity: true,
|
||||
convertMessage: true,
|
||||
convertMessageOrAttachedMessage: true,
|
||||
folderPathToURI: true,
|
||||
folderURIToPath: true,
|
||||
getNormalWindowReady: true,
|
||||
|
|
|
@ -182,63 +182,6 @@ async function getRealFileForFile(file) {
|
|||
return tempFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw message for a given msgHdr. This is not using aConvertData
|
||||
* and therefore also works for nntp/news.
|
||||
*
|
||||
* @param aMsgHdr - The message header to retrieve the raw message for.
|
||||
* @return {string} - A Promise for a binary string of the raw message.
|
||||
*/
|
||||
function MsgHdrToRawMessage(msgHdr) {
|
||||
let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
|
||||
Ci.nsIMessenger
|
||||
);
|
||||
// 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 = {
|
||||
_data: [],
|
||||
_stream: null,
|
||||
onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
|
||||
if (!this._stream) {
|
||||
this._stream = Cc[
|
||||
"@mozilla.org/scriptableinputstream;1"
|
||||
].createInstance(Ci.nsIScriptableInputStream);
|
||||
this._stream.init(aInputStream);
|
||||
}
|
||||
this._data.push(this._stream.read(aCount));
|
||||
},
|
||||
onStartRequest() {},
|
||||
onStopRequest(aRequest, aStatus) {
|
||||
if (aStatus == Cr.NS_OK) {
|
||||
resolve(this._data.join(""));
|
||||
} else {
|
||||
Cu.reportError(aStatus);
|
||||
reject();
|
||||
}
|
||||
},
|
||||
QueryInterface: ChromeUtils.generateQI([
|
||||
"nsIStreamListener",
|
||||
"nsIRequestObserver",
|
||||
]),
|
||||
};
|
||||
|
||||
service.streamMessage(
|
||||
msgUri,
|
||||
streamlistener,
|
||||
null, // aMsgWindow
|
||||
null, // aUrlListener
|
||||
false, // aConvertData
|
||||
"" //aAdditionalHeader
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the window for a tabmail tabInfo.
|
||||
*
|
||||
|
@ -1783,23 +1726,6 @@ 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.
|
||||
*/
|
||||
|
@ -1814,65 +1740,6 @@ function isAttachedMessage(msgHdr) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -1883,12 +1750,6 @@ 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);
|
||||
|
@ -1899,7 +1760,13 @@ function convertMessage(msgHdr, extension) {
|
|||
.filter(MailServices.tags.isValidKey);
|
||||
|
||||
let external = !msgHdr.folder;
|
||||
let size = msgHdr.getStringProperty("dummyMsgSize") || msgHdr.messageSize;
|
||||
|
||||
// Getting the size of attached messages does not work consistently. For imap://
|
||||
// and mailbox:// messages the returned size in msgHdr.messageSize is 0, and for
|
||||
// file:// messages the returned size is always the total file size
|
||||
// Be consistent here and always return 0. The user can obtain the message size
|
||||
// from the size of the associated attachment file.
|
||||
let size = isAttachedMessage(msgHdr) ? 0 : msgHdr.messageSize;
|
||||
|
||||
let messageObject = {
|
||||
id: messageTracker.getId(msgHdr),
|
||||
|
@ -1950,7 +1817,6 @@ 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
|
||||
|
@ -1970,6 +1836,22 @@ var messageTracker = new (class extends EventEmitter {
|
|||
MailServices.mfn.msgsMoveCopyCompleted |
|
||||
MailServices.mfn.msgKeyChanged
|
||||
);
|
||||
|
||||
this._messageOpenListener = {
|
||||
async handleEvent(event) {
|
||||
let msgHdr = event.detail;
|
||||
// It is not possible to retrieve the dummyMsgHdr of messages opened
|
||||
// from file at a later time, track them manually.
|
||||
if (
|
||||
msgHdr &&
|
||||
!msgHdr.folder &&
|
||||
msgHdr.getStringProperty("dummyMsgUrl").startsWith("file://")
|
||||
) {
|
||||
messageTracker.getId(msgHdr);
|
||||
}
|
||||
},
|
||||
};
|
||||
windowTracker.addListener("MsgLoaded", this._messageOpenListener);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
|
@ -1980,6 +1862,7 @@ var messageTracker = new (class extends EventEmitter {
|
|||
MailServices.mailSession.RemoveFolderListener(this);
|
||||
// nsIMsgFolderListener
|
||||
MailServices.mfn.removeListener(this);
|
||||
windowTracker.removeListener("MsgLoaded", this._messageOpenListener);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1991,8 +1874,8 @@ var messageTracker = new (class extends EventEmitter {
|
|||
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);
|
||||
if (msgHdr && !msgHdr.folder) {
|
||||
this._dummyMessageHeaders.set(msgIdentifier.dummyMsgUrl, msgHdr);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2016,7 +1899,7 @@ var messageTracker = new (class extends EventEmitter {
|
|||
let id = this._get(msgIdentifier);
|
||||
this._messages.delete(id);
|
||||
this._messageIds.delete(hash);
|
||||
this._dummyMessageHeaders.delete(id);
|
||||
this._dummyMessageHeaders.delete(msgIdentifier.dummyMsgUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2024,14 +1907,31 @@ var messageTracker = new (class extends EventEmitter {
|
|||
* @return {int} The messageTracker id of the message
|
||||
*/
|
||||
getId(msgHdr) {
|
||||
let msgIdentifier = msgHdr.folder
|
||||
? { folderURI: msgHdr.folder.URI, messageKey: msgHdr.messageKey }
|
||||
: {
|
||||
dummyMsgUrl: msgHdr.getStringProperty("dummyMsgUrl"),
|
||||
dummyMsgLastModifiedTime: msgHdr.getUint32Property(
|
||||
"dummyMsgLastModifiedTime"
|
||||
),
|
||||
};
|
||||
let msgIdentifier;
|
||||
if (msgHdr.folder) {
|
||||
msgIdentifier = {
|
||||
folderURI: msgHdr.folder.URI,
|
||||
messageKey: msgHdr.messageKey,
|
||||
};
|
||||
} else {
|
||||
// Normalize the dummyMsgUrl by sorting its parameters and striping them
|
||||
// to a minimum.
|
||||
let url = new URL(msgHdr.getStringProperty("dummyMsgUrl"));
|
||||
let parameters = Array.from(url.searchParams, p => p[0]).filter(
|
||||
p => !["group", "number", "key", "part"].includes(p)
|
||||
);
|
||||
for (let parameter of parameters) {
|
||||
url.searchParams.delete(parameter);
|
||||
}
|
||||
url.searchParams.sort();
|
||||
|
||||
msgIdentifier = {
|
||||
dummyMsgUrl: url.href,
|
||||
dummyMsgLastModifiedTime: msgHdr.getUint32Property(
|
||||
"dummyMsgLastModifiedTime"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
let id = this._get(msgIdentifier);
|
||||
if (id) {
|
||||
|
@ -2050,6 +1950,10 @@ var messageTracker = new (class extends EventEmitter {
|
|||
* @returns {Boolean}
|
||||
*/
|
||||
isModifiedFileMsg(msgIdentifier) {
|
||||
if (!msgIdentifier.dummyMsgUrl?.startsWith("file://")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
let file = Services.io
|
||||
.newURI(msgIdentifier.dummyMsgUrl)
|
||||
|
@ -2094,7 +1998,7 @@ var messageTracker = new (class extends EventEmitter {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
let msgHdr = this._dummyMessageHeaders.get(id);
|
||||
let msgHdr = this._dummyMessageHeaders.get(msgIdentifier.dummyMsgUrl);
|
||||
if (msgHdr && !this.isModifiedFileMsg(msgIdentifier)) {
|
||||
return msgHdr;
|
||||
}
|
||||
|
@ -2445,13 +2349,6 @@ 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);
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ async function getDisplayedMessages(tab, extension) {
|
|||
|
||||
let result = [];
|
||||
for (let msg of displayedMessages) {
|
||||
let hdr = await convertMessageOrAttachedMessage(msg, extension);
|
||||
let hdr = convertMessage(msg, extension);
|
||||
if (hdr) {
|
||||
result.push(hdr);
|
||||
}
|
||||
|
@ -92,11 +92,8 @@ this.messageDisplay = class extends ExtensionAPI {
|
|||
handleEvent(event) {
|
||||
let win = windowManager.wrapWindow(event.target);
|
||||
let tab = tabManager.convert(win.activeTab.nativeTab);
|
||||
convertMessageOrAttachedMessage(event.detail, extension).then(
|
||||
msg => {
|
||||
fire.async(tab, msg);
|
||||
}
|
||||
);
|
||||
let msg = convertMessage(event.detail, extension);
|
||||
fire.async(tab, msg);
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -142,7 +139,7 @@ this.messageDisplay = class extends ExtensionAPI {
|
|||
displayedMessage = tab.nativeTab.gMessageDisplay.displayedMessage;
|
||||
}
|
||||
|
||||
return convertMessageOrAttachedMessage(displayedMessage, extension);
|
||||
return convertMessage(displayedMessage, extension);
|
||||
},
|
||||
async getDisplayedMessages(tabId) {
|
||||
return getDisplayedMessages(tabManager.get(tabId), extension);
|
||||
|
@ -150,15 +147,14 @@ this.messageDisplay = class extends ExtensionAPI {
|
|||
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];
|
||||
}
|
||||
// Add the application/x-message-display type to the url, if missing.
|
||||
// The slash is escaped when setting the type via searchParams, but
|
||||
// core code needs it unescaped.
|
||||
let url = new URL(msgHdr.getStringProperty("dummyMsgUrl"));
|
||||
url.searchParams.delete("type");
|
||||
let msgUrl = `${url.href}${
|
||||
url.searchParams.toString() ? "&" : "?"
|
||||
}type=application/x-message-display`;
|
||||
|
||||
let window = await getNormalWindowReady(context);
|
||||
let msgWindow = window.openDialog(
|
||||
|
|
|
@ -38,6 +38,61 @@ Cu.importGlobalProperties(["File", "FileReader", "IOUtils", "PathUtils"]);
|
|||
|
||||
var { DefaultMap } = ExtensionUtils;
|
||||
|
||||
let messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
|
||||
|
||||
class nsDummyMsgHeader {
|
||||
constructor() {
|
||||
this.mProperties = [];
|
||||
this.messageSize = 0;
|
||||
this.author = null;
|
||||
this.subject = "";
|
||||
this.recipients = null;
|
||||
this.ccList = null;
|
||||
this.listPost = null;
|
||||
this.messageId = null;
|
||||
this.date = 0;
|
||||
this.accountKey = "";
|
||||
this.flags = 0;
|
||||
// If you change us to return a fake folder, please update
|
||||
// folderDisplay.js's FolderDisplayWidget's selectedMessageIsExternal getter.
|
||||
this.folder = null;
|
||||
}
|
||||
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];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
setStringProperty(aProperty, aVal) {
|
||||
this.mProperties[aProperty] = aVal;
|
||||
}
|
||||
getUint32Property(aProperty) {
|
||||
if (aProperty in this.mProperties) {
|
||||
return parseInt(this.mProperties[aProperty]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
setUint32Property(aProperty, aVal) {
|
||||
this.mProperties[aProperty] = aVal.toString();
|
||||
}
|
||||
markHasAttachments(hasAttachments) {}
|
||||
get mime2DecodedAuthor() {
|
||||
return this.author;
|
||||
}
|
||||
get mime2DecodedSubject() {
|
||||
return this.subject;
|
||||
}
|
||||
get mime2DecodedRecipients() {
|
||||
return this.recipients;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a part of a MIME message (as retrieved with MsgHdrToMimeMessage) and
|
||||
* filters out the properties we don't want to send to extensions.
|
||||
|
@ -72,117 +127,346 @@ function convertMessagePart(part) {
|
|||
return partObject;
|
||||
}
|
||||
|
||||
function convertAttachment(attachment) {
|
||||
return {
|
||||
async function convertAttachment(attachment) {
|
||||
let rv = {
|
||||
contentType: attachment.contentType,
|
||||
name: attachment.name,
|
||||
size: attachment.size,
|
||||
partName: attachment.partName,
|
||||
};
|
||||
|
||||
if (attachment.contentType.startsWith("message/")) {
|
||||
// The attached message may not have been seen/opened yet, create a dummy
|
||||
// msgHdr.
|
||||
let attachedMsgHdr = new nsDummyMsgHeader();
|
||||
attachedMsgHdr.setStringProperty("dummyMsgUrl", attachment.url);
|
||||
attachedMsgHdr.recipients = attachment.headers.to;
|
||||
attachedMsgHdr.ccList = attachment.headers.cc;
|
||||
attachedMsgHdr.bccList = attachment.headers.bcc;
|
||||
attachedMsgHdr.author = attachment.headers.from[0];
|
||||
attachedMsgHdr.date = Date.parse(attachment.headers.date[0]) * 1000;
|
||||
attachedMsgHdr.subject = attachment.headers.subject[0];
|
||||
attachedMsgHdr.messageId = attachment.headers["message-id"][0].replace(
|
||||
/^<|>$/g,
|
||||
""
|
||||
);
|
||||
rv.message = convertMessage(attachedMsgHdr);
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef MimeMessagePart
|
||||
* @property {MimeMessagePart[]} [attachments] - flat list of attachment parts
|
||||
* found in any of the nested mime parts
|
||||
* @property {string} [body] - the body of the part
|
||||
* @property {Uint8Array} [raw] - the raw binary content of the part
|
||||
* @property {string} [contentType]
|
||||
* @property {string} headers - key-value object with key being a header name
|
||||
* and value an array with all header values found
|
||||
* @property {string} [name] - filename, if part is an attachment
|
||||
* @property {string} partName - name of the mime part (e.g: "1.2")
|
||||
* @property {MimeMessagePart[]} [parts] - nested mime parts
|
||||
* @property {string} [size] - size of the part
|
||||
* @property {string} [url] - message url
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns attachments found in the message belonging to the given nsIMsgHdr.
|
||||
*
|
||||
* @param {nsIMsgHdr} msgHdr
|
||||
* @param {boolean} includeNestedAttachments - Whether attached messages will be
|
||||
* decoded to return their attachments as well
|
||||
* @returns {MimeMessageAttachment[]}
|
||||
* @param {boolean} includeNestedAttachments - Whether to return all attachments,
|
||||
* including attachments from nested mime parts.
|
||||
* @returns {Promise<MimeMessagePart[]>}
|
||||
*/
|
||||
async function getAttachments(msgHdr, includeNestedAttachments) {
|
||||
// Use jsmime based MimeParser to read NNTP messages, which are not
|
||||
// supported by MsgHdrToMimeMessage. No encryption support!
|
||||
if (msgHdr.folder?.server.type == "nntp") {
|
||||
let raw = await MsgHdrToRawMessage(msgHdr);
|
||||
let mimeMsg = MimeParser.extractMimeMsg(raw, {
|
||||
includeAttachments: true,
|
||||
decodeSubMessages: includeNestedAttachments,
|
||||
});
|
||||
return mimeMsg.attachments;
|
||||
async function getAttachments(msgHdr, includeNestedAttachments = false) {
|
||||
let mimeMsg = await getMimeMessage(msgHdr);
|
||||
if (!mimeMsg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
MsgHdrToMimeMessage(
|
||||
msgHdr,
|
||||
// Reduce returned attachments according to includeNestedAttachments.
|
||||
let level = mimeMsg.partName ? mimeMsg.partName.split(".").length : 0;
|
||||
return mimeMsg.attachments.filter(
|
||||
a => includeNestedAttachments || a.partName.split(".").length == level + 2
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the attachment identified by the provided partName.
|
||||
*
|
||||
* @param {nsIMsgHdr} msgHdr
|
||||
* @param {string} partName
|
||||
* @returns {Promise<MimeMessagePart>}
|
||||
*/
|
||||
async function getAttachment(msgHdr, partName) {
|
||||
// It's not ideal to have to call MsgHdrToMimeMessage here again, but we need
|
||||
// the name of the attached file, plus this also gives us the URI without having
|
||||
// to jump through a lot of hoops.
|
||||
let attachment = await getMimeMessage(msgHdr, partName);
|
||||
if (!attachment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (attachment.url.startsWith("news://")) {
|
||||
attachment.raw = new Uint8Array(attachment.body.length);
|
||||
for (var i = 0; i < attachment.body.length; i++) {
|
||||
attachment.raw[i] = attachment.body.charCodeAt(i);
|
||||
}
|
||||
} else {
|
||||
let channel = Services.io.newChannelFromURI(
|
||||
Services.io.newURI(attachment.url),
|
||||
null,
|
||||
(_msgHdr, mimeMsg) => {
|
||||
let attachments = includeNestedAttachments
|
||||
? mimeMsg.allInlineAttachments // all attachments, as in the "display attachments inline" UI option
|
||||
: mimeMsg.allUserAttachments; // only direct attachments, skipping attachments from sub-messages
|
||||
resolve(attachments);
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
null,
|
||||
Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
|
||||
Ci.nsIContentPolicy.TYPE_OTHER
|
||||
);
|
||||
|
||||
attachment.raw = await new Promise((resolve, reject) => {
|
||||
let listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance(
|
||||
Ci.nsIStreamLoader
|
||||
);
|
||||
listener.init({
|
||||
onStreamComplete(loader, context, status, resultLength, result) {
|
||||
if (Components.isSuccessCode(status)) {
|
||||
resolve(Uint8Array.from(result));
|
||||
} else {
|
||||
reject(new Error(`Failed to read attachment content: ${status}`));
|
||||
}
|
||||
},
|
||||
});
|
||||
channel.asyncOpen(listener, null);
|
||||
});
|
||||
}
|
||||
|
||||
return attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the <part> parameter of the dummyMsgUrl of the provided nsIMsgHdr.
|
||||
*
|
||||
* @param {nsIMsgHdr} msgHdr
|
||||
* @returns {string}
|
||||
*/
|
||||
function getSubMessagePartName(msgHdr) {
|
||||
if (msgHdr.folder || !msgHdr.getStringProperty("dummyMsgUrl")) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return new URL(msgHdr.getStringProperty("dummyMsgUrl")).searchParams.get(
|
||||
"part"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the nsIMsgHdr of the outer message, if the provided nsIMsgHdr belongs
|
||||
* to a message which is actually an attachment of another message. Returns null
|
||||
* otherwise.
|
||||
*
|
||||
* @param {nsIMsgHdr} msgHdr
|
||||
* @returns {nsIMsgHdr}
|
||||
*/
|
||||
function getParentMsgHdr(msgHdr) {
|
||||
if (msgHdr.folder || !msgHdr.getStringProperty("dummyMsgUrl")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let url = new URL(msgHdr.getStringProperty("dummyMsgUrl"));
|
||||
|
||||
if (url.protocol == "news:") {
|
||||
let newsUrl = `news-message://${url.hostname}/${url.searchParams.get(
|
||||
"group"
|
||||
)}#${url.searchParams.get("key")}`;
|
||||
return messenger.msgHdrFromURI(newsUrl);
|
||||
}
|
||||
|
||||
if (url.protocol == "mailbox:") {
|
||||
// This could be a sub-message of a message opened from file.
|
||||
let fileUrl = `file://${url.pathname}`;
|
||||
let parentMsgHdr = messageTracker._dummyMessageHeaders.get(fileUrl);
|
||||
if (parentMsgHdr) {
|
||||
return parentMsgHdr;
|
||||
}
|
||||
}
|
||||
// Everything else should be a mailbox:// or an imap:// url.
|
||||
let params = Array.from(url.searchParams, p => p[0]).filter(
|
||||
p => !["number"].includes(p)
|
||||
);
|
||||
for (let param of params) {
|
||||
url.searchParams.delete(param);
|
||||
}
|
||||
return Services.io.newURI(url.href).QueryInterface(Ci.nsIMsgMessageUrl)
|
||||
.messageHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw message for a given nsIMsgHdr.
|
||||
*
|
||||
* @param aMsgHdr - The message header to retrieve the raw message for.
|
||||
* @returns {Promise<string>} - Binary string of the raw message.
|
||||
*/
|
||||
async function getRawMessage(msgHdr) {
|
||||
// If this message is a sub-message (an attachment of another message), get it
|
||||
// as an attachment from the parent message and return its raw content.
|
||||
let subMsgPartName = getSubMessagePartName(msgHdr);
|
||||
if (subMsgPartName) {
|
||||
let parentMsgHdr = getParentMsgHdr(msgHdr);
|
||||
let attachment = await getAttachment(parentMsgHdr, subMsgPartName);
|
||||
return attachment.raw.reduce(
|
||||
(prev, curr) => prev + String.fromCharCode(curr),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
// Messages opened from file 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 = {
|
||||
_data: [],
|
||||
_stream: null,
|
||||
onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
|
||||
if (!this._stream) {
|
||||
this._stream = Cc[
|
||||
"@mozilla.org/scriptableinputstream;1"
|
||||
].createInstance(Ci.nsIScriptableInputStream);
|
||||
this._stream.init(aInputStream);
|
||||
}
|
||||
this._data.push(this._stream.read(aCount));
|
||||
},
|
||||
true,
|
||||
{ examineEncryptedParts: true, partsOnDemand: true }
|
||||
onStartRequest() {},
|
||||
onStopRequest(request, status) {
|
||||
if (Components.isSuccessCode(status)) {
|
||||
resolve(this._data.join(""));
|
||||
} else {
|
||||
reject(
|
||||
new Error(`Error while streaming message <${msgUri}>: ${status}`)
|
||||
);
|
||||
}
|
||||
},
|
||||
QueryInterface: ChromeUtils.generateQI([
|
||||
"nsIStreamListener",
|
||||
"nsIRequestObserver",
|
||||
]),
|
||||
};
|
||||
|
||||
// This is not using aConvertData and therefore works for news:// messages.
|
||||
service.streamMessage(
|
||||
msgUri,
|
||||
streamlistener,
|
||||
null, // aMsgWindow
|
||||
null, // aUrlListener
|
||||
false, // aConvertData
|
||||
"" //aAdditionalHeader
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the attachment identified by the provided partName. The returned
|
||||
* MimeMessageAttachment holds its binary content in an Uint8Array in the
|
||||
* additional property bodyAsTypedArray.
|
||||
* Returns MIME parts found in the message identified by the given nsIMsgHdr.
|
||||
*
|
||||
* @param {nsIMsgHdr} msgHdr
|
||||
* @param {string} partName
|
||||
* @returns {MimeMessageAttachment}
|
||||
* @param {string} partName - Return only a specific mime part.
|
||||
* @returns {Promise<MimeMessagePart>}
|
||||
*/
|
||||
async function getAttachment(msgHdr, partName) {
|
||||
async function getMimeMessage(msgHdr, partName = "") {
|
||||
// If this message is a sub-message (an attachment of another message), get the
|
||||
// mime parts of the parent message and return the part of the sub-message.
|
||||
let subMsgPartName = getSubMessagePartName(msgHdr);
|
||||
if (subMsgPartName) {
|
||||
let parentMsgHdr = getParentMsgHdr(msgHdr);
|
||||
if (!parentMsgHdr) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let mimeMsg = await getMimeMessage(parentMsgHdr, partName);
|
||||
if (!mimeMsg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If <partName> was specified, the returned mime message is just that part,
|
||||
// no further processing needed. But prevent x-ray vision into the parent.
|
||||
if (partName) {
|
||||
if (partName.split(".").length > subMsgPartName.split(".").length) {
|
||||
return mimeMsg;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Limit mimeMsg and attachments to the requested <subMessagePart>.
|
||||
let findSubPart = (parts, partName) => {
|
||||
let match = parts.find(a => partName.startsWith(a.partName));
|
||||
if (!match) {
|
||||
throw new ExtensionError(
|
||||
`Unexpected Error: Part ${partName} not found.`
|
||||
);
|
||||
}
|
||||
return match.partName == partName
|
||||
? match
|
||||
: findSubPart(match.parts, partName);
|
||||
};
|
||||
let subMimeMsg = findSubPart(mimeMsg.parts, subMsgPartName);
|
||||
|
||||
if (mimeMsg.attachments) {
|
||||
subMimeMsg.attachments = mimeMsg.attachments.filter(
|
||||
a =>
|
||||
a.partName != subMsgPartName && a.partName.startsWith(subMsgPartName)
|
||||
);
|
||||
}
|
||||
return subMimeMsg;
|
||||
}
|
||||
|
||||
// Use jsmime based MimeParser to read NNTP messages, which are not
|
||||
// supported by MsgHdrToMimeMessage. No encryption support!
|
||||
if (msgHdr.folder?.server.type == "nntp") {
|
||||
let raw = await MsgHdrToRawMessage(msgHdr);
|
||||
return MimeParser.extractMimeMsg(raw, {
|
||||
includeAttachments: true,
|
||||
let raw = await getRawMessage(msgHdr);
|
||||
let mimeMsg = MimeParser.extractMimeMsg(raw, {
|
||||
decodeSubMessages: !partName,
|
||||
getMimePart: partName,
|
||||
decodeSubMessages: false,
|
||||
});
|
||||
if (!mimeMsg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Re-build news:// URL, which is not provided by MimeParser.extractMimeMsg().
|
||||
let msgUri = msgHdr.folder.getUriForMsg(msgHdr);
|
||||
let baseUrl = new URL(
|
||||
messenger.messageServiceFromURI(msgUri).getUrlForUri(msgUri).spec
|
||||
);
|
||||
if (partName) {
|
||||
baseUrl.searchParams.set("part", mimeMsg.partName);
|
||||
mimeMsg.url = baseUrl.href;
|
||||
} else if (mimeMsg.attachments) {
|
||||
for (let attachment of mimeMsg.attachments) {
|
||||
baseUrl.searchParams.set("part", attachment.partName);
|
||||
attachment.url = baseUrl.href;
|
||||
}
|
||||
}
|
||||
return mimeMsg;
|
||||
}
|
||||
|
||||
// It's not ideal to have to call MsgHdrToMimeMessage here but we
|
||||
// need the name of the attached file, plus this also gives us the
|
||||
// URI without having to jump through a lot of hoops.
|
||||
let attachment = await new Promise(resolve => {
|
||||
let mimeMsg = await new Promise(resolve => {
|
||||
MsgHdrToMimeMessage(
|
||||
msgHdr,
|
||||
null,
|
||||
(_msgHdr, mimeMsg) => {
|
||||
resolve(mimeMsg.allInlineAttachments.find(a => a.partName == partName));
|
||||
mimeMsg.attachments = mimeMsg.allInlineAttachments;
|
||||
resolve(mimeMsg);
|
||||
},
|
||||
true,
|
||||
{ examineEncryptedParts: true, partsOnDemand: true }
|
||||
);
|
||||
});
|
||||
|
||||
if (!attachment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let channel = Services.io.newChannelFromURI(
|
||||
Services.io.newURI(attachment.url),
|
||||
null,
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
null,
|
||||
Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
|
||||
Ci.nsIContentPolicy.TYPE_OTHER
|
||||
);
|
||||
|
||||
attachment.bodyAsTypedArray = await new Promise((resolve, reject) => {
|
||||
let listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance(
|
||||
Ci.nsIStreamLoader
|
||||
);
|
||||
listener.init({
|
||||
onStreamComplete(loader, context, status, resultLength, result) {
|
||||
if (Components.isSuccessCode(status)) {
|
||||
resolve(Uint8Array.from(result));
|
||||
} else {
|
||||
reject(new Error(`Failed to read attachment content: ${status}`));
|
||||
}
|
||||
},
|
||||
});
|
||||
channel.asyncOpen(listener, null);
|
||||
});
|
||||
|
||||
return attachment;
|
||||
return partName
|
||||
? mimeMsg.attachments.find(a => a.partName == partName)
|
||||
: mimeMsg;
|
||||
}
|
||||
|
||||
this.messages = class extends ExtensionAPI {
|
||||
|
@ -203,6 +487,33 @@ this.messages = class extends ExtensionAPI {
|
|||
return folderMap;
|
||||
}
|
||||
|
||||
async function createTempFileMessage(msgHdr) {
|
||||
let rawBinaryString = await getRawMessage(msgHdr);
|
||||
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);
|
||||
return emlFile;
|
||||
}
|
||||
|
||||
async function moveOrCopyMessages(messageIds, { accountId, path }, isMove) {
|
||||
if (
|
||||
!context.extension.hasPermission("accountsRead") ||
|
||||
|
@ -236,10 +547,15 @@ this.messages = class extends ExtensionAPI {
|
|||
}
|
||||
|
||||
for (let msgHdr of msgHeaders) {
|
||||
let file;
|
||||
let fileUrl = msgHdr.getStringProperty("dummyMsgUrl");
|
||||
let file = Services.io
|
||||
.newURI(fileUrl)
|
||||
.QueryInterface(Ci.nsIFileURL).file;
|
||||
if (fileUrl.startsWith("file://")) {
|
||||
file = Services.io.newURI(fileUrl).QueryInterface(Ci.nsIFileURL)
|
||||
.file;
|
||||
} else {
|
||||
file = await createTempFileMessage(msgHdr);
|
||||
}
|
||||
|
||||
promises.push(
|
||||
new Promise((resolve, reject) => {
|
||||
MailServices.copy.copyFileMessage(
|
||||
|
@ -307,38 +623,6 @@ this.messages = class extends ExtensionAPI {
|
|||
}
|
||||
}
|
||||
|
||||
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") {
|
||||
try {
|
||||
let raw = await MsgHdrToRawMessage(msgHdr);
|
||||
let mimeMsg = MimeParser.extractMimeMsg(raw, {
|
||||
includeAttachments: false,
|
||||
});
|
||||
return mimeMsg;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await new Promise(resolve => {
|
||||
MsgHdrToMimeMessage(
|
||||
msgHdr,
|
||||
null,
|
||||
(_msgHdr, mimeMsg) => {
|
||||
resolve(mimeMsg);
|
||||
},
|
||||
true,
|
||||
{ examineEncryptedParts: true }
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
messages: {
|
||||
onNewMailReceived: new EventManager({
|
||||
|
@ -487,7 +771,8 @@ this.messages = class extends ExtensionAPI {
|
|||
if (!msgHdr) {
|
||||
throw new ExtensionError(`Message not found: ${messageId}.`);
|
||||
}
|
||||
return MsgHdrToRawMessage(msgHdr).catch(() => {
|
||||
return getRawMessage(msgHdr).catch(ex => {
|
||||
Cu.reportError(ex);
|
||||
throw new ExtensionError(`Error reading message ${messageId}`);
|
||||
});
|
||||
},
|
||||
|
@ -497,9 +782,10 @@ this.messages = class extends ExtensionAPI {
|
|||
throw new ExtensionError(`Message not found: ${messageId}.`);
|
||||
}
|
||||
let attachments = await getAttachments(msgHdr);
|
||||
return attachments
|
||||
.map(convertAttachment)
|
||||
.sort((a, b) => a.partName > b.partName);
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
attachments[i] = await convertAttachment(attachments[i]);
|
||||
}
|
||||
return attachments;
|
||||
},
|
||||
async getAttachmentFile(messageId, partName) {
|
||||
let msgHdr = messageTracker.getMessage(messageId);
|
||||
|
@ -512,7 +798,7 @@ this.messages = class extends ExtensionAPI {
|
|||
`Part ${partName} not found in message ${messageId}.`
|
||||
);
|
||||
}
|
||||
return new File([attachment.bodyAsTypedArray], attachment.name, {
|
||||
return new File([attachment.raw], attachment.name, {
|
||||
type: attachment.contentType,
|
||||
});
|
||||
},
|
||||
|
|
|
@ -167,7 +167,8 @@
|
|||
},
|
||||
"partName": {
|
||||
"type": "string",
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"description": "The identifier of this part, used in :ref:`messages.getAttachmentFile"
|
||||
},
|
||||
"parts": {
|
||||
"type": "array",
|
||||
|
@ -179,7 +180,8 @@
|
|||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"description": "The size of this part. The size of ``message/*`` parts is not the actual message size (on disc), but the total size of its decoded body parts, excluding headers."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -284,6 +286,11 @@
|
|||
"size": {
|
||||
"type": "integer",
|
||||
"description": "The size in bytes of this attachment."
|
||||
},
|
||||
"message": {
|
||||
"$ref": "messages.MessageHeader",
|
||||
"optional": true,
|
||||
"description": "A MessageHeader, if this attachment is a message."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ add_task(async function testExternalMessage() {
|
|||
|
||||
let files = {
|
||||
"background.js": async () => {
|
||||
const platform = await browser.runtime.getPlatformInfo();
|
||||
const emlData = {
|
||||
openExternalFileMessage: {
|
||||
headerMessageId: "sample.eml@mime.sample",
|
||||
|
@ -46,7 +45,7 @@ add_task(async function testExternalMessage() {
|
|||
ccList: ["Jimmy <jimmy.Olsen@dailyplanet.com>"],
|
||||
subject: "Test message",
|
||||
attachments: 3,
|
||||
size: platform.os == "win" ? 6947 : 6825,
|
||||
size: 0,
|
||||
external: true,
|
||||
read: null,
|
||||
recipients: ["Heinz Müller <mueller@examples.com>"],
|
||||
|
|
|
@ -83,8 +83,8 @@ Content-Disposition: attachment; filename="message2.eml"
|
|||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Message-ID: <sample-nested-attached.eml@mime.sample>
|
||||
From: Superman <clark.kent@dailyplanet.com>
|
||||
To: Jimmy <jimmy.olsen@dailyplanet.com>
|
||||
From: Jimmy <jimmy.olsen@dailyplanet.com>
|
||||
To: Superman <clark.kent@dailyplanet.com>
|
||||
Subject: Test message 2
|
||||
Date: Wed, 16 May 2000 19:32:47 -0400
|
||||
MIME-Version: 1.0
|
||||
|
|
|
@ -246,9 +246,49 @@ add_task(
|
|||
browser.test.assertEq(5, messages.length);
|
||||
let message = messages[4];
|
||||
|
||||
function validateMessage(msg, expectedValues) {
|
||||
for (let expectedValueName in expectedValues) {
|
||||
let value = msg[expectedValueName];
|
||||
let expected = expectedValues[expectedValueName];
|
||||
if (Array.isArray(expected)) {
|
||||
browser.test.assertTrue(
|
||||
Array.isArray(value),
|
||||
`Value for ${expectedValueName} should be an Array.`
|
||||
);
|
||||
browser.test.assertEq(
|
||||
expected.length,
|
||||
value.length,
|
||||
`Value for ${expectedValueName} should have the correct Array size.`
|
||||
);
|
||||
for (let i = 0; i < expected.length; i++) {
|
||||
browser.test.assertEq(
|
||||
expected[i],
|
||||
value[i],
|
||||
`Value for ${expectedValueName}[${i}] should be correct.`
|
||||
);
|
||||
}
|
||||
} else if (expected instanceof Date) {
|
||||
browser.test.assertTrue(
|
||||
value instanceof Date,
|
||||
`Value for ${expectedValueName} should be a Date.`
|
||||
);
|
||||
browser.test.assertEq(
|
||||
expected.getTime(),
|
||||
value.getTime(),
|
||||
`Date value for ${expectedValueName} should be correct.`
|
||||
);
|
||||
} else {
|
||||
browser.test.assertEq(
|
||||
expected,
|
||||
value,
|
||||
`Value for ${expectedValueName} should be correct.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request attachments.
|
||||
let attachments = await browser.messages.listAttachments(message.id);
|
||||
|
||||
browser.test.assertEq(2, attachments.length);
|
||||
browser.test.assertEq("1.2", attachments[0].partName);
|
||||
browser.test.assertEq("1.3", attachments[1].partName);
|
||||
|
@ -256,14 +296,119 @@ add_task(
|
|||
browser.test.assertEq("message1.eml", attachments[0].name);
|
||||
browser.test.assertEq("yellowPixel.png", attachments[1].name);
|
||||
|
||||
// Test getting attachments.
|
||||
let tests = [
|
||||
// Validate the returned MessageHeader for attached message1.eml.
|
||||
let subMessage = attachments[0].message;
|
||||
browser.test.assertTrue(
|
||||
subMessage.id != message.id,
|
||||
`Id of attached SubMessage (${subMessage.id}) should be different from the id of the outer message (${message.id})`
|
||||
);
|
||||
validateMessage(subMessage, {
|
||||
date: new Date(958606367000),
|
||||
author: "Superman <clark.kent@dailyplanet.com>",
|
||||
recipients: ["Jimmy <jimmy.olsen@dailyplanet.com>"],
|
||||
ccList: [],
|
||||
bccList: [],
|
||||
subject: "Test message 1",
|
||||
new: false,
|
||||
headersOnly: false,
|
||||
flagged: false,
|
||||
junk: false,
|
||||
junkScore: 0,
|
||||
headerMessageId: "sample-attached.eml@mime.sample",
|
||||
size: 0,
|
||||
tags: [],
|
||||
external: true,
|
||||
});
|
||||
|
||||
// Get attachments of sub-message messag1.eml.
|
||||
let subAttachments = await browser.messages.listAttachments(
|
||||
subMessage.id
|
||||
);
|
||||
browser.test.assertEq(4, subAttachments.length);
|
||||
browser.test.assertEq("1.2.1.2", subAttachments[0].partName);
|
||||
browser.test.assertEq("1.2.1.3", subAttachments[1].partName);
|
||||
browser.test.assertEq("1.2.1.4", subAttachments[2].partName);
|
||||
browser.test.assertEq("1.2.1.5", subAttachments[3].partName);
|
||||
|
||||
browser.test.assertEq("whitePixel.png", subAttachments[0].name);
|
||||
browser.test.assertEq("greenPixel.png", subAttachments[1].name);
|
||||
browser.test.assertEq("redPixel.png", subAttachments[2].name);
|
||||
browser.test.assertEq("message2.eml", subAttachments[3].name);
|
||||
|
||||
// Validate the returned MessageHeader for sub-message message2.eml
|
||||
// attached to sub-message message1.eml.
|
||||
let subSubMessage = subAttachments[3].message;
|
||||
browser.test.assertTrue(
|
||||
![message.id, subMessage.id].includes(subSubMessage.id),
|
||||
`Id of attached SubSubMessage (${subSubMessage.id}) should be different from the id of the outer message (${message.id}) and from the SubMessage (${subMessage.id})`
|
||||
);
|
||||
validateMessage(subSubMessage, {
|
||||
date: new Date(958519967000),
|
||||
author: "Jimmy <jimmy.olsen@dailyplanet.com>",
|
||||
recipients: ["Superman <clark.kent@dailyplanet.com>"],
|
||||
ccList: [],
|
||||
bccList: [],
|
||||
subject: "Test message 2",
|
||||
new: false,
|
||||
headersOnly: false,
|
||||
flagged: false,
|
||||
junk: false,
|
||||
junkScore: 0,
|
||||
headerMessageId: "sample-nested-attached.eml@mime.sample",
|
||||
size: 0,
|
||||
tags: [],
|
||||
external: true,
|
||||
});
|
||||
|
||||
// Test getAttachmentFile().
|
||||
// Note: This function has x-ray vision into sub-messages and can get
|
||||
// any part inside the message, even if - technically - the attachments
|
||||
// belong to subMessages. There is no difference between requesting
|
||||
// part 1.2.1.2 from the main message or from message1.eml (part 1.2).
|
||||
// X-ray vision from a sub-message back into a parent is not allowed.
|
||||
let platform = await browser.runtime.getPlatformInfo();
|
||||
let fileTests = [
|
||||
{
|
||||
partName: "1.2",
|
||||
name: "message1.eml",
|
||||
size: 2602,
|
||||
size:
|
||||
platform.os != "win" && account.type == "none" ? 2517 : 2601,
|
||||
text: "Message-ID: <sample-attached.eml@mime.sample>",
|
||||
},
|
||||
{
|
||||
partName: "1.2.1.2",
|
||||
name: "whitePixel.png",
|
||||
size: 69,
|
||||
data:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC",
|
||||
},
|
||||
{
|
||||
partName: "1.2.1.3",
|
||||
name: "greenPixel.png",
|
||||
size: 119,
|
||||
data:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY+C76AoAAhUBJel4xsMAAAAASUVORK5CYII=",
|
||||
},
|
||||
{
|
||||
partName: "1.2.1.4",
|
||||
name: "redPixel.png",
|
||||
size: 119,
|
||||
data:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY+hgkAYAAbcApOp/9LEAAAAASUVORK5CYII=",
|
||||
},
|
||||
{
|
||||
partName: "1.2.1.5",
|
||||
name: "message2.eml",
|
||||
size: platform.os != "win" && account.type == "none" ? 838 : 867,
|
||||
text: "Message-ID: <sample-nested-attached.eml@mime.sample>",
|
||||
},
|
||||
{
|
||||
partName: "1.2.1.5.1.2",
|
||||
name: "whitePixel.png",
|
||||
size: 69,
|
||||
data:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC",
|
||||
},
|
||||
{
|
||||
partName: "1.3",
|
||||
name: "yellowPixel.png",
|
||||
|
@ -272,36 +417,69 @@ add_task(
|
|||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY/j/iQEABOUB8pypNlQAAAAASUVORK5CYII=",
|
||||
},
|
||||
];
|
||||
for (let test of tests) {
|
||||
let file = await browser.messages.getAttachmentFile(
|
||||
message.id,
|
||||
test.partName
|
||||
let testMessages = [
|
||||
{
|
||||
id: message.id,
|
||||
expectedFileCounts: 7,
|
||||
},
|
||||
{
|
||||
id: subMessage.id,
|
||||
subPart: "1.2.",
|
||||
expectedFileCounts: 5,
|
||||
},
|
||||
{
|
||||
id: subSubMessage.id,
|
||||
subPart: "1.2.1.5.",
|
||||
expectedFileCounts: 1,
|
||||
},
|
||||
];
|
||||
for (let msg of testMessages) {
|
||||
let fileCounts = 0;
|
||||
for (let test of fileTests) {
|
||||
if (msg.subPart && !test.partName.startsWith(msg.subPart)) {
|
||||
await browser.test.assertRejects(
|
||||
browser.messages.getAttachmentFile(msg.id, test.partName),
|
||||
`Part ${test.partName} not found in message ${msg.id}.`,
|
||||
"Sub-message should not be able to get parts from parent message"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
fileCounts++;
|
||||
|
||||
let file = await browser.messages.getAttachmentFile(
|
||||
msg.id,
|
||||
test.partName
|
||||
);
|
||||
|
||||
// eslint-disable-next-line mozilla/use-isInstance
|
||||
browser.test.assertTrue(file instanceof File);
|
||||
browser.test.assertEq(test.name, file.name);
|
||||
browser.test.assertEq(test.size, file.size);
|
||||
|
||||
if (test.text) {
|
||||
browser.test.assertTrue(
|
||||
(await file.text()).startsWith(test.text)
|
||||
);
|
||||
}
|
||||
|
||||
if (test.data) {
|
||||
let reader = new FileReader();
|
||||
let data = await new Promise(resolve => {
|
||||
reader.onload = e => resolve(e.target.result);
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
browser.test.assertEq(
|
||||
test.data,
|
||||
data.replaceAll("\r\n", "\n").trim()
|
||||
);
|
||||
}
|
||||
}
|
||||
browser.test.assertEq(
|
||||
msg.expectedFileCounts,
|
||||
fileCounts,
|
||||
"Should have requested to correct amount of attachment files."
|
||||
);
|
||||
|
||||
// eslint-disable-next-line mozilla/use-isInstance
|
||||
browser.test.assertTrue(file instanceof File);
|
||||
browser.test.assertEq(test.name, file.name);
|
||||
browser.test.assertEq(test.size, file.size);
|
||||
|
||||
if (test.text) {
|
||||
browser.test.assertTrue(
|
||||
(await file.text()).startsWith(test.text)
|
||||
);
|
||||
}
|
||||
|
||||
if (test.data) {
|
||||
let reader = new FileReader();
|
||||
let data = await new Promise(resolve => {
|
||||
reader.onload = e => resolve(e.target.result);
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
browser.test.assertEq(
|
||||
test.data,
|
||||
data.replaceAll("\r\n", "\n").trim()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
browser.test.notifyPass("finished");
|
||||
},
|
||||
"utils.js": await getUtilsJS(),
|
||||
|
|
|
@ -76,7 +76,6 @@ var ExtractMimeMsgEmitter = {
|
|||
parts: [],
|
||||
size: 0,
|
||||
headers: {},
|
||||
rawHeaderText: "",
|
||||
attachments: [],
|
||||
// No support for encryption.
|
||||
isEncrypted: false,
|
||||
|
@ -98,13 +97,11 @@ var ExtractMimeMsgEmitter = {
|
|||
if (this.options.getMimePart) {
|
||||
if (this.mimeTree.parts[0].partName == this.options.getMimePart) {
|
||||
this.mimeMsg = this.mimeTree.parts[0];
|
||||
this.mimeMsg.bodyAsTypedArray = jsmime.mimeutils.stringToTypedArray(
|
||||
this.mimeMsg.body
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.mimeTree.attachments.sort((a, b) => a.partName > b.partName);
|
||||
this.mimeMsg = this.mimeTree;
|
||||
},
|
||||
|
||||
|
@ -115,8 +112,6 @@ var ExtractMimeMsgEmitter = {
|
|||
? headerMap.contentType.type
|
||||
: "text/plain";
|
||||
|
||||
let rawHeaderText = headerMap.rawHeaderText;
|
||||
|
||||
let headers = {};
|
||||
for (let [headerName, headerValue] of headerMap._rawHeaders) {
|
||||
// MsgHdrToMimeMessage always returns an array, even for single values.
|
||||
|
@ -130,28 +125,21 @@ var ExtractMimeMsgEmitter = {
|
|||
|
||||
// Get the most recent part from the hierarchical parts stack, which is the
|
||||
// parent of the new part to by added.
|
||||
let currentPart = this.partsPath[this.partsPath.length - 1];
|
||||
let parentPart = this.partsPath[this.partsPath.length - 1];
|
||||
|
||||
// Add a leading 1 to the partNum and convert the "$." sub-message deliminator.
|
||||
let partName =
|
||||
"1" + (partNum !== "" ? "." : "") + partNum.replaceAll("$.", ".1.");
|
||||
if (partName == "1") {
|
||||
// MsgHdrToMimeMessage differentiates between the message headers and the
|
||||
// headers of the first part. jsmime.js however returns all headers of
|
||||
// the message in the first part.
|
||||
// Add a leading 1 to the partNum and convert the "$" sub-message deliminator.
|
||||
let partName = "1" + (partNum ? "." : "") + partNum.replaceAll("$", ".1");
|
||||
|
||||
// Move rawHeaderText and add the content-* headers back to the new/first
|
||||
// part.
|
||||
currentPart.rawHeaderText = rawHeaderText;
|
||||
rawHeaderText = rawHeaderText
|
||||
.split(/\n(?![ \t])/)
|
||||
.filter(h => h.toLowerCase().startsWith("content-"))
|
||||
.join("\n")
|
||||
.trim();
|
||||
|
||||
// Move all headers and add the content-* headers back to the new/first
|
||||
// part.
|
||||
currentPart.headers = headers;
|
||||
// MsgHdrToMimeMessage differentiates between the message headers and the
|
||||
// headers of the first part. jsmime.js however returns all headers of
|
||||
// the message in the first multipart/* part: Merge all headers into the
|
||||
// parent part and only keep content-* headers.
|
||||
if (parentPart.contentType.startsWith("message/")) {
|
||||
for (let [k, v] of Object.entries(headers)) {
|
||||
if (!parentPart.headers[k]) {
|
||||
parentPart.headers[k] = v;
|
||||
}
|
||||
}
|
||||
headers = Object.fromEntries(
|
||||
Object.entries(headers).filter(h => h[0].startsWith("content-"))
|
||||
);
|
||||
|
@ -166,7 +154,6 @@ var ExtractMimeMsgEmitter = {
|
|||
partName,
|
||||
body: "",
|
||||
headers,
|
||||
rawHeaderText,
|
||||
contentType,
|
||||
size: 0,
|
||||
parts: [],
|
||||
|
@ -175,8 +162,8 @@ var ExtractMimeMsgEmitter = {
|
|||
};
|
||||
|
||||
// Add nested new part.
|
||||
currentPart.parts.push(newPart);
|
||||
// Update the newly added part to be current part.
|
||||
parentPart.parts.push(newPart);
|
||||
// Push the newly added part into the hierarchical parts stack.
|
||||
this.partsPath.push(newPart);
|
||||
},
|
||||
|
||||
|
@ -188,14 +175,12 @@ var ExtractMimeMsgEmitter = {
|
|||
// Add size.
|
||||
let size = currentPart.body.length;
|
||||
currentPart.size += size;
|
||||
let partSize = currentPart.size;
|
||||
|
||||
if (this.isAttachment(currentPart)) {
|
||||
currentPart.name = this.getAttachmentName(currentPart);
|
||||
if (this.options.includeAttachments) {
|
||||
this.mimeTree.attachments.push(currentPart);
|
||||
} else {
|
||||
deleteBody = true;
|
||||
}
|
||||
this.mimeTree.attachments.push({ ...currentPart });
|
||||
deleteBody = !this.options.getMimePart;
|
||||
}
|
||||
|
||||
if (deleteBody || currentPart.body == "") {
|
||||
|
@ -215,7 +200,7 @@ var ExtractMimeMsgEmitter = {
|
|||
|
||||
// Add the size of this part to its parent as well.
|
||||
currentPart = this.partsPath[this.partsPath.length - 1];
|
||||
currentPart.size += size;
|
||||
currentPart.size += partSize;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -405,8 +390,7 @@ var MimeParser = {
|
|||
* Returns a mimeMsg object for the given input. The returned object tries to
|
||||
* be compatible with the return value of MsgHdrToMimeMessage. Differences:
|
||||
* - no support for encryption
|
||||
* - calculated sizes differ slightly
|
||||
* - returned attachments includes the content and not a URL
|
||||
* - returned attachments include the body and not the URL
|
||||
* - returned attachments match either allInlineAttachments or
|
||||
* allUserAttachments (decodeSubMessages = false)
|
||||
* - does not eat TABs in headers, if they follow a CRLF
|
||||
|
@ -419,7 +403,6 @@ var MimeParser = {
|
|||
var emitter = Object.create(ExtractMimeMsgEmitter);
|
||||
// Set default options.
|
||||
emitter.options = {
|
||||
includeAttachments: true,
|
||||
getMimePart: "",
|
||||
decodeSubMessages: true,
|
||||
};
|
||||
|
|
Загрузка…
Ссылка в новой задаче