First version of a CatchallAlias class v0.6.x
authormartin f. krafft <madduck@madduck.net>
Mon, 09 Apr 2012 18:10:56 +0200
branchv0.6.x
changeset 504 f180ead60568
parent 503 492c179094c9
child 505 3da8c919584f
First version of a CatchallAlias class I based the CatchallAlias class in catchall.py heavily on the Alias class, but by copy, not deriving. The two are functionally related, but the implementations are too different because CatchallAliases have no localpart.
VirtualMailManager/catchall.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/catchall.py	Mon Apr 09 18:10:56 2012 +0200
@@ -0,0 +1,171 @@
+# -*- coding: UTF-8 -*-
+# Copyright (c) 2012 martin f. krafft
+# See COPYING for distribution information.
+"""
+    VirtualMailManager.catchall
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Virtual Mail Manager's CatchallAlias class to manage domain catch-all
+    aliases.
+
+    This is heavily based on (more or less a copy of) the Alias class, because
+    fundamentally, catchall aliases are aliases, but without a localpart.
+    While Alias could potentially derive from CatchallAlias to reuse some of
+    the functionality, it's probably not worth it. I found no sensible way to
+    derive CatchallAlias from Alias, or at least none that would harness the
+    powers of polymorphism.
+
+    Yet, we reuse the AliasError exception class, which makes sense.
+"""
+
+from VirtualMailManager.domain import get_gid
+from VirtualMailManager.emailaddress import \
+     EmailAddress, DestinationEmailAddress as DestAddr
+from VirtualMailManager.errors import AliasError as AErr
+from VirtualMailManager.ext.postconf import Postconf
+from VirtualMailManager.pycompat import all
+from VirtualMailManager.constants import \
+     ALIAS_EXCEEDS_EXPANSION_LIMIT, NO_SUCH_ALIAS, NO_SUCH_DOMAIN
+
+
+_ = lambda msg: msg
+cfg_dget = lambda option: None
+
+
+class CatchallAlias(object):
+    """Class to manage domain catch-all aliases."""
+    __slots__ = ('_domain', '_dests', '_gid', '_dbh')
+
+    def __init__(self, dbh, domain):
+        self._domain = domain
+        self._dbh = dbh
+        self._gid = get_gid(self._dbh, self._addr.domain)
+        if not self._gid:
+            raise AErr(_(u"The domain '%s' does not exist.") %
+                       self._addr.domainname, NO_SUCH_DOMAIN)
+        self._dests = []
+
+        self._load_dests()
+
+    def _load_dests(self):
+        """Loads all known destination addresses into the _dests list."""
+        dbc = self._dbh.cursor()
+        dbc.execute('SELECT destination FROM catchall WHERE gid = %s',
+                    self._gid)
+        dests = dbc.fetchall()
+        if dbc.rowcount > 0:
+            self._dests.extend(DestAddr(dest[0], self._dbh) for dest in dests)
+        dbc.close()
+
+    def _check_expansion(self, count_new):
+        """Checks the current expansion limit of the alias."""
+        postconf = Postconf(cfg_dget('bin.postconf'))
+        limit = long(postconf.read('virtual_alias_expansion_limit'))
+        dcount = len(self._dests)
+        failed = False
+        if dcount == limit or dcount + count_new > limit:
+            failed = True
+            errmsg = _(
+u"""Cannot add %(count_new)i new destination(s) to catchall alias for
+domain '%(domain)s'. Currently this alias expands into %(count)i/%(limit)i
+recipients. %(count_new)i additional destination(s) will render this alias
+unusable.
+Hint: Increase Postfix' virtual_alias_expansion_limit""")
+        elif dcount > limit:
+            failed = True
+            errmsg = _(
+u"""Cannot add %(count_new)i new destination(s) to catchall alias for
+domain '%(domain)s'. This alias already exceeds its expansion limit (%(count)i/%(limit)i).
+So its unusable, all messages addressed to this alias will be bounced.
+Hint: Delete some destination addresses.""")
+        if failed:
+            raise AErr(errmsg % {'domain': self._domain, 'count': dcount,
+                                 'limit': limit, 'count_new': count_new},
+                       ALIAS_EXCEEDS_EXPANSION_LIMIT)
+
+    def _delete(self, destination=None):
+        """Deletes a destination from the catchall alias, if ``destination``
+        is not ``None``.  If ``destination`` is None, the catchall alias with
+        all its destination addresses will be deleted.
+
+        """
+        dbc = self._dbh.cursor()
+        if not destination:
+            dbc.execute('DELETE FROM catchall WHERE gid = %s', self._gid)
+        else:
+            dbc.execute('DELETE FROM catchall WHERE gid = %s '
+                        'AND destination = %s', (self._gid, str(destination)))
+        if dbc.rowcount > 0:
+            self._dbh.commit()
+        dbc.close()
+
+    def __len__(self):
+        """Returns the number of destinations of the catchall alias."""
+        return len(self._dests)
+
+    @property
+    def domain(self):
+        """The Alias' domain."""
+        return self._domain
+
+    def add_destinations(self, destinations, warnings=None):
+        """Adds the `EmailAddress`es from *destinations* list to the
+        destinations of the catchall alias.
+
+        Destinations, that are already assigned to the alias, will be
+        removed from *destinations*.  When done, this method will return
+        a set with all destinations, that were saved in the database.
+        """
+        destinations = set(destinations)
+        assert destinations and \
+                all(isinstance(dest, EmailAddress) for dest in destinations)
+        if not warnings is None:
+            assert isinstance(warnings, list)
+        duplicates = destinations.intersection(set(self._dests))
+        if duplicates:
+            destinations.difference_update(set(self._dests))
+            if not warnings is None:
+                warnings.extend(duplicates)
+        if not destinations:
+            return destinations
+        self._check_expansion(len(destinations))
+        dbc = self._dbh.cursor()
+        dbc.executemany("INSERT INTO catchall (gid, destination) "
+                        "VALUES (%d, %%s)" % self._gid,
+                        ((str(destination),) for destination in destinations))
+        self._dbh.commit()
+        dbc.close()
+        self._dests.extend(destinations)
+        return destinations
+
+    def del_destination(self, destination):
+        """Deletes the specified ``destination`` address from the catchall
+        alias."""
+        assert isinstance(destination, EmailAddress)
+        if not self._dests:
+            raise AErr(_(u"There are no catchall aliases defined for "
+                         u"domain '%s'.") % self._domain, NO_SUCH_ALIAS)
+        if not destination in self._dests:
+            raise AErr(_(u"The address '%(addr)s' is not a destination of "
+                         u"the catchall alias for domain '%(domain)s'.")
+                       % {'addr': destination, 'domain': self._domain},
+                       NO_SUCH_ALIAS)
+        self._delete(destination)
+        self._dests.remove(destination)
+
+    def get_destinations(self):
+        """Returns an iterator for all destinations of the catchall alias."""
+        if not self._dests:
+            raise AErr(_(u"There are no catchall aliases defined for "
+                         u"domain '%s'.") % self._domain, NO_SUCH_ALIAS)
+        return iter(self._dests)
+
+    def delete(self):
+        """Deletes all catchall destinations for the domain."""
+        if not self._dests:
+            raise AErr(_(u"There are no catchall aliases defined for "
+                         u"domain '%s'.") % self._domain, NO_SUCH_ALIAS)
+        self._delete()
+        del self._dests[:]
+
+del _, cfg_dget