diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php
index 8d7228dea69f4c74eb6eb464be1cf6bcdf5aa766..9933fb28abdc38e645392e6e0c5c152aa2306d8f 100644
--- a/include/ajax.tickets.php
+++ b/include/ajax.tickets.php
@@ -785,9 +785,11 @@ class TicketsAjaxAPI extends AjaxController {
     }
 
     function triggerThreadAction($ticket_id, $thread_id, $action) {
-        $thread = ThreadEntry::lookup($thread_id, $ticket_id);
+        $thread = ThreadEntry::lookup($thread_id);
         if (!$thread)
             Http::response(404, 'No such ticket thread entry');
+        if ($thread->getThread()->getObjectId() != $ticket_id)
+            Http::response(404, 'No such ticket thread entry');
 
         $valid = false;
         foreach ($thread->getActions() as $group=>$list) {
diff --git a/include/api.tickets.php b/include/api.tickets.php
index d80b1582889f7bf3f944eea9808a3f675f6008f2..d70eda638a31f69bc9841d47d0714c127232a9ef 100644
--- a/include/api.tickets.php
+++ b/include/api.tickets.php
@@ -80,7 +80,8 @@ class TicketApiController extends ApiController {
                 }
                 // Validate and save immediately
                 try {
-                    $file['id'] = $fileField->uploadAttachment($file);
+                    $F = $fileField->uploadAttachment($file);
+                    $file['id'] = $F->getId();
                 }
                 catch (FileUploadError $ex) {
                     $file['error'] = $file['name'] . ': ' . $ex->getMessage();
@@ -150,14 +151,29 @@ class TicketApiController extends ApiController {
         if (!$data)
             $data = $this->getEmailRequest();
 
-        if (($thread = ThreadEntry::lookupByEmailHeaders($data))
-                && ($t=$thread->getTicket())
-                && ($data['staffId']
-                    || !$t->isClosed()
-                    || $t->isReopenable())
-                && $thread->postEmail($data)) {
-            return $thread->getTicket();
+        $seen = false;
+        if (($entry = ThreadEntry::lookupByEmailHeaders($data, $seen))
+            && ($message = $entry->postEmail($data))
+        ) {
+            if ($message instanceof ThreadEntry) {
+                return $message->getThread()->getObject();
+            }
+            else if ($seen) {
+                // Email has been processed previously
+                return $entry->getThread()->getObject();
+            }
         }
+
+        // Allow continuation of thread without initial message or note
+        elseif (($thread = Thread::lookupByEmailHeaders($data))
+            && ($message = $thread->postEmail($data))
+        ) {
+            return $thread->getObject();
+        }
+
+        // All emails which do not appear to be part of an existing thread
+        // will always create new "Tickets". All other objects will need to
+        // be created via the web interface or the API
         return $this->createTicket($data);
     }
 
diff --git a/include/class.attachment.php b/include/class.attachment.php
index 9126cff8c0c7f9f68b6071dae3c50dc09e38c7ca..bb208411157836127f7ef5f9622f2499c9d8b39e 100644
--- a/include/class.attachment.php
+++ b/include/class.attachment.php
@@ -16,36 +16,37 @@
 require_once(INCLUDE_DIR.'class.ticket.php');
 require_once(INCLUDE_DIR.'class.file.php');
 
-class Attachment {
-    var $id;
-    var $file_id;
+class Attachment extends VerySimpleModel {
+    static $meta = array(
+        'table' => ATTACHMENT_TABLE,
+        'pk' => array('id'),
+        'select_related' => array('file'),
+        'joins' => array(
+            'thread_entry' => array(
+                'constraint' => array(
+                    'object_id' => 'ThreadEntry.id',
+                    'type' => "'H'",
+                ),
+            ),
+            'file' => array(
+                'constraint' => array(
+                    'file_id' => 'AttachmentFile.id',
+                ),
+            ),
+        ),
+    );
 
-    var $ht;
     var $object;
 
-    function Attachment($id) {
-
-        $sql = 'SELECT a.* FROM '.ATTACHMENT_TABLE.' a '
-             . 'WHERE a.id='.db_input($id);
-        if (!($res=db_query($sql)) || !db_num_rows($res))
-            return;
-
-        $this->ht = db_fetch_array($res);
-        $this->file = $this->object = null;
-    }
-
     function getId() {
-        return $this->ht['id'];
+        return $this->id;
     }
 
     function getFileId() {
-        return $this->ht['file_id'];
+        return $this->file_id;
     }
 
     function getFile() {
-        if(!$this->file && $this->getFileId())
-            $this->file = AttachmentFile::lookup($this->getFileId());
-
         return $this->file;
     }
 
@@ -66,44 +67,23 @@ class Attachment {
         return $this->object;
     }
 
-    static function getIdByFileHash($hash, $objectId=0) {
-        $sql='SELECT a.id FROM '.ATTACHMENT_TABLE.' a '
-            .' INNER JOIN '.FILE_TABLE.' f ON(f.id=a.file_id) '
-            .' WHERE f.`key`='.db_input($hash);
+    static function lookupByFileHash($hash, $objectId=0) {
+        $file = static::objects()
+            ->filter(array('file__key' => $hash));
+
         if ($objectId)
-            $sql.=' AND a.object_id='.db_input($objectId);
+            $file->filter(array('object_id' => $objectId));
 
-        return db_result(db_query($sql));
+        return $file->first();
     }
 
     static function lookup($var, $objectId=0) {
-
-        $id = is_numeric($var) ? $var : self::getIdByFileHash($var,
-                $objectId);
-
-        return ($id
-                && is_numeric($id)
-                && ($attach = new Attachment($id, $objectId))
-                && $attach->getId()==$id
-            ) ? $attach : null;
+        return is_numeric($var)
+            ? parent::lookup($var)
+            : static::lookupByFileHash($var, $objectId);
     }
 }
 
-class AttachmentModel extends VerySimpleModel {
-    static $meta = array(
-        'table' => ATTACHMENT_TABLE,
-        'pk' => array('id'),
-        'joins' => array(
-            'thread' => array(
-                'constraint' => array(
-                    'object_id' => 'ThreadEntryModel.id',
-                    'type' => "'H'",
-                ),
-            ),
-        ),
-    );
-}
-
 class GenericAttachment extends VerySimpleModel {
     static $meta = array(
         'table' => ATTACHMENT_TABLE,
@@ -132,24 +112,26 @@ class GenericAttachments {
                 $fileId = $file;
             elseif (is_array($file) && isset($file['id']))
                 $fileId = $file['id'];
-            elseif (!($fileId = AttachmentFile::upload($file)))
+            elseif ($F = AttachmentFile::upload($file))
+                $fileId = $F->getId();
+            else
                 continue;
 
             $_inline = isset($file['inline']) ? $file['inline'] : $inline;
 
-            $sql ='INSERT INTO '.ATTACHMENT_TABLE
-                .' SET `type`='.db_input($this->getType())
-                .',object_id='.db_input($this->getId())
-                .',file_id='.db_input($fileId)
-                .',inline='.db_input($_inline ? 1 : 0);
+            $att = Attachment::create(array(
+                'type' => $this->getType(),
+                'object_id' => $this->getId(),
+                'file_id' => $fileId,
+                'inline' => $_inline ? 1 : 0,
+            ));
             if ($lang)
-                $sql .= ',lang='.db_input($lang);
+                $att->lang = $lang;
 
             // File may already be associated with the draft (in the
             // event it was deleted and re-added)
-            if (db_query($sql, function($errno) { return $errno != 1062; })
-                    || db_errno() == 1062)
-                $i[] = $fileId;
+            $att->save();
+            $i[] = $fileId;
         }
 
         return $i;
@@ -161,15 +143,18 @@ class GenericAttachments {
             $fileId = $file;
         elseif (is_array($file) && isset($file['id']))
             $fileId = $file['id'];
-        elseif (!($fileId = AttachmentFile::save($file)))
+        elseif ($file = AttachmentFile::create($file))
+            $fileId = $file->getId();
+        else
             return false;
 
-        $sql ='INSERT INTO '.ATTACHMENT_TABLE
-            .' SET `type`='.db_input($this->getType())
-            .',object_id='.db_input($this->getId())
-            .',file_id='.db_input($fileId)
-            .',inline='.db_input($inline ? 1 : 0);
-        if (!db_query($sql) || !db_affected_rows())
+        $att = Attachment::create(array(
+            'type' => $this->getType(),
+            'object_id' => $this->getId(),
+            'file_id' => $fileId,
+            'inline' => $inline ? 1 : 0,
+        ));
+        if (!$att->save())
             return false;
 
         return $fileId;
@@ -181,51 +166,29 @@ class GenericAttachments {
     function count($lang=false) { return count($this->getSeparates($lang)); }
 
     function _getList($separate=false, $inlines=false, $lang=false) {
-        if(!isset($this->attachments)) {
-            $this->attachments = array();
-            $sql='SELECT f.id, f.size, f.`key`, f.signature, f.name '
-                .', a.inline, a.lang, a.id as attach_id '
-                .' FROM '.FILE_TABLE.' f '
-                .' INNER JOIN '.ATTACHMENT_TABLE.' a ON(f.id=a.file_id) '
-                .' WHERE a.`type`='.db_input($this->getType())
-                .' AND a.object_id='.db_input($this->getId());
-            if(($res=db_query($sql)) && db_num_rows($res)) {
-                while($rec=db_fetch_array($res)) {
-                    $rec['download_url'] = AttachmentFile::generateDownloadUrl(
-                        $rec['id'], $rec['key'], $rec['signature']);
-                    $this->attachments[] = $rec;
-                }
-            }
-        }
-        $attachments = array();
-        foreach ($this->attachments as $a) {
-            if (($a['inline'] != $separate || $a['inline'] == $inlines)
-                    && $lang == $a['lang']) {
-                $a['file_id'] = $a['id'];
-                $a['hash'] = md5($a['file_id'].session_id().$a['key']);
-                $attachments[] = $a;
-            }
-        }
-        return $attachments;
+        return Attachment::objects()->filter(array(
+            'type' => $this->getType(),
+            'object_id' => $this->getId(),
+        ));
     }
 
     function delete($file_id) {
-        $deleted = 0;
-        $sql='DELETE FROM '.ATTACHMENT_TABLE
-            .' WHERE object_id='.db_input($this->getId())
-            .'   AND `type`='.db_input($this->getType())
-            .'   AND file_id='.db_input($file_id);
-        return db_query($sql) && db_affected_rows() > 0;
+        return Attachment::objects()->filter(array(
+            'type' => $this->getType(),
+            'object_id' => $this->getId(),
+            'file_id' => $file_id,
+        ))->delete();
     }
 
     function deleteAll($inline_only=false){
-        $deleted=0;
-        $sql='DELETE FROM '.ATTACHMENT_TABLE
-            .' WHERE object_id='.db_input($this->getId())
-            .'   AND `type`='.db_input($this->getType());
+        $objects = Attachment::objects()->filter(array(
+            'type' => $this->getType(),
+            'object_id' => $this->getId(),
+        ));
         if ($inline_only)
-            $sql .= ' AND inline = 1';
-        return db_query($sql) && db_affected_rows() > 0;
+            $objects->filter(array('inline' => 1));
+
+        return $objects->delete();
     }
 
     function deleteInlines() {
diff --git a/include/class.client.php b/include/class.client.php
index e353b977aaab06a3c3667b5d7f8e053987e120b1..28b4d8df0de7efa2f50be20a093e17d597915be8 100644
--- a/include/class.client.php
+++ b/include/class.client.php
@@ -15,7 +15,8 @@
 **********************************************************************/
 require_once INCLUDE_DIR.'class.user.php';
 
-abstract class TicketUser {
+abstract class TicketUser
+implements EmailContact {
 
     static private $token_regex = '/^(?P<type>\w{1})(?P<algo>\d+)x(?P<hash>.*)$/i';
 
@@ -54,6 +55,9 @@ abstract class TicketUser {
 
     }
 
+    function getId() { return ($this->user) ? $this->user->getId() : null; }
+    function getEmail() { return ($this->user) ? $this->user->getEmail() : null; }
+
     function sendAccessLink() {
         global $ost;
 
@@ -427,4 +431,7 @@ class ClientAccount extends UserAccount {
     }
 }
 
+// Used by the email system
+interface EmailContact {
+}
 ?>
diff --git a/include/class.config.php b/include/class.config.php
index fc488ee4619d4e5fdc8edf5761f8253546e07a18..b8c35095396f5866b47e79b023338ce2289794a9 100644
--- a/include/class.config.php
+++ b/include/class.config.php
@@ -1111,7 +1111,7 @@ class OsticketConfig extends Config {
                 ; // Pass
             elseif ($logo['error'])
                 $errors['logo'] = $logo['error'];
-            elseif (!($id = AttachmentFile::uploadLogo($logo, $error)))
+            elseif (!AttachmentFile::uploadLogo($logo, $error))
                 $errors['logo'] = sprintf(__('Unable to upload logo image: %s'), $error);
         }
 
diff --git a/include/class.draft.php b/include/class.draft.php
index 49503ed7c66bcc367580ab55779cc109ad4be07f..659325ede98dd6281a83857f92d5b4f989f9df96 100644
--- a/include/class.draft.php
+++ b/include/class.draft.php
@@ -62,11 +62,13 @@ class Draft extends VerySimpleModel {
         $body = Format::localizeInlineImages($body);
         $matches = array();
         if (preg_match_all('/"cid:([\\w.-]{32})"/', $body, $matches)) {
-            foreach ($matches[1] as $hash) {
-                if ($file_id = AttachmentFile::getIdByHash($hash))
-                    $attachments[] = array(
-                            'id' => $file_id,
-                            'inline' => true);
+            $files = AttachmentFile::objects()
+                ->filter(array('key__in' => $matches[1]));
+            foreach ($files as $F) {
+                $attachments[] = array(
+                    'id' => $F->getId(),
+                    'inline' => true
+                );
             }
         }
         return $attachments;
@@ -86,9 +88,12 @@ class Draft extends VerySimpleModel {
 
         // Purge current attachments
         $this->attachments->deleteInlines();
-        foreach ($matches[1] as $hash)
-            if ($file = AttachmentFile::getIdByHash($hash))
-                $this->attachments->upload($file, true);
+        foreach (AttachmentFile::objects()
+            ->filter(array('key__in' => $matches[1]))
+            as $F
+        ) {
+            $this->attachments->upload($F->getId(), true);
+        }
     }
 
     function setBody($body) {
diff --git a/include/class.file.php b/include/class.file.php
index 2c2ab0fc69a37f19c3cda28e49b2cfa22d7bd925..a540aebd2b986f4ebe8df6881303e8992f33f30c 100644
--- a/include/class.file.php
+++ b/include/class.file.php
@@ -14,43 +14,17 @@
 require_once(INCLUDE_DIR.'class.signal.php');
 require_once(INCLUDE_DIR.'class.error.php');
 
-class AttachmentFile {
-
-    var $id;
-    var $ht;
-
-    function AttachmentFile($id) {
-        $this->id =0;
-        return ($this->load($id));
-    }
-
-    function load($id=0) {
-
-        if(!$id && !($id=$this->getId()))
-            return false;
-
-        $sql='SELECT f.id, f.type, size, name, `key`, signature, ft, bk, f.created, '
-            .' count(DISTINCT a.object_id) as canned, '
-            .' count(DISTINCT t.id) as entries '
-            .' FROM '.FILE_TABLE.' f '
-            .' LEFT JOIN '.ATTACHMENT_TABLE.' a
-                ON(a.file_id=f.id) '
-            .' LEFT JOIN '.ATTACHMENT_TABLE.' t
-                ON(t.file_id = f.id) '
-            .' WHERE f.id='.db_input($id)
-            .' GROUP BY f.id';
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
-
-        $this->ht=db_fetch_array($res);
-        $this->id =$this->ht['id'];
-
-        return true;
-    }
-
-    function reload() {
-        return $this->load();
-    }
+class AttachmentFile extends VerySimpleModel {
+
+    static $meta = array(
+        'table' => FILE_TABLE,
+        'pk' => array('id'),
+        'joins' => array(
+            'attachments' => array(
+                'reverse' => 'Attachment.file'
+            ),
+        ),
+    );
 
     function getHashtable() {
         return $this->ht;
@@ -61,15 +35,15 @@ class AttachmentFile {
     }
 
     function getNumEntries() {
-        return $this->ht['entries'];
+        return $this->attachments->count();
     }
 
     function isCanned() {
-        return ($this->ht['canned']);
+        return $this->getNumEntries();
     }
 
     function isInUse() {
-        return ($this->getNumEntries() || $this->isCanned());
+        return $this->getNumEntries();
     }
 
     function getId() {
@@ -77,11 +51,11 @@ class AttachmentFile {
     }
 
     function getType() {
-        return $this->ht['type'];
+        return $this->type;
     }
 
     function getBackend() {
-        return $this->ht['bk'];
+        return $this->bk;
     }
 
     function getMime() {
@@ -89,25 +63,23 @@ class AttachmentFile {
     }
 
     function getSize() {
-        return $this->ht['size'];
+        return $this->size;
     }
 
     function getName() {
-        return $this->ht['name'];
+        return $this->name;
     }
 
     function getKey() {
-        return $this->ht['key'];
+        return $this->key;
     }
 
     function getSignature() {
-        $sig = $this->ht['signature'];
-        if (!$sig) return $this->getKey();
-        return $sig;
+        return $this->signature ?: $this->getKey();
     }
 
     function lastModified() {
-        return $this->ht['created'];
+        return $this->created;
     }
 
     function open() {
@@ -145,8 +117,7 @@ class AttachmentFile {
 
     function delete() {
 
-        $sql='DELETE FROM '.FILE_TABLE.' WHERE id='.db_input($this->getId()).' LIMIT 1';
-        if(!db_query($sql) || !db_affected_rows())
+        if (!parent::delete())
             return false;
 
         if ($bk = $this->open())
@@ -298,7 +269,7 @@ class AttachmentFile {
     }
 
     /* Function assumes the files types have been validated */
-    function upload($file, $ft='T') {
+    static function upload($file, $ft='T') {
 
         if(!$file['name'] || $file['error'] || !is_uploaded_file($file['tmp_name']))
             return false;
@@ -314,10 +285,10 @@ class AttachmentFile {
                     'tmp_name'=>$file['tmp_name'],
                     );
 
-        return AttachmentFile::save($info, $ft);
+        return static::create($info, $ft);
     }
 
-    function uploadLogo($file, &$error, $aspect_ratio=3) {
+    static function uploadLogo($file, &$error, $aspect_ratio=3) {
         /* Borrowed in part from
          * http://salman-w.blogspot.com/2009/04/crop-to-fit-image-using-aspphp.html
          */
@@ -348,8 +319,13 @@ class AttachmentFile {
         return false;
     }
 
-    function save(&$file, $ft='T') {
-
+    static function create(&$file, $ft='T') {
+        if (isset($file['encoding'])) {
+            switch ($file['encoding']) {
+            case 'base64':
+                $file['data'] = base64_decode($file['data']);
+            }
+        }
         if (isset($file['data'])) {
             // Allow a callback function to delay or avoid reading or
             // fetching ihe file contents
@@ -364,15 +340,18 @@ class AttachmentFile {
 
         if (isset($file['size'])) {
             // Check and see if the file is already on record
-            $sql = 'SELECT id, `key` FROM '.FILE_TABLE
-                .' WHERE signature='.db_input($file['signature'])
-                .' AND size='.db_input($file['size']);
-
-            // If the record exists in the database already, a file with the
-            // same hash and size is already on file -- just return its ID
-            if (list($id, $key) = db_fetch_row(db_query($sql))) {
-                $file['key'] = $key;
-                return $id;
+            $existing = static::objects()->filter(array(
+                'signature' => $file['signature'],
+                'size' => $file['size']
+            ))
+            ->first();
+
+            // If the record exists in the database already, a file with
+            // the same hash and size is already on file -- just return
+            // the file
+            if ($existing) {
+                $file['key'] = $existing->key;
+                return $existing;
             }
         }
         elseif (!isset($file['data'])) {
@@ -393,20 +372,19 @@ class AttachmentFile {
         if (!$file['type'])
             $file['type'] = 'application/octet-stream';
 
-        $sql='INSERT INTO '.FILE_TABLE.' SET created=NOW() '
-            .',type='.db_input(strtolower($file['type']))
-            .',name='.db_input($file['name'])
-            .',`key`='.db_input($file['key'])
-            .',ft='.db_input($ft ?: 'T')
-            .',signature='.db_input($file['signature']);
 
-        if (isset($file['size']))
-            $sql .= ',size='.db_input($file['size']);
+        $f = parent::create(array(
+            'type' => strtolower($file['type']),
+            'name' => $file['name'],
+            'key' => $file['key'],
+            'ft' => $ft ?: 'T',
+            'signature' => $file['signature'],
+        ));
 
-        if (!(db_query($sql) && ($id = db_insert_id())))
-            return false;
+        if (isset($file['size']))
+            $f->size = $file['size'];
 
-        if (!($f = AttachmentFile::lookup($id)))
+        if (!$f->save())
             return false;
 
         // Note that this is preferred over $f->open() because the file does
@@ -440,26 +418,22 @@ class AttachmentFile {
             return false;
         }
 
-        $sql = 'UPDATE '.FILE_TABLE.' SET bk='.db_input($bk->getBkChar());
+        $f->bk = $bk->getBkChar();
 
         if (!isset($file['size'])) {
             if ($size = $bk->getSize())
-                $file['size'] = $size;
+                $f->size = $size;
             // Prefer mb_strlen, because mbstring.func_overload will
             // automatically prefer it if configured.
             elseif (extension_loaded('mbstring'))
-                $file['size'] = mb_strlen($file['data'], '8bit');
+                $f->size = mb_strlen($file['data'], '8bit');
             // bootstrap.php include a compat version of mb_strlen
             else
-                $file['size'] = strlen($file['data']);
-
-            $sql .= ', `size`='.db_input($file['size']);
+                $f->size = strlen($file['data']);
         }
 
-        $sql .= ' WHERE id='.db_input($f->getId());
-        db_query($sql);
-
-        return $f->getId();
+        $f->save();
+        return $f;
     }
 
     /**
@@ -523,10 +497,8 @@ class AttachmentFile {
             return false;
         }
 
-        $sql = 'UPDATE '.FILE_TABLE.' SET bk='
-            .db_input($target->getBkChar())
-            .' WHERE id='.db_input($this->getId());
-        if (!db_query($sql) || db_affected_rows()!=1)
+        $this->bk = $target->getBkChar();
+        if (!$this->save())
             return false;
 
         return $source->unlink();
@@ -554,31 +526,14 @@ class AttachmentFile {
         return FileStorageBackend::lookup($char, $file);
     }
 
-    /* Static functions */
-    function getIdByHash($hash) {
-
-        $sql='SELECT id FROM '.FILE_TABLE.' WHERE `key`='.db_input($hash);
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id)=db_fetch_row($res);
-
-        return $id;
-    }
-
-    function lookup($id) {
-
-        $id = is_numeric($id)?$id:AttachmentFile::getIdByHash($id);
-
-        return ($id && ($file = new AttachmentFile($id)) && $file->getId()==$id)?$file:null;
+    static function lookupByHash($hash) {
+        return parent::lookup(array('key' => $hash));
     }
 
-    static function create($info, &$errors) {
-        if (isset($info['encoding'])) {
-            switch ($info['encoding']) {
-                case 'base64':
-                    $info['data'] = base64_decode($info['data']);
-            }
-        }
-        return self::save($info);
+    static function lookup($id) {
+        return is_numeric($id)
+            ? parent::lookup($id)
+            : static::lookupByHash($id);
     }
 
     /*
@@ -620,35 +575,31 @@ class AttachmentFile {
      * Removes files and associated meta-data for files which no ticket,
      * canned-response, or faq point to any more.
      */
-    /* static */ function deleteOrphans() {
+    static function deleteOrphans() {
 
         // XXX: Allow plugins to define filetypes which do not represent
         //      files attached to tickets or other things in the attachment
         //      table and are not logos
-        //FIXME: Just user straight up left join
-        $sql = 'SELECT id FROM '.FILE_TABLE.' WHERE id NOT IN ('
-                .'SELECT file_id FROM '.ATTACHMENT_TABLE
-            .") AND `ft` = 'T' AND TIMESTAMPDIFF(DAY, `created`, CURRENT_TIMESTAMP) > 1";
-
-        if (!($res = db_query($sql)))
-            return false;
-
-        while (list($id) = db_fetch_row($res))
-            if (($file = self::lookup($id)) && !$file->delete())
+        $files = static::objects()
+            ->filter(array(
+                'attachments__object_id__isnull' => true,
+                'ft' => 'T',
+                'created__gt' => new DateTime('now -1 day'),
+            ));
+
+        foreach ($files as $f) {
+            if (!$f->delete())
                 break;
+        }
 
         return true;
     }
 
     /* static */
     function allLogos() {
-        $sql = 'SELECT id FROM '.FILE_TABLE.' WHERE ft="L"
-            ORDER BY created';
-        $logos = array();
-        $res = db_query($sql);
-        while (list($id) = db_fetch_row($res))
-            $logos[] = AttachmentFile::lookup($id);
-        return $logos;
+        return static::objects()
+            ->filter(array('ft' => 'L'))
+            ->order_by('created');
     }
 }
 
diff --git a/include/class.format.php b/include/class.format.php
index 7c06ee1c464dd85db22afb8d2fabcf5f4a71e360..f922a6c5ac3141f505c4aee36818063974637fe6 100644
--- a/include/class.format.php
+++ b/include/class.format.php
@@ -410,10 +410,19 @@ class Format {
 
 
     function viewableImages($html, $script=false) {
+        $cids = $images = array();
+        // Try and get information for all the files in one query
+        if (preg_match_all('/"cid:([\w._-]{32})"/', $html, $cids)) {
+            foreach (AttachmentFile::objects()
+                ->filter(array('key__in' => $cids[1]))
+                as $file
+            ) {
+                $images[strtolower($file->getKey())] = $file;
+            }
+        }
         return preg_replace_callback('/"cid:([\w._-]{32})"/',
-        function($match) use ($script) {
-            $hash = $match[1];
-            if (!($file = AttachmentFile::lookup($hash)))
+        function($match) use ($script, $images) {
+            if (!($file = $images[strtolower($match[1])]))
                 return $match[0];
             return sprintf('"%s" data-cid="%s"',
                 $file->getDownloadUrl(false, 'inline', $script), $match[1]);
diff --git a/include/class.forms.php b/include/class.forms.php
index dfccf962c5d1e307fec038fd028f400c889de122..5122ddd933bacf16c1bf3a91e46ca377067f48bd 100644
--- a/include/class.forms.php
+++ b/include/class.forms.php
@@ -1929,10 +1929,10 @@ class FileUploadField extends FormField {
         if (!$bypass && $file['size'] > $config['size'])
             Http::response(413, 'File is too large');
 
-        if (!($id = AttachmentFile::upload($file)))
+        if (!($F = AttachmentFile::upload($file)))
             Http::response(500, 'Unable to store file: '. $file['error']);
 
-        return $id;
+        return $F->getId();
     }
 
     /**
@@ -1972,10 +1972,10 @@ class FileUploadField extends FormField {
         if ($file['size'] > $config['size'])
             throw new FileUploadError(__('File size is too large'));
 
-        if (!$id = AttachmentFile::save($file))
+        if (!$F = AttachmentFile::create($file))
             throw new FileUploadError(__('Unable to save file'));
 
-        return $id;
+        return $F;
     }
 
     function isValidFileType($name, $type=false) {
@@ -2718,7 +2718,8 @@ class FileUploadWidget extends Widget {
         if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_FILES[$this->name])) {
             foreach (AttachmentFile::format($_FILES[$this->name]) as $file) {
                 try {
-                    $ids[] = $this->field->uploadFile($file);
+                    $F = $this->field->uploadFile($file);
+                    $ids[] = $F->getId();
                 }
                 catch (FileUploadError $ex) {}
             }
diff --git a/include/class.mailer.php b/include/class.mailer.php
index a3093c203a14eac9f8e7a2082830f715352b2128..e3b40c6b2483de759a8a251aed9344f813a45039 100644
--- a/include/class.mailer.php
+++ b/include/class.mailer.php
@@ -79,15 +79,202 @@ class Mailer {
         return $this->attachments;
     }
 
-    function addAttachment($attachment) {
+    function addAttachment(Attachment $attachment) {
         // XXX: This looks too assuming; however, the attachment processor
         // in the ::send() method seems hard coded to expect this format
-        $this->attachments[$attachment['file_id']] = $attachment;
+        $this->attachments[$attachment->file_id] = $attachment->file;
+    }
+
+    function addFile(AttachmentFile $file) {
+        // XXX: This looks too assuming; however, the attachment processor
+        // in the ::send() method seems hard coded to expect this format
+        $this->attachments[$file->file_id] = $file;
     }
 
     function addAttachments($attachments) {
-        foreach ($attachments as $a)
-            $this->addAttachment($a);
+        foreach ($attachments as $a) {
+            if ($a instanceof Attachment)
+                $this->addAttachment($a);
+            elseif ($a instanceof AttachmentFile)
+                $this->addFile($a);
+        }
+    }
+
+    /**
+     * getMessageId
+     *
+     * Generates a unique message ID for an outbound message. Optionally,
+     * the recipient can be used to create a tag for the message ID where
+     * the user-id and thread-entry-id are encoded in the message-id so
+     * the message can be threaded if it is replied to without any other
+     * indicator of the thread to which it belongs. This tag is signed with
+     * the secret-salt of the installation to guard against false positives.
+     *
+     * Parameters:
+     * $recipient - (EmailContact|null) recipient of the message. The ID of
+     *      the recipient is placed in the message id TAG section so it can
+     *      be recovered if the email replied to directly by the end user.
+     * $options - (array) - options passed to ::send(). If it includes a
+     *      'thread' element, the threadId will be recorded in the TAG
+     *
+     * Returns:
+     * (string) - email message id, without leading and trailing <> chars.
+     * See the Format below for the structure.
+     *
+     * Format:
+     * VA-B-C, with dash separators and A-C explained below:
+     *
+     * V: Version code of the generated Message-Id
+     * A: Predictable random code — used for loop detection (sysid)
+     * B: Random data for unique identifier (rand)
+     * C: TAG: Base64(Pack(userid, entryId, threadId, type, Signature)),
+     *    '=' chars discarded
+     * where Signature is:
+     *   Signed Tag value, last 5 chars from
+     *        HMAC(sha1, Tag + rand + sysid, SECRET_SALT),
+     *   where Tag is:
+     *     pack(userId, entryId, threadId, type)
+     */
+    function getMessageId($recipient, $options=array(), $version='B') {
+        $tag = '';
+        $rand = Misc::randCode(9,
+            // RFC822 specifies the LHS of the addr-spec can have any char
+            // except the specials — ()<>@,;:\".[], dash is reserved as the
+            // section separator, and + is reserved for historical reasons
+            'abcdefghiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_=');
+        $sig = $this->getEmail()?$this->getEmail()->getEmail():'@osTicketMailer';
+        $sysid = static::getSystemMessageIdCode();
+        if ($recipient instanceof EmailContact) {
+            // Create a tag for the outbound email
+            $entry = (isset($options['thread']) && $options['thread'] instanceof ThreadEntry)
+                ? $options['thread'] : false;
+            $thread = $entry ? $entry->getThread()
+                : (isset($options['thread']) && $options['thread'] instanceof Thread
+                    ? $options['thread'] : false);
+            $tag = pack('VVVa',
+                $recipient->getId(),
+                $entry ? $entry->getId() : 0,
+                $thread ? $thread->getId() : 0,
+                ($recipient instanceof Staff ? 'S'
+                    : ($recipient instanceof TicketOwner ? 'U'
+                    : ($recipient instanceof Collaborator ? 'C'
+                    : '?')))
+            );
+            // Sign the tag with the system secret salt
+            $tag .= substr(hash_hmac('sha1', $tag.$rand.$sysid, SECRET_SALT, true), -5);
+            $tag = str_replace('=','',base64_encode($tag));
+        }
+        return sprintf('B%s-%s-%s-%s',
+            $sysid, $rand, $tag, $sig);
+    }
+
+    /**
+     * decodeMessageId
+     *
+     * Decodes a message-id generated by osTicket using the ::getMessageId()
+     * method of this class. This will digest the received message-id token
+     * and return an array with some information about it.
+     *
+     * Parameters:
+     * $mid - (string) message-id from an email Message-Id, In-Reply-To, and
+     *      References header.
+     *
+     * Returns:
+     * (array) of information containing all or some of the following keys
+     *      'loopback' - (bool) true or false if the message originated by
+     *          this osTicket installation.
+     *      'version' - (string|FALSE) version code of the message id
+     *      'code' - (string) unique but predictable help desk message-id
+     *      'id' - (string) random characters serving as the unique id
+     *      'entryId' - (int) thread-entry-id from which the message originated
+     *      'threadId' - (int) thread-id from which the message originated
+     *      'staffId' - (int|null) staff the email was originally sent to
+     *      'userId' - (int|null) user the email was originally sent to
+     *      'userClass' - (string) class of user the email was sent to
+     *          'U' - TicketOwner
+     *          'S' - Staff
+     *          'C' - Collborator
+     *          '?' - Something else
+     */
+    static function decodeMessageId($mid) {
+        // Drop <> tokens
+        $mid = trim($mid, '<> ');
+        // Drop email domain on rhs
+        list($lhs, $sig) = explode('@', $mid, 2);
+        // LHS should be tokenized by '-'
+        $parts = explode('-', $lhs);
+
+        $rv = array('loopback' => false, 'version' => false);
+
+        // There should be at least two tokens if the message was sent by
+        // this system. Otherwise, there's nothing to be detected
+        if (count($parts) < 2)
+            return $rv;
+
+        $decoders = array(
+        'A' => function($id, $tag) use ($sig) {
+            // Old format was VA-B-C-D@sig, where C was the packed tag and D
+            // was blank
+            $format = 'Vuid/VentryId/auserClass';
+            $chksig = substr(hash_hmac('sha1', $tag.$id, SECRET_SALT), -10);
+            if ($tag && $sig == $chksig && ($tag = base64_decode($tag))) {
+                // Find user and ticket id
+                return unpack($format, $tag);
+            }
+            return false;
+        },
+        'B' => function($id, $tag) {
+            $format = 'Vuid/VentryId/VthreadId/auserClass/a*sig';
+            if ($tag && ($tag = base64_decode($tag))) {
+                $info = unpack($format, $tag);
+                $sysid = static::getSystemMessageIdCode();
+                $shorttag = substr($tag, 0, 13);
+                $chksig = substr(hash_hmac('sha1', $shorttag.$id.$sysid,
+                    SECRET_SALT, true), -5);
+                if ($chksig == $info['sig']) {
+                    return $info;
+                }
+            }
+            return false;
+        },
+        );
+
+        // Detect the MessageId version, which should be the first char
+        $rv['version'] = @$parts[0][0];
+        if (!isset($decoders[$rv['version']]))
+            // invalid version code
+            return null;
+
+        // Drop the leading version code
+        list($rv['code'], $rv['id'], $tag) = $parts;
+        $rv['code'] = substr($rv['code'], 1);
+
+        // Verify tag signature and unpack the tag
+        $info = $decoders[$rv['version']]($rv['id'], $tag);
+        if ($info === false)
+            return $rv;
+
+        $rv += $info;
+
+        // Attempt to make the user-id more specific
+        $classes = array(
+            'S' => 'staffId', 'U' => 'userId'
+        );
+        if (isset($classes[$rv['userClass']]))
+            $rv[$classes[$rv['userClass']]] = $rv['uid'];
+
+        // Round-trip detection - the first section is the local
+        // system's message-id code
+        $rv['loopback'] = (0 === strcasecmp($rv['code'],
+            static::getSystemMessageIdCode()));
+
+        return $rv;
+    }
+
+    static function getSystemMessageIdCode() {
+        return substr(str_replace('+', '=',
+            base64_encode(md5('mail'.SECRET_SALT, true))),
+            0, 6);
     }
 
     function send($to, $subject, $message, $options=null) {
@@ -97,22 +284,30 @@ class Mailer {
         require_once (PEAR_DIR.'Mail.php'); // PEAR Mail package
         require_once (PEAR_DIR.'Mail/mime.php'); // PEAR Mail_Mime packge
 
+        $messageId = $this->getMessageId($to, $options);
+
+        if (is_object($to) && is_callable(array($to, 'getEmail'))) {
+            // Add personal name if available
+            if (is_callable(array($to, 'getName'))) {
+                $to = sprintf('"%s" <%s>',
+                    $to->getName()->getOriginal(), $to->getEmail()
+                );
+            }
+            else {
+                $to = $to->getEmail();
+            }
+        }
+
         //do some cleanup
         $to = preg_replace("/(\r\n|\r|\n)/s",'', trim($to));
         $subject = preg_replace("/(\r\n|\r|\n)/s",'', trim($subject));
 
-        /* Message ID - generated for each outgoing email */
-        $messageId = sprintf('<%s-%s-%s>',
-            substr(md5('mail'.SECRET_SALT), -9),
-            Misc::randCode(9),
-            ($this->getEmail()?$this->getEmail()->getEmail():'@osTicketMailer'));
-
         $headers = array (
             'From' => $this->getFromAddress(),
             'To' => $to,
             'Subject' => $subject,
             'Date'=> date('D, d M Y H:i:s O'),
-            'Message-ID' => $messageId,
+            'Message-ID' => "<{$messageId}>",
             'X-Mailer' =>'osTicket Mailer',
         );
 
@@ -155,6 +350,27 @@ class Mailer {
             }
         }
 
+        // Make the best effort to add In-Reply-To and References headers
+        if (isset($options['thread'])
+            && $options['thread'] instanceof ThreadEntry
+        ) {
+            if ($references = $options['thread']->getEmailReferences())
+                $headers += array('References' => $references);
+            if ($irt = $options['thread']->getEmailMessageId()) {
+                // This is an response from an email, like and autoresponse.
+                // Web posts will not have a email message-id
+                $headers += array('In-Reply-To' => $irt);
+            }
+            elseif ($parent = $options['thread']->getParent()) {
+                // 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(),
+                );
+            }
+        }
+
         // Use Mail_mime default initially
         $eol = null;
 
@@ -180,17 +396,16 @@ class Mailer {
         // then assume that it needs html processing to create a valid text
         // body
         $isHtml = true;
-        $mid_token = (isset($options['thread']))
-            ? $options['thread']->asMessageId($to) : '';
         if (!(isset($options['text']) && $options['text'])) {
             $tag = '';
             if ($cfg && $cfg->stripQuotedReply()
                     && (!isset($options['reply-tag']) || $options['reply-tag']))
-                $tag = $cfg->getReplySeparator() . '<br/><br/>';
-            $message = "<div style=\"display:none\"
-                data-mid=\"$mid_token\">$tag</div>$message";
+                $tag = '<div>'.$cfg->getReplySeparator() . '<br/><br/></div>';
+            // Embed the data-mid in such a way that it should be included
+            // in a response
+            $message = "<div data-mid=\"$messageId\">{$tag}{$message}</div>";
             $txtbody = rtrim(Format::html2text($message, 90, false))
-                . ($mid_token ? "\nRef-Mid: $mid_token\n" : '');
+                . ($messageId ? "\nRef-Mid: $messageId\n" : '');
             $mime->setTXTBody($txtbody);
         }
         else {
@@ -211,7 +426,14 @@ class Mailer {
             $self = $this;
             $message = preg_replace_callback('/cid:([\w.-]{32})/',
                 function($match) use ($domain, $mime, $self) {
-                    if (!($file = AttachmentFile::lookup($match[1])))
+                    $file = false;
+                    foreach ($self->attachments as $id=>$F) {
+                        if (strcasecmp($F->getKey(), $match[1]) === 0) {
+                            $file = $F;
+                            break;
+                        }
+                    }
+                    if (!$file)
                         return $match[0];
                     $mime->addHTMLImage($file->getData(),
                         $file->getType(), $file->getName(), false,
@@ -225,12 +447,9 @@ class Mailer {
         }
         //XXX: Attachments
         if(($attachments=$this->getAttachments())) {
-            foreach($attachments as $attachment) {
-                if ($attachment['file_id']
-                        && ($file=AttachmentFile::lookup($attachment['file_id']))) {
-                    $mime->addAttachment($file->getData(),
-                        $file->getType(), $file->getName(),false);
-                }
+            foreach($attachments as $id=>$file) {
+                $mime->addAttachment($file->getData(),
+                    $file->getType(), $file->getName(),false);
             }
         }
 
diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php
index 996192e9bca1798d64c610d2457b71581e680b18..4e0e3c43059aac9c100ea390ffd3622551d5aba6 100644
--- a/include/class.mailfetch.php
+++ b/include/class.mailfetch.php
@@ -721,23 +721,31 @@ class MailFetcher {
         Signal::send('mail.processed', $this, $vars);
 
         $seen = false;
-        if (($thread = ThreadEntry::lookupByEmailHeaders($vars, $seen))
-                && ($t=$thread->getTicket())
-                && ($vars['staffId']
-                    || !$t->isClosed()
-                    || $t->isReopenable())
-                && ($message = $thread->postEmail($vars))) {
+        if (($entry = ThreadEntry::lookupByEmailHeaders($vars, $seen))
+            && ($message = $entry->postEmail($vars))
+        ) {
             if (!$message instanceof ThreadEntry)
                 // Email has been processed previously
                 return $message;
-            $ticket = $message->getTicket();
-        } elseif ($seen) {
+            // NOTE: This might not be a "ticket"
+            $ticket = $message->getThread()->getObject();
+        }
+        elseif ($seen) {
             // Already processed, but for some reason (like rejection), no
             // thread item was created. Ignore the email
             return true;
-        } elseif (($ticket=Ticket::create($vars, $errors, 'Email'))) {
+        }
+        // Allow continuation of thread without initial message or note
+        elseif (($thread = Thread::lookupByEmailHeaders($vars))
+            && ($message = $entry->postEmail($vars))
+        ) {
+            // NOTE: This might not be a "ticket"
+            $ticket = $thread->getObject();
+        }
+        elseif (($ticket=Ticket::create($vars, $errors, 'Email'))) {
             $message = $ticket->getLastMessage();
-        } else {
+        }
+        else {
             //Report success if the email was absolutely rejected.
             if(isset($errors['errno']) && $errors['errno'] == 403) {
                 // Never process this email again!
diff --git a/include/class.organization.php b/include/class.organization.php
index fbaa19df50dd8328b900e2c235421a3148862c15..05b23ca1230eb089dc285f8344116a454f1ca0ac 100644
--- a/include/class.organization.php
+++ b/include/class.organization.php
@@ -370,6 +370,7 @@ class Organization extends OrganizationModel {
             $org->addDynamicData($vars);
         }
 
+        Signal::send('organization.created', $user);
         return $org;
     }
 
diff --git a/include/class.orm.php b/include/class.orm.php
index 17dd050357a5e1b55d24083e8cdea1a5aad59606..26ea31529dea4bb51e68094bee960dc2230cf6f4 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -256,8 +256,8 @@ class VerySimpleModel {
             // replaced in the dirty array
             if (!array_key_exists($field, $this->dirty))
                 $this->dirty[$field] = $old;
-            $this->ht[$field] = $value;
         }
+        $this->ht[$field] = $value;
     }
     function __set($field, $value) {
         return $this->set($field, $value);
@@ -2102,6 +2102,10 @@ class MysqlExecutor {
                 $types .= 'd';
             elseif (is_string($p))
                 $types .= 's';
+            elseif ($p instanceof DateTime) {
+                $types .= 's';
+                $p = $p->format('Y-m-d h:i:s');
+            }
             // TODO: Emit error if param is null
             $ps[] = &$p;
         }
diff --git a/include/class.search.php b/include/class.search.php
index 538aa7fddf51f48650c9a8ed114c28a4ddef3b23..5436ff519333b7ff3e578703fc13672cd194ed34 100644
--- a/include/class.search.php
+++ b/include/class.search.php
@@ -197,7 +197,12 @@ class SearchInterface {
         // Tickets, which can be edited as well
         // Knowledgebase articles (FAQ and canned responses)
         // Users, organizations
-        Signal::connect('model.created', array($this, 'createModel'));
+        Signal::connect('threadentry.created', array($this, 'createModel'));
+        Signal::connect('ticket.created', array($this, 'createModel'));
+        Signal::connect('user.created', array($this, 'createModel'));
+        Signal::connect('organization.created', array($this, 'createModel'));
+        Signal::connect('model.created', array($this, 'createModel'), 'FAQ');
+
         Signal::connect('model.updated', array($this, 'updateModel'));
         #Signal::connect('model.deleted', array($this, 'deleteModel'));
     }
diff --git a/include/class.staff.php b/include/class.staff.php
index 3201db170e8e1364d992e1d753ac2932dc1103eb..87bb3e073c77c3c39b687cc6452459ff128afe98 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -24,7 +24,7 @@ include_once(INCLUDE_DIR.'class.user.php');
 include_once(INCLUDE_DIR.'class.auth.php');
 
 class Staff extends VerySimpleModel
-implements AuthenticatedUser {
+implements AuthenticatedUser, EmailContact {
 
     static $meta = array(
         'table' => STAFF_TABLE,
@@ -640,7 +640,7 @@ implements AuthenticatedUser {
         return $row ? $row[0] : 0;
     }
 
-    function getIdByEmail($email) {
+    static function getIdByEmail($email) {
         $row = static::objects()->filter(array('email' => $email))
             ->values_flat('staff_id')->first();
         return $row ? $row[0] : 0;
diff --git a/include/class.task.php b/include/class.task.php
index 54da8170f882a43bc4d4935835be8ee505f051b6..610c0271fb39452354fe4eb5e2348fbcd6f82fe0 100644
--- a/include/class.task.php
+++ b/include/class.task.php
@@ -225,9 +225,11 @@ class Task extends TaskModel {
         return $this->getThread()->getEntry($id);
     }
 
-    function getThreadEntries($type, $order='') {
-        return $this->getThread()->getEntries(
-                array('type' => $type, 'order' => $order));
+    function getThreadEntries($type=false) {
+        $thread = $this->getThread()->getEntries();
+        if ($type && is_array($type))
+            $thread->filter(array('type__in' => $type));
+        return $thread;
     }
 
     function getForm() {
@@ -437,7 +439,7 @@ class Task extends TaskModel {
         // Create a thread + message.
         $thread = TaskThread::create($task);
         $thread->addDescription($vars);
-        Signal::send('model.created', $task);
+        Signal::send('task.created', $task);
 
         return $task;
     }
@@ -574,10 +576,12 @@ class TaskThread extends ObjectThread {
 
     static function create($task) {
         $id = is_object($task) ? $task->getId() : $task;
-        return parent::create(array(
+        $thread = parent::create(array(
                     'object_id' => $id,
                     'object_type' => ObjectModel::OBJECT_TYPE_TASK
                     ));
+        if ($thread->save())
+            return $thread;
     }
 
 }
diff --git a/include/class.thread.php b/include/class.thread.php
index d3abcf411210dc6154f38d09a8a68a530838f71d..0dba7135cf4c7b455334a91e3f3f1c3ef568ae7f 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -17,7 +17,8 @@
 include_once(INCLUDE_DIR.'class.ticket.php');
 include_once(INCLUDE_DIR.'class.draft.php');
 
-class ThreadModel extends VerySimpleModel {
+//Ticket thread.
+class Thread extends VerySimpleModel {
     static $meta = array(
         'table' => THREAD_TABLE,
         'pk' => array('id'),
@@ -29,66 +30,23 @@ class ThreadModel extends VerySimpleModel {
                 ),
             ),
             'entries' => array(
-                'reverse' => 'ThreadEntryModel.thread',
+                'reverse' => 'ThreadEntry.thread',
             ),
         ),
     );
-}
-
-//Ticket thread.
-class Thread {
-
-    var $ht;
-
-    function Thread($criteria) {
-        $this->load($criteria);
-    }
-
-    function load($criteria=null) {
-
-        if (!$criteria && !($criteria=$this->getId()))
-            return null;
-
-        $sql='SELECT thread.* '
-            .' ,count(DISTINCT a.id) as attachments '
-            .' ,count(DISTINCT entry.id) as entries '
-            .' FROM '.THREAD_TABLE.' thread '
-            .' LEFT JOIN '.THREAD_ENTRY_TABLE.' entry
-                ON (entry.thread_id = thread.id) '
-            .' LEFT JOIN '.ATTACHMENT_TABLE.' a
-                ON (a.object_id=entry.id AND a.`type` = "H") ';
-
-        if (is_numeric($criteria))
-            $sql.= ' WHERE thread.id='.db_input($criteria);
-        else
-            $sql.= sprintf(' WHERE thread.object_id=%d AND
-                    thread.object_type=%s',
-                    $criteria['object_id'],
-                    db_input($criteria['object_type']));
-
-        $sql.= ' GROUP BY thread.id';
 
-        $this->ht = array();
-        if (($res=db_query($sql)) && db_num_rows($res))
-            $this->ht = db_fetch_array($res);
-
-        return ($this->ht);
-    }
-
-    function reload() {
-        return $this->load();
-    }
+    var $_object;
 
     function getId() {
-        return $this->ht['id'];
+        return $this->id;
     }
 
     function getObjectId() {
-        return $this->ht['object_id'];
+        return $this->object_id;
     }
 
     function getObjectType() {
-        return $this->ht['object_type'];
+        return $this->object_type;
     }
 
     function getObject() {
@@ -101,190 +59,351 @@ class Thread {
     }
 
     function getNumAttachments() {
-        return $this->ht['attachments'];
+        return Attachment::objects()->filter(array(
+            'thread_entry__thread' => $this
+        ))->count();
     }
 
     function getNumEntries() {
-        return $this->ht['entries'];
-    }
-
-    function getEntries($criteria) {
-
-        if (!$criteria['order'] || !in_array($criteria['order'], array('DESC','ASC')))
-            $criteria['order'] = 'ASC';
-
-        $sql='SELECT entry.*
-               , COALESCE(user.name,
-                    IF(staff.staff_id,
-                        CONCAT_WS(" ", staff.firstname, staff.lastname),
-                        NULL)) as name '
-            .' ,count(DISTINCT attach.id) as attachments '
-            .' FROM '.THREAD_ENTRY_TABLE.' entry '
-            .' LEFT JOIN '.USER_TABLE.' user
-                ON (entry.user_id=user.id) '
-            .' LEFT JOIN '.STAFF_TABLE.' staff
-                ON (entry.staff_id=staff.staff_id) '
-            .' LEFT JOIN '.ATTACHMENT_TABLE.' attach
-                ON (attach.object_id = entry.id AND attach.`type`="H") '
-            .' WHERE  entry.thread_id='.db_input($this->getId());
-
-        if ($criteria['type'] && is_array($criteria['type']))
-            $sql.=' AND entry.`type` IN ('
-                    .implode(',', db_input($criteria['type'])).')';
-        elseif ($criteria['type'])
-            $sql.=' AND entry.`type` = '.db_input($criteria['type']);
-
-        $sql.=' GROUP BY entry.id '
-             .' ORDER BY entry.created '.$criteria['order'];
-
-        if ($criteria['limit'])
-            $sql.=' LIMIT '.$criteria['limit'];
-
-        $entries = array();
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while($rec=db_fetch_array($res)) {
-                $rec['body'] = ThreadEntryBody::fromFormattedText($rec['body'], $rec['format']);
-                $entries[] = $rec;
-            }
-        }
+        return $this->entries->count();
+    }
 
-        return $entries;
+    function getEntries($criteria=false) {
+        $base = $this->entries->annotate(array(
+            'has_attachments' => SqlAggregate::COUNT('attachments')
+        ));
+        if ($criteria)
+            $base->filter($criteria);
+        return $base;
     }
 
     function getEntry($id) {
         return ThreadEntry::lookup($id, $this->getId());
     }
 
+    /**
+     * postEmail
+     *
+     * After some security and sanity checks, attaches the body and subject
+     * of the message in reply to this thread item
+     *
+     * Parameters:
+     * mailinfo - (array) of information about the email, with at least the
+     *          following keys
+     *      - mid - (string) email message-id
+     *      - name - (string) personal name of email originator
+     *      - email - (string<email>) originating email address
+     *      - subject - (string) email subject line (decoded)
+     *      - body - (string) email message body (decoded)
+     */
+    function postEmail($mailinfo) {
+        global $ost;
+
+        // +==================+===================+=============+
+        // | Orig Thread-Type | Reply Thread-Type | Requires    |
+        // +==================+===================+=============+
+        // | *                | Message (M)       | From: Owner |
+        // | *                | Note (N)          | From: Staff |
+        // | Response (R)     | Message (M)       |             |
+        // | Message (M)      | Response (R)      | From: Staff |
+        // +------------------+-------------------+-------------+
 
-    function deleteAttachments() {
+        if (!$object = $this->getObject()) {
+            // How should someone find this thread?
+            return false;
+        }
+        elseif ($object instanceof Ticket && (
+               !$mailinfo['staffId']
+            && $object->isClosed()
+            && !$object->isReopenable()
+        )) {
+            // Ticket is closed, not reopenable, and email was not submitted
+            // by an agent. Email cannot be submitted
+            return false;
+        }
+
+        // Mail sent by this system will have a message-id format of
+        // <code-random-mailbox@domain.tld>
+        // where code is a predictable string based on the SECRET_SALT of
+        // this osTicket installation. If this incoming mail matches the
+        // code, then it very likely originated from this system and looped
+        @list($code) = explode('-', $mailinfo['mid'], 2);
+        if (0 === strcasecmp(ltrim($code, '<'), substr(md5('mail'.SECRET_SALT), -9))) {
+            // This mail was sent by this system. It was received due to
+            // some kind of mail delivery loop. It should not be considered
+            // a response to an existing thread entry
+            if ($ost) $ost->log(LOG_ERR, _S('Email loop detected'), sprintf(
+                _S('It appears as though &lt;%s&gt; is being used as a forwarded or fetched email account and is also being used as a user / system account. Please correct the loop or seek technical assistance.'),
+                $mailinfo['email']),
+
+                // This is quite intentional -- don't continue the loop
+                false,
+                // Force the message, even if logging is disabled
+                true);
+            return true;
+        }
+
+        $vars = array(
+            'mid' =>    $mailinfo['mid'],
+            'header' => $mailinfo['header'],
+            'poster' => $mailinfo['name'],
+            'origin' => 'Email',
+            'source' => 'Email',
+            'ip' =>     '',
+            'reply_to' => $this,
+            'recipients' => $mailinfo['recipients'],
+            'to-email-id' => $mailinfo['to-email-id'],
+        );
+
+        // XXX: Is this necessary?
+        if ($object instanceof Ticket)
+            $vars['ticketId'] = $object->getId();
+        if ($object instanceof Task)
+            $vars['taskId'] = $object->getId();
+
+        $errors = array();
+
+        if (isset($mailinfo['attachments']))
+            $vars['attachments'] = $mailinfo['attachments'];
+
+        $body = $mailinfo['message'];
+        $poster = $mailinfo['email'];
+
+        // Disambiguate if the user happens also to be a staff member of the
+        // system. The current ticket owner should _always_ post messages
+        // instead of notes or responses
+        if ($mailinfo['userId'] || (
+            $object instanceof Ticket
+            && strcasecmp($mailinfo['email'], $object->getEmail()) == 0
+        )) {
+            $vars['message'] = $body;
+            $vars['userId'] = $mailinfo['userId'] ?: $object->getUserId();
+            $vars['origin'] = 'Email';
+
+            if ($object instanceof Threadable)
+                return $object->postThreadEntry('M', $vars);
+            elseif ($this instanceof ObjectThread)
+                $this->addMessage($vars, $errors);
+            else
+                throw new Exception('Cannot continue discussion with abstract thread');
+        }
+        // XXX: Consider collaborator role
+        elseif ($mailinfo['staffId']
+                || ($mailinfo['staffId'] = Staff::getIdByEmail($mailinfo['email']))) {
+            $vars['staffId'] = $mailinfo['staffId'];
+            $vars['poster'] = Staff::lookup($mailinfo['staffId']);
+            $vars['note'] = $body;
+
+            if ($object instanceof Threadable)
+                return $object->postThreadEntry('N', $vars);
+            elseif ($this instanceof ObjectThread)
+                return $this->addNote($vars, $errors);
+            else
+                throw new Exception('Cannot continue discussion with abstract thread');
+        }
+        elseif (Email::getIdByEmail($mailinfo['email'])) {
+            // Don't process the email -- it came FROM this system
+            return true;
+        }
+        // Support the mail parsing system declaring a thread-type
+        elseif (isset($mailinfo['thread-type'])) {
+            switch ($mailinfo['thread-type']) {
+            case 'N':
+                $vars['note'] = $body;
+                $vars['poster'] = $poster;
+                if ($object instanceof Threadable)
+                    return $object->postThreadEntry('N', $vars);
+                elseif ($this instanceof ObjectThread)
+                    return $this->addNote($vars, $errors);
+                else
+                    throw new Exception('Cannot continue discussion with abstract thread');
+            }
+        }
+        // TODO: Consider security constraints
+        else {
+            //XXX: Are we potentially leaking the email address to
+            // collaborators?
+            // Try not to destroy the format of the body
+            $body->prepend(sprintf('Received From: %s', $mailinfo['email']));
+            $vars['message'] = $body;
+            $vars['userId'] = 0; //Unknown user! //XXX: Assume ticket owner?
+            $vars['origin'] = 'Email';
+            if ($object instanceof Threadable)
+                return $object->postThreadEntry('M', $vars);
+            elseif ($this instanceof ObjectThread)
+                return $this->addMessage($vars, $errors);
+            else
+                throw new Exception('Cannot continue discussion with abstract thread');
+        }
+        // Currently impossible, but indicate that this thread object could
+        // not append the incoming email.
+        return false;
+    }
 
-        // Clear reference table
-        $sql = 'DELETE `a`.* FROM '.ATTACHMENT_TABLE. ' `a` '
-             . 'INNER JOIN '.THREAD_ENTRY_TABLE.' `e`
-                    ON(`e`.id = `a`.object_id AND `a`.`type`= "H") '
-             . ' WHERE `e`.thread_id='.db_input($this->getId());
+    function deleteAttachments() {
+        $deleted = Attachment::objects()->filter(array(
+            'thread_entry__thread' => $this,
+        ))->delete();
 
-        $deleted=0;
-        if (($res=db_query($sql)) && ($deleted=db_affected_rows()))
+        if ($deleted)
             AttachmentFile::deleteOrphans();
 
         return $deleted;
     }
 
+    /**
+     * Function: lookupByEmailHeaders
+     *
+     * Attempt to locate a thread by the email headers. It should be
+     * considered a secondary lookup to ThreadEntry::lookupByEmailHeaders(),
+     * which should find an actual thread entry, which should be possible
+     * for all email communcation which is associated with a thread entry.
+     * The only time where this is useful is for threads which triggered
+     * email communication without a thread entry, for instance, like
+     * tickets created without an initial message.
+     */
+    function lookupByEmailHeaders(&$mailinfo) {
+        $possibles = array();
+        foreach (array('in-reply-to', 'references') as $header) {
+            $matches = array();
+            if (!isset($mailinfo[$header]) || !$mailinfo[$header])
+                continue;
+            // Header may have multiple entries (usually separated by
+            // spaces ( )
+            elseif (!preg_match_all('/<[^>@]+@[^>]+>/', $mailinfo[$header],
+                        $matches))
+                continue;
+
+            // The References header will have the most recent message-id
+            // (parent) on the far right.
+            // @see rfc 1036, section 2.2.5
+            // @see http://www.jwz.org/doc/threading.html
+            $possibles = array_merge($possibles, array_reverse($matches[0]));
+        }
+
+        // Add the message id if it is embedded in the body
+        $match = array();
+        if (preg_match('`(?:data-mid="|Ref-Mid: )([^"\s]*)(?:$|")`',
+                $mailinfo['message'], $match)
+            && !in_array($match[1], $possibles)
+        ) {
+            $possibles[] = $match[1];
+        }
+
+        foreach ($possibles as $mid) {
+            // Attempt to detect the ticket and user ids from the
+            // message-id header. If the message originated from
+            // osTicket, the Mailer class can break it apart. If it came
+            // from this help desk, the 'loopback' property will be set
+            // to true.
+            $mid_info = Mailer::decodeMessageId($mid);
+            if ($mid_info['loopback'] && isset($mid_info['uid'])
+                && @$mid_info['threadId']
+                && ($t = Thread::lookup($mid_info['threadId']))
+            ) {
+                if (@$mid_info['userId']) {
+                    $mailinfo['userId'] = $mid_info['userId'];
+                }
+                elseif (@$mid_info['staffId']) {
+                    $mailinfo['staffId'] = $mid_info['staffId'];
+                }
+                // ThreadEntry was positively identified
+                return $t;
+            }
+        }
+
+        return null;
+    }
+
+
     function delete() {
 
         //Self delete
-        $sql = 'DELETE FROM '.THREAD_TABLE.' WHERE
-            id='.db_input($this->getId());
-
-        if (!db_query($sql) || !db_affected_rows())
+        if (!parent::delete())
             return false;
 
         // Clear email meta data (header..etc)
-        $sql = 'UPDATE '.THREAD_ENTRY_EMAIL_TABLE.' email '
-             . 'INNER JOIN '.THREAD_ENTRY_TABLE.' entry
-                    ON (entry.id = email.thread_entry_id) '
-             . 'SET email.headers = null '
-             . 'WHERE entry.thread_id = '.db_input($this->getId());
-        db_query($sql);
+        ThreadEntryEmailInfo::objects()
+            ->filter(array('thread_entry__thread' => $this))
+            ->update(array('headers' => null));
 
         // Mass delete entries
         $this->deleteAttachments();
-        $sql = 'DELETE FROM '.THREAD_ENTRY_TABLE
-             . ' WHERE thread_id='.db_input($this->getId());
-        db_query($sql);
+
+        $this->entries->delete();
 
         return true;
     }
 
     static function create($vars) {
-
-        if (!$vars || !$vars['object_id'] || !$vars['object_type'])
-            return false;
-
-        $sql = 'INSERT INTO '.THREAD_TABLE.' SET created=NOW() '
-              .', object_id='.db_input($vars['object_id'])
-              .', object_type='.db_input($vars['object_type']);
-
-        if (db_query($sql))
-            return static::lookup(db_insert_id());
-
-        return null;
+        $inst = parent::create($vars);
+        $inst->created = SqlFunction::NOW();
+        return $inst;
     }
+}
 
-    static function lookup($id) {
-
-        return ($id
-                && ($thread = new Thread($id))
-                && $thread->getId()
-                )
-            ? $thread : null;
-    }
+class ThreadEntryEmailInfo extends VerySimpleModel {
+    static $meta = array(
+        'table' => THREAD_ENTRY_EMAIL_TABLE,
+        'pk' => array('id'),
+        'joins' => array(
+            'thread_entry' => array(
+                'constraint' => array('thread_entry_id' => 'ThreadEntry.id'),
+            ),
+        ),
+    );
 }
 
-class ThreadEntryModel extends VerySimpleModel {
+class ThreadEntry extends VerySimpleModel {
     static $meta = array(
         'table' => THREAD_ENTRY_TABLE,
         'pk' => array('id'),
+        'select_related' => array('staff', 'user', 'email_info'),
         'joins' => array(
             'thread' => array(
-                'constraint' => array('thread_id' => 'ThreadModel.id'),
+                'constraint' => array('thread_id' => 'Thread.id'),
+            ),
+            'parent' => array(
+                'constraint' => array('pid' => 'ThreadEntry.id'),
+                'null' => true,
+            ),
+            'children' => array(
+                'reverse' => 'ThreadEntry.parent',
+            ),
+            'email_info' => array(
+                'reverse' => 'ThreadEntryEmailInfo.thread_entry',
+                'list' => false,
             ),
             'attachments' => array(
-                'reverse' => 'AttachmentModel.thread',
+                'reverse' => 'Attachment.thread_entry',
+                'null' => true,
+            ),
+            'staff' => array(
+                'constraint' => array('staff_id' => 'Staff.staff_id'),
+                'null' => true,
+            ),
+            'user' => array(
+                'constraint' => array('user_id' => 'User.id'),
                 'null' => true,
             ),
         ),
     );
-}
-
-class ThreadEntry {
-
-    var $id;
-    var $ht;
 
-    var $thread;
-    var $attachments;
+    var $_headers;
+    var $_thread;
     var $_actions;
+    var $_attachments;
 
-    function ThreadEntry($id, $threadId=0, $type='') {
-        $this->load($id, $threadId, $type);
-    }
-
-    function load($id=0, $threadId=0, $type='') {
-
-        if (!$id && !($id=$this->getId()))
-            return false;
-
-        $sql='SELECT entry.*, email.mid, email.headers '
-            .' ,count(DISTINCT attach.id) as attachments '
-            .' FROM '.THREAD_ENTRY_TABLE.' entry '
-            .' LEFT JOIN '.THREAD_ENTRY_EMAIL_TABLE.' email
-                ON (email.thread_entry_id=entry.id) '
-            .' LEFT JOIN '.ATTACHMENT_TABLE.' attach
-                ON (attach.object_id=entry.id AND attach.`type` = "H") '
-            .' WHERE  entry.id='.db_input($id);
-
-        if ($type)
-            $sql.=' AND entry.type='.db_input($type);
-
-        if ($threadId)
-            $sql.=' AND entry.thread_id='.db_input($threadId);
-
-        $sql.=' GROUP BY entry.id ';
-
-        if (!($res=db_query($sql)) || !db_num_rows($res))
+    function postEmail($mailinfo) {
+        if (!($thread = $this->getThread()))
+            // Kind of hard to continue a discussion without a thread ...
             return false;
 
-        $this->ht = db_fetch_array($res);
-        $this->id = $this->ht['id'];
-        $this->attachments = new GenericAttachments($this->id, 'H');
-
-        return true;
-    }
+        elseif ($this->getEmailMessageId() == $mailinfo['mid'])
+            // Reporting success so the email can be moved or deleted.
+            return true;
 
-    function reload() {
-        return $this->load();
+        return $thread->postEmail($mailinfo);
     }
 
     function getId() {
@@ -292,27 +411,32 @@ class ThreadEntry {
     }
 
     function getPid() {
-        return $this->ht['pid'];
+        return $this->pid;
+    }
+
+    function getParent() {
+        if ($this->getPid())
+            return ThreadEntry::lookup($this->getPid());
     }
 
     function getType() {
-        return $this->ht['type'];
+        return $this->type;
     }
 
     function getSource() {
-        return $this->ht['source'];
+        return $this->source;
     }
 
     function getPoster() {
-        return $this->ht['poster'];
+        return $this->poster;
     }
 
     function getTitle() {
-        return $this->ht['title'];
+        return $this->title;
     }
 
     function getBody() {
-        return ThreadEntryBody::fromFormattedText($this->ht['body'], $this->ht['format']);
+        return ThreadEntryBody::fromFormattedText($this->body, $this->format);
     }
 
     function setBody($body) {
@@ -325,36 +449,37 @@ class ThreadEntry {
                 $body = new TextThreadEntryBody($body);
         }
 
-        $sql='UPDATE '.THREAD_ENTRY_TABLE.' SET updated=NOW()'
-            .',format='.db_input($body->getType())
-            .',body='.db_input((string) $body)
-            .' WHERE id='.db_input($this->getId());
-        return db_query($sql) && db_affected_rows();
+        $this->format = $body->getType();
+        $this->body = (string) $body;
+        return $this->save();
     }
 
     function getCreateDate() {
-        return $this->ht['created'];
+        return $this->created;
     }
 
     function getUpdateDate() {
-        return $this->ht['updated'];
+        return $this->updated;
     }
 
     function getNumAttachments() {
-        return $this->ht['attachments'];
+        return $this->attachments->count();
     }
 
     function getEmailMessageId() {
-        return $this->ht['mid'];
+        if ($this->email_info)
+            return $this->email_info->mid;
     }
 
     function getEmailHeaderArray() {
         require_once(INCLUDE_DIR.'class.mailparse.php');
 
-        if (!isset($this->ht['@headers']))
-            $this->ht['@headers'] = Mail_Parse::splitHeaders($this->ht['headers']);
-
-        return $this->ht['@headers'];
+        if (!isset($this->_headers) && $this->email_info
+            && isset($this->email_info->headers)
+        ) {
+            $this->_headers = Mail_Parse::splitHeaders($this->email_info->headers);
+        }
+        return $this->_headers;
     }
 
     function getEmailReferences($include_mid=true) {
@@ -362,32 +487,11 @@ class ThreadEntry {
         $headers = self::getEmailHeaderArray();
         if (isset($headers['References']) && $headers['References'])
             $references = $headers['References']." ";
-        if ($include_mid)
-            $references .= $this->getEmailMessageId();
+        if ($include_mid && ($mid = $this->getEmailMessageId()))
+            $references .= $mid;
         return $references;
     }
 
-    function getTaggedEmailReferences($prefix, $refId) {
-
-        $ref = "+$prefix".Base32::encode(pack('VV', $this->getId(), $refId));
-
-        $mid = substr_replace($this->getEmailMessageId(),
-                $ref, strpos($this->getEmailMessageId(), '@'), 0);
-
-        return sprintf('%s %s', $this->getEmailReferences(false), $mid);
-    }
-
-    function getEmailReferencesForUser($user) {
-        return $this->getTaggedEmailReferences('u',
-            ($user instanceof Collaborator)
-                ? $user->getUserId()
-                : $user->getId());
-    }
-
-    function getEmailReferencesForStaff($staff) {
-        return $this->getTaggedEmailReferences('s', $staff->getId());
-    }
-
     function getUIDFromEmailReference($ref) {
 
         $info = unpack('Vtid/Vuid',
@@ -399,43 +503,46 @@ class ThreadEntry {
     }
 
     function getThreadId() {
-        return $this->ht['thread_id'];
+        return $this->thread_id;
     }
 
     function getThread() {
 
-        if(!$this->thread && $this->getThreadId())
-            $this->thread = Thread::lookup($this->getThreadId());
+        if (!isset($this->_thread) && $this->thread_id)
+            // TODO: Consider typing the thread based on its type field
+            $this->_thread = ObjectThread::lookup($this->getThreadId());
 
-        return $this->thread;
+        return $this->_thread;
     }
 
     function getStaffId() {
-        return $this->ht['staff_id'];
+        return isset($this->staff_id) ? $this->staff_id : 0;
     }
 
     function getStaff() {
-
-        if(!$this->staff && $this->getStaffId())
-            $this->staff = Staff::lookup($this->getStaffId());
-
         return $this->staff;
     }
 
     function getUserId() {
-        return $this->ht['user_id'];
+        return isset($this->user_id) ? $this->user_id : 0;
     }
 
     function getUser() {
+        return $this->user;
+    }
 
-        if (!isset($this->user))
-            $this->user = User::lookup($this->getUserId());
+    function getName() {
+        if ($this->staff_id)
+            return $this->staff->getName();
+        if ($this->user_id)
+            return $this->user->getName();
 
-        return $this->user;
+        return $this->poster;
     }
 
     function getEmailHeader() {
-        return $this->ht['headers'];
+        if ($this->email_info)
+            return $this->email_info->headers;
     }
 
     function isAutoReply() {
@@ -472,8 +579,8 @@ class ThreadEntry {
                 continue;
 
             if(!$file['error']
-                    && ($id=AttachmentFile::upload($file))
-                    && $this->saveAttachment($id))
+                    && ($F=AttachmentFile::upload($file))
+                    && $this->saveAttachment($F))
                 $uploaded[]=$id;
             else {
                 if(!$file['error'])
@@ -514,8 +621,8 @@ class ThreadEntry {
         if(!$attachment || !is_array($attachment))
             return null;
 
-        $id=0;
-        if ($attachment['error'] || !($id=$this->saveAttachment($attachment))) {
+        $A=null;
+        if ($attachment['error'] || !($A=$this->saveAttachment($attachment))) {
             $error = $attachment['error'];
             if(!$error)
                 $error = sprintf(_S('Unable to import attachment - %s'),
@@ -525,7 +632,7 @@ class ThreadEntry {
                     _S('File Import Error'), $error, _S('SYSTEM'), false);
         }
 
-        return $id;
+        return $A;
     }
 
    /*
@@ -536,28 +643,47 @@ class ThreadEntry {
 
         $inline = is_array($file) && @$file['inline'];
 
-        return $this->attachments->save($file, $inline);
+        if (is_numeric($file))
+            $fileId = $file;
+        elseif ($file instanceof AttachmentFile)
+            $fileId = $file->getId();
+        elseif ($F = AttachmentFile::create($file))
+            $fileId = $F->getId();
+        elseif (is_array($file) && isset($file['id']))
+            $fileId = $file['id'];
+        else
+            return false;
+
+        $att = Attachment::create(array(
+            'type' => 'H',
+            'object_id' => $this->getId(),
+            'file_id' => $fileId,
+            'inline' => $inline ? 1 : 0,
+        ));
+        if (!$att->save())
+            return false;
+        return $att;
     }
 
     function saveAttachments($files) {
-        $ids=array();
+        $attachments = array();
         foreach ($files as $file)
-           if (($id=$this->saveAttachment($file)))
-               $ids[] = $id;
+           if (($A = $this->saveAttachment($file)))
+               $attachments[] = $A;
 
-        return $ids;
+        return $attachments;
     }
 
     function getAttachments() {
-        return $this->attachments->getAll(false);
+        return $this->attachments;
     }
 
     function getAttachmentUrls() {
         $json = array();
-        foreach ($this->getAttachments() as $att) {
-            $json[$att['key']] = array(
-                'download_url' => $att['download_url'],
-                'filename' => $att['name'],
+        foreach ($this->attachments as $att) {
+            $json[$att->file->getKey()] = array(
+                'download_url' => $att->file->getDownloadUrl(),
+                'filename' => $att->file->name,
             );
         }
 
@@ -565,146 +691,30 @@ class ThreadEntry {
     }
 
     function getAttachmentsLinks($file='attachment.php', $target='_blank', $separator=' ') {
+        // TODO: Move this to the respective UI templates
 
         $str='';
-        foreach ($this->getAttachments() as $att ) {
-            if ($att['inline']) continue;
+        foreach ($this->attachments as $att ) {
+            if ($att->inline) continue;
             $size = '';
-            if ($att['size'])
-                $size=sprintf('<em>(%s)</em>', Format::file_size($att['size']));
+            if ($att->file->size)
+                $size=sprintf('<em>(%s)</em>', Format::file_size($att->file->size));
 
-            $str.=sprintf('<a class="Icon file no-pjax" href="%s" target="%s">%s</a>%s&nbsp;%s',
-                    $att['download_url'], $target, Format::htmlchars($att['name']), $size, $separator);
+            $str .= sprintf(
+                '<a class="Icon file no-pjax" href="%s" target="%s">%s</a>%s&nbsp;%s',
+                $att->file->getDownloadUrl(), $target,
+                Format::htmlchars($att->file->name), $size, $separator);
         }
 
         return $str;
     }
-    /**
-     * postEmail
-     *
-     * After some security and sanity checks, attaches the body and subject
-     * of the message in reply to this thread item
-     *
-     * Parameters:
-     * mailinfo - (array) of information about the email, with at least the
-     *          following keys
-     *      - mid - (string) email message-id
-     *      - name - (string) personal name of email originator
-     *      - email - (string<email>) originating email address
-     *      - subject - (string) email subject line (decoded)
-     *      - body - (string) email message body (decoded)
-     */
-    function postEmail($mailinfo) {
-        global $ost;
-
-        // +==================+===================+=============+
-        // | Orig Thread-Type | Reply Thread-Type | Requires    |
-        // +==================+===================+=============+
-        // | *                | Message (M)       | From: Owner |
-        // | *                | Note (N)          | From: Staff |
-        // | Response (R)     | Message (M)       |             |
-        // | Message (M)      | Response (R)      | From: Staff |
-        // +------------------+-------------------+-------------+
-
-        if (!$ticket = $this->getTicket())
-            // Kind of hard to continue a discussion without a ticket ...
-            return false;
-
-        // Make sure the email is NOT already fetched... (undeleted emails)
-        elseif ($this->getEmailMessageId() == $mailinfo['mid'])
-            // Reporting success so the email can be moved or deleted.
-            return true;
-
-        // Mail sent by this system will have a message-id format of
-        // <code-random-mailbox@domain.tld>
-        // where code is a predictable string based on the SECRET_SALT of
-        // this osTicket installation. If this incoming mail matches the
-        // code, then it very likely originated from this system and looped
-        @list($code) = explode('-', $mailinfo['mid'], 2);
-        if (0 === strcasecmp(ltrim($code, '<'), substr(md5('mail'.SECRET_SALT), -9))) {
-            // This mail was sent by this system. It was received due to
-            // some kind of mail delivery loop. It should not be considered
-            // a response to an existing thread entry
-            if ($ost) $ost->log(LOG_ERR, _S('Email loop detected'), sprintf(
-                _S('It appears as though &lt;%s&gt; is being used as a forwarded or fetched email account and is also being used as a user / system account. Please correct the loop or seek technical assistance.'),
-                $mailinfo['email']),
-
-                // This is quite intentional -- don't continue the loop
-                false,
-                // Force the message, even if logging is disabled
-                true);
-            return true;
-        }
-
-        $vars = array(
-            'mid' =>    $mailinfo['mid'],
-            'header' => $mailinfo['header'],
-            'ticketId' => $ticket->getId(),
-            'poster' => $mailinfo['name'],
-            'origin' => 'Email',
-            'source' => 'Email',
-            'ip' =>     '',
-            'reply_to' => $this,
-            'recipients' => $mailinfo['recipients'],
-            'to-email-id' => $mailinfo['to-email-id'],
-        );
-        $errors = array();
-
-        if (isset($mailinfo['attachments']))
-            $vars['attachments'] = $mailinfo['attachments'];
-
-        $body = $mailinfo['message'];
-
-        // Disambiguate if the user happens also to be a staff member of the
-        // system. The current ticket owner should _always_ post messages
-        // instead of notes or responses
-        if ($mailinfo['userId']
-                || strcasecmp($mailinfo['email'], $ticket->getEmail()) == 0) {
-            $vars['message'] = $body;
-            $vars['userId'] = $mailinfo['userId'] ? $mailinfo['userId'] : $ticket->getUserId();
-            return $ticket->postMessage($vars, 'Email');
-        }
-        // XXX: Consider collaborator role
-        elseif ($mailinfo['staffId']
-                || ($mailinfo['staffId'] = Staff::getIdByEmail($mailinfo['email']))) {
-            $vars['staffId'] = $mailinfo['staffId'];
-            $poster = Staff::lookup($mailinfo['staffId']);
-            $vars['note'] = $body;
-            return $ticket->postNote($vars, $errors, $poster);
-        }
-        elseif (Email::getIdByEmail($mailinfo['email'])) {
-            // Don't process the email -- it came FROM this system
-            return true;
-        }
-        // Support the mail parsing system declaring a thread-type
-        elseif (isset($mailinfo['thread-type'])) {
-            switch ($mailinfo['thread-type']) {
-            case 'N':
-                $vars['note'] = $body;
-                $poster = $mailinfo['email'];
-                return $ticket->postNote($vars, $errors, $poster);
-            }
-        }
-        // TODO: Consider security constraints
-        else {
-            //XXX: Are we potentially leaking the email address to
-            // collaborators?
-            $vars['message'] = sprintf("Received From: %s\n\n%s",
-                $mailinfo['email'], $body);
-            $vars['userId'] = 0; //Unknown user! //XXX: Assume ticket owner?
-            return $ticket->postMessage($vars, 'Email');
-        }
-        // Currently impossible, but indicate that this thread object could
-        // not append the incoming email.
-        return false;
-    }
 
     /* Returns file names with id as key */
     function getFiles() {
 
         $files = array();
-        foreach($this->getAttachments() as $attachment)
-            $files[$attachment['file_id']] = $attachment['name'];
+        foreach($this->attachments as $attachment)
+            $files[$attachment->file_id] = $attachment->file->name;
 
         return $files;
     }
@@ -733,13 +743,15 @@ class ThreadEntry {
         if (!$id || !$mid)
             return false;
 
-        $sql='INSERT INTO '.THREAD_ENTRY_EMAIL_TABLE
-            .' SET thread_entry_id='.db_input($id)
-            .', mid='.db_input($mid);
+        $this->email_info = ThreadEntryEmailInfo::create(array(
+            'thread_entry_id' => $id,
+            'mid' => $mid,
+        ));
+
         if ($header)
-            $sql .= ', headers='.db_input($header);
+            $this->email_info->headers = trim($header);
 
-        return db_query($sql) ? db_insert_id() : 0;
+        return $this->email_info->save();
     }
 
     /* variables */
@@ -769,14 +781,6 @@ class ThreadEntry {
         return false;
     }
 
-    static function lookup($id, $tid=0, $type='') {
-        return ($id
-                && is_numeric($id)
-                && ($e = new ThreadEntry($id, $tid, $type))
-                && $e->getId()==$id
-                )?$e:null;
-    }
-
     /**
      * Parameters:
      * mailinfo (hash<String>) email header information. Must include keys
@@ -793,16 +797,15 @@ class ThreadEntry {
     function lookupByEmailHeaders(&$mailinfo, &$seen=false) {
         // Search for messages using the References header, then the
         // in-reply-to header
-        $search = 'SELECT thread_entry_id, mid FROM '.THREAD_ENTRY_EMAIL_TABLE
-               . ' WHERE mid=%s '
-               . ' ORDER BY thread_entry_id DESC';
-
-        if (list($id, $mid) = db_fetch_row(db_query(
-                sprintf($search, db_input($mailinfo['mid']))))) {
+        if ($entry = ThreadEntry::objects()
+            ->filter(array('email_info__mid' => $mailinfo['mid']))
+            ->first()
+        ) {
             $seen = true;
-            return ThreadEntry::lookup($id);
+            return $entry;
         }
 
+        $possibles = array();
         foreach (array('in-reply-to', 'references') as $header) {
             $matches = array();
             if (!isset($mailinfo[$header]) || !$mailinfo[$header])
@@ -817,41 +820,71 @@ class ThreadEntry {
             // (parent) on the far right.
             // @see rfc 1036, section 2.2.5
             // @see http://www.jwz.org/doc/threading.html
-            $thread = null;
-            foreach (array_reverse($matches[0]) as $mid) {
-                //Try to determine if it's a reply to a tagged email.
-                $ref = null;
-                if (strpos($mid, '+')) {
-                    list($left, $right) = explode('@',$mid);
-                    list($left, $ref) = explode('+', $left);
-                    $mid = "$left@$right";
+            $possibles = array_merge($possibles, array_reverse($matches[0]));
+        }
+
+        // Add the message id if it is embedded in the body
+        $match = array();
+        if (preg_match('`(?:data-mid="|Ref-Mid: )([^"\s]*)(?:$|")`',
+                $mailinfo['message'], $match)
+            && !in_array($match[1], $possibles)
+        ) {
+            $possibles[] = $match[1];
+        }
+
+        $thread = null;
+        foreach ($possibles as $mid) {
+            //Try to determine if it's a reply to a tagged email.
+            $ref = null;
+            if (strpos($mid, '+')) {
+                list($left, $right) = explode('@',$mid);
+                list($left, $ref) = explode('+', $left);
+                $mid = "$left@$right";
+            }
+            $entries = ThreadEntry::objects()
+                ->filter(array('email_info__mid' => $mid));
+            foreach ($entries as $t) {
+                // Capture the first match thread item
+                if (!$thread)
+                    $thread = $t;
+                // We found a match  - see if we can ID the user.
+                // XXX: Check access of ref is enough?
+                if ($ref && ($uid = $t->getUIDFromEmailReference($ref))) {
+                    if ($ref[0] =='s') //staff
+                        $mailinfo['staffId'] = $uid;
+                    else // user or collaborator.
+                        $mailinfo['userId'] = $uid;
+
+                    // Best possible case — found the thread and the
+                    // user
+                    return $t;
                 }
-                $res = db_query(sprintf($search, db_input($mid)));
-                while (list($id) = db_fetch_row($res)) {
-                    if (!($t = ThreadEntry::lookup($id)))
-                        continue;
-                    // Capture the first match thread item
-                    if (!$thread)
-                        $thread = $t;
-                    // We found a match  - see if we can ID the user.
-                    // XXX: Check access of ref is enough?
-                    if ($ref && ($uid = $t->getUIDFromEmailReference($ref))) {
-                        if ($ref[0] =='s') //staff
-                            $mailinfo['staffId'] = $uid;
-                        else // user or collaborator.
-                            $mailinfo['userId'] = $uid;
-
-                        // Best possible case — found the thread and the
-                        // user
-                        return $t;
-                    }
+            }
+            // Attempt to detect the ticket and user ids from the
+            // message-id header. If the message originated from
+            // osTicket, the Mailer class can break it apart. If it came
+            // from this help desk, the 'loopback' property will be set
+            // to true.
+            $mid_info = Mailer::decodeMessageId($mid);
+            if ($mid_info['loopback'] && isset($mid_info['uid'])
+                && @$mid_info['entryId']
+                && ($t = ThreadEntry::lookup($mid_info['entryId']))
+                && ($t->thread_id == $mid_info['threadId'])
+            ) {
+                if (@$mid_info['userId']) {
+                    $mailinfo['userId'] = $mid_info['userId'];
+                }
+                elseif (@$mid_info['staffId']) {
+                    $mailinfo['staffId'] = $mid_info['staffId'];
                 }
+                // ThreadEntry was positively identified
+                return $t;
             }
-            // Second best case — found a thread but couldn't identify the
-            // user from the header. Return the first thread entry matched
-            if ($thread)
-                return $thread;
         }
+        // Second best case — found a thread but couldn't identify the
+        // user from the header. Return the first thread entry matched
+        if ($thread)
+            return $thread;
 
         // Search for ticket by the [#123456] in the subject line
         // This is the last resort -  emails must match to avoid message
@@ -880,6 +913,8 @@ class ThreadEntry {
         }
 
         // Search for the message-id token in the body
+        // *DEPRECATED* the current algo on outgoing mail will use
+        // Mailer::getMessageId as the message id tagged here
         if (preg_match('`(?:data-mid="|Ref-Mid: )([^"\s]*)(?:$|")`',
                 $mailinfo['message'], $match))
             if ($thread = ThreadEntry::lookupByRefMessageId($match[1],
@@ -891,7 +926,9 @@ class ThreadEntry {
 
     /**
      * Find a thread entry from a message-id created from the
-     * ::asMessageId() method
+     * ::asMessageId() method.
+     *
+     * *DEPRECATED* use Mailer::decodeMessageId() instead
      */
     function lookupByRefMessageId($mid, $from) {
         $mid = trim($mid, '<>');
@@ -909,36 +946,7 @@ class ThreadEntry {
         if (!$thread)
             return false;
 
-        if (0 === strcasecmp($thread->asMessageId($from, $ver), $mid))
-            return $thread;
-    }
-
-    /**
-     * Get an email message-id that can be used to represent this thread
-     * entry. The same message-id can be passed to ::lookupByRefMessageId()
-     * to find this thread entry
-     *
-     * Formats:
-     * Initial (version <null>)
-     * <$:b32(thread-id)$:md5(to-addr.ticket-num.ticket-id)@:md5(url)>
-     *      thread-id - thread-id, little-endian INT, packed
-     *      :b32() - base32 encoded
-     *      to-addr - individual email recipient
-     *      ticket-num - external ticket number
-     *      ticket-id - internal ticket id
-     *      :md5() - last 10 hex chars of MD5 sum
-     *      url - helpdesk URL
-     */
-    function asMessageId($to, $version=false) {
-        global $ost;
-
-        $domain = md5($ost->getConfig()->getURL());
-        $ticket = $this->getThread()->getObject();
-        return sprintf('$%s$%s@%s',
-            base64_encode(pack('V', $this->getId())),
-            substr(md5($to . $ticket->getNumber() . $ticket->getId()), -10),
-            substr($domain, -10)
-        );
+        return $thread;
     }
 
     //new entry ... we're trusting the caller to check validity of the data.
@@ -987,36 +995,36 @@ class ThreadEntry {
         if ($poster && is_object($poster))
             $poster = (string) $poster;
 
-        $sql=' INSERT INTO '.THREAD_ENTRY_TABLE.' SET `created` = NOW() '
-            .' ,`type` = '.db_input($vars['type'])
-            .' ,`thread_id` = '.db_input($vars['threadId'])
-            .' ,`title` = '.db_input(Format::sanitize($vars['title'], true))
-            .' ,`format` = '.db_input($vars['body']->getType())
-            .' ,`staff_id` = '.db_input($vars['staffId'])
-            .' ,`user_id` = '.db_input($vars['userId'])
-            .' ,`poster` = '.db_input($poster)
-            .' ,`source` = '.db_input($vars['source']);
+        $entry = parent::create(array(
+            'created' => SqlFunction::NOW(),
+            'type' => $vars['type'],
+            'thread_id' => $vars['threadId'],
+            'title' => Format::sanitize($vars['title'], true),
+            'format' => $vars['body']->getType(),
+            'staff_id' => $vars['staffId'],
+            'user_id' => $vars['userId'],
+            'poster' => $poster,
+            'source' => $vars['source'],
+        ));
 
         if (!isset($vars['attachments']) || !$vars['attachments'])
             // Otherwise, body will be configured in a block below (after
             // inline attachments are saved and updated in the database)
-            $sql.=' ,body='.db_input($body);
+            $entry->body = $body;
 
         if (isset($vars['pid']))
-            $sql.=' ,pid='.db_input($vars['pid']);
+            $entry->pid = $vars['pid'];
         // Check if 'reply_to' is in the $vars as the previous ThreadEntry
         // instance. If the body of the previous message is found in the new
         // body, strip it out.
         elseif (isset($vars['reply_to'])
                 && $vars['reply_to'] instanceof ThreadEntry)
-            $sql.=' ,pid='.db_input($vars['reply_to']->getId());
+            $entry->pid = $vars['reply_to']->getId();
 
         if ($vars['ip_address'])
-            $sql.=' ,ip_address='.db_input($vars['ip_address']);
+            $entry->ip_address = $vars['ip_address'];
 
-        //echo $sql;
-        if (!db_query($sql)
-                || !($entry=self::lookup(db_insert_id(), $vars['threadId'])))
+        if (!$entry->save())
             return false;
 
         /************* ATTACHMENTS *****************/
@@ -1050,31 +1058,23 @@ class ThreadEntry {
                 }
             }
 
-            $sql = 'UPDATE '.THREAD_ENTRY_TABLE
-                .' SET body='.db_input($body)
-                .' WHERE `id`='.db_input($entry->getId());
-
-            if (!db_query($sql) || !db_affected_rows())
+            $entry->body = $body;
+            if (!$entry->save())
                 return false;
         }
 
-        // Email message id (required for all thread posts)
-        if (!isset($vars['mid']))
-            $vars['mid'] = sprintf('<%s@%s>',
-                    Misc::randCode(24), substr(md5($cfg->getUrl()), -10));
-
+        // Save mail message id, if available
         $entry->saveEmailInfo($vars);
 
         // Inline images (attached to the draft)
         $entry->saveAttachments(Draft::getAttachmentIds($body));
 
-        Signal::send('model.created', $entry);
-
+        Signal::send('threadentry.created', $this);
         return $entry;
     }
 
     static function add($vars) {
-        return ($entry=self::create($vars)) ? $entry->getId() : 0;
+        return self::create($vars);
     }
 
     // Extensible thread entry actions ------------------------
@@ -1266,6 +1266,10 @@ class TextThreadEntryBody extends ThreadEntryBody {
         return Format::stripEmptyLines($this->body);
     }
 
+    function prepend($what) {
+        $this->body = $what . "\n\n" . $this->body;
+    }
+
     function display($output=false) {
         if ($this->isEmpty())
             return '(empty)';
@@ -1313,6 +1317,10 @@ class HtmlThreadEntryBody extends ThreadEntryBody {
         return Format::searchable($body);
     }
 
+    function prepend($what) {
+        $this->body = sprintf('<div>%s<br/><br/></div>%s', $what, $this->body);
+    }
+
     function display($output=false) {
         if ($this->isEmpty())
             return '(empty)';
@@ -1334,16 +1342,12 @@ class MessageThreadEntry extends ThreadEntry {
 
     const ENTRY_TYPE = 'M';
 
-    function MessageThreadEntry($id, $threadId=0) {
-        parent::ThreadEntry($id, $threadId, self::ENTRY_TYPE);
-    }
-
     function getSubject() {
         return $this->getTitle();
     }
 
     static function create($vars, &$errors) {
-        return self::lookup(self::add($vars, $errors));
+        return static::add($vars, $errors);
     }
 
     static function add($vars, &$errors) {
@@ -1365,16 +1369,6 @@ class MessageThreadEntry extends ThreadEntry {
 
         return parent::add($vars);
     }
-
-    static function lookup($id, $tid=0) {
-
-        return ($id
-                && is_numeric($id)
-                && ($m = new MessageThreadEntry($id, $tid))
-                && $m->getId()==$id
-                )?$m:null;
-    }
-
 }
 
 /* thread entry of type response */
@@ -1382,10 +1376,6 @@ class ResponseThreadEntry extends ThreadEntry {
 
     const ENTRY_TYPE = 'R';
 
-    function ResponseThreadEntry($id, $threadId=0) {
-        parent::ThreadEntry($id, $threadId, self::ENTRY_TYPE);
-    }
-
     function getSubject() {
         return $this->getTitle();
     }
@@ -1395,7 +1385,7 @@ class ResponseThreadEntry extends ThreadEntry {
     }
 
     static function create($vars, &$errors) {
-        return self::lookup(self::add($vars, $errors));
+        return static::add($vars, $errors);
     }
 
     static function add($vars, &$errors) {
@@ -1419,31 +1409,18 @@ class ResponseThreadEntry extends ThreadEntry {
 
         return parent::add($vars);
     }
-
-    static function lookup($id, $tid=0) {
-
-        return ($id
-                && is_numeric($id)
-                && ($r = new ResponseThreadEntry($id, $tid))
-                && $r->getId()==$id
-                )?$r:null;
-    }
 }
 
 /* Thread entry of type note (Internal Note) */
 class NoteThreadEntry extends ThreadEntry {
     const ENTRY_TYPE = 'N';
 
-    function NoteThreadEntry($id, $threadId=0) {
-        parent::ThreadEntry($id, $threadId, self::ENTRY_TYPE);
-    }
-
     function getMessage() {
         return $this->getBody();
     }
 
     static function create($vars, &$errors) {
-        return self::lookup(self::add($vars, $errors));
+        return self::add($vars, $errors);
     }
 
     static function add($vars, &$errors) {
@@ -1462,68 +1439,66 @@ class NoteThreadEntry extends ThreadEntry {
 
         return parent::add($vars);
     }
-
-    static function lookup($id, $tid=0) {
-
-        return ($id
-                && is_numeric($id)
-                && ($n = new NoteThreadEntry($id, $tid))
-                && $n->getId()==$id
-                )?$n:null;
-    }
 }
 
 // Object specific thread utils.
 class ObjectThread extends Thread {
     private $_entries = array();
 
-    function __construct($id) {
+    static $types = array(
+        ObjectModel::OBJECT_TYPE_TASK => 'TaskThread',
+    );
+
+    var $counts;
 
-        parent::__construct($id);
+    function getCounts() {
+        if (!isset($this->counts) && $this->getId()) {
+            $this->counts = array();
 
-        if ($this->getId()) {
-            $sql= ' SELECT `type`, count(DISTINCT e.id) as count '
-                 .' FROM '.THREAD_TABLE. ' t '
-                 .' INNER JOIN '.THREAD_ENTRY_TABLE. ' e ON (e.thread_id = t.id) '
-                 .' WHERE t.id='.db_input($this->getId())
-                 .' GROUP BY e.`type`';
+            $stuff = static::objects()->annotate(array(
+                'count' => SqlAggregate::COUNT('thread_entry', true)
+            ))
+            ->values_flat('thread_entry__type', 'count');
+            print $stuff;
 
-            if (($res=db_query($sql)) && db_num_rows($res)) {
-                while ($row=db_fetch_row($res))
-                    $this->_entries[$row[0]] = $row[1];
+            foreach ($stuff as $row) {
+                list($type, $count) = $row;
+                $this->counts[$type] = $count;
             }
         }
     }
 
     function getNumMessages() {
-        return $this->_entries[MessageThreadEntry::ENTRY_TYPE];
+        $this->getCounts();
+        return $this->counts[MessageThreadEntry::ENTRY_TYPE];
     }
 
     function getNumResponses() {
-        return $this->_entries[ResponseThreadEntry::ENTRY_TYPE];
+        $this->getCounts();
+        return $this->counts[ResponseThreadEntry::ENTRY_TYPE];
     }
 
     function getNumNotes() {
-        return $this->_entries[NoteThreadEntry::ENTRY_TYPE];
+        $this->getCounts();
+        return $this->counts[NoteThreadEntry::ENTRY_TYPE];
     }
 
     function getMessages() {
-        return $this->getEntries(array(
-                    'type' => MessageThreadEntry::ENTRY_TYPE));
+        return $this->entries->filter(array(
+            'type' => MessageThreadEntry::ENTRY_TYPE
+        ));
     }
 
     function getLastMessage() {
-
-        $criteria = array(
-                'type'  => MessageThreadEntry::ENTRY_TYPE,
-                'order' => 'DESC',
-                'limit' => 1);
-
-        return $this->getEntry($criteria);
+        return $this->entries->filter(array(
+            'type' => MessageThreadEntry::ENTRY_TYPE
+        ))
+        ->order_by('-id')
+        ->first();
     }
 
     function getEntry($var) {
-
+        // XXX: PUNT
         if (is_numeric($var))
             $id = $var;
         else {
@@ -1537,13 +1512,15 @@ class ObjectThread extends Thread {
     }
 
     function getResponses() {
-        return $this->getEntries(array(
-                    'type' => ResponseThreadEntry::ENTRY_TYPE));
+        return $this->entries->filter(array(
+            'type' => ResponseThreadEntry::ENTRY_TYPE
+        ));
     }
 
     function getNotes() {
-        return $this->getEntries(array(
-                    'type' => NoteThreadEntry::ENTRY_TYPE));
+        return $this->entries->filter(array(
+            'type' => NoteThreadEntry::ENTRY_TYPE
+        ));
     }
 
     function addNote($vars, &$errors) {
@@ -1572,33 +1549,36 @@ class ObjectThread extends Thread {
     function getVar($name) {
         switch ($name) {
         case 'original':
-            $entries = $this->getEntries(array(
-                        'type'  => MessageThreadEntry::ENTRY_TYPE,
-                        'order' => 'ASC',
-                        'limit' => 1));
-            if ($entries && $entries[0])
-                return (string) $entries[0]['body'];
+            $entry = $this->entries->filter(array(
+                    'type'  => MessageThreadEntry::ENTRY_TYPE,
+                ))
+                ->order_by('id')
+                ->first();
+            if ($entry)
+                return $entry->getBody();
 
             break;
         case 'last_message':
         case 'lastmessage':
-            $entries = $this->getEntries(array(
-                        'type'  => MessageThreadEntry::ENTRY_TYPE,
-                        'order' => 'DESC',
-                        'limit' => 1));
-            if ($entries && $entries[0])
-                return (string) $entries[0]['body'];
+            $entry = $this->getLastMessage();
+            if ($entry)
+                return $entry->getBody();
 
             break;
         }
     }
 
-    static function lookup($criteria) {
+    static function lookup($criteria, $type=false) {
+        if (!$type)
+            return parent::lookup($criteria);
+
+        $class = false;
+        if (isset(self::$types[$type]))
+            $class = self::$types[$type];
+        if (!class_exists($class))
+            $class = get_called_class();
 
-        return ($criteria
-                && ($t= new static($criteria))
-                && $t->getId()
-                ) ? $t : null;
+        return $class::lookup($criteria);
     }
 }
 
@@ -1607,10 +1587,12 @@ class TicketThread extends ObjectThread {
 
     static function create($ticket) {
         $id = is_object($ticket) ? $ticket->getId() : $ticket;
-        return parent::create(array(
+        $thread = parent::create(array(
                     'object_id' => $id,
                     'object_type' => ObjectModel::OBJECT_TYPE_TICKET
                     ));
+        if ($thread->save())
+            return $thread;
     }
 }
 
@@ -1689,4 +1671,8 @@ abstract class ThreadEntryAction {
         );
     }
 }
+
+interface Threadable {
+    function postThreadEntry($type, $vars);
+}
 ?>
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 0b3c6574107193faf282fccb2fe34a0b32ab026b..a7a91190af8d205e386d644f2624a84359fe574f 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -75,7 +75,7 @@ class TicketModel extends VerySimpleModel {
                 'null' => true,
             ),
             'thread' => array(
-                'reverse' => 'ThreadModel.ticket',
+                'reverse' => 'Thread.ticket',
                 'list' => false,
                 'null' => true,
             ),
@@ -217,7 +217,7 @@ TicketCData::$meta['table'] = TABLE_PREFIX . 'ticket__cdata';
 
 
 class Ticket
-implements RestrictedAccess {
+implements RestrictedAccess, Threadable {
 
     var $id;
     var $number;
@@ -818,7 +818,7 @@ implements RestrictedAccess {
     }
 
     function getThreadCount() {
-        return $this->getNumMessages() + $this->getNumResponses();
+        return $this->getClientThread()->count();
     }
 
     function getNumMessages() {
@@ -834,15 +834,15 @@ implements RestrictedAccess {
     }
 
     function getMessages() {
-        return $this->getThreadEntries('M');
+        return $this->getThreadEntries(array('M'));
     }
 
     function getResponses() {
-        return $this->getThreadEntries('R');
+        return $this->getThreadEntries(array('R'));
     }
 
     function getNotes() {
-        return $this->getThreadEntries('N');
+        return $this->getThreadEntries(array('N'));
     }
 
     function getClientThread() {
@@ -853,9 +853,11 @@ implements RestrictedAccess {
         return $this->getThread()->getEntry($id);
     }
 
-    function getThreadEntries($type, $order='') {
-        return $this->getThread()->getEntries(
-                array( 'type' => $type, 'order' => $order));
+    function getThreadEntries($type=false) {
+        $thread = $this->getThread()->getEntries();
+        if ($type && is_array($type))
+            $thread->filter(array('type__in' => $type));
+        return $thread;
     }
 
     //Collaborators
@@ -1271,6 +1273,11 @@ implements RestrictedAccess {
                 'thread'=>$message
             );
         }
+        else {
+            $options += array(
+                'thread' => $this->getThread(),
+            );
+        }
 
         //Send auto response - if enabled.
         if($autorespond
@@ -1284,7 +1291,7 @@ implements RestrictedAccess {
                           'signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')
                     );
 
-            $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body'],
+            $email->sendAutoReply($this->getOwner(), $msg['subj'], $msg['body'],
                 null, $options);
         }
 
@@ -1330,7 +1337,7 @@ implements RestrictedAccess {
             foreach( $recipients as $k=>$staff) {
                 if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
                 $alert = $this->replaceVars($msg, array('recipient' => $staff));
-                $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null, $options);
+                $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
                 $sentlist[] = $staff->getEmail();
             }
         }
@@ -1359,7 +1366,7 @@ implements RestrictedAccess {
             $msg = $this->replaceVars($msg->asArray(),
                         array('signature' => ($dept && $dept->isPublic())?$dept->getSignature():''));
 
-            $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body']);
+            $email->sendAutoReply($this->getOwner(), $msg['subj'], $msg['body']);
         }
 
         $user = $this->getOwner();
@@ -1421,9 +1428,8 @@ implements RestrictedAccess {
                          'thread' => $entry);
         foreach ($recipients as $recipient) {
             if ($uid == $recipient->getUserId()) continue;
-            $options['references'] =  $entry->getEmailReferencesForUser($recipient);
             $notice = $this->replaceVars($msg, array('recipient' => $recipient));
-            $email->send($recipient->getEmail(), $notice['subj'], $notice['body'], $attachments,
+            $email->send($recipient, $notice['subj'], $notice['body'], $attachments,
                 $options);
         }
 
@@ -1497,9 +1503,8 @@ implements RestrictedAccess {
 
             $options = array(
                 'inreplyto'=>$message->getEmailMessageId(),
-                'references' => $message->getEmailReferencesForUser($user),
                 'thread'=>$message);
-            $email->sendAutoReply($user->getEmail(), $msg['subj'], $msg['body'],
+            $email->sendAutoReply($user, $msg['subj'], $msg['body'],
                 null, $options);
         }
     }
@@ -1562,7 +1567,7 @@ implements RestrictedAccess {
             foreach( $recipients as $k=>$staff) {
                 if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
                 $alert = $this->replaceVars($msg, array('recipient' => $staff));
-                $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null, $options);
+                $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
                 $sentlist[] = $staff->getEmail();
             }
         }
@@ -1611,7 +1616,7 @@ implements RestrictedAccess {
             foreach( $recipients as $k=>$staff) {
                 if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
                 $alert = $this->replaceVars($msg, array('recipient' => $staff));
-                $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null);
+                $email->sendAlert($staff, $alert['subj'], $alert['body'], null);
                 $sentlist[] = $staff->getEmail();
             }
 
@@ -1802,7 +1807,7 @@ implements RestrictedAccess {
             foreach( $recipients as $k=>$staff) {
                 if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
                 $alert = $this->replaceVars($msg, array('recipient' => $staff));
-                $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null, $options);
+                $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
                 $sentlist[] = $staff->getEmail();
             }
          }
@@ -1928,7 +1933,8 @@ implements RestrictedAccess {
     function postMessage($vars, $origin='', $alerts=true) {
         global $cfg;
 
-        $vars['origin'] = $origin;
+        if ($origin)
+            $vars['origin'] = $origin;
         if(isset($vars['ip']))
             $vars['ip_address'] = $vars['ip'];
         elseif(!$vars['ip_address'] && $_SERVER['REMOTE_ADDR'])
@@ -2034,7 +2040,7 @@ implements RestrictedAccess {
             foreach( $recipients as $k=>$staff) {
                 if(!$staff || !$staff->getEmail() || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
                 $alert = $this->replaceVars($msg, array('recipient' => $staff));
-                $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null, $options);
+                $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
                 $sentlist[] = $staff->getEmail();
             }
         }
@@ -2095,7 +2101,7 @@ implements RestrictedAccess {
                 'inreplyto'=>$response->getEmailMessageId(),
                 'references'=>$response->getEmailReferences(),
                 'thread'=>$response);
-            $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body'], $attachments,
+            $email->sendAutoReply($this, $msg['subj'], $msg['body'], $attachments,
                 $options);
         }
 
@@ -2158,7 +2164,7 @@ implements RestrictedAccess {
                     $variables + array('recipient' => $this->getOwner()));
 
             $attachments = $cfg->emailAttachments()?$response->getAttachments():array();
-            $email->send($this->getEmail(), $msg['subj'], $msg['body'], $attachments,
+            $email->send($this->getOwner(), $msg['subj'], $msg['body'], $attachments,
                 $options);
         }
 
@@ -2221,18 +2227,21 @@ implements RestrictedAccess {
         );
     }
 
-    function postNote($vars, &$errors, $poster, $alert=true) {
+    function postNote($vars, &$errors, $poster=false, $alert=true) {
         global $cfg, $thisstaff;
 
         //Who is posting the note - staff or system?
         $vars['staffId'] = 0;
-        $vars['poster'] = 'SYSTEM';
         if($poster && is_object($poster)) {
             $vars['staffId'] = $poster->getId();
             $vars['poster'] = $poster->getName();
-        }elseif($poster) { //string
+        }
+        elseif ($poster) { //string
             $vars['poster'] = $poster;
         }
+        elseif (!isset($vars['poster'])) {
+            $vars['poster'] = 'SYSTEM';
+        }
 
         if(!($note=$this->getThread()->addNote($vars, $errors)))
             return null;
@@ -2305,7 +2314,7 @@ implements RestrictedAccess {
                         )
                     continue;
                 $alert = $this->replaceVars($msg, array('recipient' => $staff));
-                $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null, $options);
+                $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
                 $sentlist[$staff->getEmail()] = 1;
             }
         }
@@ -2313,6 +2322,19 @@ implements RestrictedAccess {
         return $note;
     }
 
+    // Threadable interface
+    function postThreadEntry($type, $vars) {
+        $errors = array();
+        switch ($type) {
+        case 'M':
+            return $this->postMessage($vars, $vars['origin']);
+        case 'N':
+            return $this->postNote($vars, $errors);
+        case 'R':
+            return $this->postReply($vars, $errors);
+        }
+    }
+
     //Print ticket... export the ticket thread as PDF.
     function pdfExport($psize='Letter', $notes=false) {
         global $thisstaff;
@@ -3125,7 +3147,7 @@ implements RestrictedAccess {
         $ticket->logEvent('created');
 
         // Fire post-create signal (for extra email sending, searching)
-        Signal::send('model.created', $ticket);
+        Signal::send('ticket.created', $ticket);
 
         /* Phew! ... time for tea (KETEPA) */
 
@@ -3241,9 +3263,9 @@ implements RestrictedAccess {
                 $references[] = $response->getEmailMessageId();
             $options = array(
                 'references' => $references,
-                'thread' => $message,
+                'thread' => $message ?: $ticket->getThread(),
             );
-            $email->send($ticket->getEmail(), $msg['subj'], $msg['body'], $attachments,
+            $email->send($ticket->getOwner(), $msg['subj'], $msg['body'], $attachments,
                 $options);
         }
 
diff --git a/include/class.user.php b/include/class.user.php
index 5db55087c1538f53bd2acb0311c3022165d2c7dc..0ccfaf1a7c104c1238b346076b57792be5211307 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -178,6 +178,7 @@ class User extends UserModel {
             catch (OrmException $e) {
                 return null;
             }
+            Signal::send('user.created', $user);
         }
 
         return $user;
@@ -716,9 +717,9 @@ class PersonsName {
 
         $r = explode(' ', $name);
         $size = count($r);
-        
+
         //check if name is bad format (ex: J.Everybody), and fix them
-        if($size==1 && mb_strpos($r[0], '.') !== false) 
+        if($size==1 && mb_strpos($r[0], '.') !== false)
         {
             $r = explode('.', $name);
             $size = count($r);
diff --git a/include/client/templates/ticket-print.tmpl.php b/include/client/templates/ticket-print.tmpl.php
index 67e24a1700af259f8e802017fd405675527d9dd7..a1173b217b48111ebc8857129cd9c8e4fe74a56a 100644
--- a/include/client/templates/ticket-print.tmpl.php
+++ b/include/client/templates/ticket-print.tmpl.php
@@ -198,30 +198,29 @@ $types = array('M', 'R');
 if ($thread = $ticket->getThreadEntries($types)) {
     $threadTypes=array('M'=>'message','R'=>'response', 'N'=>'note');
     foreach ($thread as $entry) { ?>
-        <div class="thread-entry <?php echo $threadTypes[$entry['thread_type']]; ?>">
+        <div class="thread-entry <?php echo $threadTypes[$entry->type]; ?>">
             <table class="header"><tr><td>
                     <span><?php
-                        echo Format::datetime($entry['created']);?></span>
+                        echo Format::datetime($entry->created);?></span>
                     <span style="padding:0 1em" class="faded title"><?php
-                        echo Format::truncate($entry['title'], 100); ?></span>
+                        echo Format::truncate($entry->title, 100); ?></span>
                 </td>
                 <td class="flush-right faded title" style="white-space:no-wrap">
                     <?php
-                        echo Format::htmlchars($entry['name'] ?: $entry['poster']); ?></span>
+                        echo Format::htmlchars($entry->getName()); ?></span>
                 </td>
             </tr></table>
             <div class="thread-body">
-                <div><?php echo $entry['body']->display('pdf'); ?></div>
+                <div><?php echo $entry->getBody()->display('pdf'); ?></div>
             </div>
             <?php
-            if ($entry['attachments']
-                    && ($tentry = $ticket->getThreadEntry($entry['id']))
-                    && ($files = $tentry->getAttachments())) { ?>
+            if ($entry->has_attachments
+                    && ($files = $entry->attachments)) { ?>
                 <div class="info">
-<?php           foreach ($files as $F) { ?>
+<?php           foreach ($files as $A) { ?>
                     <div>
-                        <span><?php echo $F['name']; ?></span>
-                        <span class="faded">(<?php echo Format::file_size($F['size']); ?>)</span>
+                        <span><?php echo Format::htmlchars($A->file->name); ?></span>
+                        <span class="faded">(<?php echo Format::file_size($A->file->size); ?>)</span>
                     </div>
 <?php           } ?>
                 </div>
diff --git a/include/client/view.inc.php b/include/client/view.inc.php
index fe2f1b8aa481d93f4178743bfe4808ce82cda4e8..bc331dee94d8eab2efc2a64d6cbdc72ac74b3de4 100644
--- a/include/client/view.inc.php
+++ b/include/client/view.inc.php
@@ -114,24 +114,23 @@ if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) {
     foreach($thread as $entry) {
 
         //Making sure internal notes are not displayed due to backend MISTAKES!
-        if(!$threadType[$entry['type']]) continue;
-        $poster = $entry['poster'];
-        if($entry['type']=='R' && ($cfg->hideStaffName() || !$entry['staff_id']))
+        if(!$threadType[$entry->type]) continue;
+        $poster = $entry->poster;
+        if($entry->type=='R' && ($cfg->hideStaffName() || !$entry->staff_id))
             $poster = ' ';
         ?>
-        <table class="thread-entry <?php echo $threadType[$entry['type']]; ?>" cellspacing="0" cellpadding="1" width="800" border="0">
+        <table class="thread-entry <?php echo $threadType[$entry->type]; ?>" cellspacing="0" cellpadding="1" width="800" border="0">
             <tr><th><div>
-<?php echo Format::datetime($entry['created']); ?>
+<?php echo Format::datetime($entry->created); ?>
                 &nbsp;&nbsp;<span class="textra"></span>
                 <span><?php echo $poster; ?></span>
             </div>
             </th></tr>
-            <tr><td class="thread-body"><div><?php echo Format::clickableurls($entry['body']->toHtml()); ?></div></td></tr>
+            <tr><td class="thread-body"><div><?php echo Format::clickableurls($entry->getBody()->toHtml()); ?></div></td></tr>
             <?php
-            if($entry['attachments']
-                    && ($tentry=$ticket->getThreadEntry($entry['id']))
-                    && ($urls = $tentry->getAttachmentUrls())
-                    && ($links=$tentry->getAttachmentsLinks())) { ?>
+            if($entry->has_attachments
+                    && ($urls = $entry->getAttachmentUrls())
+                    && ($links = $entry->getAttachmentsLinks())) { ?>
                 <tr><td class="info"><?php echo $links; ?></td></tr>
 <?php       }
             if ($urls) { ?>
diff --git a/include/staff/templates/task-view.tmpl.php b/include/staff/templates/task-view.tmpl.php
index 6020b7a3eadfbec200c2f88ef25fff82ba010340..a9a4c72170780e61cd2edd7ada25a4ab30a6ab3b 100644
--- a/include/staff/templates/task-view.tmpl.php
+++ b/include/staff/templates/task-view.tmpl.php
@@ -212,32 +212,31 @@ foreach (DynamicFormEntry::forObject($task->getId(),
     $types = array('M', 'R', 'N');
     if(($thread=$task->getThreadEntries($types))) {
        foreach($thread as $entry) { ?>
-        <table class="thread-entry <?php echo $threadTypes[$entry['type']]; ?>" cellspacing="0" cellpadding="1" width="940" border="0">
+        <table class="thread-entry <?php echo $threadTypes[$entry->type]; ?>" cellspacing="0" cellpadding="1" width="940" border="0">
             <tr>
                 <th colspan="4" width="100%">
                 <div>
                     <span class="pull-left">
                     <span style="display:inline-block"><?php
-                        echo Format::datetime($entry['created']);?></span>
+                        echo Format::datetime($entry->created);?></span>
                     <span style="display:inline-block;padding:0 1em" class="faded title"><?php
-                        echo Format::truncate($entry['title'], 100); ?></span>
+                        echo Format::truncate($entry->title, 100); ?></span>
                     </span>
                     <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;"
                             class="tmeta faded title"><?php
-                            echo Format::htmlchars($entry['name'] ?: $entry['poster']); ?></span>
+                            echo Format::htmlchars($entry->getName()); ?></span>
                     </span>
                 </div>
                 </th>
             </tr>
             <tr><td colspan="4" class="thread-body" id="thread-id-<?php
-                echo $entry['id']; ?>"><div><?php
-                echo $entry['body']->toHtml(); ?></div></td></tr>
+                echo $entry->getId(); ?>"><div><?php
+                echo $entry->getBody()->toHtml(); ?></div></td></tr>
             <?php
             $urls = null;
-            if($entry['attachments']
-                    && ($tentry = $task->getThreadEntry($entry['id']))
+            if($entry->has_attachments
                     && ($urls = $tentry->getAttachmentUrls())
                     && ($links = $tentry->getAttachmentsLinks())) {?>
             <tr>
@@ -246,7 +245,7 @@ foreach (DynamicFormEntry::forObject($task->getId(),
             }
             if ($urls) { ?>
                 <script type="text/javascript">
-                    $('#thread-id-<?php echo $entry['id']; ?>')
+                    $('#thread-id-<?php echo $entry->getId(); ?>')
                         .data('urls', <?php
                             echo JsonDataEncoder::encode($urls); ?>)
                         .data('id', <?php echo $entry['id']; ?>);
@@ -255,8 +254,8 @@ foreach (DynamicFormEntry::forObject($task->getId(),
             } ?>
         </table>
         <?php
-        if ($entry['type'] == 'M')
-            $msgId = $entry['id'];
+        if ($entry->type == 'M')
+            $msgId = $entry->getId();
        }
     } else {
         echo '<p>'.__('Error fetching thread - get technical help.').'</p>';
diff --git a/include/staff/templates/thread-email-headers.tmpl.php b/include/staff/templates/thread-email-headers.tmpl.php
index 6e2f45809e0e9dc7d985b92eff1252fcb52ca7a0..a84216ab46be6c4816a679ada4a1e77ff96e5216 100644
--- a/include/staff/templates/thread-email-headers.tmpl.php
+++ b/include/staff/templates/thread-email-headers.tmpl.php
@@ -3,7 +3,7 @@
 <hr/>
 
 <pre style="max-height: 300px; overflow-y: scroll">
-<?php echo $headers; ?>
+<?php echo Format::htmlchars($headers); ?>
 </pre>
 
 <hr>
diff --git a/include/staff/templates/ticket-print.tmpl.php b/include/staff/templates/ticket-print.tmpl.php
index e283e4e8f2da193598e1096b8d1cbe0eb86d00e4..11323c4a14b60077e7e208cc41f0b1434726882e 100644
--- a/include/staff/templates/ticket-print.tmpl.php
+++ b/include/staff/templates/ticket-print.tmpl.php
@@ -222,29 +222,28 @@ if ($this->includenotes)
 if ($thread = $ticket->getThreadEntries($types)) {
     $threadTypes=array('M'=>'message','R'=>'response', 'N'=>'note');
     foreach ($thread as $entry) { ?>
-        <div class="thread-entry <?php echo $threadTypes[$entry['thread_type']]; ?>">
+        <div class="thread-entry <?php echo $threadTypes[$entry->type]; ?>">
             <table class="header" style="width:100%"><tr><td>
                     <span><?php
-                        echo Format::datetime($entry['created']);?></span>
+                        echo Format::datetime($entry->created);?></span>
                     <span style="padding:0 1em" class="faded title"><?php
-                        echo Format::truncate($entry['title'], 100); ?></span>
+                        echo Format::truncate($entry->title, 100); ?></span>
                 </td>
                 <td class="flush-right faded title" style="white-space:no-wrap">
                     <?php
-                        echo Format::htmlchars($entry['name'] ?: $entry['poster']); ?></span>
+                        echo Format::htmlchars($entry->getName()); ?></span>
                 </td>
             </tr></table>
             <div class="thread-body">
-                <div><?php echo $entry['body']->display('pdf'); ?></div>
+                <div><?php echo $entry->getBody()->display('pdf'); ?></div>
             <?php
-            if ($entry['attachments']
-                    && ($tentry = $ticket->getThreadEntry($entry['id']))
-                    && ($files = $tentry->getAttachments())) { ?>
+            if ($entry->has_attachments
+                    && ($files = $entry->attachments)) { ?>
                 <div class="info">
-<?php           foreach ($files as $F) { ?>
+<?php           foreach ($files as $A) { ?>
                     <div>
-                        <span><?php echo $F['name']; ?></span>
-                        <span class="faded">(<?php echo Format::file_size($F['size']); ?>)</span>
+                        <span><?php echo Format::htmlchars($A->file->name); ?></span>
+                        <span class="faded">(<?php echo Format::file_size($A->file->size); ?>)</span>
                     </div>
 <?php           } ?>
                 </div>
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index eb34d70a3e82222da418dc6c025081487bf4490c..b954c49dcb78c081294149b7013a55d97931435f 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -376,8 +376,7 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) {
 <div class="clear"></div>
 <h2 style="padding:10px 0 5px 0; font-size:11pt;"><?php echo Format::htmlchars($ticket->getSubject()); ?></h2>
 <?php
-$tcount = $ticket->getThreadCount();
-$tcount+= $ticket->getNumNotes();
+$tcount = $ticket->getThreadEntries($types)->count();
 ?>
 <ul  class="tabs threads" id="ticket_tabs" >
     <li class="active"><a href="#ticket_thread"><?php echo sprintf(__('Ticket Thread (%d)'), $tcount); ?></a></li>
@@ -396,26 +395,25 @@ $tcount+= $ticket->getNumNotes();
     /* -------- Messages & Responses & Notes (if inline)-------------*/
     $types = array('M', 'R', 'N');
     if(($thread=$ticket->getThreadEntries($types))) {
-       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">
+        foreach($thread as $entry) { ?>
+        <table class="thread-entry <?php echo $threadTypes[$entry->type]; ?>" cellspacing="0" cellpadding="1" width="940" border="0">
             <tr>
                 <th colspan="4" width="100%">
                 <div>
                     <span class="pull-left">
                     <span style="display:inline-block"><?php
-                        echo Format::datetime($entry['created']);?></span>
+                        echo Format::datetime($entry->created);?></span>
                     <span style="display:inline-block;padding:0 1em" class="faded title"><?php
-                        echo Format::truncate($entry['title'], 100); ?></span>
+                        echo Format::truncate($entry->title, 100); ?></span>
                     </span>
-<?php           if ($tentry->hasActions()) {
-                    $actions = $tentry->getActions(); ?>
-                    <div class="pull-right">
-                    <span class="action-button pull-right" data-dropdown="#entry-action-more-<?php echo $entry['id']; ?>">
+                <div class="pull-right">
+<?php           if ($entry->hasActions()) {
+                    $actions = $entry->getActions(); ?>
+                    <span class="action-button pull-right" data-dropdown="#entry-action-more-<?php echo $entry->getId(); ?>">
                         <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">
+                    <div id="entry-action-more-<?php echo $entry->getId(); ?>" class="action-dropdown anchor-right">
                 <ul class="title">
 <?php               foreach ($actions as $group => $list) {
                         foreach ($list as $id => $action) { ?>
@@ -434,36 +432,47 @@ $tcount+= $ticket->getNumNotes();
                         <span style="vertical-align:middle;" class="textra"></span>
                         <span style="vertical-align:middle;"
                             class="tmeta faded title"><?php
-                            echo Format::htmlchars($entry['name'] ?: $entry['poster']); ?></span>
+                            echo Format::htmlchars($entry->getName()); ?></span>
                     </span>
                 </div>
                 </th>
             </tr>
             <tr><td colspan="4" class="thread-body" id="thread-id-<?php
-                echo $entry['id']; ?>"><div><?php
-                echo Format::clickableurls($entry['body']->toHtml()); ?></div></td></tr>
+                echo $entry->getId(); ?>"><div><?php
+                echo Format::clickableurls($entry->getBody()->toHtml()); ?></div></td></tr>
             <?php
             $urls = null;
-            if($entry['attachments']
-                    && ($urls = $tentry->getAttachmentUrls())
-                    && ($links = $tentry->getAttachmentsLinks())) {?>
+            if ($entry->has_attachments
+                && ($urls = $entry->getAttachmentUrls())) { ?>
             <tr>
-                <td class="info" colspan="4"><?php echo $links; ?></td>
+                <td class="info" colspan="4"><?php
+                    foreach ($entry->attachments as $A) {
+                        if ($A->inline) continue;
+                        $size = '';
+                        if ($A->file->size)
+                            $size = sprintf('<em>(%s)</em>',
+                                Format::file_size($A->file->size));
+?>
+                <a class="Icon file no-pjax" href="<?php echo $A->file->getDownloadUrl();
+                    ?>" target="_blank"><?php echo Format::htmlchars($A->file->name);
+                ?></a><?php echo $size;?>&nbsp;
+<?php               } ?>
+                </td>
             </tr> <?php
             }
             if ($urls) { ?>
                 <script type="text/javascript">
-                    $('#thread-id-<?php echo $entry['id']; ?>')
+                    $('#thread-id-<?php echo $entry->getId(); ?>')
                         .data('urls', <?php
                             echo JsonDataEncoder::encode($urls); ?>)
-                        .data('id', <?php echo $entry['id']; ?>);
+                        .data('id', <?php echo $entry->getId(); ?>);
                 </script>
 <?php
             } ?>
         </table>
         <?php
-        if ($entry['type'] == 'M')
-            $msgId = $entry['id'];
+        if ($entry->type == 'M')
+            $msgId = $entry->getId();
        }
     } else {
         echo '<p><em>'.__('No entries have been posted to this ticket.').'</em></p>';
diff --git a/include/upgrader/streams/core/15b30765-dd0022fb.task.php b/include/upgrader/streams/core/15b30765-dd0022fb.task.php
index 4d7eac01ff94a7622a16b118b5c567d41d3a9af0..0bf1576190be9a1eb3cd3de3a60f87115fa99e00 100644
--- a/include/upgrader/streams/core/15b30765-dd0022fb.task.php
+++ b/include/upgrader/streams/core/15b30765-dd0022fb.task.php
@@ -188,7 +188,7 @@ class AttachmentMigrater extends MigrationTask {
             # TODO: Get the size and mime/type of each file.
             #
             # NOTE: If filesize() fails and file_get_contents() doesn't,
-            # then the AttachmentFile::save() method will automatically
+            # then the AttachmentFile::create() method will automatically
             # estimate the filesize based on the length of the string data
             # received in $info['data'] -- ie. no need to do that here.
             #
@@ -228,9 +228,9 @@ class AttachmentMigrater extends MigrationTask {
         return $this->errorList;
     }
 
-    // This is the AttachmentFile::save() method from osTicket 1.7.6. It's
+    // This is the AttachmentFile::create() method from osTicket 1.7.6. It's
     // been ported here so that further changes to the %file table and the
-    // AttachmentFile::save() method do not affect upgrades from osTicket
+    // AttachmentFile::create() method do not affect upgrades from osTicket
     // 1.6 to osTicket 1.8 and beyond.
     function saveAttachment($file) {
 
diff --git a/include/upgrader/streams/core/934954de-f1ccd3bb.task.php b/include/upgrader/streams/core/934954de-f1ccd3bb.task.php
index 7fe0f141ed159be2a6d177219c1e8abed25ce2d8..041bfad9a33730283e1203d6711bbafcaa27a548 100644
--- a/include/upgrader/streams/core/934954de-f1ccd3bb.task.php
+++ b/include/upgrader/streams/core/934954de-f1ccd3bb.task.php
@@ -4,18 +4,16 @@ class FileImport extends MigrationTask {
     var $description = "Import core osTicket attachment files";
 
     function run($runtime) {
-        $errors = array();
-
         $i18n = new Internationalization('en_US');
         $files = $i18n->getTemplate('file.yaml')->getData();
         foreach ($files as $f) {
-            if (!($id = AttachmentFile::create($f, $errors)))
+            if (!($file = AttachmentFile::create($f)))
                 continue;
 
             // Ensure the new files are never deleted (attached to Disk)
             $sql ='INSERT INTO '.ATTACHMENT_TABLE
                 .' SET object_id=0, `type`=\'D\', inline=1'
-                .', file_id='.db_input($id);
+                .', file_id='.db_input($file->getId());
             db_query($sql);
         }
     }