diff options
-rw-r--r-- | mail/CHANGELOG | 10 | ||||
-rw-r--r-- | mail/pkg/requirements.pip | 2 | ||||
-rw-r--r-- | mail/setup.py | 77 | ||||
-rw-r--r-- | mail/src/leap/mail/__init__.py | 9 | ||||
-rw-r--r-- | mail/src/leap/mail/imap/fetch.py | 65 | ||||
-rw-r--r-- | mail/src/leap/mail/imap/server.py | 98 | ||||
-rw-r--r-- | mail/src/leap/mail/imap/service/imap.py | 12 | ||||
-rw-r--r-- | mail/src/leap/mail/load_tests.py | 32 | ||||
-rw-r--r-- | mail/src/leap/mail/smtp/smtprelay.py | 31 | ||||
-rw-r--r-- | mail/src/leap/mail/smtp/tests/__init__.py | 4 | ||||
-rw-r--r-- | mail/src/leap/mail/smtp/tests/cert/server.crt | 29 | ||||
-rw-r--r-- | mail/src/leap/mail/smtp/tests/cert/server.key | 51 | ||||
-rw-r--r-- | mail/src/leap/mail/smtp/tests/test_smtprelay.py | 59 |
13 files changed, 388 insertions, 91 deletions
diff --git a/mail/CHANGELOG b/mail/CHANGELOG index 319fda59..5755e593 100644 --- a/mail/CHANGELOG +++ b/mail/CHANGELOG @@ -1,3 +1,13 @@ +0.3.6 Nov 1: + o Add support for non-ascii characters in emails. Closes #4000. + o Default to UTF-8 when there is no charset parsed from the mail + contents. + o Refactor get_email_charset to leap.common. + o Return the necessary references (factory, port) from IMAP4 launch + in order to be able to properly stop it. Related to #4199. + o Notify MUA of new mail, using IDLE as advertised. Closes: #3671 + o Use TLS wrapper mode instead of STARTTLS. Closes #3637. + 0.3.5 Oct 18: o Do not log mail doc contents. o Comply with RFC 3156. Closes #4029. diff --git a/mail/pkg/requirements.pip b/mail/pkg/requirements.pip index 6fa0df47..4780b5c7 100644 --- a/mail/pkg/requirements.pip +++ b/mail/pkg/requirements.pip @@ -1,4 +1,4 @@ leap.soledad.client>=0.3.0 -leap.common>=0.3.0 +leap.common>=0.3.5 leap.keymanager>=0.3.4 twisted # >= 12.0.3 ?? diff --git a/mail/setup.py b/mail/setup.py index f423f7b7..499a9ee9 100644 --- a/mail/setup.py +++ b/mail/setup.py @@ -17,6 +17,7 @@ """ Setup file for leap.mail """ +import re from setuptools import setup from setuptools import find_packages @@ -46,25 +47,89 @@ trove_classifiers = [ 'Topic :: Software Development :: Libraries', ] +DOWNLOAD_BASE = ('https://github.com/leapcode/leap_mail/' + 'archive/%s.tar.gz') +_versions = versioneer.get_versions() +VERSION = _versions['version'] +VERSION_FULL = _versions['full'] +DOWNLOAD_URL = "" + +# get the short version for the download url +_version_short = re.findall('\d+\.\d+\.\d+', VERSION) +if len(_version_short) > 0: + VERSION_SHORT = _version_short[0] + DOWNLOAD_URL = DOWNLOAD_BASE % VERSION_SHORT + +cmdclass = versioneer.get_cmdclass() + + +from setuptools import Command + + +class freeze_debianver(Command): + """ + Freezes the version in a debian branch. + To be used after merging the development branch onto the debian one. + """ + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + proceed = str(raw_input( + "This will overwrite the file _version.py. Continue? [y/N] ")) + if proceed != "y": + print("He. You scared. Aborting.") + return + template = r""" +# This file was generated by the `freeze_debianver` command in setup.py +# Using 'versioneer.py' (0.7+) from +# revision-control system data, or from the parent directory name of an +# unpacked source archive. Distribution tarballs contain a pre-generated copy +# of this file. + +version_version = '{version}' +version_full = '{version_full}' +""" + templatefun = r""" + +def get_versions(default={}, verbose=False): + return {'version': version_version, 'full': version_full} +""" + subst_template = template.format( + version=VERSION_SHORT, + version_full=VERSION_FULL) + templatefun + with open(versioneer.versionfile_source, 'w') as f: + f.write(subst_template) + + +cmdclass["freeze_debianver"] = freeze_debianver + # XXX add ref to docs setup( name='leap.mail', - version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass(), + version=VERSION, + cmdclass=cmdclass, url='https://leap.se/', + download_url=DOWNLOAD_URL, license='GPLv3+', author='The LEAP Encryption Access Project', author_email='info@leap.se', + maintainer='Kali Kaneko', + maintainer_email='kali@leap.se', description='Mail Services provided by Bitmask, the LEAP Client.', - long_description=( - "Mail Services provided by Bitmask, the LEAP Client." - ), + long_description=open('README.rst').read() + '\n\n\n' + + open('CHANGELOG').read(), classifiers=trove_classifiers, namespace_packages=["leap"], package_dir={'': 'src'}, packages=find_packages('src'), - test_suite='leap.mail.load_tests', + test_suite='leap.mail.load_tests.load_tests', install_requires=utils.parse_requirements(), tests_require=utils.parse_requirements( reqfiles=['pkg/requirements-testing.pip']), diff --git a/mail/src/leap/mail/__init__.py b/mail/src/leap/mail/__init__.py index 5b5ba9ba..4b25fe62 100644 --- a/mail/src/leap/mail/__init__.py +++ b/mail/src/leap/mail/__init__.py @@ -17,17 +17,10 @@ """ -Provide function for loading tests. +Client mail bits. """ -# Do not force the unittest dependency -# import unittest - - -# def load_tests(): -# return unittest.defaultTestLoader.discover('./src/leap/mail') - from ._version import get_versions __version__ = get_versions()['version'] del get_versions diff --git a/mail/src/leap/mail/imap/fetch.py b/mail/src/leap/mail/imap/fetch.py index 0a71f53b..dd65def2 100644 --- a/mail/src/leap/mail/imap/fetch.py +++ b/mail/src/leap/mail/imap/fetch.py @@ -31,15 +31,16 @@ from twisted.internet.threads import deferToThread from leap.common import events as leap_events from leap.common.check import leap_assert, leap_assert_type -from leap.soledad.client import Soledad -from leap.soledad.common.crypto import ENC_SCHEME_KEY, ENC_JSON_KEY - from leap.common.events.events_pb2 import IMAP_FETCHED_INCOMING from leap.common.events.events_pb2 import IMAP_MSG_PROCESSING from leap.common.events.events_pb2 import IMAP_MSG_DECRYPTED from leap.common.events.events_pb2 import IMAP_MSG_SAVED_LOCALLY from leap.common.events.events_pb2 import IMAP_MSG_DELETED_INCOMING from leap.common.events.events_pb2 import IMAP_UNREAD_MAIL +from leap.common.mail import get_email_charset +from leap.keymanager import errors as keymanager_errors +from leap.soledad.client import Soledad +from leap.soledad.common.crypto import ENC_SCHEME_KEY, ENC_JSON_KEY logger = logging.getLogger(__name__) @@ -197,6 +198,29 @@ class LeapIncomingMail(object): logger.warning('Unknown error while ' 'syncing soledad: %r' % (err,)) + def _log_err(self, failure): + """ + Generic errback + """ + err = failure.value + logger.error("error!: %r" % (err,)) + + def _decryption_error(self, failure): + """ + Errback for decryption errors. + """ + # XXX should signal unrecoverable maybe. + err = failure.value + logger.error("error decrypting msg: %s" % (err,)) + + def _saving_error(self, failure): + """ + Errback for local save errors. + """ + # XXX should signal unrecoverable maybe. + err = failure.value + logger.error("error saving msg locally: %s" % (err,)) + def _process_doclist(self, doclist): """ Iterates through the doclist, checks if each doc @@ -227,7 +251,13 @@ class LeapIncomingMail(object): # Deferred chain for individual messages d = deferToThread(self._decrypt_msg, doc, encdata) d.addCallback(self._process_decrypted) + d.addErrback(self._log_err) d.addCallback(self._add_message_locally) + d.addErrback(self._log_err) + # XXX check this, add_locally should not get called if we + # get an error in process + #d.addCallbacks(self._process_decrypted, self._decryption_error) + #d.addCallbacks(self._add_message_locally, self._saving_error) docs_cb.append(d) else: # Ooops, this does not. @@ -288,20 +318,29 @@ class LeapIncomingMail(object): rawmsg = msg.get(self.CONTENT_KEY, None) if not rawmsg: return False - data = self._maybe_decrypt_gpg_msg(rawmsg) - return doc, data + try: + data = self._maybe_decrypt_gpg_msg(rawmsg) + return doc, data + except keymanager_errors.EncryptionDecryptionFailed as exc: + logger.error(exc) + raise def _maybe_decrypt_gpg_msg(self, data): """ Tries to decrypt a gpg message if data looks like one. :param data: the text to be decrypted. - :type data: str + :type data: unicode :return: data, possibly descrypted. :rtype: str """ + leap_assert_type(data, unicode) + parser = Parser() + encoding = get_email_charset(data) + data = data.encode(encoding) origmsg = parser.parsestr(data) + # handle multipart/encrypted messages if origmsg.get_content_type() == 'multipart/encrypted': # sanity check @@ -320,13 +359,21 @@ class LeapIncomingMail(object): "Multipart/encrypted messages' second body part should " "have content type equal to 'octet-stream' (instead of " "%s)." % payload[1].get_content_type()) + # parse message and get encrypted content pgpencmsg = origmsg.get_payload()[1] encdata = pgpencmsg.get_payload() + # decrypt and parse decrypted message decrdata = self._keymanager.decrypt( encdata, self._pkey, passphrase=self._soledad.passphrase) + try: + decrdata = decrdata.encode(encoding) + except (UnicodeEncodeError, UnicodeDecodeError) as e: + logger.error("Unicode error {0}".format(e)) + decrdata = decrdata.encode(encoding, 'replace') + decrmsg = parser.parsestr(decrdata) # replace headers back in original message for hkey, hval in decrmsg.items(): @@ -335,6 +382,7 @@ class LeapIncomingMail(object): origmsg.replace_header(hkey, hval) except KeyError: origmsg[hkey] = hval + # replace payload by unencrypted payload origmsg.set_payload(decrmsg.get_payload()) return origmsg.as_string(unixfrom=False) @@ -352,6 +400,10 @@ class LeapIncomingMail(object): # replace encrypted by decrypted content data = data.replace(pgp_message, decrdata) # if message is not encrypted, return raw data + + if isinstance(data, unicode): + data = data.encode(encoding, 'replace') + return data def _add_message_locally(self, msgtuple): @@ -365,6 +417,7 @@ class LeapIncomingMail(object): incoming message :type msgtuple: (SoledadDocument, str) """ + print "adding message locally....." doc, data = msgtuple self._inbox.addMessage(data, (self.RECENT_FLAG,)) leap_events.signal(IMAP_MSG_SAVED_LOCALLY) diff --git a/mail/src/leap/mail/imap/server.py b/mail/src/leap/mail/imap/server.py index 10d338a1..7a9f8107 100644 --- a/mail/src/leap/mail/imap/server.py +++ b/mail/src/leap/mail/imap/server.py @@ -18,9 +18,7 @@ Soledad-backed IMAP Server. """ import copy -import email import logging -import re import StringIO import cStringIO import time @@ -33,13 +31,10 @@ from twisted.mail import imap4 from twisted.internet import defer from twisted.python import log -#from twisted import cred - -#import u1db - from leap.common import events as leap_events from leap.common.events.events_pb2 import IMAP_UNREAD_MAIL from leap.common.check import leap_assert, leap_assert_type +from leap.common.mail import get_email_charset from leap.soledad.client import Soledad logger = logging.getLogger(__name__) @@ -184,7 +179,8 @@ class SoledadBackedAccount(WithMsgFields, IndexedDB): # messages TYPE_MBOX_SEEN_IDX: [KTYPE, MBOX_VAL, 'bool(seen)'], TYPE_MBOX_RECT_IDX: [KTYPE, MBOX_VAL, 'bool(recent)'], - TYPE_MBOX_RECT_SEEN_IDX: [KTYPE, MBOX_VAL, 'bool(recent)', 'bool(seen)'], + TYPE_MBOX_RECT_SEEN_IDX: [KTYPE, MBOX_VAL, + 'bool(recent)', 'bool(seen)'], } INBOX_NAME = "INBOX" @@ -577,7 +573,7 @@ class SoledadBackedAccount(WithMsgFields, IndexedDB): return "<SoledadBackedAccount (%s)>" % self._account_name ####################################### -# Soledad Message, MessageCollection +# LeapMessage, MessageCollection # and Mailbox ####################################### @@ -695,26 +691,6 @@ class LeapMessage(WithMsgFields): the more complex MIME-based interface. """ - def _get_charset(self, content): - """ - Mini parser to retrieve the charset of an email - - :param content: mail contents - :type content: unicode - - :returns: the charset as parsed from the contents - :rtype: str - """ - charset = "UTF-8" - try: - em = email.message_from_string(content.encode("utf-8")) - # Miniparser for: Content-Type: <something>; charset=<charset> - charset_re = r'''charset=(?P<charset>[\w|\d|-]*)''' - charset = re.findall(charset_re, em["Content-Type"])[0] - except Exception: - pass - return charset - def open(self): """ Return an file-like object opened for reading. @@ -726,8 +702,14 @@ class LeapMessage(WithMsgFields): :rtype: StringIO """ fd = cStringIO.StringIO() - charset = self._get_charset(self._doc.content.get(self.RAW_KEY, '')) - fd.write(self._doc.content.get(self.RAW_KEY, '').encode(charset)) + charset = get_email_charset(self._doc.content.get(self.RAW_KEY, '')) + content = self._doc.content.get(self.RAW_KEY, '') + try: + content = content.encode(charset) + except (UnicodeEncodeError, UnicodeDecodeError) as e: + logger.error("Unicode error {0}".format(e)) + content = content.encode(charset, 'replace') + fd.write(content) fd.seek(0) return fd @@ -746,8 +728,14 @@ class LeapMessage(WithMsgFields): :rtype: StringIO """ fd = StringIO.StringIO() - charset = self._get_charset(self._doc.content.get(self.RAW_KEY, '')) - fd.write(self._doc.content.get(self.RAW_KEY, '').encode(charset)) + charset = get_email_charset(self._doc.content.get(self.RAW_KEY, '')) + content = self._doc.content.get(self.RAW_KEY, '') + try: + content = content.encode(charset) + except (UnicodeEncodeError, UnicodeDecodeError) as e: + logger.error("Unicode error {0}".format(e)) + content = content.encode(charset, 'replace') + fd.write(content) # SHOULD use a separate BODY FIELD ... fd.seek(0) return fd @@ -1111,6 +1099,7 @@ class SoledadMailbox(WithMsgFields): which we instantiate and make accessible in the `messages` attribute. """ implements(imap4.IMailboxInfo, imap4.IMailbox, imap4.ICloseableMailbox) + # XXX should finish the implementation of IMailboxListener messages = None _closed = False @@ -1127,6 +1116,8 @@ class SoledadMailbox(WithMsgFields): CMD_UIDVALIDITY = "UIDVALIDITY" CMD_UNSEEN = "UNSEEN" + listeners = [] + def __init__(self, mbox, soledad=None, rw=1): """ SoledadMailbox constructor. Needs to get passed a name, plus a @@ -1159,15 +1150,35 @@ class SoledadMailbox(WithMsgFields): if not self.getFlags(): self.setFlags(self.INIT_FLAGS) - # XXX what is/was this used for? -------- - # ---> mail/imap4.py +1155, - # _cbSelectWork makes use of this - # probably should implement hooks here - # using leap.common.events - self.listeners = [] - self.addListener = self.listeners.append - self.removeListener = self.listeners.remove - #------------------------------------------ + # the server itself is a listener to the mailbox. + # so we can notify it (and should!) after chanes in flags + # and number of messages. + print "emptying the listeners" + map(lambda i: self.listeners.remove(i), self.listeners) + + def addListener(self, listener): + """ + Rdds a listener to the listeners queue. + + :param listener: listener to add + :type listener: an object that implements IMailboxListener + """ + logger.debug('adding mailbox listener: %s' % listener) + self.listeners.append(listener) + + def removeListener(self, listener): + """ + Removes a listener from the listeners queue. + + :param listener: listener to remove + :type listener: an object that implements IMailboxListener + """ + logger.debug('removing mailbox listener: %s' % listener) + try: + self.listeners.remove(listener) + except ValueError: + logger.error( + "removeListener: cannot remove listener %s" % listener) def _get_mbox(self): """ @@ -1192,6 +1203,7 @@ class SoledadMailbox(WithMsgFields): #return map(str, self.INIT_FLAGS) # XXX CHECK against thunderbird XXX + # XXX I think this is slightly broken.. :/ mbox = self._get_mbox() if not mbox: @@ -1364,6 +1376,10 @@ class SoledadMailbox(WithMsgFields): self.messages.add_msg(message, flags=flags, date=date, uid=uid_next) + exists = len(self.messages) + recent = len(self.messages.get_recent()) + for listener in self.listeners: + listener.newMessages(exists, recent) return defer.succeed(None) # commands, do not rename methods diff --git a/mail/src/leap/mail/imap/service/imap.py b/mail/src/leap/mail/imap/service/imap.py index b840e864..5f7322a9 100644 --- a/mail/src/leap/mail/imap/service/imap.py +++ b/mail/src/leap/mail/imap/service/imap.py @@ -77,7 +77,7 @@ class LeapIMAPServer(imap4.IMAP4Server): #self.theAccount = theAccount def lineReceived(self, line): - if "login" in line: + if "login" in line.lower(): # avoid to log the pass, even though we are using a dummy auth # by now. msg = line[:7] + " [...]" @@ -141,7 +141,9 @@ def run_service(*args, **kwargs): Main entry point to run the service from the client. :returns: the LoopingCall instance that will have to be stoppped - before shutting down the client. + before shutting down the client, the port as returned by + the reactor when starts listening, and the factory for + the protocol. """ leap_assert(len(args) == 2) soledad, keymanager = args @@ -157,8 +159,8 @@ def run_service(*args, **kwargs): from twisted.internet import reactor try: - reactor.listenTCP(port, factory, - interface="localhost") + tport = reactor.listenTCP(port, factory, + interface="localhost") fetcher = LeapIncomingMail( keymanager, soledad, @@ -174,7 +176,7 @@ def run_service(*args, **kwargs): fetcher.start_loop() logger.debug("IMAP4 Server is RUNNING in port %s" % (port,)) leap_events.signal(IMAP_SERVICE_STARTED, str(port)) - return fetcher + return fetcher, tport, factory # not ok, signal error. leap_events.signal(IMAP_SERVICE_FAILED_TO_START, str(port)) diff --git a/mail/src/leap/mail/load_tests.py b/mail/src/leap/mail/load_tests.py new file mode 100644 index 00000000..ee89fcc9 --- /dev/null +++ b/mail/src/leap/mail/load_tests.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# tests.py +# Copyright (C) 2013 LEAP +# +# 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/>. + + +""" +Provide a function for loading tests. +""" + +import unittest + + +def load_tests(): + suite = unittest.TestSuite() + for test in unittest.defaultTestLoader.discover( + './src/leap/mail/', + top_level_dir='./src/'): + suite.addTest(test) + return suite diff --git a/mail/src/leap/mail/smtp/smtprelay.py b/mail/src/leap/mail/smtp/smtprelay.py index d9bbbf9b..fca66c0b 100644 --- a/mail/src/leap/mail/smtp/smtprelay.py +++ b/mail/src/leap/mail/smtp/smtprelay.py @@ -17,6 +17,20 @@ """ LEAP SMTP encrypted relay. + +The following classes comprise the SMTP relay service: + + * SMTPFactory - A twisted.internet.protocol.ServerFactory that provides + the SMTPDelivery protocol. + * SMTPDelivery - A twisted.mail.smtp.IMessageDelivery implementation. It + knows how to validate sender and receiver of messages and it generates + an EncryptedMessage for each recipient. + * SSLContextFactory - Contains the relevant ssl information for the + connection. + * EncryptedMessage - An implementation of twisted.mail.smtp.IMessage that + knows how to encrypt/sign itself before sending. + + """ import re @@ -173,7 +187,6 @@ class SMTPFactory(ServerFactory): @return: The protocol. @rtype: SMTPDelivery """ - # If needed, we might use ESMTPDelivery here instead. smtpProtocol = smtp.SMTP(SMTPDelivery(self._km, self._config)) smtpProtocol.factory = self return smtpProtocol @@ -305,7 +318,7 @@ class SMTPDelivery(object): # EncryptedMessage # -class CtxFactory(ssl.ClientContextFactory): +class SSLContextFactory(ssl.ClientContextFactory): def __init__(self, cert, key): self.cert = cert self.key = key @@ -450,6 +463,8 @@ class EncryptedMessage(object): self._config[PORT_KEY])) d = defer.Deferred() + # we don't pass an ssl context factory to the ESMTPSenderFactory + # because ssl will be handled by reactor.connectSSL() below. factory = smtp.ESMTPSenderFactory( "", # username is blank because server does not use auth. "", # password is blank because server does not use auth. @@ -457,15 +472,15 @@ class EncryptedMessage(object): self._user.dest.addrstr, StringIO(msg), d, - contextFactory=CtxFactory(self._config[CERT_KEY], - self._config[KEY_KEY]), - requireAuthentication=False) + requireAuthentication=False, + requireTransportSecurity=True) signal(proto.SMTP_SEND_MESSAGE_START, self._user.dest.addrstr) - reactor.connectTCP( + reactor.connectSSL( self._config[HOST_KEY], self._config[PORT_KEY], - factory - ) + factory, + contextFactory=SSLContextFactory(self._config[CERT_KEY], + self._config[KEY_KEY])) d.addCallback(self.sendSuccess) d.addErrback(self.sendError) return d diff --git a/mail/src/leap/mail/smtp/tests/__init__.py b/mail/src/leap/mail/smtp/tests/__init__.py index 7fed7dae..9b54de34 100644 --- a/mail/src/leap/mail/smtp/tests/__init__.py +++ b/mail/src/leap/mail/smtp/tests/__init__.py @@ -113,8 +113,8 @@ class TestCaseWithKeyManager(BaseLeapTest): 'username': address, 'password': '<password>', 'encrypted_only': True, - 'cert': 'blah', - 'key': 'bleh', + 'cert': 'src/leap/mail/smtp/tests/cert/server.crt', + 'key': 'src/leap/mail/smtp/tests/cert/server.key', } class Response(object): diff --git a/mail/src/leap/mail/smtp/tests/cert/server.crt b/mail/src/leap/mail/smtp/tests/cert/server.crt new file mode 100644 index 00000000..a27391c2 --- /dev/null +++ b/mail/src/leap/mail/smtp/tests/cert/server.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFBjCCAu4CCQCWn3oMoQrDJTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMB4XDTEzMTAyMzE0NDUwNFoXDTE2MDcxOTE0NDUwNFowRTELMAkG +A1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 +IFdpZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +APexTvEvG7cSmZdAERHt9TB11cSor54Y/F7NmYMdSOJNi4Y0kwkSslpdfipi+mt/ +NFg/uGKi1mcgvuXdVbVPZ9rCgVpIzMncO8RAP7a5+I2zKUzqMCCbLH16sYpo/rDk +VQ5V15TwLsTzOFGG8Cgp68TR8zHuZ4Edf2zMGC1IaiJ6W38LTnJgsowYOCFDAF3z +L36kxMO5gNGEUYV6tjltx+rAcXka3po+xiAgvW6q65UUgDHcIdEGG2dc9bkxxPl7 +RkprF2RwwADNzYS7Tn+Hpmjy06pfYZHNME+Iw515bCRF3GQFUU4BpGnY7EO+h4P9 +Kb1h948gUT9/oswXG+q2Kwk8AoggMJkUOWDFiCa5UjW1GBoxxb7VtZ+QTJXxlFWc +M2VzT7M/HX+P4b05vY4MXJjxPAFKrAGS7J8DKW8WJNUnXa9XSDBHg5qijDzZ/zGm +HTdG6iADnJLmOHBQgFQ12a/n9mYV2GPVC6FlgDzG9f0/SUPBUCafyWYz1LwKY4VM +2NLx/iwYMQsNIMSZQfNmufNDBr70+BShe3ZpbmKB/J33d87AuJd2HjnsThTEAAr+ +6CejyYmwFutoDUCF8IaKGJEp7OGP2//ub4nt5WwW8DYLRi8EqtzEnxPo5ZiayHMY +GHR1jpX1O5JVJFUE79bZCFFHKmtJc4kVZS4m4rTLsk83AgMBAAEwDQYJKoZIhvcN +AQEFBQADggIBAEt4PIRqVuALQSdgZ+GiZYuvEVjxoDVtMSc/ym93Gi8R7DDivFH9 +4suQc5QUiuEF8lpEtkmh+PZ+oFdQkjhBH80h7p4BUSyBy5Yi6dy7ATTlBAqwzCYZ +4wzHeJzu1SI6FinZLksoULbcw04n410aGHkLa6I9O3vCC4kXSnBlwU1sUsJphxM2 +3pkHBpvv79XYf5kFqZPzF16aO7rxFuVvqgXLyzwuyP9kH5zMA21Kioxs/pNyg1lm +5h0VinpHLPse+4tYih1L1WLMpEZiSwZgFhoRtlcdIVXokZPaX4G2EkdrMmSQruWg +Uz8Av6LEYHmRfbYwYM2kEX/+AF8thpTQDbvxjqYk5oyGX4wpKGpih1ac/jYu3O8B +VLhbxZlBYcLxCqqNsGJrWaiHj2Jf4GhUB0O9hXfaZDMqEGXT9GzOz0yF6b3pDQVy +H0lKIBb+kQzB/jhZKu4vrTAowXtt/av5d7D+rpAU1SxfUhBOPNSRoJUI5NSBbokp +a7u4azdB2IQETX3d2rhDk09EbG1XmMi5Vg1oa8nxfMOWXZnDMusJoZClKjrthmwd +rtR5et44XYhX6p217RBkYMDOVFT7aZpu4SaFeqZIuarVYodSmgXToOFXPsrLppRQ +adOT0FpU64RPNrQz5NF1bSIjqrHSaRVacf8yr7qqxNnpMsrtkDJzsMBz +-----END CERTIFICATE----- diff --git a/mail/src/leap/mail/smtp/tests/cert/server.key b/mail/src/leap/mail/smtp/tests/cert/server.key new file mode 100644 index 00000000..197a4496 --- /dev/null +++ b/mail/src/leap/mail/smtp/tests/cert/server.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/mail/src/leap/mail/smtp/tests/test_smtprelay.py b/mail/src/leap/mail/smtp/tests/test_smtprelay.py index a529c931..7fefe772 100644 --- a/mail/src/leap/mail/smtp/tests/test_smtprelay.py +++ b/mail/src/leap/mail/smtp/tests/test_smtprelay.py @@ -23,8 +23,8 @@ SMTP relay tests. import re - from datetime import datetime +from gnupg._util import _make_binary_stream from twisted.test import proto_helpers from twisted.mail.smtp import ( User, @@ -33,7 +33,6 @@ from twisted.mail.smtp import ( ) from mock import Mock - from leap.mail.smtp.smtprelay import ( SMTPFactory, EncryptedMessage, @@ -45,7 +44,6 @@ from leap.mail.smtp.tests import ( ) from leap.keymanager import openpgp - # some regexps IP_REGEX = "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}" + \ "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])" @@ -127,11 +125,22 @@ class TestSmtpRelay(TestCaseWithKeyManager): for line in self.EMAIL_DATA[4:12]: m.lineReceived(line) m.eomReceived() + # assert structure of encrypted message + self.assertTrue('Content-Type' in m._msg) + self.assertEqual('multipart/encrypted', m._msg.get_content_type()) + self.assertEqual('application/pgp-encrypted', + m._msg.get_param('protocol')) + self.assertEqual(2, len(m._msg.get_payload())) + self.assertEqual('application/pgp-encrypted', + m._msg.get_payload(0).get_content_type()) + self.assertEqual('application/octet-stream', + m._msg.get_payload(1).get_content_type()) privkey = self._km.get_key( ADDRESS, openpgp.OpenPGPKey, private=True) - decrypted = self._km.decrypt(m._message.get_payload(), privkey) + decrypted = self._km.decrypt( + m._msg.get_payload(1).get_payload(), privkey) self.assertEqual( - '\r\n'.join(self.EMAIL_DATA[9:12]) + '\r\n', + '\n' + '\r\n'.join(self.EMAIL_DATA[9:12]) + '\r\n', decrypted, 'Decrypted text differs from plaintext.') @@ -149,14 +158,24 @@ class TestSmtpRelay(TestCaseWithKeyManager): m.lineReceived(line) # trigger encryption and signing m.eomReceived() + # assert structure of encrypted message + self.assertTrue('Content-Type' in m._msg) + self.assertEqual('multipart/encrypted', m._msg.get_content_type()) + self.assertEqual('application/pgp-encrypted', + m._msg.get_param('protocol')) + self.assertEqual(2, len(m._msg.get_payload())) + self.assertEqual('application/pgp-encrypted', + m._msg.get_payload(0).get_content_type()) + self.assertEqual('application/octet-stream', + m._msg.get_payload(1).get_content_type()) # decrypt and verify privkey = self._km.get_key( ADDRESS, openpgp.OpenPGPKey, private=True) pubkey = self._km.get_key(ADDRESS_2, openpgp.OpenPGPKey) decrypted = self._km.decrypt( - m._message.get_payload(), privkey, verify=pubkey) + m._msg.get_payload(1).get_payload(), privkey, verify=pubkey) self.assertEqual( - '\r\n'.join(self.EMAIL_DATA[9:12]) + '\r\n', + '\n' + '\r\n'.join(self.EMAIL_DATA[9:12]) + '\r\n', decrypted, 'Decrypted text differs from plaintext.') @@ -175,22 +194,34 @@ class TestSmtpRelay(TestCaseWithKeyManager): m.lineReceived(line) # trigger signing m.eomReceived() + # assert structure of signed message + self.assertTrue('Content-Type' in m._msg) + self.assertEqual('multipart/signed', m._msg.get_content_type()) + self.assertEqual('application/pgp-signature', + m._msg.get_param('protocol')) + self.assertEqual('pgp-sha512', m._msg.get_param('micalg')) # assert content of message + self.assertEqual( + m._msg.get_payload(0).get_payload(decode=True), + '\r\n'.join(self.EMAIL_DATA[9:13])) + # assert content of signature self.assertTrue( - m._message.get_payload().startswith( - '-----BEGIN PGP SIGNED MESSAGE-----\n' + - 'Hash: SHA1\n\n' + - ('\r\n'.join(self.EMAIL_DATA[9:12]) + '\r\n' + - '-----BEGIN PGP SIGNATURE-----\n')), + m._msg.get_payload(1).get_payload().startswith( + '-----BEGIN PGP SIGNATURE-----\n'), 'Message does not start with signature header.') self.assertTrue( - m._message.get_payload().endswith( + m._msg.get_payload(1).get_payload().endswith( '-----END PGP SIGNATURE-----\n'), 'Message does not end with signature footer.') # assert signature is valid pubkey = self._km.get_key(ADDRESS_2, openpgp.OpenPGPKey) + # replace EOL before verifying (according to rfc3156) + signed_text = re.sub('\r?\n', '\r\n', + m._msg.get_payload(0).as_string()) self.assertTrue( - self._km.verify(m._message.get_payload(), pubkey), + self._km.verify(signed_text, + pubkey, + detached_sig=m._msg.get_payload(1).get_payload()), 'Signature could not be verified.') def test_missing_key_rejects_address(self): |