diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/leap/mx/alias_resolver.py | 429 | ||||
| -rw-r--r-- | src/leap/mx/couchdbhelper.py (renamed from src/leap/mx/couchdb.py) | 26 | ||||
| -rw-r--r-- | src/leap/mx/mail_receiver.py | 157 | 
3 files changed, 161 insertions, 451 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() | 
