diff --git a/api/pipe.php b/api/pipe.php index d7d4b33be96644c484c846d47bf36179829ccdc2..29dfcff1d1aa10a20386589927830ffed8211911 100644 --- a/api/pipe.php +++ b/api/pipe.php @@ -21,7 +21,7 @@ require_once(INCLUDE_DIR.'class.mailparse.php'); require_once(INCLUDE_DIR.'class.email.php'); //Make sure piping is enabled! -if(!$cfg->enableEmailPiping()) +if(!$cfg->isEmailPipingEnabled()) api_exit(EX_UNAVAILABLE,'Email piping not enabled - check MTA settings.'); //Get the input $data=isset($_SERVER['HTTP_HOST'])?file_get_contents('php://input'):file_get_contents('php://stdin'); @@ -99,20 +99,18 @@ if(preg_match ("[[#][0-9]{1,10}]",$var['subject'],$regs)) { } $errors=array(); $msgid=0; -if(!$ticket){ //New tickets... +if(!$ticket) { //New tickets... $ticket=Ticket::create($var,$errors,'email'); - if(!is_object($ticket) || $errors){ + if(!is_object($ticket) || $errors) { api_exit(EX_DATAERR,'Ticket create Failed '.implode("\n",$errors)."\n\n"); } + $msgid=$ticket->getLastMsgId(); -}else{ - $message=$var['message']; - //Strip quoted reply...TODO: figure out how mail clients do it without special tag.. - if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator()) && strpos($var['message'],$tag)) - list($message)=split($tag,$var['message']); + +} else { //post message....postMessage does the cleanup. - if(!($msgid=$ticket->postMessage($message,'Email',$var['mid'],$var['header']))) { - api_exit(EX_DATAERR,"Unable to post message \n\n $message\n"); + if(!($msgid=$ticket->postMessage($var['message'], 'Email',$var['mid'],$var['header']))) { + api_exit(EX_DATAERR, 'Unable to post message'); } } //Ticket created...save attachments if enabled. diff --git a/include/class.config.php b/include/class.config.php index 2c9afb2448b276dc9364a36b3f16e5c62bb22f45..cc9b497682097d0e622dbbd410b0a4f5b0fae601 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -320,23 +320,23 @@ class Config { return ($this->config['clickable_urls']); } - function canFetchMail() { - return ($this->config['enable_mail_polling']); - } - function enableStaffIPBinding() { return ($this->config['staff_ip_binding']); } - function enableCaptcha() { + function isCaptchaEnabled() { return (extension_loaded('gd') && function_exists('gd_info') && $this->config['enable_captcha']); } - function enableAutoCron() { + function isAutoCronEnabled() { return ($this->config['enable_auto_cron']); } + + function isEmailPollingEnabled() { + return ($this->config['enable_mail_polling']); + } - function enableEmailPiping() { + function isEmailPipingEnabled() { return ($this->config['enable_email_piping']); } diff --git a/include/class.cron.php b/include/class.cron.php index c8e3a81def985b99eb0f21eaea8751362e1ced05..15ca7078b27785b6721b0a9f04b2d4b84eb28c9c 100644 --- a/include/class.cron.php +++ b/include/class.cron.php @@ -20,7 +20,7 @@ class Cron { function MailFetcher() { require_once(INCLUDE_DIR.'class.mailfetch.php'); - MailFetcher::fetchMail(); //Fetch mail..frequency is limited by email account setting. + MailFetcher::run(); //Fetch mail..frequency is limited by email account setting. } function TicketMonitor() { diff --git a/include/class.email.php b/include/class.email.php index dba9a45d5d4c569c9a5e7b2c17c4254f12abdcee..0cc04010bf54d85212f3d49f8c05143be9c1c107 100644 --- a/include/class.email.php +++ b/include/class.email.php @@ -98,6 +98,27 @@ class Email { return $this->getHashtable(); } + function getMailAccountInfo() { + + /*NOTE: Do not change any of the tags - otherwise mail fetching will fail */ + $info = array( + //Mail server info + 'host' => $this->ht['mail_host'], + 'port' => $this->ht['mail_port'], + 'protocol' => $this->ht['mail_protocol'], + 'encryption' => $this->ht['mail_encryption'], + 'username' => $this->ht['userid'], + 'password' => Mcrypt::decrypt($this->ht['userpass'], SECRET_SALT), + //osTicket specific + 'email_id' => $this->getId(), //Required for email routing to work. + 'max_fetch' => $this->ht['mail_fetchmax'], + 'delete_mail' => $this->ht['mail_delete'], + 'archive_folder' => $this->ht['mail_archivefolder'] + ); + + return $info; + } + function isSMTPEnabled() { return $this->ht['smtp_active']; } @@ -106,17 +127,15 @@ class Email { return ($this->ht['smtp_spoofing']); } - function getSMTPInfo($active=true) { - $info=array(); - if(!$active || ($active && $this->isSMTPEnabled())) { - - $info = array ('host' => $this->ht['smtp_host'], - 'port' => $this->ht['smtp_port'], - 'auth' => $this->ht['smtp_auth'], - 'username' => $this->ht['userid'], - 'password' =>Mcrypt::decrypt($this->ht['userpass'],SECRET_SALT) - ); - } + function getSMTPInfo() { + + $info = array ( + 'host' => $this->ht['smtp_host'], + 'port' => $this->ht['smtp_port'], + 'auth' => $this->ht['smtp_auth'], + 'username' => $this->ht['userid'], + 'password' => Mcrypt::decrypt($this->ht['userpass'], SECRET_SALT) + ); return $info; } @@ -207,12 +226,12 @@ class Email { $vars=$vars; $vars['cpasswd']=$this->getPasswd(); //Current decrypted password. - if($this->save($this->getId(),$vars,$errors)) { - $this->reload(); - return true; - } + if(!$this->save($this->getId(), $vars, $errors)) + return false; - return false; + $this->reload(); + + return true; } @@ -341,11 +360,8 @@ class Email { if(!isset($vars['postfetch'])) $errors['postfetch']='Indicate what to do with fetched emails'; - elseif(!strcasecmp($vars['postfetch'],'archive')) { - if(!$vars['mail_archivefolder']) - $errors['postfetch']='Valid folder required'; - } - + elseif(!strcasecmp($vars['postfetch'],'archive') && !$vars['mail_archivefolder'] ) + $errors['postfetch']='Valid folder required'; } if($vars['smtp_active']) { @@ -370,10 +386,16 @@ class Email { $passwd=$vars['passwd']?$vars['passwd']:$vars['cpasswd']; if(!$errors && $vars['mail_active']) { - //note: password is unencrypted at this point...MailFetcher expect plain text. - $fetcher = new MailFetcher($vars['userid'],$passwd,$vars['mail_host'],$vars['mail_port'], - $vars['mail_protocol'],$vars['mail_encryption']); + $fetcher = new MailFetcher( + array( + 'host' => $vars['mail_host'], + 'port' => $vars['mail_port'], + 'username' => $vars['userid'], + 'password' => $passwd, + 'protocol' => $vars['mail_protocol'], + 'encryption' => $vars['mail_encryption']) + ); if(!$fetcher->connect()) { $errors['err']='Invalid login. Check '.Format::htmlchars($vars['mail_protocol']).' settings'; $errors['mail']='<br>'.$fetcher->getLastError(); diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php index fb17d8a26ac719854a683ba778c287321ddefea9..fb8402169e83b39c258118329215d2459d7547a2 100644 --- a/include/class.mailfetch.php +++ b/include/class.mailfetch.php @@ -17,112 +17,161 @@ 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'); class MailFetcher { - var $hostname; - var $username; - var $password; - var $port; - var $protocol; - var $encryption; + var $ht; var $mbox; + var $srvstr; - var $charset= 'UTF-8'; + var $charset = 'UTF-8'; + var $encodings =array('UTF-8','WINDOWS-1251', 'ISO-8859-5', 'ISO-8859-1','KOI8-R'); - function MailFetcher($username,$password,$hostname,$port,$protocol,$encryption='') { + function MailFetcher($email, $charset='UTF-8') { - if(!strcasecmp($protocol,'pop')) //force pop3 - $protocol='pop3'; - - $this->hostname=$hostname; - $this->username=$username; - $this->password=$password; - $this->protocol=strtolower($protocol); - $this->port = $port; - $this->encryption = $encryption; + + 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) { + 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'; + + $this->srvstr.='/novalidate-cert}'; - $this->serverstr=sprintf('{%s:%d/%s',$this->hostname,$this->port,strtolower($this->protocol)); - if(!strcasecmp($this->encryption,'SSL')){ - $this->serverstr.='/ssl'; } - $this->serverstr.='/novalidate-cert}INBOX'; //add other flags here as needed. - //echo $this->serverstr; - //Charset to convert the mail to. - $this->charset='UTF-8'; //Set timeouts - if(function_exists('imap_timeout')) - imap_timeout(1,20); //Open timeout. + 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->ht['username']; } + function getPassword() { + return $this->ht['password']; + } + + /* osTicket Settings */ + + function canDeleteEmails() { + return ($this->ht['delete_mail']); + } + + function getMaxFetch() { + return $this->ht['max_fetch']; + } + + function getArchiveFolder() { + return $this->ht['archive_folder']; + } + + /* Core */ + function connect() { - return $this->open()?true:false; + return ($this->mbox && $this->ping())?$this->mbox:$this->open(); } - function open() { - - //echo $this->serverstr; - if($this->mbox && imap_ping($this->mbox)) - return $this->mbox; - - $this->mbox =@imap_open($this->serverstr,$this->username,$this->password); - + 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(); + + $this->mbox = imap_open($this->srvstr.$box, $this->getUsername(), $this->getPassword()); + return $this->mbox; } - function close() { - imap_close($this->mbox,CL_EXPUNGE); + function close($flag=CL_EXPUNGE) { + imap_close($this->mbox, $flag); } - function mailcount(){ + function mailcount() { return count(imap_headers($this->mbox)); } //Get mail boxes. - function getMailboxes(){ + function getMailboxes() { + if(!($folders=imap_list($this->mbox, $this->srvstr, "*")) || !is_array($folders)) + return null; - $mstr=sprintf('{%s:%d/%s',$this->hostname,$this->port,strtolower($this->protocol)); - if(!strcasecmp($this->encryption,'SSL')) - $mstr.='/ssl'; - $mstr.='/novalidate-cert}'; - $list=array(); - if(($folders=imap_listmailbox($this->mbox,$mstr,'*')) && is_array($folders)){ - foreach($folders as $k=>$folder){ - $list[]= str_replace($mstr, "", imap_utf7_decode(trim($folder))); - } - } + $list = array(); + foreach($folders as $folder) + $list[]= str_replace($this->srvstr, '', imap_utf7_decode(trim($folder))); return $list; } //Create a folder. - function createMailbox($folder){ + function createMailbox($folder) { if(!$folder) return false; - - $mstr=sprintf('{%s:%d/%s',$this->hostname,$this->port,strtolower($this->protocol)); - if(!strcasecmp($this->encryption,'SSL')) - $mstr.='/ssl'; - $mstr.='/novalidate-cert}'.$folder; - return imap_createmailbox($this->mbox,imap_utf7_encode($mstr)); + return imap_createmailbox($this->mbox, imap_utf7_encode($this->srvstr.trim($folder))); } - /* check if a folder exits - create on if requested */ - function checkMailbox($folder,$create=false){ + /* check if a folder exists - create one if requested */ + function checkMailbox($folder, $create=false) { - if(($mailboxes=$this->getMailboxes()) && in_array($folder,$mailboxes)) + if(($mailboxes=$this->getMailboxes()) && in_array(trim($folder), $mailboxes)) return true; return ($create && $this->createMailbox($folder)); } - function decode($encoding,$text) { + function decode($encoding, $text) { switch($encoding) { case 1: @@ -145,14 +194,13 @@ class MailFetcher { } //Convert text to desired encoding..defaults to utf8 - function mime_encode($text,$charset=null,$enc='utf-8') { //Thank in part to afterburner - - $encodings=array('UTF-8','WINDOWS-1251', 'ISO-8859-5', 'ISO-8859-1','KOI8-R'); - if(function_exists("iconv") and $text) { + function mime_encode($text, $charset=null, $enc='utf-8') { //Thank in part to afterburner + + if(function_exists('iconv') and $text) { if($charset) - return iconv($charset,$enc.'//IGNORE',$text); - elseif(function_exists("mb_detect_encoding")) - return iconv(mb_detect_encoding($text,$encodings),$enc,$text); + return iconv($charset, $enc.'//IGNORE', $text); + elseif(function_exists('mb_detect_encoding')) + return iconv(mb_detect_encoding($text, $this->encodings), $enc, $text); } return utf8_encode($text); @@ -161,15 +209,15 @@ class MailFetcher { //Generic decoder - mirrors imap_utf8 function mime_decode($text) { - $a = imap_mime_header_decode($text); $str = ''; - foreach ($a as $k => $part) + $parts = imap_mime_header_decode($text); + foreach ($parts as $part) $str.= $part->text; return $str?$str:imap_utf8($text); } - function getLastError(){ + function getLastError() { return imap_last_error(); } @@ -183,67 +231,128 @@ class MailFetcher { function getHeaderInfo($mid) { - $headerinfo=imap_headerinfo($this->mbox,$mid); - $sender=$headerinfo->from[0]; + if(!($headerinfo=imap_headerinfo($this->mbox, $mid)) || !$headerinfo->from) + return null; - //Parse what we need... - $header=array( - 'from' =>array('name' =>@$sender->personal,'email' =>strtolower($sender->mailbox).'@'.$sender->host), + $sender=$headerinfo->from[0]; + //Just what we need... + $header=array('name' =>@$sender->personal, + 'email' =>(strtolower($sender->mailbox).'@'.$sender->host), 'subject'=>@$headerinfo->subject, - 'mid' =>$headerinfo->message_id); + 'mid' =>$headerinfo->message_id + ); + return $header; } + function getAttachment($part) { + + if(!$part) return null; + + if($part->ifdisposition && in_array(strtolower($part->disposition), array('attachment', 'inline'))) + return $part->dparameters[0]->value; + + if($part->ifparameters && $part->type == 5) + return $part->parameters[0]->value; + + + return null; + } + //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) { if(!$struct && $mid) $struct=@imap_fetchstructure($this->mbox, $mid); + //Match the mime type. - if($struct && !$struct->ifdparameters && strcasecmp($mimeType,$this->getMimeType($struct))==0){ + if($struct && !$struct->ifdparameters && strcasecmp($mimeType, $this->getMimeType($struct))==0) { $partNumber=$partNumber?$partNumber:1; - if(($text=imap_fetchbody($this->mbox, $mid, $partNumber))){ + if(($text=imap_fetchbody($this->mbox, $mid, $partNumber))) { if($struct->encoding==3 or $struct->encoding==4) //base64 and qp decode. - $text=$this->decode($struct->encoding,$text); + $text=$this->decode($struct->encoding, $text); $charset=null; if($encoding) { //Convert text to desired mime encoding... - if($struct->ifparameters){ + if($struct->ifparameters) { if(!strcasecmp($struct->parameters[0]->attribute,'CHARSET') && strcasecmp($struct->parameters[0]->value,'US-ASCII')) $charset=trim($struct->parameters[0]->value); } - $text=$this->mime_encode($text,$charset,$encoding); + $text=$this->mime_encode($text, $charset, $encoding); } return $text; } } + //Do recursive search $text=''; - if($struct && $struct->parts){ + if($struct && $struct->parts) { 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)))) $text.=$result; } } + return $text; } - function getHeader($mid){ + /* + 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) { + + if($part && !$part->parts) { + //Check if the part is an attachment. + $filename = ''; + if($part->ifdisposition && in_array(strtolower($part->disposition), array('attachment', 'inline'))) + $filename = $part->dparameters[0]->value; + elseif($part->ifparameters && $part->type == 5) //inline image without disposition. + $filename = $part->parameters[0]->value; + + if($filename) { + return array( + array( + 'name' => $filename, + 'mime' => $this->getMimeType($part), + 'encoding' => $part->encoding, + 'index' => ($index?$index:1) + ) + ); + } + } + + //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 getPriority($mid){ + function getPriority($mid) { return Mail_Parse::parsePriority($this->getHeader($mid)); } function getBody($mid) { $body =''; - if(!($body = $this->getPart($mid,'TEXT/PLAIN',$this->charset))) { - if(($body = $this->getPart($mid,'TEXT/HTML',$this->charset))) { + if(!($body = $this->getPart($mid,'TEXT/PLAIN', $this->charset))) { + if(($body = $this->getPart($mid,'TEXT/HTML', $this->charset))) { //Convert tags of interest before we striptags $body=str_replace("</DIV><DIV>", "\n", $body); $body=str_replace(array("<br>", "<br />", "<BR>", "<BR />"), "\n", $body); @@ -251,171 +360,201 @@ class MailFetcher { $body=Format::striptags($body); //Strip tags?? } } + return $body; } - function createTicket($mid,$emailid=0){ - global $cfg, $ost; + //email to ticket + function createTicket($mid) { + global $ost; - $mailinfo=$this->getHeaderInfo($mid); + if(!($mailinfo = $this->getHeaderInfo($mid))) + return false; - //Make sure the email is NOT one of the undeleted emails. - if($mailinfo['mid'] && ($id=Ticket::getIdByMessageId(trim($mailinfo['mid']),$mailinfo['from']['email']))){ - //TODO: Move emails to a fetched folder when delete is false?? - return true; - } + //Make sure the email is NOT already fetched... (undeleted emails) + if($mailinfo['mid'] && ($id=Ticket::getIdByMessageId(trim($mailinfo['mid']), $mailinfo['email']))) + return true; //Reporting success so the email can be moved or deleted. //Is the email address banned? - if($mailinfo['from']['email'] && EmailFilter::isBanned($mailinfo['from']['email'])) { + if($mailinfo['email'] && EmailFilter::isBanned($mailinfo['email'])) { //We need to let admin know... - $ost->logWarning('Ticket denied', 'Banned email - '.$mailinfo['from']['email']); - return true; + $ost->logWarning('Ticket denied', 'Banned email - '.$mailinfo['email']); + return true; //Report success (moved or delete) } - - $var['name']=$this->mime_decode($mailinfo['from']['name']); - $var['email']=$mailinfo['from']['email']; + $emailId = $this->getEmailId(); + + $var['name']=$this->mime_decode($mailinfo['name']); + $var['email']=$mailinfo['email']; $var['subject']=$mailinfo['subject']?$this->mime_decode($mailinfo['subject']):'[No Subject]'; $var['message']=Format::stripEmptyLines($this->getBody($mid)); $var['header']=$this->getHeader($mid); - $var['emailId']=$emailid?$emailid:$cfg->getDefaultEmailId(); //ok to default? + $var['emailId']=$emailId?$emailId:$ost->getConfig()->getDefaultEmailId(); //ok to default? $var['name']=$var['name']?$var['name']:$var['email']; //No name? use email $var['mid']=$mailinfo['mid']; - if($cfg->useEmailPriority()) + if(!$var['message']) //An email with just attachments can have empty body. + $var['message'] = '(EMPTY)'; + + if($ost->getConfig()->useEmailPriority()) $var['priorityId']=$this->getPriority($mid); $ticket=null; $newticket=true; //Check the subject line for possible ID. - if(preg_match ("[[#][0-9]{1,10}]",$var['subject'],$regs)) { - $extid=trim(preg_replace("/[^0-9]/", "", $regs[0])); - $ticket= new Ticket(Ticket::getIdByExtId($extid)); + if($var['subject'] && preg_match ("[[#][0-9]{1,10}]", $var['subject'], $regs)) { + $tid=trim(preg_replace("/[^0-9]/", "", $regs[0])); //Allow mismatched emails?? For now NO. - if(!$ticket || strcasecmp($ticket->getEmail(),$var['email'])) + if(!($ticket=Ticket::lookupByExtId($tid)) || strcasecmp($ticket->getEmail(), $var['email'])) $ticket=null; } $errors=array(); - if(!$ticket) { - # Apply email filters for the new ticket - $ef = new EmailFilter($var); $ef->apply($var); - if(!($ticket=Ticket::create($var,$errors,'Email')) || $errors) - return null; - $msgid=$ticket->getLastMsgId(); - }else{ - $message=$var['message']; - //Strip quoted reply...TODO: figure out how mail clients do it without special tag.. - if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator()) && strpos($var['message'],$tag)) - list($message)=split($tag,$var['message']); - $msgid=$ticket->postMessage($message,'Email',$var['mid'],$var['header']); + if($ticket) { + if(!($msgid=$ticket->postMessage($var['message'], 'Email', $var['mid'], $var['header']))) + return false; + + } elseif (($ticket=Ticket::create($var, $errors, 'Email'))) { + $msgid = $ticket->getLastMsgId(); + } else { + //TODO: Log error.. + return null; } - //Save attachments if any. - if($msgid && $cfg->allowEmailAttachments()){ - if(($struct = imap_fetchstructure($this->mbox,$mid)) && $struct->parts) { - if($ticket->getLastMsgId()!=$msgid) - $ticket->setLastMsgId($msgid); - $this->saveAttachments($ticket,$mid,$struct); + //Save attachments if any. + if($msgid + && $ost->getConfig()->allowEmailAttachments() + && ($struct = imap_fetchstructure($this->mbox, $mid)) + && $struct->parts + && ($attachments=$this->getAttachments($struct))) { + + //We're just checking the type of file - not size or number of attachments... + // Restrictions are mainly due to PHP file uploads limitations + foreach($attachments as $a ) { + if($ost->isFileTypeAllowed($a['name'], $a['mime'])) { + $file = array( + 'name' => $a['name'], + 'type' => $a['mime'], + 'data' => $this->decode($a['encoding'], imap_fetchbody($this->mbox, $mid, $a['index'])) + ); + $ticket->saveAttachment($file, $msgid, 'M'); + } else { + //This should be really a comment on message - NoT an internal note. + //TODO: support comments on Messages and Responses. + $error = sprintf('Attachment %s [%s] rejected because of file type', $a['name'], $a['mime']); + $ticket->postNote('Email Attachment Rejected', $error, false); + $ost->logDebug('Email Attachment Rejected (Ticket #'.$ticket->getExtId().')', $error); + } } - } + } + return $ticket; } - function saveAttachments($ticket,$mid,$part,$index=0) { - global $ost; - if($part && $part->ifdparameters && ($filename=$part->dparameters[0]->value)){ //attachment - $index=$index?$index:1; - if($ticket - && $ost->isFileTypeAllowed($filename) - && $ost->getConfig()->getMaxFileSize()>=$part->bytes) { - //extract the attachments...and do the magic. - $data=$this->decode($part->encoding, imap_fetchbody($this->mbox,$mid,$index)); - $ticket->saveAttachment(array('name'=>$filename, 'data'=>$data),$ticket->getLastMsgId(),'M'); - return; - } - //TODO: Log failure?? - } + function fetchEmails() { - //Recursive attachment search! - if($part && $part->parts) { - foreach($part->parts as $k=>$struct) { - if($index) $prefix = $index.'.'; - $this->saveAttachments($ticket,$mid,$struct,$prefix.($k+1)); - } - } - } + if(!$this->connect()) + return false; - function fetchTickets($emailid,$max=20,$deletemsgs=false,$archivefolder){ + $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. Latest first. FILO. - if($this->createTicket($i,$emailid)){ - imap_setflag_full($this->mbox, imap_uid($this->mbox,$i), "\\Seen", ST_UID); //IMAP only?? - if((!$archivefolder || !imap_mail_move($this->mbox,$i,$archivefolder)) && $deletemsgs) - imap_delete($this->mbox,$i); + 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{ + } else { $errors++; } - if(($max && $msgs>=$max) || $errors>20) + + if($max && ($msgs>=$max || $errors>($max*0.8))) break; } + + //Warn on excessive errors + if($errors>$msgs) { + $warn=sprintf('Excessive errors processing emails for %s/%s. Please manually check the inbox.', + $this->getHost(), $this->getUsername()); + $this->log($warn); + } + @imap_expunge($this->mbox); return $msgs; } - function fetchMail(){ - global $ost, $cfg; + function log($error) { + global $ost; + $ost->logWarning('Mail Fetcher', $error); + } + + /* + MailFetcher::run() + + Static function called to initiate email polling + */ + function run() { + global $ost; - if(!$cfg->canFetchMail()) + 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='osTicket requires PHP IMAP extension enabled for IMAP/POP3 fetch to work!'; + $msg='osTicket requires PHP IMAP extension enabled for IMAP/POP3 email fetch to work!'; $ost->logWarning('Mail Fetch Error', $msg); return; } - $MAX_ERRORS=5; //Max errors before we start delayed fetch attempts - hardcoded for now. + //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_host,mail_port,mail_protocol,mail_encryption,mail_delete,mail_archivefolder,mail_errors,userid,userpass FROM '.EMAIL_TABLE. - ' WHERE mail_active=1 AND (mail_errors<='.$MAX_ERRORS.' OR (TIME_TO_SEC(TIMEDIFF(NOW(),mail_lasterror))>5*60) )'. - ' AND (mail_lastfetch IS NULL OR TIME_TO_SEC(TIMEDIFF(NOW(),mail_lastfetch))>mail_fetchfreq*60) '; - //echo $sql; - if(!($accounts=db_query($sql)) || !db_num_rows($accounts)) - return; + $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 DESC' + .' LIMIT 10'; //Processing up to 10 emails at a time. + + // echo $sql; + if(!($res=db_query($sql)) || !db_num_rows($res)) + return; /* Failed query (get's logged) or nothing to do... */ //TODO: Lock the table here?? - while($row=db_fetch_array($accounts)) { - $fetcher = new MailFetcher($row['userid'], Mcrypt::decrypt($row['userpass'],SECRET_SALT), - $row['mail_host'],$row['mail_port'],$row['mail_protocol'],$row['mail_encryption']); - if($fetcher->connect()){ - $fetcher->fetchTickets($row['email_id'],$row['mail_fetchmax'],$row['mail_delete']?true:false,$row['mail_archivefolder']); + + while(list($emailId, $errors)=db_fetch_row($res)) { + $fetcher = new MailFetcher($emailId); + if($fetcher->connect()) { + $fetcher->fetchEmails(); $fetcher->close(); - db_query('UPDATE '.EMAIL_TABLE.' SET mail_errors=0, mail_lastfetch=NOW() WHERE email_id='.db_input($row['email_id'])); - }else{ - $errors=$row['mail_errors']+1; - db_query('UPDATE '.EMAIL_TABLE.' SET mail_errors=mail_errors+1, mail_lasterror=NOW() WHERE email_id='.db_input($row['email_id'])); - if($errors>=$MAX_ERRORS){ + db_query('UPDATE '.EMAIL_TABLE.' SET mail_errors=0, mail_lastfetch=NOW() WHERE email_id='.db_input($emailId)); + } 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 - $msg="\nThe system is having trouble fetching emails from the following mail account: \n". - "\nUser: ".$row['userid']. - "\nHost: ".$row['mail_host']. + $msg="\nosTicket is having trouble fetching emails from the following mail account: \n". + "\nUser: ".$fetcher->getUsername(). + "\nHost: ".$fetcher->getHost(). "\nError: ".$fetcher->getLastError(). - "\n\n ".$errors.' consecutive errors. Maximum of '.$MAX_ERRORS. ' allowed'. - "\n\n This could be connection issues related to the host. Next delayed login attempt in aprox. 10 minutes"; + "\n\n ".$errors.' consecutive errors. Maximum of '.$MAXERRORS. ' allowed'. + "\n\n This could be connection issues related to the mail server. Next delayed login attempt in aprox. $TIMEOUT minutes"; $ost->alertAdmin('Mail Fetch Failure Alert', $msg, true); } } - } + } //end while. } } ?> diff --git a/include/class.mailparse.php b/include/class.mailparse.php index f56006fa9c1e35c88d67165302a7797f43f562d7..8d162e52a6d9182b6cca5284ff061538b4124ff9 100644 --- a/include/class.mailparse.php +++ b/include/class.mailparse.php @@ -140,7 +140,7 @@ class Mail_Parse { //Cleanup the html. $body=str_replace("</DIV><DIV>", "\n", $body); $body=str_replace(array("<br>", "<br />", "<BR>", "<BR />"), "\n", $body); - $body=Format::striptags($body); + $body=Format::striptags(Format::html($body)); } } return $body; diff --git a/include/class.ticket.php b/include/class.ticket.php index 104ff20b4517ef22d785b3f4f40ede84195134e9..da45d51935322d606f6e6b60b102c6c63ea363b8 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -1296,7 +1296,16 @@ class Ticket{ global $cfg; if(!$this->getId()) return 0; - + + + + //Strip quoted reply...on emailed replies + if(!strcasecmp($source, 'Email') + && $cfg->stripQuotedReply() + && ($tag=$cfg->getReplySeparator()) && strpos($msg, $tag)) + list($msg)=split($tag, $msg); + + # XXX: Refuse auto-response messages? (via email) XXX: No - but kill our auto-responder. $sql='INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW()' @@ -1316,8 +1325,7 @@ class Ticket{ .' SET message_id='.db_input($msgid) .', email_mid='.db_input($emsgid) .', headers='.db_input($headers); - - if (!db_query($sql)) return 0; + db_query($sql); } if($newticket) return $msgid; //Our work is done... diff --git a/include/client/open.inc.php b/include/client/open.inc.php index 42cf10c92d6eb3a45fdbf3775fc3cde7103d247b..2fd076100388888765970d1a561dc3c5235a8b90 100644 --- a/include/client/open.inc.php +++ b/include/client/open.inc.php @@ -122,7 +122,7 @@ $info=($_POST && $errors)?Format::htmlchars($_POST):$info; } ?> <?php - if($cfg && $cfg->enableCaptcha() && (!$thisclient || !$thisclient->isValid())) { + if($cfg && $cfg->isCaptchaEnabled() && (!$thisclient || !$thisclient->isValid())) { if($_POST && $errors && !$errors['captcha']) $errors['captcha']='Please re-enter the text again'; ?> diff --git a/include/staff/email.inc.php b/include/staff/email.inc.php index e16b350bdbd3a58f64862da9de74b4c835e9dc23..5e2935e06a02157c4cc7f62e45a38eb4af87326d 100644 --- a/include/staff/email.inc.php +++ b/include/staff/email.inc.php @@ -77,7 +77,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); Login Password </td> <td> - <input type="text" size="35" name="passwd" value="<?php echo $info['passwd']; ?>"> + <input type="password" size="35" name="passwd" value="<?php echo $info['passwd']; ?>"> <span class="error"> <?php echo $errors['passwd']; ?> </span> <br><em><?php echo $passwdtxt; ?></em> </td> @@ -118,10 +118,10 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <tr><td>Encryption</td> <td> - <label><input type="radio" name="mail_encryption" value="NONE" - <?php echo ($info['mail_encryption']!='SSL')?'checked="checked"':''; ?> />None</label> - <label><input type="radio" name="mail_encryption" value="SSL" - <?php echo ($info['mail_encryption']=='SSL')?'checked="checked"':''; ?> />SSL</label> + <select name="mail_encryption"> + <option value='NONE'>None</option> + <option value='SSL' <?php echo ($info['mail_encryption']=='SSL')?'selected="selected"':''; ?> >SSL</option> + </select> <font class="error"> <?php echo $errors['mail_encryption']; ?></font> </td> </tr> diff --git a/open.php b/open.php index 7bdc8fc8ffccfee37516c5ec60c0e1d16119df75..a7f064fb9ca58d26f4827b0abb8537df8ef4c3b2 100644 --- a/open.php +++ b/open.php @@ -22,7 +22,7 @@ if($_POST): if($thisclient) { $_POST['name']=$thisclient->getName(); $_POST['email']=$thisclient->getEmail(); - } elseif($cfg->enableCaptcha()) { + } elseif($cfg->isCaptchaEnabled()) { if(!$_POST['captcha']) $errors['captcha']='Enter text shown on the image'; elseif(strcmp($_SESSION['captcha'],md5($_POST['captcha']))) diff --git a/scp/autocron.php b/scp/autocron.php index 366351c9defedffd2bccd478b1d3c8bd2e349ba4..08b65ba3481e65f99e6bb06567c3be46efb60631 100644 --- a/scp/autocron.php +++ b/scp/autocron.php @@ -19,10 +19,10 @@ ignore_user_abort(1);//Leave me a lone bro! @set_time_limit(0); //useless when safe_mode is on $data=sprintf ("%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%", 71,73,70,56,57,97,1,0,1,0,128,255,0,192,192,192,0,0,0,33,249,4,1,0,0,0,0,44,0,0,0,0,1,0,1,0,0,2,2,68,1,0,59); -$datasize=strlen($data); + header('Content-type: image/gif'); header('Cache-Control: no-cache, must-revalidate'); -header("Content-Length: $datasize"); +header('Content-Length: '.strlen($data)); header('Connection: Close'); print $data; @@ -32,10 +32,10 @@ ob_start(); //Keep the image output clean. Hide our dirt. $sec=time()-$_SESSION['lastcroncall']; if($sec>180): //user can call cron once every 3 minutes. require_once(INCLUDE_DIR.'class.cron.php'); -Cron::TicketMonitor(); //Age tickets: We're going to age tickets ever regardless of cron settings. -if($cfg && $cfg->enableAutoCron()){ //ONLY fetch tickets if autocron is enabled! +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('Autocron', 'Cron job executed ['.$thisstaff->getUserName().']'); + $ost->logDebug('Auto Cron', 'Mail fetcher cron call ['.$thisstaff->getUserName().']'); } $_SESSION['lastcroncall']=time();