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