VirtualMailManager/VirtualMailManager.py
changeset 32 ceb700bc4a80
parent 28 87da30d30fde
child 34 6d74e20c5b3b
equal deleted inserted replaced
31:b7a7e566833c 32:ceb700bc4a80
    14 __date__ = '$Date$'.split()[1]
    14 __date__ = '$Date$'.split()[1]
    15 
    15 
    16 import os
    16 import os
    17 import re
    17 import re
    18 import sys
    18 import sys
       
    19 import gettext
    19 from encodings.idna import ToASCII, ToUnicode
    20 from encodings.idna import ToASCII, ToUnicode
    20 from shutil import rmtree
    21 from shutil import rmtree
    21 from subprocess import Popen, PIPE
    22 from subprocess import Popen, PIPE
    22 
    23 
    23 from pyPgSQL import PgSQL # python-pgsql - http://pypgsql.sourceforge.net
    24 from pyPgSQL import PgSQL # python-pgsql - http://pypgsql.sourceforge.net
    38 re.compile(RE_DOMAIN)
    39 re.compile(RE_DOMAIN)
    39 
    40 
    40 ENCODING_IN = sys.getfilesystemencoding()
    41 ENCODING_IN = sys.getfilesystemencoding()
    41 ENCODING_OUT = sys.stdout.encoding or sys.getfilesystemencoding()
    42 ENCODING_OUT = sys.stdout.encoding or sys.getfilesystemencoding()
    42 
    43 
       
    44 gettext.bindtextdomain('vmm', '/usr/local/share/locale')
       
    45 gettext.textdomain('vmm')
       
    46 _ = gettext.gettext
       
    47 
    43 class VirtualMailManager:
    48 class VirtualMailManager:
    44     """The main class for vmm"""
    49     """The main class for vmm"""
    45     def __init__(self):
    50     def __init__(self):
    46         """Creates a new VirtualMailManager instance.
    51         """Creates a new VirtualMailManager instance.
    47         Throws a VMMNotRootException if your uid is greater 0.
    52         Throws a VMMNotRootException if your uid is greater 0.
    48         """
    53         """
    49         self.__cfgFileName = '/usr/local/etc/vmm.cfg'
    54         self.__cfgFileName = '/usr/local/etc/vmm.cfg'
    50         self.__permWarnMsg = "fix permissions for '%s'\n`chmod 0600 %s` would\
    55         self.__permWarnMsg = _("fix permissions for '%s'\n`chmod 0600 %s` would\
    51  be great." % (self.__cfgFileName, self.__cfgFileName)
    56  be great.") % (self.__cfgFileName, self.__cfgFileName)
    52         self.__warnings = []
    57         self.__warnings = []
    53         self.__Cfg = None
    58         self.__Cfg = None
    54         self.__dbh = None
    59         self.__dbh = None
    55 
    60 
    56         if os.geteuid():
    61         if os.geteuid():
    57             raise VMMNotRootException(("You are not root.\n\tGood bye!\n",
    62             raise VMMNotRootException((_("You are not root.\n\tGood bye!\n"),
    58                 ERR.CONF_NOPERM))
    63                 ERR.CONF_NOPERM))
    59         if self.__chkCfgFile():
    64         if self.__chkCfgFile():
    60             self.__Cfg = Cfg(self.__cfgFileName)
    65             self.__Cfg = Cfg(self.__cfgFileName)
    61             self.__Cfg.load()
    66             self.__Cfg.load()
    62             self.__Cfg.check()
    67             self.__Cfg.check()
    66             self.__chkenv()
    71             self.__chkenv()
    67 
    72 
    68     def __chkCfgFile(self):
    73     def __chkCfgFile(self):
    69         """Checks the configuration file, returns bool"""
    74         """Checks the configuration file, returns bool"""
    70         if not os.path.isfile(self.__cfgFileName):
    75         if not os.path.isfile(self.__cfgFileName):
    71             raise VMMException(("The file »%s« does not exists." %
    76             raise VMMException((_("The file »%s« does not exists.") %
    72                 self.__cfgFileName, ERR.CONF_NOFILE))
    77                 self.__cfgFileName, ERR.CONF_NOFILE))
    73         fstat = os.stat(self.__cfgFileName)
    78         fstat = os.stat(self.__cfgFileName)
    74         try:
    79         try:
    75             fmode = self.__getFileMode()
    80             fmode = self.__getFileMode()
    76         except:
    81         except:
    88             os.makedirs(self.__Cfg.get('domdir', 'base'), 0771)
    93             os.makedirs(self.__Cfg.get('domdir', 'base'), 0771)
    89             os.chown(self.__Cfg.get('domdir', 'base'), 0,
    94             os.chown(self.__Cfg.get('domdir', 'base'), 0,
    90                     self.__Cfg.getint('misc', 'gid_mail'))
    95                     self.__Cfg.getint('misc', 'gid_mail'))
    91             os.umask(old_umask)
    96             os.umask(old_umask)
    92         elif not os.path.isdir(self.__Cfg.get('domdir', 'base')):
    97         elif not os.path.isdir(self.__Cfg.get('domdir', 'base')):
    93             raise VMMException(('%s is not a directory' %
    98             raise VMMException((_('%s is not a directory') %
    94                 self.__Cfg.get('domdir', 'base'), ERR.NO_SUCH_DIRECTORY))
    99                 self.__Cfg.get('domdir', 'base'), ERR.NO_SUCH_DIRECTORY))
    95         for opt, val in self.__Cfg.items('bin'):
   100         for opt, val in self.__Cfg.items('bin'):
    96             if not os.path.exists(val):
   101             if not os.path.exists(val):
    97                 raise VMMException(("%s doesn't exists." % val,
   102                 raise VMMException((_("%s doesn't exists.") % val,
    98                     ERR.NO_SUCH_BINARY))
   103                     ERR.NO_SUCH_BINARY))
    99             elif not os.access(val, os.X_OK):
   104             elif not os.access(val, os.X_OK):
   100                 raise VMMException(("%s is not executable." % val,
   105                 raise VMMException((_("%s is not executable.") % val,
   101                     ERR.NOT_EXECUTABLE))
   106                     ERR.NOT_EXECUTABLE))
   102 
   107 
   103     def __getFileMode(self):
   108     def __getFileMode(self):
   104         """Determines the file access mode from file __cfgFileName,
   109         """Determines the file access mode from file __cfgFileName,
   105         returns int.
   110         returns int.
   129         
   134         
   130         Keyword arguments:
   135         Keyword arguments:
   131         localpart -- the e-mail address that should be validated (str)
   136         localpart -- the e-mail address that should be validated (str)
   132         """
   137         """
   133         if len(localpart) > 64:
   138         if len(localpart) > 64:
   134             raise VMMException(('The local part is too long',
   139             raise VMMException((_('The local part is too long'),
   135                 ERR.LOCALPART_TOO_LONG))
   140                 ERR.LOCALPART_TOO_LONG))
   136         if re.compile(RE_LOCALPART).search(localpart):
   141         if re.compile(RE_LOCALPART).search(localpart):
   137             raise VMMException((
   142             raise VMMException((
   138                 'The local part »%s« contains invalid characters.' % localpart,
   143                 _('The local part »%s« contains invalid characters.') %
   139                 ERR.LOCALPART_INVALID))
   144                 localpart, ERR.LOCALPART_INVALID))
   140         return localpart
   145         return localpart
   141 
   146 
   142     def __idn2ascii(self, domainname):
   147     def __idn2ascii(self, domainname):
   143         """Converts an idn domainname in punycode.
   148         """Converts an idn domainname in punycode.
   144         
   149         
   172         domainname -- the domain name that should be validated
   177         domainname -- the domain name that should be validated
   173         """
   178         """
   174         if not re.match(RE_ASCII_CHARS, domainname):
   179         if not re.match(RE_ASCII_CHARS, domainname):
   175             domainname = self.__idn2ascii(domainname)
   180             domainname = self.__idn2ascii(domainname)
   176         if len(domainname) > 255:
   181         if len(domainname) > 255:
   177             raise VMMException(('The domain name is too long.',
   182             raise VMMException((_('The domain name is too long.'),
   178                 ERR.DOMAIN_TOO_LONG))
   183                 ERR.DOMAIN_TOO_LONG))
   179         if not re.match(RE_DOMAIN, domainname):
   184         if not re.match(RE_DOMAIN, domainname):
   180             raise VMMException(('The domain name is invalid.',
   185             raise VMMException((_('The domain name is invalid.'),
   181                 ERR.DOMAIN_INVALID))
   186                 ERR.DOMAIN_INVALID))
   182         return domainname
   187         return domainname
   183 
   188 
   184     def __chkEmailAddress(self, address):
   189     def __chkEmailAddress(self, address):
   185         try:
   190         try:
   186             localpart, domain = address.split('@')
   191             localpart, domain = address.split('@')
   187         except ValueError:
   192         except ValueError:
   188             raise VMMException(("Missing '@' sign in e-mail address »%s«." %
   193             raise VMMException((_("Missing '@' sign in e-mail address »%s«.") %
   189                 address, ERR.INVALID_ADDRESS))
   194                 address, ERR.INVALID_ADDRESS))
   190         except AttributeError:
   195         except AttributeError:
   191             raise VMMException(("»%s« looks not like an e-mail address." %
   196             raise VMMException((_("»%s« looks not like an e-mail address.") %
   192                 address, ERR.INVALID_ADDRESS))
   197                 address, ERR.INVALID_ADDRESS))
   193         domain = self.__chkDomainname(domain)
   198         domain = self.__chkDomainname(domain)
   194         localpart = self.__chkLocalpart(localpart)
   199         localpart = self.__chkLocalpart(localpart)
   195         return '%s@%s' % (localpart, domain)
   200         return '%s@%s' % (localpart, domain)
   196 
   201 
   281 
   286 
   282     def __maildirdelete(self, domdir, uid, gid):
   287     def __maildirdelete(self, domdir, uid, gid):
   283         if uid > 0 and gid > 0:
   288         if uid > 0 and gid > 0:
   284             maildir = '%s' % uid
   289             maildir = '%s' % uid
   285             if maildir.count('..') or domdir.count('..'):
   290             if maildir.count('..') or domdir.count('..'):
   286                 raise VMMException(('FATAL: ".." in maildir path detected.',
   291                 raise VMMException((_('FATAL: ".." in maildir path detected.'),
   287                     ERR.FOUND_DOTS_IN_PATH))
   292                     ERR.FOUND_DOTS_IN_PATH))
   288             if os.path.isdir(domdir):
   293             if os.path.isdir(domdir):
   289                 os.chdir(domdir)
   294                 os.chdir(domdir)
   290                 if os.path.isdir(maildir):
   295                 if os.path.isdir(maildir):
   291                     mdstat = os.stat(maildir)
   296                     mdstat = os.stat(maildir)
   292                     if (mdstat.st_uid, mdstat.st_gid) != (uid, gid):
   297                     if (mdstat.st_uid, mdstat.st_gid) != (uid, gid):
   293                         raise VMMException(
   298                         raise VMMException((
   294                             ('FATAL: owner/group mismatch in maildir detected',
   299                            _('FATAL: owner/group mismatch in maildir detected'),
   295                                 ERR.MAILDIR_PERM_MISMATCH))
   300                            ERR.MAILDIR_PERM_MISMATCH))
   296                     rmtree(maildir, ignore_errors=True)
   301                     rmtree(maildir, ignore_errors=True)
   297 
   302 
   298     def __domdirdelete(self, domdir, gid):
   303     def __domdirdelete(self, domdir, gid):
   299         if gid > 0:
   304         if gid > 0:
   300             basedir = '%s' % self.__Cfg.get('domdir', 'base')
   305             basedir = '%s' % self.__Cfg.get('domdir', 'base')
   301             domdirdirs = domdir.replace(basedir+'/', '').split('/')
   306             domdirdirs = domdir.replace(basedir+'/', '').split('/')
   302             if basedir.count('..') or domdir.count('..'):
   307             if basedir.count('..') or domdir.count('..'):
   303                 raise VMMException(
   308                 raise VMMException(
   304                         ('FATAL: ".." in domain directory path detected.',
   309                         (_('FATAL: ".." in domain directory path detected.'),
   305                             ERR.FOUND_DOTS_IN_PATH))
   310                             ERR.FOUND_DOTS_IN_PATH))
   306             if os.path.isdir('%s/%s' % (basedir, domdirdirs[0])):
   311             if os.path.isdir('%s/%s' % (basedir, domdirdirs[0])):
   307                 os.chdir('%s/%s' % (basedir, domdirdirs[0]))
   312                 os.chdir('%s/%s' % (basedir, domdirdirs[0]))
   308                 if os.lstat(domdirdirs[1]).st_gid != gid:
   313                 if os.lstat(domdirdirs[1]).st_gid != gid:
   309                     raise VMMException(
   314                     raise VMMException(
   310                     ('FATAL: group mismatch in domain directory detected',
   315                     (_('FATAL: group mismatch in domain directory detected'),
   311                         ERR.DOMAINDIR_GROUP_MISMATCH))
   316                         ERR.DOMAINDIR_GROUP_MISMATCH))
   312                 rmtree(domdirdirs[1], ignore_errors=True)
   317                 rmtree(domdirdirs[1], ignore_errors=True)
   313 
   318 
   314     def __getSalt(self):
   319     def __getSalt(self):
   315         from random import choice
   320         from random import choice
   383     def setupIsDone(self):
   388     def setupIsDone(self):
   384         """Checks if vmm is configured, returns bool"""
   389         """Checks if vmm is configured, returns bool"""
   385         try:
   390         try:
   386             return self.__Cfg.getboolean('config', 'done')
   391             return self.__Cfg.getboolean('config', 'done')
   387         except ValueError, e:
   392         except ValueError, e:
   388             raise VMMConfigException('Configurtion error: "'+str(e)
   393             raise VMMConfigException(_("""Configurtion error: "%s"
   389                 +'"\n(in section "Connfig", option "done")'
   394 (in section "connfig", option "done")'
   390                 +'\nsee also: vmm.cfg(5)\n')
   395 see also: vmm.cfg(5)\n""") % str(e))
   391 
   396 
   392     def configure(self, section=None):
   397     def configure(self, section=None):
   393         """Starts interactive configuration.
   398         """Starts interactive configuration.
   394 
   399 
   395         Configures in interactive mode options in the given section.
   400         Configures in interactive mode options in the given section.
   402         """
   407         """
   403         try:
   408         try:
   404             if not section:
   409             if not section:
   405                 self.__Cfg.configure(self.__cfgSections)
   410                 self.__Cfg.configure(self.__cfgSections)
   406             elif section not in self.__cfgSections:
   411             elif section not in self.__cfgSections:
   407                 raise VMMException(("Invalid section: »%s«" % section,
   412                 raise VMMException((_("Invalid section: »%s«") % section,
   408                     ERR.INVALID_SECTION))
   413                     ERR.INVALID_SECTION))
   409             else:
   414             else:
   410                 self.__Cfg.configure([section])
   415                 self.__Cfg.configure([section])
   411         except:
   416         except:
   412             raise
   417             raise
   416         dom.save()
   421         dom.save()
   417         self.__domdirmake(dom.getDir(), dom.getID())
   422         self.__domdirmake(dom.getDir(), dom.getID())
   418 
   423 
   419     def domain_transport(self, domainname, transport, force=None):
   424     def domain_transport(self, domainname, transport, force=None):
   420         if force is not None and force != 'force':
   425         if force is not None and force != 'force':
   421             raise VMMDomainException(('Invalid argument: »%s«' % force,
   426             raise VMMDomainException((_('Invalid argument: »%s«') % force,
   422                 ERR.INVALID_OPTION))
   427                 ERR.INVALID_OPTION))
   423         dom = self.__getDomain(domainname, None)
   428         dom = self.__getDomain(domainname, None)
   424         if force is None:
   429         if force is None:
   425             dom.updateTransport(transport)
   430             dom.updateTransport(transport)
   426         else:
   431         else:
   427             dom.updateTransport(transport, force=True)
   432             dom.updateTransport(transport, force=True)
   428 
   433 
   429     def domain_delete(self, domainname, force=None):
   434     def domain_delete(self, domainname, force=None):
   430         if not force is None and force not in ['deluser','delalias','delall']:
   435         if not force is None and force not in ['deluser','delalias','delall']:
   431             raise VMMDomainException(('Invalid argument: »%s«' % force,
   436             raise VMMDomainException((_('Invalid argument: »%s«') % force,
   432                 ERR.INVALID_OPTION))
   437                 ERR.INVALID_OPTION))
   433         dom = self.__getDomain(domainname)
   438         dom = self.__getDomain(domainname)
   434         gid = dom.getID()
   439         gid = dom.getID()
   435         domdir = dom.getDir()
   440         domdir = dom.getDir()
   436         if self.__Cfg.getboolean('misc', 'forcedel') or force == 'delall':
   441         if self.__Cfg.getboolean('misc', 'forcedel') or force == 'delall':
   455         if detailed is None:
   460         if detailed is None:
   456             return dominfo
   461             return dominfo
   457         elif detailed == 'detailed':
   462         elif detailed == 'detailed':
   458             return dominfo, dom.getAccounts(), dom.getAliases()
   463             return dominfo, dom.getAccounts(), dom.getAliases()
   459         else:
   464         else:
   460             raise VMMDomainException(('Invalid argument: »%s«' % detailed,
   465             raise VMMDomainException(('%s: »%s«' % (_('Invalid argument'),
   461                 ERR.INVALID_OPTION))
   466                 detailed),  ERR.INVALID_OPTION))
   462 
   467 
   463     def user_add(self, emailaddress, password):
   468     def user_add(self, emailaddress, password):
   464         acc = self.__getAccount(emailaddress, password)
   469         acc = self.__getAccount(emailaddress, password)
   465         acc.save(self.__Cfg.get('maildir', 'folder'),
   470         acc.save(self.__Cfg.get('maildir', 'folder'),
   466                 self.__Cfg.getboolean('services', 'smtp'),
   471                 self.__Cfg.getboolean('services', 'smtp'),