mambax7/xnewsletter

View on GitHub
include/phpmailer_bmh/class.phpmailer-bmh.php

Summary

Maintainability
F
4 days
Test Coverage
<?php
/* class.phpmailer-bmh.php
.---------------------------------------------------------------------------.
|  Software: PHPMailer-BMH (Bounce Mail Handler)                            |
|   Version: 5.0.0rc1                                                       |
|   Contact: codeworxtech@users.sourceforge.net                             |
|      Info: http://phpmailer.codeworxtech.com                              |
| ------------------------------------------------------------------------- |
|    Author: Andy Prevost andy.prevost@worxteam.com (admin)                 |
| Copyright (c) 2002-2009, Andy Prevost. All Rights Reserved.               |
| ------------------------------------------------------------------------- |
|   License: Distributed under the General Public License (GPL)             |
|            (http://www.gnu.org/licenses/gpl.html)                         |
| This program is distributed in the hope that it will be useful - WITHOUT  |
| ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or     |
| FITNESS FOR A PARTICULAR PURPOSE.                                         |
| ------------------------------------------------------------------------- |
| This is a update of the original Bounce Mail Handler script               |
| http://sourceforge.net/projects/bmh/                                      |
| The script has been renamed from Bounce Mail Handler to PHPMailer-BMH     |
| ------------------------------------------------------------------------- |
| We offer a number of paid services:                                       |
| - Web Hosting on highly optimized fast and secure servers                 |
| - Technology Consulting                                                   |
| - Oursourcing (highly qualified programmers and graphic designers)        |
'---------------------------------------------------------------------------'
Last updated: January 21 2009 13:49 EST */

/**
 * PHPMailer-BMH (Bounce Mail Handler)
 *
 * PHPMailer-BMH is a PHP program to check your IMAP/POP3 inbox and
 * delete all 'hard' bounced emails. It features a callback function where
 * you can create a custom action. This provides you the ability to write
 * a script to match your database records and either set inactive or
 * delete records with email addresses that match the 'hard' bounce results.
 *
 * @package   PHPMailer-BMH
 * @author    Andy Prevost
 * @copyright 2008-2009, Andy Prevost
 * @license   GPL licensed
 * @link      http://sourceforge.net/projects/bmh
 */
require_once dirname(dirname(dirname(dirname(__DIR__)))) . '/mainfile.php';
require_once XOOPS_ROOT_PATH . '/modules/xnewsletter/include/phpmailer_bmh/phpmailer-bmh_rules.php';

define('VERBOSE_QUIET', 0); // means no output at all
define('VERBOSE_SIMPLE', 1); // means only output simple report
define('VERBOSE_REPORT', 2); // means output a detail report
define('VERBOSE_DEBUG', 3); // means output detail report as well as debug info.

/**
 * Class BounceMailHandler
 */
class BounceMailHandler
{
    /////////////////////////////////////////////////
    // PROPERTIES, PUBLIC
    /////////////////////////////////////////////////

    /**
     * Holds Bounce Mail Handler version.
     *
     * @var string
     */
    public $Version = '5.0.0rc1 goffy';

    /**
     * Holds result of last processing.
     *
     * @var array
     */
    public $result_total       = 0;
    public $result_processed   = 0;
    public $result_unprocessed = 0;
    public $result_deleted     = 0;
    public $result_moved       = 0;

    /**
     * Mail server
     *
     * @var string
     */
    public $mailhost = 'localhost';

    /**
     * The username of mailbox
     *
     * @var string
     */

    public $mailbox_username;
    /**
     * The password needed to access mailbox
     *
     * @var string
     */
    public $mailbox_password;

    /**
     * The last error msg
     *
     * @var string
     */
    public $error_msg;

    /**
     * Maximum limit messages processed in one batch
     *
     * @var int
     */
    public $max_messages = 3000;

    /**
     * Callback Action function name
     * the function that handles the bounce mail. Parameters:
     *   int     $msgnum        the message number returned by Bounce Mail Handler
     *   string  $bounce_type   the bounce type: 'antispam','autoreply','concurrent','content_reject','command_reject','internal_error','defer','delayed'        => array('remove'=>0,'bounce_type'=>'temporary'),'dns_loop','dns_unknown','full','inactive','latin_only','other','oversize','outofoffice','unknown','unrecognized','user_reject','warning'
     *   string  $email         the target email address
     *   string  $subject       the subject, ignore now
     *   string  $xheader       the XBounceHeader from the mail
     *   1 or 0  $remove        delete status, 0 is not deleted, 1 is deleted
     *   string  $rule_no       bounce mail detect rule no.
     *   string  $rule_cat      bounce mail detect rule category
     *   int     $totalFetched  total number of messages in the mailbox
     *
     * @var string
     */
    public $action_function = 'callbackAction';

    /**
     * Internal variable
     * The resource handler for the opened mailbox (POP3/IMAP/NNTP/etc.)
     *
     * @var object
     */
    public $_mailbox_link = false;

    /**
     * Test mode, if true will not delete messages
     *
     * @var bool
     */
    public $testmode = false;

    /**
     * Purge the unknown messages (or not)
     *
     * @var bool
     */
    public $purge_unprocessed = false;

    /**
     * Control the debug output, default is VERBOSE_SIMPLE
     *
     * @var int
     */
    public $verbose = VERBOSE_SIMPLE;

    /**
     * control the failed DSN rules output
     *
     * @var bool
     */
    public $debug_dsn_rule = false;

    /**
     * control the failed BODY rules output
     *
     * @var bool
     */
    public $debug_body_rule = false;

    /**
     * Control the method to process the mail header
     * if set true, uses the imap_fetchstructure function
     * otherwise, detect message type directly from headers,
     * a bit faster than imap_fetchstructure function and take less resources.
     * however - the difference is negligible
     *
     * @var bool
     */
    public $use_fetchstructure = true;

    /**
     * If disable_delete is equal to true, it will disable the delete function
     *
     * @var bool
     */
    public $disable_delete = false;

    /*
     * Defines new line ending
     */
    public $bmh_newline = "<br>\n";

    /*
     * Defines port number, default is '143', other common choices are '110' (pop3), '993' (gmail)
     * @var integer
     */
    public $port = 143;

    /*
     * Defines service, default is 'imap', choice includes 'pop3'
     * @var string
     */
    public $service = 'imap';

    /*
     * Defines service option, default is 'notls', other choices are 'tls', 'ssl'
     * @var string
     */
    public $service_option = 'notls';

    /*
     * Mailbox type, default is 'INBOX', other choices are (Tasks, Spam, Replies, etc.)
     * @var string
     */
    public $boxname = 'INBOX';

    /*
     * Determines if soft bounces will be moved to another mailbox folder
     * @var boolean
     */
    public $moveSoft = false;

    /*
     * Mailbox folder to move soft bounces to, default is 'soft'
     * @var string
     */
    public $softMailbox = 'INBOX.soft';

    /*
     * Determines if this mail should be moved to softMailbox
     * @var boolean
     */
    public $moveSoftFlag = false;

    /*
     * Determines if hard bounces will be moved to another mailbox folder
     * NOTE: If true, this will disable delete and perform a move operation instead
     * @var boolean
     */
    public $moveHard = false;

    /*
     * Mailbox folder to move hard bounces to, default is 'hard'
     * @var string
     */
    public $hardMailbox = 'INBOX.hard';

    /*
     * Determines if this mail should be moved to hardMailbox
     * @var boolean
     */
    public $moveHardFlag = false;

    /*
     * Deletes messages globally prior to date in variable
     * NOTE: excludes any message folder that includes 'sent' in mailbox name
     * format is same as MySQL: 'yyyy-mm-dd'
     * if variable is blank, will not process global delete
     * @var string
     */
    public $deleteMsgDate = '';

    /////////////////////////////////////////////////
    // METHODS
    /////////////////////////////////////////////////

    /**
     * Output additional msg for debug
     *
     * @param bool|string $msg           ,  if not given, output the last error msg
     * @param int         $verbose_level ,  the output level of this message
     */
    public function output($msg = false, $verbose_level = VERBOSE_SIMPLE)
    {
        if ($this->verbose >= $verbose_level) {
            if (empty($msg)) {
                echo $this->error_msg . $this->bmh_newline;
            }
            //echo $msg . $this->bmh_newline;
        }
    }

    /**
     * Open a mail box
     *
     * @return bool
     */
    public function openMailbox()
    {
        // before starting the processing, let's check the delete flag and do global deletes if true
        if ('' != trim($this->deleteMsgDate)) {
            //echo "processing global delete based on date of " . $this->deleteMsgDate . "<br>";
            //$this->globalDelete($nameRaw);
            $this->globalDelete();
        }
        // disable move operations if server is Gmail ... Gmail does not support mailbox creation
        // if ( stristr($this->mailhost,'gmail') ) {
        // $this->moveSoft = false;
        // $this->moveHard = false;
        // }
        $port = $this->port . '/' . $this->service . '/' . $this->service_option;
        set_time_limit(6000);
        //echo "{".$this->mailhost.":".$port."}" . $this->boxname."<br>";
        if (!$this->testmode) {
            $this->_mailbox_link = imap_open('{' . $this->mailhost . ':' . $port . '}' . $this->boxname, $this->mailbox_username, $this->mailbox_password, CL_EXPUNGE);
        } else {
            $this->_mailbox_link = imap_open('{' . $this->mailhost . ':' . $port . '}' . $this->boxname, $this->mailbox_username, $this->mailbox_password);
        }
        if (!$this->_mailbox_link) {
            $this->error_msg = 'Cannot create ' . $this->service . ' connection to ' . $this->mailhost . $this->bmh_newline . 'Error MSG: ' . imap_last_error();
            $this->output();

            return false;
        }
        $this->output('Connected to: ' . $this->mailhost . ' (' . $this->mailbox_username . ')');

        return true;
    }

    /**
     * Open a mail box in local file system
     *
     * @param string $file_path (The local mailbox file path)
     *
     * @return bool
     */
    public function openLocal($file_path)
    {
        set_time_limit(6000);
        if (!$this->testmode) {
            $this->_mailbox_link = imap_open((string)$file_path, '', '', CL_EXPUNGE);
        } else {
            $this->_mailbox_link = imap_open((string)$file_path, '', '');
        }
        if (!$this->_mailbox_link) {
            $this->error_msg = 'Cannot open the mailbox file to ' . $file_path . $this->bmh_newline . 'Error MSG: ' . imap_last_error();
            $this->output();

            return false;
        }
        $this->output('Opened ' . $file_path);

        return true;
    }

    /**
     * Process the messages in a mailbox
     *
     * @param bool|string $max (maximum limit messages processed in one batch, if not given uses the property $max_messages
     *
     * @return bool
     */
    public function processMailbox($max = false)
    {
        if (empty($this->action_function) || !function_exists($this->action_function)) {
            $this->error_msg = 'Action function not found!';
            $this->output();

            return false;
        }

        if ($this->moveHard && (false === $this->disable_delete)) {
            $this->disable_delete = true;
        }

        if (!empty($max)) {
            $this->max_messages = $max;
        }

        // initialize counters
        $c_total       = imap_num_msg($this->_mailbox_link);
        $c_fetched     = $c_total;
        $c_processed   = 0;
        $c_unprocessed = 0;
        $c_deleted     = 0;
        $c_moved       = 0;
        $this->output('Total: ' . $c_total . ' messages ');
        // proccess maximum number of messages
        if ($c_fetched > $this->max_messages) {
            $c_fetched = $this->max_messages;
            $this->output('Processing first ' . $c_fetched . ' messages ');
        }

        if ($this->testmode) {
            $this->output('Running in test mode, not deleting messages from mailbox<br>');
        } else {
            if ($this->disable_delete) {
                if ($this->moveHard) {
                    $this->output('Running in move mode<br>');
                } else {
                    $this->output('Running in disable_delete mode, not deleting messages from mailbox<br>');
                }
            } else {
                $this->output('Processed messages will be deleted from mailbox<br>');
            }
        }
        for ($x = 1; $x <= $c_fetched; ++$x) {
            /*
            $this->output( $x . ":",VERBOSE_REPORT);
            if ($x % 10 == 0) {
              $this->output( '.',VERBOSE_SIMPLE);
            }
            */
            // fetch the messages one at a time
            if ($this->use_fetchstructure) {
                $structure = imap_fetchstructure($this->_mailbox_link, $x);
                if (1 == $structure->type
                    && $structure->ifsubtype
                    && 'REPORT' === $structure->subtype
                    && $structure->ifparameters
                    && $this->isParameter($structure->parameters, 'REPORT-TYPE', 'delivery-status')) {
                    $processed = $this->processBounce($x, 'DSN', $c_total);
                } else { // not standard DSN msg
                    $this->output('Msg #' . $x . ' is not a standard DSN message', VERBOSE_REPORT);
                    if ($this->debug_body_rule) {
                        $this->output("  Content-Type : {$match[1]}", VERBOSE_DEBUG);
                    }
                    $processed = $this->processBounce($x, 'BODY', $c_total);
                }
            } else {
                $header = imap_fetchheader($this->_mailbox_link, $x);
                // Could be multi-line, if the new line begins with SPACE or HTAB
                if (preg_match("/Content-Type:((?:[^\n]|\n[\t ])+)(?:\n[^\t ]|$)/is", $header, $match)) {
                    if (preg_match("/multipart\/report/is", $match[1])
                        && preg_match("/report-type=[\"']?delivery-status[\"']?/is", $match[1])) {
                        // standard DSN msg
                        $processed = $this->processBounce($x, 'DSN', $c_total);
                    } else { // not standard DSN msg
                        $this->output('Msg #' . $x . ' is not a standard DSN message', VERBOSE_REPORT);
                        if ($this->debug_body_rule) {
                            $this->output("  Content-Type : {$match[1]}", VERBOSE_DEBUG);
                        }
                        $processed = $this->processBounce($x, 'BODY', $c_total);
                    }
                } else { // didn't get content-type header
                    $this->output('Msg #' . $x . ' is not a well-formatted MIME mail, missing Content-Type', VERBOSE_REPORT);
                    if ($this->debug_body_rule) {
                        $this->output('  Headers: ' . $this->bmh_newline . $header . $this->bmh_newline, VERBOSE_DEBUG);
                    }
                    $processed = $this->processBounce($x, 'BODY', $c_total);
                }
            }

            $deleteFlag[$x] = false;
            $moveFlag[$x]   = false;
            if ($processed) {
                ++$c_processed;
                if ((false === $this->testmode) && (false === $this->disable_delete)) {
                    // delete the bounce if not in test mode and not in disable_delete mode
                    @imap_delete($this->_mailbox_link, $x);
                    $deleteFlag[$x] = true;
                    ++$c_deleted;
                } elseif ($this->moveHard && $this->moveHardFlag) {
                    // check if the move directory exists, if not create it
                    $this->mailbox_exist($this->hardMailbox);
                    // move the message
                    @imap_mail_move($this->_mailbox_link, $x, $this->hardMailbox);
                    $moveFlag[$x] = true;
                    ++$c_moved;
                } elseif ($this->moveSoft && $this->moveSoftFlag) {
                    // check if the move directory exists, if not create it
                    $this->mailbox_exist($this->softMailbox);
                    // move the message
                    @imap_mail_move($this->_mailbox_link, $x, $this->softMailbox);
                    $moveFlag[$x] = true;
                    ++$c_moved;
                }
            } else { // not processed
                ++$c_unprocessed;
                if (!$this->testmode && !$this->disable_delete && $this->purge_unprocessed) {
                    // delete this bounce if not in test mode, not in disable_delete mode, and the flag BOUNCE_PURGE_UNPROCESSED is set
                    @imap_delete($this->_mailbox_link, $x);
                    $deleteFlag[$x] = true;
                    ++$c_deleted;
                }
            }
            flush();
        }
        $this->output($this->bmh_newline . 'Closing mailbox, and purging messages');
        imap_close($this->_mailbox_link);
        $this->output('Read: ' . $c_fetched . ' messages');
        $this->output($c_processed . ' action taken');
        $this->output($c_unprocessed . ' no action taken');
        $this->output($c_deleted . ' messages deleted');
        $this->output($c_moved . ' messages moved');

        $this->result_total       = $c_fetched;
        $this->result_processed   = $c_processed;
        $this->result_unprocessed = $c_unprocessed;
        $this->result_deleted     = $c_deleted;
        $this->result_moved       = $c_moved;

        return true;
    }

    /**
     * Function to determine if a particular value is found in a imap_fetchstructure key
     *
     * @param array  $currParameters (imap_fetstructure parameters)
     * @param string $varKey         (imap_fetstructure key)
     * @param string $varValue       (value to check for)
     *
     * @return bool
     */
    public function isParameter($currParameters, $varKey, $varValue)
    {
        foreach ($currParameters as $key => $value) {
            if ($key == $varKey) {
                if ($value == $varValue) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Function to process each individual message
     *
     * @param int    $pos          (message number)
     * @param string $type         (DNS or BODY type)
     * @param string $totalFetched (total number of messages in mailbox)
     *
     * @return bool
     */
    public function processBounce($pos, $type, $totalFetched)
    {
        $header  = imap_headerinfo($this->_mailbox_link, $pos);
        $subject = strip_tags($header->subject);
        if ('DSN' === $type) {
            // first part of DSN (Delivery Status Notification), human-readable explanation
            $dsn_msg           = imap_fetchbody($this->_mailbox_link, $pos, '1');
            $dsn_msg_structure = imap_bodystruct($this->_mailbox_link, $pos, '1');

            if (4 == $dsn_msg_structure->encoding) {
                $dsn_msg = quoted_printable_decode($dsn_msg);
            } elseif (3 == $dsn_msg_structure->encoding) {
                $dsn_msg = base64_decode($dsn_msg, true);
            }

            // second part of DSN (Delivery Status Notification), delivery-status
            $dsn_report = imap_fetchbody($this->_mailbox_link, $pos, '2');

            // process bounces by rules
            $result = bmhDSNRules($dsn_msg, $dsn_report, $this->debug_dsn_rule);
        } elseif ('BODY' === $type) {
            $structure = imap_fetchstructure($this->_mailbox_link, $pos);
            switch ($structure->type) {
                case 0: // Content-type = text
                case 1: // Content-type = multipart
                    $body = imap_fetchbody($this->_mailbox_link, $pos, '1');
                    // Detect encoding and decode - only base64
                    if (4 == $structure->parts[0]->encoding) {
                        $body = quoted_printable_decode($body);
                    } elseif (3 == $structure->parts[0]->encoding) {
                        $body = base64_decode($body, true);
                    }
                    $result = bmhBodyRules($body, $structure, $this->debug_body_rule);
                    break;
                case 2: // Content-type = message
                    $body = imap_body($this->_mailbox_link, $pos);
                    if (4 == $structure->encoding) {
                        $body = quoted_printable_decode($body);
                    } elseif (3 == $structure->encoding) {
                        $body = base64_decode($body, true);
                    }
                    $body   = mb_substr($body, 0, 1000);
                    $result = bmhBodyRules($body, $structure, $this->debug_body_rule);
                    break;
                default: // unsupport Content-type
                    $this->output('Msg #' . $pos . ' is unsupported Content-Type:' . $structure->type, VERBOSE_REPORT);

                    return false;
            }
        } else { // internal error
            $this->error_msg = 'Internal Error: unknown type';

            return false;
        }
        $email              = $result['email'];
        $bounce_type        = $result['bounce_type'];
        $this->moveHardFlag = false;
        $this->moveSoftFlag = false;
        if ($this->moveHard && 1 == $result['remove']) {
            $remove             = 'moved (hard)';
            $this->moveHardFlag = true;
        } elseif ($this->moveSoft && 2 == $result['remove']) {
            $remove             = 'moved (soft)';
            $this->moveSoftFlag = true;
        } elseif ($this->disable_delete) {
            $remove = 0;
        } else {
            $remove = $result['remove'];
        }
        $rule_no  = $result['rule_no'];
        $rule_cat = $result['rule_cat'];
        $xheader  = false;

        if ('0000' == $rule_no) { // internal error      return false;
            // code below will use the Callback function, but return no value
            if ('' == trim($email)) {
                $email = $header->fromaddress;
            }
            $params = [
                $pos,
                $bounce_type,
                $email,
                $subject,
                $xheader,
                $remove,
                $rule_no,
                $rule_cat,
                $totalFetched,
            ];
            call_user_func_array($this->action_function, $params);
        } else { // match rule, do bounce action
            if ($this->testmode) {
                $this->output('Match: ' . $rule_no . ':' . $rule_cat . '; ' . $bounce_type . '; ' . $email);

                return true;
            }
            $params = [
                $pos,
                $bounce_type,
                $email,
                $subject,
                $xheader,
                $remove,
                $rule_no,
                $rule_cat,
                $totalFetched,
            ];

            return call_user_func_array($this->action_function, $params);
        }

        return null;
    }

    /**
     * Function to check if a mailbox exists
     * - if not found, it will create it
     *
     * @param string $mailbox (the mailbox name, must be in 'INBOX.checkmailbox' format)
     * @param bool   $create  (whether or not to create the checkmailbox if not found, defaults to true)
     *
     * @return null|bool
     */
    public function mailbox_exist($mailbox, $create = true)
    {
        if ('' == trim($mailbox) || false === mb_strpos($mailbox, 'INBOX.')) {
            // this is a critical error with either the mailbox name blank or an invalid mailbox name
            // need to stop processing and exit at this point
            echo "Invalid mailbox name for move operation. Cannot continue.<br>\n";
            echo "TIP: the mailbox you want to move the message to must include 'INBOX.' at the start.<br>\n";
            exit();
        }
        $port         = $this->port . '/' . $this->service . '/' . $this->service_option;
        $mbox         = imap_open('{' . $this->mailhost . ':' . $port . '}', $this->mailbox_username, $this->mailbox_password, OP_HALFOPEN);
        $list         = imap_getmailboxes($mbox, '{' . $this->mailhost . ':' . $port . '}', '*');
        $mailboxFound = false;
        if (is_array($list)) {
            foreach ($list as $key => $val) {
                // get the mailbox name only
                $nameArr = explode('}', imap_utf7_decode($val->name));
                $nameRaw = $nameArr[count($nameArr) - 1];
                if ($mailbox == $nameRaw) {
                    $mailboxFound = true;
                }
            }
            if ((false === $mailboxFound) && $create) {
                @imap_createmailbox($mbox, imap_utf7_encode('{' . $this->mailhost . ':' . $port . '}' . $mailbox));
                imap_close($mbox);

                return true;
            }
            imap_close($mbox);

            return false;
        }
        imap_close($mbox);

        return false;
    }

    /**
     * Function to delete messages in a mailbox, based on date
     * NOTE: this is global ... will affect all mailboxes except any that have 'sent' in the mailbox name
     *
     * @internal param string $mailbox (the mailbox name)
     */
    public function globalDelete()
    {
        $dateArr = explode('-', $this->deleteMsgDate); // date format is yyyy-mm-dd
        $delDate = mktime(0, 0, 0, $dateArr[1], $dateArr[2], $dateArr[0]);

        $port         = $this->port . '/' . $this->service . '/' . $this->service_option;
        $mboxt        = imap_open('{' . $this->mailhost . ':' . $port . '}', $this->mailbox_username, $this->mailbox_password, OP_HALFOPEN);
        $list         = imap_getmailboxes($mboxt, '{' . $this->mailhost . ':' . $port . '}', '*');
        $mailboxFound = false;
        if (is_array($list)) {
            foreach ($list as $key => $val) {
                // get the mailbox name only
                $nameArr = explode('}', imap_utf7_decode($val->name));
                $nameRaw = $nameArr[count($nameArr) - 1];
                if (false === mb_stripos($nameRaw, 'sent')) {
                    $mboxd    = imap_open('{' . $this->mailhost . ':' . $port . '}' . $nameRaw, $this->mailbox_username, $this->mailbox_password, CL_EXPUNGE);
                    $messages = imap_sort($mboxd, SORTDATE, 0);
                    $i        = 0;
                    $check    = imap_mailboxmsginfo($mboxd);
                    foreach ($messages as $message) {
                        $header = imap_headerinfo($mboxd, $message);
                        $fdate  = date('F j, Y', $header->udate);
                        // purge if prior to global delete date
                        if ($header->udate < $delDate) {
                            imap_delete($mboxd, $message);
                        }
                        ++$i;
                    }
                    imap_expunge($mboxd);
                    imap_close($mboxd);
                }
            }
        }
    }
}