diff --git a/include/api.tickets.php b/include/api.tickets.php
index d6b04c98ccf126bfdaa7e952bacaf1aea3aa480c..32b7d6feb9ec544f015426b3790c71b61f7af815 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();
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.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..02b7f5b6860e284aa16a10f1d30d0e3b59ad055c 100644
--- a/include/class.mailer.php
+++ b/include/class.mailer.php
@@ -79,15 +79,25 @@ 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);
+        }
     }
 
     function send($to, $subject, $message, $options=null) {
@@ -211,7 +221,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 +242,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.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..522a09f81b55b015d7352cae253fd212ab338335 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -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 11a9ce9e5044251a82a56fd0fc533957955c1695..63d8fb6f12eedeea4f6667e04ffe4a0207d8c940 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,54 +59,22 @@ 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) {
@@ -261,7 +187,7 @@ class Thread {
             if ($object instanceof Threadable)
                 return $object->postNote($vars, $errors);
             elseif ($this instanceof ObjectThread)
-                $this->addNote($vars, $errors);
+                return $this->addNote($vars, $errors);
             else
                 throw new Exception('Cannot continue discussion with abstract thread');
         }
@@ -278,7 +204,7 @@ class Thread {
                 if ($object instanceof Threadable)
                     return $object->postNote($vars, $errors);
                 elseif ($this instanceof ObjectThread)
-                    $this->addNote($vars, $errors);
+                    return $this->addNote($vars, $errors);
                 else
                     throw new Exception('Cannot continue discussion with abstract thread');
             }
@@ -287,14 +213,15 @@ class Thread {
         else {
             //XXX: Are we potentially leaking the email address to
             // collaborators?
-            $vars['message'] = sprintf("Received From: %s\n\n%s",
-                $mailinfo['email'], $body);
+            // 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->postMessage($vars, $errors);
             elseif ($this instanceof ObjectThread)
-                $this->addMessage($vars, $errors);
+                return $this->addMessage($vars, $errors);
             else
                 throw new Exception('Cannot continue discussion with abstract thread');
         }
@@ -304,15 +231,11 @@ class Thread {
     }
 
     function deleteAttachments() {
+        $deleted = Attachment::objects()->filter(array(
+            'thread_entry__thread' => $this,
+        ))->delete();
 
-        // 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());
-
-        $deleted=0;
-        if (($res=db_query($sql)) && ($deleted=db_affected_rows()))
+        if ($deleted)
             AttachmentFile::deleteOrphans();
 
         return $deleted;
@@ -321,114 +244,80 @@ class Thread {
     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'),
             ),
+            '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;
-
-    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))
-            return false;
-
-        $this->ht = db_fetch_array($res);
-        $this->id = $this->ht['id'];
-        $this->attachments = new GenericAttachments($this->id, 'H');
-
-        return true;
-    }
+    var $_attachments;
 
     function postEmail($mailinfo) {
         if (!($thread = $this->getThread()))
@@ -442,36 +331,32 @@ class ThreadEntry {
         return $thread->postEmail($mailinfo);
     }
 
-    function reload() {
-        return $this->load();
-    }
-
     function getId() {
         return $this->id;
     }
 
     function getPid() {
-        return $this->ht['pid'];
+        return $this->pid;
     }
 
     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) {
@@ -484,36 +369,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) {
@@ -558,44 +444,46 @@ class ThreadEntry {
     }
 
     function getThreadId() {
-        return $this->ht['thread_id'];
+        return $this->thread_id;
     }
 
     function getThread() {
 
-        if(!$this->thread && $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());
+            $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() {
@@ -632,8 +520,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'])
@@ -674,8 +562,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'),
@@ -685,7 +573,7 @@ class ThreadEntry {
                     _S('File Import Error'), $error, _S('SYSTEM'), false);
         }
 
-        return $id;
+        return $A;
     }
 
    /*
@@ -696,28 +584,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,
             );
         }
 
@@ -725,16 +632,19 @@ 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;
@@ -744,8 +654,8 @@ class ThreadEntry {
     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;
     }
@@ -774,13 +684,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(trim($header));
+            $this->email_info->headers = trim($header);
 
-        return db_query($sql) ? db_insert_id() : 0;
+        return $this->email_info->save();
     }
 
     /* variables */
@@ -810,14 +722,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
@@ -834,14 +738,12 @@ 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;
         }
 
         foreach (array('in-reply-to', 'references') as $header) {
@@ -867,10 +769,9 @@ class ThreadEntry {
                     list($left, $ref) = explode('+', $left);
                     $mid = "$left@$right";
                 }
-                $res = db_query(sprintf($search, db_input($mid)));
-                while (list($id) = db_fetch_row($res)) {
-                    if (!($t = ThreadEntry::lookup($id)))
-                        continue;
+                $possibles = ThreadEntry::objects()
+                    ->filter(array('email_info__mid' => $mid));
+                foreach ($possibles as $t) {
                     // Capture the first match thread item
                     if (!$thread)
                         $thread = $t;
@@ -1028,36 +929,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 *****************/
@@ -1091,11 +992,8 @@ 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;
         }
 
@@ -1109,13 +1007,12 @@ class ThreadEntry {
         // 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 ------------------------
@@ -1307,6 +1204,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)';
@@ -1354,6 +1255,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)';
@@ -1375,16 +1280,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) {
@@ -1406,16 +1307,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 */
@@ -1423,10 +1314,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();
     }
@@ -1436,7 +1323,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) {
@@ -1460,31 +1347,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) {
@@ -1503,15 +1377,6 @@ 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.
@@ -1522,53 +1387,56 @@ class ObjectThread extends Thread {
         ObjectModel::OBJECT_TYPE_TASK => 'TaskThread',
     );
 
-    function __construct($id) {
+    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 {
@@ -1582,13 +1450,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) {
@@ -1617,38 +1487,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, $type=false) {
+        if (!$type)
+            return parent::lookup($criteria);
+
         $class = false;
-        if ($type && isset(self::$types[$type]))
+        if (isset(self::$types[$type]))
             $class = self::$types[$type];
         if (!class_exists($class))
             $class = get_called_class();
 
-        return ($criteria
-                && ($t = new $class($criteria))
-                && $t->getId()
-                ) ? $t : null;
+        return $class::lookup($criteria);
     }
 }
 
@@ -1657,10 +1525,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;
     }
 }
 
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 9f23ae416f7f5753f05d962820760c5fdaa06fc3..626e2265fe4ce55e7db98168cd04221f1581e573 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,
             ),
@@ -818,7 +818,7 @@ implements RestrictedAccess, Threadable {
     }
 
     function getThreadCount() {
-        return $this->getNumMessages() + $this->getNumResponses();
+        return $this->getClientThread()->count();
     }
 
     function getNumMessages() {
@@ -834,15 +834,15 @@ implements RestrictedAccess, Threadable {
     }
 
     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, Threadable {
         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
@@ -3129,7 +3131,7 @@ implements RestrictedAccess, Threadable {
         $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) */
 
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/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);
         }
     }