VirtualMailManager/account.py
branchv0.6.x
changeset 320 011066435e6f
parent 316 31d8931dc535
child 329 41789df75339
equal deleted inserted replaced
319:f4956b4ceba1 320:011066435e6f
       
     1 # -*- coding: UTF-8 -*-
       
     2 # Copyright (c) 2007 - 2010, Pascal Volk
       
     3 # See COPYING for distribution information.
       
     4 """
       
     5     VirtualMailManager.account
       
     6     ~~~~~~~~~~~~~~~~~~~~~~~~~~
       
     7 
       
     8     Virtual Mail Manager's Account class to manage e-mail accounts.
       
     9 """
       
    10 
       
    11 from VirtualMailManager.domain import Domain
       
    12 from VirtualMailManager.emailaddress import EmailAddress
       
    13 from VirtualMailManager.transport import Transport
       
    14 from VirtualMailManager.common import version_str
       
    15 from VirtualMailManager.constants import \
       
    16      ACCOUNT_EXISTS, ACCOUNT_MISSING_PASSWORD, ALIAS_PRESENT, \
       
    17      INVALID_ARGUMENT, INVALID_MAIL_LOCATION, NO_SUCH_ACCOUNT, \
       
    18      NO_SUCH_DOMAIN, UNKNOWN_SERVICE
       
    19 from VirtualMailManager.errors import AccountError as AErr
       
    20 from VirtualMailManager.maillocation import MailLocation
       
    21 from VirtualMailManager.password import pwhash
       
    22 
       
    23 
       
    24 _ = lambda msg: msg
       
    25 cfg_dget = lambda option: None
       
    26 
       
    27 
       
    28 class Account(object):
       
    29     """Class to manage e-mail accounts."""
       
    30     __slots__ = ('_addr', '_dbh', '_domain', '_mail', '_new', '_passwd',
       
    31                  '_transport', '_uid')
       
    32 
       
    33     def __init__(self, dbh, address):
       
    34         """Creates a new Account instance.
       
    35 
       
    36         When an account with the given *address* could be found in the
       
    37         database all relevant data will be loaded.
       
    38 
       
    39         Arguments:
       
    40 
       
    41         `dbh` : pyPgSQL.PgSQL.Connection
       
    42           A database connection for the database access.
       
    43         `address` : VirtualMailManager.EmailAddress.EmailAddress
       
    44           The e-mail address of the (new) Account.
       
    45         """
       
    46         if not isinstance(address, EmailAddress):
       
    47             raise TypeError("Argument 'address' is not an EmailAddress")
       
    48         self._addr = address
       
    49         self._dbh = dbh
       
    50         self._domain = Domain(self._dbh, self._addr.domainname)
       
    51         if not self._domain.gid:
       
    52             raise AErr(_(u"The domain '%s' doesn't exist.") %
       
    53                        self._addr.domainname, NO_SUCH_DOMAIN)
       
    54         self._uid = 0
       
    55         self._mail = None
       
    56         self._transport = self._domain.transport
       
    57         self._passwd = None
       
    58         self._new = True
       
    59         self._load()
       
    60 
       
    61     def __nonzero__(self):
       
    62         """Returns `True` if the Account is known, `False` if it's new."""
       
    63         return not self._new
       
    64 
       
    65     def _load(self):
       
    66         """Load 'uid', 'mid' and 'tid' from the database and set _new to
       
    67         `False` - if the user could be found. """
       
    68         dbc = self._dbh.cursor()
       
    69         dbc.execute('SELECT uid, mid, tid FROM users WHERE gid = %s AND '
       
    70                     'local_part = %s', self._domain.gid, self._addr.localpart)
       
    71         result = dbc.fetchone()
       
    72         dbc.close()
       
    73         if result:
       
    74             self._uid, _mid, _tid = result
       
    75             if _tid != self._transport.tid:
       
    76                 self._transport = Transport(self._dbh, tid=_tid)
       
    77             self._mail = MailLocation(self._dbh, mid=_mid)
       
    78             self._new = False
       
    79 
       
    80     def _set_uid(self):
       
    81         """Set the unique ID for the new Account."""
       
    82         assert self._uid == 0
       
    83         dbc = self._dbh.cursor()
       
    84         dbc.execute("SELECT nextval('users_uid')")
       
    85         self._uid = dbc.fetchone()[0]
       
    86         dbc.close()
       
    87 
       
    88     def _prepare(self, maillocation):
       
    89         """Check and set different attributes - before we store the
       
    90         information in the database.
       
    91         """
       
    92         if maillocation.dovecot_version > cfg_dget('misc.dovecot_version'):
       
    93             raise AErr(_(u"The mailbox format '%(mbfmt)s' requires Dovecot "
       
    94                          u">= v%(version)s") % {'mbfmt': maillocation.mbformat,
       
    95                        'version': version_str(maillocation.dovecot_version)},
       
    96                        INVALID_MAIL_LOCATION)
       
    97         if not maillocation.postfix and \
       
    98           self._transport.transport.lower() in ('virtual:', 'virtual'):
       
    99             raise AErr(_(u"Invalid transport '%(transport)s' for mailbox "
       
   100                          u"format '%(mbfmt)s'") %
       
   101                        {'transport': self._transport,
       
   102                         'mbfmt': maillocation.mbformat}, INVALID_MAIL_LOCATION)
       
   103         self._mail = maillocation
       
   104         self._set_uid()
       
   105 
       
   106     def _switch_state(self, state, service):
       
   107         """Switch the state of the Account's services on or off. See
       
   108         Account.enable()/Account.disable() for more information."""
       
   109         self._chk_state()
       
   110         if service not in (None, 'all', 'imap', 'pop3', 'sieve', 'smtp'):
       
   111             raise AErr(_(u"Unknown service: '%s'.") % service, UNKNOWN_SERVICE)
       
   112         if cfg_dget('misc.dovecot_version') >= 0x10200b02:
       
   113             sieve_col = 'sieve'
       
   114         else:
       
   115             sieve_col = 'managesieve'
       
   116         if service in ('smtp', 'pop3', 'imap'):
       
   117             sql = 'UPDATE users SET %s = %s WHERE uid = %d' % (service, state,
       
   118                                                                self._uid)
       
   119         elif service == 'sieve':
       
   120             sql = 'UPDATE users SET %s = %s WHERE uid = %d' % (sieve_col,
       
   121                                                                state,
       
   122                                                                self._uid)
       
   123         else:
       
   124             sql = 'UPDATE users SET smtp = %(s)s, pop3 = %(s)s, imap = %(s)s,\
       
   125  %(col)s = %(s)s WHERE uid = %(uid)d' % \
       
   126                 {'s': state, 'col': sieve_col, 'uid': self._uid}
       
   127         dbc = self._dbh.cursor()
       
   128         dbc.execute(sql)
       
   129         if dbc.rowcount > 0:
       
   130             self._dbh.commit()
       
   131         dbc.close()
       
   132 
       
   133     def _count_aliases(self):
       
   134         """Count all alias addresses where the destination address is the
       
   135         address of the Account."""
       
   136         dbc = self._dbh.cursor()
       
   137         sql = "SELECT COUNT(destination) FROM alias WHERE destination = '%s'"\
       
   138                 % self._addr
       
   139         dbc.execute(sql)
       
   140         a_count = dbc.fetchone()[0]
       
   141         dbc.close()
       
   142         return a_count
       
   143 
       
   144     def _chk_state(self):
       
   145         """Raise an AccountError if the Account is new - not yet saved in the
       
   146         database."""
       
   147         if self._new:
       
   148             raise AErr(_(u"The account '%s' doesn't exist.") % self._addr,
       
   149                        NO_SUCH_ACCOUNT)
       
   150 
       
   151     @property
       
   152     def address(self):
       
   153         """The Account's EmailAddress instance."""
       
   154         return self._addr
       
   155 
       
   156     @property
       
   157     def domain_directory(self):
       
   158         """The directory of the domain the Account belongs to."""
       
   159         if self._domain:
       
   160             return self._domain.directory
       
   161         return None
       
   162 
       
   163     @property
       
   164     def gid(self):
       
   165         """The Account's group ID."""
       
   166         if self._domain:
       
   167             return self._domain.gid
       
   168         return None
       
   169 
       
   170     @property
       
   171     def home(self):
       
   172         """The Account's home directory."""
       
   173         if not self._new:
       
   174             return '%s/%s' % (self._domain.directory, self._uid)
       
   175         return None
       
   176 
       
   177     @property
       
   178     def mail_location(self):
       
   179         """The Account's MailLocation."""
       
   180         return self._mail
       
   181 
       
   182     @property
       
   183     def uid(self):
       
   184         """The Account's unique ID."""
       
   185         return self._uid
       
   186 
       
   187     def set_password(self, password):
       
   188         """Set a password for the new Account.
       
   189 
       
   190         If you want to update the password of an existing Account use
       
   191         Account.modify().
       
   192 
       
   193         Argument:
       
   194 
       
   195         `password` : basestring
       
   196           The password for the new Account.
       
   197         """
       
   198         if not isinstance(password, basestring) or not password:
       
   199             raise AErr(_(u"Couldn't accept password: '%s'") % password,
       
   200                        ACCOUNT_MISSING_PASSWORD)
       
   201         self._passwd = password
       
   202 
       
   203     def set_transport(self, transport):
       
   204         """Set the transport for the new Account.
       
   205 
       
   206         If you want to update the transport of an existing Account use
       
   207         Account.modify().
       
   208 
       
   209         Argument:
       
   210 
       
   211         `transport` : basestring
       
   212           The string representation of the transport, e.g.: 'dovecot:'
       
   213         """
       
   214         self._transport = Transport(self._dbh, transport=transport)
       
   215 
       
   216     def enable(self, service=None):
       
   217         """Enable a/all service/s for the Account.
       
   218 
       
   219         Possible values for the *service* are: 'imap', 'pop3', 'sieve' and
       
   220         'smtp'. When all services should be enabled, use 'all' or the
       
   221         default value `None`.
       
   222 
       
   223         Arguments:
       
   224 
       
   225         `service` : basestring
       
   226           The name of a service ('imap', 'pop3', 'smtp', 'sieve'), 'all'
       
   227           or `None`.
       
   228         """
       
   229         self._switch_state(True, service)
       
   230 
       
   231     def disable(self, service=None):
       
   232         """Disable a/all service/s for the Account.
       
   233 
       
   234         For more information see: Account.enable()."""
       
   235         self._switch_state(False, service)
       
   236 
       
   237     def save(self):
       
   238         """Save the new Account in the database."""
       
   239         if not self._new:
       
   240             raise AErr(_(u"The account '%s' already exists.") % self._addr,
       
   241                        ACCOUNT_EXISTS)
       
   242         if not self._passwd:
       
   243             raise AErr(_(u"No password set for '%s'.") % self._addr,
       
   244                        ACCOUNT_MISSING_PASSWORD)
       
   245         if cfg_dget('misc.dovecot_version') >= 0x10200b02:
       
   246             sieve_col = 'sieve'
       
   247         else:
       
   248             sieve_col = 'managesieve'
       
   249         self._prepare(MailLocation(self._dbh, mbfmt=cfg_dget('mailbox.format'),
       
   250                                    directory=cfg_dget('mailbox.root')))
       
   251         sql = "INSERT INTO users (local_part, passwd, uid, gid, mid, tid,\
       
   252  smtp, pop3, imap, %s) VALUES ('%s', '%s', %d, %d, %d, %d, %s, %s, %s, %s)" % (
       
   253             sieve_col, self._addr.localpart, pwhash(self._passwd,
       
   254                                                     user=self._addr),
       
   255             self._uid, self._domain.gid, self._mail.mid, self._transport.tid,
       
   256             cfg_dget('account.smtp'), cfg_dget('account.pop3'),
       
   257             cfg_dget('account.imap'), cfg_dget('account.sieve'))
       
   258         dbc = self._dbh.cursor()
       
   259         dbc.execute(sql)
       
   260         self._dbh.commit()
       
   261         dbc.close()
       
   262         self._new = False
       
   263 
       
   264     def modify(self, field, value):
       
   265         """Update the Account's *field* to the new *value*.
       
   266 
       
   267         Possible values for *field* are: 'name', 'password' and
       
   268         'transport'.  *value* is the *field*'s new value.
       
   269 
       
   270         Arguments:
       
   271 
       
   272         `field` : basestring
       
   273           The attribute name: 'name', 'password' or 'transport'
       
   274         `value` : basestring
       
   275           The new value of the attribute.
       
   276         """
       
   277         if field not in ('name', 'password', 'transport'):
       
   278             raise AErr(_(u"Unknown field: '%s'") % field, INVALID_ARGUMENT)
       
   279         self._chk_state()
       
   280         dbc = self._dbh.cursor()
       
   281         if field == 'password':
       
   282             dbc.execute('UPDATE users SET passwd = %s WHERE uid = %s',
       
   283                         pwhash(value, user=self._addr), self._uid)
       
   284         elif field == 'transport':
       
   285             if value != self._transport.transport:
       
   286                 self._transport = Transport(self._dbh, transport=value)
       
   287                 dbc.execute('UPDATE users SET tid = %s WHERE uid = %s',
       
   288                             self._transport.tid, self._uid)
       
   289         else:
       
   290             dbc.execute('UPDATE users SET name = %s WHERE uid = %s',
       
   291                         value, self._uid)
       
   292         if dbc.rowcount > 0:
       
   293             self._dbh.commit()
       
   294         dbc.close()
       
   295 
       
   296     def get_info(self):
       
   297         """Returns a dict with some information about the Account.
       
   298 
       
   299         The keys of the dict are: 'address', 'gid', 'home', 'imap'
       
   300         'mail_location', 'name', 'pop3', 'sieve', 'smtp', transport' and
       
   301         'uid'.
       
   302         """
       
   303         self._chk_state()
       
   304         if cfg_dget('misc.dovecot_version') >= 0x10200b02:
       
   305             sieve_col = 'sieve'
       
   306         else:
       
   307             sieve_col = 'managesieve'
       
   308         sql = 'SELECT name, smtp, pop3, imap, %s FROM users WHERE uid = %d' % \
       
   309             (sieve_col, self._uid)
       
   310         dbc = self._dbh.cursor()
       
   311         dbc.execute(sql)
       
   312         info = dbc.fetchone()
       
   313         dbc.close()
       
   314         if info:
       
   315             keys = ('name', 'smtp', 'pop3', 'imap', sieve_col)
       
   316             info = dict(zip(keys, info))
       
   317             for service in keys[1:]:
       
   318                 if info[service]:
       
   319                     # TP: A service (pop3/imap) is enabled/usable for a user
       
   320                     info[service] = _('enabled')
       
   321                 else:
       
   322                     # TP: A service (pop3/imap) isn't enabled/usable for a user
       
   323                     info[service] = _('disabled')
       
   324             info['address'] = self._addr
       
   325             info['gid'] = self._domain.gid
       
   326             info['home'] = '%s/%s' % (self._domain.directory, self._uid)
       
   327             info['mail_location'] = self._mail.mail_location
       
   328             info['transport'] = self._transport.transport
       
   329             info['uid'] = self._uid
       
   330             return info
       
   331         # nearly impossibleā€½
       
   332         raise AErr(_(u"Couldn't fetch information for account: '%s'") %
       
   333                    self._addr, NO_SUCH_ACCOUNT)
       
   334 
       
   335     def get_aliases(self):
       
   336         """Return a list with all alias e-mail addresses, whose destination
       
   337         is the address of the Account."""
       
   338         self._chk_state()
       
   339         dbc = self._dbh.cursor()
       
   340         dbc.execute("SELECT address ||'@'|| domainname FROM alias, "
       
   341                     "domain_name WHERE destination = %s AND domain_name.gid = "
       
   342                     "alias.gid AND domain_name.is_primary ORDER BY address",
       
   343                     str(self._addr))
       
   344         addresses = dbc.fetchall()
       
   345         dbc.close()
       
   346         aliases = []
       
   347         if addresses:
       
   348             aliases = [alias[0] for alias in addresses]
       
   349         return aliases
       
   350 
       
   351     def delete(self, delalias=False):
       
   352         """Delete the Account from the database.
       
   353 
       
   354         Argument:
       
   355 
       
   356         `delalias` : bool
       
   357           if *delalias* is `True`, all aliases, which points to the Account,
       
   358           will be also deleted.  If there are aliases and *delalias* is
       
   359           `False`, an AccountError will be raised.
       
   360         """
       
   361         assert isinstance(delalias, bool)
       
   362         self._chk_state()
       
   363         dbc = self._dbh.cursor()
       
   364         if delalias:
       
   365             dbc.execute('DELETE FROM users WHERE uid = %s', self._uid)
       
   366             # delete also all aliases where the destination address is the same
       
   367             # as for this account.
       
   368             dbc.execute("DELETE FROM alias WHERE destination = %s",
       
   369                         str(self._addr))
       
   370             self._dbh.commit()
       
   371         else:  # check first for aliases
       
   372             a_count = self._count_aliases()
       
   373             if a_count > 0:
       
   374                 dbc.close()
       
   375                 raise AErr(_(u"There are %(count)d aliases with the "
       
   376                              u"destination address '%(address)s'.") %
       
   377                            {'count': a_count, 'address': self._addr},
       
   378                            ALIAS_PRESENT)
       
   379             dbc.execute('DELETE FROM users WHERE uid = %s', self._uid)
       
   380             self._dbh.commit()
       
   381         dbc.close()
       
   382         self._new = True
       
   383         self._uid = 0
       
   384         self._addr = self._dbh = self._domain = self._passwd = None
       
   385         self._mail = self._transport = None
       
   386 
       
   387 
       
   388 def get_account_by_uid(uid, dbh):
       
   389     """Search an Account by its UID.
       
   390 
       
   391     This function returns a dict (keys: 'address', 'gid' and 'uid'), if an
       
   392     Account with the given *uid* exists.
       
   393 
       
   394     Argument:
       
   395 
       
   396     `uid` : long
       
   397       The Account unique ID.
       
   398     `dbh` : pyPgSQL.PgSQL.Connection
       
   399       a database connection for the database access.
       
   400     """
       
   401     try:
       
   402         uid = long(uid)
       
   403     except ValueError:
       
   404         raise AErr(_(u'UID must be an int/long.'), INVALID_ARGUMENT)
       
   405     if uid < 1:
       
   406         raise AErr(_(u'UID must be greater than 0.'), INVALID_ARGUMENT)
       
   407     dbc = dbh.cursor()
       
   408     dbc.execute("SELECT local_part||'@'|| domain_name.domainname AS address, "
       
   409                 "uid, users.gid FROM users LEFT JOIN domain_name ON "
       
   410                 "(domain_name.gid = users.gid AND is_primary) WHERE uid = %s",
       
   411                 uid)
       
   412     info = dbc.fetchone()
       
   413     dbc.close()
       
   414     if not info:
       
   415         raise AErr(_(u"There is no account with the UID '%d'.") % uid,
       
   416                    NO_SUCH_ACCOUNT)
       
   417     info = dict(zip(('address', 'uid', 'gid'), info))
       
   418     return info
       
   419 
       
   420 del _, cfg_dget