/* * 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"; const { MailServices } = ChromeUtils.import( "resource:///modules/MailServices.jsm" ); var EXPORTED_SYMBOLS = ["EnigmailPersistentCrypto"]; const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm"); const lazy = {}; XPCOMUtils.defineLazyModuleGetters(lazy, { EnigmailArmor: "chrome://openpgp/content/modules/armor.jsm", EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm", EnigmailData: "chrome://openpgp/content/modules/data.jsm", EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm", EnigmailEncryption: "chrome://openpgp/content/modules/encryption.jsm", EnigmailLog: "chrome://openpgp/content/modules/log.jsm", EnigmailMime: "chrome://openpgp/content/modules/mime.jsm", EnigmailFixExchangeMsg: "chrome://openpgp/content/modules/fixExchangeMessage.jsm", EnigmailDecryption: "chrome://openpgp/content/modules/decryption.jsm", EnigmailDialog: "chrome://openpgp/content/modules/dialog.jsm", GlodaUtils: "resource:///modules/gloda/GlodaUtils.jsm", jsmime: "resource:///modules/jsmime.jsm", MailUtils: "resource:///modules/MailUtils.jsm", MailCryptoUtils: "resource:///modules/MailCryptoUtils.jsm", }); XPCOMUtils.defineLazyGetter(lazy, "l10n", () => { return new Localization(["messenger/openpgp/openpgp.ftl"], true); }); var EnigmailPersistentCrypto = { /*** * cryptMessage * * Decrypts a message and copy it to a folder. If targetKey is * not null, it encrypts a message to the target key afterwards. * * @param {nsIMsgDBHdr} hdr - message to process * @param {string} destFolder - target folder URI * @param {boolean} move - true for move, false for copy * @param {KeyObject} targetKey - target key if encryption is requested * * @returns {nsMsgKey} Message key of the new message **/ async cryptMessage(hdr, destFolder, move, targetKey) { return new Promise(function (resolve, reject) { let msgUriSpec = hdr.folder.getUriForMsg(hdr); let msgUrl = lazy.EnigmailFuncs.getUrlFromUriSpec(msgUriSpec); const crypt = new CryptMessageIntoFolder(destFolder, move, targetKey); lazy.EnigmailMime.getMimeTreeFromUrl(msgUrl, true, async function (mime) { try { let newMsgKey = await crypt.messageParseCallback(mime, hdr); resolve(newMsgKey); } catch (ex) { reject(ex); } }); }); }, changeMessageId(content, newMessageIdPrefix) { let [headerData, body] = MimeParser.extractHeadersAndBody(content); content = ""; let newHeaders = headerData.rawHeaderText; if (!newHeaders.endsWith("\r\n")) { newHeaders += "\r\n"; } headerData = undefined; let regExpMsgId = new RegExp("^message-id: <(.*)>", "mi"); let msgId; let match = newHeaders.match(regExpMsgId); if (match) { msgId = match[1]; newHeaders = newHeaders.replace( regExpMsgId, "Message-Id: <" + newMessageIdPrefix + "-$1>" ); // Match the references header across multiple lines // eslint-disable-next-line no-control-regex let regExpReferences = new RegExp("^references: .*([\r\n]*^ .*$)*", "mi"); let refLines = newHeaders.match(regExpReferences); if (refLines) { // Take the full match of the existing header let newRef = refLines[0] + " <" + msgId + ">"; newHeaders = newHeaders.replace(regExpReferences, newRef); } else { newHeaders += "References: <" + msgId + ">\r\n"; } } return newHeaders + "\r\n" + body; }, /* * Copies an email message to a folder, which is a modified copy of an * existing message, optionally creating a new message ID. * * @param {nsIMsgDBHdr} originalMsgHdr - Header of the original message * @param {string} targetFolderUri - Target folder URI * @param {boolean} deleteOrigMsg - Should the original message be deleted? * @param {string} content - New message content * @param {string} newMessageIdPrefix - If this is non-null, create a new message ID * by adding this prefix. * * @returns {nsMsgKey} Message key of the new message */ async copyMessageToFolder( originalMsgHdr, targetFolderUri, deleteOrigMsg, content, newMessageIdPrefix ) { lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: copyMessageToFolder()\n"); return new Promise((resolve, reject) => { if (newMessageIdPrefix) { content = this.changeMessageId(content, newMessageIdPrefix); } // Create the temporary file where the new message will be stored. let tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile); tempFile.append("message.eml"); tempFile.createUnique(0, 0o600); let outputStream = Cc[ "@mozilla.org/network/file-output-stream;1" ].createInstance(Ci.nsIFileOutputStream); outputStream.init(tempFile, 2, 0x200, false); // open as "write only" outputStream.write(content, content.length); outputStream.close(); // Delete file on exit, because Windows locks the file let extAppLauncher = Cc[ "@mozilla.org/uriloader/external-helper-app-service;1" ].getService(Ci.nsPIExternalAppLauncher); extAppLauncher.deleteTemporaryFileOnExit(tempFile); let msgFolder = originalMsgHdr.folder; // The following technique was copied from AttachmentDeleter in Thunderbird's // nsMessenger.cpp. There is a "unified" listener which serves as copy and delete // listener. In all cases, the `OnStopCopy()` of the delete listener selects the // replacement message. // The deletion happens in `OnStopCopy()` of the copy listener for local messages // and in `OnStopRunningUrl()` for IMAP messages if the folder is displayed since // otherwise `OnStopRunningUrl()` doesn't run. let copyListener, newKey; let statusCode = 0; let destFolder = targetFolderUri ? lazy.MailUtils.getExistingFolder(targetFolderUri) : msgFolder; copyListener = { QueryInterface: ChromeUtils.generateQI([ "nsIMsgCopyServiceListener", "nsIUrlListener", ]), GetMessageId(messageId) { // Maybe enable this later. Most of the Thunderbird code does not supply this. // messageId = { value: msgHdr.messageId }; }, SetMessageKey(key) { lazy.EnigmailLog.DEBUG( `persistentCrypto.jsm: copyMessageToFolder: Result of CopyFileMessage() is new message with key ${key}\n` ); newKey = key; }, applyFlags() { let newHdr = destFolder.GetMessageHeader(newKey); newHdr.markRead(originalMsgHdr.isRead); newHdr.markFlagged(originalMsgHdr.isFlagged); newHdr.subject = originalMsgHdr.subject; }, OnStartCopy() {}, OnStopCopy(status) { statusCode = status; if (statusCode !== 0) { lazy.EnigmailLog.ERROR( `persistentCrypto.jsm: ${statusCode} replacing message, folder="${msgFolder.name}", key=${originalMsgHdr.messageKey}/${newKey}\n` ); reject(); return; } try { tempFile.remove(); } catch (ex) {} lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: copyMessageToFolder: Triggering deletion from OnStopCopy()\n" ); this.applyFlags(); if (deleteOrigMsg) { lazy.EnigmailLog.DEBUG( `persistentCrypto.jsm: copyMessageToFolder: Deleting old message with key ${originalMsgHdr.messageKey}\n` ); msgFolder.deleteMessages( [originalMsgHdr], null, true, false, null, false ); } resolve(newKey); }, }; MailServices.copy.copyFileMessage( tempFile, destFolder, null, false, originalMsgHdr.flags, "", copyListener, null ); }); }, }; function CryptMessageIntoFolder(destFolder, move, targetKey) { this.destFolder = destFolder; this.move = move; this.targetKey = targetKey; this.cryptoChanged = false; this.decryptFailure = false; this.mimeTree = null; this.decryptionTasks = []; this.subject = ""; } CryptMessageIntoFolder.prototype = { /** Here is the effective action of a call to cryptMessage. * If no failure is seen when attempting to decrypt (!decryptFailure), * then we copy. (This includes plain messages that didn't need * decryption.) * The cryptoChanged flag is set only after we have successfully * completed a decryption (or encryption) operation, it's used to * decide whether we need a new message ID. */ async messageParseCallback(mimeTree, msgHdr) { this.mimeTree = mimeTree; this.hdr = msgHdr; if (mimeTree.headers.has("subject")) { this.subject = mimeTree.headers.get("subject"); } await this.decryptMimeTree(mimeTree); let msg = ""; // Encrypt the message if a target key is given. if (this.targetKey) { msg = this.encryptToKey(mimeTree); if (!msg) { throw new Error("Failure to encrypt message"); } this.cryptoChanged = true; } else { msg = this.mimeToString(mimeTree, true); } if (this.decryptFailure) { throw new Error("Failure to decrypt message"); } return EnigmailPersistentCrypto.copyMessageToFolder( this.hdr, this.destFolder, this.move, msg, this.cryptoChanged ? "decrypted-" + new Date().valueOf() : null ); }, encryptToKey(mimeTree) { let exitCodeObj = {}; let statusFlagsObj = {}; let errorMsgObj = {}; lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: Encrypting message.\n"); let inputMsg = this.mimeToString(mimeTree, false); let encmsg = ""; try { encmsg = lazy.EnigmailEncryption.encryptMessage( null, 0, inputMsg, "0x" + this.targetKey.fpr, "0x" + this.targetKey.fpr, "", lazy.EnigmailConstants.SEND_ENCRYPTED | lazy.EnigmailConstants.SEND_ALWAYS_TRUST, exitCodeObj, statusFlagsObj, errorMsgObj ); } catch (ex) { lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: Encryption failed: " + ex + "\n" ); return null; } // Build the pgp-encrypted mime structure let msg = ""; let rfc822Headers = []; // FIXME // First the original headers for (let header in rfc822Headers) { if ( header != "content-type" && header != "content-transfer-encoding" && header != "content-disposition" ) { msg += prettyPrintHeader(header, rfc822Headers[header]) + "\n"; } } // Then multipart/encrypted ct let boundary = lazy.EnigmailMime.createBoundary(); msg += "Content-Transfer-Encoding: 7Bit\n"; msg += "Content-Type: multipart/encrypted; "; msg += 'boundary="' + boundary + '"; protocol="application/pgp-encrypted"\n\n'; msg += "This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)\n"; // pgp-encrypted part msg += "--" + boundary + "\n"; msg += "Content-Type: application/pgp-encrypted\n"; msg += "Content-Disposition: attachment\n"; msg += "Content-Transfer-Encoding: 7Bit\n\n"; msg += "Version: 1\n\n"; // the octet stream msg += "--" + boundary + "\n"; msg += 'Content-Type: application/octet-stream; name="encrypted.asc"\n'; msg += "Content-Description: OpenPGP encrypted message\n"; msg += 'Content-Disposition: inline; filename="encrypted.asc"\n'; msg += "Content-Transfer-Encoding: 7Bit\n\n"; msg += encmsg; // Bottom boundary msg += "\n--" + boundary + "--\n"; // Fix up the line endings to be a proper dosish mail msg = msg.replace(/\r/gi, "").replace(/\n/gi, "\r\n"); return msg; }, /** * Walk through the MIME message structure and decrypt the body if there is something to decrypt */ async decryptMimeTree(mimePart) { lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: decryptMimeTree:\n"); if (this.isBrokenByExchange(mimePart)) { this.fixExchangeMessage(mimePart); } if (this.isSMIME(mimePart)) { this.decryptSMIME(mimePart); } else if (this.isPgpMime(mimePart)) { this.decryptPGPMIME(mimePart); } else if (isAttachment(mimePart)) { this.pgpDecryptAttachment(mimePart); } else { this.decryptINLINE(mimePart); } for (let i in mimePart.subParts) { await this.decryptMimeTree(mimePart.subParts[i]); } }, /*** * * Detect if mime part is PGP/MIME message that got modified by MS-Exchange: * * - multipart/mixed Container with * - application/pgp-encrypted Attachment with name "PGPMIME Version Identification" * - application/octet-stream Attachment with name "encrypted.asc" having the encrypted content in base64 * - see: * - https://doesnotexist-openpgp-integration.thunderbird/forum/viewtopic.php?f=4&t=425 * - https://sourceforge.net/p/enigmail/forum/support/thread/4add2b69/ */ isBrokenByExchange(mime) { lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: isBrokenByExchange:\n"); try { if ( mime.subParts && mime.subParts.length === 3 && mime.fullContentType.toLowerCase().includes("multipart/mixed") && mime.subParts[0].subParts.length === 0 && mime.subParts[0].fullContentType.search(/multipart\/encrypted/i) < 0 && mime.subParts[0].fullContentType.toLowerCase().includes("text/plain") && mime.subParts[1].fullContentType .toLowerCase() .includes("application/pgp-encrypted") && mime.subParts[1].fullContentType .toLowerCase() .search(/multipart\/encrypted/i) < 0 && mime.subParts[1].fullContentType .toLowerCase() .search(/PGPMIME Versions? Identification/i) >= 0 && mime.subParts[2].fullContentType .toLowerCase() .includes("application/octet-stream") && mime.subParts[2].fullContentType.toLowerCase().includes("encrypted.asc") ) { lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: isBrokenByExchange: found message broken by MS-Exchange\n" ); return true; } } catch (ex) {} return false; }, decryptSMIME(mimePart) { let encrypted = lazy.MailCryptoUtils.binaryStringToTypedArray( mimePart.body ); let cmsDecoderJS = Cc["@mozilla.org/nsCMSDecoderJS;1"].createInstance( Ci.nsICMSDecoderJS ); let decrypted = cmsDecoderJS.decrypt(encrypted); if (decrypted.length === 0) { // fail if no data found this.decryptFailure = true; return; } let data = ""; for (let c of decrypted) { data += String.fromCharCode(c); } if (lazy.EnigmailLog.getLogLevel() > 5) { lazy.EnigmailLog.DEBUG( "*** start data ***\n'" + data + "'\n***end data***\n" ); } // Search for the separator between headers and message body. let bodyIndex = data.search(/\n\s*\r?\n/); if (bodyIndex < 0) { // not found, body starts at beginning. bodyIndex = 0; } else { // found, body starts after the headers. let wsSize = data.match(/\n\s*\r?\n/); bodyIndex += wsSize[0].length; } if (data.substr(bodyIndex).search(/\r?\n$/) === 0) { return; } let m = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance( Ci.nsIMimeHeaders ); // headers are found from the beginning up to the start of the body m.initialize(data.substr(0, bodyIndex)); mimePart.headers._rawHeaders.set("content-type", [ m.extractHeader("content-type", false) || "", ]); mimePart.headers._rawHeaders.delete("content-transfer-encoding"); mimePart.headers._rawHeaders.delete("content-disposition"); mimePart.headers._rawHeaders.delete("content-description"); mimePart.subParts = []; mimePart.body = data.substr(bodyIndex); this.cryptoChanged = true; }, isSMIME(mimePart) { if (!mimePart.headers.has("content-type")) { return false; } return ( mimePart.headers.get("content-type").type.toLowerCase() === "application/pkcs7-mime" && mimePart.headers.get("content-type").get("smime-type").toLowerCase() === "enveloped-data" && mimePart.subParts.length === 0 ); }, isPgpMime(mimePart) { lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: isPgpMime()\n"); try { if (mimePart.headers.has("content-type")) { if ( mimePart.headers.get("content-type").type.toLowerCase() === "multipart/encrypted" && mimePart.headers.get("content-type").get("protocol").toLowerCase() === "application/pgp-encrypted" && mimePart.subParts.length === 2 ) { return true; } } } catch (x) {} return false; }, async decryptPGPMIME(mimePart) { lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: decryptPGPMIME(" + mimePart.partNum + ")\n" ); if (!mimePart.subParts[1]) { throw new Error("Not a correct PGP/MIME message"); } const uiFlags = lazy.EnigmailConstants.UI_INTERACTIVE | lazy.EnigmailConstants.UI_UNVERIFIED_ENC_OK | lazy.EnigmailConstants.UI_IGNORE_MDC_ERROR; let exitCodeObj = {}; let statusFlagsObj = {}; let userIdObj = {}; let sigDetailsObj = {}; let errorMsgObj = {}; let keyIdObj = {}; let blockSeparationObj = { value: "", }; let encToDetailsObj = {}; var signatureObj = {}; signatureObj.value = ""; let data = lazy.EnigmailDecryption.decryptMessage( null, uiFlags, mimePart.subParts[1].body, null, // date signatureObj, exitCodeObj, statusFlagsObj, keyIdObj, userIdObj, sigDetailsObj, errorMsgObj, blockSeparationObj, encToDetailsObj ); if (!data || data.length === 0) { if (statusFlagsObj.value & lazy.EnigmailConstants.DISPLAY_MESSAGE) { lazy.EnigmailDialog.alert(null, errorMsgObj.value); throw new Error("Decryption impossible"); } } lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: analyzeDecryptedData: got " + data.length + " bytes\n" ); if (lazy.EnigmailLog.getLogLevel() > 5) { lazy.EnigmailLog.DEBUG( "*** start data ***\n'" + data + "'\n***end data***\n" ); } if (data.length === 0) { // fail if no data found this.decryptFailure = true; return; } let bodyIndex = data.search(/\n\s*\r?\n/); if (bodyIndex < 0) { bodyIndex = 0; } else { let wsSize = data.match(/\n\s*\r?\n/); bodyIndex += wsSize[0].length; } if (data.substr(bodyIndex).search(/\r?\n$/) === 0) { return; } let m = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance( Ci.nsIMimeHeaders ); m.initialize(data.substr(0, bodyIndex)); let ct = m.extractHeader("content-type", false) || ""; let part = mimePart.partNum; if (part.length > 0 && part.search(/[^01.]/) < 0) { if (ct.search(/protected-headers/i) >= 0) { if (m.hasHeader("subject")) { let subject = m.extractHeader("subject", false) || ""; subject = subject.replace(/^(Re: )+/, "Re: "); this.mimeTree.headers._rawHeaders.set("subject", [subject]); } } else if (this.mimeTree.headers.get("subject") === "p≡p") { let subject = getPepSubject(data); if (subject) { subject = subject.replace(/^(Re: )+/, "Re: "); this.mimeTree.headers._rawHeaders.set("subject", [subject]); } } else if ( !(statusFlagsObj.value & lazy.EnigmailConstants.GOOD_SIGNATURE) && /^multipart\/signed/i.test(ct) ) { // RFC 3156, Section 6.1 message let innerMsg = lazy.EnigmailMime.getMimeTree(data, false); if (innerMsg.subParts.length > 0) { ct = innerMsg.subParts[0].fullContentType; let hdrMap = innerMsg.subParts[0].headers._rawHeaders; if (ct.search(/protected-headers/i) >= 0 && hdrMap.has("subject")) { let subject = innerMsg.subParts[0].headers._rawHeaders .get("subject") .join(""); subject = subject.replace(/^(Re: )+/, "Re: "); this.mimeTree.headers._rawHeaders.set("subject", [subject]); } } } } let boundary = getBoundary(mimePart); if (!boundary) { boundary = lazy.EnigmailMime.createBoundary(); } // append relevant headers mimePart.headers.get("content-type").type = "multipart/mixed"; mimePart.headers._rawHeaders.set("content-type", [ 'multipart/mixed; boundary="' + boundary + '"', ]); mimePart.subParts = [ { body: data, decryptedPgpMime: true, partNum: mimePart.partNum + ".1", headers: { _rawHeaders: new Map(), get() { return null; }, has() { return false; }, }, subParts: [], }, ]; this.cryptoChanged = true; }, pgpDecryptAttachment(mimePart) { lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: pgpDecryptAttachment()\n"); let attachmentHead = mimePart.body.substr(0, 30); if (attachmentHead.search(/-----BEGIN PGP \w{5,10} KEY BLOCK-----/) >= 0) { // attachment appears to be a PGP key file, skip return; } const uiFlags = lazy.EnigmailConstants.UI_INTERACTIVE | lazy.EnigmailConstants.UI_UNVERIFIED_ENC_OK | lazy.EnigmailConstants.UI_IGNORE_MDC_ERROR; let exitCodeObj = {}; let statusFlagsObj = {}; let userIdObj = {}; let sigDetailsObj = {}; let errorMsgObj = {}; let keyIdObj = {}; let blockSeparationObj = { value: "", }; let encToDetailsObj = {}; var signatureObj = {}; signatureObj.value = ""; let attachmentName = getAttachmentName(mimePart); attachmentName = attachmentName ? attachmentName.replace(/\.(pgp|asc|gpg)$/, "") : ""; let data = lazy.EnigmailDecryption.decryptMessage( null, uiFlags, mimePart.body, null, // date signatureObj, exitCodeObj, statusFlagsObj, keyIdObj, userIdObj, sigDetailsObj, errorMsgObj, blockSeparationObj, encToDetailsObj ); if (data || statusFlagsObj.value & lazy.EnigmailConstants.DECRYPTION_OKAY) { lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: pgpDecryptAttachment: decryption OK\n" ); } else if ( statusFlagsObj.value & (lazy.EnigmailConstants.DECRYPTION_FAILED | lazy.EnigmailConstants.MISSING_MDC) ) { lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: pgpDecryptAttachment: decryption without MDC protection\n" ); } else if ( statusFlagsObj.value & lazy.EnigmailConstants.DECRYPTION_FAILED ) { lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: pgpDecryptAttachment: decryption failed\n" ); // Enigmail prompts the user here, but we just keep going. } else if ( statusFlagsObj.value & lazy.EnigmailConstants.DECRYPTION_INCOMPLETE ) { // failure; message not complete lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: pgpDecryptAttachment: decryption incomplete\n" ); return; } else { // there is nothing to be decrypted lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: pgpDecryptAttachment: no decryption required\n" ); return; } lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: pgpDecryptAttachment: decrypted to " + data.length + " bytes\n" ); if (statusFlagsObj.encryptedFileName) { attachmentName = statusFlagsObj.encryptedFileName; } this.decryptedMessage = true; mimePart.body = data; mimePart.headers._rawHeaders.set( "content-disposition", `attachment; filename="${attachmentName}"` ); mimePart.headers._rawHeaders.set("content-transfer-encoding", ["base64"]); let origCt = mimePart.headers.get("content-type"); let ct = origCt.type; for (let i of origCt.entries()) { if (i[0].toLowerCase() === "name") { i[1] = i[1].replace(/\.(pgp|asc|gpg)$/, ""); } ct += `; ${i[0]}="${i[1]}"`; } mimePart.headers._rawHeaders.set("content-type", [ct]); }, async decryptINLINE(mimePart) { lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: decryptINLINE()\n"); if ("decryptedPgpMime" in mimePart && mimePart.decryptedPgpMime) { return 0; } if ("body" in mimePart && mimePart.body.length > 0) { let ct = getContentType(mimePart); if (ct === "text/html") { mimePart.body = this.stripHTMLFromArmoredBlocks(mimePart.body); } var exitCodeObj = {}; var statusFlagsObj = {}; var userIdObj = {}; var sigDetailsObj = {}; var errorMsgObj = {}; var keyIdObj = {}; var blockSeparationObj = { value: "", }; var encToDetailsObj = {}; var signatureObj = {}; signatureObj.value = ""; const uiFlags = lazy.EnigmailConstants.UI_INTERACTIVE | lazy.EnigmailConstants.UI_UNVERIFIED_ENC_OK | lazy.EnigmailConstants.UI_IGNORE_MDC_ERROR; var plaintexts = []; var blocks = lazy.EnigmailArmor.locateArmoredBlocks(mimePart.body); var tmp = []; for (let i = 0; i < blocks.length; i++) { if (blocks[i].blocktype == "MESSAGE") { tmp.push(blocks[i]); } } blocks = tmp; if (blocks.length < 1) { return 0; } let charset = "utf-8"; for (let i = 0; i < blocks.length; i++) { let plaintext = null; do { let ciphertext = mimePart.body.substring( blocks[i].begin, blocks[i].end + 1 ); if (ciphertext.length === 0) { break; } let hdr = ciphertext.search(/(\r\r|\n\n|\r\n\r\n)/); if (hdr > 0) { let chset = ciphertext.substr(0, hdr).match(/^(charset:)(.*)$/im); if (chset && chset.length == 3) { charset = chset[2].trim(); } } plaintext = lazy.EnigmailDecryption.decryptMessage( null, uiFlags, ciphertext, null, // date signatureObj, exitCodeObj, statusFlagsObj, keyIdObj, userIdObj, sigDetailsObj, errorMsgObj, blockSeparationObj, encToDetailsObj ); if (!plaintext || plaintext.length === 0) { if (statusFlagsObj.value & lazy.EnigmailConstants.DISPLAY_MESSAGE) { lazy.EnigmailDialog.alert(null, errorMsgObj.value); this.cryptoChanged = false; this.decryptFailure = true; return -1; } if ( statusFlagsObj.value & (lazy.EnigmailConstants.DECRYPTION_FAILED | lazy.EnigmailConstants.MISSING_MDC) ) { lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: decryptINLINE: no MDC protection, decrypting anyway\n" ); } if ( statusFlagsObj.value & lazy.EnigmailConstants.DECRYPTION_FAILED ) { // since we cannot find out if the user wants to cancel // we should ask let msg = await lazy.l10n.formatValue( "converter-decrypt-body-failed", { subject: this.subject, } ); if ( !lazy.EnigmailDialog.confirmDlg( null, msg, lazy.l10n.formatValueSync("dlg-button-retry"), lazy.l10n.formatValueSync("dlg-button-skip") ) ) { this.cryptoChanged = false; this.decryptFailure = true; return -1; } } else if ( statusFlagsObj.value & lazy.EnigmailConstants.DECRYPTION_INCOMPLETE ) { this.cryptoChanged = false; this.decryptFailure = true; return -1; } else { plaintext = " "; } } if (ct === "text/html") { plaintext = plaintext.replace(/\n/gi, "
\n"); } let subject = ""; if (this.mimeTree.headers.has("subject")) { subject = this.mimeTree.headers.get("subject"); } if ( i == 0 && subject === "pEp" && mimePart.partNum.length > 0 && mimePart.partNum.search(/[^01.]/) < 0 ) { let m = lazy.EnigmailMime.extractSubjectFromBody(plaintext); if (m) { plaintext = m.messageBody; this.mimeTree.headers._rawHeaders.set("subject", [m.subject]); } } if (plaintext) { plaintexts.push(plaintext); } } while (!plaintext || plaintext === ""); } var decryptedMessage = mimePart.body.substring(0, blocks[0].begin) + plaintexts[0]; for (let i = 1; i < blocks.length; i++) { decryptedMessage += mimePart.body.substring(blocks[i - 1].end + 1, blocks[i].begin + 1) + plaintexts[i]; } decryptedMessage += mimePart.body.substring( blocks[blocks.length - 1].end + 1 ); // enable base64 encoding if non-ASCII character(s) found let j = decryptedMessage.search(/[^\x01-\x7F]/); // eslint-disable-line no-control-regex if (j >= 0) { mimePart.headers._rawHeaders.set("content-transfer-encoding", [ "base64", ]); } else { mimePart.headers._rawHeaders.set("content-transfer-encoding", ["8bit"]); } mimePart.body = decryptedMessage; let origCharset = getCharset(mimePart, "content-type"); if (origCharset) { mimePart.headers_rawHeaders.set( "content-type", getHeaderValue(mimePart, "content-type").replace(origCharset, charset) ); } else { mimePart.headers._rawHeaders.set( "content-type", getHeaderValue(mimePart, "content-type") + "; charset=" + charset ); } this.cryptoChanged = true; return 1; } let ct = getContentType(mimePart); lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: Decryption skipped: " + ct + "\n" ); return 0; }, stripHTMLFromArmoredBlocks(text) { var index = 0; var begin = text.indexOf("-----BEGIN PGP"); var end = text.indexOf("-----END PGP"); while (begin > -1 && end > -1) { let sub = text.substring(begin, end); sub = sub.replace(/(<([^>]+)>)/gi, ""); sub = sub.replace(/&[A-z]+;/gi, ""); text = text.substring(0, begin) + sub + text.substring(end); index = end + 10; begin = text.indexOf("-----BEGIN PGP", index); end = text.indexOf("-----END PGP", index); } return text; }, /****** * * We have the technology we can rebuild. * * Function to reassemble the message from the MIME Tree * into a String. * ******/ mimeToString(mimePart, includeHeaders) { lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: mimeToString: part: '" + mimePart.partNum + "'\n" ); let msg = ""; let rawHdr = mimePart.headers._rawHeaders; if (includeHeaders && rawHdr.size > 0) { for (let hdr of rawHdr.keys()) { let formatted = formatMimeHeader(hdr, rawHdr.get(hdr)); msg += formatted; if (!formatted.endsWith("\r\n")) { msg += "\r\n"; } } msg += "\r\n"; } if (mimePart.body.length > 0) { let encoding = getTransferEncoding(mimePart); if (!encoding) { encoding = "8bit"; } if (encoding === "base64") { msg += lazy.EnigmailData.encodeBase64(mimePart.body); } else { let charset = getCharset(mimePart, "content-type"); if (charset) { msg += lazy.EnigmailData.convertFromUnicode(mimePart.body, charset); } else { msg += mimePart.body; } } } if (mimePart.subParts.length > 0) { let boundary = lazy.EnigmailMime.getBoundary( rawHdr.get("content-type").join("") ); for (let i in mimePart.subParts) { msg += `--${boundary}\r\n`; msg += this.mimeToString(mimePart.subParts[i], true); if (msg.search(/[\r\n]$/) < 0) { msg += "\r\n"; } msg += "\r\n"; } msg += `--${boundary}--\r\n`; } return msg; }, fixExchangeMessage(mimePart) { lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: fixExchangeMessage()\n"); let msg = this.mimeToString(mimePart, true); try { let fixedMsg = lazy.EnigmailFixExchangeMsg.getRepairedMessage(msg); let replacement = lazy.EnigmailMime.getMimeTree(fixedMsg, true); for (let i in replacement) { mimePart[i] = replacement[i]; } } catch (ex) {} }, }; /** * Format a mime header * * e.g. content-type -> Content-Type */ function formatHeader(headerLabel) { return headerLabel.replace(/^.|(-.)/g, function (match) { return match.toUpperCase(); }); } function formatMimeHeader(headerLabel, headerValue) { if (Array.isArray(headerValue)) { return headerValue .map(v => formatHeader(headerLabel) + ": " + v) .join("\r\n"); } return formatHeader(headerLabel) + ": " + headerValue + "\r\n"; } function prettyPrintHeader(headerLabel, headerData) { if (Array.isArray(headerData)) { let h = []; for (let i in headerData) { h.push( formatMimeHeader(headerLabel, lazy.GlodaUtils.deMime(headerData[i])) ); } return h.join("\r\n"); } return formatMimeHeader( headerLabel, lazy.GlodaUtils.deMime(String(headerData)) ); } function getHeaderValue(mimeStruct, header) { lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: getHeaderValue: '" + header + "'\n" ); try { if (mimeStruct.headers.has(header)) { let hdrVal = mimeStruct.headers.get(header); if (typeof hdrVal == "string") { return hdrVal; } return mimeStruct.headers[header].join(" "); } return ""; } catch (ex) { lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: getHeaderValue: header not present\n" ); return ""; } } function getContentType(mime) { try { if (mime && "headers" in mime && mime.headers.has("content-type")) { return mime.headers.get("content-type").type.toLowerCase(); } } catch (e) { lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: getContentType: " + e + "\n"); } return null; } // return the content of the boundary parameter function getBoundary(mime) { try { if (mime && "headers" in mime && mime.headers.has("content-type")) { return mime.headers.get("content-type").get("boundary"); } } catch (e) { lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: getBoundary: " + e + "\n"); } return null; } function getCharset(mime) { try { if (mime && "headers" in mime && mime.headers.has("content-type")) { let c = mime.headers.get("content-type").get("charset"); if (c) { return c.toLowerCase(); } } } catch (e) { lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: getCharset: " + e + "\n"); } return null; } function getTransferEncoding(mime) { try { if ( mime && "headers" in mime && mime.headers._rawHeaders.has("content-transfer-encoding") ) { let c = mime.headers._rawHeaders.get("content-transfer-encoding")[0]; if (c) { return c.toLowerCase(); } } } catch (e) { lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: getTransferEncoding: " + e + "\n" ); } return "8Bit"; } function isAttachment(mime) { try { if (mime && "headers" in mime) { if (mime.fullContentType.search(/^multipart\//i) === 0) { return false; } if (mime.fullContentType.search(/^text\//i) < 0) { return true; } if (mime.headers.has("content-disposition")) { let c = mime.headers.get("content-disposition")[0]; if (c) { if (c.search(/^attachment/i) === 0) { return true; } } } } } catch (x) {} return false; } /** * If the given MIME part is an attachment, return its filename. * * @param mime: a MIME part * @return: the filename or null */ function getAttachmentName(mime) { if ("headers" in mime && mime.headers.has("content-disposition")) { let c = mime.headers.get("content-disposition")[0]; if (/^attachment/i.test(c)) { return lazy.EnigmailMime.getParameter(c, "filename"); } } return null; } function getPepSubject(mimeString) { lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: getPepSubject()\n"); let subject = null; let emitter = { ct: "", firstPlainText: false, startPart(partNum, headers) { lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: getPepSubject.startPart: partNum=" + partNum + "\n" ); try { this.ct = String(headers.getRawHeader("content-type")).toLowerCase(); if (!subject && !this.firstPlainText) { let s = headers.getRawHeader("subject"); if (s) { subject = String(s); this.firstPlainText = true; } } } catch (ex) { this.ct = ""; } }, endPart(partNum) {}, deliverPartData(partNum, data) { lazy.EnigmailLog.DEBUG( "persistentCrypto.jsm: getPepSubject.deliverPartData: partNum=" + partNum + " ct=" + this.ct + "\n" ); if (!this.firstPlainText && this.ct.search(/^text\/plain/) === 0) { // check data this.firstPlainText = true; let o = lazy.EnigmailMime.extractSubjectFromBody(data); if (o) { subject = o.subject; } } }, }; let opt = { strformat: "unicode", bodyformat: "decode", }; try { let p = new lazy.jsmime.MimeParser(emitter, opt); p.deliverData(mimeString); } catch (ex) {} return subject; }