include/phpmailer_bmh/class.phpmailer-bmh.php
<?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);
}
}
}
}
}