# HG changeset patch # User Pascal Volk # Date 1265768015 0 # Node ID 65a3163bd11398aff99e37d570ddb2a51de3d9ff # Parent 05dd49fc3ea1ed79615517db56dcd1729ee8ec52 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. diff -r 05dd49fc3ea1 -r 65a3163bd113 VirtualMailManager/Alias.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 _ diff -r 05dd49fc3ea1 -r 65a3163bd113 VirtualMailManager/Handler.py --- 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()