summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/mime/src/MimeJSComponents.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/mime/src/MimeJSComponents.jsm')
-rw-r--r--comm/mailnews/mime/src/MimeJSComponents.jsm547
1 files changed, 547 insertions, 0 deletions
diff --git a/comm/mailnews/mime/src/MimeJSComponents.jsm b/comm/mailnews/mime/src/MimeJSComponents.jsm
new file mode 100644
index 0000000000..35acba7c26
--- /dev/null
+++ b/comm/mailnews/mime/src/MimeJSComponents.jsm
@@ -0,0 +1,547 @@
+/* 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 EXPORTED_SYMBOLS = [
+ "MimeHeaders",
+ "MimeWritableStructuredHeaders",
+ "MimeAddressParser",
+ "MimeConverter",
+];
+
+var { jsmime } = ChromeUtils.import("resource:///modules/jsmime.jsm");
+var { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+
+function HeaderHandler() {
+ this.value = "";
+ this.deliverData = function (str) {
+ this.value += str;
+ };
+ this.deliverEOF = function () {};
+}
+
+function StringEnumerator(iterator) {
+ this._iterator = iterator;
+ this._next = undefined;
+}
+StringEnumerator.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIUTF8StringEnumerator"]),
+ [Symbol.iterator]() {
+ return this._iterator;
+ },
+ _setNext() {
+ if (this._next !== undefined) {
+ return;
+ }
+ this._next = this._iterator.next();
+ },
+ hasMore() {
+ this._setNext();
+ return !this._next.done;
+ },
+ getNext() {
+ this._setNext();
+ let result = this._next;
+ this._next = undefined;
+ if (result.done) {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+ return result.value;
+ },
+};
+
+/**
+ * If we get XPConnect-wrapped objects for msgIAddressObjects, we will have
+ * properties defined for 'group' that throws off jsmime. This function converts
+ * the addresses into the form that jsmime expects.
+ */
+function fixXpconnectAddresses(addrs) {
+ return addrs.map(addr => {
+ // This is ideally !addr.group, but that causes a JS strict warning, if
+ // group is not in addr, since that's enabled in all chrome code now.
+ if (!("group" in addr) || addr.group === undefined || addr.group === null) {
+ return MimeAddressParser.prototype.makeMailboxObject(
+ addr.name,
+ addr.email
+ );
+ }
+ return MimeAddressParser.prototype.makeGroupObject(
+ addr.name,
+ fixXpconnectAddresses(addr.group)
+ );
+ });
+}
+
+/**
+ * This is a base handler for supporting msgIStructuredHeaders, since we have
+ * two implementations that need the readable aspects of the interface.
+ */
+function MimeStructuredHeaders() {}
+MimeStructuredHeaders.prototype = {
+ getHeader(aHeaderName) {
+ let name = aHeaderName.toLowerCase();
+ return this._headers.get(name);
+ },
+
+ hasHeader(aHeaderName) {
+ return this._headers.has(aHeaderName.toLowerCase());
+ },
+
+ getUnstructuredHeader(aHeaderName) {
+ let result = this.getHeader(aHeaderName);
+ if (result === undefined || typeof result == "string") {
+ return result;
+ }
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ },
+
+ getAddressingHeader(aHeaderName, aPreserveGroups) {
+ let addrs = this.getHeader(aHeaderName);
+ if (addrs === undefined) {
+ addrs = [];
+ } else if (!Array.isArray(addrs)) {
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+ return fixArray(addrs, aPreserveGroups);
+ },
+
+ getRawHeader(aHeaderName) {
+ let result = this.getHeader(aHeaderName);
+ if (result === undefined) {
+ return result;
+ }
+
+ let value = jsmime.headeremitter.emitStructuredHeader(
+ aHeaderName,
+ result,
+ {}
+ );
+ // Strip off the header name and trailing whitespace before returning...
+ value = value.substring(aHeaderName.length + 2).trim();
+ // ... as well as embedded newlines.
+ value = value.replace(/\r\n/g, "");
+ return value;
+ },
+
+ get headerNames() {
+ return new StringEnumerator(this._headers.keys());
+ },
+
+ buildMimeText(sanitizeDate) {
+ if (this._headers.size == 0) {
+ return "";
+ }
+ let handler = new HeaderHandler();
+ let emitter = jsmime.headeremitter.makeStreamingEmitter(handler, {
+ useASCII: true,
+ sanitizeDate,
+ });
+ for (let [value, header] of this._headers) {
+ emitter.addStructuredHeader(value, header);
+ }
+ emitter.finish();
+ return handler.value;
+ },
+};
+
+function MimeHeaders() {}
+MimeHeaders.prototype = {
+ __proto__: MimeStructuredHeaders.prototype,
+ classDescription: "Mime headers implementation",
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIMimeHeaders",
+ "msgIStructuredHeaders",
+ ]),
+
+ initialize(allHeaders) {
+ this._headers = MimeParser.extractHeaders(allHeaders);
+ },
+
+ extractHeader(header, getAll) {
+ if (!this._headers) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
+ }
+ // Canonicalized to lower-case form
+ header = header.toLowerCase();
+ if (!this._headers.has(header)) {
+ return null;
+ }
+ var values = this._headers.getRawHeader(header);
+ if (getAll) {
+ return values.join(",\r\n\t");
+ }
+ return values[0];
+ },
+
+ get allHeaders() {
+ return this._headers.rawHeaderText;
+ },
+};
+
+function MimeWritableStructuredHeaders() {
+ this._headers = new Map();
+}
+MimeWritableStructuredHeaders.prototype = {
+ __proto__: MimeStructuredHeaders.prototype,
+ QueryInterface: ChromeUtils.generateQI([
+ "msgIStructuredHeaders",
+ "msgIWritableStructuredHeaders",
+ ]),
+
+ setHeader(aHeaderName, aValue) {
+ this._headers.set(aHeaderName.toLowerCase(), aValue);
+ },
+
+ deleteHeader(aHeaderName) {
+ this._headers.delete(aHeaderName.toLowerCase());
+ },
+
+ addAllHeaders(aHeaders) {
+ for (let header of aHeaders.headerNames) {
+ this.setHeader(header, aHeaders.getHeader(header));
+ }
+ },
+
+ setUnstructuredHeader(aHeaderName, aValue) {
+ this.setHeader(aHeaderName, aValue);
+ },
+
+ setAddressingHeader(aHeaderName, aAddresses) {
+ this.setHeader(aHeaderName, fixXpconnectAddresses(aAddresses));
+ },
+
+ setRawHeader(aHeaderName, aValue) {
+ try {
+ this.setHeader(
+ aHeaderName,
+ jsmime.headerparser.parseStructuredHeader(aHeaderName, aValue)
+ );
+ } catch (e) {
+ // This means we don't have a structured encoder. Just assume it's a raw
+ // string value then.
+ this.setHeader(aHeaderName, aValue.trim());
+ }
+ },
+};
+
+// These are prototypes for nsIMsgHeaderParser implementation
+var Mailbox = {
+ toString() {
+ return this.name ? this.name + " <" + this.email + ">" : this.email;
+ },
+};
+
+var EmailGroup = {
+ toString() {
+ return this.name + ": " + this.group.map(x => x.toString()).join(", ");
+ },
+};
+
+// A helper method for parse*Header that takes into account the desire to
+// preserve group and also tweaks the output to support the prototypes for the
+// XPIDL output.
+function fixArray(addresses, preserveGroups, count) {
+ function resetPrototype(obj, prototype) {
+ let prototyped = Object.create(prototype);
+ for (let key of Object.getOwnPropertyNames(obj)) {
+ if (typeof obj[key] == "string") {
+ // eslint-disable-next-line no-control-regex
+ prototyped[key] = obj[key].replace(/\x00/g, "");
+ } else {
+ prototyped[key] = obj[key];
+ }
+ }
+ return prototyped;
+ }
+ let outputArray = [];
+ for (let element of addresses) {
+ if ("group" in element) {
+ // Fix up the prototypes of the group and the list members
+ element = resetPrototype(element, EmailGroup);
+ element.group = element.group.map(e => resetPrototype(e, Mailbox));
+
+ // Add to the output array
+ if (preserveGroups) {
+ outputArray.push(element);
+ } else {
+ outputArray = outputArray.concat(element.group);
+ }
+ } else {
+ element = resetPrototype(element, Mailbox);
+ outputArray.push(element);
+ }
+ }
+
+ if (count) {
+ count.value = outputArray.length;
+ }
+ return outputArray;
+}
+
+function MimeAddressParser() {}
+MimeAddressParser.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgHeaderParser"]),
+
+ parseEncodedHeader(aHeader, aCharset, aPreserveGroups) {
+ aHeader = aHeader || "";
+ let value = MimeParser.parseHeaderField(
+ aHeader,
+ MimeParser.HEADER_ADDRESS | MimeParser.HEADER_OPTION_ALL_I18N,
+ aCharset
+ );
+ return fixArray(value, aPreserveGroups);
+ },
+ parseEncodedHeaderW(aHeader) {
+ aHeader = aHeader || "";
+ let value = MimeParser.parseHeaderField(
+ aHeader,
+ MimeParser.HEADER_ADDRESS |
+ MimeParser.HEADER_OPTION_DECODE_2231 |
+ MimeParser.HEADER_OPTION_DECODE_2047,
+ undefined
+ );
+ return fixArray(value, false);
+ },
+ parseDecodedHeader(aHeader, aPreserveGroups) {
+ aHeader = aHeader || "";
+ let value = MimeParser.parseHeaderField(aHeader, MimeParser.HEADER_ADDRESS);
+ return fixArray(value, aPreserveGroups);
+ },
+
+ makeMimeHeader(addresses) {
+ addresses = fixXpconnectAddresses(addresses);
+ // Don't output any necessary continuations, so make line length as large as
+ // possible first.
+ let options = {
+ softMargin: 900,
+ hardMargin: 900,
+ useASCII: false, // We don't want RFC 2047 encoding here.
+ };
+ let handler = new HeaderHandler();
+ let emitter = new jsmime.headeremitter.makeStreamingEmitter(
+ handler,
+ options
+ );
+ emitter.addAddresses(addresses);
+ emitter.finish(true);
+ return handler.value.replace(/\r\n( |$)/g, "");
+ },
+
+ extractFirstName(aHeader) {
+ let addresses = this.parseDecodedHeader(aHeader, false);
+ return addresses.length > 0 ? addresses[0].name || addresses[0].email : "";
+ },
+
+ removeDuplicateAddresses(aAddrs, aOtherAddrs) {
+ // This is actually a rather complicated algorithm, especially if we want to
+ // preserve group structure. Basically, we use a set to identify which
+ // headers we have seen and therefore want to remove. To work in several
+ // various forms of edge cases, we need to normalize the entries in that
+ // structure.
+ function normalize(email) {
+ // XXX: This algorithm doesn't work with IDN yet. It looks like we have to
+ // convert from IDN then do lower case, but I haven't confirmed yet.
+ return email.toLowerCase();
+ }
+
+ // The filtration function, which removes email addresses that are
+ // duplicates of those we have already seen.
+ function filterAccept(e) {
+ if ("email" in e) {
+ // If we've seen the address, don't keep this one; otherwise, add it to
+ // the list.
+ let key = normalize(e.email);
+ if (allAddresses.has(key)) {
+ return false;
+ }
+ allAddresses.add(key);
+ } else {
+ // Groups -> filter out all the member addresses.
+ e.group = e.group.filter(filterAccept);
+ }
+ return true;
+ }
+
+ // First, collect all of the emails to forcibly delete.
+ let allAddresses = new Set();
+ for (let element of this.parseDecodedHeader(aOtherAddrs, false)) {
+ allAddresses.add(normalize(element.email));
+ }
+
+ // The actual data to filter
+ let filtered = this.parseDecodedHeader(aAddrs, true).filter(filterAccept);
+ return this.makeMimeHeader(filtered);
+ },
+
+ makeMailboxObject(aName, aEmail) {
+ let object = Object.create(Mailbox);
+ object.name = aName;
+ object.email = aEmail ? aEmail.trim() : aEmail;
+ return object;
+ },
+
+ makeGroupObject(aName, aMembers) {
+ let object = Object.create(EmailGroup);
+ object.name = aName;
+ object.group = aMembers;
+ return object;
+ },
+
+ makeFromDisplayAddress(aDisplay) {
+ if (aDisplay.includes(";") && !/:.*;/.test(aDisplay)) {
+ // Using semicolons as mailbox separators in against the standard, but
+ // used in the wild by some clients.
+ // Looks like this isn't using group syntax, so let's assume it's a
+ // non-standards compliant input string, and fix it.
+ // Replace semicolons with commas, unless the semicolon is inside a quote.
+ // The regexp uses tricky lookahead, see bug 1059988 comment #70 for details.
+ aDisplay = aDisplay.replace(/;(?=(?:(?:[^"]*"){2})*[^"]*$)/g, ",");
+ }
+
+ // The basic idea is to split on every comma, so long as there is a
+ // preceding @ or <> pair.
+ let output = [];
+ while (aDisplay.length > 0) {
+ let lt = aDisplay.indexOf("<");
+ let gt = aDisplay.indexOf(">");
+ let at = aDisplay.indexOf("@");
+ let start = 0;
+ // An address doesn't always contain both <> and @, the goal is to find
+ // the first comma after <> or @.
+ if (lt != -1 && gt > lt) {
+ start = gt;
+ }
+ if (at != -1) {
+ start = Math.min(start, at);
+ }
+ let comma = aDisplay.indexOf(",", start);
+ let addr;
+ if (comma > 0) {
+ addr = aDisplay.substr(0, comma);
+ aDisplay = aDisplay.substr(comma + 1);
+
+ // Make sure we don't have any "empty addresses" (multiple commas).
+ comma = 0;
+ while (/[,\s]/.test(aDisplay.charAt(comma))) {
+ comma++;
+ }
+ aDisplay = aDisplay.substr(comma);
+ } else {
+ addr = aDisplay;
+ aDisplay = "";
+ }
+ addr = addr.trimLeft();
+ if (addr) {
+ output.push(this._makeSingleAddress(addr));
+ }
+ }
+ return output;
+ },
+
+ /**
+ * Construct a single email address from an |name <local@domain>| token.
+ *
+ * @param {string} aInput - a string to be parsed to a mailbox object.
+ * @returns {msgIAddressObject} the mailbox parsed from the input.
+ */
+ _makeSingleAddress(aInput) {
+ // If the whole string is within quotes, unquote it first.
+ aInput = aInput.trim().replace(/^"(.*)"$/, "$1");
+
+ if (/<.*>/.test(aInput)) {
+ // We don't want to look for the address within quotes, so first remove
+ // all quoted strings containing angle chars.
+ let cleanedInput = aInput.replace(/".*[<>]+.*"/g, "");
+
+ // Extract the address from within the quotes.
+ let addrMatch = cleanedInput.match(/<([^><]*)>/);
+
+ let addr = addrMatch ? addrMatch[1] : "";
+ let addrIdx = aInput.indexOf("<" + addr + ">");
+ return this.makeMailboxObject(aInput.slice(0, addrIdx).trim(), addr);
+ }
+ return this.makeMailboxObject("", aInput);
+ },
+
+ extractHeaderAddressMailboxes(aLine) {
+ return this.parseDecodedHeader(aLine)
+ .map(addr => addr.email)
+ .join(", ");
+ },
+
+ makeMimeAddress(aName, aEmail) {
+ let object = this.makeMailboxObject(aName, aEmail);
+ return this.makeMimeHeader([object]);
+ },
+};
+
+function MimeConverter() {}
+MimeConverter.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIMimeConverter"]),
+
+ encodeMimePartIIStr_UTF8(aHeader, aStructured, aFieldNameLen, aLineLength) {
+ // Compute the encoding options. The way our API is structured in this
+ // method is really horrendous and does not align with the way that JSMime
+ // handles it. Instead, we'll need to create a fake header to take into
+ // account the aFieldNameLen parameter.
+ let fakeHeader = "-".repeat(aFieldNameLen);
+ let options = {
+ softMargin: aLineLength,
+ useASCII: true,
+ };
+ let handler = new HeaderHandler();
+ let emitter = new jsmime.headeremitter.makeStreamingEmitter(
+ handler,
+ options
+ );
+
+ // Add the text to the be encoded.
+ emitter.addHeaderName(fakeHeader);
+ if (aStructured) {
+ // Structured really means "this is an addressing header"
+ let addresses = MimeParser.parseHeaderField(
+ aHeader,
+ MimeParser.HEADER_ADDRESS | MimeParser.HEADER_OPTION_DECODE_2047
+ );
+ // This happens in one of our tests if there is a "bare" email but no
+ // @ sign. Without it, the result disappears since our emission code
+ // assumes that an empty email is not worth emitting.
+ if (
+ addresses.length === 1 &&
+ addresses[0].email === "" &&
+ addresses[0].name !== ""
+ ) {
+ addresses[0].email = addresses[0].name;
+ addresses[0].name = "";
+ }
+ emitter.addAddresses(addresses);
+ } else {
+ emitter.addUnstructured(aHeader);
+ }
+
+ // Compute the output. We need to strip off the fake prefix added earlier
+ // and the extra CRLF at the end.
+ emitter.finish(true);
+ let value = handler.value;
+ value = value.replace(new RegExp(fakeHeader + ":\\s*"), "");
+ return value.substring(0, value.length - 2);
+ },
+
+ decodeMimeHeader(aHeader, aDefaultCharset, aOverride, aUnfold) {
+ let value = MimeParser.parseHeaderField(
+ aHeader,
+ MimeParser.HEADER_UNSTRUCTURED | MimeParser.HEADER_OPTION_ALL_I18N,
+ aDefaultCharset
+ );
+ if (aUnfold) {
+ value = value.replace(/[\r\n]\t/g, " ").replace(/[\r\n]/g, "");
+ }
+ return value;
+ },
+
+ // This is identical to the above, except for factors that are handled by the
+ // xpconnect conversion process
+ decodeMimeHeaderToUTF8(...aArgs) {
+ return this.decodeMimeHeader(...aArgs);
+ },
+};