VMM/{account,domain,handler}: Added quota limit support. A few
small modifications in class Account.
--- a/VirtualMailManager/account.py Wed Feb 09 22:09:35 2011 +0000
+++ b/VirtualMailManager/account.py Thu Feb 10 20:10:28 2011 +0000
@@ -10,6 +10,7 @@
from VirtualMailManager.domain import Domain
from VirtualMailManager.emailaddress import EmailAddress
+from VirtualMailManager.quotalimit import QuotaLimit
from VirtualMailManager.transport import Transport
from VirtualMailManager.common import version_str
from VirtualMailManager.constants import \
@@ -31,7 +32,7 @@
class Account(object):
"""Class to manage e-mail accounts."""
__slots__ = ('_addr', '_dbh', '_domain', '_mail', '_new', '_passwd',
- '_transport', '_uid')
+ '_qlimit', '_transport', '_uid')
def __init__(self, dbh, address):
"""Creates a new Account instance.
@@ -59,6 +60,7 @@
self._addr.domainname, NO_SUCH_DOMAIN)
self._uid = 0
self._mail = None
+ self._qlimit = self._domain.quotalimit
self._transport = self._domain.transport
self._passwd = None
self._new = True
@@ -69,15 +71,17 @@
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. """
+ """Load 'uid', 'mid', 'qid' 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 '
+ dbc.execute('SELECT uid, mid, qid, 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
+ self._uid, _mid, _qid, _tid = result
+ if _qid != self._qlimit.qid:
+ self._qlimit = QuotaLimit(self._dbh, qid=_qid)
if _tid != self._transport.tid:
self._transport = Transport(self._dbh, tid=_tid)
self._mail = MailLocation(self._dbh, mid=_mid)
@@ -142,6 +146,25 @@
self._dbh.commit()
dbc.close()
+ def _update_tables(self, column, value):
+ """Update various columns in the users table.
+
+ Arguments:
+
+ `column` : basestring
+ Name of the table column. Currently: qid and tid
+ `value` : long
+ The referenced key
+ """
+ if column not in ('qid', 'tid'):
+ raise ValueError('Unknown column: %r' % column)
+ dbc = self._dbh.cursor()
+ dbc.execute('UPDATE users SET %s = %%s WHERE uid = %%s' % column,
+ (value, self._uid))
+ 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."""
@@ -206,6 +229,9 @@
`password` : basestring
The password for the new Account.
"""
+ if not self._new:
+ raise AErr(_(u"The account '%s' already exists.") % self._addr,
+ ACCOUNT_EXISTS)
if not isinstance(password, basestring) or not password:
raise AErr(_(u"Could not accept password: '%s'") % password,
ACCOUNT_MISSING_PASSWORD)
@@ -247,13 +273,14 @@
directory=cfg_dget('mailbox.root')))
dbc = self._dbh.cursor()
dbc.execute('INSERT INTO users (local_part, passwd, uid, gid, mid, '
- 'tid, smtp, pop3, imap, %s) VALUES' % (sieve_col,) + \
- '(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)',
+ 'qid, tid, smtp, pop3, imap, %s) VALUES' % (sieve_col,) + \
+ '(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)',
(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')))
+ self._domain.gid, self._mail.mid, self._qlimit.qid,
+ self._transport.tid, cfg_dget('account.smtp'),
+ cfg_dget('account.pop3'), cfg_dget('account.imap'),
+ cfg_dget('account.sieve')))
self._dbh.commit()
dbc.close()
self._new = False
@@ -261,28 +288,22 @@
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.
+ Possible values for *field* are: 'name', 'password'.
Arguments:
`field` : basestring
- The attribute name: 'name', 'password' or 'transport'
+ The attribute name: 'name' or 'password'
`value` : basestring
The new value of the attribute.
"""
- if field not in ('name', 'password', 'transport'):
+ if field not in ('name', 'password'):
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))
@@ -290,28 +311,67 @@
self._dbh.commit()
dbc.close()
+ def update_quotalimit(self, quotalimit):
+ """Update the user's quota limit.
+
+ Arguments:
+
+ `quotalimit` : VirtualMailManager.quotalimit.QuotaLimit
+ the new quota limit of the domain.
+ """
+ self._chk_state()
+ assert isinstance(quotalimit, QuotaLimit)
+ if quotalimit == self._qlimit:
+ return
+ self._update_tables('qid', quotalimit.qid)
+ self._qlimit = quotalimit
+
+ def update_transport(self, transport):
+ """Sets a new transport for the Account.
+
+ Arguments:
+
+ `transport` : VirtualMailManager.transport.Transport
+ the new transport
+ """
+ self._chk_state()
+ assert isinstance(transport, Transport)
+ if transport == self._transport:
+ return
+ if transport.transport.lower() in ('virtual', 'virtual:') and \
+ not self._mail.postfix:
+ raise AErr(_(u"Invalid transport '%(transport)s' for mailbox "
+ u"format '%(mbfmt)s'") %
+ {'transport': transport, 'mbfmt': self._mail.mbformat},
+ INVALID_MAIL_LOCATION)
+ self._update_tables('tid', transport.tid)
+ self._transport = transport
+
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'.
+ 'mail_location', 'name', 'pop3', 'sieve', 'smtp', transport', 'uid',
+ 'uq_bytes', 'uq_messages', 'ql_bytes', and 'ql_messages'.
"""
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)
+ dbc.execute('SELECT name, smtp, pop3, imap, %s, CASE WHEN bytes IS '
+ 'NULL THEN 0 ELSE bytes END, CASE WHEN messages IS NULL '
+ 'THEN 0 ELSE messages END FROM users LEFT JOIN userquota '
+ 'USING (uid) WHERE users.uid = %u' % (sieve_col,
+ self._uid))
info = dbc.fetchone()
dbc.close()
if info:
- keys = ('name', 'smtp', 'pop3', 'imap', sieve_col)
+ keys = ('name', 'smtp', 'pop3', 'imap', sieve_col, 'uq_bytes',
+ 'uq_messages')
info = dict(zip(keys, info))
- for service in keys[1:]:
+ for service in keys[1:5]:
if info[service]:
# TP: A service (pop3/imap) is enabled/usable for a user
info[service] = _('enabled')
@@ -322,6 +382,8 @@
info['gid'] = self._domain.gid
info['home'] = '%s/%s' % (self._domain.directory, self._uid)
info['mail_location'] = self._mail.mail_location
+ info['ql_bytes'] = self._qlimit.bytes
+ info['ql_messages'] = self._qlimit.messages
info['transport'] = self._transport.transport
info['uid'] = self._uid
return info
@@ -380,7 +442,7 @@
self._new = True
self._uid = 0
self._addr = self._dbh = self._domain = self._passwd = None
- self._mail = self._transport = None
+ self._mail = self._qlimit = self._transport = None
def get_account_by_uid(uid, dbh):
--- a/VirtualMailManager/domain.py Wed Feb 09 22:09:35 2011 +0000
+++ b/VirtualMailManager/domain.py Thu Feb 10 20:10:28 2011 +0000
@@ -16,7 +16,8 @@
ACCOUNT_AND_ALIAS_PRESENT, DOMAIN_ALIAS_EXISTS, DOMAIN_EXISTS, \
DOMAIN_INVALID, DOMAIN_TOO_LONG, NO_SUCH_DOMAIN
from VirtualMailManager.errors import DomainError as DomErr
-from VirtualMailManager.pycompat import any
+from VirtualMailManager.pycompat import all, any
+from VirtualMailManager.quotalimit import QuotaLimit
from VirtualMailManager.transport import Transport
@@ -27,7 +28,8 @@
class Domain(object):
"""Class to manage e-mail domains."""
- __slots__ = ('_directory', '_gid', '_name', '_transport', '_dbh', '_new')
+ __slots__ = ('_directory', '_gid', '_name', '_qlimit', '_transport',
+ '_dbh', '_new')
def __init__(self, dbh, domainname):
"""Creates a new Domain instance.
@@ -49,6 +51,7 @@
self._name = check_domainname(domainname)
self._dbh = dbh
self._gid = 0
+ self._qlimit = None
self._transport = None
self._directory = None
self._new = True
@@ -62,17 +65,18 @@
domain.
"""
dbc = self._dbh.cursor()
- dbc.execute('SELECT dd.gid, tid, domaindir, is_primary FROM '
+ dbc.execute('SELECT dd.gid, qid, 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]:
+ if not result[4]:
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._gid, self._directory = result[0], result[3]
+ self._qlimit = QuotaLimit(self._dbh, qid=result[1])
+ self._transport = Transport(self._dbh, tid=result[2])
self._new = False
def _set_gid(self):
@@ -109,6 +113,34 @@
raise DomErr(_(u"The domain '%s' doesn't exist.") % self._name,
NO_SUCH_DOMAIN)
+ def _update_tables(self, column, value, force=False):
+ """Update various columns in the domain_data table. When *force* is
+ `True` also the corresponding column in the users table will be
+ updated.
+
+ Arguments:
+
+ `column` : basestring
+ Name of the table column. Currently: qid and tid
+ `value` : long
+ The referenced key
+ `force` : bool
+ enforce the new setting also for existing users. Default: `False`
+ """
+ if column not in ('qid', 'tid'):
+ raise ValueError('Unknown column: %r' % column)
+ dbc = self._dbh.cursor()
+ dbc.execute('UPDATE domain_data SET %s = %%s WHERE gid = %%s' % column,
+ (value, self._gid))
+ if dbc.rowcount > 0:
+ self._dbh.commit()
+ if force:
+ dbc.execute('UPDATE users SET %s = %%s WHERE gid = %%s' % column,
+ (value, self._gid))
+ if dbc.rowcount > 0:
+ self._dbh.commit()
+ dbc.close()
+
@property
def gid(self):
"""The GID of the Domain."""
@@ -124,6 +156,16 @@
"""The Domain's directory."""
return self._directory
+ @property
+ def quotalimit(self):
+ """The Domain's quota limit."""
+ return self._qlimit
+
+ @property
+ def transport(self):
+ """The Domain's transport."""
+ return self._transport
+
def set_directory(self, basedir):
"""Set the path value of the Domain's directory, inside *basedir*.
@@ -140,10 +182,19 @@
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_quotalimit(self, quotalimit):
+ """Set the quota limit for the new Domain.
+
+ Argument:
+
+ `quotalimit` : VirtualMailManager.quotalimit.QuotaLimit
+ The quota limit of the new Domain.
+ """
+ if not self._new:
+ raise DomErr(_(u"The domain '%s' already exists.") % self._name,
+ DOMAIN_EXISTS)
+ assert isinstance(quotalimit, QuotaLimit)
+ self._qlimit = quotalimit
def set_transport(self, transport):
"""Set the transport for the new Domain.
@@ -164,11 +215,11 @@
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
+ assert all((self._directory, self._qlimit, self._transport))
dbc = self._dbh.cursor()
- dbc.execute('INSERT INTO domain_data (gid, tid, domaindir) VALUES '
- '(%s, %s, %s)', (self._gid, self._transport.tid,
- self._directory))
+ dbc.execute('INSERT INTO domain_data (gid, qid, tid, domaindir) '
+ 'VALUES (%s, %s, %s, %s)', (self._gid, self._qlimit.qid,
+ self._transport.tid, self._directory))
dbc.execute('INSERT INTO domain_name (domainname, gid, is_primary) '
'VALUES (%s, %s, TRUE)', (self._name, self._gid))
self._dbh.commit()
@@ -198,9 +249,30 @@
self._dbh.commit()
dbc.close()
self._gid = 0
- self._directory = self._transport = None
+ self._directory = self._qlimit = self._transport = None
self._new = True
+ def update_quotalimit(self, quotalimit, force=False):
+ """Update the quota limit of the Domain.
+
+ If *force* is `True` the new *quotalimit* will be applied to
+ all existing accounts of the domain. Otherwise the *quotalimit*
+ will be only applied to accounts created from now on.
+
+ Arguments:
+
+ `quotalimit` : VirtualMailManager.quotalimit.QuotaLimit
+ the new quota limit of the domain.
+ `force` : bool
+ enforce new quota limit for all accounts, default `False`
+ """
+ self._chk_state()
+ assert isinstance(quotalimit, QuotaLimit)
+ if quotalimit == self._qlimit:
+ return
+ self._update_tables('qid', quotalimit.qid, force)
+ self._qlimit = quotalimit
+
def update_transport(self, transport, force=False):
"""Sets a new transport for the Domain.
@@ -219,17 +291,7 @@
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._update_tables('tid', transport.tid, force)
self._transport = transport
def get_info(self):
@@ -237,12 +299,13 @@
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,))
+ 'aliasdomains, accounts, aliases, relocated, bytes, '
+ 'messages FROM vmm_domain_info WHERE gid = %s',
+ (self._gid,))
info = dbc.fetchone()
dbc.close()
keys = ('gid', 'domainname', 'transport', 'domaindir', 'aliasdomains',
- 'accounts', 'aliases', 'relocated')
+ 'accounts', 'aliases', 'relocated', 'bytes', 'messages')
return dict(zip(keys, info))
def get_accounts(self):
--- a/VirtualMailManager/handler.py Wed Feb 09 22:09:35 2011 +0000
+++ b/VirtualMailManager/handler.py Thu Feb 10 20:10:28 2011 +0000
@@ -36,6 +36,7 @@
DomainError, NotRootError, PermissionError, VMMError
from VirtualMailManager.mailbox import new as new_mailbox
from VirtualMailManager.pycompat import all, any
+from VirtualMailManager.quotalimit import QuotaLimit
from VirtualMailManager.relocated import Relocated
from VirtualMailManager.transport import Transport
@@ -422,17 +423,35 @@
__builtin__.__dict__['cfg_dget'] = self._cfg.dget
def domain_add(self, domainname, transport=None):
- """Wrapper around Domain.set_transport() and Domain.save()"""
+ """Wrapper around Domain's set_quotalimit, set_transport and 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_quotalimit(QuotaLimit(self._dbh,
+ bytes=self._cfg.dget('misc.quota_bytes'),
+ messages=self._cfg.dget('misc.quota_messages')))
dom.set_directory(self._cfg.dget('misc.base_directory'))
dom.save()
self._make_domain_dir(dom)
+ def domain_quotalimit(self, domainname, bytes_, messages=0, force=None):
+ """Wrapper around Domain.update_quotalimit()."""
+ if not all(isinstance(i, (int, long)) for i in (bytes_, messages)):
+ raise TypeError("'bytes_' and 'messages' have to be "
+ "integers or longs.")
+ if force is not None and force != 'force':
+ raise DomainError(_(u"Invalid argument: '%s'") % force,
+ INVALID_ARGUMENT)
+ dom = self._get_domain(domainname)
+ quotalimit = QuotaLimit(self._dbh, bytes=bytes_, messages=messages)
+ if force is None:
+ dom.update_quotalimit(quotalimit)
+ else:
+ dom.update_quotalimit(quotalimit, force=True)
+
def domain_transport(self, domainname, transport, force=None):
"""Wrapper around Domain.update_transport()"""
if force is not None and force != 'force':
@@ -674,8 +693,20 @@
acc.address, NO_SUCH_ACCOUNT)
acc.modify('name', name)
+ def user_quotalimit(self, emailaddress, bytes_, messages=0):
+ """Wrapper for Account.update_quotalimit(QuotaLimit)."""
+ if not all(isinstance(i, (int, long)) for i in (bytes_, messages)):
+ raise TypeError("'bytes_' and 'messages' have to be "
+ "integers or longs.")
+ acc = self._get_account(emailaddress)
+ if not acc:
+ raise VMMError(_(u"The account '%s' doesn't exist.") %
+ acc.address, NO_SUCH_ACCOUNT)
+ acc.update_quotalimit(QuotaLimit(self._dbh, bytes=bytes_,
+ messages=messages))
+
def user_transport(self, emailaddress, transport):
- """Wrapper for Account.modify('transport', ...)."""
+ """Wrapper for Account.update_transport(Transport)."""
if not isinstance(transport, basestring) or not transport:
raise VMMError(_(u"Could not accept transport: '%s'") % transport,
INVALID_ARGUMENT)
@@ -683,7 +714,7 @@
if not acc:
raise VMMError(_(u"The account '%s' doesn't exist.") %
acc.address, NO_SUCH_ACCOUNT)
- acc.modify('transport', transport)
+ acc.update_transport(Transport(self._dbh, transport=transport))
def user_disable(self, emailaddress, services=None):
"""Wrapper for Account.disable(*services)"""