/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const EXPORTED_SYMBOLS = [ "MsgHdrToMimeMessage", "MimeMessage", "MimeContainer", "MimeBody", "MimeUnknown", "MimeMessageAttachment", ]; const { MailServices } = ChromeUtils.import( "resource:///modules/MailServices.jsm" ); /** * The URL listener is surplus because the CallbackStreamListener ends up * getting the same set of events, effectively. */ var dumbUrlListener = { OnStartRunningUrl(aUrl) {}, OnStopRunningUrl(aUrl, aExitCode) {}, }; /** * Maintain a list of all active stream listeners so that we can cancel them all * during shutdown. If we don't cancel them, we risk calls into javascript * from C++ after the various XPConnect contexts have already begun their * teardown process. */ var activeStreamListeners = {}; var shutdownCleanupObserver = { _initialized: false, ensureInitialized() { if (this._initialized) { return; } Services.obs.addObserver(this, "quit-application"); this._initialized = true; }, observe(aSubject, aTopic, aData) { if (aTopic == "quit-application") { Services.obs.removeObserver(this, "quit-application"); for (let uri in activeStreamListeners) { let streamListener = activeStreamListeners[uri]; if (streamListener._request) { streamListener._request.cancel(Cr.NS_BINDING_ABORTED); } } } }, }; function CallbackStreamListener(aMsgHdr, aCallbackThis, aCallback) { this._msgHdr = aMsgHdr; // Messages opened from file or attachments do not have a folder property, but // have their url stored as a string property. let hdrURI = aMsgHdr.folder ? aMsgHdr.folder.getUriForMsg(aMsgHdr) : aMsgHdr.getStringProperty("dummyMsgUrl"); this._request = null; this._stream = null; if (aCallback === undefined) { this._callbacksThis = [null]; this._callbacks = [aCallbackThis]; } else { this._callbacksThis = [aCallbackThis]; this._callbacks = [aCallback]; } activeStreamListeners[hdrURI] = this; } /** * @implements {nsIRequestObserver} * @implements {nsIStreamListener} */ CallbackStreamListener.prototype = { QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]), // nsIRequestObserver part onStartRequest(aRequest) { this._request = aRequest; }, onStopRequest(aRequest, aStatusCode) { // Messages opened from file or attachments do not have a folder property, // but have their url stored as a string property. let msgURI = this._msgHdr.folder ? this._msgHdr.folder.getUriForMsg(this._msgHdr) : this._msgHdr.getStringProperty("dummyMsgUrl"); delete activeStreamListeners[msgURI]; aRequest.QueryInterface(Ci.nsIChannel); let message = MsgHdrToMimeMessage.RESULT_RENDEVOUZ[aRequest.URI.spec]; if (message === undefined) { message = null; } delete MsgHdrToMimeMessage.RESULT_RENDEVOUZ[aRequest.URI.spec]; for (let i = 0; i < this._callbacksThis.length; i++) { try { this._callbacks[i].call(this._callbacksThis[i], this._msgHdr, message); } catch (e) { // Most of the time, exceptions will silently disappear into the endless // deeps of XPConnect, and never reach the surface ever again. At least // warn the user if he has dump enabled. dump( "The MsgHdrToMimeMessage callback threw an exception: " + e + "\n" ); // That one will probably never make it to the original caller. throw e; } } this._msgHdr = null; this._request = null; this._stream = null; this._callbacksThis = null; this._callbacks = null; }, // nsIStreamListener part /** * Our onDataAvailable should actually never be called. The stream converter * is actually eating everything except the start and stop notification. */ onDataAvailable(aRequest, aInputStream, aOffset, aCount) { throw new Error( `The stream converter should have grabbed the data for ${aRequest?.URI.spec}` ); }, }; function stripEncryptedParts(aPart) { if (aPart.parts && aPart.isEncrypted) { aPart.parts = []; // Show an empty container. } else if (aPart.parts) { aPart.parts = aPart.parts.map(stripEncryptedParts); } return aPart; } /** * Starts retrieval of a MimeMessage instance for the given message header. * Your callback will be called with the message header you provide and the * * @param aMsgHdr The message header to retrieve the body for and build a MIME * representation of the message. * @param aCallbackThis The (optional) 'this' to use for your callback function. * @param aCallback The callback function to invoke on completion of message * parsing or failure. The first argument passed will be the nsIMsgDBHdr * you passed to this function. The second argument will be the MimeMessage * instance resulting from the processing on success, and null on failure. * @param [aAllowDownload=false] Should we allow the message to be downloaded * for this streaming request? The default is false, which means that we * require that the message be available offline. If false is passed and * the message is not available offline, we will propagate an exception * thrown by the underlying code. * @param [aOptions] Optional options. * @param [aOptions.saneBodySize] Limit body sizes to a 'reasonable' size in * order to combat corrupt offline/message stores creating pathological * situations where we have erroneously multi-megabyte messages. This * also likely reduces the impact of legitimately ridiculously large * messages. * @param [aOptions.examineEncryptedParts] By default, we won't reveal the * contents of multipart/encrypted parts to the consumers, unless explicitly * requested. In the case of MIME/PGP messages, for instance, the message * will appear as an empty multipart/encrypted container, unless this option * is used. */ function MsgHdrToMimeMessage( aMsgHdr, aCallbackThis, aCallback, aAllowDownload, aOptions ) { shutdownCleanupObserver.ensureInitialized(); let requireOffline = !aAllowDownload; // Messages opened from file or attachments do not have a folder property, but // have their url stored as a string property. let msgURI = aMsgHdr.folder ? aMsgHdr.folder.getUriForMsg(aMsgHdr) : aMsgHdr.getStringProperty("dummyMsgUrl"); let msgService = MailServices.messageServiceFromURI(msgURI); MsgHdrToMimeMessage.OPTION_TUNNEL = aOptions; // By default, Enigmail only decrypts a message streamed via libmime if it's // the one currently on display in the message reader. With this option, we're // letting Enigmail know that it should decrypt the message since the client // explicitly asked for it. let encryptedStr = aOptions && aOptions.examineEncryptedParts ? "&examineEncryptedParts=true" : ""; // S/MIME, our other encryption backend, is not that smart, and always // decrypts data. In order to protect sensitive data (e.g. not index it in // Gloda), unless the client asked for encrypted data, we pass to the client // callback a stripped-down version of the MIME structure where encrypted // parts have been removed. let wrapCallback = function (aCallback, aCallbackThis) { if (aOptions && aOptions.examineEncryptedParts) { return aCallback; } return (aMsgHdr, aMimeMsg) => aCallback.call(aCallbackThis, aMsgHdr, stripEncryptedParts(aMimeMsg)); }; // Apparently there used to be an old syntax where the callback was the second // argument... let callback = aCallback ? aCallback : aCallbackThis; let callbackThis = aCallback ? aCallbackThis : null; // if we're already streaming this msg, just add the callback // to the listener. let listenerForURI = activeStreamListeners[msgURI]; if (listenerForURI != undefined) { listenerForURI._callbacks.push(wrapCallback(callback, callbackThis)); listenerForURI._callbacksThis.push(callbackThis); return; } let streamListener = new CallbackStreamListener( aMsgHdr, callbackThis, wrapCallback(callback, callbackThis) ); try { msgService.streamMessage( msgURI, streamListener, // consumer null, // nsIMsgWindow dumbUrlListener, // nsIUrlListener true, // have them create the converter // additional uri payload, note that "header=" is prepended automatically "filter&emitter=js" + encryptedStr, requireOffline ); } catch (ex) { // If streamMessage throws an exception, we should make sure to clear the // activeStreamListener, or any subsequent attempt at sreaming this URI // will silently fail if (activeStreamListeners[msgURI]) { delete activeStreamListeners[msgURI]; } MsgHdrToMimeMessage.OPTION_TUNNEL = null; throw ex; } MsgHdrToMimeMessage.OPTION_TUNNEL = null; } /** * Let the jsmimeemitter provide us with results. The poor emitter (if I am * understanding things correctly) is evaluated outside of the C.u.import * world, so if we were to import him, we would not see him, but rather a new * copy of him. This goes for his globals, etc. (and is why we live in this * file right here). Also, it appears that the XPCOM JS wrappers aren't * magically unified so that we can try and pass data as expando properties * on things like the nsIUri instances either. So we have the jsmimeemitter * import us and poke things into RESULT_RENDEVOUZ. We put it here on this * function to try and be stealthy and avoid polluting the namespaces (or * encouraging bad behaviour) of our importers. * * If you can come up with a prettier way to shuttle this data, please do. */ MsgHdrToMimeMessage.RESULT_RENDEVOUZ = {}; /** * Cram rich options here for the MimeMessageEmitter to grab from. We * leverage the known control-flow to avoid needing a whole dictionary here. * We set this immediately before constructing the emitter and clear it * afterwards. Control flow is never yielded during the process and reentrancy * cannot happen via any other means. */ MsgHdrToMimeMessage.OPTION_TUNNEL = null; var HeaderHandlerBase = { /** * Look-up a header that should be present at most once. * * @param aHeaderName The header name to retrieve, case does not matter. * @param aDefaultValue The value to return if the header was not found, null * if left unspecified. * @returns the value of the header if present, and the default value if not * (defaults to null). If the header was present multiple times, the first * instance of the header is returned. Use getAll if you want all of the * values for the multiply-defined header. */ get(aHeaderName, aDefaultValue) { if (aDefaultValue === undefined) { aDefaultValue = null; } let lowerHeader = aHeaderName.toLowerCase(); if (lowerHeader in this.headers) { // we require that the list cannot be empty if present return this.headers[lowerHeader][0]; } return aDefaultValue; }, /** * Look-up a header that can be present multiple times. Use get for headers * that you only expect to be present at most once. * * @param aHeaderName The header name to retrieve, case does not matter. * @returns An array containing the values observed, which may mean a zero * length array. */ getAll(aHeaderName) { let lowerHeader = aHeaderName.toLowerCase(); if (lowerHeader in this.headers) { return this.headers[lowerHeader]; } return []; }, /** * @param aHeaderName Header name to test for its presence. * @returns true if the message has (at least one value for) the given header * name. */ has(aHeaderName) { let lowerHeader = aHeaderName.toLowerCase(); return lowerHeader in this.headers; }, _prettyHeaderString(aIndent) { if (aIndent === undefined) { aIndent = ""; } let s = ""; for (let header in this.headers) { let values = this.headers[header]; s += "\n " + aIndent + header + ": " + values; } return s; }, }; /** * @ivar partName The MIME part, ex "1.2.2.1". The partName of a (top-level) * message is "1", its first child is "1.1", its second child is "1.2", * its first child's first child is "1.1.1", etc. * @ivar headers Maps lower-cased header field names to a list of the values * seen for the given header. Use get or getAll as convenience helpers. * @ivar parts The list of the MIME part children of this message. Children * will be either MimeMessage instances, MimeMessageAttachment instances, * MimeContainer instances, or MimeUnknown instances. The latter two are * the result of limitations in the Javascript representation generation * at this time, combined with the need to most accurately represent the * MIME structure. */ function MimeMessage() { this.partName = null; this.headers = {}; this.parts = []; this.isEncrypted = false; } MimeMessage.prototype = { __proto__: HeaderHandlerBase, contentType: "message/rfc822", /** * @returns a list of all attachments contained in this message and all its * sub-messages. Only MimeMessageAttachment instances will be present in * the list (no sub-messages). */ get allAttachments() { let results = []; // messages are not attachments, don't include self for (let iChild = 0; iChild < this.parts.length; iChild++) { let child = this.parts[iChild]; results = results.concat(child.allAttachments); } return results; }, /** * @returns a list of all attachments contained in this message and all its * sub-messages, including the sub-messages. */ get allInlineAttachments() { // Do not include the top message, but only sub-messages. let results = this.partName ? [this] : []; for (let iChild = 0; iChild < this.parts.length; iChild++) { let child = this.parts[iChild]; results = results.concat(child.allInlineAttachments); } return results; }, /** * @returns a list of all attachments contained in this message, with * included/forwarded messages treated as real attachments. Attachments * contained in inner messages won't be shown. */ get allUserAttachments() { if (this.url) { // The jsmimeemitter camouflaged us as a MimeAttachment return [this]; } return this.parts .map(child => child.allUserAttachments) .reduce((a, b) => a.concat(b), []); }, /** * @returns the total size of this message, that is, the size of all subparts */ get size() { return this.parts .map(child => child.size) .reduce((a, b) => a + Math.max(b, 0), 0); }, /** * In the case of attached messages, libmime considers them as attachments, * and if the body is, say, quoted-printable encoded, then libmime will start * counting bytes and notify the js mime emitter about it. The JS mime emitter * being a nice guy, it will try to set a size on us. While this is the * expected behavior for MimeMsgAttachments, we must make sure we can handle * that (failing to write a setter results in exceptions being thrown). */ set size(whatever) { // nop }, /** * @param aMsgFolder A message folder, any message folder. Because this is * a hack. * @returns The concatenation of all of the body parts where parts * available as text/plain are pulled as-is, and parts only available * as text/html are converted to plaintext form first. In other words, * if we see a multipart/alternative with a text/plain, we take the * text/plain. If we see a text/html without an alternative, we convert * that to text. */ coerceBodyToPlaintext(aMsgFolder) { let bodies = []; for (let part of this.parts) { // an undefined value for something not having the method is fine let body = part.coerceBodyToPlaintext && part.coerceBodyToPlaintext(aMsgFolder); if (body) { bodies.push(body); } } if (bodies) { return bodies.join(""); } return ""; }, /** * Convert the message and its hierarchy into a "pretty string". The message * and each MIME part get their own line. The string never ends with a * newline. For a non-multi-part message, only a single line will be * returned. * Messages have their subject displayed, attachments have their filename and * content-type (ex: image/jpeg) displayed. "Filler" classes simply have * their class displayed. */ prettyString(aVerbose, aIndent, aDumpBody) { if (aIndent === undefined) { aIndent = ""; } let nextIndent = aIndent + " "; let s = "Message " + (this.isEncrypted ? "[encrypted] " : "") + "(" + this.size + " bytes): " + "subject" in this.headers ? this.headers.subject : ""; if (aVerbose) { s += this._prettyHeaderString(nextIndent); } for (let iPart = 0; iPart < this.parts.length; iPart++) { let part = this.parts[iPart]; s += "\n" + nextIndent + (iPart + 1) + " " + part.prettyString(aVerbose, nextIndent, aDumpBody); } return s; }, }; /** * @ivar contentType The content-type of this container. * @ivar parts The parts held by this container. These can be instances of any * of the classes found in this file. */ function MimeContainer(aContentType) { this.partName = null; this.contentType = aContentType; this.headers = {}; this.parts = []; this.isEncrypted = false; } MimeContainer.prototype = { __proto__: HeaderHandlerBase, get allAttachments() { let results = []; for (let iChild = 0; iChild < this.parts.length; iChild++) { let child = this.parts[iChild]; results = results.concat(child.allAttachments); } return results; }, get allInlineAttachments() { let results = []; for (let iChild = 0; iChild < this.parts.length; iChild++) { let child = this.parts[iChild]; results = results.concat(child.allInlineAttachments); } return results; }, get allUserAttachments() { return this.parts .map(child => child.allUserAttachments) .reduce((a, b) => a.concat(b), []); }, get size() { return this.parts .map(child => child.size) .reduce((a, b) => a + Math.max(b, 0), 0); }, set size(whatever) { // nop }, coerceBodyToPlaintext(aMsgFolder) { if (this.contentType == "multipart/alternative") { let htmlPart; // pick the text/plain if we can find one, otherwise remember the HTML one for (let part of this.parts) { if (part.contentType == "text/plain") { return part.body; } if (part.contentType == "text/html") { htmlPart = part; } else if (!htmlPart && part.contentType == "text/enriched") { // text/enriched gets transformed into HTML, so use it if we don't // already have an HTML part. htmlPart = part; } } // convert the HTML part if we have one if (htmlPart) { return aMsgFolder.convertMsgSnippetToPlainText(htmlPart.body); } } // if it's not alternative, recurse/aggregate using MimeMessage logic return MimeMessage.prototype.coerceBodyToPlaintext.call(this, aMsgFolder); }, prettyString(aVerbose, aIndent, aDumpBody) { let nextIndent = aIndent + " "; let s = "Container " + (this.isEncrypted ? "[encrypted] " : "") + "(" + this.size + " bytes): " + this.contentType; if (aVerbose) { s += this._prettyHeaderString(nextIndent); } for (let iPart = 0; iPart < this.parts.length; iPart++) { let part = this.parts[iPart]; s += "\n" + nextIndent + (iPart + 1) + " " + part.prettyString(aVerbose, nextIndent, aDumpBody); } return s; }, toString() { return "Container: " + this.contentType; }, }; /** * @class Represents a body portion that we understand and do not believe to be * a proper attachment. This means text/plain or text/html and it has no * filename. (A filename suggests an attachment.) * * @ivar contentType The content type of this body materal; text/plain or * text/html. * @ivar body The actual body content. */ function MimeBody(aContentType) { this.partName = null; this.contentType = aContentType; this.headers = {}; this.body = ""; this.isEncrypted = false; } MimeBody.prototype = { __proto__: HeaderHandlerBase, get allAttachments() { return []; // we are a leaf }, get allInlineAttachments() { return []; // we are a leaf }, get allUserAttachments() { return []; // we are a leaf }, get size() { return this.body.length; }, set size(whatever) { // nop }, appendBody(aBuf) { this.body += aBuf; }, coerceBodyToPlaintext(aMsgFolder) { if (this.contentType == "text/plain") { return this.body; } // text/enriched gets transformed into HTML by libmime if ( this.contentType == "text/html" || this.contentType == "text/enriched" ) { return aMsgFolder.convertMsgSnippetToPlainText(this.body); } return ""; }, prettyString(aVerbose, aIndent, aDumpBody) { let s = "Body: " + (this.isEncrypted ? "[encrypted] " : "") + "" + this.contentType + " (" + this.body.length + " bytes" + (aDumpBody ? ": '" + this.body + "'" : "") + ")"; if (aVerbose) { s += this._prettyHeaderString(aIndent + " "); } return s; }, toString() { return "Body: " + this.contentType + " (" + this.body.length + " bytes)"; }, }; /** * @class A MIME Leaf node that doesn't have a filename so we assume it's not * intended to be an attachment proper. This is probably meant for inline * display or is the result of someone amusing themselves by composing messages * by hand or a bad client. This class should probably be renamed or we should * introduce a better named class that we try and use in preference to this * class. * * @ivar contentType The content type of this part. */ function MimeUnknown(aContentType) { this.partName = null; this.contentType = aContentType; this.headers = {}; // Looks like libmime does not always interpret us as an attachment, which // means we'll have to have a default size. Returning undefined would cause // the recursive size computations to fail. this._size = 0; this.isEncrypted = false; // We want to make sure MimeUnknown has a part property: S/MIME encrypted // messages have a topmost MimeUnknown part, with the encrypted bit set to 1, // and we need to ensure all other encrypted parts are children of this // topmost part. this.parts = []; } MimeUnknown.prototype = { __proto__: HeaderHandlerBase, get allAttachments() { return this.parts .map(child => child.allAttachments) .reduce((a, b) => a.concat(b), []); }, get allInlineAttachments() { return this.parts .map(child => child.allInlineAttachments) .reduce((a, b) => a.concat(b), []); }, get allUserAttachments() { return this.parts .map(child => child.allUserAttachments) .reduce((a, b) => a.concat(b), []); }, get size() { return ( this._size + this.parts .map(child => child.size) .reduce((a, b) => a + Math.max(b, 0), 0) ); }, set size(aSize) { this._size = aSize; }, prettyString(aVerbose, aIndent, aDumpBody) { let nextIndent = aIndent + " "; let s = "Unknown: " + (this.isEncrypted ? "[encrypted] " : "") + "" + this.contentType + " (" + this.size + " bytes)"; if (aVerbose) { s += this._prettyHeaderString(aIndent + " "); } for (let iPart = 0; iPart < this.parts.length; iPart++) { let part = this.parts[iPart]; s += "\n" + nextIndent + (iPart + 1) + " " + (part ? part.prettyString(aVerbose, nextIndent, aDumpBody) : "NULL"); } return s; }, toString() { return "Unknown: " + this.contentType; }, }; /** * @class An attachment proper. We think it's an attachment because it has a * filename that libmime was able to figure out. * * @ivar partName @see{MimeMessage.partName} * @ivar name The filename of this attachment. * @ivar contentType The MIME content type of this part. * @ivar url The URL to stream if you want the contents of this part. * @ivar isExternal Is the attachment stored someplace else than in the message? * @ivar size The size of the attachment if available, -1 otherwise (size is set * after initialization by jsmimeemitter.js) */ function MimeMessageAttachment( aPartName, aName, aContentType, aUrl, aIsExternal ) { this.partName = aPartName; this.name = aName; this.contentType = aContentType; this.url = aUrl; this.isExternal = aIsExternal; this.headers = {}; this.isEncrypted = false; // parts is copied over from the part instance that preceded us // headers is copied over from the part instance that preceded us // isEncrypted is copied over from the part instance that preceded us } MimeMessageAttachment.prototype = { __proto__: HeaderHandlerBase, get allAttachments() { return [this]; // we are a leaf, so just us. }, get allInlineAttachments() { return [this]; // we are a leaf, so just us. }, get allUserAttachments() { return [this]; }, prettyString(aVerbose, aIndent, aDumpBody) { let s = "Attachment " + (this.isEncrypted ? "[encrypted] " : "") + "(" + this.size + " bytes): " + this.name + ", " + this.contentType; if (aVerbose) { s += this._prettyHeaderString(aIndent + " "); } return s; }, toString() { return this.prettyString(false, ""); }, };