[bridgedb/develop] Move bridgedb.Dist.EmailBasedDistributor → bridgedb.email.distributor.

isis at torproject.org isis at torproject.org
Thu Jun 25 07:10:55 UTC 2015


commit 55f36f523dae951e2c0e57cdfdc84e3a78ba7cbe
Author: Isis Lovecruft <isis at torproject.org>
Date:   Tue Apr 21 21:41:20 2015 +0000

    Move bridgedb.Dist.EmailBasedDistributor → bridgedb.email.distributor.
---
 doc/sphinx/source/bridgedb.Dist.rst           |    8 -
 doc/sphinx/source/bridgedb.email.rst          |    1 +
 doc/sphinx/source/bridgedb.rst                |    1 -
 doc/sphinx/source/conf.py                     |    2 +-
 lib/bridgedb/Dist.py                          |  186 ----------------------
 lib/bridgedb/Main.py                          |    9 +-
 lib/bridgedb/email/autoresponder.py           |    8 +-
 lib/bridgedb/email/distributor.py             |  209 +++++++++++++++++++++++++
 lib/bridgedb/email/request.py                 |    6 +-
 lib/bridgedb/email/server.py                  |    4 +-
 lib/bridgedb/email/templates.py               |    2 +-
 lib/bridgedb/parse/addr.py                    |    7 +-
 lib/bridgedb/persistent.py                    |    4 +-
 lib/bridgedb/test/email_helpers.py            |   14 +-
 lib/bridgedb/test/legacy_Tests.py             |   43 +----
 lib/bridgedb/test/test_email_autoresponder.py |    2 +-
 lib/bridgedb/test/test_email_distributor.py   |   93 +++++++++++
 lib/bridgedb/test/test_email_server.py        |    6 +-
 18 files changed, 340 insertions(+), 265 deletions(-)

diff --git a/doc/sphinx/source/bridgedb.Dist.rst b/doc/sphinx/source/bridgedb.Dist.rst
deleted file mode 100644
index 995a7ac..0000000
--- a/doc/sphinx/source/bridgedb.Dist.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-bridgedb.Dist
-----------------
-
-.. automodule:: bridgedb.Dist
-    :members:
-    :undoc-members:
-    :private-members:
-    :show-inheritance:
diff --git a/doc/sphinx/source/bridgedb.email.rst b/doc/sphinx/source/bridgedb.email.rst
index ba9b4a9..020b614 100644
--- a/doc/sphinx/source/bridgedb.email.rst
+++ b/doc/sphinx/source/bridgedb.email.rst
@@ -8,6 +8,7 @@ bridgedb.email
 
 .. automodule:: bridgedb.email.__init__
 .. automodule:: bridgedb.email.autoresponder
+.. automodule:: bridgedb.email.distributor
 .. automodule:: bridgedb.email.dkim
 .. automodule:: bridgedb.email.request
 .. automodule:: bridgedb.email.server
diff --git a/doc/sphinx/source/bridgedb.rst b/doc/sphinx/source/bridgedb.rst
index 7b1fef7..207313c 100644
--- a/doc/sphinx/source/bridgedb.rst
+++ b/doc/sphinx/source/bridgedb.rst
@@ -13,7 +13,6 @@ BridgeDB Package and Module Documentation
     bridgedb.captcha
     bridgedb.configure
     bridgedb.crypto
-    bridgedb.Dist
     bridgedb.email
     bridgedb.filters
     bridgedb.geo
diff --git a/doc/sphinx/source/conf.py b/doc/sphinx/source/conf.py
index 5038abb..cab614f 100644
--- a/doc/sphinx/source/conf.py
+++ b/doc/sphinx/source/conf.py
@@ -33,9 +33,9 @@ import bridgedb.captcha
 import bridgedb.Bridges
 import bridgedb.Bucket
 import bridgedb.crypto
-import bridgedb.Dist
 import bridgedb.email
 import bridgedb.email.autoresponder
+import bridgedb.email.distributor
 import bridgedb.email.dkim
 import bridgedb.email.request
 import bridgedb.email.server
diff --git a/lib/bridgedb/Dist.py b/lib/bridgedb/Dist.py
deleted file mode 100644
index 1be705f..0000000
--- a/lib/bridgedb/Dist.py
+++ /dev/null
@@ -1,186 +0,0 @@
-# -*- coding: utf-8 ; test-case-name: bridgedb.test.test_Dist -*-
-#
-# This file is part of BridgeDB, a Tor bridge distribution system.
-#
-# :authors: Nick Mathewson
-#           Isis Lovecruft 0xA3ADB67A2CDB8B35 <isis at torproject.org>
-#           Matthew Finkel 0x017DD169EA793BE2 <sysrqb at torproject.org>
-# :copyright: (c) 2013-2015, Isis Lovecruft
-#             (c) 2013-2015, Matthew Finkel
-#             (c) 2007-2015, The Tor Project, Inc.
-# :license: 3-Clause BSD, see LICENSE for licensing information
-
-"""This module has functions to decide which bridges to hand out to whom."""
-
-import logging
-import time
-
-import bridgedb.Storage
-
-from bridgedb.Bridges import BridgeRing
-from bridgedb.Bridges import FilteredBridgeSplitter
-from bridgedb.crypto import getHMAC
-from bridgedb.crypto import getHMACFunc
-from bridgedb.distribute import Distributor
-from bridgedb.filters import byFilters
-from bridgedb.filters import byIPv4
-from bridgedb.filters import byIPv6
-from bridgedb.filters import bySubring
-from bridgedb.parse import addr
-
-
-MAX_EMAIL_RATE = 3*3600
-
-class IgnoreEmail(addr.BadEmail):
-    """Raised when we get requests from this address after rate warning."""
-
-class TooSoonEmail(addr.BadEmail):
-    """Raised when we got a request from this address too recently."""
-
-class EmailRequestedHelp(Exception):
-    """Raised when a client has emailed requesting help."""
-
-class EmailRequestedKey(Exception):
-    """Raised when an incoming email requested a copy of our GnuPG keys."""
-
-
-class EmailBasedDistributor(Distributor):
-    """Object that hands out bridges based on the email address of an incoming
-    request and the current time period.
-
-    :type hashring: :class:`~bridgedb.Bridges.BridgeRing`
-    :ivar hashring: A hashring to hold all the bridges we hand out.
-    """
-
-    def __init__(self, key, domainmap, domainrules,
-                 answerParameters=None, whitelist=None):
-        """Create a bridge distributor which uses email.
-
-        :type emailHmac: callable
-        :param emailHmac: An hmac function used to order email addresses
-            within a ring. See :func:`~bridgedb.crypto.getHMACFunc`.
-        :param dict domainmap: A map from lowercase domains that we support
-            mail from to their canonical forms. See `EMAIL_DOMAIN_MAP` option
-            in `bridgedb.conf`.
-        :param domainrules: DOCDOC
-        :param answerParameters: DOCDOC
-        :type whitelist: dict or ``None``
-        :param whitelist: A dictionary that maps whitelisted email addresses
-            to GnuPG fingerprints.
-        """
-        super(EmailBasedDistributor, self).__init__(key)
-
-        key1 = getHMAC(key, "Map-Addresses-To-Ring")
-        self.emailHmac = getHMACFunc(key1, hex=False)
-
-        key2 = getHMAC(key, "Order-Bridges-In-Ring")
-        # XXXX clear the store when the period rolls over!
-        self.domainmap = domainmap
-        self.domainrules = domainrules
-        self.whitelist = whitelist or dict()
-        self.answerParameters = answerParameters
-
-        #XXX cache options not implemented
-        self.hashring = FilteredBridgeSplitter(key2, max_cached_rings=5)
-        self.name = "Email"
-
-    def bridgesPerResponse(self, hashring=None):
-        return super(EmailBasedDistributor, self).bridgesPerResponse(hashring)
-
-    def insert(self, bridge):
-        """Assign a bridge to this distributor."""
-        self.hashring.insert(bridge)
-
-    def getBridges(self, bridgeRequest, interval):
-        """Return a list of bridges to give to a user.
-
-        :type bridgeRequest: :class:`~bridgedb.email.request.EmailBridgeRequest`
-        :param bridgeRequest: A :class:`~bridgedb.bridgerequest.BridgeRequestBase`
-            with the :data:`~bridgedb.bridgerequest.BridgeRequestBase.client`
-            attribute set to a string containing the client's full, canonicalized
-            email address.
-        :param interval: The time period when we got this request. This can be
-            any string, so long as it changes with every period.
-        """
-        # All checks on the email address, such as checks for whitelisting and
-        # canonicalization of domain name, are done in
-        # :meth:`bridgedb.email.autoresponder.getMailTo` and
-        # :meth:`bridgedb.email.autoresponder.SMTPAutoresponder.runChecks`.
-        if (not bridgeRequest.client) or (bridgeRequest.client == 'default'):
-            raise addr.BadEmail(
-                ("%s distributor can't get bridges for invalid email email "
-                 " address: %s") % (self.name, bridgeRequest.client))
-
-        logging.info("Attempting to get bridges for %s..." % bridgeRequest.client)
-
-        now = time.time()
-
-        with bridgedb.Storage.getDB() as db:
-            wasWarned = db.getWarnedEmail(bridgeRequest.client)
-            lastSaw = db.getEmailTime(bridgeRequest.client)
-            if lastSaw is not None:
-                if bridgeRequest.client in self.whitelist.keys():
-                    logging.info(("Whitelisted email address %s was last seen "
-                                  "%d seconds ago.")
-                                 % (bridgeRequest.client, now - lastSaw))
-                elif (lastSaw + MAX_EMAIL_RATE) >= now:
-                    wait = (lastSaw + MAX_EMAIL_RATE) - now
-                    logging.info("Client %s must wait another %d seconds."
-                                 % (bridgeRequest.client, wait))
-                    if wasWarned:
-                        raise IgnoreEmail("Client was warned.",
-                                          bridgeRequest.client)
-                    else:
-                        logging.info("Sending duplicate request warning.")
-                        db.setWarnedEmail(bridgeRequest.client, True, now)
-                        db.commit()
-                        raise TooSoonEmail("Must wait %d seconds" % wait,
-                                           bridgeRequest.client)
-            # warning period is over
-            elif wasWarned:
-                db.setWarnedEmail(bridgeRequest.client, False)
-
-            pos = self.emailHmac("<%s>%s" % (interval, bridgeRequest.client))
-
-            ring = None
-            ruleset = frozenset(bridgeRequest.filters)
-            if ruleset in self.hashring.filterRings.keys():
-                logging.debug("Cache hit %s" % ruleset)
-                _, ring = self.hashring.filterRings[ruleset]
-            else:
-                # cache miss, add new ring
-                logging.debug("Cache miss %s" % ruleset)
-
-                # add new ring
-                key1 = getHMAC(self.key, "Order-Bridges-In-Ring")
-                ring = BridgeRing(key1, self.answerParameters)
-                self.hashring.addRing(ring, ruleset, byFilters(ruleset),
-                                      populate_from=self.hashring.bridges)
-
-            returnNum = self.bridgesPerResponse(ring)
-            result = ring.getBridges(pos, returnNum)
-
-            db.setEmailTime(bridgeRequest.client, now)
-            db.commit()
-
-        return result
-
-    def cleanDatabase(self):
-        with bridgedb.Storage.getDB() as db:
-            try:
-                db.cleanEmailedBridges(time.time() - MAX_EMAIL_RATE)
-                db.cleanWarnedEmails(time.time() - MAX_EMAIL_RATE)
-            except:
-                db.rollback()
-                raise
-            else:
-                db.commit()
-
-    def prepopulateRings(self):
-        # populate all rings (for dumping assignments and testing)
-        for filterFn in [byIPv4, byIPv6]:
-            ruleset = frozenset([filterFn])
-            key1 = getHMAC(self.key, "Order-Bridges-In-Ring")
-            ring = BridgeRing(key1, self.answerParameters)
-            self.hashring.addRing(ring, ruleset, byFilters([filterFn]),
-                                  populate_from=self.hashring.bridges)
diff --git a/lib/bridgedb/Main.py b/lib/bridgedb/Main.py
index 3a15145..b281d21 100644
--- a/lib/bridgedb/Main.py
+++ b/lib/bridgedb/Main.py
@@ -30,13 +30,13 @@ from bridgedb.bridges import ServerDescriptorDigestMismatch
 from bridgedb.bridges import ServerDescriptorWithoutNetworkstatus
 from bridgedb.bridges import Bridge
 from bridgedb.configure import loadConfig
+from bridgedb.email.distributor import EmailDistributor
 from bridgedb.https.distributor import HTTPSDistributor
 from bridgedb.parse import descriptors
 
 import bridgedb.Storage
 
 from bridgedb import Bridges
-from bridgedb import Dist
 from bridgedb.Stability import updateBridgeHistory
 
 
@@ -197,7 +197,7 @@ def createBridgeRings(cfg, proxyList, key):
     :rtype: tuple
     :returns: A BridgeSplitter hashring, an
         :class:`~bridgedb.https.distributor.HTTPSDistributor` or None, and an
-        EmailBasedDistributor or None.
+        :class:`~bridgedb.email.distributor.EmailDistributor` or None.
     """
     # Create a BridgeSplitter to assign the bridges to the different
     # distributors.
@@ -222,7 +222,7 @@ def createBridgeRings(cfg, proxyList, key):
     # As appropriate, create an email-based distributor.
     if cfg.EMAIL_DIST and cfg.EMAIL_SHARE:
         logging.debug("Setting up Email Distributor...")
-        emailDistributor = Dist.EmailBasedDistributor(
+        emailDistributor = EmailDistributor(
             crypto.getHMAC(key, "Email-Dist-Key"),
             cfg.EMAIL_DOMAIN_MAP.copy(),
             cfg.EMAIL_DOMAIN_RULES.copy(),
@@ -333,7 +333,8 @@ def run(options, reactor=reactor):
             known open proxies.
         :ivar ipDistributor: A
             :class:`~bridgedb.https.distributor.HTTPSDistributor`.
-        :ivar emailDistributor: A :class:`Dist.EmailBasedDistributor`.
+        :ivar emailDistributor: A
+            :class:`~bridgedb.email.distributor.EmailDistributor`.
         :ivar dict tasks: A dictionary of ``{name: task}``, where name is a
             string to associate with the ``task``, and ``task`` is some
             scheduled event, repetitive or otherwise, for the :class:`reactor
diff --git a/lib/bridgedb/email/autoresponder.py b/lib/bridgedb/email/autoresponder.py
index b98717f..ad63bfd 100644
--- a/lib/bridgedb/email/autoresponder.py
+++ b/lib/bridgedb/email/autoresponder.py
@@ -47,13 +47,13 @@ from twisted.python import failure
 
 from bridgedb import safelog
 from bridgedb.crypto import NEW_BUFFER_INTERFACE
-from bridgedb.Dist import EmailRequestedHelp
-from bridgedb.Dist import EmailRequestedKey
-from bridgedb.Dist import TooSoonEmail
-from bridgedb.Dist import IgnoreEmail
 from bridgedb.email import dkim
 from bridgedb.email import request
 from bridgedb.email import templates
+from bridgedb.email.distributor import EmailRequestedHelp
+from bridgedb.email.distributor import EmailRequestedKey
+from bridgedb.email.distributor import TooSoonEmail
+from bridgedb.email.distributor import IgnoreEmail
 from bridgedb.parse import addr
 from bridgedb.parse.addr import canonicalizeEmailDomain
 from bridgedb.util import levenshteinDistance
diff --git a/lib/bridgedb/email/distributor.py b/lib/bridgedb/email/distributor.py
new file mode 100644
index 0000000..b73c082
--- /dev/null
+++ b/lib/bridgedb/email/distributor.py
@@ -0,0 +1,209 @@
+# -*- coding: utf-8 ; test-case-name: bridgedb.test.test_email_distributor -*-
+#
+# This file is part of BridgeDB, a Tor bridge distribution system.
+#
+# :authors: Nick Mathewson
+#           Isis Lovecruft 0xA3ADB67A2CDB8B35 <isis at torproject.org>
+#           Matthew Finkel 0x017DD169EA793BE2 <sysrqb at torproject.org>
+# :copyright: (c) 2013-2015, Isis Lovecruft
+#             (c) 2013-2015, Matthew Finkel
+#             (c) 2007-2015, The Tor Project, Inc.
+# :license: see LICENSE for licensing information
+
+"""A :class:`~bridgedb.distribute.Distributor` which hands out
+:class:`bridges <bridgedb.bridges.Bridge>` to clients via an email interface.
+"""
+
+import logging
+import time
+
+import bridgedb.Storage
+
+from bridgedb.Bridges import BridgeRing
+from bridgedb.Bridges import FilteredBridgeSplitter
+from bridgedb.crypto import getHMAC
+from bridgedb.crypto import getHMACFunc
+from bridgedb.distribute import Distributor
+from bridgedb.filters import byFilters
+from bridgedb.filters import byIPv4
+from bridgedb.filters import byIPv6
+from bridgedb.filters import bySubring
+from bridgedb.parse import addr
+
+
+#: The minimum amount of time (in seconds) which must pass before a client who
+#: has previously been given an email response must wait before being eligible
+#: to receive another response.
+MAX_EMAIL_RATE = 3 * 3600
+
+
+class IgnoreEmail(addr.BadEmail):
+    """Raised when we get requests from this address after rate warning."""
+
+
+class TooSoonEmail(addr.BadEmail):
+    """Raised when we got a request from this address too recently."""
+
+
+class EmailRequestedHelp(Exception):
+    """Raised when a client has emailed requesting help."""
+
+
+class EmailRequestedKey(Exception):
+    """Raised when an incoming email requested a copy of our GnuPG keys."""
+
+
+class EmailDistributor(Distributor):
+    """Object that hands out bridges based on the email address of an incoming
+    request and the current time period.
+
+    :type hashring: :class:`~bridgedb.Bridges.BridgeRing`
+    :ivar hashring: A hashring to hold all the bridges we hand out.
+    """
+
+    #: The minimum amount of time (in seconds) which must pass before a client
+    #: who has previously been given an email response must wait before being
+    #: eligible to receive another response.
+    emailRateMax = MAX_EMAIL_RATE
+
+    def __init__(self, key, domainmap, domainrules,
+                 answerParameters=None, whitelist=None):
+        """Create a bridge distributor which uses email.
+
+        :type emailHmac: callable
+        :param emailHmac: An hmac function used to order email addresses
+            within a ring. See :func:`~bridgedb.crypto.getHMACFunc`.
+        :param dict domainmap: A map from lowercase domains that we support
+            mail from to their canonical forms. See `EMAIL_DOMAIN_MAP` option
+            in `bridgedb.conf`.
+        :param domainrules: DOCDOC
+        :param answerParameters: DOCDOC
+        :type whitelist: dict or ``None``
+        :param whitelist: A dictionary that maps whitelisted email addresses
+            to GnuPG fingerprints.
+        """
+        super(EmailDistributor, self).__init__(key)
+
+        self.domainmap = domainmap
+        self.domainrules = domainrules
+        self.whitelist = whitelist or dict()
+        self.answerParameters = answerParameters
+
+        key1 = getHMAC(key, "Map-Addresses-To-Ring")
+        key2 = getHMAC(key, "Order-Bridges-In-Ring")
+
+        self.emailHmac = getHMACFunc(key1, hex=False)
+        #XXX cache options not implemented
+        self.hashring = FilteredBridgeSplitter(key2, max_cached_rings=5)
+
+        self.name = "Email"
+
+    def bridgesPerResponse(self, hashring=None):
+        return super(EmailDistributor, self).bridgesPerResponse(hashring)
+
+    def getBridges(self, bridgeRequest, interval):
+        """Return a list of bridges to give to a user.
+
+        .. hint:: All checks on the email address (which should be stored in
+            the ``bridgeRequest.client`` attribute), such as checks for
+            whitelisting and canonicalization of domain name, are done in
+            :meth:`bridgedb.email.autoresponder.getMailTo` and
+            :meth:`bridgedb.email.autoresponder.SMTPAutoresponder.runChecks`.
+
+        :type bridgeRequest:
+            :class:`~bridgedb.email.request.EmailBridgeRequest`
+        :param bridgeRequest: A
+            :class:`~bridgedb.bridgerequest.BridgeRequestBase` with the
+            :data:`~bridgedb.bridgerequest.BridgeRequestBase.client` attribute
+            set to a string containing the client's full, canonicalized email
+            address.
+        :param interval: The time period when we got this request. This can be
+            any string, so long as it changes with every period.
+        """
+        if (not bridgeRequest.client) or (bridgeRequest.client == 'default'):
+            raise addr.BadEmail(
+                ("%s distributor can't get bridges for invalid email address: "
+                 "%s") % (self.name, bridgeRequest.client), bridgeRequest.client)
+
+        logging.info("Attempting to get bridges for %s..." % bridgeRequest.client)
+
+        now = time.time()
+
+        with bridgedb.Storage.getDB() as db:
+            wasWarned = db.getWarnedEmail(bridgeRequest.client)
+            lastSaw = db.getEmailTime(bridgeRequest.client)
+            if lastSaw is not None:
+                if bridgeRequest.client in self.whitelist:
+                    logging.info(
+                        "Whitelisted address %s was last seen %d seconds ago."
+                        % (bridgeRequest.client, now - lastSaw))
+                elif (lastSaw + self.emailRateMax) >= now:
+                    wait = (lastSaw + self.emailRateMax) - now
+                    logging.info("Client %s must wait another %d seconds."
+                                 % (bridgeRequest.client, wait))
+                    if wasWarned:
+                        raise IgnoreEmail(
+                            "Client %s was warned." % bridgeRequest.client,
+                            bridgeRequest.client)
+                    else:
+                        logging.info("Sending duplicate request warning.")
+                        db.setWarnedEmail(bridgeRequest.client, True, now)
+                        db.commit()
+                        raise TooSoonEmail("Must wait %d seconds" % wait,
+                                           bridgeRequest.client)
+            # warning period is over
+            elif wasWarned:
+                db.setWarnedEmail(bridgeRequest.client, False)
+
+            pos = self.emailHmac("<%s>%s" % (interval, bridgeRequest.client))
+
+            ring = None
+            filtres = frozenset(bridgeRequest.filters)
+            if filtres in self.hashring.filterRings:
+                logging.debug("Cache hit %s" % filtres)
+                _, ring = self.hashring.filterRings[filtres]
+            else:
+                logging.debug("Cache miss %s" % filtres)
+                key = getHMAC(self.key, "Order-Bridges-In-Ring")
+                ring = BridgeRing(key, self.answerParameters)
+                self.hashring.addRing(ring, filtres, byFilters(filtres),
+                                      populate_from=self.hashring.bridges)
+
+            returnNum = self.bridgesPerResponse(ring)
+            result = ring.getBridges(pos, returnNum)
+
+            db.setEmailTime(bridgeRequest.client, now)
+            db.commit()
+
+        return result
+
+    def cleanDatabase(self):
+        """Clear all emailed response and warning times from the database."""
+        logging.info(("Cleaning all response and warning times for the %s "
+                      "distributor from the database...") % self.name)
+        with bridgedb.Storage.getDB() as db:
+            try:
+                db.cleanEmailedBridges(time.time() - self.emailRateMax)
+                db.cleanWarnedEmails(time.time() - self.emailRateMax)
+            except:
+                db.rollback()
+                raise
+            else:
+                db.commit()
+
+    def prepopulateRings(self):
+        """Prepopulate this distributor's hashrings and subhashrings with
+        bridges.
+        """
+        logging.info("Prepopulating %s distributor hashrings..." % self.name)
+
+        for filterFn in [byIPv4, byIPv6]:
+            ruleset = frozenset([filterFn])
+            key = getHMAC(self.key, "Order-Bridges-In-Ring")
+            ring = BridgeRing(key, self.answerParameters)
+            self.hashring.addRing(ring, ruleset, byFilters([filterFn]),
+                                  populate_from=self.hashring.bridges)
+
+        # Since prepopulateRings is called every half hour when the bridge
+        # descriptors are re-parsed, we should clean the database then.
+        self.cleanDatabase()
diff --git a/lib/bridgedb/email/request.py b/lib/bridgedb/email/request.py
index 32446ac..50fd32c 100644
--- a/lib/bridgedb/email/request.py
+++ b/lib/bridgedb/email/request.py
@@ -40,8 +40,8 @@ import logging
 import re
 
 from bridgedb import bridgerequest
-from bridgedb.Dist import EmailRequestedHelp
-from bridgedb.Dist import EmailRequestedKey
+from bridgedb.email.distributor import EmailRequestedHelp
+from bridgedb.email.distributor import EmailRequestedKey
 
 
 #: A regular expression for matching the Pluggable Transport method TYPE in
@@ -105,7 +105,7 @@ class EmailBridgeRequest(bridgerequest.BridgeRequestBase):
 
     def __init__(self):
         """Process a new bridge request received through the
-        :class:`~bridgedb.Dist.EmailBasedDistributor`.
+        :class:`~bridgedb.email.distributor.EmailDistributor`.
         """
         super(EmailBridgeRequest, self).__init__()
         self._wantsKey = False
diff --git a/lib/bridgedb/email/server.py b/lib/bridgedb/email/server.py
index 8034d35..736c3f6 100644
--- a/lib/bridgedb/email/server.py
+++ b/lib/bridgedb/email/server.py
@@ -103,7 +103,7 @@ class MailServerContext(object):
         """Create a context for storing configs for email bridge distribution.
 
         :type config: :class:`bridgedb.persistent.Conf`
-        :type distributor: :class:`bridgedb.Dist.EmailBasedDistributor`
+        :type distributor: :class:`~bridgedb.email.distributor.EmailDistributor`
         :param distributor: The distributor will handle getting the correct
             bridges (or none) for a client for us.
         :type schedule: :class:`bridgedb.schedule.ScheduledInterval`
@@ -466,7 +466,7 @@ def addServer(config, distributor):
 
     :type config: :class:`bridgedb.configure.Conf`
     :param config: A configuration object.
-    :type distributor: :class:`bridgedb.Dist.EmailBasedDistributor`
+    :type distributor: :class:`bridgedb.email.distributor.EmailDistributor`
     :param dist: A distributor which will handle database interactions, and
         will decide which bridges to give to who and when.
     """
diff --git a/lib/bridgedb/email/templates.py b/lib/bridgedb/email/templates.py
index 4db4e3c..6998716 100644
--- a/lib/bridgedb/email/templates.py
+++ b/lib/bridgedb/email/templates.py
@@ -30,7 +30,7 @@ import os
 from datetime import datetime
 
 from bridgedb import strings
-from bridgedb.Dist import MAX_EMAIL_RATE
+from bridgedb.email.distributor import MAX_EMAIL_RATE
 
 
 def addCommands(template):
diff --git a/lib/bridgedb/parse/addr.py b/lib/bridgedb/parse/addr.py
index b3ad680..cd512d1 100644
--- a/lib/bridgedb/parse/addr.py
+++ b/lib/bridgedb/parse/addr.py
@@ -217,7 +217,7 @@ def canonicalizeEmailDomain(domain, domainmap):
     :param str domain: The domain portion of an email address to validate. It
         will be checked that it is one of the domains allowed to email
         requests for bridges to the
-        :class:`~bridgedb.Dist.EmailBasedDistributor`.
+        :class:`~bridgedb.email.distributor.EmailDistributor`.
     :param dict domainmap: A map of permitted alternate domains (in lowercase)
         to their canonical domain names (in lowercase). This can be configured
         with the ``EMAIL_DOMAIN_MAP`` option in ``bridgedb.conf``, for
@@ -441,8 +441,9 @@ def normalizeEmail(emailaddr, domainmap, domainrules, ignorePlus=True):
 
     The email address, **emailaddr**, will be parsed and validated, and then
     checked that it originated from one of the domains allowed to email
-    requests for bridges to the :class:`~bridgedb.Dist.EmailBasedDistributor`
-    via the :func:`canonicaliseEmailDomain` function.
+    requests for bridges to the
+    :class:`~bridgedb.email.distributor.EmailDistributor` via the
+    :func:`canonicaliseEmailDomain` function.
 
     :param str emailaddr: An email address to normalise.
     :param dict domainmap: A map of permitted alternate domains (in lowercase)
diff --git a/lib/bridgedb/persistent.py b/lib/bridgedb/persistent.py
index 0853c1e..22673dd 100644
--- a/lib/bridgedb/persistent.py
+++ b/lib/bridgedb/persistent.py
@@ -24,8 +24,8 @@ from twisted.python.reflect import safe_repr
 from twisted.spread import jelly
 
 from bridgedb import Bridges
-from bridgedb import Dist
 from bridgedb import filters
+from bridgedb.email import distributor as emailDistributor
 from bridgedb.https import distributor as httpsDistributor
 from bridgedb.configure import Conf
 #from bridgedb.proxy import ProxySet
@@ -35,7 +35,7 @@ _state = None
 #: Types and classes which are allowed to be jellied:
 _security = jelly.SecurityOptions()
 #_security.allowInstancesOf(ProxySet)
-_security.allowModules(filters, Bridges, Dist, httpsDistributor)
+_security.allowModules(filters, Bridges, emailDistributor, httpsDistributor)
 
 
 class MissingState(Exception):
diff --git a/lib/bridgedb/test/email_helpers.py b/lib/bridgedb/test/email_helpers.py
index 20aee57..14c86f4 100644
--- a/lib/bridgedb/test/email_helpers.py
+++ b/lib/bridgedb/test/email_helpers.py
@@ -13,9 +13,9 @@
 
 import io
 
-from bridgedb.Dist import IgnoreEmail
-from bridgedb.Dist import TooSoonEmail
 from bridgedb.persistent import Conf
+from bridgedb.email.distributor import IgnoreEmail
+from bridgedb.email.distributor import TooSoonEmail
 from bridgedb.email.server import MailServerContext
 from bridgedb.schedule import Unscheduled
 from bridgedb.test import util
@@ -120,8 +120,8 @@ def _createMailServerContext(config=None, distributor=None):
 
 
 class DummyEmailDistributor(object):
-    """A mocked :class:`bridgedb.Dist.EmailBasedDistributor` which is used to
-    test :class:`bridgedb.EmailServer`.
+    """A mocked :class:`bridgedb.email.distributor.EmailDistributor` which is used
+    to test :class:`bridgedb.EmailServer`.
     """
 
     _bridgesPerResponseMin = 3
@@ -144,9 +144,9 @@ class DummyEmailDistributor(object):
 
 
 class DummyEmailDistributorWithState(DummyEmailDistributor):
-    """A mocked :class:`bridgedb.Dist.EmailBasedDistributor` which raises
-    :exc:`bridgedb.Dist.TooSoonEmail` on the second email and
-    :exc:`bridgedb.Dist.IgnoreEmail` on the third.
+    """A mocked :class:`bridgedb.email.distributor.EmailDistributor` which raises
+    :exc:`bridgedb.email.distributor.TooSoonEmail` on the second email and
+    :exc:`bridgedb.email.distributor.IgnoreEmail` on the third.
 
     Note that the state tracking is done in a really dumb way. For example, we
     currently don't consider requests for help text or GnuPG keys to be a
diff --git a/lib/bridgedb/test/legacy_Tests.py b/lib/bridgedb/test/legacy_Tests.py
index c0a9ff0..40f7ae4 100644
--- a/lib/bridgedb/test/legacy_Tests.py
+++ b/lib/bridgedb/test/legacy_Tests.py
@@ -19,7 +19,6 @@ from datetime import datetime
 
 import bridgedb.Bridges
 import bridgedb.Main
-import bridgedb.Dist
 import bridgedb.schedule
 import bridgedb.Storage
 import re
@@ -27,6 +26,9 @@ import ipaddr
 
 from bridgedb.Stability import BridgeHistory
 
+from bridgedb.email.distributor import EmailDistributor
+from bridgedb.email.distributor import IgnoreEmail
+from bridgedb.email.distributor import TooSoonEmail
 from bridgedb.parse import addr
 from bridgedb.test.util import bracketIPv6
 from bridgedb.test.util import randomIP
@@ -94,42 +96,6 @@ def fakeBridge6(orport=8080, running=True, stable=True, or_addresses=False,
     return b
 
 
-class EmailBridgeDistTests(unittest.TestCase):
-    def setUp(self):
-        self.fd, self.fname = tempfile.mkstemp()
-        self.db = bridgedb.Storage.Database(self.fname)
-        bridgedb.Storage.setDB(self.db)
-        self.cur = self.db._conn.cursor()
-
-    def tearDown(self):
-        self.db.close()
-        os.close(self.fd)
-        os.unlink(self.fname)
-
-    def testEmailRateLimit(self):
-        db = self.db
-        EMAIL_DOMAIN_MAP = {'example.com':'example.com'}
-        d = bridgedb.Dist.EmailBasedDistributor(
-                "Foo",
-                {'example.com': 'example.com',
-                    'dkim.example.com': 'dkim.example.com'},
-                {'example.com': [], 'dkim.example.com': ['dkim']})
-        for _ in xrange(256):
-            d.insert(fakeBridge())
-        d.getBridges('abc at example.com', 1)
-        self.assertRaises(bridgedb.Dist.TooSoonEmail,
-                d.getBridges, 'abc at example.com', 1)
-        self.assertRaises(bridgedb.Dist.IgnoreEmail,
-                d.getBridges, 'abc at example.com', 1)
-
-    def testUnsupportedDomain(self):
-        db = self.db
-        self.assertRaises(bridgedb.parse.addr.UnsupportedDomain,
-                          bridgedb.parse.addr.normalizeEmail,
-                          'bad at email.com',
-                          {'example.com':'example.com'},
-                          {'example.com':[]})
-
 
 class SQLStorageTests(unittest.TestCase):
     def setUp(self):
@@ -358,8 +324,7 @@ class BridgeStabilityTests(unittest.TestCase):
 def testSuite():
     suite = unittest.TestSuite()
     loader = unittest.TestLoader()
-
-    for klass in [SQLStorageTests, EmailBridgeDistTests, BridgeStabilityTests]:
+    for klass in [SQLStorageTests, BridgeStabilityTests]:
         suite.addTest(loader.loadTestsFromTestCase(klass))
     return suite
 
diff --git a/lib/bridgedb/test/test_email_autoresponder.py b/lib/bridgedb/test/test_email_autoresponder.py
index 2895802..98302ca 100644
--- a/lib/bridgedb/test/test_email_autoresponder.py
+++ b/lib/bridgedb/test/test_email_autoresponder.py
@@ -25,7 +25,7 @@ from twisted.test import proto_helpers
 
 from bridgedb.email import autoresponder
 from bridgedb.email.server import SMTPMessage
-from bridgedb.Dist import TooSoonEmail
+from bridgedb.email.distributor import TooSoonEmail
 from bridgedb.test.email_helpers import _createConfig
 from bridgedb.test.email_helpers import _createMailServerContext
 from bridgedb.test.email_helpers import DummyEmailDistributorWithState
diff --git a/lib/bridgedb/test/test_email_distributor.py b/lib/bridgedb/test/test_email_distributor.py
new file mode 100644
index 0000000..96eb306
--- /dev/null
+++ b/lib/bridgedb/test/test_email_distributor.py
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of BridgeDB, a Tor bridge distribution system.
+#
+# :authors: Isis Lovecruft 0xA3ADB67A2CDB8B35 <isis at torproject.org>
+# :copyright: (c) 2013-2015 Isis Lovecruft
+#             (c) 2007-2015, The Tor Project, Inc.
+# :license: see included LICENSE for information
+
+"""Tests for :mod:`bridgedb.email.distributor`."""
+
+from __future__ import print_function
+
+import logging
+import tempfile
+import os
+
+from twisted.trial import unittest
+
+import bridgedb.Storage
+
+from bridgedb.bridges import Bridge
+from bridgedb.email.distributor import EmailDistributor
+from bridgedb.email.distributor import IgnoreEmail
+from bridgedb.email.distributor import TooSoonEmail
+from bridgedb.email.request import EmailBridgeRequest
+from bridgedb.parse.addr import UnsupportedDomain
+from bridgedb.parse.addr import normalizeEmail
+from bridgedb.test.util import generateFakeBridges
+
+logging.disable(50)
+
+
+BRIDGES = generateFakeBridges()
+
+
+class EmailDistributorTests(unittest.TestCase):
+    """Tests for :class:`bridgedb.email.distributor.EmailDistributor`."""
+
+    def setUp(self):
+        self.fd, self.fname = tempfile.mkstemp()
+        bridgedb.Storage.initializeDBLock()
+        self.db = bridgedb.Storage.openDatabase(self.fname)
+        bridgedb.Storage.setDBFilename(self.fname)
+        self.cur = self.db.cursor()
+        self.db.close()
+
+        self.bridges = BRIDGES
+        self.key = 'aQpeOFIj8q20s98awfoiq23rpOIjFaqpEWFoij1X'
+        self.domainmap = {
+            'example.com':      'example.com',
+            'dkim.example.com': 'dkim.example.com',
+        }
+        self.domainrules = {
+            'example.com':      ['ignore_dots'],
+            'dkim.example.com': ['dkim', 'ignore_dots']
+        }
+
+    def tearDown(self):
+        self.db.close()
+        os.close(self.fd)
+        os.unlink(self.fname)
+
+    def makeClientRequest(self, clientEmailAddress):
+        bridgeRequest = EmailBridgeRequest()
+        bridgeRequest.client = clientEmailAddress
+        bridgeRequest.isValid(True)
+        bridgeRequest.generateFilters()
+        return bridgeRequest
+
+    def test_EmailDistributor_rate_limit(self):
+        """A client's first email should return bridges.  The second should
+        return a warning, and the third should receive no response.
+        """
+        dist = EmailDistributor(self.key, self.domainmap, self.domainrules)
+        [dist.hashring.insert(bridge) for bridge in self.bridges]
+
+        bridgeRequest = self.makeClientRequest('abc at example.com')
+
+        # The first request should get a response with bridges
+        bridges = dist.getBridges(bridgeRequest, 1)
+        self.assertGreater(len(bridges), 0)
+        [self.assertIsInstance(b, Bridge) for b in bridges]
+        self.assertEqual(len(bridges), 3)
+
+        # The second gets a warning, and the third is ignored
+        self.assertRaises(TooSoonEmail, dist.getBridges, bridgeRequest, 1)
+        self.assertRaises(IgnoreEmail,  dist.getBridges, bridgeRequest, 1)
+
+    def test_EmailDistributor_unsupported_domain(self):
+        """An unsupported domain should raise an UnsupportedDomain exception."""
+        self.assertRaises(UnsupportedDomain, normalizeEmail,
+                          'bad at email.com', self.domainmap, self.domainrules)
diff --git a/lib/bridgedb/test/test_email_server.py b/lib/bridgedb/test/test_email_server.py
index 561c14c..9c4fabb 100644
--- a/lib/bridgedb/test/test_email_server.py
+++ b/lib/bridgedb/test/test_email_server.py
@@ -31,9 +31,9 @@ from twisted.trial import unittest
 
 from zope.interface import implementedBy
 
-from bridgedb.Dist import EmailBasedDistributor
-from bridgedb.Dist import TooSoonEmail
 from bridgedb.email import server
+from bridgedb.email.distributor import EmailDistributor
+from bridgedb.email.distributor import TooSoonEmail
 from bridgedb.parse.addr import BadEmail
 from bridgedb.schedule import Unscheduled
 from bridgedb.test import util
@@ -504,7 +504,7 @@ class EmailServerServiceTests(SMTPTestCaseMixin, unittest.TestCase):
     """Unittests for :func:`bridgedb.email.server.addServer`."""
 
     def setUp(self):
-        """Create a server.MailServerContext and EmailBasedDistributor."""
+        """Create a MailServerContext and EmailDistributor."""
         self.config = _createConfig()
         self.context = _createMailServerContext(self.config)
         self.smtpFromAddr = self.context.smtpFromAddr  # 'bridges at localhost'





More information about the tor-commits mailing list