Completed
Push — master ( 9127cb...6c003f )
by Lars
13:00
created

BounceMailHandler::openMailbox()   A

Complexity

Conditions 5
Paths 16

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 5.1158

Importance

Changes 0
Metric Value
cc 5
nc 16
nop 0
dl 0
loc 35
ccs 20
cts 24
cp 0.8333
crap 5.1158
rs 9.0488
c 0
b 0
f 0
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
                    /** @noinspection UnusedFunctionResultInspection */
346
                    \imap_errors();
347
                    /** @noinspection UnusedFunctionResultInspection */
348
                    \imap_alerts();
349
                    \imap_close($mboxd);
350
                }
351
            }
352
353
            /** @noinspection UnusedFunctionResultInspection */
354
            \imap_errors();
355
            /** @noinspection UnusedFunctionResultInspection */
356
            \imap_alerts();
357
            \imap_close($mboxt);
358
359
            return true;
360
        }
361
362
        /** @noinspection UnusedFunctionResultInspection */
363
        \imap_errors();
364
        /** @noinspection UnusedFunctionResultInspection */
365
        \imap_alerts();
366
        \imap_close($mboxt);
367
368
        return false;
369
    }
370
371
    /**
372
     * Function to determine if a particular value is found in a imap_fetchstructure key.
373
     *
374
     * @param array  $currParameters imap_fetstructure parameters
375
     * @param string $varKey         imap_fetstructure key
376
     * @param string $varValue       value to check for
377
     *
378
     * @return bool
379
     */
380
    public function isParameter(array $currParameters, string $varKey, string $varValue): bool
381
    {
382
        foreach ($currParameters as $object) {
383
            if (
384
                \strtoupper($object->attribute) == \strtoupper($varKey)
385
                &&
386
                \strtoupper($object->value) == \strtoupper($varValue)
387
            ) {
388
                return true;
389
            }
390
        }
391
392
        return false;
393
    }
394
395
    /**
396
     * Function to check if a mailbox exists - if not found, it will create it.
397
     *
398
     * @param string $mailbox the mailbox name, must be in 'INBOX.checkmailbox' format
399
     * @param bool   $create  whether or not to create the checkmailbox if not found, defaults to true
400
     *
401
     * @return bool
402
     */
403
    public function mailboxExist(string $mailbox, bool $create = true): bool
404
    {
405
        if (\trim($mailbox) === '') {
406 3
            // this is a critical error with either the mailbox name blank or an invalid mailbox name
407
            // need to stop processing and exit at this point
408 3
            echo 'Invalid mailbox name for move operation. Cannot continue: ' . $mailbox . "<br />\n";
409 3
            exit();
410
        }
411
412 3
        $port = $this->port . '/' . $this->service . '/' . $this->serviceOption;
413
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
414 3
        $mbox = @\imap_open('{' . $this->mailhost . ':' . $port . '}', $this->mailboxUserName, $this->mailboxPassword, OP_HALFOPEN);
415 3
416
        if ($mbox === false) {
417
            return false;
418
        }
419
420
        $list = \imap_getmailboxes($mbox, '{' . $this->mailhost . ':' . $port . '}', '*');
421
        $mailboxFound = false;
422
423
        if (\is_array($list)) {
424 2
            foreach ($list as $key => $val) {
425
                // get the mailbox name only
426 2
                $nameArr = \explode('}', \imap_utf7_decode($val->name));
427
                $nameRaw = $nameArr[\count($nameArr) - 1];
428 2
                if ($mailbox == $nameRaw) {
429
                    $mailboxFound = true;
430
                }
431 2
            }
432
433
            if ($mailboxFound === false && $create) {
434 2
                /** @noinspection PhpUsageOfSilenceOperatorInspection */
435
                @\imap_createmailbox($mbox, \imap_utf7_encode('{' . $this->mailhost . ':' . $port . '}' . $mailbox));
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...
436
                /** @noinspection UnusedFunctionResultInspection */
437
                \imap_errors();
438
                /** @noinspection UnusedFunctionResultInspection */
439
                \imap_alerts();
440 2
                \imap_close($mbox);
441
442 2
                return true;
443
            }
444
445
            /** @noinspection UnusedFunctionResultInspection */
446
            \imap_errors();
447
            /** @noinspection UnusedFunctionResultInspection */
448
            \imap_alerts();
449
            \imap_close($mbox);
450
451
            return false;
452
        }
453
454 3
        /** @noinspection UnusedFunctionResultInspection */
455
        \imap_errors();
456 3
        /** @noinspection UnusedFunctionResultInspection */
457
        \imap_alerts();
458
        \imap_close($mbox);
459
460
        return false;
461
    }
462
463 3
    /**
464
     * open a mail box in local file system
465
     *
466
     * @param string $filePath The local mailbox file path
467 3
     *
468
     * @return bool
469
     */
470
    public function openLocal(string $filePath): bool
471
    {
472 3
        \set_time_limit(self::SECONDS_TIMEOUT);
473 3
474 3
        if (!$this->testMode) {
475 3
            $this->mailboxLink = \imap_open($filePath, '', '', CL_EXPUNGE);
476 3
        } else {
477 3
            $this->mailboxLink = \imap_open($filePath, '', '', OP_READONLY);
478 3
        }
479
480
        if (!$this->mailboxLink) {
481 3
            $this->errorMessage = 'Cannot open the mailbox file to ' . $filePath . $this->bmhNewLine . 'Error MSG: ' . \imap_last_error();
482
            $this->output();
483
484
            return false;
485
        }
486 3
487 2
        $this->output('Opened ' . $filePath);
488 2
489 1
        return true;
490 1
    }
491 1
492 1
    /**
493
     * open a mail box
494
     *
495 1
     * @return bool
496
     */
497
    public function openMailbox(): bool
498
    {
499
        // before starting the processing, let's check the delete flag and do global deletes if true
500 3
        if (\trim($this->deleteMsgDate) !== '') {
501
            echo 'processing global delete based on date of ' . $this->deleteMsgDate . '<br />';
502
            $this->globalDelete();
503 3
        }
504
505 3
        // disable move operations if server is Gmail ... Gmail does not support mailbox creation
506
        if (\stripos($this->mailhost, 'gmail') !== false) {
507
            $this->moveSoft = false;
508
            $this->moveHard = false;
509 3
        }
510 3
511 3
        $port = $this->port . '/' . $this->service . '/' . $this->serviceOption;
512 3
513 3
        \set_time_limit(self::SECONDS_TIMEOUT);
514 3
515 3
        if (!$this->testMode) {
516 3
            $this->mailboxLink = \imap_open('{' . $this->mailhost . ':' . $port . '}' . $this->boxname, $this->mailboxUserName, $this->mailboxPassword, CL_EXPUNGE);
517 3
        } else {
518 3
            $this->mailboxLink = \imap_open('{' . $this->mailhost . ':' . $port . '}' . $this->boxname, $this->mailboxUserName, $this->mailboxPassword, OP_READONLY);
519 3
        }
520 2
521 3
        if (!$this->mailboxLink) {
522 2
            $this->errorMessage = 'Cannot create ' . $this->service . ' connection to ' . $this->mailhost . $this->bmhNewLine . 'Error MSG: ' . \imap_last_error();
523 2
            $this->output();
524
525 3
            return false;
526
        }
527 3
528
        $this->output('Connected to: ' . $this->mailhost . ' (' . $this->mailboxUserName . ')');
529
530
        return true;
531
    }
532
533
    /**
534
     * output additional msg for debug
535 3
     *
536
     * @param mixed $msg          if not given, output the last error msg
537 3
     * @param int   $verboseLevel the output level of this message
538
     */
539
    public function output($msg = '', int $verboseLevel = self::VERBOSE_SIMPLE)
540
    {
541
        if ($this->verbose >= $verboseLevel) {
542
            if ($msg) {
543
                echo $msg . $this->bmhNewLine;
544
            } else {
545
                echo $this->errorMessage . $this->bmhNewLine;
546
            }
547
        }
548
    }
549
550
    /**
551
     * Function to process each individual message.
552
     *
553
     * @param int    $pos          message number
554
     * @param string $type         DNS or BODY type
555
     * @param int    $totalFetched total number of messages in mailbox
556
     *
557
     * @return array|false <p>"$result"-array or false</p>
558
     */
559
    public function processBounce(int $pos, string $type, int $totalFetched)
560
    {
561
        $header = \imap_headerinfo($this->mailboxLink, $pos);
562
        $subject = isset($header->subject) ? \strip_tags($header->subject) : '[NO SUBJECT]';
563
        $body = '';
564
        $headerFull = \imap_fetchheader($this->mailboxLink, $pos);
565
        $bodyFull = \imap_body($this->mailboxLink, $pos);
566
567
        if ($type == 'DSN') {
568
            // first part of DSN (Delivery Status Notification), human-readable explanation
569
            $dsnMsg = \imap_fetchbody($this->mailboxLink, $pos, '1');
570
            $dsnMsgStructure = \imap_bodystruct($this->mailboxLink, $pos, '1');
571 3
572 3 View Code Duplication
            if ($dsnMsgStructure->encoding == 4) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
573
                $dsnMsg = \quoted_printable_decode($dsnMsg);
574 3
            } elseif ($dsnMsgStructure->encoding == 3) {
575 2
                $dsnMsg = \base64_decode($dsnMsg, true);
576
            }
577 2
578
            // second part of DSN (Delivery Status Notification), delivery-status
579 2
            $dsnReport = \imap_fetchbody($this->mailboxLink, $pos, '2');
580
581
            // process bounces by rules
582
            $result = bmhDSNRules($dsnMsg, $dsnReport, $this->debugDsnRule);
583
            $result = \is_callable($this->customDSNRulesCallback) ? \call_user_func($this->customDSNRulesCallback, $result, $dsnMsg, $dsnReport, $this->debugDsnRule) : $result;
584 2
        } elseif ($type == 'BODY') {
585 2
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
586 2
            $structure = @\imap_fetchstructure($this->mailboxLink, $pos);
587
588
            if (!\is_object($structure)) {
589
                return false;
590
            }
591
592
            switch ($structure->type) {
593
                case 0: // Content-type = text
594
                    $body = \imap_fetchbody($this->mailboxLink, $pos, '1');
595
                    $result = bmhBodyRules($body, $structure, $this->debugBodyRule);
0 ignored issues
show
Documentation introduced by
$structure is of type object, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
596
                    $result = \is_callable($this->customBodyRulesCallback) ? \call_user_func($this->customBodyRulesCallback, $result, $body, $structure, $this->debugBodyRule) : $result;
597
598
                    break;
599
600
                case 1: // Content-type = multipart
601
                    $body = \imap_fetchbody($this->mailboxLink, $pos, '1');
602
603
                    // Detect encoding and decode - only base64
604
                    if ($structure->parts[0]->encoding == 4) {
605
                        $body = \quoted_printable_decode($body);
606
                    } elseif ($structure->parts[0]->encoding == 3) {
607
                        $body = \base64_decode($body, true);
608
                    }
609
610
                    $result = bmhBodyRules($body, $structure, $this->debugBodyRule);
0 ignored issues
show
Documentation introduced by
$structure is of type object, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
611
                    $result = \is_callable($this->customBodyRulesCallback) ? \call_user_func($this->customBodyRulesCallback, $result, $body, $structure, $this->debugBodyRule) : $result;
612
613
                    break;
614
615 2
                case 2: // Content-type = message
616
                    $body = \imap_body($this->mailboxLink, $pos);
617 3
618 3 View Code Duplication
                    if ($structure->encoding == 4) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
619
                        $body = \quoted_printable_decode($body);
620
                    } elseif ($structure->encoding == 3) {
621
                        $body = \base64_decode($body, true);
622
                    }
623
624
                    $body = \substr($body, 0, 1000);
625
                    $result = bmhBodyRules($body, $structure, $this->debugBodyRule);
0 ignored issues
show
Documentation introduced by
$structure is of type object, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
626
                    $result = \is_callable($this->customBodyRulesCallback) ? \call_user_func($this->customBodyRulesCallback, $result, $body, $structure, $this->debugBodyRule) : $result;
627
628
                    break;
629
630 3
                default: // un-support Content-type
631
                    $this->output('Msg #' . $pos . ' is unsupported Content-Type:' . $structure->type, self::VERBOSE_REPORT);
632
633 3
                    return false;
634 3
            }
635
        } else {
636
            // internal error
637 3
            $this->errorMessage = 'Internal Error: unknown type';
638 3
639
            return false;
640 3
        }
641
642
        $email = $result['email'];
643 3
        $bounceType = $result['bounce_type'];
644 3
645
        // workaround: I think there is a error in one of the reg-ex in "phpmailer-bmh_rules.php".
646 3
        if ($email && \strpos($email, 'TO:<') !== false) {
647 3
            $email = \str_replace('TO:<', '', $email);
648 3
        }
649 3
650 3
        if ($this->moveHard && $result['bounce_type'] == 'hard') {
651
            $remove = 'moved (hard)';
652 3
        } elseif ($this->moveSoft && $result['bounce_type'] == 'soft') {
653
            $remove = 'moved (soft)';
654
        } elseif ($this->disableDelete) {
655
            $remove = 0;
656
        } else {
657
            $remove = $result['remove'];
658
        }
659
660
        $ruleNumber = $result['rule_no'];
661
        $ruleCategory = $result['rule_cat'];
662
        $status_code = $result['status_code'];
663
        $action = $result['action'];
664 2
        $diagnostic_code = $result['diagnostic_code'];
665
        $xheader = false;
666 2
667
        if ($ruleNumber === '0000') {
668 2
            // unrecognized
669 2
            if (
670 2
                \trim($email) === ''
671 2
                &&
672 2
                \property_exists($header, 'fromaddress') === true
673
            ) {
674
                $email = $header->fromaddress;
675
            }
676
677 View Code Duplication
            if ($this->testMode) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
678
                $this->output('Match: ' . $ruleNumber . ':' . $ruleCategory . '; ' . $bounceType . '; ' . $email);
679
            } else {
680
                // code below will use the Callback function, but return no value
681
                $params = [
682
                    $pos,
683
                    $bounceType,
684
                    $email,
685
                    $subject,
686
                    $header,
687
                    $remove,
688 3
                    $ruleNumber,
689
                    $ruleCategory,
690 3
                    $totalFetched,
691 3
                    $body,
692 3
                    $headerFull,
693 3
                    $bodyFull,
694 3
                    $status_code,
695
                    $action,
696 3
                    $diagnostic_code,
697
                ];
698 2
                \call_user_func_array($this->actionFunction, $params);
699 2
            }
700 View Code Duplication
        } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
701 2
            // match rule, do bounce action
702 1
            if ($this->testMode) {
703 2
                $this->output('Match: ' . $ruleNumber . ':' . $ruleCategory . '; ' . $bounceType . '; ' . $email);
704
705
                return true;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return true; (boolean) is incompatible with the return type documented by BounceMailHandler\BounceMailHandler::processBounce of type array|false.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
706
            }
707
708 2
            $params = [
709
                $pos,
710
                $bounceType,
711 2
                $email,
712 2
                $subject,
713
                $xheader,
714 3
                $remove,
715
                $ruleNumber,
716 3
                $ruleCategory,
717
                $totalFetched,
718 3
                $body,
719 1
                $headerFull,
720
                $bodyFull,
721
                $status_code,
722 3
                $action,
723 3
                $diagnostic_code,
724 3
            ];
725 3
            \call_user_func_array($this->actionFunction, $params);
726 3
727 3
            return $result;
728
        }
729 1
730 1
        return false;
731
    }
732
733 1
    /**
734 1
     * process the messages in a mailbox
735 1
     *
736
     * @param bool|int $max $max maximum limit messages processed in one batch,
737
     *                      if not given uses the property $maxMessages
738
     *
739 1
     * @return bool
740 1
     */
741 1
    public function processMailbox($max = false): bool
742
    {
743
        if (
744
            empty($this->actionFunction)
745
            ||
746
            !\is_callable($this->actionFunction)
747
        ) {
748
            $this->errorMessage = 'Action function not found!';
749
            $this->output();
750
751
            return false;
752
        }
753
754
        if ($this->moveHard && ($this->disableDelete === false)) {
755
            $this->disableDelete = true;
756
        }
757
758
        if (!empty($max)) {
759
            $this->maxMessages = $max;
0 ignored issues
show
Documentation Bug introduced by
It seems like $max can also be of type boolean. However, the property $maxMessages is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
760
        }
761 3
762 3
        // initialize counters
763
        $totalCount = \imap_num_msg($this->mailboxLink);
764
        $fetchedCount = $totalCount;
765
        $processedCount = 0;
766
        $unprocessedCount = 0;
767
        $deletedCount = 0;
768
        $movedCount = 0;
769 3
        $this->output('Total: ' . $totalCount . ' messages ');
770 3
771
        // process maximum number of messages
772
        if ($fetchedCount > $this->maxMessages) {
773 3
            $fetchedCount = $this->maxMessages;
774
            $this->output('Processing first ' . $fetchedCount . ' messages ');
775
        }
776
777 3
        if ($this->testMode) {
778
            $this->output('Running in test mode, not deleting messages from mailbox<br />');
779 3
        } else {
780
            if ($this->disableDelete) {
781 3
                if ($this->moveHard) {
782 1
                    $this->output('Running in move mode<br />');
783 1
                } else {
784 2
                    $this->output('Running in disableDelete mode, not deleting messages from mailbox<br />');
785
                }
786
            } else {
787 3
                $this->output('Processed messages will be deleted from mailbox<br />');
788 3
            }
789 3
        }
790 3
791 3
        for ($x = 1; $x <= $fetchedCount; ++$x) {
792 3
793
            // fetch the messages one at a time
794 3
            if ($this->useFetchstructure) {
795
                /** @noinspection PhpUsageOfSilenceOperatorInspection */
796
                $structure = @\imap_fetchstructure($this->mailboxLink, $x);
797 3
798 3
                if (
799 3
                    $structure
800 3
                    &&
801 3
                    \is_object($structure)
802 3
                    &&
803
                    $structure->type == 1
804 3
                    &&
805 2
                    $structure->ifsubtype
806 2
                    &&
807
                    $structure->ifparameters
808
                    &&
809 1
                    \strtoupper($structure->subtype) == 'REPORT'
810 1
                    &&
811 1
                    $this->isParameter($structure->parameters, 'REPORT-TYPE', 'delivery-status')
812 1
                ) {
813 1
                    $processedResult = $this->processBounce($x, 'DSN', $totalCount);
814 1
                } else {
815 1
                    // not standard DSN msg
816 1
                    $this->output('Msg #' . $x . ' is not a standard DSN message', self::VERBOSE_REPORT);
817 1
818 1
                    if ($this->debugBodyRule) {
819 1
                        if ($structure->ifdescription) {
820 1
                            $this->output("  Content-Type : {$structure->description}", self::VERBOSE_DEBUG);
821 1
                        } else {
822 1
                            $this->output('  Content-Type : unsupported', self::VERBOSE_DEBUG);
823 1
                        }
824 1
                    }
825 1
826
                    $processedResult = $this->processBounce($x, 'BODY', $totalCount);
827 3
                }
828
            } else {
829 2
                $header = \imap_fetchheader($this->mailboxLink, $x);
830 2
831
                // Could be multi-line, if the new line begins with SPACE or HTAB
832 2
                if ($header && \preg_match("/Content-Type:((?:[^\n]|\n[\t ])+)(?:\n[^\t ]|$)/i", $header, $match)) {
833
                    if (
834
                        \preg_match("/multipart\/report/i", $match[1])
835
                        &&
836
                        \preg_match("/report-type=[\"']?delivery-status[\"']?/i", $match[1])
837
                    ) {
838
                        // standard DSN msg
839
                        $processedResult = $this->processBounce($x, 'DSN', $totalCount);
840
                    } else {
841
                        // not standard DSN msg
842
                        $this->output('Msg #' . $x . ' is not a standard DSN message', self::VERBOSE_REPORT);
843
844
                        if ($this->debugBodyRule) {
845
                            $this->output("  Content-Type : {$match[1]}", self::VERBOSE_DEBUG);
846
                        }
847
848
                        $processedResult = $this->processBounce($x, 'BODY', $totalCount);
849
                    }
850
                } else {
851
                    // didn't get content-type header
852
                    $this->output('Msg #' . $x . ' is not a well-formatted MIME mail, missing Content-Type', self::VERBOSE_REPORT);
853
854
                    if ($this->debugBodyRule) {
855
                        $this->output('  Headers: ' . $this->bmhNewLine . $header . $this->bmhNewLine, self::VERBOSE_DEBUG);
856 3
                    }
857
858
                    $processedResult = $this->processBounce($x, 'BODY', $totalCount);
859
                }
860
            }
861
862
            $deleteFlag[$x] = false;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$deleteFlag was never initialized. Although not strictly required by PHP, it is generally a good practice to add $deleteFlag = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
863
            $moveFlag[$x] = false;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$moveFlag was never initialized. Although not strictly required by PHP, it is generally a good practice to add $moveFlag = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
864
865
            if ($processedResult !== false) {
866
                ++$processedCount;
867 3
868
                if (!$this->disableDelete) {
869 3
                    // delete the bounce if not in disableDelete mode
870
                    if (!$this->testMode) {
871
                        /** @noinspection PhpUsageOfSilenceOperatorInspection */
872
                        @\imap_delete($this->mailboxLink, $x);
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...
873
                    }
874
875
                    $deleteFlag[$x] = true;
0 ignored issues
show
Bug introduced by
The variable $deleteFlag does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
876 3
                    ++$deletedCount;
877
                } elseif ($this->moveHard && $processedResult['bounce_type'] === 'hard') {
878 3
                    // check if the move directory exists, if not create it
879
                    if (!$this->testMode) {
880 3
                        $this->mailboxExist($this->hardMailbox);
881 2
                    }
882
883
                    // move the message
884 1
                    if (!$this->testMode) {
885 1
                        /** @noinspection PhpUsageOfSilenceOperatorInspection */
886
                        @\imap_mail_move($this->mailboxLink, (string) $x, $this->hardMailbox);
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...
887 1
                    }
888 1
889
                    $moveFlag[$x] = true;
0 ignored issues
show
Bug introduced by
The variable $moveFlag does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
890 1
                    ++$movedCount;
891 1
                } elseif ($this->moveSoft && $processedResult['bounce_type'] === 'soft') {
892 1
                    // check if the move directory exists, if not create it
893 1
                    if (!$this->testMode) {
894 1
                        $this->mailboxExist($this->softMailbox);
895 1
                    }
896
897 1
                    // move the message
898
                    if (!$this->testMode) {
899 1
                        /** @noinspection PhpUsageOfSilenceOperatorInspection */
900 1
                        @\imap_mail_move($this->mailboxLink, (string) $x, $this->softMailbox);
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...
901
                    }
902 1
903
                    $moveFlag[$x] = true;
904 1
                    ++$movedCount;
905
                }
906 1
            } else {
907
                // not processed
908
                ++$unprocessedCount;
909
                if (!$this->disableDelete && $this->purgeUnprocessed) {
910
                    // delete this bounce if not in disableDelete mode, and the flag BOUNCE_PURGE_UNPROCESSED is set
911
                    if (!$this->testMode) {
912
                        /** @noinspection PhpUsageOfSilenceOperatorInspection */
913
                        @\imap_delete($this->mailboxLink, $x);
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...
914
                    }
915
916
                    $deleteFlag[$x] = true;
917
                    ++$deletedCount;
918
                }
919
920
                // check if the move directory exists, if not create it
921
                $this->mailboxExist($this->unprocessedBox);
922
                // move the message
923
                /** @noinspection PhpUsageOfSilenceOperatorInspection */
924
                @\imap_mail_move($this->mailboxLink, (string) $x, $this->unprocessedBox);
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
                $moveFlag[$x] = true;
926
            }
927
928
            \flush();
929
        }
930
931
        $this->output($this->bmhNewLine . 'Closing mailbox, and purging messages');
932
933
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
934
        @\imap_expunge($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...
935
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
936
        @\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...
937
938
        $this->output('Read: ' . $fetchedCount . ' messages');
939
        $this->output($processedCount . ' action taken');
940
        $this->output($unprocessedCount . ' no action taken');
941
        $this->output($deletedCount . ' messages deleted');
942
        $this->output($movedCount . ' messages moved');
943
944
        return true;
945
    }
946
}
947