77use DateTimeImmutable ;
88use Exception ;
99use PhpList \Core \Domain \Messaging \Model \Bounce ;
10+ use PhpList \Core \Domain \Messaging \Model \UserMessage ;
11+ use PhpList \Core \Domain \Messaging \Model \UserMessageBounce ;
1012use PhpList \Core \Domain \Messaging \Repository \MessageRepository ;
1113use PhpList \Core \Domain \Messaging \Service \LockService ;
1214use 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
0 commit comments