/* -*- Mode: JavaScript; 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/. */

/* import-globals-from ../../../../mailnews/extensions/smime/msgReadSMIMEOverlay.js */
/* import-globals-from ../../../base/content/aboutMessage.js */
/* import-globals-from ../../../base/content/msgHdrView.js */
/* import-globals-from ../../../base/content/msgSecurityPane.js */

// mailCommon.js
/* globals gEncryptedURIService */

var gMyLastEncryptedURI = null;

var gSMIMEBundle = null;

var gSignatureStatusForURI = null;
var gEncryptionStatusForURI = null;

// Get the necko URL for the message URI.
function neckoURLForMessageURI(aMessageURI) {
  let msgSvc = MailServices.messageServiceFromURI(aMessageURI);
  let neckoURI = msgSvc.getUrlForUri(aMessageURI);
  return neckoURI.spec;
}

var gIgnoreStatusFromMimePart = null;

function setIgnoreStatusFromMimePart(mimePart) {
  gIgnoreStatusFromMimePart = mimePart;
}

/**
 * Set the cryptoBox content according to the given encryption states of the
 * displayed message. null should be passed as a state if the message does not
 * encrypted or is not signed.
 *
 * @param {string|null} tech - The name for the encryption technology in use
 *   for the message.
 * @param {"ok"|"notok"|null} encryptedState - The encrypted state of the
 *   message.
 * @param {"ok"|"notok"|"verified"|"unverified"|"unknown"|"mismatch"|null}
 *   signedState - The signed state of the message.
 * @param {boolean} forceShow - Show the box if unsigned and unencrypted.
 * @param {string} mimePartNumber - Should be set to the MIME part number
 *   that triggers this status update. If the value matches a currently
 *   ignored MIME part, then this function call will be ignored.
 */
function setMessageCryptoBox(
  tech,
  encryptedState,
  signedState,
  forceShow,
  mimePartNumber
) {
  if (
    !!gIgnoreStatusFromMimePart &&
    mimePartNumber == gIgnoreStatusFromMimePart
  ) {
    return;
  }

  let container = document.getElementById("cryptoBox");
  let encryptedIcon = document.getElementById("encryptedHdrIcon");
  let signedIcon = document.getElementById("signedHdrIcon");
  let button = document.getElementById("encryptionTechBtn");
  let buttonText = button.querySelector(".crypto-label");

  let hidden = !forceShow && (!tech || (!encryptedState && !signedState));
  container.hidden = hidden;
  button.hidden = hidden;
  if (hidden) {
    container.removeAttribute("tech");
    buttonText.textContent = "";
  } else {
    container.setAttribute("tech", tech);
    buttonText.textContent = tech;
  }

  if (encryptedState) {
    encryptedIcon.hidden = false;
    encryptedIcon.setAttribute(
      "src",
      `chrome://messenger/skin/icons/message-encrypted-${encryptedState}.svg`
    );
    // Set alt text.
    document.l10n.setAttributes(
      encryptedIcon,
      `openpgp-message-header-encrypted-${encryptedState}-icon`
    );
  } else {
    encryptedIcon.hidden = true;
    encryptedIcon.removeAttribute("data-l10n-id");
    encryptedIcon.removeAttribute("alt");
    encryptedIcon.removeAttribute("src");
  }

  if (signedState) {
    if (signedState === "notok") {
      // Show the same as mismatch.
      signedState = "mismatch";
    }
    signedIcon.hidden = false;
    signedIcon.setAttribute(
      "src",
      `chrome://messenger/skin/icons/message-signed-${signedState}.svg`
    );
    // Set alt text.
    document.l10n.setAttributes(
      signedIcon,
      `openpgp-message-header-signed-${signedState}-icon`
    );
  } else {
    signedIcon.hidden = true;
    signedIcon.removeAttribute("data-l10n-id");
    signedIcon.removeAttribute("alt");
    signedIcon.removeAttribute("src");
  }
}

function smimeSignedStateToString(signedState) {
  switch (signedState) {
    case -1:
      return null;
    case Ci.nsICMSMessageErrors.SUCCESS:
      return "ok";
    case Ci.nsICMSMessageErrors.VERIFY_NOT_YET_ATTEMPTED:
      return "unknown";
    case Ci.nsICMSMessageErrors.VERIFY_CERT_WITHOUT_ADDRESS:
    case Ci.nsICMSMessageErrors.VERIFY_HEADER_MISMATCH:
      return "mismatch";
    default:
      return "notok";
  }
}

function smimeEncryptedStateToString(encryptedState) {
  switch (encryptedState) {
    case -1:
      return null;
    case Ci.nsICMSMessageErrors.SUCCESS:
      return "ok";
    default:
      return "notok";
  }
}

/**
 * Refresh the cryptoBox content using the global gEncryptionStatus and
 * gSignatureStatus variables.
 *
 * @param {string} mimePartNumber - Should be set to the MIME part number
 *   that triggers this status update.
 */
function refreshSmimeMessageEncryptionStatus(mimePartNumber = undefined) {
  let signed = smimeSignedStateToString(gSignatureStatus);
  let encrypted = smimeEncryptedStateToString(gEncryptionStatus);
  setMessageCryptoBox("S/MIME", encrypted, signed, false, mimePartNumber);
}

var smimeHeaderSink = {
  /**
   * @returns the URI of the selected message, or null if the current
   *         message displayed isn't in a folder, for example if the
   *         message is displayed in a separate window.
   */
  getSelectedMessageURI() {
    if (!gMessage) {
      return null;
    }
    if (!gFolder) {
      // The folder should be absent only if the message gets opened
      // from an external file (.eml), which is opened in its own window.
      // That window won't get reused for other messages. We conclude
      // the incoming status is for this window.
      // This special handling is necessary, because the necko URL for
      // separate windows that is seen by the MIME code differs from the
      // one we see here in JS.
      return null;
    }

    return neckoURLForMessageURI(gMessageURI);
  },

  signedStatus(
    aNestingLevel,
    aSignatureStatus,
    aSignerCert,
    aMsgNeckoURL,
    aOriginMimePartNumber
  ) {
    if (
      !!gIgnoreStatusFromMimePart &&
      aOriginMimePartNumber == gIgnoreStatusFromMimePart
    ) {
      return;
    }

    if (aNestingLevel > 1) {
      // we are not interested
      return;
    }

    if (aMsgNeckoURL != this.getSelectedMessageURI()) {
      // Status isn't for selected message.
      return;
    }

    if (gSignatureStatusForURI == aMsgNeckoURL) {
      // We already received a status previously for this URL.
      // Don't allow overriding an existing bad status.
      if (gSignatureStatus != Ci.nsICMSMessageErrors.SUCCESS) {
        return;
      }
    }

    gSignatureStatusForURI = aMsgNeckoURL;
    // eslint-disable-next-line no-global-assign
    gSignatureStatus = aSignatureStatus;
    gSignerCert = aSignerCert;

    refreshSmimeMessageEncryptionStatus(aOriginMimePartNumber);

    let signed = smimeSignedStateToString(aSignatureStatus);
    if (signed == "unknown" || signed == "mismatch") {
      this.showSenderIfSigner();
    }

    // For telemetry purposes.
    window.dispatchEvent(
      new CustomEvent("secureMsgLoaded", {
        detail: {
          key: "signed-smime",
          data: signed,
        },
      })
    );
  },

  /**
   * Force showing Sender if we have a Sender and it's not signed by From.
   * For a valid cert that means the Sender signed it - and the signed mismatch
   * mark is shown. To understand why it's not a confirmed signing it's useful
   * to have the Sender header showing.
   */
  showSenderIfSigner() {
    if (!("sender" in currentHeaderData)) {
      // Sender not set, or same as From (so no longer present).
      return;
    }

    if (Services.prefs.getBoolPref("mailnews.headers.showSender")) {
      // Sender header will be show due to pref - nothing more to do.
      return;
    }

    let fromMailboxes = MailServices.headerParser
      .extractHeaderAddressMailboxes(currentHeaderData.from.headerValue)
      .split(",");
    for (let i = 0; i < fromMailboxes.length; i++) {
      if (gSignerCert.containsEmailAddress(fromMailboxes[i])) {
        return; // It's signed by a From. Nothing more to do
      }
    }

    let senderInfo = { name: "sender", outputFunction: outputEmailAddresses };
    let senderEntry = new MsgHeaderEntry("expanded", senderInfo);

    gExpandedHeaderView[senderInfo.name] = senderEntry;
    UpdateExpandedMessageHeaders();
  },

  encryptionStatus(
    aNestingLevel,
    aEncryptionStatus,
    aRecipientCert,
    aMsgNeckoURL,
    aOriginMimePartNumber
  ) {
    if (
      !!gIgnoreStatusFromMimePart &&
      aOriginMimePartNumber == gIgnoreStatusFromMimePart
    ) {
      return;
    }

    if (aNestingLevel > 1) {
      // we are not interested
      return;
    }

    if (aMsgNeckoURL != this.getSelectedMessageURI()) {
      // Status isn't for selected message.
      return;
    }

    if (gEncryptionStatusForURI == aMsgNeckoURL) {
      // We already received a status previously for this URL.
      // Don't allow overriding an existing bad status.
      if (gEncryptionStatus != Ci.nsICMSMessageErrors.SUCCESS) {
        return;
      }
    }

    gEncryptionStatusForURI = aMsgNeckoURL;
    // eslint-disable-next-line no-global-assign
    gEncryptionStatus = aEncryptionStatus;
    gEncryptionCert = aRecipientCert;

    refreshSmimeMessageEncryptionStatus(aOriginMimePartNumber);

    if (gEncryptedURIService) {
      // Remember the message URI and the corresponding necko URI.
      gMyLastEncryptedURI = gMessageURI;
      gEncryptedURIService.rememberEncrypted(gMyLastEncryptedURI);
      gEncryptedURIService.rememberEncrypted(
        neckoURLForMessageURI(gMyLastEncryptedURI)
      );
    }

    switch (aEncryptionStatus) {
      case Ci.nsICMSMessageErrors.SUCCESS:
      case Ci.nsICMSMessageErrors.ENCRYPT_INCOMPLETE:
        break;
      default:
        var brand = document
          .getElementById("bundle_brand")
          .getString("brandShortName");
        var title = gSMIMEBundle
          .GetStringFromName("CantDecryptTitle")
          .replace(/%brand%/g, brand);
        var body = gSMIMEBundle
          .GetStringFromName("CantDecryptBody")
          .replace(/%brand%/g, brand);

        // TODO: This should be replaced with a real page, and made not ugly.
        HideMessageHeaderPane();
        MailE10SUtils.loadURI(
          getMessagePaneBrowser(),
          "data:text/html;base64," +
            btoa(
              `<html>
              <head>
                <title>${title}</title>
              </head>
              <body>
                <h1>${title}</h1>
                ${body}
              </body>
            </html>`
            )
        );
        break;
    }

    // For telemetry purposes.
    window.dispatchEvent(
      new CustomEvent("secureMsgLoaded", {
        detail: {
          key: "encrypted-smime",
          data: smimeEncryptedStateToString(aEncryptionStatus),
        },
      })
    );
  },

  ignoreStatusFrom(aOriginMimePartNumber) {
    setIgnoreStatusFromMimePart(aOriginMimePartNumber);
  },

  QueryInterface: ChromeUtils.generateQI(["nsIMsgSMIMEHeaderSink"]),
};

function forgetEncryptedURI() {
  if (gMyLastEncryptedURI && gEncryptedURIService) {
    gEncryptedURIService.forgetEncrypted(gMyLastEncryptedURI);
    gEncryptedURIService.forgetEncrypted(
      neckoURLForMessageURI(gMyLastEncryptedURI)
    );
    gMyLastEncryptedURI = null;
  }
}

function onSMIMEStartHeaders() {
  // eslint-disable-next-line no-global-assign
  gEncryptionStatus = -1;
  // eslint-disable-next-line no-global-assign
  gSignatureStatus = -1;

  gSignatureStatusForURI = null;
  gEncryptionStatusForURI = null;

  gSignerCert = null;
  gEncryptionCert = null;

  setMessageCryptoBox(null, null, null, false);

  forgetEncryptedURI();
  onMessageSecurityPopupHidden();
}

function onSMIMEEndHeaders() {}

function onSmartCardChange() {
  // only reload encrypted windows
  if (gMyLastEncryptedURI && gEncryptionStatus != -1) {
    ReloadMessage();
  }
}

function onSMIMEBeforeShowHeaderPane() {
  // For signed messages with differing Sender as signer we force showing Sender.
  // If we're now in a different message, hide the (old) sender row and remove
  // it from the header view, so that Sender normally isn't shown.
  if (
    "sender" in gExpandedHeaderView &&
    !Services.prefs.getBoolPref("mailnews.headers.showSender")
  ) {
    gExpandedHeaderView.sender.enclosingRow.hidden = true;
    delete gExpandedHeaderView.sender;
  }
}

function msgHdrViewSMIMEOnLoad(event) {
  window.crypto.enableSmartCardEvents = true;
  document.addEventListener("smartcard-insert", onSmartCardChange);
  document.addEventListener("smartcard-remove", onSmartCardChange);
  if (!gSMIMEBundle) {
    gSMIMEBundle = Services.strings.createBundle(
      "chrome://messenger-smime/locale/msgReadSMIMEOverlay.properties"
    );
  }

  // Add ourself to the list of message display listeners so we get notified
  // when we are about to display a message.
  var listener = {};
  listener.onStartHeaders = onSMIMEStartHeaders;
  listener.onEndHeaders = onSMIMEEndHeaders;
  listener.onBeforeShowHeaderPane = onSMIMEBeforeShowHeaderPane;
  gMessageListeners.push(listener);

  // eslint-disable-next-line no-global-assign
  gEncryptedURIService = Cc[
    "@mozilla.org/messenger-smime/smime-encrypted-uris-service;1"
  ].getService(Ci.nsIEncryptedSMIMEURIsService);
}

function msgHdrViewSMIMEOnUnload(event) {
  window.crypto.enableSmartCardEvents = false;
  document.removeEventListener("smartcard-insert", onSmartCardChange);
  document.removeEventListener("smartcard-remove", onSmartCardChange);
  forgetEncryptedURI();
  removeEventListener("messagepane-loaded", msgHdrViewSMIMEOnLoad, true);
  removeEventListener("messagepane-unloaded", msgHdrViewSMIMEOnUnload, true);
  removeEventListener(
    "messagepane-hide",
    msgHdrViewSMIMEOnMessagePaneHide,
    true
  );
  removeEventListener(
    "messagepane-unhide",
    msgHdrViewSMIMEOnMessagePaneUnhide,
    true
  );
}

function msgHdrViewSMIMEOnMessagePaneHide() {
  setMessageCryptoBox(null, null, null, false);
}

function msgHdrViewSMIMEOnMessagePaneUnhide() {
  refreshSmimeMessageEncryptionStatus();
}

addEventListener("messagepane-loaded", msgHdrViewSMIMEOnLoad, true);
addEventListener("messagepane-unloaded", msgHdrViewSMIMEOnUnload, true);
addEventListener("messagepane-hide", msgHdrViewSMIMEOnMessagePaneHide, true);
addEventListener(
  "messagepane-unhide",
  msgHdrViewSMIMEOnMessagePaneUnhide,
  true
);
