VMM/*: Moved some methods from classes to modules __init__.
- Adjusted many import statements.
- Small adjustments and whitespace cosmetics in Config.py
# -*- coding: UTF-8 -*-# Copyright (c) 2007 - 2010, Pascal Volk# See COPYING for distribution information.""" VirtualMailManager.Handler A wrapper class. It wraps round all other classes and does some dependencies checks. Additionally it communicates with the PostgreSQL database, creates or deletes directories of domains or users."""importosimportrefromshutilimportrmtreefromsubprocessimportPopen,PIPEfrompyPgSQLimportPgSQL# python-pgsql - http://pypgsql.sourceforge.netimportVirtualMailManager.constants.ERRORasERRfromVirtualMailManagerimportENCODING,ace2idna,exec_ok,read_passfromVirtualMailManager.AccountimportAccountfromVirtualMailManager.AliasimportAliasfromVirtualMailManager.AliasDomainimportAliasDomainfromVirtualMailManager.ConfigimportConfigasCfgfromVirtualMailManager.DomainimportDomainfromVirtualMailManager.EmailAddressimportEmailAddressfromVirtualMailManager.Exceptionsimport*fromVirtualMailManager.RelocatedimportRelocatedfromVirtualMailManager.ext.PostconfimportPostconfSALTCHARS='./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'RE_DOMAIN_SRCH="""^[a-z0-9-\.]+$"""RE_LOCALPART="""[^\w!#$%&'\*\+-\.\/=?^_`{\|}~]"""RE_MBOX_NAMES="""^[\x20-\x25\x27-\x7E]*$"""classHandler(object):"""Wrapper class to simplify the access on all the stuff from VirtualMailManager"""# TODO: accept a LazyConfig object as argument__slots__=('__Cfg','__cfgFileName','__dbh','__scheme','__warnings','_postconf')def__init__(self):"""Creates a new Handler instance. Throws a VMMNotRootException if your uid is greater 0. """self.__cfgFileName=''self.__warnings=[]self.__Cfg=Noneself.__dbh=Noneifos.geteuid():raiseVMMNotRootException(_(u"You are not root.\n\tGood bye!\n"),ERR.CONF_NOPERM)ifself.__chkCfgFile():self.__Cfg=Cfg(self.__cfgFileName)self.__Cfg.load()ifnotos.sys.argv[1]in('cf','configure','h','help','v','version'):self.__Cfg.check()self.__chkenv()self.__scheme=self.__Cfg.dget('misc.password_scheme')self._postconf=Postconf(self.__Cfg.dget('bin.postconf'))def__findCfgFile(self):forpathin['/root','/usr/local/etc','/etc']:tmp=os.path.join(path,'vmm.cfg')ifos.path.isfile(tmp):self.__cfgFileName=tmpbreakifnotlen(self.__cfgFileName):raiseVMMException(_(u"No “vmm.cfg” found in: /root:/usr/local/etc:/etc"),ERR.CONF_NOFILE)def__chkCfgFile(self):"""Checks the configuration file, returns bool"""self.__findCfgFile()fstat=os.stat(self.__cfgFileName)fmode=int(oct(fstat.st_mode&0777))iffmode%100andfstat.st_uid!=fstat.st_gid \orfmode%10andfstat.st_uid==fstat.st_gid:raiseVMMPermException(_(u'fix permissions (%(perms)s) for “%(file)s”\n\`chmod 0600 %(file)s` would be great.')%{'file':self.__cfgFileName,'perms':fmode},ERR.CONF_WRONGPERM)else:returnTruedef__chkenv(self):""""""basedir=self.__Cfg.dget('misc.base_directory')ifnotos.path.exists(basedir):old_umask=os.umask(0006)os.makedirs(basedir,0771)os.chown(basedir,0,self.__Cfg.dget('misc.gid_mail'))os.umask(old_umask)elifnotos.path.isdir(basedir):raiseVMMException(_(u'“%s” is not a directory.\n\(vmm.cfg: section "misc", option "base_directory")')%basedir,ERR.NO_SUCH_DIRECTORY)foropt,valinself.__Cfg.items('bin'):try:exec_ok(val)exceptVMMException,e:code=e.code()ifcodeisERR.NO_SUCH_BINARY:raiseVMMException(_(u'“%(binary)s” doesn\'t exist.\n\(vmm.cfg: section "bin", option "%(option)s")')%{'binary':val,'option':opt},ERR.NO_SUCH_BINARY)elifcodeisERR.NOT_EXECUTABLE:raiseVMMException(_(u'“%(binary)s” is not executable.\n\(vmm.cfg: section "bin", option "%(option)s")')%{'binary':val,'option':opt},ERR.NOT_EXECUTABLE)else:raisedef__dbConnect(self):"""Creates a pyPgSQL.PgSQL.connection instance."""ifself.__dbhisNoneor(isinstance(self.__dbh,PgSQL.Connection)andnotself.__dbh._isOpen):try:self.__dbh=PgSQL.connect(database=self.__Cfg.dget('database.name'),user=self.__Cfg.pget('database.user'),host=self.__Cfg.dget('database.host'),password=self.__Cfg.pget('database.pass'),client_encoding='utf8',unicode_results=True)dbc=self.__dbh.cursor()dbc.execute("SET NAMES 'UTF8'")dbc.close()exceptPgSQL.libpq.DatabaseError,e:raiseVMMException(str(e),ERR.DATABASE_ERROR)def_exists(dbh,query):dbc=dbh.cursor()dbc.execute(query)gid=dbc.fetchone()dbc.close()ifgidisNone:returnFalseelse:returnTrue_exists=staticmethod(_exists)defaccountExists(dbh,address):sql="SELECT gid FROM users WHERE gid = (SELECT gid FROM domain_name\ WHERE domainname = '%s') AND local_part = '%s'"%(address._domainname,address._localpart)returnHandler._exists(dbh,sql)accountExists=staticmethod(accountExists)defaliasExists(dbh,address):sql="SELECT DISTINCT gid FROM alias WHERE gid = (SELECT gid FROM\ domain_name WHERE domainname = '%s') AND address = '%s'"%\(address._domainname,address._localpart)returnHandler._exists(dbh,sql)aliasExists=staticmethod(aliasExists)defrelocatedExists(dbh,address):sql="SELECT gid FROM relocated WHERE gid = (SELECT gid FROM\ domain_name WHERE domainname = '%s') AND address = '%s'"%\(address._domainname,address._localpart)returnHandler._exists(dbh,sql)relocatedExists=staticmethod(relocatedExists)def__getAccount(self,address,password=None):self.__dbConnect()address=EmailAddress(address)ifnotpasswordisNone:password=self.__pwhash(password)returnAccount(self.__dbh,address,password)def__getAlias(self,address,destination=None):self.__dbConnect()address=EmailAddress(address)ifdestinationisnotNone:destination=EmailAddress(destination)returnAlias(self.__dbh,address,destination)def__getRelocated(self,address,destination=None):self.__dbConnect()address=EmailAddress(address)ifdestinationisnotNone:destination=EmailAddress(destination)returnRelocated(self.__dbh,address,destination)def__getDomain(self,domainname,transport=None):iftransportisNone:transport=self.__Cfg.dget('misc.transport')self.__dbConnect()returnDomain(self.__dbh,domainname,self.__Cfg.dget('misc.base_directory'),transport)def__getDiskUsage(self,directory):"""Estimate file space usage for the given directory. Keyword arguments: directory -- the directory to summarize recursively disk usage for """ifself.__isdir(directory):returnPopen([self.__Cfg.dget('bin.du'),"-hs",directory],stdout=PIPE).communicate()[0].split('\t')[0]else:return0def__isdir(self,directory):isdir=os.path.isdir(directory)ifnotisdir:self.__warnings.append(_('No such directory: %s')%directory)returnisdirdef__makedir(self,directory,mode=None,uid=None,gid=None):ifmodeisNone:mode=self.__Cfg.dget('account.directory_mode')ifuidisNone:uid=0ifgidisNone:gid=0os.makedirs(directory,mode)os.chown(directory,uid,gid)def__domDirMake(self,domdir,gid):os.umask(0006)oldpwd=os.getcwd()basedir=self.__Cfg.dget('misc.base_directory')domdirdirs=domdir.replace(basedir+'/','').split('/')os.chdir(basedir)ifnotos.path.isdir(domdirdirs[0]):self.__makedir(domdirdirs[0],489,0,self.__Cfg.dget('misc.gid_mail'))os.chdir(domdirdirs[0])os.umask(0007)self.__makedir(domdirdirs[1],self.__Cfg.dget('domain.directory_mode'),0,gid)os.chdir(oldpwd)def__subscribeFL(self,folderlist,uid,gid):fname=os.path.join(self.__Cfg.dget('maildir.name'),'subscriptions')sf=file(fname,'w')forfinfolderlist:sf.write(f+'\n')sf.flush()sf.close()os.chown(fname,uid,gid)os.chmod(fname,384)def__mailDirMake(self,domdir,uid,gid):"""Creates maildirs and maildir subfolders. Keyword arguments: domdir -- the path to the domain directory uid -- user id from the account gid -- group id from the account """os.umask(0007)oldpwd=os.getcwd()os.chdir(domdir)maildir=self.__Cfg.dget('maildir.name')folders=[maildir]forfolderinself.__Cfg.dget('maildir.folders').split(':'):folder=folder.strip()iflen(folder)andnotfolder.count('..')\andre.match(RE_MBOX_NAMES,folder):folders.append('%s/.%s'%(maildir,folder))subdirs=['cur','new','tmp']mode=self.__Cfg.dget('account.directory_mode')self.__makedir('%s'%uid,mode,uid,gid)os.chdir('%s'%uid)forfolderinfolders:self.__makedir(folder,mode,uid,gid)forsubdirinsubdirs:self.__makedir(os.path.join(folder,subdir),mode,uid,gid)self.__subscribeFL([f.replace(maildir+'/.','')forfinfolders[1:]],uid,gid)os.chdir(oldpwd)def__userDirDelete(self,domdir,uid,gid):ifuid>0andgid>0:userdir='%s'%uidifuserdir.count('..')ordomdir.count('..'):raiseVMMException(_(u'Found ".." in home directory path.'),ERR.FOUND_DOTS_IN_PATH)ifos.path.isdir(domdir):os.chdir(domdir)ifos.path.isdir(userdir):mdstat=os.stat(userdir)if(mdstat.st_uid,mdstat.st_gid)!=(uid,gid):raiseVMMException(_(u'Detected owner/group mismatch in home directory.'),ERR.MAILDIR_PERM_MISMATCH)rmtree(userdir,ignore_errors=True)else:raiseVMMException(_(u"No such directory: %s")%os.path.join(domdir,userdir),ERR.NO_SUCH_DIRECTORY)def__domDirDelete(self,domdir,gid):ifgid>0:ifnotself.__isdir(domdir):returnbasedir=self.__Cfg.dget('misc.base_directory')domdirdirs=domdir.replace(basedir+'/','').split('/')domdirparent=os.path.join(basedir,domdirdirs[0])ifbasedir.count('..')ordomdir.count('..'):raiseVMMException(_(u'Found ".." in domain directory path.'),ERR.FOUND_DOTS_IN_PATH)ifos.path.isdir(domdirparent):os.chdir(domdirparent)ifos.lstat(domdirdirs[1]).st_gid!=gid:raiseVMMException(_(u'Detected group mismatch in domain directory.'),ERR.DOMAINDIR_GROUP_MISMATCH)rmtree(domdirdirs[1],ignore_errors=True)def__getSalt(self):fromrandomimportchoicesalt=Noneifself.__scheme=='CRYPT':salt='%s%s'%(choice(SALTCHARS),choice(SALTCHARS))elifself.__schemein['MD5','MD5-CRYPT']:salt='$1$%s$'%''.join([choice(SALTCHARS)forxinxrange(8)])returnsaltdef__pwCrypt(self,password):# for: CRYPT, MD5 and MD5-CRYPTfromcryptimportcryptreturncrypt(password,self.__getSalt())def__pwSHA1(self,password):# for: SHA/SHA1importshafrombase64importstandard_b64encodesha1=sha.new(password)returnstandard_b64encode(sha1.digest())def__pwMD5(self,password,emailaddress=None):importmd5_md5=md5.new(password)ifself.__scheme=='LDAP-MD5':frombase64importstandard_b64encodereturnstandard_b64encode(_md5.digest())elifself.__scheme=='PLAIN-MD5':return_md5.hexdigest()elifself.__scheme=='DIGEST-MD5'andemailaddressisnotNone:# use an empty realm - works better with usenames like user@dom_md5=md5.new('%s::%s'%(emailaddress,password))return_md5.hexdigest()def__pwMD4(self,password):# for: PLAIN-MD4fromCrypto.HashimportMD4_md4=MD4.new(password)return_md4.hexdigest()def__pwhash(self,password,scheme=None,user=None):ifschemeisnotNone:self.__scheme=schemeifself.__schemein['CRYPT','MD5','MD5-CRYPT']:return'{%s}%s'%(self.__scheme,self.__pwCrypt(password))elifself.__schemein['SHA','SHA1']:return'{%s}%s'%(self.__scheme,self.__pwSHA1(password))elifself.__schemein['PLAIN-MD5','LDAP-MD5','DIGEST-MD5']:return'{%s}%s'%(self.__scheme,self.__pwMD5(password,user))elifself.__scheme=='MD4':return'{%s}%s'%(self.__scheme,self.__pwMD4(password))elifself.__schemein['SMD5','SSHA','CRAM-MD5','HMAC-MD5','LANMAN','NTLM','RPA']:returnPopen([self.__Cfg.dget('bin.dovecotpw'),'-s',self.__scheme,'-p',password],stdout=PIPE).communicate()[0][:-1]else:return'{%s}%s'%(self.__scheme,password)defhasWarnings(self):"""Checks if warnings are present, returns bool."""returnbool(len(self.__warnings))defgetWarnings(self):"""Returns a list with all available warnings."""returnself.__warningsdefcfgDget(self,option):returnself.__Cfg.dget(option)defcfgPget(self,option):returnself.__Cfg.pget(option)defcfgSet(self,option,value):returnself.__Cfg.set(option,value)defconfigure(self,section=None):"""Starts interactive configuration. Configures in interactive mode options in the given section. If no section is given (default) all options from all sections will be prompted. Keyword arguments: section -- the section to configure (default None): """ifsectionisNone:self.__Cfg.configure(self.__Cfg.getsections())elifself.__Cfg.has_section(section):self.__Cfg.configure([section])else:raiseVMMException(_(u"Invalid section: “%s”")%section,ERR.INVALID_SECTION)defdomainAdd(self,domainname,transport=None):dom=self.__getDomain(domainname,transport)dom.save()self.__domDirMake(dom.getDir(),dom.getID())defdomainTransport(self,domainname,transport,force=None):ifforceisnotNoneandforce!='force':raiseVMMDomainException(_(u"Invalid argument: “%s”")%force,ERR.INVALID_OPTION)dom=self.__getDomain(domainname,None)ifforceisNone:dom.updateTransport(transport)else:dom.updateTransport(transport,force=True)defdomainDelete(self,domainname,force=None):ifnotforceisNoneandforcenotin['deluser','delalias','delall']:raiseVMMDomainException(_(u"Invalid argument: “%s”")%force,ERR.INVALID_OPTION)dom=self.__getDomain(domainname)gid=dom.getID()domdir=dom.getDir()ifself.__Cfg.dget('domain.force_deletion')orforce=='delall':dom.delete(True,True)elifforce=='deluser':dom.delete(delUser=True)elifforce=='delalias':dom.delete(delAlias=True)else:dom.delete()ifself.__Cfg.dget('domain.delete_directory'):self.__domDirDelete(domdir,gid)defdomainInfo(self,domainname,details=None):ifdetailsnotin[None,'accounts','aliasdomains','aliases','full','relocated','detailed']:raiseVMMException(_(u'Invalid argument: “%s”')%details,ERR.INVALID_AGUMENT)ifdetails=='detailed':details='full'self.__warnings.append(_(u'\The keyword “detailed” is deprecated and will be removed in a future release.\n\ Please use the keyword “full” to get full details.'))dom=self.__getDomain(domainname)dominfo=dom.getInfo()ifdominfo['domainname'].startswith('xn--'):dominfo['domainname']+=' (%s)'%ace2idna(dominfo['domainname'])ifdetailsisNone:returndominfoelifdetails=='accounts':return(dominfo,dom.getAccounts())elifdetails=='aliasdomains':return(dominfo,dom.getAliaseNames())elifdetails=='aliases':return(dominfo,dom.getAliases())elifdetails=='relocated':return(dominfo,dom.getRelocated())else:return(dominfo,dom.getAliaseNames(),dom.getAccounts(),dom.getAliases(),dom.getRelocated())defaliasDomainAdd(self,aliasname,domainname):"""Adds an alias domain to the domain. Keyword arguments: aliasname -- the name of the alias domain (str) domainname -- name of the target domain (str) """dom=self.__getDomain(domainname)aliasDom=AliasDomain(self.__dbh,aliasname,dom)aliasDom.save()defaliasDomainInfo(self,aliasname):self.__dbConnect()aliasDom=AliasDomain(self.__dbh,aliasname,None)returnaliasDom.info()defaliasDomainSwitch(self,aliasname,domainname):"""Modifies the target domain of an existing alias domain. Keyword arguments: aliasname -- the name of the alias domain (str) domainname -- name of the new target domain (str) """dom=self.__getDomain(domainname)aliasDom=AliasDomain(self.__dbh,aliasname,dom)aliasDom.switch()defaliasDomainDelete(self,aliasname):"""Deletes the specified alias domain. Keyword arguments: aliasname -- the name of the alias domain (str) """self.__dbConnect()aliasDom=AliasDomain(self.__dbh,aliasname,None)aliasDom.delete()defdomainList(self,pattern=None):fromDomainimportsearchlike=FalseifpatternisnotNone:ifpattern.startswith('%')orpattern.endswith('%'):like=Trueifpattern.startswith('%')andpattern.endswith('%'):domain=pattern[1:-1]elifpattern.startswith('%'):domain=pattern[1:]elifpattern.endswith('%'):domain=pattern[:-1]ifnotre.match(RE_DOMAIN_SRCH,domain):raiseVMMException(_(u"The pattern “%s” contains invalid characters.")%pattern,ERR.DOMAIN_INVALID)self.__dbConnect()returnsearch(self.__dbh,pattern=pattern,like=like)defuserAdd(self,emailaddress,password):acc=self.__getAccount(emailaddress,password)ifpasswordisNone:password=read_pass()acc.setPassword(self.__pwhash(password))acc.save(self.__Cfg.dget('maildir.name'),self.__Cfg.dget('misc.dovecot_version'),self.__Cfg.dget('account.smtp'),self.__Cfg.dget('account.pop3'),self.__Cfg.dget('account.imap'),self.__Cfg.dget('account.sieve'))self.__mailDirMake(acc.getDir('domain'),acc.getUID(),acc.getGID())defaliasAdd(self,aliasaddress,targetaddress):alias=self.__getAlias(aliasaddress,targetaddress)alias.save(long(self._postconf.read('virtual_alias_expansion_limit')))gid=self.__getDomain(alias._dest._domainname).getID()ifgid>0andnotHandler.accountExists(self.__dbh,alias._dest)andnotHandler.aliasExists(self.__dbh,alias._dest):self.__warnings.append(_(u"The destination account/alias “%s” doesn't exist.")%\alias._dest)defuserDelete(self,emailaddress,force=None):ifforcenotin[None,'delalias']:raiseVMMException(_(u"Invalid argument: “%s”")%force,ERR.INVALID_AGUMENT)acc=self.__getAccount(emailaddress)uid=acc.getUID()gid=acc.getGID()acc.delete(force)ifself.__Cfg.dget('account.delete_directory'):try:self.__userDirDelete(acc.getDir('domain'),uid,gid)exceptVMMException,e:ife.code()in[ERR.FOUND_DOTS_IN_PATH,ERR.MAILDIR_PERM_MISMATCH,ERR.NO_SUCH_DIRECTORY]:warning=_(u"""\The account has been successfully deleted from the database. But an error occurred while deleting the following directory: “%(directory)s” Reason: %(reason)s""")%{'directory':acc.getDir('home'),'reason':e.msg()}self.__warnings.append(warning)else:raiseedefaliasInfo(self,aliasaddress):alias=self.__getAlias(aliasaddress)returnalias.getInfo()defaliasDelete(self,aliasaddress,targetaddress=None):alias=self.__getAlias(aliasaddress,targetaddress)alias.delete()defuserInfo(self,emailaddress,details=None):ifdetailsnotin(None,'du','aliases','full'):raiseVMMException(_(u'Invalid argument: “%s”')%details,ERR.INVALID_AGUMENT)acc=self.__getAccount(emailaddress)info=acc.getInfo(self.__Cfg.dget('misc.dovecot_version'))ifself.__Cfg.dget('account.disk_usage')ordetailsin('du','full'):info['disk usage']=self.__getDiskUsage('%(maildir)s'%info)ifdetailsin(None,'du'):returninfoifdetailsin('aliases','full'):return(info,acc.getAliases())returninfodefuserByID(self,uid):fromHandler.AccountimportgetAccountByIDself.__dbConnect()returngetAccountByID(uid,self.__dbh)defuserPassword(self,emailaddress,password):acc=self.__getAccount(emailaddress)ifacc.getUID()==0:raiseVMMException(_(u"Account doesn't exist"),ERR.NO_SUCH_ACCOUNT)ifpasswordisNone:password=read_pass()acc.modify('password',self.__pwhash(password,user=emailaddress))defuserName(self,emailaddress,name):acc=self.__getAccount(emailaddress)acc.modify('name',name)defuserTransport(self,emailaddress,transport):acc=self.__getAccount(emailaddress)acc.modify('transport',transport)defuserDisable(self,emailaddress,service=None):ifservice=='managesieve':service='sieve'self.__warnings.append(_(u'\The service name “managesieve” is deprecated and will be removed\n\ in a future release.\n\ Please use the service name “sieve” instead.'))acc=self.__getAccount(emailaddress)acc.disable(self.__Cfg.dget('misc.dovecot_version'),service)defuserEnable(self,emailaddress,service=None):ifservice=='managesieve':service='sieve'self.__warnings.append(_(u'\The service name “managesieve” is deprecated and will be removed\n\ in a future release.\n\ Please use the service name “sieve” instead.'))acc=self.__getAccount(emailaddress)acc.enable(self.__Cfg.dget('misc.dovecot_version'),service)defrelocatedAdd(self,emailaddress,targetaddress):relocated=self.__getRelocated(emailaddress,targetaddress)relocated.save()defrelocatedInfo(self,emailaddress):relocated=self.__getRelocated(emailaddress)returnrelocated.getInfo()defrelocatedDelete(self,emailaddress):relocated=self.__getRelocated(emailaddress)relocated.delete()def__del__(self):ifisinstance(self.__dbh,PgSQL.Connection)andself.__dbh._isOpen:self.__dbh.close()