/* 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/. */ this.EXPORTED_SYMBOLS = ['MsgHdrToMimeMessage', 'MimeMessage', 'MimeContainer', 'MimeBody', 'MimeUnknown', 'MimeMessageAttachment']; var Cc = Components.classes; var Ci = Components.interfaces; var Cr = Components.results; var Cu = Components.utils; Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); var EMITTER_MIME_CODE = "application/x-js-mime-message"; /** * The URL listener is surplus because the CallbackStreamListener ends up * getting the same set of events, effectively. */ var dumbUrlListener = { OnStartRunningUrl: function (aUrl) { }, OnStopRunningUrl: function (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: function mimemsg_shutdownCleanupObserver_init() { if (this._initialized) return; Services.obs.addObserver(this, "quit-application", false); this._initialized = true; }, observe: function mimemsg_shutdownCleanupObserver_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; let hdrURI = aMsgHdr.folder.getUriForMsg(aMsgHdr); 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; } CallbackStreamListener.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener]), // nsIRequestObserver part onStartRequest: function (aRequest, aContext) { this._request = aRequest; }, onStopRequest: function (aRequest, aContext, aStatusCode) { let msgURI = this._msgHdr.folder.getUriForMsg(this._msgHdr); delete activeStreamListeners[msgURI]; aContext.QueryInterface(Ci.nsIURI); let message = MsgHdrToMimeMessage.RESULT_RENDEVOUZ[aContext.spec]; if (message === undefined) message = null; delete MsgHdrToMimeMessage.RESULT_RENDEVOUZ[aContext.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; }, /* okay, our onDataAvailable should actually never be called. the stream converter is actually eating everything except the start and stop notification. */ // nsIStreamListener part onDataAvailable: function (aRequest,aContext,aInputStream,aOffset,aCount) { dump("this should not be happening! arrgggggh!\n"); if (this._stream === null) { this._stream = Cc["@mozilla.org/scriptableinputstream;1"]. createInstance(Ci.nsIScriptableInputStream); this._stream.init(aInputStream); } this._stream.read(aCount); }, }; var gMessenger = Cc["@mozilla.org/messenger;1"]. createInstance(Ci.nsIMessenger); 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 * situtations where we have erroneously multi-megabyte messages. This * also likely reduces the impact of legitimately ridiculously large * messages. * @param [aOptions.partsOnDemand] If this is a message stored on an IMAP * server, and for whatever reason, it isn't available locally, then setting * this option to true will make sure that attachments aren't downloaded. * This makes sure the message is available quickly. * @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; let msgURI = aMsgHdr.folder.getUriForMsg(aMsgHdr); let msgService = gMessenger.messageServiceFromURI(msgURI); MsgHdrToMimeMessage.OPTION_TUNNEL = aOptions; let partsOnDemandStr = (aOptions && aOptions.partsOnDemand) ? "&fetchCompleteMessage=false" : ""; // 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; else 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 { let streamURI = 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"+partsOnDemandStr+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. * @return 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: function MimeMessage_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]; else 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. * @return An array containing the values observed, which may mean a zero * length array. */ getAll: function MimeMessage_getAll(aHeaderName) { let lowerHeader = aHeaderName.toLowerCase(); if (lowerHeader in this.headers) return this.headers[lowerHeader]; else return []; }, /** * @param aHeaderName Header name to test for its presence. * @return true if the message has (at least one value for) the given header * name. */ has: function MimeMessage_has(aHeaderName) { let lowerHeader = aHeaderName.toLowerCase(); return lowerHeader in this.headers; }, _prettyHeaderString: function MimeMessage__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", /** * @return 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; }, /** * @return 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]; else // Why is there no flatten method for arrays? return this.parts.map(child => child.allUserAttachments) .reduce((a, b) => a.concat(b), []); }, /** * @return 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. * @return 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: function MimeMessage_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(""); else 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: function MimeMessage_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 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: function MimeContainer_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; // text/enriched gets transformed into HTML, use it if we don't already // have an HTML part. else if (!htmlPart && part.contentType == "text/enriched") 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: function MimeContainer_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: function MimeContainer_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 allUserAttachments() { return []; // we are a leaf }, get size() { return this.body.length; }, set size (whatever) { // nop }, appendBody: function MimeBody_append(aBuf) { this.body += aBuf; }, coerceBodyToPlaintext: function MimeBody_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: function MimeBody_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: function MimeBody_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 intepret 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 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: function MimeUnknown_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: function MimeUnknown_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, // This is a legacy property. get isRealAttachment() { return true; }, get allAttachments() { return [this]; // we are a leaf, so just us. }, get allUserAttachments() { return [this]; }, prettyString: function MimeMessageAttachment_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: function MimeMessageAttachment_toString() { return this.prettyString(false, ""); }, };