# HG changeset patch # User Pascal Volk # Date 1280282883 0 # Node ID 011066435e6f22fa5df1a9db6537bfe059c27247 # Parent f4956b4ceba1e75ce24c00725cea7607478424f4 VMM/*: Made all modules names lowercase, adjusted imports. diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/Account.py --- 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 diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/Alias.py --- 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 diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/AliasDomain.py --- 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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/Config.py --- 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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/Domain.py --- 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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/EmailAddress.py --- 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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/Handler.py --- 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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/Relocated.py --- 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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/Transport.py --- 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() diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/__init__.py --- 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 """ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/account.py --- /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 diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/alias.py --- /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 diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/aliasdomain.py --- /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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/cli/Config.py --- 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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/cli/Handler.py --- 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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/cli/__init__.py --- 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 ~~~~~~~~~~~~~~~~~~~~~~ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/cli/config.py --- /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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/cli/handler.py --- /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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/common.py --- 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 """ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/config.py --- /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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/domain.py --- /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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/emailaddress.py --- /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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/errors.py --- 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 diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/ext/Postconf.py --- 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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/ext/postconf.py --- /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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/handler.py --- /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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/mailbox.py --- 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 diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/maillocation.py --- 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/password.py --- 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 diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/relocated.py --- /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 _ diff -r f4956b4ceba1 -r 011066435e6f VirtualMailManager/transport.py --- /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()