Completed
Push — master ( b2cf95...fabdf2 )
by Lars
03:31
created

src/BounceMailHandler/BounceMailHandler.php (1 issue)

Upgrade to new PHP Analysis Engine

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
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
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