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