diff --git a/include/api.tickets.php b/include/api.tickets.php
index 7346f2b2943bbc5c0451d62fcc7952677034b6d5..1cc93d995fc52a563b6c8ce826137b4f198522c5 100644
--- a/include/api.tickets.php
+++ b/include/api.tickets.php
@@ -12,9 +12,9 @@ class TicketApiController extends ApiController {
         $supported = array(
             "alert", "autorespond", "source", "topicId",
             "name", "email", "subject", "phone", "phone_ext",
-            "attachments" => array("*" => 
+            "attachments" => array("*" =>
                 array("name", "type", "data", "encoding")
-            ), 
+            ),
             "message", "ip", "priorityId"
         );
 
@@ -24,6 +24,37 @@ class TicketApiController extends ApiController {
         return $supported;
     }
 
+    /*
+     Validate data - overwrites parent's validator for additional validations.
+    */
+    function validate(&$data, $format) {
+        global $ost;
+
+        //Call parent to Validate the structure
+        if(!parent::validate($data, $format))
+            $this->exerr(400, 'Unexpected or invalid data received');
+
+        //Nuke attachments IF API files are not allowed.
+        if(!$ost->getConfig()->allowAPIAttachments())
+            $data['attachments'] = array();
+
+        //Validate attachments: Do error checking... soft fail - set the error and pass on the request.
+        if($data['attachments'] && is_array($data['attachments'])) {
+            foreach($data['attachments'] as &$attachment) {
+                if(!$ost->isFileTypeAllowed($attachment))
+                    $data['error'] = 'Invalid file type (ext) for '.Format::htmlchars($attachment['name']);
+                elseif ($attachment['encoding'] && !strcasecmp($attachment['encoding'], 'base64')) {
+                    if(!($attachment['data'] = base64_decode($attachment['data'], true)))
+                        $attachment['error'] = sprintf('%s: Poorly encoded base64 data', Format::htmlchars($attachment['name']));
+                }
+            }
+            unset($attachment);
+        }
+
+        return true;
+    }
+
+
     function create($format) {
 
         if(!($key=$this->requireApiKey()) || !$key->canCreateTickets())
@@ -62,7 +93,7 @@ class TicketApiController extends ApiController {
                 return $this->exerr(403, 'Ticket denied');
             else
                 return $this->exerr(
-                        400, 
+                        400,
                         "Unable to create new ticket: validation errors:\n"
                         .Format::array_implode(": ", "\n", $errors)
                         );
@@ -70,11 +101,6 @@ class TicketApiController extends ApiController {
             return $this->exerr(500, "Unable to create new ticket: unknown error");
         }
 
-        
-        # Save attachment(s)
-        if($data['attachments'])
-            $ticket->importAttachments($data['attachments'], $ticket->getLastMsgId(), 'M');
-
         return $ticket;
     }
 
@@ -97,7 +123,7 @@ class PipeApiController extends TicketApiController {
     //Overwrite grandparent's (ApiController) response method.
     function response($code, $resp) {
 
-        //Use postfix exit codes - instead of HTTP 
+        //Use postfix exit codes - instead of HTTP
         switch($code) {
             case 201: //Success
                 $exitcode = 0;
@@ -119,8 +145,8 @@ class PipeApiController extends TicketApiController {
                 $exitcode = 69;
                 break;
             case 500: //Server error.
-            default: //Temp (unknown) failure - retry 
-                $exitcode = 75; 
+            default: //Temp (unknown) failure - retry
+                $exitcode = 75;
         }
 
         //echo "$code ($exitcode):$resp";
diff --git a/include/class.api.php b/include/class.api.php
index 6fc0445e5ef66c2da6eaf925af5027c52a6c0fdb..d22be61100f5dff66663107377f0b9abffe37df2 100644
--- a/include/class.api.php
+++ b/include/class.api.php
@@ -192,6 +192,7 @@ class ApiController {
      * work will be done for XML requests
      */
     function getRequest($format) {
+        global $ost;
         
         $input = (substr(php_sapi_name(), 0, 3) == 'cli')?'php://stdin':'php://input';
 
@@ -219,7 +220,8 @@ class ApiController {
         if (!($data = $parser->parse($stream)))
             $this->exerr(400, $parser->lastError());
        
-        $this->validate($data, $this->getRequestStructure($format));
+        //Validate structure of the request.
+        $this->validate($data, $format);
 
         return $data;
     }
@@ -239,19 +241,33 @@ class ApiController {
      * expected. It is assumed that the functions actually implementing the
      * API will further validate the contents of the request
      */
-    function validate($data, $structure, $prefix="") {
+    function validateRequestStructure($data, $structure, $prefix="") {
+       
         foreach ($data as $key=>$info) {
             if (is_array($structure) and is_array($info)) {
                 $search = (isset($structure[$key]) && !is_numeric($key)) ? $key : "*"; 
                 if (isset($structure[$search])) {
-                    $this->validate($info, $structure[$search], "$prefix$key/");
+                    $this->validateRequestStructure($info, $structure[$search], "$prefix$key/");
                     continue;
                 }
             } elseif (in_array($key, $structure)) {
                 continue;
             }
-            $this->exerr(400, "$prefix$key: Unexpected data received");
+            return $this->exerr(400, "$prefix$key: Unexpected data received");
         }
+
+        return true;
+    }
+
+    /**
+     * Validate request.
+     *
+     */
+    function validate(&$data, $format) {
+        return $this->validateRequestStructure(
+                $data, 
+                $this->getRequestStructure($format)
+                );
     }
 
     /**
diff --git a/include/class.config.php b/include/class.config.php
index a445f952d277c94850d8b4b6de81ff58b165838d..e6bed4657f69b4c432e617a750042ad3271b3611 100644
--- a/include/class.config.php
+++ b/include/class.config.php
@@ -2,7 +2,7 @@
 /*********************************************************************
     class.config.php
 
-    osTicket config info manager. 
+    osTicket config info manager.
 
     Peter Rotich <peter@osticket.com>
     Copyright (c)  2006-2013 osTicket
@@ -17,17 +17,17 @@
 require_once(INCLUDE_DIR.'class.email.php');
 
 class Config {
-    
+
     var $id = 0;
     var $config = array();
 
-    var $defaultDept;   //Default Department    
+    var $defaultDept;   //Default Department
     var $defaultSLA;   //Default SLA
-    var $defaultEmail;  //Default Email 
+    var $defaultEmail;  //Default Email
     var $alertEmail;  //Alert Email
     var $defaultSMTPEmail; //Default  SMTP Email
 
-    function Config($id) { 
+    function Config($id) {
         $this->load($id);
     }
 
@@ -39,11 +39,11 @@ class Config {
         $sql='SELECT *, (TIME_TO_SEC(TIMEDIFF(NOW(), UTC_TIMESTAMP()))/3600) as db_tz_offset '
             .' FROM '.CONFIG_TABLE
             .' WHERE id='.db_input($id);
-        
+
         if(!($res=db_query($sql)) || !db_num_rows($res))
             return false;
 
-            
+
         $this->config = db_fetch_array($res);
         $this->id = $this->config['id'];
 
@@ -100,7 +100,7 @@ class Config {
 
         return null;
     }
-    
+
     function getDBTZoffset() {
         return $this->config['db_tz_offset'];
     }
@@ -135,15 +135,15 @@ class Config {
     function getConfigInfo() {
         return $this->config;
     }
-   
+
     function getTitle() {
         return $this->config['helpdesk_title'];
     }
-    
+
     function getUrl() {
-        return $this->config['helpdesk_url'];        
+        return $this->config['helpdesk_url'];
     }
-    
+
     function getBaseUrl() { //Same as above with no trailing slash.
         return rtrim($this->getUrl(),'/');
     }
@@ -171,11 +171,11 @@ class Config {
     function showNotesInline(){
         return $this->config['show_notes_inline'];
     }
-        
+
     function getClientTimeout() {
         return $this->getClientSessionTimeout();
     }
- 
+
     function getClientSessionTimeout() {
         return $this->config['client_session_timeout']*60;
     }
@@ -191,8 +191,8 @@ class Config {
     function getStaffTimeout() {
         return $this->getStaffSessionTimeout();
     }
-        
-    function getStaffSessionTimeout() {                
+
+    function getStaffSessionTimeout() {
         return $this->config['staff_session_timeout']*60;
     }
 
@@ -218,7 +218,7 @@ class Config {
             $this->defaultDept=Dept::lookup($this->getDefaultDeptId());
 
         return $this->defaultDept;
-    }   
+    }
 
     function getDefaultEmailId() {
         return $this->config['default_email_id'];
@@ -280,7 +280,7 @@ class Config {
     }
 
     function getDefaultTemplate() {
-    
+
         if(!$this->defaultTemplate && $this->getDefaultTemplateId())
             $this->defaultTemplate = Template::lookup($this->getDefaultTemplateId());
 
@@ -319,7 +319,7 @@ class Config {
     function clickableURLS() {
         return ($this->config['clickable_urls']);
     }
-        
+
     function enableStaffIPBinding() {
         return ($this->config['staff_ip_binding']);
     }
@@ -335,12 +335,12 @@ class Config {
     function isEmailPollingEnabled() {
         return ($this->config['enable_mail_polling']);
     }
-        
+
     function allowPriorityChange() {
         return ($this->config['allow_priority_change']);
     }
 
-        
+
     function useEmailPriority() {
         return ($this->config['use_email_priority']);
     }
@@ -352,7 +352,7 @@ class Config {
     function getReplySeparator() {
         return $this->config['reply_separator'];
     }
-  
+
     function stripQuotedReply() {
         return ($this->config['strip_quoted_reply']);
     }
@@ -360,7 +360,7 @@ class Config {
     function saveEmailHeaders() {
         return true; //No longer an option...hint: big plans for headers coming!!
     }
-    
+
     function useRandomIds() {
         return ($this->config['random_ticket_ids']);
     }
@@ -369,7 +369,7 @@ class Config {
     function autoRespONNewTicket() {
         return ($this->config['ticket_autoresponder']);
     }
-    
+
     function autoRespONNewMessage() {
         return ($this->config['message_autoresponder']);
     }
@@ -385,11 +385,11 @@ class Config {
     function alertLastRespondentONNewMessage() {
         return ($this->config['message_alert_laststaff']);
     }
-   
+
     function alertAssignedONNewMessage() {
         return ($this->config['message_alert_assigned']);
     }
-    
+
     function alertDeptManagerONNewMessage() {
         return ($this->config['message_alert_dept_manager']);
     }
@@ -417,7 +417,7 @@ class Config {
     function alertAdminONNewTicket() {
         return ($this->config['ticket_alert_admin']);
     }
-     
+
     function alertDeptManagerONNewTicket() {
         return ($this->config['ticket_alert_dept_manager']);
     }
@@ -433,11 +433,11 @@ class Config {
     function alertAssignedONTransfer() {
         return ($this->config['transfer_alert_assigned']);
     }
-    
+
     function alertDeptManagerONTransfer() {
         return ($this->config['transfer_alert_dept_manager']);
     }
-    
+
     function alertDeptMembersONTransfer() {
         return ($this->config['transfer_alert_dept_members']);
     }
@@ -486,7 +486,7 @@ class Config {
     function showAnsweredTickets() {
         return ($this->config['show_answered_tickets']);
     }
-        
+
     function hideStaffName() {
         return ($this->config['hide_staff_name']);
     }
@@ -494,10 +494,10 @@ class Config {
     function sendOverLimitNotice() {
         return ($this->config['overlimit_notice_active']);
     }
-        
+
     /* Error alerts sent to admin email when enabled */
     function alertONSQLError() {
-        return ($this->config['send_sql_errors']);                    
+        return ($this->config['send_sql_errors']);
     }
     function alertONLoginError() {
         return ($this->config['send_login_errors']);
@@ -507,7 +507,7 @@ class Config {
         return ($this->config['send_mailparse_errors']);
     }
 
-    
+
 
     /* Attachments */
     function getAllowedFileTypes() {
@@ -529,21 +529,27 @@ class Config {
     function allowAttachmentsOnlogin() {
         return ($this->allowOnlineAttachments() && $this->config['allow_online_attachments_onlogin']);
     }
-    
+
     function allowEmailAttachments() {
         return ($this->allowAttachments() && $this->config['allow_email_attachments']);
     }
 
+    //TODO: change db field to allow_api_attachments - which will include  email/json/xml attachments
+    //       terminology changed on the UI
+    function allowAPIAttachments() {
+        return $this->allowEmailAttachments();
+    }
+
     /* Needed by upgrader on 1.6 and older releases upgrade - not not remove */
     function getUploadDir() {
         return $this->config['upload_dir'];
     }
-    
+
     function updateSettings($vars, &$errors) {
 
         if(!$vars || $errors)
             return false;
-        
+
         switch(strtolower($vars['t'])) {
             case 'system':
                 return $this->updateSystemSettings($vars, $errors);
@@ -702,10 +708,10 @@ class Config {
         $f['default_email_id']=array('type'=>'int',   'required'=>1, 'error'=>'Default email required');
         $f['alert_email_id']=array('type'=>'int',   'required'=>1, 'error'=>'Selection required');
         $f['admin_email']=array('type'=>'email',   'required'=>1, 'error'=>'System admin email required');
-       
+
         if($vars['strip_quoted_reply'] && !$vars['reply_separator'])
             $errors['reply_separator']='Reply separator required to strip quoted reply.';
-        
+
         if($vars['admin_email'] && Email::getIdByEmail($vars['admin_email'])) //Make sure admin email is not also a system email.
             $errors['admin_email']='Email already setup as system email';
 
@@ -724,7 +730,7 @@ class Config {
             .' WHERE id='.db_input($this->getId());
 
 
-        
+
         return (db_query($sql));
     }
 
@@ -732,16 +738,16 @@ class Config {
 
 
         if($vars['allow_attachments']) {
-        
+
             if(!ini_get('file_uploads'))
                 $errors['err']='The \'file_uploads\' directive is disabled in php.ini';
-                
+
             if(!is_numeric($vars['max_file_size']))
-                $errors['max_file_size']='Maximum file size required';       
-               
+                $errors['max_file_size']='Maximum file size required';
+
             if(!$vars['allowed_filetypes'])
                 $errors['allowed_filetypes']='Allowed file extentions required';
-        
+
             if(!($maxfileuploads=ini_get('max_file_uploads')))
                 $maxfileuploads=DEFAULT_MAX_FILE_UPLOADS;
 
@@ -842,7 +848,7 @@ class Config {
         }
 
         if($errors) return false;
-        
+
         $sql= 'UPDATE '.CONFIG_TABLE.' SET updated=NOW() '
              .',ticket_alert_active='.db_input($vars['ticket_alert_active'])
              .',ticket_alert_admin='.db_input(isset($vars['ticket_alert_admin'])?1:0)
diff --git a/include/class.faq.php b/include/class.faq.php
index f902dd0f0f4c1412896a319b95fee45730e2ede8..447719fbda62e1eac0fd3fe68cc66cbefc069352 100644
--- a/include/class.faq.php
+++ b/include/class.faq.php
@@ -170,7 +170,7 @@ class FAQ {
         }
 
         //Upload new attachments IF any.
-        if($_FILES['attachments'] && ($files=Format::files($_FILES['attachments'])))
+        if($_FILES['attachments'] && ($files=AttachmentFile::format($_FILES['attachments'])))
             $this->uploadAttachments($files);
 
         $this->reload();
@@ -282,7 +282,7 @@ class FAQ {
         if(($faq=self::lookup($id))) {
             $faq->updateTopics($vars['topics']);
                
-            if($_FILES['attachments'] && ($files=Format::files($_FILES['attachments'])))
+            if($_FILES['attachments'] && ($files=AttachmentFile::format($_FILES['attachments'])))
                 $faq->uploadAttachments($files);
 
             $faq->reload();
diff --git a/include/class.file.php b/include/class.file.php
index 27a8881608ef4bda37a3c2a8be0fb838973626ef..6908dd8eec6efb4e2a24a0618fd1945471859878 100644
--- a/include/class.file.php
+++ b/include/class.file.php
@@ -210,6 +210,53 @@ class AttachmentFile {
         
         return ($id && ($file = new AttachmentFile($id)) && $file->getId()==$id)?$file:null;
     }
+
+    /* 
+      Method formats http based $_FILE uploads - plus basic validation.
+      @restrict - make sure file type & size are allowed.
+     */
+    function format($files, $restrict=false) {
+        global $ost;
+
+        if(!$files || !is_array($files))
+            return null;
+
+        //Reformat $_FILE  for the sane.
+        $attachments = array();
+        foreach($files as $k => $a) {
+            if(is_array($a))
+                foreach($a as $i => $v)
+                    $attachments[$i][$k] = $v;
+        }
+
+        //Basic validation.
+        foreach($attachments as $i => &$file) {
+            //skip no file upload "error" - why PHP calls it an error is beyond me.
+            if($file['error'] && $file['error']==UPLOAD_ERR_NO_FILE) {
+                unset($attachments[$i]); 
+                continue;
+            }
+
+            if($file['error']) //PHP defined error!
+                $file['error'] = 'File upload error #'.$file['error'];
+            elseif(!$file['tmp_name'] || !is_uploaded_file($file['tmp_name']))
+                $file['error'] = 'Invalid or bad upload POST';
+            elseif($restrict) { // make sure file type & size are allowed.
+                if(!$ost->isFileTypeAllowed($file))
+                    $file['error'] = 'Invalid file type for '.Format::htmlchars($file['name']);
+                elseif($ost->getConfig()->getMaxFileSize()
+                        && $file['size']>$ost->getConfig()->getMaxFileSize())
+                    $file['error'] = sprintf('File %s (%s) is too big. Maximum of %s allowed',
+                            Format::htmlchars($file['name']),
+                            Format::file_size($file['size']),
+                            Format::file_size($ost->getConfig()->getMaxFileSize()));
+            }
+        }
+        unset($file);
+
+        return array_filter($attachments);
+    }
+
     /**
      * Removes files and associated meta-data for files which no ticket,
      * canned-response, or faq point to any more.
diff --git a/include/class.format.php b/include/class.format.php
index 1ec32b733f5c4c2c671dbdc8773f0cfaca299fbf..7f6cc957b152d2e1b28a033ad481b3166bf7a667 100644
--- a/include/class.format.php
+++ b/include/class.format.php
@@ -34,19 +34,6 @@ class Format {
         return preg_replace('/\s+/', '_', $filename);
     }
 
-    /* re-arrange $_FILES array for the sane */
-    function files($files) {
-
-        foreach($files as $k => $a) {
-            if(is_array($a))
-                foreach($a as $i => $v)
-                    $result[$i][$k] = $v;
-        }
-
-        return $result?array_filter($result):$files;
-    }
-
-
     /* encode text into desired encoding - taking into accout charset when available. */
     function encode($text, $charset=null, $encoding='utf-8') {
 
@@ -109,24 +96,50 @@ class Format {
         return Format::html($html,array('safe'=>1,'balance'=>1));
     }
 
+    function sanitize($text, $striptags= true) {
+        
+        //balance and neutralize unsafe tags.
+        $text = Format::safe_html($text);
+
+        //If requested - strip tags with decoding disabled.
+        return $striptags?Format::striptags($text, false):$text;
+    }
+
     function htmlchars($var) {
+        return Format::htmlencode($var);
+    }
+
+    function htmlencode($var) {
         $flags = ENT_COMPAT | ENT_QUOTES;
         if (phpversion() >= '5.4.0')
             $flags |= ENT_HTML401;
+
         return is_array($var)
-            ? array_map(array('Format','htmlchars'),$var)
+            ? array_map(array('Format','htmlencode'), $var)
             : htmlentities($var, $flags, 'UTF-8');
     }
 
+    function htmldecode($var) {
+
+        if(is_array($var))
+            return array_map(array('Format','htmldecode'), $var);
+
+        $flags = ENT_COMPAT;
+        if (phpversion() >= '5.4.0')
+            $flags |= ENT_HTML401;
+            
+        return html_entity_decode($var, $flags, 'UTF-8');
+    }
+
     function input($var) {
-        return Format::htmlchars($var);
+        return Format::htmlencode($var);
     }
 
     //Format text for display..
     function display($text) {
         global $cfg;
 
-        $text=Format::htmlchars($text); //take care of html special chars
+        //make urls clickable.
         if($cfg && $cfg->clickableURLS() && $text)
             $text=Format::clickableurls($text);
 
@@ -140,14 +153,12 @@ class Format {
         return nl2br($text);
     }
 
-    function striptags($var) {
-        $flags = ENT_COMPAT;
-        if (phpversion() >= '5.4.0')
-            $flags |= ENT_HTML401;
-        return is_array($var)
-            ? array_map(array('Format','striptags'),$var)
-              //strip all tags ...no mercy!
-            : strip_tags(html_entity_decode($var, $flags, 'UTF-8'));
+    function striptags($var, $decode=true) {
+
+        if(is_array($var))
+            return array_map(array('Format','striptags'), $var, array_fill(0, count($var), $decode));
+
+        return strip_tags($decode?Format::htmldecode($var):$var);
     }
 
     //make urls clickable. Mainly for display 
diff --git a/include/class.mailer.php b/include/class.mailer.php
index debf2f849a5b09f9a1dd7466602999f42737dc8d..b4ec97c243718f1b86b3a669c8bc901a6c947096 100644
--- a/include/class.mailer.php
+++ b/include/class.mailer.php
@@ -94,7 +94,8 @@ class Mailer {
         //do some cleanup
         $to = preg_replace("/(\r\n|\r|\n)/s",'', trim($to));
         $subject = preg_replace("/(\r\n|\r|\n)/s",'', trim($subject));
-        $body = preg_replace("/(\r\n|\r)/s", "\n", trim($message));
+        //We're decoding html entities here becasuse we only support plain text for now - html support comming.
+        $body = Format::htmldecode(preg_replace("/(\r\n|\r)/s", "\n", trim($message)));
 
         /* Message ID - generated for each outgoing email */
         $messageId = sprintf('<%s%d-%s>', Misc::randCode(6), time(),
diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php
index 5253dfe818eae2e1d8c02757e8db647352cbb598..1bcf6d690cdbc244c233ae65b90ad334ed79185b 100644
--- a/include/class.mailfetch.php
+++ b/include/class.mailfetch.php
@@ -29,20 +29,20 @@ class MailFetcher {
 
     var $charset = 'UTF-8';
     var $encodings =array('UTF-8','WINDOWS-1251', 'ISO-8859-5', 'ISO-8859-1','KOI8-R');
-    
+
     function MailFetcher($email, $charset='UTF-8') {
 
-        
+
         if($email && is_numeric($email)) //email_id
             $email=Email::lookup($email);
 
         if(is_object($email))
             $this->ht = $email->getMailAccountInfo();
         elseif(is_array($email) && $email['host']) //hashtable of mail account info
-            $this->ht = $email; 
+            $this->ht = $email;
         else
             $this->ht = null;
-           
+
         $this->charset = $charset;
 
         if($this->ht) {
@@ -59,12 +59,12 @@ class MailFetcher {
             $this->srvstr=sprintf('{%s:%d/%s', $this->getHost(), $this->getPort(), $this->getProtocol());
             if(!strcasecmp($this->getEncryption(), 'SSL'))
                 $this->srvstr.='/ssl';
-        
+
             $this->srvstr.='/novalidate-cert}';
 
         }
 
-        //Set timeouts 
+        //Set timeouts
         if(function_exists('imap_timeout')) imap_timeout(1,20);
 
     }
@@ -92,7 +92,7 @@ class MailFetcher {
     function getUsername() {
         return $this->ht['username'];
     }
-    
+
     function getPassword() {
         return $this->ht['password'];
     }
@@ -112,7 +112,7 @@ class MailFetcher {
     }
 
     /* Core */
-    
+
     function connect() {
         return ($this->mbox && $this->ping())?$this->mbox:$this->open();
     }
@@ -123,7 +123,7 @@ class MailFetcher {
 
     /* Default folder is inbox - TODO: provide user an option to fetch from diff folder/label */
     function open($box='INBOX') {
-      
+
         if($this->mbox)
            $this->close();
 
@@ -157,7 +157,7 @@ class MailFetcher {
     function createMailbox($folder) {
 
         if(!$folder) return false;
-            
+
         return imap_createmailbox($this->mbox, imap_utf7_encode($this->srvstr.trim($folder)));
     }
 
@@ -187,23 +187,23 @@ class MailFetcher {
             $text=imap_qprint($text);
             break;
         }
-        
+
         return $text;
     }
 
     //Convert text to desired encoding..defaults to utf8
-    function mime_encode($text, $charset=null, $encoding='utf-8') { //Thank in part to afterburner 
+    function mime_encode($text, $charset=null, $encoding='utf-8') { //Thank in part to afterburner
         return Format::encode($text, $charset, $encoding);
     }
-    
+
     //Generic decoder - resulting text is utf8 encoded -> mirrors imap_utf8
     function mime_decode($text, $encoding='utf-8') {
-        
+
         $str = '';
         $parts = imap_mime_header_decode($text);
         foreach ($parts as $part)
             $str.= $this->mime_encode($part->text, $part->charset, $encoding);
-        
+
         return $str?$str:imap_utf8($text);
     }
 
@@ -215,12 +215,12 @@ class MailFetcher {
         $mimeType = array('TEXT', 'MULTIPART', 'MESSAGE', 'APPLICATION', 'AUDIO', 'IMAGE', 'VIDEO', 'OTHER');
         if(!$struct || !$struct->subtype)
             return 'TEXT/PLAIN';
-        
+
         return $mimeType[(int) $struct->type].'/'.$struct->subtype;
     }
 
     function getHeaderInfo($mid) {
-        
+
         if(!($headerinfo=imap_headerinfo($this->mbox, $mid)) || !$headerinfo->from)
             return null;
 
@@ -237,7 +237,7 @@ class MailFetcher {
 
     //search for specific mime type parts....encoding is the desired encoding.
     function getPart($mid, $mimeType, $encoding=false, $struct=null, $partNumber=false) {
-          
+
         if(!$struct && $mid)
             $struct=@imap_fetchstructure($this->mbox, $mid);
 
@@ -264,7 +264,7 @@ class MailFetcher {
         $text='';
         if($struct && $struct->parts) {
             while(list($i, $substruct) = each($struct->parts)) {
-                if($partNumber) 
+                if($partNumber)
                     $prefix = $partNumber . '.';
                 if(($result=$this->getPart($mid, $mimeType, $encoding, $substruct, $prefix.($i+1))))
                     $text.=$result;
@@ -332,29 +332,28 @@ class MailFetcher {
         return imap_fetchheader($this->mbox, $mid,FT_PREFETCHTEXT);
     }
 
-    
+
     function getPriority($mid) {
         return Mail_Parse::parsePriority($this->getHeader($mid));
     }
 
     function getBody($mid) {
-        
+
         $body ='';
         if(!($body = $this->getPart($mid,'TEXT/PLAIN', $this->charset))) {
             if(($body = $this->getPart($mid,'TEXT/HTML', $this->charset))) {
                 //Convert tags of interest before we striptags
                 $body=str_replace("</DIV><DIV>", "\n", $body);
                 $body=str_replace(array("<br>", "<br />", "<BR>", "<BR />"), "\n", $body);
-                $body=Format::html($body); //Balance html tags before stripping.
-                $body=Format::striptags($body); //Strip tags??
+                $body=Format::safe_html($body); //Balance html tags & neutralize unsafe tags.
             }
         }
 
         return $body;
     }
 
-    //email to ticket 
-    function createTicket($mid) { 
+    //email to ticket
+    function createTicket($mid) {
         global $ost;
 
         if(!($mailinfo = $this->getHeaderInfo($mid)))
@@ -387,7 +386,7 @@ class MailFetcher {
 
         if($ost->getConfig()->useEmailPriority())
             $vars['priorityId']=$this->getPriority($mid);
-       
+
         $ticket=null;
         $newticket=true;
         //Check the subject line for possible ID.
@@ -397,14 +396,14 @@ class MailFetcher {
             if(!($ticket=Ticket::lookupByExtId($tid, $vars['email'])))
                 $ticket=null;
         }
-        
+
         $errors=array();
         if($ticket) {
-            if(!($msgid=$ticket->postMessage($vars, 'Email')))
+            if(!($message=$ticket->postMessage($vars, 'Email')))
                 return false;
 
         } elseif (($ticket=Ticket::create($vars, $errors, 'Email'))) {
-            $msgid = $ticket->getLastMsgId();
+            $message = $ticket->getLastMessage();
         } else {
             //Report success if the email was absolutely rejected.
             if(isset($errors['errno']) && $errors['errno'] == 403)
@@ -421,19 +420,22 @@ class MailFetcher {
         }
 
         //Save attachments if any.
-        if($msgid 
+        if($message
                 && $ost->getConfig()->allowEmailAttachments()
-                && ($struct = imap_fetchstructure($this->mbox, $mid)) 
-                && $struct->parts 
+                && ($struct = imap_fetchstructure($this->mbox, $mid))
+                && $struct->parts
                 && ($attachments=$this->getAttachments($struct))) {
-                
+
             foreach($attachments as $a ) {
-                $file = array(
-                        'name'  => $a['name'],
-                        'type'  => $a['type'],
-                        'data'  => $this->decode(imap_fetchbody($this->mbox, $mid, $a['index']), $a['encoding'])
-                        );
-                $ticket->importAttachments(array($file), $msgid, 'M');
+                $file = array('name'  => $a['name'], 'type'  => $a['type']);
+
+                //Check the file  type
+                if(!$ost->isFileTypeAllowed($file))
+                    $file['error'] = 'Invalid file type (ext) for '.Format::htmlchars($file['name']);
+                else //only fetch the body if necessary TODO: Make it a callback.
+                    $file['data'] = $this->decode(imap_fetchbody($this->mbox, $mid, $a['index']), $a['encoding']);
+
+                $message->importAttachment($file);
             }
         }
 
@@ -491,11 +493,11 @@ class MailFetcher {
     /*
        MailFetcher::run()
 
-       Static function called to initiate email polling 
+       Static function called to initiate email polling
      */
     function run() {
         global $ost;
-      
+
         if(!$ost->getConfig()->isEmailPollingEnabled())
             return;
 
@@ -507,7 +509,7 @@ class MailFetcher {
             return;
         }
 
-        //Hardcoded error control... 
+        //Hardcoded error control...
         $MAXERRORS = 5; //Max errors before we start delayed fetch attempts
         $TIMEOUT = 10; //Timeout in minutes after max errors is reached.
 
diff --git a/include/class.mailparse.php b/include/class.mailparse.php
index 342fc5298b59f55a204d86248af0920e7dbe2b0e..3e412675a3ada7ff0d319b15f2e8a89f4b9cade9 100644
--- a/include/class.mailparse.php
+++ b/include/class.mailparse.php
@@ -19,16 +19,16 @@ require_once(PEAR_DIR.'Mail/mimeDecode.php');
 require_once(PEAR_DIR.'Mail/RFC822.php');
 
 class Mail_Parse {
-    
+
     var $mime_message;
     var $include_bodies;
     var $decode_headers;
     var $decode_bodies;
-    
+
     var $struct;
-    
+
     function Mail_parse($mimeMessage,$includeBodies=true,$decodeHeaders=TRUE,$decodeBodies=TRUE){
-        
+
         $this->mime_message=$mimeMessage;
         $this->include_bodies=$includeBodies;
         $this->decode_headers=$decodeHeaders;
@@ -42,9 +42,9 @@ class Mail_Parse {
                         'include_bodies'=> $this->include_bodies,
                         'decode_headers'=> $this->decode_headers,
                         'decode_bodies' => $this->decode_bodies);
-        $this->splitBodyHeader();    
+        $this->splitBodyHeader();
         $this->struct=Mail_mimeDecode::decode($params);
-        
+
         return (PEAR::isError($this->struct) || !(count($this->struct->headers)>1))?FALSE:TRUE;
     }
 
@@ -94,7 +94,7 @@ class Mail_Parse {
         }
         return $array;
     }
-    
+
 
     function getStruct(){
         return $this->struct;
@@ -109,8 +109,8 @@ class Mail_Parse {
     function getError(){
         return PEAR::isError($this->struct)?$this->struct->getMessage():'';
     }
-   
-    
+
+
     function getFromAddressList(){
         return Mail_Parse::parseAddressList($this->struct->headers['from']);
     }
@@ -119,7 +119,7 @@ class Mail_Parse {
         //Delivered-to incase it was a BBC mail.
        return Mail_Parse::parseAddressList($this->struct->headers['to']?$this->struct->headers['to']:$this->struct->headers['delivered-to']);
     }
-        
+
     function getCcAddressList(){
         return $this->struct->headers['cc']?Mail_Parse::parseAddressList($this->struct->headers['cc']):null;
     }
@@ -127,27 +127,27 @@ class Mail_Parse {
     function getMessageId(){
         return $this->struct->headers['message-id'];
     }
- 
+
     function getSubject(){
         return $this->struct->headers['subject'];
     }
-    
+
     function getBody(){
-        
+
         $body='';
         if(!($body=$this->getPart($this->struct,'text/plain'))) {
             if(($body=$this->getPart($this->struct,'text/html'))) {
                 //Cleanup the html.
-                $body=str_replace("</DIV><DIV>", "\n", $body);                        
+                $body=str_replace("</DIV><DIV>", "\n", $body);
                 $body=str_replace(array("<br>", "<br />", "<BR>", "<BR />"), "\n", $body);
-                $body=Format::striptags(Format::html($body));
+                $body=Format::safe_html($body); //Balance html tags & neutralize unsafe tags.
             }
         }
         return $body;
     }
-    
+
     function getPart($struct,$ctypepart) {
-        
+
         if($struct && !$struct->parts) {
             $ctype = @strtolower($struct->ctype_primary.'/'.$struct->ctype_secondary);
             if($ctype && strcasecmp($ctype,$ctypepart)==0)
@@ -164,7 +164,7 @@ class Mail_Parse {
         return $data;
     }
 
-     
+
     function mime_encode($text, $charset=null, $encoding='utf-8') {
         return Format::encode($text, $charset, $encoding);
     }
@@ -175,15 +175,15 @@ class Mail_Parse {
             $part=$this->getStruct();
 
         if($part && $part->disposition
-                && (!strcasecmp($part->disposition,'attachment') 
-                    || !strcasecmp($part->disposition,'inline') 
+                && (!strcasecmp($part->disposition,'attachment')
+                    || !strcasecmp($part->disposition,'inline')
                     || !strcasecmp($part->ctype_primary,'image'))){
-            
+
             if(!($filename=$part->d_parameters['filename']) && $part->d_parameters['filename*'])
                 $filename=$part->d_parameters['filename*']; //Do we need to decode?
-           
+
             $file=array(
-                    'name'  => $filename, 
+                    'name'  => $filename,
                     'type'  => strtolower($part->ctype_primary.'/'.$part->ctype_secondary),
                     'data'  => $this->mime_encode($part->body, $part->ctype_parameters['charset'])
                     );
@@ -245,8 +245,9 @@ class EmailDataParser {
     function EmailDataParser($stream=null) {
         $this->stream = $stream;
     }
-    
+
     function parse($stream) {
+        global $cfg;
 
         $contents ='';
         if(is_resource($stream)) {
@@ -260,7 +261,7 @@ class EmailDataParser {
         $parser= new Mail_Parse($contents);
         if(!$parser->decode()) //Decode...returns false on decoding errors
             return $this->err('Email parse failed ['.$parser->getError().']');
-        
+
         $data =array();
         //FROM address: who sent the email.
         if(($fromlist = $parser->getFromAddressList()) && !PEAR::isError($fromlist)) {
@@ -293,7 +294,7 @@ class EmailDataParser {
                     break;
             }
         }
-            
+
         $data['subject'] = Format::utf8encode($parser->getSubject());
         $data['message'] = Format::utf8encode(Format::stripEmptyLines($parser->getBody()));
         $data['header'] = $parser->getHeader();
@@ -301,8 +302,8 @@ class EmailDataParser {
         $data['priorityId'] = $parser->getPriority();
         $data['emailId'] = $emailId;
 
-        //attachments XXX: worry about encoding??
-        $data['attachments'] = $parser->getAttachments();
+        if($cfg && $cfg->allowEmailAttachments())
+            $data['attachments'] = $parser->getAttachments();
 
         return $data;
     }
diff --git a/include/class.osticket.php b/include/class.osticket.php
index c357ba0e619557954045828fe9b33d32a8ad0874..4445aca6cd116d85c7f54e7f6cb3eca956131d89 100644
--- a/include/class.osticket.php
+++ b/include/class.osticket.php
@@ -146,33 +146,6 @@ class osTicket {
         return ($ext && is_array($allowed) && in_array(".$ext", $allowed));
     }
 
-    /* Function expects a well formatted array - see  Format::files()
-       It's up to the caller to reject the upload on error.
-     */
-    function validateFileUploads(&$files, $checkFileTypes=true) {
-       
-        $errors=0;
-        foreach($files as &$file) {
-            //skip no file upload "error" - why PHP calls it an error is beyond me.
-            if($file['error'] && $file['error']==UPLOAD_ERR_NO_FILE) continue;
-
-            if($file['error']) //PHP defined error!
-                $file['error'] = 'File upload error #'.$file['error'];
-            elseif(!$file['tmp_name'] || !is_uploaded_file($file['tmp_name']))
-                $file['error'] = 'Invalid or bad upload POST';
-            elseif($checkFileTypes && !$this->isFileTypeAllowed($file))
-                $file['error'] = 'Invalid file type for '.Format::htmlchars($file['name']);
-            elseif($file['size']>$this->getConfig()->getMaxFileSize())
-                $file['error'] = sprintf('File (%s) is too big. Maximum of %s allowed',
-                        Format::htmlchars($file['name']),
-                        Format::file_size($this->getConfig()->getMaxFileSize()));
-            
-            if($file['error']) $errors++;
-        }
-
-        return (!$errors);
-    }
-
     /* Replace Template Variables */
     function replaceTemplateVariables($input, $vars=array()) {
         
diff --git a/include/class.pdf.php b/include/class.pdf.php
index b0650b7796de3c2932cc1f24e0dd6527f5a1f46a..f210149346c7d36d561bdca8bc3fb9ad59771226 100644
--- a/include/class.pdf.php
+++ b/include/class.pdf.php
@@ -21,11 +21,11 @@ require (FPDF_DIR . 'fpdf.php');
 
 class Ticket2PDF extends FPDF
 {
-	
+
 	var $includenotes = false;
-	
+
 	var $pageOffset = 0;
-	
+
     var $ticket = null;
 
 	function Ticket2PDF($ticket, $psize='Letter', $notes=false) {
@@ -47,7 +47,7 @@ class Ticket2PDF extends FPDF
     function getTicket() {
         return $this->ticket;
     }
-	
+
 	//report header...most stuff are hard coded for now...
 	function Header() {
         global $cfg;
@@ -66,7 +66,7 @@ class Ticket2PDF extends FPDF
         $this->Cell(0, 5, 'Date & Time based on GMT '.$_SESSION['TZ_OFFSET'], 0, 1, 'R');
 		$this->Ln(10);
 	}
-	
+
 	//Page footer baby
 	function Footer() {
         global $thisstaff;
@@ -94,10 +94,10 @@ class Ticket2PDF extends FPDF
 
         if(function_exists('iconv'))
             return iconv('UTF-8', 'windows-1252', $text);
-        
+
         return utf8_encode($text);
     }
-    
+
     function _print() {
 
         if(!($ticket=$this->getTicket()))
@@ -107,7 +107,7 @@ class Ticket2PDF extends FPDF
         $l = 35;
         $c = $w-$l;
 
-        
+
         $this->SetFont('Arial', 'B', 11);
         $this->cMargin = 0;
         $this->SetFont('Arial', 'B', 11);
@@ -215,7 +215,11 @@ class Ticket2PDF extends FPDF
                         'R'=>array(255, 224, 179),
                         'N'=>array(250, 250, 210));
         //Get ticket thread
-        if(($entries = $ticket->getThread(($this->includenotes)))) { 
+        $types = array('M', 'R');
+        if($this->includenotes)
+            $types[] = 'N';
+
+        if(($entries = $ticket->getThreadEntries($types))) {
             foreach($entries as $entry) {
 
                 $color = $colors[$entry['thread_type']];
@@ -228,11 +232,12 @@ class Ticket2PDF extends FPDF
                 $this->Cell($w/2, 7, $entry['poster'], 'TBR', 1, 'L', true);
                 $this->SetFont('');
                 $text= $entry['body'];
-                if($entry['attachments'] 
-                        && ($attachments = $ticket->getAttachments($entry['id'], $entry['thread_type']))) {
+                if($entry['attachments']
+                        && ($tentry=$ticket->getThreadEntry($entry['id']))
+                        && ($attachments = $tentry->getAttachments())) {
                     foreach($attachments as $attachment)
                         $files[]= $attachment['name'];
-                    
+
                     $text.="\nFiles Attached: [".implode(', ',$files)."]\n";
                 }
                 $this->WriteText($w*2, $text, 1);
@@ -240,6 +245,6 @@ class Ticket2PDF extends FPDF
             }
         }
 
-    }	
+    }
 }
 ?>
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;
     }
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 9dd35529defaa69a4fda345c840fb67271046a8a..94733adc14eb52fe26847297fb1d294a94bb7f72 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -34,29 +34,12 @@ include_once(INCLUDE_DIR.'class.canned.php');
 class Ticket {
 
     var $id;
-    var $extid;
-    var $email;
-    var $status;
-    var $created;
-    var $reopened;
-    var $updated;
-    var $lastrespdate;
-    var $lastmsgdate;
-    var $duedate;
-    var $priority;
-    var $priority_id;
-    var $fullname;
-    var $staff_id;
-    var $team_id;
-    var $dept_id;
-    var $topic_id;
-    var $dept_name;
-    var $subject;
-    var $helptopic;
-    var $overdue;
+    var $number;
+
+    var $ht;
 
     var $lastMsgId;
-    
+
     var $dept;  //Dept obj
     var $sla;   // SLA obj
     var $staff; //Staff obj
@@ -64,25 +47,22 @@ class Ticket {
     var $team;  //Team obj
     var $topic; //Topic obj
     var $tlock; //TicketLock obj
-    
-    function Ticket($id){
+
+    var $thread; //Thread obj.
+
+    function Ticket($id) {
         $this->id = 0;
         $this->load($id);
     }
-    
+
     function load($id=0) {
 
         if(!$id && !($id=$this->getId()))
             return false;
 
-        //TODO: delete helptopic field in ticket table.
-       
         $sql='SELECT  ticket.*, lock_id, dept_name, priority_desc '
-            .' ,IF(sla.id IS NULL, NULL, DATE_ADD(ticket.created, INTERVAL sla.grace_period HOUR)) as sla_duedate ' 
+            .' ,IF(sla.id IS NULL, NULL, DATE_ADD(ticket.created, INTERVAL sla.grace_period HOUR)) as sla_duedate '
             .' ,count(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 '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.dept_id) '
             .' LEFT JOIN '.SLA_TABLE.' sla ON (ticket.sla_id=sla.id AND sla.isactive=1) '
@@ -92,12 +72,6 @@ class Ticket {
                 .'ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW()) '
             .' 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($id)
             .' GROUP BY ticket.ticket_id';
 
@@ -105,35 +79,12 @@ class Ticket {
         if(!($res=db_query($sql)) || !db_num_rows($res))
             return false;
 
-        
-        $this->ht=db_fetch_array($res);
-        
+
+        $this->ht = db_fetch_array($res);
+
         $this->id       = $this->ht['ticket_id'];
-        $this->extid    = $this->ht['ticketID'];
-         
-        $this->email    = $this->ht['email'];
-        $this->fullname = $this->ht['name'];
-        $this->status   = $this->ht['status'];
-        $this->created  = $this->ht['created'];
-        $this->reopened = $this->ht['reopened'];
-        $this->updated  = $this->ht['updated'];
-        $this->duedate  = $this->ht['duedate'];
-        $this->closed   = $this->ht['closed'];
-        $this->lastmsgdate  = $this->ht['lastmessagedate'];
-        $this->lastrespdate = $this->ht['lastresponsedate'];
-        
-        $this->lock_id  = $this->ht['lock_id'];
-        $this->priority_id = $this->ht['priority_id'];
-        $this->priority = $this->ht['priority_desc'];
-        $this->staff_id = $this->ht['staff_id'];
-        $this->team_id = $this->ht['team_id']; 
-        $this->dept_id  = $this->ht['dept_id'];
-        $this->dept_name = $this->ht['dept_name'];
-        $this->sla_id = $this->ht['sla_id'];
-        $this->topic_id = $this->ht['topic_id'];
-        $this->subject = $this->ht['subject'];
-        $this->overdue = $this->ht['isoverdue'];
-        
+        $this->number   = $this->ht['ticketID'];
+
         //Reset the sub classes (initiated ondemand)...good for reloads.
         $this->staff = null;
         $this->client = null;
@@ -143,14 +94,18 @@ class Ticket {
         $this->tlock = null;
         $this->stats = null;
         $this->topic = null;
-        
+        $this->thread = null;
+
+        //REQUIRED: Preload thread obj - checked on lookup!
+        $this->getThread();
+
         return true;
     }
-        
+
     function reload() {
         return $this->load();
     }
-    
+
     function isOpen() {
         return (strcasecmp($this->getStatus(),'Open')==0);
     }
@@ -168,9 +123,9 @@ class Ticket {
     }
 
     function isOverdue() {
-        return ($this->overdue);
+        return ($this->ht['isoverdue']);
     }
-    
+
     function isAnswered() {
        return ($this->ht['isanswered']);
     }
@@ -195,10 +150,10 @@ class Ticket {
         if(!is_object($client) && !($client=Client::lookup($client)))
             return false;
 
-        if(!strcasecmp($client->getEmail(),$this->getEmail()))
+        if(!strcasecmp($client->getEmail(), $this->getEmail()))
             return true;
 
-        return ($cfg && $cfg->showRelatedTickets() 
+        return ($cfg && $cfg->showRelatedTickets()
             && $client->getTicketId()==$this->getExtId());
     }
 
@@ -208,15 +163,15 @@ class Ticket {
     }
 
     function getExtId() {
-        return  $this->extid;
+        return  $this->getNumber();
     }
 
     function getNumber() {
-        return $this->getExtId();
+        return $this->number;
     }
-   
-    function getEmail(){
-        return $this->email;
+
+    function getEmail() {
+        return $this->ht['email'];
     }
 
     function getAuthToken() {
@@ -224,25 +179,25 @@ class Ticket {
         return md5($this->getId() . $this->getEmail() . SECRET_SALT);
     }
 
-    function getName(){
-        return $this->fullname;
+    function getName() {
+        return $this->ht['name'];
     }
 
     function getSubject() {
-        return $this->subject;
+        return $this->ht['subject'];
     }
 
     /* Help topic title  - NOT object -> $topic */
     function getHelpTopic() {
 
-        if(!$this->helpTopic && ($topic=$this->getTopic()))
-            $this->helpTopic = $topic->getName();
-            
-        return $this->helpTopic;
+        if(!$this->ht['helptopic'] && ($topic=$this->getTopic()))
+            $this->ht['helptopic'] = $topic->getName();
+
+        return $this->ht['helptopic'];
     }
-   
-    function getCreateDate(){
-        return $this->created;
+
+    function getCreateDate() {
+        return $this->ht['created'];
     }
 
     function getOpenDate() {
@@ -250,15 +205,15 @@ class Ticket {
     }
 
     function getReopenDate() {
-        return $this->reopened;
+        return $this->ht['reopened'];
     }
-    
-    function getUpdateDate(){
-        return $this->updated;
+
+    function getUpdateDate() {
+        return $this->ht['updated'];
     }
 
-    function getDueDate(){
-        return $this->duedate;
+    function getDueDate() {
+        return $this->ht['duedate'];
     }
 
     function getSLADueDate() {
@@ -267,7 +222,7 @@ class Ticket {
 
     function getEstDueDate() {
 
-        //Real due date 
+        //Real due date
         if(($duedate=$this->getDueDate()))
             return $duedate;
 
@@ -275,30 +230,34 @@ class Ticket {
         return $this->getSLADueDate();
     }
 
-    function getCloseDate(){
-        return $this->closed;
+    function getCloseDate() {
+        return $this->ht['closed'];
     }
 
-    function getStatus(){
-        return $this->status;
+    function getStatus() {
+        return $this->ht['status'];
     }
-   
-    function getDeptId(){
-       return $this->dept_id;
+
+    function getDeptId() {
+       return $this->ht['dept_id'];
     }
-   
-    function getDeptName(){
-       return $this->dept_name;
+
+    function getDeptName() {
+
+        if(!$this->ht['dept_name'] && ($dept = $this->getDept()))
+            $this->ht['dept_name'] = $dept->getName();
+
+       return $this->ht['dept_name'];
     }
 
     function getPriorityId() {
-        return $this->priority_id;
+        return $this->ht['priority_id'];
     }
-    
-    function getPriority() {
-        return $this->priority;
+
+    function getPriority() { //TODO: Make it an obj.
+        return  $this->ht['priority_desc'];
     }
-     
+
     function getPhone() {
         return $this->ht['phone'];
     }
@@ -318,7 +277,7 @@ class Ticket {
     function getSource() {
         return $this->ht['source'];
     }
-    
+
     function getIP() {
         return $this->ht['ip_address'];
     }
@@ -341,24 +300,24 @@ class Ticket {
                     'duedate'   =>  $this->getDueDate()?(Format::userdate('m/d/Y', Misc::db2gmtime($this->getDueDate()))):'',
                     'time'  =>  $this->getDueDate()?(Format::userdate('G:i', Misc::db2gmtime($this->getDueDate()))):'',
                     );
-                  
+
         return $info;
     }
 
     function getLockId() {
-        return $this->lock_id;
+        return $this->ht['lock_id'];
     }
-    
-    function getLock(){
-        
+
+    function getLock() {
+
         if(!$this->tlock && $this->getLockId())
-            $this->tlock= TicketLock::lookup($this->getLockId(),$this->getId());
-        
+            $this->tlock= TicketLock::lookup($this->getLockId(), $this->getId());
+
         return $this->tlock;
     }
-    
+
     function acquireLock($staffId, $lockTime) {
-       
+
         if(!$staffId or !$lockTime) //Lockig disabled?
             return null;
 
@@ -369,18 +328,18 @@ class Ticket {
 
             //Lock already exits...renew it
             $lock->renew($lockTime); //New clock baby.
-            
+
             return $lock;
         }
         //No lock on the ticket or it is expired
-        $this->tlock=null; //clear crap
-        $this->lock_id=TicketLock::acquire($this->getId(), $staffId, $lockTime); //Create a new lock..
+        $this->tlock = null; //clear crap
+        $this->ht['lock_id'] = TicketLock::acquire($this->getId(), $staffId, $lockTime); //Create a new lock..
         //load and return the newly created lock if any!
         return $this->getLock();
     }
-    
-    function getDept(){
-        
+
+    function getDept() {
+
         if(!$this->dept && $this->getDeptId())
             $this->dept= Dept::lookup($this->getDeptId());
 
@@ -394,12 +353,12 @@ class Ticket {
 
         return $this->client;
     }
-    
-    function getStaffId(){
-        return $this->staff_id;
+
+    function getStaffId() {
+        return $this->ht['staff_id'];
     }
 
-    function getStaff(){
+    function getStaff() {
 
         if(!$this->staff && $this->getStaffId())
             $this->staff= Staff::lookup($this->getStaffId());
@@ -407,11 +366,11 @@ class Ticket {
         return $this->staff;
     }
 
-    function getTeamId(){
-        return $this->team_id;
+    function getTeamId() {
+        return $this->ht['team_id'];
     }
 
-    function getTeam(){
+    function getTeam() {
 
         if(!$this->team && $this->getTeamId())
             $this->team = Team::lookup($this->getTeamId());
@@ -431,11 +390,11 @@ class Ticket {
     }
 
     function getAssignees() {
-     
+
         $assignees=array();
         if($staff=$this->getStaff())
             $assignees[] = $staff->getName();
-                       
+
         if($team=$this->getTeam())
             $assignees[] = $team->getName();
 
@@ -448,10 +407,10 @@ class Ticket {
     }
 
     function getTopicId() {
-        return $this->topic_id;
+        return $this->ht['topic_id'];
     }
 
-    function getTopic() { 
+    function getTopic() {
 
         if(!$this->topic && $this->getTopicId())
             $this->topic = Topic::lookup($this->getTopicId());
@@ -459,9 +418,9 @@ class Ticket {
         return $this->topic;
     }
 
- 
+
     function getSLAId() {
-        return $this->sla_id;
+        return $this->ht['sla_id'];
     }
 
     function getSLA() {
@@ -483,7 +442,7 @@ class Ticket {
 
         if(!($res=db_query($sql)) || !db_num_rows($res))
             return null;
-            
+
         list($id)=db_fetch_row($res);
 
         return Staff::lookup($id);
@@ -491,19 +450,7 @@ class Ticket {
     }
 
     function getLastMessageDate() {
-
-        if($this->lastmsgdate)
-            return $this->lastmsgdate;
-
-        //for old versions...XXX: still needed????
-        $sql='SELECT created FROM '.TICKET_THREAD_TABLE
-            .' WHERE ticket_id='.db_input($this->getId())
-            ."   AND thread_type = 'M'"
-            .' ORDER BY created DESC LIMIT 1';
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($this->lastmsgdate)=db_fetch_row($res);
-
-        return $this->lastmsgdate;
+        return $this->ht['lastmessage'];
     }
 
     function getLastMsgDate() {
@@ -511,35 +458,28 @@ class Ticket {
     }
 
     function getLastResponseDate() {
-               
-        if($this->lastrespdate)
-            return $this->lastrespdate;
-
-        $sql='SELECT created FROM '.TICKET_THREAD_TABLE
-            .' WHERE ticket_id='.db_input($this->getId())
-            .'   AND thread_type="R"'
-            .' ORDER BY created DESC LIMIT 1';
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($this->lastrespdate)=db_fetch_row($res);
-
-        return $this->lastrespdate;
+        return $this->ht['lastresponse'];
     }
 
     function getLastRespDate() {
         return $this->getLastResponseDate();
     }
 
-        
+
     function getLastMsgId() {
         return $this->lastMsgId;
     }
 
-    function getRelatedTicketsCount(){
+    function getLastMessage() {
+        return Message::lookup($this->getLastMsgId(), $this->getId());
+    }
+
+    function getThread() {
 
-        $sql='SELECT count(*)  FROM '.TICKET_TABLE
-            .' WHERE email='.db_input($this->getEmail());
+        if(!$this->thread)
+            $this->thread = Thread::lookup($this);
 
-        return db_result(db_query($sql));
+        return $this->thread;
     }
 
     function getThreadCount() {
@@ -547,121 +487,42 @@ class Ticket {
     }
 
     function getNumMessages() {
-        return $this->ht['messages'];
+        return $this->getThread()->getNumMessages();
     }
 
     function getNumResponses() {
-        return $this->ht['responses'];
+        return $this->getThread()->getNumResponses();
     }
 
     function getNumNotes() {
-        return $this->ht['notes'];
+        return $this->getThread()->getNumNotes();
     }
 
     function getMessages() {
-        return $this->getThreadByType('M');
+        return $this->getThreadEntries('M');
     }
 
-    function getResponses($msgId=0) {
-        return $this->getThreadByType('R', $msgId);
+    function getResponses() {
+        return $this->getThreadEntries('R');
     }
 
     function getNotes() {
-        return $this->getThreadByType('N');
+        return $this->getThreadEntries('N');
     }
 
     function getClientThread() {
-        return $this->getThreadWithoutNotes();
-    }
-
-    function getThreadWithNotes() {
-        return $this->getThread(true);
+        return $this->getThreadEntries(array('M', 'R'));
     }
-    
-    function getThreadWithoutNotes() {
-        return $this->getThread(false);
-    }
-
-    function getThread($includeNotes=false, $order='') {
-
-        $treadtypes=array('M', 'R'); // messages and responses.
-        if($includeNotes) //Include notes??
-            $treadtypes[] = 'N';
-
-        return $this->getThreadByType($treadtypes, $order);
-    }
-        
-    function getThreadByType($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->getId());
-
-        if($type && is_array($type))
-            $sql.=" AND thread.thread_type IN('".implode("','", $type)."')";
-        elseif($type)
-            $sql.=' AND thread.thread_type='.db_input($type);
-
-        $sql.=' GROUP BY thread.id '
-             .' ORDER BY thread.created '.$order;
-
-        $thread=array();
-        if(($res=db_query($sql)) && db_num_rows($res))
-            while($rec=db_fetch_array($res))
-                $thread[] = $rec;
 
-        return $thread;
+    function getThreadEntry($id) {
+        return $this->getThread()->getEntry($id);
     }
 
-    function getAttachments($refId=0, $type=null) {
-
-        if($refId && !$type)
-            return NULL;
-
-        //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->getId());
-       
-        if($refId) 
-            $sql.=' AND a.ref_id='.db_input($refId);
-
-        if($type)
-            $sql.=' AND a.ref_type='.db_input($type);
-
-        $attachments = array();
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while($rec=db_fetch_array($res))
-                $attachments[] = $rec;
-        }
-
-        return $attachments;
+    function getThreadEntries($type, $order='') {
+        return $this->getThread()->getEntries($type, $order);
     }
 
-    function getAttachmentsLinks($refId, $type, $separator=' ',$target='') {
 
-        $str='';
-        foreach($this->getAttachments($refId, $type) as $attachment ) {
-            /* The has here can be changed  but must match validation in attachment.php */
-            $hash=md5($attachment['file_id'].session_id().$attachment['file_hash']); 
-            if($attachment['size'])
-                $size=sprintf('<em>(%s)</em>', Format::file_size($attachment['size']));
-                
-            $str.=sprintf('<a class="Icon file" href="attachment.php?id=%d&h=%s" target="%s">%s</a>%s&nbsp;%s',
-                    $attachment['attach_id'], $hash, $target, Format::htmlchars($attachment['name']), $size, $separator);
-        }
-
-        return $str;
-    }
 
     /* -------------------- Setters --------------------- */
     function setLastMsgId($msgid) {
@@ -671,10 +532,10 @@ class Ticket {
     function setPriority($priorityId) {
 
         //XXX: what happens to SLA priority???
-        
-        if(!$priorityId || $priorityId==$this->getPriorityId()) 
+
+        if(!$priorityId || $priorityId==$this->getPriorityId())
             return ($priorityId);
-        
+
         $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW() '
             .', priority_id='.db_input($priorityId)
             .' WHERE ticket_id='.db_input($this->getId());
@@ -683,31 +544,33 @@ class Ticket {
     }
 
     //DeptId can NOT be 0. No orphans please!
-    function setDeptId($deptId){
-       
+    function setDeptId($deptId) {
+
         //Make sure it's a valid department//
         if(!($dept=Dept::lookup($deptId)) || $dept->getId()==$this->getDeptId())
             return false;
 
-      
+
         $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), dept_id='.db_input($deptId)
             .' WHERE ticket_id='.db_input($this->getId());
 
         return (db_query($sql) && db_affected_rows());
     }
- 
+
     //Set staff ID...assign/unassign/release (id can be 0)
     function setStaffId($staffId) {
 
         if(!is_numeric($staffId)) return false;
-       
+
         $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), staff_id='.db_input($staffId)
             .' WHERE ticket_id='.db_input($this->getId());
 
         if (!db_query($sql)  || !db_affected_rows())
             return false;
-        
-        $this->staff_id = $staffId;
+
+        $this->staff = null;
+        $this->ht['staff_id'] = $staffId;
+
         return true;
     }
 
@@ -751,9 +614,9 @@ class Ticket {
 
     //Set team ID...assign/unassign/release (id can be 0)
     function setTeamId($teamId) {
-        
+
         if(!is_numeric($teamId)) return false;
-      
+
         $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), team_id='.db_input($teamId)
             .' WHERE ticket_id='.db_input($this->getId());
 
@@ -763,7 +626,7 @@ class Ticket {
     //Status helper.
     function setStatus($status) {
 
-        if(strcasecmp($this->getStatus(),$status)==0)
+        if(strcasecmp($this->getStatus(), $status)==0)
             return true; //No changes needed.
 
         switch(strtolower($status)) {
@@ -818,11 +681,11 @@ class Ticket {
     }
 
     //Close the ticket
-    function close(){
+    function close() {
         global $thisstaff;
-        
+
         $sql='UPDATE '.TICKET_TABLE.' SET closed=NOW(),isoverdue=0, duedate=NULL, updated=NOW(), status='.db_input('closed');
-        if($thisstaff) //Give the closing  staff credit. 
+        if($thisstaff) //Give the closing  staff credit.
             $sql.=', staff_id='.db_input($thisstaff->getId());
 
         $sql.=' WHERE ticket_id='.db_input($this->getId());
@@ -837,14 +700,14 @@ class Ticket {
     }
 
     //set status to open on a closed ticket.
-    function reopen($isanswered=0){
+    function reopen($isanswered=0) {
 
         $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), reopened=NOW() '
             .' ,status='.db_input('open')
             .' ,isanswered='.db_input($isanswered)
             .' WHERE ticket_id='.db_input($this->getId());
 
-        //TODO: log reopen event here 
+        //TODO: log reopen event here
 
         $this->logEvent('reopened', 'closed');
         return (db_query($sql) && db_affected_rows());
@@ -854,46 +717,46 @@ class Ticket {
         global $cfg;
 
         //Log stuff here...
-        
+
         if(!$autorespond && !$alertstaff) return true; //No alerts to send.
 
         /* ------ SEND OUT NEW TICKET AUTORESP && ALERTS ----------*/
-        
+
         $this->reload(); //get the new goodies.
         $dept= $this->getDept();
 
         if(!$dept || !($tpl = $dept->getTemplate()))
             $tpl= $cfg->getDefaultTemplate();
-        
+
         if(!$tpl) return false;  //bail out...missing stuff.
 
         if(!$dept || !($email=$dept->getAutoRespEmail()))
             $email =$cfg->getDefaultEmail();
 
         //Send auto response - if enabled.
-        if($autorespond && $email && $cfg->autoRespONNewTicket() 
-                && $dept->autoRespONNewTicket() 
+        if($autorespond && $email && $cfg->autoRespONNewTicket()
+                && $dept->autoRespONNewTicket()
                 &&  ($msg=$tpl->getAutoRespMsgTemplate())) {
-            
-            $msg = $this->replaceVars($msg, 
+
+            $msg = $this->replaceVars($msg,
                     array('message' => $message,
                           'signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')
                     );
 
             if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator()))
                 $msg['body'] ="\n$tag\n\n".$msg['body'];
-            
+
             $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body']);
         }
-        
+
         if(!($email=$cfg->getAlertEmail()))
             $email =$cfg->getDefaultEmail();
-          
+
         //Send alert to out sleepy & idle staff.
         if($alertstaff && $email
-                && $cfg->alertONNewTicket() 
+                && $cfg->alertONNewTicket()
                 && ($msg=$tpl->getNewTicketAlertMsgTemplate())) {
-                    
+
             $msg = $this->replaceVars($msg, array('message' => $message));
 
             $recipients=$sentlist=array();
@@ -903,26 +766,26 @@ class Ticket {
                 $email->sendAlert($cfg->getAdminEmail(), $msg['subj'], $alert);
                 $sentlist[]=$cfg->getAdminEmail();
             }
-              
+
             //Only alerts dept members if the ticket is NOT assigned.
             if($cfg->alertDeptMembersONNewTicket() && !$this->isAssigned()) {
                 if(($members=$dept->getMembers()))
                     $recipients=array_merge($recipients, $members);
             }
-            
+
             if($cfg->alertDeptManagerONNewTicket() && $dept && ($manager=$dept->getManager()))
                 $recipients[]= $manager;
-               
-            foreach( $recipients as $k=>$staff){
-                if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(),$sentlist)) continue;
+
+            foreach( $recipients as $k=>$staff) {
+                if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
                 $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']);
                 $email->sendAlert($staff->getEmail(), $msg['subj'], $alert);
                 $sentlist[] = $staff->getEmail();
             }
-           
-           
+
+
         }
-        
+
         return true;
     }
 
@@ -937,43 +800,43 @@ class Ticket {
 
         //Send notice to user.
         $dept = $this->getDept();
-                    
+
         if(!$dept || !($tpl=$dept->getTemplate()))
             $tpl=$cfg->getDefaultTemplate();
-            
+
         if(!$dept || !($email=$dept->getAutoRespEmail()))
             $email=$cfg->getDefaultEmail();
 
         if($tpl && ($msg=$tpl->getOverlimitMsgTemplate()) && $email) {
-            
-            $msg = $this->replaceVars($msg, 
+
+            $msg = $this->replaceVars($msg,
                         array('signature' => ($dept && $dept->isPublic())?$dept->getSignature():''));
-            
+
             $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body']);
         }
 
         $client= $this->getClient();
-        
+
         //Alert admin...this might be spammy (no option to disable)...but it is helpful..I think.
         $alert='Max. open tickets reached for '.$this->getEmail()."\n"
               .'Open ticket: '.$client->getNumOpenTickets()."\n"
               .'Max Allowed: '.$cfg->getMaxOpenTickets()."\n\nNotice sent to the user.";
-            
+
         $ost->alertAdmin('Overlimit Notice', $alert);
-       
+
         return true;
     }
 
-    function onResponse(){
+    function onResponse() {
         db_query('UPDATE '.TICKET_TABLE.' SET isanswered=1,lastresponse=NOW(), updated=NOW() WHERE ticket_id='.db_input($this->getId()));
     }
 
-    function onMessage($autorespond=true, $alert=true){
+    function onMessage($autorespond=true, $alert=true) {
         global $cfg;
 
         db_query('UPDATE '.TICKET_TABLE.' SET isanswered=0,lastmessage=NOW() WHERE ticket_id='.db_input($this->getId()));
-            
-        //auto-assign to closing staff or last respondent 
+
+        //auto-assign to closing staff or last respondent
         if(!($staff=$this->getStaff()) || !$staff->isAvailable()) {
             if($cfg->autoAssignReopenedTickets() && ($lastrep=$this->getLastRespondent()) && $lastrep->isAvailable()) {
                 $this->setStaffId($lastrep->getId()); //direct assignment;
@@ -1001,7 +864,7 @@ class Ticket {
 
         if(!$dept || !($email = $dept->getAutoRespEmail()))
             $email = $cfg->getDefaultEmail();
-      
+
         //If enabled...send confirmation to user. ( New Message AutoResponse)
         if($email && $tpl && ($msg=$tpl->getNewMessageAutorepMsgTemplate())) {
 
@@ -1011,7 +874,7 @@ class Ticket {
             //Reply separator tag.
             if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator()))
                 $msg['body'] ="\n$tag\n\n".$msg['body'];
-        
+
             $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body']);
         }
     }
@@ -1028,7 +891,7 @@ class Ticket {
 
         $comments = $comments?$comments:'Ticket assignment';
         $assigner = $thisstaff?$thisstaff:'SYSTEM (Auto Assignment)';
-        
+
         //Log an internal note - no alerts on the internal note.
         $this->logNote('Ticket Assigned to '.$assignee->getName(), $comments, $assigner, false);
 
@@ -1060,7 +923,7 @@ class Ticket {
         //Get the message template
         if($email && $recipients && $tpl && ($msg=$tpl->getAssignedAlertMsgTemplate())) {
 
-            $msg = $this->replaceVars($msg, 
+            $msg = $this->replaceVars($msg,
                         array('comments' => $comments,
                               'assignee' => $assignee,
                               'assigner' => $assigner
@@ -1069,7 +932,7 @@ class Ticket {
             //Send the alerts.
             $sentlist=array();
             foreach( $recipients as $k=>$staff) {
-                if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(),$sentlist)) continue;
+                if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
                 $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']);
                 $email->sendAlert($staff->getEmail(), $msg['subj'], $alert);
                 $sentlist[] = $staff->getEmail();
@@ -1100,7 +963,7 @@ class Ticket {
 
         //Get the message template
         if($tpl && ($msg=$tpl->getOverdueAlertMsgTemplate()) && $email) {
-            
+
             $msg = $this->replaceVars($msg, array('comments' => $comments));
 
             //recipients
@@ -1121,8 +984,8 @@ class Ticket {
                 $recipients[]= $manager;
 
             $sentlist=array();
-            foreach( $recipients as $k=>$staff){
-                if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(),$sentlist)) continue;
+            foreach( $recipients as $k=>$staff) {
+                if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
                 $alert = str_replace("%{recipient}", $staff->getFirstName(), $msg['body']);
                 $email->sendAlert($staff->getEmail(), $msg['subj'], $alert);
                 $sentlist[] = $staff->getEmail();
@@ -1131,9 +994,9 @@ class Ticket {
         }
 
         return true;
-    
+
     }
-   
+
     //ticket obj as variable = ticket number.
     function asVar() {
        return $this->getNumber();
@@ -1161,17 +1024,17 @@ class Ticket {
                 break;
             case 'create_date':
                 return Format::date(
-                        $cfg->getDateTimeFormat(), 
+                        $cfg->getDateTimeFormat(),
                         Misc::db2gmtime($this->getCreateDate()),
                         $cfg->getTZOffset(),
                         $cfg->observeDaylightSaving());
                 break;
              case 'due_date':
                 $duedate ='';
-                if($this->getDueDate())
+                if($this->getEstDueDate())
                     $duedate = Format::date(
                             $cfg->getDateTimeFormat(),
-                            Misc::db2gmtime($this->getDueDate()),
+                            Misc::db2gmtime($this->getEstDueDate()),
                             $cfg->getTZOffset(),
                             $cfg->observeDaylightSaving());
 
@@ -1211,9 +1074,9 @@ class Ticket {
     }
 
     function markOverdue($whine=true) {
-        
+
         global $cfg;
-        
+
         if($this->isOverdue())
             return true;
 
@@ -1231,7 +1094,7 @@ class Ticket {
 
     function clearOverdue() {
 
-        if(!$this->isOverdue()) 
+        if(!$this->isOverdue())
             return true;
 
         $sql='UPDATE '.TICKET_TABLE.' SET isoverdue=0, updated=NOW() ';
@@ -1244,11 +1107,11 @@ class Ticket {
         return (db_query($sql) && db_affected_rows());
     }
 
-    //Dept Tranfer...with alert.. done by staff 
+    //Dept Tranfer...with alert.. done by staff
     function transfer($deptId, $comments, $alert = true) {
-        
+
         global $cfg, $thisstaff;
-      
+
         if(!$thisstaff || !$thisstaff->canTransferTickets())
             return false;
 
@@ -1256,8 +1119,8 @@ class Ticket {
 
         if(!$deptId || !$this->setDeptId($deptId))
             return false;
-       
-        // Reopen ticket if closed 
+
+        // Reopen ticket if closed
         if($this->isClosed()) $this->reopen();
 
         $this->reload();
@@ -1265,14 +1128,14 @@ class Ticket {
         // Set SLA of the new department
         if(!$this->getSLAId())
             $this->selectSLAId();
-                  
+
         /*** log the transfer comments as internal note - with alerts disabled - ***/
         $title='Ticket transfered from '.$currentDept.' to '.$this->getDeptName();
-        $comments=$comments?$comments:$title; 
+        $comments=$comments?$comments:$title;
         $this->logNote($title, $comments, $thisstaff, false);
 
         $this->logEvent('transferred');
-        
+
         //Send out alerts if enabled AND requested
         if(!$alert || !$cfg->alertONTransfer() || !($dept=$this->getDept())) return true; //no alerts!!
 
@@ -1280,16 +1143,16 @@ class Ticket {
          //Get template.
          if(!($tpl = $dept->getTemplate()))
              $tpl= $cfg->getDefaultTemplate();
-        
+
          //Email to use!
          if(!($email=$cfg->getAlertEmail()))
              $email =$cfg->getDefaultEmail();
-                
-         //Get the message template 
+
+         //Get the message template
          if($tpl && ($msg=$tpl->getTransferAlertMsgTemplate()) && $email) {
-            
+
              $msg = $this->replaceVars($msg, array('comments' => $comments, 'staff' => $thisstaff));
-            //recipients            
+            //recipients
             $recipients=array();
             //Assigned staff or team... if any
             if($this->isAssigned() && $cfg->alertAssignedONTransfer()) {
@@ -1306,11 +1169,11 @@ class Ticket {
             //Always alert dept manager??
             if($cfg->alertDeptManagerONTransfer() && $dept && ($manager=$dept->getManager()))
                 $recipients[]= $manager;
-             
+
             $sentlist=array();
-            foreach( $recipients as $k=>$staff){
-                if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(),$sentlist)) continue;
-                $alert = str_replace('%{recipient}',$staff->getFirstName(), $msg['body']);
+            foreach( $recipients as $k=>$staff) {
+                if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
+                $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']);
                 $email->sendAlert($staff->getEmail(), $msg['subj'], $alert);
                 $sentlist[] = $staff->getEmail();
             }
@@ -1323,7 +1186,7 @@ class Ticket {
 
         if(!is_object($staff) && !($staff=Staff::lookup($staff)))
             return false;
-        
+
         if(!$this->setStaffId($staff->getId()))
             return false;
 
@@ -1357,7 +1220,7 @@ class Ticket {
         global $thisstaff;
 
         $rv=0;
-        $id=preg_replace("/[^0-9]/", "",$assignId);
+        $id=preg_replace("/[^0-9]/", "", $assignId);
         if($assignId[0]=='t') {
             $rv=$this->assignToTeam($id, $note, $alert);
         } elseif($assignId[0]=='s' || is_numeric($assignId)) {
@@ -1368,7 +1231,7 @@ class Ticket {
 
         return $rv;
     }
-    
+
     //unassign primary assignee
     function unassign() {
 
@@ -1391,52 +1254,40 @@ class Ticket {
 
         return true;
     }
-    
+
     function release() {
         return $this->unassign();
     }
 
     //Insert message from client
-    function postMessage($vars, $source='', $alerts=true) {
+    function postMessage($vars, $origin='', $alerts=true) {
         global $cfg;
-       
-        if(!$vars || !$vars['message'])
-            return 0;
-        
+
         //Strip quoted reply...on emailed replies
-        if(!strcasecmp($source, 'Email') 
-                && $cfg->stripQuotedReply() 
+        if(!strcasecmp($origin, 'Email')
+                && $cfg->stripQuotedReply()
                 && ($tag=$cfg->getReplySeparator()) && strpos($vars['message'], $tag))
-            list($vars['message']) = split($tag, $vars['message']);
-
-        # XXX: Refuse auto-response messages? (via email) XXX: No - but kill our auto-responder.
-
-
-        $sql='INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW()'
-            .' ,thread_type="M" '
-            .' ,ticket_id='.db_input($this->getId())
-            # XXX: Put Subject header into the 'title' field
-            .' ,body='.db_input(Format::striptags($vars['message'])) //Tags/code stripped...meaning client can not send in code..etc
-            .' ,source='.db_input($source?$source:$_SERVER['REMOTE_ADDR'])
-            .' ,ip_address='.db_input($_SERVER['REMOTE_ADDR']);
-    
-        if(!db_query($sql) || !($msgid=db_insert_id()))
-            return 0; //bail out....
-
-        $this->setLastMsgId($msgid);
-        
-        if (isset($vars['mid'])) {
-            $sql='INSERT INTO '.TICKET_EMAIL_INFO_TABLE
-                .' SET message_id='.db_input($msgid)
-                .', email_mid='.db_input($vars['mid'])
-                .', headers='.db_input($vars['header']);
-            db_query($sql);
-        }
+            if(list($msg) = split($tag, $vars['message']))
+                $vars['message'] = $msg;
 
-        if(!$alerts) return $msgid; //Our work is done...
+        if($vars['ip'])
+            $vars['ip_address'] = $vars['ip'];
+        elseif(!$vars['ip_address'] && $_SERVER['REMOTE_ADDR'])
+            $vars['ip_address'] = $_SERVER['REMOTE_ADDR'];
+
+        $errors = array();
+        if(!($message = $this->getThread()->addMessage($vars, $errors)))
+            return null;
+
+        $this->setLastMsgId($message->getId());
+
+        if (isset($vars['mid']))
+            $message->saveEmailInfo($vars);
+
+        if(!$alerts) return $message; //Our work is done...
 
         $autorespond = true;
-        if ($autorespond && $vars['header'] && TicketFilter::isAutoResponse($vars['header']))
+        if ($autorespond && $message->isAutoResponse())
             $autorespond=false;
 
         $this->onMessage($autorespond); //must be called b4 sending alerts to staff.
@@ -1452,33 +1303,33 @@ class Ticket {
         //If enabled...send alert to staff (New Message Alert)
         if($cfg->alertONNewMessage() && $tpl && $email && ($msg=$tpl->getNewMessageAlertMsgTemplate())) {
 
-            $msg = $this->replaceVars($msg, array('message' => $vars['message']));
+            $msg = $this->replaceVars($msg, array('message' => $message));
 
             //Build list of recipients and fire the alerts.
             $recipients=array();
             //Last respondent.
             if($cfg->alertLastRespondentONNewMessage() || $cfg->alertAssignedONNewMessage())
                 $recipients[]=$this->getLastRespondent();
-                
+
             //Assigned staff if any...could be the last respondent
-            
+
             if($this->isAssigned() && ($staff=$this->getStaff()))
                 $recipients[]=$staff;
-                
+
             //Dept manager
             if($cfg->alertDeptManagerONNewMessage() && $dept && ($manager=$dept->getManager()))
                 $recipients[]=$manager;
-                
+
             $sentlist=array(); //I know it sucks...but..it works.
-            foreach( $recipients as $k=>$staff){
+            foreach( $recipients as $k=>$staff) {
                 if(!$staff || !$staff->getEmail() || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
                 $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']);
                 $email->sendAlert($staff->getEmail(), $msg['subj'], $alert);
                 $sentlist[] = $staff->getEmail();
             }
         }
-        
-        return $msgid;
+
+        return $message;
     }
 
     function postCannedReply($canned, $msgId, $alert=true) {
@@ -1492,16 +1343,17 @@ class Ticket {
             $files[] = $file['id'];
 
         $info = array('msgId' => $msgId,
+                      'poster' => 'SYSTEM (Canned Reply)',
                       'response' => $this->replaceVars($canned->getResponse()),
                       'cannedattachments' => $files);
 
         $errors = array();
-        if(!($respId=$this->postReply($info, $errors, false)))
-            return false;
+        if(!($response=$this->postReply($info, $errors, false)))
+            return null;
 
         $this->markUnAnswered();
 
-        if(!$alert) return $respId;
+        if(!$alert) return $response;
 
         $dept = $this->getDept();
 
@@ -1518,65 +1370,41 @@ class Ticket {
             else
                 $signature='';
 
-            $msg = $this->replaceVars($msg, array('response' => $info['response'], 'signature' => $signature));
+            $msg = $this->replaceVars($msg, array('response' => $response, 'signature' => $signature));
 
             if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator()))
                 $msg['body'] ="\n$tag\n\n".$msg['body'];
 
-            $attachments =($cfg->emailAttachments() && $files)?$this->getAttachments($respId, 'R'):array();
+            $attachments =($cfg->emailAttachments() && $files)?$response->getAttachments():array();
             $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body'], $attachments);
         }
 
-        return $respId;
+        return $response;
     }
 
-    /* public */ 
+    /* public */
     function postReply($vars, &$errors, $alert = true) {
         global $thisstaff, $cfg;
 
-        if(!$vars['msgId'])
-            $errors['msgId'] ='Missing messageId - internal error';
-        if(!$vars['response'])
-            $errors['response'] = 'Response message required';
 
-        if($errors) return 0;
+        if(!$vars['poster'] && $thisstaff)
+            $vars['poster'] = $thisstaff->getName();
 
-        $poster = $thisstaff?$thisstaff->getName():'SYSTEM (Canned Reply)';
+        if(!$vars['staffId'] && $thisstaff)
+            $vars['staffId'] = $thisstaff->getId();
 
-        $sql='INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW() '
-            .' ,thread_type="R"'
-            .' ,ticket_id='.db_input($this->getId())
-            .' ,pid='.db_input($vars['msgId'])
-            .' ,body='.db_input(Format::striptags($vars['response']))
-            .' ,staff_id='.db_input($thisstaff?$thisstaff->getId():0)
-            .' ,poster='.db_input($poster)
-            .' ,ip_address='.db_input($thisstaff?$thisstaff->getIP():'');
-
-        if(!db_query($sql) || !($respId=db_insert_id()))
-            return false;
+        if(!($response = $this->getThread()->addResponse($vars, $errors)))
+            return null;
 
         //Set status - if checked.
         if(isset($vars['reply_ticket_status']) && $vars['reply_ticket_status'])
             $this->setStatus($vars['reply_ticket_status']);
 
-        /* We can NOT recover from attachment related failures at this point */
-        $attachments = array();
-        //Web based upload.. note that we're not "validating" attachments on response.
-        if($_FILES['attachments'] && ($files=Format::files($_FILES['attachments'])))
-            $attachments=$this->uploadAttachments($files, $respId, 'R');
-
-        //Canned attachments...
-        if($vars['cannedattachments'] && is_array($vars['cannedattachments'])) {
-            foreach($vars['cannedattachments'] as $fileId)
-                if($fileId && $this->saveAttachment($fileId, $respId, 'R'))
-                    $attachments[] = $fileId;
-        }
-
         $this->onResponse(); //do house cleaning..
         $this->reload();
 
         /* email the user??  - if disabled - the bail out */
-        if(!$alert) return $respId;
+        if(!$alert) return $response;
 
         $dept = $this->getDept();
 
@@ -1594,24 +1422,24 @@ class Ticket {
                 $signature=$dept->getSignature();
             else
                 $signature='';
-            
-            $msg = $this->replaceVars($msg, 
-                    array('response' => $vars['response'], 'signature' => $signature, 'staff' => $thisstaff));
+
+            $msg = $this->replaceVars($msg,
+                    array('response' => $response, 'signature' => $signature, 'staff' => $thisstaff));
 
             if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator()))
                 $msg['body'] ="\n$tag\n\n".$msg['body'];
 
             //Set attachments if emailing.
-            $attachments =($cfg->emailAttachments() && $attachments)?$this->getAttachments($respId,'R'):array();
+            $attachments = $cfg->emailAttachments()?$response->getAttachments():array();
             //TODO: setup  5 param (options... e.g mid trackable on replies)
             $email->send($this->getEmail(), $msg['subj'], $msg['body'], $attachments);
         }
 
-        return $respId;
+        return $response;
     }
 
     //Activity log - saved as internal notes WHEN enabled!!
-    function logActivity($title,$note){
+    function logActivity($title, $note) {
         global $cfg;
 
         if(!$cfg || !$cfg->logTicketActivity())
@@ -1659,42 +1487,23 @@ class Ticket {
                 $alert);
     }
 
-    function postNote($vars, &$errors, $poster, $alert=true) {        
+    function postNote($vars, &$errors, $poster, $alert=true) {
         global $cfg, $thisstaff;
 
-        if(!$vars || !is_array($vars))
-            $errors['err'] = 'Missing or invalid data';
-        elseif(!$vars['note'])
-            $errors['note'] = 'Note required';
-
-        if($errors) return false;
-		
-        $staffId = 0;
+        //Who is posting the note - staff or system?
+        $vars['staffId'] = 0;
+        $vars['poster'] = 'SYSTEM';
         if($poster && is_object($poster)) {
-            $staffId = $poster->getId();
-            $poster = $poster->getName();
-        } elseif(!$poster) {
-            $poster ='SYSTEM';
+            $vars['staffId'] = $poster->getId();
+            $vars['poster'] = $poster->getName();
+        }elseif($poster) { //string
+            $vars['poster'] = $poster;
         }
 
-        //TODO: move to class.thread.php
+        if(!($note=$this->getThread()->addNote($vars, $errors)))
+            return null;
 
-        $sql= 'INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW() '.
-                ',thread_type="N"'.
-                ',ticket_id='.db_input($this->getId()).
-                ',title='.db_input(Format::striptags($vars['title']?$vars['title']:'[No Title]')).
-                ',body='.db_input(Format::striptags($vars['note'])).
-                ',staff_id='.db_input($staffId).
-                ',poster='.db_input($poster);
-        //echo $sql;
-        if(!db_query($sql) || !($id=db_insert_id()))
-            return false;
-                
-        //Upload attachments IF ANY - TODO: validate attachment types??
-        if($_FILES['attachments'] && ($files=Format::files($_FILES['attachments'])))
-            $attachments = $this->uploadAttachments($files, $id, 'N');
-            
-        //Set state: Error on state change not critical! 
+        //Set state: Error on state change not critical!
         if(isset($vars['state']) && $vars['state']) {
             if($this->setState($vars['state']))
                 $this->reload();
@@ -1702,11 +1511,8 @@ class Ticket {
 
         // If alerts are not enabled then return a success.
         if(!$alert || !$cfg->alertONNewNote() || !($dept=$this->getDept()))
-            return $id;
+            return $note;
 
-        //Note obj.
-        $note = Note::lookup($id, $this->getId());
-        
         if(!($tpl = $dept->getTemplate()))
             $tpl= $cfg->getDefaultTemplate();
 
@@ -1715,36 +1521,36 @@ class Ticket {
 
 
         if($tpl && ($msg=$tpl->getNoteAlertMsgTemplate()) && $email) {
-                   
+
             $msg = $this->replaceVars($msg, array('note' => $note));
 
-            // Alert recipients    
+            // Alert recipients
             $recipients=array();
-            
+
             //Last respondent.
             if($cfg->alertLastRespondentONNewNote())
                 $recipients[]=$this->getLastRespondent();
-            
+
             //Assigned staff if any...could be the last respondent
             if($cfg->alertAssignedONNewNote() && $this->isAssigned() && $this->getStaffId())
                 $recipients[]=$this->getStaff();
-                
+
             //Dept manager
             if($cfg->alertDeptManagerONNewNote() && $dept && $dept->getManagerId())
                 $recipients[]=$dept->getManager();
 
-            $attachments =($attachments)?$this->getAttachments($id, 'N'):array();
+            $attachments = $note->getAttachments();
             $sentlist=array();
             foreach( $recipients as $k=>$staff) {
                 if(!$staff || !is_object($staff) || !$staff->getEmail() || !$staff->isAvailable()) continue;
-                if(in_array($staff->getEmail(),$sentlist) || ($staffId && $staffId==$staff->getId())) continue; 
-                $alert = str_replace('%{recipient}',$staff->getFirstName(), $msg['body']);
+                if(in_array($staff->getEmail(), $sentlist) || ($staffId && $staffId==$staff->getId())) continue;
+                $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']);
                 $email->sendAlert($staff->getEmail(), $msg['subj'], $alert, $attachments);
                 $sentlist[] = $staff->getEmail();
             }
         }
-        
-        return $id;
+
+        return $note;
     }
 
     //Print ticket... export the ticket thread as PDF.
@@ -1757,123 +1563,25 @@ class Ticket {
         exit;
     }
 
-    //online based attached files.
-    function uploadAttachments($files, $refid, $type, $checkFileTypes=false) {
-        global $ost;
-
-        $uploaded=array();
-        $ost->validateFileUploads($files, $checkFileTypes); //Validator sets errors - if any
-        foreach($files as $file) {
-            if(!$file['error'] 
-                    && ($id=AttachmentFile::upload($file)) 
-                    && $this->saveAttachment($id, $refid, $type))
-                $uploaded[]=$id;
-            elseif($file['error']!=UPLOAD_ERR_NO_FILE) { 
-                
-                // log file upload errors as interal notes + syslog debug.
-                if($file['error'] && gettype($file['error'])=='string')
-                    $error = $file['error'];
-                else
-                    $error ='Error #'.$file['error'];
-
-                $this->logNote('File Upload Error', $error, 'SYSTEM', false);
-               
-                $ost->logDebug('File Upload Error (Ticket #'.$this->getExtId().')', $error);
-            }
-            
-        }
-        
-        return $uploaded;
-    }
-
-    /* Wrapper or uploadAttachments 
-       - used on client interface 
-       - file type check is forced
-       - $_FILES  is passed.
-    */
-    function uploadFiles($files, $refid, $type) {
-        return $this->uploadAttachments(Format::files($files), $refid, $type, true);    
-    }
-
-    /* Emailed & API attachments handler */
-    function importAttachments($attachments, $refid, $type, $checkFileTypes=true) {
-        global $ost;
-
-        if(!$attachments || !is_array($attachments)) return null;
-
-        $files = array();        
-        foreach ($attachments as &$info) {
-            //Do error checking...
-            if ($checkFileTypes && !$ost->isFileTypeAllowed($info))
-                $info['error'] = 'Invalid file type (ext) for '.Format::htmlchars($info['name']);
-            elseif ($info['encoding'] && !strcasecmp($info['encoding'], 'base64')) {
-                if(!($info['data'] = base64_decode($info['data'], true)))
-                    $info['error'] = sprintf('%s: Poorly encoded base64 data', Format::htmlchars($info['name']));
-            }
-
-            if($info['error']) {
-                $this->logNote('File Import Error', $info['error'], 'SYSTEM', false);
-            } elseif (($id=$this->saveAttachment($info, $refid, $type))) {
-                $files[] = $id;
-            }
-        }
-
-        return $files;
-    }
-
-
-    /*
-       Save attachment to the DB. upload/import (above).
-       
-       @file is a mixed var - can be ID or file hash.
-     */
-    function saveAttachment($file, $refid, $type) {
-
-        if(!$refid || !$type || !($fileId=is_numeric($file)?$file:AttachmentFile::save($file)))
-            return 0;
-
-        $sql ='INSERT INTO '.TICKET_ATTACHMENT_TABLE.' SET created=NOW() '
-             .' ,ticket_id='.db_input($this->getId())
-             .' ,file_id='.db_input($fileId)
-             .' ,ref_id='.db_input($refid)
-             .' ,ref_type='.db_input($type);
-
-        return (db_query($sql) && ($id=db_insert_id()))?$id:0;
-    }
-    
-
-
-    function deleteAttachments(){
-        
-        $deleted=0;
-        // Clear reference table
-        $res=db_query('DELETE FROM '.TICKET_ATTACHMENT_TABLE.' WHERE ticket_id='.db_input($this->getId()));
-        if ($res && db_affected_rows())
-            $deleted = AttachmentFile::deleteOrphans();
-
-        return $deleted;
-    }
-
+    function delete() {
 
-    function delete(){
-        
-        $sql='DELETE FROM '.TICKET_TABLE.' WHERE ticket_id='.$this->getId().' LIMIT 1';
+        $sql = 'DELETE FROM '.TICKET_TABLE.' WHERE ticket_id='.$this->getId().' LIMIT 1';
         if(!db_query($sql) || !db_affected_rows())
             return false;
 
-        db_query('DELETE FROM '.TICKET_THREAD_TABLE.' WHERE ticket_id='.db_input($this->getId()));
-        $this->deleteAttachments();
-        
+        //delete just orphaned ticket thread & associated attachments.
+        $this->getThread()->delete();
+
         return true;
     }
 
     function update($vars, &$errors) {
 
         global $cfg, $thisstaff;
-        
+
         if(!$cfg || !$thisstaff || !$thisstaff->canEditTickets())
             return false;
-         
+
         $fields=array();
         $fields['name']     = array('type'=>'string',   'required'=>1, 'error'=>'Name required');
         $fields['email']    = array('type'=>'email',    'required'=>1, 'error'=>'Valid email required');
@@ -1889,7 +1597,7 @@ class Ticket {
         if(!Validator::process($fields, $vars, $errors) && !$errors['err'])
             $errors['err'] = 'Missing or invalid data - check the errors and try again';
 
-        if($vars['duedate']) {     
+        if($vars['duedate']) {
             if($this->isClosed())
                 $errors['duedate']='Duedate can NOT be set on a closed ticket';
             elseif(!$vars['time'] || strpos($vars['time'],':')===false)
@@ -1899,7 +1607,7 @@ class Ticket {
             elseif(strtotime($vars['duedate'].' '.$vars['time'])<=time())
                 $errors['duedate']='Due date must be in the future';
         }
-        
+
         //Make sure phone extension is valid
         if($vars['phone_ext'] ) {
             if(!is_numeric($vars['phone_ext']) && !$errors['phone'])
@@ -1920,11 +1628,11 @@ class Ticket {
             .' ,topic_id='.db_input($vars['topicId'])
             .' ,sla_id='.db_input($vars['slaId'])
             .' ,duedate='.($vars['duedate']?db_input(date('Y-m-d G:i',Misc::dbtime($vars['duedate'].' '.$vars['time']))):'NULL');
-             
+
         if($vars['duedate']) { //We are setting new duedate...
             $sql.=' ,isoverdue=0';
         }
-             
+
         $sql.=' WHERE ticket_id='.db_input($this->getId());
 
         if(!db_query($sql) || !db_affected_rows())
@@ -1935,20 +1643,20 @@ class Ticket {
 
         $this->logNote('Ticket Updated', $vars['note'], $thisstaff);
         $this->reload();
-        
+
         return true;
     }
 
-   
+
    /*============== Static functions. Use Ticket::function(params); ==================*/
     function getIdByExtId($extId, $email=null) {
-        
-        if(!$extId || !is_numeric($extId)) 
+
+        if(!$extId || !is_numeric($extId))
             return 0;
 
         $sql ='SELECT  ticket_id FROM '.TICKET_TABLE.' ticket '
              .' WHERE ticketID='.db_input($extId);
-        
+
         if($email)
             $sql.=' AND email='.db_input($email);
 
@@ -1959,9 +1667,14 @@ class Ticket {
     }
 
 
-   
+
     function lookup($id) { //Assuming local ID is the only lookup used!
-        return ($id && is_numeric($id) && ($ticket= new Ticket($id)) && $ticket->getId()==$id)?$ticket:null;    
+        return ($id
+                && is_numeric($id)
+                && ($ticket= new Ticket($id))
+                && $ticket->getId()==$id
+                && $ticket->getThread())
+            ?$ticket:null;
     }
 
     function lookupByExtId($id, $email=null) {
@@ -1980,7 +1693,7 @@ class Ticket {
         return $id;
     }
 
-    function getIdByMessageId($mid,$email) {
+    function getIdByMessageId($mid, $email) {
 
         if(!$mid || !$email)
             return 0;
@@ -1996,7 +1709,7 @@ class Ticket {
         return $id;
     }
 
-    function getOpenTicketsByEmail($email){
+    function getOpenTicketsByEmail($email) {
 
         $sql='SELECT count(*) as open FROM '.TICKET_TABLE.' WHERE status='.db_input('open').' AND email='.db_input($email);
         if(($res=db_query($sql)) && db_num_rows($res))
@@ -2005,10 +1718,10 @@ class Ticket {
         return $num;
     }
 
-    /* Quick staff's tickets stats */ 
+    /* Quick staff's tickets stats */
     function getStaffStats($staff) {
         global $cfg;
-        
+
         /* Unknown or invalid staff */
         if(!$staff || (!is_object($staff) && !($staff=Staff::lookup($staff))) || !$staff->isStaff() || $cfg->getDBVersion())
             return null;
@@ -2017,26 +1730,26 @@ class Ticket {
             .' ,count(overdue.ticket_id) as overdue, count(assigned.ticket_id) as assigned, count(closed.ticket_id) as closed '
             .' FROM '.TICKET_TABLE.' ticket '
             .' LEFT JOIN '.TICKET_TABLE.' open
-                ON (open.ticket_id=ticket.ticket_id 
-                        AND open.status=\'open\' 
+                ON (open.ticket_id=ticket.ticket_id
+                        AND open.status=\'open\'
                         AND open.isanswered=0
                         '.((!($cfg->showAssignedTickets() || $staff->showAssignedTickets()))?
                         ' AND open.staff_id=0 ':'').') '
             .' LEFT JOIN '.TICKET_TABLE.' answered
-                ON (answered.ticket_id=ticket.ticket_id 
-                        AND answered.status=\'open\' 
+                ON (answered.ticket_id=ticket.ticket_id
+                        AND answered.status=\'open\'
                         AND answered.isanswered=1) '
             .' LEFT JOIN '.TICKET_TABLE.' overdue
-                ON (overdue.ticket_id=ticket.ticket_id 
-                        AND overdue.status=\'open\' 
+                ON (overdue.ticket_id=ticket.ticket_id
+                        AND overdue.status=\'open\'
                         AND overdue.isoverdue=1) '
             .' LEFT JOIN '.TICKET_TABLE.' assigned
-                ON (assigned.ticket_id=ticket.ticket_id 
-                        AND assigned.status=\'open\' 
+                ON (assigned.ticket_id=ticket.ticket_id
+                        AND assigned.status=\'open\'
                         AND assigned.staff_id='.db_input($staff->getId()).')'
             .' LEFT JOIN '.TICKET_TABLE.' closed
-                ON (closed.ticket_id=ticket.ticket_id 
-                        AND closed.status=\'closed\' 
+                ON (closed.ticket_id=ticket.ticket_id
+                        AND closed.status=\'closed\'
                         AND closed.staff_id='.db_input($staff->getId()).')'
             .' WHERE (ticket.staff_id='.db_input($staff->getId());
 
@@ -2048,12 +1761,15 @@ class Ticket {
 
         $sql.=')';
 
+        if(!$cfg || !($cfg->showAssignedTickets() || $staff->showAssignedTickets()))
+            $sql.=' AND (ticket.staff_id=0 OR ticket.staff_id='.db_input($staff->getId()).') ';
+
         return db_fetch_array(db_query($sql));
     }
 
 
-    /* Quick client's tickets stats 
-       @email - valid email. 
+    /* Quick client's tickets stats
+       @email - valid email.
      */
     function getClientStats($email) {
 
@@ -2075,7 +1791,7 @@ class Ticket {
      * The mother of all functions...You break it you fix it!
      *
      *  $autorespond and $alertstaff overwrites config settings...
-     */      
+     */
     function create($vars, &$errors, $origin, $autorespond=true, $alertstaff=true) {
         global $ost, $cfg, $thisclient, $_FILES;
 
@@ -2091,14 +1807,14 @@ class Ticket {
             }
 
             //Make sure the open ticket limit hasn't been reached. (LOOP CONTROL)
-            if($cfg->getMaxOpenTickets()>0 && strcasecmp($origin,'staff') 
+            if($cfg->getMaxOpenTickets()>0 && strcasecmp($origin,'staff')
                     && ($client=Client::lookupByEmail($vars['email']))
                     && ($openTickets=$client->getNumOpenTickets())
                     && ($openTickets>=$cfg->getMaxOpenTickets()) ) {
 
                 $errors['err']="You've reached the maximum open tickets allowed.";
-                $ost->logWarning('Ticket denied -'.$vars['email'], 
-                        sprintf('Max open tickets (%d) reached for %s ', 
+                $ost->logWarning('Ticket denied -'.$vars['email'],
+                        sprintf('Max open tickets (%d) reached for %s ',
                             $cfg->getMaxOpenTickets(), $vars['email']));
 
                 return 0;
@@ -2108,12 +1824,12 @@ class Ticket {
         //Init ticket filters...
         $ticket_filter = new TicketFilter($origin, $vars);
         // Make sure email contents should not be rejected
-        if($ticket_filter 
+        if($ticket_filter
                 && ($filter=$ticket_filter->shouldReject())) {
             $errors['err']='Ticket denied. Error #403';
             $errors['errno'] = 403;
-            $ost->logWarning('Ticket denied', 
-                    sprintf('Ticket rejected ( %s) by filter "%s"', 
+            $ost->logWarning('Ticket denied',
+                    sprintf('Ticket rejected ( %s) by filter "%s"',
                         $vars['email'], $filter->getName()));
 
             return 0;
@@ -2145,7 +1861,7 @@ class Ticket {
         }
         $fields['priorityId']   = array('type'=>'int',      'required'=>0, 'error'=>'Invalid Priority');
         $fields['phone']        = array('type'=>'phone',    'required'=>0, 'error'=>'Valid phone # required');
-        
+
         if(!Validator::process($fields, $vars, $errors) && !$errors['err'])
             $errors['err'] ='Missing or invalid data - check the errors and try again';
 
@@ -2213,7 +1929,7 @@ class Ticket {
         $deptId=$deptId?$deptId:$cfg->getDefaultDeptId();
         $topicId=$vars['topicId']?$vars['topicId']:0;
         $ipaddress=$vars['ip']?$vars['ip']:$_SERVER['REMOTE_ADDR'];
-        
+
         //We are ready son...hold on to the rails.
         $extId=Ticket::genExtRandID();
         $sql='INSERT INTO '.TICKET_TABLE.' SET created=NOW() '
@@ -2227,7 +1943,7 @@ class Ticket {
             .' ,subject='.db_input(Format::striptags($vars['subject']))
             .' ,phone="'.db_input($vars['phone'],false).'"'
             .' ,phone_ext='.db_input($vars['phone_ext']?$vars['phone_ext']:'')
-            .' ,ip_address='.db_input($ipaddress) 
+            .' ,ip_address='.db_input($ipaddress)
             .' ,source='.db_input($source);
 
         //Make sure the origin is staff - avoid firebug hack!
@@ -2239,17 +1955,20 @@ class Ticket {
             return null;
 
         /* -------------------- POST CREATE ------------------------ */
-        
-        if(!$cfg->useRandomIds()){
+
+        if(!$cfg->useRandomIds()) {
             //Sequential ticketIDs support really..really suck arse.
             $extId=$id; //To make things really easy we are going to use autoincrement ticket_id.
-            db_query('UPDATE '.TICKET_TABLE.' SET ticketID='.db_input($extId).' WHERE ticket_id='.$id.' LIMIT 1'); 
+            db_query('UPDATE '.TICKET_TABLE.' SET ticketID='.db_input($extId).' WHERE ticket_id='.$id.' LIMIT 1');
             //TODO: RETHING what happens if this fails?? [At the moment on failure random ID is used...making stuff usable]
         }
 
         $dept = $ticket->getDept();
+
         //post the message.
-        $msgid=$ticket->postMessage($vars , $source, false);
+        unset($vars['cannedattachments']); //Ticket::open() might have it set as part of  open & respond.
+        $vars['title'] = $vars['subject']; //Use the initial subject as title of the post.
+        $message = $ticket->postMessage($vars , $origin, false);
 
         // Configure service-level-agreement for this ticket
         $ticket->selectSLAId($vars['slaId']);
@@ -2267,10 +1986,8 @@ class Ticket {
 
         # Messages that are clearly auto-responses from email systems should
         # not have a return 'ping' message
-        if ($autorespond && $vars['header'] &&
-                TicketFilter::isAutoResponse(Mail_Parse::splitHeaders($vars['header']))) {
+        if ($autorespond && $message && $message->isAutoResponse())
             $autorespond=false;
-        }
 
         //Don't auto respond to mailer daemons.
         if( $autorespond &&
@@ -2281,8 +1998,8 @@ class Ticket {
 
         //post canned auto-response IF any (disables new ticket auto-response).
         if ($vars['cannedResponseId']
-            && $ticket->postCannedReply($vars['cannedResponseId'], $msgid, $autorespond)) {
-                $ticket->markUnAnswered(); //Leave the ticket as unanswred. 
+            && $ticket->postCannedReply($vars['cannedResponseId'], $message->getId(), $autorespond)) {
+                $ticket->markUnAnswered(); //Leave the ticket as unanswred.
                 $autorespond = false;
         }
 
@@ -2292,7 +2009,7 @@ class Ticket {
             $autorespond=false;
 
         /***** See if we need to send some alerts ****/
-        $ticket->onNewTicket($vars['message'], $autorespond, $alertstaff);
+        $ticket->onNewTicket($message, $autorespond, $alertstaff);
 
         /************ check if the user JUST reached the max. open tickets limit **********/
         if($cfg->getMaxOpenTickets()>0
@@ -2300,7 +2017,7 @@ class Ticket {
                     && ($client->getNumOpenTickets()==$cfg->getMaxOpenTickets())) {
             $ticket->onOpenLimit(($autorespond && strcasecmp($origin, 'staff')));
         }
-        
+
         /* Start tracking ticket lifecycle events */
         $ticket->logEvent('created');
 
@@ -2310,33 +2027,34 @@ class Ticket {
     }
 
     function open($vars, &$errors) {
-        global $thisstaff,$cfg;
+        global $thisstaff, $cfg;
 
         if(!$thisstaff || !$thisstaff->canCreateTickets()) return false;
-        
+
+        if($vars['source'] && !in_array(strtolower($vars['source']),array('email','phone','other')))
+            $errors['source']='Invalid source - '.Format::htmlchars($vars['source']);
+
         if(!$vars['issue'])
             $errors['issue']='Summary of the issue required';
         else
             $vars['message']=$vars['issue'];
 
-        if($vars['source'] && !in_array(strtolower($vars['source']),array('email','phone','other')))
-            $errors['source']='Invalid source - '.Format::htmlchars($vars['source']);
-
         if(!($ticket=Ticket::create($vars, $errors, 'staff', false, (!$vars['assignId']))))
             return false;
 
         $vars['msgId']=$ticket->getLastMsgId();
-        $respId = 0;
-        
+
         // post response - if any
-        if($vars['response']) {
+        $response = null;
+        if($vars['response'] && $thisstaff->canPostReply()) {
             $vars['response'] = $ticket->replaceVars($vars['response']);
-            if(($respId=$ticket->postReply($vars, $errors, false))) {
+            if(($response=$ticket->postReply($vars, $errors, false))) {
                 //Only state supported is closed on response
                 if(isset($vars['ticket_state']) && $thisstaff->canCloseTickets())
                     $ticket->setState($vars['ticket_state']);
             }
         }
+
         //Post Internal note
         if($vars['assignId'] && $thisstaff->canAssignTickets()) { //Assign ticket to staff or team.
             $ticket->assign($vars['assignId'], $vars['note']);
@@ -2347,24 +2065,24 @@ class Ticket {
         }
 
         $ticket->reload();
-        
+
         if(!$cfg->notifyONNewStaffTicket() || !isset($vars['alertuser']))
             return $ticket; //No alerts.
 
         //Send Notice to user --- if requested AND enabled!!
-                
+
         $dept=$ticket->getDept();
         if(!$dept || !($tpl=$dept->getTemplate()))
             $tpl=$cfg->getDefaultTemplate();
-                                
+
         if(!$dept || !($email=$dept->getEmail()))
             $email =$cfg->getDefaultEmail();
 
         if($tpl && ($msg=$tpl->getNewTicketNoticeMsgTemplate()) && $email) {
-                        
+
             $message = $vars['issue'];
-            if($vars['response'])
-                $message.="\n\n".$vars['response'];
+            if($response)
+                $message.="\n\n".$response->getBody();
 
             if($vars['signature']=='mine')
                 $signature=$thisstaff->getSignature();
@@ -2372,23 +2090,23 @@ class Ticket {
                 $signature=$dept->getSignature();
             else
                 $signature='';
-            
-            $msg = $ticket->replaceVars($msg, 
+
+            $msg = $ticket->replaceVars($msg,
                     array('message' => $message, 'signature' => $signature));
 
             if($cfg->stripQuotedReply() && ($tag=trim($cfg->getReplySeparator())))
                 $msg['body'] ="\n$tag\n\n".$msg['body'];
 
-            $attachments =($cfg->emailAttachments() && $respId)?$ticket->getAttachments($respId,'R'):array();
+            $attachments =($cfg->emailAttachments() && $response)?$response->getAttachments():array();
             $email->send($ticket->getEmail(), $msg['subj'], $msg['body'], $attachments);
         }
 
         return $ticket;
-    
+
     }
-   
+
     function checkOverdue() {
-       
+
         $sql='SELECT ticket_id FROM '.TICKET_TABLE.' T1 '
             .' INNER JOIN '.SLA_TABLE.' T2 ON (T1.sla_id=T2.id AND T2.isactive=1) '
             .' WHERE status=\'open\' AND isoverdue=0 '
@@ -2407,6 +2125,6 @@ class Ticket {
 
         }
    }
-    
+
 }
 ?>
diff --git a/include/client/view.inc.php b/include/client/view.inc.php
index fa8a5b4a5420df21565f463b79c59ac33ecceca7..6684413c753e61bcb3188b94f5c8581754e9aa2b 100644
--- a/include/client/view.inc.php
+++ b/include/client/view.inc.php
@@ -72,7 +72,9 @@ if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) {
             <tr><th><?php echo Format::db_datetime($entry['created']); ?> &nbsp;&nbsp;<span><?php echo $poster; ?></span></th></tr>
             <tr><td><?php echo Format::display($entry['body']); ?></td></tr>
             <?php
-            if($entry['attachments'] && ($links=$ticket->getAttachmentsLinks($entry['id'], $entry['thread_type']))) { ?>
+            if($entry['attachments']
+                    && ($tentry=$ticket->getThreadEntry($entry['id']))
+                    && ($links=$tentry->getAttachmentsLinks())) { ?>
                 <tr><td class="info"><?php echo $links; ?></td></tr>
             <?php
             } ?>
diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php
index 60d61257fe3ba781a4e98ae3bfd6ebab280f4259..4d3f47f6f90849c6b4b6ed313fa95bcb96df2cc4 100644
--- a/include/staff/settings-tickets.inc.php
+++ b/include/staff/settings-tickets.inc.php
@@ -96,7 +96,7 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
                 <input type="checkbox" name="show_related_tickets" value="1" <?php echo $config['show_related_tickets'] ?'checked="checked"':''; ?> >
                 <em>(Show all related tickets on user login - otherwise access is restricted to one ticket view per login)</em>
             </td>
-        </tr>        
+        </tr>
         <tr>
             <td width="180">Show Notes Inline:</td>
             <td>
@@ -154,7 +154,7 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
         </tr>
         <tr>
             <th colspan="2">
-                <em><b>Attachments</b>:  Size setting mainly apply to web tickets.</em>
+                <em><b>Attachments</b>:  Size and max. uploads setting mainly apply to web tickets.</em>
             </th>
         </tr>
         <tr>
@@ -166,14 +166,14 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
             </td>
         </tr>
         <tr>
-            <td width="180">Emailed Attachments:</td>
+            <td width="180">Emailed/API Attachments:</td>
             <td>
-                <input type="checkbox" name="allow_email_attachments" <?php echo $config['allow_email_attachments']?'checked="checked"':''; ?>> Accept emailed files
+                <input type="checkbox" name="allow_email_attachments" <?php echo $config['allow_email_attachments']?'checked="checked"':''; ?>> Accept emailed/API attachments.
                     &nbsp;<font class="error">&nbsp;<?php echo $errors['allow_email_attachments']; ?></font>
             </td>
         </tr>
         <tr>
-            <td width="180">Online Attachments:</td>
+            <td width="180">Online/Web Attachments:</td>
             <td>
                 <input type="checkbox" name="allow_online_attachments" <?php echo $config['allow_online_attachments']?'checked="checked"':''; ?> >
                     Allow web upload &nbsp;&nbsp;&nbsp;&nbsp;
diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php
index f36c1e0bcd97dcf1fd067fd71e585675283788f5..ed29b87391f632efbdddc886db8809edfefb51ee 100644
--- a/include/staff/ticket-open.inc.php
+++ b/include/staff/ticket-open.inc.php
@@ -219,6 +219,10 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <textarea name="issue" cols="21" rows="8" style="width:80%;"><?php echo $info['issue']; ?></textarea>
             </td>
         </tr>
+        <?php
+        //is the user allowed to post replies??
+        if($thisstaff->canPostReply()) {
+            ?>
         <tr>
             <th colspan="2">
                 <em><strong>Response</strong>: Optional response to the above issue.</em>
@@ -270,8 +274,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                             </div>
                         </td>
                     </tr>
-            <?php
-            } ?>
+                <?php
+                } ?>
 
             <?php
             if($thisstaff->canCloseTickets()) { ?>
@@ -304,6 +308,9 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
             </table>
             </td>
         </tr>
+        <?php
+        } //end canPostReply
+        ?>
         <tr>
             <th colspan="2">
                 <em><strong>Internal Note</strong>: Optional internal note (recommended on assignment) <font class="error">&nbsp;<?php echo $errors['note']; ?></font></em>
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index 2a1a7aeb6953582744ba7cbebc549c4eda3d2183..45f25805b215f158cf0496da5f4c61340e307737 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -306,7 +306,9 @@ if(!$cfg->showNotesInline()) { ?>
                 </td>
             </tr>
             <?php
-            if($note['attachments'] && ($links=$ticket->getAttachmentsLinks($note['id'],'N'))) {?>
+             if($note['attachments'] 
+                    && ($tentry=$ticket->getThreadEntry($note['id'])) 
+                    && ($links=$tentry->getAttachmentsLinks())) { ?>
             <tr>
                 <td class="info" colspan="2"><?php echo $links; ?></td>
             </tr>
@@ -325,7 +327,10 @@ if(!$cfg->showNotesInline()) { ?>
     <?php
     $threadTypes=array('M'=>'message','R'=>'response', 'N'=>'note');
     /* -------- Messages & Responses & Notes (if inline)-------------*/
-    if(($thread=$ticket->getThread($cfg->showNotesInline()))) {
+    $types = array('M', 'R');
+    if($cfg->showNotesInline())
+        $types[] = 'N';
+    if(($thread=$ticket->getThreadEntries($types))) {
        foreach($thread as $entry) {
            ?>
         <table class="<?php echo $threadTypes[$entry['thread_type']]; ?>" cellspacing="0" cellpadding="1" width="940" border="0">
@@ -336,7 +341,9 @@ if(!$cfg->showNotesInline()) { ?>
             </tr>
             <tr><td colspan=3><?php echo Format::display($entry['body']); ?></td></tr>
             <?php
-            if($entry['attachments'] && ($links=$ticket->getAttachmentsLinks($entry['id'], $entry['thread_type']))) {?>
+            if($entry['attachments'] 
+                    && ($tentry=$ticket->getThreadEntry($entry['id']))
+                    && ($links=$tentry->getAttachmentsLinks())) {?>
             <tr>
                 <td class="info" colspan=3><?php echo $links; ?></td>
             </tr>
@@ -512,7 +519,7 @@ if(!$cfg->showNotesInline()) { ?>
         <input type="hidden" name="a" value="postnote">
         <table border="0" cellspacing="0" cellpadding="3">
             <?php 
-            if($errors['note']) {?>
+            if($errors['postnote']) {?>
             <tr>
                 <td width="160">&nbsp;</td>
                 <td class="error"><?php echo $errors['postnote']; ?></td>
diff --git a/scp/canned.php b/scp/canned.php
index edd4a4c36f185e3e379337e4d59882264388328c..c085e4116c2530d21db6932d22696aa2b981eb6b 100644
--- a/scp/canned.php
+++ b/scp/canned.php
@@ -44,7 +44,7 @@ if($_POST && $thisstaff->canManageCannedResponses()) {
                     }
                 }
                 //Upload NEW attachments IF ANY - TODO: validate attachment types??
-                if($_FILES['attachments'] && ($files=Format::files($_FILES['attachments'])))
+                if($_FILES['attachments'] && ($files=AttachmentFile::format($_FILES['attachments'])))
                     $canned->uploadAttachments($files);
 
                 $canned->reload();
@@ -58,7 +58,7 @@ if($_POST && $thisstaff->canManageCannedResponses()) {
                 $msg='Canned response added successfully';
                 $_REQUEST['a']=null;
                 //Upload attachments
-                if($_FILES['attachments'] && ($c=Canned::lookup($id)) && ($files=Format::files($_FILES['attachments'])))
+                if($_FILES['attachments'] && ($c=Canned::lookup($id)) && ($files=AttachmentFile::format($_FILES['attachments'])))
                     $c->uploadAttachments($files);
 
             } elseif(!$errors['err']) {
diff --git a/scp/tickets.php b/scp/tickets.php
index 37683eabfc7adc13ed2a7580ea35fdbdb34ce496..0a9ba30a443e88034c12c453989fc4537822d184 100644
--- a/scp/tickets.php
+++ b/scp/tickets.php
@@ -1,9 +1,9 @@
 <?php
 /*************************************************************************
     tickets.php
-    
+
     Handles all tickets related actions.
- 
+
     Peter Rotich <peter@osticket.com>
     Copyright (c)  2006-2013 osTicket
     http://www.osticket.com
@@ -46,23 +46,25 @@ if($_POST && !$errors):
                 $errors['err'] = 'Action denied. Contact admin for access';
             else {
 
-                if(!$_POST['msgId'])
-                    $errors['err']='Missing message ID - Internal error';
                 if(!$_POST['response'])
                     $errors['response']='Response required';
-            
                 //Use locks to avoid double replies
                 if($lock && $lock->getStaffId()!=$thisstaff->getId())
                     $errors['err']='Action Denied. Ticket is locked by someone else!';
-            
+
                 //Make sure the email is not banned
                 if(!$errors['err'] && TicketFilter::isBanned($ticket->getEmail()))
                     $errors['err']='Email is in banlist. Must be removed to reply.';
             }
 
             $wasOpen =($ticket->isOpen());
+
             //If no error...do the do.
-            if(!$errors && ($respId=$ticket->postReply($_POST, $errorsi, isset($_POST['emailreply'])))) {
+            $vars = $_POST;
+            if(!$errors && $_FILES['attachments'])
+                $vars['files'] = AttachmentFile::format($_FILES['attachments']);
+
+            if(!$errors && ($response=$ticket->postReply($vars, $errors, isset($_POST['emailreply'])))) {
                 $msg='Reply posted successfully';
                 $ticket->reload();
                 if($ticket->isClosed() && $wasOpen)
@@ -73,7 +75,7 @@ if($_POST && !$errors):
             }
             break;
         case 'transfer': /** Transfer ticket **/
-            //Check permission 
+            //Check permission
             if(!$thisstaff->canTransferTickets())
                 $errors['err']=$errors['transfer'] = 'Action Denied. You are not allowed to transfer tickets.';
             else {
@@ -85,13 +87,13 @@ if($_POST && !$errors):
                     $errors['deptId'] = 'Ticket already in the department';
                 elseif(!($dept=Dept::lookup($_POST['deptId'])))
                     $errors['deptId'] = 'Unknown or invalid department';
-            
+
                 //Transfer message - required.
                 if(!$_POST['transfer_comments'])
                     $errors['transfer_comments'] = 'Transfer comments required';
                 elseif(strlen($_POST['transfer_comments'])<5)
                     $errors['transfer_comments'] = 'Transfer comments too short!';
-           
+
                 //If no errors - them attempt the transfer.
                 if(!$errors && $ticket->transfer($_POST['deptId'], $_POST['transfer_comments'])) {
                     $msg = 'Ticket transferred successfully to '.$ticket->getDeptName();
@@ -112,7 +114,7 @@ if($_POST && !$errors):
              else {
 
                  $id = preg_replace("/[^0-9]/", "",$_POST['assignId']);
-                 $claim = (is_numeric($_POST['assignId']) && $_POST['assignId']==$thisstaff->getId()); 
+                 $claim = (is_numeric($_POST['assignId']) && $_POST['assignId']==$thisstaff->getId());
 
                  if(!$_POST['assignId'] || !$id)
                      $errors['assignId'] = 'Select assignee';
@@ -132,7 +134,7 @@ if($_POST && !$errors):
                      $errors['assign_comments'] = 'Assignment comments required';
                  elseif(strlen($_POST['assign_comments'])<5)
                          $errors['assign_comments'] = 'Comment too short';
-                 
+
                  if(!$errors && $ticket->assign($_POST['assignId'], $_POST['assign_comments'], !$claim)) {
                      if($claim) {
                          $msg = 'Ticket is NOW assigned to you!';
@@ -146,7 +148,7 @@ if($_POST && !$errors):
                      $errors['assign'] = 'Correct the error(s) below and try again!';
                  }
              }
-            break; 
+            break;
         case 'postnote': /* Post Internal Note */
             //Make sure the staff can set desired state
             if($_POST['state']) {
@@ -158,12 +160,22 @@ if($_POST && !$errors):
             }
 
             $wasOpen = ($ticket->isOpen());
-            if(($noteId=$ticket->postNote($_POST, $errors, $thisstaff))) {
+
+            $vars = $_POST;
+            if($_FILES['attachments'])
+                $vars['files'] = AttachmentFile::format($_FILES['attachments']);
+
+            if(($note=$ticket->postNote($vars, $errors, $thisstaff))) {
+
                 $msg='Internal note posted successfully';
                 if($wasOpen && $ticket->isClosed())
                     $ticket = null; //Going back to main listing.
+
             } else {
-                $errors['err'] = 'Unable to post internal note - missing or invalid data.';
+
+                if(!$errors['err'])
+                    $errors['err'] = 'Unable to post internal note - missing or invalid data.';
+
                 $errors['postnote'] = 'Unable to post the note. Correct the error(s) below and try again!';
             }
             break;
@@ -195,9 +207,9 @@ if($_POST && !$errors):
                             $note = $_POST['ticket_status_notes'];
                         else
                             $note='Ticket closed (without comments)';
-                        
+
                         $ticket->logNote('Ticket Closed', $note, $thisstaff);
-                        
+
                         //Going back to main listing.
                         TicketLock::removeStaffLocks($thisstaff->getId(), $ticket->getId());
                         $page=$ticket=null;
@@ -299,7 +311,7 @@ if($_POST && !$errors):
                     } elseif(Banlist::remove($ticket->getEmail())) {
                         $msg = 'Email removed from banlist';
                     } elseif(!BanList::includes($ticket->getEmail())) {
-                        $warn = 'Email is not in the banlist'; 
+                        $warn = 'Email is not in the banlist';
                     } else {
                         $errors['err']='Unable to remove the email from banlist. Try again.';
                     }
@@ -333,7 +345,7 @@ if($_POST && !$errors):
         switch($_POST['a']) {
             case 'mass_process':
                 if(!$thisstaff->canManageTickets())
-                    $errors['err']='You do not have permission to mass manage tickets. Contact admin for such access';    
+                    $errors['err']='You do not have permission to mass manage tickets. Contact admin for such access';
                 elseif(!$_POST['tids'] || !is_array($_POST['tids']))
                     $errors['err']='No tickets selected. You must select at least one ticket.';
                 else {
@@ -364,7 +376,7 @@ if($_POST && !$errors):
                             if($thisstaff->canCloseTickets()) {
                                 $note='Ticket closed without response by '.$thisstaff->getName();
                                 foreach($_POST['tids'] as $k=>$v) {
-                                    if(($t=Ticket::lookup($v)) && $t->isOpen() && @$t->close()) { 
+                                    if(($t=Ticket::lookup($v)) && $t->isOpen() && @$t->close()) {
                                         $i++;
                                         $t->logNote('Ticket Closed', $note, $thisstaff);
                                     }
@@ -401,7 +413,7 @@ if($_POST && !$errors):
                                 foreach($_POST['tids'] as $k=>$v) {
                                     if(($t=Ticket::lookup($v)) && @$t->delete()) $i++;
                                 }
-                        
+
                                 //Log a warning
                                 if($i) {
                                     $log = sprintf('%s (%s) just deleted %d ticket(s)',
@@ -429,13 +441,19 @@ if($_POST && !$errors):
                 $ticket=null;
                 if(!$thisstaff || !$thisstaff->canCreateTickets()) {
                      $errors['err']='You do not have permission to create tickets. Contact admin for such access';
-                }elseif(($ticket=Ticket::open($_POST, $errors))) {
-                    $msg='Ticket created successfully';
-                    $_REQUEST['a']=null;
-                    if(!$ticket->checkStaffAccess($thisstaff) || $ticket->isClosed())
-                        $ticket=null;
-                }elseif(!$errors['err']) {
-                    $errors['err']='Unable to create the ticket. Correct the error(s) and try again';
+                } else {
+                    $vars = $_POST;
+                    if($_FILES['attachments'])
+                        $vars['files'] = AttachmentFile::format($_FILES['attachments']);
+
+                    if(($ticket=Ticket::open($vars, $errors))) {
+                        $msg='Ticket created successfully';
+                        $_REQUEST['a']=null;
+                        if(!$ticket->checkStaffAccess($thisstaff) || $ticket->isClosed())
+                            $ticket=null;
+                    } elseif(!$errors['err']) {
+                        $errors['err']='Unable to create the ticket. Correct the error(s) and try again';
+                    }
                 }
                 break;
         }
@@ -470,7 +488,7 @@ if($cfg->showAnsweredTickets()) {
                                'title'=>'Answered Tickets',
                                'href'=>'tickets.php?status=answered',
                                'iconclass'=>'answeredTickets'),
-                            ($_REQUEST['status']=='answered')); 
+                            ($_REQUEST['status']=='answered'));
     }
 }
 
@@ -515,7 +533,7 @@ if($thisstaff->canCreateTickets()) {
     $nav->addSubMenu(array('desc'=>'New Ticket',
                            'href'=>'tickets.php?a=open',
                            'iconclass'=>'newTicket'),
-                        ($_REQUEST['a']=='open'));    
+                        ($_REQUEST['a']=='open'));
 }
 
 
@@ -524,7 +542,7 @@ if($ticket) {
     $ost->setPageTitle('Ticket #'.$ticket->getNumber());
     $nav->setActiveSubMenu(-1);
     $inc = 'ticket-view.inc.php';
-    if($_REQUEST['a']=='edit' && $thisstaff->canEditTickets()) 
+    if($_REQUEST['a']=='edit' && $thisstaff->canEditTickets())
         $inc = 'ticket-edit.inc.php';
     elseif($_REQUEST['a'] == 'print' && !$ticket->pdfExport($_REQUEST['psize'], $_REQUEST['notes']))
         $errors['err'] = 'Internal error: Unable to export the ticket to PDF for print.';
diff --git a/tickets.php b/tickets.php
index d175d49b595e8d1676df0a69b6293d47d7c10c78..d1293db874536f9d2edf836b37838a019366ea79 100644
--- a/tickets.php
+++ b/tickets.php
@@ -40,12 +40,11 @@ if($_POST && is_object($ticket) && $ticket->getId()):
 
         if(!$errors) {
             //Everything checked out...do the magic.
-            if(($msgid=$ticket->postMessage(array('message'=>$_POST['message']), 'Web'))) {
-    
-                //Upload files
-                if($cfg->allowOnlineAttachments() && $_FILES['attachments'])
-                    $ticket->uploadFiles($_FILES['attachments'], $msgid, 'M');
+            $vars = array('message'=>$_POST['message']);
+            if($cfg->allowOnlineAttachments() && $_FILES['attachments'])
+                $vars['files'] = AttachmentFile::format($_FILES['attachments'], true);
 
+            if(($msgid=$ticket->postMessage($vars, 'Web'))) {
                 $msg='Message Posted Successfully';
             } else {
                 $errors['err']='Unable to post the message. Try again';