diff --git a/css/thread.css b/css/thread.css
index 2001dde95a01ad6761be77a93eadd390544f494d..6fe012353f3137d0629f15d4ac73667694460c16 100644
--- a/css/thread.css
+++ b/css/thread.css
@@ -467,9 +467,6 @@ table.thread-entry {
 table.thread-entry th div span {
     vertical-align: middle;
 }
-table.thread-entry th div :not(.title) {
-    font-weight: 600;
-}
 table.thread-entry th div .title {
     font-weight: 400;
 }
diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php
index 6cd63e98ad71c3603754b88e25cc78ee61d1bf4b..85a0c15165de97cc3fa784ce8b21a4dfe1c7ce29 100644
--- a/include/ajax.tickets.php
+++ b/include/ajax.tickets.php
@@ -780,6 +780,25 @@ class TicketsAjaxAPI extends AjaxController {
         return self::_changeSelectedTicketsStatus($state, $info, $errors);
     }
 
+    function triggerThreadAction($ticket_id, $thread_id, $action) {
+        $thread = ThreadEntry::lookup($thread_id, $ticket_id);
+        if (!$thread)
+            Http::response(404, 'No such ticket thread entry');
+
+        $valid = false;
+        foreach ($thread->getActions() as $group=>$list) {
+            foreach ($list as $name=>$A) {
+                if ($A->getId() == $action) {
+                    $valid = true; break;
+                }
+            }
+        }
+        if (!$valid)
+            Http::response(400, 'Not a valid action for this thread');
+
+        $thread->triggerAction($action);
+    }
+
     private function _changeSelectedTicketsStatus($state, $info=array(), $errors=array()) {
 
         $count = $_REQUEST['count'] ?:
diff --git a/include/class.file.php b/include/class.file.php
index caafae2bf9cab7018b6aee5d4a8503d2f7d84295..82b998a783e969b70918399f7559d0dd78d69f7c 100644
--- a/include/class.file.php
+++ b/include/class.file.php
@@ -208,8 +208,10 @@ class AttachmentFile {
         if ($bk->sendRedirectUrl('inline'))
             return;
         $this->makeCacheable();
-        Http::download($this->getName(), $this->getType() ?: 'application/octet-stream',
-            null, 'inline');
+        $type = $this->getType() ?: 'application/octet-stream';
+        if (isset($_REQUEST['overridetype']))
+            $type = $_REQUEST['overridetype'];
+        Http::download($this->getName(), $type, null, 'inline');
         header('Content-Length: '.$this->getSize());
         $this->sendData(false);
         exit();
diff --git a/include/class.thread.php b/include/class.thread.php
index 71dc5525f4a1c41f5db5ed95f74f2053a5be62b5..ff5b1e7078628b8d982520c91437c5c4ac141273 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -1079,6 +1079,65 @@ class ThreadEntry {
     static function add($vars) {
         return ($entry=self::create($vars)) ? $entry->getId() : 0;
     }
+
+    // Extensible thread entry actions ------------------------
+    /**
+     * getActions
+     *
+     * Retrieve a list of possible actions. This list is shown to the agent
+     * via drop-down list at the top-right of the thread entry when rendered
+     * in the UI.
+     */
+    function getActions() {
+        if (!isset($this->_actions)) {
+            $this->_actions = array();
+
+            foreach (self::$action_registry as $group=>$list) {
+                $T = array();
+                $this->_actions[__($group)] = &$T;
+                foreach ($list as $id=>$action) {
+                    $A = new $action($this);
+                    if ($A->isVisible()) {
+                        $T[$id] = $A;
+                    }
+                }
+                unset($T);
+            }
+        }
+        return $this->_actions;
+    }
+
+    function hasActions() {
+        foreach ($this->getActions() as $group => $list) {
+            if (count($list))
+                return true;
+        }
+        return false;
+    }
+
+    function triggerAction($name) {
+        foreach ($this->getActions() as $group=>$list) {
+            foreach ($list as $id=>$action) {
+                if (0 === strcasecmp($id, $name)) {
+                    if (!$action->isEnabled())
+                        return false;
+
+                    $action->trigger();
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    static $action_registry = array();
+
+    static function registerAction($group, $action) {
+        if (!isset(self::$action_registry[$group]))
+            self::$action_registry[$group] = array();
+
+        self::$action_registry[$group][$action::getId()] = $action;
+    }
 }
 
 
@@ -1253,7 +1312,7 @@ class HtmlThreadEntryBody extends ThreadEntryBody {
 
     function getSearchable() {
         // <br> -> \n
-        $body = preg_replace(array('`<br(\s*)?/?>`i', '`</div>`i'), "\n", $this->body);
+        $body = preg_replace(array('`<br(\s*)?/?>`i', '`</div>`i'), "\n", $this->body); # <?php
         $body = Format::htmldecode(Format::striptags($body));
         return Format::searchable($body);
     }
@@ -1558,4 +1617,69 @@ class TicketThread extends ObjectThread {
                     ));
     }
 }
+
+/**
+ * Class: ThreadEntryAction
+ *
+ * Defines a simple action to be performed on a thread entry item, such as
+ * viewing the raw email headers used to generate the message, resend the
+ * confirmation emails, etc.
+ */
+abstract class ThreadEntryAction {
+    static $name;               // Friendly, translatable name
+    static $id;                 // Unique identifier used for plumbing
+    static $icon = 'cog';
+
+    var $thread;
+
+    function getName() {
+        $class = get_class($this);
+        return __($class::$name);
+    }
+
+    static function getId() {
+        return static::$id;
+    }
+
+    function getIcon() {
+        $class = get_class($this);
+        return 'icon-' . $class::$icon;
+    }
+
+    function __construct(ThreadEntry $thread) {
+        $this->thread = $thread;
+    }
+
+    abstract function trigger();
+
+    function getTicket() {
+        return $this->thread->getTicket();
+    }
+
+    function isEnabled() {
+        return $this->isVisible();
+    }
+    function isVisible() {
+        return true;
+    }
+
+    /**
+     * getJsStub
+     *
+     * Retrieves a small JavaScript snippet to insert into the rendered page
+     * which should, via an AJAX callback, trigger this action to be
+     * performed. The URL for this sort of activity is already provided for
+     * you via the ::getAjaxUrl() method in this class.
+     */
+    abstract function getJsStub();
+
+    function getAjaxUrl($dialog=false) {
+        return sprintf('%stickets/%d/thread/%d/%s',
+            $dialog ? '#' : 'ajax.php/',
+            $this->thread->getTicketId(),
+            $this->thread->getId(),
+            static::getId()
+        );
+    }
+}
 ?>
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index e6522e7dee47fbb44e6a87aeb10e35379828b5e3..3542a663134e6ec02108d2efc2871d5cd8855c97 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -393,7 +393,8 @@ $tcount+= $ticket->getNumNotes();
     /* -------- Messages & Responses & Notes (if inline)-------------*/
     $types = array('M', 'R', 'N');
     if(($thread=$ticket->getThreadEntries($types))) {
-       foreach($thread as $entry) { ?>
+       foreach($thread as $entry) {
+        $tentry = $ticket->getThreadEntry($entry['id']); ?>
         <table class="thread-entry <?php echo $threadTypes[$entry['type']]; ?>" cellspacing="0" cellpadding="1" width="940" border="0">
             <tr>
                 <th colspan="4" width="100%">
@@ -404,6 +405,28 @@ $tcount+= $ticket->getNumNotes();
                     <span style="display:inline-block;padding:0 1em" class="faded title"><?php
                         echo Format::truncate($entry['title'], 100); ?></span>
                     </span>
+<?php           if ($tentry->hasActions()) {
+                    $actions = $tentry->getActions(); ?>
+                    <div class="pull-right">
+                    <span class="action-button" data-dropdown="#entry-action-more-<?php echo $entry['id']; ?>">
+                        <i class="icon-caret-down"></i>
+                        <span ><i class="icon-cog"></i></span>
+                    </span>
+                    <div id="entry-action-more-<?php echo $entry['id']; ?>" class="action-dropdown anchor-right">
+                <ul class="title">
+<?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
+                            echo $action->getName();
+                ?></a></li>
+<?php                   }
+                    } ?>
+                </ul>
+                    </div>
+<?php           } ?>
                     <span class="pull-right" style="white-space:no-wrap;display:inline-block">
                         <span style="vertical-align:middle;" class="textra"></span>
                         <span style="vertical-align:middle;"
@@ -419,7 +442,6 @@ $tcount+= $ticket->getNumNotes();
             <?php
             $urls = null;
             if($entry['attachments']
-                    && ($tentry = $ticket->getThreadEntry($entry['id']))
                     && ($urls = $tentry->getAttachmentUrls())
                     && ($links = $tentry->getAttachmentsLinks())) {?>
             <tr>
diff --git a/scp/ajax.php b/scp/ajax.php
index 1c313c18c4afab80c58cbb3579de2cf254a34c76..131243adeb18c39b487631bcb9c5aee7b50044c1 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -142,6 +142,7 @@ $dispatcher = patterns('',
         url_get('^(?P<tid>\d+)/canned-resp/(?P<cid>\w+).(?P<format>json|txt)', 'cannedResponse'),
         url_get('^(?P<tid>\d+)/status/(?P<status>\w+)(?:/(?P<sid>\d+))?$', 'changeTicketStatus'),
         url_post('^(?P<tid>\d+)/status$', 'setTicketStatus'),
+        url('^(?P<tid>\d+)/thread/(?P<thread_id>\d+)/(?P<action>\w+)$', 'triggerThreadAction'),
         url_get('^status/(?P<status>\w+)(?:/(?P<sid>\d+))?$', 'changeSelectedTicketsStatus'),
         url_post('^status/(?P<state>\w+)$', 'setSelectedTicketsStatus'),
         url_get('^(?P<tid>\d+)/tasks$', 'tasks'),