summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/db/gloda/modules/MimeMessage.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/db/gloda/modules/MimeMessage.jsm')
-rw-r--r--comm/mailnews/db/gloda/modules/MimeMessage.jsm821
1 files changed, 821 insertions, 0 deletions
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, "");
+ },
+};