diff options
Diffstat (limited to 'mail')
| -rw-r--r-- | mail/README.rst | 5 | ||||
| -rw-r--r-- | mail/pkg/requirements-dev.pip | 14 | ||||
| -rw-r--r-- | mail/pkg/requirements.pip | 3 | ||||
| -rw-r--r-- | mail/setup.py | 46 | ||||
| -rw-r--r-- | mail/src/leap/__init__.py | 6 | ||||
| -rw-r--r-- | mail/src/leap/mail/__init__.py | 0 | ||||
| -rw-r--r-- | mail/src/leap/mail/imap/__init__.py | 0 | ||||
| -rw-r--r-- | mail/src/leap/mail/imap/server.py | 558 | ||||
| -rw-r--r-- | mail/src/leap/mail/imap/tests/__init__.py | 232 | ||||
| -rwxr-xr-x | mail/src/leap/mail/imap/tests/imapclient.py | 206 | ||||
| -rw-r--r-- | mail/src/leap/mail/imap/tests/rfc822.message | 86 | ||||
| -rw-r--r-- | mail/src/leap/mail/imap/tests/test_imap.py | 957 | ||||
| -rw-r--r-- | mail/src/leap/mail/smtp/README.rst | 43 | ||||
| -rw-r--r-- | mail/src/leap/mail/smtp/__init__.py | 0 | ||||
| -rw-r--r-- | mail/src/leap/mail/smtp/smtprelay.py | 246 | ||||
| -rw-r--r-- | mail/src/leap/mail/smtp/tests/185CA770.key | 79 | ||||
| -rw-r--r-- | mail/src/leap/mail/smtp/tests/185CA770.pub | 52 | ||||
| -rw-r--r-- | mail/src/leap/mail/smtp/tests/__init__.py | 218 | ||||
| -rw-r--r-- | mail/src/leap/mail/smtp/tests/mail.txt | 10 | ||||
| -rw-r--r-- | mail/src/leap/mail/smtp/tests/test_smtprelay.py | 78 | 
20 files changed, 2839 insertions, 0 deletions
| diff --git a/mail/README.rst b/mail/README.rst new file mode 100644 index 0000000..92a4fa6 --- /dev/null +++ b/mail/README.rst @@ -0,0 +1,5 @@ +leap.mail +========= +Mail services for the LEAP CLient. + +More info: https://leap.se diff --git a/mail/pkg/requirements-dev.pip b/mail/pkg/requirements-dev.pip new file mode 100644 index 0000000..4bd76f6 --- /dev/null +++ b/mail/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/mail/pkg/requirements.pip b/mail/pkg/requirements.pip new file mode 100644 index 0000000..1b5e5ef --- /dev/null +++ b/mail/pkg/requirements.pip @@ -0,0 +1,3 @@ +leap.common +leap.soledad +twisted diff --git a/mail/setup.py b/mail/setup.py new file mode 100644 index 0000000..4de7251 --- /dev/null +++ b/mail/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/mail/src/leap/__init__.py b/mail/src/leap/__init__.py new file mode 100644 index 0000000..f48ad10 --- /dev/null +++ b/mail/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/mail/src/leap/mail/__init__.py b/mail/src/leap/mail/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/mail/src/leap/mail/__init__.py diff --git a/mail/src/leap/mail/imap/__init__.py b/mail/src/leap/mail/imap/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/mail/src/leap/mail/imap/__init__.py diff --git a/mail/src/leap/mail/imap/server.py b/mail/src/leap/mail/imap/server.py new file mode 100644 index 0000000..4e9c22c --- /dev/null +++ b/mail/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/mail/src/leap/mail/imap/tests/__init__.py b/mail/src/leap/mail/imap/tests/__init__.py new file mode 100644 index 0000000..9a4c663 --- /dev/null +++ b/mail/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) + +lQcYBFC9+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 +AA/+JHtlL39G1wsH9R6UEfUQJGXR9MiIiwZoKcnRB2o8+DS+OLjg0JOh8XehtuCs +E/8oGQKtQqa5bEIstX7IZoYmYFiUQi9LOzIblmp2vxOm+HKkxa4JszWci2/ZmC3t +KtaA4adl9XVnshoQ7pijuCMUKB3naBEOAxd8s9d/JeReGIYkJErdrnVfNk5N71Ds +FmH5Ll3XtEDvgBUQP3nkA6QFjpsaB94FHjL3gDwum/cxzj6pCglcvHOzEhfY0Ddb +J967FozQTaf2JW3O+w3LOqtcKWpq87B7+O61tVidQPSSuzPjCtFF0D2LC9R/Hpky +KTMQ6CaKja4MPhjwywd4QPcHGYSqjMpflvJqi+kYIt8psUK/YswWjnr3r4fbuqVY +VhtiHvnBHQjz135lUqWvEz4hM3Xpnxydx7aRlv5NlevK8+YIO5oFbWbGNTWsPZI5 +jpoFBpSsnR1Q5tnvtNHauvoWV+XN2qAOBTG+/nEbDYH6Ak3aaE9jrpTdYh0CotYF +q7csANsDy3JvkAzeU6WnYpsHHaAjqOGyiZGsLej1UcXPFMosE/aUo4WQhiS8Zx2c +zOVKOi/X5vQ2GdNT9Qolz8AriwzsvFR+bxPzyd8V6ALwDsoXvwEYinYBKK8j0OPv +OOihSR6HVsuP9NUZNU9ewiGzte/+/r6pNXHvR7wTQ8EWLcEIAN6Zyrb0bHZTIlxt +VWur/Ht2mIZrBaO50qmM5RD3T5oXzWXi/pjLrIpBMfeZR9DWfwQwjYzwqi7pxtYx +nJvbMuY505rfnMoYxb4J+cpRXV8MS7Dr1vjjLVUC9KiwSbM3gg6emfd2yuA93ihv +Pe3mffzLIiQa4mRE3wtGcioC43nWuV2K2e1KjxeFg07JhrezA/1Cak505ab/tmvP +4YmjR5c44+yL/YcQ3HdFgs4mV+nVbptRXvRcPpolJsgxPccGNdvHhsoR4gwXMS3F +RRPD2z6x8xeN73Q4KH3bm01swQdwFBZbWVfmUGLxvN7leCdfs9+iFJyqHiCIB6Iv +mQfp8F0IAOwSo8JhWN+V1dwML4EkIrM8wUb4yecNLkyR6TpPH/qXx4PxVMC+vy6x +sCtjeHIwKE+9vqnlhd5zOYh7qYXEJtYwdeDDmDbL8oks1LFfd+FyAuZXY33DLwn0 +cRYsr2OEZmaajqUB3NVmj3H4uJBN9+paFHyFSXrH68K1Fk2o3n+RSf2EiX+eICwI +L6rqoF5sSVUghBWdNegV7qfy4anwTQwrIMGjgU5S6PKW0Dr/3iO5z3qQpGPAj5OW +ATqPWkDICLbObPxD5cJlyyNE2wCA9VVc6/1d6w4EVwSq9h3/WTpATEreXXxTGptd +LNiTA1nmakBYNO2Iyo3djhaqBdWjk+EIAKtVEnJH9FAVwWOvaj1RoZMA5DnDMo7e +SnhrCXl8AL7Z1WInEaybasTJXn1uQ8xY52Ua4b8cbuEKRKzw/70NesFRoMLYoHTO +dyeszvhoDHberpGRTciVmpMu7Hyi33rM31K9epA4ib6QbbCHnxkWOZB+Bhgj1hJ8 +xb4RBYWiWpAYcg0+DAC3w9gfxQhtUlZPIbmbrBmrVkO2GVGUj8kH6k4UV6kUHEGY +HQWQR0HcbKcXW81ZXCCD0l7ROuEWQtTe5Jw7dJ4/QFuqZnPutXVRNOZqpl6eRShw +7X2/a29VXBpmHA95a88rSQsL+qm7Fb3prqRmuMCtrUZgFz7HLSTuUMR867QcTGVh +cCBUZXN0IEtleSA8bGVhcEBsZWFwLnNlPokCNwQTAQgAIQUCUL352QIbAwULCQgH +AwUVCgkICwUWAgMBAAIeAQIXgAAKCRAvRV4oJNGN30+xEACh9yLkZ4jqW0/wwyIM +MI896MQf1tAwzMj16MJYUjrjNK4Bn57QaQW926HsxF8C/OjT0MTRhq7heYZJnnEo +rj0rzpkJapUveTRkKeoTRtGGigqJYfkOTU7KRVwgJBXIfaKlI3tC3cX0j0H1fVKX +hLxsj5pNSPRCVf2A5mePg44HtXe6oVWSJ8+EcdTa0shf03NhAtFaY0BbFGPSm9mA +QUe4rxugwXPLctIyV4uweFo5BXFBCb4kKTBdnQi3aJwnoWLNT6rDdTe4/nhY0Hfo +alTCYGLkhio77gBHwpTOjEMO/hZhcDMi4CvxMPw7bRxAwq4u+0j0pDhkiLcQs4U4 +Ou/fH+pia+1nF5h19cNVXIm+RX2fL0wxVYc/14AIAK3YT6PVev9XYEkogSj0P7Kb +HKOruYpnToXJBERNJZwGL1U+ihPNUyroRf29t7u8flnXsOpCtBEIWAO8Muy5pWjV +3O6zAUCfWetAieCQ7WrQVmdJDa7dlX3Qx1XagUzqZdAq2jVI1hOWDA2rKytnReSF +/A97rmLaWZ8aoNCs8i4NLcy9Lbzi9QtornYGVCEmTTym0tM9L/mn7gAJ8dqUwt7n +s24dibfElky4ZZeItD+D7OZGeh0FDuejvv2dXFqL1/pkHpGBZhEckg0fZ95NbgMC +4pSZkZnqRpr2GwfB5aFfB6sIIJ0HGARQvfnZARAAtCP8Z9bm6KzIA7wbXx9LBIcJ +1wQvOPf99s4nOrnQev9xH5PZ820qS9xUjrlyE2bGYAhz5Cmq56ENs7THErIdZHtQ +uYEBprO+VFZjP50vtmCOL2PDl/xgv6J9r1Mp3KnR/m0esR+YceDW1qX07IkB5s+Q +us80v5LmmxnWcikWmR7dt1kOyV/+M6Y6mwvfQ4x3D/QUpO7SfMCOG5DGA7hVUHU/ +Tuh8MihmMFFOLAEEQI+1wkxr1W09HaYCcB+EZqxLSaBwMeFoYPJie9dBNBgps39o +6pDbjsO+or4JNuyoHvh8NNI5iY0IR2NMlW4mqCcHEazys2koxFTYK6YD95Vz0RkA +K4BErCDk3lVR0PH4StmLU3gmPayIjvi9Dl9saPRyu4Xx2WVi+q6spl3ckn4c4f3+ +iD8hxVp74+wa5ew0fIXjIpMoHCar/nndsse4i8glCddINdiOPPmhI9Wi3nT+5Z2t +9omPP2dEh0CzR+j1zvUpT3KtmhVICqhO+QP9BTJOwrp81NTlq9mbUyzTtVk/9dy3 +zoYbhKvY08k6LJ9FsQYySqtfJZ4cwl5WsOhALWwOwlMLA9wkz0eemgFxStyOylzl +QKoIK7zHuU6XYOXa32KSPIWaLy+WgIG/u2ObWtdE3CXVIUuSt5BQFnv7XVNHJllD +Az9VDEkOSYOiSEFVoUsAEQEAAQAP/1AagnZQZyzHDEgw4QELAspYHCWLXE5aZInX +wTUJhK31IgIXNn9bJ0hFiSpQR2xeMs9oYtRuPOu0P8oOFMn4/z374fkjZy8QVY3e +PlL+3EUeqYtkMwlGNmVw5a/NbNuNfm5Darb7pEfbYd1gPcni4MAYw7R2SG/57GbC +9gucvspHIfOSfBNLBthDzmK8xEKe1yD2eimfc2T7IRYb6hmkYfeds5GsqvGI6mwI +85h4uUHWRc5JOlhVM6yX8hSWx0L60Z3DZLChmc8maWnFXd7C8eQ6P1azJJbW71Ih +7CoK0XW4LE82vlQurSRFgTwfl7wFYszW2bOzCuhHDDtYnwH86Nsu0DC78ZVRnvxn +E8Ke/AJgrdhIOo4UAyR+aZD2+2mKd7/waOUTUrUtTzc7i8N3YXGi/EIaNReBXaq+ +ZNOp24BlFzRp+FCF/pptDW9HjPdiV09x0DgICmeZS4Gq/4vFFIahWctg52NGebT0 +Idxngjj+xDtLaZlLQoOz0n5ByjO/Wi0ANmMv1sMKCHhGvdaSws2/PbMR2r4caj8m +KXpIgdinM/wUzHJ5pZyF2U/qejsRj8Kw8KH/tfX4JCLhiaP/mgeTuWGDHeZQERAT +xPmRFHaLP9/ZhvGNh6okIYtrKjWTLGoXvKLHcrKNisBLSq+P2WeFrlme1vjvJMo/ +jPwLT5o9CADQmcbKZ+QQ1ZM9v99iDZol7SAMZX43JC019sx6GK0u6xouJBcLfeB4 +OXacTgmSYdTa9RM9fbfVpti01tJ84LV2SyL/VJq/enJF4XQPSynT/tFTn1PAor6o +tEAAd8fjKdJ6LnD5wb92SPHfQfXqI84rFEO8rUNIE/1ErT6DYifDzVCbfD2KZdoF +cOSp7TpD77sY1bs74ocBX5ejKtd+aH99D78bJSMM4pSDZsIEwnomkBHTziubPwJb +OwnATy0LmSMAWOw5rKbsh5nfwCiUTM20xp0t5JeXd+wPVWbpWqI2EnkCEN+RJr9i +7dp/ymDQ+Yt5wrsN3NwoyiexPOG91WQVCADdErHsnglVZZq9Z8Wx7KwecGCUurJ2 +H6lKudv5YOxPnAzqZS5HbpZd/nRTMZh2rdXCr5m2YOuewyYjvM757AkmUpM09zJX +MQ1S67/UX2y8/74TcRF97Ncx9HeELs92innBRXoFitnNguvcO6Esx4BTe1OdU6qR +ER3zAmVf22Le9ciXbu24DN4mleOH+OmBx7X2PqJSYW9GAMTsRB081R6EWKH7romQ +waxFrZ4DJzZ9ltyosEJn5F32StyLrFxpcrdLUoEaclZCv2qka7sZvi0EvovDVEBU +e10jOx9AOwf8Gj2ufhquQ6qgVYCzbP+YrodtkFrXRS3IsljIchj1M2ffB/0bfoUs +rtER9pLvYzCjBPg8IfGLw0o754Qbhh/ReplCRTusP/fQMybvCvfxreS3oyEriu/G +GufRomjewZ8EMHDIgUsLcYo2UHZsfF7tcazgxMGmMvazp4r8vpgrvW/8fIN/6Adu +tF+WjWDTvJLFJCe6O+BFJOWrssNrrra1zGtLC1s8s+Wfpe+bGPL5zpHeebGTwH1U +22eqgJArlEKxrfarz7W5+uHZJHSjF/K9ZvunLGD0n9GOPMpji3UO3zeM8IYoWn7E +/EWK1XbjnssNemeeTZ+sDh+qrD7BOi+vCX1IyBxbfqnQfJZvmcPWpruy1UsO+aIC +0GY8Jr3OL69dDQ21jueJAh8EGAEIAAkFAlC9+dkCGwwACgkQL0VeKCTRjd9HCw/+ +LQSVgLLF4ulYlPCjWIIuQwrPbJfWUVVr2dPUFVM85DCv8gBzk5c121snXh9Swovm +laBbw6ate3BmbXLh64jVE9Za5sbTWi7PCcbO/bpRy4d6oLmitmNw6cq0vjTLxUYy +bwuiJxWREkfxuU85EKdouN062YDevH+/YResmlJrcCE7LRlJFeRlKsrrwBU3BqYd +GgFJjKjQC1peeQ9fj62Y7xfwE9+PXbkiWO5u/Bk8hb1VZH1SoIRU98NHVcp6BVvp +VK0jLAXuSauSczULmpRjbyt1lhaAqivDTWEEZXiNNbRyp17c3nVdPWOcgBr42hdQ +z25CgZgyLCsvu82wuXLKJblrIPJX3Yf+si6KqEWBsmwdOWybsjygaF5HvzgFqAAD +U0goPWoQ71PorP2XOUNp5ZLkBQp5etvtkksjVNMIhnHn8PGMuoxO39EUGlWj2B5l +Cu8tSosAzB1pS8NcLZzoNoI9dOHrmgJmP+GrOUkcf5GhNZbMoj4GNfGBRYX0SZlQ +GuDrwNKYj73C4MWyNnnUFyq8nDHJ/G1NpaF2hiof9RBL4PUU/f92JkceXPBXA8gL +Mz2ig1OButwPPLFGQhWqxXAGrsS3Ny+BhTJfnfIbbkaLLphBpDZm1D9XKbAUvdd1 +RZXoH+FTg9UAW87eqU610npOkT6cRaBxaMK/mDtGNdc= +=JTFu +-----END PGP PRIVATE KEY BLOCK----- +""" diff --git a/mail/src/leap/mail/imap/tests/imapclient.py b/mail/src/leap/mail/imap/tests/imapclient.py new file mode 100755 index 0000000..027396c --- /dev/null +++ b/mail/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/mail/src/leap/mail/imap/tests/rfc822.message b/mail/src/leap/mail/imap/tests/rfc822.message new file mode 100644 index 0000000..ee97ab9 --- /dev/null +++ b/mail/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/mail/src/leap/mail/imap/tests/test_imap.py b/mail/src/leap/mail/imap/tests/test_imap.py new file mode 100644 index 0000000..6792e4b --- /dev/null +++ b/mail/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/mail/src/leap/mail/smtp/README.rst b/mail/src/leap/mail/smtp/README.rst new file mode 100644 index 0000000..2b2a118 --- /dev/null +++ b/mail/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/mail/src/leap/mail/smtp/__init__.py b/mail/src/leap/mail/smtp/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/mail/src/leap/mail/smtp/__init__.py diff --git a/mail/src/leap/mail/smtp/smtprelay.py b/mail/src/leap/mail/smtp/smtprelay.py new file mode 100644 index 0000000..6479873 --- /dev/null +++ b/mail/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/mail/src/leap/mail/smtp/tests/185CA770.key b/mail/src/leap/mail/smtp/tests/185CA770.key new file mode 100644 index 0000000..587b416 --- /dev/null +++ b/mail/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/mail/src/leap/mail/smtp/tests/185CA770.pub b/mail/src/leap/mail/smtp/tests/185CA770.pub new file mode 100644 index 0000000..38af19f --- /dev/null +++ b/mail/src/leap/mail/smtp/tests/185CA770.pub @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +mQINBFCJNL4BEADFsI1TCD4yq7ZqL7VhdVviTuX6JUps8/mVEhRVOZhojLcTYaqQ +gs6T6WabRxcK7ymOnf4K8NhYdz6HFoJN46BT87etokx7J/Sl2OhpiqBQEY+jW8Rp ++3MSGrGmvFw0s1lGrz/cXzM7UNgWSTOnYZ5nJS1veMhy0jseZOUK7ekp2oEDjGZh +pzgd3zICCR2SvlpLIXB2Nr/CUcuRWTcc5LlKmbjMybu0E/uuY14st3JL+7qI6QX0 +atFm0VhFVpagOl0vWKxakUx4hC7j1wH2ADlCvSZPG0StSLUyHkJx3UPsmYxOZFao +ATED3Okjwga6E7PJEbzyqAkvzw/M973kaZCUSH75ZV0cQnpdgXV3DK1gSa3d3gug +W1lE0V7pwnN2NTOYfBMi+WloCs/bp4iZSr4QP1duZ3IqKraeBDCk7MoFo4A9Wk07 +kvqPwF9IBgatu62WVEZIzwyViN+asFUGfgp+8D7gtnlWAw0V6y/lSTzyl+dnLP98 +Hfr2eLBylFs+Kl3Pivpg2uHw09LLCrjeLEN3dj9SfBbA9jDIo9Zhs1voiIK/7Shx +E0BRJaBgG3C4QaytYEu7RFFOKuvBai9w2Y5OfsKFo8rA7v4dxFFDvzKGujCtNnwf +oyaGlZmMBU5MUmHUNiG8ON21COZBtK5oMScuY1VC9CQonj3OClg3IbU9SQARAQAB +tCRkcmVicyAoZ3BnIHRlc3Qga2V5KSA8ZHJlYnNAbGVhcC5zZT6JAjgEEwECACIF +AlCJNL4CGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJELcvXsYYXKdwXYcP +/23T1DW6eq4CI0UbsGLxieiLohbhViUNJt4gLrgKGaORhwD6mQLhpHTFWLtHbQrW +evDCf0KYwQk/UUS/poCIHy31Eo9mZJz+J2+m2ohubtvJdCiHf3uSAokCgZyLI8Ss +YSwVR0s0WktT/r2XhdgtkfV8jctHgpwMMgjHA9EfE1UThkO4uGULDGzI9Sz6TsIV +fxBqdt5w/rqVjlW5/8EBzmFdpDIgP/H4s5lkSFrFeP+vLjAPlgz+C+NAJydH3LNO +H/XXjPT+qY5ZtNFQ+6pJWBBsOjKa9oPzO95BodZ5QL/iJkARpSzJXkV8sx2N1gIc +5kSljuJKBbinkfUNBDM5NkeP5LmoHVhKTPm4cBOMT1dbT2YZ/ahU86UA+Kf1U+Kj +UCjg9PmkxVq3uuUbdnBRTCyO1JhBzS4tDQfSQiAIuPpfm/VkDZZmjgvBIThsEc5n +oVMh5lzNwTVtvDJ8ihSe2wRTHe7ic/rwDzq6+js7jb1Qr9iNiApE7IoSI/88ORl8 +ZDzu2GzLvayhtJyhdTdyQdYFCe/Ax5oW43mhkrx8PObDw9P9XT2SuPBooPIutmgs +X/DpST5bfGYQQaJc7OiE1DWu+UcPXybI91Frr9lRWiLkee2xVpmN7APSj4HX+41U +JQGSpVcpYuyJMmmZsQ2TRUKE3SAvbqNGoICAz/Myr9J9uQINBFCJNL4BEADjvAQE +e7AXao9kUTETXlkgOEvRrh31d2EAEZ6mLGyE7NY0YAKO48fWeQvNHGTsTlFSuwiY +7U6lNCQfUD/88FfYZOYusumkE8rHJUMkzTddGl7v98yZWuQ7oc/grPuVHFQJjSV/ +RYEzFb4SgxCGPyzZm/TKASzLrdQpA8fljwleOVjiAQg3Y9h6SI4nuKYbNP+uUloz +4lYN1rBWZY1caSQhlz+PBHrdm3/Gn2B9Zcd7EDHDW8iQAiD7c/mCs3O4yCsCkls4 +NVcmIpG5PXlgsXM+lSjSSoveGB/XO1PF903WfBtJBmFJXY2A73rMpZ7kJXBgrUhi +jgIazfwbtkdIKRYGJduO7u/Qpo6LXnzHb/ARlA1urvf0nqfcTd3VvEvO4ejP7b7N +AoZZAw6SXncwne04XYpBrxUSX1v/LE//qqEyrJMkkGgyHjSZ8WbsisBCyzAbwmRq +WpsJp5BBob7mVqtIfw8Mk7OEABPDlR1yUjN1De4TOJSSFIfrESidRQF103UmOHFD +WkvjirY9iyfOcxaoXwGeKRcCwoGGybVOYXo9i5zCTW2HEpI8W5PnOSI5KGURfhMY +2ZXNFS2DU+e2a4sGf6V+NqH4zsjEf3ZB0LhYpP+Cu79WXpn2k3ZtO0hDAZJ0ejmb +Mv+fr4hLV4ood6bUzGk/a6MPoPnBaMXV5IrCrwARAQABiQIfBBgBAgAJBQJQiTS+ +AhsMAAoJELcvXsYYXKdw3rQP/A/yLHEgZVmN04wg9j+7d/l9+FEivBBQ90CbHvXZ +wlji3LJBUCKCH4UTO+dnrxbtlvaMBL8UkHRvqLUADqi+9pNRB9oOy7AA7lfCOCzm +Dpy8niBPSG/ghTwZJs0xM63WKeiE1FO6qoJIXIJ5bShh6HpLSI8RPILcQihHcsZM +2s7pupwkEJEgWTl1VN3s5vgNkBxz+l09RhG3SmydLSLiUfAA7tPKQcdnPywwQa3l +Ura67dQ287kflPDCbHy1XGF/0cN1figNuKC0c4Pdct3H4t7DVP5JneQsONk49NAf +hV0J8udTKrOCsqh6G4+tiCOvHi+R0QdqXBrKGnnUMsbtnGEnlcmqwTolDzWJKBgy +aCnQ8mUSATTStFvgEk39M3h4a2P6JLdcm01bb27esJExGylXj1GmJjZoa3tXAI+L +jcBc2I5MyXJvvF1n7Q/k1fHBOQ8lXZxdNEjw7j86HUQD12hqUFDC5BOET69UwMHL +qDx/WhR2f7Huq8Ok0By3dLzJNFPw6HIpF6UidTenGhN5UcCP/pxn1zYgq/DCHaH4 +JAXjvOvhu5NFF0uL+RSpNZ8E8reD5BSQ7wCV+q2T8SGfTZrQOWqOb8rtQ0+SIaF4 +A88LqwHvPkfsuoLQN9uIVyMHhTbe0s2D3yOEgI7p5ZpvDyAX96MAa5N4/sOhEKGg +q3iu +=RChS +-----END PGP PUBLIC KEY BLOCK----- diff --git a/mail/src/leap/mail/smtp/tests/__init__.py b/mail/src/leap/mail/smtp/tests/__init__.py new file mode 100644 index 0000000..d7b942a --- /dev/null +++ b/mail/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) + +lQcYBFC9+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 +AA/+JHtlL39G1wsH9R6UEfUQJGXR9MiIiwZoKcnRB2o8+DS+OLjg0JOh8XehtuCs +E/8oGQKtQqa5bEIstX7IZoYmYFiUQi9LOzIblmp2vxOm+HKkxa4JszWci2/ZmC3t +KtaA4adl9XVnshoQ7pijuCMUKB3naBEOAxd8s9d/JeReGIYkJErdrnVfNk5N71Ds +FmH5Ll3XtEDvgBUQP3nkA6QFjpsaB94FHjL3gDwum/cxzj6pCglcvHOzEhfY0Ddb +J967FozQTaf2JW3O+w3LOqtcKWpq87B7+O61tVidQPSSuzPjCtFF0D2LC9R/Hpky +KTMQ6CaKja4MPhjwywd4QPcHGYSqjMpflvJqi+kYIt8psUK/YswWjnr3r4fbuqVY +VhtiHvnBHQjz135lUqWvEz4hM3Xpnxydx7aRlv5NlevK8+YIO5oFbWbGNTWsPZI5 +jpoFBpSsnR1Q5tnvtNHauvoWV+XN2qAOBTG+/nEbDYH6Ak3aaE9jrpTdYh0CotYF +q7csANsDy3JvkAzeU6WnYpsHHaAjqOGyiZGsLej1UcXPFMosE/aUo4WQhiS8Zx2c +zOVKOi/X5vQ2GdNT9Qolz8AriwzsvFR+bxPzyd8V6ALwDsoXvwEYinYBKK8j0OPv +OOihSR6HVsuP9NUZNU9ewiGzte/+/r6pNXHvR7wTQ8EWLcEIAN6Zyrb0bHZTIlxt +VWur/Ht2mIZrBaO50qmM5RD3T5oXzWXi/pjLrIpBMfeZR9DWfwQwjYzwqi7pxtYx +nJvbMuY505rfnMoYxb4J+cpRXV8MS7Dr1vjjLVUC9KiwSbM3gg6emfd2yuA93ihv +Pe3mffzLIiQa4mRE3wtGcioC43nWuV2K2e1KjxeFg07JhrezA/1Cak505ab/tmvP +4YmjR5c44+yL/YcQ3HdFgs4mV+nVbptRXvRcPpolJsgxPccGNdvHhsoR4gwXMS3F +RRPD2z6x8xeN73Q4KH3bm01swQdwFBZbWVfmUGLxvN7leCdfs9+iFJyqHiCIB6Iv +mQfp8F0IAOwSo8JhWN+V1dwML4EkIrM8wUb4yecNLkyR6TpPH/qXx4PxVMC+vy6x +sCtjeHIwKE+9vqnlhd5zOYh7qYXEJtYwdeDDmDbL8oks1LFfd+FyAuZXY33DLwn0 +cRYsr2OEZmaajqUB3NVmj3H4uJBN9+paFHyFSXrH68K1Fk2o3n+RSf2EiX+eICwI +L6rqoF5sSVUghBWdNegV7qfy4anwTQwrIMGjgU5S6PKW0Dr/3iO5z3qQpGPAj5OW +ATqPWkDICLbObPxD5cJlyyNE2wCA9VVc6/1d6w4EVwSq9h3/WTpATEreXXxTGptd +LNiTA1nmakBYNO2Iyo3djhaqBdWjk+EIAKtVEnJH9FAVwWOvaj1RoZMA5DnDMo7e +SnhrCXl8AL7Z1WInEaybasTJXn1uQ8xY52Ua4b8cbuEKRKzw/70NesFRoMLYoHTO +dyeszvhoDHberpGRTciVmpMu7Hyi33rM31K9epA4ib6QbbCHnxkWOZB+Bhgj1hJ8 +xb4RBYWiWpAYcg0+DAC3w9gfxQhtUlZPIbmbrBmrVkO2GVGUj8kH6k4UV6kUHEGY +HQWQR0HcbKcXW81ZXCCD0l7ROuEWQtTe5Jw7dJ4/QFuqZnPutXVRNOZqpl6eRShw +7X2/a29VXBpmHA95a88rSQsL+qm7Fb3prqRmuMCtrUZgFz7HLSTuUMR867QcTGVh +cCBUZXN0IEtleSA8bGVhcEBsZWFwLnNlPokCNwQTAQgAIQUCUL352QIbAwULCQgH +AwUVCgkICwUWAgMBAAIeAQIXgAAKCRAvRV4oJNGN30+xEACh9yLkZ4jqW0/wwyIM +MI896MQf1tAwzMj16MJYUjrjNK4Bn57QaQW926HsxF8C/OjT0MTRhq7heYZJnnEo +rj0rzpkJapUveTRkKeoTRtGGigqJYfkOTU7KRVwgJBXIfaKlI3tC3cX0j0H1fVKX +hLxsj5pNSPRCVf2A5mePg44HtXe6oVWSJ8+EcdTa0shf03NhAtFaY0BbFGPSm9mA +QUe4rxugwXPLctIyV4uweFo5BXFBCb4kKTBdnQi3aJwnoWLNT6rDdTe4/nhY0Hfo +alTCYGLkhio77gBHwpTOjEMO/hZhcDMi4CvxMPw7bRxAwq4u+0j0pDhkiLcQs4U4 +Ou/fH+pia+1nF5h19cNVXIm+RX2fL0wxVYc/14AIAK3YT6PVev9XYEkogSj0P7Kb +HKOruYpnToXJBERNJZwGL1U+ihPNUyroRf29t7u8flnXsOpCtBEIWAO8Muy5pWjV +3O6zAUCfWetAieCQ7WrQVmdJDa7dlX3Qx1XagUzqZdAq2jVI1hOWDA2rKytnReSF +/A97rmLaWZ8aoNCs8i4NLcy9Lbzi9QtornYGVCEmTTym0tM9L/mn7gAJ8dqUwt7n +s24dibfElky4ZZeItD+D7OZGeh0FDuejvv2dXFqL1/pkHpGBZhEckg0fZ95NbgMC +4pSZkZnqRpr2GwfB5aFfB6sIIJ0HGARQvfnZARAAtCP8Z9bm6KzIA7wbXx9LBIcJ +1wQvOPf99s4nOrnQev9xH5PZ820qS9xUjrlyE2bGYAhz5Cmq56ENs7THErIdZHtQ +uYEBprO+VFZjP50vtmCOL2PDl/xgv6J9r1Mp3KnR/m0esR+YceDW1qX07IkB5s+Q +us80v5LmmxnWcikWmR7dt1kOyV/+M6Y6mwvfQ4x3D/QUpO7SfMCOG5DGA7hVUHU/ +Tuh8MihmMFFOLAEEQI+1wkxr1W09HaYCcB+EZqxLSaBwMeFoYPJie9dBNBgps39o +6pDbjsO+or4JNuyoHvh8NNI5iY0IR2NMlW4mqCcHEazys2koxFTYK6YD95Vz0RkA +K4BErCDk3lVR0PH4StmLU3gmPayIjvi9Dl9saPRyu4Xx2WVi+q6spl3ckn4c4f3+ +iD8hxVp74+wa5ew0fIXjIpMoHCar/nndsse4i8glCddINdiOPPmhI9Wi3nT+5Z2t +9omPP2dEh0CzR+j1zvUpT3KtmhVICqhO+QP9BTJOwrp81NTlq9mbUyzTtVk/9dy3 +zoYbhKvY08k6LJ9FsQYySqtfJZ4cwl5WsOhALWwOwlMLA9wkz0eemgFxStyOylzl +QKoIK7zHuU6XYOXa32KSPIWaLy+WgIG/u2ObWtdE3CXVIUuSt5BQFnv7XVNHJllD +Az9VDEkOSYOiSEFVoUsAEQEAAQAP/1AagnZQZyzHDEgw4QELAspYHCWLXE5aZInX +wTUJhK31IgIXNn9bJ0hFiSpQR2xeMs9oYtRuPOu0P8oOFMn4/z374fkjZy8QVY3e +PlL+3EUeqYtkMwlGNmVw5a/NbNuNfm5Darb7pEfbYd1gPcni4MAYw7R2SG/57GbC +9gucvspHIfOSfBNLBthDzmK8xEKe1yD2eimfc2T7IRYb6hmkYfeds5GsqvGI6mwI +85h4uUHWRc5JOlhVM6yX8hSWx0L60Z3DZLChmc8maWnFXd7C8eQ6P1azJJbW71Ih +7CoK0XW4LE82vlQurSRFgTwfl7wFYszW2bOzCuhHDDtYnwH86Nsu0DC78ZVRnvxn +E8Ke/AJgrdhIOo4UAyR+aZD2+2mKd7/waOUTUrUtTzc7i8N3YXGi/EIaNReBXaq+ +ZNOp24BlFzRp+FCF/pptDW9HjPdiV09x0DgICmeZS4Gq/4vFFIahWctg52NGebT0 +Idxngjj+xDtLaZlLQoOz0n5ByjO/Wi0ANmMv1sMKCHhGvdaSws2/PbMR2r4caj8m +KXpIgdinM/wUzHJ5pZyF2U/qejsRj8Kw8KH/tfX4JCLhiaP/mgeTuWGDHeZQERAT +xPmRFHaLP9/ZhvGNh6okIYtrKjWTLGoXvKLHcrKNisBLSq+P2WeFrlme1vjvJMo/ +jPwLT5o9CADQmcbKZ+QQ1ZM9v99iDZol7SAMZX43JC019sx6GK0u6xouJBcLfeB4 +OXacTgmSYdTa9RM9fbfVpti01tJ84LV2SyL/VJq/enJF4XQPSynT/tFTn1PAor6o +tEAAd8fjKdJ6LnD5wb92SPHfQfXqI84rFEO8rUNIE/1ErT6DYifDzVCbfD2KZdoF +cOSp7TpD77sY1bs74ocBX5ejKtd+aH99D78bJSMM4pSDZsIEwnomkBHTziubPwJb +OwnATy0LmSMAWOw5rKbsh5nfwCiUTM20xp0t5JeXd+wPVWbpWqI2EnkCEN+RJr9i +7dp/ymDQ+Yt5wrsN3NwoyiexPOG91WQVCADdErHsnglVZZq9Z8Wx7KwecGCUurJ2 +H6lKudv5YOxPnAzqZS5HbpZd/nRTMZh2rdXCr5m2YOuewyYjvM757AkmUpM09zJX +MQ1S67/UX2y8/74TcRF97Ncx9HeELs92innBRXoFitnNguvcO6Esx4BTe1OdU6qR +ER3zAmVf22Le9ciXbu24DN4mleOH+OmBx7X2PqJSYW9GAMTsRB081R6EWKH7romQ +waxFrZ4DJzZ9ltyosEJn5F32StyLrFxpcrdLUoEaclZCv2qka7sZvi0EvovDVEBU +e10jOx9AOwf8Gj2ufhquQ6qgVYCzbP+YrodtkFrXRS3IsljIchj1M2ffB/0bfoUs +rtER9pLvYzCjBPg8IfGLw0o754Qbhh/ReplCRTusP/fQMybvCvfxreS3oyEriu/G +GufRomjewZ8EMHDIgUsLcYo2UHZsfF7tcazgxMGmMvazp4r8vpgrvW/8fIN/6Adu +tF+WjWDTvJLFJCe6O+BFJOWrssNrrra1zGtLC1s8s+Wfpe+bGPL5zpHeebGTwH1U +22eqgJArlEKxrfarz7W5+uHZJHSjF/K9ZvunLGD0n9GOPMpji3UO3zeM8IYoWn7E +/EWK1XbjnssNemeeTZ+sDh+qrD7BOi+vCX1IyBxbfqnQfJZvmcPWpruy1UsO+aIC +0GY8Jr3OL69dDQ21jueJAh8EGAEIAAkFAlC9+dkCGwwACgkQL0VeKCTRjd9HCw/+ +LQSVgLLF4ulYlPCjWIIuQwrPbJfWUVVr2dPUFVM85DCv8gBzk5c121snXh9Swovm +laBbw6ate3BmbXLh64jVE9Za5sbTWi7PCcbO/bpRy4d6oLmitmNw6cq0vjTLxUYy +bwuiJxWREkfxuU85EKdouN062YDevH+/YResmlJrcCE7LRlJFeRlKsrrwBU3BqYd +GgFJjKjQC1peeQ9fj62Y7xfwE9+PXbkiWO5u/Bk8hb1VZH1SoIRU98NHVcp6BVvp +VK0jLAXuSauSczULmpRjbyt1lhaAqivDTWEEZXiNNbRyp17c3nVdPWOcgBr42hdQ +z25CgZgyLCsvu82wuXLKJblrIPJX3Yf+si6KqEWBsmwdOWybsjygaF5HvzgFqAAD +U0goPWoQ71PorP2XOUNp5ZLkBQp5etvtkksjVNMIhnHn8PGMuoxO39EUGlWj2B5l +Cu8tSosAzB1pS8NcLZzoNoI9dOHrmgJmP+GrOUkcf5GhNZbMoj4GNfGBRYX0SZlQ +GuDrwNKYj73C4MWyNnnUFyq8nDHJ/G1NpaF2hiof9RBL4PUU/f92JkceXPBXA8gL +Mz2ig1OButwPPLFGQhWqxXAGrsS3Ny+BhTJfnfIbbkaLLphBpDZm1D9XKbAUvdd1 +RZXoH+FTg9UAW87eqU610npOkT6cRaBxaMK/mDtGNdc= +=JTFu +-----END PGP PRIVATE KEY BLOCK----- +""" diff --git a/mail/src/leap/mail/smtp/tests/mail.txt b/mail/src/leap/mail/smtp/tests/mail.txt new file mode 100644 index 0000000..9542047 --- /dev/null +++ b/mail/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/mail/src/leap/mail/smtp/tests/test_smtprelay.py b/mail/src/leap/mail/smtp/tests/test_smtprelay.py new file mode 100644 index 0000000..eaa4d04 --- /dev/null +++ b/mail/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) | 
