Skip to content
Snippets Groups Projects
class.thread.php 72.9 KiB
Newer Older
<?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');
//Ticket thread.
class Thread extends VerySimpleModel {
    static $meta = array(
        'table' => THREAD_TABLE,
        'pk' => array('id'),
        'joins' => array(
            'ticket' => array(
                'constraint' => array(
                    'object_type' => "'T'",
                    'object_id' => 'TicketModel.ticket_id',
            'task' => array(
                'constraint' => array(
                    'object_type' => "'A'",
                    'object_id' => 'Task.id',
                ),
            ),
            'collaborators' => array(
                'reverse' => 'Collaborator.thread',
            ),
                'reverse' => 'ThreadEntry.thread',
            'events' => array(
                'reverse' => 'ThreadEvent.thread',
                'class' => 'ThreadEvents',
            ),
    const MODE_STAFF = 1;
    const MODE_CLIENT = 2;

    var $_collaborators; // Cache for collabs
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) {
        $base = $this->entries->annotate(array(
            'has_attachments' => SqlAggregate::COUNT('attachments', false,
                new Q(array('attachments__inline'=>0)))
        $base->exclude(array('flags__hasbit'=>ThreadEntry::FLAG_HIDDEN));
        if ($criteria)
            $base->filter($criteria);
        return $base;
    // 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() {
        return $this->getCollaborators(array('isactive'=>1));
    }

    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('isactive' => $criteria['isactive']));

        // TODO: sort by name of the user
        $collaborators->order_by('user__name');

        if (!$criteria)
            $this->_collaborators = $collaborators;

        return $collaborators;
    }

    function addCollaborator($user, $vars, &$errors) {

        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;

        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->getObject()->postThreadEntry('N',
                    array(
                        'title' => _S('Collaborators Removed'),
                        'note' => implode("<br>", $collabs)));
        }

        //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(),
                'isactive' => 1,
            ));
        }

        if ($cids) {
            $this->collaborators->filter(array(
                'thread_id' => $this->getId(),
                Q::not(array('id__in' => $cids))
            ))->update(array(
                'updated' => SqlFunction::NOW(),
                'isactive' => 0,
            ));
        }

        unset($this->ht['active_collaborators']);
        $this->_collaborators = null;

        return true;
    }
    // Render thread
    function render($type=false, $mode=self::MODE_STAFF) {

        $entries = $this->getEntries();
        if ($type && is_array($type))
            $entries->filter(array('type__in' => $type));

        $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) {
        global $ost;

        // +==================+===================+=============+
        // | 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?
            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;
        }

        // Mail sent by this system will have a message-id format of
        // <code-random-mailbox@domain.tld>
        // where code is a predictable string based on the SECRET_SALT of
        // this osTicket installation. If this incoming mail matches the
        // code, then it very likely originated from this system and looped
        @list($code) = explode('-', $mailinfo['mid'], 2);
        if (0 === strcasecmp(ltrim($code, '<'), substr(md5('mail'.SECRET_SALT), -9))) {
            // 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 true;
        }

        $vars = array(
            'mid' =>    $mailinfo['mid'],
            'header' => $mailinfo['header'],
            'poster' => $mailinfo['name'],
            'origin' => 'Email',
            'source' => 'Email',
            'ip' =>     '',
            'reply_to' => $this,
            'recipients' => $mailinfo['recipients'],
            'to-email-id' => $mailinfo['to-email-id'],
        );

        // 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'];
        $poster = $mailinfo['email'];

        // 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 ($mailinfo['userId'] || (
            $object instanceof Ticket
            && strcasecmp($mailinfo['email'], $object->getEmail()) == 0
        )) {
            $vars['message'] = $body;
            $vars['userId'] = $mailinfo['userId'] ?: $object->getUserId();

            if ($object instanceof Threadable)
                return $object->postThreadEntry('M', $vars);
            elseif ($this instanceof ObjectThread)
                $this->addMessage($vars, $errors);
            else
                throw new Exception('Cannot continue discussion with abstract thread');
        }
        // Consider collaborator role (disambiguate staff members as
        // collaborators). Normally, the block above should match based
        // on the Referenced message-id header
        elseif ($object instanceof Ticket
            && ($E = UserEmail::lookup($mailinfo['email']))
            && ($C = Collaborator::lookup(array(
                'ticketId' => $object->getId(), 'userId' => $E->user_id
            )))
        ) {
            $vars['userId'] = $mailinfo['userId'] ?: $C->getUserId();
            $vars['message'] = $body;
            if ($object instanceof Threadable)
                return $object->postThreadEntry('M', $vars);
            elseif ($this instanceof ObjectThread)
                $this->addMessage($vars, $errors);
            else
                throw new Exception('Cannot continue discussion with abstract thread');
        }
        // Accept internal note from staff members' replies
        elseif ($mailinfo['staffId']
                || ($mailinfo['staffId'] = Staff::getIdByEmail($mailinfo['email']))) {
            $vars['staffId'] = $mailinfo['staffId'];
            $vars['poster'] = Staff::lookup($mailinfo['staffId']);
            $vars['note'] = $body;

            if ($object instanceof Threadable)
                return $object->postThreadEntry('N', $vars);
            elseif ($this instanceof ObjectThread)
                return $this->addNote($vars, $errors);
            else
                throw new Exception('Cannot continue discussion with abstract thread');
        }
        elseif (Email::getIdByEmail($mailinfo['email'])) {
            // Don't process the email -- it came FROM this system
            return true;
        }
        // Support the mail parsing system declaring a thread-type
        elseif (isset($mailinfo['thread-type'])) {
            switch ($mailinfo['thread-type']) {
            case 'N':
                $vars['note'] = $body;
                $vars['poster'] = $poster;
                if ($object instanceof Threadable)
                    return $object->postThreadEntry('N', $vars);
                elseif ($this instanceof ObjectThread)
                    return $this->addNote($vars, $errors);
                else
                    throw new Exception('Cannot continue discussion with abstract thread');
            }
        }
        // TODO: Consider security constraints
        else {
            //XXX: Are we potentially leaking the email address to
            // collaborators?
            // Try not to destroy the format of the body
            $header = sprintf("Received From: %s <%s>\n\n", $mailinfo['name'],
                $mailinfo['email']);
            if ($body instanceof HtmlThreadBody)
                $header = nl2br(Format::htmlchars($header));
            // Add the banner to the top of the message
            if ($body instanceof ThreadBody)
                $body->prepend($header);
            $vars['userId'] = 0; //Unknown user! //XXX: Assume ticket owner?
            $vars['origin'] = 'Email';
            if ($object instanceof Threadable)
                return $object->postThreadEntry('M', $vars);
            elseif ($this instanceof ObjectThread)
                return $this->addMessage($vars, $errors);
            else
                throw new Exception('Cannot continue discussion with abstract thread');
        }
        // 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('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],
                        $matches))
                continue;

            // 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[0]));
        }

        // Add the message id if it is embedded in the body
        $match = array();
        if (preg_match('`(?:data-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['loopback'] && 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;
            }
        }

        return null;
    }

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
            return false;

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();
Peter Rotich's avatar
Peter Rotich committed
    static function create($vars) {
        $inst = parent::create($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'),
        '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 PERM_EDIT     = 'thread.edit';

    static protected $perms = array(
        self::PERM_EDIT => array(
            'title' => /* @trans */ 'Edit Thread',
            'desc'  => /* @trans */ 'Ability to edit thread items of other agents',
        ),
    );

    function postEmail($mailinfo) {
        if (!($thread = $this->getThread()))
            // Kind of hard to continue a discussion without a thread ...
            return false;

        elseif ($this->getEmailMessageId() == $mailinfo['mid'])
            // Reporting success so the email can be moved or deleted.
            return true;

        return $thread->postEmail($mailinfo);
    }

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

    function getPid() {
        return $this->get('pid', 0);
    function getParent() {
        return $this->parent;
        return ThreadEntryBody::fromFormattedText($this->body, $this->format);
    function setBody($body) {
        global $cfg;

Peter Rotich's avatar
Peter Rotich committed
        if (!$body instanceof ThreadEntryBody) {
            if ($cfg->isHtmlThreadEnabled())
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;
    function getName() {
        if ($this->staff_id)
            return $this->staff->getName();
        if ($this->user_id)
            return $this->user->getName();
Peter Rotich's avatar
Peter Rotich committed
    function getEmailHeader() {
        if ($this->email_info)
            return $this->email_info->headers;
    function isAutoReply() {
        if (!isset($this->is_autoreply))
            $this->is_autoreply = $this->getEmailHeaderArray()
                ?  TicketFilter::isAutoReply($this->getEmailHeaderArray()) : false;
        return $this->is_autoreply;
    function isBounce() {
        if (!isset($this->is_bounce))
            $this->is_bounce = $this->getEmailHeaderArray()
                ? TicketFilter::isBounce($this->getEmailHeaderArray()) : false;
        return $this->is_bounce;
    function isBounceOrAutoReply() {
        return ($this->isAutoReply() || $this->isBounce());
    function hasFlag($flag) {
        return ($this->get('flags', 0) & $flag) != 0;
    }
    function clearFlag($flag) {
        return $this->set('flags', $this->get('flags') & ~$flag);
    }
    function setFlag($flag) {
        return $this->set('flags', $this->get('flags') | $flag);
    }

Peter Rotich's avatar
Peter Rotich committed
    //Web uploads - caller is expected to format, validate and set any errors.
    function uploadFiles($files) {

        if(!$files || !is_array($files))
            return false;

        $uploaded=array();
        foreach($files as $file) {
            if($file['error'] && $file['error']==UPLOAD_ERR_NO_FILE)
                continue;

            if(!$file['error']
                    && ($F=AttachmentFile::upload($file))
                    && $this->saveAttachment($F))
                $uploaded[]= $F->getId();
Peter Rotich's avatar
Peter Rotich committed
            else {
                if(!$file['error'])
                    $error = sprintf(__('Unable to upload file - %s'),$file['name']);
Peter Rotich's avatar
Peter Rotich committed
                elseif(is_numeric($file['error']))
                    $error ='Error #'.$file['error']; //TODO: Transplate to string.
                else
                    $error = $file['error'];
                /*
                 Log the error as an internal note.
                 XXX: We're doing it here because it will eventually become a thread post comment (hint: comments coming!)
                 XXX: logNote must watch for possible loops
               */
                $this->getThread()->getObject()->logNote(__('File Upload Error'), $error, 'SYSTEM', false);
    function importAttachments(&$attachments) {
Peter Rotich's avatar
Peter Rotich committed

        if(!$attachments || !is_array($attachments))
            return null;

        $files = array();
        foreach($attachments as &$attachment)
Peter Rotich's avatar
Peter Rotich committed
            if(($id=$this->importAttachment($attachment)))
                $files[] = $id;

        return $files;
    }

    /* Emailed & API attachments handler */
    function importAttachment(&$attachment) {
Peter Rotich's avatar
Peter Rotich committed

        if(!$attachment || !is_array($attachment))
            return null;

        $A=null;
        if ($attachment['error'] || !($A=$this->saveAttachment($attachment))) {
Peter Rotich's avatar
Peter Rotich committed
            $error = $attachment['error'];
            if(!$error)
                $error = sprintf(_S('Unable to import attachment - %s'),
                        $attachment['name']);
Peter Rotich's avatar
Peter Rotich committed
            //FIXME: logComment here
            $this->getThread()->getObject()->logNote(
                    _S('File Import Error'), $error, _S('SYSTEM'), false);
Peter Rotich's avatar
Peter Rotich committed
    }

   /*
    Save attachment to the DB.
    @file is a mixed var - can be ID or file hashtable.
    */
    function saveAttachment(&$file) {
        $inline = is_array($file) && @$file['inline'];

        if (is_numeric($file))
            $fileId = $file;
        elseif ($file instanceof AttachmentFile)
            $fileId = $file->getId();
        elseif ($F = AttachmentFile::create($file))
            $fileId = $F->getId();
        elseif (is_array($file) && isset($file['id']))
            $fileId = $file['id'];
        else
            return false;

        $att = Attachment::create(array(
            'type' => 'H',
            'object_id' => $this->getId(),
            'file_id' => $fileId,
            'inline' => $inline ? 1 : 0,
        ));
        if (!$att->save())
            return false;
        return $att;
Peter Rotich's avatar
Peter Rotich committed
    }

    function saveAttachments($files) {
Peter Rotich's avatar
Peter Rotich committed
        foreach ($files as $file)
           if (($A = $this->saveAttachment($file)))
               $attachments[] = $A;
Peter Rotich's avatar
Peter Rotich committed
    }

    function getAttachments() {
    function getAttachmentUrls() {
        foreach ($this->attachments as $att) {
            $json[$att->file->getKey()] = array(
                'download_url' => $att->file->getDownloadUrl(),
                'filename' => $att->file->name,
    function getAttachmentsLinks($file='attachment.php', $target='_blank', $separator=' ') {
        // TODO: Move this to the respective UI templates
Peter Rotich's avatar
Peter Rotich committed

        $str='';
        foreach ($this->attachments as $att ) {
            if ($att->inline) continue;
Peter Rotich's avatar
Peter Rotich committed
            $size = '';
            if ($att->file->size)
                $size=sprintf('<em>(%s)</em>', Format::file_size($att->file->size));
            $str .= sprintf(
                '<a class="Icon file no-pjax" href="%s" target="%s">%s</a>%s&nbsp;%s',
                $att->file->getDownloadUrl(), $target,
                Format::htmlchars($att->file->name), $size, $separator);
Peter Rotich's avatar
Peter Rotich committed
        }

        return $str;
    }

    /* save email info
     * TODO: Refactor it to include outgoing emails on responses.
     */

    function saveEmailInfo($vars) {

        // Don't save empty message ID
        if (!$vars || !$vars['mid'])
Peter Rotich's avatar
Peter Rotich committed
            return 0;

        $this->ht['email_mid'] = $vars['mid'];
        $header = false;
        if (isset($vars['header']))
            $header = $vars['header'];
        self::logEmailHeaders($this->getId(), $vars['mid'], $header);
    /* static */
    function logEmailHeaders($id, $mid, $header=false) {
Peter Rotich's avatar
Peter Rotich committed

        if (!$id || !$mid)
            return false;

        $this->email_info = ThreadEntryEmailInfo::create(array(
            'thread_entry_id' => $id,
            'mid' => $mid,
        ));

            $this->email_info->headers = trim($header);
Peter Rotich's avatar
Peter Rotich committed

        return $this->email_info->save();
    function __toString() {
        return (string) $this->getBody();
    // TemplateVariable interface
        return (string) $this->getBody()->display('email');
    function getVar($tag) {
        if ($tag && is_callable(array($this, 'get'.ucfirst($tag))))
            return call_user_func(array($this, 'get'.ucfirst($tag)));

        switch(strtolower($tag)) {