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