diff options
Diffstat (limited to 'comm/mail/extensions/openpgp/content/modules/fixExchangeMsg.jsm')
-rw-r--r-- | comm/mail/extensions/openpgp/content/modules/fixExchangeMsg.jsm | 433 |
1 files changed, 433 insertions, 0 deletions
diff --git a/comm/mail/extensions/openpgp/content/modules/fixExchangeMsg.jsm b/comm/mail/extensions/openpgp/content/modules/fixExchangeMsg.jsm new file mode 100644 index 0000000000..753041cf1c --- /dev/null +++ b/comm/mail/extensions/openpgp/content/modules/fixExchangeMsg.jsm @@ -0,0 +1,433 @@ +/* + * 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 https://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["EnigmailFixExchangeMsg"]; + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm", + EnigmailLog: "chrome://openpgp/content/modules/log.jsm", + EnigmailMime: "chrome://openpgp/content/modules/mime.jsm", + EnigmailStreams: "chrome://openpgp/content/modules/streams.jsm", + EnigmailPersistentCrypto: + "chrome://openpgp/content/modules/persistentCrypto.jsm", +}); + +var EnigmailFixExchangeMsg = { + /* + * Fix a broken message from MS-Exchange and replace it with the original message + * + * @param {nsIMsgDBHdr} hdr - Header of the message to fix (= pointer to message) + * @param {string} brokenByApp - Type of app that created the message. Currently one of + * exchange, iPGMail + * @param {string} [destFolderUri] optional destination Folder URI + * + * @return {nsMsgKey} upon success, the promise returns the messageKey + */ + async fixExchangeMessage(hdr, brokenByApp, destFolderUri = null) { + let msgUriSpec = hdr.folder.getUriForMsg(hdr); + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: fixExchangeMessage: msgUriSpec: " + msgUriSpec + "\n" + ); + + this.hdr = hdr; + this.brokenByApp = brokenByApp; + this.destFolderUri = destFolderUri; + + this.msgSvc = MailServices.messageServiceFromURI(msgUriSpec); + + let fixedMsgData = await this.getMessageBody(); + + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: fixExchangeMessage: got fixedMsgData\n" + ); + this.ensureExpectedStructure(fixedMsgData); + return lazy.EnigmailPersistentCrypto.copyMessageToFolder( + this.hdr, + this.destFolderUri, + true, + fixedMsgData, + null + ); + }, + + getMessageBody() { + lazy.EnigmailLog.DEBUG("fixExchangeMsg.jsm: getMessageBody:\n"); + + var self = this; + + return new Promise(function (resolve, reject) { + let url = lazy.EnigmailFuncs.getUrlFromUriSpec( + self.hdr.folder.getUriForMsg(self.hdr) + ); + + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: getting data from URL " + url + "\n" + ); + + let s = lazy.EnigmailStreams.newStringStreamListener(function (data) { + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: analyzeDecryptedData: got " + + data.length + + " bytes\n" + ); + + if (lazy.EnigmailLog.getLogLevel() > 5) { + lazy.EnigmailLog.DEBUG( + "*** start data ***\n'" + data + "'\n***end data***\n" + ); + } + + let [good, errorCode, msg] = self.getRepairedMessage(data); + + if (!good) { + reject(errorCode); + } else { + resolve(msg); + } + }); + + try { + let channel = lazy.EnigmailStreams.createChannel(url); + channel.asyncOpen(s, null); + } catch (e) { + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: getMessageBody: exception " + e + "\n" + ); + } + }); + }, + + getRepairedMessage(data) { + this.determineCreatorApp(data); + + let hdrEnd = data.search(/\r?\n\r?\n/); + + if (hdrEnd <= 0) { + // cannot find end of header data + return [false, 0, ""]; + } + + let hdrLines = data.substr(0, hdrEnd).split(/\r?\n/); + let hdrObj = this.getFixedHeaderData(hdrLines); + + if (hdrObj.headers.length === 0 || hdrObj.boundary.length === 0) { + return [false, 1, ""]; + } + + let boundary = hdrObj.boundary; + let body; + + switch (this.brokenByApp) { + case "exchange": + body = this.getCorrectedExchangeBodyData( + data.substr(hdrEnd + 2), + boundary + ); + break; + case "iPGMail": + body = this.getCorrectediPGMailBodyData( + data.substr(hdrEnd + 2), + boundary + ); + break; + default: + lazy.EnigmailLog.ERROR( + "fixExchangeMsg.jsm: getRepairedMessage: unknown appType " + + this.brokenByApp + + "\n" + ); + return [false, 99, ""]; + } + + if (body) { + return [true, 0, hdrObj.headers + "\r\n" + body]; + } + return [false, 22, ""]; + }, + + determineCreatorApp(msgData) { + // perform extra testing if iPGMail is assumed + if (this.brokenByApp === "exchange") { + return; + } + + let msgTree = lazy.EnigmailMime.getMimeTree(msgData, false); + + try { + let isIPGMail = + msgTree.subParts.length === 3 && + (msgTree.subParts[0].headers.get("content-type").type.toLowerCase() === + "text/plain" || + msgTree.subParts[0].headers.get("content-type").type.toLowerCase() === + "multipart/alternative") && + msgTree.subParts[1].headers.get("content-type").type.toLowerCase() === + "application/pgp-encrypted" && + msgTree.subParts[2].headers.get("content-type").type.toLowerCase() === + "text/plain"; + + if (!isIPGMail) { + this.brokenByApp = "exchange"; + } + } catch (x) {} + }, + + /** + * repair header data, such that they are working for PGP/MIME + * + * @return: object: { + * headers: String - all headers ready for appending to message + * boundary: String - MIME part boundary (incl. surrounding "" or '') + * } + */ + getFixedHeaderData(hdrLines) { + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: getFixedHeaderData: hdrLines[]:'" + + hdrLines.length + + "'\n" + ); + let r = { + headers: "", + boundary: "", + }; + + for (let i = 0; i < hdrLines.length; i++) { + if (hdrLines[i].search(/^content-type:/i) >= 0) { + // Join the rest of the content type lines together. + // See RFC 2425, section 5.8.1 + let contentTypeLine = hdrLines[i]; + i++; + while (i < hdrLines.length) { + let endOfCTL = false; + // Does the line start with a space or a tab, followed by something else? + if (hdrLines[i].search(/^[ \t]+?/) === 0) { + contentTypeLine += hdrLines[i]; + i++; + if (i == hdrLines.length) { + endOfCTL = true; + } + } else { + endOfCTL = true; + } + if (endOfCTL) { + // we got the complete content-type header + contentTypeLine = contentTypeLine.replace(/[\r\n]/g, ""); + let h = lazy.EnigmailFuncs.getHeaderData(contentTypeLine); + r.boundary = h.boundary || ""; + break; + } + } + } else { + r.headers += hdrLines[i] + "\r\n"; + } + } + + r.boundary = r.boundary.replace(/^(['"])(.*)(\1)$/, "$2"); + + r.headers += + "Content-Type: multipart/encrypted;\r\n" + + ' protocol="application/pgp-encrypted";\r\n' + + ' boundary="' + + r.boundary + + '"\r\n' + + "X-Enigmail-Info: Fixed broken PGP/MIME message\r\n"; + + return r; + }, + + /** + * Get corrected body for MS-Exchange messages + */ + getCorrectedExchangeBodyData(bodyData, boundary) { + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: getCorrectedExchangeBodyData: boundary='" + + boundary + + "'\n" + ); + // Escape regex chars in the boundary. + boundary = boundary.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); + let boundRx = new RegExp("^--" + boundary, "gm"); + let match = boundRx.exec(bodyData); + + if (match.index < 0) { + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: getCorrectedExchangeBodyData: did not find index of mime type to skip\n" + ); + return null; + } + + let skipStart = match.index; + // found first instance -- that's the message part to ignore + match = boundRx.exec(bodyData); + if (match.index <= 0) { + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: getCorrectedExchangeBodyData: did not find boundary of PGP/MIME version identification\n" + ); + return null; + } + + let versionIdent = match.index; + + if ( + bodyData + .substring(skipStart, versionIdent) + .search( + /^content-type:[ \t]*(text\/(plain|html)|multipart\/alternative)/im + ) < 0 + ) { + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: getCorrectedExchangeBodyData: first MIME part is not content-type text/plain or text/html\n" + ); + return null; + } + + match = boundRx.exec(bodyData); + if (match.index < 0) { + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: getCorrectedExchangeBodyData: did not find boundary of PGP/MIME encrypted data\n" + ); + return null; + } + + let encData = match.index; + let mimeHdr = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance( + Ci.nsIMimeHeaders + ); + mimeHdr.initialize(bodyData.substring(versionIdent, encData)); + let ct = mimeHdr.extractHeader("content-type", false); + + if (!ct || ct.search(/application\/pgp-encrypted/i) < 0) { + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: getCorrectedExchangeBodyData: wrong content-type of version-identification\n" + ); + lazy.EnigmailLog.DEBUG(" ct = '" + ct + "'\n"); + return null; + } + + mimeHdr.initialize(bodyData.substr(encData, 5000)); + ct = mimeHdr.extractHeader("content-type", false); + if (!ct || ct.search(/application\/octet-stream/i) < 0) { + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: getCorrectedExchangeBodyData: wrong content-type of PGP/MIME data\n" + ); + lazy.EnigmailLog.DEBUG(" ct = '" + ct + "'\n"); + return null; + } + + return bodyData.substr(versionIdent); + }, + + /** + * Get corrected body for iPGMail messages + */ + getCorrectediPGMailBodyData(bodyData, boundary) { + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: getCorrectediPGMailBodyData: boundary='" + + boundary + + "'\n" + ); + // Escape regex chars. + boundary = boundary.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); + let boundRx = new RegExp("^--" + boundary, "gm"); + let match = boundRx.exec(bodyData); + + if (match.index < 0) { + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: getCorrectediPGMailBodyData: did not find index of mime type to skip\n" + ); + return null; + } + + // found first instance -- that's the message part to ignore + match = boundRx.exec(bodyData); + if (match.index <= 0) { + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: getCorrectediPGMailBodyData: did not find boundary of text/plain msg part\n" + ); + return null; + } + + let encData = match.index; + + match = boundRx.exec(bodyData); + if (match.index < 0) { + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: getCorrectediPGMailBodyData: did not find end boundary of PGP/MIME encrypted data\n" + ); + return null; + } + + let mimeHdr = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance( + Ci.nsIMimeHeaders + ); + + mimeHdr.initialize(bodyData.substr(encData, 5000)); + let ct = mimeHdr.extractHeader("content-type", false); + if (!ct || ct.search(/application\/pgp-encrypted/i) < 0) { + lazy.EnigmailLog.DEBUG( + "fixExchangeMsg.jsm: getCorrectediPGMailBodyData: wrong content-type of PGP/MIME data\n" + ); + lazy.EnigmailLog.DEBUG(" ct = '" + ct + "'\n"); + return null; + } + + return ( + "--" + + boundary + + "\r\n" + + "Content-Type: application/pgp-encrypted\r\n" + + "Content-Description: PGP/MIME version identification\r\n\r\n" + + "Version: 1\r\n\r\n" + + bodyData + .substring(encData, match.index) + .replace( + /^Content-Type: +application\/pgp-encrypted/im, + "Content-Type: application/octet-stream" + ) + + "--" + + boundary + + "--\r\n" + ); + }, + + ensureExpectedStructure(msgData) { + let msgTree = lazy.EnigmailMime.getMimeTree(msgData, true); + + // check message structure + let ok = + msgTree.headers.get("content-type").type.toLowerCase() === + "multipart/encrypted" && + msgTree.headers.get("content-type").get("protocol").toLowerCase() === + "application/pgp-encrypted" && + msgTree.subParts.length === 2 && + msgTree.subParts[0].headers.get("content-type").type.toLowerCase() === + "application/pgp-encrypted" && + msgTree.subParts[1].headers.get("content-type").type.toLowerCase() === + "application/octet-stream"; + + if (ok) { + // check for existence of PGP Armor + let body = msgTree.subParts[1].body; + let p0 = body.search(/^-----BEGIN PGP MESSAGE-----$/m); + let p1 = body.search(/^-----END PGP MESSAGE-----$/m); + + ok = p0 >= 0 && p1 > p0 + 32; + } + if (!ok) { + throw new Error("unexpected MIME structure"); + } + }, +}; |