VirtualMailManager/domain.py
branchv0.6.x
changeset 320 011066435e6f
parent 316 31d8931dc535
child 331 270b57af85de
equal deleted inserted replaced
319:f4956b4ceba1 320:011066435e6f
       
     1 # -*- coding: UTF-8 -*-
       
     2 # Copyright (c) 2007 - 2010, Pascal Volk
       
     3 # See COPYING for distribution information.
       
     4 """
       
     5     VirtualMailManager.domain
       
     6     ~~~~~~~~~~~~~~~~~~~~~~~~~
       
     7 
       
     8     Virtual Mail Manager's Domain class to manage e-mail domains.
       
     9 """
       
    10 
       
    11 import os
       
    12 import re
       
    13 from random import choice
       
    14 
       
    15 from VirtualMailManager.constants import \
       
    16      ACCOUNT_AND_ALIAS_PRESENT, ACCOUNT_PRESENT, ALIAS_PRESENT, \
       
    17      DOMAIN_ALIAS_EXISTS, DOMAIN_EXISTS, DOMAIN_INVALID, DOMAIN_TOO_LONG, \
       
    18      NO_SUCH_DOMAIN
       
    19 from VirtualMailManager.errors import DomainError as DomErr
       
    20 from VirtualMailManager.transport import Transport
       
    21 
       
    22 
       
    23 MAILDIR_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'
       
    24 RE_DOMAIN = re.compile(r"^(?:[a-z0-9-]{1,63}\.){1,}[a-z]{2,6}$")
       
    25 _ = lambda msg: msg
       
    26 
       
    27 
       
    28 class Domain(object):
       
    29     """Class to manage e-mail domains."""
       
    30     __slots__ = ('_directory', '_gid', '_name', '_transport', '_dbh', '_new')
       
    31 
       
    32     def __init__(self, dbh, domainname):
       
    33         """Creates a new Domain instance.
       
    34 
       
    35         Loads all relevant data from the database, if the domain could be
       
    36         found.  To create a new domain call the methods set_directory() and
       
    37         set_transport() before save().
       
    38 
       
    39         A DomainError will be thrown when the *domainname* is the name of
       
    40         an alias domain.
       
    41 
       
    42         Arguments:
       
    43 
       
    44         `dbh` : pyPgSQL.PgSQL.Connection
       
    45           a database connection for the database access
       
    46         `domainname` : basestring
       
    47           The name of the domain
       
    48         """
       
    49         self._name = check_domainname(domainname)
       
    50         self._dbh = dbh
       
    51         self._gid = 0
       
    52         self._transport = None
       
    53         self._directory = None
       
    54         self._new = True
       
    55         self._load()
       
    56 
       
    57     def _load(self):
       
    58         """Load information from the database and checks if the domain name
       
    59         is the primary one.
       
    60 
       
    61         Raises a DomainError if Domain._name isn't the primary name of the
       
    62         domain.
       
    63         """
       
    64         dbc = self._dbh.cursor()
       
    65         dbc.execute('SELECT dd.gid, tid, domaindir, is_primary FROM '
       
    66                     'domain_data dd, domain_name dn WHERE domainname = %s AND '
       
    67                     'dn.gid = dd.gid', self._name)
       
    68         result = dbc.fetchone()
       
    69         dbc.close()
       
    70         if result:
       
    71             if not result[3]:
       
    72                 raise DomErr(_(u"The domain '%s' is an alias domain.") %
       
    73                              self._name, DOMAIN_ALIAS_EXISTS)
       
    74             self._gid, self._directory = result[0], result[2]
       
    75             self._transport = Transport(self._dbh, tid=result[1])
       
    76             self._new = False
       
    77 
       
    78     def _set_gid(self):
       
    79         """Sets the ID of the domain - if not set yet."""
       
    80         assert self._gid == 0
       
    81         dbc = self._dbh.cursor()
       
    82         dbc.execute("SELECT nextval('domain_gid')")
       
    83         self._gid = dbc.fetchone()[0]
       
    84         dbc.close()
       
    85 
       
    86     def _has(self, what):
       
    87         """Checks if aliases or accounts are assigned to the domain.
       
    88 
       
    89         If there are assigned accounts or aliases True will be returned,
       
    90         otherwise False will be returned.
       
    91 
       
    92         Argument:
       
    93 
       
    94         `what` : basestring
       
    95             "alias" or "users"
       
    96         """
       
    97         assert what in ('alias', 'users')
       
    98         dbc = self._dbh.cursor()
       
    99         if what == 'users':
       
   100             dbc.execute("SELECT count(gid) FROM users WHERE gid=%s", self._gid)
       
   101         else:
       
   102             dbc.execute("SELECT count(gid) FROM alias WHERE gid=%s", self._gid)
       
   103         count = dbc.fetchone()
       
   104         dbc.close()
       
   105         return count[0] > 0
       
   106 
       
   107     def _chk_delete(self, deluser, delalias):
       
   108         """Checks dependencies for deletion.
       
   109 
       
   110         Arguments:
       
   111         deluser -- ignore available accounts (bool)
       
   112         delalias -- ignore available aliases (bool)
       
   113         """
       
   114         if not deluser:
       
   115             hasuser = self._has('users')
       
   116         else:
       
   117             hasuser = False
       
   118         if not delalias:
       
   119             hasalias = self._has('alias')
       
   120         else:
       
   121             hasalias = False
       
   122         if hasuser and hasalias:
       
   123             raise DomErr(_(u'There are accounts and aliases.'),
       
   124                          ACCOUNT_AND_ALIAS_PRESENT)
       
   125         elif hasuser:
       
   126             raise DomErr(_(u'There are accounts.'), ACCOUNT_PRESENT)
       
   127         elif hasalias:
       
   128             raise DomErr(_(u'There are aliases.'), ALIAS_PRESENT)
       
   129 
       
   130     def _chk_state(self):
       
   131         """Throws a DomainError if the Domain is new - not saved in the
       
   132         database."""
       
   133         if self._new:
       
   134             raise DomErr(_(u"The domain '%s' doesn't exist.") % self._name,
       
   135                          NO_SUCH_DOMAIN)
       
   136 
       
   137     @property
       
   138     def gid(self):
       
   139         """The GID of the Domain."""
       
   140         return self._gid
       
   141 
       
   142     @property
       
   143     def name(self):
       
   144         """The Domain's name."""
       
   145         return self._name
       
   146 
       
   147     @property
       
   148     def directory(self):
       
   149         """The Domain's directory."""
       
   150         return self._directory
       
   151 
       
   152     def set_directory(self, basedir):
       
   153         """Set the path value of the Domain's directory, inside *basedir*.
       
   154 
       
   155         Argument:
       
   156 
       
   157         `basedir` : basestring
       
   158           The base directory of all domains
       
   159         """
       
   160         assert self._new and self._directory is None
       
   161         self._set_gid()
       
   162         self._directory = os.path.join(basedir, choice(MAILDIR_CHARS),
       
   163                                        str(self._gid))
       
   164 
       
   165     @property
       
   166     def transport(self):
       
   167         """The Domain's transport."""
       
   168         return self._transport
       
   169 
       
   170     def set_transport(self, transport):
       
   171         """Set the transport for the new Domain.
       
   172 
       
   173         Argument:
       
   174 
       
   175         `transport` : VirtualMailManager.Transport
       
   176           The transport of the new Domain
       
   177         """
       
   178         assert self._new and isinstance(transport, Transport)
       
   179         self._transport = transport
       
   180 
       
   181     def save(self):
       
   182         """Stores the new domain in the database."""
       
   183         if not self._new:
       
   184             raise DomErr(_(u"The domain '%s' already exists.") % self._name,
       
   185                          DOMAIN_EXISTS)
       
   186         assert self._directory is not None and self._transport is not None
       
   187         dbc = self._dbh.cursor()
       
   188         dbc.execute("INSERT INTO domain_data VALUES (%s, %s, %s)", self._gid,
       
   189                     self._transport.tid, self._directory)
       
   190         dbc.execute("INSERT INTO domain_name VALUES (%s, %s, %s)", self._name,
       
   191                     self._gid, True)
       
   192         self._dbh.commit()
       
   193         dbc.close()
       
   194         self._new = False
       
   195 
       
   196     def delete(self, deluser=False, delalias=False):
       
   197         """Deletes the domain.
       
   198 
       
   199         Arguments:
       
   200 
       
   201         `deluser` : bool
       
   202           force deletion of all available accounts, default `False`
       
   203         `delalias` : bool
       
   204           force deletion of all available aliases, default `False`
       
   205         """
       
   206         self._chk_state()
       
   207         self._chk_delete(deluser, delalias)
       
   208         dbc = self._dbh.cursor()
       
   209         for tbl in ('alias', 'users', 'relocated', 'domain_name',
       
   210                     'domain_data'):
       
   211             dbc.execute("DELETE FROM %s WHERE gid = %d" % (tbl, self._gid))
       
   212         self._dbh.commit()
       
   213         dbc.close()
       
   214         self._gid = 0
       
   215         self._directory = self._transport = None
       
   216         self._new = True
       
   217 
       
   218     def update_transport(self, transport, force=False):
       
   219         """Sets a new transport for the Domain.
       
   220 
       
   221         If *force* is `True` the new *transport* will be assigned to all
       
   222         existing accounts.  Otherwise the *transport* will be only used for
       
   223         accounts created from now on.
       
   224 
       
   225         Arguments:
       
   226 
       
   227         `transport` : VirtualMailManager.Transport
       
   228           the new transport
       
   229         `force` : bool
       
   230           enforce new transport setting for all accounts, default `False`
       
   231         """
       
   232         self._chk_state()
       
   233         assert isinstance(transport, Transport)
       
   234         if transport == self._transport:
       
   235             return
       
   236         dbc = self._dbh.cursor()
       
   237         dbc.execute("UPDATE domain_data SET tid = %s WHERE gid = %s",
       
   238                     transport.tid, self._gid)
       
   239         if dbc.rowcount > 0:
       
   240             self._dbh.commit()
       
   241         if force:
       
   242             dbc.execute("UPDATE users SET tid = %s WHERE gid = %s",
       
   243                         transport.tid, self._gid)
       
   244             if dbc.rowcount > 0:
       
   245                 self._dbh.commit()
       
   246         dbc.close()
       
   247         self._transport = transport
       
   248 
       
   249     def get_info(self):
       
   250         """Returns a dictionary with information about the domain."""
       
   251         self._chk_state()
       
   252         dbc = self._dbh.cursor()
       
   253         dbc.execute('SELECT gid, domainname, transport, domaindir, '
       
   254                     'aliasdomains accounts, aliases, relocated FROM '
       
   255                     'vmm_domain_info WHERE gid = %s', self._gid)
       
   256         info = dbc.fetchone()
       
   257         dbc.close()
       
   258         keys = ('gid', 'domainname', 'transport', 'domaindir', 'aliasdomains',
       
   259                 'accounts', 'aliases', 'relocated')
       
   260         return dict(zip(keys, info))
       
   261 
       
   262     def get_accounts(self):
       
   263         """Returns a list with all accounts of the domain."""
       
   264         self._chk_state()
       
   265         dbc = self._dbh.cursor()
       
   266         dbc.execute('SELECT local_part from users where gid = %s ORDER BY '
       
   267                     'local_part', self._gid)
       
   268         users = dbc.fetchall()
       
   269         dbc.close()
       
   270         accounts = []
       
   271         if users:
       
   272             addr = u'@'.join
       
   273             _dom = self._name
       
   274             accounts = [addr((account[0], _dom)) for account in users]
       
   275         return accounts
       
   276 
       
   277     def get_aliases(self):
       
   278         """Returns a list with all aliases e-mail addresses of the domain."""
       
   279         self._chk_state()
       
   280         dbc = self._dbh.cursor()
       
   281         dbc.execute('SELECT DISTINCT address FROM alias WHERE gid = %s ORDER '
       
   282                     'BY address', self._gid)
       
   283         addresses = dbc.fetchall()
       
   284         dbc.close()
       
   285         aliases = []
       
   286         if addresses:
       
   287             addr = u'@'.join
       
   288             _dom = self._name
       
   289             aliases = [addr((alias[0], _dom)) for alias in addresses]
       
   290         return aliases
       
   291 
       
   292     def get_relocated(self):
       
   293         """Returns a list with all addresses of relocated users."""
       
   294         self._chk_state()
       
   295         dbc = self._dbh.cursor()
       
   296         dbc.execute('SELECT address FROM relocated WHERE gid = %s ORDER BY '
       
   297                     'address', self._gid)
       
   298         addresses = dbc.fetchall()
       
   299         dbc.close()
       
   300         relocated = []
       
   301         if addresses:
       
   302             addr = u'@'.join
       
   303             _dom = self._name
       
   304             relocated = [addr((address[0], _dom)) for address in addresses]
       
   305         return relocated
       
   306 
       
   307     def get_aliase_names(self):
       
   308         """Returns a list with all alias domain names of the domain."""
       
   309         self._chk_state()
       
   310         dbc = self._dbh.cursor()
       
   311         dbc.execute('SELECT domainname FROM domain_name WHERE gid = %s AND '
       
   312                     'NOT is_primary ORDER BY domainname', self._gid)
       
   313         anames = dbc.fetchall()
       
   314         dbc.close()
       
   315         aliasdomains = []
       
   316         if anames:
       
   317             aliasdomains = [aname[0] for aname in anames]
       
   318         return aliasdomains
       
   319 
       
   320 
       
   321 def check_domainname(domainname):
       
   322     """Returns the validated domain name `domainname`.
       
   323 
       
   324     Throws an `DomainError`, if the domain name is too long or doesn't
       
   325     look like a valid domain name (label.label.label).
       
   326 
       
   327     """
       
   328     if not RE_DOMAIN.match(domainname):
       
   329         domainname = domainname.encode('idna')
       
   330     if len(domainname) > 255:
       
   331         raise DomErr(_(u'The domain name is too long'), DOMAIN_TOO_LONG)
       
   332     if not RE_DOMAIN.match(domainname):
       
   333         raise DomErr(_(u"The domain name '%s' is invalid") % domainname,
       
   334                      DOMAIN_INVALID)
       
   335     return domainname
       
   336 
       
   337 
       
   338 def get_gid(dbh, domainname):
       
   339     """Returns the group id of the domain *domainname*.
       
   340 
       
   341     If the domain couldn't be found in the database 0 will be returned.
       
   342     """
       
   343     domainname = check_domainname(domainname)
       
   344     dbc = dbh.cursor()
       
   345     dbc.execute('SELECT gid FROM domain_name WHERE domainname=%s', domainname)
       
   346     gid = dbc.fetchone()
       
   347     dbc.close()
       
   348     if gid:
       
   349         return gid[0]
       
   350     return 0
       
   351 
       
   352 
       
   353 def search(dbh, pattern=None, like=False):
       
   354     """'Search' for domains by *pattern* in the database.
       
   355 
       
   356     *pattern* may be a domain name or a partial domain name - starting
       
   357     and/or ending with a '%' sign.  When the *pattern* starts or ends with
       
   358     a '%' sign *like* has to be `True` to perform a wildcard search.
       
   359     To retrieve all available domains use the arguments' default values.
       
   360 
       
   361     This function returns a tuple with a list and a dict: (order, domains).
       
   362     The order list contains the domains' gid, alphabetical sorted by the
       
   363     primary domain name.  The domains dict's keys are the gids of the
       
   364     domains. The value of item is a list.  The first list element contains
       
   365     the primary domain name or `None`.  The elements [1:] contains the
       
   366     names of alias domains.
       
   367 
       
   368     Arguments:
       
   369 
       
   370     `pattern` : basestring
       
   371       a (partial) domain name (starting and/or ending with a "%" sign)
       
   372     `like` : bool
       
   373       should be `True` when *pattern* starts/ends with a "%" sign
       
   374     """
       
   375     if pattern and not like:
       
   376         pattern = check_domainname(pattern)
       
   377     sql = 'SELECT gid, domainname, is_primary FROM domain_name'
       
   378     if pattern:
       
   379         if like:
       
   380             sql += " WHERE domainname LIKE '%s'" % pattern
       
   381         else:
       
   382             sql += " WHERE domainname = '%s'" % pattern
       
   383     sql += ' ORDER BY is_primary DESC, domainname'
       
   384     dbc = dbh.cursor()
       
   385     dbc.execute(sql)
       
   386     result = dbc.fetchall()
       
   387     dbc.close()
       
   388 
       
   389     gids = [domain[0] for domain in result if domain[2]]
       
   390     domains = {}
       
   391     for gid, domain, is_primary in result:
       
   392         if is_primary:
       
   393             if not gid in domains:
       
   394                 domains[gid] = [domain]
       
   395             else:
       
   396                 domains[gid].insert(0, domain)
       
   397         else:
       
   398             if gid in gids:
       
   399                 if gid in domains:
       
   400                     domains[gid].append(domain)
       
   401                 else:
       
   402                     domains[gid] = [domain]
       
   403             else:
       
   404                 gids.append(gid)
       
   405                 domains[gid] = [None, domain]
       
   406     return gids, domains
       
   407 
       
   408 del _