--- a/VirtualMailManager/Account.py Tue Apr 13 18:11:52 2010 +0000
+++ b/VirtualMailManager/Account.py Thu Apr 15 03:05:01 2010 +0000
@@ -2,14 +2,22 @@
# Copyright (c) 2007 - 2010, Pascal Volk
# See COPYING for distribution information.
-"""Virtual Mail Manager's Account class to manage e-mail accounts."""
+"""
+ VirtualMailManager.Account
-import VirtualMailManager.constants.ERROR as ERR
+ Virtual Mail Manager's Account class to manage e-mail accounts.
+"""
+
from VirtualMailManager.Domain import Domain
from VirtualMailManager.EmailAddress import EmailAddress
-from VirtualMailManager.errors import AccountError as AccE
+from VirtualMailManager.Transport import Transport
+from VirtualMailManager.constants.ERROR import \
+ ACCOUNT_EXISTS, ACCOUNT_MISSING_PASSWORD, ALIAS_EXISTS, ALIAS_PRESENT, \
+ INVALID_AGUMENT, NO_SUCH_ACCOUNT, NO_SUCH_DOMAIN, RELOCATED_EXISTS, \
+ UNKNOWN_MAILLOCATION_NAME, UNKNOWN_SERVICE
+from VirtualMailManager.errors import AccountError as AErr
from VirtualMailManager.maillocation import MailLocation, known_format
-from VirtualMailManager.Transport import Transport
+from VirtualMailManager.pycompat import all
_ = lambda msg: msg
@@ -17,170 +25,267 @@
class Account(object):
"""Class to manage e-mail accounts."""
- __slots__ = ('_addr', '_base', '_gid', '_mid', '_passwd', '_tid', '_uid',
+ __slots__ = ('_addr', '_domain', '_mid', '_new', '_passwd', '_tid', '_uid',
'_dbh')
- def __init__(self, dbh, address, password=None):
+ 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` : basestring
+ 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._base = None
- if isinstance(address, EmailAddress):
- self._addr = address
- else:
- raise TypeError("Argument 'address' is not an EmailAddress")
+ 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._gid = 0
self._mid = 0
self._tid = 0
- self._passwd = password
- self._setAddr()
- self._exists()
- from VirtualMailManager.Handler import Handler
- if self._uid < 1 and Handler.aliasExists(self._dbh, self._addr):
- # TP: Hm, what quotation marks should be used?
- # If you are unsure have a look at:
- # http://en.wikipedia.org/wiki/Quotation_mark,_non-English_usage
- raise AccE(_(u"There is already an alias with the address “%s”.") %
- self._addr, ERR.ALIAS_EXISTS)
- if self._uid < 1 and Handler.relocatedExists(self._dbh, self._addr):
- raise AccE(
- _(u"There is already a relocated user with the address “%s”.") %
- self._addr, ERR.RELOCATED_EXISTS)
+ self._passwd = None
+ self._new = True
+ self._load()
- def _exists(self):
+ 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._gid, self._addr.localpart)
+ self._domain.gid, self._addr.localpart)
result = dbc.fetchone()
dbc.close()
- if result is not None:
+ if result:
self._uid, self._mid, self._tid = result
- return True
- else:
- return False
+ self._new = False
- def _setAddr(self):
- dom = Domain(self._dbh, self._addr.domainname)
- self._gid = dom.gid
- if self._gid == 0:
- raise AccE(_(u"The domain “%s” doesn't exist.") %
- self._addr.domainname, ERR.NO_SUCH_DOMAIN)
- self._base = dom.directory
- self._tid = dom.transport.tid
-
- def _setID(self):
+ 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):
- if not known_format(maillocation):
- raise AccE(_(u'Unknown mail_location mailbox format: %r') %
- maillocation, ERR.UNKNOWN_MAILLOCATION_NAME)
- self._setID()
+ """Check and set different attributes - before we store the
+ information in the database."""
+ if not known_format(maillocation):
+ raise AErr(_(u'Unknown mail_location mailbox format: %r') %
+ maillocation, UNKNOWN_MAILLOCATION_NAME)
self._mid = MailLocation(format=maillocation).mid
+ if not self._tid:
+ self._tid = self._domain.tid
+ self._set_uid()
- def _switchState(self, state, dcvers, service):
- if not isinstance(state, bool):
- return False
- if not service in (None, 'all', 'imap', 'pop3', 'sieve', 'smtp'):
- raise AccE(_(u"Unknown service “%s”.") % service,
- ERR.UNKNOWN_SERVICE)
- if self._uid < 1:
- raise AccE(_(u"The account “%s” doesn't exist.") % self._addr,
- ERR.NO_SUCH_ACCOUNT)
+ def _switch_state(self, state, dcvers, 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 dcvers > 11:
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)
+ self._uid)
elif service == 'sieve':
sql = 'UPDATE users SET %s = %s WHERE uid = %d' % (sieve_col,
- state, self._uid)
+ 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}
+ %(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 __aliaseCount(self):
+ def _count_aliases(self):
+ """Count all alias addresses where the destination address is the
+ address of the Account."""
dbc = self._dbh.cursor()
- q = "SELECT COUNT(destination) FROM alias WHERE destination = '%s'"\
- % self._addr
- dbc.execute(q)
+ 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 setPassword(self, password):
- self._passwd = password
+ 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 domain_directory(self):
+ """The directory of the domain the Account belongs to."""
+ return self._domain.directory
- def getUID(self):
+ @property
+ def gid(self):
+ """The Account's group ID."""
+ return self._domain.gid
+
+ @property
+ def home(self):
+ """The Account's home directory."""
+ return '%s/%s' % (self._domain.directory, self._uid)
+
+ @property
+ def uid(self):
+ """The Account's unique ID."""
return self._uid
- def getGID(self):
- return self._gid
+ 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 hashed password for the new Account."""
+ self._passwd = password
- def getDir(self, directory):
- if directory == 'domain':
- return '%s' % self._base
- elif directory == 'home':
- return '%s/%i' % (self._base, self._uid)
+ 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._tid = Transport(self._dbh, transport=transport).tid
def enable(self, dcvers, service=None):
- self._switchState(True, dcvers, service)
+ """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:
+
+ `dcvers` : int
+ The concatenated major and minor version number from
+ `dovecot --version`.
+ `service` : basestring
+ The name of a service ('imap', 'pop3', 'smtp', 'sieve'), 'all'
+ or `None`.
+ """
+ self._switch_state(True, dcvers, service)
def disable(self, dcvers, service=None):
- self._switchState(False, dcvers, service)
+ """Disable a/all service/s for the Account.
+
+ For more information see: Account.enable()."""
+ self._switch_state(False, dcvers, service)
def save(self, maillocation, dcvers, smtp, pop3, imap, sieve):
- if self._uid < 1:
- if dcvers > 11:
- sieve_col = 'sieve'
- else:
- sieve_col = 'managesieve'
- self._prepare(maillocation)
- 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, self._passwd, self._uid,
- self._gid, self._mid, self._tid, smtp, pop3, imap, sieve)
- dbc = self._dbh.cursor()
- dbc.execute(sql)
- self._dbh.commit()
- dbc.close()
+ """Save the new Account in the database.
+
+ Arguments:
+
+ `maillocation` : basestring
+ The mailbox format of the mail_location: 'maildir', 'mbox',
+ 'dbox' or 'mdbox'.
+ `dcvers` : int
+ The concatenated major and minor version number from
+ `dovecot --version`.
+ `smtp, pop3, imap, sieve` : bool
+ Indicates if the user of the Account should be able to use this
+ services.
+ """
+ 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)
+ assert all(isinstance(service, bool) for service in (smtp, pop3, imap,
+ sieve))
+ if dcvers > 11:
+ sieve_col = 'sieve'
else:
- raise AccE(_(u'The account “%s” already exists.') % self._addr,
- ERR.ACCOUNT_EXISTS)
+ sieve_col = 'managesieve'
+ self._prepare(maillocation)
+ 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, self._passwd, self._uid,
+ self._domain.gid, self._mid, self._tid, smtp, pop3, imap, 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*.
- def modify(self, what, value):
- if self._uid == 0:
- raise AccE(_(u"The account “%s” doesn't exist.") % self._addr,
- ERR.NO_SUCH_ACCOUNT)
- if what not in ['name', 'password', 'transport']:
- return False
+ Possible values for *filed* 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. The password is expected as a
+ hashed password string.
+ """
+ if field not in ('name', 'password', 'transport'):
+ raise AErr(_(u"Unknown field: '%s'") % field, INVALID_AGUMENT)
+ self._chk_state()
dbc = self._dbh.cursor()
- if what == 'password':
+ if field == 'password':
dbc.execute('UPDATE users SET passwd = %s WHERE uid = %s',
- value, self._uid)
- elif what == 'transport':
+ value, self._uid)
+ elif field == 'transport':
self._tid = Transport(self._dbh, transport=value).tid
dbc.execute('UPDATE users SET tid = %s WHERE uid = %s',
- self._tid, self._uid)
+ self._tid, self._uid)
else:
dbc.execute('UPDATE users SET name = %s WHERE uid = %s',
- value, self._uid)
+ value, self._uid)
if dbc.rowcount > 0:
self._dbh.commit()
dbc.close()
- def getInfo(self, dcvers):
+ def get_info(self, dcvers):
+ """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'.
+
+ Argument:
+
+ `dcvers` : int
+ The concatenated major and minor version number from
+ `dovecot --version`.
+ """
+ self._chk_state()
if dcvers > 11:
sieve_col = 'sieve'
else:
@@ -191,28 +296,32 @@
dbc.execute(sql)
info = dbc.fetchone()
dbc.close()
- if info is None:
- raise AccE(_(u"The account “%s” doesn't exist.") % self._addr,
- ERR.NO_SUCH_ACCOUNT)
- else:
- keys = ['name', 'uid', 'gid', 'mid', 'transport', 'smtp',
- 'pop3', 'imap', sieve_col]
+ if info:
+ keys = ('name', 'uid', 'gid', 'mid', 'transport', 'smtp',
+ 'pop3', 'imap', sieve_col)
info = dict(zip(keys, info))
for service in ('smtp', 'pop3', 'imap', sieve_col):
- if bool(info[service]):
- # TP: A service (pop3/imap/…) is enabled/usable for a user
+ 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['home'] = '%s/%s' % (self._base, info['uid'])
+ info['home'] = '%s/%s' % (self._domain.directory, info['uid'])
info['mail_location'] = MailLocation(mid=info['mid']).mail_location
info['transport'] = Transport(self._dbh,
tid=info['transport']).transport
+ del info['mid']
return info
+ # nearly impossible‽
+ raise AErr(_(u"Couldn't fetch information for account: '%s'") \
+ % self._addr, NO_SUCH_ACCOUNT)
- def getAliases(self):
+ 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\
@@ -220,57 +329,67 @@
addresses = dbc.fetchall()
dbc.close()
aliases = []
- if len(addresses) > 0:
+ if addresses:
aliases = [alias[0] for alias in addresses]
return aliases
def delete(self, delalias):
- if self._uid < 1:
- raise AccE(_(u"The account “%s” doesn't exist.") % self._addr,
- ERR.NO_SUCH_ACCOUNT)
+ """Delete the Account from the database.
+
+ Argument:
+
+ `delalias` : basestring
+ if the values of delalias is 'delalias', all aliases, which
+ points to the Account, will be also deleted."""
+ self._chk_state()
dbc = self._dbh.cursor()
if delalias == 'delalias':
dbc.execute('DELETE FROM users WHERE uid= %s', self._uid)
- u_rc = dbc.rowcount
# 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))
- if u_rc > 0 or dbc.rowcount > 0:
- self._dbh.commit()
- else: # check first for aliases
- a_count = self.__aliaseCount()
+ str(self._addr))
+ self._dbh.commit()
+ else: # check first for aliases
+ a_count = self._count_aliases()
if a_count == 0:
dbc.execute('DELETE FROM users WHERE uid = %s', self._uid)
- if dbc.rowcount > 0:
- self._dbh.commit()
+ self._dbh.commit()
else:
dbc.close()
- raise AccE(
- _(u"There are %(count)d aliases with the destination address\
- “%(address)s”.") % {'count': a_count, 'address': self._addr},
- ERR.ALIAS_PRESENT)
+ raise AErr(_(u"There are %(count)d aliases with the \
+destination address '%(address)s'.") % \
+ {'count': a_count, 'address': self._addr},
+ ALIAS_PRESENT)
dbc.close()
def getAccountByID(uid, dbh):
+ """Search an Account by its UID.
+
+ 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 AccE(_(u'uid must be an int/long.'), ERR.INVALID_AGUMENT)
+ raise AErr(_(u'UID must be an int/long.'), INVALID_AGUMENT)
if uid < 1:
- raise AccE(_(u'uid must be greater than 0.'), ERR.INVALID_AGUMENT)
+ raise AErr(_(u'UID must be greater than 0.'), INVALID_AGUMENT)
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 info is None:
- raise AccE(_(u"There is no account with the UID “%d”.") % uid,
- ERR.NO_SUCH_ACCOUNT)
- keys = ['address', 'uid', 'gid']
- info = dict(zip(keys, info))
+ 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