diff options
28 files changed, 583 insertions, 86 deletions
@@ -59,3 +59,4 @@ debian/leap-mx.postrm.debhelper debian/leap-mx.prerm.debhelper debian/leap-mx.substvars debian/leap-mx/ +_trial_temp @@ -0,0 +1,9 @@ +Isis Agora Lovecruft <isis@torproject.org> +Tomás Touceda <chiiph@leap.se> +Kali Kaneko <kali@leap.se> +drebs <drebs@leap.se> +Ivan Alejandro <ivanalejandro0@gmail.com> +Micah Anderson <micah@riseup.net> +varac <varacanero@zeromail.org> +Bruno Wagner Goncalves <bwagner@thoughtworks.com> +Ruben Pollan <meskio@sindominio.net> diff --git a/changes/feature_6942_use_syslog b/changes/feature_6942_use_syslog new file mode 100644 index 0000000..ffa8f62 --- /dev/null +++ b/changes/feature_6942_use_syslog @@ -0,0 +1 @@ +- Use syslog for logging (Closes: #6859) diff --git a/changes/feature_7272-msg-key-not-found b/changes/feature_7272-msg-key-not-found new file mode 100644 index 0000000..2d82df8 --- /dev/null +++ b/changes/feature_7272-msg-key-not-found @@ -0,0 +1 @@ +- return a more meaningful msg if user exists but has no key (Closes: #7272) diff --git a/changes/feature_7435_unit_testing b/changes/feature_7435_unit_testing new file mode 100644 index 0000000..32778b7 --- /dev/null +++ b/changes/feature_7435_unit_testing @@ -0,0 +1 @@ +- set up unit testing infrastructure (Closes: #7435) diff --git a/changes/feature_7439_remove_provenance b/changes/feature_7439_remove_provenance new file mode 100644 index 0000000..188b9a2 --- /dev/null +++ b/changes/feature_7439_remove_provenance @@ -0,0 +1 @@ +- Don't add X-Leap-Provenance header (Closes: #7439) diff --git a/changes/feature_7565_couchdb_refactor b/changes/feature_7565_couchdb_refactor new file mode 100644 index 0000000..dc6ac0b --- /dev/null +++ b/changes/feature_7565_couchdb_refactor @@ -0,0 +1 @@ +- Update code to the new CouchDatabase soledad code diff --git a/changes/next-changelog.txt b/changes/next-changelog.txt new file mode 100644 index 0000000..fbee095 --- /dev/null +++ b/changes/next-changelog.txt @@ -0,0 +1,30 @@ +0.8.0 - xxx ++++++++++++++++++++++++++++++++ + +Please add lines to this file, they will be moved to the CHANGELOG.rst during +the next release. + +There are two template lines for each category, use them as reference. + +I've added a new category `Misc` so we can track doc/style/packaging stuff. + +Features +~~~~~~~~ +- `#5959 <https://leap.se/code/issues/5959>`_: Make alias resolver to return *uuid@deliver.local* +- `#1234 <https://leap.se/code/issues/1234>`_: Description of the new feature corresponding with issue #1234. +- New feature without related issue number. + +Bugfixes +~~~~~~~~ +- `#1235 <https://leap.se/code/issues/1235>`_: Description for the fixed stuff corresponding with issue #1235. +- Bugfix without related issue number. + +Misc +~~~~ +- `#7271 <https://leap.se/code/issues/7271>`_: Document the return codes of the TCP maps. +- `#1236 <https://leap.se/code/issues/1236>`_: Description of the new feature corresponding with issue #1236. +- Some change without issue number. + +Known Issues +~~~~~~~~~~~~ +- `#1236 <https://leap.se/code/issues/1236>`_: Description of the known issue corresponding with issue #1236. diff --git a/doc/DESIGN.md b/doc/DESIGN.md index e98976d..e33c6ae 100644 --- a/doc/DESIGN.md +++ b/doc/DESIGN.md @@ -145,6 +145,18 @@ virtual transport instead, we should append the domain (eg 123456@example.org). see http://www.postfix.org/ADDRESS_REWRITING_README.html#resolve +#### Return values + +The return codes and content of the tcp maps are: + + +----------------------------------------------------------+ + | virtual_alias_map | check_recipient_access | ++----------------+---------------------+------------------------------------+ +| user not found | 500 "NOT FOUND SRY" | 500 "REJECT" | +| key not found | 200 "<uuid>" | 400 "4.7.13 USER ACCOUNT DISABLED" | +| both found | 200 "<uuid>" | 200 "OK" | ++----------------+---------------------+------------------------------------+ + ### Current status diff --git a/pkg/generate_wheels.sh b/pkg/generate_wheels.sh new file mode 100755 index 0000000..e8096af --- /dev/null +++ b/pkg/generate_wheels.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# Generate wheels for dependencies + +if [ "$WHEELHOUSE" = "" ]; then + WHEELHOUSE=$HOME/wheelhouse +fi + +pip wheel --wheel-dir $WHEELHOUSE pip +pip wheel --wheel-dir $WHEELHOUSE -r pkg/requirements.pip +if [ -f pkg/requirements-testing.pip ]; then + pip wheel --wheel-dir $WHEELHOUSE -r pkg/requirements-testing.pip +fi diff --git a/pkg/leap-mx.init b/pkg/leap-mx.init index d38cc2c..3878bce 100644 --- a/pkg/leap-mx.init +++ b/pkg/leap-mx.init @@ -13,8 +13,9 @@ PATH=/sbin:/bin:/usr/sbin:/usr/bin PIDFILE=/var/run/leap_mx.pid RUNDIR=/var/lib/leap_mx/ FILE=/usr/share/app/leap_mx.tac -LOGFILE=/var/log/leap_mx.log TWISTD_PATH=/usr/bin/twistd +USER=leap-mx +GROUP=leap-mx [ -r /etc/default/leap_mx ] && . /etc/default/leap_mx @@ -32,8 +33,10 @@ case "$1" in --pidfile=$PIDFILE \ --rundir=$RUNDIR \ --python=$FILE \ - --logfile=$LOGFILE \ - --prefix=leap-mx + --syslog \ + --prefix=leap-mx \ + --uid=$USER \ + --gid=$GROUP echo "." ;; diff --git a/pkg/leap_mx.tac b/pkg/leap_mx.tac index 7da59cf..6f7b104 100644 --- a/pkg/leap_mx.tac +++ b/pkg/leap_mx.tac @@ -68,19 +68,18 @@ cdb = couchdbhelper.ConnectedCouchDB(server, application = service.Application("LEAP MX") # Alias map -alias_map = internet.TCPServer(alias_port, AliasResolverFactory(couchdb=cdb)) +alias_map = internet.TCPServer( + alias_port, AliasResolverFactory(couchdb=cdb), + interface="localhost") alias_map.setServiceParent(application) # Check recipient access -check_recipient = internet.TCPServer(check_recipient_port, - CheckRecipientAccessFactory(couchdb=cdb)) +check_recipient = internet.TCPServer( + check_recipient_port, CheckRecipientAccessFactory(couchdb=cdb), + interface="localhost") check_recipient.setServiceParent(application) # Mail receiver -mail_couch_url_prefix = "http://%s:%s@%s:%s" % (user, - password, - server, - port) directories = [] for section in config.sections(): if section in ("couchdb", "alias map", "check recipient", "bounce"): @@ -89,6 +88,5 @@ for section in config.sections(): recursive = config.getboolean(section, "recursive") directories.append([to_watch, recursive]) -mr = MailReceiver(mail_couch_url_prefix, cdb, directories, bounce_from, - bounce_subject) +mr = MailReceiver(cdb, directories, bounce_from, bounce_subject) mr.setServiceParent(application) diff --git a/pkg/pip_install_requirements.sh b/pkg/pip_install_requirements.sh new file mode 100755 index 0000000..57732e2 --- /dev/null +++ b/pkg/pip_install_requirements.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# Update pip and install LEAP base/testing requirements. +# For convenience, $insecure_packages are allowed with insecure flags enabled. +# Use at your own risk. +# See $usage for help + +insecure_packages="" +leap_wheelhouse=https://lizard.leap.se/wheels + +show_help() { + usage="Usage: $0 [--testing] [--use-leap-wheels]\n --testing\t\tInstall dependencies from requirements-testing.pip\n +\t\t\tOtherwise, it will install requirements.pip\n +--use-leap-wheels\tUse wheels from leap.se" + echo -e $usage + + exit 1 +} + +process_arguments() { + testing=false + use_leap_wheels=false + + while [ "$#" -gt 0 ]; do + # From http://stackoverflow.com/a/31443098 + case "$1" in + --help) show_help;; + --testing) testing=true; shift 1;; + --use-leap-wheels) use_leap_wheels=true; shift 1;; + + -h) show_help;; + -*) echo "unknown option: $1" >&2; exit 1;; + esac + done +} + +return_wheelhouse() { + if $use_leap_wheels ; then + WHEELHOUSE=$leap_wheelhouse + elif [ "$WHEELHOUSE" = "" ]; then + WHEELHOUSE=$HOME/wheelhouse + fi + + # Tested with bash and zsh + if [[ $WHEELHOUSE != http* && ! -d "$WHEELHOUSE" ]]; then + mkdir $WHEELHOUSE + fi + + echo "$WHEELHOUSE" +} + +return_install_options() { + wheelhouse=`return_wheelhouse` + install_options="-U --find-links=$wheelhouse" + if $use_leap_wheels ; then + install_options="$install_options --trusted-host lizard.leap.se" + fi + + echo $install_options +} + +return_insecure_flags() { + for insecure_package in $insecure_packages; do + flags="$flags --allow-external $insecure_package --allow-unverified $insecure_package" + done + + echo $flags +} + +return_packages() { + if $testing ; then + packages="-r pkg/requirements-testing.pip" + else + packages="-r pkg/requirements.pip" + fi + + echo $packages +} + +process_arguments $@ +install_options=`return_install_options` +insecure_flags=`return_insecure_flags` +packages=`return_packages` + +pip install -U wheel +pip install $install_options pip +pip install $install_options $insecure_flags $packages diff --git a/pkg/requirements-leap.pip b/pkg/requirements-leap.pip new file mode 100644 index 0000000..482d1e2 --- /dev/null +++ b/pkg/requirements-leap.pip @@ -0,0 +1,3 @@ +leap.common>=0.3.5 +leap.soledad.common>=0.4.5 +leap.keymanager>=0.3.4 diff --git a/pkg/requirements-testing.pip b/pkg/requirements-testing.pip new file mode 100644 index 0000000..94b8e9c --- /dev/null +++ b/pkg/requirements-testing.pip @@ -0,0 +1,2 @@ +pep8 +setuptools-trial diff --git a/pkg/requirements.pip b/pkg/requirements.pip index d20aa25..328b1c3 100644 --- a/pkg/requirements.pip +++ b/pkg/requirements.pip @@ -7,9 +7,3 @@ paisley>=0.3.1 # in soledad-common, but we need to declare here # for the time being. couchdb - -leap.common>=0.3.7 -leap.soledad.common>=0.5.0 -leap.keymanager>=0.3.8 - -chardet # we fallback to chardet if cchardet is not available, but it's preferred diff --git a/pkg/utils/get_authors.sh b/pkg/utils/get_authors.sh new file mode 100755 index 0000000..0169bb1 --- /dev/null +++ b/pkg/utils/get_authors.sh @@ -0,0 +1,2 @@ +#!/bin/sh +git log --format='%aN <%aE>' | awk '{arr[$0]++} END{for (i in arr){print arr[i], i;}}' | sort -rn | cut -d' ' -f2- diff --git a/pkg/utils/reqs.py b/pkg/utils/reqs.py index 5e2324f..251c7e9 100644 --- a/pkg/utils/reqs.py +++ b/pkg/utils/reqs.py @@ -22,6 +22,22 @@ import re import sys +def is_develop_mode(): + """ + Returns True if we're calling the setup script using the argument for + setuptools development mode. + + This avoids messing up with dependency pinning and order, the + responsibility of installing the leap dependencies is left to the + developer. + """ + args = sys.argv + devflags = "setup.py", "develop" + if (args[0], args[1]) == devflags: + return True + return False + + def get_reqs_from_files(reqfiles): """ Returns the contents of the top requirement file listed as a @@ -51,8 +67,8 @@ def parse_requirements(reqfiles=['requirements.txt', if re.match(r'\s*-e\s+', line): pass # do not try to do anything with externals on vcs - #requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1', - #line)) + # requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1', + # line)) # http://foo.bar/baz/foobar/zipball/master#egg=foobar elif re.match(r'\s*https?:', line): requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1', diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..4a2ab2b --- /dev/null +++ b/setup.cfg @@ -0,0 +1,10 @@ +[aliases] +test = trial + +[pep8] +exclude = versioneer.py,_version.py,*.egg,build,dist,docs +ignore = E731 + +[flake8] +exclude = versioneer.py,_version.py,*.egg,build,dist,docs +ignore = E731 @@ -20,6 +20,9 @@ setup file for leap.mx import os import re from setuptools import setup, find_packages +from setuptools import Command + +from pkg.utils.reqs import parse_requirements, is_develop_mode import versioneer versioneer.versionfile_source = 'src/leap/mx/_version.py' @@ -27,8 +30,6 @@ versioneer.versionfile_build = 'leap/mx/_version.py' versioneer.tag_prefix = '' # tags are like 1.2.0 versioneer.parentdir_prefix = 'leap.mx-' -from pkg.utils.reqs import parse_requirements - trove_classifiers = [ 'Development Status :: 3 - Alpha', 'Environment :: No Input/Output (Daemon)', @@ -60,9 +61,6 @@ if len(_version_short) > 0: cmdclass = versioneer.get_cmdclass() -from setuptools import Command - - class freeze_debianver(Command): """ Freezes the version in a debian branch. @@ -113,7 +111,24 @@ else: # be automatically # placed by distutils, using whatever interpreter is # available. - data_files = [("/usr/share/app/", ["pkg/leap_mx.tac"])] + data_files = [("/usr/share/app/", ["pkg/mx.tac"])] + + +requirements = parse_requirements() + +if is_develop_mode(): + print + print ("[WARNING] Skipping leap-specific dependencies " + "because development mode is detected.") + print ("[WARNING] You can install " + "the latest published versions with " + "'pip install -r pkg/requirements-leap.pip'") + print ("[WARNING] Or you can instead do 'python setup.py develop' " + "from the parent folder of each one of them.") + print +else: + requirements += parse_requirements( + reqfiles=["pkg/requirements-leap.pip"]) setup( name='leap.mx', @@ -135,8 +150,10 @@ setup( namespace_packages=["leap"], package_dir={'': 'src'}, packages=find_packages('src'), - #test_suite='leap.mx.tests', - install_requires=parse_requirements(), + test_suite='leap.mx.tests', + tests_require=parse_requirements( + reqfiles=['pkg/requirements-testing.pip']), + install_requires=requirements, classifiers=trove_classifiers, data_files=data_files ) diff --git a/src/leap/mx/alias_resolver.py b/src/leap/mx/alias_resolver.py index bf7a58b..a5b5107 100644 --- a/src/leap/mx/alias_resolver.py +++ b/src/leap/mx/alias_resolver.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- encoding: utf-8 -*- # alias_resolver.py -# Copyright (C) 2013 LEAP +# Copyright (C) 2013, 2015 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 @@ -60,6 +60,7 @@ class LEAPPostfixTCPMapAliasServer(postfix.PostfixTCPMapServer): TCP_MAP_CODE_PERMANENT_FAILURE, postfix.quote("NOT FOUND SRY")) else: + uuid += "@deliver.local" # properly encode uuid, otherwise twisted complains when replying if isinstance(uuid, unicode): uuid = uuid.encode("utf8") diff --git a/src/leap/mx/check_recipient_access.py b/src/leap/mx/check_recipient_access.py index f994e78..67bfd04 100644 --- a/src/leap/mx/check_recipient_access.py +++ b/src/leap/mx/check_recipient_access.py @@ -43,7 +43,6 @@ class LEAPPostFixTCPMapAccessServer(postfix.PostfixTCPMapServer): are looked up by the factory, and will return a permanent or a temporary failure in case either the user or the key don't exist, respectivelly. """ - def _cbGot(self, value): """ Return a code and message depending on the result of the factory's @@ -65,7 +64,7 @@ class LEAPPostFixTCPMapAccessServer(postfix.PostfixTCPMapServer): elif pubkey is None: self.sendCode( TCP_MAP_CODE_TEMPORARY_FAILURE, - postfix.quote("4.7.13 USER ACCOUNT DISABLED")) + postfix.quote("4.7.13 NO PUBKEY FOUND")) else: self.sendCode( TCP_MAP_CODE_SUCCESS, @@ -85,4 +84,3 @@ class CheckRecipientAccessFactory(LEAPPostfixTCPMapServerFactory): @property def _query_message(self): return "check recipient access" - diff --git a/src/leap/mx/couchdbhelper.py b/src/leap/mx/couchdbhelper.py index 1752b4e..115ecbe 100644 --- a/src/leap/mx/couchdbhelper.py +++ b/src/leap/mx/couchdbhelper.py @@ -23,7 +23,9 @@ maps, user UUIDs, and GPG keyIDs. from paisley import client +from twisted.internet import defer from twisted.python import log +from leap.soledad.common.couch import CouchDatabase class ConnectedCouchDB(client.CouchDB): @@ -50,6 +52,10 @@ class ConnectedCouchDB(client.CouchDB): :param str password: (optional) The password for authorization. :type password: str """ + self._mail_couch_url = "http://%s:%s@%s:%s" % (username, + password, + host, + port) client.CouchDB.__init__(self, host, port=port, @@ -131,3 +137,32 @@ class ConnectedCouchDB(client.CouchDB): d.addCallbacks(_get_pubkey_cbk, log.err) return d + + def put_doc(self, uuid, doc): + """ + Update a document. + + If the document currently has conflicts, put will fail. + If the database specifies a maximum document size and the document + exceeds it, put will fail and raise a DocumentTooBig exception. + + :param uuid: The uuid of a user + :type uuid: str + :param doc: A Document with new content. + :type doc: leap.soledad.common.couch.CouchDocument + + :return: A deferred which fires with the new revision identifier for + the document if the Document object has being updated, or + which fails with CouchDBError if there was any error. + """ + # TODO: that should be implemented with paisley + url = self._mail_couch_url + "/user-%s" % (uuid,) + try: + db = CouchDatabase.open_database(url, create=False) + return defer.succeed(db.put_doc(doc)) + except Exception as e: + return defer.fail(CouchDBError(e.message)) + + +class CouchDBError(Exception): + pass diff --git a/src/leap/mx/mail_receiver.py b/src/leap/mx/mail_receiver.py index 446fd38..ea13658 100644 --- a/src/leap/mx/mail_receiver.py +++ b/src/leap/mx/mail_receiver.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- encoding: utf-8 -*- # mail_receiver.py -# Copyright (C) 2013 LEAP +# Copyright (C) 2013, 2015 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 @@ -51,7 +51,7 @@ from zope.interface import implements from leap.soledad.common.crypto import EncryptionSchemes from leap.soledad.common.crypto import ENC_JSON_KEY from leap.soledad.common.crypto import ENC_SCHEME_KEY -from leap.soledad.common.couch import CouchDatabase, CouchDocument +from leap.soledad.common.document import ServerDocument from leap.keymanager import openpgp @@ -75,15 +75,11 @@ class MailReceiver(Service): """ RETRY_DIR_WATCH_DELAY = 60 * 5 # 5 minutes - def __init__(self, mail_couch_url, users_cdb, directories, bounce_from, + def __init__(self, users_cdb, directories, bounce_from, bounce_subject): """ Constructor - :param mail_couch_url: URL prefix for the couchdb where mail - should be stored - :type mail_couch_url: str - :param users_cdb: CouchDB instance from where to get the uuid and pubkey for a user :type users_cdb: ConnectedCouchDB @@ -98,7 +94,6 @@ class MailReceiver(Service): :type bounce_subject: str """ # IService doesn't define an __init__ - self._mail_couch_url = mail_couch_url self._users_cdb = users_cdb self._directories = directories self._bounce_from = bounce_from @@ -175,7 +170,7 @@ class MailReceiver(Service): :return: doc to sync with Soledad or None, None if something went wrong. - :rtype: CouchDocument + :rtype: ServerDocument """ if pubkey is None or len(pubkey) == 0: log.msg("_encrypt_message: Something went wrong, here's all " @@ -185,7 +180,7 @@ class MailReceiver(Service): # find message's encoding message_as_string = message.as_string() - doc = CouchDocument(doc_id=str(pyuuid.uuid4())) + doc = ServerDocument(doc_id=str(pyuuid.uuid4())) # store plain text if pubkey is not available data = {'incoming': True, 'content': message_as_string} @@ -203,16 +198,6 @@ class MailReceiver(Service): with openpgp.TempGPGWrapper(gpgbinary='/usr/bin/gpg') as gpg: gpg.import_keys(pubkey) key = gpg.list_keys().pop() - - # add X-Leap-Provenance header if message is not encrypted - if message.get_content_type() != 'multipart/encrypted' and \ - '-----BEGIN PGP MESSAGE-----' not in \ - message_as_string: - message.add_header( - 'X-Leap-Provenance', - email.utils.formatdate(), - pubkey=key["keyid"]) - data = {'incoming': True, 'content': message.as_string()} doc.content = { self.INCOMING_KEY: True, self.ERROR_DECRYPTING_KEY: False, @@ -225,55 +210,44 @@ class MailReceiver(Service): return doc + @defer.inlineCallbacks def _export_message(self, uuid, doc): """ - Given a UUID and a CouchDocument, it saves it directly in the + Given a UUID and a ServerDocument, it saves it directly in the couchdb that serves as a backend for Soledad, in a db accessible to the recipient of the mail. :param uuid: the mail owner's uuid :type uuid: str - :param doc: CouchDocument that represents the email - :type doc: CouchDocument + :param doc: ServerDocument that represents the email + :type doc: ServerDocument - :return: True if it's ok to remove the message, False - otherwise - :rtype: bool + :return: A Deferred which fires if it's ok to remove the message, + or fails otherwise + :rtype: Deferred """ if uuid is None or doc is None: log.msg("_export_message: Something went wrong, here's all " "I know: %r | %r" % (uuid, doc)) - return False + raise Exception("No uuid or doc") log.msg("Exporting message for %s" % (uuid,)) - - db = CouchDatabase(self._mail_couch_url, "user-%s" % (uuid,)) - db.put_doc(doc) - + yield self._users_cdb.put_doc(uuid, doc) log.msg("Done exporting") - return True - - def _conditional_remove(self, do_remove, filepath): + def _remove(self, filepath): """ - Removes the message if do_remove is True. + Removes the message. - :param do_remove: True if the message should be removed, False - otherwise - :type do_remove: bool :param filepath: path to the mail :type filepath: twisted.python.filepath.FilePath """ - if do_remove: - # remove the original mail - try: - log.msg("Removing %r" % (filepath.path,)) - filepath.remove() - log.msg("Done removing") - except Exception: - log.err() - else: - log.msg("Not removing %r" % (filepath.path,)) + try: + log.msg("Removing %r" % (filepath.path,)) + filepath.remove() + log.msg("Done removing") + except Exception: + log.err() def _get_owner(self, mail): """ @@ -295,7 +269,7 @@ class MailReceiver(Service): return None final_address = delivereds.pop(0) _, addr = email.utils.parseaddr(final_address) - uuid, _ = addr.split("@") + uuid = addr.split("@")[0] return uuid @defer.inlineCallbacks @@ -317,7 +291,7 @@ class MailReceiver(Service): except InvalidReturnPathError: # give up bouncing this message! log.msg("Will not bounce message because of invalid return path.") - yield self._conditional_remove(True, filepath) + yield self._remove(filepath) def sleep(self, secs): """ @@ -413,8 +387,8 @@ class MailReceiver(Service): log.msg("Encrypting message to %s's pubkey" % (uuid,)) doc = yield self._encrypt_message(pubkey, msg) - do_remove = yield self._export_message(uuid, doc) - yield self._conditional_remove(do_remove, filepath) + yield self._export_message(uuid, doc) + yield self._remove(filepath) @defer.inlineCallbacks def _process_incoming_email(self, otherself, filepath, mask): @@ -440,4 +414,3 @@ class MailReceiver(Service): except Exception as e: log.msg("Something went wrong while processing {0!r}: {1!r}" .format(filepath, e)) - log.err() diff --git a/src/leap/mx/tcp_map.py b/src/leap/mx/tcp_map.py index 96db70a..07bf51d 100644 --- a/src/leap/mx/tcp_map.py +++ b/src/leap/mx/tcp_map.py @@ -41,7 +41,6 @@ class LEAPPostfixTCPMapServerFactory(ServerFactory, object): __metaclass__ = ABCMeta - def __init__(self, couchdb): """ Initialize the factory. diff --git a/src/leap/mx/tests/__init__.py b/src/leap/mx/tests/__init__.py index 2002c48..13df919 100644 --- a/src/leap/mx/tests/__init__.py +++ b/src/leap/mx/tests/__init__.py @@ -22,6 +22,7 @@ code, using twisted.trial, for testing leap_mx. __all__ = ['test_alias_resolver'] + def run(): """xxx fill me in""" pass diff --git a/src/leap/mx/tests/test_mail_receiver.py b/src/leap/mx/tests/test_mail_receiver.py new file mode 100644 index 0000000..e72cb1a --- /dev/null +++ b/src/leap/mx/tests/test_mail_receiver.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +# test_mail_receiver.py +# Copyright (C) 2015 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/>. +""" +MailReceiver tests +""" + +import json +import os +import os.path +import shutil +import tempfile + +from email.message import Message +from twisted.internet import defer, reactor +from twisted.trial import unittest + +from leap.keymanager import openpgp +from leap.mx.couchdbhelper import CouchDBError +from leap.mx.mail_receiver import MailReceiver + + +BOUNCE_ADDRESS = "bounce@leap.se" +BOUNCE_SUBJECT = "bounce subject" +ADDRESS = "leap@leap.se" +UUID = "13d5203bdd09be1e638bdb1d315251cb" + + +class MailReceiverTestCase(unittest.TestCase): + def setUp(self): + self.directory = tempfile.mkdtemp(prefix="leap_tests-") + os.mkdir(os.path.join(self.directory, "new")) + + self.users_cdb = self.usersCdb() + self.receiver = MailReceiver( + users_cdb=self.users_cdb, + directories=[(self.directory, True)], + bounce_from=BOUNCE_ADDRESS, + bounce_subject=BOUNCE_SUBJECT) + self.receiver.startService() + + def tearDown(self): + self.receiver.stopService() + shutil.rmtree(self.directory) + + def usersCdb(self): + self.pubKey = PUBLIC_KEY + self.docs = [] + self.defer_put_doc = defer.Deferred() + + class UsersCdb(object): + def getPubkey(_, uuid): + return self.pubKey + + def put_doc(_, uuid, doc): + self.docs.append({'uuid': uuid, 'doc': doc}) + if not self.defer_put_doc.called: + reactor.callLater(1, self.defer_put_doc.callback, + (uuid, doc)) + return defer.succeed(None) + + return UsersCdb() + + @defer.inlineCallbacks + def test_single_mail(self): + msg, path = self.addMail("foo bar") + uuid, doc = yield self.defer_put_doc + self.assertEqual(uuid, UUID) + decmsg = self.decryptDoc(doc) + self.assertEqual(msg, decmsg) + self.assertFalse(os.path.exists(path)) + + @defer.inlineCallbacks + def test_put_doc_raises(self): + defer_called = defer.Deferred() + + def put_doc_raise(*args): + defer_called.callback(None) + return defer.fail(CouchDBError()) + + self.users_cdb.put_doc = put_doc_raise + _, path = self.addMail() + yield defer_called + self.assertTrue(os.path.exists(path)) + + def addMail(self, body="", filename="foo", to=ADDRESS, + frm="someone@domain.org", subject="sent subject"): + msg = Message() + msg.add_header("To", to) + msg.add_header( + "Delivered-To", UUID + "@deliver.local") + msg.add_header("From", frm) + msg.add_header("Subject", subject) + msg.set_payload(body) + + path = os.path.join(self.directory, "new", filename) + with open(path, "w") as f: + f.write(msg.as_string()) + + return msg.as_string(), path + + def decryptDoc(self, doc): + encdoc = doc.content['_enc_json'] + decdoc = {} + + with openpgp.TempGPGWrapper(gpgbinary='/usr/bin/gpg') as gpg: + gpg.import_keys(PRIVATE_KEY) + decstr = gpg.decrypt(encdoc) + decdoc = json.loads(decstr.data) + + self.assertTrue(decdoc['incoming']) + return decdoc['content'] + + +# key 24D18DDF: public key "Leap Test Key <leap@leap.se>" +KEY_FINGERPRINT = "E36E738D69173C13D709E44F2F455E2824D18DDF" +PUBLIC_KEY = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +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 +=MuOY +-----END PGP PUBLIC KEY BLOCK----- +""" +PRIVATE_KEY = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +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 +=JTFu +-----END PGP PRIVATE KEY BLOCK----- +""" diff --git a/src/leap/mx/tester.py b/src/leap/mx/tests/tester.py index 05d2d05..05d2d05 100644 --- a/src/leap/mx/tester.py +++ b/src/leap/mx/tests/tester.py |