Skip to content
Snippets Groups Projects
class.thread.php 98.4 KiB
Newer Older
  • Learn to ignore specific revisions
  • <?php
    /*********************************************************************
        class.thread.php
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        Thread of things!
    
    Peter Rotich's avatar
    Peter Rotich committed
        XXX: Please DO NOT add any ticket related logic! use ticket class.
    
    
        Peter Rotich <peter@osticket.com>
    
        Copyright (c)  2006-2013 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.ticket.php');
    
    include_once(INCLUDE_DIR.'class.draft.php');
    
    include_once(INCLUDE_DIR.'class.role.php');
    
    Peter Rotich's avatar
    Peter Rotich committed
    //Ticket thread.
    
    class Thread extends VerySimpleModel
    implements Searchable {
    
        static $meta = array(
            'table' => THREAD_TABLE,
            'pk' => array('id'),
            'joins' => array(
                'ticket' => array(
                    'constraint' => array(
                        'object_type' => "'T'",
    
                        'object_id' => 'Ticket.ticket_id',
    
                'task' => array(
                    'constraint' => array(
                        'object_type' => "'A'",
                        'object_id' => 'Task.id',
                    ),
                ),
                'collaborators' => array(
                    'reverse' => 'Collaborator.thread',
                ),
    
    Peter Rotich's avatar
    Peter Rotich committed
    
                'referrals' => array(
                    'reverse' => 'ThreadReferral.thread',
                ),
    
                    'reverse' => 'ThreadEntry.thread',
    
                'events' => array(
                    'reverse' => 'ThreadEvent.thread',
    
        const MODE_STAFF = 1;
        const MODE_CLIENT = 2;
    
        var $_entries;
    
        var $_collaborators; // Cache for collabs
    
        var $_participants;
    
    Peter Rotich's avatar
    Peter Rotich committed
    
        function getId() {
    
    Peter Rotich's avatar
    Peter Rotich committed
        function getObjectId() {
    
    Peter Rotich's avatar
    Peter Rotich committed
        function getObjectType() {
    
        function getObject() {
    
            if (!$this->_object)
                $this->_object = ObjectModel::lookup(
                        $this->getObjectId(), $this->getObjectType());
    
            return $this->_object;
    
        function getNumAttachments() {
    
            return Attachment::objects()->filter(array(
                'thread_entry__thread' => $this
            ))->count();
    
    Peter Rotich's avatar
    Peter Rotich committed
        function getNumEntries() {
    
            return $this->entries->count();
        }
    
        function getEntries($criteria=false) {
    
            if (!isset($this->_entries)) {
                $this->_entries = $this->entries->annotate(array(
                    'has_attachments' => SqlAggregate::COUNT(SqlCase::N()
                        ->when(array('attachments__inline'=>0), 1)
                        ->otherwise(null)
                    ),
                ));
                $this->_entries->exclude(array('flags__hasbit'=>ThreadEntry::FLAG_HIDDEN));
                if ($criteria)
                    $this->_entries->filter($criteria);
            }
            return $this->_entries;
    
    Peter Rotich's avatar
    Peter Rotich committed
        // Referrals
        function getNumReferrals() {
            return $this->referrals->count();
        }
    
        function getReferrals() {
            return $this->referrals;
        }
    
    
        // Collaborators
        function getNumCollaborators() {
            return $this->collaborators->count();
    
        function getNumActiveCollaborators() {
    
            if (!isset($this->ht['active_collaborators']))
                $this->ht['active_collaborators'] = count($this->getActiveCollaborators());
    
            return $this->ht['active_collaborators'];
    
        function getActiveCollaborators() {
    
            $collaborators = $this->getCollaborators();
            $active = array();
            foreach ($collaborators as $c) {
    
    aydreeihn's avatar
    aydreeihn committed
              if ($c->isActive())
    
                $active[] = $c;
            }
            return $active;
    
        function getCollaborators($criteria=array()) {
    
            if ($this->_collaborators && !$criteria)
                return $this->_collaborators;
    
            $collaborators = $this->collaborators
                ->filter(array('thread_id' => $this->getId()));
    
            if (isset($criteria['isactive']))
    
              $collaborators->filter(array('flags__hasbit'=>Collaborator::FLAG_ACTIVE));
    
    
    
            // TODO: sort by name of the user
            $collaborators->order_by('user__name');
    
            if (!$criteria)
                $this->_collaborators = $collaborators;
    
            return $collaborators;
    
        function addCollaborator($user, $vars, &$errors, $event=true) {
    
    
            if (!$user)
                return null;
    
            $vars = array_merge(array(
                    'threadId' => $this->getId(),
                    'userId' => $user->getId()), $vars);
            if (!($c=Collaborator::add($vars, $errors)))
                return null;
    
            $this->_collaborators = null;
    
    
            if ($event)
                $this->getEvents()->log($this->getObject(),
                    'collab',
                    array('add' => array($user->getId() => array(
                            'name' => $user->getName()->getOriginal(),
                            'src' => @$vars['source'],
                        ))
                    )
                );
    
    
            return $c;
    
        function updateCollaborators($vars, &$errors) {
            global $thisstaff;
    
            if (!$thisstaff) return;
    
            //Deletes
            if($vars['del'] && ($ids=array_filter($vars['del']))) {
                $collabs = array();
                foreach ($ids as $k => $cid) {
                    if (($c=Collaborator::lookup($cid))
    
                            && ($c->getThreadId() == $this->getId())
    
                            && $c->delete())
                         $collabs[] = $c;
    
    
                     $this->getEvents()->log($this->getObject(), 'collab', array(
                         'del' => array($c->user_id => array('name' => $c->getName()->getOriginal()))
                     ));
    
            //statuses
            $cids = null;
            if($vars['cid'] && ($cids=array_filter($vars['cid']))) {
                $this->collaborators->filter(array(
                    'thread_id' => $this->getId(),
                    'id__in' => $cids
                ))->update(array(
                    'updated' => SqlFunction::NOW(),
                ));
    
    
                foreach ($vars['cid'] as $c) {
                  $collab = Collaborator::lookup($c);
                  if(get_class($collab) == 'Collaborator') {
                    $collab->setFlag(Collaborator::FLAG_ACTIVE, true);
                    $collab->save();
                  }
                }
    
            $inactive = $this->collaborators->filter(array(
    
                'thread_id' => $this->getId(),
                Q::not(array('id__in' => $cids ?: array(0)))
            ));
    
            if($inactive) {
              foreach ($inactive as $i) {
                $i->setFlag(Collaborator::FLAG_ACTIVE, false);
                $i->save();
              }
              $inactive->update(array(
                  'updated' => SqlFunction::NOW(),
              ));
            }
    
    
            unset($this->ht['active_collaborators']);
            $this->_collaborators = null;
    
            return true;
    
        //UserList of participants (collaborators)
        function getParticipants() {
    
            if (!isset($this->_participants)) {
                $list = new UserList();
                if ($collabs = $this->getActiveCollaborators()) {
                    foreach ($collabs as $c)
                        $list->add($c);
                }
    
                $this->_participants = $list;
            }
    
            return $this->_participants;
    
        function getReferral($id, $type) {
    
            return $this->referrals->findFirst(array(
    
    Peter Rotich's avatar
    Peter Rotich committed
                        'object_id'     => $id,
                        'object_type'   => $type));
        }
    
        function isReferred($to=null, $strict=false) {
    
    
            if (is_null($to) || !$this->referrals)
    
                return ($this->referrals && $this->referrals->count());
    
    Peter Rotich's avatar
    Peter Rotich committed
    
            switch (true) {
            case $to instanceof Staff:
                // Referred to the staff
                if ($this->getReferral($to->getId(),
                            ObjectModel::OBJECT_TYPE_STAFF))
                    return true;
    
                // Strict check only checks the Agent
                if ($strict)
                    return false;
    
                // Referred to staff's department
    
                if ($this->referrals->findFirst(array(
                       'object_id__in' => $to->getDepts(),
                       'object_type'   => ObjectModel::OBJECT_TYPE_DEPT)))
    
    Peter Rotich's avatar
    Peter Rotich committed
                    return true;
    
    
                // Referred to staff's teams
                if ($to->getTeams() && $this->referrals->findFirst(array(
    
    Peter Rotich's avatar
    Peter Rotich committed
                                'object_id__in' => $to->getTeams(),
    
                                'object_type'   => ObjectModel::OBJECT_TYPE_TEAM
                                )))
    
    Peter Rotich's avatar
    Peter Rotich committed
                    return true;
    
    Peter Rotich's avatar
    Peter Rotich committed
                break;
    
    aydreeihn's avatar
    aydreeihn committed
            case $to instanceof Team:
                //Referred to a Team
                return ($this->getReferral($to->getId(),
                            ObjectModel::OBJECT_TYPE_TEAM));
                break;
    
    Peter Rotich's avatar
    Peter Rotich committed
            case $to instanceof Dept:
                // Refered to the dept
    
                return ($this->getReferral($to->getId(),
                            ObjectModel::OBJECT_TYPE_DEPT));
    
    Peter Rotich's avatar
    Peter Rotich committed
                break;
            }
    
            return false;
    
    Peter Rotich's avatar
    Peter Rotich committed
        function refer($to) {
    
    
    Peter Rotich's avatar
    Peter Rotich committed
            if ($this->isReferred($to, true))
    
    aydreeihn's avatar
    aydreeihn committed
                return false;
    
    Peter Rotich's avatar
    Peter Rotich committed
            $vars = array('thread_id' => $this->getId());
            switch (true) {
            case $to instanceof Staff:
                $vars['object_id'] = $to->getId();
                $vars['object_type'] = ObjectModel::OBJECT_TYPE_STAFF;
                break;
            case $to instanceof Team:
                $vars['object_id'] = $to->getId();
                $vars['object_type'] = ObjectModel::OBJECT_TYPE_TEAM;
                break;
            case $to instanceof Dept:
                $vars['object_id'] = $to->getId();
                $vars['object_type'] = ObjectModel::OBJECT_TYPE_DEPT;
                break;
            default:
                return false;
            }
    
            return ThreadReferral::create($vars);
        }
    
        // Render thread
    
        function render($type=false, $options=array()) {
    
            $mode = $options['mode'] ?: self::MODE_STAFF;
    
            // Register thread actions prior to rendering the thread.
    
            if (!class_exists('tea_showemailheaders'))
                include_once INCLUDE_DIR . 'class.thread_actions.php';
    
            $entries = $this->getEntries();
    
    
            if ($type && is_array($type)) {
              $visibility = Q::all(array('type__in' => $type));
    
    
    aydreeihn's avatar
    aydreeihn committed
              if ($type['user_id']) {
                $visibility->add(array('user_id' => $type['user_id']));
    
                $visibility->ored = true;
              }
    
              $entries->filter($visibility);
            }
    
    Peter Rotich's avatar
    Peter Rotich committed
            if ($options['sort'] && !strcasecmp($options['sort'], 'DESC'))
                $entries->order_by('-id');
    
            // Precache all the attachments on this thread
            AttachmentFile::objects()->filter(array(
                'attachments__thread_entry__thread__id' => $this->id
            ))->all();
    
            $events = $this->getEvents();
    
            $inc = ($mode == self::MODE_STAFF) ? STAFFINC_DIR : CLIENTINC_DIR;
            include $inc . 'templates/thread-entries.tmpl.php';
    
    Peter Rotich's avatar
    Peter Rotich committed
    
        function getEntry($id) {
    
    Peter Rotich's avatar
    Peter Rotich committed
            return ThreadEntry::lookup($id, $this->getId());
    
        function getEvents() {
            return $this->events;
        }
    
        /**
         * postEmail
         *
         * After some security and sanity checks, attaches the body and subject
         * of the message in reply to this thread item
         *
         * Parameters:
         * mailinfo - (array) of information about the email, with at least the
         *          following keys
         *      - mid - (string) email message-id
         *      - name - (string) personal name of email originator
         *      - email - (string<email>) originating email address
         *      - subject - (string) email subject line (decoded)
         *      - body - (string) email message body (decoded)
         */
    
        function postEmail($mailinfo, $entry=null) {
    
            // +==================+===================+=============+
            // | Orig Thread-Type | Reply Thread-Type | Requires    |
            // +==================+===================+=============+
            // | *                | Message (M)       | From: Owner |
            // | *                | Note (N)          | From: Staff |
            // | Response (R)     | Message (M)       |             |
            // | Message (M)      | Response (R)      | From: Staff |
            // +------------------+-------------------+-------------+
    
            if (!$object = $this->getObject()) {
    
                // How should someone find this thread?
    
    Peter Rotich's avatar
    Peter Rotich committed
                return false;
    
            }
            elseif ($object instanceof Ticket && (
                   !$mailinfo['staffId']
                && $object->isClosed()
                && !$object->isReopenable()
            )) {
                // Ticket is closed, not reopenable, and email was not submitted
                // by an agent. Email cannot be submitted
                return false;
            }
    
            $vars = array(
                'mid' =>    $mailinfo['mid'],
                'header' => $mailinfo['header'],
                'poster' => $mailinfo['name'],
                'origin' => 'Email',
                'source' => 'Email',
                'ip' =>     '',
    
                'reply_to' => $entry,
    
                'recipients' => $mailinfo['recipients'],
    
                'thread_entry_recipients' => $mailinfo['thread_entry_recipients'],
    
                'to-email-id' => $mailinfo['to-email-id'],
    
    Peter Rotich's avatar
    Peter Rotich committed
                'autorespond' => !isset($mailinfo['passive']),
    
            // XXX: Is this necessary?
            if ($object instanceof Ticket)
                $vars['ticketId'] = $object->getId();
            if ($object instanceof Task)
                $vars['taskId'] = $object->getId();
    
            $errors = array();
    
            if (isset($mailinfo['attachments']))
                $vars['attachments'] = $mailinfo['attachments'];
    
            $body = $mailinfo['message'];
    
            // extra handling for determining Cc collabs
    
            if ($mailinfo['email']) {
              $staffSenderId = Staff::getIdByEmail($mailinfo['email']);
    
              if (!$staffSenderId) {
                $senderId = UserEmailModel::getIdByEmail($mailinfo['email']);
                if ($senderId) {
                  $mailinfo['userId'] = $senderId;
    
                  if ($object instanceof Ticket && $senderId != $object->user_id && $senderId != $object->staff_id) {
                    $mailinfo['userClass'] = 'C';
    
                    $collaboratorId = Collaborator::getIdByUserId($senderId, $this->getId());
                    $collaborator = Collaborator::lookup($collaboratorId);
    
                    if ($collaborator && ($collaborator->isCc()))
                      $vars['thread-type'] = 'M';
                  }
                }
              }
            }
    
    
            // Attempt to determine the user posting the entry and the
            // corresponding entry type by the information determined by the
            // mail parser (via the In-Reply-To header)
            switch ($mailinfo['userClass']) {
            case 'C': # Thread collaborator
    
                $vars['flags'] = ThreadEntry::FLAG_COLLABORATOR;
    
            case 'U': # Ticket owner
                $vars['thread-type'] = 'M';
                $vars['userId'] = $mailinfo['userId'];
                break;
    
            case 'A': # System administrator
            case 'S': # Staff member (agent)
                $vars['thread-type'] = 'N';
    
                $vars['staffId'] = $mailinfo['staffId'];
    
                if ($vars['staffId'])
                    $vars['poster'] = Staff::lookup($mailinfo['staffId']);
    
    
            // The user type was not identified by the mail parsing system. It
            // is likely that the In-Reply-To and References headers were not
            // properly brokered by the user's mail client. Use the old logic to
            // determine the post type.
            default:
                // Disambiguate if the user happens also to be a staff member of
                // the system. The current ticket owner should _always_ post
                // messages instead of notes or responses
                if ($object instanceof Ticket
                    && strcasecmp($mailinfo['email'], $object->getEmail()) == 0
                ) {
                    $vars['thread-type'] = 'M';
                    $vars['userId'] = $object->getUserId();
                }
                // Consider collaborator role (disambiguate staff members as
                // collaborators). Normally, the block above should match based
                // on the Referenced message-id header
    
                elseif ($C = $this->collaborators->filter(array(
                    'user__emails__address' => $mailinfo['email']
                ))->first()) {
    
                    $vars['thread-type'] = 'M';
                    // XXX: There's no way that mailinfo[userId] would be set
                    $vars['userId'] = $mailinfo['userId'] ?: $C->getUserId();
                    $vars['flags'] = ThreadEntry::FLAG_COLLABORATOR;
                }
    
                // Don't process the email -- it came FROM this system
    
                elseif (Email::getIdByEmail($mailinfo['email'])) {
                    return false;
    
    Peter Rotich's avatar
    Peter Rotich committed
            // Ensure we record the name of the person posting
            $vars['poster'] = $vars['poster']
                ?: $mailinfo['name'] ?: $mailinfo['email'];
    
    
            // TODO: Consider security constraints
    
            if (!$vars['thread-type']) {
    
                //XXX: Are we potentially leaking the email address to
                // collaborators?
    
                // Try not to destroy the format of the body
    
                $header = sprintf(
                    _S('Received From: %1$s <%2$s>') . "\n\n",
                    $mailinfo['name'], $mailinfo['email']);
    
                if ($body instanceof HtmlThreadEntryBody)
    
                    $header = nl2br(Format::htmlchars($header));
                // Add the banner to the top of the message
    
                if ($body instanceof ThreadEntryBody)
    
                    $body->prepend($header);
    
                $vars['userId'] = 0; //Unknown user! //XXX: Assume ticket owner?
    
                $vars['thread-type'] = 'M';
            }
    
            switch ($vars['thread-type']) {
            case 'M':
                $vars['message'] = $body;
    
                if ($object instanceof Threadable)
    
                    return $object->postThreadEntry('M', $vars);
    
                elseif ($this instanceof ObjectThread)
    
                    return $this->addMessage($vars, $errors);
    
                break;
    
            case 'N':
                $vars['note'] = $body;
                if ($object instanceof Threadable)
                    return $object->postThreadEntry('N', $vars);
                elseif ($this instanceof ObjectThread)
                    return $this->addNote($vars, $errors);
    
    
            throw new Exception('Unable to continue thread via email.');
    
    
            // Currently impossible, but indicate that this thread object could
            // not append the incoming email.
            return false;
    
    Peter Rotich's avatar
    Peter Rotich committed
    
        function deleteAttachments() {
    
            $deleted = Attachment::objects()->filter(array(
                'thread_entry__thread' => $this,
            ))->delete();
    
    Peter Rotich's avatar
    Peter Rotich committed
                AttachmentFile::deleteOrphans();
    
    Peter Rotich's avatar
    Peter Rotich committed
    
            return $deleted;
        }
    
    
        function removeCollaborators() {
    
            return Collaborator::objects()
                ->filter(array('thread_id'=>$this->getId()))
                ->delete();
    
        /**
         * Function: lookupByEmailHeaders
         *
         * Attempt to locate a thread by the email headers. It should be
         * considered a secondary lookup to ThreadEntry::lookupByEmailHeaders(),
         * which should find an actual thread entry, which should be possible
         * for all email communcation which is associated with a thread entry.
         * The only time where this is useful is for threads which triggered
         * email communication without a thread entry, for instance, like
         * tickets created without an initial message.
         */
        function lookupByEmailHeaders(&$mailinfo) {
            $possibles = array();
    
            foreach (array('mid', 'in-reply-to', 'references') as $header) {
    
                $matches = array();
                if (!isset($mailinfo[$header]) || !$mailinfo[$header])
                    continue;
                // Header may have multiple entries (usually separated by
                // spaces ( )
    
                elseif (!preg_match_all('/<([^>@]+@[^>]+)>/', $mailinfo[$header],
    
                // The References header will have the most recent message-id
                // (parent) on the far right.
                // @see rfc 1036, section 2.2.5
                // @see http://www.jwz.org/doc/threading.html
    
                $possibles = array_merge($possibles, array_reverse($matches[1]));
    
            // Add the message id if it is embedded in the body
            $match = array();
    
            if (preg_match('`(?:class="mid-|Ref-Mid: )([^"\s]*)(?:$|")`',
    
                    $mailinfo['message'], $match)
                && !in_array($match[1], $possibles)
            ) {
                $possibles[] = $match[1];
            }
    
            foreach ($possibles as $mid) {
                // Attempt to detect the ticket and user ids from the
                // message-id header. If the message originated from
                // osTicket, the Mailer class can break it apart. If it came
                // from this help desk, the 'loopback' property will be set
                // to true.
                $mid_info = Mailer::decodeMessageId($mid);
    
                if (!$mid_info || !$mid_info['loopback'])
                    continue;
                if (isset($mid_info['uid'])
    
                    && @$mid_info['threadId']
                    && ($t = Thread::lookup($mid_info['threadId']))
                ) {
                    if (@$mid_info['userId']) {
                        $mailinfo['userId'] = $mid_info['userId'];
                    }
                    elseif (@$mid_info['staffId']) {
                        $mailinfo['staffId'] = $mid_info['staffId'];
                    }
                    // ThreadEntry was positively identified
                    return $t;
                }
            }
    
        static function getSearchableFields() {
            return array(
                'lastmessage' => new DatetimeField(array(
                    'label' => __('Last Message'),
                )),
                'lastresponse' => new DatetimeField(array(
                    'label' => __('Last Response'),
                )),
            );
        }
    
        static function supportsCustomData() {
            false;
        }
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        function delete() {
    
    Peter Rotich's avatar
    Peter Rotich committed
            //Self delete
    
    Peter Rotich's avatar
    Peter Rotich committed
            // Clear email meta data (header..etc)
    
            ThreadEntryEmailInfo::objects()
                ->filter(array('thread_entry__thread' => $this))
                ->update(array('headers' => null));
    
    Peter Rotich's avatar
    Peter Rotich committed
    
            // Mass delete entries
    
    Peter Rotich's avatar
    Peter Rotich committed
            $this->deleteAttachments();
    
            $this->removeCollaborators();
    
            // Null out the events
            $this->events->update(array('thread_id' => 0));
    
    
    Peter Rotich's avatar
    Peter Rotich committed
            return true;
        }
    
    
        static function create($vars=false) {
    
            $inst = new static($vars);
    
            $inst->created = SqlFunction::NOW();
            return $inst;
    
    Peter Rotich's avatar
    Peter Rotich committed
        }
    
    Peter Rotich's avatar
    Peter Rotich committed
    
    
    class ThreadEntryEmailInfo extends VerySimpleModel {
        static $meta = array(
            'table' => THREAD_ENTRY_EMAIL_TABLE,
            'pk' => array('id'),
            'joins' => array(
                'thread_entry' => array(
                    'constraint' => array('thread_entry_id' => 'ThreadEntry.id'),
                ),
            ),
        );
    
    class ThreadEntry extends VerySimpleModel
    implements TemplateVariable {
    
        static $meta = array(
            'table' => THREAD_ENTRY_TABLE,
            'pk' => array('id'),
    
            'select_related' => array('staff', 'user', 'email_info'),
    
            'ordering' => array('created', 'id'),
    
            'joins' => array(
                'thread' => array(
    
                    'constraint' => array('thread_id' => 'Thread.id'),
    
                'parent' => array(
                    'constraint' => array('pid' => 'ThreadEntry.id'),
                    'null' => true,
                ),
                'children' => array(
                    'reverse' => 'ThreadEntry.parent',
                ),
                'email_info' => array(
                    'reverse' => 'ThreadEntryEmailInfo.thread_entry',
                    'list' => false,
                ),
    
                    'reverse' => 'Attachment.thread_entry',
                    'null' => true,
                ),
                'staff' => array(
                    'constraint' => array('staff_id' => 'Staff.staff_id'),
                    'null' => true,
                ),
                'user' => array(
                    'constraint' => array('user_id' => 'User.id'),
    
        const FLAG_ORIGINAL_MESSAGE         = 0x0001;
    
        const FLAG_EDITED                   = 0x0002;
        const FLAG_HIDDEN                   = 0x0004;
    
        const FLAG_GUARDED                  = 0x0008;   // No replace on edit
    
        const FLAG_RESENT                   = 0x0010;
    
        const FLAG_COLLABORATOR             = 0x0020;   // Message from collaborator
        const FLAG_BALANCED                 = 0x0040;   // HTML does not need to be balanced on ::display()
    
        const FLAG_SYSTEM                   = 0x0080;   // Entry is a system note.
    
        const FLAG_REPLY_ALL                = 0x00100;  // Agent response, reply all
        const FLAG_REPLY_USER               = 0x00200;  // Agent response, reply to User
    
        const PERM_EDIT     = 'thread.edit';
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        var $_body;
    
        var $is_autoreply;
        var $is_bounce;
    
        static protected $perms = array(
            self::PERM_EDIT => array(
                'title' => /* @trans */ 'Edit Thread',
                'desc'  => /* @trans */ 'Ability to edit thread items of other agents',
            ),
        );
    
    aydreeihn's avatar
    aydreeihn committed
        // Thread entry types
        static protected $types = array(
                'M' => 'message',
                'R' => 'response',
                'N' => 'note',
        );
    
        function getTypeName() {
          return self::$types[$this->type];
        }
    
    
        function postEmail($mailinfo) {
    
            global $ost;
    
            if (!($thread = $this->getThread()))
                // Kind of hard to continue a discussion without a thread ...
    
            elseif ($this->getEmailMessageId() == $mailinfo['mid'])
                // Reporting success so the email can be moved or deleted.
                return true;
    
            // Mail sent by this system will have a predictable message-id
            // If this incoming mail matches the code, then it very likely
            // originated from this system and looped
            $info = Mailer::decodeMessageId($mailinfo['mid']);
            if ($info && $info['loopback']) {
                // This mail was sent by this system. It was received due to
                // some kind of mail delivery loop. It should not be considered
                // a response to an existing thread entry
                if ($ost)
                    $ost->log(LOG_ERR, _S('Email loop detected'), sprintf(
                    _S('It appears as though &lt;%s&gt; is being used as a forwarded or fetched email account and is also being used as a user / system account. Please correct the loop or seek technical assistance.'),
                    $mailinfo['email']),
    
                    // This is quite intentional -- don't continue the loop
                    false,
                    // Force the message, even if logging is disabled
                    true);
                return $this;
            }
    
            return $thread->postEmail($mailinfo, $this);
    
        }
    
        function getId() {
            return $this->id;
        }
    
        function getPid() {
    
            return $this->get('pid', 0);
    
        function getParent() {
    
            return $this->parent;
    
    Peter Rotich's avatar
    Peter Rotich committed
    
            if (!isset($this->_body)) {
                $body = $this->body;
                if ($body == null && $this->getNumAttachments()) {
                    foreach ($this->attachments as $a)
                        if ($a->inline && ($f=$a->getFile()))
                            $body .= $f->getData();
                }
    
                $this->_body = ThreadEntryBody::fromFormattedText($body, $this->format,
                    array('balanced' => $this->hasFlag(self::FLAG_BALANCED))
                );
            }
    
            return $this->_body;
    
        function setBody($body) {
            global $cfg;
    
    
    Peter Rotich's avatar
    Peter Rotich committed
            if (!$body instanceof ThreadEntryBody) {
    
                if ($cfg->isRichTextEnabled())
    
    Peter Rotich's avatar
    Peter Rotich committed
                    $body = new HtmlThreadEntryBody($body);
    
    Peter Rotich's avatar
    Peter Rotich committed
                    $body = new TextThreadEntryBody($body);
    
            $this->format = $body->getType();
            $this->body = (string) $body;
            return $this->save();
    
        function getMessage() {
            return $this->getBody();
        }
    
    
        function getCreateDate() {
    
        }
    
        function getNumAttachments() {
    
            return $this->attachments->count();
    
        function getEmailMessageId() {
    
            if ($this->email_info)
                return $this->email_info->mid;
    
        function getEmailHeaderArray() {
    
            require_once(INCLUDE_DIR.'class.mailparse.php');
    
    
            if (!isset($this->_headers) && $this->email_info
                && isset($this->email_info->headers)
            ) {
                $this->_headers = Mail_Parse::splitHeaders($this->email_info->headers);
            }
            return $this->_headers;
    
        function getEmailReferences($include_mid=true) {
            $references = '';
            $headers = self::getEmailHeaderArray();
            if (isset($headers['References']) && $headers['References'])
                $references = $headers['References']." ";
    
            if ($include_mid && ($mid = $this->getEmailMessageId()))
                $references .= $mid;
    
            return $references;
    
        /**
         * Retrieve a list of all the recients of this message if the message
         * was received via email.
         *
         * Returns:
         * (array<RFC_822>) list of recipients parsed with the Mail/RFC822
         * address parsing utility. Returns an empty array if the message was
         * not received via email.
         */
        function getAllEmailRecipients() {
            $headers = self::getEmailHeaderArray();
            $recipients = array();
            if (!$headers)
                return $recipients;
    
            foreach (array('To', 'Cc') as $H) {
                if (!isset($headers[$H]))
                    continue;
    
                if (!($all = Mail_Parse::parseAddressList($headers[$H])))
                    continue;
    
                $recipients = array_merge($recipients, $all);
            }
            return $recipients;
        }
    
    
        /**
         * Recurse through the ancestry of this thread entry to find the first
         * thread entry which cites a email Message-ID field.
         *
         * Returns:
         * <ThreadEntry> or null if neither this thread entry nor any of its
         * ancestry contains an email header with an email Message-ID header.
         */
        function findOriginalEmailMessage() {
            $P = $this;
            while (!$P->getEmailMessageId()
                && ($P = $P->getParent()));
            return $P;
        }
    
    
        function getUIDFromEmailReference($ref) {
    
            $info = unpack('Vtid/Vuid',
                    Base32::decode(strtolower(substr($ref, -13))));
    
            if ($info && $info['tid'] == $this->getId())
                return $info['uid'];
    
        }
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        function getThreadId() {
    
    Peter Rotich's avatar
    Peter Rotich committed
        }
    
        function getThread() {
    
            if (!isset($this->_thread) && $this->thread_id)
    
                // TODO: Consider typing the thread based on its type field
    
                $this->_thread = ObjectThread::lookup($this->getThreadId());
    
            return isset($this->staff_id) ? $this->staff_id : 0;
    
        }
    
        function getStaff() {
            return $this->staff;
        }
    
    
        function getUserId() {
    
            return isset($this->user_id) ? $this->user_id : 0;