Skip to content
Snippets Groups Projects
class.mailfetch.php 20.5 KiB
Newer Older
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-2013 osTicket
Jared Hancock's avatar
Jared Hancock committed
    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');
Peter Rotich's avatar
Peter Rotich committed
require_once(INCLUDE_DIR.'class.email.php');
Jared Hancock's avatar
Jared Hancock committed
require_once(INCLUDE_DIR.'class.filter.php');

class MailFetcher {

Peter Rotich's avatar
Peter Rotich committed
    var $ht;
Jared Hancock's avatar
Jared Hancock committed

    var $mbox;
Peter Rotich's avatar
Peter Rotich committed
    var $srvstr;
Peter Rotich's avatar
Peter Rotich committed
    var $charset = 'UTF-8';
Peter Rotich's avatar
Peter Rotich committed
    function MailFetcher($email, $charset='UTF-8') {
Peter Rotich's avatar
Peter Rotich committed
        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
Peter Rotich's avatar
Peter Rotich committed
        else
            $this->ht = null;
Peter Rotich's avatar
Peter Rotich committed
        $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';
Peter Rotich's avatar
Peter Rotich committed
            $this->srvstr.='/novalidate-cert}';
Peter Rotich's avatar
Peter Rotich committed
        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'];
Peter Rotich's avatar
Peter Rotich committed
    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->mailbox_encode($this->ht['archive_folder']);
Peter Rotich's avatar
Peter Rotich committed
    }

    /* Core */
Jared Hancock's avatar
Jared Hancock committed
    function connect() {
Peter Rotich's avatar
Peter Rotich committed
        return ($this->mbox && $this->ping())?$this->mbox:$this->open();
Peter Rotich's avatar
Peter Rotich committed
    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') {
Peter Rotich's avatar
Peter Rotich committed
           $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(NULL, 0, array(
                'DISABLE_AUTHENTICATOR' => array('GSSAPI', 'NTLM')));

        $this->mbox = call_user_func_array('imap_open', $args);
Peter Rotich's avatar
Peter Rotich committed

Jared Hancock's avatar
Jared Hancock committed
        return $this->mbox;
    }

Peter Rotich's avatar
Peter Rotich committed
    function close($flag=CL_EXPUNGE) {
        imap_close($this->mbox, $flag);
Peter Rotich's avatar
Peter Rotich committed
    function mailcount() {
Jared Hancock's avatar
Jared Hancock committed
        return count(imap_headers($this->mbox));
    }

    //Get mail boxes.
Peter Rotich's avatar
Peter Rotich committed
    function getMailboxes() {
Peter Rotich's avatar
Peter Rotich committed
        if(!($folders=imap_list($this->mbox, $this->srvstr, "*")) || !is_array($folders))
            return null;
Peter Rotich's avatar
Peter Rotich committed
        $list = array();
Peter Rotich's avatar
Peter Rotich committed
        foreach($folders as $folder)
Peter Rotich's avatar
Peter Rotich committed
            $list[]= str_replace($this->srvstr, '', imap_utf7_decode(trim($folder)));
Jared Hancock's avatar
Jared Hancock committed

        return $list;
    }

    //Create a folder.
Peter Rotich's avatar
Peter Rotich committed
    function createMailbox($folder) {
Jared Hancock's avatar
Jared Hancock committed

        if(!$folder) return false;
        return imap_createmailbox($this->mbox,
           $this->srvstr.$this->mailbox_encode(trim($folder)));
Peter Rotich's avatar
Peter Rotich committed
    /* check if a folder exists - create one if requested */
    function checkMailbox($folder, $create=false) {
Peter Rotich's avatar
Peter Rotich committed
        if(($mailboxes=$this->getMailboxes()) && in_array(trim($folder), $mailboxes))
Jared Hancock's avatar
Jared Hancock committed
            return true;

        return ($create && $this->createMailbox($folder));
    }


    function decode($text, $encoding) {
Jared Hancock's avatar
Jared Hancock committed

        switch($encoding) {
            case 1:
            $text=imap_8bit($text);
            break;
            case 2:
            $text=imap_binary($text);
            break;
            case 3:
            // 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);
Jared Hancock's avatar
Jared Hancock committed
            break;
            case 4:
            $text=imap_qprint($text);
            break;
Jared Hancock's avatar
Jared Hancock committed
        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 Format::encode($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);
    }

    //Generic decoder - resulting text is utf8 encoded -> mirrors imap_utf8
    function mime_decode($text, $encoding='utf-8') {
Jared Hancock's avatar
Jared Hancock committed
        $str = '';
Peter Rotich's avatar
Peter Rotich committed
        $parts = imap_mime_header_decode($text);
Peter Rotich's avatar
Peter Rotich committed
        foreach ($parts as $part)
            $str.= $this->mime_encode($part->text, $part->charset, $encoding);
Jared Hancock's avatar
Jared Hancock committed
        return $str?$str:imap_utf8($text);
    }

Peter Rotich's avatar
Peter Rotich committed
    function getLastError() {
Jared Hancock's avatar
Jared Hancock committed
        return imap_last_error();
    }

    function getMimeType($struct) {
        $mimeType = array('TEXT', 'MULTIPART', 'MESSAGE', 'APPLICATION', 'AUDIO', 'IMAGE', 'VIDEO', 'OTHER');
        if(!$struct || !$struct->subtype)
            return 'TEXT/PLAIN';
Jared Hancock's avatar
Jared Hancock committed
        return $mimeType[(int) $struct->type].'/'.$struct->subtype;
    }

    function getHeaderInfo($mid) {
Peter Rotich's avatar
Peter Rotich committed
        if(!($headerinfo=imap_headerinfo($this->mbox, $mid)) || !$headerinfo->from)
            return null;
Peter Rotich's avatar
Peter Rotich committed
        $sender=$headerinfo->from[0];
        //Just what we need...
        $header=array('name'  =>@$sender->personal,
                      'email'  => trim(strtolower($sender->mailbox).'@'.$sender->host),
Jared Hancock's avatar
Jared Hancock committed
                      'subject'=>@$headerinfo->subject,
                      'mid'    => trim(@$headerinfo->message_id),
                      'header' => $this->getHeader($mid),
                      'in-reply-to' => $headerinfo->in_reply_to,
                      'references' => $headerinfo->references,
        if ($replyto = $headerinfo->reply_to) {
            $header['reply-to'] = $replyto[0]->mailbox.'@'.$replyto[0]->host;
            $header['reply-to-name'] = $replyto[0]->personal;
        }

        //Try to determine target email - useful when fetched inbox has
        // aliases that are independent emails within osTicket.
        $emailId = 0;
        $tolist = array();
        if($headerinfo->to)
            $tolist = array_merge($tolist, $headerinfo->to);
        if($headerinfo->cc)
            $tolist = array_merge($tolist, $headerinfo->cc);
        if($headerinfo->bcc)
            $tolist = array_merge($tolist, $headerinfo->bcc);

        foreach($tolist as $addr)
            if(($emailId=Email::getIdByEmail(strtolower($addr->mailbox).'@'.$addr->host)))
                break;

        $header['emailId'] = $emailId;

        // 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'])
            if (!($header['mid'] = Mail_Parse::findHeaderEntry($header['header'],
                    'message-id')))
                $header['mid'] = '<' . md5($header['header']) . '@local>';

Jared Hancock's avatar
Jared Hancock committed
        return $header;
    }

    //search for specific mime type parts....encoding is the desired encoding.
Peter Rotich's avatar
Peter Rotich committed
    function getPart($mid, $mimeType, $encoding=false, $struct=null, $partNumber=false) {
Jared Hancock's avatar
Jared Hancock committed
        if(!$struct && $mid)
            $struct=@imap_fetchstructure($this->mbox, $mid);
Peter Rotich's avatar
Peter Rotich committed

Jared Hancock's avatar
Jared Hancock committed
        //Match the mime type.
Peter Rotich's avatar
Peter Rotich committed
        if($struct && !$struct->ifdparameters && strcasecmp($mimeType, $this->getMimeType($struct))==0) {
Jared Hancock's avatar
Jared Hancock committed
            $partNumber=$partNumber?$partNumber:1;
Peter Rotich's avatar
Peter Rotich committed
            if(($text=imap_fetchbody($this->mbox, $mid, $partNumber))) {
Jared Hancock's avatar
Jared Hancock committed
                if($struct->encoding==3 or $struct->encoding==4) //base64 and qp decode.
                    $text=$this->decode($text, $struct->encoding);
Jared Hancock's avatar
Jared Hancock committed

                $charset=null;
                if($encoding) { //Convert text to desired mime encoding...
Peter Rotich's avatar
Peter Rotich committed
                    if($struct->ifparameters) {
Jared Hancock's avatar
Jared Hancock committed
                        if(!strcasecmp($struct->parameters[0]->attribute,'CHARSET') && strcasecmp($struct->parameters[0]->value,'US-ASCII'))
                            $charset=trim($struct->parameters[0]->value);
                    }
Peter Rotich's avatar
Peter Rotich committed
                    $text=$this->mime_encode($text, $charset, $encoding);
Jared Hancock's avatar
Jared Hancock committed
                }
                return $text;
            }
        }
Peter Rotich's avatar
Peter Rotich committed

Jared Hancock's avatar
Jared Hancock committed
        //Do recursive search
        $text='';
Peter Rotich's avatar
Peter Rotich committed
        if($struct && $struct->parts) {
Jared Hancock's avatar
Jared Hancock committed
            while(list($i, $substruct) = each($struct->parts)) {
Jared Hancock's avatar
Jared Hancock committed
                    $prefix = $partNumber . '.';
Peter Rotich's avatar
Peter Rotich committed
                if(($result=$this->getPart($mid, $mimeType, $encoding, $substruct, $prefix.($i+1))))
Jared Hancock's avatar
Jared Hancock committed
                    $text.=$result;
            }
        }
Peter Rotich's avatar
Peter Rotich committed

Jared Hancock's avatar
Jared Hancock committed
        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;
    }

Peter Rotich's avatar
Peter Rotich committed
    /*
     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 = false;
            if ($part->ifdisposition
                    && in_array(strtolower($part->disposition),
                        array('attachment', 'inline'))) {
                $filename = $this->findFilename($part->dparameters);
            }
            // Inline attachments without disposition.
            if (!$filename && $part->ifparameters && $part->parameters
                    && $part->type > 0) {
                $filename = $this->findFilename($part->parameters);
Peter Rotich's avatar
Peter Rotich committed

            if($filename) {
                return array(
                        array(
                            'name'  => $this->mime_decode($filename),
                            'type'  => $this->getMimeType($part),
Peter Rotich's avatar
Peter Rotich committed
                            '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) {
Jared Hancock's avatar
Jared Hancock committed
        return imap_fetchheader($this->mbox, $mid,FT_PREFETCHTEXT);
    }

Peter Rotich's avatar
Peter Rotich committed
    function getPriority($mid) {
Jared Hancock's avatar
Jared Hancock committed
        return Mail_Parse::parsePriority($this->getHeader($mid));
    }

    function getBody($mid) {
Jared Hancock's avatar
Jared Hancock committed
        $body ='';
        if ($body = $this->getPart($mid,'TEXT/PLAIN', $this->charset))
            // The Content-Type was text/plain, so escape anything that
            // looks like HTML
            $body=Format::htmlchars($body);
        elseif ($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::safe_html($body); //Balance html tags & neutralize unsafe tags.
Peter Rotich's avatar
Peter Rotich committed

Jared Hancock's avatar
Jared Hancock committed
        return $body;
    }

    //email to ticket
    function createTicket($mid) {
Peter Rotich's avatar
Peter Rotich committed
        global $ost;
Peter Rotich's avatar
Peter Rotich committed
        if(!($mailinfo = $this->getHeaderInfo($mid)))
            return false;
	    //Is the email address banned?
        if($mailinfo['email'] && TicketFilter::isBanned($mailinfo['email'])) {
	        //We need to let admin know...
            $ost->logWarning('Ticket denied', 'Banned email - '.$mailinfo['email'], false);
Peter Rotich's avatar
Peter Rotich committed
	        return true; //Report success (moved or delete)
        $vars = $mailinfo;
        $vars['name']=$this->mime_decode($mailinfo['name']);
        $vars['subject']=$mailinfo['subject']?$this->mime_decode($mailinfo['subject']):'[No Subject]';
        $vars['message']=Format::stripEmptyLines($this->getBody($mid));
        $vars['emailId']=$mailinfo['emailId']?$mailinfo['emailId']:$this->getEmailId();
        //Missing FROM name  - use email address.
        if(!$vars['name'])
            $vars['name'] = $vars['email'];

        //An email with just attachments can have empty body.
        if(!$vars['message'])
            $vars['message'] = '-';
Peter Rotich's avatar
Peter Rotich committed

        if($ost->getConfig()->useEmailPriority())
            $vars['priorityId']=$this->getPriority($mid);
Jared Hancock's avatar
Jared Hancock committed
        $ticket=null;
        $newticket=true;
Jared Hancock's avatar
Jared Hancock committed
        $errors=array();
        if (($thread = ThreadEntry::lookupByEmailHeaders($vars))
                && ($message = $thread->postEmail($vars))) {
            if (!$message instanceof ThreadEntry)
                // Email has been processed previously
                return $message;
            $ticket = $message->getTicket();
        } elseif (($ticket=Ticket::create($vars, $errors, 'Email'))) {
            $message = $ticket->getLastMessage();
Peter Rotich's avatar
Peter Rotich committed
        } else {
            //Report success if the email was absolutely rejected.
            if(isset($errors['errno']) && $errors['errno'] == 403)
                return true;

            # check if it's a bounce!
            if($vars['header'] && TicketFilter::isAutoBounce($vars['header'])) {
                $ost->logWarning('Bounced email', $vars['message'], false);
Peter Rotich's avatar
Peter Rotich committed
            //TODO: Log error..
            return null;
Peter Rotich's avatar
Peter Rotich committed
        //Save attachments if any.
Peter Rotich's avatar
Peter Rotich committed
                && $ost->getConfig()->allowEmailAttachments()
                && ($struct = imap_fetchstructure($this->mbox, $mid))
Peter Rotich's avatar
Peter Rotich committed
                && ($attachments=$this->getAttachments($struct))) {
Peter Rotich's avatar
Peter Rotich committed
            foreach($attachments as $a ) {
                $file = array('name'  => $a['name'], 'type'  => $a['type']);

                //Check the file  type
                if(!$ost->isFileTypeAllowed($file))
                    $file['error'] = 'Invalid file type (ext) for '.Format::htmlchars($file['name']);
                else //only fetch the body if necessary TODO: Make it a callback.
                    $file['data'] = $this->decode(imap_fetchbody($this->mbox, $mid, $a['index']), $a['encoding']);

                $message->importAttachment($file);
Jared Hancock's avatar
Jared Hancock committed
        return $ticket;
    }


Peter Rotich's avatar
Peter Rotich committed
    function fetchEmails() {
Peter Rotich's avatar
Peter Rotich committed
        if(!$this->connect())
            return false;
Peter Rotich's avatar
Peter Rotich committed
        $archiveFolder = $this->getArchiveFolder();
        $delete = $this->canDeleteEmails();
        $max = $this->getMaxFetch();
Jared Hancock's avatar
Jared Hancock committed

        $nummsgs=imap_num_msg($this->mbox);
        //echo "New Emails:  $nummsgs\n";
        $msgs=$errors=0;
Peter Rotich's avatar
Peter Rotich committed
        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);

Jared Hancock's avatar
Jared Hancock committed
                $msgs++;
                $errors=0; //We are only interested in consecutive errors.
Peter Rotich's avatar
Peter Rotich committed
            } else {
Jared Hancock's avatar
Jared Hancock committed
                $errors++;
            }
Peter Rotich's avatar
Peter Rotich committed

            if($max && ($msgs>=$max || $errors>($max*0.8)))
Jared Hancock's avatar
Jared Hancock committed
                break;
        }
Peter Rotich's avatar
Peter Rotich committed

        //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);
        }

Jared Hancock's avatar
Jared Hancock committed
        @imap_expunge($this->mbox);

        return $msgs;
    }

    function log($error) {
        global $ost;
        $ost->logWarning('Mail Fetcher', $error);
    }
Peter Rotich's avatar
Peter Rotich committed

    /*
       MailFetcher::run()

       Static function called to initiate email polling
Peter Rotich's avatar
Peter Rotich committed
     */
    function run() {
        global $ost;
        if(!$ost->getConfig()->isEmailPollingEnabled())
Jared Hancock's avatar
Jared Hancock committed
            return;

        //We require imap ext to fetch emails via IMAP/POP3
Peter Rotich's avatar
Peter Rotich committed
        //We check here just in case the extension gets disabled post email config...
Jared Hancock's avatar
Jared Hancock committed
        if(!function_exists('imap_open')) {
Peter Rotich's avatar
Peter Rotich committed
            $msg='osTicket requires PHP IMAP extension enabled for IMAP/POP3 email fetch to work!';
            $ost->logWarning('Mail Fetch Error', $msg);
Peter Rotich's avatar
Peter Rotich committed
        $MAXERRORS = 5; //Max errors before we start delayed fetch attempts
        $TIMEOUT = 10; //Timeout in minutes after max errors is reached.
Peter Rotich's avatar
Peter Rotich committed
        $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... */
Jared Hancock's avatar
Jared Hancock committed

        //TODO: Lock the table here??
Peter Rotich's avatar
Peter Rotich committed

        while(list($emailId, $errors)=db_fetch_row($res)) {
            $fetcher = new MailFetcher($emailId);
            if($fetcher->connect()) {
                $fetcher->fetchEmails();
Jared Hancock's avatar
Jared Hancock committed
                $fetcher->close();
Peter Rotich's avatar
Peter Rotich committed
                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) {
Jared Hancock's avatar
Jared Hancock committed
                    //We've reached the MAX consecutive errors...will attempt logins at delayed intervals
Peter Rotich's avatar
Peter Rotich committed
                    $msg="\nosTicket is having trouble fetching emails from the following mail account: \n".
                        "\nUser: ".$fetcher->getUsername().
                        "\nHost: ".$fetcher->getHost().
Jared Hancock's avatar
Jared Hancock committed
                        "\nError: ".$fetcher->getLastError().
Peter Rotich's avatar
Peter Rotich committed
                        "\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 approx. $TIMEOUT minutes";
                    $ost->alertAdmin('Mail Fetch Failure Alert', $msg, true);
Peter Rotich's avatar
Peter Rotich committed
        } //end while.