Newer
Older
<?php
/*********************************************************************
class.thread.php
Ticket thread
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');
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
//Ticket thread.
class Thread {
var $id; // same as ticket ID.
var $ticket;
function Thread($ticket) {
$this->ticket = $ticket;
$this->id = 0;
$this->load();
}
function load() {
if(!$this->getTicketId())
return null;
$sql='SELECT ticket.ticket_id as id '
.' ,count(DISTINCT attach.attach_id) as attachments '
.' ,count(DISTINCT message.id) as messages '
.' ,count(DISTINCT response.id) as responses '
.' ,count(DISTINCT note.id) as notes '
.' FROM '.TICKET_TABLE.' ticket '
.' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON ('
.'ticket.ticket_id=attach.ticket_id) '
.' LEFT JOIN '.TICKET_THREAD_TABLE.' message ON ('
."ticket.ticket_id=message.ticket_id AND message.thread_type = 'M') "
.' LEFT JOIN '.TICKET_THREAD_TABLE.' response ON ('
."ticket.ticket_id=response.ticket_id AND response.thread_type = 'R') "
.' LEFT JOIN '.TICKET_THREAD_TABLE.' note ON ( '
."ticket.ticket_id=note.ticket_id AND note.thread_type = 'N') "
.' WHERE ticket.ticket_id='.db_input($this->getTicketId())
.' GROUP BY ticket.ticket_id';
if(!($res=db_query($sql)) || !db_num_rows($res))
return false;
$this->ht = db_fetch_array($res);
$this->id = $this->ht['id'];
return true;
}
function getId() {
return $this->id;
}
function getTicketId() {
return $this->getTicket()?$this->getTicket()->getId():0;
}
function getTicket() {
return $this->ticket;
}
function getNumAttachments() {
return $this->ht['attachments'];
}
function getNumMessages() {
return $this->ht['messages'];
}
function getNumResponses() {
return $this->ht['responses'];
}
function getNumNotes() {
return $this->ht['notes'];
}
function getCount() {
return $this->getNumMessages() + $this->getNumResponses();
}
function getMessages() {
return $this->getEntries('M');
}
function getResponses() {
return $this->getEntries('R');
}
function getNotes() {
return $this->getEntries('N');
}
function getEntries($type, $order='ASC') {
if(!$order || !in_array($order, array('DESC','ASC')))
$order='ASC';
Peter Rotich
committed
$sql='SELECT thread.*
, COALESCE(user.name,
IF(staff.staff_id,
CONCAT_WS(" ", staff.firstname, staff.lastname),
NULL)) as name '
.' ,count(DISTINCT attach.attach_id) as attachments '
.' FROM '.TICKET_THREAD_TABLE.' thread '
Peter Rotich
committed
.' LEFT JOIN '.USER_TABLE.' user
ON (thread.user_id=user.id) '
.' LEFT JOIN '.STAFF_TABLE.' staff
ON (thread.staff_id=staff.staff_id) '
.' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach
ON (thread.ticket_id=attach.ticket_id
.' WHERE thread.ticket_id='.db_input($this->getTicketId());
if($type && is_array($type))
$sql.=' AND thread.thread_type IN('.implode(',', db_input($type)).')';
elseif($type)
$sql.=' AND thread.thread_type='.db_input($type);
$sql.=' GROUP BY thread.id '
.' ORDER BY thread.created '.$order;
$entries = array();
if(($res=db_query($sql)) && db_num_rows($res)) {
while($rec=db_fetch_array($res)) {
$rec['body'] = ThreadBody::fromFormattedText($rec['body'], $rec['format']);
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
return $entries;
}
function getEntry($id) {
return ThreadEntry::lookup($id, $this->getTicketId());
}
function addNote($vars, &$errors) {
//Add ticket Id.
$vars['ticketId'] = $this->getTicketId();
return Note::create($vars, $errors);
}
function addMessage($vars, &$errors) {
$vars['ticketId'] = $this->getTicketId();
$vars['staffId'] = 0;
return Message::create($vars, $errors);
}
function addResponse($vars, &$errors) {
$vars['ticketId'] = $this->getTicketId();
return Response::create($vars, $errors);
}
function deleteAttachments() {
$deleted=0;
// Clear reference table
$res=db_query('DELETE FROM '.TICKET_ATTACHMENT_TABLE.' WHERE ticket_id='.db_input($this->getTicketId()));
if ($res && db_affected_rows())
$deleted = AttachmentFile::deleteOrphans();
return $deleted;
}
function delete() {
$sql = 'UPDATE '.TICKET_EMAIL_INFO_TABLE.' mid
INNER JOIN '.TICKET_THREAD_TABLE.' thread ON (thread.id = mid.thread_id)
SET mid.headers = null WHERE thread.ticket_id = '
.db_input($this->getTicketId());
$res=db_query('DELETE FROM '.TICKET_THREAD_TABLE.' WHERE ticket_id='.db_input($this->getTicketId()));
if(!$res || !db_affected_rows())
return false;
$this->deleteAttachments();
$this->removeCollaborators();
function removeCollaborators() {
$sql='DELETE FROM '.TICKET_COLLABORATOR_TABLE
.' WHERE ticket_id='.db_input($this->getTicketId());
return (db_query($sql) && db_affected_rows());
}
/* static */
function lookup($ticket) {
return ($ticket
&& is_object($ticket)
&& ($thread = new Thread($ticket))
&& $thread->getId()
)?$thread:null;
}
function getVar($name) {
switch ($name) {
case 'original':
return Message::firstByTicketId($this->ticket->getId())
->getBody();
break;
case 'last_message':
case 'lastmessage':
return $this->ticket->getLastMessage()->getBody();
break;
}
}
Class ThreadEntry {
var $id;
var $ht;
var $staff;
var $ticket;
function ThreadEntry($id, $type='', $ticketId=0) {
$this->load($id, $type, $ticketId);
}
function load($id=0, $type='', $ticketId=0) {
if(!$id && !($id=$this->getId()))
return false;
.' ,count(DISTINCT attach.attach_id) as attachments '
.' FROM '.TICKET_THREAD_TABLE.' thread '
.' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach
ON (thread.ticket_id=attach.ticket_id
.' WHERE thread.id='.db_input($id);
if($type)
$sql.=' AND thread.thread_type='.db_input($type);
if($ticketId)
$sql.=' AND thread.ticket_id='.db_input($ticketId);
$sql.=' GROUP BY thread.id ';
if(!($res=db_query($sql)) || !db_num_rows($res))
return false;
$this->ht = db_fetch_array($res);
$this->id = $this->ht['id'];
$this->staff = $this->ticket = null;
return true;
}
function reload() {
return $this->load();
}
function getId() {
return $this->id;
}
function getPid() {
return $this->ht['pid'];
}
function getParent() {
if ($this->getPid())
return ThreadEntry::lookup($this->getPid());
}
function getType() {
return $this->ht['thread_type'];
}
function getSource() {
return $this->ht['source'];
}
function getPoster() {
return $this->ht['poster'];
}
function getTitle() {
return $this->ht['title'];
}
function getBody() {
return ThreadBody::fromFormattedText($this->ht['body'], $this->ht['format']);
function setBody($body) {
global $cfg;
if (!$body instanceof ThreadBody) {
if ($cfg->isHtmlThreadEnabled())
$body = new HtmlThreadBody($body);
else
$body = new TextThreadBody($body);
}
$sql='UPDATE '.TICKET_THREAD_TABLE.' SET updated=NOW()'
.',format='.db_input($body->getType())
.',body='.db_input((string) $body)
.' WHERE id='.db_input($this->getId());
return db_query($sql) && db_affected_rows();
}
function getCreateDate() {
return $this->ht['created'];
}
function getUpdateDate() {
return $this->ht['updated'];
}
function getNumAttachments() {
return $this->ht['attachments'];
}
function getTicketId() {
return $this->ht['ticket_id'];
}
function _deferEmailInfo() {
if (isset($this->ht['email_mid']))
return;
// Don't do this more than once
$this->ht['email_mid'] = false;
$sql = 'SELECT email_mid, headers FROM '.TICKET_EMAIL_INFO_TABLE
.' WHERE thread_id='.db_input($this->getId());
if (!($res = db_query($sql)))
return;
list($this->ht['email_mid'], $this->ht['headers']) = db_fetch_row($res);
}
function getEmailMessageId() {
$this->_deferEmailInfo();
return $this->ht['email_mid'];
}
function getEmailHeaderArray() {
require_once(INCLUDE_DIR.'class.mailparse.php');
$this->_deferEmailInfo();
if (!isset($this->ht['@headers']))
$this->ht['@headers'] = Mail_Parse::splitHeaders($this->ht['headers']);
return $this->ht['@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;
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
/**
* 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;
}
function getUIDFromEmailReference($ref) {
$info = unpack('Vtid/Vuid',
Base32::decode(strtolower(substr($ref, -13))));
if ($info && $info['tid'] == $this->getId())
return $info['uid'];
}
function getTicket() {
if(!$this->ticket && $this->getTicketId())
$this->ticket = Ticket::lookup($this->getTicketId());
return $this->ticket;
}
function getStaffId() {
return $this->ht['staff_id'];
}
function getStaff() {
if(!$this->staff && $this->getStaffId())
$this->staff = Staff::lookup($this->getStaffId());
return $this->staff;
}
function getUserId() {
return $this->ht['user_id'];
}
function getUser() {
if (!isset($this->user)) {
if (!($ticket = $this->getTicket()))
return null;
if ($ticket->getOwnerId() == $this->getUserId())
$this->user = new TicketOwner(
User::lookup($this->getUserId()), $ticket);
else
$this->user = Collaborator::lookup(array(
'userId'=>$this->getUserId(), 'ticketId'=>$this->getTicketId()));
}
return $this->user;
}
$this->_deferEmailInfo();
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());
}
//Web uploads - caller is expected to format, validate and set any errors.
function uploadFiles($files) {
if(!$files || !is_array($files))
return false;
$uploaded=array();
foreach($files as $file) {
if($file['error'] && $file['error']==UPLOAD_ERR_NO_FILE)
continue;
if(!$file['error']
&& ($id=AttachmentFile::upload($file))
&& $this->saveAttachment($id))
$uploaded[]=$id;
else {
if(!$file['error'])
$error = 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->getTicket()->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;
$id=0;
if ($attachment['error'] || !($id=$this->saveAttachment($attachment))) {
$error = $attachment['error'];
if(!$error)
$error = sprintf(_S('Unable to import attachment - %s'),$attachment['name']);
$this->getTicket()->logNote(_S('File Import Error'), $error,
_S('SYSTEM'), false);
}
return $id;
}
/*
Save attachment to the DB.
@file is a mixed var - can be ID or file hashtable.
*/
if (is_numeric($file))
$fileId = $file;
elseif (is_array($file) && isset($file['id']))
$fileId = $file['id'];
elseif (!($fileId = AttachmentFile::save($file)))
$inline = is_array($file) && @$file['inline'];
// TODO: Add a unique index to TICKET_ATTACHMENT_TABLE (file_id,
// ref_id), and remove this block
if ($id = db_result(db_query('SELECT attach_id FROM '.TICKET_ATTACHMENT_TABLE
.' WHERE file_id='.db_input($fileId).' AND ref_id='
.db_input($this->getId()))))
return $id;
$sql ='INSERT IGNORE INTO '.TICKET_ATTACHMENT_TABLE.' SET created=NOW() '
.' ,file_id='.db_input($fileId)
.' ,ticket_id='.db_input($this->getTicketId())
.' ,inline='.db_input($inline ? 1 : 0)
return (db_query($sql) && ($id=db_insert_id()))?$id:0;
}
function saveAttachments($files) {
$ids=array();
foreach($files as $file)
if(($id=$this->saveAttachment($file)))
$ids[] = $id;
return $ids;
}
function getAttachments() {
if($this->attachments)
return $this->attachments;
//XXX: inner join the file table instead?
$sql='SELECT a.attach_id, f.id as file_id, f.size, lower(f.`key`) as file_hash, f.`signature` as file_sig, f.name, a.inline '
.' FROM '.FILE_TABLE.' f '
.' INNER JOIN '.TICKET_ATTACHMENT_TABLE.' a ON(f.id=a.file_id) '
.' WHERE a.ticket_id='.db_input($this->getTicketId())
$this->attachments = array();
if(($res=db_query($sql)) && db_num_rows($res)) {
while($rec=db_fetch_array($res)) {
$rec['download_url'] = AttachmentFile::generateDownloadUrl(
$rec['file_id'], $rec['file_hash'], $rec['file_sig']);
$json = array();
foreach ($this->getAttachments() as $att) {
$json[$att['file_hash']] = array(
'download_url' => $att['download_url'],
'filename' => $att['name'],
);
}
return $json;
}
function getAttachmentsLinks($file='attachment.php', $target='', $separator=' ') {
$str='';
foreach($this->getAttachments() as $attachment ) {
if ($attachment['inline'])
continue;
$size = '';
if($attachment['size'])
$size=sprintf('<em>(%s)</em>', Format::file_size($attachment['size']));
$str.=sprintf('<a class="Icon file no-pjax" href="%s" target="%s">%s</a>%s %s',
$attachment['download_url'], $target, Format::htmlchars($attachment['name']), $size, $separator);
/**
* 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) {
// +==================+===================+=============+
// | 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 (!$ticket = $this->getTicket())
// Kind of hard to continue a discussion without a ticket ...
return false;
// Make sure the email is NOT already fetched... (undeleted emails)
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 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
$msgId_info = Mailer::decodeMessageId($mailinfo['mid']);
if ($msgId_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 <%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);
$vars = array(
'mid' => $mailinfo['mid'],
'header' => $mailinfo['header'],
'ticketId' => $ticket->getId(),
'poster' => $mailinfo['name'],
'origin' => 'Email',
'source' => 'Email',
'ip' => '',
'reply_to' => $this,
'recipients' => $mailinfo['recipients'],
'to-email-id' => $mailinfo['to-email-id'],
if (isset($mailinfo['attachments']))
$vars['attachments'] = $mailinfo['attachments'];
$body = $mailinfo['message'];
// 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']
|| strcasecmp($mailinfo['email'], $ticket->getEmail()) == 0) {
$vars['message'] = $body;
$vars['userId'] = $mailinfo['userId'] ? $mailinfo['userId'] : $ticket->getUserId();
return $ticket->postMessage($vars, 'Email');
}
// XXX: Consider collaborator role
elseif ($mailinfo['staffId']
|| ($mailinfo['staffId'] = Staff::getIdByEmail($mailinfo['email']))) {
$vars['staffId'] = $mailinfo['staffId'];
$poster = Staff::lookup($mailinfo['staffId']);
$vars['note'] = $body;
return $ticket->postNote($vars, $errors, $poster);
}
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;
$poster = $mailinfo['email'];
return $ticket->postNote($vars, $errors, $poster);
}
}
// TODO: Consider security constraints
else {
//XXX: Are we potentially leaking the email address to
// collaborators?
$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?
return $ticket->postMessage($vars, 'Email');
}
// Currently impossible, but indicate that this thread object could
// not append the incoming email.
return false;
}
/* Returns file names with id as key */
function getFiles() {
$files = array();
foreach($this->getAttachments() as $attachment)
$files[$attachment['file_id']] = $attachment['name'];
return $files;
}
/* save email info
* TODO: Refactor it to include outgoing emails on responses.
*/
function saveEmailInfo($vars) {
// 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) {
$sql='INSERT INTO '.TICKET_EMAIL_INFO_TABLE
.' SET thread_id='.db_input($id)
.', email_mid='.db_input($mid); //TODO: change it to message_id.
if ($header)
$sql .= ', headers='.db_input($header);
return db_query($sql)?db_insert_id():0;
}
/* variables */
function __toString() {
return (string) $this->getBody();
return (string) $this->getBody()->display('email');
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
function getVar($tag) {
global $cfg;
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 Format::date(
$cfg->getDateTimeFormat(),
Misc::db2gmtime($this->getCreateDate()),
$cfg->getTZOffset(),
$cfg->observeDaylightSaving());
break;
case 'update_date':
return Format::date(
$cfg->getDateTimeFormat(),
Misc::db2gmtime($this->getUpdateDate()),
$cfg->getTZOffset(),
$cfg->observeDaylightSaving());
break;
}
return false;
}
/* static calls */
return ($id
&& is_numeric($id)
&& ($e = new ThreadEntry($id, $type, $tid))
/**
* 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
$search = 'SELECT thread_id, email_mid FROM '.TICKET_EMAIL_INFO_TABLE
. ' WHERE email_mid=%s ORDER BY thread_id DESC';
if (list($id, $mid) = db_fetch_row(db_query(
sprintf($search, db_input($mailinfo['mid']))))) {
$seen = true;
return ThreadEntry::lookup($id);
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
foreach (array_reverse($matches[0]) as $mid) {
//Try to determine if it's a reply to a tagged email.
$ref = null;
if (strpos($mid, '+')) {
list($left, $right) = explode('@',$mid);
list($left, $ref) = explode('+', $left);
$mid = "$left@$right";
}
$res = db_query(sprintf($search, db_input($mid)));
while (list($id) = db_fetch_row($res)) {
if (!($t = ThreadEntry::lookup($id)))
continue;
// Capture the first match thread item
if (!$thread)
$thread = $t;
// We found a match - see if we can ID the user.
// XXX: Check access of ref is enough?
if ($ref && ($uid = $t->getUIDFromEmailReference($ref))) {
if ($ref[0] =='s') //staff
$mailinfo['staffId'] = $uid;
else // user or collaborator.
$mailinfo['userId'] = $uid;
// Best possible case — found the thread and the
// user
return $t;
}
// 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 = ThreadEntry::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;
}
// Second best case — found a thread but couldn't identify the
// user from the header. Return the first thread entry matched
if ($thread)
return $thread;
}
// Search for ticket by the [#123456] in the subject line
// This is the last resort - emails must match to avoid message
// injection by third-party.
$subject = $mailinfo['subject'];
$match = array();
if ($subject
&& $mailinfo['email']
// Required `#` followed by one or more of
// punctuation (-) then letters, numbers, and symbols
// (Try not to match closing punctuation (`]`) in [#12345])
&& preg_match("/#((\p{P}*[^\p{C}\p{Z}\p{P}]+)+)/u", $subject, $match)
//Lookup by ticket number
&& ($ticket = Ticket::lookupByNumber($match[1]))
//Lookup the user using the email address
&& ($user = User::lookup(array('emails__address' => $mailinfo['email'])))) {
//We have a valid ticket and user
if ($ticket->getUserId() == $user->getId() //owner
|| ($c = Collaborator::lookup( // check if collaborator
array('userId' => $user->getId(),
'ticketId' => $ticket->getId())))) {
$mailinfo['userId'] = $user->getId();
return $ticket->getLastMessage();
}
}
// Search for the message-id token in the body
if (preg_match('`(?:data-mid="|Ref-Mid: )([^"\s]*)(?:$|")`',
$mailinfo['message'], $match))
if ($thread = ThreadEntry::lookupByRefMessageId($match[1],
$mailinfo['email']))
return $thread;