VirtualMailManager/VirtualMailManager.py
branchv0.6.x
changeset 174 974bafa59330
parent 168 fd496561acc6
child 175 b241272eb1bd
equal deleted inserted replaced
173:c0e2c7687dd3 174:974bafa59330
    30 RE_LOCALPART = """[^\w!#$%&'\*\+-\.\/=?^_`{\|}~]"""
    30 RE_LOCALPART = """[^\w!#$%&'\*\+-\.\/=?^_`{\|}~]"""
    31 RE_MBOX_NAMES = """^[\x20-\x25\x27-\x7E]*$"""
    31 RE_MBOX_NAMES = """^[\x20-\x25\x27-\x7E]*$"""
    32 
    32 
    33 class VirtualMailManager(object):
    33 class VirtualMailManager(object):
    34     """The main class for vmm"""
    34     """The main class for vmm"""
    35     __slots__ = ('__Cfg', '__cfgFileName', '__cfgSections', '__dbh', '__scheme',
    35     __slots__ = ('__Cfg', '__cfgFileName', '__dbh', '__scheme', '__warnings',
    36             '__warnings', '_postconf')
    36                  '_postconf')
    37     def __init__(self):
    37     def __init__(self):
    38         """Creates a new VirtualMailManager instance.
    38         """Creates a new VirtualMailManager instance.
    39         Throws a VMMNotRootException if your uid is greater 0.
    39         Throws a VMMNotRootException if your uid is greater 0.
    40         """
    40         """
    41         self.__cfgFileName = ''
    41         self.__cfgFileName = ''
    48                 ERR.CONF_NOPERM)
    48                 ERR.CONF_NOPERM)
    49         if self.__chkCfgFile():
    49         if self.__chkCfgFile():
    50             self.__Cfg = Cfg(self.__cfgFileName)
    50             self.__Cfg = Cfg(self.__cfgFileName)
    51             self.__Cfg.load()
    51             self.__Cfg.load()
    52             self.__Cfg.check()
    52             self.__Cfg.check()
    53             self.__cfgSections = self.__Cfg.getsections()
    53             self.__scheme = self.__Cfg.dget('misc.password_scheme')
    54             self.__scheme = self.__Cfg.get('misc', 'password_scheme')
    54             self._postconf = Postconf(self.__Cfg.dget('bin.postconf'))
    55             self._postconf = Postconf(self.__Cfg.get('bin', 'postconf'))
       
    56         if not os.sys.argv[1] in ['cf', 'configure']:
    55         if not os.sys.argv[1] in ['cf', 'configure']:
    57             self.__chkenv()
    56             self.__chkenv()
    58 
    57 
    59     def __findCfgFile(self):
    58     def __findCfgFile(self):
    60         for path in ['/root', '/usr/local/etc', '/etc']:
    59         for path in ['/root', '/usr/local/etc', '/etc']:
    81         else:
    80         else:
    82             return True
    81             return True
    83 
    82 
    84     def __chkenv(self):
    83     def __chkenv(self):
    85         """"""
    84         """"""
    86         basedir = self.__Cfg.get('misc', 'base_directory')
    85         basedir = self.__Cfg.dget('misc.base_directory')
    87         if not os.path.exists(basedir):
    86         if not os.path.exists(basedir):
    88             old_umask = os.umask(0006)
    87             old_umask = os.umask(0006)
    89             os.makedirs(basedir, 0771)
    88             os.makedirs(basedir, 0771)
    90             os.chown(basedir, 0, self.__Cfg.getint('misc', 'gid_mail'))
    89             os.chown(basedir, 0, self.__Cfg.dget('misc.gid_mail'))
    91             os.umask(old_umask)
    90             os.umask(old_umask)
    92         elif not os.path.isdir(basedir):
    91         elif not os.path.isdir(basedir):
    93             raise VMMException(_(u'“%s” is not a directory.\n\
    92             raise VMMException(_(u'“%s” is not a directory.\n\
    94 (vmm.cfg: section "misc", option "base_directory")') %
    93 (vmm.cfg: section "misc", option "base_directory")') %
    95                                  basedir, ERR.NO_SUCH_DIRECTORY)
    94                                  basedir, ERR.NO_SUCH_DIRECTORY)
   106     def __dbConnect(self):
   105     def __dbConnect(self):
   107         """Creates a pyPgSQL.PgSQL.connection instance."""
   106         """Creates a pyPgSQL.PgSQL.connection instance."""
   108         if self.__dbh is None or not self.__dbh._isOpen:
   107         if self.__dbh is None or not self.__dbh._isOpen:
   109             try:
   108             try:
   110                 self.__dbh = PgSQL.connect(
   109                 self.__dbh = PgSQL.connect(
   111                         database=self.__Cfg.get('database', 'name'),
   110                         database=self.__Cfg.dget('database.name'),
   112                         user=self.__Cfg.get('database', 'user'),
   111                         user=self.__Cfg.pget('database.user'),
   113                         host=self.__Cfg.get('database', 'host'),
   112                         host=self.__Cfg.dget('database.host'),
   114                         password=self.__Cfg.get('database', 'pass'),
   113                         password=self.__Cfg.pget('database.pass'),
   115                         client_encoding='utf8', unicode_results=True)
   114                         client_encoding='utf8', unicode_results=True)
   116                 dbc = self.__dbh.cursor()
   115                 dbc = self.__dbh.cursor()
   117                 dbc.execute("SET NAMES 'UTF8'")
   116                 dbc.execute("SET NAMES 'UTF8'")
   118                 dbc.close()
   117                 dbc.close()
   119             except PgSQL.libpq.DatabaseError, e:
   118             except PgSQL.libpq.DatabaseError, e:
   232             destination = EmailAddress(destination)
   231             destination = EmailAddress(destination)
   233         return Relocated(self.__dbh, address, destination)
   232         return Relocated(self.__dbh, address, destination)
   234 
   233 
   235     def __getDomain(self, domainname, transport=None):
   234     def __getDomain(self, domainname, transport=None):
   236         if transport is None:
   235         if transport is None:
   237             transport = self.__Cfg.get('misc', 'transport')
   236             transport = self.__Cfg.dget('misc.transport')
   238         self.__dbConnect()
   237         self.__dbConnect()
   239         return Domain(self.__dbh, domainname,
   238         return Domain(self.__dbh, domainname,
   240                 self.__Cfg.get('misc', 'base_directory'), transport)
   239                 self.__Cfg.dget('misc.base_directory'), transport)
   241 
   240 
   242     def __getDiskUsage(self, directory):
   241     def __getDiskUsage(self, directory):
   243         """Estimate file space usage for the given directory.
   242         """Estimate file space usage for the given directory.
   244 
   243 
   245         Keyword arguments:
   244         Keyword arguments:
   246         directory -- the directory to summarize recursively disk usage for
   245         directory -- the directory to summarize recursively disk usage for
   247         """
   246         """
   248         if self.__isdir(directory):
   247         if self.__isdir(directory):
   249             return Popen([self.__Cfg.get('bin', 'du'), "-hs", directory],
   248             return Popen([self.__Cfg.dget('bin.du'), "-hs", directory],
   250                 stdout=PIPE).communicate()[0].split('\t')[0]
   249                 stdout=PIPE).communicate()[0].split('\t')[0]
   251         else:
   250         else:
   252             return 0
   251             return 0
   253 
   252 
   254     def __isdir(self, directory):
   253     def __isdir(self, directory):
   257             self.__warnings.append(_('No such directory: %s') % directory)
   256             self.__warnings.append(_('No such directory: %s') % directory)
   258         return isdir
   257         return isdir
   259 
   258 
   260     def __makedir(self, directory, mode=None, uid=None, gid=None):
   259     def __makedir(self, directory, mode=None, uid=None, gid=None):
   261         if mode is None:
   260         if mode is None:
   262             mode = self.__Cfg.getint('account', 'directory_mode')
   261             mode = self.__Cfg.dget('account.directory_mode')
   263         if uid is None:
   262         if uid is None:
   264             uid = 0
   263             uid = 0
   265         if gid is None:
   264         if gid is None:
   266             gid = 0
   265             gid = 0
   267         os.makedirs(directory, mode)
   266         os.makedirs(directory, mode)
   268         os.chown(directory, uid, gid)
   267         os.chown(directory, uid, gid)
   269 
   268 
   270     def __domDirMake(self, domdir, gid):
   269     def __domDirMake(self, domdir, gid):
   271         os.umask(0006)
   270         os.umask(0006)
   272         oldpwd = os.getcwd()
   271         oldpwd = os.getcwd()
   273         basedir = self.__Cfg.get('misc', 'base_directory')
   272         basedir = self.__Cfg.dget('misc.base_directory')
   274         domdirdirs = domdir.replace(basedir+'/', '').split('/')
   273         domdirdirs = domdir.replace(basedir+'/', '').split('/')
   275 
   274 
   276         os.chdir(basedir)
   275         os.chdir(basedir)
   277         if not os.path.isdir(domdirdirs[0]):
   276         if not os.path.isdir(domdirdirs[0]):
   278             self.__makedir(domdirdirs[0], 489, 0,
   277             self.__makedir(domdirdirs[0], 489, 0,
   279                            self.__Cfg.getint('misc', 'gid_mail'))
   278                            self.__Cfg.dget('misc.gid_mail'))
   280         os.chdir(domdirdirs[0])
   279         os.chdir(domdirdirs[0])
   281         os.umask(0007)
   280         os.umask(0007)
   282         self.__makedir(domdirdirs[1],
   281         self.__makedir(domdirdirs[1], self.__Cfg.dget('domain.directory_mode'),
   283                        self.__Cfg.getint('domain', 'directory_mode'), 0, gid)
   282                        0, gid)
   284         os.chdir(oldpwd)
   283         os.chdir(oldpwd)
   285 
   284 
   286     def __subscribeFL(self, folderlist, uid, gid):
   285     def __subscribeFL(self, folderlist, uid, gid):
   287         fname = os.path.join(self.__Cfg.get('maildir','name'), 'subscriptions')
   286         fname = os.path.join(self.__Cfg.dget('maildir.name'), 'subscriptions')
   288         sf = file(fname, 'w')
   287         sf = file(fname, 'w')
   289         for f in folderlist:
   288         for f in folderlist:
   290             sf.write(f+'\n')
   289             sf.write(f+'\n')
   291         sf.flush()
   290         sf.flush()
   292         sf.close()
   291         sf.close()
   303         """
   302         """
   304         os.umask(0007)
   303         os.umask(0007)
   305         oldpwd = os.getcwd()
   304         oldpwd = os.getcwd()
   306         os.chdir(domdir)
   305         os.chdir(domdir)
   307 
   306 
   308         maildir = self.__Cfg.get('maildir', 'name')
   307         maildir = self.__Cfg.dget('maildir.name')
   309         folders = [maildir]
   308         folders = [maildir]
   310         for folder in self.__Cfg.get('maildir', 'folders').split(':'):
   309         for folder in self.__Cfg.dget('maildir.folders').split(':'):
   311             folder = folder.strip()
   310             folder = folder.strip()
   312             if len(folder) and not folder.count('..')\
   311             if len(folder) and not folder.count('..')\
   313             and re.match(RE_MBOX_NAMES, folder):
   312             and re.match(RE_MBOX_NAMES, folder):
   314                 folders.append('%s/.%s' % (maildir, folder))
   313                 folders.append('%s/.%s' % (maildir, folder))
   315         subdirs = ['cur', 'new', 'tmp']
   314         subdirs = ['cur', 'new', 'tmp']
   316         mode = self.__Cfg.getint('account', 'directory_mode')
   315         mode = self.__Cfg.dget('account.directory_mode')
   317 
   316 
   318         self.__makedir('%s' % uid, mode, uid, gid)
   317         self.__makedir('%s' % uid, mode, uid, gid)
   319         os.chdir('%s' % uid)
   318         os.chdir('%s' % uid)
   320         for folder in folders:
   319         for folder in folders:
   321             self.__makedir(folder, mode, uid, gid)
   320             self.__makedir(folder, mode, uid, gid)
   346 
   345 
   347     def __domDirDelete(self, domdir, gid):
   346     def __domDirDelete(self, domdir, gid):
   348         if gid > 0:
   347         if gid > 0:
   349             if not self.__isdir(domdir):
   348             if not self.__isdir(domdir):
   350                 return
   349                 return
   351             basedir = self.__Cfg.get('misc', 'base_directory')
   350             basedir = self.__Cfg.dget('misc.base_directory')
   352             domdirdirs = domdir.replace(basedir+'/', '').split('/')
   351             domdirdirs = domdir.replace(basedir+'/', '').split('/')
   353             domdirparent = os.path.join(basedir, domdirdirs[0])
   352             domdirparent = os.path.join(basedir, domdirdirs[0])
   354             if basedir.count('..') or domdir.count('..'):
   353             if basedir.count('..') or domdir.count('..'):
   355                 raise VMMException(_(u'Found ".." in domain directory path.'),
   354                 raise VMMException(_(u'Found ".." in domain directory path.'),
   356                         ERR.FOUND_DOTS_IN_PATH)
   355                         ERR.FOUND_DOTS_IN_PATH)
   413             return '{%s}%s' % (self.__scheme, self.__pwMD5(password, user))
   412             return '{%s}%s' % (self.__scheme, self.__pwMD5(password, user))
   414         elif self.__scheme == 'MD4':
   413         elif self.__scheme == 'MD4':
   415             return '{%s}%s' % (self.__scheme, self.__pwMD4(password))
   414             return '{%s}%s' % (self.__scheme, self.__pwMD4(password))
   416         elif self.__scheme in ['SMD5', 'SSHA', 'CRAM-MD5', 'HMAC-MD5',
   415         elif self.__scheme in ['SMD5', 'SSHA', 'CRAM-MD5', 'HMAC-MD5',
   417                 'LANMAN', 'NTLM', 'RPA']:
   416                 'LANMAN', 'NTLM', 'RPA']:
   418             return Popen([self.__Cfg.get('bin', 'dovecotpw'), '-s',
   417             return Popen([self.__Cfg.dget('bin.dovecotpw'), '-s',
   419                 self.__scheme,'-p',password],stdout=PIPE).communicate()[0][:-1]
   418                 self.__scheme,'-p',password],stdout=PIPE).communicate()[0][:-1]
   420         else:
   419         else:
   421             return '{%s}%s' % (self.__scheme, password)
   420             return '{%s}%s' % (self.__scheme, password)
   422 
   421 
   423     def hasWarnings(self):
   422     def hasWarnings(self):
   426 
   425 
   427     def getWarnings(self):
   426     def getWarnings(self):
   428         """Returns a list with all available warnings."""
   427         """Returns a list with all available warnings."""
   429         return self.__warnings
   428         return self.__warnings
   430 
   429 
   431     def cfgGetBoolean(self, section, option):
   430     def cfgDget(self, option):
   432         return self.__Cfg.getboolean(section, option)
   431         return self.__Cfg.dget(option)
   433 
   432 
   434     def cfgGetInt(self, section, option):
   433     def cfgPget(self, option):
   435         return self.__Cfg.getint(section, option)
   434         return self.__Cfg.pget(option)
   436 
   435 
   437     def cfgGetString(self, section, option):
   436     def cfgSet(self, option, value):
   438         return self.__Cfg.get(section, option)
   437         return self.__Cfg.set(option, value)
   439 
   438 
   440     def setupIsDone(self):
   439     def setupIsDone(self):
   441         """Checks if vmm is configured, returns bool"""
   440         """Checks if vmm is configured, returns bool"""
   442         try:
   441         try:
   443             return self.__Cfg.getboolean('config', 'done')
   442             return self.__Cfg.dget('config.done')
   444         except ValueError, e:
   443         except ValueError, e:
   445             raise VMMConfigException(_(u"""Configuration error: "%s"
   444             raise VMMConfigException(_(u"""Configuration error: "%s"
   446 (in section "config", option "done") see also: vmm.cfg(5)\n""") % str(e),
   445 (in section "config", option "done") see also: vmm.cfg(5)\n""") % str(e),
   447                   ERR.CONF_ERROR)
   446                   ERR.CONF_ERROR)
   448 
   447 
   456         Keyword arguments:
   455         Keyword arguments:
   457         section -- the section to configure (default None):
   456         section -- the section to configure (default None):
   458             'database', 'maildir', 'bin' or 'misc'
   457             'database', 'maildir', 'bin' or 'misc'
   459         """
   458         """
   460         if section is None:
   459         if section is None:
   461             self.__Cfg.configure(self.__cfgSections)
   460             self.__Cfg.configure(self.__Cfg.getsections())
   462         elif section in self.__cfgSections:
   461         elif section in self.__Cfg.getsections():
   463             self.__Cfg.configure([section])
   462             self.__Cfg.configure([section])
   464         else:
   463         else:
   465             raise VMMException(_(u"Invalid section: “%s”") % section,
   464             raise VMMException(_(u"Invalid section: “%s”") % section,
   466                 ERR.INVALID_SECTION)
   465                 ERR.INVALID_SECTION)
   467 
   466 
   485             raise VMMDomainException(_(u"Invalid argument: “%s”") % force,
   484             raise VMMDomainException(_(u"Invalid argument: “%s”") % force,
   486                 ERR.INVALID_OPTION)
   485                 ERR.INVALID_OPTION)
   487         dom = self.__getDomain(domainname)
   486         dom = self.__getDomain(domainname)
   488         gid = dom.getID()
   487         gid = dom.getID()
   489         domdir = dom.getDir()
   488         domdir = dom.getDir()
   490         if self.__Cfg.getboolean('domain', 'force_deletion')\
   489         if self.__Cfg.dget('domain.force_deletion') or force == 'delall':
   491         or force == 'delall':
       
   492             dom.delete(True, True)
   490             dom.delete(True, True)
   493         elif force == 'deluser':
   491         elif force == 'deluser':
   494             dom.delete(delUser=True)
   492             dom.delete(delUser=True)
   495         elif force == 'delalias':
   493         elif force == 'delalias':
   496             dom.delete(delAlias=True)
   494             dom.delete(delAlias=True)
   497         else:
   495         else:
   498             dom.delete()
   496             dom.delete()
   499         if self.__Cfg.getboolean('domain', 'delete_directory'):
   497         if self.__Cfg.dget('domain.delete_directory'):
   500             self.__domDirDelete(domdir, gid)
   498             self.__domDirDelete(domdir, gid)
   501 
   499 
   502     def domainInfo(self, domainname, details=None):
   500     def domainInfo(self, domainname, details=None):
   503         if details not in [None, 'accounts', 'aliasdomains', 'aliases', 'full',
   501         if details not in [None, 'accounts', 'aliasdomains', 'aliases', 'full',
   504                 'relocated', 'detailed']:
   502                 'relocated', 'detailed']:
   587     def userAdd(self, emailaddress, password):
   585     def userAdd(self, emailaddress, password):
   588         acc = self.__getAccount(emailaddress, password)
   586         acc = self.__getAccount(emailaddress, password)
   589         if password is None:
   587         if password is None:
   590             password = self._readpass()
   588             password = self._readpass()
   591             acc.setPassword(self.__pwhash(password))
   589             acc.setPassword(self.__pwhash(password))
   592         acc.save(self.__Cfg.get('maildir', 'name'),
   590         acc.save(self.__Cfg.dget('maildir.name'),
   593                 self.__Cfg.getint('misc', 'dovecot_version'),
   591                  self.__Cfg.dget('misc.dovecot_version'),
   594                 self.__Cfg.getboolean('account', 'smtp'),
   592                  self.__Cfg.dget('account.smtp'),
   595                 self.__Cfg.getboolean('account', 'pop3'),
   593                  self.__Cfg.dget('account.pop3'),
   596                 self.__Cfg.getboolean('account', 'imap'),
   594                  self.__Cfg.dget('account.imap'),
   597                 self.__Cfg.getboolean('account', 'sieve'))
   595                  self.__Cfg.dget('account.sieve'))
   598         self.__mailDirMake(acc.getDir('domain'), acc.getUID(), acc.getGID())
   596         self.__mailDirMake(acc.getDir('domain'), acc.getUID(), acc.getGID())
   599 
   597 
   600     def aliasAdd(self, aliasaddress, targetaddress):
   598     def aliasAdd(self, aliasaddress, targetaddress):
   601         alias = self.__getAlias(aliasaddress, targetaddress)
   599         alias = self.__getAlias(aliasaddress, targetaddress)
   602         alias.save(long(self._postconf.read('virtual_alias_expansion_limit')))
   600         alias.save(long(self._postconf.read('virtual_alias_expansion_limit')))
   614                     ERR.INVALID_AGUMENT)
   612                     ERR.INVALID_AGUMENT)
   615         acc = self.__getAccount(emailaddress)
   613         acc = self.__getAccount(emailaddress)
   616         uid = acc.getUID()
   614         uid = acc.getUID()
   617         gid = acc.getGID()
   615         gid = acc.getGID()
   618         acc.delete(force)
   616         acc.delete(force)
   619         if self.__Cfg.getboolean('account', 'delete_directory'):
   617         if self.__Cfg.dget('account.delete_directory'):
   620             try:
   618             try:
   621                 self.__userDirDelete(acc.getDir('domain'), uid, gid)
   619                 self.__userDirDelete(acc.getDir('domain'), uid, gid)
   622             except VMMException, e:
   620             except VMMException, e:
   623                 if e.code() in [ERR.FOUND_DOTS_IN_PATH,
   621                 if e.code() in [ERR.FOUND_DOTS_IN_PATH,
   624                         ERR.MAILDIR_PERM_MISMATCH, ERR.NO_SUCH_DIRECTORY]:
   622                         ERR.MAILDIR_PERM_MISMATCH, ERR.NO_SUCH_DIRECTORY]:
   638     def aliasDelete(self, aliasaddress, targetaddress=None):
   636     def aliasDelete(self, aliasaddress, targetaddress=None):
   639         alias = self.__getAlias(aliasaddress, targetaddress)
   637         alias = self.__getAlias(aliasaddress, targetaddress)
   640         alias.delete()
   638         alias.delete()
   641 
   639 
   642     def userInfo(self, emailaddress, details=None):
   640     def userInfo(self, emailaddress, details=None):
   643         if details not in [None, 'du', 'aliases', 'full']:
   641         if details not in (None, 'du', 'aliases', 'full'):
   644             raise VMMException(_(u'Invalid argument: “%s”') % details,
   642             raise VMMException(_(u'Invalid argument: “%s”') % details,
   645                     ERR.INVALID_AGUMENT)
   643                                ERR.INVALID_AGUMENT)
   646         acc = self.__getAccount(emailaddress)
   644         acc = self.__getAccount(emailaddress)
   647         info = acc.getInfo(self.__Cfg.getint('misc', 'dovecot_version'))
   645         info = acc.getInfo(self.__Cfg.dget('misc.dovecot_version'))
   648         if self.__Cfg.getboolean('account', 'disk_usage')\
   646         if self.__Cfg.dget('account.disk_usage') or details in ('du', 'full'):
   649         or details in ['du', 'full']:
       
   650             info['disk usage'] = self.__getDiskUsage('%(maildir)s' % info)
   647             info['disk usage'] = self.__getDiskUsage('%(maildir)s' % info)
   651             if details in [None, 'du']:
   648             if details in (None, 'du'):
   652                 return info
   649                 return info
   653         if details in ['aliases', 'full']:
   650         if details in ('aliases', 'full'):
   654             return (info, acc.getAliases())
   651             return (info, acc.getAliases())
   655         return info
   652         return info
   656 
   653 
   657     def userByID(self, uid):
   654     def userByID(self, uid):
   658         from Account import getAccountByID
   655         from Account import getAccountByID
   681             self.__warnings.append(_(u'\
   678             self.__warnings.append(_(u'\
   682 The service name “managesieve” is deprecated and will be removed\n\
   679 The service name “managesieve” is deprecated and will be removed\n\
   683    in a future release.\n\
   680    in a future release.\n\
   684    Please use the service name “sieve” instead.'))
   681    Please use the service name “sieve” instead.'))
   685         acc = self.__getAccount(emailaddress)
   682         acc = self.__getAccount(emailaddress)
   686         acc.disable(self.__Cfg.getint('misc', 'dovecot_version'), service)
   683         acc.disable(self.__Cfg.dget('misc.dovecot_version'), service)
   687 
   684 
   688     def userEnable(self, emailaddress, service=None):
   685     def userEnable(self, emailaddress, service=None):
   689         if service == 'managesieve':
   686         if service == 'managesieve':
   690             service = 'sieve'
   687             service = 'sieve'
   691             self.__warnings.append(_(u'\
   688             self.__warnings.append(_(u'\
   692 The service name “managesieve” is deprecated and will be removed\n\
   689 The service name “managesieve” is deprecated and will be removed\n\
   693    in a future release.\n\
   690    in a future release.\n\
   694    Please use the service name “sieve” instead.'))
   691    Please use the service name “sieve” instead.'))
   695         acc = self.__getAccount(emailaddress)
   692         acc = self.__getAccount(emailaddress)
   696         acc.enable(self.__Cfg.getint('misc', 'dovecot_version'), service)
   693         acc.enable(self.__Cfg.dget('misc.dovecot_version'), service)
   697 
   694 
   698     def relocatedAdd(self, emailaddress, targetaddress):
   695     def relocatedAdd(self, emailaddress, targetaddress):
   699         relocated = self.__getRelocated(emailaddress, targetaddress)
   696         relocated = self.__getRelocated(emailaddress, targetaddress)
   700         relocated.save()
   697         relocated.save()
   701 
   698