diff --git a/include/api.tickets.php b/include/api.tickets.php index fb12e623c03328eada3e02bfddba0178c39860c9..4aebd5341ede14d291460400c4352598b24aa4df 100644 --- a/include/api.tickets.php +++ b/include/api.tickets.php @@ -39,7 +39,7 @@ class TicketApiController extends ApiController { if(!strcasecmp($format, 'email')) { $supported = array_merge($supported, array('header', 'mid', 'emailId', 'ticketId', 'reply-to', 'reply-to-name', - 'in-reply-to', 'references', + 'in-reply-to', 'references', 'thread-type', 'recipients' => array('*' => array('name', 'email', 'source')) )); @@ -147,10 +147,6 @@ class TicketApiController extends ApiController { function processEmail() { $data = $this->getEmailRequest(); - if($data['ticketId'] && ($ticket=Ticket::lookup($data['ticketId']))) { - if(($msgid=$ticket->postMessage($data, 'Email'))) - return $ticket; - } if (($thread = ThreadEntry::lookupByEmailHeaders($data)) && $thread->postEmail($data)) { diff --git a/include/class.mailer.php b/include/class.mailer.php index f6b83784edd727361e927d195a1c65702759a26d..2ad10dd5a2743936bb1cf5701f0f27422c38ae28 100644 --- a/include/class.mailer.php +++ b/include/class.mailer.php @@ -109,7 +109,8 @@ class Mailer { 'Subject' => $subject, 'Date'=> date('D, d M Y H:i:s O'), 'Message-ID' => $messageId, - 'X-Mailer' =>'osTicket Mailer' + 'X-Mailer' =>'osTicket Mailer', + 'Return-Path' => $this->getEmail()->getEmail(), ); //Set bulk/auto-response headers. diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php index 9184ea8d146f6b1e4c683d8cc159d3d6a12ec27d..fb4d3dd40d4892fe310e5e53bf1d2c0494d2c297 100644 --- a/include/class.mailfetch.php +++ b/include/class.mailfetch.php @@ -325,7 +325,7 @@ class MailFetcher { } //search for specific mime type parts....encoding is the desired encoding. - function getPart($mid, $mimeType, $encoding=false, $struct=null, $partNumber=false) { + function getPart($mid, $mimeType, $encoding=false, $struct=null, $partNumber=false, $recurse=-1) { if(!$struct && $mid) $struct=@imap_fetchstructure($this->mbox, $mid); @@ -359,11 +359,12 @@ class MailFetcher { //Do recursive search $text=''; - if($struct && $struct->parts) { + if($struct && $struct->parts && $recurse) { while(list($i, $substruct) = each($struct->parts)) { if($partNumber) $prefix = $partNumber . '.'; - if(($result=$this->getPart($mid, $mimeType, $encoding, $substruct, $prefix.($i+1)))) + if (($result=$this->getPart($mid, $mimeType, $encoding, + $substruct, $prefix.($i+1), $recurse-1))) $text.=$result; } } @@ -451,6 +452,46 @@ class MailFetcher { return imap_fetchheader($this->mbox, $mid,FT_PREFETCHTEXT); } + function isBounceNotice($mid) { + if (!($body = $this->getPart($mid, 'message/delivery-status'))) + return false; + + $info = Mail_Parse::splitHeaders($body); + if (!isset($info['Action'])) + return false; + + return strcasecmp($info['Action'], 'failed') === 0; + } + + function getDeliveryStatusMessage($mid) { + if (!($struct = @imap_fetchstructure($this->mbox, $mid))) + return false; + + $ctype = $this->getMimeType($struct); + if (strtolower($ctype) == 'multipart/report') { + foreach ($struct->parameters as $p) { + if (strtolower($p->attribute) == 'report-type' + && $p->value == 'delivery-status') { + return sprintf('<pre>%s</pre>', + Format::htmlchars( + $this->getPart($mid, 'text/plain', $this->charset, $struct, false, 1) + )); + } + } + } + return false; + } + + function getOriginalMessage($mid) { + if (!($body = $this->getPart($mid, 'message/rfc822'))) + return null; + + $msg = new Mail_Parse($body); + if (!$msg->decode()) + return null; + + return $msg->struct; + } function getPriority($mid) { return Mail_Parse::parsePriority($this->getHeader($mid)); @@ -501,9 +542,23 @@ class MailFetcher { $vars = $mailinfo; $vars['name'] = $mailinfo['name']; $vars['subject'] = $mailinfo['subject'] ? $mailinfo['subject'] : '[No Subject]'; - $vars['message'] = Format::stripEmptyLines($this->getBody($mid)); $vars['emailId'] = $mailinfo['emailId'] ? $mailinfo['emailId'] : $this->getEmailId(); + if ($this->isBounceNotice($mid)) { + // Fetch the original References and assign to 'references' + if ($msg = $this->getOriginalMessage($mid)) { + $vars['references'] = $msg->headers['references']; + unset($vars['in-reply-to']); + } + // Fetch deliver status report + $vars['message'] = $this->getDeliveryStatusMessage($mid); + $vars['thread-type'] = 'N'; + } + else { + $vars['message'] = Format::stripEmptyLines($this->getBody($mid)); + } + + //Missing FROM name - use email address. if(!$vars['name']) list($vars['name']) = explode('@', $vars['email']); @@ -658,24 +713,31 @@ class MailFetcher { .' WHERE mail_active=1 ' .' AND (mail_errors<='.$MAXERRORS.' OR (TIME_TO_SEC(TIMEDIFF(NOW(), mail_lasterror))>'.($TIMEOUT*60).') )' .' AND (mail_lastfetch IS NULL OR TIME_TO_SEC(TIMEDIFF(NOW(), mail_lastfetch))>mail_fetchfreq*60)' - .' ORDER BY mail_lastfetch DESC' - .' LIMIT 10'; //Processing up to 10 emails at a time. + .' ORDER BY mail_lastfetch ASC'; - // echo $sql; - if(!($res=db_query($sql)) || !db_num_rows($res)) + if (!($res=db_query($sql)) || !db_num_rows($res)) return; /* Failed query (get's logged) or nothing to do... */ - //TODO: Lock the table here?? + //Get max execution time so we can figure out how long we can fetch + // take fetching emails. + if (!($max_time = ini_get('max_execution_time'))) + $max_time = 300; + + //Start time + $start_time = Misc::micro_time(); + while (list($emailId, $errors)=db_fetch_row($res)) { + //Break if we're 80% into max execution time + if ((Misc::micro_time()-$start_time) > ($max_time*0.80)) + break; - while(list($emailId, $errors)=db_fetch_row($res)) { $fetcher = new MailFetcher($emailId); - if($fetcher->connect()) { + if ($fetcher->connect()) { db_query('UPDATE '.EMAIL_TABLE.' SET mail_errors=0, mail_lastfetch=NOW() WHERE email_id='.db_input($emailId)); $fetcher->fetchEmails(); $fetcher->close(); } else { db_query('UPDATE '.EMAIL_TABLE.' SET mail_errors=mail_errors+1, mail_lasterror=NOW() WHERE email_id='.db_input($emailId)); - if(++$errors>=$MAXERRORS) { + if (++$errors>=$MAXERRORS) { //We've reached the MAX consecutive errors...will attempt logins at delayed intervals $msg="\nosTicket is having trouble fetching emails from the following mail account: \n". "\nUser: ".$fetcher->getUsername(). diff --git a/include/class.mailparse.php b/include/class.mailparse.php index fc865d4de2c333738ea107f73b450cb1cdcbff30..493603052e75fcf9e916f982428b065b3a43f095 100644 --- a/include/class.mailparse.php +++ b/include/class.mailparse.php @@ -180,6 +180,40 @@ class Mail_Parse { return Mail_Parse::parseAddressList($header); } + function isBounceNotice() { + if (!($body = $this->getPart($this->struct, 'message/delivery-status'))) + return false; + + $info = self::splitHeaders($body); + if (!isset($info['Action'])) + return false; + + return strcasecmp($info['Action'], 'failed') === 0; + } + + function getDeliveryStatusMessage() { + $ctype = @strtolower($this->struct->ctype_primary.'/'.$this->struct->ctype_secondary); + if ($ctype == 'multipart/report' + && isset($this->struct->ctype_parameters['report-type']) + && $this->struct->ctype_parameters['report-type'] == 'delivery-status' + ) { + return sprintf('<pre>%s</pre>', + Format::htmlchars( + $this->getPart($this->struct, 'text/plain', 1) + )); + } + return false; + } + + function getOriginalMessage() { + foreach ($this->struct->parts as $p) { + $ctype = $p->ctype_primary.'/'.$p->ctype_secondary; + if (strtolower($ctype) === 'message/rfc822') + return $p->parts[0]; + } + return null; + } + function getBody(){ global $cfg; @@ -208,7 +242,7 @@ class Mail_Parse { return $body; } - function getPart($struct, $ctypepart) { + function getPart($struct, $ctypepart, $recurse=-1) { if($struct && !$struct->parts) { $ctype = @strtolower($struct->ctype_primary.'/'.$struct->ctype_secondary); @@ -224,9 +258,10 @@ class Mail_Parse { } $data=''; - if($struct && $struct->parts) { + if($struct && $struct->parts && $recurse) { foreach($struct->parts as $i=>$part) { - if($part && !$part->disposition && ($text=$this->getPart($part,$ctypepart))) + if($part && !$part->disposition + && ($text=$this->getPart($part,$ctypepart,$recurse - 1))) $data.=$text; } } @@ -423,15 +458,26 @@ class EmailDataParser { $data['emailId'] = $emailId; } + if ($parser->isBounceNotice()) { + // Fetch the original References and assign to 'references' + if ($msg = $parser->getOriginalMessage()) + $data['references'] = $msg->headers['references']; + // Fetch deliver status report + $data['message'] = $parser->getDeliveryStatusMessage(); + $data['thread-type'] = 'N'; + } + else { + // Typical email + $data['message'] = Format::stripEmptyLines($parser->getBody()); + $data['in-reply-to'] = $parser->struct->headers['in-reply-to']; + $data['references'] = $parser->struct->headers['references']; + } + $data['subject'] = $parser->getSubject(); - $data['message'] = Format::stripEmptyLines($parser->getBody()); $data['header'] = $parser->getHeader(); $data['mid'] = $parser->getMessageId(); $data['priorityId'] = $parser->getPriority(); - $data['in-reply-to'] = $parser->struct->headers['in-reply-to']; - $data['references'] = $parser->struct->headers['references']; - if (($replyto = $parser->getReplyTo()) && !PEAR::isError($replyto)) { $replyto = $replyto[0]; $data['reply-to'] = $replyto->mailbox.'@'.$replyto->host; diff --git a/include/class.thread.php b/include/class.thread.php index c5c1707cd5b2580d93f4c25d7199c941defecace..a05dcedb9446ab36f8241e9e61d846e802f5c467 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -613,6 +613,7 @@ Class ThreadEntry { 'reply_to' => $this, 'recipients' => $mailinfo['recipients'], ); + $errors = array(); if (isset($mailinfo['attachments'])) $vars['attachments'] = $mailinfo['attachments']; @@ -633,7 +634,6 @@ Class ThreadEntry { || ($mailinfo['staffId'] = Staff::getIdByEmail($mailinfo['email']))) { $vars['staffId'] = $mailinfo['staffId']; $poster = Staff::lookup($mailinfo['staffId']); - $errors = array(); $vars['note'] = $body; return $ticket->postNote($vars, $errors, $poster); } @@ -641,6 +641,15 @@ Class ThreadEntry { // 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 diff --git a/scp/autocron.php b/scp/autocron.php index 1e2460786f13405b24139bb7e4ca6bddc363c579..85e3cfbbb34c55f8085580daac7bd1fdd4e2c0b6 100644 --- a/scp/autocron.php +++ b/scp/autocron.php @@ -26,6 +26,12 @@ header('Cache-Control: no-cache, must-revalidate'); header('Content-Length: '.strlen($data)); header('Connection: Close'); print $data; +// Flush the request buffer +while(@ob_end_flush()); +flush(); +//Terminate the request +if (function_exists('fastcgi_finish_request')) + fastcgi_finish_request(); ob_start(); //Keep the image output clean. Hide our dirt. //TODO: Make cron DB based to allow for better time limits. Direct calls for now sucks big time. @@ -37,7 +43,7 @@ if($sec>180 && $ost && !$ost->isUpgradePending()): //user can call cron once eve require_once(INCLUDE_DIR.'class.cron.php'); $thisstaff = null; //Clear staff obj to avoid false credit internal notes & auto-assignment -Cron::TicketMonitor(); //Age tickets: We're going to age tickets regardless of cron settings. +Cron::TicketMonitor(); //Age tickets: We're going to age tickets regardless of cron settings. if($cfg && $cfg->isAutoCronEnabled()) { //ONLY fetch tickets if autocron is enabled! Cron::MailFetcher(); //Fetch mail. $ost->logDebug('Auto Cron', 'Mail fetcher cron call ['.$caller.']'); @@ -45,6 +51,5 @@ if($cfg && $cfg->isAutoCronEnabled()) { //ONLY fetch tickets if autocron is enab $_SESSION['lastcroncall']=time(); endif; -$output = ob_get_contents(); ob_end_clean(); ?> diff --git a/scp/js/scp.js b/scp/js/scp.js index 2ad14158f8ed3b23877b9260787b20b1465506ae..30458bcaa9a76c51b02ae1fdc35ae014519a41ef 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -400,6 +400,13 @@ $(document).ready(function(){ return false; }); + $(document).keydown(function(e) { + if (e.keyCode == 27) { + $('div.dialog').hide(); + $('#overlay').hide(); + } + }); + /* advanced search */ $('.dialog#advanced-search').css({ top : ($(window).height() / 6),