From 165ce061bbcd3c65c5d7a52bfea2605f58d94083 Mon Sep 17 00:00:00 2001 From: Richard Bairwell Date: Tue, 16 Dec 2014 13:18:55 +0000 Subject: [PATCH 01/15] Fix issue #16 https://github.com/cfortune/PHP-Bounce-Handler/issues/16 --- bounce_driver.class.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bounce_driver.class.php b/bounce_driver.class.php index d9a140b..a06bdff 100644 --- a/bounce_driver.class.php +++ b/bounce_driver.class.php @@ -455,7 +455,9 @@ function contenttype_decode ($mimepart) { $line = substr($line, 0, -1); else $line .= "\r\n"; - $decoded .= preg_replace("/=([0-9A-F][0-9A-F])/e", 'chr(hexdec("$1"))', $line); + $decoded .= preg_replace_callback("/=([0-9A-F][0-9A-F])/", function($matches){ return chr(hexdec($matches[0])); } , $line); + + } case 'base64': { $decoded .= base64_decode($line); From 652d8e857bd7bda7b1514bf5134b95928a0768cf Mon Sep 17 00:00:00 2001 From: Richard Bairwell Date: Tue, 16 Dec 2014 15:32:14 +0000 Subject: [PATCH 02/15] Code formatting to PSR1/PSR2 and symfony styles. Added quite a few PHPDOC style comments --- Make_statuscodes.php | 60 +- bounce_driver.class.php | 1158 +++++++++++++++++++++++++-------------- bounce_responses.php | 608 +++++++++++++------- cmdlinetest.php | 51 +- testdriver1.php | 385 +++++++------ 5 files changed, 1397 insertions(+), 865 deletions(-) diff --git a/Make_statuscodes.php b/Make_statuscodes.php index ef16be8..b69e14f 100644 --- a/Make_statuscodes.php +++ b/Make_statuscodes.php @@ -10,21 +10,21 @@ print "#and smtp-enhanced-status-codes-3.csv\n\n"; $fh = fopen("http://www.iana.org/assignments/smtp-enhanced-status-codes/smtp-enhanced-status-codes-1.csv", "r"); -if ($fh !== FALSE) { - $a = fgets($fh); # 1st line is titles - while (!feof($fh)) { - $a = fgetcsv($fh, 0, ',', '"'); - $a[0] = preg_replace('/\..*/', '', $a[0]); # 5.x.x -> 5 - $a[1] = preg_replace('/\n\s*/', ' ', $a[1]); #remove line breaks - $a[1] = preg_replace('/\s\s+/', ' ', $a[1]); #remove double spaces - $a[1] = preg_replace('/"/', '\\"', $a[1]); #requote quotes - $a[2] = preg_replace('/\n\s*/', ' ', $a[2]); #remove line breaks - $a[2] = preg_replace('/\s\s+/', ' ', $a[2]); #remove double spaces - $a[2] = preg_replace('/"/', '\\"', $a[2]); #requote quotes - print "\$status_code_classes['$a[0]']['title'] = \"". $a[1]. "\"; # $a[3]\n"; - print "\$status_code_classes['$a[0]']['descr'] = \"". $a[2]. "\";\n"; - } - fclose ($fh); +if ($fh !== false) { + $a = fgets($fh); # 1st line is titles + while (!feof($fh)) { + $a = fgetcsv($fh, 0, ',', '"'); + $a[0] = preg_replace('/\..*/', '', $a[0]); # 5.x.x -> 5 + $a[1] = preg_replace('/\n\s*/', ' ', $a[1]); #remove line breaks + $a[1] = preg_replace('/\s\s+/', ' ', $a[1]); #remove double spaces + $a[1] = preg_replace('/"/', '\\"', $a[1]); #requote quotes + $a[2] = preg_replace('/\n\s*/', ' ', $a[2]); #remove line breaks + $a[2] = preg_replace('/\s\s+/', ' ', $a[2]); #remove double spaces + $a[2] = preg_replace('/"/', '\\"', $a[2]); #requote quotes + print "\$status_code_classes['$a[0]']['title'] = \"" . $a[1] . "\"; # $a[3]\n"; + print "\$status_code_classes['$a[0]']['descr'] = \"" . $a[2] . "\";\n"; + } + fclose($fh); } print "\n"; @@ -38,21 +38,21 @@ $fh = fopen("http://www.iana.org/assignments/smtp-enhanced-status-codes/smtp-enhanced-status-codes-3.csv", "r"); -if ($fh !== FALSE) { - $a = fgets($fh); # 1st line is titles - while (!feof($fh)) { - $a = fgetcsv($fh, 0, ',', '"'); - $a[0] = preg_replace('/^X./i', '', $a[0]); #X.5.0 -> 5.0 - $a[1] = preg_replace('/\n\s*/', ' ', $a[1]); #remove line breaks - $a[1] = preg_replace('/\s\s+/', ' ', $a[1]); #remove double spaces - $a[1] = preg_replace('/"/', '\\"', $a[1]); #requote quotes - $a[3] = preg_replace('/\n\s*/', ' ', $a[3]); #remove line breaks - $a[3] = preg_replace('/\s\s+/', ' ', $a[3]); #remove double spaces - $a[3] = preg_replace('/"/', '\\"', $a[3]); #requote quotes - print "\$status_code_subclasses['$a[0]']['title'] = \"". $a[1]. "\"; # $a[4]\n"; - print "\$status_code_subclasses['$a[0]']['descr'] = \"". $a[3]. "\";\n"; - } - fclose ($fh); +if ($fh !== false) { + $a = fgets($fh); # 1st line is titles + while (!feof($fh)) { + $a = fgetcsv($fh, 0, ',', '"'); + $a[0] = preg_replace('/^X./i', '', $a[0]); #X.5.0 -> 5.0 + $a[1] = preg_replace('/\n\s*/', ' ', $a[1]); #remove line breaks + $a[1] = preg_replace('/\s\s+/', ' ', $a[1]); #remove double spaces + $a[1] = preg_replace('/"/', '\\"', $a[1]); #requote quotes + $a[3] = preg_replace('/\n\s*/', ' ', $a[3]); #remove line breaks + $a[3] = preg_replace('/\s\s+/', ' ', $a[3]); #remove double spaces + $a[3] = preg_replace('/"/', '\\"', $a[3]); #requote quotes + print "\$status_code_subclasses['$a[0]']['title'] = \"" . $a[1] . "\"; # $a[4]\n"; + print "\$status_code_subclasses['$a[0]']['descr'] = \"" . $a[3] . "\";\n"; + } + fclose($fh); } print "\n\n?>"; diff --git a/bounce_driver.class.php b/bounce_driver.class.php index a06bdff..33c9a1c 100644 --- a/bounce_driver.class.php +++ b/bounce_driver.class.php @@ -34,74 +34,169 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -class BounceHandler{ - + +/** + * Class BounceHandler + */ +class BounceHandler +{ + /**** VARS ****************************************************************/ public $head_hash = array(); + /** + * @var array + */ public $fbl_hash = array(); + /** + * @var array + */ public $body_hash = array(); // not necessary - private $bouncelist = array(); // from bounce_responses - private $autorespondlist = array(); // from bounce_responses - private $bouncesubj = array(); // from bounce_responses - - public $looks_like_a_bounce = false; - public $looks_like_an_FBL = false; - public $looks_like_an_autoresponse = false; + /** + * @var bool + */ + public $looks_like_a_bounce = false; // from bounce_responses + /** + * @var bool + */ + public $looks_like_an_FBL = false; // from bounce_responses + /** + * @var bool + */ + public $looks_like_an_autoresponse = false; // from bounce_responses + /** + * @var bool + */ public $is_hotmail_fbl = false; - - // these are for feedback reports, so you can extract uids from the emails - // eg X-my-custom-header: userId12345 - // eg + /** + * @var string + */ public $web_beacon_preg_1 = ""; + /** + * @var string + */ public $web_beacon_preg_2 = ""; + /** + * @var string + */ public $x_header_search_1 = ""; - public $x_header_search_2 = ""; - // accessors + // these are for feedback reports, so you can extract uids from the emails + // eg X-my-custom-header: userId12345 + // eg + /** + * @var string + */ + public $x_header_search_2 = ""; + /** + * @var string + */ public $type = ""; + /** + * @var string + */ public $web_beacon_1 = ""; + /** + * @var string + */ public $web_beacon_2 = ""; + + // accessors + /** + * @var string + */ public $feedback_type = ""; + /** + * @var string + */ public $x_header_beacon_1 = ""; + /** + * @var string + */ public $x_header_beacon_2 = ""; - - // these accessors are useful only for FBL's - // or if the output array has only one index + /** + * @var string + */ public $action = ""; + /** + * @var string + */ public $status = ""; + /** + * @var string + */ public $subject = ""; + + // these accessors are useful only for FBL's + // or if the output array has only one index + /** + * @var string + */ public $recipient = ""; + /** + * @var array + */ + public $output = array(); + /** + * @var array + */ + private $bouncelist = array(); + /** + * @var array + */ + private $autorespondlist = array(); // the raw data set, a multiArray - public $output = array(); + /** + * @var array + */ + private $bouncesubj = array(); - - /**** INSTANTIATION *******************************************************/ - public function __construct(){ - $this->output[0]['action'] = ""; - $this->output[0]['status'] = ""; + /** + * Constructor + */ + public function __construct() + { + $this->output[0]['action'] = ""; + $this->output[0]['status'] = ""; $this->output[0]['recipient'] = ""; - require('bounce_responses.php'); + require_once('bounce_responses.php'); $this->bouncelist = $bouncelist; $this->autorespondlist = $autorespondlist; $this->bouncesubj = $bouncesubj; } - + /**** METHODS *************************************************************/ - // this is the most commonly used public method - // quick and dirty - // useage: $multiArray = $this->get_the_facts($strEmail); - public function parse_email($eml){ + // + /** + * This is the most commonly used public method - quick and dirty email parsing. + * + * Usage: $multiArray = $this->get_the_facts($strEmail); + * + * @param string $eml + * + * @return array + */ + public function parse_email($eml) + { return $this->get_the_facts($eml); } - public function get_the_facts($eml){ + + /** + * Gets the facts about the email + * @param string $eml + * + * @return array + */ + public function get_the_facts($eml) + { // fluff up the email $bounce = $this->init_bouncehandler($eml); - if (strpos($bounce, "\r\n\r\n") !== FALSE) + if (strpos($bounce, "\r\n\r\n") !== false) { list($head, $body) = preg_split("/\r\n\r\n/", $bounce, 2); - else + } else { list($head, $body) = array($bounce, ''); + } $this->head_hash = $this->parse_head($head); // parse the email into data structures @@ -113,7 +208,7 @@ public function get_the_facts($eml){ $this->looks_like_a_bounce = $this->is_a_bounce(); $this->looks_like_an_FBL = $this->is_an_ARF(); $this->looks_like_an_autoresponse = !$this->looks_like_a_bounce && !$this->looks_like_an_FBL && $this->is_an_autoresponse(); - + /* If you are trying to save processing power, and don't care much * about accuracy then uncomment this statement in order to skip the * heroic text parsing below. @@ -125,13 +220,15 @@ public function get_the_facts($eml){ /*** now we try all our weird text parsing methods (E-mail is weird!) ******************************/ - // is it a Feedback Loop, in Abuse Feedback Reporting Format (ARF)? - // http://en.wikipedia.org/wiki/Abuse_Reporting_Format#Abuse_Feedback_Reporting_Format_.28ARF.29 - if($this->looks_like_an_FBL){ + /** + * Is it a feedback loop in abuse feedback reporting format (ARF)? + * @see http://en.wikipedia.org/wiki/Abuse_Reporting_Format#Abuse_Feedback_Reporting_Format_.28ARF.29 + */ + if ($this->looks_like_an_FBL) { $this->output[0]['action'] = 'failed'; $this->output[0]['status'] = "5.7.1"; $this->subject = trim(str_ireplace("Fw:", "", $this->head_hash['Subject'])); - if ($this->is_hotmail_fbl === true){ + if ($this->is_hotmail_fbl === true) { // fill in the fbl_hash with sensible values $this->fbl_hash['Source-ip'] = ''; $this->fbl_hash['Original-mail-from'] = ''; @@ -140,52 +237,63 @@ public function get_the_facts($eml){ $this->fbl_hash['Content-disposition'] = 'inline'; $this->fbl_hash['Content-type'] = 'message/feedback-report'; $this->fbl_hash['User-agent'] = 'Hotmail FBL'; - if (isset($this->first_body_hash['Date'])) + if (isset($this->first_body_hash['Date'])) { $this->fbl_hash['Received-date'] = $this->first_body_hash['Date']; - if (isset($this->head_hash['Subject']) && preg_match('/complaint about message from ([0-9.]+)/', $this->head_hash['Subject'], $matches)) + } + if (isset($this->head_hash['Subject']) && preg_match('/complaint about message from ([0-9.]+)/', + $this->head_hash['Subject'], $matches) + ) { $this->fbl_hash['Source-ip'] = $matches[1]; - if (!empty($this->recipient)) + } + if (!empty($this->recipient)) { $this->fbl_hash['Original-rcpt-to'] = $this->recipient; - if (isset($this->first_body_hash['X-sid-pra'])) + } + if (isset($this->first_body_hash['X-sid-pra'])) { $this->fbl_hash['Original-mail-from'] = $this->first_body_hash['X-sid-pra']; - } - else { + } + } else { $this->fbl_hash = $this->standard_parser($mime_sections['machine_parsable_body_part']); $returnedhash = $this->standard_parser($mime_sections['returned_message_body_part']); - if (!empty($returnedhash['Return-path'])) + if (!empty($returnedhash['Return-path'])) { $this->fbl_hash['Original-mail-from'] = $returnedhash['Return-path']; - elseif (empty($this->fbl_hash['Original-mail-from']) && !empty($returnedhash['From'])) + } elseif (empty($this->fbl_hash['Original-mail-from']) && !empty($returnedhash['From'])) { $this->fbl_hash['Original-mail-from'] = $returnedhash['From']; - if (empty($this->fbl_hash['Original-rcpt-to']) && !empty($this->fbl_hash['Removal-recipient']) ) + } + if (empty($this->fbl_hash['Original-rcpt-to']) && !empty($this->fbl_hash['Removal-recipient'])) { $this->fbl_hash['Original-rcpt-to'] = $this->fbl_hash['Removal-recipient']; - elseif (isset($returnedhash['To'])) + } elseif (isset($returnedhash['To'])) { $this->fbl_hash['Original-rcpt-to'] = $returnedhash['To']; - else + } else { $this->fbl_hash['Original-rcpt-to'] = ''; - if (!isset($this->fbl_hash['Source-ip'])) - if (!empty($returnedhash['X-originating-ip'])) + } + if (!isset($this->fbl_hash['Source-ip'])) { + if (!empty($returnedhash['X-originating-ip'])) { $this->fbl_hash['Source-ip'] = $this->strip_angle_brackets($returnedhash['X-originating-ip']); - else + } else { $this->fbl_hash['Source-ip'] = ''; + } + } } // warning, some servers will remove the name of the original intended recipient from the FBL report, // replacing it with redacted@rcpt-hostname.com, making it utterly useless, of course (unless you used a web-beacon). // here we try our best to give you the actual intended recipient, if possible. - if (preg_match('/Undisclosed|redacted/i', $this->fbl_hash['Original-rcpt-to']) && isset($this->fbl_hash['Removal-recipient']) ) { + if (preg_match('/Undisclosed|redacted/i', + $this->fbl_hash['Original-rcpt-to']) && isset($this->fbl_hash['Removal-recipient']) + ) { $this->fbl_hash['Original-rcpt-to'] = @$this->fbl_hash['Removal-recipient']; } - if (empty($this->fbl_hash['Received-date']) && !empty($this->fbl_hash[@'Arrival-date']) ) { + if (empty($this->fbl_hash['Received-date']) && !empty($this->fbl_hash[@'Arrival-date'])) { $this->fbl_hash['Received-date'] = @$this->fbl_hash['Arrival-date']; } $this->fbl_hash['Original-mail-from'] = $this->strip_angle_brackets(@$this->fbl_hash['Original-mail-from']); - $this->fbl_hash['Original-rcpt-to'] = $this->strip_angle_brackets(@$this->fbl_hash['Original-rcpt-to']); + $this->fbl_hash['Original-rcpt-to'] = $this->strip_angle_brackets(@$this->fbl_hash['Original-rcpt-to']); $this->output[0]['recipient'] = $this->fbl_hash['Original-rcpt-to']; } -#??? else if (preg_match("/auto.{0,20}reply|vacation|(out|away|on holiday).*office/i", $this->head_hash['Subject'])){ -# // looks like a vacation autoreply, ignoring -# $this->output[0]['action'] = 'autoreply'; -# } + #??? else if (preg_match("/auto.{0,20}reply|vacation|(out|away|on holiday).*office/i", $this->head_hash['Subject'])){ + # // looks like a vacation autoreply, ignoring + # $this->output[0]['action'] = 'autoreply'; + # } // is this an autoresponse ? elseif ($this->looks_like_an_autoresponse) { @@ -193,76 +301,67 @@ public function get_the_facts($eml){ $this->output[0]['autoresponse'] = $this->autoresponse; #??? 4.3.2 // grab the first recipient and break $this->output[0]['recipient'] = isset($this->head_hash['Return-path']) ? $this->strip_angle_brackets($this->head_hash['Return-path']) : ''; - if(empty($this->output[0]['recipient'])){ + if (empty($this->output[0]['recipient'])) { $arrFailed = $this->find_email_addresses($body); - for($j=0; $joutput[$j]['recipient'] = trim($arrFailed[$j]); - break; + break; } } - } - - else if ($this->is_RFC1892_multipart_report() === TRUE){ + } else if ($this->is_RFC1892_multipart_report() === true) { $rpt_hash = $this->parse_machine_parsable_body_part($mime_sections['machine_parsable_body_part']); if (isset($rpt_hash['per_recipient'])) { - for($i=0; $ioutput[$i]['recipient'] = $this->find_recipient($rpt_hash['per_recipient'][$i]); $mycode = @$this->format_status_code($rpt_hash['per_recipient'][$i]['Status']); $this->output[$i]['status'] = @$mycode['code']; $this->output[$i]['action'] = @$rpt_hash['per_recipient'][$i]['Action']; } - } - else { + } else { $arrFailed = $this->find_email_addresses($mime_sections['first_body_part']); - for($j=0; $joutput[$j]['recipient'] = trim($arrFailed[$j]); - $this->output[$j]['status'] = $this->get_status_code_from_text($this->output[$j]['recipient'],0); + $this->output[$j]['status'] = $this->get_status_code_from_text($this->output[$j]['recipient'], 0); $this->output[$j]['action'] = $this->get_action_from_status_code($this->output[$j]['status']); } - } - } - - else if(isset($this->head_hash['X-failed-recipients'])) { + } + } else if (isset($this->head_hash['X-failed-recipients'])) { // Busted Exim MTA // Up to 50 email addresses can be listed on each header. // There can be multiple X-Failed-Recipients: headers. - (not supported) $arrFailed = split(',', $this->head_hash['X-failed-recipients']); - for($j=0; $joutput[$j]['recipient'] = trim($arrFailed[$j]); - $this->output[$j]['status'] = $this->get_status_code_from_text($this->output[$j]['recipient'],0); + $this->output[$j]['status'] = $this->get_status_code_from_text($this->output[$j]['recipient'], 0); $this->output[$j]['action'] = $this->get_action_from_status_code($this->output[$j]['status']); } - } - - else if(!empty($boundary) && $this->looks_like_a_bounce){ + } else if (!empty($boundary) && $this->looks_like_a_bounce) { // oh god it could be anything, but at least it has mime parts, so let's try anyway $arrFailed = $this->find_email_addresses($mime_sections['first_body_part']); - for($j=0; $joutput[$j]['recipient'] = trim($arrFailed[$j]); - $this->output[$j]['status'] = $this->get_status_code_from_text($this->output[$j]['recipient'],0); + $this->output[$j]['status'] = $this->get_status_code_from_text($this->output[$j]['recipient'], 0); $this->output[$j]['action'] = $this->get_action_from_status_code($this->output[$j]['status']); } - } - - else if($this->looks_like_a_bounce){ + } else if ($this->looks_like_a_bounce) { // last ditch attempt // could possibly produce erroneous output, or be very resource consuming, // so be careful. You should comment out this section if you are very concerned // about 100% accuracy or if you want very fast performance. // Leave it turned on if you know that all messages to be analyzed are bounces. $arrFailed = $this->find_email_addresses($body); - for($j=0; $joutput[$j]['recipient'] = trim($arrFailed[$j]); - $this->output[$j]['status'] = $this->get_status_code_from_text($this->output[$j]['recipient'],0); + $this->output[$j]['status'] = $this->get_status_code_from_text($this->output[$j]['recipient'], 0); $this->output[$j]['action'] = $this->get_action_from_status_code($this->output[$j]['status']); } } // else if()..... add a parser for your busted-ass MTA here - + // remove empty array indices $tmp = array(); - foreach($this->output as $arr){ - if(empty($arr['recipient']) && empty($arr['status']) && empty($arr['action']) ){ + foreach ($this->output as $arr) { + if (empty($arr['recipient']) && empty($arr['status']) && empty($arr['action'])) { continue; } $tmp[] = $arr; @@ -280,24 +379,34 @@ public function get_the_facts($eml){ $this->feedback_type = (isset($this->fbl_hash['Feedback-type'])) ? $this->fbl_hash['Feedback-type'] : ""; // sniff out any web beacons - if($this->web_beacon_preg_1) + if ($this->web_beacon_preg_1) { $this->web_beacon_1 = $this->find_web_beacon($body, $this->web_beacon_preg_1); - if($this->web_beacon_preg_2) + } + if ($this->web_beacon_preg_2) { $this->web_beacon_2 = $this->find_web_beacon($body, $this->web_beacon_preg_2); - if($this->x_header_search_1) - $this->x_header_beacon_1 = $this->find_x_header ($this->x_header_search_1); - if($this->x_header_search_2) - $this->x_header_beacon_2 = $this->find_x_header ($this->x_header_search_2); + } + if ($this->x_header_search_1) { + $this->x_header_beacon_1 = $this->find_x_header($this->x_header_search_1); + } + if ($this->x_header_search_2) { + $this->x_header_beacon_2 = $this->find_x_header($this->x_header_search_2); + } return $this->output; } - - function init_bouncehandler($blob, $format='string'){ + /** + * @param $blob + * @param string $format + * + * @return mixed + */ + function init_bouncehandler($blob, $format = 'string') + { $this->head_hash = array(); $this->fbl_hash = array(); - $this->body_hash = array(); + $this->body_hash = array(); $this->looks_like_a_bounce = false; $this->looks_like_an_FBL = false; $this->is_hotmail_fbl = false; @@ -330,9 +439,9 @@ function init_bouncehandler($blob, $format='string'){ $strEmail = str_replace("\r\n", "\n", $blob); // line returns 1 $strEmail = str_replace("\n", "\r\n", $strEmail);// line returns 2 -# $strEmail = str_replace("=\r\n", "", $strEmail); // remove MIME line breaks (would never exist as #1 above would have dealt with) -# $strEmail = str_replace("=3D", "=", $strEmail); // equals sign - dealt with in the MIME decode section now -# $strEmail = str_replace("=09", " ", $strEmail); // tabs + # $strEmail = str_replace("=\r\n", "", $strEmail); // remove MIME line breaks (would never exist as #1 above would have dealt with) + # $strEmail = str_replace("=3D", "=", $strEmail); // equals sign - dealt with in the MIME decode section now + # $strEmail = str_replace("=09", " ", $strEmail); // tabs //} //else if($format=='array'){ @@ -345,285 +454,358 @@ function init_bouncehandler($blob, $format='string'){ return $strEmail; } - // general purpose recursive heuristic function - // to try to extract useful info from the bounces produced by busted MTAs - function get_status_code_from_text($recipient, $index){ - for($i=$index; $ibody_hash); $i++){ - $line = trim($this->body_hash[$i]); - - //skip Message-ID lines - if (stripos($line, 'Message-ID') !== FALSE) - continue; - - /******** recurse into the email if you find the recipient ********/ - if(stristr($line, $recipient)!==FALSE){ - // the status code MIGHT be in the next few lines after the recipient line, - // depending on the message from the foreign host... What a laugh riot! - $status_code = $this->get_status_code_from_text($recipient, $i+1); - if($status_code){ - return $status_code; - } - - } - - /******** exit conditions ********/ - // if it's the end of the human readable part in this stupid bounce - if(stristr($line, '------ This is a copy of the message')!==FALSE){ - break; - } - //if we see an email address other than our current recipient's, - if(count($this->find_email_addresses($line))>=1 - && stristr($line, $recipient) === FALSE - && strstr($line, 'FROM:<') === FALSE) { // Kanon added this line because Hotmail puts the e-mail address too soon and there actually is error message stuff after it. - break; - } - - //******** pattern matching ********/ - foreach ($this->bouncelist as $bouncetext => $bouncecode) { - if (preg_match("/$bouncetext/i", $line, $matches)) - return (isset($matches[1])) ? $matches[1] : $bouncecode; - } - // Search for a rfc3463 style return code - if (preg_match('/\W([245]\.[01234567]\.[0-9]{1,2})\W/', $line, $matches)) { - return $matches[1]; -#??? this seems somewhat redundant -# $mycode = str_replace('.', '', $matches[1]); -# $mycode = $this->format_status_code($mycode); -# return implode('.', $mycode['code']); #x.y.z format - } - // search for RFC2821 return code - // thanks to mark.tolman@gmail.com - // Maybe at some point it should have it's own place within the main parsing scheme (at line 88) - if (preg_match('/\]?: ([45][01257][012345]) /', $line, $matches) || - preg_match('/^([45][01257][012345]) (?:.*?)(?:denied|inactive|deactivated|rejected|disabled|unknown|no such|not (?:our|activated|a valid))+/i', $line, $matches)) - { - $mycode = $matches[1]; - // map RFC2821 -> RFC3463 codes - if ($mycode == '550' || $mycode == '551' || $mycode == '553' || $mycode == '554') - return '5.1.1'; #perm error - elseif ($mycode == '452' || $mycode == '552') - return '4.2.2'; #mailbox full - elseif ($mycode == '450' || $mycode == '421') - return '4.3.2'; #temp unavailable - #???$mycode = $this->format_status_code($mycode); - #???return implode('.', $mycode['code']); + /** + * Try to extract useful info from the headers bounces produced by busted MTAs + * + * @param string|array $headers Headers of the email + * + * @return array + */ + function parse_head($headers) + { + if (!is_array($headers)) { + $headers = explode("\r\n", $headers); + } + $hash = $this->standard_parser($headers); + if (isset($hash['Content-type'])) {//preg_match('/Multipart\/Report/i', $hash['Content-type'])){ + $multipart_report = explode(';', $hash['Content-type']); + $hash['Content-type'] = ''; + $hash['Content-type']['type'] = strtolower($multipart_report[0]); + foreach ($multipart_report as $mr) { + if (preg_match('/([^=.]*?)=(.*)/i', $mr, $matches)) { + // didn't work when the content-type boundary ID contained an equal sign, + // that exists in bounces from many Exchange servers + //if(preg_match('/([a-z]*)=(.*)?/i', $mr, $matches)){ + $hash['Content-type'][strtolower(trim($matches[1]))] = str_replace('"', '', $matches[2]); + } } - } - return '5.5.0'; #other or unknown status - } - - function is_RFC1892_multipart_report(){ - return @$this->head_hash['Content-type']['type']=='multipart/report' - && @$this->head_hash['Content-type']['report-type']=='delivery-status' - && @$this->head_hash['Content-type'][boundary]!==''; + return $hash; } - function parse_head($headers){ - if(!is_array($headers)) - $headers = explode("\r\n", $headers); - $hash = $this->standard_parser($headers); - if(isset($hash['Content-type'])) {//preg_match('/Multipart\/Report/i', $hash['Content-type'])){ - $multipart_report = explode (';', $hash['Content-type']); - $hash['Content-type']=''; - $hash['Content-type']['type'] = strtolower($multipart_report[0]); - foreach($multipart_report as $mr){ - if(preg_match('/([^=.]*?)=(.*)/i', $mr, $matches)){ - // didn't work when the content-type boundary ID contained an equal sign, - // that exists in bounces from many Exchange servers - //if(preg_match('/([a-z]*)=(.*)?/i', $mr, $matches)){ - $hash['Content-type'][strtolower(trim($matches[1]))]= str_replace('"','',$matches[2]); + /** + * Try and understand information from the headers of the email. + * + * @param string|array $content Header of the email + * + * @return array + */ + function standard_parser($content) + { // associative array orstr + // receives email head as array of lines + // simple parse (Entity: value\n) + $hash = array('Received' => ''); + if (!is_array($content)) { + $content = explode("\r\n", $content); + } + foreach ($content as $line) { + if (preg_match('/^([^\s.]*):\s*(.*)\s*/', $line, $array)) { + $entity = ucfirst(strtolower($array[1])); + if (isset($array[2]) && strpos($array[2], '=?') !== false + ) // decode MIME Header encoding (subject lines etc) + { + $array[2] = iconv_mime_decode($array[2], ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); + } + if (empty($hash[$entity])) { + $hash[$entity] = trim($array[2]); + } else if ($hash['Received']) { + // grab extra Received headers :( + // pile it on with pipe delimiters, + // oh well, SMTP is broken in this way + if ($entity and $array[2] and $array[2] != $hash[$entity]) { + $hash[$entity] .= "|" . trim($array[2]); + } + } + } elseif (isset($line) && isset($entity) && preg_match('/^\s+(.+)\s*/', $line) && $entity) { + $line = trim($line); + if (strpos($array[2], '=?') !== false) { + $line = iconv_mime_decode($array[2], ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); } + $hash[$entity] .= ' ' . $line; } } + // special formatting + $hash['Received'] = @explode('|', $hash['Received']); + $hash['Subject'] = isset($hash['Subject']) ?: ''; return $hash; } - function contenttype_decode ($mimepart) { + /** + * Split an email into multiple mime sections + * @param string|array $body Body of the email + * @param string $boundary The boundary MIME separator. + * + * @return array + */ + function parse_body_into_mime_sections($body, $boundary) + { + if (!$boundary) { + return array(); + } + if (is_array($body)) { + $body = implode("\r\n", $body); + } + $body = explode($boundary, $body); + $mime_sections['first_body_part'] = isset($body[1]) ? $this->contenttype_decode($body[1]) : ''; #proper MIME decode + $mime_sections['machine_parsable_body_part'] = isset($body[2]) ? $this->contenttype_decode($body[2]) : ''; + $mime_sections['returned_message_body_part'] = isset($body[3]) ? $this->contenttype_decode($body[3]) : ''; + return $mime_sections; + } + + /** + * Decode a content transfer-encoded part of the email. + * + * @param string $mimepart MIME encoded email body + * + * @return string + */ + function contenttype_decode($mimepart) + { $encoding = '7bit'; $decoded = ''; foreach (explode("\r\n", $mimepart) as $line) { if (preg_match("/^Content-Transfer-Encoding:\s*(\S+)/", $line, $match)) { $encoding = $match[1]; $decoded .= $line . "\r\n"; - } - else switch ($encoding) { - case 'quoted-printable': { - if (substr($line, -1) == '=') - $line = substr($line, 0, -1); - else - $line .= "\r\n"; - $decoded .= preg_replace_callback("/=([0-9A-F][0-9A-F])/", function($matches){ return chr(hexdec($matches[0])); } , $line); + } else { + switch ($encoding) { + case 'quoted-printable': { + if (substr($line, -1) == '=') { + $line = substr($line, 0, -1); + } else { + $line .= "\r\n"; + } + $decoded .= preg_replace_callback("/=([0-9A-F][0-9A-F])/", function ($matches) { + return chr(hexdec($matches[0])); + }, $line); + } + case 'base64': { + $decoded .= base64_decode($line); + break; + } + default: # 7bit, 8bit, binary + $decoded .= $line . "\r\n"; } - case 'base64': { - $decoded .= base64_decode($line); - break; - } - default: # 7bit, 8bit, binary - $decoded .= $line."\r\n"; } } return $decoded; } - function parse_body_into_mime_sections($body, $boundary){ - if(!$boundary) return array(); - if (is_array($body)) - $body = implode("\r\n", $body); - $body = explode($boundary, $body); - $mime_sections['first_body_part'] = isset($body[1]) ? $this->contenttype_decode($body[1]) : ''; #proper MIME decode - $mime_sections['machine_parsable_body_part'] = isset($body[2]) ? $this->contenttype_decode($body[2]) : ''; - $mime_sections['returned_message_body_part'] = isset($body[3]) ? $this->contenttype_decode($body[3]) : ''; - return $mime_sections; + /** + * Sees if this is an "obvious bounce". + * + * @return bool + */ + function is_a_bounce() + { + foreach ($this->bouncesubj as $s) { + if (preg_match("/^$s/i", $this->head_hash['Subject'])) { + return true; + } + } + #if(@preg_match('/auto_reply/',$this->head_hash['Precedence'])) return true; # autoresponse, not bounce + if (isset($this->head_hash['From']) && + preg_match("/^(postmaster|mailer-daemon)\@?/i", $this->head_hash['From']) + ) { + return true; + } + return false; } + /** + * Sees if this is an obvious "Abuse reporting format" email. + * + * @return bool + */ + function is_an_ARF() + { + if (isset($this->head_hash['Content-type']['report-type']) && preg_match('/feedback-report/', + $this->head_hash['Content-type']['report-type']) + ) { + return true; + } + if (isset($this->head_hash['X-loop']) && preg_match('/scomp/', $this->head_hash['X-loop'])) { + return true; + } + if (isset($this->head_hash['X-hmxmroriginalrecipient'])) { + $this->is_hotmail_fbl = true; + $this->recipient = $this->head_hash['X-hmxmroriginalrecipient']; + return true; + } + if (isset($this->first_body_hash['X-hmxmroriginalrecipient'])) { + $this->is_hotmail_fbl = true; + $this->recipient = $this->first_body_hash['X-hmxmroriginalrecipient']; + return true; + } + return false; + } - function standard_parser($content){ // associative array orstr - // receives email head as array of lines - // simple parse (Entity: value\n) - $hash = array('Received'=>''); - if(!is_array($content)) $content = explode("\r\n", $content); - foreach($content as $line){ - if(preg_match('/^([^\s.]*):\s*(.*)\s*/', $line, $array)){ - $entity = ucfirst(strtolower($array[1])); - if (isset($array[2]) && strpos($array[2], '=?') !== FALSE) // decode MIME Header encoding (subject lines etc) - $array[2] = iconv_mime_decode($array[2], ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); - if(empty($hash[$entity])){ - $hash[$entity] = trim($array[2]); - } - else if($hash['Received']){ - // grab extra Received headers :( - // pile it on with pipe delimiters, - // oh well, SMTP is broken in this way - if ($entity and $array[2] and $array[2] != $hash[$entity]){ - $hash[$entity] .= "|" . trim($array[2]); - } - } + /** + * Sees if this is an obvious autoresponder email. + * + * @return bool + */ + function is_an_autoresponse() + { + foreach (array('Auto-submitted', 'X-autorespond') as $a) { + if (isset($this->head_hash[$a])) { + $this->autoresponse = "$a: " . $this->head_hash[$a]; + return true; } - elseif (isset($line) && isset($entity) && preg_match('/^\s+(.+)\s*/', $line) && $entity) { - $line = trim($line); - if (strpos($array[2], '=?') !== FALSE) - $line = iconv_mime_decode($array[2], ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); - $hash[$entity] .= ' '. $line; + } + foreach (array('Precedence', 'X-precedence') as $a) { + if (isset($this->head_hash[$a]) && preg_match('/^(auto|junk)/i', $this->head_hash[$a])) { + $this->autoresponse = "$a: " . $this->head_hash[$a]; + return true; } } - // special formatting - $hash['Received']= @explode('|', $hash['Received']); - $hash['Subject'] = isset($hash['Subject']) ? : ''; - return $hash; + + $subj = isset($this->head_hash['Subject']) ? $this->head_hash['Subject'] : ''; + foreach ($this->autorespondlist as $a) { + if (preg_match("/$a/i", $subj)) { + $this->autoresponse = $this->head_hash['Subject']; + return true; + } + } + return false; + } + + /** + * Strip angled brackets. + * + * @param string $recipient Removes angled brackets from an email address. + * + * @return string + */ + private function strip_angle_brackets($recipient) + { + if (preg_match('/[<[](.*)[>\]]/', $recipient, $matches)) { + return trim($matches[1]); + } else { + return trim($recipient); + } } - function parse_machine_parsable_body_part($str){ + /** + * Finds email addresses in a body. + * + * @TODO Appears that it should return multiple email addresses. + * @TODO Fix: Fails with quite a few email addresses (especially ones with tld's longer than 4 characters!) + * + * @param string $first_body_part Body of the email + * + * @return array + */ + function find_email_addresses($first_body_part) + { + // not finished yet. This finds only one address. + if (preg_match("/\b([A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4})\b/i", $first_body_part, $matches)) { + return array($matches[1]); + } else { + return array(); + } + } + + /** + * Is this an RFC 1892 multiple return email? + * + * @return bool + */ + function is_RFC1892_multipart_report() + { + return @$this->head_hash['Content-type']['type'] == 'multipart/report' + && @$this->head_hash['Content-type']['report-type'] == 'delivery-status' + && @$this->head_hash['Content-type'][boundary] !== ''; + } + + /** + * @param $str + * + * @return mixed + */ + function parse_machine_parsable_body_part($str) + { //Per-Message DSN fields $hash = $this->parse_dsn_fields($str); $hash['mime_header'] = $this->standard_parser($hash['mime_header']); $hash['per_message'] = isset($hash['per_message']) ? $this->standard_parser($hash['per_message']) : array(); - if(isset($hash['per_message']['X-postfix-sender'])){ - $arr = explode (';', $hash['per_message']['X-postfix-sender']); - $hash['per_message']['X-postfix-sender']=''; + if (isset($hash['per_message']['X-postfix-sender'])) { + $arr = explode(';', $hash['per_message']['X-postfix-sender']); + $hash['per_message']['X-postfix-sender'] = ''; $hash['per_message']['X-postfix-sender']['type'] = @trim($arr[0]); $hash['per_message']['X-postfix-sender']['addr'] = @trim($arr[1]); } - if(isset($hash['per_message']['Reporting-mta'])){ - $arr = explode (';', $hash['per_message']['Reporting-mta']); - $hash['per_message']['Reporting-mta']=''; + if (isset($hash['per_message']['Reporting-mta'])) { + $arr = explode(';', $hash['per_message']['Reporting-mta']); + $hash['per_message']['Reporting-mta'] = ''; $hash['per_message']['Reporting-mta']['type'] = @trim($arr[0]); $hash['per_message']['Reporting-mta']['addr'] = @trim($arr[1]); } //Per-Recipient DSN fields - if(isset($hash['per_recipient'])) { - for($i=0; $istandard_parser(explode("\r\n", $hash['per_recipient'][$i])); - $arr = isset($temp['Final-recipient']) ? explode (';', $temp['Final-recipient']) : array(); + $arr = isset($temp['Final-recipient']) ? explode(';', $temp['Final-recipient']) : array(); $temp['Final-recipient'] = $this->format_final_recipient_array($arr); //$temp['Final-recipient']['type'] = trim($arr[0]); //$temp['Final-recipient']['addr'] = trim($arr[1]); - $temp['Original-recipient']= array(); + $temp['Original-recipient'] = array(); $temp['Original-recipient']['type'] = isset($arr[0]) ? trim($arr[0]) : ''; $temp['Original-recipient']['addr'] = isset($arr[1]) ? trim($arr[1]) : ''; - $arr = isset($temp['Diagnostic-code']) ? explode (';', $temp['Diagnostic-code']) : array(); + $arr = isset($temp['Diagnostic-code']) ? explode(';', $temp['Diagnostic-code']) : array(); $temp['Diagnostic-code'] = array(); $temp['Diagnostic-code']['type'] = isset($arr[0]) ? trim($arr[0]) : ''; $temp['Diagnostic-code']['text'] = isset($arr[1]) ? trim($arr[1]) : ''; // now this is wierd: plenty of times you see the status code is a permanent failure, // but the diagnostic code is a temporary failure. So we will assert the most general // temporary failure in this case. - $ddc=''; $judgement=''; + $ddc = ''; + $judgement = ''; $ddc = $this->decode_diagnostic_code($temp['Diagnostic-code']['text']); $judgement = $this->get_action_from_status_code($ddc); - if($judgement == 'transient'){ - if(stristr($temp['Action'],'failed')!==FALSE){ - $temp['Action']='transient'; - $temp['Status']='4.3.0'; + if ($judgement == 'transient') { + if (stristr($temp['Action'], 'failed') !== false) { + $temp['Action'] = 'transient'; + $temp['Status'] = '4.3.0'; } } - $hash['per_recipient'][$i]=''; - $hash['per_recipient'][$i]=$temp; + $hash['per_recipient'][$i] = ''; + $hash['per_recipient'][$i] = $temp; } } return $hash; } - function get_head_from_returned_message_body_part($mime_sections){ - $temp = explode("\r\n\r\n", $mime_sections[returned_message_body_part]); - $head = $this->standard_parser($temp[1]); - $head['From'] = $this->extract_address($head['From']); - $head['To'] = $this->extract_address($head['To']); - return $head; - } - - function extract_address($str){ - $from_stuff = preg_split('/[ \"\'\<\>:\(\)\[\]]/', $str); - foreach ($from_stuff as $things){ - if (strpos($things, '@')!==FALSE){$from = $things;} - } - return $from; - } - - function find_recipient($per_rcpt){ - $recipient = ''; - if($per_rcpt['Original-recipient']['addr'] !== ''){ - $recipient = $per_rcpt['Original-recipient']['addr']; - } - else if($per_rcpt['Final-recipient']['addr'] !== ''){ - $recipient = $per_rcpt['Final-recipient']['addr']; + /** + * @param $dsn_fields + * + * @return mixed + */ + function parse_dsn_fields($dsn_fields) + { + if (!is_array($dsn_fields)) { + $dsn_fields = explode("\r\n\r\n", $dsn_fields); } - $recipient = $this->strip_angle_brackets($recipient); - return $recipient; - } - - function find_type(){ - if($this->looks_like_a_bounce) - return "bounce"; - elseif ($this->looks_like_an_FBL) - return "fbl"; - elseif ($this->looks_like_an_autoresponse) - return "autoresponse"; - else - return false; - } - - function parse_dsn_fields($dsn_fields){ - if(!is_array($dsn_fields)) $dsn_fields = explode("\r\n\r\n", $dsn_fields); $j = 0; reset($dsn_fields); - for($i=0; $i '', + 'type' => '' + ); + if (isset($arr[1])) { + if (strpos($arr[0], '@') !== false) { + $output['addr'] = $this->strip_angle_brackets($arr[0]); + $output['type'] = (!empty($arr[1])) ? trim($arr[1]) : 'unknown'; + } else { + $output['type'] = trim($arr[0]); + $output['addr'] = $this->strip_angle_brackets($arr[1]); + } } - return $ret; + return $output; } - function fetch_status_messages($code){ - include_once ("bounce_statuscodes.php"); - $ret = $this->format_status_code($code); - $arr = explode('.', $ret['code']); - $str = "

". $status_code_classes[$arr[0]]['title'] . " - " .$status_code_classes[$arr[0]]['descr']. " ". $status_code_subclasses[$arr[1].".".$arr[2]]['title'] . " - " .$status_code_subclasses[$arr[1].".".$arr[2]]['descr']. "

"; - return $str; + /** + * @param $dcode + * + * @return mixed + */ + function decode_diagnostic_code($dcode) + { + if (preg_match("/(\d\.\d\.\d)\s/", $dcode, $array)) { + return $array[1]; + } else if (preg_match("/(\d\d\d)\s/", $dcode, $array)) { + return $array[1]; + } } - function get_action_from_status_code($code){ - if($code=='') + /** + * @param $code + * + * @return string + */ + function get_action_from_status_code($code) + { + if ($code == '') { return ''; + } $ret = $this->format_status_code($code); switch (isset($ret['code']) ? $ret['code'][0] : '') { case(2): @@ -673,138 +880,247 @@ function get_action_from_status_code($code){ } } - function decode_diagnostic_code($dcode){ - if(preg_match("/(\d\.\d\.\d)\s/", $dcode, $array)){ - return $array[1]; - } - else if(preg_match("/(\d\d\d)\s/", $dcode, $array)){ - return $array[1]; + /** + * @param $code + * + * @return string + */ + function format_status_code($code) + { + $ret = ""; + if (preg_match('/([245]\.[01234567]\.\d{1,2})\s*(.*)/', $code, $matches)) { + $ret['code'] = $matches[1]; + $ret['text'] = $matches[2]; + } else if (preg_match('/([245])([01234567])(\d{1,2})\s*(.*)/', $code, $matches)) { + $ret['code'] = $matches[1] . '.' . $matches[2] . '.' . $matches[3]; + $ret['text'] = $matches[4]; } + return $ret; } - function is_a_bounce(){ - foreach ($this->bouncesubj as $s) - if (preg_match("/^$s/i", $this->head_hash['Subject'])) - return true; - #if(@preg_match('/auto_reply/',$this->head_hash['Precedence'])) return true; # autoresponse, not bounce - if (isset($this->head_hash['From']) && - preg_match("/^(postmaster|mailer-daemon)\@?/i", $this->head_hash['From'])) - return true; - return false; + /** + * @param $per_rcpt + * + * @return string + */ + function find_recipient($per_rcpt) + { + $recipient = ''; + if ($per_rcpt['Original-recipient']['addr'] !== '') { + $recipient = $per_rcpt['Original-recipient']['addr']; + } else if ($per_rcpt['Final-recipient']['addr'] !== '') { + $recipient = $per_rcpt['Final-recipient']['addr']; + } + $recipient = $this->strip_angle_brackets($recipient); + return $recipient; } - - function find_email_addresses($first_body_part){ - // not finished yet. This finds only one address. - if (preg_match("/\b([A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4})\b/i", $first_body_part, $matches)){ - return array($matches[1]); + + /** + * @param $recipient + * @param $index + * + * @return string + */ + function get_status_code_from_text($recipient, $index) + { + for ($i = $index; $i < count($this->body_hash); $i++) { + $line = trim($this->body_hash[$i]); + + //skip Message-ID lines + if (stripos($line, 'Message-ID') !== false) { + continue; + } + + /******** recurse into the email if you find the recipient ********/ + if (stristr($line, $recipient) !== false) { + // the status code MIGHT be in the next few lines after the recipient line, + // depending on the message from the foreign host... What a laugh riot! + $status_code = $this->get_status_code_from_text($recipient, $i + 1); + if ($status_code) { + return $status_code; + } + + } + + /******** exit conditions ********/ + // if it's the end of the human readable part in this stupid bounce + if (stristr($line, '------ This is a copy of the message') !== false) { + break; + } + //if we see an email address other than our current recipient's, + if (count($this->find_email_addresses($line)) >= 1 + && stristr($line, $recipient) === false + && strstr($line, 'FROM:<') === false + ) { // Kanon added this line because Hotmail puts the e-mail address too soon and there actually is error message stuff after it. + break; + } + + //******** pattern matching ********/ + foreach ($this->bouncelist as $bouncetext => $bouncecode) { + if (preg_match("/$bouncetext/i", $line, $matches)) { + return (isset($matches[1])) ? $matches[1] : $bouncecode; + } + } + + // Search for a rfc3463 style return code + if (preg_match('/\W([245]\.[01234567]\.[0-9]{1,2})\W/', $line, $matches)) { + return $matches[1]; + #??? this seems somewhat redundant + # $mycode = str_replace('.', '', $matches[1]); + # $mycode = $this->format_status_code($mycode); + # return implode('.', $mycode['code']); #x.y.z format + } + + // search for RFC2821 return code + // thanks to mark.tolman@gmail.com + // Maybe at some point it should have it's own place within the main parsing scheme (at line 88) + if (preg_match('/\]?: ([45][01257][012345]) /', $line, $matches) || + preg_match('/^([45][01257][012345]) (?:.*?)(?:denied|inactive|deactivated|rejected|disabled|unknown|no such|not (?:our|activated|a valid))+/i', + $line, $matches) + ) { + $mycode = $matches[1]; + // map RFC2821 -> RFC3463 codes + if ($mycode == '550' || $mycode == '551' || $mycode == '553' || $mycode == '554') { + return '5.1.1'; + } #perm error + elseif ($mycode == '452' || $mycode == '552') { + return '4.2.2'; + } #mailbox full + elseif ($mycode == '450' || $mycode == '421') { + return '4.3.2'; + } #temp unavailable + #???$mycode = $this->format_status_code($mycode); + #???return implode('.', $mycode['code']); + } + } - else - return array(); + return '5.5.0'; #other or unknown status } // these functions are for feedback loops - function is_an_ARF(){ - if(isset($this->head_hash['Content-type']['report-type']) && preg_match('/feedback-report/',$this->head_hash['Content-type']['report-type'])) - return true; - if(isset($this->head_hash['X-loop']) && preg_match('/scomp/',$this->head_hash['X-loop'])) - return true; - if(isset($this->head_hash['X-hmxmroriginalrecipient'])) { - $this->is_hotmail_fbl = TRUE; - $this->recipient = $this->head_hash['X-hmxmroriginalrecipient']; - return true; - } - if(isset($this->first_body_hash['X-hmxmroriginalrecipient']) ) { - $this->is_hotmail_fbl = TRUE; - $this->recipient = $this->first_body_hash['X-hmxmroriginalrecipient']; - return true; + + /** + * @return bool|string + */ + function find_type() + { + if ($this->looks_like_a_bounce) { + return "bounce"; + } elseif ($this->looks_like_an_FBL) { + return "fbl"; + } elseif ($this->looks_like_an_autoresponse) { + return "autoresponse"; + } else { + return false; } - return false; } - + // look for common auto-responders - function is_an_autoresponse() { - foreach (array ('Auto-submitted', 'X-autorespond') as $a) { - if (isset($this->head_hash[$a])) { - $this->autoresponse = "$a: ". $this->head_hash[$a]; - return TRUE; - } - } - foreach (array ('Precedence', 'X-precedence') as $a) { - if (isset($this->head_hash[$a]) && preg_match('/^(auto|junk)/i', $this->head_hash[$a])) { - $this->autoresponse = "$a: ". $this->head_hash[$a]; - return TRUE; - } + + /** + * @param $body + * @param $preg + * + * @return string + */ + public function find_web_beacon($body, $preg) + { + if (!isset($preg) || !$preg) { + return ""; } - - $subj = isset($this->head_hash['Subject']) ? $this->head_hash['Subject'] : ''; - foreach ($this->autorespondlist as $a) { - if (preg_match("/$a/i", $subj)) { - $this->autoresponse = $this->head_hash['Subject']; - return TRUE; - } + if (preg_match($preg, $body, $matches)) { + return $matches[1]; } - return FALSE; } - - - + + // use a perl regular expression to find the web beacon - public function find_web_beacon($body, $preg){ - if(!isset($preg) || !$preg) - return ""; - if(preg_match($preg, $body, $matches)) - return $matches[1]; - } - - public function find_x_header($xheader){ + + /** + * @param $xheader + * + * @return string + */ + public function find_x_header($xheader) + { $xheader = ucfirst(strtolower($xheader)); // check the header - if(isset($this->head_hash[$xheader])){ + if (isset($this->head_hash[$xheader])) { return $this->head_hash[$xheader]; } // check the body too $tmp_body_hash = $this->standard_parser($this->body_hash); - if(isset($tmp_body_hash[$xheader])){ + if (isset($tmp_body_hash[$xheader])) { return $tmp_body_hash[$xheader]; } return ""; } - - private function find_fbl_recipients($fbl){ - if(isset($fbl['Original-rcpt-to'])){ - return $fbl['Original-rcpt-to']; - } - else if(isset($fbl['Removal-recipient'])){ - return trim(str_replace('--', '', $fbl['Removal-recipient'])); + + /** + * @param $mime_sections + * + * @return array + */ + function get_head_from_returned_message_body_part($mime_sections) + { + $temp = explode("\r\n\r\n", $mime_sections[returned_message_body_part]); + $head = $this->standard_parser($temp[1]); + $head['From'] = $this->extract_address($head['From']); + $head['To'] = $this->extract_address($head['To']); + return $head; + } + + /** + * @param $str + * + * @return mixed + */ + function extract_address($str) + { + $from_stuff = preg_split('/[ \"\'\<\>:\(\)\[\]]/', $str); + foreach ($from_stuff as $things) { + if (strpos($things, '@') !== false) { + $from = $things; + } } + return $from; } - private function strip_angle_brackets($recipient){ - if (preg_match('/[<[](.*)[>\]]/', $recipient, $matches)) - return trim($matches[1]); - else - return trim($recipient); + /** + * @param $code + * + * @return string + */ + function fetch_status_messages($code) + { + include_once("bounce_statuscodes.php"); + $ret = $this->format_status_code($code); + $arr = explode('.', $ret['code']); + $str = "

" . $status_code_classes[$arr[0]]['title'] . " - " . + $status_code_classes[$arr[0]]['descr'] . " " . $status_code_subclasses[$arr[1] . "." . + $arr[2]]['title'] . " - " . $status_code_subclasses[$arr[1] . "." . $arr[2]]['descr'] . "

"; + return $str; } /*The syntax of the final-recipient field is as follows: "Final-Recipient" ":" address-type ";" generic-address */ - private function format_final_recipient_array($arr){ - $output = array('addr'=>'', - 'type'=>''); - if (isset($arr[1])) { - if (strpos($arr[0], '@') !== FALSE){ - $output['addr'] = $this->strip_angle_brackets($arr[0]); - $output['type'] = (!empty($arr[1])) ? trim($arr[1]) : 'unknown'; - } - else { - $output['type'] = trim($arr[0]); - $output['addr'] = $this->strip_angle_brackets($arr[1]); - } + + /** + * @param $fbl + * + * @return string + */ + private function find_fbl_recipients($fbl) + { + if (isset($fbl['Original-rcpt-to'])) { + return $fbl['Original-rcpt-to']; + } else if (isset($fbl['Removal-recipient'])) { + return trim(str_replace('--', '', $fbl['Removal-recipient'])); } - return $output; } -}/** END class BounceHandler **/ -?> +} + +/** END class BounceHandler **/ \ No newline at end of file diff --git a/bounce_responses.php b/bounce_responses.php index 37b1bf9..8967072 100644 --- a/bounce_responses.php +++ b/bounce_responses.php @@ -6,219 +6,414 @@ # text in messages from which to figure out what kind of bounce $bouncelist = array( - '[45]\d\d[- ]#?([45]\.\d\.\d{1,2})' => 'x', # use the code from the regex - 'Diagnostic[- ][Cc]ode: smtp; ?\d\d\ ([45]\.\d\.\d{1,2})' => 'x', # use the code from the regex - 'Status: ([45]\.\d\.\d{1,2})' => 'x', # use the code from the regex + '[45]\d\d[- ]#?([45]\.\d\.\d{1,2})' => 'x', + # use the code from the regex + 'Diagnostic[- ][Cc]ode: smtp; ?\d\d\ ([45]\.\d\.\d{1,2})' => 'x', + # use the code from the regex + 'Status: ([45]\.\d\.\d{1,2})' => 'x', + # use the code from the regex - 'not yet been delivered' => '4.2.0', # - 'Message will be retried for' => '4.2.0', # - 'Connection frequency limited\. http:\/\/service\.mail\.qq\.com' => '4.2.0', + 'not yet been delivered' => '4.2.0', + # + 'Message will be retried for' => '4.2.0', + # + 'Connection frequency limited\. http:\/\/service\.mail\.qq\.com' => '4.2.0', + 'Benutzer hat zuviele Mails auf dem Server' => '4.2.2', + #.DE "mailbox full" + 'exceeded storage allocation' => '4.2.2', + # + 'Mailbox full' => '4.2.2', + # + 'mailbox is full' => '4.2.2', + #BH + 'Mailbox quota usage exceeded' => '4.2.2', + #BH + 'Mailbox size limit exceeded' => '4.2.2', + # + 'over ?quota' => '4.2.2', + # + 'quota exceeded' => '4.2.2', + # + 'Quota violation' => '4.2.2', + # + 'User has exhausted allowed storage space' => '4.2.2', + # + 'User has too many messages on the server' => '4.2.2', + # + 'User mailbox exceeds allowed size' => '4.2.2', + # + 'mailfolder is full' => '4.2.2', + # + 'user has Exceeded' => '4.2.2', + # + 'not enough storage space' => '4.2.2', + # - 'Benutzer hat zuviele Mails auf dem Server' => '4.2.2', #.DE "mailbox full" - 'exceeded storage allocation' => '4.2.2', # - 'Mailbox full' => '4.2.2', # - 'mailbox is full' => '4.2.2', #BH - 'Mailbox quota usage exceeded' => '4.2.2', #BH - 'Mailbox size limit exceeded' => '4.2.2', # - 'over ?quota' => '4.2.2', # - 'quota exceeded' => '4.2.2', # - 'Quota violation' => '4.2.2', # - 'User has exhausted allowed storage space' => '4.2.2', # - 'User has too many messages on the server' => '4.2.2', # - 'User mailbox exceeds allowed size' => '4.2.2', # - 'mailfolder is full' => '4.2.2', # - 'user has Exceeded' => '4.2.2', # - 'not enough storage space' => '4.2.2', # + 'Delivery attempts will continue to be made for' => '4.3.2', + #SB: 4.3.2 is a more generic 'defer'; Kanon added. From Symantec_AntiVirus_for_SMTP_Gateways@uqam.ca Im not sure why Symantec delayed this message, but x.2.x means something to do with the mailbox, which seemed appropriate. x.5.x (protocol) or x.7.x (security) also seem possibly appropriate. It seems a lot of times its x.5.x when it seems to me it should be x.7.x, so maybe x.5.x is standard when mail is rejected due to spam-like characteristics instead of x.7.x like I think it should be. + 'delivery temporarily suspended' => '4.3.2', + # + 'Greylisted for 5 minutes' => '4.3.2', + # + 'Greylisting in action' => '4.3.2', + # + 'Server busy' => '4.3.2', + # + 'server too busy' => '4.3.2', + # + 'system load is too high' => '4.3.2', + # + 'temporarily deferred' => '4.3.2', + # + 'temporarily unavailable' => '4.3.2', + # + 'Throttling' => '4.3.2', + # + 'too busy to accept mail' => '4.3.2', + # + 'too many connections' => '4.3.2', + # + 'too many sessions' => '4.3.2', + # + 'Too much load' => '4.3.2', + # + 'try again later' => '4.3.2', + # + 'Try later' => '4.3.2', + # + 'retry timeout exceeded' => '4.4.7', + # + 'queue too long' => '4.4.7', + # - 'Delivery attempts will continue to be made for' => '4.3.2', #SB: 4.3.2 is a more generic 'defer'; Kanon added. From Symantec_AntiVirus_for_SMTP_Gateways@uqam.ca Im not sure why Symantec delayed this message, but x.2.x means something to do with the mailbox, which seemed appropriate. x.5.x (protocol) or x.7.x (security) also seem possibly appropriate. It seems a lot of times its x.5.x when it seems to me it should be x.7.x, so maybe x.5.x is standard when mail is rejected due to spam-like characteristics instead of x.7.x like I think it should be. - 'delivery temporarily suspended' => '4.3.2', # - 'Greylisted for 5 minutes' => '4.3.2', # - 'Greylisting in action' => '4.3.2', # - 'Server busy' => '4.3.2', # - 'server too busy' => '4.3.2', # - 'system load is too high' => '4.3.2', # - 'temporarily deferred' => '4.3.2', # - 'temporarily unavailable' => '4.3.2', # - 'Throttling' => '4.3.2', # - 'too busy to accept mail' => '4.3.2', # - 'too many connections' => '4.3.2', # - 'too many sessions' => '4.3.2', # - 'Too much load' => '4.3.2', # - 'try again later' => '4.3.2', # - 'Try later' => '4.3.2', # - 'retry timeout exceeded' => '4.4.7', # - 'queue too long' => '4.4.7', # + '554 delivery error:' => '5.1.1', + #SB: Yahoo/rogers.com generic delivery failure (see also OU-00) + 'account has been disabled' => '5.1.1', + # + 'account is unavailable' => '5.1.1', + # + 'Account not found' => '5.1.1', + # + 'Address invalid' => '5.1.1', + # + 'Address is unknown' => '5.1.1', + # + 'Address unknown' => '5.1.1', + # + 'Addressee unknown' => '5.1.1', + # + 'ADDRESS_NOT_FOUND' => '5.1.1', + # + 'bad address' => '5.1.1', + # + 'Bad destination mailbox address' => '5.1.1', + # + 'destin. Sconosciuto' => '5.1.1', + #.IT "user unknown" + 'Destinatario errato' => '5.1.1', + #.IT "invalid" + 'Destinatario sconosciuto o mailbox disatttivata' => '5.1.1', + #.IT "unknown /disabled" + 'does not exist' => '5.1.1', + # + 'Email Address was not found' => '5.1.1', + # + 'Excessive userid unknowns' => '5.1.1', + # + 'Indirizzo inesistente' => '5.1.1', + #.IT "no user" + 'Invalid account' => '5.1.1', + # + 'invalid address' => '5.1.1', + # + 'Invalid or unknown virtual user' => '5.1.1', + # + 'Invalid mailbox' => '5.1.1', + # + 'Invalid recipient' => '5.1.1', + # + 'Mailbox not found' => '5.1.1', + # + 'mailbox unavailable' => '5.1.1', + # + 'nie istnieje' => '5.1.1', + #.PL "does not exist" + 'Nie ma takiego konta' => '5.1.1', + #.PL "no such account" + 'No mail box available for this user' => '5.1.1', + # + 'no mailbox here' => '5.1.1', + # + 'No one with that email address here' => '5.1.1', + # + 'no such address' => '5.1.1', + # + 'no such email address' => '5.1.1', + # + 'No such mail drop defined' => '5.1.1', + # + 'No such mailbox' => '5.1.1', + # + 'No such person at this address' => '5.1.1', + # + 'no such recipient' => '5.1.1', + # + 'No such user' => '5.1.1', + # + 'not a known user' => '5.1.1', + # + 'not a valid mailbox' => '5.1.1', + # + 'not a valid user' => '5.1.1', + # + 'not available' => '5.1.1', + # + 'not exists' => '5.1.1', + # + 'Recipient address rejected' => '5.1.1', + # + 'Recipient not allowed' => '5.1.1', + # + 'Recipient not found' => '5.1.1', + # + 'recipient rejected' => '5.1.1', + # + 'Recipient unknown' => '5.1.1', + # + "server doesn't handle mail for that user" => '5.1.1', + # + 'This account is disabled' => '5.1.1', + # + 'This address no longer accepts mail' => '5.1.1', + # + 'This email address is not known to this system' => '5.1.1', + # + 'Unknown account' => '5.1.1', + # + 'unknown address or alias' => '5.1.1', + # + 'Unknown email address' => '5.1.1', + # + 'Unknown local part' => '5.1.1', + # + 'unknown or illegal alias' => '5.1.1', + # + 'unknown or illegal user' => '5.1.1', + # + 'Unknown recipient' => '5.1.1', + # + 'unknown user' => '5.1.1', + # + 'user disabled' => '5.1.1', + # + "User doesn't exist in this server" => '5.1.1', + # + 'user invalid' => '5.1.1', + # + 'User is suspended' => '5.1.1', + # + 'User is unknown' => '5.1.1', + # + 'User not found' => '5.1.1', + # + 'User not known' => '5.1.1', + # + 'User unknown' => '5.1.1', + # + 'valid RCPT command must precede DATA' => '5.1.1', + # + 'was not found in LDAP server' => '5.1.1', + # + 'We are sorry but the address is invalid' => '5.1.1', + # + 'Unable to find alias user' => '5.1.1', + # - '554 delivery error:' => '5.1.1', #SB: Yahoo/rogers.com generic delivery failure (see also OU-00) - 'account has been disabled' => '5.1.1', # - 'account is unavailable' => '5.1.1', # - 'Account not found' => '5.1.1', # - 'Address invalid' => '5.1.1', # - 'Address is unknown' => '5.1.1', # - 'Address unknown' => '5.1.1', # - 'Addressee unknown' => '5.1.1', # - 'ADDRESS_NOT_FOUND' => '5.1.1', # - 'bad address' => '5.1.1', # - 'Bad destination mailbox address' => '5.1.1', # - 'destin. Sconosciuto' => '5.1.1', #.IT "user unknown" - 'Destinatario errato' => '5.1.1', #.IT "invalid" - 'Destinatario sconosciuto o mailbox disatttivata' => '5.1.1', #.IT "unknown /disabled" - 'does not exist' => '5.1.1', # - 'Email Address was not found' => '5.1.1', # - 'Excessive userid unknowns' => '5.1.1', # - 'Indirizzo inesistente' => '5.1.1', #.IT "no user" - 'Invalid account' => '5.1.1', # - 'invalid address' => '5.1.1', # - 'Invalid or unknown virtual user' => '5.1.1', # - 'Invalid mailbox' => '5.1.1', # - 'Invalid recipient' => '5.1.1', # - 'Mailbox not found' => '5.1.1', # - 'mailbox unavailable' => '5.1.1', # - 'nie istnieje' => '5.1.1', #.PL "does not exist" - 'Nie ma takiego konta' => '5.1.1', #.PL "no such account" - 'No mail box available for this user' => '5.1.1', # - 'no mailbox here' => '5.1.1', # - 'No one with that email address here' => '5.1.1', # - 'no such address' => '5.1.1', # - 'no such email address' => '5.1.1', # - 'No such mail drop defined' => '5.1.1', # - 'No such mailbox' => '5.1.1', # - 'No such person at this address' => '5.1.1', # - 'no such recipient' => '5.1.1', # - 'No such user' => '5.1.1', # - 'not a known user' => '5.1.1', # - 'not a valid mailbox' => '5.1.1', # - 'not a valid user' => '5.1.1', # - 'not available' => '5.1.1', # - 'not exists' => '5.1.1', # - 'Recipient address rejected' => '5.1.1', # - 'Recipient not allowed' => '5.1.1', # - 'Recipient not found' => '5.1.1', # - 'recipient rejected' => '5.1.1', # - 'Recipient unknown' => '5.1.1', # - "server doesn't handle mail for that user" => '5.1.1', # - 'This account is disabled' => '5.1.1', # - 'This address no longer accepts mail' => '5.1.1', # - 'This email address is not known to this system' => '5.1.1', # - 'Unknown account' => '5.1.1', # - 'unknown address or alias' => '5.1.1', # - 'Unknown email address' => '5.1.1', # - 'Unknown local part' => '5.1.1', # - 'unknown or illegal alias' => '5.1.1', # - 'unknown or illegal user' => '5.1.1', # - 'Unknown recipient' => '5.1.1', # - 'unknown user' => '5.1.1', # - 'user disabled' => '5.1.1', # - "User doesn't exist in this server" => '5.1.1', # - 'user invalid' => '5.1.1', # - 'User is suspended' => '5.1.1', # - 'User is unknown' => '5.1.1', # - 'User not found' => '5.1.1', # - 'User not known' => '5.1.1', # - 'User unknown' => '5.1.1', # - 'valid RCPT command must precede DATA' => '5.1.1', # - 'was not found in LDAP server' => '5.1.1', # - 'We are sorry but the address is invalid' => '5.1.1', # - 'Unable to find alias user' => '5.1.1', # + "domain isn't in my list of allowed rcpthosts" => '5.1.2', + # + 'Esta casilla ha expirado por falta de uso' => '5.1.2', + #BH ES:expired + 'host ?name is unknown' => '5.1.2', + # + 'no relaying allowed' => '5.1.2', + # + 'no such domain' => '5.1.2', + # + 'not our customer' => '5.1.2', + # + 'relay not permitted' => '5.1.2', + # + 'Relay access denied' => '5.1.2', + # + 'relaying denied' => '5.1.2', + # + 'Relaying not allowed' => '5.1.2', + # + 'This system is not configured to relay mail' => '5.1.2', + # + 'Unable to relay' => '5.1.2', + # + 'unrouteable mail domain' => '5.1.2', + #BH + 'we do not relay' => '5.1.2', + # - "domain isn't in my list of allowed rcpthosts" => '5.1.2', # - 'Esta casilla ha expirado por falta de uso' => '5.1.2', #BH ES:expired - 'host ?name is unknown' => '5.1.2', # - 'no relaying allowed' => '5.1.2', # - 'no such domain' => '5.1.2', # - 'not our customer' => '5.1.2', # - 'relay not permitted' => '5.1.2', # - 'Relay access denied' => '5.1.2', # - 'relaying denied' => '5.1.2', # - 'Relaying not allowed' => '5.1.2', # - 'This system is not configured to relay mail' => '5.1.2', # - 'Unable to relay' => '5.1.2', # - 'unrouteable mail domain' => '5.1.2', #BH - 'we do not relay' => '5.1.2', # + 'Old address no longer valid' => '5.1.6', + # + 'recipient no longer on server' => '5.1.6', + # - 'Old address no longer valid' => '5.1.6', # - 'recipient no longer on server' => '5.1.6', # + 'Sender address rejected' => '5.1.8', + # - 'Sender address rejected' => '5.1.8', # + 'exceeded the rate limit' => '5.2.0', + # + 'Local Policy Violation' => '5.2.0', + # + 'Mailbox currently suspended' => '5.2.0', + # + 'mailbox unavailable' => '5.2.0', + # + 'mail can not be delivered' => '5.2.0', + # + 'Delivery failed' => '5.2.0', + # + 'mail couldn\'t be delivered' => '5.2.0', + # + 'The account or domain may not exist' => '5.2.0', + #I guess.... seems like 5.1.1, 5.1.2, or 5.4.4 would fit too, but 5.2.0 seemed most generic - 'exceeded the rate limit' => '5.2.0', # - 'Local Policy Violation' => '5.2.0', # - 'Mailbox currently suspended' => '5.2.0', # - 'mailbox unavailable' => '5.2.0', # - 'mail can not be delivered' => '5.2.0', # - 'Delivery failed' => '5.2.0', # - 'mail couldn\'t be delivered' => '5.2.0', # - 'The account or domain may not exist' => '5.2.0', #I guess.... seems like 5.1.1, 5.1.2, or 5.4.4 would fit too, but 5.2.0 seemed most generic + 'Account disabled' => '5.2.1', + # + 'account has been disabled' => '5.2.1', + # + 'Account Inactive' => '5.2.1', + # + 'Adressat unbekannt oder Mailbox deaktiviert' => '5.2.1', + # + 'Destinataire inconnu ou boite aux lettres desactivee' => '5.2.1', + #.FR disabled + 'mail is not currently being accepted for this mailbox' => '5.2.1', + # + 'El usuario esta en estado: inactivo' => '5.2.1', + #.IT inactive + 'email account that you tried to reach is disabled' => '5.2.1', + # + 'inactive user' => '5.2.1', + # + 'Mailbox disabled for this recipient' => '5.2.1', + # + 'mailbox has been blocked due to inactivity' => '5.2.1', + # + 'mailbox is currently unavailable' => '5.2.1', + # + 'Mailbox is disabled' => '5.2.1', + # + 'Mailbox is inactive' => '5.2.1', + # + 'Mailbox Locked or Suspended' => '5.2.1', + # + 'mailbox temporarily disabled' => '5.2.1', + # + 'Podane konto jest zablokowane administracyjnie lub nieaktywne' => '5.2.1', + #.PL locked or inactive + "Questo indirizzo e' bloccato per inutilizzo" => '5.2.1', + #.IT blocked/expired + 'Recipient mailbox was disabled' => '5.2.1', + # + 'Domain name not found' => '5.2.1', + 'couldn\'t find any host named' => '5.4.4', + # + 'couldn\'t find any host by that name' => '5.4.4', + # + 'PERM_FAILURE: DNS Error' => '5.4.4', + #SB: Routing failure + 'Temporary lookup failure' => '5.4.4', + # + 'unrouteable address' => '5.4.4', + # + "can't connect to" => '5.4.4', + # - 'Account disabled' => '5.2.1', # - 'account has been disabled' => '5.2.1', # - 'Account Inactive' => '5.2.1', # - 'Adressat unbekannt oder Mailbox deaktiviert' => '5.2.1', # - 'Destinataire inconnu ou boite aux lettres desactivee' => '5.2.1', #.FR disabled - 'mail is not currently being accepted for this mailbox' => '5.2.1', # - 'El usuario esta en estado: inactivo' => '5.2.1', #.IT inactive - 'email account that you tried to reach is disabled' => '5.2.1', # - 'inactive user' => '5.2.1', # - 'Mailbox disabled for this recipient' => '5.2.1', # - 'mailbox has been blocked due to inactivity' => '5.2.1', # - 'mailbox is currently unavailable' => '5.2.1', # - 'Mailbox is disabled' => '5.2.1', # - 'Mailbox is inactive' => '5.2.1', # - 'Mailbox Locked or Suspended' => '5.2.1', # - 'mailbox temporarily disabled' => '5.2.1', # - 'Podane konto jest zablokowane administracyjnie lub nieaktywne'=> '5.2.1', #.PL locked or inactive - "Questo indirizzo e' bloccato per inutilizzo" => '5.2.1', #.IT blocked/expired - 'Recipient mailbox was disabled' => '5.2.1', # - 'Domain name not found' => '5.2.1', + 'Too many hops' => '5.4.6', + # - 'couldn\'t find any host named' => '5.4.4', # - 'couldn\'t find any host by that name' => '5.4.4', # - 'PERM_FAILURE: DNS Error' => '5.4.4', #SB: Routing failure - 'Temporary lookup failure' => '5.4.4', # - 'unrouteable address' => '5.4.4', # - "can't connect to" => '5.4.4', # + 'Requested action aborted' => '5.5.0', + # - 'Too many hops' => '5.4.6', # + 'rejecting password protected file attachment' => '5.6.2', + #RFC "Conversion required and prohibited" - 'Requested action aborted' => '5.5.0', # - - 'rejecting password protected file attachment' => '5.6.2', #RFC "Conversion required and prohibited" - - '550 OU-00' => '5.7.1', #SB hotmail returns a OU-001 if you're on their blocklist - '550 SC-00' => '5.7.1', #SB hotmail returns a SC-00x if you're on their blocklist - '550 DY-00' => '5.7.1', #SB hotmail returns a DY-00x if you're a dynamic IP - '554 denied' => '5.7.1', # - 'You have been blocked by the recipient' => '5.7.1', # - 'requires that you verify' => '5.7.1', # - 'Access denied' => '5.7.1', # - 'Administrative prohibition - unable to validate recipient' => '5.7.1', # - 'Blacklisted' => '5.7.1', # - 'blocke?d? for spam' => '5.7.1', # - 'conection refused' => '5.7.1', # - 'Connection refused due to abuse' => '5.7.1', # - 'dial-up or dynamic-ip denied' => '5.7.1', # - 'Domain has received too many bounces' => '5.7.1', # - 'failed several antispam checks' => '5.7.1', # - 'found in a DNS blacklist' => '5.7.1', # - 'IPs blocked' => '5.7.1', # - 'is blocked by' => '5.7.1', # - 'Mail Refused' => '5.7.1', # - 'Message does not pass DomainKeys' => '5.7.1', # - 'Message looks like spam' => '5.7.1', # - 'Message refused by' => '5.7.1', # - 'not allowed access from your location' => '5.7.1', # - 'permanently deferred' => '5.7.1', # - 'Rejected by policy' => '5.7.1', # - 'rejected by Windows Live Hotmail for policy reasons' => '5.7.1', # - 'Rejected for policy reasons' => '5.7.1', # - 'Rejecting banned content' => '5.7.1', # - 'Sorry, looks like spam' => '5.7.1', # - 'spam message discarded' => '5.7.1', # - 'Too many spams from your IP' => '5.7.1', # - 'TRANSACTION FAILED' => '5.7.1', # - 'Transaction rejected' => '5.7.1', # - 'Wiadomosc zostala odrzucona przez system antyspamowy' => '5.7.1', #.PL rejected as spam - 'Your message was declared Spam' => '5.7.1' # + '550 OU-00' => '5.7.1', + #SB hotmail returns a OU-001 if you're on their blocklist + '550 SC-00' => '5.7.1', + #SB hotmail returns a SC-00x if you're on their blocklist + '550 DY-00' => '5.7.1', + #SB hotmail returns a DY-00x if you're a dynamic IP + '554 denied' => '5.7.1', + # + 'You have been blocked by the recipient' => '5.7.1', + # + 'requires that you verify' => '5.7.1', + # + 'Access denied' => '5.7.1', + # + 'Administrative prohibition - unable to validate recipient' => '5.7.1', + # + 'Blacklisted' => '5.7.1', + # + 'blocke?d? for spam' => '5.7.1', + # + 'conection refused' => '5.7.1', + # + 'Connection refused due to abuse' => '5.7.1', + # + 'dial-up or dynamic-ip denied' => '5.7.1', + # + 'Domain has received too many bounces' => '5.7.1', + # + 'failed several antispam checks' => '5.7.1', + # + 'found in a DNS blacklist' => '5.7.1', + # + 'IPs blocked' => '5.7.1', + # + 'is blocked by' => '5.7.1', + # + 'Mail Refused' => '5.7.1', + # + 'Message does not pass DomainKeys' => '5.7.1', + # + 'Message looks like spam' => '5.7.1', + # + 'Message refused by' => '5.7.1', + # + 'not allowed access from your location' => '5.7.1', + # + 'permanently deferred' => '5.7.1', + # + 'Rejected by policy' => '5.7.1', + # + 'rejected by Windows Live Hotmail for policy reasons' => '5.7.1', + # + 'Rejected for policy reasons' => '5.7.1', + # + 'Rejecting banned content' => '5.7.1', + # + 'Sorry, looks like spam' => '5.7.1', + # + 'spam message discarded' => '5.7.1', + # + 'Too many spams from your IP' => '5.7.1', + # + 'TRANSACTION FAILED' => '5.7.1', + # + 'Transaction rejected' => '5.7.1', + # + 'Wiadomosc zostala odrzucona przez system antyspamowy' => '5.7.1', + #.PL rejected as spam + 'Your message was declared Spam' => '5.7.1' + # ); # triggers for autoresponders @@ -245,7 +440,7 @@ 'delivery status notif', 'failure delivery', 'failure notice', - 'mail delivery fail', #catches failure and failed + 'mail delivery fail', #catches failure and failed 'mail delivery system', 'mailserver notification', 'mail status report', @@ -254,9 +449,9 @@ 'mdaemon notification', 'message delayed', 'nondeliverable mail', - 'Non[_ ]remis[_ ]', #fr - 'No[_ ]se[_ ]puede[_ ]entregar', #es - 'Onbestelbaar', #nl + 'Non[_ ]remis[_ ]', #fr + 'No[_ ]se[_ ]puede[_ ]entregar', #es + 'Onbestelbaar', #nl 'returned e?mail', 'returned to sender', 'returning message to sender', @@ -275,5 +470,4 @@ # print "'$l1'($j) = '$l2'($k)\n"; # } # } -#} -?> \ No newline at end of file +#} \ No newline at end of file diff --git a/cmdlinetest.php b/cmdlinetest.php index 1a6459d..d3327fc 100644 --- a/cmdlinetest.php +++ b/cmdlinetest.php @@ -3,61 +3,60 @@ $total = array(); -function checkmail($email) { +function checkmail($email) +{ global $total; $bh = new Bouncehandler(); $bounceinfo = $bh->get_the_facts($email); -# var_dump($bounceinfo); -# var_dump($bh); - print "TYPE ". @$bh->type. "\n"; + # var_dump($bounceinfo); + # var_dump($bh); + print "TYPE " . @$bh->type . "\n"; if ($bh->type == 'bounce') { - print "ACTION ". $bounceinfo[0]['action']. "\n"; - print "STATUS ". $bounceinfo[0]['status']. "\n"; - print "RECIPIENT ". $bounceinfo[0]['recipient']. "\n"; + print "ACTION " . $bounceinfo[0]['action'] . "\n"; + print "STATUS " . $bounceinfo[0]['status'] . "\n"; + print "RECIPIENT " . $bounceinfo[0]['recipient'] . "\n"; } if ($bh->type == 'fbl') { - print "ENV FROM ". @$bh->fbl_hash['Original-mail-from']. "\n"; - print "AGENT ". @$bh->fbl_hash['User-agent']. "\n"; - print "IP ". @$bh->fbl_hash['Source-ip']. "\n"; + print "ENV FROM " . @$bh->fbl_hash['Original-mail-from'] . "\n"; + print "AGENT " . @$bh->fbl_hash['User-agent'] . "\n"; + print "IP " . @$bh->fbl_hash['Source-ip'] . "\n"; } if ($bh->type == 'autoresponse') { - print "AUTO ". $bounceinfo[0]['autoresponse']. "\n"; + print "AUTO " . $bounceinfo[0]['autoresponse'] . "\n"; + } + if ($bh->type) { + @$total[$bh->type]++; + } else { + @$total['unknown']++; } - if ($bh->type) - @$total[$bh->type]++; - else - @$total['unknown']++; print "\n"; } - if (defined('STDIN')) { if (count($argv) > 1) { for ($i = 1; $i < count($argv); $i++) { if (is_dir($argv[$i])) { $dh = opendir($argv[$i]); while ($fn = readdir($dh)) { - if (substr($fn,0,1) !== '.') { - $email = file_get_contents($argv[$i].'/'.$fn); - print $argv[1]."/$fn\n"; + if (substr($fn, 0, 1) !== '.') { + $email = file_get_contents($argv[$i] . '/' . $fn); + print $argv[1] . "/$fn\n"; checkmail($email); } } - } - else { + } else { $email = file_get_contents($argv[$i]); - print $argv[1]."\n"; + print $argv[1] . "\n"; checkmail($email); } } foreach ($total as $k => $v) { print "$k = $v\n"; } - - } - else { - $handle = fopen("php://stdin","r"); + + } else { + $handle = fopen("php://stdin", "r"); $email = stream_get_contents($handle); fclose($handle); checkmail($email); diff --git a/testdriver1.php b/testdriver1.php index 12419dc..3e20f0c 100644 --- a/testdriver1.php +++ b/testdriver1.php @@ -7,26 +7,26 @@ require_once("bounce_driver.class.php"); $bouncehandler = new Bouncehandler(); -if(!empty($_GET['testall'])){ +if (!empty($_GET['testall'])) { $files = get_sorted_file_list('eml'); if (is_array($files)) { - echo "

File Tests:

\n"; - foreach($files as $file) { - echo "$file "; - $bounce = file_get_contents("eml/".$file); + echo "

File Tests:

\n"; + foreach ($files as $file) { + echo "$file "; + $bounce = file_get_contents("eml/" . $file); $multiArray = $bouncehandler->get_the_facts($bounce); - if( !empty($multiArray[0]['action']) - && !empty($multiArray[0]['status']) - && !empty($multiArray[0]['recipient']) ){ + if (!empty($multiArray[0]['action']) + && !empty($multiArray[0]['status']) + && !empty($multiArray[0]['recipient']) + ) { print " - Passed
\n"; - } - else{ + } else { print " - WRONG
\n"; print "
\n";
                 print_r($multiArray[0]);
                 print "
\n"; } - } + } } } ?> @@ -36,69 +36,92 @@

Chris Fortune ~ http://cfortune.kics.bc.ca

+

June 19, 2014