Helpdesk da PluGzOne, baseado no osTicket
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1053 lines
38 KiB

<?php
/*********************************************************************
class.mailfetch.php
mail fetcher class. Uses IMAP ext for now.
Peter Rotich <peter@osticket.com>
Copyright (c) 2006-2013 osTicket
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:
**********************************************************************/
require_once(INCLUDE_DIR.'class.mailparse.php');
require_once(INCLUDE_DIR.'class.ticket.php');
require_once(INCLUDE_DIR.'class.dept.php');
require_once(INCLUDE_DIR.'class.email.php');
require_once(INCLUDE_DIR.'class.filter.php');
require_once(INCLUDE_DIR.'class.banlist.php');
require_once(INCLUDE_DIR.'tnef_decoder.php');
class MailFetcher {
var $ht;
var $mbox;
var $srvstr;
var $authuser;
var $username;
var $charset = 'UTF-8';
var $tnef = false;
function __construct($email, $charset='UTF-8') {
if($email && is_numeric($email)) //email_id
$email=Email::lookup($email);
if(is_object($email))
$this->ht = $email->getMailAccountInfo();
elseif(is_array($email) && $email['host']) //hashtable of mail account info
$this->ht = $email;
else
$this->ht = null;
$this->charset = $charset;
if ($this->ht) {
// Support Exchange shared mailbox auth
// (eg. user@domain.com\shared@domain.com)
$usernames = explode('\\', $this->ht['username']);
$count = count($usernames);
if ($count == 3) {
$this->authuser = $usernames[0].'\\'.$usernames[1];
$this->username = $usernames[2];
} elseif ($count == 2) {
if (strpos($usernames[0], '@') !== false) {
$this->authuser = $usernames[0];
$this->username = $usernames[1];
} else
$this->username = $usernames[0].'\\'.$usernames[1];
} else {
$this->username = $this->ht['username'];
}
if(!strcasecmp($this->ht['protocol'],'pop')) //force pop3
$this->ht['protocol'] = 'pop3';
else
$this->ht['protocol'] = strtolower($this->ht['protocol']);
//Max fetch per poll
if(!$this->ht['max_fetch'] || !is_numeric($this->ht['max_fetch']))
$this->ht['max_fetch'] = 20;
//Mail server string
$this->srvstr=sprintf('{%s:%d/%s', $this->getHost(), $this->getPort(), $this->getProtocol());
if(!strcasecmp($this->getEncryption(), 'SSL'))
$this->srvstr.='/ssl';
if ($this->authuser)
$this->srvstr .= sprintf('/authuser=%s', $this->authuser);
if (strcasecmp($this->getEncryption(), 'SSL'))
$this->srvstr.='/notls}';
else
$this->srvstr.='/novalidate-cert}';
}
//Set timeouts
if(function_exists('imap_timeout')) imap_timeout(1,20);
}
function getEmailId() {
return $this->ht['email_id'];
}
function getHost() {
return $this->ht['host'];
}
function getPort() {
return $this->ht['port'];
}
function getProtocol() {
return $this->ht['protocol'];
}
function getEncryption() {
return $this->ht['encryption'];
}
function getUsername() {
return $this->username;
}
function getPassword() {
return $this->ht['password'];
}
/* osTicket Settings */
function canDeleteEmails() {
return ($this->ht['delete_mail']);
}
function getMaxFetch() {
return $this->ht['max_fetch'];
}
function getMailFolder() {
return $this->mailbox_encode($this->ht['folder'] ?: 'INBOX');
}
function getArchiveFolder() {
return $this->mailbox_encode($this->ht['archive_folder']);
}
/* Core */
function connect() {
return ($this->mbox && $this->ping())?$this->mbox:$this->open($this->getMailFolder());
}
function ping() {
return ($this->mbox && imap_ping($this->mbox));
}
/* Default folder is inbox - TODO: provide user an option to fetch from diff folder/label */
function open($box='INBOX') {
if ($this->mbox)
$this->close();
$args = array($this->srvstr.$this->mailbox_encode($box),
$this->getUsername(), $this->getPassword());
// Disable Kerberos and NTLM authentication if it happens to be
// supported locally or remotely
if (version_compare(PHP_VERSION, '5.3.2', '>='))
$args = array_merge($args, array(NULL, 0, array(
'DISABLE_AUTHENTICATOR' => array('GSSAPI', 'NTLM'))));
$this->mbox = @call_user_func_array('imap_open', $args);
return $this->mbox;
}
function close($flag=CL_EXPUNGE) {
imap_close($this->mbox, $flag);
}
function mailcount() {
return count(imap_headers($this->mbox));
}
//Get mail boxes.
function getMailboxes() {
if(!($folders=imap_list($this->mbox, $this->srvstr, "*")) || !is_array($folders))
return null;
$list = array();
foreach($folders as $folder)
$list[]= str_replace($this->srvstr, '', imap_utf7_decode(trim($folder)));
return $list;
}
//Create a folder.
function createMailbox($folder) {
if(!$folder) return false;
return imap_createmailbox($this->mbox,
$this->srvstr.$this->mailbox_encode(trim($folder)));
}
/* check if a folder exists - create one if requested */
function checkMailbox($folder, $create=false) {
if(($mailboxes=$this->getMailboxes()) && in_array(trim($folder), $mailboxes))
return true;
return ($create && $this->createMailbox($folder));
}
function decode($text, $encoding) {
switch($encoding) {
case 1:
$text=imap_8bit($text);
break;
case 2:
$text=imap_binary($text);
break;
case 3:
if (strlen($text) > (1 << 20)) {
try {
if (!($temp = tempnam(sys_get_temp_dir(), 'attachments'))
|| !($f = fopen($temp, 'w'))
) {
throw new Exception();
}
$s_filter = stream_filter_append($f, 'convert.base64-decode',STREAM_FILTER_WRITE);
if (!fwrite($f, $text))
throw new Exception();
stream_filter_remove($s_filter);
fclose($f);
if (!($f = fopen($temp, 'r')) || !($text = fread($f, filesize($temp))))
throw new Exception();
fclose($f);
unlink($temp);
break;
}
catch (Exception $e) {
// Noop. Fall through to imap_base64 method below
@fclose($f);
@unlink($temp);
}
}
// imap_base64 implies strict mode. If it refuses to decode the
// data, then fallback to base64_decode in non-strict mode
$text = (($conv=imap_base64($text))) ? $conv : base64_decode($text);
break;
case 4:
$text=imap_qprint($text);
break;
}
return $text;
}
//Convert text to desired encoding..defaults to utf8
function mime_encode($text, $charset=null, $encoding='utf-8') { //Thank in part to afterburner
return Charset::transcode($text, $charset, $encoding);
}
function mailbox_encode($mailbox) {
if (!$mailbox)
return null;
// Properly encode the mailbox to UTF-7, according to rfc2060,
// section 5.1.3
elseif (function_exists('mb_convert_encoding'))
return mb_convert_encoding($mailbox, 'UTF7-IMAP', 'utf-8');
else
// XXX: This function has some issues on some versions of PHP
return imap_utf7_encode($mailbox);
}
/**
* Mime header value decoder. Usually unicode characters are encoded
* according to RFC-2047. This function will decode RFC-2047 encoded
* header values as well as detect headers which are not encoded.
*
* Caveats:
* If headers contain non-ascii characters the result of this function
* is completely undefined. If osTicket is corrupting your email
* headers, your mail software is not encoding the header text
* correctly.
*
* Returns:
* Header value, transocded to UTF-8
*/
function mime_decode($text, $encoding='utf-8') {
// Handle poorly or completely un-encoded header values (
if (function_exists('mb_detect_encoding'))
if (($src_enc = mb_detect_encoding($text))
&& (strcasecmp($src_enc, 'ASCII') !== 0))
return Charset::transcode($text, $src_enc, $encoding);
// Handle ASCII text and RFC-2047 encoding
$str = '';
$parts = imap_mime_header_decode($text);
foreach ($parts as $part)
$str.= $this->mime_encode($part->text, $part->charset, $encoding);
return $str?$str:imap_utf8($text);
}
function getLastError() {
return imap_last_error();
}
function getMimeType($struct) {
$mimeType = array('TEXT', 'MULTIPART', 'MESSAGE', 'APPLICATION', 'AUDIO', 'IMAGE', 'VIDEO', 'OTHER');
if(!$struct || !$struct->subtype)
return 'TEXT/PLAIN';
return $mimeType[(int) $struct->type].'/'.$struct->subtype;
}
function getHeaderInfo($mid) {
if(!($headerinfo=imap_headerinfo($this->mbox, $mid)) || !$headerinfo->from)
return null;
$raw_header = $this->getHeader($mid);
$info = array(
'raw_header' => &$raw_header,
'headers' => $headerinfo,
'decoder' => $this,
'type' => $this->getMimeType($headerinfo),
);
Signal::send('mail.decoded', $this, $info);
$sender=$headerinfo->from[0];
//Just what we need...
$header=array('name' => $this->mime_decode(@$sender->personal),
'email' => trim(strtolower($sender->mailbox).'@'.$sender->host),
'subject'=> $this->mime_decode(@$headerinfo->subject),
'mid' => trim(@$headerinfo->message_id),
'header' => $raw_header,
'in-reply-to' => $headerinfo->in_reply_to,
'references' => $headerinfo->references,
);
if ($replyto = $headerinfo->reply_to) {
$header['reply-to'] = $replyto[0]->mailbox.'@'.$replyto[0]->host;
$header['reply-to-name'] = $replyto[0]->personal;
}
// Put together a list of recipients
$tolist = array();
if($headerinfo->to)
$tolist['to'] = $headerinfo->to;
if($headerinfo->cc)
$tolist['cc'] = $headerinfo->cc;
//Add delivered-to address to list.
if (stripos($header['header'], 'delivered-to:') !==false
&& ($dt = Mail_Parse::findHeaderEntry($header['header'],
'delivered-to', true))) {
if (($delivered_to = Mail_Parse::parseAddressList($dt)))
$tolist['delivered-to'] = $delivered_to;
}
$header['system_emails'] = array();
$header['recipients'] = array();
$header['thread_entry_recipients'] = array();
foreach($tolist as $source => $list) {
foreach($list as $addr) {
if (!($emailId=Email::getIdByEmail(strtolower($addr->mailbox).'@'.$addr->host))) {
//Skip virtual Delivered-To addresses
if ($source == 'delivered-to') continue;
$name = $this->mime_decode(@$addr->personal);
$email = strtolower($addr->mailbox).'@'.$addr->host;
$header['recipients'][] = array(
'source' => sprintf(_S("Email (%s)"),$source),
'name' => $name,
'email' => $email);
$header['thread_entry_recipients'][$source][] = sprintf('%s <%s>', $name, $email);
} elseif ($emailId) {
$header['system_emails'][] = $emailId;
$system_email = Email::lookup($emailId);
$header['thread_entry_recipients']['to'][] = (string) $system_email;
if (!$header['emailId'])
$header['emailId'] = $emailId;
}
}
}
if (isset($header['thread_entry_recipients']['to']))
$header['thread_entry_recipients']['to'] = array_unique($header['thread_entry_recipients']['to']);
//See if any of the recipients is a delivered to address
if ($tolist['delivered-to']) {
foreach ($tolist['delivered-to'] as $addr) {
foreach ($header['recipients'] as $i => $r) {
if (strcasecmp($r['email'], $addr->mailbox.'@'.$addr->host) === 0)
$header['recipients'][$i]['source'] = 'delivered-to';
}
}
}
//BCCed?
if ($bcc = $headerinfo->bcc) {
foreach ($bcc as $addr) {
if (!($emailId=Email::getIdByEmail($addr->mailbox.'@'.$addr->host)))
continue;
$header['system_emails'][] = $emailId;
if (!$header['emailId'])
$header['emailId'] = $emailId;
}
}
$header['system_emails'] = array_unique($header['system_emails']);
// Ensure we have a message-id. If unable to read it out of the
// email, use the hash of the entire email headers
if (!$header['mid'] && $header['header']) {
$header['mid'] = Mail_Parse::findHeaderEntry($header['header'],
'message-id');
if (is_array($header['mid']))
$header['mid'] = array_pop(array_filter($header['mid']));
if (!$header['mid'])
$header['mid'] = '<' . md5($header['header']) . '@local>';
}
return $header;
}
function fetchBody($mid, $index, $encoding) {
$body = imap_fetchbody($this->mbox, $mid, $index);
if ($body && $encoding)
$body = $this->decode($body, $encoding);
return $body;
}
//search for specific mime type parts....encoding is the desired encoding.
function getPart($mid, $mimeType, $encoding=false, $struct=null,
$partNumber=false, $recurse=-1, $recurseIntoRfc822=false) {
if(!$struct && $mid)
$struct=@imap_fetchstructure($this->mbox, $mid);
//Match the mime type.
if($struct
&& strcasecmp($mimeType, $this->getMimeType($struct))==0
&& (!$struct->ifdparameters
|| !$this->findFilename($struct->dparameters))) {
$partNumber=$partNumber?$partNumber:1;
if(!($text=imap_fetchbody($this->mbox, $mid, $partNumber))
&& $partNumber == 1
&& $struct->disposition == 'inline')
$text=imap_body($this->mbox, $mid);
if ($text) {
if($struct->encoding==3 or $struct->encoding==4) //base64 and qp decode.
$text=$this->decode($text, $struct->encoding);
$charset=null;
if ($encoding) { //Convert text to desired mime encoding...
if ($struct->ifparameters && $struct->parameters) {
foreach ($struct->parameters as $p) {
if (!strcasecmp($p->attribute, 'charset')) {
$charset = trim($p->value);
break;
}
}
}
$text = $this->mime_encode($text, $charset, $encoding);
}
return $text;
}
}
if ($this->tnef && !strcasecmp($mimeType, 'text/html')
&& ($content = $this->tnef->getBody('text/html', $encoding)))
return $content;
// Do recursive search
$text = '';
$ctype = $this->getMimeType($struct);
if ($struct && $struct->parts && $recurse
// Do not recurse into email (rfc822) attachments unless requested
&& (strtolower($ctype) !== 'message/rfc822' || $recurseIntoRfc822)
) {
while(list($i, $substruct) = each($struct->parts)) {
if ($partNumber)
$prefix = $partNumber . '.';
if ($result = $this->getPart($mid, $mimeType, $encoding,
$substruct, $prefix.($i+1), $recurse-1, $recurseIntoRfc822)
) {
$text .= $result;
}
}
}
return $text;
}
/**
* Searches the attribute list for a possible filename attribute. If
* found, the attribute value is returned. If the attribute uses rfc5987
* to encode the attribute value, the value is returned properly decoded
* if possible
*
* Attribute Search Preference:
* filename
* filename*
* name
* name*
*/
function findFilename($attributes) {
foreach (array('filename', 'name') as $pref) {
foreach ($attributes as $a) {
if (strtolower($a->attribute) == $pref)
return $a->value;
// Allow the RFC5987 specification of the filename
elseif (strtolower($a->attribute) == $pref.'*')
return Format::decodeRfc5987($a->value);
}
}
return false;
}
/*
getAttachments
search and return a hashtable of attachments....
NOTE: We're not actually fetching the body of the attachment - we'll do it on demand to save some memory.
*/
function getAttachments($part, $index=0) {
$ctype = $part ? $this->getMimeType($part) : false;
if($part && (!$part->parts
|| strcasecmp($ctype, 'message/rfc822') === 0)) {
//Check if the part is an attachment.
$filename = false;
if ($part->ifdisposition && $part->ifdparameters
&& in_array(strtolower($part->disposition),
array('attachment', 'inline'))) {
$filename = $this->findFilename($part->dparameters);
}
// Inline attachments without disposition. NOTE that elseif is
// not utilized here b/c ::findFilename may return null
if (!$filename && $part->ifparameters && $part->parameters
&& $part->type > 0) {
$filename = $this->findFilename($part->parameters);
}
$content_id = ($part->ifid)
? rtrim(ltrim($part->id, '<'), '>') : false;
// Some mail clients / servers (like Lotus Notes / Domino) will
// send images without a filename. For such a case, generate a
// random filename for the image
if (!$filename && $content_id && $part->type == 5) {
$filename = _S('image').'-'.Misc::randCode(4).'.'.strtolower($part->subtype);
}
// Attached message/rfc822 without filename.
if (!$filename
&& $ctype
&& strcasecmp($ctype, 'message/rfc822') === 0)
$filename = 'email-message-'.Misc::randCode(4).'.eml';
if($filename) {
return array(
array(
'name' => $this->mime_decode($filename),
'type' => $this->getMimeType($part),
'size' => $part->bytes ?: null,
'encoding' => $part->encoding,
'index' => ($index?$index:1),
'cid' => $content_id,
)
);
}
}
//Recursive attachment search!
$attachments = array();
if($part && $part->parts) {
foreach($part->parts as $k=>$struct) {
if($index) $prefix = $index.'.';
$attachments = array_merge($attachments, $this->getAttachments($struct, $prefix.($k+1)));
}
}
return $attachments;
}
function getHeader($mid) {
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'
) {
if ($body = $this->getPart(
$mid, 'text/plain', $this->charset, $struct, false, 3, false
)) {
return new TextThreadEntryBody($body);
}
}
}
}
return false;
}
function getOriginalMessageHeaders($mid) {
if (!($body = $this->getPart($mid, 'message/rfc822'))) {
// Handle rfc1892 style bounces
if (!($body = $this->getPart($mid, 'text/rfc822-headers'))) {
return null;
}
else {
// Add a junk body for the parser
$body .= "\n\nIgnored";
}
}
$msg = new Mail_Parse($body);
if (!$msg->decode())
return null;
return $msg->struct->headers;
}
function getPriority($mid) {
if ($this->tnef && isset($this->tnef->Importance)) {
// PidTagImportance is 0, 1, or 2, 2 is high
// http://msdn.microsoft.com/en-us/library/ee237166(v=exchg.80).aspx
$urgency = 4 - $this->tnef->Importance;
}
elseif ($priority = Mail_Parse::parsePriority($this->getHeader($mid))) {
$urgency = $priority + 1;
}
if ($urgency) {
$sql = 'SELECT `priority_id` FROM '.PRIORITY_TABLE
.' WHERE `priority_urgency`='.db_input($urgency)
.' LIMIT 1';
$id = db_result(db_query($sql));
return $id;
}
}
function getBody($mid) {
global $cfg;
if ($cfg->isRichTextEnabled()) {
if ($html=$this->getPart($mid, 'text/html', $this->charset))
$body = new HtmlThreadEntryBody($html);
elseif ($text=$this->getPart($mid, 'text/plain', $this->charset))
$body = new TextThreadEntryBody($text);
}
elseif ($text=$this->getPart($mid, 'text/plain', $this->charset))
$body = new TextThreadEntryBody($text);
elseif ($html=$this->getPart($mid, 'text/html', $this->charset))
$body = new TextThreadEntryBody(
Format::html2text(Format::safe_html($html),
100, false));
if (!isset($body))
$body = new TextThreadEntryBody('');
if ($cfg->stripQuotedReply())
$body->stripQuotedReply($cfg->getReplySeparator());
return $body;
}
//email to ticket
function createTicket($mid) {
global $ost;
unset($this->tnef);
if(!($mailinfo = $this->getHeaderInfo($mid)))
return false;
// TODO: If the content-type of the message is 'message/rfc822',
// then this is a message with the forwarded message as the
// attachment. Download the body and pass it along to the mail
// parsing engine.
$info = Mail_Parse::splitHeaders($mailinfo['header']);
//make sure reply-to headers are correctly formatted
if ($mailinfo['reply-to'] && !Validator::is_email($mailinfo['reply-to']) && $info['Reply-To']) {
$replyto = Mail_Parse::parseAddressList($info['Reply-To']);
if ($replyto[0]) {
$mailinfo['reply-to'] = sprintf('%s@%s', $replyto[0]->mailbox, $replyto[0]->host);
$mailinfo['reply-to-name'] = $replyto[0]->personal;
} else {
$mailinfo['reply-to'] = null;
}
}
if (strtolower($info['Content-Type']) == 'message/rfc822') {
if ($wrapped = $this->getPart($mid, 'message/rfc822')) {
require_once INCLUDE_DIR.'api.tickets.php';
// Simulate piping the contents into the system
$api = new TicketApiController();
$parser = new EmailDataParser();
if ($data = $parser->parse($wrapped))
return $api->processEmail($data);
}
// If any of this fails, create the ticket as usual
}
//Is the email address banned?
if($mailinfo['email'] && Banlist::isBanned($mailinfo['email'])) {
//We need to let admin know...
$ost->logWarning(_S('Ticket denied'),
sprintf(_S('Banned email — %s'),$mailinfo['email']), false);
return true; //Report success (moved or delete)
}
// Process overloaded attachments
$attachments = array();
if (($struct = @imap_fetchstructure($this->mbox, $mid))
&& ($attachments = $this->getAttachments($struct))) {
foreach ($attachments as $i=>$info) {
switch (strtolower($info['type'])) {
// Parse MS TNEF emails
case 'application/ms-tnef':
try {
$data = $this->decode(imap_fetchbody($this->mbox,
$mid, $info['index']), $info['encoding']);
$tnef = new TnefStreamParser($data);
$this->tnef = $tnef->getMessage();
// No longer considered an attachment
unset($attachments[$i]);
// There should only be one of these
break;
} catch (TnefException $ex) {
// Noop -- winmail.dat remains an attachment
}
break;
// Parse attached email message
case 'message/rfc822':
try {
// Fetch the header of attached mime message.
$body = $this->fetchBody($mid, $info['index'].'.0',
$info['encoding']);
// Add fake body to make the parser happy
if (!$body)
$body.="\n\nJunk";
$parser = new Mail_Parse($body);
if ($parser->decode()
&& ($subj = $parser->getSubject()))
$attachments[$i]['name'] = $subj.'.eml';
} catch(Exception $ex) {
// Noop -- use random name
}
$body = $parser = null;
break;
}
}
}
$vars = $mailinfo;
$vars['name'] = $mailinfo['name'];
$vars['subject'] = $mailinfo['subject'] ?: '[No Subject]';
$vars['emailId'] = $mailinfo['emailId'] ?: $this->getEmailId();
$vars['to-email-id'] = $mailinfo['emailId'] ?: 0;
$vars['mailflags'] = new ArrayObject();
$vars['system_emails'] = $mailinfo['system_emails'];
if ($this->isBounceNotice($mid)) {
// Fetch the original References and assign to 'references'
if ($headers = $this->getOriginalMessageHeaders($mid)) {
$vars['references'] = $headers['references'];
$vars['in-reply-to'] = $headers['message-id'] ?: @$headers['in-reply-to'] ?: null;
}
// Fetch deliver status report
$vars['message'] = $this->getDeliveryStatusMessage($mid) ?: $this->getBody($mid);
$vars['thread-type'] = 'N';
$vars['mailflags']['bounce'] = true;
}
else {
$vars['message'] = $this->getBody($mid);
$vars['mailflags']['bounce'] = TicketFilter::isBounce($info);
}
//Missing FROM name - use email address.
if(!$vars['name'])
list($vars['name']) = explode('@', $vars['email']);
if($ost->getConfig()->useEmailPriority())
$vars['priorityId']=$this->getPriority($mid);
$ticket=null;
$newticket=true;
$errors=array();
$seen = false;
// Use the settings on the thread entry on the ticket details
// form to validate the attachments in the email
$tform = TicketForm::objects()->one()->getForm();
$messageField = $tform->getField('message');
$fileField = $messageField->getWidget()->getAttachments();
// Fetch attachments if any.
if ($messageField->isAttachmentsEnabled()) {
// Include TNEF attachments in the attachments list
if ($this->tnef) {
foreach ($this->tnef->attachments as $at) {
$attachments[] = array(
'cid' => @$at->AttachContentId ?: false,
'data' => $at,
'size' => @$at->DataSize ?: null,
'type' => @$at->AttachMimeTag ?: false,
'name' => $at->getName(),
);
}
}
$vars['attachments'] = array();
foreach ($attachments as $a) {
$file = array('name' => $a['name'], 'type' => $a['type']);
if (@$a['data'] instanceof TnefAttachment) {
$file['data'] = $a['data']->getData();
}
else {
// only fetch the body if necessary
$self = $this;
$file['data_cbk'] = function() use ($self, $mid, $a) {
return $self->decode(imap_fetchbody($self->mbox,
$mid, $a['index']), $a['encoding']);
};
}
// Include the Content-Id if specified (for inline images)
$file['cid'] = isset($a['cid']) ? $a['cid'] : false;
// Validate and save immediately
try {
if ($f = $fileField->uploadAttachment($file))
$file['id'] = $f->getId();
}
catch (FileUploadError $ex) {
$name = $file['name'];
$file = array();
$file['error'] = Format::htmlchars($name) . ': ' . $ex->getMessage();
}
$vars['attachments'][] = $file;
}
}
// Allow signal handlers to interact with the message decoding
Signal::send('mail.processed', $this, $vars);
$seen = false;
if (($entry = ThreadEntry::lookupByEmailHeaders($vars, $seen))
&& ($entry instanceof ThreadEntry)
&& ($message = $entry->postEmail($vars))
) {
if (!$message instanceof ThreadEntry)
// Email has been processed previously
return $message;
// NOTE: This might not be a "ticket"
$ticket = $message->getThread()->getObject();
}
elseif ($entry && !$entry instanceof ThreadEntry) {
return null;
}
elseif ($seen) {
// Already processed, but for some reason (like rejection), no
// thread item was created. Ignore the email
return true;
}
// Allow continuation of thread without initial message or note
elseif (($thread = Thread::lookupByEmailHeaders($vars))
&& ($message = $thread->postEmail($vars))
) {
// NOTE: This might not be a "ticket"
$ticket = $thread->getObject();
}
elseif (($ticket=Ticket::create($vars, $errors, 'Email'))) {
$message = $ticket->getLastMessage();
}
else {
//Report success if the email was absolutely rejected.
if(isset($errors['errno']) && $errors['errno'] == 403) {
// Never process this email again!
ThreadEntry::logEmailHeaders(0, $vars['mid']);
return true;
}
// Log an error to the system logs
$mailbox = Email::lookup($vars['emailId']);
$ost->logError(_S('Mail Processing Exception'), sprintf(
_S("Mailbox: %s | Error(s): %s"),
$mailbox->getEmail(),
print_r($errors, true)
), false);
// Indicate failure of mail processing
return null;
}
return $ticket;
}
static function getSupportedProtos() {
return array(
'IMAP/SSL' => 'IMAP + SSL',
'IMAP' => 'IMAP',
'POP/SSL' => 'POP + SSL',
'POP' => 'POP',
);
}
function fetchEmails() {
if(!$this->connect())
return false;
$archiveFolder = $this->getArchiveFolder();
$delete = $this->canDeleteEmails();
$max = $this->getMaxFetch();
$nummsgs=imap_num_msg($this->mbox);
//echo "New Emails: $nummsgs\n";
$msgs=$errors=0;
for($i=$nummsgs; $i>0; $i--) { //process messages in reverse.
if($this->createTicket($i)) {
imap_setflag_full($this->mbox, imap_uid($this->mbox, $i), "\\Seen", ST_UID); //IMAP only??
if((!$archiveFolder || !imap_mail_move($this->mbox, $i, $archiveFolder)) && $delete)
imap_delete($this->mbox, $i);
$msgs++;
$errors=0; //We are only interested in consecutive errors.
} else {
$errors++;
}
if($max && ($msgs>=$max || $errors>($max*0.8)))
break;
}
//Warn on excessive errors
if($errors>$msgs) {
$warn=sprintf(_S('Excessive errors processing emails for %1$s/%2$s. Please manually check the inbox.'),
$this->getHost(), $this->getUsername());
$this->log($warn);
}
@imap_expunge($this->mbox);
return $msgs;
}
function log($error) {
global $ost;
$ost->logWarning(_S('Mail Fetcher'), $error);
}
/*
MailFetcher::run()
Static function called to initiate email polling
*/
function run() {
global $ost;
if(!$ost->getConfig()->isEmailPollingEnabled())
return;
//We require imap ext to fetch emails via IMAP/POP3
//We check here just in case the extension gets disabled post email config...
if(!function_exists('imap_open')) {
$msg=_S('osTicket requires PHP IMAP extension enabled for IMAP/POP3 email fetch to work!');
$ost->logWarning(_S('Mail Fetch Error'), $msg);
return;
}
//Hardcoded error control...
$MAXERRORS = 5; //Max errors before we start delayed fetch attempts
$TIMEOUT = 10; //Timeout in minutes after max errors is reached.
$sql=' SELECT email_id, mail_errors FROM '.EMAIL_TABLE
.' 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 ASC';
if (!($res=db_query($sql)) || !db_num_rows($res))
return; /* Failed query (get's logged) or nothing to do... */
//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;
$fetcher = new MailFetcher($emailId);
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) {
//We've reached the MAX consecutive errors...will attempt logins at delayed intervals
// XXX: Translate me
$msg="\nosTicket is having trouble fetching emails from the following mail account: \n".
"\n"._S('User').": ".$fetcher->getUsername().
"\n"._S('Host').": ".$fetcher->getHost().
"\n"._S('Error').": ".$fetcher->getLastError().
"\n\n ".sprintf(_S('%1$d consecutive errors. Maximum of %2$d allowed'), $errors, $MAXERRORS).
"\n\n ".sprintf(_S('This could be connection issues related to the mail server. Next delayed login attempt in aprox. %d minutes'),$TIMEOUT);
$ost->alertAdmin(_S('Mail Fetch Failure Alert'), $msg, true);
}
}
} //end while.
}
}
?>