Revision control

Copy as Markdown

Other Tools

/*
* 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.importESModule(
);
var { EnigmailCore } = ChromeUtils.importESModule(
);
var { EnigmailFuncs } = ChromeUtils.importESModule(
);
var { EnigmailArmor } = ChromeUtils.importESModule(
);
var { EnigmailKeyRing } = ChromeUtils.importESModule(
);
var { EnigmailConstants } = ChromeUtils.importESModule(
);
var { EnigmailDecryption } = ChromeUtils.importESModule(
);
var { EnigmailEncryption } = ChromeUtils.importESModule(
);
var { EnigmailMsgRead } = ChromeUtils.importESModule(
);
var { EnigmailMimeEncrypt } = ChromeUtils.importESModule(
);
var { MailStringUtils } = ChromeUtils.importESModule(
);
const { OpenPGPAlias } = ChromeUtils.importESModule(
);
var { jsmime } = ChromeUtils.importESModule(
);
const { getMimeTreeFromUrl } = ChromeUtils.importESModule(
);
var l10nOpenPGP = new Localization(["messenger/openpgp/openpgp.ftl"]);
var Enigmail = {};
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: [],
async composeStartup() {
if (!gMsgCompose || !gMsgCompose.compFields) {
throw new Error("OpenPGP initialization failed");
}
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();
},
isSmimeEnabled() {
return (
gCurrentIdentity.getUnicharAttribute("signing_cert_name") !== "" ||
gCurrentIdentity.getUnicharAttribute("encryption_cert_name") !== ""
);
},
getOriginalMsgUri() {
const 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;
},
/** @param {?string} msgUri */
getMsgHdr(msgUri) {
try {
if (!msgUri) {
msgUri = this.getOriginalMsgUri();
}
if (msgUri) {
return gMessenger.msgHdrFromURI(msgUri);
}
} catch (ex) {
// See also bug 1635648
console.warn(`Get msg hdr failed for msgUri=${msgUri}`, ex);
}
return null;
},
getMsgProperties(draft, msgUri, msgHdr, mimeMsg, obtainedDraftFlagsObj) {
obtainedDraftFlagsObj.value = false;
const self = this;
let properties = 0;
try {
if (msgHdr) {
properties = msgHdr.getUint32Property("enigmail");
if (draft) {
if (self.getSavedDraftOptions(mimeMsg)) {
obtainedDraftFlagsObj.value = true;
}
updateEncryptionDependencies();
}
}
} catch (ex) {
console.error(ex);
}
if (gEncryptedURIService.isEncrypted(msgUri)) {
properties |= EnigmailConstants.DECRYPTION_OKAY;
}
return properties;
},
getSavedDraftOptions(mimeMsg) {
if (!mimeMsg || !mimeMsg.headers.has("x-enigmail-draft-status")) {
return false;
}
const stat = mimeMsg.headers.get("x-enigmail-draft-status").join("");
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() {
let msgUri = null;
let msgHdr = null;
msgUri = this.getOriginalMsgUri();
if (msgUri) {
msgHdr = this.getMsgHdr(msgUri);
if (msgHdr) {
try {
const msgUrl = EnigmailMsgRead.getUrlFromUriSpec(msgUri);
getMimeTreeFromUrl(msgUrl.spec, false, mimeMsg => {
Enigmail.msg.continueComposeOpenWithMimeTree(
msgUri,
msgHdr,
mimeMsg
);
});
} catch (ex) {
console.warn(ex);
this.continueComposeOpenWithMimeTree(msgUri, msgHdr, null);
}
} else {
this.continueComposeOpenWithMimeTree(msgUri, msgHdr, null);
}
} else {
this.continueComposeOpenWithMimeTree(msgUri, msgHdr, null);
}
},
continueComposeOpenWithMimeTree(msgUri, msgHdr, mimeMsg) {
const selectedElement = document.activeElement;
const 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) {
const globalSaysItsEncrypted =
gEncryptedURIService &&
gMsgCompose.originalMsgURI &&
gEncryptedURIService.isEncrypted(gMsgCompose.originalMsgURI);
if (globalSaysItsEncrypted) {
useEncryptionUnlessWeHaveDraftInfo = true;
useSMIMEUnlessWeKnowOtherwise = true;
}
}
const obtainedDraftFlagsObj = { value: false };
if (msgUri) {
const 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) {
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) {
if (
this.attachOwnKeyObj.attachedKey &&
this.attachOwnKeyObj.attachedKey != id
) {
// remove attached key if user ID changed
this.removeAttachedKey();
}
const revokedIDs = EnigmailKeyRing.findRevokedPersonalKeysByEmail(
gCurrentIdentity.email
);
if (!this.attachOwnKeyObj.attachedKey) {
const 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
) {
EnigmailCore.init();
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) {
Services.prompt.alert(window, null, 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() {
const 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);
}
},
// Used on send failure, to reset the pre-send modifications
resetUpdatedFields() {
this.removeAttachedKey();
// reset subject
const p = gMsgCompose?.compFields.composeSecure;
if (p && EnigmailMimeEncrypt.isEnigmailCompField(p)) {
const si = p.wrappedJSObject;
if (si.originalSubject) {
gMsgCompose.compFields.subject = si.originalSubject;
}
}
},
replaceEditorText(text) {
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 OpenPGP is enabled for the account.
*/
isEnigmailEnabledForIdentity() {
return !!gCurrentIdentity.getUnicharAttribute("openpgp_key_id");
},
/**
* Determine if Autocrypt is enabled for the account
*/
isAutocryptEnabled() {
return false;
},
processFinalState() {},
/**
* Check if encryption is possible (have keys for everyone or not).
*
* @returns {object} details. Details of invalid keys.
* @returns {object[]} details.errArray
* @returns {string[]} details.errArray[].addr email address.
* @returns {string[]} details.errArray[].msg related error.
*/
async determineSendFlags() {
const 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 recList;
if (compFields.to) {
recList = compFields.splitRecipients(compFields.to, true);
this.addRecipients(toAddrList, recList);
}
if (compFields.cc) {
recList = compFields.splitRecipients(compFields.cc, true);
this.addRecipients(toAddrList, recList);
}
if (compFields.bcc) {
recList = compFields.splitRecipients(compFields.bcc, true);
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;
},
addRecipients(toAddrList, recList) {
for (var i = 0; i < recList.length; i++) {
try {
toAddrList.push(
EnigmailFuncs.stripEmail(recList[i].replace(/[",]/g, ""))
);
} catch (ex) {}
}
},
setDraftStatus() {
// 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() {
const keyId = gCurrentIdentity?.getUnicharAttribute("openpgp_key_id");
return keyId ? "0x" + keyId : null;
},
/**
* Manage the wrapping of inline signed mails
*
* @param {object} wrapresultObj - Result
* @param {boolean} wrapresultObj.cancelled - true if send operation is to
* be cancelled, else false
* @param {boolean} wrapresultObj.usePpgMime - true if message send option
* was changed to PGP/MIME, else false.
*/
async wrapInLine(wrapresultObj) {
wrapresultObj.cancelled = false;
wrapresultObj.usePpgMime = false;
try {
var editor = gMsgCompose.editor.QueryInterface(Ci.nsIEditorMailSupport);
var encoderFlags =
Ci.nsIDocumentEncoder.OutputFormatted |
Ci.nsIDocumentEncoder.OutputLFLineBreak;
var wrapWidth = Services.prefs.getIntPref("mailnews.wraplength");
if (wrapWidth > 0 && wrapWidth < 68 && editor.wrapWidth > 0) {
const text = await l10nOpenPGP.formatValue("minimal-line-wrapping", {
width: wrapWidth,
});
if (Services.prompt.confirm(window, null, text)) {
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) {
// Excess lines detected.
var resultObj = {};
window.openDialog(
"",
"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) {
console.error("Wrap inline FAILED.", ex);
}
},
/**
* Save draft message. We do not want most of the other processing for
* encrypted mails here...
*
* @param {boolean} senderKeyIsGnuPG - Whether sender key is from external GnuPG.
*/
async saveDraftMessage(senderKeyIsGnuPG) {
// 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.
const doEncrypt =
Enigmail.msg.isEnigmailEnabledForIdentity() &&
gCurrentIdentity.autoEncryptDrafts;
this.setDraftStatus(doEncrypt);
if (!doEncrypt) {
try {
const p = gMsgCompose?.compFields.composeSecure;
if (EnigmailMimeEncrypt.isEnigmailCompField(p)) {
p.wrappedJSObject.sendFlags = 0;
}
} catch (ex) {
console.warn(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;
}
const fromAddr = this.getSenderUserId();
EnigmailCore.init();
const senderKeyUsable = await EnigmailEncryption.determineOwnKeyUsability(
sendFlags,
fromAddr,
senderKeyIsGnuPG
);
if (senderKeyUsable.errorMsg) {
let fullAlert = await document.l10n.formatValue(
"msg-compose-cannot-save-draft"
);
fullAlert += " - " + senderKeyUsable.errorMsg;
Services.prompt.alert(window, null, fullAlert);
return false;
}
let secInfo;
const param = gMsgCompose?.compFields.composeSecure;
if (EnigmailMimeEncrypt.isEnigmailCompField(param)) {
secInfo = param.wrappedJSObject;
} else {
try {
secInfo = EnigmailMimeEncrypt.createMimeEncrypt(param);
if (secInfo && gMsgCompose) {
gMsgCompose.compFields.composeSecure = secInfo;
}
} catch (ex) {
console.error("Saving draft FAILED.", 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;
},
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!)
const si = gMsgCompose?.compFields.composeSecure;
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;
gMsgCompose.compFields.composeSecure = newSecurityInfo;
}
} catch (ex) {
console.error(ex);
}
}
}
return newSecurityInfo;
},
/**
* Determine message recipients.
*
* @param {integer} sendFlags - Send flags.
* @returns {Promise<?object>} details - Details, or null if OpenPGP not set
* up for the current identity.
* @returns {integer} details.sendFlags
* @returns {string} details.fromAddr
* @returns {string[]} details.toAddrList - To and Cc addresses.
* @returns {integer[]} details.bccAddrList
*/
async determineMsgRecipients(sendFlags) {
let fromAddr = gCurrentIdentity.email;
const toAddrList = [];
const bccAddrList = [];
if (!Enigmail.msg.isEnigmailEnabledForIdentity()) {
return null;
}
let optSendFlags = 0;
if (Services.prefs.getBoolPref("temp.openpgp.encryptToSelf")) {
optSendFlags |= EnigmailConstants.SEND_ENCRYPT_TO_SELF;
}
sendFlags |= optSendFlags;
var userIdValue = this.getSenderUserId();
if (userIdValue) {
fromAddr = userIdValue;
}
if (gMsgCompose.compFields.to) {
const recList = gMsgCompose.compFields.splitRecipients(
gMsgCompose.compFields.to,
true
);
this.addRecipients(toAddrList, recList);
}
if (gMsgCompose.compFields.cc) {
const recList = gMsgCompose.compFields.splitRecipients(
gMsgCompose.compFields.cc,
true
);
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 (gMsgCompose.compFields.bcc) {
const recList = gMsgCompose.compFields.splitRecipients(
gMsgCompose.compFields.bcc,
true
);
this.addRecipients(bccAddrList, recList);
}
return {
sendFlags,
optSendFlags,
fromAddr,
toAddrList,
bccAddrList,
};
},
prepareSending(sendFlags) {
// 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
) {
if (!newSecurityInfo && gMsgCompose) {
gMsgCompose.compFields.composeSecure =
EnigmailMimeEncrypt.createMimeEncrypt(
gMsgCompose.compFields.composeSecure
);
newSecurityInfo = gMsgCompose.compFields.composeSecure.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;
return newSecurityInfo;
},
/**
* @param {nsIMsgCompDeliverMode} msgSendType
*/
async prepareSendMsg(msgSendType) {
const senderKeyIsGnuPG =
Services.prefs.getBoolPref("mail.openpgp.allow_external_gnupg") &&
gCurrentIdentity.getBoolAttribute("is_gnupg_key_id");
let sendFlags = this.getEncryptionFlags();
switch (msgSendType) {
case Ci.nsIMsgCompDeliverMode.SaveAsDraft:
case Ci.nsIMsgCompDeliverMode.SaveAsTemplate:
case Ci.nsIMsgCompDeliverMode.AutoSaveAsDraft:
// Saving drafts is simpler and works differently than the rest of
// OpenPGP. All rules except account-settings are ignored.
return this.saveDraftMessage(senderKeyIsGnuPG);
}
if (
!gMsgCompose.compFields.to &&
!gMsgCompose.compFields.cc &&
!gMsgCompose.compFields.bcc &&
!gMsgCompose.compFields.newsgroups
) {
throw new Error("No recipients specified!");
}
this.unsetAdditionalHeader("x-enigmail-draft-status");
const senderKeyId = gCurrentIdentity.getUnicharAttribute("openpgp_key_id");
if ((gSendEncrypted || gSendSigned) && !senderKeyId) {
const msgId = gSendEncrypted
? "cannot-send-enc-because-no-own-key"
: "cannot-send-sig-because-no-own-key";
const fullAlert = await document.l10n.formatValue(msgId, {
key: gCurrentIdentity.email,
});
Services.prompt.alert(window, null, fullAlert);
return false;
}
if (senderKeyIsGnuPG) {
sendFlags |= EnigmailConstants.SEND_SENDER_KEY_EXTERNAL;
}
if ((gSendEncrypted || gSendSigned) && senderKeyId) {
const senderKeyUsable = await EnigmailEncryption.determineOwnKeyUsability(
sendFlags,
senderKeyId,
senderKeyIsGnuPG
);
if (senderKeyUsable.errorMsg) {
const fullAlert = await document.l10n.formatValue(
"cannot-use-own-key-because",
{
problem: senderKeyUsable.errorMsg,
}
);
Services.prompt.alert(window, null, fullAlert);
return false;
}
}
let cannotEncryptMissingInfo = false;
if (gSendEncrypted) {
const canEncryptDetails = await this.determineSendFlags();
if (canEncryptDetails.errArray.length != 0) {
cannotEncryptMissingInfo = true;
}
}
if (gWindowLocked) {
Services.prompt.alert(
window,
null,
await document.l10n.formatValue("window-locked")
);
return false;
}
const newSecurityInfo = this.resetDirty();
this.dirty = 1;
const SIGN = EnigmailConstants.SEND_SIGNED;
const ENCRYPT = EnigmailConstants.SEND_ENCRYPTED;
try {
this.modifiedAttach = null;
// fill fromAddr, toAddrList, bcc etc
const rcpt = await this.determineMsgRecipients(sendFlags);
if (!rcpt) {
return true;
}
sendFlags = rcpt.sendFlags;
if (cannotEncryptMissingInfo) {
showMessageComposeSecurityStatus(true);
return false;
}
if (this.sendPgpMime) {
// Use PGP/MIME
sendFlags |= EnigmailConstants.SEND_PGP_MIME;
}
const toAddrStr = rcpt.toAddrList.join(", ");
const bccAddrStr = rcpt.bccAddrList.join(", ");
if (gAttachMyPublicPGPKey) {
await this.attachOwnKey(senderKeyId);
}
const autocryptGossipHeaders = await this.getAutocryptGossip();
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.
const composeSecure = this.prepareSecurityInfo(
sendFlags,
uiFlags,
rcpt,
newSecurityInfo,
autocryptGossipHeaders
);
composeSecure.recipients = toAddrStr;
composeSecure.bccRecipients = bccAddrStr;
} else if (!this.processed && sendFlags & (ENCRYPT | SIGN)) {
// Use inline PGP.
const 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(gMsgCompose.compFields);
if (
!this.prepareSending(
sendFlags,
rcpt.toAddrList.join(", "),
toAddrStr + ", " + bccAddrStr,
Services.io.offline
)
) {
return false;
}
} catch (ex) {
console.warn("Prepare send message FAILED.", ex);
return false;
}
// Make sure that we use base64 encoding for signed payload of
// signed-only emails, only, because some MTAs rewrite the encoding
// of message with a 7bit/8bit encoding.
// We usually don't use base64 encoding for the inner payload of
// encrypted messages, even if that payload is a signed message,
// because we already have sufficient wrapping when using
// encryption (avoiding unnecessary base64 layers saves space).
// However, when using GPGME for signing, the UTF-8 bytes given to
// GPGME might get incorrectly transformed, so let's use base64
// here, too.
// (We don't encode PGP/INLINE signed messages, that would be
// against the intention.)
if (
usingPGPMime &&
sendFlags & SIGN &&
(senderKeyIsGnuPG || !(sendFlags & ENCRYPT))
) {
gMsgCompose.compFields.forceMsgEncoding = true;
}
// The encryption process for PGP/MIME messages follows "here".
return true;
},
async signInline(sendInfo) {
// sign message using inline-PGP
if (sendInfo.sendFlags & EnigmailConstants.SEND_ENCRYPTED) {
throw new Error("Encryption not supported in inline messages!");
}
if (gMsgCompose.composeHTML) {
throw new Error(
"Signing inline only supported for plain text composition!"
);
}
EnigmailCore.init();
if (Services.prefs.getBoolPref("mail.strictly_mime")) {
if (
Services.prompt.confirm(
null,
null,
await l10nOpenPGP.formatValue("quoted-printable-warn")
)
) {
Services.prefs.setBoolPref("mail.strictly_mime", false);
}
}
var sendFlowed = Services.prefs.getBoolPref(
"mailnews.send_plaintext_flowed"
);
var encoderFlags =
Ci.nsIDocumentEncoder.OutputFormatted |
Ci.nsIDocumentEncoder.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).
escText = escText.replace(/^From /gm, "~From ");
escText = escText.replace(/^>/gm, "|");
escText = escText.replace(/^[ \t]+$/gm, "");
escText = escText.replace(/^ /gm, "~ ");
// 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);
// Encode plaintext to utf-8 from unicode
var plainText = MailStringUtils.stringToByteString(escText);
// 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;
if (cipherText && exitCode === 0) {
// Encryption/signing succeeded; overwrite plaintext
cipherText = cipherText.replace(/\r\n/g, "\n");
// Decode ciphertext from utf-8 to unicode and overwrite
this.replaceEditorText(MailStringUtils.byteStringToString(cipherText));
// Save original text (for undo)
this.processed = {
origText,
};
} else {
// Restore original text
this.replaceEditorText(origText);
if (sendInfo.sendFlags & EnigmailConstants.SEND_SIGNED) {
// 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;
}
Services.prompt.alert(
window,
null,
(await document.l10n.formatValue("send-aborted")) + "\n" + txt
);
} else {
const [title, message] = await document.l10n.formatValues([
{ id: "send-aborted" },
{ id: "msg-compose-internal-error" },
]);
Services.prompt.alert(window, title, message);
}
},
messageSendCheck() {
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 = Services.prompt.confirmEx(
window,
bundle.getString("sendMessageCheckWindowTitle"),
bundle.getString("sendMessageCheckLabel"),
Services.prompt.BUTTON_TITLE_IS_STRING *
Services.prompt.BUTTON_POS_0 +
Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.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.
*
* @param {string} hdr - Header type (e.g. X-Enigmail-Version)
* @param {string} val - Header data (e.g. 1.2.3.4)
*/
setAdditionalHeader(hdr, val) {
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) {
console.error(ex);
}
},
getCurrentIncomingServer() {
const currentAccountKey = getCurrentAccountKey();
const 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() {
const fromMail = EnigmailFuncs.stripEmail(gMsgCompose.compFields.from);
const 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.
const allEmails = MailServices.headerParser
.extractHeaderAddressMailboxes(
gMsgCompose.compFields.to +
", " +
gMsgCompose.compFields.cc +
optionalReplyToGossip
)
.split(/, /);
// Use a Set to ensure we have each address only once.
const uniqueEmails = new Set();
for (const 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) {
const k = await EnigmailKeyRing.getRecipientAutocryptKeyForEmail(email);
if (!k) {
continue;
}
const 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() {
const 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.
*
* @param {CustomEvent} event - A compose-send-message event.
*/
sendMessageListener(event) {
const sendMsgType = event.detail.msgType;
if (
!(
this.sendProcess &&
sendMsgType == Ci.nsIMsgCompDeliverMode.AutoSaveAsDraft
)
) {
this.modifyCompFields();
if (!gSelectedTechnologyIsPGP) {
return;
}
this.sendProcess = true;
try {
const encryptResult = EnigmailFuncs.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 {
event.preventDefault();
event.stopPropagation();
}
this.sendProcess = false;
},
async decryptQuote(interactive) {
if (gWindowLocked || this.processed) {
return;
}
EnigmailCore.init();
var encoderFlags =
Ci.nsIDocumentEncoder.OutputFormatted |
Ci.nsIDocumentEncoder.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);
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.
const 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.
const 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 = "";
}
// Encode ciphertext from unicode to utf-8
var cipherText = MailStringUtils.stringToByteString(pgpBlock);
// 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 "utf-8" to unicode
plainText = MailStringUtils.byteStringToString(plainText).replace(
/\r\n/g,
"\n"
);
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) {
Services.prompt.alert(window, null, 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
);
}
var doubleDashSeparator = Services.prefs.getBoolPref(
"temp.openpgp.doubleDashSeparator"
);
if (
gMsgCompose.type != Ci.nsIMsgCompType.Template &&
gMsgCompose.type != Ci.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();
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;
}
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,
Ci.nsISelectionController.SCROLL_SYNCHRONOUS
);
}
},
checkInlinePgpReply(head, tail) {
let hLines = head.search(/[^\s>]/) < 0 ? 0 : 1;
if (hLines > 0) {
switch (gMsgCompose.type) {
case Ci.nsIMsgCompType.Reply:
case Ci.nsIMsgCompType.ReplyAll:
case Ci.nsIMsgCompType.ReplyToSender:
case Ci.nsIMsgCompType.ReplyToGroup:
case Ci.nsIMsgCompType.ReplyToSenderAndGroup:
case Ci.nsIMsgCompType.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
const 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) {
if (this.editor) {
var mailEditor;
try {
mailEditor = this.editor.QueryInterface(Ci.nsIEditorMailSupport);
mailEditor.insertTextWithQuotations(plainText);
} catch (ex) {
console.error("No mail editor.", ex);
this.editor.insertText(plainText);
}
}
},
editorInsertAsQuotation(plainText) {
if (this.editor) {
var mailEditor;
try {
mailEditor = this.editor.QueryInterface(Ci.nsIEditorMailSupport);
} catch (ex) {}
if (!mailEditor) {
return 0;
}
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);
}
},
async 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;
},
},
];
await 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 {integer} priority - Priority of the message [1 = high (error) ... 3 = low (info)]
* @param {string} msgText - Text to be displayed in notification bar
* @param {string} messageId - Unique message type identification
* @param {string} detailsText - optional text to be displayed by clicking
* on "Details" button. If null or "", then the Detail button will not
* 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;
}
const buttonArr = [];
if (detailsText && detailsText.length > 0) {
const [accessKey, label] = await document.l10n.formatValues([
{ id: "msg-compose-details-button-access-key" },
{ id: "msg-compose-details-button-label" },
]);
buttonArr.push({
accessKey,
label,
callback() {
Services.prompt.alert(window, null, detailsText);
},
});
}
await 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() {
const [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();
}
},
editorGetContentAs(mimeType, flags) {
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() {
const subjElem = document.getElementById("msgSubject");
if (subjElem) {
const r = subjElem.value.replace(/^(Re: )+/, "Re: ");
if (r !== subjElem.value) {
subjElem.value = r;
if (typeof subjElem.oninput === "function") {
subjElem.oninput();
}
}
}
},
};
/**
* @implements {nsIMsgComposeStateListener}
*/
Enigmail.composeStateListener = {
NotifyComposeFieldsReady() {
Enigmail.msg.editor = gMsgCompose.editor.QueryInterface(Ci.nsIEditor);
if (!Enigmail.msg.editor) {
return;
}
Enigmail.msg.fixMessageSubject();
},
ComposeProcessDone(aResult) {
// Note: called after a mail was sent (or saved)
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 && gMsgCompose) {
gMsgCompose.compFields.composeSecure = gSMFields;
}
},
NotifyComposeBodyReady() {
const isEmpty = Enigmail.msg.editor.documentIsEmpty;
const isEditable = Enigmail.msg.editor.isDocumentEditable;
Enigmail.msg.composeBodyReady = true;
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() {},
};
window.addEventListener(
"compose-startup-done",
Enigmail.msg.composeStartup.bind(Enigmail.msg),
{
capture: false,
once: true,
}
);
window.addEventListener("compose-window-unload", () => {
if (gMsgCompose) {
gMsgCompose.UnregisterStateListener(Enigmail.composeStateListener);
}
});