1<?php
2
3require_once dirname(__FILE__).'/accesscheck.php';
4
5if (!$GLOBALS['commandline'] && !$GLOBALS['inRemoteCall']) {
6    // browser session
7    ob_end_flush();
8    if (!MANUALLY_PROCESS_BOUNCES) {
9        echo $GLOBALS['I18N']->get('This page can only be called from the commandline');
10
11        return;
12    }
13    if (isset($_GET['login']) || isset($_GET['password'])) {
14        echo Error(s('Remote processing of the queue is now handled with a processing secret'));
15
16        return;
17    }
18
19    //# we're in a normal session, so the csrf token should work
20    verifyCsrfGetToken();
21}
22
23flush();
24$outputdone = 0;
25function prepareOutput()
26{
27    global $outputdone;
28    if (!$outputdone) {
29        $outputdone = 1;
30
31        return formStart('name="outputform" class="processbounces" ').'<textarea name="output" rows=10 cols=50></textarea></form>';
32    }
33}
34
35$report = '';
36//# some general functions
37function finishBounceProcessing($flag, $message)
38{
39    if ($flag == 'error') {
40        $subject = $GLOBALS['I18N']->get('Bounce processing error');
41    } elseif ($flag == 'info') {
42        $subject = $GLOBALS['I18N']->get('Bounce Processing info');
43    }
44    if (!TEST && $message) {
45        sendReport($subject, $message);
46    }
47}
48
49function bounceProcessError($message)
50{
51    outputProcessBounce("$message");
52    finishBounceProcessing('error', $message);
53    exit;
54}
55
56function processbounces_shutdown()
57{
58    global $report, $process_id;
59    releaseLock($process_id);
60    // $report .= "Connection status:".connection_status();
61    finishBounceProcessing('info', $report);
62}
63
64function outputProcessBounce($message, $reset = 0)
65{
66    $infostring = '['.date('D j M Y H:i',
67            time()).'] ['.getenv('REMOTE_HOST').'] ['.getClientIP().']';
68    //print "$infostring $message<br/>\n";
69    $message = preg_replace("/\n/", '', $message);
70    //# contribution from http://forums.phplist.com/viewtopic.php?p=14648
71    //# in languages with accented characters replace the HTML back
72    //Replace the "&rsquo;" which is not replaced by html_decode
73    $message = preg_replace('/&rsquo;/', "'", $message);
74    //Decode HTML chars
75    //$message = html_entity_decode($message,ENT_QUOTES,$_SESSION['adminlanguage']['charset']);
76    $message = html_entity_decode($message, ENT_QUOTES, 'UTF-8');
77    if ($GLOBALS['commandline']) {
78        cl_output($message);
79    } elseif ($GLOBALS['inRemoteCall']) {
80        ob_end_clean();
81        echo $message, "\n";
82        ob_start();
83    } else {
84        if ($reset) {
85            echo '<script language="Javascript" type="text/javascript">
86//        if (document.forms[0].name == "outputform") {
87          document.outputform.output.value = "";
88          document.outputform.output.value += "\n";
89//        }
90      </script>' .PHP_EOL;
91        }
92
93        echo '<script language="Javascript" type="text/javascript">
94//      if (document.forms[0].name == "outputform") {
95        document.outputform.output.value += "' .$message.'";
96        document.outputform.output.value += "\n";
97//      } else
98//        document.writeln("' .$message.'");
99    </script>' .PHP_EOL;
100    }
101
102    flush();
103}
104
105function findMessageId($text)
106{
107    $msgid = 0;
108
109    if (preg_match('/(?:X-MessageId|X-Message): (.*)\r\n/iU', $text, $match)) {
110        $msgid = trim($match[1]);
111    }
112
113    return $msgid;
114}
115
116function findUserID($text)
117{
118    global $tables;
119    $userid = 0;
120
121    if (preg_match('/(?:X-ListMember|X-User): (.*)\r\n/iU', $text, $match)) {
122        $user = trim($match[1]);
123
124        // some versions used the email to identify the users, some the userid and others the uniqid
125        // use backward compatible way to find user
126        if (strpos($user, '@') !== false) {
127            $userid_req = Sql_Fetch_Row_Query(sprintf('select id from %s where email = "%s"', $tables['user'],
128                sql_escape($user)));
129            if ($userid_req) {
130                $userid = $userid_req[0];
131            }
132        } elseif (preg_match("/^\d$/", $user)) {
133            $userid = $user;
134        } elseif (!empty($user)) {
135            $userid_req = Sql_Fetch_Row_Query(sprintf('select id from %s where uniqid = "%s"', $tables['user'],
136                sql_escape($user)));
137            if ($userid_req) {
138                $userid = $userid_req[0];
139            }
140        }
141    } else {
142        //## if we didn't find any, parse anything looking like an email address and check if it's a subscriber.
143        //# this is probably fairly time consuming, but as the process is only done once every so often
144        //# that should not be too bad
145
146        preg_match_all('/[._a-zA-Z0-9-]+@[.a-zA-Z0-9-]+/', $text, $regs);
147
148        foreach ($regs[0] as $email) {
149            $useridQ = Sql_Fetch_Row_Query(sprintf('select id from %s where email = "%s"', $tables['user'],
150                sql_escape($email)));
151            if (!empty($useridQ[0])) {
152                $userid = $useridQ[0];
153                break;
154            }
155        }
156    }
157
158    return $userid;
159}
160
161function decodeBody($header, $body)
162{
163    $transfer_encoding = '';
164    if (preg_match('/Content-Transfer-Encoding: ([\w-]+)/i', $header, $regs)) {
165        $transfer_encoding = strtolower($regs[1]);
166    }
167    switch ($transfer_encoding) {
168        case 'quoted-printable':
169            $decoded_body = @imap_qprint($body);
170            break;
171        case 'base64':
172            $decoded_body = @imap_base64($body);
173            break;
174        case '7bit':
175        case '8bit':
176        default:
177            // $body = $body;
178    }
179    if (!empty($decoded_body)) {
180        return $decoded_body;
181    } else {
182        return $body;
183    }
184}
185
186function processImapBounce($link, $num, $header)
187{
188    global $tables;
189    $headerinfo = imap_headerinfo($link, $num);
190    $bounceDate = @strtotime($headerinfo->date);
191    $body = imap_body($link, $num);
192    $body = decodeBody($header, $body);
193
194    $msgid = findMessageId($body);
195    $userid = findUserID($body);
196    if (VERBOSE) {
197        outputProcessBounce('UID'.$userid.' MSGID'.$msgid);
198    }
199
200    //# @TODO add call to plugins to determine what to do.
201    // for now, quick hack to zap MsExchange Delayed messages
202    if (preg_match('/Action: delayed\s+Status: 4\.4\.7/im', $body)) {
203        //# just say we did something, when actually we didn't
204        return true;
205    }
206    $bounceDateFormatted = date('Y-m-d H:i:s', $bounceDate);
207
208    Sql_Query(sprintf('insert into %s (date,header,data)
209    values("%s","%s","%s")',
210        $tables['bounce'],
211        $bounceDateFormatted,
212        addslashes($header),
213        addslashes($body)));
214
215    $bounceid = Sql_Insert_Id();
216
217    return processBounceData($bounceid, $msgid, $userid, $bounceDateFormatted);
218}
219
220function processBounceData($bounceid, $msgid, $userid, $bounceDate = null)
221{
222    global $tables;
223    $useremailQ = Sql_fetch_row_query(sprintf('select email from %s where id = %d', $tables['user'], $userid));
224    if (empty($useremailQ)) {
225        $useremail = "";
226    } else {
227        $useremail = $useremailQ[0];
228    }
229
230    if ($bounceDate === null) {
231        $bounceDate = date('Y-m-d H:i', time());
232    }
233
234    if ($msgid === 'systemmessage' && !empty($userid)) {
235        Sql_Query(sprintf('update %s
236      set status = "bounced system message",
237      comment = "%s marked unconfirmed"
238      where id = %d',
239            $tables['bounce'],
240            $userid, $bounceid));
241
242        #Use the date of the bounce, instead of "now" as processing may be different
243        Sql_Query(sprintf('INSERT INTO %s
244            (
245                        user,
246                        message,
247                        bounce,
248                        time
249            )
250            VALUES
251            (
252                        %d,
253                        -1,
254                        %d,
255                        "%s"
256            )',
257                $tables['user_message_bounce'],
258                $userid, $bounceid, $bounceDate)
259        );
260        logEvent("$userid ".$GLOBALS['I18N']->get('system message bounced, user marked unconfirmed'));
261        addUserHistory($useremail, $GLOBALS['I18N']->get('Bounced system message'), '
262    <br/>' .$GLOBALS['I18N']->get('User marked unconfirmed')."
263    <br/><a href=\"./?page=bounce&amp;id=$bounceid\">".$GLOBALS['I18N']->get('View Bounce').'</a>
264
265    ');
266        Sql_Query(sprintf('update %s
267      set confirmed = 0
268      where id = %d',
269            $tables['user'],
270            $userid));
271    } elseif (!empty($msgid) && !empty($userid)) {
272        //# check if we already have this um as a bounce
273        //# so that we don't double count "delayed" like bounces
274        $exists = Sql_Fetch_Row_Query(sprintf('select count(*) from %s where user = %d and message = %d',
275            $tables['user_message_bounce'], $userid, $msgid));
276        if (empty($exists[0])) {
277            Sql_Query(sprintf('insert into %s
278        set user = %d, message = %d, bounce = %d',
279                $tables['user_message_bounce'],
280                $userid, $msgid, $bounceid));
281            Sql_Query(sprintf('update %s
282        set status = "bounced list message %d",
283        comment = "%s bouncecount increased"
284        where id = %d',
285                $tables['bounce'],
286                $msgid,
287                $userid, $bounceid));
288            Sql_Query(sprintf('update %s
289        set bouncecount = bouncecount + 1
290        where id = %d',
291                $tables['message'],
292                $msgid));
293            Sql_Query(sprintf('update %s
294        set bouncecount = bouncecount + 1
295        where id = %d',
296                $tables['user'],
297                $userid));
298        } else {
299            //# we create the relationship, but don't increase counters
300            Sql_Query(sprintf('insert into %s
301        set user = %d, message = %d, bounce = %d',
302                $tables['user_message_bounce'],
303                $userid, $msgid, $bounceid));
304
305            //# we cannot translate this text
306            Sql_Query(sprintf('update %s
307        set status = "duplicate bounce for %d",
308        comment = "duplicate bounce for subscriber %d on message %d"
309        where id = %d',
310                $tables['bounce'],
311                $userid,
312                $userid, $msgid, $bounceid));
313        }
314    } elseif ($userid) {
315        Sql_Query(sprintf('update %s
316      set status = "bounced unidentified message",
317      comment = "%s bouncecount increased"
318      where id = %d',
319            $tables['bounce'],
320            $userid, $bounceid));
321        Sql_Query(sprintf('update %s
322      set bouncecount = bouncecount + 1
323      where id = %d',
324            $tables['user'],
325            $userid));
326    } elseif ($msgid === 'systemmessage') {
327        Sql_Query(sprintf('update %s
328      set status = "bounced system message",
329      comment = "unknown user"
330      where id = %d',
331            $tables['bounce'],
332            $bounceid));
333        logEvent("$userid ".$GLOBALS['I18N']->get('system message bounced, but unknown user'));
334    } elseif ($msgid) {
335        Sql_Query(sprintf('update %s
336      set status = "bounced list message %d",
337      comment = "unknown user"
338      where id = %d',
339            $tables['bounce'],
340            $msgid,
341            $bounceid));
342        Sql_Query(sprintf('update %s
343      set bouncecount = bouncecount + 1
344      where id = %d',
345            $tables['message'],
346            $msgid));
347    } else {
348        Sql_Query(sprintf('update %s
349      set status = "unidentified bounce",
350      comment = "not processed"
351      where id = %d',
352            $tables['bounce'],
353            $bounceid));
354
355        return false;
356    }
357
358    return true;
359}
360
361function processPop($server, $user, $password)
362{
363    $port = $GLOBALS['bounce_mailbox_port'];
364    if (!$port) {
365        $port = '110/pop3/notls';
366    }
367    set_time_limit(6000);
368
369    $link = imap_open('{'.$server.':'.$port.'}INBOX', $user, $password);
370
371    if (!$link) {
372        outputProcessBounce($GLOBALS['I18N']->get('Cannot create POP3 connection to')." $server: ".imap_last_error());
373
374        return false;
375    }
376
377    return processMessages($link, 100000);
378}
379
380function processMbox($file)
381{
382    set_time_limit(6000);
383
384    if (!TEST) {
385        $link = imap_open($file, '', '', CL_EXPUNGE);
386    } else {
387        $link = imap_open($file, '', '');
388    }
389    if (!$link) {
390        outputProcessBounce($GLOBALS['I18N']->get('Cannot open mailbox file').' '.imap_last_error());
391
392        return false;
393    }
394
395    return processMessages($link, 100000);
396}
397
398function processMessages($link, $max = 3000)
399{
400    global $bounce_mailbox_purge_unprocessed, $bounce_mailbox_purge;
401    $num = imap_num_msg($link);
402    outputProcessBounce(s('%d bounces to fetch from the mailbox', $num).PHP_EOL);
403
404    if ($num == 0) {
405        imap_close($link);
406
407        return '';
408    }
409    outputProcessBounce($GLOBALS['I18N']->get('Please do not interrupt this process').PHP_EOL);
410    $report = ' '.s('%d bounces to process', $num).PHP_EOL;
411    if ($num > $max) {
412        outputProcessBounce(s('Processing first %d bounces', $max).PHP_EOL);
413        $report .= s('Processing first %d bounces', $max).PHP_EOL;
414        $num = $max;
415    }
416    if (TEST) {
417        echo s('Running in test mode, not deleting messages from mailbox').'<br/>';
418    } else {
419        echo s('Processed messages will be deleted from the mailbox').'<br/>';
420    }
421    $nberror = 0;
422//  for ($x=1;$x<150;$x++) {
423    for ($x = 1; $x <= $num; ++$x) {
424        set_time_limit(60);
425        $header = imap_fetchheader($link, $x);
426        if ($x % 25 == 0) {
427            //    outputProcessBounce( $x . " ". nl2br($header));
428            outputProcessBounce($x.' done', 1);
429        }
430        echo PHP_EOL;
431        flush();
432        $processed = processImapBounce($link, $x, $header);
433        if ($processed) {
434            if (!TEST && $bounce_mailbox_purge) {
435                if (VERBOSE) {
436                    outputProcessBounce($GLOBALS['I18N']->get('Deleting message')." $x");
437                }
438                imap_delete($link, $x);
439            } elseif (VERBOSE) {
440                outputProcessBounce(s('Not deleting processed message')." $x $bounce_mailbox_purge");
441            }
442        } else {
443            if (!TEST && $bounce_mailbox_purge_unprocessed) {
444                if (VERBOSE) {
445                    outputProcessBounce($GLOBALS['I18N']->get('Deleting message')." $x");
446                }
447                imap_delete($link, $x);
448            } elseif (VERBOSE) {
449                outputProcessBounce(s('Not deleting unprocessed message')." $x");
450            }
451        }
452        flush();
453    }
454    flush();
455    outputProcessBounce(s('Closing mailbox, and purging messages'));
456    set_time_limit(60 * $num);
457    if (!TEST) {
458        imap_close($link, CL_EXPUNGE);
459    } else {
460        imap_close($link);
461    }
462
463    return $report;
464}
465
466if (!function_exists('imap_open')) {
467    Error($GLOBALS['I18N']->get('IMAP is not included in your PHP installation, cannot continue').
468        '<br/>'.$GLOBALS['I18N']->get('Check out').
469        ' <a href="http://www.php.net/manual/en/ref.imap.php">http://www.php.net/manual/en/ref.imap.php</a>');
470
471    return;
472}
473
474if (empty($bounce_mailbox) && (empty($bounce_mailbox_host) || empty($bounce_mailbox_user) || empty($bounce_mailbox_password))) {
475    Error($GLOBALS['I18N']->get('Bounce mechanism not properly configured'));
476
477    return;
478}
479
480// lets not do this unless we do some locking first
481register_shutdown_function('processbounces_shutdown');
482$abort = ignore_user_abort(1);
483if (!empty($GLOBALS['commandline']) && isset($cline['f'])) {
484    // force set, so kill other processes
485    cl_output(s('Force set, killing other send processes'));
486    $process_id = getPageLock(1);
487} else {
488    $process_id = getPageLock();
489}
490if (empty($process_id)) {
491    return;
492}
493echo prepareOutput();
494flushBrowser();
495
496$download_report = '';
497switch ($bounce_protocol) {
498    case 'pop':
499        $download_report = processPop($bounce_mailbox_host, $bounce_mailbox_user, $bounce_mailbox_password);
500        break;
501    case 'mbox':
502        $download_report = processMbox($bounce_mailbox);
503        break;
504    default:
505        Error($GLOBALS['I18N']->get('bounce_protocol not supported'));
506
507        return;
508}
509
510if ($GLOBALS['commandline'] && $download_report === false) {
511    cl_output(s('Download failed, exiting'));
512
513    return;
514}
515// now we have filled database with all available bounces
516
517//# reprocess the unidentified ones, as the bounce detection has improved, so it might catch more
518
519cl_output('reprocessing');
520$reparsed = $count = 0;
521$reidentified = 0;
522$req = Sql_Query(sprintf('select * from %s where status = "unidentified bounce"', $tables['bounce']));
523$total = Sql_Affected_Rows();
524cl_output(s('%d bounces to reprocess', $total));
525while ($bounce = Sql_Fetch_Assoc($req)) {
526    ++$count;
527    if ($count % 25 == 0) {
528        cl_progress(s('%d out of %d processed', $count, $total));
529    }
530    $bounceBody = decodeBody($bounce['header'], $bounce['data']);
531    $userid = findUserID($bounceBody);
532    $messageid = findMessageId($bounceBody);
533    if (!empty($userid) || !empty($messageid)) {
534        ++$reparsed;
535        if (processBounceData($bounce['id'], $messageid, $userid)) {
536            ++$reidentified;
537        }
538    }
539}
540cl_output(s('%d out of %d processed', $count, $total));
541if (VERBOSE) {
542    outputProcessBounce(s('%d bounces were re-processed and %d bounces were re-identified', $reparsed, $reidentified));
543}
544$advanced_report = '';
545$bouncerules = loadBounceRules();
546if (count($bouncerules)) {
547    outputProcessBounce($GLOBALS['I18N']->get('Processing bounces based on active bounce rules'));
548    $matched = 0;
549    $notmatched = 0;
550    $limit = ' limit 10000';
551    $limit = '';
552
553    //# run this in batches. With many bounces this query runs OOM
554    $bounceCount = Sql_Fetch_Row_Query(sprintf('select count(*) from %s', $GLOBALS['tables']['user_message_bounce']));
555    $total = $bounceCount[0];
556    $counter = 0;
557    $batchSize = 500; //# @TODO make a config, to allow tweaking on bigger systems
558    while ($counter < $total) {
559        $limit = ' limit '.$counter.', '.$batchSize;
560        $counter += $batchSize;
561        cl_progress(s('processed %d out of %d bounces for advanced bounce rules', $counter, $total));
562
563        $req = Sql_Query(sprintf('select * from %s as bounce, %s as umb where bounce.id = umb.bounce %s',
564            $GLOBALS['tables']['bounce'], $GLOBALS['tables']['user_message_bounce'], $limit));
565        while ($row = Sql_Fetch_Array($req)) {
566            $alive = checkLock($process_id);
567            if ($alive) {
568                keepLock($process_id);
569            } else {
570                bounceProcessError($GLOBALS['I18N']->get('Process Killed by other process'));
571            }
572            //    cl_output(memory_get_usage());
573
574            //    outputProcessBounce('User '.$row['user']);
575            $rule = matchBounceRules($row['header']."\n\n".$row['data'], $bouncerules);
576            //    outputProcessBounce('Action '.$rule['action']);
577            //    outputProcessBounce('Rule'.$rule['id']);
578            $userdata = array();
579            if ($rule && is_array($rule)) {
580                if ($row['user']) {
581                    $userdata = Sql_Fetch_Array_Query("select * from {$tables['user']} where id = ".$row['user']);
582                }
583                $report_linkroot = $GLOBALS['admin_scheme'].'://'.$GLOBALS['website'].$GLOBALS['adminpages'];
584
585                Sql_Query(sprintf('update %s set count = count + 1 where id = %d',
586                    $GLOBALS['tables']['bounceregex'], $rule['id']));
587                Sql_Query(sprintf('insert ignore into %s (regex,bounce) values(%d,%d)',
588                    $GLOBALS['tables']['bounceregex_bounce'], $rule['id'], $row['bounce']));
589
590                //17860 - check the current status to avoid doing it over and over
591                $currentStatus = Sql_Fetch_Assoc_Query(sprintf('select confirmed,blacklisted from %s where id = %d', $GLOBALS['tables']['user'],$row['user']));
592                $confirmed = !empty($currentStatus['confirmed']);
593                $blacklisted = !empty($currentStatus['blacklisted']);
594
595                switch ($rule['action']) {
596                    case 'deleteuser':
597                        logEvent('User '.$userdata['email'].' deleted by bounce rule '.PageLink2('bouncerule&amp;id='.$rule['id'],
598                                $rule['id']));
599                        $advanced_report .= 'User '.$userdata['email'].' deleted by bounce rule '.$rule['id'].PHP_EOL;
600                        $advanced_report .= 'User: '.$report_linkroot.'/?page=user&amp;id='.$userdata['id'].PHP_EOL;
601                        $advanced_report .= 'Rule: '.$report_linkroot.'/?page=bouncerule&amp;id='.$rule['id'].PHP_EOL;
602                        deleteUser($row['user']);
603                        break;
604                    case 'unconfirmuser':
605                        if ($confirmed) {
606                            logEvent('User ' . $userdata['email'] . ' unconfirmed by bounce rule ' . PageLink2('bouncerule&amp;id=' . $rule['id'],
607                                    $rule['id']));
608                            Sql_Query(sprintf('update %s set confirmed = 0 where id = %d', $GLOBALS['tables']['user'],
609                                $row['user']));
610                            $advanced_report .= 'User ' . $userdata['email'] . ' made unconfirmed by bounce rule ' . $rule['id'] . PHP_EOL;
611                            $advanced_report .= 'User: ' . $report_linkroot . '/?page=user&amp;id=' . $userdata['id'] . PHP_EOL;
612                            $advanced_report .= 'Rule: ' . $report_linkroot . '/?page=bouncerule&amp;id=' . $rule['id'] . PHP_EOL;
613                            addUserHistory($userdata['email'], s('Auto Unconfirmed'),
614                                s('Subscriber auto unconfirmed for') . ' ' . s('bounce rule') . ' ' . $rule['id']);
615                            addSubscriberStatistics('auto unsubscribe', 1);
616                        }
617                        break;
618                    case 'deleteuserandbounce':
619                        logEvent('User '.$userdata['email'].' deleted by bounce rule '.PageLink2('bouncerule&amp;id='.$rule['id'],
620                                $rule['id']));
621                        $advanced_report .= 'User '.$userdata['email'].' deleted by bounce rule '.$rule['id'].PHP_EOL;
622                        $advanced_report .= 'User: '.$report_linkroot.'/?page=user&amp;id='.$userdata['id'].PHP_EOL;
623                        $advanced_report .= 'Rule: '.$report_linkroot.'/?page=bouncerule&amp;id='.$rule['id'].PHP_EOL;
624                        deleteUser($row['user']);
625                        deleteBounce($row['bounce']);
626                        break;
627                    case 'unconfirmuseranddeletebounce':
628                        if ($confirmed) {
629                            logEvent('User ' . $userdata['email'] . ' unconfirmed by bounce rule ' . PageLink2('bouncerule&amp;id=' . $rule['id'],
630                                    $rule['id']));
631                            Sql_Query(sprintf('update %s set confirmed = 0 where id = %d', $GLOBALS['tables']['user'],
632                                $row['user']));
633                            $advanced_report .= 'User ' . $userdata['email'] . ' made unconfirmed by bounce rule ' . $rule['id'] . PHP_EOL;
634                            $advanced_report .= 'User: ' . $report_linkroot . '/?page=user&amp;id=' . $userdata['id'] . PHP_EOL;
635                            $advanced_report .= 'Rule: ' . $report_linkroot . '/?page=bouncerule&amp;id=' . $rule['id'] . PHP_EOL;
636                            addUserHistory($userdata['email'], s('Auto unconfirmed'),
637                                s('Subscriber auto unconfirmed for') . ' ' . $GLOBALS['I18N']->get('bounce rule') . ' ' . $rule['id']);
638                            addSubscriberStatistics('auto unsubscribe', 1);
639                        }
640                        deleteBounce($row['bounce']);
641                        break;
642                    case 'blacklistuser':
643                        if (!$blacklisted) {
644                            logEvent('User ' . $userdata['email'] . ' blacklisted by bounce rule ' . PageLink2('bouncerule&amp;id=' . $rule['id'],
645                                    $rule['id']));
646                            addUserToBlacklist($userdata['email'],
647                                s('Subscriber auto blacklisted  by bounce rule', $rule['id']));
648                            $advanced_report .= 'User ' . $userdata['email'] . ' blacklisted by bounce rule ' . $rule['id'] . PHP_EOL;
649                            $advanced_report .= 'User: ' . $report_linkroot . '/?page=user&amp;id=' . $userdata['id'] . PHP_EOL;
650                            $advanced_report .= 'Rule: ' . $report_linkroot . '/?page=bouncerule&amp;id=' . $rule['id'] . PHP_EOL;
651                            addUserHistory($userdata['email'], $GLOBALS['I18N']->get('Auto Unsubscribed'),
652                                $GLOBALS['I18N']->get('User auto unsubscribed for') . ' ' . $GLOBALS['I18N']->get('bounce rule') . ' ' . $rule['id']);
653                            addSubscriberStatistics('auto blacklist', 1);
654                        }
655                        break;
656                    case 'blacklistuseranddeletebounce':
657                        if (!$blacklisted) {
658                            logEvent('User ' . $userdata['email'] . ' blacklisted by bounce rule ' . PageLink2('bouncerule&amp;id=' . $rule['id'],
659                                    $rule['id']));
660                            addUserToBlacklist($userdata['email'],
661                                s('Subscriber auto blacklisted by bounce rule %d', $rule['id']));
662                            $advanced_report .= 'User ' . $userdata['email'] . ' blacklisted by bounce rule ' . $rule['id'] . PHP_EOL;
663                            $advanced_report .= 'User: ' . $report_linkroot . '/?page=user&amp;id=' . $userdata['id'] . PHP_EOL;
664                            $advanced_report .= 'Rule: ' . $report_linkroot . '/?page=bouncerule&amp;id=' . $rule['id'] . PHP_EOL;
665                            addUserHistory($userdata['email'], $GLOBALS['I18N']->get('Auto Unsubscribed'),
666                                $GLOBALS['I18N']->get('User auto unsubscribed for') . ' ' . $GLOBALS['I18N']->get('bounce rule') . ' ' . $rule['id']);
667                            addSubscriberStatistics('auto blacklist', 1);
668                        }
669                        deleteBounce($row['bounce']);
670                        break;
671                    case 'blacklistemail':
672                        logEvent('email '.$userdata['email'].' blacklisted by bounce rule '.PageLink2('bouncerule&amp;id='.$rule['id'],
673                                $rule['id']));
674                        addEmailToBlackList($userdata['email'],
675                            s('Email address auto blacklisted by bounce rule %d', $rule['id']));
676                        $advanced_report .= 'email '.$userdata['email'].' blacklisted by bounce rule '.$rule['id'].PHP_EOL;
677                        $advanced_report .= 'User: '.$report_linkroot.'/?page=user&amp;id='.$userdata['id'].PHP_EOL;
678                        $advanced_report .= 'Rule: '.$report_linkroot.'/?page=bouncerule&amp;id='.$rule['id'].PHP_EOL;
679                        addUserHistory($userdata['email'], $GLOBALS['I18N']->get('Auto Unsubscribed'),
680                            $GLOBALS['I18N']->get('email auto unsubscribed for').' '.$GLOBALS['I18N']->get('bounce rule').' '.$rule['id']);
681                        addSubscriberStatistics('auto blacklist', 1);
682                        break;
683                    case 'blacklistemailanddeletebounce':
684                        logEvent('email '.$userdata['email'].' blacklisted by bounce rule '.PageLink2('bouncerule&amp;id='.$rule['id'],
685                                $rule['id']));
686                        addEmailToBlackList($userdata['email'],
687                            s('Email address auto blacklisted by bounce rule %d', $rule['id']));
688                        $advanced_report .= 'email '.$userdata['email'].' blacklisted by bounce rule '.$rule['id'].PHP_EOL;
689                        $advanced_report .= 'User: '.$report_linkroot.'/?page=user&amp;id='.$userdata['id'].PHP_EOL;
690                        $advanced_report .= 'Rule: '.$report_linkroot.'/?page=bouncerule&amp;id='.$rule['id'].PHP_EOL;
691                        addUserHistory($userdata['email'], $GLOBALS['I18N']->get('Auto Unsubscribed'),
692                            $GLOBALS['I18N']->get('User auto unsubscribed for').' '.$GLOBALS['I18N']->get('bounce rule').' '.$rule['id']);
693                        addSubscriberStatistics('auto blacklist', 1);
694                        deleteBounce($row['bounce']);
695                        break;
696                    case 'deletebounce':
697                        deleteBounce($row['bounce']);
698            if (REPORT_DELETED_BOUNCES == 1) {
699                $advanced_report .= 'Deleted bounce ' . $userdata['email'] . ' --> Bounce deleted by bounce rule ' . $rule['id'] . PHP_EOL;
700            }
701                        break;
702                }
703
704                ++$matched;
705            } else {
706                ++$notmatched;
707            }
708        }
709    }
710    outputProcessBounce($matched.' '.$GLOBALS['I18N']->get('bounces processed by advanced processing'));
711    outputProcessBounce($notmatched.' '.$GLOBALS['I18N']->get('bounces were not matched by advanced processing rules'));
712}
713
714// have a look who should be flagged as unconfirmed
715outputProcessBounce($GLOBALS['I18N']->get('Identifying consecutive bounces'));
716
717// we only need users who are confirmed at the moment
718$userid_req = Sql_query(sprintf('select distinct umb.user from %s umb, %s u
719  where u.id = umb.user and u.confirmed and !u.blacklisted',
720    $tables['user_message_bounce'],
721    $tables['user']
722));
723$total = Sql_Affected_Rows();
724if (!$total) {
725    outputProcessBounce($GLOBALS['I18N']->get('Nothing to do'));
726}
727
728$usercnt = 0;
729$unsubscribed_users = '';
730while ($user = Sql_Fetch_Row($userid_req)) {
731    keepLock($process_id);
732    set_time_limit(600);
733    //$msg_req = Sql_Query(sprintf('select * from
734    //%s um left join %s umb on (um.messageid = umb.message and userid = user)
735    //where userid = %d and um.status = "sent"
736    //order by entered desc',
737    //$tables["usermessage"],$tables["user_message_bounce"],
738    //$user[0]));
739
740    //# 17361 - update of the above query, to include the bounce table and to exclude duplicate bounces
741    $msg_req = Sql_Query(sprintf('select umb.*,um.*,b.status,b.comment from %s um left join %s umb on (um.messageid = umb.message and userid = user)
742    left join %s b on umb.bounce = b.id
743    where userid = %d and um.status = "sent"
744    order by entered desc',
745        $tables['usermessage'], $tables['user_message_bounce'], $tables['bounce'],
746        $user[0]));
747
748    /*  $cnt = 0;
749      $alive = 1;$removed = 0;
750      while ($alive && !$removed && $bounce = Sql_Fetch_Array($msg_req)) {
751        $alive = checkLock($process_id);
752        if ($alive)
753          keepLock($process_id);
754        else
755          bounceProcessError($GLOBALS['I18N']->get("Process Killed by other process"));
756        if (sprintf('%d',$bounce["bounce"]) == $bounce["bounce"]) {
757          $cnt++;
758          if ($cnt >= $bounce_unsubscribe_threshold) {
759            $removed = 1;
760            outputProcessBounce(sprintf('unsubscribing %d -> %d bounces',$user[0],$cnt));
761            $userurl = PageLink2("user&amp;id=$user[0]",$user[0]);
762            logEvent($GLOBALS['I18N']->get("User")." $userurl ".$GLOBALS['I18N']->get("has consecutive bounces")." ($cnt) ".$GLOBALS['I18N']->get("over threshold, user marked unconfirmed"));
763            $emailreq = Sql_Fetch_Row_Query("select email from {$tables["user"]} where id = $user[0]");
764            addUserHistory($emailreq[0],$GLOBALS['I18N']->get("Auto Unsubscribed"),$GLOBALS['I18N']->get("User auto unsubscribed for")." $cnt ".$GLOBALS['I18N']->get("consecutive bounces"));
765            Sql_Query(sprintf('update %s set confirmed = 0 where id = %d',$tables["user"],$user[0]));
766            addSubscriberStatistics('auto unsubscribe',1);
767            $email_req = Sql_Fetch_Row_Query(sprintf('select email from %s where id = %d',$tables["user"],$user[0]));
768            $unsubscribed_users .= $email_req[0] . " [$user[0]] ($cnt)\n";
769          }
770        } elseif ($bounce["bounce"] == "") {
771          $cnt = 0;
772        }
773      }*/
774    //$alive = 1;$removed = 0; DT 051105
775    $cnt = 0;
776    $alive = 1;
777    $removed = $msgokay = $unconfirmed = $unsubscribed = 0;
778    //while ($alive && !$removed && $bounce = Sql_Fetch_Array($msg_req)) { DT 051105
779    while ($alive && !$removed && !$msgokay && $bounce = Sql_Fetch_Array($msg_req)) {
780        $alive = checkLock($process_id);
781        if ($alive) {
782            keepLock($process_id);
783        } else {
784            bounceProcessError('Process Killed by other process');
785        }
786
787        if (stripos($bounce['status'], 'duplicate') === false && stripos($bounce['comment'], 'duplicate') === false) {
788            if (sprintf('%d', $bounce['bounce']) == $bounce['bounce']) {
789                ++$cnt;
790                if ($cnt >= $bounce_unsubscribe_threshold) {
791                    if (!$unsubscribed) {
792                        outputProcessBounce(sprintf('unsubscribing %d -> %d bounces', $user[0], $cnt));
793                        $userurl = PageLink2("user&amp;id=$user[0]", $user[0]);
794                        logEvent(s('User (url:%s) has consecutive bounces (%d) over threshold (%d), user marked unconfirmed',
795                            $userurl, $cnt, $bounce_unsubscribe_threshold));
796                        $emailreq = Sql_Fetch_Row_Query("select email from {$tables['user']} where id = $user[0]");
797                        addUserHistory($emailreq[0], s('Auto Unconfirmed'),
798                            s('Subscriber auto unconfirmed for %d consecutive bounces', $cnt));
799                        Sql_Query(sprintf('update %s set confirmed = 0 where id = %d', $tables['user'], $user[0]));
800                        $email_req = Sql_Fetch_Row_Query(sprintf('select email from %s where id = %d', $tables['user'],
801                            $user[0]));
802                        $unsubscribed_users .= $email_req[0]."\t\t($cnt)\t\t".$GLOBALS['scheme'].'://'.getConfig('website').$GLOBALS['adminpages'].'/?page=user&amp;id='.$user[0].PHP_EOL;
803                        $unsubscribed = 1;
804                    }
805                    if (BLACKLIST_EMAIL_ON_BOUNCE && $cnt >= BLACKLIST_EMAIL_ON_BOUNCE) {
806                        $removed = 1;
807                        //0012262: blacklist email when email bounces
808                        cl_output(s('%d consecutive bounces, threshold reached, blacklisting subscriber', $cnt));
809                        addEmailToBlackList($emailreq[0], s('%d consecutive bounces, threshold reached', $cnt));
810                    }
811                }
812            } elseif ($bounce['bounce'] == '') {
813                //$cnt = 0; DT 051105
814                $cnt = 0;
815                $msgokay = 1; //DT 051105 - escaping loop if message received okay
816            }
817        }
818    }
819    if ($usercnt % 5 == 0) {
820        //    outputProcessBounce($GLOBALS['I18N']->get("Identifying consecutive bounces"));
821        cl_progress(s('processed %d out of %d subscribers', $usercnt, $total), 1);
822    }
823    ++$usercnt;
824    flush();
825}
826
827//outputProcessBounce($GLOBALS['I18N']->get("Identifying consecutive bounces"));
828outputProcessBounce(PHP_EOL.s('total of %d subscribers processed', $total).'                            ');
829
830$report = '';
831
832if ($advanced_report) {
833    $report .= $GLOBALS['I18N']->get('Report of advanced bounce processing:')."\n$advanced_report\n";
834}
835if ($unsubscribed_users) {
836    $report .= PHP_EOL.$GLOBALS['I18N']->get('Below are users who have been marked unconfirmed. The number in () is the number of consecutive bounces.').PHP_EOL;
837    $report .= "\n$unsubscribed_users";
838}
839if ($report) {
840    $report = $GLOBALS['I18N']->get('Report:')."\n$download_report\n".$report;
841}
842// shutdown will take care of reporting
843//finish("info",$report);
844
845// IMAP errors following when Notices are on are a PHP bug
846# http://bugs.php.net/bug.php?id=7207
847