VirtualMailManager/catchall.py
changeset 760 b678a1c43027
parent 748 659c4476c57c
child 761 e4e656f19771
equal deleted inserted replaced
748:659c4476c57c 760:b678a1c43027
     1 # -*- coding: UTF-8 -*-
       
     2 # Copyright (c) 2012 - 2014, martin f. krafft
       
     3 # See COPYING for distribution information.
       
     4 """
       
     5     VirtualMailManager.catchall
       
     6     ~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
     7 
       
     8     Virtual Mail Manager's CatchallAlias class to manage domain catch-all
       
     9     aliases.
       
    10 
       
    11     This is heavily based on (more or less a copy of) the Alias class, because
       
    12     fundamentally, catchall aliases are aliases, but without a localpart.
       
    13     While Alias could potentially derive from CatchallAlias to reuse some of
       
    14     the functionality, it's probably not worth it. I found no sensible way to
       
    15     derive CatchallAlias from Alias, or at least none that would harness the
       
    16     powers of polymorphism.
       
    17 
       
    18     Yet, we reuse the AliasError exception class, which makes sense.
       
    19 """
       
    20 
       
    21 from VirtualMailManager.domain import get_gid
       
    22 from VirtualMailManager.emailaddress import \
       
    23      EmailAddress, DestinationEmailAddress as DestAddr
       
    24 from VirtualMailManager.errors import AliasError as AErr
       
    25 from VirtualMailManager.ext.postconf import Postconf
       
    26 from VirtualMailManager.pycompat import all
       
    27 from VirtualMailManager.constants import \
       
    28      ALIAS_EXCEEDS_EXPANSION_LIMIT, NO_SUCH_ALIAS, NO_SUCH_DOMAIN
       
    29 
       
    30 
       
    31 _ = lambda msg: msg
       
    32 cfg_dget = lambda option: None
       
    33 
       
    34 
       
    35 class CatchallAlias(object):
       
    36     """Class to manage domain catch-all aliases."""
       
    37     __slots__ = ('_domain', '_dests', '_gid', '_dbh')
       
    38 
       
    39     def __init__(self, dbh, domain):
       
    40         self._domain = domain
       
    41         self._dbh = dbh
       
    42         self._gid = get_gid(self._dbh, self.domain)
       
    43         if not self._gid:
       
    44             raise AErr(_(u"The domain '%s' does not exist.") %
       
    45                        self.domain, NO_SUCH_DOMAIN)
       
    46         self._dests = []
       
    47 
       
    48         self._load_dests()
       
    49 
       
    50     def _load_dests(self):
       
    51         """Loads all known destination addresses into the _dests list."""
       
    52         dbc = self._dbh.cursor()
       
    53         dbc.execute('SELECT destination FROM catchall WHERE gid = %s',
       
    54                     (self._gid,))
       
    55         dests = dbc.fetchall()
       
    56         if dbc.rowcount > 0:
       
    57             self._dests.extend(DestAddr(dest[0], self._dbh) for dest in dests)
       
    58         dbc.close()
       
    59 
       
    60     def _check_expansion(self, count_new):
       
    61         """Checks the current expansion limit of the alias."""
       
    62         postconf = Postconf(cfg_dget('bin.postconf'))
       
    63         limit = long(postconf.read('virtual_alias_expansion_limit'))
       
    64         dcount = len(self._dests)
       
    65         failed = False
       
    66         if dcount == limit or dcount + count_new > limit:
       
    67             failed = True
       
    68             errmsg = _(
       
    69 u"""Cannot add %(count_new)i new destination(s) to catch-all alias for
       
    70 domain '%(domain)s'. Currently this alias expands into %(count)i/%(limit)i
       
    71 recipients. %(count_new)i additional destination(s) will render this alias
       
    72 unusable.
       
    73 Hint: Increase Postfix' virtual_alias_expansion_limit""")
       
    74         elif dcount > limit:
       
    75             failed = True
       
    76             errmsg = _(
       
    77 u"""Cannot add %(count_new)i new destination(s) to catch-all alias for domain
       
    78 '%(domain)s'. This alias already exceeds its expansion limit \
       
    79 (%(count)i/%(limit)i).
       
    80 So its unusable, all messages addressed to this alias will be bounced.
       
    81 Hint: Delete some destination addresses.""")
       
    82         if failed:
       
    83             raise AErr(errmsg % {'domain': self._domain, 'count': dcount,
       
    84                                  'limit': limit, 'count_new': count_new},
       
    85                        ALIAS_EXCEEDS_EXPANSION_LIMIT)
       
    86 
       
    87     def _delete(self, destinations=None):
       
    88         """Delete one ore multiple destinations from the catchall alias, if
       
    89         ``destinations`` is not ``None``.  If ``destinations`` is None, the
       
    90         catchall alias with all its destination addresses will be deleted.
       
    91 
       
    92         """
       
    93         dbc = self._dbh.cursor()
       
    94         if not destinations:
       
    95             dbc.execute('DELETE FROM catchall WHERE gid = %s', (self._gid,))
       
    96         else:
       
    97             dbc.executemany('DELETE FROM catchall WHERE gid = %d AND '
       
    98                             'destination = %%s' % self._gid,
       
    99                             ((str(dest),) for dest in destinations))
       
   100         if dbc.rowcount > 0:
       
   101             self._dbh.commit()
       
   102         dbc.close()
       
   103 
       
   104     def __len__(self):
       
   105         """Returns the number of destinations of the catchall alias."""
       
   106         return len(self._dests)
       
   107 
       
   108     @property
       
   109     def domain(self):
       
   110         """The Alias' domain."""
       
   111         return self._domain
       
   112 
       
   113     def add_destinations(self, destinations, warnings=None):
       
   114         """Adds the `EmailAddress`es from *destinations* list to the
       
   115         destinations of the catchall alias.
       
   116 
       
   117         Destinations, that are already assigned to the alias, will be
       
   118         removed from *destinations*.  When done, this method will return
       
   119         a set with all destinations, that were saved in the database.
       
   120         """
       
   121         destinations = set(destinations)
       
   122         assert destinations and \
       
   123                 all(isinstance(dest, EmailAddress) for dest in destinations)
       
   124         if not warnings is None:
       
   125             assert isinstance(warnings, list)
       
   126         duplicates = destinations.intersection(set(self._dests))
       
   127         if duplicates:
       
   128             destinations.difference_update(set(self._dests))
       
   129             if not warnings is None:
       
   130                 warnings.extend(duplicates)
       
   131         if not destinations:
       
   132             return destinations
       
   133         self._check_expansion(len(destinations))
       
   134         dbc = self._dbh.cursor()
       
   135         dbc.executemany("INSERT INTO catchall (gid, destination) "
       
   136                         "VALUES (%d, %%s)" % self._gid,
       
   137                         ((str(destination),) for destination in destinations))
       
   138         self._dbh.commit()
       
   139         dbc.close()
       
   140         self._dests.extend(destinations)
       
   141         return destinations
       
   142 
       
   143     def del_destinations(self, destinations, warnings=None):
       
   144         """Deletes the specified ``destinations`` from the catchall alias."""
       
   145         destinations = set(destinations)
       
   146         assert destinations and \
       
   147                 all(isinstance(dest, EmailAddress) for dest in destinations)
       
   148         if not warnings is None:
       
   149             assert isinstance(warnings, list)
       
   150         if not self._dests:
       
   151             raise AErr(_(u"There are no catch-all aliases defined for "
       
   152                          u"domain '%s'.") % self._domain, NO_SUCH_ALIAS)
       
   153         unknown = destinations.difference(set(self._dests))
       
   154         if unknown:
       
   155             destinations.intersection_update(set(self._dests))
       
   156             if not warnings is None:
       
   157                 warnings.extend(unknown)
       
   158         if not destinations:
       
   159             raise AErr(_(u"No suitable destinations left to remove from the "
       
   160                          u"catch-all alias of domain '%s'.") % self._domain,
       
   161                        NO_SUCH_ALIAS)
       
   162         self._delete(destinations)
       
   163         for destination in destinations:
       
   164             self._dests.remove(destination)
       
   165 
       
   166     def get_destinations(self):
       
   167         """Returns an iterator for all destinations of the catchall alias."""
       
   168         if not self._dests:
       
   169             raise AErr(_(u"There are no catch-all aliases defined for "
       
   170                          u"domain '%s'.") % self._domain, NO_SUCH_ALIAS)
       
   171         return iter(self._dests)
       
   172 
       
   173     def delete(self):
       
   174         """Deletes all catchall destinations for the domain."""
       
   175         if not self._dests:
       
   176             raise AErr(_(u"There are no catch-all aliases defined for "
       
   177                          u"domain '%s'.") % self._domain, NO_SUCH_ALIAS)
       
   178         self._delete()
       
   179         del self._dests[:]
       
   180 
       
   181 del _, cfg_dget