Bug 1693332 - Implement permanent message decryption and unencrypted copying to a folder. r=mkmelin
authorKai Engert <kaie@kuix.de>
Mon, 29 Nov 2021 20:39:34 +0200
changeset 34415 4dbfe690171b5f723c447339830bef95bd8da417
parent 34414 d2444a4140e7cbc54fdcd4988f35f77112b8e2d0
child 34416 ef3c9fd0bc35c437ad9fa3414892d1f002444987
push id19418
push usermkmelin@iki.fi
push dateMon, 29 Nov 2021 18:43:44 +0000
treeherdercomm-central@c3d468fc8947 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs1693332
Bug 1693332 - Implement permanent message decryption and unencrypted copying to a folder. r=mkmelin Loosely based on https://github.com/Betterbird/thunderbird-patches/blob/main/91/bugs/1693332-decrypt-to-folder.patch Differential Revision: https://phabricator.services.mozilla.com/D127184
mail/base/content/mainPopupSet.inc.xhtml
mail/base/content/nsContextMenu.js
mail/base/test/browser/browser_mailContext.js
mail/extensions/openpgp/content/modules/data.jsm
mail/extensions/openpgp/content/modules/filters.jsm
mail/extensions/openpgp/content/modules/fixExchangeMsg.jsm
mail/extensions/openpgp/content/modules/mime.jsm
mail/extensions/openpgp/content/modules/persistentCrypto.jsm
mail/extensions/openpgp/content/ui/enigmailMessengerOverlay.js
mail/locales/en-US/messenger/messenger.ftl
mailnews/build/nsMailModule.cpp
mailnews/extensions/smime/moz.build
mailnews/extensions/smime/nsCMS.cpp
mailnews/extensions/smime/nsCMS.h
mailnews/extensions/smime/nsICMSDecoder.idl
mailnews/extensions/smime/nsICMSDecoderJS.idl
mailnews/extensions/smime/nsMsgSMIMECID.h
--- a/mail/base/content/mainPopupSet.inc.xhtml
+++ b/mail/base/content/mainPopupSet.inc.xhtml
@@ -330,16 +330,36 @@
                  favoritesLabel="&contextMoveCopyMsgFavoritesMenu.label;"
                  favoritesAccessKey="&contextMoveCopyMsgFavoritesMenu.accesskey;"/>
     </menu>
     <menuitem id="mailContext-moveToFolderAgain"
               command="cmd_moveToFolderAgain"
               label="&moveToFolderAgain.label;"
               accesskey="&moveToFolderAgain.accesskey;"/>
 
+#ifdef MOZ_OPENPGP
+    <menu id="mailContext-decryptToFolder"
+          class="openpgp-item"
+          data-l10n-id="context-menu-decrypt-to-folder"
+          data-l10n-attrs="accesskey"
+          oncommand="Enigmail.msg.decryptToFolder(event.target._folder, false);">
+      <menupopup is="folder-menupopup"
+                 id="mailContext-decryptToTargetFolder"
+                 mode="filing"
+                 showFileHereLabel="true"
+                 showRecent="true"
+                 recentLabel="&contextMoveCopyMsgRecentMenu.label;"
+                 recentAccessKey="&contextMoveCopyMsgRecentMenu.accesskey;"
+                 showFavorites="true"
+                 favoritesLabel="&contextMoveCopyMsgFavoritesMenu.label;"
+                 favoritesAccessKey="&contextMoveCopyMsgFavoritesMenu.accesskey;"
+                 hasbeenopened="false" />
+    </menu>
+#endif
+
     <menu id="mailContext-calendar-convert-menu"
           class="hide-when-calendar-deactivated"
           label="&calendar.context.convertmenu.label;"
           accesskey="&calendar.context.convertmenu.accesskey.mail;">
       <menupopup id="mailContext-calendar-convert-menupopup">
         <menuitem id="mailContext-calendar-convert-event-menuitem"
                   label="&calendar.context.convertmenu.event.label;"
                   accesskey="&calendar.context.convertmenu.event.accesskey;"
--- a/mail/base/content/nsContextMenu.js
+++ b/mail/base/content/nsContextMenu.js
@@ -3,16 +3,17 @@
  * 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/. */
 
 /* import-globals-from folderDisplay.js */
 /* import-globals-from mailTabs.js */
 /* import-globals-from mailWindow.js */
 /* import-globals-from messageDisplay.js */
 /* import-globals-from utilityOverlay.js */
+/* global EnigmailURIs: false, gEncryptedURIService: true */
 
 var { InlineSpellChecker, SpellCheckHelper } = ChromeUtils.import(
   "resource://gre/modules/InlineSpellChecker.jsm"
 );
 var { PlacesUtils } = ChromeUtils.import(
   "resource://gre/modules/PlacesUtils.jsm"
 );
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
@@ -545,16 +546,17 @@ class nsContextMenu {
         "mailContext-editAsNew",
         "mailContext-editDraftMsg",
         "mailContext-newMsgFromTemplate",
         "mailContext-editTemplateMsg",
         "mailContext-copyMessageUrl",
         "mailContext-moveMenu",
         "mailContext-copyMenu",
         "mailContext-moveToFolderAgain",
+        "mailContext-decryptToFolder",
         "mailContext-ignoreThread",
         "mailContext-ignoreSubthread",
         "mailContext-watchThread",
         "mailContext-tags",
         "mailContext-mark",
         "mailContext-saveAs",
         "mailContext-print",
         "mailContext-delete",
@@ -647,16 +649,25 @@ class nsContextMenu {
     this.showItem("mailContext-moveToFolderAgain", msgModifyItems);
     if (msgModifyItems) {
       initMoveToFolderAgainMenu(
         document.getElementById("mailContext-moveToFolderAgain")
       );
       goUpdateCommand("cmd_moveToFolderAgain");
     }
 
+    let showDecrypt = this.numSelectedMessages > 1;
+    if (this.numSelectedMessages == 1) {
+      let msgURI = gFolderDisplay.selectedMessageUris[0];
+      showDecrypt =
+        EnigmailURIs.isEncryptedUri(msgURI) ||
+        gEncryptedURIService.isEncrypted(msgURI);
+    }
+    this.showItem("mailContext-decryptToFolder", showDecrypt);
+
     this.showItem("mailContext-tags", msgModifyItems);
 
     this.showItem("mailContext-mark", msgModifyItems);
 
     this.showItem(
       "mailContext-ignoreThread",
       !this.inStandaloneWindow &&
         this.numSelectedMessages >= 1 &&
--- a/mail/base/test/browser/browser_mailContext.js
+++ b/mail/base/test/browser/browser_mailContext.js
@@ -193,17 +193,17 @@ add_task(async function testMessagePane(
   EventUtils.synthesizeMouse(
     treeChildren,
     coords.x + coords.width / 2,
     coords.y + coords.height / 2,
     { type: "contextmenu" }
   );
   checkMenuitems(mailContext);
 
-  // One message is selected.
+  // One message is selected (the message is not encrypted)
 
   window.gFolderDisplay.selectViewIndex(0);
   await BrowserTestUtils.browserLoaded(messagePane);
   let shownPromise = BrowserTestUtils.waitForEvent(mailContext, "popupshown");
   await BrowserTestUtils.synthesizeMouseAtCenter(
     ":root",
     { type: "contextmenu" },
     messagePane
@@ -286,16 +286,17 @@ add_task(async function testMessagePane(
     mailContext,
     "mailContext-multiForwardAsAttachment",
     "mailContext-tags",
     "mailContext-mark",
     "mailContext-archive",
     "mailContext-moveMenu",
     "mailContext-copyMenu",
     "mailContext-moveToFolderAgain",
+    "mailContext-decryptToFolder",
     "mailContext-delete",
     "mailContext-ignoreThread",
     "mailContext-ignoreSubthread",
     "mailContext-watchThread",
     "mailContext-saveAs",
     "mailContext-print",
     "downloadSelected"
   );
--- a/mail/extensions/openpgp/content/modules/data.jsm
+++ b/mail/extensions/openpgp/content/modules/data.jsm
@@ -56,33 +56,28 @@ var EnigmailData = {
   },
 
   convertToUnicode(text, charset) {
     if (!text || (charset && charset.toLowerCase() == "iso-8859-1")) {
       return text;
     }
 
     // Encode plaintext
-    try {
-      return converter(charset).ConvertToUnicode(text);
-    } catch (ex) {
-      return text;
-    }
+    return converter(charset).ConvertToUnicode(text);
   },
 
   convertFromUnicode(text, charset) {
     if (!text) {
       return "";
     }
 
-    try {
-      return converter(charset).ConvertFromUnicode(text);
-    } catch (ex) {
-      return text;
-    }
+    let conv = converter(charset);
+    let result = conv.ConvertFromUnicode(text);
+    result += conv.Finish();
+    return result;
   },
 
   convertGpgToUnicode(text) {
     if (typeof text === "string") {
       text = text.replace(/\\x3a/gi, "\\e3A");
       var a = text.search(/\\x[0-9a-fA-F]{2}/);
       while (a >= 0) {
         var ch = unescape("%" + text.substr(a + 2, 2));
--- a/mail/extensions/openpgp/content/modules/filters.jsm
+++ b/mail/extensions/openpgp/content/modules/filters.jsm
@@ -38,27 +38,29 @@ XPCOMUtils.defineLazyGetter(this, "l10n"
 var gNewMailListenerInitiated = false;
 
 /**
  * filter action for creating a decrypted version of the mail and
  * deleting the original mail at the same time
  */
 
 const filterActionMoveDecrypt = {
-  applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) {
+  async applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) {
     EnigmailLog.DEBUG(
       "filters.jsm: filterActionMoveDecrypt: Move to: " + aActionValue + "\n"
     );
 
-    EnigmailPersistentCrypto.dispatchMessages(
-      aMsgHdrs,
-      aActionValue,
-      aListener,
-      true
-    );
+    for (let msgHdr of aMsgHdrs) {
+      await EnigmailPersistentCrypto.cryptMessage(
+        msgHdr,
+        aActionValue,
+        true,
+        null
+      );
+    }
   },
 
   isValidForType(type, scope) {
     return true;
   },
 
   validateActionValue(value, folder, type) {
     l10n.formatValue("filter-decrypt-move-warn-experimental").then(value => {
@@ -73,27 +75,29 @@ const filterActionMoveDecrypt = {
   },
 };
 
 /**
  * filter action for creating a decrypted copy of the mail, leaving the original
  * message untouched
  */
 const filterActionCopyDecrypt = {
-  applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) {
+  async applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) {
     EnigmailLog.DEBUG(
       "filters.jsm: filterActionCopyDecrypt: Copy to: " + aActionValue + "\n"
     );
 
-    EnigmailPersistentCrypto.dispatchMessages(
-      aMsgHdrs,
-      aActionValue,
-      aListener,
-      false
-    );
+    for (let msgHdr of aMsgHdrs) {
+      await EnigmailPersistentCrypto.cryptMessage(
+        msgHdr,
+        aActionValue,
+        false,
+        null
+      );
+    }
   },
 
   isValidForType(type, scope) {
     EnigmailLog.DEBUG(
       "filters.jsm: filterActionCopyDecrypt.isValidForType(" + type + ")\n"
     );
 
     let r = true;
@@ -114,17 +118,17 @@ const filterActionCopyDecrypt = {
     return null;
   },
 };
 
 /**
  * filter action for to encrypt a mail to a specific key
  */
 const filterActionEncrypt = {
-  applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) {
+  async applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) {
     // Ensure KeyRing is loaded.
     if (aMsgWindow) {
       EnigmailCore.getService(aMsgWindow.domWindow);
     } else {
       EnigmailCore.getService();
     }
     EnigmailKeyRing.getAllKeys();
 
@@ -162,21 +166,20 @@ const filterActionEncrypt = {
 
     // Maybe skip messages here if they are already encrypted to
     // the target key? There might be some use case for unconditionally
     // encrypting here. E.g. to use the local preferences and remove all
     // other recipients.
     // Also not encrypting to already encrypted messages would make the
     // behavior less transparent as it's not obvious.
 
-    if (aMsgHdrs.length) {
-      EnigmailPersistentCrypto.dispatchMessages(
-        aMsgHdrs,
+    for (let msgHdr of aMsgHdrs) {
+      await EnigmailPersistentCrypto.cryptMessage(
+        msgHdr,
         null /* same folder */,
-        aListener,
         true /* move */,
         keyObj /* target key */
       );
     }
   },
 
   isValidForType(type, scope) {
     return true;
--- a/mail/extensions/openpgp/content/modules/fixExchangeMsg.jsm
+++ b/mail/extensions/openpgp/content/modules/fixExchangeMsg.jsm
@@ -12,77 +12,59 @@ const { XPCOMUtils } = ChromeUtils.impor
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   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",
-  Services: "resource://gre/modules/Services.jsm",
-  MailServices: "resource:///modules/MailServices.jsm",
-  MailUtils: "resource:///modules/MailUtils.jsm",
+  EnigmailPersistentCrypto:
+    "chrome://openpgp/content/modules/persistentCrypto.jsm",
 });
 
-/*
- *  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 Promise; upon success, the promise returns the messageKey
- */
 var EnigmailFixExchangeMsg = {
-  fixExchangeMessage(hdr, brokenByApp, destFolderUri) {
-    var self = this;
-    return new Promise(function(resolve, reject) {
-      let msgUriSpec = hdr.folder.getUriForMsg(hdr);
-      EnigmailLog.DEBUG(
-        "fixExchangeMsg.jsm: fixExchangeMessage: msgUriSpec: " +
-          msgUriSpec +
-          "\n"
-      );
-
-      self.hdr = hdr;
-      self.destFolder = hdr.folder;
-      self.resolve = resolve;
-      self.reject = reject;
-      self.brokenByApp = brokenByApp;
+  /*
+   * 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);
+    EnigmailLog.DEBUG(
+      "fixExchangeMsg.jsm: fixExchangeMessage: msgUriSpec: " + msgUriSpec + "\n"
+    );
 
-      if (destFolderUri) {
-        self.destFolder = MailUtils.getExistingFolder(destFolderUri);
-      }
+    this.hdr = hdr;
+    this.brokenByApp = brokenByApp;
+    this.destFolderUri = destFolderUri;
 
-      let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
-        Ci.nsIMessenger
-      );
-      self.msgSvc = messenger.messageServiceFromURI(msgUriSpec);
+    let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
+      Ci.nsIMessenger
+    );
+    this.msgSvc = messenger.messageServiceFromURI(msgUriSpec);
+
+    let fixedMsgData = await this.getMessageBody();
 
-      let p = self.getMessageBody();
-      p.then(function(fixedMsgData) {
-        EnigmailLog.DEBUG(
-          "fixExchangeMsg.jsm: fixExchangeMessage: got fixedMsgData\n"
-        );
-        if (self.checkMessageStructure(fixedMsgData)) {
-          self.copyToTargetFolder(fixedMsgData);
-        } else {
-          reject();
-        }
-      });
-      p.catch(function(reason) {
-        EnigmailLog.DEBUG(
-          "fixExchangeMsg.jsm: fixExchangeMessage: caught rejection: " +
-            reason +
-            "\n"
-        );
-        reject();
-      });
-    });
+    EnigmailLog.DEBUG(
+      "fixExchangeMsg.jsm: fixExchangeMessage: got fixedMsgData\n"
+    );
+    this.ensureExpectedStructure(fixedMsgData);
+    return EnigmailPersistentCrypto.copyMessageToFolder(
+      this.hdr,
+      this.destFolderUri,
+      true,
+      fixedMsgData,
+      null
+    );
   },
 
   getMessageBody() {
     EnigmailLog.DEBUG("fixExchangeMsg.jsm: getMessageBody:\n");
 
     var self = this;
 
     return new Promise(function(resolve, reject) {
@@ -403,130 +385,38 @@ var EnigmailFixExchangeMsg = {
           "Content-Type: application/octet-stream"
         ) +
       "--" +
       boundary +
       "--\r\n"
     );
   },
 
-  checkMessageStructure(msgData) {
+  ensureExpectedStructure(msgData) {
     let msgTree = EnigmailMime.getMimeTree(msgData, true);
 
-    try {
-      // 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;
-      }
-      return ok;
-    } catch (x) {}
-    return false;
-  },
-
-  copyToTargetFolder(msgData) {
-    var self = this;
-    let tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
-    tempFile.append("message.eml");
-    tempFile.createUnique(0, 0o600);
-
-    // ensure that file gets deleted on exit, if something goes wrong ...
-    var extAppLauncher = Cc["@mozilla.org/mime;1"].getService(
-      Ci.nsPIExternalAppLauncher
-    );
-
-    var foStream = Cc[
-      "@mozilla.org/network/file-output-stream;1"
-    ].createInstance(Ci.nsIFileOutputStream);
-    foStream.init(tempFile, 2, 0x200, false); // open as "write only"
-    foStream.write(msgData, msgData.length);
-    foStream.close();
-
-    extAppLauncher.deleteTemporaryFileOnExit(tempFile);
-
-    // note: nsIMsgFolder.copyFileMessage seems to have a bug on Windows, when
-    // the nsIFile has been already used by foStream (because of Windows lock system?), so we
-    // must initialize another nsIFile object, pointing to the temporary file
-    var fileSpec = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
-    fileSpec.initWithPath(tempFile.path);
+    // 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";
 
-    var copyListener = {
-      QueryInterface: ChromeUtils.generateQI(["nsIMsgCopyServiceListener"]),
-      msgKey: null,
-      GetMessageId(messageId) {},
-      OnProgress(progress, progressMax) {},
-      OnStartCopy() {},
-      SetMessageKey(key) {
-        this.msgKey = key;
-      },
-      OnStopCopy(statusCode) {
-        if (statusCode !== 0) {
-          EnigmailLog.DEBUG(
-            "fixExchangeMsg.jsm: error copying message: " + statusCode + "\n"
-          );
-          try {
-            tempFile.remove(false);
-          } catch (ex) {
-            EnigmailLog.DEBUG(
-              "persistentCrypto.jsm: Could not delete temp file\n"
-            );
-          }
-          self.reject(3);
-          return;
-        }
-        EnigmailLog.DEBUG("fixExchangeMsg.jsm: copy complete\n");
-
-        EnigmailLog.DEBUG(
-          "fixExchangeMsg.jsm: deleting message key=" +
-            self.hdr.messageKey +
-            "\n"
-        );
+    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);
 
-        self.hdr.folder.deleteMessages(
-          [self.hdr],
-          null,
-          true,
-          false,
-          null,
-          false
-        );
-        EnigmailLog.DEBUG("fixExchangeMsg.jsm: deleted original message\n");
-
-        try {
-          tempFile.remove(false);
-        } catch (ex) {
-          EnigmailLog.DEBUG(
-            "persistentCrypto.jsm: Could not delete temp file\n"
-          );
-        }
-        self.resolve(this.msgKey);
-      },
-    };
-
-    MailServices.copy.copyFileMessage(
-      fileSpec,
-      this.destFolder,
-      null,
-      false,
-      0,
-      this.hdr.flags,
-      copyListener,
-      null
-    );
+      ok = p0 >= 0 && p1 > p0 + 32;
+    }
+    if (!ok) {
+      throw new Error("unexpected MIME structure");
+    }
   },
 };
--- a/mail/extensions/openpgp/content/modules/mime.jsm
+++ b/mail/extensions/openpgp/content/modules/mime.jsm
@@ -551,16 +551,17 @@ function getMimeTree(mimeStr, getBody = 
         currentPart.body += EnigmailData.arrayBufferToString(data);
       }
     },
   };
 
   let opt = {
     strformat: "unicode",
     bodyformat: getBody ? "decode" : "none",
+    stripcontinuations: false,
   };
 
   try {
     let p = new jsmime.MimeParser(jsmimeEmitter, opt);
     p.deliverData(mimeStr);
     return mimeTree.subParts[0];
   } catch (ex) {
     return null;
--- a/mail/extensions/openpgp/content/modules/persistentCrypto.jsm
+++ b/mail/extensions/openpgp/content/modules/persistentCrypto.jsm
@@ -8,198 +8,297 @@
 
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 var EXPORTED_SYMBOLS = ["EnigmailPersistentCrypto"];
 
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
+const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   EnigmailArmor: "chrome://openpgp/content/modules/armor.jsm",
   EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
-  EnigmailCore: "chrome://openpgp/content/modules/core.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",
   MailServices: "resource:///modules/MailServices.jsm",
   MailUtils: "resource:///modules/MailUtils.jsm",
-  setTimeout: "resource://gre/modules/Timer.jsm",
+  MailCryptoUtils: "resource:///modules/MailCryptoUtils.jsm",
 });
 
 XPCOMUtils.defineLazyGetter(this, "l10n", () => {
   return new Localization(["messenger/openpgp/openpgp.ftl"], true);
 });
 
-/*
- *  Decrypt a message and copy it to a folder
- *
- * @param nsIMsgDBHdr hdr   Header of the message
- * @param String destFolder   Folder URI
- * @param Boolean move      If true the original message will be deleted
- *
- * @return a Promise that we do that
- */
 var EnigmailPersistentCrypto = {
   /***
-   *  dispatchMessages
+   * cryptMessage
    *
-   *  Because Thunderbird throws all messages at once at us thus we have to rate limit the dispatching
-   *  of the message processing. Because there is only a negligible performance gain when dispatching
-   *  several message at once we serialize to not overwhelm low power devices.
-   *
-   *  If targetFolder is null the message will be copied / moved in the same folder as the original
-   *  message.
+   * Decrypts a message and copy it to a folder. If targetKey is
+   * not null, it encrypts a message to the target key afterwards.
    *
-   *  If targetKey is not null the message will be encrypted again to the targetKey.
-   *
-   *  The function is implemented asynchronously.
+   * @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 encyption is requested
    *
-   *  Parameters
-   *   aMsgHdrs:     Array of nsIMsgDBHdr
-   *   targetFolder: String; target folder URI or null
-   *   copyListener: listener for async request (nsIMsgCopyServiceListener)
-   *   move:         Boolean: type of action; true = "move" / false = "copy"
-   *   targetKey:    KeyObject of target key if encryption is requested
-   *
+   * @returns {nsMsgKey} Message key of the new message
    **/
-
-  dispatchMessages(aMsgHdrs, targetFolder, copyListener, move, targetKey) {
-    EnigmailLog.DEBUG("persistentCrypto.jsm: dispatchMessages()\n");
-
-    let enigmailSvc = EnigmailCore.getService();
-    if (copyListener && !enigmailSvc) {
-      // could not initiate Enigmail - do nothing
-      copyListener.OnStopCopy(0);
-      return;
-    }
-
-    if (copyListener) {
-      copyListener.OnStartCopy();
-    }
-    let promise = EnigmailPersistentCrypto.cryptMessage(
-      aMsgHdrs[0],
-      targetFolder,
-      move,
-      targetKey
-    );
-
-    let processNext = function(data) {
-      aMsgHdrs.splice(0, 1);
-      if (aMsgHdrs.length > 0) {
-        EnigmailPersistentCrypto.dispatchMessages(
-          aMsgHdrs,
-          targetFolder,
-          copyListener,
-          move,
-          targetKey
-        );
-      } else {
-        // last message was finished processing
-        if (copyListener) {
-          copyListener.OnStopCopy(0);
-        }
-        EnigmailLog.DEBUG("persistentCrypto.jsm: dispatchMessages - DONE\n");
-      }
-    };
-
-    promise.then(processNext);
-
-    promise.catch(function(err) {
-      processNext(null);
-    });
-  },
-
-  /***
-   *  cryptMessage
-   *
-   *  Decrypts a message. If targetKey is not null it
-   *  encrypts a message to the target key afterwards.
-   *
-   *  Parameters
-   *   hdr:        nsIMsgDBHdr of the message to encrypt
-   *   destFolder: String; target folder URI
-   *   move:       Boolean: type of action; true = "move" / false = "copy"
-   *   targetKey:  KeyObject of target key if encryption is requested
-   **/
-  cryptMessage(hdr, destFolder, move, targetKey) {
+  async cryptMessage(hdr, destFolder, move, targetKey) {
     return new Promise(function(resolve, reject) {
       let msgUriSpec = hdr.folder.getUriForMsg(hdr);
       let msgUrl = EnigmailFuncs.getUrlFromUriSpec(msgUriSpec);
 
-      const crypt = new CryptMessageIntoFolder(
-        destFolder,
-        move,
-        resolve,
-        targetKey
+      const crypt = new CryptMessageIntoFolder(destFolder, move, targetKey);
+
+      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>"
       );
 
-      try {
-        EnigmailMime.getMimeTreeFromUrl(msgUrl, true, function(mime) {
-          crypt.messageParseCallback(mime, hdr);
-        });
-      } catch (ex) {
-        reject("msgHdrsDeleteoMimeMessage failed: " + ex.toString());
+      // 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
+  ) {
+    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 nsDelAttachListener 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
+        ? 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) {
+          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) {
+            EnigmailLog.ERROR(
+              `persistentCrypto.jsm: ${statusCode} replacing message, folder="${msgFolder.name}", key=${originalMsgHdr.messageKey}/${newKey}\n`
+            );
+            reject();
+            return;
+          }
+
+          try {
+            tempFile.remove();
+          } catch (ex) {}
+
+          EnigmailLog.DEBUG(
+            "persistentCrypto.jsm: copyMessageToFolder: Triggering deletion from OnStopCopy()\n"
+          );
+          this.applyFlags();
+
+          if (deleteOrigMsg) {
+            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, resolve, targetKey) {
+function CryptMessageIntoFolder(destFolder, move, targetKey) {
   this.destFolder = destFolder;
   this.move = move;
-  this.resolve = resolve;
   this.targetKey = targetKey;
-  this.messageDecrypted = false;
+  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) {
-        // do nothing (still better than destroying the message)
-        this.resolve(true);
-        return;
+        throw new Error("Failure to encrypt message");
       }
-      this.messageDecrypted = true;
-    } else if (this.messageDecrypted) {
+      this.cryptoChanged = true;
+    } else {
       msg = this.mimeToString(mimeTree, true);
     }
 
-    if (this.messageDecrypted) {
-      this.resolve(await this.storeMessage(msg));
-    } else {
-      this.resolve(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 = {};
     EnigmailLog.DEBUG("persistentCrypto.jsm: Encrypting message.\n");
 
@@ -278,17 +377,19 @@ CryptMessageIntoFolder.prototype = {
    */
   async decryptMimeTree(mimePart) {
     EnigmailLog.DEBUG("persistentCrypto.jsm: decryptMimeTree:\n");
 
     if (this.isBrokenByExchange(mimePart)) {
       this.fixExchangeMessage(mimePart);
     }
 
-    if (this.isPgpMime(mimePart)) {
+    if (this.isSMIME(mimePart)) {
+      this.decryptSMIME(mimePart);
+    } else if (this.isPgpMime(mimePart)) {
       this.decryptPGPMIME(mimePart);
     } else if (isAttachment(mimePart)) {
       this.decryptAttachment(mimePart);
     } else {
       this.decryptINLINE(mimePart);
     }
 
     for (let i in mimePart.subParts) {
@@ -338,16 +439,90 @@ CryptMessageIntoFolder.prototype = {
         );
         return true;
       }
     } catch (ex) {}
 
     return false;
   },
 
+  decryptSMIME(mimePart) {
+    let encrypted = 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 (EnigmailLog.getLogLevel() > 5) {
+      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) {
     EnigmailLog.DEBUG("persistentCrypto.jsm: isPgpMime()\n");
 
     try {
       if (mimePart.headers.has("content-type")) {
         if (
           mimePart.headers.get("content-type").type.toLowerCase() ===
             "multipart/encrypted" &&
@@ -419,24 +594,26 @@ CryptMessageIntoFolder.prototype = {
     );
 
     if (EnigmailLog.getLogLevel() > 5) {
       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 {
-      ++bodyIndex;
+      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
@@ -453,16 +630,33 @@ CryptMessageIntoFolder.prototype = {
           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 & EnigmailConstants.GOOD_SIGNATURE) &&
+        /^multipart\/signed/i.test(ct)
+      ) {
+        // RFC 3156, Section 6.1 message
+        let innerMsg = 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 = EnigmailMime.createBoundary();
     }
 
@@ -484,55 +678,121 @@ CryptMessageIntoFolder.prototype = {
           has() {
             return false;
           },
         },
         subParts: [],
       },
     ];
 
-    this.messageDecrypted = true;
+    this.cryptoChanged = true;
   },
 
   decryptAttachment(mimePart) {
     EnigmailLog.DEBUG("persistentCrypto.jsm: decryptAttachment()\n");
-    throw new Error("Not implemented");
-
-    /*
     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, we just go-a-head
+      // attachment appears to be a PGP key file, skip
       return;
     }
 
+    const uiFlags =
+      EnigmailConstants.UI_INTERACTIVE |
+      EnigmailConstants.UI_UNVERIFIED_ENC_OK |
+      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)$/, "") : "";
+    attachmentName = attachmentName
+      ? attachmentName.replace(/\.(pgp|asc|gpg)$/, "")
+      : "";
+
+    let data = EnigmailDecryption.decryptMessage(
+      null,
+      uiFlags,
+      mimePart.body,
+      signatureObj,
+      exitCodeObj,
+      statusFlagsObj,
+      keyIdObj,
+      userIdObj,
+      sigDetailsObj,
+      errorMsgObj,
+      blockSeparationObj,
+      encToDetailsObj
+    );
 
-    EnigmailLog.DEBUG("persistentCrypto.jsm: decryptAttachment: decrypted to " + listener.stdoutData.length + " bytes\n");
-    if (statusFlagsObj.encryptedFileName && statusFlagsObj.encryptedFileName.length > 0) {
+    if (data || statusFlagsObj.value & EnigmailConstants.DECRYPTION_OKAY) {
+      EnigmailLog.DEBUG(
+        "persistentCrypto.jsm: decryptAttachment: decryption OK\n"
+      );
+    } else if (
+      statusFlagsObj.value &
+      (EnigmailConstants.DECRYPTION_FAILED | EnigmailConstants.MISSING_MDC)
+    ) {
+      EnigmailLog.DEBUG(
+        "persistentCrypto.jsm: decryptAttachment: decryption without MDC protection\n"
+      );
+    } else if (statusFlagsObj.value & EnigmailConstants.DECRYPTION_FAILED) {
+      EnigmailLog.DEBUG(
+        "persistentCrypto.jsm: decryptAttachment: decryption failed\n"
+      );
+      // Enigmail promts the user here, but we just keep going.
+    } else if (statusFlagsObj.value & EnigmailConstants.DECRYPTION_INCOMPLETE) {
+      // failure; message not complete
+      EnigmailLog.DEBUG(
+        "persistentCrypto.jsm: decryptAttachment: decryption incomplete\n"
+      );
+      return;
+    } else {
+      // there is nothing to be decrypted
+      EnigmailLog.DEBUG(
+        "persistentCrypto.jsm: decryptAttachment: no decryption required\n"
+      );
+      return;
+    }
+
+    EnigmailLog.DEBUG(
+      "persistentCrypto.jsm: decryptAttachment: decrypted to " +
+        data.length +
+        " bytes\n"
+    );
+    if (statusFlagsObj.encryptedFileName) {
       attachmentName = statusFlagsObj.encryptedFileName;
     }
 
     this.decryptedMessage = true;
-    mimePart.body = listener.stdoutData;
-    mimePart.headers._rawHeaders.set("content-disposition", `attachment; filename="${attachmentName}"`);
+    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) {
     EnigmailLog.DEBUG("persistentCrypto.jsm: decryptINLINE()\n");
 
     if ("decryptedPgpMime" in mimePart && mimePart.decryptedPgpMime) {
       return 0;
     }
@@ -611,17 +871,18 @@ CryptMessageIntoFolder.prototype = {
             sigDetailsObj,
             errorMsgObj,
             blockSeparationObj,
             encToDetailsObj
           );
           if (!plaintext || plaintext.length === 0) {
             if (statusFlagsObj.value & EnigmailConstants.DISPLAY_MESSAGE) {
               EnigmailDialog.alert(null, errorMsgObj.value);
-              this.messageDecrypted = false;
+              this.cryptoChanged = false;
+              this.decryptFailure = true;
               return -1;
             }
 
             if (
               statusFlagsObj.value &
               (EnigmailConstants.DECRYPTION_FAILED |
                 EnigmailConstants.MISSING_MDC)
             ) {
@@ -642,23 +903,25 @@ CryptMessageIntoFolder.prototype = {
               if (
                 !EnigmailDialog.confirmDlg(
                   null,
                   msg,
                   l10n.formatValueSync("dlg-button-retry"),
                   l10n.formatValueSync("dlg-button-skip")
                 )
               ) {
-                this.messageDecrypted = false;
+                this.cryptoChanged = false;
+                this.decryptFailure = true;
                 return -1;
               }
             } else if (
               statusFlagsObj.value & EnigmailConstants.DECRYPTION_INCOMPLETE
             ) {
-              this.messageDecrypted = false;
+              this.cryptoChanged = false;
+              this.decryptFailure = true;
               return -1;
             } else {
               plaintext = " ";
             }
           }
 
           if (ct === "text/html") {
             plaintext = plaintext.replace(/\n/gi, "<br/>\n");
@@ -706,30 +969,30 @@ CryptMessageIntoFolder.prototype = {
         mimePart.headers._rawHeaders.set("content-transfer-encoding", [
           "base64",
         ]);
       } else {
         mimePart.headers._rawHeaders.set("content-transfer-encoding", ["8bit"]);
       }
       mimePart.body = decryptedMessage;
 
-      let origCharset = getCharset(getHeaderValue(mimePart, "content-type"));
+      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.messageDecrypted = true;
+      this.cryptoChanged = true;
       return 1;
     }
 
     let ct = getContentType(mimePart);
     EnigmailLog.DEBUG(
       "persistentCrypto.jsm: Decryption skipped:  " + ct + "\n"
     );
 
@@ -771,173 +1034,63 @@ CryptMessageIntoFolder.prototype = {
       "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()) {
-        msg += formatMimeHeader(hdr, rawHdr.get(hdr)) + "\r\n";
+        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 === "quoted-printable") {
-        mimePart.headers._rawHeaders.set("content-transfer-encoding", [
-          "base64",
-        ]);
-        encoding = "base64";
-      }
-
       if (encoding === "base64") {
         msg += EnigmailData.encodeBase64(mimePart.body);
       } else {
-        msg += mimePart.body;
+        let charset = getCharset(mimePart, "content-type");
+        if (charset) {
+          msg += EnigmailData.convertFromUnicode(mimePart.body, charset);
+        } else {
+          msg += mimePart.body;
+        }
       }
     }
 
     if (mimePart.subParts.length > 0) {
       let boundary = 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;
   },
 
-  storeMessage(msg) {
-    let self = this;
-
-    return new Promise((resolve, reject) => {
-      //XXX Do we wanna use the tmp for this?
-      let tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
-      tempFile.append("message.eml");
-      tempFile.createUnique(0, 384); // == 0600, octal is deprecated
-
-      // ensure that file gets deleted on exit, if something goes wrong ...
-      let extAppLauncher = Cc["@mozilla.org/mime;1"].getService(
-        Ci.nsPIExternalAppLauncher
-      );
-
-      let foStream = Cc[
-        "@mozilla.org/network/file-output-stream;1"
-      ].createInstance(Ci.nsIFileOutputStream);
-      foStream.init(tempFile, 2, 0x200, false); // open as "write only"
-      foStream.write(msg, msg.length);
-      foStream.close();
-
-      extAppLauncher.deleteTemporaryFileOnExit(tempFile);
-
-      //
-      //  This was taken from the HeaderToolsLite Example Addon "original by Frank DiLecce"
-      //
-      // this is interesting: nsIMsgFolder.copyFileMessage seems to have a bug on Windows, when
-      // the nsIFile has been already used by foStream (because of Windows lock system?), so we
-      // must initialize another nsIFile object, pointing to the temporary file
-      let fileSpec = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
-      fileSpec.initWithPath(tempFile.path);
-
-      let copyListener = {
-        QueryInterface: ChromeUtils.generateQI(["nsIMsgCopyServiceListener"]),
-        GetMessageId(messageId) {},
-        OnProgress(progress, progressMax) {},
-        OnStartCopy() {
-          EnigmailLog.DEBUG(
-            "persistentCrypto.jsm: copyListener: OnStartCopy()\n"
-          );
-        },
-        SetMessageKey(key) {
-          EnigmailLog.DEBUG(
-            "persistentCrypto.jsm: copyListener: SetMessageKey(" + key + ")\n"
-          );
-        },
-        OnStopCopy(statusCode) {
-          EnigmailLog.DEBUG(
-            "persistentCrypto.jsm: copyListener: OnStopCopy()\n"
-          );
-          if (statusCode !== 0) {
-            EnigmailLog.DEBUG(
-              "persistentCrypto.jsm: Error copying message: " +
-                statusCode +
-                "\n"
-            );
-            try {
-              tempFile.remove(false);
-            } catch (ex) {
-              try {
-                fileSpec.remove(false);
-              } catch (e2) {
-                EnigmailLog.DEBUG(
-                  "persistentCrypto.jsm: Could not delete temp file\n"
-                );
-              }
-            }
-            resolve(true);
-            return;
-          }
-          EnigmailLog.DEBUG("persistentCrypto.jsm: Copy complete\n");
-
-          if (self.move) {
-            deleteOriginalMail(self.hdr);
-          }
-
-          try {
-            tempFile.remove(false);
-          } catch (ex) {
-            try {
-              fileSpec.remove(false);
-            } catch (e2) {
-              EnigmailLog.DEBUG(
-                "persistentCrypto.jsm: Could not delete temp file\n"
-              );
-            }
-          }
-
-          EnigmailLog.DEBUG("persistentCrypto.jsm: Cave Johnson. We're done\n");
-          resolve(true);
-        },
-      };
-
-      EnigmailLog.DEBUG("persistentCrypto.jsm: copySvc ready for copy\n");
-      try {
-        if (self.mimeTree.headers.has("subject")) {
-          self.hdr.subject = self.mimeTree.headers.get("subject");
-        }
-      } catch (ex) {}
-
-      MailServices.copy.copyFileMessage(
-        fileSpec,
-        MailUtils.getExistingFolder(self.destFolder),
-        null,
-        false,
-        0,
-        "",
-        copyListener,
-        null
-      );
-    });
-  },
-
   fixExchangeMessage(mimePart) {
     EnigmailLog.DEBUG("persistentCrypto.jsm: fixExchangeMessage()\n");
 
     let msg = this.mimeToString(mimePart, true);
 
     try {
       let fixedMsg = EnigmailFixExchangeMsg.getRepairedMessage(msg);
       let replacement = EnigmailMime.getMimeTree(fixedMsg, true);
@@ -958,32 +1111,21 @@ CryptMessageIntoFolder.prototype = {
 function formatHeader(headerLabel) {
   return headerLabel.replace(/^.|(-.)/g, function(match) {
     return match.toUpperCase();
   });
 }
 
 function formatMimeHeader(headerLabel, headerValue) {
   if (Array.isArray(headerValue)) {
-    headerValue = headerValue.join("");
+    return headerValue
+      .map(v => formatHeader(headerLabel) + ": " + v)
+      .join("\r\n");
   }
-  if (headerLabel.search(/^(sender|from|reply-to|to|cc|bcc)$/i) === 0) {
-    return (
-      formatHeader(headerLabel) +
-      ": " +
-      EnigmailMime.formatHeaderData(
-        EnigmailMime.formatEmailAddress(headerValue)
-      )
-    );
-  }
-  return (
-    formatHeader(headerLabel) +
-    ": " +
-    EnigmailMime.formatHeaderData(EnigmailMime.encodeHeaderValue(headerValue))
-  );
+  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, GlodaUtils.deMime(headerData[i])));
     }
@@ -1085,16 +1227,32 @@ function isAttachment(mime) {
           }
         }
       }
     }
   } 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 EnigmailMime.getParameter(c, "filename");
+    }
+  }
+  return null;
+}
+
 function getPepSubject(mimeString) {
   EnigmailLog.DEBUG("persistentCrypto.jsm: getPepSubject()\n");
 
   let subject = null;
 
   let emitter = {
     ct: "",
     firstPlainText: false,
@@ -1147,35 +1305,8 @@ function getPepSubject(mimeString) {
 
   try {
     let p = new jsmime.MimeParser(emitter, opt);
     p.deliverData(mimeString);
   } catch (ex) {}
 
   return subject;
 }
-
-/**
- * Lazy deletion of original messages
- */
-function deleteOriginalMail(msgHdr) {
-  EnigmailLog.DEBUG(
-    "persistentCrypto.jsm: deleteOriginalMail(" + msgHdr.messageKey + ")\n"
-  );
-
-  let delMsg = function() {
-    try {
-      EnigmailLog.DEBUG(
-        "persistentCrypto.jsm: deleting original message " +
-          msgHdr.messageKey +
-          "\n"
-      );
-
-      msgHdr.folder.deleteMessages([msgHdr], null, false, false, null, true);
-    } catch (e) {
-      EnigmailLog.DEBUG(
-        "persistentCrypto.jsm: deletion failed. Error: " + e.toString() + "\n"
-      );
-    }
-  };
-
-  setTimeout(delMsg, 500);
-}
--- a/mail/extensions/openpgp/content/ui/enigmailMessengerOverlay.js
+++ b/mail/extensions/openpgp/content/ui/enigmailMessengerOverlay.js
@@ -2856,30 +2856,45 @@ Enigmail.msg = {
       if (event.button === 0 && event.detail == 2) {
         // double click
         this.handleAttachment("openAttachment", attachment);
         event.stopPropagation();
       }
     }
   },
 
-  // create a decrypted copy of all selected messages in a target folder
-
-  decryptToFolder(destFolder) {
+  // decrypted and copy/move all selected messages in a target folder
+
+  async decryptToFolder(destFolder, move) {
     let msgHdrs = gFolderDisplay ? gFolderDisplay.selectedMessages : null;
     if (!msgHdrs || msgHdrs.length === 0) {
       return;
     }
 
-    EnigmailPersistentCrypto.dispatchMessages(
-      msgHdrs,
-      destFolder.URI,
-      false,
-      false
-    );
+    let total = gFolderDisplay.selectedMessages.length;
+
+    let failures = 0;
+    for (let msgHdr of msgHdrs) {
+      await EnigmailPersistentCrypto.cryptMessage(
+        msgHdr,
+        destFolder.URI,
+        move,
+        false
+      ).catch(err => {
+        failures++;
+      });
+    }
+
+    if (failures) {
+      let info = await document.l10n.formatValue("decrypt-and-copy-failures", {
+        failures,
+        total,
+      });
+      Services.prompt.alert(null, document.title, info);
+    }
   },
 
   async searchKeysOnInternet(aHeaderNode) {
     let address = aHeaderNode
       .closest("mail-emailaddress")
       .getAttribute("emailAddress");
 
     KeyLookupHelper.lookupAndImportByEmail(window, address, true, null);
--- a/mail/locales/en-US/messenger/messenger.ftl
+++ b/mail/locales/en-US/messenger/messenger.ftl
@@ -115,16 +115,20 @@ context-menu-redirect-msg =
     .label = Redirect
 
 mail-context-delete-messages =
   .label = { $count ->
      [one] Delete message
     *[other] Delete selected messages
   }
 
+context-menu-decrypt-to-folder =
+    .label = Copy As Decrypted To
+    .accesskey = y
+
 ## Message header pane
 
 other-action-redirect-msg =
     .label = Redirect
 
 message-header-msg-flagged =
     .title = Starred
     .aria-label = Starred
@@ -166,8 +170,12 @@ repair-text-encoding-button =
   .label = Repair Text Encoding
   .tooltiptext = Guess correct text encoding from message content
 
 ## no-reply handling
 
 no-reply-title = Reply Not Supported
 no-reply-message = The reply address ({ $email }) does not appear to be a monitored address. Messages to this address will likely not be read by anyone.
 no-reply-reply-anyway-button = Reply Anyway
+
+## error messages
+
+decrypt-and-copy-failures = { $failures } of { $total } messages could not be decrypted and were not copied.
--- a/mailnews/build/nsMailModule.cpp
+++ b/mailnews/build/nsMailModule.cpp
@@ -596,25 +596,27 @@ NS_DEFINE_NAMED_CID(NS_MSGMDNGENERATOR_C
 
 ////////////////////////////////////////////////////////////////////////////////
 // smime factories
 ////////////////////////////////////////////////////////////////////////////////
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsMsgComposeSecure)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsSMimeJSHelper)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsEncryptedSMIMEURIsService)
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsCMSDecoder, Init)
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsCMSDecoderJS, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsCMSEncoder, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsCMSMessage, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsCMSSecureMessage, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsCertPicker, Init)
 
 NS_DEFINE_NAMED_CID(NS_MSGCOMPOSESECURE_CID);
 NS_DEFINE_NAMED_CID(NS_SMIMEJSJELPER_CID);
 NS_DEFINE_NAMED_CID(NS_SMIMEENCRYPTURISERVICE_CID);
 NS_DEFINE_NAMED_CID(NS_CMSDECODER_CID);
+NS_DEFINE_NAMED_CID(NS_CMSDECODERJS_CID);
 NS_DEFINE_NAMED_CID(NS_CMSENCODER_CID);
 NS_DEFINE_NAMED_CID(NS_CMSMESSAGE_CID);
 NS_DEFINE_NAMED_CID(NS_CMSSECUREMESSAGE_CID);
 NS_DEFINE_NAMED_CID(NS_CERT_PICKER_CID);
 
 ////////////////////////////////////////////////////////////////////////////////
 // PGP/MIME factories
 ////////////////////////////////////////////////////////////////////////////////
@@ -827,16 +829,17 @@ const mozilla::Module::CIDEntry kMailNew
     // mdn Entries
     {&kNS_MSGMDNGENERATOR_CID, false, NULL, nsMsgMdnGeneratorConstructor},
     // SMime Entries
     {&kNS_MSGCOMPOSESECURE_CID, false, NULL, nsMsgComposeSecureConstructor},
     {&kNS_SMIMEJSJELPER_CID, false, NULL, nsSMimeJSHelperConstructor},
     {&kNS_SMIMEENCRYPTURISERVICE_CID, false, NULL,
      nsEncryptedSMIMEURIsServiceConstructor},
     {&kNS_CMSDECODER_CID, false, NULL, nsCMSDecoderConstructor},
+    {&kNS_CMSDECODERJS_CID, false, NULL, nsCMSDecoderJSConstructor},
     {&kNS_CMSENCODER_CID, false, NULL, nsCMSEncoderConstructor},
     {&kNS_CMSMESSAGE_CID, false, NULL, nsCMSMessageConstructor},
     {&kNS_CMSSECUREMESSAGE_CID, false, NULL, nsCMSSecureMessageConstructor},
     {&kNS_CERT_PICKER_CID, false, nullptr, nsCertPickerConstructor},
     // PGP/MIME Entries
     {&kNS_PGPMIME_CONTENT_TYPE_HANDLER_CID, false, NULL,
      nsPgpMimeMimeContentTypeHandlerConstructor},
     {&kNS_PGPMIMEPROXY_CID, false, NULL, nsPgpMimeProxyConstructor},
@@ -1024,16 +1027,17 @@ const mozilla::Module::ContractIDEntry k
     // mdn Entries
     {NS_MSGMDNGENERATOR_CONTRACTID, &kNS_MSGMDNGENERATOR_CID},
     // SMime Entries
     {NS_MSGCOMPOSESECURE_CONTRACTID, &kNS_MSGCOMPOSESECURE_CID},
     {NS_SMIMEJSHELPER_CONTRACTID, &kNS_SMIMEJSJELPER_CID},
     {NS_SMIMEENCRYPTURISERVICE_CONTRACTID, &kNS_SMIMEENCRYPTURISERVICE_CID},
     {NS_CMSSECUREMESSAGE_CONTRACTID, &kNS_CMSSECUREMESSAGE_CID},
     {NS_CMSDECODER_CONTRACTID, &kNS_CMSDECODER_CID},
+    {NS_CMSDECODERJS_CONTRACTID, &kNS_CMSDECODERJS_CID},
     {NS_CMSENCODER_CONTRACTID, &kNS_CMSENCODER_CID},
     {NS_CMSMESSAGE_CONTRACTID, &kNS_CMSMESSAGE_CID},
     {NS_CERTPICKDIALOGS_CONTRACTID, &kNS_CERT_PICKER_CID},
     {NS_CERT_PICKER_CONTRACTID, &kNS_CERT_PICKER_CID},
     // PGP/MIME Entries
     {"@mozilla.org/mimecth;1?type=multipart/encrypted",
      &kNS_PGPMIME_CONTENT_TYPE_HANDLER_CID},
     {NS_PGPMIMEPROXY_CONTRACTID, &kNS_PGPMIMEPROXY_CID},
--- a/mailnews/extensions/smime/moz.build
+++ b/mailnews/extensions/smime/moz.build
@@ -1,16 +1,17 @@
 # vim: set filetype=python:
 # 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/.
 
 XPIDL_SOURCES += [
     "nsICertPickDialogs.idl",
     "nsICMSDecoder.idl",
+    "nsICMSDecoderJS.idl",
     "nsICMSEncoder.idl",
     "nsICMSMessage.idl",
     "nsICMSMessageErrors.idl",
     "nsICMSSecureMessage.idl",
     "nsIEncryptedSMIMEURIsSrvc.idl",
     "nsIMsgSMIMEHeaderSink.idl",
     "nsISMimeJSHelper.idl",
     "nsIUserCertPicker.idl",
--- a/mailnews/extensions/smime/nsCMS.cpp
+++ b/mailnews/extensions/smime/nsCMS.cpp
@@ -958,35 +958,52 @@ loser:
   if (m_cmsMsg) {
     NSS_CMSMessage_Destroy(m_cmsMsg);
     m_cmsMsg = nullptr;
   }
   return rv;
 }
 
 NS_IMPL_ISUPPORTS(nsCMSDecoder, nsICMSDecoder)
+NS_IMPL_ISUPPORTS(nsCMSDecoderJS, nsICMSDecoderJS)
 
 nsCMSDecoder::nsCMSDecoder() : m_dcx(nullptr) {}
+nsCMSDecoderJS::nsCMSDecoderJS() : m_dcx(nullptr) {}
 
 nsCMSDecoder::~nsCMSDecoder() { destructorSafeDestroyNSSReference(); }
+nsCMSDecoderJS::~nsCMSDecoderJS() { destructorSafeDestroyNSSReference(); }
 
 nsresult nsCMSDecoder::Init() {
   nsresult rv;
   nsCOMPtr<nsISupports> nssInitialized =
       do_GetService("@mozilla.org/psm;1", &rv);
   return rv;
 }
 
+nsresult nsCMSDecoderJS::Init() {
+  nsresult rv;
+  nsCOMPtr<nsISupports> nssInitialized =
+      do_GetService("@mozilla.org/psm;1", &rv);
+  return rv;
+}
+
 void nsCMSDecoder::destructorSafeDestroyNSSReference() {
   if (m_dcx) {
     NSS_CMSDecoder_Cancel(m_dcx);
     m_dcx = nullptr;
   }
 }
 
+void nsCMSDecoderJS::destructorSafeDestroyNSSReference() {
+  if (m_dcx) {
+    NSS_CMSDecoder_Cancel(m_dcx);
+    m_dcx = nullptr;
+  }
+}
+
 /* void start (in NSSCMSContentCallback cb, in voidPtr arg); */
 NS_IMETHODIMP nsCMSDecoder::Start(NSSCMSContentCallback cb, void* arg) {
   MOZ_LOG(gCMSLog, LogLevel::Debug, ("nsCMSDecoder::Start"));
   m_ctx = new PipUIContext();
 
   m_dcx = NSS_CMSDecoder_Start(0, cb, arg, 0, m_ctx, 0, 0);
   if (!m_dcx) {
     MOZ_LOG(gCMSLog, LogLevel::Debug,
@@ -1014,16 +1031,56 @@ NS_IMETHODIMP nsCMSDecoder::Finish(nsICM
     // we gave it on construction.
     // Make sure the context will live long enough.
     obj->referenceContext(m_ctx);
     NS_ADDREF(*aCMSMsg = obj);
   }
   return NS_OK;
 }
 
+void nsCMSDecoderJS::content_callback(void* arg, const char* input,
+                                      unsigned long length) {
+  nsCMSDecoderJS* self = reinterpret_cast<nsCMSDecoderJS*>(arg);
+  self->mDecryptedData.AppendElements(input, length);
+}
+
+NS_IMETHODIMP nsCMSDecoderJS::Decrypt(const nsTArray<uint8_t>& aInput,
+                                      nsTArray<uint8_t>& _retval) {
+  if (aInput.IsEmpty()) {
+    return NS_ERROR_FAILURE;
+  }
+
+  m_ctx = new PipUIContext();
+
+  m_dcx = NSS_CMSDecoder_Start(0, nsCMSDecoderJS::content_callback, this, 0,
+                               m_ctx, 0, 0);
+  if (!m_dcx) {
+    MOZ_LOG(gCMSLog, LogLevel::Debug,
+            ("nsCMSDecoderJS::Start - can't start decoder"));
+    return NS_ERROR_FAILURE;
+  }
+
+  NSS_CMSDecoder_Update(m_dcx, (char*)aInput.Elements(), aInput.Length());
+
+  NSSCMSMessage* cmsMsg;
+  cmsMsg = NSS_CMSDecoder_Finish(m_dcx);
+  m_dcx = nullptr;
+  if (cmsMsg) {
+    nsCMSMessage* obj = new nsCMSMessage(cmsMsg);
+    // The NSS object cmsMsg still carries a reference to the context
+    // we gave it on construction.
+    // Make sure the context will live long enough.
+    obj->referenceContext(m_ctx);
+    mCMSMessage = obj;
+  }
+
+  _retval = mDecryptedData.Clone();
+  return NS_OK;
+}
+
 NS_IMPL_ISUPPORTS(nsCMSEncoder, nsICMSEncoder)
 
 nsCMSEncoder::nsCMSEncoder() : m_ecx(nullptr) {}
 
 nsCMSEncoder::~nsCMSEncoder() { destructorSafeDestroyNSSReference(); }
 
 nsresult nsCMSEncoder::Init() {
   nsresult rv;
--- a/mailnews/extensions/smime/nsCMS.h
+++ b/mailnews/extensions/smime/nsCMS.h
@@ -7,16 +7,17 @@
 #define __NS_CMS_H__
 
 #include "nsISupports.h"
 #include "nsCOMPtr.h"
 #include "nsIInterfaceRequestor.h"
 #include "nsICMSMessage.h"
 #include "nsICMSEncoder.h"
 #include "nsICMSDecoder.h"
+#include "nsICMSDecoderJS.h"
 #include "sechash.h"
 #include "cms.h"
 
 #define NS_CMSMESSAGE_CID                            \
   {                                                  \
     0xa4557478, 0xae16, 0x11d5, {                    \
       0xba, 0x4b, 0x00, 0x10, 0x83, 0x03, 0xb1, 0x17 \
     }                                                \
@@ -71,16 +72,37 @@ class nsCMSDecoder : public nsICMSDecode
 
  private:
   virtual ~nsCMSDecoder();
   nsCOMPtr<nsIInterfaceRequestor> m_ctx;
   NSSCMSDecoderContext* m_dcx;
   void destructorSafeDestroyNSSReference();
 };
 
+class nsCMSDecoderJS : public nsICMSDecoderJS {
+ public:
+  NS_DECL_THREADSAFE_ISUPPORTS
+  NS_DECL_NSICMSDECODERJS
+
+  nsCMSDecoderJS();
+  nsresult Init();
+
+ private:
+  virtual ~nsCMSDecoderJS();
+  nsCOMPtr<nsIInterfaceRequestor> m_ctx;
+  NSSCMSDecoderContext* m_dcx;
+  void destructorSafeDestroyNSSReference();
+
+  nsTArray<uint8_t> mDecryptedData;
+  nsCOMPtr<nsICMSMessage> mCMSMessage;
+
+  static void content_callback(void* arg, const char* input,
+                               unsigned long length);
+};
+
 // ===============================================
 // nsCMSEncoder - implementation of nsICMSEncoder
 // ===============================================
 
 #define NS_CMSENCODER_CID                            \
   {                                                  \
     0xa15789aa, 0x8903, 0x462b, {                    \
       0x81, 0xe9, 0x4a, 0xa2, 0xcf, 0xf4, 0xd5, 0xcb \
--- a/mailnews/extensions/smime/nsICMSDecoder.idl
+++ b/mailnews/extensions/smime/nsICMSDecoder.idl
@@ -12,17 +12,18 @@ typedef void (*NSSCMSContentCallback)(vo
 %}
 
 native NSSCMSContentCallback(NSSCMSContentCallback);
 
 interface nsICMSMessage;
 
 /**
  * nsICMSDecoder
- *  Interface to decode an CMS message
+ *  Streaming interface to decode an CMS message, the input data may be
+ *  passed in chunks. Cannot be called from JS.
  */
 [uuid(c7c7033b-f341-4065-aadd-7eef55ce0dda)]
 interface nsICMSDecoder : nsISupports
 {
   void start(in NSSCMSContentCallback cb, in voidPtr arg);
   void update(in string aBuf, in long aLen);
   void finish(out nsICMSMessage msg);
 };
new file mode 100644
--- /dev/null
+++ b/mailnews/extensions/smime/nsICMSDecoderJS.idl
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsICMSMessage;
+
+/**
+ * nsICMSDecoderJS
+ *  Interface to decode a CMS message, can be called from JavaScript.
+ */
+[scriptable,uuid(7e0a6708-17f4-4573-8115-1ca14f1442e0)]
+interface nsICMSDecoderJS : nsISupports
+{
+  /**
+   * Return the result of decoding/decrypting the given CMS message.
+   *
+   * @param aInput  The bytes of a CMS message.
+   * @return        The decoded data
+   */
+  Array<uint8_t> decrypt(in Array<uint8_t> input);
+};
+
--- a/mailnews/extensions/smime/nsMsgSMIMECID.h
+++ b/mailnews/extensions/smime/nsMsgSMIMECID.h
@@ -18,16 +18,25 @@
 
 #define NS_SMIMEJSJELPER_CID                         \
   { /* d57d928c-60e4-4f81-999d-5c762e611205 */       \
     0xd57d928c, 0x60e4, 0x4f81, {                    \
       0x99, 0x9d, 0x5c, 0x76, 0x2e, 0x61, 0x12, 0x05 \
     }                                                \
   }
 
+#define NS_CMSDECODERJS_CONTRACTID "@mozilla.org/nsCMSDecoderJS;1"
+
+#define NS_CMSDECODERJS_CID                          \
+  { /* fb62c8ed-b875-488a-be35-ab9764bcad25 */       \
+    0xfb62c8ed, 0xb875, 0x488a, {                    \
+      0xbe, 0x35, 0xab, 0x97, 0x64, 0xbc, 0xad, 0x25 \
+    }                                                \
+  }
+
 #define NS_SMIMEENCRYPTURISERVICE_CONTRACTID \
   "@mozilla.org/messenger-smime/smime-encrypted-uris-service;1"
 
 #define NS_SMIMEENCRYPTURISERVICE_CID                \
   { /* a0134d58-018f-4d40-a099-fa079e5024a6 */       \
     0xa0134d58, 0x018f, 0x4d40, {                    \
       0xa0, 0x99, 0xfa, 0x07, 0x9e, 0x50, 0x24, 0xa6 \
     }                                                \