* 'create_tables.pgsql'
- Removed unneeded newlines from views dovecot_user and postfix_uid
* 'update_tables_0.2.x-0.3.pgsql'
- Removed from repository
* 'update_tables_0.3.x-0.4.py'
- Added to repository
#!/usr/bin/env python# -*- coding: UTF-8 -*-# Copyright 2007-2008 VEB IT# See COPYING for distribution information.# $Id$"""The main class for vmm."""fromconstants.VERSIONimportVERSION__author__='Pascal Volk <p.volk@veb-it.de>'__version__=VERSION__revision__='rev '+'$Rev$'.split()[1]__date__='$Date$'.split()[1]importosimportreimportsysfromencodings.idnaimportToASCII,ToUnicodefromshutilimportrmtreefromsubprocessimportPopen,PIPEfrompyPgSQLimportPgSQL# python-pgsql - http://pypgsql.sourceforge.netfromExceptionsimport*importconstants.ERRORasERRfromConfigimportVMMConfigasCfgfromAccountimportAccountfromAliasimportAliasfromDomainimportDomainRE_ASCII_CHARS="""^[\x20-\x7E]*$"""RE_DOMAIN="""^(?:[a-z0-9-]{1,63}\.){1,}[a-z]{2,6}$"""RE_LOCALPART="""[^\w!#$%&'\*\+-\.\/=?^_`{\|}~]"""RE_MAILLOCATION="""^[\w]{1,20}$"""re.compile(RE_ASCII_CHARS)re.compile(RE_DOMAIN)ENCODING_IN=sys.getfilesystemencoding()ENCODING_OUT=sys.stdout.encodingorsys.getfilesystemencoding()classVirtualMailManager:"""The main class for vmm"""def__init__(self):"""Creates a new VirtualMailManager instance. Throws a VMMNotRootException if your uid is greater 0. """self.__cfgFileName='/usr/local/etc/vmm.cfg'self.__permWarnMsg="fix permissions for '%s'\n`chmod 0600 %s` would\ be great."%(self.__cfgFileName,self.__cfgFileName)self.__warnings=[]self.__Cfg=Noneself.__dbh=Noneifos.geteuid():raiseVMMNotRootException(("You are not root.\n\tGood bye!\n",ERR.CONF_NOPERM))ifself.__chkCfgFile():self.__Cfg=Cfg(self.__cfgFileName)self.__Cfg.load()self.__Cfg.check()self.__cfgSections=self.__Cfg.getsections()self.__chkenv()def__chkCfgFile(self):"""Checks the configuration file, returns bool"""ifnotos.path.isfile(self.__cfgFileName):raiseVMMException(("The file »%s« does not exists."%self.__cfgFileName,ERR.CONF_NOFILE))fstat=os.stat(self.__cfgFileName)try:fmode=self.__getFileMode()except:raiseiffmode%100andfstat.st_uid!=fstat.st_gid \orfmode%10andfstat.st_uid==fstat.st_gid:raiseVMMPermException((self.__permWarnMsg,ERR.CONF_ERROR))else:returnTruedef__chkenv(self):""""""ifnotos.path.exists(self.__Cfg.get('maildir','base')):old_umask=os.umask(0007)os.makedirs(self.__Cfg.get('maildir','base'),0770)os.umask(old_umask)elifnotos.path.isdir(self.__Cfg.get('maildir','base')):raiseVMMException(('%s is not a directory'%self.__Cfg.get('maildir','base'),ERR.NO_SUCH_DIRECTORY))foropt,valinself.__Cfg.items('bin'):ifnotos.path.exists(val):raiseVMMException(("%s doesn't exists.",ERR.NO_SUCH_BINARY))elifnotos.access(val,os.X_OK):raiseVMMException(("%s is not executable.",ERR.NOT_EXECUTABLE))def__getFileMode(self):"""Determines the file access mode from file __cfgFileName, returns int. """try:returnint(oct(os.stat(self.__cfgFileName).st_mode&0777))except:raisedef__dbConnect(self):"""Creates a pyPgSQL.PgSQL.connection instance."""try:self.__dbh=PgSQL.connect(database=self.__Cfg.get('database','name'),user=self.__Cfg.get('database','user'),host=self.__Cfg.get('database','host'),password=self.__Cfg.get('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__chkLocalpart(self,localpart):"""Validates the local part of an e-mail address. Keyword arguments: localpart -- the e-mail address that should be validated (str) """iflen(localpart)>64:raiseVMMException(('The local part is too long',ERR.LOCALPART_TOO_LONG))ifre.compile(RE_LOCALPART).search(localpart):raiseVMMException(('The local part »%s« contains invalid characters.'%localpart,ERR.LOCALPART_INVALID))returnlocalpartdef__idn2ascii(self,domainname):"""Converts an idn domainname in punycode. Keyword arguments: domainname -- the domainname to convert (str) """tmp=[]forlabelindomainname.split('.'):iflen(label)==0:continuetmp.append(ToASCII(unicode(label,ENCODING_IN)))return'.'.join(tmp)def__ace2idna(self,domainname):"""Convertis a domainname from ACE according to IDNA Keyword arguments: domainname -- the domainname to convert (str) """tmp=[]forlabelindomainname.split('.'):iflen(label)==0:continuetmp.append(ToUnicode(label))return'.'.join(tmp)def__chkDomainname(self,domainname):"""Validates the domain name of an e-mail address. Keyword arguments: domainname -- the domain name that should be validated """ifnotre.match(RE_ASCII_CHARS,domainname):domainname=self.__idn2ascii(domainname)iflen(domainname)>255:raiseVMMException(('The domain name is too long.',ERR.DOMAIN_TOO_LONG))ifnotre.match(RE_DOMAIN,domainname):raiseVMMException(('The domain name is invalid.',ERR.DOMAIN_INVALID))returndomainnamedef__chkEmailAddress(self,address):try:localpart,domain=address.split('@')exceptValueError:raiseVMMException(("Missing '@' sign in e-mail address »%s«."%address,ERR.INVALID_ADDRESS))exceptAttributeError:raiseVMMException(("»%s« looks not like an e-mail address."%address,ERR.INVALID_ADDRESS))domain=self.__chkDomainname(domain)localpart=self.__chkLocalpart(localpart)return'%s@%s'%(localpart,domain)def__getAccount(self,address,password=None):address=self.__chkEmailAddress(address)self.__dbConnect()ifnotpasswordisNone:password=self.__pwhash(password)returnAccount(self.__dbh,address,password)def__getAlias(self,address,destination=None):address=self.__chkEmailAddress(address)ifnotdestinationisNone:ifdestination.count('@'):destination=self.__chkEmailAddress(destination)else:destination=self.__chkLocalpart(destination)self.__dbConnect()returnAlias(self.__dbh,address,destination)def__getDomain(self,domainname,transport=None):domainname=self.__chkDomainname(domainname)iftransportisNone:transport=self.__Cfg.get('misc','transport')self.__dbConnect()returnDomain(self.__dbh,domainname,self.__Cfg.get('maildir','base'),transport)def__getDiskUsage(self,directory):"""Estimate file space usage for the given directory. Keyword arguments: directory -- the directory to summarize recursively disk usage for """returnPopen([self.__Cfg.get('bin','du'),"-hs",directory],stdout=PIPE).communicate()[0].split('\t')[0]def__makedir(self,directory,mode=None,uid=None,gid=None):ifmodeisNone:mode=self.__Cfg.getint('maildir','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.get('maildir','base')domdirdirs=domdir.replace(basedir+'/','').split('/')os.chdir(basedir)ifnotos.path.isdir(domdirdirs[0]):self.__makedir(domdirdirs[0],489,0,self.__Cfg.getint('misc','gid_mail'))os.chdir(domdirdirs[0])os.umask(0007)self.__makedir(domdirdirs[1],self.__Cfg.getint('domdir','mode'),0,gid)os.chdir(oldpwd)def__maildirmake(self,domdir,uid,gid):"""Creates maildirs and maildir subfolders. Keyword arguments: uid -- user id from the account gid -- group id from the account """os.umask(0007)oldpwd=os.getcwd()os.chdir(domdir)maildir='%s'%self.__Cfg.get('maildir','folder')folders=[maildir,maildir+'/.Drafts',maildir+'/.Sent',maildir+'/.Templates',maildir+'/.Trash']subdirs=['cur','new','tmp']mode=self.__Cfg.getint('maildir','mode')self.__makedir('%s'%uid,mode,uid,gid)os.chdir('%s'%uid)forfolderinfolders:self.__makedir(folder,mode,uid,gid)forsubdirinsubdirs:self.__makedir(folder+'/'+subdir,mode,uid,gid)os.chdir(oldpwd)def__maildirdelete(self,domdir,uid,gid):ifuid>0andgid>0:maildir='%s'%uidifmaildir.count('..')ordomdir.count('..'):raiseVMMException(('FATAL: ".." in maildir path detected.',ERR.FOUND_DOTS_IN_PATH))ifos.path.isdir(domdir):os.chdir(domdir)ifos.path.isdir(maildir):mdstat=os.stat(maildir)if(mdstat.st_uid,mdstat.st_gid)!=(uid,gid):raiseVMMException(('FATAL: owner/group mismatch in maildir detected',ERR.MAILDIR_PERM_MISMATCH))rmtree(maildir,ignore_errors=True)def__domdirdelete(self,domdir,gid):ifgid>0:basedir='%s'%self.__Cfg.get('maildir','base')domdirdirs=domdir.replace(basedir+'/','').split('/')ifbasedir.count('..')ordomdir.count('..'):raiseVMMException(('FATAL: ".." in domain directory path detected.',ERR.FOUND_DOTS_IN_PATH))ifos.path.isdir('%s/%s'%(basedir,domdirdirs[0])):os.chdir('%s/%s'%(basedir,domdirdirs[0]))ifos.lstat(domdirdirs[1]).st_gid!=gid:raiseVMMException(('FATAL: group mismatch in domain directory detected',ERR.DOMAINDIR_GROUP_MISMATCH))rmtree(domdirdirs[1],ignore_errors=True)def__pwhash(self,password,scheme=None,user=None):# XXX alle Schemen berücksichtigen XXXifschemeisNone:scheme=self.__Cfg.get('misc','passwdscheme')returnPopen([self.__Cfg.get('bin','dovecotpw'),'-s',scheme,'-p',password],stdout=PIPE).communicate()[0][len(scheme)+2:-1]defhasWarnings(self):"""Checks if warnings are present, returns bool."""returnbool(len(self.__warnings))defgetWarnings(self):"""Returns a list with all available warnings."""returnself.__warningsdefsetupIsDone(self):"""Checks if vmm is configured, returns bool"""try:returnself.__Cfg.getboolean('config','done')exceptValueError,e:raiseVMMConfigException('Configurtion error: "'+str(e)+'"\n(in section "Connfig", option "done")'+'\nsee also: vmm.cfg(5)\n')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): 'database', 'maildir', 'bin' or 'misc' """try:ifnotsection:self.__Cfg.configure(self.__cfgSections)elifsectionnotinself.__cfgSections:raiseVMMException(("Invalid section: »%s«"%section,ERR.INVALID_SECTION))else:self.__Cfg.configure([section])except:raisedefdomain_add(self,domainname,transport=None):dom=self.__getDomain(domainname,transport)dom.save()self.__domdirmake(dom.getDir(),dom.getID())defdomain_transport(self,domainname,transport):dom=self.__getDomain(domainname,None)dom.updateTransport(transport)defdomain_delete(self,domainname,force=None):ifnotforceisNoneandforcenotin['deluser','delalias','delall']:raiseVMMDomainException(('Invalid argument: «%s»'%force,ERR.INVALID_OPTION))dom=self.__getDomain(domainname)gid=dom.getID()domdir=dom.getDir()ifself.__Cfg.getboolean('misc','forcedel')orforce=='delall':dom.delete(True,True)elifforce=='deluser':dom.delete(delUser=True)elifforce=='delalias':dom.delete(delAlias=True)else:dom.delete()ifself.__Cfg.getboolean('domdir','delete'):self.__domdirdelete(domdir,gid)defdomain_info(self,domainname,detailed=None):dom=self.__getDomain(domainname)dominfo=dom.getInfo()ifdominfo['domainname'].startswith('xn--'):dominfo['domainname']+=' (%s)'\%self.__ace2idna(dominfo['domainname'])ifdominfo['aliases']isNone:dominfo['aliases']=0ifdetailedisNone:returndominfoelifdetailed=='detailed':returndominfo,dom.getAccounts(),dom.getAliases()else:raiseVMMDomainException(('Invalid argument: »%s«'%detailed,ERR.INVALID_OPTION))defuser_add(self,emailaddress,password):acc=self.__getAccount(emailaddress,password)acc.save(self.__Cfg.get('maildir','folder'))self.__maildirmake(acc.getDir('domain'),acc.getUID(),acc.getGID())defalias_add(self,aliasaddress,targetaddress):alias=self.__getAlias(aliasaddress,targetaddress)alias.save()defuser_delete(self,emailaddress):acc=self.__getAccount(emailaddress)uid=acc.getUID()gid=acc.getGID()acc.delete()ifself.__Cfg.getboolean('maildir','delete'):self.__maildirdelete(acc.getDir('domain'),uid,gid)defalias_info(self,aliasaddress):alias=self.__getAlias(aliasaddress)returnalias.getInfo()defalias_delete(self,aliasaddress,targetaddress=None):alias=self.__getAlias(aliasaddress,targetaddress)alias.delete()defuser_info(self,emailaddress,diskusage=False):acc=self.__getAccount(emailaddress)info=acc.getInfo()ifself.__Cfg.getboolean('maildir','diskusage')ordiskusage:info['disk usage']=self.__getDiskUsage('%(maildir)s'%info)returninfodefuser_password(self,emailaddress,password):acc=self.__getAccount(emailaddress)acc.modify('password',self.__pwhash(password))defuser_name(self,emailaddress,name):acc=self.__getAccount(emailaddress)acc.modify('name',name)defuser_disable(self,emailaddress):acc=self.__getAccount(emailaddress)acc.disable()defuser_enable(self,emailaddress):acc=self.__getAccount(emailaddress)acc.enable()def__del__(self):ifnotself.__dbhisNoneandself.__dbh._isOpen:self.__dbh.close()