VirtualMailManager/VirtualMailManager.py
changeset 48 0d5f58f8b8f5
parent 47 191d5a5adc4a
child 49 9bd033177377
equal deleted inserted replaced
47:191d5a5adc4a 48:0d5f58f8b8f5
    49         self.__warnings = []
    49         self.__warnings = []
    50         self.__Cfg = None
    50         self.__Cfg = None
    51         self.__dbh = None
    51         self.__dbh = None
    52 
    52 
    53         if os.geteuid():
    53         if os.geteuid():
    54             raise VMMNotRootException((_(u"You are not root.\n\tGood bye!\n"),
    54             raise VMMNotRootException(_(u"You are not root.\n\tGood bye!\n"),
    55                 ERR.CONF_NOPERM))
    55                 ERR.CONF_NOPERM)
    56         if self.__chkCfgFile():
    56         if self.__chkCfgFile():
    57             self.__Cfg = Cfg(self.__cfgFileName)
    57             self.__Cfg = Cfg(self.__cfgFileName)
    58             self.__Cfg.load()
    58             self.__Cfg.load()
    59             self.__Cfg.check()
    59             self.__Cfg.check()
    60             self.__cfgSections = self.__Cfg.getsections()
    60             self.__cfgSections = self.__Cfg.getsections()
    63             self.__chkenv()
    63             self.__chkenv()
    64 
    64 
    65     def __chkCfgFile(self):
    65     def __chkCfgFile(self):
    66         """Checks the configuration file, returns bool"""
    66         """Checks the configuration file, returns bool"""
    67         if not os.path.isfile(self.__cfgFileName):
    67         if not os.path.isfile(self.__cfgFileName):
    68             raise VMMException((_(u"The file »%s« does not exists.") %
    68             raise VMMException(_(u"The file »%s« does not exists.") %
    69                 self.__cfgFileName, ERR.CONF_NOFILE))
    69                 self.__cfgFileName, ERR.CONF_NOFILE)
    70         fstat = os.stat(self.__cfgFileName)
    70         fstat = os.stat(self.__cfgFileName)
    71         try:
    71         fmode = int(oct(fstat.st_mode & 0777))
    72             fmode = self.__getFileMode()
       
    73         except:
       
    74             raise
       
    75         if fmode % 100 and fstat.st_uid != fstat.st_gid \
    72         if fmode % 100 and fstat.st_uid != fstat.st_gid \
    76         or fmode % 10 and fstat.st_uid == fstat.st_gid:
    73         or fmode % 10 and fstat.st_uid == fstat.st_gid:
    77             raise VMMPermException((self.__permWarnMsg, ERR.CONF_ERROR))
    74             raise VMMPermException(self.__permWarnMsg, ERR.CONF_ERROR)
    78         else:
    75         else:
    79             return True
    76             return True
    80 
    77 
    81     def __chkenv(self):
    78     def __chkenv(self):
    82         """"""
    79         """"""
    85             os.makedirs(self.__Cfg.get('domdir', 'base'), 0771)
    82             os.makedirs(self.__Cfg.get('domdir', 'base'), 0771)
    86             os.chown(self.__Cfg.get('domdir', 'base'), 0,
    83             os.chown(self.__Cfg.get('domdir', 'base'), 0,
    87                     self.__Cfg.getint('misc', 'gid_mail'))
    84                     self.__Cfg.getint('misc', 'gid_mail'))
    88             os.umask(old_umask)
    85             os.umask(old_umask)
    89         elif not os.path.isdir(self.__Cfg.get('domdir', 'base')):
    86         elif not os.path.isdir(self.__Cfg.get('domdir', 'base')):
    90             raise VMMException((_(u'»%s« is not a directory.\n\
    87             raise VMMException(_(u'»%s« is not a directory.\n\
    91 (vmm.cfg: section "domdir", option "base")') %
    88 (vmm.cfg: section "domdir", option "base")') %
    92                 self.__Cfg.get('domdir', 'base'), ERR.NO_SUCH_DIRECTORY))
    89                 self.__Cfg.get('domdir', 'base'), ERR.NO_SUCH_DIRECTORY)
    93         for opt, val in self.__Cfg.items('bin'):
    90         for opt, val in self.__Cfg.items('bin'):
    94             if not os.path.exists(val):
    91             if not os.path.exists(val):
    95                 raise VMMException((_(u'»%s« doesn\'t exists.\n\
    92                 raise VMMException(_(u'»%s« doesn\'t exists.\n\
    96 (vmm.cfg: section "bin", option "%s")') % (val, opt), ERR.NO_SUCH_BINARY))
    93 (vmm.cfg: section "bin", option "%s")') %
       
    94                     (val, opt), ERR.NO_SUCH_BINARY)
    97             elif not os.access(val, os.X_OK):
    95             elif not os.access(val, os.X_OK):
    98                 raise VMMException((_(u'»%s« is not executable.\n\
    96                 raise VMMException(_(u'»%s« is not executable.\n\
    99 (vmm.cfg: section "bin", option "%s")') % (val, opt), ERR.NOT_EXECUTABLE))
    97 (vmm.cfg: section "bin", option "%s")') %
   100 
    98                     (val, opt), ERR.NOT_EXECUTABLE)
   101     def __getFileMode(self):
       
   102         """Determines the file access mode from file __cfgFileName,
       
   103         returns int.
       
   104         """
       
   105         try:
       
   106             return int(oct(os.stat(self.__cfgFileName).st_mode & 0777))
       
   107         except:
       
   108             raise
       
   109 
    99 
   110     def __dbConnect(self):
   100     def __dbConnect(self):
   111         """Creates a pyPgSQL.PgSQL.connection instance."""
   101         """Creates a pyPgSQL.PgSQL.connection instance."""
   112         try:
   102         try:
   113             self.__dbh = PgSQL.connect(
   103             self.__dbh = PgSQL.connect(
   118                     client_encoding='utf8', unicode_results=True)
   108                     client_encoding='utf8', unicode_results=True)
   119             dbc = self.__dbh.cursor()
   109             dbc = self.__dbh.cursor()
   120             dbc.execute("SET NAMES 'UTF8'")
   110             dbc.execute("SET NAMES 'UTF8'")
   121             dbc.close()
   111             dbc.close()
   122         except PgSQL.libpq.DatabaseError, e:
   112         except PgSQL.libpq.DatabaseError, e:
   123             raise VMMException((str(e), ERR.DATABASE_ERROR))
   113             raise VMMException(str(e), ERR.DATABASE_ERROR)
   124 
   114 
   125     def chkLocalpart(localpart):
   115     def chkLocalpart(localpart):
   126         """Validates the local part of an e-mail address.
   116         """Validates the local part of an e-mail address.
   127         
   117         
   128         Keyword arguments:
   118         Keyword arguments:
   129         localpart -- the e-mail address that should be validated (str)
   119         localpart -- the e-mail address that should be validated (str)
   130         """
   120         """
   131         if len(localpart) < 1:
   121         if len(localpart) < 1:
   132             raise VMMException((_(u'No localpart specified.'),
   122             raise VMMException(_(u'No localpart specified.'),
   133                 ERR.LOCALPART_INVALID))
   123                 ERR.LOCALPART_INVALID)
   134         if len(localpart) > 64:
   124         if len(localpart) > 64:
   135             raise VMMException((_(u'The local part »%s« is too long') %
   125             raise VMMException(_(u'The local part »%s« is too long') %
   136                 localpart, ERR.LOCALPART_TOO_LONG))
   126                 localpart, ERR.LOCALPART_TOO_LONG)
   137         ic = re.compile(RE_LOCALPART).findall(localpart)
   127         ic = re.compile(RE_LOCALPART).findall(localpart)
   138         if len(ic):
   128         if len(ic):
   139             ichrs = ''
   129             ichrs = ''
   140             for c in set(ic):
   130             for c in set(ic):
   141                 ichrs += u"»%s« " % c
   131                 ichrs += u"»%s« " % c
   142             raise VMMException((
   132             raise VMMException(
   143                 _(u"The local part »%s« contains invalid characters: %s") %
   133                 _(u"The local part »%s« contains invalid characters: %s") %
   144                 (localpart, ichrs), ERR.LOCALPART_INVALID))
   134                 (localpart, ichrs), ERR.LOCALPART_INVALID)
   145         return localpart
   135         return localpart
   146     chkLocalpart = staticmethod(chkLocalpart)
   136     chkLocalpart = staticmethod(chkLocalpart)
   147 
   137 
   148     def idn2ascii(domainname):
   138     def idn2ascii(domainname):
   149         """Converts an idn domainname in punycode.
   139         """Converts an idn domainname in punycode.
   182         """
   172         """
   183         re.compile(RE_ASCII_CHARS)
   173         re.compile(RE_ASCII_CHARS)
   184         if not re.match(RE_ASCII_CHARS, domainname):
   174         if not re.match(RE_ASCII_CHARS, domainname):
   185             domainname = VirtualMailManager.idn2ascii(domainname)
   175             domainname = VirtualMailManager.idn2ascii(domainname)
   186         if len(domainname) > 255:
   176         if len(domainname) > 255:
   187             raise VMMException((_(u'The domain name is too long.'),
   177             raise VMMException(_(u'The domain name is too long.'),
   188                 ERR.DOMAIN_TOO_LONG))
   178                 ERR.DOMAIN_TOO_LONG)
   189         re.compile(RE_DOMAIN)
   179         re.compile(RE_DOMAIN)
   190         if not re.match(RE_DOMAIN, domainname):
   180         if not re.match(RE_DOMAIN, domainname):
   191             raise VMMException((_(u'The domain name is invalid.'),
   181             raise VMMException(_(u'The domain name is invalid.'),
   192                 ERR.DOMAIN_INVALID))
   182                 ERR.DOMAIN_INVALID)
   193         return domainname
   183         return domainname
   194     chkDomainname = staticmethod(chkDomainname)
   184     chkDomainname = staticmethod(chkDomainname)
   195 
   185 
   196     def chkEmailAddress(address):
   186     def chkEmailAddress(address):
   197         try:
   187         try:
   198             localpart, domain = address.split('@')
   188             localpart, domain = address.split('@')
   199         except ValueError:
   189         except ValueError:
   200             raise VMMException((_(u"Missing '@' sign in e-mail address »%s«.") %
   190             raise VMMException(_(u"Missing '@' sign in e-mail address »%s«.") %
   201                 address, ERR.INVALID_ADDRESS))
   191                 address, ERR.INVALID_ADDRESS)
   202         except AttributeError:
   192         except AttributeError:
   203             raise VMMException((_(u"»%s« looks not like an e-mail address.") %
   193             raise VMMException(_(u"»%s« looks not like an e-mail address.") %
   204                 address, ERR.INVALID_ADDRESS))
   194                 address, ERR.INVALID_ADDRESS)
   205         domain = VirtualMailManager.chkDomainname(domain)
   195         domain = VirtualMailManager.chkDomainname(domain)
   206         localpart = VirtualMailManager.chkLocalpart(localpart)
   196         localpart = VirtualMailManager.chkLocalpart(localpart)
   207         return '%s@%s' % (localpart, domain)
   197         return '%s@%s' % (localpart, domain)
   208     chkEmailAddress = staticmethod(chkEmailAddress)
   198     chkEmailAddress = staticmethod(chkEmailAddress)
   209 
   199 
   317 
   307 
   318     def __maildirdelete(self, domdir, uid, gid):
   308     def __maildirdelete(self, domdir, uid, gid):
   319         if uid > 0 and gid > 0:
   309         if uid > 0 and gid > 0:
   320             maildir = '%s' % uid
   310             maildir = '%s' % uid
   321             if maildir.count('..') or domdir.count('..'):
   311             if maildir.count('..') or domdir.count('..'):
   322                 raise VMMException((_(u'Found ".." in maildir path.'),
   312                 raise VMMException(_(u'Found ".." in maildir path.'),
   323                     ERR.FOUND_DOTS_IN_PATH))
   313                     ERR.FOUND_DOTS_IN_PATH)
   324             if os.path.isdir(domdir):
   314             if os.path.isdir(domdir):
   325                 os.chdir(domdir)
   315                 os.chdir(domdir)
   326                 if os.path.isdir(maildir):
   316                 if os.path.isdir(maildir):
   327                     mdstat = os.stat(maildir)
   317                     mdstat = os.stat(maildir)
   328                     if (mdstat.st_uid, mdstat.st_gid) != (uid, gid):
   318                     if (mdstat.st_uid, mdstat.st_gid) != (uid, gid):
   329                         raise VMMException((
   319                         raise VMMException(
   330                           _(u'Owner/group mismatch in maildir detected.'),
   320                           _(u'Owner/group mismatch in maildir detected.'),
   331                           ERR.MAILDIR_PERM_MISMATCH))
   321                           ERR.MAILDIR_PERM_MISMATCH)
   332                     rmtree(maildir, ignore_errors=True)
   322                     rmtree(maildir, ignore_errors=True)
   333                 else:
   323                 else:
   334                     raise VMMException((_(u"No such directory: %s/%s") %
   324                     raise VMMException(_(u"No such directory: %s/%s") %
   335                         (domdir, uid), ERR.NO_SUCH_DIRECTORY))
   325                         (domdir, uid), ERR.NO_SUCH_DIRECTORY)
   336 
   326 
   337     def __domdirdelete(self, domdir, gid):
   327     def __domdirdelete(self, domdir, gid):
   338         if gid > 0:
   328         if gid > 0:
   339             if not self.__isdir(domdir):
   329             if not self.__isdir(domdir):
   340                 return
   330                 return
   341             basedir = '%s' % self.__Cfg.get('domdir', 'base')
   331             basedir = '%s' % self.__Cfg.get('domdir', 'base')
   342             domdirdirs = domdir.replace(basedir+'/', '').split('/')
   332             domdirdirs = domdir.replace(basedir+'/', '').split('/')
   343             if basedir.count('..') or domdir.count('..'):
   333             if basedir.count('..') or domdir.count('..'):
   344                 raise VMMException(
   334                 raise VMMException(
   345                         (_(u'FATAL: ".." in domain directory path detected.'),
   335                         _(u'FATAL: ".." in domain directory path detected.'),
   346                             ERR.FOUND_DOTS_IN_PATH))
   336                             ERR.FOUND_DOTS_IN_PATH)
   347             if os.path.isdir('%s/%s' % (basedir, domdirdirs[0])):
   337             if os.path.isdir('%s/%s' % (basedir, domdirdirs[0])):
   348                 os.chdir('%s/%s' % (basedir, domdirdirs[0]))
   338                 os.chdir('%s/%s' % (basedir, domdirdirs[0]))
   349                 if os.lstat(domdirdirs[1]).st_gid != gid:
   339                 if os.lstat(domdirdirs[1]).st_gid != gid:
   350                     raise VMMException(
   340                     raise VMMException(
   351                     (_(u'FATAL: group mismatch in domain directory detected'),
   341                     _(u'FATAL: group mismatch in domain directory detected'),
   352                         ERR.DOMAINDIR_GROUP_MISMATCH))
   342                         ERR.DOMAINDIR_GROUP_MISMATCH)
   353                 rmtree(domdirdirs[1], ignore_errors=True)
   343                 rmtree(domdirdirs[1], ignore_errors=True)
   354 
   344 
   355     def __getSalt(self):
   345     def __getSalt(self):
   356         from random import choice
   346         from random import choice
   357         salt = None
   347         salt = None
   434         """Checks if vmm is configured, returns bool"""
   424         """Checks if vmm is configured, returns bool"""
   435         try:
   425         try:
   436             return self.__Cfg.getboolean('config', 'done')
   426             return self.__Cfg.getboolean('config', 'done')
   437         except ValueError, e:
   427         except ValueError, e:
   438             raise VMMConfigException(_(u"""Configurtion error: "%s"
   428             raise VMMConfigException(_(u"""Configurtion error: "%s"
   439 (in section "connfig", option "done") see also: vmm.cfg(5)\n""") % 
   429 (in section "connfig", option "done") see also: vmm.cfg(5)\n""") % str(e),
   440                 str(e))
   430     ERR.CONF_ERROR)
   441 
   431 
   442     def configure(self, section=None):
   432     def configure(self, section=None):
   443         """Starts interactive configuration.
   433         """Starts interactive configuration.
   444 
   434 
   445         Configures in interactive mode options in the given section.
   435         Configures in interactive mode options in the given section.
   448 
   438 
   449         Keyword arguments:
   439         Keyword arguments:
   450         section -- the section to configure (default None):
   440         section -- the section to configure (default None):
   451             'database', 'maildir', 'bin' or 'misc'
   441             'database', 'maildir', 'bin' or 'misc'
   452         """
   442         """
   453         try:
   443         if section is None:
   454             if not section:
   444             self.__Cfg.configure(self.__cfgSections)
   455                 self.__Cfg.configure(self.__cfgSections)
   445         elif section in self.__cfgSections:
   456             elif section not in self.__cfgSections:
   446             self.__Cfg.configure([section])
   457                 raise VMMException((_(u"Invalid section: '%s'") % section,
   447         else:
   458                     ERR.INVALID_SECTION))
   448             raise VMMException(_(u"Invalid section: '%s'") % section,
   459             else:
   449                 ERR.INVALID_SECTION)
   460                 self.__Cfg.configure([section])
       
   461         except:
       
   462             raise
       
   463 
   450 
   464     def domain_add(self, domainname, transport=None):
   451     def domain_add(self, domainname, transport=None):
   465         dom = self.__getDomain(domainname, transport)
   452         dom = self.__getDomain(domainname, transport)
   466         dom.save()
   453         dom.save()
   467         self.__domdirmake(dom.getDir(), dom.getID())
   454         self.__domdirmake(dom.getDir(), dom.getID())
   468 
   455 
   469     def domain_transport(self, domainname, transport, force=None):
   456     def domain_transport(self, domainname, transport, force=None):
   470         if force is not None and force != 'force':
   457         if force is not None and force != 'force':
   471             raise VMMDomainException((_(u"Invalid argument: '%s'") % force,
   458             raise VMMDomainException(_(u"Invalid argument: '%s'") % force,
   472                 ERR.INVALID_OPTION))
   459                 ERR.INVALID_OPTION)
   473         dom = self.__getDomain(domainname, None)
   460         dom = self.__getDomain(domainname, None)
   474         if force is None:
   461         if force is None:
   475             dom.updateTransport(transport)
   462             dom.updateTransport(transport)
   476         else:
   463         else:
   477             dom.updateTransport(transport, force=True)
   464             dom.updateTransport(transport, force=True)
   478 
   465 
   479     def domain_delete(self, domainname, force=None):
   466     def domain_delete(self, domainname, force=None):
   480         if not force is None and force not in ['deluser','delalias','delall']:
   467         if not force is None and force not in ['deluser','delalias','delall']:
   481             raise VMMDomainException((_(u"Invalid argument: »%s«") % force,
   468             raise VMMDomainException(_(u"Invalid argument: »%s«") % force,
   482                 ERR.INVALID_OPTION))
   469                 ERR.INVALID_OPTION)
   483         dom = self.__getDomain(domainname)
   470         dom = self.__getDomain(domainname)
   484         gid = dom.getID()
   471         gid = dom.getID()
   485         domdir = dom.getDir()
   472         domdir = dom.getDir()
   486         if self.__Cfg.getboolean('misc', 'forcedel') or force == 'delall':
   473         if self.__Cfg.getboolean('misc', 'forcedel') or force == 'delall':
   487             dom.delete(True, True)
   474             dom.delete(True, True)
   506             return dominfo
   493             return dominfo
   507         elif detailed == 'detailed':
   494         elif detailed == 'detailed':
   508             return (dominfo, dom.getAliaseNames(), dom.getAccounts(),
   495             return (dominfo, dom.getAliaseNames(), dom.getAccounts(),
   509                     dom.getAliases())
   496                     dom.getAliases())
   510         else:
   497         else:
   511             raise VMMDomainException((_(u'Invalid argument: »%s«') % detailed,
   498             raise VMMDomainException(_(u'Invalid argument: »%s«') % detailed,
   512                 ERR.INVALID_OPTION))
   499                 ERR.INVALID_OPTION)
   513 
   500 
   514     def domain_alias_add(self, aliasname, domainname):
   501     def domain_alias_add(self, aliasname, domainname):
   515         """Adds an alias name to the domain.
   502         """Adds an alias name to the domain.
   516         
   503         
   517         Keyword arguments:
   504         Keyword arguments:
   547                     domain = pattern[1:]
   534                     domain = pattern[1:]
   548                 elif pattern.endswith('%'):
   535                 elif pattern.endswith('%'):
   549                     domain = pattern[:-1]
   536                     domain = pattern[:-1]
   550                 re.compile(RE_DOMAIN_SRCH)
   537                 re.compile(RE_DOMAIN_SRCH)
   551                 if not re.match(RE_DOMAIN_SRCH, domain):
   538                 if not re.match(RE_DOMAIN_SRCH, domain):
   552                     raise VMMException((
   539                     raise VMMException(
   553                     _(u"The pattern '%s' contains invalid characters.") %
   540                     _(u"The pattern '%s' contains invalid characters.") %
   554                     pattern, ERR.DOMAIN_INVALID))
   541                     pattern, ERR.DOMAIN_INVALID)
   555             else:
   542             else:
   556                 pattern = VirtualMailManager.chkDomainname(pattern)
   543                 pattern = VirtualMailManager.chkDomainname(pattern)
   557                 # XXX chk by domain if not like
   544                 # XXX chk by domain if not like
   558         self.__dbConnect()
   545         self.__dbConnect()
   559         return search(self.__dbh, pattern=pattern, like=like)
   546         return search(self.__dbh, pattern=pattern, like=like)
   580         gid = acc.getGID()
   567         gid = acc.getGID()
   581         acc.delete()
   568         acc.delete()
   582         if self.__Cfg.getboolean('maildir', 'delete'):
   569         if self.__Cfg.getboolean('maildir', 'delete'):
   583             try:
   570             try:
   584                 self.__maildirdelete(acc.getDir('domain'), uid, gid)
   571                 self.__maildirdelete(acc.getDir('domain'), uid, gid)
   585             except (VMMException), e:
   572             except VMMException, e:
   586                 if e[0][1] in [ERR.FOUND_DOTS_IN_PATH,
   573                 if e.code() in [ERR.FOUND_DOTS_IN_PATH,
   587                         ERR.MAILDIR_PERM_MISMATCH, ERR.NO_SUCH_DIRECTORY]:
   574                         ERR.MAILDIR_PERM_MISMATCH, ERR.NO_SUCH_DIRECTORY]:
   588                     warning = _(u"""\
   575                     warning = _(u"""\
   589 The account has been successfully deleted from the database.
   576 The account has been successfully deleted from the database.
   590     But an error occurred while deleting the following directory:
   577     But an error occurred while deleting the following directory:
   591     »%s«
   578     »%s«
   592     Reason: %s""") % (acc.getDir('home'), e[0][0])
   579     Reason: %s""") % (acc.getDir('home'), e.msg())
   593                     self.__warnings.append(warning)
   580                     self.__warnings.append(warning)
   594                 else:
   581                 else:
   595                     raise e
   582                     raise e
   596 
   583 
   597     def alias_info(self, aliasaddress):
   584     def alias_info(self, aliasaddress):
   615         return getAccountByID(uid, self.__dbh)
   602         return getAccountByID(uid, self.__dbh)
   616 
   603 
   617     def user_password(self, emailaddress, password):
   604     def user_password(self, emailaddress, password):
   618         acc = self.__getAccount(emailaddress)
   605         acc = self.__getAccount(emailaddress)
   619         if acc.getUID() == 0:
   606         if acc.getUID() == 0:
   620            raise VMMException((_(u"Account doesn't exists"),
   607            raise VMMException(_(u"Account doesn't exists"),
   621                ERR.NO_SUCH_ACCOUNT))
   608                ERR.NO_SUCH_ACCOUNT)
   622         if password is None:
   609         if password is None:
   623             password = self._readpass()
   610             password = self._readpass()
   624         acc.modify('password', self.__pwhash(password))
   611         acc.modify('password', self.__pwhash(password))
   625 
   612 
   626     def user_name(self, emailaddress, name):
   613     def user_name(self, emailaddress, name):