diff --git a/include/class.format.php b/include/class.format.php index e203415806b62f4555e3873c306bf7ae060fe33f..3377b4fe4938ac9d1be98c3ad592bab39bdd3647 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -218,7 +218,7 @@ class Format { 'schemes' => 'href: aim, feed, file, ftp, gopher, http, https, irc, mailto, news, nntp, sftp, ssh, telnet; *:file, http, https; src: cid, http, https, data', 'hook_tag' => function($e, $a=0) { return Format::__html_cleanup($e, $a); }, 'elements' => '*+iframe', - 'spec' => 'iframe=-*,height,width,type,src(match="`^(https?:)?//(www\.)?(youtube|dailymotion|vimeo)\.com/`i"),frameborder;', + 'spec' => 'iframe=-*,height,width,type,src(match="`^(https?:)?//(www\.)?(youtube|dailymotion|vimeo)\.com/`i"),frameborder; div=data-mid', ); return Format::html($html, $config); diff --git a/include/class.mailer.php b/include/class.mailer.php index 65e98f500d47223d0881e6b6a91024e05ac5c090..bb4e445d05da25ef350d82cb2c13088f01c22431 100644 --- a/include/class.mailer.php +++ b/include/class.mailer.php @@ -100,8 +100,10 @@ class Mailer { $subject = preg_replace("/(\r\n|\r|\n)/s",'', trim($subject)); /* Message ID - generated for each outgoing email */ - $messageId = sprintf('<%s-%s>', Misc::randCode(16), - ($this->getEmail()?$this->getEmail()->getEmail():'@osTicketMailer')); + $messageId = sprintf('<%s-%s-%s>', + substr(md5('mail'.SECRET_SALT), -9), + Misc::randCode(9), + ($this->getEmail()?$this->getEmail()->getEmail():'@osTicketMailer')); $headers = array ( 'From' => $this->getFromAddress(), @@ -152,10 +154,17 @@ class Mailer { // then assume that it needs html processing to create a valid text // body $isHtml = true; + $mid_token = (isset($options['thread'])) + ? $options['thread']->asMessageId($to) : ''; if (!(isset($options['text']) && $options['text'])) { + if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) + $message = "<div style=\"display:none\" data-mid=\"$mid_token\">$tag</div>" + .$message; // Make sure nothing unsafe has creeped into the message $message = Format::safe_html($message); //XXX?? - $mime->setTXTBody(Format::html2text($message, 90, false)); + $txtbody = rtrim(Format::html2text($message, 90, false)) + . ($mid_token ? "\nRef-Mid: $mid_token\n" : ''); + $mime->setTXTBody($txtbody); } else { $mime->setTXTBody($message); diff --git a/include/class.thread.php b/include/class.thread.php index 147712324f1e46fe4a4ab77ccedb8125dc3b8f9f..29b7a5bdf9ffcfdeb812126c2b065d9c905a3a2a 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -186,13 +186,11 @@ class Thread { function delete() { - /* XXX: Leave this out until TICKET_EMAIL_INFO_TABLE has a primary - * key - $sql = 'DELETE mid.* FROM '.TICKET_EMAIL_INFO_TABLE.' mid + $sql = 'UPDATE '.TICKET_EMAIL_INFO_TABLE.' mid INNER JOIN '.TICKET_THREAD_TABLE.' thread ON (thread.id = mid.thread_id) - WHERE thread.ticket_id = '.db_input($this->getTicketId()); + SET mid.headers = null WHERE thread.ticket_id = ' + .db_input($this->getTicketId()); db_query($sql); - */ $res=db_query('DELETE FROM '.TICKET_THREAD_TABLE.' WHERE ticket_id='.db_input($this->getTicketId())); if(!$res || !db_affected_rows()) @@ -613,6 +611,8 @@ Class ThreadEntry { * - body - (string) email message body (decoded) */ function postEmail($mailinfo) { + global $ost; + // +==================+===================+=============+ // | Orig Thread-Type | Reply Thread-Type | Requires | // +==================+===================+=============+ @@ -631,6 +631,28 @@ Class ThreadEntry { // 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, 'Email loop detected', sprintf( + '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'], @@ -866,9 +888,67 @@ Class ThreadEntry { } } + // Search for the message-id token in the body + if (preg_match('`(?:data-mid="|Ref-Mid: )([^"\s]*)(?:$|")`', + $mailinfo['message'], $match)) + if ($thread = ThreadEntry::lookupByMessageId($match[1])) + return $thread; + return null; } + /** + * Find a thread entry from a message-id created from the + * ::asMessageId() method + */ + function lookupByMessageId($mid, $from) { + $mid = trim($mid, '<>'); + list($ver, $ids, $mails) = explode('$', $mid, 3); + + // Current version is <null> + if ($ver !== '') + return false; + + $ids = @unpack('Vthread', base64_decode($ids)); + if (!$ids || !$ids['thread']) + return false; + + $thread = ThreadEntry::lookup($ids['thread']); + if (!$thread) + return false; + + if (0 === strcasecmp($thread->asMessageId($from, $vers), $mid)) + return $thread; + } + + /** + * Get an email message-id that can be used to represent this thread + * entry. The same message-id can be passed to ::lookupByMessageId() to + * find this thread entry + * + * Formats: + * Initial (version <null>) + * <$:b32(thread-id)$:md5(to-addr.ticket-num.ticket-id)@:md5(url)> + * thread-id - thread-id, little-endian INT, packed + * :b32() - base32 encoded + * to-addr - individual email recipient + * ticket-num - external ticket number + * ticket-id - internal ticket id + * :md5() - last 10 hex chars of MD5 sum + * url - helpdesk URL + */ + function asMessageId($to, $version=false) { + global $ost; + + $domain = md5($ost->getConfig()->getURL()); + $ticket = $this->getTicket(); + return sprintf('$%s$%s@%s', + base64_encode(pack('V', $this->getId())), + substr(md5($to . $ticket->getNumber() . $ticket->getId()), -10), + substr($domain, -10) + ); + } + //new entry ... we're trusting the caller to check validity of the data. function create($vars) { global $cfg; diff --git a/include/class.ticket.php b/include/class.ticket.php index 5baf918db5976dcd60df97e447278d2942abc59c..b26237755f2df3802a407d662e0c6e125fb57e62 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -889,7 +889,8 @@ class Ticket { $options = array( 'inreplyto'=>$message->getEmailMessageId(), - 'references'=>$message->getEmailReferences()); + 'references'=>$message->getEmailReferences(), + 'thread'=>$message); //Send auto response - if enabled. if($autorespond @@ -903,9 +904,6 @@ class Ticket { 'signature' => ($dept && $dept->isPublic())?$dept->getSignature():'') ); - if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) - $msg['body'] = "<p style=\"display:none\">$tag<p>".$msg['body']; - $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body'], null, $options); } @@ -1094,13 +1092,10 @@ class Ticket { 'recipient' => $user, 'signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')); - //Reply separator tag. - if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) - $msg['body'] = "<p style=\"display:none\">$tag<p>".$msg['body']; - $options = array( - 'inreplyto' => $message->getEmailMessageId(), - 'references' => $message->getEmailReferencesForUser($user)); + 'inreplyto'=>$message->getEmailMessageId(), + 'references' => $message->getEmailReferencesForUser($user), + 'thread'=>$message); $email->sendAutoReply($user->getEmail(), $msg['subj'], $msg['body'], null, $options); } @@ -1157,7 +1152,8 @@ class Ticket { $sentlist=array(); $options = array( 'inreplyto'=>$note->getEmailMessageId(), - 'references'=>$note->getEmailReferences()); + 'references'=>$note->getEmailReferences(), + 'thread'=>$note); foreach( $recipients as $k=>$staff) { if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = $this->replaceVars($msg, array('recipient' => $staff)); @@ -1408,7 +1404,8 @@ class Ticket { $sentlist=array(); $options = array( 'inreplyto'=>$note->getEmailMessageId(), - 'references'=>$note->getEmailReferences()); + 'references'=>$note->getEmailReferences(), + 'thread'=>$note); foreach( $recipients as $k=>$staff) { if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = $this->replaceVars($msg, array('recipient' => $staff)); @@ -1595,7 +1592,8 @@ class Ticket { ); $options = array( 'inreplyto' => $message->getEmailMessageId(), - 'references' => $message->getEmailReferences()); + 'references' => $message->getEmailReferences(), + 'thread'=>$message); //If enabled...send alert to staff (New Message Alert) if($cfg->alertONNewMessage() && ($email = $cfg->getAlertEmail()) @@ -1670,13 +1668,11 @@ class Ticket { $msg = $this->replaceVars($msg->asArray(), array('response' => $response, 'signature' => $signature)); - if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) - $msg['body'] = "<p style=\"display:none\">$tag<p>".$msg['body']; - $attachments =($cfg->emailAttachments() && $files)?$response->getAttachments():array(); $options = array( 'inreplyto'=>$response->getEmailMessageId(), - 'references'=>$response->getEmailReferences()); + 'references'=>$response->getEmailReferences(), + 'thread'=>$response); $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body'], $attachments, $options); } @@ -1727,7 +1723,8 @@ class Ticket { 'poster' => $thisstaff); $options = array( 'inreplyto' => $response->getEmailMessageId(), - 'references' => $response->getEmailReferences()); + 'references' => $response->getEmailReferences(), + 'thread'=>$response); if(($email=$dept->getEmail()) && ($tpl = $dept->getTemplate()) @@ -1736,8 +1733,6 @@ class Ticket { $msg = $this->replaceVars($msg->asArray(), $variables + array('recipient' => $this->getOwner())); - if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) - $msg['body'] = "<p style=\"display:none\">$tag<p>".$msg['body']; $attachments = $cfg->emailAttachments()?$response->getAttachments():array(); $email->send($this->getEmail(), $msg['subj'], $msg['body'], $attachments, $options); @@ -1852,7 +1847,8 @@ class Ticket { $options = array( 'inreplyto'=>$note->getEmailMessageId(), - 'references'=>$note->getEmailReferences()); + 'references'=>$note->getEmailReferences(), + 'thread'=>$note); $sentlist=array(); foreach( $recipients as $k=>$staff) { if(!is_object($staff) @@ -2512,13 +2508,13 @@ class Ticket { ) ); - if($cfg->stripQuotedReply() && ($tag=trim($cfg->getReplySeparator()))) - $msg['body'] = "<p style=\"display:none\">$tag<p>".$msg['body']; - $references = $ticket->getLastMessage()->getEmailMessageId(); if (isset($response)) $references = array($response->getEmailMessageId(), $references); - $options = array('references' => $references); + $options = array( + 'references' => $references, + 'thread'=>$this->getLastMessage() + ); $email->send($ticket->getEmail(), $msg['subj'], $msg['body'], $attachments, $options); }