summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTomas Touceda <chiiph@leap.se>2013-04-19 19:05:13 -0300
committerTomas Touceda <chiiph@leap.se>2013-04-22 10:48:51 -0300
commitb216d5679680e1d7ed9c2cff7d15f43edff5c102 (patch)
treeb78375db66cc8fc3fa003b246b9e927283a42096
parent90e2dbf504e26f6cd3f4ed5e54456078f37984e0 (diff)
More fixes
-rw-r--r--src/leap/mx/alias_resolver.py429
-rw-r--r--src/leap/mx/couchdbhelper.py (renamed from src/leap/mx/couchdb.py)26
-rw-r--r--src/leap/mx/mail_receiver.py157
-rwxr-xr-xstart_mx.py217
4 files changed, 250 insertions, 579 deletions
diff --git a/src/leap/mx/alias_resolver.py b/src/leap/mx/alias_resolver.py
index f71c4d8..eb08c06 100644
--- a/src/leap/mx/alias_resolver.py
+++ b/src/leap/mx/alias_resolver.py
@@ -1,411 +1,60 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
-'''
-alias_resolver.py
-=================
+# alias_resolver.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/>.
+
+"""
Classes for resolving postfix aliases.
-:authors: Isis Agora Lovecruft
-:version: 0.0.1-beta
-:license: see included LICENSE file
-:copyright: (c) 2013 Isis Agora Lovecruft
-
TODO:
-
o Look into using twisted.protocols.postfix.policies classes for
controlling concurrent connections and throttling resource consumption.
-'''
+"""
-import os
-import uuid
+import logging
try:
- from twisted.internet import address, defer, reactor
- from twisted.mail import maildir, alias
+ # TODO: we should probably use the system alias somehow
+ # from twisted.mail import alias
from twisted.protocols import postfix
except ImportError:
print "This software requires Twisted. Please see the README file"
print "for instructions on getting required dependencies."
-from leap.mx import couchdb
-from leap.mx.util import net, log, config, exceptions
-
-
-def createUUID(alias):
- """Creates Universal Unique ID by taking the SHA1 HASH of an email alias:
-
- >>> uuid.uuid5(uuid.NAMESPACE_URL, "isis@leap.se")
- UUID('7194878e-4aea-563f-85a4-4f58519f3c4f')
-
- TODO: Is there a commonly accepted way to check that an email address
- is valid?
-
- :param str alias: An email address alias.
- :returns: A :class:`uuid.UUID` containing attributes specifying the UUID.
- """
- return uuid.uuid5(uuid.NAMESPACE_URL, str(alias))
-
-
-class DatabaseNotConnected(Exception):
- """Raised when not currently connected to a database."""
-
-class StatusCodes(object):
- """The Postfix manual states:
-
- The request completion status is one of OK, RETRY, NOKEY (lookup failed
- because the key was not found), BAD (malformed request) or DENY (the
- table is not approved for proxy read or update access).
-
- In brief, Postfix will send ``get SPACE key NEWLINE``, or
- ``put SPACE key NEWLINE`` where ``key`` is an alias or email address.
- It expects non-printable ascii characters to be url-encoded, i.e. a
- get-request would look like:
-
- ``get%20isis@leap.se%0A``
-
- and in response, Postfix expects an SMTP-like status code and a string
- describing the nature of or reason for the response, no longer than
- 4096 "characters" (which, due to UTF-8 ubiquity, we'll err on the safe
- side and assume that means 4096 bytes.)
-
- From the Postfix manual on its TCP map protocol
- (http://www.postfix.org/tcp_table.5.html):
-
- 500 SPACE text NEWLINE
- In case of a lookup request, the requested data
- does not exist. In case of an update request, the
- request was rejected. The text describes the
- nature of the problem.
-
- 400 SPACE text NEWLINE
- This indicates an error condition. The text
- describes the nature of the problem. The client
- should retry the request later.
-
- 200 SPACE text NEWLINE
- The request was successful. In the case of a lookup
- request, the text contains an encoded version of
- the requested data.
-
- Other SMTP codes: http://www.greenend.org.uk/rjk/tech/smtpreplies.html
-
- >>> statcodes = StatusCodes()
- >>> if <query local user database for email address>:
- >>> response_message = statcodes(200)
- >>> aliasresolver.tellMTA()
- """
-
- OK = "OK Others might say 'HELLA AWESOME'...but we're not convinced."
- RETRY = "RETRY Server is busy plotting revolution; requests might take a while."
- BAD = "BAD bad Leroy Brown, baddest man in the whole...er. Malformed request."
- NOKEY = "NOKEY Couldn't find your keys, sorry. Did you check in the sofa?"
- DEFER = "DEFER_IF_LOCAL xxx fill me in"
- DENY = "DENY no gurlz aloud in teh tree house."
- FAIL = "FAIL this belongs on the failblog"
-
- SMTPCodes = { '200': OK,
- '400': RETRY,
- '500': BAD,
- '550': NOKEY,
- '552': DEFER,
- '553': DENY,
- '554': FAIL, }
-
- SMTPStrings = { 'OK' 200,
- 'RETRY': 400,
- 'BAD': 500,
- 'NOKEY': 550,
- 'DEFER': 552,
- 'DENY': 553,
- 'FAIL': 554, }
-
- def __init__(self, status_code=None):
- """Construct an SMTP status code generator.
-
- :type status_code: str or int
- :param status_code: (optional) see :func:`StatusCode.get`.
- """
- if status_code:
- self.get(status_code)
-
- def get(self, status_code=None)
- """Takes an SMTP-like status code and returns an SMTP-like message.
-
- :type status_code: str or int
- :param status_code: The string or integer for the response we want
- to give back to the MTA, after looking up an
- email address in the local user database.
- Can be one of:
- * ``OK`` or ``200``
- * ``RETRY`` or ``400``
- * ``BAD`` or ``500``
- * ``NOKEY`` or ``550``
- * ``DEFER`` or ``552``
- * ``DENY`` or ``553``
- * ``FAIL`` or ``554``
- """
- if status_code:
- if isinstance(status_code, str):
- if status_code.upper() in self.SMTPStrings.keys():
- return self.SMTPStrings[status_code], getattr(
- self, status_code.upper(), '')
- else:
- return 500, self.FAIL
- elif isinstance(status_code, int):
- for k, v in self.SMTPCodes.items():
- ## we want to return None if it's 500
- if k == str(status_code) and k != '500':
- return status_code, v
- log.debug("%s" % self.NOKEY)
- return None, ''
-
-
-class AliasResolver(postfix.PostfixTCPMapServer):
- """Resolve postfix aliases, similarly to using "$ postmap -q <alias>".
-
- This class starts a simple LineReceiver server which listens for a string
- specifying an alias ``key`` to look up, which will be used to query the
- local user database. You can test it with:
-
- $ ./alias_resolver.py &
- $ /usr/bin/postmap -q <key> tcp:localhost:1347
-
- Resources:
- http://www.postfix.org/proxymap.8.html
- https://www.iana.org/assignments/smtp-enhanced-status-codes/
- """
-
- virtual_transport = '@example.com'
- use_virtual_transport = False
-
- def __init__(self, *args, **kwargs):
- """Create a server which listens for Postfix aliases to resolve.
-
- :param int timeout: Number of seconds to wait for a response.
- :param str delimiter: The delimiter to use for the EOL on responses.
- (Default: '\n')
- """
- super(postfix.PostfixTCPMapServer, self).__init__(*args, **kwargs)
- self.status_codes = StatusCodes()
-
- def sendCode(self, code, message=None):
- """Send an SMTP-like code with a message.
-
- :type code: str or int
- :param code: The status code to send, see
- ``alias_resolver.StatusCodes``.
- """
- try:
- assert isinstance(code, int), "status code must be type int"
- except AssertionError as ae:
- log.err(ae.message)
- self.sendLine('500 internal server error: %s' % ae.message)
-
- msg = self.status_codes.get(code)
- if message is not None and isinstance(message, str):
- msg += (" " + message)
- self.sendLine('%3.3d %s' % (code, message or ''))
-
- def do_get(self, key):
- """Make a query to resolve an alias."""
- if key is None:
- self.sendCode(500)
- log.warn("Command 'get' takes one parameter.")
- else:
- d = defer.maybeDeferred(self.factory.get, key)
- d.addCallbacks(self._cbGot, self._cbNot)
- d.addErrback(log.err)
-
- @defer.inlineCallbacks
- def do_put(self, keyAndValue):
- """Add a key and value to the database, provided it does not exist.
-
- :param str keyAndValue: An alias and email address, separated by a
- space, i.e. ``"isis isis@leap.se"``.
- """
- if keyAndValue is None:
- self.sendCode(500)
- log.warn("Command 'put' takes two parameters.")
- else:
- try:
- key, value = keyAndValue.split(None, 1)
- except ValueError:
- self.sendCode(500)
- log.warn("Command 'put' takes two parameters.")
- else:
- alreadyThere = yield self.do_query(key)
- if alreadyThere is None:
- d = defer.maybeDeferred(self.factory.put, key, value)
- d.addCallbacks(self._cbPut, self._cbPout)
- d.addCallbacks(log.err)
- else:
- self.sendCode(553)
-
- @defer.inlineCallbacks
- def do_delete(self, key):
- """Delete an alias from the CouchDB.
-
- xxx I'm not sure if implementing this would be a good idea...
-
- :param str key: An email address to delete from the CouchDB.
- """
- raise NotImplemented
-
- def check_recipient_access(self, key):
- """Make a query to the CouchDB to resolve an alias.
-
- If the ``key`` is an email address which the CouchDB has information
- for that account, we should respond to Postfix with an '200%20\n".
-
- :param str key: An email address to look up in the CouchDB.
- """
- return self.do_get(key)
-
- def virtual_alias_map(self, key):
- """Get the Universal Unique ID for the alias address. If
- virtual_transport is True, then suffix the UUID with a domain.
-
- xxx I don't think we actually need couchdb for this, the UUID is an
- identifier, not an authenticator. And the SHA1 should always be the
- same, so unless it's considered to expensive to compute (less than
- querying a database, I would presume), it seems silly to do this.
-
- Instead, we should query CouchDB with the UUID to get the GPG keyid.
-
- xxx Or are we supposed to query Soledad for this?
-
- :param str key: An email address to look up in the CouchDB.
- :returns: The UUID of the user.
- """
- ## xxx need email address parser
- userid = createUUID(key)
-
- if self.use_virtual_transport \
- and isinstance(self.virtual_transport, str):
- return userid.get_urn() + self.virtual_transport
- else:
- return userid.get_urn()
+logger = logging.getLogger(__name__)
class AliasResolverFactory(postfix.PostfixTCPMapDeferringDictServerFactory):
- """A Factory for creating :class:`AliasResolver` servers, which handles
- inputs and outputs, and keeps an in-memory mapping of Postfix aliases in
- the form of a dictionary.
-
- >>> from leap.mx import alias_resolver
- >>> aliasResolverFactory = alias_resolver.AliasResolver(
- ... data={'isis': 'isis@leap.se',
- ... 'drebs': 'drebs@leap.se',
- ... 'elijah': 'elijah@leap.se',})
- >>> aliasResolver = aliasResolverFactory.buildProtocol()
- >>> aliasResolver.check_recipient_access('isis')
- """
- protocol = AliasResolver
- database = couchdb.ConnectedCouchDB
-
- def __init__(self, addr='127.0.0.1', port=4242, timeout=120,
- data=None, virtual_transport=None, use_virtual_transport=False,
- couch_host=None, couch_port=None, couch_dbname='users',
- couch_username=None, couch_password=None):
- """Create a Factory which returns :class:`AliasResolver` servers.
-
- :param str addr: A string giving the IP address of this server, for
- talking to postfix. Default: '127.0.0.1'
- :param int port: An integer that specifies the port number that this
- server should listen and respond on, for talking to
- Postfix. on. Default: 4242
- :param int timeout: An integer specifying the number of seconds to wait
- until we should time out. Default: 120
- :param dict data: A dict to use to initialise or update the alias
- mapping.
- :param str virtual_transport: The domain portion of an email address
- to suffix the UUID responses of
- ``AliasResolver.virtual_alias_map`` with.
- :param bool use_virtual_transport: If True, suffix UUIDs with the
- ``virtual_transport`` string.
+ def __init__(self, couchdb, *args, **kwargs):
+ postfix.PostfixTCPMapDeferringDictServerFactory.__init__(self, *args, **kwargs)
+ self._cdb = couchdb
- :param str couch_host: The IP address of the CouchDB server to query.
- :param int couch_port: The port of the CouchDB server to query.
- :param str couch_dbname: The database in the CouchDB to bind to.
- :param str couch_username: The username for authenticating to the
- CouchDB.
- :param str couch_password: The password for authentication.
- """
- super(postfix.PostfixTCPMapDeferringDictServerFactory,
- self).__init__(data=data)
- self.timeout = timeout
- self.virtual_transport = virtual_transport
- self.use_virtual_transport = use_virtual_transport
- self.noisy = True if config.advanced.noisy else False
-
- if couch_port is None:
- couch_port = 5984
- if couch_dbname is None:
- couch_dbname = 'users'
- self.database_connected = False
- if couch_host is not None:
- self.couch = self.connectDatabase(couch_host, couch_port,
- couch_dbname, couch_username,
- couch_password)
+ def _to_str(self, result):
+ if isinstance(result, unicode):
+ result = result.encode("utf8")
+ return result
+ def get(self, key):
+ orig_key = key
try:
- assert isinstance(port, int), "Port number must be an integer"
- assert isinstance(timeout, int), "Timeout must be an integer"
- except AssertionError, ae:
- raise SystemExit(ae.message)
-
- if net.checkIPaddress(addr):
- self.addr = address._IPAddress('TCP', addr, int(port))
- else:
- log.msg("Using default address: 127.0.0.1:%s" % port)
- self.addr = address._IPAddress('TCP', '127.0.0.1', int(port))
-
- log.msg("To configure Postfix to query this alias_resolver,")
- log.msg("you should do:")
- log.msg(" $ postconf -e 'check_recipient_access = tcp:%s:%d"
- % (addr, port))
-
- def buildProtocol(self):
- """Create an instance of the :class:`AliasResolver` server."""
- proto = self.protocol()
- proto.timeout = self.timeout
- proto.virtual_transport = self.virtual_transport
- proto.use_virtual_transport = self.use_virtual_transport
- proto.factory = self
- return proto
-
- def _cb_connectDatabase(self):
- self.database_connected = True
-
- def connectDatabase(self, couch_host, couch_port=None, couch_dbname=None,
- couch_username=None, couch_password=None):
- """Connect to the CouchDB instance."""
- if not self.database_connected:
- d = self.database(couch_host, couch_port, dbName=couch_dbname,
- username=couch_username, password=couch_password)
- d.addCallback(self._cb_connectDatabase)
- d.addErrback(log.err)
- return d
- else:
- return self.couch ## xxx are we sure we only want one connection?
-
- def get(self, key, **kwargs):
- """Query the CouchDB for a user's info.
-
- :param str key: The alias to look up. Should be either an email address
- or a username. (xxx do we want to also support lookups
- by UUID?)
- """
- if self.database_connected:
- return self.couch.queryByEmailOrAlias(key)
- else:
- raise DatabaseNotConnected("Must be connected to a database.")
-
- def put(self, key, **kwargs):
- """Add an alias to the CouchDB database."""
- raise NotImplemented
-
-
-if __name__ == "__main__":
-
- print "To test alias_resolver.py, please use /test/test_alias_resolver.py"
+ key = key.split("@")[0]
+ key = key.split("+")[0]
+ except Exception as e:
+ key = orig_key
+ logger.exception("%s" % (e,))
+ d = self._cdb.queryByLoginOrAlias(key)
+ d.addCallback(self._to_str)
+ return d
diff --git a/src/leap/mx/couchdb.py b/src/leap/mx/couchdbhelper.py
index 277d356..cbb087d 100644
--- a/src/leap/mx/couchdb.py
+++ b/src/leap/mx/couchdbhelper.py
@@ -15,12 +15,10 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-'''
-couchdb.py
-==========
+"""
Classes for working with CouchDB or BigCouch instances which store email alias
maps, user UUIDs, and GPG keyIDs.
-'''
+"""
try:
from paisley import client
@@ -70,6 +68,9 @@ class ConnectedCouchDB(client.CouchDB):
username=username,
password=password,
*args, **kwargs)
+
+ self._cache = {}
+
if dbName is None:
databases = self.listDB()
databases.addCallback(self._print_databases)
@@ -108,6 +109,8 @@ class ConnectedCouchDB(client.CouchDB):
"""
assert isinstance(alias, str), "Email or alias queries must be string"
+ # TODO: Cache results
+
d = self.openView(docId="User",
viewId="by_login_or_alias/",
key=alias,
@@ -131,7 +134,19 @@ class ConnectedCouchDB(client.CouchDB):
"""
for row in result["rows"]:
if row["key"] == alias:
- return row["id"]
+ uuid = row["id"]
+ self._cache[uuid] = row["value"]
+ return uuid
+ return None
+
+
+ def getPubKey(self, uuid):
+ pubkey = None
+ try:
+ pubkey = self._cache[uuid]
+ except:
+ pass
+ return pubkey
if __name__ == "__main__":
@@ -146,6 +161,7 @@ if __name__ == "__main__":
@d.addCallback
def right(result):
print "Should be an actual uuid:", result
+ print cdb.getPubKey(result)
d2 = cdb.queryByLoginOrAlias("asdjaoisdjoiqwjeoi")
@d2.addCallback
diff --git a/src/leap/mx/mail_receiver.py b/src/leap/mx/mail_receiver.py
index 9636591..09200ac 100644
--- a/src/leap/mx/mail_receiver.py
+++ b/src/leap/mx/mail_receiver.py
@@ -1,68 +1,97 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+# mail_receiver.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/>.
+
import os
-import pyinotify
import logging
import argparse
import ConfigParser
from email import message_from_string
+from functools import partial
-logger = logging.getLogger(name='leap_mx')
+from twisted.internet import inotify, reactor
+from twisted.python import filepath
+from leap.mx import couchdbhelper
+from leap.soledad.backends.couch import CouchDatabase
-def _get_uuid(uid, user, password, server):
- # TODO: implement!
- return ""
+logger = logging.getLogger(__name__)
-def _get_pubkey(uuid, user, password, server):
+def _get_pubkey(uuid):
# TODO: implent!
- return ""
+ logger.debug("Fetching pubkey for %s" % (uuid,))
+ return uuid, ""
-def _encrypt_message(pubkey, message):
+def _encrypt_message(uuid_pubkey, message):
# TODO: implement!
- return message
+ uuid, pubkey = uuid_pubkey
+ logger.debug("Encrypting message to %s's pubkey" % (uuid,))
+ logger.debug("Pubkey: %s" % (pubkey,))
+
+ if pubkey is None or len(pubkey) == 0:
+ # TODO: This is only for testing!! REMOVE!
+ return uuid, message
+
+ encrypted = ""
+
+ return uuid, encrypted
+
+
+def _export_message(uuid_message, couch_url):
+ uuid, message = uuid_message
+ logger.debug("Exporting message for %s" % (uuid,))
+
+ if uuid is None:
+ uuid = 0
+ db_url = couch_url + '/user-%s' % uuid
+ db = CouchDatabase.open_database(db_url, create=True)
+ doc = db.create_doc({'content': str(message)})
-def _export_message(uuid, message, user, password, server):
- # TODO: Implement!
return True
-# <Event dir=False mask=0x100 maskname=IN_CREATE name=1366132684.P9922.delloise path=Maildir/tmp pathname=Maildir/tmp/1366132684.P9922.delloise wd=2 >
-# <Event dir=False mask=0x20 maskname=IN_OPEN name=1366132684.P9922.delloise path=Maildir/tmp pathname=Maildir/tmp/1366132684.P9922.delloise wd=2 >
-# <Event dir=False mask=0x2 maskname=IN_MODIFY name=1366132684.P9922.delloise path=Maildir/tmp pathname=Maildir/tmp/1366132684.P9922.delloise wd=2 >
-# <Event dir=False mask=0x8 maskname=IN_CLOSE_WRITE name=1366132684.P9922.delloise path=Maildir/tmp pathname=Maildir/tmp/1366132684.P9922.delloise wd=2 >
-# <Event dir=False mask=0x100 maskname=IN_CREATE name=1366132684.V14I40088dM542424.delloise path=Maildir/new pathname=Maildir/new/1366132684.V14I40088dM542424.delloise wd=4 >
-# <Event dir=False mask=0x200 maskname=IN_DELETE name=1366132684.P9922.delloise path=Maildir/tmp pathname=Maildir/tmp/1366132684.P9922.delloise wd=2 >
-
-class EventHandler(pyinotify.ProcessEvent):
- def __init__(self, user, password, server, *args, **kwargs):
- pyinotify.ProcessEvent.__init__(self, *args, **kwargs)
- self._user = user
- self._password = password
- self._server = server
-
- def process_IN_CREATE(self, event):
- if os.path.split(event.path)[-1] == "new":
- logger.debug("Processing new mail at %s" % (event.pathname,))
- with open(event.pathname, "r") as f:
- mail_data = f.read()
- mail = message_from_string(mail_data)
- owner = mail["Delivered-To"]
- logger.debug("%s received a new mail" % (owner,))
- # get user uuid
- uuid = _get_uuid(owner, self._user, self._password, self._server)
- # get the pubkey for uuid
- pubkey = _get_pubkey(uuid, self._user, self._password, self._server)
- # encrypt the message to the pubkey
- encrypted = _encrypt_message(pubkey, mail_data)
- # save the message in a couchdb
- if _export_message(uuid, encrypted, self._user, self._password, self._server):
- # remove the original mail
- try:
- os.remove(event.pathname)
- except Exception as e:
- # TODO: better handle exceptions
- logger.error(e.message())
+
+def _conditional_remove(do_remove, filepath):
+ if do_remove:
+ # remove the original mail
+ try:
+ logger.debug("Removing %s" % (filepath.path,))
+ filepath.remove()
+ 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["Delivered-To"]
+ logger.debug("%s received a new mail" % (owner,))
+ d = users_db.queryByLoginOrAlias(owner)
+ d.addCallback(_get_pubkey)
+ d.addCallback(_encrypt_message, (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"
@@ -88,7 +117,7 @@ def main():
level = logging.WARNING
if config_file is None:
- config_file = "mail_receiver.cfg"
+ config_file = "leap_mx.cfg"
logger.setLevel(level)
console = logging.StreamHandler()
@@ -108,24 +137,40 @@ def main():
config = ConfigParser.ConfigParser()
config.read(config_file)
- user = config.get("couchdb", "user")
- password = config.get("couchdb", "password")
+ 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)
- wm = pyinotify.WatchManager()
- mask = pyinotify.IN_CREATE
- handler = EventHandler(user, password, server)
- notifier = pyinotify.Notifier(wm, handler)
+ 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.add_watch(to_watch, mask, rec=recursive)
+ wm.watch(filepath.FilePath(to_watch), mask, callbacks=[incoming_partial], recursive=recursive)
- notifier.loop()
+ reactor.run()
if __name__ == "__main__":
main()
diff --git a/start_mx.py b/start_mx.py
index d45209c..d2905c5 100755
--- a/start_mx.py
+++ b/start_mx.py
@@ -1,146 +1,107 @@
#!/usr/bin/env python
-#-*- coding: utf-8 -*-
-"""
- ____
- | MX |_________________________
- ___|____| An encrypting remailer |________
- | |__________________________| |
- | is designed for use on a mail exchange |
- | with OpenPGP implementations and Postfix, |
- | and is part of the Leap Encryption Access |
- | Project platform. |
- |___________________________________________|
-"""
- # authors: Isis Agora Lovecruft, <isis@leap.se> 0x2cdb8b35
- # license: AGPLv3, see included LICENCE file.
- # copyright: copyright (c) 2013 Isis Agora Lovecruft
-
-
-from __future__ import print_function
-from os import getcwd
-from os import path as ospath
-
+# -*- encoding: utf-8 -*-
+# start_mx.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/>.
+
+import argparse
import sys
+import ConfigParser
+import logging
-
-application_name = "leap_mx"
-
-def __get_dirs__():
- """Get the absolute path of the top-level repository directory."""
- here = getcwd()
- base = here.rsplit(application_name, 1)[0]
- repo = ospath.join(base, application_name)
- leap = ospath.join(repo, 'src')
- ours = ospath.join(leap, application_name.replace('_', '/'))
- return repo, leap, ours
-
-## py3k check, snagged from python-gnupg-0.3.2 by Vinay Sajip
-try:
- unicode
- _py3k = False
-except NameError:
- _py3k = True
-
-## Set the $PYTHONPATH:
-repo, leap, ours = __get_dirs__()
-sys.path[:] = map(ospath.abspath, sys.path)
-sys.path.insert(0, leap)
-
-## Now we should be able to import ourselves without installation:
try:
- from leap.mx import runner
- from leap.mx.util import config, log, version
+ from leap.mx import couchdb
+ from leap.mx.alias_resolver import AliasResolverFactory
except ImportError, ie:
- print("%s \nExiting... \n" % ie.message)
+ print "%s \nExiting... \n" % ie.message
sys.exit(1)
try:
- from twisted.python import usage, runtime, failure
- from twisted.python.util import spewer
+ from twisted.internet import reactor
+ from twisted.internet.endpoints import TCP4ServerEndpoint
except ImportError, ie:
- print("This software requires Twisted>=12.0.2, please see the README for")
- print("help on using virtualenv and pip to obtain requirements.")
+ print "This software requires Twisted>=12.0.2, please see the README for"
+ print "help on using virtualenv and pip to obtain requirements."
+logger = logging.getLogger(__name__)
-class MXOptions(usage.Options):
- """Command line options for leap_mx."""
- optParameters = [
- ['config', 'c', 'mx.conf', 'Config file to use']]
- optFlags = [
- ['all-tests', 'a', 'Run all unittests'],
- ['verbose', 'v', 'Increase logging verbosity']]
+if __name__ == "__main__":
+ epilog = "Copyright 2012 The LEAP Encryption Access Project"
+ parser = argparse.ArgumentParser(description="""LEAP MX""",
+ 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
- def opt_version(self):
- """Print leap_mx version and exit."""
- print("Authors: %s" % version.getAuthors())
- print("Licence: AGPLv3, see included LICENSE file")
- print("Copyright: © 2013 Isis Lovecruft, see included COPYLEFT file")
- print("Version: %s" % version.getVersion())
- sys.exit(0)
+ if config_file is None:
+ config_file = "mx.conf"
- def opt_spewer(self):
- """Print *all of the things*. Useful for debugging."""
- sys.settrace(spewer)
+ 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)
- def parseArgs(self):
- """Called with the remaining unrecognised commandline options."""
- log.warn("Couldn't recognise option: %s" % self)
+ logger.info("~~~~~~~~~~~~~~~~~~~")
+ logger.info(" LEAP MX")
+ logger.info("~~~~~~~~~~~~~~~~~~~")
+ logger.info("Reading configuration from %s" % (config_file,))
-if __name__ == "__main__":
- dependency_check = runner.CheckRequirements(version.getPackageName(),
- version.getPipfile())
- ## the following trickery is for printing the module docstring
- ## *before* the options help, and printing it only once:
- import __main__
- print("%s" % __main__.__doc__)
- __main__.__doc__ = ("""
-Example Usage:
- $ start_mx.py --config="./my-mx.conf" --spewer
-""")
-
- mx_options = MXOptions()
- if len(sys.argv) <= 1:
- mx_options.opt_help()
- sys.exit(0)
- try:
- mx_options.parseOptions()
- except usage.UsageError, ue:
- print("%s" % ue.message)
- sys.exit(1)
- options = mx_options.opts
-
- ## Get the config settings:
- config.filename = options['config']
- config.loadConfig()
-
- if config.basic.enable_logfile:
- ## Log to file:
- logfilename = config.basic.logfile
- logfilepath = ospath.join(repo, 'logs')
- log.start(logfilename, logfilepath)
- else:
- ## Otherwise just log to stdout:
- log.start()
+ config = ConfigParser.ConfigParser()
+ config.read(config_file)
- log.msg("Testing logging functionality")
- if runtime.platform.supportsThreads():
- thread_support = "with thread support."
- else:
- thread_support = "without thread support."
- log.debug("Running %s, with Python %s on %s platform %s"
- % (application_name, runtime.shortPythonVersion(),
- runtime.platform.getType(), thread_support))
-
- if options['verbose']:
- config.basic.debug = True
- failure.traceupLength = 7
- failure.startDebugMode()
-
- if options['all-tests']:
- from leap.mx import tests
- tests.run()
- else:
- mx_options.getUsage()
- sys.exit(1)
+ 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")
+
+ cdb = couchdb.ConnectedCouchDB(server,
+ port=port,
+ dbName="users",
+ username=users_user,
+ password=users_password)
+
+ # TODO: use the couchdb for mail
+
+ # TODO: make the listening ports configurable
+ alias_endpoint = TCP4ServerEndpoint(reactor, 4242)
+ alias_endpoint.listen(AliasResolverFactory(couchdb=cdb))
+
+ reactor.run()