VMM/Account: reworked class Account. v0.6.x
authorPascal Volk <neverseen@users.sourceforge.net>
Thu, 15 Apr 2010 03:05:01 +0000
branchv0.6.x
changeset 245 f9a6b6701cf9
parent 244 698ba4208ddc
child 246 481280686789
VMM/Account: reworked class Account.
VirtualMailManager/Account.py
VirtualMailManager/constants/ERROR.py
--- 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
 
 
--- a/VirtualMailManager/constants/ERROR.py	Tue Apr 13 18:11:52 2010 +0000
+++ b/VirtualMailManager/constants/ERROR.py	Thu Apr 15 03:05:01 2010 +0000
@@ -4,6 +4,7 @@
 
 ACCOUNT_AND_ALIAS_PRESENT = 20
 ACCOUNT_EXISTS = 21
+ACCOUNT_MISSING_PASSWORD = 69
 ACCOUNT_PRESENT = 22
 ALIASDOMAIN_EXISTS = 23
 ALIASDOMAIN_ISDOMAIN = 24