diff --git a/assets/default/css/theme.css b/assets/default/css/theme.css
index abe2829c1eacbb7e06454311c51046987eef5109..4b880e8c3ceaf57e5adb2575e835a0a2a9ff2872 100644
--- a/assets/default/css/theme.css
+++ b/assets/default/css/theme.css
@@ -1067,6 +1067,10 @@ table.custom-data .headline {
 img.avatar {
     border-radius: inherit;
 }
+.avatar > img.avatar {
+    width: 100%;
+    height: 100%;
+}
 .thread-entry .header {
     padding: 8px 0.9em;
     border: 1px solid #ccc;
diff --git a/avatar.php b/avatar.php
new file mode 100644
index 0000000000000000000000000000000000000000..77c0a7fbeb566b8b3b91226a1d254a2d64b2c0d1
--- /dev/null
+++ b/avatar.php
@@ -0,0 +1,36 @@
+<?php
+/*********************************************************************
+    avatar.php
+
+    Simple download utility for internally-generated avatars
+
+    Peter Rotich <peter@osticket.com>
+    Jared Hancock <jared@osticket.com>
+    Copyright (c)  2006-2014 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+require('client.inc.php');
+
+if (!isset($_GET['uid']) || !isset($_GET['mode']))
+    Http::response(400, '`uid` and `mode` parameters are required');
+
+require_once INCLUDE_DIR . 'class.avatar.php';
+
+try {
+    $ra = new RandomAvatar($_GET['mode']);
+    $avatar = $ra->makeAvatar($_GET['uid']);
+
+    Http::response(200, false, 'image/png', false);
+    Http::cacheable($_GET['uid'], false, 86400);
+    imagepng($avatar, null, 1);
+    imagedestroy($avatar);
+    exit;
+}
+catch (InvalidArgumentException $ex) {
+    Http::response(422, 'No such avatar image set');
+}
diff --git a/images/avatar-sprite-ateam.png b/images/avatar-sprite-ateam.png
new file mode 100644
index 0000000000000000000000000000000000000000..0e16f01f7505840da760de8fcd46057f731a7aad
Binary files /dev/null and b/images/avatar-sprite-ateam.png differ
diff --git a/images/mystery-oscar.png b/images/mystery-oscar.png
new file mode 100644
index 0000000000000000000000000000000000000000..3aa3b83b69ec8d1b18f1b0f9676cc62d818e6795
Binary files /dev/null and b/images/mystery-oscar.png differ
diff --git a/include/ajax.staff.php b/include/ajax.staff.php
index e3fec3a653dea884cce34c13ee19bf84881c5a3b..5356e9ccf6c52efb3f550da04c6cb3fb3ced1d75 100644
--- a/include/ajax.staff.php
+++ b/include/ajax.staff.php
@@ -206,4 +206,27 @@ class StaffAjaxAPI extends AjaxController {
 
         include STAFFINC_DIR . 'templates/quick-add.tmpl.php';
     }
+
+    function setAvatar($id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+        if ($id != $thisstaff->getId() && !$thisstaff->isAdmin())
+            Http::response(403, 'Access denied');
+        if ($id == $thisstaff->getId())
+            $staff = $thisstaff;
+        else
+            $staff = Staff::lookup((int) $id);
+
+        if (!($avatar = $staff->getAvatar()))
+            Http::response(404, 'User does not have an avatar');
+
+        if ($code = $avatar->toggle())
+          return $this->encode(array(
+            'img' => (string) $avatar,
+            // XXX: This is very inflexible
+            'code' => $code,
+          ));
+    }
 }
diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php
index 6cdf27629d9a8def65e593f2768d47c41a80a1e0..2a644269fb759f74b6e3a0077fee9a3459882234 100644
--- a/include/ajax.tickets.php
+++ b/include/ajax.tickets.php
@@ -116,7 +116,7 @@ class TicketsAjaxAPI extends AjaxController {
             if ($lock->getStaffId() != $thisstaff->getId())
                 return $this->json_encode(array('id'=>0, 'retry'=>false,
                     'msg' => sprintf(__('Currently locked by %s'),
-                        $lock->getStaffName())
+                        $lock->getStaff()->getAvatarAndName())
                     ));
 
             //Ticket already locked by staff...try renewing it.
@@ -152,7 +152,7 @@ class TicketsAjaxAPI extends AjaxController {
             // user doesn't own the lock anymore??? sorry...try to next time.
             Http::response(403, $this->encode(array('id'=>0, 'retry'=>false,
                 'msg' => sprintf(__('Currently locked by %s'),
-                    $lock->getStaffName())
+                    $lock->getStaff->getAvatarAndName())
             ))); //Give up...
 
         // Ensure staff still has access
diff --git a/include/class.avatar.php b/include/class.avatar.php
new file mode 100644
index 0000000000000000000000000000000000000000..c328855a1abd3a6bb2b914bbaa587dec471507f0
--- /dev/null
+++ b/include/class.avatar.php
@@ -0,0 +1,238 @@
+<?php
+/*********************************************************************
+    class.avatar.php
+
+    Avatar sources for users and agents
+
+    Jared Hancock <jared@osticket.com>
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  2006-2015 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+
+abstract class Avatar {
+    var $user;
+
+    function __construct($user) {
+        $this->user = $user;
+    }
+
+    abstract function getUrl($size);
+
+    function getImageTag($size=null) {
+        return '<img class="avatar" alt="'.__('Avatar').'" src="'.$this->getUrl($size).'" />';
+    }
+
+    function __toString() {
+        return $this->getImageTag();
+    }
+
+    function isChangeable() {
+        return false;
+    }
+    function toggle() {}
+}
+
+abstract class AvatarSource {
+    static $id;
+    static $name;
+    var $mode;
+
+    function __construct($mode=null) {
+        if (isset($mode))
+            $this->mode = $mode;
+    }
+
+    function getName() {
+        return __(static::$name);
+    }
+
+    abstract function getAvatar($user);
+
+    static $registry = array();
+    static function register($class) {
+        if (!class_exists($class))
+            throw new Exception($class.': Does not exist');
+        if (!isset($class::$id))
+            throw new Exception($class.': AvatarClass must specify $id');
+        static::$registry[$class::$id] = $class;
+    }
+
+    static function lookup($id, $mode=null) {
+        $class = static::$registry[$id];
+        if (!isset($class))
+            ; // TODO: Return built-in avatar source
+        if (is_string($class))
+            $class = static::$registry[$id] = new $class($mode);
+        return $class;
+    }
+
+    static function allSources() {
+        return static::$registry;
+    }
+
+    static function getModes() {
+        return null;
+    }
+}
+
+class LocalAvatarSource
+extends AvatarSource {
+    static $id = 'local';
+    static $name = /* @trans */ 'Built-In';
+    var $mode = 'ateam';
+
+    static function getModes() {
+        return array(
+            'ateam' => __("Oscar's A-Team"),
+        );
+    }
+
+    function getAvatar($user) {
+        return new LocalAvatar($user, $this->mode);
+    }
+}
+AvatarSource::register('LocalAvatarSource');
+
+class LocalAvatar
+extends Avatar {
+    var $mode;
+    var $code;
+
+    function __construct($user, $mode) {
+        parent::__construct($user);
+        $this->mode = $mode;
+    }
+
+    function getUrl($size) {
+        if (false && ($file = $this->user->getAvatarFile()))
+            return $file->getDownloadUrl();
+
+        $code = $this->code;
+        if (!$code && method_exists($this->user, 'getExtraAttr'))
+            $code = $this->user->getExtraAttr('avatar');
+
+        if ($code)
+            $uid = md5($code);
+        else
+            // Generate a random string of 0-6 chars for the avatar signature
+            $uid = md5(strtolower($this->user->getEmail()));
+
+        return ROOT_PATH . 'avatar.php?'.Http::build_query(array('uid'=>$uid,
+            'mode' => $this->mode));
+    }
+
+    function toggle() {
+        $this->code = Misc::randCode(21);
+        return $this->code;
+    }
+
+    function isChangeable() {
+        return true;
+    }
+}
+
+class RandomAvatar {
+    var $mode;
+
+    static $sprites = array(
+        'ateam' => array(
+            'file' => 'images/avatar-sprite-ateam.png',
+            'grid' => 96,
+        ),
+    );
+
+    function __construct($mode) {
+        $this->mode = $mode;
+    }
+
+    function makeAvatar($uid) {
+        $sprite = self::$sprites[$this->mode];
+        if (!$sprite || !is_readable(ROOT_DIR . $sprite['file']) || !extension_loaded('gd'))
+            Http::redirect(ROOT_PATH.'images/mystery-oscar.png');
+
+        $source =  imagecreatefrompng(ROOT_DIR . $sprite['file']);
+        $grid = $sprite['grid'];
+        $avatar = imagecreatetruecolor($grid, $grid);
+        $width = imagesx($source) / $grid;
+        $height = imagesy($source) / $grid;
+
+        // Start with a white matte
+        $white = imagecolorallocate($avatar, 255, 255, 255);
+        imagefill($avatar, 0, 0, $white);
+
+        for ($i=0, $k=$height; $i<$k; $i++) {
+            $idx = hexdec($uid[$i]) % $width;
+            imagecopy($avatar, $source, 0, 0, $idx*$grid, $i*$grid, $grid, $grid);
+        }
+
+        return $avatar;
+    }
+}
+
+class AvatarsByGravatar
+extends AvatarSource {
+    static $name = 'Gravatar';
+    static $id = 'gravatar';
+    var $mode;
+
+    function __construct($mode=null) {
+        $this->mode = $mode ?: 'retro';
+    }
+
+    static function getModes() {
+        return array(
+            'mm' => __('Mystery Man'),
+            'identicon' => 'Identicon',
+            'monsterid' => 'Monster',
+            'wavatar' => 'Wavatar',
+            'retro' => 'Retro',
+        );
+    }
+
+    function getAvatar($user) {
+        return new Gravatar($user, $this->mode);
+    }
+}
+AvatarSource::register('AvatarsByGravatar');
+
+class Gravatar
+extends Avatar {
+    var $email;
+    var $d;
+    var $size;
+
+    function __construct($user, $imageset) {
+        $this->email = $user->getEmail();
+        $this->d = $imageset;
+    }
+
+    function setSize($size) {
+        $this->size = $size;
+    }
+
+    /**
+     * Get either a Gravatar URL or complete image tag for a specified email address.
+     *
+     * @param string $email The email address
+     * @param string $s Size in pixels, defaults to 80px [ 1 - 2048 ]
+     * @param string $d Default imageset to use [ 404 | mm | identicon | monsterid | wavatar ]
+     * @param string $r Maximum rating (inclusive) [ g | pg | r | x ]
+     * @param boole $img True to return a complete IMG tag False for just the URL
+     * @param array $atts Optional, additional key/value attributes to include in the IMG tag
+     * @return String containing either just a URL or a complete image tag
+     * @source http://gravatar.com/site/implement/images/php/
+     */
+    function getUrl($size=null) {
+        $size = $this->size ?: 80;
+        $url = '//www.gravatar.com/avatar/';
+        $url .= md5( strtolower( $this->email ) );
+        $url .= "?s=$size&d={$this->d}";
+        return $url;
+    }
+}
diff --git a/include/class.config.php b/include/class.config.php
index 1c2de449977114090433b6733c3c1893b7c8c62f..5a169199d1cca71c2f269c59620ec619ffcfee22 100644
--- a/include/class.config.php
+++ b/include/class.config.php
@@ -173,6 +173,8 @@ class OsticketConfig extends Config {
         'help_topic_sort_mode' => 'a',
         'client_verify_email' => 1,
         'verify_email_addrs' => 1,
+        'client_avatar' => 'gravatar.mm',
+        'agent_avatar' => 'gravatar.mm',
     );
 
     function OsticketConfig($section=null) {
@@ -405,6 +407,18 @@ class OsticketConfig extends Config {
         return $this->get('staff_max_logins');
     }
 
+    function getStaffAvatarSource() {
+        require_once INCLUDE_DIR . 'class.avatar.php';
+        list($source, $mode) = explode('.', $this->get('agent_avatar'), 2);
+        return AvatarSource::lookup($source, $mode);
+    }
+
+    function getClientAvatarSource() {
+        require_once INCLUDE_DIR . 'class.avatar.php';
+        list($source, $mode) = explode('.', $this->get('client_avatar'), 2);
+        return AvatarSource::lookup($source, $mode);
+    }
+
     function getLockTime() {
         return $this->get('autolock_minutes');
     }
@@ -1099,6 +1113,11 @@ class OsticketConfig extends Config {
         $f['pw_reset_window']=array('type'=>'int', 'required'=>1, 'min'=>1,
             'error'=>__('Valid password reset window required'));
 
+        require_once INCLUDE_DIR.'class.avatar.php';
+        list($avatar_source) = explode('.', $vars['agent_avatar']);
+        if (!AvatarSource::lookup($avatar_source))
+            $errors['agent_avatar'] = __('Select a value from the list');
+
         if(!Validator::process($f, $vars, $errors) || $errors)
             return false;
 
@@ -1111,7 +1130,7 @@ class OsticketConfig extends Config {
             'allow_pw_reset'=>isset($vars['allow_pw_reset'])?1:0,
             'pw_reset_window'=>$vars['pw_reset_window'],
             'agent_name_format'=>$vars['agent_name_format'],
-
+            'agent_avatar'=>$vars['agent_avatar'],
         ));
     }
 
@@ -1119,6 +1138,11 @@ class OsticketConfig extends Config {
         $f=array();
         $f['client_session_timeout']=array('type'=>'int',   'required'=>1, 'error'=>'Enter idle time in minutes');
 
+        require_once INCLUDE_DIR.'class.avatar.php';
+        list($avatar_source) = explode('.', $vars['client_avatar']);
+        if (!AvatarSource::lookup($avatar_source))
+            $errors['client_avatar'] = __('Select a value from the list');
+
         if(!Validator::process($f, $vars, $errors) || $errors)
             return false;
 
@@ -1130,7 +1154,7 @@ class OsticketConfig extends Config {
             'client_registration'=>$vars['client_registration'],
             'client_verify_email'=>isset($vars['client_verify_email'])?1:0,
             'client_name_format'=>$vars['client_name_format'],
-
+            'client_avatar'=>$vars['client_avatar'],
         ));
     }
 
diff --git a/include/class.http.php b/include/class.http.php
index 5e14ee932bb2acbd013b348083a4176a13fc42cb..2fd09a8d70f7aa37041bd8b57597216024cc5cc7 100644
--- a/include/class.http.php
+++ b/include/class.http.php
@@ -37,10 +37,15 @@ class Http {
         header('HTTP/1.1 '.Http::header_code_verbose($code));
 		header('Status: '.Http::header_code_verbose($code)."\r\n");
 		header("Connection: Close\r\n");
-		header("Content-Type: $contentType; charset=$charset\r\n");
-        header('Content-Length: '.strlen($content)."\r\n\r\n");
-       	print $content;
-        exit;
+        $ct = "Content-Type: $contentType";
+        if ($charset)
+            $ct .= "; charset=$charset";
+        header($ct);
+        if ($content) {
+            header('Content-Length: '.strlen($content)."\r\n\r\n");
+            print $content;
+            exit;
+        }
     }
 
     function redirect($url,$delay=0,$msg='') {
diff --git a/include/class.lock.php b/include/class.lock.php
index acbe1365e2c3f8fa2dbb64c4f4f39b227c418890..1a758a46877260111f21b4a06c5a9d73a2bebb44 100644
--- a/include/class.lock.php
+++ b/include/class.lock.php
@@ -50,6 +50,10 @@ class Lock extends VerySimpleModel {
         return $this->staff->getName();
     }
 
+    function getStaff() {
+        return $this->staff;
+    }
+
     function getCreateTime() {
         return $this->created;
     }
diff --git a/include/class.staff.php b/include/class.staff.php
index ed93f6441f7115cda215f6555a6544e39c989c48..c1ed5a32fdd50d538896b9b03401e5c3d09e2380 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -240,29 +240,14 @@ implements AuthenticatedUser, EmailContact, TemplateVariable {
     function getEmail() {
         return $this->email;
     }
-    /**
-     * Get either a Gravatar URL or complete image tag for a specified email address.
-     *
-     * @param string $email The email address
-     * @param string $s Size in pixels, defaults to 80px [ 1 - 2048 ]
-     * @param string $d Default imageset to use [ 404 | mm | identicon | monsterid | wavatar ]
-     * @param string $r Maximum rating (inclusive) [ g | pg | r | x ]
-     * @param boole $img True to return a complete IMG tag False for just the URL
-     * @param array $atts Optional, additional key/value attributes to include in the IMG tag
-     * @return String containing either just a URL or a complete image tag
-     * @source http://gravatar.com/site/implement/images/php/
-     */
-    function get_gravatar($s = 80, $img = false, $atts = array(), $d = 'retro', $r = 'g' ) {
-        $url = '//www.gravatar.com/avatar/';
-        $url .= md5( strtolower( $this->getEmail() ) );
-        $url .= "?s=$s&d=$d&r=$r";
-        if ( $img ) {
-            $url = '<img src="' . $url . '"';
-            foreach ( $atts as $key => $val )
-                $url .= ' ' . $key . '="' . $val . '"';
-            $url .= ' />';
-        }
-        return $url;
+
+    function getAvatar($size=null) {
+        global $cfg;
+        $source = $cfg->getStaffAvatarSource();
+        $avatar = $source->getAvatar($this);
+        if (isset($size))
+            $avatar->setSize($size);
+        return $avatar;
     }
 
     function getUserName() {
@@ -277,6 +262,10 @@ implements AuthenticatedUser, EmailContact, TemplateVariable {
         return new AgentsName(array('first' => $this->ht['firstname'], 'last' => $this->ht['lastname']));
     }
 
+    function getAvatarAndName() {
+        return $this->getAvatar().Format::htmlchars((string) $this->getName());
+    }
+
     function getFirstName() {
         return $this->firstname;
     }
@@ -633,6 +622,9 @@ implements AuthenticatedUser, EmailContact, TemplateVariable {
         $this->lang = $vars['lang'];
         $this->onvacation = isset($vars['onvacation'])?1:0;
 
+        if (isset($vars['avatar_code']))
+          $this->setExtraAttr('avatar', $vars['avatar_code']);
+
         if ($errors)
             return false;
 
diff --git a/include/class.thread.php b/include/class.thread.php
index 81fb47aed86b942e0ffe61a3de87b8a6bc5a51ad..c3649b280f4ed3c4b1677704a97baa479b434e1e 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -1555,11 +1555,11 @@ class ThreadEvent extends VerySimpleModel {
 
     var $_data;
 
-    function getAvatar($size=16) {
+    function getAvatar($size=null) {
         if ($this->uid && $this->uid_type == 'S')
-            return $this->agent->get_gravatar($size);
+            return $this->agent->getAvatar($size);
         if ($this->uid && $this->uid_type == 'U')
-            return $this->user->get_gravatar($size);
+            return $this->user->getAvatar($size);
     }
 
     function getUserName() {
@@ -1601,9 +1601,9 @@ class ThreadEvent extends VerySimpleModel {
                 case 'assignees':
                     $assignees = array();
                     if ($S = $self->staff) {
-                        $url = $S->get_gravatar(16);
+                        $avatar = $S->getAvatar();
                         $assignees[] =
-                            "<img class=\"avatar\" src=\"{$url}\"> ".$S->getName();
+                            $avatar.$S->getName();
                     }
                     if ($T = $self->team) {
                         $assignees[] = $T->getLocalName();
@@ -1611,8 +1611,8 @@ class ThreadEvent extends VerySimpleModel {
                     return implode('/', $assignees);
                 case 'somebody':
                     $name = $self->getUserName();
-                    if ($url = $self->getAvatar())
-                        $name = "<img class=\"avatar\" src=\"{$url}\"> ".$name;
+                    if ($avatar = $self->getAvatar())
+                        $name = $avatar.$name;
                     return $name;
                 case 'timestamp':
                     return sprintf('<time class="relative" datetime="%s" title="%s">%s</time>',
@@ -1622,8 +1622,8 @@ class ThreadEvent extends VerySimpleModel {
                     );
                 case 'agent':
                     $name = $self->agent->getName();
-                    if ($url = $self->getAvatar())
-                        $name = "<img class=\"avatar\" src=\"{$url}\"> ".$name;
+                    if ($avatar = $self->getAvatar())
+                        $name = $avatar.$name;
                     return $name;
                 case 'dept':
                     if ($dept = $self->getDept())
diff --git a/include/class.user.php b/include/class.user.php
index 16279f76eb834247940fc8f96f2e7807af497f64..79370c0fcb4cced661dbe4c9d149b98790bbc200 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -267,29 +267,11 @@ implements TemplateVariable {
     function getEmail() {
         return new EmailAddress($this->default_email->address);
     }
-    /**
-     * Get either a Gravatar URL or complete image tag for a specified email address.
-     *
-     * @param string $email The email address
-     * @param string $s Size in pixels, defaults to 80px [ 1 - 2048 ]
-     * @param string $d Default imageset to use [ 404 | mm | identicon | monsterid | wavatar ]
-     * @param string $r Maximum rating (inclusive) [ g | pg | r | x ]
-     * @param boole $img True to return a complete IMG tag False for just the URL
-     * @param array $atts Optional, additional key/value attributes to include in the IMG tag
-     * @return String containing either just a URL or a complete image tag
-     * @source http://gravatar.com/site/implement/images/php/
-     */
-    function get_gravatar($s = 80, $img = false, $atts = array(), $d = 'retro', $r = 'g' ) {
-        $url = '//www.gravatar.com/avatar/';
-        $url .= md5( strtolower( $this->default_email->address ) );
-        $url .= "?s=$s&d=$d&r=$r";
-        if ( $img ) {
-            $url = '<img src="' . $url . '"';
-            foreach ( $atts as $key => $val )
-                $url .= ' ' . $key . '="' . $val . '"';
-            $url .= ' />';
-        }
-        return $url;
+
+    function getAvatar() {
+        global $cfg;
+        $source = $cfg->getClientAvatarSource();
+        return $source->getAvatar($this);
     }
 
     function getFullName() {
diff --git a/include/client/templates/thread-entry.tmpl.php b/include/client/templates/thread-entry.tmpl.php
index fbad6983cbe25791ec07af612fbd243e7280b4b1..9e42b053a841fbd971eaef0165ae29cc79b4c283 100644
--- a/include/client/templates/thread-entry.tmpl.php
+++ b/include/client/templates/thread-entry.tmpl.php
@@ -3,8 +3,8 @@ $entryTypes = array('M'=>'message', 'R'=>'response', 'N'=>'note');
 $user = $entry->getUser() ?: $entry->getStaff();
 $name = $user ? $user->getName() : $entry->poster;
 $avatar = '';
-if ($user && ($url = $user->get_gravatar(48)))
-    $avatar = "<img class=\"avatar\" src=\"{$url}\"> ";
+if ($user)
+    $avatar = $user->getAvatar();
 ?>
 
 <div class="thread-entry <?php echo $entryTypes[$entry->type]; ?> <?php if ($avatar) echo 'avatar'; ?>">
diff --git a/include/staff/profile.inc.php b/include/staff/profile.inc.php
index 280c63f2c22f5d69afaa1f6486945241553ea283..85a7a1338fe16f097c3dee80f88fe85886982819 100644
--- a/include/staff/profile.inc.php
+++ b/include/staff/profile.inc.php
@@ -17,6 +17,37 @@ if(!defined('OSTSTAFFINC') || !$staff || !$thisstaff) die('Access Denied');
   <div class="tab_content" id="account">
     <table class="table two-column" width="940" border="0" cellspacing="0" cellpadding="2">
       <tbody>
+        <tr><td colspan="2"><div>
+        <div class="avatar pull-left" style="margin: 10px 15px; width: 100px; height: 100px;">
+<?php       $avatar = $staff->getAvatar();
+            echo $avatar;
+if ($avatar->isChangeable()) { ?>
+          <div style="text-align: center">
+            <a class="button no-pjax"
+                href="#ajax.php/staff/<?php echo $staff->getId(); ?>/avatar/change"
+                onclick="javascript:
+    event.preventDefault();
+    var $a = $(this),
+        form = $a.closest('form');
+    $.ajax({
+      url: $a.attr('href').substr(1),
+      dataType: 'json',
+      success: function(json) {
+        if (!json || !json.code)
+          return;
+        var code = form.find('[name=avatar_code]');
+        if (!code.length)
+          code = form.append($('<input>').attr({type: 'hidden', name: 'avatar_code'}));
+        code.val(json.code).trigger('change');
+        $a.closest('.avatar').find('img').replaceWith($(json.img));
+      }
+    });
+    return false;"><i class="icon-retweet"></i></a>
+          </div>
+<?php
+} ?>
+        </div>
+        <table class="table two-column" border="0" cellspacing="2" cellpadding="2" style="width:760px">
         <tr>
           <td class="required"><?php echo __('Name'); ?>:</td>
           <td>
@@ -59,6 +90,7 @@ if(!defined('OSTSTAFFINC') || !$staff || !$thisstaff) die('Access Denied');
             <div class="error"><?php echo $errors['mobile']; ?></div>
           </td>
         </tr>
+        </table></div></td></tr>
       </tbody>
       <!-- ================================================ -->
       <tbody>
diff --git a/include/staff/settings-agents.inc.php b/include/staff/settings-agents.inc.php
index bd51beb31f1f1c3e776c1ca8933c09cc216571de..5a7e83230a6a6aaef958b77f6b7780684b477c3c 100644
--- a/include/staff/settings-agents.inc.php
+++ b/include/staff/settings-agents.inc.php
@@ -35,6 +35,31 @@ if (!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config
                 <i class="help-tip icon-question-sign" href="#agent_name_format"></i>
             </td>
         </tr>
+        <tr>
+            <td width="180"><?php echo __('Avatar Source'); ?>:</td>
+            <td>
+                <select name="agent_avatar">
+<?php           require_once INCLUDE_DIR . 'class.avatar.php';
+                foreach (AvatarSource::allSources() as $id=>$class) {
+                    $modes = $class::getModes();
+                    if ($modes) {
+                        echo "<optgroup label=\"{$class::getName()}\">";
+                        foreach ($modes as $mid=>$mname) {
+                            $oid = "$id.$mid";
+                            $selected = ($config['agent_avatar'] == $oid) ? 'selected="selected"' : '';
+                            echo "<option {$selected} value=\"{$oid}\">{$class::getName()} / {$mname}</option>";
+                        }
+                        echo "</optgroup>";
+                    }
+                    else {
+                        $selected = ($config['agent_avatar'] == $id) ? 'selected="selected"' : '';
+                        echo "<option {$selected} value=\"{$id}\">{$class::getName()}</option>";
+                    }
+                } ?>
+                </select>
+                <div class="error"><?php echo Format::htmlchars($errors['agent_avatar']); ?></div>
+            </td>
+        </tr>
         <tr>
             <th colspan="2">
                 <em><b><?php echo __('Authentication Settings'); ?></b></em>
diff --git a/include/staff/settings-users.inc.php b/include/staff/settings-users.inc.php
index 62f4161df6ef88142aa8605c47af8261b383ee10..61840a4da806934960218a496709b96e6205af4c 100644
--- a/include/staff/settings-users.inc.php
+++ b/include/staff/settings-users.inc.php
@@ -36,6 +36,31 @@ if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config)
                 <i class="help-tip icon-question-sign" href="#client_name_format"></i>
             </td>
         </tr>
+        <tr>
+            <td width="180"><?php echo __('Avatar Source'); ?>:</td>
+            <td>
+                <select name="client_avatar">
+<?php           require_once INCLUDE_DIR . 'class.avatar.php';
+                foreach (AvatarSource::allSources() as $id=>$class) {
+                    $modes = $class::getModes();
+                    if ($modes) {
+                        echo "<optgroup label=\"{$class::getName()}\">";
+                        foreach ($modes as $mid=>$mname) {
+                            $oid = "$id.$mid";
+                            $selected = ($config['client_avatar'] == $oid) ? 'selected="selected"' : '';
+                            echo "<option {$selected} value=\"{$oid}\">{$mname}</option>";
+                        }
+                        echo "</optgroup>";
+                    }
+                    else {
+                        $selected = ($config['client_avatar'] == $id) ? 'selected="selected"' : '';
+                        echo "<option {$selected} value=\"{$id}\">{$class::getName()}</option>";
+                    }
+                } ?>
+                </select>
+                <div class="error"><?php echo Format::htmlchars($errors['client_avatar']); ?></div>
+            </td>
+        </tr>
         <tr>
             <th colspan="2">
                 <em><b><?php echo __('Authentication Settings'); ?></b></em>
diff --git a/include/staff/staff.inc.php b/include/staff/staff.inc.php
index 9a5ad3d296bd20e7c7dd67da71d0958d534679d3..020e281622c359deb25df76694bc9440ea3a1bfa 100644
--- a/include/staff/staff.inc.php
+++ b/include/staff/staff.inc.php
@@ -57,6 +57,11 @@ else {
   <div class="tab_content" id="account">
     <table class="table two-column" width="940" border="0" cellspacing="0" cellpadding="2">
       <tbody>
+        <tr><td colspan="2"><div>
+        <div class="avatar pull-left" style="width: 100px; margin: 10px;">
+            <?php echo $staff->getAvatar(); ?>
+        </div>
+        <table class="table two-column" border="0" cellspacing="2" cellpadding="2" style="width: 760px">
         <tr>
           <td class="required"><?php echo __('Name'); ?>:</td>
           <td>
@@ -99,6 +104,7 @@ else {
             <div class="error"><?php echo $errors['mobile']; ?></div>
           </td>
         </tr>
+        </table></div></td></tr>
       </tbody>
       <!-- ================================================ -->
       <tbody>
diff --git a/include/staff/templates/thread-entry.tmpl.php b/include/staff/templates/thread-entry.tmpl.php
index 62cd3e3362509d15f13faee81508f9b51060059d..8df932b07640c958f3762217ba50b71322904413 100644
--- a/include/staff/templates/thread-entry.tmpl.php
+++ b/include/staff/templates/thread-entry.tmpl.php
@@ -3,8 +3,8 @@ $entryTypes = array('M'=>'message', 'R'=>'response', 'N'=>'note');
 $user = $entry->getUser() ?: $entry->getStaff();
 $name = $user ? $user->getName() : $entry->poster;
 $avatar = '';
-if ($user && ($url = $user->get_gravatar(48)))
-    $avatar = "<img class=\"avatar\" src=\"{$url}\"> ";
+if ($user)
+    $avatar = $user->getAvatar();
 
 ?>
 <div class="thread-entry <?php echo $entryTypes[$entry->type]; ?> <?php if ($avatar) echo 'avatar'; ?>">
diff --git a/include/staff/templates/user.tmpl.php b/include/staff/templates/user.tmpl.php
index 9400e4f93de8ed5ea0f96d5ffca19ba6a170f0a4..b1debfd90653b08fb1669c8a410c09460af73630 100644
--- a/include/staff/templates/user.tmpl.php
+++ b/include/staff/templates/user.tmpl.php
@@ -16,7 +16,9 @@ if ($info['error']) {
     echo sprintf('<p id="msg_notice">%s</p>', $info['msg']);
 } ?>
 <div id="user-profile" style="display:<?php echo $forms ? 'none' : 'block'; ?>;margin:5px;">
-    <i class="icon-user icon-4x pull-left icon-border"></i>
+    <div class="avatar pull-left" style="margin: 0 10px;">
+    <?php echo $user->getAvatar(); ?>
+    </div>
     <?php
     if ($ticket) { ?>
     <a class="action-button pull-right change-user" style="overflow:inherit"
diff --git a/include/staff/user-view.inc.php b/include/staff/user-view.inc.php
index 7c894ca6dc2fc6ad74657bc3c99f3c5c4cb6ae9b..4d02b3d66d166a9be7965ace9e6cdeab3ff9a72c 100644
--- a/include/staff/user-view.inc.php
+++ b/include/staff/user-view.inc.php
@@ -77,7 +77,10 @@ $org = $user->getOrganization();
         </td>
     </tr>
 </table>
-<table class="ticket_info" cellspacing="0" cellpadding="0" width="940" border="0">
+<div class="avatar pull-left" style="margin: 10px; width: 80px;">
+    <?php echo $user->getAvatar(); ?>
+</div>
+<table class="ticket_info" cellspacing="0" cellpadding="0" width="830" border="0">
     <tr>
         <td width="50%">
             <table border="0" cellspacing="" cellpadding="4" width="100%">
diff --git a/scp/ajax.php b/scp/ajax.php
index 5df6b35b966831f028f5e6c2e59c6653e91a55b1..c20995c61baabea70fe934dd849ee0f96d0b40b5 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -245,7 +245,8 @@ $dispatcher = patterns('',
         url('^/(?P<id>\d+)/change-password$', 'changePassword'),
         url_get('^/(?P<id>\d+)/perms', 'getAgentPerms'),
         url('^/reset-permissions', 'resetPermissions'),
-        url('^/change-department', 'changeDepartment')
+        url('^/change-department', 'changeDepartment'),
+        url('^/(?P<id>\d+)/avatar/change', 'setAvatar')
     ))
 );
 
diff --git a/scp/css/scp.css b/scp/css/scp.css
index 37b30ef848068e9dbc9b087bd3204de4ea960b27..690ebe063ee4eac9b98c19e32dbc825e258cfe95 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -875,7 +875,9 @@ h2 .reload {
     display:inline-block;
     width:48px;
     height:auto;
-    border-radius: 5px;
+}
+.avatar {
+    border-radius: 12%;
 }
 .thread-entry.message > .avatar {
     margin-left: initial;
@@ -884,6 +886,10 @@ h2 .reload {
 img.avatar {
     border-radius: inherit;
 }
+.avatar > img.avatar {
+    width: 100%;
+    height: 100%;
+}
 .thread-entry .header {
     padding: 8px 0.9em;
     border: 1px solid #ccc;
@@ -2523,7 +2529,7 @@ td.indented {
   top: auto;
   box-shadow: 0 -3px 10px rgba(0,0,0,0.2);
 }
-.message.bar .avatar {
+.message.bar .avatar[class*=" oscar-"] {
   display: inline-block;
   width: 36px;
   height: 36px;
@@ -2559,6 +2565,13 @@ td.indented {
   border-bottom: none;
   border-top: 3px solid red;
 }
+.message.bar .title .avatar {
+    width: auto;
+    max-height: 20px;
+    border-radius: 3px;
+    margin: -4px 0.3em 0;
+    vertical-align: middle;
+}
 
 #thread-items::before {
   border-left: 2px dotted #ddd;
@@ -2616,7 +2629,7 @@ td.indented {
     vertical-align: middle;
     border-radius: 3px;
     width: auto;
-    max-height: 24px;
+    max-height: 20px;
     margin: -3px 3px 0;
 }
 .thread-event .description {
diff --git a/scp/js/scp.js b/scp/js/scp.js
index 0568c60641f4faae00b866043f304aee5e6aeb59..32c18ca7c19fc887daaa3cfdd362f1f73c931660 100644
--- a/scp/js/scp.js
+++ b/scp/js/scp.js
@@ -146,11 +146,11 @@ var scp_prep = function() {
         }
     };
 
-    $("form#save :input[name]").change(function() {
+    $("form#save").on('change', ':input[name]', function() {
         if (!$(this).is('.nowarn')) warnOnLeave($(this));
     });
 
-    $("form#save :input[type=reset]").click(function() {
+    $("form#save").on('change', ':input[type=reset]', function() {
         var fObj = $(this).closest('form');
         if(fObj.data('changed')){
             $('input[type=submit]', fObj).removeClass('save pending');
@@ -772,16 +772,17 @@ $.uid = 1;
       buttonText: __('OK'),
       classes: '',
       dismissible: true,
+      html: false,
       onok: null,
-      position: 'top'
+      position: 'top',
     };
 
     this.show = function(title, message, options) {
       this.hide();
       options = $.extend({}, this.defaults, options);
       var bar = this.bar = $(options.bar).addClass(options.classes)
-        .append($('<div class="title"></div>').text(title))
-        .append($('<div class="body"></div>').text(message))
+        .append($('<div class="title"></div>').html(title))
+        .append($('<div class="body"></div>').html(message))
         .addClass(options.position);
       if (options.avatar)
         bar.prepend($('<div class="avatar pull-left" title="Oscar"></div>')