summaryrefslogtreecommitdiff
path: root/src/leap
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap')
-rw-r--r--src/leap/mx/alias_resolver.py28
-rw-r--r--src/leap/mx/check_recipient_access.py4
-rw-r--r--src/leap/mx/couchdbhelper.py17
-rw-r--r--src/leap/mx/mail_receiver.py341
4 files changed, 192 insertions, 198 deletions
diff --git a/src/leap/mx/alias_resolver.py b/src/leap/mx/alias_resolver.py
index 3f93f0d..68c6212 100644
--- a/src/leap/mx/alias_resolver.py
+++ b/src/leap/mx/alias_resolver.py
@@ -24,18 +24,15 @@ TODO:
controlling concurrent connections and throttling resource consumption.
"""
-import logging
-
try:
# TODO: we should probably use the system alias somehow
# from twisted.mail import alias
from twisted.protocols import postfix
+ from twisted.python import log
except ImportError:
print "This software requires Twisted. Please see the README file"
print "for instructions on getting required dependencies."
-logger = logging.getLogger(__name__)
-
class AliasResolverFactory(postfix.PostfixTCPMapDeferringDictServerFactory):
def __init__(self, couchdb, *args, **kwargs):
@@ -46,23 +43,24 @@ class AliasResolverFactory(postfix.PostfixTCPMapDeferringDictServerFactory):
if isinstance(result, unicode):
result = result.encode("utf8")
if result is None:
- logger.debug("Result not found")
+ log.msg("Result not found")
return result
def get(self, key):
- orig_key = key
try:
- logger.debug("Processing key: %s" % (key,))
+ log.msg("Processing key: %s" % (key,))
if key.find("@") == -1:
- logger.debug("Ignoring key since it's not an email address")
+ log.msg("Ignoring key since it's not an email address")
return None
key = key.split("@")[0]
key = key.split("+")[0]
- logger.debug("Final key to query: %s" % (key,))
- except Exception as e:
- key = orig_key
- logger.exception("%s" % (e,))
- d = self._cdb.queryByLoginOrAlias(key)
- d.addCallback(self._to_str)
- return d
+ log.msg("Final key to query: %s" % (key,))
+ d = self._cdb.queryByLoginOrAlias(key)
+ d.addCallback(self._to_str)
+ d.addErrback(log.err)
+ return d
+ except:
+ log.err()
+
+ return None
diff --git a/src/leap/mx/check_recipient_access.py b/src/leap/mx/check_recipient_access.py
index 3ea6e91..1b44504 100644
--- a/src/leap/mx/check_recipient_access.py
+++ b/src/leap/mx/check_recipient_access.py
@@ -20,14 +20,10 @@
Classes for resolving postfix recipient access
"""
-import logging
-
from twisted.protocols import postfix
from leap.mx.alias_resolver import AliasResolverFactory
-logger = logging.getLogger(__name__)
-
class CheckRecipientAccess(postfix.PostfixTCPMapServer):
def _cbGot(self, value):
diff --git a/src/leap/mx/couchdbhelper.py b/src/leap/mx/couchdbhelper.py
index 2f6b548..7c4c8ce 100644
--- a/src/leap/mx/couchdbhelper.py
+++ b/src/leap/mx/couchdbhelper.py
@@ -20,8 +20,6 @@ Classes for working with CouchDB or BigCouch instances which store email alias
maps, user UUIDs, and GPG keyIDs.
"""
-import logging
-
from functools import partial
try:
@@ -32,12 +30,11 @@ except ImportError:
try:
from twisted.internet import defer
+ from twisted.python import log
except ImportError:
print "This software requires Twisted. Please see the README file"
print "for instructions on getting required dependencies."
-logger = logging.getLogger(__name__)
-
class ConnectedCouchDB(client.CouchDB):
"""
@@ -84,9 +81,9 @@ class ConnectedCouchDB(client.CouchDB):
@param data: response from the listDB command
@type data: array
"""
- logger.msg("Available databases:")
+ log.msg("Available databases:")
for database in data:
- logger.msg(" * %s" % (database,))
+ log.msg(" * %s" % (database,))
def createDB(self, dbName):
"""
@@ -119,7 +116,7 @@ class ConnectedCouchDB(client.CouchDB):
reduce=False,
include_docs=True)
- d.addCallbacks(partial(self._get_uuid, alias), logger.error)
+ d.addCallbacks(partial(self._get_uuid, alias), log.err)
return d
@@ -138,14 +135,10 @@ class ConnectedCouchDB(client.CouchDB):
for row in result["rows"]:
if row["key"] == alias:
uuid = row["id"]
- try:
- self._cache[uuid] = row["doc"]["public_key"]
- except:
- pass # no public key for this user
+ self._cache[uuid] = row["doc"].get("public_key", None)
return uuid
return None
-
def getPubKey(self, uuid):
pubkey = None
try:
diff --git a/src/leap/mx/mail_receiver.py b/src/leap/mx/mail_receiver.py
index 00d93ba..2494a21 100644
--- a/src/leap/mx/mail_receiver.py
+++ b/src/leap/mx/mail_receiver.py
@@ -16,190 +16,197 @@
# 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 service definition
+"""
+
import os
import uuid as pyuuid
-import logging
-import argparse
-import ConfigParser
import json
from email import message_from_string
-from functools import partial
-
-from twisted.internet import inotify, reactor
-from twisted.python import filepath
-from leap.mx import couchdbhelper
+from twisted.application.service import Service
+from twisted.internet import inotify
+from twisted.python import filepath, log
from leap.soledad import LeapDocument
from leap.soledad.backends.leap_backend import EncryptionSchemes
from leap.soledad.backends.couch import CouchDatabase
from leap.common.keymanager import openpgp
-logger = logging.getLogger(__name__)
-
-
-def _get_pubkey(uuid, cdb):
- logger.debug("Fetching pubkey for %s" % (uuid,))
- return uuid, cdb.getPubKey(uuid)
-def _encrypt_message(uuid_pubkey, address_message):
- uuid, pubkey = uuid_pubkey
- address, message = address_message
- logger.debug("Encrypting message to %s's pubkey" % (uuid,))
- logger.debug("Pubkey: %s" % (pubkey,))
+class MailReceiver(Service):
+ """
+ Service that monitors incoming email and processes it
+ """
+
+ def __init__(self, mail_couch_url, users_cdb, directories):
+ """
+ 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
+ @param directories: list of directories to monitor
+ @type directories: list of tuples (path: str, recursive: bool)
+ """
+ # Service doesn't define an __init__
+ self._mail_couch_url = mail_couch_url
+ self._users_cdb = users_cdb
+ self._directories = directories
+
+ def startService(self):
+ """
+ Starts the MailReceiver service
+ """
+ Service.startService(self)
+ wm = inotify.INotify()
+ wm.startReading()
+
+ mask = inotify.IN_CREATE
+
+ for directory, recursive in self._directories:
+ log.msg("Watching %s --- Recursive: %s" % (directory, recursive))
+ wm.watch(filepath.FilePath(directory), mask, callbacks=[self._process_incoming_email], recursive=recursive)
+
+ def _get_pubkey(self, uuid):
+ """
+ Given a UUID for a user, retrieve its public key
+
+ @param uuid: UUID for a user
+ @type uuid: str
+
+ @return: uuid, public key
+ @rtype: tuple of (str, str)
+ """
+ log.msg("Fetching pubkey for %s" % (uuid,))
+ return uuid, self._users_cdb.getPubKey(uuid)
+
+ def _encrypt_message(self, uuid_pubkey, address, message):
+ """
+ Given a UUID, a public key, address and a message, it encrypts
+ the message to that public key.
+ The address is needed in order to build the OpenPGPKey object.
+
+ @param uuid_pubkey: tuple that holds the uuid and the public
+ key as it is returned by the previous call in the chain
+ @type uuid_pubkey: tuple (str, str)
+ @param address: mail address for this message
+ @type address: str
+ @param message: message contents
+ @type message: str
+
+ @return: uuid, doc to sync with Soledad
+ @rtype: tuple(str, LeapDocument)
+ """
+ uuid, pubkey = uuid_pubkey
+ log.msg("Encrypting message to %s's pubkey" % (uuid,))
+ log.msg("Pubkey: %s" % (pubkey,))
+
+ doc = LeapDocument(doc_id=str(pyuuid.uuid4()))
+
+ data = {'incoming': True, 'content': message}
+
+ if pubkey is None or len(pubkey) == 0:
+ doc.content = {
+ "_encryption_scheme": EncryptionSchemes.NONE,
+ "_unencrypted_json": json.dumps(data)
+ }
+ return uuid, doc
+
+ def _ascii_to_openpgp_cb(gpg):
+ key = gpg.list_keys().pop()
+ return openpgp._build_key_from_gpg(address, key, pubkey)
+
+ openpgp_key = openpgp._safe_call(_ascii_to_openpgp_cb, pubkey)
- doc = LeapDocument(encryption_scheme=EncryptionSchemes.PUBKEY,
- doc_id=str(pyuuid.uuid4()))
-
- data = {'incoming': True, 'content': message}
-
- if pubkey is None or len(pubkey) == 0:
doc.content = {
- "_unencrypted_json": json.dumps(data)
+ "_encryption_scheme": EncryptionSchemes.PUBKEY,
+ "_encrypted_json": openpgp.encrypt_asym(json.dumps(data), openpgp_key)
}
- return uuid, doc
-
- def _ascii_to_openpgp_cb(gpg):
- key = gpg.list_keys().pop()
- return openpgp._build_key_from_gpg(address, key, pubkey)
-
- openpgp_key = openpgp._safe_call(_ascii_to_openpgp_cb, pubkey)
-
- doc.content = {
- "_encrypted_json": openpgp.encrypt_asym(json.dumps(data), openpgp_key)
- }
-
- return uuid, doc
-
-
-def _export_message(uuid_doc, couch_url):
- uuid, doc = uuid_doc
- logger.debug("Exporting message for %s" % (uuid,))
-
- if uuid is None:
- uuid = 0
-
- db = CouchDatabase(couch_url, "user-%s" % (uuid,))
- db.put_doc(doc)
- logger.debug("Done exporting")
-
- return True
-
-
-def _conditional_remove(do_remove, filepath):
- if do_remove:
- # remove the original mail
- try:
- logger.debug("Removing %s" % (filepath.path,))
- filepath.remove()
- logger.debug("Done removing")
- except Exception as e:
- # TODO: better handle exceptions
- logger.exception("%s" % (e,))
-
-
-def _process_incoming_email(users_db, mail_couchdb_url_prefix, self, filepath, mask):
- if os.path.split(filepath.dirname())[-1] == "new":
- logger.debug("Processing new mail at %s" % (filepath.path,))
- with filepath.open("r") as f:
- mail_data = f.read()
- mail = message_from_string(mail_data)
- owner = mail["To"]
- if owner is None: # default to Delivered-To
- owner = mail["Delivered-To"]
- owner = owner.split("@")[0]
- owner = owner.split("+")[0]
- logger.debug("Mail owner: %s" % (owner,))
-
- logger.debug("%s received a new mail" % (owner,))
- d = users_db.queryByLoginOrAlias(owner)
- d.addCallback(_get_pubkey, (users_db))
- d.addCallback(_encrypt_message, (owner, mail_data))
- d.addCallback(_export_message, (mail_couchdb_url_prefix))
- d.addCallback(_conditional_remove, (filepath))
-
-
-def main():
- epilog = "Copyright 2012 The LEAP Encryption Access Project"
- parser = argparse.ArgumentParser(description="""LEAP MX Mail receiver""", epilog=epilog)
- parser.add_argument('-d', '--debug', action="store_true",
- help="Launches the LEAP MX mail receiver with debug output")
- parser.add_argument('-l', '--logfile', metavar="LOG FILE", nargs='?',
- action="store", dest="log_file",
- help="Writes the logs to the specified file")
- parser.add_argument('-c', '--config', metavar="CONFIG FILE", nargs='?',
- action="store", dest="config",
- help="Where to look for the configuration file. " \
- "Default: mail_receiver.cfg")
-
- opts, _ = parser.parse_known_args()
-
- debug = opts.debug
- config_file = opts.config
-
- if debug:
- level = logging.DEBUG
- else:
- level = logging.WARNING
-
- if config_file is None:
- config_file = "leap_mx.cfg"
-
- logger.setLevel(level)
- console = logging.StreamHandler()
- console.setLevel(level)
- formatter = logging.Formatter(
- '%(asctime)s '
- '- %(name)s - %(levelname)s - %(message)s')
- console.setFormatter(formatter)
- logger.addHandler(console)
+ return uuid, doc
- logger.info("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
- logger.info(" LEAP MX Mail receiver")
- logger.info("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
+ def _export_message(self, uuid_doc):
+ """
+ Given a UUID and a LeapDocument, 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_doc: tuple that holds the UUID and LeapDocument
+ @type uuid_doc: tuple(str, LeapDocument)
+
+ @return: True if it's ok to remove the message, False
+ otherwise
+ @rtype: bool
+ """
+ uuid, doc = uuid_doc
+ log.msg("Exporting message for %s" % (uuid,))
+
+ if uuid is None:
+ uuid = 0
+
+ db = CouchDatabase(self._mail_couch_url, "user-%s" % (uuid,))
+ db.put_doc(doc)
+
+ log.msg("Done exporting")
+
+ return True
+
+ def _conditional_remove(self, do_remove, filepath):
+ """
+ Removes the message if do_remove is True
+
+ @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 %s" % (filepath.path,))
+ filepath.remove()
+ log.msg("Done removing")
+ except:
+ log.err()
+
+ def _process_incoming_email(self, otherself, filepath, mask):
+ """
+ Callback that processes incoming email
+
+ @param otherself: Watch object for the current callback from
+ inotify
+ @type otherself: twisted.internet.inotify._Watch
+ @param filepath: Path of the file that changed
+ @type filepath: twisted.python.filepath.FilePath
+ @param mask: identifier for the type of change that triggered
+ this callback
+ @type mask: int
+ """
+ if os.path.split(filepath.dirname())[-1] == "new":
+ log.msg("Processing new mail at %s" % (filepath.path,))
+ with filepath.open("r") as f:
+ mail_data = f.read()
+ mail = message_from_string(mail_data)
+ owner = mail["To"]
+ if owner is None: # default to Delivered-To
+ owner = mail["Delivered-To"]
+ owner = owner.split("@")[0]
+ owner = owner.split("+")[0]
+ log.msg("Mail owner: %s" % (owner,))
+
+ log.msg("%s received a new mail" % (owner,))
+ d = self._users_cdb.queryByLoginOrAlias(owner)
+ d.addCallbacks(self._get_pubkey, log.err)
+ d.addCallbacks(self._encrypt_message, log.err, (owner, mail_data))
+ d.addCallbacks(self._export_message, log.err)
+ d.addCallbacks(self._conditional_remove, log.err, (filepath,))
+ d.addErrback(log.err)
- logger.info("Reading configuration from %s" % (config_file,))
-
- config = ConfigParser.ConfigParser()
- config.read(config_file)
-
- users_user = config.get("couchdb", "users_user")
- users_password = config.get("couchdb", "users_password")
-
- mail_user = config.get("couchdb", "mail_user")
- mail_password = config.get("couchdb", "mail_password")
-
- server = config.get("couchdb", "server")
- port = config.get("couchdb", "port")
-
- wm = inotify.INotify(reactor)
- wm.startReading()
-
- mask = inotify.IN_CREATE
-
- users_db = couchdbhelper.ConnectedCouchDB(server,
- port=port,
- dbName="users",
- username=users_user,
- password=users_password)
-
- mail_couch_url_prefix = "http://%s:%s@localhost:%s" % (mail_user,
- mail_password,
- port)
-
- incoming_partial = partial(_process_incoming_email, users_db, mail_couch_url_prefix)
- for section in config.sections():
- if section in ("couchdb"):
- continue
- to_watch = config.get(section, "path")
- recursive = config.getboolean(section, "recursive")
- logger.debug("Watching %s --- Recursive: %s" % (to_watch, recursive))
- wm.watch(filepath.FilePath(to_watch), mask, callbacks=[incoming_partial], recursive=recursive)
-
- reactor.run()
-
-if __name__ == "__main__":
- main()