diff --git a/attachment.php b/attachment.php
index d780ae2da4187c41a0607afa30267504c7597f2c..1c0941d7d545d5bbdfaeaa50029d9900ba6e5b39 100644
--- a/attachment.php
+++ b/attachment.php
@@ -17,8 +17,8 @@
 require('secure.inc.php');
 require_once(INCLUDE_DIR.'class.attachment.php');
 //Basic checks
-if(!$thisclient 
-        || !$_GET['id'] 
+if(!$thisclient
+        || !$_GET['id']
         || !$_GET['h']
         || !($attachment=Attachment::lookup($_GET['id']))
         || !($file=$attachment->getFile()))
@@ -26,9 +26,9 @@ if(!$thisclient
 
 //Validate session access hash - we want to make sure the link is FRESH! and the user has access to the parent ticket!!
 $vhash=md5($attachment->getFileId().session_id().$file->getHash());
-if(strcasecmp(trim($_GET['h']),$vhash) 
-        || !($ticket=$attachment->getTicket()) 
-        || !$ticket->checkClientAccess($thisclient)) 
+if(strcasecmp(trim($_GET['h']),$vhash)
+        || !($ticket=$attachment->getTicket())
+        || !$ticket->checkUserAccess($thisclient))
     die('Unknown or invalid attachment');
 //Download the file..
 $file->download();
diff --git a/client.inc.php b/client.inc.php
index 2ab016d15c6a252af4816ef867748c991f8fd760..84eeaca1136446c977a6ce5e5ec26093b8244cc6 100644
--- a/client.inc.php
+++ b/client.inc.php
@@ -43,11 +43,9 @@ require_once(INCLUDE_DIR.'class.dept.php');
 //clear some vars
 $errors=array();
 $msg='';
-$thisclient=$nav=null;
+$nav=null;
 //Make sure the user is valid..before doing anything else.
-if($_SESSION['_client']['userID'] && $_SESSION['_client']['key'])
-    $thisclient = new ClientSession($_SESSION['_client']['userID'],$_SESSION['_client']['key']);
-
+$thisclient = UserAuthenticationBackend::getUser();
 //is the user logged in?
 if($thisclient && $thisclient->getId() && $thisclient->isValid()){
      $thisclient->refreshSession();
diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php
index 6325950742f9dc68fb55aeb7d87142eb15c5f487..5137cbaabc670f7e15d0842df05fa127328aab83 100644
--- a/include/ajax.tickets.php
+++ b/include/ajax.tickets.php
@@ -31,11 +31,11 @@ class TicketsAjaxAPI extends AjaxController {
         $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
         $tickets=array();
 
-        $sql='SELECT DISTINCT ticketID, email.address AS email'
+        $sql='SELECT DISTINCT `number`, email.address AS email'
             .' FROM '.TICKET_TABLE.' ticket'
             .' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id'
             .' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id'
-            .' WHERE ticketID LIKE \''.db_input($_REQUEST['q'], false).'%\'';
+            .' WHERE `number` LIKE \''.db_input($_REQUEST['q'], false).'%\'';
 
         $sql.=' AND ( staff_id='.db_input($thisstaff->getId());
 
@@ -464,7 +464,7 @@ class TicketsAjaxAPI extends AjaxController {
             Http::response(404, 'No such ticket');
         elseif (!$bk || !$id)
             Http::response(422, 'Backend and user id required');
-        elseif (!($backend = AuthenticationBackend::getBackend($bk)))
+        elseif (!($backend = StaffAuthenticationBackend::getBackend($bk)))
             Http::response(404, 'User not found');
 
         $user_info = $backend->lookup($id);
diff --git a/include/ajax.users.php b/include/ajax.users.php
index 7be4874ec1f64e528fafcc0676ac6e60498c1d0d..698f2f49de10ffd767b7cfce62a2fb32f05b1a6e 100644
--- a/include/ajax.users.php
+++ b/include/ajax.users.php
@@ -51,7 +51,7 @@ class UsersAjaxAPI extends AjaxController {
             }
         }
 
-        foreach (AuthenticationBackend::searchUsers($_REQUEST['q']) as $u) {
+        foreach (StaffAuthenticationBackend::searchUsers($_REQUEST['q']) as $u) {
             $name = "{$u['first']} {$u['last']}";
             $users[] = array('email' => $u['email'], 'name'=>$name,
                 'info' => "{$u['email']} - $name (remote)",
@@ -122,7 +122,7 @@ class UsersAjaxAPI extends AjaxController {
             Http::response(403, 'Login Required');
         elseif (!$bk || !$id)
             Http::response(422, 'Backend and user id required');
-        elseif (!($backend = AuthenticationBackend::getBackend($bk)))
+        elseif (!($backend = StaffAuthenticationBackend::getBackend($bk)))
             Http::response(404, 'User not found');
 
         $user_info = $backend->lookup($id);
@@ -177,7 +177,7 @@ class UsersAjaxAPI extends AjaxController {
             Http::response(400, 'Query argument is required');
 
         $users = array();
-        foreach (AuthenticationBackend::allRegistered() as $ab) {
+        foreach (StaffAuthenticationBackend::allRegistered() as $ab) {
             if (!$ab instanceof AuthDirectorySearch)
                 continue;
 
diff --git a/include/api.tickets.php b/include/api.tickets.php
index daa7ffe431a17d43d39227347f4b3efc0ac8053b..fb12e623c03328eada3e02bfddba0178c39860c9 100644
--- a/include/api.tickets.php
+++ b/include/api.tickets.php
@@ -97,7 +97,7 @@ class TicketApiController extends ApiController {
         if(!$ticket)
             return $this->exerr(500, "Unable to create new ticket: unknown error");
 
-        $this->response(201, $ticket->getExtId());
+        $this->response(201, $ticket->getNumber());
     }
 
     /* private helper functions */
diff --git a/include/class.auth.php b/include/class.auth.php
index 08760ed9d910ec0d1f1f41213a2aca13b9cd94d2..e3bce85fe5b83f8e5c11c626ac6115845a185a18 100644
--- a/include/class.auth.php
+++ b/include/class.auth.php
@@ -2,13 +2,36 @@
 require(INCLUDE_DIR.'class.ostsession.php');
 require(INCLUDE_DIR.'class.usersession.php');
 
-class AuthenticatedUser {
-    // How the user was authenticated
-    var $backend;
+
+abstract class AuthenticatedUser {
+    //Authorization key returned by the backend used to authorize the user
+    private $authkey;
 
     // Get basic information
-    function getId() {}
-    function getUsername() {}
+    abstract function getId();
+    abstract function getUsername();
+    abstract function getRole();
+
+    //Backend used to authenticate the user
+    abstract function getAuthBackend();
+
+    //Authentication key
+    function setAuthKey($key) {
+        $this->authkey = $key;
+    }
+
+    function getAuthKey() {
+        return $this->authkey;
+    }
+
+    // logOut the user
+    function logOut() {
+
+        if ($bk = $this->getAuthBackend())
+            return $bk->signOut($this);
+
+        return false;
+    }
 }
 
 interface AuthDirectorySearch {
@@ -42,15 +65,25 @@ interface AuthDirectorySearch {
  * receives a username and optional password. If the authentication
  * succeeds, an instance deriving from <User> should be returned.
  */
-class AuthenticationBackend {
-    static private $registry = array();
+abstract class AuthenticationBackend {
+    static protected $registry = array();
     static $name;
     static $id;
 
+
     /* static */
     static function register($class) {
-        if (is_string($class))
+        if (is_string($class) && class_exists($class))
             $class = new $class();
+
+        if (!is_object($class)
+                || !($class instanceof AuthenticationBackend))
+            return false;
+
+        return static::_register($class);
+    }
+
+    static function _register($class) {
         // XXX: Raise error if $class::id is already in the registry
         static::$registry[$class::$id] = $class;
     }
@@ -60,55 +93,57 @@ class AuthenticationBackend {
     }
 
     static function getBackend($id) {
-        return static::$registry[$id];
+
+        if ($id
+                && ($backends = static::allRegistered())
+                && isset($backends[$id]))
+            return $backends[$id];
     }
 
-    /* static */
-    function process($username, $password=null, &$errors) {
+    static function process($username, $password=null, &$errors) {
+
         if (!$username)
             return false;
 
-        $backend = static::_getAllowedBackends($username);
-
-        foreach (static::$registry as $bk) {
-            if ($backend && $bk->supportsAuthentication() && $bk::$id != $backend)
+        $backends =  static::getAllowedBackends($username);
+        foreach (static::allRegistered() as $bk) {
+            if ($backends //Allowed backends
+                    && $bk->supportsAuthentication()
+                    && !in_array($bk::$id, $backends))
                 // User cannot be authenticated against this backend
                 continue;
+
             // All backends are queried here, even if they don't support
             // authentication so that extensions like lockouts and audits
             // can be supported.
             $result = $bk->authenticate($username, $password);
-            if ($result instanceof AuthenticatedUser) {
-                static::_login($result, $username, $bk);
-                $result->backend = $bk;
+
+            if ($result instanceof AuthenticatedUser
+                    && ($bk->login($result, $bk)))
                 return $result;
-            }
             // TODO: Handle permission denied, for instance
             elseif ($result instanceof AccessDenied) {
                 $errors['err'] = $result->reason;
                 break;
             }
         }
+
         $info = array('username'=>$username, 'password'=>$password);
         Signal::send('auth.login.failed', null, $info);
     }
 
-    function singleSignOn(&$errors) {
-        global $ost;
+    function processSignOn(&$errors) {
 
-        foreach (static::$registry as $bk) {
+        foreach (static::allRegistered() as $bk) {
             // All backends are queried here, even if they don't support
             // authentication so that extensions like lockouts and audits
             // can be supported.
             $result = $bk->signOn();
             if ($result instanceof AuthenticatedUser) {
-                // Ensure staff members are allowed to be authenticated
-                // against this backend
-                if ($result instanceof Staff
-                        && !static::_isBackendAllowed($result, $bk))
+                //Perform further Object specific checks and the actual login
+                if (!$bk->login($result, $bk))
                     continue;
-                static::_login($result, $result->getUserName(), $bk);
-                $result->backend = $bk;
+
                 return $result;
             }
             // TODO: Handle permission denied, for instance
@@ -121,7 +156,7 @@ class AuthenticationBackend {
 
     static function searchUsers($query) {
         $users = array();
-        foreach (static::$registry as $bk) {
+        foreach (static::allRegistered() as $bk) {
             if ($bk instanceof AuthDirectorySearch) {
                 $users += $bk->search($query);
             }
@@ -129,42 +164,129 @@ class AuthenticationBackend {
         return $users;
     }
 
-    function _isBackendAllowed($staff, $bk) {
-        $sql = 'SELECT backend FROM '.STAFF_TABLE
-            .' WHERE staff_id='.db_input($staff->getId());
-        $backend = db_result(db_query($sql));
-        return !$backend || strcasecmp($bk::$id, $backend) === 0;
+    /**
+     * Fetches the friendly name of the backend
+     */
+    function getName() {
+        return static::$name;
+    }
+
+    /**
+     * Indicates if the backed supports authentication. Useful if the
+     * backend is used for logging or lockout only
+     */
+    function supportsAuthentication() {
+        return true;
+    }
+
+    /**
+     * Indicates if the backend supports changing a user's password. This
+     * would be done in two fashions. Either the currently-logged in user
+     * want to change its own password or a user requests to have their
+     * password reset. This requires an administrative privilege which this
+     * backend might not possess, so it's defined in supportsPasswordReset()
+     */
+    function supportsPasswordChange() {
+        return false;
+    }
+
+    function supportsPasswordReset() {
+        return false;
+    }
+
+    function signOn() {
+        return null;
+    }
+
+    protected function validate($auth) {
+        return null;
+    }
+
+    abstract function authenticate($username, $password);
+    abstract function login($user, $bk);
+    abstract static function getUser(); //Validates  authenticated users.
+    abstract function getAllowedBackends($userid);
+    abstract protected function getAuthKey($user);
+    abstract static function signOut($user);
+}
+
+class RemoteAuthenticationBackend {
+    var $create_unknown_user = false;
+}
+
+abstract class StaffAuthenticationBackend  extends AuthenticationBackend {
+
+    static private $_registry = array();
+
+    static function _register($class) {
+        static::$_registry[$class::$id] = $class;
     }
 
-    function _getAllowedBackends($username) {
-        $username = trim($_POST['userid']);
+    static function allRegistered() {
+        return array_merge(self::$_registry, parent::allRegistered());
+    }
+
+    function isBackendAllowed($staff, $bk) {
+
+        if (!($backends=self::getAllowedBackends($staff->getId())))
+            return true;  //No restrictions
+
+        return in_array($bk::$id, array_map('strtolower', $backends));
+    }
+
+    function getAllowedBackends($userid) {
+
+        $backends =array();
+        //XXX: Only one backend can be specified at the moment.
         $sql = 'SELECT backend FROM '.STAFF_TABLE
-            .' WHERE username='.db_input($username)
-            .' OR email='.db_input($username);
-        return db_result(db_query($sql));
+              .' WHERE backend IS NOT NULL ';
+        if (is_numeric($userid))
+            $sql.= ' AND staff_id='.db_input($userid);
+        else {
+            $sql.= ' AND (username='.db_input($userid) .' OR email='.db_input($userid).')';
+        }
+
+        if (($res=db_query($sql)) && db_num_rows($res))
+            $backends[] = db_result($res);
+
+        return array_filter($backends);
     }
 
-    function _login($user, $username, $bk) {
+    function login($staff, $bk) {
         global $ost;
 
-        if ($user instanceof Staff) {
-            //Log debug info.
-            $ost->logDebug('Staff login',
-                sprintf("%s logged in [%s], via %s", $user->getUserName(),
-                    $_SERVER['REMOTE_ADDR'], get_class($bk))); //Debug.
+        if (!$bk || !($staff instanceof Staff))
+            return false;
 
-            $sql='UPDATE '.STAFF_TABLE.' SET lastlogin=NOW() '
-                .' WHERE staff_id='.db_input($user->getId());
-            db_query($sql);
-            //Now set session crap and lets roll baby!
-            $_SESSION['_staff'] = array(); //clear.
-            $_SESSION['_staff']['userID'] = $username;
+        // Ensure staff is allowed for realz to be authenticated via the backend.
+        if (!static::isBackendAllowed($staff, $bk)
+            || !($authkey=$bk->getAuthKey($staff)))
+            return false;
 
-            $user->refreshSession(); //set the hash.
+        //Log debug info.
+        $ost->logDebug('Staff login',
+            sprintf("%s logged in [%s], via %s", $staff->getUserName(),
+                $_SERVER['REMOTE_ADDR'], get_class($bk))); //Debug.
 
-            $_SESSION['TZ_OFFSET'] = $user->getTZoffset();
-            $_SESSION['TZ_DST'] = $user->observeDaylight();
-        }
+        $sql='UPDATE '.STAFF_TABLE.' SET lastlogin=NOW() '
+            .' WHERE staff_id='.db_input($staff->getId());
+        db_query($sql);
+
+        //Tag the authkey.
+        $authkey = $bk::$id.':'.$authkey;
+
+        //Now set session crap and lets roll baby!
+        $authsession = &$_SESSION['_auth']['staff'];
+
+        $authsession = array(); //clear.
+        $authsession['id'] = $staff->getId();
+        $authsession['key'] =  $authkey;
+
+        $staff->setAuthKey($authkey);
+        $staff->refreshSession(); //set the hash.
+
+        $_SESSION['TZ_OFFSET'] = $staff->getTZoffset();
+        $_SESSION['TZ_DST'] = $staff->observeDaylight();
 
         //Regenerate session id.
         $sid = session_id(); //Current id
@@ -175,54 +297,168 @@ class AuthenticationBackend {
                 && $sid!=session_id())
             $session->destroy($sid);
 
-        Signal::send('auth.login.succeeded', $user);
+        Signal::send('auth.login.succeeded', $staff);
+
+        $staff->cancelResetTokens();
 
-        $user->cancelResetTokens();
+        return true;
     }
 
-    /**
-     * Fetches the friendly name of the backend
+    /* Base signOut
+     *
+     * Backend should extend the signout and perform any additional signout
+     * it requires.
      */
-    function getName() {
-        return static::$name;
+
+    static function signOut($staff) {
+        global $ost;
+
+        $_SESSION['_auth']['staff'] = array();
+        $ost->logDebug('Staff logout',
+                sprintf("%s logged out [%s]",
+                    $staff->getUserName(),
+                    $_SERVER['REMOTE_ADDR'])); //Debug.
+
+        Signal::send('auth.logout', $staff);
     }
 
-    /**
-     * Indicates if the backed supports authentication. Useful if the
-     * backend is used for logging or lockout only
-     */
-    function supportsAuthentication() {
-        return true;
+    // Called to get authenticated user (if any)
+    static function getUser() {
+
+        if (!isset($_SESSION['_auth']['staff'])
+                || !$_SESSION['_auth']['staff']['key'])
+            return null;
+
+        list($id, $auth) = explode(':', $_SESSION['_auth']['staff']['key']);
+
+        if (!($bk=static::getBackend($id)) //get the backend
+                || !$bk->supportsAuthentication() //Make sure it can authenticate
+                || !($staff = $bk->validate($auth)) //Get AuthicatedUser
+                || !($staff instanceof Staff)
+                || $staff->getId() != $_SESSION['_auth']['staff']['id'] // check ID
+                )
+            return null;
+
+        $staff->setAuthKey($_SESSION['_auth']['staff']['key']);
+
+
+        return $staff;
     }
 
-    /**
-     * Indicates if the backend supports changing a user's password. This
-     * would be done in two fashions. Either the currently-logged in user
-     * want to change its own password or a user requests to have their
-     * password reset. This requires an administrative privilege which this
-     * backend might not possess, so it's defined in supportsPasswordReset()
-     */
-    function supportsPasswordChange() {
+    function authenticate($username, $password) {
         return false;
     }
 
-    function supportsPasswordReset() {
-        return false;
+    // Generic authentication key for staff's backend is the username
+    protected function getAuthKey($staff) {
+
+        if(!($staff instanceof Staff))
+            return null;
+
+        return $staff->getUsername();
+    }
+
+    protected function validate($authkey) {
+
+        if (($staff = new StaffSession($authkey)) && $staff->getId())
+            return $staff;
+    }
+}
+
+abstract class UserAuthenticationBackend  extends AuthenticationBackend {
+
+    static private $_registry = array();
+
+    static function _register($class) {
+        static::$_registry[$class::$id] = $class;
+    }
+
+    static function allRegistered() {
+        return array_merge(self::$_registry, parent::allRegistered());
+    }
+
+    function getAllowedBackends($userid) {
+        // White listing backends for specific user not supported.
+        return array();
+    }
+
+    function login($user, $bk) {
+        global $ost;
+
+        if (!$user || !$bk
+                || !$bk::$id //Must have ID
+                || !($authkey = $bk->getAuthKey($user)))
+            return false;
+
+        //Tag the authkey.
+        $authkey = $bk::$id.':'.$authkey;
+
+        //Set the session goodies
+        $authsession = &$_SESSION['_auth']['user'];
+
+        $authsession = array(); //clear.
+        $authsession['id'] = $user->getId();
+        $authsession['key'] = $authkey;
+        $_SESSION['TZ_OFFSET'] = $ost->getConfig()->getTZoffset();
+        $_SESSION['TZ_DST'] = $ost->getConfig()->observeDaylightSaving();
+
+        //The backend used decides the format of the auth key.
+        // XXX: encrypt to hide the bk??
+        $user->setAuthKey($authkey);
+
+        $user->refreshSession(); //set the hash.
+
+        //Log login info...
+        $msg=sprintf('%s (%s) logged in [%s]',
+                $user->getUserName(), $user->getId(), $_SERVER['REMOTE_ADDR']);
+        $ost->logDebug('User login', $msg);
+
+        //Regenerate session ID.
+        $sid=session_id(); //Current session id.
+        session_regenerate_id(TRUE); //get new ID.
+        if(($session=$ost->getSession()) && is_object($session) && $sid!=session_id())
+            $session->destroy($sid);
+
+        return true;
     }
 
-    /* abstract */
     function authenticate($username, $password) {
         return false;
     }
 
-    /* abstract */
-    function signOn() {
-        return false;
+    static function signOut($user) {
+        global $ost;
+
+        $_SESSION['_auth']['user'] = array();
+        $ost->logDebug('User logout',
+                sprintf("%s logged out [%s]",
+                    $user->getUserName(), $_SERVER['REMOTE_ADDR']));
     }
-}
 
-class RemoteAuthenticationBackend {
-    var $create_unknown_user = false;
+    protected function getAuthKey($user) {
+        return  $user->getUsername();
+    }
+
+    static function getUser() {
+
+        if (!isset($_SESSION['_auth']['user'])
+                || !$_SESSION['_auth']['user']['key'])
+            return null;
+
+        list($id, $auth) = explode(':', $_SESSION['_auth']['user']['key']);
+
+        if (!($bk=static::getBackend($id)) //get the backend
+                || !$bk->supportsAuthentication() //Make sure it can authenticate
+                || !($user=$bk->validate($auth)) //Get AuthicatedUser
+                || !($user instanceof AuthenticatedUser) // Make sure it user
+                || $user->getId() != $_SESSION['_auth']['user']['id'] // check ID
+                )
+            return null;
+
+        $user->setAuthKey($_SESSION['_auth']['user']['key']);
+
+        return $user;
+    }
 }
 
 /**
@@ -241,52 +477,137 @@ class AccessDenied {
  * Simple authentication backend which will lock the login form after a
  * configurable number of attempts
  */
-class AuthLockoutBackend extends AuthenticationBackend {
+abstract class AuthStrikeBackend extends AuthenticationBackend {
 
     function authenticate($username, $password=null) {
-        global $cfg, $ost;
+        return static::authStrike($username, $password);
+    }
 
-        if($_SESSION['_staff']['laststrike']) {
-            if((time()-$_SESSION['_staff']['laststrike'])<$cfg->getStaffLoginTimeout()) {
-                $_SESSION['_staff']['laststrike'] = time(); //reset timer.
+    function signOn() {
+        return static::authStrike('Unknown');
+    }
+
+    static function signOut($user) {
+        return false;
+    }
+
+
+    function login($user, $bk) {
+        return false;
+    }
+
+    static function getUser() {
+        return null;
+    }
+
+    function supportsAuthentication() {
+        return false;
+    }
+
+    function getAllowedBackends($userid) {
+        return array();
+    }
+
+    function getAuthKey($user) {
+        return null;
+    }
+
+    abstract function  authStrike($username, $password=null);
+}
+
+/*
+ * Backend to monitor staff's failed login attempts
+ */
+class StaffAuthStrikeBackend extends  AuthStrikeBackend {
+
+    function authstrike($username, $password=null) {
+        global $ost;
+
+        $cfg = $ost->getConfig();
+
+        $authsession = &$_SESSION['_auth']['staff'];
+
+        if($authsession['laststrike']) {
+            if((time()-$authsession['laststrike'])<$cfg->getStaffLoginTimeout()) {
+                $authsession['laststrike'] = time(); //reset timer.
                 return new AccessDenied('Max. failed login attempts reached');
             } else { //Timeout is over.
                 //Reset the counter for next round of attempts after the timeout.
-                $_SESSION['_staff']['laststrike']=null;
-                $_SESSION['_staff']['strikes']=0;
+                $authsession['laststrike']=null;
+                $authsession['strikes']=0;
             }
         }
 
-        $_SESSION['_staff']['strikes']+=1;
-        if($_SESSION['_staff']['strikes']>$cfg->getStaffMaxLogins()) {
-            $_SESSION['_staff']['laststrike']=time();
+        $authsession['strikes']+=1;
+        if($authsession['strikes']>$cfg->getStaffMaxLogins()) {
+            $authsession['laststrike']=time();
             $alert='Excessive login attempts by a staff member?'."\n".
                    'Username: '.$username."\n"
                    .'IP: '.$_SERVER['REMOTE_ADDR']."\n"
                    .'TIME: '.date('M j, Y, g:i a T')."\n\n"
-                   .'Attempts #'.$_SESSION['_staff']['strikes']."\n"
+                   .'Attempts #'.$authsession['strikes']."\n"
                    .'Timeout: '.($cfg->getStaffLoginTimeout()/60)." minutes \n\n";
             $ost->logWarning('Excessive login attempts ('.$username.')', $alert,
                     $cfg->alertONLoginError());
             return new AccessDenied('Forgot your login info? Contact Admin.');
         //Log every other failed login attempt as a warning.
-        } elseif($_SESSION['_staff']['strikes']%2==0) {
+        } elseif($authsession['strikes']%2==0) {
             $alert='Username: '.$username."\n"
                     .'IP: '.$_SERVER['REMOTE_ADDR']."\n"
                     .'TIME: '.date('M j, Y, g:i a T')."\n\n"
-                    .'Attempts #'.$_SESSION['_staff']['strikes'];
+                    .'Attempts #'.$authsession['strikes'];
             $ost->logWarning('Failed staff login attempt ('.$username.')', $alert, false);
         }
     }
+}
+StaffAuthenticationBackend::register(StaffAuthStrikeBackend);
+
+/*
+ * Backend to monitor user's failed login attempts
+ */
+class UserAuthStrikeBackend extends  AuthStrikeBackend {
+
+    function authstrike($username, $password=null) {
+        global $ost;
+
+        $cfg = $ost->getConfig();
+
+        $authsession = &$_SESSION['_auth']['user'];
+
+        //Check time for last max failed login attempt strike.
+        if($authsession['laststrike']) {
+            if((time()-$authsession['laststrike'])<$cfg->getClientLoginTimeout()) {
+                $authsession['laststrike'] = time(); //renew the strike.
+                return new AccessDenied('You\'ve reached maximum failed login attempts allowed.');
+            } else { //Timeout is over.
+                //Reset the counter for next round of attempts after the timeout.
+                $authsession['laststrike'] = null;
+                $authsession['strikes'] = 0;
+            }
+        }
+
+        $authsession['strikes']+=1;
+        if($authsession['strikes']>$cfg->getClientMaxLogins()) {
+            $authsession['laststrike'] = time();
+            $alert='Excessive login attempts by a user.'."\n".
+                    'Login: '.$username.': '.$password."\n".
+                    'IP: '.$_SERVER['REMOTE_ADDR']."\n".'Time:'.date('M j, Y, g:i a T')."\n\n".
+                    'Attempts #'.$authsession['strikes'];
+            $ost->logError('Excessive login attempts (user)', $alert, ($cfg->alertONLoginError()));
+            return new AccessDenied('Access Denied');
+        } elseif($authsession['strikes']%2==0) { //Log every other failed login attempt as a warning.
+            $alert='Login: '.$username.': '.$password."\n".'IP: '.$_SERVER['REMOTE_ADDR'].
+                   "\n".'TIME: '.date('M j, Y, g:i a T')."\n\n".'Attempts #'.$authsession['strikes'];
+            $ost->logWarning('Failed login attempt (user)', $alert);
+        }
 
-    function supportsAuthentication() {
-        return false;
     }
 }
-AuthenticationBackend::register(AuthLockoutBackend);
+UserAuthenticationBackend::register(UserAuthStrikeBackend);
 
-class osTicketAuthentication extends AuthenticationBackend {
-    static $name = "Local Authenication";
+
+class osTicketAuthentication extends StaffAuthenticationBackend {
+    static $name = "Local Authentication";
     static $id = "local";
 
     function authenticate($username, $password) {
@@ -303,6 +624,129 @@ class osTicketAuthentication extends AuthenticationBackend {
             return $user;
         }
     }
+
+}
+StaffAuthenticationBackend::register(osTicketAuthentication);
+
+class PasswordResetTokenBackend extends StaffAuthenticationBackend {
+    static $id = "pwreset.staff";
+
+    function supportsAuthentication() {
+        return false;
+    }
+
+    function signOn($errors=array()) {
+        if (!isset($_POST['userid']) || !isset($_POST['token']))
+            return false;
+        elseif (!($_config = new Config('pwreset')))
+            return false;
+        elseif (($staff = new StaffSession($_POST['userid'])) &&
+                !$staff->getId())
+            $errors['msg'] = 'Invalid user-id given';
+        elseif (!($id = $_config->get($_POST['token']))
+                || $id != $staff->getId())
+            $errors['msg'] = 'Invalid reset token';
+        elseif (!($ts = $_config->lastModified($_POST['token']))
+                && ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
+            $errors['msg'] = 'Invalid reset token';
+        elseif (!$staff->forcePasswdRest())
+            $errors['msg'] = 'Unable to reset password';
+        else
+            return $staff;
+    }
+
+    function login($staff, $bk) {
+        $_SESSION['_staff']['reset-token'] = $_POST['token'];
+        Signal::send('auth.pwreset.login', $staff);
+        return parent::login($staff, $bk);
+    }
 }
-AuthenticationBackend::register(osTicketAuthentication);
+StaffAuthenticationBackend::register(PasswordResetTokenBackend);
+
+/*
+ * AuthToken Authentication Backend
+ *
+ * Provides auto-login facility for end users with valid link
+ *
+ * Ticket used to loggin is tracked durring the session this is
+ * important in the future when auto-logins will be
+ * limited to single ticket view.
+ */
+class AuthTokenAuthentication extends UserAuthenticationBackend {
+    static $name = "Auth Token Authentication";
+    static $id = "authtoken";
+
+
+    function signOn() {
+
+        $user = null;
+        if ($_GET['auth']) {
+            if (($u = TicketUser::lookupByToken($_GET['auth'])))
+                $user = new ClientSession($u);
+        }
+        // Support old ticket based tokens.
+        elseif ($_GET['t'] && $_GET['e'] && $_GET['a']) {
+            if (($ticket = Ticket::lookupByNumber($_GET['t'], $_GET['e']))
+                    // Using old ticket auth code algo - hardcoded here because it
+                    // will be removed in ticket class in the upcoming rewrite
+                    && !strcasecmp($_GET['a'], md5($ticket->getId() .  $_GET['e'] . SECRET_SALT))
+                    && ($owner = $ticket->getOwner()))
+                $user = new ClientSession($owner);
+        }
+
+        return $user;
+    }
+
+
+    protected function getAuthKey($user) {
+
+        if (!$this->supportsAuthentication() || !$user)
+            return null;
+
+        //Generate authkey based the type of ticket user
+        // It's required to validate users going forward.
+        $authkey = sprintf('%s%dt%dh%s',  //XXX: Placeholder
+                    ($user->isOwner() ? 'o':'c'),
+                    $user->getId(),
+                    $user->getTicketId(),
+                    md5($user->getId().$this->id));
+
+        return $authkey;
+    }
+
+    protected function validate($authkey) {
+
+        $regex = '/^(?P<type>\w{1})(?P<id>\d+)t(?P<tid>\d+)h(?P<hash>.*)$/i';
+        $matches = array();
+        if (!preg_match($regex, $authkey, $matches))
+            return false;
+
+        $user = null;
+        switch ($matches['type']) {
+            case 'c': //Collaborator
+                $criteria = array( 'userId' => $matches['id'],
+                        'ticketId' => $matches['tid']);
+                if (($c = Collaborator::lookup($criteria))
+                        && ($c->getTicketId() == $matches['tid']))
+                    $user = new ClientSession($c);
+                break;
+            case 'o': //Ticket owner
+                if (($ticket = Ticket::lookup($matches['tid']))
+                        && ($o = $ticket->getOwner())
+                        && ($o->getId() == $matches['id']))
+                    $user = new ClientSession($o);
+                break;
+        }
+
+        //Make sure the authkey matches.
+        if (!$user || strcmp($this->getAuthKey($user), $authkey))
+            return null;
+
+
+        return $user;
+    }
+
+}
+UserAuthenticationBackend::register(AuthTokenAuthentication);
+
 ?>
diff --git a/include/class.base32.php b/include/class.base32.php
new file mode 100755
index 0000000000000000000000000000000000000000..ce83828f873d208f72fb3cdce35ab990d6aab673
--- /dev/null
+++ b/include/class.base32.php
@@ -0,0 +1,121 @@
+<?php
+/*
+ * Base32 encoder/decoder
+ *
+ * Jared Hancock <jared@osticket.com>
+ * Copyright (c) osTicket.com
+ */
+
+
+class Base32 {
+
+    /**
+     * encode a binary string
+     *
+     * @param    $inString   Binary string to base32 encode
+     * @return   $outString  Base32 encoded $inString
+     *
+     * Original code from
+     * http://www.phpkode.com/source/p/moodle/moodle/lib/base32.php. Optimized
+     * to double performance
+     */
+
+    function encode($inString)
+    {
+        $outString = "";
+        $compBits = "";
+        static $BASE32_TABLE = array(
+            '00000' => 'a', '00001' => 'b', '00010' => 'c', '00011' => 'd',
+            '00100' => 'e', '00101' => 'f', '00110' => 'g', '00111' => 'h',
+            '01000' => 'i', '01001' => 'j', '01010' => 'k', '01011' => 'l',
+            '01100' => 'm', '01101' => 'n', '01110' => 'o', '01111' => 'p',
+            '10000' => 'q', '10001' => 'r', '10010' => 's', '10011' => 't',
+            '10100' => 'u', '10101' => 'v', '10110' => 'w', '10111' => 'x',
+            '11000' => 'y', '11001' => 'z', '11010' => '0', '11011' => '1',
+            '11100' => '2', '11101' => '3', '11110' => '4', '11111' => '5');
+
+        /* Turn the compressed string into a string that represents the bits as 0 and 1. */
+        for ($i = 0, $k = strlen($inString); $i < $k; $i++) {
+            $compBits .= str_pad(decbin(ord($inString[$i])), 8, '0', STR_PAD_LEFT);
+        }
+
+        /* Pad the value with enough 0's to make it a multiple of 5 */
+        if ((($len = strlen($compBits)) % 5) != 0) {
+            $compBits = str_pad($compBits, $len+(5-($len % 5)), '0', STR_PAD_RIGHT);
+        }
+
+        /* Create an array by chunking it every 5 chars */
+        $fiveBitsArray = str_split($compBits, 5);
+
+        /* Look-up each chunk and add it to $outstring */
+        foreach ($fiveBitsArray as $fiveBitsString) {
+            $outString .= $BASE32_TABLE[$fiveBitsString];
+        }
+
+        return $outString;
+    }
+
+
+
+    /**
+     * decode to a binary string
+     *
+     * @param    $inString   String to base32 decode
+     *
+     * @return   $outString  Base32 decoded $inString
+     *
+     * @access   private
+     *
+     */
+
+    function decode($inString) {
+        /* declaration */
+        $deCompBits = '';
+        $outString = '';
+
+        static $BASE32_TABLE = array(
+            'a' => '00000', 'b' => '00001', 'c' => '00010', 'd' => '00011',
+            'e' => '00100', 'f' => '00101', 'g' => '00110', 'h' => '00111',
+            'i' => '01000', 'j' => '01001', 'k' => '01010', 'l' => '01011',
+            'm' => '01100', 'n' => '01101', 'o' => '01110', 'p' => '01111',
+            'q' => '10000', 'r' => '10001', 's' => '10010', 't' => '10011',
+            'u' => '10100', 'v' => '10101', 'w' => '10110', 'x' => '10111',
+            'y' => '11000', 'z' => '11001', '0' => '11010', '1' => '11011',
+            '2' => '11100', '3' => '11101', '4' => '11110', '5' => '11111');
+
+        /* Step 1 */
+        $inputCheck = strlen($inString) % 8;
+        if(($inputCheck == 1)||($inputCheck == 3)||($inputCheck == 6)) {
+            trigger_error('input to Base32Decode was a bad mod length: '.$inputCheck);
+            return false;
+        }
+
+        /* $deCompBits is a string that represents the bits as 0 and 1.*/
+        for ($i = 0, $k = strlen($inString); $i < $k; $i++) {
+            $inChar = $inString[$i];
+            if(isset($BASE32_TABLE[$inChar])) {
+                $deCompBits .= $BASE32_TABLE[$inChar];
+            } else {
+                trigger_error('input to Base32Decode had a bad character: '.$inChar);
+                return false;
+            }
+        }
+
+        /* Break the decompressed string into octets for returning */
+        foreach (str_split($deCompBits, 8) as $chunk) {
+            if (strlen($chunk) != 8) {
+                // Ensure correct padding
+                if (substr_count($chunk, '1')>0) {
+                    trigger_error('found non-zero padding in Base32Decode');
+                    return false;
+                }
+                break;
+            }
+            $outString .= chr(bindec($chunk));
+        }
+
+        return $outString;
+    }
+}
+
+?>
diff --git a/include/class.client.php b/include/class.client.php
index 7464b841ca8ab7bfb5217d2965760ea99ff28fb9..93aa1b07f0b978f6a11fdfa64eb25b3b62de7808 100644
--- a/include/class.client.php
+++ b/include/class.client.php
@@ -2,10 +2,7 @@
 /*********************************************************************
     class.client.php
 
-    Handles everything about client
-
-    XXX: Please note that osTicket uses email address and ticket ID to authenticate the user*!
-          Client is modeled on the info of the ticket used to login .
+    Handles everything about EndUser
 
     Peter Rotich <peter@osticket.com>
     Copyright (c)  2006-2013 osTicket
@@ -16,106 +13,183 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
+abstract class TicketUser {
+
+    static private $token_regex = '/^(?P<type>\w{1})(?P<algo>\d+)x(?P<hash>.*)$/i';
 
-class Client {
+    protected  $user;
 
-    var $id;
-    var $fullname;
-    var $username;
-    var $email;
+    function __construct($user) {
+        $this->user = $user;
+    }
 
-    var $_answers;
+    function __call($name, $args) {
+        global $cfg;
 
-    var $ticket_id;
-    var $ticketID;
+        if($this->user && is_callable(array($this->user, $name)))
+            return  $args
+                ? call_user_func_array(array($this->user, $name), $args)
+                : call_user_func(array($this->user, $name));
 
-    var $ht;
+        $tag =  substr($name, 3);
+        switch (strtolower($tag)) {
+            case 'ticket_link':
+                return sprintf('%s/view.php?auth=%s',
+                        $cfg->getBaseUrl(),
+                        urlencode($this->getAuthToken()));
+                break;
+        }
 
+        return false;
 
-    function Client($id, $email=null) {
-        $this->id =0;
-        $this->load($id,$email);
     }
 
-    function load($id=0, $email=null) {
+    protected function getAuthToken($algo=1) {
 
-        if(!$id && !($id=$this->getId()))
-            return false;
+        //Format: // <user type><algo id used>x<pack of uid & tid><hash of the algo>
+        $authtoken = sprintf('%s%dx%s',
+                ($this->isOwner() ? 'o' : 'c'),
+                $algo,
+                Base32::encode(pack('VV',$this->getId(), $this->getTicketId())));
 
-        $sql='SELECT ticket.ticket_id, ticketID, email.address as email '
-            .' FROM '.TICKET_TABLE.' ticket '
-            .' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id'
-            .' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id'
-            .' WHERE ticketID='.db_input($id);
+        switch($algo) {
+            case 1:
+                $authtoken .= substr(base64_encode(
+                            md5($this->getId().$this->getTicket()->getCreateDate().$this->getTicketId().SECRET_SALT, true)), 8);
+                break;
+            default:
+                return null;
+        }
 
-        if($email)
-            $sql.=' AND email.address = '.db_input($email);
+        return $authtoken;
+    }
+
+    static function lookupByToken($token) {
+
+        //Expecting well formatted token see getAuthToken routine for details.
+        $matches = array();
+        if (!preg_match(static::$token_regex, $token, $matches))
+            return null;
+
+        //Unpack the user and ticket ids
+        $matches +=unpack('Vuid/Vtid',
+                Base32::decode(strtolower(substr($matches['hash'], 0, 13))));
+
+        $user = null;
+        switch ($matches['type']) {
+            case 'c': //Collaborator c
+                if (($user = Collaborator::lookup($matches['uid']))
+                        && $user->getTicketId() != $matches['tid'])
+                    $user = null;
+                break;
+            case 'o': //Ticket owner
+                if (($ticket = Ticket::lookup($matches['tid']))) {
+                    if (($user = $ticket->getOwner())
+                            && $user->getId() != $matches['uid'])
+                        $user = null;
+                }
+                break;
+        }
 
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return NULL;
+        if (!$user
+                || !$user instanceof TicketUser
+                || strcasecmp($user->getAuthToken($matches['algo']), $token))
+            return false;
 
-        $this->ht = db_fetch_array($res);
-        $this->id         = $this->ht['ticketID']; //placeholder
-        $this->ticket_id  = $this->ht['ticket_id'];
-        $this->ticketID   = $this->ht['ticketID'];
+        return $user;
+    }
 
-        $user = User::lookup(array('emails__address'=>$this->ht['email']));
-        $this->fullname   = $user->getFullName();
+    static function lookupByEmail($email) {
 
-        $this->username   = $this->ht['email'];
-        $this->email      = $this->ht['email'];
+        if (!($user=User::lookup(array('emails__address' => $email))))
+            return null;
 
-        $this->stats = array();
+        return new EndUser($user);
+    }
 
-        return($this->id);
+    function isOwner() {
+        return  ($this->user
+                    && $this->user->getId() == $this->getTicket()->getOwnerId());
     }
 
-    function loadDynamicData() {
-        $this->_answers = array();
-        foreach (DynamicFormEntry::forClient($this->getId()) as $form)
-            foreach ($form->getAnswers() as $answer)
-                $this->_answers[$answer->getField()->get('name')] =
-                    $answer->getValue();
+    abstract function getTicketId();
+    abstract function getTicket();
+}
+
+class TicketOwner extends  TicketUser {
+
+    protected $ticket;
+
+    function __construct($user, $ticket) {
+        parent::__construct($user);
+        $this->ticket = $ticket;
     }
 
-    function reload() {
-        return $this->load();
+    function getTicket() {
+        return $this->ticket;
     }
 
-    function isClient() {
-        return TRUE;
+    function getTicketId() {
+        return $this->ticket->getId();
     }
+}
 
-    function getId() {
-        return $this->id;
+/*
+ * Decorator class for authenticated user
+ *
+ */
+
+class  EndUser extends AuthenticatedUser {
+
+    protected $user;
+
+    function __construct($user) {
+        $this->user = $user;
     }
 
-    function getEmail() {
-        return $this->email;
+    /*
+     * Delegate calls to the user
+     */
+    function __call($name, $args) {
+
+        if(!$this->user
+                || !is_callable(array($this->user, $name)))
+            return false;
+
+        return  $args
+            ? call_user_func_array(array($this->user, $name), $args)
+            : call_user_func(array($this->user, $name));
     }
 
-    function getUserName() {
-        return $this->username;
+    function getId() {
+        //We ONLY care about user ID at the ticket level
+        if ($this->user instanceof Collaborator)
+            return $this->user->getUserId();
+
+        return $this->user->getId();
     }
 
-    function getName() {
-        return $this->fullname;
+    function getUserName() {
+        //XXX: Revisit when real usernames are introduced  or when email
+        // requirement is removed.
+        return $this->user->getEmail();
     }
 
-    function getPhone() {
-        return $this->_answers['phone'];
+    function getRole() {
+        return $this->isOwner() ? 'owner' : 'collaborator';
     }
 
-    function getTicketID() {
-        return $this->ticketID;
+    function getAuthBackend() {
+        list($authkey,) = explode(':', $this->getAuthKey());
+        return UserAuthenticationBackend::getBackend($authkey);
     }
 
     function getTicketStats() {
 
-        if(!$this->stats['tickets'])
-            $this->stats['tickets'] = Ticket::getClientStats($this->getEmail());
+        if (!isset($this->ht['stats']))
+            $this->ht['stats'] = $this->getStats();
 
-        return $this->stats['tickets'];
+        return $this->ht['stats'];
     }
 
     function getNumTickets() {
@@ -130,114 +204,23 @@ class Client {
         return ($stats=$this->getTicketStats())?$stats['closed']:0;
     }
 
-    /* ------------- Static ---------------*/
-    function getLastTicketIdByEmail($email) {
-        $sql='SELECT ticket.ticketID FROM '.TICKET_TABLE.' ticket '
-            .' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id'
-            .' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id'
-            .' WHERE email.address = '.db_input($email)
-            .' ORDER BY ticket.created '
-            .' LIMIT 1';
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($tid) = db_fetch_row($res);
+    private function getStats() {
 
-        return $tid;
-    }
-
-    function lookup($id, $email=null) {
-        return ($id && is_numeric($id) && ($c=new Client($id,$email)) && $c->getId()==$id)?$c:null;
-    }
+        $sql='SELECT count(open.ticket_id) as open, count(closed.ticket_id) as closed '
+            .' FROM '.TICKET_TABLE.' ticket '
+            .' LEFT JOIN '.TICKET_TABLE.' open
+                ON (open.ticket_id=ticket.ticket_id AND open.status=\'open\') '
+            .' LEFT JOIN '.TICKET_TABLE.' closed
+                ON (closed.ticket_id=ticket.ticket_id AND closed.status=\'closed\')'
+            .' LEFT JOIN '.TICKET_COLLABORATOR_TABLE.' collab
+                ON (collab.ticket_id=ticket.ticket_id
+                    AND collab.user_id = '.db_input($this->getId()).' )'
+            .' WHERE ticket.user_id = '.db_input($this->getId())
+            .' OR collab.user_id = '.db_input($this->getId());
 
-    function lookupByEmail($email) {
-        return (($id=self::getLastTicketIdByEmail($email)))?self::lookup($id, $email):null;
+        return db_fetch_array(db_query($sql));
     }
 
-    /* static */ function login($ticketID, $email, $auth=null, &$errors=array()) {
-        global $ost;
-
 
-        $cfg = $ost->getConfig();
-        $auth = trim($auth);
-        $email = trim($email);
-        $ticketID = trim($ticketID);
-
-        # Only consider auth token for GET requests, and for GET requests,
-        # REQUIRE the auth token
-        $auto_login = ($_SERVER['REQUEST_METHOD'] == 'GET');
-
-        //Check time for last max failed login attempt strike.
-        if($_SESSION['_client']['laststrike']) {
-            if((time()-$_SESSION['_client']['laststrike'])<$cfg->getClientLoginTimeout()) {
-                $errors['login'] = 'Excessive failed login attempts';
-                $errors['err'] = 'You\'ve reached maximum failed login attempts allowed. Try again later or <a href="open.php">open a new ticket</a>';
-                $_SESSION['_client']['laststrike'] = time(); //renew the strike.
-            } else { //Timeout is over.
-                //Reset the counter for next round of attempts after the timeout.
-                $_SESSION['_client']['laststrike'] = null;
-                $_SESSION['_client']['strikes'] = 0;
-            }
-        }
-
-        if($auto_login && !$auth)
-            $errors['login'] = 'Invalid method';
-        elseif(!$ticketID || !Validator::is_email($email))
-            $errors['login'] = 'Valid email and ticket number required';
-
-        //Bail out on error.
-        if($errors) return false;
-
-        //See if we can fetch local ticket id associated with the ID given
-        if(($ticket=Ticket::lookupByExtId($ticketID, $email)) && $ticket->getId()) {
-            //At this point we know the ticket ID is valid.
-            //TODO: 1) Check how old the ticket is...3 months max?? 2) Must be the latest 5 tickets??
-            //Check the email given.
-
-            # Require auth token for automatic logins (GET METHOD).
-            if (!strcasecmp($ticket->getEmail(), $email) && (!$auto_login || $auth === $ticket->getAuthToken())) {
-
-                //valid match...create session goodies for the client.
-                $user = new ClientSession($email,$ticket->getExtId());
-                $_SESSION['_client'] = array(); //clear.
-                $_SESSION['_client']['userID'] = $ticket->getEmail(); //Email
-                $_SESSION['_client']['key'] = $ticket->getExtId(); //Ticket ID --acts as password when used with email. See above.
-                $_SESSION['_client']['token'] = $user->getSessionToken();
-                $_SESSION['TZ_OFFSET'] = $cfg->getTZoffset();
-                $_SESSION['TZ_DST'] = $cfg->observeDaylightSaving();
-                $user->refreshSession(); //set the hash.
-                //Log login info...
-                $msg=sprintf('%s/%s logged in [%s]', $ticket->getEmail(), $ticket->getExtId(), $_SERVER['REMOTE_ADDR']);
-                $ost->logDebug('User login', $msg);
-
-                //Regenerate session ID.
-                $sid=session_id(); //Current session id.
-                session_regenerate_id(TRUE); //get new ID.
-                if(($session=$ost->getSession()) && is_object($session) && $sid!=session_id())
-                    $session->destroy($sid);
-
-                return $user;
-
-            }
-        }
-
-        //If we get to this point we know the login failed.
-        $errors['login'] = 'Invalid login';
-        $_SESSION['_client']['strikes']+=1;
-        if(!$errors && $_SESSION['_client']['strikes']>$cfg->getClientMaxLogins()) {
-            $errors['login'] = 'Access Denied';
-            $errors['err'] = 'Forgot your login info? Please <a href="open.php">open a new ticket</a>.';
-            $_SESSION['_client']['laststrike'] = time();
-            $alert='Excessive login attempts by a user.'."\n".
-                    'Email: '.$email."\n".'Ticket#: '.$ticketID."\n".
-                    'IP: '.$_SERVER['REMOTE_ADDR']."\n".'Time:'.date('M j, Y, g:i a T')."\n\n".
-                    'Attempts #'.$_SESSION['_client']['strikes'];
-            $ost->logError('Excessive login attempts (user)', $alert, ($cfg->alertONLoginError()));
-        } elseif($_SESSION['_client']['strikes']%2==0) { //Log every other failed login attempt as a warning.
-            $alert='Email: '.$email."\n".'Ticket #: '.$ticketID."\n".'IP: '.$_SERVER['REMOTE_ADDR'].
-                   "\n".'TIME: '.date('M j, Y, g:i a T')."\n\n".'Attempts #'.$_SESSION['_client']['strikes'];
-            $ost->logWarning('Failed login attempt (user)', $alert);
-        }
-
-        return false;
-    }
 }
 ?>
diff --git a/include/class.collaborator.php b/include/class.collaborator.php
index bf823dacdb366279ec88b5de9657843283f72ba9..b8adb93262d1dba483457b7decd82b8af7dd6078 100644
--- a/include/class.collaborator.php
+++ b/include/class.collaborator.php
@@ -14,8 +14,9 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 require_once(INCLUDE_DIR . 'class.user.php');
+require_once(INCLUDE_DIR . 'class.client.php');
 
-class Collaborator {
+class Collaborator extends TicketUser {
 
     var $ht;
 
@@ -23,8 +24,8 @@ class Collaborator {
     var $ticket;
 
     function __construct($id) {
-
         $this->load($id);
+        parent::__construct($this->getUser());
     }
 
     function load($id) {
@@ -36,24 +37,13 @@ class Collaborator {
             .' WHERE id='.db_input($id);
 
         $this->ht = db_fetch_array(db_query($sql));
-        $this->ticket = $this->user = null;
+        $this->ticket = null;
     }
 
     function reload() {
         return $this->load();
     }
 
-    function __call($name, $args) {
-
-        if(!($user=$this->getUser()) || !method_exists($user, $name))
-            return null;
-
-        if($args)
-            return  call_user_func_array(array($user, $name), $args);
-
-        return call_user_func(array($user, $name));
-    }
-
     function __toString() {
         return Format::htmlchars(sprintf('%s <%s>', $this->getName(),
                     $this->getEmail()));
@@ -67,6 +57,10 @@ class Collaborator {
         return ($this->ht['isactive']);
     }
 
+    function getCreateDate() {
+        return $this->ht['created'];
+    }
+
     function getTicketId() {
         return $this->ht['ticket_id'];
     }
@@ -148,12 +142,11 @@ class Collaborator {
             .' WHERE ticket_id='.db_input($info['ticketId'])
             .' AND user_id='.db_input($info['userId']);
 
-        list($id) = db_fetch_row(db_query($sql));
-
-        return $id;
+        return db_result(db_query($sql));
     }
 
     static function lookup($criteria) {
+
         $id = is_numeric($criteria)
             ? $criteria : self::getIdByInfo($criteria);
 
diff --git a/include/class.crypto.php b/include/class.crypto.php
index 92ab1e953f1b3786cac39e25e34acc7abfd59f9b..a9aebcd2f87b6e5e66b67a0c70ba0c6855ddd0cf 100644
--- a/include/class.crypto.php
+++ b/include/class.crypto.php
@@ -26,6 +26,8 @@ define('CRYPT_PHPSECLIB', 3);
 
 define('CRYPT_IS_WINDOWS', !strncasecmp(PHP_OS, 'WIN', 3));
 
+
+require_once INCLUDE_DIR.'class.base32.php';
 require_once PEAR_DIR.'Crypt/Hash.php';
 require_once PEAR_DIR.'Crypt/AES.php';
 
diff --git a/include/class.export.php b/include/class.export.php
index b5c5a44396eb07aec4c6a20c87d34a0a7c9eeff5..b95a7d16d587541188d40d4cc6f45097b00b757f 100644
--- a/include/class.export.php
+++ b/include/class.export.php
@@ -57,7 +57,7 @@ class Export {
             $sql = str_replace(' FROM ', ',' . implode(',', $select) . ' FROM ', $sql);
         return self::dumpQuery($sql,
             array(
-                'ticketID' =>       'Ticket Id',
+                'number' =>       'Ticket Number',
                 'created' =>        'Date',
                 'subject' =>        'Subject',
                 'name' =>           'From',
diff --git a/include/class.nav.php b/include/class.nav.php
index 799c76a8b4dc0a78b74374c2b2aa673cf74c5132..1648332948ec1a32e5ac9a12aba75827d3087a15 100644
--- a/include/class.nav.php
+++ b/include/class.nav.php
@@ -282,12 +282,12 @@ class UserNav {
             $navs['new']=array('desc'=>'Open&nbsp;New&nbsp;Ticket','href'=>'open.php','title'=>'');
             if($user && $user->isValid()) {
                 if($cfg && $cfg->showRelatedTickets()) {
-                    $navs['tickets']=array('desc'=>sprintf('My&nbsp;Tickets&nbsp;(%d)',$user->getNumTickets()),
+                    $navs['tickets']=array('desc'=>sprintf('Tickets&nbsp;(%d)',$user->getNumTickets()),
                                            'href'=>'tickets.php',
                                             'title'=>'Show all tickets');
                 } else {
                     $navs['tickets']=array('desc'=>'View&nbsp;Ticket&nbsp;Thread',
-                                           'href'=>sprintf('tickets.php?id=%d',$user->getTicketID()),
+                                           'href'=>sprintf('tickets.php?id=%d',$user->getTicketId()),
                                            'title'=>'View ticket status');
                 }
             } else {
diff --git a/include/class.signal.php b/include/class.signal.php
index 928c15c4d2392ae767a0c465b6730d859f3dc85f..424ccccc9665a36db41bb5a1ff271150d946ec82 100644
--- a/include/class.signal.php
+++ b/include/class.signal.php
@@ -25,6 +25,8 @@
  * the codebase there exists a Signal::send() for the same named signal.
  */
 class Signal {
+    static private $subscribers = array();
+
     /**
      * Subscribe to a signal.
      *
@@ -51,10 +53,9 @@ class Signal {
      * signal handler. The function will receive the signal data and should
      * return true if the signal handler should be called.
      */
-    /*static*/ function connect($signal, $callable, $object=null,
+    static function connect($signal, $callable, $object=null,
             $check=null) {
-        global $_subscribers;
-        if (!isset($_subscribers[$signal])) $_subscribers[$signal] = array();
+        if (!isset(self::$subscribers[$signal])) self::$subscribers[$signal] = array();
         // XXX: Ensure $object if set is a class
         if ($object && !is_string($object))
             trigger_error("Invalid object: $object: Expected class");
@@ -62,7 +63,7 @@ class Signal {
             trigger_error("Invalid check function: Must be callable");
             $check = null;
         }
-        $_subscribers[$signal][] = array($object, $callable, $check);
+        self::$subscribers[$signal][] = array($object, $callable, $check);
     }
 
     /**
@@ -85,11 +86,10 @@ class Signal {
      * possible to propogate changes in the signal handlers back to the
      * originating context.
      */
-    /*static*/ function send($signal, $object, &$data=null) {
-        global $_subscribers;
-        if (!isset($_subscribers[$signal]))
+    static function send($signal, $object, &$data=null) {
+        if (!isset(self::$subscribers[$signal]))
             return;
-        foreach ($_subscribers[$signal] as $sub) {
+        foreach (self::$subscribers[$signal] as $sub) {
             list($s, $callable, $check) = $sub;
             if ($s && !is_a($object, $s))
                 continue;
@@ -99,6 +99,4 @@ class Signal {
         }
     }
 }
-
-$_subscribers = array();
 ?>
diff --git a/include/class.staff.php b/include/class.staff.php
index efd4f341c03f40cb3fa79756cc038533b7c86c7f..e353c5cd908c6ce9a1e316f03b3098fe3cc94556 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -83,8 +83,12 @@ class Staff extends AuthenticatedUser {
         return $this->load();
     }
 
+    function __toString() {
+        return (string) $this->getName();
+    }
+
     function asVar() {
-        return $this->getName();
+        return $this->__toString();
     }
 
     function getHastable() {
@@ -95,6 +99,17 @@ class Staff extends AuthenticatedUser {
         return $this->config->getInfo() + $this->getHastable();
     }
 
+    // AuthenticatedUser implementation...
+    // TODO: Move to an abstract class that extends Staff
+    function getRole() {
+        return 'staff';
+    }
+
+    function getAuthBackend() {
+        list($authkey, ) = explode(':', $this->getAuthKey());
+        return StaffAuthenticationBackend::getBackend($authkey);
+    }
+
     /*compares user password*/
     function check_passwd($password, $autoupdate=true) {
 
diff --git a/include/class.thread.php b/include/class.thread.php
index 975a3a47294705ba3341851fd4dc53b2eb6d64a8..6fa0d9c13cc74e85dd8e57dce0c4f90411c1166f 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -118,8 +118,7 @@ class Thread {
             .' FROM '.TICKET_THREAD_TABLE.' thread '
             .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach
                 ON (thread.ticket_id=attach.ticket_id
-                        AND thread.id=attach.ref_id
-                        AND thread.thread_type=attach.ref_type) '
+                        AND thread.id=attach.ref_id) '
             .' WHERE  thread.ticket_id='.db_input($this->getTicketId());
 
         if($type && is_array($type))
@@ -161,6 +160,7 @@ class Thread {
     function addResponse($vars, &$errors) {
 
         $vars['ticketId'] = $this->getTicketId();
+        $vars['userId'] = 0;
 
         return Response::create($vars, $errors);
     }
@@ -181,7 +181,7 @@ class Thread {
         /* XXX: Leave this out until TICKET_EMAIL_INFO_TABLE has a primary
          *      key
         $sql = 'DELETE mid.* FROM '.TICKET_EMAIL_INFO_TABLE.' mid
-            INNER JOIN '.TICKET_THREAD_TABLE.' thread ON (thread.id = mid.message_id)
+            INNER JOIN '.TICKET_THREAD_TABLE.' thread ON (thread.id = mid.thread_id)
             WHERE thread.ticket_id = '.db_input($this->getTicketId());
         db_query($sql);
          */
@@ -244,11 +244,10 @@ Class ThreadEntry {
             .' ,count(DISTINCT attach.attach_id) as attachments '
             .' FROM '.TICKET_THREAD_TABLE.' thread '
             .' LEFT JOIN '.TICKET_EMAIL_INFO_TABLE.' info
-                ON (thread.id=info.message_id) '
+                ON (thread.id=info.thread_id) '
             .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach
                 ON (thread.ticket_id=attach.ticket_id
-                        AND thread.id=attach.ref_id
-                        AND thread.thread_type=attach.ref_type) '
+                        AND thread.id=attach.ref_id) '
             .' WHERE  thread.id='.db_input($id);
 
         if($type)
@@ -337,7 +336,7 @@ Class ThreadEntry {
         require_once(INCLUDE_DIR.'class.mailparse.php');
 
         $sql = 'SELECT headers FROM '.TICKET_EMAIL_INFO_TABLE
-            .' WHERE message_id='.$this->getId();
+            .' WHERE thread_id='.$this->getId();
         $headers = db_result(db_query($sql));
         return Mail_Parse::splitHeaders($headers);
     }
@@ -352,6 +351,37 @@ Class ThreadEntry {
         return $this->_references;
     }
 
+    function getTaggedEmailReferences($prefix, $refId) {
+
+        $ref = "+$prefix".Base32::encode(pack('VV', $this->getId(), $refId));
+
+        $mid = substr_replace($this->getEmailMessageId(),
+                $ref, strpos($this->getEmailMessageId(), '@'), 0);
+
+        //TODO: Confirm how references are ordered on reply - we want the tagged
+        // reference to be processed first.
+
+        return sprintf('%s %s', $this->getEmailReferences(), $mid);
+    }
+
+    function getEmailReferencesForUser($user) {
+        return $this->getTaggedEmailReferences('u', $user->getId());
+    }
+
+    function getEmailReferencesForStaff($staff) {
+        return $this->getTaggedEmailReferences('s', $staff->getId());
+    }
+
+    function getUIDFromEmailReference($ref) {
+
+        $info = unpack('Vtid/Vuid',
+                Base32::decode(strtolower(substr($ref, -13))));
+
+        if ($info && $info['tid'] == $this->getId())
+            return $info['uid'];
+
+    }
+
     function getTicket() {
 
         if(!$this->ticket && $this->getTicketId())
@@ -372,6 +402,18 @@ Class ThreadEntry {
         return $this->staff;
     }
 
+    function getUserId() {
+        return $this->ht['user_id'];
+    }
+
+    function getUser() {
+
+        if (!isset($this->user))
+            $this->user = User::lookup($this->getUserId());
+
+        return $this->user;
+    }
+
     function getEmailHeader() {
         return $this->ht['headers'];
     }
@@ -466,8 +508,7 @@ Class ThreadEntry {
         $sql ='INSERT IGNORE INTO '.TICKET_ATTACHMENT_TABLE.' SET created=NOW() '
              .' ,file_id='.db_input($fileId)
              .' ,ticket_id='.db_input($this->getTicketId())
-             .' ,ref_id='.db_input($this->getId())
-             .' ,ref_type='.db_input($this->getType());
+             .' ,ref_id='.db_input($this->getId());
 
         return (db_query($sql) && ($id=db_insert_id()))?$id:0;
     }
@@ -491,8 +532,7 @@ Class ThreadEntry {
             .' FROM '.FILE_TABLE.' f '
             .' INNER JOIN '.TICKET_ATTACHMENT_TABLE.' a ON(f.id=a.file_id) '
             .' WHERE a.ticket_id='.db_input($this->getTicketId())
-            .' AND a.ref_id='.db_input($this->getId())
-            .' AND a.ref_type='.db_input($this->getType());
+            .' AND a.ref_id='.db_input($this->getId());
 
         $this->attachments = array();
         if(($res=db_query($sql)) && db_num_rows($res)) {
@@ -585,14 +625,17 @@ Class ThreadEntry {
         // Disambiguate if the user happens also to be a staff member of the
         // system. The current ticket owner should _always_ post messages
         // instead of notes or responses
-        if (strcasecmp($mailinfo['email'], $ticket->getEmail()) == 0) {
+        if ($mailinfo['userId']
+                || strcasecmp($mailinfo['email'], $ticket->getEmail()) == 0) {
             $vars['message'] = $body;
+            $vars['userId'] = $mailinfo['userId'] ? $mailinfo['userId'] : $ticket->getUserId();
             return $ticket->postMessage($vars, 'Email');
         }
         // XXX: Consider collaborator role
-        elseif ($staff_id = Staff::getIdByEmail($mailinfo['email'])) {
-            $vars['staffId'] = $staff_id;
-            $poster = Staff::lookup($staff_id);
+        elseif ($mailinfo['staffId']
+                || ($mailinfo['staffId'] = Staff::getIdByEmail($mailinfo['email']))) {
+            $vars['staffId'] = $mailinfo['staffId'];
+            $poster = Staff::lookup($mailinfo['staffId']);
             $errors = array();
             $vars['note'] = $body;
             return $ticket->postNote($vars, $errors, $poster);
@@ -603,8 +646,11 @@ Class ThreadEntry {
         }
         // TODO: Consider security constraints
         else {
+            //XXX: Are we potentially leaking the email address to
+            // collaborators?
             $vars['message'] = sprintf("Received From: %s\n\n%s",
                 $mailinfo['email'], $body);
+            $vars['userId'] = 0; //Unknown user! //XXX: Assume ticket owner?
             return $ticket->postMessage($vars, 'Email');
         }
         // Currently impossible, but indicate that this thread object could
@@ -643,7 +689,7 @@ Class ThreadEntry {
     /* static */
     function logEmailHeaders($id, $mid, $header=false) {
         $sql='INSERT INTO '.TICKET_EMAIL_INFO_TABLE
-            .' SET message_id='.db_input($id) //TODO: change it to thread_id
+            .' SET thread_id='.db_input($id)
             .', email_mid='.db_input($mid); //TODO: change it to message_id.
         if ($header)
             $sql .= ', headers='.db_input($header);
@@ -652,10 +698,14 @@ Class ThreadEntry {
 
     /* variables */
 
-    function asVar() {
+    function __toString() {
         return $this->getBody();
     }
 
+    function asVar() {
+        return (string) $this;
+    }
+
     function getVar($tag) {
         global $cfg;
 
@@ -705,11 +755,11 @@ Class ThreadEntry {
      *      previously seen. This is useful if no thread-id is associated
      *      with the email (if it was rejected for instance).
      */
-    function lookupByEmailHeaders($mailinfo, &$seen=false) {
+    function lookupByEmailHeaders(&$mailinfo, &$seen=false) {
         // Search for messages using the References header, then the
         // in-reply-to header
-        $search = 'SELECT message_id, email_mid FROM '.TICKET_EMAIL_INFO_TABLE
-               . ' WHERE email_mid=%s ORDER BY message_id DESC';
+        $search = 'SELECT thread_id, email_mid FROM '.TICKET_EMAIL_INFO_TABLE
+               . ' WHERE email_mid=%s ORDER BY thread_id DESC';
 
         if (list($id, $mid) = db_fetch_row(db_query(
                 sprintf($search, db_input($mailinfo['mid']))))) {
@@ -732,10 +782,27 @@ Class ThreadEntry {
             // @see rfc 1036, section 2.2.5
             // @see http://www.jwz.org/doc/threading.html
             foreach (array_reverse($matches[0]) as $mid) {
+                //Try to determine if it's a reply to a tagged email.
+                $ref = null;
+                if (strpos($mid, '+')) {
+                    list($left, $right) = explode('@',$mid);
+                    list($left, $ref) = explode('+', $left);
+                    $mid = "$left@$right";
+                }
                 $res = db_query(sprintf($search, db_input($mid)));
                 while (list($id) = db_fetch_row($res)) {
-                    if ($t = ThreadEntry::lookup($id))
-                        return $t;
+                    if (!($t = ThreadEntry::lookup($id))) continue;
+
+                    //We found a match  - see if we can ID the user.
+                    // XXX: Check access of ref is enough?
+                    if ($ref && ($uid = $t->getUIDFromEmailReference($ref))) {
+                        if ($ref[0] =='s') //staff
+                            $mailinfo['staffId'] = $uid;
+                        else //user or collaborator.
+                            $mailinfo['userId'] = $uid;
+                    }
+
+                    return $t;
                 }
             }
         }
@@ -745,12 +812,23 @@ Class ThreadEntry {
         // injection by third-party.
         $subject = $mailinfo['subject'];
         $match = array();
-        if ($subject && $mailinfo['email']
+        if ($subject
+                && $mailinfo['email']
                 && preg_match("/#(?:[\p{L}-]+)?([0-9]{1,10})/u", $subject, $match)
-                && ($tid = Ticket::getIdByExtId((int)$match[1], $mailinfo['email']))
-                )
-            // Return last message for the thread
-            return Message::lastByTicketId($tid);
+                //Lookup by ticket number
+                && ($ticket = Ticket::lookupByNumber((int)$match[1]))
+                //Lookup the user using the email address
+                && ($user = User::lookup(array('emails__address' => $mailinfo['email'])))) {
+            //We have a valid ticket and user
+            if ($ticket->getUserId() == $user->getId() //owner
+                    ||  ($c = Collaborator::lookup( // check if collaborator
+                            array('userId' => $user->getId(),
+                                  'ticketId' => $ticket->getId())))) {
+
+                $mailinfo['userId'] = $user->getId();
+                return $ticket->getLastMessage();
+            }
+        }
 
         return null;
     }
@@ -798,7 +876,7 @@ Class ThreadEntry {
 
         $poster = $vars['poster'];
         if ($poster && is_object($poster))
-            $poster = $poster->getName();
+            $poster = (string) $poster;
 
         $sql=' INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW() '
             .' ,thread_type='.db_input($vars['type'])
@@ -806,6 +884,7 @@ Class ThreadEntry {
             .' ,title='.db_input(Format::sanitize($vars['title'], true))
             .' ,body='.db_input($vars['body'])
             .' ,staff_id='.db_input($vars['staffId'])
+            .' ,user_id='.db_input($vars['userId'])
             .' ,poster='.db_input($poster)
             .' ,source='.db_input($vars['source']);
 
@@ -883,6 +962,11 @@ class Message extends ThreadEntry {
         $vars['type'] = 'M';
         $vars['body'] = $vars['message'];
 
+        if (!$vars['poster']
+                && $vars['userId']
+                && ($user = User::lookup($vars['userId'])))
+            $vars['poster'] = (string) $user->getName();
+
         return ThreadEntry::add($vars);
     }
 
@@ -949,6 +1033,11 @@ class Response extends ThreadEntry {
         if(!$vars['pid'] && $vars['msgId'])
             $vars['pid'] = $vars['msgId'];
 
+        if (!$vars['poster']
+                && $vars['staffId']
+                && ($staff = Staff::lookup($vars['staffId'])))
+            $vars['poster'] = (string) $staff->getName();
+
         return ThreadEntry::add($vars);
     }
 
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 29ccc502e00545b1a4c856931dc5e53db2a36045..a815cb9328fdb385631407ffa9f09a64c23114e2 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -85,7 +85,7 @@ class Ticket {
         $this->ht = db_fetch_array($res);
 
         $this->id       = $this->ht['ticket_id'];
-        $this->number   = $this->ht['ticketID'];
+        $this->number   = $this->ht['number'];
         $this->_answers = array();
 
         $this->loadDynamicData();
@@ -156,17 +156,28 @@ class Ticket {
                  || $staff->getId()==$this->getStaffId());
     }
 
-    function checkClientAccess($client) {
-        global $cfg;
+    function checkUserAccess($user) {
 
-        if(!is_object($client) && !($client=Client::lookup($client)))
+        if (!$user || !($user instanceof EndUser))
             return false;
 
-        if(!strcasecmp($client->getEmail(), $this->getEmail()))
+        //Ticket Owner
+        if ($user->getId() == $this->getUserId())
+            return true;
+
+        //Collaborator?
+        // 1) If the user was authorized via this ticket.
+        if ($user->getTicketId() == $this->getId()
+                && !strcasecmp($user->getRole(), 'collaborator'))
+            return true;
+
+        // 2) Query the database to check for expanded access...
+        if (Collaborator::lookup(array(
+                        'userId' => $user->getId(),
+                        'ticketId' => $this->getId())))
             return true;
 
-        return ($cfg && $cfg->showRelatedTickets()
-            && $client->getTicketId()==$this->getExtId());
+        return false;
     }
 
     //Getters
@@ -174,10 +185,6 @@ class Ticket {
         return  $this->id;
     }
 
-    function getExtId() {
-        return  $this->getNumber();
-    }
-
     function getNumber() {
         return $this->number;
     }
@@ -187,9 +194,12 @@ class Ticket {
     }
 
     function getOwner() {
-        if (!isset($this->user))
-            $this->user = User::lookup($this->getOwnerId());
-        return $this->user;
+
+        if (!isset($this->owner)
+                && ($u=User::lookup($this->getOwnerId())))
+            $this->owner = new TicketOwner($u, $this);
+
+        return $this->owner;
     }
 
     function getEmail(){
@@ -374,12 +384,16 @@ class Ticket {
         return $this->dept;
     }
 
-    function getClient() {
+    function getUserId() {
+        return $this->getOwnerId();
+    }
+
+    function getUser() {
 
-        if(!$this->client)
-            $this->client = Client::lookup($this->getExtId(), $this->getEmail());
+        if(!isset($this->user) && $this->getOwner())
+            $this->user = new EndUser($this->getOwner());
 
-        return $this->client;
+        return $this->user;
     }
 
     function getStaffId() {
@@ -583,6 +597,23 @@ class Ticket {
         return $this->collaborators;
     }
 
+    //UserList of recipients  (owner + collaborators)
+    function getRecipients() {
+
+        if (!isset($this->recipients)) {
+            $list = new UserList();
+            $list->add($this->getOwner());
+            if ($collabs = $this->getActiveCollaborators()) {
+                foreach ($collabs as $c)
+                    $list->add($c);
+            }
+            $this->recipients = $list;
+        }
+
+        return $this->recipients;
+    }
+
+
     function addCollaborator($user, $vars, &$errors) {
 
         if (!$user || $user->getId()==$this->getOwnerId())
@@ -595,6 +626,7 @@ class Ticket {
             return null;
 
         $this->collaborators = null;
+        $this->recipients = null;
 
         return $c;
     }
@@ -852,6 +884,7 @@ class Ticket {
 
             $msg = $this->replaceVars($msg->asArray(),
                     array('message' => $message,
+                          'recipient' => $this->getOwner(),
                           'signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')
                     );
 
@@ -920,11 +953,11 @@ class Ticket {
             $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body']);
         }
 
-        $client= $this->getClient();
+        $user = $this->getOwner();
 
         //Alert admin...this might be spammy (no option to disable)...but it is helpful..I think.
         $alert='Max. open tickets reached for '.$this->getEmail()."\n"
-              .'Open ticket: '.$client->getNumOpenTickets()."\n"
+              .'Open ticket: '.$user->getNumOpenTickets()."\n"
               .'Max Allowed: '.$cfg->getMaxOpenTickets()."\n\nNotice sent to the user.";
 
         $ost->alertAdmin('Overlimit Notice', $alert);
@@ -937,41 +970,53 @@ class Ticket {
         $this->reload();
     }
 
-    function  activityNotice($vars) {
+    /*
+     * Notify collaborators on response or new message
+     *
+     */
+
+    function  notifyCollaborators($entry) {
         global $cfg;
 
-        if (!$vars
-                || !$vars['variables']
-                || !($collaborators=$this->getActiveCollaborators())
+        if (!$entry instanceof ThreadEntry
+                || !($recipients=$this->getRecipients())
                 || !($dept=$this->getDept())
                 || !($tpl=$dept->getTemplate())
                 || !($msg=$tpl->getActivityNoticeMsgTemplate())
                 || !($email=$dept->getEmail()))
             return;
 
+        //Who posted the entry?
+        $uid = 0;
+        if ($entry instanceof Message) {
+            $poster = $entry->getUser();
+            $uid = $entry->getUserId();
+        } else
+            $poster = $entry->getStaff();
 
-        $msg = $this->replaceVars($msg->asArray(), $vars['variables']);
+        $vars = array(
+                'message' => (string) $entry,
+                'poster' => $poster? $poster : 'A collaborator');
 
-        if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator()))
-            $msg['body'] = "<p style=\"display:none\">$tag<p>".$msg['body'];
+        $msg = $this->replaceVars($msg->asArray(), $vars);
 
-        $attachments = ($cfg->emailAttachments() && $vars['attachments'])?$vars['attachments']:array();
-        $options = array();
-        if($vars['inreplyto'])
-            $options['inreplyto'] = $vars['inreplyto'];
-        if($vars['references'])
-            $options['references'] = $vars['references'];
+        if ($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator()))
+            $msg['body'] = "<p style=\"display:none\">$tag<p>".$msg['body'];
 
-        foreach($collaborators as $collaborator) {
-            $notice = $this->replaceVars($msg, array('recipient' => $collaborator));
-            $email->send($collaborator->getEmail(), $notice['subj'], $notice['body'], $attachments,
+        $attachments = $cfg->emailAttachments()?$entry->getAttachments():array();
+        $options = array('inreplyto' => $entry->getEmailMessageId());
+        foreach ($recipients as $recipient) {
+            if ($uid == $recipient->getId()) continue;
+            $options['references'] =  $entry->getEmailReferencesForUser($recipient);
+            $notice = $this->replaceVars($msg, array('recipient' => $recipient));
+            $email->send($recipient->getEmail(), $notice['subj'], $notice['body'], $attachments,
                 $options);
         }
 
         return;
     }
 
-    function onMessage($autorespond=true, $message=null) {
+    function onMessage($message, $autorespond=true) {
         global $cfg;
 
         db_query('UPDATE '.TICKET_TABLE.' SET isanswered=0,lastmessage=NOW() WHERE ticket_id='.db_input($this->getId()));
@@ -988,13 +1033,18 @@ class Ticket {
         if($this->isClosed()) $this->reopen(); //reopen..
 
        /**********   double check auto-response  ************/
-        if($autorespond && (Email::getIdByEmail($this->getEmail())))
+        if (!($user = $message->getUser()))
+            $autorespond=false;
+        elseif ($autorespond && (Email::getIdByEmail($user->getEmail())))
             $autorespond=false;
-        elseif($autorespond && ($dept=$this->getDept()))
+        elseif ($autorespond && ($dept=$this->getDept()))
             $autorespond=$dept->autoRespONNewMessage();
 
 
-        if(!$autorespond || !$cfg->autoRespONNewMessage()) return;  //no autoresp or alerts.
+        if(!$autorespond
+                || !$cfg->autoRespONNewMessage()
+                || !$message) return;  //no autoresp or alerts.
+
         $this->reload();
         $dept = $this->getDept();
         $email = $dept->getAutoRespEmail();
@@ -1005,18 +1055,18 @@ class Ticket {
                 && ($msg=$tpl->getNewMessageAutorepMsgTemplate())) {
 
             $msg = $this->replaceVars($msg->asArray(),
-                            array('signature' => ($dept && $dept->isPublic())?$dept->getSignature():''));
+                            array(
+                                'recipient' => $user,
+                                'signature' => ($dept && $dept->isPublic())?$dept->getSignature():''));
 
             //Reply separator tag.
             if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator()))
                 $msg['body'] = "<p style=\"display:none\">$tag<p>".$msg['body'];
 
-            if (!$message)
-                $message = $this->getLastMessage();
             $options = array(
-                'inreplyto'=>$message->getEmailMessageId(),
-                'references'=>$message->getEmailReferences());
-            $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body'],
+                'inreplyto' => $message->getEmailMessageId(),
+                'references' => $message->getEmailReferencesForUser($user));
+            $email->sendAutoReply($user->getEmail(), $msg['subj'], $msg['body'],
                 null, $options);
         }
     }
@@ -1456,9 +1506,10 @@ class Ticket {
 
         //Add email recipients as collaborators
         if ($vars['recipients']) {
-            //TODO:  Disable collaborators added by other collaborator
-            //  staff approval will be required.
-            $info = array('isactive' => 1);
+            //New collaborators added by other collaborators are disable --
+            // requires staff approval.
+            $info = array(
+                    'isactive' => ($message->getUserId() == $this->getUserId())? 1: 0);
             $collabs = array();
             foreach ($vars['recipients'] as $recipient) {
                 if (($user=User::fromVars($recipient)))
@@ -1482,7 +1533,7 @@ class Ticket {
         if ($autorespond && $message->isAutoResponse())
             $autorespond=false;
 
-        $this->onMessage($autorespond, $message); //must be called b4 sending alerts to staff.
+        $this->onMessage($message, $autorespond); //must be called b4 sending alerts to staff.
 
         $dept = $this->getDept();
 
@@ -1526,12 +1577,7 @@ class Ticket {
             }
         }
 
-        unset($variables['message']);
-        $variables['message'] = $message->asVar();
-
-        $info = $options + array('variables' => $variables);
-        $this->activityNotice($info);
-
+        $this->notifyCollaborators($message);
 
         return $message;
     }
@@ -1636,7 +1682,8 @@ class Ticket {
                 && ($tpl = $dept->getTemplate())
                 && ($msg=$tpl->getReplyMsgTemplate())) {
 
-            $msg = $this->replaceVars($msg->asArray(), $variables);
+            $msg = $this->replaceVars($msg->asArray(),
+                    $variables + array('recipient' => $this->getOwner()));
 
             if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator()))
                 $msg['body'] = "<p style=\"display:none\">$tag<p>".$msg['body'];
@@ -1645,12 +1692,8 @@ class Ticket {
                 $options);
         }
 
-        if($vars['emailcollab']) {
-            unset($variables['response']);
-            $variables['message'] = $response->asVar();
-            $info = $options + array('variables' => $variables);
-            $this->activityNotice($info);
-        }
+        if($vars['emailcollab'])
+            $this->notifyCollaborators($response);
 
         return $response;
     }
@@ -1770,7 +1813,7 @@ class Ticket {
     function pdfExport($psize='Letter', $notes=false) {
         require_once(INCLUDE_DIR.'class.pdf.php');
         $pdf = new Ticket2PDF($this, $psize, $notes);
-        $name='Ticket-'.$this->getExtId().'.pdf';
+        $name='Ticket-'.$this->getNumber().'.pdf';
         $pdf->Output($name, 'I');
         //Remember what the user selected - for autoselect on the next print.
         $_SESSION['PAPER_SIZE'] = $psize;
@@ -1869,15 +1912,15 @@ class Ticket {
 
 
    /*============== Static functions. Use Ticket::function(params); =============nolint*/
-    function getIdByExtId($extId, $email=null) {
+    function getIdByNumber($number, $email=null) {
 
-        if(!$extId || !is_numeric($extId))
+        if(!$number)
             return 0;
 
         $sql ='SELECT ticket.ticket_id FROM '.TICKET_TABLE.' ticket '
              .' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id'
              .' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id'
-             .' WHERE ticket.ticketID='.db_input($extId);
+             .' WHERE ticket.`number`='.db_input($number);
 
         if($email)
             $sql .= ' AND email.address = '.db_input($email);
@@ -1898,20 +1941,20 @@ class Ticket {
             ?$ticket:null;
     }
 
-    function lookupByExtId($id, $email=null) {
-        return self::lookup(self:: getIdByExtId($id, $email));
+    function lookupByNumber($number, $email=null) {
+        return self::lookup(self:: getIdByNumber($number, $email));
     }
 
-    function genExtRandID() {
-        global $cfg;
+    function genRandTicketNumber($len = EXT_TICKET_ID_LEN) {
 
-        //We can allow collissions...extId and email must be unique ...so same id with diff emails is ok..
-        // But for clarity...we are going to make sure it is unique.
-        $id=Misc::randNumber(EXT_TICKET_ID_LEN);
-        if(db_num_rows(db_query('SELECT ticket_id FROM '.TICKET_TABLE.' WHERE ticketID='.db_input($id))))
-            return Ticket::genExtRandID();
+        //We can allow collissions...number and email must be unique ...so
+        // same number with diff emails is ok.. But for clarity...we are going to make sure it is unique.
+        $number = Misc::randNumber($len);
+        if(db_num_rows(db_query('SELECT ticket_id FROM '.TICKET_TABLE.'
+                        WHERE `number`='.db_input($number))))
+            return Ticket::genRandTicketNumber($len);
 
-        return $id;
+        return $number;
     }
 
     function getIdByMessageId($mid, $email) {
@@ -1992,11 +2035,8 @@ class Ticket {
     /* Quick client's tickets stats
        @email - valid email.
      */
-    function getClientStats($email) {
-
-        if(!$email || !Validator::is_email($email))
-            return null;
-        if (!$user = User::lookup(array('emails__address'=>$email)))
+    function getUserStats($user) {
+        if(!$user || !($user instanceof EndUser))
             return null;
 
         $sql='SELECT count(open.ticket_id) as open, count(closed.ticket_id) as closed '
@@ -2046,8 +2086,8 @@ class Ticket {
 
             //Make sure the open ticket limit hasn't been reached. (LOOP CONTROL)
             if($cfg->getMaxOpenTickets()>0 && strcasecmp($origin,'staff')
-                    && ($client=Client::lookupByEmail($vars['email']))
-                    && ($openTickets=$client->getNumOpenTickets())
+                    && ($user=TicketUser::lookupByEmail($vars['email']))
+                    && ($openTickets=$user->getNumOpenTickets())
                     && ($openTickets>=$cfg->getMaxOpenTickets()) ) {
 
                 $errors['err']="You've reached the maximum open tickets allowed.";
@@ -2201,11 +2241,11 @@ class Ticket {
         $ipaddress=$vars['ip']?$vars['ip']:$_SERVER['REMOTE_ADDR'];
 
         //We are ready son...hold on to the rails.
-        $extId=Ticket::genExtRandID();
+        $number = Ticket::genRandTicketNumber();
         $sql='INSERT INTO '.TICKET_TABLE.' SET created=NOW() '
             .' ,lastmessage= NOW()'
             .' ,user_id='.db_input($user->id)
-            .' ,ticketID='.db_input($extId)
+            .' ,`number`='.db_input($number)
             .' ,dept_id='.db_input($deptId)
             .' ,topic_id='.db_input($topicId)
             .' ,ip_address='.db_input($ipaddress)
@@ -2222,9 +2262,9 @@ class Ticket {
         /* -------------------- POST CREATE ------------------------ */
 
         if(!$cfg->useRandomIds()) {
-            //Sequential ticketIDs support really..really suck arse.
-            $extId=$id; //To make things really easy we are going to use autoincrement ticket_id.
-            db_query('UPDATE '.TICKET_TABLE.' SET ticketID='.db_input($extId).' WHERE ticket_id='.$id.' LIMIT 1');
+            //Sequential ticket number support really..really suck arse.
+            //To make things really easy we are going to use autoincrement ticket_id.
+            db_query('UPDATE '.TICKET_TABLE.' SET `number`='.db_input($id).' WHERE ticket_id='.$id.' LIMIT 1');
             //TODO: RETHING what happens if this fails?? [At the moment on failure random ID is used...making stuff usable]
         }
 
@@ -2238,6 +2278,7 @@ class Ticket {
         //post the message.
         unset($vars['cannedattachments']); //Ticket::open() might have it set as part of  open & respond.
         $vars['title'] = $vars['subject']; //Use the initial subject as title of the post.
+        $vars['userId'] = $ticket->getUserId();
         $message = $ticket->postMessage($vars , $origin, false);
 
         // Configure service-level-agreement for this ticket
@@ -2283,8 +2324,8 @@ class Ticket {
 
         /************ check if the user JUST reached the max. open tickets limit **********/
         if($cfg->getMaxOpenTickets()>0
-                    && ($client=$ticket->getClient())
-                    && ($client->getNumOpenTickets()==$cfg->getMaxOpenTickets())) {
+                    && ($user=$ticket->getOwner())
+                    && ($user->getNumOpenTickets()==$cfg->getMaxOpenTickets())) {
             $ticket->onOpenLimit(($autorespond && strcasecmp($origin, 'staff')));
         }
 
diff --git a/include/class.user.php b/include/class.user.php
index 9102771c846e69f64d0d5442e5d7b8700c5b3c20..e74e922a56e65ebf82bccbcdc2d7393688fc3e7f 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -138,6 +138,10 @@ class User extends UserModel {
         return $this->updated;
     }
 
+    function getCreateDate() {
+        return $this->created;
+    }
+
     function to_json() {
 
         $info = array(
@@ -464,4 +468,55 @@ class UserEmail extends UserEmailModel {
     }
 }
 
+
+/*
+ *  Generic user list.
+ */
+class UserList implements  IteratorAggregate, ArrayAccess {
+    private $users;
+
+    function __construct($list = array()) {
+        $this->users = $list;
+    }
+
+    function add($user) {
+        $this->offsetSet(null, $user);
+    }
+
+    function offsetSet($offset, $value) {
+
+        if (is_null($offset))
+            $this->users[] = $value;
+        else
+            $this->users[$offset] = $value;
+    }
+
+    function offsetExists($offset) {
+        return isset($this->users[$offset]);
+    }
+
+    function offsetUnset($offset) {
+        unset($this->users[$offset]);
+    }
+
+    function offsetGet($offset) {
+        return isset($this->users[$offset]) ? $this->users[$offset] : null;
+    }
+
+    function getIterator() {
+        return new ArrayIterator($this->users);
+    }
+
+    function __toString() {
+
+        $list = array();
+        foreach($this->users as $user) {
+            if (is_object($user))
+                $list [] = $user->getName();
+        }
+
+        return $list ? implode(', ', $list) : '';
+    }
+}
+
 ?>
diff --git a/include/class.usersession.php b/include/class.usersession.php
index e77f65fac77f63853cb3c6a4dea99d93e51ea491..1eae3af81e058246f68d49c11c2660f489ce5d97 100644
--- a/include/class.usersession.php
+++ b/include/class.usersession.php
@@ -111,13 +111,13 @@ class UserSession {
 
 }
 
-class ClientSession extends Client {
+class ClientSession extends EndUser {
 
     var $session;
 
-    function ClientSession($email, $id){
-        parent::Client($id, $email);
-        $this->session= new UserSession($email);
+    function __construct($user) {
+        parent::__construct($user);
+        $this->session= new UserSession($user->getUserName());
     }
 
     function isValid(){
@@ -156,13 +156,13 @@ class StaffSession extends Staff {
 
     var $session;
 
-    function StaffSession($var){
-        parent::Staff($var);
+    function __construct($var) {
+        parent::__construct($var);
         $this->session= new UserSession($this->getId());
     }
 
     function isValid(){
-        global $_SESSION,$cfg;
+        global $_SESSION, $cfg;
 
         if(!$this->getId() || $this->session->getSessionId()!=session_id())
             return false;
diff --git a/include/class.variable.php b/include/class.variable.php
index 36d49e6a39accdcdee68aaa06cd8059779474e00..233c9fc18696886460991b35bf3dba18701762cc 100644
--- a/include/class.variable.php
+++ b/include/class.variable.php
@@ -63,11 +63,15 @@ class VariableReplacer {
 
         if(!$obj) return "";
 
-        if(!$var && is_callable(array($obj, 'asVar')))
-            return call_user_func(array($obj, 'asVar'));
+        if (!$var) {
+            if (method_exists($obj, 'asVar'))
+                return call_user_func(array($obj, 'asVar'));
+            elseif (method_exists($obj, '__toString'))
+                return (string) $obj;
+        }
 
         list($v, $part) = explode('.', $var, 2);
-        if($v && is_callable(array($obj, 'get'.ucfirst($v)))) {
+        if ($v && is_callable(array($obj, 'get'.ucfirst($v)))) {
             $rv = call_user_func(array($obj, 'get'.ucfirst($v)));
             if(!$rv || !is_object($rv))
                 return $rv;
@@ -75,7 +79,7 @@ class VariableReplacer {
             return $this->getVar($rv, $part);
         }
 
-        if(!$var || !is_callable(array($obj, 'getVar')))
+        if (!$var || !method_exists($obj, 'getVar'))
             return "";
 
         $parts = explode('.', $var);
diff --git a/include/client/header.inc.php b/include/client/header.inc.php
index 9f20098b4b1b4fcfc2d891973717c42876296fde..4f1c93f88433f82744d328bbe7b70e0687ad6669 100644
--- a/include/client/header.inc.php
+++ b/include/client/header.inc.php
@@ -48,7 +48,7 @@ header("Content-Type: text/html; charset=UTF-8\r\n");
                  ?>
                 <?php
                 if($cfg->showRelatedTickets()) {?>
-                <a href="<?php echo ROOT_PATH; ?>tickets.php">My Tickets <b>(<?php echo $thisclient->getNumTickets(); ?>)</b></a> -
+                <a href="<?php echo ROOT_PATH; ?>tickets.php">Tickets <b>(<?php echo $thisclient->getNumTickets(); ?>)</b></a> -
                 <?php
                 } ?>
                 <a href="<?php echo ROOT_PATH; ?>logout.php?auth=<?php echo $ost->getLinkToken(); ?>">Log Out</a>
diff --git a/include/client/open.inc.php b/include/client/open.inc.php
index 75211c0982b687475e41e2b9bc4e738aab1d1a7d..3f070c703d9c32b63fc43ca1f3001aff0207f546 100644
--- a/include/client/open.inc.php
+++ b/include/client/open.inc.php
@@ -4,7 +4,7 @@ $info=array();
 if($thisclient && $thisclient->isValid()) {
     $info=array('name'=>$thisclient->getName(),
                 'email'=>$thisclient->getEmail(),
-                'phone'=>$thisclient->getPhone());
+                'phone'=>$thisclient->getPhoneNumber());
 }
 
 $info=($_POST && $errors)?Format::htmlchars($_POST):$info;
diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php
index 7c2c6e56436e5fcf454caf8c6dca5d117f4f6e72..1c227ba4cdec255b7e75607abfeab143e549d305 100644
--- a/include/client/tickets.inc.php
+++ b/include/client/tickets.inc.php
@@ -18,8 +18,8 @@ if(isset($_REQUEST['status'])) { //Query string status has nothing to do with th
     $status='open'; //Defaulting to open
 }
 
-$sortOptions=array('id'=>'ticketID', 'name'=>'user.name', 'subject'=>'subject.value',
-                    'email'=>'email.address', 'status'=>'ticket.status', 'dept'=>'dept_name','date'=>'ticket.created');
+$sortOptions=array('id'=>'`number`', 'subject'=>'subject.value',
+                    'status'=>'ticket.status', 'dept'=>'dept_name','date'=>'ticket.created');
 $orderWays=array('DESC'=>'DESC','ASC'=>'ASC');
 //Sorting options...
 $order_by=$order=null;
@@ -38,9 +38,8 @@ if($order_by && strpos($order_by,','))
 $x=$sort.'_sort';
 $$x=' class="'.strtolower($order).'" ';
 
-$qselect='SELECT ticket.ticket_id,ticket.ticketID,ticket.dept_id,isanswered, '
-    .'dept.ispublic, subject.value as subject, '
-    .'user.name, email.address as email, '
+$qselect='SELECT ticket.ticket_id,ticket.`number`,ticket.dept_id,isanswered, '
+    .'dept.ispublic, subject.value as subject,'
     .'dept_name,ticket. status, ticket.source, ticket.created ';
 
 $dynfields='(SELECT entry.object_id, value FROM '.FORM_ANSWER_TABLE.' ans '.
@@ -51,11 +50,13 @@ $subject_sql = sprintf($dynfields, 'subject');
 
 $qfrom='FROM '.TICKET_TABLE.' ticket '
       .' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.dept_id) '
-      .' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id'
-      .' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id'
+      .' LEFT JOIN '.TICKET_COLLABORATOR_TABLE.' collab
+        ON (collab.ticket_id = ticket.ticket_id
+                AND collab.user_id ='.$thisclient->getId().' )'
       .' LEFT JOIN '.$subject_sql.' subject ON ticket.ticket_id = subject.object_id ';
 
-$qwhere =' WHERE email.address='.db_input($thisclient->getEmail());
+$qwhere = sprintf(' WHERE ( ticket.user_id=%d OR collab.user_id=%d )',
+            $thisclient->getId(), $thisclient->getId());
 
 if($status){
     $qwhere.=' AND ticket.status='.db_input($status);
@@ -65,7 +66,7 @@ $search=($_REQUEST['a']=='search' && $_REQUEST['q']);
 if($search) {
     $qstr.='&a='.urlencode($_REQUEST['a']).'&q='.urlencode($_REQUEST['q']);
     if(is_numeric($_REQUEST['q'])) {
-        $qwhere.=" AND ticket.ticketID LIKE '$queryterm%'";
+        $qwhere.=" AND ticket.`number` LIKE '$queryterm%'";
     } else {//Deep search!
         $queryterm=db_real_escape($_REQUEST['q'],false); //escape the term ONLY...no quotes.
         $qwhere.=' AND ( '
@@ -100,7 +101,7 @@ if($search)
 $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting
 
 ?>
-<h1>My Tickets</h1>
+<h1>Tickets</h1>
 <br>
 <form action="tickets.php" method="get" id="ticketSearchForm">
     <input type="hidden" name="a"  value="search">
@@ -152,24 +153,24 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting
             if($row['attachments'])
                 $subject.='  &nbsp;&nbsp;<span class="Icon file"></span>';
 
-            $ticketID=$row['ticketID'];
+            $ticketNumber=$row['number'];
             if($row['isanswered'] && !strcasecmp($row['status'],'open')) {
                 $subject="<b>$subject</b>";
-                $ticketID="<b>$ticketID</b>";
+                $ticketNumber="<b>$ticketNumber</b>";
             }
             $phone=Format::phone($row['phone']);
             if($row['phone_ext'])
                 $phone.=' '.$row['phone_ext'];
             ?>
-            <tr id="<?php echo $row['ticketID']; ?>">
+            <tr id="<?php echo $row['ticket_id']; ?>">
                 <td class="centered">
                 <a class="Icon <?php echo strtolower($row['source']); ?>Ticket" title="<?php echo $row['email']; ?>"
-                    href="tickets.php?id=<?php echo $row['ticketID']; ?>"><?php echo $ticketID; ?></a>
+                    href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $ticketNumber; ?></a>
                 </td>
                 <td>&nbsp;<?php echo Format::db_date($row['created']); ?></td>
                 <td>&nbsp;<?php echo ucfirst($row['status']); ?></td>
                 <td>
-                    <a href="tickets.php?id=<?php echo $row['ticketID']; ?>"><?php echo $subject; ?></a>
+                    <a href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $subject; ?></a>
                 </td>
                 <td>&nbsp;<?php echo Format::truncate($dept,30); ?></td>
                 <td><?php echo $phone; ?></td>
diff --git a/include/client/view.inc.php b/include/client/view.inc.php
index 910d021de0fd98940996d90b311fb54a848a134e..c36eaec96dd81d06804a5cd44d26fc986a6d2737 100644
--- a/include/client/view.inc.php
+++ b/include/client/view.inc.php
@@ -1,5 +1,5 @@
 <?php
-if(!defined('OSTCLIENTINC') || !$thisclient || !$ticket || !$ticket->checkClientAccess($thisclient)) die('Access Denied!');
+if(!defined('OSTCLIENTINC') || !$thisclient || !$ticket || !$ticket->checkUserAccess($thisclient)) die('Access Denied!');
 
 $info=($_POST && $errors)?Format::htmlchars($_POST):array();
 
@@ -13,8 +13,8 @@ if(!$dept || !$dept->isPublic())
     <tr>
         <td colspan="2" width="100%">
             <h1>
-                Ticket #<?php echo $ticket->getExtId(); ?> &nbsp;
-                <a href="view.php?id=<?php echo $ticket->getExtId(); ?>" title="Reload"><span class="Icon refresh">&nbsp;</span></a>
+                Ticket #<?php echo $ticket->getNumber(); ?> &nbsp;
+                <a href="view.php?id=<?php echo $ticket->getId(); ?>" title="Reload"><span class="Icon refresh">&nbsp;</span></a>
             </h1>
         </td>
     </tr>
@@ -124,10 +124,10 @@ if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) {
 <?php }elseif($warn) { ?>
     <div id="msg_warning"><?php echo $warn; ?></div>
 <?php } ?>
-<form id="reply" action="tickets.php?id=<?php echo $ticket->getExtId(); ?>#reply" name="reply" method="post" enctype="multipart/form-data">
+<form id="reply" action="tickets.php?id=<?php echo $ticket->getId(); ?>#reply" name="reply" method="post" enctype="multipart/form-data">
     <?php csrf_token(); ?>
     <h2>Post a Reply</h2>
-    <input type="hidden" name="id" value="<?php echo $ticket->getExtId(); ?>">
+    <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>">
     <input type="hidden" name="a" value="reply">
     <table border="0" cellspacing="0" cellpadding="3" style="width:100%">
         <tr>
@@ -143,7 +143,7 @@ if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) {
                 <br/>
                 <textarea name="message" id="message" cols="50" rows="9" wrap="soft"
                     data-draft-namespace="ticket.client"
-                    data-draft-object-id="<?php echo $ticket->getExtId(); ?>"
+                    data-draft-object-id="<?php echo $ticket->getId(); ?>"
                     class="richtext ifhtml draft"><?php echo $info['message']; ?></textarea>
             </td>
         </tr>
diff --git a/include/i18n/en_US/templates/email/message.autoresp.yaml b/include/i18n/en_US/templates/email/message.autoresp.yaml
index 30c641de0f74048f297333de499197865c7a2dbd..61905d57f04f06f754f0c30a6f590f3eaca48292 100644
--- a/include/i18n/en_US/templates/email/message.autoresp.yaml
+++ b/include/i18n/en_US/templates/email/message.autoresp.yaml
@@ -14,9 +14,9 @@ notes: |
 subject: |
     Message Confirmation
 body: |
-    <h3><strong>Dear %{ticket.name.first},</strong></h3>
+    <h3><strong>Dear %{recipient.name.first},</strong></h3>
     Your reply to support request <a
-    href="%{ticket.client_link}">#%{ticket.number}</a> has been noted
+    href="%{recipient.ticket_link}">#%{ticket.number}</a> has been noted
     <br>
     <br>
     <div style="color: rgb(127, 127, 127); ">
@@ -26,7 +26,7 @@ body: |
     <hr>
     <div style="color: rgb(127, 127, 127); font-size: small; text-align:
     center"><em>You can view the support request progress <a
-    href="%{ticket.client_link}">online here</a></em><br>
+    href="%{recipient.ticket_link}">online here</a></em><br>
     <a href="http://osticket.com/"><img
     src="cid:b56944cb4722cc5cda9d1e23a3ea7fbc" alt="Powered by osTicket"
     width="126" height="19" style="width: 126px;"></a></div>
diff --git a/include/i18n/en_US/templates/email/ticket.activity.notice.yaml b/include/i18n/en_US/templates/email/ticket.activity.notice.yaml
index 4248931a815db98cd8cd3a7ce35a2048ee07da2c..ee3023175914465493c0e808456bee1f7f858ce9 100644
--- a/include/i18n/en_US/templates/email/ticket.activity.notice.yaml
+++ b/include/i18n/en_US/templates/email/ticket.activity.notice.yaml
@@ -27,9 +27,9 @@ body: |
     <hr>
     <div style="color: rgb(127, 127, 127); font-size: small; text-align: center;">
     <em>You're getting this email because you are a collaborator
-    on ticket <a href="%{ticket.client_link}" style="color: rgb(84, 141, 212);"
+    on ticket <a href="%{recipient.ticket_link}" style="color: rgb(84, 141, 212);"
     >#%{ticket.number}</a>.  To participate, simply reply to this email
-    or <a href="%{ticket.client_link}" style="color: rgb(84, 141, 212);"
+    or <a href="%{recipient.ticket_link}" style="color: rgb(84, 141, 212);"
     >click here</a> for a complete archive of the ticket thread.</em>
     </div>
     <div style="text-align: center;"> <a href="http://osticket.com/"><img
diff --git a/include/i18n/en_US/templates/email/ticket.autoreply.yaml b/include/i18n/en_US/templates/email/ticket.autoreply.yaml
index e50551566ac31ec72311dcd584fe70bde5bfc467..eb7a50c5a6e3852d636a89214a36c03967c327cd 100644
--- a/include/i18n/en_US/templates/email/ticket.autoreply.yaml
+++ b/include/i18n/en_US/templates/email/ticket.autoreply.yaml
@@ -21,11 +21,11 @@ body: |
     float: right; margin: 0px 0px 10px 10px;">
     <br>
     <br>
-    <strong>Dear %{ticket.name.first},</strong>
+    <strong>Dear %{recipient.name.first},</strong>
     <br>
     <br>
     A request for support has been created and assigned ticket <a
-    href="%{ticket.client_link}">#%{ticket.number}</a> with the following
+    href="%{recipient.ticket_link}">#%{ticket.number}</a> with the following
     automatic reply
     <br>
     <br>
@@ -43,7 +43,7 @@ body: |
     <div style="color: rgb(127, 127, 127); font-size: small; text-align:
     center;"><em>We hope this response has sufficiently answered your questions.
     If not, please do not send another email. Instead, reply to this email
-    or <a href="%{ticket.client_link}"><span style="color: rgb(84, 141,
+    or <a href="%{recipient.ticket_link}"><span style="color: rgb(84, 141,
     212);" >login to your account</span></a> for a complete archive of all
     your support requests and responses.</em></div>
     <div style="text-align: center;">
diff --git a/include/i18n/en_US/templates/email/ticket.autoresp.yaml b/include/i18n/en_US/templates/email/ticket.autoresp.yaml
index ebdfde6f106bed221ff5ebe44af66f4456dea58f..47e05b5995ab8e918f33ff58292e8eb67ef8631b 100644
--- a/include/i18n/en_US/templates/email/ticket.autoresp.yaml
+++ b/include/i18n/en_US/templates/email/ticket.autoresp.yaml
@@ -25,7 +25,7 @@ body: |
     </tr>
     <tr>
         <td style="width:67%; padding-top: 12pt; padding-right: 12pt">
-            Dear %{ticket.name.first},
+            Dear %{recipient.name.first},
             <br>
             <br>
             We received your request and assigned ticket #%{ticket.number}
@@ -39,7 +39,7 @@ body: |
             <br>
             <br>
             A representative will follow-up with you as soon as possible.
-            You can <a href="%{ticket.client_link}">view this ticket's
+            You can <a href="%{recipient.ticket_link}">view this ticket's
             progress online</a>.
             <br>
             <br>
@@ -54,7 +54,7 @@ body: |
             <span style="color: rgb(127, 127, 127); "> If you wish to send
             additional comments or information regarding this issue, please
             don't open a new ticket. Simply</span> <a
-            href="%{ticket.client_link}"><span style="color: rgb(84, 141, 212);"
+            href="%{recipient.ticket_link}"><span style="color: rgb(84, 141, 212);"
             >login</span></a> <span style="color: rgb(127, 127, 127);"
             >and update the ticket.</span>
             <br>
diff --git a/include/i18n/en_US/templates/email/ticket.notice.yaml b/include/i18n/en_US/templates/email/ticket.notice.yaml
index 530f32a1f78fb33bab55b6b7b2942ffdaa798d73..55d0bb0d218acd01e22bffbd20f8a342ce496a06 100644
--- a/include/i18n/en_US/templates/email/ticket.notice.yaml
+++ b/include/i18n/en_US/templates/email/ticket.notice.yaml
@@ -19,11 +19,11 @@ body: |
     float: right; margin: 0px 0px 10px 10px;">
     <br>
     <br>
-    <strong>Dear %{ticket.name.first},</strong>
+    <strong>Dear %{recipient.name.first},</strong>
     <br>
     <br>
     Our customer care team has created a ticket, <a
-    href="%{ticket.client_link}">#%{ticket.number}</a> on your behalf, with
+    href="%{recipient.ticket_link}">#%{ticket.number}</a> on your behalf, with
     the following details and summary:
     <br>
     <br>
@@ -36,7 +36,7 @@ body: |
     <br>
     <br>
     If need be, a representative will follow-up with you as soon as
-    possible. You can also <a href="%{ticket.client_link}">view this
+    possible. You can also <a href="%{recipient.ticket_link}">view this
     ticket's progress online</a>.
     <br>
     <br>
@@ -47,7 +47,7 @@ body: |
     <div style="color: rgb(127, 127, 127); font-size: small; "><em>If you
     wish to provide additional comments or information regarding the issue,
     please do not send another email. Instead, reply to this email or <a
-    href="%{ticket.client_link}"><span style="color: rgb(84, 141, 212);"
+    href="%{recipient.ticket_link}"><span style="color: rgb(84, 141, 212);"
     >login to your account</span></a> for a complete archive of all your
     support requests and responses.</em></div>
     <br>
diff --git a/include/i18n/en_US/templates/email/ticket.reply.yaml b/include/i18n/en_US/templates/email/ticket.reply.yaml
index c576a720945eb7b490132aa8d31c30d468da8a17..80774c737635171d069cb4f9fa9e3f4dac8b2989 100644
--- a/include/i18n/en_US/templates/email/ticket.reply.yaml
+++ b/include/i18n/en_US/templates/email/ticket.reply.yaml
@@ -15,7 +15,7 @@ body: |
     width="113" height="64" style="float: right; width: 113px; margin: 0px 0px 10px 10px;">
     <h3><span style="color: rgb(127, 127, 127); font-weight: normal;
     font-family: Georgia; font-size: 30pt">We're Here For You</span></h3>
-    <strong>Dear %{ticket.name},</strong>
+    <strong>Dear %%{recipient.name},</strong>
     <br>
     <br>
     %{response}
@@ -29,7 +29,7 @@ body: |
     <div style="color: rgb(127, 127, 127); font-size: small; text-align: center;"
     ><em>We hope this response has sufficiently answered your questions. If
     not, please do not send another email. Instead, reply to this email or
-    <a href="%{ticket.client_link}" style="color: rgb(84, 141, 212);" >login
+    <a href="%{recipient.ticket_link}" style="color: rgb(84, 141, 212);" >login
     to your account</a> for a complete archive of all your support requests
     and responses.</em></div>
     <div style="text-align: center;">
diff --git a/include/staff/staff.inc.php b/include/staff/staff.inc.php
index 667b462312b603a205c912584dafb35fe15ac06c..55351fbee7864581e1294a72e56d758d2d068c48 100644
--- a/include/staff/staff.inc.php
+++ b/include/staff/staff.inc.php
@@ -121,7 +121,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                     $('#password-fields').show();
                 ">
                 <option value="">&mdash; Use any available backend &mdash;</option>
-            <?php foreach (AuthenticationBackend::allRegistered() as $ab) {
+            <?php foreach (StaffAuthenticationBackend::allRegistered() as $ab) {
                 if (!$ab->supportsAuthentication()) continue; ?>
                 <option value="<?php echo $ab::$id; ?>" <?php
                     if ($info['backend'] == $ab::$id)
diff --git a/include/staff/ticket-edit.inc.php b/include/staff/ticket-edit.inc.php
index cddb2ee3c6b36f248110fb85833cdb87760b9b9a..e60f275c2633c237a51f91eeab1b048bf0abf980 100644
--- a/include/staff/ticket-edit.inc.php
+++ b/include/staff/ticket-edit.inc.php
@@ -11,7 +11,7 @@ if ($_POST)
  <input type="hidden" name="do" value="update">
  <input type="hidden" name="a" value="edit">
  <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>">
- <h2>Update Ticket #<?php echo $ticket->getExtId(); ?></h2>
+ <h2>Update Ticket #<?php echo $ticket->getNumber(); ?></h2>
  <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
     <tbody>
         <tr>
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index 6fee37da63a4817fb462815677b2a0bf14370f87..a9b1c8edf41bb9f0169008239dd1b56599d84b05 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -15,6 +15,7 @@ if($cfg->getLockTime() && !$ticket->acquireLock($thisstaff->getId(),$cfg->getLoc
 //Get the goodies.
 $dept  = $ticket->getDept();  //Dept
 $staff = $ticket->getStaff(); //Assigned or closed by..
+$user  = $ticket->getOwner(); //Ticket User (EndUser)
 $team  = $ticket->getTeam();  //Assigned team.
 $sla   = $ticket->getSLA();
 $lock  = $ticket->getLock();  //Ticket lock obj
@@ -40,7 +41,8 @@ if($ticket->isOverdue())
 <table width="940" cellpadding="2" cellspacing="0" border="0">
     <tr>
         <td width="50%" class="has_bottom_border">
-             <h2><a href="tickets.php?id=<?php echo $ticket->getId(); ?>" title="Reload"><i class="icon-refresh"></i> Ticket #<?php echo $ticket->getExtId(); ?></a></h2>
+             <h2><a href="tickets.php?id=<?php echo $ticket->getId(); ?>"
+             title="Reload"><i class="icon-refresh"></i> Ticket #<?php echo $ticket->getNumber(); ?></a></h2>
         </td>
         <td width="50%" class="right_align has_bottom_border">
             <?php
@@ -161,19 +163,19 @@ if($ticket->isOverdue())
                             ><?php echo Format::htmlchars($ticket->getName());
                         ?></span></a>
                         <?php
-                        if(($client=$ticket->getClient())) {
+                        if($user) {
                             echo sprintf('&nbsp;&nbsp;<a href="tickets.php?a=search&ownerId=%d" title="Related Tickets" data-dropdown="#action-dropdown-stats">(<b>%d</b>)</a>',
-                                    urlencode($ticket->getOwnerId()), $client->getNumTickets());
+                                    urlencode($user->getId()), $user->getNumTickets());
                         ?>
                             <div id="action-dropdown-stats" class="action-dropdown anchor-right">
                                 <ul>
                                     <?php
-                                    if(($open=$client->getNumOpenTickets()))
+                                    if(($open=$user->getNumOpenTickets()))
                                         echo sprintf('<li><a href="tickets.php?a=search&status=open&ownerId=%s"><i class="icon-folder-open-alt"></i> %d Open Tickets</a></li>',
-                                                $ticket->getOwnerId(), $open);
-                                    if(($closed=$client->getNumClosedTickets()))
+                                                $user->getId(), $open);
+                                    if(($closed=$user->getNumClosedTickets()))
                                         echo sprintf('<li><a href="tickets.php?a=search&status=closed&ownerId=%d"><i class="icon-folder-close-alt"></i> %d Closed Tickets</a></li>',
-                                                $ticket->getOwnerId(), $closed);
+                                                $user->getId(), $closed);
                                     ?>
                                     <li><a href="tickets.php?a=search&ownerId=<?php echo $ticket->getOwnerId(); ?>"><i class="icon-double-angle-right"></i> All Tickets</a></li>
                                 </u>
diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php
index bbd837c9ce8b94f5cd292854f13e85950a47be18..530d6b2d93ec7262370d81fed89c0a7bab14bdfa 100644
--- a/include/staff/tickets.inc.php
+++ b/include/staff/tickets.inc.php
@@ -115,7 +115,7 @@ if($search):
         $qstr.='&query='.urlencode($searchTerm);
         $queryterm=db_real_escape($searchTerm,false); //escape the term ONLY...no quotes.
         if (is_numeric($searchTerm)) {
-            $qwhere.=" AND ticket.ticketID LIKE '$queryterm%'";
+            $qwhere.=" AND ticket.`number` LIKE '$queryterm%'";
         } elseif (strpos($searchTerm,'@') && Validator::is_email($searchTerm)) {
             //pulling all tricks!
             # XXX: What about searching for email addresses in the body of
@@ -143,7 +143,7 @@ if ($_REQUEST['advsid'] && isset($_SESSION['adv_'.$_REQUEST['advsid']])) {
         db_input($_SESSION['adv_'.$_REQUEST['advsid']])).')';
 }
 
-$sortOptions=array('date'=>'effective_date','ID'=>'ticketID',
+$sortOptions=array('date'=>'effective_date','ID'=>'`number`',
     'pri'=>'priority_id','name'=>'user.name','subj'=>'subject',
     'status'=>'ticket.status','assignee'=>'assigned','staff'=>'staff',
     'dept'=>'dept_name');
@@ -195,7 +195,7 @@ $$x=' class="'.strtolower($order).'" ';
 if($_GET['limit'])
     $qstr.='&limit='.urlencode($_GET['limit']);
 
-$qselect ='SELECT ticket.ticket_id,lock_id,ticketID,ticket.dept_id,ticket.staff_id,ticket.team_id '
+$qselect ='SELECT ticket.ticket_id,lock_id,`number`,ticket.dept_id,ticket.staff_id,ticket.team_id '
     .' ,user.name'
     .' ,email.address as email, dept_name '
          .' ,ticket.status,ticket.source,isoverdue,isanswered,ticket.created ';
@@ -386,7 +386,7 @@ if ($results) {
                 }else{
                     $lc=Format::truncate($row['dept_name'],40);
                 }
-                $tid=$row['ticketID'];
+                $tid=$row['number'];
                 $subject = Format::htmlchars(Format::truncate($row['subject'],40));
                 $threadcount=$row['thread_count'];
                 if(!strcasecmp($row['status'],'open') && !$row['isanswered'] && !$row['lock_id']) {
diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig
index 4fecf9e0832d67cadca7a05979a2e1f7fe464853..ac8e6b7747418718552fa3ecf14189a652bf9901 100644
--- a/include/upgrader/streams/core.sig
+++ b/include/upgrader/streams/core.sig
@@ -1 +1 @@
-ed60ba203a473f4f32ac49eb45db16c7
+934954de8914d9bd2bb8343e805340ae
diff --git a/include/upgrader/streams/core/ed60ba20-934954de.patch.sql b/include/upgrader/streams/core/ed60ba20-934954de.patch.sql
new file mode 100644
index 0000000000000000000000000000000000000000..56c0b164ebc842d86be4a97e761764553e69f4f3
--- /dev/null
+++ b/include/upgrader/streams/core/ed60ba20-934954de.patch.sql
@@ -0,0 +1,50 @@
+/**
+ * @version v1.8.1
+ * @signature 934954de8914d9bd2bb8343e805340ae
+ * @title Various schema improvements and bug fixes
+ *
+ */
+
+-- [#317](https://github.com/osTicket/osTicket-1.8/issues/317)
+ALTER TABLE `%TABLE_PREFIX%faq`
+    CHANGE `created` `created` datetime NOT NULL,
+    CHANGE `updated` `updated` datetime NOT NULL;
+
+-- [#328](https://github.com/osTicket/osTicket-1.8/issues/328)
+UPDATE `%TABLE_PREFIX%filter_rule`
+    SET `how` = 'equal' WHERE `how` IS NULL;
+
+-- [#331](https://github.com/osTicket/osTicket-1.8/issues/331)
+ALTER TABLE `%TABLE_PREFIX%ticket_email_info`
+    CHANGE `message_id` `thread_id` int(11) unsigned NOT NULL,
+    ADD PRIMARY KEY (`thread_id`),
+    DROP INDEX  `message_id`,
+    ADD INDEX  `email_mid` (`email_mid`);
+
+-- [#386](https://github.com/osTicket/osTicket-1.8/issues/386)
+UPDATE `%TABLE_PREFIX%email_template`
+    SET `body` = REPLACE(`body`, '%{recipient}', '%{recipient.name}');
+
+-- Change EndUser link to be recipient specific
+UPDATE `%TABLE_PREFIX%email_template`
+    SET `body` = REPLACE(`body`, '%{ticket.client_link}', '%{recipient.ticket_link}');
+
+-- Add inline flag and drop ref_type
+ALTER TABLE  `%TABLE_PREFIX%ticket_attachment`
+    ADD  `inline` tinyint(1) NOT NULL default  0 AFTER  `ref_id`,
+    DROP  `ref_type`;
+
+ALTER TABLE `%TABLE_PREFIX%ticket_thread`
+    ADD `user_id` int(11) unsigned not null default 0 AFTER `staff_id`;
+
+ALTER TABLE `%TABLE_PREFIX%ticket`
+    ADD `email_id` int(11) unsigned not null default 0 AFTER `team_id`,
+    CHANGE `ticketID` `number` varchar(20);
+
+ALTER TABLE `%TABLE_PREFIX%ticket_collaborator`
+    ADD`created` datetime NOT NULL AFTER `role`;
+
+-- Finished with patch
+UPDATE `%TABLE_PREFIX%config`
+    SET `value` = '934954de8914d9bd2bb8343e805340ae'
+    WHERE `key` = 'schema_signature' AND `namespace` = 'core';
diff --git a/login.php b/login.php
index 789938980ff451cdcc72920d2b9f88080dcc7a47..435eebd93a03e7f4f7d1b75509e3c1685b627a02 100644
--- a/login.php
+++ b/login.php
@@ -21,11 +21,11 @@ define('OSTCLIENTINC',TRUE); //make includes happy
 require_once(INCLUDE_DIR.'class.client.php');
 require_once(INCLUDE_DIR.'class.ticket.php');
 
-if($_POST) {
-
-    if(($user=Client::login(trim($_POST['lticket']), trim($_POST['lemail']), null, $errors))) {
+if ($_POST) {
+    if (($user = UserAuthenticationBackend::process($_POST['lemail'],
+                    $_POST['lticket'], $errors))) {
         //XXX: Ticket owner is assumed.
-        @header('Location: tickets.php?id='.$user->getTicketID());
+        @header('Location: tickets.php?id='.$user->getTicketId());
         require_once('tickets.php'); //Just in case of 'header already sent' error.
         exit;
     } elseif(!$errors['err']) {
diff --git a/logout.php b/logout.php
index 6c6482d9c8c6fe4e452c6a0494abdcba8ae01a93..4b9ea91b133fae4adf96823b8037428d1ed5acd0 100644
--- a/logout.php
+++ b/logout.php
@@ -16,12 +16,10 @@
 
 require('client.inc.php');
 //Check token: Make sure the user actually clicked on the link to logout.
-if(!$_GET['auth'] || !$ost->validateLinkToken($_GET['auth']))
+if(!$thisclient || !$_GET['auth'] || !$ost->validateLinkToken($_GET['auth']))
    @header('Location: index.php');
 
-$_SESSION['_client']=array();
-session_unset();
-session_destroy();
+$thisclient->logOut();
 header('Location: index.php');
 require('index.php');
 ?>
diff --git a/open.php b/open.php
index 706c9ccd4a31e6febee613c9416acfc19988ef36..4cb7684a8167b7cd5a3c07f67d876b83c313cb4d 100644
--- a/open.php
+++ b/open.php
@@ -20,9 +20,8 @@ $errors=array();
 if($_POST):
     $vars = $_POST;
     $vars['deptId']=$vars['emailId']=0; //Just Making sure we don't accept crap...only topicId is expected.
-    if($thisclient) {
-        $vars['name']=$thisclient->getName();
-        $vars['email']=$thisclient->getEmail();
+    if ($thisclient) {
+        $vars['uid']=$thisclient->getId();
     } elseif($cfg->isCaptchaEnabled()) {
         if(!$_POST['captcha'])
             $errors['captcha']='Enter text shown on the image';
@@ -54,11 +53,9 @@ if($_POST):
         }
         //Logged in...simply view the newly created ticket.
         if($thisclient && $thisclient->isValid()) {
-            if(!$cfg->showRelatedTickets())
-                $_SESSION['_client']['key']= $ticket->getExtId(); //Resetting login Key to the current ticket!
             session_write_close();
             session_regenerate_id();
-            @header('Location: tickets.php?id='.$ticket->getExtId());
+            @header('Location: tickets.php?id='.$ticket->getId());
         }
     }else{
         $errors['err']=$errors['err']?$errors['err']:'Unable to create a ticket. Please correct errors below and try again!';
diff --git a/scp/login.php b/scp/login.php
index 17424b6aeda068aa29b8bd233416cc3c604ad868..840716c65deda81e7b87026450ea3bfd6fd51d93 100644
--- a/scp/login.php
+++ b/scp/login.php
@@ -26,7 +26,7 @@ $dest=($dest && (!strstr($dest,'login.php') && !strstr($dest,'ajax.php')))?$dest
 if($_POST) {
     // Lookup support backends for this staff
     $username = trim($_POST['userid']);
-    if ($user = AuthenticationBackend::process($username,
+    if ($user = StaffAuthenticationBackend::process($username,
             $_POST['passwd'], $errors)) {
         @header("Location: $dest");
         require_once('index.php'); //Just incase header is messed up.
@@ -35,11 +35,10 @@ if($_POST) {
 
     $msg = $errors['err']?$errors['err']:'Invalid login';
 }
-
 // Consider single sign-on authentication backends
-if (!$thisstaff || !($thisstaff->getId() || $thisstaff->isValid())) {
-    if (($user = AuthenticationBackend::singleSignOn($errors))
-            && ($user instanceof Staff))
+else if (!$thisstaff || !($thisstaff->getId() || $thisstaff->isValid())) {
+    if (($user = StaffAuthenticationBackend::processSignOn($errors))
+            && ($user instanceof StaffSession))
        @header("Location: $dest");
 }
 
diff --git a/scp/logout.php b/scp/logout.php
index 7076dcec4c0984192acab06de20c5a16cba91416..4cf519d35bc3a66597853f3a96f661190a245b33 100644
--- a/scp/logout.php
+++ b/scp/logout.php
@@ -15,16 +15,15 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 require('staff.inc.php');
+
 //Check token: Make sure the user actually clicked on the link to logout.
 if(!$_GET['auth'] || !$ost->validateLinkToken($_GET['auth']))
     @header('Location: index.php');
 
-$ost->logDebug('Staff logout',
-        sprintf("%s logged out [%s]", 
-            $thisstaff->getUserName(), $_SERVER['REMOTE_ADDR'])); //Debug.
-$_SESSION['_staff']=array();
-session_unset();
-session_destroy();
+$thisstaff->logOut();
+//Clear upgrader session on logout.
+$_SESSION['ost_upgrader'] = null;
+
 @header('Location: login.php');
 require('login.php');
 ?>
diff --git a/scp/pwreset.php b/scp/pwreset.php
index 5b7a20fa86ab35ce1c9e701107a8a9e8be93dee0..6d749e2d5fe8c3b882bf2e0ffb1f012a046d3636 100644
--- a/scp/pwreset.php
+++ b/scp/pwreset.php
@@ -47,26 +47,15 @@ if($_POST) {
         case 'newpasswd':
             // TODO: Compare passwords
             $tpl = 'pwreset.login.php';
-            $_config = new Config('pwreset');
-            if (($staff = new StaffSession($_POST['userid'])) &&
-                    !$staff->getId())
-                $msg = 'Invalid user-id given';
-            elseif (!($id = $_config->get($_POST['token']))
-                    || $id != $staff->getId())
-                $msg = 'Invalid reset token';
-            elseif (!($ts = $_config->lastModified($_POST['token']))
-                    && ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
-                $msg = 'Invalid reset token';
-            elseif (!$staff->forcePasswdRest())
-                $msg = 'Unable to reset password';
-            else {
+            $errors = array();
+            if ($staff = StaffAuthenticationBackend::processSignOn($errors)) {
                 $info = array('page' => 'index.php');
-                Signal::send('auth.pwreset.login', $staff, $info);
-                Staff::_do_login($staff, $_POST['userid']);
-                $_SESSION['_staff']['reset-token'] = $_POST['token'];
                 header('Location: '.$info['page']);
                 exit();
             }
+            elseif (isset($errors['msg'])) {
+                $msg = $errors['msg'];
+            }
             break;
     }
 }
diff --git a/scp/staff.inc.php b/scp/staff.inc.php
index 73fe46d3804b449966f0dc58cbda68308a8145fc..e900ab0f89e7a9c0f2f51eab3efda01299746dd0 100644
--- a/scp/staff.inc.php
+++ b/scp/staff.inc.php
@@ -57,14 +57,13 @@ if(!function_exists('staffLoginPage')) { //Ajax interface can pre-declare the fu
     }
 }
 
-$thisstaff = new StaffSession($_SESSION['_staff']['userID']); //Set staff object.
+$thisstaff = StaffAuthenticationBackend::getUser();
 //1) is the user Logged in for real && is staff.
-if(!$thisstaff->getId() || !$thisstaff->isValid()){
+if (!$thisstaff || !$thisstaff->getId() || !$thisstaff->isValid()) {
     if (isset($_SESSION['_staff']['auth']['msg'])) {
         $msg = $_SESSION['_staff']['auth']['msg'];
         unset($_SESSION['_staff']['auth']['msg']);
-    }
-    elseif (isset($_SESSION['_staff']['userID']) && !$thisstaff->isValid())
+    } elseif ($thisstaff && !$thisstaff->isValid())
         $msg = 'Session timed out due to inactivity';
     else
         $msg = 'Authentication Required';
diff --git a/scp/tickets.php b/scp/tickets.php
index f28ac8e3e31a1677cceede9c867e49e204473b83..dd7dd0cd924aac00bda229407ee13318f120de33 100644
--- a/scp/tickets.php
+++ b/scp/tickets.php
@@ -222,7 +222,7 @@ if($_POST && !$errors):
                     } elseif($ticket->isClosed()) {
                         $errors['err'] = 'Ticket is already closed!';
                     } elseif($ticket->close()) {
-                        $msg='Ticket #'.$ticket->getExtId().' status set to CLOSED';
+                        $msg='Ticket #'.$ticket->getNumber().' status set to CLOSED';
                         //Log internal note
                         if($_POST['ticket_status_notes'])
                             $note = $_POST['ticket_status_notes'];
diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql
index 060c3114885d0923651aa8569165d4cabe3790d3..1dbdd3b7794c5ebd17a4ae6d8494d2642fcbcde3 100644
--- a/setup/inc/streams/core/install-mysql.sql
+++ b/setup/inc/streams/core/install-mysql.sql
@@ -33,8 +33,8 @@ CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%faq` (
   `answer` text NOT NULL,
   `keywords` tinytext,
   `notes` text,
-  `created` date NOT NULL,
-  `updated` date NOT NULL,
+  `created` datetime NOT NULL,
+  `updated` datetime NOT NULL,
   PRIMARY KEY  (`faq_id`),
   UNIQUE KEY `question` (`question`),
   KEY `category_id` (`category_id`),
@@ -506,7 +506,7 @@ CREATE TABLE `%TABLE_PREFIX%team_member` (
 DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket`;
 CREATE TABLE `%TABLE_PREFIX%ticket` (
   `ticket_id` int(11) unsigned NOT NULL auto_increment,
-  `ticketID` int(11) unsigned NOT NULL default '0',
+  `number` varchar(20),
   `user_id` int(11) unsigned NOT NULL default '0',
   `user_email_id` int(11) unsigned NOT NULL default '0',
   `dept_id` int(10) unsigned NOT NULL default '0',
@@ -514,6 +514,7 @@ CREATE TABLE `%TABLE_PREFIX%ticket` (
   `topic_id` int(10) unsigned NOT NULL default '0',
   `staff_id` int(10) unsigned NOT NULL default '0',
   `team_id` int(10) unsigned NOT NULL default '0',
+  `email_id` int(11) unsigned NOT NULL default '0',
   `ip_address` varchar(64) NOT NULL default '',
   `status` enum('open','closed') NOT NULL default 'open',
   `source` enum('Web','Email','Phone','API','Other') NOT NULL default 'Other',
@@ -544,11 +545,10 @@ CREATE TABLE `%TABLE_PREFIX%ticket_attachment` (
   `ticket_id` int(11) unsigned NOT NULL default '0',
   `file_id` int(10) unsigned NOT NULL default '0',
   `ref_id` int(11) unsigned NOT NULL default '0',
-  `ref_type` enum('M','R','N') NOT NULL default 'M',
+  `inline` tinyint(1) NOT NULL default  '0',
   `created` datetime NOT NULL,
   PRIMARY KEY  (`attach_id`),
   KEY `ticket_id` (`ticket_id`),
-  KEY `ref_type` (`ref_type`),
   KEY `ref_id` (`ref_id`),
   KEY `file_id` (`file_id`)
 ) DEFAULT CHARSET=utf8;
@@ -567,10 +567,11 @@ CREATE TABLE `%TABLE_PREFIX%ticket_lock` (
 
 DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_email_info`;
 CREATE TABLE `%TABLE_PREFIX%ticket_email_info` (
-  `message_id` int(11) unsigned NOT NULL,
+  `thread_id` int(11) unsigned NOT NULL,
   `email_mid` varchar(255) NOT NULL,
   `headers` text,
-  KEY `message_id` (`email_mid`)
+  PRIMARY KEY (`thread_id`),
+  KEY `email_mid` (`email_mid`)
 ) DEFAULT CHARSET=utf8;
 
 DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_event`;
@@ -608,6 +609,7 @@ CREATE TABLE `%TABLE_PREFIX%ticket_thread` (
   `pid` int(11) unsigned NOT NULL default '0',
   `ticket_id` int(11) unsigned NOT NULL default '0',
   `staff_id` int(11) unsigned NOT NULL default '0',
+  `user_id` int(11) unsigned not null default 0,
   `thread_type` enum('M','R','N') NOT NULL,
   `poster` varchar(128) NOT NULL default '',
   `source` varchar(32) NOT NULL default '',
@@ -629,6 +631,7 @@ CREATE TABLE `%TABLE_PREFIX%ticket_collaborator` (
   `user_id` int(11) unsigned NOT NULL DEFAULT '0',
   -- M => (message) clients, N => (note) 3rd-Party, R => (reply) external authority
   `role` char(1) NOT NULL DEFAULT 'M',
+  `created` datetime NOT NULL,
   `updated` datetime NOT NULL,
   PRIMARY KEY (`id`),
   UNIQUE KEY `collab` (`ticket_id`,`user_id`)
diff --git a/tickets.php b/tickets.php
index b47ed0f1528335e9ee5eb871fcd8ca3c34d7b975..7c49a74d34611ff7f7f4b6fd0aeda80c31fcce0f 100644
--- a/tickets.php
+++ b/tickets.php
@@ -20,10 +20,10 @@ require_once(INCLUDE_DIR.'class.ticket.php');
 require_once(INCLUDE_DIR.'class.json.php');
 $ticket=null;
 if($_REQUEST['id']) {
-    if(!($ticket=Ticket::lookupByExtId($_REQUEST['id']))) {
+    if (!($ticket = Ticket::lookup($_REQUEST['id']))) {
         $errors['err']='Unknown or invalid ticket ID.';
-    }elseif(!$ticket->checkClientAccess($thisclient)) {
-        $errors['err']='Unknown or invalid ticket ID.'; //Using generic message on purpose!
+    } elseif(!$ticket->checkUserAccess($thisclient)) {
+        $errors['err']='Unknown or invalid ticket.'; //Using generic message on purpose!
         $ticket=null;
     }
 }
@@ -33,7 +33,7 @@ if($_POST && is_object($ticket) && $ticket->getId()):
     $errors=array();
     switch(strtolower($_POST['a'])){
     case 'reply':
-        if(!$ticket->checkClientAccess($thisclient)) //double check perm again!
+        if(!$ticket->checkUserAccess($thisclient)) //double check perm again!
             $errors['err']='Access Denied. Possibly invalid ticket ID';
 
         if(!$_POST['message'])
@@ -41,7 +41,10 @@ if($_POST && is_object($ticket) && $ticket->getId()):
 
         if(!$errors) {
             //Everything checked out...do the magic.
-            $vars = array('message'=>$_POST['message']);
+            $vars = array(
+                    'userId' => $thisclient->getId(),
+                    'poster' => (string) $thisclient->getName(),
+                    'message' => $_POST['message']);
             if($cfg->allowOnlineAttachments() && $_FILES['attachments'])
                 $vars['files'] = AttachmentFile::format($_FILES['attachments'], true);
             if (isset($_POST['draft_id']))
@@ -51,7 +54,7 @@ if($_POST && is_object($ticket) && $ticket->getId()):
                 $msg='Message Posted Successfully';
                 // Cleanup drafts for the ticket. If not closed, only clean
                 // for this staff. Else clean all drafts for the ticket.
-                Draft::deleteForNamespace('ticket.client.' . $ticket->getExtId());
+                Draft::deleteForNamespace('ticket.client.' . $ticket->getId());
             } else {
                 $errors['err']='Unable to post the message. Try again';
             }
@@ -66,7 +69,7 @@ if($_POST && is_object($ticket) && $ticket->getId()):
     $ticket->reload();
 endif;
 $nav->setActiveNav('tickets');
-if($ticket && $ticket->checkClientAccess($thisclient)) {
+if($ticket && $ticket->checkUserAccess($thisclient)) {
     $inc='view.inc.php';
 } elseif($cfg->showRelatedTickets() && $thisclient->getNumTickets()) {
     $inc='tickets.inc.php';
diff --git a/view.php b/view.php
index 10e5374fe71b8dcf3551c0ee922ac5b4800e961a..2398fff5628b9e0f380d38d53304352e650ecf66 100644
--- a/view.php
+++ b/view.php
@@ -3,7 +3,6 @@
     view.php
 
     Ticket View.
-    TODO: Support different views based on auth_token - e.g for BCC'ed users vs. Ticket owner.
 
     Peter Rotich <peter@osticket.com>
     Copyright (c)  2006-2010 osTicket
@@ -17,20 +16,16 @@
 **********************************************************************/
 require_once('client.inc.php');
 
-//If the user is NOT logged in - try auto-login (if params exists).
-if(!$thisclient || !$thisclient->isValid()) {
-    // * On login Client::login will redirect the user to tickets.php view.
-    // * See TODO above for planned multi-view.
-    $user = null;
-    if($_GET['t'] && $_GET['e'] && $_GET['a'])
-        $user = Client::login($_GET['t'], $_GET['e'], $_GET['a'], $errors);
 
-    //XXX: For now we're assuming the user is the ticket owner
-    // (multi-view based on auth token will come later).
-    if($user && $user->getTicketID()==trim($_GET['t']))
-        @header('Location: tickets.php?id='.$user->getTicketID());
+//If the user is NOT logged in - try auto-login (if params exists).
+if (!$thisclient || !$thisclient->isValid()) {
+    // Try autologin the user
+    // Authenticated user can be of type ticket owner or collaborator
+    $errors = array();
+    $user =  UserAuthenticationBackend::processSignOn($errors);
+    if ($user && $user->getTicketId())
+        @header('Location: tickets.php?id='.$user->getTicketId());
 }
-
 //Simply redirecting to tickets.php until multiview is implemented.
 require('tickets.php');
 ?>