Skip to content

Commit 7630187

Browse files
committed
ProcessBouncesCommand all methods
1 parent 981ca1e commit 7630187

File tree

4 files changed

+94
-32
lines changed

4 files changed

+94
-32
lines changed

src/Domain/Messaging/Command/ProcessBouncesCommand.php

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use DateTimeImmutable;
88
use Exception;
99
use PhpList\Core\Domain\Messaging\Model\Bounce;
10+
use PhpList\Core\Domain\Messaging\Model\UserMessage;
11+
use PhpList\Core\Domain\Messaging\Model\UserMessageBounce;
1012
use PhpList\Core\Domain\Messaging\Repository\MessageRepository;
1113
use PhpList\Core\Domain\Messaging\Service\LockService;
1214
use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager;
@@ -35,8 +37,8 @@ protected function configure(): void
3537
->addOption('password', null, InputOption::VALUE_OPTIONAL, 'Mailbox password')
3638
->addOption('mailbox', null, InputOption::VALUE_OPTIONAL, 'Mailbox name(s) for POP (comma separated) or mbox file path', 'INBOX')
3739
->addOption('maximum', null, InputOption::VALUE_OPTIONAL, 'Max messages to process per run', '1000')
38-
->addOption('purge', null, InputOption::VALUE_NONE, 'Delete processed messages from mailbox')
39-
->addOption('purge-unprocessed', null, InputOption::VALUE_NONE, 'Delete unprocessed messages from mailbox')
40+
->addOption('purge', null, InputOption::VALUE_NONE, 'Delete/remove processed messages from mailbox')
41+
->addOption('purge-unprocessed', null, InputOption::VALUE_NONE, 'Delete/remove unprocessed messages from mailbox')
4042
->addOption('rules-batch-size', null, InputOption::VALUE_OPTIONAL, 'Advanced rules batch size', '1000')
4143
->addOption('unsubscribe-threshold', null, InputOption::VALUE_OPTIONAL, 'Consecutive bounces threshold to unconfirm user', '3')
4244
->addOption('blacklist-threshold', null, InputOption::VALUE_OPTIONAL, 'Consecutive bounces threshold to blacklist email (0 to disable)', '0')
@@ -53,6 +55,7 @@ public function __construct(
5355
private readonly LoggerInterface $logger,
5456
private readonly SubscriberManager $subscriberManager,
5557
private readonly SubscriberHistoryManager $subscriberHistoryManager,
58+
private readonly SubscriberRepository $subscriberRepository,
5659
) {
5760
parent::__construct();
5861
}
@@ -441,45 +444,53 @@ private function processAdvancedRules(SymfonyStyle $io, int $batchSize): void
441444
$io->writeln(sprintf('%d bounces were not matched by advanced processing rules', $notmatched));
442445
}
443446

444-
// --- Consecutive bounces logic (mirrors final section) ---
445447
private function handleConsecutiveBounces(SymfonyStyle $io, int $unsubscribeThreshold, int $blacklistThreshold): void
446448
{
447449
$io->section('Identifying consecutive bounces');
448-
$userIds = $this->bounces->distinctUsersWithBouncesConfirmedNotBlacklisted();
449-
$total = \count($userIds);
450+
$users = $this->subscriberRepository->distinctUsersWithBouncesConfirmedNotBlacklisted();
451+
$total = count($users);
450452
if ($total === 0) {
451453
$io->writeln('Nothing to do');
452454
return;
453455
}
454456
$usercnt = 0;
455-
foreach ($userIds as $userId) {
457+
foreach ($users as $user) {
456458
$usercnt++;
457-
$history = $this->bounces->userMessageHistoryWithBounces($userId); // ordered desc, includes bounce status/comment
459+
$history = $this->bounceManager->getUserMessageHistoryWithBounces($user);
458460
$cnt = 0; $removed = false; $msgokay = false; $unsubscribed = false;
459461
foreach ($history as $bounce) {
460-
if (stripos($bounce->status ?? '', 'duplicate') === false && stripos($bounce->comment ?? '', 'duplicate') === false) {
461-
if ($bounce->bounceId) { // there is a bounce
462+
/** @var $bounce array{um: UserMessage, umb: UserMessageBounce|null, b: Bounce|null} */
463+
if (
464+
stripos($bounce['b']->getStatus() ?? '', 'duplicate') === false
465+
&& stripos($bounce['b']->getComment() ?? '', 'duplicate') === false
466+
) {
467+
if ($bounce['b']->getId()) {
462468
$cnt++;
463469
if ($cnt >= $unsubscribeThreshold) {
464470
if (!$unsubscribed) {
465-
$email = $this->users->emailById($userId);
466-
$this->users->markUnconfirmed($userId);
467-
$this->users->addHistory($email, 'Auto Unconfirmed', sprintf('Subscriber auto unconfirmed for %d consecutive bounces', $cnt));
471+
$this->subscriberManager->markUnconfirmed($user->getId());
472+
$this->subscriberHistoryManager->addHistory(
473+
subscriber: $user,
474+
message: 'Auto Unconfirmed',
475+
details: sprintf('Subscriber auto unconfirmed for %d consecutive bounces', $cnt)
476+
);
468477
$unsubscribed = true;
469478
}
470479
if ($blacklistThreshold > 0 && $cnt >= $blacklistThreshold) {
471-
$email = $this->users->emailById($userId);
472-
$this->users->blacklistByEmail($email, sprintf('%d consecutive bounces, threshold reached', $cnt));
480+
$this->subscriberManager->blacklist(
481+
subscriber: $user,
482+
reason: sprintf('%d consecutive bounces, threshold reached', $cnt)
483+
);
473484
$removed = true;
474485
}
475486
}
476-
} else { // empty bounce means message received ok
477-
$cnt = 0;
478-
$msgokay = true;
487+
} else {
479488
break;
480489
}
481490
}
482-
if ($removed || $msgokay) { break; }
491+
if ($removed || $msgokay) {
492+
break;
493+
}
483494
}
484495
if ($usercnt % 5 === 0) {
485496
$io->writeln(sprintf('processed %d out of %d subscribers', $usercnt, $total));
@@ -488,25 +499,17 @@ private function handleConsecutiveBounces(SymfonyStyle $io, int $unsubscribeThre
488499
$io->writeln(sprintf('total of %d subscribers processed', $total));
489500
}
490501

491-
// --- Helpers: decoding and parsing ---
492502
private function decodeBody(string $header, string $body): string
493503
{
494504
$transferEncoding = '';
495505
if (preg_match('/Content-Transfer-Encoding: ([\w-]+)/i', $header, $regs)) {
496506
$transferEncoding = strtolower($regs[1]);
497507
}
498-
$decoded = null;
499-
switch ($transferEncoding) {
500-
case 'quoted-printable':
501-
$decoded = quoted_printable_decode($body);
502-
break;
503-
case 'base64':
504-
$decoded = base64_decode($body) ?: '';
505-
break;
506-
default:
507-
$decoded = $body;
508-
}
509-
return $decoded;
508+
return match ($transferEncoding) {
509+
'quoted-printable' => quoted_printable_decode($body),
510+
'base64' => base64_decode($body) ?: '',
511+
default => $body,
512+
};
510513
}
511514

512515
private function findMessageId(string $text): string|int|null

src/Domain/Messaging/Repository/UserMessageBounceRepository.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
use PhpList\Core\Domain\Common\Repository\CursorPaginationTrait;
99
use PhpList\Core\Domain\Common\Repository\Interfaces\PaginatableRepositoryInterface;
1010
use PhpList\Core\Domain\Messaging\Model\Bounce;
11+
use PhpList\Core\Domain\Messaging\Model\UserMessage;
1112
use PhpList\Core\Domain\Messaging\Model\UserMessageBounce;
13+
use PhpList\Core\Domain\Subscription\Model\Subscriber;
1214

1315
class UserMessageBounceRepository extends AbstractRepository implements PaginatableRepositoryInterface
1416
{
@@ -54,4 +56,38 @@ public function getPaginatedWithJoinNoRelation(int $fromId, int $limit): array
5456
->getQuery()
5557
->getResult();
5658
}
59+
60+
/**
61+
* @return array<int, array{
62+
* um: UserMessage,
63+
* umb: UserMessageBounce|null,
64+
* b: Bounce|null
65+
* }>
66+
*/
67+
public function getUserMessageHistoryWithBounces(Subscriber $subscriber): array
68+
{
69+
$qb = $this->getEntityManager()
70+
->createQueryBuilder()
71+
->select('um', 'umb', 'b')
72+
->from(UserMessage::class, 'um')
73+
->leftJoin(
74+
join: UserMessageBounce::class,
75+
alias: 'umb',
76+
conditionType: 'WITH',
77+
condition: 'umb.messageId = IDENTITY(um.message) AND umb.userId = IDENTITY(um.user)'
78+
)
79+
->leftJoin(
80+
join: Bounce::class,
81+
alias: 'b',
82+
conditionType: 'WITH',
83+
condition: 'b.id = umb.bounceId'
84+
)
85+
->where('um.user = :userId')
86+
->andWhere('um.status = :status')
87+
->setParameter('userId', $subscriber->getId())
88+
->setParameter('status', 'sent')
89+
->orderBy('um.entered', 'DESC');
90+
91+
return $qb->getQuery()->getResult();
92+
}
5793
}

src/Domain/Messaging/Service/Manager/BounceManager.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
use DateTime;
88
use DateTimeImmutable;
99
use PhpList\Core\Domain\Messaging\Model\Bounce;
10+
use PhpList\Core\Domain\Messaging\Model\UserMessage;
1011
use PhpList\Core\Domain\Messaging\Model\UserMessageBounce;
1112
use PhpList\Core\Domain\Messaging\Repository\BounceRepository;
1213
use PhpList\Core\Domain\Messaging\Repository\UserMessageBounceRepository;
14+
use PhpList\Core\Domain\Subscription\Model\Subscriber;
1315

1416
class BounceManager
1517
{
@@ -102,8 +104,17 @@ public function getUserMessageBounceCount(): int
102104

103105
/**
104106
* @return array<int, array{umb: UserMessageBounce, bounce: Bounce}>
105-
*/ public function fetchUserMessageBounceBatch(int $fromId, int $batchSize): array
107+
*/
108+
public function fetchUserMessageBounceBatch(int $fromId, int $batchSize): array
106109
{
107110
return $this->userMessageBounceRepository->getPaginatedWithJoinNoRelation($fromId, $batchSize);
108111
}
112+
113+
/**
114+
* @return array<int, array{um: UserMessage, umb: UserMessageBounce|null, b: Bounce|null}>
115+
*/
116+
public function getUserMessageHistoryWithBounces(Subscriber $subscriber): array
117+
{
118+
return $this->userMessageBounceRepository->getUserMessageHistoryWithBounces($subscriber);
119+
}
109120
}

src/Domain/Subscription/Repository/SubscriberRepository.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,16 @@ public function incrementBounceCount(int $subscriberId): void
152152
->getQuery()
153153
->execute();
154154
}
155+
156+
/** @return Subscriber[] */
157+
public function distinctUsersWithBouncesConfirmedNotBlacklisted(): array
158+
{
159+
return $this->createQueryBuilder('s')
160+
->select('s.id')
161+
->where('s.bounceCount > 0')
162+
->andWhere('s.confirmed = 1')
163+
->andWhere('s.blacklisted = 0')
164+
->getQuery()
165+
->getScalarResult();
166+
}
155167
}

0 commit comments

Comments
 (0)