diff options
author | Patrick Totzke <patricktotzke@gmail.com> | 2017-09-26 15:57:24 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-09-26 15:57:24 +0100 |
commit | 0dcb12db64eda70d59fc2747ce322a7263edcb73 (patch) | |
tree | 438b947a473fb142b8894f0da31b23f4411c4b53 | |
parent | 018aa2e339f9cb3bb4af1d26cce31cbad003409d (diff) | |
parent | 1b3f12f4e3a7bd6bbfe06e42b41658484a291174 (diff) |
Merge pull request #1160 from meskio/fix-1140
Fix #1140
-rw-r--r-- | alot/account.py | 7 | ||||
-rw-r--r-- | alot/commands/envelope.py | 7 | ||||
-rw-r--r-- | alot/commands/utils.py | 15 | ||||
-rw-r--r-- | alot/crypto.py | 3 | ||||
-rw-r--r-- | alot/defaults/alot.rc.spec | 11 | ||||
-rw-r--r-- | docs/source/configuration/accounts_table | 18 | ||||
-rw-r--r-- | tests/commands/utils_tests.py | 206 |
7 files changed, 259 insertions, 8 deletions
diff --git a/alot/account.py b/alot/account.py index b1223902..a4ce439a 100644 --- a/alot/account.py +++ b/alot/account.py @@ -190,6 +190,8 @@ class Account(object): """regex matching alternative addresses""" realname = None """real name used to format from-headers""" + encrypt_to_self = None + """encrypt outgoing encrypted emails to this account's private key""" gpg_key = None """gpg fingerprint for this account's private key""" signature = None @@ -207,8 +209,8 @@ class Account(object): signature_filename=None, signature_as_attachment=False, sent_box=None, sent_tags=None, draft_box=None, draft_tags=None, abook=None, sign_by_default=False, - encrypt_by_default=u"none", case_sensitive_username=False, - **_): + encrypt_by_default=u"none", encrypt_to_self=None, + case_sensitive_username=False, **_): sent_tags = sent_tags or [] if 'sent' not in sent_tags: sent_tags.append('sent') @@ -221,6 +223,7 @@ class Account(object): for a in (aliases or [])] self.alias_regexp = alias_regexp self.realname = realname + self.encrypt_to_self = encrypt_to_self self.gpg_key = gpg_key self.signature = signature self.signature_filename = signature_filename diff --git a/alot/commands/envelope.py b/alot/commands/envelope.py index f497ecc2..d008d227 100644 --- a/alot/commands/envelope.py +++ b/alot/commands/envelope.py @@ -582,7 +582,12 @@ class EncryptCommand(Command): elif self.action == 'toggleencrypt': encrypt = not envelope.encrypt if encrypt: - yield utils.set_encrypt(ui, envelope, signed_only=self.trusted) + if self.encrypt_keys: + for keyid in self.encrypt_keys: + tmp_key = crypto.get_key(keyid) + envelope.encrypt_keys[tmp_key.fpr] = tmp_key + else: + yield utils.set_encrypt(ui, envelope, signed_only=self.trusted) envelope.encrypt = encrypt if not envelope.encrypt: # This is an extra conditional as it can even happen if encrypt is diff --git a/alot/commands/utils.py b/alot/commands/utils.py index 1bf64cbb..7e4e0307 100644 --- a/alot/commands/utils.py +++ b/alot/commands/utils.py @@ -9,6 +9,8 @@ import logging from twisted.internet.defer import inlineCallbacks, returnValue from ..errors import GPGProblem, GPGCode +from ..settings.const import settings +from ..settings.errors import NoMatchingAccount from .. import crypto @@ -46,6 +48,19 @@ def set_encrypt(ui, envelope, block_error=False, signed_only=False): if keys: envelope.encrypt_keys.update(keys) envelope.encrypt = True + + if 'From' in envelope.headers: + try: + acc = settings.get_account_by_address(envelope['From']) + if acc.encrypt_to_self: + if acc.gpg_key: + logging.debug('encrypt to self: %s', acc.gpg_key.fpr) + envelope.encrypt_keys[acc.gpg_key.fpr] = acc.gpg_key + else: + logging.debug('encrypt to self: no gpg_key in account') + except NoMatchingAccount: + logging.debug('encrypt to self: no account found') + else: envelope.encrypt = False diff --git a/alot/crypto.py b/alot/crypto.py index 6e3e8fa6..5ed3a909 100644 --- a/alot/crypto.py +++ b/alot/crypto.py @@ -157,7 +157,7 @@ def detached_signature_for(plaintext_str, keys): return sign_result.signatures, sigblob -def encrypt(plaintext_str, keys=None): +def encrypt(plaintext_str, keys): """Encrypt data and return the encrypted form. :param str plaintext_str: the mail to encrypt @@ -166,6 +166,7 @@ def encrypt(plaintext_str, keys=None): :returns: encrypted mail :rtype: str """ + assert keys, 'Must provide at least one key to encrypt with' ctx = gpg.core.Context(armor=True) out = ctx.encrypt(plaintext_str, recipients=keys, sign=False, always_trust=True)[0] diff --git a/alot/defaults/alot.rc.spec b/alot/defaults/alot.rc.spec index eeae4348..5962dda1 100644 --- a/alot/defaults/alot.rc.spec +++ b/alot/defaults/alot.rc.spec @@ -351,8 +351,15 @@ thread_focus_linewise = boolean(default=True) # 1.0, please move to `all`, `none`, or `trusted`. encrypt_by_default = option('all', 'none', 'trusted', 'True', 'False', 'true', 'false', 'Yes', 'No', 'yes', 'no', '1', '0', default='none') - # The GPG key ID you want to use with this account. If unset, alot will - # use your default key. + # If this is true when encrypting a message it will also be encrypted + # with the key defined for this account. + # + # .. warning:: + # + # Before 0.6 this was controlled via gpg.conf. + encrypt_to_self = boolean(default=True) + + # The GPG key ID you want to use with this account. gpg_key = gpg_key_hint(default=None) # Whether the server treats the address as case-senstive or diff --git a/docs/source/configuration/accounts_table b/docs/source/configuration/accounts_table index 27e7529a..33c554a3 100644 --- a/docs/source/configuration/accounts_table +++ b/docs/source/configuration/accounts_table @@ -159,12 +159,26 @@ :default: none +.. _encrypt-to-self: + +.. describe:: encrypt_to_self + + If this is true when encrypting a message it will also be encrypted + with the key defined for this account. + + .. warning:: + + Before 0.6 this was controlled via gpg.conf. + + :type: boolean + :default: True + + .. _gpg-key: .. describe:: gpg_key - The GPG key ID you want to use with this account. If unset, alot will - use your default key. + The GPG key ID you want to use with this account. :type: string :default: None diff --git a/tests/commands/utils_tests.py b/tests/commands/utils_tests.py new file mode 100644 index 00000000..5339bbf8 --- /dev/null +++ b/tests/commands/utils_tests.py @@ -0,0 +1,206 @@ +# encoding=utf-8 +# Copyright © 2017 Dylan Baker +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import +import tempfile +import os +import shutil + +import gpg +import mock +from twisted.trial import unittest +from twisted.internet.defer import inlineCallbacks + +from alot import crypto +from alot import errors +from alot.commands import utils +from alot.db.envelope import Envelope + +from .. import utilities + +MOD_CLEAN = utilities.ModuleCleanup() + +# A useful single fingerprint for tests that only care about one key. This +# key will not be ambiguous +FPR = "F74091D4133F87D56B5D343C1974EC55FBC2D660" + +# Some additional keys, these keys may be ambigiuos +EXTRA_FPRS = [ + "DD19862809A7573A74058FF255937AFBB156245D", + "2071E9C8DB4EF5466F4D233CF730DF92C4566CE7", +] + +DEVNULL = open('/dev/null', 'w') +MOD_CLEAN.add_cleanup(DEVNULL.close) + + +@MOD_CLEAN.wrap_setup +def setUpModule(): + home = tempfile.mkdtemp() + MOD_CLEAN.add_cleanup(shutil.rmtree, home) + mock_home = mock.patch.dict(os.environ, {'GNUPGHOME': home}) + mock_home.start() + MOD_CLEAN.add_cleanup(mock_home.stop) + + with gpg.core.Context(armor=True) as ctx: + # Add the public and private keys. They have no password + search_dir = os.path.join(os.path.dirname(__file__), '../static/gpg-keys') + for each in os.listdir(search_dir): + if os.path.splitext(each)[1] == '.gpg': + with open(os.path.join(search_dir, each)) as f: + ctx.op_import(f) + + +@MOD_CLEAN.wrap_teardown +def tearDownModule(): + pass + + +class TestGetKeys(unittest.TestCase): + + # pylint: disable=protected-access + + @inlineCallbacks + def test_get_keys(self): + """Test that getting keys works when all keys are present.""" + expected = crypto.get_key(FPR, validate=True, encrypt=True, + signed_only=False) + ui = mock.Mock() + ids = [FPR] + actual = yield utils._get_keys(ui, ids) + self.assertIn(FPR, actual) + self.assertEqual(actual[FPR].fpr, expected.fpr) + + @inlineCallbacks + def test_get_keys_missing(self): + """Test that getting keys works when some keys are missing.""" + expected = crypto.get_key(FPR, validate=True, encrypt=True, + signed_only=False) + ui = mock.Mock() + ids = [FPR, "6F6B15509CF8E59E6E469F327F438280EF8D349F"] + actual = yield utils._get_keys(ui, ids) + self.assertIn(FPR, actual) + self.assertEqual(actual[FPR].fpr, expected.fpr) + + @inlineCallbacks + def test_get_keys_signed_only(self): + """Test gettings keys when signed only is required.""" + ui = mock.Mock() + ids = [FPR] + actual = yield utils._get_keys(ui, ids, signed_only=True) + self.assertEqual(actual, {}) + + @inlineCallbacks + def test_get_keys_ambiguous(self): + """Test gettings keys when when the key is ambiguous.""" + key = crypto.get_key(FPR, validate=True, encrypt=True, signed_only=False) + ui = mock.Mock() + + # Creat a ui.choice object that can satisfy twisted, but can also be + # queried for calls as a mock + @inlineCallbacks + def choice(*args, **kwargs): + yield None + ui.choice = mock.Mock(wraps=choice) + + ids = [FPR] + with mock.patch('alot.commands.utils.crypto.get_key', + mock.Mock(side_effect=errors.GPGProblem( + 'test', errors.GPGCode.AMBIGUOUS_NAME))): + with mock.patch('alot.commands.utils.crypto.list_keys', + mock.Mock(return_value=[key])): + yield utils._get_keys(ui, ids, signed_only=False) + ui.choice.assert_called_once() + + +class _Account(object): + def __init__(self, encrypt_to_self=True, gpg_key=None): + self.encrypt_to_self = encrypt_to_self + self.gpg_key = gpg_key + + +class TestSetEncrypt(unittest.TestCase): + + @inlineCallbacks + def test_get_keys_from_to(self): + ui = mock.Mock() + envelope = Envelope() + envelope['To'] = 'ambig@example.com, test@example.com' + yield utils.set_encrypt(ui, envelope) + self.assertTrue(envelope.encrypt) + self.assertEqual( + [f.fpr for f in envelope.encrypt_keys.itervalues()], + [crypto.get_key(FPR).fpr, crypto.get_key(EXTRA_FPRS[0]).fpr]) + + @inlineCallbacks + def test_get_keys_from_cc(self): + ui = mock.Mock() + envelope = Envelope() + envelope['Cc'] = 'ambig@example.com, test@example.com' + yield utils.set_encrypt(ui, envelope) + self.assertTrue(envelope.encrypt) + self.assertEqual( + [f.fpr for f in envelope.encrypt_keys.itervalues()], + [crypto.get_key(FPR).fpr, crypto.get_key(EXTRA_FPRS[0]).fpr]) + + @inlineCallbacks + def test_get_partial_keys(self): + ui = mock.Mock() + envelope = Envelope() + envelope['Cc'] = 'foo@example.com, test@example.com' + yield utils.set_encrypt(ui, envelope) + self.assertTrue(envelope.encrypt) + self.assertEqual( + [f.fpr for f in envelope.encrypt_keys.itervalues()], + [crypto.get_key(FPR).fpr]) + + @inlineCallbacks + def test_get_no_keys(self): + ui = mock.Mock() + envelope = Envelope() + envelope['To'] = 'foo@example.com' + yield utils.set_encrypt(ui, envelope) + self.assertFalse(envelope.encrypt) + self.assertEqual(envelope.encrypt_keys, {}) + + @inlineCallbacks + def test_encrypt_to_self_true(self): + ui = mock.Mock() + envelope = Envelope() + envelope['From'] = 'test@example.com' + envelope['To'] = 'ambig@example.com' + gpg_key = crypto.get_key(FPR) + account = _Account(encrypt_to_self=True, gpg_key=gpg_key) + with mock.patch('alot.commands.thread.settings.get_account_by_address', + mock.Mock(return_value=account)): + yield utils.set_encrypt(ui, envelope) + self.assertTrue(envelope.encrypt) + self.assertIn(FPR, envelope.encrypt_keys) + self.assertEqual(gpg_key, envelope.encrypt_keys[FPR]) + + @inlineCallbacks + def test_encrypt_to_self_false(self): + ui = mock.Mock() + envelope = Envelope() + envelope['From'] = 'test@example.com' + envelope['To'] = 'ambig@example.com' + gpg_key = crypto.get_key(FPR) + account = _Account(encrypt_to_self=False, gpg_key=gpg_key) + with mock.patch('alot.commands.thread.settings.get_account_by_address', + mock.Mock(return_value=account)): + yield utils.set_encrypt(ui, envelope) + self.assertTrue(envelope.encrypt) + self.assertNotIn(FPR, envelope.encrypt_keys) |