VMM/*: Made all modules names lowercase, adjusted imports.
--- a/VirtualMailManager/Account.py Wed Jul 28 01:03:56 2010 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,421 +0,0 @@
-# -*- coding: UTF-8 -*-
-# Copyright (c) 2007 - 2010, Pascal Volk
-# See COPYING for distribution information.
-
-"""
- VirtualMailManager.Account
- ~~~~~~~~~~~~~~~~~~~~~~~~~~
-
- Virtual Mail Manager's Account class to manage e-mail accounts.
-"""
-
-from VirtualMailManager.Domain import Domain
-from VirtualMailManager.EmailAddress import EmailAddress
-from VirtualMailManager.Transport import Transport
-from VirtualMailManager.common import version_str
-from VirtualMailManager.constants import \
- ACCOUNT_EXISTS, ACCOUNT_MISSING_PASSWORD, ALIAS_PRESENT, \
- INVALID_ARGUMENT, INVALID_MAIL_LOCATION, NO_SUCH_ACCOUNT, \
- NO_SUCH_DOMAIN, UNKNOWN_SERVICE
-from VirtualMailManager.errors import AccountError as AErr
-from VirtualMailManager.maillocation import MailLocation
-from VirtualMailManager.password import pwhash
-
-
-_ = lambda msg: msg
-cfg_dget = lambda option: None
-
-
-class Account(object):
- """Class to manage e-mail accounts."""
- __slots__ = ('_addr', '_dbh', '_domain', '_mail', '_new', '_passwd',
- '_transport', '_uid')
-
- def __init__(self, dbh, address):
- """Creates a new Account instance.
-
- When an account with the given *address* could be found in the
- database all relevant data will be loaded.
-
- Arguments:
-
- `dbh` : pyPgSQL.PgSQL.Connection
- A database connection for the database access.
- `address` : VirtualMailManager.EmailAddress.EmailAddress
- The e-mail address of the (new) Account.
- """
- if not isinstance(address, EmailAddress):
- raise TypeError("Argument 'address' is not an EmailAddress")
- self._addr = address
- self._dbh = dbh
- self._domain = Domain(self._dbh, self._addr.domainname)
- if not self._domain.gid:
- raise AErr(_(u"The domain '%s' doesn't exist.") %
- self._addr.domainname, NO_SUCH_DOMAIN)
- self._uid = 0
- self._mail = None
- self._transport = self._domain.transport
- self._passwd = None
- self._new = True
- self._load()
-
- def __nonzero__(self):
- """Returns `True` if the Account is known, `False` if it's new."""
- return not self._new
-
- def _load(self):
- """Load 'uid', 'mid' and 'tid' from the database and set _new to
- `False` - if the user could be found. """
- dbc = self._dbh.cursor()
- dbc.execute('SELECT uid, mid, tid FROM users WHERE gid = %s AND '
- 'local_part = %s', self._domain.gid, self._addr.localpart)
- result = dbc.fetchone()
- dbc.close()
- if result:
- self._uid, _mid, _tid = result
- if _tid != self._transport.tid:
- self._transport = Transport(self._dbh, tid=_tid)
- self._mail = MailLocation(self._dbh, mid=_mid)
- self._new = False
-
- def _set_uid(self):
- """Set the unique ID for the new Account."""
- assert self._uid == 0
- dbc = self._dbh.cursor()
- dbc.execute("SELECT nextval('users_uid')")
- self._uid = dbc.fetchone()[0]
- dbc.close()
-
- def _prepare(self, maillocation):
- """Check and set different attributes - before we store the
- information in the database.
- """
- if maillocation.dovecot_version > cfg_dget('misc.dovecot_version'):
- raise AErr(_(u"The mailbox format '%(mbfmt)s' requires Dovecot "
- u">= v%(version)s") % {'mbfmt': maillocation.mbformat,
- 'version': version_str(maillocation.dovecot_version)},
- INVALID_MAIL_LOCATION)
- if not maillocation.postfix and \
- self._transport.transport.lower() in ('virtual:', 'virtual'):
- raise AErr(_(u"Invalid transport '%(transport)s' for mailbox "
- u"format '%(mbfmt)s'") %
- {'transport': self._transport,
- 'mbfmt': maillocation.mbformat}, INVALID_MAIL_LOCATION)
- self._mail = maillocation
- self._set_uid()
-
- def _switch_state(self, state, service):
- """Switch the state of the Account's services on or off. See
- Account.enable()/Account.disable() for more information."""
- self._chk_state()
- if service not in (None, 'all', 'imap', 'pop3', 'sieve', 'smtp'):
- raise AErr(_(u"Unknown service: '%s'.") % service, UNKNOWN_SERVICE)
- if cfg_dget('misc.dovecot_version') >= 0x10200b02:
- sieve_col = 'sieve'
- else:
- sieve_col = 'managesieve'
- if service in ('smtp', 'pop3', 'imap'):
- sql = 'UPDATE users SET %s = %s WHERE uid = %d' % (service, state,
- self._uid)
- elif service == 'sieve':
- sql = 'UPDATE users SET %s = %s WHERE uid = %d' % (sieve_col,
- state,
- self._uid)
- else:
- sql = 'UPDATE users SET smtp = %(s)s, pop3 = %(s)s, imap = %(s)s,\
- %(col)s = %(s)s WHERE uid = %(uid)d' % \
- {'s': state, 'col': sieve_col, 'uid': self._uid}
- dbc = self._dbh.cursor()
- dbc.execute(sql)
- if dbc.rowcount > 0:
- self._dbh.commit()
- dbc.close()
-
- def _count_aliases(self):
- """Count all alias addresses where the destination address is the
- address of the Account."""
- dbc = self._dbh.cursor()
- sql = "SELECT COUNT(destination) FROM alias WHERE destination = '%s'"\
- % self._addr
- dbc.execute(sql)
- a_count = dbc.fetchone()[0]
- dbc.close()
- return a_count
-
- def _chk_state(self):
- """Raise an AccountError if the Account is new - not yet saved in the
- database."""
- if self._new:
- raise AErr(_(u"The account '%s' doesn't exist.") % self._addr,
- NO_SUCH_ACCOUNT)
-
- @property
- def address(self):
- """The Account's EmailAddress instance."""
- return self._addr
-
- @property
- def domain_directory(self):
- """The directory of the domain the Account belongs to."""
- if self._domain:
- return self._domain.directory
- return None
-
- @property
- def gid(self):
- """The Account's group ID."""
- if self._domain:
- return self._domain.gid
- return None
-
- @property
- def home(self):
- """The Account's home directory."""
- if not self._new:
- return '%s/%s' % (self._domain.directory, self._uid)
- return None
-
- @property
- def mail_location(self):
- """The Account's MailLocation."""
- return self._mail
-
- @property
- def uid(self):
- """The Account's unique ID."""
- return self._uid
-
- def set_password(self, password):
- """Set a password for the new Account.
-
- If you want to update the password of an existing Account use
- Account.modify().
-
- Argument:
-
- `password` : basestring
- The password for the new Account.
- """
- if not isinstance(password, basestring) or not password:
- raise AErr(_(u"Couldn't accept password: '%s'") % password,
- ACCOUNT_MISSING_PASSWORD)
- self._passwd = password
-
- def set_transport(self, transport):
- """Set the transport for the new Account.
-
- If you want to update the transport of an existing Account use
- Account.modify().
-
- Argument:
-
- `transport` : basestring
- The string representation of the transport, e.g.: 'dovecot:'
- """
- self._transport = Transport(self._dbh, transport=transport)
-
- def enable(self, service=None):
- """Enable a/all service/s for the Account.
-
- Possible values for the *service* are: 'imap', 'pop3', 'sieve' and
- 'smtp'. When all services should be enabled, use 'all' or the
- default value `None`.
-
- Arguments:
-
- `service` : basestring
- The name of a service ('imap', 'pop3', 'smtp', 'sieve'), 'all'
- or `None`.
- """
- self._switch_state(True, service)
-
- def disable(self, service=None):
- """Disable a/all service/s for the Account.
-
- For more information see: Account.enable()."""
- self._switch_state(False, service)
-
- def save(self):
- """Save the new Account in the database."""
- if not self._new:
- raise AErr(_(u"The account '%s' already exists.") % self._addr,
- ACCOUNT_EXISTS)
- if not self._passwd:
- raise AErr(_(u"No password set for '%s'.") % self._addr,
- ACCOUNT_MISSING_PASSWORD)
- if cfg_dget('misc.dovecot_version') >= 0x10200b02:
- sieve_col = 'sieve'
- else:
- sieve_col = 'managesieve'
- self._prepare(MailLocation(self._dbh, mbfmt=cfg_dget('mailbox.format'),
- directory=cfg_dget('mailbox.root')))
- sql = "INSERT INTO users (local_part, passwd, uid, gid, mid, tid,\
- smtp, pop3, imap, %s) VALUES ('%s', '%s', %d, %d, %d, %d, %s, %s, %s, %s)" % (
- sieve_col, self._addr.localpart, pwhash(self._passwd,
- user=self._addr),
- self._uid, self._domain.gid, self._mail.mid, self._transport.tid,
- cfg_dget('account.smtp'), cfg_dget('account.pop3'),
- cfg_dget('account.imap'), cfg_dget('account.sieve'))
- dbc = self._dbh.cursor()
- dbc.execute(sql)
- self._dbh.commit()
- dbc.close()
- self._new = False
-
- def modify(self, field, value):
- """Update the Account's *field* to the new *value*.
-
- Possible values for *field* are: 'name', 'password' and
- 'transport'. *value* is the *field*'s new value.
-
- Arguments:
-
- `field` : basestring
- The attribute name: 'name', 'password' or 'transport'
- `value` : basestring
- The new value of the attribute.
- """
- if field not in ('name', 'password', 'transport'):
- raise AErr(_(u"Unknown field: '%s'") % field, INVALID_ARGUMENT)
- self._chk_state()
- dbc = self._dbh.cursor()
- if field == 'password':
- dbc.execute('UPDATE users SET passwd = %s WHERE uid = %s',
- pwhash(value, user=self._addr), self._uid)
- elif field == 'transport':
- if value != self._transport.transport:
- self._transport = Transport(self._dbh, transport=value)
- dbc.execute('UPDATE users SET tid = %s WHERE uid = %s',
- self._transport.tid, self._uid)
- else:
- dbc.execute('UPDATE users SET name = %s WHERE uid = %s',
- value, self._uid)
- if dbc.rowcount > 0:
- self._dbh.commit()
- dbc.close()
-
- def get_info(self):
- """Returns a dict with some information about the Account.
-
- The keys of the dict are: 'address', 'gid', 'home', 'imap'
- 'mail_location', 'name', 'pop3', 'sieve', 'smtp', transport' and
- 'uid'.
- """
- self._chk_state()
- if cfg_dget('misc.dovecot_version') >= 0x10200b02:
- sieve_col = 'sieve'
- else:
- sieve_col = 'managesieve'
- sql = 'SELECT name, smtp, pop3, imap, %s FROM users WHERE uid = %d' % \
- (sieve_col, self._uid)
- dbc = self._dbh.cursor()
- dbc.execute(sql)
- info = dbc.fetchone()
- dbc.close()
- if info:
- keys = ('name', 'smtp', 'pop3', 'imap', sieve_col)
- info = dict(zip(keys, info))
- for service in keys[1:]:
- if info[service]:
- # TP: A service (pop3/imap) is enabled/usable for a user
- info[service] = _('enabled')
- else:
- # TP: A service (pop3/imap) isn't enabled/usable for a user
- info[service] = _('disabled')
- info['address'] = self._addr
- info['gid'] = self._domain.gid
- info['home'] = '%s/%s' % (self._domain.directory, self._uid)
- info['mail_location'] = self._mail.mail_location
- info['transport'] = self._transport.transport
- info['uid'] = self._uid
- return info
- # nearly impossible‽
- raise AErr(_(u"Couldn't fetch information for account: '%s'") %
- self._addr, NO_SUCH_ACCOUNT)
-
- def get_aliases(self):
- """Return a list with all alias e-mail addresses, whose destination
- is the address of the Account."""
- self._chk_state()
- dbc = self._dbh.cursor()
- dbc.execute("SELECT address ||'@'|| domainname FROM alias, "
- "domain_name WHERE destination = %s AND domain_name.gid = "
- "alias.gid AND domain_name.is_primary ORDER BY address",
- str(self._addr))
- addresses = dbc.fetchall()
- dbc.close()
- aliases = []
- if addresses:
- aliases = [alias[0] for alias in addresses]
- return aliases
-
- def delete(self, delalias=False):
- """Delete the Account from the database.
-
- Argument:
-
- `delalias` : bool
- if *delalias* is `True`, all aliases, which points to the Account,
- will be also deleted. If there are aliases and *delalias* is
- `False`, an AccountError will be raised.
- """
- assert isinstance(delalias, bool)
- self._chk_state()
- dbc = self._dbh.cursor()
- if delalias:
- dbc.execute('DELETE FROM users WHERE uid = %s', self._uid)
- # delete also all aliases where the destination address is the same
- # as for this account.
- dbc.execute("DELETE FROM alias WHERE destination = %s",
- str(self._addr))
- self._dbh.commit()
- else: # check first for aliases
- a_count = self._count_aliases()
- if a_count > 0:
- dbc.close()
- raise AErr(_(u"There are %(count)d aliases with the "
- u"destination address '%(address)s'.") %
- {'count': a_count, 'address': self._addr},
- ALIAS_PRESENT)
- dbc.execute('DELETE FROM users WHERE uid = %s', self._uid)
- self._dbh.commit()
- dbc.close()
- self._new = True
- self._uid = 0
- self._addr = self._dbh = self._domain = self._passwd = None
- self._mail = self._transport = None
-
-
-def get_account_by_uid(uid, dbh):
- """Search an Account by its UID.
-
- This function returns a dict (keys: 'address', 'gid' and 'uid'), if an
- Account with the given *uid* exists.
-
- Argument:
-
- `uid` : long
- The Account unique ID.
- `dbh` : pyPgSQL.PgSQL.Connection
- a database connection for the database access.
- """
- try:
- uid = long(uid)
- except ValueError:
- raise AErr(_(u'UID must be an int/long.'), INVALID_ARGUMENT)
- if uid < 1:
- raise AErr(_(u'UID must be greater than 0.'), INVALID_ARGUMENT)
- dbc = dbh.cursor()
- dbc.execute("SELECT local_part||'@'|| domain_name.domainname AS address, "
- "uid, users.gid FROM users LEFT JOIN domain_name ON "
- "(domain_name.gid = users.gid AND is_primary) WHERE uid = %s",
- uid)
- info = dbc.fetchone()
- dbc.close()
- if not info:
- raise AErr(_(u"There is no account with the UID '%d'.") % uid,
- NO_SUCH_ACCOUNT)
- info = dict(zip(('address', 'uid', 'gid'), info))
- return info
-
-del _, cfg_dget
--- a/VirtualMailManager/Alias.py Wed Jul 28 01:03:56 2010 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,165 +0,0 @@
-# -*- coding: UTF-8 -*-
-# Copyright (c) 2007 - 2010, Pascal Volk
-# See COPYING for distribution information.
-
-"""
- VirtualMailManager.Alias
-
- Virtual Mail Manager's Alias class to manage e-mail aliases.
-"""
-
-from VirtualMailManager.Domain import get_gid
-from VirtualMailManager.EmailAddress import EmailAddress
-from VirtualMailManager.errors import AliasError as AErr
-from VirtualMailManager.ext.Postconf import Postconf
-from VirtualMailManager.pycompat import all
-from VirtualMailManager.constants import \
- ALIAS_EXCEEDS_EXPANSION_LIMIT, NO_SUCH_ALIAS, NO_SUCH_DOMAIN
-
-
-_ = lambda msg: msg
-cfg_dget = lambda option: None
-
-
-class Alias(object):
- """Class to manage e-mail aliases."""
- __slots__ = ('_addr', '_dests', '_gid', '_dbh')
-
- def __init__(self, dbh, address):
- assert isinstance(address, EmailAddress)
- self._addr = address
- self._dbh = dbh
- self._gid = get_gid(self._dbh, self._addr.domainname)
- if not self._gid:
- raise AErr(_(u"The domain '%s' doesn't exist.") %
- self._addr.domainname, NO_SUCH_DOMAIN)
- self._dests = []
-
- self.__load_dests()
-
- def __load_dests(self):
- """Loads all known destination addresses into the _dests list."""
- dbc = self._dbh.cursor()
- dbc.execute('SELECT destination FROM alias WHERE gid = %s AND '
- 'address = %s', self._gid, self._addr.localpart)
- dests = dbc.fetchall()
- if dbc.rowcount > 0:
- self._dests.extend(EmailAddress(dest[0]) for dest in dests)
- dbc.close()
-
- def __check_expansion(self, count_new):
- """Checks the current expansion limit of the alias."""
- postconf = Postconf(cfg_dget('bin.postconf'))
- limit = long(postconf.read('virtual_alias_expansion_limit'))
- dcount = len(self._dests)
- failed = False
- if dcount == limit or dcount + count_new > limit:
- failed = True
- errmsg = _(
-u"""Can't add %(count_new)i new destination(s) to alias '%(address)s'.
-Currently this alias expands into %(count)i/%(limit)i recipients.
-%(count_new)i additional destination(s) will render this alias unusable.
-Hint: Increase Postfix' virtual_alias_expansion_limit""")
- elif dcount > limit:
- failed = True
- errmsg = _(
-u"""Can't add %(count_new)i new destination(s) to alias '%(address)s'.
-This alias already exceeds its expansion limit (%(count)i/%(limit)i).
-So its unusable, all messages addressed to this alias will be bounced.
-Hint: Delete some destination addresses.""")
- if failed:
- raise AErr(errmsg % {'address': self._addr, 'count': dcount,
- 'limit': limit, 'count_new': count_new},
- ALIAS_EXCEEDS_EXPANSION_LIMIT)
-
- def __delete(self, destination=None):
- """Deletes a destination from the alias, if ``destination`` is
- not ``None``. If ``destination`` is None, the alias with all
- its destination addresses will be deleted.
-
- """
- dbc = self._dbh.cursor()
- if not destination:
- dbc.execute('DELETE FROM alias WHERE gid = %s AND address = %s',
- self._gid, self._addr.localpart)
- else:
- dbc.execute('DELETE FROM alias WHERE gid = %s AND address = %s '
- 'AND destination = %s', self._gid,
- self._addr.localpart, str(destination))
- if dbc.rowcount > 0:
- self._dbh.commit()
- dbc.close()
-
- def __len__(self):
- """Returns the number of destinations of the alias."""
- return len(self._dests)
-
- @property
- def address(self):
- """The Alias' EmailAddress instance."""
- return self._addr
-
- def add_destinations(self, destinations, warnings=None):
- """Adds the `EmailAddress`es from *destinations* list to the
- destinations of the alias.
-
- Destinations, that are already assigned to the alias, will be
- removed from *destinations*. When done, this method will return
- a set with all destinations, that were saved in the database.
- """
- destinations = set(destinations)
- assert destinations and \
- all(isinstance(dest, EmailAddress) for dest in destinations)
- if not warnings is None:
- assert isinstance(warnings, list)
- if self._addr in destinations:
- destinations.remove(self._addr)
- if not warnings is None:
- warnings.append(self._addr)
- duplicates = destinations.intersection(set(self._dests))
- if duplicates:
- destinations.difference_update(set(self._dests))
- if not warnings is None:
- warnings.extend(duplicates)
- if not destinations:
- return destinations
- self.__check_expansion(len(destinations))
- dbc = self._dbh.cursor()
- dbc.executemany("INSERT INTO alias VALUES (%d, '%s', %%s)" %
- (self._gid, self._addr.localpart),
- (str(destination) for destination in destinations))
- self._dbh.commit()
- dbc.close()
- self._dests.extend(destinations)
- return destinations
-
- def del_destination(self, destination):
- """Deletes the specified ``destination`` address from the alias."""
- assert isinstance(destination, EmailAddress)
- if not self._dests:
- raise AErr(_(u"The alias '%s' doesn't exist.") % self._addr,
- NO_SUCH_ALIAS)
- if not destination in self._dests:
- raise AErr(_(u"The address '%(addr)s' isn't a destination of "
- u"the alias '%(alias)s'.") % {'addr': self._addr,
- 'alias': destination}, NO_SUCH_ALIAS)
- self.__delete(destination)
- self._dests.remove(destination)
-
- def get_destinations(self):
- """Returns an iterator for all destinations of the alias."""
- if not self._dests:
- raise AErr(_(u"The alias '%s' doesn't exist.") % self._addr,
- NO_SUCH_ALIAS)
- return iter(self._dests)
-
- def delete(self):
- """Deletes the alias with all its destinations."""
- if not self._dests:
- raise AErr(_(u"The alias '%s' doesn't exist.") % self._addr,
- NO_SUCH_ALIAS)
- self.__delete()
- del self._dests[:]
-
-
-del _, cfg_dget
--- a/VirtualMailManager/AliasDomain.py Wed Jul 28 01:03:56 2010 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,144 +0,0 @@
-# -*- coding: UTF-8 -*-
-# Copyright (c) 2008 - 2010, Pascal Volk
-# See COPYING for distribution information.
-
-"""
- VirtualMailManager.AliasDomain
-
- Virtual Mail Manager's AliasDomain class to manage alias domains.
-"""
-
-from VirtualMailManager.Domain import Domain, check_domainname
-from VirtualMailManager.constants import \
- ALIASDOMAIN_EXISTS, ALIASDOMAIN_ISDOMAIN, ALIASDOMAIN_NO_DOMDEST, \
- NO_SUCH_ALIASDOMAIN, NO_SUCH_DOMAIN
-from VirtualMailManager.errors import AliasDomainError as ADErr
-
-
-_ = lambda msg: msg
-
-
-class AliasDomain(object):
- """Class to manage e-mail alias domains."""
- __slots__ = ('_gid', '_name', '_domain', '_dbh')
-
- def __init__(self, dbh, domainname):
- """Creates a new AliasDomain instance.
-
- Arguments:
-
- `dbh` : pyPgSQL.PgSQL.Connection
- a database connection for the database access
- `domainname` : basestring
- the name of the AliasDomain"""
- self._dbh = dbh
- self._name = check_domainname(domainname)
- self._gid = 0
- self._domain = None
- self._load()
-
- def _load(self):
- """Loads the AliasDomain's GID from the database and checks if the
- domain name is marked as primary."""
- dbc = self._dbh.cursor()
- dbc.execute('SELECT gid, is_primary FROM domain_name WHERE '
- 'domainname = %s', self._name)
- result = dbc.fetchone()
- dbc.close()
- if result:
- if result[1]:
- raise ADErr(_(u"The domain '%s' is a primary domain.") %
- self._name, ALIASDOMAIN_ISDOMAIN)
- self._gid = result[0]
-
- def set_destination(self, dest_domain):
- """Set the destination of a new AliasDomain or updates the
- destination of an existing AliasDomain.
-
- Argument:
-
- `dest_domain` : VirtualMailManager.Domain.Domain
- the AliasDomain's destination domain
- """
- assert isinstance(dest_domain, Domain)
- self._domain = dest_domain
-
- def save(self):
- """Stores information about the new AliasDomain in the database."""
- if self._gid > 0:
- raise ADErr(_(u"The alias domain '%s' already exists.") %
- self._name, ALIASDOMAIN_EXISTS)
- if not self._domain:
- raise ADErr(_(u'No destination domain set for the alias domain.'),
- ALIASDOMAIN_NO_DOMDEST)
- if self._domain.gid < 1:
- raise ADErr(_(u"The target domain '%s' doesn't exist.") %
- self._domain.name, NO_SUCH_DOMAIN)
- dbc = self._dbh.cursor()
- dbc.execute('INSERT INTO domain_name VALUES (%s, %s, FALSE)',
- self._name, self._domain.gid)
- self._dbh.commit()
- dbc.close()
- self._gid = self._domain.gid
-
- def info(self):
- """Returns a dict (keys: "alias" and "domain") with the names of the
- AliasDomain and its primary domain."""
- if self._gid < 1:
- raise ADErr(_(u"The alias domain '%s' doesn't exist.") %
- self._name, NO_SUCH_ALIASDOMAIN)
- dbc = self._dbh.cursor()
- dbc.execute('SELECT domainname FROM domain_name WHERE gid = %s AND '
- 'is_primary', self._gid)
- domain = dbc.fetchone()
- dbc.close()
- if domain:
- return {'alias': self._name, 'domain': domain[0]}
- else: # an almost unlikely case, isn't it?
- raise ADErr(_(u'There is no primary domain for the alias domain '
- u"'%s'.") % self._name, NO_SUCH_DOMAIN)
-
- def switch(self):
- """Switch the destination of the AliasDomain to the new destination,
- set with the method `set_destination()`.
- """
- if not self._domain:
- raise ADErr(_(u'No destination domain set for the alias domain.'),
- ALIASDOMAIN_NO_DOMDEST)
- if self._domain.gid < 1:
- raise ADErr(_(u"The target domain '%s' doesn't exist.") %
- self._domain.name, NO_SUCH_DOMAIN)
- if self._gid < 1:
- raise ADErr(_(u"The alias domain '%s' doesn't exist.") %
- self._name, NO_SUCH_ALIASDOMAIN)
- if self._gid == self._domain.gid:
- raise ADErr(_(u"The alias domain '%(alias)s' is already assigned "
- u"to the domain '%(domain)s'.") %
- {'alias': self._name, 'domain': self._domain.name},
- ALIASDOMAIN_EXISTS)
- dbc = self._dbh.cursor()
- dbc.execute('UPDATE domain_name SET gid = %s WHERE gid = %s AND '
- 'domainname = %s AND NOT is_primary', self._domain.gid,
- self._gid, self._name)
- self._dbh.commit()
- dbc.close()
- self._gid = self._domain.gid
-
- def delete(self):
- """Delete the AliasDomain's record form the database.
-
- Raises an AliasDomainError if the AliasDomain doesn't exist.
- """
- if self._gid < 1:
- raise ADErr(_(u"The alias domain '%s' doesn't exist.") %
- self._name, NO_SUCH_ALIASDOMAIN)
- dbc = self._dbh.cursor()
- dbc.execute('DELETE FROM domain_name WHERE domainname = %s AND NOT '
- 'is_primary', self._name)
- if dbc.rowcount > 0:
- self._dbh.commit()
- self._gid = 0
- dbc.close()
-
-
-del _
--- a/VirtualMailManager/Config.py Wed Jul 28 01:03:56 2010 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,452 +0,0 @@
-# -*- coding: UTF-8 -*-
-# Copyright (c) 2007 - 2010, Pascal Volk
-# See COPYING for distribution information.
-
-"""
- VirtualMailManager.Config
- ~~~~~~~~~~~~~~~~~~~~~~~~~
-
- VMM's configuration module for simplified configuration access.
-"""
-
-import re
-
-from ConfigParser import \
- Error, MissingSectionHeaderError, NoOptionError, NoSectionError, \
- ParsingError, RawConfigParser
-from cStringIO import StringIO# TODO: move interactive stff to cli
-
-from VirtualMailManager.common import exec_ok, get_unicode, is_dir, version_hex
-from VirtualMailManager.constants import CONF_ERROR
-from VirtualMailManager.errors import ConfigError, VMMError
-from VirtualMailManager.maillocation import known_format
-from VirtualMailManager.password import verify_scheme as _verify_scheme
-
-
-_ = lambda msg: msg
-
-
-class BadOptionError(Error):
- """Raised when a option isn't in the format 'section.option'."""
- pass
-
-
-class ConfigValueError(Error):
- """Raised when creating or validating of new values fails."""
- pass
-
-
-class NoDefaultError(Error):
- """Raised when the requested option has no default value."""
-
- def __init__(self, section, option):
- Error.__init__(self, 'Option %r in section %r has no default value' %
- (option, section))
-
-
-class LazyConfig(RawConfigParser):
- """The **lazy** derivate of the `RawConfigParser`.
-
- There are two additional getters:
-
- `pget()`
- The polymorphic getter, which returns a option's value with the
- appropriate type.
- `dget()`
- Like `LazyConfig.pget()`, but returns the option's default, from
- `LazyConfig._cfg['sectionname']['optionname'].default`, if the
- option is not configured in a ini-like configuration file.
-
- `set()` differs from `RawConfigParser`'s `set()` method. `set()`
- takes the `section` and `option` arguments combined to a single
- string in the form "section.option".
- """
-
- def __init__(self):
- RawConfigParser.__init__(self)
- self._modified = False
- # sample _cfg dict. Create your own in your derived class.
- self._cfg = {
- 'sectionname': {
- 'optionname': LazyConfigOption(int, 1, self.getint),
- }
- }
-
- def bool_new(self, value):
- """Converts the string `value` into a `bool` and returns it.
-
- | '1', 'on', 'yes' and 'true' will become `True`
- | '0', 'off', 'no' and 'false' will become `False`
-
- Throws a `ConfigValueError` for all other values, except bools.
- """
- if isinstance(value, bool):
- return value
- if value.lower() in self._boolean_states:
- return self._boolean_states[value.lower()]
- else:
- raise ConfigValueError(_(u"Not a boolean: '%s'") %
- get_unicode(value))
-
- def getboolean(self, section, option):
- """Returns the boolean value of the option, in the given
- section.
-
- For a boolean True, the value must be set to '1', 'on', 'yes',
- 'true' or True. For a boolean False, the value must set to '0',
- 'off', 'no', 'false' or False.
- If the option has another value assigned this method will raise
- a ValueError.
- """
- # if the setting was modified it may be still a boolean value lets see
- tmp = self.get(section, option)
- if isinstance(tmp, bool):
- return tmp
- if not tmp.lower() in self._boolean_states:
- raise ValueError('Not a boolean: %s' % tmp)
- return self._boolean_states[tmp.lower()]
-
- def _get_section_option(self, section_option):
- """splits ``section_option`` (section.option) in two parts and
- returns them as list ``[section, option]``, if:
-
- * it likes the format of ``section_option``
- * the ``section`` is known
- * the ``option`` is known
-
- Else one of the following exceptions will be thrown:
-
- * `BadOptionError`
- * `NoSectionError`
- * `NoOptionError`
- """
- sect_opt = section_option.lower().split('.')
- # TODO: cache it
- if len(sect_opt) != 2: # do we need a regexp to check the format?
- raise BadOptionError(_(u"Bad format: '%s' - expected: "
- u"section.option") %
- get_unicode(section_option))
- if not sect_opt[0] in self._cfg:
- raise NoSectionError(sect_opt[0])
- if not sect_opt[1] in self._cfg[sect_opt[0]]:
- raise NoOptionError(sect_opt[1], sect_opt[0])
- return sect_opt
-
- def items(self, section):
- """returns an iterable that returns key, value ``tuples`` from
- the given ``section``.
- """
- if section in self._sections: # check if the section was parsed
- sect = self._sections[section]
- elif not section in self._cfg:
- raise NoSectionError(section)
- else:
- return ((k, self._cfg[section][k].default) \
- for k in self._cfg[section].iterkeys())
- # still here? Get defaults and merge defaults with configured setting
- defaults = dict((k, self._cfg[section][k].default) \
- for k in self._cfg[section].iterkeys())
- defaults.update(sect)
- if '__name__' in defaults:
- del defaults['__name__']
- return defaults.iteritems()
-
- def dget(self, option):
- """Returns the value of the `option`.
-
- If the option could not be found in the configuration file, the
- configured default value, from ``LazyConfig._cfg`` will be
- returned.
-
- Arguments:
-
- `option` : string
- the configuration option in the form "section.option"
-
- Throws a `NoDefaultError`, if no default value was passed to
- `LazyConfigOption.__init__()` for the `option`.
- """
- section, option = self._get_section_option(option)
- try:
- return self._cfg[section][option].getter(section, option)
- except (NoSectionError, NoOptionError):
- if not self._cfg[section][option].default is None: # may be False
- return self._cfg[section][option].default
- else:
- raise NoDefaultError(section, option)
-
- def pget(self, option):
- """Returns the value of the `option`."""
- section, option = self._get_section_option(option)
- return self._cfg[section][option].getter(section, option)
-
- def set(self, option, value):
- """Set the `value` of the `option`.
-
- Throws a `ValueError` if `value` couldn't be converted using
- `LazyConfigOption.cls`.
- """
- # pylint: disable=W0221
- # @pylint: _L A Z Y_
- section, option = self._get_section_option(option)
- val = self._cfg[section][option].cls(value)
- if self._cfg[section][option].validate:
- val = self._cfg[section][option].validate(val)
- if not RawConfigParser.has_section(self, section):
- self.add_section(section)
- RawConfigParser.set(self, section, option, val)
- self._modified = True
-
- def has_section(self, section):
- """Checks if `section` is a known configuration section."""
- return section.lower() in self._cfg
-
- def has_option(self, option):
- """Checks if the option (section.option) is a known
- configuration option.
- """
- # pylint: disable=W0221
- # @pylint: _L A Z Y_
- try:
- self._get_section_option(option)
- return True
- except(BadOptionError, NoSectionError, NoOptionError):
- return False
-
- def sections(self):
- """Returns an iterator object for all configuration sections."""
- return self._cfg.iterkeys()
-
-
-class LazyConfigOption(object):
- """A simple container class for configuration settings.
-
- `LazyConfigOption` instances are required by `LazyConfig` instances,
- and instances of classes derived from `LazyConfig`, like the
- `Config` class.
- """
- __slots__ = ('__cls', '__default', '__getter', '__validate')
-
- def __init__(self, cls, default, getter, validate=None):
- """Creates a new `LazyConfigOption` instance.
-
- Arguments:
-
- `cls` : type
- The class/type of the option's value
- `default`
- Default value of the option. Use ``None`` if the option should
- not have a default value.
- `getter` : callable
- A method's name of `RawConfigParser` and derived classes, to
- get a option's value, e.g. `self.getint`.
- `validate` : NoneType or a callable
- None or any method, that takes one argument, in order to
- check the value, when `LazyConfig.set()` is called.
- """
- self.__cls = cls
- if not default is None: # enforce the type of the default value
- self.__default = self.__cls(default)
- else:
- self.__default = default
- if not callable(getter):
- raise TypeError('getter has to be a callable, got a %r' %
- getter.__class__.__name__)
- self.__getter = getter
- if validate and not callable(validate):
- raise TypeError('validate has to be callable or None, got a %r' %
- validate.__class__.__name__)
- self.__validate = validate
-
- @property
- def cls(self):
- """The class of the option's value e.g. `str`, `unicode` or `bool`."""
- return self.__cls
-
- @property
- def default(self):
- """The option's default value, may be `None`"""
- return self.__default
-
- @property
- def getter(self):
- """The getter method or function to get the option's value"""
- return self.__getter
-
- @property
- def validate(self):
- """A method or function to validate the value"""
- return self.__validate
-
-
-class Config(LazyConfig):
- """This class is for reading vmm's configuration file."""
-
- def __init__(self, filename):
- """Creates a new Config instance
-
- Arguments:
-
- `filename` : str
- path to the configuration file
- """
- LazyConfig.__init__(self)
- self._cfg_filename = filename
- self._cfg_file = None
- self.__missing = {}
-
- LCO = LazyConfigOption
- bool_t = self.bool_new
- self._cfg = {
- 'account': {
- 'delete_directory': LCO(bool_t, False, self.getboolean),
- 'directory_mode': LCO(int, 448, self.getint),
- 'disk_usage': LCO(bool_t, False, self.getboolean),
- 'password_length': LCO(int, 8, self.getint),
- 'random_password': LCO(bool_t, False, self.getboolean),
- 'imap': LCO(bool_t, True, self.getboolean),
- 'pop3': LCO(bool_t, True, self.getboolean),
- 'sieve': LCO(bool_t, True, self.getboolean),
- 'smtp': LCO(bool_t, True, self.getboolean),
- },
- 'bin': {
- 'dovecotpw': LCO(str, '/usr/sbin/dovecotpw', self.get,
- exec_ok),
- 'du': LCO(str, '/usr/bin/du', self.get, exec_ok),
- 'postconf': LCO(str, '/usr/sbin/postconf', self.get, exec_ok),
- },
- 'database': {
- 'host': LCO(str, 'localhost', self.get),
- 'name': LCO(str, 'mailsys', self.get),
- 'pass': LCO(str, None, self.get),
- 'user': LCO(str, None, self.get),
- },
- 'domain': {
- 'auto_postmaster': LCO(bool_t, True, self.getboolean),
- 'delete_directory': LCO(bool_t, False, self.getboolean),
- 'directory_mode': LCO(int, 504, self.getint),
- 'force_deletion': LCO(bool_t, False, self.getboolean),
- },
- 'mailbox': {
- 'folders': LCO(str, 'Drafts:Sent:Templates:Trash',
- self.unicode),
- 'format': LCO(str, 'maildir', self.get, check_mailbox_format),
- 'root': LCO(str, 'Maildir', self.unicode),
- 'subscribe': LCO(bool_t, True, self.getboolean),
- },
- 'misc': {
- 'base_directory': LCO(str, '/srv/mail', self.get, is_dir),
- 'crypt_blowfish_rounds': LCO(int, 5, self.getint),
- 'crypt_sha256_rounds': LCO(int, 5000, self.getint),
- 'crypt_sha512_rounds': LCO(int, 5000, self.getint),
- 'dovecot_version': LCO(str, None, self.hexversion,
- check_version_format),
- 'password_scheme': LCO(str, 'CRAM-MD5', self.get,
- verify_scheme),
- 'transport': LCO(str, 'dovecot:', self.get),
- },
- }
-
- def load(self):
- """Loads the configuration, read only.
-
- Raises a ConfigError if the configuration syntax is
- invalid.
- """
- try:
- self._cfg_file = open(self._cfg_filename, 'r')
- self.readfp(self._cfg_file)
- except (MissingSectionHeaderError, ParsingError), err:
- raise ConfigError(str(err), CONF_ERROR)
- finally:
- if self._cfg_file and not self._cfg_file.closed:
- self._cfg_file.close()
-
- def check(self):
- """Performs a configuration check.
-
- Raises a ConfigError if settings w/o a default value are missed.
- Or a ConfigValueError if 'misc.dovecot_version' has the wrong
- format.
- """
- # TODO: There are only two settings w/o defaults.
- # So there is no need for cStringIO
- if not self.__chk_cfg():
- errmsg = StringIO()
- errmsg.write(_(u'Missing options, which have no default value.\n'))
- errmsg.write(_(u'Using configuration file: %s\n') %
- self._cfg_filename)
- for section, options in self.__missing.iteritems():
- errmsg.write(_(u'* Section: %s\n') % section)
- for option in options:
- errmsg.write((u' %s\n') % option)
- raise ConfigError(errmsg.getvalue(), CONF_ERROR)
- check_version_format(self.get('misc', 'dovecot_version'))
-
- def hexversion(self, section, option):
- """Converts the version number (e.g.: 1.2.3) from the *option*'s
- value to an int."""
- return version_hex(self.get(section, option))
-
- def unicode(self, section, option):
- """Returns the value of the `option` from `section`, converted
- to Unicode."""
- return get_unicode(self.get(section, option))
-
- def __chk_cfg(self):
- """Checks all section's options for settings w/o a default
- value.
-
- Returns `True` if everything is fine, else `False`.
- """
- errors = False
- for section in self._cfg.iterkeys():
- missing = []
- for option, value in self._cfg[section].iteritems():
- if (value.default is None and
- not RawConfigParser.has_option(self, section, option)):
- missing.append(option)
- errors = True
- if missing:
- self.__missing[section] = missing
- return not errors
-
-
-def check_mailbox_format(format):
- """
- Check if the mailbox format *format* is supported. When the *format*
- is supported it will be returned, otherwise a `ConfigValueError` will
- be raised.
- """
- format = format.lower()
- if known_format(format):
- return format
- raise ConfigValueError(_(u"Unsupported mailbox format: '%s'") %
- get_unicode(format))
-
-
-def check_version_format(version_string):
- """Check if the *version_string* has the proper format, e.g.: '1.2.3'.
- Returns the validated version string if it has the expected format.
- Otherwise a `ConfigValueError` will be raised.
- """
- version_re = r'^\d+\.\d+\.(?:\d+|(?:alpha|beta|rc)\d+)$'
- if not re.match(version_re, version_string):
- raise ConfigValueError(_(u"Not a valid Dovecot version: '%s'") %
- get_unicode(version_string))
- return version_string
-
-
-def verify_scheme(scheme):
- """Checks if the password scheme *scheme* can be accepted and returns
- the verified scheme.
- """
- try:
- scheme, encoding = _verify_scheme(scheme)
- except VMMError, err: # 'cast' it
- raise ConfigValueError(err.msg)
- if not encoding:
- return scheme
- return '%s.%s' % (scheme, encoding)
-
-del _
--- a/VirtualMailManager/Domain.py Wed Jul 28 01:03:56 2010 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,409 +0,0 @@
-# -*- coding: UTF-8 -*-
-# Copyright (c) 2007 - 2010, Pascal Volk
-# See COPYING for distribution information.
-
-"""
- VirtualMailManager.Domain
-
- Virtual Mail Manager's Domain class to manage e-mail domains.
-"""
-
-import os
-import re
-from random import choice
-
-from VirtualMailManager.constants import \
- ACCOUNT_AND_ALIAS_PRESENT, ACCOUNT_PRESENT, ALIAS_PRESENT, \
- DOMAIN_ALIAS_EXISTS, DOMAIN_EXISTS, DOMAIN_INVALID, DOMAIN_TOO_LONG, \
- NO_SUCH_DOMAIN
-from VirtualMailManager.errors import DomainError as DomErr
-from VirtualMailManager.Transport import Transport
-
-
-MAILDIR_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'
-RE_DOMAIN = re.compile(r"^(?:[a-z0-9-]{1,63}\.){1,}[a-z]{2,6}$")
-_ = lambda msg: msg
-
-
-class Domain(object):
- """Class to manage e-mail domains."""
- __slots__ = ('_directory', '_gid', '_name', '_transport', '_dbh', '_new')
-
- def __init__(self, dbh, domainname):
- """Creates a new Domain instance.
-
- Loads all relevant data from the database, if the domain could be
- found. To create a new domain call the methods set_directory() and
- set_transport() before save().
-
- A DomainError will be thrown when the *domainname* is the name of
- an alias domain.
-
- Arguments:
-
- `dbh` : pyPgSQL.PgSQL.Connection
- a database connection for the database access
- `domainname` : basestring
- The name of the domain
- """
- self._name = check_domainname(domainname)
- self._dbh = dbh
- self._gid = 0
- self._transport = None
- self._directory = None
- self._new = True
- self._load()
-
- def _load(self):
- """Load information from the database and checks if the domain name
- is the primary one.
-
- Raises a DomainError if Domain._name isn't the primary name of the
- domain.
- """
- dbc = self._dbh.cursor()
- dbc.execute('SELECT dd.gid, tid, domaindir, is_primary FROM '
- 'domain_data dd, domain_name dn WHERE domainname = %s AND '
- 'dn.gid = dd.gid', self._name)
- result = dbc.fetchone()
- dbc.close()
- if result:
- if not result[3]:
- raise DomErr(_(u"The domain '%s' is an alias domain.") %
- self._name, DOMAIN_ALIAS_EXISTS)
- self._gid, self._directory = result[0], result[2]
- self._transport = Transport(self._dbh, tid=result[1])
- self._new = False
-
- def _set_gid(self):
- """Sets the ID of the domain - if not set yet."""
- assert self._gid == 0
- dbc = self._dbh.cursor()
- dbc.execute("SELECT nextval('domain_gid')")
- self._gid = dbc.fetchone()[0]
- dbc.close()
-
- def _has(self, what):
- """Checks if aliases or accounts are assigned to the domain.
-
- If there are assigned accounts or aliases True will be returned,
- otherwise False will be returned.
-
- Argument:
-
- `what` : basestring
- "alias" or "users"
- """
- assert what in ('alias', 'users')
- dbc = self._dbh.cursor()
- if what == 'users':
- dbc.execute("SELECT count(gid) FROM users WHERE gid=%s", self._gid)
- else:
- dbc.execute("SELECT count(gid) FROM alias WHERE gid=%s", self._gid)
- count = dbc.fetchone()
- dbc.close()
- return count[0] > 0
-
- def _chk_delete(self, deluser, delalias):
- """Checks dependencies for deletion.
-
- Arguments:
- deluser -- ignore available accounts (bool)
- delalias -- ignore available aliases (bool)
- """
- if not deluser:
- hasuser = self._has('users')
- else:
- hasuser = False
- if not delalias:
- hasalias = self._has('alias')
- else:
- hasalias = False
- if hasuser and hasalias:
- raise DomErr(_(u'There are accounts and aliases.'),
- ACCOUNT_AND_ALIAS_PRESENT)
- elif hasuser:
- raise DomErr(_(u'There are accounts.'), ACCOUNT_PRESENT)
- elif hasalias:
- raise DomErr(_(u'There are aliases.'), ALIAS_PRESENT)
-
- def _chk_state(self):
- """Throws a DomainError if the Domain is new - not saved in the
- database."""
- if self._new:
- raise DomErr(_(u"The domain '%s' doesn't exist.") % self._name,
- NO_SUCH_DOMAIN)
-
- @property
- def gid(self):
- """The GID of the Domain."""
- return self._gid
-
- @property
- def name(self):
- """The Domain's name."""
- return self._name
-
- @property
- def directory(self):
- """The Domain's directory."""
- return self._directory
-
- def set_directory(self, basedir):
- """Set the path value of the Domain's directory, inside *basedir*.
-
- Argument:
-
- `basedir` : basestring
- The base directory of all domains
- """
- assert self._new and self._directory is None
- self._set_gid()
- self._directory = os.path.join(basedir, choice(MAILDIR_CHARS),
- str(self._gid))
-
- @property
- def transport(self):
- """The Domain's transport."""
- return self._transport
-
- def set_transport(self, transport):
- """Set the transport for the new Domain.
-
- Argument:
-
- `transport` : VirtualMailManager.Transport
- The transport of the new Domain
- """
- assert self._new and isinstance(transport, Transport)
- self._transport = transport
-
- def save(self):
- """Stores the new domain in the database."""
- if not self._new:
- raise DomErr(_(u"The domain '%s' already exists.") % self._name,
- DOMAIN_EXISTS)
- assert self._directory is not None and self._transport is not None
- dbc = self._dbh.cursor()
- dbc.execute("INSERT INTO domain_data VALUES (%s, %s, %s)", self._gid,
- self._transport.tid, self._directory)
- dbc.execute("INSERT INTO domain_name VALUES (%s, %s, %s)", self._name,
- self._gid, True)
- self._dbh.commit()
- dbc.close()
- self._new = False
-
- def delete(self, deluser=False, delalias=False):
- """Deletes the domain.
-
- Arguments:
-
- `deluser` : bool
- force deletion of all available accounts, default `False`
- `delalias` : bool
- force deletion of all available aliases, default `False`
- """
- self._chk_state()
- self._chk_delete(deluser, delalias)
- dbc = self._dbh.cursor()
- for tbl in ('alias', 'users', 'relocated', 'domain_name',
- 'domain_data'):
- dbc.execute("DELETE FROM %s WHERE gid = %d" % (tbl, self._gid))
- self._dbh.commit()
- dbc.close()
- self._gid = 0
- self._directory = self._transport = None
- self._new = True
-
- def update_transport(self, transport, force=False):
- """Sets a new transport for the Domain.
-
- If *force* is `True` the new *transport* will be assigned to all
- existing accounts. Otherwise the *transport* will be only used for
- accounts created from now on.
-
- Arguments:
-
- `transport` : VirtualMailManager.Transport
- the new transport
- `force` : bool
- enforce new transport setting for all accounts, default `False`
- """
- self._chk_state()
- assert isinstance(transport, Transport)
- if transport == self._transport:
- return
- dbc = self._dbh.cursor()
- dbc.execute("UPDATE domain_data SET tid = %s WHERE gid = %s",
- transport.tid, self._gid)
- if dbc.rowcount > 0:
- self._dbh.commit()
- if force:
- dbc.execute("UPDATE users SET tid = %s WHERE gid = %s",
- transport.tid, self._gid)
- if dbc.rowcount > 0:
- self._dbh.commit()
- dbc.close()
- self._transport = transport
-
- def get_info(self):
- """Returns a dictionary with information about the domain."""
- self._chk_state()
- dbc = self._dbh.cursor()
- dbc.execute('SELECT gid, domainname, transport, domaindir, '
- 'aliasdomains accounts, aliases, relocated FROM '
- 'vmm_domain_info WHERE gid = %s', self._gid)
- info = dbc.fetchone()
- dbc.close()
- keys = ('gid', 'domainname', 'transport', 'domaindir', 'aliasdomains',
- 'accounts', 'aliases', 'relocated')
- return dict(zip(keys, info))
-
- def get_accounts(self):
- """Returns a list with all accounts of the domain."""
- self._chk_state()
- dbc = self._dbh.cursor()
- dbc.execute('SELECT local_part from users where gid = %s ORDER BY '
- 'local_part', self._gid)
- users = dbc.fetchall()
- dbc.close()
- accounts = []
- if users:
- addr = u'@'.join
- _dom = self._name
- accounts = [addr((account[0], _dom)) for account in users]
- return accounts
-
- def get_aliases(self):
- """Returns a list with all aliases e-mail addresses of the domain."""
- self._chk_state()
- dbc = self._dbh.cursor()
- dbc.execute('SELECT DISTINCT address FROM alias WHERE gid = %s ORDER '
- 'BY address', self._gid)
- addresses = dbc.fetchall()
- dbc.close()
- aliases = []
- if addresses:
- addr = u'@'.join
- _dom = self._name
- aliases = [addr((alias[0], _dom)) for alias in addresses]
- return aliases
-
- def get_relocated(self):
- """Returns a list with all addresses of relocated users."""
- self._chk_state()
- dbc = self._dbh.cursor()
- dbc.execute('SELECT address FROM relocated WHERE gid = %s ORDER BY '
- 'address', self._gid)
- addresses = dbc.fetchall()
- dbc.close()
- relocated = []
- if addresses:
- addr = u'@'.join
- _dom = self._name
- relocated = [addr((address[0], _dom)) for address in addresses]
- return relocated
-
- def get_aliase_names(self):
- """Returns a list with all alias domain names of the domain."""
- self._chk_state()
- dbc = self._dbh.cursor()
- dbc.execute('SELECT domainname FROM domain_name WHERE gid = %s AND '
- 'NOT is_primary ORDER BY domainname', self._gid)
- anames = dbc.fetchall()
- dbc.close()
- aliasdomains = []
- if anames:
- aliasdomains = [aname[0] for aname in anames]
- return aliasdomains
-
-
-def check_domainname(domainname):
- """Returns the validated domain name `domainname`.
-
- Throws an `DomainError`, if the domain name is too long or doesn't
- look like a valid domain name (label.label.label).
-
- """
- if not RE_DOMAIN.match(domainname):
- domainname = domainname.encode('idna')
- if len(domainname) > 255:
- raise DomErr(_(u'The domain name is too long'), DOMAIN_TOO_LONG)
- if not RE_DOMAIN.match(domainname):
- raise DomErr(_(u"The domain name '%s' is invalid") % domainname,
- DOMAIN_INVALID)
- return domainname
-
-
-def get_gid(dbh, domainname):
- """Returns the group id of the domain *domainname*.
-
- If the domain couldn't be found in the database 0 will be returned.
- """
- domainname = check_domainname(domainname)
- dbc = dbh.cursor()
- dbc.execute('SELECT gid FROM domain_name WHERE domainname=%s', domainname)
- gid = dbc.fetchone()
- dbc.close()
- if gid:
- return gid[0]
- return 0
-
-
-def search(dbh, pattern=None, like=False):
- """'Search' for domains by *pattern* in the database.
-
- *pattern* may be a domain name or a partial domain name - starting
- and/or ending with a '%' sign. When the *pattern* starts or ends with
- a '%' sign *like* has to be `True` to perform a wildcard search.
- To retrieve all available domains use the arguments' default values.
-
- This function returns a tuple with a list and a dict: (order, domains).
- The order list contains the domains' gid, alphabetical sorted by the
- primary domain name. The domains dict's keys are the gids of the
- domains. The value of item is a list. The first list element contains
- the primary domain name or `None`. The elements [1:] contains the
- names of alias domains.
-
- Arguments:
-
- `pattern` : basestring
- a (partial) domain name (starting and/or ending with a "%" sign)
- `like` : bool
- should be `True` when *pattern* starts/ends with a "%" sign
- """
- if pattern and not like:
- pattern = check_domainname(pattern)
- sql = 'SELECT gid, domainname, is_primary FROM domain_name'
- if pattern:
- if like:
- sql += " WHERE domainname LIKE '%s'" % pattern
- else:
- sql += " WHERE domainname = '%s'" % pattern
- sql += ' ORDER BY is_primary DESC, domainname'
- dbc = dbh.cursor()
- dbc.execute(sql)
- result = dbc.fetchall()
- dbc.close()
-
- gids = [domain[0] for domain in result if domain[2]]
- domains = {}
- for gid, domain, is_primary in result:
- if is_primary:
- if not gid in domains:
- domains[gid] = [domain]
- else:
- domains[gid].insert(0, domain)
- else:
- if gid in gids:
- if gid in domains:
- domains[gid].append(domain)
- else:
- domains[gid] = [domain]
- else:
- gids.append(gid)
- domains[gid] = [None, domain]
- return gids, domains
-
-
-del _
--- a/VirtualMailManager/EmailAddress.py Wed Jul 28 01:03:56 2010 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,104 +0,0 @@
-# -*- coding: UTF-8 -*-
-# Copyright (c) 2008 - 2010, Pascal Volk
-# See COPYING for distribution information.
-
-"""
- VirtualMailManager.EmailAddress
-
- Virtual Mail Manager's EmailAddress class to handle e-mail addresses.
-"""
-import re
-
-from VirtualMailManager.Domain import check_domainname
-from VirtualMailManager.constants import \
- DOMAIN_NO_NAME, INVALID_ADDRESS, LOCALPART_INVALID, LOCALPART_TOO_LONG
-from VirtualMailManager.errors import EmailAddressError as EAErr
-
-
-RE_LOCALPART = re.compile(r"[^\w!#$%&'\*\+-\.\/=?^_`{\|}~]")
-_ = lambda msg: msg
-
-
-class EmailAddress(object):
- """Simple class for validated e-mail addresses."""
- __slots__ = ('_localpart', '_domainname')
-
- def __init__(self, address):
- """Creates a new instance from the string/unicode ``address``."""
- assert isinstance(address, basestring)
- self._localpart = None
- self._domainname = None
- self._chk_address(address)
-
- @property
- def localpart(self):
- """The local-part of the address *local-part@domain*"""
- return self._localpart
-
- @property
- def domainname(self):
- """The domain part of the address *local-part@domain*"""
- return self._domainname
-
- def __eq__(self, other):
- if isinstance(other, self.__class__):
- return self._localpart == other.localpart and \
- self._domainname == other.domainname
- return NotImplemented
-
- def __ne__(self, other):
- if isinstance(other, self.__class__):
- return self._localpart != other.localpart or \
- self._domainname != other.domainname
- return NotImplemented
-
- def __hash__(self):
- return hash((self._localpart.lower(), self._domainname.lower()))
-
- def __repr__(self):
- return "EmailAddress('%s@%s')" % (self._localpart, self._domainname)
-
- def __str__(self):
- return '%s@%s' % (self._localpart, self._domainname)
-
- def _chk_address(self, address):
- """Checks if the string ``address`` could be used for an e-mail
- address. If so, it will assign the corresponding values to the
- attributes `_localpart` and `_domainname`."""
- parts = address.split('@')
- p_len = len(parts)
- if p_len < 2:
- raise EAErr(_(u"Missing the '@' sign in address %r") % address,
- INVALID_ADDRESS)
- elif p_len > 2:
- raise EAErr(_(u"Too many '@' signs in address %r") % address,
- INVALID_ADDRESS)
- if not parts[0]:
- raise EAErr(_(u'Missing local-part in address %r') % address,
- LOCALPART_INVALID)
- if not parts[1]:
- raise EAErr(_(u'Missing domain name in address %r') % address,
- DOMAIN_NO_NAME)
- self._localpart = check_localpart(parts[0])
- self._domainname = check_domainname(parts[1])
-
-
-def check_localpart(localpart):
- """Returns the validated local-part `localpart`.
-
- Throws a `EmailAddressError` if the local-part is too long or contains
- invalid characters.
- """
- if len(localpart) > 64:
- raise EAErr(_(u"The local-part '%s' is too long") % localpart,
- LOCALPART_TOO_LONG)
- invalid_chars = set(RE_LOCALPART.findall(localpart))
- if invalid_chars:
- i_chars = u''.join((u'"%s" ' % c for c in invalid_chars))
- raise EAErr(_(u"The local-part '%(l_part)s' contains invalid "
- u"characters: %(i_chars)s") % {'l_part': localpart,
- 'i_chars': i_chars}, LOCALPART_INVALID)
- return localpart
-
-
-del _
--- a/VirtualMailManager/Handler.py Wed Jul 28 01:03:56 2010 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,657 +0,0 @@
-# -*- 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.
-"""
-
-import os
-import re
-
-from shutil import rmtree
-from subprocess import Popen, PIPE
-
-from pyPgSQL import PgSQL # python-pgsql - http://pypgsql.sourceforge.net
-
-from VirtualMailManager.Account import Account
-from VirtualMailManager.Alias import Alias
-from VirtualMailManager.AliasDomain import AliasDomain
-from VirtualMailManager.common import exec_ok
-from VirtualMailManager.Config import Config as Cfg
-from VirtualMailManager.constants import \
- ACCOUNT_EXISTS, ALIAS_EXISTS, CONF_NOFILE, CONF_NOPERM, CONF_WRONGPERM, \
- DATABASE_ERROR, DOMAINDIR_GROUP_MISMATCH, DOMAIN_INVALID, \
- FOUND_DOTS_IN_PATH, INVALID_ARGUMENT, MAILDIR_PERM_MISMATCH, \
- NOT_EXECUTABLE, NO_SUCH_ACCOUNT, NO_SUCH_ALIAS, NO_SUCH_BINARY, \
- NO_SUCH_DIRECTORY, NO_SUCH_RELOCATED, RELOCATED_EXISTS
-from VirtualMailManager.Domain import Domain, get_gid
-from VirtualMailManager.EmailAddress import EmailAddress
-from VirtualMailManager.errors import \
- DomainError, NotRootError, PermissionError, VMMError
-from VirtualMailManager.mailbox import new as new_mailbox
-from VirtualMailManager.pycompat import any
-from VirtualMailManager.Relocated import Relocated
-from VirtualMailManager.Transport import Transport
-
-
-_ = lambda msg: msg
-
-CFG_FILE = 'vmm.cfg'
-CFG_PATH = '/root:/usr/local/etc:/etc'
-RE_DOMAIN_SEARCH = """^[a-z0-9-\.]+$"""
-TYPE_ACCOUNT = 0x1
-TYPE_ALIAS = 0x2
-TYPE_RELOCATED = 0x4
-OTHER_TYPES = {
- TYPE_ACCOUNT: (_(u'an account'), ACCOUNT_EXISTS),
- TYPE_ALIAS: (_(u'an alias'), ALIAS_EXISTS),
- TYPE_RELOCATED: (_(u'a relocated user'), RELOCATED_EXISTS),
-}
-
-
-class Handler(object):
- """Wrapper class to simplify the access on all the stuff from
- VirtualMailManager"""
- __slots__ = ('_cfg', '_cfg_fname', '_dbh', '_warnings')
-
- def __init__(self, skip_some_checks=False):
- """Creates a new Handler instance.
-
- ``skip_some_checks`` : bool
- When a derived class knows how to handle all checks this
- argument may be ``True``. By default it is ``False`` and
- all checks will be performed.
-
- Throws a NotRootError if your uid is greater 0.
- """
- self._cfg_fname = ''
- self._warnings = []
- self._cfg = None
- self._dbh = None
-
- if os.geteuid():
- raise NotRootError(_(u"You are not root.\n\tGood bye!\n"),
- CONF_NOPERM)
- if self._check_cfg_file():
- self._cfg = Cfg(self._cfg_fname)
- self._cfg.load()
- if not skip_some_checks:
- self._cfg.check()
- self._chkenv()
-
- def _find_cfg_file(self):
- """Search the CFG_FILE in CFG_PATH.
- Raise a VMMError when no vmm.cfg could be found.
- """
- for path in CFG_PATH.split(':'):
- tmp = os.path.join(path, CFG_FILE)
- if os.path.isfile(tmp):
- self._cfg_fname = tmp
- break
- if not self._cfg_fname:
- raise VMMError(_(u"Could not find '%(cfg_file)s' in: "
- u"'%(cfg_path)s'") % {'cfg_file': CFG_FILE,
- 'cfg_path': CFG_PATH}, CONF_NOFILE)
-
- def _check_cfg_file(self):
- """Checks the configuration file, returns bool"""
- self._find_cfg_file()
- fstat = os.stat(self._cfg_fname)
- fmode = int(oct(fstat.st_mode & 0777))
- if fmode % 100 and fstat.st_uid != fstat.st_gid or \
- fmode % 10 and fstat.st_uid == fstat.st_gid:
- raise PermissionError(_(u"wrong permissions for '%(file)s': "
- u"%(perms)s\n`chmod 0600 %(file)s` would "
- u"be great.") % {'file': self._cfg_fname,
- 'perms': fmode}, CONF_WRONGPERM)
- else:
- return True
-
- def _chkenv(self):
- """Make sure our base_directory is a directory and that all
- required executables exists and are executable.
- If not, a VMMError will be raised"""
- basedir = self._cfg.dget('misc.base_directory')
- if not os.path.exists(basedir):
- old_umask = os.umask(0006)
- os.makedirs(basedir, 0771)
- os.chown(basedir, 0, 0)
- os.umask(old_umask)
- elif not os.path.isdir(basedir):
- raise VMMError(_(u"'%(path)s' is not a directory.\n(%(cfg_file)s: "
- u"section 'misc', option 'base_directory')") %
- {'path': basedir, 'cfg_file': self._cfg_fname},
- NO_SUCH_DIRECTORY)
- for opt, val in self._cfg.items('bin'):
- try:
- exec_ok(val)
- except VMMError, err:
- if err.code is NO_SUCH_BINARY:
- raise VMMError(_(u"'%(binary)s' doesn't exist.\n"
- u"(%(cfg_file)s: section 'bin', option "
- u"'%(option)s')") % {'binary': val,
- 'cfg_file': self._cfg_fname, 'option': opt},
- err.code)
- elif err.code is NOT_EXECUTABLE:
- raise VMMError(_(u"'%(binary)s' is not executable.\n"
- u"(%(cfg_file)s: section 'bin', option "
- u"'%(option)s')") % {'binary': val,
- 'cfg_file': self._cfg_fname, 'option': opt},
- err.code)
- else:
- raise
-
- def _db_connect(self):
- """Creates a pyPgSQL.PgSQL.connection instance."""
- if self._dbh is None or (isinstance(self._dbh, PgSQL.Connection) and
- not self._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()
- except PgSQL.libpq.DatabaseError, err:
- raise VMMError(str(err), DATABASE_ERROR)
-
- def _chk_other_address_types(self, address, exclude):
- """Checks if the EmailAddress *address* is known as `TYPE_ACCOUNT`,
- `TYPE_ALIAS` or `TYPE_RELOCATED`, but not as the `TYPE_*` specified
- by *exclude*. If the *address* is known as one of the `TYPE_*`s
- the according `TYPE_*` constant will be returned. Otherwise 0 will
- be returned."""
- assert exclude in (TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED) and \
- isinstance(address, EmailAddress)
- if exclude is not TYPE_ACCOUNT:
- account = Account(self._dbh, address)
- if account:
- return TYPE_ACCOUNT
- if exclude is not TYPE_ALIAS:
- alias = Alias(self._dbh, address)
- if alias:
- return TYPE_ALIAS
- if exclude is not TYPE_RELOCATED:
- relocated = Relocated(self._dbh, address)
- if relocated:
- return TYPE_RELOCATED
- return 0
-
- def _is_other_address(self, address, exclude):
- """Checks if *address* is known for an Account (TYPE_ACCOUNT),
- Alias (TYPE_ALIAS) or Relocated (TYPE_RELOCATED), except for
- *exclude*. Returns `False` if the address is not known for other
- types.
-
- Raises a `VMMError` if the address is known.
- """
- other = self._chk_other_address_types(address, exclude)
- if not other:
- return False
- msg = _(u"There is already %(a_type)s with the address '%(address)s'.")
- raise VMMError(msg % {'a_type': OTHER_TYPES[other][0],
- 'address': address}, OTHER_TYPES[other][1])
-
- def _get_account(self, address):
- """Return an Account instances for the given address (str)."""
- address = EmailAddress(address)
- self._db_connect()
- return Account(self._dbh, address)
-
- def _get_alias(self, address):
- """Return an Alias instances for the given address (str)."""
- address = EmailAddress(address)
- self._db_connect()
- return Alias(self._dbh, address)
-
- def _get_relocated(self, address):
- """Return a Relocated instances for the given address (str)."""
- address = EmailAddress(address)
- self._db_connect()
- return Relocated(self._dbh, address)
-
- def _get_domain(self, domainname):
- """Return a Domain instances for the given domain name (str)."""
- self._db_connect()
- return Domain(self._dbh, domainname)
-
- def _get_disk_usage(self, directory):
- """Estimate file space usage for the given directory.
-
- Keyword arguments:
- directory -- the directory to summarize recursively disk usage for
- """
- if self._isdir(directory):
- return Popen([self._cfg.dget('bin.du'), "-hs", directory],
- stdout=PIPE).communicate()[0].split('\t')[0]
- else:
- return 0
-
- def _isdir(self, directory):
- """Check if `directory` is a directory. Returns bool.
- When `directory` isn't a directory, a warning will be appended to
- _warnings."""
- isdir = os.path.isdir(directory)
- if not isdir:
- self._warnings.append(_('No such directory: %s') % directory)
- return isdir
-
- def _make_domain_dir(self, domain):
- """Create a directory for the `domain` and its accounts."""
- cwd = os.getcwd()
- hashdir, domdir = domain.directory.split(os.path.sep)[-2:]
- os.chdir(self._cfg.dget('misc.base_directory'))
- if not os.path.isdir(hashdir):
- os.mkdir(hashdir, 0711)
- os.chown(hashdir, 0, 0)
- os.mkdir(os.path.join(hashdir, domdir),
- self._cfg.dget('domain.directory_mode'))
- os.chown(domain.directory, 0, domain.gid)
- os.chdir(cwd)
-
- def _make_home(self, account):
- """Create a home directory for the new Account *account*."""
- os.umask(0007)
- os.chdir(account.domain_directory)
- os.mkdir('%s' % account.uid, self._cfg.dget('account.directory_mode'))
- os.chown('%s' % account.uid, account.uid, account.gid)
-
- def _delete_home(self, domdir, uid, gid):
- """Delete a user's home directory."""
- if uid > 0 and gid > 0:
- userdir = '%s' % uid
- if userdir.count('..') or domdir.count('..'):
- raise VMMError(_(u'Found ".." in home directory path.'),
- FOUND_DOTS_IN_PATH)
- if os.path.isdir(domdir):
- os.chdir(domdir)
- if os.path.isdir(userdir):
- mdstat = os.stat(userdir)
- if (mdstat.st_uid, mdstat.st_gid) != (uid, gid):
- raise VMMError(_(u'Detected owner/group mismatch in '
- u'home directory.'),
- MAILDIR_PERM_MISMATCH)
- rmtree(userdir, ignore_errors=True)
- else:
- raise VMMError(_(u"No such directory: %s") %
- os.path.join(domdir, userdir),
- NO_SUCH_DIRECTORY)
-
- def _delete_domain_dir(self, domdir, gid):
- """Delete a domain's directory."""
- if gid > 0:
- if not self._isdir(domdir):
- return
- basedir = self._cfg.dget('misc.base_directory')
- domdirdirs = domdir.replace(basedir + '/', '').split('/')
- domdirparent = os.path.join(basedir, domdirdirs[0])
- if basedir.count('..') or domdir.count('..'):
- raise VMMError(_(u'Found ".." in domain directory path.'),
- FOUND_DOTS_IN_PATH)
- if os.path.isdir(domdirparent):
- os.chdir(domdirparent)
- if os.lstat(domdirdirs[1]).st_gid != gid:
- raise VMMError(_(u'Detected group mismatch in domain '
- u'directory.'), DOMAINDIR_GROUP_MISMATCH)
- rmtree(domdirdirs[1], ignore_errors=True)
-
- def has_warnings(self):
- """Checks if warnings are present, returns bool."""
- return bool(len(self._warnings))
-
- def get_warnings(self):
- """Returns a list with all available warnings and resets all
- warnings.
- """
- ret_val = self._warnings[:]
- del self._warnings[:]
- return ret_val
-
- def cfg_dget(self, option):
- """Get the configured value of the *option* (section.option).
- When the option was not configured its default value will be
- returned."""
- return self._cfg.dget(option)
-
- def cfg_pget(self, option):
- """Get the configured value of the *option* (section.option)."""
- return self._cfg.pget(option)
-
- def cfg_install(self):
- """Installs the cfg_dget method as ``cfg_dget`` into the built-in
- namespace."""
- import __builtin__
- assert 'cfg_dget' not in __builtin__.__dict__
- __builtin__.__dict__['cfg_dget'] = self._cfg.dget
-
- def domain_add(self, domainname, transport=None):
- """Wrapper around Domain.set_transport() and Domain.save()"""
- dom = self._get_domain(domainname)
- if transport is None:
- dom.set_transport(Transport(self._dbh,
- transport=self._cfg.dget('misc.transport')))
- else:
- dom.set_transport(Transport(self._dbh, transport=transport))
- dom.set_directory(self._cfg.dget('misc.base_directory'))
- dom.save()
- self._make_domain_dir(dom)
-
- def domain_transport(self, domainname, transport, force=None):
- """Wrapper around Domain.update_transport()"""
- if force is not None and force != 'force':
- raise DomainError(_(u"Invalid argument: '%s'") % force,
- INVALID_ARGUMENT)
- dom = self._get_domain(domainname)
- trsp = Transport(self._dbh, transport=transport)
- if force is None:
- dom.update_transport(trsp)
- else:
- dom.update_transport(trsp, force=True)
-
- def domain_delete(self, domainname, force=None):
- """Wrapper around Domain.delete()"""
- if force and force not in ('deluser', 'delalias', 'delall'):
- raise DomainError(_(u"Invalid argument: '%s'") % force,
- INVALID_ARGUMENT)
- dom = self._get_domain(domainname)
- gid = dom.gid
- domdir = dom.directory
- if self._cfg.dget('domain.force_deletion') or force == 'delall':
- dom.delete(True, True)
- elif force == 'deluser':
- dom.delete(deluser=True)
- elif force == 'delalias':
- dom.delete(delalias=True)
- else:
- dom.delete()
- if self._cfg.dget('domain.delete_directory'):
- self._delete_domain_dir(domdir, gid)
-
- def domain_info(self, domainname, details=None):
- """Wrapper around Domain.get_info(), Domain.get_accounts(),
- Domain.get_aliase_names(), Domain.get_aliases() and
- Domain.get_relocated."""
- if details not in [None, 'accounts', 'aliasdomains', 'aliases', 'full',
- 'relocated']:
- raise VMMError(_(u'Invalid argument: “%s”') % details,
- INVALID_ARGUMENT)
- dom = self._get_domain(domainname)
- dominfo = dom.get_info()
- if dominfo['domainname'].startswith('xn--'):
- dominfo['domainname'] += ' (%s)' % \
- dominfo['domainname'].decode('idna')
- if details is None:
- return dominfo
- elif details == 'accounts':
- return (dominfo, dom.get_accounts())
- elif details == 'aliasdomains':
- return (dominfo, dom.get_aliase_names())
- elif details == 'aliases':
- return (dominfo, dom.get_aliases())
- elif details == 'relocated':
- return(dominfo, dom.get_relocated())
- else:
- return (dominfo, dom.get_aliase_names(), dom.get_accounts(),
- dom.get_aliases(), dom.get_relocated())
-
- def aliasdomain_add(self, aliasname, domainname):
- """Adds an alias domain to the domain.
-
- Arguments:
-
- `aliasname` : basestring
- The name of the alias domain
- `domainname` : basestring
- The name of the target domain
- """
- dom = self._get_domain(domainname)
- alias_dom = AliasDomain(self._dbh, aliasname)
- alias_dom.set_destination(dom)
- alias_dom.save()
-
- def aliasdomain_info(self, aliasname):
- """Returns a dict (keys: "alias" and "domain") with the names of
- the alias domain and its primary domain."""
- self._db_connect()
- alias_dom = AliasDomain(self._dbh, aliasname)
- return alias_dom.info()
-
- def aliasdomain_switch(self, aliasname, domainname):
- """Modifies the target domain of an existing alias domain.
-
- Arguments:
-
- `aliasname` : basestring
- The name of the alias domain
- `domainname` : basestring
- The name of the new target domain
- """
- dom = self._get_domain(domainname)
- alias_dom = AliasDomain(self._dbh, aliasname)
- alias_dom.set_destination(dom)
- alias_dom.switch()
-
- def aliasdomain_delete(self, aliasname):
- """Deletes the given alias domain.
-
- Argument:
-
- `aliasname` : basestring
- The name of the alias domain
- """
- self._db_connect()
- alias_dom = AliasDomain(self._dbh, aliasname)
- alias_dom.delete()
-
- def domain_list(self, pattern=None):
- """Wrapper around function search() from module Domain."""
- from VirtualMailManager.Domain import search
- like = False
- if pattern and (pattern.startswith('%') or pattern.endswith('%')):
- like = True
- if not re.match(RE_DOMAIN_SEARCH, pattern.strip('%')):
- raise VMMError(_(u"The pattern '%s' contains invalid "
- u"characters.") % pattern, DOMAIN_INVALID)
- self._db_connect()
- return search(self._dbh, pattern=pattern, like=like)
-
- def user_add(self, emailaddress, password):
- """Wrapper around Account.set_password() and Account.save()."""
- acc = self._get_account(emailaddress)
- acc.set_password(password)
- acc.save()
- oldpwd = os.getcwd()
- self._make_home(acc)
- mailbox = new_mailbox(acc)
- mailbox.create()
- folders = self._cfg.dget('mailbox.folders').split(':')
- if any(folders):
- bad = mailbox.add_boxes(folders,
- self._cfg.dget('mailbox.subscribe'))
- if bad:
- self._warnings.append(_(u"Skipped mailbox folders:") +
- '\n\t- ' + '\n\t- '.join(bad))
- os.chdir(oldpwd)
-
- def alias_add(self, aliasaddress, *targetaddresses):
- """Creates a new `Alias` entry for the given *aliasaddress* with
- the given *targetaddresses*."""
- alias = self._get_alias(aliasaddress)
- destinations = [EmailAddress(address) for address in targetaddresses]
- warnings = []
- destinations = alias.add_destinations(destinations, warnings)
- if warnings:
- self._warnings.append(_('Ignored destination addresses:'))
- self._warnings.extend((' * %s' % w for w in warnings))
- for destination in destinations:
- if get_gid(self._dbh, destination.domainname) and \
- not self._chk_other_address_types(destination, TYPE_RELOCATED):
- self._warnings.append(_(u"The destination account/alias '%s' "
- u"doesn't exist.") % destination)
-
- def user_delete(self, emailaddress, force=None):
- """Wrapper around Account.delete(...)"""
- if force not in (None, 'delalias'):
- raise VMMError(_(u"Invalid argument: '%s'") % force,
- INVALID_ARGUMENT)
- acc = self._get_account(emailaddress)
- if not acc:
- raise VMMError(_(u"The account '%s' doesn't exist.") %
- acc.address, NO_SUCH_ACCOUNT)
- uid = acc.uid
- gid = acc.gid
- dom_dir = acc.domain_directory
- acc_dir = acc.home
- acc.delete(bool(force))
- if self._cfg.dget('account.delete_directory'):
- try:
- self._delete_home(dom_dir, uid, gid)
- except VMMError, err:
- if err.code in (FOUND_DOTS_IN_PATH, MAILDIR_PERM_MISMATCH,
- 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_dir, 'reason': err.msg}
- self._warnings.append(warning)
- else:
- raise
-
- def alias_info(self, aliasaddress):
- """Returns an iterator object for all destinations (`EmailAddress`
- instances) for the `Alias` with the given *aliasaddress*."""
- alias = self._get_alias(aliasaddress)
- if alias:
- return alias.get_destinations()
- if not self._is_other_address(alias.address, TYPE_ALIAS):
- raise VMMError(_(u"The alias '%s' doesn't exist.") %
- alias.address, NO_SUCH_ALIAS)
-
- def alias_delete(self, aliasaddress, targetaddress=None):
- """Deletes the `Alias` *aliasaddress* with all its destinations from
- the database. If *targetaddress* is not ``None``, only this
- destination will be removed from the alias."""
- alias = self._get_alias(aliasaddress)
- if targetaddress is None:
- alias.delete()
- else:
- alias.del_destination(EmailAddress(targetaddress))
-
- def user_info(self, emailaddress, details=None):
- """Wrapper around Account.get_info(...)"""
- if details not in (None, 'du', 'aliases', 'full'):
- raise VMMError(_(u"Invalid argument: '%s'") % details,
- INVALID_ARGUMENT)
- acc = self._get_account(emailaddress)
- if not acc:
- if not self._is_other_address(acc.address, TYPE_ACCOUNT):
- raise VMMError(_(u"The account '%s' doesn't exist.") %
- acc.address, NO_SUCH_ACCOUNT)
- info = acc.get_info()
- if self._cfg.dget('account.disk_usage') or details in ('du', 'full'):
- path = os.path.join(acc.home, acc.mail_location.directory)
- info['disk usage'] = self._get_disk_usage(path)
- if details in (None, 'du'):
- return info
- if details in ('aliases', 'full'):
- return (info, acc.get_aliases())
- return info
-
- def user_by_uid(self, uid):
- """Search for an Account by its *uid*.
- Returns a dict (address, uid and gid) if a user could be found."""
- from VirtualMailManager.Account import get_account_by_uid
- self._db_connect()
- return get_account_by_uid(uid, self._dbh)
-
- def user_password(self, emailaddress, password):
- """Wrapper for Account.modify('password' ...)."""
- if not isinstance(password, basestring) or not password:
- raise VMMError(_(u"Could not accept password: '%s'") % password,
- INVALID_ARGUMENT)
- acc = self._get_account(emailaddress)
- if not acc:
- raise VMMError(_(u"The account '%s' doesn't exist.") %
- acc.address, NO_SUCH_ACCOUNT)
- acc.modify('password', password)
-
- def user_name(self, emailaddress, name):
- """Wrapper for Account.modify('name', ...)."""
- if not isinstance(name, basestring) or not name:
- raise VMMError(_(u"Could not accept name: '%s'") % name,
- INVALID_ARGUMENT)
- acc = self._get_account(emailaddress)
- if not acc:
- raise VMMError(_(u"The account '%s' doesn't exist.") %
- acc.address, NO_SUCH_ACCOUNT)
- acc.modify('name', name)
-
- def user_transport(self, emailaddress, transport):
- """Wrapper for Account.modify('transport', ...)."""
- if not isinstance(transport, basestring) or not transport:
- raise VMMError(_(u"Could not accept transport: '%s'") % transport,
- INVALID_ARGUMENT)
- acc = self._get_account(emailaddress)
- if not acc:
- raise VMMError(_(u"The account '%s' doesn't exist.") %
- acc.address, NO_SUCH_ACCOUNT)
- acc.modify('transport', transport)
-
- def user_disable(self, emailaddress, service=None):
- """Wrapper for Account.disable(service)"""
- if service not in (None, 'all', 'imap', 'pop3', 'smtp', 'sieve'):
- raise VMMError(_(u"Could not accept service: '%s'") % service,
- INVALID_ARGUMENT)
- acc = self._get_account(emailaddress)
- if not acc:
- raise VMMError(_(u"The account '%s' doesn't exist.") %
- acc.address, NO_SUCH_ACCOUNT)
- acc.disable(service)
-
- def user_enable(self, emailaddress, service=None):
- """Wrapper for Account.enable(service)"""
- if service not in (None, 'all', 'imap', 'pop3', 'smtp', 'sieve'):
- raise VMMError(_(u"Could not accept service: '%s'") % service,
- INVALID_ARGUMENT)
- acc = self._get_account(emailaddress)
- if not acc:
- raise VMMError(_(u"The account '%s' doesn't exist.") %
- acc.address, NO_SUCH_ACCOUNT)
- acc.enable(service)
-
- def relocated_add(self, emailaddress, targetaddress):
- """Creates a new `Relocated` entry in the database. If there is
- already a relocated user with the given *emailaddress*, only the
- *targetaddress* for the relocated user will be updated."""
- relocated = self._get_relocated(emailaddress)
- relocated.set_destination(EmailAddress(targetaddress))
-
- def relocated_info(self, emailaddress):
- """Returns the target address of the relocated user with the given
- *emailaddress*."""
- relocated = self._get_relocated(emailaddress)
- if relocated:
- return relocated.get_info()
- if not self._is_other_address(relocated.address, TYPE_RELOCATED):
- raise VMMError(_(u"The relocated user '%s' doesn't exist.") %
- relocated.address, NO_SUCH_RELOCATED)
-
- def relocated_delete(self, emailaddress):
- """Deletes the relocated user with the given *emailaddress* from
- the database."""
- relocated = self._get_relocated(emailaddress)
- relocated.delete()
-
-del _
--- a/VirtualMailManager/Relocated.py Wed Jul 28 01:03:56 2010 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,115 +0,0 @@
-# -*- coding: UTF-8 -*-
-# Copyright (c) 2008 - 2010, Pascal Volk
-# See COPYING for distribution information.
-
-"""
- VirtualMailManager.Relocated
-
- Virtual Mail Manager's Relocated class to handle relocated users.
-"""
-
-from VirtualMailManager.Domain import get_gid
-from VirtualMailManager.EmailAddress import EmailAddress
-from VirtualMailManager.errors import RelocatedError as RErr
-from VirtualMailManager.constants import NO_SUCH_DOMAIN, \
- NO_SUCH_RELOCATED, RELOCATED_ADDR_DEST_IDENTICAL, RELOCATED_EXISTS
-
-
-_ = lambda msg: msg
-
-
-class Relocated(object):
- """Class to handle e-mail addresses of relocated users."""
- __slots__ = ('_addr', '_dest', '_gid', '_dbh')
-
- def __init__(self, dbh, address):
- """Creates a new *Relocated* instance. The ``address`` is the
- old e-mail address of the user.
-
- Use `setDestination()` to set/update the new address, where the
- user has moved to.
-
- """
- assert isinstance(address, EmailAddress)
- self._addr = address
- self._dbh = dbh
- self._gid = get_gid(self._dbh, self._addr.domainname)
- if not self._gid:
- raise RErr(_(u"The domain %r doesn't exist.") %
- self._addr.domainname, NO_SUCH_DOMAIN)
- self._dest = None
-
- self.__load()
-
- def __nonzero__(self):
- """Returns `True` if the Relocated is known, `False` if it's new."""
- return self._dest is not None
-
- def __load(self):
- """Loads the destination address from the database into the
- `_dest` attribute.
-
- """
- dbc = self._dbh.cursor()
- dbc.execute('SELECT destination FROM relocated WHERE gid = %s AND '
- 'address = %s', self._gid, self._addr.localpart)
- destination = dbc.fetchone()
- dbc.close()
- if destination:
- self._dest = EmailAddress(destination[0])
-
- @property
- def address(self):
- """The Relocated's EmailAddress instance."""
- return self._addr
-
- def set_destination(self, destination):
- """Sets/updates the new address of the relocated user."""
- update = False
- assert isinstance(destination, EmailAddress)
- if self._addr == destination:
- raise RErr(_(u'Address and destination are identical.'),
- RELOCATED_ADDR_DEST_IDENTICAL)
- if self._dest:
- if self._dest == destination:
- raise RErr(_(u"The relocated user '%s' already exists.") %
- self._addr, RELOCATED_EXISTS)
- else:
- self._dest = destination
- update = True
- else:
- self._dest = destination
-
- dbc = self._dbh.cursor()
- if not update:
- dbc.execute('INSERT INTO relocated VALUES (%s, %s, %s)',
- self._gid, self._addr.localpart, str(self._dest))
- else:
- dbc.execute('UPDATE relocated SET destination = %s WHERE gid = %s '
- 'AND address = %s', str(self._dest), self._gid,
- self._addr.localpart)
- self._dbh.commit()
- dbc.close()
-
- def get_info(self):
- """Returns the address to which mails should be sent."""
- if not self._dest:
- raise RErr(_(u"The relocated user '%s' doesn't exist.") %
- self._addr, NO_SUCH_RELOCATED)
- return self._dest
-
- def delete(self):
- """Deletes the relocated entry from the database."""
- if not self._dest:
- raise RErr(_(u"The relocated user '%s' doesn't exist.") %
- self._addr, NO_SUCH_RELOCATED)
- dbc = self._dbh.cursor()
- dbc.execute('DELETE FROM relocated WHERE gid = %s AND address = %s',
- self._gid, self._addr.localpart)
- if dbc.rowcount > 0:
- self._dbh.commit()
- dbc.close()
- self._dest = None
-
-
-del _
--- a/VirtualMailManager/Transport.py Wed Jul 28 01:03:56 2010 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-# -*- coding: UTF-8 -*-
-# Copyright (c) 2008 - 2010, Pascal Volk
-# See COPYING for distribution information.
-
-"""
- VirtualMailManager.Transport
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
- Virtual Mail Manager's Transport class to manage the transport for
- domains and accounts.
-"""
-
-from VirtualMailManager.constants import UNKNOWN_TRANSPORT_ID
-from VirtualMailManager.errors import TransportError
-from VirtualMailManager.pycompat import any
-
-
-class Transport(object):
- """A wrapper class that provides access to the transport table"""
- __slots__ = ('_tid', '_transport', '_dbh')
-
- def __init__(self, dbh, tid=None, transport=None):
- """Creates a new Transport instance.
-
- Either tid or transport must be specified. When both arguments
- are given, tid will be used.
-
- Keyword arguments:
- dbh -- a pyPgSQL.PgSQL.connection
- tid -- the id of a transport (int/long)
- transport -- the value of the transport (str)
-
- """
- self._dbh = dbh
- assert any((tid, transport))
- if tid:
- assert not isinstance(tid, bool) and isinstance(tid, (int, long))
- self._tid = tid
- self._loadByID()
- else:
- assert isinstance(transport, basestring)
- self._transport = transport
- self._loadByName()
-
- @property
- def tid(self):
- """The transport's unique ID."""
- return self._tid
-
- @property
- def transport(self):
- """The transport's value, ex: 'dovecot:'"""
- return self._transport
-
- def __eq__(self, other):
- if isinstance(other, self.__class__):
- return self._tid == other.tid
- return NotImplemented
-
- def __ne__(self, other):
- if isinstance(other, self.__class__):
- return self._tid != other.tid
- return NotImplemented
-
- def __str__(self):
- return self._transport
-
- def _loadByID(self):
- dbc = self._dbh.cursor()
- dbc.execute('SELECT transport FROM transport WHERE tid=%s', self._tid)
- result = dbc.fetchone()
- dbc.close()
- if result:
- self._transport = result[0]
- else:
- raise TransportError(_(u'Unknown tid specified.'),
- UNKNOWN_TRANSPORT_ID)
-
- def _loadByName(self):
- dbc = self._dbh.cursor()
- dbc.execute('SELECT tid FROM transport WHERE transport = %s',
- self._transport)
- result = dbc.fetchone()
- dbc.close()
- if result:
- self._tid = result[0]
- else:
- self._save()
-
- def _save(self):
- dbc = self._dbh.cursor()
- dbc.execute("SELECT nextval('transport_id')")
- self._tid = dbc.fetchone()[0]
- dbc.execute('INSERT INTO transport VALUES (%s, %s)', self._tid,
- self._transport)
- self._dbh.commit()
- dbc.close()
--- a/VirtualMailManager/__init__.py Wed Jul 28 01:03:56 2010 +0000
+++ b/VirtualMailManager/__init__.py Wed Jul 28 02:08:03 2010 +0000
@@ -1,9 +1,9 @@
# -*- coding: UTF-8 -*-
# Copyright (c) 2007 - 2010, Pascal Volk
# See COPYING for distribution information.
-
"""
VirtualMailManager
+ ~~~~~~~~~~~~~~~~~~
VirtualMailManager package initialization code
"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/account.py Wed Jul 28 02:08:03 2010 +0000
@@ -0,0 +1,420 @@
+# -*- coding: UTF-8 -*-
+# Copyright (c) 2007 - 2010, Pascal Volk
+# See COPYING for distribution information.
+"""
+ VirtualMailManager.account
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Virtual Mail Manager's Account class to manage e-mail accounts.
+"""
+
+from VirtualMailManager.domain import Domain
+from VirtualMailManager.emailaddress import EmailAddress
+from VirtualMailManager.transport import Transport
+from VirtualMailManager.common import version_str
+from VirtualMailManager.constants import \
+ ACCOUNT_EXISTS, ACCOUNT_MISSING_PASSWORD, ALIAS_PRESENT, \
+ INVALID_ARGUMENT, INVALID_MAIL_LOCATION, NO_SUCH_ACCOUNT, \
+ NO_SUCH_DOMAIN, UNKNOWN_SERVICE
+from VirtualMailManager.errors import AccountError as AErr
+from VirtualMailManager.maillocation import MailLocation
+from VirtualMailManager.password import pwhash
+
+
+_ = lambda msg: msg
+cfg_dget = lambda option: None
+
+
+class Account(object):
+ """Class to manage e-mail accounts."""
+ __slots__ = ('_addr', '_dbh', '_domain', '_mail', '_new', '_passwd',
+ '_transport', '_uid')
+
+ def __init__(self, dbh, address):
+ """Creates a new Account instance.
+
+ When an account with the given *address* could be found in the
+ database all relevant data will be loaded.
+
+ Arguments:
+
+ `dbh` : pyPgSQL.PgSQL.Connection
+ A database connection for the database access.
+ `address` : VirtualMailManager.EmailAddress.EmailAddress
+ The e-mail address of the (new) Account.
+ """
+ if not isinstance(address, EmailAddress):
+ raise TypeError("Argument 'address' is not an EmailAddress")
+ self._addr = address
+ self._dbh = dbh
+ self._domain = Domain(self._dbh, self._addr.domainname)
+ if not self._domain.gid:
+ raise AErr(_(u"The domain '%s' doesn't exist.") %
+ self._addr.domainname, NO_SUCH_DOMAIN)
+ self._uid = 0
+ self._mail = None
+ self._transport = self._domain.transport
+ self._passwd = None
+ self._new = True
+ self._load()
+
+ def __nonzero__(self):
+ """Returns `True` if the Account is known, `False` if it's new."""
+ return not self._new
+
+ def _load(self):
+ """Load 'uid', 'mid' and 'tid' from the database and set _new to
+ `False` - if the user could be found. """
+ dbc = self._dbh.cursor()
+ dbc.execute('SELECT uid, mid, tid FROM users WHERE gid = %s AND '
+ 'local_part = %s', self._domain.gid, self._addr.localpart)
+ result = dbc.fetchone()
+ dbc.close()
+ if result:
+ self._uid, _mid, _tid = result
+ if _tid != self._transport.tid:
+ self._transport = Transport(self._dbh, tid=_tid)
+ self._mail = MailLocation(self._dbh, mid=_mid)
+ self._new = False
+
+ def _set_uid(self):
+ """Set the unique ID for the new Account."""
+ assert self._uid == 0
+ dbc = self._dbh.cursor()
+ dbc.execute("SELECT nextval('users_uid')")
+ self._uid = dbc.fetchone()[0]
+ dbc.close()
+
+ def _prepare(self, maillocation):
+ """Check and set different attributes - before we store the
+ information in the database.
+ """
+ if maillocation.dovecot_version > cfg_dget('misc.dovecot_version'):
+ raise AErr(_(u"The mailbox format '%(mbfmt)s' requires Dovecot "
+ u">= v%(version)s") % {'mbfmt': maillocation.mbformat,
+ 'version': version_str(maillocation.dovecot_version)},
+ INVALID_MAIL_LOCATION)
+ if not maillocation.postfix and \
+ self._transport.transport.lower() in ('virtual:', 'virtual'):
+ raise AErr(_(u"Invalid transport '%(transport)s' for mailbox "
+ u"format '%(mbfmt)s'") %
+ {'transport': self._transport,
+ 'mbfmt': maillocation.mbformat}, INVALID_MAIL_LOCATION)
+ self._mail = maillocation
+ self._set_uid()
+
+ def _switch_state(self, state, service):
+ """Switch the state of the Account's services on or off. See
+ Account.enable()/Account.disable() for more information."""
+ self._chk_state()
+ if service not in (None, 'all', 'imap', 'pop3', 'sieve', 'smtp'):
+ raise AErr(_(u"Unknown service: '%s'.") % service, UNKNOWN_SERVICE)
+ if cfg_dget('misc.dovecot_version') >= 0x10200b02:
+ sieve_col = 'sieve'
+ else:
+ sieve_col = 'managesieve'
+ if service in ('smtp', 'pop3', 'imap'):
+ sql = 'UPDATE users SET %s = %s WHERE uid = %d' % (service, state,
+ self._uid)
+ elif service == 'sieve':
+ sql = 'UPDATE users SET %s = %s WHERE uid = %d' % (sieve_col,
+ state,
+ self._uid)
+ else:
+ sql = 'UPDATE users SET smtp = %(s)s, pop3 = %(s)s, imap = %(s)s,\
+ %(col)s = %(s)s WHERE uid = %(uid)d' % \
+ {'s': state, 'col': sieve_col, 'uid': self._uid}
+ dbc = self._dbh.cursor()
+ dbc.execute(sql)
+ if dbc.rowcount > 0:
+ self._dbh.commit()
+ dbc.close()
+
+ def _count_aliases(self):
+ """Count all alias addresses where the destination address is the
+ address of the Account."""
+ dbc = self._dbh.cursor()
+ sql = "SELECT COUNT(destination) FROM alias WHERE destination = '%s'"\
+ % self._addr
+ dbc.execute(sql)
+ a_count = dbc.fetchone()[0]
+ dbc.close()
+ return a_count
+
+ def _chk_state(self):
+ """Raise an AccountError if the Account is new - not yet saved in the
+ database."""
+ if self._new:
+ raise AErr(_(u"The account '%s' doesn't exist.") % self._addr,
+ NO_SUCH_ACCOUNT)
+
+ @property
+ def address(self):
+ """The Account's EmailAddress instance."""
+ return self._addr
+
+ @property
+ def domain_directory(self):
+ """The directory of the domain the Account belongs to."""
+ if self._domain:
+ return self._domain.directory
+ return None
+
+ @property
+ def gid(self):
+ """The Account's group ID."""
+ if self._domain:
+ return self._domain.gid
+ return None
+
+ @property
+ def home(self):
+ """The Account's home directory."""
+ if not self._new:
+ return '%s/%s' % (self._domain.directory, self._uid)
+ return None
+
+ @property
+ def mail_location(self):
+ """The Account's MailLocation."""
+ return self._mail
+
+ @property
+ def uid(self):
+ """The Account's unique ID."""
+ return self._uid
+
+ def set_password(self, password):
+ """Set a password for the new Account.
+
+ If you want to update the password of an existing Account use
+ Account.modify().
+
+ Argument:
+
+ `password` : basestring
+ The password for the new Account.
+ """
+ if not isinstance(password, basestring) or not password:
+ raise AErr(_(u"Couldn't accept password: '%s'") % password,
+ ACCOUNT_MISSING_PASSWORD)
+ self._passwd = password
+
+ def set_transport(self, transport):
+ """Set the transport for the new Account.
+
+ If you want to update the transport of an existing Account use
+ Account.modify().
+
+ Argument:
+
+ `transport` : basestring
+ The string representation of the transport, e.g.: 'dovecot:'
+ """
+ self._transport = Transport(self._dbh, transport=transport)
+
+ def enable(self, service=None):
+ """Enable a/all service/s for the Account.
+
+ Possible values for the *service* are: 'imap', 'pop3', 'sieve' and
+ 'smtp'. When all services should be enabled, use 'all' or the
+ default value `None`.
+
+ Arguments:
+
+ `service` : basestring
+ The name of a service ('imap', 'pop3', 'smtp', 'sieve'), 'all'
+ or `None`.
+ """
+ self._switch_state(True, service)
+
+ def disable(self, service=None):
+ """Disable a/all service/s for the Account.
+
+ For more information see: Account.enable()."""
+ self._switch_state(False, service)
+
+ def save(self):
+ """Save the new Account in the database."""
+ if not self._new:
+ raise AErr(_(u"The account '%s' already exists.") % self._addr,
+ ACCOUNT_EXISTS)
+ if not self._passwd:
+ raise AErr(_(u"No password set for '%s'.") % self._addr,
+ ACCOUNT_MISSING_PASSWORD)
+ if cfg_dget('misc.dovecot_version') >= 0x10200b02:
+ sieve_col = 'sieve'
+ else:
+ sieve_col = 'managesieve'
+ self._prepare(MailLocation(self._dbh, mbfmt=cfg_dget('mailbox.format'),
+ directory=cfg_dget('mailbox.root')))
+ sql = "INSERT INTO users (local_part, passwd, uid, gid, mid, tid,\
+ smtp, pop3, imap, %s) VALUES ('%s', '%s', %d, %d, %d, %d, %s, %s, %s, %s)" % (
+ sieve_col, self._addr.localpart, pwhash(self._passwd,
+ user=self._addr),
+ self._uid, self._domain.gid, self._mail.mid, self._transport.tid,
+ cfg_dget('account.smtp'), cfg_dget('account.pop3'),
+ cfg_dget('account.imap'), cfg_dget('account.sieve'))
+ dbc = self._dbh.cursor()
+ dbc.execute(sql)
+ self._dbh.commit()
+ dbc.close()
+ self._new = False
+
+ def modify(self, field, value):
+ """Update the Account's *field* to the new *value*.
+
+ Possible values for *field* are: 'name', 'password' and
+ 'transport'. *value* is the *field*'s new value.
+
+ Arguments:
+
+ `field` : basestring
+ The attribute name: 'name', 'password' or 'transport'
+ `value` : basestring
+ The new value of the attribute.
+ """
+ if field not in ('name', 'password', 'transport'):
+ raise AErr(_(u"Unknown field: '%s'") % field, INVALID_ARGUMENT)
+ self._chk_state()
+ dbc = self._dbh.cursor()
+ if field == 'password':
+ dbc.execute('UPDATE users SET passwd = %s WHERE uid = %s',
+ pwhash(value, user=self._addr), self._uid)
+ elif field == 'transport':
+ if value != self._transport.transport:
+ self._transport = Transport(self._dbh, transport=value)
+ dbc.execute('UPDATE users SET tid = %s WHERE uid = %s',
+ self._transport.tid, self._uid)
+ else:
+ dbc.execute('UPDATE users SET name = %s WHERE uid = %s',
+ value, self._uid)
+ if dbc.rowcount > 0:
+ self._dbh.commit()
+ dbc.close()
+
+ def get_info(self):
+ """Returns a dict with some information about the Account.
+
+ The keys of the dict are: 'address', 'gid', 'home', 'imap'
+ 'mail_location', 'name', 'pop3', 'sieve', 'smtp', transport' and
+ 'uid'.
+ """
+ self._chk_state()
+ if cfg_dget('misc.dovecot_version') >= 0x10200b02:
+ sieve_col = 'sieve'
+ else:
+ sieve_col = 'managesieve'
+ sql = 'SELECT name, smtp, pop3, imap, %s FROM users WHERE uid = %d' % \
+ (sieve_col, self._uid)
+ dbc = self._dbh.cursor()
+ dbc.execute(sql)
+ info = dbc.fetchone()
+ dbc.close()
+ if info:
+ keys = ('name', 'smtp', 'pop3', 'imap', sieve_col)
+ info = dict(zip(keys, info))
+ for service in keys[1:]:
+ if info[service]:
+ # TP: A service (pop3/imap) is enabled/usable for a user
+ info[service] = _('enabled')
+ else:
+ # TP: A service (pop3/imap) isn't enabled/usable for a user
+ info[service] = _('disabled')
+ info['address'] = self._addr
+ info['gid'] = self._domain.gid
+ info['home'] = '%s/%s' % (self._domain.directory, self._uid)
+ info['mail_location'] = self._mail.mail_location
+ info['transport'] = self._transport.transport
+ info['uid'] = self._uid
+ return info
+ # nearly impossible‽
+ raise AErr(_(u"Couldn't fetch information for account: '%s'") %
+ self._addr, NO_SUCH_ACCOUNT)
+
+ def get_aliases(self):
+ """Return a list with all alias e-mail addresses, whose destination
+ is the address of the Account."""
+ self._chk_state()
+ dbc = self._dbh.cursor()
+ dbc.execute("SELECT address ||'@'|| domainname FROM alias, "
+ "domain_name WHERE destination = %s AND domain_name.gid = "
+ "alias.gid AND domain_name.is_primary ORDER BY address",
+ str(self._addr))
+ addresses = dbc.fetchall()
+ dbc.close()
+ aliases = []
+ if addresses:
+ aliases = [alias[0] for alias in addresses]
+ return aliases
+
+ def delete(self, delalias=False):
+ """Delete the Account from the database.
+
+ Argument:
+
+ `delalias` : bool
+ if *delalias* is `True`, all aliases, which points to the Account,
+ will be also deleted. If there are aliases and *delalias* is
+ `False`, an AccountError will be raised.
+ """
+ assert isinstance(delalias, bool)
+ self._chk_state()
+ dbc = self._dbh.cursor()
+ if delalias:
+ dbc.execute('DELETE FROM users WHERE uid = %s', self._uid)
+ # delete also all aliases where the destination address is the same
+ # as for this account.
+ dbc.execute("DELETE FROM alias WHERE destination = %s",
+ str(self._addr))
+ self._dbh.commit()
+ else: # check first for aliases
+ a_count = self._count_aliases()
+ if a_count > 0:
+ dbc.close()
+ raise AErr(_(u"There are %(count)d aliases with the "
+ u"destination address '%(address)s'.") %
+ {'count': a_count, 'address': self._addr},
+ ALIAS_PRESENT)
+ dbc.execute('DELETE FROM users WHERE uid = %s', self._uid)
+ self._dbh.commit()
+ dbc.close()
+ self._new = True
+ self._uid = 0
+ self._addr = self._dbh = self._domain = self._passwd = None
+ self._mail = self._transport = None
+
+
+def get_account_by_uid(uid, dbh):
+ """Search an Account by its UID.
+
+ This function returns a dict (keys: 'address', 'gid' and 'uid'), if an
+ Account with the given *uid* exists.
+
+ Argument:
+
+ `uid` : long
+ The Account unique ID.
+ `dbh` : pyPgSQL.PgSQL.Connection
+ a database connection for the database access.
+ """
+ try:
+ uid = long(uid)
+ except ValueError:
+ raise AErr(_(u'UID must be an int/long.'), INVALID_ARGUMENT)
+ if uid < 1:
+ raise AErr(_(u'UID must be greater than 0.'), INVALID_ARGUMENT)
+ dbc = dbh.cursor()
+ dbc.execute("SELECT local_part||'@'|| domain_name.domainname AS address, "
+ "uid, users.gid FROM users LEFT JOIN domain_name ON "
+ "(domain_name.gid = users.gid AND is_primary) WHERE uid = %s",
+ uid)
+ info = dbc.fetchone()
+ dbc.close()
+ if not info:
+ raise AErr(_(u"There is no account with the UID '%d'.") % uid,
+ NO_SUCH_ACCOUNT)
+ info = dict(zip(('address', 'uid', 'gid'), info))
+ return info
+
+del _, cfg_dget
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/alias.py Wed Jul 28 02:08:03 2010 +0000
@@ -0,0 +1,164 @@
+# -*- coding: UTF-8 -*-
+# Copyright (c) 2007 - 2010, Pascal Volk
+# See COPYING for distribution information.
+"""
+ VirtualMailManager.alias
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Virtual Mail Manager's Alias class to manage e-mail aliases.
+"""
+
+from VirtualMailManager.domain import get_gid
+from VirtualMailManager.emailaddress import EmailAddress
+from VirtualMailManager.errors import AliasError as AErr
+from VirtualMailManager.ext.postconf import Postconf
+from VirtualMailManager.pycompat import all
+from VirtualMailManager.constants import \
+ ALIAS_EXCEEDS_EXPANSION_LIMIT, NO_SUCH_ALIAS, NO_SUCH_DOMAIN
+
+
+_ = lambda msg: msg
+cfg_dget = lambda option: None
+
+
+class Alias(object):
+ """Class to manage e-mail aliases."""
+ __slots__ = ('_addr', '_dests', '_gid', '_dbh')
+
+ def __init__(self, dbh, address):
+ assert isinstance(address, EmailAddress)
+ self._addr = address
+ self._dbh = dbh
+ self._gid = get_gid(self._dbh, self._addr.domainname)
+ if not self._gid:
+ raise AErr(_(u"The domain '%s' doesn't exist.") %
+ self._addr.domainname, NO_SUCH_DOMAIN)
+ self._dests = []
+
+ self.__load_dests()
+
+ def __load_dests(self):
+ """Loads all known destination addresses into the _dests list."""
+ dbc = self._dbh.cursor()
+ dbc.execute('SELECT destination FROM alias WHERE gid = %s AND '
+ 'address = %s', self._gid, self._addr.localpart)
+ dests = dbc.fetchall()
+ if dbc.rowcount > 0:
+ self._dests.extend(EmailAddress(dest[0]) for dest in dests)
+ dbc.close()
+
+ def __check_expansion(self, count_new):
+ """Checks the current expansion limit of the alias."""
+ postconf = Postconf(cfg_dget('bin.postconf'))
+ limit = long(postconf.read('virtual_alias_expansion_limit'))
+ dcount = len(self._dests)
+ failed = False
+ if dcount == limit or dcount + count_new > limit:
+ failed = True
+ errmsg = _(
+u"""Can't add %(count_new)i new destination(s) to alias '%(address)s'.
+Currently this alias expands into %(count)i/%(limit)i recipients.
+%(count_new)i additional destination(s) will render this alias unusable.
+Hint: Increase Postfix' virtual_alias_expansion_limit""")
+ elif dcount > limit:
+ failed = True
+ errmsg = _(
+u"""Can't add %(count_new)i new destination(s) to alias '%(address)s'.
+This alias already exceeds its expansion limit (%(count)i/%(limit)i).
+So its unusable, all messages addressed to this alias will be bounced.
+Hint: Delete some destination addresses.""")
+ if failed:
+ raise AErr(errmsg % {'address': self._addr, 'count': dcount,
+ 'limit': limit, 'count_new': count_new},
+ ALIAS_EXCEEDS_EXPANSION_LIMIT)
+
+ def __delete(self, destination=None):
+ """Deletes a destination from the alias, if ``destination`` is
+ not ``None``. If ``destination`` is None, the alias with all
+ its destination addresses will be deleted.
+
+ """
+ dbc = self._dbh.cursor()
+ if not destination:
+ dbc.execute('DELETE FROM alias WHERE gid = %s AND address = %s',
+ self._gid, self._addr.localpart)
+ else:
+ dbc.execute('DELETE FROM alias WHERE gid = %s AND address = %s '
+ 'AND destination = %s', self._gid,
+ self._addr.localpart, str(destination))
+ if dbc.rowcount > 0:
+ self._dbh.commit()
+ dbc.close()
+
+ def __len__(self):
+ """Returns the number of destinations of the alias."""
+ return len(self._dests)
+
+ @property
+ def address(self):
+ """The Alias' EmailAddress instance."""
+ return self._addr
+
+ def add_destinations(self, destinations, warnings=None):
+ """Adds the `EmailAddress`es from *destinations* list to the
+ destinations of the alias.
+
+ Destinations, that are already assigned to the alias, will be
+ removed from *destinations*. When done, this method will return
+ a set with all destinations, that were saved in the database.
+ """
+ destinations = set(destinations)
+ assert destinations and \
+ all(isinstance(dest, EmailAddress) for dest in destinations)
+ if not warnings is None:
+ assert isinstance(warnings, list)
+ if self._addr in destinations:
+ destinations.remove(self._addr)
+ if not warnings is None:
+ warnings.append(self._addr)
+ duplicates = destinations.intersection(set(self._dests))
+ if duplicates:
+ destinations.difference_update(set(self._dests))
+ if not warnings is None:
+ warnings.extend(duplicates)
+ if not destinations:
+ return destinations
+ self.__check_expansion(len(destinations))
+ dbc = self._dbh.cursor()
+ dbc.executemany("INSERT INTO alias VALUES (%d, '%s', %%s)" %
+ (self._gid, self._addr.localpart),
+ (str(destination) for destination in destinations))
+ self._dbh.commit()
+ dbc.close()
+ self._dests.extend(destinations)
+ return destinations
+
+ def del_destination(self, destination):
+ """Deletes the specified ``destination`` address from the alias."""
+ assert isinstance(destination, EmailAddress)
+ if not self._dests:
+ raise AErr(_(u"The alias '%s' doesn't exist.") % self._addr,
+ NO_SUCH_ALIAS)
+ if not destination in self._dests:
+ raise AErr(_(u"The address '%(addr)s' isn't a destination of "
+ u"the alias '%(alias)s'.") % {'addr': self._addr,
+ 'alias': destination}, NO_SUCH_ALIAS)
+ self.__delete(destination)
+ self._dests.remove(destination)
+
+ def get_destinations(self):
+ """Returns an iterator for all destinations of the alias."""
+ if not self._dests:
+ raise AErr(_(u"The alias '%s' doesn't exist.") % self._addr,
+ NO_SUCH_ALIAS)
+ return iter(self._dests)
+
+ def delete(self):
+ """Deletes the alias with all its destinations."""
+ if not self._dests:
+ raise AErr(_(u"The alias '%s' doesn't exist.") % self._addr,
+ NO_SUCH_ALIAS)
+ self.__delete()
+ del self._dests[:]
+
+del _, cfg_dget
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/aliasdomain.py Wed Jul 28 02:08:03 2010 +0000
@@ -0,0 +1,143 @@
+# -*- coding: UTF-8 -*-
+# Copyright (c) 2008 - 2010, Pascal Volk
+# See COPYING for distribution information.
+"""
+ VirtualMailManager.aliasdomain
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Virtual Mail Manager's AliasDomain class to manage alias domains.
+"""
+
+from VirtualMailManager.domain import Domain, check_domainname
+from VirtualMailManager.constants import \
+ ALIASDOMAIN_EXISTS, ALIASDOMAIN_ISDOMAIN, ALIASDOMAIN_NO_DOMDEST, \
+ NO_SUCH_ALIASDOMAIN, NO_SUCH_DOMAIN
+from VirtualMailManager.errors import AliasDomainError as ADErr
+
+
+_ = lambda msg: msg
+
+
+class AliasDomain(object):
+ """Class to manage e-mail alias domains."""
+ __slots__ = ('_gid', '_name', '_domain', '_dbh')
+
+ def __init__(self, dbh, domainname):
+ """Creates a new AliasDomain instance.
+
+ Arguments:
+
+ `dbh` : pyPgSQL.PgSQL.Connection
+ a database connection for the database access
+ `domainname` : basestring
+ the name of the AliasDomain"""
+ self._dbh = dbh
+ self._name = check_domainname(domainname)
+ self._gid = 0
+ self._domain = None
+ self._load()
+
+ def _load(self):
+ """Loads the AliasDomain's GID from the database and checks if the
+ domain name is marked as primary."""
+ dbc = self._dbh.cursor()
+ dbc.execute('SELECT gid, is_primary FROM domain_name WHERE '
+ 'domainname = %s', self._name)
+ result = dbc.fetchone()
+ dbc.close()
+ if result:
+ if result[1]:
+ raise ADErr(_(u"The domain '%s' is a primary domain.") %
+ self._name, ALIASDOMAIN_ISDOMAIN)
+ self._gid = result[0]
+
+ def set_destination(self, dest_domain):
+ """Set the destination of a new AliasDomain or updates the
+ destination of an existing AliasDomain.
+
+ Argument:
+
+ `dest_domain` : VirtualMailManager.Domain.Domain
+ the AliasDomain's destination domain
+ """
+ assert isinstance(dest_domain, Domain)
+ self._domain = dest_domain
+
+ def save(self):
+ """Stores information about the new AliasDomain in the database."""
+ if self._gid > 0:
+ raise ADErr(_(u"The alias domain '%s' already exists.") %
+ self._name, ALIASDOMAIN_EXISTS)
+ if not self._domain:
+ raise ADErr(_(u'No destination domain set for the alias domain.'),
+ ALIASDOMAIN_NO_DOMDEST)
+ if self._domain.gid < 1:
+ raise ADErr(_(u"The target domain '%s' doesn't exist.") %
+ self._domain.name, NO_SUCH_DOMAIN)
+ dbc = self._dbh.cursor()
+ dbc.execute('INSERT INTO domain_name VALUES (%s, %s, FALSE)',
+ self._name, self._domain.gid)
+ self._dbh.commit()
+ dbc.close()
+ self._gid = self._domain.gid
+
+ def info(self):
+ """Returns a dict (keys: "alias" and "domain") with the names of the
+ AliasDomain and its primary domain."""
+ if self._gid < 1:
+ raise ADErr(_(u"The alias domain '%s' doesn't exist.") %
+ self._name, NO_SUCH_ALIASDOMAIN)
+ dbc = self._dbh.cursor()
+ dbc.execute('SELECT domainname FROM domain_name WHERE gid = %s AND '
+ 'is_primary', self._gid)
+ domain = dbc.fetchone()
+ dbc.close()
+ if domain:
+ return {'alias': self._name, 'domain': domain[0]}
+ else: # an almost unlikely case, isn't it?
+ raise ADErr(_(u'There is no primary domain for the alias domain '
+ u"'%s'.") % self._name, NO_SUCH_DOMAIN)
+
+ def switch(self):
+ """Switch the destination of the AliasDomain to the new destination,
+ set with the method `set_destination()`.
+ """
+ if not self._domain:
+ raise ADErr(_(u'No destination domain set for the alias domain.'),
+ ALIASDOMAIN_NO_DOMDEST)
+ if self._domain.gid < 1:
+ raise ADErr(_(u"The target domain '%s' doesn't exist.") %
+ self._domain.name, NO_SUCH_DOMAIN)
+ if self._gid < 1:
+ raise ADErr(_(u"The alias domain '%s' doesn't exist.") %
+ self._name, NO_SUCH_ALIASDOMAIN)
+ if self._gid == self._domain.gid:
+ raise ADErr(_(u"The alias domain '%(alias)s' is already assigned "
+ u"to the domain '%(domain)s'.") %
+ {'alias': self._name, 'domain': self._domain.name},
+ ALIASDOMAIN_EXISTS)
+ dbc = self._dbh.cursor()
+ dbc.execute('UPDATE domain_name SET gid = %s WHERE gid = %s AND '
+ 'domainname = %s AND NOT is_primary', self._domain.gid,
+ self._gid, self._name)
+ self._dbh.commit()
+ dbc.close()
+ self._gid = self._domain.gid
+
+ def delete(self):
+ """Delete the AliasDomain's record form the database.
+
+ Raises an AliasDomainError if the AliasDomain doesn't exist.
+ """
+ if self._gid < 1:
+ raise ADErr(_(u"The alias domain '%s' doesn't exist.") %
+ self._name, NO_SUCH_ALIASDOMAIN)
+ dbc = self._dbh.cursor()
+ dbc.execute('DELETE FROM domain_name WHERE domainname = %s AND NOT '
+ 'is_primary', self._name)
+ if dbc.rowcount > 0:
+ self._dbh.commit()
+ self._gid = 0
+ dbc.close()
+
+del _
--- a/VirtualMailManager/cli/Config.py Wed Jul 28 01:03:56 2010 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,94 +0,0 @@
-# -*- coding: UTF-8 -*-
-# Copyright (c) 2010, Pascal Volk
-# See COPYING for distribution information.
-
-"""
- VirtualMailManager.cli.CliConfig
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
- Adds some interactive stuff to the Config class.
-"""
-
-from ConfigParser import RawConfigParser
-from shutil import copy2
-
-from VirtualMailManager import ENCODING
-from VirtualMailManager.Config import Config, ConfigValueError, LazyConfig
-from VirtualMailManager.errors import ConfigError
-from VirtualMailManager.cli import w_err, w_std
-from VirtualMailManager.constants import VMM_TOO_MANY_FAILURES
-
-_ = lambda msg: msg
-
-
-class CliConfig(Config):
- """Adds the interactive ``configure`` method to the `Config` class
- and overwrites `LazyConfig.set(), in order to update a single option
- in the configuration file with a single command line command.
- """
-
- def configure(self, sections):
- """Interactive method for configuring all options of the given
- iterable ``sections`` object."""
- input_fmt = _(u'Enter new value for option %(option)s '
- u'[%(current_value)s]: ')
- failures = 0
-
- w_std(_(u'Using configuration file: %s\n') % self._cfg_filename)
- for section in sections:
- w_std(_(u'* Configuration section: %r') % section)
- for opt, val in self.items(section):
- failures = 0
- while True:
- newval = raw_input(input_fmt.encode(ENCODING, 'replace') %
- {'option': opt, 'current_value': val})
- if newval and newval != val:
- try:
- LazyConfig.set(self, '%s.%s' % (section, opt),
- newval)
- break
- except (ValueError, ConfigValueError), err:
- w_err(0, _(u'Warning: %s') % err)
- failures += 1
- if failures > 2:
- raise ConfigError(_(u'Too many failures - try '
- u'again later.'),
- VMM_TOO_MANY_FAILURES)
- else:
- break
- print
- if self._modified:
- self.__save_changes()
-
- def set(self, option, value):
- """Set the value of an option.
-
- If the new `value` has been set, the configuration file will be
- immediately updated.
-
- Throws a ``ValueError`` if `value` couldn't be converted to
- ``LazyConfigOption.cls``"""
- section, option_ = self._get_section_option(option)
- val = self._cfg[section][option_].cls(value)
- if self._cfg[section][option_].validate:
- val = self._cfg[section][option_].validate(val)
- # Do not write default values also skip identical values
- if not self._cfg[section][option_].default is None:
- old_val = self.dget(option)
- else:
- old_val = self.pget(option)
- if val == old_val:
- return
- if not RawConfigParser.has_section(self, section):
- self.add_section(section)
- RawConfigParser.set(self, section, option_, val)
- self.__save_changes()
-
- def __save_changes(self):
- """Writes changes to the configuration file."""
- copy2(self._cfg_filename, self._cfg_filename + '.bak')
- self._cfg_file = open(self._cfg_filename, 'w')
- self.write(self._cfg_file)
- self._cfg_file.close()
-
-del _
--- a/VirtualMailManager/cli/Handler.py Wed Jul 28 01:03:56 2010 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-# -*- coding: UTF-8 -*-
-# Copyright (c) 2010, Pascal Volk
-# See COPYING for distribution information.
-
-"""
- VirtualMailManager.cli.Handler
-
- A derived Handler class with a few changes/additions for cli use.
-"""
-
-import os
-
-from VirtualMailManager.errors import VMMError
-from VirtualMailManager.Handler import Handler
-from VirtualMailManager.cli import read_pass
-from VirtualMailManager.cli.Config import CliConfig as Cfg
-from VirtualMailManager.constants import INVALID_SECTION
-
-_ = lambda msg: msg
-
-
-class CliHandler(Handler):
- """This class uses a `CliConfig` for configuration stuff, instead of
- the non-interactive `Config` class.
-
- It provides the additional methods cfgSet() and configure().
-
- Additionally it uses `VirtualMailManager.cli.read_pass()` for for the
- interactive password dialog.
- """
-
- __slots__ = () # nothing additional, also no __dict__/__weakref__
-
- def __init__(self):
- """Creates a new CliHandler instance.
-
- Throws a NotRootError if your uid is greater 0.
- """
- # Overwrite the parent CTor partly, we use the CliConfig class
- # and add some command line checks.
- skip_some_checks = os.sys.argv[1] in ('cf', 'configure', 'h', 'help',
- 'v', 'version')
- super(CliHandler, self).__init__(skip_some_checks)
-
- self._cfg = Cfg(self._cfg_fname)
- self._cfg.load()
- if not skip_some_checks:
- self._cfg.check()
- self._chkenv()
-
- def cfg_set(self, option, value):
- """Set a new value for the given option."""
- return self._cfg.set(option, value)
-
- def configure(self, section=None):
- """Starts the 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.
- """
- if section is None:
- self._cfg.configure(self._cfg.sections())
- elif self._cfg.has_section(section):
- self._cfg.configure([section])
- else:
- raise VMMError(_(u'Invalid section: “%s”') % section,
- INVALID_SECTION)
-
- def user_add(self, emailaddress, password=None):
- """Prefix the parent user_add() with the interactive password
- dialog."""
- if password is None:
- password = read_pass()
- super(CliHandler, self).user_add(emailaddress, password)
-
- def user_password(self, emailaddress, password=None):
- """Prefix the parent user_password() with the interactive password
- dialog."""
- if password is None:
- password = read_pass()
- super(CliHandler, self).user_password(emailaddress, password)
-
-del _
--- a/VirtualMailManager/cli/__init__.py Wed Jul 28 01:03:56 2010 +0000
+++ b/VirtualMailManager/cli/__init__.py Wed Jul 28 02:08:03 2010 +0000
@@ -1,7 +1,6 @@
# -*- coding: UTF-8 -*-
# Copyright (c) 2010, Pascal Volk
# See COPYING for distribution information.
-
"""
VirtualMailManager.cli
~~~~~~~~~~~~~~~~~~~~~~
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/cli/config.py Wed Jul 28 02:08:03 2010 +0000
@@ -0,0 +1,93 @@
+# -*- coding: UTF-8 -*-
+# Copyright (c) 2010, Pascal Volk
+# See COPYING for distribution information.
+"""
+ VirtualMailManager.cli.config
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Adds some interactive stuff to the Config class.
+"""
+
+from ConfigParser import RawConfigParser
+from shutil import copy2
+
+from VirtualMailManager import ENCODING
+from VirtualMailManager.config import Config, ConfigValueError, LazyConfig
+from VirtualMailManager.errors import ConfigError
+from VirtualMailManager.cli import w_err, w_std
+from VirtualMailManager.constants import VMM_TOO_MANY_FAILURES
+
+_ = lambda msg: msg
+
+
+class CliConfig(Config):
+ """Adds the interactive ``configure`` method to the `Config` class
+ and overwrites `LazyConfig.set(), in order to update a single option
+ in the configuration file with a single command line command.
+ """
+
+ def configure(self, sections):
+ """Interactive method for configuring all options of the given
+ iterable ``sections`` object."""
+ input_fmt = _(u'Enter new value for option %(option)s '
+ u'[%(current_value)s]: ')
+ failures = 0
+
+ w_std(_(u'Using configuration file: %s\n') % self._cfg_filename)
+ for section in sections:
+ w_std(_(u'* Configuration section: %r') % section)
+ for opt, val in self.items(section):
+ failures = 0
+ while True:
+ newval = raw_input(input_fmt.encode(ENCODING, 'replace') %
+ {'option': opt, 'current_value': val})
+ if newval and newval != val:
+ try:
+ LazyConfig.set(self, '%s.%s' % (section, opt),
+ newval)
+ break
+ except (ValueError, ConfigValueError), err:
+ w_err(0, _(u'Warning: %s') % err)
+ failures += 1
+ if failures > 2:
+ raise ConfigError(_(u'Too many failures - try '
+ u'again later.'),
+ VMM_TOO_MANY_FAILURES)
+ else:
+ break
+ print
+ if self._modified:
+ self.__save_changes()
+
+ def set(self, option, value):
+ """Set the value of an option.
+
+ If the new `value` has been set, the configuration file will be
+ immediately updated.
+
+ Throws a ``ValueError`` if `value` couldn't be converted to
+ ``LazyConfigOption.cls``"""
+ section, option_ = self._get_section_option(option)
+ val = self._cfg[section][option_].cls(value)
+ if self._cfg[section][option_].validate:
+ val = self._cfg[section][option_].validate(val)
+ # Do not write default values also skip identical values
+ if not self._cfg[section][option_].default is None:
+ old_val = self.dget(option)
+ else:
+ old_val = self.pget(option)
+ if val == old_val:
+ return
+ if not RawConfigParser.has_section(self, section):
+ self.add_section(section)
+ RawConfigParser.set(self, section, option_, val)
+ self.__save_changes()
+
+ def __save_changes(self):
+ """Writes changes to the configuration file."""
+ copy2(self._cfg_filename, self._cfg_filename + '.bak')
+ self._cfg_file = open(self._cfg_filename, 'w')
+ self.write(self._cfg_file)
+ self._cfg_file.close()
+
+del _
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/cli/handler.py Wed Jul 28 02:08:03 2010 +0000
@@ -0,0 +1,84 @@
+# -*- coding: UTF-8 -*-
+# Copyright (c) 2010, Pascal Volk
+# See COPYING for distribution information.
+"""
+ VirtualMailManager.cli.handler
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ A derived Handler class with a few changes/additions for cli use.
+"""
+
+import os
+
+from VirtualMailManager.errors import VMMError
+from VirtualMailManager.handler import Handler
+from VirtualMailManager.cli import read_pass
+from VirtualMailManager.cli.config import CliConfig as Cfg
+from VirtualMailManager.constants import INVALID_SECTION
+
+_ = lambda msg: msg
+
+
+class CliHandler(Handler):
+ """This class uses a `CliConfig` for configuration stuff, instead of
+ the non-interactive `Config` class.
+
+ It provides the additional methods cfgSet() and configure().
+
+ Additionally it uses `VirtualMailManager.cli.read_pass()` for for the
+ interactive password dialog.
+ """
+
+ __slots__ = () # nothing additional, also no __dict__/__weakref__
+
+ def __init__(self):
+ """Creates a new CliHandler instance.
+
+ Throws a NotRootError if your uid is greater 0.
+ """
+ # Overwrite the parent CTor partly, we use the CliConfig class
+ # and add some command line checks.
+ skip_some_checks = os.sys.argv[1] in ('cf', 'configure', 'h', 'help',
+ 'v', 'version')
+ super(CliHandler, self).__init__(skip_some_checks)
+
+ self._cfg = Cfg(self._cfg_fname)
+ self._cfg.load()
+ if not skip_some_checks:
+ self._cfg.check()
+ self._chkenv()
+
+ def cfg_set(self, option, value):
+ """Set a new value for the given option."""
+ return self._cfg.set(option, value)
+
+ def configure(self, section=None):
+ """Starts the 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.
+ """
+ if section is None:
+ self._cfg.configure(self._cfg.sections())
+ elif self._cfg.has_section(section):
+ self._cfg.configure([section])
+ else:
+ raise VMMError(_(u'Invalid section: “%s”') % section,
+ INVALID_SECTION)
+
+ def user_add(self, emailaddress, password=None):
+ """Prefix the parent user_add() with the interactive password
+ dialog."""
+ if password is None:
+ password = read_pass()
+ super(CliHandler, self).user_add(emailaddress, password)
+
+ def user_password(self, emailaddress, password=None):
+ """Prefix the parent user_password() with the interactive password
+ dialog."""
+ if password is None:
+ password = read_pass()
+ super(CliHandler, self).user_password(emailaddress, password)
+
+del _
--- a/VirtualMailManager/common.py Wed Jul 28 01:03:56 2010 +0000
+++ b/VirtualMailManager/common.py Wed Jul 28 02:08:03 2010 +0000
@@ -1,9 +1,9 @@
# -*- coding: UTF-8 -*-
# Copyright (c) 2010, Pascal Volk
# See COPYING for distribution information.
-
"""
VirtualMailManager.common
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
Some common functions
"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/config.py Wed Jul 28 02:08:03 2010 +0000
@@ -0,0 +1,451 @@
+# -*- coding: UTF-8 -*-
+# Copyright (c) 2007 - 2010, Pascal Volk
+# See COPYING for distribution information.
+"""
+ VirtualMailManager.config
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ VMM's configuration module for simplified configuration access.
+"""
+
+import re
+
+from ConfigParser import \
+ Error, MissingSectionHeaderError, NoOptionError, NoSectionError, \
+ ParsingError, RawConfigParser
+from cStringIO import StringIO# TODO: move interactive stff to cli
+
+from VirtualMailManager.common import exec_ok, get_unicode, is_dir, version_hex
+from VirtualMailManager.constants import CONF_ERROR
+from VirtualMailManager.errors import ConfigError, VMMError
+from VirtualMailManager.maillocation import known_format
+from VirtualMailManager.password import verify_scheme as _verify_scheme
+
+
+_ = lambda msg: msg
+
+
+class BadOptionError(Error):
+ """Raised when a option isn't in the format 'section.option'."""
+ pass
+
+
+class ConfigValueError(Error):
+ """Raised when creating or validating of new values fails."""
+ pass
+
+
+class NoDefaultError(Error):
+ """Raised when the requested option has no default value."""
+
+ def __init__(self, section, option):
+ Error.__init__(self, 'Option %r in section %r has no default value' %
+ (option, section))
+
+
+class LazyConfig(RawConfigParser):
+ """The **lazy** derivate of the `RawConfigParser`.
+
+ There are two additional getters:
+
+ `pget()`
+ The polymorphic getter, which returns a option's value with the
+ appropriate type.
+ `dget()`
+ Like `LazyConfig.pget()`, but returns the option's default, from
+ `LazyConfig._cfg['sectionname']['optionname'].default`, if the
+ option is not configured in a ini-like configuration file.
+
+ `set()` differs from `RawConfigParser`'s `set()` method. `set()`
+ takes the `section` and `option` arguments combined to a single
+ string in the form "section.option".
+ """
+
+ def __init__(self):
+ RawConfigParser.__init__(self)
+ self._modified = False
+ # sample _cfg dict. Create your own in your derived class.
+ self._cfg = {
+ 'sectionname': {
+ 'optionname': LazyConfigOption(int, 1, self.getint),
+ }
+ }
+
+ def bool_new(self, value):
+ """Converts the string `value` into a `bool` and returns it.
+
+ | '1', 'on', 'yes' and 'true' will become `True`
+ | '0', 'off', 'no' and 'false' will become `False`
+
+ Throws a `ConfigValueError` for all other values, except bools.
+ """
+ if isinstance(value, bool):
+ return value
+ if value.lower() in self._boolean_states:
+ return self._boolean_states[value.lower()]
+ else:
+ raise ConfigValueError(_(u"Not a boolean: '%s'") %
+ get_unicode(value))
+
+ def getboolean(self, section, option):
+ """Returns the boolean value of the option, in the given
+ section.
+
+ For a boolean True, the value must be set to '1', 'on', 'yes',
+ 'true' or True. For a boolean False, the value must set to '0',
+ 'off', 'no', 'false' or False.
+ If the option has another value assigned this method will raise
+ a ValueError.
+ """
+ # if the setting was modified it may be still a boolean value lets see
+ tmp = self.get(section, option)
+ if isinstance(tmp, bool):
+ return tmp
+ if not tmp.lower() in self._boolean_states:
+ raise ValueError('Not a boolean: %s' % tmp)
+ return self._boolean_states[tmp.lower()]
+
+ def _get_section_option(self, section_option):
+ """splits ``section_option`` (section.option) in two parts and
+ returns them as list ``[section, option]``, if:
+
+ * it likes the format of ``section_option``
+ * the ``section`` is known
+ * the ``option`` is known
+
+ Else one of the following exceptions will be thrown:
+
+ * `BadOptionError`
+ * `NoSectionError`
+ * `NoOptionError`
+ """
+ sect_opt = section_option.lower().split('.')
+ # TODO: cache it
+ if len(sect_opt) != 2: # do we need a regexp to check the format?
+ raise BadOptionError(_(u"Bad format: '%s' - expected: "
+ u"section.option") %
+ get_unicode(section_option))
+ if not sect_opt[0] in self._cfg:
+ raise NoSectionError(sect_opt[0])
+ if not sect_opt[1] in self._cfg[sect_opt[0]]:
+ raise NoOptionError(sect_opt[1], sect_opt[0])
+ return sect_opt
+
+ def items(self, section):
+ """returns an iterable that returns key, value ``tuples`` from
+ the given ``section``.
+ """
+ if section in self._sections: # check if the section was parsed
+ sect = self._sections[section]
+ elif not section in self._cfg:
+ raise NoSectionError(section)
+ else:
+ return ((k, self._cfg[section][k].default) \
+ for k in self._cfg[section].iterkeys())
+ # still here? Get defaults and merge defaults with configured setting
+ defaults = dict((k, self._cfg[section][k].default) \
+ for k in self._cfg[section].iterkeys())
+ defaults.update(sect)
+ if '__name__' in defaults:
+ del defaults['__name__']
+ return defaults.iteritems()
+
+ def dget(self, option):
+ """Returns the value of the `option`.
+
+ If the option could not be found in the configuration file, the
+ configured default value, from ``LazyConfig._cfg`` will be
+ returned.
+
+ Arguments:
+
+ `option` : string
+ the configuration option in the form "section.option"
+
+ Throws a `NoDefaultError`, if no default value was passed to
+ `LazyConfigOption.__init__()` for the `option`.
+ """
+ section, option = self._get_section_option(option)
+ try:
+ return self._cfg[section][option].getter(section, option)
+ except (NoSectionError, NoOptionError):
+ if not self._cfg[section][option].default is None: # may be False
+ return self._cfg[section][option].default
+ else:
+ raise NoDefaultError(section, option)
+
+ def pget(self, option):
+ """Returns the value of the `option`."""
+ section, option = self._get_section_option(option)
+ return self._cfg[section][option].getter(section, option)
+
+ def set(self, option, value):
+ """Set the `value` of the `option`.
+
+ Throws a `ValueError` if `value` couldn't be converted using
+ `LazyConfigOption.cls`.
+ """
+ # pylint: disable=W0221
+ # @pylint: _L A Z Y_
+ section, option = self._get_section_option(option)
+ val = self._cfg[section][option].cls(value)
+ if self._cfg[section][option].validate:
+ val = self._cfg[section][option].validate(val)
+ if not RawConfigParser.has_section(self, section):
+ self.add_section(section)
+ RawConfigParser.set(self, section, option, val)
+ self._modified = True
+
+ def has_section(self, section):
+ """Checks if `section` is a known configuration section."""
+ return section.lower() in self._cfg
+
+ def has_option(self, option):
+ """Checks if the option (section.option) is a known
+ configuration option.
+ """
+ # pylint: disable=W0221
+ # @pylint: _L A Z Y_
+ try:
+ self._get_section_option(option)
+ return True
+ except(BadOptionError, NoSectionError, NoOptionError):
+ return False
+
+ def sections(self):
+ """Returns an iterator object for all configuration sections."""
+ return self._cfg.iterkeys()
+
+
+class LazyConfigOption(object):
+ """A simple container class for configuration settings.
+
+ `LazyConfigOption` instances are required by `LazyConfig` instances,
+ and instances of classes derived from `LazyConfig`, like the
+ `Config` class.
+ """
+ __slots__ = ('__cls', '__default', '__getter', '__validate')
+
+ def __init__(self, cls, default, getter, validate=None):
+ """Creates a new `LazyConfigOption` instance.
+
+ Arguments:
+
+ `cls` : type
+ The class/type of the option's value
+ `default`
+ Default value of the option. Use ``None`` if the option should
+ not have a default value.
+ `getter` : callable
+ A method's name of `RawConfigParser` and derived classes, to
+ get a option's value, e.g. `self.getint`.
+ `validate` : NoneType or a callable
+ None or any method, that takes one argument, in order to
+ check the value, when `LazyConfig.set()` is called.
+ """
+ self.__cls = cls
+ if not default is None: # enforce the type of the default value
+ self.__default = self.__cls(default)
+ else:
+ self.__default = default
+ if not callable(getter):
+ raise TypeError('getter has to be a callable, got a %r' %
+ getter.__class__.__name__)
+ self.__getter = getter
+ if validate and not callable(validate):
+ raise TypeError('validate has to be callable or None, got a %r' %
+ validate.__class__.__name__)
+ self.__validate = validate
+
+ @property
+ def cls(self):
+ """The class of the option's value e.g. `str`, `unicode` or `bool`."""
+ return self.__cls
+
+ @property
+ def default(self):
+ """The option's default value, may be `None`"""
+ return self.__default
+
+ @property
+ def getter(self):
+ """The getter method or function to get the option's value"""
+ return self.__getter
+
+ @property
+ def validate(self):
+ """A method or function to validate the value"""
+ return self.__validate
+
+
+class Config(LazyConfig):
+ """This class is for reading vmm's configuration file."""
+
+ def __init__(self, filename):
+ """Creates a new Config instance
+
+ Arguments:
+
+ `filename` : str
+ path to the configuration file
+ """
+ LazyConfig.__init__(self)
+ self._cfg_filename = filename
+ self._cfg_file = None
+ self.__missing = {}
+
+ LCO = LazyConfigOption
+ bool_t = self.bool_new
+ self._cfg = {
+ 'account': {
+ 'delete_directory': LCO(bool_t, False, self.getboolean),
+ 'directory_mode': LCO(int, 448, self.getint),
+ 'disk_usage': LCO(bool_t, False, self.getboolean),
+ 'password_length': LCO(int, 8, self.getint),
+ 'random_password': LCO(bool_t, False, self.getboolean),
+ 'imap': LCO(bool_t, True, self.getboolean),
+ 'pop3': LCO(bool_t, True, self.getboolean),
+ 'sieve': LCO(bool_t, True, self.getboolean),
+ 'smtp': LCO(bool_t, True, self.getboolean),
+ },
+ 'bin': {
+ 'dovecotpw': LCO(str, '/usr/sbin/dovecotpw', self.get,
+ exec_ok),
+ 'du': LCO(str, '/usr/bin/du', self.get, exec_ok),
+ 'postconf': LCO(str, '/usr/sbin/postconf', self.get, exec_ok),
+ },
+ 'database': {
+ 'host': LCO(str, 'localhost', self.get),
+ 'name': LCO(str, 'mailsys', self.get),
+ 'pass': LCO(str, None, self.get),
+ 'user': LCO(str, None, self.get),
+ },
+ 'domain': {
+ 'auto_postmaster': LCO(bool_t, True, self.getboolean),
+ 'delete_directory': LCO(bool_t, False, self.getboolean),
+ 'directory_mode': LCO(int, 504, self.getint),
+ 'force_deletion': LCO(bool_t, False, self.getboolean),
+ },
+ 'mailbox': {
+ 'folders': LCO(str, 'Drafts:Sent:Templates:Trash',
+ self.unicode),
+ 'format': LCO(str, 'maildir', self.get, check_mailbox_format),
+ 'root': LCO(str, 'Maildir', self.unicode),
+ 'subscribe': LCO(bool_t, True, self.getboolean),
+ },
+ 'misc': {
+ 'base_directory': LCO(str, '/srv/mail', self.get, is_dir),
+ 'crypt_blowfish_rounds': LCO(int, 5, self.getint),
+ 'crypt_sha256_rounds': LCO(int, 5000, self.getint),
+ 'crypt_sha512_rounds': LCO(int, 5000, self.getint),
+ 'dovecot_version': LCO(str, None, self.hexversion,
+ check_version_format),
+ 'password_scheme': LCO(str, 'CRAM-MD5', self.get,
+ verify_scheme),
+ 'transport': LCO(str, 'dovecot:', self.get),
+ },
+ }
+
+ def load(self):
+ """Loads the configuration, read only.
+
+ Raises a ConfigError if the configuration syntax is
+ invalid.
+ """
+ try:
+ self._cfg_file = open(self._cfg_filename, 'r')
+ self.readfp(self._cfg_file)
+ except (MissingSectionHeaderError, ParsingError), err:
+ raise ConfigError(str(err), CONF_ERROR)
+ finally:
+ if self._cfg_file and not self._cfg_file.closed:
+ self._cfg_file.close()
+
+ def check(self):
+ """Performs a configuration check.
+
+ Raises a ConfigError if settings w/o a default value are missed.
+ Or a ConfigValueError if 'misc.dovecot_version' has the wrong
+ format.
+ """
+ # TODO: There are only two settings w/o defaults.
+ # So there is no need for cStringIO
+ if not self.__chk_cfg():
+ errmsg = StringIO()
+ errmsg.write(_(u'Missing options, which have no default value.\n'))
+ errmsg.write(_(u'Using configuration file: %s\n') %
+ self._cfg_filename)
+ for section, options in self.__missing.iteritems():
+ errmsg.write(_(u'* Section: %s\n') % section)
+ for option in options:
+ errmsg.write((u' %s\n') % option)
+ raise ConfigError(errmsg.getvalue(), CONF_ERROR)
+ check_version_format(self.get('misc', 'dovecot_version'))
+
+ def hexversion(self, section, option):
+ """Converts the version number (e.g.: 1.2.3) from the *option*'s
+ value to an int."""
+ return version_hex(self.get(section, option))
+
+ def unicode(self, section, option):
+ """Returns the value of the `option` from `section`, converted
+ to Unicode."""
+ return get_unicode(self.get(section, option))
+
+ def __chk_cfg(self):
+ """Checks all section's options for settings w/o a default
+ value.
+
+ Returns `True` if everything is fine, else `False`.
+ """
+ errors = False
+ for section in self._cfg.iterkeys():
+ missing = []
+ for option, value in self._cfg[section].iteritems():
+ if (value.default is None and
+ not RawConfigParser.has_option(self, section, option)):
+ missing.append(option)
+ errors = True
+ if missing:
+ self.__missing[section] = missing
+ return not errors
+
+
+def check_mailbox_format(format):
+ """
+ Check if the mailbox format *format* is supported. When the *format*
+ is supported it will be returned, otherwise a `ConfigValueError` will
+ be raised.
+ """
+ format = format.lower()
+ if known_format(format):
+ return format
+ raise ConfigValueError(_(u"Unsupported mailbox format: '%s'") %
+ get_unicode(format))
+
+
+def check_version_format(version_string):
+ """Check if the *version_string* has the proper format, e.g.: '1.2.3'.
+ Returns the validated version string if it has the expected format.
+ Otherwise a `ConfigValueError` will be raised.
+ """
+ version_re = r'^\d+\.\d+\.(?:\d+|(?:alpha|beta|rc)\d+)$'
+ if not re.match(version_re, version_string):
+ raise ConfigValueError(_(u"Not a valid Dovecot version: '%s'") %
+ get_unicode(version_string))
+ return version_string
+
+
+def verify_scheme(scheme):
+ """Checks if the password scheme *scheme* can be accepted and returns
+ the verified scheme.
+ """
+ try:
+ scheme, encoding = _verify_scheme(scheme)
+ except VMMError, err: # 'cast' it
+ raise ConfigValueError(err.msg)
+ if not encoding:
+ return scheme
+ return '%s.%s' % (scheme, encoding)
+
+del _
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/domain.py Wed Jul 28 02:08:03 2010 +0000
@@ -0,0 +1,408 @@
+# -*- coding: UTF-8 -*-
+# Copyright (c) 2007 - 2010, Pascal Volk
+# See COPYING for distribution information.
+"""
+ VirtualMailManager.domain
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Virtual Mail Manager's Domain class to manage e-mail domains.
+"""
+
+import os
+import re
+from random import choice
+
+from VirtualMailManager.constants import \
+ ACCOUNT_AND_ALIAS_PRESENT, ACCOUNT_PRESENT, ALIAS_PRESENT, \
+ DOMAIN_ALIAS_EXISTS, DOMAIN_EXISTS, DOMAIN_INVALID, DOMAIN_TOO_LONG, \
+ NO_SUCH_DOMAIN
+from VirtualMailManager.errors import DomainError as DomErr
+from VirtualMailManager.transport import Transport
+
+
+MAILDIR_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'
+RE_DOMAIN = re.compile(r"^(?:[a-z0-9-]{1,63}\.){1,}[a-z]{2,6}$")
+_ = lambda msg: msg
+
+
+class Domain(object):
+ """Class to manage e-mail domains."""
+ __slots__ = ('_directory', '_gid', '_name', '_transport', '_dbh', '_new')
+
+ def __init__(self, dbh, domainname):
+ """Creates a new Domain instance.
+
+ Loads all relevant data from the database, if the domain could be
+ found. To create a new domain call the methods set_directory() and
+ set_transport() before save().
+
+ A DomainError will be thrown when the *domainname* is the name of
+ an alias domain.
+
+ Arguments:
+
+ `dbh` : pyPgSQL.PgSQL.Connection
+ a database connection for the database access
+ `domainname` : basestring
+ The name of the domain
+ """
+ self._name = check_domainname(domainname)
+ self._dbh = dbh
+ self._gid = 0
+ self._transport = None
+ self._directory = None
+ self._new = True
+ self._load()
+
+ def _load(self):
+ """Load information from the database and checks if the domain name
+ is the primary one.
+
+ Raises a DomainError if Domain._name isn't the primary name of the
+ domain.
+ """
+ dbc = self._dbh.cursor()
+ dbc.execute('SELECT dd.gid, tid, domaindir, is_primary FROM '
+ 'domain_data dd, domain_name dn WHERE domainname = %s AND '
+ 'dn.gid = dd.gid', self._name)
+ result = dbc.fetchone()
+ dbc.close()
+ if result:
+ if not result[3]:
+ raise DomErr(_(u"The domain '%s' is an alias domain.") %
+ self._name, DOMAIN_ALIAS_EXISTS)
+ self._gid, self._directory = result[0], result[2]
+ self._transport = Transport(self._dbh, tid=result[1])
+ self._new = False
+
+ def _set_gid(self):
+ """Sets the ID of the domain - if not set yet."""
+ assert self._gid == 0
+ dbc = self._dbh.cursor()
+ dbc.execute("SELECT nextval('domain_gid')")
+ self._gid = dbc.fetchone()[0]
+ dbc.close()
+
+ def _has(self, what):
+ """Checks if aliases or accounts are assigned to the domain.
+
+ If there are assigned accounts or aliases True will be returned,
+ otherwise False will be returned.
+
+ Argument:
+
+ `what` : basestring
+ "alias" or "users"
+ """
+ assert what in ('alias', 'users')
+ dbc = self._dbh.cursor()
+ if what == 'users':
+ dbc.execute("SELECT count(gid) FROM users WHERE gid=%s", self._gid)
+ else:
+ dbc.execute("SELECT count(gid) FROM alias WHERE gid=%s", self._gid)
+ count = dbc.fetchone()
+ dbc.close()
+ return count[0] > 0
+
+ def _chk_delete(self, deluser, delalias):
+ """Checks dependencies for deletion.
+
+ Arguments:
+ deluser -- ignore available accounts (bool)
+ delalias -- ignore available aliases (bool)
+ """
+ if not deluser:
+ hasuser = self._has('users')
+ else:
+ hasuser = False
+ if not delalias:
+ hasalias = self._has('alias')
+ else:
+ hasalias = False
+ if hasuser and hasalias:
+ raise DomErr(_(u'There are accounts and aliases.'),
+ ACCOUNT_AND_ALIAS_PRESENT)
+ elif hasuser:
+ raise DomErr(_(u'There are accounts.'), ACCOUNT_PRESENT)
+ elif hasalias:
+ raise DomErr(_(u'There are aliases.'), ALIAS_PRESENT)
+
+ def _chk_state(self):
+ """Throws a DomainError if the Domain is new - not saved in the
+ database."""
+ if self._new:
+ raise DomErr(_(u"The domain '%s' doesn't exist.") % self._name,
+ NO_SUCH_DOMAIN)
+
+ @property
+ def gid(self):
+ """The GID of the Domain."""
+ return self._gid
+
+ @property
+ def name(self):
+ """The Domain's name."""
+ return self._name
+
+ @property
+ def directory(self):
+ """The Domain's directory."""
+ return self._directory
+
+ def set_directory(self, basedir):
+ """Set the path value of the Domain's directory, inside *basedir*.
+
+ Argument:
+
+ `basedir` : basestring
+ The base directory of all domains
+ """
+ assert self._new and self._directory is None
+ self._set_gid()
+ self._directory = os.path.join(basedir, choice(MAILDIR_CHARS),
+ str(self._gid))
+
+ @property
+ def transport(self):
+ """The Domain's transport."""
+ return self._transport
+
+ def set_transport(self, transport):
+ """Set the transport for the new Domain.
+
+ Argument:
+
+ `transport` : VirtualMailManager.Transport
+ The transport of the new Domain
+ """
+ assert self._new and isinstance(transport, Transport)
+ self._transport = transport
+
+ def save(self):
+ """Stores the new domain in the database."""
+ if not self._new:
+ raise DomErr(_(u"The domain '%s' already exists.") % self._name,
+ DOMAIN_EXISTS)
+ assert self._directory is not None and self._transport is not None
+ dbc = self._dbh.cursor()
+ dbc.execute("INSERT INTO domain_data VALUES (%s, %s, %s)", self._gid,
+ self._transport.tid, self._directory)
+ dbc.execute("INSERT INTO domain_name VALUES (%s, %s, %s)", self._name,
+ self._gid, True)
+ self._dbh.commit()
+ dbc.close()
+ self._new = False
+
+ def delete(self, deluser=False, delalias=False):
+ """Deletes the domain.
+
+ Arguments:
+
+ `deluser` : bool
+ force deletion of all available accounts, default `False`
+ `delalias` : bool
+ force deletion of all available aliases, default `False`
+ """
+ self._chk_state()
+ self._chk_delete(deluser, delalias)
+ dbc = self._dbh.cursor()
+ for tbl in ('alias', 'users', 'relocated', 'domain_name',
+ 'domain_data'):
+ dbc.execute("DELETE FROM %s WHERE gid = %d" % (tbl, self._gid))
+ self._dbh.commit()
+ dbc.close()
+ self._gid = 0
+ self._directory = self._transport = None
+ self._new = True
+
+ def update_transport(self, transport, force=False):
+ """Sets a new transport for the Domain.
+
+ If *force* is `True` the new *transport* will be assigned to all
+ existing accounts. Otherwise the *transport* will be only used for
+ accounts created from now on.
+
+ Arguments:
+
+ `transport` : VirtualMailManager.Transport
+ the new transport
+ `force` : bool
+ enforce new transport setting for all accounts, default `False`
+ """
+ self._chk_state()
+ assert isinstance(transport, Transport)
+ if transport == self._transport:
+ return
+ dbc = self._dbh.cursor()
+ dbc.execute("UPDATE domain_data SET tid = %s WHERE gid = %s",
+ transport.tid, self._gid)
+ if dbc.rowcount > 0:
+ self._dbh.commit()
+ if force:
+ dbc.execute("UPDATE users SET tid = %s WHERE gid = %s",
+ transport.tid, self._gid)
+ if dbc.rowcount > 0:
+ self._dbh.commit()
+ dbc.close()
+ self._transport = transport
+
+ def get_info(self):
+ """Returns a dictionary with information about the domain."""
+ self._chk_state()
+ dbc = self._dbh.cursor()
+ dbc.execute('SELECT gid, domainname, transport, domaindir, '
+ 'aliasdomains accounts, aliases, relocated FROM '
+ 'vmm_domain_info WHERE gid = %s', self._gid)
+ info = dbc.fetchone()
+ dbc.close()
+ keys = ('gid', 'domainname', 'transport', 'domaindir', 'aliasdomains',
+ 'accounts', 'aliases', 'relocated')
+ return dict(zip(keys, info))
+
+ def get_accounts(self):
+ """Returns a list with all accounts of the domain."""
+ self._chk_state()
+ dbc = self._dbh.cursor()
+ dbc.execute('SELECT local_part from users where gid = %s ORDER BY '
+ 'local_part', self._gid)
+ users = dbc.fetchall()
+ dbc.close()
+ accounts = []
+ if users:
+ addr = u'@'.join
+ _dom = self._name
+ accounts = [addr((account[0], _dom)) for account in users]
+ return accounts
+
+ def get_aliases(self):
+ """Returns a list with all aliases e-mail addresses of the domain."""
+ self._chk_state()
+ dbc = self._dbh.cursor()
+ dbc.execute('SELECT DISTINCT address FROM alias WHERE gid = %s ORDER '
+ 'BY address', self._gid)
+ addresses = dbc.fetchall()
+ dbc.close()
+ aliases = []
+ if addresses:
+ addr = u'@'.join
+ _dom = self._name
+ aliases = [addr((alias[0], _dom)) for alias in addresses]
+ return aliases
+
+ def get_relocated(self):
+ """Returns a list with all addresses of relocated users."""
+ self._chk_state()
+ dbc = self._dbh.cursor()
+ dbc.execute('SELECT address FROM relocated WHERE gid = %s ORDER BY '
+ 'address', self._gid)
+ addresses = dbc.fetchall()
+ dbc.close()
+ relocated = []
+ if addresses:
+ addr = u'@'.join
+ _dom = self._name
+ relocated = [addr((address[0], _dom)) for address in addresses]
+ return relocated
+
+ def get_aliase_names(self):
+ """Returns a list with all alias domain names of the domain."""
+ self._chk_state()
+ dbc = self._dbh.cursor()
+ dbc.execute('SELECT domainname FROM domain_name WHERE gid = %s AND '
+ 'NOT is_primary ORDER BY domainname', self._gid)
+ anames = dbc.fetchall()
+ dbc.close()
+ aliasdomains = []
+ if anames:
+ aliasdomains = [aname[0] for aname in anames]
+ return aliasdomains
+
+
+def check_domainname(domainname):
+ """Returns the validated domain name `domainname`.
+
+ Throws an `DomainError`, if the domain name is too long or doesn't
+ look like a valid domain name (label.label.label).
+
+ """
+ if not RE_DOMAIN.match(domainname):
+ domainname = domainname.encode('idna')
+ if len(domainname) > 255:
+ raise DomErr(_(u'The domain name is too long'), DOMAIN_TOO_LONG)
+ if not RE_DOMAIN.match(domainname):
+ raise DomErr(_(u"The domain name '%s' is invalid") % domainname,
+ DOMAIN_INVALID)
+ return domainname
+
+
+def get_gid(dbh, domainname):
+ """Returns the group id of the domain *domainname*.
+
+ If the domain couldn't be found in the database 0 will be returned.
+ """
+ domainname = check_domainname(domainname)
+ dbc = dbh.cursor()
+ dbc.execute('SELECT gid FROM domain_name WHERE domainname=%s', domainname)
+ gid = dbc.fetchone()
+ dbc.close()
+ if gid:
+ return gid[0]
+ return 0
+
+
+def search(dbh, pattern=None, like=False):
+ """'Search' for domains by *pattern* in the database.
+
+ *pattern* may be a domain name or a partial domain name - starting
+ and/or ending with a '%' sign. When the *pattern* starts or ends with
+ a '%' sign *like* has to be `True` to perform a wildcard search.
+ To retrieve all available domains use the arguments' default values.
+
+ This function returns a tuple with a list and a dict: (order, domains).
+ The order list contains the domains' gid, alphabetical sorted by the
+ primary domain name. The domains dict's keys are the gids of the
+ domains. The value of item is a list. The first list element contains
+ the primary domain name or `None`. The elements [1:] contains the
+ names of alias domains.
+
+ Arguments:
+
+ `pattern` : basestring
+ a (partial) domain name (starting and/or ending with a "%" sign)
+ `like` : bool
+ should be `True` when *pattern* starts/ends with a "%" sign
+ """
+ if pattern and not like:
+ pattern = check_domainname(pattern)
+ sql = 'SELECT gid, domainname, is_primary FROM domain_name'
+ if pattern:
+ if like:
+ sql += " WHERE domainname LIKE '%s'" % pattern
+ else:
+ sql += " WHERE domainname = '%s'" % pattern
+ sql += ' ORDER BY is_primary DESC, domainname'
+ dbc = dbh.cursor()
+ dbc.execute(sql)
+ result = dbc.fetchall()
+ dbc.close()
+
+ gids = [domain[0] for domain in result if domain[2]]
+ domains = {}
+ for gid, domain, is_primary in result:
+ if is_primary:
+ if not gid in domains:
+ domains[gid] = [domain]
+ else:
+ domains[gid].insert(0, domain)
+ else:
+ if gid in gids:
+ if gid in domains:
+ domains[gid].append(domain)
+ else:
+ domains[gid] = [domain]
+ else:
+ gids.append(gid)
+ domains[gid] = [None, domain]
+ return gids, domains
+
+del _
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/emailaddress.py Wed Jul 28 02:08:03 2010 +0000
@@ -0,0 +1,103 @@
+# -*- coding: UTF-8 -*-
+# Copyright (c) 2008 - 2010, Pascal Volk
+# See COPYING for distribution information.
+"""
+ VirtualMailManager.emailaddress
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Virtual Mail Manager's EmailAddress class to handle e-mail addresses.
+"""
+import re
+
+from VirtualMailManager.domain import check_domainname
+from VirtualMailManager.constants import \
+ DOMAIN_NO_NAME, INVALID_ADDRESS, LOCALPART_INVALID, LOCALPART_TOO_LONG
+from VirtualMailManager.errors import EmailAddressError as EAErr
+
+
+RE_LOCALPART = re.compile(r"[^\w!#$%&'\*\+-\.\/=?^_`{\|}~]")
+_ = lambda msg: msg
+
+
+class EmailAddress(object):
+ """Simple class for validated e-mail addresses."""
+ __slots__ = ('_localpart', '_domainname')
+
+ def __init__(self, address):
+ """Creates a new instance from the string/unicode ``address``."""
+ assert isinstance(address, basestring)
+ self._localpart = None
+ self._domainname = None
+ self._chk_address(address)
+
+ @property
+ def localpart(self):
+ """The local-part of the address *local-part@domain*"""
+ return self._localpart
+
+ @property
+ def domainname(self):
+ """The domain part of the address *local-part@domain*"""
+ return self._domainname
+
+ def __eq__(self, other):
+ if isinstance(other, self.__class__):
+ return self._localpart == other.localpart and \
+ self._domainname == other.domainname
+ return NotImplemented
+
+ def __ne__(self, other):
+ if isinstance(other, self.__class__):
+ return self._localpart != other.localpart or \
+ self._domainname != other.domainname
+ return NotImplemented
+
+ def __hash__(self):
+ return hash((self._localpart.lower(), self._domainname.lower()))
+
+ def __repr__(self):
+ return "EmailAddress('%s@%s')" % (self._localpart, self._domainname)
+
+ def __str__(self):
+ return '%s@%s' % (self._localpart, self._domainname)
+
+ def _chk_address(self, address):
+ """Checks if the string ``address`` could be used for an e-mail
+ address. If so, it will assign the corresponding values to the
+ attributes `_localpart` and `_domainname`."""
+ parts = address.split('@')
+ p_len = len(parts)
+ if p_len < 2:
+ raise EAErr(_(u"Missing the '@' sign in address %r") % address,
+ INVALID_ADDRESS)
+ elif p_len > 2:
+ raise EAErr(_(u"Too many '@' signs in address %r") % address,
+ INVALID_ADDRESS)
+ if not parts[0]:
+ raise EAErr(_(u'Missing local-part in address %r') % address,
+ LOCALPART_INVALID)
+ if not parts[1]:
+ raise EAErr(_(u'Missing domain name in address %r') % address,
+ DOMAIN_NO_NAME)
+ self._localpart = check_localpart(parts[0])
+ self._domainname = check_domainname(parts[1])
+
+
+def check_localpart(localpart):
+ """Returns the validated local-part `localpart`.
+
+ Throws a `EmailAddressError` if the local-part is too long or contains
+ invalid characters.
+ """
+ if len(localpart) > 64:
+ raise EAErr(_(u"The local-part '%s' is too long") % localpart,
+ LOCALPART_TOO_LONG)
+ invalid_chars = set(RE_LOCALPART.findall(localpart))
+ if invalid_chars:
+ i_chars = u''.join((u'"%s" ' % c for c in invalid_chars))
+ raise EAErr(_(u"The local-part '%(l_part)s' contains invalid "
+ u"characters: %(i_chars)s") % {'l_part': localpart,
+ 'i_chars': i_chars}, LOCALPART_INVALID)
+ return localpart
+
+del _
--- a/VirtualMailManager/errors.py Wed Jul 28 01:03:56 2010 +0000
+++ b/VirtualMailManager/errors.py Wed Jul 28 02:08:03 2010 +0000
@@ -1,9 +1,9 @@
# -*- coding: UTF-8 -*-
# Copyright (c) 2007 - 2010, Pascal Volk
# See COPYING for distribution information.
-
"""
VirtualMailManager.errors
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
VMM's Exception classes
"""
@@ -20,6 +20,7 @@
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.msg, self.code)
+
class ConfigError(VMMError):
"""Exception class for configuration exceptions"""
pass
--- a/VirtualMailManager/ext/Postconf.py Wed Jul 28 01:03:56 2010 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,127 +0,0 @@
-# -*- coding: UTF-8 -*-
-# Copyright (c) 2008 - 2010, Pascal Volk
-# See COPYING for distribution information.
-"""
- VirtualMailManager.ext.Postconf
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
- Wrapper class for Postfix's postconf.
- Postconf instances can be used to read actual values of configuration
- parameters or edit the value of a configuration parameter.
-
- postconf.read(parameter) -> value
- postconf.edit(parameter, value)
-"""
-
-import re
-from subprocess import Popen, PIPE
-
-from VirtualMailManager.errors import VMMError
-from VirtualMailManager.constants import VMM_ERROR
-
-_ = lambda msg: msg
-
-
-class Postconf(object):
- """Wrapper class for Postfix's postconf."""
- __slots__ = ('_bin', '_val')
- _parameter_re = re.compile(r'^\w+$')
- _variables_re = re.compile(r'\$\b\w+\b')
-
- def __init__(self, postconf_bin):
- """Creates a new Postconf instance.
-
- Argument:
-
- `postconf_bin` : str
- absolute path to the Postfix postconf binary.
- """
- self._bin = postconf_bin
- self._val = ''
-
- def edit(self, parameter, value):
- """Set the `parameter`'s value to `value`.
-
- Arguments:
-
- `parameter` : str
- the name of a Postfix configuration parameter
- `value` : str
- the parameter's new value.
- """
- self._check_parameter(parameter)
- stdout, stderr = Popen((self._bin, '-e', parameter + '=' + str(value)),
- stderr=PIPE).communicate()
- if stderr:
- raise VMMError(stderr.strip(), VMM_ERROR)
-
- def read(self, parameter, expand_vars=True):
- """Returns the parameters value.
-
- If expand_vars is True (default), all variables in the value will be
- expanded:
- e.g. mydestination: mail.example.com, localhost.example.com, localhost
- Otherwise the value may contain one or more variables.
- e.g. mydestination: $myhostname, localhost.$mydomain, localhost
-
- Arguments:
-
- `parameter` : str
- the name of a Postfix configuration parameter.
- `expand_vars` : bool
- indicates if variables should be expanded or not, default True
- """
- self._check_parameter(parameter)
- self._val = self._read(parameter)
- if expand_vars:
- self._expand_vars()
- return self._val
-
- def _check_parameter(self, parameter):
- """Check that the `parameter` looks like a configuration parameter.
- If not, a VMMError will be raised."""
- if not self.__class__._parameter_re.match(parameter):
- raise VMMError(_(u"The value '%s' doesn't look like a valid "
- u"postfix configuration parameter name.") %
- parameter, VMM_ERROR)
-
- def _expand_vars(self):
- """Expand the $variables in self._val to their values."""
- while True:
- pvars = set(self.__class__._variables_re.findall(self._val))
- if not pvars:
- break
- if len(pvars) > 1:
- self._expand_multi_vars(self._read_multi(pvars))
- continue
- pvars = pvars.pop()
- self._val = self._val.replace(pvars, self._read(pvars[1:]))
-
- def _expand_multi_vars(self, old_new):
- """Replace all $vars in self._val with their values."""
- for old, new in old_new.iteritems():
- self._val = self._val.replace('$' + old, new)
-
- def _read(self, parameter):
- """Ask postconf for the value of a single configuration parameter."""
- stdout, stderr = Popen([self._bin, '-h', parameter], stdout=PIPE,
- stderr=PIPE).communicate()
- if stderr:
- raise VMMError(stderr.strip(), VMM_ERROR)
- return stdout.strip()
-
- def _read_multi(self, parameters):
- """Ask postconf for multiple configuration parameters. Returns a dict
- parameter: value items."""
- cmd = [self._bin]
- cmd.extend(parameter[1:] for parameter in parameters)
- stdout, stderr = Popen(cmd, stdout=PIPE, stderr=PIPE).communicate()
- if stderr:
- raise VMMError(stderr.strip(), VMM_ERROR)
- par_val = {}
- for line in stdout.splitlines():
- par, val = line.split(' = ')
- par_val[par] = val
- return par_val
-
-del _
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/ext/postconf.py Wed Jul 28 02:08:03 2010 +0000
@@ -0,0 +1,127 @@
+# -*- coding: UTF-8 -*-
+# Copyright (c) 2008 - 2010, Pascal Volk
+# See COPYING for distribution information.
+"""
+ VirtualMailManager.ext.postconf
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Wrapper class for Postfix's postconf.
+ Postconf instances can be used to read actual values of configuration
+ parameters or edit the value of a configuration parameter.
+
+ postconf.read(parameter) -> value
+ postconf.edit(parameter, value)
+"""
+
+import re
+from subprocess import Popen, PIPE
+
+from VirtualMailManager.errors import VMMError
+from VirtualMailManager.constants import VMM_ERROR
+
+_ = lambda msg: msg
+
+
+class Postconf(object):
+ """Wrapper class for Postfix's postconf."""
+ __slots__ = ('_bin', '_val')
+ _parameter_re = re.compile(r'^\w+$')
+ _variables_re = re.compile(r'\$\b\w+\b')
+
+ def __init__(self, postconf_bin):
+ """Creates a new Postconf instance.
+
+ Argument:
+
+ `postconf_bin` : str
+ absolute path to the Postfix postconf binary.
+ """
+ self._bin = postconf_bin
+ self._val = ''
+
+ def edit(self, parameter, value):
+ """Set the `parameter`'s value to `value`.
+
+ Arguments:
+
+ `parameter` : str
+ the name of a Postfix configuration parameter
+ `value` : str
+ the parameter's new value.
+ """
+ self._check_parameter(parameter)
+ stdout, stderr = Popen((self._bin, '-e', parameter + '=' + str(value)),
+ stderr=PIPE).communicate()
+ if stderr:
+ raise VMMError(stderr.strip(), VMM_ERROR)
+
+ def read(self, parameter, expand_vars=True):
+ """Returns the parameters value.
+
+ If expand_vars is True (default), all variables in the value will be
+ expanded:
+ e.g. mydestination: mail.example.com, localhost.example.com, localhost
+ Otherwise the value may contain one or more variables.
+ e.g. mydestination: $myhostname, localhost.$mydomain, localhost
+
+ Arguments:
+
+ `parameter` : str
+ the name of a Postfix configuration parameter.
+ `expand_vars` : bool
+ indicates if variables should be expanded or not, default True
+ """
+ self._check_parameter(parameter)
+ self._val = self._read(parameter)
+ if expand_vars:
+ self._expand_vars()
+ return self._val
+
+ def _check_parameter(self, parameter):
+ """Check that the `parameter` looks like a configuration parameter.
+ If not, a VMMError will be raised."""
+ if not self.__class__._parameter_re.match(parameter):
+ raise VMMError(_(u"The value '%s' doesn't look like a valid "
+ u"postfix configuration parameter name.") %
+ parameter, VMM_ERROR)
+
+ def _expand_vars(self):
+ """Expand the $variables in self._val to their values."""
+ while True:
+ pvars = set(self.__class__._variables_re.findall(self._val))
+ if not pvars:
+ break
+ if len(pvars) > 1:
+ self._expand_multi_vars(self._read_multi(pvars))
+ continue
+ pvars = pvars.pop()
+ self._val = self._val.replace(pvars, self._read(pvars[1:]))
+
+ def _expand_multi_vars(self, old_new):
+ """Replace all $vars in self._val with their values."""
+ for old, new in old_new.iteritems():
+ self._val = self._val.replace('$' + old, new)
+
+ def _read(self, parameter):
+ """Ask postconf for the value of a single configuration parameter."""
+ stdout, stderr = Popen([self._bin, '-h', parameter], stdout=PIPE,
+ stderr=PIPE).communicate()
+ if stderr:
+ raise VMMError(stderr.strip(), VMM_ERROR)
+ return stdout.strip()
+
+ def _read_multi(self, parameters):
+ """Ask postconf for multiple configuration parameters. Returns a dict
+ parameter: value items."""
+ cmd = [self._bin]
+ cmd.extend(parameter[1:] for parameter in parameters)
+ stdout, stderr = Popen(cmd, stdout=PIPE, stderr=PIPE).communicate()
+ if stderr:
+ raise VMMError(stderr.strip(), VMM_ERROR)
+ par_val = {}
+ for line in stdout.splitlines():
+ par, val = line.split(' = ')
+ par_val[par] = val
+ return par_val
+
+del _
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/handler.py Wed Jul 28 02:08:03 2010 +0000
@@ -0,0 +1,657 @@
+# -*- 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.
+"""
+
+import os
+import re
+
+from shutil import rmtree
+from subprocess import Popen, PIPE
+
+from pyPgSQL import PgSQL # python-pgsql - http://pypgsql.sourceforge.net
+
+from VirtualMailManager.account import Account
+from VirtualMailManager.alias import Alias
+from VirtualMailManager.aliasdomain import AliasDomain
+from VirtualMailManager.common import exec_ok
+from VirtualMailManager.config import Config as Cfg
+from VirtualMailManager.constants import \
+ ACCOUNT_EXISTS, ALIAS_EXISTS, CONF_NOFILE, CONF_NOPERM, CONF_WRONGPERM, \
+ DATABASE_ERROR, DOMAINDIR_GROUP_MISMATCH, DOMAIN_INVALID, \
+ FOUND_DOTS_IN_PATH, INVALID_ARGUMENT, MAILDIR_PERM_MISMATCH, \
+ NOT_EXECUTABLE, NO_SUCH_ACCOUNT, NO_SUCH_ALIAS, NO_SUCH_BINARY, \
+ NO_SUCH_DIRECTORY, NO_SUCH_RELOCATED, RELOCATED_EXISTS
+from VirtualMailManager.domain import Domain, get_gid
+from VirtualMailManager.emailaddress import EmailAddress
+from VirtualMailManager.errors import \
+ DomainError, NotRootError, PermissionError, VMMError
+from VirtualMailManager.mailbox import new as new_mailbox
+from VirtualMailManager.pycompat import any
+from VirtualMailManager.relocated import Relocated
+from VirtualMailManager.transport import Transport
+
+
+_ = lambda msg: msg
+
+CFG_FILE = 'vmm.cfg'
+CFG_PATH = '/root:/usr/local/etc:/etc'
+RE_DOMAIN_SEARCH = """^[a-z0-9-\.]+$"""
+TYPE_ACCOUNT = 0x1
+TYPE_ALIAS = 0x2
+TYPE_RELOCATED = 0x4
+OTHER_TYPES = {
+ TYPE_ACCOUNT: (_(u'an account'), ACCOUNT_EXISTS),
+ TYPE_ALIAS: (_(u'an alias'), ALIAS_EXISTS),
+ TYPE_RELOCATED: (_(u'a relocated user'), RELOCATED_EXISTS),
+}
+
+
+class Handler(object):
+ """Wrapper class to simplify the access on all the stuff from
+ VirtualMailManager"""
+ __slots__ = ('_cfg', '_cfg_fname', '_dbh', '_warnings')
+
+ def __init__(self, skip_some_checks=False):
+ """Creates a new Handler instance.
+
+ ``skip_some_checks`` : bool
+ When a derived class knows how to handle all checks this
+ argument may be ``True``. By default it is ``False`` and
+ all checks will be performed.
+
+ Throws a NotRootError if your uid is greater 0.
+ """
+ self._cfg_fname = ''
+ self._warnings = []
+ self._cfg = None
+ self._dbh = None
+
+ if os.geteuid():
+ raise NotRootError(_(u"You are not root.\n\tGood bye!\n"),
+ CONF_NOPERM)
+ if self._check_cfg_file():
+ self._cfg = Cfg(self._cfg_fname)
+ self._cfg.load()
+ if not skip_some_checks:
+ self._cfg.check()
+ self._chkenv()
+
+ def _find_cfg_file(self):
+ """Search the CFG_FILE in CFG_PATH.
+ Raise a VMMError when no vmm.cfg could be found.
+ """
+ for path in CFG_PATH.split(':'):
+ tmp = os.path.join(path, CFG_FILE)
+ if os.path.isfile(tmp):
+ self._cfg_fname = tmp
+ break
+ if not self._cfg_fname:
+ raise VMMError(_(u"Could not find '%(cfg_file)s' in: "
+ u"'%(cfg_path)s'") % {'cfg_file': CFG_FILE,
+ 'cfg_path': CFG_PATH}, CONF_NOFILE)
+
+ def _check_cfg_file(self):
+ """Checks the configuration file, returns bool"""
+ self._find_cfg_file()
+ fstat = os.stat(self._cfg_fname)
+ fmode = int(oct(fstat.st_mode & 0777))
+ if fmode % 100 and fstat.st_uid != fstat.st_gid or \
+ fmode % 10 and fstat.st_uid == fstat.st_gid:
+ raise PermissionError(_(u"wrong permissions for '%(file)s': "
+ u"%(perms)s\n`chmod 0600 %(file)s` would "
+ u"be great.") % {'file': self._cfg_fname,
+ 'perms': fmode}, CONF_WRONGPERM)
+ else:
+ return True
+
+ def _chkenv(self):
+ """Make sure our base_directory is a directory and that all
+ required executables exists and are executable.
+ If not, a VMMError will be raised"""
+ basedir = self._cfg.dget('misc.base_directory')
+ if not os.path.exists(basedir):
+ old_umask = os.umask(0006)
+ os.makedirs(basedir, 0771)
+ os.chown(basedir, 0, 0)
+ os.umask(old_umask)
+ elif not os.path.isdir(basedir):
+ raise VMMError(_(u"'%(path)s' is not a directory.\n(%(cfg_file)s: "
+ u"section 'misc', option 'base_directory')") %
+ {'path': basedir, 'cfg_file': self._cfg_fname},
+ NO_SUCH_DIRECTORY)
+ for opt, val in self._cfg.items('bin'):
+ try:
+ exec_ok(val)
+ except VMMError, err:
+ if err.code is NO_SUCH_BINARY:
+ raise VMMError(_(u"'%(binary)s' doesn't exist.\n"
+ u"(%(cfg_file)s: section 'bin', option "
+ u"'%(option)s')") % {'binary': val,
+ 'cfg_file': self._cfg_fname, 'option': opt},
+ err.code)
+ elif err.code is NOT_EXECUTABLE:
+ raise VMMError(_(u"'%(binary)s' is not executable.\n"
+ u"(%(cfg_file)s: section 'bin', option "
+ u"'%(option)s')") % {'binary': val,
+ 'cfg_file': self._cfg_fname, 'option': opt},
+ err.code)
+ else:
+ raise
+
+ def _db_connect(self):
+ """Creates a pyPgSQL.PgSQL.connection instance."""
+ if self._dbh is None or (isinstance(self._dbh, PgSQL.Connection) and
+ not self._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()
+ except PgSQL.libpq.DatabaseError, err:
+ raise VMMError(str(err), DATABASE_ERROR)
+
+ def _chk_other_address_types(self, address, exclude):
+ """Checks if the EmailAddress *address* is known as `TYPE_ACCOUNT`,
+ `TYPE_ALIAS` or `TYPE_RELOCATED`, but not as the `TYPE_*` specified
+ by *exclude*. If the *address* is known as one of the `TYPE_*`s
+ the according `TYPE_*` constant will be returned. Otherwise 0 will
+ be returned."""
+ assert exclude in (TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED) and \
+ isinstance(address, EmailAddress)
+ if exclude is not TYPE_ACCOUNT:
+ account = Account(self._dbh, address)
+ if account:
+ return TYPE_ACCOUNT
+ if exclude is not TYPE_ALIAS:
+ alias = Alias(self._dbh, address)
+ if alias:
+ return TYPE_ALIAS
+ if exclude is not TYPE_RELOCATED:
+ relocated = Relocated(self._dbh, address)
+ if relocated:
+ return TYPE_RELOCATED
+ return 0
+
+ def _is_other_address(self, address, exclude):
+ """Checks if *address* is known for an Account (TYPE_ACCOUNT),
+ Alias (TYPE_ALIAS) or Relocated (TYPE_RELOCATED), except for
+ *exclude*. Returns `False` if the address is not known for other
+ types.
+
+ Raises a `VMMError` if the address is known.
+ """
+ other = self._chk_other_address_types(address, exclude)
+ if not other:
+ return False
+ msg = _(u"There is already %(a_type)s with the address '%(address)s'.")
+ raise VMMError(msg % {'a_type': OTHER_TYPES[other][0],
+ 'address': address}, OTHER_TYPES[other][1])
+
+ def _get_account(self, address):
+ """Return an Account instances for the given address (str)."""
+ address = EmailAddress(address)
+ self._db_connect()
+ return Account(self._dbh, address)
+
+ def _get_alias(self, address):
+ """Return an Alias instances for the given address (str)."""
+ address = EmailAddress(address)
+ self._db_connect()
+ return Alias(self._dbh, address)
+
+ def _get_relocated(self, address):
+ """Return a Relocated instances for the given address (str)."""
+ address = EmailAddress(address)
+ self._db_connect()
+ return Relocated(self._dbh, address)
+
+ def _get_domain(self, domainname):
+ """Return a Domain instances for the given domain name (str)."""
+ self._db_connect()
+ return Domain(self._dbh, domainname)
+
+ def _get_disk_usage(self, directory):
+ """Estimate file space usage for the given directory.
+
+ Keyword arguments:
+ directory -- the directory to summarize recursively disk usage for
+ """
+ if self._isdir(directory):
+ return Popen([self._cfg.dget('bin.du'), "-hs", directory],
+ stdout=PIPE).communicate()[0].split('\t')[0]
+ else:
+ return 0
+
+ def _isdir(self, directory):
+ """Check if `directory` is a directory. Returns bool.
+ When `directory` isn't a directory, a warning will be appended to
+ _warnings."""
+ isdir = os.path.isdir(directory)
+ if not isdir:
+ self._warnings.append(_('No such directory: %s') % directory)
+ return isdir
+
+ def _make_domain_dir(self, domain):
+ """Create a directory for the `domain` and its accounts."""
+ cwd = os.getcwd()
+ hashdir, domdir = domain.directory.split(os.path.sep)[-2:]
+ os.chdir(self._cfg.dget('misc.base_directory'))
+ if not os.path.isdir(hashdir):
+ os.mkdir(hashdir, 0711)
+ os.chown(hashdir, 0, 0)
+ os.mkdir(os.path.join(hashdir, domdir),
+ self._cfg.dget('domain.directory_mode'))
+ os.chown(domain.directory, 0, domain.gid)
+ os.chdir(cwd)
+
+ def _make_home(self, account):
+ """Create a home directory for the new Account *account*."""
+ os.umask(0007)
+ os.chdir(account.domain_directory)
+ os.mkdir('%s' % account.uid, self._cfg.dget('account.directory_mode'))
+ os.chown('%s' % account.uid, account.uid, account.gid)
+
+ def _delete_home(self, domdir, uid, gid):
+ """Delete a user's home directory."""
+ if uid > 0 and gid > 0:
+ userdir = '%s' % uid
+ if userdir.count('..') or domdir.count('..'):
+ raise VMMError(_(u'Found ".." in home directory path.'),
+ FOUND_DOTS_IN_PATH)
+ if os.path.isdir(domdir):
+ os.chdir(domdir)
+ if os.path.isdir(userdir):
+ mdstat = os.stat(userdir)
+ if (mdstat.st_uid, mdstat.st_gid) != (uid, gid):
+ raise VMMError(_(u'Detected owner/group mismatch in '
+ u'home directory.'),
+ MAILDIR_PERM_MISMATCH)
+ rmtree(userdir, ignore_errors=True)
+ else:
+ raise VMMError(_(u"No such directory: %s") %
+ os.path.join(domdir, userdir),
+ NO_SUCH_DIRECTORY)
+
+ def _delete_domain_dir(self, domdir, gid):
+ """Delete a domain's directory."""
+ if gid > 0:
+ if not self._isdir(domdir):
+ return
+ basedir = self._cfg.dget('misc.base_directory')
+ domdirdirs = domdir.replace(basedir + '/', '').split('/')
+ domdirparent = os.path.join(basedir, domdirdirs[0])
+ if basedir.count('..') or domdir.count('..'):
+ raise VMMError(_(u'Found ".." in domain directory path.'),
+ FOUND_DOTS_IN_PATH)
+ if os.path.isdir(domdirparent):
+ os.chdir(domdirparent)
+ if os.lstat(domdirdirs[1]).st_gid != gid:
+ raise VMMError(_(u'Detected group mismatch in domain '
+ u'directory.'), DOMAINDIR_GROUP_MISMATCH)
+ rmtree(domdirdirs[1], ignore_errors=True)
+
+ def has_warnings(self):
+ """Checks if warnings are present, returns bool."""
+ return bool(len(self._warnings))
+
+ def get_warnings(self):
+ """Returns a list with all available warnings and resets all
+ warnings.
+ """
+ ret_val = self._warnings[:]
+ del self._warnings[:]
+ return ret_val
+
+ def cfg_dget(self, option):
+ """Get the configured value of the *option* (section.option).
+ When the option was not configured its default value will be
+ returned."""
+ return self._cfg.dget(option)
+
+ def cfg_pget(self, option):
+ """Get the configured value of the *option* (section.option)."""
+ return self._cfg.pget(option)
+
+ def cfg_install(self):
+ """Installs the cfg_dget method as ``cfg_dget`` into the built-in
+ namespace."""
+ import __builtin__
+ assert 'cfg_dget' not in __builtin__.__dict__
+ __builtin__.__dict__['cfg_dget'] = self._cfg.dget
+
+ def domain_add(self, domainname, transport=None):
+ """Wrapper around Domain.set_transport() and Domain.save()"""
+ dom = self._get_domain(domainname)
+ if transport is None:
+ dom.set_transport(Transport(self._dbh,
+ transport=self._cfg.dget('misc.transport')))
+ else:
+ dom.set_transport(Transport(self._dbh, transport=transport))
+ dom.set_directory(self._cfg.dget('misc.base_directory'))
+ dom.save()
+ self._make_domain_dir(dom)
+
+ def domain_transport(self, domainname, transport, force=None):
+ """Wrapper around Domain.update_transport()"""
+ if force is not None and force != 'force':
+ raise DomainError(_(u"Invalid argument: '%s'") % force,
+ INVALID_ARGUMENT)
+ dom = self._get_domain(domainname)
+ trsp = Transport(self._dbh, transport=transport)
+ if force is None:
+ dom.update_transport(trsp)
+ else:
+ dom.update_transport(trsp, force=True)
+
+ def domain_delete(self, domainname, force=None):
+ """Wrapper around Domain.delete()"""
+ if force and force not in ('deluser', 'delalias', 'delall'):
+ raise DomainError(_(u"Invalid argument: '%s'") % force,
+ INVALID_ARGUMENT)
+ dom = self._get_domain(domainname)
+ gid = dom.gid
+ domdir = dom.directory
+ if self._cfg.dget('domain.force_deletion') or force == 'delall':
+ dom.delete(True, True)
+ elif force == 'deluser':
+ dom.delete(deluser=True)
+ elif force == 'delalias':
+ dom.delete(delalias=True)
+ else:
+ dom.delete()
+ if self._cfg.dget('domain.delete_directory'):
+ self._delete_domain_dir(domdir, gid)
+
+ def domain_info(self, domainname, details=None):
+ """Wrapper around Domain.get_info(), Domain.get_accounts(),
+ Domain.get_aliase_names(), Domain.get_aliases() and
+ Domain.get_relocated."""
+ if details not in [None, 'accounts', 'aliasdomains', 'aliases', 'full',
+ 'relocated']:
+ raise VMMError(_(u'Invalid argument: “%s”') % details,
+ INVALID_ARGUMENT)
+ dom = self._get_domain(domainname)
+ dominfo = dom.get_info()
+ if dominfo['domainname'].startswith('xn--'):
+ dominfo['domainname'] += ' (%s)' % \
+ dominfo['domainname'].decode('idna')
+ if details is None:
+ return dominfo
+ elif details == 'accounts':
+ return (dominfo, dom.get_accounts())
+ elif details == 'aliasdomains':
+ return (dominfo, dom.get_aliase_names())
+ elif details == 'aliases':
+ return (dominfo, dom.get_aliases())
+ elif details == 'relocated':
+ return(dominfo, dom.get_relocated())
+ else:
+ return (dominfo, dom.get_aliase_names(), dom.get_accounts(),
+ dom.get_aliases(), dom.get_relocated())
+
+ def aliasdomain_add(self, aliasname, domainname):
+ """Adds an alias domain to the domain.
+
+ Arguments:
+
+ `aliasname` : basestring
+ The name of the alias domain
+ `domainname` : basestring
+ The name of the target domain
+ """
+ dom = self._get_domain(domainname)
+ alias_dom = AliasDomain(self._dbh, aliasname)
+ alias_dom.set_destination(dom)
+ alias_dom.save()
+
+ def aliasdomain_info(self, aliasname):
+ """Returns a dict (keys: "alias" and "domain") with the names of
+ the alias domain and its primary domain."""
+ self._db_connect()
+ alias_dom = AliasDomain(self._dbh, aliasname)
+ return alias_dom.info()
+
+ def aliasdomain_switch(self, aliasname, domainname):
+ """Modifies the target domain of an existing alias domain.
+
+ Arguments:
+
+ `aliasname` : basestring
+ The name of the alias domain
+ `domainname` : basestring
+ The name of the new target domain
+ """
+ dom = self._get_domain(domainname)
+ alias_dom = AliasDomain(self._dbh, aliasname)
+ alias_dom.set_destination(dom)
+ alias_dom.switch()
+
+ def aliasdomain_delete(self, aliasname):
+ """Deletes the given alias domain.
+
+ Argument:
+
+ `aliasname` : basestring
+ The name of the alias domain
+ """
+ self._db_connect()
+ alias_dom = AliasDomain(self._dbh, aliasname)
+ alias_dom.delete()
+
+ def domain_list(self, pattern=None):
+ """Wrapper around function search() from module Domain."""
+ from VirtualMailManager.domain import search
+ like = False
+ if pattern and (pattern.startswith('%') or pattern.endswith('%')):
+ like = True
+ if not re.match(RE_DOMAIN_SEARCH, pattern.strip('%')):
+ raise VMMError(_(u"The pattern '%s' contains invalid "
+ u"characters.") % pattern, DOMAIN_INVALID)
+ self._db_connect()
+ return search(self._dbh, pattern=pattern, like=like)
+
+ def user_add(self, emailaddress, password):
+ """Wrapper around Account.set_password() and Account.save()."""
+ acc = self._get_account(emailaddress)
+ acc.set_password(password)
+ acc.save()
+ oldpwd = os.getcwd()
+ self._make_home(acc)
+ mailbox = new_mailbox(acc)
+ mailbox.create()
+ folders = self._cfg.dget('mailbox.folders').split(':')
+ if any(folders):
+ bad = mailbox.add_boxes(folders,
+ self._cfg.dget('mailbox.subscribe'))
+ if bad:
+ self._warnings.append(_(u"Skipped mailbox folders:") +
+ '\n\t- ' + '\n\t- '.join(bad))
+ os.chdir(oldpwd)
+
+ def alias_add(self, aliasaddress, *targetaddresses):
+ """Creates a new `Alias` entry for the given *aliasaddress* with
+ the given *targetaddresses*."""
+ alias = self._get_alias(aliasaddress)
+ destinations = [EmailAddress(address) for address in targetaddresses]
+ warnings = []
+ destinations = alias.add_destinations(destinations, warnings)
+ if warnings:
+ self._warnings.append(_('Ignored destination addresses:'))
+ self._warnings.extend((' * %s' % w for w in warnings))
+ for destination in destinations:
+ if get_gid(self._dbh, destination.domainname) and \
+ not self._chk_other_address_types(destination, TYPE_RELOCATED):
+ self._warnings.append(_(u"The destination account/alias '%s' "
+ u"doesn't exist.") % destination)
+
+ def user_delete(self, emailaddress, force=None):
+ """Wrapper around Account.delete(...)"""
+ if force not in (None, 'delalias'):
+ raise VMMError(_(u"Invalid argument: '%s'") % force,
+ INVALID_ARGUMENT)
+ acc = self._get_account(emailaddress)
+ if not acc:
+ raise VMMError(_(u"The account '%s' doesn't exist.") %
+ acc.address, NO_SUCH_ACCOUNT)
+ uid = acc.uid
+ gid = acc.gid
+ dom_dir = acc.domain_directory
+ acc_dir = acc.home
+ acc.delete(bool(force))
+ if self._cfg.dget('account.delete_directory'):
+ try:
+ self._delete_home(dom_dir, uid, gid)
+ except VMMError, err:
+ if err.code in (FOUND_DOTS_IN_PATH, MAILDIR_PERM_MISMATCH,
+ 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_dir, 'reason': err.msg}
+ self._warnings.append(warning)
+ else:
+ raise
+
+ def alias_info(self, aliasaddress):
+ """Returns an iterator object for all destinations (`EmailAddress`
+ instances) for the `Alias` with the given *aliasaddress*."""
+ alias = self._get_alias(aliasaddress)
+ if alias:
+ return alias.get_destinations()
+ if not self._is_other_address(alias.address, TYPE_ALIAS):
+ raise VMMError(_(u"The alias '%s' doesn't exist.") %
+ alias.address, NO_SUCH_ALIAS)
+
+ def alias_delete(self, aliasaddress, targetaddress=None):
+ """Deletes the `Alias` *aliasaddress* with all its destinations from
+ the database. If *targetaddress* is not ``None``, only this
+ destination will be removed from the alias."""
+ alias = self._get_alias(aliasaddress)
+ if targetaddress is None:
+ alias.delete()
+ else:
+ alias.del_destination(EmailAddress(targetaddress))
+
+ def user_info(self, emailaddress, details=None):
+ """Wrapper around Account.get_info(...)"""
+ if details not in (None, 'du', 'aliases', 'full'):
+ raise VMMError(_(u"Invalid argument: '%s'") % details,
+ INVALID_ARGUMENT)
+ acc = self._get_account(emailaddress)
+ if not acc:
+ if not self._is_other_address(acc.address, TYPE_ACCOUNT):
+ raise VMMError(_(u"The account '%s' doesn't exist.") %
+ acc.address, NO_SUCH_ACCOUNT)
+ info = acc.get_info()
+ if self._cfg.dget('account.disk_usage') or details in ('du', 'full'):
+ path = os.path.join(acc.home, acc.mail_location.directory)
+ info['disk usage'] = self._get_disk_usage(path)
+ if details in (None, 'du'):
+ return info
+ if details in ('aliases', 'full'):
+ return (info, acc.get_aliases())
+ return info
+
+ def user_by_uid(self, uid):
+ """Search for an Account by its *uid*.
+ Returns a dict (address, uid and gid) if a user could be found."""
+ from VirtualMailManager.account import get_account_by_uid
+ self._db_connect()
+ return get_account_by_uid(uid, self._dbh)
+
+ def user_password(self, emailaddress, password):
+ """Wrapper for Account.modify('password' ...)."""
+ if not isinstance(password, basestring) or not password:
+ raise VMMError(_(u"Could not accept password: '%s'") % password,
+ INVALID_ARGUMENT)
+ acc = self._get_account(emailaddress)
+ if not acc:
+ raise VMMError(_(u"The account '%s' doesn't exist.") %
+ acc.address, NO_SUCH_ACCOUNT)
+ acc.modify('password', password)
+
+ def user_name(self, emailaddress, name):
+ """Wrapper for Account.modify('name', ...)."""
+ if not isinstance(name, basestring) or not name:
+ raise VMMError(_(u"Could not accept name: '%s'") % name,
+ INVALID_ARGUMENT)
+ acc = self._get_account(emailaddress)
+ if not acc:
+ raise VMMError(_(u"The account '%s' doesn't exist.") %
+ acc.address, NO_SUCH_ACCOUNT)
+ acc.modify('name', name)
+
+ def user_transport(self, emailaddress, transport):
+ """Wrapper for Account.modify('transport', ...)."""
+ if not isinstance(transport, basestring) or not transport:
+ raise VMMError(_(u"Could not accept transport: '%s'") % transport,
+ INVALID_ARGUMENT)
+ acc = self._get_account(emailaddress)
+ if not acc:
+ raise VMMError(_(u"The account '%s' doesn't exist.") %
+ acc.address, NO_SUCH_ACCOUNT)
+ acc.modify('transport', transport)
+
+ def user_disable(self, emailaddress, service=None):
+ """Wrapper for Account.disable(service)"""
+ if service not in (None, 'all', 'imap', 'pop3', 'smtp', 'sieve'):
+ raise VMMError(_(u"Could not accept service: '%s'") % service,
+ INVALID_ARGUMENT)
+ acc = self._get_account(emailaddress)
+ if not acc:
+ raise VMMError(_(u"The account '%s' doesn't exist.") %
+ acc.address, NO_SUCH_ACCOUNT)
+ acc.disable(service)
+
+ def user_enable(self, emailaddress, service=None):
+ """Wrapper for Account.enable(service)"""
+ if service not in (None, 'all', 'imap', 'pop3', 'smtp', 'sieve'):
+ raise VMMError(_(u"Could not accept service: '%s'") % service,
+ INVALID_ARGUMENT)
+ acc = self._get_account(emailaddress)
+ if not acc:
+ raise VMMError(_(u"The account '%s' doesn't exist.") %
+ acc.address, NO_SUCH_ACCOUNT)
+ acc.enable(service)
+
+ def relocated_add(self, emailaddress, targetaddress):
+ """Creates a new `Relocated` entry in the database. If there is
+ already a relocated user with the given *emailaddress*, only the
+ *targetaddress* for the relocated user will be updated."""
+ relocated = self._get_relocated(emailaddress)
+ relocated.set_destination(EmailAddress(targetaddress))
+
+ def relocated_info(self, emailaddress):
+ """Returns the target address of the relocated user with the given
+ *emailaddress*."""
+ relocated = self._get_relocated(emailaddress)
+ if relocated:
+ return relocated.get_info()
+ if not self._is_other_address(relocated.address, TYPE_RELOCATED):
+ raise VMMError(_(u"The relocated user '%s' doesn't exist.") %
+ relocated.address, NO_SUCH_RELOCATED)
+
+ def relocated_delete(self, emailaddress):
+ """Deletes the relocated user with the given *emailaddress* from
+ the database."""
+ relocated = self._get_relocated(emailaddress)
+ relocated.delete()
+
+del _
--- a/VirtualMailManager/mailbox.py Wed Jul 28 01:03:56 2010 +0000
+++ b/VirtualMailManager/mailbox.py Wed Jul 28 02:08:03 2010 +0000
@@ -1,7 +1,6 @@
# -*- coding: UTF-8 -*-
# Copyright (c) 2010, Pascal Volk
# See COPYING for distribution information.
-
"""
VirtualMailManager.mailbox
~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -15,7 +14,7 @@
from binascii import a2b_base64, b2a_base64
from subprocess import Popen, PIPE
-from VirtualMailManager.Account import Account
+from VirtualMailManager.account import Account
from VirtualMailManager.common import is_dir
from VirtualMailManager.errors import VMMError
from VirtualMailManager.constants import VMM_ERROR
--- a/VirtualMailManager/maillocation.py Wed Jul 28 01:03:56 2010 +0000
+++ b/VirtualMailManager/maillocation.py Wed Jul 28 02:08:03 2010 +0000
@@ -1,7 +1,6 @@
# -*- coding: UTF-8 -*-
# Copyright (c) 2008 - 2010, Pascal Volk
# See COPYING for distribution information.
-
"""
VirtualMailManager.maillocation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--- a/VirtualMailManager/password.py Wed Jul 28 01:03:56 2010 +0000
+++ b/VirtualMailManager/password.py Wed Jul 28 02:08:03 2010 +0000
@@ -1,9 +1,9 @@
# -*- coding: UTF-8 -*-
# Copyright (c) 2010, Pascal Volk
# See COPYING for distribution information.
-
"""
VirtualMailManager.password
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
VirtualMailManager's password module to generate password hashes from
passwords or random passwords. This module provides following
@@ -24,7 +24,7 @@
from VirtualMailManager.pycompat import hashlib
from VirtualMailManager import ENCODING
-from VirtualMailManager.EmailAddress import EmailAddress
+from VirtualMailManager.emailaddress import EmailAddress
from VirtualMailManager.common import get_unicode, version_str
from VirtualMailManager.constants import VMM_ERROR
from VirtualMailManager.errors import VMMError
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/relocated.py Wed Jul 28 02:08:03 2010 +0000
@@ -0,0 +1,114 @@
+# -*- coding: UTF-8 -*-
+# Copyright (c) 2008 - 2010, Pascal Volk
+# See COPYING for distribution information.
+"""
+ VirtualMailManager.relocated
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Virtual Mail Manager's Relocated class to handle relocated users.
+"""
+
+from VirtualMailManager.domain import get_gid
+from VirtualMailManager.emailaddress import EmailAddress
+from VirtualMailManager.errors import RelocatedError as RErr
+from VirtualMailManager.constants import NO_SUCH_DOMAIN, \
+ NO_SUCH_RELOCATED, RELOCATED_ADDR_DEST_IDENTICAL, RELOCATED_EXISTS
+
+
+_ = lambda msg: msg
+
+
+class Relocated(object):
+ """Class to handle e-mail addresses of relocated users."""
+ __slots__ = ('_addr', '_dest', '_gid', '_dbh')
+
+ def __init__(self, dbh, address):
+ """Creates a new *Relocated* instance. The ``address`` is the
+ old e-mail address of the user.
+
+ Use `setDestination()` to set/update the new address, where the
+ user has moved to.
+
+ """
+ assert isinstance(address, EmailAddress)
+ self._addr = address
+ self._dbh = dbh
+ self._gid = get_gid(self._dbh, self._addr.domainname)
+ if not self._gid:
+ raise RErr(_(u"The domain %r doesn't exist.") %
+ self._addr.domainname, NO_SUCH_DOMAIN)
+ self._dest = None
+
+ self.__load()
+
+ def __nonzero__(self):
+ """Returns `True` if the Relocated is known, `False` if it's new."""
+ return self._dest is not None
+
+ def __load(self):
+ """Loads the destination address from the database into the
+ `_dest` attribute.
+
+ """
+ dbc = self._dbh.cursor()
+ dbc.execute('SELECT destination FROM relocated WHERE gid = %s AND '
+ 'address = %s', self._gid, self._addr.localpart)
+ destination = dbc.fetchone()
+ dbc.close()
+ if destination:
+ self._dest = EmailAddress(destination[0])
+
+ @property
+ def address(self):
+ """The Relocated's EmailAddress instance."""
+ return self._addr
+
+ def set_destination(self, destination):
+ """Sets/updates the new address of the relocated user."""
+ update = False
+ assert isinstance(destination, EmailAddress)
+ if self._addr == destination:
+ raise RErr(_(u'Address and destination are identical.'),
+ RELOCATED_ADDR_DEST_IDENTICAL)
+ if self._dest:
+ if self._dest == destination:
+ raise RErr(_(u"The relocated user '%s' already exists.") %
+ self._addr, RELOCATED_EXISTS)
+ else:
+ self._dest = destination
+ update = True
+ else:
+ self._dest = destination
+
+ dbc = self._dbh.cursor()
+ if not update:
+ dbc.execute('INSERT INTO relocated VALUES (%s, %s, %s)',
+ self._gid, self._addr.localpart, str(self._dest))
+ else:
+ dbc.execute('UPDATE relocated SET destination = %s WHERE gid = %s '
+ 'AND address = %s', str(self._dest), self._gid,
+ self._addr.localpart)
+ self._dbh.commit()
+ dbc.close()
+
+ def get_info(self):
+ """Returns the address to which mails should be sent."""
+ if not self._dest:
+ raise RErr(_(u"The relocated user '%s' doesn't exist.") %
+ self._addr, NO_SUCH_RELOCATED)
+ return self._dest
+
+ def delete(self):
+ """Deletes the relocated entry from the database."""
+ if not self._dest:
+ raise RErr(_(u"The relocated user '%s' doesn't exist.") %
+ self._addr, NO_SUCH_RELOCATED)
+ dbc = self._dbh.cursor()
+ dbc.execute('DELETE FROM relocated WHERE gid = %s AND address = %s',
+ self._gid, self._addr.localpart)
+ if dbc.rowcount > 0:
+ self._dbh.commit()
+ dbc.close()
+ self._dest = None
+
+del _
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/transport.py Wed Jul 28 02:08:03 2010 +0000
@@ -0,0 +1,96 @@
+# -*- coding: UTF-8 -*-
+# Copyright (c) 2008 - 2010, Pascal Volk
+# See COPYING for distribution information.
+"""
+ VirtualMailManager.transport
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Virtual Mail Manager's Transport class to manage the transport for
+ domains and accounts.
+"""
+
+from VirtualMailManager.constants import UNKNOWN_TRANSPORT_ID
+from VirtualMailManager.errors import TransportError
+from VirtualMailManager.pycompat import any
+
+
+class Transport(object):
+ """A wrapper class that provides access to the transport table"""
+ __slots__ = ('_tid', '_transport', '_dbh')
+
+ def __init__(self, dbh, tid=None, transport=None):
+ """Creates a new Transport instance.
+
+ Either tid or transport must be specified. When both arguments
+ are given, tid will be used.
+
+ Keyword arguments:
+ dbh -- a pyPgSQL.PgSQL.connection
+ tid -- the id of a transport (int/long)
+ transport -- the value of the transport (str)
+
+ """
+ self._dbh = dbh
+ assert any((tid, transport))
+ if tid:
+ assert not isinstance(tid, bool) and isinstance(tid, (int, long))
+ self._tid = tid
+ self._loadByID()
+ else:
+ assert isinstance(transport, basestring)
+ self._transport = transport
+ self._loadByName()
+
+ @property
+ def tid(self):
+ """The transport's unique ID."""
+ return self._tid
+
+ @property
+ def transport(self):
+ """The transport's value, ex: 'dovecot:'"""
+ return self._transport
+
+ def __eq__(self, other):
+ if isinstance(other, self.__class__):
+ return self._tid == other.tid
+ return NotImplemented
+
+ def __ne__(self, other):
+ if isinstance(other, self.__class__):
+ return self._tid != other.tid
+ return NotImplemented
+
+ def __str__(self):
+ return self._transport
+
+ def _loadByID(self):
+ dbc = self._dbh.cursor()
+ dbc.execute('SELECT transport FROM transport WHERE tid=%s', self._tid)
+ result = dbc.fetchone()
+ dbc.close()
+ if result:
+ self._transport = result[0]
+ else:
+ raise TransportError(_(u'Unknown tid specified.'),
+ UNKNOWN_TRANSPORT_ID)
+
+ def _loadByName(self):
+ dbc = self._dbh.cursor()
+ dbc.execute('SELECT tid FROM transport WHERE transport = %s',
+ self._transport)
+ result = dbc.fetchone()
+ dbc.close()
+ if result:
+ self._tid = result[0]
+ else:
+ self._save()
+
+ def _save(self):
+ dbc = self._dbh.cursor()
+ dbc.execute("SELECT nextval('transport_id')")
+ self._tid = dbc.fetchone()[0]
+ dbc.execute('INSERT INTO transport VALUES (%s, %s)', self._tid,
+ self._transport)
+ self._dbh.commit()
+ dbc.close()