Newer
Older
<?php
/*********************************************************************
class.thread.php
XXX: Please DO NOT add any ticket related logic! use ticket class.
Peter Rotich <peter@osticket.com>
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',
),
'entries' => array(
'reverse' => 'ThreadEntry.thread',
),
),
);
var $_object;
return $this->id;
return $this->object_id;
return $this->object_type;
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();
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;
function render($type=false) {
$entries = $this->getEntries();
if ($type && is_array($type))
$entries->filter(array('type__in' => $type));
include STAFFINC_DIR . 'templates/thread-entries.tmpl.php';
}
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/**
* 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;
}
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
// 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 <%s> 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['message'] = $body;
$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;
}
$deleted = Attachment::objects()->filter(array(
'thread_entry__thread' => $this,
))->delete();
if ($deleted)
function removeCollaborators() {
return Collaborator::objects()
->filter(array('thread_id'=>$this->getId()))
->delete();
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
/**
* 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;
}
if (!parent::delete())
ThreadEntryEmailInfo::objects()
->filter(array('thread_entry__thread' => $this))
->update(array('headers' => null));
$this->removeCollaborators();
$this->entries->delete();
$inst = parent::create($vars);
$inst->created = SqlFunction::NOW();
return $inst;
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,
),
'attachments' => array(
'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'),
'null' => true,
),
),
);
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';
var $_headers;
var $_thread;
var $_attachments;
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() {
function getType() {
return $this->type;
}
function getSource() {
return $this->source;
}
function getPoster() {
return $this->poster;
}
function getTitle() {
return $this->title;
}
function getBody() {
return ThreadEntryBody::fromFormattedText($this->body, $this->format);
function setBody($body) {
global $cfg;
if ($cfg->isHtmlThreadEnabled())
$this->format = $body->getType();
$this->body = (string) $body;
return $this->save();
function getMessage() {
return $this->getBody();
}
function getCreateDate() {
return $this->created;
}
function getUpdateDate() {
return $this->updated;
}
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;
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
/**
* 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'];
}
return $this->thread_id;
if (!isset($this->_thread) && $this->thread_id)
// TODO: Consider typing the thread based on its type field
$this->_thread = ObjectThread::lookup($this->getThreadId());
return $this->_thread;
}
function getStaffId() {
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 getUser() {
return $this->user;
}
function getName() {
if ($this->staff_id)
return $this->staff->getName();
if ($this->user_id)
return $this->user->getName();
return $this->poster;
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);
}
//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))
$error = sprintf(__('Unable to upload file - %s'),$file['name']);
elseif(is_numeric($file['error']))
$error ='Error #'.$file['error']; //TODO: Transplate to string.
else
$error = $file['error'];
/*
Log the error as an internal note.
XXX: We're doing it here because it will eventually become a thread post comment (hint: comments coming!)
XXX: logNote must watch for possible loops
*/
$this->getThread()->getObject()->logNote(__('File Upload Error'), $error, 'SYSTEM', false);
function importAttachments(&$attachments) {
if(!$attachments || !is_array($attachments))
return null;
$files = array();
foreach($attachments as &$attachment)
if(($id=$this->importAttachment($attachment)))
$files[] = $id;
return $files;
}
/* Emailed & API attachments handler */
function importAttachment(&$attachment) {
if(!$attachment || !is_array($attachment))
return null;
$A=null;
if ($attachment['error'] || !($A=$this->saveAttachment($attachment))) {
$error = sprintf(_S('Unable to import attachment - %s'),
$attachment['name']);
$this->getThread()->getObject()->logNote(
_S('File Import Error'), $error, _S('SYSTEM'), false);
return $A;
}
/*
Save attachment to the DB.
@file is a mixed var - can be ID or file hashtable.
*/
$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;
$attachments = array();
if (($A = $this->saveAttachment($file)))
$attachments[] = $A;
return $attachments;
return $this->attachments;
foreach ($this->attachments as $att) {
$json[$att->file->getKey()] = array(
'download_url' => $att->file->getDownloadUrl(),
'filename' => $att->file->name,
return $json;
}
function getAttachmentsLinks($file='attachment.php', $target='_blank', $separator=' ') {
// TODO: Move this to the respective UI templates
foreach ($this->attachments as $att ) {
if ($att->inline) continue;
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 %s',
$att->file->getDownloadUrl(), $target,
Format::htmlchars($att->file->name), $size, $separator);
}
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'])
$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) {
$this->email_info = ThreadEntryEmailInfo::create(array(
'thread_entry_id' => $id,
'mid' => $mid,
));
$this->email_info->headers = trim($header);
return $this->email_info->save();
/* variables */
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)) {
case 'create_date':
return new FormattedDate($this->getCreateDate());
case 'update_date':
return new FormattedDate($this->getUpdateDate());
case 'files':
throw new OOBContent(OOBContent::FILES, $this->attachments->all());
}
return false;
}
static function getVarScope() {
return array(
'files' => __('Attached files'),
'body' => __('Message body'),
'create_date' => array(
'class' => 'FormattedDate', 'desc' => __('Date created'),
),
'ip_address' => __('IP address of remote user, for web submissions'),
'poster' => __('Submitter of the thread item'),
'class' => 'Staff', 'desc' => __('Agent posting the note or response'),
'title' => __('Subject, if any'),
'class' => 'User', 'desc' => __('User posting the message'),
/**
* Parameters:
* mailinfo (hash<String>) email header information. Must include keys
* - "mid" => Message-Id header of incoming mail
* - "in-reply-to" => Message-Id the email is a direct response to
* - "references" => List of Message-Id's the email is in response
* - "subject" => Find external ticket number in the subject line
*
* seen (by-ref:bool) a flag that will be set if the message-id was
* positively found, indicating that the message-id has been
* previously seen. This is useful if no thread-id is associated
* with the email (if it was rejected for instance).
function lookupByEmailHeaders(&$mailinfo, &$seen=false) {
// Search for messages using the References header, then the
// in-reply-to header
if ($entry = ThreadEntry::objects()
->filter(array('email_info__mid' => $mailinfo['mid']))
->first()
) {
return $entry;
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
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];
}
$thread = null;
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['entryId']
&& ($t = ThreadEntry::lookup($mid_info['entryId']))
&& ($t->thread_id == $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;
}
// Try to determine if it's a reply to a tagged email.
// (Deprecated)
$ref = null;
if (strpos($mid, '+')) {
list($left, $right) = explode('@',$mid);
list($left, $ref) = explode('+', $left);
$mid = "$left@$right";
}
$entries = ThreadEntry::objects()
->filter(array('email_info__mid' => $mid));
foreach ($entries as $t) {
// Capture the first match thread item