<?php
/*********************************************************************
    class.ticket.php

    The most important class! Don't play with fire please.

    Peter Rotich <peter@osticket.com>
    Copyright (c)  2006-2012 osTicket
    http://www.osticket.com

    Released under the GNU General Public License WITHOUT ANY WARRANTY.
    See LICENSE.TXT for details.

    vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/
include_once(INCLUDE_DIR.'class.staff.php');
include_once(INCLUDE_DIR.'class.client.php');
include_once(INCLUDE_DIR.'class.team.php');
include_once(INCLUDE_DIR.'class.email.php');
include_once(INCLUDE_DIR.'class.dept.php');
include_once(INCLUDE_DIR.'class.topic.php');
include_once(INCLUDE_DIR.'class.lock.php');
include_once(INCLUDE_DIR.'class.file.php');
include_once(INCLUDE_DIR.'class.attachment.php');
include_once(INCLUDE_DIR.'class.pdf.php');
include_once(INCLUDE_DIR.'class.banlist.php');
include_once(INCLUDE_DIR.'class.template.php');
include_once(INCLUDE_DIR.'class.priority.php');
include_once(INCLUDE_DIR.'class.sla.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 $lastMsgId;
    
    var $dept;  //Dept obj
    var $sla;   // SLA obj
    var $staff; //Staff obj
    var $client; //Client Obj
    var $team;  //Team obj
    var $topic; //Topic obj
    var $tlock; //TicketLock 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 '
            .' ,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 '.TICKET_PRIORITY_TABLE.' pri ON ('
                .'ticket.priority_id=pri.priority_id) '
            .' LEFT JOIN '.TICKET_LOCK_TABLE.' tlock ON ('
                .'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';

        //echo $sql;
        if(!($res=db_query($sql)) || !db_num_rows($res))
            return false;

        
        $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'];
        
        //Reset the sub classes (initiated ondemand)...good for reloads.
        $this->staff = null;
        $this->client = null;
        $this->team  = null;
        $this->dept = null;
        $this->sla = null;
        $this->tlock = null;
        $this->stats = null;
        $this->topic = null;
        
        return true;
    }
        
    function reload() {
        return $this->load();
    }
    
    function isOpen() {
        return (strcasecmp($this->getStatus(),'Open')==0);
    }

    function isReopened() {
        return ($this->getReopenDate());
    }

    function isClosed() {
        return (strcasecmp($this->getStatus(),'Closed')==0);
    }

    function isAssigned() {
        return ($this->isOpen() && ($this->getStaffId() || $this->getTeamId()));
    }

    function isOverdue() {
        return ($this->overdue);
    }
    
    function isAnswered() {
       return ($this->ht['isanswered']);
    }

    function isLocked() {
        return ($this->getLockId());
    }

    function checkStaffAccess($staff) {

        if(!is_object($staff) && !($staff=Staff::lookup($staff)))
            return false;

        return ((!$staff->showAssignedOnly() && $staff->canAccessDept($this->getDeptId()))
                 || ($this->getTeamId() && $staff->isTeamMember($this->getTeamId()))
                 || $staff->getId()==$this->getStaffId());
    }

    function checkClientAccess($client) {
        global $cfg;

        if(!is_object($client) && !($client=Client::lookup($client)))
            return false;

        if(!strcasecmp($client->getEmail(),$this->getEmail()))
            return true;

        return ($cfg && $cfg->showRelatedTickets() 
            && $client->getTicketId()==$this->getExtId());
    }

    //Getters
    function getId(){
        return  $this->id;
    }

    function getExtId(){
        return  $this->extid;
    }
   
    function getEmail(){
        return $this->email;
    }

    function getAuthToken() {
        # XXX: Support variable email address (for CCs)
        return md5($this->getId() . $this->getEmail() . SECRET_SALT);
    }

    function getName(){
        return $this->fullname;
    }

    function getSubject() {
        return $this->subject;
    }

    /* Help topic title  - NOT object -> $topic */
    function getHelpTopic() {

        if(!$this->helpTopic && ($topic=$this->getTopic()))
            $this->helpTopic = $topic->getName();
            
        return $this->helpTopic;
    }
   
    function getCreateDate(){
        return $this->created;
    }

    function getOpenDate() {
        return $this->getCreateDate();
    }

    function getReopenDate() {
        return $this->reopened;
    }
    
    function getUpdateDate(){
        return $this->updated;
    }

    function getDueDate(){
        return $this->duedate;
    }

    function getCloseDate(){
        return $this->closed;
    }

    function getStatus(){
        return $this->status;
    }
   
    function getDeptId(){
       return $this->dept_id;
    }
   
    function getDeptName(){
       return $this->dept_name;
    }

    function getPriorityId() {
        return $this->priority_id;
    }
    
    function getPriority() {
        return $this->priority;
    }
     
    function getPhone() {
        return $this->ht['phone'];
    }

    function getPhoneExt() {
        return $this->ht['phone_ext'];
    }

    function getPhoneNumber() {
        $phone=Format::phone($this->getPhone());
        if(($ext=$this->getPhoneExt()))
            $phone.=" $ext";

        return $phone;
    }

    function getSource() {
        return $this->ht['source'];
    }
    
    function getIP() {
        return $this->ht['ip_address'];
    }

    function getHashtable() {
        return $this->ht;
    }

    function getUpdateInfo() {

        $info=array('name'  =>  $this->getName(),
                    'email' =>  $this->getEmail(),
                    'phone' =>  $this->getPhone(),
                    'phone_ext' =>  $this->getPhoneExt(),
                    'subject'   =>  $this->getSubject(),
                    'source'    =>  $this->getSource(),
                    'topicId'   =>  $this->getTopicId(),
                    'priorityId'    =>  $this->getPriorityId(),
                    'slaId' =>  $this->getSLAId(),
                    '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;
    }
    
    function getLock(){
        
        if(!$this->tlock && $this->getLockId())
            $this->tlock= TicketLock::lookup($this->getLockId(),$this->getId());
        
        return $this->tlock;
    }
    
    function acquireLock($staffId, $lockTime) {
       
        if(!$staffId or !$lockTime) //Lockig disabled?
            return null;

        //Check if the ticket is already locked.
        if(($lock=$this->getLock()) && !$lock->isExpired()) {
            if($lock->getStaffId()!=$staffId) //someone else locked the ticket.
                return null;

            //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..
        //load and return the newly created lock if any!
        return $this->getLock();
    }
    
    function getDept(){
        
        if(!$this->dept && $this->getDeptId())
            $this->dept= Dept::lookup($this->getDeptId());

        return $this->dept;
    }

    function getClient() {

        if(!$this->client)
            $this->client = Client::lookup($this->getExtId(), $this->getEmail());

        return $this->client;
    }
    
    function getStaffId(){
        return $this->staff_id;
    }

    function getStaff(){

        if(!$this->staff && $this->getStaffId())
            $this->staff= Staff::lookup($this->getStaffId());

        return $this->staff;
    }

    function getTeamId(){
        return $this->team_id;
    }

    function getTeam(){

        if(!$this->team && $this->getTeamId())
            $this->team = Team::lookup($this->getTeamId());

        return $this->team;
    }

    function getAssignee() {

        if($staff=$this->getStaff())
            return $staff->getName();

        if($team=$this->getTeam())
            return $team->getName();

        return '';
    }

    function getAssignees() {
     
        $assignees=array();
        if($staff=$this->getStaff())
            $assignees[] = $staff->getName();
                       
        if($team=$this->getTeam())
            $assignees[] = $team->getName();

        return $assignees;
    }

    function getAssigned($glue='/') {
        $assignees = $this->getAssignees();
        return $assignees?implode($glue, $assignees):'';
    }

    function getTopicId() {
        return $this->topic_id;
    }

    function getTopic() { 

        if(!$this->topic && $this->getTopicId())
            $this->topic = Topic::lookup($this->getTopicId());

        return $this->topic;
    }

 
    function getSLAId() {
        return $this->sla_id;
    }

    function getSLA() {

        if(!$this->sla && $this->getSLAId())
            $this->sla = SLA::lookup($this->getSLAId);

        return $this->sla;
    }

    function getLastRespondent() {

        $sql ='SELECT  resp.staff_id '
             .' FROM '.TICKET_THREAD_TABLE.' resp '
             .' LEFT JOIN '.STAFF_TABLE. ' USING(staff_id) '
             .' WHERE  resp.ticket_id='.db_input($this->getId()).' AND resp.staff_id>0 '
             .'   AND  resp.thread_type="R"'
             .' ORDER BY resp.created DESC LIMIT 1';

        if(!($res=db_query($sql)) || !db_num_rows($res))
            return null;
            
        list($id)=db_fetch_row($res);

        return Staff::lookup($id);

    }

    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;
    }

    function getLastMsgDate() {
        return $this->getLastMessageDate();
    }

    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;
    }

    function getLastRespDate() {
        return $this->getLastResponseDate();
    }

        
    function getLastMsgId() {
        return $this->lastMsgId;
    }

    function getRelatedTicketsCount(){

        $sql='SELECT count(*)  FROM '.TICKET_TABLE
            .' WHERE email='.db_input($this->getEmail());

        return db_result(db_query($sql));
    }

    function getThreadCount() {
        return $this->getNumMessages() + $this->getNumResponses();
    }

    function getNumMessages() {
        return $this->ht['messages'];
    }

    function getNumResponses() {
        return $this->ht['responses'];
    }

    function getNumNotes() {
        return $this->ht['notes'];
    }

    function getMessages() {
        return $this->getThreadByType('M');
    }

    function getResponses($msgId=0) {
        return $this->getThreadByType('R', $msgID);
    }

    function getNotes() {
        return $this->getThreadByType('N');
    }

    function getClientThread() {
        return $this->getThreadWithoutNotes();
    }

    function getThreadWithNotes() {
        return $this->getThread(true);
    }
    
    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 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 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) {
        return $this->lastMsgId=$msgid;
    }

    function setPriority($priorityId) {

        //XXX: what happens to SLA priority???
        
        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());

        return (($res=db_query($sql)) && db_affected_rows($res));
    }

    //DeptId can NOT be 0. No orphans please!
    function setDeptId($deptId){
        
        //Make sure it's a valid department//
        if(!($dept=Dept::lookup($deptId)))
            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){
       
        $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()) {
            $this->staff_id = $staffId;
            return true;
        }
        return false;
    }

    function setSLAId($slaId) {
        if ($slaId == $this->getSLAId()) return true;
        return db_query(
             'UPDATE '.TICKET_TABLE.' SET sla_id='.db_input($slaId)
            .' WHERE ticket_id='.db_input($this->getId()))
            && db_affected_rows();
    }
    /**
     * Selects the appropriate service-level-agreement plan for this ticket.
     * When tickets are transfered between departments, the SLA of the new
     * department should be applied to the ticket. This would be usefule,
     * for instance, if the ticket is transferred to a different department
     * which has a shorter grace period, the ticket should be considered
     * overdue in the shorter window now that it is owned by the new
     * department.
     *
     * $trump - if received, should trump any other possible SLA source.
     *          This is used in the case of email filters, where the SLA
     *          specified in the filter should trump any other SLA to be
     *          considered.
     */
    function selectSLAId($trump=null) {
        global $cfg;
        # XXX Should the SLA be overwritten if it was originally set via an
        #     email filter? This method doesn't consider such a case
        if ($trump !== null) {
            $slaId = $trump;
        } elseif ($this->getDept()->getSLAId()) {
            $slaId = $this->getDept()->getSLAId();
        } elseif ($this->getTopicId() && $this->getTopic()) {
            $slaId = $this->getTopic()->getSLAId();
        } else {
            $slaId = $cfg->getDefaultSLAId();
        }
        return ($slaId && $this->setSLAId($slaId)) ? $slaId : false;
    }

    //Set team ID...assign/unassign/release (id can be 0)
    function setTeamId($teamId){

      $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), team_id='.db_input($teamId)
          .' WHERE ticket_id='.db_input($this->getId());

      return (db_query($sql)  && db_affected_rows());
    }

    //Status helper.
    function setStatus($status) {

        if(strcasecmp($this->getStatus(),$status)==0)
            return true; //No changes needed.

        switch(strtolower($status)) {
            case 'open':
                return $this->reopen();
                break;
            case 'closed':
                return $this->close();
                break;
        }

        return false;
    }

    function setState($state, $alerts=false) {

        switch(strtolower($state)) {
            case 'open':
                return $this->setStatus('open');
                break;
            case 'closed':
                return $this->setStatus('closed');
                break;
            case 'answered':
                return $this->setAnsweredState(1);
                break;
            case 'unanswered':
                return $this->setAnsweredState(0);
                break;
            case 'overdue':
                return $this->markOverdue();
                break;
        }

        return false;
    }




    function setAnsweredState($isanswered) {

        $sql='UPDATE '.TICKET_TABLE.' SET isanswered='.db_input($isanswered)
            .' WHERE ticket_id='.db_input($this->getId());

        return (db_query($sql) && db_affected_rows());
    }

    //Close the ticket
    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. 
            $sql.=', staff_id='.db_input($thisstaff->getId());

        $sql.=' WHERE ticket_id='.db_input($this->getId());

        $this->logEvent('closed');
        return (db_query($sql) && db_affected_rows());
    }

    //set status to open on a closed ticket.
    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 

        $this->logEvent('reopened', 'closed');
        return (db_query($sql) && db_affected_rows());
    }

    function onNewTicket($message, $autorespond=true, $alertstaff=true) {
        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() 
                &&  ($msg=$tpl->getAutoRespMsgTemplate())) {
              
            $body=$this->replaceTemplateVars($msg['body']);
            $subj=$this->replaceTemplateVars($msg['subj']);
            $body = str_replace('%message', $message, $body);
            $body = str_replace('%signature',($dept && $dept->isPublic())?$dept->getSignature():'',$body);
            
            if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator()))
                $body ="\n$tag\n\n".$body;
            
            //TODO: add auto flags....be nice to mail servers and sysadmins!!
            $email->send($this->getEmail(),$subj,$body);
        }
        
        if(!($email=$cfg->getAlertEmail()))
            $email =$cfg->getDefaultEmail();
          
        //Send alert to out sleepy & idle staff.
        if($alertstaff && $email
                && $cfg->alertONNewTicket() 
                && ($msg=$tpl->getNewTicketAlertMsgTemplate())) {
              
            $body=$this->replaceTemplateVars($msg['body']);
            $subj=$this->replaceTemplateVars($msg['subj']);
            $body = str_replace('%message', $message, $body);
            
            $recipients=$sentlist=array();
            
            //Alert admin??
            if($cfg->alertAdminONNewTicket()) {
                $alert = str_replace("%staff",'Admin',$body);
                $email->send($cfg->getAdminEmail(),$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;
                $alert = str_replace("%staff",$staff->getFirstName(),$body);
                $email->send($staff->getEmail(),$subj,$alert);
                $sentlist[] = $staff->getEmail();
            }
           
           
        }
        
        return true;
    }

    function onOpenLimit($sendNotice=true) {
        global $ost, $cfg;

        //Log the limit notice as a warning for admin.
        $msg=sprintf('Max open tickets (%d) reached  for %s ', $cfg->getMaxOpenTickets(), $this->getEmail());
        $ost->logWarning('Max. Open Tickets Limit ('.$this->getEmail().')', $msg);

        if(!$sendNotice || !$cfg->sendOverLimitNotice()) return true;

        //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) {
            $body=$this->replaceTemplateVars($msg['body']);
            $subj=$this->replaceTemplateVars($msg['subj']);
            $body = str_replace('%signature',($dept && $dept->isPublic())?$dept->getSignature():'',$body);
            $email->send($this->getEmail(), $subj, $body);
        }

        $client= $this->getClient();
        
        //Alert admin...this might be spammy (no option to disable)...but it is helpful..I think.
        $msg='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', $msg);
       
        return true;
    }

    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){
        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 
        if(!($staff=$this->getStaff()) || !$staff->isAvailable()) {
            if($cfg->autoAssignReopenedTickets() && ($lastrep=$this->getLastRespondent()) && $lastrep->isAvailable()) {
                $this->setStaffId($lastrep->getId()); //direct assignment;
            } else {
                $this->setStaffId(0); //unassign - last respondent is not available.
            }
        }

        if($this->isClosed()) $this->reopen(); //reopen..

       /**********   double check auto-response  ************/
        if($autorespond && (Email::getIdByEmail($this->getEmail())))
            $autorespond=false;
        elseif($autorespond && ($dept=$this->getDept()))
            $autorespond=$dept->autoRespONNewMessage();


        if(!$autorespond && !$cfg->autoRespONNewMessage()) return;  //no autoresp or alerts.

        $this->reload();


        if(!$dept || !($tpl = $dept->getTemplate()))
            $tpl= $cfg->getDefaultTemplate();
       
        //If enabled...send confirmation to user. ( New Message AutoResponse)
        if($tpl && ($msg=$tpl->getNewMessageAutorepMsgTemplate())) {
                        
            $body=$this->replaceTemplateVars($msg['body']);
            $subj=$this->replaceTemplateVars($msg['subj']);
            $body = str_replace('%signature',($dept && $dept->isPublic())?$dept->getSignature():'',$body);

            //Reply separator tag.
            if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator()))
                $body ="\n$tag\n\n".$body;
            
            if(!$dept || !($email=$dept->getAutoRespEmail()))
                $email=$cfg->getDefaultEmail();
            
            if($email) {
                $email->send($this->getEmail(),$subj,$body);
            }
        }
    }

    function onAssign($note, $alert=true) {
        global $cfg, $thisstaff;

        if($this->isClosed()) $this->reopen(); //Assigned tickets must be open - otherwise why assign?

        $this->reload();

        //Log an internal note - no alerts on the internal note.
        $note=$note?$note:'Ticket assignment';
        $this->postNote('Ticket Assigned to '.$this->getAssignee(),$note,false);

        //See if we need to send alerts
        if(!$alert || !$cfg->alertONAssignment()) return true; //No alerts!

        $dept = $this->getDept();

        //Get template.
        if(!$dept || !($tpl = $dept->getTemplate()))
            $tpl= $cfg->getDefaultTemplate();

        //Email to use!
        if(!($email=$cfg->getAlertEmail()))
            $email =$cfg->getDefaultEmail();

        //Get the message template
        if($tpl && ($msg=$tpl->getAssignedAlertMsgTemplate()) && $email) {

            $body=$this->replaceTemplateVars($msg['body']);
            $subj=$this->replaceTemplateVars($msg['subj']);
            $body = str_replace('%note', $note, $body);
            $body = str_replace('%message', $note, $body); //Previous versions used message.
            $body = str_replace('%assignee', $this->getAssignee(), $body);
            $body = str_replace('%assigner', ($thisstaff)?$thisstaff->getName():'System',$body);
            //recipients
            $recipients=array();
            //Assigned staff or team... if any
            // Assigning a ticket to a team when already assigned to staff disables alerts to the team (!))
            if($cfg->alertStaffONAssignment() && $this->getStaffId())
                $recipients[]=$this->getStaff();
            elseif($this->getTeamId() && ($team=$this->getTeam())) {
                if($cfg->alertTeamMembersONAssignment() && ($members=$team->getMembers()))
                    $recipients+=$members;
                elseif($cfg->alertTeamLeadONAssignment() && ($lead=$team->getTeamLead()))
                    $recipients[]=$lead;
            }
            //Send the alerts.
            $sentlist=array();
            foreach( $recipients as $k=>$staff){
                if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(),$sentlist)) continue;
                $alert = str_replace('%staff', $staff->getFirstName(), $body);
                $email->send($staff->getEmail(), $subj, $alert);
                $sentlist[] = $staff->getEmail();
            }
        }

        return true;
    }

   function onOverdue($whine=true, $comments="") {
        global $cfg;

        if($whine && ($sla=$this->getSLA()) && !$sla->alertOnOverdue())
            $whine = false;

        //check if we need to send alerts.
        if(!$whine || !$cfg->alertONOverdueTicket())
            return true;

        $dept = $this->getDept();
        //Get department-defined or default template.
        if(!$dept || !($tpl = $dept->getTemplate()))
            $tpl= $cfg->getDefaultTemplate();

        //Email to use!
        if(!($email=$cfg->getAlertEmail()))
            $email =$cfg->getDefaultEmail();

        //Get the message template
        if($tpl && ($msg=$tpl->getOverdueAlertMsgTemplate()) && $email) {

            $body=$this->replaceTemplateVars($msg['body']);
            $subj=$this->replaceTemplateVars($msg['subj']);
            $body = str_replace('%comments', $comments, $body); //Planned support.

            //recipients
            $recipients=array();
            //Assigned staff or team... if any
            if($this->isAssigned() && $cfg->alertAssignedONTransfer()) {
                if($this->getStaffId())
                    $recipients[]=$this->getStaff();
                elseif($this->getTeamId() && ($team=$this->getTeam()) && ($members=$team->getMembers()))
                    $recipients=array_merge($recipients, $members);
            } elseif($cfg->alertDeptMembersONOverdueTicket() && !$this->isAssigned()) {
                //Only alerts dept members if the ticket is NOT assigned.
                if(($members=$dept->getMembers()))
                    $recipients=array_merge($recipients, $members);
            }
            //Always alert dept manager??
            if($cfg->alertDeptManagerONOverdueTicket() && $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("%staff",$staff->getFirstName(),$body);
                $email->send($staff->getEmail(),$subj,$alert);
                $sentlist[] = $staff->getEmail();
            }

        }

        return true;
    }

    //Replace base variables.
    function replaceTemplateVars($text){
        global $cfg;

        $dept = $this->getDept();
        $staff= $this->getStaff();
        $team = $this->getTeam();

        //TODO: add new vars (team, sla...etc)


        $search = array('/%id/','/%ticket/','/%email/','/%name/','/%subject/','/%topic/','/%phone/','/%status/','/%priority/',
                        '/%dept/','/%staff/','/%team/','/%assigned/','/%createdate/','/%duedate/','/%closedate/',
                        '/%url/','/%auth/', '/%clientlink/');
        $replace = array($this->getId(),
                         $this->getExtId(),
                         $this->getEmail(),
                         $this->getName(),
                         $this->getSubject(),
                         $this->getHelpTopic(),
                         $this->getPhoneNumber(),
                         $this->getStatus(),
                         $this->getPriority(),
                         ($dept?$dept->getName():''),
                         ($staff?$staff->getName():''),
                         ($team?$team->getName():''),
                         $this->getAssigned(),
                         Format::db_daydatetime($this->getCreateDate()),
                         Format::db_daydatetime($this->getDueDate()),
                         Format::db_daydatetime($this->getCloseDate()),
                         $cfg->getBaseUrl(),
                         $this->getAuthToken(),
                         '%url/view.php?t=%ticket&e=%email&a=%auth');
        while ($text != ($T = preg_replace($search,$replace,$text))) {
            $text = $T;
        }
        return $text;
    }

    function markUnAnswered() {
        return (!$this->isAnswered() || $this->setAnsweredState(0));
    }

    function markAnswered() {
        return ($this->isAnswered() || $this->setAnsweredState(1));
    }

    function markOverdue($whine=true) {
        
        global $cfg;
        
        if($this->isOverdue())
            return true;

        $sql='UPDATE '.TICKET_TABLE.' SET isoverdue=1, updated=NOW() '
            .' WHERE ticket_id='.db_input($this->getId());

        if(!db_query($sql) || !db_affected_rows())
            return false;

        $this->logEvent('overdue');
        $this->onOverdue($whine);

        return true;
    }

    //Dept Tranfer...with alert.. done by staff 
    function transfer($deptId, $comments, $alert = true) {
        global $cfg, $thisstaff;
      
        if(!$this->setDeptId($deptId))
            return false;
         
        // Change to SLA of the new department
        $this->selectSLAId();
         $currentDept = $this->getDeptName(); //XXX: add to olddept to tpl vars??

         // Reopen ticket if closed 
         if($this->isClosed())
             $this->reopen();
        
         $this->reload(); //reload - new dept!!
         $this->logEvent('transferred');

         //Send out alerts if enabled AND requested
         if(!$alert || !$cfg->alertONTransfer() || !($dept=$this->getDept())) return true; //no alerts!!


         //Get template.
         if(!($tpl = $dept->getTemplate()))
             $tpl= $cfg->getDefaultTemplate();
        
         //Email to use!
         if(!($email=$cfg->getAlertEmail()))
             $email =$cfg->getDefaultEmail();
                
         //Get the message template 
         if($tpl && ($msg=$tpl->getTransferAlertMsgTemplate()) && $email) {
            
             $body=$this->replaceTemplateVars($msg['body']);
             $subj=$this->replaceTemplateVars($msg['subj']);
             $body = str_replace('%note', $comments, $body);
                        
            //recipients            
            $recipients=array();
            //Assigned staff or team... if any
            if($this->isAssigned() && $cfg->alertAssignedONTransfer()) {
                if($this->getStaffId())
                    $recipients[]=$this->getStaff();
                elseif($this->getTeamId() && ($team=$this->getTeam()) && ($members=$team->getMembers()))
                    $recipients+=$members;
            } elseif($cfg->alertDeptMembersONTransfer() && !$this->isAssigned()) {
                //Only alerts dept members if the ticket is NOT assigned.
                if(($members=$dept->getMembers()))
                    $recipients+=$members;
            }

            //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("%staff",$staff->getFirstName(),$body);
                $email->send($staff->getEmail(),$subj,$alert);
                $sentlist[] = $staff->getEmail();
            }
         }

         return true;
    }

    function assignToStaff($staff, $note, $alert=true) {

        if(!is_object($staff) && !($staff=Staff::lookup($staff)))
            return false;
        
        if(!$this->setStaffId($staff->getId()))
            return false;

        $this->onAssign($note, $alert);
        $this->logEvent('assigned');

        return true;
    }

    function assignToTeam($team, $note, $alert=true) {

        if(!is_object($team) && !($team=Team::lookup($team)))
            return false;

        if(!$this->setTeamId($team->getId()))
            return false;

        //Clear - staff if it's a closed ticket
        //  staff_id is overloaded -> assigned to & closed by.
        if($this->isClosed())
            $this->setStaffId(0);

        $this->onAssign($note, $alert);
        $this->logEvent('assigned');

        return true;
    }

    //Assign ticket to staff or team - overloaded ID.
    function assign($assignId, $note, $alert=true) {
        global $thisstaff;

        $rv=0;
        $id=preg_replace("/[^0-9]/", "",$assignId);
        if($assignId[0]=='t') {
            $rv=$this->assignToTeam($id, $note, $alert);
        } elseif($assignId[0]=='s' || is_numeric($assignId)) {
            $alert=($thisstaff && $thisstaff->getId()==$id)?false:$alert; //No alerts on self assigned tickets!!!
            //We don't care if a team is already assigned to the ticket - staff assignment takes precedence
            $rv=$this->assignToStaff($id, $note, $alert);
        }

        return $rv;
    }
    
    //unassign primary assignee
    function unassign() {

        if(!$this->isAssigned()) //We can't release what is not assigned buddy!
            return true;

        //We're unassigning in the order of precedence.
        if($this->getStaffId())
            return $this->setStaffId(0);
        elseif($this->getTeamId())
            return $this->setTeamId(0);

        return false;
    }

    function release() {
        return $this->unassign();
    }

    //Insert message from client
    function postMessage($message,$source='',$emsgid=null,$headers='',$newticket=false){
        global $cfg;
       
        if(!$this->getId()) return 0;

        //Strip quoted reply...on emailed replies
        if(!strcasecmp($source, 'Email') 
                && $cfg->stripQuotedReply() 
                && ($tag=$cfg->getReplySeparator()) && strpos($message, $tag))
            list($message)=split($tag, $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($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 ($emsgid !== null) {
            $sql='INSERT INTO '.TICKET_EMAIL_INFO_TABLE
                .' SET message_id='.db_input($msgid)
                .', email_mid='.db_input($emsgid)
                .', headers='.db_input($headers);
            db_query($sql);
        }

        if($newticket) return $msgid; //Our work is done...

        $autorespond = true;
        if ($autorespond && $headers && TicketFilter::isAutoResponse(Mail_Parse::splitHeaders($headers)))
            $autorespond=false;

        $this->onMessage($autorespond); //must be called b4 sending alerts to staff.

        $dept = $this->getDept();

        if(!$dept || !($tpl = $dept->getTemplate()))
            $tpl= $cfg->getDefaultTemplate();

        if(!($email=$cfg->getAlertEmail()))
            $email =$cfg->getDefaultEmail();

        //If enabled...send alert to staff (New Message Alert)
        if($cfg->alertONNewMessage() && $tpl && $email && ($msg=$tpl->getNewMessageAlertMsgTemplate())) {

            $body=$this->replaceTemplateVars($msg['body']);
            $subj=$this->replaceTemplateVars($msg['subj']);
            $body = str_replace("%message", $message,$body);

            //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){
                if(!$staff || !$staff->getEmail() || !$staff->isAvailable() && in_array($staff->getEmail(),$sentlist)) continue;
                $alert = str_replace("%staff",$staff->getFirstName(),$body);
                $email->send($staff->getEmail(),$subj,$alert);
                $sentlist[] = $staff->getEmail();
            }
        }
        
        return $msgid;
    }

    /* public */ 
    function postReply($vars, $errors, $alert = true) {
        global $thisstaff,$cfg;

        if(!$thisstaff || !$thisstaff->isStaff() || !$cfg) return 0;

        if(!$vars['msgId'])
            $errors['msgId'] ='Missing messageId - internal error';
        if(!$vars['response'])
            $errors['response'] = 'Response message required';

        if($errors) return 0;

        $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->getId())
            .' ,poster='.db_input($thisstaff->getName())
            .' ,ip_address='.db_input($thisstaff->getIP());

        if(!db_query($sql) || !($respId=db_insert_id()))
            return false;

        //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;

        $dept = $this->getDept();

        if(!($tpl = $dept->getTemplate()))
            $tpl= $cfg->getDefaultTemplate();

        if(!$dept || !($email=$dept->getEmail()))
            $email = $cfg->getDefaultEmail();

        if($tpl && ($msg=$tpl->getReplyMsgTemplate()) && $email) {
            $body=$this->replaceTemplateVars($msg['body']);
            $subj=$this->replaceTemplateVars($msg['subj']);
            $body = str_replace('%response',$vars['response'],$body);

            if($vars['signature']=='mine')
                $signature=$thisstaff->getSignature();
            elseif($vars['signature']=='dept' && $dept && $dept->isPublic())
                $signature=$dept->getSignature();
            else
                $signature='';

            $body = str_replace("%signature",$signature,$body);

            if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator()))
                $body ="\n$tag\n\n".$body;

            //Set attachments if emailing.
            $attachments =($cfg->emailAttachments() && $attachments)?$this->getAttachments($respId,'R'):array();
            //TODO: setup  5 param (options... e.g mid trackable on replies)
            $email->send($this->getEmail(), $subj, $body, $attachments);
        }

        return $respId;
    }

    //Activity log - saved as internal notes WHEN enabled!!
    function logActivity($title,$note){
        global $cfg;

        if(!$cfg || !$cfg->logTicketActivity())
            return 0;

        return $this->postNote($title,$note,false,'System');
    }

    // History log -- used for statistics generation (pretty reports)
    function logEvent($state, $annul=null, $staff=null) {
        global $thisstaff;

        if ($staff === null) {
            if ($thisstaff) $staff=$thisstaff->getUserName();
            else $staff='SYSTEM';               # XXX: Security Violation ?
        }
        # Annul previous entries if requested (for instance, reopening a
        # ticket will annul an 'closed' entry). This will be useful to
        # easily prevent repeated statistics.
        if ($annul) {
            db_query('UPDATE '.TICKET_EVENT_TABLE.' SET annulled=1'
                .' WHERE ticket_id='.db_input($this->getId())
                  .' AND state='.db_input($annul));
        }

        return db_query('INSERT INTO '.TICKET_EVENT_TABLE
            .' SET ticket_id='.db_input($this->getId())
            .', staff_id='.db_input($this->getStaffId())
            .', team_id='.db_input($this->getTeamId())
            .', dept_id='.db_input($this->getDeptId())
            .', topic_id='.db_input($this->getTopicId())
            .', timestamp=NOW(), state='.db_input($state)
            .', staff='.db_input($staff))
            && db_affected_rows() == 1;
    }

    //Insert Internal Notes 
    function postNote($title,$note,$alert=true,$poster='') {        
        global $thisstaff,$cfg;
		
        $poster=($poster || !$thisstaff)?$poster:$thisstaff->getName();
		
        $sql= 'INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW() '.
                ',thread_type="N"'.
                ',ticket_id='.db_input($this->getId()).
                ',title='.db_input(Format::striptags($title)).
                ',body='.db_input(Format::striptags($note)).
                ',staff_id='.db_input($thisstaff?$thisstaff->getId():0).
                ',poster='.db_input($poster);
        //echo $sql;
        if(!db_query($sql) || !($id=db_insert_id()))
            return false;

        // If alerts are not enabled then return a success.
        if(!$alert || !$cfg->alertONNewNote() || !($dept=$this->getDept()))
            return $id;
        
        if(!($tpl = $dept->getTemplate()))
            $tpl= $cfg->getDefaultTemplate();

        if(!($email=$cfg->getAlertEmail()))
            $email =$cfg->getDefaultEmail();


        if($tpl && ($msg=$tpl->getNoteAlertMsgTemplate()) && $email) {
                    
            $body=$this->replaceTemplateVars($msg['body']);
            $subj=$this->replaceTemplateVars($msg['subj']);
            $body = str_replace('%note',"$title\n\n$note",$body);
            # TODO: Support a variable replacement of the staff writing the
            #       note

            // 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();

            $sentlist=array();
            foreach( $recipients as $k=>$staff) {
                if(!$staff || !is_object($staff) || !$staff->getEmail() || !$staff->isAvailable()) continue;
                if(in_array($staff->getEmail(),$sentlist) || ($thisstaff && $thisstaff->getId()==$staff->getId())) continue; 
                $alert = str_replace('%staff',$staff->getFirstName(),$body);
                $email->send($staff->getEmail(),$subj,$alert);
                $sentlist[] = $staff->getEmail();
            }
        }
        
        return $id;
    }

    //Print ticket... export the ticket thread as PDF.
    function pdfExport($psize='Letter', $notes=false) {
        $pdf = new Ticket2PDF($this, $psize, $notes);
        $name='Ticket-'.$this->getExtId().'.pdf';
        $pdf->Output($name, 'I');
        //Remember what the user selected - for autoselect on the next print.
        $_SESSION['PAPER_SIZE'] = $psize;
        exit;
    }

    //online based attached files.
    function uploadAttachments($files, $refid, $type) {
        global $ost;

        $uploaded=array();
        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->postNote('File Upload Error', $error, false);
               
                $ost->logDebug('File Upload Error (Ticket #'.$this->getExtId().')', $error);
            }
            
        }
        
        return $uploaded;
    }

    /*
       Save attachment to the DB. uploads (above), email or json/xml.
       
       @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(){
        
        $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();
        
        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');
        $fields['subject']  = array('type'=>'string',   'required'=>1, 'error'=>'Subject required');
        $fields['topicId']  = array('type'=>'int',      'required'=>1, 'error'=>'Help topic required');
        $fields['slaId']    = array('type'=>'int',      'required'=>1, 'error'=>'SLA required');
        $fields['priorityId'] = array('type'=>'int',    'required'=>1, 'error'=>'Priority required');
        $fields['phone']    = array('type'=>'phone',    'required'=>0, 'error'=>'Valid phone # required');
        $fields['duedate']  = array('type'=>'date',     'required'=>0, 'error'=>'Invalid date - must be MM/DD/YY');

        $fields['note']     = array('type'=>'text',     'required'=>1, 'error'=>'Reason for the update required');

        if(!Validator::process($fields, $vars, $errors) && !$errors['err'])
            $errors['err'] ='Missing or invalid data - check the errors and try again';

        if($vars['duedate']) {     
            if($this->isClosed())
                $errors['duedate']='Duedate can NOT be set on a closed ticket';
            elseif(!$vars['time'] || strpos($vars['time'],':')===false)
                $errors['time']='Select time';
            elseif(strtotime($vars['duedate'].' '.$vars['time'])===false)
                $errors['duedate']='Invalid duedate';
            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'])
                $errors['phone']='Invalid phone ext.';
            elseif(!$vars['phone']) //make sure they just didn't enter ext without phone #
                $errors['phone']='Phone number required';
        }

        if($errors) return false;

        $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW() '
            .' ,email='.db_input($vars['email'])
            .' ,name='.db_input(Format::striptags($vars['name']))
            .' ,subject='.db_input(Format::striptags($vars['subject']))
            .' ,phone="'.db_input($vars['phone'],false).'"'
            .' ,phone_ext='.db_input($vars['phone_ext']?$vars['phone_ext']:NULL)
            .' ,priority_id='.db_input($vars['priorityId'])
            .' ,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())
            return false;

        if(!$vars['note'])
            $vars['note']=sprintf('Ticket Updated by %s', $thisstaff->getName());

        $this->postNote('Ticket Updated', $vars['note']);
        $this->reload();
        
        return true;
    }

   
   /*============== Static functions. Use Ticket::function(params); ==================*/
    function getIdByExtId($extid) {
        $sql ='SELECT  ticket_id FROM '.TICKET_TABLE.' ticket WHERE ticketID='.db_input($extid);
        if(($res=db_query($sql)) && db_num_rows($res))
            list($id)=db_fetch_row($res);

        return $id;
    }


   
    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;    
    }

    function lookupByExtId($id) {
        return self::lookup(self:: getIdByExtId($id));
    }

    function genExtRandID() {
        global $cfg;

        //We can allow collissions...extId and email must be unique ...so same id with diff emails is ok..
        // But for clarity...we are going to make sure it is unique.
        $id=Misc::randNumber(EXT_TICKET_ID_LEN);
        if(db_num_rows(db_query('SELECT ticket_id FROM '.TICKET_TABLE.' WHERE ticketID='.db_input($id))))
            return Ticket::genExtRandID();

        return $id;
    }

    function getIdByMessageId($mid,$email) {

        if(!$mid || !$email)
            return 0;

        $sql='SELECT ticket.ticket_id FROM '.TICKET_TABLE. ' ticket '.
             ' LEFT JOIN '.TICKET_THREAD_TABLE.' msg USING(ticket_id) '.
             ' INNER JOIN '.TICKET_EMAIL_INFO_TABLE.' emsg ON (msg.id = emsg.message_id) '.
             ' WHERE email_mid='.db_input($mid).' AND email='.db_input($email);
        $id=0;
        if(($res=db_query($sql)) && db_num_rows($res))
            list($id)=db_fetch_row($res);

        return $id;
    }

    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))
            list($num)=db_fetch_row($res);

        return $num;
    }

    /* 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;


        $sql='SELECT count(open.ticket_id) as open, count(answered.ticket_id) as answered '
            .' ,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\' AND open.isanswered=0) '
            .' LEFT JOIN '.TICKET_TABLE.' answered
                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\' AND overdue.isoverdue=1) '
            .' LEFT JOIN '.TICKET_TABLE.' assigned
                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\' AND closed.staff_id='.db_input($staff->getId()).')'
            .' WHERE (ticket.staff_id='.db_input($staff->getId());

        if(($teams=$staff->getTeams()))
            $sql.=' OR ticket.team_id IN('.implode(',', array_filter($teams)).')';

        if(!$staff->showAssignedOnly() && ($depts=$staff->getDepts())) //Staff with limited access just see Assigned tickets.
            $sql.=' OR ticket.dept_id IN('.implode(',', $depts).') ';

        $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. 
     */
    function getClientStats($email) {

        if(!$email || !Validator::is_email($email))
            return null;

        $sql='SELECT count(open.ticket_id) as open, 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\') '
            .' LEFT JOIN '.TICKET_TABLE.' closed
                ON (closed.ticket_id=ticket.ticket_id AND closed.status=\'closed\')'
            .' WHERE ticket.email='.db_input($email);

        return db_fetch_array(db_query($sql));
    }

    /*
     * 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;

        //Check for 403
        if ($vars['email']  && Validator::is_email($vars['email'])) {

            //Make sure the email address is not banned
            if(TicketFilter::isBanned($vars['email'])) {
                $errors['err']='Ticket denied. Error #403';
                $ost->logWarning('Ticket denied', 'Banned email - '.$vars['email']);
                return 0;
            }

            //Make sure the open ticket limit hasn't been reached. (LOOP CONTROL)
            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 ', 
                            $cfg->getMaxOpenTickets(), $vars['email']));

                return 0;
            }
        }

        //Init ticket filters...
        $ticket_filter = new TicketFilter($origin, $vars);
        // Make sure email contents should not be rejected
        if($ticket_filter 
                && ($filter=$ticket_filter->shouldReject())) {
            $errors['err']='Ticket denied. Error #403';
            $ost->logWarning('Ticket denied', 
                    sprintf('Ticket rejected ( %s) by filter "%s"', 
                        $vars['email'], $filter->getName()));

            return 0;
        }

        $id=0;
        $fields=array();
        $fields['name']     = array('type'=>'string',   'required'=>1, 'error'=>'Name required');
        $fields['email']    = array('type'=>'email',    'required'=>1, 'error'=>'Valid email required');
        $fields['subject']  = array('type'=>'string',   'required'=>1, 'error'=>'Subject required');
        $fields['message']  = array('type'=>'text',     'required'=>1, 'error'=>'Message required');
        switch (strtolower($origin)) {
            case 'web':
                $fields['topicId']  = array('type'=>'int',  'required'=>1, 'error'=>'Select help topic');
                break;
            case 'staff':
                $fields['deptId']   = array('type'=>'int',  'required'=>1, 'error'=>'Dept. required');
                $fields['topicId']  = array('type'=>'int',  'required'=>1, 'error'=>'Topic required');
                $fields['duedate']  = array('type'=>'date', 'required'=>0, 'error'=>'Invalid date - must be MM/DD/YY');
            case 'api':
                $fields['source']   = array('type'=>'string', 'required'=>1, 'error'=>'Indicate source');
                break;
            case 'email':
                $fields['emailId']  = array('type'=>'int',  'required'=>1, 'error'=>'Email unknown');
                break;
            default:
                # TODO: Return error message
                $errors['err']=$errors['origin'] = 'Invalid origin given';
        }
        $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';

        //Make sure phone extension is valid
        if($vars['phone_ext'] ) {
            if(!is_numeric($vars['phone_ext']) && !$errors['phone'])
                $errors['phone']='Invalid phone ext.';
            elseif(!$vars['phone']) //make sure they just didn't enter ext without phone # XXX: reconsider allowing!
                $errors['phone']='Phone number required';
        }

        //Make sure the due date is valid
        if($vars['duedate']) {
            if(!$vars['time'] || strpos($vars['time'],':')===false)
                $errors['time']='Select time';
            elseif(strtotime($vars['duedate'].' '.$vars['time'])===false)
                $errors['duedate']='Invalid duedate';
            elseif(strtotime($vars['duedate'].' '.$vars['time'])<=time())
                $errors['duedate']='Due date must be in the future';
        }

        //Any error above is fatal.
        if($errors)  return 0;

        # Perform ticket filter actions on the new ticket arguments
        if ($ticket_filter) $ticket_filter->apply($vars);

        # Some things will need to be unpacked back into the scope of this
        # function
        if (isset($vars['autorespond'])) $autorespond=$vars['autorespond'];

        // OK...just do it.
        $deptId=$vars['deptId']; //pre-selected Dept if any.
        $priorityId=$vars['priorityId'];
        $source=ucfirst($vars['source']);
        $topic=NULL;
        // Intenal mapping magic...see if we need to overwrite anything
        if(isset($vars['topicId']) && ($topic=Topic::lookup($vars['topicId']))) { //Ticket created via web by user/or staff
            $deptId=$deptId?$deptId:$topic->getDeptId();
            $priorityId=$priorityId?$priorityId:$topic->getPriorityId();
            if($autorespond) $autorespond=$topic->autoRespond();
            $source=$vars['source']?$vars['source']:'Web';
        }elseif($vars['emailId'] && !$vars['deptId'] && ($email=Email::lookup($vars['emailId']))) { //Emailed Tickets
            $deptId=$email->getDeptId();
            $priorityId=$priorityId?$priorityId:$email->getPriorityId();
            if($autorespond) $autorespond=$email->autoRespond();
            $email=null;
            $source='Email';
        }
        //Last minute checks
        $priorityId=$priorityId?$priorityId:$cfg->getDefaultPriorityId();
        $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() '
            .' ,lastmessage= NOW()'
            .' ,ticketID='.db_input($extId)
            .' ,dept_id='.db_input($deptId)
            .' ,topic_id='.db_input($topicId)
            .' ,priority_id='.db_input($priorityId)
            .' ,email='.db_input($vars['email'])
            .' ,name='.db_input(Format::striptags($vars['name']))
            .' ,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) 
            .' ,source='.db_input($source);

        //Make sure the origin is staff - avoid firebug hack!
        if($vars['duedate'] && !strcasecmp($origin,'staff'))
             $sql.=' ,duedate='.db_input(date('Y-m-d G:i',Misc::dbtime($vars['duedate'].' '.$vars['time'])));


        if(!db_query($sql) || !($id=db_insert_id()) || !($ticket =Ticket::lookup($id)))
            return null;

        /* -------------------- POST CREATE ------------------------ */
        
        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'); 
            //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['message'],$source,$vars['mid'],$vars['header'],true);

        // Configure service-level-agreement for this ticket
        $ticket->selectSLAId($vars['slaId']);

        //Auto assign staff or team - auto assignment based on filter rules.
        if($vars['staffId'] && !$vars['assignId'])
             $ticket->assignToStaff($vars['staffId'],'auto-assignment');
        if($vars['teamId'] && !$vars['assignId'])
            $ticket->assignToTeam($vars['teamId'],'auto-assignment');

        /**********   double check auto-response  ************/
        //Overwrite auto responder if the FROM email is one of the internal emails...loop control.
        if($autorespond && (Email::getIdByEmail($ticket->getEmail())))
            $autorespond=false;

        if($autorespond && $dept && !$dept->autoRespONNewTicket())
            $autorespond=false;

        # 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']))) {
            $autorespond=false;
        }

        //Don't auto respond to mailer daemons.
        if( $autorespond &&
            (strpos(strtolower($vars['email']),'mailer-daemon@')!==false
             || strpos(strtolower($vars['email']),'postmaster@')!==false)) {
            $autorespond=false;
        }

        if ($vars['cannedResponseId']
                && ($canned = Canned::lookup($vars['cannedResponseId']))
                && $canned->isEnabled()) {
            $files = array();
            foreach ($canned->getAttachments() as $file)
                $files[] = $file['id'];
            $ticket->postReply(
                    array(
                        'msgId'     => $msgid,
                        'response'  =>
                            $ticket->replaceTemplateVars($canned->getResponse()),
                        'cannedattachments' => $files
                    ),$errors, true);
                    
            // If a canned-response is immediately queued for this ticket,
            // disable the autoresponse
            $autorespond=false;
        }

        /***** See if we need to send some alerts ****/
        $ticket->onNewTicket($vars['message'], $autorespond, $alertstaff);

        /************ check if the user JUST reached the max. open tickets limit **********/
        if($cfg->getMaxOpenTickets()>0
                    && ($client=$ticket->getClient())
                    && ($client->getNumOpenTickets()==$cfg->getMaxOpenTickets())) {
            $ticket->onOpenLimit(($autorespond && strcasecmp($origin, 'staff')));
        }
        
        /* Start tracking ticket lifecycle events */
        $ticket->logEvent('created');

        /* Phew! ... time for tea (KETEPA) */

        return $ticket;
    }

    function open($vars, &$errors) {
        global $thisstaff,$cfg;

        if(!$thisstaff || !$thisstaff->canCreateTickets()) return false;
        
        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']) {
            $vars['response']=$ticket->replaceTemplateVars($vars['response']);
            if(($respId=$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']);
        } elseif($vars['note']) { //Not assigned...save optional note if any
            $ticket->postNote('New Ticket',$vars['note'],false);
        } else { //Not assignment and no internal note - log activity
            $ticket->logActivity('New Ticket by Staff','Ticket created by staff -'.$thisstaff->getName());
        }

        $ticket->reload();
        
        if(!$cfg->notifyONNewStaffTicket() || !isset($var['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']."\n\n".$vars['response'];
            $body=$ticket->replaceTemplateVars($msg['body']);
            $subj=$ticket->replaceTemplateVars($msg['subj']);
            $body = str_replace('%message',$message,$body);

            if($vars['signature']=='mine')
                $signature=$thisstaff->getSignature();
            elseif($vars['signature']=='dept' && $dept && $dept->isPublic())
                $signature=$dept->getSignature();
            else
                $signature='';

            $body = str_replace('%signature',$signature,$body);

            if($cfg->stripQuotedReply() && ($tag=trim($cfg->getReplySeparator())))
                $body ="\n$tag\n\n".$body;

            $attachments =($cfg->emailAttachments() && $respId)?$this->getAttachments($respId,'R'):array();
            $email->send($ticket->getEmail(), $subj, $body, $attachments);
        }

        return $ticket;
    
    }
   
    function checkOverdue() {
       
        $sql='SELECT ticket_id FROM '.TICKET_TABLE.' T1 '
            .' JOIN '.SLA_TABLE.' T2 ON (T1.sla_id=T2.id) '
            .' WHERE status=\'open\' AND isoverdue=0 '
            .' AND ((reopened is NULL AND duedate is NULL AND TIME_TO_SEC(TIMEDIFF(NOW(),T1.created))>=T2.grace_period*3600) '
            .' OR (reopened is NOT NULL AND duedate is NULL AND TIME_TO_SEC(TIMEDIFF(NOW(),reopened))>=T2.grace_period*3600) '
            .' OR (duedate is NOT NULL AND duedate<NOW()) '
            .' ) ORDER BY T1.created LIMIT 50'; //Age upto 50 tickets at a time?
        //echo $sql;
        if(($res=db_query($sql)) && db_num_rows($res)) {
            while(list($id)=db_fetch_row($res)) {
                if(($ticket=Ticket::lookup($id)) && $ticket->markOverdue())
                    $ticket->logActivity('Ticket Marked Overdue', 'Ticket flagged as overdue by the system.');
            }
        } else {
            //TODO: Trigger escalation on already overdue tickets - make sure last overdue event > grace_period.

        }
   }
    
}
?>