diff options
-rw-r--r-- | README.rst | 5 | ||||
-rw-r--r-- | pkg/requirements-dev.pip | 14 | ||||
-rw-r--r-- | pkg/requirements.pip | 3 | ||||
-rw-r--r-- | setup.py | 46 | ||||
-rw-r--r-- | src/leap/__init__.py | 6 | ||||
-rw-r--r-- | src/leap/mail/__init__.py | 0 | ||||
-rw-r--r-- | src/leap/mail/imap/__init__.py | 0 | ||||
-rw-r--r-- | src/leap/mail/imap/server.py | 558 | ||||
-rw-r--r-- | src/leap/mail/imap/tests/__init__.py | 232 | ||||
-rwxr-xr-x | src/leap/mail/imap/tests/imapclient.py | 206 | ||||
-rw-r--r-- | src/leap/mail/imap/tests/rfc822.message | 86 | ||||
-rw-r--r-- | src/leap/mail/imap/tests/test_imap.py | 957 | ||||
-rw-r--r-- | src/leap/mail/smtp/README.rst | 43 | ||||
-rw-r--r-- | src/leap/mail/smtp/__init__.py | 0 | ||||
-rw-r--r-- | src/leap/mail/smtp/smtprelay.py | 246 | ||||
-rw-r--r-- | src/leap/mail/smtp/tests/185CA770.key | 79 | ||||
-rw-r--r-- | src/leap/mail/smtp/tests/185CA770.pub | 52 | ||||
-rw-r--r-- | src/leap/mail/smtp/tests/__init__.py | 218 | ||||
-rw-r--r-- | src/leap/mail/smtp/tests/mail.txt | 10 | ||||
-rw-r--r-- | src/leap/mail/smtp/tests/test_smtprelay.py | 78 |
20 files changed, 2839 insertions, 0 deletions
diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..92a4fa6 --- /dev/null +++ b/README.rst @@ -0,0 +1,5 @@ +leap.mail +========= +Mail services for the LEAP CLient. + +More info: https://leap.se diff --git a/pkg/requirements-dev.pip b/pkg/requirements-dev.pip new file mode 100644 index 0000000..4bd76f6 --- /dev/null +++ b/pkg/requirements-dev.pip @@ -0,0 +1,14 @@ +# --------------------------- +# -- external requirements -- +# -- during development -- +# --------------------------- +# +# For temporary work, you can point this to your developer repo. +# consolidated changes will be pushed to pypi and then added +# to the main requirements.pip +# +# NOTE: you have to run pip install -r pkg/requirements.pip for pip +# to install it. (do it after python setup.py develop and it +# will only install this) + +-e git+git://leap.se/soledad@develop#egg=leap.soledad diff --git a/pkg/requirements.pip b/pkg/requirements.pip new file mode 100644 index 0000000..1b5e5ef --- /dev/null +++ b/pkg/requirements.pip @@ -0,0 +1,3 @@ +leap.common +leap.soledad +twisted diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4de7251 --- /dev/null +++ b/setup.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# setup.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/>. +""" +setup file for leap.mail +""" +from setuptools import setup, find_packages + +requirements = [ + "leap.soledad", + "leap.common", + "twisted", +] + +# XXX add classifiers, docs + +setup( + name='leap.mail', + version='0.2.0-dev', + url='https://leap.se/', + license='GPLv3+', + author='The LEAP Encryption Access Project', + author_email='info@leap.se', + description='Mail Services in the LEAP Client project.', + long_description=( + "Mail Services in the LEAP Client project." + ), + namespace_packages=["leap"], + package_dir={'': 'src'}, + packages=find_packages('src'), + #test_suite='leap.mail.tests', + #install_requires=requirements, +) diff --git a/src/leap/__init__.py b/src/leap/__init__.py new file mode 100644 index 0000000..f48ad10 --- /dev/null +++ b/src/leap/__init__.py @@ -0,0 +1,6 @@ +# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages +try: + __import__('pkg_resources').declare_namespace(__name__) +except ImportError: + from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) diff --git a/src/leap/mail/__init__.py b/src/leap/mail/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/leap/mail/__init__.py diff --git a/src/leap/mail/imap/__init__.py b/src/leap/mail/imap/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/leap/mail/imap/__init__.py diff --git a/src/leap/mail/imap/server.py b/src/leap/mail/imap/server.py new file mode 100644 index 0000000..4e9c22c --- /dev/null +++ b/src/leap/mail/imap/server.py @@ -0,0 +1,558 @@ +import copy + +from zope.interface import implements + +from twisted.mail import imap4 +from twisted.internet import defer + +#from twisted import cred + +import u1db + + +# TODO delete this SimpleMailbox +class SimpleMailbox: + """ + A simple Mailbox for reference + We don't intend to use this, only for debugging purposes + until we stabilize unittests with SoledadMailbox + """ + implements(imap4.IMailboxInfo, imap4.IMailbox, imap4.ICloseableMailbox) + + flags = ('\\Flag1', 'Flag2', '\\AnotherSysFlag', 'LastFlag') + messages = [] + mUID = 0 + rw = 1 + closed = False + + def __init__(self): + self.listeners = [] + self.addListener = self.listeners.append + self.removeListener = self.listeners.remove + + def getFlags(self): + return self.flags + + def getUIDValidity(self): + return 42 + + def getUIDNext(self): + return len(self.messages) + 1 + + def getMessageCount(self): + return 9 + + def getRecentCount(self): + return 3 + + def getUnseenCount(self): + return 4 + + def isWriteable(self): + return self.rw + + def destroy(self): + pass + + def getHierarchicalDelimiter(self): + return '/' + + def requestStatus(self, names): + r = {} + if 'MESSAGES' in names: + r['MESSAGES'] = self.getMessageCount() + if 'RECENT' in names: + r['RECENT'] = self.getRecentCount() + if 'UIDNEXT' in names: + r['UIDNEXT'] = self.getMessageCount() + 1 + if 'UIDVALIDITY' in names: + r['UIDVALIDITY'] = self.getUID() + if 'UNSEEN' in names: + r['UNSEEN'] = self.getUnseenCount() + return defer.succeed(r) + + def addMessage(self, message, flags, date=None): + self.messages.append((message, flags, date, self.mUID)) + self.mUID += 1 + return defer.succeed(None) + + def expunge(self): + delete = [] + for i in self.messages: + if '\\Deleted' in i[1]: + delete.append(i) + for i in delete: + self.messages.remove(i) + return [i[3] for i in delete] + + def close(self): + self.closed = True + + +################################### +# SoledadAccount Index +################################### + +class MissingIndexError(Exception): + """raises when tried to access a non existent index document""" + + +class BadIndexError(Exception): + """raises when index is malformed or has the wrong cardinality""" + + +EMPTY_INDEXDOC = {"is_index": True, "mailboxes": [], "subscriptions": []} +get_empty_indexdoc = lambda: copy.deepcopy(EMPTY_INDEXDOC) + + +class SoledadAccountIndex(object): + """ + Index for the Soledad Account + keeps track of mailboxes and subscriptions + """ + _index = None + + def __init__(self, soledad=None): + self._soledad = soledad + self._db = soledad._db + self._initialize_db() + + def _initialize_db(self): + """initialize the database""" + db_indexes = dict(self._soledad._db.list_indexes()) + name, expression = "isindex", ["bool(is_index)"] + if name not in db_indexes: + self._soledad._db.create_index(name, *expression) + try: + self._index = self._get_index_doc() + except MissingIndexError: + print "no index!!! creating..." + self._create_index_doc() + + def _create_index_doc(self): + """creates an empty index document""" + indexdoc = get_empty_indexdoc() + self._index = self._soledad.create_doc( + indexdoc) + + def _get_index_doc(self): + """gets index document""" + indexdoc = self._db.get_from_index("isindex", "*") + if not indexdoc: + raise MissingIndexError + if len(indexdoc) > 1: + raise BadIndexError + return indexdoc[0] + + def _update_index_doc(self): + """updates index document""" + self._db.put_doc(self._index) + + # setters and getters for the index document + + def _get_mailboxes(self): + """Get mailboxes associated with this account.""" + return self._index.content.setdefault('mailboxes', []) + + def _set_mailboxes(self, mailboxes): + """Set mailboxes associated with this account.""" + self._index.content['mailboxes'] = list(set(mailboxes)) + self._update_index_doc() + + mailboxes = property( + _get_mailboxes, _set_mailboxes, doc="Account mailboxes.") + + def _get_subscriptions(self): + """Get subscriptions associated with this account.""" + return self._index.content.setdefault('subscriptions', []) + + def _set_subscriptions(self, subscriptions): + """Set subscriptions associated with this account.""" + self._index.content['subscriptions'] = list(set(subscriptions)) + self._update_index_doc() + + subscriptions = property( + _get_subscriptions, _set_subscriptions, doc="Account subscriptions.") + + def addMailbox(self, name): + """add a mailbox to the mailboxes list.""" + name = name.upper() + self.mailboxes.append(name) + self._update_index_doc() + + def removeMailbox(self, name): + """remove a mailbox from the mailboxes list.""" + self.mailboxes.remove(name) + self._update_index_doc() + + def addSubscription(self, name): + """add a subscription to the subscriptions list.""" + name = name.upper() + self.subscriptions.append(name) + self._update_index_doc() + + def removeSubscription(self, name): + """remove a subscription from the subscriptions list.""" + self.subscriptions.remove(name) + self._update_index_doc() + + +####################################### +# Soledad Account +####################################### + +class SoledadBackedAccount(object): + + implements(imap4.IAccount, imap4.INamespacePresenter) + + #mailboxes = None + #subscriptions = None + + top_id = 0 # XXX move top_id to _index + _soledad = None + _db = None + + def __init__(self, name, soledad=None): + self.name = name + self._soledad = soledad + self._db = soledad._db + self._index = SoledadAccountIndex(soledad=soledad) + + #self.mailboxes = {} + #self.subscriptions = [] + + def allocateID(self): + id = self.top_id # XXX move to index !!! + self.top_id += 1 + return id + + @property + def mailboxes(self): + return self._index.mailboxes + + @property + def subscriptions(self): + return self._index.subscriptions + + ## + ## IAccount + ## + + def addMailbox(self, name, mbox=None): + name = name.upper() + if name in self.mailboxes: + raise imap4.MailboxCollision, name + if mbox is None: + mbox = self._emptyMailbox(name, self.allocateID()) + self._index.addMailbox(name) + return 1 + + def create(self, pathspec): + paths = filter(None, pathspec.split('/')) + for accum in range(1, len(paths)): + try: + self.addMailbox('/'.join(paths[:accum])) + except imap4.MailboxCollision: + pass + try: + self.addMailbox('/'.join(paths)) + except imap4.MailboxCollision: + if not pathspec.endswith('/'): + return False + return True + + def _emptyMailbox(self, name, id): + # XXX implement!!! + raise NotImplementedError + + def select(self, name, readwrite=1): + return self.mailboxes.get(name.upper()) + + def delete(self, name): + name = name.upper() + # See if this mailbox exists at all + mbox = self.mailboxes.get(name) + if not mbox: + raise imap4.MailboxException("No such mailbox") + # See if this box is flagged \Noselect + if r'\Noselect' in mbox.getFlags(): + # Check for hierarchically inferior mailboxes with this one + # as part of their root. + for others in self.mailboxes.keys(): + if others != name and others.startswith(name): + raise imap4.MailboxException, ( + "Hierarchically inferior mailboxes " + "exist and \\Noselect is set") + mbox.destroy() + + # iff there are no hierarchically inferior names, we will + # delete it from our ken. + if self._inferiorNames(name) > 1: + del self.mailboxes[name] + + def rename(self, oldname, newname): + oldname = oldname.upper() + newname = newname.upper() + if oldname not in self.mailboxes: + raise imap4.NoSuchMailbox, oldname + + inferiors = self._inferiorNames(oldname) + inferiors = [(o, o.replace(oldname, newname, 1)) for o in inferiors] + + for (old, new) in inferiors: + if new in self.mailboxes: + raise imap4.MailboxCollision, new + + for (old, new) in inferiors: + self.mailboxes[new] = self.mailboxes[old] + del self.mailboxes[old] + + def _inferiorNames(self, name): + inferiors = [] + for infname in self.mailboxes.keys(): + if infname.startswith(name): + inferiors.append(infname) + return inferiors + + def isSubscribed(self, name): + return name.upper() in self.subscriptions + + def subscribe(self, name): + name = name.upper() + if name not in self.subscriptions: + self._index.addSubscription(name) + + def unsubscribe(self, name): + name = name.upper() + if name not in self.subscriptions: + raise imap4.MailboxException, "Not currently subscribed to " + name + self._index.removeSubscription(name) + + def listMailboxes(self, ref, wildcard): + ref = self._inferiorNames(ref.upper()) + wildcard = imap4.wildcardToRegexp(wildcard, '/') + return [(i, self.mailboxes[i]) for i in ref if wildcard.match(i)] + + ## + ## INamespacePresenter + ## + + def getPersonalNamespaces(self): + return [["", "/"]] + + def getSharedNamespaces(self): + return None + + def getOtherNamespaces(self): + return None + +####################################### +# Soledad Message, MessageCollection +# and Mailbox +####################################### + +FLAGS_INDEX = 'flags' +SEEN_INDEX = 'seen' +INDEXES = {FLAGS_INDEX: ['flags'], + SEEN_INDEX: ['bool(seen)'], +} + + +class Message(u1db.Document): + """A rfc822 message item.""" + # XXX TODO use email module + + def _get_subject(self): + """Get the message title.""" + return self.content.get('subject') + + def _set_subject(self, subject): + """Set the message title.""" + self.content['subject'] = subject + + subject = property(_get_subject, _set_subject, + doc="Subject of the message.") + + def _get_seen(self): + """Get the seen status of the message.""" + return self.content.get('seen', False) + + def _set_seen(self, value): + """Set the seen status.""" + self.content['seen'] = value + + seen = property(_get_seen, _set_seen, doc="Seen flag.") + + def _get_flags(self): + """Get flags associated with the message.""" + return self.content.setdefault('flags', []) + + def _set_flags(self, flags): + """Set flags associated with the message.""" + self.content['flags'] = list(set(flags)) + + flags = property(_get_flags, _set_flags, doc="Message flags.") + +EMPTY_MSG = { + "subject": "", + "seen": False, + "flags": [], + "mailbox": "", +} +get_empty_msg = lambda: copy.deepcopy(EMPTY_MSG) + + +class MessageCollection(object): + """ + A collection of messages + """ + + def __init__(self, mbox=None, db=None): + assert mbox + self.db = db + self.initialize_db() + + def initialize_db(self): + """Initialize the database.""" + # Ask the database for currently existing indexes. + db_indexes = dict(self.db.list_indexes()) + # Loop through the indexes we expect to find. + for name, expression in INDEXES.items(): + print 'name is', name + if name not in db_indexes: + # The index does not yet exist. + print 'creating index' + self.db.create_index(name, *expression) + continue + + if expression == db_indexes[name]: + print 'expression up to date' + # The index exists and is up to date. + continue + # The index exists but the definition is not what expected, so we + # delete it and add the proper index expression. + print 'deleting index' + self.db.delete_index(name) + self.db.create_index(name, *expression) + + def add_msg(self, subject=None, flags=None): + """Create a new message document.""" + if flags is None: + flags = [] + content = get_empty_msg() + if subject or flags: + content['subject'] = subject + content['flags'] = flags + # Store the document in the database. Since we did not set a document + # id, the database will store it as a new document, and generate + # a valid id. + return self.db.create_doc(content) + + def get_all(self): + """Get all messages""" + return self.db.get_from_index(SEEN_INDEX, "*") + + def get_unseen(self): + """Get only unseen messages""" + return self.db.get_from_index(SEEN_INDEX, "0") + + def count(self): + return len(self.get_all()) + + +class SoledadMailbox: + """ + A Soledad-backed IMAP mailbox + """ + + implements(imap4.IMailboxInfo, imap4.IMailbox, imap4.ICloseableMailbox) + + flags = ('\\Seen', '\\Answered', '\\Flagged', + '\\Deleted', '\\Draft', '\\Recent', 'List') + + #messages = [] + messages = None + mUID = 0 + rw = 1 + closed = False + + def __init__(self, mbox, soledad=None): + # XXX sanity check: + #soledad is not None and isinstance(SQLCipherDatabase, soldad._db) + self.listeners = [] + self.addListener = self.listeners.append + self.removeListener = self.listeners.remove + self._soledad = soledad + if soledad: + self.messages = MessageCollection( + mbox=mbox, db=soledad._db) + + def getFlags(self): + return self.messages.db.get_index_keys(FLAGS_INDEX) + + def getUIDValidity(self): + return 42 + + def getUIDNext(self): + return self.messages.count() + 1 + + def getMessageCount(self): + return self.messages.count() + + def getUnseenCount(self): + return len(self.messages.get_unseen()) + + def getRecentCount(self): + # XXX + return 3 + + def isWriteable(self): + return self.rw + + def destroy(self): + pass + + def getHierarchicalDelimiter(self): + return '/' + + def requestStatus(self, names): + r = {} + if 'MESSAGES' in names: + r['MESSAGES'] = self.getMessageCount() + if 'RECENT' in names: + r['RECENT'] = self.getRecentCount() + if 'UIDNEXT' in names: + r['UIDNEXT'] = self.getMessageCount() + 1 + if 'UIDVALIDITY' in names: + r['UIDVALIDITY'] = self.getUID() + if 'UNSEEN' in names: + r['UNSEEN'] = self.getUnseenCount() + return defer.succeed(r) + + def addMessage(self, message, flags, date=None): + # self.messages.add_msg((msg, flags, date, self.mUID)) + #self.messages.append((message, flags, date, self.mUID)) + # XXX CHANGE-ME + self.messages.add_msg(subject=message, flags=flags, date=date) + self.mUID += 1 + return defer.succeed(None) + + def deleteAllDocs(self): + """deletes all docs""" + docs = self.messages.db.get_all_docs()[1] + for doc in docs: + self.messages.db.delete_doc(doc) + + def expunge(self): + """deletes all messages flagged \\Deleted""" + # XXX FIXME! + delete = [] + for i in self.messages: + if '\\Deleted' in i[1]: + delete.append(i) + for i in delete: + self.messages.remove(i) + return [i[3] for i in delete] + + def close(self): + self.closed = True diff --git a/src/leap/mail/imap/tests/__init__.py b/src/leap/mail/imap/tests/__init__.py new file mode 100644 index 0000000..9a4c663 --- /dev/null +++ b/src/leap/mail/imap/tests/__init__.py @@ -0,0 +1,232 @@ +#-*- encoding: utf-8 -*- +""" +leap/email/imap/tests/__init__.py +---------------------------------- +Module intialization file for leap.mx.tests, a module containing unittesting +code, using twisted.trial, for testing leap_mx. + +@authors: Kali Kaneko, <kali@leap.se> +@license: GPLv3, see included LICENSE file +@copyright: © 2013 Kali Kaneko, see COPYLEFT file +""" + +__all__ = ['test_imap'] + + +def run(): + """xxx fill me in""" + pass + +import u1db + +from leap.common.testing.basetest import BaseLeapTest + +from leap.soledad import Soledad +from leap.soledad.util import GPGWrapper +from leap.soledad.backends.leap_backend import LeapDocument + + +#----------------------------------------------------------------------------- +# Some tests inherit from BaseSoledadTest in order to have a working Soledad +# instance in each test. +#----------------------------------------------------------------------------- + +class BaseSoledadIMAPTest(BaseLeapTest): + """ + Instantiates GPG and Soledad for usage in LeapIMAPServer tests. + Copied from BaseSoledadTest, but moving setup to classmethod + """ + + def setUp(self): + # config info + self.gnupg_home = "%s/gnupg" % self.tempdir + self.db1_file = "%s/db1.u1db" % self.tempdir + self.db2_file = "%s/db2.u1db" % self.tempdir + self.email = 'leap@leap.se' + # open test dbs + self._db1 = u1db.open(self.db1_file, create=True, + document_factory=LeapDocument) + self._db2 = u1db.open(self.db2_file, create=True, + document_factory=LeapDocument) + # initialize soledad by hand so we can control keys + self._soledad = Soledad(self.email, gnupg_home=self.gnupg_home, + initialize=False, + prefix=self.tempdir) + self._soledad._init_dirs() + self._soledad._gpg = GPGWrapper(gnupghome=self.gnupg_home) + self._soledad._gpg.import_keys(PUBLIC_KEY) + self._soledad._gpg.import_keys(PRIVATE_KEY) + self._soledad._load_openpgp_keypair() + if not self._soledad._has_secret(): + self._soledad._gen_secret() + self._soledad._load_secret() + self._soledad._init_db() + + def tearDown(self): + self._db1.close() + self._db2.close() + self._soledad.close() + + +# Key material for testing +KEY_FINGERPRINT = "E36E738D69173C13D709E44F2F455E2824D18DDF" +PUBLIC_KEY = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +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 +=MuOY +-----END PGP PUBLIC KEY BLOCK----- +""" +PRIVATE_KEY = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +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 +=JTFu +-----END PGP PRIVATE KEY BLOCK----- +""" diff --git a/src/leap/mail/imap/tests/imapclient.py b/src/leap/mail/imap/tests/imapclient.py new file mode 100755 index 0000000..027396c --- /dev/null +++ b/src/leap/mail/imap/tests/imapclient.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python + +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + + +""" +Simple IMAP4 client which connects to our custome +IMAP4 server: imapserver.py. +""" + +import sys + +from twisted.internet import protocol +from twisted.internet import defer +from twisted.internet import stdio +from twisted.mail import imap4 +from twisted.protocols import basic +from twisted.python import util +from twisted.python import log + + +class TrivialPrompter(basic.LineReceiver): + #from os import linesep as delimiter + + promptDeferred = None + + def prompt(self, msg): + assert self.promptDeferred is None + self.display(msg) + self.promptDeferred = defer.Deferred() + return self.promptDeferred + + def display(self, msg): + self.transport.write(msg) + + def lineReceived(self, line): + if self.promptDeferred is None: + return + d, self.promptDeferred = self.promptDeferred, None + d.callback(line) + + +class SimpleIMAP4Client(imap4.IMAP4Client): + """ + Add callbacks when the client receives greeting messages from + an IMAP server. + """ + greetDeferred = None + + def serverGreeting(self, caps): + self.serverCapabilities = caps + if self.greetDeferred is not None: + d, self.greetDeferred = self.greetDeferred, None + d.callback(self) + + +class SimpleIMAP4ClientFactory(protocol.ClientFactory): + usedUp = False + protocol = SimpleIMAP4Client + + def __init__(self, username, onConn): + self.username = username + self.onConn = onConn + + def buildProtocol(self, addr): + assert not self.usedUp + self.usedUp = True + + p = self.protocol() + p.factory = self + p.greetDeferred = self.onConn + + p.registerAuthenticator(imap4.PLAINAuthenticator(self.username)) + p.registerAuthenticator(imap4.LOGINAuthenticator(self.username)) + p.registerAuthenticator( + imap4.CramMD5ClientAuthenticator(self.username)) + + return p + + def clientConnectionFailed(self, connector, reason): + d, self.onConn = self.onConn, None + d.errback(reason) + + +def cbServerGreeting(proto, username, password): + """ + Initial callback - invoked after the server sends us its greet message. + """ + # Hook up stdio + tp = TrivialPrompter() + stdio.StandardIO(tp) + + # And make it easily accessible + proto.prompt = tp.prompt + proto.display = tp.display + + # Try to authenticate securely + return proto.authenticate( + password).addCallback( + cbAuthentication, proto).addErrback( + ebAuthentication, proto, username, password) + + +def ebConnection(reason): + """ + Fallback error-handler. If anything goes wrong, log it and quit. + """ + log.startLogging(sys.stdout) + log.err(reason) + return reason + + +def cbAuthentication(result, proto): + """ + Callback after authentication has succeeded. + List a bunch of mailboxes. + """ + return proto.list("", "*" + ).addCallback(cbMailboxList, proto + ) + + +def ebAuthentication(failure, proto, username, password): + """ + Errback invoked when authentication fails. + If it failed because no SASL mechanisms match, offer the user the choice + of logging in insecurely. + If you are trying to connect to your Gmail account, you will be here! + """ + failure.trap(imap4.NoSupportedAuthentication) + return proto.prompt( + "No secure authentication available. Login insecurely? (y/N) " + ).addCallback(cbInsecureLogin, proto, username, password + ) + + +def cbInsecureLogin(result, proto, username, password): + """ + Callback for "insecure-login" prompt. + """ + if result.lower() == "y": + # If they said yes, do it. + return proto.login(username, password + ).addCallback(cbAuthentication, proto + ) + return defer.fail(Exception("Login failed for security reasons.")) + + +def cbMailboxList(result, proto): + """ + Callback invoked when a list of mailboxes has been retrieved. + """ + result = [e[2] for e in result] + s = '\n'.join( + ['%d. %s' % (n + 1, m) for (n, m) in zip(range(len(result)), result)]) + if not s: + return defer.fail(Exception("No mailboxes exist on server!")) + return proto.prompt(s + "\nWhich mailbox? [1] " + ).addCallback(cbPickMailbox, proto, result + ) + + +def cbPickMailbox(result, proto, mboxes): + """ + When the user selects a mailbox, "examine" it. + """ + mbox = mboxes[int(result or '1') - 1] + return proto.status(mbox, 'MESSAGES', 'UNSEEN' + ).addCallback(cbMboxStatus, proto) + + +def cbMboxStatus(result, proto): + print "You have %s messages (%s unseen)!" % ( + result['MESSAGES'], result['UNSEEN']) + return proto.logout() + + +def cbClose(result): + """ + Close the connection when we finish everything. + """ + from twisted.internet import reactor + reactor.stop() + + +def main(): + hostname = raw_input('IMAP4 Server Hostname: ') + port = raw_input('IMAP4 Server Port (the default is 143): ') + username = raw_input('IMAP4 Username: ') + password = util.getPassword('IMAP4 Password: ') + + onConn = defer.Deferred( + ).addCallback(cbServerGreeting, username, password + ).addErrback(ebConnection + ).addBoth(cbClose) + + factory = SimpleIMAP4ClientFactory(username, onConn) + + from twisted.internet import reactor + conn = reactor.connectTCP(hostname, int(port), factory) + reactor.run() + + +if __name__ == '__main__': + main() diff --git a/src/leap/mail/imap/tests/rfc822.message b/src/leap/mail/imap/tests/rfc822.message new file mode 100644 index 0000000..ee97ab9 --- /dev/null +++ b/src/leap/mail/imap/tests/rfc822.message @@ -0,0 +1,86 @@ +Return-Path: <twisted-commits-admin@twistedmatrix.com> +Delivered-To: exarkun@meson.dyndns.org +Received: from localhost [127.0.0.1] + by localhost with POP3 (fetchmail-6.2.1) + for exarkun@localhost (single-drop); Thu, 20 Mar 2003 14:50:20 -0500 (EST) +Received: from pyramid.twistedmatrix.com (adsl-64-123-27-105.dsl.austtx.swbell.net [64.123.27.105]) + by intarweb.us (Postfix) with ESMTP id 4A4A513EA4 + for <exarkun@meson.dyndns.org>; Thu, 20 Mar 2003 14:49:27 -0500 (EST) +Received: from localhost ([127.0.0.1] helo=pyramid.twistedmatrix.com) + by pyramid.twistedmatrix.com with esmtp (Exim 3.35 #1 (Debian)) + id 18w648-0007Vl-00; Thu, 20 Mar 2003 13:51:04 -0600 +Received: from acapnotic by pyramid.twistedmatrix.com with local (Exim 3.35 #1 (Debian)) + id 18w63j-0007VK-00 + for <twisted-commits@twistedmatrix.com>; Thu, 20 Mar 2003 13:50:39 -0600 +To: twisted-commits@twistedmatrix.com +From: etrepum CVS <etrepum@twistedmatrix.com> +Reply-To: twisted-python@twistedmatrix.com +X-Mailer: CVSToys +Message-Id: <E18w63j-0007VK-00@pyramid.twistedmatrix.com> +Subject: [Twisted-commits] rebuild now works on python versions from 2.2.0 and up. +Sender: twisted-commits-admin@twistedmatrix.com +Errors-To: twisted-commits-admin@twistedmatrix.com +X-BeenThere: twisted-commits@twistedmatrix.com +X-Mailman-Version: 2.0.11 +Precedence: bulk +List-Help: <mailto:twisted-commits-request@twistedmatrix.com?subject=help> +List-Post: <mailto:twisted-commits@twistedmatrix.com> +List-Subscribe: <http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits>, + <mailto:twisted-commits-request@twistedmatrix.com?subject=subscribe> +List-Id: <twisted-commits.twistedmatrix.com> +List-Unsubscribe: <http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits>, + <mailto:twisted-commits-request@twistedmatrix.com?subject=unsubscribe> +List-Archive: <http://twistedmatrix.com/pipermail/twisted-commits/> +Date: Thu, 20 Mar 2003 13:50:39 -0600 + +Modified files: +Twisted/twisted/python/rebuild.py 1.19 1.20 + +Log message: +rebuild now works on python versions from 2.2.0 and up. + + +ViewCVS links: +http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/twisted/python/rebuild.py.diff?r1=text&tr1=1.19&r2=text&tr2=1.20&cvsroot=Twisted + +Index: Twisted/twisted/python/rebuild.py +diff -u Twisted/twisted/python/rebuild.py:1.19 Twisted/twisted/python/rebuild.py:1.20 +--- Twisted/twisted/python/rebuild.py:1.19 Fri Jan 17 13:50:49 2003 ++++ Twisted/twisted/python/rebuild.py Thu Mar 20 11:50:08 2003 +@@ -206,15 +206,27 @@ + clazz.__dict__.clear() + clazz.__getattr__ = __getattr__ + clazz.__module__ = module.__name__ ++ if newclasses: ++ import gc ++ if (2, 2, 0) <= sys.version_info[:3] < (2, 2, 2): ++ hasBrokenRebuild = 1 ++ gc_objects = gc.get_objects() ++ else: ++ hasBrokenRebuild = 0 + for nclass in newclasses: + ga = getattr(module, nclass.__name__) + if ga is nclass: + log.msg("WARNING: new-class %s not replaced by reload!" % reflect.qual(nclass)) + else: +- import gc +- for r in gc.get_referrers(nclass): +- if isinstance(r, nclass): ++ if hasBrokenRebuild: ++ for r in gc_objects: ++ if not getattr(r, '__class__', None) is nclass: ++ continue + r.__class__ = ga ++ else: ++ for r in gc.get_referrers(nclass): ++ if getattr(r, '__class__', None) is nclass: ++ r.__class__ = ga + if doLog: + log.msg('') + log.msg(' (fixing %s): ' % str(module.__name__)) + + +_______________________________________________ +Twisted-commits mailing list +Twisted-commits@twistedmatrix.com +http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits diff --git a/src/leap/mail/imap/tests/test_imap.py b/src/leap/mail/imap/tests/test_imap.py new file mode 100644 index 0000000..6792e4b --- /dev/null +++ b/src/leap/mail/imap/tests/test_imap.py @@ -0,0 +1,957 @@ +#-*- encoding: utf-8 -*- +""" +leap/email/imap/tests/test_imap.py +---------------------------------- +Test case for leap.email.imap.server + +@authors: Kali Kaneko, <kali@leap.se> +@license: GPLv3, see included LICENSE file +@copyright: © 2013 Kali Kaneko, see COPYLEFT file +""" + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +import codecs +import locale +import os +import types +import tempfile +import shutil + + +from zope.interface import implements + +from twisted.mail.imap4 import MessageSet +from twisted.mail import imap4 +from twisted.protocols import loopback +from twisted.internet import defer +from twisted.internet import error +from twisted.internet import reactor +from twisted.internet import interfaces +from twisted.internet.task import Clock +from twisted.trial import unittest +from twisted.python import util, log +from twisted.python import failure + +from twisted import cred +import twisted.cred.error +import twisted.cred.checkers +import twisted.cred.credentials +import twisted.cred.portal + +from twisted.test.proto_helpers import StringTransport, StringTransportWithDisconnection + + +import u1db + +from leap.common.testing.basetest import BaseLeapTest +from leap.mail.imap.server import SoledadMailbox +from leap.mail.imap.tests import PUBLIC_KEY +from leap.mail.imap.tests import PRIVATE_KEY + +from leap.soledad import Soledad +from leap.soledad.util import GPGWrapper +from leap.soledad.backends.leap_backend import LeapDocument + + +def strip(f): + return lambda result, f=f: f() + + +def sortNest(l): + l = l[:] + l.sort() + for i in range(len(l)): + if isinstance(l[i], types.ListType): + l[i] = sortNest(l[i]) + elif isinstance(l[i], types.TupleType): + l[i] = tuple(sortNest(list(l[i]))) + return l + + +def initialize_soledad(email, gnupg_home, tempdir): + """ + initializes soledad by hand + """ + _soledad = Soledad(email, gnupg_home=gnupg_home, + initialize=False, + prefix=tempdir) + _soledad._init_dirs() + _soledad._gpg = GPGWrapper(gnupghome=gnupg_home) + _soledad._gpg.import_keys(PUBLIC_KEY) + _soledad._gpg.import_keys(PRIVATE_KEY) + _soledad._load_openpgp_keypair() + if not _soledad._has_secret(): + _soledad._gen_secret() + _soledad._load_secret() + _soledad._init_db() + return _soledad + + +########################################## +# account, simpleserver +########################################## + + +class SoledadBackedAccount(imap4.MemoryAccount): + #mailboxFactory = SimpleMailbox + mailboxFactory = SoledadMailbox + soledadInstance = None + + # XXX should reimplement IAccount -> SoledadAccount + # and receive the soledad instance on the constructor. + # SoledadMailbox should allow to filter by mailbox name + # _soledad db should include mailbox field + # and a document with "INDEX" info (mailboxes / subscriptions) + + def _emptyMailbox(self, name, id): + return self.mailboxFactory(self.soledadInstance) + + def select(self, name, rw=1): + # XXX rethink this. + # Need to be classmethods... + mbox = imap4.MemoryAccount.select(self, name) + if mbox is not None: + mbox.rw = rw + return mbox + + +class SimpleLEAPServer(imap4.IMAP4Server): + def __init__(self, *args, **kw): + imap4.IMAP4Server.__init__(self, *args, **kw) + realm = TestRealm() + realm.theAccount = SoledadBackedAccount('testuser') + # XXX soledadInstance here? + + portal = cred.portal.Portal(realm) + c = cred.checkers.InMemoryUsernamePasswordDatabaseDontUse() + self.checker = c + self.portal = portal + portal.registerChecker(c) + self.timeoutTest = False + + def lineReceived(self, line): + if self.timeoutTest: + #Do not send a respones + return + + imap4.IMAP4Server.lineReceived(self, line) + + _username = 'testuser' + _password = 'password-test' + + def authenticateLogin(self, username, password): + if username == self._username and password == self._password: + return imap4.IAccount, self.theAccount, lambda: None + raise cred.error.UnauthorizedLogin() + + +class TestRealm: + theAccount = None + + def requestAvatar(self, avatarId, mind, *interfaces): + return imap4.IAccount, self.theAccount, lambda: None + +###################### +# Test LEAP Server +###################### + + +class SimpleClient(imap4.IMAP4Client): + + def __init__(self, deferred, contextFactory=None): + imap4.IMAP4Client.__init__(self, contextFactory) + self.deferred = deferred + self.events = [] + + def serverGreeting(self, caps): + self.deferred.callback(None) + + def modeChanged(self, writeable): + self.events.append(['modeChanged', writeable]) + self.transport.loseConnection() + + def flagsChanged(self, newFlags): + self.events.append(['flagsChanged', newFlags]) + self.transport.loseConnection() + + def newMessages(self, exists, recent): + self.events.append(['newMessages', exists, recent]) + self.transport.loseConnection() + + +class IMAP4HelperMixin(BaseLeapTest): + + serverCTX = None + clientCTX = None + + @classmethod + def setUpClass(cls): + cls.old_path = os.environ['PATH'] + cls.old_home = os.environ['HOME'] + cls.tempdir = tempfile.mkdtemp(prefix="leap_tests-") + cls.home = cls.tempdir + bin_tdir = os.path.join( + cls.tempdir, + 'bin') + os.environ["PATH"] = bin_tdir + os.environ["HOME"] = cls.tempdir + + # Soledad: config info + cls.gnupg_home = "%s/gnupg" % cls.tempdir + cls.email = 'leap@leap.se' + #cls.db1_file = "%s/db1.u1db" % cls.tempdir + #cls.db2_file = "%s/db2.u1db" % cls.tempdir + # open test dbs + #cls._db1 = u1db.open(cls.db1_file, create=True, + #document_factory=LeapDocument) + #cls._db2 = u1db.open(cls.db2_file, create=True, + #document_factory=LeapDocument) + + # initialize soledad by hand so we can control keys + cls._soledad = initialize_soledad( + cls.email, + cls.gnupg_home, + cls.tempdir) + + cls.sm = SoledadMailbox(soledad=cls._soledad) + + @classmethod + def tearDownClass(cls): + #cls._db1.close() + #cls._db2.close() + cls._soledad.close() + + os.environ["PATH"] = cls.old_path + os.environ["HOME"] = cls.old_home + # safety check + assert cls.tempdir.startswith('/tmp/leap_tests-') + shutil.rmtree(cls.tempdir) + + def setUp(self): + d = defer.Deferred() + self.server = SimpleLEAPServer(contextFactory=self.serverCTX) + self.client = SimpleClient(d, contextFactory=self.clientCTX) + self.connected = d + + theAccount = SoledadBackedAccount('testuser') + theAccount.soledadInstance = self._soledad + + # XXX used for something??? + #theAccount.mboxType = SoledadMailbox + SimpleLEAPServer.theAccount = theAccount + + def tearDown(self): + self.delete_all_docs() + del self.server + del self.client + del self.connected + + def populateMessages(self): + self._soledad.messages.add_msg(subject="test1") + self._soledad.messages.add_msg(subject="test2") + self._soledad.messages.add_msg(subject="test3") + # XXX should change Flags too + self._soledad.messages.add_msg(subject="test4") + + def delete_all_docs(self): + self.server.theAccount.messages.deleteAllDocs() + + def _cbStopClient(self, ignore): + self.client.transport.loseConnection() + + def _ebGeneral(self, failure): + self.client.transport.loseConnection() + self.server.transport.loseConnection() + log.err(failure, "Problem with %r" % (self.function,)) + + def loopback(self): + return loopback.loopbackAsync(self.server, self.client) + + +class IMAP4ServerTestCase(IMAP4HelperMixin, unittest.TestCase): + + def testCapability(self): + caps = {} + + def getCaps(): + def gotCaps(c): + caps.update(c) + self.server.transport.loseConnection() + return self.client.getCapabilities().addCallback(gotCaps) + d1 = self.connected.addCallback( + strip(getCaps)).addErrback(self._ebGeneral) + d = defer.gatherResults([self.loopback(), d1]) + expected = {'IMAP4rev1': None, 'NAMESPACE': None, 'IDLE': None} + + return d.addCallback(lambda _: self.assertEqual(expected, caps)) + + def testCapabilityWithAuth(self): + caps = {} + self.server.challengers[ + 'CRAM-MD5'] = cred.credentials.CramMD5Credentials + + def getCaps(): + def gotCaps(c): + caps.update(c) + self.server.transport.loseConnection() + return self.client.getCapabilities().addCallback(gotCaps) + d1 = self.connected.addCallback( + strip(getCaps)).addErrback(self._ebGeneral) + d = defer.gatherResults([self.loopback(), d1]) + + expCap = {'IMAP4rev1': None, 'NAMESPACE': None, + 'IDLE': None, 'AUTH': ['CRAM-MD5']} + + return d.addCallback(lambda _: self.assertEqual(expCap, caps)) + + def testLogout(self): + self.loggedOut = 0 + + def logout(): + def setLoggedOut(): + self.loggedOut = 1 + self.client.logout().addCallback(strip(setLoggedOut)) + self.connected.addCallback(strip(logout)).addErrback(self._ebGeneral) + d = self.loopback() + return d.addCallback(lambda _: self.assertEqual(self.loggedOut, 1)) + + def testNoop(self): + self.responses = None + + def noop(): + def setResponses(responses): + self.responses = responses + self.server.transport.loseConnection() + self.client.noop().addCallback(setResponses) + self.connected.addCallback(strip(noop)).addErrback(self._ebGeneral) + d = self.loopback() + return d.addCallback(lambda _: self.assertEqual(self.responses, [])) + + def testLogin(self): + def login(): + d = self.client.login('testuser', 'password-test') + d.addCallback(self._cbStopClient) + d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral) + d = defer.gatherResults([d1, self.loopback()]) + return d.addCallback(self._cbTestLogin) + + def _cbTestLogin(self, ignored): + self.assertEqual(self.server.account, SimpleLEAPServer.theAccount) + self.assertEqual(self.server.state, 'auth') + + def testFailedLogin(self): + def login(): + d = self.client.login('testuser', 'wrong-password') + d.addBoth(self._cbStopClient) + + d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral) + d2 = self.loopback() + d = defer.gatherResults([d1, d2]) + return d.addCallback(self._cbTestFailedLogin) + + def _cbTestFailedLogin(self, ignored): + self.assertEqual(self.server.account, None) + self.assertEqual(self.server.state, 'unauth') + + + def testLoginRequiringQuoting(self): + self.server._username = '{test}user' + self.server._password = '{test}password' + + def login(): + d = self.client.login('{test}user', '{test}password') + d.addBoth(self._cbStopClient) + + d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral) + d = defer.gatherResults([self.loopback(), d1]) + return d.addCallback(self._cbTestLoginRequiringQuoting) + + def _cbTestLoginRequiringQuoting(self, ignored): + self.assertEqual(self.server.account, SimpleLEAPServer.theAccount) + self.assertEqual(self.server.state, 'auth') + + + def testNamespace(self): + self.namespaceArgs = None + def login(): + return self.client.login('testuser', 'password-test') + def namespace(): + def gotNamespace(args): + self.namespaceArgs = args + self._cbStopClient(None) + return self.client.namespace().addCallback(gotNamespace) + + d1 = self.connected.addCallback(strip(login)) + d1.addCallback(strip(namespace)) + d1.addErrback(self._ebGeneral) + d2 = self.loopback() + d = defer.gatherResults([d1, d2]) + d.addCallback(lambda _: self.assertEqual(self.namespaceArgs, + [[['', '/']], [], []])) + return d + + def testSelect(self): + SimpleLEAPServer.theAccount.addMailbox('test-mailbox') + self.selectedArgs = None + + def login(): + return self.client.login('testuser', 'password-test') + + def select(): + def selected(args): + self.selectedArgs = args + self._cbStopClient(None) + d = self.client.select('test-mailbox') + d.addCallback(selected) + return d + + d1 = self.connected.addCallback(strip(login)) + d1.addCallback(strip(select)) + d1.addErrback(self._ebGeneral) + d2 = self.loopback() + return defer.gatherResults([d1, d2]).addCallback(self._cbTestSelect) + + def _cbTestSelect(self, ignored): + mbox = SimpleLEAPServer.theAccount.mailboxes['TEST-MAILBOX'] + self.assertEqual(self.server.mbox, mbox) + self.assertEqual(self.selectedArgs, { + 'EXISTS': 9, 'RECENT': 3, 'UIDVALIDITY': 42, + 'FLAGS': ('\\Seen', '\\Answered', '\\Flagged', + '\\Deleted', '\\Draft', '\\Recent', 'List'), + 'READ-WRITE': 1 + }) + + def test_examine(self): + """ + L{IMAP4Client.examine} issues an I{EXAMINE} command to the server and + returns a L{Deferred} which fires with a C{dict} with as many of the + following keys as the server includes in its response: C{'FLAGS'}, + C{'EXISTS'}, C{'RECENT'}, C{'UNSEEN'}, C{'READ-WRITE'}, C{'READ-ONLY'}, + C{'UIDVALIDITY'}, and C{'PERMANENTFLAGS'}. + + Unfortunately the server doesn't generate all of these so it's hard to + test the client's handling of them here. See + L{IMAP4ClientExamineTests} below. + + See U{RFC 3501<http://www.faqs.org/rfcs/rfc3501.html>}, section 6.3.2, + for details. + """ + SimpleLEAPServer.theAccount.addMailbox('test-mailbox') + self.examinedArgs = None + + def login(): + return self.client.login('testuser', 'password-test') + + def examine(): + def examined(args): + self.examinedArgs = args + self._cbStopClient(None) + d = self.client.examine('test-mailbox') + d.addCallback(examined) + return d + + d1 = self.connected.addCallback(strip(login)) + d1.addCallback(strip(examine)) + d1.addErrback(self._ebGeneral) + d2 = self.loopback() + d = defer.gatherResults([d1, d2]) + return d.addCallback(self._cbTestExamine) + + def _cbTestExamine(self, ignored): + mbox = SimpleLEAPServer.theAccount.mailboxes['TEST-MAILBOX'] + self.assertEqual(self.server.mbox, mbox) + self.assertEqual(self.examinedArgs, { + 'EXISTS': 9, 'RECENT': 3, 'UIDVALIDITY': 42, + 'FLAGS': ('\\Seen', '\\Answered', '\\Flagged', + '\\Deleted', '\\Draft', '\\Recent', 'List'), + 'READ-WRITE': False}) + + def testCreate(self): + succeed = ('testbox', 'test/box', 'test/', 'test/box/box', 'INBOX') + fail = ('testbox', 'test/box') + + def cb(): + self.result.append(1) + + def eb(failure): + self.result.append(0) + + def login(): + return self.client.login('testuser', 'password-test') + + def create(): + for name in succeed + fail: + d = self.client.create(name) + d.addCallback(strip(cb)).addErrback(eb) + d.addCallbacks(self._cbStopClient, self._ebGeneral) + + self.result = [] + d1 = self.connected.addCallback(strip(login)).addCallback( + strip(create)) + d2 = self.loopback() + d = defer.gatherResults([d1, d2]) + return d.addCallback(self._cbTestCreate, succeed, fail) + + def _cbTestCreate(self, ignored, succeed, fail): + self.assertEqual(self.result, [1] * len(succeed) + [0] * len(fail)) + mbox = SimpleLEAPServer.theAccount.mailboxes.keys() + answers = ['inbox', 'testbox', 'test/box', 'test', 'test/box/box'] + mbox.sort() + answers.sort() + self.assertEqual(mbox, [a.upper() for a in answers]) + + def testDelete(self): + SimpleLEAPServer.theAccount.addMailbox('delete/me') + + def login(): + return self.client.login('testuser', 'password-test') + + def delete(): + return self.client.delete('delete/me') + + d1 = self.connected.addCallback(strip(login)) + d1.addCallbacks(strip(delete), self._ebGeneral) + d1.addCallbacks(self._cbStopClient, self._ebGeneral) + d2 = self.loopback() + d = defer.gatherResults([d1, d2]) + d.addCallback(lambda _: + self.assertEqual(SimpleLEAPServer.theAccount.mailboxes.keys(), [])) + return d + + def testIllegalInboxDelete(self): + self.stashed = None + + def login(): + return self.client.login('testuser', 'password-test') + + def delete(): + return self.client.delete('inbox') + + def stash(result): + self.stashed = result + + d1 = self.connected.addCallback(strip(login)) + d1.addCallbacks(strip(delete), self._ebGeneral) + d1.addBoth(stash) + d1.addCallbacks(self._cbStopClient, self._ebGeneral) + d2 = self.loopback() + d = defer.gatherResults([d1, d2]) + d.addCallback(lambda _: self.failUnless(isinstance(self.stashed, + failure.Failure))) + return d + + def testNonExistentDelete(self): + + def login(): + return self.client.login('testuser', 'password-test') + + def delete(): + return self.client.delete('delete/me') + + def deleteFailed(failure): + self.failure = failure + + self.failure = None + d1 = self.connected.addCallback(strip(login)) + d1.addCallback(strip(delete)).addErrback(deleteFailed) + d1.addCallbacks(self._cbStopClient, self._ebGeneral) + d2 = self.loopback() + d = defer.gatherResults([d1, d2]) + d.addCallback(lambda _: self.assertEqual(str(self.failure.value), + 'No such mailbox')) + return d + + def testIllegalDelete(self): + m = SoledadMailbox() + m.flags = (r'\Noselect',) + SimpleLEAPServer.theAccount.addMailbox('delete', m) + SimpleLEAPServer.theAccount.addMailbox('delete/me') + + def login(): + return self.client.login('testuser', 'password-test') + + def delete(): + return self.client.delete('delete') + + def deleteFailed(failure): + self.failure = failure + + self.failure = None + d1 = self.connected.addCallback(strip(login)) + d1.addCallback(strip(delete)).addErrback(deleteFailed) + d1.addCallbacks(self._cbStopClient, self._ebGeneral) + d2 = self.loopback() + d = defer.gatherResults([d1, d2]) + expected = ("Hierarchically inferior mailboxes exist " + "and \\Noselect is set") + d.addCallback(lambda _: + self.assertEqual(str(self.failure.value), expected)) + return d + + def testRename(self): + SimpleLEAPServer.theAccount.addMailbox('oldmbox') + + def login(): + return self.client.login('testuser', 'password-test') + + def rename(): + return self.client.rename('oldmbox', 'newname') + + d1 = self.connected.addCallback(strip(login)) + d1.addCallbacks(strip(rename), self._ebGeneral) + d1.addCallbacks(self._cbStopClient, self._ebGeneral) + d2 = self.loopback() + d = defer.gatherResults([d1, d2]) + d.addCallback(lambda _: + self.assertEqual( + SimpleLEAPServer.theAccount.mailboxes.keys(), + ['NEWNAME'])) + return d + + def testIllegalInboxRename(self): + self.stashed = None + + def login(): + return self.client.login('testuser', 'password-test') + + def rename(): + return self.client.rename('inbox', 'frotz') + + def stash(stuff): + self.stashed = stuff + + d1 = self.connected.addCallback(strip(login)) + d1.addCallbacks(strip(rename), self._ebGeneral) + d1.addBoth(stash) + d1.addCallbacks(self._cbStopClient, self._ebGeneral) + d2 = self.loopback() + d = defer.gatherResults([d1, d2]) + d.addCallback(lambda _: + self.failUnless(isinstance( + self.stashed, failure.Failure))) + return d + + def testHierarchicalRename(self): + SimpleLEAPServer.theAccount.create('oldmbox/m1') + SimpleLEAPServer.theAccount.create('oldmbox/m2') + + def login(): + return self.client.login('testuser', 'password-test') + + def rename(): + return self.client.rename('oldmbox', 'newname') + + d1 = self.connected.addCallback(strip(login)) + d1.addCallbacks(strip(rename), self._ebGeneral) + d1.addCallbacks(self._cbStopClient, self._ebGeneral) + d2 = self.loopback() + d = defer.gatherResults([d1, d2]) + return d.addCallback(self._cbTestHierarchicalRename) + + def _cbTestHierarchicalRename(self, ignored): + mboxes = SimpleLEAPServer.theAccount.mailboxes.keys() + expected = ['newname', 'newname/m1', 'newname/m2'] + mboxes.sort() + self.assertEqual(mboxes, [s.upper() for s in expected]) + + def testSubscribe(self): + + def login(): + return self.client.login('testuser', 'password-test') + + def subscribe(): + return self.client.subscribe('this/mbox') + + d1 = self.connected.addCallback(strip(login)) + d1.addCallbacks(strip(subscribe), self._ebGeneral) + d1.addCallbacks(self._cbStopClient, self._ebGeneral) + d2 = self.loopback() + d = defer.gatherResults([d1, d2]) + d.addCallback(lambda _: + self.assertEqual(SimpleLEAPServer.theAccount.subscriptions, + ['THIS/MBOX'])) + return d + + def testUnsubscribe(self): + SimpleLEAPServer.theAccount.subscriptions = ['THIS/MBOX', 'THAT/MBOX'] + def login(): + return self.client.login('testuser', 'password-test') + def unsubscribe(): + return self.client.unsubscribe('this/mbox') + + d1 = self.connected.addCallback(strip(login)) + d1.addCallbacks(strip(unsubscribe), self._ebGeneral) + d1.addCallbacks(self._cbStopClient, self._ebGeneral) + d2 = self.loopback() + d = defer.gatherResults([d1, d2]) + d.addCallback(lambda _: + self.assertEqual(SimpleLEAPServer.theAccount.subscriptions, + ['THAT/MBOX'])) + return d + + def _listSetup(self, f): + SimpleLEAPServer.theAccount.addMailbox('root/subthing') + SimpleLEAPServer.theAccount.addMailbox('root/another-thing') + SimpleLEAPServer.theAccount.addMailbox('non-root/subthing') + + def login(): + return self.client.login('testuser', 'password-test') + + def listed(answers): + self.listed = answers + + self.listed = None + d1 = self.connected.addCallback(strip(login)) + d1.addCallbacks(strip(f), self._ebGeneral) + d1.addCallbacks(listed, self._ebGeneral) + d1.addCallbacks(self._cbStopClient, self._ebGeneral) + d2 = self.loopback() + return defer.gatherResults([d1, d2]).addCallback(lambda _: self.listed) + + def testList(self): + def list(): + return self.client.list('root', '%') + d = self._listSetup(list) + d.addCallback(lambda listed: self.assertEqual( + sortNest(listed), + sortNest([ + (SoledadMailbox.flags, "/", "ROOT/SUBTHING"), + (SoledadMailbox.flags, "/", "ROOT/ANOTHER-THING") + ]) + )) + return d + + def testLSub(self): + SimpleLEAPServer.theAccount.subscribe('ROOT/SUBTHING') + + def lsub(): + return self.client.lsub('root', '%') + d = self._listSetup(lsub) + d.addCallback(self.assertEqual, + [(SoledadMailbox.flags, "/", "ROOT/SUBTHING")]) + return d + + def testStatus(self): + SimpleLEAPServer.theAccount.addMailbox('root/subthing') + + def login(): + return self.client.login('testuser', 'password-test') + + def status(): + return self.client.status( + 'root/subthing', 'MESSAGES', 'UIDNEXT', 'UNSEEN') + + def statused(result): + self.statused = result + + self.statused = None + d1 = self.connected.addCallback(strip(login)) + d1.addCallbacks(strip(status), self._ebGeneral) + d1.addCallbacks(statused, self._ebGeneral) + d1.addCallbacks(self._cbStopClient, self._ebGeneral) + d2 = self.loopback() + d = defer.gatherResults([d1, d2]) + d.addCallback(lambda _: self.assertEqual( + self.statused, + {'MESSAGES': 9, 'UIDNEXT': '10', 'UNSEEN': 4} + )) + return d + + def testFailedStatus(self): + def login(): + return self.client.login('testuser', 'password-test') + + def status(): + return self.client.status( + 'root/nonexistent', 'MESSAGES', 'UIDNEXT', 'UNSEEN') + + def statused(result): + self.statused = result + + def failed(failure): + self.failure = failure + + self.statused = self.failure = None + d1 = self.connected.addCallback(strip(login)) + d1.addCallbacks(strip(status), self._ebGeneral) + d1.addCallbacks(statused, failed) + d1.addCallbacks(self._cbStopClient, self._ebGeneral) + d2 = self.loopback() + return defer.gatherResults([d1, d2]).addCallback( + self._cbTestFailedStatus) + + def _cbTestFailedStatus(self, ignored): + self.assertEqual( + self.statused, None + ) + self.assertEqual( + self.failure.value.args, + ('Could not open mailbox',) + ) + + def testFullAppend(self): + infile = util.sibpath(__file__, 'rfc822.message') + message = open(infile) + SimpleLEAPServer.theAccount.addMailbox('root/subthing') + + def login(): + return self.client.login('testuser', 'password-test') + + def append(): + return self.client.append( + 'root/subthing', + message, + ('\\SEEN', '\\DELETED'), + 'Tue, 17 Jun 2003 11:22:16 -0600 (MDT)', + ) + + d1 = self.connected.addCallback(strip(login)) + d1.addCallbacks(strip(append), self._ebGeneral) + d1.addCallbacks(self._cbStopClient, self._ebGeneral) + d2 = self.loopback() + d = defer.gatherResults([d1, d2]) + return d.addCallback(self._cbTestFullAppend, infile) + + def _cbTestFullAppend(self, ignored, infile): + mb = SimpleLEAPServer.theAccount.mailboxes['ROOT/SUBTHING'] + self.assertEqual(1, len(mb.messages)) + self.assertEqual( + (['\\SEEN', '\\DELETED'], 'Tue, 17 Jun 2003 11:22:16 -0600 (MDT)', 0), + mb.messages[0][1:] + ) + self.assertEqual(open(infile).read(), mb.messages[0][0].getvalue()) + + def testPartialAppend(self): + infile = util.sibpath(__file__, 'rfc822.message') + message = open(infile) + SimpleLEAPServer.theAccount.addMailbox('PARTIAL/SUBTHING') + + def login(): + return self.client.login('testuser', 'password-test') + + def append(): + message = file(infile) + return self.client.sendCommand( + imap4.Command( + 'APPEND', + 'PARTIAL/SUBTHING (\\SEEN) "Right now" {%d}' % os.path.getsize(infile), + (), self.client._IMAP4Client__cbContinueAppend, message + ) + ) + d1 = self.connected.addCallback(strip(login)) + d1.addCallbacks(strip(append), self._ebGeneral) + d1.addCallbacks(self._cbStopClient, self._ebGeneral) + d2 = self.loopback() + d = defer.gatherResults([d1, d2]) + return d.addCallback(self._cbTestPartialAppend, infile) + + def _cbTestPartialAppend(self, ignored, infile): + mb = SimpleLEAPServer.theAccount.mailboxes['PARTIAL/SUBTHING'] + self.assertEqual(1, len(mb.messages)) + self.assertEqual( + (['\\SEEN'], 'Right now', 0), + mb.messages[0][1:] + ) + self.assertEqual(open(infile).read(), mb.messages[0][0].getvalue()) + + def testCheck(self): + SimpleLEAPServer.theAccount.addMailbox('root/subthing') + + def login(): + return self.client.login('testuser', 'password-test') + + def select(): + return self.client.select('root/subthing') + + def check(): + return self.client.check() + + d = self.connected.addCallback(strip(login)) + d.addCallbacks(strip(select), self._ebGeneral) + d.addCallbacks(strip(check), self._ebGeneral) + d.addCallbacks(self._cbStopClient, self._ebGeneral) + return self.loopback() + + # Okay, that was fun + + def testClose(self): + m = SoledadMailbox() + m.messages = [ + ('Message 1', ('\\Deleted', 'AnotherFlag'), None, 0), + ('Message 2', ('AnotherFlag',), None, 1), + ('Message 3', ('\\Deleted',), None, 2), + ] + SimpleLEAPServer.theAccount.addMailbox('mailbox', m) + + def login(): + return self.client.login('testuser', 'password-test') + + def select(): + return self.client.select('mailbox') + + def close(): + return self.client.close() + + d = self.connected.addCallback(strip(login)) + d.addCallbacks(strip(select), self._ebGeneral) + d.addCallbacks(strip(close), self._ebGeneral) + d.addCallbacks(self._cbStopClient, self._ebGeneral) + d2 = self.loopback() + return defer.gatherResults([d, d2]).addCallback(self._cbTestClose, m) + + def _cbTestClose(self, ignored, m): + self.assertEqual(len(m.messages), 1) + self.assertEqual(m.messages[0], + ('Message 2', ('AnotherFlag',), None, 1)) + self.failUnless(m.closed) + + def testExpunge(self): + m = SoledadMailbox() + m.messages = [ + ('Message 1', ('\\Deleted', 'AnotherFlag'), None, 0), + ('Message 2', ('AnotherFlag',), None, 1), + ('Message 3', ('\\Deleted',), None, 2), + ] + SimpleLEAPServer.theAccount.addMailbox('mailbox', m) + + def login(): + return self.client.login('testuser', 'password-test') + + def select(): + return self.client.select('mailbox') + + def expunge(): + return self.client.expunge() + + def expunged(results): + self.failIf(self.server.mbox is None) + self.results = results + + self.results = None + d1 = self.connected.addCallback(strip(login)) + d1.addCallbacks(strip(select), self._ebGeneral) + d1.addCallbacks(strip(expunge), self._ebGeneral) + d1.addCallbacks(expunged, self._ebGeneral) + d1.addCallbacks(self._cbStopClient, self._ebGeneral) + d2 = self.loopback() + d = defer.gatherResults([d1, d2]) + return d.addCallback(self._cbTestExpunge, m) + + def _cbTestExpunge(self, ignored, m): + self.assertEqual(len(m.messages), 1) + self.assertEqual(m.messages[0], + ('Message 2', ('AnotherFlag',), None, 1)) + + self.assertEqual(self.results, [0, 2]) + + + +class IMAP4ServerSearchTestCase(IMAP4HelperMixin, unittest.TestCase): + """ + Tests for the behavior of the search_* functions in L{imap4.IMAP4Server}. + """ + pass diff --git a/src/leap/mail/smtp/README.rst b/src/leap/mail/smtp/README.rst new file mode 100644 index 0000000..2b2a118 --- /dev/null +++ b/src/leap/mail/smtp/README.rst @@ -0,0 +1,43 @@ +Leap SMTP Relay +=============== + +Outgoing mail workflow: + + * LEAP client runs a thin SMTP proxy on the user's device, bound to + localhost. + * User's MUA is configured outgoing SMTP to localhost + * When SMTP proxy receives an email from MUA + * SMTP proxy queries Key Manager for the user's private key and public + keys of all recipients + * Message is signed by sender and encrypted to recipients. + * If recipient's key is missing, email goes out in cleartext (unless + user has configured option to send only encrypted email) + * Finally, message is relayed to provider's SMTP relay + + +Dependencies +------------ + +Leap SMTP Relay depends on the following python libraries: + + * Twisted 12.3.0 [1] + * zope.interface 4.0.3 [2] + +[1] http://pypi.python.org/pypi/Twisted/12.3.0 +[2] http://pypi.python.org/pypi/zope.interface/4.0.3 + + +How to run +---------- + +To launch the SMTP relay, run the following command: + + twistd -y smtprelay.tac + + +Running tests +------------- + +Tests are run using Twisted's Trial API, like this: + + trial leap.email.smtp.tests diff --git a/src/leap/mail/smtp/__init__.py b/src/leap/mail/smtp/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/leap/mail/smtp/__init__.py diff --git a/src/leap/mail/smtp/smtprelay.py b/src/leap/mail/smtp/smtprelay.py new file mode 100644 index 0000000..6479873 --- /dev/null +++ b/src/leap/mail/smtp/smtprelay.py @@ -0,0 +1,246 @@ +""" +LEAP SMTP encrypted relay. +""" + +import re +import gnupg +from zope.interface import implements +from StringIO import StringIO +from twisted.mail import smtp +from twisted.internet.protocol import ServerFactory +from twisted.internet import reactor +from twisted.internet import defer +from twisted.application import internet, service +from twisted.python import log +from email.Header import Header +from leap import soledad + + +class SMTPInfoNotAvailable(Exception): + pass + + +class SMTPFactory(ServerFactory): + """ + Factory for an SMTP server with encrypted relaying capabilities. + """ + + def __init__(self, soledad, gpg=None): + self._soledad = soledad + self._gpg = gpg + + def buildProtocol(self, addr): + "Return a protocol suitable for the job." + # TODO: use ESMTP here. + smtpProtocol = smtp.SMTP(SMTPDelivery(self._soledad, self._gpg)) + smtpProtocol.factory = self + return smtpProtocol + + +class SMTPDelivery(object): + """ + Validate email addresses and handle message delivery. + """ + + implements(smtp.IMessageDelivery) + + def __init__(self, soledad, gpg=None): + self._soledad = soledad + if gpg: + self._gpg = gpg + else: + self._gpg = GPGWrapper() + + def receivedHeader(self, helo, origin, recipients): + myHostname, clientIP = helo + headerValue = "by %s from %s with ESMTP ; %s" % ( + myHostname, clientIP, smtp.rfc822date()) + # email.Header.Header used for automatic wrapping of long lines + return "Received: %s" % Header(headerValue) + + def validateTo(self, user): + """Assert existence of and trust on recipient's GPG public key.""" + # try to find recipient's public key + try: + # this will raise an exception if key is not found + trust = self._gpg.find_key(user.dest.addrstr)['trust'] + # if key is not ultimatelly trusted, then the message will not + # be encrypted. So, we check for this below + #if trust != 'u': + # raise smtp.SMTPBadRcpt(user) + log.msg("Accepting mail for %s..." % user.dest) + return lambda: EncryptedMessage(user, soledad=self._soledad, + gpg=self._gpg) + except LookupError: + # if key was not found, check config to see if will send anyway. + if self.encrypted_only: + raise smtp.SMTPBadRcpt(user) + # TODO: send signal to cli/gui that this user's key was not found? + log.msg("Warning: will send an unencrypted message (because " + "encrypted_only' is set to False).") + + def validateFrom(self, helo, originAddress): + # accept mail from anywhere. To reject an address, raise + # smtp.SMTPBadSender here. + return originAddress + + +class EncryptedMessage(): + """ + Receive plaintext from client, encrypt it and send message to a + recipient. + """ + implements(smtp.IMessage) + + SMTP_HOSTNAME = "mail.leap.se" + SMTP_PORT = 25 + + def __init__(self, user, soledad, gpg=None): + self.user = user + self._soledad = soledad + self.fetchConfig() + self.lines = [] + if gpg: + self._gpg = gpg + else: + self._gpg = GPGWrapper() + + def lineReceived(self, line): + """Store email DATA lines as they arrive.""" + self.lines.append(line) + + def eomReceived(self): + """Encrypt and send message.""" + log.msg("Message data complete.") + self.lines.append('') # add a trailing newline + self.parseMessage() + try: + self.encrypt() + return self.sendMessage() + except LookupError: + return None + + def parseMessage(self): + """Separate message headers from body.""" + sep = self.lines.index('') + self.headers = self.lines[:sep] + self.body = self.lines[sep + 1:] + + def connectionLost(self): + log.msg("Connection lost unexpectedly!") + log.err() + # unexpected loss of connection; don't save + self.lines = [] + + def sendSuccess(self, r): + log.msg(r) + + def sendError(self, e): + log.msg(e) + log.err() + + def prepareHeader(self): + self.headers.insert(1, "From: %s" % self.user.orig.addrstr) + self.headers.insert(2, "To: %s" % self.user.dest.addrstr) + self.headers.append('') + + def sendMessage(self): + self.prepareHeader() + msg = '\n'.join(self.headers + [self.cyphertext]) + d = defer.Deferred() + factory = smtp.ESMTPSenderFactory(self.smtp_username, + self.smtp_password, + self.smtp_username, + self.user.dest.addrstr, + StringIO(msg), + d) + # the next call is TSL-powered! + reactor.connectTCP(self.SMTP_HOSTNAME, self.SMTP_PORT, factory) + d.addCallback(self.sendSuccess) + d.addErrback(self.sendError) + return d + + def encrypt(self, always_trust=True): + # TODO: do not "always trust" here. + try: + fp = self._gpg.find_key(self.user.dest.addrstr)['fingerprint'] + log.msg("Encrypting to %s" % fp) + self.cyphertext = str( + self._gpg.encrypt( + '\n'.join(self.body), [fp], always_trust=always_trust)) + except LookupError: + if self.encrypted_only: + raise + log.msg("Warning: sending unencrypted mail (because " + "'encrypted_only' is set to False).") + + # this will be replaced by some other mechanism of obtaining credentials + # for SMTP server. + def fetchConfig(self): + # TODO: Soledad/LEAP bootstrap should store the SMTP info on local db, + # so this relay can load it when it needs. + if not self._soledad: + # TODO: uncomment below exception when integration with Soledad is + # smooth. + #raise SMTPInfoNotAvailable() + # TODO: remove dummy settings below when soledad bootstrap is + # working. + self.smtp_host = '' + self.smtp_port = '' + self.smtp_username = '' + self.smtp_password = '' + self.encrypted_only = True + else: + self.smtp_config = self._soledad.get_doc('smtp_relay_config') + for confname in [ + 'smtp_host', 'smtp_port', 'smtp_username', + 'smtp_password', 'encrypted_only', + ]: + setattr(self, confname, doc.content[confname]) + + +class GPGWrapper(): + """ + This is a temporary class for handling GPG requests, and should be + replaced by a more general class used throughout the project. + """ + + GNUPG_HOME = "~/.config/leap/gnupg" + GNUPG_BINARY = "/usr/bin/gpg" # TODO: change this based on OS + + def __init__(self, gpghome=GNUPG_HOME, gpgbinary=GNUPG_BINARY): + self.gpg = gnupg.GPG(gnupghome=gpghome, gpgbinary=gpgbinary) + + def find_key(self, email): + """ + Find user's key based on their email. + """ + for key in self.gpg.list_keys(): + for uid in key['uids']: + if re.search(email, uid): + return key + raise LookupError("GnuPG public key for %s not found!" % email) + + def encrypt(self, data, recipient, always_trust=True): + # TODO: do not 'always_trust'. + return self.gpg.encrypt(data, recipient, always_trust=always_trust) + + def decrypt(self, data): + return self.gpg.decrypt(data) + + def import_keys(self, data): + return self.gpg.import_keys(data) + + +# service configuration +port = 25 +user_email = 'user@leap.se' # TODO: replace for real mail from gui/cli + +# instantiate soledad for client app storage and sync +s = soledad.Soledad(user_email) +factory = SMTPFactory(s) + +# enable the use of this service with twistd +application = service.Application("LEAP SMTP Relay") +service = internet.TCPServer(port, factory) +service.setServiceParent(application) diff --git a/src/leap/mail/smtp/tests/185CA770.key b/src/leap/mail/smtp/tests/185CA770.key new file mode 100644 index 0000000..587b416 --- /dev/null +++ b/src/leap/mail/smtp/tests/185CA770.key @@ -0,0 +1,79 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +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 +=6HcJ +-----END PGP PRIVATE KEY BLOCK----- diff --git a/src/leap/mail/smtp/tests/185CA770.pub b/src/leap/mail/smtp/tests/185CA770.pub new file mode 100644 index 0000000..38af19f --- /dev/null +++ b/src/leap/mail/smtp/tests/185CA770.pub @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +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 +q3iu +=RChS +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/leap/mail/smtp/tests/__init__.py b/src/leap/mail/smtp/tests/__init__.py new file mode 100644 index 0000000..d7b942a --- /dev/null +++ b/src/leap/mail/smtp/tests/__init__.py @@ -0,0 +1,218 @@ +import os +import shutil +import tempfile + +from twisted.trial import unittest + +from leap.common.testing.basetest import BaseLeapTest +from leap.mail.smtp.smtprelay import GPGWrapper + + +class OpenPGPTestCase(unittest.TestCase, BaseLeapTest): + + def setUp(self): + # mimic LeapBaseTest.setUpClass behaviour, because this is deprecated + # in Twisted: http://twistedmatrix.com/trac/ticket/1870 + self.old_path = os.environ['PATH'] + self.old_home = os.environ['HOME'] + self.tempdir = tempfile.mkdtemp(prefix="leap_tests-") + self.home = self.tempdir + bin_tdir = os.path.join( + self.tempdir, + 'bin') + os.environ["PATH"] = bin_tdir + os.environ["HOME"] = self.tempdir + # setup our own stuff + self.gnupg_home = self.tempdir + '/gnupg' + os.mkdir(self.gnupg_home) + self.email = 'leap@leap.se' + self._gpg = GPGWrapper(gpghome=self.gnupg_home) + + self.assertEqual(self._gpg.import_keys(PUBLIC_KEY).summary(), + '1 imported', "error importing public key") + self.assertEqual(self._gpg.import_keys(PRIVATE_KEY).summary(), + # note that gnupg does not return a successful import + # for private keys. Bug? + '0 imported', "error importing private key") + + def tearDown(self): + # mimic LeapBaseTest.tearDownClass behaviour + os.environ["PATH"] = self.old_path + os.environ["HOME"] = self.old_home + # safety check + assert self.tempdir.startswith('/tmp/leap_tests-') + shutil.rmtree(self.tempdir) + + def test_openpgp_encrypt_decrypt(self): + "Test if openpgp can encrypt and decrypt." + text = "simple raw text" + encrypted = str(self._gpg.encrypt(text, KEY_FINGERPRINT, + # TODO: handle always trust issue + always_trust=True)) + self.assertNotEqual(text, encrypted, "failed encrypting text") + decrypted = str(self._gpg.decrypt(encrypted)) + self.assertEqual(text, decrypted, "failed decrypting text") + + +# Key material for testing +KEY_FINGERPRINT = "E36E738D69173C13D709E44F2F455E2824D18DDF" +PUBLIC_KEY = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +mQINBFC9+dkBEADNRfwV23TWEoGc/x0wWH1P7PlXt8MnC2Z1kKaKKmfnglVrpOiz +iLWoiU58sfZ0L5vHkzXHXCBf6Eiy/EtUIvdiWAn+yASJ1mk5jZTBKO/WMAHD8wTO +zpMsFmWyg3xc4DkmFa9KQ5EVU0o/nqPeyQxNMQN7px5pPwrJtJFmPxnxm+aDkPYx +irDmz/4DeDNqXliazGJKw7efqBdlwTHkl9Akw2gwy178pmsKwHHEMOBOFFvX61AT +huKqHYmlCGSliwbrJppTG7jc1/ls3itrK+CWTg4txREkSpEVmfcASvw/ZqLbjgfs +d/INMwXnR9U81O8+7LT6yw/ca4ppcFoJD7/XJbkRiML6+bJ4Dakiy6i727BzV17g +wI1zqNvm5rAhtALKfACha6YO43aJzairO4II1wxVHvRDHZn2IuKDDephQ3Ii7/vb +hUOf6XCSmchkAcpKXUOvbxm1yfB1LRa64mMc2RcZxf4mW7KQkulBsdV5QG2276lv +U2UUy2IutXcGP5nXC+f6sJJGJeEToKJ57yiO/VWJFjKN8SvP+7AYsQSqINUuEf6H +T5gCPCraGMkTUTPXrREvu7NOohU78q6zZNaL3GW8ai7eSeANSuQ8Vzffx7Wd8Y7i +Pw9sYj0SMFs1UgjbuL6pO5ueHh+qyumbtAq2K0Bci0kqOcU4E9fNtdiovQARAQAB +tBxMZWFwIFRlc3QgS2V5IDxsZWFwQGxlYXAuc2U+iQI3BBMBCAAhBQJQvfnZAhsD +BQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEC9FXigk0Y3fT7EQAKH3IuRniOpb +T/DDIgwwjz3oxB/W0DDMyPXowlhSOuM0rgGfntBpBb3boezEXwL86NPQxNGGruF5 +hkmecSiuPSvOmQlqlS95NGQp6hNG0YaKColh+Q5NTspFXCAkFch9oqUje0LdxfSP +QfV9UpeEvGyPmk1I9EJV/YDmZ4+Djge1d7qhVZInz4Rx1NrSyF/Tc2EC0VpjQFsU +Y9Kb2YBBR7ivG6DBc8ty0jJXi7B4WjkFcUEJviQpMF2dCLdonCehYs1PqsN1N7j+ +eFjQd+hqVMJgYuSGKjvuAEfClM6MQw7+FmFwMyLgK/Ew/DttHEDCri77SPSkOGSI +txCzhTg6798f6mJr7WcXmHX1w1Vcib5FfZ8vTDFVhz/XgAgArdhPo9V6/1dgSSiB +KPQ/spsco6u5imdOhckERE0lnAYvVT6KE81TKuhF/b23u7x+Wdew6kK0EQhYA7wy +7LmlaNXc7rMBQJ9Z60CJ4JDtatBWZ0kNrt2VfdDHVdqBTOpl0CraNUjWE5YMDasr +K2dF5IX8D3uuYtpZnxqg0KzyLg0tzL0tvOL1C2iudgZUISZNPKbS0z0v+afuAAnx +2pTC3uezbh2Jt8SWTLhll4i0P4Ps5kZ6HQUO56O+/Z1cWovX+mQekYFmERySDR9n +3k1uAwLilJmRmepGmvYbB8HloV8HqwgguQINBFC9+dkBEAC0I/xn1uborMgDvBtf +H0sEhwnXBC849/32zic6udB6/3Efk9nzbSpL3FSOuXITZsZgCHPkKarnoQ2ztMcS +sh1ke1C5gQGms75UVmM/nS+2YI4vY8OX/GC/on2vUyncqdH+bR6xH5hx4NbWpfTs +iQHmz5C6zzS/kuabGdZyKRaZHt23WQ7JX/4zpjqbC99DjHcP9BSk7tJ8wI4bkMYD +uFVQdT9O6HwyKGYwUU4sAQRAj7XCTGvVbT0dpgJwH4RmrEtJoHAx4Whg8mJ710E0 +GCmzf2jqkNuOw76ivgk27Kge+Hw00jmJjQhHY0yVbiaoJwcRrPKzaSjEVNgrpgP3 +lXPRGQArgESsIOTeVVHQ8fhK2YtTeCY9rIiO+L0OX2xo9HK7hfHZZWL6rqymXdyS +fhzh/f6IPyHFWnvj7Brl7DR8heMikygcJqv+ed2yx7iLyCUJ10g12I48+aEj1aLe +dP7lna32iY8/Z0SHQLNH6PXO9SlPcq2aFUgKqE75A/0FMk7CunzU1OWr2ZtTLNO1 +WT/13LfOhhuEq9jTyTosn0WxBjJKq18lnhzCXlaw6EAtbA7CUwsD3CTPR56aAXFK +3I7KXOVAqggrvMe5Tpdg5drfYpI8hZovL5aAgb+7Y5ta10TcJdUhS5K3kFAWe/td +U0cmWUMDP1UMSQ5Jg6JIQVWhSwARAQABiQIfBBgBCAAJBQJQvfnZAhsMAAoJEC9F +Xigk0Y3fRwsP/i0ElYCyxeLpWJTwo1iCLkMKz2yX1lFVa9nT1BVTPOQwr/IAc5OX +NdtbJ14fUsKL5pWgW8OmrXtwZm1y4euI1RPWWubG01ouzwnGzv26UcuHeqC5orZj +cOnKtL40y8VGMm8LoicVkRJH8blPORCnaLjdOtmA3rx/v2EXrJpSa3AhOy0ZSRXk +ZSrK68AVNwamHRoBSYyo0AtaXnkPX4+tmO8X8BPfj125IljubvwZPIW9VWR9UqCE +VPfDR1XKegVb6VStIywF7kmrknM1C5qUY28rdZYWgKorw01hBGV4jTW0cqde3N51 +XT1jnIAa+NoXUM9uQoGYMiwrL7vNsLlyyiW5ayDyV92H/rIuiqhFgbJsHTlsm7I8 +oGheR784BagAA1NIKD1qEO9T6Kz9lzlDaeWS5AUKeXrb7ZJLI1TTCIZx5/DxjLqM +Tt/RFBpVo9geZQrvLUqLAMwdaUvDXC2c6DaCPXTh65oCZj/hqzlJHH+RoTWWzKI+ +BjXxgUWF9EmZUBrg68DSmI+9wuDFsjZ51BcqvJwxyfxtTaWhdoYqH/UQS+D1FP3/ +diZHHlzwVwPICzM9ooNTgbrcDzyxRkIVqsVwBq7EtzcvgYUyX53yG25Giy6YQaQ2 +ZtQ/VymwFL3XdUWV6B/hU4PVAFvO3qlOtdJ6TpE+nEWgcWjCv5g7RjXX +=MuOY +-----END PGP PUBLIC KEY BLOCK----- +""" +PRIVATE_KEY = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +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 +=JTFu +-----END PGP PRIVATE KEY BLOCK----- +""" diff --git a/src/leap/mail/smtp/tests/mail.txt b/src/leap/mail/smtp/tests/mail.txt new file mode 100644 index 0000000..9542047 --- /dev/null +++ b/src/leap/mail/smtp/tests/mail.txt @@ -0,0 +1,10 @@ +HELO drebs@riseup.net +MAIL FROM: drebs@riseup.net +RCPT TO: drebs@riseup.net +RCPT TO: drebs@leap.se +DATA +Subject: leap test + +Hello world! +. +QUIT diff --git a/src/leap/mail/smtp/tests/test_smtprelay.py b/src/leap/mail/smtp/tests/test_smtprelay.py new file mode 100644 index 0000000..eaa4d04 --- /dev/null +++ b/src/leap/mail/smtp/tests/test_smtprelay.py @@ -0,0 +1,78 @@ +from datetime import datetime +import re +from leap.email.smtp.smtprelay import ( + SMTPFactory, + #SMTPDelivery, # an object + EncryptedMessage, +) +from leap.email.smtp import tests +from twisted.test import proto_helpers +from twisted.mail.smtp import User + + +# 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])" +HOSTNAME_REGEX = "(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*" + \ + "([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])" +IP_OR_HOST_REGEX = '(' + IP_REGEX + '|' + HOSTNAME_REGEX + ')' + + +class TestSmtpRelay(tests.OpenPGPTestCase): + + EMAIL_DATA = ['HELO relay.leap.se', + 'MAIL FROM: <user@leap.se>', + 'RCPT TO: <leap@leap.se>', + 'DATA', + 'From: User <user@leap.se>', + 'To: Leap <leap@leap.se>', + 'Date: ' + datetime.now().strftime('%c'), + 'Subject: test message', + '', + 'This is a secret message.', + 'Yours,', + 'A.', + '', + '.', + 'QUIT'] + + def assertMatch(self, string, pattern, msg=None): + if not re.match(pattern, string): + msg = self._formatMessage(msg, '"%s" does not match pattern "%s".' + % (string, pattern)) + raise self.failureException(msg) + + def test_relay_accepts_valid_email(self): + """ + Test if SMTP server responds correctly for valid interaction. + """ + + SMTP_ANSWERS = ['220 ' + IP_OR_HOST_REGEX + + ' NO UCE NO UBE NO RELAY PROBES', + '250 ' + IP_OR_HOST_REGEX + ' Hello ' + + IP_OR_HOST_REGEX + ', nice to meet you', + '250 Sender address accepted', + '250 Recipient address accepted', + '354 Continue'] + proto = SMTPFactory(None, self._gpg).buildProtocol(('127.0.0.1', 0)) + transport = proto_helpers.StringTransport() + proto.makeConnection(transport) + for i, line in enumerate(self.EMAIL_DATA): + proto.lineReceived(line + '\r\n') + self.assertMatch(transport.value(), + '\r\n'.join(SMTP_ANSWERS[0:i + 1])) + proto.setTimeout(None) + + def test_message_encrypt(self): + """ + Test if message gets encrypted to destination email. + """ + proto = SMTPFactory(None, self._gpg).buildProtocol(('127.0.0.1', 0)) + user = User('leap@leap.se', 'relay.leap.se', proto, 'leap@leap.se') + m = EncryptedMessage(user, None, self._gpg) + for line in self.EMAIL_DATA[4:12]: + m.lineReceived(line) + m.parseMessage() + m.encrypt() + decrypted = str(self._gpg.decrypt(m.cyphertext)) + self.assertEqual('\n'.join(self.EMAIL_DATA[9:12]), decrypted) |