diff options
15 files changed, 271 insertions, 115 deletions
diff --git a/mail/.gitignore b/mail/.gitignore new file mode 100644 index 00000000..db344fd8 --- /dev/null +++ b/mail/.gitignore @@ -0,0 +1,20 @@ +*.pyc +build/ +dist/ +*.egg +*.egg-info +src/_trial_temp +*.swp +*.swo +.* +!.coveragerc +!.tx +!.gitignore +bin/ +core +docs/_build +include/ +lib/ +local/ +share/ +MANIFEST diff --git a/mail/changes/feature_add-dependency-for-keymanager b/mail/changes/feature_add-dependency-for-keymanager new file mode 100644 index 00000000..0ac1c2ab --- /dev/null +++ b/mail/changes/feature_add-dependency-for-keymanager @@ -0,0 +1 @@ + o Add dependency for leap.keymanager. diff --git a/mail/setup.py b/mail/setup.py index 8d4e415a..3feb275d 100644 --- a/mail/setup.py +++ b/mail/setup.py @@ -17,11 +17,15 @@ """ setup file for leap.mail """ + + from setuptools import setup, find_packages + requirements = [ "leap.soledad", "leap.common>=0.2.3-dev", + "leap.keymanager>=0.2.1", "twisted", ] @@ -31,7 +35,6 @@ tests_requirements = [ ] # XXX add classifiers, docs - setup( name='leap.mail', version='0.2.0-dev', @@ -45,8 +48,8 @@ setup( ), namespace_packages=["leap"], package_dir={'': 'src'}, - packages=find_packages('src', exclude=['leap.mail.tests']), - test_suite='leap.mail.tests', + packages=find_packages('src'), + test_suite='leap.mail.load_tests', install_requires=requirements, tests_require=tests_requirements, ) diff --git a/mail/src/leap/mail/__init__.py b/mail/src/leap/mail/__init__.py index e69de29b..04a99519 100644 --- a/mail/src/leap/mail/__init__.py +++ b/mail/src/leap/mail/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# __init__.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +""" +Provide function for loading tests. +""" + + +import unittest + + +def load_tests(): + return unittest.defaultTestLoader.discover('./src/leap/mail') diff --git a/mail/src/leap/mail/imap/fetch.py b/mail/src/leap/mail/imap/fetch.py index df5d046c..48a45e68 100644 --- a/mail/src/leap/mail/imap/fetch.py +++ b/mail/src/leap/mail/imap/fetch.py @@ -104,8 +104,8 @@ class LeapIncomingMail(object): """ Process a successfully decrypted message. - :param doc: a LeapDocument instance containing the incoming message - :type doc: LeapDocument + :param doc: a SoledadDocument instance containing the incoming message + :type doc: SoledadDocument :param data: the json-encoded, decrypted content of the incoming message diff --git a/mail/src/leap/mail/imap/server.py b/mail/src/leap/mail/imap/server.py index 45c43b72..813d8503 100644 --- a/mail/src/leap/mail/imap/server.py +++ b/mail/src/leap/mail/imap/server.py @@ -37,7 +37,7 @@ from twisted.python import log from leap.common.check import leap_assert, leap_assert_type from leap.soledad import Soledad -from leap.soledad.backends.sqlcipher import SQLCipherDatabase +from leap.soledad.sqlcipher import SQLCipherDatabase logger = logging.getLogger(__name__) @@ -81,7 +81,7 @@ class WithMsgFields(object): INBOX_VAL = "inbox" - # Flags for LeapDocument for indexing. + # Flags for SoledadDocument for indexing. SEEN_KEY = "seen" RECENT_KEY = "recent" @@ -233,7 +233,7 @@ class SoledadBackedAccount(WithMsgFields, IndexedDB): :param name: the name of the mailbox :type name: str - :rtype: LeapDocument + :rtype: SoledadDocument """ name = name.upper() doc = self._soledad.get_from_index( @@ -580,9 +580,9 @@ class LeapMessage(WithMsgFields): """ Initializes a LeapMessage. - :param doc: A LeapDocument containing the internal + :param doc: A SoledadDocument containing the internal representation of the message - :type doc: LeapDocument + :type doc: SoledadDocument """ self._doc = doc @@ -620,13 +620,13 @@ class LeapMessage(WithMsgFields): """ Sets the flags for this message - Returns a LeapDocument that needs to be updated by the caller. + Returns a SoledadDocument that needs to be updated by the caller. :param flags: the flags to update in the message. :type flags: sequence of str - :return: a LeapDocument instance - :rtype: LeapDocument + :return: a SoledadDocument instance + :rtype: SoledadDocument """ log.msg('setting flags') doc = self._doc @@ -639,13 +639,13 @@ class LeapMessage(WithMsgFields): """ Adds flags to this message. - Returns a LeapDocument that needs to be updated by the caller. + Returns a SoledadDocument that needs to be updated by the caller. :param flags: the flags to add to the message. :type flags: sequence of str - :return: a LeapDocument instance - :rtype: LeapDocument + :return: a SoledadDocument instance + :rtype: SoledadDocument """ oldflags = self.getFlags() return self.setFlags(list(set(flags + oldflags))) @@ -654,13 +654,13 @@ class LeapMessage(WithMsgFields): """ Remove flags from this message. - Returns a LeapDocument that needs to be updated by the caller. + Returns a SoledadDocument that needs to be updated by the caller. :param flags: the flags to be removed from the message. :type flags: sequence of str - :return: a LeapDocument instance - :rtype: LeapDocument + :return: a SoledadDocument instance + :rtype: SoledadDocument """ oldflags = self.getFlags() return self.setFlags(list(set(oldflags) - set(flags))) @@ -751,6 +751,7 @@ class LeapMessage(WithMsgFields): :rtype: dict """ headers = self._get_headers() + names = map(lambda s: s.upper(), names) if negate: cond = lambda key: key.upper() not in names else: @@ -771,8 +772,23 @@ class LeapMessage(WithMsgFields): def getSubPart(part): return None + # + # accessors + # + + def __getitem__(self, key): + """ + Return the content of the message document. + + @param key: The key + @type key: str + + @return: The content value indexed by C{key} or None + @rtype: str + """ + return self._doc.content.get(key, None) -class MessageCollection(WithMsgFields): +class MessageCollection(WithMsgFields, IndexedDB): """ A collection of messages, surprisingly. @@ -795,6 +811,10 @@ class MessageCollection(WithMsgFields): WithMsgFields.RAW_KEY: "", } + # get from SoledadBackedAccount the needed index-related constants + INDEXES = SoledadBackedAccount.INDEXES + TYPE_IDX = SoledadBackedAccount.TYPE_IDX + def __init__(self, mbox=None, soledad=None): """ Constructor for MessageCollection. @@ -824,7 +844,7 @@ class MessageCollection(WithMsgFields): self.mbox = mbox.upper() self._soledad = soledad - #self.db = db + self.initialize_db() self._parser = Parser() def _get_empty_msg(self): @@ -883,11 +903,18 @@ class MessageCollection(WithMsgFields): # XXX get lower case for keys? content[self.HEADERS_KEY] = headers - content[self.SUBJECT_KEY] = headers[self.SUBJECT_FIELD] + # set subject based on message headers and eventually replace by + # subject given as param + if self.SUBJECT_FIELD in headers: + content[self.SUBJECT_KEY] = headers[self.SUBJECT_FIELD] + if subject is not None: + content[self.SUBJECT_KEY] = subject content[self.RAW_KEY] = stringify(raw) - if not date: + if not date and self.DATE_FIELD in headers: content[self.DATE_KEY] = headers[self.DATE_FIELD] + else: + content[self.DATE_KEY] = date # ...should get a sanity check here. content[self.UID_KEY] = uid @@ -899,7 +926,7 @@ class MessageCollection(WithMsgFields): Removes a message. :param msg: a u1db doc containing the message - :type msg: LeapDocument + :type msg: SoledadDocument """ self._soledad.delete_doc(msg) @@ -912,9 +939,9 @@ class MessageCollection(WithMsgFields): :param uid: the message uid to query by :type uid: int - :return: A LeapDocument instance matching the query, + :return: A SoledadDocument instance matching the query, or None if not found. - :rtype: LeapDocument + :rtype: SoledadDocument """ docs = self._soledad.get_from_index( SoledadBackedAccount.TYPE_MBOX_UID_IDX, @@ -942,7 +969,7 @@ class MessageCollection(WithMsgFields): If you want acess to the content, use __iter__ instead :return: a list of u1db documents - :rtype: list of LeapDocument + :rtype: list of SoledadDocument """ # XXX this should return LeapMessage instances return self._soledad.get_from_index( @@ -1113,8 +1140,8 @@ class SoledadMailbox(WithMsgFields): """ Returns mailbox document. - :return: A LeapDocument containing this mailbox. - :rtype: LeapDocument + :return: A SoledadDocument containing this mailbox. + :rtype: SoledadDocument """ query = self._soledad.get_from_index( SoledadBackedAccount.TYPE_MBOX_IDX, @@ -1129,16 +1156,15 @@ class SoledadMailbox(WithMsgFields): :returns: tuple of flags for this mailbox :rtype: tuple of str """ - return map(str, self.INIT_FLAGS) + #return map(str, self.INIT_FLAGS) - # TODO -- returning hardcoded flags for now, - # no need of setting flags. + # XXX CHECK against thunderbird XXX - #mbox = self._get_mbox() - #if not mbox: - #return None - #flags = mbox.content.get(self.FLAGS_KEY, []) - #return map(str, flags) + mbox = self._get_mbox() + if not mbox: + return None + flags = mbox.content.get(self.FLAGS_KEY, []) + return map(str, flags) def setFlags(self, flags): """ @@ -1439,7 +1465,7 @@ class SoledadMailbox(WithMsgFields): """ docs = self.messages.get_all() for doc in docs: - self.messages.db.delete_doc(doc) + self.messages._soledad.delete_doc(doc) def _update(self, doc): """ diff --git a/mail/src/leap/mail/imap/tests/__init__.py b/mail/src/leap/mail/imap/tests/__init__.py index 315d649a..fdeda76e 100644 --- a/mail/src/leap/mail/imap/tests/__init__.py +++ b/mail/src/leap/mail/imap/tests/__init__.py @@ -17,13 +17,13 @@ def run(): """xxx fill me in""" pass +import os 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 +from leap.soledad.document import SoledadDocument #----------------------------------------------------------------------------- @@ -38,30 +38,65 @@ class BaseSoledadIMAPTest(BaseLeapTest): """ 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_file = os.path.join( + self.tempdir, "db1.u1db") + self.db2_file = os.path.join( + self.tempdir, "db2.u1db") + self._db1 = u1db.open(self.db1_file, create=True, - document_factory=LeapDocument) + document_factory=SoledadDocument) 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, - bootstrap=False, - prefix=self.tempdir) - self._soledad._init_dirs() - self._soledad._gpg = GPGWrapper(gnupghome=self.gnupg_home) - - if not self._soledad._has_privkey(): - self._soledad._set_privkey(PRIVATE_KEY) - if not self._soledad._has_symkey(): - self._soledad._gen_symkey() - self._soledad._load_symkey() - self._soledad._init_db() + document_factory=SoledadDocument) + + # soledad config info + self.email = 'leap@leap.se' + secrets_path = os.path.join( + self.tempdir, Soledad.STORAGE_SECRETS_FILE_NAME) + local_db_path = os.path.join( + self.tempdir, Soledad.LOCAL_DATABASE_FILE_NAME) + server_url = '' + cert_file = None + + self._soledad = self._soledad_instance( + self.email, '123', + secrets_path=secrets_path, + local_db_path=local_db_path, + server_url=server_url, + cert_file=cert_file) + + def _soledad_instance(self, uuid, passphrase, secrets_path, local_db_path, + server_url, cert_file): + """ + Return a Soledad instance for tests. + """ + # mock key fetching and storing so Soledad doesn't fail when trying to + # reach the server. + Soledad._fetch_keys_from_shared_db = Mock(return_value=None) + Soledad._assert_keys_in_shared_db = Mock(return_value=None) + + # instantiate soledad + def _put_doc_side_effect(doc): + self._doc_put = doc + + class MockSharedDB(object): + + get_doc = Mock(return_value=None) + put_doc = Mock(side_effect=_put_doc_side_effect) + + def __call__(self): + return self + + Soledad._shared_db = MockSharedDB() + + return Soledad( + uuid, + passphrase, + secrets_path=secrets_path, + local_db_path=local_db_path, + server_url=server_url, + cert_file=cert_file, + ) def tearDown(self): self._db1.close() diff --git a/mail/src/leap/mail/imap/tests/test_imap.py b/mail/src/leap/mail/imap/tests/test_imap.py index 6b6c24e2..8804fe0e 100644 --- a/mail/src/leap/mail/imap/tests/test_imap.py +++ b/mail/src/leap/mail/imap/tests/test_imap.py @@ -31,24 +31,18 @@ try: except ImportError: from StringIO import StringIO -#import codecs -#import locale import os import types import tempfile import shutil -#from zope.interface import implements +from mock import Mock + -#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 @@ -59,8 +53,6 @@ import twisted.cred.checkers import twisted.cred.credentials import twisted.cred.portal -#from twisted.test.proto_helpers import StringTransport, StringTransportWithDisconnection - #import u1db @@ -68,8 +60,6 @@ from leap.common.testing.basetest import BaseLeapTest from leap.mail.imap.server import SoledadMailbox from leap.mail.imap.server import SoledadBackedAccount from leap.mail.imap.server import MessageCollection -#from leap.mail.imap.tests import PUBLIC_KEY -#from leap.mail.imap.tests import PRIVATE_KEY from leap.soledad import Soledad from leap.soledad import SoledadCrypto @@ -107,19 +97,23 @@ def initialize_soledad(email, gnupg_home, tempdir): server_url = "http://provider" cert_file = "" + class MockSharedDB(object): + + get_doc = Mock(return_value=None) + put_doc = Mock() + + def __call__(self): + return self + + Soledad._shared_db = MockSharedDB() + _soledad = Soledad( - uuid, # user's uuid, obtained through signal events - passphrase, # how to get this? - secret_path, # how to get this? - local_db_path, # how to get this? - server_url, # can be None for now - cert_file, - bootstrap=False) - _soledad._init_dirs() - _soledad._crypto = SoledadCrypto(_soledad) - _soledad._shared_db = None - _soledad._init_keys() - _soledad._init_db() + uuid, + passphrase, + secret_path, + local_db_path, + server_url, + cert_file) return _soledad @@ -253,9 +247,9 @@ class IMAP4HelperMixin(BaseLeapTest): #cls.db2_file = "%s/db2.u1db" % cls.tempdir # open test dbs #cls._db1 = u1db.open(cls.db1_file, create=True, - #document_factory=LeapDocument) + #document_factory=SoledadDocument) #cls._db2 = u1db.open(cls.db2_file, create=True, - #document_factory=LeapDocument) + #document_factory=SoledadDocument) # initialize soledad by hand so we can control keys cls._soledad = initialize_soledad( @@ -401,12 +395,21 @@ class MessageCollectionTestCase(IMAP4HelperMixin, unittest.TestCase): """ Test empty message and collection """ - em = self.messages.get_empty_msg() - self.assertEqual(em, - {"subject": "", "seen": False, - "flags": [], "mailbox": "inbox", - "mbox-uid": 1, - "raw": ""}) + em = self.messages._get_empty_msg() + self.assertEqual( + em, + { + "date": '', + "flags": [], + "headers": {}, + "mbox": "inbox", + "raw": "", + "recent": True, + "seen": False, + "subject": "", + "type": "msg", + "uid": 1, + }) self.assertEqual(self.messages.count(), 0) def testFilterByMailbox(self): @@ -419,13 +422,13 @@ class MessageCollectionTestCase(IMAP4HelperMixin, unittest.TestCase): mc.add_msg('', subject="test3") self.assertEqual(self.messages.count(), 3) - newmsg = mc.get_empty_msg() + newmsg = mc._get_empty_msg() newmsg['mailbox'] = "mailbox/foo" newmsg['subject'] = "test another mailbox" - mc.db.create_doc(newmsg) + mc._soledad.create_doc(newmsg) self.assertEqual(mc.count(), 3) - self.assertEqual(len(mc.db.get_from_index(mc.MAILBOX_INDEX, "*")), - 4) + self.assertEqual( + len(mc._soledad.get_from_index(mc.TYPE_IDX, "*")), 4) class LeapIMAP4ServerTestCase(IMAP4HelperMixin, unittest.TestCase): @@ -561,10 +564,13 @@ class LeapIMAP4ServerTestCase(IMAP4HelperMixin, unittest.TestCase): """ Try deleting a mailbox with sub-folders, and \NoSelect flag set. An exception is expected + + Obs: this test will fail if SoledadMailbox returns hardcoded flags. """ SimpleLEAPServer.theAccount.addMailbox('delete') to_delete = SimpleLEAPServer.theAccount.getMailbox('delete') to_delete.setFlags((r'\Noselect',)) + to_delete.getFlags() SimpleLEAPServer.theAccount.addMailbox('delete/me') def login(): @@ -1180,7 +1186,6 @@ class LeapIMAP4ServerTestCase(IMAP4HelperMixin, unittest.TestCase): """ name = 'mailbox-close' self.server.theAccount.addMailbox(name) - #import ipdb; ipdb.set_trace() m = SimpleLEAPServer.theAccount.getMailbox(name) m.messages.add_msg('', subject="Message 1", diff --git a/mail/src/leap/mail/smtp/__init__.py b/mail/src/leap/mail/smtp/__init__.py index daa7ccfd..78eb4f88 100644 --- a/mail/src/leap/mail/smtp/__init__.py +++ b/mail/src/leap/mail/smtp/__init__.py @@ -22,8 +22,6 @@ SMTP relay helper function. from twisted.internet import reactor -#from leap import soledad -#from leap.common.keymanager import KeyManager from leap.mail.smtp.smtprelay import SMTPFactory diff --git a/mail/src/leap/mail/smtp/smtprelay.py b/mail/src/leap/mail/smtp/smtprelay.py index d87dc87b..17388409 100644 --- a/mail/src/leap/mail/smtp/smtprelay.py +++ b/mail/src/leap/mail/smtp/smtprelay.py @@ -21,7 +21,6 @@ LEAP SMTP encrypted relay. import re import os -import gnupg import tempfile @@ -38,13 +37,13 @@ from email.parser import Parser from leap.common.check import leap_assert, leap_assert_type -from leap.common.keymanager import KeyManager -from leap.common.keymanager.openpgp import ( +from leap.keymanager import KeyManager +from leap.keymanager.openpgp import ( OpenPGPKey, encrypt_asym, sign, ) -from leap.common.keymanager.errors import KeyNotFound +from leap.keymanager.errors import KeyNotFound # diff --git a/mail/src/leap/mail/tests/smtp/185CA770.key b/mail/src/leap/mail/smtp/tests/185CA770.key index 587b4164..587b4164 100644 --- a/mail/src/leap/mail/tests/smtp/185CA770.key +++ b/mail/src/leap/mail/smtp/tests/185CA770.key diff --git a/mail/src/leap/mail/tests/smtp/185CA770.pub b/mail/src/leap/mail/smtp/tests/185CA770.pub index 38af19f8..38af19f8 100644 --- a/mail/src/leap/mail/tests/smtp/185CA770.pub +++ b/mail/src/leap/mail/smtp/tests/185CA770.pub diff --git a/mail/src/leap/mail/tests/smtp/__init__.py b/mail/src/leap/mail/smtp/tests/__init__.py index c69c34f0..73c9421a 100644 --- a/mail/src/leap/mail/tests/smtp/__init__.py +++ b/mail/src/leap/mail/smtp/tests/__init__.py @@ -30,8 +30,7 @@ from twisted.trial import unittest from leap.soledad import Soledad -from leap.soledad.crypto import SoledadCrypto -from leap.common.keymanager import ( +from leap.keymanager import ( KeyManager, openpgp, ) @@ -59,26 +58,53 @@ class TestCaseWithKeyManager(BaseLeapTest): address = 'leap@leap.se' # user's address in the form user@provider uuid = 'leap@leap.se' passphrase = '123' - secret_path = os.path.join(self.tempdir, 'secret.gpg') + secrets_path = os.path.join(self.tempdir, 'secret.gpg') local_db_path = os.path.join(self.tempdir, 'soledad.u1db') server_url = 'http://provider/' cert_file = '' + self._soledad = self._soledad_instance( + uuid, passphrase, secrets_path, local_db_path, server_url, + cert_file) + self._km = self._keymanager_instance(address) + + def _soledad_instance(self, uuid, passphrase, secrets_path, local_db_path, + server_url, cert_file): + """ + Return a Soledad instance for tests. + """ # mock key fetching and storing so Soledad doesn't fail when trying to # reach the server. Soledad._fetch_keys_from_shared_db = Mock(return_value=None) Soledad._assert_keys_in_shared_db = Mock(return_value=None) # instantiate soledad - self._soledad = Soledad( + def _put_doc_side_effect(doc): + self._doc_put = doc + + class MockSharedDB(object): + + get_doc = Mock(return_value=None) + put_doc = Mock(side_effect=_put_doc_side_effect) + + def __call__(self): + return self + + Soledad._shared_db = MockSharedDB() + + return Soledad( uuid, passphrase, - secret_path, - local_db_path, - server_url, - cert_file, + secrets_path=secrets_path, + local_db_path=local_db_path, + server_url=server_url, + cert_file=cert_file, ) + def _keymanager_instance(self, address): + """ + Return a Key Manager instance for tests. + """ self._config = { 'host': 'http://provider/', 'port': 25, @@ -87,14 +113,29 @@ class TestCaseWithKeyManager(BaseLeapTest): 'encrypted_only': True } + class Response(object): + status_code = 200 + headers = {'content-type': 'application/json'} + + def json(self): + return {'address': ADDRESS_2, 'openpgp': PUBLIC_KEY_2} + + def raise_for_status(self): + pass + nickserver_url = '' # the url of the nickserver - self._km = KeyManager(address, nickserver_url, self._soledad) + km = KeyManager(address, nickserver_url, self._soledad, + ca_cert_path='') + km._fetcher.put = Mock() + km._fetcher.get = Mock(return_value=Response()) # insert test keys in key manager. pgp = openpgp.OpenPGPScheme(self._soledad) pgp.put_ascii_key(PRIVATE_KEY) pgp.put_ascii_key(PRIVATE_KEY_2) + return km + def tearDown(self): # mimic LeapBaseTest.tearDownClass behaviour os.environ["PATH"] = self.old_path diff --git a/mail/src/leap/mail/tests/smtp/mail.txt b/mail/src/leap/mail/smtp/tests/mail.txt index 95420470..95420470 100644 --- a/mail/src/leap/mail/tests/smtp/mail.txt +++ b/mail/src/leap/mail/smtp/tests/mail.txt diff --git a/mail/src/leap/mail/tests/smtp/test_smtprelay.py b/mail/src/leap/mail/smtp/tests/test_smtprelay.py index e48f1291..65c45580 100644 --- a/mail/src/leap/mail/tests/smtp/test_smtprelay.py +++ b/mail/src/leap/mail/smtp/tests/test_smtprelay.py @@ -38,12 +38,12 @@ from leap.mail.smtp.smtprelay import ( SMTPFactory, EncryptedMessage, ) -from leap.mail.tests.smtp import ( +from leap.mail.smtp.tests import ( TestCaseWithKeyManager, ADDRESS, ADDRESS_2, ) -from leap.common.keymanager import openpgp +from leap.keymanager import openpgp # some regexps |