VMM/{Alias,Handler}: reworked Alias class, adjusted Handler class. v0.6.x
authorPascal Volk <neverseen@users.sourceforge.net>
Wed, 10 Feb 2010 02:13:35 +0000
branchv0.6.x
changeset 196 65a3163bd113
parent 195 05dd49fc3ea1
child 197 d2712e8c724e
VMM/{Alias,Handler}: reworked Alias class, adjusted Handler class. Handler: - attribute _dbh is no longer private, the VMM/cli/Handler uses it also. - adjusted to changes in Alias and EmailAddress classes.
VirtualMailManager/Alias.py
VirtualMailManager/Handler.py
--- a/VirtualMailManager/Alias.py	Tue Feb 09 22:14:08 2010 +0000
+++ b/VirtualMailManager/Alias.py	Wed Feb 10 02:13:35 2010 +0000
@@ -2,121 +2,153 @@
 # Copyright (c) 2007 - 2010, Pascal Volk
 # See COPYING for distribution information.
 
-"""Virtual Mail Manager's Alias class to manage e-mail aliases."""
+"""
+    VirtualMailManager.Alias
 
-import VirtualMailManager.constants.ERROR as ERR
+    Virtual Mail Manager's Alias class to manage e-mail aliases.
+"""
+
 from VirtualMailManager.Domain import Domain
 from VirtualMailManager.EmailAddress import EmailAddress
 from VirtualMailManager.Exceptions import VMMAliasException as VMMAE
-import VirtualMailManager as VMM
+from VirtualMailManager.constants.ERROR import ALIAS_ADDR_DEST_IDENTICAL, \
+     ALIAS_EXCEEDS_EXPANSION_LIMIT, ALIAS_EXISTS, NO_SUCH_ALIAS, NO_SUCH_DOMAIN
+
+
+_ = lambda msg: msg
+
 
 class Alias(object):
     """Class to manage e-mail aliases."""
-    __slots__ = ('_addr', '_dest', '_gid', '_isNew', '_dbh')
-    def __init__(self, dbh, address, destination=None):
+    __slots__ = ('_addr', '_dests', '_gid', '_dbh')
+
+    def __init__(self, dbh, address):
         if isinstance(address, EmailAddress):
             self._addr = address
         else:
             raise TypeError("Argument 'address' is not an EmailAddress")
-        if destination is None:
-            self._dest = None
-        elif isinstance(destination, EmailAddress):
-            self._dest = destination
-        else:
-            raise TypeError("Argument 'destination' is not an EmailAddress")
-        if address == destination:
-            raise VMMAE(_(u"Address and destination are identical."),
-                ERR.ALIAS_ADDR_DEST_IDENTICAL)
         self._dbh = dbh
         self._gid = 0
-        self._isNew = False
-        self._setAddr()
-        if not self._dest is None:
-            self._exists()
-        if VMM.VirtualMailManager.accountExists(self._dbh, self._addr):
-            raise VMMAE(_(u"There is already an account with address “%s”.") %\
-                    self._addr, ERR.ACCOUNT_EXISTS)
-        if VMM.VirtualMailManager.relocatedExists(self._dbh, self._addr):
-            raise VMMAE(
-              _(u"There is already a relocated user with the address “%s”.") %\
-                    self._addr, ERR.RELOCATED_EXISTS)
+        self._dests = []
 
-    def _exists(self):
-        dbc = self._dbh.cursor()
-        dbc.execute("SELECT gid FROM alias WHERE gid=%s AND address=%s\
- AND destination=%s", self._gid, self._addr._localpart, str(self._dest))
-        gid = dbc.fetchone()
-        dbc.close()
-        if gid is None:
-            self._isNew = True
+        self.__set_gid()
+        self.__load_dests()
 
-    def _setAddr(self):
-        dom = Domain(self._dbh, self._addr._domainname)
+    def __set_gid(self):
+        """Sets the alias' _gid based on its _addr.domainname."""
+        dom = Domain(self._dbh, self._addr.domainname)
         self._gid = dom.getID()
         if self._gid == 0:
-            raise VMMAE(_(u"The domain “%s” doesn't exist.") %\
-                    self._addr._domainname, ERR.NO_SUCH_DOMAIN)
+            raise VMMAE(_(u"The domain “%s” doesn't exist.") %
+                        self._addr.domainname, NO_SUCH_DOMAIN)
 
-    def _checkExpansion(self, limit):
+    def __load_dests(self):
+        """Loads all known destination addresses into the _dests list."""
         dbc = self._dbh.cursor()
-        dbc.execute('SELECT count(gid) FROM alias where gid=%s AND address=%s',
-                self._gid, self._addr._localpart)
-        curEx = dbc.fetchone()[0]
+        dbc.execute(
+                'SELECT destination FROM alias WHERE gid=%s AND address=%s',
+                    self._gid, self._addr.localpart)
+        dests = iter(dbc.fetchall())
+        if dbc.rowcount > 0:
+            dest_add = self._dests.append
+            for dest in dests:
+                dest_add(EmailAddress(dest[0]))
         dbc.close()
-        if curEx == limit:
-            errmsg = _(u"""Can't add new destination to alias “%(address)s”.
-Currently this alias expands into %(count)i recipients.
+
+    def __check_expansion(self, limit):
+        """Checks the current expansion limit of the alias."""
+        dcount = len(self._dests)
+        failed = False
+        if dcount == limit:
+            failed = True
+            errmsg = _(
+u"""Can't add new destination to alias “%(address)s”.
+Currently this alias expands into %(count)i/%(limit)i recipients.
 One more destination will render this alias unusable.
-Hint: Increase Postfix' virtual_alias_expansion_limit
-""") % {'address': self._addr, 'count': curEx}
-            raise VMMAE(errmsg, ERR.ALIAS_EXCEEDS_EXPANSION_LIMIT)
+Hint: Increase Postfix' virtual_alias_expansion_limit""")
+        elif dcount > limit:
+            failed = True
+            errmsg = _(
+u"""Can't add new destination to alias “%(address)s”.
+This alias already exceeds it's 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 VMMAE(errmsg % {'address': self._addr, 'count': dcount,
+                                  'limit': limit},
+                        ALIAS_EXCEEDS_EXPANSION_LIMIT)
 
-    def save(self, expansion_limit):
-        if self._dest is None:
-           raise VMMAE(_(u"No destination address specified for alias."),
-               ERR.ALIAS_MISSING_DEST)
-        if self._isNew:
-            self._checkExpansion(expansion_limit)
+    def __delete(self, destination=None):
+        """Deletes a destination from the alias, if ``destination`` is not
+        ``None``. If ``destination`` is None, the alias with all it's
+        destination addresses will be deleted."""
+        dbc = self._dbh.cursor()
+        if destination is None:
+            dbc.execute("DELETE FROM alias WHERE gid=%s AND address=%s",
+                        self._gid, self._addr.localpart)
+        else:
+            dbc.execute("DELETE FROM alias WHERE gid=%s AND address=%s AND \
+ destination=%s",
+                        self._gid, self._addr.localpart, str(destination))
+        if dbc.rowcount > 0:
+            self._dbh.commit()
+        dbc.close()
+
+    def __len__(self):
+        """Returns the number of destinations of the alias."""
+        return len(self._dests)
+
+    def addDestination(self, destination, expansion_limit):
+        """Adds the ``destination`` `EmailAddress` to the alias."""
+        if not isinstance(destination, EmailAddress):
+            raise TypeError("Argument 'destination' is not an EmailAddress")
+        if self._addr == destination:
+            raise VMMAE(_(u"Address and destination are identical."),
+                        ALIAS_ADDR_DEST_IDENTICAL)
+        if not destination in self._dests:
+            self.__check_expansion(expansion_limit)
             dbc = self._dbh.cursor()
-            dbc.execute("INSERT INTO alias (gid, address, destination) VALUES\
- (%s, %s, %s)", self._gid, self._addr._localpart, str(self._dest))
+            dbc.execute('INSERT INTO alias (gid, address, destination) \
+VALUES (%s, %s, %s)',
+                        self._gid, self._addr.localpart, str(destination))
             self._dbh.commit()
             dbc.close()
+            self._dests.append(destination)
         else:
-            raise VMMAE(
-               _(u"The alias “%(a)s” with destination “%(d)s” already exists.")\
-                       % {'a': self._addr, 'd': self._dest}, ERR.ALIAS_EXISTS)
+            raise VMMAE(_(
+                u'The alias “%(a)s” has already the destination “%(d)s”.') %
+                        {'a': self._addr, 'd': destination}, ALIAS_EXISTS)
 
-    def getInfo(self):
-        dbc = self._dbh.cursor()
-        dbc.execute('SELECT destination FROM alias WHERE gid=%s AND address=%s',
-                self._gid, self._addr._localpart)
-        destinations = dbc.fetchall()
-        dbc.close()
-        if len(destinations) > 0:
-            targets = [destination[0] for destination in destinations]
-            return targets
+    def delDestination(self, destination):
+        """Deletes the specified ``destination`` address from the alias."""
+        if not isinstance(destination, EmailAddress):
+            raise TypeError("Argument 'destination' is not an EmailAddress")
+        if not self._dests:
+            raise VMMAE(_(u"The alias “%s” doesn't exist.") % self._addr,
+                        NO_SUCH_ALIAS)
+        if not destination in self._dests:
+            raise VMMAE(_(u"The address “%(d)s” isn't a destination of \
+the alias “%(a)s”.") %
+                        {'a': self._addr, 'd': destination}, NO_SUCH_ALIAS)
+        self.__delete(destination)
+        self._dests.remove(destination)
+
+    def getDestinations(self):
+        """Returns an iterator for all destinations of the alias."""
+        if self._dests:
+            return iter(self._dests)
         else:
             raise VMMAE(_(u"The alias “%s” doesn't exist.") % self._addr,
-                    ERR.NO_SUCH_ALIAS)
+                        NO_SUCH_ALIAS)
 
     def delete(self):
-        dbc = self._dbh.cursor()
-        if self._dest is None:
-            dbc.execute("DELETE FROM alias WHERE gid=%s AND address=%s",
-                    self._gid, self._addr._localpart)
+        """Deletes the alias with all it's destinations."""
+        if self._dests:
+            self.__delete()
+            del self._dests[:]
         else:
-            dbc.execute("DELETE FROM alias WHERE gid=%s AND address=%s AND \
- destination=%s", self._gid, self._addr._localpart, str(self._dest))
-        rowcount = dbc.rowcount
-        dbc.close()
-        if rowcount > 0:
-            self._dbh.commit()
-        else:
-            if self._dest is None:
-                msg = _(u"The alias “%s” doesn't exist.") % self._addr
-            else:
-                msg = _(u"The alias “%(a)s” with destination “%(d)s” doesn't\
- exist.") % {'a': self._addr, 'd': self._dest}
-            raise VMMAE(msg, ERR.NO_SUCH_ALIAS)
+            raise VMMAE(_(u"The alias “%s” doesn't exist.") % self._addr,
+                        NO_SUCH_ALIAS)
 
+
+del _
--- a/VirtualMailManager/Handler.py	Tue Feb 09 22:14:08 2010 +0000
+++ b/VirtualMailManager/Handler.py	Wed Feb 10 02:13:35 2010 +0000
@@ -42,7 +42,7 @@
 class Handler(object):
     """Wrapper class to simplify the access on all the stuff from
     VirtualMailManager"""
-    __slots__ = ('_Cfg', '_cfgFileName', '__dbh', '_scheme', '__warnings',
+    __slots__ = ('_Cfg', '_cfgFileName', '_dbh', '_scheme', '__warnings',
                  '_postconf')
 
     def __init__(self, skip_some_checks=False):
@@ -58,7 +58,7 @@
         self._cfgFileName = ''
         self.__warnings = []
         self._Cfg = None
-        self.__dbh = None
+        self._dbh = None
 
         if os.geteuid():
             raise VMMNotRootException(_(u"You are not root.\n\tGood bye!\n"),
@@ -129,16 +129,16 @@
 
     def __dbConnect(self):
         """Creates a pyPgSQL.PgSQL.connection instance."""
-        if self.__dbh is None or (isinstance(self.__dbh, PgSQL.Connection) and
-                                  not self.__dbh._isOpen):
+        if self._dbh is None or (isinstance(self._dbh, PgSQL.Connection) and
+                                  not self._dbh._isOpen):
             try:
-                self.__dbh = PgSQL.connect(
+                self._dbh = PgSQL.connect(
                         database=self._Cfg.dget('database.name'),
                         user=self._Cfg.pget('database.user'),
                         host=self._Cfg.dget('database.host'),
                         password=self._Cfg.pget('database.pass'),
                         client_encoding='utf8', unicode_results=True)
-                dbc = self.__dbh.cursor()
+                dbc = self._dbh.cursor()
                 dbc.execute("SET NAMES 'UTF8'")
                 dbc.close()
             except PgSQL.libpq.DatabaseError, e:
@@ -157,22 +157,22 @@
 
     def accountExists(dbh, address):
         sql = "SELECT gid FROM users WHERE gid = (SELECT gid FROM domain_name\
- WHERE domainname = '%s') AND local_part = '%s'" % (address._domainname,
-            address._localpart)
+ WHERE domainname = '%s') AND local_part = '%s'" % (address.domainname,
+            address.localpart)
         return Handler._exists(dbh, sql)
     accountExists = staticmethod(accountExists)
 
     def aliasExists(dbh, address):
         sql = "SELECT DISTINCT gid FROM alias WHERE gid = (SELECT gid FROM\
  domain_name WHERE domainname = '%s') AND address = '%s'" % (
-                address._domainname, address._localpart)
+                address.domainname, address.localpart)
         return Handler._exists(dbh, sql)
     aliasExists = staticmethod(aliasExists)
 
     def relocatedExists(dbh, address):
         sql = "SELECT gid FROM relocated WHERE gid = (SELECT gid FROM\
  domain_name WHERE domainname = '%s') AND address = '%s'" % (
-                address._domainname, address._localpart)
+                address.domainname, address.localpart)
         return Handler._exists(dbh, sql)
     relocatedExists = staticmethod(relocatedExists)
 
@@ -181,27 +181,25 @@
         address = EmailAddress(address)
         if not password is None:
             password = self.__pwhash(password)
-        return Account(self.__dbh, address, password)
+        return Account(self._dbh, address, password)
 
-    def __getAlias(self, address, destination=None):
+    def __getAlias(self, address):
         self.__dbConnect()
         address = EmailAddress(address)
-        if destination is not None:
-            destination = EmailAddress(destination)
-        return Alias(self.__dbh, address, destination)
+        return Alias(self._dbh, address)
 
     def __getRelocated(self, address, destination=None):
         self.__dbConnect()
         address = EmailAddress(address)
         if destination is not None:
             destination = EmailAddress(destination)
-        return Relocated(self.__dbh, address, destination)
+        return Relocated(self._dbh, address, destination)
 
     def __getDomain(self, domainname, transport=None):
         if transport is None:
             transport = self._Cfg.dget('misc.transport')
         self.__dbConnect()
-        return Domain(self.__dbh, domainname,
+        return Domain(self._dbh, domainname,
                 self._Cfg.dget('misc.base_directory'), transport)
 
     def __getDiskUsage(self, directory):
@@ -474,12 +472,12 @@
         domainname -- name of the target domain (str)
         """
         dom = self.__getDomain(domainname)
-        aliasDom = AliasDomain(self.__dbh, aliasname, dom)
+        aliasDom = AliasDomain(self._dbh, aliasname, dom)
         aliasDom.save()
 
     def aliasDomainInfo(self, aliasname):
         self.__dbConnect()
-        aliasDom = AliasDomain(self.__dbh, aliasname, None)
+        aliasDom = AliasDomain(self._dbh, aliasname, None)
         return aliasDom.info()
 
     def aliasDomainSwitch(self, aliasname, domainname):
@@ -490,7 +488,7 @@
         domainname -- name of the new target domain (str)
         """
         dom = self.__getDomain(domainname)
-        aliasDom = AliasDomain(self.__dbh, aliasname, dom)
+        aliasDom = AliasDomain(self._dbh, aliasname, dom)
         aliasDom.switch()
 
     def aliasDomainDelete(self, aliasname):
@@ -500,7 +498,7 @@
         aliasname -- the name of the alias domain (str)
         """
         self.__dbConnect()
-        aliasDom = AliasDomain(self.__dbh, aliasname, None)
+        aliasDom = AliasDomain(self._dbh, aliasname, None)
         aliasDom.delete()
 
     def domainList(self, pattern=None):
@@ -520,7 +518,7 @@
                     _(u"The pattern “%s” contains invalid characters.") %
                     pattern, ERR.DOMAIN_INVALID)
         self.__dbConnect()
-        return search(self.__dbh, pattern=pattern, like=like)
+        return search(self._dbh, pattern=pattern, like=like)
 
     def userAdd(self, emailaddress, password):
         if password is None or (isinstance(password, basestring) and
@@ -536,14 +534,18 @@
         self.__mailDirMake(acc.getDir('domain'), acc.getUID(), acc.getGID())
 
     def aliasAdd(self, aliasaddress, targetaddress):
-        alias = self.__getAlias(aliasaddress, targetaddress)
-        alias.save(long(self._postconf.read('virtual_alias_expansion_limit')))
-        gid = self.__getDomain(alias._dest._domainname).getID()
-        if gid > 0 and (not Handler.accountExists(self.__dbh, alias._dest) and
-                        not Handler.aliasExists(self.__dbh, alias._dest)):
+        """Creates a new `Alias` entry for the given *aliasaddress* with
+        the given *targetaddress*."""
+        alias = self.__getAlias(aliasaddress)
+        destination = EmailAddress(targetaddress)
+        alias.addDestination(destination,
+                    long(self._postconf.read('virtual_alias_expansion_limit')))
+        gid = self.__getDomain(destination.domainname).getID()
+        if gid > 0 and (not Handler.accountExists(self._dbh, destination) and
+                        not Handler.aliasExists(self._dbh, destination)):
             self.__warnings.append(
                 _(u"The destination account/alias “%s” doesn't exist.") %
-                                   alias._dest)
+                                   destination)
 
     def userDelete(self, emailaddress, force=None):
         if force not in [None, 'delalias']:
@@ -570,12 +572,34 @@
                     raise
 
     def aliasInfo(self, aliasaddress):
+        """Returns an iterator object for all destinations (`EmailAddress`
+        instances) for the `Alias` with the given *aliasaddress*."""
         alias = self.__getAlias(aliasaddress)
-        return alias.getInfo()
+        try:
+            return alias.getDestinations()
+        except VMMAliasException, e:
+            if e.code() == ERR.NO_SUCH_ALIAS:
+                if Handler.accountExists(self._dbh, alias._addr):
+                    raise VMMException(
+                        _(u'There is already an account with address “%s”.') %
+                                       aliasaddress, ERR.ACCOUNT_EXISTS)
+                if Handler.relocatedExists(self._dbh, alias._addr):
+                    raise VMMException(_(u'There is already a relocated user \
+with the address “%s”.') %
+                                       aliasaddress, ERR.RELOCATED_EXISTS)
+                raise
+            else:
+                raise
 
     def aliasDelete(self, aliasaddress, targetaddress=None):
-        alias = self.__getAlias(aliasaddress, targetaddress)
-        alias.delete()
+        """Deletes the `Alias` *aliasaddress* with all its destinations from
+        the database. If *targetaddress* is not ``None``, only this
+        destination will be removed from the alias."""
+        alias = self.__getAlias(aliasaddress)
+        if targetaddress is None:
+            alias.delete()
+        else:
+            alias.delDestination(EmailAddress(targetaddress))
 
     def userInfo(self, emailaddress, details=None):
         if details not in (None, 'du', 'aliases', 'full'):
@@ -594,7 +618,7 @@
     def userByID(self, uid):
         from Handler.Account import getAccountByID
         self.__dbConnect()
-        return getAccountByID(uid, self.__dbh)
+        return getAccountByID(uid, self._dbh)
 
     def userPassword(self, emailaddress, password):
         if password is None or (isinstance(password, basestring) and
@@ -647,5 +671,5 @@
         relocated.delete()
 
     def __del__(self):
-        if isinstance(self.__dbh, PgSQL.Connection) and self.__dbh._isOpen:
-            self.__dbh.close()
+        if isinstance(self._dbh, PgSQL.Connection) and self._dbh._isOpen:
+            self._dbh.close()