diff --git a/include/ajax.content.php b/include/ajax.content.php
index 9ea0d7d1431bdf58aac6b2ffada47acb7e4585bd..dc1bc9ab1f9678913ee1581e3ea672d879d53671 100644
--- a/include/ajax.content.php
+++ b/include/ajax.content.php
@@ -119,6 +119,11 @@ class ContentAjaxAPI extends AjaxController {
         switch ($type) {
         case 'none':
             break;
+        case 'agent':
+            if (!($staff = Staff::lookup($id)))
+                Http::response(404, 'No such staff member');
+            echo Format::viewableImages($staff->getSignature());
+            break;
         case 'mine':
             echo Format::viewableImages($thisstaff->getSignature());
             break;
diff --git a/include/class.mailer.php b/include/class.mailer.php
index c398df9f5ccf26f8bca4a8d16a21009fbc52cdf6..0abb652469fa8d6491dcd5efa1f37ac5bc139664 100644
--- a/include/class.mailer.php
+++ b/include/class.mailer.php
@@ -361,12 +361,12 @@ class Mailer {
                     'References' => $options['thread']->getEmailReferences()
                 );
             }
-            elseif ($parent = $options['thread']->getParent()) {
+            elseif ($original = $options['thread']->findOriginalEmailMessage()) {
                 // Use the parent item as the email information source. This
                 // will apply for staff replies
                 $headers += array(
-                    'In-Reply-To' => $parent->getEmailMessageId(),
-                    'References' => $parent->getEmailReferences(),
+                    'In-Reply-To' => $original->getEmailMessageId(),
+                    'References' => $original->getEmailReferences(),
                 );
             }
 
diff --git a/include/class.orm.php b/include/class.orm.php
index 58aeaede544bc124cc7e1cf6a905d6baad12871c..38e1d931d49db8057591de5d9d250e911519d3f0 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -71,34 +71,39 @@ class ModelMeta implements ArrayAccess {
     }
 
     function processJoin(&$j) {
+        $constraint = array();
         if (isset($j['reverse'])) {
             list($fmodel, $key) = explode('.', $j['reverse']);
             $info = $fmodel::$meta['joins'][$key];
-            $constraint = array();
             if (!is_array($info['constraint']))
                 throw new OrmConfigurationException(sprintf(__(
                     // `reverse` here is the reverse of an ORM relationship
                     '%s: Reverse does not specify any constraints'),
                     $j['reverse']));
             foreach ($info['constraint'] as $foreign => $local) {
-                list(,$field) = explode('.', $local);
-                $constraint[$field ?: $local] = "$fmodel.$foreign";
+                list($L,$field) = is_array($local) ? $local : explode('.', $local);
+                $constraint[$field ?: $L] = array($fmodel, $foreign);
             }
-            $j['constraint'] = $constraint;
             if (!isset($j['list']))
                 $j['list'] = true;
             if (!isset($j['null']))
                 // By default, reverse releationships can be empty lists
                 $j['null'] = true;
         }
-        // XXX: Make this better (ie. composite keys)
-        foreach ($j['constraint'] as $local => $foreign) {
-            list($class, $field) = explode('.', $foreign);
+        else {
+            foreach ($j['constraint'] as $local => $foreign) {
+                list($class, $field) = $constraint[$local]
+                    = explode('.', $foreign);
+            }
+        }
+        foreach ($constraint as $local => $foreign) {
+            list($class, $field) = $foreign;
             if ($local[0] == "'" || $field[0] == "'" || !class_exists($class))
                 continue;
-            $j['fkey'] = array($class, $field);
+            $j['fkey'] = $foreign;
             $j['local'] = $local;
         }
+        $j['constraint'] = $constraint;
     }
 
     function offsetGet($field) {
@@ -178,13 +183,13 @@ class VerySimpleModel {
             elseif (isset($j['fkey'])) {
                 $criteria = array();
                 foreach ($j['constraint'] as $local => $foreign) {
-                    list($klas,$F) = explode('.', $foreign);
+                    list($klas,$F) = $foreign;
                     if (class_exists($klas))
                         $class = $klas;
                     if ($local[0] == "'") {
                         $criteria[$F] = trim($local,"'");
                     }
-                    elseif ($foreign[0] == "'") {
+                    elseif ($F[0] == "'") {
                         // Does not affect the local model
                         continue;
                     }
@@ -535,14 +540,20 @@ class SqlFunction {
         $this->args = array_slice(func_get_args(), 1);
     }
 
+    function input($what, $compiler, $model) {
+        if ($what instanceof SqlFunction)
+            $A = $what->toSql($compiler, $model);
+        elseif ($what instanceof Q)
+            $A = $compiler->compileQ($what, $model);
+        else
+            $A = $compiler->input($what);
+        return $A;
+    }
+
     function toSql($compiler, $model=false, $alias=false) {
         $args = array();
         foreach ($this->args as $A) {
-            if ($A instanceof SqlFunction)
-                $A = $A->toSql($compiler, $model);
-            else
-                $A = $compiler->input($A);
-            $args[] = $A;
+            $args[] = $this->input($A, $compiler, $model);
         }
         return sprintf('%s(%s)%s', $this->func, implode(',', $args),
             $alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : '');
@@ -562,6 +573,40 @@ class SqlFunction {
     }
 }
 
+class SqlCase extends SqlFunction {
+    var $cases = array();
+    var $else = false;
+
+    static function N() {
+        return new static('CASE');
+    }
+
+    function when($expr, $result) {
+        $this->cases[] = array($expr, $result);
+        return $this;
+    }
+    function otherwise($result) {
+        $this->else = $result;
+        return $this;
+    }
+
+    function toSql($compiler, $model=false, $alias=false) {
+        $cases = array();
+        foreach ($this->cases as $A) {
+            list($expr, $result) = $A;
+            $expr = $this->input($expr, $compiler, $model);
+            $result = $this->input($result, $compiler, $model);
+            $cases[] = "WHEN {$expr} THEN {$result}";
+        }
+        if ($this->else) {
+            $else = $this->input($this->else, $compiler, $model);
+            $cases[] = "ELSE {$else}";
+        }
+        return sprintf('CASE %s END%s', implode(' ', $cases),
+            $alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : '');
+    }
+}
+
 class SqlExpr extends SqlFunction {
     function __construct($args) {
         $this->args = $args;
@@ -687,7 +732,12 @@ class SqlAggregate extends SqlFunction {
 
         // For DISTINCT, require a field specification — not a relationship
         // specification.
-        list($field, $rmodel) = $compiler->getField($this->expr, $model, $options);
+        $E = $this->expr;
+        if ($E instanceof SqlFunction) {
+            $field = $E->toSql($compiler, $model, $alias);
+        }
+        else {
+        list($field, $rmodel) = $compiler->getField($E, $model, $options);
         if ($this->distinct) {
             $pk = false;
             foreach ($rmodel::$meta['pk'] as $f) {
@@ -708,6 +758,7 @@ class SqlAggregate extends SqlFunction {
                 }
             }
         }
+        }
 
         return sprintf('%s(%s%s)%s', $this->func,
             $this->distinct ? 'DISTINCT ' : '', $field,
@@ -1455,23 +1506,19 @@ class SqlCompiler {
         $operator = static::$operators['exact'];
         if (!isset($options['table'])) {
             $field = array_pop($parts);
-            if (array_key_exists($field, static::$operators)) {
+            if (isset(static::$operators[$field])) {
                 $operator = static::$operators[$field];
                 $field = array_pop($parts);
             }
         }
 
         $path = array();
-
-        // Determine the alias for the root model table
-        $alias = (isset($this->joins['']))
-            ? $this->joins['']['alias']
-            : $this->quote($model::$meta['table']);
+        $rootModel = $model;
 
         // Call pushJoin for each segment in the join path. A new JOIN
         // fragment will need to be emitted and/or cached
-        $self = $this;
-        $push = function($p, $path, $extra=false) use (&$model, $self) {
+        $joins = array();
+        $push = function($p, $path, $model) use (&$joins) {
             $model::_inspect();
             if (!($info = $model::$meta['joins'][$p])) {
                 throw new OrmException(sprintf(
@@ -1479,27 +1526,38 @@ class SqlCompiler {
                     $model, $p));
             }
             $crumb = implode('__', $path);
-            $path[] = $p;
-            $tip = implode('__', $path);
-            $alias = $self->pushJoin($crumb, $tip, $model, $info, $extra);
+            $tip = ($crumb) ? "{$crumb}__{$p}" : $p;
+            $joins[] = array($crumb, $tip, $model, $info);
             // Roll to foreign model
-            foreach ($info['constraint'] as $local => $foreign) {
-                list($model, $f) = explode('.', $foreign);
-                if (class_exists($model))
-                    break;
-            }
-            return array($alias, $f);
+            return $info['fkey'];
         };
 
-        foreach ($parts as $i=>$p) {
-            list($alias) = $push($p, $path, @$options['constraint']);
+        foreach ($parts as $p) {
+            list($model) = $push($p, $path, $model);
             $path[] = $p;
         }
 
         // If comparing a relationship, join the foreign table
         // This is a comparison with a relationship — use the foreign key
         if (isset($model::$meta['joins'][$field])) {
-            list($alias, $field) = $push($field, $path, @$options['constraint']);
+            list($model, $field) = $push($field, $path, $model);
+        }
+
+        // Add the conststraint as the last arg to the last join
+        if (isset($options['constraint'])) {
+            $joins[count($joins)-1][] = $options['constraint'];
+        }
+
+        // Apply the joins list to $this->pushJoin
+        foreach ($joins as $A) {
+            $alias = call_user_func_array(array($this, 'pushJoin'), $A);
+        }
+
+        if (!isset($alias)) {
+            // Determine the alias for the root model table
+            $alias = (isset($this->joins['']))
+                ? $this->joins['']['alias']
+                : $this->quote($rootModel::$meta['table']);
         }
 
         if (isset($options['table']) && $options['table'])
@@ -1550,8 +1608,8 @@ class SqlCompiler {
         // coordination between the data returned from the database (where
         // table alias is available) and the corresponding data.
         $T = array('alias' => $alias);
-        $this->joins[$path] = &$T;
-        $T['sql'] = $this->compileJoin($tip, $model, $alias, $info, $constraint);
+        $this->joins[$path] = $T;
+        $this->joins[$path]['sql'] = $this->compileJoin($tip, $model, $alias, $info, $constraint);
         return $alias;
     }
 
@@ -1712,12 +1770,6 @@ class DbEngine {
 
 class MySqlCompiler extends SqlCompiler {
 
-    // Consts for ::input()
-    const SLOT_JOINS = 1;
-    const SLOT_WHERE = 2;
-
-    protected $input_join_count = 0;
-
     static $operators = array(
         'exact' => '%1$s = %2$s',
         'contains' => array('self', '__contains'),
@@ -1803,21 +1855,21 @@ class MySqlCompiler extends SqlCompiler {
         else
             $table = $this->quote($model::$meta['table']);
         foreach ($info['constraint'] as $local => $foreign) {
-            list($rmodel, $right) = explode('.', $foreign);
+            list($rmodel, $right) = $foreign;
             // Support a constant constraint with
             // "'constant'" => "Model.field_name"
             if ($local[0] == "'") {
                 $constraints[] = sprintf("%s.%s = %s",
                     $alias, $this->quote($right),
-                    $this->input(trim($local, '\'"'), self::SLOT_JOINS)
+                    $this->input(trim($local, '\'"'))
                 );
             }
             // Support local constraint
             // field_name => "'constant'"
-            elseif ($foreign[0] == "'" && !$right) {
+            elseif ($rmodel[0] == "'" && !$right) {
                 $constraints[] = sprintf("%s.%s = %s",
                     $table, $this->quote($local),
-                    $this->input(trim($foreign, '\'"'), self::SLOT_JOINS)
+                    $this->input(trim($rmodel, '\'"'))
                 );
             }
             else {
@@ -1829,7 +1881,7 @@ class MySqlCompiler extends SqlCompiler {
         }
         // Support extra join constraints
         if ($extra instanceof Q) {
-            $constraints[] = $this->compileQ($extra, $model, self::SLOT_JOINS);
+            $constraints[] = $this->compileQ($extra, $model);
         }
         if (!isset($rmodel))
             $rmodel = $model;
@@ -1838,9 +1890,9 @@ class MySqlCompiler extends SqlCompiler {
             // XXX: Support parameters from the nested query
             ? $rmodel::getQuery($this)
             : $this->quote($rmodel::$meta['table']);
-        $base = "$join$table $alias";
+        $base = "{$join}{$table} {$alias}";
         if ($constraints)
-            $base .= ' ON ('.implode(' AND ', $constraints).')';
+           $base .= ' ON ('.implode(' AND ', $constraints).')';
         return $base;
     }
 
@@ -1853,11 +1905,6 @@ class MySqlCompiler extends SqlCompiler {
      * Parameters:
      * $what - (mixed) value to be sent to the database. No escaping is
      *      necessary. Pass a raw value here.
-     * $slot - (int) clause location of the input in compiled SQL statement.
-     *      Currently, SLOT_JOINS and SLOT_WHERE is supported. SLOT_JOINS
-     *      inputs are inserted ahead of the SLOT_WHERE inputs as the joins
-     *      come logically before the where claused in the finalized
-     *      statement.
      *
      * Returns:
      * (string) token to be placed into the compiled SQL statement. For
@@ -1876,16 +1923,8 @@ class MySqlCompiler extends SqlCompiler {
             return 'NULL';
         }
         else {
-            switch ($slot) {
-            case self::SLOT_JOINS:
-                // This should be inserted before the WHERE inputs
-                array_splice($this->params, $this->input_join_count++, 0,
-                    array($what));
-                break;
-            default:
-                $this->params[] = $what;
-            }
-            return '?';
+            $this->params[] = $what;
+            return ':'.(count($this->params));
         }
     }
 
@@ -2193,7 +2232,7 @@ class MySqlCompiler extends SqlCompiler {
     }
 }
 
-class MysqlExecutor {
+class MySqlExecutor {
 
     var $stmt;
     var $fields = array();
@@ -2214,17 +2253,28 @@ class MysqlExecutor {
         return $this->map;
     }
 
+    function fixupParams() {
+        $self = $this;
+        $params = array();
+        $sql = preg_replace_callback('/:(\d+)/', function($m) use ($self, &$params) {
+            $params[] = $self->params[$m[1]-1];
+            return '?';
+        }, $this->sql);
+        return array($sql, $params);
+    }
+
     function _prepare() {
         $this->execute();
         $this->_setup_output();
     }
 
     function execute() {
-        if (!($this->stmt = db_prepare($this->sql)))
+        list($sql, $params) = $this->fixupParams();
+        if (!($this->stmt = db_prepare($sql)))
             throw new InconsistentModelException(
-                'Unable to prepare query: '.db_error().' '.$this->sql);
-        if (count($this->params))
-            $this->_bind($this->params);
+                'Unable to prepare query: '.db_error().' '.$sql);
+        if (count($params))
+            $this->_bind($params);
         if (!$this->stmt->execute() || ! $this->stmt->store_result()) {
             throw new OrmException('Unable to execute query: ' . $this->stmt->error);
         }
@@ -2335,9 +2385,8 @@ class MysqlExecutor {
 
     function __toString() {
         $self = $this;
-        $x = 0;
-        return preg_replace_callback('/\?/', function($m) use ($self, &$x) {
-            $p = $self->params[$x++];
+        return preg_replace_callback('/:(\d+)/', function($m) use ($self) {
+            $p = $self->params[$m[1]-1];
             return db_real_escape($p, is_string($p));
         }, $this->sql);
     }
diff --git a/include/class.role.php b/include/class.role.php
index b2bcd2c97d0813d7cff5fd03d854bbf607cadeb1..ce4256c5ba75da5a0c67eac8b51319a040758562 100644
--- a/include/class.role.php
+++ b/include/class.role.php
@@ -310,9 +310,15 @@ class RolePermission {
         return static::$_permissions;
     }
 
-    static function register($group, $perms) {
-        static::$_permissions[$group] = array_merge(
-                            static::$_permissions[$group] ?: array(), $perms);
+    static function register($group, $perms, $prepend=false) {
+        if ($prepend) {
+            static::$_permissions[$group] = array_merge(
+                $perms, static::$_permissions[$group] ?: array());
+        }
+        else {
+            static::$_permissions[$group] = array_merge(
+                static::$_permissions[$group] ?: array(), $perms);
+        }
     }
 }
 ?>
diff --git a/include/class.thread.php b/include/class.thread.php
index 3cf9308025ffed40b4ca3744dd538c16d9e4715f..2b1d8bec7963775e21de4a6e4a67c2c0ede44286 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -16,6 +16,7 @@
 **********************************************************************/
 include_once(INCLUDE_DIR.'class.ticket.php');
 include_once(INCLUDE_DIR.'class.draft.php');
+include_once(INCLUDE_DIR.'class.role.php');
 
 //Ticket thread.
 class Thread extends VerySimpleModel {
@@ -82,6 +83,7 @@ class Thread extends VerySimpleModel {
             'has_attachments' => SqlAggregate::COUNT('attachments', false,
                 new Q(array('attachments__inline'=>0)))
         ));
+        $base->exclude(array('flags__hasbit'=>ThreadEntry::FLAG_HIDDEN));
         if ($criteria)
             $base->filter($criteria);
         return $base;
@@ -398,7 +400,8 @@ class ThreadEntry extends VerySimpleModel {
     static $meta = array(
         'table' => THREAD_ENTRY_TABLE,
         'pk' => array('id'),
-        'select_related' => array('staff', 'user'),
+        'select_related' => array('staff', 'user', 'email_info'),
+        'ordering' => array('created'),
         'joins' => array(
             'thread' => array(
                 'constraint' => array('thread_id' => 'Thread.id'),
@@ -430,12 +433,24 @@ class ThreadEntry extends VerySimpleModel {
     );
 
     const FLAG_ORIGINAL_MESSAGE         = 0x0001;
+    const FLAG_EDITED                   = 0x0002;
+    const FLAG_HIDDEN                   = 0x0004;
+    const FLAG_GUARDED                  = 0x0008;   // No replace on edit
+
+    const PERM_EDIT     = 'thread.edit';
 
     var $_headers;
     var $_thread;
     var $_actions;
     var $_attachments;
 
+    static protected $perms = array(
+        self::PERM_EDIT => array(
+            'title' => /* @trans */ 'Edit Thread',
+            'desc'  => /* @trans */ 'Ability to edit thread items of other agents',
+        ),
+    );
+
     function postEmail($mailinfo) {
         if (!($thread = $this->getThread()))
             // Kind of hard to continue a discussion without a thread ...
@@ -457,8 +472,7 @@ class ThreadEntry extends VerySimpleModel {
     }
 
     function getParent() {
-        if ($this->getPid())
-            return ThreadEntry::lookup($this->getPid());
+        return $this->parent;
     }
 
     function getType() {
@@ -565,6 +579,21 @@ class ThreadEntry extends VerySimpleModel {
         return $recipients;
     }
 
+    /**
+     * Recurse through the ancestry of this thread entry to find the first
+     * thread entry which cites a email Message-ID field.
+     *
+     * Returns:
+     * <ThreadEntry> or null if neither this thread entry nor any of its
+     * ancestry contains an email header with an email Message-ID header.
+     */
+    function findOriginalEmailMessage() {
+        $P = $this;
+        while (!$P->getEmailMessageId()
+            && ($P = $P->getParent()));
+        return $P;
+    }
+
     function getUIDFromEmailReference($ref) {
 
         $info = unpack('Vtid/Vuid',
@@ -1248,8 +1277,14 @@ class ThreadEntry extends VerySimpleModel {
 
         self::$action_registry[$group][$action::getId()] = $action;
     }
+
+    static function getPermissions() {
+        return self::$perms;
+    }
 }
 
+RolePermission::register(/* @trans */ 'Tickets', ThreadEntry::getPermissions());
+
 
 class ThreadEntryBody /* extends SplString */ {
 
@@ -1734,7 +1769,7 @@ abstract class ThreadEntryAction {
     static $id;                 // Unique identifier used for plumbing
     static $icon = 'cog';
 
-    var $thread;
+    var $entry;
 
     function getName() {
         $class = get_class($this);
@@ -1751,13 +1786,13 @@ abstract class ThreadEntryAction {
     }
 
     function __construct(ThreadEntry $thread) {
-        $this->thread = $thread;
+        $this->entry = $thread;
     }
 
     abstract function trigger();
 
     function getTicket() {
-        return $this->thread->getTicket();
+        return $this->entry->getObject();
     }
 
     function isEnabled() {
@@ -1791,8 +1826,8 @@ abstract class ThreadEntryAction {
     function getAjaxUrl($dialog=false) {
         return sprintf('%stickets/%d/thread/%d/%s',
             $dialog ? '#' : 'ajax.php/',
-            $this->thread->getThread()->getObjectId(),
-            $this->thread->getId(),
+            $this->entry->getThread()->getObjectId(),
+            $this->entry->getId(),
             static::getId()
         );
     }
diff --git a/include/class.thread_actions.php b/include/class.thread_actions.php
index ce6ac82469df253fe5aecb942759de7f4a814252..30ddc810c056e7edfee8949820f4f2665d9501a3 100644
--- a/include/class.thread_actions.php
+++ b/include/class.thread_actions.php
@@ -23,14 +23,188 @@ class TEA_ShowEmailHeaders extends ThreadEntryAction {
     static $name = /* trans */ 'View Email Headers';
     static $icon = 'envelope';
 
-    function isEnabled() {
+    function isVisible() {
         global $thisstaff;
 
+        if (!$this->entry->getEmailHeader())
+            return false;
+
         return $thisstaff && $thisstaff->isAdmin();
     }
 
+    function getJsStub() {
+        return sprintf("$.dialog('%s');",
+            $this->getAjaxUrl()
+        );
+    }
+
+    function trigger() {
+        switch ($_SERVER['REQUEST_METHOD']) {
+        case 'GET':
+            return $this->trigger__get();
+        }
+    }
+
+    private function trigger__get() {
+        $headers = $this->entry->getEmailHeader();
+
+        include STAFFINC_DIR . 'templates/thread-email-headers.tmpl.php';
+    }
+}
+ThreadEntry::registerAction(/* trans */ 'E-Mail', 'TEA_ShowEmailHeaders');
+
+class TEA_EditThreadEntry extends ThreadEntryAction {
+    static $id = 'edit';
+    static $name = /* trans */ 'Edit';
+    static $icon = 'pencil';
+
     function isVisible() {
-        return (bool) $this->thread->getEmailHeader();
+        // Can't edit system posts
+        return ($this->entry->staff_id || $this->entry->user_id)
+            && $this->entry->type != 'R' && $this->isEnabled();
+    }
+
+    function isEnabled() {
+        global $thisstaff;
+
+        $T = $this->entry->getThread()->getObject();
+        // You can edit your own posts or posts by your department members
+        // if your a manager, or everyone's if your an admin
+        return $thisstaff && (
+            $thisstaff->getId() == $this->entry->staff_id
+            || ($T instanceof Ticket
+                && $T->getDept()->getManagerId() == $thisstaff->getId()
+            )
+            || ($T instanceof Ticket
+                && $thisstaff->getRole($T->getDeptId())->hasPerm(ThreadEntry::PERM_EDIT)
+            )
+        );
+    }
+
+    function getJsStub() {
+        return sprintf(<<<JS
+var url = '%s';
+$.dialog(url, [201], function(xhr, resp) {
+  var json = JSON.parse(resp);
+  if (!json || !json.thread_id)
+    return;
+  $('#thread-id-'+json.thread_id)
+    .attr('id', 'thread-id-' + json.new_id)
+    .find('div')
+    .html(json.body)
+    .closest('td')
+    .effect('highlight')
+}, {size:'large'});
+JS
+        , $this->getAjaxUrl());
+    }
+
+
+    function trigger() {
+        switch ($_SERVER['REQUEST_METHOD']) {
+        case 'GET':
+            return $this->trigger__get();
+        case 'POST':
+            return $this->trigger__post();
+        }
+    }
+
+    protected function trigger__get() {
+        global $cfg, $thisstaff;
+
+        $poster = $this->entry->getStaff();
+
+        include STAFFINC_DIR . 'templates/thread-entry-edit.tmpl.php';
+    }
+
+    function updateEntry($guard=false) {
+        $old = $this->entry;
+        $type = ($old->format == 'html')
+            ? 'HtmlThreadEntryBody' : 'TextThreadEntryBody';
+        $new = new $type($_POST['body']);
+
+        if ($new->getClean() == $old->body)
+            // No update was performed
+            return $old;
+
+        $entry = ThreadEntry::create(array(
+            // Copy most information from the old entry
+            'poster' => $old->poster,
+            'userId' => $old->user_id,
+            'staffId' => $old->staff_id,
+            'type' => $old->type,
+            'threadId' => $old->thread_id,
+
+            // Connect the new entry to be a child of the previous
+            'pid' => $old->id,
+
+            // Add in new stuff
+            'title' => $_POST['title'],
+            'body' => $new,
+            'ip_address' => $_SERVER['REMOTE_ADDR'],
+        ));
+
+        if (!$entry)
+            return false;
+
+        // Note, anything that points to the $old entry as PID should remain
+        // that way for email header lookups and such to remain consistent
+
+        if ($old->flags & ThreadEntry::FLAG_EDITED
+            and !($old->flags & ThreadEntry::FLAG_GUARDED)
+        ) {
+            // Replace previous edit --------------------------
+            $original = $old->getParent();
+            // Drop the previous edit, and base this edit off the original
+            $old->delete();
+            $old = $original;
+        }
+
+        // Mark the new entry as edited (but not hidden)
+        $entry->flags = ($old->flags & ~ThreadEntry::FLAG_HIDDEN)
+            | ThreadEntry::FLAG_EDITED;
+
+        // Guard against deletes on future edit if requested. This is done
+        // if an email was triggered by the last edit. In such a case, it
+        // should not be replace by a subsequent edit.
+        if ($guard)
+            $entry->flags |= ThreadEntry::FLAG_GUARDED;
+
+        // Sort in the same place in the thread — XXX: Add a `sequence` id
+        $entry->created = $old->created;
+        $entry->updated = SqlFunction::NOW();
+        $entry->save();
+
+        // Hide the old entry from the object thread
+        $old->flags |= ThreadEntry::FLAG_HIDDEN;
+        $old->save();
+
+        return $entry;
+    }
+
+    protected function trigger__post() {
+        global $thisstaff;
+
+        if (!($entry = $this->updateEntry()))
+            return $this->trigger__get();
+
+        Http::response('201', JsonDataEncoder::encode(array(
+            'thread_id' => $this->entry->id,
+            'new_id' => $entry->id,
+            'body' => $entry->getBody()->toHtml(),
+        )));
+    }
+}
+ThreadEntry::registerAction(/* trans */ 'Manage', 'TEA_EditThreadEntry');
+
+class TEA_OrigThreadEntry extends ThreadEntryAction {
+    static $id = 'previous';
+    static $name = /* trans */ 'View History';
+    static $icon = 'copy';
+
+    function isVisible() {
+        // Can't edit system posts
+        return $this->entry->flags & ThreadEntry::FLAG_EDITED;
     }
 
     function getJsStub() {
@@ -47,9 +221,113 @@ class TEA_ShowEmailHeaders extends ThreadEntryAction {
     }
 
     private function trigger__get() {
-        $headers = $this->thread->getEmailHeader();
+        $entry = $this->entry->getParent();
+        if (!$entry)
+            Http::response(404, 'No history for this entry');
+        include STAFFINC_DIR . 'templates/thread-entry-view.tmpl.php';
+    }
+}
+ThreadEntry::registerAction(/* trans */ 'Manage', 'TEA_OrigThreadEntry');
 
-        include STAFFINC_DIR . 'templates/thread-email-headers.tmpl.php';
+class TEA_EditAndResendThreadEntry extends TEA_EditThreadEntry {
+    static $id = 'edit_resend';
+    static $name = /* trans */ 'Edit and Resend';
+    static $icon = 'reply-all';
+
+    function isVisible() {
+        // Can only resend replies
+        return $this->entry->staff_id && $this->entry->type == 'R'
+            && $this->isEnabled();
+    }
+
+    protected function trigger__post() {
+        $resend = @$_POST['commit'] == 'resend';
+
+        if (!($entry = $this->updateEntry($resend)))
+            return $this->trigger__get();
+
+        if (@$_POST['commit'] == 'resend')
+            $this->resend($entry);
+
+        Http::response('201', JsonDataEncoder::encode(array(
+            'thread_id' => $this->entry->id,
+            'new_id' => $entry->id,
+            'body' => $entry->getBody()->toHtml(),
+        )));
+    }
+
+    function resend($response) {
+        global $cfg, $thisstaff;
+
+        $vars = $_POST;
+        $ticket = $response->getThread()->getObject();
+
+        $dept = $ticket->getDept();
+        $poster = $response->getStaff();
+
+        if ($thisstaff && $vars['signature'] == 'mine')
+            $signature = $thisstaff->getSignature();
+        elseif ($poster && $vars['signature'] == 'theirs')
+            $signature = $poster->getSignature();
+        elseif ($vars['signature'] == 'dept' && $dept && $dept->isPublic())
+            $signature = $dept->getSignature();
+        else
+            $signature = '';
+
+        $variables = array(
+            'response' => $response,
+            'signature' => $signature,
+            'staff' => $response->getStaff(),
+            'poster' => $response->getStaff());
+        $options = array('thread' => $response);
+
+        if (($email=$dept->getEmail())
+            && ($tpl = $dept->getTemplate())
+            && ($msg=$tpl->getReplyMsgTemplate())
+        ) {
+            $msg = $ticket->replaceVars($msg->asArray(),
+                $variables + array('recipient' => $ticket->getOwner()));
+
+            $attachments = $cfg->emailAttachments()
+                ? $response->getAttachments() : array();
+            $email->send($ticket->getOwner(), $msg['subj'], $msg['body'],
+                $attachments, $options);
+        }
+        // TODO: Add an option to the dialog
+        $ticket->notifyCollaborators($response, array('signature' => $signature));
     }
 }
-ThreadEntry::registerAction(/* trans */ 'E-Mail', 'TEA_ShowEmailHeaders');
+ThreadEntry::registerAction(/* trans */ 'Manage', 'TEA_EditAndResendThreadEntry');
+
+class TEA_ResendThreadEntry extends TEA_EditAndResendThreadEntry {
+    static $id = 'resend';
+    static $name = /* trans */ 'Resend';
+    static $icon = 'reply-all';
+
+    function isVisible() {
+        // Can only resend replies
+        return $this->entry->staff_id && $this->entry->type == 'R'
+            && !parent::isEnabled();
+    }
+    function isEnabled() {
+        return true;
+    }
+
+    protected function trigger__get() {
+        global $cfg, $thisstaff;
+
+        $poster = $this->entry->getStaff();
+
+        include STAFFINC_DIR . 'templates/thread-entry-resend.tmpl.php';
+    }
+
+    protected function trigger__post() {
+        $resend = @$_POST['commit'] == 'resend';
+
+        if (@$_POST['commit'] == 'resend')
+            $this->resend($this->entry);
+
+        Http::response('201', 'Okee dokey');
+    }
+}
+ThreadEntry::registerAction(/* trans */ 'Manage', 'TEA_ResendThreadEntry');
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 30a5df7fd556d85d55074691e093289992d5aeab..2a14829dc83ed5d3658e7e15a7f750a04a6cc874 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -192,7 +192,7 @@ EOF;
     }
 }
 
-RolePermission::register(/* @trans */ 'Tickets', TicketModel::getPermissions());
+RolePermission::register(/* @trans */ 'Tickets', TicketModel::getPermissions(), true);
 
 class TicketCData extends VerySimpleModel {
     static $meta = array(
diff --git a/include/staff/templates/thread-entry-edit.tmpl.php b/include/staff/templates/thread-entry-edit.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..e1ed1f81c9e062f8c0a45bc4a1bc6c925759681f
--- /dev/null
+++ b/include/staff/templates/thread-entry-edit.tmpl.php
@@ -0,0 +1,82 @@
+<h3><?php echo __('Edit Thread Entry'); ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<hr/>
+
+<form method="post" action="<?php echo $this->getAjaxUrl(true); ?>">
+
+<input type="text" style="width:100%;font-size:14px" placeholder="<?php
+    echo __('Title'); ?>" name="title" value="<?php
+    echo Format::htmlchars($this->entry->title); ?>"/>
+<hr style="height:0"/>
+<textarea style="display: block; width: 100%; height: auto; min-height: 150px;"
+<?php if ($poster && $this->entry->type == 'R') {
+    $signature_type = $poster->getDefaultSignatureType();
+    $signature = '';
+    if (($T = $this->entry->getThread()->getObject()) instanceof Ticket)
+        $dept = $T->getDept();
+    switch ($poster->getDefaultSignatureType()) {
+    case 'dept':
+        if ($dept && $dept->canAppendSignature())
+           $signature = $dept->getSignature();
+       break;
+    case 'mine':
+        $signature = $poster->getSignature();
+        $signature_type = 'theirs';
+        break;
+    } ?>
+    data-dept-id="<?php echo $dept->getId(); ?>"
+    data-poster-id="<?php echo $this->entry->staff_id; ?>"
+    data-signature-field="signature"
+    data-signature="<?php echo Format::htmlchars(Format::viewableImages($signature)); ?>"
+<?php } ?>
+    name="body"
+    class="large <?php
+        if ($cfg->isHtmlThreadEnabled() && $this->entry->format == 'html')
+            echo 'richtext';
+    ?>"><?php echo Format::viewableImages($this->entry->body);
+?></textarea>
+
+<?php if ($this->entry->type == 'R') { ?>
+<div style="margin:10px 0;"><strong><?php echo __('Signature'); ?>:</strong>
+    <label><input type="radio" name="signature" value="none" checked="checked"> <?php echo __('None');?></label>
+    <?php
+    if ($poster
+        && $poster->getId() != $thisstaff->getId()
+        && $poster->getSignature()
+    ) { ?>
+    <label><input type="radio" name="signature" value="theirs"
+        <?php echo ($info['signature']=='theirs')?'checked="checked"':''; ?>> <?php echo __('Their Signature');?></label>
+    <?php
+    }
+    if ($thisstaff->getSignature()) {?>
+    <label><input type="radio" name="signature" value="mine"
+        <?php echo ($info['signature']=='mine')?'checked="checked"':''; ?>> <?php echo __('My Signature');?></label>
+    <?php
+    } ?>
+    <?php
+    if ($dept && $dept->canAppendSignature()) { ?>
+    <label><input type="radio" name="signature" value="dept"
+        <?php echo ($info['signature']=='dept')?'checked="checked"':''; ?>>
+        <?php echo sprintf(__('Department Signature (%s)'), Format::htmlchars($dept->getName())); ?></label>
+    <?php
+    } ?>
+</div>
+<?php } # end of type == 'R' ?>
+
+<hr>
+<div class="full-width">
+    <span class="buttons pull-left">
+        <input type="button" name="cancel" class="close"
+            value="<?php echo __('Cancel'); ?>">
+    </span>
+    <span class="buttons pull-right">
+        <button type="submit" name="commit" value="save" class="button"
+            ><?php echo __('Save'); ?></button>
+<?php if ($this->entry->type == 'R') { ?>
+        <button type="submit" name="commit" value="resend" class="button"
+            ><?php echo __('Save and Resend'); ?></button>
+<?php } ?>
+    </span>
+</div>
+
+</form>
diff --git a/include/staff/templates/thread-entry-resend.tmpl.php b/include/staff/templates/thread-entry-resend.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..8a3e283c6418c0cdadd26b3db34a4fbc8c59f440
--- /dev/null
+++ b/include/staff/templates/thread-entry-resend.tmpl.php
@@ -0,0 +1,50 @@
+<h3><?php echo __('Resend Entry'); ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<hr/>
+
+<form method="post" action="<?php echo $this->getAjaxUrl(true); ?>">
+
+<div class="thread-body" style="background-color: transparent; max-height: 150px; width: 100%; overflow: scroll;">
+    <?php echo $this->entry->getBody()->toHtml(); ?>
+</div>
+
+<?php if ($this->entry->type == 'R') { ?>
+<div style="margin:10px 0;"><strong><?php echo __('Signature'); ?>:</strong>
+    <label><input type="radio" name="signature" value="none" checked="checked"> <?php echo __('None');?></label>
+    <?php
+    if ($poster
+        && $poster->getId() != $thisstaff->getId()
+        && $poster->getSignature()
+    ) { ?>
+    <label><input type="radio" name="signature" value="theirs"
+        <?php echo ($info['signature']=='theirs')?'checked="checked"':''; ?>> <?php echo __('Their Signature');?></label>
+    <?php
+    }
+    if ($thisstaff->getSignature()) {?>
+    <label><input type="radio" name="signature" value="mine"
+        <?php echo ($info['signature']=='mine')?'checked="checked"':''; ?>> <?php echo __('My Signature');?></label>
+    <?php
+    } ?>
+    <?php
+    if ($dept && $dept->canAppendSignature()) { ?>
+    <label><input type="radio" name="signature" value="dept"
+        <?php echo ($info['signature']=='dept')?'checked="checked"':''; ?>>
+        <?php echo sprintf(__('Department Signature (%s)'), Format::htmlchars($dept->getName())); ?></label>
+    <?php
+    } ?>
+</div>
+<?php } # end of type == 'R' ?>
+
+<hr>
+<p class="full-width">
+    <span class="buttons pull-left">
+        <input type="button" name="cancel" class="close"
+            value="<?php echo __('Cancel'); ?>">
+    </span>
+    <span class="buttons pull-right">
+        <input type="submit" name="save"
+            value="<?php echo __('Resend'); ?>">
+    </span>
+</p>
+
+</form>
diff --git a/include/staff/templates/thread-entry-view.tmpl.php b/include/staff/templates/thread-entry-view.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..2b76d851f3611a89e452492117593ebf17203b37
--- /dev/null
+++ b/include/staff/templates/thread-entry-view.tmpl.php
@@ -0,0 +1,58 @@
+<h3><?php echo __('Original Thread Entry'); ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<hr/>
+
+<div id="history" class="accordian">
+
+<?php
+$E = $entry;
+do { ?>
+<dt>
+    <a href="#"><i class="icon-copy"></i>
+    <strong><?php if ($E->title)
+        echo Format::htmlchars($E->title).' — '; ?></strong>
+    <em><?php if (strpos($E->updated, '0000-') === false)
+        echo sprintf(__('Edited on %s'), Format::datetime($E->updated));
+    else
+        echo __('Original'); ?></em>
+    </a>
+</dt>
+<dd class="hidden" style="background-color:transparent">
+    <div class="thread-body" style="background-color:transparent">
+        <?php echo $E->getBody()->toHtml(); ?>
+    </div>
+</dd>
+<?php
+}
+while (($E = $E->getParent()) && $E->type == $entry->type);
+?>
+
+</div>
+
+<hr>
+<p class="full-width">
+    <span class="buttons pull-right">
+        <input type="button" name="cancel" class="close"
+            value="<?php echo __('Close'); ?>">
+    </span>
+</p>
+
+</form>
+
+<script type="text/javascript">
+$(function() {
+  var I = setInterval(function() {
+    var A = $('#history.accordian');
+    if (!A.length) return;
+    clearInterval(I);
+
+    var allPanels = $('dd', A).hide().removeClass('hidden');
+    $('dt > a', A).click(function() {
+      $('dt', A).removeClass('active');
+      allPanels.slideUp();
+      $(this).parent().addClass('active').next().slideDown();
+      return false;
+    });
+    allPanels.last().show();
+  }, 100);
+});
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index e2cc27c5f3441b18d42b9c6b227b6ae0583158dc..bdc09745da094d2daef7cb944830c7a4a267e9e7 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -423,9 +423,13 @@ $tcount = $ticket->getThreadEntries($types)->count();
 <?php               foreach ($actions as $group => $list) {
                         foreach ($list as $id => $action) { ?>
                     <li>
-                    <a class="no-pjax" href="#" onclick="javascript:
-                            <?php echo str_replace('"', '\\"', $action->getJsStub()); ?>; return false;">
-                        <i class="<?php echo $action->getIcon(); ?>"></i> <?php
+                    <a class="no-pjax <?php
+                        if (!$action->isEnabled())
+                            echo 'disabled';
+                    ?>" href="#" onclick="javascript:
+                        if ($(this).hasClass('disabled')) return false;
+                        <?php echo str_replace('"', '\\"', $action->getJsStub()); ?>; return false;">
+                        <i class="icon-fixed-width <?php echo $action->getIcon(); ?>"></i> <?php
                             echo $action->getName();
                 ?></a></li>
 <?php                   }
@@ -434,7 +438,13 @@ $tcount = $ticket->getThreadEntries($types)->count();
                     </div>
 <?php           } ?>
                     <span style="vertical-align:middle">
-                        <span style="vertical-align:middle;" class="textra"></span>
+                        <span style="vertical-align:middle;" class="textra">
+        <?php if ($entry->flags & ThreadEntry::FLAG_EDITED) { ?>
+                <span class="label label-bare" title="<?php
+        echo sprintf(__('Edited on %s by %s'), Format::datetime($entry->updated), 'You');
+                ?>"><?php echo __('Edited'); ?></span>
+        <?php } ?>
+                        </span>
                         <span style="vertical-align:middle;"
                             class="tmeta faded title"><?php
                             echo Format::htmlchars($entry->getName()); ?></span>
diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php
index bdab6cd1db6cdbe7d3d7aad223c878c87504b520..8f6a86ce857528594c9e6ee2c607d2ecdcb5485c 100644
--- a/include/staff/tickets.inc.php
+++ b/include/staff/tickets.inc.php
@@ -150,12 +150,11 @@ if (!$view_all_tickets) {
     if ($teams = array_filter($thisstaff->getTeams()))
         $assigned->add(array('team_id__in' => $teams));
 
-    $visibility = array(
-        new Q(array('status__state'=>'open', $assigned))
-    );
+    $visibility = Q::any(array('status__state'=>'open', $assigned));
+
     // -- Routed to a department of mine
     if (!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts()))
-        $visibility[] = new Q(array('dept_id__in' => $depts));
+        $visibility->add(array('dept_id__in' => $depts));
 
     $tickets->filter(Q::any($visibility));
 }
@@ -266,9 +265,19 @@ $tickets->values('lock__staff_id', 'staff_id', 'isoverdue', 'team_id', 'ticket_i
 
 // Add in annotations
 $tickets->annotate(array(
-    'collab_count' => SqlAggregate::COUNT('thread__collaborators'),
-    'attachment_count' => SqlAggregate::COUNT('thread__entries__attachments'),
-    'thread_count' => SqlAggregate::COUNT('thread__entries'),
+    'collab_count' => SqlAggregate::COUNT('thread__collaborators', true),
+    'attachment_count' => SqlAggregate::COUNT(SqlCase::N()
+       ->when(new SqlField('thread__entries__attachments__inline'), null)
+       ->otherwise(new SqlField('thread__entries__attachments')),
+        true
+    ),
+    'thread_count' => SqlAggregate::COUNT(SqlCase::N()
+        ->when(
+            new Q(array('thread__entries__flags__hasbit'=>ThreadEntry::FLAG_HIDDEN)),
+            null)
+        ->otherwise(new SqlField('thread__entries__id')),
+       true
+    ),
 ));
 
 // Save the query to the session for exporting
diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js
index bd32edda61afbc875b2aa08d3fef296f6912db39..203c398944384fa5fb620e6cdf5652aa0d8d950e 100644
--- a/js/redactor-osticket.js
+++ b/js/redactor-osticket.js
@@ -156,10 +156,10 @@ RedactorPlugins.signature = function() {
             else
                 this.$signatureBox.hide();
             $('input[name='+$el.data('signatureField')+']', $el.closest('form'))
-                .on('change', false, false, $.proxy(this.updateSignature, this));
+                .on('change', false, false, $.proxy(this.signature.updateSignature, this));
             if ($el.data('deptField'))
                 $(':input[name='+$el.data('deptField')+']', $el.closest('form'))
-                    .on('change', false, false, $.proxy(this.updateSignature, this));
+                    .on('change', false, false, $.proxy(this.signature.updateSignature, this));
             // Expand on hover
             var outer = this.$signatureBox,
                 inner = $('.inner', this.$signatureBox).get(0),
@@ -200,6 +200,9 @@ RedactorPlugins.signature = function() {
             else
                 return inner.empty().parent().hide();
         }
+        else if (selected == 'theirs' && $el.data('posterId')) {
+            url += 'agent/' + $el.data('posterId');
+        }
         else if (type == 'none')
            return inner.empty().parent().hide();
         else
@@ -251,7 +254,8 @@ $(function() {
                     'file', 'table', 'link', '|', 'alignment', '|',
                     'horizontalrule'],
                 'buttonSource': !el.hasClass('no-bar'),
-                'autoresize': !el.hasClass('no-bar'),
+                'autoresize': !el.hasClass('no-bar') && !el.closest('.dialog').length,
+                'maxHeight': el.closest('.dialog').length ? selectedSize : false,
                 'minHeight': selectedSize,
                 'focus': false,
                 'plugins': el.hasClass('no-bar')
diff --git a/scp/css/dropdown.css b/scp/css/dropdown.css
index 6105ea27fc4098cc3ba0fce3ae3467af0e8e7afb..236deb3664c1c7f424e901130e9a71c41e7fd2c4 100644
--- a/scp/css/dropdown.css
+++ b/scp/css/dropdown.css
@@ -42,6 +42,11 @@
   color: #FFF !important;
   cursor: pointer;
 }
+.action-dropdown ul li > a.disabled {
+  pointer-events: none;
+  color: #999;
+  color: rgba(85,85,85,0.5);
+}
 .action-dropdown hr {
   height: 1px;
   border: none;
diff --git a/scp/css/scp.css b/scp/css/scp.css
index 64735d3de27b7959e24fe40d1179aa2aff0764e1..7817e76560c9610788831f445bd6caa6f4c743e5 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -1947,7 +1947,7 @@ tr.disabled th {
 
 .label {
   font-size: 11px;
-  padding: 1px 4px 2px;
+  padding: 1px 4px;
   -webkit-border-radius: 3px;
   -moz-border-radius: 3px;
   border-radius: 3px;
@@ -1959,6 +1959,13 @@ tr.disabled th {
   text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
   background-color: #999999;
 }
+.label-bare {
+  background-color: transparent;
+  background-color: rgba(0,0,0,0);
+  border: 1px solid #999999;
+  color: #999999;
+  text-shadow: none;
+}
 .label-info {
   background-color: #3a87ad;
 }
diff --git a/scp/js/scp.js b/scp/js/scp.js
index 8f99d9545c636d7b4e802521632f33c162ca1b19..aed3618a2be9c6bb4a99301f5e38303cafca4821 100644
--- a/scp/js/scp.js
+++ b/scp/js/scp.js
@@ -562,16 +562,25 @@ $.dialog = function (url, codes, cb, options) {
             queue: false,
             complete: function() { if (options.onshow) options.onshow(); }
         });
+        var submit_button = null;
         $(document).off('.dialog');
+        $(document).on('click.dialog',
+            '#popup input[type=submit], #popup button[type=submit]',
+            function(e) { submit_button = $(this); });
         $(document).on('submit.dialog', '.dialog#popup form', function(e) {
             e.preventDefault();
-            var $form = $(this);
+            var $form = $(this),
+                data = $form.serialize();
+            if (submit_button) {
+                data += '&' + escape(submit_button.attr('name')) + '='
+                    + escape(submit_button.attr('value'));
+            }
             $('div#popup-loading', $popup).show()
                 .find('h1').css({'margin-top':function() { return $popup.height()/3-$(this).height()/3}});
             $.ajax({
                 type:  $form.attr('method'),
                 url: 'ajax.php/'+$form.attr('action').substr(1),
-                data: $form.serialize(),
+                data: data,
                 cache: false,
                 success: function(resp, status, xhr) {
                     if (xhr && xhr.status && codes
diff --git a/scp/roles.php b/scp/roles.php
index 824d13b14e285a4ae22d22283a9e988f36eaf0bf..749dee96fad1f00cf7d40535a3a242b7b4b93356 100644
--- a/scp/roles.php
+++ b/scp/roles.php
@@ -21,6 +21,7 @@ include_once INCLUDE_DIR . 'class.canned.php';
 include_once INCLUDE_DIR . 'class.faq.php';
 include_once INCLUDE_DIR . 'class.email.php';
 include_once INCLUDE_DIR . 'class.report.php';
+include_once INCLUDE_DIR . 'class.thread.php';
 
 $errors = array();
 $role=null;