VirtualMailManager/cli/handler.py
branchv0.6.x
changeset 185 6e1ef32fbd82
parent 184 d0425225ce52
equal deleted inserted replaced
184:d0425225ce52 185:6e1ef32fbd82
     1 # -*- coding: UTF-8 -*-
     1 # -*- coding: UTF-8 -*-
     2 # Copyright (c) 2007 - 2010, Pascal Volk
     2 # Copyright (c) 2007 - 2010, Pascal Volk
     3 # See COPYING for distribution information.
     3 # See COPYING for distribution information.
     4 
     4 
     5 """The main class for vmm."""
     5 """
     6 
     6    VirtualMailManager.Handler
     7 
     7 
     8 from encodings.idna import ToASCII, ToUnicode
     8    A wrapper class. It wraps round all other classes and does some
     9 from getpass import getpass
     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 
    10 from shutil import rmtree
    18 from shutil import rmtree
    11 from subprocess import Popen, PIPE
    19 from subprocess import Popen, PIPE
    12 
    20 
    13 from pyPgSQL import PgSQL # python-pgsql - http://pypgsql.sourceforge.net
    21 from pyPgSQL import PgSQL # python-pgsql - http://pypgsql.sourceforge.net
    14 
    22 
    15 from __main__ import os, re, ENCODING, ERR, w_std
    23 import VirtualMailManager.constants.ERROR as ERR
    16 from ext.Postconf import Postconf
    24 from VirtualMailManager import ENCODING, ace2idna, exec_ok, read_pass
    17 from Account import Account
    25 from VirtualMailManager.Account import Account
    18 from Alias import Alias
    26 from VirtualMailManager.Alias import Alias
    19 from AliasDomain import AliasDomain
    27 from VirtualMailManager.AliasDomain import AliasDomain
    20 from Config import Config as Cfg
    28 from VirtualMailManager.Config import Config as Cfg
    21 from Domain import Domain
    29 from VirtualMailManager.Domain import Domain
    22 from EmailAddress import EmailAddress
    30 from VirtualMailManager.EmailAddress import EmailAddress
    23 from Exceptions import *
    31 from VirtualMailManager.Exceptions import *
    24 from Relocated import Relocated
    32 from VirtualMailManager.Relocated import Relocated
       
    33 from VirtualMailManager.ext.Postconf import Postconf
    25 
    34 
    26 SALTCHARS = './0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
    35 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-\.]+$"""
    36 RE_DOMAIN_SRCH = """^[a-z0-9-\.]+$"""
    30 RE_LOCALPART = """[^\w!#$%&'\*\+-\.\/=?^_`{\|}~]"""
    37 RE_LOCALPART = """[^\w!#$%&'\*\+-\.\/=?^_`{\|}~]"""
    31 RE_MBOX_NAMES = """^[\x20-\x25\x27-\x7E]*$"""
    38 RE_MBOX_NAMES = """^[\x20-\x25\x27-\x7E]*$"""
    32 
    39 
    33 class VirtualMailManager(object):
    40 class Handler(object):
    34     """The main class for vmm"""
    41     """Wrapper class to simplify the access on all the stuff from
       
    42     VirtualMailManager"""
       
    43     # TODO: accept a LazyConfig object as argument
    35     __slots__ = ('__Cfg', '__cfgFileName', '__dbh', '__scheme', '__warnings',
    44     __slots__ = ('__Cfg', '__cfgFileName', '__dbh', '__scheme', '__warnings',
    36                  '_postconf')
    45                  '_postconf')
    37     def __init__(self):
    46     def __init__(self):
    38         """Creates a new VirtualMailManager instance.
    47         """Creates a new Handler instance.
    39         Throws a VMMNotRootException if your uid is greater 0.
    48         Throws a VMMNotRootException if your uid is greater 0.
    40         """
    49         """
    41         self.__cfgFileName = ''
    50         self.__cfgFileName = ''
    42         self.__warnings = []
    51         self.__warnings = []
    43         self.__Cfg = None
    52         self.__Cfg = None
    91         elif not os.path.isdir(basedir):
   100         elif not os.path.isdir(basedir):
    92             raise VMMException(_(u'“%s” is not a directory.\n\
   101             raise VMMException(_(u'“%s” is not a directory.\n\
    93 (vmm.cfg: section "misc", option "base_directory")') %
   102 (vmm.cfg: section "misc", option "base_directory")') %
    94                                  basedir, ERR.NO_SUCH_DIRECTORY)
   103                                  basedir, ERR.NO_SUCH_DIRECTORY)
    95         for opt, val in self.__Cfg.items('bin'):
   104         for opt, val in self.__Cfg.items('bin'):
    96             if not os.path.exists(val):
   105             try:
    97                 raise VMMException(_(u'“%(binary)s” doesn\'t exist.\n\
   106                 exec_ok(val)
       
   107             except VMMException, e:
       
   108                 code = e.code()
       
   109                 if code is ERR.NO_SUCH_BINARY:
       
   110                     raise VMMException(_(u'“%(binary)s” doesn\'t exist.\n\
    98 (vmm.cfg: section "bin", option "%(option)s")') %{'binary': val,'option': opt},
   111 (vmm.cfg: section "bin", option "%(option)s")') %{'binary': val,'option': opt},
    99                     ERR.NO_SUCH_BINARY)
   112                                        ERR.NO_SUCH_BINARY)
   100             elif not os.access(val, os.X_OK):
   113                 elif code is ERR.NOT_EXECUTABLE:
   101                 raise VMMException(_(u'“%(binary)s” is not executable.\n\
   114                     raise VMMException(_(u'“%(binary)s” is not executable.\n\
   102 (vmm.cfg: section "bin", option "%(option)s")') %{'binary': val,'option': opt},
   115 (vmm.cfg: section "bin", option "%(option)s")') %{'binary': val,'option': opt},
   103                     ERR.NOT_EXECUTABLE)
   116                                        ERR.NOT_EXECUTABLE)
       
   117                 else:
       
   118                     raise
   104 
   119 
   105     def __dbConnect(self):
   120     def __dbConnect(self):
   106         """Creates a pyPgSQL.PgSQL.connection instance."""
   121         """Creates a pyPgSQL.PgSQL.connection instance."""
   107         if self.__dbh is None or not self.__dbh._isOpen:
   122         if self.__dbh is None or (isinstance(self.__dbh, PgSQL.Connection) and
       
   123                                   not self.__dbh._isOpen):
   108             try:
   124             try:
   109                 self.__dbh = PgSQL.connect(
   125                 self.__dbh = PgSQL.connect(
   110                         database=self.__Cfg.dget('database.name'),
   126                         database=self.__Cfg.dget('database.name'),
   111                         user=self.__Cfg.pget('database.user'),
   127                         user=self.__Cfg.pget('database.user'),
   112                         host=self.__Cfg.dget('database.host'),
   128                         host=self.__Cfg.dget('database.host'),
   116                 dbc.execute("SET NAMES 'UTF8'")
   132                 dbc.execute("SET NAMES 'UTF8'")
   117                 dbc.close()
   133                 dbc.close()
   118             except PgSQL.libpq.DatabaseError, e:
   134             except PgSQL.libpq.DatabaseError, e:
   119                 raise VMMException(str(e), ERR.DATABASE_ERROR)
   135                 raise VMMException(str(e), ERR.DATABASE_ERROR)
   120 
   136 
   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):
   137     def _exists(dbh, query):
   158         dbc = dbh.cursor()
   138         dbc = dbh.cursor()
   159         dbc.execute(query)
   139         dbc.execute(query)
   160         gid = dbc.fetchone()
   140         gid = dbc.fetchone()
   161         dbc.close()
   141         dbc.close()
   167 
   147 
   168     def accountExists(dbh, address):
   148     def accountExists(dbh, address):
   169         sql = "SELECT gid FROM users WHERE gid = (SELECT gid FROM domain_name\
   149         sql = "SELECT gid FROM users WHERE gid = (SELECT gid FROM domain_name\
   170  WHERE domainname = '%s') AND local_part = '%s'" % (address._domainname,
   150  WHERE domainname = '%s') AND local_part = '%s'" % (address._domainname,
   171             address._localpart)
   151             address._localpart)
   172         return VirtualMailManager._exists(dbh, sql)
   152         return Handler._exists(dbh, sql)
   173     accountExists = staticmethod(accountExists)
   153     accountExists = staticmethod(accountExists)
   174 
   154 
   175     def aliasExists(dbh, address):
   155     def aliasExists(dbh, address):
   176         sql = "SELECT DISTINCT gid FROM alias WHERE gid = (SELECT gid FROM\
   156         sql = "SELECT DISTINCT gid FROM alias WHERE gid = (SELECT gid FROM\
   177  domain_name WHERE domainname = '%s') AND address = '%s'" %\
   157  domain_name WHERE domainname = '%s') AND address = '%s'" %\
   178             (address._domainname, address._localpart)
   158             (address._domainname, address._localpart)
   179         return VirtualMailManager._exists(dbh, sql)
   159         return Handler._exists(dbh, sql)
   180     aliasExists = staticmethod(aliasExists)
   160     aliasExists = staticmethod(aliasExists)
   181 
   161 
   182     def relocatedExists(dbh, address):
   162     def relocatedExists(dbh, address):
   183         sql = "SELECT gid FROM relocated WHERE gid = (SELECT gid FROM\
   163         sql = "SELECT gid FROM relocated WHERE gid = (SELECT gid FROM\
   184  domain_name WHERE domainname = '%s') AND address = '%s'" %\
   164  domain_name WHERE domainname = '%s') AND address = '%s'" %\
   185             (address._domainname, address._localpart)
   165             (address._domainname, address._localpart)
   186         return VirtualMailManager._exists(dbh, sql)
   166         return Handler._exists(dbh, sql)
   187     relocatedExists = staticmethod(relocatedExists)
   167     relocatedExists = staticmethod(relocatedExists)
   188 
   168 
   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 
   169 
   213     def __getAccount(self, address, password=None):
   170     def __getAccount(self, address, password=None):
   214         self.__dbConnect()
   171         self.__dbConnect()
   215         address = EmailAddress(address)
   172         address = EmailAddress(address)
   216         if not password is None:
   173         if not password is None:
   498 The keyword “detailed” is deprecated and will be removed in a future release.\n\
   455 The keyword “detailed” is deprecated and will be removed in a future release.\n\
   499    Please use the keyword “full” to get full details.'))
   456    Please use the keyword “full” to get full details.'))
   500         dom = self.__getDomain(domainname)
   457         dom = self.__getDomain(domainname)
   501         dominfo = dom.getInfo()
   458         dominfo = dom.getInfo()
   502         if dominfo['domainname'].startswith('xn--'):
   459         if dominfo['domainname'].startswith('xn--'):
   503             dominfo['domainname'] += ' (%s)'\
   460             dominfo['domainname'] += ' (%s)' % ace2idna(dominfo['domainname'])
   504                 % VirtualMailManager.ace2idna(dominfo['domainname'])
       
   505         if details is None:
   461         if details is None:
   506             return dominfo
   462             return dominfo
   507         elif details == 'accounts':
   463         elif details == 'accounts':
   508             return (dominfo, dom.getAccounts())
   464             return (dominfo, dom.getAccounts())
   509         elif details == 'aliasdomains':
   465         elif details == 'aliasdomains':
   573         return search(self.__dbh, pattern=pattern, like=like)
   529         return search(self.__dbh, pattern=pattern, like=like)
   574 
   530 
   575     def userAdd(self, emailaddress, password):
   531     def userAdd(self, emailaddress, password):
   576         acc = self.__getAccount(emailaddress, password)
   532         acc = self.__getAccount(emailaddress, password)
   577         if password is None:
   533         if password is None:
   578             password = self._readpass()
   534             password = read_pass()
   579             acc.setPassword(self.__pwhash(password))
   535             acc.setPassword(self.__pwhash(password))
   580         acc.save(self.__Cfg.dget('maildir.name'),
   536         acc.save(self.__Cfg.dget('maildir.name'),
   581                  self.__Cfg.dget('misc.dovecot_version'),
   537                  self.__Cfg.dget('misc.dovecot_version'),
   582                  self.__Cfg.dget('account.smtp'),
   538                  self.__Cfg.dget('account.smtp'),
   583                  self.__Cfg.dget('account.pop3'),
   539                  self.__Cfg.dget('account.pop3'),
   587 
   543 
   588     def aliasAdd(self, aliasaddress, targetaddress):
   544     def aliasAdd(self, aliasaddress, targetaddress):
   589         alias = self.__getAlias(aliasaddress, targetaddress)
   545         alias = self.__getAlias(aliasaddress, targetaddress)
   590         alias.save(long(self._postconf.read('virtual_alias_expansion_limit')))
   546         alias.save(long(self._postconf.read('virtual_alias_expansion_limit')))
   591         gid = self.__getDomain(alias._dest._domainname).getID()
   547         gid = self.__getDomain(alias._dest._domainname).getID()
   592         if gid > 0 and not VirtualMailManager.accountExists(self.__dbh,
   548         if gid > 0 and not Handler.accountExists(self.__dbh,
   593         alias._dest) and not VirtualMailManager.aliasExists(self.__dbh,
   549         alias._dest) and not Handler.aliasExists(self.__dbh,
   594         alias._dest):
   550         alias._dest):
   595             self.__warnings.append(
   551             self.__warnings.append(
   596                 _(u"The destination account/alias “%s” doesn't exist.")%\
   552                 _(u"The destination account/alias “%s” doesn't exist.")%\
   597                         alias._dest)
   553                         alias._dest)
   598 
   554 
   640         if details in ('aliases', 'full'):
   596         if details in ('aliases', 'full'):
   641             return (info, acc.getAliases())
   597             return (info, acc.getAliases())
   642         return info
   598         return info
   643 
   599 
   644     def userByID(self, uid):
   600     def userByID(self, uid):
   645         from Account import getAccountByID
   601         from Handler.Account import getAccountByID
   646         self.__dbConnect()
   602         self.__dbConnect()
   647         return getAccountByID(uid, self.__dbh)
   603         return getAccountByID(uid, self.__dbh)
   648 
   604 
   649     def userPassword(self, emailaddress, password):
   605     def userPassword(self, emailaddress, password):
   650         acc = self.__getAccount(emailaddress)
   606         acc = self.__getAccount(emailaddress)
   651         if acc.getUID() == 0:
   607         if acc.getUID() == 0:
   652            raise VMMException(_(u"Account doesn't exist"), ERR.NO_SUCH_ACCOUNT)
   608            raise VMMException(_(u"Account doesn't exist"), ERR.NO_SUCH_ACCOUNT)
   653         if password is None:
   609         if password is None:
   654             password = self._readpass()
   610             password = read_pass()
   655         acc.modify('password', self.__pwhash(password, user=emailaddress))
   611         acc.modify('password', self.__pwhash(password, user=emailaddress))
   656 
   612 
   657     def userName(self, emailaddress, name):
   613     def userName(self, emailaddress, name):
   658         acc = self.__getAccount(emailaddress)
   614         acc = self.__getAccount(emailaddress)
   659         acc.modify('name', name)
   615         acc.modify('name', name)
   693     def relocatedDelete(self, emailaddress):
   649     def relocatedDelete(self, emailaddress):
   694         relocated = self.__getRelocated(emailaddress)
   650         relocated = self.__getRelocated(emailaddress)
   695         relocated.delete()
   651         relocated.delete()
   696 
   652 
   697     def __del__(self):
   653     def __del__(self):
   698         if not self.__dbh is None and self.__dbh._isOpen:
   654         if isinstance(self.__dbh, PgSQL.Connection) and self.__dbh._isOpen:
   699             self.__dbh.close()
   655             self.__dbh.close()