Skip to content
Snippets Groups Projects
class.mailfetch.php 15.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • Jared Hancock's avatar
    Jared Hancock committed
    <?php
    /*********************************************************************
        class.mailfetch.php
    
        mail fetcher class. Uses IMAP ext for now.
    
        Peter Rotich <peter@osticket.com>
        Copyright (c)  2006-2012 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.filter.php');
    
    class MailFetcher {
        var $hostname;
        var $username;
        var $password;
    
        var $port;
        var $protocol;
        var $encryption;
    
        var $mbox;
    
        var $charset= 'UTF-8';
        
        function MailFetcher($username,$password,$hostname,$port,$protocol,$encryption='') {
    
            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;
    
            $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.
        }
        
        function connect() {
            return $this->open()?true:false;
        }
    
        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);
            
            return $this->mbox;
        }
    
        function close() {
            imap_close($this->mbox,CL_EXPUNGE);
        }
    
        function mailcount(){
            return count(imap_headers($this->mbox));
        }
    
        //Get mail boxes.
        function getMailboxes(){
    
    
            $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)));
                }
            }
    
            return $list;
        }
    
        //Create a 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));
        }
    
        /* check if a folder exits - create on if requested */
        function checkMailbox($folder,$create=false){
    
            if(($mailboxes=$this->getMailboxes()) && in_array($folder,$mailboxes))
                return true;
    
            return ($create && $this->createMailbox($folder));
        }
    
    
        function decode($encoding,$text) {
    
            switch($encoding) {
                case 1:
                $text=imap_8bit($text);
                break;
                case 2:
                $text=imap_binary($text);
                break;
                case 3:
                $text=imap_base64($text);
                break;
                case 4:
                $text=imap_qprint($text);
                break;
                case 5:
                default:
                 $text=$text;
            } 
            return $text;
        }
    
        //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) {
                if($charset)
                    return iconv($charset,$enc.'//IGNORE',$text);
                elseif(function_exists("mb_detect_encoding"))
                    return iconv(mb_detect_encoding($text,$encodings),$enc,$text);
            }
    
            return utf8_encode($text);
        }
        
        //Generic decoder - mirrors imap_utf8
        function mime_decode($text) {
            
            $a = imap_mime_header_decode($text);
            $str = '';
            foreach ($a as $k => $part)
                $str.= $part->text;
            
            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) {
            
            $headerinfo=imap_headerinfo($this->mbox,$mid);
            $sender=$headerinfo->from[0];
    
            //Parse what we need...
            $header=array(
                          'from'   =>array('name'  =>@$sender->personal,'email' =>strtolower($sender->mailbox).'@'.$sender->host),
                          'subject'=>@$headerinfo->subject,
                          'mid'    =>$headerinfo->message_id);
            return $header;
        }
    
        //search for specific mime type parts....encoding is the desired encoding.
        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){
                $partNumber=$partNumber?$partNumber:1;
                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);
    
                    $charset=null;
                    if($encoding) { //Convert text to desired mime encoding...
                        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);
                    }
                    return $text;
                }
            }
            //Do recursive search
            $text='';
            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))))
                        $text.=$result;
                }
            }
            return $text;
        }
    
        function getHeader($mid){
            return imap_fetchheader($this->mbox, $mid,FT_PREFETCHTEXT);
        }
    
        
        function getPriority($mid){
            return Mail_Parse::parsePriority($this->getHeader($mid));
        }
    
        function getBody($mid) {
            
            $body ='';
    
            if(!($body = $this->getPart($mid,'TEXT/PLAIN',$this->charset))) {
    
    Jared Hancock's avatar
    Jared Hancock committed
                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);
                    $body=Format::html($body); //Balance html tags before stripping.
                    $body=Format::striptags($body); //Strip tags??
                }
            }
            return $body;
        }
    
        function createTicket($mid,$emailid=0){
    
    Jared Hancock's avatar
    Jared Hancock committed
    
            $mailinfo=$this->getHeaderInfo($mid);
    
            //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;
            }
    
    
    	    //Is the email address banned?
    
    Jared Hancock's avatar
    Jared Hancock committed
            if($mailinfo['from']['email'] && EmailFilter::isBanned($mailinfo['from']['email'])) {
    
    	        //We need to let admin know...
                $ost->logWarning('Ticket denied', 'Banned email - '.$mailinfo['from']['email']);
    	        return true;
    
    Jared Hancock's avatar
    Jared Hancock committed
            }
    	
    
            $var['name']=$this->mime_decode($mailinfo['from']['name']);
            $var['email']=$mailinfo['from']['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['name']=$var['name']?$var['name']:$var['email']; //No name? use email
            $var['mid']=$mailinfo['mid'];
    
            if($cfg->useEmailPriority())
    
                $var['priorityId']=$this->getPriority($mid);
    
    Jared Hancock's avatar
    Jared Hancock committed
           
            $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));
                //Allow mismatched emails?? For now NO.
                if(!$ticket || 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']);
            }
            //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);
    
                }
            } 
            return $ticket;
        }
    
        function saveAttachments($ticket,$mid,$part,$index=0) {
    
    Jared Hancock's avatar
    Jared Hancock committed
    
            if($part && $part->ifdparameters && ($filename=$part->dparameters[0]->value)){ //attachment
                $index=$index?$index:1;
    
                if($ticket 
                        && $ost->isFileTypeAllowed($filename) 
                        && $ost->getConfig()->getMaxFileSize()>=$part->bytes) {
    
    Jared Hancock's avatar
    Jared Hancock committed
                    //extract the attachments...and do the magic.
                    $data=$this->decode($part->encoding, imap_fetchbody($this->mbox,$mid,$index));
    
    Bastian Kuhn's avatar
    Bastian Kuhn committed
                    $ticket->saveAttachment(array('name'=>$filename, 'data'=>$data),$ticket->getLastMsgId(),'M');
    
    Jared Hancock's avatar
    Jared Hancock committed
                    return;
                }
                //TODO: Log failure??
            }
    
            //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));
                }
            }
    
        }
    
    
        function fetchTickets($emailid,$max=20,$deletemsgs=false,$archivefolder){
    
    Jared Hancock's avatar
    Jared Hancock committed
    
            $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)
    
    Jared Hancock's avatar
    Jared Hancock committed
                        imap_delete($this->mbox,$i);
                    $msgs++;
                    $errors=0; //We are only interested in consecutive errors.
                }else{
                    $errors++;
                }
                if(($max && $msgs>=$max) || $errors>20)
                    break;
            }
            @imap_expunge($this->mbox);
    
            return $msgs;
        }
    
        function fetchMail(){
    
    Jared Hancock's avatar
    Jared Hancock committed
          
            if(!$cfg->canFetchMail())
                return;
    
            //We require imap ext to fetch emails via IMAP/POP3
            if(!function_exists('imap_open')) {
    
                $msg='osTicket requires PHP IMAP extension enabled for IMAP/POP3 fetch to work!';
                $ost->logWarning('Mail Fetch Error', $msg);
    
    Jared Hancock's avatar
    Jared Hancock committed
                return;
            }
    
            $MAX_ERRORS=5; //Max errors before we start delayed fetch attempts - hardcoded for now.
    
    
            $sql=' SELECT email_id,mail_host,mail_port,mail_protocol,mail_encryption,mail_delete,mail_archivefolder,mail_errors,userid,userpass FROM '.EMAIL_TABLE.
    
    Jared Hancock's avatar
    Jared Hancock committed
                 ' 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;
    
            //TODO: Lock the table here??
            while($row=db_fetch_array($accounts)) {
    
                $fetcher = new MailFetcher($row['userid'], Mcrypt::decrypt($row['userpass'],SECRET_SALT),
    
    Jared Hancock's avatar
    Jared Hancock committed
                                           $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']);
    
    Jared Hancock's avatar
    Jared Hancock committed
                    $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){
                        //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'].
                            "\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";
    
                        $ost->alertAdmin('Mail Fetch Failure Alert', $msg, true);