From 44b7929eadece0130d850b5bd040884a46ba48e5 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Tue, 22 Aug 2017 15:24:35 -0700 Subject: crypto: Remove try/except that only applied to pygpgme list_keys won't raise an exception if there are not keys, it will return a generator that creates an empty list: >>> from alot import crypto >>> list(crypto.list_keys('doesntexist@example.com')) [] --- alot/crypto.py | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/alot/crypto.py b/alot/crypto.py index 52eb8c58..f2526c36 100644 --- a/alot/crypto.py +++ b/alot/crypto.py @@ -81,31 +81,22 @@ def get_key(keyid, validate=False, encrypt=False, sign=False, valid_key = None - # Catching exceptions for list_keys - try: - for k in list_keys(hint=keyid): - try: - validate_key(k, encrypt=encrypt, sign=sign) - except GPGProblem: - # if the key is invalid for given action skip it - continue - - if valid_key: - # we have already found one valid key and now we find - # another? We really received an ambiguous keyid - raise GPGProblem( - "More than one key found matching this filter. " - "Please be more specific " - "(use a key ID like 4AC8EE1D).", - code=GPGCode.AMBIGUOUS_NAME) - valid_key = k - except gpg.errors.GPGMEError as e: - # This if will be triggered if there is no key matching at all. - if e.getcode() == gpg.errors.AMBIGUOUS_NAME: + for k in list_keys(hint=keyid): + try: + validate_key(k, encrypt=encrypt, sign=sign) + except GPGProblem: + # if the key is invalid for given action skip it + continue + + if valid_key: + # we have already found one valid key and now we find + # another? We really received an ambiguous keyid raise GPGProblem( - 'Can not find any key for "{}".'.format(keyid), - code=GPGCode.NOT_FOUND) - raise + "More than one key found matching this filter. " + "Please be more specific " + "(use a key ID like 4AC8EE1D).", + code=GPGCode.AMBIGUOUS_NAME) + valid_key = k if not valid_key: # there were multiple keys found but none of them are valid for -- cgit v1.2.3 From f26e1ff1c3d7a99ce0534e9ecebbf900b31825f7 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Tue, 22 Aug 2017 15:31:01 -0700 Subject: crypto: Don't cover the raise if other exception case --- alot/crypto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alot/crypto.py b/alot/crypto.py index f2526c36..f3dd6d11 100644 --- a/alot/crypto.py +++ b/alot/crypto.py @@ -111,7 +111,7 @@ def get_key(keyid, validate=False, encrypt=False, sign=False, 'Can not find usable key for "{}".'.format(keyid), code=GPGCode.NOT_FOUND) else: - raise e + raise e # pragma: nocover if signed_only and not check_uid_validity(key, keyid): raise GPGProblem('Cannot find a trusworthy key for "{}".'.format(keyid), code=GPGCode.NOT_FOUND) -- cgit v1.2.3 From 67301a23a14c943714ae1fd7bf6b4e21156e6ef5 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Tue, 22 Aug 2017 16:15:08 -0700 Subject: tests/db/utils: Add some more coverage to add_signature_headers There are a couple of pieces of this function that aren't covered, including a bug. yay bugs. --- tests/db/utils_test.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/tests/db/utils_test.py b/tests/db/utils_test.py index 26768597..ddb16786 100644 --- a/tests/db/utils_test.py +++ b/tests/db/utils_test.py @@ -21,6 +21,7 @@ import mock from alot import crypto from alot import helper from alot.db import utils +from alot.errors import GPGProblem from ..utilities import make_key, make_uid, TestCaseClassCleanup @@ -342,14 +343,14 @@ class TestAddSignatureHeaders(unittest.TestCase): def add_header(self, header, value): self.headers.append((header, value)) - def check(self, key, valid): + def check(self, key, valid, error_msg=u''): mail = self.FakeMail() with mock.patch('alot.db.utils.crypto.get_key', mock.Mock(return_value=key)), \ mock.patch('alot.db.utils.crypto.check_uid_validity', mock.Mock(return_value=valid)): - utils.add_signature_headers(mail, [mock.Mock(fpr='')], u'') + utils.add_signature_headers(mail, [mock.Mock(fpr='')], error_msg) return mail @@ -388,6 +389,31 @@ class TestAddSignatureHeaders(unittest.TestCase): self.assertIn( (utils.X_SIGNATURE_MESSAGE_HEADER, u'Valid: Andreá'), mail.headers) + def test_error_message_unicode(self): + mail = self.check(mock.Mock(), mock.Mock(), u'error message') + self.assertIn((utils.X_SIGNATURE_VALID_HEADER, u'False'), mail.headers) + self.assertIn( + (utils.X_SIGNATURE_MESSAGE_HEADER, u'Invalid: error message'), + mail.headers) + + def test_error_message_bytes(self): + mail = self.check(mock.Mock(), mock.Mock(), b'error message') + self.assertIn((utils.X_SIGNATURE_VALID_HEADER, u'False'), mail.headers) + self.assertIn( + (utils.X_SIGNATURE_MESSAGE_HEADER, u'Invalid: error message'), + mail.headers) + + @unittest.expectedFailure + def test_get_key_fails(self): + mail = self.FakeMail() + with mock.patch('alot.db.utils.crypto.get_key', + mock.Mock(side_effect=GPGProblem(u'', 0))): + utils.add_signature_headers(mail, [mock.Mock(fpr='')], u'') + self.assertIn((utils.X_SIGNATURE_VALID_HEADER, u'False'), mail.headers) + self.assertIn( + (utils.X_SIGNATURE_MESSAGE_HEADER, u'Untrusted: '), + mail.headers) + class TestMessageFromFile(TestCaseClassCleanup): -- cgit v1.2.3 From 10a339e57ccdf49a95ded1f1a3961a1cfcd65dee Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Tue, 22 Aug 2017 16:24:53 -0700 Subject: db/utils: Don't mark signature valid if there is no key to verify --- alot/db/utils.py | 13 +++++++------ tests/db/utils_test.py | 1 - 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/alot/db/utils.py b/alot/db/utils.py index 488e0ed7..a91a8fae 100644 --- a/alot/db/utils.py +++ b/alot/db/utils.py @@ -45,6 +45,8 @@ def add_signature_headers(mail, sigs, error_msg): string indicating no error ''' sig_from = u'' + sig_known = True + uid_trusted = False if isinstance(error_msg, str): error_msg = error_msg.decode('utf-8') @@ -60,13 +62,11 @@ def add_signature_headers(mail, sigs, error_msg): uid_trusted = True break else: - # No trusted uid found, we did not break but drop from the - # for loop. - uid_trusted = False + # No trusted uid found, since we did not break from the loop. sig_from = key.uids[0].uid.decode('utf-8') - except: + except GPGProblem: sig_from = sigs[0].fpr.decode('utf-8') - uid_trusted = False + sig_known = False if error_msg: msg = u'Invalid: {}'.format(error_msg) @@ -75,7 +75,8 @@ def add_signature_headers(mail, sigs, error_msg): else: msg = u'Untrusted: {}'.format(sig_from) - mail.add_header(X_SIGNATURE_VALID_HEADER, 'False' if error_msg else 'True') + mail.add_header(X_SIGNATURE_VALID_HEADER, + 'False' if (error_msg or not sig_known) else 'True') mail.add_header(X_SIGNATURE_MESSAGE_HEADER, msg) diff --git a/tests/db/utils_test.py b/tests/db/utils_test.py index ddb16786..af5f2517 100644 --- a/tests/db/utils_test.py +++ b/tests/db/utils_test.py @@ -403,7 +403,6 @@ class TestAddSignatureHeaders(unittest.TestCase): (utils.X_SIGNATURE_MESSAGE_HEADER, u'Invalid: error message'), mail.headers) - @unittest.expectedFailure def test_get_key_fails(self): mail = self.FakeMail() with mock.patch('alot.db.utils.crypto.get_key', -- cgit v1.2.3 From aeed119b86c6d1acb13b103eeafb6bf775d99762 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Tue, 22 Aug 2017 16:29:49 -0700 Subject: db/utils: Update docstring for extract_body add returns and rtype for sphinx and reflow the rest of the docstring a bit. --- alot/db/utils.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/alot/db/utils.py b/alot/db/utils.py index a91a8fae..34f86991 100644 --- a/alot/db/utils.py +++ b/alot/db/utils.py @@ -298,16 +298,20 @@ def extract_headers(mail, headers=None): def extract_body(mail, types=None, field_key='copiousoutput'): - """ - returns a body text string for given mail. - If types is `None`, `text/*` is used: - The exact preferred type is specified by the prefer_plaintext config option - which defaults to text/html. + """Returns a string view of a Message. + + If the `types` argument is set then any encoding types there will be used + as the prefered encoding to extract. If `types` is None then + :ref:`prefer_plaintext ` will be consulted; if it is True + then text/plain parts will be returned, if it is false then text/html will + be returned if present or text/plain if there are no text/html parts. :param mail: the mail to use :type mail: :class:`email.Message` :param types: mime content types to use for body string - :type types: list of str + :type types: list[str] + :returns: The combined text of any parts to be used + :rtype: str """ preferred = 'text/plain' if settings.get( -- cgit v1.2.3 From 599dfa61c82c3192c6ac9ef59a8950225e4575d0 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Wed, 23 Aug 2017 09:05:37 -0700 Subject: tests/db/utils: Add tests for extract_body function there are probably still some corners with mailcap handling (parameters like copiousoutput) that are untested, but this covers a large swath of the functionality. --- tests/db/utils_test.py | 121 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/tests/db/utils_test.py b/tests/db/utils_test.py index af5f2517..03db8500 100644 --- a/tests/db/utils_test.py +++ b/tests/db/utils_test.py @@ -1,5 +1,6 @@ # encoding: utf-8 # Copyright (C) 2017 Lucas Hoffmann +# Copyright © 2017 Dylan Baker # This file is released under the GNU GPL, version 3 or a later revision. # For further details see the COPYING file from __future__ import absolute_import @@ -650,3 +651,123 @@ class TestMessageFromFile(TestCaseClassCleanup): self.assertIn('This is some text', [n.get_payload() for n in m.walk()]) self.assertIn(utils.X_SIGNATURE_VALID_HEADER, m) self.assertIn(utils.X_SIGNATURE_MESSAGE_HEADER, m) + + +class TestExtractBody(unittest.TestCase): + + @staticmethod + def _set_basic_headers(mail): + mail['Subject'] = 'Test email' + mail['To'] = 'foo@example.com' + mail['From'] = 'bar@example.com' + + def test_single_text_plain(self): + mail = email.mime.text.MIMEText('This is an email') + self._set_basic_headers(mail) + actual = utils.extract_body(mail) + + expected = 'This is an email' + + self.assertEqual(actual, expected) + + def test_two_text_plain(self): + mail = email.mime.multipart.MIMEMultipart() + self._set_basic_headers(mail) + mail.attach(email.mime.text.MIMEText('This is an email')) + mail.attach(email.mime.text.MIMEText('This is a second part')) + + actual = utils.extract_body(mail) + expected = 'This is an email\n\nThis is a second part' + + self.assertEqual(actual, expected) + + def test_text_plain_and_other(self): + mail = email.mime.multipart.MIMEMultipart() + self._set_basic_headers(mail) + mail.attach(email.mime.text.MIMEText('This is an email')) + mail.attach(email.mime.application.MIMEApplication(b'1')) + + actual = utils.extract_body(mail) + expected = 'This is an email' + + self.assertEqual(actual, expected) + + def test_text_plain_with_attachment_text(self): + mail = email.mime.multipart.MIMEMultipart() + self._set_basic_headers(mail) + mail.attach(email.mime.text.MIMEText('This is an email')) + attachment = email.mime.text.MIMEText('this shouldnt be displayed') + attachment['Content-Disposition'] = 'attachment' + mail.attach(attachment) + + actual = utils.extract_body(mail) + expected = 'This is an email' + + self.assertEqual(actual, expected) + + def _make_mixed_plain_html(self): + mail = email.mime.multipart.MIMEMultipart() + self._set_basic_headers(mail) + mail.attach(email.mime.text.MIMEText('This is an email')) + mail.attach(email.mime.text.MIMEText( + 'This is an html email', + 'html')) + return mail + + @mock.patch('alot.db.utils.settings.get', mock.Mock(return_value=True)) + def test_prefer_plaintext(self): + expected = 'This is an email' + mail = self._make_mixed_plain_html() + actual = utils.extract_body(mail) + + self.assertEqual(actual, expected) + + # Mock the handler to cat, so that no transformations of the html are made + # making the result non-deterministic + @mock.patch('alot.db.utils.settings.get', mock.Mock(return_value=False)) + @mock.patch('alot.db.utils.settings.mailcap_find_match', + mock.Mock(return_value=(None, {'view': 'cat'}))) + def test_prefer_html(self): + expected = 'This is an html email' + mail = self._make_mixed_plain_html() + actual = utils.extract_body(mail) + + self.assertEqual(actual, expected) + + @mock.patch('alot.db.utils.settings.get', mock.Mock(return_value=False)) + @mock.patch('alot.db.utils.settings.mailcap_find_match', + mock.Mock(return_value=(None, {'view': 'cat'}))) + def test_types_provided(self): + # This should not return html, even though html is set to preferred + # since a types variable is passed + expected = 'This is an email' + mail = self._make_mixed_plain_html() + actual = utils.extract_body(mail, types=['text/plain']) + + self.assertEqual(actual, expected) + + @mock.patch('alot.db.utils.settings.mailcap_find_match', + mock.Mock(return_value=(None, {'view': 'cat'}))) + def test_require_mailcap_stdin(self): + mail = email.mime.multipart.MIMEMultipart() + self._set_basic_headers(mail) + mail.attach(email.mime.text.MIMEText( + 'This is an html email', + 'html')) + actual = utils.extract_body(mail) + expected = 'This is an html email' + + self.assertEqual(actual, expected) + + @mock.patch('alot.db.utils.settings.mailcap_find_match', + mock.Mock(return_value=(None, {'view': 'cat %s'}))) + def test_require_mailcap_file(self): + mail = email.mime.multipart.MIMEMultipart() + self._set_basic_headers(mail) + mail.attach(email.mime.text.MIMEText( + 'This is an html email', + 'html')) + actual = utils.extract_body(mail) + expected = 'This is an html email' + + self.assertEqual(actual, expected) -- cgit v1.2.3 From e16e846db354e5775c69891c2b58b12e110f79b8 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Wed, 23 Aug 2017 09:08:39 -0700 Subject: tests/db/utils: Add tests for message_from_string --- tests/db/utils_test.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/db/utils_test.py b/tests/db/utils_test.py index 03db8500..3e7ef9d3 100644 --- a/tests/db/utils_test.py +++ b/tests/db/utils_test.py @@ -771,3 +771,21 @@ class TestExtractBody(unittest.TestCase): expected = 'This is an html email' self.assertEqual(actual, expected) + + +class TestMessageFromString(unittest.TestCase): + + """Tests for message_from_string. + + Because the implementation is that this is a wrapper around + message_from_file, it's not important to have a large swath of tests, just + enough to show that things are being passed correctly. + """ + + def test(self): + m = email.mime.text.MIMEText(u'This is some text', 'plain', 'utf-8') + m['Subject'] = 'test' + m['From'] = 'me' + m['To'] = 'Nobody' + message = utils.message_from_string(m.as_string()) + self.assertEqual(message.get_payload(), 'This is some text') -- cgit v1.2.3