VMM/{account,domain,handler}: Added quota limit support. A few v0.6.x
authorPascal Volk <neverseen@users.sourceforge.net>
Thu, 10 Feb 2011 20:10:28 +0000
branchv0.6.x
changeset 390 660b42391c8e
parent 389 5f7e9f778b29
child 391 8217ddd5220d
VMM/{account,domain,handler}: Added quota limit support. A few small modifications in class Account.
VirtualMailManager/account.py
VirtualMailManager/domain.py
VirtualMailManager/handler.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):
--- 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)"""