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/mime/src/MimeJSComponents.jsm | 547 ++++++++++++++++++++++++++++ 1 file changed, 547 insertions(+) create mode 100644 comm/mailnews/mime/src/MimeJSComponents.jsm (limited to 'comm/mailnews/mime/src/MimeJSComponents.jsm') 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 | 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); + }, +}; -- cgit v1.2.3