diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 5aacd7a..f53ee7d 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -16,7 +16,7 @@ When we want to change the plugin preferences we can do so by modifying three areas in the code: - `options.xul` the markup, here you define the identifier for the option that will be used also elsewhere -- `prefsFactory.js` update getter and setter functions corresponding to the option +- `prefsFactory.js` update getter and setter functions corresponding to the option and the default value - `options.js` update `addAll`, the window load handler, the dialogAccept handler ## Compat and Prefs diff --git a/chrome/content/modules/pEp.js b/chrome/content/modules/pEp.js index 668f5de..a0a0f62 100644 --- a/chrome/content/modules/pEp.js +++ b/chrome/content/modules/pEp.js @@ -29,6 +29,10 @@ if (typeof btoa === "undefined" && typeof require == "function") { var btoa = require('btoa'); } +function deepCopy(obj) { + return obj && JSON.parse(JSON.stringify(obj)); +} + class pEp { constructor(env, logger, adapter, files, os) { if (pEp.exists) return pEp.instance; @@ -228,9 +232,6 @@ class pEp { } static cloneMessage(source) { - function deepCopy(obj) { - return obj && JSON.parse(JSON.stringify(obj)); - } let clone = new pEp.Message(source.id, source.shortmsg, source.longmsg, @@ -244,6 +245,40 @@ class pEp { source.bcc && clone.setBcc(source.bcc); return clone; } + + setPrefs(prefs) { + this.prefs = prefs; + } + + disclaimerStep(message) { + let mode = this.prefs.getDisclaimerMode(); + let disclaimer = this.prefs.getDisclaimer(); + function withDisclaimer(message) { + let modified = deepCopy(message); + modified.longmsg += "\r\n" + disclaimer; + return modified; + } + if (mode === "disclaimer-encrypted") { + return this + .getOngoingRating( + message.from, + message.to, + message.cc, + message.bcc) + .then((rating) => { + if (rating > 5) { + return withDisclaimer(message); + } else { + return message; + } + }) + .catch(() => message ); + } else if (mode === "disclaimer-all") { + return Promise.resolve(withDisclaimer(message)); + } else { + return Promise.resolve(message); + } + } } pEp.Identity = class { diff --git a/chrome/content/options.css b/chrome/content/options.css index d21f68a..aad014c 100644 --- a/chrome/content/options.css +++ b/chrome/content/options.css @@ -6,6 +6,9 @@ tabpanel { h3 { padding-left: 8px; /* to match any groupbox below */ } +select, textarea { + margin: 0.5em 0; +} .section { margin-top: 0.1em; margin-bottom: 0.1em; @@ -26,3 +29,6 @@ h3 { padding: 0; margin: 0; } +.hidden { + display: none; +} diff --git a/chrome/content/options.js b/chrome/content/options.js index ce167f9..4ccf47f 100644 --- a/chrome/content/options.js +++ b/chrome/content/options.js @@ -1,10 +1,16 @@ // see developer.thunderbird.net/add-ons/updates/tb68 +// i got an exception when trying to access preferences before this call Preferences.addAll([ { id: "extensions.p4tb.storeAllSecurely", type: "bool" }, { id: "extensions.p4tb.warnUnencrypted", type: "bool" }, { id: "extensions.p4tb.unprotectedSubjects", type: "bool" }, { id: "extensions.p4tb.storeProtectedOptions", type: "bool" }, { id: "extensions.p4tb.pEpSync", type: "bool" }, + { id: "extensions.p4tb.disclaimerMode", type: "string" }, + { id: "extensions.p4tb.disclaimer", type: "string" }, + // `pristine` is used only by us in order to provide defaults, see + // `options.js` + { id: "extensions.p4tb.initialised", type: "bool" }, // examples with different types //{ id: "extensions.nameOfAddon.pref2", type: "string" }, //{ id: "extensions.nameOfAddon.pref3", type: "int" }, @@ -15,17 +21,31 @@ const Compat = ChromeUtils.import("chrome://p4t/content/compatFactory.js") const Prefs = ChromeUtils.import("chrome://p4t/content/prefsFactory.js") .prefsFactory(Compat); + +function update() { + let hidden = document.querySelector('#disclaimerMode').value === "disclaimer-none" + document.querySelector('#disclaimer').classList.toggle("hidden", hidden); +} + window.addEventListener('load', function () { + Prefs.maybeSetDefaults(); document.getElementById('storeAllSecurely').checked = Prefs.isStoreAllSecurely(); document.getElementById('warnUnencrypted').checked = Prefs.isWarnUnencrypted(); + document.getElementById('disclaimerMode').value = Prefs.getDisclaimerMode(); + document.getElementById('disclaimer').value = Prefs.getDisclaimer(); + let acceptButton = document.querySelector('button#accept'); let cancelButton = document.querySelector('button#cancel'); acceptButton.addEventListener('click', function () { Prefs.setIsStoreAllSecurely(document.getElementById('storeAllSecurely').checked); Prefs.setWarnUnencrypted(document.getElementById('warnUnencrypted').checked); + Prefs.setDisclaimerMode(document.getElementById('disclaimerMode').value); + Prefs.setDisclaimer(document.getElementById('disclaimer').value); window.close(); }); cancelButton.addEventListener('click', function () { window.close(); }); + update(); + document.addEventListener('change', update); }); diff --git a/chrome/content/options.xul b/chrome/content/options.xul index fcf83ef..0a6fb70 100644 --- a/chrome/content/options.xul +++ b/chrome/content/options.xul @@ -38,6 +38,27 @@ + + + + + + + None + + + All mail + + + Only encrypted mail + + + + Disclaimer text here + + Cancel diff --git a/chrome/content/pEpMimeEncrypt.js b/chrome/content/pEpMimeEncrypt.js index 1b37d85..9eaad30 100644 --- a/chrome/content/pEpMimeEncrypt.js +++ b/chrome/content/pEpMimeEncrypt.js @@ -343,6 +343,8 @@ PgpMimeEncrypt.prototype = { console.log(pEpMessage); console.log("=============="); + return this.pEpController.disclaimerStep(pEpMessage); + }).then((pEpMessage) => { this.pEpController.encryptMailWithMessage(pEpMessage).then((result) => { resultObj = result; diff --git a/chrome/content/pepmessengercompose.js b/chrome/content/pepmessengercompose.js index 7b43c0b..c375605 100644 --- a/chrome/content/pepmessengercompose.js +++ b/chrome/content/pepmessengercompose.js @@ -99,12 +99,13 @@ var pEpComposer = { // resolved. pEpComposer init is run when the compose window // is open and privacy will be checked when the message is // sent. A race condition is unlikely yet possible. + pEpController.setPrefs(this.prefs); TbAbstraction.getCurrentIdentity().setIntAttribute("encryptionpolicy", 2); }, initPrivacyWarning() { let id = this.window.arguments[0].originalMsgURI; - if (typeof id == 'undefined') { + if (id === '') { // these windows have no original, users are starting a // new conversation return Promise.reject(); diff --git a/chrome/content/pepmsghdrview.js b/chrome/content/pepmsghdrview.js index 0f7047b..0761968 100644 --- a/chrome/content/pepmsghdrview.js +++ b/chrome/content/pepmsghdrview.js @@ -1,6 +1,10 @@ var pEpController = ChromeUtils.import("chrome://p4t/content/p4tb.js").pEpController; var Helper = ChromeUtils.import("chrome://p4t/content/TbHelper.js").TbHelper; var MessageView = ChromeUtils.import("chrome://p4t/content/TbMessageView.js").TbMessageView; +const Compat = ChromeUtils.import("chrome://p4t/content/compatFactory.js") + .compatFactory(window); +const Prefs = ChromeUtils.import("chrome://p4t/content/prefsFactory.js") + .prefsFactory(Compat); var PEP_COLUMN_NAME = "pEpStatusCol"; diff --git a/chrome/content/prefsFactory.js b/chrome/content/prefsFactory.js index 5290c99..54cbe4f 100644 --- a/chrome/content/prefsFactory.js +++ b/chrome/content/prefsFactory.js @@ -1,3 +1,4 @@ + function prefsFactory(Compat) { function newVersion () { return Services.prefs.getBranch('extensions.p4tb.'); @@ -5,9 +6,10 @@ function prefsFactory(Compat) { function oldVersion () { return Components.classes["@mozilla.org/preferences-service;1"] .getService(Components.interfaces.nsIPrefService) - .getBranch("extensions.stockwatcher2."); + .getBranch("extensions.p4tb."); } const prefs = Compat.versionBranch(newVersion, oldVersion); + return { isStoreAllSecurely: () => { return prefs.getBoolPref('storeAllSecurely'); @@ -20,6 +22,55 @@ function prefsFactory(Compat) { }, setWarnUnencrypted: (v) => { return prefs.setBoolPref('warnUnencrypted', v); + }, + setDisclaimer: (v) => { + return prefs.setStringPref('disclaimer', v); + }, + getDisclaimer: () => { + return prefs.getStringPref('disclaimer'); + }, + setDisclaimerMode: (v) => { + return prefs.setStringPref('disclaimerMode', v); + }, + getDisclaimerMode: () => { + return prefs.getStringPref('disclaimerMode'); + }, + maybeSetDefaults: () => { + // set up default values to avoid exceptions when we try to access a + // preference for the first time + if (prefs.getBoolPref("initialised")) { + return; + } else { + let defaults = { + bool: { + "storeAllSecurely": true, + "warnUnencrypted": false, + "unprotectedSubjects": false, + "storeProtectedOptions": true, + "pEpSync": false, + "pristine": false // this is just for us to set up defaults + }, + string: { + "disclaimerMode": "disclaimer-none", + "disclaimer": "Disclaimer text here" + } + } + function ObjectEntries(object) { + return Object + .keys(object) + .map((key) => { + return [key, object[key]]; + }); + } + function setter(type) { + return function([key, value]) { + prefs["set"+type+"Pref"](key, value); + } + } + ObjectEntries(defaults.bool).map(setter("Bool")); + ObjectEntries(defaults.string).map(setter("String")); + prefs.setBoolPref("initialised", true); + } } } } diff --git a/manual-and-test.md b/manual-and-test.md index 0646448..0673962 100644 --- a/manual-and-test.md +++ b/manual-and-test.md @@ -30,3 +30,14 @@ your mail server When you reply or forward a private message to untrusted receivers, the conversation will continue without encryption. In this case you will see a warning when this option is set + +## Disclaimer + +This option is designed to work with the disclaimer functionality +provided by your mail server. You can choose "Only encrypted mail" in +order for p≡p to add the disclaimer to encrypted messages that cannot +be modified by your mail server. + +Alternatively you can disable the disclaimer on your server and select +"All mail" here to use only p≡p's disclaimer. + diff --git a/tests/unit/controller.js b/tests/unit/controller.js index a82f40f..c309a67 100644 --- a/tests/unit/controller.js +++ b/tests/unit/controller.js @@ -1,6 +1,7 @@ /* eslint-disable no-console */ let {describe, it, before, beforeEach} = require('mocha'); let chai = require('chai').use(require('chai-as-promised')); +let sinon = require('sinon'); let getQueue = require('../mock').getQueue; let getController = require('../boilerplate').getController; let expect = require('chai').expect @@ -174,4 +175,50 @@ describe('controller calls', () => { return result.should.become([]); }); }); + describe('appends the disclaimer, P4TB-131', () => { + it('to unencrypted messages', () => { + controller.setPrefs({ + getDisclaimer: () => "disclaimer", + getDisclaimerMode: () => "disclaimer-all" + }); + let prom = controller.disclaimerStep(messages.unencrypted.simple); + return expect(prom).to.eventually.have.property("longmsg", "body\r\ndisclaimer"); + }); + it('to encrypted messages', () => { + queue.respondWith({"jsonrpc":"2.0","id":21,"result":{"outParams":[{"rating": 6}],"return":{"status":0,"hex":"0 \"PEP_STATUS_OK\""},"errorstack":["(1 elements cleared)"]}}); + controller.setPrefs({ + getDisclaimer: () => "disclaimer", + getDisclaimerMode: () => "disclaimer-encrypted" + }); + let prom = controller.disclaimerStep(messages.unencrypted.simple); + return expect(prom).to.eventually.have.property("longmsg", "body\r\ndisclaimer"); + }); + it('encrypted mode with unencrypted message', () => { + queue.respondWith({"jsonrpc":"2.0","id":21,"result":{"outParams":[{"rating":3}],"return":{"status":0,"hex":"0 \"PEP_STATUS_OK\""},"errorstack":["(1 elements cleared)"]}}); + controller.setPrefs({ + getDisclaimer: () => "disclaimer", + getDisclaimerMode: () => "disclaimer-encrypted" + }); + let prom = controller.disclaimerStep(messages.unencrypted.simple); + return expect(prom).to.eventually.have.property("longmsg", "body"); + }); + it('none mode', () => { + controller.setPrefs({ + getDisclaimer: () => "disclaimer", + getDisclaimerMode: () => "disclaimer-none" + }); + let prom = controller.disclaimerStep(messages.unencrypted.simple); + return expect(prom).to.eventually.have.property("longmsg", "body"); + }); + it('leaves the message untouched when the call to the engine fails', () => { + controller.setPrefs({ + getDisclaimer: () => "disclaimer", + getDisclaimerMode: () => "disclaimer-encrypted" + }); + let rating = sinon.stub(controller, "getOngoingRating"); + rating.returns(Promise.reject()); + let prom = controller.disclaimerStep(messages.unencrypted.simple); + return expect(prom).to.eventually.have.property("longmsg", "body"); + }); + }); });