diff --git a/include/class.thread.php b/include/class.thread.php
index cae40fa3b0ab1c419b1ce2443a91c3481e6e57e2..705ba5ec5c623b40844d648b5cef6ccbfc945e5b 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -3,7 +3,7 @@
     class.thread.php
 
     Ticket thread
-    TODO: move thread related logic here from class.ticket.php
+    XXX: Please DO NOT add any ticket related logic! use ticket class.
 
     Peter Rotich <peter@osticket.com>
     Copyright (c)  2006-2013 osTicket
@@ -16,6 +16,188 @@
 **********************************************************************/
 include_once(INCLUDE_DIR.'class.ticket.php');
 
+//Ticket thread.
+class Thread {
+
+    var $id; // same as ticket ID.
+    var $ticket;
+
+    function Thread($ticket) {
+
+        $this->ticket = $ticket;
+
+        $this->id = 0;
+
+        $this->load();
+    }
+
+    function load() {
+
+        if(!$this->getTicketId())
+            return null;
+
+        $sql='SELECT ticket.ticket_id as id '
+            .' ,count(DISTINCT attach.attach_id) as attachments '
+            .' ,count(DISTINCT message.id) as messages '
+            .' ,count(DISTINCT response.id) as responses '
+            .' ,count(DISTINCT note.id) as notes '
+            .' FROM '.TICKET_TABLE.' ticket '
+            .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON ('
+                .'ticket.ticket_id=attach.ticket_id) '
+            .' LEFT JOIN '.TICKET_THREAD_TABLE.' message ON ('
+                ."ticket.ticket_id=message.ticket_id AND message.thread_type = 'M') "
+            .' LEFT JOIN '.TICKET_THREAD_TABLE.' response ON ('
+                ."ticket.ticket_id=response.ticket_id AND response.thread_type = 'R') "
+            .' LEFT JOIN '.TICKET_THREAD_TABLE.' note ON ( '
+                ."ticket.ticket_id=note.ticket_id AND note.thread_type = 'N') "
+            .' WHERE ticket.ticket_id='.db_input($this->getTicketId())
+            .' GROUP BY ticket.ticket_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 getId() {
+        return $this->id;
+    }
+
+    function getTicketId() {
+        return $this->getTicket()?$this->getTicket()->getId():0;
+    }
+
+    function getTicket() {
+        return $this->ticket;
+    }
+
+    function getNumAttachments() {
+        return $this->ht['attachments'];
+    }
+
+    function getNumMessages() {
+        return $this->ht['messages'];
+    }
+
+    function getNumResponses() {
+        return $this->ht['responses'];
+    }
+
+    function getNumNotes() {
+        return $this->ht['notes'];
+    }
+
+    function getCount() {
+        return $this->getNumMessages() + $this->getNumResponses();
+    }
+
+    function getMessages() {
+        return $this->getEntries('M');
+    }
+
+    function getResponses() {
+        return $this->getEntries('R');
+    }
+
+    function getNotes() {
+        return $this->getEntries('N');
+    }
+
+    function getEntries($type, $order='ASC') {
+
+        if(!$order || !in_array($order, array('DESC','ASC')))
+            $order='ASC';
+
+        $sql='SELECT thread.* '
+            .' ,count(DISTINCT attach.attach_id) as attachments '
+            .' FROM '.TICKET_THREAD_TABLE.' thread '
+            .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach
+                ON (thread.ticket_id=attach.ticket_id
+                        AND thread.id=attach.ref_id
+                        AND thread.thread_type=attach.ref_type) '
+            .' WHERE  thread.ticket_id='.db_input($this->getTicketId());
+
+        if($type && is_array($type))
+            $sql.=' AND thread.thread_type IN('.implode(',', db_input($type)).')';
+        elseif($type)
+            $sql.=' AND thread.thread_type='.db_input($type);
+
+        $sql.=' GROUP BY thread.id '
+             .' ORDER BY thread.created '.$order;
+
+        $entries = array();
+        if(($res=db_query($sql)) && db_num_rows($res))
+            while($rec=db_fetch_array($res))
+                $entries[] = $rec;
+
+        return $entries;
+    }
+
+    function getEntry($id) {
+        return ThreadEntry::lookup($id, $this->getTicketId());
+    }
+
+    function addNote($vars, &$errors) {
+
+        //Add ticket Id.
+        $vars['ticketId'] = $this->getTicketId();
+
+        return Note::create($vars, $errors);
+    }
+
+    function addMessage($vars, &$errors) {
+
+        $vars['ticketId'] = $this->getTicketId();
+        $vars['staffId'] = 0;
+
+        return Message::create($vars, $errors);
+    }
+
+    function addResponse($vars, &$errors) {
+
+        $vars['ticketId'] = $this->getTicketId();
+
+        return Response::create($vars, $errors);
+    }
+
+    function deleteAttachments() {
+
+        $deleted=0;
+        // Clear reference table
+        $res=db_query('DELETE FROM '.TICKET_ATTACHMENT_TABLE.' WHERE ticket_id='.db_input($this->getTicketId()));
+        if ($res && db_affected_rows())
+            $deleted = AttachmentFile::deleteOrphans();
+
+        return $deleted;
+    }
+
+    function delete() {
+
+        $res=db_query('DELETE FROM '.TICKET_THREAD_TABLE.' WHERE ticket_id='.db_input($this->getTicketId()));
+        if(!$res || !db_affected_rows())
+            return false;
+
+        $this->deleteAttachments();
+
+        return true;
+    }
+
+    /* static */
+    function lookup($ticket) {
+
+        return ($ticket
+                && is_object($ticket)
+                && ($thread = new Thread($ticket))
+                && $thread->getId()
+                )?$thread:null;
+    }
+}
+
+
 Class ThreadEntry {
 
     var $id;
@@ -23,7 +205,10 @@ Class ThreadEntry {
 
     var $staff;
     var $ticket;
-    
+
+    var $attachments;
+
+
     function ThreadEntry($id, $type='', $ticketId=0) {
         $this->load($id, $type, $ticketId);
     }
@@ -33,12 +218,14 @@ Class ThreadEntry {
         if(!$id && !($id=$this->getId()))
             return false;
 
-        $sql='SELECT thread.* '
+        $sql='SELECT thread.*, info.* '
             .' ,count(DISTINCT attach.attach_id) as attachments '
             .' FROM '.TICKET_THREAD_TABLE.' thread '
-            .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach 
-                ON (thread.ticket_id=attach.ticket_id 
-                        AND thread.id=attach.ref_id 
+            .' LEFT JOIN '.TICKET_EMAIL_INFO_TABLE.' info
+                ON (thread.id=info.message_id) '
+            .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach
+                ON (thread.ticket_id=attach.ticket_id
+                        AND thread.id=attach.ref_id
                         AND thread.thread_type=attach.ref_type) '
             .' WHERE  thread.id='.db_input($id);
 
@@ -49,7 +236,7 @@ Class ThreadEntry {
             $sql.=' AND thread.ticket_id='.db_input($ticketId);
 
         $sql.=' GROUP BY thread.id ';
-        
+
         if(!($res=db_query($sql)) || !db_num_rows($res))
             return false;
 
@@ -57,6 +244,7 @@ Class ThreadEntry {
         $this->id = $this->ht['id'];
 
         $this->staff = $this->ticket = null;
+        $this->attachments = array();
 
         return true;
     }
@@ -122,13 +310,186 @@ Class ThreadEntry {
     }
 
     function getStaff() {
-      
+
         if(!$this->staff && $this->getStaffId())
             $this->staff = Staff::lookup($this->getStaffId());
 
         return $this->staff;
     }
 
+    function getEmailHeader() {
+        return $this->ht['headers'];
+    }
+
+    function isAutoResponse() {
+        return $this->getEmailHeader()?TicketFilter::isAutoResponse($this->getEmailHeader()):false;
+    }
+
+    //Web uploads - caller is expected to format, validate and set any errors.
+    function uploadFiles($files) {
+
+        if(!$files || !is_array($files))
+            return false;
+
+        $uploaded=array();
+        foreach($files as $file) {
+            if($file['error'] && $file['error']==UPLOAD_ERR_NO_FILE)
+                continue;
+
+            if(!$file['error']
+                    && ($id=AttachmentFile::upload($file))
+                    && $this->saveAttachment($id))
+                $uploaded[]=$id;
+            else {
+                if(!$file['error'])
+                    $error = 'Unable to upload file - '.$file['name'];
+                elseif(is_numeric($file['error']))
+                    $error ='Error #'.$file['error']; //TODO: Transplate to string.
+                else
+                    $error = $file['error'];
+                /*
+                 Log the error as an internal note.
+                 XXX: We're doing it here because it will eventually become a thread post comment (hint: comments coming!)
+                 XXX: logNote must watch for possible loops
+               */
+                $this->getTicket()->logNote('File Upload Error', $error, 'SYSTEM', false);
+            }
+
+        }
+
+        return $uploaded;
+    }
+
+    function importAttachments($attachments) {
+
+        if(!$attachments || !is_array($attachments))
+            return null;
+
+        $files = array();
+        foreach($attachments as  $attachment)
+            if(($id=$this->importAttachment($attachment)))
+                $files[] = $id;
+
+        return $files;
+    }
+
+    /* Emailed & API attachments handler */
+    function importAttachment($attachment) {
+
+        if(!$attachment || !is_array($attachment))
+            return null;
+
+        $id=0;
+        if (!$attachment['error'] && ($id=$this->saveAttachment($attachment)))
+            $files[] = $id;
+        else {
+            $error = $attachment['error'];
+
+            if(!$error)
+                $error = 'Unable to import attachment - '.$attachment['name'];
+
+            $this->getTicket()->logNote('File Import Error', $error, 'SYSTEM', false);
+        }
+
+        return $id;
+    }
+
+   /*
+    Save attachment to the DB.
+    @file is a mixed var - can be ID or file hashtable.
+    */
+    function saveAttachment($file) {
+
+        if(!($fileId=is_numeric($file)?$file:AttachmentFile::save($file)))
+            return 0;
+
+        $sql ='INSERT INTO '.TICKET_ATTACHMENT_TABLE.' SET created=NOW() '
+             .' ,file_id='.db_input($fileId)
+             .' ,ticket_id='.db_input($this->getTicketId())
+             .' ,ref_id='.db_input($this->getId())
+             .' ,ref_type='.db_input($this->getType());
+
+        return (db_query($sql) && ($id=db_insert_id()))?$id:0;
+    }
+
+    function saveAttachments($files) {
+        $ids=array();
+        foreach($files as $file)
+           if(($id=$this->saveAttachment($file)))
+               $ids[] = $id;
+
+        return $ids;
+    }
+
+    function getAttachments() {
+
+        if($this->attachments)
+            return $this->attachments;
+
+        //XXX: inner join the file table instead?
+        $sql='SELECT a.attach_id, f.id as file_id, f.size, f.hash as file_hash, f.name '
+            .' FROM '.FILE_TABLE.' f '
+            .' INNER JOIN '.TICKET_ATTACHMENT_TABLE.' a ON(f.id=a.file_id) '
+            .' WHERE a.ticket_id='.db_input($this->getTicketId())
+            .' AND a.ref_id='.db_input($this->getId())
+            .' AND a.ref_type='.db_input($this->getType());
+
+        $this->attachments = array();
+        if(($res=db_query($sql)) && db_num_rows($res)) {
+            while($rec=db_fetch_array($res))
+                $this->attachments[] = $rec;
+        }
+
+        return $this->attachments;
+    }
+
+    function getAttachmentsLinks($file='attachment.php', $target='', $separator=' ') {
+
+        $str='';
+        foreach($this->getAttachments() as $attachment ) {
+            /* The hash can be changed  but must match validation in @file */
+            $hash=md5($attachment['file_id'].session_id().$attachment['file_hash']);
+            $size = '';
+            if($attachment['size'])
+                $size=sprintf('<em>(%s)</em>', Format::file_size($attachment['size']));
+
+            $str.=sprintf('<a class="Icon file" href="%s?id=%d&h=%s" target="%s">%s</a>%s&nbsp;%s',
+                    $file, $attachment['attach_id'], $hash, $target, Format::htmlchars($attachment['name']), $size, $separator);
+        }
+
+        return $str;
+    }
+
+
+    /* Returns file names with id as key */
+    function getFiles() {
+
+        $files = array();
+        foreach($this->getAttachments() as $attachment)
+            $files[$attachment['file_id']] = $attachment['name'];
+
+        return $files;
+    }
+
+
+    /* save email info
+     * TODO: Refactor it to include outgoing emails on responses.
+     */
+
+    function saveEmailInfo($vars) {
+
+        if(!$vars || !$vars['mid'])
+            return 0;
+
+        $sql='INSERT INTO '.TICKET_EMAIL_INFO_TABLE
+            .' SET message_id='.db_input($this->getId()) //TODO: change it to thread_id
+            .', email_mid='.db_input($vars['mid']) //TODO: change it to mid.
+            .', headers='.db_input($vars['header']);
+
+        return db_query($sql)?db_insert_id():0;
+    }
+
+
     /* variables */
 
     function asVar() {
@@ -164,12 +525,59 @@ Class ThreadEntry {
     /* static calls */
 
     function lookup($id, $tid=0, $type='') {
-        return ($id 
-                && is_numeric($id) 
-                && ($e = new ThreadEntry($id, $type, $tid)) 
+        return ($id
+                && is_numeric($id)
+                && ($e = new ThreadEntry($id, $type, $tid))
                 && $e->getId()==$id
                 )?$e:null;
     }
+
+    //new entry ... we're trusting the caller to check validity of the data.
+    function create($vars) {
+
+        //Must have...
+        if(!$vars['ticketId'] || !$vars['type'] || !in_array($vars['type'], array('M','R','N')))
+            return false;
+
+        $sql=' INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW() '
+            .' ,thread_type='.db_input($vars['type'])
+            .' ,ticket_id='.db_input($vars['ticketId'])
+            .' ,title='.db_input(Format::sanitize($vars['title']))
+            .' ,body='.db_input(Format::sanitize($vars['body']))
+            .' ,staff_id='.db_input($vars['staffId'])
+            .' ,poster='.db_input($vars['poster'])
+            .' ,source='.db_input($vars['source']);
+
+        if(isset($vars['pid']))
+            $sql.=' ,pid='.db_input($vars['pid']);
+
+        if($vars['ip_address'])
+            $sql.=' ,ip_address='.db_input($vars['ip_address']);
+
+        //echo $sql;
+        if(!db_query($sql) || !($entry=self::lookup(db_insert_id(), $vars['ticketId'])))
+            return false;
+
+        /************* ATTACHMENTS *****************/
+
+        //Upload/save attachments IF ANY
+        if($vars['files']) //expects well formatted and VALIDATED files array.
+            $entry->uploadFiles($vars['files']);
+
+        //Emailed or API attachments
+        if($vars['attachments'])
+            $entry->importAttachments($vars['attachments']);
+
+        //Canned attachments...
+        if($vars['cannedattachments'] && is_array($vars['cannedattachments']))
+            $entry->saveAttachments($vars['cannedattachments']);
+
+        return $entry;
+    }
+
+    function add($vars) {
+        return ($entry=self::create($vars))?$entry->getId():0;
+    }
 }
 
 /* Message - Ticket thread entry of type message */
@@ -183,11 +591,30 @@ class Message extends ThreadEntry {
         return $this->getTitle();
     }
 
-    function lookup($id, $tid, $type='M') {
-                
-        return ($id 
+    function create($vars, &$errors) {
+        return self::lookup(self::add($vars, $errors));
+    }
+
+    function add($vars, &$errors) {
+
+        if(!$vars || !is_array($vars) || !$vars['ticketId'])
+            $errors['err'] = 'Missing or invalid data';
+        elseif(!$vars['message'])
+            $errors['message'] = 'Message required';
+
+        if($errors) return false;
+
+        $vars['type'] = 'M';
+        $vars['body'] = $vars['message'];
+
+        return ThreadEntry::add($vars);
+    }
+
+    function lookup($id, $tid=0, $type='M') {
+
+        return ($id
                 && is_numeric($id)
-                && ($m = new Message($id, $tid)) 
+                && ($m = new Message($id, $tid))
                 && $m->getId()==$id
                 )?$m:null;
     }
@@ -208,11 +635,33 @@ class Response extends ThreadEntry {
         return $this->getStaff();
     }
 
-    function lookup($id, $tid, $type='R') {
-                
-        return ($id 
+    function create($vars, &$errors) {
+        return self::lookup(self::add($vars, $errors));
+    }
+
+    function add($vars, &$errors) {
+
+        if(!$vars || !is_array($vars) || !$vars['ticketId'])
+            $errors['err'] = 'Missing or invalid data';
+        elseif(!$vars['response'])
+            $errors['response'] = 'Response required';
+
+        if($errors) return false;
+
+        $vars['type'] = 'R';
+        $vars['body'] = $vars['response'];
+        if(!$vars['pid'] && $vars['msgId'])
+            $vars['pid'] = $vars['msgId'];
+
+        return ThreadEntry::add($vars);
+    }
+
+
+    function lookup($id, $tid=0, $type='R') {
+
+        return ($id
                 && is_numeric($id)
-                && ($r = new Response($id, $tid))           
+                && ($r = new Response($id, $tid))
                 && $r->getId()==$id
                 )?$r:null;
     }
@@ -229,11 +678,33 @@ class Note extends ThreadEntry {
         return $this->getBody();
     }
 
-    function lookup($id, $tid, $type='N') {
-                
-        return ($id 
+    /* static */
+    function create($vars, &$errors) {
+        return self::lookup(self::add($vars, $errors));
+    }
+
+    function add($vars, &$errors) {
+
+        //Check required params.
+        if(!$vars || !is_array($vars) || !$vars['ticketId'])
+            $errors['err'] = 'Missing or invalid data';
+        elseif(!$vars['note'])
+            $errors['note'] = 'Note required';
+
+        if($errors) return false;
+
+        //TODO: use array_intersect_key  when we move to php 5 to extract just what we need.
+        $vars['type'] = 'N';
+        $vars['body'] = $vars['note'];
+
+        return ThreadEntry::add($vars);
+    }
+
+    function lookup($id, $tid=0, $type='N') {
+
+        return ($id
                 && is_numeric($id)
-                && ($n = new Note($id, $tid))          
+                && ($n = new Note($id, $tid))
                 && $n->getId()==$id
                 )?$n:null;
     }