summaryrefslogtreecommitdiff
path: root/src/leap/mx/alias_resolver.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/mx/alias_resolver.py')
-rw-r--r--src/leap/mx/alias_resolver.py275
1 files changed, 275 insertions, 0 deletions
diff --git a/src/leap/mx/alias_resolver.py b/src/leap/mx/alias_resolver.py
new file mode 100644
index 0000000..e079b80
--- /dev/null
+++ b/src/leap/mx/alias_resolver.py
@@ -0,0 +1,275 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+alias_resolver.py
+=================
+Classes for resolving postfix aliases.
+
+@authors: Isis Agora Lovecruft
+@version: 0.0.1-beta
+@license: see included LICENSE file
+@copyright: copyright 2013 Isis Agora Lovecruft
+
+TODO:
+
+ o Look into using twisted.protocols.postfix.policies classes for
+ controlling concurrent connections and throttling resource consumption.
+
+ o alias.ProcessAlias()
+
+## have uuid -> get gpg keyid
+
+alias.ProcessAlias('/path/to/mail_reciever', *args)
+
+'''
+
+import os
+import uuid
+
+try:
+ from twisted.internet import address, defer, reactor
+ from twisted.mail import maildir, 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.util import net, log, config, exceptions
+
+
+def createUUID(alias):
+ """
+ Creates Universal Unique ID by taking the SHA-1 HASH of an email alias:
+
+ >>> uuid.uuid5(uuid.NAMESPACE_URL, "isis@leap.se")
+ UUID('7194878e-4aea-563f-85a4-4f58519f3c4f')
+
+ @param alias: An email address alias.
+ @returns: A :class:`uuid.UUID` containing attributes specifying the UUID.
+ """
+ return uuid.uuid5(uuid.NAMESPACE_URL, str(alias))
+
+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).
+
+ Other SMTP codes: http://www.greenend.org.uk/rjk/tech/smtpreplies.html
+ """
+ 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 xxx fill me in"
+
+ fakeSMTPCodes = { '250': OK,
+ '300': RETRY,
+ '500': BAD,
+ '550': NOKEY,
+ '552': DEFER,
+ '553': DENY,
+ '554': FAIL, }
+
+ def __init__(self, status_code=None):
+ """xxx fill me in"""
+ if status_code:
+ self.get(status_code)
+
+ def get(self, status_code=None)
+ """xxx fill me in"""
+ if status_code:
+ if isinstance(status_code, str):
+ return status_code, getattr(self, status_code.upper(), None)
+ elif isinstance(status_code, int):
+ for k, v in self.fake_smtp_codes.items():
+ ## we want to return None if it's 550
+ if k == str(status_code) and k != '550':
+ 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 to look up, :param:`key`, and which will be used to
+ query the local Postfix server. 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/
+ """
+ def __init__(self, *args, **kwargs):
+ """Create a server which listens for Postfix aliases to resolve."""
+ 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."""
+ if not message:
+ message = self.status_codes.get(code)
+ 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."""
+ 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 mapping database.
+
+ xxx not sure if this is a good idea...
+ """
+ raise NotImplemented
+
+ def check_recipient_access(self, key):
+ """Make a query to resolve an alias."""
+ self.do_get(self, 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 need email address parser
+ client_id = createUUID(key)
+
+ if self.virtual_transport:
+ return client.get_urn() + '@example.com'
+ else:
+ return client.get_urn()
+
+ def _cbGot(self, value):
+ """Callback for self.get()"""
+ if value is None:
+ self.sendCode(550)
+ else:
+ self.sendCode(250, quote(value))
+
+ def _cbNot(self, fail):
+ """Errback for self.get()"""
+ self.sendCode(554, fail.getErrorMessage())
+
+ def _cbPut(self, value):
+ """xxx fill me in"""
+ pass
+
+ def _cbPout(self, fail):
+ """xxx fill me in"""
+ pass
+
+
+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.
+
+ xxx fill me in
+ """
+ protocol = AliasResolver
+
+ def __init__(self, addr='127.0.0.1', port=4242, timeout=120, data=None):
+ """
+ Create a Factory which returns :class:`AliasResolver` servers.
+
+ @param addr: A string giving the IP address of this server.
+ Default: '127.0.0.1'
+ @param port: An integer that specifies the port number to listen
+ on. Default: 4242
+ @param timeout: An integer specifying the number of seconds to wait
+ until we should time out. Default: 120
+ @param data: A dict to use to initialise or update the alias mapping.
+ """
+ super(postfix.PostfixTCPMapDeferringDictServerFactory,
+ self).__init__(data=data)
+ self.timeout = timeout
+ self.noisy = True if config.advanced.noisy else False
+
+ 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.factory = self
+ return proto
+
+ def get(self, *args, **kwargs):
+ """
+ xxx connect me to the couchdb
+ """
+ pass
+
+ def put(self, *args, **kwargs):
+ """
+ xxx connect me to the couchdb
+ """
+ pass
+
+
+if __name__ == "__main__":
+
+ print "To test alias_resolver.py, please use /test/test_alias_resolver.py"