diff --git a/include/api.tickets.php b/include/api.tickets.php index d80b1582889f7bf3f944eea9808a3f675f6008f2..d6b04c98ccf126bfdaa7e952bacaf1aea3aa480c 100644 --- a/include/api.tickets.php +++ b/include/api.tickets.php @@ -150,14 +150,25 @@ class TicketApiController extends ApiController { if (!$data) $data = $this->getEmailRequest(); - if (($thread = ThreadEntry::lookupByEmailHeaders($data)) - && ($t=$thread->getTicket()) - && ($data['staffId'] - || !$t->isClosed() - || $t->isReopenable()) - && $thread->postEmail($data)) { - return $thread->getTicket(); + if (($entry = ThreadEntry::lookupByEmailHeaders($data, $seen)) + && ($thread = $entry->getThread()) + && ($t = $thread->getObject()) + && (!$t instanceof Ticket || ( + $vars['staffId'] + || !$t->isClosed() + || $t->isReopenable() + )) + && ($message = $entry->postEmail($data)) + ) { + if ($message instanceof ThreadEntry) { + return $message->getThread()->getObject(); + } + else if ($message) { + // Email has been processed previously + return $t; + } } + return $this->createTicket($data); } diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php index 996192e9bca1798d64c610d2457b71581e680b18..04475abc01cc10a1135c304cdc8627192e05a0dd 100644 --- a/include/class.mailfetch.php +++ b/include/class.mailfetch.php @@ -721,16 +721,21 @@ class MailFetcher { Signal::send('mail.processed', $this, $vars); $seen = false; - if (($thread = ThreadEntry::lookupByEmailHeaders($vars, $seen)) - && ($t=$thread->getTicket()) - && ($vars['staffId'] - || !$t->isClosed() - || $t->isReopenable()) - && ($message = $thread->postEmail($vars))) { + if (($entry = ThreadEntry::lookupByEmailHeaders($vars, $seen)) + && ($thread = $entry->getThread()) + && ($t = $thread->getObject()) + && (!$t instanceof Ticket || ( + $vars['staffId'] + || !$t->isClosed() + || $t->isReopenable() + )) + && ($message = $entry->postEmail($vars)) + ) { if (!$message instanceof ThreadEntry) // Email has been processed previously return $message; - $ticket = $message->getTicket(); + // NOTE: This might not be a "ticket" + $ticket = $message->getThread()->getObject(); } elseif ($seen) { // Already processed, but for some reason (like rejection), no // thread item was created. Ignore the email diff --git a/include/class.thread.php b/include/class.thread.php index d3abcf411210dc6154f38d09a8a68a530838f71d..11a9ce9e5044251a82a56fd0fc533957955c1695 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -155,6 +155,153 @@ class Thread { return ThreadEntry::lookup($id, $this->getId()); } + /** + * 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; + + // 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(); + $vars['origin'] = 'Email'; + + if ($object instanceof Threadable) + return $object->postMessage($vars, $vars['origin']); + elseif ($this instanceof ObjectThread) + $this->addMessage($vars, $errors); + else + throw new Exception('Cannot continue discussion with abstract thread'); + } + // XXX: Consider collaborator role + 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->postNote($vars, $errors); + elseif ($this instanceof ObjectThread) + $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->postNote($vars, $errors); + elseif ($this instanceof ObjectThread) + $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? + $vars['message'] = sprintf("Received From: %s\n\n%s", + $mailinfo['email'], $body); + $vars['userId'] = 0; //Unknown user! //XXX: Assume ticket owner? + $vars['origin'] = 'Email'; + if ($object instanceof Threadable) + return $object->postMessage($vars, $errors); + elseif ($this instanceof ObjectThread) + $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; + } function deleteAttachments() { @@ -283,6 +430,18 @@ class ThreadEntry { return true; } + 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 reload() { return $this->load(); } @@ -405,7 +564,8 @@ class ThreadEntry { function getThread() { if(!$this->thread && $this->getThreadId()) - $this->thread = Thread::lookup($this->getThreadId()); + // TODO: Consider typing the thread based on its type field + $this->thread = ObjectThread::lookup($this->getThreadId()); return $this->thread; } @@ -579,125 +739,6 @@ class ThreadEntry { return $str; } - /** - * 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 (!$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 - @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'], - 'ticketId' => $ticket->getId(), - 'poster' => $mailinfo['name'], - 'origin' => 'Email', - 'source' => 'Email', - 'ip' => '', - 'reply_to' => $this, - 'recipients' => $mailinfo['recipients'], - 'to-email-id' => $mailinfo['to-email-id'], - ); - $errors = array(); - - 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? - $vars['message'] = sprintf("Received From: %s\n\n%s", - $mailinfo['email'], $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() { @@ -737,7 +778,7 @@ class ThreadEntry { .' SET thread_entry_id='.db_input($id) .', mid='.db_input($mid); if ($header) - $sql .= ', headers='.db_input($header); + $sql .= ', headers='.db_input(trim($header)); return db_query($sql) ? db_insert_id() : 0; } @@ -1477,6 +1518,10 @@ class NoteThreadEntry extends ThreadEntry { class ObjectThread extends Thread { private $_entries = array(); + static $types = array( + ObjectModel::OBJECT_TYPE_TASK => 'TaskThread', + ); + function __construct($id) { parent::__construct($id); @@ -1593,10 +1638,15 @@ class ObjectThread extends Thread { } } - static function lookup($criteria) { + static function lookup($criteria, $type=false) { + $class = false; + if ($type && isset(self::$types[$type])) + $class = self::$types[$type]; + if (!class_exists($class)) + $class = get_called_class(); return ($criteria - && ($t= new static($criteria)) + && ($t = new $class($criteria)) && $t->getId() ) ? $t : null; } @@ -1689,4 +1739,12 @@ abstract class ThreadEntryAction { ); } } + +interface Threadable { + /* + function postMessage($vars, $errors); + function postNote($vars, $errors); + function postReply($vars, $errors); + */ +} ?> diff --git a/include/class.ticket.php b/include/class.ticket.php index 0b3c6574107193faf282fccb2fe34a0b32ab026b..9f23ae416f7f5753f05d962820760c5fdaa06fc3 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -217,7 +217,7 @@ TicketCData::$meta['table'] = TABLE_PREFIX . 'ticket__cdata'; class Ticket -implements RestrictedAccess { +implements RestrictedAccess, Threadable { var $id; var $number; @@ -1928,7 +1928,8 @@ implements RestrictedAccess { function postMessage($vars, $origin='', $alerts=true) { global $cfg; - $vars['origin'] = $origin; + if ($origin) + $vars['origin'] = $origin; if(isset($vars['ip'])) $vars['ip_address'] = $vars['ip']; elseif(!$vars['ip_address'] && $_SERVER['REMOTE_ADDR']) @@ -2221,18 +2222,21 @@ implements RestrictedAccess { ); } - function postNote($vars, &$errors, $poster, $alert=true) { + function postNote($vars, &$errors, $poster=false, $alert=true) { global $cfg, $thisstaff; //Who is posting the note - staff or system? $vars['staffId'] = 0; - $vars['poster'] = 'SYSTEM'; if($poster && is_object($poster)) { $vars['staffId'] = $poster->getId(); $vars['poster'] = $poster->getName(); - }elseif($poster) { //string + } + elseif ($poster) { //string $vars['poster'] = $poster; } + elseif (!isset($vars['poster'])) { + $vars['poster'] = 'SYSTEM'; + } if(!($note=$this->getThread()->addNote($vars, $errors))) return null;