VirtualMailManager/handler.py
branchv0.6.x
changeset 320 011066435e6f
parent 319 f4956b4ceba1
child 325 06c0457036a0
equal deleted inserted replaced
319:f4956b4ceba1 320:011066435e6f
       
     1 # -*- coding: UTF-8 -*-
       
     2 # Copyright (c) 2007 - 2010, Pascal Volk
       
     3 # See COPYING for distribution information.
       
     4 """
       
     5    VirtualMailManager.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 pyPgSQL import PgSQL  # python-pgsql - http://pypgsql.sourceforge.net
       
    22 
       
    23 from VirtualMailManager.account import Account
       
    24 from VirtualMailManager.alias import Alias
       
    25 from VirtualMailManager.aliasdomain import AliasDomain
       
    26 from VirtualMailManager.common import exec_ok
       
    27 from VirtualMailManager.config import Config as Cfg
       
    28 from VirtualMailManager.constants import \
       
    29      ACCOUNT_EXISTS, ALIAS_EXISTS, CONF_NOFILE, CONF_NOPERM, CONF_WRONGPERM, \
       
    30      DATABASE_ERROR, DOMAINDIR_GROUP_MISMATCH, DOMAIN_INVALID, \
       
    31      FOUND_DOTS_IN_PATH, INVALID_ARGUMENT, MAILDIR_PERM_MISMATCH, \
       
    32      NOT_EXECUTABLE, NO_SUCH_ACCOUNT, NO_SUCH_ALIAS, NO_SUCH_BINARY, \
       
    33      NO_SUCH_DIRECTORY, NO_SUCH_RELOCATED, RELOCATED_EXISTS
       
    34 from VirtualMailManager.domain import Domain, get_gid
       
    35 from VirtualMailManager.emailaddress import EmailAddress
       
    36 from VirtualMailManager.errors import \
       
    37      DomainError, NotRootError, PermissionError, VMMError
       
    38 from VirtualMailManager.mailbox import new as new_mailbox
       
    39 from VirtualMailManager.pycompat import any
       
    40 from VirtualMailManager.relocated import Relocated
       
    41 from VirtualMailManager.transport import Transport
       
    42 
       
    43 
       
    44 _ = lambda msg: msg
       
    45 
       
    46 CFG_FILE = 'vmm.cfg'
       
    47 CFG_PATH = '/root:/usr/local/etc:/etc'
       
    48 RE_DOMAIN_SEARCH = """^[a-z0-9-\.]+$"""
       
    49 TYPE_ACCOUNT = 0x1
       
    50 TYPE_ALIAS = 0x2
       
    51 TYPE_RELOCATED = 0x4
       
    52 OTHER_TYPES = {
       
    53     TYPE_ACCOUNT: (_(u'an account'), ACCOUNT_EXISTS),
       
    54     TYPE_ALIAS: (_(u'an alias'), ALIAS_EXISTS),
       
    55     TYPE_RELOCATED: (_(u'a relocated user'), RELOCATED_EXISTS),
       
    56 }
       
    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', '_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 
       
    79         if os.geteuid():
       
    80             raise NotRootError(_(u"You are not root.\n\tGood bye!\n"),
       
    81                                CONF_NOPERM)
       
    82         if self._check_cfg_file():
       
    83             self._cfg = Cfg(self._cfg_fname)
       
    84             self._cfg.load()
       
    85         if not skip_some_checks:
       
    86             self._cfg.check()
       
    87             self._chkenv()
       
    88 
       
    89     def _find_cfg_file(self):
       
    90         """Search the CFG_FILE in CFG_PATH.
       
    91         Raise a VMMError when no vmm.cfg could be found.
       
    92         """
       
    93         for path in CFG_PATH.split(':'):
       
    94             tmp = os.path.join(path, CFG_FILE)
       
    95             if os.path.isfile(tmp):
       
    96                 self._cfg_fname = tmp
       
    97                 break
       
    98         if not self._cfg_fname:
       
    99             raise VMMError(_(u"Could not find '%(cfg_file)s' in: "
       
   100                              u"'%(cfg_path)s'") % {'cfg_file': CFG_FILE,
       
   101                            'cfg_path': CFG_PATH}, CONF_NOFILE)
       
   102 
       
   103     def _check_cfg_file(self):
       
   104         """Checks the configuration file, returns bool"""
       
   105         self._find_cfg_file()
       
   106         fstat = os.stat(self._cfg_fname)
       
   107         fmode = int(oct(fstat.st_mode & 0777))
       
   108         if fmode % 100 and fstat.st_uid != fstat.st_gid or \
       
   109            fmode % 10 and fstat.st_uid == fstat.st_gid:
       
   110             raise PermissionError(_(u"wrong permissions for '%(file)s': "
       
   111                                     u"%(perms)s\n`chmod 0600 %(file)s` would "
       
   112                                     u"be great.") % {'file': self._cfg_fname,
       
   113                                   'perms': fmode}, CONF_WRONGPERM)
       
   114         else:
       
   115             return True
       
   116 
       
   117     def _chkenv(self):
       
   118         """Make sure our base_directory is a directory and that all
       
   119         required executables exists and are executable.
       
   120         If not, a VMMError will be raised"""
       
   121         basedir = self._cfg.dget('misc.base_directory')
       
   122         if not os.path.exists(basedir):
       
   123             old_umask = os.umask(0006)
       
   124             os.makedirs(basedir, 0771)
       
   125             os.chown(basedir, 0, 0)
       
   126             os.umask(old_umask)
       
   127         elif not os.path.isdir(basedir):
       
   128             raise VMMError(_(u"'%(path)s' is not a directory.\n(%(cfg_file)s: "
       
   129                              u"section 'misc', option 'base_directory')") %
       
   130                            {'path': basedir, 'cfg_file': self._cfg_fname},
       
   131                            NO_SUCH_DIRECTORY)
       
   132         for opt, val in self._cfg.items('bin'):
       
   133             try:
       
   134                 exec_ok(val)
       
   135             except VMMError, err:
       
   136                 if err.code is NO_SUCH_BINARY:
       
   137                     raise VMMError(_(u"'%(binary)s' doesn't exist.\n"
       
   138                                      u"(%(cfg_file)s: section 'bin', option "
       
   139                                      u"'%(option)s')") % {'binary': val,
       
   140                                    'cfg_file': self._cfg_fname, 'option': opt},
       
   141                                    err.code)
       
   142                 elif err.code is NOT_EXECUTABLE:
       
   143                     raise VMMError(_(u"'%(binary)s' is not executable.\n"
       
   144                                      u"(%(cfg_file)s: section 'bin', option "
       
   145                                      u"'%(option)s')") % {'binary': val,
       
   146                                    'cfg_file': self._cfg_fname, 'option': opt},
       
   147                                    err.code)
       
   148                 else:
       
   149                     raise
       
   150 
       
   151     def _db_connect(self):
       
   152         """Creates a pyPgSQL.PgSQL.connection instance."""
       
   153         if self._dbh is None or (isinstance(self._dbh, PgSQL.Connection) and
       
   154                                   not self._dbh._isOpen):
       
   155             try:
       
   156                 self._dbh = PgSQL.connect(
       
   157                         database=self._cfg.dget('database.name'),
       
   158                         user=self._cfg.pget('database.user'),
       
   159                         host=self._cfg.dget('database.host'),
       
   160                         password=self._cfg.pget('database.pass'),
       
   161                         client_encoding='utf8', unicode_results=True)
       
   162                 dbc = self._dbh.cursor()
       
   163                 dbc.execute("SET NAMES 'UTF8'")
       
   164                 dbc.close()
       
   165             except PgSQL.libpq.DatabaseError, err:
       
   166                 raise VMMError(str(err), DATABASE_ERROR)
       
   167 
       
   168     def _chk_other_address_types(self, address, exclude):
       
   169         """Checks if the EmailAddress *address* is known as `TYPE_ACCOUNT`,
       
   170         `TYPE_ALIAS` or `TYPE_RELOCATED`, but not as the `TYPE_*` specified
       
   171         by *exclude*.  If the *address* is known as one of the `TYPE_*`s
       
   172         the according `TYPE_*` constant will be returned.  Otherwise 0 will
       
   173         be returned."""
       
   174         assert exclude in (TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED) and \
       
   175                 isinstance(address, EmailAddress)
       
   176         if exclude is not TYPE_ACCOUNT:
       
   177             account = Account(self._dbh, address)
       
   178             if account:
       
   179                 return TYPE_ACCOUNT
       
   180         if exclude is not TYPE_ALIAS:
       
   181             alias = Alias(self._dbh, address)
       
   182             if alias:
       
   183                 return TYPE_ALIAS
       
   184         if exclude is not TYPE_RELOCATED:
       
   185             relocated = Relocated(self._dbh, address)
       
   186             if relocated:
       
   187                 return TYPE_RELOCATED
       
   188         return 0
       
   189 
       
   190     def _is_other_address(self, address, exclude):
       
   191         """Checks if *address* is known for an Account (TYPE_ACCOUNT),
       
   192         Alias (TYPE_ALIAS) or Relocated (TYPE_RELOCATED), except for
       
   193         *exclude*.  Returns `False` if the address is not known for other
       
   194         types.
       
   195 
       
   196         Raises a `VMMError` if the address is known.
       
   197         """
       
   198         other = self._chk_other_address_types(address, exclude)
       
   199         if not other:
       
   200             return False
       
   201         msg = _(u"There is already %(a_type)s with the address '%(address)s'.")
       
   202         raise VMMError(msg % {'a_type': OTHER_TYPES[other][0],
       
   203                               'address': address}, OTHER_TYPES[other][1])
       
   204 
       
   205     def _get_account(self, address):
       
   206         """Return an Account instances for the given address (str)."""
       
   207         address = EmailAddress(address)
       
   208         self._db_connect()
       
   209         return Account(self._dbh, address)
       
   210 
       
   211     def _get_alias(self, address):
       
   212         """Return an Alias instances for the given address (str)."""
       
   213         address = EmailAddress(address)
       
   214         self._db_connect()
       
   215         return Alias(self._dbh, address)
       
   216 
       
   217     def _get_relocated(self, address):
       
   218         """Return a Relocated instances for the given address (str)."""
       
   219         address = EmailAddress(address)
       
   220         self._db_connect()
       
   221         return Relocated(self._dbh, address)
       
   222 
       
   223     def _get_domain(self, domainname):
       
   224         """Return a Domain instances for the given domain name (str)."""
       
   225         self._db_connect()
       
   226         return Domain(self._dbh, domainname)
       
   227 
       
   228     def _get_disk_usage(self, directory):
       
   229         """Estimate file space usage for the given directory.
       
   230 
       
   231         Keyword arguments:
       
   232         directory -- the directory to summarize recursively disk usage for
       
   233         """
       
   234         if self._isdir(directory):
       
   235             return Popen([self._cfg.dget('bin.du'), "-hs", directory],
       
   236                 stdout=PIPE).communicate()[0].split('\t')[0]
       
   237         else:
       
   238             return 0
       
   239 
       
   240     def _isdir(self, directory):
       
   241         """Check if `directory` is a directory. Returns bool.
       
   242         When `directory` isn't a directory, a warning will be appended to
       
   243         _warnings."""
       
   244         isdir = os.path.isdir(directory)
       
   245         if not isdir:
       
   246             self._warnings.append(_('No such directory: %s') % directory)
       
   247         return isdir
       
   248 
       
   249     def _make_domain_dir(self, domain):
       
   250         """Create a directory for the `domain` and its accounts."""
       
   251         cwd = os.getcwd()
       
   252         hashdir, domdir = domain.directory.split(os.path.sep)[-2:]
       
   253         os.chdir(self._cfg.dget('misc.base_directory'))
       
   254         if not os.path.isdir(hashdir):
       
   255             os.mkdir(hashdir, 0711)
       
   256             os.chown(hashdir, 0, 0)
       
   257         os.mkdir(os.path.join(hashdir, domdir),
       
   258                  self._cfg.dget('domain.directory_mode'))
       
   259         os.chown(domain.directory, 0, domain.gid)
       
   260         os.chdir(cwd)
       
   261 
       
   262     def _make_home(self, account):
       
   263         """Create a home directory for the new Account *account*."""
       
   264         os.umask(0007)
       
   265         os.chdir(account.domain_directory)
       
   266         os.mkdir('%s' % account.uid, self._cfg.dget('account.directory_mode'))
       
   267         os.chown('%s' % account.uid, account.uid, account.gid)
       
   268 
       
   269     def _delete_home(self, domdir, uid, gid):
       
   270         """Delete a user's home directory."""
       
   271         if uid > 0 and gid > 0:
       
   272             userdir = '%s' % uid
       
   273             if userdir.count('..') or domdir.count('..'):
       
   274                 raise VMMError(_(u'Found ".." in home directory path.'),
       
   275                                FOUND_DOTS_IN_PATH)
       
   276             if os.path.isdir(domdir):
       
   277                 os.chdir(domdir)
       
   278                 if os.path.isdir(userdir):
       
   279                     mdstat = os.stat(userdir)
       
   280                     if (mdstat.st_uid, mdstat.st_gid) != (uid, gid):
       
   281                         raise VMMError(_(u'Detected owner/group mismatch in '
       
   282                                          u'home directory.'),
       
   283                                        MAILDIR_PERM_MISMATCH)
       
   284                     rmtree(userdir, ignore_errors=True)
       
   285                 else:
       
   286                     raise VMMError(_(u"No such directory: %s") %
       
   287                                    os.path.join(domdir, userdir),
       
   288                                    NO_SUCH_DIRECTORY)
       
   289 
       
   290     def _delete_domain_dir(self, domdir, gid):
       
   291         """Delete a domain's directory."""
       
   292         if gid > 0:
       
   293             if not self._isdir(domdir):
       
   294                 return
       
   295             basedir = self._cfg.dget('misc.base_directory')
       
   296             domdirdirs = domdir.replace(basedir + '/', '').split('/')
       
   297             domdirparent = os.path.join(basedir, domdirdirs[0])
       
   298             if basedir.count('..') or domdir.count('..'):
       
   299                 raise VMMError(_(u'Found ".." in domain directory path.'),
       
   300                                FOUND_DOTS_IN_PATH)
       
   301             if os.path.isdir(domdirparent):
       
   302                 os.chdir(domdirparent)
       
   303                 if os.lstat(domdirdirs[1]).st_gid != gid:
       
   304                     raise VMMError(_(u'Detected group mismatch in domain '
       
   305                                      u'directory.'), DOMAINDIR_GROUP_MISMATCH)
       
   306                 rmtree(domdirdirs[1], ignore_errors=True)
       
   307 
       
   308     def has_warnings(self):
       
   309         """Checks if warnings are present, returns bool."""
       
   310         return bool(len(self._warnings))
       
   311 
       
   312     def get_warnings(self):
       
   313         """Returns a list with all available warnings and resets all
       
   314         warnings.
       
   315         """
       
   316         ret_val = self._warnings[:]
       
   317         del self._warnings[:]
       
   318         return ret_val
       
   319 
       
   320     def cfg_dget(self, option):
       
   321         """Get the configured value of the *option* (section.option).
       
   322         When the option was not configured its default value will be
       
   323         returned."""
       
   324         return self._cfg.dget(option)
       
   325 
       
   326     def cfg_pget(self, option):
       
   327         """Get the configured value of the *option* (section.option)."""
       
   328         return self._cfg.pget(option)
       
   329 
       
   330     def cfg_install(self):
       
   331         """Installs the cfg_dget method as ``cfg_dget`` into the built-in
       
   332         namespace."""
       
   333         import __builtin__
       
   334         assert 'cfg_dget' not in __builtin__.__dict__
       
   335         __builtin__.__dict__['cfg_dget'] = self._cfg.dget
       
   336 
       
   337     def domain_add(self, domainname, transport=None):
       
   338         """Wrapper around Domain.set_transport() and Domain.save()"""
       
   339         dom = self._get_domain(domainname)
       
   340         if transport is None:
       
   341             dom.set_transport(Transport(self._dbh,
       
   342                               transport=self._cfg.dget('misc.transport')))
       
   343         else:
       
   344             dom.set_transport(Transport(self._dbh, transport=transport))
       
   345         dom.set_directory(self._cfg.dget('misc.base_directory'))
       
   346         dom.save()
       
   347         self._make_domain_dir(dom)
       
   348 
       
   349     def domain_transport(self, domainname, transport, force=None):
       
   350         """Wrapper around Domain.update_transport()"""
       
   351         if force is not None and force != 'force':
       
   352             raise DomainError(_(u"Invalid argument: '%s'") % force,
       
   353                               INVALID_ARGUMENT)
       
   354         dom = self._get_domain(domainname)
       
   355         trsp = Transport(self._dbh, transport=transport)
       
   356         if force is None:
       
   357             dom.update_transport(trsp)
       
   358         else:
       
   359             dom.update_transport(trsp, force=True)
       
   360 
       
   361     def domain_delete(self, domainname, force=None):
       
   362         """Wrapper around Domain.delete()"""
       
   363         if force and force not in ('deluser', 'delalias', 'delall'):
       
   364             raise DomainError(_(u"Invalid argument: '%s'") % force,
       
   365                               INVALID_ARGUMENT)
       
   366         dom = self._get_domain(domainname)
       
   367         gid = dom.gid
       
   368         domdir = dom.directory
       
   369         if self._cfg.dget('domain.force_deletion') or force == 'delall':
       
   370             dom.delete(True, True)
       
   371         elif force == 'deluser':
       
   372             dom.delete(deluser=True)
       
   373         elif force == 'delalias':
       
   374             dom.delete(delalias=True)
       
   375         else:
       
   376             dom.delete()
       
   377         if self._cfg.dget('domain.delete_directory'):
       
   378             self._delete_domain_dir(domdir, gid)
       
   379 
       
   380     def domain_info(self, domainname, details=None):
       
   381         """Wrapper around Domain.get_info(), Domain.get_accounts(),
       
   382         Domain.get_aliase_names(), Domain.get_aliases() and
       
   383         Domain.get_relocated."""
       
   384         if details not in [None, 'accounts', 'aliasdomains', 'aliases', 'full',
       
   385                            'relocated']:
       
   386             raise VMMError(_(u'Invalid argument: ā€œ%sā€') % details,
       
   387                            INVALID_ARGUMENT)
       
   388         dom = self._get_domain(domainname)
       
   389         dominfo = dom.get_info()
       
   390         if dominfo['domainname'].startswith('xn--'):
       
   391             dominfo['domainname'] += ' (%s)' % \
       
   392                                      dominfo['domainname'].decode('idna')
       
   393         if details is None:
       
   394             return dominfo
       
   395         elif details == 'accounts':
       
   396             return (dominfo, dom.get_accounts())
       
   397         elif details == 'aliasdomains':
       
   398             return (dominfo, dom.get_aliase_names())
       
   399         elif details == 'aliases':
       
   400             return (dominfo, dom.get_aliases())
       
   401         elif details == 'relocated':
       
   402             return(dominfo, dom.get_relocated())
       
   403         else:
       
   404             return (dominfo, dom.get_aliase_names(), dom.get_accounts(),
       
   405                     dom.get_aliases(), dom.get_relocated())
       
   406 
       
   407     def aliasdomain_add(self, aliasname, domainname):
       
   408         """Adds an alias domain to the domain.
       
   409 
       
   410         Arguments:
       
   411 
       
   412         `aliasname` : basestring
       
   413           The name of the alias domain
       
   414         `domainname` : basestring
       
   415           The name of the target domain
       
   416         """
       
   417         dom = self._get_domain(domainname)
       
   418         alias_dom = AliasDomain(self._dbh, aliasname)
       
   419         alias_dom.set_destination(dom)
       
   420         alias_dom.save()
       
   421 
       
   422     def aliasdomain_info(self, aliasname):
       
   423         """Returns a dict (keys: "alias" and "domain") with the names of
       
   424         the alias domain and its primary domain."""
       
   425         self._db_connect()
       
   426         alias_dom = AliasDomain(self._dbh, aliasname)
       
   427         return alias_dom.info()
       
   428 
       
   429     def aliasdomain_switch(self, aliasname, domainname):
       
   430         """Modifies the target domain of an existing alias domain.
       
   431 
       
   432         Arguments:
       
   433 
       
   434         `aliasname` : basestring
       
   435           The name of the alias domain
       
   436         `domainname` : basestring
       
   437           The name of the new target domain
       
   438         """
       
   439         dom = self._get_domain(domainname)
       
   440         alias_dom = AliasDomain(self._dbh, aliasname)
       
   441         alias_dom.set_destination(dom)
       
   442         alias_dom.switch()
       
   443 
       
   444     def aliasdomain_delete(self, aliasname):
       
   445         """Deletes the given alias domain.
       
   446 
       
   447         Argument:
       
   448 
       
   449         `aliasname` : basestring
       
   450           The name of the alias domain
       
   451         """
       
   452         self._db_connect()
       
   453         alias_dom = AliasDomain(self._dbh, aliasname)
       
   454         alias_dom.delete()
       
   455 
       
   456     def domain_list(self, pattern=None):
       
   457         """Wrapper around function search() from module Domain."""
       
   458         from VirtualMailManager.domain import search
       
   459         like = False
       
   460         if pattern and (pattern.startswith('%') or pattern.endswith('%')):
       
   461             like = True
       
   462             if not re.match(RE_DOMAIN_SEARCH, pattern.strip('%')):
       
   463                 raise VMMError(_(u"The pattern '%s' contains invalid "
       
   464                                  u"characters.") % pattern, DOMAIN_INVALID)
       
   465         self._db_connect()
       
   466         return search(self._dbh, pattern=pattern, like=like)
       
   467 
       
   468     def user_add(self, emailaddress, password):
       
   469         """Wrapper around Account.set_password() and Account.save()."""
       
   470         acc = self._get_account(emailaddress)
       
   471         acc.set_password(password)
       
   472         acc.save()
       
   473         oldpwd = os.getcwd()
       
   474         self._make_home(acc)
       
   475         mailbox = new_mailbox(acc)
       
   476         mailbox.create()
       
   477         folders = self._cfg.dget('mailbox.folders').split(':')
       
   478         if any(folders):
       
   479             bad = mailbox.add_boxes(folders,
       
   480                                     self._cfg.dget('mailbox.subscribe'))
       
   481             if bad:
       
   482                 self._warnings.append(_(u"Skipped mailbox folders:") +
       
   483                                       '\n\t- ' + '\n\t- '.join(bad))
       
   484         os.chdir(oldpwd)
       
   485 
       
   486     def alias_add(self, aliasaddress, *targetaddresses):
       
   487         """Creates a new `Alias` entry for the given *aliasaddress* with
       
   488         the given *targetaddresses*."""
       
   489         alias = self._get_alias(aliasaddress)
       
   490         destinations = [EmailAddress(address) for address in targetaddresses]
       
   491         warnings = []
       
   492         destinations = alias.add_destinations(destinations, warnings)
       
   493         if warnings:
       
   494             self._warnings.append(_('Ignored destination addresses:'))
       
   495             self._warnings.extend(('  * %s' % w for w in warnings))
       
   496         for destination in destinations:
       
   497             if get_gid(self._dbh, destination.domainname) and \
       
   498                not self._chk_other_address_types(destination, TYPE_RELOCATED):
       
   499                 self._warnings.append(_(u"The destination account/alias '%s' "
       
   500                                         u"doesn't exist.") % destination)
       
   501 
       
   502     def user_delete(self, emailaddress, force=None):
       
   503         """Wrapper around Account.delete(...)"""
       
   504         if force not in (None, 'delalias'):
       
   505             raise VMMError(_(u"Invalid argument: '%s'") % force,
       
   506                            INVALID_ARGUMENT)
       
   507         acc = self._get_account(emailaddress)
       
   508         if not acc:
       
   509             raise VMMError(_(u"The account '%s' doesn't exist.") %
       
   510                            acc.address, NO_SUCH_ACCOUNT)
       
   511         uid = acc.uid
       
   512         gid = acc.gid
       
   513         dom_dir = acc.domain_directory
       
   514         acc_dir = acc.home
       
   515         acc.delete(bool(force))
       
   516         if self._cfg.dget('account.delete_directory'):
       
   517             try:
       
   518                 self._delete_home(dom_dir, uid, gid)
       
   519             except VMMError, err:
       
   520                 if err.code in (FOUND_DOTS_IN_PATH, MAILDIR_PERM_MISMATCH,
       
   521                                 NO_SUCH_DIRECTORY):
       
   522                     warning = _(u"""\
       
   523 The account has been successfully deleted from the database.
       
   524     But an error occurred while deleting the following directory:
       
   525     ā€œ%(directory)sā€
       
   526     Reason: %(reason)s""") % \
       
   527                                 {'directory': acc_dir, 'reason': err.msg}
       
   528                     self._warnings.append(warning)
       
   529                 else:
       
   530                     raise
       
   531 
       
   532     def alias_info(self, aliasaddress):
       
   533         """Returns an iterator object for all destinations (`EmailAddress`
       
   534         instances) for the `Alias` with the given *aliasaddress*."""
       
   535         alias = self._get_alias(aliasaddress)
       
   536         if alias:
       
   537             return alias.get_destinations()
       
   538         if not self._is_other_address(alias.address, TYPE_ALIAS):
       
   539             raise VMMError(_(u"The alias '%s' doesn't exist.") %
       
   540                            alias.address, NO_SUCH_ALIAS)
       
   541 
       
   542     def alias_delete(self, aliasaddress, targetaddress=None):
       
   543         """Deletes the `Alias` *aliasaddress* with all its destinations from
       
   544         the database. If *targetaddress* is not ``None``, only this
       
   545         destination will be removed from the alias."""
       
   546         alias = self._get_alias(aliasaddress)
       
   547         if targetaddress is None:
       
   548             alias.delete()
       
   549         else:
       
   550             alias.del_destination(EmailAddress(targetaddress))
       
   551 
       
   552     def user_info(self, emailaddress, details=None):
       
   553         """Wrapper around Account.get_info(...)"""
       
   554         if details not in (None, 'du', 'aliases', 'full'):
       
   555             raise VMMError(_(u"Invalid argument: '%s'") % details,
       
   556                            INVALID_ARGUMENT)
       
   557         acc = self._get_account(emailaddress)
       
   558         if not acc:
       
   559             if not self._is_other_address(acc.address, TYPE_ACCOUNT):
       
   560                 raise VMMError(_(u"The account '%s' doesn't exist.") %
       
   561                                acc.address, NO_SUCH_ACCOUNT)
       
   562         info = acc.get_info()
       
   563         if self._cfg.dget('account.disk_usage') or details in ('du', 'full'):
       
   564             path = os.path.join(acc.home, acc.mail_location.directory)
       
   565             info['disk usage'] = self._get_disk_usage(path)
       
   566             if details in (None, 'du'):
       
   567                 return info
       
   568         if details in ('aliases', 'full'):
       
   569             return (info, acc.get_aliases())
       
   570         return info
       
   571 
       
   572     def user_by_uid(self, uid):
       
   573         """Search for an Account by its *uid*.
       
   574         Returns a dict (address, uid and gid) if a user could be found."""
       
   575         from VirtualMailManager.account import get_account_by_uid
       
   576         self._db_connect()
       
   577         return get_account_by_uid(uid, self._dbh)
       
   578 
       
   579     def user_password(self, emailaddress, password):
       
   580         """Wrapper for Account.modify('password' ...)."""
       
   581         if not isinstance(password, basestring) or not password:
       
   582             raise VMMError(_(u"Could not accept password: '%s'") % password,
       
   583                            INVALID_ARGUMENT)
       
   584         acc = self._get_account(emailaddress)
       
   585         if not acc:
       
   586             raise VMMError(_(u"The account '%s' doesn't exist.") %
       
   587                            acc.address, NO_SUCH_ACCOUNT)
       
   588         acc.modify('password', password)
       
   589 
       
   590     def user_name(self, emailaddress, name):
       
   591         """Wrapper for Account.modify('name', ...)."""
       
   592         if not isinstance(name, basestring) or not name:
       
   593             raise VMMError(_(u"Could not accept name: '%s'") % name,
       
   594                            INVALID_ARGUMENT)
       
   595         acc = self._get_account(emailaddress)
       
   596         if not acc:
       
   597             raise VMMError(_(u"The account '%s' doesn't exist.") %
       
   598                            acc.address, NO_SUCH_ACCOUNT)
       
   599         acc.modify('name', name)
       
   600 
       
   601     def user_transport(self, emailaddress, transport):
       
   602         """Wrapper for Account.modify('transport', ...)."""
       
   603         if not isinstance(transport, basestring) or not transport:
       
   604             raise VMMError(_(u"Could not accept transport: '%s'") % transport,
       
   605                            INVALID_ARGUMENT)
       
   606         acc = self._get_account(emailaddress)
       
   607         if not acc:
       
   608             raise VMMError(_(u"The account '%s' doesn't exist.") %
       
   609                            acc.address, NO_SUCH_ACCOUNT)
       
   610         acc.modify('transport', transport)
       
   611 
       
   612     def user_disable(self, emailaddress, service=None):
       
   613         """Wrapper for Account.disable(service)"""
       
   614         if service not in (None, 'all', 'imap', 'pop3', 'smtp', 'sieve'):
       
   615             raise VMMError(_(u"Could not accept service: '%s'") % service,
       
   616                            INVALID_ARGUMENT)
       
   617         acc = self._get_account(emailaddress)
       
   618         if not acc:
       
   619             raise VMMError(_(u"The account '%s' doesn't exist.") %
       
   620                            acc.address, NO_SUCH_ACCOUNT)
       
   621         acc.disable(service)
       
   622 
       
   623     def user_enable(self, emailaddress, service=None):
       
   624         """Wrapper for Account.enable(service)"""
       
   625         if service not in (None, 'all', 'imap', 'pop3', 'smtp', 'sieve'):
       
   626             raise VMMError(_(u"Could not accept service: '%s'") % service,
       
   627                            INVALID_ARGUMENT)
       
   628         acc = self._get_account(emailaddress)
       
   629         if not acc:
       
   630             raise VMMError(_(u"The account '%s' doesn't exist.") %
       
   631                            acc.address, NO_SUCH_ACCOUNT)
       
   632         acc.enable(service)
       
   633 
       
   634     def relocated_add(self, emailaddress, targetaddress):
       
   635         """Creates a new `Relocated` entry in the database. If there is
       
   636         already a relocated user with the given *emailaddress*, only the
       
   637         *targetaddress* for the relocated user will be updated."""
       
   638         relocated = self._get_relocated(emailaddress)
       
   639         relocated.set_destination(EmailAddress(targetaddress))
       
   640 
       
   641     def relocated_info(self, emailaddress):
       
   642         """Returns the target address of the relocated user with the given
       
   643         *emailaddress*."""
       
   644         relocated = self._get_relocated(emailaddress)
       
   645         if relocated:
       
   646             return relocated.get_info()
       
   647         if not self._is_other_address(relocated.address, TYPE_RELOCATED):
       
   648             raise VMMError(_(u"The relocated user '%s' doesn't exist.") %
       
   649                            relocated.address, NO_SUCH_RELOCATED)
       
   650 
       
   651     def relocated_delete(self, emailaddress):
       
   652         """Deletes the relocated user with the given *emailaddress* from
       
   653         the database."""
       
   654         relocated = self._get_relocated(emailaddress)
       
   655         relocated.delete()
       
   656 
       
   657 del _