VirtualMailManager/handler.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) 2007 - 2014, Pascal Volk
       
     3 # See COPYING for distribution information.
       
     4 """
       
     5    VirtualMailManager.handler
       
     6    ~~~~~~~~~~~~~~~~~~~~~~~~~~
       
     7 
       
     8    A wrapper class. It wraps round all other classes and does some
       
     9    dependencies checks.
       
    10 
       
    11    Additionally it communicates with the PostgreSQL database, creates
       
    12    or deletes directories of domains or users.
       
    13 """
       
    14 
       
    15 import os
       
    16 import re
       
    17 
       
    18 from shutil import rmtree
       
    19 from subprocess import Popen, PIPE
       
    20 
       
    21 from VirtualMailManager.account import Account
       
    22 from VirtualMailManager.alias import Alias
       
    23 from VirtualMailManager.aliasdomain import AliasDomain
       
    24 from VirtualMailManager.catchall import CatchallAlias
       
    25 from VirtualMailManager.common import exec_ok, lisdir
       
    26 from VirtualMailManager.config import Config as Cfg
       
    27 from VirtualMailManager.constants import MIN_GID, MIN_UID, \
       
    28      ACCOUNT_EXISTS, ALIAS_EXISTS, CONF_NOFILE, CONF_NOPERM, CONF_WRONGPERM, \
       
    29      DATABASE_ERROR, DOMAINDIR_GROUP_MISMATCH, DOMAIN_INVALID, \
       
    30      FOUND_DOTS_IN_PATH, INVALID_ARGUMENT, MAILDIR_PERM_MISMATCH, \
       
    31      NOT_EXECUTABLE, NO_SUCH_ACCOUNT, NO_SUCH_ALIAS, NO_SUCH_BINARY, \
       
    32      NO_SUCH_DIRECTORY, NO_SUCH_RELOCATED, RELOCATED_EXISTS, UNKNOWN_SERVICE, \
       
    33      VMM_ERROR, LOCALPART_INVALID, TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED
       
    34 from VirtualMailManager.domain import Domain
       
    35 from VirtualMailManager.emailaddress import DestinationEmailAddress, \
       
    36      EmailAddress, RE_LOCALPART
       
    37 from VirtualMailManager.errors import \
       
    38      DomainError, NotRootError, PermissionError, VMMError
       
    39 from VirtualMailManager.mailbox import new as new_mailbox
       
    40 from VirtualMailManager.pycompat import all, any
       
    41 from VirtualMailManager.quotalimit import QuotaLimit
       
    42 from VirtualMailManager.relocated import Relocated
       
    43 from VirtualMailManager.serviceset import ServiceSet, SERVICES
       
    44 from VirtualMailManager.transport import Transport
       
    45 
       
    46 
       
    47 _ = lambda msg: msg
       
    48 _db_mod = None
       
    49 
       
    50 CFG_FILE = 'vmm.cfg'
       
    51 CFG_PATH = '/root:/usr/local/etc:/etc'
       
    52 RE_DOMAIN_SEARCH = """^[a-z0-9-\.]+$"""
       
    53 OTHER_TYPES = {
       
    54     TYPE_ACCOUNT: (_(u'an account'), ACCOUNT_EXISTS),
       
    55     TYPE_ALIAS: (_(u'an alias'), ALIAS_EXISTS),
       
    56     TYPE_RELOCATED: (_(u'a relocated user'), RELOCATED_EXISTS),
       
    57 }
       
    58 
       
    59 
       
    60 class Handler(object):
       
    61     """Wrapper class to simplify the access on all the stuff from
       
    62     VirtualMailManager"""
       
    63     __slots__ = ('_cfg', '_cfg_fname', '_db_connect', '_dbh', '_warnings')
       
    64 
       
    65     def __init__(self, skip_some_checks=False):
       
    66         """Creates a new Handler instance.
       
    67 
       
    68         ``skip_some_checks`` : bool
       
    69             When a derived class knows how to handle all checks this
       
    70             argument may be ``True``. By default it is ``False`` and
       
    71             all checks will be performed.
       
    72 
       
    73         Throws a NotRootError if your uid is greater 0.
       
    74         """
       
    75         self._cfg_fname = ''
       
    76         self._warnings = []
       
    77         self._cfg = None
       
    78         self._dbh = None
       
    79         self._db_connect = None
       
    80 
       
    81         if os.geteuid():
       
    82             raise NotRootError(_(u"You are not root.\n\tGood bye!\n"),
       
    83                                CONF_NOPERM)
       
    84         if self._check_cfg_file():
       
    85             self._cfg = Cfg(self._cfg_fname)
       
    86             self._cfg.load()
       
    87         if not skip_some_checks:
       
    88             self._cfg.check()
       
    89             self._chkenv()
       
    90             self._set_db_connect()
       
    91 
       
    92     def _find_cfg_file(self):
       
    93         """Search the CFG_FILE in CFG_PATH.
       
    94         Raise a VMMError when no vmm.cfg could be found.
       
    95         """
       
    96         for path in CFG_PATH.split(':'):
       
    97             tmp = os.path.join(path, CFG_FILE)
       
    98             if os.path.isfile(tmp):
       
    99                 self._cfg_fname = tmp
       
   100                 break
       
   101         if not self._cfg_fname:
       
   102             raise VMMError(_(u"Could not find '%(cfg_file)s' in: "
       
   103                              u"'%(cfg_path)s'") % {'cfg_file': CFG_FILE,
       
   104                            'cfg_path': CFG_PATH}, CONF_NOFILE)
       
   105 
       
   106     def _check_cfg_file(self):
       
   107         """Checks the configuration file, returns bool"""
       
   108         self._find_cfg_file()
       
   109         fstat = os.stat(self._cfg_fname)
       
   110         fmode = int(oct(fstat.st_mode & 0777))
       
   111         if fmode % 100 and fstat.st_uid != fstat.st_gid or \
       
   112            fmode % 10 and fstat.st_uid == fstat.st_gid:
       
   113             # TP: Please keep the backticks around the command. `chmod 0600 …`
       
   114             raise PermissionError(_(u"wrong permissions for '%(file)s': "
       
   115                                     u"%(perms)s\n`chmod 0600 %(file)s` would "
       
   116                                     u"be great.") % {'file': self._cfg_fname,
       
   117                                   'perms': fmode}, CONF_WRONGPERM)
       
   118         else:
       
   119             return True
       
   120 
       
   121     def _chkenv(self):
       
   122         """Make sure our base_directory is a directory and that all
       
   123         required executables exists and are executable.
       
   124         If not, a VMMError will be raised"""
       
   125         dir_created = False
       
   126         basedir = self._cfg.dget('misc.base_directory')
       
   127         if not os.path.exists(basedir):
       
   128             old_umask = os.umask(0006)
       
   129             os.makedirs(basedir, 0771)
       
   130             os.chown(basedir, 0, 0)
       
   131             os.umask(old_umask)
       
   132             dir_created = True
       
   133         if not dir_created and not lisdir(basedir):
       
   134             raise VMMError(_(u"'%(path)s' is not a directory.\n(%(cfg_file)s: "
       
   135                              u"section 'misc', option 'base_directory')") %
       
   136                            {'path': basedir, 'cfg_file': self._cfg_fname},
       
   137                            NO_SUCH_DIRECTORY)
       
   138         for opt, val in self._cfg.items('bin'):
       
   139             try:
       
   140                 exec_ok(val)
       
   141             except VMMError, err:
       
   142                 if err.code in (NO_SUCH_BINARY, NOT_EXECUTABLE):
       
   143                     raise VMMError(err.msg + _(u"\n(%(cfg_file)s: section "
       
   144                                    u"'bin', option '%(option)s')") %
       
   145                                    {'cfg_file': self._cfg_fname,
       
   146                                     'option': opt}, err.code)
       
   147                 else:
       
   148                     raise
       
   149 
       
   150     def _set_db_connect(self):
       
   151         """check which module to use and set self._db_connect"""
       
   152         global _db_mod
       
   153         if self._cfg.dget('database.module').lower() == 'psycopg2':
       
   154             try:
       
   155                 _db_mod = __import__('psycopg2')
       
   156             except ImportError:
       
   157                 raise VMMError(_(u"Unable to import database module '%s'.") %
       
   158                                'psycopg2', VMM_ERROR)
       
   159             self._db_connect = self._psycopg2_connect
       
   160         else:
       
   161             try:
       
   162                 tmp = __import__('pyPgSQL', globals(), locals(), ['PgSQL'])
       
   163             except ImportError:
       
   164                 raise VMMError(_(u"Unable to import database module '%s'.") %
       
   165                                'pyPgSQL', VMM_ERROR)
       
   166             _db_mod = tmp.PgSQL
       
   167             self._db_connect = self._pypgsql_connect
       
   168 
       
   169     def _pypgsql_connect(self):
       
   170         """Creates a pyPgSQL.PgSQL.connection instance."""
       
   171         if self._dbh is None or (isinstance(self._dbh, _db_mod.Connection) and
       
   172                                   not self._dbh._isOpen):
       
   173             try:
       
   174                 self._dbh = _db_mod.connect(
       
   175                         database=self._cfg.dget('database.name'),
       
   176                         user=self._cfg.pget('database.user'),
       
   177                         host=self._cfg.dget('database.host'),
       
   178                         port=self._cfg.dget('database.port'),
       
   179                         password=self._cfg.pget('database.pass'),
       
   180                         client_encoding='utf8', unicode_results=True)
       
   181                 dbc = self._dbh.cursor()
       
   182                 dbc.execute("SET NAMES 'UTF8'")
       
   183                 dbc.close()
       
   184             except _db_mod.libpq.DatabaseError, err:
       
   185                 raise VMMError(str(err), DATABASE_ERROR)
       
   186 
       
   187     def _psycopg2_connect(self):
       
   188         """Return a new psycopg2 connection object."""
       
   189         if self._dbh is None or \
       
   190           (isinstance(self._dbh, _db_mod.extensions.connection) and
       
   191            self._dbh.closed):
       
   192             try:
       
   193                 self._dbh = _db_mod.connect(
       
   194                         host=self._cfg.dget('database.host'),
       
   195                         sslmode=self._cfg.dget('database.sslmode'),
       
   196                         port=self._cfg.dget('database.port'),
       
   197                         database=self._cfg.dget('database.name'),
       
   198                         user=self._cfg.pget('database.user'),
       
   199                         password=self._cfg.pget('database.pass'))
       
   200                 self._dbh.set_client_encoding('utf8')
       
   201                 _db_mod.extensions.register_type(_db_mod.extensions.UNICODE)
       
   202                 dbc = self._dbh.cursor()
       
   203                 dbc.execute("SET NAMES 'UTF8'")
       
   204                 dbc.close()
       
   205             except _db_mod.DatabaseError, err:
       
   206                 raise VMMError(str(err), DATABASE_ERROR)
       
   207 
       
   208     def _chk_other_address_types(self, address, exclude):
       
   209         """Checks if the EmailAddress *address* is known as `TYPE_ACCOUNT`,
       
   210         `TYPE_ALIAS` or `TYPE_RELOCATED`, but not as the `TYPE_*` specified
       
   211         by *exclude*.  If the *address* is known as one of the `TYPE_*`s
       
   212         the according `TYPE_*` constant will be returned.  Otherwise 0 will
       
   213         be returned."""
       
   214         assert exclude in (TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED) and \
       
   215                 isinstance(address, EmailAddress)
       
   216         if exclude is not TYPE_ACCOUNT:
       
   217             account = Account(self._dbh, address)
       
   218             if account:
       
   219                 return TYPE_ACCOUNT
       
   220         if exclude is not TYPE_ALIAS:
       
   221             alias = Alias(self._dbh, address)
       
   222             if alias:
       
   223                 return TYPE_ALIAS
       
   224         if exclude is not TYPE_RELOCATED:
       
   225             relocated = Relocated(self._dbh, address)
       
   226             if relocated:
       
   227                 return TYPE_RELOCATED
       
   228         return 0
       
   229 
       
   230     def _is_other_address(self, address, exclude):
       
   231         """Checks if *address* is known for an Account (TYPE_ACCOUNT),
       
   232         Alias (TYPE_ALIAS) or Relocated (TYPE_RELOCATED), except for
       
   233         *exclude*.  Returns `False` if the address is not known for other
       
   234         types.
       
   235 
       
   236         Raises a `VMMError` if the address is known.
       
   237         """
       
   238         other = self._chk_other_address_types(address, exclude)
       
   239         if not other:
       
   240             return False
       
   241         # TP: %(a_type)s will be one of: 'an account', 'an alias' or
       
   242         # 'a relocated user'
       
   243         msg = _(u"There is already %(a_type)s with the address '%(address)s'.")
       
   244         raise VMMError(msg % {'a_type': OTHER_TYPES[other][0],
       
   245                               'address': address}, OTHER_TYPES[other][1])
       
   246 
       
   247     def _get_account(self, address):
       
   248         """Return an Account instances for the given address (str)."""
       
   249         address = EmailAddress(address)
       
   250         self._db_connect()
       
   251         return Account(self._dbh, address)
       
   252 
       
   253     def _get_alias(self, address):
       
   254         """Return an Alias instances for the given address (str)."""
       
   255         address = EmailAddress(address)
       
   256         self._db_connect()
       
   257         return Alias(self._dbh, address)
       
   258 
       
   259     def _get_catchall(self, domain):
       
   260         """Return a CatchallAlias instances for the given domain (str)."""
       
   261         self._db_connect()
       
   262         return CatchallAlias(self._dbh, domain)
       
   263 
       
   264     def _get_relocated(self, address):
       
   265         """Return a Relocated instances for the given address (str)."""
       
   266         address = EmailAddress(address)
       
   267         self._db_connect()
       
   268         return Relocated(self._dbh, address)
       
   269 
       
   270     def _get_domain(self, domainname):
       
   271         """Return a Domain instances for the given domain name (str)."""
       
   272         self._db_connect()
       
   273         return Domain(self._dbh, domainname)
       
   274 
       
   275     def _get_disk_usage(self, directory):
       
   276         """Estimate file space usage for the given directory.
       
   277 
       
   278         Arguments:
       
   279 
       
   280         `directory` : basestring
       
   281           The directory to summarize recursively disk usage for
       
   282         """
       
   283         if lisdir(directory):
       
   284             return Popen([self._cfg.dget('bin.du'), "-hs", directory],
       
   285                          stdout=PIPE).communicate()[0].split('\t')[0]
       
   286         else:
       
   287             self._warnings.append(_('No such directory: %s') % directory)
       
   288             return 0
       
   289 
       
   290     def _make_domain_dir(self, domain):
       
   291         """Create a directory for the `domain` and its accounts."""
       
   292         cwd = os.getcwd()
       
   293         hashdir, domdir = domain.directory.split(os.path.sep)[-2:]
       
   294         dir_created = False
       
   295         os.chdir(self._cfg.dget('misc.base_directory'))
       
   296         old_umask = os.umask(0022)
       
   297         if not os.path.exists(hashdir):
       
   298             os.mkdir(hashdir, 0711)
       
   299             os.chown(hashdir, 0, 0)
       
   300             dir_created = True
       
   301         if not dir_created and not lisdir(hashdir):
       
   302             raise VMMError(_(u"'%s' is not a directory.") % hashdir,
       
   303                            NO_SUCH_DIRECTORY)
       
   304         if os.path.exists(domain.directory):
       
   305             raise VMMError(_(u"The file/directory '%s' already exists.") %
       
   306                            domain.directory, VMM_ERROR)
       
   307         os.mkdir(os.path.join(hashdir, domdir),
       
   308                  self._cfg.dget('domain.directory_mode'))
       
   309         os.chown(domain.directory, 0, domain.gid)
       
   310         os.umask(old_umask)
       
   311         os.chdir(cwd)
       
   312 
       
   313     def _make_home(self, account):
       
   314         """Create a home directory for the new Account *account*."""
       
   315         domdir = account.domain.directory
       
   316         if not lisdir(domdir):
       
   317             self._make_domain_dir(account.domain)
       
   318         os.umask(0007)
       
   319         uid = account.uid
       
   320         os.chdir(domdir)
       
   321         os.mkdir('%s' % uid, self._cfg.dget('account.directory_mode'))
       
   322         os.chown('%s' % uid, uid, account.gid)
       
   323 
       
   324     def _make_account_dirs(self, account):
       
   325         """Create all necessary directories for the account."""
       
   326         oldpwd = os.getcwd()
       
   327         self._make_home(account)
       
   328         mailbox = new_mailbox(account)
       
   329         mailbox.create()
       
   330         folders = self._cfg.dget('mailbox.folders').split(':')
       
   331         if any(folders):
       
   332             bad = mailbox.add_boxes(folders,
       
   333                                     self._cfg.dget('mailbox.subscribe'))
       
   334             if bad:
       
   335                 self._warnings.append(_(u"Skipped mailbox folders:") +
       
   336                                       '\n\t- ' + '\n\t- '.join(bad))
       
   337         os.chdir(oldpwd)
       
   338 
       
   339     def _delete_home(self, domdir, uid, gid):
       
   340         """Delete a user's home directory.
       
   341 
       
   342         Arguments:
       
   343 
       
   344         `domdir` : basestring
       
   345           The directory of the domain the user belongs to
       
   346           (commonly AccountObj.domain.directory)
       
   347         `uid` : int/long
       
   348           The user's UID (commonly AccountObj.uid)
       
   349         `gid` : int/long
       
   350           The user's GID (commonly AccountObj.gid)
       
   351         """
       
   352         assert all(isinstance(xid, (long, int)) for xid in (uid, gid)) and \
       
   353                 isinstance(domdir, basestring)
       
   354         if uid < MIN_UID or gid < MIN_GID:
       
   355             raise VMMError(_(u"UID '%(uid)u' and/or GID '%(gid)u' are less "
       
   356                              u"than %(min_uid)u/%(min_gid)u.") % {'uid': uid,
       
   357                            'gid': gid, 'min_gid': MIN_GID, 'min_uid': MIN_UID},
       
   358                            MAILDIR_PERM_MISMATCH)
       
   359         if domdir.count('..'):
       
   360             raise VMMError(_(u'Found ".." in domain directory path: %s') %
       
   361                            domdir, FOUND_DOTS_IN_PATH)
       
   362         if not lisdir(domdir):
       
   363             raise VMMError(_(u"No such directory: %s") % domdir,
       
   364                            NO_SUCH_DIRECTORY)
       
   365         os.chdir(domdir)
       
   366         userdir = '%s' % uid
       
   367         if not lisdir(userdir):
       
   368             self._warnings.append(_(u"No such directory: %s") %
       
   369                                   os.path.join(domdir, userdir))
       
   370             return
       
   371         mdstat = os.lstat(userdir)
       
   372         if (mdstat.st_uid, mdstat.st_gid) != (uid, gid):
       
   373             raise VMMError(_(u'Detected owner/group mismatch in home '
       
   374                              u'directory.'), MAILDIR_PERM_MISMATCH)
       
   375         rmtree(userdir, ignore_errors=True)
       
   376 
       
   377     def _delete_domain_dir(self, domdir, gid):
       
   378         """Delete a domain's directory.
       
   379 
       
   380         Arguments:
       
   381 
       
   382         `domdir` : basestring
       
   383           The domain's directory (commonly DomainObj.directory)
       
   384         `gid` : int/long
       
   385           The domain's GID (commonly DomainObj.gid)
       
   386         """
       
   387         assert isinstance(domdir, basestring) and isinstance(gid, (long, int))
       
   388         if gid < MIN_GID:
       
   389             raise VMMError(_(u"GID '%(gid)u' is less than '%(min_gid)u'.") %
       
   390                            {'gid': gid, 'min_gid': MIN_GID},
       
   391                            DOMAINDIR_GROUP_MISMATCH)
       
   392         if domdir.count('..'):
       
   393             raise VMMError(_(u'Found ".." in domain directory path: %s') %
       
   394                            domdir, FOUND_DOTS_IN_PATH)
       
   395         if not lisdir(domdir):
       
   396             self._warnings.append(_('No such directory: %s') % domdir)
       
   397             return
       
   398         dirst = os.lstat(domdir)
       
   399         if dirst.st_gid != gid:
       
   400             raise VMMError(_(u'Detected group mismatch in domain directory: '
       
   401                              u'%s') % domdir, DOMAINDIR_GROUP_MISMATCH)
       
   402         rmtree(domdir, ignore_errors=True)
       
   403 
       
   404     def has_warnings(self):
       
   405         """Checks if warnings are present, returns bool."""
       
   406         return bool(len(self._warnings))
       
   407 
       
   408     def get_warnings(self):
       
   409         """Returns a list with all available warnings and resets all
       
   410         warnings.
       
   411         """
       
   412         ret_val = self._warnings[:]
       
   413         del self._warnings[:]
       
   414         return ret_val
       
   415 
       
   416     def cfg_dget(self, option):
       
   417         """Get the configured value of the *option* (section.option).
       
   418         When the option was not configured its default value will be
       
   419         returned."""
       
   420         return self._cfg.dget(option)
       
   421 
       
   422     def cfg_pget(self, option):
       
   423         """Get the configured value of the *option* (section.option)."""
       
   424         return self._cfg.pget(option)
       
   425 
       
   426     def cfg_install(self):
       
   427         """Installs the cfg_dget method as ``cfg_dget`` into the built-in
       
   428         namespace."""
       
   429         import __builtin__
       
   430         assert 'cfg_dget' not in __builtin__.__dict__
       
   431         __builtin__.__dict__['cfg_dget'] = self._cfg.dget
       
   432 
       
   433     def domain_add(self, domainname, transport=None):
       
   434         """Wrapper around Domain's set_quotalimit, set_transport and save."""
       
   435         dom = self._get_domain(domainname)
       
   436         if transport is None:
       
   437             dom.set_transport(Transport(self._dbh,
       
   438                               transport=self._cfg.dget('domain.transport')))
       
   439         else:
       
   440             dom.set_transport(Transport(self._dbh, transport=transport))
       
   441         dom.set_quotalimit(QuotaLimit(self._dbh,
       
   442                            bytes=long(self._cfg.dget('domain.quota_bytes')),
       
   443                            messages=self._cfg.dget('domain.quota_messages')))
       
   444         dom.set_serviceset(ServiceSet(self._dbh,
       
   445                                       imap=self._cfg.dget('domain.imap'),
       
   446                                       pop3=self._cfg.dget('domain.pop3'),
       
   447                                       sieve=self._cfg.dget('domain.sieve'),
       
   448                                       smtp=self._cfg.dget('domain.smtp')))
       
   449         dom.set_directory(self._cfg.dget('misc.base_directory'))
       
   450         dom.save()
       
   451         self._make_domain_dir(dom)
       
   452 
       
   453     def domain_quotalimit(self, domainname, bytes_, messages=0, force=None):
       
   454         """Wrapper around Domain.update_quotalimit()."""
       
   455         if not all(isinstance(i, (int, long)) for i in (bytes_, messages)):
       
   456             raise TypeError("'bytes_' and 'messages' have to be "
       
   457                             "integers or longs.")
       
   458         if force is not None and force != 'force':
       
   459             raise DomainError(_(u"Invalid argument: '%s'") % force,
       
   460                               INVALID_ARGUMENT)
       
   461         dom = self._get_domain(domainname)
       
   462         quotalimit = QuotaLimit(self._dbh, bytes=bytes_, messages=messages)
       
   463         if force is None:
       
   464             dom.update_quotalimit(quotalimit)
       
   465         else:
       
   466             dom.update_quotalimit(quotalimit, force=True)
       
   467 
       
   468     def domain_services(self, domainname, force=None, *services):
       
   469         """Wrapper around Domain.update_serviceset()."""
       
   470         kwargs = dict.fromkeys(SERVICES, False)
       
   471         if force is not None and force != 'force':
       
   472             raise DomainError(_(u"Invalid argument: '%s'") % force,
       
   473                               INVALID_ARGUMENT)
       
   474         for service in set(services):
       
   475             if service not in SERVICES:
       
   476                 raise DomainError(_(u"Unknown service: '%s'") % service,
       
   477                                   UNKNOWN_SERVICE)
       
   478             kwargs[service] = True
       
   479 
       
   480         dom = self._get_domain(domainname)
       
   481         serviceset = ServiceSet(self._dbh, **kwargs)
       
   482         dom.update_serviceset(serviceset, (True, False)[not force])
       
   483 
       
   484     def domain_transport(self, domainname, transport, force=None):
       
   485         """Wrapper around Domain.update_transport()"""
       
   486         if force is not None and force != 'force':
       
   487             raise DomainError(_(u"Invalid argument: '%s'") % force,
       
   488                               INVALID_ARGUMENT)
       
   489         dom = self._get_domain(domainname)
       
   490         trsp = Transport(self._dbh, transport=transport)
       
   491         if force is None:
       
   492             dom.update_transport(trsp)
       
   493         else:
       
   494             dom.update_transport(trsp, force=True)
       
   495 
       
   496     def domain_note(self, domainname, note):
       
   497         """Wrapper around Domain.update_note()"""
       
   498         dom = self._get_domain(domainname)
       
   499         dom.update_note(note)
       
   500 
       
   501     def domain_delete(self, domainname, force=False):
       
   502         """Wrapper around Domain.delete()"""
       
   503         if not isinstance(force, bool):
       
   504             raise TypeError('force must be a bool')
       
   505         dom = self._get_domain(domainname)
       
   506         gid = dom.gid
       
   507         domdir = dom.directory
       
   508         if self._cfg.dget('domain.force_deletion') or force:
       
   509             dom.delete(True)
       
   510         else:
       
   511             dom.delete(False)
       
   512         if self._cfg.dget('domain.delete_directory'):
       
   513             self._delete_domain_dir(domdir, gid)
       
   514 
       
   515     def domain_info(self, domainname, details=None):
       
   516         """Wrapper around Domain.get_info(), Domain.get_accounts(),
       
   517         Domain.get_aliase_names(), Domain.get_aliases() and
       
   518         Domain.get_relocated."""
       
   519         if details not in [None, 'accounts', 'aliasdomains', 'aliases', 'full',
       
   520                            'relocated', 'catchall']:
       
   521             raise VMMError(_(u"Invalid argument: '%s'") % details,
       
   522                            INVALID_ARGUMENT)
       
   523         dom = self._get_domain(domainname)
       
   524         dominfo = dom.get_info()
       
   525         if dominfo['domain name'].startswith('xn--') or \
       
   526            dominfo['domain name'].count('.xn--'):
       
   527             dominfo['domain name'] += ' (%s)' % \
       
   528                                       dominfo['domain name'].decode('idna')
       
   529         if details is None:
       
   530             return dominfo
       
   531         elif details == 'accounts':
       
   532             return (dominfo, dom.get_accounts())
       
   533         elif details == 'aliasdomains':
       
   534             return (dominfo, dom.get_aliase_names())
       
   535         elif details == 'aliases':
       
   536             return (dominfo, dom.get_aliases())
       
   537         elif details == 'relocated':
       
   538             return(dominfo, dom.get_relocated())
       
   539         elif details == 'catchall':
       
   540             return(dominfo, dom.get_catchall())
       
   541         else:
       
   542             return (dominfo, dom.get_aliase_names(), dom.get_accounts(),
       
   543                     dom.get_aliases(), dom.get_relocated(), dom.get_catchall())
       
   544 
       
   545     def aliasdomain_add(self, aliasname, domainname):
       
   546         """Adds an alias domain to the domain.
       
   547 
       
   548         Arguments:
       
   549 
       
   550         `aliasname` : basestring
       
   551           The name of the alias domain
       
   552         `domainname` : basestring
       
   553           The name of the target domain
       
   554         """
       
   555         dom = self._get_domain(domainname)
       
   556         alias_dom = AliasDomain(self._dbh, aliasname)
       
   557         alias_dom.set_destination(dom)
       
   558         alias_dom.save()
       
   559 
       
   560     def aliasdomain_info(self, aliasname):
       
   561         """Returns a dict (keys: "alias" and "domain") with the names of
       
   562         the alias domain and its primary domain."""
       
   563         self._db_connect()
       
   564         alias_dom = AliasDomain(self._dbh, aliasname)
       
   565         return alias_dom.info()
       
   566 
       
   567     def aliasdomain_switch(self, aliasname, domainname):
       
   568         """Modifies the target domain of an existing alias domain.
       
   569 
       
   570         Arguments:
       
   571 
       
   572         `aliasname` : basestring
       
   573           The name of the alias domain
       
   574         `domainname` : basestring
       
   575           The name of the new target domain
       
   576         """
       
   577         dom = self._get_domain(domainname)
       
   578         alias_dom = AliasDomain(self._dbh, aliasname)
       
   579         alias_dom.set_destination(dom)
       
   580         alias_dom.switch()
       
   581 
       
   582     def aliasdomain_delete(self, aliasname):
       
   583         """Deletes the given alias domain.
       
   584 
       
   585         Argument:
       
   586 
       
   587         `aliasname` : basestring
       
   588           The name of the alias domain
       
   589         """
       
   590         self._db_connect()
       
   591         alias_dom = AliasDomain(self._dbh, aliasname)
       
   592         alias_dom.delete()
       
   593 
       
   594     def domain_list(self, pattern=None):
       
   595         """Wrapper around function search() from module Domain."""
       
   596         from VirtualMailManager.domain import search
       
   597         like = False
       
   598         if pattern and (pattern.startswith('%') or pattern.endswith('%')):
       
   599             like = True
       
   600             if not re.match(RE_DOMAIN_SEARCH, pattern.strip('%')):
       
   601                 raise VMMError(_(u"The pattern '%s' contains invalid "
       
   602                                  u"characters.") % pattern, DOMAIN_INVALID)
       
   603         self._db_connect()
       
   604         return search(self._dbh, pattern=pattern, like=like)
       
   605 
       
   606     def address_list(self, typelimit, pattern=None):
       
   607         """TODO"""
       
   608         llike = dlike = False
       
   609         lpattern = dpattern = None
       
   610         if pattern:
       
   611             parts = pattern.split('@', 2)
       
   612             if len(parts) == 2:
       
   613                 # The pattern includes '@', so let's treat the
       
   614                 # parts separately to allow for pattern search like %@domain.%
       
   615                 lpattern = parts[0]
       
   616                 llike = lpattern.startswith('%') or lpattern.endswith('%')
       
   617                 dpattern = parts[1]
       
   618                 dlike = dpattern.startswith('%') or dpattern.endswith('%')
       
   619 
       
   620                 if llike:
       
   621                     checkp = lpattern.strip('%')
       
   622                 else:
       
   623                     checkp = lpattern
       
   624                 if len(checkp) > 0 and re.search(RE_LOCALPART, checkp):
       
   625                     raise VMMError(_(u"The pattern '%s' contains invalid "
       
   626                                      u"characters.") % pattern,
       
   627                                    LOCALPART_INVALID)
       
   628             else:
       
   629                 # else just match on domains
       
   630                 # (or should that be local part, I don't know…)
       
   631                 dpattern = parts[0]
       
   632                 dlike = dpattern.startswith('%') or dpattern.endswith('%')
       
   633 
       
   634             if dlike:
       
   635                 checkp = dpattern.strip('%')
       
   636             else:
       
   637                 checkp = dpattern
       
   638             if len(checkp) > 0 and not re.match(RE_DOMAIN_SEARCH, checkp):
       
   639                 raise VMMError(_(u"The pattern '%s' contains invalid "
       
   640                                  u"characters.") % pattern, DOMAIN_INVALID)
       
   641         self._db_connect()
       
   642         from VirtualMailManager.common import search_addresses
       
   643         return search_addresses(self._dbh, typelimit=typelimit,
       
   644                                 lpattern=lpattern, llike=llike,
       
   645                                 dpattern=dpattern, dlike=dlike)
       
   646 
       
   647     def user_add(self, emailaddress, password):
       
   648         """Wrapper around Account.set_password() and Account.save()."""
       
   649         acc = self._get_account(emailaddress)
       
   650         if acc:
       
   651             raise VMMError(_(u"The account '%s' already exists.") %
       
   652                            acc.address, ACCOUNT_EXISTS)
       
   653         self._is_other_address(acc.address, TYPE_ACCOUNT)
       
   654         acc.set_password(password)
       
   655         acc.save()
       
   656         self._make_account_dirs(acc)
       
   657 
       
   658     def alias_add(self, aliasaddress, *targetaddresses):
       
   659         """Creates a new `Alias` entry for the given *aliasaddress* with
       
   660         the given *targetaddresses*."""
       
   661         alias = self._get_alias(aliasaddress)
       
   662         if not alias:
       
   663             self._is_other_address(alias.address, TYPE_ALIAS)
       
   664         destinations = [DestinationEmailAddress(addr, self._dbh)
       
   665                         for addr in targetaddresses]
       
   666         warnings = []
       
   667         destinations = alias.add_destinations(destinations, warnings)
       
   668         if warnings:
       
   669             self._warnings.append(_('Ignored destination addresses:'))
       
   670             self._warnings.extend(('  * %s' % w for w in warnings))
       
   671         for destination in destinations:
       
   672             if destination.gid and \
       
   673                not self._chk_other_address_types(destination, TYPE_RELOCATED):
       
   674                 self._warnings.append(_(u"The destination account/alias '%s' "
       
   675                                         u"does not exist.") % destination)
       
   676 
       
   677     def user_delete(self, emailaddress, force=False):
       
   678         """Wrapper around Account.delete(...)"""
       
   679         if not isinstance(force, bool):
       
   680             raise TypeError('force must be a bool')
       
   681         acc = self._get_account(emailaddress)
       
   682         if not acc:
       
   683             raise VMMError(_(u"The account '%s' does not exist.") %
       
   684                            acc.address, NO_SUCH_ACCOUNT)
       
   685         uid = acc.uid
       
   686         gid = acc.gid
       
   687         dom_dir = acc.domain.directory
       
   688         acc_dir = acc.home
       
   689         acc.delete(force)
       
   690         if self._cfg.dget('account.delete_directory'):
       
   691             try:
       
   692                 self._delete_home(dom_dir, uid, gid)
       
   693             except VMMError, err:
       
   694                 if err.code in (FOUND_DOTS_IN_PATH, MAILDIR_PERM_MISMATCH,
       
   695                                 NO_SUCH_DIRECTORY):
       
   696                     warning = _(u"""\
       
   697 The account has been successfully deleted from the database.
       
   698     But an error occurred while deleting the following directory:
       
   699     '%(directory)s'
       
   700     Reason: %(reason)s""") % {'directory': acc_dir, 'reason': err.msg}
       
   701                     self._warnings.append(warning)
       
   702                 else:
       
   703                     raise
       
   704 
       
   705     def alias_info(self, aliasaddress):
       
   706         """Returns an iterator object for all destinations (`EmailAddress`
       
   707         instances) for the `Alias` with the given *aliasaddress*."""
       
   708         alias = self._get_alias(aliasaddress)
       
   709         if alias:
       
   710             return alias.get_destinations()
       
   711         if not self._is_other_address(alias.address, TYPE_ALIAS):
       
   712             raise VMMError(_(u"The alias '%s' does not exist.") %
       
   713                            alias.address, NO_SUCH_ALIAS)
       
   714 
       
   715     def alias_delete(self, aliasaddress, targetaddresses=None):
       
   716         """Deletes the `Alias` *aliasaddress* with all its destinations from
       
   717         the database. If *targetaddresses* is not ``None``, only the given
       
   718         destinations will be removed from the alias."""
       
   719         alias = self._get_alias(aliasaddress)
       
   720         error = None
       
   721         if targetaddresses is None:
       
   722             alias.delete()
       
   723         else:
       
   724             destinations = [DestinationEmailAddress(addr, self._dbh)
       
   725                             for addr in targetaddresses]
       
   726             warnings = []
       
   727             try:
       
   728                 alias.del_destinations(destinations, warnings)
       
   729             except VMMError, err:
       
   730                 error = err
       
   731             if warnings:
       
   732                 self._warnings.append(_('Ignored destination addresses:'))
       
   733                 self._warnings.extend(('  * %s' % w for w in warnings))
       
   734             if error:
       
   735                 raise error
       
   736 
       
   737     def catchall_add(self, domain, *targetaddresses):
       
   738         """Creates a new `CatchallAlias` entry for the given *domain* with
       
   739         the given *targetaddresses*."""
       
   740         catchall = self._get_catchall(domain)
       
   741         destinations = [DestinationEmailAddress(addr, self._dbh)
       
   742                         for addr in targetaddresses]
       
   743         warnings = []
       
   744         destinations = catchall.add_destinations(destinations, warnings)
       
   745         if warnings:
       
   746             self._warnings.append(_('Ignored destination addresses:'))
       
   747             self._warnings.extend(('  * %s' % w for w in warnings))
       
   748         for destination in destinations:
       
   749             if destination.gid and \
       
   750                not self._chk_other_address_types(destination, TYPE_RELOCATED):
       
   751                 self._warnings.append(_(u"The destination account/alias '%s' "
       
   752                                         u"does not exist.") % destination)
       
   753 
       
   754     def catchall_info(self, domain):
       
   755         """Returns an iterator object for all destinations (`EmailAddress`
       
   756         instances) for the `CatchallAlias` with the given *domain*."""
       
   757         return self._get_catchall(domain).get_destinations()
       
   758 
       
   759     def catchall_delete(self, domain, targetaddresses=None):
       
   760         """Deletes the `CatchallAlias` for domain *domain* with all its
       
   761         destinations from the database.  If *targetaddresses* is not
       
   762         ``None``,  only those destinations will be removed from the alias."""
       
   763         catchall = self._get_catchall(domain)
       
   764         error = None
       
   765         if targetaddresses is None:
       
   766             catchall.delete()
       
   767         else:
       
   768             destinations = [DestinationEmailAddress(addr, self._dbh)
       
   769                             for addr in targetaddresses]
       
   770             warnings = []
       
   771             try:
       
   772                 catchall.del_destinations(destinations, warnings)
       
   773             except VMMError, err:
       
   774                 error = err
       
   775             if warnings:
       
   776                 self._warnings.append(_('Ignored destination addresses:'))
       
   777                 self._warnings.extend(('  * %s' % w for w in warnings))
       
   778             if error:
       
   779                 raise error
       
   780 
       
   781     def user_info(self, emailaddress, details=None):
       
   782         """Wrapper around Account.get_info(...)"""
       
   783         if details not in (None, 'du', 'aliases', 'full'):
       
   784             raise VMMError(_(u"Invalid argument: '%s'") % details,
       
   785                            INVALID_ARGUMENT)
       
   786         acc = self._get_account(emailaddress)
       
   787         if not acc:
       
   788             if not self._is_other_address(acc.address, TYPE_ACCOUNT):
       
   789                 raise VMMError(_(u"The account '%s' does not exist.") %
       
   790                                acc.address, NO_SUCH_ACCOUNT)
       
   791         info = acc.get_info()
       
   792         if self._cfg.dget('account.disk_usage') or details in ('du', 'full'):
       
   793             path = os.path.join(acc.home, acc.mail_location.directory)
       
   794             info['disk usage'] = self._get_disk_usage(path)
       
   795             if details in (None, 'du'):
       
   796                 return info
       
   797         if details in ('aliases', 'full'):
       
   798             return (info, acc.get_aliases())
       
   799         return info
       
   800 
       
   801     def user_by_uid(self, uid):
       
   802         """Search for an Account by its *uid*.
       
   803         Returns a dict (address, uid and gid) if a user could be found."""
       
   804         from VirtualMailManager.account import get_account_by_uid
       
   805         self._db_connect()
       
   806         return get_account_by_uid(uid, self._dbh)
       
   807 
       
   808     def user_password(self, emailaddress, password):
       
   809         """Wrapper for Account.modify('password' ...)."""
       
   810         if not isinstance(password, basestring) or not password:
       
   811             raise VMMError(_(u"Could not accept password: '%s'") % password,
       
   812                            INVALID_ARGUMENT)
       
   813         acc = self._get_account(emailaddress)
       
   814         if not acc:
       
   815             raise VMMError(_(u"The account '%s' does not exist.") %
       
   816                            acc.address, NO_SUCH_ACCOUNT)
       
   817         acc.modify('password', password)
       
   818 
       
   819     def user_name(self, emailaddress, name):
       
   820         """Wrapper for Account.modify('name', ...)."""
       
   821         acc = self._get_account(emailaddress)
       
   822         if not acc:
       
   823             raise VMMError(_(u"The account '%s' does not exist.") %
       
   824                            acc.address, NO_SUCH_ACCOUNT)
       
   825         acc.modify('name', name)
       
   826 
       
   827     def user_note(self, emailaddress, note):
       
   828         """Wrapper for Account.modify('note', ...)."""
       
   829         acc = self._get_account(emailaddress)
       
   830         if not acc:
       
   831             raise VMMError(_(u"The account '%s' does not exist.") %
       
   832                            acc.address, NO_SUCH_ACCOUNT)
       
   833         acc.modify('note', note)
       
   834 
       
   835     def user_quotalimit(self, emailaddress, bytes_, messages=0):
       
   836         """Wrapper for Account.update_quotalimit(QuotaLimit)."""
       
   837         acc = self._get_account(emailaddress)
       
   838         if not acc:
       
   839             raise VMMError(_(u"The account '%s' does not exist.") %
       
   840                         acc.address, NO_SUCH_ACCOUNT)
       
   841         if bytes_ == 'domain':
       
   842             quotalimit = None
       
   843         else:
       
   844             if not all(isinstance(i, (int, long)) for i in (bytes_, messages)):
       
   845                 raise TypeError("'bytes_' and 'messages' have to be "
       
   846                                 "integers or longs.")
       
   847             quotalimit = QuotaLimit(self._dbh, bytes=bytes_,
       
   848                                     messages=messages)
       
   849         acc.update_quotalimit(quotalimit)
       
   850 
       
   851     def user_transport(self, emailaddress, transport):
       
   852         """Wrapper for Account.update_transport(Transport)."""
       
   853         if not isinstance(transport, basestring) or not transport:
       
   854             raise VMMError(_(u"Could not accept transport: '%s'") % transport,
       
   855                            INVALID_ARGUMENT)
       
   856         acc = self._get_account(emailaddress)
       
   857         if not acc:
       
   858             raise VMMError(_(u"The account '%s' does not exist.") %
       
   859                            acc.address, NO_SUCH_ACCOUNT)
       
   860         if transport == 'domain':
       
   861             transport = None
       
   862         else:
       
   863             transport = Transport(self._dbh, transport=transport)
       
   864         acc.update_transport(transport)
       
   865 
       
   866     def user_services(self, emailaddress, *services):
       
   867         """Wrapper around Account.update_serviceset()."""
       
   868         acc = self._get_account(emailaddress)
       
   869         if not acc:
       
   870             raise VMMError(_(u"The account '%s' does not exist.") %
       
   871                         acc.address, NO_SUCH_ACCOUNT)
       
   872         if len(services) == 1 and services[0] == 'domain':
       
   873             serviceset = None
       
   874         else:
       
   875             kwargs = dict.fromkeys(SERVICES, False)
       
   876             for service in set(services):
       
   877                 if service not in SERVICES:
       
   878                     raise VMMError(_(u"Unknown service: '%s'") % service,
       
   879                                 UNKNOWN_SERVICE)
       
   880                 kwargs[service] = True
       
   881             serviceset = ServiceSet(self._dbh, **kwargs)
       
   882         acc.update_serviceset(serviceset)
       
   883 
       
   884     def relocated_add(self, emailaddress, targetaddress):
       
   885         """Creates a new `Relocated` entry in the database. If there is
       
   886         already a relocated user with the given *emailaddress*, only the
       
   887         *targetaddress* for the relocated user will be updated."""
       
   888         relocated = self._get_relocated(emailaddress)
       
   889         if not relocated:
       
   890             self._is_other_address(relocated.address, TYPE_RELOCATED)
       
   891         destination = DestinationEmailAddress(targetaddress, self._dbh)
       
   892         relocated.set_destination(destination)
       
   893         if destination.gid and \
       
   894            not self._chk_other_address_types(destination, TYPE_RELOCATED):
       
   895             self._warnings.append(_(u"The destination account/alias '%s' "
       
   896                                     u"does not exist.") % destination)
       
   897 
       
   898     def relocated_info(self, emailaddress):
       
   899         """Returns the target address of the relocated user with the given
       
   900         *emailaddress*."""
       
   901         relocated = self._get_relocated(emailaddress)
       
   902         if relocated:
       
   903             return relocated.get_info()
       
   904         if not self._is_other_address(relocated.address, TYPE_RELOCATED):
       
   905             raise VMMError(_(u"The relocated user '%s' does not exist.") %
       
   906                            relocated.address, NO_SUCH_RELOCATED)
       
   907 
       
   908     def relocated_delete(self, emailaddress):
       
   909         """Deletes the relocated user with the given *emailaddress* from
       
   910         the database."""
       
   911         relocated = self._get_relocated(emailaddress)
       
   912         relocated.delete()
       
   913 
       
   914 del _