diff options
Diffstat (limited to 'comm/mail/extensions/openpgp/content/modules/filters.jsm')
-rw-r--r-- | comm/mail/extensions/openpgp/content/modules/filters.jsm | 598 |
1 files changed, 598 insertions, 0 deletions
diff --git a/comm/mail/extensions/openpgp/content/modules/filters.jsm b/comm/mail/extensions/openpgp/content/modules/filters.jsm new file mode 100644 index 0000000000..09abd448e5 --- /dev/null +++ b/comm/mail/extensions/openpgp/content/modules/filters.jsm @@ -0,0 +1,598 @@ +/* + * 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 EXPORTED_SYMBOLS = ["EnigmailFilters"]; + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { EnigmailConstants } = ChromeUtils.import( + "chrome://openpgp/content/modules/constants.jsm" +); + +const lazy = {}; +XPCOMUtils.defineLazyModuleGetters(lazy, { + EnigmailCore: "chrome://openpgp/content/modules/core.jsm", + EnigmailData: "chrome://openpgp/content/modules/data.jsm", + EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm", + EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm", + EnigmailLog: "chrome://openpgp/content/modules/log.jsm", + EnigmailMime: "chrome://openpgp/content/modules/mime.jsm", + + EnigmailPersistentCrypto: + "chrome://openpgp/content/modules/persistentCrypto.jsm", + + EnigmailStreams: "chrome://openpgp/content/modules/streams.jsm", + EnigmailDialog: "chrome://openpgp/content/modules/dialog.jsm", + jsmime: "resource:///modules/jsmime.jsm", + NetUtil: "resource://gre/modules/NetUtil.jsm", +}); + +let l10n = new Localization(["messenger/openpgp/openpgp.ftl"], true); + +var gNewMailListenerInitiated = false; + +/** + * filter action for creating a decrypted version of the mail and + * deleting the original mail at the same time + */ + +const filterActionMoveDecrypt = { + async applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) { + lazy.EnigmailLog.DEBUG( + "filters.jsm: filterActionMoveDecrypt: Move to: " + aActionValue + "\n" + ); + + for (let msgHdr of aMsgHdrs) { + await lazy.EnigmailPersistentCrypto.cryptMessage( + msgHdr, + aActionValue, + true, + null + ); + } + }, + + isValidForType(type, scope) { + return true; + }, + + validateActionValue(value, folder, type) { + l10n.formatValue("filter-decrypt-move-warn-experimental").then(value => { + lazy.EnigmailDialog.alert(null, value); + }); + + if (value === "") { + return l10n.formatValueSync("filter-folder-required"); + } + + return null; + }, +}; + +/** + * filter action for creating a decrypted copy of the mail, leaving the original + * message untouched + */ +const filterActionCopyDecrypt = { + async applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) { + lazy.EnigmailLog.DEBUG( + "filters.jsm: filterActionCopyDecrypt: Copy to: " + aActionValue + "\n" + ); + + for (let msgHdr of aMsgHdrs) { + await lazy.EnigmailPersistentCrypto.cryptMessage( + msgHdr, + aActionValue, + false, + null + ); + } + }, + + isValidForType(type, scope) { + lazy.EnigmailLog.DEBUG( + "filters.jsm: filterActionCopyDecrypt.isValidForType(" + type + ")\n" + ); + + let r = true; + return r; + }, + + validateActionValue(value, folder, type) { + lazy.EnigmailLog.DEBUG( + "filters.jsm: filterActionCopyDecrypt.validateActionValue(" + + value + + ")\n" + ); + + if (value === "") { + return l10n.formatValueSync("filter-folder-required"); + } + + return null; + }, +}; + +/** + * filter action for to encrypt a mail to a specific key + */ +const filterActionEncrypt = { + async applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) { + // Ensure KeyRing is loaded. + if (aMsgWindow) { + lazy.EnigmailCore.getService(aMsgWindow.domWindow); + } else { + lazy.EnigmailCore.getService(); + } + lazy.EnigmailKeyRing.getAllKeys(); + + lazy.EnigmailLog.DEBUG( + "filters.jsm: filterActionEncrypt: Encrypt to: " + aActionValue + "\n" + ); + let keyObj = lazy.EnigmailKeyRing.getKeyById(aActionValue); + + if (keyObj === null) { + lazy.EnigmailLog.DEBUG( + "filters.jsm: failed to find key by id: " + aActionValue + "\n" + ); + let keyId = lazy.EnigmailKeyRing.getValidKeyForRecipient(aActionValue); + if (keyId) { + keyObj = lazy.EnigmailKeyRing.getKeyById(keyId); + } + } + + if (keyObj === null && aListener) { + lazy.EnigmailLog.DEBUG("filters.jsm: no valid key - aborting\n"); + + aListener.OnStartCopy(); + aListener.OnStopCopy(1); + + return; + } + + lazy.EnigmailLog.DEBUG( + "filters.jsm: key to encrypt to: " + + JSON.stringify(keyObj) + + ", userId: " + + keyObj.userId + + "\n" + ); + + // Maybe skip messages here if they are already encrypted to + // the target key? There might be some use case for unconditionally + // encrypting here. E.g. to use the local preferences and remove all + // other recipients. + // Also not encrypting to already encrypted messages would make the + // behavior less transparent as it's not obvious. + + for (let msgHdr of aMsgHdrs) { + await lazy.EnigmailPersistentCrypto.cryptMessage( + msgHdr, + null /* same folder */, + true /* move */, + keyObj /* target key */ + ); + } + }, + + isValidForType(type, scope) { + return true; + }, + + validateActionValue(value, folder, type) { + // Initialize KeyRing. Ugly as it blocks the GUI but + // we need it. + lazy.EnigmailCore.getService(); + lazy.EnigmailKeyRing.getAllKeys(); + + lazy.EnigmailLog.DEBUG( + "filters.jsm: validateActionValue: Encrypt to: " + value + "\n" + ); + if (value === "") { + return l10n.formatValueSync("filter-key-required"); + } + + let keyObj = lazy.EnigmailKeyRing.getKeyById(value); + + if (keyObj === null) { + lazy.EnigmailLog.DEBUG( + "filters.jsm: failed to find key by id. Looking for uid.\n" + ); + let keyId = lazy.EnigmailKeyRing.getValidKeyForRecipient(value); + if (keyId) { + keyObj = lazy.EnigmailKeyRing.getKeyById(keyId); + } + } + + if (keyObj === null) { + return l10n.formatValueSync("filter-key-not-found", { + desc: value, + }); + } + + if (!keyObj.secretAvailable) { + // We warn but we allow it. There might be use cases where + // thunderbird + enigmail is used as a gateway filter with + // the secret not available on one machine and the decryption + // is intended to happen on different systems. + l10n + .formatValue("filter-warn-key-not-secret", { + desc: value, + }) + .then(value => { + lazy.EnigmailDialog.alert(null, value); + }); + } + + return null; + }, +}; + +function isPGPEncrypted(data) { + // We only check the first mime subpart for application/pgp-encrypted. + // If it is text/plain or text/html we look into that for the + // message marker. + // If there are no subparts we just look in the body. + // + // This intentionally does not match more complex cases + // with sub parts being encrypted etc. as auto processing + // these kinds of mails will be error prone and better not + // done through a filter + + var mimeTree = lazy.EnigmailMime.getMimeTree(data, true); + if (!mimeTree.subParts.length) { + // No subParts. Check for PGP Marker in Body + return mimeTree.body.includes("-----BEGIN PGP MESSAGE-----"); + } + + // Check the type of the first subpart. + var firstPart = mimeTree.subParts[0]; + var ct = firstPart.fullContentType; + if (typeof ct == "string") { + ct = ct.replace(/[\r\n]/g, " "); + // Proper PGP/MIME ? + if (ct.search(/application\/pgp-encrypted/i) >= 0) { + return true; + } + // Look into text/plain pgp messages and text/html messages. + if (ct.search(/text\/plain/i) >= 0 || ct.search(/text\/html/i) >= 0) { + return firstPart.body.includes("-----BEGIN PGP MESSAGE-----"); + } + } + return false; +} + +/** + * filter term for OpenPGP Encrypted mail + */ +const filterTermPGPEncrypted = { + id: EnigmailConstants.FILTER_TERM_PGP_ENCRYPTED, + name: l10n.formatValueSync("filter-term-pgpencrypted-label"), + needsBody: true, + match(aMsgHdr, searchValue, searchOp) { + var folder = aMsgHdr.folder; + var stream = folder.getMsgInputStream(aMsgHdr, {}); + + var messageSize = folder.hasMsgOffline(aMsgHdr.messageKey) + ? aMsgHdr.offlineMessageSize + : aMsgHdr.messageSize; + var data; + try { + data = lazy.NetUtil.readInputStreamToString(stream, messageSize); + } catch (ex) { + lazy.EnigmailLog.DEBUG( + "filters.jsm: filterTermPGPEncrypted: failed to get data.\n" + ); + // If we don't know better to return false. + stream.close(); + return false; + } + + var isPGP = isPGPEncrypted(data); + + stream.close(); + + return ( + (searchOp == Ci.nsMsgSearchOp.Is && isPGP) || + (searchOp == Ci.nsMsgSearchOp.Isnt && !isPGP) + ); + }, + + getEnabled(scope, op) { + return true; + }, + + getAvailable(scope, op) { + return true; + }, + + getAvailableOperators(scope, length) { + length.value = 2; + return [Ci.nsMsgSearchOp.Is, Ci.nsMsgSearchOp.Isnt]; + }, +}; + +function initNewMailListener() { + lazy.EnigmailLog.DEBUG("filters.jsm: initNewMailListener()\n"); + + if (!gNewMailListenerInitiated) { + let notificationService = Cc[ + "@mozilla.org/messenger/msgnotificationservice;1" + ].getService(Ci.nsIMsgFolderNotificationService); + notificationService.addListener( + newMailListener, + notificationService.msgAdded + ); + } + gNewMailListenerInitiated = true; +} + +function shutdownNewMailListener() { + lazy.EnigmailLog.DEBUG("filters.jsm: shutdownNewMailListener()\n"); + + if (gNewMailListenerInitiated) { + let notificationService = Cc[ + "@mozilla.org/messenger/msgnotificationservice;1" + ].getService(Ci.nsIMsgFolderNotificationService); + notificationService.removeListener(newMailListener); + gNewMailListenerInitiated = false; + } +} + +function getIdentityForSender(senderEmail, msgServer) { + let identities = MailServices.accounts.getIdentitiesForServer(msgServer); + return identities.find( + id => id.email.toLowerCase() === senderEmail.toLowerCase() + ); +} + +var consumerList = []; + +function JsmimeEmitter(requireBody) { + this.requireBody = requireBody; + this.mimeTree = { + partNum: "", + headers: null, + body: "", + parent: null, + subParts: [], + }; + this.stack = []; + this.currPartNum = ""; +} + +JsmimeEmitter.prototype = { + createPartObj(partNum, headers, parent) { + return { + partNum, + headers, + body: "", + parent, + subParts: [], + }; + }, + + getMimeTree() { + return this.mimeTree.subParts[0]; + }, + + /** JSMime API */ + startMessage() { + this.currentPart = this.mimeTree; + }, + endMessage() {}, + + startPart(partNum, headers) { + lazy.EnigmailLog.DEBUG( + "filters.jsm: JsmimeEmitter.startPart: partNum=" + partNum + "\n" + ); + //this.stack.push(partNum); + let newPart = this.createPartObj(partNum, headers, this.currentPart); + + if (partNum.indexOf(this.currPartNum) === 0) { + // found sub-part + this.currentPart.subParts.push(newPart); + } else { + // found same or higher level + this.currentPart.subParts.push(newPart); + } + this.currPartNum = partNum; + this.currentPart = newPart; + }, + + endPart(partNum) { + lazy.EnigmailLog.DEBUG( + "filters.jsm: JsmimeEmitter.startPart: partNum=" + partNum + "\n" + ); + this.currentPart = this.currentPart.parent; + }, + + deliverPartData(partNum, data) { + lazy.EnigmailLog.DEBUG( + "filters.jsm: JsmimeEmitter.deliverPartData: partNum=" + partNum + "\n" + ); + if (this.requireBody) { + if (typeof data === "string") { + this.currentPart.body += data; + } else { + this.currentPart.body += lazy.EnigmailData.arrayBufferToString(data); + } + } + }, +}; + +function processIncomingMail(url, requireBody, aMsgHdr) { + lazy.EnigmailLog.DEBUG("filters.jsm: processIncomingMail()\n"); + + let inputStream = lazy.EnigmailStreams.newStringStreamListener(msgData => { + let opt = { + strformat: "unicode", + bodyformat: "decode", + }; + + try { + let e = new JsmimeEmitter(requireBody); + let p = new lazy.jsmime.MimeParser(e, opt); + p.deliverData(msgData); + + for (let c of consumerList) { + try { + c.consumeMessage(e.getMimeTree(), msgData, aMsgHdr); + } catch (ex) { + lazy.EnigmailLog.DEBUG( + "filters.jsm: processIncomingMail: exception: " + + ex.toString() + + "\n" + ); + } + } + } catch (ex) {} + }); + + try { + let channel = lazy.EnigmailStreams.createChannel(url); + channel.asyncOpen(inputStream, null); + } catch (e) { + lazy.EnigmailLog.DEBUG( + "filters.jsm: processIncomingMail: open stream exception " + + e.toString() + + "\n" + ); + } +} + +function getRequireMessageProcessing(aMsgHdr) { + let isInbox = + aMsgHdr.folder.getFlag(Ci.nsMsgFolderFlags.CheckNew) || + aMsgHdr.folder.getFlag(Ci.nsMsgFolderFlags.Inbox); + let requireBody = false; + let inboxOnly = true; + let selfSentOnly = false; + let processReadMail = false; + + for (let c of consumerList) { + if (!c.incomingMailOnly) { + inboxOnly = false; + } + if (!c.unreadOnly) { + processReadMail = true; + } + if (!c.headersOnly) { + requireBody = true; + } + if (c.selfSentOnly) { + selfSentOnly = true; + } + } + + if (!processReadMail && aMsgHdr.isRead) { + return null; + } + if (inboxOnly && !isInbox) { + return null; + } + if (selfSentOnly) { + let sender = lazy.EnigmailFuncs.parseEmails(aMsgHdr.author, true); + let id = null; + if (sender && sender[0]) { + id = getIdentityForSender(sender[0].email, aMsgHdr.folder.server); + } + + if (!id) { + return null; + } + } + + lazy.EnigmailLog.DEBUG( + "filters.jsm: getRequireMessageProcessing: author: " + aMsgHdr.author + "\n" + ); + + let u = lazy.EnigmailFuncs.getUrlFromUriSpec( + aMsgHdr.folder.getUriForMsg(aMsgHdr) + ); + + if (!u) { + return null; + } + + let op = u.spec.indexOf("?") > 0 ? "&" : "?"; + let url = u.spec + op + "header=enigmailFilter"; + + return { + url, + requireBody, + }; +} + +const newMailListener = { + msgAdded(aMsgHdr) { + lazy.EnigmailLog.DEBUG( + "filters.jsm: newMailListener.msgAdded() - got new mail in " + + aMsgHdr.folder.prettiestName + + "\n" + ); + + if (consumerList.length === 0) { + return; + } + + let ret = getRequireMessageProcessing(aMsgHdr); + if (ret) { + processIncomingMail(ret.url, ret.requireBody, aMsgHdr); + } + }, +}; + +/** + messageStructure - Object: + - partNum: String - MIME part number + - headers: Object(nsIStructuredHeaders) - MIME part headers + - body: String or typedarray - the body part + - parent: Object(messageStructure) - link to the parent part + - subParts: Array of Object(messageStructure) - array of the sub-parts + */ + +var EnigmailFilters = { + onStartup() { + let filterService = Cc[ + "@mozilla.org/messenger/services/filters;1" + ].getService(Ci.nsIMsgFilterService); + filterService.addCustomTerm(filterTermPGPEncrypted); + initNewMailListener(); + }, + + onShutdown() { + shutdownNewMailListener(); + }, + + /** + * add a new consumer to listen to new mails + * + * @param consumer - Object + * - headersOnly: Boolean - needs full message body? [FUTURE] + * - incomingMailOnly: Boolean - only work on folder(s) that obtain new mail + * (Inbox and folders that listen to new mail) + * - unreadOnly: Boolean - only process unread mails + * - selfSentOnly: Boolean - only process mails with sender Email == Account Email + * - consumeMessage: function(messageStructure, rawMessageData, nsIMsgHdr) + */ + addNewMailConsumer(consumer) { + lazy.EnigmailLog.DEBUG("filters.jsm: addNewMailConsumer()\n"); + consumerList.push(consumer); + }, + + removeNewMailConsumer(consumer) {}, + + moveDecrypt: filterActionMoveDecrypt, + copyDecrypt: filterActionCopyDecrypt, + encrypt: filterActionEncrypt, +}; |