Skip to content

Commit 92ff09d

Browse files
committed
AdvancedBounceRulesProcessor
1 parent 538ffae commit 92ff09d

File tree

3 files changed

+142
-122
lines changed

3 files changed

+142
-122
lines changed

config/services/processor.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ services:
99

1010
PhpList\Core\Domain\Messaging\Service\Processor\MboxBounceProcessor:
1111
tags: ['phplist.bounce_protocol_processor']
12+
13+
PhpList\Core\Domain\Messaging\Service\Processor\AdvancedBounceRulesProcessor: ~

src/Domain/Messaging/Command/ProcessBouncesCommand.php

Lines changed: 5 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
use PhpList\Core\Domain\Messaging\Model\UserMessageBounce;
1212
use PhpList\Core\Domain\Messaging\Service\BounceProcessingService;
1313
use PhpList\Core\Domain\Messaging\Service\Processor\BounceProtocolProcessor;
14+
use PhpList\Core\Domain\Messaging\Service\Processor\AdvancedBounceRulesProcessor;
1415
use PhpList\Core\Domain\Messaging\Service\LockService;
1516
use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager;
16-
use PhpList\Core\Domain\Messaging\Service\Manager\BounceRuleManager;
1717
use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository;
1818
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager;
1919
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager;
@@ -49,7 +49,6 @@ protected function configure(): void
4949

5050
public function __construct(
5151
private readonly BounceManager $bounceManager,
52-
private readonly BounceRuleManager $ruleManager,
5352
private readonly LockService $lockService,
5453
private readonly LoggerInterface $logger,
5554
private readonly SubscriberManager $subscriberManager,
@@ -58,6 +57,7 @@ public function __construct(
5857
private readonly BounceProcessingService $processingService,
5958
/** @var iterable<BounceProtocolProcessor> */
6059
private readonly iterable $protocolProcessors,
60+
private readonly AdvancedBounceRulesProcessor $advancedRulesProcessor,
6161
) {
6262
parent::__construct();
6363
}
@@ -97,28 +97,23 @@ protected function execute(InputInterface $input, OutputInterface $output): int
9797

9898
if ($processor === null) {
9999
$io->error('Unsupported protocol: '.$protocol);
100+
100101
return Command::FAILURE;
101102
}
102103

103104
$downloadReport .= $processor->process($input, $io);
104105

105-
// Reprocess unidentified bounces (status = "unidentified bounce")
106106
$this->reprocessUnidentified($io);
107107

108-
// Advanced bounce rules
109-
$this->processAdvancedRules($io, (int)$input->getOption('rules-batch-size'));
108+
$this->advancedRulesProcessor->process($io, (int)$input->getOption('rules-batch-size'));
110109

111-
// Identify and unconfirm users with consecutive bounces
112110
$this->handleConsecutiveBounces(
113111
$io,
114112
(int)$input->getOption('unsubscribe-threshold'),
115113
(int)$input->getOption('blacklist-threshold')
116114
);
117115

118-
// Summarize and report (email or log)
119-
$this->logger->info('Bounce processing completed', [
120-
'downloadReport' => $downloadReport,
121-
]);
116+
$this->logger->info('Bounce processing completed', ['downloadReport' => $downloadReport]);
122117

123118
$io->success('Bounce processing completed.');
124119

@@ -159,117 +154,6 @@ private function reprocessUnidentified(SymfonyStyle $io): void
159154
$io->writeln(sprintf('%d bounces were re-processed and %d bounces were re-identified', $reparsed, $reidentified));
160155
}
161156

162-
private function processAdvancedRules(SymfonyStyle $io, int $batchSize): void
163-
{
164-
$io->section('Processing bounces based on active bounce rules');
165-
$rules = $this->ruleManager->loadActiveRules();
166-
if (!$rules) {
167-
$io->writeln('No active rules');
168-
return;
169-
}
170-
171-
$total = $this->bounceManager->getUserMessageBounceCount();
172-
$fromId = 0;
173-
$matched = 0;
174-
$notmatched = 0;
175-
$counter = 0;
176-
177-
while ($counter < $total) {
178-
$batch = $this->bounceManager->fetchUserMessageBounceBatch($fromId, $batchSize);
179-
$counter += count($batch);
180-
$io->writeln(sprintf('processed %d out of %d bounces for advanced bounce rules', min($counter, $total), $total));
181-
foreach ($batch as $row) {
182-
$fromId = $row['umb']->getId();
183-
// $row has: bounce(header,data,id), umb(user,message,bounce)
184-
$text = $row['bounce']->getHeader()."\n\n".$row['bounce']->getData();
185-
$rule = $this->ruleManager->matchBounceRules($text, $rules);
186-
$userId = (int)$row['umb']->getUserId();
187-
$bounce = $row['bounce'];
188-
$userdata = $userId ? $this->subscriberManager->getSubscriberById($userId) : null;
189-
$confirmed = $userdata?->isConfirmed() ?? false;
190-
$blacklisted = $userdata?->isBlacklisted() ?? false;
191-
192-
if ($rule) {
193-
$this->ruleManager->incrementCount($rule);
194-
$rule->setCount($rule->getCount() + 1);
195-
$this->ruleManager->linkRuleToBounce($rule, $bounce);
196-
197-
switch ($rule->getAction()) {
198-
case 'deleteuser':
199-
if ($userdata) {
200-
$this->logger->info('User deleted by bounce rule', ['user' => $userdata->getEmail(), 'rule' => $rule->getId()]);
201-
$this->subscriberManager->deleteSubscriber($userdata);
202-
}
203-
break;
204-
case 'unconfirmuser':
205-
if ($userdata && $confirmed) {
206-
$this->subscriberManager->markUnconfirmed($userId);
207-
$this->subscriberHistoryManager->addHistory($userdata, 'Auto Unconfirmed', 'Subscriber auto unconfirmed for bounce rule '.$rule->getId());
208-
}
209-
break;
210-
case 'deleteuserandbounce':
211-
if ($userdata) {
212-
$this->subscriberManager->deleteSubscriber($userdata);
213-
}
214-
$this->bounceManager->delete($bounce);
215-
break;
216-
case 'unconfirmuseranddeletebounce':
217-
if ($userdata && $confirmed) {
218-
$this->subscriberManager->markUnconfirmed($userId);
219-
$this->subscriberHistoryManager->addHistory($userdata, 'Auto unconfirmed', 'Subscriber auto unconfirmed for bounce rule '.$rule->getId());
220-
}
221-
$this->bounceManager->delete($bounce);
222-
break;
223-
case 'decreasecountconfirmuseranddeletebounce':
224-
if ($userdata) {
225-
$this->subscriberManager->decrementBounceCount($userdata);
226-
if (!$confirmed) {
227-
$this->subscriberManager->markConfirmed($userId);
228-
$this->subscriberHistoryManager->addHistory($userdata, 'Auto confirmed', 'Subscriber auto confirmed for bounce rule '.$rule->getId());
229-
}
230-
}
231-
$this->bounceManager->delete($bounce);
232-
break;
233-
case 'blacklistuser':
234-
if ($userdata && !$blacklisted) {
235-
$this->subscriberManager->blacklist($userdata, 'Subscriber auto blacklisted by bounce rule '.$rule->getId());
236-
$this->subscriberHistoryManager->addHistory($userdata, 'Auto Unsubscribed', 'User auto unsubscribed for bounce rule '.$rule->getId());
237-
}
238-
break;
239-
case 'blacklistuseranddeletebounce':
240-
if ($userdata && !$blacklisted) {
241-
$this->subscriberManager->blacklist($userdata, 'Subscriber auto blacklisted by bounce rule '.$rule->getId());
242-
$this->subscriberHistoryManager->addHistory($userdata, 'Auto Unsubscribed', 'User auto unsubscribed for bounce rule '.$rule->getId());
243-
}
244-
$this->bounceManager->delete($bounce);
245-
break;
246-
case 'blacklistemail':
247-
if ($userdata) {
248-
$this->subscriberManager->blacklist($userdata, 'Email address auto blacklisted by bounce rule '.$rule->getId());
249-
$this->subscriberHistoryManager->addHistory($userdata, 'Auto Unsubscribed', 'email auto unsubscribed for bounce rule '.$rule->getId());
250-
}
251-
break;
252-
case 'blacklistemailanddeletebounce':
253-
if ($userdata) {
254-
$this->subscriberManager->blacklist($userdata, 'Email address auto blacklisted by bounce rule '.$rule->getId());
255-
$this->subscriberHistoryManager->addHistory($userdata, 'Auto Unsubscribed', 'User auto unsubscribed for bounce rule '.$rule->getId());
256-
}
257-
$this->bounceManager->delete($bounce);
258-
break;
259-
case 'deletebounce':
260-
$this->bounceManager->delete($bounce);
261-
break;
262-
}
263-
$matched++;
264-
} else {
265-
$notmatched++;
266-
}
267-
}
268-
}
269-
$io->writeln(sprintf('%d bounces processed by advanced processing', $matched));
270-
$io->writeln(sprintf('%d bounces were not matched by advanced processing rules', $notmatched));
271-
}
272-
273157
private function handleConsecutiveBounces(SymfonyStyle $io, int $unsubscribeThreshold, int $blacklistThreshold): void
274158
{
275159
$io->section('Identifying consecutive bounces');
@@ -324,5 +208,4 @@ private function handleConsecutiveBounces(SymfonyStyle $io, int $unsubscribeThre
324208
}
325209
$io->writeln(sprintf('total of %d subscribers processed', $total));
326210
}
327-
328211
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Messaging\Service\Processor;
6+
7+
use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager;
8+
use PhpList\Core\Domain\Messaging\Service\Manager\BounceRuleManager;
9+
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager;
10+
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager;
11+
use Psr\Log\LoggerInterface;
12+
use Symfony\Component\Console\Style\SymfonyStyle;
13+
14+
class AdvancedBounceRulesProcessor
15+
{
16+
public function __construct(
17+
private readonly BounceManager $bounceManager,
18+
private readonly BounceRuleManager $ruleManager,
19+
private readonly LoggerInterface $logger,
20+
private readonly SubscriberManager $subscriberManager,
21+
private readonly SubscriberHistoryManager $subscriberHistoryManager,
22+
) {
23+
}
24+
25+
public function process(SymfonyStyle $io, int $batchSize): void
26+
{
27+
$io->section('Processing bounces based on active bounce rules');
28+
$rules = $this->ruleManager->loadActiveRules();
29+
if (!$rules) {
30+
$io->writeln('No active rules');
31+
return;
32+
}
33+
34+
$total = $this->bounceManager->getUserMessageBounceCount();
35+
$fromId = 0;
36+
$matched = 0;
37+
$notmatched = 0;
38+
$counter = 0;
39+
40+
while ($counter < $total) {
41+
$batch = $this->bounceManager->fetchUserMessageBounceBatch($fromId, $batchSize);
42+
$counter += count($batch);
43+
$io->writeln(sprintf('processed %d out of %d bounces for advanced bounce rules', min($counter, $total), $total));
44+
foreach ($batch as $row) {
45+
$fromId = $row['umb']->getId();
46+
// $row has: bounce(header,data,id), umb(user,message,bounce)
47+
$text = $row['bounce']->getHeader()."\n\n".$row['bounce']->getData();
48+
$rule = $this->ruleManager->matchBounceRules($text, $rules);
49+
$userId = (int)$row['umb']->getUserId();
50+
$bounce = $row['bounce'];
51+
$userdata = $userId ? $this->subscriberManager->getSubscriberById($userId) : null;
52+
$confirmed = $userdata?->isConfirmed() ?? false;
53+
$blacklisted = $userdata?->isBlacklisted() ?? false;
54+
55+
if ($rule) {
56+
$this->ruleManager->incrementCount($rule);
57+
$rule->setCount($rule->getCount() + 1);
58+
$this->ruleManager->linkRuleToBounce($rule, $bounce);
59+
60+
switch ($rule->getAction()) {
61+
case 'deleteuser':
62+
if ($userdata) {
63+
$this->logger->info('User deleted by bounce rule', ['user' => $userdata->getEmail(), 'rule' => $rule->getId()]);
64+
$this->subscriberManager->deleteSubscriber($userdata);
65+
}
66+
break;
67+
case 'unconfirmuser':
68+
if ($userdata && $confirmed) {
69+
$this->subscriberManager->markUnconfirmed($userId);
70+
$this->subscriberHistoryManager->addHistory($userdata, 'Auto Unconfirmed', 'Subscriber auto unconfirmed for bounce rule '.$rule->getId());
71+
}
72+
break;
73+
case 'deleteuserandbounce':
74+
if ($userdata) {
75+
$this->subscriberManager->deleteSubscriber($userdata);
76+
}
77+
$this->bounceManager->delete($bounce);
78+
break;
79+
case 'unconfirmuseranddeletebounce':
80+
if ($userdata && $confirmed) {
81+
$this->subscriberManager->markUnconfirmed($userId);
82+
$this->subscriberHistoryManager->addHistory($userdata, 'Auto unconfirmed', 'Subscriber auto unconfirmed for bounce rule '.$rule->getId());
83+
}
84+
$this->bounceManager->delete($bounce);
85+
break;
86+
case 'decreasecountconfirmuseranddeletebounce':
87+
if ($userdata) {
88+
$this->subscriberManager->decrementBounceCount($userdata);
89+
if (!$confirmed) {
90+
$this->subscriberManager->markConfirmed($userId);
91+
$this->subscriberHistoryManager->addHistory($userdata, 'Auto confirmed', 'Subscriber auto confirmed for bounce rule '.$rule->getId());
92+
}
93+
}
94+
$this->bounceManager->delete($bounce);
95+
break;
96+
case 'blacklistuser':
97+
if ($userdata && !$blacklisted) {
98+
$this->subscriberManager->blacklist($userdata, 'Subscriber auto blacklisted by bounce rule '.$rule->getId());
99+
$this->subscriberHistoryManager->addHistory($userdata, 'Auto Unsubscribed', 'User auto unsubscribed for bounce rule '.$rule->getId());
100+
}
101+
break;
102+
case 'blacklistuseranddeletebounce':
103+
if ($userdata && !$blacklisted) {
104+
$this->subscriberManager->blacklist($userdata, 'Subscriber auto blacklisted by bounce rule '.$rule->getId());
105+
$this->subscriberHistoryManager->addHistory($userdata, 'Auto Unsubscribed', 'User auto unsubscribed for bounce rule '.$rule->getId());
106+
}
107+
$this->bounceManager->delete($bounce);
108+
break;
109+
case 'blacklistemail':
110+
if ($userdata) {
111+
$this->subscriberManager->blacklist($userdata, 'Email address auto blacklisted by bounce rule '.$rule->getId());
112+
$this->subscriberHistoryManager->addHistory($userdata, 'Auto Unsubscribed', 'email auto unsubscribed for bounce rule '.$rule->getId());
113+
}
114+
break;
115+
case 'blacklistemailanddeletebounce':
116+
if ($userdata) {
117+
$this->subscriberManager->blacklist($userdata, 'Email address auto blacklisted by bounce rule '.$rule->getId());
118+
$this->subscriberHistoryManager->addHistory($userdata, 'Auto Unsubscribed', 'User auto unsubscribed for bounce rule '.$rule->getId());
119+
}
120+
$this->bounceManager->delete($bounce);
121+
break;
122+
case 'deletebounce':
123+
$this->bounceManager->delete($bounce);
124+
break;
125+
}
126+
$matched++;
127+
} else {
128+
$notmatched++;
129+
}
130+
}
131+
}
132+
$io->writeln(sprintf('%d bounces processed by advanced processing', $matched));
133+
$io->writeln(sprintf('%d bounces were not matched by advanced processing rules', $notmatched));
134+
}
135+
}

0 commit comments

Comments
 (0)