Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .ddev/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: <projectname> # Name of the project, automatically provides
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
133 changes: 30 additions & 103 deletions Classes/Domain/Repository/MailLogRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}

Expand All @@ -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
Expand All @@ -148,6 +66,9 @@ public function add($mailLog): void

/**
* @param MailLog $mailLog
* @throws UnknownObjectException
* @throws IllegalObjectTypeException
* @noinspection PhpParameterNameChangedDuringInheritanceInspection
*/
#[Override]
public function update($mailLog): void
Expand All @@ -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();
}
}
48 changes: 48 additions & 0 deletions Classes/EventListener/CleanupEventListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Pluswerk\MailLogger\EventListener;

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.
* Uses cache-based locking to prevent parallel runs and throttle execution.
*/
final readonly class CleanupEventListener
{
public function __construct(
private CleanupService $cleanupService,
) {
}

/**
* 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
{
try {
$this->cleanupService->tryRunCleanup();
} catch (Throwable) {
// Silently ignore to not break page rendering
// CleanupService already logs the error
}
}
}
49 changes: 24 additions & 25 deletions Classes/Logging/LoggingTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,36 +29,35 @@
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]
public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage
{
$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;
Expand Down Expand Up @@ -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());
}
}

Expand Down
33 changes: 33 additions & 0 deletions Classes/Logging/LoggingTransportFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Pluswerk\MailLogger\Logging;

use Pluswerk\MailLogger\Domain\Model\MailLog;
use Pluswerk\MailLogger\Domain\Repository\MailLogRepository;
use Symfony\Component\Mailer\Transport\TransportInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager;

/**
* Factory for creating LoggingTransport instances with proper dependency injection.
*/
class LoggingTransportFactory
{
public function __construct(
protected MailLogRepository $mailLogRepository,
protected PersistenceManager $persistenceManager,
protected MailLog $mailLog,
) {
}

public function create(TransportInterface $originalTransport): LoggingTransport
{
return new LoggingTransport(
$originalTransport,
$this->mailLogRepository,
$this->persistenceManager,
$this->mailLog,
);
}
}
Loading