summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTomas Touceda <chiiph@leap.se>2013-04-17 12:26:21 -0300
committerTomas Touceda <chiiph@leap.se>2013-04-17 12:26:21 -0300
commit28567f1864cd8c1b15ab6aa0d2caff26f33adc5c (patch)
tree3ff0b90967fa8ae3e06ef382c912f5100a521063
parente908729083e2d60487a24f91da6dc9f259b78f98 (diff)
parent471c584b4fac9de68200f3b252292f1735f3d1a4 (diff)
Merge remote-tracking branch 'isis/feature/connect-couch' into feature/mail_receiver
-rw-r--r--DESIGN.md238
-rw-r--r--README.md26
-rw-r--r--src/leap/mx/alias_resolver.py348
-rw-r--r--src/leap/mx/couchdb.py59
4 files changed, 533 insertions, 138 deletions
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 0000000..2d9fe82
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,238 @@
+# design #
+
+## overview #
+----------------------
+This page pertains to the incoming mail exchange servers of the provider.
+
+General overview of how incoming email will work:
+
+ 1. Incoming message is received by provider's MX servers.
+ 2. The MTA (postfix in our case) does a ton of checks on the message before we
+ even check to see if the recipient is valid (this comes from experience
+ running the riseup mail infrastructure, where the vast majority of messages
+ can be rejected early in the SMTP reception and thus save a ton of processing
+ time on the server).
+ 3. Postfix then queries the database to check if the recipient is valid, if
+ they are over quota, if their account is enabled, and to resolve any aliases
+ for the account.
+ 4. The message is then delivered to an on-disk message spool.
+ 5. A daemon watches for new files in this spool. Each message is encrypted to
+ the user's public key, and stored in the user's incoming message queue (stored
+ in couchdb), and removed from disk.
+ 6. When the user next logs in with their client, the user's message queue is
+ emptied by the client.
+ 7. Each message is decrypted by the client, and then stored in the user's
+ "inbox" as an unread message.
+ 8. This local inbox uses soledad for storage
+ 9. Soledad, in the background, will then re-encrypt this email (now a soledad
+ document), and sync to the cloud.
+
+## postfix pipeline ##
+---------------------------
+incoming mx servers will run postfix, configured in a particular way:
+
+ 1. postscreen: before accepting an incoming message, checks RBLs, checks RFC
+ validity, checks for spam pipelining.
+ (pass) proceed to next step.
+ (fail) return SMTP error, which bounces email.
+ 2. more SMTP checks: valid hostnames, etc.
+ (pass) accepted, proceed to next step.
+ (fail) return SMTP error, which bounces email.
+ 3. check_recipient_access -- look up each recipient and ensure they are
+ allowed to receive messages.
+ (pass) empty result, proceed to next step.
+ (fail) return SMTP error code and error comment, bounce message.
+ 4. milter processessing (spamassassin & clamav)
+ (pass) continue
+ (fail) bounce message, flag as spam, or silently kill.
+ 5. virtual_alias_maps -- map user defined aliases and forwards
+ (local address) continue if new address is for this mx
+ (remote address) continue. normally, postfix would relay to the remote domain, but we don't want that.
+ 6. deliver message to spool
+ (write) save the message to disk on the mx.
+ 7. postfix's job is done, mail_receiver picks up email from spool directory
+
+Questions:
+
+ * what is the best way to have postfix write a message to a spool directory?
+ There is a built-in facility for saving to a maildir, so we could just
+ specify a common maildir for everyone. alternately, we could pipe to a
+ simple command that was responsible for safely saving the file to disk. a
+ third possibility would be to have a local long running daemon that spoke
+ lmtp that postfix forward the message on to for delivery.
+ * if virtual_alias_maps comes after check_recipient_access, then a user with
+ aliases set but who is over quota will not be able to forward email. i think
+ this is fine.
+ * if we are going to support forwarding, we should ensure that the message
+ gets encrypted before getting forwarded. so, postfix should not do any
+ forwarding. instead, this should be the job of mail_receiver.
+
+Considerations:
+
+ 1. high load should fill queue, not crash pipeline: It is important that the
+ pipeline be able to handle massive bursts of email, as often happens with
+ email. This means map lookups need to be very fast, and when there is a high
+ load of email postfix should not be waiting on the mail receiver but must be
+ able to pass the message off quickly and have the slower mail receiver churn
+ through the backlog as best it can.
+ 2. don't lose messages: It is important to not lose any messages when there is
+ a problem. So, generally, a copy of an email should always exist in some spool
+ somewhere, and that copy should not be deleted until there is confirmation
+ that the next stage has succeeded.
+
+## alias_resolver ##
+------------------------------
+The alias_resolver will be a daemon running on MX servers that handles lookups
+in the user database of email aliases, forwards, quota, and account status.
+
+Communication with:
+
+ 1. postfix:: alias_resolver will be bound to localhost and speak postfix's
+ very simple [tcp map protocol -> http://www.postfix.org/tcp_table.5.html].
+
+ 2. couchdb:: alias_resolver will make couchdb queries to a local http load
+ balancer that connects to a couchdb/bigcouch
+ cluster. [directly accessing the couch->https://we.riseup.net/leap+platform/querying-the-couchdb]
+ might help getting started.
+
+### Discussion: ###
+
+ 1. we want the lookups to be fast. using views in couchdb, these should be
+ very fast. when using bigcouch, we can make it faster by specifying a read
+ quorum of 1 (instead of the default 2). this will make it so that only a
+ single couchdb needs to be queried to find the result. i don't know if this
+ would cause problems, but aliases don't change very often.
+
+alias_resolver will be responsible for two map lookups in postfix:
+
+#### check_recipient ####
+-------------------------
+postfix config:
+
+@check_recipient_access tcp:localhost:1000@
+
+postfix will send "get username@domain.org" and alias_resolver should return an
+empty result ("200 \n", i think) if postfix should deliver email to the
+user. otherwise, it should return an error. here is example response, verbatim,
+that can be used to bounce over quota users:
+
+```
+200 DEFER_IF_PERMIT Sorry, your message cannot be delivered because the
+recipient's mailbox is full. If you can contact them another way, you may wish
+to tell them of this problem.
+```
+
+"DEFER_IF_PERMIT" will let the other MX know that this error is temporary and
+that they should try again soon. Typically, an MX will try repeatedly, at
+longer and longer intervals, for four days before giving up.
+
+#### virtual alias map ####
+---------------------------
+postfix config:
+
+@virtual_alias_map tcp:localhost:1001@
+
+postfix will send "get alias-address@domain.org" and alias_resolver should
+return "200 id_123456\n", where 123456 is the unique id of the user that has
+alias-address@domain.org.
+
+couchdb should have a view that will let us query on an (alias) address and
+return the user id.
+
+note: if the result of the alias map (e.g. id_123456) does not have a domain
+suffix, i think postfix will use the 'default transport'. if we want it to use
+the virtual transport instead, we should append the domain (eg
+id_123456@example.org). see
+http://www.postfix.org/ADDRESS_REWRITING_README.html#resolve
+
+
+### Current status: ###
+The current implementation of alias_resolver is in
+leap-mx/src/leap/mx/alias_resolver.py.
+
+The class ```alias_resolver.StatusCodes``` deals with creating SMTP-like
+response messages for Postfix, speaking Postfix's TCP Map protocol (from item
+#1).
+
+As for Discussion item #1:
+
+It might be possible to use
+[python-memcached](https://pypi.python.org/pypi/python-memcached/) as an
+interface to a [memcached](http://memcached.org/) instance to speed up database
+lookups, by keeping an in memory mapping of recent request/response
+pairs. Also, Twisted now (I think as of 12.0.0) ships with a protocol for
+handling Memcached servers, this is in ```twisted.protocols.memcache```. This
+should be prioritised for later, if it is decided that querying the CouchDB is
+too expensive or time-consuming.
+
+Thus far, to speed up alias lookup, an in-memory mapping of alias<->resolution
+pairs is created by ```alias_resolver.AliasResolverFactory()```, which can be
+optionally seeded with a dictionary of ```{ 'alias': 'resolution' }``` pairs
+by doing:
+~~~~~~
+>>> from leap.mx import alias_resolver
+>>> aliasResolverFactory = alias_resolver.AliasResolverFactory(
+... addr='1.2.3.4', port=4242, data={'isis': 'isis@leap.se',
+... 'drebs': 'drebs@leap.se'})
+>>> aliasResolver = aliasResolverFactory.buildProtocol()
+>>> aliasResolver.check_recipient_access('isis')
+200 OK Others might say 'HELLA AWESOME'...but we're not convinced.
+~~~~~~
+
+TODO:
+ 1. The AliasResolverFactory needs to be connected to the CouchDB. The
+ classmethod in which this should occur is ```AliasResolverFactory.get()```.
+
+ 2. I am not sure where to get the user's UUID from (Soledad?). Wherever we get
+ it from, it will need to be returned in
+ ```AliasResolver.virtual_alias_map()```, and if we want Postfix to hear about
+ it, then that response will need to be fed into ```AliasResolver.sendCode```.
+
+ 3. Other than those two things, I think everything is done. The only potential
+ other thing I can think of is that the codes in
+ ```alias_resolver.StatusCodes``` might need to be urlencoded for Postfix to
+ accept them, but this is like two lines of code from urllib.
+
+
+
+## mail_receiver ##
+
+the mail_receiver is a daemon that runs on incoming MX servers and is
+responsible for encrypting incoming email to the user's public key and saving
+the email to an incoming queue database for that user.
+
+communicates with:
+
+ * message spool directory:: mail_reciever sits and waits for new email to be
+ written to the spool directory (maybe using this
+ https://github.com/seb-m/pyinotify, i think it is better than FAM). when a
+ new file is dumped into the spool, mail_receiver reads the file, encrypts
+ the entire thing using the public key of the recipient, and saves to
+ couchdb.
+ * couchdb get:: mail_receiver does a query on user id to get back user's
+ public openpgp key. read quorum of 1 is probably ok.
+ * couchdb put:: mail_receiver communicates with couchdb for storing encrypted
+ email for each user (eventually, mail_receiver will communicate with a local
+ http proxy, that communicates with a bigcouch cluster, but the api is
+ identical)
+
+discussion:
+ * i am not sure if postfix adds a header to indicate to whom a message was
+ actually delivered. if not, this is a problem, because then how do we know
+ what db to put it in or what public key to use? this is perhaps a good
+ reason to not let postfix handle writing the message to disk, but instead
+ pipe it to another command (because postfix sets env variables for stuff
+ like recipient).
+
+ * should the incoming message queue be a separate database or should it be
+ just documents in the user's main database with special flags?
+
+ * whenever possible, we should refer to the user by a fixed id, not their
+ username, because we want to support the ability to change usernames. so,
+ for example, database names should not be based on usernames.
+
+### Current Status: ###
+None of this is done, although having it be a separate daemon sound weird.
+
+You would probably want to use ```twisted.mail.mail.FileMonitoringService``` to
+watch the mailbox (is the mailbox virtual or a maildir or mbox or?)
diff --git a/README.md b/README.md
index 52d6d16..5b4ccf3 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,22 @@
leap_mx
=======
**Note:** Currently in development. Feel free to test, and please [report
- bugs](mailto:isis@leap.se).
+ bugs on our tracker](https://we.riseup.net/leap/mx) or [by email](mailto:isis@leap.se).
An asynchronous, transparently-encrypting remailer for the LEAP platform,
using BigCouch/CouchDB and PGP/GnuPG, written in Twisted Python.
## [install](#install) ##
-=========================
-[tl;dr](#tl;dr)
### [virtualenv](#virtualenv) ###
=================================
+Impatient? Don't like virtualenvs? [tl;dr](#tl;dr)
+
Virtualenv is somewhat equivalent to fakeroot for python packages, and -- due
to being packaged with copies of pip and python -- can be used to bootstrap
its own install process, allowing pip and python to be used with sudo.
-#### [installing without sudo] ####
+#### installing without sudo ####
To install without using sudo, a bootstrap script to handle the setup process
is provided. It does the following:
@@ -38,7 +38,7 @@ $ ./bootstrap
$ workon leap_mx
~~~
-#### [installing in a regular virtualenv] ###
+#### installing in a regular virtualenv ###
To install python, virtualenv, and get started, do:
~~~
@@ -62,8 +62,20 @@ Although, **it is advised** to install inside a python virtualenv.
## [running](#running) ##
=========================
-To get running, clone this repo, and (assuming you've already set up your virtualenv and obtained all the requirements) do:
+To get running, clone this repo, and (assuming you've already set up your
+virtualenv and obtained all the requirements) do:
~~~
$ ./start_mx.py --help
-~~~ \ No newline at end of file
+~~~
+
+## [hacking](#hacking) ##
+=========================
+Please see the HACKING and DESIGN docs.
+
+Our bugtracker is [here](https://leap.se/code/projects/eip_server/issue/new).
+
+Please use that for bug reports and feature requests instead of github's
+tracker. We're using github for code commenting and review between
+collaborators.
+
diff --git a/src/leap/mx/alias_resolver.py b/src/leap/mx/alias_resolver.py
index e079b80..f71c4d8 100644
--- a/src/leap/mx/alias_resolver.py
+++ b/src/leap/mx/alias_resolver.py
@@ -5,22 +5,15 @@ 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
+: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.
-
- o alias.ProcessAlias()
-
-## have uuid -> get gpg keyid
-
-alias.ProcessAlias('/path/to/mail_reciever', *args)
-
'''
import os
@@ -34,73 +27,145 @@ 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 SHA-1 HASH of an email 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')
- >>> 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 alias: An email address alias.
- @returns: A :class:`uuid.UUID` containing attributes specifying the UUID.
+ :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 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.
- 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).
+ 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 xxx fill me in"
-
- fakeSMTPCodes = { '250': OK,
- '300': RETRY,
- '500': BAD,
- '550': NOKEY,
- '552': DEFER,
- '553': DENY,
- '554': FAIL, }
+ 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):
- """xxx fill me in"""
+ """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)
- """xxx fill me in"""
+ """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):
- return status_code, getattr(self, status_code.upper(), None)
+ 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.fake_smtp_codes.items():
- ## we want to return None if it's 550
- if k == str(status_code) and k != '550':
+ 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>".
+ """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:
+ 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
@@ -109,15 +174,36 @@ class AliasResolver(postfix.PostfixTCPMapServer):
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."""
+ """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."""
- if not message:
- message = self.status_codes.get(code)
+ """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):
@@ -132,7 +218,11 @@ class AliasResolver(postfix.PostfixTCPMapServer):
@defer.inlineCallbacks
def do_put(self, keyAndValue):
- """Add a key and value to the database, provided it does not exist."""
+ """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.")
@@ -153,20 +243,26 @@ class AliasResolver(postfix.PostfixTCPMapServer):
@defer.inlineCallbacks
def do_delete(self, key):
- """
- Delete an alias from the mapping database.
+ """Delete an alias from the CouchDB.
- xxx not sure if this is a good idea...
+ 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 resolve an alias."""
- self.do_get(self, key)
+ """Make a query to the CouchDB to resolve an alias.
- def virtual_alias_map(self, key):
+ 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.
"""
- Get the Universal Unique ID for the alias address. If
+ 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
@@ -175,62 +271,83 @@ class AliasResolver(postfix.PostfixTCPMapServer):
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
- client_id = createUUID(key)
+ userid = 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)
+ if self.use_virtual_transport \
+ and isinstance(self.virtual_transport, str):
+ return userid.get_urn() + self.virtual_transport
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
+ return userid.get_urn()
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
+ """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
-
- 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.
+ 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.
+
+ :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)
+
try:
assert isinstance(port, int), "Port number must be an integer"
assert isinstance(timeout, int), "Timeout must be an integer"
@@ -249,25 +366,44 @@ class AliasResolverFactory(postfix.PostfixTCPMapDeferringDictServerFactory):
% (addr, port))
def buildProtocol(self):
- """
- Create an instance of the :class:`AliasResolver` server.
- """
+ """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 get(self, *args, **kwargs):
- """
- xxx connect me to the couchdb
- """
- pass
+ def _cb_connectDatabase(self):
+ self.database_connected = True
- def put(self, *args, **kwargs):
- """
- xxx connect me to the couchdb
+ 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?)
"""
- pass
+ 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__":
diff --git a/src/leap/mx/couchdb.py b/src/leap/mx/couchdb.py
index b5d4127..04cfc4d 100644
--- a/src/leap/mx/couchdb.py
+++ b/src/leap/mx/couchdb.py
@@ -26,44 +26,49 @@ from leap.mx.util import log
class ConnectedCouchDB(client.CouchDB):
- """
- Connect to a CouchDB instance.
+ """Connect to a CouchDB instance.
- ## xxx will we need to open CouchDB documents and views?
- ## yes, these are in a _design document
+ CouchDB document for testing is '_design', and the view is simply
+ a preconfigured set of mapped responses.
"""
- def __init__(self, host, port, dbName=None, username=None,
+ def __init__(self, host, port=5984, dbName=None, username=None,
password=None, *args, **kwargs):
"""
Connect to a CouchDB instance.
- @param host: A hostname string for the CouchDB server.
- @param port: The port of the CouchDB server, as an integer.
- @param dbName: (optional) The default database to connect to.
- @param username: (optional) The username for authorization.
- @param password: (optional) The password for authorization.
- @returns: A :class:`twisted.internet.defer.Deferred` representing the
+ :param str host: A hostname string for the CouchDB server.
+ :param int port: The port of the CouchDB server.
+ :param str dbName: (optional) The default database to bind queries to.
+ :param str username: (optional) The username for authorization.
+ :param str password: (optional) The password for authorization.
+ :returns: A :class:`twisted.internet.defer.Deferred` representing the
the client connection to the CouchDB instance.
"""
- super(client.CouchDB, self).__init__(host, port,
+ super(client.CouchDB, self).__init__(host,
+ port=port,
dbName=dbName,
username=username,
password=password,
*args, **kwargs)
- if dbName:
- self.bindToDB(dbName)
- else:
+ if dbName is None:
databases = self.listDB()
log.msg("Available databases: %s" % databases)
+ def createDB(self, dbName):
+ """Overrides ``paisley.client.CouchDB.createDB``."""
+ pass
+
+ def deleteDB(self, dbName):
+ """Overrides ``paisley.client.CouchDB.deleteDB``."""
+ pass
+
def queryByEmailOrAlias(self, alias, dbDoc="User",
view="by_email_or_alias"):
- """
- Check to see if a particular email or alias exists.
+ """Check to see if a particular email or alias exists.
- @param alias: A string representing the email or alias to check.
- @param dbDoc: The CouchDB document to open.
- @param view: The view of the CouchDB document to use.
+ :param str alias: A string representing the email or alias to check.
+ :param str dbDoc: The CouchDB document to open.
+ :param str view: The view of the CouchDB document to use.
"""
assert isinstance(alias, str), "Email or alias queries must be string"
@@ -84,8 +89,10 @@ class ConnectedCouchDB(client.CouchDB):
return d
def query(self, uri):
- """
- Query a CouchDB instance that we are connected to.
+ """Query a CouchDB instance that we are connected to.
+
+ :param str uri: A particular URI in the CouchDB, i.e.
+ "/users/_design/User/_view/by_email_or_alias".
"""
try:
self.checkURI(uri) ## xxx write checkURI()
@@ -102,12 +109,14 @@ class ConnectedCouchDB(client.CouchDB):
@defer.inlineCallbacks
def listUsersAndEmails(self, limit=1000, reverse=False):
- """
- List all users and email addresses, up to the given limit.
+ """List all users and email addresses, up to the given limit.
+
+ :param int limit: The number of results to limit the response to.
+ :param bool reverse: Start at the end of the database mapping.
"""
query = "/users/_design/User/_view/by_email_or_alias/?reduce=false"
answer = yield self.query(query, limit=limit, reverse=reverse)
-
+
if answer:
parsed = yield self.parseResult(answer)
if parsed: