VirtualMailManager/VirtualMailManager.py
branchv0.6.x
changeset 184 d0425225ce52
parent 183 eb4c73d9d0a4
child 185 6e1ef32fbd82
equal deleted inserted replaced
183:eb4c73d9d0a4 184:d0425225ce52
     1 # -*- coding: UTF-8 -*-
       
     2 # Copyright (c) 2007 - 2010, Pascal Volk
       
     3 # See COPYING for distribution information.
       
     4 
       
     5 """The main class for vmm."""
       
     6 
       
     7 
       
     8 from encodings.idna import ToASCII, ToUnicode
       
     9 from getpass import getpass
       
    10 from shutil import rmtree
       
    11 from subprocess import Popen, PIPE
       
    12 
       
    13 from pyPgSQL import PgSQL # python-pgsql - http://pypgsql.sourceforge.net
       
    14 
       
    15 from __main__ import os, re, ENCODING, ERR, w_std
       
    16 from ext.Postconf import Postconf
       
    17 from Account import Account
       
    18 from Alias import Alias
       
    19 from AliasDomain import AliasDomain
       
    20 from Config import Config as Cfg
       
    21 from Domain import Domain
       
    22 from EmailAddress import EmailAddress
       
    23 from Exceptions import *
       
    24 from Relocated import Relocated
       
    25 
       
    26 SALTCHARS = './0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
       
    27 RE_ASCII_CHARS = """^[\x20-\x7E]*$"""
       
    28 RE_DOMAIN = """^(?:[a-z0-9-]{1,63}\.){1,}[a-z]{2,6}$"""
       
    29 RE_DOMAIN_SRCH = """^[a-z0-9-\.]+$"""
       
    30 RE_LOCALPART = """[^\w!#$%&'\*\+-\.\/=?^_`{\|}~]"""
       
    31 RE_MBOX_NAMES = """^[\x20-\x25\x27-\x7E]*$"""
       
    32 
       
    33 class VirtualMailManager(object):
       
    34     """The main class for vmm"""
       
    35     __slots__ = ('__Cfg', '__cfgFileName', '__dbh', '__scheme', '__warnings',
       
    36                  '_postconf')
       
    37     def __init__(self):
       
    38         """Creates a new VirtualMailManager instance.
       
    39         Throws a VMMNotRootException if your uid is greater 0.
       
    40         """
       
    41         self.__cfgFileName = ''
       
    42         self.__warnings = []
       
    43         self.__Cfg = None
       
    44         self.__dbh = None
       
    45 
       
    46         if os.geteuid():
       
    47             raise VMMNotRootException(_(u"You are not root.\n\tGood bye!\n"),
       
    48                 ERR.CONF_NOPERM)
       
    49         if self.__chkCfgFile():
       
    50             self.__Cfg = Cfg(self.__cfgFileName)
       
    51             self.__Cfg.load()
       
    52         if not os.sys.argv[1] in ('cf','configure','h','help','v','version'):
       
    53             self.__Cfg.check()
       
    54             self.__chkenv()
       
    55             self.__scheme = self.__Cfg.dget('misc.password_scheme')
       
    56             self._postconf = Postconf(self.__Cfg.dget('bin.postconf'))
       
    57 
       
    58     def __findCfgFile(self):
       
    59         for path in ['/root', '/usr/local/etc', '/etc']:
       
    60             tmp = os.path.join(path, 'vmm.cfg')
       
    61             if os.path.isfile(tmp):
       
    62                 self.__cfgFileName = tmp
       
    63                 break
       
    64         if not len(self.__cfgFileName):
       
    65             raise VMMException(
       
    66                 _(u"No “vmm.cfg” found in: /root:/usr/local/etc:/etc"),
       
    67                 ERR.CONF_NOFILE)
       
    68 
       
    69     def __chkCfgFile(self):
       
    70         """Checks the configuration file, returns bool"""
       
    71         self.__findCfgFile()
       
    72         fstat = os.stat(self.__cfgFileName)
       
    73         fmode = int(oct(fstat.st_mode & 0777))
       
    74         if fmode % 100 and fstat.st_uid != fstat.st_gid \
       
    75         or fmode % 10 and fstat.st_uid == fstat.st_gid:
       
    76             raise VMMPermException(_(
       
    77                 u'fix permissions (%(perms)s) for “%(file)s”\n\
       
    78 `chmod 0600 %(file)s` would be great.') % {'file':
       
    79                 self.__cfgFileName, 'perms': fmode}, ERR.CONF_WRONGPERM)
       
    80         else:
       
    81             return True
       
    82 
       
    83     def __chkenv(self):
       
    84         """"""
       
    85         basedir = self.__Cfg.dget('misc.base_directory')
       
    86         if not os.path.exists(basedir):
       
    87             old_umask = os.umask(0006)
       
    88             os.makedirs(basedir, 0771)
       
    89             os.chown(basedir, 0, self.__Cfg.dget('misc.gid_mail'))
       
    90             os.umask(old_umask)
       
    91         elif not os.path.isdir(basedir):
       
    92             raise VMMException(_(u'“%s” is not a directory.\n\
       
    93 (vmm.cfg: section "misc", option "base_directory")') %
       
    94                                  basedir, ERR.NO_SUCH_DIRECTORY)
       
    95         for opt, val in self.__Cfg.items('bin'):
       
    96             if not os.path.exists(val):
       
    97                 raise VMMException(_(u'“%(binary)s” doesn\'t exist.\n\
       
    98 (vmm.cfg: section "bin", option "%(option)s")') %{'binary': val,'option': opt},
       
    99                     ERR.NO_SUCH_BINARY)
       
   100             elif not os.access(val, os.X_OK):
       
   101                 raise VMMException(_(u'“%(binary)s” is not executable.\n\
       
   102 (vmm.cfg: section "bin", option "%(option)s")') %{'binary': val,'option': opt},
       
   103                     ERR.NOT_EXECUTABLE)
       
   104 
       
   105     def __dbConnect(self):
       
   106         """Creates a pyPgSQL.PgSQL.connection instance."""
       
   107         if self.__dbh is None or not self.__dbh._isOpen:
       
   108             try:
       
   109                 self.__dbh = PgSQL.connect(
       
   110                         database=self.__Cfg.dget('database.name'),
       
   111                         user=self.__Cfg.pget('database.user'),
       
   112                         host=self.__Cfg.dget('database.host'),
       
   113                         password=self.__Cfg.pget('database.pass'),
       
   114                         client_encoding='utf8', unicode_results=True)
       
   115                 dbc = self.__dbh.cursor()
       
   116                 dbc.execute("SET NAMES 'UTF8'")
       
   117                 dbc.close()
       
   118             except PgSQL.libpq.DatabaseError, e:
       
   119                 raise VMMException(str(e), ERR.DATABASE_ERROR)
       
   120 
       
   121     def idn2ascii(domainname):
       
   122         """Converts an idn domainname in punycode.
       
   123 
       
   124         Arguments:
       
   125         domainname -- the domainname to convert (unicode)
       
   126         """
       
   127         return '.'.join([ToASCII(lbl) for lbl in domainname.split('.') if lbl])
       
   128     idn2ascii = staticmethod(idn2ascii)
       
   129 
       
   130     def ace2idna(domainname):
       
   131         """Convertis a domainname from ACE according to IDNA
       
   132 
       
   133         Arguments:
       
   134         domainname -- the domainname to convert (str)
       
   135         """
       
   136         return u'.'.join([ToUnicode(lbl) for lbl in domainname.split('.')\
       
   137                 if lbl])
       
   138     ace2idna = staticmethod(ace2idna)
       
   139 
       
   140     def chkDomainname(domainname):
       
   141         """Validates the domain name of an e-mail address.
       
   142 
       
   143         Keyword arguments:
       
   144         domainname -- the domain name that should be validated
       
   145         """
       
   146         if not re.match(RE_ASCII_CHARS, domainname):
       
   147             domainname = VirtualMailManager.idn2ascii(domainname)
       
   148         if len(domainname) > 255:
       
   149             raise VMMException(_(u'The domain name is too long.'),
       
   150                 ERR.DOMAIN_TOO_LONG)
       
   151         if not re.match(RE_DOMAIN, domainname):
       
   152             raise VMMException(_(u'The domain name “%s” is invalid.') %\
       
   153                     domainname, ERR.DOMAIN_INVALID)
       
   154         return domainname
       
   155     chkDomainname = staticmethod(chkDomainname)
       
   156 
       
   157     def _exists(dbh, query):
       
   158         dbc = dbh.cursor()
       
   159         dbc.execute(query)
       
   160         gid = dbc.fetchone()
       
   161         dbc.close()
       
   162         if gid is None:
       
   163             return False
       
   164         else:
       
   165             return True
       
   166     _exists = staticmethod(_exists)
       
   167 
       
   168     def accountExists(dbh, address):
       
   169         sql = "SELECT gid FROM users WHERE gid = (SELECT gid FROM domain_name\
       
   170  WHERE domainname = '%s') AND local_part = '%s'" % (address._domainname,
       
   171             address._localpart)
       
   172         return VirtualMailManager._exists(dbh, sql)
       
   173     accountExists = staticmethod(accountExists)
       
   174 
       
   175     def aliasExists(dbh, address):
       
   176         sql = "SELECT DISTINCT gid FROM alias WHERE gid = (SELECT gid FROM\
       
   177  domain_name WHERE domainname = '%s') AND address = '%s'" %\
       
   178             (address._domainname, address._localpart)
       
   179         return VirtualMailManager._exists(dbh, sql)
       
   180     aliasExists = staticmethod(aliasExists)
       
   181 
       
   182     def relocatedExists(dbh, address):
       
   183         sql = "SELECT gid FROM relocated WHERE gid = (SELECT gid FROM\
       
   184  domain_name WHERE domainname = '%s') AND address = '%s'" %\
       
   185             (address._domainname, address._localpart)
       
   186         return VirtualMailManager._exists(dbh, sql)
       
   187     relocatedExists = staticmethod(relocatedExists)
       
   188 
       
   189     def _readpass(self):
       
   190         # TP: Please preserve the trailing space.
       
   191         readp_msg0 = _(u'Enter new password: ').encode(ENCODING, 'replace')
       
   192         # TP: Please preserve the trailing space.
       
   193         readp_msg1 = _(u'Retype new password: ').encode(ENCODING, 'replace')
       
   194         mismatched = True
       
   195         flrs = 0
       
   196         while mismatched:
       
   197             if flrs > 2:
       
   198                 raise VMMException(_(u'Too many failures - try again later.'),
       
   199                         ERR.VMM_TOO_MANY_FAILURES)
       
   200             clear0 = getpass(prompt=readp_msg0)
       
   201             clear1 = getpass(prompt=readp_msg1)
       
   202             if clear0 != clear1:
       
   203                 flrs += 1
       
   204                 w_std(_(u'Sorry, passwords do not match'))
       
   205                 continue
       
   206             if len(clear0) < 1:
       
   207                 flrs += 1
       
   208                 w_std(_(u'Sorry, empty passwords are not permitted'))
       
   209                 continue
       
   210             mismatched = False
       
   211         return clear0
       
   212 
       
   213     def __getAccount(self, address, password=None):
       
   214         self.__dbConnect()
       
   215         address = EmailAddress(address)
       
   216         if not password is None:
       
   217             password = self.__pwhash(password)
       
   218         return Account(self.__dbh, address, password)
       
   219 
       
   220     def __getAlias(self, address, destination=None):
       
   221         self.__dbConnect()
       
   222         address = EmailAddress(address)
       
   223         if destination is not None:
       
   224             destination = EmailAddress(destination)
       
   225         return Alias(self.__dbh, address, destination)
       
   226 
       
   227     def __getRelocated(self,address, destination=None):
       
   228         self.__dbConnect()
       
   229         address = EmailAddress(address)
       
   230         if destination is not None:
       
   231             destination = EmailAddress(destination)
       
   232         return Relocated(self.__dbh, address, destination)
       
   233 
       
   234     def __getDomain(self, domainname, transport=None):
       
   235         if transport is None:
       
   236             transport = self.__Cfg.dget('misc.transport')
       
   237         self.__dbConnect()
       
   238         return Domain(self.__dbh, domainname,
       
   239                 self.__Cfg.dget('misc.base_directory'), transport)
       
   240 
       
   241     def __getDiskUsage(self, directory):
       
   242         """Estimate file space usage for the given directory.
       
   243 
       
   244         Keyword arguments:
       
   245         directory -- the directory to summarize recursively disk usage for
       
   246         """
       
   247         if self.__isdir(directory):
       
   248             return Popen([self.__Cfg.dget('bin.du'), "-hs", directory],
       
   249                 stdout=PIPE).communicate()[0].split('\t')[0]
       
   250         else:
       
   251             return 0
       
   252 
       
   253     def __isdir(self, directory):
       
   254         isdir = os.path.isdir(directory)
       
   255         if not isdir:
       
   256             self.__warnings.append(_('No such directory: %s') % directory)
       
   257         return isdir
       
   258 
       
   259     def __makedir(self, directory, mode=None, uid=None, gid=None):
       
   260         if mode is None:
       
   261             mode = self.__Cfg.dget('account.directory_mode')
       
   262         if uid is None:
       
   263             uid = 0
       
   264         if gid is None:
       
   265             gid = 0
       
   266         os.makedirs(directory, mode)
       
   267         os.chown(directory, uid, gid)
       
   268 
       
   269     def __domDirMake(self, domdir, gid):
       
   270         os.umask(0006)
       
   271         oldpwd = os.getcwd()
       
   272         basedir = self.__Cfg.dget('misc.base_directory')
       
   273         domdirdirs = domdir.replace(basedir+'/', '').split('/')
       
   274 
       
   275         os.chdir(basedir)
       
   276         if not os.path.isdir(domdirdirs[0]):
       
   277             self.__makedir(domdirdirs[0], 489, 0,
       
   278                            self.__Cfg.dget('misc.gid_mail'))
       
   279         os.chdir(domdirdirs[0])
       
   280         os.umask(0007)
       
   281         self.__makedir(domdirdirs[1], self.__Cfg.dget('domain.directory_mode'),
       
   282                        0, gid)
       
   283         os.chdir(oldpwd)
       
   284 
       
   285     def __subscribeFL(self, folderlist, uid, gid):
       
   286         fname = os.path.join(self.__Cfg.dget('maildir.name'), 'subscriptions')
       
   287         sf = file(fname, 'w')
       
   288         for f in folderlist:
       
   289             sf.write(f+'\n')
       
   290         sf.flush()
       
   291         sf.close()
       
   292         os.chown(fname, uid, gid)
       
   293         os.chmod(fname, 384)
       
   294 
       
   295     def __mailDirMake(self, domdir, uid, gid):
       
   296         """Creates maildirs and maildir subfolders.
       
   297 
       
   298         Keyword arguments:
       
   299         domdir -- the path to the domain directory
       
   300         uid -- user id from the account
       
   301         gid -- group id from the account
       
   302         """
       
   303         os.umask(0007)
       
   304         oldpwd = os.getcwd()
       
   305         os.chdir(domdir)
       
   306 
       
   307         maildir = self.__Cfg.dget('maildir.name')
       
   308         folders = [maildir]
       
   309         for folder in self.__Cfg.dget('maildir.folders').split(':'):
       
   310             folder = folder.strip()
       
   311             if len(folder) and not folder.count('..')\
       
   312             and re.match(RE_MBOX_NAMES, folder):
       
   313                 folders.append('%s/.%s' % (maildir, folder))
       
   314         subdirs = ['cur', 'new', 'tmp']
       
   315         mode = self.__Cfg.dget('account.directory_mode')
       
   316 
       
   317         self.__makedir('%s' % uid, mode, uid, gid)
       
   318         os.chdir('%s' % uid)
       
   319         for folder in folders:
       
   320             self.__makedir(folder, mode, uid, gid)
       
   321             for subdir in subdirs:
       
   322                 self.__makedir(os.path.join(folder, subdir), mode, uid, gid)
       
   323         self.__subscribeFL([f.replace(maildir+'/.', '') for f in folders[1:]],
       
   324                 uid, gid)
       
   325         os.chdir(oldpwd)
       
   326 
       
   327     def __userDirDelete(self, domdir, uid, gid):
       
   328         if uid > 0 and gid > 0:
       
   329             userdir = '%s' % uid
       
   330             if userdir.count('..') or domdir.count('..'):
       
   331                 raise VMMException(_(u'Found ".." in home directory path.'),
       
   332                     ERR.FOUND_DOTS_IN_PATH)
       
   333             if os.path.isdir(domdir):
       
   334                 os.chdir(domdir)
       
   335                 if os.path.isdir(userdir):
       
   336                     mdstat = os.stat(userdir)
       
   337                     if (mdstat.st_uid, mdstat.st_gid) != (uid, gid):
       
   338                         raise VMMException(
       
   339                          _(u'Detected owner/group mismatch in home directory.'),
       
   340                          ERR.MAILDIR_PERM_MISMATCH)
       
   341                     rmtree(userdir, ignore_errors=True)
       
   342                 else:
       
   343                     raise VMMException(_(u"No such directory: %s") %
       
   344                         os.path.join(domdir, userdir), ERR.NO_SUCH_DIRECTORY)
       
   345 
       
   346     def __domDirDelete(self, domdir, gid):
       
   347         if gid > 0:
       
   348             if not self.__isdir(domdir):
       
   349                 return
       
   350             basedir = self.__Cfg.dget('misc.base_directory')
       
   351             domdirdirs = domdir.replace(basedir+'/', '').split('/')
       
   352             domdirparent = os.path.join(basedir, domdirdirs[0])
       
   353             if basedir.count('..') or domdir.count('..'):
       
   354                 raise VMMException(_(u'Found ".." in domain directory path.'),
       
   355                         ERR.FOUND_DOTS_IN_PATH)
       
   356             if os.path.isdir(domdirparent):
       
   357                 os.chdir(domdirparent)
       
   358                 if os.lstat(domdirdirs[1]).st_gid != gid:
       
   359                     raise VMMException(_(
       
   360                         u'Detected group mismatch in domain directory.'),
       
   361                         ERR.DOMAINDIR_GROUP_MISMATCH)
       
   362                 rmtree(domdirdirs[1], ignore_errors=True)
       
   363 
       
   364     def __getSalt(self):
       
   365         from random import choice
       
   366         salt = None
       
   367         if self.__scheme == 'CRYPT':
       
   368             salt = '%s%s' % (choice(SALTCHARS), choice(SALTCHARS))
       
   369         elif self.__scheme in ['MD5', 'MD5-CRYPT']:
       
   370             salt = '$1$%s$' % ''.join([choice(SALTCHARS) for x in xrange(8)])
       
   371         return salt
       
   372 
       
   373     def __pwCrypt(self, password):
       
   374         # for: CRYPT, MD5 and MD5-CRYPT
       
   375         from crypt import crypt
       
   376         return crypt(password, self.__getSalt())
       
   377 
       
   378     def __pwSHA1(self, password):
       
   379         # for: SHA/SHA1
       
   380         import sha
       
   381         from base64 import standard_b64encode
       
   382         sha1 = sha.new(password)
       
   383         return standard_b64encode(sha1.digest())
       
   384 
       
   385     def __pwMD5(self, password, emailaddress=None):
       
   386         import md5
       
   387         _md5 = md5.new(password)
       
   388         if self.__scheme == 'LDAP-MD5':
       
   389             from base64 import standard_b64encode
       
   390             return standard_b64encode(_md5.digest())
       
   391         elif self.__scheme == 'PLAIN-MD5':
       
   392             return _md5.hexdigest()
       
   393         elif self.__scheme == 'DIGEST-MD5' and emailaddress is not None:
       
   394             # use an empty realm - works better with usenames like user@dom
       
   395             _md5 = md5.new('%s::%s' % (emailaddress, password))
       
   396             return _md5.hexdigest()
       
   397 
       
   398     def __pwMD4(self, password):
       
   399         # for: PLAIN-MD4
       
   400         from Crypto.Hash import MD4
       
   401         _md4 = MD4.new(password)
       
   402         return _md4.hexdigest()
       
   403 
       
   404     def __pwhash(self, password, scheme=None, user=None):
       
   405         if scheme is not None:
       
   406             self.__scheme = scheme
       
   407         if self.__scheme in ['CRYPT', 'MD5', 'MD5-CRYPT']:
       
   408             return '{%s}%s' % (self.__scheme, self.__pwCrypt(password))
       
   409         elif self.__scheme in ['SHA', 'SHA1']:
       
   410             return '{%s}%s' % (self.__scheme, self.__pwSHA1(password))
       
   411         elif self.__scheme in ['PLAIN-MD5', 'LDAP-MD5', 'DIGEST-MD5']:
       
   412             return '{%s}%s' % (self.__scheme, self.__pwMD5(password, user))
       
   413         elif self.__scheme == 'MD4':
       
   414             return '{%s}%s' % (self.__scheme, self.__pwMD4(password))
       
   415         elif self.__scheme in ['SMD5', 'SSHA', 'CRAM-MD5', 'HMAC-MD5',
       
   416                 'LANMAN', 'NTLM', 'RPA']:
       
   417             return Popen([self.__Cfg.dget('bin.dovecotpw'), '-s',
       
   418                 self.__scheme,'-p',password],stdout=PIPE).communicate()[0][:-1]
       
   419         else:
       
   420             return '{%s}%s' % (self.__scheme, password)
       
   421 
       
   422     def hasWarnings(self):
       
   423         """Checks if warnings are present, returns bool."""
       
   424         return bool(len(self.__warnings))
       
   425 
       
   426     def getWarnings(self):
       
   427         """Returns a list with all available warnings."""
       
   428         return self.__warnings
       
   429 
       
   430     def cfgDget(self, option):
       
   431         return self.__Cfg.dget(option)
       
   432 
       
   433     def cfgPget(self, option):
       
   434         return self.__Cfg.pget(option)
       
   435 
       
   436     def cfgSet(self, option, value):
       
   437         return self.__Cfg.set(option, value)
       
   438 
       
   439     def configure(self, section=None):
       
   440         """Starts interactive configuration.
       
   441 
       
   442         Configures in interactive mode options in the given section.
       
   443         If no section is given (default) all options from all sections
       
   444         will be prompted.
       
   445 
       
   446         Keyword arguments:
       
   447         section -- the section to configure (default None):
       
   448         """
       
   449         if section is None:
       
   450             self.__Cfg.configure(self.__Cfg.getsections())
       
   451         elif self.__Cfg.has_section(section):
       
   452             self.__Cfg.configure([section])
       
   453         else:
       
   454             raise VMMException(_(u"Invalid section: “%s”") % section,
       
   455                                ERR.INVALID_SECTION)
       
   456 
       
   457     def domainAdd(self, domainname, transport=None):
       
   458         dom = self.__getDomain(domainname, transport)
       
   459         dom.save()
       
   460         self.__domDirMake(dom.getDir(), dom.getID())
       
   461 
       
   462     def domainTransport(self, domainname, transport, force=None):
       
   463         if force is not None and force != 'force':
       
   464             raise VMMDomainException(_(u"Invalid argument: “%s”") % force,
       
   465                 ERR.INVALID_OPTION)
       
   466         dom = self.__getDomain(domainname, None)
       
   467         if force is None:
       
   468             dom.updateTransport(transport)
       
   469         else:
       
   470             dom.updateTransport(transport, force=True)
       
   471 
       
   472     def domainDelete(self, domainname, force=None):
       
   473         if not force is None and force not in ['deluser','delalias','delall']:
       
   474             raise VMMDomainException(_(u"Invalid argument: “%s”") % force,
       
   475                 ERR.INVALID_OPTION)
       
   476         dom = self.__getDomain(domainname)
       
   477         gid = dom.getID()
       
   478         domdir = dom.getDir()
       
   479         if self.__Cfg.dget('domain.force_deletion') or force == 'delall':
       
   480             dom.delete(True, True)
       
   481         elif force == 'deluser':
       
   482             dom.delete(delUser=True)
       
   483         elif force == 'delalias':
       
   484             dom.delete(delAlias=True)
       
   485         else:
       
   486             dom.delete()
       
   487         if self.__Cfg.dget('domain.delete_directory'):
       
   488             self.__domDirDelete(domdir, gid)
       
   489 
       
   490     def domainInfo(self, domainname, details=None):
       
   491         if details not in [None, 'accounts', 'aliasdomains', 'aliases', 'full',
       
   492                 'relocated', 'detailed']:
       
   493             raise VMMException(_(u'Invalid argument: “%s”') % details,
       
   494                     ERR.INVALID_AGUMENT)
       
   495         if details == 'detailed':
       
   496             details = 'full'
       
   497             self.__warnings.append(_(u'\
       
   498 The keyword “detailed” is deprecated and will be removed in a future release.\n\
       
   499    Please use the keyword “full” to get full details.'))
       
   500         dom = self.__getDomain(domainname)
       
   501         dominfo = dom.getInfo()
       
   502         if dominfo['domainname'].startswith('xn--'):
       
   503             dominfo['domainname'] += ' (%s)'\
       
   504                 % VirtualMailManager.ace2idna(dominfo['domainname'])
       
   505         if details is None:
       
   506             return dominfo
       
   507         elif details == 'accounts':
       
   508             return (dominfo, dom.getAccounts())
       
   509         elif details == 'aliasdomains':
       
   510             return (dominfo, dom.getAliaseNames())
       
   511         elif details == 'aliases':
       
   512             return (dominfo, dom.getAliases())
       
   513         elif details == 'relocated':
       
   514             return(dominfo, dom.getRelocated())
       
   515         else:
       
   516             return (dominfo, dom.getAliaseNames(), dom.getAccounts(),
       
   517                     dom.getAliases(), dom.getRelocated())
       
   518 
       
   519     def aliasDomainAdd(self, aliasname, domainname):
       
   520         """Adds an alias domain to the domain.
       
   521 
       
   522         Keyword arguments:
       
   523         aliasname -- the name of the alias domain (str)
       
   524         domainname -- name of the target domain (str)
       
   525         """
       
   526         dom = self.__getDomain(domainname)
       
   527         aliasDom = AliasDomain(self.__dbh, aliasname, dom)
       
   528         aliasDom.save()
       
   529 
       
   530     def aliasDomainInfo(self, aliasname):
       
   531         self.__dbConnect()
       
   532         aliasDom = AliasDomain(self.__dbh, aliasname, None)
       
   533         return aliasDom.info()
       
   534 
       
   535     def aliasDomainSwitch(self, aliasname, domainname):
       
   536         """Modifies the target domain of an existing alias domain.
       
   537 
       
   538         Keyword arguments:
       
   539         aliasname -- the name of the alias domain (str)
       
   540         domainname -- name of the new target domain (str)
       
   541         """
       
   542         dom = self.__getDomain(domainname)
       
   543         aliasDom = AliasDomain(self.__dbh, aliasname, dom)
       
   544         aliasDom.switch()
       
   545 
       
   546     def aliasDomainDelete(self, aliasname):
       
   547         """Deletes the specified alias domain.
       
   548 
       
   549         Keyword arguments:
       
   550         aliasname -- the name of the alias domain (str)
       
   551         """
       
   552         self.__dbConnect()
       
   553         aliasDom = AliasDomain(self.__dbh, aliasname, None)
       
   554         aliasDom.delete()
       
   555 
       
   556     def domainList(self, pattern=None):
       
   557         from Domain import search
       
   558         like = False
       
   559         if pattern is not None:
       
   560             if pattern.startswith('%') or pattern.endswith('%'):
       
   561                 like = True
       
   562                 if pattern.startswith('%') and pattern.endswith('%'):
       
   563                     domain = pattern[1:-1]
       
   564                 elif pattern.startswith('%'):
       
   565                     domain = pattern[1:]
       
   566                 elif pattern.endswith('%'):
       
   567                     domain = pattern[:-1]
       
   568                 if not re.match(RE_DOMAIN_SRCH, domain):
       
   569                     raise VMMException(
       
   570                     _(u"The pattern “%s” contains invalid characters.") %
       
   571                     pattern, ERR.DOMAIN_INVALID)
       
   572         self.__dbConnect()
       
   573         return search(self.__dbh, pattern=pattern, like=like)
       
   574 
       
   575     def userAdd(self, emailaddress, password):
       
   576         acc = self.__getAccount(emailaddress, password)
       
   577         if password is None:
       
   578             password = self._readpass()
       
   579             acc.setPassword(self.__pwhash(password))
       
   580         acc.save(self.__Cfg.dget('maildir.name'),
       
   581                  self.__Cfg.dget('misc.dovecot_version'),
       
   582                  self.__Cfg.dget('account.smtp'),
       
   583                  self.__Cfg.dget('account.pop3'),
       
   584                  self.__Cfg.dget('account.imap'),
       
   585                  self.__Cfg.dget('account.sieve'))
       
   586         self.__mailDirMake(acc.getDir('domain'), acc.getUID(), acc.getGID())
       
   587 
       
   588     def aliasAdd(self, aliasaddress, targetaddress):
       
   589         alias = self.__getAlias(aliasaddress, targetaddress)
       
   590         alias.save(long(self._postconf.read('virtual_alias_expansion_limit')))
       
   591         gid = self.__getDomain(alias._dest._domainname).getID()
       
   592         if gid > 0 and not VirtualMailManager.accountExists(self.__dbh,
       
   593         alias._dest) and not VirtualMailManager.aliasExists(self.__dbh,
       
   594         alias._dest):
       
   595             self.__warnings.append(
       
   596                 _(u"The destination account/alias “%s” doesn't exist.")%\
       
   597                         alias._dest)
       
   598 
       
   599     def userDelete(self, emailaddress, force=None):
       
   600         if force not in [None, 'delalias']:
       
   601             raise VMMException(_(u"Invalid argument: “%s”") % force,
       
   602                     ERR.INVALID_AGUMENT)
       
   603         acc = self.__getAccount(emailaddress)
       
   604         uid = acc.getUID()
       
   605         gid = acc.getGID()
       
   606         acc.delete(force)
       
   607         if self.__Cfg.dget('account.delete_directory'):
       
   608             try:
       
   609                 self.__userDirDelete(acc.getDir('domain'), uid, gid)
       
   610             except VMMException, e:
       
   611                 if e.code() in [ERR.FOUND_DOTS_IN_PATH,
       
   612                         ERR.MAILDIR_PERM_MISMATCH, ERR.NO_SUCH_DIRECTORY]:
       
   613                     warning = _(u"""\
       
   614 The account has been successfully deleted from the database.
       
   615     But an error occurred while deleting the following directory:
       
   616     “%(directory)s”
       
   617     Reason: %(reason)s""") % {'directory': acc.getDir('home'),'reason': e.msg()}
       
   618                     self.__warnings.append(warning)
       
   619                 else:
       
   620                     raise e
       
   621 
       
   622     def aliasInfo(self, aliasaddress):
       
   623         alias = self.__getAlias(aliasaddress)
       
   624         return alias.getInfo()
       
   625 
       
   626     def aliasDelete(self, aliasaddress, targetaddress=None):
       
   627         alias = self.__getAlias(aliasaddress, targetaddress)
       
   628         alias.delete()
       
   629 
       
   630     def userInfo(self, emailaddress, details=None):
       
   631         if details not in (None, 'du', 'aliases', 'full'):
       
   632             raise VMMException(_(u'Invalid argument: “%s”') % details,
       
   633                                ERR.INVALID_AGUMENT)
       
   634         acc = self.__getAccount(emailaddress)
       
   635         info = acc.getInfo(self.__Cfg.dget('misc.dovecot_version'))
       
   636         if self.__Cfg.dget('account.disk_usage') or details in ('du', 'full'):
       
   637             info['disk usage'] = self.__getDiskUsage('%(maildir)s' % info)
       
   638             if details in (None, 'du'):
       
   639                 return info
       
   640         if details in ('aliases', 'full'):
       
   641             return (info, acc.getAliases())
       
   642         return info
       
   643 
       
   644     def userByID(self, uid):
       
   645         from Account import getAccountByID
       
   646         self.__dbConnect()
       
   647         return getAccountByID(uid, self.__dbh)
       
   648 
       
   649     def userPassword(self, emailaddress, password):
       
   650         acc = self.__getAccount(emailaddress)
       
   651         if acc.getUID() == 0:
       
   652            raise VMMException(_(u"Account doesn't exist"), ERR.NO_SUCH_ACCOUNT)
       
   653         if password is None:
       
   654             password = self._readpass()
       
   655         acc.modify('password', self.__pwhash(password, user=emailaddress))
       
   656 
       
   657     def userName(self, emailaddress, name):
       
   658         acc = self.__getAccount(emailaddress)
       
   659         acc.modify('name', name)
       
   660 
       
   661     def userTransport(self, emailaddress, transport):
       
   662         acc = self.__getAccount(emailaddress)
       
   663         acc.modify('transport', transport)
       
   664 
       
   665     def userDisable(self, emailaddress, service=None):
       
   666         if service == 'managesieve':
       
   667             service = 'sieve'
       
   668             self.__warnings.append(_(u'\
       
   669 The service name “managesieve” is deprecated and will be removed\n\
       
   670    in a future release.\n\
       
   671    Please use the service name “sieve” instead.'))
       
   672         acc = self.__getAccount(emailaddress)
       
   673         acc.disable(self.__Cfg.dget('misc.dovecot_version'), service)
       
   674 
       
   675     def userEnable(self, emailaddress, service=None):
       
   676         if service == 'managesieve':
       
   677             service = 'sieve'
       
   678             self.__warnings.append(_(u'\
       
   679 The service name “managesieve” is deprecated and will be removed\n\
       
   680    in a future release.\n\
       
   681    Please use the service name “sieve” instead.'))
       
   682         acc = self.__getAccount(emailaddress)
       
   683         acc.enable(self.__Cfg.dget('misc.dovecot_version'), service)
       
   684 
       
   685     def relocatedAdd(self, emailaddress, targetaddress):
       
   686         relocated = self.__getRelocated(emailaddress, targetaddress)
       
   687         relocated.save()
       
   688 
       
   689     def relocatedInfo(self, emailaddress):
       
   690         relocated = self.__getRelocated(emailaddress)
       
   691         return relocated.getInfo()
       
   692 
       
   693     def relocatedDelete(self, emailaddress):
       
   694         relocated = self.__getRelocated(emailaddress)
       
   695         relocated.delete()
       
   696 
       
   697     def __del__(self):
       
   698         if not self.__dbh is None and self.__dbh._isOpen:
       
   699             self.__dbh.close()