Skip to content

Commit

Permalink
Remove requirement of ticket id in subject line
Browse files Browse the repository at this point in the history
This patch affords an administrator the ability to remove the
[#%{ticket.number}] from the email template subject line for the new ticket
autoresponse and the new message autoresponse. Previously, the ticket number
with a prefixed hash in brackets was used to identify which ticket thread an
email was in reference to.

With this patch, the email message-id (which was already kept on file) is
sent in the MIME "References" header. When a user responds to and
autoresponse email, the "References" will include this message-id in the
return email. The ticket thread is then matched up with the email based on
the message-id rather than the subject line.

Ticket numbers are still supported in the subject line, in the event that
non-compliant email clients do not properly include the References header.
  • Loading branch information
Jared Hancock committed Sep 3, 2013
1 parent acd4f06 commit 29b3714
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 42 deletions.
4 changes: 4 additions & 0 deletions include/api.tickets.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ function processEmail() {
return $ticket;
}

if (($thread = ThreadEntry::lookupByEmailHeaders($data))
&& $thread->postEmail($data)) {
return true;
}
return $this->createTicket($data);
}

Expand Down
5 changes: 0 additions & 5 deletions include/class.api.php
Original file line number Diff line number Diff line change
Expand Up @@ -423,11 +423,6 @@ function fixup($data) {
if(!$data['emailId'])
$data['emailId'] = $cfg->getDefaultEmailId();

if($data['email'] && preg_match ('[[#][0-9]{1,10}]', $data['subject'], $matches)) {
if(($tid=Ticket::getIdByExtId(trim(preg_replace('/[^0-9]/', '', $matches[0])), $data['email'])))
$data['ticketId'] = $tid;
}

if(!$cfg->useEmailPriority())
unset($data['priorityId']);

Expand Down
12 changes: 12 additions & 0 deletions include/class.mailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ function send($to, $subject, $message, $options=null) {
$headers+= array('Precedence' => 'auto_reply');
}

if ($options) {
if (isset($options['replyto']))
$headers += array('In-Reply-To' => $options['replyto']);
if (isset($options['references'])) {
if (is_array($options['references']))
$headers += array('References' =>
implode(' ', $options['references']));
else
$headers += array('References' => $options['references']);
}
}

$mime = new Mail_mime();
$mime->setTXTBody($body);
//XXX: Attachments
Expand Down
24 changes: 9 additions & 15 deletions include/class.mailfetch.php
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ function getHeaderInfo($mid) {
'subject'=>@$headerinfo->subject,
'mid' => trim(@$headerinfo->message_id),
'header' => $this->getHeader($mid),
'in-reply-to' => $headerinfo->in_reply_to,
'references' => $headerinfo->references,
);

if ($replyto = $headerinfo->reply_to) {
Expand Down Expand Up @@ -400,10 +402,6 @@ function createTicket($mid) {
return true; //Report success (moved or delete)
}

//Make sure the email is NOT already fetched... (undeleted emails)
if($mailinfo['mid'] && ($id=Ticket::getIdByMessageId($mailinfo['mid'], $mailinfo['email'])))
return true; //Reporting success so the email can be moved or deleted.

$vars = $mailinfo;
$vars['name']=$this->mime_decode($mailinfo['name']);
$vars['subject']=$mailinfo['subject']?$this->mime_decode($mailinfo['subject']):'[No Subject]';
Expand All @@ -423,19 +421,15 @@ function createTicket($mid) {

$ticket=null;
$newticket=true;
//Check the subject line for possible ID.
if($vars['subject'] && preg_match ("[[#][0-9]{1,10}]", $vars['subject'], $regs)) {
$tid=trim(preg_replace("/[^0-9]/", "", $regs[0]));
//Allow mismatched emails?? For now NO.
if(!($ticket=Ticket::lookupByExtId($tid, $vars['email'])))
$ticket=null;
}

$errors=array();
if($ticket) {
if(!($message=$ticket->postMessage($vars, 'Email')))
return false;

if (($thread = ThreadEntry::lookupByEmailHeaders($vars))
&& ($message = $thread->postEmail($vars))) {
if ($message === true)
// Email has been processed previously
return true;
elseif ($message)
$ticket = $message->getTicket();
} elseif (($ticket=Ticket::create($vars, $errors, 'Email'))) {
$message = $ticket->getLastMessage();
} else {
Expand Down
161 changes: 158 additions & 3 deletions include/class.thread.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,14 @@ function deleteAttachments() {

function delete() {

/* XXX: Leave this out until TICKET_EMAIL_INFO_TABLE has a primary
* key
$sql = 'DELETE mid.* FROM '.TICKET_EMAIL_INFO_TABLE.' mid
INNER JOIN '.TICKET_THREAD_TABLE.' thread ON (thread.id = mid.message_id)
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())
return false;
Expand Down Expand Up @@ -230,7 +238,7 @@ function load($id=0, $type='', $ticketId=0) {
if(!$id && !($id=$this->getId()))
return false;

$sql='SELECT thread.*, info.* '
$sql='SELECT thread.*, info.email_mid '
.' ,count(DISTINCT attach.attach_id) as attachments '
.' FROM '.TICKET_THREAD_TABLE.' thread '
.' LEFT JOIN '.TICKET_EMAIL_INFO_TABLE.' info
Expand Down Expand Up @@ -309,6 +317,10 @@ function getTicketId() {
return $this->ht['ticket_id'];
}

function getEmailMessageId() {
return $this->ht['email_mid'];
}

function getTicket() {

if(!$this->ticket && $this->getTicketId())
Expand Down Expand Up @@ -471,7 +483,76 @@ function getAttachmentsLinks($file='attachment.php', $target='', $separator=' ')

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) {
// +==================+===================+=============+
// | 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;

$vars = array(
'mid' => $mailinfo['mid'],
'ticketId' => $ticket->getId(),
'poster' => $mailinfo['name'],
'origin' => 'Email',
'source' => 'Email',
'ip' => '',
'reply_to' => $this,
);

$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['email'] == $ticket->getEmail()) {
$vars['message'] = $body;
return $ticket->postMessage($vars, 'Email');
}
elseif ($staff_id = Staff::getIdByEmail($mailinfo['email'])) {
$vars['staffId'] = $staff_id;
$poster = Staff::lookup($staff_id);
$errors = array();
$vars['note'] = $body;
return $ticket->postNote($vars, $errors, $poster);
}
// TODO: Consider security constraints
else {
$vars['message'] = sprintf("Received From: %s\n\n%s",
$mailinfo['email'], $body);
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() {
Expand All @@ -495,8 +576,11 @@ function saveEmailInfo($vars) {

$sql='INSERT INTO '.TICKET_EMAIL_INFO_TABLE
.' SET message_id='.db_input($this->getId()) //TODO: change it to thread_id
.', email_mid='.db_input($vars['mid']) //TODO: change it to mid.
.', headers='.db_input($vars['header']);
.', email_mid='.db_input($vars['mid']); //TODO: change it to mid.
if (isset($vars['header']))
$sql .= ', headers='.db_input($vars['header']);

$this->ht['email_mid'] = $vars['mid'];

return db_query($sql)?db_insert_id():0;
}
Expand Down Expand Up @@ -544,8 +628,56 @@ function lookup($id, $tid=0, $type='') {
)?$e:null;
}

/**
* 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
*/
function lookupByEmailHeaders($mailinfo) {
// Search for messages using the References header, then the
// in-reply-to header
$search = 'SELECT message_id FROM '.TICKET_EMAIL_INFO_TABLE
. ' WHERE email_mid=%s ORDER BY message_id DESC';

if ($id = db_result(db_query(
sprintf($search, db_input($mailinfo['mid'])))))
return ThreadEntry::lookup($id);

foreach (array('mid', 'in-reply-to', 'references') as $header) {
$matches = array();
if (!isset($mailinfo[$header]) || !$mailinfo[$header])
continue;
// Header may have multiple entries (usually separated by
// semi-colons (;))
elseif (!preg_match_all('/<[^>@]+@[^>]+>/', $mailinfo[$header],
$matches))
continue;

foreach ($matches[0] as $mid) {
$res = db_query(sprintf($search, db_input($mid)));
while (list($id) = db_fetch_row($res)) {
if ($t = ThreadEntry::lookup($id))
return $t;
}
}
}

// Search for ticket by the [#123456] in the subject line
$subject = $mailinfo['subject'];
$match = array();
if ($subject && preg_match("/\[#([0-9]{1,10})\]/", $subject, $match))
// Return last message for the thread
return Message::lastByExtTicketId((int)$match[1]);

return null;
}

//new entry ... we're trusting the caller to check validity of the data.
function create($vars) {
global $cfg;

//Must have...
if(!$vars['ticketId'] || !$vars['type'] || !in_array($vars['type'], array('M','R','N')))
Expand All @@ -562,6 +694,12 @@ function create($vars) {

if(isset($vars['pid']))
$sql.=' ,pid='.db_input($vars['pid']);
// Check if 'reply_to' is in the $vars as the previous ThreadEntry
// instance. If the body of the previous message is found in the new
// body, strip it out.
elseif (isset($vars['reply_to'])
&& $vars['reply_to'] instanceof ThreadEntry)
$sql.=' ,pid='.db_input($vars['reply_to']->getId());

if($vars['ip_address'])
$sql.=' ,ip_address='.db_input($vars['ip_address']);
Expand All @@ -584,6 +722,12 @@ function create($vars) {
if($vars['cannedattachments'] && is_array($vars['cannedattachments']))
$entry->saveAttachments($vars['cannedattachments']);

// Email message id (required for all thread posts)
if (!isset($vars['mid']))
$vars['mid'] = sprintf('<%s@%s>', Misc::randCode(24),
substr(md5($cfg->getUrl()), -10));
$entry->saveEmailInfo($vars);

return $entry;
}

Expand Down Expand Up @@ -630,6 +774,17 @@ function lookup($id, $tid=0, $type='M') {
&& $m->getId()==$id
)?$m:null;
}

function lastByExtTicketId($ticketId) {
$sql = 'SELECT thread.id FROM '.TICKET_THREAD_TABLE
.' thread JOIN '.TICKET_TABLE.' ticket ON (ticket.ticket_id = thread.ticket_id)
WHERE thread_type=\'M\' AND ticket.ticketID = '.db_input($ticketId)
.' ORDER BY thread.id DESC LIMIT 1';
if (($res = db_query($sql)) && (list($id) = db_fetch_row($res)))
return Message::lookup($id);
else
return null;
}
}

/* Response - Ticket thread entry of type response */
Expand Down
Loading

0 comments on commit 29b3714

Please sign in to comment.