# HG changeset patch # User Pascal Volk # Date 1297368628 0 # Node ID 660b42391c8e871e325e6e9f2afe7fe254a22ea7 # Parent 5f7e9f778b29514d01dbbc6a537a0c40a5a57d32 VMM/{account,domain,handler}: Added quota limit support. A few small modifications in class Account. diff -r 5f7e9f778b29 -r 660b42391c8e VirtualMailManager/account.py --- 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): diff -r 5f7e9f778b29 -r 660b42391c8e VirtualMailManager/domain.py --- 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): diff -r 5f7e9f778b29 -r 660b42391c8e VirtualMailManager/handler.py --- 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)"""