summaryrefslogtreecommitdiff
path: root/src/leap/mail/imap
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/mail/imap')
-rw-r--r--src/leap/mail/imap/fetch.py312
-rw-r--r--src/leap/mail/imap/tests/test_incoming_mail.py38
2 files changed, 189 insertions, 161 deletions
diff --git a/src/leap/mail/imap/fetch.py b/src/leap/mail/imap/fetch.py
index 01373be..dbc726a 100644
--- a/src/leap/mail/imap/fetch.py
+++ b/src/leap/mail/imap/fetch.py
@@ -36,7 +36,6 @@ from twisted.internet import defer, reactor
from twisted.internet.task import LoopingCall
from twisted.internet.task import deferLater
from u1db import errors as u1db_errors
-from zope.proxy import sameProxiedObjects
from leap.common import events as leap_events
from leap.common.check import leap_assert, leap_assert_type
@@ -138,13 +137,6 @@ class LeapIncomingMail(object):
# initialize a mail parser only once
self._parser = Parser()
- @property
- def _pkey(self):
- if sameProxiedObjects(self._keymanager, None):
- logger.warning('tried to get key, but null keymanager found')
- return None
- return self._keymanager.get_key(self._userid, OpenPGPKey, private=True)
-
#
# Public API: fetch, start_loop, stop.
#
@@ -312,40 +304,46 @@ class LeapIncomingMail(object):
:param doc: A document containing an encrypted message.
:type doc: SoledadDocument
- :return: A tuple containing the document and the decrypted message.
- :rtype: (SoledadDocument, str)
+ :return: A Deferred that will be fired with the document and the
+ decrypted message.
+ :rtype: SoledadDocument, str
"""
log.msg('decrypting msg')
- success = False
- try:
- decrdata = self._keymanager.decrypt(
- doc.content[ENC_JSON_KEY],
- self._pkey)
- success = True
- except Exception as exc:
- # XXX move this to errback !!!
- logger.error("Error while decrypting msg: %r" % (exc,))
- decrdata = ""
- leap_events.signal(IMAP_MSG_DECRYPTED, "1" if success else "0")
+ def process_decrypted(res):
+ if isinstance(res, tuple):
+ decrdata, _ = res
+ success = True
+ else:
+ decrdata = ""
+ success = False
- data = self._process_decrypted_doc((doc, decrdata))
- return (doc, data)
+ leap_events.signal(IMAP_MSG_DECRYPTED, "1" if success else "0")
- def _process_decrypted_doc(self, msgtuple):
+ data = self._process_decrypted_doc(doc, decrdata)
+ return doc, data
+
+ d = self._keymanager.decrypt(
+ doc.content[ENC_JSON_KEY],
+ self._userid, OpenPGPKey)
+ d.addErrback(self._errback)
+ d.addCallback(process_decrypted)
+ return d
+
+ def _process_decrypted_doc(self, doc, data):
"""
Process a document containing a succesfully decrypted message.
- :param msgtuple: a tuple consisting of a SoledadDocument
- instance containing the incoming message
- and data, the json-encoded, decrypted content of the
- incoming message
- :type msgtuple: (SoledadDocument, str)
+ :param doc: the incoming message
+ :type doc: SoledadDocument
+ :param data: the json-encoded, decrypted content of the incoming
+ message
+ :type data: str
+
:return: the processed data.
:rtype: str
"""
log.msg('processing decrypted doc')
- doc, data = msgtuple
# XXX turn this into an errBack for each one of
# the deferreds that would process an individual document
@@ -421,45 +419,40 @@ class LeapIncomingMail(object):
encoding = get_email_charset(data)
msg = self._parser.parsestr(data)
- # try to obtain sender public key
- senderPubkey = None
fromHeader = msg.get('from', None)
+ senderAddress = None
if (fromHeader is not None
and (msg.get_content_type() == MULTIPART_ENCRYPTED
or msg.get_content_type() == MULTIPART_SIGNED)):
- _, senderAddress = parseaddr(fromHeader)
- try:
- senderPubkey = self._keymanager.get_key(
- senderAddress, OpenPGPKey)
- except keymanager_errors.KeyNotFound:
- pass
-
- valid_sig = False # we will add a header saying if sig is valid
- decrypt_multi = self._decrypt_multipart_encrypted_msg
- decrypt_inline = self._maybe_decrypt_inline_encrypted_msg
+ senderAddress = parseaddr(fromHeader)
+
+ def add_leap_header(decrmsg, signkey):
+ if (senderAddress is None or
+ isinstance(signkey, keymanager_errors.KeyNotFound)):
+ decrmsg.add_header(
+ self.LEAP_SIGNATURE_HEADER,
+ self.LEAP_SIGNATURE_COULD_NOT_VERIFY)
+ elif isinstance(signkey, keymanager_errors.InvalidSignature):
+ decrmsg.add_header(
+ self.LEAP_SIGNATURE_HEADER,
+ self.LEAP_SIGNATURE_INVALID)
+ else:
+ decrmsg.add_header(
+ self.LEAP_SIGNATURE_HEADER,
+ self.LEAP_SIGNATURE_VALID,
+ pubkey=signkey.key_id)
+ return decrmsg.as_string()
if msg.get_content_type() == MULTIPART_ENCRYPTED:
- decrmsg, valid_sig = decrypt_multi(
- msg, encoding, senderPubkey)
+ d = self._decrypt_multipart_encrypted_msg(
+ msg, encoding, senderAddress)
else:
- decrmsg, valid_sig = decrypt_inline(
- msg, encoding, senderPubkey)
-
- # add x-leap-signature header
- if senderPubkey is None:
- decrmsg.add_header(
- self.LEAP_SIGNATURE_HEADER,
- self.LEAP_SIGNATURE_COULD_NOT_VERIFY)
- else:
- decrmsg.add_header(
- self.LEAP_SIGNATURE_HEADER,
- self.LEAP_SIGNATURE_VALID if valid_sig else
- self.LEAP_SIGNATURE_INVALID,
- pubkey=senderPubkey.key_id)
-
- return decrmsg.as_string()
+ d = self._maybe_decrypt_inline_encrypted_msg(
+ msg, encoding, senderAddress)
+ d.addCallback(add_leap_header)
+ return d
- def _decrypt_multipart_encrypted_msg(self, msg, encoding, senderPubkey):
+ def _decrypt_multipart_encrypted_msg(self, msg, encoding, senderAddress):
"""
Decrypt a message with content-type 'multipart/encrypted'.
@@ -467,12 +460,13 @@ class LeapIncomingMail(object):
:type msg: Message
:param encoding: The encoding of the email message.
:type encoding: str
- :param senderPubkey: The key of the sender of the message.
- :type senderPubkey: OpenPGPKey
+ :param senderAddress: The email address of the sender of the message.
+ :type senderAddress: str
- :return: A tuple containing a decrypted message and
- a bool indicating whether the signature is valid.
- :rtype: (Message, bool)
+ :return: A Deferred that will be fired with a tuple containing a
+ decrypted Message and the signing OpenPGPKey if the signature
+ is valid or InvalidSignature or KeyNotFound.
+ :rtype: Deferred
"""
log.msg('decrypting multipart encrypted msg')
msg = copy.deepcopy(msg)
@@ -483,33 +477,33 @@ class LeapIncomingMail(object):
encdata = pgpencmsg.get_payload()
# decrypt or fail gracefully
- try:
- decrdata, valid_sig = self._decrypt_and_verify_data(
- encdata, senderPubkey)
- except keymanager_errors.DecryptError as e:
- logger.warning('Failed to decrypt encrypted message (%s). '
- 'Storing message without modifications.' % str(e))
- # Bailing out!
- return (msg, False)
+ def build_msg(res):
+ decrdata, signkey = res
- decrmsg = self._parser.parsestr(decrdata)
- # remove original message's multipart/encrypted content-type
- del(msg['content-type'])
+ decrmsg = self._parser.parsestr(decrdata)
+ # remove original message's multipart/encrypted content-type
+ del(msg['content-type'])
- # replace headers back in original message
- for hkey, hval in decrmsg.items():
- try:
- # this will raise KeyError if header is not present
- msg.replace_header(hkey, hval)
- except KeyError:
- msg[hkey] = hval
+ # replace headers back in original message
+ for hkey, hval in decrmsg.items():
+ try:
+ # this will raise KeyError if header is not present
+ msg.replace_header(hkey, hval)
+ except KeyError:
+ msg[hkey] = hval
+
+ # all ok, replace payload by unencrypted payload
+ msg.set_payload(decrmsg.get_payload())
+ return (msg, signkey)
- # all ok, replace payload by unencrypted payload
- msg.set_payload(decrmsg.get_payload())
- return (msg, valid_sig)
+ d = self._keymanager.decrypt(
+ encdata, self._userid, OpenPGPKey,
+ verify=senderAddress)
+ d.addCallbacks(build_msg, self._decryption_error, errbackArgs=(msg,))
+ return d
def _maybe_decrypt_inline_encrypted_msg(self, origmsg, encoding,
- senderPubkey):
+ senderAddress):
"""
Possibly decrypt an inline OpenPGP encrypted message.
@@ -517,12 +511,13 @@ class LeapIncomingMail(object):
:type origmsg: Message
:param encoding: The encoding of the email message.
:type encoding: str
- :param senderPubkey: The key of the sender of the message.
- :type senderPubkey: OpenPGPKey
+ :param senderAddress: The email address of the sender of the message.
+ :type senderAddress: str
- :return: A tuple containing a decrypted message and
- a bool indicating whether the signature is valid.
- :rtype: (Message, bool)
+ :return: A Deferred that will be fired with a tuple containing a
+ decrypted Message and the signing OpenPGPKey if the signature
+ is valid or InvalidSignature or KeyNotFound.
+ :rtype: Deferred
"""
log.msg('maybe decrypting inline encrypted msg')
# serialize the original message
@@ -530,54 +525,48 @@ class LeapIncomingMail(object):
g = Generator(buf)
g.flatten(origmsg)
data = buf.getvalue()
+
+ def decrypted_data(res):
+ decrdata, signkey = res
+ return data.replace(pgp_message, decrdata), signkey
+
+ def encode_and_return(res):
+ data, signkey = res
+ if isinstance(data, unicode):
+ data = data.encode(encoding, 'replace')
+ return (self._parser.parsestr(data), signkey)
+
# handle exactly one inline PGP message
- valid_sig = False
if PGP_BEGIN in data:
begin = data.find(PGP_BEGIN)
end = data.find(PGP_END)
pgp_message = data[begin:end + len(PGP_END)]
- try:
- decrdata, valid_sig = self._decrypt_and_verify_data(
- pgp_message, senderPubkey)
- # replace encrypted by decrypted content
- data = data.replace(pgp_message, decrdata)
- except keymanager_errors.DecryptError:
- logger.warning('Failed to decrypt potential inline encrypted '
- 'message. Storing message as is...')
-
- # if message is not encrypted, return raw data
- if isinstance(data, unicode):
- data = data.encode(encoding, 'replace')
- return (self._parser.parsestr(data), valid_sig)
+ d = self._keymanager.decrypt(
+ pgp_message, self._userid, OpenPGPKey,
+ verify=senderAddress)
+ d.addCallbacks(decrypted_data, self._decryption_error,
+ errbackArgs=(data,))
+ else:
+ d = defer.succeed((data, None))
+ d.addCallback(encode_and_return)
+ return d
- def _decrypt_and_verify_data(self, data, senderPubkey):
+ def _decryption_error(self, failure, msg):
"""
- Decrypt C{data} using our private key and attempt to verify a
- signature using C{senderPubkey}.
-
- :param data: The text to be decrypted.
- :type data: unicode
- :param senderPubkey: The public key of the sender of the message.
- :type senderPubkey: OpenPGPKey
-
- :return: The decrypted data and a boolean stating whether the
- signature could be verified.
- :rtype: (str, bool)
-
- :raise DecryptError: Raised if failed to decrypt.
+ Check for known decryption errors
"""
- log.msg('decrypting and verifying data')
- valid_sig = False
- try:
- decrdata = self._keymanager.decrypt(
- data, self._pkey,
- verify=senderPubkey)
- if senderPubkey is not None:
- valid_sig = True
- except keymanager_errors.InvalidSignature:
- decrdata = self._keymanager.decrypt(
- data, self._pkey)
- return (decrdata, valid_sig)
+ if failure.check(keymanager_errors.DecryptError):
+ logger.warning('Failed to decrypt encrypted message (%s). '
+ 'Storing message without modifications.'
+ % str(failure.value))
+ return (msg, None)
+ elif failure.check(keymanager_errors.KeyNotFound):
+ logger.error('Failed to find private key for decryption (%s). '
+ 'Storing message without modifications.'
+ % str(failure.value))
+ return (msg, None)
+ else:
+ return failure
def _extract_keys(self, msgtuple):
"""
@@ -592,6 +581,10 @@ class LeapIncomingMail(object):
and data, the json-encoded, decrypted content of the
incoming message
:type msgtuple: (SoledadDocument, str)
+
+ :return: A Deferred that will be fired with msgtuple when key
+ extraction finishes
+ :rtype: Deferred
"""
OpenPGP_HEADER = 'OpenPGP'
doc, data = msgtuple
@@ -603,13 +596,17 @@ class LeapIncomingMail(object):
_, fromAddress = parseaddr(msg['from'])
header = msg.get(OpenPGP_HEADER, None)
+ dh = defer.success()
if header is not None:
- self._extract_openpgp_header(header, fromAddress)
+ dh = self._extract_openpgp_header(header, fromAddress)
+ da = defer.success()
if msg.is_multipart():
- self._extract_attached_key(msg.get_payload(), fromAddress)
+ da = self._extract_attached_key(msg.get_payload(), fromAddress)
- return msgtuple
+ d = defer.gatherResults([dh, da])
+ d.addCallback(lambda _: msgtuple)
+ return d
def _extract_openpgp_header(self, header, address):
"""
@@ -619,7 +616,11 @@ class LeapIncomingMail(object):
:type header: str
:param address: email address in the from header
:type address: str
+
+ :return: A Deferred that will be fired when header extraction is done
+ :rtype: Deferred
"""
+ d = defer.success()
fields = dict([f.strip(' ').split('=') for f in header.split(';')])
if 'url' in fields:
url = shlex.split(fields['url'])[0] # remove quotations
@@ -627,21 +628,28 @@ class LeapIncomingMail(object):
addressHostname = address.split('@')[1]
if (urlparts.scheme == 'https'
and urlparts.hostname == addressHostname):
- try:
- self._keymanager.fetch_key(address, url, OpenPGPKey)
- logger.info("Imported key from header %s" % (url,))
- except keymanager_errors.KeyNotFound:
- logger.warning("Url from OpenPGP header %s failed"
- % (url,))
- except keymanager_errors.KeyAttributesDiffer:
- logger.warning("Key from OpenPGP header url %s didn't "
- "match the from address %s"
- % (url, address))
+ def fetch_error(failure):
+ if failure.check(keymanager_errors.KeyNotFound):
+ logger.warning("Url from OpenPGP header %s failed"
+ % (url,))
+ elif failure.check(keymanager_errors.KeyAttributesDiffer):
+ logger.warning("Key from OpenPGP header url %s didn't "
+ "match the from address %s"
+ % (url, address))
+ else:
+ return failure
+
+ d = self._keymanager.fetch_key(address, url, OpenPGPKey)
+ d.addCallback(
+ lambda _:
+ logger.info("Imported key from header %s" % (url,)))
+ d.addErrback(fetch_error)
else:
logger.debug("No valid url on OpenPGP header %s" % (url,))
else:
logger.debug("There is no url on the OpenPGP header: %s"
% (header,))
+ return d
def _extract_attached_key(self, attachments, address):
"""
@@ -651,16 +659,22 @@ class LeapIncomingMail(object):
:type attachments: list(email.Message)
:param address: email address in the from header
:type address: str
+
+ :return: A Deferred that will be fired when all the keys are stored
+ :rtype: Deferred
"""
MIME_KEY = "application/pgp-keys"
+ deferreds = []
for attachment in attachments:
if MIME_KEY == attachment.get_content_type():
logger.debug("Add key from attachment")
- self._keymanager.put_raw_key(
+ d = self._keymanager.put_raw_key(
attachment.get_payload(),
OpenPGPKey,
address=address)
+ deferreds.append(d)
+ return defer.gatherResults(deferreds)
def _add_message_locally(self, msgtuple):
"""
@@ -672,6 +686,9 @@ class LeapIncomingMail(object):
and data, the json-encoded, decrypted content of the
incoming message
:type msgtuple: (SoledadDocument, str)
+
+ :return: A Deferred that will be fired when the messages is stored
+ :rtype: Defferred
"""
doc, data = msgtuple
log.msg('adding message %s to local db' % (doc.doc_id,))
@@ -690,6 +707,7 @@ class LeapIncomingMail(object):
d = self._inbox.addMessage(data, flags=(self.RECENT_FLAG,),
notify_on_disk=True)
d.addCallbacks(msgSavedCallback, self._errback)
+ return d
#
# helpers
diff --git a/src/leap/mail/imap/tests/test_incoming_mail.py b/src/leap/mail/imap/tests/test_incoming_mail.py
index ce6d56a..03c0164 100644
--- a/src/leap/mail/imap/tests/test_incoming_mail.py
+++ b/src/leap/mail/imap/tests/test_incoming_mail.py
@@ -28,7 +28,6 @@ from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.parser import Parser
from mock import Mock
-from twisted.trial import unittest
from leap.keymanager.openpgp import OpenPGPKey
from leap.mail.imap.account import SoledadBackedAccount
@@ -48,7 +47,7 @@ from leap.soledad.common.crypto import (
)
-class LeapIncomingMailTestCase(TestCaseWithKeyManager, unittest.TestCase):
+class LeapIncomingMailTestCase(TestCaseWithKeyManager):
"""
Tests for the incoming mail parser
"""
@@ -147,31 +146,42 @@ subject: independence of cyberspace
key = MIMEApplication("", "pgp-keys")
key.set_payload(KEY)
message.attach(key)
- email = self._create_incoming_email(message.as_string())
- self._mock_soledad_get_from_index(fields.JUST_MAIL_IDX, [email])
- self.fetcher._keymanager.put_raw_key = Mock()
def put_raw_key_called(ret):
self.fetcher._keymanager.put_raw_key.assert_called_once_with(
KEY, OpenPGPKey, address=self.FROM_ADDRESS)
- d = self.fetcher.fetch()
+ d = self.mock_fetch(message.as_string())
d.addCallback(put_raw_key_called)
return d
+ def _mock_fetch(self, message):
+ self.fetcher._keymanager.fetch_key = Mock()
+ d = self._create_incoming_email(message)
+ d.addCallback(
+ lambda email:
+ self._mock_soledad_get_from_index(fields.JUST_MAIL_IDX, [email]))
+ d.addCallback(lambda _: self.fetcher.fetch())
+ return d
+
def _create_incoming_email(self, email_str):
email = SoledadDocument()
- pubkey = self._km.get_key(ADDRESS, OpenPGPKey)
data = json.dumps(
{"incoming": True, "content": email_str},
ensure_ascii=False)
- email.content = {
- fields.INCOMING_KEY: True,
- fields.ERROR_DECRYPTING_KEY: False,
- ENC_SCHEME_KEY: EncryptionSchemes.PUBKEY,
- ENC_JSON_KEY: str(self._km.encrypt(data, pubkey))
- }
- return email
+
+ def set_email_content(pubkey):
+ email.content = {
+ fields.INCOMING_KEY: True,
+ fields.ERROR_DECRYPTING_KEY: False,
+ ENC_SCHEME_KEY: EncryptionSchemes.PUBKEY,
+ ENC_JSON_KEY: str(self._km.encrypt(data, pubkey))
+ }
+ return email
+
+ d = self._km.get_key(ADDRESS, OpenPGPKey)
+ d.addCallback(set_email_content)
+ return d
def _mock_soledad_get_from_index(self, index_name, value):
get_from_index = self._soledad.get_from_index