summaryrefslogtreecommitdiff
path: root/mailnews/mime/src/mimeJSComponents.js
diff options
context:
space:
mode:
Diffstat (limited to 'mailnews/mime/src/mimeJSComponents.js')
-rw-r--r--mailnews/mime/src/mimeJSComponents.js512
1 files changed, 512 insertions, 0 deletions
diff --git a/mailnews/mime/src/mimeJSComponents.js b/mailnews/mime/src/mimeJSComponents.js
new file mode 100644
index 0000000000..5ba7ff084b
--- /dev/null
+++ b/mailnews/mime/src/mimeJSComponents.js
@@ -0,0 +1,512 @@
+/* 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/. */
+
+Components.utils.import("resource://gre/modules/Deprecated.jsm");
+Components.utils.import("resource:///modules/jsmime.jsm");
+Components.utils.import("resource:///modules/mimeParser.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.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: XPCOMUtils.generateQI([
+ Components.interfaces.nsIUTF8StringEnumerator]),
+ _setNext: function () {
+ if (this._next !== undefined)
+ return;
+ this._next = this._iterator.next();
+ },
+ hasMore: function () {
+ this._setNext();
+ return !this._next.done;
+ },
+ getNext: function () {
+ this._setNext();
+ let result = this._next;
+ this._next = undefined;
+ if (result.done)
+ throw Components.results.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);
+ } else {
+ 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: function (aHeaderName) {
+ let name = aHeaderName.toLowerCase();
+ return this._headers.get(name);
+ },
+
+ hasHeader: function (aHeaderName) {
+ return this._headers.has(aHeaderName.toLowerCase());
+ },
+
+ getUnstructuredHeader: function (aHeaderName) {
+ let result = this.getHeader(aHeaderName);
+ if (result === undefined || typeof result == "string")
+ return result;
+ throw Components.results.NS_ERROR_ILLEGAL_VALUE;
+ },
+
+ getAddressingHeader: function (aHeaderName, aPreserveGroups, count) {
+ let addrs = this.getHeader(aHeaderName);
+ if (addrs === undefined) {
+ addrs = [];
+ } else if (!Array.isArray(addrs)) {
+ throw Components.results.NS_ERROR_ILLEGAL_VALUE;
+ }
+ return fixArray(addrs, aPreserveGroups, count);
+ },
+
+ getRawHeader: function (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: function () {
+ if (this._headers.size == 0) {
+ return "";
+ }
+ let handler = new HeaderHandler();
+ let emitter = jsmime.headeremitter.makeStreamingEmitter(handler, {
+ useASCII: true
+ });
+ 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",
+ classID: Components.ID("d1258011-f391-44fd-992e-c6f4b461a42f"),
+ contractID: "@mozilla.org/messenger/mimeheaders;1",
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIMimeHeaders,
+ Components.interfaces.msgIStructuredHeaders]),
+
+ initialize: function MimeHeaders_initialize(allHeaders) {
+ this._headers = MimeParser.extractHeaders(allHeaders);
+ },
+
+ extractHeader: function MimeHeaders_extractHeader(header, getAll) {
+ if (!this._headers)
+ throw Components.results.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");
+ else
+ return values[0];
+ },
+
+ get allHeaders() {
+ return this._headers.rawHeaderText;
+ }
+};
+
+function MimeWritableStructuredHeaders() {
+ this._headers = new Map();
+}
+MimeWritableStructuredHeaders.prototype = {
+ __proto__: MimeStructuredHeaders.prototype,
+ classID: Components.ID("c560806a-425f-4f0f-bf69-397c58c599a7"),
+ QueryInterface: XPCOMUtils.generateQI([
+ Components.interfaces.msgIStructuredHeaders,
+ Components.interfaces.msgIWritableStructuredHeaders]),
+
+ setHeader: function (aHeaderName, aValue) {
+ this._headers.set(aHeaderName.toLowerCase(), aValue);
+ },
+
+ deleteHeader: function (aHeaderName) {
+ this._headers.delete(aHeaderName.toLowerCase());
+ },
+
+ addAllHeaders: function (aHeaders) {
+ let headerList = aHeaders.headerNames;
+ while (headerList.hasMore()) {
+ let header = headerList.getNext();
+ this.setHeader(header, aHeaders.getHeader(header));
+ }
+ },
+
+ setUnstructuredHeader: function (aHeaderName, aValue) {
+ this.setHeader(aHeaderName, aValue);
+ },
+
+ setAddressingHeader: function (aHeaderName, aAddresses, aCount) {
+ this.setHeader(aHeaderName, fixXpconnectAddresses(aAddresses));
+ },
+
+ setRawHeader: function (aHeaderName, aValue, aCharset) {
+ aValue = jsmime.headerparser.convert8BitHeader(aValue, aCharset);
+ 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);
+ }
+ }
+};
+
+// These are prototypes for nsIMsgHeaderParser implementation
+var Mailbox = {
+ toString: function () {
+ return this.name ? this.name + " <" + this.email + ">" : this.email;
+ }
+};
+
+var EmailGroup = {
+ toString: function () {
+ 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") {
+ 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 = {
+ classID: Components.ID("96bd8769-2d0e-4440-963d-22b97fb3ba77"),
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIMsgHeaderParser]),
+
+ parseEncodedHeader: function (aHeader, aCharset, aPreserveGroups, count) {
+ aHeader = aHeader || "";
+ let value = MimeParser.parseHeaderField(aHeader,
+ MimeParser.HEADER_ADDRESS | MimeParser.HEADER_OPTION_ALL_I18N, aCharset);
+ return fixArray(value, aPreserveGroups, count);
+ },
+ parseDecodedHeader: function (aHeader, aPreserveGroups, count) {
+ aHeader = aHeader || "";
+ let value = MimeParser.parseHeaderField(aHeader, MimeParser.HEADER_ADDRESS);
+ return fixArray(value, aPreserveGroups, count);
+ },
+
+ makeMimeHeader: function (addresses, length) {
+ 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: function (aHeader) {
+ let address = this.parseDecodedHeader(aHeader, false)[0];
+ return address.name || address.email;
+ },
+
+ removeDuplicateAddresses: function (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: function (aName, aEmail) {
+ let object = Object.create(Mailbox);
+ object.name = aName;
+ object.email = aEmail ? aEmail.trim() : aEmail;
+ return object;
+ },
+
+ makeGroupObject: function (aName, aMembers) {
+ let object = Object.create(EmailGroup);
+ object.name = aName;
+ object.group = aMembers;
+ return object;
+ },
+
+ makeFromDisplayAddress: function (aDisplay, count) {
+ // The basic idea is to split on every comma, so long as there is a
+ // preceding @.
+ let output = [];
+ while (aDisplay.length) {
+ let at = aDisplay.indexOf('@');
+ let comma = aDisplay.indexOf(',', at + 1);
+ let addr;
+ if (comma > 0) {
+ addr = aDisplay.substr(0, comma);
+ aDisplay = aDisplay.substr(comma + 1);
+ } else {
+ addr = aDisplay;
+ aDisplay = "";
+ }
+ output.push(this._makeSingleAddress(addr.trimLeft()));
+ }
+ if (count)
+ count.value = output.length;
+ return output;
+ },
+
+ /// Construct a single email address from a name <local@domain> token.
+ _makeSingleAddress: function (aDisplayName) {
+ if (aDisplayName.includes('<')) {
+ let lbracket = aDisplayName.lastIndexOf('<');
+ let rbracket = aDisplayName.lastIndexOf('>');
+ return this.makeMailboxObject(
+ lbracket == 0 ? '' : aDisplayName.slice(0, lbracket).trim(),
+ aDisplayName.slice(lbracket + 1, rbracket));
+ } else {
+ return this.makeMailboxObject('', aDisplayName);
+ }
+ },
+
+ // What follows is the deprecated API that will be removed shortly.
+
+ parseHeadersWithArray: function (aHeader, aAddrs, aNames, aFullNames) {
+ let addrs = [], names = [], fullNames = [];
+ // Parse header, but without HEADER_OPTION_ALLOW_RAW.
+ let value = MimeParser.parseHeaderField(aHeader || "",
+ MimeParser.HEADER_ADDRESS |
+ MimeParser.HEADER_OPTION_DECODE_2231 |
+ MimeParser.HEADER_OPTION_DECODE_2047,
+ undefined);
+ let allAddresses = fixArray(value, false);
+
+ // Don't index the dummy empty address.
+ if (aHeader.trim() == "")
+ allAddresses = [];
+ for (let address of allAddresses) {
+ addrs.push(address.email);
+ names.push(address.name || null);
+ fullNames.push(address.toString());
+ }
+
+ aAddrs.value = addrs;
+ aNames.value = names;
+ aFullNames.value = fullNames;
+ return allAddresses.length;
+ },
+
+ extractHeaderAddressMailboxes: function (aLine) {
+ return this.parseDecodedHeader(aLine).map(addr => addr.email).join(", ");
+ },
+
+ extractHeaderAddressNames: function (aLine) {
+ return this.parseDecodedHeader(aLine).map(addr => addr.name || addr.email)
+ .join(", ");
+ },
+
+ extractHeaderAddressName: function (aLine) {
+ let addrs = this.parseDecodedHeader(aLine).map(addr =>
+ addr.name || addr.email);
+ return addrs.length == 0 ? "" : addrs[0];
+ },
+
+ makeMimeAddress: function (aName, aEmail) {
+ let object = this.makeMailboxObject(aName, aEmail);
+ return this.makeMimeHeader([object]);
+ },
+};
+
+function MimeConverter() {
+}
+MimeConverter.prototype = {
+ classID: Components.ID("93f8c049-80ed-4dda-9000-94ad8daba44c"),
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIMimeConverter]),
+
+ encodeMimePartIIStr_UTF8: function (aHeader, aStructured, aCharset,
+ aFieldNameLen, aLineLength) {
+ // The JSMime encoder only works in UTF-8, so if someone requests to not do
+ // it, they need to change their code.
+ if (aCharset.toLowerCase() != "utf-8") {
+ Deprecated.warning("Encoding to non-UTF-8 values is obsolete",
+ "http://bugzilla.mozilla.org/show_bug.cgi?id=790855");
+ }
+
+ // 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: function (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: function () {
+ return this.decodeMimeHeader.apply(this, arguments);
+ },
+};
+
+var components = [MimeHeaders, MimeWritableStructuredHeaders, MimeAddressParser,
+ MimeConverter];
+var NSGetFactory = XPCOMUtils.generateNSGetFactory(components);