summaryrefslogtreecommitdiffstats
path: root/comm/mail/extensions/openpgp/content/ui
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/extensions/openpgp/content/ui')
-rw-r--r--comm/mail/extensions/openpgp/content/ui/attachmentItemContext.inc.xhtml17
-rw-r--r--comm/mail/extensions/openpgp/content/ui/backupKeyPassword.js123
-rw-r--r--comm/mail/extensions/openpgp/content/ui/backupKeyPassword.xhtml67
-rw-r--r--comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.js152
-rw-r--r--comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.xhtml73
-rw-r--r--comm/mail/extensions/openpgp/content/ui/commonWorkflows.js194
-rw-r--r--comm/mail/extensions/openpgp/content/ui/composeKeyStatus.js222
-rw-r--r--comm/mail/extensions/openpgp/content/ui/composeKeyStatus.xhtml94
-rw-r--r--comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.js102
-rw-r--r--comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.xhtml55
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailCommon.js69
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.js172
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.xhtml42
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.js1442
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.xhtml406
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailMessengerOverlay.js3460
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.js181
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.xhtml71
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailMsgComposeOverlay.js3034
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailMsgHdrViewOverlay.js1214
-rw-r--r--comm/mail/extensions/openpgp/content/ui/keyAssistant.inc.xhtml119
-rw-r--r--comm/mail/extensions/openpgp/content/ui/keyAssistant.js956
-rw-r--r--comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.js1119
-rw-r--r--comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.xhtml405
-rw-r--r--comm/mail/extensions/openpgp/content/ui/keyWizard.js1195
-rw-r--r--comm/mail/extensions/openpgp/content/ui/keyWizard.xhtml506
-rw-r--r--comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.js177
-rw-r--r--comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.xhtml86
28 files changed, 15753 insertions, 0 deletions
diff --git a/comm/mail/extensions/openpgp/content/ui/attachmentItemContext.inc.xhtml b/comm/mail/extensions/openpgp/content/ui/attachmentItemContext.inc.xhtml
new file mode 100644
index 0000000000..060752b497
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/attachmentItemContext.inc.xhtml
@@ -0,0 +1,17 @@
+# 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/.
+
+ <menuseparator id="openpgpCtxItemsSeparator"/>
+ <menuitem id="enigmail_ctxImportKey"
+ data-l10n-id="openpgp-ctx-import-key"
+ oncommand="Enigmail.msg.handleAttachmentSel('importKey');"/>
+ <menuitem id="enigmail_ctxDecryptOpen"
+ data-l10n-id="openpgp-ctx-decrypt-open"
+ oncommand="Enigmail.msg.handleAttachmentSel('openAttachment');"/>
+ <menuitem id="enigmail_ctxDecryptSave"
+ data-l10n-id="openpgp-ctx-decrypt-save"
+ oncommand="Enigmail.msg.handleAttachmentSel('saveAttachment');"/>
+ <menuitem id="enigmail_ctxVerifyAtt"
+ data-l10n-id="openpgp-ctx-verify-att"
+ oncommand="Enigmail.msg.handleAttachmentSel('verifySig');"/>
diff --git a/comm/mail/extensions/openpgp/content/ui/backupKeyPassword.js b/comm/mail/extensions/openpgp/content/ui/backupKeyPassword.js
new file mode 100644
index 0000000000..9916429bcb
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/backupKeyPassword.js
@@ -0,0 +1,123 @@
+/* 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/. */
+"use strict";
+
+/**
+ * @file Implements the functionality of backupKeyPassword.xhtml:
+ * a dialog that lets the user enter the password used to protect
+ * a backup of OpenPGP secret keys.
+ * Based on setp12password.js and setp12password.xhtml
+ */
+
+/**
+ * @property {boolean} confirmedPassword
+ * Set to true if the user entered two matching passwords and
+ * confirmed the dialog.
+ * @property {string} password
+ * The password the user entered. Undefined value if
+ * |confirmedPassword| is not true.
+ */
+
+let gAcceptButton;
+
+window.addEventListener("DOMContentLoaded", onLoad);
+
+/**
+ * onload() handler.
+ */
+function onLoad() {
+ // Ensure the first password textbox has focus.
+ document.getElementById("pw1").focus();
+ document.addEventListener("dialogaccept", onDialogAccept);
+ gAcceptButton = document
+ .getElementById("backupKeyPassword")
+ .getButton("accept");
+ gAcceptButton.disabled = true;
+}
+
+/**
+ * ondialogaccept() handler.
+ */
+function onDialogAccept() {
+ window.arguments[0].okCallback(
+ document.getElementById("pw1").value,
+ window.arguments[0].fprArray,
+ window.arguments[0].file,
+ true
+ );
+}
+
+/**
+ * Calculates the strength of the given password, suitable for use in updating
+ * a progress bar that represents said strength.
+ *
+ * The strength of the password is calculated by checking the number of:
+ * - Characters
+ * - Numbers
+ * - Non-alphanumeric chars
+ * - Upper case characters
+ *
+ * @param {string} password
+ * The password to calculate the strength of.
+ * @returns {number}
+ * The strength of the password in the range [0, 100].
+ */
+function getPasswordStrength(password) {
+ let lengthStrength = password.length;
+ if (lengthStrength > 5) {
+ lengthStrength = 5;
+ }
+
+ let nonNumericChars = password.replace(/[0-9]/g, "");
+ let numericStrength = password.length - nonNumericChars.length;
+ if (numericStrength > 3) {
+ numericStrength = 3;
+ }
+
+ let nonSymbolChars = password.replace(/\W/g, "");
+ let symbolStrength = password.length - nonSymbolChars.length;
+ if (symbolStrength > 3) {
+ symbolStrength = 3;
+ }
+
+ let nonUpperAlphaChars = password.replace(/[A-Z]/g, "");
+ let upperAlphaStrength = password.length - nonUpperAlphaChars.length;
+ if (upperAlphaStrength > 3) {
+ upperAlphaStrength = 3;
+ }
+
+ let strength =
+ lengthStrength * 10 -
+ 20 +
+ numericStrength * 10 +
+ symbolStrength * 15 +
+ upperAlphaStrength * 10;
+ if (strength < 0) {
+ strength = 0;
+ }
+ if (strength > 100) {
+ strength = 100;
+ }
+
+ return strength;
+}
+
+/**
+ * oninput() handler for both password textboxes.
+ *
+ * @param {boolean} recalculatePasswordStrength
+ * Whether to recalculate the strength of the first password.
+ */
+function onPasswordInput(recalculatePasswordStrength) {
+ let pw1 = document.getElementById("pw1").value;
+
+ if (recalculatePasswordStrength) {
+ document.getElementById("pwmeter").value = getPasswordStrength(pw1);
+ }
+
+ // Disable the accept button if the two passwords don't match, and enable it
+ // if the passwords do match.
+ let pw2 = document.getElementById("pw2").value;
+ gAcceptButton.disabled = pw1 != pw2 || !pw1.length;
+}
diff --git a/comm/mail/extensions/openpgp/content/ui/backupKeyPassword.xhtml b/comm/mail/extensions/openpgp/content/ui/backupKeyPassword.xhtml
new file mode 100644
index 0000000000..1fe18e1b0f
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/backupKeyPassword.xhtml
@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/inlineNotification.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/backupKeyPassword.css" type="text/css"?>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title data-l10n-id="set-password-window-title"></title>
+ <link rel="localization" href="messenger/openpgp/backupKeyPassword.ftl" />
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js" />
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js" />
+ <script
+ defer="defer"
+ src="chrome://openpgp/content/ui/backupKeyPassword.js"
+ />
+ </head>
+ <body>
+ <xul:dialog id="backupKeyPassword" buttons="accept,cancel">
+ <p data-l10n-id="set-password-message"></p>
+
+ <fieldset>
+ <legend data-l10n-id="set-password-legend"></legend>
+
+ <label for="pw1" data-l10n-id="set-password-backup-pw-label" />
+ <input
+ id="pw1"
+ type="password"
+ class="input-inline"
+ oninput="onPasswordInput(true);"
+ />
+
+ <label for="pw2" data-l10n-id="set-password-backup-pw2-label" />
+ <input
+ id="pw2"
+ type="password"
+ class="input-inline"
+ oninput="onPasswordInput(false);"
+ />
+
+ <label for="pwmeter" data-l10n-id="password-quality-meter" />
+ <progress id="pwmeter" value="0" max="100"></progress>
+ </fieldset>
+
+ <div class="inline-notification-container info-container self-center">
+ <img
+ class="notification-image"
+ src="chrome://messenger/skin/icons/information.svg"
+ alt=""
+ />
+ <p data-l10n-id="set-password-reminder"></p>
+ </div>
+ </xul:dialog>
+ </body>
+</html>
diff --git a/comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.js b/comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.js
new file mode 100644
index 0000000000..f6974ddce9
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.js
@@ -0,0 +1,152 @@
+/* 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/. */
+
+var { EnigmailKeyRing } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+);
+var { EnigmailKey } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/key.jsm"
+);
+var { RNP, RnpPrivateKeyUnlockTracker } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/RNP.jsm"
+);
+
+let gFingerprints = [];
+let gKeyCreated;
+
+window.addEventListener("DOMContentLoaded", onLoad);
+function onLoad() {
+ let keyId = window.arguments[0].keyId;
+ let keyObj = EnigmailKeyRing.getKeyById(window.arguments[0].keyId);
+ if (!keyObj) {
+ throw new Error(`Key not found: ${keyId}`);
+ }
+ if (!keyObj.secretAvailable) {
+ keyObj = null;
+ throw new Error(`Not your key: ${keyId}`);
+ }
+
+ if (!keyObj.iSimpleOneSubkeySameExpiry()) {
+ window.close();
+ return;
+ }
+
+ gFingerprints = [keyObj.fpr, keyObj.subKeys[0].fpr];
+ gKeyCreated = keyObj.keyCreated;
+
+ let currentExpiryInfo = document.getElementById("info-current-expiry");
+
+ if (!keyObj.expiryTime) {
+ document.l10n.setAttributes(currentExpiryInfo, "info-does-not-expire");
+ } else {
+ let nowSeconds = Math.floor(Date.now() / 1000);
+ if (keyObj.expiryTime < nowSeconds) {
+ document.l10n.setAttributes(currentExpiryInfo, "info-already-expired");
+ } else {
+ document.l10n.setAttributes(currentExpiryInfo, "info-will-expire", {
+ date: keyObj.expiry,
+ });
+ }
+ }
+
+ // Don't explain how to use longer, if this key already never expires.
+ document.getElementById("longerUsage").hidden = !keyObj.expiryTime;
+
+ let popup = document.getElementById("expiry-in");
+ let rtf = new Intl.RelativeTimeFormat(undefined, {
+ numeric: "always",
+ style: "long",
+ });
+ let today = new Date();
+ for (let i = 1; i < 24; i++) {
+ let d = new Date(
+ today.getFullYear(),
+ today.getMonth() + i,
+ today.getDate()
+ );
+ let option = document.createElement("option");
+ option.value = Math.floor(d.getTime() / 1000); // In seconds.
+ option.label = rtf.format(i, "month");
+ popup.appendChild(option);
+ }
+ for (let i = 2; i <= 10; i++) {
+ let d = new Date(
+ today.getFullYear() + i,
+ today.getMonth(),
+ today.getDate()
+ );
+ let option = document.createElement("option");
+ option.value = Math.floor(d.getTime() / 1000); // In seconds.
+ option.label = rtf.format(i, "year");
+ popup.appendChild(option);
+ }
+ if (keyObj.expiryTime) {
+ popup.selectedIndex = [...popup.children].findIndex(
+ o => o.value >= keyObj.expiryTime
+ );
+ } else {
+ popup.selectedIndex = 23; // 2 years
+ }
+ document.getElementById("radio-expire-yes").value = popup.value;
+
+ popup.addEventListener("change", event => {
+ document.getElementById("radio-expire-yes").value = event.target.value;
+ document.getElementById("radio-expire-yes").checked = true;
+ });
+}
+
+async function onAccept() {
+ let expirySecs = +document.querySelector("input[name='expiry']:checked")
+ .value;
+ if (expirySecs < 0) {
+ // Keep.
+ return true;
+ }
+ // Key Expiration Time - this is the number of seconds after the key creation
+ // time that the key expires.
+ let keyExpirationTime = expirySecs ? expirySecs - gKeyCreated : 0;
+
+ let pwCache = {
+ passwords: [],
+ };
+
+ let unlockFailed = false;
+ let keyTrackers = [];
+ for (let fp of gFingerprints) {
+ let tracker = RnpPrivateKeyUnlockTracker.constructFromFingerprint(fp);
+ tracker.setAllowPromptingUserForPassword(true);
+ tracker.setAllowAutoUnlockWithCachedPasswords(true);
+ tracker.setPasswordCache(pwCache);
+ await tracker.unlock();
+ keyTrackers.push(tracker);
+ if (!tracker.isUnlocked()) {
+ unlockFailed = true;
+ break;
+ }
+ }
+
+ let rv = false;
+ if (!unlockFailed) {
+ rv = RNP.changeExpirationDate(gFingerprints, keyExpirationTime);
+ }
+
+ for (let t of keyTrackers) {
+ t.release();
+ }
+ return rv;
+}
+
+document.addEventListener("dialogaccept", async function (event) {
+ // Prevent the closing of the dialog to wait until the call
+ // to onAccept() has properly returned.
+ event.preventDefault();
+ let result = await onAccept();
+ // If the change was unsuccessful, leave this dialog open.
+ if (!result) {
+ return;
+ }
+ // Otherwise, update the parent window and close the dialog.
+ window.arguments[0].modified();
+ window.close();
+});
diff --git a/comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.xhtml b/comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.xhtml
new file mode 100644
index 0000000000..ddfd67ce24
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.xhtml
@@ -0,0 +1,73 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/inlineNotification.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/changeExpiryDlg.css" type="text/css"?>
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title data-l10n-id="openpgp-change-expiry-title"></title>
+ <link rel="localization" href="messenger/openpgp/changeExpiryDlg.ftl" />
+ <script
+ defer="defer"
+ src="chrome://openpgp/content/ui/changeExpiryDlg.js"
+ ></script>
+ </head>
+ <body>
+ <xul:dialog id="changeExpiryDlg" buttons="cancel,accept">
+ <div class="inline-notification-container info-container self-center">
+ <img
+ class="notification-image"
+ src="chrome://messenger/skin/icons/information.svg"
+ alt=""
+ />
+ <p data-l10n-id="info-explanation-1"></p>
+ </div>
+
+ <p id="info-current-expiry"></p>
+
+ <p id="longerUsage" data-l10n-id="info-explanation-2"></p>
+
+ <fieldset>
+ <div>
+ <input
+ id="radio-expire-keep"
+ type="radio"
+ name="expiry"
+ value="-1"
+ checked="checked"
+ />
+ <label
+ for="radio-expire-keep"
+ data-l10n-id="expire-no-change-label"
+ />
+ </div>
+
+ <div>
+ <input id="radio-expire-yes" type="radio" name="expiry" value="" />
+ <label for="radio-expire-yes" data-l10n-id="expire-in-time-label" />
+
+ <select id="expiry-in"></select>
+ </div>
+
+ <div>
+ <input id="radio-expire-no" type="radio" name="expiry" value="0" />
+ <label
+ for="radio-expire-no"
+ data-l10n-id="expire-never-expire-label"
+ />
+ </div>
+ </fieldset>
+ </xul:dialog>
+ </body>
+</html>
diff --git a/comm/mail/extensions/openpgp/content/ui/commonWorkflows.js b/comm/mail/extensions/openpgp/content/ui/commonWorkflows.js
new file mode 100644
index 0000000000..ac6c054e2f
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/commonWorkflows.js
@@ -0,0 +1,194 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+var { EnigmailDialog } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/dialog.jsm"
+);
+var { EnigmailKey } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/key.jsm"
+);
+var { EnigmailKeyRing } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+);
+var { EnigmailArmor } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/armor.jsm"
+);
+var { MailStringUtils } = ChromeUtils.import(
+ "resource:///modules/MailStringUtils.jsm"
+);
+
+var l10n = new Localization(["messenger/openpgp/openpgp.ftl"], true);
+
+/**
+ * opens a prompt, asking the user to enter passphrase for given key id
+ * returns: the passphrase if entered (empty string is allowed)
+ * resultFlags.canceled is set to true if the user clicked cancel
+ */
+function passphrasePromptCallback(win, promptString, resultFlags) {
+ let password = { value: "" };
+ if (!Services.prompt.promptPassword(win, "", promptString, password)) {
+ resultFlags.canceled = true;
+ return "";
+ }
+
+ resultFlags.canceled = false;
+ return password.value;
+}
+
+/**
+ * @param {nsIFile} file
+ * @returns {string} The first block of the wanted type, or empty string.
+ * Skip blocks of wrong type.
+ */
+async function getKeyBlockFromFile(file, wantSecret) {
+ let contents = await IOUtils.readUTF8(file.path).catch(() => "");
+ let searchOffset = 0;
+
+ while (searchOffset < contents.length) {
+ const beginIndexObj = {};
+ const endIndexObj = {};
+ const blockType = EnigmailArmor.locateArmoredBlock(
+ contents,
+ searchOffset,
+ "",
+ beginIndexObj,
+ endIndexObj,
+ {}
+ );
+ if (!blockType) {
+ return "";
+ }
+
+ if (
+ (wantSecret && blockType.search(/^PRIVATE KEY BLOCK$/) !== 0) ||
+ (!wantSecret && blockType.search(/^PUBLIC KEY BLOCK$/) !== 0)
+ ) {
+ searchOffset = endIndexObj.value;
+ continue;
+ }
+
+ return contents.substr(
+ beginIndexObj.value,
+ endIndexObj.value - beginIndexObj.value + 1
+ );
+ }
+ return "";
+}
+
+/**
+ * import OpenPGP keys from file
+ *
+ * @param {string} what - "rev" for revocation, "pub" for public keys
+ */
+async function EnigmailCommon_importObjectFromFile(what) {
+ if (what != "rev" && what != "pub") {
+ throw new Error(`Can't import. Invalid argument: ${what}`);
+ }
+
+ let importingRevocation = what == "rev";
+ let promptStr = importingRevocation ? "import-rev-file" : "import-key-file";
+
+ let files = EnigmailDialog.filePicker(
+ window,
+ l10n.formatValueSync(promptStr),
+ "",
+ false,
+ true,
+ "*.asc",
+ "",
+ [l10n.formatValueSync("gnupg-file"), "*.asc;*.gpg;*.pgp"]
+ );
+
+ if (!files.length) {
+ return;
+ }
+
+ for (let file of files) {
+ if (file.fileSize > 5000000) {
+ document.l10n.formatValue("file-to-big-to-import").then(value => {
+ EnigmailDialog.alert(window, value);
+ });
+ continue;
+ }
+
+ let errorMsgObj = {};
+
+ if (importingRevocation) {
+ await EnigmailKeyRing.importRevFromFile(file);
+ continue;
+ }
+
+ let importBinary = false;
+ let keyBlock = await getKeyBlockFromFile(file, false);
+
+ // if we don't find an ASCII block, try to import as binary.
+ if (!keyBlock) {
+ importBinary = true;
+ let data = await IOUtils.read(file.path);
+ keyBlock = MailStringUtils.uint8ArrayToByteString(data);
+ }
+
+ // Generate a preview of the imported key.
+ let preview = await EnigmailKey.getKeyListFromKeyBlock(
+ keyBlock,
+ errorMsgObj,
+ true, // interactive
+ true,
+ false // not secret
+ );
+
+ if (!preview || !preview.length || errorMsgObj.value) {
+ document.l10n.formatValue("import-keys-failed").then(value => {
+ EnigmailDialog.alert(window, value + "\n\n" + errorMsgObj.value);
+ });
+ continue;
+ }
+
+ if (preview.length > 0) {
+ let confirmImport = false;
+ let autoAcceptance = null;
+ let outParam = {};
+ confirmImport = EnigmailDialog.confirmPubkeyImport(
+ window,
+ preview,
+ outParam
+ );
+ if (confirmImport) {
+ autoAcceptance = outParam.acceptance;
+ }
+
+ if (confirmImport) {
+ // import
+ let resultKeys = {};
+
+ let importExitCode = EnigmailKeyRing.importKey(
+ window,
+ false, // interactive, we already asked for confirmation
+ keyBlock,
+ importBinary,
+ null, // expected keyId, ignored
+ errorMsgObj,
+ resultKeys,
+ false, // minimize
+ [], // filter
+ true, // allow prompt for permissive
+ autoAcceptance
+ );
+
+ if (importExitCode !== 0) {
+ document.l10n.formatValue("import-keys-failed").then(value => {
+ EnigmailDialog.alert(window, value + "\n\n" + errorMsgObj.value);
+ });
+ continue;
+ }
+
+ EnigmailDialog.keyImportDlg(window, resultKeys.value);
+ }
+ }
+ }
+}
diff --git a/comm/mail/extensions/openpgp/content/ui/composeKeyStatus.js b/comm/mail/extensions/openpgp/content/ui/composeKeyStatus.js
new file mode 100644
index 0000000000..a6320072ab
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/composeKeyStatus.js
@@ -0,0 +1,222 @@
+/* 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/. */
+
+var { EnigmailFuncs } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/funcs.jsm"
+);
+var EnigmailKeyRing = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+).EnigmailKeyRing;
+var { EnigmailWindows } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/windows.jsm"
+);
+var { EnigmailKey } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/key.jsm"
+);
+const { OpenPGPAlias } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/OpenPGPAlias.jsm"
+);
+const { PgpSqliteDb2 } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/sqliteDb.jsm"
+);
+
+var gListBox;
+var gViewButton;
+
+var gEmailAddresses = [];
+var gRowToEmail = [];
+
+// One boolean entry per row. True means it is an alias row.
+// This allows us to use different dialog behavior for alias entries.
+var gAliasRows = [];
+
+var gMapAddressToKeyObjs = null;
+
+function addRecipients(toAddrList, recList) {
+ for (var i = 0; i < recList.length; i++) {
+ try {
+ let entry = EnigmailFuncs.stripEmail(recList[i].replace(/[",]/g, ""));
+ toAddrList.push(entry);
+ } catch (ex) {
+ console.debug(ex);
+ }
+ }
+}
+
+async function setListEntries() {
+ gMapAddressToKeyObjs = new Map();
+
+ for (let addr of gEmailAddresses) {
+ addr = addr.toLowerCase();
+
+ let statusStringID = null;
+ let statusStringDirect = "";
+
+ let aliasKeyList = EnigmailKeyRing.getAliasKeyList(addr);
+ let isAlias = !!aliasKeyList;
+
+ if (isAlias) {
+ let aliasKeys = EnigmailKeyRing.getAliasKeys(aliasKeyList);
+ if (!aliasKeys.length) {
+ // failure, at least one alias key is unusable/unavailable
+ statusStringDirect = await document.l10n.formatValue(
+ "openpgp-compose-alias-status-error"
+ );
+ } else {
+ statusStringDirect = await document.l10n.formatValue(
+ "openpgp-compose-alias-status-direct",
+ {
+ count: aliasKeys.length,
+ }
+ );
+ }
+ } else {
+ // We ask to include keys which are expired, because that's what
+ // our sub dialog oneRecipientStatus needs. This is for
+ // efficiency - because otherwise the sub dialog would have to
+ // query all keys again.
+ // The consequence is, we need to later call isValidForEncryption
+ // for the keys we have obtained, to confirm they are really valid.
+ let foundKeys = await EnigmailKeyRing.getMultValidKeysForOneRecipient(
+ addr,
+ true
+ );
+ if (!foundKeys || !foundKeys.length) {
+ statusStringID = "openpgp-recip-missing";
+ } else {
+ gMapAddressToKeyObjs.set(addr, foundKeys);
+ for (let keyObj of foundKeys) {
+ let goodPersonal = false;
+ if (keyObj.secretAvailable) {
+ goodPersonal = await PgpSqliteDb2.isAcceptedAsPersonalKey(
+ keyObj.fpr
+ );
+ }
+ if (
+ goodPersonal ||
+ (EnigmailKeyRing.isValidForEncryption(keyObj) &&
+ (keyObj.acceptance == "verified" ||
+ keyObj.acceptance == "unverified"))
+ ) {
+ statusStringID = "openpgp-recip-good";
+ break;
+ }
+ }
+ if (!statusStringID) {
+ statusStringID = "openpgp-recip-none-accepted";
+ }
+ }
+ }
+
+ let listitem = document.createXULElement("richlistitem");
+
+ let emailItem = document.createXULElement("label");
+ emailItem.setAttribute("value", addr);
+ emailItem.setAttribute("crop", "end");
+ emailItem.setAttribute("style", "width: var(--recipientWidth)");
+ listitem.appendChild(emailItem);
+
+ let status = document.createXULElement("label");
+
+ if (statusStringID) {
+ document.l10n.setAttributes(status, statusStringID);
+ } else {
+ status.setAttribute("value", statusStringDirect);
+ }
+
+ status.setAttribute("crop", "end");
+ status.setAttribute("style", "width: var(--statusWidth)");
+ listitem.appendChild(status);
+
+ gListBox.appendChild(listitem);
+
+ gRowToEmail.push(addr);
+ gAliasRows.push(isAlias);
+ }
+}
+
+async function onLoad() {
+ let params = window.arguments[0];
+ if (!params) {
+ return;
+ }
+
+ try {
+ await OpenPGPAlias.load();
+ } catch (ex) {
+ console.log("failed to load OpenPGP alias file: " + ex);
+ }
+
+ gListBox = document.getElementById("infolist");
+ gViewButton = document.getElementById("detailsButton");
+
+ var arrLen = {};
+ var recList;
+
+ if (params.compFields.to) {
+ recList = params.compFields.splitRecipients(
+ params.compFields.to,
+ true,
+ arrLen
+ );
+ addRecipients(gEmailAddresses, recList);
+ }
+ if (params.compFields.cc) {
+ recList = params.compFields.splitRecipients(
+ params.compFields.cc,
+ true,
+ arrLen
+ );
+ addRecipients(gEmailAddresses, recList);
+ }
+ if (params.compFields.bcc) {
+ recList = params.compFields.splitRecipients(
+ params.compFields.bcc,
+ true,
+ arrLen
+ );
+ addRecipients(gEmailAddresses, recList);
+ }
+
+ await setListEntries();
+}
+
+async function reloadAndReselect(selIndex = -1) {
+ while (true) {
+ let child = gListBox.lastChild;
+ // keep first child, which is the header
+ if (child == gListBox.firstChild) {
+ break;
+ }
+ gListBox.removeChild(child);
+ }
+ gRowToEmail = [];
+ await setListEntries();
+ gListBox.selectedIndex = selIndex;
+}
+
+function onSelectionChange(event) {
+ // We don't offer detail management/discovery for email addresses
+ // that match an alias rule.
+ gViewButton.disabled =
+ !gListBox.selectedItems.length || gAliasRows[gListBox.selectedIndex];
+}
+
+function viewSelectedEmail() {
+ let selIndex = gListBox.selectedIndex;
+ if (gViewButton.disabled || selIndex == -1) {
+ return;
+ }
+ let email = gRowToEmail[selIndex];
+ window.openDialog(
+ "chrome://openpgp/content/ui/oneRecipientStatus.xhtml",
+ "",
+ "chrome,modal,resizable,centerscreen",
+ {
+ email,
+ keys: gMapAddressToKeyObjs.get(email),
+ }
+ );
+ reloadAndReselect(selIndex);
+}
diff --git a/comm/mail/extensions/openpgp/content/ui/composeKeyStatus.xhtml b/comm/mail/extensions/openpgp/content/ui/composeKeyStatus.xhtml
new file mode 100644
index 0000000000..fc0192a8a6
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/composeKeyStatus.xhtml
@@ -0,0 +1,94 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/openPgpComposeStatus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<window
+ data-l10n-id="openpgp-compose-key-status-title"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ style="width: 40em; height: 25em"
+ persist="width height"
+ lightweightthemes="true"
+ onload="onLoad();"
+>
+ <dialog id="composeKeyStatus" buttons="accept">
+ <script src="chrome://openpgp/content/ui/composeKeyStatus.js" />
+ <script>
+ <![CDATA[
+ function resizeColumns() {
+ let list = document.getElementById("infolist");
+ let cols = list.getElementsByTagName("treecol");
+ list.style.setProperty("--recipientWidth", cols[0].getBoundingClientRect().width + "px");
+ list.style.setProperty("--statusWidth", cols[1].getBoundingClientRect().width + "px");
+ }
+ addEventListener("load", resizeColumns, { once: true });
+ addEventListener("resize", resizeColumns);
+ ]]>
+ </script>
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link
+ rel="localization"
+ href="messenger/openpgp/composeKeyStatus.ftl"
+ />
+ </linkset>
+
+ <description data-l10n-id="openpgp-compose-key-status-intro-need-keys" />
+
+ <separator class="thin" />
+ <label
+ data-l10n-id="openpgp-compose-key-status-keys-heading"
+ control="infolist"
+ />
+
+ <richlistbox
+ id="infolist"
+ class="theme-listbox"
+ flex="1"
+ onselect="onSelectionChange(event);"
+ >
+ <treecols>
+ <treecol
+ id="recipientComposeKeyCol"
+ data-l10n-id="openpgp-compose-key-status-recipient"
+ />
+ <treecol
+ id="statusComposeKeyCol"
+ data-l10n-id="openpgp-compose-key-status-status"
+ />
+ </treecols>
+ </richlistbox>
+ <hbox pack="start">
+ <button
+ id="detailsButton"
+ disabled="true"
+ data-l10n-id="openpgp-compose-key-status-open-details"
+ oncommand="viewSelectedEmail();"
+ />
+ </hbox>
+
+ <separator class="thin" />
+
+ <vbox flex="1">
+ <html:span
+ class="tail-with-learn-more"
+ data-l10n-id="openpgp-compose-general-info-alias"
+ ></html:span>
+ <label
+ is="text-link"
+ id="openPgpAliasLearnMore"
+ href="https://support.mozilla.org/kb/openpgp-recipient-alias-configuration"
+ data-l10n-id="openpgp-compose-general-info-alias-learn-more"
+ />
+ </vbox>
+ </dialog>
+</window>
diff --git a/comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.js b/comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.js
new file mode 100644
index 0000000000..d575c111b7
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.js
@@ -0,0 +1,102 @@
+/* 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/. */
+
+"use strict";
+
+// Dialog event listeners.
+window.addEventListener("dialogaccept", onAccept);
+window.addEventListener("load", init);
+
+var gUndecided = null;
+var gUnverified = null;
+
+async function init() {
+ let num = window.arguments[0].keys.length;
+ let label1 = document.getElementById("importInfo");
+ document.l10n.setAttributes(label1, "openpgp-pubkey-import-intro", {
+ num,
+ });
+ let label2 = document.getElementById("acceptInfo");
+ document.l10n.setAttributes(label2, "openpgp-pubkey-import-accept", {
+ num,
+ });
+
+ let l10nElements = [];
+ l10nElements.push(label1);
+ l10nElements.push(label2);
+
+ // TODO: This should be changed to use data-l10n-id in the .xhtml
+ // at a later time. We reuse strings on the 78 branch that don't have
+ // the .label definition in the .ftl file.
+
+ let [rUnd, rUnv] = await document.l10n.formatValues([
+ { id: "openpgp-key-undecided" },
+ { id: "openpgp-key-unverified" },
+ ]);
+
+ gUndecided = document.getElementById("acceptUndecided");
+ gUndecided.label = rUnd;
+ gUnverified = document.getElementById("acceptUnverified");
+ gUnverified.label = rUnv;
+
+ let keyList = document.getElementById("importKeyList");
+
+ for (let key of window.arguments[0].keys) {
+ let container = document.createXULElement("hbox");
+ container.classList.add("key-import-row");
+
+ let titleContainer = document.createXULElement("vbox");
+ let headerHBox = document.createXULElement("hbox");
+
+ let idSpan = document.createElement("span");
+ let idLabel = document.createXULElement("label");
+ idSpan.appendChild(idLabel);
+ idSpan.classList.add("openpgp-key-id");
+ headerHBox.appendChild(idSpan);
+
+ document.l10n.setAttributes(idLabel, "openpgp-pubkey-import-id", {
+ kid: "0x" + key.keyId,
+ });
+
+ let fprSpan = document.createElement("span");
+ let fprLabel = document.createXULElement("label");
+ fprSpan.appendChild(fprLabel);
+ fprSpan.classList.add("openpgp-key-fpr");
+ headerHBox.appendChild(fprSpan);
+
+ document.l10n.setAttributes(fprLabel, "openpgp-pubkey-import-fpr", {
+ fpr: key.fpr,
+ });
+
+ titleContainer.appendChild(headerHBox);
+
+ for (let uid of key.userIds) {
+ let name = document.createXULElement("label");
+ name.classList.add("openpgp-key-name");
+ name.value = uid.userId;
+ titleContainer.appendChild(name);
+ }
+
+ container.appendChild(titleContainer);
+ keyList.appendChild(container);
+ }
+
+ await document.l10n.translateElements(l10nElements);
+ window.sizeToContent();
+ window.moveTo(
+ (screen.width - window.outerWidth) / 2,
+ (screen.height - window.outerHeight) / 2
+ );
+}
+
+function onAccept(event) {
+ window.arguments[0].confirmed = true;
+ if (gUndecided.selected) {
+ window.arguments[0].acceptance = "undecided";
+ } else if (gUnverified.selected) {
+ window.arguments[0].acceptance = "unverified";
+ } else {
+ throw new Error("internal error, no expected radio button was selected");
+ }
+}
diff --git a/comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.xhtml b/comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.xhtml
new file mode 100644
index 0000000000..35935e0be7
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.xhtml
@@ -0,0 +1,55 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/confirmPubkeyImport.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/inlineNotification.css" type="text/css"?>
+
+<!DOCTYPE window>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <dialog
+ id="confirmPubkeyImportDialog"
+ data-l10n-id="pubkey-import-button"
+ data-l10n-attrs="buttonlabelaccept"
+ buttons="accept,cancel"
+ >
+ <script src="chrome://openpgp/content/ui/confirmPubkeyImport.js" />
+
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link rel="localization" href="messenger/openpgp/openpgp.ftl" />
+ <html:link
+ rel="localization"
+ href="messenger/openpgp/oneRecipientStatus.ftl"
+ />
+ </linkset>
+
+ <vbox>
+ <description id="importInfo" />
+
+ <vbox id="importKeyListContainer">
+ <vbox id="importKeyList" />
+ </vbox>
+
+ <separator />
+
+ <vbox id="acceptancePanel">
+ <description id="acceptInfo" />
+ <html:div>
+ <html:fieldset>
+ <radiogroup id="acceptanceRadio" class="indent">
+ <radio id="acceptUndecided" value="undecided" selected="true" />
+ <radio id="acceptUnverified" value="unverified" />
+ </radiogroup>
+ </html:fieldset>
+ </html:div>
+ </vbox>
+ </vbox>
+ </dialog>
+</window>
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailCommon.js b/comm/mail/extensions/openpgp/content/ui/enigmailCommon.js
new file mode 100644
index 0000000000..ea02824856
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailCommon.js
@@ -0,0 +1,69 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+var { EnigmailCore } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/core.jsm"
+);
+var { RNP } = ChromeUtils.import("chrome://openpgp/content/modules/RNP.jsm");
+
+var l10nCommon = new Localization(["messenger/openpgp/openpgp.ftl"], true);
+
+var gEnigmailSvc;
+function GetEnigmailSvc() {
+ if (!gEnigmailSvc) {
+ gEnigmailSvc = EnigmailCore.getService(window);
+ }
+ return gEnigmailSvc;
+}
+
+async function EnigRevokeKey(keyObj, callbackFunc) {
+ var enigmailSvc = GetEnigmailSvc();
+ if (!enigmailSvc) {
+ return;
+ }
+
+ if (keyObj.keyTrust == "r") {
+ Services.prompt.alert(
+ null,
+ document.title,
+ l10nCommon.formatValueSync("already-revoked")
+ );
+ return;
+ }
+
+ let promptFlags =
+ Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL;
+
+ let confirm = Services.prompt.confirmEx(
+ window,
+ l10nCommon.formatValueSync("openpgp-key-revoke-title"),
+ l10nCommon.formatValueSync("revoke-key-question", {
+ identity: `0x${keyObj.keyId} - ${keyObj.userId}`,
+ }),
+ promptFlags,
+ l10nCommon.formatValueSync("key-man-button-revoke-key"),
+ null,
+ null,
+ null,
+ {}
+ );
+
+ if (confirm != 0) {
+ return;
+ }
+
+ await RNP.revokeKey(keyObj.fpr);
+ callbackFunc(true);
+
+ Services.prompt.alert(
+ null,
+ l10nCommon.formatValueSync("openpgp-key-revoke-success"),
+ l10nCommon.formatValueSync("after-revoke-info")
+ );
+}
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.js b/comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.js
new file mode 100644
index 0000000000..a0069ed3c2
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.js
@@ -0,0 +1,172 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EnigmailWindows = ChromeUtils.import(
+ "chrome://openpgp/content/modules/windows.jsm"
+).EnigmailWindows;
+var EnigmailKeyRing = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+).EnigmailKeyRing;
+var EnigmailDialog = ChromeUtils.import(
+ "chrome://openpgp/content/modules/dialog.jsm"
+).EnigmailDialog;
+var EnigmailData = ChromeUtils.import(
+ "chrome://openpgp/content/modules/data.jsm"
+).EnigmailData;
+
+async function onLoad() {
+ let dlg = document.getElementById("enigmailKeyImportInfo");
+
+ let keys = [];
+
+ if (window.screen.width > 500) {
+ dlg.setAttribute("maxwidth", window.screen.width - 150);
+ }
+
+ if (window.screen.height > 300) {
+ dlg.setAttribute("maxheight", window.screen.height - 100);
+ }
+
+ var keyList = window.arguments[0].keyList;
+
+ let onClickFunc = function (event) {
+ let keyId = event.target.getAttribute("keyid");
+ EnigmailWindows.openKeyDetails(window, keyId, false);
+ };
+
+ for (let keyId of keyList) {
+ if (keyId.search(/^0x/) === 0) {
+ keyId = keyId.substr(2).toUpperCase();
+ }
+ let keyObj = EnigmailKeyRing.getKeyById(keyId);
+ if (keyObj && keyObj.fpr) {
+ let keyGroupBox = buildKeyGroupBox(keyObj);
+ keyGroupBox
+ .getElementsByClassName("enigmailKeyImportDetails")[0]
+ .addEventListener("click", onClickFunc, true);
+ keys.push(keyGroupBox);
+ }
+ }
+
+ dlg.getButton("accept").focus();
+
+ if (keys.length) {
+ let keysInfoBox = document.getElementById("keyInfo"),
+ keyBox = document.createXULElement("vbox");
+
+ keyBox.classList.add("grid-three-column");
+ for (let key of keys) {
+ keyBox.appendChild(key);
+ }
+
+ keysInfoBox.appendChild(keyBox);
+ } else {
+ EnigmailDialog.alert(
+ window,
+ await document.l10n.formatValue("import-info-no-keys")
+ );
+ setTimeout(window.close, 0);
+ return;
+ }
+
+ setTimeout(resizeDlg);
+ setTimeout(() => window.sizeToContent());
+}
+
+function buildKeyGroupBox(keyObj) {
+ let groupBox = document.createXULElement("vbox");
+ let userid = document.createXULElement("label");
+
+ groupBox.classList.add("enigmailGroupbox", "enigmailGroupboxMargin");
+ userid.setAttribute("value", keyObj.userId);
+ userid.setAttribute("class", "enigmailKeyImportUserId");
+
+ let infoBox = document.createElement("div");
+ let infoLabelH1 = document.createXULElement("label");
+ let infoLabelH2 = document.createXULElement("label");
+ let infoLabelB1 = document.createXULElement("label");
+ let infoLabelB2 = document.createXULElement("label");
+ let infoLabelB3 = document.createXULElement("label");
+
+ document.l10n.setAttributes(infoLabelH1, "import-info-bits");
+ document.l10n.setAttributes(infoLabelH2, "import-info-created");
+ infoLabelB1.setAttribute("value", keyObj.keySize);
+ infoLabelB2.setAttribute("value", keyObj.created);
+
+ infoLabelH1.classList.add("enigmailKeyImportHeader");
+ infoLabelH2.classList.add("enigmailKeyImportHeader");
+
+ infoBox.classList.add("grid-two-column-fr");
+ infoBox.appendChild(infoLabelH1);
+ infoBox.appendChild(infoLabelH2);
+ infoBox.appendChild(infoLabelB1);
+ infoBox.appendChild(infoLabelB2);
+
+ let fprBox = document.createXULElement("div");
+ let fprLabel = document.createXULElement("label");
+ document.l10n.setAttributes(fprLabel, "import-info-fpr");
+ fprLabel.setAttribute("class", "enigmailKeyImportHeader");
+ let gridTemplateColumns = "";
+ for (let i = 0; i < keyObj.fpr.length; i += 4) {
+ var label = document.createXULElement("label");
+ label.setAttribute("value", keyObj.fpr.substr(i, 4));
+ if (i < keyObj.fpr.length / 2) {
+ gridTemplateColumns += "auto ";
+ }
+ fprBox.appendChild(label);
+ }
+
+ fprBox.style.display = "inline-grid";
+ fprBox.style.gridTemplateColumns = gridTemplateColumns;
+
+ groupBox.appendChild(userid);
+ groupBox.appendChild(infoBox);
+ groupBox.appendChild(fprLabel);
+ groupBox.appendChild(fprBox);
+
+ document.l10n.setAttributes(infoLabelB3, "import-info-details");
+ infoLabelB3.setAttribute("keyid", keyObj.keyId);
+ infoLabelB3.setAttribute("class", "enigmailKeyImportDetails");
+ groupBox.appendChild(infoLabelB3);
+
+ return groupBox;
+}
+
+function resizeDlg() {
+ var txt = document.getElementById("keyInfo");
+ var box = document.getElementById("outerbox");
+
+ var deltaWidth = window.outerWidth - box.clientWidth;
+ var newWidth = txt.scrollWidth + deltaWidth + 20;
+
+ if (newWidth > window.screen.width - 50) {
+ newWidth = window.screen.width - 50;
+ }
+
+ txt.style["white-space"] = "pre-wrap";
+ window.resizeTo(newWidth, window.outerHeight);
+
+ var textHeight = txt.scrollHeight;
+ var boxHeight = box.clientHeight;
+ var deltaHeight = window.outerHeight - boxHeight;
+
+ var newHeight = textHeight + deltaHeight + 25;
+
+ if (newHeight > window.screen.height - 100) {
+ newHeight = window.screen.height - 100;
+ }
+
+ window.resizeTo(newWidth, newHeight);
+}
+
+function dlgClose(buttonNumber) {
+ window.arguments[1].value = buttonNumber;
+ window.close();
+}
+
+document.addEventListener("dialogaccept", function (event) {
+ dlgClose(0);
+});
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.xhtml b/comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.xhtml
new file mode 100644
index 0000000000..c5c8436398
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.xhtml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<!--
+ * 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/.
+-->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/enigmail.css" type="text/css"?>
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/shared/grid-layout.css"?>
+
+<!DOCTYPE window [ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD; ]>
+
+<window
+ data-l10n-id="import-info-title"
+ onload="onLoad();"
+ style="min-width: 100px; max-width: 750px"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <dialog id="enigmailKeyImportInfo" buttons="accept">
+ <script
+ type="application/x-javascript"
+ src="chrome://openpgp/content/ui/enigmailKeyImportInfo.js"
+ />
+ <linkset>
+ <html:link rel="localization" href="messenger/openpgp/openpgp.ftl" />
+ </linkset>
+
+ <vbox align="center" flex="1" style="overflow: auto" id="outerbox">
+ <hbox align="center" flex="1">
+ <description
+ flex="1"
+ id="keyInfo"
+ class="plain"
+ style="white-space: pre"
+ />
+ </hbox>
+ </vbox>
+ </dialog>
+</window>
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.js b/comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.js
new file mode 100644
index 0000000000..e72cf8e6bc
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.js
@@ -0,0 +1,1442 @@
+/*
+ * 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";
+
+/* global GetEnigmailSvc, EnigRevokeKey */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var { EnigmailCore } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/core.jsm"
+);
+var { EnigmailStreams } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/streams.jsm"
+);
+var { EnigmailFuncs } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/funcs.jsm"
+);
+var { EnigmailWindows } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/windows.jsm"
+);
+var { EnigmailKeyServer } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyserver.jsm"
+);
+var { EnigmailWks } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/webKey.jsm"
+);
+var { EnigmailCryptoAPI } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/cryptoAPI.jsm"
+);
+var { KeyLookupHelper } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyLookupHelper.jsm"
+);
+var { EnigmailTrust } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/trust.jsm"
+);
+var { PgpSqliteDb2 } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/sqliteDb.jsm"
+);
+var { EnigmailLog } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/log.jsm"
+);
+var { EnigmailKeyRing } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+);
+var { EnigmailKey } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/key.jsm"
+);
+var { EnigmailConstants } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/constants.jsm"
+);
+var { EnigmailDialog } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/dialog.jsm"
+);
+var { EnigmailKeyserverURIs } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyserverUris.jsm"
+);
+
+const ENIG_KEY_EXPIRED = "e";
+const ENIG_KEY_REVOKED = "r";
+const ENIG_KEY_INVALID = "i";
+const ENIG_KEY_DISABLED = "d";
+const ENIG_KEY_NOT_VALID =
+ ENIG_KEY_EXPIRED + ENIG_KEY_REVOKED + ENIG_KEY_INVALID + ENIG_KEY_DISABLED;
+
+var l10n = new Localization(["messenger/openpgp/openpgp.ftl"], true);
+
+const INPUT = 0;
+const RESULT = 1;
+
+var gUserList;
+var gKeyList;
+var gEnigLastSelectedKeys = null;
+var gKeySortList = null;
+var gSearchInput = null;
+var gTreeChildren = null;
+var gShowInvalidKeys = null;
+var gShowOthersKeys = null;
+var gTimeoutId = null;
+
+function enigmailKeyManagerLoad() {
+ EnigmailLog.DEBUG("enigmailKeyManager.js: enigmailKeyManagerLoad\n");
+
+ // Close the key manager if GnuPG is not available
+ if (!EnigmailCore.getService(window)) {
+ window.close();
+ return;
+ }
+
+ gUserList = document.getElementById("pgpKeyList");
+ gSearchInput = document.getElementById("filterKey");
+ gShowInvalidKeys = document.getElementById("showInvalidKeys");
+ gShowOthersKeys = document.getElementById("showOthersKeys");
+
+ window.addEventListener("reload-keycache", reloadKeys);
+ gSearchInput.addEventListener("keydown", event => {
+ switch (event.key) {
+ case "Escape":
+ event.target.value = "";
+ // fall through
+ case "Enter":
+ if (gTimeoutId) {
+ clearTimeout(gTimeoutId);
+ gTimeoutId = null;
+ }
+ gKeyListView.applyFilter(0);
+ event.preventDefault();
+ break;
+ default:
+ gTimeoutId = setTimeout(() => {
+ gKeyListView.applyFilter(0);
+ }, 200);
+ break;
+ }
+ });
+
+ gUserList.addEventListener("click", onListClick, true);
+ document.getElementById("statusText").value = l10n.formatValueSync(
+ "key-man-loading-keys"
+ );
+ document.getElementById("progressBar").style.visibility = "visible";
+ setTimeout(loadkeyList, 100);
+
+ gUserList.view = gKeyListView;
+ gSearchInput.focus();
+
+ // Dialog event listeners.
+ document.addEventListener("dialogaccept", onDialogAccept);
+ document.addEventListener("dialogcancel", onDialogClose);
+}
+
+function onDialogAccept() {
+ if (window.arguments[0].okCallback) {
+ window.arguments[0].okCallback();
+ }
+ window.close();
+}
+
+function onDialogClose() {
+ if (window.arguments[0].cancelCallback) {
+ window.arguments[0].cancelCallback();
+ }
+ window.close();
+}
+
+function loadkeyList() {
+ EnigmailLog.DEBUG("enigmailKeyManager.js: loadkeyList\n");
+
+ sortTree();
+ gKeyListView.applyFilter(0);
+ document.getElementById("pleaseWait").hidePopup();
+ document.getElementById("statusText").value = "";
+ document.getElementById("progressBar").style.visibility = "collapse";
+}
+
+function clearKeyCache() {
+ EnigmailKeyRing.clearCache();
+ refreshKeys();
+}
+
+function refreshKeys() {
+ EnigmailLog.DEBUG("enigmailKeyManager.js: refreshKeys\n");
+ var keyList = getSelectedKeys();
+ gEnigLastSelectedKeys = [];
+ for (var i = 0; i < keyList.length; i++) {
+ gEnigLastSelectedKeys[keyList[i]] = 1;
+ }
+
+ buildKeyList(true);
+}
+
+function reloadKeys() {
+ let i = 0;
+ let c = Components.stack;
+
+ while (c) {
+ if (c.name == "reloadKeys") {
+ i++;
+ }
+ c = c.caller;
+ }
+
+ // detect recursion and don't continue if too much recursion
+ // this can happen if the key list is empty
+ if (i < 4) {
+ buildKeyList(true);
+ }
+}
+
+function buildKeyList(refresh) {
+ EnigmailLog.DEBUG("enigmailKeyManager.js: buildKeyList\n");
+
+ var keyListObj = {};
+
+ if (refresh) {
+ EnigmailKeyRing.clearCache();
+ }
+
+ keyListObj = EnigmailKeyRing.getAllKeys(
+ window,
+ getSortColumn(),
+ getSortDirection()
+ );
+
+ if (!keyListObj.keySortList) {
+ return;
+ }
+
+ gKeyList = keyListObj.keyList;
+ gKeySortList = keyListObj.keySortList;
+
+ gKeyListView.keysRefreshed();
+}
+
+function getSelectedKeys() {
+ let selList = [];
+ let rangeCount = gUserList.view.selection.getRangeCount();
+ for (let i = 0; i < rangeCount; i++) {
+ let start = {};
+ let end = {};
+ gUserList.view.selection.getRangeAt(i, start, end);
+ for (let c = start.value; c <= end.value; c++) {
+ try {
+ //selList.push(gUserList.view.getItemAtIndex(c).getAttribute("keyNum"));
+ selList.push(gKeyListView.getFilteredRow(c).keyNum);
+ } catch (ex) {
+ return [];
+ }
+ }
+ }
+ return selList;
+}
+
+function getSelectedKeyIds() {
+ let keyList = getSelectedKeys();
+
+ let a = [];
+ for (let i in keyList) {
+ a.push(gKeyList[keyList[i]].keyId);
+ }
+
+ return a;
+}
+
+function enigmailKeyMenu() {
+ var keyList = getSelectedKeys();
+
+ let haveSecretForAll;
+ if (keyList.length == 0) {
+ haveSecretForAll = false;
+ } else {
+ haveSecretForAll = true;
+ for (let key of keyList) {
+ if (!gKeyList[key].secretAvailable) {
+ haveSecretForAll = false;
+ break;
+ }
+ }
+ }
+
+ let singleSecretSelected = keyList.length == 1 && haveSecretForAll;
+
+ // Make the selected key count available to translations.
+ for (let el of document.querySelectorAll(".enigmail-bulk-key-operation")) {
+ el.setAttribute(
+ "data-l10n-args",
+ JSON.stringify({ count: keyList.length })
+ );
+ }
+
+ document.getElementById("backupSecretKey").disabled = !haveSecretForAll;
+ document.getElementById("uploadToServer").disabled = !singleSecretSelected;
+
+ document.getElementById("revokeKey").disabled =
+ keyList.length != 1 || !gKeyList[keyList[0]].secretAvailable;
+ document.getElementById("ctxRevokeKey").hidden =
+ keyList.length != 1 || !gKeyList[keyList[0]].secretAvailable;
+
+ document.getElementById("importFromClipbrd").disabled =
+ !Services.clipboard.hasDataMatchingFlavors(
+ ["text/plain"],
+ Ci.nsIClipboard.kGlobalClipboard
+ );
+
+ for (let item of document.querySelectorAll(
+ ".requires-single-key-selection"
+ )) {
+ item.disabled = keyList.length != 1;
+ }
+
+ for (let item of document.querySelectorAll(".requires-key-selection")) {
+ item.disabled = keyList.length == 0;
+ }
+
+ // Disable the "Generate key" menu item if no mail account is available.
+ document
+ .getElementById("genKey")
+ .setAttribute("disabled", MailServices.accounts.defaultAccount == null);
+
+ // Disable the context menu if no keys are selected.
+ return keyList.length > 0;
+}
+
+function onListClick(event) {
+ if (event.detail > 2) {
+ return;
+ }
+
+ if (event.type === "click") {
+ // Mouse event
+ let { col } = gUserList.getCellAt(event.clientX, event.clientY);
+
+ if (!col) {
+ // not clicked on a valid column (e.g. scrollbar)
+ return;
+ }
+ }
+
+ if (event.detail != 2) {
+ return;
+ }
+
+ // do not propagate double clicks
+ event.stopPropagation();
+ enigmailKeyDetails();
+}
+
+function enigmailSelectAllKeys() {
+ gUserList.view.selection.selectAll();
+}
+
+/**
+ * Open the Key Properties subdialog.
+ *
+ * @param {string|null} keyId - Optional ID of the selected OpenPGP Key.
+ */
+function enigmailKeyDetails(keyId = null) {
+ if (!keyId) {
+ let keyList = getSelectedKeys();
+ // Interrupt if we don't have a single selected key nor a key was passed.
+ if (keyList.length != 1) {
+ return;
+ }
+ keyId = gKeyList[keyList[0]].keyId;
+ }
+
+ if (EnigmailWindows.openKeyDetails(window, keyId, false)) {
+ refreshKeys();
+ }
+}
+
+async function enigmailDeleteKey() {
+ var keyList = getSelectedKeys();
+ var deleteSecret = false;
+
+ if (keyList.length == 1) {
+ // one key selected
+ var userId = gKeyList[keyList[0]].userId;
+ if (gKeyList[keyList[0]].secretAvailable) {
+ if (
+ !EnigmailDialog.confirmDlg(
+ window,
+ l10n.formatValueSync("delete-secret-key", {
+ userId,
+ }),
+ l10n.formatValueSync("dlg-button-delete")
+ )
+ ) {
+ return;
+ }
+ deleteSecret = true;
+ } else if (
+ !EnigmailDialog.confirmDlg(
+ window,
+ l10n.formatValueSync("delete-pub-key", {
+ userId,
+ }),
+ l10n.formatValueSync("dlg-button-delete")
+ )
+ ) {
+ return;
+ }
+ } else {
+ // several keys selected
+ for (var i = 0; i < keyList.length; i++) {
+ if (gKeyList[keyList[i]].secretAvailable) {
+ deleteSecret = true;
+ }
+ }
+
+ if (deleteSecret) {
+ if (
+ !EnigmailDialog.confirmDlg(
+ window,
+ l10n.formatValueSync("delete-mix"),
+ l10n.formatValueSync("dlg-button-delete")
+ )
+ ) {
+ return;
+ }
+ } else if (
+ !EnigmailDialog.confirmDlg(
+ window,
+ l10n.formatValueSync("delete-selected-pub-key"),
+ l10n.formatValueSync("dlg-button-delete")
+ )
+ ) {
+ return;
+ }
+ }
+
+ const cApi = EnigmailCryptoAPI();
+ for (let j in keyList) {
+ let fpr = gKeyList[keyList[j]].fpr;
+ await cApi.deleteKey(fpr, deleteSecret);
+ await PgpSqliteDb2.deleteAcceptance(fpr);
+ }
+ clearKeyCache();
+ gUserList.view.selection.clearSelection();
+}
+
+async function enigCreateKeyMsg() {
+ var keyList = getSelectedKeyIds();
+ 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 = {};
+
+ var keyIdArray = [];
+ for (let id of keyList) {
+ keyIdArray.push("0x" + id);
+ }
+
+ await EnigmailKeyRing.extractPublicKeys(
+ keyIdArray, // full
+ null,
+ null,
+ tmpFile,
+ exitCodeObj,
+ errorMsgObj
+ );
+ if (exitCodeObj.value !== 0) {
+ EnigmailDialog.alert(window, errorMsgObj.value);
+ return;
+ }
+
+ // create attachment
+ var ioServ = Services.io;
+ var tmpFileURI = ioServ.newFileURI(tmpFile);
+ var keyAttachment = Cc[
+ "@mozilla.org/messengercompose/attachment;1"
+ ].createInstance(Ci.nsIMsgAttachment);
+ keyAttachment.url = tmpFileURI.spec;
+ if (keyList.length == 1) {
+ keyAttachment.name = "0x" + keyList[0] + ".asc";
+ } else {
+ keyAttachment.name = "pgpkeys.asc";
+ }
+ keyAttachment.temporary = true;
+ keyAttachment.contentType = "application/pgp-keys";
+
+ // create Msg
+ var msgCompFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ msgCompFields.addAttachment(keyAttachment);
+
+ var msgCompSvc = Cc["@mozilla.org/messengercompose;1"].getService(
+ Ci.nsIMsgComposeService
+ );
+
+ var msgCompParam = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ msgCompParam.composeFields = msgCompFields;
+ msgCompParam.identity = EnigmailFuncs.getDefaultIdentity();
+ msgCompParam.type = Ci.nsIMsgCompType.New;
+ msgCompParam.format = Ci.nsIMsgCompFormat.Default;
+ msgCompParam.originalMsgURI = "";
+ msgCompSvc.OpenComposeWindowWithParams("", msgCompParam);
+}
+
+async function enigmailRevokeKey() {
+ var keyList = getSelectedKeys();
+ let keyInfo = gKeyList[keyList[0]];
+ EnigRevokeKey(keyInfo, function (success) {
+ if (success) {
+ refreshKeys();
+ }
+ });
+}
+
+async function enigmailExportKeys(which) {
+ let exportSecretKey = which == "secret";
+ var keyList = getSelectedKeys();
+ var defaultFileName;
+
+ if (keyList.length == 1) {
+ let extension = exportSecretKey ? "secret.asc" : "public.asc";
+ defaultFileName = gKeyList[keyList[0]].userId.replace(/[<>]/g, "");
+ defaultFileName =
+ defaultFileName +
+ "-" +
+ `(0x${gKeyList[keyList[0]].keyId})` +
+ "-" +
+ extension;
+ } else {
+ let id = exportSecretKey
+ ? "default-pub-sec-key-filename"
+ : "default-pub-key-filename";
+ defaultFileName = l10n.formatValueSync(id) + ".asc";
+ }
+
+ if (exportSecretKey) {
+ var fprArray = [];
+ for (let id of keyList) {
+ fprArray.push(gKeyList[id].fpr);
+ }
+ EnigmailKeyRing.backupSecretKeysInteractive(
+ window,
+ defaultFileName,
+ fprArray
+ );
+ } else {
+ let keyList2 = getSelectedKeyIds();
+ var keyIdArray = [];
+ for (let id of keyList2) {
+ keyIdArray.push("0x" + id);
+ }
+ await EnigmailKeyRing.exportPublicKeysInteractive(
+ window,
+ defaultFileName,
+ keyIdArray
+ );
+ }
+}
+
+async function enigmailImportFromClipbrd() {
+ if (
+ !EnigmailDialog.confirmDlg(
+ window,
+ l10n.formatValueSync("import-from-clip"),
+ l10n.formatValueSync("key-man-button-import")
+ )
+ ) {
+ return;
+ }
+
+ let cBoardContent = await navigator.clipboard.readText();
+ var errorMsgObj = {};
+ var preview = await EnigmailKey.getKeyListFromKeyBlock(
+ cBoardContent,
+ errorMsgObj,
+ true,
+ true,
+ false
+ );
+ // should we allow importing secret keys?
+ if (preview && preview.length > 0) {
+ let confirmImport = false;
+ let outParam = {};
+ confirmImport = EnigmailDialog.confirmPubkeyImport(
+ window,
+ preview,
+ outParam
+ );
+ if (confirmImport) {
+ // import
+ EnigmailKeyRing.importKey(
+ window,
+ false,
+ cBoardContent,
+ false,
+ "",
+ errorMsgObj,
+ null,
+ false,
+ [],
+ true,
+ outParam.acceptance
+ );
+ var keyList = preview.map(function (a) {
+ return a.id;
+ });
+ EnigmailDialog.keyImportDlg(window, keyList);
+ refreshKeys();
+ }
+ } else {
+ document.l10n.formatValue("preview-failed").then(value => {
+ EnigmailDialog.alert(window, value);
+ });
+ }
+}
+
+/**
+ * Places the fingerprint of each selected key onto the keyboard.
+ */
+async function copyOpenPGPFingerPrints() {
+ let fprs = getSelectedKeys()
+ .map(idx => gKeyList[idx].fpr)
+ .join("\n");
+ return navigator.clipboard.writeText(fprs);
+}
+
+/**
+ * Places the key id of each key selected onto the clipboard.
+ */
+async function copyOpenPGPKeyIds() {
+ let ids = getSelectedKeyIds();
+ return navigator.clipboard.writeText(ids.map(id => `0x${id}`).join("\n"));
+}
+
+async function enigmailCopyToClipbrd() {
+ var keyList = getSelectedKeyIds();
+ if (keyList.length === 0) {
+ document.l10n.formatValue("no-key-selected").then(value => {
+ EnigmailDialog.info(window, value);
+ });
+ return;
+ }
+ var exitCodeObj = {};
+ var errorMsgObj = {};
+
+ var keyIdArray = [];
+ for (let id of keyList) {
+ keyIdArray.push("0x" + id);
+ }
+
+ let keyData = await EnigmailKeyRing.extractPublicKeys(
+ keyIdArray, // full
+ null,
+ null,
+ null,
+ exitCodeObj,
+ errorMsgObj
+ );
+ if (exitCodeObj.value !== 0) {
+ l10n.formatValue("copy-to-clipbrd-failed").then(value => {
+ EnigmailDialog.alert(window, value);
+ });
+ return;
+ }
+ navigator.clipboard
+ .writeText(keyData)
+ .then(() => {
+ l10n.formatValue("copy-to-clipbrd-ok").then(value => {
+ EnigmailDialog.info(window, value);
+ });
+ })
+ .catch(err => {
+ l10n.formatValue("copy-to-clipbrd-failed").then(value => {
+ EnigmailDialog.alert(window, value);
+ });
+ });
+}
+
+async function enigmailSearchKey() {
+ var result = {
+ value: "",
+ };
+ if (
+ !Services.prompt.prompt(
+ window,
+ l10n.formatValueSync("enig-prompt"),
+ l10n.formatValueSync("openpgp-key-man-discover-prompt"),
+ result,
+ "",
+ {}
+ )
+ ) {
+ return;
+ }
+
+ result.value = result.value.trim();
+
+ let imported = false;
+ if (EnigmailFuncs.stringLooksLikeEmailAddress(result.value)) {
+ imported = await KeyLookupHelper.lookupAndImportByEmail(
+ "interactive-import",
+ window,
+ result.value,
+ true
+ );
+ } else {
+ imported = await KeyLookupHelper.lookupAndImportByKeyID(
+ "interactive-import",
+ window,
+ result.value,
+ true
+ );
+ }
+
+ if (imported) {
+ refreshKeys();
+ }
+}
+
+async function enigmailUploadKey() {
+ // Always upload to the first configured keyserver with a supported protocol.
+ let selKeyList = getSelectedKeys();
+ if (selKeyList.length != 1) {
+ return;
+ }
+
+ let keyId = gKeyList[selKeyList[0]].keyId;
+ let ks = EnigmailKeyserverURIs.getUploadKeyServer();
+
+ let ok = await EnigmailKeyServer.upload(keyId, ks);
+ document.l10n
+ .formatValue(ok ? "openpgp-key-publish-ok" : "openpgp-key-publish-fail", {
+ keyserver: ks,
+ })
+ .then(value => {
+ EnigmailDialog.alert(window, value);
+ });
+}
+
+/*
+function enigmailUploadToWkd() {
+ let selKeyList = getSelectedKeys();
+ let keyList = [];
+ for (let i = 0; i < selKeyList.length; i++) {
+ keyList.push(gKeyList[selKeyList[i]]);
+ }
+
+ EnigmailWks.wksUpload(keyList, window)
+ .then(result => {
+ if (result.length > 0) {
+ EnigmailDialog.info(window, "Key(s) sent successfully");
+ } else if (keyList.length === 1) {
+ EnigmailDialog.alert(
+ window,
+ "Sending of keys failed" +
+ "\n\n" +
+ "The key %S does not have a WKS identity.".replace("%S", keyList[0].userId)
+ );
+ } else {
+ EnigmailDialog.alert(
+ window,
+ "The upload was not successful - your provider does not seem to support WKS."
+ );
+ }
+ })
+ .catch(error => {
+ EnigmailDialog.alert(
+ "Sending of keys failed" + "\n" + error
+ );
+ });
+}
+*/
+
+function enigmailImportKeysFromUrl() {
+ var result = {
+ value: "",
+ };
+ if (
+ !Services.prompt.prompt(
+ window,
+ l10n.formatValueSync("enig-prompt"),
+ l10n.formatValueSync("import-from-url"),
+ result,
+ "",
+ {}
+ )
+ ) {
+ return;
+ }
+ var p = new Promise(function (resolve, reject) {
+ var cbFunc = async function (data) {
+ EnigmailLog.DEBUG("enigmailImportKeysFromUrl: _cbFunc()\n");
+ var errorMsgObj = {};
+
+ var preview = await EnigmailKey.getKeyListFromKeyBlock(
+ data,
+ errorMsgObj,
+ true,
+ true,
+ false
+ );
+ // should we allow importing secret keys?
+ if (preview && preview.length > 0) {
+ let confirmImport = false;
+ let outParam = {};
+ confirmImport = EnigmailDialog.confirmPubkeyImport(
+ window,
+ preview,
+ outParam
+ );
+ if (confirmImport) {
+ EnigmailKeyRing.importKey(
+ window,
+ false,
+ data,
+ false,
+ "",
+ errorMsgObj,
+ null,
+ false,
+ [],
+ true,
+ outParam.acceptance
+ );
+ errorMsgObj.preview = preview;
+ resolve(errorMsgObj);
+ }
+ } else {
+ EnigmailDialog.alert(
+ window,
+ await document.l10n.formatValue("preview-failed")
+ );
+ }
+ };
+
+ try {
+ var bufferListener = EnigmailStreams.newStringStreamListener(cbFunc);
+ var msgUri = Services.io.newURI(result.value.trim());
+
+ var channel = EnigmailStreams.createChannel(msgUri);
+ channel.asyncOpen(bufferListener, msgUri);
+ } catch (ex) {
+ var err = {
+ value: ex,
+ };
+ reject(err);
+ }
+ });
+
+ p.then(function (errorMsgObj) {
+ var keyList = errorMsgObj.preview.map(function (a) {
+ return a.id;
+ });
+ EnigmailDialog.keyImportDlg(window, keyList);
+ refreshKeys();
+ }).catch(async function (reason) {
+ EnigmailDialog.alert(
+ window,
+ await document.l10n.formatValue("general-error", {
+ reason: reason.value,
+ })
+ );
+ });
+}
+
+function initiateAcKeyTransfer() {
+ EnigmailWindows.inititateAcSetupMessage();
+}
+
+//
+// ----- key filtering functionality -----
+//
+
+function determineHiddenKeys(keyObj, showInvalidKeys, showOthersKeys) {
+ var show = true;
+
+ const INVALID_KEYS = "ierdD";
+
+ if (
+ !showInvalidKeys &&
+ INVALID_KEYS.includes(EnigmailTrust.getTrustCode(keyObj))
+ ) {
+ show = false;
+ }
+
+ if (!showOthersKeys && !keyObj.secretAvailable) {
+ show = false;
+ }
+
+ return show;
+}
+
+function getSortDirection() {
+ return gUserList.getAttribute("sortDirection") == "ascending" ? 1 : -1;
+}
+
+function sortTree(column) {
+ var columnName;
+ var order = getSortDirection();
+
+ //if the column is passed and it's already sorted by that column, reverse sort
+ if (column) {
+ columnName = column.id;
+ if (gUserList.getAttribute("sortResource") == columnName) {
+ order *= -1;
+ } else {
+ document
+ .getElementById(gUserList.getAttribute("sortResource"))
+ .removeAttribute("sortDirection");
+ order = 1;
+ }
+ } else {
+ columnName = gUserList.getAttribute("sortResource");
+ }
+ gUserList.setAttribute(
+ "sortDirection",
+ order == 1 ? "ascending" : "descending"
+ );
+ let col = document.getElementById(columnName);
+ if (col) {
+ col.setAttribute("sortDirection", order == 1 ? "ascending" : "descending");
+ gUserList.setAttribute("sortResource", columnName);
+ } else {
+ gUserList.setAttribute("sortResource", "enigUserNameCol");
+ }
+ buildKeyList(false);
+}
+
+function getSortColumn() {
+ switch (gUserList.getAttribute("sortResource")) {
+ case "enigUserNameCol":
+ return "userid";
+ case "keyCol":
+ return "keyid";
+ case "createdCol":
+ return "created";
+ case "expCol":
+ return "expiry";
+ case "fprCol":
+ return "fpr";
+ default:
+ return "?";
+ }
+}
+
+/**
+ * Open the OpenPGP Key Wizard to generate a new key or import secret keys.
+ *
+ * @param {boolean} isImport - If the keyWizard should automatically switch to
+ * the import or create screen as requested by the user.
+ */
+function openKeyWizard(isImport = false) {
+ let args = {
+ gSubDialog: null,
+ cancelCallback: clearKeyCache,
+ okCallback: clearKeyCache,
+ okImportCallback: clearKeyCache,
+ okExternalCallback: clearKeyCache,
+ keyDetailsDialog: enigmailKeyDetails,
+ isCreate: !isImport,
+ isImport,
+ };
+
+ window.browsingContext.topChromeWindow.openDialog(
+ "chrome://openpgp/content/ui/keyWizard.xhtml",
+ "enigmail:KeyWizard",
+ "dialog,modal,centerscreen,resizable",
+ args
+ );
+}
+
+/***************************** TreeView for user list ***********************************/
+/**
+ * gKeyListView implements the nsITreeView interface for the displayed list.
+ *
+ * For speed reasons, we use two lists:
+ * - keyViewList: contains the full list of pointers to all keys and rows that are
+ * potentially displayed ordered according to the sort column
+ * - keyFilterList: contains the indexes to keyViewList of the keys that are displayed
+ * according to the current filter criteria.
+ */
+var gKeyListView = {
+ keyViewList: [],
+ keyFilterList: [],
+
+ //// nsITreeView implementation
+
+ rowCount: 0,
+ selection: null,
+
+ canDrop(index, orientation, dataTransfer) {
+ return false;
+ },
+
+ cycleCell(row, col) {},
+ cycleHeader(col) {},
+ drop(row, orientation, dataTransfer) {},
+
+ getCellProperties(row, col) {
+ let r = this.getFilteredRow(row);
+ if (!r) {
+ return "";
+ }
+
+ let keyObj = gKeyList[r.keyNum];
+ if (!keyObj) {
+ return "";
+ }
+
+ let keyTrustStyle = "";
+
+ switch (r.rowType) {
+ case "key":
+ case "uid":
+ switch (keyObj.keyTrust) {
+ case "q":
+ keyTrustStyle = "enigmail_keyValid_unknown";
+ break;
+ case "r":
+ keyTrustStyle = "enigmail_keyValid_revoked";
+ break;
+ case "e":
+ keyTrustStyle = "enigmail_keyValid_expired";
+ break;
+ case "n":
+ keyTrustStyle = "enigmail_keyTrust_untrusted";
+ break;
+ case "m":
+ keyTrustStyle = "enigmail_keyTrust_marginal";
+ break;
+ case "f":
+ keyTrustStyle = "enigmail_keyTrust_full";
+ break;
+ case "u":
+ keyTrustStyle = "enigmail_keyTrust_ultimate";
+ break;
+ case "-":
+ keyTrustStyle = "enigmail_keyTrust_unknown";
+ break;
+ default:
+ keyTrustStyle = "enigmail_keyTrust_unknown";
+ break;
+ }
+
+ if (
+ keyObj.keyTrust.length > 0 &&
+ ENIG_KEY_NOT_VALID.includes(keyObj.keyTrust.charAt(0))
+ ) {
+ keyTrustStyle += " enigKeyInactive";
+ }
+
+ if (r.rowType === "key" && keyObj.secretAvailable) {
+ keyTrustStyle += " enigmailOwnKey";
+ }
+ break;
+ }
+
+ return keyTrustStyle;
+ },
+
+ getCellText(row, col) {
+ let r = this.getFilteredRow(row);
+ if (!r) {
+ return "";
+ }
+ let keyObj = gKeyList[r.keyNum];
+ if (!keyObj) {
+ return "???";
+ }
+
+ switch (r.rowType) {
+ case "key":
+ switch (col.id) {
+ case "enigUserNameCol":
+ return keyObj.userId;
+ case "keyCol":
+ return `0x${keyObj.keyId}`;
+ case "createdCol":
+ return keyObj.created;
+ case "expCol":
+ return keyObj.effectiveExpiry;
+ case "fprCol":
+ return keyObj.fprFormatted;
+ }
+ break;
+ case "uid":
+ switch (col.id) {
+ case "enigUserNameCol":
+ return keyObj.userIds[r.uidNum].userId;
+ }
+ break;
+ }
+
+ return "";
+ },
+ getCellValue(row, col) {
+ return "";
+ },
+ getColumnProperties(col) {
+ return "";
+ },
+
+ getImageSrc(row, col) {
+ let r = this.getFilteredRow(row);
+ if (!r) {
+ return null;
+ }
+ //let keyObj = gKeyList[r.keyNum];
+
+ return null;
+ },
+
+ /**
+ * indentation level for rows
+ */
+ getLevel(row) {
+ let r = this.getFilteredRow(row);
+ if (!r) {
+ return 0;
+ }
+
+ switch (r.rowType) {
+ case "key":
+ return 0;
+ case "uid":
+ return 1;
+ }
+
+ return 0;
+ },
+
+ getParentIndex(idx) {
+ return -1;
+ },
+ getProgressMode(row, col) {},
+
+ getRowProperties(row) {
+ return "";
+ },
+ hasNextSibling(rowIndex, afterIndex) {
+ return false;
+ },
+ isContainer(row) {
+ let r = this.getFilteredRow(row);
+ if (!r) {
+ return false;
+ }
+ switch (r.rowType) {
+ case "key":
+ return true;
+ }
+
+ return false;
+ },
+ isContainerEmpty(row) {
+ let r = this.getFilteredRow(row);
+ if (!r) {
+ return true;
+ }
+ switch (r.rowType) {
+ case "key":
+ return !r.hasSubUID;
+ }
+ return true;
+ },
+ isContainerOpen(row) {
+ return this.getFilteredRow(row).isOpen;
+ },
+ isEditable(row, col) {
+ return false;
+ },
+ isSelectable(row, col) {
+ return true;
+ },
+ isSeparator(index) {
+ return false;
+ },
+ isSorted() {
+ return false;
+ },
+ performAction(action) {},
+ performActionOnCell(action, row, col) {},
+ performActionOnRow(action, row) {},
+ selectionChanged() {},
+ // void setCellText(in long row, in nsITreeColumn col, in AString value);
+ // void setCellValue(in long row, in nsITreeColumn col, in AString value);
+ setTree(treebox) {
+ this.treebox = treebox;
+ },
+
+ toggleOpenState(row) {
+ let r = this.getFilteredRow(row);
+ if (!r) {
+ return;
+ }
+ let realRow = this.keyFilterList[row];
+ switch (r.rowType) {
+ case "key":
+ if (r.isOpen) {
+ let i = 0;
+ while (
+ this.getFilteredRow(row + 1 + i) &&
+ this.getFilteredRow(row + 1 + i).keyNum === r.keyNum
+ ) {
+ ++i;
+ }
+
+ this.keyViewList.splice(realRow + 1, i);
+ r.isOpen = false;
+ this.applyFilter(row);
+ } else {
+ this.appendUids("uid", r.keyNum, realRow, this.keyViewList[row]);
+
+ r.isOpen = true;
+ this.applyFilter(row);
+ }
+ break;
+ }
+ },
+
+ /**
+ * add UIDs for a given key to key view
+ *
+ * @param uidType: String - one of uid (user ID), uat (photo)
+ * @param keyNum: Number - index of key in gKeyList
+ * @param realRow: Number - index of row in keyViewList (i.e. without filter)
+ *
+ * @returns Number: number of UIDs added
+ */
+ appendUids(uidType, keyNum, realRow, parentRow) {
+ let keyObj = gKeyList[keyNum];
+ let uidAdded = 0;
+
+ for (let i = 0; i < keyObj.userIds.length; i++) {
+ if (keyObj.userIds[i].type === uidType) {
+ if (keyObj.userIds[i].userId == keyObj.userId) {
+ continue;
+ }
+ ++uidAdded;
+ this.keyViewList.splice(realRow + uidAdded, 0, {
+ rowType: uidType,
+ keyNum,
+ parent: parentRow,
+ uidNum: i,
+ });
+ }
+ }
+
+ return uidAdded;
+ },
+
+ /**
+ * Reload key list entirely
+ */
+ keysRefreshed() {
+ this.keyViewList = [];
+ this.keyFilterList = [];
+ for (let i = 0; i < gKeySortList.length; i++) {
+ this.keyViewList.push({
+ row: i,
+ rowType: "key",
+ fpr: gKeySortList[i].fpr,
+ keyNum: gKeySortList[i].keyNum,
+ isOpen: false,
+ hasSubUID: gKeyList[gKeySortList[i].keyNum].userIds.length > 1,
+ });
+ }
+
+ this.applyFilter(0);
+ let oldRowCount = this.rowCount;
+ this.rowCount = this.keyViewList.length;
+ gUserList.rowCountChanged(0, this.rowCount - oldRowCount);
+ },
+
+ /**
+ * If no search term is entered, decide which keys to display
+ *
+ * @returns array of keyNums (= display some keys) or null (= display ALL keys)
+ */
+ showOrHideAllKeys() {
+ var showInvalidKeys = gShowInvalidKeys.getAttribute("checked") == "true";
+ var showOthersKeys = gShowOthersKeys.getAttribute("checked") == "true";
+
+ document.getElementById("nothingFound").hidePopup();
+
+ if (showInvalidKeys && showOthersKeys) {
+ return null;
+ }
+
+ let keyShowList = [];
+ for (let i = 0; i < gKeyList.length; i++) {
+ if (determineHiddenKeys(gKeyList[i], showInvalidKeys, showOthersKeys)) {
+ keyShowList.push(i);
+ }
+ }
+
+ return keyShowList;
+ },
+
+ /**
+ * Search for keys that match filter criteria
+ *
+ * @returns array of keyNums (= display some keys) or null (= display ALL keys)
+ */
+ getFilteredKeys() {
+ let searchTxt = gSearchInput.value;
+
+ if (!searchTxt || searchTxt.length === 0) {
+ return this.showOrHideAllKeys();
+ }
+
+ if (!gKeyList) {
+ return [];
+ }
+ let showInvalidKeys = gShowInvalidKeys.getAttribute("checked") == "true";
+ let showOthersKeys = gShowOthersKeys.getAttribute("checked") == "true";
+
+ // skip leading 0x in case we search for a key:
+ if (searchTxt.length > 2 && searchTxt.substr(0, 2).toLowerCase() == "0x") {
+ searchTxt = searchTxt.substr(2);
+ }
+
+ searchTxt = searchTxt.toLowerCase();
+ searchTxt = searchTxt.replace(/^(\s*)(.*)/, "$2").replace(/\s+$/, ""); // trim spaces
+
+ // check if we search for a full fingerprint (with optional spaces every 4 letters)
+ var fpr = null;
+ if (searchTxt.length == 49) {
+ // possible fingerprint with spaces?
+ if (
+ searchTxt.search(/^[0-9a-f ]*$/) >= 0 &&
+ searchTxt[4] == " " &&
+ searchTxt[9] == " " &&
+ searchTxt[14] == " " &&
+ searchTxt[19] == " " &&
+ searchTxt[24] == " " &&
+ searchTxt[29] == " " &&
+ searchTxt[34] == " " &&
+ searchTxt[39] == " " &&
+ searchTxt[44] == " "
+ ) {
+ fpr = searchTxt.replace(/ /g, "");
+ }
+ } else if (searchTxt.length == 40) {
+ // possible fingerprint without spaces
+ if (searchTxt.search(/^[0-9a-f ]*$/) >= 0) {
+ fpr = searchTxt;
+ }
+ }
+
+ let keyShowList = [];
+
+ for (let i = 0; i < gKeyList.length; i++) {
+ let keyObj = gKeyList[i];
+ let uid = keyObj.userId;
+ let showKey = false;
+
+ // does a user ID (partially) match?
+ for (let idx = 0; idx < keyObj.userIds.length; idx++) {
+ uid = keyObj.userIds[idx].userId;
+ if (uid.toLowerCase().includes(searchTxt)) {
+ showKey = true;
+ }
+ }
+
+ // does the full fingerprint (without spaces) match?
+ // - no partial match check because this is special for the collapsed spaces inside the fingerprint
+ if (showKey === false && fpr && keyObj.fpr.toLowerCase() == fpr) {
+ showKey = true;
+ }
+ // does the fingerprint (partially) match?
+ if (showKey === false && keyObj.fpr.toLowerCase().includes(searchTxt)) {
+ showKey = true;
+ }
+ // does a sub key of (partially) match?
+ if (showKey === false) {
+ for (
+ let subKeyIdx = 0;
+ subKeyIdx < keyObj.subKeys.length;
+ subKeyIdx++
+ ) {
+ let subkey = keyObj.subKeys[subKeyIdx].keyId;
+ if (subkey.toLowerCase().includes(searchTxt)) {
+ showKey = true;
+ }
+ }
+ }
+ // take option to show invalid/untrusted... keys into account
+ if (
+ showKey &&
+ determineHiddenKeys(keyObj, showInvalidKeys, showOthersKeys)
+ ) {
+ keyShowList.push(i);
+ }
+ }
+
+ return keyShowList;
+ },
+
+ /**
+ * Trigger re-displaying the list of keys and apply a filter
+ *
+ * @param selectedRow: Number - the row that is currently selected or
+ * clicked on
+ */
+ applyFilter(selectedRow) {
+ let keyDisplayList = this.getFilteredKeys();
+
+ this.keyFilterList = [];
+ if (keyDisplayList === null) {
+ for (let i = 0; i < this.keyViewList.length; i++) {
+ this.keyFilterList.push(i);
+ }
+
+ this.adjustRowCount(this.keyViewList.length, selectedRow);
+ } else {
+ for (let i = 0; i < this.keyViewList.length; i++) {
+ if (keyDisplayList.includes(this.keyViewList[i].keyNum)) {
+ this.keyFilterList.push(i);
+ }
+ }
+
+ this.adjustRowCount(this.keyFilterList.length, selectedRow);
+ }
+ },
+
+ /**
+ * Re-calculate the row count and instruct the view to update
+ */
+ adjustRowCount(newRowCount, selectedRow) {
+ if (this.rowCount === newRowCount) {
+ gUserList.invalidate();
+ return;
+ }
+
+ let delta = newRowCount - this.rowCount;
+ this.rowCount = newRowCount;
+ gUserList.rowCountChanged(selectedRow, delta);
+ },
+
+ /**
+ * Determine the row object from the a filtered row number
+ *
+ * @param row: Number - row number of displayed (=filtered) list
+ * @returns Object: keyViewList entry of corresponding row
+ */
+ getFilteredRow(row) {
+ let r = this.keyFilterList[row];
+ if (r !== undefined) {
+ return this.keyViewList[r];
+ }
+ return null;
+ },
+
+ treebox: null,
+};
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.xhtml b/comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.xhtml
new file mode 100644
index 0000000000..3c66224b8f
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.xhtml
@@ -0,0 +1,406 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/enigmail.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window>
+
+<window
+ id="enigmailKeyManager"
+ data-l10n-id="openpgp-key-man-title"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ onload="enigmailKeyManagerLoad();"
+ height="450"
+ width="700"
+ style="min-height: 450px"
+>
+ <dialog
+ id="openPgpKeyManagerDialog"
+ data-l10n-id="openpgp-card-details-close-window-label"
+ data-l10n-attrs="buttonlabelaccept"
+ buttons="accept"
+ >
+ <script
+ type="application/x-javascript"
+ src="chrome://openpgp/content/ui/enigmailCommon.js"
+ />
+ <script
+ type="application/x-javascript"
+ src="chrome://openpgp/content/ui/enigmailKeyManager.js"
+ />
+ <script
+ type="application/x-javascript"
+ src="chrome://openpgp/content/ui/commonWorkflows.js"
+ />
+ <script
+ type="application/x-javascript"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ />
+
+ <linkset>
+ <html:link rel="localization" href="messenger/openpgp/openpgp.ftl" />
+ </linkset>
+
+ <commandset id="tasksCommands" />
+
+ <command id="cmd_close" oncommand="window.close()" />
+ <command id="cmd_enigmailDeleteKey" oncommand="enigmailDeleteKey()" />
+
+ <keyset id="winKeys">
+ <key
+ id="key_selectAll"
+ data-l10n-id="openpgp-key-man-select-all-key"
+ oncommand="enigmailSelectAllKeys()"
+ modifiers="accel"
+ />
+
+ <key
+ id="key_keyDetails"
+ data-l10n-id="openpgp-key-man-key-details-key"
+ oncommand="enigmailKeyDetails()"
+ modifiers="accel"
+ />
+
+ <key
+ id="key_enigDelete"
+ keycode="VK_DELETE"
+ command="cmd_enigmailDeleteKey"
+ />
+ <key id="key_close" />
+ <key id="key_quit" />
+ </keyset>
+
+ <toolbar type="menubar" style="margin-inline: -8px -10px; margin-top: -8px">
+ <menubar id="main-menubar">
+ <menu id="menu_File" data-l10n-id="openpgp-key-man-file-menu">
+ <menupopup id="menu_FilePopup" onpopupshowing="enigmailKeyMenu();">
+ <menuitem
+ id="importPubFromFile"
+ data-l10n-id="openpgp-key-man-import-public-from-file"
+ oncommand="EnigmailCommon_importObjectFromFile('pub');"
+ />
+ <menuitem
+ id="importSecFromFile"
+ data-l10n-id="openpgp-key-man-import-secret-from-file"
+ oncommand="openKeyWizard(true)"
+ />
+ <menuitem
+ id="importSigFromFile"
+ data-l10n-id="openpgp-key-man-import-sig-from-file"
+ oncommand="EnigmailCommon_importObjectFromFile('rev');"
+ />
+ <menuseparator />
+ <menuitem
+ id="exportPublicKey"
+ class="requires-key-selection"
+ data-l10n-id="openpgp-key-man-export-to-file"
+ oncommand="enigmailExportKeys('public');"
+ />
+ <menuitem
+ id="sendKey"
+ data-l10n-id="openpgp-key-man-send-keys"
+ class="requires-key-selection"
+ oncommand="enigCreateKeyMsg();"
+ />
+ <menuseparator />
+ <menuitem
+ id="backupSecretKey"
+ data-l10n-id="openpgp-key-man-backup-secret-keys"
+ oncommand="enigmailExportKeys('secret');"
+ />
+ <menuseparator />
+ <menuitem
+ id="refreshKeys"
+ data-l10n-id="openpgp-key-man-reload"
+ oncommand="clearKeyCache();"
+ />
+ <!-- add Close and Exit menu items -->
+ <menuitem
+ id="menu_close"
+ data-l10n-id="openpgp-key-man-close"
+ oncommand="onDialogClose()"
+ />
+ </menupopup>
+ </menu>
+
+ <menu data-l10n-id="openpgp-key-man-edit-menu">
+ <menupopup onpopupshowing="enigmailKeyMenu();">
+ <menuitem
+ id="importFromClipbrd"
+ data-l10n-id="openpgp-key-man-import-from-clipbrd"
+ oncommand="enigmailImportFromClipbrd();"
+ />
+ <menuitem
+ id="importFromUrl"
+ data-l10n-id="openpgp-key-man-import-from-url"
+ oncommand="enigmailImportKeysFromUrl();"
+ />
+ <menuitem
+ id="copyFprs"
+ data-l10n-id="openpgp-key-man-copy-fprs"
+ data-l10n-args='{"count": 0}'
+ class="requires-key-selection enigmail-bulk-key-operation"
+ oncommand="copyOpenPGPFingerPrints()"
+ />
+ <menuitem
+ id="copyKeyIds"
+ data-l10n-id="openpgp-key-man-copy-key-ids"
+ data-l10n-args='{"count": 0}'
+ class="requires-key-selection enigmail-bulk-key-operation"
+ oncommand="copyOpenPGPKeyIds()"
+ />
+ <menuitem
+ id="copyToClipbrd"
+ data-l10n-id="openpgp-key-man-copy-to-clipboard"
+ data-l10n-args='{"count": 0}'
+ class="requires-key-selection enigmail-bulk-key-operation"
+ oncommand="enigmailCopyToClipbrd();"
+ />
+ <menuseparator />
+
+ <menuitem
+ id="revokeKey"
+ data-l10n-id="openpgp-key-man-revoke-key"
+ oncommand="enigmailRevokeKey()"
+ />
+
+ <menuitem
+ id="deleteKey"
+ data-l10n-id="openpgp-key-man-del-key"
+ key="key_enigDelete"
+ class="requires-key-selection"
+ oncommand="enigmailDeleteKey();"
+ />
+
+ <menuseparator />
+
+ <menuitem
+ id="selectAll"
+ data-l10n-id="openpgp-key-man-select-all"
+ oncommand="enigmailSelectAllKeys()"
+ key="key_selectAll"
+ />
+ </menupopup>
+ </menu>
+
+ <menu id="viewMenu" data-l10n-id="openpgp-key-man-view-menu">
+ <menupopup onpopupshowing="enigmailKeyMenu()">
+ <!-- view menu -->
+ <menuitem
+ id="keyDetails"
+ data-l10n-id="openpgp-key-man-key-props"
+ class="requires-single-key-selection"
+ key="key_keyDetails"
+ oncommand="enigmailKeyDetails();"
+ />
+ <menuseparator />
+ <menuitem
+ id="showInvalidKeys"
+ data-l10n-id="openpgp-key-man-show-invalid-keys"
+ type="checkbox"
+ checked="true"
+ persist="checked"
+ oncommand="applyFilter();"
+ />
+ <menuitem
+ id="showOthersKeys"
+ data-l10n-id="openpgp-key-man-show-others-keys"
+ type="checkbox"
+ checked="true"
+ persist="checked"
+ oncommand="applyFilter();"
+ />
+ </menupopup>
+ </menu>
+
+ <menu id="keyserverMenu" data-l10n-id="openpgp-key-man-keyserver-menu">
+ <menupopup onpopupshowing="enigmailKeyMenu()">
+ <menuitem
+ id="importFromServer"
+ data-l10n-id="openpgp-key-man-discover-cmd"
+ oncommand="enigmailSearchKey()"
+ />
+ <menuitem
+ id="uploadToServer"
+ data-l10n-id="openpgp-key-man-publish-cmd"
+ oncommand="enigmailUploadKey()"
+ />
+ </menupopup>
+ </menu>
+
+ <menu id="generateMenu" data-l10n-id="openpgp-key-man-generate-menu">
+ <menupopup onpopupshowing="enigmailKeyMenu();">
+ <!-- generate menu -->
+ <menuitem
+ id="genKey"
+ data-l10n-id="openpgp-key-man-generate"
+ oncommand="openKeyWizard()"
+ />
+ </menupopup>
+ </menu>
+ </menubar>
+ </toolbar>
+
+ <popupset>
+ <menupopup id="ctxmenu" onpopupshowing="return enigmailKeyMenu();">
+ <menu id="ctxmenu-copy" data-l10n-id="openpgp-key-man-ctx-copy">
+ <menupopup id="ctxmenu-copy-popup">
+ <menuitem
+ id="ctxCopyFprs"
+ data-l10n-id="openpgp-key-man-ctx-copy-fprs"
+ data-l10n-args='{"count": 0}'
+ class="requires-key-selection enigmail-bulk-key-operation"
+ oncommand="copyOpenPGPFingerPrints()"
+ />
+ <menuitem
+ id="ctxCopyKeyIds"
+ data-l10n-id="openpgp-key-man-ctx-copy-key-ids"
+ data-l10n-args='{"count": 0}'
+ class="requires-key-selection enigmail-bulk-key-operation"
+ oncommand="copyOpenPGPKeyIds()"
+ />
+ <menuitem
+ id="ctxCopyPublicKeys"
+ data-l10n-id="openpgp-key-man-ctx-copy-public-keys"
+ data-l10n-args='{"count": 0}'
+ class="requires-key-selection enigmail-bulk-key-operation"
+ oncommand="enigmailCopyToClipbrd()"
+ />
+ </menupopup>
+ </menu>
+ <menuitem
+ data-l10n-id="openpgp-key-man-export-to-file"
+ id="ctxExport"
+ oncommand="enigmailExportKeys('public')"
+ />
+ <menuitem
+ data-l10n-id="openpgp-key-man-send-keys"
+ id="ctxSendKey"
+ oncommand="enigCreateKeyMsg()"
+ />
+
+ <menuseparator />
+
+ <menuitem
+ id="ctxRevokeKey"
+ data-l10n-id="openpgp-key-man-revoke-key"
+ oncommand="enigmailRevokeKey()"
+ />
+ <menuitem
+ id="ctxDeleteKey"
+ data-l10n-id="openpgp-key-man-del-key"
+ class="requires-key-selection"
+ oncommand="enigmailDeleteKey()"
+ />
+ <menuitem
+ id="ctxDetails"
+ data-l10n-id="openpgp-key-man-key-props"
+ class="requires-single-key-selection"
+ oncommand="enigmailKeyDetails()"
+ />
+ </menupopup>
+ </popupset>
+
+ <separator class="thin" />
+
+ <hbox flex="0" align="center">
+ <html:input
+ id="filterKey"
+ size="35"
+ data-l10n-id="openpgp-key-man-filter-label"
+ />
+ </hbox>
+
+ <tooltip
+ id="nothingFound"
+ data-l10n-id="openpgp-key-man-nothing-found-tooltip"
+ noautohide="true"
+ />
+ <tooltip
+ id="pleaseWait"
+ data-l10n-id="openpgp-key-man-please-wait-tooltip"
+ noautohide="true"
+ />
+
+ <separator class="thin" />
+
+ <hbox flex="1" style="min-height: 300px">
+ <tree
+ id="pgpKeyList"
+ flex="1"
+ enableColumnDrag="true"
+ seltype="multiple"
+ persist="sortDirection sortResource"
+ sortDirection="ascending"
+ sortResource="enigUserNameCol"
+ hidecolumnpicker="false"
+ context="ctxmenu"
+ >
+ <treecols>
+ <treecol
+ id="enigUserNameCol"
+ primary="true"
+ class="sortDirectionIndicator"
+ onclick="sortTree(this)"
+ data-l10n-id="openpgp-key-man-user-id-label"
+ style="width: 400px; flex: 1 auto"
+ persist="width ordinal hidden"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="keyCol"
+ style="width: 100px; flex: 1 auto"
+ data-l10n-id="openpgp-key-id-label"
+ class="sortDirectionIndicator"
+ onclick="sortTree(this)"
+ persist="width ordinal hidden"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="createdCol"
+ style="width: 70px; flex: 1 auto"
+ data-l10n-id="openpgp-key-created-label"
+ class="sortDirectionIndicator"
+ onclick="sortTree(this)"
+ persist="width ordinal hidden"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="expCol"
+ style="width: 70px; flex: 1 auto"
+ data-l10n-id="openpgp-key-expiry-label"
+ class="sortDirectionIndicator"
+ onclick="sortTree(this)"
+ persist="width ordinal hidden"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="fprCol"
+ style="width: 70px; flex: 1 auto"
+ data-l10n-id="openpgp-key-man-fingerprint-label"
+ class="sortDirectionIndicator"
+ onclick="sortTree(this)"
+ hidden="true"
+ persist="width ordinal hidden"
+ />
+ </treecols>
+
+ <treechildren id="pgpKeyListChildren" properties="" />
+ </tree>
+ </hbox>
+
+ <hbox id="statusLine">
+ <label id="statusText" value="" />
+ <html:progress id="progressBar" style="visibility: collapsed" />
+ </hbox>
+ </dialog>
+</window>
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailMessengerOverlay.js b/comm/mail/extensions/openpgp/content/ui/enigmailMessengerOverlay.js
new file mode 100644
index 0000000000..d62c676d25
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailMessengerOverlay.js
@@ -0,0 +1,3460 @@
+/* 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 ../../../../base/content/aboutMessage.js */
+/* import-globals-from ../../../../base/content/msgHdrView.js */
+/* import-globals-from ../../../../base/content/msgSecurityPane.js */
+
+// TODO: check if this is safe
+/* eslint-disable no-unsanitized/property */
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ CollectedKeysDB: "chrome://openpgp/content/modules/CollectedKeysDB.jsm",
+ EnigmailArmor: "chrome://openpgp/content/modules/armor.jsm",
+ EnigmailCryptoAPI: "chrome://openpgp/content/modules/cryptoAPI.jsm",
+ EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
+ EnigmailCore: "chrome://openpgp/content/modules/core.jsm",
+ EnigmailData: "chrome://openpgp/content/modules/data.jsm",
+ EnigmailDecryption: "chrome://openpgp/content/modules/decryption.jsm",
+ EnigmailDialog: "chrome://openpgp/content/modules/dialog.jsm",
+ EnigmailFixExchangeMsg: "chrome://openpgp/content/modules/fixExchangeMsg.jsm",
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ EnigmailKey: "chrome://openpgp/content/modules/key.jsm",
+ EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
+ EnigmailKeyServer: "chrome://openpgp/content/modules/keyserver.jsm",
+ EnigmailKeyserverURIs: "chrome://openpgp/content/modules/keyserverUris.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ EnigmailMime: "chrome://openpgp/content/modules/mime.jsm",
+ EnigmailMsgRead: "chrome://openpgp/content/modules/msgRead.jsm",
+ EnigmailPersistentCrypto:
+ "chrome://openpgp/content/modules/persistentCrypto.jsm",
+ EnigmailSingletons: "chrome://openpgp/content/modules/singletons.jsm",
+ EnigmailStreams: "chrome://openpgp/content/modules/streams.jsm",
+ EnigmailTrust: "chrome://openpgp/content/modules/trust.jsm",
+ EnigmailURIs: "chrome://openpgp/content/modules/uris.jsm",
+ EnigmailVerify: "chrome://openpgp/content/modules/mimeVerify.jsm",
+ EnigmailWindows: "chrome://openpgp/content/modules/windows.jsm",
+ // EnigmailWks: "chrome://openpgp/content/modules/webKey.jsm",
+ KeyLookupHelper: "chrome://openpgp/content/modules/keyLookupHelper.jsm",
+ MailStringUtils: "resource:///modules/MailStringUtils.jsm",
+ PgpSqliteDb2: "chrome://openpgp/content/modules/sqliteDb.jsm",
+ RNP: "chrome://openpgp/content/modules/RNP.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "l10n", () => {
+ return new Localization(["messenger/openpgp/openpgp.ftl"], true);
+});
+
+var Enigmail = {};
+
+Enigmail.getEnigmailSvc = function () {
+ return EnigmailCore.getService(window);
+};
+
+Enigmail.msg = {
+ decryptedMessage: null,
+ securityInfo: null,
+ lastSaveDir: "",
+ messagePane: null,
+ decryptButton: null,
+ savedHeaders: null,
+ removeListener: false,
+ enableExperiments: false,
+ headersList: [
+ "content-transfer-encoding",
+ "x-enigmail-version",
+ "x-pgp-encoding-format",
+ //"autocrypt-setup-message",
+ ],
+ buggyMailType: null,
+ changedAttributes: [],
+ allAttachmentsDone: false,
+ messageDecryptDone: false,
+ showPartialDecryptionReminder: false,
+
+ get notificationBox() {
+ return gMessageNotificationBar.msgNotificationBar;
+ },
+
+ removeNotification(value) {
+ let item = this.notificationBox.getNotificationWithValue(value);
+ // Remove the notification only if the user didn't previously close it.
+ if (item) {
+ this.notificationBox.removeNotification(item, true);
+ }
+ },
+
+ messengerStartup() {
+ Enigmail.msg.messagePane = document.getElementById("messagepane");
+
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: Startup\n");
+
+ Enigmail.msg.savedHeaders = null;
+
+ Enigmail.msg.decryptButton = document.getElementById(
+ "button-enigmail-decrypt"
+ );
+
+ setTimeout(function () {
+ // if nothing happened, then load all keys after 1 hour
+ // to trigger the key check
+ EnigmailKeyRing.getAllKeys();
+ }, 3600 * 1000); // 1 hour
+
+ // Need to add event listener to Enigmail.msg.messagePane to make it work
+ // Adding to msgFrame doesn't seem to work
+ Enigmail.msg.messagePane.addEventListener(
+ "unload",
+ Enigmail.msg.messageFrameUnload.bind(Enigmail.msg),
+ true
+ );
+
+ EnigmailMsgRead.ensureExtraAddonHeaders();
+ gMessageListeners.push(Enigmail.msg.messageListener);
+ Enigmail.msg.messageListener.onEndHeaders();
+ },
+
+ messageListener: {
+ onStartHeaders() {
+ Enigmail.hdrView.reset();
+ Enigmail.msg.mimeParts = null;
+
+ /*
+ if ("autocrypt" in gExpandedHeaderView) {
+ delete gExpandedHeaderView.autocrypt;
+ }
+ */
+ if ("openpgp" in gExpandedHeaderView) {
+ delete gExpandedHeaderView.openpgp;
+ }
+ },
+ onEndHeaders() {},
+ onEndAttachments() {},
+ },
+
+ /*
+ viewSecurityInfo(event, displaySmimeMsg) {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: viewSecurityInfo\n");
+
+ if (event && event.button !== 0) {
+ return;
+ }
+
+ if (gSignatureStatus >= 0 || gEncryptionStatus >= 0) {
+ showMessageReadSecurityInfo();
+ } else if (Enigmail.msg.securityInfo) {
+ this.viewOpenpgpInfo();
+ } else {
+ showMessageReadSecurityInfo();
+ }
+ },
+ */
+
+ clearLastMessage() {
+ EnigmailSingletons.clearLastDecryptedMessage();
+ },
+
+ messageReload(noShowReload) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: messageReload: " + noShowReload + "\n"
+ );
+
+ this.clearLastMessage();
+ ReloadMessage();
+ },
+
+ messengerClose() {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: messengerClose()\n");
+ },
+
+ reloadCompleteMsg() {
+ this.clearLastMessage();
+ ReloadMessage();
+ },
+
+ setAttachmentReveal(attachmentList) {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: setAttachmentReveal\n");
+
+ var revealBox = document.getElementById("enigmailRevealAttachments");
+ if (revealBox) {
+ // there are situations when evealBox is not yet present
+ revealBox.setAttribute("hidden", !attachmentList ? "true" : "false");
+ }
+ },
+
+ messageCleanup() {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: messageCleanup\n");
+ for (let value of [
+ "decryptInlinePGReminder",
+ "decryptInlinePG",
+ "brokenExchangeProgress",
+ "hasNestedEncryptedParts",
+ "hasConflictingKeyOpenPGP",
+ ]) {
+ this.removeNotification(value);
+ }
+ Enigmail.msg.showPartialDecryptionReminder = false;
+
+ let element = document.getElementById("openpgpKeyBox");
+ if (element) {
+ element.hidden = true;
+ }
+ element = document.getElementById("signatureKeyBox");
+ if (element) {
+ element.hidden = true;
+ element.removeAttribute("keyid");
+ }
+
+ this.setAttachmentReveal(null);
+
+ Enigmail.msg.decryptedMessage = null;
+ Enigmail.msg.securityInfo = null;
+
+ Enigmail.msg.allAttachmentsDone = false;
+ Enigmail.msg.messageDecryptDone = false;
+
+ let cryptoBox = document.getElementById("cryptoBox");
+ if (cryptoBox) {
+ cryptoBox.removeAttribute("decryptDone");
+ }
+
+ Enigmail.msg.toAndCCSet = null;
+ Enigmail.msg.authorEmail = "";
+
+ Enigmail.msg.keyCollectCandidates = new Map();
+
+ EnigmailKeyRing.emailAddressesWithSecretKey = null;
+
+ Enigmail.msg.attachedKeys = [];
+ Enigmail.msg.attachedSenderEmailKeysIndex = [];
+
+ Enigmail.msg.autoProcessPgpKeyAttachmentTransactionID++;
+ Enigmail.msg.autoProcessPgpKeyAttachmentCount = 0;
+ Enigmail.msg.autoProcessPgpKeyAttachmentProcessed = 0;
+ Enigmail.msg.unhideMissingSigKeyBoxIsTODO = false;
+ Enigmail.msg.missingSigKey = null;
+ Enigmail.msg.buggyMailType = null;
+ },
+
+ messageFrameUnload() {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: messageFrameUnload\n");
+ Enigmail.msg.savedHeaders = null;
+ Enigmail.msg.messageCleanup();
+ },
+
+ getCurrentMsgUriSpec() {
+ return gMessageURI || "";
+ },
+
+ getCurrentMsgUrl() {
+ var uriSpec = this.getCurrentMsgUriSpec();
+ return EnigmailMsgRead.getUrlFromUriSpec(uriSpec);
+ },
+
+ updateOptionsDisplay() {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: updateOptionsDisplay: \n");
+ var optList = ["autoDecrypt"];
+
+ for (let j = 0; j < optList.length; j++) {
+ let menuElement = document.getElementById("enigmail_" + optList[j]);
+ menuElement.setAttribute(
+ "checked",
+ Services.prefs.getBoolPref("temp.openpgp.autoDecrypt")
+ ? "true"
+ : "false"
+ );
+
+ menuElement = document.getElementById("enigmail_" + optList[j] + "2");
+ if (menuElement) {
+ menuElement.setAttribute(
+ "checked",
+ Services.prefs.getBoolPref("temp.openpgp.autoDecrypt")
+ ? "true"
+ : "false"
+ );
+ }
+ }
+
+ optList = ["decryptverify"];
+ for (let j = 0; j < optList.length; j++) {
+ let menuElement = document.getElementById("enigmail_" + optList[j]);
+ if (Enigmail.msg.decryptButton && Enigmail.msg.decryptButton.disabled) {
+ menuElement.setAttribute("disabled", "true");
+ } else {
+ menuElement.removeAttribute("disabled");
+ }
+
+ menuElement = document.getElementById("enigmail_" + optList[j] + "2");
+ if (menuElement) {
+ if (Enigmail.msg.decryptButton && Enigmail.msg.decryptButton.disabled) {
+ menuElement.setAttribute("disabled", "true");
+ } else {
+ menuElement.removeAttribute("disabled");
+ }
+ }
+ }
+ },
+
+ setMainMenuLabel() {
+ let o = ["menu_Enigmail", "appmenu-Enigmail"];
+
+ let m0 = document.getElementById(o[0]);
+ let m1 = document.getElementById(o[1]);
+
+ m1.setAttribute("enigmaillabel", m0.getAttribute("enigmaillabel"));
+
+ for (let menuId of o) {
+ let menu = document.getElementById(menuId);
+
+ if (menu) {
+ let lbl = menu.getAttribute("enigmaillabel");
+ menu.setAttribute("label", lbl);
+ }
+ }
+ },
+
+ displayMainMenu(menuPopup) {
+ let obj = menuPopup.firstChild;
+
+ while (obj) {
+ if (
+ obj.getAttribute("enigmailtype") == "enigmail" ||
+ obj.getAttribute("advanced") == "true"
+ ) {
+ obj.removeAttribute("hidden");
+ }
+
+ obj = obj.nextSibling;
+ }
+
+ EnigmailFuncs.collapseAdvanced(
+ menuPopup,
+ "hidden",
+ Enigmail.msg.updateOptionsDisplay()
+ );
+ },
+
+ /**
+ * Determine if Autocrypt is enabled for the currently selected message
+ */
+ /*
+ isAutocryptEnabled() {
+ try {
+ let email = EnigmailFuncs.stripEmail(
+ gFolderDisplay.selectedMessage.recipients
+ ).toLowerCase();
+ let identity = MailServices.accounts.allIdentities.find(id =>
+ id.email?.toLowerCase() == email
+ );
+
+ if (identity) {
+ let acct = EnigmailFuncs.getAccountForIdentity(identity);
+ return acct.incomingServer.getBoolValue("enableAutocrypt");
+ }
+ } catch (ex) {}
+
+ return false;
+ },
+ */
+
+ messageImport() {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: messageImport:\n");
+
+ return this.messageParse(
+ true,
+ true,
+ "",
+ this.getCurrentMsgUriSpec(),
+ false
+ );
+ },
+
+ /***
+ * check that handler for multipart/signed is set to Enigmail.
+ * if handler is different, change it and reload message
+ *
+ * @return: - true if handler is OK
+ * - false if handler was changed and message is reloaded
+ */
+ checkPgpmimeHandler() {
+ if (
+ EnigmailVerify.currentCtHandler !== EnigmailConstants.MIME_HANDLER_PGPMIME
+ ) {
+ EnigmailVerify.registerPGPMimeHandler();
+ this.messageReload();
+ return false;
+ }
+
+ return true;
+ },
+
+ // callback function for automatic decryption
+ async messageAutoDecrypt() {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: messageAutoDecrypt:\n");
+ await Enigmail.msg.messageDecrypt(null, true);
+ },
+
+ async notifyMessageDecryptDone() {
+ Enigmail.msg.messageDecryptDone = true;
+ await Enigmail.msg.processAfterAttachmentsAndDecrypt();
+
+ // Show the partial inline encryption reminder only if the decryption action
+ // came from a partially inline encrypted message.
+ if (Enigmail.msg.showPartialDecryptionReminder) {
+ Enigmail.msg.showPartialDecryptionReminder = false;
+
+ this.notificationBox.appendNotification(
+ "decryptInlinePGReminder",
+ {
+ label: await document.l10n.formatValue(
+ "openpgp-reminder-partial-display"
+ ),
+ priority: this.notificationBox.PRIORITY_INFO_HIGH,
+ },
+ null
+ );
+ }
+ },
+
+ // analyse message header and decrypt/verify message
+ async messageDecrypt(event, isAuto) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: messageDecrypt: " + event + "\n"
+ );
+
+ event = !!event;
+
+ this.mimeParts = null;
+
+ if (!isAuto) {
+ EnigmailVerify.setManualUri(this.getCurrentMsgUriSpec());
+ }
+
+ let contentType = "text/plain";
+ if ("content-type" in currentHeaderData) {
+ contentType = currentHeaderData["content-type"].headerValue;
+ }
+
+ // don't parse message if we know it's a PGP/MIME message
+ if (
+ contentType.search(/^multipart\/encrypted(;|$)/i) === 0 &&
+ contentType.search(/application\/pgp-encrypted/i) > 0
+ ) {
+ this.movePEPsubject();
+ await this.messageDecryptCb(event, isAuto, null);
+ await this.notifyMessageDecryptDone();
+ return;
+ } else if (
+ contentType.search(/^multipart\/signed(;|$)/i) === 0 &&
+ contentType.search(/application\/pgp-signature/i) > 0
+ ) {
+ this.movePEPsubject();
+ await this.messageDecryptCb(event, isAuto, null);
+ await this.notifyMessageDecryptDone();
+ return;
+ }
+
+ let url = this.getCurrentMsgUrl();
+ if (!url) {
+ await Enigmail.msg.messageDecryptCb(event, isAuto, null);
+ await Enigmail.msg.notifyMessageDecryptDone();
+ return;
+ }
+ await new Promise(resolve => {
+ EnigmailMime.getMimeTreeFromUrl(
+ url.spec,
+ false,
+ async function (mimeMsg) {
+ await Enigmail.msg.messageDecryptCb(event, isAuto, mimeMsg);
+ await Enigmail.msg.notifyMessageDecryptDone();
+ resolve();
+ }
+ );
+ });
+ },
+
+ /***
+ * walk through the (sub-) mime tree and determine PGP/MIME encrypted and signed message parts
+ *
+ * @param mimePart: parent object to walk through
+ * @param resultObj: object containing two arrays. The resultObj must be pre-initialized by the caller
+ * - encrypted
+ * - signed
+ */
+ enumerateMimeParts(mimePart, resultObj) {
+ EnigmailLog.DEBUG(
+ 'enumerateMimeParts: partNum="' + mimePart.partNum + '"\n'
+ );
+ EnigmailLog.DEBUG(" " + mimePart.fullContentType + "\n");
+ EnigmailLog.DEBUG(
+ " " + mimePart.subParts.length + " subparts\n"
+ );
+
+ try {
+ var ct = mimePart.fullContentType;
+ if (typeof ct == "string") {
+ ct = ct.replace(/[\r\n]/g, " ");
+ if (ct.search(/multipart\/signed.*application\/pgp-signature/i) >= 0) {
+ resultObj.signed.push(mimePart.partNum);
+ } else if (ct.search(/application\/pgp-encrypted/i) >= 0) {
+ resultObj.encrypted.push(mimePart.partNum);
+ }
+ }
+ } catch (ex) {
+ // catch exception if no headers or no content-type defined.
+ }
+
+ var i;
+ for (i in mimePart.subParts) {
+ this.enumerateMimeParts(mimePart.subParts[i], resultObj);
+ }
+ },
+
+ async messageDecryptCb(event, isAuto, mimeMsg) {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: messageDecryptCb:\n");
+
+ let enigmailSvc;
+ let contentType = "";
+ try {
+ if (!mimeMsg) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: messageDecryptCb: mimeMsg is null\n"
+ );
+ try {
+ contentType = currentHeaderData["content-type"].headerValue;
+ } catch (ex) {
+ contentType = "text/plain";
+ }
+ mimeMsg = {
+ partNum: "1",
+ headers: {
+ has() {
+ return false;
+ },
+ contentType: {
+ type: contentType,
+ mediatype: "",
+ subtype: "",
+ },
+ },
+ fullContentType: contentType,
+ body: "",
+ parent: null,
+ subParts: [],
+ };
+ }
+
+ // Copy selected headers
+ Enigmail.msg.savedHeaders = {
+ autocrypt: [],
+ };
+
+ for (let h in currentHeaderData) {
+ if (h.search(/^autocrypt\d*$/) === 0) {
+ Enigmail.msg.savedHeaders.autocrypt.push(
+ currentHeaderData[h].headerValue
+ );
+ }
+ }
+
+ if (!mimeMsg.fullContentType) {
+ mimeMsg.fullContentType = "text/plain";
+ }
+
+ Enigmail.msg.savedHeaders["content-type"] = mimeMsg.fullContentType;
+ this.mimeParts = mimeMsg;
+
+ for (var index = 0; index < Enigmail.msg.headersList.length; index++) {
+ var headerName = Enigmail.msg.headersList[index];
+ var headerValue = "";
+
+ if (mimeMsg.headers.has(headerName)) {
+ let h = mimeMsg.headers.get(headerName);
+ if (Array.isArray(h)) {
+ headerValue = h.join("");
+ } else {
+ headerValue = h;
+ }
+ }
+ Enigmail.msg.savedHeaders[headerName] = headerValue;
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: header " +
+ headerName +
+ ": '" +
+ headerValue +
+ "'\n"
+ );
+ }
+
+ var msgSigned =
+ mimeMsg.fullContentType.search(/^multipart\/signed/i) === 0 &&
+ EnigmailMime.getProtocol(mimeMsg.fullContentType).search(
+ /^application\/pgp-signature/i
+ ) === 0;
+ var msgEncrypted =
+ mimeMsg.fullContentType.search(/^multipart\/encrypted/i) === 0 &&
+ EnigmailMime.getProtocol(mimeMsg.fullContentType).search(
+ /^application\/pgp-encrypted/i
+ ) === 0;
+ var resultObj = {
+ encrypted: [],
+ signed: [],
+ };
+
+ if (mimeMsg.subParts.length > 0) {
+ this.enumerateMimeParts(mimeMsg, resultObj);
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: embedded objects: " +
+ resultObj.encrypted.join(", ") +
+ " / " +
+ resultObj.signed.join(", ") +
+ "\n"
+ );
+
+ msgSigned = msgSigned || resultObj.signed.length > 0;
+ msgEncrypted = msgEncrypted || resultObj.encrypted.length > 0;
+
+ /*
+ if (
+ "autocrypt-setup-message" in Enigmail.msg.savedHeaders &&
+ Enigmail.msg.savedHeaders["autocrypt-setup-message"].toLowerCase() ===
+ "v1"
+ ) {
+ if (
+ currentAttachments[0].contentType.search(
+ /^application\/autocrypt-setup$/i
+ ) === 0
+ ) {
+ Enigmail.hdrView.displayAutoCryptSetupMsgHeader();
+ return;
+ }
+ }
+ */
+
+ // HACK for Zimbra OpenPGP Zimlet
+ // Zimbra illegally changes attachment content-type to application/pgp-encrypted which interfers with below
+ // see https://sourceforge.net/p/enigmail/bugs/600/
+
+ try {
+ if (
+ mimeMsg.subParts.length > 1 &&
+ mimeMsg.headers.has("x-mailer") &&
+ mimeMsg.headers.get("x-mailer")[0].includes("ZimbraWebClient") &&
+ mimeMsg.subParts[0].fullContentType.includes("text/plain") &&
+ mimeMsg.fullContentType.includes("multipart/mixed") &&
+ mimeMsg.subParts[1].fullContentType.includes(
+ "application/pgp-encrypted"
+ )
+ ) {
+ await this.messageParse(
+ event,
+ false,
+ Enigmail.msg.savedHeaders["content-transfer-encoding"],
+ this.getCurrentMsgUriSpec(),
+ isAuto
+ );
+ return;
+ }
+ } catch (ex) {
+ console.debug(ex);
+ }
+
+ // HACK for MS-EXCHANGE-Server Problem:
+ // check for possible bad mime structure due to buggy exchange server:
+ // - multipart/mixed Container with
+ // - application/pgp-encrypted Attachment with name "PGPMIME Versions Identification"
+ // - application/octet-stream Attachment with name "encrypted.asc" having the encrypted content in base64
+ // - see:
+ // - http://www.mozilla-enigmail.org/forum/viewtopic.php?f=4&t=425
+ // - http://sourceforge.net/p/enigmail/forum/support/thread/4add2b69/
+
+ // iPGMail produces a similar broken structure, see here:
+ // - https://sourceforge.net/p/enigmail/forum/support/thread/afc9c246/#5de7
+
+ // Don't attempt to detect again, if we have already decided
+ // it's a buggy exchange message (buggyMailType is already set).
+
+ if (
+ !Enigmail.msg.buggyMailType &&
+ mimeMsg.subParts.length == 3 &&
+ mimeMsg.fullContentType.search(/multipart\/mixed/i) >= 0 &&
+ mimeMsg.subParts[0].fullContentType.search(/multipart\/encrypted/i) <
+ 0 &&
+ mimeMsg.subParts[0].fullContentType.search(
+ /(text\/(plain|html)|multipart\/alternative)/i
+ ) >= 0 &&
+ mimeMsg.subParts[1].fullContentType.search(
+ /application\/pgp-encrypted/i
+ ) >= 0
+ ) {
+ if (
+ mimeMsg.subParts[1].fullContentType.search(
+ /multipart\/encrypted/i
+ ) < 0 &&
+ mimeMsg.subParts[1].fullContentType.search(
+ /PGP\/?MIME Versions? Identification/i
+ ) >= 0 &&
+ mimeMsg.subParts[2].fullContentType.search(
+ /application\/octet-stream/i
+ ) >= 0 &&
+ mimeMsg.subParts[2].fullContentType.search(/encrypted.asc/i) >= 0
+ ) {
+ this.buggyMailType = "exchange";
+ } else {
+ this.buggyMailType = "iPGMail";
+ }
+
+ // signal that the structure matches to save the content later on
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay: messageDecryptCb: enabling MS-Exchange hack\n"
+ );
+
+ await this.buggyMailHeader();
+ return;
+ }
+ }
+
+ var contentEncoding = "";
+ var msgUriSpec = this.getCurrentMsgUriSpec();
+
+ if (Enigmail.msg.savedHeaders) {
+ contentType = Enigmail.msg.savedHeaders["content-type"];
+ contentEncoding =
+ Enigmail.msg.savedHeaders["content-transfer-encoding"];
+ }
+
+ let smime =
+ contentType.search(
+ /multipart\/signed; protocol="application\/pkcs7-signature/i
+ ) >= 0;
+ if (!smime && (msgSigned || msgEncrypted)) {
+ // PGP/MIME messages
+ enigmailSvc = Enigmail.getEnigmailSvc();
+ if (!enigmailSvc) {
+ return;
+ }
+
+ if (!Enigmail.msg.checkPgpmimeHandler()) {
+ return;
+ }
+
+ if (isAuto && !Services.prefs.getBoolPref("temp.openpgp.autoDecrypt")) {
+ if (EnigmailVerify.getManualUri() != this.getCurrentMsgUriSpec()) {
+ // decryption set to manual
+ Enigmail.hdrView.updatePgpStatus(
+ EnigmailConstants.POSSIBLE_PGPMIME,
+ 0, // exitCode, statusFlags
+ 0,
+ "",
+ "", // keyId, userId
+ "", // sigDetails
+ await l10n.formatValue("possibly-pgp-mime"), // infoMsg
+ null, // blockSeparation
+ null, // encToDetails
+ null
+ ); // xtraStatus
+ }
+ } else if (!isAuto) {
+ Enigmail.msg.messageReload(false);
+ }
+ return;
+ }
+
+ // inline-PGP messages
+ if (!isAuto || Services.prefs.getBoolPref("temp.openpgp.autoDecrypt")) {
+ await this.messageParse(
+ event,
+ false,
+ contentEncoding,
+ msgUriSpec,
+ isAuto
+ );
+ }
+ } catch (ex) {
+ EnigmailLog.writeException(
+ "enigmailMessengerOverlay.js: messageDecryptCb",
+ ex
+ );
+ }
+ },
+
+ /**
+ * Display header about reparing buggy MS-Exchange messages.
+ */
+ async buggyMailHeader() {
+ let uri = this.getCurrentMsgUrl();
+ Enigmail.hdrView.headerPane.updateSecurityStatus(
+ "",
+ 0,
+ 0,
+ 0,
+ "",
+ "",
+ "",
+ "",
+ "",
+ uri,
+ "",
+ "1"
+ );
+
+ // Warn that we can't fix a message that was opened from a local file.
+ if (!gFolder) {
+ Enigmail.msg.notificationBox.appendNotification(
+ "brokenExchange",
+ {
+ label: await document.l10n.formatValue(
+ "openpgp-broken-exchange-opened"
+ ),
+ priority: Enigmail.msg.notificationBox.PRIORITY_WARNING_MEDIUM,
+ },
+ null
+ );
+ return;
+ }
+
+ let buttons = [
+ {
+ "l10n-id": "openpgp-broken-exchange-repair",
+ popup: null,
+ callback(notification, button) {
+ Enigmail.msg.fixBuggyExchangeMail();
+ return false; // Close notification.
+ },
+ },
+ ];
+
+ Enigmail.msg.notificationBox.appendNotification(
+ "brokenExchange",
+ {
+ label: await document.l10n.formatValue("openpgp-broken-exchange-info"),
+ priority: Enigmail.msg.notificationBox.PRIORITY_WARNING_MEDIUM,
+ },
+ buttons
+ );
+ },
+
+ getFirstPGPMessageType(msgText) {
+ let indexEncrypted = msgText.indexOf("-----BEGIN PGP MESSAGE-----");
+ let indexSigned = msgText.indexOf("-----BEGIN PGP SIGNED MESSAGE-----");
+ if (indexEncrypted >= 0) {
+ if (
+ indexSigned == -1 ||
+ (indexSigned >= 0 && indexEncrypted < indexSigned)
+ ) {
+ return "encrypted";
+ }
+ }
+
+ if (indexSigned >= 0) {
+ return "signed";
+ }
+
+ return "";
+ },
+
+ trimIfEncrypted(msgText) {
+ // If it's an encrypted message, we want to trim (at least) the
+ // separator line between the header and the content.
+ // However, trimming all lines should be safe.
+
+ if (Enigmail.msg.getFirstPGPMessageType(msgText) == "encrypted") {
+ // \xA0 is non-breaking-space
+ msgText = msgText.replace(/^[ \t\xA0]+/gm, "");
+ }
+ return msgText;
+ },
+
+ async messageParse(
+ interactive,
+ importOnly,
+ contentEncoding,
+ msgUriSpec,
+ isAuto,
+ pbMessageIndex = "0"
+ ) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: messageParse: " + interactive + "\n"
+ );
+
+ var bodyElement = this.getBodyElement(pbMessageIndex);
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: bodyElement=" + bodyElement + "\n"
+ );
+
+ if (!bodyElement) {
+ return;
+ }
+
+ let topElement = bodyElement;
+ var findStr = /* interactive ? null : */ "-----BEGIN PGP";
+ var msgText = null;
+ var foundIndex = -1;
+
+ let bodyElementFound = false;
+ let hasHeadOrTailNode = false;
+
+ if (bodyElement.firstChild) {
+ let node = bodyElement.firstChild;
+ while (node) {
+ if (
+ node.firstChild &&
+ node.firstChild.nodeName.toUpperCase() == "LEGEND" &&
+ node.firstChild.className == "moz-mime-attachment-header-name"
+ ) {
+ // we reached the area where inline attachments are displayed
+ // --> don't try to decrypt displayed inline attachments
+ break;
+ }
+ if (node.nodeName === "DIV") {
+ if (bodyElementFound) {
+ hasHeadOrTailNode = true;
+ break;
+ }
+
+ foundIndex = node.textContent.indexOf(findStr);
+
+ if (foundIndex < 0) {
+ hasHeadOrTailNode = true;
+ node = node.nextSibling;
+ continue;
+ }
+
+ if (foundIndex >= 0) {
+ if (
+ node.textContent.indexOf(findStr + " LICENSE AUTHORIZATION") ==
+ foundIndex
+ ) {
+ foundIndex = -1;
+ node = node.nextSibling;
+ continue;
+ }
+ }
+
+ if (foundIndex === 0) {
+ bodyElement = node;
+ bodyElementFound = true;
+ } else if (
+ foundIndex > 0 &&
+ node.textContent.substr(foundIndex - 1, 1).search(/[\r\n]/) === 0
+ ) {
+ bodyElement = node;
+ bodyElementFound = true;
+ }
+ }
+ node = node.nextSibling;
+ }
+ }
+
+ if (foundIndex >= 0 && !this.hasInlineQuote(topElement)) {
+ let beginIndex = {};
+ let endIndex = {};
+ let indentStr = {};
+
+ if (
+ Enigmail.msg.savedHeaders["content-type"].search(/^text\/html/i) === 0
+ ) {
+ let p = Cc["@mozilla.org/parserutils;1"].createInstance(
+ Ci.nsIParserUtils
+ );
+ const de = Ci.nsIDocumentEncoder;
+ msgText = p.convertToPlainText(
+ topElement.innerHTML,
+ de.OutputRaw | de.OutputBodyOnly,
+ 0
+ );
+ } else {
+ msgText = bodyElement.textContent;
+ }
+
+ if (!isAuto) {
+ let blockType = EnigmailArmor.locateArmoredBlock(
+ msgText,
+ 0,
+ "",
+ beginIndex,
+ endIndex,
+ indentStr
+ );
+ if (!blockType) {
+ msgText = "";
+ } else {
+ msgText = msgText.substring(beginIndex.value, endIndex.value + 1);
+ }
+ }
+
+ msgText = this.trimIfEncrypted(msgText);
+ }
+
+ if (!msgText) {
+ // No PGP content
+ return;
+ }
+
+ let charset = currentCharacterSet ?? "";
+ if (charset != "UTF-8") {
+ // Encode ciphertext to charset from unicode
+ msgText = EnigmailData.convertFromUnicode(msgText, charset);
+ }
+
+ if (isAuto) {
+ let ht = hasHeadOrTailNode || this.hasHeadOrTailBesidesInlinePGP(msgText);
+ if (ht) {
+ let infoId;
+ let buttonId;
+ if (
+ ht & EnigmailConstants.UNCERTAIN_SIGNATURE ||
+ Enigmail.msg.getFirstPGPMessageType(msgText) == "signed"
+ ) {
+ infoId = "openpgp-partially-signed";
+ buttonId = "openpgp-partial-verify-button";
+ } else {
+ infoId = "openpgp-partially-encrypted";
+ buttonId = "openpgp-partial-decrypt-button";
+ }
+
+ let [description, buttonLabel] = await document.l10n.formatValues([
+ { id: infoId },
+ { id: buttonId },
+ ]);
+
+ let buttons = [
+ {
+ label: buttonLabel,
+ popup: null,
+ callback(aNotification, aButton) {
+ Enigmail.msg.processOpenPGPSubset();
+ return false; // Close notification.
+ },
+ },
+ ];
+
+ this.notificationBox.appendNotification(
+ "decryptInlinePG",
+ {
+ label: description,
+ priority: this.notificationBox.PRIORITY_INFO_HIGH,
+ },
+ buttons
+ );
+ return;
+ }
+ }
+
+ var mozPlainText = bodyElement.innerHTML.search(/class="moz-text-plain"/);
+
+ if (mozPlainText >= 0 && mozPlainText < 40) {
+ // workaround for too much expanded emoticons in plaintext msg
+ var r = new RegExp(
+ /( )(;-\)|:-\)|;\)|:\)|:-\(|:\(|:-\\|:-P|:-D|:-\[|:-\*|>:o|8-\)|:-\$|:-X|=-O|:-!|O:-\)|:'\()( )/g
+ );
+ if (msgText.search(r) >= 0) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: messageParse: performing emoticons fixing\n"
+ );
+ msgText = msgText.replace(r, "$2");
+ }
+ }
+
+ // ignoring text following armored block
+
+ //EnigmailLog.DEBUG("enigmailMessengerOverlay.js: msgText='"+msgText+"'\n");
+
+ var mailNewsUrl = EnigmailMsgRead.getUrlFromUriSpec(msgUriSpec);
+
+ var urlSpec = mailNewsUrl ? mailNewsUrl.spec : "";
+
+ let retry = 1;
+
+ await Enigmail.msg.messageParseCallback(
+ msgText,
+ EnigmailDecryption.getMsgDate(window),
+ contentEncoding,
+ charset,
+ interactive,
+ importOnly,
+ urlSpec,
+ "",
+ retry,
+ "", // head
+ "", // tail
+ msgUriSpec,
+ isAuto,
+ pbMessageIndex
+ );
+ },
+
+ hasInlineQuote(node) {
+ if (node.innerHTML.search(/<blockquote.*-----BEGIN PGP /i) < 0) {
+ return false;
+ }
+
+ return EnigmailMsgRead.searchQuotedPgp(node);
+ },
+
+ hasHeadOrTailBesidesInlinePGP(msgText) {
+ let startIndex = msgText.search(/-----BEGIN PGP (SIGNED )?MESSAGE-----/m);
+ let endIndex = msgText.indexOf("-----END PGP");
+ let hasHead = false;
+ let hasTail = false;
+ let crypto = 0;
+
+ if (startIndex > 0) {
+ let pgpMsg = msgText.match(/(-----BEGIN PGP (SIGNED )?MESSAGE-----)/m)[0];
+ if (pgpMsg.search(/SIGNED/) > 0) {
+ crypto = EnigmailConstants.UNCERTAIN_SIGNATURE;
+ } else {
+ crypto = EnigmailConstants.DECRYPTION_FAILED;
+ }
+ let startSection = msgText.substr(0, startIndex - 1);
+ hasHead = startSection.search(/\S/) >= 0;
+ }
+
+ if (endIndex > startIndex) {
+ let nextLine = msgText.substring(endIndex).search(/[\n\r]/);
+ if (nextLine > 0) {
+ hasTail = msgText.substring(endIndex + nextLine).search(/\S/) >= 0;
+ }
+ }
+
+ if (hasHead || hasTail) {
+ return EnigmailConstants.PARTIALLY_PGP | crypto;
+ }
+
+ return 0;
+ },
+
+ async processOpenPGPSubset() {
+ Enigmail.msg.showPartialDecryptionReminder = true;
+ await this.messageDecrypt(null, false);
+ },
+
+ getBodyElement() {
+ let msgFrame = document.getElementById("messagepane");
+ if (!msgFrame || !msgFrame.contentDocument) {
+ return null;
+ }
+ return msgFrame.contentDocument.getElementsByTagName("body")[0];
+ },
+
+ async messageParseCallback(
+ msgText,
+ msgDate,
+ contentEncoding,
+ charset,
+ interactive,
+ importOnly,
+ messageUrl,
+ signature,
+ retry,
+ head,
+ tail,
+ msgUriSpec,
+ isAuto,
+ pbMessageIndex
+ ) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: messageParseCallback: " +
+ interactive +
+ ", " +
+ interactive +
+ ", importOnly=" +
+ importOnly +
+ ", charset=" +
+ charset +
+ ", msgUrl=" +
+ messageUrl +
+ ", retry=" +
+ retry +
+ ", signature='" +
+ signature +
+ "'\n"
+ );
+
+ if (!msgText) {
+ return;
+ }
+
+ var enigmailSvc = Enigmail.getEnigmailSvc();
+ if (!enigmailSvc) {
+ return;
+ }
+
+ var plainText;
+ var exitCode;
+ var newSignature = "";
+ var statusFlags = 0;
+ var extStatusFlags = 0;
+
+ var errorMsgObj = {
+ value: "",
+ };
+ var keyIdObj = {};
+ var userIdObj = {};
+ var sigDetailsObj = {};
+ var encToDetailsObj = {};
+
+ var blockSeparationObj = {
+ value: "",
+ };
+
+ if (importOnly) {
+ // Import public key
+ await this.importKeyFromMsgBody(msgText);
+ return;
+ }
+ let armorHeaders = EnigmailArmor.getArmorHeaders(msgText);
+ if ("charset" in armorHeaders) {
+ charset = armorHeaders.charset;
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: messageParseCallback: OVERRIDING charset=" +
+ charset +
+ "\n"
+ );
+ }
+
+ var exitCodeObj = {};
+ var statusFlagsObj = {};
+ var signatureObj = {};
+ signatureObj.value = signature;
+
+ var uiFlags = interactive
+ ? EnigmailConstants.UI_INTERACTIVE |
+ // EnigmailConstants.UI_ALLOW_KEY_IMPORT |
+ EnigmailConstants.UI_UNVERIFIED_ENC_OK
+ : 0;
+
+ plainText = EnigmailDecryption.decryptMessage(
+ window,
+ uiFlags,
+ msgText,
+ msgDate,
+ signatureObj,
+ exitCodeObj,
+ statusFlagsObj,
+ keyIdObj,
+ userIdObj,
+ sigDetailsObj,
+ errorMsgObj,
+ blockSeparationObj,
+ encToDetailsObj
+ );
+
+ //EnigmailLog.DEBUG("enigmailMessengerOverlay.js: messageParseCallback: plainText='"+plainText+"'\n");
+
+ exitCode = exitCodeObj.value;
+ newSignature = signatureObj.value;
+
+ if (plainText === "" && exitCode === 0) {
+ plainText = " ";
+ }
+
+ statusFlags = statusFlagsObj.value;
+ extStatusFlags = statusFlagsObj.ext;
+
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: messageParseCallback: newSignature='" +
+ newSignature +
+ "'\n"
+ );
+
+ var errorMsg = errorMsgObj.value;
+
+ if (importOnly) {
+ if (interactive && errorMsg) {
+ EnigmailDialog.alert(window, errorMsg);
+ }
+ return;
+ }
+
+ var displayedUriSpec = Enigmail.msg.getCurrentMsgUriSpec();
+ if (!msgUriSpec || displayedUriSpec == msgUriSpec) {
+ if (exitCode && !statusFlags) {
+ // Failure, but we don't know why it failed.
+ // Peek inside msgText, and check what kind of content it is,
+ // so we can show a minimal error.
+
+ let msgType = Enigmail.msg.getFirstPGPMessageType(msgText);
+ if (msgType == "encrypted") {
+ statusFlags = EnigmailConstants.DECRYPTION_FAILED;
+ } else if (msgType == "signed") {
+ statusFlags = EnigmailConstants.BAD_SIGNATURE;
+ }
+ }
+
+ Enigmail.hdrView.updatePgpStatus(
+ exitCode,
+ statusFlags,
+ extStatusFlags,
+ keyIdObj.value,
+ userIdObj.value,
+ sigDetailsObj.value,
+ errorMsg,
+ null, // blockSeparation
+ encToDetailsObj.value,
+ null
+ ); // xtraStatus
+ }
+
+ var noSecondTry =
+ EnigmailConstants.GOOD_SIGNATURE |
+ EnigmailConstants.EXPIRED_SIGNATURE |
+ EnigmailConstants.EXPIRED_KEY_SIGNATURE |
+ EnigmailConstants.EXPIRED_KEY |
+ EnigmailConstants.REVOKED_KEY |
+ EnigmailConstants.NO_PUBKEY |
+ EnigmailConstants.NO_SECKEY |
+ EnigmailConstants.IMPORTED_KEY |
+ EnigmailConstants.MISSING_PASSPHRASE |
+ EnigmailConstants.BAD_PASSPHRASE |
+ EnigmailConstants.UNKNOWN_ALGO |
+ EnigmailConstants.DECRYPTION_OKAY |
+ EnigmailConstants.OVERFLOWED;
+
+ if (exitCode !== 0 && !(statusFlags & noSecondTry)) {
+ // Bad signature/armor
+ if (retry == 1) {
+ msgText = EnigmailData.convertFromUnicode(msgText, "UTF-8");
+ await Enigmail.msg.messageParseCallback(
+ msgText,
+ msgDate,
+ contentEncoding,
+ charset,
+ interactive,
+ importOnly,
+ messageUrl,
+ signature,
+ retry + 1,
+ head,
+ tail,
+ msgUriSpec,
+ isAuto,
+ pbMessageIndex
+ );
+ return;
+ } else if (retry == 2) {
+ // Try to verify signature by accessing raw message text directly
+ // (avoid recursion by setting retry parameter to false on callback)
+ newSignature = "";
+ await Enigmail.msg.msgDirectDecrypt(
+ interactive,
+ importOnly,
+ contentEncoding,
+ charset,
+ newSignature,
+ 0,
+ head,
+ tail,
+ msgUriSpec,
+ msgDate,
+ Enigmail.msg.messageParseCallback,
+ isAuto
+ );
+ return;
+ } else if (retry == 3) {
+ msgText = EnigmailData.convertFromUnicode(msgText, "UTF-8");
+ await Enigmail.msg.messageParseCallback(
+ msgText,
+ msgDate,
+ contentEncoding,
+ charset,
+ interactive,
+ importOnly,
+ messageUrl,
+ null,
+ retry + 1,
+ head,
+ tail,
+ msgUriSpec,
+ isAuto,
+ pbMessageIndex
+ );
+ return;
+ }
+ }
+
+ if (!plainText) {
+ // Show the subset that we cannot process, together with status.
+ plainText = msgText;
+ }
+
+ if (retry >= 2) {
+ plainText = EnigmailData.convertFromUnicode(
+ EnigmailData.convertToUnicode(plainText, "UTF-8"),
+ charset
+ );
+ }
+
+ // TODO: what is blockSeparation ? How to emulate with RNP?
+ /*
+ if (blockSeparationObj.value.includes(" ")) {
+ var blocks = blockSeparationObj.value.split(/ /);
+ var blockInfo = blocks[0].split(/:/);
+ plainText =
+ EnigmailData.convertFromUnicode(
+ "*Parts of the message have NOT been signed nor encrypted*",
+ charset
+ ) +
+ "\n\n" +
+ plainText.substr(0, blockInfo[1]) +
+ "\n\n" +
+ "*Multiple message blocks found -- decryption/verification aborted*";
+ }
+ */
+
+ // Save decrypted message status, headers, and content
+ var headerList = {
+ subject: "",
+ from: "",
+ date: "",
+ to: "",
+ cc: "",
+ };
+
+ var index, headerName;
+
+ if (!gViewAllHeaders) {
+ for (index = 0; index < headerList.length; index++) {
+ headerList[index] = "";
+ }
+ } else {
+ for (index = 0; index < gExpandedHeaderList.length; index++) {
+ headerList[gExpandedHeaderList[index].name] = "";
+ }
+
+ for (headerName in currentHeaderData) {
+ headerList[headerName] = "";
+ }
+ }
+
+ for (headerName in headerList) {
+ if (currentHeaderData[headerName]) {
+ headerList[headerName] = currentHeaderData[headerName].headerValue;
+ }
+ }
+
+ // WORKAROUND
+ if (headerList.cc == headerList.to) {
+ headerList.cc = "";
+ }
+
+ var hasAttachments = currentAttachments && currentAttachments.length;
+ var attachmentsEncrypted = true;
+
+ for (index in currentAttachments) {
+ if (!Enigmail.msg.checkEncryptedAttach(currentAttachments[index])) {
+ if (
+ !EnigmailMsgRead.checkSignedAttachment(
+ currentAttachments,
+ index,
+ currentAttachments
+ )
+ ) {
+ attachmentsEncrypted = false;
+ }
+ }
+ }
+
+ Enigmail.msg.decryptedMessage = {
+ url: messageUrl,
+ uri: msgUriSpec,
+ headerList,
+ hasAttachments,
+ attachmentsEncrypted,
+ charset,
+ plainText,
+ };
+
+ // don't display decrypted message if message selection has changed
+ displayedUriSpec = Enigmail.msg.getCurrentMsgUriSpec();
+ if (msgUriSpec && displayedUriSpec && displayedUriSpec != msgUriSpec) {
+ return;
+ }
+
+ // Create and load one-time message URI
+ var messageContent = Enigmail.msg.getDecryptedMessage(
+ "message/rfc822",
+ false
+ );
+
+ var node;
+ var bodyElement = Enigmail.msg.getBodyElement(pbMessageIndex);
+
+ if (bodyElement.firstChild) {
+ node = bodyElement.firstChild;
+
+ let divFound = false;
+
+ while (node) {
+ if (node.nodeName == "DIV") {
+ if (divFound) {
+ node.innerHTML = "";
+ } else {
+ // for safety reasons, we replace the complete visible message with
+ // the decrypted or signed part (bug 983)
+ divFound = true;
+ node.innerHTML = EnigmailFuncs.formatPlaintextMsg(
+ EnigmailData.convertToUnicode(messageContent, charset)
+ );
+ Enigmail.msg.movePEPsubject();
+ }
+ }
+ node = node.nextSibling;
+ }
+
+ if (divFound) {
+ return;
+ }
+
+ let preFound = false;
+
+ // if no <DIV> node is found, try with <PRE> (bug 24762)
+ node = bodyElement.firstChild;
+ while (node) {
+ if (node.nodeName == "PRE") {
+ if (preFound) {
+ node.innerHTML = "";
+ } else {
+ preFound = true;
+ node.innerHTML = EnigmailFuncs.formatPlaintextMsg(
+ EnigmailData.convertToUnicode(messageContent, charset)
+ );
+ Enigmail.msg.movePEPsubject();
+ }
+ }
+ node = node.nextSibling;
+ }
+
+ if (preFound) {
+ return;
+ }
+ }
+
+ EnigmailLog.ERROR(
+ "enigmailMessengerOverlay.js: no node found to replace message display\n"
+ );
+ },
+
+ importAttachedSenderKey() {
+ for (let info of Enigmail.msg.attachedSenderEmailKeysIndex) {
+ EnigmailKeyRing.importKeyDataWithConfirmation(
+ window,
+ [info.keyInfo],
+ Enigmail.msg.attachedKeys[info.idx],
+ true,
+ ["0x" + info.keyInfo.fpr]
+ );
+ }
+ },
+
+ async searchSignatureKey() {
+ let keyId = document
+ .getElementById("signatureKeyBox")
+ .getAttribute("keyid");
+ if (!keyId) {
+ return false;
+ }
+ return KeyLookupHelper.lookupAndImportByKeyID(
+ "interactive-import",
+ window,
+ keyId,
+ true
+ );
+ },
+
+ notifySigKeyMissing(keyId) {
+ Enigmail.msg.missingSigKey = keyId;
+ if (
+ Enigmail.msg.allAttachmentsDone &&
+ Enigmail.msg.messageDecryptDone &&
+ Enigmail.msg.autoProcessPgpKeyAttachmentProcessed ==
+ Enigmail.msg.autoProcessPgpKeyAttachmentCount
+ ) {
+ Enigmail.msg.unhideMissingSigKeyBox();
+ } else {
+ Enigmail.msg.unhideMissingSigKeyBoxIsTODO = true;
+ }
+ },
+
+ unhideMissingSigKeyBox() {
+ let sigKeyIsAttached = false;
+ for (let info of Enigmail.msg.attachedSenderEmailKeysIndex) {
+ if (info.keyInfo.keyId == Enigmail.msg.missingSigKey) {
+ sigKeyIsAttached = true;
+ break;
+ }
+ }
+ if (!sigKeyIsAttached) {
+ let b = document.getElementById("signatureKeyBox");
+ b.removeAttribute("hidden");
+ b.setAttribute("keyid", Enigmail.msg.missingSigKey);
+ }
+ },
+
+ async importKeyFromMsgBody(msgData) {
+ let beginIndexObj = {};
+ let endIndexObj = {};
+ let indentStrObj = {};
+ let blockType = EnigmailArmor.locateArmoredBlock(
+ msgData,
+ 0,
+ "",
+ beginIndexObj,
+ endIndexObj,
+ indentStrObj
+ );
+ if (!blockType || blockType !== "PUBLIC KEY BLOCK") {
+ return;
+ }
+
+ let keyData = msgData.substring(beginIndexObj.value, endIndexObj.value);
+
+ let errorMsgObj = {};
+ let preview = await EnigmailKey.getKeyListFromKeyBlock(
+ keyData,
+ errorMsgObj,
+ true,
+ true,
+ false
+ );
+ if (preview && errorMsgObj.value === "") {
+ EnigmailKeyRing.importKeyDataWithConfirmation(
+ window,
+ preview,
+ keyData,
+ false
+ );
+ } else {
+ document.l10n.formatValue("preview-failed").then(value => {
+ EnigmailDialog.alert(window, value + "\n" + errorMsgObj.value);
+ });
+ }
+ },
+
+ /**
+ * Extract the subject from the 1st content line and move it to the subject line
+ */
+ movePEPsubject() {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: movePEPsubject:\n");
+
+ let bodyElement = this.getBodyElement();
+ if (
+ bodyElement.textContent.search(/^\r?\n?Subject: [^\r\n]+\r?\n\r?\n/i) ===
+ 0 &&
+ "subject" in currentHeaderData &&
+ currentHeaderData.subject.headerValue === "pEp"
+ ) {
+ let m = EnigmailMime.extractSubjectFromBody(bodyElement.textContent);
+ if (m) {
+ let node = bodyElement.firstChild;
+ let found = false;
+
+ while (!found && node) {
+ if (node.nodeName == "DIV") {
+ node.innerHTML = EnigmailFuncs.formatPlaintextMsg(m.messageBody);
+ found = true;
+ }
+ node = node.nextSibling;
+ }
+
+ // if no <DIV> node is found, try with <PRE> (bug 24762)
+ node = bodyElement.firstChild;
+ while (!found && node) {
+ if (node.nodeName == "PRE") {
+ node.innerHTML = EnigmailFuncs.formatPlaintextMsg(m.messageBody);
+ found = true;
+ }
+ node = node.nextSibling;
+ }
+
+ Enigmail.hdrView.setSubject(m.subject);
+ }
+ }
+ },
+
+ /**
+ * Fix broken PGP/MIME messages from MS-Exchange by replacing the broken original
+ * message with a fixed copy.
+ *
+ * no return
+ */
+ async fixBuggyExchangeMail() {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: fixBuggyExchangeMail:\n");
+
+ this.notificationBox.appendNotification(
+ "brokenExchangeProgress",
+ {
+ label: await document.l10n.formatValue("openpgp-broken-exchange-wait"),
+ priority: this.notificationBox.PRIORITY_INFO_HIGH,
+ },
+ null
+ );
+
+ let msg = gMessage;
+ EnigmailFixExchangeMsg.fixExchangeMessage(msg, this.buggyMailType)
+ .then(msgKey => {
+ // Display the new message which now has the key msgKey.
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: fixBuggyExchangeMail: _success: msgKey=" +
+ msgKey +
+ "\n"
+ );
+ // TODO: scope is about:message, and this doesn't work
+ // parent.gDBView.selectMsgByKey(msgKey);
+ // ReloadMessage();
+ })
+ .catch(async function (ex) {
+ console.debug(ex);
+ EnigmailDialog.alert(
+ window,
+ await l10n.formatValue("fix-broken-exchange-msg-failed")
+ );
+ });
+
+ // Remove the brokenExchangeProgress notification at the end of the process.
+ this.removeNotification("brokenExchangeProgress");
+ },
+
+ /**
+ * Hide attachments containing OpenPGP keys
+ */
+ hidePgpKeys() {
+ let keys = [];
+ for (let i = 0; i < currentAttachments.length; i++) {
+ if (
+ currentAttachments[i].contentType.search(/^application\/pgp-keys/i) ===
+ 0
+ ) {
+ keys.push(i);
+ }
+ }
+
+ if (keys.length > 0) {
+ let attachmentList = document.getElementById("attachmentList");
+
+ for (let i = keys.length; i > 0; i--) {
+ currentAttachments.splice(keys[i - 1], 1);
+ }
+
+ if (attachmentList) {
+ // delete all keys from attachment list
+ while (attachmentList.firstChild) {
+ attachmentList.firstChild.remove();
+ }
+
+ // build new attachment list
+
+ /* global gBuildAttachmentsForCurrentMsg: true */
+ let orig = gBuildAttachmentsForCurrentMsg;
+ gBuildAttachmentsForCurrentMsg = false;
+ displayAttachmentsForExpandedView();
+ gBuildAttachmentsForCurrentMsg = orig;
+ }
+ }
+ },
+
+ // check if the attachment could be encrypted
+ checkEncryptedAttach(attachment) {
+ return (
+ EnigmailMsgRead.getAttachmentName(attachment).match(
+ /\.(gpg|pgp|asc)$/i
+ ) ||
+ (attachment.contentType.match(/^application\/pgp(-.*)?$/i) &&
+ attachment.contentType.search(/^application\/pgp-signature/i) < 0)
+ );
+ },
+
+ getDecryptedMessage(contentType, includeHeaders) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: getDecryptedMessage: " +
+ contentType +
+ ", " +
+ includeHeaders +
+ "\n"
+ );
+
+ if (!Enigmail.msg.decryptedMessage) {
+ return "No decrypted message found!\n";
+ }
+
+ var enigmailSvc = Enigmail.getEnigmailSvc();
+ if (!enigmailSvc) {
+ return "";
+ }
+
+ var headerList = Enigmail.msg.decryptedMessage.headerList;
+ var statusLine = Enigmail.msg.securityInfo
+ ? Enigmail.msg.securityInfo.statusLine
+ : "";
+ var contentData = "";
+ var headerName;
+
+ if (contentType == "message/rfc822") {
+ // message/rfc822
+
+ if (includeHeaders) {
+ try {
+ var msg = gMessage;
+ if (msg) {
+ let msgHdr = {
+ From: msg.author,
+ Subject: msg.subject,
+ To: msg.recipients,
+ Cc: msg.ccList,
+ Date: new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "short",
+ timeStyle: "short",
+ }).format(new Date(msg.dateInSeconds * 1000)),
+ };
+
+ if (
+ msg?.folder?.flags & Ci.nsMsgFolderFlags.Newsgroup &&
+ currentHeaderData.newsgroups
+ ) {
+ msgHdr.Newsgroups = currentHeaderData.newsgroups.headerValue;
+ }
+
+ for (let headerName in msgHdr) {
+ if (msgHdr[headerName] && msgHdr[headerName].length > 0) {
+ contentData += headerName + ": " + msgHdr[headerName] + "\r\n";
+ }
+ }
+ }
+ } catch (ex) {
+ // the above seems to fail every now and then
+ // so, here is the fallback
+ for (let headerName in headerList) {
+ let headerValue = headerList[headerName];
+ contentData += headerName + ": " + headerValue + "\r\n";
+ }
+ }
+
+ contentData += "Content-Type: text/plain";
+
+ if (Enigmail.msg.decryptedMessage.charset) {
+ contentData += "; charset=" + Enigmail.msg.decryptedMessage.charset;
+ }
+
+ contentData += "\r\n";
+ }
+
+ contentData += "\r\n";
+
+ if (
+ Enigmail.msg.decryptedMessage.hasAttachments &&
+ !Enigmail.msg.decryptedMessage.attachmentsEncrypted
+ ) {
+ contentData += EnigmailData.convertFromUnicode(
+ l10n.formatValueSync("enig-content-note") + "\r\n\r\n",
+ Enigmail.msg.decryptedMessage.charset
+ );
+ }
+
+ contentData += Enigmail.msg.decryptedMessage.plainText;
+ } else {
+ // text/html or text/plain
+
+ if (contentType == "text/html") {
+ contentData +=
+ '<meta http-equiv="Content-Type" content="text/html; charset=' +
+ Enigmail.msg.decryptedMessage.charset +
+ '">\r\n';
+ contentData += "<html><head></head><body>\r\n";
+ }
+
+ if (statusLine) {
+ if (contentType == "text/html") {
+ contentData +=
+ EnigmailMsgRead.escapeTextForHTML(statusLine, false) +
+ "<br>\r\n<hr>\r\n";
+ } else {
+ contentData += statusLine + "\r\n\r\n";
+ }
+ }
+
+ if (includeHeaders) {
+ for (headerName in headerList) {
+ let headerValue = headerList[headerName];
+
+ if (headerValue) {
+ if (contentType == "text/html") {
+ contentData +=
+ "<b>" +
+ EnigmailMsgRead.escapeTextForHTML(headerName, false) +
+ ":</b> " +
+ EnigmailMsgRead.escapeTextForHTML(headerValue, false) +
+ "<br>\r\n";
+ } else {
+ contentData += headerName + ": " + headerValue + "\r\n";
+ }
+ }
+ }
+ }
+
+ if (contentType == "text/html") {
+ contentData +=
+ "<pre>" +
+ EnigmailMsgRead.escapeTextForHTML(
+ Enigmail.msg.decryptedMessage.plainText,
+ false
+ ) +
+ "</pre>\r\n";
+
+ contentData += "</body></html>\r\n";
+ } else {
+ contentData += "\r\n" + Enigmail.msg.decryptedMessage.plainText;
+ }
+
+ if (AppConstants.platform != "win") {
+ contentData = contentData.replace(/\r\n/g, "\n");
+ }
+ }
+
+ return contentData;
+ },
+
+ async msgDirectDecrypt(
+ interactive,
+ importOnly,
+ contentEncoding,
+ charset,
+ signature,
+ bufferSize,
+ head,
+ tail,
+ msgUriSpec,
+ msgDate,
+ callbackFunction,
+ isAuto
+ ) {
+ EnigmailLog.WRITE(
+ "enigmailMessengerOverlay.js: msgDirectDecrypt: contentEncoding=" +
+ contentEncoding +
+ ", signature=" +
+ signature +
+ "\n"
+ );
+ let mailNewsUrl = this.getCurrentMsgUrl();
+ if (!mailNewsUrl) {
+ return;
+ }
+
+ let PromiseStreamListener = function () {
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+ this._data = null;
+ this._stream = null;
+ };
+
+ PromiseStreamListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+
+ onStartRequest(request) {
+ this.data = "";
+ this.inStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+ },
+
+ onStopRequest(request, statusCode) {
+ if (statusCode != Cr.NS_OK) {
+ this._reject(`Streaming failed: ${statusCode}`);
+ return;
+ }
+
+ let start = this.data.indexOf("-----BEGIN PGP");
+ let end = this.data.indexOf("-----END PGP");
+
+ if (start >= 0 && end > start) {
+ let tStr = this.data.substr(end);
+ let n = tStr.indexOf("\n");
+ let r = tStr.indexOf("\r");
+ let lEnd = -1;
+ if (n >= 0 && r >= 0) {
+ lEnd = Math.min(r, n);
+ } else if (r >= 0) {
+ lEnd = r;
+ } else if (n >= 0) {
+ lEnd = n;
+ }
+
+ if (lEnd >= 0) {
+ end += lEnd;
+ }
+
+ let data = Enigmail.msg.trimIfEncrypted(
+ this.data.substring(start, end + 1)
+ );
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: data: >" + data.substr(0, 100) + "<\n"
+ );
+
+ let currentMsgURL = Enigmail.msg.getCurrentMsgUrl();
+ let urlSpec = currentMsgURL ? currentMsgURL.spec : "";
+
+ let l = urlSpec.length;
+ if (urlSpec.substr(0, l) != mailNewsUrl.spec.substr(0, l)) {
+ EnigmailLog.ERROR(
+ "enigmailMessengerOverlay.js: Message URL mismatch " +
+ currentMsgURL +
+ " vs. " +
+ urlSpec +
+ "\n"
+ );
+ this._reject(`Msg url mismatch: ${currentMsgURL} vs ${urlSpec}`);
+ return;
+ }
+
+ Enigmail.msg
+ .messageParseCallback(
+ data,
+ msgDate,
+ contentEncoding,
+ charset,
+ interactive,
+ importOnly,
+ mailNewsUrl.spec,
+ signature,
+ 3,
+ head,
+ tail,
+ msgUriSpec,
+ isAuto
+ )
+ .then(() => this._resolve(this.data));
+ }
+ },
+
+ onDataAvailable(request, stream, off, count) {
+ this.inStream.init(stream);
+ this.data += this.inStream.read(count);
+ },
+
+ get promise() {
+ return this._promise;
+ },
+ };
+
+ let streamListener = new PromiseStreamListener();
+ let msgSvc = MailServices.messageServiceFromURI(msgUriSpec);
+ msgSvc.streamMessage(
+ msgUriSpec,
+ streamListener,
+ top.msgWindow,
+ null,
+ false,
+ null,
+ false
+ );
+ await streamListener;
+ },
+
+ revealAttachments(index) {
+ if (!index) {
+ index = 0;
+ }
+
+ if (index < currentAttachments.length) {
+ this.handleAttachment(
+ "revealName/" + index.toString(),
+ currentAttachments[index]
+ );
+ }
+ },
+
+ /**
+ * Set up some event handlers for the attachment items in #attachmentList.
+ */
+ handleAttachmentEvent() {
+ let attList = document.getElementById("attachmentList");
+
+ for (let att of attList.itemChildren) {
+ att.addEventListener("click", this.attachmentItemClick.bind(this), true);
+ }
+ },
+
+ // handle a selected attachment (decrypt & open or save)
+ handleAttachmentSel(actionType) {
+ let contextMenu = document.getElementById("attachmentItemContext");
+ let anAttachment = contextMenu.attachments[0];
+
+ switch (actionType) {
+ case "saveAttachment":
+ case "openAttachment":
+ case "importKey":
+ case "revealName":
+ this.handleAttachment(actionType, anAttachment);
+ break;
+ case "verifySig":
+ this.verifyDetachedSignature(anAttachment);
+ break;
+ }
+ },
+
+ /**
+ * save the original file plus the signature file to disk and then verify the signature
+ */
+ async verifyDetachedSignature(anAttachment) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: verifyDetachedSignature: url=" +
+ anAttachment.url +
+ "\n"
+ );
+
+ var enigmailSvc = Enigmail.getEnigmailSvc();
+ if (!enigmailSvc) {
+ return;
+ }
+
+ var origAtt, signatureAtt;
+ var isEncrypted = false;
+
+ if (
+ EnigmailMsgRead.getAttachmentName(anAttachment).search(/\.sig$/i) > 0 ||
+ anAttachment.contentType.search(/^application\/pgp-signature/i) === 0
+ ) {
+ // we have the .sig file; need to know the original file;
+
+ signatureAtt = anAttachment;
+ var origName = EnigmailMsgRead.getAttachmentName(anAttachment).replace(
+ /\.sig$/i,
+ ""
+ );
+
+ for (let i = 0; i < currentAttachments.length; i++) {
+ if (
+ origName == EnigmailMsgRead.getAttachmentName(currentAttachments[i])
+ ) {
+ origAtt = currentAttachments[i];
+ break;
+ }
+ }
+
+ if (!origAtt) {
+ for (let i = 0; i < currentAttachments.length; i++) {
+ if (
+ origName ==
+ EnigmailMsgRead.getAttachmentName(currentAttachments[i]).replace(
+ /\.pgp$/i,
+ ""
+ )
+ ) {
+ isEncrypted = true;
+ origAtt = currentAttachments[i];
+ break;
+ }
+ }
+ }
+ } else {
+ // we have a supposedly original file; need to know the .sig file;
+
+ origAtt = anAttachment;
+ var attachName = EnigmailMsgRead.getAttachmentName(anAttachment);
+ var sigName = attachName + ".sig";
+
+ for (let i = 0; i < currentAttachments.length; i++) {
+ if (
+ sigName == EnigmailMsgRead.getAttachmentName(currentAttachments[i])
+ ) {
+ signatureAtt = currentAttachments[i];
+ break;
+ }
+ }
+
+ if (!signatureAtt && attachName.search(/\.pgp$/i) > 0) {
+ sigName = attachName.replace(/\.pgp$/i, ".sig");
+ for (let i = 0; i < currentAttachments.length; i++) {
+ if (
+ sigName == EnigmailMsgRead.getAttachmentName(currentAttachments[i])
+ ) {
+ isEncrypted = true;
+ signatureAtt = currentAttachments[i];
+ break;
+ }
+ }
+ }
+ }
+
+ if (!signatureAtt) {
+ EnigmailDialog.alert(
+ window,
+ l10n.formatValueSync("attachment-no-match-to-signature", {
+ attachment: EnigmailMsgRead.getAttachmentName(origAtt),
+ })
+ );
+ return;
+ }
+ if (!origAtt) {
+ EnigmailDialog.alert(
+ window,
+ l10n.formatValueSync("attachment-no-match-from-signature", {
+ attachment: EnigmailMsgRead.getAttachmentName(signatureAtt),
+ })
+ );
+ return;
+ }
+
+ // open
+ var outFile1 = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ outFile1.append(EnigmailMsgRead.getAttachmentName(origAtt));
+ outFile1.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ let response = await fetch(origAtt.url);
+ if (!response.ok) {
+ throw new Error(`Bad response for url=${origAtt.url}`);
+ }
+ await IOUtils.writeUTF8(outFile1.path, await response.text());
+
+ if (isEncrypted) {
+ // Try to decrypt message if we suspect the message is encrypted.
+ // If it fails we will just verify the encrypted data.
+ let readBinaryFile = async () => {
+ let data = await IOUtils.read(outFile1.path);
+ return MailStringUtils.uint8ArrayToByteString(data);
+ };
+ await EnigmailDecryption.decryptAttachment(
+ window,
+ outFile1,
+ EnigmailMsgRead.getAttachmentName(origAtt),
+ readBinaryFile,
+ {},
+ {},
+ {}
+ );
+ }
+
+ var outFile2 = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ outFile2.append(EnigmailMsgRead.getAttachmentName(signatureAtt));
+ outFile2.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ let response2 = await fetch(signatureAtt.url);
+ if (!response2.ok) {
+ throw new Error(`Bad response for url=${signatureAtt.url}`);
+ }
+ await IOUtils.writeUTF8(outFile2.path, await response2.text());
+
+ let cApi = EnigmailCryptoAPI();
+ let promise = cApi.verifyAttachment(outFile1.path, outFile2.path);
+ promise.then(async function (message) {
+ EnigmailDialog.info(
+ window,
+ l10n.formatValueSync("signature-verified-ok", {
+ attachment: EnigmailMsgRead.getAttachmentName(origAtt),
+ }) +
+ "\n\n" +
+ message
+ );
+ });
+ promise.catch(async function (err) {
+ EnigmailDialog.alert(
+ window,
+ l10n.formatValueSync("signature-verify-failed", {
+ attachment: EnigmailMsgRead.getAttachmentName(origAtt),
+ }) +
+ "\n\n" +
+ err
+ );
+ });
+
+ outFile1.remove(false);
+ outFile2.remove(false);
+ },
+
+ handleAttachment(actionType, attachment) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: handleAttachment: actionType=" +
+ actionType +
+ ", attachment(url)=" +
+ attachment.url +
+ "\n"
+ );
+
+ let bufferListener = EnigmailStreams.newStringStreamListener(async data => {
+ Enigmail.msg.decryptAttachmentCallback([
+ {
+ actionType,
+ attachment,
+ forceBrowser: false,
+ data,
+ },
+ ]);
+ });
+ let msgUri = Services.io.newURI(attachment.url);
+ let channel = EnigmailStreams.createChannel(msgUri);
+ channel.asyncOpen(bufferListener, msgUri);
+ },
+
+ setAttachmentName(attachment, newLabel, index) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: setAttachmentName (" + newLabel + "):\n"
+ );
+
+ var attList = document.getElementById("attachmentList");
+ if (attList) {
+ var attNode = attList.firstChild;
+ while (attNode) {
+ if (attNode.getAttribute("name") == attachment.name) {
+ attNode.setAttribute("name", newLabel);
+ }
+ attNode = attNode.nextSibling;
+ }
+ }
+
+ if (typeof attachment.displayName == "undefined") {
+ attachment.name = newLabel;
+ } else {
+ attachment.displayName = newLabel;
+ }
+
+ if (index && index.length > 0) {
+ this.revealAttachments(parseInt(index, 10) + 1);
+ }
+ },
+
+ async decryptAttachmentCallback(cbArray) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: decryptAttachmentCallback:\n"
+ );
+
+ var callbackArg = cbArray[0];
+
+ var exitCodeObj = {};
+ var statusFlagsObj = {};
+ var errorMsgObj = {};
+ var exitStatus = -1;
+
+ var outFile;
+ var origFilename;
+ var rawFileName = EnigmailMsgRead.getAttachmentName(
+ callbackArg.attachment
+ ).replace(/\.(asc|pgp|gpg)$/i, "");
+
+ // TODO: We don't have code yet to extract the original filename
+ // from an encrypted data block.
+ /*
+ if (callbackArg.actionType != "importKey") {
+ let cApi = EnigmailCryptoAPI();
+ let origFilename = await cApi.getFileName(window, callbackArg.data);
+ if (origFilename && origFilename.length > rawFileName.length) {
+ rawFileName = origFilename;
+ }
+ }
+ */
+
+ if (callbackArg.actionType == "saveAttachment") {
+ outFile = EnigmailDialog.filePicker(
+ window,
+ l10n.formatValueSync("save-attachment-header"),
+ Enigmail.msg.lastSaveDir,
+ true,
+ false,
+ "",
+ rawFileName,
+ null
+ );
+ if (!outFile) {
+ return;
+ }
+ } else if (callbackArg.actionType.substr(0, 10) == "revealName") {
+ if (origFilename && origFilename.length > 0) {
+ Enigmail.msg.setAttachmentName(
+ callbackArg.attachment,
+ origFilename + ".pgp",
+ callbackArg.actionType.substr(11, 10)
+ );
+ }
+ Enigmail.msg.setAttachmentReveal(null);
+ return;
+ } else {
+ // open
+ outFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ outFile.append(rawFileName);
+ outFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+ }
+
+ if (callbackArg.actionType == "importKey") {
+ var preview = await EnigmailKey.getKeyListFromKeyBlock(
+ callbackArg.data,
+ errorMsgObj,
+ true,
+ true,
+ false
+ );
+
+ if (errorMsgObj.value !== "" || !preview || preview.length === 0) {
+ // try decrypting the attachment
+ exitStatus = await EnigmailDecryption.decryptAttachment(
+ window,
+ outFile,
+ EnigmailMsgRead.getAttachmentName(callbackArg.attachment),
+ callbackArg.data,
+ exitCodeObj,
+ statusFlagsObj,
+ errorMsgObj
+ );
+ if (exitStatus && exitCodeObj.value === 0) {
+ // success decrypting, let's try again
+ callbackArg.data = String.fromCharCode(
+ ...(await IOUtils.read(outFile.path))
+ );
+ preview = await EnigmailKey.getKeyListFromKeyBlock(
+ callbackArg.data,
+ errorMsgObj,
+ true,
+ true,
+ false
+ );
+ }
+ }
+
+ if (preview && errorMsgObj.value === "") {
+ EnigmailKeyRing.importKeyDataWithConfirmation(
+ window,
+ preview,
+ callbackArg.data,
+ false
+ );
+ } else {
+ document.l10n.formatValue("preview-failed").then(value => {
+ EnigmailDialog.alert(window, value + "\n" + errorMsgObj.value);
+ });
+ }
+ outFile.remove(true);
+ return;
+ }
+
+ exitStatus = await EnigmailDecryption.decryptAttachment(
+ window,
+ outFile,
+ EnigmailMsgRead.getAttachmentName(callbackArg.attachment),
+ callbackArg.data,
+ exitCodeObj,
+ statusFlagsObj,
+ errorMsgObj
+ );
+
+ if (!exitStatus || exitCodeObj.value !== 0) {
+ exitStatus = false;
+ if (
+ statusFlagsObj.value & EnigmailConstants.DECRYPTION_OKAY &&
+ statusFlagsObj.value & EnigmailConstants.UNCERTAIN_SIGNATURE
+ ) {
+ if (callbackArg.actionType == "openAttachment") {
+ let [title, button] = await document.l10n.formatValues([
+ { id: "decrypt-ok-no-sig" },
+ { id: "msg-ovl-button-cont-anyway" },
+ ]);
+
+ exitStatus = EnigmailDialog.confirmDlg(window, title, button);
+ } else {
+ EnigmailDialog.info(
+ window,
+ await document.l10n.formatValue("decrypt-ok-no-sig")
+ );
+ }
+ } else {
+ let msg = await document.l10n.formatValue("failed-decrypt");
+ if (errorMsgObj.errorMsg) {
+ msg += "\n\n" + errorMsgObj.errorMsg;
+ }
+ EnigmailDialog.info(window, msg);
+ exitStatus = false;
+ }
+ }
+ if (exitStatus) {
+ if (statusFlagsObj.value & EnigmailConstants.IMPORTED_KEY) {
+ if (exitCodeObj.keyList) {
+ let importKeyList = exitCodeObj.keyList.map(function (a) {
+ return a.id;
+ });
+ EnigmailDialog.keyImportDlg(window, importKeyList);
+ }
+ } else if (statusFlagsObj.value & EnigmailConstants.DISPLAY_MESSAGE) {
+ HandleSelectedAttachments("open");
+ } else if (
+ statusFlagsObj.value & EnigmailConstants.DISPLAY_MESSAGE ||
+ callbackArg.actionType == "openAttachment"
+ ) {
+ var ioServ = Services.io;
+ var outFileUri = ioServ.newFileURI(outFile);
+ var fileExt = outFile.leafName.replace(/(.*\.)(\w+)$/, "$2");
+ if (fileExt && !callbackArg.forceBrowser) {
+ var extAppLauncher = Cc[
+ "@mozilla.org/uriloader/external-helper-app-service;1"
+ ].getService(Ci.nsPIExternalAppLauncher);
+ extAppLauncher.deleteTemporaryFileOnExit(outFile);
+
+ try {
+ var mimeService = Cc["@mozilla.org/mime;1"].getService(
+ Ci.nsIMIMEService
+ );
+ var fileMimeType = mimeService.getTypeFromFile(outFile);
+ var fileMimeInfo = mimeService.getFromTypeAndExtension(
+ fileMimeType,
+ fileExt
+ );
+
+ fileMimeInfo.launchWithFile(outFile);
+ } catch (ex) {
+ // if the attachment file type is unknown, an exception is thrown,
+ // so let it be handled by a browser window
+ Enigmail.msg.loadExternalURL(outFileUri.asciiSpec);
+ }
+ } else {
+ // open the attachment using an external application
+ Enigmail.msg.loadExternalURL(outFileUri.asciiSpec);
+ }
+ }
+ }
+ },
+
+ loadExternalURL(url) {
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadURI(Services.io.newURI(url));
+ },
+
+ // retrieves the most recent navigator window (opens one if need be)
+ loadURLInNavigatorWindow(url, aOpenFlag) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: loadURLInNavigatorWindow: " +
+ url +
+ ", " +
+ aOpenFlag +
+ "\n"
+ );
+
+ var navWindow;
+
+ // if this is a browser window, just use it
+ if ("document" in top) {
+ var possibleNavigator = top.document.getElementById("main-window");
+ if (
+ possibleNavigator &&
+ possibleNavigator.getAttribute("windowtype") == "navigator:browser"
+ ) {
+ navWindow = top;
+ }
+ }
+
+ // if not, get the most recently used browser window
+ if (!navWindow) {
+ var wm = Services.wm;
+ navWindow = wm.getMostRecentWindow("navigator:browser");
+ }
+
+ if (navWindow) {
+ if ("fixupAndLoadURIString" in navWindow) {
+ navWindow.fixupAndLoadURIString(url);
+ } else {
+ navWindow._content.location.href = url;
+ }
+ } else if (aOpenFlag) {
+ // if no browser window available and it's ok to open a new one, do so
+ navWindow = window.open(url, "Enigmail");
+ }
+
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: loadURLInNavigatorWindow: navWindow=" +
+ navWindow +
+ "\n"
+ );
+
+ return navWindow;
+ },
+
+ /**
+ * Open an encrypted attachment item.
+ */
+ attachmentItemClick(event) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: attachmentItemClick: event=" + event + "\n"
+ );
+
+ let attachment = event.currentTarget.attachment;
+ if (this.checkEncryptedAttach(attachment)) {
+ if (event.button === 0 && event.detail == 2) {
+ // double click
+ this.handleAttachment("openAttachment", attachment);
+ event.stopPropagation();
+ }
+ }
+ },
+
+ // decrypted and copy/move all selected messages in a target folder
+
+ async decryptToFolder(destFolder, move) {
+ let msgHdrs = gDBView.getSelectedMsgHdrs();
+ if (!msgHdrs || msgHdrs.length === 0) {
+ return;
+ }
+
+ let total = msgHdrs.length;
+ let failures = 0;
+ for (let msgHdr of msgHdrs) {
+ await EnigmailPersistentCrypto.cryptMessage(
+ msgHdr,
+ destFolder.URI,
+ move,
+ false
+ ).catch(err => {
+ failures++;
+ });
+ }
+
+ if (failures) {
+ let info = await document.l10n.formatValue(
+ "decrypt-and-copy-failures-multiple",
+ {
+ failures,
+ total,
+ }
+ );
+ Services.prompt.alert(null, document.title, info);
+ }
+ },
+
+ async searchKeysOnInternet(event) {
+ return KeyLookupHelper.lookupAndImportByEmail(
+ "interactive-import",
+ window,
+ event.currentTarget.parentNode.headerField?.emailAddress,
+ true
+ );
+ },
+
+ onUnloadEnigmail() {
+ window.removeEventListener("unload", Enigmail.msg.messengerClose);
+ window.removeEventListener(
+ "unload-enigmail",
+ Enigmail.msg.onUnloadEnigmail
+ );
+ window.removeEventListener("load-enigmail", Enigmail.msg.messengerStartup);
+
+ this.messageCleanup();
+
+ if (this.messagePane) {
+ this.messagePane.removeEventListener(
+ "unload",
+ Enigmail.msg.messageFrameUnload,
+ true
+ );
+ }
+
+ for (let c of this.changedAttributes) {
+ let elem = document.getElementById(c.id);
+ if (elem) {
+ elem.setAttribute(c.attrib, c.value);
+ }
+ }
+
+ this.messengerClose();
+
+ if (Enigmail.columnHandler) {
+ Enigmail.columnHandler.onUnloadEnigmail();
+ }
+ if (Enigmail.hdrView) {
+ Enigmail.hdrView.onUnloadEnigmail();
+ }
+
+ // eslint-disable-next-line no-global-assign
+ Enigmail = undefined;
+ },
+
+ /**
+ * Process key data from a message.
+ *
+ * @param {string} keyData - The key data.
+ * @param {boolean} isBinaryAutocrypt - false if ASCII armored data.
+ * @param {string} [description] - Key source description, if any.
+ */
+ async commonProcessAttachedKey(keyData, isBinaryAutocrypt, description) {
+ if (!keyData) {
+ return;
+ }
+
+ // Processing is slow for some types of keys.
+ // We want to avoid automatic key import/updates for users who
+ // have OpenPGP disabled (no account has an OpenPGP key configured).
+ if (
+ !MailServices.accounts.allIdentities.find(id =>
+ id.getUnicharAttribute("openpgp_key_id")
+ )
+ ) {
+ return;
+ }
+
+ let errorMsgObj = {};
+ let preview = await EnigmailKey.getKeyListFromKeyBlock(
+ keyData,
+ errorMsgObj,
+ true,
+ true,
+ false,
+ true
+ );
+
+ // If we cannot analyze the keyblock, or if it's empty, or if we
+ // got an error message, then the key is bad and shouldn't be used.
+ if (!preview || !preview.length || errorMsgObj.value) {
+ return;
+ }
+
+ this.fetchParticipants();
+
+ for (let newKey of preview) {
+ let oldKey = EnigmailKeyRing.getKeyById(newKey.fpr);
+ if (!oldKey) {
+ // If the key is unknown, an expired key cannot help us
+ // for anything new, so don't use it.
+ if (newKey.keyTrust == "e") {
+ continue;
+ }
+
+ // Potentially merge the revocation into CollectedKeysDB, it if
+ // already has that key.
+ if (newKey.keyTrust == "r") {
+ let db = await CollectedKeysDB.getInstance();
+ let existing = await db.findKeyForFingerprint(newKey.fpr);
+ if (existing) {
+ let key = await db.mergeExisting(newKey, newKey.pubKey, {
+ uri: `mid:${gMessage.messageId}`,
+ type: isBinaryAutocrypt ? "autocrypt" : "attachment",
+ description,
+ });
+ await db.storeKey(key);
+ Services.obs.notifyObservers(null, "openpgp-key-change");
+ }
+ continue;
+ }
+
+ // It doesn't make sense to import a public key,
+ // if we have a secret key for that email address.
+ // Because, if we are the owner of that email address, why would
+ // we need a public key referring to our own email address,
+ // sent to us by someone else?
+
+ let keyInOurName = false;
+ for (let userId of newKey.userIds) {
+ if (userId.type !== "uid") {
+ continue;
+ }
+ if (EnigmailTrust.isInvalid(userId.keyTrust)) {
+ continue;
+ }
+ if (
+ await EnigmailKeyRing.hasSecretKeyForEmail(
+ EnigmailFuncs.getEmailFromUserID(userId.userId).toLowerCase()
+ )
+ ) {
+ keyInOurName = true;
+ break;
+ }
+ }
+ if (keyInOurName) {
+ continue;
+ }
+
+ // Only advertise the key for import if it contains a user ID
+ // that points to the email author email address.
+ let relatedParticipantEmailAddress = null;
+ if (this.hasUserIdForEmail(newKey.userIds, this.authorEmail)) {
+ relatedParticipantEmailAddress = this.authorEmail;
+ }
+
+ if (relatedParticipantEmailAddress) {
+ // If it's a non expired, non revoked new key, in the email
+ // author's name (email address match), then offer it for
+ // manual (immediate) import.
+ let nextIndex = Enigmail.msg.attachedKeys.length;
+ let info = {
+ fpr: "0x" + newKey.fpr,
+ idx: nextIndex,
+ keyInfo: newKey,
+ binary: isBinaryAutocrypt,
+ };
+ Enigmail.msg.attachedSenderEmailKeysIndex.push(info);
+ Enigmail.msg.attachedKeys.push(newKey.pubKey);
+ }
+
+ // We want to collect keys for potential later use, however,
+ // we also want to avoid that an attacker can send us a large
+ // number of keys to poison our cache, so we only collect keys
+ // that are related to the author or one of the recipients.
+ // Also, we don't want a public key, if we already have a
+ // secret key for that email address.
+
+ if (!relatedParticipantEmailAddress) {
+ // Not related to the author
+ for (let toOrCc of this.toAndCCSet) {
+ if (this.hasUserIdForEmail(newKey.userIds, toOrCc)) {
+ // Might be ok to import, so remember to which email
+ // the key is related and leave the loop.
+ relatedParticipantEmailAddress = toOrCc;
+ break;
+ }
+ }
+ }
+
+ if (relatedParticipantEmailAddress) {
+ // It seems OK to import, however, don't import yet.
+ // Wait until after we have processed all attachments to
+ // the current message. Because we don't want to import
+ // multiple keys for the same email address, that wouldn't
+ // make sense. Remember the import candidate, and postpone
+ // until we are done looking at all attachments.
+
+ if (this.keyCollectCandidates.has(relatedParticipantEmailAddress)) {
+ // The email contains more than one public key for this
+ // email address.
+ this.keyCollectCandidates.set(relatedParticipantEmailAddress, {
+ skip: true,
+ });
+ } else {
+ let candidate = {};
+ candidate.skip = false;
+ candidate.newKeyObj = newKey;
+ candidate.pubKey = newKey.pubKey;
+ candidate.source = {
+ uri: `mid:${gMessage.messageId}`,
+ type: isBinaryAutocrypt ? "autocrypt" : "attachment",
+ description,
+ };
+ this.keyCollectCandidates.set(
+ relatedParticipantEmailAddress,
+ candidate
+ );
+ }
+ }
+
+ // done with processing for new keys (!oldKey)
+ continue;
+ }
+
+ // The key is known (we have an oldKey), then it makes sense to
+ // import, even if it's expired/revoked, to learn about the
+ // changed validity.
+
+ // Also, we auto import/merge such keys, even if the sender
+ // doesn't match any key user ID. Why is this useful?
+ // If I am Alice, and the email is from Bob, the email could have
+ // Charlie's revoked or extended key attached. It's useful for
+ // me to learn that.
+
+ // User IDs are another reason. The key might contain a new
+ // additional user ID, or a revoked user ID.
+ // That's relevant for Autocrypt headers, which only have one user
+ // ID. If we had imported the key with just one user ID in the
+ // past, and now we're being sent the same key for a different
+ // user ID, we must not skip it, even if it the validity is the
+ // same.
+ // Let's update on all possible changes of the user ID list,
+ // additions, removals, differences.
+
+ let shouldUpdate = false;
+
+ // new validity?
+ if (
+ oldKey.expiryTime < newKey.expiryTime ||
+ (oldKey.keyTrust != "r" && newKey.keyTrust == "r")
+ ) {
+ shouldUpdate = true;
+ } else if (
+ oldKey.userIds.length != newKey.userIds.length ||
+ !oldKey.userIds.every((el, ix) => el === newKey.userIds[ix])
+ ) {
+ shouldUpdate = true;
+ }
+
+ if (!shouldUpdate) {
+ continue;
+ }
+
+ if (
+ !(await EnigmailKeyRing.importKeyDataSilent(
+ window,
+ newKey.pubKey,
+ isBinaryAutocrypt,
+ "0x" + newKey.fpr
+ ))
+ ) {
+ console.debug(
+ "EnigmailKeyRing.importKeyDataSilent failed 0x" + newKey.fpr
+ );
+ }
+ }
+ },
+
+ /**
+ * Show the import key notification.
+ */
+ async unhideImportKeyBox() {
+ Enigmail.hdrView.notifyHasKeyAttached();
+ document.getElementById("openpgpKeyBox").removeAttribute("hidden");
+
+ // Check if the proposed key to import was previously accepted.
+ let hasAreadyAcceptedOther =
+ await PgpSqliteDb2.hasAnyPositivelyAcceptedKeyForEmail(
+ Enigmail.msg.authorEmail
+ );
+ if (hasAreadyAcceptedOther) {
+ Enigmail.msg.notificationBox.appendNotification(
+ "hasConflictingKeyOpenPGP",
+ {
+ label: await document.l10n.formatValue("openpgp-be-careful-new-key", {
+ email: Enigmail.msg.authorEmail,
+ }),
+ priority: Enigmail.msg.notificationBox.PRIORITY_INFO_HIGH,
+ },
+ null
+ );
+ }
+ },
+
+ /*
+ * This function is called from several places. Any call may trigger
+ * the final processing for this message, it depends on the amount
+ * of attachments present, and whether we decrypt immediately, or
+ * after a delay (for inline encryption).
+ */
+ async processAfterAttachmentsAndDecrypt() {
+ // Return early if message processing isn't ready yet.
+ if (!Enigmail.msg.allAttachmentsDone || !Enigmail.msg.messageDecryptDone) {
+ return;
+ }
+
+ // Return early if we haven't yet processed all attachments.
+ if (
+ Enigmail.msg.autoProcessPgpKeyAttachmentProcessed <
+ Enigmail.msg.autoProcessPgpKeyAttachmentCount
+ ) {
+ return;
+ }
+
+ if (Enigmail.msg.unhideMissingSigKeyBoxIsTODO) {
+ Enigmail.msg.unhideMissingSigKeyBox();
+ }
+
+ // We have already processed all attached pgp-keys, we're ready
+ // to make final decisions on how to notify the user about
+ // available or missing keys.
+ // If we already found a good key for the sender's email
+ // in attachments, then don't look at the autocrypt header.
+ if (Enigmail.msg.attachedSenderEmailKeysIndex.length) {
+ this.unhideImportKeyBox();
+ } else if (
+ Enigmail.msg.savedHeaders &&
+ "autocrypt" in Enigmail.msg.savedHeaders &&
+ Enigmail.msg.savedHeaders.autocrypt.length > 0 &&
+ "from" in currentHeaderData
+ ) {
+ let fromAddr = EnigmailFuncs.stripEmail(
+ currentHeaderData.from.headerValue
+ ).toLowerCase();
+ // There might be multiple headers, we only want the one
+ // matching the sender's address.
+ for (let ac of Enigmail.msg.savedHeaders.autocrypt) {
+ let acAddr = MimeParser.getParameter(ac, "addr");
+ if (fromAddr == acAddr) {
+ let senderAutocryptKey;
+ try {
+ senderAutocryptKey = atob(
+ MimeParser.getParameter(ac.replace(/ /g, ""), "keydata")
+ );
+ } catch {}
+ if (senderAutocryptKey) {
+ // Make sure to let the message load before doing potentially *very*
+ // time consuming auto processing (seconds!?).
+ await new Promise(resolve => ChromeUtils.idleDispatch(resolve));
+ await this.commonProcessAttachedKey(senderAutocryptKey, true);
+
+ if (Enigmail.msg.attachedSenderEmailKeysIndex.length) {
+ this.unhideImportKeyBox();
+ }
+ }
+ }
+ }
+ }
+
+ for (let gossipKey of EnigmailSingletons.lastDecryptedMessage.gossip) {
+ await this.commonProcessAttachedKey(gossipKey, true);
+ }
+
+ if (this.keyCollectCandidates && this.keyCollectCandidates.size) {
+ let db = await CollectedKeysDB.getInstance();
+
+ for (let candidate of this.keyCollectCandidates.values()) {
+ if (candidate.skip) {
+ continue;
+ }
+ // If key is known in the db: merge + update.
+ let key = await db.mergeExisting(
+ candidate.newKeyObj,
+ candidate.pubKey,
+ candidate.source
+ );
+
+ await db.storeKey(key);
+ Services.obs.notifyObservers(null, "openpgp-key-change");
+ }
+ }
+
+ // Should we notify the user about available encrypted nested parts,
+ // which have not been automatically decrypted?
+ if (
+ EnigmailSingletons.isRecentUriWithNestedEncryptedPart(
+ Enigmail.msg.getCurrentMsgUriSpec()
+ )
+ ) {
+ let buttons = [
+ {
+ "l10n-id": "openpgp-show-encrypted-parts",
+ popup: null,
+ callback(notification, button) {
+ top.viewEncryptedPart(Enigmail.msg.getCurrentMsgUriSpec());
+ return true; // keep notification
+ },
+ },
+ ];
+
+ Enigmail.msg.notificationBox.appendNotification(
+ "hasNestedEncryptedParts",
+ {
+ label: await document.l10n.formatValue(
+ "openpgp-has-nested-encrypted-parts"
+ ),
+ priority: Enigmail.msg.notificationBox.PRIORITY_INFO_HIGH,
+ },
+ buttons
+ );
+ }
+
+ document.dispatchEvent(
+ new CustomEvent("openpgpprocessed", {
+ detail: { messageDecryptDone: true },
+ })
+ );
+ },
+
+ async notifyEndAllAttachments() {
+ Enigmail.msg.allAttachmentsDone = true;
+
+ if (!Enigmail.msg.autoProcessPgpKeyAttachmentCount) {
+ await Enigmail.msg.processAfterAttachmentsAndDecrypt();
+ }
+ },
+
+ toAndCCSet: null,
+ authorEmail: "",
+
+ // Used to remember the list of keys that we might want to add to
+ // our cache of seen keys. Will be used after we are done looking
+ // at all attachments.
+ keyCollectCandidates: new Map(),
+
+ attachedKeys: [],
+ attachedSenderEmailKeysIndex: [], // each: {idx (to-attachedKeys), keyInfo, binary}
+
+ fetchParticipants() {
+ if (this.toAndCCSet) {
+ return;
+ }
+
+ // toAndCCSet non-null indicates that we already fetched.
+ this.toAndCCSet = new Set();
+
+ // This message may have already disappeared.
+ if (!gMessage) {
+ return;
+ }
+
+ let addresses = MailServices.headerParser.parseEncodedHeader(
+ gMessage.author
+ );
+ if (addresses.length) {
+ this.authorEmail = addresses[0].email.toLowerCase();
+ }
+
+ addresses = MailServices.headerParser.parseEncodedHeader(
+ gMessage.recipients + "," + gMessage.ccList
+ );
+ for (let addr of addresses) {
+ this.toAndCCSet.add(addr.email.toLowerCase());
+ }
+ },
+
+ hasUserIdForEmail(userIds, authorEmail) {
+ authorEmail = authorEmail.toLowerCase();
+
+ for (let id of userIds) {
+ if (id.type !== "uid") {
+ continue;
+ }
+
+ if (
+ EnigmailFuncs.getEmailFromUserID(id.userId).toLowerCase() == authorEmail
+ ) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ autoProcessPgpKeyAttachmentTransactionID: 0,
+ autoProcessPgpKeyAttachmentCount: 0,
+ autoProcessPgpKeyAttachmentProcessed: 0,
+ unhideMissingSigKeyBoxIsTODO: false,
+ unhideMissingSigKey: null,
+
+ autoProcessPgpKeyAttachment(attachment) {
+ if (
+ attachment.contentType != "application/pgp-keys" &&
+ !attachment.name.endsWith(".asc")
+ ) {
+ return;
+ }
+
+ Enigmail.msg.autoProcessPgpKeyAttachmentCount++;
+
+ let bufferListener = EnigmailStreams.newStringStreamListener(async data => {
+ // Make sure to let the message load before doing potentially *very*
+ // time consuming auto processing (seconds!?).
+ await new Promise(resolve => ChromeUtils.idleDispatch(resolve));
+ await this.commonProcessAttachedKey(data, false, attachment.name);
+ Enigmail.msg.autoProcessPgpKeyAttachmentProcessed++;
+ if (
+ Enigmail.msg.autoProcessPgpKeyAttachmentProcessed ==
+ Enigmail.msg.autoProcessPgpKeyAttachmentCount
+ ) {
+ await Enigmail.msg.processAfterAttachmentsAndDecrypt();
+ }
+ });
+ let msgUri = Services.io.newURI(attachment.url);
+ let channel = EnigmailStreams.createChannel(msgUri);
+ channel.asyncOpen(bufferListener, msgUri);
+ },
+
+ /**
+ * Populate the message security popup panel with OpenPGP data.
+ */
+ async loadOpenPgpMessageSecurityInfo() {
+ let sigInfoWithDateLabel = null;
+ let sigInfoLabel = null;
+ let sigInfo = null;
+ let sigClass = null;
+ let wantToShowDate = false;
+
+ // All scenarios that set wantToShowDate to true should set both
+ // sigInfoWithDateLabel and sigInfoLabel, to ensure we have a
+ // fallback label, if the date is unavailable.
+ switch (Enigmail.hdrView.msgSignatureState) {
+ case EnigmailConstants.MSG_SIG_NONE:
+ sigInfoLabel = "openpgp-no-sig";
+ sigClass = "none";
+ sigInfo = "openpgp-no-sig-info";
+ break;
+
+ case EnigmailConstants.MSG_SIG_UNCERTAIN_KEY_UNAVAILABLE:
+ sigInfoLabel = "openpgp-uncertain-sig";
+ sigClass = "unknown";
+ sigInfo = "openpgp-sig-uncertain-no-key";
+ break;
+
+ case EnigmailConstants.MSG_SIG_UNCERTAIN_UID_MISMATCH:
+ sigInfoLabel = "openpgp-uncertain-sig";
+ sigInfoWithDateLabel = "openpgp-uncertain-sig-with-date";
+ wantToShowDate = true;
+ sigClass = "mismatch";
+ sigInfo = "openpgp-sig-uncertain-uid-mismatch";
+ break;
+
+ case EnigmailConstants.MSG_SIG_UNCERTAIN_KEY_NOT_ACCEPTED:
+ sigInfoLabel = "openpgp-uncertain-sig";
+ sigInfoWithDateLabel = "openpgp-uncertain-sig-with-date";
+ wantToShowDate = true;
+ sigClass = "unknown";
+ sigInfo = "openpgp-sig-uncertain-not-accepted";
+ break;
+
+ case EnigmailConstants.MSG_SIG_INVALID_KEY_REJECTED:
+ sigInfoLabel = "openpgp-invalid-sig";
+ sigInfoWithDateLabel = "openpgp-invalid-sig-with-date";
+ wantToShowDate = true;
+ sigClass = "mismatch";
+ sigInfo = "openpgp-sig-invalid-rejected";
+ break;
+
+ case EnigmailConstants.MSG_SIG_INVALID:
+ sigInfoLabel = "openpgp-invalid-sig";
+ sigInfoWithDateLabel = "openpgp-invalid-sig-with-date";
+ wantToShowDate = true;
+ sigClass = "mismatch";
+ sigInfo = "openpgp-sig-invalid-technical-problem";
+ break;
+
+ case EnigmailConstants.MSG_SIG_VALID_KEY_UNVERIFIED:
+ sigInfoLabel = "openpgp-good-sig";
+ sigInfoWithDateLabel = "openpgp-good-sig-with-date";
+ wantToShowDate = true;
+ sigClass = "unverified";
+ sigInfo = "openpgp-sig-valid-unverified";
+ break;
+
+ case EnigmailConstants.MSG_SIG_VALID_KEY_VERIFIED:
+ sigInfoLabel = "openpgp-good-sig";
+ sigInfoWithDateLabel = "openpgp-good-sig-with-date";
+ wantToShowDate = true;
+ sigClass = "verified";
+ sigInfo = "openpgp-sig-valid-verified";
+ break;
+
+ case EnigmailConstants.MSG_SIG_VALID_SELF:
+ sigInfoLabel = "openpgp-good-sig";
+ sigInfoWithDateLabel = "openpgp-good-sig-with-date";
+ wantToShowDate = true;
+ sigClass = "ok";
+ sigInfo = "openpgp-sig-valid-own-key";
+ break;
+
+ default:
+ console.error(
+ "Unexpected msgSignatureState: " + Enigmail.hdrView.msgSignatureState
+ );
+ }
+
+ let signatureLabel = document.getElementById("signatureLabel");
+ if (wantToShowDate && Enigmail.hdrView.msgSignatureDate) {
+ let date = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "short",
+ timeStyle: "short",
+ }).format(Enigmail.hdrView.msgSignatureDate);
+ document.l10n.setAttributes(signatureLabel, sigInfoWithDateLabel, {
+ date,
+ });
+ } else {
+ document.l10n.setAttributes(signatureLabel, sigInfoLabel);
+ }
+
+ // Remove the second class to properly update the signature icon.
+ signatureLabel.classList.remove(signatureLabel.classList.item(1));
+ signatureLabel.classList.add(sigClass);
+
+ let signatureExplanation = document.getElementById("signatureExplanation");
+ // eslint-disable-next-line mozilla/prefer-formatValues
+ signatureExplanation.textContent = await document.l10n.formatValue(sigInfo);
+
+ let encInfoLabel = null;
+ let encInfo = null;
+ let encClass = null;
+
+ switch (Enigmail.hdrView.msgEncryptionState) {
+ case EnigmailConstants.MSG_ENC_NONE:
+ encInfoLabel = "openpgp-enc-none";
+ encInfo = "openpgp-enc-none-label";
+ encClass = "none";
+ break;
+
+ case EnigmailConstants.MSG_ENC_NO_SECRET_KEY:
+ encInfoLabel = "openpgp-enc-invalid-label";
+ encInfo = "openpgp-enc-invalid";
+ encClass = "notok";
+ break;
+
+ case EnigmailConstants.MSG_ENC_FAILURE:
+ encInfoLabel = "openpgp-enc-invalid-label";
+ encInfo = "openpgp-enc-clueless";
+ encClass = "notok";
+ break;
+
+ case EnigmailConstants.MSG_ENC_OK:
+ encInfoLabel = "openpgp-enc-valid-label";
+ encInfo = "openpgp-enc-valid";
+ encClass = "ok";
+ break;
+
+ default:
+ console.error(
+ "Unexpected msgEncryptionState: " +
+ Enigmail.hdrView.msgEncryptionState
+ );
+ }
+
+ document.getElementById("techLabel").textContent = "- OpenPGP";
+
+ let encryptionLabel = document.getElementById("encryptionLabel");
+ // eslint-disable-next-line mozilla/prefer-formatValues
+ encryptionLabel.textContent = await document.l10n.formatValue(encInfoLabel);
+
+ // Remove the second class to properly update the encryption icon.
+ encryptionLabel.classList.remove(encryptionLabel.classList.item(1));
+ encryptionLabel.classList.add(encClass);
+
+ document.getElementById("encryptionExplanation").textContent =
+ // eslint-disable-next-line mozilla/prefer-formatValues
+ await document.l10n.formatValue(encInfo);
+
+ if (Enigmail.hdrView.msgSignatureKeyId) {
+ let sigKeyInfo = EnigmailKeyRing.getKeyById(
+ Enigmail.hdrView.msgSignatureKeyId
+ );
+
+ document.getElementById("signatureKey").collapsed = false;
+
+ if (
+ sigKeyInfo &&
+ sigKeyInfo.keyId != Enigmail.hdrView.msgSignatureKeyId
+ ) {
+ document.l10n.setAttributes(
+ document.getElementById("signatureKeyId"),
+ "openpgp-sig-key-id-with-subkey-id",
+ {
+ key: `0x${sigKeyInfo.keyId}`,
+ subkey: `0x${Enigmail.hdrView.msgSignatureKeyId}`,
+ }
+ );
+ } else {
+ document.l10n.setAttributes(
+ document.getElementById("signatureKeyId"),
+ "openpgp-sig-key-id",
+ {
+ key: `0x${Enigmail.hdrView.msgSignatureKeyId}`,
+ }
+ );
+ }
+
+ if (sigKeyInfo) {
+ document.getElementById("viewSignatureKey").collapsed = false;
+ gSigKeyId = Enigmail.hdrView.msgSignatureKeyId;
+ }
+ }
+
+ let myIdToSkipInList;
+ if (
+ Enigmail.hdrView.msgEncryptionKeyId &&
+ Enigmail.hdrView.msgEncryptionKeyId.keyId
+ ) {
+ myIdToSkipInList = Enigmail.hdrView.msgEncryptionKeyId.keyId;
+
+ // If we were given a separate primaryKeyId, it means keyId is a subkey.
+ let havePrimaryId = !!Enigmail.hdrView.msgEncryptionKeyId.primaryKeyId;
+ document.getElementById("encryptionKey").collapsed = false;
+
+ if (havePrimaryId) {
+ document.l10n.setAttributes(
+ document.getElementById("encryptionKeyId"),
+ "openpgp-enc-key-with-subkey-id",
+ {
+ key: `0x${Enigmail.hdrView.msgEncryptionKeyId.primaryKeyId}`,
+ subkey: `0x${Enigmail.hdrView.msgEncryptionKeyId.keyId}`,
+ }
+ );
+ } else {
+ document.l10n.setAttributes(
+ document.getElementById("encryptionKeyId"),
+ "openpgp-enc-key-id",
+ {
+ key: `0x${Enigmail.hdrView.msgEncryptionKeyId.keyId}`,
+ }
+ );
+ }
+
+ if (
+ EnigmailKeyRing.getKeyById(Enigmail.hdrView.msgEncryptionKeyId.keyId)
+ ) {
+ document.getElementById("viewEncryptionKey").collapsed = false;
+ gEncKeyId = Enigmail.hdrView.msgEncryptionKeyId.keyId;
+ }
+ }
+
+ let otherLabel = document.getElementById("otherLabel");
+ if (myIdToSkipInList) {
+ document.l10n.setAttributes(otherLabel, "openpgp-other-enc-all-key-ids");
+ } else {
+ document.l10n.setAttributes(
+ otherLabel,
+ "openpgp-other-enc-additional-key-ids"
+ );
+ }
+
+ if (!Enigmail.hdrView.msgEncryptionAllKeyIds) {
+ return;
+ }
+
+ let keyList = document.getElementById("otherEncryptionKeysList");
+ // Remove all the previously populated keys.
+ while (keyList.lastChild) {
+ keyList.removeChild(keyList.lastChild);
+ }
+
+ let showExtraKeysList = false;
+ for (let key of Enigmail.hdrView.msgEncryptionAllKeyIds) {
+ if (key.keyId == myIdToSkipInList) {
+ continue;
+ }
+
+ let container = document.createXULElement("vbox");
+ container.classList.add("other-key-row");
+
+ let havePrimaryId2 = !!key.primaryKeyId;
+ let keyInfo = EnigmailKeyRing.getKeyById(
+ havePrimaryId2 ? key.primaryKeyId : key.keyId
+ );
+
+ // Use textContent for label XUl elements to enable text wrapping.
+ let name = document.createXULElement("label");
+ name.classList.add("openpgp-key-name");
+ name.setAttribute("context", "simpleCopyPopup");
+ if (keyInfo) {
+ name.textContent = keyInfo.userId;
+ } else {
+ document.l10n.setAttributes(name, "openpgp-other-enc-all-key-ids");
+ }
+
+ let id = document.createXULElement("label");
+ id.setAttribute("context", "simpleCopyPopup");
+ id.classList.add("openpgp-key-id");
+ id.textContent = havePrimaryId2
+ ? ` 0x${key.primaryKeyId} (0x${key.keyId})`
+ : ` 0x${key.keyId}`;
+
+ container.appendChild(name);
+ container.appendChild(id);
+
+ keyList.appendChild(container);
+ showExtraKeysList = true;
+ }
+
+ // Show extra keys if present in the message.
+ document.getElementById("otherEncryptionKeys").collapsed =
+ !showExtraKeysList;
+ },
+};
+
+window.addEventListener(
+ "load-enigmail",
+ Enigmail.msg.messengerStartup.bind(Enigmail.msg)
+);
+window.addEventListener(
+ "unload",
+ Enigmail.msg.messengerClose.bind(Enigmail.msg)
+);
+window.addEventListener(
+ "unload-enigmail",
+ Enigmail.msg.onUnloadEnigmail.bind(Enigmail.msg)
+);
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.js b/comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.js
new file mode 100644
index 0000000000..1ff4c2c27e
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.js
@@ -0,0 +1,181 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+function onLoad() {
+ document.documentElement.style.minHeight = "120px";
+ var dlg = document.getElementById("enigmailMsgBox");
+ dlg.getButton("cancel").setAttribute("hidden", "true");
+ dlg.getButton("extra1").setAttribute("hidden", "true");
+ dlg.getButton("extra2").setAttribute("hidden", "true");
+
+ document.getElementById("filler").style.maxWidth =
+ screen.availWidth - 50 + "px";
+
+ let args = window.arguments[0];
+ let msgtext = args.msgtext;
+ let button1 = args.button1;
+ let button2 = args.button2;
+ let button3 = args.button3;
+ let buttonCancel = args.cancelButton;
+ let checkboxLabel = args.checkboxLabel;
+
+ if (args.iconType) {
+ let icn = document.getElementById("infoImage");
+ icn.removeAttribute("collapsed");
+ let iconClass = "";
+
+ switch (args.iconType) {
+ case 2:
+ iconClass = "question-icon";
+ break;
+ case 3:
+ iconClass = "alert-icon";
+ break;
+ case 4:
+ iconClass = "error-icon";
+ break;
+ default:
+ iconClass = "message-icon";
+ }
+ icn.setAttribute("class", "spaced " + iconClass);
+ }
+
+ if (args.dialogTitle) {
+ if (AppConstants.platform == "macosx") {
+ let t = document.getElementById("macosDialogTitle");
+ t.setAttribute("value", args.dialogTitle);
+ t.removeAttribute("collapsed");
+ }
+
+ dlg.setAttribute("title", args.dialogTitle);
+ } else {
+ document.l10n.setAttributes(dlg, "enig-alert-title");
+ }
+
+ if (button1) {
+ setButton("accept", button1);
+ }
+ if (button2) {
+ setButton("extra1", button2);
+ }
+ if (button3) {
+ setButton("extra2", button3);
+ }
+ if (buttonCancel) {
+ setButton("cancel", buttonCancel);
+ }
+
+ if (checkboxLabel) {
+ let checkboxElem = document.getElementById("theCheckBox");
+ checkboxElem.setAttribute("label", checkboxLabel);
+ document.getElementById("checkboxContainer").removeAttribute("hidden");
+ }
+
+ dlg.getButton("accept").focus();
+ let textbox = document.getElementById("msgtext");
+ textbox.appendChild(textbox.ownerDocument.createTextNode(msgtext));
+
+ window.addEventListener("keypress", onKeyPress);
+ setTimeout(resizeDlg, 0);
+}
+
+function resizeDlg() {
+ let availHeight = screen.availHeight;
+ if (window.outerHeight > availHeight - 100) {
+ let box = document.getElementById("msgContainer");
+ let dlg = document.getElementById("enigmailMsgBox");
+ let btnHeight = dlg.getButton("accept").parentNode.clientHeight + 20;
+ let boxHeight = box.clientHeight;
+ let dlgHeight = dlg.clientHeight;
+
+ box.setAttribute("style", "overflow: auto;");
+ box.setAttribute(
+ "height",
+ boxHeight - btnHeight - (dlgHeight - availHeight)
+ );
+ window.resizeTo(window.outerWidth, availHeight);
+ }
+}
+
+function setButton(buttonId, label) {
+ var labelType = buttonId;
+
+ var dlg = document.getElementById("enigmailMsgBox");
+ var elem = dlg.getButton(labelType);
+
+ var i = label.indexOf(":");
+ if (i === 0) {
+ elem = dlg.getButton(label.substr(1));
+ elem.setAttribute("hidden", "false");
+ elem.setAttribute("oncommand", "dlgClose('" + buttonId + "')");
+ return;
+ }
+ if (i > 0) {
+ labelType = label.substr(0, i);
+ label = label.substr(i + 1);
+ elem = dlg.getButton(labelType);
+ }
+ i = label.indexOf("&");
+ if (i >= 0) {
+ var c = label.substr(i + 1, 1);
+ if (c != "&") {
+ elem.setAttribute("accesskey", c);
+ }
+ label = label.substr(0, i) + label.substr(i + 1);
+ }
+ elem.setAttribute("label", label);
+ elem.setAttribute("oncommand", "dlgClose('" + buttonId + "')");
+ elem.removeAttribute("hidden");
+}
+
+function dlgClose(buttonId) {
+ let buttonNumber = 99;
+
+ switch (buttonId) {
+ case "accept":
+ buttonNumber = 0;
+ break;
+ case "extra1":
+ buttonNumber = 1;
+ break;
+ case "extra2":
+ buttonNumber = 2;
+ break;
+ case "cancel":
+ buttonNumber = -1;
+ }
+
+ window.arguments[1].value = buttonNumber;
+ window.arguments[1].checked =
+ document.getElementById("theCheckBox").getAttribute("checked") == "true";
+ window.close();
+}
+
+function checkboxCb() {
+ // do nothing
+}
+
+async function copyToClipbrd() {
+ let s = window.getSelection().toString();
+ return navigator.clipboard.writeText(s);
+}
+
+function onKeyPress(event) {
+ if (event.key == "c" && event.getModifierState("Accel")) {
+ copyToClipbrd();
+ event.stopPropagation();
+ }
+}
+
+document.addEventListener("dialogaccept", function (event) {
+ dlgClose("accept");
+});
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.xhtml b/comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.xhtml
new file mode 100644
index 0000000000..3fec668c8c
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.xhtml
@@ -0,0 +1,71 @@
+<?xml version="1.0"?>
+<!--
+ * 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/.
+-->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/enigmail.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?>
+
+<!DOCTYPE window [ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD; ]>
+
+<window
+ title=""
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ buttons="accept,help,cancel,extra1,extra2"
+ onload="onLoad();"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <dialog id="enigmailMsgBox" buttonpack="center">
+ <script
+ type="application/x-javascript"
+ src="chrome://openpgp/content/ui/enigmailMsgBox.js"
+ />
+ <linkset>
+ <html:link rel="localization" href="messenger/openpgp/openpgp.ftl" />
+ </linkset>
+
+ <popupset>
+ <menupopup id="ctxmenu">
+ <menuitem
+ data-l10n-id="openpgp-copy-cmd-label"
+ oncommand="copyToClipbrd()"
+ />
+ </menupopup>
+ </popupset>
+
+ <hbox id="filler" style="min-width: 0%">
+ <spacer style="width: 29em" />
+ </hbox>
+
+ <html:div class="grid-two-column-fr">
+ <html:div class="flex-items-center">
+ <image id="infoImage" class="spaced" collapsed="true" />
+ </html:div>
+ <html:div class="flex-items-center">
+ <vbox id="infoContainer" pack="center">
+ <label
+ id="macosDialogTitle"
+ collapsed="true"
+ class="enigmailDialogTitle"
+ />
+ <vbox id="msgContainer" style="max-width: 45em">
+ <description
+ id="msgtext"
+ context="ctxmenu"
+ noinitialfocus="true"
+ class="enigmailDialogBody"
+ />
+ </vbox>
+ </vbox>
+ </html:div>
+ <html:div id="checkboxContainer" class="grid-item-col2" hidden="hidden">
+ <checkbox id="theCheckBox" oncommand="checkboxCb()" />
+ </html:div>
+ </html:div>
+ </dialog>
+</window>
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailMsgComposeOverlay.js b/comm/mail/extensions/openpgp/content/ui/enigmailMsgComposeOverlay.js
new file mode 100644
index 0000000000..db973f0ee9
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailMsgComposeOverlay.js
@@ -0,0 +1,3034 @@
+/*
+ * 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);
+ }
+});
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailMsgHdrViewOverlay.js b/comm/mail/extensions/openpgp/content/ui/enigmailMsgHdrViewOverlay.js
new file mode 100644
index 0000000000..5bb4619793
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailMsgHdrViewOverlay.js
@@ -0,0 +1,1214 @@
+/* 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 ../../../../base/content/aboutMessage.js */
+/* import-globals-from ../../../../base/content/msgHdrView.js */
+/* import-globals-from ../../../smime/content/msgHdrViewSMIMEOverlay.js */
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
+ EnigmailCore: "chrome://openpgp/content/modules/core.jsm",
+ EnigmailDialog: "chrome://openpgp/content/modules/dialog.jsm",
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ EnigmailKey: "chrome://openpgp/content/modules/key.jsm",
+ EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ EnigmailMime: "chrome://openpgp/content/modules/mime.jsm",
+ EnigmailMsgRead: "chrome://openpgp/content/modules/msgRead.jsm",
+ EnigmailSingletons: "chrome://openpgp/content/modules/singletons.jsm",
+ EnigmailURIs: "chrome://openpgp/content/modules/uris.jsm",
+ EnigmailVerify: "chrome://openpgp/content/modules/mimeVerify.jsm",
+ EnigmailWindows: "chrome://openpgp/content/modules/windows.jsm",
+ // EnigmailWks: "chrome://openpgp/content/modules/webKey.jsm",
+});
+
+Enigmail.hdrView = {
+ lastEncryptedMsgKey: null,
+ lastEncryptedUri: null,
+ flexbuttonAction: null,
+
+ msgSignedStateString: null,
+ msgEncryptedStateString: null,
+ msgSignatureState: EnigmailConstants.MSG_SIG_NONE,
+ msgEncryptionState: EnigmailConstants.MSG_ENC_NONE,
+ msgSignatureKeyId: "",
+ msgSignatureDate: null,
+ msgEncryptionKeyId: null,
+ msgEncryptionAllKeyIds: null,
+ msgHasKeyAttached: false,
+
+ ignoreStatusFromMimePart: "",
+ receivedStatusFromParts: new Set(),
+
+ reset() {
+ this.msgSignedStateString = null;
+ this.msgEncryptedStateString = null;
+ this.msgSignatureState = EnigmailConstants.MSG_SIG_NONE;
+ this.msgEncryptionState = EnigmailConstants.MSG_ENC_NONE;
+ this.msgSignatureKeyId = "";
+ this.msgSignatureDate = null;
+ this.msgEncryptionKeyId = null;
+ this.msgEncryptionAllKeyIds = null;
+ this.msgHasKeyAttached = false;
+ for (let value of ["decryptionFailed", "brokenExchange"]) {
+ Enigmail.msg.removeNotification(value);
+ }
+ this.ignoreStatusFromMimePart = "";
+ this.receivedStatusFromParts = new Set();
+ },
+
+ hdrViewLoad() {
+ EnigmailLog.DEBUG("enigmailMsgHdrViewOverlay.js: this.hdrViewLoad\n");
+
+ this.msgHdrViewLoad();
+
+ let addrPopup = document.getElementById("emailAddressPopup");
+ if (addrPopup) {
+ addrPopup.addEventListener(
+ "popupshowing",
+ Enigmail.hdrView.displayAddressPopup.bind(addrPopup)
+ );
+ }
+
+ // Thunderbird
+ let attCtx = document.getElementById("attachmentItemContext");
+ if (attCtx) {
+ attCtx.addEventListener(
+ "popupshowing",
+ this.onShowAttachmentContextMenu.bind(Enigmail.hdrView)
+ );
+ }
+ },
+
+ displayAddressPopup(event) {
+ let target = event.target;
+ EnigmailFuncs.collapseAdvanced(target, "hidden");
+ },
+
+ statusBarHide() {
+ /* elements might not have been set yet, so we try and ignore */
+ try {
+ this.reset();
+
+ Enigmail.msg.setAttachmentReveal(null);
+ if (Enigmail.msg.securityInfo) {
+ Enigmail.msg.securityInfo.statusFlags = 0;
+ }
+
+ let bodyElement = document.getElementById("messagepane");
+ bodyElement.removeAttribute("collapsed");
+ } catch (ex) {
+ console.debug(ex);
+ }
+ },
+
+ updatePgpStatus(
+ exitCode,
+ statusFlags,
+ extStatusFlags,
+ keyId,
+ userId,
+ sigDetails,
+ errorMsg,
+ blockSeparation,
+ encToDetails,
+ xtraStatus,
+ mimePartNumber
+ ) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: this.updatePgpStatus: exitCode=" +
+ exitCode +
+ ", statusFlags=" +
+ statusFlags +
+ ", extStatusFlags=" +
+ extStatusFlags +
+ ", keyId=" +
+ keyId +
+ ", userId=" +
+ userId +
+ ", " +
+ errorMsg +
+ "\n"
+ );
+
+ /*
+ if (
+ Enigmail.msg.securityInfo &&
+ Enigmail.msg.securityInfo.xtraStatus &&
+ Enigmail.msg.securityInfo.xtraStatus === "wks-request"
+ ) {
+ return;
+ }
+ */
+
+ if (gMessageURI) {
+ this.lastEncryptedMsgKey = gMessageURI;
+ }
+
+ if (!errorMsg) {
+ errorMsg = "";
+ } else {
+ console.debug("OpenPGP error status: " + errorMsg);
+ }
+
+ var replaceUid = null;
+ if (keyId && gMessage) {
+ replaceUid = EnigmailMsgRead.matchUidToSender(keyId, gMessage.author);
+ }
+
+ if (!replaceUid && userId) {
+ replaceUid = userId.replace(/\n.*$/gm, "");
+ }
+
+ if (
+ Enigmail.msg.savedHeaders &&
+ "x-pgp-encoding-format" in Enigmail.msg.savedHeaders &&
+ Enigmail.msg.savedHeaders["x-pgp-encoding-format"].search(
+ /partitioned/i
+ ) === 0
+ ) {
+ if (currentAttachments && currentAttachments.length) {
+ Enigmail.msg.setAttachmentReveal(currentAttachments);
+ }
+ }
+
+ if (userId && replaceUid) {
+ // no EnigmailData.convertGpgToUnicode here; strings are already UTF-8
+ replaceUid = replaceUid.replace(/\\[xe]3a/gi, ":");
+ errorMsg = errorMsg.replace(userId, replaceUid);
+ }
+
+ var errorLines = "";
+
+ if (exitCode == EnigmailConstants.POSSIBLE_PGPMIME) {
+ exitCode = 0;
+ } else if (errorMsg) {
+ // no EnigmailData.convertGpgToUnicode here; strings are already UTF-8
+ errorLines = errorMsg.split(/\r?\n/);
+ }
+
+ if (errorLines && errorLines.length > 22) {
+ // Retain only first twenty lines and last two lines of error message
+ var lastLines =
+ errorLines[errorLines.length - 2] +
+ "\n" +
+ errorLines[errorLines.length - 1] +
+ "\n";
+
+ while (errorLines.length > 20) {
+ errorLines.pop();
+ }
+
+ errorMsg = errorLines.join("\n") + "\n...\n" + lastLines;
+ }
+
+ let encryptedMimePart = "";
+ if (statusFlags & EnigmailConstants.PGP_MIME_ENCRYPTED) {
+ encryptedMimePart = mimePartNumber;
+ }
+
+ var msgSigned =
+ statusFlags &
+ (EnigmailConstants.BAD_SIGNATURE |
+ EnigmailConstants.GOOD_SIGNATURE |
+ EnigmailConstants.EXPIRED_KEY_SIGNATURE |
+ EnigmailConstants.EXPIRED_SIGNATURE |
+ EnigmailConstants.UNCERTAIN_SIGNATURE |
+ EnigmailConstants.REVOKED_KEY |
+ EnigmailConstants.EXPIRED_KEY_SIGNATURE |
+ EnigmailConstants.EXPIRED_SIGNATURE);
+
+ if (msgSigned && statusFlags & EnigmailConstants.IMPORTED_KEY) {
+ console.debug("unhandled status IMPORTED_KEY");
+ statusFlags &= ~EnigmailConstants.IMPORTED_KEY;
+ }
+
+ // TODO: visualize the following signature attributes,
+ // cross-check with corresponding email attributes
+ // - date
+ // - signer uid
+ // - signer key
+ // - signing and hash alg
+
+ this.msgSignatureKeyId = keyId;
+
+ if (encToDetails) {
+ this.msgEncryptionKeyId = encToDetails.myRecipKey;
+ this.msgEncryptionAllKeyIds = encToDetails.allRecipKeys;
+ }
+
+ this.msgSignatureDate = sigDetails?.sigDate;
+
+ let tmp = {
+ statusFlags,
+ extStatusFlags,
+ keyId,
+ userId,
+ msgSigned,
+ blockSeparation,
+ xtraStatus,
+ encryptedMimePart,
+ };
+ Enigmail.msg.securityInfo = tmp;
+
+ //Enigmail.msg.createArtificialAutocryptHeader();
+
+ /*
+ if (statusFlags & EnigmailConstants.UNCERTAIN_SIGNATURE) {
+ this.tryImportAutocryptHeader();
+ }
+ */
+
+ this.updateStatusFlags(mimePartNumber);
+ this.updateMsgDb();
+ },
+
+ /**
+ * Check whether we got a WKS request
+ */
+ /*
+ checkWksConfirmRequest(jsonStr) {
+ let requestObj;
+ try {
+ requestObj = JSON.parse(jsonStr);
+ } catch (ex) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: checkWksConfirmRequest parsing JSON failed\n"
+ );
+ return;
+ }
+
+ if (
+ "type" in requestObj &&
+ requestObj.type.toLowerCase() === "confirmation-request"
+ ) {
+ EnigmailWks.getWksClientPathAsync(window, function(wksClientPath) {
+ if (!wksClientPath) {
+ return;
+ }
+
+ Enigmail.hdrView.displayFlexAction(
+ "Web Key Directory Confirmation Request",
+ "Confirm Request",
+ "wks-request"
+ );
+ Enigmail.hdrView.displayWksMessage();
+ });
+ } else {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: checkWksConfirmRequest failed condition\n"
+ );
+ }
+ },
+ */
+
+ /**
+ * Display a localized message in lieu of the original message text
+ */
+ /*
+ displayWksMessage() {
+ EnigmailLog.DEBUG("enigmailMsgHdrViewOverlay.js: displayWksMessage()\n");
+
+ if (Enigmail.msg.securityInfo.xtraStatus === "wks-request") {
+ let enigMsgPane = document.getElementById("enigmailMsgDisplay");
+ let bodyElement = document.getElementById("messagepane");
+ bodyElement.setAttribute("collapsed", true);
+ enigMsgPane.removeAttribute("collapsed");
+ enigMsgPane.textContent = "This message has been sent by your email provider to confirm deployment of your OpenPGP public key\nin their Web Key Directory.\nProviding your public key helps others to discover your key and thus being able to encrypt messages to you.\n\nIf you want to deploy your key in the Web Key Directory now, please click on the button "Confirm Request" in the status bar.\nOtherwise, simply ignore this message."
+ );
+ }
+ },
+ */
+
+ /**
+ * Update the various variables that track the OpenPGP status of
+ * the current message.
+ *
+ * @param {string} triggeredByMimePartNumber - the MIME part that
+ * was processed and has triggered this status update request.
+ */
+ async updateStatusFlags(triggeredByMimePartNumber) {
+ let secInfo = Enigmail.msg.securityInfo;
+ let statusFlags = secInfo.statusFlags;
+ let extStatusFlags =
+ "extStatusFlags" in secInfo ? secInfo.extStatusFlags : 0;
+
+ let signed;
+ let encrypted;
+
+ if (
+ statusFlags &
+ (EnigmailConstants.DECRYPTION_FAILED |
+ EnigmailConstants.DECRYPTION_INCOMPLETE)
+ ) {
+ encrypted = "notok";
+ let unhideBar = false;
+ let infoId;
+ if (statusFlags & EnigmailConstants.NO_SECKEY) {
+ this.msgEncryptionState = EnigmailConstants.MSG_ENC_NO_SECRET_KEY;
+
+ unhideBar = true;
+ infoId = "openpgp-cannot-decrypt-because-missing-key";
+ } else {
+ this.msgEncryptionState = EnigmailConstants.MSG_ENC_FAILURE;
+ if (statusFlags & EnigmailConstants.MISSING_MDC) {
+ unhideBar = true;
+ infoId = "openpgp-cannot-decrypt-because-mdc";
+ }
+ }
+
+ if (unhideBar) {
+ Enigmail.msg.notificationBox.appendNotification(
+ "decryptionFailed",
+ {
+ label: await document.l10n.formatValue(infoId),
+ image: "chrome://global/skin/icons/warning.svg",
+ priority: Enigmail.msg.notificationBox.PRIORITY_CRITICAL_MEDIUM,
+ },
+ null
+ );
+ }
+
+ this.msgSignatureState = EnigmailConstants.MSG_SIG_NONE;
+ } else if (statusFlags & EnigmailConstants.DECRYPTION_OKAY) {
+ EnigmailURIs.rememberEncryptedUri(this.lastEncryptedMsgKey);
+ encrypted = "ok";
+ this.msgEncryptionState = EnigmailConstants.MSG_ENC_OK;
+ if (secInfo.xtraStatus && secInfo.xtraStatus == "buggyMailFormat") {
+ console.log(
+ await document.l10n.formatValue("decrypted-msg-with-format-error")
+ );
+ }
+ }
+
+ if (
+ statusFlags &
+ (EnigmailConstants.BAD_SIGNATURE |
+ EnigmailConstants.REVOKED_KEY |
+ EnigmailConstants.EXPIRED_KEY_SIGNATURE |
+ EnigmailConstants.EXPIRED_SIGNATURE)
+ ) {
+ if (statusFlags & EnigmailConstants.INVALID_RECIPIENT) {
+ this.msgSignatureState = EnigmailConstants.MSG_SIG_INVALID_KEY_REJECTED;
+ } else {
+ this.msgSignatureState = EnigmailConstants.MSG_SIG_INVALID;
+ }
+ signed = "notok";
+ } else if (statusFlags & EnigmailConstants.GOOD_SIGNATURE) {
+ if (statusFlags & EnigmailConstants.TRUSTED_IDENTITY) {
+ this.msgSignatureState = EnigmailConstants.MSG_SIG_VALID_KEY_VERIFIED;
+ signed = "verified";
+ } else if (extStatusFlags & EnigmailConstants.EXT_SELF_IDENTITY) {
+ signed = "ok";
+ this.msgSignatureState = EnigmailConstants.MSG_SIG_VALID_SELF;
+ } else {
+ signed = "unverified";
+ this.msgSignatureState = EnigmailConstants.MSG_SIG_VALID_KEY_UNVERIFIED;
+ }
+ } else if (statusFlags & EnigmailConstants.UNCERTAIN_SIGNATURE) {
+ signed = "unknown";
+ if (statusFlags & EnigmailConstants.INVALID_RECIPIENT) {
+ signed = "mismatch";
+ this.msgSignatureState =
+ EnigmailConstants.MSG_SIG_UNCERTAIN_UID_MISMATCH;
+ } else if (statusFlags & EnigmailConstants.NO_PUBKEY) {
+ this.msgSignatureState =
+ EnigmailConstants.MSG_SIG_UNCERTAIN_KEY_UNAVAILABLE;
+ Enigmail.msg.notifySigKeyMissing(secInfo.keyId);
+ } else {
+ this.msgSignatureState =
+ EnigmailConstants.MSG_SIG_UNCERTAIN_KEY_NOT_ACCEPTED;
+ }
+ }
+ // (statusFlags & EnigmailConstants.INLINE_KEY) ???
+
+ if (encrypted) {
+ this.msgEncryptedStateString = encrypted;
+ }
+ if (signed) {
+ this.msgSignedStateString = signed;
+ }
+ this.updateVisibleSecurityStatus(triggeredByMimePartNumber);
+
+ /*
+ // special handling after trying to fix buggy mail format (see buggyExchangeEmailContent in code)
+ if (secInfo.xtraStatus && secInfo.xtraStatus == "buggyMailFormat") {
+ }
+ */
+
+ if (encrypted) {
+ // For telemetry purposes.
+ window.dispatchEvent(
+ new CustomEvent("secureMsgLoaded", {
+ detail: {
+ key: "encrypted-openpgp",
+ data: encrypted,
+ },
+ })
+ );
+ }
+ if (signed) {
+ window.dispatchEvent(
+ new CustomEvent("secureMsgLoaded", {
+ detail: {
+ key: "signed-openpgp",
+ data: signed,
+ },
+ })
+ );
+ }
+ },
+
+ /**
+ * Should be called as soon as it is known that the message has
+ * an OpenPGP key attached.
+ */
+ notifyHasKeyAttached() {
+ this.msgHasKeyAttached = true;
+ this.updateVisibleSecurityStatus();
+ },
+
+ /**
+ * Should be called whenever more information about the OpenPGP
+ * message state became available, such as encryption or signature
+ * status, or the availability of an attached key.
+ *
+ * @param {string} triggeredByMimePartNumber - optional number of the
+ * MIME part that was processed and has triggered this status update
+ * request.
+ */
+ updateVisibleSecurityStatus(triggeredByMimePartNumber = undefined) {
+ setMessageCryptoBox(
+ "OpenPGP",
+ this.msgEncryptedStateString,
+ this.msgSignedStateString,
+ this.msgHasKeyAttached,
+ triggeredByMimePartNumber
+ );
+ },
+
+ editKeyExpiry() {
+ EnigmailWindows.editKeyExpiry(
+ window,
+ [Enigmail.msg.securityInfo.userId],
+ [Enigmail.msg.securityInfo.keyId]
+ );
+ ReloadMessage();
+ },
+
+ editKeyTrust() {
+ let key = EnigmailKeyRing.getKeyById(Enigmail.msg.securityInfo.keyId);
+
+ EnigmailWindows.editKeyTrust(
+ window,
+ [Enigmail.msg.securityInfo.userId],
+ [key.keyId]
+ );
+ ReloadMessage();
+ },
+
+ signKey() {
+ let key = EnigmailKeyRing.getKeyById(Enigmail.msg.securityInfo.keyId);
+
+ EnigmailWindows.signKey(
+ window,
+ Enigmail.msg.securityInfo.userId,
+ key.keyId,
+ null
+ );
+ ReloadMessage();
+ },
+
+ msgHdrViewLoad() {
+ EnigmailLog.DEBUG("enigmailMsgHdrViewOverlay.js: this.msgHdrViewLoad\n");
+
+ this.messageListener = {
+ onStartHeaders() {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: _listener_onStartHeaders\n"
+ );
+
+ try {
+ Enigmail.hdrView.statusBarHide();
+ EnigmailVerify.setWindow(window, Enigmail.msg.getCurrentMsgUriSpec());
+
+ let msgFrame = document.getElementById("messagepane").contentDocument;
+
+ if (msgFrame) {
+ msgFrame.addEventListener(
+ "unload",
+ Enigmail.hdrView.messageUnload.bind(Enigmail.hdrView),
+ true
+ );
+ msgFrame.addEventListener(
+ "load",
+ Enigmail.hdrView.messageLoad.bind(Enigmail.hdrView),
+ true
+ );
+ }
+
+ Enigmail.hdrView.forgetEncryptedMsgKey();
+ Enigmail.hdrView.setWindowCallback();
+ } catch (ex) {
+ console.debug(ex);
+ }
+ },
+
+ onEndHeaders() {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: _listener_onEndHeaders\n"
+ );
+ },
+
+ onEndAttachments() {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: _listener_onEndAttachments\n"
+ );
+
+ try {
+ EnigmailVerify.setWindow(null, null);
+ } catch (ex) {}
+
+ Enigmail.hdrView.messageLoad();
+ },
+
+ beforeStartHeaders() {
+ return true;
+ },
+ };
+
+ gMessageListeners.push(this.messageListener);
+
+ // fire the handlers since some windows open directly with a visible message
+ this.messageListener.onStartHeaders();
+ this.messageListener.onEndAttachments();
+ },
+
+ messageUnload(event) {
+ EnigmailLog.DEBUG("enigmailMsgHdrViewOverlay.js: this.messageUnload\n");
+ if (Enigmail.hdrView.flexbuttonAction === null) {
+ if (Enigmail.msg.securityInfo && Enigmail.msg.securityInfo.xtraStatus) {
+ Enigmail.msg.securityInfo.xtraStatus = "";
+ }
+ this.forgetEncryptedMsgKey();
+ }
+ },
+
+ async messageLoad(event) {
+ EnigmailLog.DEBUG("enigmailMsgHdrViewOverlay.js: this.messageLoad\n");
+
+ await Enigmail.msg.messageAutoDecrypt();
+ Enigmail.msg.handleAttachmentEvent();
+ },
+
+ dispKeyDetails() {
+ if (!Enigmail.msg.securityInfo) {
+ return;
+ }
+
+ let key = EnigmailKeyRing.getKeyById(Enigmail.msg.securityInfo.keyId);
+
+ EnigmailWindows.openKeyDetails(window, key.keyId, false);
+ },
+
+ forgetEncryptedMsgKey() {
+ if (Enigmail.hdrView.lastEncryptedMsgKey) {
+ EnigmailURIs.forgetEncryptedUri(Enigmail.hdrView.lastEncryptedMsgKey);
+ Enigmail.hdrView.lastEncryptedMsgKey = null;
+ }
+
+ if (Enigmail.hdrView.lastEncryptedUri && gEncryptedURIService) {
+ gEncryptedURIService.forgetEncrypted(Enigmail.hdrView.lastEncryptedUri);
+ Enigmail.hdrView.lastEncryptedUri = null;
+ }
+ },
+
+ onShowAttachmentContextMenu(event) {
+ let contextMenu = document.getElementById("attachmentItemContext");
+ let separator = document.getElementById("openpgpCtxItemsSeparator");
+ let decryptOpenMenu = document.getElementById("enigmail_ctxDecryptOpen");
+ let decryptSaveMenu = document.getElementById("enigmail_ctxDecryptSave");
+ let importMenu = document.getElementById("enigmail_ctxImportKey");
+ let verifyMenu = document.getElementById("enigmail_ctxVerifyAtt");
+
+ if (contextMenu.attachments.length == 1) {
+ let attachment = contextMenu.attachments[0];
+
+ if (/^application\/pgp-keys/i.test(attachment.contentType)) {
+ importMenu.hidden = false;
+ decryptOpenMenu.hidden = true;
+ decryptSaveMenu.hidden = true;
+ verifyMenu.hidden = true;
+ } else if (Enigmail.msg.checkEncryptedAttach(attachment)) {
+ if (
+ (typeof attachment.name !== "undefined" &&
+ attachment.name.match(/\.asc\.(gpg|pgp)$/i)) ||
+ (typeof attachment.displayName !== "undefined" &&
+ attachment.displayName.match(/\.asc\.(gpg|pgp)$/i))
+ ) {
+ importMenu.hidden = false;
+ } else {
+ importMenu.hidden = true;
+ }
+ decryptOpenMenu.hidden = false;
+ decryptSaveMenu.hidden = false;
+ if (
+ EnigmailMsgRead.checkSignedAttachment(
+ attachment,
+ null,
+ currentAttachments
+ )
+ ) {
+ verifyMenu.hidden = false;
+ } else {
+ verifyMenu.hidden = true;
+ }
+ if (typeof attachment.displayName == "undefined") {
+ if (!attachment.name) {
+ attachment.name = "message.pgp";
+ }
+ } else if (!attachment.displayName) {
+ attachment.displayName = "message.pgp";
+ }
+ } else if (
+ EnigmailMsgRead.checkSignedAttachment(
+ attachment,
+ null,
+ currentAttachments
+ )
+ ) {
+ importMenu.hidden = true;
+ decryptOpenMenu.hidden = true;
+ decryptSaveMenu.hidden = true;
+
+ verifyMenu.hidden = false;
+ } else {
+ importMenu.hidden = true;
+ decryptOpenMenu.hidden = true;
+ decryptSaveMenu.hidden = true;
+ verifyMenu.hidden = true;
+ }
+
+ separator.hidden =
+ decryptOpenMenu.hidden &&
+ decryptSaveMenu.hidden &&
+ importMenu.hidden &&
+ verifyMenu.hidden;
+ } else {
+ decryptOpenMenu.hidden = true;
+ decryptSaveMenu.hidden = true;
+ importMenu.hidden = true;
+ verifyMenu.hidden = true;
+ separator.hidden = true;
+ }
+ },
+
+ updateMsgDb() {
+ EnigmailLog.DEBUG("enigmailMsgHdrViewOverlay.js: this.updateMsgDb\n");
+ var msg = gMessage;
+ if (!msg || !msg.folder) {
+ return;
+ }
+
+ var msgHdr = msg.folder.GetMessageHeader(msg.messageKey);
+
+ if (this.msgEncryptionState === EnigmailConstants.MSG_ENC_OK) {
+ Enigmail.msg.securityInfo.statusFlags |=
+ EnigmailConstants.DECRYPTION_OKAY;
+ }
+ msgHdr.setUint32Property("enigmail", Enigmail.msg.securityInfo.statusFlags);
+ },
+
+ enigCanDetachAttachments() {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: this.enigCanDetachAttachments\n"
+ );
+
+ var canDetach = true;
+ if (
+ Enigmail.msg.securityInfo &&
+ typeof Enigmail.msg.securityInfo.statusFlags != "undefined"
+ ) {
+ canDetach = !(
+ Enigmail.msg.securityInfo.statusFlags &
+ (EnigmailConstants.PGP_MIME_SIGNED |
+ EnigmailConstants.PGP_MIME_ENCRYPTED)
+ );
+ }
+ return canDetach;
+ },
+
+ setSubject(subject) {
+ // Strip multiple localized Re: prefixes. This emulates NS_MsgStripRE().
+ let prefixes = Services.prefs
+ .getComplexValue("mailnews.localizedRe", Ci.nsIPrefLocalizedString)
+ .data.split(",")
+ .filter(Boolean);
+ if (!prefixes.includes("Re")) {
+ prefixes.push("Re");
+ }
+ // Construct a regular expression like this: ^(Re: |Aw: )+
+ let newSubject = subject.replace(
+ new RegExp(`^(${prefixes.join(": |")}: )+`, "i"),
+ ""
+ );
+ let hadRe = newSubject != subject;
+
+ // Update the message.
+ gMessage.subject = newSubject;
+ let oldFlags = gMessage.flags;
+ if (hadRe) {
+ gMessage.flags |= Ci.nsMsgMessageFlags.HasRe;
+ newSubject = "Re: " + newSubject;
+ }
+ document.title = newSubject;
+ currentHeaderData.subject.headerValue = newSubject;
+ document.getElementById("expandedsubjectBox").headerValue = newSubject;
+ // This even works if the flags haven't changed. Causes repaint in all thread trees.
+ gMessage.folder?.msgDatabase.notifyHdrChangeAll(
+ gMessage,
+ oldFlags,
+ gMessage.flags,
+ {}
+ );
+ },
+
+ updateHdrBox(header, value) {
+ let e = document.getElementById("expanded" + header + "Box");
+ if (e) {
+ e.headerValue = value;
+ }
+ },
+
+ setWindowCallback() {
+ EnigmailLog.DEBUG("enigmailMsgHdrViewOverlay.js: setWindowCallback\n");
+
+ EnigmailSingletons.messageReader = this.headerPane;
+ },
+
+ clearWindowCallback() {
+ if (EnigmailSingletons.messageReader == this.headerPane) {
+ EnigmailSingletons.messageReader = null;
+ }
+ },
+
+ headerPane: {
+ isCurrentMessage(uri) {
+ let uriSpec = uri ? uri.spec : null;
+
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: EnigMimeHeaderSink.isCurrentMessage: uri.spec=" +
+ uriSpec +
+ "\n"
+ );
+
+ return true;
+ },
+
+ /**
+ * Determine if a given MIME part number is a multipart/related message or a child thereof
+ *
+ * @param mimePart: Object - The MIME Part object to evaluate from the MIME tree
+ * @param searchPartNum: String - The part number to determine
+ */
+ isMultipartRelated(mimePart, searchPartNum) {
+ if (
+ searchPartNum.indexOf(mimePart.partNum) == 0 &&
+ mimePart.partNum.length <= searchPartNum.length
+ ) {
+ if (mimePart.fullContentType.search(/^multipart\/related/i) === 0) {
+ return true;
+ }
+
+ for (let i in mimePart.subParts) {
+ if (this.isMultipartRelated(mimePart.subParts[i], searchPartNum)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Determine if a given mime part number should be displayed.
+ * Returns true if one of these conditions is true:
+ * - this is the 1st displayed block of the message
+ * - the message part displayed corresponds to the decrypted part
+ *
+ * @param mimePartNumber: String - the MIME part number that was decrypted/verified
+ * @param uriSpec: String - the URI spec that is being displayed
+ */
+ displaySubPart(mimePartNumber, uriSpec) {
+ if (!mimePartNumber || !uriSpec) {
+ return true;
+ }
+ let part = EnigmailMime.getMimePartNumber(uriSpec);
+
+ if (part.length === 0) {
+ // only display header if 1st message part
+ if (mimePartNumber.search(/^1(\.1)*$/) < 0) {
+ return false;
+ }
+ } else {
+ let r = EnigmailFuncs.compareMimePartLevel(mimePartNumber, part);
+
+ // analyzed mime part is contained in viewed message part
+ if (r === 2) {
+ if (mimePartNumber.substr(part.length).search(/^\.1(\.1)*$/) < 0) {
+ return false;
+ }
+ } else if (r !== 0) {
+ return false;
+ }
+
+ if (Enigmail.msg.mimeParts) {
+ if (this.isMultipartRelated(Enigmail.msg.mimeParts, mimePartNumber)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Determine if there are message parts that are not encrypted
+ *
+ * @param mimePartNumber String - the MIME part number that was authenticated
+ *
+ * @returns Boolean: true: there are siblings / false: no siblings
+ */
+ hasUnauthenticatedParts(mimePartNumber) {
+ function hasUnauthenticatedSiblings(
+ mimeSubTree,
+ mimePartToCheck,
+ parentOfMimePartToCheck
+ ) {
+ if (mimeSubTree.partNum === parentOfMimePartToCheck) {
+ // If this is an encrypted message that is the parent of mimePartToCheck,
+ // then we know that all its childs (including mimePartToCheck) are authenticated.
+ if (
+ mimeSubTree.fullContentType.search(
+ /^multipart\/encrypted.{1,255}protocol="?application\/pgp-encrypted"?/i
+ ) === 0
+ ) {
+ return false;
+ }
+ }
+ if (
+ mimeSubTree.partNum.indexOf(parentOfMimePartToCheck) == 0 &&
+ mimeSubTree.partNum !== mimePartToCheck
+ ) {
+ // This is a sibling (same parent, different part number).
+ return true;
+ }
+
+ for (let i in mimeSubTree.subParts) {
+ if (
+ hasUnauthenticatedSiblings(
+ mimeSubTree.subParts[i],
+ mimePartToCheck,
+ parentOfMimePartToCheck
+ )
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ if (!mimePartNumber || !Enigmail.msg.mimeParts) {
+ return false;
+ }
+
+ let parentNum = "";
+ if (mimePartNumber.includes(".")) {
+ parentNum = mimePartNumber.replace(/\.\d+$/, "");
+ }
+
+ return hasUnauthenticatedSiblings(
+ Enigmail.msg.mimeParts,
+ mimePartNumber,
+ parentNum
+ );
+ },
+
+ /**
+ * Request that OpenPGP security status from the given MIME part
+ * shall be ignored (not shown in the UI). If status for that
+ * MIME part was already received, then reset the status.
+ *
+ * @param {string} originMimePartNumber - Ignore security status
+ * of this MIME part.
+ */
+ ignoreStatusFrom(originMimePartNumber) {
+ Enigmail.hdrView.ignoreStatusFromMimePart = originMimePartNumber;
+ setIgnoreStatusFromMimePart(originMimePartNumber);
+ if (Enigmail.hdrView.receivedStatusFromParts.has(originMimePartNumber)) {
+ Enigmail.hdrView.reset();
+ Enigmail.hdrView.ignoreStatusFromMimePart = originMimePartNumber;
+ }
+ },
+
+ async updateSecurityStatus(
+ unusedUriSpec,
+ exitCode,
+ statusFlags,
+ extStatusFlags,
+ keyId,
+ userId,
+ sigDetails,
+ errorMsg,
+ blockSeparation,
+ uri,
+ extraDetails,
+ mimePartNumber
+ ) {
+ if (
+ Enigmail.hdrView.ignoreStatusFromMimePart != "" &&
+ mimePartNumber == Enigmail.hdrView.ignoreStatusFromMimePart
+ ) {
+ return;
+ }
+
+ Enigmail.hdrView.receivedStatusFromParts.add(mimePartNumber);
+
+ // uriSpec is not used for Enigmail anymore. It is here because other addons and pEp rely on it
+
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: updateSecurityStatus: mimePart=" +
+ mimePartNumber +
+ "\n"
+ );
+
+ let uriSpec = uri ? uri.spec : null;
+
+ if (this.isCurrentMessage(uri)) {
+ if (statusFlags & EnigmailConstants.DECRYPTION_OKAY) {
+ if (gEncryptedURIService) {
+ // remember encrypted message URI to enable TB prevention against EFAIL attack
+ Enigmail.hdrView.lastEncryptedUri = gMessageURI;
+ gEncryptedURIService.rememberEncrypted(
+ Enigmail.hdrView.lastEncryptedUri
+ );
+ }
+ }
+
+ if (!this.displaySubPart(mimePartNumber, uriSpec)) {
+ return;
+ }
+ if (this.hasUnauthenticatedParts(mimePartNumber)) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: updateSecurityStatus: found unauthenticated part\n"
+ );
+ statusFlags |= EnigmailConstants.PARTIALLY_PGP;
+ }
+
+ let encToDetails = null;
+
+ if (extraDetails && extraDetails.length > 0) {
+ try {
+ let o = JSON.parse(extraDetails);
+ if ("encryptedTo" in o) {
+ encToDetails = o.encryptedTo;
+ }
+ } catch (x) {
+ console.debug(x);
+ }
+ }
+
+ Enigmail.hdrView.updatePgpStatus(
+ exitCode,
+ statusFlags,
+ extStatusFlags,
+ keyId,
+ userId,
+ sigDetails,
+ errorMsg,
+ blockSeparation,
+ encToDetails,
+ null,
+ mimePartNumber
+ );
+ }
+ },
+
+ processDecryptionResult(uri, actionType, processData, mimePartNumber) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: EnigMimeHeaderSink.processDecryptionResult:\n"
+ );
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: actionType= " +
+ actionType +
+ ", mimePart=" +
+ mimePartNumber +
+ "\n"
+ );
+
+ let msg = gMessage;
+ if (!msg) {
+ return;
+ }
+ if (!this.isCurrentMessage(uri)) {
+ return;
+ }
+
+ switch (actionType) {
+ case "modifyMessageHeaders":
+ this.modifyMessageHeaders(uri, processData, mimePartNumber);
+ break;
+ /*
+ case "wksConfirmRequest":
+ Enigmail.hdrView.checkWksConfirmRequest(processData);
+ */
+ }
+ },
+
+ modifyMessageHeaders(uri, headerData, mimePartNumber) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: EnigMimeHeaderSink.modifyMessageHeaders:\n"
+ );
+
+ let uriSpec = uri ? uri.spec : null;
+ let hdr;
+
+ try {
+ hdr = JSON.parse(headerData);
+ } catch (ex) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: modifyMessageHeaders: - no headers to display\n"
+ );
+ return;
+ }
+
+ if (typeof hdr !== "object") {
+ return;
+ }
+ if (!this.displaySubPart(mimePartNumber, uriSpec)) {
+ return;
+ }
+
+ let msg = gMessage;
+
+ if ("subject" in hdr) {
+ Enigmail.hdrView.setSubject(hdr.subject);
+ }
+
+ if ("date" in hdr) {
+ msg.date = Date.parse(hdr.date) * 1000;
+ }
+ /*
+ if ("newsgroups" in hdr) {
+ updateHdrBox("newsgroups", hdr.newsgroups);
+ }
+
+ if ("followup-to" in hdr) {
+ updateHdrBox("followup-to", hdr["followup-to"]);
+ }
+
+ if ("from" in hdr) {
+ gExpandedHeaderView.from.outputFunction(gExpandedHeaderView.from, hdr.from);
+ msg.setStringProperty("Enigmail-From", hdr.from);
+ }
+
+ if ("to" in hdr) {
+ gExpandedHeaderView.to.outputFunction(gExpandedHeaderView.to, hdr.to);
+ msg.setStringProperty("Enigmail-To", hdr.to);
+ }
+
+ if ("cc" in hdr) {
+ gExpandedHeaderView.cc.outputFunction(gExpandedHeaderView.cc, hdr.cc);
+ msg.setStringProperty("Enigmail-Cc", hdr.cc);
+ }
+
+ if ("reply-to" in hdr) {
+ gExpandedHeaderView["reply-to"].outputFunction(gExpandedHeaderView["reply-to"], hdr["reply-to"]);
+ msg.setStringProperty("Enigmail-ReplyTo", hdr["reply-to"]);
+ }
+ */
+ },
+
+ handleSMimeMessage(uri) {
+ if (
+ Enigmail.hdrView.msgSignedStateString != null ||
+ Enigmail.hdrView.msgEncryptedStateString != null
+ ) {
+ // If we already processed an OpenPGP part, then we are handling
+ // a message with an inner S/MIME part. We must not reload
+ // the message here, because we'd run into an endless loop.
+ return;
+ }
+ if (this.isCurrentMessage(uri)) {
+ EnigmailVerify.unregisterPGPMimeHandler();
+ Enigmail.msg.messageReload(false);
+ }
+ },
+ },
+
+ /*
+ onUnloadEnigmail() {
+ window.removeEventListener("load-enigmail", Enigmail.hdrView.hdrViewLoad);
+ for (let i = 0; i < gMessageListeners.length; i++) {
+ if (gMessageListeners[i] === Enigmail.hdrView.messageListener) {
+ gMessageListeners.splice(i, 1);
+ break;
+ }
+ }
+
+ let signedHdrElement = document.getElementById("signedHdrIcon");
+ if (signedHdrElement) {
+ signedHdrElement.setAttribute(
+ "onclick",
+ "showMessageReadSecurityInfo();"
+ );
+ }
+
+ let encryptedHdrElement = document.getElementById("encryptedHdrIcon");
+ if (encryptedHdrElement) {
+ encryptedHdrElement.setAttribute(
+ "onclick",
+ "showMessageReadSecurityInfo();"
+ );
+ }
+
+ let addrPopup = document.getElementById("emailAddressPopup");
+ if (addrPopup) {
+ addrPopup.removeEventListener(
+ "popupshowing",
+ Enigmail.hdrView.displayAddressPopup
+ );
+ }
+
+ let attCtx = document.getElementById("attachmentItemContext");
+ if (attCtx) {
+ attCtx.removeEventListener(
+ "popupshowing",
+ this.onShowAttachmentContextMenu
+ );
+ }
+
+ let msgFrame = EnigmailWindows.getFrame(window, "messagepane");
+ if (msgFrame) {
+ msgFrame.removeEventListener(
+ "unload",
+ Enigmail.hdrView.messageUnload,
+ true
+ );
+ msgFrame.removeEventListener("load", Enigmail.hdrView.messageLoad);
+ }
+ },
+ */
+};
+
+window.addEventListener(
+ "load-enigmail",
+ Enigmail.hdrView.hdrViewLoad.bind(Enigmail.hdrView)
+);
+window.addEventListener("unload", () => Enigmail.hdrView.clearWindowCallback());
diff --git a/comm/mail/extensions/openpgp/content/ui/keyAssistant.inc.xhtml b/comm/mail/extensions/openpgp/content/ui/keyAssistant.inc.xhtml
new file mode 100644
index 0000000000..e3a56edf77
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/keyAssistant.inc.xhtml
@@ -0,0 +1,119 @@
+# 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/.
+
+<html:dialog id="keyAssistant" xmlns="http://www.w3.org/1999/xhtml"
+ class="modal-dialog">
+
+ <h1 class="dialog-title" data-l10n-id="openpgp-key-assistant-title"></h1>
+
+ <div id="discoverView" class="modal-dialog-body dialog-body-view">
+ <div>
+ <p data-l10n-id="openpgp-key-assistant-discover-title"></p>
+ <div id="discoveryOutput"></div>
+ </div>
+ <menu class="dialog-menu-container menu-in-body">
+ <button data-l10n-id="openpgp-key-assistant-cancel-button"
+ onclick="gKeyAssistant.resetViews();"></button>
+ </menu>
+ </div><!-- #discoverView -->
+
+ <div id="resolveView" class="modal-dialog-body dialog-body-view">
+ <div>
+ <p id="resolveViewTitle"></p>
+
+ <!-- Usable section. -->
+ <section id="resolveViewValid" hidden="hidden">
+ <p id="resolveViewValidDescription" class="font-bold"
+ data-l10n-id="openpgp-key-assistant-valid-description"></p>
+ <ul id="resolveValidKeysList" class="reset-list radio-list"></ul>
+ <p id="openpgp-key-assistant-help-accept"
+ class="tip-caption margin-top-1em"
+ data-l10n-id="openpgp-key-assistant-rogue-warning">
+ <a onclick="openContentTab(this.href);"
+ href="https://support.mozilla.org/kb/thunderbird-help-openpgp-counterfeit-key"
+ data-l10n-name="openpgp-link"></a>
+ </p>
+ </section>
+
+ <!-- Unusable section. -->
+ <section id="resolveViewInvalid" hidden="hidden">
+ <p id="resolveViewExpiredDescription" class="font-bold margin-top-1em"></p>
+ <ul id="resolveInvalidKeysList" class="reset-list radio-list"></ul>
+ </section>
+
+ <button data-l10n-id="openpgp-key-assistant-discover-online-button"
+ onclick="gKeyAssistant.changeView('discover', 'resolving');"></button>
+ <button data-l10n-id="openpgp-key-assistant-import-keys-button"
+ onclick="gKeyAssistant.importFromFile('resolving');"></button>
+ </div>
+
+ <menu class="dialog-menu-container two-columns menu-in-body">
+ <button data-l10n-id="openpgp-key-assistant-back-button"
+ onclick="gKeyAssistant.resetViews();"></button>
+ <button id="resolveViewAcceptKey"
+ data-l10n-id="openpgp-key-assistant-accept-button"
+ class="primary"
+ disabled="disabled"></button>
+ </menu>
+ </div><!-- #resolveView -->
+
+ <div id="mainView" class="modal-dialog-body dialog-body-view">
+ <div id="modalDialogNotification" class="modal-dialog-notifications">
+ <!-- Notifications will be lazily loaded here. -->
+ </div>
+
+ <!-- Issues section. -->
+ <section id="keyAssistantIssues" hidden="hidden">
+ <p id="keyAssistantIssuesHeader" class="font-bold"
+ data-l10n-id="openpgp-key-assistant-recipients-issue-header"/>
+ <p id="keyAssistantIssuesDescription">
+ <a onclick="openContentTab(this.href);"
+ href="https://support.mozilla.org/kb/thunderbird-help-cannot-encrypt"
+ data-l10n-name="openpgp-link"></a>
+ </p>
+
+ <ul id="keysListIssues" class="reset-list key-list"></ul>
+
+ <button data-l10n-id="openpgp-key-assistant-discover-online-button"
+ onclick="gKeyAssistant.changeView('discover', 'overview');"></button>
+ <button data-l10n-id="openpgp-key-assistant-import-keys-button"
+ onclick="gKeyAssistant.importFromFile('overview');"></button>
+
+ <p id="openpgp-key-assistant-help-alias"
+ class="tip-caption margin-top-1em"
+ data-l10n-id="openpgp-key-assistant-info-alias">
+ <a onclick="openContentTab(this.href);"
+ href="https://support.mozilla.org/kb/thunderbird-help-openpgp-alias"
+ data-l10n-name="openpgp-link"></a>
+ </p>
+ </section>
+
+ <!-- No issues section. -->
+ <section id="keyAssistantValid" class="margin-top-1em" hidden="hidden">
+ <div class="container-with-link">
+ <p id="keyAssistantValidDescription"></p>
+ <button id="toggleRecipientsButton"
+ class="button-link"
+ data-l10n-id="openpgp-key-assistant-recipients-show-button"></button>
+ </div>
+
+ <ul id="keysListValid" class="reset-list key-list" hidden="hidden"></ul>
+ </section>
+ </div><!-- #mainView -->
+
+ <menu id="mainButtons" class="dialog-menu-container two-columns">
+ <div>
+ <button data-l10n-id="openpgp-key-assistant-close-button"
+ onclick="gKeyAssistant.close();"></button>
+ </div>
+ <div>
+ <button id="disableEncryptionButton"
+ data-l10n-id="openpgp-key-assistant-disable-button"></button>
+ <button id="sendEncryptedButton"
+ data-l10n-id="openpgp-key-assistant-confirm-button"
+ class="primary"
+ disabled="disabled"></button>
+ </div>
+ </menu>
+</html:dialog>
diff --git a/comm/mail/extensions/openpgp/content/ui/keyAssistant.js b/comm/mail/extensions/openpgp/content/ui/keyAssistant.js
new file mode 100644
index 0000000000..ca6104c2cb
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/keyAssistant.js
@@ -0,0 +1,956 @@
+/*
+ * 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/.
+ */
+
+/* global MozElements */
+/* import-globals-from ../../../../components/compose/content/MsgComposeCommands.js */
+/* import-globals-from commonWorkflows.js */
+/* globals goDoCommand */ // From globalOverlay.js
+
+"use strict";
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
+ KeyLookupHelper: "chrome://openpgp/content/modules/keyLookupHelper.jsm",
+ OpenPGPAlias: "chrome://openpgp/content/modules/OpenPGPAlias.jsm",
+ PgpSqliteDb2: "chrome://openpgp/content/modules/sqliteDb.jsm",
+ // FIXME: using this creates a conflict with another file where this symbol
+ // was imported with ChromeUtils instead of defined as lazy getter.
+ // EnigmailWindows: "chrome://openpgp/content/modules/windows.jsm",
+});
+var { EnigmailWindows } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/windows.jsm"
+);
+
+window.addEventListener("load", () => {
+ gKeyAssistant.onLoad();
+});
+window.addEventListener("unload", () => {
+ gKeyAssistant.onUnload();
+});
+
+var gKeyAssistant = {
+ dialog: null,
+ recipients: [],
+ currentRecip: null,
+
+ /*
+ * Variable ignoreExternal should be set to true whenever a
+ * keyAsssistant window is open that cannot tolerate changes to
+ * the keyAsssistant's own variables, that track the current user
+ * interaction.
+ *
+ * While the key assistant is showing, it takes care to update the
+ * elements on screen, based on the expected changes. Usually,
+ * it will perform a refresh after a current action is completed.
+ *
+ * Without this protection, you'd get data races and side effects like
+ * email addresses being shown twice, and worse.
+ */
+ ignoreExternal: false,
+
+ /**
+ * Initialize the main notification box for the account setup process.
+ */
+ get notificationBox() {
+ if (!this._notificationBox) {
+ this._notificationBox = new MozElements.NotificationBox(element => {
+ element.setAttribute("notificationside", "bottom");
+ document.getElementById("modalDialogNotification").append(element);
+ });
+ }
+ return this._notificationBox;
+ },
+
+ onLoad() {
+ this.dialog = document.getElementById("keyAssistant");
+
+ this._setupEventListeners();
+ },
+
+ _setupEventListeners() {
+ document
+ .getElementById("disableEncryptionButton")
+ .addEventListener("click", () => {
+ gSendEncrypted = false;
+ gUserTouchedSendEncrypted = true;
+ checkEncryptionState();
+ this.close();
+ });
+ document
+ .getElementById("sendEncryptedButton")
+ .addEventListener("click", () => {
+ goDoCommand("cmd_sendWithCheck");
+ this.close();
+ });
+ document
+ .getElementById("toggleRecipientsButton")
+ .addEventListener("click", () => {
+ this.toggleRecipientsList();
+ });
+
+ this.dialog.addEventListener("close", () => {
+ this.close();
+ });
+ },
+
+ async close() {
+ await checkEncryptionState();
+ this.dialog.close();
+ },
+
+ onUnload() {
+ this.recipients = [];
+ },
+
+ setMainDisableButton() {
+ document.getElementById("disableEncryptionButton").hidden =
+ !gSendEncrypted || (this.usableKeys && !this.problematicKeys);
+ },
+
+ /**
+ * Open the key assistant modal dialog.
+ *
+ * @param {string[]} recipients - An array of strings containing all currently
+ * written recipients.
+ * @param {boolean} isSending - If the key assistant was triggered during a
+ * sending attempt.
+ */
+ show(recipients, isSending) {
+ this.recipients = recipients;
+ this.buildMainView();
+ this.resetViews();
+
+ document.getElementById("sendEncryptedButton").hidden = !isSending;
+ this.setMainDisableButton();
+ this.dialog.showModal();
+ },
+
+ resetViews() {
+ this.notificationBox.removeAllNotifications();
+ this.dialog.removeAttribute("style");
+
+ for (let view of document.querySelectorAll(".dialog-body-view")) {
+ view.hidden = true;
+ }
+
+ document.getElementById("mainButtons").hidden = false;
+ document.getElementById("mainView").hidden = false;
+ },
+
+ changeView(view, context) {
+ this.resetViews();
+
+ this.dialog.setAttribute(
+ "style",
+ `min-height: ${this.dialog.getBoundingClientRect().height}px`
+ );
+
+ document.getElementById("mainView").hidden = true;
+ document.getElementById(`${view}View`).hidden = false;
+
+ switch (view) {
+ case "discover":
+ this.hideMainButtons();
+ this.initOnlineDiscovery(context);
+ break;
+
+ case "resolve":
+ this.hideMainButtons();
+ break;
+
+ default:
+ break;
+ }
+ },
+
+ hideMainButtons() {
+ document.getElementById("mainButtons").hidden = true;
+ },
+
+ usableKeys: 0,
+ problematicKeys: 0,
+
+ /**
+ * Populate the main view of the key assistant with the list of recipients and
+ * its keys, separating the recipients that have issues from those without
+ * issues.
+ */
+ async buildMainView() {
+ // Restore empty UI state.
+ document.getElementById("keyAssistantIssues").hidden = true;
+ document.getElementById("keysListIssues").replaceChildren();
+ document.getElementById("keyAssistantValid").hidden = true;
+ document.getElementById("keysListValid").replaceChildren();
+
+ this.usableKeys = 0;
+ this.problematicKeys = 0;
+
+ for (let addr of this.recipients) {
+ // Fetch all keys for the current recipient.
+ let keyMetas = await EnigmailKeyRing.getEncryptionKeyMeta(addr);
+ if (keyMetas.some(k => k.readiness == "alias")) {
+ let aliasKeyList = EnigmailKeyRing.getAliasKeyList(addr);
+ let aliasKeys = EnigmailKeyRing.getAliasKeys(aliasKeyList);
+ if (!aliasKeys.length) {
+ // failure, at least one alias key is unusable/unavailable
+
+ let descriptionDiv = document.createElement("div");
+ document.l10n.setAttributes(
+ descriptionDiv,
+ "openpgp-compose-alias-status-error"
+ );
+
+ this.addToProblematicList(addr, descriptionDiv, null);
+ this.problematicKeys++;
+ } else {
+ let aliasText = document.createElement("div");
+ document.l10n.setAttributes(
+ aliasText,
+ "openpgp-compose-alias-status-direct",
+ { count: aliasKeys.length }
+ );
+
+ this.addToReadyList(addr, aliasText);
+ this.usableKeys++;
+ }
+ } else {
+ // not alias
+
+ let acceptedKeys = keyMetas.filter(k => k.readiness == "accepted");
+ if (acceptedKeys.length) {
+ let button = document.createElement("button");
+ document.l10n.setAttributes(
+ button,
+ "openpgp-key-assistant-view-key-button"
+ );
+ button.addEventListener("click", () => {
+ gKeyAssistant.viewKeyFromOverview(addr, acceptedKeys[0]);
+ });
+
+ this.addToReadyList(addr, button);
+ this.usableKeys++;
+ } else {
+ let descriptionDiv = document.createElement("div");
+
+ let canOfferResolving = keyMetas.some(
+ k =>
+ k.readiness == "collected" ||
+ k.readiness == "expiredAccepted" ||
+ k.readiness == "expiredUndecided" ||
+ k.readiness == "expiredOtherAccepted" ||
+ k.readiness == "undecided" ||
+ k.readiness == "otherAccepted" ||
+ k.readiness == "expiredRejected" ||
+ k.readiness == "rejected"
+ );
+
+ let button = null;
+ if (canOfferResolving) {
+ this.fillKeysStatus(descriptionDiv, keyMetas);
+
+ button = document.createElement("button");
+ document.l10n.setAttributes(
+ button,
+ "openpgp-key-assistant-issue-resolve-button"
+ );
+ button.addEventListener("click", () => {
+ this.buildResolveView(addr, keyMetas);
+ });
+ } else {
+ document.l10n.setAttributes(
+ descriptionDiv,
+ "openpgp-key-assistant-no-key-available"
+ );
+ }
+
+ this.addToProblematicList(addr, descriptionDiv, button);
+ this.problematicKeys++;
+ }
+ }
+ }
+
+ document.getElementById("keyAssistantIssues").hidden =
+ !this.problematicKeys;
+ document.l10n.setAttributes(
+ document.getElementById("keyAssistantIssuesDescription"),
+ "openpgp-key-assistant-recipients-issue-description",
+ { count: this.problematicKeys }
+ );
+
+ document.getElementById("keyAssistantValid").hidden = !this.usableKeys;
+
+ if (!this.problematicKeys && this.usableKeys) {
+ document.l10n.setAttributes(
+ document.getElementById("keyAssistantValidDescription"),
+ "openpgp-key-assistant-recipients-description-no-issues"
+ );
+ document.getElementById("toggleRecipientsButton").click();
+ } else {
+ document.l10n.setAttributes(
+ document.getElementById("keyAssistantValidDescription"),
+ "openpgp-key-assistant-recipients-description",
+ { count: this.usableKeys }
+ );
+ }
+
+ document.getElementById("sendEncryptedButton").disabled =
+ this.problematicKeys || !this.usableKeys;
+ this.setMainDisableButton();
+ },
+
+ isAccepted(acc) {
+ return (
+ acc.emailDecided &&
+ (acc.fingerprintAcceptance == "verified" ||
+ acc.fingerprintAcceptance == "unverified")
+ );
+ },
+
+ async viewKeyFromResolve(keyMeta) {
+ let oldAccept = {};
+ await PgpSqliteDb2.getAcceptance(
+ keyMeta.keyObj.fpr,
+ this.currentRecip,
+ oldAccept
+ );
+
+ this.ignoreExternal = true;
+ await this._viewKey(keyMeta);
+ this.ignoreExternal = false;
+
+ // If the key is not yet accepted, then we want to automatically
+ // close the email-resolve view, if the user accepts the key
+ // while viewing the key details.
+ let autoCloseOnAccept = !this.isAccepted(oldAccept);
+
+ let newAccept = {};
+ await PgpSqliteDb2.getAcceptance(
+ keyMeta.keyObj.fpr,
+ this.currentRecip,
+ newAccept
+ );
+
+ if (autoCloseOnAccept && this.isAccepted(newAccept)) {
+ this.resetViews();
+ this.buildMainView();
+ } else {
+ // While viewing the key, the user could have triggered a refresh,
+ // which could have changed the validity of the key.
+ let keyMetas = await EnigmailKeyRing.getEncryptionKeyMeta(
+ this.currentRecip
+ );
+ this.buildResolveView(this.currentRecip, keyMetas);
+ }
+ },
+
+ async viewKeyFromOverview(recip, keyMeta) {
+ this.ignoreExternal = true;
+ await this._viewKey(keyMeta);
+ this.ignoreExternal = false;
+
+ // While viewing the key, the user could have triggered a refresh,
+ // which could have changed the validity of the key.
+ // In theory it would be sufficient to refresh the main view
+ // for the single email address.
+ await checkEncryptionState("openpgp-key-assistant-refresh");
+ this.buildMainView();
+ },
+
+ async _viewKey(keyMeta) {
+ let exists = EnigmailKeyRing.getKeyById(keyMeta.keyObj.keyId);
+
+ if (!exists) {
+ if (keyMeta.readiness != "collected") {
+ return;
+ }
+ await EnigmailKeyRing.importKeyDataSilent(
+ window,
+ keyMeta.collectedKey.pubKey,
+ true
+ );
+ }
+
+ EnigmailWindows.openKeyDetails(window, keyMeta.keyObj.keyId, false);
+ },
+
+ addToReadyList(recipient, detailElement) {
+ let list = document.getElementById("keysListValid");
+ let row = document.createElement("li");
+ row.classList.add("key-row");
+
+ let info = document.createElement("div");
+ info.classList.add("key-info");
+ let title = document.createElement("b");
+ title.textContent = recipient;
+
+ info.appendChild(title);
+ row.append(info, detailElement);
+ list.appendChild(row);
+ },
+
+ fillKeysStatus(element, keyMetas) {
+ let unaccepted = keyMetas.filter(
+ k =>
+ k.readiness == "undecided" ||
+ k.readiness == "rejected" ||
+ k.readiness == "otherAccepted"
+ );
+ let collected = keyMetas.filter(k => k.readiness == "collected");
+
+ // Multiple keys available.
+ if (unaccepted.length + collected.length > 1) {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-multiple-keys"
+ );
+ // TODO: add note to be careful?
+ return;
+ }
+
+ // Not expired but not accepted keys.
+ if (unaccepted.length) {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-key-unaccepted",
+ {
+ count: unaccepted.length,
+ }
+ );
+ if (unaccepted.length == 1) {
+ element.before("0x" + unaccepted[0].keyObj.keyId);
+ }
+ return;
+ }
+
+ let expiredAccepted = keyMetas.filter(
+ k => k.readiness == "expiredAccepted"
+ );
+
+ // Key accepted but expired.
+ if (expiredAccepted.length) {
+ if (expiredAccepted.length == 1) {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-key-accepted-expired",
+ {
+ date: expiredAccepted[0].keyObj.effectiveExpiry,
+ }
+ );
+ element.before("0x" + expiredAccepted[0].keyObj.keyId);
+ } else {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-keys-accepted-expired"
+ );
+ }
+ return;
+ }
+
+ let expiredUnaccepted = keyMetas.filter(
+ k =>
+ k.readiness == "expiredUndecided" ||
+ k.readiness == "expiredRejected" ||
+ k.readiness == "expiredOtherAccepted"
+ );
+
+ // Key not accepted and expired.
+ if (expiredUnaccepted.length) {
+ if (expiredUnaccepted.length == 1) {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-key-unaccepted-expired-one",
+ {
+ date: expiredUnaccepted[0].keyObj.effectiveExpiry,
+ }
+ );
+ element.before("0x" + expiredUnaccepted[0].keyObj.keyId);
+ } else {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-key-unaccepted-expired-many"
+ );
+ }
+ return;
+ }
+
+ let unacceptedNotYetImported = keyMetas.filter(
+ k => k.readiness == "collected"
+ );
+
+ if (unacceptedNotYetImported.length) {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-keys-has-collected",
+ {
+ count: unacceptedNotYetImported.length,
+ }
+ );
+ if (unacceptedNotYetImported.length == 1) {
+ element.before("0x" + unacceptedNotYetImported[0].keyObj.keyId);
+ }
+ return;
+ }
+
+ // We found nothing, so let's return a default message.
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-no-key-available"
+ );
+ },
+
+ addToProblematicList(recipient, descriptionDiv, resolveButton) {
+ let list = document.getElementById("keysListIssues");
+ let row = document.createElement("li");
+ row.classList.add("key-row");
+
+ let info = document.createElement("div");
+ info.classList.add("key-info");
+ let title = document.createElement("b");
+ title.textContent = recipient;
+ info.append(title, descriptionDiv);
+
+ if (resolveButton) {
+ row.append(info, resolveButton);
+ } else {
+ row.appendChild(info);
+ }
+
+ list.appendChild(row);
+ },
+
+ fillKeyOriginAndStatus(element, keyMeta) {
+ // The key was collected from somewhere.
+ if (keyMeta.collectedKey) {
+ let sourceSpan = document.createElement("span");
+ document.l10n.setAttributes(
+ sourceSpan,
+ "openpgp-key-assistant-key-source",
+ {
+ count: keyMeta.collectedKey.sources.length,
+ }
+ );
+ element.append(sourceSpan, ": ");
+ let linkSpan = document.createElement("span");
+ linkSpan.classList.add("comma-separated");
+
+ let sourceLinks = keyMeta.collectedKey.sources.map(source => {
+ source.type = source.type.toLowerCase(); // Earlier "WKD" was "wkd".
+ let a = document.createElement("a");
+ if (source.uri) {
+ a.href = source.uri;
+ a.title = source.uri;
+ }
+ if (source.description) {
+ if (a.title) {
+ a.title += " - ";
+ }
+ a.title += source.description;
+ }
+ let span = document.createElement("span");
+ // openpgp-key-assistant-key-collected-attachment
+ // openpgp-key-assistant-key-collected-autocrypt
+ // openpgp-key-assistant-key-collected-keyserver
+ // openpgp-key-assistant-key-collected-wkd
+ document.l10n.setAttributes(
+ span,
+ `openpgp-key-assistant-key-collected-${source.type}`
+ );
+ a.appendChild(span);
+ return a;
+ });
+ linkSpan.append(...sourceLinks);
+ element.appendChild(linkSpan);
+ return;
+ }
+
+ // The key was rejected.
+ if (keyMeta.readiness == "rejected") {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-key-rejected"
+ );
+ return;
+ }
+
+ // Key is expired.
+ if (
+ keyMeta.readiness == "expiredAccepted" ||
+ keyMeta.readiness == "expiredUndecided" ||
+ keyMeta.readiness == "expiredOtherAccepted" ||
+ keyMeta.readiness == "expiredRejected"
+ ) {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-this-key-accepted-expired",
+ {
+ date: keyMeta.keyObj.effectiveExpiry,
+ }
+ );
+ return;
+ }
+
+ if (keyMeta.readiness == "otherAccepted") {
+ // Was the key already accepted for another email address?
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-key-accepted-other",
+ {
+ date: keyMeta.keyObj.effectiveExpiry,
+ }
+ );
+ }
+ },
+
+ async buildResolveView(recipient, keyMetas) {
+ this.currentRecip = recipient;
+ document.getElementById("resolveViewAcceptKey").disabled = true;
+
+ let unaccepted = keyMetas.filter(
+ k =>
+ k.readiness == "undecided" ||
+ k.readiness == "rejected" ||
+ k.readiness == "otherAccepted"
+ );
+ let collected = keyMetas.filter(k => k.readiness == "collected");
+ let expiredAccepted = keyMetas.filter(
+ k => k.readiness == "expiredAccepted"
+ );
+ let expiredUnaccepted = keyMetas.filter(
+ k =>
+ k.readiness == "expiredUndecided" ||
+ k.readiness == "expiredRejected" ||
+ k.readiness == "expiredOtherAccepted"
+ );
+
+ this.usableKeys = unaccepted.length + collected.length;
+ let problematicKeys = expiredAccepted.length + expiredUnaccepted.length;
+ let numKeys = this.usableKeys + problematicKeys;
+
+ document.l10n.setAttributes(
+ document.getElementById("resolveViewTitle"),
+ "openpgp-key-assistant-resolve-title",
+ {
+ recipient,
+ numKeys,
+ }
+ );
+
+ document.l10n.setAttributes(
+ document.getElementById("resolveViewExpiredDescription"),
+ "openpgp-key-assistant-invalid-title",
+ { numKeys }
+ );
+
+ document.getElementById("resolveViewValid").hidden = !this.usableKeys;
+ let usableList = document.getElementById("resolveValidKeysList");
+ usableList.replaceChildren();
+
+ function createKeyRow(keyMeta, isValid) {
+ let row = document.createElement("li");
+ let label = document.createElement("label");
+ label.classList.add("flex-center");
+
+ let input = document.createElement("input");
+ input.type = "radio";
+ input.name = isValid ? "valid-key" : "invalid-key";
+ input.value = keyMeta.keyObj.keyId;
+ input.disabled = !isValid;
+
+ if (isValid) {
+ input.addEventListener("change", () => {
+ document.getElementById("resolveViewAcceptKey").disabled = false;
+ });
+ }
+ label.appendChild(input);
+
+ let keyId = document.createElement("b");
+ keyId.textContent = "0x" + keyMeta.keyObj.keyId;
+
+ let creationTime = document.createElement("time");
+ creationTime.setAttribute(
+ "datetime",
+ new Date(keyMeta.keyObj.keyCreated * 1000).toISOString()
+ );
+ document.l10n.setAttributes(
+ creationTime,
+ "openpgp-key-assistant-key-created",
+ { date: keyMeta.keyObj.created }
+ );
+ label.append(keyId, " - ", creationTime);
+ row.appendChild(label);
+
+ let fingerprint = document.createElement("div");
+ fingerprint.classList.add("key-info-block");
+ let fpDesc = document.createElement("span");
+ let fpLink = document.createElement("a");
+ fpLink.href = "#";
+ fpLink.textContent = EnigmailKey.formatFpr(keyMeta.keyObj.fpr);
+ fpLink.addEventListener("click", event => {
+ event.preventDefault();
+ gKeyAssistant.viewKeyFromResolve(keyMeta);
+ });
+ document.l10n.setAttributes(
+ fpDesc,
+ "openpgp-key-assistant-key-fingerprint"
+ );
+ fingerprint.append(fpDesc, ": ", fpLink);
+ row.appendChild(fingerprint);
+
+ let info = document.createElement("div");
+ info.classList.add("key-info-block");
+ row.append(info);
+
+ gKeyAssistant.fillKeyOriginAndStatus(info, keyMeta);
+ return row;
+ }
+
+ for (let meta of unaccepted) {
+ usableList.appendChild(createKeyRow(meta, true));
+ }
+
+ for (let meta of collected) {
+ usableList.appendChild(createKeyRow(meta, true));
+ }
+
+ document.getElementById("resolveViewInvalid").hidden = !problematicKeys;
+ let problematicList = document.getElementById("resolveInvalidKeysList");
+ problematicList.replaceChildren();
+
+ for (let meta of expiredAccepted) {
+ problematicList.appendChild(createKeyRow(meta, false));
+ }
+ for (let meta of expiredUnaccepted) {
+ problematicList.appendChild(createKeyRow(meta, false));
+ }
+
+ document.getElementById("resolveViewAcceptKey").onclick = () => {
+ this.acceptSelectedKey(recipient, keyMetas);
+ };
+ this.changeView("resolve");
+ },
+
+ async acceptSelectedKey(recipient, keyMetas) {
+ let selectedKey = document.querySelector(
+ 'input[name="valid-key"]:checked'
+ )?.value;
+ if (!selectedKey) {
+ // The accept button was enabled but nothing was selected.
+ return;
+ }
+ let fingerprint;
+
+ this.ignoreExternal = true;
+
+ let existingKey = EnigmailKeyRing.getKeyById(selectedKey);
+ if (existingKey) {
+ fingerprint = existingKey.fpr;
+ } else {
+ let unacceptedNotYetImported = keyMetas.filter(
+ k => k.readiness == "collected"
+ );
+
+ for (let keyMeta of unacceptedNotYetImported) {
+ if (keyMeta.keyObj.keyId != selectedKey) {
+ continue;
+ }
+ await EnigmailKeyRing.importKeyDataSilent(
+ window,
+ keyMeta.collectedKey.pubKey,
+ true
+ );
+ fingerprint = keyMeta.keyObj.fpr;
+ }
+ }
+
+ if (!fingerprint) {
+ throw new Error(`Key not found for id=${selectedKey}`);
+ }
+
+ await PgpSqliteDb2.addAcceptedEmail(fingerprint, recipient).catch(
+ console.error
+ );
+
+ // Trigger the UI refresh of the compose window.
+ await checkEncryptionState("openpgp-key-assistant-refresh");
+
+ this.ignoreExternal = false;
+ this.resetViews();
+ this.buildMainView();
+ },
+
+ async initOnlineDiscovery(context) {
+ let container = document.getElementById("discoveryOutput");
+ container.replaceChildren();
+
+ function write(recipient) {
+ let p = document.createElement("p");
+ let span = document.createElement("span");
+ document.l10n.setAttributes(span, "openpgp-key-assistant-discover-keys", {
+ recipient,
+ });
+ let span2 = document.createElement("span");
+ span2.classList.add("loading-inline");
+ p.append(span, " ", span2);
+ container.appendChild(p);
+ }
+
+ let gotNewData = false; // XXX: not used for anything atm
+
+ // Checking gotNewData isn't really sufficient, because the discovery could
+ // find an update for an existing key, which was expired, and is now valid
+ // again. Let's always rebuild for now.
+
+ if (context == "overview") {
+ this.ignoreExternal = true;
+ for (let email of this.recipients) {
+ if (OpenPGPAlias.hasAliasDefinition(email)) {
+ continue;
+ }
+ write(email);
+ let rv = await KeyLookupHelper.fullOnlineDiscovery(
+ "silent-collection",
+ window,
+ email,
+ null
+ );
+ gotNewData = gotNewData || rv;
+ }
+
+ // Wait a sec before closing the view, so the user has time to see what
+ // happened.
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ this.resetViews();
+ this.buildMainView();
+
+ // Online discovery and key collection triggered key change
+ // notifications. We must allow those notifications arrive while
+ // ignoreExternal is still true.
+ // Use settimeout to reset ignoreExternal to false afterwards.
+ setTimeout(function () {
+ this.ignoreExternal = false;
+ });
+ return;
+ }
+
+ // We should never arrive here for an email address that has an
+ // alias rule, because for those we don't want to perform online
+ // discovery.
+
+ if (OpenPGPAlias.hasAliasDefinition(this.currentRecip)) {
+ throw new Error(`${this.currentRecip} has an alias rule`);
+ }
+
+ write(this.currentRecip);
+
+ this.ignoreExternal = true;
+ gotNewData = await KeyLookupHelper.fullOnlineDiscovery(
+ "silent-collection",
+ window,
+ this.currentRecip,
+ null
+ );
+ // Online discovery and key collection triggered key change
+ // notifications. We must allow those notifications arrive while
+ // ignoreExternal is still true.
+ // Use settimeout to reset ignoreExternal to false afterwards.
+ setTimeout(function () {
+ this.ignoreExternal = false;
+ });
+
+ // If the recipient now has a usable previously accepted key, go back to
+ // the main view and show a successful notification.
+ let keyMetas = await EnigmailKeyRing.getEncryptionKeyMeta(
+ this.currentRecip
+ );
+
+ if (keyMetas.some(k => k.readiness == "accepted")) {
+ // Trigger the UI refresh of the compose window.
+ await checkEncryptionState("openpgp-key-assistant-refresh");
+
+ // Wait a sec before closing the view, so the user has time to see what
+ // happened.
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ this.resetViews();
+ this.buildMainView();
+
+ let notification =
+ this.notificationBox.getNotificationWithValue("acceptedKeyUpdated");
+
+ // If a notification already exists, simply update the message.
+ if (notification) {
+ document.l10n.setAttributes(
+ notification.messageText,
+ "openpgp-key-assistant-expired-key-update",
+ {
+ recipient: this.currentRecip,
+ }
+ );
+ return;
+ }
+
+ notification = this.notificationBox.appendNotification(
+ "acceptedKeyUpdated",
+ {
+ label: {
+ "l10n-id": "openpgp-key-assistant-expired-key-update",
+ "l10n-args": { recipient: this.currentRecip },
+ },
+ priority: this.notificationBox.PRIORITY_INFO_HIGH,
+ },
+ null
+ );
+ notification.setAttribute("type", "success");
+ return;
+ }
+
+ this.buildResolveView(this.currentRecip, keyMetas);
+ gKeyAssistant.changeView("resolve");
+ },
+
+ toggleRecipientsList() {
+ let list = document.getElementById("keysListValid");
+ list.hidden = !list.hidden;
+
+ document.l10n.setAttributes(
+ document.getElementById("toggleRecipientsButton"),
+ list.hidden
+ ? "openpgp-key-assistant-recipients-show-button"
+ : "openpgp-key-assistant-recipients-hide-button"
+ );
+ },
+
+ async importFromFile(context) {
+ await EnigmailCommon_importObjectFromFile("pub");
+ if (context == "overview") {
+ this.buildMainView();
+ } else {
+ this.buildResolveView(
+ this.currentRecip,
+ await EnigmailKeyRing.getEncryptionKeyMeta(this.currentRecip)
+ );
+ }
+ },
+
+ onExternalKeyChange() {
+ if (!this.dialog || !this.dialog.open) {
+ return;
+ }
+
+ if (this.ignoreExternal) {
+ return;
+ }
+
+ // Refresh the "overview", which will potentially close a currently
+ // shown "resolve" view.
+ this.resetViews();
+ this.buildMainView();
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.js b/comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.js
new file mode 100644
index 0000000000..d82848f932
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.js
@@ -0,0 +1,1119 @@
+/* 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/. */
+
+// from enigmailKeyManager.js:
+/* global l10n */
+
+"use strict";
+
+var { CommonUtils } = ChromeUtils.importESModule(
+ "resource://services-common/utils.sys.mjs"
+);
+var { EnigmailFuncs } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/funcs.jsm"
+);
+var { EnigmailLog } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/log.jsm"
+);
+var { EnigmailKey } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/key.jsm"
+);
+var { EnigmailKeyRing } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+);
+var { PgpSqliteDb2 } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/sqliteDb.jsm"
+);
+var { EnigmailCryptoAPI } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/cryptoAPI.jsm"
+);
+var { KeyLookupHelper } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyLookupHelper.jsm"
+);
+var { RNP, RnpPrivateKeyUnlockTracker } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/RNP.jsm"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
+});
+
+var gModePersonal = false;
+
+// This is the ID that was given to us as a parameter.
+// Note that it might be the ID of a subkey.
+var gKeyId = null;
+
+var gUserId = null;
+var gKeyList = null;
+var gSigTree = null;
+
+var gAllEmails = [];
+var gOriginalAcceptedEmails = null;
+var gAcceptedEmails = null;
+
+var gHaveUnacceptedEmails = false;
+var gFingerprint = "";
+var gHasMissingSecret = false;
+
+var gAcceptanceRadio = null;
+var gPersonalRadio = null;
+
+var gOriginalAcceptance;
+var gOriginalPersonal;
+var gUpdateAllowed = false;
+
+let gAllEmailCheckboxes = [];
+let gOkButton;
+
+let gPrivateKeyTrackers = [];
+
+window.addEventListener("DOMContentLoaded", onLoad);
+window.addEventListener("unload", onUnload);
+
+function onUnload() {
+ releasePrivateKeys();
+}
+
+function releasePrivateKeys() {
+ for (let tracker of gPrivateKeyTrackers) {
+ tracker.release();
+ }
+ gPrivateKeyTrackers = [];
+}
+
+async function onLoad() {
+ if (window.arguments[1]) {
+ window.arguments[1].refresh = false;
+ }
+
+ gAcceptanceRadio = document.getElementById("acceptanceRadio");
+ gPersonalRadio = document.getElementById("personalRadio");
+
+ gKeyId = window.arguments[0].keyId;
+
+ gOkButton = document.querySelector("dialog").getButton("accept");
+ gOkButton.focus();
+
+ await reloadData(true);
+
+ let sepPassphraseEnabled =
+ gModePersonal &&
+ Services.prefs.getBoolPref("mail.openpgp.passphrases.enabled");
+ document.getElementById("passphraseTab").hidden = !sepPassphraseEnabled;
+ document.getElementById("passphrasePanel").hidden = !sepPassphraseEnabled;
+ if (sepPassphraseEnabled) {
+ await loadPassphraseProtection();
+ }
+
+ onAcceptanceChanged();
+}
+
+/***
+ * Set the label text of a HTML element
+ */
+function setLabel(elementId, label) {
+ let node = document.getElementById(elementId);
+ node.setAttribute("value", label);
+}
+
+async function changeExpiry() {
+ let keyObj = EnigmailKeyRing.getKeyById(gKeyId);
+ if (!keyObj || !keyObj.secretAvailable) {
+ return;
+ }
+
+ if (!keyObj.iSimpleOneSubkeySameExpiry()) {
+ Services.prompt.alert(
+ null,
+ document.title,
+ await document.l10n.formatValue("openpgp-cannot-change-expiry")
+ );
+ return;
+ }
+
+ let args = {
+ keyId: keyObj.keyId,
+ modified: onDataModified,
+ };
+
+ // The keyDetailsDlg can be opened from different locations, some of which
+ // don't belong to the Account Settings, therefore they won't have access to
+ // the gSubDialog object.
+ if (parent.gSubDialog) {
+ parent.gSubDialog.open(
+ "chrome://openpgp/content/ui/changeExpiryDlg.xhtml",
+ undefined,
+ args
+ );
+ return;
+ }
+
+ window.openDialog(
+ "chrome://openpgp/content/ui/changeExpiryDlg.xhtml",
+ "",
+ "dialog,modal,centerscreen,resizable",
+ args
+ );
+}
+
+async function refreshOnline() {
+ let keyObj = EnigmailKeyRing.getKeyById(gKeyId);
+ if (!keyObj) {
+ return;
+ }
+
+ let imported = await KeyLookupHelper.lookupAndImportByKeyID(
+ "interactive-import",
+ window,
+ keyObj.fpr,
+ true
+ );
+ if (imported) {
+ onDataModified();
+ }
+}
+
+async function loadPassphraseProtection() {
+ let keyObj = EnigmailKeyRing.getKeyById(gKeyId);
+ if (!keyObj || !keyObj.secretAvailable) {
+ return;
+ }
+
+ let primaryKey = RnpPrivateKeyUnlockTracker.constructFromFingerprint(
+ keyObj.fpr
+ );
+ primaryKey.setAllowPromptingUserForPassword(false);
+ primaryKey.setAllowAutoUnlockWithCachedPasswords(false);
+ let isSecretForPrimaryAvailable = primaryKey.available();
+ let canUnlockSecretForPrimary = false;
+ if (isSecretForPrimaryAvailable) {
+ await primaryKey.unlock();
+ canUnlockSecretForPrimary = primaryKey.isUnlocked();
+ gPrivateKeyTrackers.push(primaryKey);
+ }
+
+ let countSubkeysWithSecretAvailable = 0;
+ let countSubkeysCanAutoUnlock = 0;
+
+ for (let i = 0; i < keyObj.subKeys.length; i++) {
+ let subKey = RnpPrivateKeyUnlockTracker.constructFromFingerprint(
+ keyObj.subKeys[i].fpr
+ );
+ subKey.setAllowPromptingUserForPassword(false);
+ subKey.setAllowAutoUnlockWithCachedPasswords(false);
+ let isSecretForPrimaryAvailable = subKey.available();
+ let canUnlockSecretForPrimary = false;
+ if (isSecretForPrimaryAvailable) {
+ ++countSubkeysWithSecretAvailable;
+ await subKey.unlock();
+ canUnlockSecretForPrimary = subKey.isUnlocked();
+ if (canUnlockSecretForPrimary) {
+ countSubkeysCanAutoUnlock++;
+ }
+ gPrivateKeyTrackers.push(subKey);
+ }
+ }
+
+ let userPassphraseMode = "user-passphrase";
+ let usingPP = LoginHelper.isPrimaryPasswordSet();
+ let protectionMode;
+
+ // Could we use the automatic passphrase to unlock all secret keys for
+ // which the key material is available?
+
+ if (
+ (!isSecretForPrimaryAvailable || canUnlockSecretForPrimary) &&
+ countSubkeysWithSecretAvailable == countSubkeysCanAutoUnlock
+ ) {
+ protectionMode = usingPP ? "primary-password" : "unprotected";
+ } else {
+ protectionMode = userPassphraseMode;
+ }
+
+ // Strings used here:
+ // openpgp-passphrase-status-unprotected
+ // openpgp-passphrase-status-primary-password
+ // openpgp-passphrase-status-user-passphrase
+ document.l10n.setAttributes(
+ document.getElementById("passphraseStatus"),
+ `openpgp-passphrase-status-${protectionMode}`
+ );
+
+ // Strings used here:
+ // openpgp-passphrase-instruction-unprotected
+ // openpgp-passphrase-instruction-primary-password
+ // openpgp-passphrase-instruction-user-passphrase
+ document.l10n.setAttributes(
+ document.getElementById("passphraseInstruction"),
+ `openpgp-passphrase-instruction-${protectionMode}`
+ );
+
+ document.getElementById("unlockBox").hidden =
+ protectionMode != userPassphraseMode;
+ document.getElementById("lockBox").hidden =
+ protectionMode == userPassphraseMode;
+ document.getElementById("usePrimaryPassword").hidden = true;
+ document.getElementById("removeProtection").hidden = true;
+
+ document.l10n.setAttributes(
+ document.getElementById("setPassphrase"),
+ protectionMode == userPassphraseMode
+ ? "openpgp-passphrase-change"
+ : "openpgp-passphrase-set"
+ );
+
+ document.getElementById("passwordInput").value = "";
+ document.getElementById("passwordConfirm").value = "";
+}
+
+async function unlock() {
+ let pwCache = {
+ passwords: [],
+ };
+
+ for (let tracker of gPrivateKeyTrackers) {
+ tracker.setAllowPromptingUserForPassword(true);
+ tracker.setAllowAutoUnlockWithCachedPasswords(true);
+ tracker.setPasswordCache(pwCache);
+ await tracker.unlock();
+ if (!tracker.isUnlocked()) {
+ return;
+ }
+ }
+
+ document.l10n.setAttributes(
+ document.getElementById("passphraseInstruction"),
+ "openpgp-passphrase-unlocked"
+ );
+ document.getElementById("unlockBox").hidden = true;
+ document.getElementById("lockBox").hidden = false;
+ document.getElementById("passwordInput").value = "";
+ document.getElementById("passwordConfirm").value = "";
+
+ document.getElementById(
+ LoginHelper.isPrimaryPasswordSet()
+ ? "usePrimaryPassword"
+ : "removeProtection"
+ ).hidden = false;
+
+ // Necessary to set the disabled status of the button
+ onPasswordInput();
+}
+
+function onPasswordInput() {
+ let pw1 = document.getElementById("passwordInput").value;
+ let pw2 = document.getElementById("passwordConfirm").value;
+
+ // Disable the button if the two passwords don't match, and enable it
+ // if the passwords do match.
+ let disabled = pw1 != pw2 || !pw1.length;
+
+ document.getElementById("setPassphrase").disabled = disabled;
+}
+
+async function setPassphrase() {
+ let pw = document.getElementById("passwordInput").value;
+
+ for (let tracker of gPrivateKeyTrackers) {
+ tracker.setPassphrase(pw);
+ }
+ await RNP.saveKeyRings();
+
+ releasePrivateKeys();
+ loadPassphraseProtection();
+}
+
+async function useAutoPassphrase() {
+ for (let tracker of gPrivateKeyTrackers) {
+ await tracker.setAutoPassphrase();
+ }
+ await RNP.saveKeyRings();
+
+ releasePrivateKeys();
+ loadPassphraseProtection();
+}
+
+function onAcceptanceChanged() {
+ // The check for gAcceptedEmails.size is to handle an edge case.
+ // If a key was previously accepted, for an email address that is
+ // now revoked, and another email address has been added,
+ // then the key can be marked as accepted without any accepted
+ // email address.
+ // In this scenario, we must allow the user to edit the accepted
+ // email addresses, even if there's just one email address available.
+ // Another scenario is a data inconsistency, with accepted key,
+ // but no accepted email.
+
+ let originalAccepted = isAccepted(gOriginalAcceptance);
+ let wantAccepted = isAccepted(gAcceptanceRadio.value);
+
+ let disableEmailsTab =
+ (wantAccepted &&
+ gAllEmails.length < 2 &&
+ gAcceptedEmails.size != 0 &&
+ (!originalAccepted || !gHaveUnacceptedEmails)) ||
+ !wantAccepted;
+
+ document.getElementById("emailAddressesTab").disabled = disableEmailsTab;
+ document.getElementById("emailAddressesPanel").disabled = disableEmailsTab;
+
+ setOkButtonState();
+}
+
+function onDataModified() {
+ EnigmailKeyRing.clearCache();
+ enableRefresh();
+ reloadData(false);
+}
+
+function isAccepted(value) {
+ return value == "unverified" || value == "verified";
+}
+
+async function reloadData(firstLoad) {
+ gUserId = null;
+
+ var treeChildren = document.getElementById("keyListChildren");
+
+ // clean lists
+ while (treeChildren.firstChild) {
+ treeChildren.firstChild.remove();
+ }
+
+ let keyObj = EnigmailKeyRing.getKeyById(gKeyId);
+ if (!keyObj) {
+ return;
+ }
+
+ let acceptanceIntroText = "";
+ let acceptanceVerificationText = "";
+
+ if (keyObj.fpr) {
+ gFingerprint = keyObj.fpr;
+ setLabel("fingerprint", EnigmailKey.formatFpr(keyObj.fpr));
+ }
+
+ gSigTree = document.getElementById("signatures_tree");
+ let cApi = EnigmailCryptoAPI();
+ let signatures = await cApi.getKeyObjSignatures(keyObj);
+ gSigTree.view = new SigListView(signatures);
+
+ document.getElementById("subkeyList").view = new SubkeyListView(keyObj);
+
+ gUserId = keyObj.userId;
+
+ setLabel("keyId", "0x" + keyObj.keyId);
+ setLabel("keyCreated", keyObj.created);
+
+ let keyIsExpired =
+ keyObj.effectiveExpiryTime &&
+ keyObj.effectiveExpiryTime < Math.floor(Date.now() / 1000);
+
+ let expiryInfo;
+ let expireArgument = null;
+ let expiryInfoKey = "";
+ if (keyObj.keyTrust == "r") {
+ expiryInfoKey = "key-revoked-simple";
+ } else if (keyObj.keyTrust == "e" || keyIsExpired) {
+ expiryInfoKey = "key-expired-date";
+ expireArgument = keyObj.effectiveExpiry;
+ } else if (keyObj.effectiveExpiry.length === 0) {
+ expiryInfoKey = "key-does-not-expire";
+ } else {
+ expiryInfo = keyObj.effectiveExpiry;
+ }
+ if (expiryInfoKey) {
+ expiryInfo = l10n.formatValueSync(expiryInfoKey, {
+ keyExpiry: expireArgument,
+ });
+ }
+ setLabel("keyExpiry", expiryInfo);
+
+ gModePersonal = keyObj.secretAvailable;
+
+ document.getElementById("passphraseTab").hidden = !gModePersonal;
+ document.getElementById("passphrasePanel").hidden = !gModePersonal;
+
+ if (gModePersonal) {
+ gPersonalRadio.removeAttribute("hidden");
+ gAcceptanceRadio.setAttribute("hidden", "true");
+ acceptanceIntroText = "key-accept-personal";
+ let value = l10n.formatValueSync("key-type-pair");
+ setLabel("keyType", value);
+
+ gUpdateAllowed = true;
+ if (firstLoad) {
+ gOriginalPersonal = await PgpSqliteDb2.isAcceptedAsPersonalKey(
+ keyObj.fpr
+ );
+ gPersonalRadio.value = gOriginalPersonal ? "personal" : "not_personal";
+ }
+
+ if (keyObj.keyTrust != "r") {
+ document.getElementById("changeExpiryButton").removeAttribute("hidden");
+ }
+ } else {
+ gPersonalRadio.setAttribute("hidden", "true");
+ let value = l10n.formatValueSync("key-type-public");
+ setLabel("keyType", value);
+
+ let isStillValid = !(
+ keyObj.keyTrust == "r" ||
+ keyObj.keyTrust == "e" ||
+ keyIsExpired
+ );
+ if (!isStillValid) {
+ gAcceptanceRadio.setAttribute("hidden", "true");
+ if (keyObj.keyTrust == "r") {
+ acceptanceIntroText = "key-revoked-simple";
+ } else if (keyObj.keyTrust == "e" || keyIsExpired) {
+ acceptanceIntroText = "key-expired-simple";
+ }
+ } else {
+ gAcceptanceRadio.removeAttribute("hidden");
+ acceptanceIntroText = "key-do-you-accept";
+ acceptanceVerificationText = "key-verification";
+ gUpdateAllowed = true;
+
+ //await RNP.calculateAcceptance(keyObj.keyId, null);
+
+ let acceptanceResult = await PgpSqliteDb2.getFingerprintAcceptance(
+ null,
+ keyObj.fpr
+ );
+
+ if (firstLoad) {
+ if (!acceptanceResult) {
+ gOriginalAcceptance = "undecided";
+ } else {
+ gOriginalAcceptance = acceptanceResult;
+ }
+ gAcceptanceRadio.value = gOriginalAcceptance;
+ }
+ }
+
+ if (firstLoad) {
+ gAcceptedEmails = new Set();
+
+ for (let i = 0; i < keyObj.userIds.length; i++) {
+ if (keyObj.userIds[i].type === "uid") {
+ let uidEmail = EnigmailFuncs.getEmailFromUserID(
+ keyObj.userIds[i].userId
+ );
+ if (uidEmail) {
+ gAllEmails.push(uidEmail);
+
+ if (isAccepted(gOriginalAcceptance)) {
+ let rv = {};
+ await PgpSqliteDb2.getAcceptance(keyObj.fpr, uidEmail, rv);
+ if (rv.emailDecided) {
+ gAcceptedEmails.add(uidEmail);
+ } else {
+ gHaveUnacceptedEmails = true;
+ }
+ } else {
+ // For not-yet-accepted keys, our default is to accept
+ // all shown email addresses.
+ gAcceptedEmails.add(uidEmail);
+ }
+ }
+ }
+ }
+
+ // clone
+ gOriginalAcceptedEmails = new Set(gAcceptedEmails);
+ }
+ }
+
+ await createUidData(keyObj);
+
+ if (acceptanceIntroText) {
+ let acceptanceIntro = document.getElementById("acceptanceIntro");
+ document.l10n.setAttributes(acceptanceIntro, acceptanceIntroText);
+ }
+
+ if (acceptanceVerificationText) {
+ let acceptanceVerification = document.getElementById(
+ "acceptanceVerification"
+ );
+ document.l10n.setAttributes(
+ acceptanceVerification,
+ acceptanceVerificationText,
+ {
+ addr: EnigmailFuncs.getEmailFromUserID(gUserId).toLowerCase(),
+ }
+ );
+ }
+
+ document.getElementById("key-detail-has-insecure").hidden =
+ !keyObj.hasIgnoredAttributes;
+}
+
+function setOkButtonState() {
+ let atLeastOneChecked = gAllEmailCheckboxes.some(c => c.checked);
+ gOkButton.disabled = !atLeastOneChecked && isAccepted(gAcceptanceRadio.value);
+}
+
+async function createUidData(keyDetails) {
+ var uidList = document.getElementById("userIds");
+ while (uidList.firstChild) {
+ uidList.firstChild.remove();
+ }
+
+ let primaryIdIndex = 0;
+
+ for (let i = 0; i < keyDetails.userIds.length; i++) {
+ if (keyDetails.userIds[i].type === "uid") {
+ if (keyDetails.userIds[i].userId == keyDetails.userId) {
+ primaryIdIndex = i;
+ break;
+ }
+ }
+ }
+
+ for (let i = -1; i < keyDetails.userIds.length; i++) {
+ // Handle entry primaryIdIndex first.
+
+ let indexToUse;
+ if (i == -1) {
+ indexToUse = primaryIdIndex;
+ } else if (i == primaryIdIndex) {
+ // already handled when i was -1
+ continue;
+ } else {
+ indexToUse = i;
+ }
+
+ if (keyDetails.userIds[indexToUse].type === "uid") {
+ let uidStr = keyDetails.userIds[indexToUse].userId;
+
+ /* - attempted code with <ul id="userIds">, doesn't work yet
+ let item = document.createElement("li");
+
+ let text = document.createElement("div");
+ text.textContent = uidStr;
+ item.append(text);
+
+ let lf = document.createElement("br");
+ item.append(lf);
+ uidList.appendChild(item);
+ */
+
+ uidList.appendItem(uidStr);
+ }
+ }
+
+ if (gModePersonal) {
+ document.getElementById("emailAddressesTab").hidden = true;
+ } else {
+ let emailList = document.getElementById("addressesList");
+
+ let atLeastOneChecked = false;
+ let gUniqueEmails = new Set();
+
+ for (let i = 0; i < gAllEmails.length; i++) {
+ let email = gAllEmails[i];
+ if (gUniqueEmails.has(email)) {
+ continue;
+ }
+ gUniqueEmails.add(email);
+
+ let checkbox = document.createXULElement("checkbox");
+
+ checkbox.value = email;
+ checkbox.setAttribute("label", email);
+
+ checkbox.checked = gAcceptedEmails.has(email);
+ if (checkbox.checked) {
+ atLeastOneChecked = true;
+ }
+
+ checkbox.disabled = !gUpdateAllowed;
+ checkbox.addEventListener("command", () => {
+ setOkButtonState();
+ });
+
+ emailList.appendChild(checkbox);
+ gAllEmailCheckboxes.push(checkbox);
+ }
+
+ // Usually, if we have only one email address available,
+ // we want to hide the tab.
+ // There are edge cases - if we have a data inconsistency
+ // (key accepted, but no email accepted), then we must show,
+ // to allow the user to repair.
+
+ document.getElementById("emailAddressesTab").hidden =
+ gUniqueEmails.size < 2 && atLeastOneChecked;
+ }
+}
+
+function setAttr(attribute, value) {
+ var elem = document.getElementById(attribute);
+ if (elem) {
+ elem.value = value;
+ }
+}
+
+function enableRefresh() {
+ if (window.arguments[1]) {
+ window.arguments[1].refresh = true;
+ }
+
+ window.arguments[0].modified();
+}
+
+// ------------------ onCommand Functions -----------------
+
+/*
+function signKey() {
+ if (EnigmailWindows.signKey(window, gUserId, gKeyId)) {
+ enableRefresh();
+ reloadData(false);
+ }
+}
+*/
+
+/*
+function manageUids() {
+ let keyObj = EnigmailKeyRing.getKeyById(gKeyId);
+
+ var inputObj = {
+ keyId: keyObj.keyId,
+ ownKey: keyObj.secretAvailable,
+ };
+
+ var resultObj = {
+ refresh: false,
+ };
+ window.openDialog(
+ "chrome://openpgp/content/ui/enigmailManageUidDlg.xhtml",
+ "",
+ "dialog,modal,centerscreen,resizable=yes",
+ inputObj,
+ resultObj
+ );
+ if (resultObj.refresh) {
+ enableRefresh();
+ reloadData(false);
+ }
+}
+*/
+
+function genRevocationCert() {
+ throw new Error("Not implemented");
+
+ /*
+ var defaultFileName = userId.replace(/[<>]/g, "");
+ defaultFileName += " (0x" + keyId + ") rev.asc";
+ var outFile = EnigFilePicker("XXXsaveRevokeCertAs",
+ "", true, "*.asc",
+ defaultFileName, ["XXXasciiArmorFile", "*.asc"];
+ if (!outFile) return -1;
+
+ return 0;
+ */
+}
+
+/**
+ * @param {Object[]] signatures - list of signature objects
+ * signatures.userId {string} - User ID.
+ * signatures.uidLabel {string} - UID label.
+ * signatures.created
+ * signatures.fpr {string} - Fingerprint.
+ * signatures.sigList {Object[]} - Objects
+ * signatures.sigList.userId
+ * signatures.sigList.created
+ * signatures.sigList.signerKeyId
+ * signatures.sigList.sigType
+ * signatures.sigList.sigKnown
+ */
+function SigListView(signatures) {
+ this.keyObj = [];
+
+ for (let sig of signatures) {
+ let k = {
+ uid: sig.userId,
+ keyId: sig.keyId,
+ created: sig.created,
+ expanded: true,
+ sigList: [],
+ };
+
+ for (let s of sig.sigList) {
+ k.sigList.push({
+ uid: s.userId,
+ created: s.created,
+ keyId: s.signerKeyId,
+ sigType: s.sigType,
+ });
+ }
+ this.keyObj.push(k);
+ }
+
+ this.prevKeyObj = null;
+ this.prevRow = -1;
+
+ this.updateRowCount();
+}
+
+/**
+ * @implements {nsITreeView}
+ */
+SigListView.prototype = {
+ updateRowCount() {
+ let rc = 0;
+
+ for (let i in this.keyObj) {
+ rc += this.keyObj[i].expanded ? this.keyObj[i].sigList.length + 1 : 1;
+ }
+
+ this.rowCount = rc;
+ },
+
+ setLastKeyObj(keyObj, row) {
+ this.prevKeyObj = keyObj;
+ this.prevRow = row;
+ return keyObj;
+ },
+
+ getSigAtIndex(row) {
+ if (this.lastIndex == row) {
+ return this.lastKeyObj;
+ }
+
+ let j = 0,
+ l = 0;
+
+ for (let i in this.keyObj) {
+ if (j === row) {
+ return this.setLastKeyObj(this.keyObj[i], row);
+ }
+ j++;
+
+ if (this.keyObj[i].expanded) {
+ l = this.keyObj[i].sigList.length;
+
+ if (j + l >= row && row - j < l) {
+ return this.setLastKeyObj(this.keyObj[i].sigList[row - j], row);
+ }
+ j += l;
+ }
+ }
+
+ return null;
+ },
+
+ getCellText(row, column) {
+ let s = this.getSigAtIndex(row);
+
+ if (s) {
+ switch (column.id) {
+ case "sig_uid_col":
+ return s.uid;
+ case "sig_keyid_col":
+ return "0x" + s.keyId;
+ case "sig_created_col":
+ return s.created;
+ }
+ }
+
+ return "";
+ },
+
+ setTree(treebox) {
+ this.treebox = treebox;
+ },
+
+ isContainer(row) {
+ let s = this.getSigAtIndex(row);
+ return "sigList" in s;
+ },
+
+ isSeparator(row) {
+ return false;
+ },
+
+ isSorted() {
+ return false;
+ },
+
+ getLevel(row) {
+ let s = this.getSigAtIndex(row);
+ return "sigList" in s ? 0 : 1;
+ },
+
+ cycleHeader(col, elem) {},
+
+ getImageSrc(row, col) {
+ return null;
+ },
+
+ getRowProperties(row, props) {},
+
+ getCellProperties(row, col) {
+ return "";
+ },
+
+ canDrop(row, orientation, data) {
+ return false;
+ },
+
+ getColumnProperties(colid, col, props) {},
+
+ isContainerEmpty(row) {
+ return false;
+ },
+
+ getParentIndex(idx) {
+ return -1;
+ },
+
+ getProgressMode(row, col) {},
+
+ isContainerOpen(row) {
+ let s = this.getSigAtIndex(row);
+ return s.expanded;
+ },
+
+ isSelectable(row, col) {
+ return true;
+ },
+
+ toggleOpenState(row) {
+ let s = this.getSigAtIndex(row);
+ s.expanded = !s.expanded;
+ let r = this.rowCount;
+ this.updateRowCount();
+ gSigTree.rowCountChanged(row, this.rowCount - r);
+ },
+};
+
+function createSubkeyItem(mainKeyIsSecret, subkey) {
+ // Get expiry state of this subkey
+ let expire;
+ if (subkey.keyTrust === "r") {
+ expire = l10n.formatValueSync("key-valid-revoked");
+ } else if (subkey.expiryTime === 0) {
+ expire = l10n.formatValueSync("key-expiry-never");
+ } else {
+ expire = subkey.expiry;
+ }
+
+ let subkeyType = "";
+
+ if (mainKeyIsSecret && (!subkey.secretAvailable || !subkey.secretMaterial)) {
+ subkeyType = "(!) ";
+ gHasMissingSecret = true;
+ }
+ if (subkey.type === "pub") {
+ subkeyType += l10n.formatValueSync("key-type-primary");
+ } else {
+ subkeyType += l10n.formatValueSync("key-type-subkey");
+ }
+
+ let usagetext = "";
+ let i;
+ // e = encrypt
+ // s = sign
+ // c = certify
+ // a = authentication
+ // Capital Letters are ignored, as these reflect summary properties of a key
+
+ var singlecode = "";
+ for (i = 0; i < subkey.keyUseFor.length; i++) {
+ singlecode = subkey.keyUseFor.substr(i, 1);
+ switch (singlecode) {
+ case "e":
+ if (usagetext.length > 0) {
+ usagetext = usagetext + ", ";
+ }
+ usagetext = usagetext + l10n.formatValueSync("key-usage-encrypt");
+ break;
+ case "s":
+ if (usagetext.length > 0) {
+ usagetext = usagetext + ", ";
+ }
+ usagetext = usagetext + l10n.formatValueSync("key-usage-sign");
+ break;
+ case "c":
+ if (usagetext.length > 0) {
+ usagetext = usagetext + ", ";
+ }
+ usagetext = usagetext + l10n.formatValueSync("key-usage-certify");
+ break;
+ case "a":
+ if (usagetext.length > 0) {
+ usagetext = usagetext + ", ";
+ }
+ usagetext =
+ usagetext + l10n.formatValueSync("key-usage-authentication");
+ break;
+ } // * case *
+ } // * for *
+
+ let keyObj = {
+ keyType: subkeyType,
+ keyId: "0x" + subkey.keyId,
+ algo: subkey.algoSym,
+ size: subkey.keySize,
+ creationDate: subkey.created,
+ expiry: expire,
+ usage: usagetext,
+ };
+
+ return keyObj;
+}
+
+function SubkeyListView(keyObj) {
+ gHasMissingSecret = false;
+
+ this.subkeys = [];
+ this.rowCount = keyObj.subKeys.length + 1;
+ this.subkeys.push(createSubkeyItem(keyObj.secretAvailable, keyObj));
+
+ for (let i = 0; i < keyObj.subKeys.length; i++) {
+ this.subkeys.push(
+ createSubkeyItem(keyObj.secretAvailable, keyObj.subKeys[i])
+ );
+ }
+
+ document.getElementById("legendMissingSecret").hidden = !gHasMissingSecret;
+}
+
+// implements nsITreeView
+SubkeyListView.prototype = {
+ getCellText(row, column) {
+ let s = this.subkeys[row];
+
+ if (s) {
+ switch (column.id) {
+ case "keyTypeCol":
+ return s.keyType;
+ case "keyIdCol":
+ return s.keyId;
+ case "algoCol":
+ return s.algo;
+ case "sizeCol":
+ return s.size;
+ case "createdCol":
+ return s.creationDate;
+ case "expiryCol":
+ return s.expiry;
+ case "keyUsageCol":
+ return s.usage;
+ }
+ }
+
+ return "";
+ },
+
+ setTree(treebox) {
+ this.treebox = treebox;
+ },
+
+ isContainer(row) {
+ return false;
+ },
+
+ isSeparator(row) {
+ return false;
+ },
+
+ isSorted() {
+ return false;
+ },
+
+ getLevel(row) {
+ return 0;
+ },
+
+ cycleHeader(col, elem) {},
+
+ getImageSrc(row, col) {
+ return null;
+ },
+
+ getRowProperties(row, props) {},
+
+ getCellProperties(row, col) {
+ return "";
+ },
+
+ canDrop(row, orientation, data) {
+ return false;
+ },
+
+ getColumnProperties(colid, col, props) {},
+
+ isContainerEmpty(row) {
+ return false;
+ },
+
+ getParentIndex(idx) {
+ return -1;
+ },
+
+ getProgressMode(row, col) {},
+
+ isContainerOpen(row) {
+ return false;
+ },
+
+ isSelectable(row, col) {
+ return true;
+ },
+
+ toggleOpenState(row) {},
+};
+
+function sigHandleDblClick(event) {}
+
+document.addEventListener("dialogaccept", async function (event) {
+ // Prevent the closing of the dialog to wait until all the SQLite operations
+ // have properly been executed.
+ event.preventDefault();
+
+ // The user's personal OpenPGP key acceptance was edited.
+ if (gModePersonal) {
+ if (gUpdateAllowed && gPersonalRadio.value != gOriginalPersonal) {
+ if (gPersonalRadio.value == "personal") {
+ await PgpSqliteDb2.acceptAsPersonalKey(gFingerprint);
+ } else {
+ await PgpSqliteDb2.deletePersonalKeyAcceptance(gFingerprint);
+ }
+
+ enableRefresh();
+ }
+ window.close();
+ return;
+ }
+
+ // If the recipient's key hasn't been revoked or invalidated, and the
+ // signature acceptance was edited.
+ if (gUpdateAllowed) {
+ let selectedEmails = new Set();
+ for (let checkbox of gAllEmailCheckboxes) {
+ if (checkbox.checked) {
+ selectedEmails.add(checkbox.value);
+ }
+ }
+
+ if (
+ gAcceptanceRadio.value != gOriginalAcceptance ||
+ !CommonUtils.setEqual(gAcceptedEmails, selectedEmails)
+ ) {
+ await PgpSqliteDb2.updateAcceptance(
+ gFingerprint,
+ [...selectedEmails],
+ gAcceptanceRadio.value
+ );
+
+ enableRefresh();
+ }
+ }
+
+ window.close();
+});
diff --git a/comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.xhtml b/comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.xhtml
new file mode 100644
index 0000000000..a7f57d0339
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.xhtml
@@ -0,0 +1,405 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/keyDetails.css" type="text/css"?>
+
+<!DOCTYPE html>
+<html
+ id="enigmailKeyDetailsDlg"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <head>
+ <title data-l10n-id="openpgp-key-details-doc-title"></title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="messenger/openpgp/openpgp.ftl" />
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js" />
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js" />
+ <script defer="defer" src="chrome://openpgp/content/ui/enigmailCommon.js" />
+ <script
+ defer="defer"
+ src="chrome://openpgp/content/ui/enigmailKeyManager.js"
+ />
+ <script defer="defer" src="chrome://openpgp/content/ui/keyDetailsDlg.js" />
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ buttons="accept,cancel"
+ data-l10n-id="openpgp-card-details-close-window-label"
+ >
+ <html:div class="key-details-container">
+ <html:aside class="key-details-grid">
+ <label
+ class="key-detail-label"
+ data-l10n-id="openpgp-key-details-user-id3-label"
+ />
+
+ <richlistbox
+ id="userIds"
+ class="additional-key-identity plain"
+ flex="1"
+ />
+
+ <label
+ class="key-detail-label"
+ data-l10n-id="openpgp-key-details-key-type-label"
+ />
+ <hbox class="input-container">
+ <html:input
+ id="keyType"
+ type="text"
+ class="plain"
+ readonly="readonly"
+ value="?"
+ />
+ </hbox>
+
+ <label
+ class="key-detail-label"
+ data-l10n-id="openpgp-key-details-key-id-label"
+ />
+ <hbox class="input-container">
+ <html:input
+ id="keyId"
+ type="text"
+ class="plain"
+ readonly="readonly"
+ value="?"
+ />
+ </hbox>
+
+ <label
+ class="key-detail-label"
+ data-l10n-id="openpgp-key-details-fingerprint-label"
+ />
+ <hbox class="input-container">
+ <html:input
+ id="fingerprint"
+ type="text"
+ class="plain"
+ readonly="readonly"
+ value="?"
+ />
+ </hbox>
+
+ <label
+ class="key-detail-label"
+ data-l10n-id="openpgp-key-details-created-header"
+ />
+ <hbox class="input-container">
+ <html:input
+ id="keyCreated"
+ type="text"
+ class="plain"
+ readonly="readonly"
+ value="?"
+ />
+ </hbox>
+
+ <label
+ class="key-detail-label"
+ data-l10n-id="openpgp-key-details-expiry-header"
+ />
+ <hbox class="input-container">
+ <html:input
+ id="keyExpiry"
+ type="text"
+ class="plain"
+ readonly="readonly"
+ value="?"
+ />
+ </hbox>
+ </html:aside>
+
+ <html:aside>
+ <vbox>
+ <button
+ id="refreshOnlineButton"
+ data-l10n-id="openpgp-key-man-refresh-online"
+ oncommand="refreshOnline()"
+ />
+ <button
+ id="changeExpiryButton"
+ data-l10n-id="openpgp-key-man-change-expiry"
+ oncommand="changeExpiry()"
+ hidden="true"
+ />
+ </vbox>
+ </html:aside>
+ </html:div>
+
+ <html:div id="key-detail-has-insecure" hidden="hidden">
+ <html:span
+ class="tail-with-learn-more"
+ data-l10n-id="openpgp-key-details-attr-ignored"
+ ></html:span>
+ <label
+ is="text-link"
+ href="https://support.mozilla.org/kb/openpgp-unsafe-key-properties-ignored"
+ data-l10n-id="e2e-learn-more"
+ />
+ </html:div>
+
+ <separator />
+
+ <tabbox flex="1" style="margin: 5px" id="mainTabs">
+ <tabs id="mainTabBox">
+ <tab id="acceptanceTab" data-l10n-id="openpgp-acceptance-label" />
+ <tab
+ id="emailAddressesTab"
+ data-l10n-id="openpgp-key-man-ignored-ids"
+ />
+ <tab
+ id="passphraseTab"
+ data-l10n-id="openpgp-passphrase-protection"
+ />
+ <tab
+ id="signaturesTab"
+ data-l10n-id="openpgp-key-details-signatures-tab"
+ />
+ <tab
+ id="structureTab"
+ data-l10n-id="openpgp-key-details-structure-tab"
+ />
+ </tabs>
+
+ <tabpanels flex="1" id="mainTabPanel">
+ <!-- Acceptance Tab -->
+ <vbox id="acceptancePanel" flex="1">
+ <description id="acceptanceIntro" />
+ <separator class="thin" />
+
+ <html:div>
+ <html:fieldset>
+ <radiogroup
+ id="acceptanceRadio"
+ hidden="true"
+ class="indent"
+ oncommand="onAcceptanceChanged();"
+ >
+ <radio
+ id="acceptRejected"
+ value="rejected"
+ data-l10n-id="openpgp-acceptance-rejected-label"
+ />
+ <radio
+ id="acceptUndecided"
+ value="undecided"
+ data-l10n-id="openpgp-acceptance-undecided-label"
+ />
+ <radio
+ id="acceptUnverified"
+ value="unverified"
+ data-l10n-id="openpgp-acceptance-unverified-label"
+ />
+ <radio
+ id="acceptVerified"
+ value="verified"
+ data-l10n-id="openpgp-acceptance-verified-label"
+ />
+ </radiogroup>
+ <radiogroup id="personalRadio" class="indent" hidden="true">
+ <radio
+ id="notPersonal"
+ value="not_personal"
+ data-l10n-id="openpgp-personal-no-label"
+ />
+ <radio
+ id="yesPersonal"
+ value="personal"
+ data-l10n-id="openpgp-personal-yes-label"
+ />
+ </radiogroup>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin" />
+ <description id="acceptanceVerification" />
+ </vbox>
+
+ <!-- email addresses tab -->
+ <vbox id="emailAddressesPanel" flex="1">
+ <description data-l10n-id="openpgp-ign-addr-intro" />
+ <separator class="thin" />
+
+ <vbox id="addressesListContainer">
+ <vbox id="addressesList" class="indent" />
+ </vbox>
+ </vbox>
+
+ <!-- passphrase tab -->
+ <vbox id="passphrasePanel" flex="1">
+ <description id="passphraseStatus" />
+ <separator class="thin" />
+ <description id="passphraseInstruction" />
+ <separator class="thin" />
+
+ <vbox id="unlockBox">
+ <hbox>
+ <button
+ id="unlock"
+ data-l10n-id="openpgp-passphrase-unlock"
+ oncommand="unlock()"
+ />
+ </hbox>
+ </vbox>
+
+ <vbox id="lockBox">
+ <hbox>
+ <label data-l10n-id="openpgp-passphrase-new" />
+ <html:input
+ id="passwordInput"
+ type="password"
+ class="input-inline"
+ oninput="onPasswordInput();"
+ />
+ </hbox>
+ <hbox>
+ <label data-l10n-id="openpgp-passphrase-new-repeat" />
+ <html:input
+ id="passwordConfirm"
+ type="password"
+ class="input-inline"
+ oninput="onPasswordInput();"
+ />
+ <button
+ id="setPassphrase"
+ disabled="true"
+ oncommand="setPassphrase();"
+ />
+ </hbox>
+ <separator class="thin" />
+ </vbox>
+
+ <hbox>
+ <button
+ id="removeProtection"
+ hidden="true"
+ data-l10n-id="openpgp-remove-protection"
+ oncommand="useAutoPassphrase()"
+ />
+ <button
+ id="usePrimaryPassword"
+ hidden="true"
+ data-l10n-id="openpgp-use-primary-password"
+ oncommand="useAutoPassphrase()"
+ />
+ </hbox>
+ </vbox>
+
+ <!-- certifications tab -->
+ <vbox id="signaturesPanel">
+ <tree
+ id="signatures_tree"
+ flex="1"
+ hidecolumnpicker="true"
+ ondblclick="sigHandleDblClick(event)"
+ >
+ <treecols>
+ <treecol
+ id="sig_uid_col"
+ style="flex: 1 auto"
+ data-l10n-id="openpgp-key-details-uid-certified-col"
+ primary="true"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="sig_keyid_col"
+ data-l10n-id="openpgp-key-id-label"
+ persist="width"
+ minwidth="140"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="sig_created_col"
+ data-l10n-id="openpgp-key-details-created-label"
+ persist="width"
+ minwidth="100"
+ />
+ </treecols>
+
+ <treechildren />
+ </tree>
+ </vbox>
+
+ <!-- structure tab -->
+ <vbox id="structurePanel">
+ <hbox flex="1">
+ <tree
+ id="subkeyList"
+ flex="1"
+ enableColumnDrag="true"
+ hidecolumnpicker="false"
+ >
+ <treecols>
+ <treecol
+ id="keyTypeCol"
+ data-l10n-id="openpgp-key-details-key-part-label"
+ style="width: 71px"
+ persist="width"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="keyUsageCol"
+ data-l10n-id="openpgp-key-details-usage-label"
+ style="flex: 1 auto"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="keyIdCol"
+ style="width: 77px"
+ data-l10n-id="openpgp-key-details-id-label"
+ persist="width"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="algoCol"
+ style="width: 60px"
+ data-l10n-id="openpgp-key-details-algorithm-label"
+ persist="width"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="sizeCol"
+ style="width: 37px"
+ data-l10n-id="openpgp-key-details-size-label"
+ persist="width"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="createdCol"
+ style="width: 70px"
+ data-l10n-id="openpgp-key-details-created-label"
+ persist="width"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="expiryCol"
+ style="width: 70px"
+ data-l10n-id="openpgp-key-details-expiry-label"
+ persist="width"
+ />
+ </treecols>
+
+ <treechildren id="keyListChildren" />
+ </tree>
+ </hbox>
+ <label
+ id="legendMissingSecret"
+ class="tip-caption"
+ data-l10n-id="openpgp-key-details-legend-secret-missing"
+ hidden="true"
+ />
+ </vbox>
+ </tabpanels>
+ </tabbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mail/extensions/openpgp/content/ui/keyWizard.js b/comm/mail/extensions/openpgp/content/ui/keyWizard.js
new file mode 100644
index 0000000000..fe699bcc7e
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/keyWizard.js
@@ -0,0 +1,1195 @@
+/* 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/. */
+
+"use strict";
+
+/* global GetEnigmailSvc */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { EnigmailCryptoAPI } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/cryptoAPI.jsm"
+);
+var { OpenPGPMasterpass } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/masterpass.jsm"
+);
+var { EnigmailDialog } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/dialog.jsm"
+);
+var { EnigmailKey } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/key.jsm"
+);
+var { EnigmailKeyRing } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+);
+var { EnigmailWindows } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/windows.jsm"
+);
+var { PgpSqliteDb2 } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/sqliteDb.jsm"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
+});
+
+// UI variables.
+var gIdentity;
+var gIdentityList;
+var gSubDialog;
+var kStartSection;
+var kDialog;
+var kCurrentSection = "start";
+var kGenerating = false;
+var kButtonLabel;
+
+// OpenPGP variables.
+var gKeygenRequest;
+var gAllData = "";
+var gGeneratedKey = null;
+var gFiles;
+
+const DEFAULT_FILE_PERMS = 0o600;
+
+// The revocation strings are not localization since the revocation certificate
+// will be published to others who may not know the native language of the user.
+const revocationFilePrefix1 =
+ "This is a revocation certificate for the OpenPGP key:";
+const revocationFilePrefix2 = `
+A revocation certificate is kind of a "kill switch" to publicly
+declare that a key shall no longer be used. It is not possible
+to retract such a revocation certificate once it has been published.
+
+Use it to revoke this key in case of a secret key compromise, or loss of
+the secret key, or loss of passphrase of the secret key.
+
+To avoid an accidental use of this file, a colon has been inserted
+before the 5 dashes below. Remove this colon with a text editor
+before importing and publishing this revocation certificate.
+
+:`;
+
+var syncl10n = new Localization(["messenger/openpgp/keyWizard.ftl"], true);
+
+// Dialog event listeners.
+document.addEventListener("dialogaccept", wizardContinue);
+document.addEventListener("dialogextra1", goBack);
+document.addEventListener("dialogcancel", onClose);
+
+/**
+ * Initialize the keyWizard dialog.
+ */
+async function init() {
+ gSubDialog = window.arguments[0].gSubDialog;
+ gIdentity = window.arguments[0].identity || null;
+ gIdentityList = document.getElementById("userIdentity");
+
+ kStartSection = document.getElementById("wizardStart");
+ kDialog = document.querySelector("dialog");
+
+ await initIdentity();
+
+ // Show the GnuPG radio selection if the pref is enabled.
+ if (Services.prefs.getBoolPref("mail.openpgp.allow_external_gnupg")) {
+ document.getElementById("externalOpenPgp").removeAttribute("hidden");
+ }
+
+ // After the dialog is visible, disable the event listeners causing it to
+ // close when clicking on the overlay or hitting the Esc key, and remove the
+ // close button from the header. This is necessary to control the escape
+ // point and prevent the accidental dismiss of the dialog during important
+ // processes, like the generation or importing of a key.
+ setTimeout(() => {
+ // Check if the attribute is not null. This can be removed after the full
+ // conversion of the Key Manager into a SubDialog in Bug 1652537.
+ if (gSubDialog) {
+ gSubDialog._topDialog._removeDialogEventListeners();
+ gSubDialog._topDialog._closeButton.remove();
+ resizeDialog();
+ }
+ }, 150);
+
+ // Switch directly to the create screen if requested by the user.
+ if (window.arguments[0].isCreate) {
+ document.getElementById("openPgpKeyChoices").value = 0;
+
+ switchSection();
+ }
+
+ // Switch directly to the import screen if requested by the user.
+ if (window.arguments[0].isImport) {
+ document.getElementById("openPgpKeyChoices").value = 1;
+
+ // Disable the "Continue" button so the user can't accidentally click on it.
+ // See bug 1689980.
+ kDialog.getButton("accept").setAttribute("disabled", true);
+
+ switchSection();
+ }
+}
+
+function onProtectionChange() {
+ let pw1Element = document.getElementById("passwordInput");
+ let pw2Element = document.getElementById("passwordConfirm");
+
+ let pw1 = pw1Element.value;
+ let pw2 = pw2Element.value;
+
+ let inputDisabled = document.getElementById("keygenAutoProtection").selected;
+ pw1Element.disabled = inputDisabled;
+ pw2Element.disabled = inputDisabled;
+
+ let buttonEnabled = inputDisabled || (!inputDisabled && pw1 == pw2 && pw1);
+ let ok = kDialog.getButton("accept");
+ ok.disabled = !buttonEnabled;
+}
+
+/**
+ * Populate the identity menulist with all the valid and available identities
+ * and autoselect the current identity if available.
+ */
+async function initIdentity() {
+ let identityListPopup = document.getElementById("userIdentityPopup");
+
+ for (let identity of MailServices.accounts.allIdentities) {
+ // Skip invalid and non-email identities.
+ if (!identity.valid || !identity.email) {
+ continue;
+ }
+
+ // Interrupt if no server was defined for this identity.
+ let servers = MailServices.accounts.getServersForIdentity(identity);
+ if (servers.length == 0) {
+ continue;
+ }
+
+ let item = document.createXULElement("menuitem");
+ item.setAttribute(
+ "label",
+ `${identity.identityName} - ${servers[0].prettyName}`
+ );
+ item.setAttribute("class", "identity-popup-item");
+ item.setAttribute("accountname", servers[0].prettyName);
+ item.setAttribute("identitykey", identity.key);
+ item.setAttribute("email", identity.email);
+
+ identityListPopup.appendChild(item);
+
+ if (gIdentity && gIdentity.key == identity.key) {
+ gIdentityList.selectedItem = item;
+ }
+ }
+
+ // If not identity was originally passed during the creation of this dialog,
+ // select the first available value.
+ if (!gIdentity) {
+ gIdentityList.selectedIndex = 0;
+ }
+
+ await setIdentity();
+}
+
+/**
+ * Update the currently used identity to reflect the user selection from the
+ * identity menulist.
+ */
+async function setIdentity() {
+ if (gIdentityList.selectedItem) {
+ gIdentity = MailServices.accounts.getIdentity(
+ gIdentityList.selectedItem.getAttribute("identitykey")
+ );
+
+ document.l10n.setAttributes(
+ document.documentElement,
+ "key-wizard-dialog-window",
+ {
+ identity: gIdentity.email,
+ }
+ );
+ }
+}
+
+/**
+ * Intercept the dialogaccept command to implement a wizard like setup workflow.
+ *
+ * @param {Event} event - The DOM Event.
+ */
+function wizardContinue(event) {
+ event.preventDefault();
+
+ // Pretty impossible scenario but just in case if no radio button is
+ // currently selected, bail out.
+ if (!document.getElementById("openPgpKeyChoices").value) {
+ return;
+ }
+
+ // Trigger an action based on the currently visible section.
+ if (kCurrentSection != "start") {
+ wizardNextStep();
+ return;
+ }
+
+ // Disable the `Continue` button.
+ kDialog.getButton("accept").setAttribute("disabled", true);
+
+ kStartSection.addEventListener("transitionend", switchSection, {
+ once: true,
+ });
+ kStartSection.classList.add("hide");
+}
+
+/**
+ * Separated method dealing with the section switching to allow the removal of
+ * the event listener to prevent stacking.
+ */
+function switchSection() {
+ kStartSection.setAttribute("hidden", true);
+
+ // Save the current label of the accept button in order to restore it later.
+ kButtonLabel = kDialog.getButton("accept").label;
+
+ // Update the UI based on the radiogroup selection.
+ switch (document.getElementById("openPgpKeyChoices").value) {
+ case "0":
+ wizardCreateKey();
+ break;
+
+ case "1":
+ wizardImportKey();
+ break;
+
+ case "2":
+ wizardExternalKey();
+ break;
+ }
+
+ // Show the `Go back` button.
+ kDialog.getButton("extra1").hidden = false;
+ resizeDialog();
+}
+
+/**
+ * Handle the next step of the wizard based on the currently visible section.
+ */
+async function wizardNextStep() {
+ switch (kCurrentSection) {
+ case "create":
+ await openPgpKeygenStart();
+ break;
+
+ case "import":
+ await openPgpImportStart();
+ break;
+
+ case "importComplete":
+ openPgpImportComplete();
+ break;
+
+ case "external":
+ openPgpExternalComplete();
+ break;
+ }
+}
+
+/**
+ * Go back to the initial view of the wizard.
+ */
+function goBack() {
+ let section = document.querySelector(".wizard-section:not([hidden])");
+ section.addEventListener("transitionend", backToStart, { once: true });
+ section.classList.add("hide-reverse");
+}
+
+/**
+ * Hide the currently visible section at the end of the animation, remove the
+ * listener to prevent stacking, and trigger the reveal of the first section.
+ *
+ * @param {Event} event - The DOM Event.
+ */
+function backToStart(event) {
+ // Hide the `Go Back` button.
+ kDialog.getButton("extra1").hidden = true;
+
+ // Enable the `Continue` button.
+ kDialog.getButton("accept").removeAttribute("disabled");
+
+ kDialog.getButton("accept").label = kButtonLabel;
+ kDialog.getButton("accept").classList.remove("primary");
+
+ // Reset the import section.
+ clearImportWarningNotifications();
+ document.getElementById("importKeyIntro").hidden = false;
+ document.getElementById("importKeyListContainer").collapsed = true;
+
+ event.target.setAttribute("hidden", true);
+
+ // Reset section key.
+ kCurrentSection = "start";
+
+ revealSection("wizardStart");
+}
+
+/**
+ * Create a new inline notification to append to the import warning container.
+ *
+ * @returns {XULElement} - The description element inside the notification.
+ */
+async function addImportWarningNotification() {
+ let notification = document.createXULElement("hbox");
+ notification.classList.add(
+ "inline-notification-container",
+ "error-container"
+ );
+
+ let wrapper = document.createXULElement("hbox");
+ wrapper.classList.add("inline-notification-wrapper", "align-center");
+
+ let image = document.createElement("img");
+ image.classList.add("notification-image");
+ image.setAttribute("src", "chrome://global/skin/icons/warning.svg");
+ image.setAttribute("alt", "");
+
+ let description = document.createXULElement("description");
+
+ wrapper.appendChild(image);
+ wrapper.appendChild(description);
+
+ notification.appendChild(wrapper);
+
+ let container = document.getElementById("openPgpImportWarning");
+ container.appendChild(notification);
+
+ // Show the notification container.
+ container.removeAttribute("hidden");
+
+ return description;
+}
+
+/**
+ * Remove all inline errors from the notification area of the import section.
+ */
+function clearImportWarningNotifications() {
+ let container = document.getElementById("openPgpImportWarning");
+
+ // Remove any existing notification.
+ for (let notification of container.querySelectorAll(
+ ".inline-notification-container"
+ )) {
+ notification.remove();
+ }
+
+ // Hide the entire notification container.
+ container.hidden = true;
+}
+
+/**
+ * Show the Key Creation section.
+ */
+async function wizardCreateKey() {
+ kCurrentSection = "create";
+ revealSection("wizardCreateKey");
+
+ kDialog.getButton("accept").label = await document.l10n.formatValue(
+ "openpgp-keygen-button"
+ );
+ kDialog.getButton("accept").classList.add("primary");
+
+ if (!gIdentity.fullName) {
+ document.getElementById("openPgpWarning").collapsed = false;
+ document.l10n.setAttributes(
+ document.getElementById("openPgpWarningDescription"),
+ "openpgp-keygen-long-expiry"
+ );
+ return;
+ }
+
+ let sepPassphraseEnabled = Services.prefs.getBoolPref(
+ "mail.openpgp.passphrases.enabled"
+ );
+ document.getElementById("keygenPassphraseSection").hidden =
+ !sepPassphraseEnabled;
+
+ if (sepPassphraseEnabled) {
+ let usingPP = LoginHelper.isPrimaryPasswordSet();
+ let autoProt = document.getElementById("keygenAutoProtection");
+
+ document.l10n.setAttributes(
+ autoProt,
+ usingPP
+ ? "radio-keygen-protect-primary-pass"
+ : "radio-keygen-no-protection"
+ );
+
+ autoProt.setAttribute("selected", true);
+ document
+ .getElementById("keygenPassphraseProtection")
+ .removeAttribute("selected");
+ }
+
+ // This also handles enable/disabling the accept/ok button.
+ onProtectionChange();
+}
+
+/**
+ * Show the Key Import section.
+ */
+function wizardImportKey() {
+ kCurrentSection = "import";
+ revealSection("wizardImportKey");
+
+ let sepPassphraseEnabled = Services.prefs.getBoolPref(
+ "mail.openpgp.passphrases.enabled"
+ );
+ let keepPassphrasesItem = document.getElementById(
+ "openPgpKeygenKeepPassphrases"
+ );
+ keepPassphrasesItem.hidden = !sepPassphraseEnabled;
+ keepPassphrasesItem.checked = false;
+}
+
+/**
+ * Show the Key Setup via external smartcard section.
+ */
+async function wizardExternalKey() {
+ kCurrentSection = "external";
+ revealSection("wizardExternalKey");
+
+ kDialog.getButton("accept").label = await document.l10n.formatValue(
+ "openpgp-save-external-button"
+ );
+ kDialog.getButton("accept").classList.add("primary");
+
+ // If the user is already using an external GnuPG key, populate the input,
+ // show the warning description, and enable the primary button.
+ if (gIdentity.getBoolAttribute("is_gnupg_key_id")) {
+ document.getElementById("externalKey").value =
+ gIdentity.getUnicharAttribute("last_entered_external_gnupg_key_id");
+ document.getElementById("openPgpExternalWarning").collapsed = false;
+ kDialog.getButton("accept").removeAttribute("disabled");
+ } else {
+ document.getElementById("openPgpExternalWarning").collapsed = true;
+ kDialog.getButton("accept").setAttribute("disabled", true);
+ }
+}
+
+/**
+ * Animate the reveal of a section of the wizard.
+ *
+ * @param {string} id - The id of the section to reveal.
+ */
+function revealSection(id) {
+ let section = document.getElementById(id);
+ section.removeAttribute("hidden");
+
+ // Timeout to animate after the hidden attribute has been removed.
+ setTimeout(() => {
+ section.classList.remove("hide", "hide-reverse");
+ });
+
+ resizeDialog();
+}
+
+/**
+ * Enable or disable the elements based on the radiogroup selection.
+ *
+ * @param {Event} event - The DOM event triggered on change.
+ */
+function onExpirationChange(event) {
+ document
+ .getElementById("expireInput")
+ .toggleAttribute("disabled", event.target.value != 0);
+ document.getElementById("timeScale").disabled = event.target.value != 0;
+
+ validateExpiration();
+}
+
+/**
+ * Enable or disable the #keySize input field based on the current selection of
+ * the #keyType radio group.
+ *
+ * @param {Event} event - The DOM Event.
+ */
+function onKeyTypeChange(event) {
+ document.getElementById("keySize").disabled = event.target.value == "ECC";
+}
+
+/**
+ * Intercept the cancel event to prevent accidental closing if the generation of
+ * a key is currently in progress.
+ *
+ * @param {Event} event - The DOM event.
+ */
+function onClose(event) {
+ if (kGenerating) {
+ event.preventDefault();
+ }
+
+ window.arguments[0].cancelCallback();
+}
+
+/**
+ * Validate the expiration time of a newly generated key when the user changes
+ * values. Disable the "Generate Key" button and show an alert if the selected
+ * value is less than 1 day or more than 100 years.
+ */
+async function validateExpiration() {
+ // If the key doesn't have an expiration date, hide the warning message and
+ // enable the "Generate Key" button.
+ if (document.getElementById("openPgpKeygeExpiry").value == 1) {
+ document.getElementById("openPgpWarning").collapsed = true;
+ kDialog.getButton("accept").removeAttribute("disabled");
+ return;
+ }
+
+ // Calculate the selected expiration date.
+ let expiryTime =
+ Number(document.getElementById("expireInput").value) *
+ Number(document.getElementById("timeScale").value);
+
+ // If the expiration date exceeds 100 years.
+ if (expiryTime > 36500) {
+ document.getElementById("openPgpWarning").collapsed = false;
+ document.l10n.setAttributes(
+ document.getElementById("openPgpWarningDescription"),
+ "openpgp-keygen-long-expiry"
+ );
+ kDialog.getButton("accept").setAttribute("disabled", true);
+ resizeDialog();
+ return;
+ }
+
+ // If the expiration date is shorter than 1 day.
+ if (expiryTime <= 0) {
+ document.getElementById("openPgpWarning").collapsed = false;
+ document.l10n.setAttributes(
+ document.getElementById("openPgpWarningDescription"),
+ "openpgp-keygen-short-expiry"
+ );
+ kDialog.getButton("accept").setAttribute("disabled", true);
+ resizeDialog();
+ return;
+ }
+
+ // If the previous conditions are false, hide the warning message and
+ // enable the "Generate Key" button since the expiration date is valid.
+ document.getElementById("openPgpWarning").collapsed = true;
+ kDialog.getButton("accept").removeAttribute("disabled");
+}
+
+/**
+ * Resize the dialog to account for the newly visible sections.
+ */
+function resizeDialog() {
+ // Check if the attribute is not null. This can be removed after the full
+ // conversion of the Key Manager into a SubDialog in Bug 1652537.
+ if (gSubDialog && gSubDialog._topDialog) {
+ gSubDialog._topDialog.resizeVertically();
+ } else {
+ window.sizeToContent();
+ }
+}
+
+/**
+ * Start the generation of a new OpenPGP Key.
+ */
+async function openPgpKeygenStart() {
+ let openPgpWarning = document.getElementById("openPgpWarning");
+ let openPgpWarningText = document.getElementById("openPgpWarningDescription");
+ openPgpWarning.collapsed = true;
+
+ // If a key generation request is already pending, warn the user and
+ // don't proceed.
+ if (gKeygenRequest) {
+ let req = gKeygenRequest.QueryInterface(Ci.nsIRequest);
+
+ if (req.isPending()) {
+ openPgpWarning.collapsed = false;
+ document.l10n.setAttributes(openPgpWarningText, "openpgp-keygen-ongoing");
+ return;
+ }
+ }
+
+ // Reset global variables to be sure.
+ gGeneratedKey = null;
+ gAllData = "";
+
+ let enigmailSvc = GetEnigmailSvc();
+ if (!enigmailSvc) {
+ openPgpWarning.collapsed = false;
+ document.l10n.setAttributes(
+ openPgpWarningText,
+ "openpgp-keygen-error-core"
+ );
+ closeOverlay();
+
+ throw new Error("GetEnigmailSvc failed");
+ }
+
+ // Show wizard overlay before the start of the generation process. This is
+ // necessary because the generation happens synchronously and blocks the UI.
+ // We need to show the overlay before it, otherwise it would flash and freeze.
+ // This should be moved after the Services.prompt.confirmEx() method
+ // once Bug 1617444 is implemented.
+ let overlay = document.getElementById("wizardOverlay");
+ overlay.removeAttribute("hidden");
+ overlay.classList.remove("hide");
+
+ // Ask for confirmation before triggering the generation of a new key.
+ document.l10n.setAttributes(
+ document.getElementById("wizardOverlayQuestion"),
+ "openpgp-key-confirm",
+ {
+ identity: `${gIdentity.fullName} <b>"${gIdentity.email}"</b>`,
+ }
+ );
+
+ document.l10n.setAttributes(
+ document.getElementById("wizardOverlayTitle"),
+ "openpgp-keygen-progress-title"
+ );
+}
+
+async function openPgpKeygenConfirm() {
+ document.getElementById("openPgpKeygenConfirm").collapsed = true;
+ document.getElementById("openPgpKeygenProcess").removeAttribute("collapsed");
+
+ let openPgpWarning = document.getElementById("openPgpWarning");
+ let openPgpWarningText = document.getElementById("openPgpWarningDescription");
+ openPgpWarning.collapsed = true;
+
+ kGenerating = true;
+
+ let password;
+ let cApi = EnigmailCryptoAPI();
+ let newId = null;
+
+ let sepPassphraseEnabled = Services.prefs.getBoolPref(
+ "mail.openpgp.passphrases.enabled"
+ );
+
+ if (
+ !sepPassphraseEnabled ||
+ document.getElementById("keygenAutoProtection").selected
+ ) {
+ password = await OpenPGPMasterpass.retrieveOpenPGPPassword();
+ } else {
+ password = document.getElementById("passwordInput").value;
+ }
+ newId = await cApi.genKey(
+ `${gIdentity.fullName} <${gIdentity.email}>`,
+ document.getElementById("keyType").value,
+ Number(document.getElementById("keySize").value),
+ document.getElementById("openPgpKeygeExpiry").value == 1
+ ? 0
+ : Number(document.getElementById("expireInput").value) *
+ Number(document.getElementById("timeScale").value),
+ password
+ );
+
+ gGeneratedKey = newId;
+
+ EnigmailWindows.keyManReloadKeys();
+
+ gKeygenRequest = null;
+ kGenerating = false;
+
+ // For wathever reason, the key wasn't generated. Show an error message and
+ // hide the processing overlay.
+ if (!gGeneratedKey) {
+ openPgpWarning.collapsed = false;
+ document.l10n.setAttributes(
+ openPgpWarningText,
+ "openpgp-keygen-error-failed"
+ );
+ closeOverlay();
+
+ throw new Error("key generation failed");
+ }
+
+ console.debug("saving new key id " + gGeneratedKey);
+ Services.prefs.savePrefFile(null);
+
+ // Hide wizard overlay at the end of the generation process.
+ closeOverlay();
+ EnigmailKeyRing.clearCache();
+
+ let rev = await cApi.unlockAndGetNewRevocation(
+ `0x${gGeneratedKey}`,
+ password
+ );
+ if (!rev) {
+ openPgpWarning.collapsed = false;
+ document.l10n.setAttributes(
+ openPgpWarningText,
+ "openpgp-keygen-error-revocation",
+ {
+ key: gGeneratedKey,
+ }
+ );
+ closeOverlay();
+
+ throw new Error("failed to obtain revocation for key " + gGeneratedKey);
+ }
+
+ let revFull =
+ revocationFilePrefix1 +
+ "\n\n" +
+ gGeneratedKey +
+ "\n" +
+ revocationFilePrefix2 +
+ rev;
+
+ let revFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ revFile.append(`0x${gGeneratedKey}_rev.asc`);
+
+ // Create a revokation cert in the Thunderbird profile directory.
+ await IOUtils.writeUTF8(revFile.path, revFull);
+
+ // Key successfully created. Close the dialog and show a confirmation message.
+ // Assigning the key to an identity is the responsibility of the caller,
+ // so we pass back what we created.
+ window.arguments[0].okCallback(gGeneratedKey);
+ window.close();
+}
+
+/**
+ * Cancel the keygen process, ask for confirmation before proceeding.
+ */
+async function openPgpKeygenCancel() {
+ let [abortTitle, abortText] = await document.l10n.formatValues([
+ { id: "openpgp-keygen-abort-title" },
+ { id: "openpgp-keygen-abort" },
+ ]);
+
+ if (
+ kGenerating &&
+ Services.prompt.confirmEx(
+ window,
+ abortTitle,
+ abortText,
+ Services.prompt.STD_YES_NO_BUTTONS,
+ "",
+ "",
+ "",
+ "",
+ {}
+ ) != 0
+ ) {
+ return;
+ }
+
+ closeOverlay();
+ gKeygenRequest.kill(false);
+ kGenerating = false;
+}
+
+/**
+ * Close the processing wizard overlay.
+ */
+function closeOverlay() {
+ document.getElementById("openPgpKeygenConfirm").removeAttribute("collapsed");
+ document.getElementById("openPgpKeygenProcess").collapsed = true;
+
+ let overlay = document.getElementById("wizardOverlay");
+
+ overlay.addEventListener("transitionend", hideOverlay, { once: true });
+ overlay.classList.add("hide");
+}
+
+/**
+ * Add the "hidden" attribute tot he processing wizard overlay after the CSS
+ * transition ended.
+ *
+ * @param {Event} event - The DOM Event.
+ */
+function hideOverlay(event) {
+ event.target.setAttribute("hidden", true);
+ resizeDialog();
+}
+
+async function importSecretKey() {
+ let [importTitle, importType] = await document.l10n.formatValues([
+ { id: "import-key-file" },
+ { id: "gnupg-file" },
+ ]);
+
+ // Reset the array of selected files.
+ gFiles = [];
+
+ let files = EnigmailDialog.filePicker(
+ window,
+ importTitle,
+ "",
+ false,
+ true,
+ "*.asc",
+ "",
+ [importType, "*.asc;*.gpg;*.pgp"]
+ );
+
+ if (!files.length) {
+ return;
+ }
+
+ // Clear and hide the warning notification section.
+ clearImportWarningNotifications();
+
+ // Clear the key list from any previously listed key.
+ let keyList = document.getElementById("importKeyList");
+ while (keyList.lastChild) {
+ keyList.lastChild.remove();
+ }
+
+ let keyCount = 0;
+ for (let file of files) {
+ // Skip the file and show a warning message if larger than 5MB.
+ if (file.fileSize > 5000000) {
+ document.l10n.setAttributes(
+ await addImportWarningNotification(),
+ "import-error-file-size"
+ );
+ continue;
+ }
+
+ let errorMsgObj = {};
+ // Fetch the list of all the available keys inside the selected file.
+ let importKeys = await EnigmailKey.getKeyListFromKeyFile(
+ file,
+ errorMsgObj,
+ false,
+ true
+ );
+
+ // Skip the file and show a warning message if the import failed.
+ if (!importKeys || !importKeys.length || errorMsgObj.value) {
+ document.l10n.setAttributes(
+ await addImportWarningNotification(),
+ "import-error-failed",
+ {
+ error: errorMsgObj.value,
+ }
+ );
+ continue;
+ }
+
+ await appendFetchedKeys(importKeys);
+ keyCount += importKeys.length;
+
+ // Add the current file to the list of valid files to import.
+ gFiles.push(file);
+ }
+
+ // Update the list count recap and show the container.
+ document.l10n.setAttributes(
+ document.getElementById("keyListCount"),
+ "openpgp-import-key-list-amount-2",
+ {
+ count: keyCount,
+ }
+ );
+
+ document.getElementById("importKeyListContainer").collapsed = !keyCount;
+
+ // Hide the intro section and enable the import of keys only if we have valid
+ // keys currently listed.
+ if (keyCount) {
+ document.getElementById("importKeyIntro").hidden = true;
+ kDialog.getButton("accept").removeAttribute("disabled");
+ kDialog.getButton("accept").classList.add("primary");
+ }
+
+ resizeDialog();
+}
+
+/**
+ * Populate the key list in the import dialog with all the valid keys fetched
+ * from a single file.
+ *
+ * @param {string[]} importKeys - The array of keys fetched from a single file.
+ */
+async function appendFetchedKeys(importKeys) {
+ let keyList = document.getElementById("importKeyList");
+
+ // List all the keys fetched from the file.
+ for (let key of importKeys) {
+ let container = document.createXULElement("hbox");
+ container.classList.add("key-import-row", "selected");
+
+ let titleContainer = document.createXULElement("vbox");
+
+ let id = document.createXULElement("label");
+ id.classList.add("openpgp-key-id");
+ id.value = `0x${key.id}`;
+
+ let name = document.createXULElement("label");
+ name.classList.add("openpgp-key-name");
+ name.value = key.name;
+
+ titleContainer.appendChild(id);
+ titleContainer.appendChild(name);
+
+ // Allow users to treat imported keys as "Personal".
+ let checkbox = document.createXULElement("checkbox");
+ checkbox.setAttribute("id", `${key.id}-set-personal`);
+ document.l10n.setAttributes(checkbox, "import-key-personal-checkbox");
+ checkbox.checked = true;
+
+ container.appendChild(titleContainer);
+ container.appendChild(checkbox);
+
+ keyList.appendChild(container);
+ }
+}
+
+async function openPgpImportStart() {
+ if (!gFiles.length) {
+ return;
+ }
+
+ kGenerating = true;
+
+ // Show the overlay.
+ let overlay = document.getElementById("wizardImportOverlay");
+ overlay.removeAttribute("hidden");
+ overlay.classList.remove("hide");
+
+ // Clear and hide the warning notification section.
+ clearImportWarningNotifications();
+
+ // Clear the list of any previously improted keys from the DOM.
+ let keyList = document.getElementById("importKeyListRecap");
+ while (keyList.lastChild) {
+ keyList.lastChild.remove();
+ }
+
+ let keyCount = 0;
+ for (let file of gFiles) {
+ let resultKeys = {};
+ let errorMsgObj = {};
+
+ // keepPassphrases false is the classic behavior.
+ let keepPassphrases = false;
+
+ // If the pref is on, we allow the user to decide what to do.
+ let allowSeparatePassphrases = Services.prefs.getBoolPref(
+ "mail.openpgp.passphrases.enabled"
+ );
+ if (allowSeparatePassphrases) {
+ keepPassphrases = document.getElementById(
+ "openPgpKeygenKeepPassphrases"
+ ).checked;
+ }
+
+ let exitCode = await EnigmailKeyRing.importSecKeyFromFile(
+ window,
+ passphrasePromptCallback,
+ keepPassphrases,
+ file,
+ errorMsgObj,
+ resultKeys
+ );
+
+ // Skip this file if something went wrong.
+ if (exitCode !== 0) {
+ document.l10n.setAttributes(
+ await addImportWarningNotification(),
+ "openpgp-import-keys-failed",
+ {
+ error: errorMsgObj.value,
+ }
+ );
+ continue;
+ }
+
+ await appendImportedKeys(resultKeys);
+ keyCount += resultKeys.keys.length;
+ }
+
+ // Hide the previous key list container and title.
+ document.getElementById("importKeyListContainer").collapsed = keyCount;
+ document.getElementById("importKeyTitle").hidden = keyCount;
+
+ // Show the successful final screen only if at least one key was imported.
+ if (keyCount) {
+ // Update the dialog buttons for the final stage.
+ kDialog.getButton("extra1").hidden = true;
+ kDialog.getButton("cancel").hidden = true;
+
+ // Update the `Continue` button.
+ document.l10n.setAttributes(
+ kDialog.getButton("accept"),
+ "openpgp-keygen-import-complete"
+ );
+ kCurrentSection = "importComplete";
+
+ // Show the recently built key list.
+ document.getElementById("importKeyListSuccess").collapsed = false;
+ }
+
+ // Hide the loading overlay.
+ overlay.addEventListener("transitionend", hideOverlay, { once: true });
+ overlay.classList.add("hide");
+
+ resizeDialog();
+ kGenerating = false;
+}
+
+/**
+ * Populate the key list in the import dialog with all the valid keys imported
+ * from a single file.
+ *
+ * @param {string[]} resultKeys - The array of keys imported from a single file.
+ */
+async function appendImportedKeys(resultKeys) {
+ let keyList = document.getElementById("importKeyListRecap");
+
+ for (let keyId of resultKeys.keys) {
+ if (keyId.search(/^0x/) === 0) {
+ keyId = keyId.substr(2).toUpperCase();
+ }
+
+ let key = EnigmailKeyRing.getKeyById(keyId);
+
+ if (key && key.fpr) {
+ // If the checkbox was checked, update the acceptance of the key.
+ if (document.getElementById(`${key.keyId}-set-personal`).checked) {
+ PgpSqliteDb2.acceptAsPersonalKey(key.fpr);
+ }
+
+ let container = document.createXULElement("hbox");
+ container.classList.add("key-import-row");
+
+ // Start key info section.
+ let grid = document.createXULElement("hbox");
+ grid.classList.add("extra-information-label");
+
+ // Key identity.
+ let identityLabel = document.createXULElement("label");
+ identityLabel.classList.add("extra-information-label-type");
+ document.l10n.setAttributes(
+ identityLabel,
+ "openpgp-import-identity-label"
+ );
+
+ let identityValue = document.createXULElement("label");
+ identityValue.value = key.userId;
+
+ grid.appendChild(identityLabel);
+ grid.appendChild(identityValue);
+
+ // Key fingerprint.
+ let fingerprintLabel = document.createXULElement("label");
+ document.l10n.setAttributes(
+ fingerprintLabel,
+ "openpgp-import-fingerprint-label"
+ );
+ fingerprintLabel.classList.add("extra-information-label-type");
+
+ let fingerprintInput = document.createXULElement("label");
+ fingerprintInput.value = EnigmailKey.formatFpr(key.fpr);
+
+ grid.appendChild(fingerprintLabel);
+ grid.appendChild(fingerprintInput);
+
+ // Key creation date.
+ let createdLabel = document.createXULElement("label");
+ document.l10n.setAttributes(createdLabel, "openpgp-import-created-label");
+ createdLabel.classList.add("extra-information-label-type");
+
+ let createdValue = document.createXULElement("label");
+ createdValue.value = key.created;
+
+ grid.appendChild(createdLabel);
+ grid.appendChild(createdValue);
+
+ // Key bits.
+ let bitsLabel = document.createXULElement("label");
+ bitsLabel.classList.add("extra-information-label-type");
+ document.l10n.setAttributes(bitsLabel, "openpgp-import-bits-label");
+
+ let bitsValue = document.createXULElement("label");
+ bitsValue.value = key.keySize;
+
+ grid.appendChild(bitsLabel);
+ grid.appendChild(bitsValue);
+ // End key info section.
+
+ let info = document.createXULElement("button");
+ info.classList.add("openpgp-image-btn", "openpgp-props-btn");
+ document.l10n.setAttributes(info, "openpgp-import-key-props");
+ info.addEventListener("command", () => {
+ window.arguments[0].keyDetailsDialog(key.keyId);
+ });
+
+ container.appendChild(grid);
+ container.appendChild(info);
+
+ keyList.appendChild(container);
+ }
+ }
+}
+
+function openPgpImportComplete() {
+ window.arguments[0].okImportCallback();
+ window.close();
+}
+
+/**
+ * Opens a prompt asking the user to enter the passphrase for a given key id.
+ *
+ * @param {object} win - The current window.
+ * @param {string} keyId - The ID of the imported key.
+ * @param {object} resultFlags - Keep track of the cancelled action.
+ *
+ * @returns {string} - The entered passphrase or empty.
+ */
+function passphrasePromptCallback(win, promptString, resultFlags) {
+ let passphrase = { value: "" };
+
+ // We need to fetch these strings synchronously in order to properly work with
+ // the RNP key import method, which is not async.
+ let title = syncl10n.formatValueSync("openpgp-passphrase-prompt-title");
+
+ let prompt = Services.prompt.promptPassword(
+ win,
+ title,
+ promptString,
+ passphrase,
+ null,
+ {}
+ );
+
+ if (!prompt) {
+ let overlay = document.getElementById("wizardImportOverlay");
+ overlay.addEventListener("transitionend", hideOverlay, { once: true });
+ overlay.classList.add("hide");
+ kGenerating = false;
+ }
+
+ resultFlags.canceled = !prompt;
+ return !prompt ? "" : passphrase.value;
+}
+
+function toggleSaveButton(event) {
+ kDialog
+ .getButton("accept")
+ .toggleAttribute("disabled", !event.target.value.trim());
+}
+
+/**
+ * Save the GnuPG Key for the current identity and trigger a callback.
+ */
+function openPgpExternalComplete() {
+ gIdentity.setBoolAttribute("is_gnupg_key_id", true);
+
+ let externalKey = document.getElementById("externalKey").value;
+ gIdentity.setUnicharAttribute("openpgp_key_id", externalKey);
+
+ window.arguments[0].okExternalCallback(externalKey);
+ window.close();
+}
diff --git a/comm/mail/extensions/openpgp/content/ui/keyWizard.xhtml b/comm/mail/extensions/openpgp/content/ui/keyWizard.xhtml
new file mode 100644
index 0000000000..c630eacb48
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/keyWizard.xhtml
@@ -0,0 +1,506 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/keyWizard.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/inlineNotification.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window>
+
+<window
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="init();"
+ lightweightthemes="true"
+ style="min-width: 50em"
+>
+ <dialog
+ id="openPgpKeyWizardDialog"
+ data-l10n-id="key-wizard-dialog"
+ data-l10n-attrs="buttonlabelaccept,buttonlabelextra1"
+ buttons="accept,cancel"
+ >
+ <script src="chrome://openpgp/content/ui/enigmailCommon.js" />
+ <script src="chrome://openpgp/content/ui/keyWizard.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link rel="localization" href="messenger/openpgp/keyWizard.ftl" />
+ </linkset>
+
+ <html:div
+ id="wizardOverlay"
+ class="wizard-section overlay hide"
+ hidden="hidden"
+ >
+ <hbox class="inline-notification-container info-container">
+ <hbox class="inline-notification-wrapper">
+ <html:img
+ class="notification-image"
+ src="chrome://messenger/skin/icons/information.svg"
+ alt=""
+ />
+ <description data-l10n-id="openpgp-generate-key-info" />
+ </hbox>
+ </hbox>
+
+ <vbox id="openPgpKeygenConfirm" class="self-center" align="center">
+ <description id="wizardOverlayQuestion" />
+ <separator class="thin" />
+ <hbox>
+ <button
+ data-l10n-id="openpgp-keygen-dismiss"
+ oncommand="closeOverlay();"
+ />
+ <button
+ id="openPgpKeygenConfirmButton"
+ data-l10n-id="openpgp-keygen-confirm"
+ oncommand="openPgpKeygenConfirm();"
+ />
+ </hbox>
+ </vbox>
+
+ <vbox
+ id="openPgpKeygenProcess"
+ class="self-center"
+ align="center"
+ collapsed="true"
+ >
+ <html:legend id="wizardOverlayTitle"></html:legend>
+ <html:img
+ class="loading-status"
+ src="chrome://global/skin/icons/loading.png"
+ alt=""
+ />
+ <button
+ data-l10n-id="openpgp-keygen-cancel"
+ class="self-center"
+ oncommand="openPgpKeygenCancel();"
+ />
+ </vbox>
+ </html:div>
+
+ <html:div
+ id="wizardImportOverlay"
+ class="wizard-section overlay hide"
+ hidden="hidden"
+ >
+ <vbox id="importLoading" class="self-center" align="center">
+ <html:legend
+ data-l10n-id="openpgp-keygen-import-progress-title"
+ ></html:legend>
+ <html:img
+ class="loading-status"
+ src="chrome://global/skin/icons/loading.png"
+ alt=""
+ />
+ </vbox>
+ </html:div>
+
+ <vbox id="wizardStart" class="wizard-section">
+ <hbox class="inline-notification-container info-container">
+ <hbox class="inline-notification-wrapper">
+ <html:img
+ class="notification-image"
+ src="chrome://messenger/skin/icons/information.svg"
+ alt=""
+ />
+ <description>
+ <html:span
+ class="tail-with-learn-more"
+ data-l10n-id="key-wizard-warning"
+ >
+ </html:span>
+ <label
+ is="text-link"
+ href="https://support.mozilla.org/kb/introduction-to-e2e-encryption"
+ data-l10n-id="key-wizard-learn-more"
+ class="learnMore text-link"
+ />
+ </description>
+ </hbox>
+ </hbox>
+
+ <html:div>
+ <html:fieldset>
+ <radiogroup id="openPgpKeyChoices" class="indent">
+ <radio
+ id="createOpenPgp"
+ value="0"
+ data-l10n-id="radio-create-key"
+ />
+ <radio
+ id="importOpenPgp"
+ value="1"
+ data-l10n-id="radio-import-key"
+ />
+ <radio
+ id="externalOpenPgp"
+ value="2"
+ data-l10n-id="radio-gnupg-key"
+ hidden="true"
+ />
+ </radiogroup>
+ </html:fieldset>
+ </html:div>
+ </vbox>
+
+ <vbox
+ id="wizardCreateKey"
+ class="wizard-section hide-reverse"
+ hidden="true"
+ >
+ <label
+ data-l10n-id="openpgp-generate-key-title"
+ class="dialogheader-title"
+ />
+
+ <html:div>
+ <html:fieldset>
+ <hbox align="center">
+ <html:legend
+ class="identity-legend"
+ data-l10n-id="openpgp-import-identity-label"
+ >
+ </html:legend>
+ <menulist id="userIdentity" flex="1" oncommand="setIdentity();">
+ <menupopup id="userIdentityPopup" />
+ </menulist>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <html:div id="keygenPassphraseSection">
+ <html:fieldset>
+ <html:legend
+ data-l10n-id="openpgp-keygen-secret-protection"
+ ></html:legend>
+
+ <radiogroup id="openPgpKeyProtection" class="indent">
+ <radio
+ id="keygenAutoProtection"
+ value="0"
+ oncommand="onProtectionChange();"
+ />
+ <vbox>
+ <hbox>
+ <radio
+ id="keygenPassphraseProtection"
+ value="1"
+ data-l10n-id="radio-keygen-passphrase-protection"
+ oncommand="onProtectionChange();"
+ />
+ <html:input
+ id="passwordInput"
+ type="password"
+ oninput="onProtectionChange();"
+ />
+ </hbox>
+ <hbox class="indent">
+ <label data-l10n-id="openpgp-passphrase-repeat" />
+ <html:input
+ id="passwordConfirm"
+ type="password"
+ class="input-inline"
+ oninput="onProtectionChange();"
+ />
+ </hbox>
+ </vbox>
+ </radiogroup>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin" />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend data-l10n-id="openpgp-keygen-expiry-title"></html:legend>
+ <description data-l10n-id="openpgp-keygen-expiry-description" />
+
+ <radiogroup id="openPgpKeygeExpiry" class="indent">
+ <hbox flex="1" align="center">
+ <radio
+ id="keygenExpiration"
+ value="0"
+ data-l10n-id="radio-keygen-expiry"
+ oncommand="onExpirationChange(event);"
+ />
+ <html:input
+ id="expireInput"
+ type="number"
+ class="size4 input-inline autosync"
+ maxlength="5"
+ value="3"
+ min="1"
+ max="100"
+ aria-labelledby="keygenExpiration"
+ oninput="validateExpiration();"
+ />
+ <menulist id="timeScale">
+ <menupopup>
+ <menuitem
+ id="years"
+ value="365"
+ data-l10n-id="openpgp-keygen-years-label"
+ selected="true"
+ oncommand="validateExpiration();"
+ />
+ <menuitem
+ id="months"
+ value="30"
+ data-l10n-id="openpgp-keygen-months-label"
+ oncommand="validateExpiration();"
+ />
+ <menuitem
+ id="days"
+ value="1"
+ data-l10n-id="openpgp-keygen-days-label"
+ oncommand="validateExpiration();"
+ />
+ </menupopup>
+ </menulist>
+ </hbox>
+ <radio
+ id="keygenDoesNotExpire"
+ value="1"
+ data-l10n-id="radio-keygen-no-expiry"
+ oncommand="onExpirationChange(event);"
+ />
+ </radiogroup>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin" />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend
+ data-l10n-id="openpgp-keygen-advanced-title"
+ ></html:legend>
+ <description data-l10n-id="openpgp-keygen-advanced-description" />
+
+ <vbox class="indent grid-size">
+ <hbox align="center">
+ <label for="keyType" data-l10n-id="openpgp-keygen-keytype" />
+ </hbox>
+ <hbox align="center">
+ <menulist id="keyType">
+ <menupopup>
+ <menuitem
+ id="keyType_rsa"
+ value="RSA"
+ data-l10n-id="openpgp-keygen-type-rsa"
+ selected="true"
+ oncommand="onKeyTypeChange(event);"
+ />
+ <menuitem
+ id="keyType_ecc"
+ value="ECC"
+ data-l10n-id="openpgp-keygen-type-ecc"
+ oncommand="onKeyTypeChange(event);"
+ />
+ </menupopup>
+ </menulist>
+ </hbox>
+ <spacer />
+
+ <hbox align="center">
+ <label for="keySize" data-l10n-id="openpgp-keygen-keysize" />
+ </hbox>
+ <hbox align="center">
+ <menulist id="keySize">
+ <menupopup>
+ <menuitem
+ id="keySize_3072"
+ value="3072"
+ label="3072"
+ selected="true"
+ />
+ <menuitem id="keySize_4096" value="4096" label="4096" />
+ </menupopup>
+ </menulist>
+ </hbox>
+ <spacer />
+ </vbox>
+ </html:fieldset>
+ </html:div>
+
+ <separator />
+
+ <hbox
+ id="openPgpWarning"
+ class="inline-notification-container error-container"
+ collapsed="true"
+ >
+ <hbox class="inline-notification-wrapper">
+ <html:img
+ class="notification-image"
+ src="chrome://global/skin/icons/warning.svg"
+ alt=""
+ />
+ <description id="openPgpWarningDescription" />
+ </hbox>
+ </hbox>
+ </vbox>
+
+ <vbox
+ id="wizardImportKey"
+ class="wizard-section hide-reverse"
+ hidden="true"
+ >
+ <label
+ id="importKeyTitle"
+ data-l10n-id="openpgp-import-key-title"
+ class="dialogheader-title"
+ />
+
+ <vbox id="openPgpImportWarning" hidden="true" />
+
+ <vbox id="importKeyIntro" align="start">
+ <html:div>
+ <html:fieldset>
+ <html:legend data-l10n-id="openpgp-import-key-legend"></html:legend>
+ <description data-l10n-id="openpgp-import-key-description" />
+ <description
+ data-l10n-id="openpgp-import-key-info"
+ class="tip-caption"
+ />
+
+ <separator />
+
+ <button
+ data-l10n-id="openpgp-import-key-button"
+ oncommand="importSecretKey();"
+ />
+
+ <separator class="thin" />
+ </html:fieldset>
+ </html:div>
+ </vbox>
+
+ <vbox id="importKeyListContainer" collapsed="true">
+ <hbox class="inline-notification-container success-container">
+ <hbox class="inline-notification-wrapper align-center">
+ <html:img
+ class="notification-image"
+ src="chrome://global/skin/icons/check.svg"
+ alt=""
+ />
+ <description id="keyListCount" />
+ </hbox>
+ </hbox>
+
+ <description data-l10n-id="openpgp-import-key-list-description" />
+
+ <vbox id="importKeyList" />
+
+ <description
+ data-l10n-id="openpgp-import-key-list-caption"
+ class="tip-caption"
+ />
+
+ <separator class="thin" />
+ <checkbox
+ id="openPgpKeygenKeepPassphrases"
+ data-l10n-id="openpgp-import-keep-passphrases"
+ />
+
+ <separator />
+ </vbox>
+
+ <vbox id="importKeyListSuccess" collapsed="true">
+ <hbox class="inline-notification-container success-container">
+ <hbox class="inline-notification-wrapper align-center">
+ <html:img
+ class="notification-image"
+ src="chrome://global/skin/icons/check.svg"
+ alt=""
+ />
+ <description data-l10n-id="openpgp-import-success" />
+ </hbox>
+ </hbox>
+
+ <separator />
+
+ <vbox id="importKeyListRecap" />
+
+ <vbox align="center">
+ <html:legend
+ data-l10n-id="openpgp-import-success-title"
+ ></html:legend>
+ <description
+ data-l10n-id="openpgp-import-success-description"
+ class="description-centered"
+ />
+ </vbox>
+
+ <separator class="thin" />
+ </vbox>
+ </vbox>
+
+ <vbox
+ id="wizardExternalKey"
+ class="wizard-section hide-reverse"
+ hidden="true"
+ >
+ <label
+ data-l10n-id="openpgp-external-key-title"
+ class="dialogheader-title"
+ />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend data-l10n-id="openpgp-external-key-description">
+ </html:legend>
+
+ <description data-l10n-id="openpgp-external-key-info" />
+
+ <separator />
+
+ <hbox align="center">
+ <label
+ for="externalKey"
+ data-l10n-id="openpgp-external-key-label"
+ ></label>
+ <hbox class="input-container" flex="1">
+ <html:input
+ id="externalKey"
+ type="text"
+ class="input-inline"
+ data-l10n-id="openpgp-external-key-input"
+ oninput="toggleSaveButton(event);"
+ />
+ </hbox>
+ </hbox>
+
+ <separator />
+
+ <hbox
+ id="openPgpExternalWarning"
+ class="inline-notification-container info-container"
+ collapsed="true"
+ >
+ <hbox class="inline-notification-wrapper">
+ <html:img
+ class="notification-image"
+ src="chrome://messenger/skin/icons/information.svg"
+ alt=""
+ />
+ <description data-l10n-id="openpgp-external-key-warning" />
+ </hbox>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin" />
+ </vbox>
+ </dialog>
+</window>
diff --git a/comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.js b/comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.js
new file mode 100644
index 0000000000..e1d369e1ab
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.js
@@ -0,0 +1,177 @@
+/* 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/. */
+
+var { EnigmailFuncs } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/funcs.jsm"
+);
+var EnigmailKeyRing = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+).EnigmailKeyRing;
+var { EnigmailWindows } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/windows.jsm"
+);
+var { EnigmailDialog } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/dialog.jsm"
+);
+var { EnigmailKey } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/key.jsm"
+);
+var KeyLookupHelper = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyLookupHelper.jsm"
+).KeyLookupHelper;
+const { PgpSqliteDb2 } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/sqliteDb.jsm"
+);
+
+var gListBox;
+var gViewButton;
+
+var gAddr;
+var gRowToKey = [];
+
+async function setListEntries(keys = null) {
+ let index = 0;
+
+ // Temporary code for debugging/development, should be removed when
+ // a final patch for bug 1627956 lands.
+ console.log(await EnigmailKeyRing.getEncryptionKeyMeta(gAddr));
+
+ if (!keys) {
+ keys = await EnigmailKeyRing.getMultValidKeysForOneRecipient(gAddr, true);
+ }
+
+ for (let keyObj of keys) {
+ let listitem = document.createXULElement("richlistitem");
+
+ let keyId = document.createXULElement("label");
+ keyId.setAttribute("value", "0x" + keyObj.keyId);
+ keyId.setAttribute("crop", "end");
+ keyId.setAttribute("style", "width: var(--keyWidth)");
+ listitem.appendChild(keyId);
+
+ let acceptanceText;
+
+ // Further above, we called getMultValidKeysForOneRecipient
+ // and asked to ignore if a key is expired.
+ // If the following check fails, the key must be expired.
+ if (!EnigmailKeyRing.isValidForEncryption(keyObj)) {
+ acceptanceText = "openpgp-key-expired";
+ } else if (keyObj.secretAvailable) {
+ if (await PgpSqliteDb2.isAcceptedAsPersonalKey(keyObj.fpr)) {
+ acceptanceText = "openpgp-key-own";
+ } else {
+ acceptanceText = "openpgp-key-secret-not-personal";
+ }
+ } else {
+ if (!("acceptance" in keyObj)) {
+ throw new Error(
+ "expected getMultValidKeysForOneRecipient to set acceptance"
+ );
+ }
+ switch (keyObj.acceptance) {
+ case "rejected":
+ acceptanceText = "openpgp-key-rejected";
+ break;
+ case "unverified":
+ acceptanceText = "openpgp-key-unverified";
+ break;
+ case "verified":
+ acceptanceText = "openpgp-key-verified";
+ break;
+ case "undecided":
+ acceptanceText = "openpgp-key-undecided";
+ break;
+ default:
+ throw new Error("unexpected acceptance value: " + keyObj.acceptance);
+ }
+ }
+
+ let status = document.createXULElement("label");
+ document.l10n.setAttributes(status, acceptanceText);
+ status.setAttribute("crop", "end");
+ status.setAttribute("style", "width: var(--statusWidth)");
+ listitem.appendChild(status);
+
+ let issued = document.createXULElement("label");
+ issued.setAttribute("value", keyObj.created);
+ issued.setAttribute("crop", "end");
+ issued.setAttribute("style", "width: var(--issuedWidth)");
+ listitem.appendChild(issued);
+
+ let expire = document.createXULElement("label");
+ expire.setAttribute("value", keyObj.expiry);
+ expire.setAttribute("crop", "end");
+ expire.setAttribute("style", "width: var(--expireWidth)");
+ listitem.appendChild(expire);
+
+ gListBox.appendChild(listitem);
+
+ gRowToKey[index] = keyObj.keyId;
+ index++;
+ }
+}
+
+async function onLoad() {
+ let params = window.arguments[0];
+ if (!params) {
+ return;
+ }
+
+ gListBox = document.getElementById("infolist");
+ gViewButton = document.getElementById("detailsButton");
+
+ gAddr = params.email;
+
+ document.l10n.setAttributes(
+ document.getElementById("intro"),
+ "openpgp-intro",
+ { key: gAddr }
+ );
+
+ await setListEntries(params.keys);
+}
+
+async function reloadAndSelect(selIndex = -1) {
+ while (true) {
+ let child = gListBox.lastChild;
+ // keep first child, which is the header
+ if (child == gListBox.firstChild) {
+ break;
+ }
+ gListBox.removeChild(child);
+ }
+ gRowToKey = [];
+ await setListEntries();
+ gListBox.selectedIndex = selIndex;
+}
+
+function onSelectionChange(event) {
+ let haveSelection = gListBox.selectedItems.length;
+ gViewButton.disabled = !haveSelection;
+}
+
+function viewSelectedKey() {
+ let selIndex = gListBox.selectedIndex;
+ if (gViewButton.disabled || selIndex == -1) {
+ return;
+ }
+ EnigmailWindows.openKeyDetails(window, gRowToKey[selIndex], false);
+ reloadAndSelect(selIndex);
+}
+
+async function discoverKey() {
+ let keyIds = gRowToKey;
+ let foundNewData = await KeyLookupHelper.fullOnlineDiscovery(
+ "interactive-import",
+ window,
+ gAddr,
+ keyIds
+ );
+ if (foundNewData) {
+ reloadAndSelect();
+ } else {
+ let value = await document.l10n.formatValue("no-key-found2");
+ EnigmailDialog.alert(window, value);
+ }
+}
diff --git a/comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.xhtml b/comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.xhtml
new file mode 100644
index 0000000000..5875224d4c
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.xhtml
@@ -0,0 +1,86 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/openPgpComposeStatus.css" type="text/css"?>
+
+<window
+ data-l10n-id="openpgp-one-recipient-status-title"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ style="width: 50em; height: 22em"
+ persist="width height"
+ onload="onLoad();"
+>
+ <dialog id="oneRecipientStatus" buttons="accept">
+ <script src="chrome://openpgp/content/ui/oneRecipientStatus.js" />
+ <linkset>
+ <html:link
+ rel="localization"
+ href="messenger/openpgp/oneRecipientStatus.ftl"
+ />
+ <html:link rel="localization" href="messenger/openpgp/openpgp.ftl" />
+ </linkset>
+ <script>
+ <![CDATA[
+ function resizeColumns() {
+ let list = document.getElementById("infolist");
+ let cols = list.getElementsByTagName("treecol");
+ list.style.setProperty("--keyWidth", cols[0].getBoundingClientRect().width + "px");
+ list.style.setProperty("--statusWidth", cols[1].getBoundingClientRect().width + "px");
+ list.style.setProperty("--issuedWidth", cols[2].getBoundingClientRect().width + "px");
+ list.style.setProperty("--expireWidth", cols[3].getBoundingClientRect().width - 5 + "px");
+ }
+ addEventListener("load", resizeColumns, { once: true });
+ addEventListener("resize", resizeColumns);
+ ]]>
+ </script>
+
+ <description data-l10n-id="openpgp-one-recipient-status-instruction1" />
+ <separator class="thin" />
+ <description data-l10n-id="openpgp-one-recipient-status-instruction2" />
+ <separator class="thin" />
+ <label id="intro" control="infolist" />
+
+ <richlistbox
+ id="infolist"
+ class="theme-listbox"
+ flex="1"
+ onselect="onSelectionChange(event);"
+ >
+ <treecols>
+ <treecol
+ id="recipientKeyIdCol"
+ data-l10n-id="openpgp-one-recipient-status-key-id"
+ />
+ <treecol
+ id="recipientStatusCol"
+ data-l10n-id="openpgp-one-recipient-status-status"
+ />
+ <treecol
+ style="flex: 1 auto"
+ data-l10n-id="openpgp-one-recipient-status-created-date"
+ />
+ <treecol
+ style="flex: 1 auto"
+ data-l10n-id="openpgp-one-recipient-status-expires-date"
+ />
+ </treecols>
+ </richlistbox>
+ <hbox pack="start">
+ <button
+ id="detailsButton"
+ disabled="true"
+ data-l10n-id="openpgp-one-recipient-status-open-details"
+ oncommand="viewSelectedKey();"
+ />
+ <button
+ id="discoverButton"
+ data-l10n-id="openpgp-one-recipient-status-discover"
+ oncommand="discoverKey();"
+ />
+ </hbox>
+ </dialog>
+</window>