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:
John Bieling 2022-09-13 20:23:47 +10:00
Родитель df9cdb819c
Коммит 20b508c7ee
9 изменённых файлов: 708 добавлений и 364 удалений

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

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