/*
 * 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";

/* import-globals-from ../../../../components/compose/content/MsgComposeCommands.js */
/* import-globals-from ../../../../components/compose/content/addressingWidgetOverlay.js */
/* global MsgAccountManager */
/* global gCurrentIdentity */

var { MailServices } = ChromeUtils.import(
  "resource:///modules/MailServices.jsm"
);
var EnigmailCore = ChromeUtils.import(
  "chrome://openpgp/content/modules/core.jsm"
).EnigmailCore;
var EnigmailFuncs = ChromeUtils.import(
  "chrome://openpgp/content/modules/funcs.jsm"
).EnigmailFuncs;
var { EnigmailLog } = ChromeUtils.import(
  "chrome://openpgp/content/modules/log.jsm"
);
var EnigmailArmor = ChromeUtils.import(
  "chrome://openpgp/content/modules/armor.jsm"
).EnigmailArmor;
var EnigmailData = ChromeUtils.import(
  "chrome://openpgp/content/modules/data.jsm"
).EnigmailData;
var EnigmailDialog = ChromeUtils.import(
  "chrome://openpgp/content/modules/dialog.jsm"
).EnigmailDialog;
var EnigmailWindows = ChromeUtils.import(
  "chrome://openpgp/content/modules/windows.jsm"
).EnigmailWindows;
var EnigmailKeyRing = ChromeUtils.import(
  "chrome://openpgp/content/modules/keyRing.jsm"
).EnigmailKeyRing;
var EnigmailURIs = ChromeUtils.import(
  "chrome://openpgp/content/modules/uris.jsm"
).EnigmailURIs;
var EnigmailConstants = ChromeUtils.import(
  "chrome://openpgp/content/modules/constants.jsm"
).EnigmailConstants;
var EnigmailDecryption = ChromeUtils.import(
  "chrome://openpgp/content/modules/decryption.jsm"
).EnigmailDecryption;
var EnigmailEncryption = ChromeUtils.import(
  "chrome://openpgp/content/modules/encryption.jsm"
).EnigmailEncryption;
var EnigmailWkdLookup = ChromeUtils.import(
  "chrome://openpgp/content/modules/wkdLookup.jsm"
).EnigmailWkdLookup;
var EnigmailMime = ChromeUtils.import(
  "chrome://openpgp/content/modules/mime.jsm"
).EnigmailMime;
var EnigmailMsgRead = ChromeUtils.import(
  "chrome://openpgp/content/modules/msgRead.jsm"
).EnigmailMsgRead;
var EnigmailMimeEncrypt = ChromeUtils.import(
  "chrome://openpgp/content/modules/mimeEncrypt.jsm"
).EnigmailMimeEncrypt;
const { EnigmailCryptoAPI } = ChromeUtils.import(
  "chrome://openpgp/content/modules/cryptoAPI.jsm"
);
const { OpenPGPAlias } = ChromeUtils.import(
  "chrome://openpgp/content/modules/OpenPGPAlias.jsm"
);
var { jsmime } = ChromeUtils.import("resource:///modules/jsmime.jsm");

var l10nOpenPGP = new Localization(["messenger/openpgp/openpgp.ftl"]);

// Account encryption policy values:
// const kEncryptionPolicy_Never = 0;
// 'IfPossible' was used by ns4.
// const kEncryptionPolicy_IfPossible = 1;
var kEncryptionPolicy_Always = 2;

var Enigmail = {};

const IOSERVICE_CONTRACTID = "@mozilla.org/network/io-service;1";
const LOCAL_FILE_CONTRACTID = "@mozilla.org/file/local;1";

Enigmail.msg = {
  editor: null,
  dirty: 0,
  // dirty means: composer contents were modified by this code, right?
  processed: null, // contains information for undo of inline signed/encrypt
  timeoutId: null, // TODO: once set, it's never reset
  sendPgpMime: true,
  //sendMode: null, // the current default for sending a message (0, SIGN, ENCRYPT, or SIGN|ENCRYPT)
  //sendModeDirty: false, // send mode or final send options changed?

  // processed strings to signal final encrypt/sign/pgpmime state:
  statusEncryptedStr: "???",
  statusSignedStr: "???",
  //statusPGPMimeStr: "???",
  //statusSMimeStr: "???",
  //statusInlinePGPStr: "???",
  statusAttachOwnKey: "???",

  sendProcess: false,
  composeBodyReady: false,
  modifiedAttach: null,
  lastFocusedWindow: null,
  draftSubjectEncrypted: false,
  attachOwnKeyObj: {
    attachedObj: null,
    attachedKey: null,
  },

  keyLookupDone: [],

  addrOnChangeTimeout: 250,
  /* timeout when entering something into the address field */

  async composeStartup() {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.composeStartup\n"
    );

    if (!gMsgCompose || !gMsgCompose.compFields) {
      EnigmailLog.DEBUG(
        "enigmailMsgComposeOverlay.js: no gMsgCompose, leaving\n"
      );
      return;
    }

    gMsgCompose.RegisterStateListener(Enigmail.composeStateListener);
    Enigmail.msg.composeBodyReady = false;

    // Listen to message sending event
    addEventListener(
      "compose-send-message",
      Enigmail.msg.sendMessageListener.bind(Enigmail.msg),
      true
    );

    await OpenPGPAlias.load().catch(console.error);

    Enigmail.msg.composeOpen();
    //Enigmail.msg.processFinalState();
  },

  // TODO: call this from global compose when options change
  enigmailComposeProcessFinalState() {
    //Enigmail.msg.processFinalState();
  },

  /*
  handleClick: function(event, modifyType) {
    EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.handleClick\n");
    switch (event.button) {
      case 2:
        // do not process the event any further
        // needed on Windows to prevent displaying the context menu
        event.preventDefault();
        this.doPgpButton();
        break;
      case 0:
        this.doPgpButton(modifyType);
        break;
    }
  },
  */

  /* return whether the account specific setting key is enabled or disabled
   */
  /*
  getAccDefault: function(key) {
    //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.getAccDefault: identity="+this.identity.key+"("+this.identity.email+") key="+key+"\n");
    let res = null;
    let mimePreferOpenPGP = this.identity.getIntAttribute("mimePreferOpenPGP");
    let isSmimeEnabled = Enigmail.msg.isSmimeEnabled();
    let wasEnigmailEnabledForIdentity = Enigmail.msg.wasEnigmailEnabledForIdentity();
    let preferSmimeByDefault = false;

    if (isSmimeEnabled && wasEnigmailEnabledForIdentity) {
    }

    if (wasEnigmailEnabledForIdentity) {
      switch (key) {
        case 'sign':
          if (preferSmimeByDefault) {
            res = (this.identity.signMail);
          }
          else {
            res = (this.identity.getIntAttribute("defaultSigningPolicy") > 0);
          }
          break;
        case 'encrypt':
          if (preferSmimeByDefault) {
            res = (this.identity.encryptionPolicy > 0);
          }
          else {
            res = (this.identity.getIntAttribute("defaultEncryptionPolicy") > 0);
          }
          break;
        case 'sign-pgp':
          res = (this.identity.getIntAttribute("defaultSigningPolicy") > 0);
          break;
        case 'pgpMimeMode':
          res = this.identity.getBoolAttribute(key);
          break;
        case 'attachPgpKey':
          res = this.identity.attachPgpKey;
          break;
      }
      //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.getAccDefault:   "+key+"="+res+"\n");
      return res;
    }
    else if (Enigmail.msg.isSmimeEnabled()) {
      switch (key) {
        case 'sign':
          res = this.identity.signMail;
          break;
        case 'encrypt':
          res = (this.identity.encryptionPolicy > 0);
          break;
        default:
          res = false;
      }
      return res;
    }
    else {
      // every detail is disabled if OpenPGP in general is disabled:
      switch (key) {
        case 'sign':
        case 'encrypt':
        case 'pgpMimeMode':
        case 'attachPgpKey':
        case 'sign-pgp':
          return false;
      }
    }

    // should not be reached
    EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.getAccDefault:   internal error: invalid key '" + key + "'\n");
    return null;
  },
  */

  /**
   * Determine if any of Enigmail (OpenPGP) or S/MIME encryption is enabled for the account
   */
  /*
  isAnyEncryptionEnabled: function() {
    let id = getCurrentIdentity();

    return ((id.getUnicharAttribute("encryption_cert_name") !== "") ||
      Enigmail.msg.wasEnigmailEnabledForIdentity());
  },
  */

  isSmimeEnabled() {
    return (
      gCurrentIdentity.getUnicharAttribute("signing_cert_name") !== "" ||
      gCurrentIdentity.getUnicharAttribute("encryption_cert_name") !== ""
    );
  },

  /**
   * Determine if any of Enigmail (OpenPGP) or S/MIME signing is enabled for the account
   */
  /*
  getSigningEnabled: function() {
    let id = getCurrentIdentity();

    return ((id.getUnicharAttribute("signing_cert_name") !== "") ||
      Enigmail.msg.wasEnigmailEnabledForIdentity());
  },
  */

  /*
  getSmimeSigningEnabled: function() {
    let id = getCurrentIdentity();

    if (!id.getUnicharAttribute("signing_cert_name")) return false;

    return id.signMail;
  },
  */

  /*
  // set the current default for sending a message
  // depending on the identity
  processAccountSpecificDefaultOptions: function() {
    EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.processAccountSpecificDefaultOptions\n");

    const SIGN = EnigmailConstants.SEND_SIGNED;
    const ENCRYPT = EnigmailConstants.SEND_ENCRYPTED;

    this.sendMode = 0;

    if (this.getSmimeSigningEnabled()) {
      this.sendMode |= SIGN;
    }

    if (!Enigmail.msg.wasEnigmailEnabledForIdentity()) {
      return;
    }

    if (this.getAccDefault("encrypt")) {
      this.sendMode |= ENCRYPT;
    }
    if (this.getAccDefault("sign")) {
      this.sendMode |= SIGN;
    }

    //this.sendPgpMime = this.getAccDefault("pgpMimeMode");
    //console.debug("processAccountSpecificDefaultOptions sendPgpMime: " + this.sendPgpMime);
    gAttachMyPublicPGPKey = this.getAccDefault("attachPgpKey");
    this.setOwnKeyStatus();
    this.attachOwnKeyObj.attachedObj = null;
    this.attachOwnKeyObj.attachedKey = null;

    //this.finalSignDependsOnEncrypt = (this.getAccDefault("signIfEnc") || this.getAccDefault("signIfNotEnc"));
  },
  */

  getOriginalMsgUri() {
    let draftId = gMsgCompose.compFields.draftId;
    let msgUri = null;

    if (draftId) {
      // original message is draft
      msgUri = draftId.replace(/\?.*$/, "");
    } else if (gMsgCompose.originalMsgURI) {
      // original message is a "true" mail
      msgUri = gMsgCompose.originalMsgURI;
    }

    return msgUri;
  },

  getMsgHdr(msgUri) {
    try {
      if (!msgUri) {
        msgUri = this.getOriginalMsgUri();
      }
      if (msgUri) {
        return gMessenger.msgHdrFromURI(msgUri);
      }
    } catch (ex) {
      // See also bug 1635648
      console.debug("exception in getMsgHdr: " + ex);
      EnigmailLog.DEBUG(
        "enigmailMessengerOverlay.js: exception in getMsgHdr: " + ex + "\n"
      );
    }
    return null;
  },

  getMsgProperties(draft, msgUri, msgHdr, mimeMsg, obtainedDraftFlagsObj) {
    EnigmailLog.DEBUG(
      "enigmailMessengerOverlay.js: Enigmail.msg.getMsgProperties:\n"
    );
    obtainedDraftFlagsObj.value = false;

    let self = this;
    let properties = 0;
    try {
      if (msgHdr) {
        properties = msgHdr.getUint32Property("enigmail");

        if (draft) {
          if (self.getSavedDraftOptions(mimeMsg)) {
            obtainedDraftFlagsObj.value = true;
          }
          updateEncryptionDependencies();
        }
      }
    } catch (ex) {
      EnigmailLog.DEBUG(
        "enigmailMessengerOverlay.js: Enigmail.msg.getMsgProperties: got exception '" +
          ex.toString() +
          "'\n"
      );
    }

    if (EnigmailURIs.isEncryptedUri(msgUri)) {
      properties |= EnigmailConstants.DECRYPTION_OKAY;
    }

    return properties;
  },

  getSavedDraftOptions(mimeMsg) {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.getSavedDraftOptions\n"
    );
    if (!mimeMsg || !mimeMsg.headers.has("x-enigmail-draft-status")) {
      return false;
    }

    let stat = mimeMsg.headers.get("x-enigmail-draft-status").join("");
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.getSavedDraftOptions: draftStatus: " +
        stat +
        "\n"
    );

    if (stat.substr(0, 1) == "N") {
      switch (Number(stat.substr(1, 1))) {
        case 2:
          // treat as "user decision to enable encryption, disable auto"
          gUserTouchedSendEncrypted = true;
          gSendEncrypted = true;
          updateEncryptionDependencies();
          break;
        case 0:
          // treat as "user decision to disable encryption, disable auto"
          gUserTouchedSendEncrypted = true;
          gSendEncrypted = false;
          updateEncryptionDependencies();
          break;
        case 1:
        default:
          // treat as "no user decision, automatic mode"
          break;
      }

      switch (Number(stat.substr(2, 1))) {
        case 2:
          gSendSigned = true;
          gUserTouchedSendSigned = true;
          break;
        case 0:
          gUserTouchedSendSigned = true;
          gSendSigned = false;
          break;
        case 1:
        default:
          // treat as "no user decision, automatic mode, based on encryption or other prefs"
          break;
      }

      switch (Number(stat.substr(3, 1))) {
        case 1:
          break;
        case EnigmailConstants.ENIG_FORCE_SMIME:
          // 3
          gSelectedTechnologyIsPGP = false;
          break;
        case 2: // pgp/mime
        case 0: // inline
        default:
          gSelectedTechnologyIsPGP = true;
          break;
      }

      switch (Number(stat.substr(4, 1))) {
        case 1:
          gUserTouchedAttachMyPubKey = true;
          gAttachMyPublicPGPKey = true;
          break;
        case 2:
          gUserTouchedAttachMyPubKey = false;
          break;
        case 0:
        default:
          gUserTouchedAttachMyPubKey = true;
          gAttachMyPublicPGPKey = false;
          break;
      }

      switch (Number(stat.substr(4, 1))) {
        case 1:
          gUserTouchedAttachMyPubKey = true;
          gAttachMyPublicPGPKey = true;
          break;
        case 2:
          gUserTouchedAttachMyPubKey = false;
          break;
        case 0:
        default:
          gUserTouchedAttachMyPubKey = true;
          gAttachMyPublicPGPKey = false;
          break;
      }

      switch (Number(stat.substr(5, 1))) {
        case 1:
          gUserTouchedEncryptSubject = true;
          gEncryptSubject = true;
          break;
        case 2:
          gUserTouchedEncryptSubject = false;
          break;
        case 0:
        default:
          gUserTouchedEncryptSubject = true;
          gEncryptSubject = false;
          break;
      }
    }
    //Enigmail.msg.setOwnKeyStatus();
    return true;
  },

  composeOpen() {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.composeOpen\n"
    );

    let msgUri = null;
    let msgHdr = null;

    msgUri = this.getOriginalMsgUri();
    if (msgUri) {
      msgHdr = this.getMsgHdr(msgUri);
      if (msgHdr) {
        try {
          let msgUrl = EnigmailMsgRead.getUrlFromUriSpec(msgUri);
          EnigmailMime.getMimeTreeFromUrl(msgUrl.spec, false, mimeMsg => {
            Enigmail.msg.continueComposeOpenWithMimeTree(
              msgUri,
              msgHdr,
              mimeMsg
            );
          });
        } catch (ex) {
          EnigmailLog.DEBUG(
            "enigmailMessengerOverlay.js: composeOpen: exception in getMimeTreeFromUrl: " +
              ex +
              "\n"
          );
          this.continueComposeOpenWithMimeTree(msgUri, msgHdr, null);
        }
      } else {
        this.continueComposeOpenWithMimeTree(msgUri, msgHdr, null);
      }
    } else {
      this.continueComposeOpenWithMimeTree(msgUri, msgHdr, null);
    }
  },

  continueComposeOpenWithMimeTree(msgUri, msgHdr, mimeMsg) {
    let selectedElement = document.activeElement;

    let msgIsDraft =
      gMsgCompose.type === Ci.nsIMsgCompType.Draft ||
      gMsgCompose.type === Ci.nsIMsgCompType.Template;

    if (!gSendEncrypted || msgIsDraft) {
      let useEncryptionUnlessWeHaveDraftInfo = false;
      let usePGPUnlessWeKnowOtherwise = false;
      let useSMIMEUnlessWeKnowOtherwise = false;

      if (msgIsDraft) {
        let globalSaysItsEncrypted =
          gEncryptedURIService &&
          gMsgCompose.originalMsgURI &&
          gEncryptedURIService.isEncrypted(gMsgCompose.originalMsgURI);

        if (globalSaysItsEncrypted) {
          useEncryptionUnlessWeHaveDraftInfo = true;
          useSMIMEUnlessWeKnowOtherwise = true;
        }
      }

      let obtainedDraftFlagsObj = { value: false };
      if (msgUri) {
        let msgFlags = this.getMsgProperties(
          msgIsDraft,
          msgUri,
          msgHdr,
          mimeMsg,
          obtainedDraftFlagsObj
        );
        if (msgFlags & EnigmailConstants.DECRYPTION_OKAY) {
          usePGPUnlessWeKnowOtherwise = true;
          useSMIMEUnlessWeKnowOtherwise = false;
        }
        if (msgIsDraft && obtainedDraftFlagsObj.value) {
          useEncryptionUnlessWeHaveDraftInfo = false;
          usePGPUnlessWeKnowOtherwise = false;
          useSMIMEUnlessWeKnowOtherwise = false;
        }
        if (!msgIsDraft) {
          if (msgFlags & EnigmailConstants.DECRYPTION_OKAY) {
            EnigmailLog.DEBUG(
              "enigmailMsgComposeOverlay.js: Enigmail.msg.composeOpen: has encrypted originalMsgUri\n"
            );
            EnigmailLog.DEBUG(
              "originalMsgURI=" + gMsgCompose.originalMsgURI + "\n"
            );
            gSendEncrypted = true;
            updateEncryptionDependencies();
            gSelectedTechnologyIsPGP = true;
            useEncryptionUnlessWeHaveDraftInfo = false;
            usePGPUnlessWeKnowOtherwise = false;
            useSMIMEUnlessWeKnowOtherwise = false;
          }
        }
        this.removeAttachedKey();
      }

      if (useEncryptionUnlessWeHaveDraftInfo) {
        gSendEncrypted = true;
        updateEncryptionDependencies();
      }
      if (gSendEncrypted && !obtainedDraftFlagsObj.value) {
        gSendSigned = true;
      }
      if (usePGPUnlessWeKnowOtherwise) {
        gSelectedTechnologyIsPGP = true;
      } else if (useSMIMEUnlessWeKnowOtherwise) {
        gSelectedTechnologyIsPGP = false;
      }
    }

    // check for attached signature files and remove them
    var bucketList = document.getElementById("attachmentBucket");
    if (bucketList.hasChildNodes()) {
      var node = bucketList.firstChild;
      while (node) {
        if (node.attachment.contentType == "application/pgp-signature") {
          if (!this.findRelatedAttachment(bucketList, node)) {
            // Let's release the attachment object held by the node else it won't go away until the window is destroyed
            node.attachment = null;
            node = bucketList.removeChild(node);
          }
        }
        node = node.nextSibling;
      }
    }

    // If we removed all the children and the bucket wasn't meant
    // to stay open, close it.
    if (!Services.prefs.getBoolPref("mail.compose.show_attachment_pane")) {
      UpdateAttachmentBucket(bucketList.hasChildNodes());
    }

    this.warnUserIfSenderKeyExpired();

    //this.processFinalState();
    if (selectedElement) {
      selectedElement.focus();
    }
  },

  // check if an signature is related to another attachment
  findRelatedAttachment(bucketList, node) {
    // check if filename ends with .sig
    if (node.attachment.name.search(/\.sig$/i) < 0) {
      return null;
    }

    var relatedNode = bucketList.firstChild;
    var findFile = node.attachment.name.toLowerCase();
    var baseAttachment = null;
    while (relatedNode) {
      if (relatedNode.attachment.name.toLowerCase() + ".sig" == findFile) {
        baseAttachment = relatedNode.attachment;
      }
      relatedNode = relatedNode.nextSibling;
    }
    return baseAttachment;
  },

  async attachOwnKey(id) {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.attachOwnKey: " + id + "\n"
    );

    if (
      this.attachOwnKeyObj.attachedKey &&
      this.attachOwnKeyObj.attachedKey != id
    ) {
      // remove attached key if user ID changed
      this.removeAttachedKey();
    }
    let revokedIDs = EnigmailKeyRing.findRevokedPersonalKeysByEmail(
      gCurrentIdentity.email
    );

    if (!this.attachOwnKeyObj.attachedKey) {
      let hex = "0x" + id;
      var attachedObj = await this.extractAndAttachKey(
        hex,
        revokedIDs,
        gCurrentIdentity.email,
        true,
        true // one key plus revocations
      );
      if (attachedObj) {
        this.attachOwnKeyObj.attachedObj = attachedObj;
        this.attachOwnKeyObj.attachedKey = hex;
      }
    }
  },

  async extractAndAttachKey(
    primaryId,
    revokedIds,
    emailForFilename,
    warnOnError
  ) {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.extractAndAttachKey: \n"
    );
    var enigmailSvc = EnigmailCore.getService(window);
    if (!enigmailSvc) {
      return null;
    }

    var tmpFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
    tmpFile.append("key.asc");
    tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);

    // save file
    var exitCodeObj = {};
    var errorMsgObj = {};

    await EnigmailKeyRing.extractPublicKeys(
      [], // full
      [primaryId], // reduced
      revokedIds, // minimal
      tmpFile,
      exitCodeObj,
      errorMsgObj
    );
    if (exitCodeObj.value !== 0) {
      if (warnOnError) {
        EnigmailDialog.alert(window, errorMsgObj.value);
      }
      return null;
    }

    // create attachment
    var tmpFileURI = Services.io.newFileURI(tmpFile);
    var keyAttachment = Cc[
      "@mozilla.org/messengercompose/attachment;1"
    ].createInstance(Ci.nsIMsgAttachment);
    keyAttachment.url = tmpFileURI.spec;
    keyAttachment.name = primaryId.substr(-16, 16);
    if (keyAttachment.name.search(/^0x/) < 0) {
      keyAttachment.name = "0x" + keyAttachment.name;
    }
    let withRevSuffix = "";
    if (revokedIds && revokedIds.length) {
      withRevSuffix = "_and_old_rev";
    }
    keyAttachment.name =
      "OpenPGP_" + keyAttachment.name + withRevSuffix + ".asc";
    keyAttachment.temporary = true;
    keyAttachment.contentType = "application/pgp-keys";
    keyAttachment.size = tmpFile.fileSize;

    if (
      !gAttachmentBucket.itemChildren.find(
        item => item.attachment.name == keyAttachment.name
      )
    ) {
      await this.addAttachment(keyAttachment);
    }

    gContentChanged = true;
    return keyAttachment;
  },

  addAttachment(attachment) {
    return AddAttachments([attachment]);
  },

  removeAttachedKey() {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.removeAttachedKey: \n"
    );

    let bucketList = document.getElementById("attachmentBucket");
    let node = bucketList.firstElementChild;

    if (bucketList.itemCount && this.attachOwnKeyObj.attachedObj) {
      // Undo attaching own key.
      while (node) {
        if (node.attachment.url == this.attachOwnKeyObj.attachedObj.url) {
          node = bucketList.removeChild(node);
          // Let's release the attachment object held by the node else it won't
          // go away until the window is destroyed.
          node.attachment = null;
          this.attachOwnKeyObj.attachedObj = null;
          this.attachOwnKeyObj.attachedKey = null;
          node = null; // exit loop.
        } else {
          node = node.nextSibling;
        }
      }

      // Update the visibility of the attachment pane.
      UpdateAttachmentBucket(bucketList.itemCount);
    }
  },

  getSecurityParams(compFields = null) {
    if (!compFields) {
      if (!gMsgCompose) {
        return null;
      }

      compFields = gMsgCompose.compFields;
    }

    return compFields.composeSecure;
  },

  setSecurityParams(newSecurityParams) {
    if (!gMsgCompose || !gMsgCompose.compFields) {
      return;
    }
    gMsgCompose.compFields.composeSecure = newSecurityParams;
  },

  // Used on send failure, to reset the pre-send modifications
  resetUpdatedFields() {
    this.removeAttachedKey();

    // reset subject
    let p = Enigmail.msg.getSecurityParams();
    if (p && EnigmailMimeEncrypt.isEnigmailCompField(p)) {
      let si = p.wrappedJSObject;
      if (si.originalSubject) {
        gMsgCompose.compFields.subject = si.originalSubject;
      }
    }
  },

  replaceEditorText(text) {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.replaceEditorText:\n"
    );

    this.editorSelectAll();
    // Overwrite text in clipboard for security
    // (Otherwise plaintext will be available in the clipbaord)

    if (this.editor.textLength > 0) {
      this.editorInsertText("Enigmail");
    } else {
      this.editorInsertText(" ");
    }

    this.editorSelectAll();
    this.editorInsertText(text);
  },

  /**
   * Determine if Enigmail is enabled for the account
   */

  isEnigmailEnabledForIdentity() {
    return !!gCurrentIdentity.getUnicharAttribute("openpgp_key_id");
  },

  /**
   * Determine if Autocrypt is enabled for the account
   */
  isAutocryptEnabled() {
    return false;
    /*
    if (Enigmail.msg.wasEnigmailEnabledForIdentity()) {
      let srv = this.getCurrentIncomingServer();
      return (srv ? srv.getBoolValue("enableAutocrypt") : false);
    }

    return false;
    */
  },

  /*
  doPgpButton: function(what) {
    EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.doPgpButton: what=" + what + "\n");

    if (Enigmail.msg.wasEnigmailEnabledForIdentity()) {
      EnigmailCore.getService(window); // try to access Enigmail to launch the wizard if needed
    }

    // ignore settings for this account?
    try {
      if (!this.isAnyEncryptionEnabled() && !this.getSigningEnabled()) {
        return;
      }
    }
    catch (ex) {}

    switch (what) {
      case 'sign':
      case 'encrypt':
        this.setSendMode(what);
        break;

      case 'trustKeys':
        this.tempTrustAllKeys();
        break;

      case 'nothing':
        break;

      case 'displaySecuritySettings':
        this.displaySecuritySettings();
        break;
      default:
        this.displaySecuritySettings();
    }

  },
  */

  // changes the DEFAULT sendMode
  // - also called internally for saved emails
  /*
  setSendMode: function(sendMode) {
    EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.setSendMode: sendMode=" + sendMode + "\n");
    const SIGN = EnigmailConstants.SEND_SIGNED;
    const ENCRYPT = EnigmailConstants.SEND_ENCRYPTED;

    var origSendMode = this.sendMode;
    switch (sendMode) {
      case 'sign':
        this.sendMode |= SIGN;
        break;
      case 'encrypt':
        this.sendMode |= ENCRYPT;
        break;
      default:
        EnigmailDialog.alert(window, "Enigmail.msg.setSendMode - unexpected value: " + sendMode);
        break;
    }
    // sendMode changed ?
    // - sign and send are internal initializations
    if (!this.sendModeDirty && (this.sendMode != origSendMode) && sendMode != 'sign' && sendMode != 'encrypt') {
      this.sendModeDirty = true;
    }
    this.processFinalState();
  },
  */

  /**
    key function to process the final encrypt/sign/pgpmime state from all settings
   *
    @param sendFlags: contains the sendFlags if the message is really processed. Optional, can be null
      - uses as INPUT:
         - this.sendMode
         - this.encryptForced, this.encryptSigned
      - uses as OUTPUT:
         - this.statusEncrypt, this.statusSign

    no return value
  */
  processFinalState(sendFlags) {},

  /* check if encryption is possible (have keys for everyone or not)
   */
  async determineSendFlags() {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.focusChange: Enigmail.msg.determineSendFlags\n"
    );

    let detailsObj = {};
    var compFields = gMsgCompose.compFields;

    if (!Enigmail.msg.composeBodyReady) {
      compFields = Cc[
        "@mozilla.org/messengercompose/composefields;1"
      ].createInstance(Ci.nsIMsgCompFields);
    }
    Recipients2CompFields(compFields);

    // disabled, see bug 1625135
    // gMsgCompose.expandMailingLists();

    if (Enigmail.msg.isEnigmailEnabledForIdentity()) {
      var toAddrList = [];
      var arrLen = {};
      var recList;
      if (compFields.to) {
        recList = compFields.splitRecipients(compFields.to, true, arrLen);
        this.addRecipients(toAddrList, recList);
      }
      if (compFields.cc) {
        recList = compFields.splitRecipients(compFields.cc, true, arrLen);
        this.addRecipients(toAddrList, recList);
      }
      if (compFields.bcc) {
        recList = compFields.splitRecipients(compFields.bcc, true, arrLen);
        this.addRecipients(toAddrList, recList);
      }

      let addresses = [];
      try {
        addresses = EnigmailFuncs.stripEmail(toAddrList.join(", ")).split(",");
      } catch (ex) {}

      // Resolve all the email addresses if possible.
      await EnigmailKeyRing.getValidKeysForAllRecipients(addresses, detailsObj);
      //this.autoPgpEncryption = (validKeyList !== null);
    }

    // process and signal new resulting state
    //this.processFinalState();

    return detailsObj;
  },

  /*
  displaySecuritySettings: function() {
    EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.displaySecuritySettings\n");

    var inputObj = {
      gSendEncrypted: gSendEncrypted,
      gSendSigned: gSendSigned,
      success: false,
      resetDefaults: false
    };
    window.openDialog("chrome://openpgp/content/ui/enigmailEncryptionDlg.xhtml", "", "dialog,modal,centerscreen", inputObj);

    if (!inputObj.success) return; // Cancel pressed

    if (inputObj.resetDefaults) {
      // reset everything to defaults
      this.encryptForced = 1;
      this.signForced = 1;
    }
    else {
      if (this.signForced != inputObj.sign) {
        this.dirty = 2;
        this.signForced = inputObj.sign;
      }

        this.dirty = 2;

      this.encryptForced = inputObj.encrypt;
    }

    //this.processFinalState();
  },
  */

  addRecipients(toAddrList, recList) {
    for (var i = 0; i < recList.length; i++) {
      try {
        toAddrList.push(
          EnigmailFuncs.stripEmail(recList[i].replace(/[",]/g, ""))
        );
      } catch (ex) {}
    }
  },

  setDraftStatus(doEncrypt) {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.setDraftStatus - enabling draft mode\n"
    );

    // Draft Status:
    // N (for new style) plus 5 digits:
    // 1: encryption
    // 2: signing
    // 3: PGP/MIME
    // 4: attach own key
    // 5: subject encrypted

    var draftStatus = "N";

    // Encryption:
    // 2 -> required/enabled
    // 0 -> disabled

    if (!gUserTouchedSendEncrypted && !gIsRelatedToEncryptedOriginal) {
      // After opening draft, it's allowed to use automatic decision.
      draftStatus += "1";
    } else {
      // After opening draft, use the same state that is set now.
      draftStatus += gSendEncrypted ? "2" : "0";
    }

    if (!gUserTouchedSendSigned) {
      // After opening draft, it's allowed to use automatic decision.
      draftStatus += "1";
    } else {
      // After opening draft, use the same state that is set now.
      // Signing:
      // 2 -> enabled
      // 0 -> disabled
      draftStatus += gSendSigned ? "2" : "0";
    }

    // MIME/technology
    // ENIG_FORCE_SMIME == 3 -> S/MIME
    // ENIG_FORCE_ALWAYS == 2 -> PGP/MIME
    // 0 -> PGP inline
    if (gSelectedTechnologyIsPGP) {
      // inline signing currently not implemented
      draftStatus += "2";
    } else {
      draftStatus += "3";
    }

    if (!gUserTouchedAttachMyPubKey) {
      draftStatus += "2";
    } else {
      draftStatus += gAttachMyPublicPGPKey ? "1" : "0";
    }

    if (!gUserTouchedEncryptSubject) {
      draftStatus += "2";
    } else {
      draftStatus += gSendEncrypted && gEncryptSubject ? "1" : "0";
    }

    this.setAdditionalHeader("X-Enigmail-Draft-Status", draftStatus);
  },

  getSenderUserId() {
    let keyId = gCurrentIdentity?.getUnicharAttribute("openpgp_key_id");
    return keyId ? "0x" + keyId : null;
  },

  /**
   * Determine if S/MIME or OpenPGP should be used
   *
   * @param sendFlags: Number - input send flags.
   *
   * @return: Boolean:
   *   1: use OpenPGP
   *   0: use S/MIME
   */
  /*
  preferPgpOverSmime: function(sendFlags) {

    let si = Enigmail.msg.getSecurityParams(null);
    let isSmime = !EnigmailMimeEncrypt.isEnigmailCompField(si);

    if (isSmime &&
      (sendFlags & (EnigmailConstants.SEND_SIGNED | EnigmailConstants.SEND_ENCRYPTED))) {

      if (si.requireEncryptMessage || si.signMessage) {

        if (sendFlags & EnigmailConstants.SAVE_MESSAGE) {
          // use S/MIME if it's enabled for saving drafts
          return 0;
        }
        else {
          return this.mimePreferOpenPGP;
        }
      }
    }

    return 1;
  },
  */

  /* Manage the wrapping of inline signed mails
   *
   * @wrapresultObj: Result:
   * @wrapresultObj.cancelled, true if send operation is to be cancelled, else false
   * @wrapresultObj.usePpgMime, true if message send option was changed to PGP/MIME, else false
   */

  async wrapInLine(wrapresultObj) {
    EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: WrapInLine\n");
    wrapresultObj.cancelled = false;
    wrapresultObj.usePpgMime = false;
    try {
      const dce = Ci.nsIDocumentEncoder;
      var editor = gMsgCompose.editor.QueryInterface(Ci.nsIEditorMailSupport);
      var encoderFlags = dce.OutputFormatted | dce.OutputLFLineBreak;

      var wrapWidth = Services.prefs.getIntPref("mailnews.wraplength");
      if (wrapWidth > 0 && wrapWidth < 68 && editor.wrapWidth > 0) {
        if (
          EnigmailDialog.confirmDlg(
            window,
            await l10nOpenPGP.formatValue("minimal-line-wrapping", {
              width: wrapWidth,
            })
          )
        ) {
          wrapWidth = 68;
          Services.prefs.setIntPref("mailnews.wraplength", wrapWidth);
        }
      }

      if (wrapWidth && editor.wrapWidth > 0) {
        // First use standard editor wrap mechanism:
        editor.wrapWidth = wrapWidth - 2;
        editor.rewrap(true);
        editor.wrapWidth = wrapWidth;

        // Now get plaintext from editor
        var wrapText = this.editorGetContentAs("text/plain", encoderFlags);

        // split the lines into an array
        wrapText = wrapText.split(/\r\n|\r|\n/g);

        var i = 0;
        var excess = 0;
        // inspect all lines of mail text to detect if we still have excessive lines which the "standard" editor wrapper leaves
        for (i = 0; i < wrapText.length; i++) {
          if (wrapText[i].length > wrapWidth) {
            excess = 1;
          }
        }

        if (excess) {
          EnigmailLog.DEBUG(
            "enigmailMsgComposeOverlay.js: Excess lines detected\n"
          );
          var resultObj = {};
          window.openDialog(
            "chrome://openpgp/content/ui/enigmailWrapSelection.xhtml",
            "",
            "dialog,modal,centerscreen",
            resultObj
          );
          try {
            if (resultObj.cancelled) {
              // cancel pressed -> do not send, return instead.
              wrapresultObj.cancelled = true;
              return;
            }
          } catch (ex) {
            // cancel pressed -> do not send, return instead.
            wrapresultObj.cancelled = true;
            return;
          }

          var limitedLine = "";
          var restOfLine = "";

          var WrapSelect = resultObj.Select;
          switch (WrapSelect) {
            case "0": // Selection: Force rewrap
              for (i = 0; i < wrapText.length; i++) {
                if (wrapText[i].length > wrapWidth) {
                  // If the current line is too long, limit it hard to wrapWidth and insert the rest as the next line into wrapText array
                  limitedLine = wrapText[i].slice(0, wrapWidth);
                  restOfLine = wrapText[i].slice(wrapWidth);

                  // We should add quotes at the beginning of "restOfLine", if limitedLine is a quoted line
                  // However, this would be purely academic, because limitedLine will always be "standard"-wrapped
                  // by the editor-rewrapper at the space between quote sign (>) and the quoted text.

                  wrapText.splice(i, 1, limitedLine, restOfLine);
                }
              }
              break;
            case "1": // Selection: Send as is
              break;
            case "2": // Selection: Use MIME
              wrapresultObj.usePpgMime = true;
              break;
            case "3": // Selection: Edit manually -> do not send, return instead.
              wrapresultObj.cancelled = true;
              return;
          } //switch
        }
        // Now join all lines together again and feed it back into the compose editor.
        var newtext = wrapText.join("\n");
        this.replaceEditorText(newtext);
      }
    } catch (ex) {
      EnigmailLog.DEBUG(
        "enigmailMsgComposeOverlay.js: Exception while wrapping=" + ex + "\n"
      );
    }
  },

  // Save draft message. We do not want most of the other processing for encrypted mails here...
  async saveDraftMessage(senderKeyIsGnuPG) {
    EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: saveDraftMessage()\n");

    // If we have an encryption key configured, then encrypt saved
    // drafts by default, as a precaution. This is independent from the
    // final decision of sending the message encrypted or not.
    // However, we allow the user to disable encrypted drafts.
    let doEncrypt =
      Enigmail.msg.isEnigmailEnabledForIdentity() &&
      gCurrentIdentity.autoEncryptDrafts;

    this.setDraftStatus(doEncrypt);

    if (!doEncrypt) {
      try {
        let p = Enigmail.msg.getSecurityParams();
        if (EnigmailMimeEncrypt.isEnigmailCompField(p)) {
          p.wrappedJSObject.sendFlags = 0;
        }
      } catch (ex) {
        console.debug(ex);
      }

      return true;
    }

    let sendFlags =
      EnigmailConstants.SEND_PGP_MIME |
      EnigmailConstants.SEND_ENCRYPTED |
      EnigmailConstants.SEND_ENCRYPT_TO_SELF |
      EnigmailConstants.SAVE_MESSAGE;

    if (gEncryptSubject) {
      sendFlags |= EnigmailConstants.ENCRYPT_SUBJECT;
    }
    if (senderKeyIsGnuPG) {
      sendFlags |= EnigmailConstants.SEND_SENDER_KEY_EXTERNAL;
    }

    let fromAddr = this.getSenderUserId();

    let enigmailSvc = EnigmailCore.getService(window);
    if (!enigmailSvc) {
      return true;
    }

    let senderKeyUsable = await EnigmailEncryption.determineOwnKeyUsability(
      sendFlags,
      fromAddr,
      senderKeyIsGnuPG
    );
    if (senderKeyUsable.errorMsg) {
      let fullAlert = await document.l10n.formatValue(
        "msg-compose-cannot-save-draft"
      );
      fullAlert += " - " + senderKeyUsable.errorMsg;
      EnigmailDialog.alert(window, fullAlert);
      return false;
    }

    //if (this.preferPgpOverSmime(sendFlags) === 0) return true; // use S/MIME

    let secInfo;

    let param = Enigmail.msg.getSecurityParams();

    if (EnigmailMimeEncrypt.isEnigmailCompField(param)) {
      secInfo = param.wrappedJSObject;
    } else {
      try {
        secInfo = EnigmailMimeEncrypt.createMimeEncrypt(param);
        if (secInfo) {
          Enigmail.msg.setSecurityParams(secInfo);
        }
      } catch (ex) {
        EnigmailLog.writeException(
          "enigmailMsgComposeOverlay.js: Enigmail.msg.saveDraftMessage",
          ex
        );
        return false;
      }
    }

    secInfo.sendFlags = sendFlags;
    secInfo.UIFlags = 0;
    secInfo.senderEmailAddr = fromAddr;
    secInfo.recipients = "";
    secInfo.bccRecipients = "";
    secInfo.originalSubject = gMsgCompose.compFields.subject;
    this.dirty = 1;

    if (sendFlags & EnigmailConstants.ENCRYPT_SUBJECT) {
      gMsgCompose.compFields.subject = "";
    }

    return true;
  },

  createEnigmailSecurityFields(oldSecurityInfo) {
    let newSecurityInfo = EnigmailMimeEncrypt.createMimeEncrypt(
      Enigmail.msg.getSecurityParams()
    );

    if (!newSecurityInfo) {
      throw Components.Exception("", Cr.NS_ERROR_FAILURE);
    }

    Enigmail.msg.setSecurityParams(newSecurityInfo);
  },

  /*
  sendSmimeEncrypted: function(msgSendType, sendFlags, isOffline) {
    let recList;
    let toAddrList = [];
    let arrLen = {};
    const DeliverMode = Ci.nsIMsgCompDeliverMode;

    switch (msgSendType) {
      case DeliverMode.SaveAsDraft:
      case DeliverMode.SaveAsTemplate:
      case DeliverMode.AutoSaveAsDraft:
        break;
      default:
        if (gAttachMyPublicPGPKey) {
          await this.attachOwnKey();
          Attachments2CompFields(gMsgCompose.compFields); // update list of attachments
        }
    }

    gSMFields.signMessage = (sendFlags & EnigmailConstants.SEND_SIGNED ? true : false);
    gSMFields.requireEncryptMessage = (sendFlags & EnigmailConstants.SEND_ENCRYPTED ? true : false);

    Enigmail.msg.setSecurityParams(gSMFields);

    let conf = this.isSendConfirmationRequired(sendFlags);

    if (conf === null) return false;
    if (conf) {
      // confirm before send requested
      let msgCompFields = gMsgCompose.compFields;
      let splitRecipients = msgCompFields.splitRecipients;

      if (msgCompFields.to.length > 0) {
        recList = splitRecipients(msgCompFields.to, true, arrLen);
        this.addRecipients(toAddrList, recList);
      }

      if (msgCompFields.cc.length > 0) {
        recList = splitRecipients(msgCompFields.cc, true, arrLen);
        this.addRecipients(toAddrList, recList);
      }

      switch (msgSendType) {
        case DeliverMode.SaveAsDraft:
        case DeliverMode.SaveAsTemplate:
        case DeliverMode.AutoSaveAsDraft:
          break;
        default:
          if (!this.confirmBeforeSend(toAddrList.join(", "), "", sendFlags, isOffline)) {
            return false;
          }
      }
    }

    return true;
  },
  */

  getEncryptionFlags() {
    let f = 0;

    if (gSendEncrypted) {
      f |= EnigmailConstants.SEND_ENCRYPTED;
    } else {
      f &= ~EnigmailConstants.SEND_ENCRYPTED;
    }

    if (gSendSigned) {
      f |= EnigmailConstants.SEND_SIGNED;
    } else {
      f &= ~EnigmailConstants.SEND_SIGNED;
    }

    if (gSendEncrypted && gSendSigned) {
      if (Services.prefs.getBoolPref("mail.openpgp.separate_mime_layers")) {
        f |= EnigmailConstants.SEND_TWO_MIME_LAYERS;
      }
    }

    if (gSendEncrypted && gEncryptSubject) {
      f |= EnigmailConstants.ENCRYPT_SUBJECT;
    }

    return f;
  },

  resetDirty() {
    let newSecurityInfo = null;

    if (this.dirty) {
      // make sure the sendFlags are reset before the message is processed
      // (it may have been set by a previously cancelled send operation!)

      let si = Enigmail.msg.getSecurityParams();

      if (EnigmailMimeEncrypt.isEnigmailCompField(si)) {
        si.sendFlags = 0;
        si.originalSubject = gMsgCompose.compFields.subject;
      } else {
        try {
          newSecurityInfo = EnigmailMimeEncrypt.createMimeEncrypt(si);
          if (newSecurityInfo) {
            newSecurityInfo.sendFlags = 0;
            newSecurityInfo.originalSubject = gMsgCompose.compFields.subject;

            Enigmail.msg.setSecurityParams(newSecurityInfo);
          }
        } catch (ex) {
          EnigmailLog.writeException(
            "enigmailMsgComposeOverlay.js: Enigmail.msg.resetDirty",
            ex
          );
        }
      }
    }

    return newSecurityInfo;
  },

  async determineMsgRecipients(sendFlags) {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.determineMsgRecipients: currentId=" +
        gCurrentIdentity +
        ", " +
        gCurrentIdentity.email +
        "\n"
    );

    let fromAddr = gCurrentIdentity.email;
    let toAddrList = [];
    let recList;
    let bccAddrList = [];
    let arrLen = {};
    let splitRecipients;

    if (!Enigmail.msg.isEnigmailEnabledForIdentity()) {
      return true;
    }

    let optSendFlags = 0;
    let msgCompFields = gMsgCompose.compFields;
    let newsgroups = msgCompFields.newsgroups;

    if (Services.prefs.getBoolPref("temp.openpgp.encryptToSelf")) {
      optSendFlags |= EnigmailConstants.SEND_ENCRYPT_TO_SELF;
    }

    sendFlags |= optSendFlags;

    var userIdValue = this.getSenderUserId();
    if (userIdValue) {
      fromAddr = userIdValue;
    }

    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.determineMsgRecipients:gMsgCompose=" +
        gMsgCompose +
        "\n"
    );

    splitRecipients = msgCompFields.splitRecipients;

    if (msgCompFields.to.length > 0) {
      recList = splitRecipients(msgCompFields.to, true, arrLen);
      this.addRecipients(toAddrList, recList);
    }

    if (msgCompFields.cc.length > 0) {
      recList = splitRecipients(msgCompFields.cc, true, arrLen);
      this.addRecipients(toAddrList, recList);
    }

    // We allow sending to BCC recipients, we assume the user interface
    // has warned the user that there is no privacy of BCC recipients.
    if (msgCompFields.bcc.length > 0) {
      recList = splitRecipients(msgCompFields.bcc, true, arrLen);
      this.addRecipients(bccAddrList, recList);
    }

    if (newsgroups) {
      toAddrList.push(newsgroups);

      if (sendFlags & EnigmailConstants.SEND_ENCRYPTED) {
        if (!Services.prefs.getBoolPref("temp.openpgp.encryptToNews")) {
          document.l10n.formatValue("sending-news").then(value => {
            EnigmailDialog.alert(window, value);
          });
          return false;
        } else if (
          !EnigmailDialog.confirmBoolPref(
            window,
            await l10nOpenPGP.formatValue("send-to-news-warning"),
            "temp.openpgp.warnOnSendingNewsgroups",
            await l10nOpenPGP.formatValue("msg-compose-button-send")
          )
        ) {
          return false;
        }
      }
    }

    return {
      sendFlags,
      optSendFlags,
      fromAddr,
      toAddrList,
      bccAddrList,
    };
  },

  prepareSending(sendFlags, toAddrStr, gpgKeys, isOffline) {
    // perform confirmation dialog if necessary/requested
    if (
      sendFlags & EnigmailConstants.SEND_WITH_CHECK &&
      !this.messageSendCheck()
    ) {
      // Abort send
      if (!this.processed) {
        this.removeAttachedKey();
      }

      return false;
    }

    return true;
  },

  prepareSecurityInfo(
    sendFlags,
    uiFlags,
    rcpt,
    newSecurityInfo,
    autocryptGossipHeaders
  ) {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.prepareSecurityInfo(): Using PGP/MIME, flags=" +
        sendFlags +
        "\n"
    );

    let oldSecurityInfo = Enigmail.msg.getSecurityParams();

    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.prepareSecurityInfo: oldSecurityInfo = " +
        oldSecurityInfo +
        "\n"
    );

    if (!newSecurityInfo) {
      this.createEnigmailSecurityFields(Enigmail.msg.getSecurityParams());
      newSecurityInfo = Enigmail.msg.getSecurityParams().wrappedJSObject;
    }

    newSecurityInfo.originalSubject = gMsgCompose.compFields.subject;
    newSecurityInfo.originalReferences = gMsgCompose.compFields.references;

    if (sendFlags & EnigmailConstants.SEND_ENCRYPTED) {
      if (sendFlags & EnigmailConstants.ENCRYPT_SUBJECT) {
        gMsgCompose.compFields.subject = "";
      }

      if (Services.prefs.getBoolPref("temp.openpgp.protectReferencesHdr")) {
        gMsgCompose.compFields.references = "";
      }
    }

    newSecurityInfo.sendFlags = sendFlags;
    newSecurityInfo.UIFlags = uiFlags;
    newSecurityInfo.senderEmailAddr = rcpt.fromAddr;
    newSecurityInfo.bccRecipients = rcpt.bccAddrStr;
    newSecurityInfo.autocryptGossipHeaders = autocryptGossipHeaders;

    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.prepareSecurityInfo: securityInfo = " +
        newSecurityInfo +
        "\n"
    );
    return newSecurityInfo;
  },

  async prepareSendMsg(msgSendType) {
    // msgSendType: value from nsIMsgCompDeliverMode
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.prepareSendMsg: msgSendType=" +
        msgSendType +
        ", gSendSigned=" +
        gSendSigned +
        ", gSendEncrypted=" +
        gSendEncrypted +
        "\n"
    );

    const SIGN = EnigmailConstants.SEND_SIGNED;
    const ENCRYPT = EnigmailConstants.SEND_ENCRYPTED;
    const DeliverMode = Ci.nsIMsgCompDeliverMode;

    var ioService = Services.io;
    // EnigSend: Handle both plain and encrypted messages below
    var isOffline = ioService && ioService.offline;

    let senderKeyIsGnuPG =
      Services.prefs.getBoolPref("mail.openpgp.allow_external_gnupg") &&
      gCurrentIdentity.getBoolAttribute("is_gnupg_key_id");

    let sendFlags = this.getEncryptionFlags();

    switch (msgSendType) {
      case DeliverMode.SaveAsDraft:
      case DeliverMode.SaveAsTemplate:
      case DeliverMode.AutoSaveAsDraft:
        EnigmailLog.DEBUG(
          "enigmailMsgComposeOverlay.js: Enigmail.msg.prepareSendMsg: detected save draft\n"
        );

        // saving drafts is simpler and works differently than the rest of Enigmail.
        // All rules except account-settings are ignored.
        return this.saveDraftMessage(senderKeyIsGnuPG);
    }

    this.unsetAdditionalHeader("x-enigmail-draft-status");

    let msgCompFields = gMsgCompose.compFields;
    let newsgroups = msgCompFields.newsgroups; // Check if sending to any newsgroups

    if (
      msgCompFields.to === "" &&
      msgCompFields.cc === "" &&
      msgCompFields.bcc === "" &&
      newsgroups === ""
    ) {
      // don't attempt to send message if no recipient specified
      var bundle = document.getElementById("bundle_composeMsgs");
      EnigmailDialog.alert(window, bundle.getString("12511"));
      return false;
    }

    let senderKeyId = gCurrentIdentity.getUnicharAttribute("openpgp_key_id");

    if ((gSendEncrypted || gSendSigned) && !senderKeyId) {
      let msgId = gSendEncrypted
        ? "cannot-send-enc-because-no-own-key"
        : "cannot-send-sig-because-no-own-key";
      let fullAlert = await document.l10n.formatValue(msgId, {
        key: gCurrentIdentity.email,
      });
      EnigmailDialog.alert(window, fullAlert);
      return false;
    }

    if (senderKeyIsGnuPG) {
      sendFlags |= EnigmailConstants.SEND_SENDER_KEY_EXTERNAL;
    }

    if ((gSendEncrypted || gSendSigned) && senderKeyId) {
      let senderKeyUsable = await EnigmailEncryption.determineOwnKeyUsability(
        sendFlags,
        senderKeyId,
        senderKeyIsGnuPG
      );
      if (senderKeyUsable.errorMsg) {
        let fullAlert = await document.l10n.formatValue(
          "cannot-use-own-key-because",
          {
            problem: senderKeyUsable.errorMsg,
          }
        );
        EnigmailDialog.alert(window, fullAlert);
        return false;
      }
    }

    let cannotEncryptMissingInfo = false;
    if (gSendEncrypted) {
      let canEncryptDetails = await this.determineSendFlags();
      if (canEncryptDetails.errArray.length != 0) {
        cannotEncryptMissingInfo = true;
      }
    }

    if (gWindowLocked) {
      EnigmailDialog.alert(
        window,
        await document.l10n.formatValue("window-locked")
      );
      return false;
    }

    let newSecurityInfo = this.resetDirty();
    this.dirty = 1;

    try {
      this.modifiedAttach = null;

      // fill fromAddr, toAddrList, bcc etc
      let rcpt = await this.determineMsgRecipients(sendFlags);
      if (typeof rcpt === "boolean") {
        return rcpt;
      }
      sendFlags = rcpt.sendFlags;

      if (cannotEncryptMissingInfo) {
        showMessageComposeSecurityStatus(true);
        return false;
      }

      if (this.sendPgpMime) {
        // Use PGP/MIME
        sendFlags |= EnigmailConstants.SEND_PGP_MIME;
      }

      let toAddrStr = rcpt.toAddrList.join(", ");
      let bccAddrStr = rcpt.bccAddrList.join(", ");

      if (gAttachMyPublicPGPKey) {
        await this.attachOwnKey(senderKeyId);
      }

      let autocryptGossipHeaders = await this.getAutocryptGossip();

      /*
      if (this.preferPgpOverSmime(sendFlags) === 0) {
        // use S/MIME
        Attachments2CompFields(gMsgCompose.compFields); // update list of attachments
        sendFlags = 0;
        return true;
      }
      */

      var usingPGPMime =
        sendFlags & EnigmailConstants.SEND_PGP_MIME &&
        sendFlags & (ENCRYPT | SIGN);

      // ----------------------- Rewrapping code, taken from function "encryptInline"

      if (sendFlags & ENCRYPT && !usingPGPMime) {
        throw new Error("Sending encrypted inline not supported!");
      }
      if (sendFlags & SIGN && !usingPGPMime && gMsgCompose.composeHTML) {
        throw new Error(
          "Sending signed inline only supported for plain text composition!"
        );
      }

      // Check wrapping, if sign only and inline and plaintext
      if (
        sendFlags & SIGN &&
        !(sendFlags & ENCRYPT) &&
        !usingPGPMime &&
        !gMsgCompose.composeHTML
      ) {
        var wrapresultObj = {};

        await this.wrapInLine(wrapresultObj);

        if (wrapresultObj.usePpgMime) {
          sendFlags |= EnigmailConstants.SEND_PGP_MIME;
          usingPGPMime = EnigmailConstants.SEND_PGP_MIME;
        }
        if (wrapresultObj.cancelled) {
          return false;
        }
      }

      var uiFlags = EnigmailConstants.UI_INTERACTIVE;

      if (usingPGPMime) {
        uiFlags |= EnigmailConstants.UI_PGP_MIME;
      }

      if (sendFlags & (ENCRYPT | SIGN) && usingPGPMime) {
        // Use PGP/MIME
        newSecurityInfo = this.prepareSecurityInfo(
          sendFlags,
          uiFlags,
          rcpt,
          newSecurityInfo,
          autocryptGossipHeaders
        );
        newSecurityInfo.recipients = toAddrStr;
        newSecurityInfo.bccRecipients = bccAddrStr;
      } else if (!this.processed && sendFlags & (ENCRYPT | SIGN)) {
        // use inline PGP

        let sendInfo = {
          sendFlags,
          fromAddr: rcpt.fromAddr,
          toAddr: toAddrStr,
          bccAddr: bccAddrStr,
          uiFlags,
          bucketList: document.getElementById("attachmentBucket"),
        };

        if (!(await this.signInline(sendInfo))) {
          return false;
        }
      }

      // update the list of attachments
      Attachments2CompFields(msgCompFields);

      if (
        !this.prepareSending(
          sendFlags,
          rcpt.toAddrList.join(", "),
          toAddrStr + ", " + bccAddrStr,
          isOffline
        )
      ) {
        return false;
      }

      if (msgCompFields.characterSet != "ISO-2022-JP") {
        if (
          (usingPGPMime && sendFlags & (ENCRYPT | SIGN)) ||
          (!usingPGPMime && sendFlags & ENCRYPT)
        ) {
          try {
            // make sure plaintext is not changed to 7bit
            if (typeof msgCompFields.forceMsgEncoding == "boolean") {
              msgCompFields.forceMsgEncoding = true;
              EnigmailLog.DEBUG(
                "enigmailMsgComposeOverlay.js: Enigmail.msg.prepareSendMsg: enabled forceMsgEncoding\n"
              );
            }
          } catch (ex) {
            console.debug(ex);
          }
        }
      }
    } catch (ex) {
      EnigmailLog.writeException(
        "enigmailMsgComposeOverlay.js: Enigmail.msg.prepareSendMsg",
        ex
      );
      return false;
    }

    // The encryption process for PGP/MIME messages follows "here". It's
    // called automatically from nsMsgCompose->sendMsg().
    // registration for this is done in core.jsm: startup()

    return true;
  },

  async signInline(sendInfo) {
    // sign message using inline-PGP

    if (sendInfo.sendFlags & ENCRYPT) {
      throw new Error("Encryption not supported in inline messages!");
    }
    if (gMsgCompose.composeHTML) {
      throw new Error(
        "Signing inline only supported for plain text composition!"
      );
    }

    const dce = Ci.nsIDocumentEncoder;
    const SIGN = EnigmailConstants.SEND_SIGNED;
    const ENCRYPT = EnigmailConstants.SEND_ENCRYPTED;

    var enigmailSvc = EnigmailCore.getService(window);
    if (!enigmailSvc) {
      return false;
    }

    if (Services.prefs.getBoolPref("mail.strictly_mime")) {
      if (
        EnigmailDialog.confirmIntPref(
          window,
          await l10nOpenPGP.formatValue("quoted-printable-warn"),
          "temp.openpgp.quotedPrintableWarn"
        )
      ) {
        Services.prefs.setBoolPref("mail.strictly_mime", false);
      }
    }

    var sendFlowed = Services.prefs.getBoolPref(
      "mailnews.send_plaintext_flowed"
    );
    var encoderFlags = dce.OutputFormatted | dce.OutputLFLineBreak;

    // plaintext: Wrapping code has been moved to superordinate function prepareSendMsg to enable interactive format switch

    var exitCodeObj = {};
    var statusFlagsObj = {};
    var errorMsgObj = {};
    var exitCode;

    // Get plain text
    // (Do we need to set the nsIDocumentEncoder.* flags?)
    var origText = this.editorGetContentAs("text/plain", encoderFlags);
    if (!origText) {
      origText = "";
    }

    if (origText.length > 0) {
      // Sign/encrypt body text

      var escText = origText; // Copy plain text for possible escaping

      if (sendFlowed) {
        // Prevent space stuffing a la RFC 2646 (format=flowed).

        //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: escText["+encoderFlags+"] = '"+escText+"'\n");

        escText = escText.replace(/^From /gm, "~From ");
        escText = escText.replace(/^>/gm, "|");
        escText = escText.replace(/^[ \t]+$/gm, "");
        escText = escText.replace(/^ /gm, "~ ");

        //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: escText = '"+escText+"'\n");
        // Replace plain text and get it again
        this.replaceEditorText(escText);

        escText = this.editorGetContentAs("text/plain", encoderFlags);
      }

      // Replace plain text and get it again (to avoid linewrapping problems)
      this.replaceEditorText(escText);

      escText = this.editorGetContentAs("text/plain", encoderFlags);

      //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: escText["+encoderFlags+"] = '"+escText+"'\n");

      var charset = this.editorGetCharset();
      EnigmailLog.DEBUG(
        "enigmailMsgComposeOverlay.js: Enigmail.msg.signInline: charset=" +
          charset +
          "\n"
      );

      // Encode plaintext to charset from unicode
      var plainText = EnigmailData.convertFromUnicode(escText, charset);

      // this will sign, not encrypt
      var cipherText = EnigmailEncryption.encryptMessage(
        window,
        sendInfo.uiFlags,
        plainText,
        sendInfo.fromAddr,
        sendInfo.toAddr,
        sendInfo.bccAddr,
        sendInfo.sendFlags,
        exitCodeObj,
        statusFlagsObj,
        errorMsgObj
      );

      exitCode = exitCodeObj.value;

      //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: cipherText = '"+cipherText+"'\n");
      if (cipherText && exitCode === 0) {
        // Encryption/signing succeeded; overwrite plaintext

        cipherText = cipherText.replace(/\r\n/g, "\n");

        // Decode ciphertext from charset to unicode and overwrite
        this.replaceEditorText(
          EnigmailData.convertToUnicode(cipherText, charset)
        );

        // Save original text (for undo)
        this.processed = {
          origText,
          charset,
        };
      } else {
        // Restore original text
        this.replaceEditorText(origText);

        if (sendInfo.sendFlags & SIGN) {
          // Encryption/signing failed

          this.sendAborted(window, errorMsgObj);
          return false;
        }
      }
    }

    return true;
  },

  async sendAborted(window, errorMsgObj) {
    if (errorMsgObj && errorMsgObj.value) {
      var txt = errorMsgObj.value;
      var txtLines = txt.split(/\r?\n/);
      var errorMsg = "";
      for (var i = 0; i < txtLines.length; ++i) {
        var line = txtLines[i];
        var tokens = line.split(/ /);
        // process most important business reasons for invalid recipient (and sender) errors:
        if (
          tokens.length == 3 &&
          (tokens[0] == "INV_RECP" || tokens[0] == "INV_SGNR")
        ) {
          var reason = tokens[1];
          var key = tokens[2];
          if (reason == "10") {
            errorMsg +=
              (await document.l10n.formatValue("key-not-trusted", { key })) +
              "\n";
          } else if (reason == "1") {
            errorMsg +=
              (await document.l10n.formatValue("key-not-found", { key })) +
              "\n";
          } else if (reason == "4") {
            errorMsg +=
              (await document.l10n.formatValue("key-revoked", { key })) + "\n";
          } else if (reason == "5") {
            errorMsg +=
              (await document.l10n.formatValue("key-expired", { key })) + "\n";
          }
        }
      }
      if (errorMsg !== "") {
        txt = errorMsg + "\n" + txt;
      }
      EnigmailDialog.info(
        window,
        (await document.l10n.formatValue("send-aborted")) + "\n" + txt
      );
    } else {
      let [title, message] = await document.l10n.formatValues([
        { id: "send-aborted" },
        { id: "msg-compose-internal-error" },
      ]);
      EnigmailDialog.info(window, title + "\n" + message);
    }
  },

  messageSendCheck() {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.messageSendCheck\n"
    );

    try {
      var warn = Services.prefs.getBoolPref("mail.warn_on_send_accel_key");

      if (warn) {
        var checkValue = {
          value: false,
        };
        var bundle = document.getElementById("bundle_composeMsgs");
        var buttonPressed = EnigmailDialog.getPromptSvc().confirmEx(
          window,
          bundle.getString("sendMessageCheckWindowTitle"),
          bundle.getString("sendMessageCheckLabel"),
          EnigmailDialog.getPromptSvc().BUTTON_TITLE_IS_STRING *
            EnigmailDialog.getPromptSvc().BUTTON_POS_0 +
            EnigmailDialog.getPromptSvc().BUTTON_TITLE_CANCEL *
              EnigmailDialog.getPromptSvc().BUTTON_POS_1,
          bundle.getString("sendMessageCheckSendButtonLabel"),
          null,
          null,
          bundle.getString("CheckMsg"),
          checkValue
        );
        if (buttonPressed !== 0) {
          return false;
        }
        if (checkValue.value) {
          Services.prefs.setBoolPref("mail.warn_on_send_accel_key", false);
        }
      }
    } catch (ex) {}

    return true;
  },

  /**
   * set non-standard message Header
   * (depending on TB version)
   *
   * hdr: String: header type (e.g. X-Enigmail-Version)
   * val: String: header data (e.g. 1.2.3.4)
   */
  setAdditionalHeader(hdr, val) {
    if ("otherRandomHeaders" in gMsgCompose.compFields) {
      // TB <= 36
      gMsgCompose.compFields.otherRandomHeaders += hdr + ": " + val + "\r\n";
    } else {
      gMsgCompose.compFields.setHeader(hdr, val);
    }
  },

  unsetAdditionalHeader(hdr) {
    gMsgCompose.compFields.deleteHeader(hdr);
  },

  // called just before sending
  modifyCompFields() {
    try {
      if (
        !Enigmail.msg.isEnigmailEnabledForIdentity() ||
        !gCurrentIdentity.sendAutocryptHeaders
      ) {
        return;
      }
      if ((gSendSigned || gSendEncrypted) && !gSelectedTechnologyIsPGP) {
        // If we're sending an S/MIME message, we don't want to send
        // the OpenPGP autocrypt header.
        return;
      }
      this.setAutocryptHeader();
    } catch (ex) {
      EnigmailLog.writeException(
        "enigmailMsgComposeOverlay.js: Enigmail.msg.modifyCompFields",
        ex
      );
    }
  },

  getCurrentIncomingServer() {
    let currentAccountKey = getCurrentAccountKey();
    let account = MailServices.accounts.getAccount(currentAccountKey);

    return account.incomingServer; /* returns nsIMsgIncomingServer */
  },

  /**
   * Obtain all Autocrypt-Gossip header lines that should be included in
   * the outgoing message, excluding the sender's (from) email address.
   * If there is just one recipient (ignoring the from address),
   * no headers will be returned.
   *
   * @returns {string} - All header lines including line endings,
   *                     could be the empty string.
   */
  async getAutocryptGossip() {
    let fromMail = EnigmailFuncs.stripEmail(gMsgCompose.compFields.from);
    let replyToMail = EnigmailFuncs.stripEmail(gMsgCompose.compFields.replyTo);

    let optionalReplyToGossip = "";
    if (replyToMail != fromMail) {
      optionalReplyToGossip = ", " + gMsgCompose.compFields.replyTo;
    }

    // Assumes that extractHeaderAddressMailboxes will separate all
    // entries with the sequence comma-space.
    let allEmails = MailServices.headerParser
      .extractHeaderAddressMailboxes(
        gMsgCompose.compFields.to +
          ", " +
          gMsgCompose.compFields.cc +
          optionalReplyToGossip
      )
      .split(/, /);

    // Use a Set to ensure we have each address only once.
    let uniqueEmails = new Set();
    for (let e of allEmails) {
      uniqueEmails.add(e);
    }

    // Potentially to/cc might contain the sender email address.
    // Remove it, if it's there.
    uniqueEmails.delete(fromMail);

    // When sending to yourself, only, allEmails.length is 0.
    // When sending to exactly one other person (with or without
    // "from" in to/cc), then allEmails.length is 1. In that scenario,
    // that recipient obviously already has their own key, and doesn't
    // need the gossip. The sender's key will be included in the
    // separate autocrypt (non-gossip) header.

    if (uniqueEmails.size < 2) {
      return "";
    }

    let gossip = "";
    for (const email of uniqueEmails) {
      let k = await EnigmailKeyRing.getRecipientAutocryptKeyForEmail(email);
      if (!k) {
        continue;
      }
      let keyData =
        " " + k.replace(/(.{72})/g, "$1\r\n ").replace(/\r\n $/, "");
      gossip +=
        "Autocrypt-Gossip: addr=" + email + "; keydata=\r\n" + keyData + "\r\n";
    }

    return gossip;
  },

  setAutocryptHeader() {
    let senderKeyId = gCurrentIdentity.getUnicharAttribute("openpgp_key_id");
    if (!senderKeyId) {
      return;
    }

    let fromMail = gCurrentIdentity.email;
    try {
      fromMail = EnigmailFuncs.stripEmail(gMsgCompose.compFields.from);
    } catch (ex) {}

    let keyData = EnigmailKeyRing.getAutocryptKey("0x" + senderKeyId, fromMail);

    if (keyData) {
      keyData =
        " " + keyData.replace(/(.{72})/g, "$1\r\n ").replace(/\r\n $/, "");
      this.setAdditionalHeader(
        "Autocrypt",
        "addr=" + fromMail + "; keydata=\r\n" + keyData
      );
    }
  },

  /**
   * Handle the 'compose-send-message' event from TB
   */
  sendMessageListener(event) {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.sendMessageListener\n"
    );

    let msgcomposeWindow = document.getElementById("msgcomposeWindow");
    let sendMsgType = Number(msgcomposeWindow.getAttribute("msgtype"));

    if (
      !(
        this.sendProcess &&
        sendMsgType == Ci.nsIMsgCompDeliverMode.AutoSaveAsDraft
      )
    ) {
      this.modifyCompFields();
      if (!gSelectedTechnologyIsPGP) {
        return;
      }

      this.sendProcess = true;
      //let bc = document.getElementById("enigmail-bc-sendprocess");

      try {
        const cApi = EnigmailCryptoAPI();
        let encryptResult = cApi.sync(this.prepareSendMsg(sendMsgType));
        if (!encryptResult) {
          this.resetUpdatedFields();
          event.preventDefault();
          event.stopPropagation();
        }
      } catch (ex) {
        console.error("GenericSendMessage FAILED: " + ex);
        this.resetUpdatedFields();
        event.preventDefault();
        event.stopPropagation();
      }
    } else {
      EnigmailLog.DEBUG(
        "enigmailMsgComposeOverlay.js: Enigmail.msg.sendMessageListener: sending in progress - autosave aborted\n"
      );
      event.preventDefault();
      event.stopPropagation();
    }
    this.sendProcess = false;
  },

  async decryptQuote(interactive) {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.decryptQuote: " +
        interactive +
        "\n"
    );

    if (gWindowLocked || this.processed) {
      return;
    }

    var enigmailSvc = EnigmailCore.getService(window);
    if (!enigmailSvc) {
      return;
    }

    const dce = Ci.nsIDocumentEncoder;
    var encoderFlags = dce.OutputFormatted | dce.OutputLFLineBreak;

    var docText = this.editorGetContentAs("text/plain", encoderFlags);

    var blockBegin = docText.indexOf("-----BEGIN PGP ");
    if (blockBegin < 0) {
      return;
    }

    // Determine indentation string
    var indentBegin = docText.substr(0, blockBegin).lastIndexOf("\n");
    var indentStr = docText.substring(indentBegin + 1, blockBegin);

    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.decryptQuote: indentStr='" +
        indentStr +
        "'\n"
    );

    var beginIndexObj = {};
    var endIndexObj = {};
    var indentStrObj = {};
    var blockType = EnigmailArmor.locateArmoredBlock(
      docText,
      0,
      indentStr,
      beginIndexObj,
      endIndexObj,
      indentStrObj
    );
    if (blockType != "MESSAGE" && blockType != "SIGNED MESSAGE") {
      return;
    }

    var beginIndex = beginIndexObj.value;
    var endIndex = endIndexObj.value;

    var head = docText.substr(0, beginIndex);
    var tail = docText.substr(endIndex + 1);

    var pgpBlock = docText.substr(beginIndex, endIndex - beginIndex + 1);
    var indentRegexp;

    if (indentStr) {
      if (indentStr == "> ") {
        // replace ">> " with "> > " to allow correct quoting
        pgpBlock = pgpBlock.replace(/^>>/gm, "> >");
      }

      // Escape regex chars.
      let escapedIndent1 = indentStr.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");

      // Delete indentation
      indentRegexp = new RegExp("^" + escapedIndent1, "gm");

      pgpBlock = pgpBlock.replace(indentRegexp, "");
      //tail     =     tail.replace(indentRegexp, "");

      if (indentStr.match(/[ \t]*$/)) {
        indentStr = indentStr.replace(/[ \t]*$/gm, "");
        // Escape regex chars.
        let escapedIndent2 = indentStr.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");
        indentRegexp = new RegExp("^" + escapedIndent2 + "$", "gm");

        pgpBlock = pgpBlock.replace(indentRegexp, "");
      }

      // Handle blank indented lines
      pgpBlock = pgpBlock.replace(/^[ \t]*>[ \t]*$/gm, "");
      //tail     =     tail.replace(/^[ \t]*>[ \t]*$/g, "");

      // Trim leading space in tail
      tail = tail.replace(/^\s*\n/m, "\n");
    }

    if (tail.search(/\S/) < 0) {
      // No non-space characters in tail; delete it
      tail = "";
    }

    //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.decryptQuote: pgpBlock='"+pgpBlock+"'\n");

    var charset = this.editorGetCharset();
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.decryptQuote: charset=" +
        charset +
        "\n"
    );

    // Encode ciphertext from unicode to charset
    var cipherText = EnigmailData.convertFromUnicode(pgpBlock, charset);

    // Decrypt message
    var signatureObj = {};
    signatureObj.value = "";
    var exitCodeObj = {};
    var statusFlagsObj = {};
    var userIdObj = {};
    var keyIdObj = {};
    var sigDetailsObj = {};
    var errorMsgObj = {};
    var blockSeparationObj = {};
    var encToDetailsObj = {};

    var uiFlags = EnigmailConstants.UI_UNVERIFIED_ENC_OK;

    var plainText = "";

    plainText = EnigmailDecryption.decryptMessage(
      window,
      uiFlags,
      cipherText,
      null, // date
      signatureObj,
      exitCodeObj,
      statusFlagsObj,
      keyIdObj,
      userIdObj,
      sigDetailsObj,
      errorMsgObj,
      blockSeparationObj,
      encToDetailsObj
    );
    // Decode plaintext from charset to unicode
    plainText = EnigmailData.convertToUnicode(plainText, charset).replace(
      /\r\n/g,
      "\n"
    );

    //if (Services.prefs.getBoolPref("temp.openpgp.keepSettingsForReply")) {
    if (statusFlagsObj.value & EnigmailConstants.DECRYPTION_OKAY) {
      //this.setSendMode('encrypt');

      // TODO : Check, when is this code reached?
      // automatic enabling encryption currently depends on
      // adjustSignEncryptAfterIdentityChanged to be always reached
      gIsRelatedToEncryptedOriginal = true;
      gSendEncrypted = true;
      updateEncryptionDependencies();
    }
    //}

    var exitCode = exitCodeObj.value;

    if (exitCode !== 0) {
      // Error processing
      var errorMsg = errorMsgObj.value;

      var statusLines = errorMsg ? errorMsg.split(/\r?\n/) : [];

      var displayMsg;
      if (statusLines && statusLines.length) {
        // Display only first ten lines of error message
        while (statusLines.length > 10) {
          statusLines.pop();
        }

        displayMsg = statusLines.join("\n");

        if (interactive) {
          EnigmailDialog.info(window, displayMsg);
        }
      }
    }

    if (blockType == "MESSAGE" && exitCode === 0 && plainText.length === 0) {
      plainText = " ";
    }

    if (!plainText) {
      if (blockType != "SIGNED MESSAGE") {
        return;
      }

      // Extract text portion of clearsign block
      plainText = EnigmailArmor.extractSignaturePart(
        pgpBlock,
        EnigmailConstants.SIGNATURE_TEXT
      );
    }

    const nsIMsgCompType = Ci.nsIMsgCompType;
    var doubleDashSeparator = Services.prefs.getBoolPref(
      "temp.openpgp.doubleDashSeparator"
    );
    if (
      gMsgCompose.type != nsIMsgCompType.Template &&
      gMsgCompose.type != nsIMsgCompType.Draft &&
      doubleDashSeparator
    ) {
      var signOffset = plainText.search(/[\r\n]-- +[\r\n]/);

      if (signOffset < 0 && blockType == "SIGNED MESSAGE") {
        signOffset = plainText.search(/[\r\n]--[\r\n]/);
      }

      if (signOffset > 0) {
        // Strip signature portion of quoted message
        plainText = plainText.substr(0, signOffset + 1);
      }
    }

    this.editorSelectAll();

    //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.decryptQuote: plainText='"+plainText+"'\n");

    if (head) {
      this.editorInsertText(head);
    }

    var quoteElement;

    if (indentStr) {
      quoteElement = this.editorInsertAsQuotation(plainText);
    } else {
      this.editorInsertText(plainText);
    }

    if (tail) {
      this.editorInsertText(tail);
    }

    if (statusFlagsObj.value & EnigmailConstants.DECRYPTION_OKAY) {
      this.checkInlinePgpReply(head, tail);
    }

    if (interactive) {
      return;
    }

    // Position cursor
    var replyOnTop = gCurrentIdentity.replyOnTop;

    if (!indentStr || !quoteElement) {
      replyOnTop = 1;
    }

    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.decryptQuote: replyOnTop=" +
        replyOnTop +
        ", quoteElement=" +
        quoteElement +
        "\n"
    );

    if (this.editor.selectionController) {
      var selection = this.editor.selectionController;
      selection.completeMove(false, false); // go to start;

      switch (replyOnTop) {
        case 0:
          // Position after quote
          this.editor.endOfDocument();
          if (tail) {
            for (let cPos = 0; cPos < tail.length; cPos++) {
              selection.characterMove(false, false); // move backwards
            }
          }
          break;

        case 2:
          // Select quote

          if (head) {
            for (let cPos = 0; cPos < head.length; cPos++) {
              selection.characterMove(true, false);
            }
          }
          selection.completeMove(true, true);
          if (tail) {
            for (let cPos = 0; cPos < tail.length; cPos++) {
              selection.characterMove(false, true); // move backwards
            }
          }
          break;

        default:
          // Position at beginning of document

          if (this.editor) {
            this.editor.beginningOfDocument();
          }
      }

      this.editor.selectionController.scrollSelectionIntoView(
        Ci.nsISelectionController.SELECTION_NORMAL,
        Ci.nsISelectionController.SELECTION_ANCHOR_REGION,
        true
      );
    }

    //this.processFinalState();
  },

  checkInlinePgpReply(head, tail) {
    const CT = Ci.nsIMsgCompType;
    let hLines = head.search(/[^\s>]/) < 0 ? 0 : 1;

    if (hLines > 0) {
      switch (gMsgCompose.type) {
        case CT.Reply:
        case CT.ReplyAll:
        case CT.ReplyToSender:
        case CT.ReplyToGroup:
        case CT.ReplyToSenderAndGroup:
        case CT.ReplyToList: {
          // if head contains at only a few line of text, we assume it's the
          // header above the quote (e.g. XYZ wrote:) and the user's signature

          let h = head.split(/\r?\n/);
          hLines = -1;

          for (let i = 0; i < h.length; i++) {
            if (h[i].search(/[^\s>]/) >= 0) {
              hLines++;
            }
          }
        }
      }
    }

    if (
      hLines > 0 &&
      (!gCurrentIdentity.sigOnReply || gCurrentIdentity.sigBottom)
    ) {
      // display warning if no signature on top of message
      this.displayPartialEncryptedWarning();
    } else if (hLines > 10) {
      this.displayPartialEncryptedWarning();
    } else if (
      tail.search(/[^\s>]/) >= 0 &&
      !(gCurrentIdentity.sigOnReply && gCurrentIdentity.sigBottom)
    ) {
      // display warning if no signature below message
      this.displayPartialEncryptedWarning();
    }
  },

  editorInsertText(plainText) {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.editorInsertText\n"
    );
    if (this.editor) {
      var mailEditor;
      try {
        mailEditor = this.editor.QueryInterface(Ci.nsIEditorMailSupport);
        mailEditor.insertTextWithQuotations(plainText);
      } catch (ex) {
        EnigmailLog.DEBUG(
          "enigmailMsgComposeOverlay.js: Enigmail.msg.editorInsertText: no mail editor\n"
        );
        this.editor.insertText(plainText);
      }
    }
  },

  editorInsertAsQuotation(plainText) {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.editorInsertAsQuotation\n"
    );
    if (this.editor) {
      var mailEditor;
      try {
        mailEditor = this.editor.QueryInterface(Ci.nsIEditorMailSupport);
      } catch (ex) {}

      if (!mailEditor) {
        return 0;
      }

      EnigmailLog.DEBUG(
        "enigmailMsgComposeOverlay.js: Enigmail.msg.editorInsertAsQuotation: mailEditor=" +
          mailEditor +
          "\n"
      );

      mailEditor.insertAsCitedQuotation(plainText, "", false);

      return 1;
    }
    return 0;
  },

  isSenderKeyExpired() {
    const senderKeyId = this.getSenderUserId();

    if (senderKeyId) {
      const key = EnigmailKeyRing.getKeyById(senderKeyId);
      return key?.expiryTime && Math.round(Date.now() / 1000) > key.expiryTime;
    }

    return false;
  },

  removeNotificationIfPresent(name) {
    const notif = gComposeNotification.getNotificationWithValue(name);
    if (notif) {
      gComposeNotification.removeNotification(notif);
    }
  },

  warnUserThatSenderKeyExpired() {
    const label = {
      "l10n-id": "openpgp-selection-status-error",
      "l10n-args": { key: this.getSenderUserId() },
    };

    const buttons = [
      {
        "l10n-id": "settings-context-open-account-settings-item2",
        callback() {
          MsgAccountManager(
            "am-e2e.xhtml",
            MailServices.accounts.getServersForIdentity(gCurrentIdentity)[0]
          );
          Services.wm.getMostRecentWindow("mail:3pane")?.focus();
          return true;
        },
      },
    ];

    gComposeNotification.appendNotification(
      "openpgpSenderKeyExpired",
      {
        label,
        priority: gComposeNotification.PRIORITY_WARNING_MEDIUM,
      },
      buttons
    );
  },

  warnUserIfSenderKeyExpired() {
    if (!this.isSenderKeyExpired()) {
      this.removeNotificationIfPresent("openpgpSenderKeyExpired");
      return;
    }

    this.warnUserThatSenderKeyExpired();
  },

  /**
   * Display a notification to the user at the bottom of the window
   *
   * @param priority: Number - Priority of the message [1 = high (error) ... 3 = low (info)]
   * @param msgText: String - Text to be displayed in notification bar
   * @param messageId: String - Unique message type identification
   * @param detailsText: String - optional text to be displayed by clicking on "Details" button.
   *                              if null or "", then the Detail button will no be displayed.
   */
  async notifyUser(priority, msgText, messageId, detailsText) {
    let prio;

    switch (priority) {
      case 1:
        prio = gComposeNotification.PRIORITY_CRITICAL_MEDIUM;
        break;
      case 3:
        prio = gComposeNotification.PRIORITY_INFO_MEDIUM;
        break;
      default:
        prio = gComposeNotification.PRIORITY_WARNING_MEDIUM;
    }

    let buttonArr = [];

    if (detailsText && detailsText.length > 0) {
      let [accessKey, label] = await document.l10n.formatValues([
        { id: "msg-compose-details-button-access-key" },
        { id: "msg-compose-details-button-label" },
      ]);

      buttonArr.push({
        accessKey,
        label,
        callback(aNotificationBar, aButton) {
          EnigmailDialog.info(window, detailsText);
        },
      });
    }
    gComposeNotification.appendNotification(
      messageId,
      {
        label: msgText,
        priority: prio,
      },
      buttonArr
    );
  },

  /**
   * Display a warning message if we are replying to or forwarding
   * a partially decrypted inline-PGP email
   */
  async displayPartialEncryptedWarning() {
    let [msgLong, msgShort] = await document.l10n.formatValues([
      { id: "msg-compose-partially-encrypted-inlinePGP" },
      { id: "msg-compose-partially-encrypted-short" },
    ]);

    this.notifyUser(1, msgShort, "notifyPartialDecrypt", msgLong);
  },

  editorSelectAll() {
    if (this.editor) {
      this.editor.selectAll();
    }
  },

  editorGetCharset() {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.editorGetCharset\n"
    );
    return this.editor.documentCharacterSet;
  },

  editorGetContentAs(mimeType, flags) {
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: Enigmail.msg.editorGetContentAs\n"
    );
    if (this.editor) {
      return this.editor.outputToString(mimeType, flags);
    }

    return null;
  },

  async focusChange() {
    // call original TB function
    CommandUpdate_MsgCompose();

    var focusedWindow = top.document.commandDispatcher.focusedWindow;

    // we're just setting focus to where it was before
    if (focusedWindow == Enigmail.msg.lastFocusedWindow) {
      // skip
      return;
    }

    Enigmail.msg.lastFocusedWindow = focusedWindow;
  },

  /**
   * Merge multiple  Re: Re: into one Re: in message subject
   */
  fixMessageSubject() {
    let subjElem = document.getElementById("msgSubject");
    if (subjElem) {
      let r = subjElem.value.replace(/^(Re: )+/, "Re: ");
      if (r !== subjElem.value) {
        subjElem.value = r;
        if (typeof subjElem.oninput === "function") {
          subjElem.oninput();
        }
      }
    }
  },
};

Enigmail.composeStateListener = {
  NotifyComposeFieldsReady() {
    // Note: NotifyComposeFieldsReady is only called when a new window is created (i.e. not in case a window object is reused).
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: ECSL.NotifyComposeFieldsReady\n"
    );

    try {
      Enigmail.msg.editor = gMsgCompose.editor.QueryInterface(Ci.nsIEditor);
    } catch (ex) {}

    if (!Enigmail.msg.editor) {
      return;
    }

    Enigmail.msg.fixMessageSubject();

    function enigDocStateListener() {}

    enigDocStateListener.prototype = {
      QueryInterface: ChromeUtils.generateQI(["nsIDocumentStateListener"]),

      NotifyDocumentWillBeDestroyed() {
        EnigmailLog.DEBUG(
          "enigmailMsgComposeOverlay.js: EDSL.enigDocStateListener.NotifyDocumentWillBeDestroyed\n"
        );
      },

      NotifyDocumentStateChanged(nowDirty) {
        EnigmailLog.DEBUG(
          "enigmailMsgComposeOverlay.js: EDSL.enigDocStateListener.NotifyDocumentStateChanged\n"
        );
      },
    };

    var docStateListener = new enigDocStateListener();

    Enigmail.msg.editor.addDocumentStateListener(docStateListener);
  },

  ComposeProcessDone(aResult) {
    // Note: called after a mail was sent (or saved)
    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: ECSL.ComposeProcessDone: " + aResult + "\n"
    );

    if (aResult != Cr.NS_OK) {
      Enigmail.msg.removeAttachedKey();
    }

    // ensure that securityInfo is set back to S/MIME flags (especially required if draft was saved)
    if (gSMFields) {
      Enigmail.msg.setSecurityParams(gSMFields);
    }
  },

  NotifyComposeBodyReady() {
    EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: ECSL.ComposeBodyReady\n");

    var isEmpty, isEditable;

    isEmpty = Enigmail.msg.editor.documentIsEmpty;
    isEditable = Enigmail.msg.editor.isDocumentEditable;
    Enigmail.msg.composeBodyReady = true;

    EnigmailLog.DEBUG(
      "enigmailMsgComposeOverlay.js: ECSL.ComposeBodyReady: isEmpty=" +
        isEmpty +
        ", isEditable=" +
        isEditable +
        "\n"
    );

    /*
    if (Enigmail.msg.disableSmime) {
      if (gMsgCompose && gMsgCompose.compFields && Enigmail.msg.getSecurityParams()) {
        let si = Enigmail.msg.getSecurityParams(null);
        si.signMessage = false;
        si.requireEncryptMessage = false;
      }
      else {
        EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: ECSL.ComposeBodyReady: could not disable S/MIME\n");
      }
    }
    */

    if (isEditable && !isEmpty) {
      if (!Enigmail.msg.timeoutId && !Enigmail.msg.dirty) {
        Enigmail.msg.timeoutId = setTimeout(function () {
          Enigmail.msg.decryptQuote(false);
        }, 0);
      }
    }

    // This must be called by the last registered NotifyComposeBodyReady()
    // stateListener. We need this in order to know when the entire init
    // sequence of the composeWindow has finished, so the WebExtension compose
    // API can do its final modifications.
    window.composeEditorReady = true;
    window.dispatchEvent(new CustomEvent("compose-editor-ready"));
  },

  SaveInFolderDone(folderURI) {
    //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: ECSL.SaveInFolderDone\n");
  },
};

window.addEventListener(
  "load",
  Enigmail.msg.composeStartup.bind(Enigmail.msg),
  {
    capture: false,
    once: true,
  }
);

window.addEventListener("compose-window-unload", () => {
  if (gMsgCompose) {
    gMsgCompose.UnregisterStateListener(Enigmail.composeStateListener);
  }
});
