From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- comm/mailnews/db/gloda/modules/MimeMessage.jsm | 821 +++++++++++++++++++++++++ 1 file changed, 821 insertions(+) create mode 100644 comm/mailnews/db/gloda/modules/MimeMessage.jsm (limited to 'comm/mailnews/db/gloda/modules/MimeMessage.jsm') diff --git a/comm/mailnews/db/gloda/modules/MimeMessage.jsm b/comm/mailnews/db/gloda/modules/MimeMessage.jsm new file mode 100644 index 0000000000..8859f10877 --- /dev/null +++ b/comm/mailnews/db/gloda/modules/MimeMessage.jsm @@ -0,0 +1,821 @@ +/* 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, ""); + }, +}; -- cgit v1.2.3