Revision control

Copy as Markdown

Other Tools

/* 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/. */
var kStateUnknown = 0;
var kStateInHeaders = 1;
var kStateInBody = 2;
var kStateInAttachment = 3;
/**
* When the saneBodySize flag is active, limit body parts to at most this many
* bytes. See |MsgHdrToMimeMessage| for more information on the flag.
*
* The choice of 20k was made on the very scientific basis of running a query
* against my indexed e-mail and finding the point where these things taper
* off. I chose 20 because things had tapered off pretty firmly by 16, so
* 20 gave it some space and it was also the end of a mini-plateau.
*/
var MAX_SANE_BODY_PART_SIZE = 20 * 1024;
/**
* Custom nsIMimeEmitter to build a sub-optimal javascript representation of a
* MIME message. The intent is that a better mechanism than is evolved to
* provide a javascript-accessible representation of the message.
*
* Processing occurs in two passes. During the first pass, libmime is parsing
* the stream it is receiving, and generating header and body events for all
* MimeMessage instances it encounters. This provides us with the knowledge
* of each nested message in addition to the top level message, their headers
* and sort-of their bodies. The sort-of is that we may get more than
* would normally be displayed in cases involving multipart/alternatives.
* We have augmented libmime to have a notify_nested_options parameter which
* is enabled when we are the consumer. This option causes MimeMultipart to
* always emit a content-type header (via addHeaderField), defaulting to
* text/plain when an explicit value is not present. Additionally,
* addHeaderField is called with a custom "x-jsemitter-part-path" header with
* the value being the part path (ex: 1.2.2). Having the part path greatly
* simplifies our life for building the part hierarchy.
* During the second pass, the libmime object model is traversed, generating
* attachment notifications for all leaf nodes. From our perspective, this
* means file attachments and embedded messages (message/rfc822). We use this
* pass to create the attachment objects proper, which we then substitute into
* the part tree we have already built.
*/
export function MimeMessageEmitter() {
this._mimeMsg = ChromeUtils.importESModule(
);
this._utils = ChromeUtils.importESModule(
);
this._url = null;
this._partRE = this._utils.GlodaUtils.PART_RE;
this._outputListener = null;
this._curPart = null;
this._curAttachment = null;
this._partMap = {};
this._bogusPartTranslation = {};
this._state = kStateUnknown;
this._writeBody = false;
}
var deathToNewlines = /\n/g;
MimeMessageEmitter.prototype = {
QueryInterface: ChromeUtils.generateQI(["nsIMimeEmitter"]),
initialize(aUrl) {
this._url = aUrl;
this._curPart = new this._mimeMsg.MimeMessage();
// the partName is intentionally ""! not a place-holder!
this._curPart.partName = "";
this._curAttachment = "";
this._partMap[""] = this._curPart;
// pull options across...
const options = this._mimeMsg.MsgHdrToMimeMessage.OPTION_TUNNEL;
this._saneBodySize =
options && "saneBodySize" in options ? options.saneBodySize : false;
this._mimeMsg.MsgHdrToMimeMessage.RESULT_RENDEVOUZ[aUrl.spec] =
this._curPart;
},
complete() {
this._url = null;
this._outputListener = null;
this._curPart = null;
this._curAttachment = null;
this._partMap = null;
this._bogusPartTranslation = null;
},
setPipe() {
// we do not care about these
},
set outputListener(aListener) {
this._outputListener = aListener;
},
get outputListener() {
return this._outputListener;
},
_stripParams(aValue) {
const indexSemi = aValue.indexOf(";");
if (indexSemi >= 0) {
aValue = aValue.substring(0, indexSemi);
}
return aValue;
},
_beginPayload(aContentType) {
const contentTypeNoParams = this._stripParams(aContentType).toLowerCase();
if (
contentTypeNoParams == "text/plain" ||
contentTypeNoParams == "text/html" ||
contentTypeNoParams == "text/enriched"
) {
this._curPart = new this._mimeMsg.MimeBody(contentTypeNoParams);
this._writeBody = true;
} else if (contentTypeNoParams == "message/rfc822") {
// startHeader will take care of this
this._curPart = new this._mimeMsg.MimeMessage();
// do not fall through into the content-type setting case; this
// content-type needs to get clobbered by the actual content-type of
// the enclosed message.
this._writeBody = false;
return;
} else if (contentTypeNoParams.startsWith("multipart/")) {
// this is going to fall-down with TNEF encapsulation and such, we really
// need to just be consuming the object model.
this._curPart = new this._mimeMsg.MimeContainer(contentTypeNoParams);
this._writeBody = false;
} else {
this._curPart = new this._mimeMsg.MimeUnknown(contentTypeNoParams);
this._writeBody = false;
}
// put the full content-type in the headers and normalize out any newlines
this._curPart.headers["content-type"] = [
aContentType.replace(deathToNewlines, ""),
];
},
// ----- Header Routines
/**
* StartHeader provides the base case for our processing. It is the first
* notification we receive when processing begins on the outer rfc822
* message. We do not receive an x-jsemitter-part-path notification for the
* message, but the aIsRootMailHeader tells us everything we need to know.
* (Or it would if we hadn't already set everything up in initialize.)
*
* When dealing with nested RFC822 messages, we will receive the
* addHeaderFields for the content-type and the x-jsemitter-part-path
* prior to the startHeader call. This is because the MIME multipart
* container that holds the message is the one generating the notification.
* For that reason, we do not process them here, but instead in
* addHeaderField and _beginPayload.
*
* We do need to track our state for addHeaderField's benefit though.
*/
startHeader() {
this._state = kStateInHeaders;
},
/**
* Receives a header field name and value for the current MIME part, which
* can be an rfc822/message or one of its sub-parts.
*
* The emitter architecture treats rfc822/messages as special because it was
* architected around presentation. In that case, the organizing concept
* is the single top-level rfc822/message. (It did not 'look into' nested
* messages in most cases.)
* As a result the interface is biased towards being 'in the headers' or
* 'in the body', corresponding to calls to startHeader and startBody,
* respectively.
* This information is interesting to us because the message itself is an
* odd pseudo-mime-part. Because it has only one child, its headers are,
* in a way, its payload, but they also serve as the description of its
* MIME child part. This introduces a complication in that we see the
* content-type for the message's "body" part before we actually see any
* of the headers. To deal with this, we punt on the construction of the
* body part to the call to startBody() and predicate our logic on the
* _state field.
*/
addHeaderField(aField, aValue) {
if (this._state == kStateInBody) {
aField = aField.toLowerCase();
if (aField == "content-type") {
this._beginPayload(aValue, true);
} else if (aField == "x-jsemitter-part-path") {
// This is either naming the current part, or referring to an already
// existing part (in the case of multipart/related on its second pass).
// As such, check if the name already exists in our part map.
const partName = this._stripParams(aValue);
// if it does, then make the already-existing part at that path current
if (partName in this._partMap) {
this._curPart = this._partMap[partName];
this._writeBody = "body" in this._curPart;
} else {
// otherwise, name the part we are holding onto and place it.
this._curPart.partName = partName;
this._placePart(this._curPart);
}
} else if (aField == "x-jsemitter-encrypted" && aValue == "1") {
this._curPart.isEncrypted = true;
}
// There is no other field to be emitted in the body case other than the
// ones we just handled. (They were explicitly added for the js
// emitter.)
} else if (this._state == kStateInHeaders) {
const lowerField = aField.toLowerCase();
if (lowerField in this._curPart.headers) {
this._curPart.headers[lowerField].push(aValue);
} else {
this._curPart.headers[lowerField] = [aValue];
}
}
},
addAllHeaders() {
// This is called by the parsing code after the calls to AddHeaderField (or
// AddAttachmentField if the part is an attachment), and seems to serve
// a specialized, quasi-redundant purpose. (nsMimeBaseEmitter creates a
// nsIMimeHeaders instance and hands it to the nsIMsgMailNewsUrl.)
// nop
},
writeHTMLHeaders() {
// It doesn't look like this should even be part of the interface; I think
// only the nsMimeHtmlDisplayEmitter::EndHeader call calls this signature.
// nop
},
endHeader() {},
updateCharacterSet() {
// we do not need to worry about this. it turns out this notification is
// exclusively for the benefit of the UI. libmime, believe it or not,
// is actually doing the right thing under the hood and handles all the
// encoding issues for us.
// so, get ready for the only time you will ever hear this:
// three cheers for libmime!
},
/**
* Place a part in its proper location; requires the parent to be present.
* However, we no longer require in-order addition of children. (This is
* currently a hedge against extension code doing wacky things. Our
* motivating use-case is multipart/related which actually does generate
* everything in order on its first pass, but has a wacky second pass. It
* does not actually trigger the out-of-order code because we have
* augmented the libmime code to generate its x-jsemitter-part-path info
* a second time, in which case we reuse the part we already created.)
*
* @param {MimeMessage} aPart - Part to place.
*/
_placePart(aPart) {
const partName = aPart.partName;
this._partMap[partName] = aPart;
const [storagePartName, , parentPart] = this._findOrCreateParent(partName);
const lastDotIndex = storagePartName.lastIndexOf(".");
if (parentPart !== undefined) {
const indexInParent =
parseInt(storagePartName.substring(lastDotIndex + 1)) - 1;
// handle out-of-order notification...
if (indexInParent < parentPart.parts.length) {
parentPart.parts[indexInParent] = aPart;
} else {
while (indexInParent > parentPart.parts.length) {
parentPart.parts.push(null);
}
parentPart.parts.push(aPart);
}
}
},
/**
* In case the MIME structure is wrong, (i.e. we have no parent to add the
* current part to), this function recursively makes sure we create the
* missing bits in the hierarchy.
* What happens in the case of encrypted emails (mimecryp.cpp):
* 1. is the message
* 1.1 doesn't exist
* 1.1.1 is the multipart/alternative that holds the text/plain and text/html
* 1.1.1.1 is text/plain
* 1.1.1.2 is text/html
* This function fills the missing bits.
*/
_findOrCreateParent(aPartName) {
const partName = aPartName + "";
const parentName = partName.substring(0, partName.lastIndexOf("."));
let parentPart;
if (parentName in this._partMap) {
parentPart = this._partMap[parentName];
const lastDotIndex = partName.lastIndexOf(".");
const indexInParent = parseInt(partName.substring(lastDotIndex + 1)) - 1;
if (
"parts" in parentPart &&
indexInParent == parentPart.parts.length - 1
) {
return [partName, parentName, parentPart];
}
return this._findAnotherContainer(aPartName);
}
// Find the grandparent
const [, , grandParentPart] = this._findOrCreateParent(parentName);
// Create the missing part.
parentPart = new this._mimeMsg.MimeContainer("multipart/fake-container");
// Add it to the grandparent, remember we added it in the hierarchy.
grandParentPart.parts.push(parentPart);
this._partMap[parentName] = parentPart;
return [partName, parentName, parentPart];
},
/**
* In the case of UUEncoded attachments, libmime tells us about the attachment
* as a child of a MimeBody. This obviously doesn't make us happy, so in case
* libmime wants us to attach an attachment to something that's not a
* container, we walk up the mime tree to find a suitable container to hold
* the attachment.
* The results are cached so that they're consistent across calls — this
* ensures the call to _replacePart works fine.
*/
_findAnotherContainer(aPartName) {
if (aPartName in this._bogusPartTranslation) {
return this._bogusPartTranslation[aPartName];
}
let parentName = aPartName + "";
let parentPart;
while (!(parentPart && "parts" in parentPart) && parentName.length) {
parentName = parentName.substring(0, parentName.lastIndexOf("."));
parentPart = this._partMap[parentName];
}
const childIndex = parentPart.parts.length;
const fallbackPartName =
(parentName ? parentName + "." : "") + (childIndex + 1);
return (this._bogusPartTranslation[aPartName] = [
fallbackPartName,
parentName,
parentPart,
]);
},
/**
* In the case of attachments, we need to replace an existing part with a
* more representative part...
*
* @param {MimeMessage} aPart - Part to place.
*/
_replacePart(aPart) {
// _partMap always maps the libmime names to parts
const partName = aPart.partName;
this._partMap[partName] = aPart;
const [storagePartName, , parentPart] = this._findOrCreateParent(partName);
const childNamePart = storagePartName.substring(
storagePartName.lastIndexOf(".") + 1
);
const childIndex = parseInt(childNamePart) - 1;
// The attachment has been encapsulated properly in a MIME part (most of
// the cases). This does not hold for UUencoded-parts for instance (see
// test_mime_attachments_size.js for instance).
if (childIndex < parentPart.parts.length) {
const oldPart = parentPart.parts[childIndex];
parentPart.parts[childIndex] = aPart;
// copy over information from the original part
aPart.parts = oldPart.parts;
aPart.headers = oldPart.headers;
aPart.isEncrypted = oldPart.isEncrypted;
} else {
parentPart.parts[childIndex] = aPart;
}
},
// ----- Attachment Routines
// The attachment processing happens after the initial streaming phase (during
// which time we receive the messages, both bodies and headers). Our caller
// traverses the libmime child object hierarchy, emitting an attachment for
// each leaf object or sub-message.
startAttachment(aName, aContentType, aUrl, aIsExternalAttachment) {
this._state = kStateInAttachment;
// we need to strip our magic flags from the URL; this regexp matches all
// the specific flags that the jsmimeemitter understands (we abuse the URL
// parameters to pass information all the way to here)
aUrl = aUrl.replace(
/((header=filter|emitter=js|examineEncryptedParts=(true|false)))&?/g,
""
);
// the url should contain a part= piece that tells us the part name, which
// we then use to figure out where to place that part if it's a real
// attachment.
let partMatch, partName;
if (aUrl.startsWith("http") || aUrl.startsWith("file")) {
// if we have a remote url, unlike non external mail part urls, it may also
// contain query strings starting with ?; PART_RE does not handle this.
partMatch = aUrl.match(/[?&]part=[^&]+$/);
partMatch = partMatch && partMatch[0];
partName = partMatch && partMatch.split("part=")[1];
} else {
partMatch = this._partRE.exec(aUrl);
partName = partMatch && partMatch[1];
}
this._curAttachment = partName;
if (aContentType == "message/rfc822") {
// we want to offer extension authors a way to see attachments as the
// message readers sees them, which means attaching an extra url property
// to the part that was already created before
if (partName) {
// we disguise this MimeMessage into something that can be used as a
// MimeAttachment so that it is transparent for the user code
this._partMap[partName].url = aUrl;
this._partMap[partName].isExternal = aIsExternalAttachment;
this._partMap[partName].name = aName;
}
} else if (partName) {
const part = new this._mimeMsg.MimeMessageAttachment(
partName,
aName,
aContentType,
aUrl,
aIsExternalAttachment
);
// replace the existing part with the attachment...
this._replacePart(part);
}
},
addAttachmentField(aField, aValue) {
// What gets passed in here is X-Mozilla-PartURL with a value that
// is completely identical to aUrl from the call to startAttachment.
// (it's the same variable they use in each case). As such, there is
// no reason to handle that here.
// However, we also pass information about the size of the attachment, and
// that we want to handle
if (
aField == "X-Mozilla-PartSize" &&
this._curAttachment in this._partMap
) {
this._partMap[this._curAttachment].size = parseInt(aValue);
}
},
endAttachment() {
// don't need to do anything here, since we don't care about the headers.
},
endAllAttachments() {
// nop
},
// ----- Body Routines
/**
* We don't get an x-jsemitter-part-path for the message body, and we ignored
* our body part's content-type in addHeaderField, so this serves as our
* notice to set up the part (giving it a name).
*/
startBody() {
this._state = kStateInBody;
const subPartName =
this._curPart.partName == "" ? "1" : this._curPart.partName + ".1";
this._beginPayload(this._curPart.get("content-type", "text/plain"));
this._curPart.partName = subPartName;
this._placePart(this._curPart);
},
/**
* Write to the body. When saneBodySize is active, we stop adding if we are
* already at the limit for this body part.
*/
writeBody(aBuf) {
if (
this._writeBody &&
(!this._saneBodySize || this._curPart.size < MAX_SANE_BODY_PART_SIZE)
) {
this._curPart.appendBody(aBuf);
}
},
endBody() {},
// ----- Generic Write (confusing)
// (binary data writing...)
write(aBuf, aSize, aOutAmountWritten) {
// we don't actually ever get called because we don't have the attachment
// binary payloads pass through us, but we do the following just in case
// we did get called (otherwise the caller gets mad and throws exceptions).
aOutAmountWritten.value = aSize;
},
// (string writing)
utilityWrite(aBuf) {
this.write(aBuf, aBuf.length, {});
},
};