Skip to content

Commit 34ec99c

Browse files
committed
BounceDataProcessor
1 parent 0f86880 commit 34ec99c

File tree

9 files changed

+406
-161
lines changed

9 files changed

+406
-161
lines changed

config/services/services.yml

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,24 +44,15 @@ services:
4444
autowire: true
4545
autoconfigure: true
4646

47-
PhpList\Core\Domain\Messaging\Service\ConsecutiveBounceHandler: ~
48-
49-
PhpList\Core\Domain\Common\Mail\MailReaderInterface: '@PhpList\Core\Domain\Common\Mail\NativeImapMailReader'
47+
PhpList\Core\Domain\Messaging\Service\ConsecutiveBounceHandler:
48+
autowire: true
49+
autoconfigure: true
50+
arguments:
51+
$unsubscribeThreshold: '%app.unsubscribe_threshold%'
52+
$blacklistThreshold: '%app.blacklist_threshold%'
5053

5154
Webklex\PHPIMAP\ClientManager: ~
5255

53-
PhpList\Core\Domain\Common\Mail\WebklexMailReader:
54-
arguments:
55-
$cm: '@Webklex\PHPIMAP\ClientManager'
56-
$config:
57-
host: '%imap_bounce.host%'
58-
port: '%imap_bounce.port%'
59-
encryption: '%imap_bounce.encryption%'
60-
validate_cert: true
61-
username: '%imap_bounce.email%'
62-
password: '%imap_bounce.password%'
63-
protocol: 'imap'
64-
6556
PhpList\Core\Domain\Common\Mail\NativeImapMailReader:
6657
arguments:
6758
$mailbox: '%env(IMAP_MAILBOX)%' # e.g. "{imap.example.com:993/imap/ssl}INBOX" or "/var/mail/user"

src/Domain/Common/Mail/MailReaderInterface.php

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/Domain/Common/Mail/NativeImapMailReader.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use IMAP\Connection;
99
use RuntimeException;
1010

11-
class NativeImapMailReader implements MailReaderInterface
11+
class NativeImapMailReader
1212
{
1313
public function open(string $mailbox, ?string $user = null, ?string $password = null, int $options = 0): Connection
1414
{

src/Domain/Common/Mail/WebklexMailReader.php

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/Domain/Messaging/Command/ProcessBouncesCommand.php

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,8 @@ protected function configure(): void
2525
{
2626
$this
2727
->addOption('protocol', null, InputOption::VALUE_REQUIRED, 'Mailbox protocol: pop or mbox', 'pop')
28-
->addOption('host', null, InputOption::VALUE_OPTIONAL, 'POP host (without braces) e.g. mail.example.com')
29-
->addOption('port', null, InputOption::VALUE_OPTIONAL, 'POP port/options, e.g. 110/pop3/notls', '110/pop3/notls')
30-
->addOption('user', null, InputOption::VALUE_OPTIONAL, 'Mailbox username')
31-
->addOption('password', null, InputOption::VALUE_OPTIONAL, 'Mailbox password')
32-
->addOption('mailbox', null, InputOption::VALUE_OPTIONAL, 'Mailbox name(s) for POP (comma separated) or mbox file path', 'INBOX')
33-
->addOption('maximum', null, InputOption::VALUE_OPTIONAL, 'Max messages to process per run', '1000')
34-
->addOption('purge', null, InputOption::VALUE_NONE, 'Delete/remove processed messages from mailbox')
3528
->addOption('purge-unprocessed', null, InputOption::VALUE_NONE, 'Delete/remove unprocessed messages from mailbox')
3629
->addOption('rules-batch-size', null, InputOption::VALUE_OPTIONAL, 'Advanced rules batch size', '1000')
37-
->addOption('unsubscribe-threshold', null, InputOption::VALUE_OPTIONAL, 'Consecutive bounces threshold to unconfirm user', '3')
38-
->addOption('blacklist-threshold', null, InputOption::VALUE_OPTIONAL, 'Consecutive bounces threshold to blacklist email (0 to disable)', '0')
3930
->addOption('test', 't', InputOption::VALUE_NONE, 'Test mode: do not delete from mailbox')
4031
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force run: kill other processes if locked');
4132
}
@@ -74,8 +65,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7465
try {
7566
$io->title('Processing bounces');
7667
$protocol = (string)$input->getOption('protocol');
77-
$unsubscribeThreshold = (int)$input->getOption('unsubscribe-threshold');
78-
$blacklistThreshold = (int)$input->getOption('blacklist-threshold');
7968

8069
$downloadReport = '';
8170

@@ -96,7 +85,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
9685
$downloadReport .= $processor->process($input, $io);
9786
$this->unidentifiedBounceReprocessor->process($io);
9887
$this->advancedRulesProcessor->process($io, (int)$input->getOption('rules-batch-size'));
99-
$this->consecutiveBounceHandler->handle($io, $unsubscribeThreshold, $blacklistThreshold);
88+
$this->consecutiveBounceHandler->handle($io);
10089

10190
$this->logger->info('Bounce processing completed', ['downloadReport' => $downloadReport]);
10291
$io->success('Bounce processing completed.');

src/Domain/Messaging/Service/ConsecutiveBounceHandler.php

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,30 @@
1515

1616
class ConsecutiveBounceHandler
1717
{
18+
private BounceManager $bounceManager;
19+
private SubscriberRepository $subscriberRepository;
20+
private SubscriberManager $subscriberManager;
21+
private SubscriberHistoryManager $subscriberHistoryManager;
22+
private int $unsubscribeThreshold;
23+
private int $blacklistThreshold;
24+
1825
public function __construct(
19-
private readonly BounceManager $bounceManager,
20-
private readonly SubscriberRepository $subscriberRepository,
21-
private readonly SubscriberManager $subscriberManager,
22-
private readonly SubscriberHistoryManager $subscriberHistoryManager,
26+
BounceManager $bounceManager,
27+
SubscriberRepository $subscriberRepository,
28+
SubscriberManager $subscriberManager,
29+
SubscriberHistoryManager $subscriberHistoryManager,
30+
int $unsubscribeThreshold,
31+
int $blacklistThreshold,
2332
) {
33+
$this->bounceManager = $bounceManager;
34+
$this->subscriberRepository = $subscriberRepository;
35+
$this->subscriberManager = $subscriberManager;
36+
$this->subscriberHistoryManager = $subscriberHistoryManager;
37+
$this->unsubscribeThreshold = $unsubscribeThreshold;
38+
$this->blacklistThreshold = $blacklistThreshold;
2439
}
2540

26-
public function handle(SymfonyStyle $io, int $unsubscribeThreshold, int $blacklistThreshold): void
41+
public function handle(SymfonyStyle $io): void
2742
{
2843
$io->section('Identifying consecutive bounces');
2944
$users = $this->subscriberRepository->distinctUsersWithBouncesConfirmedNotBlacklisted();
@@ -45,7 +60,7 @@ public function handle(SymfonyStyle $io, int $unsubscribeThreshold, int $blackli
4560
) {
4661
if ($bounce['b']->getId()) {
4762
$cnt++;
48-
if ($cnt >= $unsubscribeThreshold) {
63+
if ($cnt >= $this->unsubscribeThreshold) {
4964
if (!$unsubscribed) {
5065
$this->subscriberManager->markUnconfirmed($user->getId());
5166
$this->subscriberHistoryManager->addHistory(
@@ -55,7 +70,7 @@ public function handle(SymfonyStyle $io, int $unsubscribeThreshold, int $blackli
5570
);
5671
$unsubscribed = true;
5772
}
58-
if ($blacklistThreshold > 0 && $cnt >= $blacklistThreshold) {
73+
if ($this->blacklistThreshold > 0 && $cnt >= $this->blacklistThreshold) {
5974
$this->subscriberManager->blacklist(
6075
subscriber: $user,
6176
reason: sprintf('%d consecutive bounces, threshold reached', $cnt)

src/Domain/Messaging/Service/NativeBounceProcessingService.php

Lines changed: 16 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,30 @@
44

55
namespace PhpList\Core\Domain\Messaging\Service;
66

7-
use DateTimeImmutable;
8-
use PhpList\Core\Domain\Common\Mail\MailReaderInterface;
9-
use PhpList\Core\Domain\Messaging\Model\Bounce;
10-
use PhpList\Core\Domain\Messaging\Repository\MessageRepository;
7+
use PhpList\Core\Domain\Common\Mail\NativeImapMailReader;
118
use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager;
12-
use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository;
13-
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager;
14-
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager;
15-
use Psr\Log\LoggerInterface;
9+
use PhpList\Core\Domain\Messaging\Service\Processor\BounceDataProcessor;
1610
use RuntimeException;
1711
use Symfony\Component\Console\Style\SymfonyStyle;
1812
use Throwable;
1913

2014
class NativeBounceProcessingService
2115
{
16+
private BounceManager $bounceManager;
17+
private NativeImapMailReader $mailReader;
18+
private MessageParser $messageParser;
19+
private BounceDataProcessor $bounceDataProcessor;
20+
2221
public function __construct(
23-
private readonly BounceManager $bounceManager,
24-
private readonly SubscriberRepository $users,
25-
private readonly MessageRepository $messages,
26-
private readonly LoggerInterface $logger,
27-
private readonly SubscriberManager $subscriberManager,
28-
private readonly SubscriberHistoryManager $subscriberHistoryManager,
29-
private readonly MailReaderInterface $mailReader,
30-
private readonly MessageParser $messageParser,
22+
BounceManager $bounceManager,
23+
NativeImapMailReader $mailReader,
24+
MessageParser $messageParser,
25+
BounceDataProcessor $bounceDataProcessor,
3126
) {
27+
$this->bounceManager = $bounceManager;
28+
$this->mailReader = $mailReader;
29+
$this->messageParser = $messageParser;
30+
$this->bounceDataProcessor = $bounceDataProcessor;
3231
}
3332

3433
public function processMailbox(
@@ -103,82 +102,6 @@ private function processImapBounce($link, int $num, string $header): bool
103102

104103
$bounce = $this->bounceManager->create($bounceDate, $header, $body);
105104

106-
return $this->processBounceData($bounce, $msgId, $userId, $bounceDate);
107-
}
108-
109-
public function processBounceData(Bounce $bounce, ?string $msgId, ?int $userId, DateTimeImmutable $bounceDate): bool
110-
{
111-
$user = $userId ? $this->subscriberManager->getSubscriberById($userId) : null;
112-
113-
if ($msgId === 'systemmessage' && $userId) {
114-
$this->bounceManager->update(
115-
bounce: $bounce,
116-
status: 'bounced system message',
117-
comment: sprintf('%d marked unconfirmed', $userId)
118-
);
119-
$this->bounceManager->linkUserMessageBounce($bounce, $bounceDate, $userId);
120-
$this->subscriberManager->markUnconfirmed($userId);
121-
$this->logger->info('system message bounced, user marked unconfirmed', ['userId' => $userId]);
122-
if ($user) {
123-
$this->subscriberHistoryManager->addHistory(
124-
subscriber: $user,
125-
message: 'Bounced system message',
126-
details: sprintf('User marked unconfirmed. Bounce #%d', $bounce->getId())
127-
);
128-
}
129-
130-
return true;
131-
}
132-
133-
if ($msgId && $userId) {
134-
if (!$this->bounceManager->existsUserMessageBounce($userId, (int)$msgId)) {
135-
$this->bounceManager->linkUserMessageBounce($bounce, $bounceDate, $userId, (int)$msgId);
136-
$this->bounceManager->update(
137-
bounce: $bounce,
138-
status: sprintf('bounced list message %d', $msgId),
139-
comment: sprintf('%d bouncecount increased', $userId)
140-
);
141-
$this->messages->incrementBounceCount((int)$msgId);
142-
$this->users->incrementBounceCount($userId);
143-
} else {
144-
$this->bounceManager->linkUserMessageBounce($bounce, $bounceDate, $userId, (int)$msgId);
145-
$this->bounceManager->update(
146-
bounce: $bounce,
147-
status: sprintf('duplicate bounce for %d', $userId),
148-
comment: sprintf('duplicate bounce for subscriber %d on message %d', $userId, $msgId)
149-
);
150-
}
151-
152-
return true;
153-
}
154-
155-
if ($userId) {
156-
$this->bounceManager->update(
157-
bounce: $bounce,
158-
status: 'bounced unidentified message',
159-
comment: sprintf('%d bouncecount increased', $userId)
160-
);
161-
$this->users->incrementBounceCount($userId);
162-
163-
return true;
164-
}
165-
166-
if ($msgId === 'systemmessage') {
167-
$this->bounceManager->update($bounce, 'bounced system message', 'unknown user');
168-
$this->logger->info('system message bounced, but unknown user');
169-
170-
return true;
171-
}
172-
173-
if ($msgId) {
174-
$this->bounceManager->update($bounce, sprintf('bounced list message %d', $msgId), 'unknown user');
175-
$this->messages->incrementBounceCount((int)$msgId);
176-
177-
return true;
178-
}
179-
180-
$this->bounceManager->update($bounce, 'unidentified bounce', 'not processed');
181-
182-
return false;
105+
return $this->bounceDataProcessor->process($bounce, $msgId, $userId, $bounceDate);
183106
}
184107
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Messaging\Service\Processor;
6+
7+
use DateTimeImmutable;
8+
use PhpList\Core\Domain\Messaging\Model\Bounce;
9+
use PhpList\Core\Domain\Messaging\Repository\MessageRepository;
10+
use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager;
11+
use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository;
12+
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager;
13+
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager;
14+
use Psr\Log\LoggerInterface;
15+
16+
class BounceDataProcessor
17+
{
18+
public function __construct(
19+
private readonly BounceManager $bounceManager,
20+
private readonly SubscriberRepository $users,
21+
private readonly MessageRepository $messages,
22+
private readonly LoggerInterface $logger,
23+
private readonly SubscriberManager $subscriberManager,
24+
private readonly SubscriberHistoryManager $subscriberHistoryManager,
25+
) {
26+
}
27+
28+
public function process(Bounce $bounce, ?string $msgId, ?int $userId, DateTimeImmutable $bounceDate): bool
29+
{
30+
$user = $userId ? $this->subscriberManager->getSubscriberById($userId) : null;
31+
32+
if ($msgId === 'systemmessage' && $userId) {
33+
$this->bounceManager->update(
34+
bounce: $bounce,
35+
status: 'bounced system message',
36+
comment: sprintf('%d marked unconfirmed', $userId)
37+
);
38+
$this->bounceManager->linkUserMessageBounce($bounce, $bounceDate, $userId);
39+
$this->subscriberManager->markUnconfirmed($userId);
40+
$this->logger->info('system message bounced, user marked unconfirmed', ['userId' => $userId]);
41+
if ($user) {
42+
$this->subscriberHistoryManager->addHistory(
43+
subscriber: $user,
44+
message: 'Bounced system message',
45+
details: sprintf('User marked unconfirmed. Bounce #%d', $bounce->getId())
46+
);
47+
}
48+
49+
return true;
50+
}
51+
52+
if ($msgId && $userId) {
53+
if (!$this->bounceManager->existsUserMessageBounce($userId, (int)$msgId)) {
54+
$this->bounceManager->linkUserMessageBounce($bounce, $bounceDate, $userId, (int)$msgId);
55+
$this->bounceManager->update(
56+
bounce: $bounce,
57+
status: sprintf('bounced list message %d', $msgId),
58+
comment: sprintf('%d bouncecount increased', $userId)
59+
);
60+
$this->messages->incrementBounceCount((int)$msgId);
61+
$this->users->incrementBounceCount($userId);
62+
} else {
63+
$this->bounceManager->linkUserMessageBounce($bounce, $bounceDate, $userId, (int)$msgId);
64+
$this->bounceManager->update(
65+
bounce: $bounce,
66+
status: sprintf('duplicate bounce for %d', $userId),
67+
comment: sprintf('duplicate bounce for subscriber %d on message %d', $userId, $msgId)
68+
);
69+
}
70+
71+
return true;
72+
}
73+
74+
if ($userId) {
75+
$this->bounceManager->update(
76+
bounce: $bounce,
77+
status: 'bounced unidentified message',
78+
comment: sprintf('%d bouncecount increased', $userId)
79+
);
80+
$this->users->incrementBounceCount($userId);
81+
82+
return true;
83+
}
84+
85+
if ($msgId === 'systemmessage') {
86+
$this->bounceManager->update($bounce, 'bounced system message', 'unknown user');
87+
$this->logger->info('system message bounced, but unknown user');
88+
89+
return true;
90+
}
91+
92+
if ($msgId) {
93+
$this->bounceManager->update($bounce, sprintf('bounced list message %d', $msgId), 'unknown user');
94+
$this->messages->incrementBounceCount((int)$msgId);
95+
96+
return true;
97+
}
98+
99+
$this->bounceManager->update($bounce, 'unidentified bounce', 'not processed');
100+
101+
return false;
102+
}
103+
}

0 commit comments

Comments
 (0)