Skip to content

Commit fb30e72

Browse files
committed
ConsecutiveBounceHandler
1 parent 0a378e9 commit fb30e72

33 files changed

+379
-86
lines changed

resources/translations/messages.en.xlf

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,13 +313,80 @@
313313
<target>%reparsed% bounces were re-processed and %reidentified% bounces were re-identified</target>
314314
</trans-unit>
315315

316+
<trans-unit id="messaging.consecutive_bounces">
317+
<source>Identifying consecutive bounces</source>
318+
<target>Identifying consecutive bounces</target>
319+
</trans-unit>
320+
<trans-unit id="messaging.nothing_to_do">
321+
<source>Nothing to do</source>
322+
<target>Nothing to do</target>
323+
</trans-unit>
324+
<trans-unit id="messaging.consecutive_bounces.report">
325+
<source>Processed %processed% out of %total% subscribers</source>
326+
<target>Processed %processed% out of %total% subscribers</target>
327+
</trans-unit>
328+
<trans-unit id="messaging.consecutive_bounces.final_report">
329+
<source>Total of %total% subscribers processed</source>
330+
<target>Total of %total% subscribers processed</target>
331+
</trans-unit>
332+
<trans-unit id="messaging.consecutive_bounces.unconfirm">
333+
<source>Subscriber auto unconfirmed for %count% consecutive bounces</source>
334+
<target>Subscriber auto unconfirmed for %count% consecutive bounces</target>
335+
</trans-unit>
336+
<trans-unit id="messaging.consecutive_bounces.threshold_reached">
337+
<source>%count% consecutive bounces, threshold reached</source>
338+
<target>%count% consecutive bounces, threshold reached</target>
339+
</trans-unit>
340+
341+
<trans-unit id="messaging.reached_max_processing_time">
342+
<source>Reached max processing time; stopping cleanly.</source>
343+
<target>Reached max processing time; stopping cleanly.</target>
344+
</trans-unit>
345+
346+
<trans-unit id="messaging.add_uuid_to_subscribers">
347+
<source>Giving a UUID to %count% subscribers, this may take a while</source>
348+
<target>Giving a UUID to %count% subscribers, this may take a while</target>
349+
</trans-unit>
350+
<trans-unit id="messaging.add_uuid_to_campaigns">
351+
<source>Giving a UUID to %count% campaigns</source>
352+
<target>Giving a UUID to %count% campaigns</target>
353+
</trans-unit>
354+
355+
<trans-unit id="messaging.send_rate_limit">
356+
<source>Batch limit reached, sleeping %sleep%s to respect MAILQUEUE_BATCH_PERIOD</source>
357+
<target>Batch limit reached, sleeping %sleep%s to respect MAILQUEUE_BATCH_PERIOD</target>
358+
</trans-unit>
359+
360+
<trans-unit id="messaging.validation.image_url">
361+
<source>Value must be an array of image URLs.</source>
362+
<target>Value must be an array of image URLs.</target>
363+
</trans-unit>
364+
<trans-unit id="messaging.validation.image_url_not_full">
365+
<source>Image "%url%" is not a full URL.</source>
366+
<target>Image "%url%" is not a full URL.</target>
367+
</trans-unit>
368+
<trans-unit id="messaging.validation.image_url_not_exists">
369+
<source>Image "%url%" does not exist (HTTP %code%)</source>
370+
<target>Image "%url%" does not exist (HTTP %code%)</target>
371+
</trans-unit>
372+
<trans-unit id="messaging.validation.image_url_cant_validate">
373+
<source>Image "%url%" could not be validated: %message%</source>
374+
<target>Image "%url%" could not be validated: %message%</target>
375+
</trans-unit>
376+
377+
<trans-unit id="messaging.validation.urls_not_full">
378+
<source>Not full URLs: %urls%</source>
379+
<target>Not full URLs: %urls%</target>
380+
</trans-unit>
381+
382+
316383
<!-- Subscription -->
317384
<trans-unit id="subscription.list_not_found">
318385
<source>Subscriber list not found.</source>
319386
<target>Subscriber list not found.</target>
320387
</trans-unit>
321388

322-
<trans-unit id="subscription.subscriber_not_found">
389+
<trans-unit id="subscription.subscriber_does_not_exists">
323390
<source>Subscriber does not exists.</source>
324391
<target>Subscriber does not exists.</target>
325392
</trans-unit>
@@ -328,6 +395,55 @@
328395
<source>Subscription not found for this subscriber and list.</source>
329396
<target>Subscription not found for this subscriber and list.</target>
330397
</trans-unit>
398+
<trans-unit id="subscription.attribute_definition_exists">
399+
<source>Attribute definition already exists</source>
400+
<target>Attribute definition already exists</target>
401+
</trans-unit>
402+
<trans-unit id="subscription.attribute_definition_name_exists">
403+
<source>Another attribute with this name already exists.</source>
404+
<target>Another attribute with this name already exists.</target>
405+
</trans-unit>
406+
407+
<trans-unit id="subscription.subscriber_page_not_found">
408+
<source>Subscribe page not found</source>
409+
<target>Subscribe page not found</target>
410+
</trans-unit>
411+
<trans-unit id="subscription.value_is_required">
412+
<source>Value is required</source>
413+
<target>Value is required</target>
414+
</trans-unit>
415+
<trans-unit id="subscription.subscriber_not_found">
416+
<source>Subscriber not found</source>
417+
<target>Subscriber not found</target>
418+
</trans-unit>
419+
<trans-unit id="subscription.unexpected_error">
420+
<source>Unexpected error: %error%</source>
421+
<target>Unexpected error: %error%</target>
422+
</trans-unit>
423+
<trans-unit id="subscription.add_to_blacklist_for">
424+
<source>Added to blacklist for reason %reason%</source>
425+
<target>Added to blacklist for reason %reason%</target>
426+
</trans-unit>
427+
<trans-unit id="subscription.could_not_read_file">
428+
<source>Could not read the uploaded file.</source>
429+
<target>Could not read the uploaded file.</target>
430+
</trans-unit>
431+
<trans-unit id="subscription.csv_import_error">
432+
<source>Error processing %email%: %error%</source>
433+
<target>Error processing %email%: %error%</target>
434+
</trans-unit>
435+
<trans-unit id="subscription.csv_import_general_error">
436+
<source>General import error: %error%</source>
437+
<target>General import error: %error%</target>
438+
</trans-unit>
439+
<trans-unit id="subscription.value_must_be_string">
440+
<source>Value must be a string.</source>
441+
<target>Value must be a string.</target>
442+
</trans-unit>
443+
<trans-unit id="subscription.invalid_attribute_type">
444+
<source>Invalid attribute type: "%type%". Valid types are: %valid_types%</source>
445+
<target>Invalid attribute type: "%type%". Valid types are: %valid_types%</target>
446+
</trans-unit>
331447

332448
</body>
333449
</file>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Messaging\Exception;
6+
7+
use RuntimeException;
8+
use Throwable;
9+
10+
class ImapConnectionException extends RuntimeException
11+
{
12+
public function __construct(?Throwable $previous = null)
13+
{
14+
parent::__construct('Cannot connect to IMAP server', 0, $previous);
15+
}
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Messaging\Exception;
6+
7+
use RuntimeException;
8+
use Throwable;
9+
10+
class OpenMboxFileException extends RuntimeException
11+
{
12+
public function __construct(?Throwable $previous = null)
13+
{
14+
parent::__construct('Cannot open mbox file', 0, $previous);
15+
}
16+
}

src/Domain/Messaging/Service/ConsecutiveBounceHandler.php

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager;
1414
use PhpList\Core\Domain\Subscription\Service\SubscriberBlacklistService;
1515
use Symfony\Component\Console\Style\SymfonyStyle;
16+
use Symfony\Contracts\Translation\TranslatorInterface;
1617

1718
class ConsecutiveBounceHandler
1819
{
1920
private BounceManager $bounceManager;
2021
private SubscriberRepository $subscriberRepository;
2122
private SubscriberHistoryManager $subscriberHistoryManager;
2223
private SubscriberBlacklistService $blacklistService;
24+
private TranslatorInterface $translator;
2325
private int $unsubscribeThreshold;
2426
private int $blacklistThreshold;
2527

@@ -28,26 +30,29 @@ public function __construct(
2830
SubscriberRepository $subscriberRepository,
2931
SubscriberHistoryManager $subscriberHistoryManager,
3032
SubscriberBlacklistService $blacklistService,
33+
TranslatorInterface $translator,
3134
int $unsubscribeThreshold,
3235
int $blacklistThreshold,
3336
) {
3437
$this->bounceManager = $bounceManager;
3538
$this->subscriberRepository = $subscriberRepository;
3639
$this->subscriberHistoryManager = $subscriberHistoryManager;
3740
$this->blacklistService = $blacklistService;
41+
$this->translator = $translator;
3842
$this->unsubscribeThreshold = $unsubscribeThreshold;
3943
$this->blacklistThreshold = $blacklistThreshold;
4044
}
4145

4246
public function handle(SymfonyStyle $io): void
4347
{
44-
$io->section('Identifying consecutive bounces');
48+
$io->section($this->translator->trans('Identifying consecutive bounces'));
4549

4650
$users = $this->subscriberRepository->distinctUsersWithBouncesConfirmedNotBlacklisted();
4751
$total = count($users);
4852

4953
if ($total === 0) {
50-
$io->writeln('Nothing to do');
54+
$io->writeln($this->translator->trans('Nothing to do'));
55+
5156
return;
5257
}
5358

@@ -57,11 +62,14 @@ public function handle(SymfonyStyle $io): void
5762
$processed++;
5863

5964
if ($processed % 5 === 0) {
60-
$io->writeln(\sprintf('processed %d out of %d subscribers', $processed, $total));
65+
$io->writeln($this->translator->trans('Processed %processed% out of %total% subscribers', [
66+
'%processed%' => $processed,
67+
'%total%' => $total,
68+
]));
6169
}
6270
}
6371

64-
$io->writeln(\sprintf('total of %d subscribers processed', $total));
72+
$io->writeln($this->translator->trans('Total of %total% subscribers processed', ['%total%' => $total]));
6573
}
6674

6775
private function processUser(Subscriber $user): void
@@ -123,15 +131,19 @@ private function applyThresholdActions($user, int $consecutive, bool $alreadyUns
123131
$this->subscriberRepository->markUnconfirmed($user->getId());
124132
$this->subscriberHistoryManager->addHistory(
125133
subscriber: $user,
126-
message: 'Auto Unconfirmed',
127-
details: sprintf('Subscriber auto unconfirmed for %d consecutive bounces', $consecutive)
134+
message: $this->translator->trans('Auto unconfirmed'),
135+
details: $this->translator->trans('Subscriber auto unconfirmed for %count% consecutive bounces', [
136+
'%count%' => $consecutive
137+
])
128138
);
129139
}
130140

131141
if ($this->blacklistThreshold > 0 && $consecutive >= $this->blacklistThreshold) {
132142
$this->blacklistService->blacklist(
133143
subscriber: $user,
134-
reason: sprintf('%d consecutive bounces, threshold reached', $consecutive)
144+
reason: $this->translator->trans('%count% consecutive bounces, threshold reached', [
145+
'%count%' => $consecutive
146+
])
135147
);
136148
return true;
137149
}

src/Domain/Messaging/Service/MaxProcessTimeLimiter.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Psr\Log\LoggerInterface;
88
use Symfony\Component\Console\Output\OutputInterface;
9+
use Symfony\Contracts\Translation\TranslatorInterface;
910

1011
/**
1112
* Limits the total processing time of a long-running operation.
@@ -15,8 +16,11 @@ class MaxProcessTimeLimiter
1516
private float $startedAt = 0.0;
1617
private int $maxSeconds;
1718

18-
public function __construct(private readonly LoggerInterface $logger, ?int $maxSeconds = null)
19-
{
19+
public function __construct(
20+
private readonly LoggerInterface $logger,
21+
private readonly TranslatorInterface $translator,
22+
?int $maxSeconds = null
23+
) {
2024
$this->maxSeconds = $maxSeconds ?? 600;
2125
}
2226

@@ -36,7 +40,7 @@ public function shouldStop(?OutputInterface $output = null): bool
3640
$elapsed = microtime(true) - $this->startedAt;
3741
if ($elapsed >= $this->maxSeconds) {
3842
$this->logger->warning(sprintf('Reached max processing time of %d seconds', $this->maxSeconds));
39-
$output?->writeln('Reached max processing time; stopping cleanly.');
43+
$output?->writeln($this->translator->trans('Reached max processing time; stopping cleanly.'));
4044

4145
return true;
4246
}

src/Domain/Messaging/Service/MessageProcessingPreparator.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PhpList\Core\Domain\Messaging\Repository\MessageRepository;
1111
use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository;
1212
use Symfony\Component\Console\Output\OutputInterface;
13+
use Symfony\Contracts\Translation\TranslatorInterface;
1314

1415
class MessageProcessingPreparator
1516
{
@@ -20,17 +21,20 @@ class MessageProcessingPreparator
2021
private SubscriberRepository $subscriberRepository;
2122
private MessageRepository $messageRepository;
2223
private LinkTrackService $linkTrackService;
24+
private TranslatorInterface $translator;
2325

2426
public function __construct(
2527
EntityManagerInterface $entityManager,
2628
SubscriberRepository $subscriberRepository,
2729
MessageRepository $messageRepository,
28-
LinkTrackService $linkTrackService
30+
LinkTrackService $linkTrackService,
31+
TranslatorInterface $translator,
2932
) {
3033
$this->entityManager = $entityManager;
3134
$this->subscriberRepository = $subscriberRepository;
3235
$this->messageRepository = $messageRepository;
3336
$this->linkTrackService = $linkTrackService;
37+
$this->translator = $translator;
3438
}
3539

3640
public function ensureSubscribersHaveUuid(OutputInterface $output): void
@@ -39,7 +43,9 @@ public function ensureSubscribersHaveUuid(OutputInterface $output): void
3943

4044
$numSubscribers = count($subscribersWithoutUuid);
4145
if ($numSubscribers > 0) {
42-
$output->writeln(sprintf('Giving a UUID to %d subscribers, this may take a while', $numSubscribers));
46+
$output->writeln($this->translator->trans('Giving a UUID to %count% subscribers, this may take a while', [
47+
'%count%' => $numSubscribers
48+
]));
4349
foreach ($subscribersWithoutUuid as $subscriber) {
4450
$subscriber->setUniqueId(bin2hex(random_bytes(16)));
4551
}
@@ -53,7 +59,9 @@ public function ensureCampaignsHaveUuid(OutputInterface $output): void
5359

5460
$numCampaigns = count($campaignsWithoutUuid);
5561
if ($numCampaigns > 0) {
56-
$output->writeln(sprintf('Giving a UUID to %d campaigns', $numCampaigns));
62+
$output->writeln($this->translator->trans('Giving a UUID to %count% campaigns', [
63+
'%count%' => $numCampaigns
64+
]));
5765
foreach ($campaignsWithoutUuid as $campaign) {
5866
$campaign->setUuid(bin2hex(random_bytes(18)));
5967
}

src/Domain/Messaging/Service/NativeBounceProcessingService.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66

77
use IMAP\Connection;
88
use PhpList\Core\Domain\Common\Mail\NativeImapMailReader;
9+
use PhpList\Core\Domain\Messaging\Exception\OpenMboxFileException;
910
use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager;
1011
use PhpList\Core\Domain\Messaging\Service\Processor\BounceDataProcessor;
1112
use Psr\Log\LoggerInterface;
12-
use RuntimeException;
1313
use Throwable;
1414

1515
class NativeBounceProcessingService implements BounceProcessingServiceInterface
@@ -69,9 +69,12 @@ private function openOrFail(string $mailbox, bool $testMode): Connection
6969
{
7070
try {
7171
return $this->mailReader->open($mailbox, $testMode ? 0 : CL_EXPUNGE);
72-
} catch (Throwable $e) {
73-
$this->logger->error('Cannot open mailbox file: '.$e->getMessage());
74-
throw new RuntimeException('Cannot open mbox file');
72+
} catch (Throwable $throwable) {
73+
$this->logger->error('Cannot open mailbox file', [
74+
'mailbox' => $mailbox,
75+
'error' => $throwable->getMessage(),
76+
]);
77+
throw new OpenMboxFileException($throwable);
7578
}
7679
}
7780

src/Domain/Messaging/Service/SendRateLimiter.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PhpList\Core\Domain\Common\IspRestrictionsProvider;
1010
use PhpList\Core\Domain\Messaging\Repository\UserMessageRepository;
1111
use Symfony\Component\Console\Output\OutputInterface;
12+
use Symfony\Contracts\Translation\TranslatorInterface;
1213

1314
/**
1415
* Encapsulates batching and throttling logic for sending emails respecting
@@ -26,6 +27,7 @@ class SendRateLimiter
2627
public function __construct(
2728
private readonly IspRestrictionsProvider $ispRestrictionsProvider,
2829
private readonly UserMessageRepository $userMessageRepository,
30+
private readonly TranslatorInterface $translator,
2931
private readonly ?int $mailqueueBatchSize = null,
3032
private readonly ?int $mailqueueBatchPeriod = null,
3133
private readonly ?int $mailqueueThrottle = null,
@@ -76,9 +78,9 @@ public function awaitTurn(?OutputInterface $output = null): bool
7678
$elapsed = microtime(true) - $this->batchStart;
7779
$remaining = (int)ceil($this->batchPeriod - $elapsed);
7880
if ($remaining > 0) {
79-
$output?->writeln(sprintf(
80-
'Batch limit reached, sleeping %ds to respect MAILQUEUE_BATCH_PERIOD',
81-
$remaining
81+
$output?->writeln($this->translator->trans(
82+
'Batch limit reached, sleeping %sleep%s to respect MAILQUEUE_BATCH_PERIOD',
83+
['%sleep%' => $remaining]
8284
));
8385
sleep($remaining);
8486
}

0 commit comments

Comments
 (0)