From 121e338000f3f7f8d80581e02b086e0a970a2029 Mon Sep 17 00:00:00 2001 From: Lars Moelleken Date: Fri, 19 Jul 2019 02:31:08 +0200 Subject: [PATCH] [+]: update code style v2 + code clean-up --- .editorconfig | 6 +- composer.json | 7 +- src/BounceMailHandler/BounceMailHandler.php | 772 ++++++++++---------- 3 files changed, 393 insertions(+), 392 deletions(-) diff --git a/.editorconfig b/.editorconfig index d1d8a41..8c13b37 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,8 +3,8 @@ root = true [*] indent_style = space -indent_size = 2 +indent_size = 4 end_of_line = lf charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true +#trim_trailing_whitespace = true +#insert_final_newline = true diff --git a/composer.json b/composer.json index 52d3ac1..4a82da0 100644 --- a/composer.json +++ b/composer.json @@ -34,11 +34,14 @@ "ext-imap": "*" }, "require-dev": { - "phpunit/phpunit": "~6.0|~7.0" + "phpunit/phpunit": "~6.0 || ~7.0" }, "autoload": { "psr-0": { "BounceMailHandler": "src/" - } + }, + "files": [ + "src/BounceMailHandler/phpmailer-bmh_rules.php" + ] } } diff --git a/src/BounceMailHandler/BounceMailHandler.php b/src/BounceMailHandler/BounceMailHandler.php index 4eaf2b4..153feb3 100644 --- a/src/BounceMailHandler/BounceMailHandler.php +++ b/src/BounceMailHandler/BounceMailHandler.php @@ -7,12 +7,17 @@ * * @copyright 2008-2009 Andry Prevost. All Rights Reserved. * @copyright 2011-2012 Anthon Pang. - * @copyright 2015-2016 Lars Moelleken. + * @copyright 2015-2019 Lars Moelleken. * @license GPL */ namespace BounceMailHandler; -require_once __DIR__ . '/phpmailer-bmh_rules.php'; +use function bmhBodyRules; +use function bmhDSNRules; +use const CL_EXPUNGE; +use const OP_HALFOPEN; +use const OP_READONLY; +use const SORTDATE; /** * BounceMailHandler class @@ -25,20 +30,15 @@ */ class BounceMailHandler { - const VERBOSE_QUIET = 0; // suppress output + const SECONDS_TIMEOUT = 6000; - const VERBOSE_SIMPLE = 1; // simple report + const VERBOSE_DEBUG = 3; // detailed report plus debug info - const VERBOSE_REPORT = 2; // detailed report + const VERBOSE_QUIET = 0; // suppress output - const VERBOSE_DEBUG = 3; // detailed report plus debug info + const VERBOSE_REPORT = 2; // detailed report - /** - * get version - * - * @return string - */ - const SECONDS_TIMEOUT = 6000; + const VERBOSE_SIMPLE = 1; // simple report /** * mail-server @@ -303,47 +303,6 @@ public function getVersion(): string return $this->version; } - /** - * open a mail box - * - * @return bool - */ - public function openMailbox(): bool - { - // 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 . '
'; - $this->globalDelete(); - } - - // disable move operations if server is Gmail ... Gmail does not support mailbox creation - if (\stripos($this->mailhost, 'gmail') !== false) { - $this->moveSoft = false; - $this->moveHard = false; - } - - $port = $this->port . '/' . $this->service . '/' . $this->serviceOption; - - \set_time_limit(self::SECONDS_TIMEOUT); - - if (!$this->testMode) { - $this->mailboxLink = \imap_open('{' . $this->mailhost . ':' . $port . '}' . $this->boxname, $this->mailboxUserName, $this->mailboxPassword, \CL_EXPUNGE | ($this->testMode ? \OP_READONLY : 0)); - } else { - $this->mailboxLink = \imap_open('{' . $this->mailhost . ':' . $port . '}' . $this->boxname, $this->mailboxUserName, $this->mailboxPassword, ($this->testMode ? \OP_READONLY : 0)); - } - - if (!$this->mailboxLink) { - $this->errorMessage = 'Cannot create ' . $this->service . ' connection to ' . $this->mailhost . $this->bmhNewLine . 'Error MSG: ' . \imap_last_error(); - $this->output(); - - return false; - } - - $this->output('Connected to: ' . $this->mailhost . ' (' . $this->mailboxUserName . ')'); - - return true; - } - /** * Function to delete messages in a mailbox, based on date * @@ -355,7 +314,7 @@ public function globalDelete(): bool $delDate = \mktime(0, 0, 0, $dateArr[1], $dateArr[2], $dateArr[0]); $port = $this->port . '/' . $this->service . '/' . $this->serviceOption; - $mboxt = \imap_open('{' . $this->mailhost . ':' . $port . '}', $this->mailboxUserName, $this->mailboxPassword, \OP_HALFOPEN); + $mboxt = \imap_open('{' . $this->mailhost . ':' . $port . '}', $this->mailboxUserName, $this->mailboxPassword, OP_HALFOPEN); if ($mboxt === false) { return false; @@ -370,9 +329,8 @@ public function globalDelete(): bool $nameRaw = $nameArr[\count($nameArr) - 1]; if (\stripos($nameRaw, 'sent') === false) { - $mboxd = \imap_open('{' . $this->mailhost . ':' . $port . '}' . $nameRaw, $this->mailboxUserName, $this->mailboxPassword, \CL_EXPUNGE); - $messages = \imap_sort($mboxd, \SORTDATE, 0); - $i = 0; + $mboxd = \imap_open('{' . $this->mailhost . ':' . $port . '}' . $nameRaw, $this->mailboxUserName, $this->mailboxPassword, CL_EXPUNGE); + $messages = \imap_sort($mboxd, SORTDATE, 0); foreach ($messages as $message) { $header = \imap_headerinfo($mboxd, $message); @@ -381,7 +339,6 @@ public function globalDelete(): bool if ($header->udate < $delDate) { \imap_delete($mboxd, $message); } - ++$i; } \imap_expunge($mboxd); @@ -400,20 +357,83 @@ public function globalDelete(): bool } /** - * output additional msg for debug + * Function to determine if a particular value is found in a imap_fetchstructure key. * - * @param mixed $msg if not given, output the last error msg - * @param int $verboseLevel the output level of this message + * @param array $currParameters imap_fetstructure parameters + * @param string $varKey imap_fetstructure key + * @param string $varValue value to check for + * + * @return bool */ - public function output($msg = '', int $verboseLevel = self::VERBOSE_SIMPLE) + public function isParameter(array $currParameters, string $varKey, string $varValue): bool { - if ($this->verbose >= $verboseLevel) { - if ($msg) { - echo $msg . $this->bmhNewLine; - } else { - echo $this->errorMessage . $this->bmhNewLine; + foreach ($currParameters as $object) { + if ( + \strtoupper($object->attribute) == \strtoupper($varKey) + && + \strtoupper($object->value) == \strtoupper($varValue) + ) { + return true; + } + } + + return false; + } + + /** + * 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 bool + */ + public function mailboxExist(string $mailbox, bool $create = true): bool + { + if (\trim($mailbox) === '') { + // 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: ' . $mailbox . "
\n"; + exit(); + } + + $port = $this->port . '/' . $this->service . '/' . $this->serviceOption; + /** @noinspection PhpUsageOfSilenceOperatorInspection */ + $mbox = @\imap_open('{' . $this->mailhost . ':' . $port . '}', $this->mailboxUserName, $this->mailboxPassword, OP_HALFOPEN); + + if ($mbox === false) { + return false; + } + + $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 ($mailboxFound === false && $create) { + /** @noinspection PhpUsageOfSilenceOperatorInspection */ + @\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; } /** @@ -428,9 +448,9 @@ public function openLocal(string $filePath): bool \set_time_limit(self::SECONDS_TIMEOUT); if (!$this->testMode) { - $this->mailboxLink = \imap_open($filePath, '', '', \CL_EXPUNGE | ($this->testMode ? \OP_READONLY : 0)); + $this->mailboxLink = \imap_open($filePath, '', '', CL_EXPUNGE | ($this->testMode ? OP_READONLY : 0)); } else { - $this->mailboxLink = \imap_open($filePath, '', '', ($this->testMode ? \OP_READONLY : 0)); + $this->mailboxLink = \imap_open($filePath, '', '', ($this->testMode ? OP_READONLY : 0)); } if (!$this->mailboxLink) { @@ -446,327 +466,148 @@ public function openLocal(string $filePath): bool } /** - * process the messages in a mailbox - * - * @param bool|int $max $max maximum limit messages processed in one batch, - * if not given uses the property $maxMessages + * open a mail box * * @return bool */ - public function processMailbox($max = false): bool + public function openMailbox(): bool { - if ( - empty($this->actionFunction) - || - !\is_callable($this->actionFunction) - ) { - $this->errorMessage = 'Action function not found!'; - $this->output(); - - return false; + // 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 . '
'; + $this->globalDelete(); } - if ($this->moveHard && ($this->disableDelete === false)) { - $this->disableDelete = true; + // disable move operations if server is Gmail ... Gmail does not support mailbox creation + if (\stripos($this->mailhost, 'gmail') !== false) { + $this->moveSoft = false; + $this->moveHard = false; } - if (!empty($max)) { - $this->maxMessages = $max; + $port = $this->port . '/' . $this->service . '/' . $this->serviceOption; + + \set_time_limit(self::SECONDS_TIMEOUT); + + if (!$this->testMode) { + $this->mailboxLink = \imap_open('{' . $this->mailhost . ':' . $port . '}' . $this->boxname, $this->mailboxUserName, $this->mailboxPassword, CL_EXPUNGE | ($this->testMode ? OP_READONLY : 0)); + } else { + $this->mailboxLink = \imap_open('{' . $this->mailhost . ':' . $port . '}' . $this->boxname, $this->mailboxUserName, $this->mailboxPassword, ($this->testMode ? OP_READONLY : 0)); } - // initialize counters - $totalCount = \imap_num_msg($this->mailboxLink); - $fetchedCount = $totalCount; - $processedCount = 0; - $unprocessedCount = 0; - $deletedCount = 0; - $movedCount = 0; - $this->output('Total: ' . $totalCount . ' messages '); + if (!$this->mailboxLink) { + $this->errorMessage = 'Cannot create ' . $this->service . ' connection to ' . $this->mailhost . $this->bmhNewLine . 'Error MSG: ' . \imap_last_error(); + $this->output(); - // process maximum number of messages - if ($fetchedCount > $this->maxMessages) { - $fetchedCount = $this->maxMessages; - $this->output('Processing first ' . $fetchedCount . ' messages '); + return false; } - if ($this->testMode) { - $this->output('Running in test mode, not deleting messages from mailbox
'); - } else { - if ($this->disableDelete) { - if ($this->moveHard) { - $this->output('Running in move mode
'); - } else { - $this->output('Running in disableDelete mode, not deleting messages from mailbox
'); - } + $this->output('Connected to: ' . $this->mailhost . ' (' . $this->mailboxUserName . ')'); + + return true; + } + + /** + * output additional msg for debug + * + * @param mixed $msg if not given, output the last error msg + * @param int $verboseLevel the output level of this message + */ + public function output($msg = '', int $verboseLevel = self::VERBOSE_SIMPLE) + { + if ($this->verbose >= $verboseLevel) { + if ($msg) { + echo $msg . $this->bmhNewLine; } else { - $this->output('Processed messages will be deleted from mailbox
'); + echo $this->errorMessage . $this->bmhNewLine; } } + } - for ($x = 1; $x <= $fetchedCount; ++$x) { + /** + * Function to process each individual message. + * + * @param int $pos message number + * @param string $type DNS or BODY type + * @param int $totalFetched total number of messages in mailbox + * + * @return array|false

"$result"-array or false

+ */ + public function processBounce(int $pos, string $type, int $totalFetched) + { + $header = \imap_headerinfo($this->mailboxLink, $pos); + $subject = isset($header->subject) ? \strip_tags($header->subject) : '[NO SUBJECT]'; + $body = ''; + $headerFull = \imap_fetchheader($this->mailboxLink, $pos); + $bodyFull = \imap_body($this->mailboxLink, $pos); - // fetch the messages one at a time - if ($this->useFetchstructure) { - /** @noinspection PhpUsageOfSilenceOperatorInspection */ - $structure = @\imap_fetchstructure($this->mailboxLink, $x); + if ($type == 'DSN') { + // first part of DSN (Delivery Status Notification), human-readable explanation + $dsnMsg = \imap_fetchbody($this->mailboxLink, $pos, '1'); + $dsnMsgStructure = \imap_bodystruct($this->mailboxLink, $pos, '1'); - if ( - $structure - && - \is_object($structure) - && - $structure->type == 1 - && - $structure->ifsubtype - && - $structure->ifparameters - && - \strtoupper($structure->subtype) == 'REPORT' - && - $this->isParameter($structure->parameters, 'REPORT-TYPE', 'delivery-status') - ) { - $processedResult = $this->processBounce($x, 'DSN', $totalCount); - } else { - // not standard DSN msg - $this->output('Msg #' . $x . ' is not a standard DSN message', self::VERBOSE_REPORT); + if ($dsnMsgStructure->encoding == 4) { + $dsnMsg = \quoted_printable_decode($dsnMsg); + } elseif ($dsnMsgStructure->encoding == 3) { + $dsnMsg = \base64_decode($dsnMsg, true); + } - if ($this->debugBodyRule) { - if ($structure->ifdescription) { - $this->output(" Content-Type : {$structure->description}", self::VERBOSE_DEBUG); - } else { - $this->output(' Content-Type : unsupported', self::VERBOSE_DEBUG); - } - } + // second part of DSN (Delivery Status Notification), delivery-status + $dsnReport = \imap_fetchbody($this->mailboxLink, $pos, '2'); - $processedResult = $this->processBounce($x, 'BODY', $totalCount); - } - } else { - $header = \imap_fetchheader($this->mailboxLink, $x); - - // Could be multi-line, if the new line begins with SPACE or HTAB - if (\preg_match("/Content-Type:((?:[^\n]|\n[\t ])+)(?:\n[^\t ]|$)/i", $header, $match)) { - if ( - \preg_match("/multipart\/report/i", $match[1]) - && - \preg_match("/report-type=[\"']?delivery-status[\"']?/i", $match[1]) - ) { - // standard DSN msg - $processedResult = $this->processBounce($x, 'DSN', $totalCount); - } else { - // not standard DSN msg - $this->output('Msg #' . $x . ' is not a standard DSN message', self::VERBOSE_REPORT); - - if ($this->debugBodyRule) { - $this->output(" Content-Type : {$match[1]}", self::VERBOSE_DEBUG); - } - - $processedResult = $this->processBounce($x, 'BODY', $totalCount); - } - } else { - // didn't get content-type header - $this->output('Msg #' . $x . ' is not a well-formatted MIME mail, missing Content-Type', self::VERBOSE_REPORT); - - if ($this->debugBodyRule) { - $this->output(' Headers: ' . $this->bmhNewLine . $header . $this->bmhNewLine, self::VERBOSE_DEBUG); - } - - $processedResult = $this->processBounce($x, 'BODY', $totalCount); - } - } - - $deleteFlag[$x] = false; - $moveFlag[$x] = false; - - if ($processedResult !== false) { - ++$processedCount; - - if (!$this->disableDelete) { - // delete the bounce if not in disableDelete mode - if (!$this->testMode) { - /** @noinspection PhpUsageOfSilenceOperatorInspection */ - @\imap_delete($this->mailboxLink, $x); - } - - $deleteFlag[$x] = true; - ++$deletedCount; - } elseif ($this->moveHard && $processedResult['bounce_type'] === 'hard') { - // check if the move directory exists, if not create it - if (!$this->testMode) { - $this->mailboxExist($this->hardMailbox); - } - - // move the message - if (!$this->testMode) { - /** @noinspection PhpUsageOfSilenceOperatorInspection */ - @\imap_mail_move($this->mailboxLink, (string) $x, $this->hardMailbox); - } - - $moveFlag[$x] = true; - ++$movedCount; - } elseif ($this->moveSoft && $processedResult['bounce_type'] === 'soft') { - // check if the move directory exists, if not create it - if (!$this->testMode) { - $this->mailboxExist($this->softMailbox); - } - - // move the message - if (!$this->testMode) { - /** @noinspection PhpUsageOfSilenceOperatorInspection */ - @\imap_mail_move($this->mailboxLink, (string) $x, $this->softMailbox); - } - - $moveFlag[$x] = true; - ++$movedCount; - } - } else { - // not processed - ++$unprocessedCount; - if (!$this->disableDelete && $this->purgeUnprocessed) { - // delete this bounce if not in disableDelete mode, and the flag BOUNCE_PURGE_UNPROCESSED is set - if (!$this->testMode) { - /** @noinspection PhpUsageOfSilenceOperatorInspection */ - @\imap_delete($this->mailboxLink, $x); - } - - $deleteFlag[$x] = true; - ++$deletedCount; - } - - // check if the move directory exists, if not create it - $this->mailboxExist($this->unprocessedBox); - // move the message - /** @noinspection PhpUsageOfSilenceOperatorInspection */ - @\imap_mail_move($this->mailboxLink, (string) $x, $this->unprocessedBox); - $moveFlag[$x] = true; - } - - \flush(); - } - - $this->output($this->bmhNewLine . 'Closing mailbox, and purging messages'); - - /** @noinspection PhpUsageOfSilenceOperatorInspection */ - @\imap_expunge($this->mailboxLink); - \imap_close($this->mailboxLink); - - $this->output('Read: ' . $fetchedCount . ' messages'); - $this->output($processedCount . ' action taken'); - $this->output($unprocessedCount . ' no action taken'); - $this->output($deletedCount . ' messages deleted'); - $this->output($movedCount . ' messages 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(array $currParameters, string $varKey, string $varValue): bool - { - foreach ($currParameters as $object) { - if ( - \strtoupper($object->attribute) == \strtoupper($varKey) - && - \strtoupper($object->value) == \strtoupper($varValue) - ) { - return true; - } - } - - return false; - } - - /** - * Function to process each individual message. - * - * @param int $pos message number - * @param string $type DNS or BODY type - * @param int $totalFetched total number of messages in mailbox - * - * @return array|false

"$result"-array or false

- */ - public function processBounce(int $pos, string $type, int $totalFetched) - { - $header = \imap_headerinfo($this->mailboxLink, $pos); - $subject = isset($header->subject) ? \strip_tags($header->subject) : '[NO SUBJECT]'; - $body = ''; - $headerFull = \imap_fetchheader($this->mailboxLink, $pos); - $bodyFull = \imap_body($this->mailboxLink, $pos); - - if ($type == 'DSN') { - // first part of DSN (Delivery Status Notification), human-readable explanation - $dsnMsg = \imap_fetchbody($this->mailboxLink, $pos, '1'); - $dsnMsgStructure = \imap_bodystruct($this->mailboxLink, $pos, '1'); - - if ($dsnMsgStructure->encoding == 4) { - $dsnMsg = \quoted_printable_decode($dsnMsg); - } elseif ($dsnMsgStructure->encoding == 3) { - $dsnMsg = \base64_decode($dsnMsg, true); - } - - // second part of DSN (Delivery Status Notification), delivery-status - $dsnReport = \imap_fetchbody($this->mailboxLink, $pos, '2'); - - // process bounces by rules - $result = \bmhDSNRules($dsnMsg, $dsnReport, $this->debugDsnRule); - $result = \is_callable($this->customDSNRulesCallback) ? \call_user_func($this->customDSNRulesCallback, $result, $dsnMsg, $dsnReport, $this->debugDsnRule) : $result; - } elseif ($type == 'BODY') { - /** @noinspection PhpUsageOfSilenceOperatorInspection */ - $structure = @\imap_fetchstructure($this->mailboxLink, $pos); + // process bounces by rules + $result = bmhDSNRules($dsnMsg, $dsnReport, $this->debugDsnRule); + $result = \is_callable($this->customDSNRulesCallback) ? \call_user_func($this->customDSNRulesCallback, $result, $dsnMsg, $dsnReport, $this->debugDsnRule) : $result; + } elseif ($type == 'BODY') { + /** @noinspection PhpUsageOfSilenceOperatorInspection */ + $structure = @\imap_fetchstructure($this->mailboxLink, $pos); if (!\is_object($structure)) { return false; } switch ($structure->type) { - case 0: // Content-type = text - $body = \imap_fetchbody($this->mailboxLink, $pos, '1'); - $result = \bmhBodyRules($body, $structure, $this->debugBodyRule); - $result = \is_callable($this->customBodyRulesCallback) ? \call_user_func($this->customBodyRulesCallback, $result, $body, $structure, $this->debugBodyRule) : $result; + case 0: // Content-type = text + $body = \imap_fetchbody($this->mailboxLink, $pos, '1'); + $result = bmhBodyRules($body, $structure, $this->debugBodyRule); + $result = \is_callable($this->customBodyRulesCallback) ? \call_user_func($this->customBodyRulesCallback, $result, $body, $structure, $this->debugBodyRule) : $result; - break; + break; - case 1: // Content-type = multipart - $body = \imap_fetchbody($this->mailboxLink, $pos, '1'); + case 1: // Content-type = multipart + $body = \imap_fetchbody($this->mailboxLink, $pos, '1'); - // Detect encoding and decode - only base64 - if ($structure->parts[0]->encoding == 4) { - $body = \quoted_printable_decode($body); - } elseif ($structure->parts[0]->encoding == 3) { - $body = \base64_decode($body, true); - } + // Detect encoding and decode - only base64 + if ($structure->parts[0]->encoding == 4) { + $body = \quoted_printable_decode($body); + } elseif ($structure->parts[0]->encoding == 3) { + $body = \base64_decode($body, true); + } - $result = \bmhBodyRules($body, $structure, $this->debugBodyRule); - $result = \is_callable($this->customBodyRulesCallback) ? \call_user_func($this->customBodyRulesCallback, $result, $body, $structure, $this->debugBodyRule) : $result; + $result = bmhBodyRules($body, $structure, $this->debugBodyRule); + $result = \is_callable($this->customBodyRulesCallback) ? \call_user_func($this->customBodyRulesCallback, $result, $body, $structure, $this->debugBodyRule) : $result; - break; + break; - case 2: // Content-type = message - $body = \imap_body($this->mailboxLink, $pos); + case 2: // Content-type = message + $body = \imap_body($this->mailboxLink, $pos); - if ($structure->encoding == 4) { - $body = \quoted_printable_decode($body); - } elseif ($structure->encoding == 3) { - $body = \base64_decode($body, true); - } + if ($structure->encoding == 4) { + $body = \quoted_printable_decode($body); + } elseif ($structure->encoding == 3) { + $body = \base64_decode($body, true); + } - $body = \substr($body, 0, 1000); - $result = \bmhBodyRules($body, $structure, $this->debugBodyRule); - $result = \is_callable($this->customBodyRulesCallback) ? \call_user_func($this->customBodyRulesCallback, $result, $body, $structure, $this->debugBodyRule) : $result; + $body = \substr($body, 0, 1000); + $result = bmhBodyRules($body, $structure, $this->debugBodyRule); + $result = \is_callable($this->customBodyRulesCallback) ? \call_user_func($this->customBodyRulesCallback, $result, $body, $structure, $this->debugBodyRule) : $result; - break; + break; - default: // un-support Content-type - $this->output('Msg #' . $pos . ' is unsupported Content-Type:' . $structure->type, self::VERBOSE_REPORT); + default: // un-support Content-type + $this->output('Msg #' . $pos . ' is unsupported Content-Type:' . $structure->type, self::VERBOSE_REPORT); - return false; - } + return false; + } } else { // internal error $this->errorMessage = 'Internal Error: unknown type'; @@ -802,10 +643,10 @@ public function processBounce(int $pos, string $type, int $totalFetched) if ($ruleNumber === '0000') { // unrecognized if ( - \trim($email) == '' - && - \property_exists($header, 'fromaddress') === true - ) { + \trim($email) === '' + && + \property_exists($header, 'fromaddress') === true + ) { $email = $header->fromaddress; } @@ -866,58 +707,215 @@ public function processBounce(int $pos, string $type, int $totalFetched) } /** - * Function to check if a mailbox exists - if not found, it will create it. + * process the messages in a mailbox * - * @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 + * @param bool|int $max $max maximum limit messages processed in one batch, + * if not given uses the property $maxMessages * * @return bool */ - public function mailboxExist(string $mailbox, bool $create = true): bool + public function processMailbox($max = false): bool { - if (\trim($mailbox) == '') { - // 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: ' . $mailbox . "
\n"; - exit(); + if ( + empty($this->actionFunction) + || + !\is_callable($this->actionFunction) + ) { + $this->errorMessage = 'Action function not found!'; + $this->output(); + + return false; } - $port = $this->port . '/' . $this->service . '/' . $this->serviceOption; - /** @noinspection PhpUsageOfSilenceOperatorInspection */ - $mbox = @\imap_open('{' . $this->mailhost . ':' . $port . '}', $this->mailboxUserName, $this->mailboxPassword, \OP_HALFOPEN); + if ($this->moveHard && ($this->disableDelete === false)) { + $this->disableDelete = true; + } - if ($mbox === false) { - return false; + if (!empty($max)) { + $this->maxMessages = $max; } - $list = \imap_getmailboxes($mbox, '{' . $this->mailhost . ':' . $port . '}', '*'); - $mailboxFound = false; + // initialize counters + $totalCount = \imap_num_msg($this->mailboxLink); + $fetchedCount = $totalCount; + $processedCount = 0; + $unprocessedCount = 0; + $deletedCount = 0; + $movedCount = 0; + $this->output('Total: ' . $totalCount . ' messages '); - 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; + // process maximum number of messages + if ($fetchedCount > $this->maxMessages) { + $fetchedCount = $this->maxMessages; + $this->output('Processing first ' . $fetchedCount . ' messages '); + } + + if ($this->testMode) { + $this->output('Running in test mode, not deleting messages from mailbox
'); + } else { + if ($this->disableDelete) { + if ($this->moveHard) { + $this->output('Running in move mode
'); + } else { + $this->output('Running in disableDelete mode, not deleting messages from mailbox
'); } + } else { + $this->output('Processed messages will be deleted from mailbox
'); } + } - if ($mailboxFound === false && $create) { + for ($x = 1; $x <= $fetchedCount; ++$x) { + + // fetch the messages one at a time + if ($this->useFetchstructure) { /** @noinspection PhpUsageOfSilenceOperatorInspection */ - @\imap_createmailbox($mbox, \imap_utf7_encode('{' . $this->mailhost . ':' . $port . '}' . $mailbox)); - \imap_close($mbox); + $structure = @\imap_fetchstructure($this->mailboxLink, $x); - return true; + if ( + $structure + && + \is_object($structure) + && + $structure->type == 1 + && + $structure->ifsubtype + && + $structure->ifparameters + && + \strtoupper($structure->subtype) == 'REPORT' + && + $this->isParameter($structure->parameters, 'REPORT-TYPE', 'delivery-status') + ) { + $processedResult = $this->processBounce($x, 'DSN', $totalCount); + } else { + // not standard DSN msg + $this->output('Msg #' . $x . ' is not a standard DSN message', self::VERBOSE_REPORT); + + if ($this->debugBodyRule) { + if ($structure->ifdescription) { + $this->output(" Content-Type : {$structure->description}", self::VERBOSE_DEBUG); + } else { + $this->output(' Content-Type : unsupported', self::VERBOSE_DEBUG); + } + } + + $processedResult = $this->processBounce($x, 'BODY', $totalCount); + } + } else { + $header = \imap_fetchheader($this->mailboxLink, $x); + + // Could be multi-line, if the new line begins with SPACE or HTAB + if (\preg_match("/Content-Type:((?:[^\n]|\n[\t ])+)(?:\n[^\t ]|$)/i", $header, $match)) { + if ( + \preg_match("/multipart\/report/i", $match[1]) + && + \preg_match("/report-type=[\"']?delivery-status[\"']?/i", $match[1]) + ) { + // standard DSN msg + $processedResult = $this->processBounce($x, 'DSN', $totalCount); + } else { + // not standard DSN msg + $this->output('Msg #' . $x . ' is not a standard DSN message', self::VERBOSE_REPORT); + + if ($this->debugBodyRule) { + $this->output(" Content-Type : {$match[1]}", self::VERBOSE_DEBUG); + } + + $processedResult = $this->processBounce($x, 'BODY', $totalCount); + } + } else { + // didn't get content-type header + $this->output('Msg #' . $x . ' is not a well-formatted MIME mail, missing Content-Type', self::VERBOSE_REPORT); + + if ($this->debugBodyRule) { + $this->output(' Headers: ' . $this->bmhNewLine . $header . $this->bmhNewLine, self::VERBOSE_DEBUG); + } + + $processedResult = $this->processBounce($x, 'BODY', $totalCount); + } } - \imap_close($mbox); + $deleteFlag[$x] = false; + $moveFlag[$x] = false; - return false; + if ($processedResult !== false) { + ++$processedCount; + + if (!$this->disableDelete) { + // delete the bounce if not in disableDelete mode + if (!$this->testMode) { + /** @noinspection PhpUsageOfSilenceOperatorInspection */ + @\imap_delete($this->mailboxLink, $x); + } + + $deleteFlag[$x] = true; + ++$deletedCount; + } elseif ($this->moveHard && $processedResult['bounce_type'] === 'hard') { + // check if the move directory exists, if not create it + if (!$this->testMode) { + $this->mailboxExist($this->hardMailbox); + } + + // move the message + if (!$this->testMode) { + /** @noinspection PhpUsageOfSilenceOperatorInspection */ + @\imap_mail_move($this->mailboxLink, (string) $x, $this->hardMailbox); + } + + $moveFlag[$x] = true; + ++$movedCount; + } elseif ($this->moveSoft && $processedResult['bounce_type'] === 'soft') { + // check if the move directory exists, if not create it + if (!$this->testMode) { + $this->mailboxExist($this->softMailbox); + } + + // move the message + if (!$this->testMode) { + /** @noinspection PhpUsageOfSilenceOperatorInspection */ + @\imap_mail_move($this->mailboxLink, (string) $x, $this->softMailbox); + } + + $moveFlag[$x] = true; + ++$movedCount; + } + } else { + // not processed + ++$unprocessedCount; + if (!$this->disableDelete && $this->purgeUnprocessed) { + // delete this bounce if not in disableDelete mode, and the flag BOUNCE_PURGE_UNPROCESSED is set + if (!$this->testMode) { + /** @noinspection PhpUsageOfSilenceOperatorInspection */ + @\imap_delete($this->mailboxLink, $x); + } + + $deleteFlag[$x] = true; + ++$deletedCount; + } + + // check if the move directory exists, if not create it + $this->mailboxExist($this->unprocessedBox); + // move the message + /** @noinspection PhpUsageOfSilenceOperatorInspection */ + @\imap_mail_move($this->mailboxLink, (string) $x, $this->unprocessedBox); + $moveFlag[$x] = true; + } + + \flush(); } - \imap_close($mbox); + $this->output($this->bmhNewLine . 'Closing mailbox, and purging messages'); - return false; + /** @noinspection PhpUsageOfSilenceOperatorInspection */ + @\imap_expunge($this->mailboxLink); + \imap_close($this->mailboxLink); + + $this->output('Read: ' . $fetchedCount . ' messages'); + $this->output($processedCount . ' action taken'); + $this->output($unprocessedCount . ' no action taken'); + $this->output($deletedCount . ' messages deleted'); + $this->output($movedCount . ' messages moved'); + + return true; } }