Index: trac/env.py
===================================================================
--- trac/env.py	(revision 5898)
+++ trac/env.py	(working copy)
@@ -25,6 +25,7 @@
 from trac.core import Component, ComponentManager, implements, Interface, \
                       ExtensionPoint, TracError
 from trac.db import DatabaseManager
+from trac.user import UserManager, User
 from trac.util import get_pkginfo
 from trac.versioncontrol import RepositoryManager
 from trac.web.href import Href
@@ -329,29 +330,16 @@
         self.log = logger_factory(logtype, logfile, self.log_level, self.path,
                                   format=format)
 
-    def get_known_users(self, cnx=None):
+    def get_known_users(self):
         """Generator that yields information about all known users, i.e. users
         that have logged in to this Trac environment and possibly set their name
         and email.
 
         This function generates one tuple for every user, of the form
         (username, name, email) ordered alpha-numerically by username.
-
-        @param cnx: the database connection; if ommitted, a new connection is
-                    retrieved
         """
-        if not cnx:
-            cnx = self.get_db_cnx()
-        cursor = cnx.cursor()
-        cursor.execute("SELECT DISTINCT s.sid, n.value, e.value "
-                       "FROM session AS s "
-                       " LEFT JOIN session_attribute AS n ON (n.sid=s.sid "
-                       "  and n.authenticated=1 AND n.name = 'name') "
-                       " LEFT JOIN session_attribute AS e ON (e.sid=s.sid "
-                       "  AND e.authenticated=1 AND e.name = 'email') "
-                       "WHERE s.authenticated=1 ORDER BY s.sid")
-        for username,name,email in cursor:
-            yield username, name, email
+        for user in UserManager(self).get_all_users():
+            yield user.username, user['name'], user['email']
 
     def backup(self, dest=None):
         """Simple SQLite-specific backup of the database.
Index: trac/notification.py
===================================================================
--- trac/notification.py	(revision 5898)
+++ trac/notification.py	(working copy)
@@ -22,6 +22,7 @@
 from trac import __version__
 from trac.config import BoolOption, IntOption, Option
 from trac.core import *
+from trac.user import UserManager
 from trac.util.text import CRLF
 from trac.web.chrome import Chrome
 
@@ -158,6 +159,7 @@
     smtp_port = 25
     from_email = 'trac+tickets@localhost'
     subject = ''
+    server = None
     template_name = None
     nodomaddr_re = re.compile(r'[\w\d_\.\-]+')
     addrsep_re = re.compile(r'[;\s,]+')
@@ -177,14 +179,7 @@
         self.longaddr_re = re.compile(r'^\s*(.*)\s+<(%s)>\s*$' % addrfmt);
         self._use_tls = self.env.config.getbool('notification', 'use_tls')
         self._init_pref_encoding()
-        domains = self.env.config.get('notification', 'ignore_domains', '')
-        self._ignore_domains = [x.strip() for x in domains.lower().split(',')]
-        # Get the email addresses of all known users
-        self.email_map = {}
-        for username, name, email in self.env.get_known_users(self.db):
-            if email:
-                self.email_map[username] = email
-                
+
     def _init_pref_encoding(self):
         from email.Charset import Charset, QP, BASE64
         self._charset = Charset()
@@ -270,8 +265,10 @@
         if not is_email(address):
             if address == 'anonymous':
                 return None
-            if self.email_map.has_key(address):
-                address = self.email_map[address]
+            
+            email = UserManager(self.env).get_user(address)['email']
+            if email and len(email) > 0:
+                address = email
             elif NotifyEmail.nodomaddr_re.match(address):
                 if self.config.getbool('notification', 'use_short_addr'):
                     return address
Index: trac/perm.py
===================================================================
--- trac/perm.py	(revision 5898)
+++ trac/perm.py	(working copy)
@@ -20,6 +20,7 @@
 
 from trac.config import ExtensionOption, OrderedExtensionsOption
 from trac.core import *
+from trac.user import UserManager
 from trac.util.compat import set
 from trac.util.translation import _
 
@@ -157,7 +158,8 @@
         db = self.env.get_db_cnx()
         cursor = db.cursor()
         result = set()
-        users = set([u[0] for u in self.env.get_known_users()])
+        users = set([username for username
+                    in UserManager(self.env).get_usernames()])
         for user in users:
             userperms = self.get_user_permissions(user)
             for group in permissions:
Index: trac/prefs/web_ui.py
===================================================================
--- trac/prefs/web_ui.py	(revision 5914)
+++ trac/prefs/web_ui.py	(working copy)
@@ -22,6 +22,7 @@
 
 from trac.core import *
 from trac.prefs.api import IPreferencePanelProvider
+from trac.user import UserManager, User
 from trac.util.datefmt import all_timezones, get_timezone
 from trac.util.translation import _
 from trac.web import HTTPNotFound, IRequestHandler
@@ -93,8 +94,16 @@
                 self._do_save(req)
             req.redirect(req.href.prefs(panel or None))
 
+        name = email = ''
+        if req.authname != 'anonymous':
+            user = UserManager(self.env).get_user(req.authname)
+            name, email = user['name'], user['email']
+        else:
+            name = req.session.get('name')
+            email = req.session.get('email')
         return 'prefs_%s.html' % (panel or 'general'), {
-            'settings': {'session': req.session, 'session_id': req.session.sid},
+            'settings': {'session': req.session, 'session_id': req.session.sid,
+                         'name': name, 'email': email},
             'timezones': all_timezones, 'timezone': get_timezone
         }
 
@@ -118,9 +127,19 @@
                 elif field == 'newsid' and val:
                     req.session.change_sid(val)
                 else:
-                    req.session[field] = val
-            elif field in req.args and field in req.session:
-                del req.session[field]
+                    if req.authname != 'anonymous' and \
+                            field in ('name', 'email'):
+                        user = UserManager(self.env).get_user(req.authname)
+                        user[field] = val
+                    else:
+                        req.session[field] = val
+            elif field in req.args:
+                if req.authname != 'anonymous' and \
+                        field in ('name', 'email'):
+                    user = UserManager(self.env).get_user(req.authname)
+                    user[field] = ''
+                elif field in req.session:
+                    del req.session[field]
 
     def _do_load(self, req):
         if req.authname == 'anonymous':
Index: trac/test.py
===================================================================
--- trac/test.py	(revision 5898)
+++ trac/test.py	(working copy)
@@ -187,7 +187,7 @@
     def get_db_cnx(self):
         return self.db
 
-    def get_known_users(self, db):
+    def get_known_users(self):
         return self.known_users
 
 
Index: trac/ticket/admin.py
===================================================================
--- trac/ticket/admin.py	(revision 5898)
+++ trac/ticket/admin.py	(working copy)
@@ -107,11 +107,7 @@
 
         if self.config.getbool('ticket', 'restrict_owner'):
             perm = PermissionSystem(self.env)
-            def valid_owner(username):
-                return perm.get_user_permissions(username).get('TICKET_MODIFY')
-            data['owners'] = [username for username, name, email
-                              in self.env.get_known_users()
-                              if valid_owner(username)]
+            data['owners'] = perm.get_users_with_permission('TICKET_MODIFY')
         else:
             data['owners'] = None
 
Index: trac/ticket/report.py
===================================================================
--- trac/ticket/report.py	(revision 5898)
+++ trac/ticket/report.py	(working copy)
@@ -27,6 +27,7 @@
 from trac.core import *
 from trac.db import get_column_names
 from trac.perm import IPermissionRequestor
+from trac.user import UserManager
 from trac.util import sorted
 from trac.util.datefmt import format_datetime, format_time
 from trac.util.text import to_unicode, unicode_urlencode
@@ -369,9 +370,7 @@
         # Get the email addresses of all known users
         email_map = {}
         if Chrome(self.env).show_email_addresses:
-            for username, name, email in self.env.get_known_users():
-                if email:
-                    email_map[username] = email
+            email_map = UserManager(self.env).get_attribute_mapper('email')
 
         data.update({'header_groups': header_groups,
                      'row_groups': row_groups,
Index: trac/timeline/web_ui.py
===================================================================
--- trac/timeline/web_ui.py	(revision 5912)
+++ trac/timeline/web_ui.py	(working copy)
@@ -30,6 +30,7 @@
 from trac.perm import IPermissionRequestor
 from trac.timeline.api import ITimelineEventProvider, TimelineEvent
 from trac.util.compat import sorted
+from trac.user import UserManager
 from trac.util.datefmt import format_date, format_datetime, parse_date, \
                               to_timestamp, utc, pretty_timedelta
 from trac.util.text import to_unicode
@@ -167,9 +168,7 @@
             # Get the email addresses of all known users
             email_map = {}
             if Chrome(self.env).show_email_addresses:
-                for username, name, email in self.env.get_known_users():
-                    if email:
-                        email_map[username] = email
+                email_map = UserManager(self.env).get_attribute_mapper('email')
             data['email_map'] = email_map
             return 'timeline.rss', data, 'application/rss+xml'
 
Index: trac/user.py
===================================================================
--- trac/user.py	(revision 0)
+++ trac/user.py	(revision 0)
@@ -0,0 +1,352 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2006 Waldemar Kornewald, wkornewald@haiku-os.org
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
+#
+# Author: Waldemar Kornewald, wkornewald@haiku-os.org
+
+from trac.core import *
+from trac.config import *
+
+
+class IUserStore(Interface):
+    """
+    Extension point interface for backends that store known users.
+    """
+
+    def supports_user_operation(self, operation):
+        """
+        Returns whether the operation (a method name) is supported.
+
+        @return supported
+        """
+
+    def create_user(self, username, password):
+        """
+        Creates a new user with the given username and password.
+
+        @return success
+        """
+
+    def get_usernames(self):
+        """
+        Generator that yields an ordered list of known usernames.
+
+        @return username
+        """
+
+    def check_password(self, username, password):
+        """
+        Checks if the password is correct for the given user.
+        """
+
+    def change_password(self, username, password):
+        """
+        Changes a user's password.
+
+        @return success
+        """
+
+    def delete_user(self, username):
+        """
+        Deletes a user.
+
+        @return success
+            Returns False if the user didn't exist.
+        """
+
+
+class IUserAttributeProvider(Interface):
+    """
+    Extension point interface for backends that store user attributes.
+    """
+
+    def supports_attribute_operation(self, operation):
+        """
+        Returns whether the operation (a method name) is supported.
+
+        @return supported
+        """
+
+    def get_user_attribute(self, username, attribute):
+        """
+        Returns a user attribute.
+
+        If the attribute is not set it returs an empty string.
+        If the attribute is not supported None is returned.
+        """
+
+    def set_user_attribute(self, username, attribute, value):
+        """
+        Sets a user attribute.
+
+        @return success
+            Returns False if setting the attribute is not supported.
+        """
+
+    def delete_all_user_attributes(self, username):
+        """
+        Deletes all of the given user's attributes.
+        """
+
+class UserAttributeMapper(object):
+    """
+    Dict that paps usernames to attributes and caches those values to
+    increase efficiency.
+    """
+
+    _usernames_cache = None
+    _cache = {}
+
+    def __init__(self, attribute, manager):
+        self._attribute = attribute
+        self._manager = manager
+
+    def __getitem__(self, username):
+        if not self._usernames_cache:
+            self._usernames_cache = [username for username
+                                     in self._manager.get_usernames()]
+        if username not in self._usernames_cache:
+            return None
+        if username not in self._cache:
+            value = self._manager.get_user(username)[self._attribute]
+            if value:
+                self._cache[username] = value
+            return value
+        else:
+            return self._cache.get(username)
+
+    def __contains__(self, username):
+        value = self[username]
+        return value is not None and len(value) > 0
+
+    def __setitem__(self,key,value):
+        raise NotImplementedError, "dict is immutable"
+    def __delitem__(self,key):
+        raise NotImplementedError, "dict is immutable"
+    def clear(self):
+        raise NotImplementedError, "dict is immutable"
+    def setdefault(self,k,default=None):
+        raise NotImplementedError, "dict is immutable"
+    def popitem(self):
+        raise NotImplementedError, "dict is immutable"
+    def update(self,other):
+        raise NotImplementedError, "dict is immutable"
+
+
+class UserManager(Component):
+    """
+    Component responsible for managing users and user attributes.
+    """
+
+    store = ExtensionOption('users',
+        'store', IUserStore, 'SessionUserStore',
+        doc="""The user store that should be used for authentication
+            (''since 0.11'').""")
+    attribute_providers = OrderedExtensionsOption('users',
+        'attribute_providers', IUserAttributeProvider,
+        doc="""Ordered list of user attribute providers (''since 0.11'').""")
+
+    # public API
+
+    def supports_operation(self, operation):
+        if self.store.supports_user_operation(operation):
+            return True
+        for provider in self.attribute_providers:
+            if self.provider.supports_attribute_operation(operation):
+                return True
+        return False
+
+    # IUserStore methods
+
+    def create_user(self, username, password):
+        """
+        Creates a new user with the given username and password.
+
+        @return User object or None if the user couldn't be created
+        """
+        if not self.store.supports_user_operation('create_user'):
+            return None
+        if self.store.create_user(username, password):
+            return User(username, self.store, self.attribute_providers)
+
+    def get_user(self, username):
+        """
+        Returns a User object for the username.
+
+        @return User object or None if the user doesn't exist
+        """
+        return User(username, self.store, self.attribute_providers)
+
+    def get_all_users(self):
+        """
+        Generator for User objects.
+        """
+        if not self.store.supports_user_operation('get_usernames'):
+            return
+        for username in self.store.get_usernames():
+            yield User(username, self.store, self.attribute_providers)
+
+    def get_usernames(self):
+        """
+        Generator for usernames.
+        """
+        if not self.store.supports_user_operation('get_usernames'):
+            return
+        return self.store.get_usernames()
+
+    def get_attribute_mapper(self, attribute):
+        return UserAttributeMapper(attribute, self)
+
+
+class User(object):
+    """
+    Object representing a user.
+    """
+
+    def __init__(self, username, store, attribute_providers):
+        self._username = username
+        self.store = store
+        self.attribute_providers = attribute_providers
+
+    # public API
+
+    @property
+    def username(self):
+        return self._username
+
+    def exists(self):
+        return self.username in [username for username
+                                 in self.store.get_usernames()]
+
+    # IUserStore methods
+
+    def check_password(self, password):
+        if not self.store.supports_user_operation('check_password'):
+            return False
+        return self.store.check_password(self.username, password)
+
+    def change_password(self, password):
+        if not self.store.supports_user_operation('change_password'):
+            return False
+        return self.store.change_password(self.username, password)
+
+    def delete(self):
+        if not self.store.supports_user_operation('delete_user'):
+            return False
+        self.delete_all_attributes()
+        return self.store.delete_user(self.username)
+
+    # IUserAttributeProvider methods
+
+    def __getitem__(self, attribute):
+        for provider in self.attribute_providers:
+            if not provider.supports_attribute_operation('get_user_attribute'):
+                continue
+            value = provider.get_user_attribute(self.username, attribute)
+            if value is not None:
+                return value
+        return None
+
+    def __setitem__(self, attribute, value):
+        for provider in self.attribute_providers:
+            if provider.supports_attribute_operation('set_user_attribute') \
+                    and provider.set_user_attribute(self.username, attribute,
+                                                    value):
+                return True
+        return False
+
+    def delete_all_attributes(self):
+        for provider in self.attribute_providers:
+            if provider.supports_attribute_operation('delete_all_user_attributes'):
+                provider.delete_all_user_attributes(username)
+
+
+class SessionUserStore(Component):
+    """
+    Component for managing authenticated users stored in sessions.
+    """
+
+    implements(IUserStore)
+
+    def supports_user_operation(self, operation):
+        return hasattr(self, operation)
+
+    def get_usernames(self):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT sid FROM session "
+                       "WHERE authenticated=1 "
+                       "ORDER BY sid")
+        for row in cursor:
+            yield row[0]
+
+
+class SessionUserAttributeProvider(Component):
+    """
+    Component for providing user attributes via Trac sessions.
+    """
+
+    implements(IUserAttributeProvider)
+
+    def supports_attribute_operation(self, operation):
+        return hasattr(self, operation)
+
+    def get_user_attribute(self, username, attribute):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT value FROM session_attribute "
+                       "WHERE sid=%s AND name=%s AND authenticated=1",
+                       (username, attribute))
+        row = cursor.fetchone()
+        if row:
+            return row[0]
+
+        # if the attribute doesn't exist we return an empty string to
+        # indicate that the attribute is supported, but not set
+        return ''
+
+    def set_user_attribute(self, username, attribute, value):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+
+        if not value or value == '':
+            cursor.execute("DELETE FROM session_attribute "
+                           "WHERE sid=%s AND name=%s AND authenticated=1",
+                          (username, attribute))
+            db.commit()
+            return True
+
+        # check if attribute exists
+        cursor.execute("SELECT value FROM session_attribute "
+                       "WHERE sid=%s AND name=%s AND authenticated=1",
+                       (username, attribute))
+        if cursor.fetchone():
+            # update the attribute
+            cursor.execute("UPDATE session_attribute SET value=%s "
+                           "WHERE sid=%s AND name=%s AND authenticated=1",
+                           (value, username, attribute))
+        else:
+            # create new attribute
+            cursor.execute("INSERT INTO session_attribute "
+                           "(sid,authenticated,name,value) "
+                           "VALUES(%s,1,%s,%s)",
+                           (username, attribute, value))
+        db.commit()
+        return True
+
+    def delete_all_user_attributes(self, username):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("DELETE FROM session_attribute "
+                       "WHERE sid=%s AND authenticated=1",
+                      (username,))
+        db.commit()
Index: trac/versioncontrol/web_ui/log.py
===================================================================
--- trac/versioncontrol/web_ui/log.py	(revision 5898)
+++ trac/versioncontrol/web_ui/log.py	(working copy)
@@ -23,6 +23,7 @@
 from trac.context import Context
 from trac.core import *
 from trac.perm import IPermissionRequestor
+from trac.user import UserManager
 from trac.util import Ranges
 from trac.util.datefmt import http_date
 from trac.util.html import html
@@ -193,9 +194,7 @@
         if format == 'rss':
             # Get the email addresses of all known users
             if Chrome(self.env).show_email_addresses:
-                for username,name,email in self.env.get_known_users():
-                    if email:
-                        email_map[username] = email
+                email_map = UserManager(self.env).get_attribute_mapper('email')
         elif format == 'changelog':
             for rev in revs:
                 changeset = changes[rev]

