summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Totzke <patricktotzke@gmail.com>2017-09-26 15:57:24 +0100
committerGitHub <noreply@github.com>2017-09-26 15:57:24 +0100
commit0dcb12db64eda70d59fc2747ce322a7263edcb73 (patch)
tree438b947a473fb142b8894f0da31b23f4411c4b53
parent018aa2e339f9cb3bb4af1d26cce31cbad003409d (diff)
parent1b3f12f4e3a7bd6bbfe06e42b41658484a291174 (diff)
Merge pull request #1160 from meskio/fix-1140
Fix #1140
-rw-r--r--alot/account.py7
-rw-r--r--alot/commands/envelope.py7
-rw-r--r--alot/commands/utils.py15
-rw-r--r--alot/crypto.py3
-rw-r--r--alot/defaults/alot.rc.spec11
-rw-r--r--docs/source/configuration/accounts_table18
-rw-r--r--tests/commands/utils_tests.py206
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)