Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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();
}
}
30 changes: 30 additions & 0 deletions Classes/EventListener/CleanupEventListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Pluswerk\MailLogger\EventListener;

use Pluswerk\MailLogger\Service\CleanupService;
use Throwable;

/**
* 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,
) {
}

public function __invoke(): 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,
);
}
}
11 changes: 7 additions & 4 deletions Classes/Logging/MailerExtender.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Loading