These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | /** |
||
6 | * Bounce Mail Handler (formerly known as BMH and PHPMailer-BMH) |
||
7 | * |
||
8 | * @copyright 2008-2009 Andry Prevost. All Rights Reserved. |
||
9 | * @copyright 2011-2012 Anthon Pang. |
||
10 | * @copyright 2015-2019 Lars Moelleken. |
||
11 | * @license GPL |
||
12 | */ |
||
13 | namespace BounceMailHandler; |
||
14 | |||
15 | use function bmhBodyRules; |
||
16 | use function bmhDSNRules; |
||
17 | use const CL_EXPUNGE; |
||
18 | use const OP_HALFOPEN; |
||
19 | 1 | use const OP_READONLY; |
|
20 | use const SORTDATE; |
||
21 | |||
22 | /** |
||
23 | * BounceMailHandler class |
||
24 | * |
||
25 | * BounceMailHandler is a PHP program to check your IMAP/POP3 inbox and |
||
26 | * delete all 'hard' bounced emails. It features a callback function where |
||
27 | * you can create a custom action. This provides you the ability to write |
||
28 | * a script to match your database records and either set inactive or |
||
29 | * delete records with email addresses that match the 'hard' bounce results. |
||
30 | */ |
||
31 | class BounceMailHandler |
||
32 | { |
||
33 | const SECONDS_TIMEOUT = 6000; |
||
34 | |||
35 | const VERBOSE_DEBUG = 3; // detailed report plus debug info |
||
36 | |||
37 | const VERBOSE_QUIET = 0; // suppress output |
||
38 | |||
39 | const VERBOSE_REPORT = 2; // detailed report |
||
40 | |||
41 | const VERBOSE_SIMPLE = 1; // simple report |
||
42 | |||
43 | /** |
||
44 | * mail-server |
||
45 | * |
||
46 | * @var string |
||
47 | */ |
||
48 | public $mailhost = 'localhost'; |
||
49 | |||
50 | /** |
||
51 | * the username of mailbox |
||
52 | * |
||
53 | * @var string |
||
54 | */ |
||
55 | public $mailboxUserName = ''; |
||
56 | |||
57 | /** |
||
58 | * the password needed to access mailbox |
||
59 | * |
||
60 | * @var string |
||
61 | */ |
||
62 | public $mailboxPassword = ''; |
||
63 | |||
64 | /** |
||
65 | * the last error msg |
||
66 | * |
||
67 | * @var string |
||
68 | */ |
||
69 | public $errorMessage = ''; |
||
70 | |||
71 | /** |
||
72 | * maximum limit messages processed in one batch |
||
73 | * |
||
74 | * @var int |
||
75 | */ |
||
76 | public $maxMessages = 3000; |
||
77 | |||
78 | /** |
||
79 | * callback Action function name the function that handles the bounce mail. Parameters: |
||
80 | * |
||
81 | * int $msgnum the message number returned by Bounce Mail Handler |
||
82 | * string $bounce_type the bounce type: |
||
83 | * 'antispam', |
||
84 | * 'autoreply', |
||
85 | * 'concurrent', |
||
86 | * 'content_reject', |
||
87 | * 'command_reject', |
||
88 | * 'internal_error', |
||
89 | * 'defer', |
||
90 | * 'delayed' |
||
91 | * => |
||
92 | * array( |
||
93 | * 'remove' => 0, |
||
94 | * 'bounce_type' => 'temporary' |
||
95 | * ), |
||
96 | * 'dns_loop', |
||
97 | * 'dns_unknown', |
||
98 | * 'full', |
||
99 | * 'inactive', |
||
100 | * 'latin_only', |
||
101 | * 'other', |
||
102 | * 'oversize', |
||
103 | * 'outofoffice', |
||
104 | * 'unknown', |
||
105 | * 'unrecognized', |
||
106 | * 'user_reject', |
||
107 | * 'warning' |
||
108 | * string $email the target email address |
||
109 | * string $subject the subject, ignore now |
||
110 | * string $xheader the XBounceHeader from the mail |
||
111 | * 1 or 0 $remove delete status, 0 is not deleted, 1 is deleted |
||
112 | * string $rule_no bounce mail detect rule no. |
||
113 | * string $rule_cat bounce mail detect rule category |
||
114 | * int $totalFetched total number of messages in the mailbox |
||
115 | * |
||
116 | * @var mixed |
||
117 | */ |
||
118 | public $actionFunction = 'callbackAction'; |
||
119 | |||
120 | /** |
||
121 | * Callback custom body rules |
||
122 | * ``` |
||
123 | * function customBodyRulesCallback( $result, $body, $structure, $debug ) |
||
124 | * { |
||
125 | * return $result; |
||
126 | * } |
||
127 | * ``` |
||
128 | * |
||
129 | * @var callable|null |
||
130 | */ |
||
131 | public $customBodyRulesCallback; |
||
132 | |||
133 | /** |
||
134 | * Callback custom DSN (Delivery Status Notification) rules |
||
135 | * ``` |
||
136 | * function customDSNRulesCallback( $result, $dsnMsg, $dsnReport, $debug ) |
||
137 | * { |
||
138 | * return $result; |
||
139 | * } |
||
140 | * ``` |
||
141 | * |
||
142 | * @var callable|null |
||
143 | */ |
||
144 | public $customDSNRulesCallback; |
||
145 | |||
146 | /** |
||
147 | * test-mode, if true will not delete messages |
||
148 | * |
||
149 | * @var bool |
||
150 | */ |
||
151 | public $testMode = false; |
||
152 | |||
153 | /** |
||
154 | * purge the unknown messages (or not) |
||
155 | * |
||
156 | * @var bool |
||
157 | */ |
||
158 | public $purgeUnprocessed = false; |
||
159 | |||
160 | /** |
||
161 | * control the debug output, default is VERBOSE_SIMPLE |
||
162 | * |
||
163 | * @var int |
||
164 | */ |
||
165 | public $verbose = self::VERBOSE_SIMPLE; |
||
166 | |||
167 | /** |
||
168 | * control the failed DSN rules output |
||
169 | * |
||
170 | * @var bool |
||
171 | */ |
||
172 | public $debugDsnRule = false; |
||
173 | |||
174 | /** |
||
175 | * control the failed BODY rules output |
||
176 | * |
||
177 | * @var bool |
||
178 | */ |
||
179 | public $debugBodyRule = false; |
||
180 | |||
181 | /** |
||
182 | * Control the method to process the mail header |
||
183 | * if set true, uses the imap_fetchstructure function |
||
184 | * otherwise, detect message type directly from headers, |
||
185 | * a bit faster than imap_fetchstructure function and take less resources. |
||
186 | * |
||
187 | * however - the difference is negligible |
||
188 | * |
||
189 | * @var bool |
||
190 | */ |
||
191 | public $useFetchstructure = true; |
||
192 | |||
193 | /** |
||
194 | * If disableDelete is equal to true, it will disable the delete function. |
||
195 | * |
||
196 | * @var bool |
||
197 | */ |
||
198 | public $disableDelete = false; |
||
199 | |||
200 | /** |
||
201 | * defines new line ending |
||
202 | * |
||
203 | * @var string |
||
204 | */ |
||
205 | public $bmhNewLine = "<br />\n"; |
||
206 | |||
207 | /** |
||
208 | * defines port number, default is '143', other common choices are '110' (pop3), '993' (gmail) |
||
209 | * |
||
210 | * @var int |
||
211 | */ |
||
212 | public $port = 143; |
||
213 | |||
214 | /** |
||
215 | * defines service, default is 'imap', choice includes 'pop3' |
||
216 | * |
||
217 | * @var string |
||
218 | */ |
||
219 | public $service = 'imap'; |
||
220 | |||
221 | /** |
||
222 | * defines service option, default is 'notls', other choices are 'tls', 'ssl' |
||
223 | * |
||
224 | * @var string |
||
225 | */ |
||
226 | public $serviceOption = 'notls'; |
||
227 | |||
228 | /** |
||
229 | * mailbox type, default is 'INBOX', other choices are (Tasks, Spam, Replies, etc.) |
||
230 | * |
||
231 | * @var string |
||
232 | */ |
||
233 | public $boxname = 'INBOX'; |
||
234 | |||
235 | /** |
||
236 | * determines if soft bounces will be moved to another mailbox folder |
||
237 | * |
||
238 | * @var bool |
||
239 | */ |
||
240 | public $moveSoft = false; |
||
241 | |||
242 | /** |
||
243 | * mailbox folder to move soft bounces to, default is 'soft' |
||
244 | * |
||
245 | * @var string |
||
246 | */ |
||
247 | public $softMailbox = 'INBOX.soft'; |
||
248 | |||
249 | /** |
||
250 | * determines if hard bounces will be moved to another mailbox folder |
||
251 | * |
||
252 | * NOTE: If true, this will disable delete and perform a move operation instead |
||
253 | * |
||
254 | * @var bool |
||
255 | */ |
||
256 | public $moveHard = false; |
||
257 | |||
258 | /** |
||
259 | * mailbox folder to move hard bounces to, default is 'hard' |
||
260 | * |
||
261 | * @var string |
||
262 | */ |
||
263 | public $hardMailbox = 'INBOX.hard'; |
||
264 | |||
265 | /* |
||
266 | * Mailbox folder to move unprocessed mails |
||
267 | * @var string |
||
268 | */ |
||
269 | public $unprocessedBox = 'INBOX.unprocessed'; |
||
270 | |||
271 | /** |
||
272 | * deletes messages globally prior to date in variable |
||
273 | * |
||
274 | * NOTE: excludes any message folder that includes 'sent' in mailbox name |
||
275 | * format is same as MySQL: 'yyyy-mm-dd' |
||
276 | * if variable is blank, will not process global delete |
||
277 | * |
||
278 | * @var string |
||
279 | */ |
||
280 | public $deleteMsgDate = ''; |
||
281 | |||
282 | /** |
||
283 | * (internal variable) |
||
284 | * |
||
285 | * The resource handler for the opened mailbox (POP3/IMAP/NNTP/etc.) |
||
286 | * |
||
287 | * @var resource |
||
288 | */ |
||
289 | protected $mailboxLink = false; |
||
290 | |||
291 | /** |
||
292 | * Holds Bounce Mail Handler version. |
||
293 | * |
||
294 | * @var string |
||
295 | */ |
||
296 | private $version = '6.0-dev'; |
||
297 | |||
298 | /** |
||
299 | * @return string |
||
300 | */ |
||
301 | public function getVersion(): string |
||
302 | { |
||
303 | return $this->version; |
||
304 | } |
||
305 | |||
306 | /** |
||
307 | * Function to delete messages in a mailbox, based on date |
||
308 | 1 | * |
|
309 | * NOTE: this is global ... will affect all mailboxes except any that have 'sent' in the mailbox name |
||
310 | */ |
||
311 | 1 | public function globalDelete(): bool |
|
312 | { |
||
313 | $dateArr = \explode('-', $this->deleteMsgDate); // date format is yyyy-mm-dd |
||
314 | $delDate = \mktime(0, 0, 0, (int) ($dateArr[1]), (int) ($dateArr[2]), (int) ($dateArr[0])); |
||
315 | |||
316 | $port = $this->port . '/' . $this->service . '/' . $this->serviceOption; |
||
317 | 1 | $mboxt = \imap_open('{' . $this->mailhost . ':' . $port . '}', $this->mailboxUserName, $this->mailboxPassword, OP_HALFOPEN); |
|
318 | |||
319 | if ($mboxt === false) { |
||
320 | return false; |
||
321 | } |
||
322 | 1 | ||
323 | $list = \imap_getmailboxes($mboxt, '{' . $this->mailhost . ':' . $port . '}', '*'); |
||
324 | 1 | ||
325 | if (\is_array($list)) { |
||
326 | 1 | foreach ($list as $key => $val) { |
|
327 | 1 | // get the mailbox name only |
|
328 | 1 | $nameArr = \explode('}', \imap_utf7_decode($val->name)); |
|
329 | $nameRaw = $nameArr[\count($nameArr) - 1]; |
||
330 | |||
331 | if (\stripos($nameRaw, 'sent') === false) { |
||
332 | 1 | $mboxd = \imap_open('{' . $this->mailhost . ':' . $port . '}' . $nameRaw, $this->mailboxUserName, $this->mailboxPassword, CL_EXPUNGE); |
|
333 | $messages = \imap_sort($mboxd, SORTDATE, 0); |
||
334 | |||
335 | foreach ($messages as $message) { |
||
336 | $header = \imap_headerinfo($mboxd, $message); |
||
337 | |||
338 | 1 | // purge if prior to global delete date |
|
339 | if ($header->udate < $delDate) { |
||
340 | 1 | \imap_delete($mboxd, $message); |
|
341 | } |
||
342 | } |
||
343 | |||
344 | \imap_expunge($mboxd); |
||
345 | \imap_errors(); |
||
346 | \imap_alerts(); |
||
347 | \imap_close($mboxd); |
||
348 | } |
||
349 | } |
||
350 | |||
351 | \imap_errors(); |
||
352 | \imap_alerts(); |
||
353 | \imap_close($mboxt); |
||
354 | |||
355 | return true; |
||
356 | } |
||
357 | |||
358 | \imap_errors(); |
||
359 | \imap_alerts(); |
||
360 | \imap_close($mboxt); |
||
361 | |||
362 | return false; |
||
363 | } |
||
364 | |||
365 | /** |
||
366 | * Function to determine if a particular value is found in a imap_fetchstructure key. |
||
367 | * |
||
368 | * @param array $currParameters imap_fetstructure parameters |
||
369 | * @param string $varKey imap_fetstructure key |
||
370 | * @param string $varValue value to check for |
||
371 | * |
||
372 | * @return bool |
||
373 | */ |
||
374 | public function isParameter(array $currParameters, string $varKey, string $varValue): bool |
||
375 | { |
||
376 | foreach ($currParameters as $object) { |
||
377 | if ( |
||
378 | \strtoupper($object->attribute) == \strtoupper($varKey) |
||
379 | && |
||
380 | \strtoupper($object->value) == \strtoupper($varValue) |
||
381 | ) { |
||
382 | return true; |
||
383 | } |
||
384 | } |
||
385 | |||
386 | return false; |
||
387 | } |
||
388 | |||
389 | /** |
||
390 | * Function to check if a mailbox exists - if not found, it will create it. |
||
391 | * |
||
392 | * @param string $mailbox the mailbox name, must be in 'INBOX.checkmailbox' format |
||
393 | * @param bool $create whether or not to create the checkmailbox if not found, defaults to true |
||
394 | * |
||
395 | * @return bool |
||
396 | */ |
||
397 | public function mailboxExist(string $mailbox, bool $create = true): bool |
||
398 | { |
||
399 | if (\trim($mailbox) === '') { |
||
400 | // this is a critical error with either the mailbox name blank or an invalid mailbox name |
||
401 | // need to stop processing and exit at this point |
||
402 | echo 'Invalid mailbox name for move operation. Cannot continue: ' . $mailbox . "<br />\n"; |
||
403 | exit(); |
||
404 | } |
||
405 | |||
406 | 3 | $port = $this->port . '/' . $this->service . '/' . $this->serviceOption; |
|
407 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ |
||
408 | 3 | $mbox = @\imap_open('{' . $this->mailhost . ':' . $port . '}', $this->mailboxUserName, $this->mailboxPassword, OP_HALFOPEN); |
|
409 | 3 | ||
410 | if ($mbox === false) { |
||
411 | return false; |
||
412 | 3 | } |
|
413 | |||
414 | 3 | $list = \imap_getmailboxes($mbox, '{' . $this->mailhost . ':' . $port . '}', '*'); |
|
415 | 3 | $mailboxFound = false; |
|
416 | |||
417 | if (\is_array($list)) { |
||
418 | foreach ($list as $key => $val) { |
||
419 | // get the mailbox name only |
||
420 | $nameArr = \explode('}', \imap_utf7_decode($val->name)); |
||
421 | $nameRaw = $nameArr[\count($nameArr) - 1]; |
||
422 | if ($mailbox == $nameRaw) { |
||
423 | $mailboxFound = true; |
||
424 | 2 | } |
|
425 | } |
||
426 | 2 | ||
427 | if ($mailboxFound === false && $create) { |
||
428 | 2 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ |
|
429 | @\imap_createmailbox($mbox, \imap_utf7_encode('{' . $this->mailhost . ':' . $port . '}' . $mailbox)); |
||
430 | \imap_errors(); |
||
431 | 2 | \imap_alerts(); |
|
432 | \imap_close($mbox); |
||
433 | |||
434 | 2 | return true; |
|
435 | } |
||
436 | |||
437 | \imap_errors(); |
||
438 | \imap_alerts(); |
||
439 | \imap_close($mbox); |
||
440 | 2 | ||
441 | return false; |
||
442 | 2 | } |
|
443 | |||
444 | \imap_errors(); |
||
445 | \imap_alerts(); |
||
446 | \imap_close($mbox); |
||
447 | |||
448 | return false; |
||
449 | } |
||
450 | |||
451 | /** |
||
452 | * open a mail box in local file system |
||
453 | * |
||
454 | 3 | * @param string $filePath The local mailbox file path |
|
455 | * |
||
456 | 3 | * @return bool |
|
457 | */ |
||
458 | public function openLocal(string $filePath): bool |
||
459 | { |
||
460 | \set_time_limit(self::SECONDS_TIMEOUT); |
||
461 | |||
462 | if (!$this->testMode) { |
||
463 | 3 | $this->mailboxLink = \imap_open($filePath, '', '', CL_EXPUNGE | ($this->testMode ? OP_READONLY : 0)); |
|
464 | } else { |
||
465 | $this->mailboxLink = \imap_open($filePath, '', '', ($this->testMode ? OP_READONLY : 0)); |
||
466 | } |
||
467 | 3 | ||
468 | if (!$this->mailboxLink) { |
||
469 | $this->errorMessage = 'Cannot open the mailbox file to ' . $filePath . $this->bmhNewLine . 'Error MSG: ' . \imap_last_error(); |
||
470 | $this->output(); |
||
471 | |||
472 | 3 | return false; |
|
473 | 3 | } |
|
474 | 3 | ||
475 | 3 | $this->output('Opened ' . $filePath); |
|
476 | 3 | ||
477 | 3 | return true; |
|
478 | 3 | } |
|
479 | |||
480 | /** |
||
481 | 3 | * open a mail box |
|
482 | * |
||
483 | * @return bool |
||
484 | */ |
||
485 | public function openMailbox(): bool |
||
486 | 3 | { |
|
487 | 2 | // before starting the processing, let's check the delete flag and do global deletes if true |
|
488 | 2 | if (\trim($this->deleteMsgDate) !== '') { |
|
489 | 1 | echo 'processing global delete based on date of ' . $this->deleteMsgDate . '<br />'; |
|
490 | 1 | $this->globalDelete(); |
|
491 | 1 | } |
|
492 | 1 | ||
493 | // disable move operations if server is Gmail ... Gmail does not support mailbox creation |
||
494 | if (\stripos($this->mailhost, 'gmail') !== false) { |
||
495 | 1 | $this->moveSoft = false; |
|
496 | $this->moveHard = false; |
||
497 | } |
||
498 | |||
499 | $port = $this->port . '/' . $this->service . '/' . $this->serviceOption; |
||
500 | 3 | ||
501 | \set_time_limit(self::SECONDS_TIMEOUT); |
||
502 | |||
503 | 3 | if (!$this->testMode) { |
|
504 | $this->mailboxLink = \imap_open('{' . $this->mailhost . ':' . $port . '}' . $this->boxname, $this->mailboxUserName, $this->mailboxPassword, CL_EXPUNGE | ($this->testMode ? OP_READONLY : 0)); |
||
505 | 3 | } else { |
|
506 | $this->mailboxLink = \imap_open('{' . $this->mailhost . ':' . $port . '}' . $this->boxname, $this->mailboxUserName, $this->mailboxPassword, ($this->testMode ? OP_READONLY : 0)); |
||
507 | } |
||
508 | |||
509 | 3 | if (!$this->mailboxLink) { |
|
510 | 3 | $this->errorMessage = 'Cannot create ' . $this->service . ' connection to ' . $this->mailhost . $this->bmhNewLine . 'Error MSG: ' . \imap_last_error(); |
|
511 | 3 | $this->output(); |
|
512 | 3 | ||
513 | 3 | return false; |
|
514 | 3 | } |
|
515 | 3 | ||
516 | 3 | $this->output('Connected to: ' . $this->mailhost . ' (' . $this->mailboxUserName . ')'); |
|
517 | 3 | ||
518 | 3 | return true; |
|
519 | 3 | } |
|
520 | 2 | ||
521 | 3 | /** |
|
522 | 2 | * output additional msg for debug |
|
523 | 2 | * |
|
524 | * @param mixed $msg if not given, output the last error msg |
||
525 | 3 | * @param int $verboseLevel the output level of this message |
|
526 | */ |
||
527 | 3 | public function output($msg = '', int $verboseLevel = self::VERBOSE_SIMPLE) |
|
528 | { |
||
529 | if ($this->verbose >= $verboseLevel) { |
||
530 | if ($msg) { |
||
531 | echo $msg . $this->bmhNewLine; |
||
532 | } else { |
||
533 | echo $this->errorMessage . $this->bmhNewLine; |
||
534 | } |
||
535 | 3 | } |
|
536 | } |
||
537 | 3 | ||
538 | /** |
||
539 | * Function to process each individual message. |
||
540 | * |
||
541 | * @param int $pos message number |
||
542 | * @param string $type DNS or BODY type |
||
543 | * @param int $totalFetched total number of messages in mailbox |
||
544 | * |
||
545 | * @return array|false <p>"$result"-array or false</p> |
||
546 | */ |
||
547 | public function processBounce(int $pos, string $type, int $totalFetched) |
||
548 | { |
||
549 | $header = \imap_headerinfo($this->mailboxLink, $pos); |
||
550 | $subject = isset($header->subject) ? \strip_tags($header->subject) : '[NO SUBJECT]'; |
||
551 | $body = ''; |
||
552 | $headerFull = \imap_fetchheader($this->mailboxLink, $pos); |
||
553 | $bodyFull = \imap_body($this->mailboxLink, $pos); |
||
554 | |||
555 | if ($type == 'DSN') { |
||
556 | // first part of DSN (Delivery Status Notification), human-readable explanation |
||
557 | $dsnMsg = \imap_fetchbody($this->mailboxLink, $pos, '1'); |
||
558 | $dsnMsgStructure = \imap_bodystruct($this->mailboxLink, $pos, '1'); |
||
559 | |||
560 | View Code Duplication | if ($dsnMsgStructure->encoding == 4) { |
|
561 | $dsnMsg = \quoted_printable_decode($dsnMsg); |
||
562 | } elseif ($dsnMsgStructure->encoding == 3) { |
||
563 | $dsnMsg = \base64_decode($dsnMsg, true); |
||
564 | } |
||
565 | |||
566 | // second part of DSN (Delivery Status Notification), delivery-status |
||
567 | $dsnReport = \imap_fetchbody($this->mailboxLink, $pos, '2'); |
||
568 | |||
569 | // process bounces by rules |
||
570 | $result = bmhDSNRules($dsnMsg, $dsnReport, $this->debugDsnRule); |
||
571 | 3 | $result = \is_callable($this->customDSNRulesCallback) ? \call_user_func($this->customDSNRulesCallback, $result, $dsnMsg, $dsnReport, $this->debugDsnRule) : $result; |
|
572 | 3 | } elseif ($type == 'BODY') { |
|
573 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ |
||
574 | 3 | $structure = @\imap_fetchstructure($this->mailboxLink, $pos); |
|
575 | 2 | ||
576 | if (!\is_object($structure)) { |
||
577 | 2 | return false; |
|
578 | } |
||
579 | 2 | ||
580 | switch ($structure->type) { |
||
581 | case 0: // Content-type = text |
||
582 | $body = \imap_fetchbody($this->mailboxLink, $pos, '1'); |
||
583 | $result = bmhBodyRules($body, $structure, $this->debugBodyRule); |
||
584 | 2 | $result = \is_callable($this->customBodyRulesCallback) ? \call_user_func($this->customBodyRulesCallback, $result, $body, $structure, $this->debugBodyRule) : $result; |
|
585 | 2 | ||
586 | 2 | break; |
|
587 | |||
588 | case 1: // Content-type = multipart |
||
589 | $body = \imap_fetchbody($this->mailboxLink, $pos, '1'); |
||
590 | |||
591 | // Detect encoding and decode - only base64 |
||
592 | if ($structure->parts[0]->encoding == 4) { |
||
593 | $body = \quoted_printable_decode($body); |
||
594 | } elseif ($structure->parts[0]->encoding == 3) { |
||
595 | $body = \base64_decode($body, true); |
||
596 | } |
||
597 | |||
598 | $result = bmhBodyRules($body, $structure, $this->debugBodyRule); |
||
599 | $result = \is_callable($this->customBodyRulesCallback) ? \call_user_func($this->customBodyRulesCallback, $result, $body, $structure, $this->debugBodyRule) : $result; |
||
600 | |||
601 | break; |
||
602 | |||
603 | case 2: // Content-type = message |
||
604 | $body = \imap_body($this->mailboxLink, $pos); |
||
605 | |||
606 | View Code Duplication | if ($structure->encoding == 4) { |
|
607 | $body = \quoted_printable_decode($body); |
||
608 | } elseif ($structure->encoding == 3) { |
||
609 | $body = \base64_decode($body, true); |
||
610 | } |
||
611 | |||
612 | $body = \substr($body, 0, 1000); |
||
613 | $result = bmhBodyRules($body, $structure, $this->debugBodyRule); |
||
614 | $result = \is_callable($this->customBodyRulesCallback) ? \call_user_func($this->customBodyRulesCallback, $result, $body, $structure, $this->debugBodyRule) : $result; |
||
615 | 2 | ||
616 | break; |
||
617 | 3 | ||
618 | 3 | default: // un-support Content-type |
|
619 | $this->output('Msg #' . $pos . ' is unsupported Content-Type:' . $structure->type, self::VERBOSE_REPORT); |
||
620 | |||
621 | return false; |
||
622 | } |
||
623 | } else { |
||
624 | // internal error |
||
625 | $this->errorMessage = 'Internal Error: unknown type'; |
||
626 | |||
627 | return false; |
||
628 | } |
||
629 | |||
630 | 3 | $email = $result['email']; |
|
631 | $bounceType = $result['bounce_type']; |
||
632 | |||
633 | 3 | // workaround: I think there is a error in one of the reg-ex in "phpmailer-bmh_rules.php". |
|
634 | 3 | if ($email && \strpos($email, 'TO:<') !== false) { |
|
635 | $email = \str_replace('TO:<', '', $email); |
||
636 | } |
||
637 | 3 | ||
638 | 3 | if ($this->moveHard && $result['bounce_type'] == 'hard') { |
|
639 | $remove = 'moved (hard)'; |
||
640 | 3 | } elseif ($this->moveSoft && $result['bounce_type'] == 'soft') { |
|
641 | $remove = 'moved (soft)'; |
||
642 | } elseif ($this->disableDelete) { |
||
643 | 3 | $remove = 0; |
|
644 | 3 | } else { |
|
645 | $remove = $result['remove']; |
||
646 | 3 | } |
|
647 | 3 | ||
648 | 3 | $ruleNumber = $result['rule_no']; |
|
649 | 3 | $ruleCategory = $result['rule_cat']; |
|
650 | 3 | $status_code = $result['status_code']; |
|
651 | $action = $result['action']; |
||
652 | 3 | $diagnostic_code = $result['diagnostic_code']; |
|
653 | $xheader = false; |
||
654 | |||
655 | if ($ruleNumber === '0000') { |
||
656 | // unrecognized |
||
657 | if ( |
||
658 | \trim($email) === '' |
||
659 | && |
||
660 | \property_exists($header, 'fromaddress') === true |
||
661 | ) { |
||
662 | $email = $header->fromaddress; |
||
663 | } |
||
664 | 2 | ||
665 | View Code Duplication | if ($this->testMode) { |
|
666 | 2 | $this->output('Match: ' . $ruleNumber . ':' . $ruleCategory . '; ' . $bounceType . '; ' . $email); |
|
667 | } else { |
||
668 | 2 | // code below will use the Callback function, but return no value |
|
669 | 2 | $params = [ |
|
670 | 2 | $pos, |
|
671 | 2 | $bounceType, |
|
672 | 2 | $email, |
|
673 | $subject, |
||
674 | $header, |
||
675 | $remove, |
||
676 | $ruleNumber, |
||
677 | $ruleCategory, |
||
678 | $totalFetched, |
||
679 | $body, |
||
680 | $headerFull, |
||
681 | $bodyFull, |
||
682 | $status_code, |
||
683 | $action, |
||
684 | $diagnostic_code, |
||
685 | ]; |
||
686 | \call_user_func_array($this->actionFunction, $params); |
||
687 | } |
||
688 | 3 | View Code Duplication | } else { |
689 | // match rule, do bounce action |
||
690 | 3 | if ($this->testMode) { |
|
691 | 3 | $this->output('Match: ' . $ruleNumber . ':' . $ruleCategory . '; ' . $bounceType . '; ' . $email); |
|
692 | 3 | ||
693 | 3 | return true; |
|
694 | 3 | } |
|
695 | |||
696 | 3 | $params = [ |
|
697 | $pos, |
||
698 | 2 | $bounceType, |
|
699 | 2 | $email, |
|
700 | $subject, |
||
701 | 2 | $xheader, |
|
702 | 1 | $remove, |
|
703 | 2 | $ruleNumber, |
|
704 | $ruleCategory, |
||
705 | $totalFetched, |
||
706 | $body, |
||
707 | $headerFull, |
||
708 | 2 | $bodyFull, |
|
709 | $status_code, |
||
710 | $action, |
||
711 | 2 | $diagnostic_code, |
|
712 | 2 | ]; |
|
713 | \call_user_func_array($this->actionFunction, $params); |
||
714 | 3 | ||
715 | return $result; |
||
716 | 3 | } |
|
717 | |||
718 | 3 | return false; |
|
719 | 1 | } |
|
720 | |||
721 | /** |
||
722 | 3 | * process the messages in a mailbox |
|
723 | 3 | * |
|
724 | 3 | * @param bool|int $max $max maximum limit messages processed in one batch, |
|
725 | 3 | * if not given uses the property $maxMessages |
|
726 | 3 | * |
|
727 | 3 | * @return bool |
|
728 | */ |
||
729 | 1 | public function processMailbox($max = false): bool |
|
730 | 1 | { |
|
731 | if ( |
||
732 | empty($this->actionFunction) |
||
733 | 1 | || |
|
734 | 1 | !\is_callable($this->actionFunction) |
|
735 | 1 | ) { |
|
736 | $this->errorMessage = 'Action function not found!'; |
||
737 | $this->output(); |
||
738 | |||
739 | 1 | return false; |
|
740 | 1 | } |
|
741 | 1 | ||
742 | if ($this->moveHard && ($this->disableDelete === false)) { |
||
743 | $this->disableDelete = true; |
||
744 | } |
||
745 | |||
746 | if (!empty($max)) { |
||
747 | $this->maxMessages = $max; |
||
748 | } |
||
749 | |||
750 | // initialize counters |
||
751 | $totalCount = \imap_num_msg($this->mailboxLink); |
||
752 | $fetchedCount = $totalCount; |
||
753 | $processedCount = 0; |
||
754 | $unprocessedCount = 0; |
||
755 | $deletedCount = 0; |
||
756 | $movedCount = 0; |
||
757 | $this->output('Total: ' . $totalCount . ' messages '); |
||
758 | |||
759 | // process maximum number of messages |
||
760 | if ($fetchedCount > $this->maxMessages) { |
||
761 | 3 | $fetchedCount = $this->maxMessages; |
|
762 | 3 | $this->output('Processing first ' . $fetchedCount . ' messages '); |
|
763 | } |
||
764 | |||
765 | if ($this->testMode) { |
||
766 | $this->output('Running in test mode, not deleting messages from mailbox<br />'); |
||
767 | } else { |
||
768 | if ($this->disableDelete) { |
||
769 | 3 | if ($this->moveHard) { |
|
770 | 3 | $this->output('Running in move mode<br />'); |
|
771 | } else { |
||
772 | $this->output('Running in disableDelete mode, not deleting messages from mailbox<br />'); |
||
773 | 3 | } |
|
774 | } else { |
||
775 | $this->output('Processed messages will be deleted from mailbox<br />'); |
||
776 | } |
||
777 | 3 | } |
|
778 | |||
779 | 3 | for ($x = 1; $x <= $fetchedCount; ++$x) { |
|
780 | |||
781 | 3 | // fetch the messages one at a time |
|
782 | 1 | if ($this->useFetchstructure) { |
|
783 | 1 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ |
|
784 | 2 | $structure = @\imap_fetchstructure($this->mailboxLink, $x); |
|
785 | |||
786 | if ( |
||
787 | 3 | $structure |
|
788 | 3 | && |
|
789 | 3 | \is_object($structure) |
|
790 | 3 | && |
|
791 | 3 | $structure->type == 1 |
|
792 | 3 | && |
|
793 | $structure->ifsubtype |
||
794 | 3 | && |
|
795 | $structure->ifparameters |
||
796 | && |
||
797 | 3 | \strtoupper($structure->subtype) == 'REPORT' |
|
798 | 3 | && |
|
799 | 3 | $this->isParameter($structure->parameters, 'REPORT-TYPE', 'delivery-status') |
|
800 | 3 | ) { |
|
801 | 3 | $processedResult = $this->processBounce($x, 'DSN', $totalCount); |
|
802 | 3 | } else { |
|
803 | // not standard DSN msg |
||
804 | 3 | $this->output('Msg #' . $x . ' is not a standard DSN message', self::VERBOSE_REPORT); |
|
805 | 2 | ||
806 | 2 | if ($this->debugBodyRule) { |
|
807 | if ($structure->ifdescription) { |
||
808 | $this->output(" Content-Type : {$structure->description}", self::VERBOSE_DEBUG); |
||
809 | 1 | } else { |
|
810 | 1 | $this->output(' Content-Type : unsupported', self::VERBOSE_DEBUG); |
|
811 | 1 | } |
|
812 | 1 | } |
|
813 | 1 | ||
814 | 1 | $processedResult = $this->processBounce($x, 'BODY', $totalCount); |
|
815 | 1 | } |
|
816 | 1 | } else { |
|
817 | 1 | $header = \imap_fetchheader($this->mailboxLink, $x); |
|
818 | 1 | ||
819 | 1 | // Could be multi-line, if the new line begins with SPACE or HTAB |
|
820 | 1 | if ($header && \preg_match("/Content-Type:((?:[^\n]|\n[\t ])+)(?:\n[^\t ]|$)/i", $header, $match)) { |
|
821 | 1 | if ( |
|
822 | 1 | \preg_match("/multipart\/report/i", $match[1]) |
|
823 | 1 | && |
|
824 | 1 | \preg_match("/report-type=[\"']?delivery-status[\"']?/i", $match[1]) |
|
825 | 1 | ) { |
|
826 | // standard DSN msg |
||
827 | 3 | $processedResult = $this->processBounce($x, 'DSN', $totalCount); |
|
828 | } else { |
||
829 | 2 | // not standard DSN msg |
|
830 | 2 | $this->output('Msg #' . $x . ' is not a standard DSN message', self::VERBOSE_REPORT); |
|
831 | |||
832 | 2 | if ($this->debugBodyRule) { |
|
833 | $this->output(" Content-Type : {$match[1]}", self::VERBOSE_DEBUG); |
||
834 | } |
||
835 | |||
836 | $processedResult = $this->processBounce($x, 'BODY', $totalCount); |
||
837 | } |
||
838 | } else { |
||
839 | // didn't get content-type header |
||
840 | $this->output('Msg #' . $x . ' is not a well-formatted MIME mail, missing Content-Type', self::VERBOSE_REPORT); |
||
841 | |||
842 | if ($this->debugBodyRule) { |
||
843 | $this->output(' Headers: ' . $this->bmhNewLine . $header . $this->bmhNewLine, self::VERBOSE_DEBUG); |
||
844 | } |
||
845 | |||
846 | $processedResult = $this->processBounce($x, 'BODY', $totalCount); |
||
847 | } |
||
848 | } |
||
849 | |||
850 | $deleteFlag[$x] = false; |
||
851 | $moveFlag[$x] = false; |
||
852 | |||
853 | if ($processedResult !== false) { |
||
854 | ++$processedCount; |
||
855 | |||
856 | 3 | if (!$this->disableDelete) { |
|
857 | // delete the bounce if not in disableDelete mode |
||
858 | if (!$this->testMode) { |
||
859 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ |
||
860 | @\imap_delete($this->mailboxLink, $x); |
||
861 | } |
||
862 | |||
863 | $deleteFlag[$x] = true; |
||
864 | ++$deletedCount; |
||
865 | } elseif ($this->moveHard && $processedResult['bounce_type'] === 'hard') { |
||
866 | // check if the move directory exists, if not create it |
||
867 | 3 | if (!$this->testMode) { |
|
868 | $this->mailboxExist($this->hardMailbox); |
||
869 | 3 | } |
|
870 | |||
871 | // move the message |
||
872 | if (!$this->testMode) { |
||
873 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ |
||
874 | @\imap_mail_move($this->mailboxLink, (string) $x, $this->hardMailbox); |
||
875 | } |
||
876 | 3 | ||
877 | $moveFlag[$x] = true; |
||
878 | 3 | ++$movedCount; |
|
879 | } elseif ($this->moveSoft && $processedResult['bounce_type'] === 'soft') { |
||
880 | 3 | // check if the move directory exists, if not create it |
|
881 | 2 | if (!$this->testMode) { |
|
882 | $this->mailboxExist($this->softMailbox); |
||
883 | } |
||
884 | 1 | ||
885 | 1 | // move the message |
|
886 | if (!$this->testMode) { |
||
887 | 1 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ |
|
888 | 1 | @\imap_mail_move($this->mailboxLink, (string) $x, $this->softMailbox); |
|
889 | } |
||
890 | 1 | ||
891 | 1 | $moveFlag[$x] = true; |
|
892 | 1 | ++$movedCount; |
|
893 | 1 | } |
|
894 | 1 | } else { |
|
895 | 1 | // not processed |
|
896 | ++$unprocessedCount; |
||
897 | 1 | if (!$this->disableDelete && $this->purgeUnprocessed) { |
|
898 | // delete this bounce if not in disableDelete mode, and the flag BOUNCE_PURGE_UNPROCESSED is set |
||
899 | 1 | if (!$this->testMode) { |
|
900 | 1 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ |
|
901 | @\imap_delete($this->mailboxLink, $x); |
||
902 | 1 | } |
|
903 | |||
904 | 1 | $deleteFlag[$x] = true; |
|
905 | ++$deletedCount; |
||
906 | 1 | } |
|
907 | |||
908 | // check if the move directory exists, if not create it |
||
909 | $this->mailboxExist($this->unprocessedBox); |
||
910 | // move the message |
||
911 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ |
||
912 | @\imap_mail_move($this->mailboxLink, (string) $x, $this->unprocessedBox); |
||
913 | $moveFlag[$x] = true; |
||
914 | } |
||
915 | |||
916 | \flush(); |
||
917 | } |
||
918 | |||
919 | $this->output($this->bmhNewLine . 'Closing mailbox, and purging messages'); |
||
920 | |||
921 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ |
||
922 | @\imap_expunge($this->mailboxLink); |
||
923 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ |
||
924 | @\imap_close($this->mailboxLink); |
||
0 ignored issues
–
show
|
|||
925 | |||
926 | $this->output('Read: ' . $fetchedCount . ' messages'); |
||
927 | $this->output($processedCount . ' action taken'); |
||
928 | $this->output($unprocessedCount . ' no action taken'); |
||
929 | $this->output($deletedCount . ' messages deleted'); |
||
930 | $this->output($movedCount . ' messages moved'); |
||
931 | |||
932 | return true; |
||
933 | } |
||
934 | } |
||
935 |
If you suppress an error, we recommend checking for the error condition explicitly: