From 83f386ea7258e9ecb92c3d5dbcb09ed514f437b4 Mon Sep 17 00:00:00 2001 From: Victor Shyba Date: Thu, 31 Aug 2017 03:17:19 -0300 Subject: [feature] add support for soledad incoming api -- Related: #8664 --- pkg/mx.tac | 10 ++++- pkg/requirements-leap.pip | 1 + src/leap/mx/mail_receiver.py | 13 +++++-- src/leap/mx/soledadhelper.py | 89 ++++++++++++++++++++++++++++++++++++++++++++ src/leap/mx/tests/tester.py | 9 ++++- 5 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 src/leap/mx/soledadhelper.py diff --git a/pkg/mx.tac b/pkg/mx.tac index 42d40a8..fe01a0e 100644 --- a/pkg/mx.tac +++ b/pkg/mx.tac @@ -21,6 +21,7 @@ import ConfigParser from functools import partial from leap.mx import couchdbhelper +from leap.mx import soledadhelper from leap.mx.mail_receiver import MailReceiver from leap.mx.alias_resolver import AliasResolverFactory from leap.mx.check_recipient_access import CheckRecipientAccessFactory @@ -66,6 +67,11 @@ cdb = couchdbhelper.ConnectedCouchDB(server, username=user, password=password) +incoming_api = False +if config.has_section("incoming api"): + args = [config.get("incoming api", option) for option in ["host", "port", "token"]] + incoming_api = soledadhelper.SoledadIncomingAPI(*args) + application = service.Application("LEAP MX") @@ -91,11 +97,11 @@ fingerprint_map.setServiceParent(application) directories = [] for section in config.sections(): if section in ("couchdb", "alias map", "check recipient", - "fingerprint map", "bounce"): + "fingerprint map", "bounce", "incoming api"): continue to_watch = config.get(section, "path") recursive = config.getboolean(section, "recursive") directories.append([to_watch, recursive]) -mr = MailReceiver(cdb, directories, bounce_from, bounce_subject) +mr = MailReceiver(cdb, directories, bounce_from, bounce_subject, incoming_api) mr.setServiceParent(application) diff --git a/pkg/requirements-leap.pip b/pkg/requirements-leap.pip index 1cfe861..da61bde 100644 --- a/pkg/requirements-leap.pip +++ b/pkg/requirements-leap.pip @@ -1,2 +1,3 @@ leap.common>=0.5.1 leap.soledad.common>=0.8.0 +treq diff --git a/src/leap/mx/mail_receiver.py b/src/leap/mx/mail_receiver.py index 276ae13..3b3997a 100644 --- a/src/leap/mx/mail_receiver.py +++ b/src/leap/mx/mail_receiver.py @@ -83,7 +83,7 @@ class MailReceiver(Service): MAX_BOUNCE_DELTA = timedelta(days=5) def __init__(self, users_cdb, directories, bounce_from, - bounce_subject): + bounce_subject, incoming_api_helper=False): """ Constructor @@ -107,6 +107,7 @@ class MailReceiver(Service): self._bounce_subject = bounce_subject self._bounce_timestamp = {} self._processing_skipped = False + self._incoming_api = incoming_api_helper def startService(self): """ @@ -237,8 +238,14 @@ class MailReceiver(Service): "I know: %r | %r" % (uuid, doc)) raise Exception("No uuid or doc") - log.msg("Exporting message for %s" % (uuid,)) - yield self._users_cdb.put_doc(uuid, doc) + if self._incoming_api: + log.msg("Exporting message for %s over Incoming API" % (uuid,)) + # TODO: Stop using ServerDocument when old code gets deprecated + content = doc.content[ENC_JSON_KEY] + yield self._incoming_api.put_doc(uuid, doc.doc_id, content) + else: + log.msg("Exporting message for %s directly into CouchDB" % (uuid,)) + yield self._users_cdb.put_doc(uuid, doc) log.msg("Done exporting") def _remove(self, filepath): diff --git a/src/leap/mx/soledadhelper.py b/src/leap/mx/soledadhelper.py new file mode 100644 index 0000000..4b2a099 --- /dev/null +++ b/src/leap/mx/soledadhelper.py @@ -0,0 +1,89 @@ +# -*- encoding: utf-8 -*- +# soledadhelper.py +# Copyright (C) 2017 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 . + + +""" +Classes for working with Soledad Incoming API. +See: http://soledad.readthedocs.io/en/latest/incoming_box.html +""" + + +import base64 +import treq +from six import raise_from +from io import BytesIO +from twisted.internet import defer + + +class UnavailableIncomingAPIException(Exception): + pass + + +class SoledadIncomingAPI: + """ + Delivers messages using Soledad Incoming API. + """ + + def __init__(self, host, port, token): + """ + Creates a SoledadIncomingAPI helper to deliver messages into user's + database. + + :param host: A hostname string for the Soledad incoming service host. + This will usually be localhost, unless served over stunnel. + :type host: str + :param port: The port of the Soledad incoming service host. + :type port: int + :param token: Incoming service authentication token as configured in + Soledad. + :type token: str + """ + self._incoming_url = "http://%s:%s/incoming/" % (host, port) + b64_token = base64.b64encode(token) + self._auth_header = {'Authorization': ['Token %s' % b64_token]} + + @defer.inlineCallbacks + def put_doc(self, uuid, doc_id, content): + """ + Make a PUT request to Soledad's incoming API, delivering a message into + user's database. + + :param uuid: The uuid of a user + :type uuid: str + :param content: Message content. + :type content: str + + :return: A deferred which fires after the HTTP request is complete, or + which fails with the correspondent exception if there was any + error. + """ + url = self._incoming_url + "user-%s/%s" % (uuid, doc_id) + try: + response = yield treq.put( + url, + BytesIO(str(content)), + headers=self._auth_header, + persistent=False) + except Exception as original_exception: + error_message = "Server unreacheable or unknown error: %s" + error_message %= (original_exception.message) + our_exception = UnavailableIncomingAPIException(error_message) + raise_from(our_exception, original_exception) + if not response.code == 200: + error_message = '%s returned status %s instead of 200' + error_message %= (url, response.code) + raise UnavailableIncomingAPIException(error_message) diff --git a/src/leap/mx/tests/tester.py b/src/leap/mx/tests/tester.py index 05d2d05..8ca0987 100644 --- a/src/leap/mx/tests/tester.py +++ b/src/leap/mx/tests/tester.py @@ -2,10 +2,11 @@ import ConfigParser import sys import os -from twisted.internet import reactor, defer +from twisted.internet import reactor from twisted.python import filepath, log from leap.mx import couchdbhelper +from leap.mx import soledadhelper from leap.mx.mail_receiver import MailReceiver if __name__ == "__main__": @@ -30,6 +31,10 @@ if __name__ == "__main__": dbName="identities", username=user, password=password) + incoming_api = False + if config.has_section("incoming api"): + args = (config.get(option) for option in ["host", "port", "token"]) + incoming_api = soledadhelper.SoledadIncomingAPI(*args) # Mail receiver mail_couch_url_prefix = "http://%s:%s@%s:%s" % (user, @@ -37,7 +42,7 @@ if __name__ == "__main__": server, port) - mr = MailReceiver(mail_couch_url_prefix, cdb, []) + mr = MailReceiver(mail_couch_url_prefix, cdb, [], incoming_api) fpath = filepath.FilePath(fullpath) d = mr._process_incoming_email(None, fpath, 0) -- cgit v1.2.3