From 71f60e27c07f58481d658b8dc849b0b7cf6f3ac7 Mon Sep 17 00:00:00 2001 From: Lamm Date: Fri, 23 Jan 2026 16:11:22 +0100 Subject: [PATCH 1/5] Run anonymization and cleanup more periodically and assure configuration manager is initialized --- .ddev/config.yaml | 4 + .github/workflows/tasks.yml | 2 +- .../Domain/Repository/MailLogRepository.php | 133 ++++------------ .../EventListener/CleanupEventListener.php | 47 ++++++ Classes/Logging/LoggingTransport.php | 49 +++--- Classes/Logging/LoggingTransportFactory.php | 33 ++++ Classes/Logging/MailerExtender.php | 11 +- Classes/Service/CleanupService.php | 142 ++++++++++++++++++ Classes/Service/CleanupSettingsService.php | 78 ++++++++++ Configuration/Services.yaml | 21 +++ .../AbstractMailLogRepositoryTest.php | 91 +++++------ ext_conf_template.txt | 2 + ext_tables.sql | 2 +- phpunit.xml | 2 +- 14 files changed, 427 insertions(+), 190 deletions(-) create mode 100644 Classes/EventListener/CleanupEventListener.php create mode 100644 Classes/Logging/LoggingTransportFactory.php create mode 100644 Classes/Service/CleanupService.php create mode 100644 Classes/Service/CleanupSettingsService.php create mode 100644 ext_conf_template.txt diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 114b170..e490515 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -18,6 +18,10 @@ web_environment: - typo3DatabasePassword=db corepack_enable: false +hooks: + post-start: + - exec-host: ddev mysql -uroot -proot -e "GRANT ALL ON \`db_%\`.* TO 'db'@'%';" + # Key features of DDEV's config.yaml: # name: # Name of the project, automatically provides diff --git a/.github/workflows/tasks.yml b/.github/workflows/tasks.yml index 0623a43..479fa45 100644 --- a/.github/workflows/tasks.yml +++ b/.github/workflows/tasks.yml @@ -22,7 +22,7 @@ jobs: - uses: mirromutth/mysql-action@v1.1 with: mysql version: '8.0' - mysql database: 'typo3_test' + mysql database: 'db' mysql root password: 'root' - uses: actions/checkout@v3 - uses: actions/cache@v3 diff --git a/Classes/Domain/Repository/MailLogRepository.php b/Classes/Domain/Repository/MailLogRepository.php index 4ce5bdb..bcfca63 100644 --- a/Classes/Domain/Repository/MailLogRepository.php +++ b/Classes/Domain/Repository/MailLogRepository.php @@ -6,13 +6,12 @@ use Override; use DateTime; -use Exception; use InvalidArgumentException; use Pluswerk\MailLogger\Domain\Model\MailLog; -use TYPO3\CMS\Core\Database\ConnectionPool; +use Pluswerk\MailLogger\Service\CleanupSettingsService; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Configuration\ConfigurationManager; -use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; +use TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException; +use TYPO3\CMS\Extbase\Persistence\Exception\UnknownObjectException; use TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings; use TYPO3\CMS\Extbase\Persistence\QueryInterface; use TYPO3\CMS\Extbase\Persistence\Repository; @@ -26,23 +25,12 @@ class MailLogRepository extends Repository 'crdate' => QueryInterface::ORDER_DESCENDING, ]; - protected string $defaultLifetime = '30 days'; - - protected string $defaultAnonymizeAfter = '7 days'; - - protected string $lifetime = '30 days'; - - protected string $anonymizeAfter = '7 days'; - - protected string $anonymizeSymbol = '***'; - - protected bool $anonymize = true; - /** * Constructs a new Repository */ - public function __construct(private readonly ConnectionPool $connectionPool) - { + public function __construct( + private readonly CleanupSettingsService $cleanupSettingsService, + ) { parent::__construct(); } @@ -53,82 +41,12 @@ public function initializeObject(): void $querySettings = GeneralUtility::makeInstance(Typo3QuerySettings::class); $querySettings->setRespectStoragePage(false); $this->setDefaultQuerySettings($querySettings); - - // mail logger typoscript settings - $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class); - $fullSettings = $configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT); - $settings = $fullSettings['module.']['tx_maillogger.']['settings.']; - - $this->lifetime = $this->defaultLifetime; - if (isset($settings['cleanup.']['lifetime'])) { - $this->lifetime = $settings['cleanup.']['lifetime']; - } - - $this->anonymizeAfter = $this->defaultAnonymizeAfter; - if (isset($settings['cleanup.']['anonymizeAfter'])) { - $this->anonymizeAfter = $settings['cleanup.']['anonymizeAfter']; - } - - if (isset($settings['cleanup.']['anonymize'])) { - $this->anonymize = (bool)$settings['cleanup.']['anonymize']; - } - - // cleanup - $this->cleanupDatabase(); - - // anonymize - $this->anonymizeAll(); - } - - /** - * Delete old mail log entries (default: 30 days and hard deletion) - */ - private function cleanupDatabase(): void - { - if ($this->lifetime !== '') { - $deletionTimestamp = strtotime('-' . $this->lifetime); - if ($deletionTimestamp === false) { - throw new Exception(sprintf('Given lifetime string in TypoScript is wrong. lifetime: "%s"', $this->lifetime), 9235306650); - } - - $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tx_maillogger_domain_model_maillog'); - $queryBuilder->getRestrictions()->removeAll(); - $queryBuilder->delete('tx_maillogger_domain_model_maillog') - ->where($queryBuilder->expr()->lte('crdate', $queryBuilder->createNamedParameter($deletionTimestamp))) - ->executeStatement(); - } - } - - /** - * Anonymize mail logs (default: after 7 days) - */ - private function anonymizeAll(): void - { - if ($this->anonymize) { - $timestamp = strtotime('-' . $this->anonymizeAfter); - if ($timestamp === false) { - throw new Exception(sprintf('Given lifetime string in TypoScript is wrong. anonymize: "%s"', $this->anonymizeAfter), 3198610142); - } - - $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tx_maillogger_domain_model_maillog'); - $queryBuilder->getRestrictions()->removeAll(); - $queryBuilder->update('tx_maillogger_domain_model_maillog') - ->set('tstamp', time()) - ->set('subject', $this->anonymizeSymbol) - ->set('message', $this->anonymizeSymbol) - ->set('mail_from', $this->anonymizeSymbol) - ->set('mail_to', $this->anonymizeSymbol) - ->set('mail_copy', $this->anonymizeSymbol) - ->set('mail_blind_copy', $this->anonymizeSymbol) - ->set('headers', $this->anonymizeSymbol) - ->set('debug', $this->anonymizeSymbol) - ->where($queryBuilder->expr()->lte('crdate', $queryBuilder->createNamedParameter($timestamp))) - ->executeStatement(); - } } /** * @param MailLog $mailLog + * @noinspection PhpParameterNameChangedDuringInheritanceInspection + * @throws IllegalObjectTypeException */ #[Override] public function add($mailLog): void @@ -148,6 +66,9 @@ public function add($mailLog): void /** * @param MailLog $mailLog + * @throws UnknownObjectException + * @throws IllegalObjectTypeException + * @noinspection PhpParameterNameChangedDuringInheritanceInspection */ #[Override] public function update($mailLog): void @@ -163,44 +84,50 @@ public function update($mailLog): void private function anonymizeMailLogIfNeeded(MailLog $mailLog): void { + if (!$this->cleanupSettingsService->isLoaded()) { + return; + } + if ($mailLog->getCrdate() === null) { throw new InvalidArgumentException('MailLog must have a crdate', 8348363881); } - if (!$this->anonymize) { + if (!$this->cleanupSettingsService->shouldAnonymize()) { return; } - if ($mailLog->getCrdate() > date_modify(new DateTime(), '-' . $this->anonymizeAfter)->getTimestamp()) { + $anonymizeAfter = $this->cleanupSettingsService->getAnonymizeAfter(); + if ($mailLog->getCrdate() > date_modify(new DateTime(), '-' . $anonymizeAfter)->getTimestamp()) { return; } - $mailLog->setSubject($this->anonymizeSymbol); - $mailLog->setMessage($this->anonymizeSymbol); - $mailLog->setMailFrom($this->anonymizeSymbol); - $mailLog->setMailTo($this->anonymizeSymbol); - $mailLog->setMailCopy($this->anonymizeSymbol); - $mailLog->setMailBlindCopy($this->anonymizeSymbol); - $mailLog->setHeaders($this->anonymizeSymbol); + $anonymizeSymbol = $this->cleanupSettingsService->getAnonymizeSymbol(); + $mailLog->setSubject($anonymizeSymbol); + $mailLog->setMessage($anonymizeSymbol); + $mailLog->setMailFrom($anonymizeSymbol); + $mailLog->setMailTo($anonymizeSymbol); + $mailLog->setMailCopy($anonymizeSymbol); + $mailLog->setMailBlindCopy($anonymizeSymbol); + $mailLog->setHeaders($anonymizeSymbol); } public function getLifetime(): string { - return $this->lifetime; + return $this->cleanupSettingsService->getLifetime(); } public function shouldAnonymize(): bool { - return $this->anonymize; + return $this->cleanupSettingsService->shouldAnonymize(); } public function getAnonymizeSymbol(): string { - return $this->anonymizeSymbol; + return $this->cleanupSettingsService->getAnonymizeSymbol(); } public function getAnonymizeAfter(): string { - return $this->anonymizeAfter; + return $this->cleanupSettingsService->getAnonymizeAfter(); } } diff --git a/Classes/EventListener/CleanupEventListener.php b/Classes/EventListener/CleanupEventListener.php new file mode 100644 index 0000000..1ade52c --- /dev/null +++ b/Classes/EventListener/CleanupEventListener.php @@ -0,0 +1,47 @@ +runCleanup(); + } + + /** + * Triggered on backend page render + */ + public function onBackendRender(AfterBackendPageRenderEvent $event): void + { + $this->runCleanup(); + } + + private function runCleanup(): void + { + try { + $this->cleanupService->tryRunCleanup(); + } catch (Throwable) { + // Silently ignore to not break page rendering + // CleanupService already logs the error + } + } +} diff --git a/Classes/Logging/LoggingTransport.php b/Classes/Logging/LoggingTransport.php index ab7883b..abdae07 100644 --- a/Classes/Logging/LoggingTransport.php +++ b/Classes/Logging/LoggingTransport.php @@ -29,10 +29,14 @@ use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager; use TYPO3\CMS\Extbase\Utility\DebuggerUtility; -class LoggingTransport implements TransportInterface, Stringable +class LoggingTransport implements TransportInterface { - public function __construct(protected TransportInterface $originalTransport) - { + public function __construct( + protected TransportInterface $originalTransport, + protected MailLogRepository $mailLogRepository, + protected PersistenceManager $persistenceManager, + protected MailLog $mailLog, + ) { } #[Override] @@ -40,25 +44,20 @@ public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMess { $this->fixTcaIfNotPresentIsUsedInInstallTool(); - $mailLogRepository = GeneralUtility::makeInstance(MailLogRepository::class); - // write mail to log before send - $mailLog = GeneralUtility::makeInstance(MailLog::class); - $this->assignMailLog($mailLog, $message); - $mailLogRepository->add($mailLog); - GeneralUtility::makeInstance(PersistenceManager::class)->persistAll(); - + $this->assignMailLog($message); + $this->mailLogRepository->add($this->mailLog); + $this->persistenceManager->persistAll(); $sendResult = $this->originalSend($message, $envelope); // write result to log after send - $this->assignMailLog($mailLog, $message); - $mailLog->setResult($sendResult->result); - $mailLog->setStatus($sendResult->status->value); - $mailLog->setDebug($sendResult->getDebugMessage()); + $this->mailLog->setResult($sendResult->result); + $this->mailLog->setStatus($sendResult->status->value); + $this->mailLog->setDebug($sendResult->getDebugMessage()); - $mailLogRepository->update($mailLog); - GeneralUtility::makeInstance(PersistenceManager::class)->persistAll(); + $this->mailLogRepository->update($this->mailLog); + $this->persistenceManager->persistAll(); if ($sendResult->throwable) { throw $sendResult->throwable; @@ -107,22 +106,22 @@ public function __toString(): string return $this->originalTransport->__toString(); } - protected function assignMailLog(MailLog $mailLog, RawMessage $message): void + protected function assignMailLog(RawMessage $message): void { if (!$message instanceof Email) { return; } $messageBody = $message->getBody(); - $mailLog->setMessage($this->getBodyAsHtml($messageBody)); - $mailLog->setSubject($message->getSubject()); - $mailLog->setMailFrom($this->addressesToString($message->getFrom())); - $mailLog->setMailTo($this->addressesToString($message->getTo())); - $mailLog->setMailCopy($this->addressesToString($message->getCc())); - $mailLog->setMailBlindCopy($this->addressesToString($message->getBcc())); - $mailLog->setHeaders($message->getHeaders()->toString()); + $this->mailLog->setMessage($this->getBodyAsHtml($messageBody)); + $this->mailLog->setSubject($message->getSubject()); + $this->mailLog->setMailFrom($this->addressesToString($message->getFrom())); + $this->mailLog->setMailTo($this->addressesToString($message->getTo())); + $this->mailLog->setMailCopy($this->addressesToString($message->getCc())); + $this->mailLog->setMailBlindCopy($this->addressesToString($message->getBcc())); + $this->mailLog->setHeaders($message->getHeaders()->toString()); if ($message instanceof TemplateBasedMailMessage) { - $mailLog->setTypoScriptKey($message->getTypoScriptKey()); + $this->mailLog->setTypoScriptKey($message->getTypoScriptKey()); } } diff --git a/Classes/Logging/LoggingTransportFactory.php b/Classes/Logging/LoggingTransportFactory.php new file mode 100644 index 0000000..50acb1f --- /dev/null +++ b/Classes/Logging/LoggingTransportFactory.php @@ -0,0 +1,33 @@ +mailLogRepository, + $this->persistenceManager, + $this->mailLog, + ); + } +} diff --git a/Classes/Logging/MailerExtender.php b/Classes/Logging/MailerExtender.php index 3b877f0..b620858 100644 --- a/Classes/Logging/MailerExtender.php +++ b/Classes/Logging/MailerExtender.php @@ -14,15 +14,18 @@ */ class MailerExtender extends Mailer { - public function __construct(?TransportInterface $transport = null, ?EventDispatcherInterface $eventDispatcher = null) - { + public function __construct( + protected LoggingTransportFactory $loggingTransportFactory, + ?TransportInterface $transport = null, + ?EventDispatcherInterface $eventDispatcher = null, + ) { parent::__construct($transport, $eventDispatcher); - $this->transport = new LoggingTransport($this->transport); + $this->transport = $this->loggingTransportFactory->create($this->transport); } #[Override] public function getRealTransport(): TransportInterface { - return new LoggingTransport(parent::getRealTransport()); + return $this->loggingTransportFactory->create(parent::getRealTransport()); } } diff --git a/Classes/Service/CleanupService.php b/Classes/Service/CleanupService.php new file mode 100644 index 0000000..c74ef94 --- /dev/null +++ b/Classes/Service/CleanupService.php @@ -0,0 +1,142 @@ +cache->has(self::CACHE_KEY)) { + return; + } + + $minInterval = $this->getCleanupMinInterval(); + + // Acquire lock by setting cache entry with lifetime = minInterval + $this->cache->set(self::CACHE_KEY, time(), [], $minInterval); + + try { + $this->cleanupDatabase(); + $this->anonymizeAll(); + } catch (Exception $exception) { + $this->logger?->error('Mail logger cleanup failed', [ + 'exception' => $exception, + 'message' => $exception->getMessage(), + ]); + + // Release lock on failure so cleanup can be retried + $this->cache->remove(self::CACHE_KEY); + } + } + + /** + * Delete old mail log entries + */ + private function cleanupDatabase(): void + { + $lifetime = $this->cleanupSettingsService->getLifetime(); + + if ($lifetime === '') { + return; + } + + $deletionTimestamp = strtotime('-' . $lifetime); + if ($deletionTimestamp === false) { + throw new Exception( + sprintf('Given lifetime string in TypoScript is wrong. lifetime: "%s"', $lifetime), + 9235306650 + ); + } + + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME); + $queryBuilder->getRestrictions()->removeAll(); + $queryBuilder->delete(self::TABLE_NAME) + ->where($queryBuilder->expr()->lte('crdate', $queryBuilder->createNamedParameter($deletionTimestamp))) + ->executeStatement(); + } + + /** + * Anonymize mail logs older than specified time + */ + private function anonymizeAll(): void + { + if (!$this->cleanupSettingsService->shouldAnonymize()) { + return; + } + + $anonymizeAfter = $this->cleanupSettingsService->getAnonymizeAfter(); + $anonymizeSymbol = $this->cleanupSettingsService->getAnonymizeSymbol(); + + $timestamp = strtotime('-' . $anonymizeAfter); + if ($timestamp === false) { + throw new Exception( + sprintf('Given lifetime string in TypoScript is wrong. anonymizeAfter: "%s"', $anonymizeAfter), + 3198610142 + ); + } + + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME); + $queryBuilder->getRestrictions()->removeAll(); + $queryBuilder->update(self::TABLE_NAME) + ->set('tstamp', time()) + ->set('subject', $anonymizeSymbol) + ->set('message', $anonymizeSymbol) + ->set('mail_from', $anonymizeSymbol) + ->set('mail_to', $anonymizeSymbol) + ->set('mail_copy', $anonymizeSymbol) + ->set('mail_blind_copy', $anonymizeSymbol) + ->set('headers', $anonymizeSymbol) + ->set('debug', $anonymizeSymbol) + ->where( + $queryBuilder->expr()->lte('crdate', $queryBuilder->createNamedParameter($timestamp)), + // Skip already fully anonymized records + $queryBuilder->expr()->or( + $queryBuilder->expr()->neq('subject', $queryBuilder->createNamedParameter($anonymizeSymbol)), + $queryBuilder->expr()->neq('message', $queryBuilder->createNamedParameter($anonymizeSymbol)), + $queryBuilder->expr()->neq('mail_from', $queryBuilder->createNamedParameter($anonymizeSymbol)), + $queryBuilder->expr()->neq('mail_to', $queryBuilder->createNamedParameter($anonymizeSymbol)), + $queryBuilder->expr()->neq('mail_copy', $queryBuilder->createNamedParameter($anonymizeSymbol)), + $queryBuilder->expr()->neq('mail_blind_copy', $queryBuilder->createNamedParameter($anonymizeSymbol)), + $queryBuilder->expr()->neq('headers', $queryBuilder->createNamedParameter($anonymizeSymbol)), + $queryBuilder->expr()->neq('debug', $queryBuilder->createNamedParameter($anonymizeSymbol)), + ), + ) + ->executeStatement(); + } + + private function getCleanupMinInterval(): int + { + try { + $config = $this->extensionConfiguration->get('mail_logger'); + return (int)($config['cleanupMinInterval'] ?? 3600); + } catch (Exception) { + return 3600; + } + } +} diff --git a/Classes/Service/CleanupSettingsService.php b/Classes/Service/CleanupSettingsService.php new file mode 100644 index 0000000..195374c --- /dev/null +++ b/Classes/Service/CleanupSettingsService.php @@ -0,0 +1,78 @@ +loadSettings(); + return $this->loaded; + } + + public function getLifetime(): string + { + $this->loadSettings(); + return $this->lifetime; + } + + public function shouldAnonymize(): bool + { + $this->loadSettings(); + return $this->anonymize; + } + + public function getAnonymizeAfter(): string + { + $this->loadSettings(); + return $this->anonymizeAfter; + } + + public function getAnonymizeSymbol(): string + { + return self::ANONYMIZE_SYMBOL; + } + + private function loadSettings(): void + { + if ($this->loaded) { + return; + } + + try { + $fullSettings = $this->configurationManager->getConfiguration( + ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT + ); + } catch (NoServerRequestGivenException) { + // Some rare cases have no server request available yet. + // Keep defaults and mark as not loaded so callers can check via isLoaded(). + return; + } + + $settings = $fullSettings['module.']['tx_maillogger.']['settings.'] ?? []; + + $this->lifetime = $settings['cleanup.']['lifetime'] ?? self::DEFAULT_LIFETIME; + $this->anonymize = (bool)($settings['cleanup.']['anonymize'] ?? true); + $this->anonymizeAfter = $settings['cleanup.']['anonymizeAfter'] ?? self::DEFAULT_ANONYMIZE_AFTER; + + $this->loaded = true; + } +} diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index cccbc8a..b17b8be 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -14,6 +14,12 @@ services: resource: '../Classes/Domain/Model/*' shared: false + Pluswerk\MailLogger\Logging\MailerExtender: + arguments: + $loggingTransportFactory: '@Pluswerk\MailLogger\Logging\LoggingTransportFactory' + $transport: null + $eventDispatcher: null + TYPO3\CMS\Core\Mail\MailerInterface: alias: Pluswerk\MailLogger\Logging\MailerExtender TYPO3\CMS\Core\Mail\Mailer: @@ -25,3 +31,18 @@ services: command: 'maillogger:testmail' description: 'This command sends a mail with your complete setup and logs it. Additionally you can provide a mail template key and test your template in the default language, of course without variables.' schedulable: false + + Pluswerk\MailLogger\Service\CleanupService: + arguments: + $cache: '@cache.hash' + + Pluswerk\MailLogger\EventListener\CleanupEventListener: + tags: + - name: event.listener + identifier: 'mail-logger/cleanup-frontend' + method: 'onFrontendRender' + event: TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent + - name: event.listener + identifier: 'mail-logger/cleanup-backend' + method: 'onBackendRender' + event: TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent diff --git a/Tests/Functional/MailLogRepository/AbstractMailLogRepositoryTest.php b/Tests/Functional/MailLogRepository/AbstractMailLogRepositoryTest.php index 5c82905..1cf10a5 100644 --- a/Tests/Functional/MailLogRepository/AbstractMailLogRepositoryTest.php +++ b/Tests/Functional/MailLogRepository/AbstractMailLogRepositoryTest.php @@ -5,6 +5,7 @@ namespace Pluswerk\MailLogger\Tests\Functional\MailLogRepository; use Override; +use Pluswerk\MailLogger\Service\CleanupService; use ReflectionObject; use DateTime; use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; @@ -23,10 +24,7 @@ abstract class AbstractMailLogRepositoryTest extends FunctionalTestCase { use MatchesSnapshots; - /** - * @var string - */ - private const DELAY_ANONYMIZE = '8 days'; + private const string DELAY_ANONYMIZE = '8 days'; protected array $testExtensionsToLoad = [ 'typo3conf/ext/mail_logger', @@ -43,9 +41,7 @@ protected function setUp(): void public function testInitializeObject(): void { - $persistenceManager = GeneralUtility::makeInstance(PersistenceManager::class); - - $mailLogRepository = $this->initializeMailLogRepository($persistenceManager); + $mailLogRepository = GeneralUtility::makeInstance(MailLogRepository::class); $this->assertMatchesJsonSnapshot( json_encode( @@ -62,56 +58,43 @@ public function testInitializeObject(): void public function testAdd(): void { - $persistenceManager = GeneralUtility::makeInstance(PersistenceManager::class); - - $mailLogRepository = $this->initializeMailLogRepository($persistenceManager); - - $mailLog = $this->createAndSaveMailLog($mailLogRepository, $persistenceManager, 558); + $mailLog = $this->createAndSaveMailLog(558); $this->assertModelSnapshot($mailLog); } public function testUpdate(): void { - $persistenceManager = GeneralUtility::makeInstance(PersistenceManager::class); - - $mailLogRepository = $this->initializeMailLogRepository($persistenceManager); - - $mailLog = $this->createAndSaveMailLog($mailLogRepository, $persistenceManager, 555); + $mailLog = $this->createAndSaveMailLog(555); - $mailLog = $this->updatingMailLog($mailLogRepository, $persistenceManager, $mailLog); + $mailLog = $this->updatingMailLog($mailLog); $this->assertModelSnapshot($mailLog); } public function testUpdateWithDelayAnonymize(): void { - $persistenceManager = GeneralUtility::makeInstance(PersistenceManager::class); - - $mailLogRepository = $this->initializeMailLogRepository($persistenceManager); - /** @var MailLog $mailLog */ - $mailLog = $this->createAndSaveMailLog($mailLogRepository, $persistenceManager, 2345); + $mailLog = $this->createAndSaveMailLog(2345); $mailLog->_setProperty('crdate', date_modify(new DateTime(), '-' . self::DELAY_ANONYMIZE)->getTimestamp() - 5); - $mailLog = $this->updatingMailLog($mailLogRepository, $persistenceManager, $mailLog); + $mailLog = $this->updatingMailLog($mailLog); $this->assertModelSnapshot($mailLog); } public function testCleanupDatabase(): void { - $persistenceManager = GeneralUtility::makeInstance(PersistenceManager::class); - - $mailLogRepository = $this->initializeMailLogRepository($persistenceManager); - - $this->createAndSaveMailLog($mailLogRepository, $persistenceManager, 789); + $this->createAndSaveMailLog(789); GeneralUtility::makeInstance(ConnectionPool::class) ->getConnectionForTable('tx_maillogger_domain_model_maillog') ->update('tx_maillogger_domain_model_maillog', ['tstamp' => 0, 'crdate' => 0], ['uid' => 1]); - $this->cleanupDatabasePart($mailLogRepository, $persistenceManager); + $persistenceManager = GeneralUtility::makeInstance(PersistenceManager::class); + $mailLogRepository = GeneralUtility::makeInstance(MailLogRepository::class); + + $this->cleanupDatabasePart($persistenceManager); /** @var MailLog $mailLog */ $mailLog = $mailLogRepository->findAll()->getFirst(); @@ -120,33 +103,23 @@ public function testCleanupDatabase(): void public function testAnonymizeAll(): void { - $persistenceManager = GeneralUtility::makeInstance(PersistenceManager::class); - - $mailLogRepository = $this->initializeMailLogRepository($persistenceManager); - - $this->createAndSaveMailLog($mailLogRepository, $persistenceManager, 7894); + $this->createAndSaveMailLog(7894); $timestamp = date_modify(new DateTime(), '-' . self::DELAY_ANONYMIZE)->getTimestamp() - 5; GeneralUtility::makeInstance(ConnectionPool::class) ->getConnectionForTable('tx_maillogger_domain_model_maillog') ->update('tx_maillogger_domain_model_maillog', ['tstamp' => $timestamp, 'crdate' => $timestamp], ['uid' => 1]); - + $persistenceManager = GeneralUtility::makeInstance(PersistenceManager::class); $persistenceManager->clearState(); - $this->anonymizeAllPart($mailLogRepository, $persistenceManager); + $this->anonymizeAllPart(); + $mailLogRepository = GeneralUtility::makeInstance(MailLogRepository::class); /** @var MailLog $mailLog */ $mailLog = $mailLogRepository->findAll()->getFirst(); - $this->assertModelSnapshot($mailLog); - } - protected function initializeMailLogRepository(PersistenceManager $persistenceManager): MailLogRepository - { - $mailLogRepository = GeneralUtility::makeInstance(MailLogRepository::class); - $mailLogRepository->injectPersistenceManager($persistenceManager); - $mailLogRepository->initializeObject(); - return $mailLogRepository; + $this->assertModelSnapshot($mailLog); } protected function getNewMailLog(int $seed): MailLog @@ -166,24 +139,29 @@ protected function getNewMailLog(int $seed): MailLog /** * @throws NotImplementedException */ - protected function createAndSaveMailLog(MailLogRepository $mailLogRepository, PersistenceManager $persistenceManager, int $seed): MailLog + protected function createAndSaveMailLog(int $seed): MailLog { + $persistenceManager = GeneralUtility::makeInstance(PersistenceManager::class); + $mailLogRepository = GeneralUtility::makeInstance(MailLogRepository::class); + $mailLogRepository->add($this->getNewMailLog($seed)); + $persistenceManager->persistAll(); $persistenceManager->clearState(); - $mailLog = $mailLogRepository->findAll()->getFirst(); - $persistenceManager->persistAll(); -// $persistenceManager->clearState(); - return $mailLog; + return $mailLogRepository->findAll()->getFirst(); } /** * @throws NotImplementedException */ - protected function updatingMailLog(MailLogRepository $mailLogRepository, PersistenceManager $persistenceManager, MailLog $mailLog): MailLog + protected function updatingMailLog(MailLog $mailLog): MailLog { + $persistenceManager = GeneralUtility::makeInstance(PersistenceManager::class); + $mailLogRepository = GeneralUtility::makeInstance(MailLogRepository::class); + $mailLogRepository->update($mailLog); + $persistenceManager->persistAll(); $persistenceManager->clearState(); @@ -196,9 +174,10 @@ protected function updatingMailLog(MailLogRepository $mailLogRepository, Persist /** * @throws NotImplementedException */ - protected function cleanupDatabasePart(MailLogRepository $mailLogRepository, PersistenceManager $persistenceManager): void + protected function cleanupDatabasePart(PersistenceManager $persistenceManager): void { - $this->callInaccessibleMethod($mailLogRepository, 'cleanupDatabase'); + $cleanupService = GeneralUtility::makeInstance(CleanupService::class); + $this->callInaccessibleMethod($cleanupService, 'cleanupDatabase'); $persistenceManager->persistAll(); $persistenceManager->clearState(); } @@ -206,9 +185,11 @@ protected function cleanupDatabasePart(MailLogRepository $mailLogRepository, Per /** * @throws NotImplementedException */ - protected function anonymizeAllPart(MailLogRepository $mailLogRepository, PersistenceManager $persistenceManager): void + protected function anonymizeAllPart(): void { - $this->callInaccessibleMethod($mailLogRepository, 'anonymizeAll'); + $persistenceManager = GeneralUtility::makeInstance(PersistenceManager::class); + $cleanupService = GeneralUtility::makeInstance(CleanupService::class); + $this->callInaccessibleMethod($cleanupService, 'anonymizeAll'); $persistenceManager->persistAll(); $persistenceManager->clearState(); } diff --git a/ext_conf_template.txt b/ext_conf_template.txt new file mode 100644 index 0000000..6a96cfc --- /dev/null +++ b/ext_conf_template.txt @@ -0,0 +1,2 @@ +# cat=Cleanup; type=int+; label=Minimum interval between cleanup runs (in seconds) +cleanupMinInterval = 3600 diff --git a/ext_tables.sql b/ext_tables.sql index 310c3d8..50e122d 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -70,5 +70,5 @@ CREATE TABLE tx_maillogger_domain_model_maillog ( cruser_id int(11) unsigned DEFAULT '0' NOT NULL, PRIMARY KEY (uid), - KEY parent (pid), + KEY parent (pid) ); diff --git a/phpunit.xml b/phpunit.xml index 794ae60..d74d7e2 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -30,7 +30,7 @@ - + From d5fee94ca184fcd86d958c916f30a76cb0b33641 Mon Sep 17 00:00:00 2001 From: Lamm Date: Fri, 23 Jan 2026 16:15:09 +0100 Subject: [PATCH 2/5] Code cleanup --- Classes/EventListener/CleanupEventListener.php | 7 ++++--- Classes/Service/CleanupService.php | 4 +++- Classes/Service/CleanupSettingsService.php | 8 +++++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Classes/EventListener/CleanupEventListener.php b/Classes/EventListener/CleanupEventListener.php index 1ade52c..c98d265 100644 --- a/Classes/EventListener/CleanupEventListener.php +++ b/Classes/EventListener/CleanupEventListener.php @@ -17,12 +17,13 @@ { public function __construct( private CleanupService $cleanupService, - ) {} + ) { + } /** * Triggered on frontend page render */ - public function onFrontendRender(AfterCacheableContentIsGeneratedEvent $event): void + public function onFrontendRender(): void { $this->runCleanup(); } @@ -30,7 +31,7 @@ public function onFrontendRender(AfterCacheableContentIsGeneratedEvent $event): /** * Triggered on backend page render */ - public function onBackendRender(AfterBackendPageRenderEvent $event): void + public function onBackendRender(): void { $this->runCleanup(); } diff --git a/Classes/Service/CleanupService.php b/Classes/Service/CleanupService.php index c74ef94..3c22938 100644 --- a/Classes/Service/CleanupService.php +++ b/Classes/Service/CleanupService.php @@ -16,6 +16,7 @@ class CleanupService implements LoggerAwareInterface use LoggerAwareTrait; private const string CACHE_KEY = 'tx_maillogger_cleanup_lock'; + private const string TABLE_NAME = 'tx_maillogger_domain_model_maillog'; public function __construct( @@ -23,7 +24,8 @@ public function __construct( private readonly CleanupSettingsService $cleanupSettingsService, private readonly FrontendInterface $cache, private readonly ExtensionConfiguration $extensionConfiguration, - ) {} + ) { + } /** * Try to run cleanup if not already running and interval has passed. diff --git a/Classes/Service/CleanupSettingsService.php b/Classes/Service/CleanupSettingsService.php index 195374c..ec6645b 100644 --- a/Classes/Service/CleanupSettingsService.php +++ b/Classes/Service/CleanupSettingsService.php @@ -10,17 +10,23 @@ class CleanupSettingsService { private const string DEFAULT_LIFETIME = '30 days'; + private const string DEFAULT_ANONYMIZE_AFTER = '7 days'; + private const string ANONYMIZE_SYMBOL = '***'; private bool $loaded = false; + private string $lifetime = self::DEFAULT_LIFETIME; + private bool $anonymize = true; + private string $anonymizeAfter = self::DEFAULT_ANONYMIZE_AFTER; public function __construct( private readonly ConfigurationManagerInterface $configurationManager, - ) {} + ) { + } public function isLoaded(): bool { From 7757b6a76886e0f9b524f509b10e9383a8d5003a Mon Sep 17 00:00:00 2001 From: Lamm Date: Fri, 23 Jan 2026 16:19:04 +0100 Subject: [PATCH 3/5] Add cms-extbase requirement --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3002ad7..1de0665 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,8 @@ "require": { "php": "~8.3.0 || ~8.4.0 || ~8.5.0", "composer-runtime-api": "^2", - "typo3/cms-core": "^12.4.0 || ^13.4.0" + "typo3/cms-core": "^12.4.0 || ^13.4.0", + "typo3/cms-extbase": "^12.4.0 || ^13.4.0" }, "require-dev": { "ext-json": "*", From e6faeadf36e25596134398c22a6281a72d2be500 Mon Sep 17 00:00:00 2001 From: Lamm Date: Fri, 23 Jan 2026 16:33:22 +0100 Subject: [PATCH 4/5] Fix exception not found in TYPO3 v12 --- Classes/Service/CleanupSettingsService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/Service/CleanupSettingsService.php b/Classes/Service/CleanupSettingsService.php index ec6645b..dad240c 100644 --- a/Classes/Service/CleanupSettingsService.php +++ b/Classes/Service/CleanupSettingsService.php @@ -4,8 +4,8 @@ namespace Pluswerk\MailLogger\Service; +use RuntimeException; use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; -use TYPO3\CMS\Extbase\Configuration\Exception\NoServerRequestGivenException; class CleanupSettingsService { @@ -67,7 +67,7 @@ private function loadSettings(): void $fullSettings = $this->configurationManager->getConfiguration( ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT ); - } catch (NoServerRequestGivenException) { + } catch (RuntimeException) { // Some rare cases have no server request available yet. // Keep defaults and mark as not loaded so callers can check via isLoaded(). return; From d108c7a6d5603a45946d7254803f83ec8b7c515b Mon Sep 17 00:00:00 2001 From: Lamm Date: Fri, 23 Jan 2026 16:50:31 +0100 Subject: [PATCH 5/5] Simplification after code review --- .../EventListener/CleanupEventListener.php | 20 +------------- Classes/Service/CleanupService.php | 26 +++++++++---------- Configuration/Services.yaml | 2 -- 3 files changed, 13 insertions(+), 35 deletions(-) diff --git a/Classes/EventListener/CleanupEventListener.php b/Classes/EventListener/CleanupEventListener.php index c98d265..6b077be 100644 --- a/Classes/EventListener/CleanupEventListener.php +++ b/Classes/EventListener/CleanupEventListener.php @@ -6,8 +6,6 @@ use Pluswerk\MailLogger\Service\CleanupService; use Throwable; -use TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent; -use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent; /** * Event listener that triggers mail log cleanup on frontend and backend requests. @@ -20,23 +18,7 @@ public function __construct( ) { } - /** - * Triggered on frontend page render - */ - public function onFrontendRender(): void - { - $this->runCleanup(); - } - - /** - * Triggered on backend page render - */ - public function onBackendRender(): void - { - $this->runCleanup(); - } - - private function runCleanup(): void + public function __invoke(): void { try { $this->cleanupService->tryRunCleanup(); diff --git a/Classes/Service/CleanupService.php b/Classes/Service/CleanupService.php index 3c22938..c093b32 100644 --- a/Classes/Service/CleanupService.php +++ b/Classes/Service/CleanupService.php @@ -104,6 +104,7 @@ private function anonymizeAll(): void } $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME); + $namedParameterAnonymizeSymbol = $queryBuilder->createNamedParameter($anonymizeSymbol); $queryBuilder->getRestrictions()->removeAll(); $queryBuilder->update(self::TABLE_NAME) ->set('tstamp', time()) @@ -119,14 +120,14 @@ private function anonymizeAll(): void $queryBuilder->expr()->lte('crdate', $queryBuilder->createNamedParameter($timestamp)), // Skip already fully anonymized records $queryBuilder->expr()->or( - $queryBuilder->expr()->neq('subject', $queryBuilder->createNamedParameter($anonymizeSymbol)), - $queryBuilder->expr()->neq('message', $queryBuilder->createNamedParameter($anonymizeSymbol)), - $queryBuilder->expr()->neq('mail_from', $queryBuilder->createNamedParameter($anonymizeSymbol)), - $queryBuilder->expr()->neq('mail_to', $queryBuilder->createNamedParameter($anonymizeSymbol)), - $queryBuilder->expr()->neq('mail_copy', $queryBuilder->createNamedParameter($anonymizeSymbol)), - $queryBuilder->expr()->neq('mail_blind_copy', $queryBuilder->createNamedParameter($anonymizeSymbol)), - $queryBuilder->expr()->neq('headers', $queryBuilder->createNamedParameter($anonymizeSymbol)), - $queryBuilder->expr()->neq('debug', $queryBuilder->createNamedParameter($anonymizeSymbol)), + $queryBuilder->expr()->neq('subject', $namedParameterAnonymizeSymbol), + $queryBuilder->expr()->neq('message', $namedParameterAnonymizeSymbol), + $queryBuilder->expr()->neq('mail_from', $namedParameterAnonymizeSymbol), + $queryBuilder->expr()->neq('mail_to', $namedParameterAnonymizeSymbol), + $queryBuilder->expr()->neq('mail_copy', $namedParameterAnonymizeSymbol), + $queryBuilder->expr()->neq('mail_blind_copy', $namedParameterAnonymizeSymbol), + $queryBuilder->expr()->neq('headers', $namedParameterAnonymizeSymbol), + $queryBuilder->expr()->neq('debug', $namedParameterAnonymizeSymbol), ), ) ->executeStatement(); @@ -134,11 +135,8 @@ private function anonymizeAll(): void private function getCleanupMinInterval(): int { - try { - $config = $this->extensionConfiguration->get('mail_logger'); - return (int)($config['cleanupMinInterval'] ?? 3600); - } catch (Exception) { - return 3600; - } + /** @noinspection PhpUnhandledExceptionInspection */ + $config = $this->extensionConfiguration->get('mail_logger'); + return (int)($config['cleanupMinInterval'] ?? 3600); } } diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index b17b8be..6df6151 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -40,9 +40,7 @@ services: tags: - name: event.listener identifier: 'mail-logger/cleanup-frontend' - method: 'onFrontendRender' event: TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent - name: event.listener identifier: 'mail-logger/cleanup-backend' - method: 'onBackendRender' event: TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent