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