diff --git a/.github/workflows/tasks.yml b/.github/workflows/tasks.yml index 479fa45..98cf687 100644 --- a/.github/workflows/tasks.yml +++ b/.github/workflows/tasks.yml @@ -10,20 +10,12 @@ jobs: fail-fast: false matrix: php: [ '8.3', '8.4', '8.5' ] - typo3: [ '12', '13' ] - exclude: - - php: '8.5' - typo3: '12' + typo3: [ '13' ] steps: - name: Setup PHP with PECL extension uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - - uses: mirromutth/mysql-action@v1.1 - with: - mysql version: '8.0' - mysql database: 'db' - mysql root password: 'root' - uses: actions/checkout@v3 - uses: actions/cache@v3 with: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..390c73e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +All notable changes to this extension are documented in this file. + +## Removed + +- Removed the legacy `StandaloneView` APIs from `TemplateBasedMailMessage`: `getMessageView()`, `setMessageView()`, `getSubjectView()`, and `setSubjectView()`. + Configure message and subject templates through the mail-template and TypoScript configuration instead. +- Removed MailUtility::getMailById, use MailUtility::getMailByKey instead diff --git a/Classes/Command/TestMailCommand.php b/Classes/Command/TestMailCommand.php index d0b7a71..77656af 100644 --- a/Classes/Command/TestMailCommand.php +++ b/Classes/Command/TestMailCommand.php @@ -11,8 +11,6 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; -use TYPO3\CMS\Core\Http\ServerRequest; -use TYPO3\CMS\Core\Mail\MailerInterface; use TYPO3\CMS\Core\Mail\MailMessage; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -34,9 +32,6 @@ protected function configure(): void #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - // TYPO3 request needed for ConfigurationManager to work. fake it as backend request here - $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); - $args = $input->getArguments(); if ($args['templatekey'] !== '') { $mail = MailUtility::getMailByKey($args['templatekey']); diff --git a/Classes/Domain/Model/TemplateBasedMailMessage.php b/Classes/Domain/Model/TemplateBasedMailMessage.php index 1647d5b..eb5de32 100644 --- a/Classes/Domain/Model/TemplateBasedMailMessage.php +++ b/Classes/Domain/Model/TemplateBasedMailMessage.php @@ -4,15 +4,15 @@ namespace Pluswerk\MailLogger\Domain\Model; -use Override; use Exception; +use Override; use Pluswerk\MailLogger\Utility\ConfigurationUtility; use Symfony\Component\Mime\Crypto\DkimSigner; use TYPO3\CMS\Core\Mail\MailMessage; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Fluid\View\StandaloneView; - -use function array_filter; +use TYPO3\CMS\Core\View\ViewFactoryData; +use TYPO3\CMS\Core\View\ViewFactoryInterface; +use TYPO3\CMS\Fluid\View\FluidViewAdapter; class TemplateBasedMailMessage extends MailMessage { @@ -25,9 +25,29 @@ class TemplateBasedMailMessage extends MailMessage protected string $typoScriptKey = ''; + protected ?string $messageTemplatePathAndFilename = null; + + /** + * @var array + */ + protected array $partialRootPaths = []; + + /** + * @var array + */ + protected array $layoutRootPaths = []; + + /** + * @var array + */ + protected array $templateSettings = []; + + protected string $subjectTemplateSource = ''; + + protected string $messageTemplateSource = ''; + public function __construct( - protected StandaloneView $messageView, - protected StandaloneView $subjectView + protected readonly ViewFactoryInterface $viewFactory, ) { parent::__construct(); } @@ -39,7 +59,6 @@ public function getMailTemplate(): MailTemplate /** * @param array $viewParameters This is necessary if you use Fluid for your mail fields - * @return $this */ public function setMailTemplate(MailTemplate $mailTemplate, bool $assignMailTemplate = true, array $viewParameters = []): self { @@ -55,28 +74,6 @@ public function setMailTemplate(MailTemplate $mailTemplate, bool $assignMailTemp return $this; } - public function getMessageView(): StandaloneView - { - return $this->messageView; - } - - public function setMessageView(StandaloneView $messageView): self - { - $this->messageView = $messageView; - return $this; - } - - public function getSubjectView(): StandaloneView - { - return $this->subjectView; - } - - public function setSubjectView(StandaloneView $subjectView): self - { - $this->subjectView = $subjectView; - return $this; - } - /** * @return array */ @@ -115,14 +112,13 @@ public function assignDefaultsFromTypoScript(string $typoScriptKey, string $temp public function send(): bool { try { - $body = $this->renderView($this->messageView); - $this->html($body); + $this->html($this->renderMessageBody()); } catch (Exception $exception) { throw new Exception('Error while setting mail body template: ' . $exception->getMessage(), 1449133006, $exception); } try { - $this->setSubject($this->renderView($this->subjectView)); + $this->setSubject($this->renderSubject()); } catch (Exception $exception) { throw new Exception('Error while setting mail subject template: ' . $exception->getMessage(), 1449133007, $exception); } @@ -134,10 +130,9 @@ public function send(): bool private function signMail(): void { $settings = ConfigurationUtility::getCurrentModuleConfiguration('settings'); - if (isset($settings['dkim']) && isset($settings['dkim'][$this->mailTemplate->getDkimKey()])) { + if (isset($settings['dkim'][$this->mailTemplate->getDkimKey()])) { $conf = $settings['dkim'][$this->mailTemplate->getDkimKey()]; - // needs testing: $signer = new DkimSigner($this->formPrivateKey($conf['key']), $conf['domain'], $conf['selector'], [ 'headers_to_ignore' => [ 'Return-Path', @@ -149,7 +144,6 @@ private function signMail(): void } } - private function formPrivateKey(string $key): string { $begin = '-----BEGIN RSA PRIVATE KEY-----'; @@ -166,7 +160,6 @@ private function assignMailTemplateValues(array $values): void $this->assignDefaultsFromTypoScript($values['typoScriptKey'], $this->mailTemplate->getTemplatePathKey()); } - // set From $fromAddress = $this->getRenderedValue($values['mailFromAddress'] ?? ''); if ($fromAddress !== '') { $fromName = $this->getRenderedValue($values['mailFromName'] ?? ''); @@ -174,7 +167,6 @@ private function assignMailTemplateValues(array $values): void $this->setFrom($this->cleanUpMailAddressesAndNames([$fromAddress => $fromName])); } - // set To $toAddresses = GeneralUtility::trimExplode(',', $this->getRenderedValue($values['mailToAddresses'] ?? ''), true); if ($toAddresses !== []) { $toNames = GeneralUtility::trimExplode(',', $this->getRenderedValue($values['mailToNames'] ?? '')); @@ -186,7 +178,6 @@ private function assignMailTemplateValues(array $values): void $this->setTo($this->cleanUpMailAddressesAndNames($combinedTo)); } - // set CC and BCC if (!empty($values['mailCopyAddresses'])) { $this->setCc(GeneralUtility::trimExplode(',', $this->getRenderedValue($values['mailCopyAddresses']), true)); } @@ -197,19 +188,13 @@ private function assignMailTemplateValues(array $values): void $this->assignMailTemplatePaths($values); - // set subject and message if (!empty($values['subject'])) { - $this->subjectView->setTemplateSource($values['subject']); + $this->subjectTemplateSource = $values['subject']; } if (!empty($values['message'])) { - $mailView = GeneralUtility::makeInstance(StandaloneView::class); - $mailView->setTemplateSource($values['message']); - $mailView->assignMultiple($this->viewParameters); - $this->messageView->assign('message', $mailView->render()); + $this->messageTemplateSource = $values['message']; } - - $this->messageView->assign('mailTemplate', $this->mailTemplate); } /** @@ -228,28 +213,55 @@ private function cleanUpMailAddressesAndNames(array $addressesAndNames): array return $addressesAndNames; } - /** - * Short method to render a standalone fluid template - */ private function getRenderedValue(string $value): string { - // Check if the string is not empty and contains any Fluid stuff if ($value !== '' && (str_contains($value, '{') || str_contains($value, '<'))) { - /** @var StandaloneView $valueView */ - $valueView = GeneralUtility::makeInstance(StandaloneView::class); - $valueView->setTemplateSource($value); - $value = $this->renderView($valueView); + return $this->renderTemplateSource($value, $this->viewParameters); } return $value; } + private function renderMessageBody(): string + { + $variables = $this->viewParameters; + $variables['mailTemplate'] = $this->mailTemplate; + if ($this->messageTemplateSource !== '') { + $variables['message'] = $this->renderTemplateSource($this->messageTemplateSource, $this->viewParameters); + } + + $view = $this->viewFactory->create(new ViewFactoryData( + partialRootPaths: $this->partialRootPaths, + layoutRootPaths: $this->layoutRootPaths, + templatePathAndFilename: $this->messageTemplatePathAndFilename + )); + $view->assignMultiple($variables); + if ($this->templateSettings !== []) { + $view->assign('settings', $this->templateSettings); + } + + return $view->render(); + } + + private function renderSubject(): string + { + return $this->subjectTemplateSource !== '' + ? $this->renderTemplateSource($this->subjectTemplateSource, $this->viewParameters) + : $this->getSubject(); + } + /** - * Render view with all parameters + * @param array $variables */ - private function renderView(StandaloneView $view): string + private function renderTemplateSource(string $templateSource, array $variables): string { - return $view->assignMultiple($this->viewParameters)->render(); + $view = $this->viewFactory->create(new ViewFactoryData()); + if (!$view instanceof FluidViewAdapter) { + throw new Exception('TemplateBasedMailMessage requires a Fluid view adapter.', 1720965301); + } + + $view->getRenderingContext()->getTemplatePaths()->setTemplateSource($templateSource); + return $view->assignMultiple($variables)->render(); } /** @@ -257,33 +269,47 @@ private function renderView(StandaloneView $view): string */ private function assignMailTemplatePaths(array $values): void { - if (!$this->messageView->getPartialRootPaths()) { - $this->messageView->setPartialRootPaths( - array_filter( - [ - $values['defaultTemplatePaths']['partialRootPaths'], - $values['templatePaths']['partialRootPaths'] ?? [], - ] - ) + if ($this->messageTemplatePathAndFilename === null) { + $this->partialRootPaths = $this->normalizeRootPaths( + $values['defaultTemplatePaths']['partialRootPaths'] ?? [], + $values['templatePaths']['partialRootPaths'] ?? [], ); - - $this->messageView->setTemplatePathAndFilename( - GeneralUtility::getFileAbsFileName($values['templatePaths']['templatePath'] ?: $values['defaultTemplatePaths']['templatePath']) + $this->layoutRootPaths = $this->normalizeRootPaths( + $values['defaultTemplatePaths']['layoutRootPaths'] ?? [], + $values['templatePaths']['layoutRootPaths'] ?? [], ); + $templatePath = $values['templatePaths']['templatePath'] ?: $values['defaultTemplatePaths']['templatePath']; + $this->messageTemplatePathAndFilename = GeneralUtility::getFileAbsFileName($templatePath); + $this->templateSettings = is_array($values['templatePaths']['settings'] ?? null) + ? $values['templatePaths']['settings'] + : []; + } + } - $this->messageView->setLayoutRootPaths( - array_filter( - [ - $values['defaultTemplatePaths']['layoutRootPaths'], - $values['templatePaths']['layoutRootPaths'] ?? [], - ] - ) - ); + /** + * @return array + */ + private function normalizeRootPaths(mixed ...$paths): array + { + $normalizedPaths = []; + foreach ($paths as $path) { + if (is_string($path) && $path !== '') { + $normalizedPaths[] = $path; + continue; + } - if (isset($values['templatePaths']['settings']) && !empty($values['templatePaths']['settings'])) { - $this->messageView->assignMultiple(['settings' => $values['templatePaths']['settings']]); + if (!is_array($path)) { + continue; + } + + foreach ($path as $key => $value) { + if (is_string($value) && $value !== '') { + $normalizedPaths[$key] = $value; + } } } + + return $normalizedPaths; } public function getTypoScriptKey(): string diff --git a/Classes/Domain/Repository/MailLogRepository.php b/Classes/Domain/Repository/MailLogRepository.php index bcfca63..a2583a0 100644 --- a/Classes/Domain/Repository/MailLogRepository.php +++ b/Classes/Domain/Repository/MailLogRepository.php @@ -21,6 +21,8 @@ */ class MailLogRepository extends Repository { + private const string CORRELATION_HEADER = 'X-Mail-Logger-Correlation-Id'; + protected $defaultOrderings = [ 'crdate' => QueryInterface::ORDER_DESCENDING, ]; @@ -130,4 +132,18 @@ public function getAnonymizeAfter(): string { return $this->cleanupSettingsService->getAnonymizeAfter(); } + + public function findByCorrelationId(string $correlationId): ?MailLog + { + $query = $this->createQuery(); + $query->matching( + $query->like( + 'headers', + '%' . self::CORRELATION_HEADER . ': ' . $correlationId . '%', + ), + ); + + $mailLog = $query->execute()->getFirst(); + return $mailLog instanceof MailLog ? $mailLog : null; + } } diff --git a/Classes/Logging/LoggingDelayedTransport.php b/Classes/Logging/LoggingDelayedTransport.php new file mode 100644 index 0000000..81847bf --- /dev/null +++ b/Classes/Logging/LoggingDelayedTransport.php @@ -0,0 +1,49 @@ +getOriginalTransport(); + if (!$originalTransport instanceof DelayedTransportInterface) { + throw new TransportException('The wrapped mailer transport is not a delayed transport and cannot flush a queue.', 1720965302); + } + + return $originalTransport->flushQueue($transport); + } + + public function recover(int $timeout = 900): void + { + $originalTransport = $this->getOriginalTransport(); + if ($originalTransport instanceof FileSpool) { + $originalTransport->recover($timeout); + } + } + + public function setMessageLimit(int $limit): void + { + $originalTransport = $this->getOriginalTransport(); + if ($originalTransport instanceof FileSpool) { + $originalTransport->setMessageLimit($limit); + } + } + + public function setTimeLimit(int $limit): void + { + $originalTransport = $this->getOriginalTransport(); + if ($originalTransport instanceof FileSpool) { + $originalTransport->setTimeLimit($limit); + } + } +} diff --git a/Classes/Logging/LoggingTransport.php b/Classes/Logging/LoggingTransport.php index abdae07..3b14864 100644 --- a/Classes/Logging/LoggingTransport.php +++ b/Classes/Logging/LoggingTransport.php @@ -5,7 +5,6 @@ namespace Pluswerk\MailLogger\Logging; use Override; -use Stringable; use Throwable; use Pluswerk\MailLogger\Domain\Model\MailLog; use Pluswerk\MailLogger\Domain\Model\TemplateBasedMailMessage; @@ -17,9 +16,9 @@ use Symfony\Component\Mailer\Transport\NullTransport; use Symfony\Component\Mailer\Transport\SendmailTransport; use Symfony\Component\Mailer\Transport\TransportInterface; -use Symfony\Component\Mailer\Transport\Transports; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\Message; use Symfony\Component\Mime\Part\AbstractMultipartPart; use Symfony\Component\Mime\Part\AbstractPart; use Symfony\Component\Mime\Part\TextPart; @@ -27,15 +26,17 @@ use TYPO3\CMS\Core\Mail\DelayedTransportInterface; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager; -use TYPO3\CMS\Extbase\Utility\DebuggerUtility; class LoggingTransport implements TransportInterface { + private const string CORRELATION_HEADER = 'X-Mail-Logger-Correlation-Id'; + + protected ?MailLog $mailLog = null; + public function __construct( protected TransportInterface $originalTransport, protected MailLogRepository $mailLogRepository, protected PersistenceManager $persistenceManager, - protected MailLog $mailLog, ) { } @@ -44,10 +45,18 @@ public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMess { $this->fixTcaIfNotPresentIsUsedInInstallTool(); - // write mail to log before send - $this->assignMailLog($message); - $this->mailLogRepository->add($this->mailLog); - $this->persistenceManager->persistAll(); + [$message, $correlationId] = $this->getOrCreateCorrelationId($message); + $this->mailLog = $correlationId !== null + ? $this->mailLogRepository->findByCorrelationId($correlationId) + : null; + + if (!$this->mailLog instanceof MailLog) { + // Write a new mail log before sending or queueing the message. + $this->mailLog = GeneralUtility::makeInstance(MailLog::class); + $this->assignMailLog($message); + $this->mailLogRepository->add($this->mailLog); + $this->persistenceManager->persistAll(); + } $sendResult = $this->originalSend($message, $envelope); @@ -66,6 +75,50 @@ public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMess return $sendResult->sentMessage; } + /** + * @return array{RawMessage, string} + */ + private function getOrCreateCorrelationId(RawMessage $message): array + { + if ($message instanceof Message) { + $headers = $message->getHeaders(); + $header = $headers->get(self::CORRELATION_HEADER); + if ($header !== null) { + return [$message, $header->getBodyAsString()]; + } + + $correlationId = bin2hex(random_bytes(16)); + $headers->addTextHeader(self::CORRELATION_HEADER, $correlationId); + + return [$message, $correlationId]; + } + + $rawMessage = $message->toString(); + $correlationId = $this->getCorrelationIdFromRawMessage($rawMessage); + if ($correlationId !== null) { + return [$message, $correlationId]; + } + + $correlationId = bin2hex(random_bytes(16)); + + return [ + new RawMessage(self::CORRELATION_HEADER . ': ' . $correlationId . "\r\n" . $rawMessage), + $correlationId, + ]; + } + + private function getCorrelationIdFromRawMessage(string $rawMessage): ?string + { + $headerBlock = preg_split("/\r?\n\r?\n/", $rawMessage, 2)[0] ?? ''; + $unfoldedHeaders = preg_replace("/\r?\n[ \t]+/", ' ', $headerBlock) ?? $headerBlock; + + if (preg_match('/^' . preg_quote(self::CORRELATION_HEADER, '/') . ':\s*(.+)$/im', $unfoldedHeaders, $matches) !== 1) { + return null; + } + + return trim($matches[1]); + } + public function getOriginalTransport(): TransportInterface { return $this->originalTransport; @@ -163,6 +216,10 @@ protected function addressesToString(array $addresses): string ); } + /** + * @deprecated + * TODO Seems in v13, installtool ignores mail-logger extension. + */ protected function fixTcaIfNotPresentIsUsedInInstallTool(): void { if (empty($GLOBALS['TCA']['tx_maillogger_domain_model_maillog'])) { diff --git a/Classes/Logging/LoggingTransportFactory.php b/Classes/Logging/LoggingTransportFactory.php index 50acb1f..a82dc36 100644 --- a/Classes/Logging/LoggingTransportFactory.php +++ b/Classes/Logging/LoggingTransportFactory.php @@ -4,9 +4,9 @@ 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\Core\Mail\DelayedTransportInterface; use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager; /** @@ -17,17 +17,23 @@ class LoggingTransportFactory public function __construct( protected MailLogRepository $mailLogRepository, protected PersistenceManager $persistenceManager, - protected MailLog $mailLog, ) { } public function create(TransportInterface $originalTransport): LoggingTransport { + if ($originalTransport instanceof DelayedTransportInterface) { + return new LoggingDelayedTransport( + $originalTransport, + $this->mailLogRepository, + $this->persistenceManager, + ); + } + return new LoggingTransport( $originalTransport, $this->mailLogRepository, $this->persistenceManager, - $this->mailLog, ); } } diff --git a/Classes/Logging/MailerExtender.php b/Classes/Logging/MailerExtender.php index a66222d..39320c6 100644 --- a/Classes/Logging/MailerExtender.php +++ b/Classes/Logging/MailerExtender.php @@ -7,6 +7,7 @@ use Override; use Psr\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Mailer\Transport\TransportInterface; +use TYPO3\CMS\Core\Mail\DelayedTransportInterface; use TYPO3\CMS\Core\Mail\Mailer; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -27,6 +28,19 @@ public function __construct( $this->transport = $this->loggingTransportFactory->create($this->transport); } + #[Override] + public function getTransport(): TransportInterface + { + if ( + $this->transport instanceof LoggingTransport + && $this->transport->getOriginalTransport() instanceof DelayedTransportInterface + ) { + return $this->transport->getOriginalTransport(); + } + + return parent::getTransport(); + } + #[Override] public function getRealTransport(): TransportInterface { diff --git a/Classes/Service/CleanupSettingsService.php b/Classes/Service/CleanupSettingsService.php index dad240c..eddd1f9 100644 --- a/Classes/Service/CleanupSettingsService.php +++ b/Classes/Service/CleanupSettingsService.php @@ -4,8 +4,8 @@ namespace Pluswerk\MailLogger\Service; +use Pluswerk\MailLogger\Utility\ConfigurationUtility; use RuntimeException; -use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; class CleanupSettingsService { @@ -24,7 +24,7 @@ class CleanupSettingsService private string $anonymizeAfter = self::DEFAULT_ANONYMIZE_AFTER; public function __construct( - private readonly ConfigurationManagerInterface $configurationManager, + private readonly ConfigurationUtility $configurationUtility, ) { } @@ -64,20 +64,16 @@ private function loadSettings(): void } try { - $fullSettings = $this->configurationManager->getConfiguration( - ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT - ); + $settings = $this->configurationUtility->getConfiguration('settings'); } 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; + $settings = []; } - $settings = $fullSettings['module.']['tx_maillogger.']['settings.'] ?? []; + $cleanupSettings = $settings['cleanup'] ?? []; - $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->lifetime = $cleanupSettings['lifetime'] ?? self::DEFAULT_LIFETIME; + $this->anonymize = (bool)($cleanupSettings['anonymize'] ?? true); + $this->anonymizeAfter = $cleanupSettings['anonymizeAfter'] ?? self::DEFAULT_ANONYMIZE_AFTER; $this->loaded = true; } diff --git a/Classes/Utility/ConfigurationUtility.php b/Classes/Utility/ConfigurationUtility.php index 261dcc4..f98896b 100644 --- a/Classes/Utility/ConfigurationUtility.php +++ b/Classes/Utility/ConfigurationUtility.php @@ -4,10 +4,10 @@ namespace Pluswerk\MailLogger\Utility; -use Exception; -use ReflectionException; -use ReflectionMethod; +use Psr\Http\Message\ServerRequestInterface; use RuntimeException; +use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; +use TYPO3\CMS\Core\Http\ServerRequestFactory; use TYPO3\CMS\Core\TypoScript\TypoScriptService; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Configuration\BackendConfigurationManager; @@ -25,7 +25,7 @@ public function __construct( /** * @return array - * @throws ReflectionException + * @throws RuntimeException */ public static function getCurrentModuleConfiguration(string $key): array { @@ -34,27 +34,15 @@ public static function getCurrentModuleConfiguration(string $key): array /** * @return array - * @throws ReflectionException + * @throws RuntimeException */ public function getConfiguration(string $key): array { if (!self::$currentModuleConfiguration) { // we always use the BackendConfigurationManager, because flux is overwriting the ConfigurationManager // and always uses the FrontendConfigurationManager instead of the correct one for the current context - - // TYPO3 v13 requires $request parameter, v12 does not accept it - $reflection = new ReflectionMethod($this->backendConfigurationManager, 'getTypoScriptSetup'); - if ($reflection->getNumberOfParameters() > 0) { - // v13: pass request parameter - $request = $GLOBALS['TYPO3_REQUEST'] ?? throw new Exception('No $GLOBALS[\'TYPO3_REQUEST\'] found', 1688138054); - /** @phpstan-ignore-next-line arguments.count - v13 compatibility */ - $fullTypoScript = $this->backendConfigurationManager->getTypoScriptSetup($request); - } else { - // v12: no parameters - /** @phpstan-ignore-next-line arguments.count - v12 compatibility */ - $fullTypoScript = $this->backendConfigurationManager->getTypoScriptSetup(); - } - + $request = $this->getRequest(); + $fullTypoScript = $this->backendConfigurationManager->getTypoScriptSetup($request); if (empty($fullTypoScript['module.']['tx_maillogger.'])) { throw new RuntimeException('Constants and setup TypoScript are not included!', 7780827935); } @@ -64,4 +52,18 @@ public function getConfiguration(string $key): array return self::$currentModuleConfiguration[$key]; } + + /** + * @deprecated Will replace by SiteSet + */ + private function getRequest(): ServerRequestInterface + { + if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface) { + return $GLOBALS['TYPO3_REQUEST']; + } + + return GeneralUtility::makeInstance(ServerRequestFactory::class) + ->createServerRequest('GET', 'https://localhost/') + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + } } diff --git a/Classes/Utility/MailUtility.php b/Classes/Utility/MailUtility.php index c050e6a..722fbef 100644 --- a/Classes/Utility/MailUtility.php +++ b/Classes/Utility/MailUtility.php @@ -31,25 +31,4 @@ public static function getMailByKey(string $key, ?int $languageUid = null, array return $mail->setMailTemplate($mailTemplate, true, $viewParameters); } - - /** - * Shortcut to send mails - * \Pluswerk\MailLogger\Utility\MailUtility::getMailById($mailTemplateId, ['var' => $var])->send(); - * - * @param int $mailTemplateId The identifier uid of your template - * @param array $viewParameters This is necessary if you use Fluid for your mail fields - * @throws Exception - * @deprecated will be removed. use \Pluswerk\MailLogger\Utility\MailUtility::getMailByKey instead - */ - public static function getMailById(int $mailTemplateId, array $viewParameters = []): TemplateBasedMailMessage - { - $mail = GeneralUtility::makeInstance(TemplateBasedMailMessage::class); - $templateRepository = GeneralUtility::makeInstance(MailTemplateRepository::class); - $mailTemplate = $templateRepository->findByUid($mailTemplateId); - if (!$mailTemplate) { - throw new Exception('No "MailTemplate" was found for uid "' . $mailTemplateId . '". Please check your database records!', 6976725035); - } - - return $mail->setMailTemplate($mailTemplate, true, $viewParameters); - } } diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index f5189ca..f2274bb 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -8,6 +8,7 @@ services: resource: '../Classes' exclude: - '../Classes/Logging/LoggingTransport.php' + - '../Classes/Logging/LoggingDelayedTransport.php' - '../Classes/Dto/' Pluswerk\MailLogger\Domain\Model\: diff --git a/README.md b/README.md index 050ce4d..5917ed2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # EXT:mail_logger by anders und sehr GmbH -This is an TYPO3 extension with some mail functions: +This is a TYPO3 extension with some mail functions: 1. [E-mail logging](#1-e-mail-logging) 2. [E-mail templates](#2-e-mail-templates) @@ -15,17 +15,13 @@ Install via composer or just copy the files into the TYPO3 extension folder: composer require pluswerk/mail-logger ``` -Add the Typoscript files to your sites Typoscript: -- add `@import 'EXT:mail_logger/Configuration/TypoScript/constants.typoscript'` in your constants -- and `@import 'EXT:mail_logger/Configuration/TypoScript/setup.typoscript'` in your setup. - ## 1. E-mail logging -The extension automatically log all outgoing mails of the TYPO3 system, which are sent via the TYPO3 mail API. Just install the extension and it works. All outgoing mails can be found in the backend module of this TYPO3 mail logger. +The extension automatically logs all outgoing mails of the TYPO3 system that are sent via the TYPO3 mail API. Just install the extension and it works. All outgoing mails can be found in the backend module of this TYPO3 mail logger. -By default the maximum logging time of e-mails is 30 days and can be changed as following: +By default the maximum logging time of e-mails is 30 days and can be changed as follows: [see strtotime](http://php.net/manual/en/function.strtotime.php#refsect1-function.strtotime-examples) -The mails will be anonymized after 7 days by default. It can be changed to anonymize directly, by setting anonymizeAfter to 0. +The mails will be anonymized after 7 days by default. This can be changed to anonymize them immediately by setting `anonymizeAfter` to `0`. ```ts module.tx_maillogger.settings.cleanup { lifetime = 30 days @@ -41,11 +37,11 @@ You can configure TYPO3 e-mail templates, written in Fluid, which are editable f *How does this work?* E-mails will be basically configured in a TypoScript configuration (configuration of the sender address for example). Afterwards a database entry will be generated from the editor, which extends this template with additional information (fluid template or receiver for example). -The instance of such an e-mail can be extended or overridden afterwards via php in your own extension (for example: dynamic receiver). +The instance of such an e-mail can be extended or overridden afterwards via PHP in your own extension (for example: a dynamic receiver). ### TypoScript example -You always have to create a TypoScript template for a mail. The "label" is the only required field, the orher fields are optional. +You always have to create a TypoScript template for a mail. The "label" is the only required field; the other fields are optional. ```typo3_typoscript # E-mail template @@ -78,14 +74,9 @@ The message will be rendered by Fluid, so it is possible to print variables or u ``` -### sending E-mails via PHP - -E-mail instances "\\Pluswerk\\MailLogger\\Domain\\Model\\Mail\\TemplateBasedMailMessage" inherit from SwiftMailer class -"\\Swift\_Message". -Therefor an e-mail instance have got following functions: -The easiest way is to use the functions of the "\\Pluswerk\\MailLogger\\Utility\\MailUtility" class. +### Sending e-mails via PHP -##### basic sample: +##### Basic sample ```php 'This mail was sent at ' . time(), 'myUser' => $myExtbaseUser])->send(); ``` -#### example - passing E-mail parameters and sending attachment (FPDF for example) +#### Example: passing e-mail parameters and sending an attachment ```php Output($pdfFileName, 'S'); - $pdfFileAttachment = \Swift_Attachment::newInstance($pdfFileByteStream, $pdfFileName, 'application/pdf'); - $mail->attach($pdfFileAttachment); + $mail->attach($pdfFileByteStream, $pdfFileName, 'application/pdf'); $mail->send(); } catch (\Exception $e) { // handle error @@ -122,13 +112,13 @@ try { } ``` -You should always catch exceptions in your php code. Experience has shown that editors often don't add a template (or translation) etc. +You should always catch exceptions in your PHP code. Experience has shown that editors often don't add a template or translation. Corresponding errors should somehow be handled! ### Custom fluid templates -Sometimes you additionally want to wrap the mail templates from database with your own markup. +Sometimes you additionally want to wrap the mail templates from the database with your own markup. Therefore we provide the option to customize the mail for your needs via fluid. -Again - via Typoscript - you can configure a rendering definition for every mail template. +Again, via TypoScript, you can configure a rendering definition for every mail template. ```typo3_typoscript module.tx_maillogger.settings.templateOverrides { @@ -156,16 +146,16 @@ module.tx_maillogger.settings.templateOverrides {

This is my passed value: {settings.myValue}

``` -The Variables "message" and "mailTemplate" are automatically provided to your template. -You can use the actual message by simply wrapping it with a "f:format.raw"-viewhelper. +The variables `message` and `mailTemplate` are automatically provided to your template. +You can use the actual message by simply wrapping it with the `f:format.raw` ViewHelper. You can provide your own partial- and layout-paths for every template you add. Alternatively it will use the default paths provided by this extension. -You can add your own parameters to the template via "settings"-option. +You can add your own parameters to the template via the `settings` option. -### example - Use a e-mail template in your own plugin +### Example: use an e-mail template in your own plugin -If a mail template can be selected dynamically by the editor, you can integrate a Flexform in the plugin, +If a mail template can be selected dynamically by the editor, you can integrate a FlexForm in the plugin, adding the following configuration: ```xml @@ -185,19 +175,17 @@ adding the following configuration: ``` -### DKIM signing of mails (NOT SUPPORTED IN VERSION 2.0 until now) +### DKIM signing of mails -You can set a DKIM-signing for every mailtemplate you use for spam protection reasons. -Therefore you have to define typoscript keys which you can select in the backend of a mail template. +You can configure DKIM signing for every mail template. Define the DKIM keys in TypoScript and select the appropriate key in the backend mail-template record. -Please note that you have to strip "-----BEGIN RSA PRIVATE KEY-----" and "-----END RSA PRIVATE KEY-----", as they are added from php with special chars you don't want to type via typoscript. -So only paste your private keychain as key. +Please strip `-----BEGIN RSA PRIVATE KEY-----` and `-----END RSA PRIVATE KEY-----`; the extension adds these delimiters when signing the mail. Only paste the private key contents into TypoScript. -For an example regarding using DKIM signing and adding the TXT-records to your DNS you can visit [this article](https://support.rackspace.com/how-to/create-a-dkim-txt-record/) +For an example of using DKIM signing and adding the TXT records to your DNS, see [this article](https://support.rackspace.com/how-to/create-a-dkim-txt-record/). -Key: Your private key without "-----BEGIN RSA PRIVATE KEY-----" and "-----END RSA PRIVATE KEY-----" +Key: Your private key without `-----BEGIN RSA PRIVATE KEY-----` and `-----END RSA PRIVATE KEY-----` Domain: The domain from which you want to send your mail (e.g. info@example.com) -Selector will most likely remain "default". +The selector will most likely remain `default`. ```typo3_typoscript module.tx_maillogger.settings.dkim { diff --git a/Tests/Fixtures/TemplateBasedMailMessageTest/mail_templates.csv b/Tests/Fixtures/TemplateBasedMailMessageTest/mail_templates.csv new file mode 100644 index 0000000..2b7148b --- /dev/null +++ b/Tests/Fixtures/TemplateBasedMailMessageTest/mail_templates.csv @@ -0,0 +1,4 @@ +tx_maillogger_domain_model_mailtemplate,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,uid,pid,typo_script_key,dkim_key,template_path_key,title,subject,message,mail_from_name,mail_from_address,mail_to_names,mail_to_addresses,mail_copy_addresses,mail_blind_copy_addresses,tstamp,crdate,cruser_id,deleted,hidden,starttime,endtime,t3ver_oid,t3ver_id,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_count,t3ver_tstamp,t3ver_move_id,sys_language_uid,l10n_parent,l10n_diffsource +,1,1,functionalMail,,,Functional mail,,,,,,,,,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\NULL +,2,1,directSubjectMail,,,Direct subject mail,,,,,,,,,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\NULL diff --git a/Tests/Fixtures/TemplateBasedMailMessageTest/setup.typoscript b/Tests/Fixtures/TemplateBasedMailMessageTest/setup.typoscript new file mode 100644 index 0000000..e679dd8 --- /dev/null +++ b/Tests/Fixtures/TemplateBasedMailMessageTest/setup.typoscript @@ -0,0 +1,26 @@ +module.tx_maillogger.settings { + cleanup { + anonymize = 0 + anonymizeAfter = 0 + lifetime = 1 day + } + + mailTemplates { + functionalMail { + mailFromName = Sender {name} + mailFromAddress = sender@example.test + mailToNames = Receiver {name} + mailToAddresses = receiver@example.test + subject = Functional subject for {name} + message =

Rendered body for {name} with {color}

+ } + + directSubjectMail { + mailFromName = Sender + mailFromAddress = sender@example.test + mailToNames = Receiver + mailToAddresses = receiver@example.test + message =

Body with manually assigned subject for {name}

+ } + } +} diff --git a/Tests/Functional/Logging/DelayedTransportTest.php b/Tests/Functional/Logging/DelayedTransportTest.php new file mode 100644 index 0000000..2890644 --- /dev/null +++ b/Tests/Functional/Logging/DelayedTransportTest.php @@ -0,0 +1,113 @@ +spoolPath = Environment::getVarPath() . '/tests/mail-spool-' . bin2hex(random_bytes(8)); + } + + #[Override] + protected function tearDown(): void + { + if (is_dir($this->spoolPath)) { + GeneralUtility::rmdir($this->spoolPath, true); + } + + parent::tearDown(); + } + + public function testFlushingQueuedMailUpdatesExistingLogEntry(): void + { + $fileSpool = new FileSpool($this->spoolPath); + $fileSpool->setMessageLimit(0); + $fileSpool->setTimeLimit(0); + + $mailLogRepository = GeneralUtility::makeInstance(MailLogRepository::class); + $persistenceManager = GeneralUtility::makeInstance(PersistenceManager::class); + $delayedTransport = new LoggingDelayedTransport( + $fileSpool, + $mailLogRepository, + $persistenceManager, + ); + $realTransport = new LoggingTransport( + new NullTransport(), + $mailLogRepository, + $persistenceManager, + ); + + $email = (new Email()) + ->from('sender@example.test') + ->to('receiver@example.test') + ->subject('Delayed transport test') + ->html('

Body that must survive queue flushing.

'); + + $delayedTransport->send($email); + $persistenceManager->clearState(); + + $queuedLog = $this->getSingleMailLog(); + self::assertSame('Email queued', $queuedLog['result']); + self::assertSame(MailStatus::QUEUED->value, (int)$queuedLog['status']); + self::assertStringContainsString('Body that must survive queue flushing.', $queuedLog['message']); + self::assertStringContainsString('X-Mail-Logger-Correlation-Id:', $queuedLog['headers']); + + self::assertSame(MailStatus::SENT_OK->value, $delayedTransport->flushQueue($realTransport)); + $persistenceManager->clearState(); + + $sentLog = $this->getSingleMailLog(); + self::assertSame((int)$queuedLog['uid'], (int)$sentLog['uid']); + self::assertSame('Email Nulled (NullTransport)', $sentLog['result']); + self::assertSame(MailStatus::NOT_SENT->value, (int)$sentLog['status']); + self::assertSame($queuedLog['subject'], $sentLog['subject']); + self::assertSame($queuedLog['message'], $sentLog['message']); + self::assertSame($queuedLog['mail_from'], $sentLog['mail_from']); + self::assertSame($queuedLog['mail_to'], $sentLog['mail_to']); + } + + /** + * @return array + */ + private function getSingleMailLog(): array + { + $mailLogs = $this->getConnectionPool() + ->getConnectionForTable('tx_maillogger_domain_model_maillog') + ->select( + ['*'], + 'tx_maillogger_domain_model_maillog', + [], + [], + ['uid' => 'ASC'], + ) + ->fetchAllAssociative(); + + self::assertCount(1, $mailLogs); + + return $mailLogs[0]; + } +} diff --git a/Tests/Functional/MailLogRepository/AbstractMailLogRepositoryTest.php b/Tests/Functional/MailLogRepository/AbstractMailLogRepositoryTest.php index 1cf10a5..f8bb2c9 100644 --- a/Tests/Functional/MailLogRepository/AbstractMailLogRepositoryTest.php +++ b/Tests/Functional/MailLogRepository/AbstractMailLogRepositoryTest.php @@ -4,7 +4,9 @@ namespace Pluswerk\MailLogger\Tests\Functional\MailLogRepository; +use JsonException; use Override; +use PHPUnit\Framework\Attributes\Test; use Pluswerk\MailLogger\Service\CleanupService; use ReflectionObject; use DateTime; @@ -12,6 +14,7 @@ use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\CMS\Extbase\Persistence\Generic\Exception\NotImplementedException; +use TYPO3\CMS\Extbase\Persistence\QueryResultInterface; use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; use Pluswerk\MailLogger\Domain\Model\MailLog; use Pluswerk\MailLogger\Domain\Repository\MailLogRepository; @@ -35,15 +38,13 @@ protected function setUp(): void { parent::setUp(); $this->importCSVDataSet(__DIR__ . '/../../Fixtures/pages.csv'); - // TYPO3 request needed for ConfigurationManager to work. fake it as backend request here - $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); } public function testInitializeObject(): void { $mailLogRepository = GeneralUtility::makeInstance(MailLogRepository::class); - $this->assertMatchesJsonSnapshot( + self::assertMatchesJsonSnapshot( json_encode( [ 'lifetime' => $mailLogRepository->getLifetime(), @@ -60,7 +61,7 @@ public function testAdd(): void { $mailLog = $this->createAndSaveMailLog(558); - $this->assertModelSnapshot($mailLog); + self::assertModelSnapshot($mailLog); } public function testUpdate(): void @@ -69,7 +70,7 @@ public function testUpdate(): void $mailLog = $this->updatingMailLog($mailLog); - $this->assertModelSnapshot($mailLog); + self::assertModelSnapshot($mailLog); } public function testUpdateWithDelayAnonymize(): void @@ -80,9 +81,16 @@ public function testUpdateWithDelayAnonymize(): void $mailLog = $this->updatingMailLog($mailLog); - $this->assertModelSnapshot($mailLog); + self::assertModelSnapshot($mailLog); } + /** + * Assures old entries get deleted. + * + * @throws NotImplementedException + * @throws JsonException + */ + #[Test] public function testCleanupDatabase(): void { $this->createAndSaveMailLog(789); @@ -96,9 +104,10 @@ public function testCleanupDatabase(): void $this->cleanupDatabasePart($persistenceManager); - /** @var MailLog $mailLog */ - $mailLog = $mailLogRepository->findAll()->getFirst(); - $this->assertModelSnapshot($mailLog); + $result = $mailLogRepository->findAll(); + self::assertInstanceOf(QueryResultInterface::class, $result); + $mailLog = $result->getFirst(); + self::assertModelSnapshot($mailLog); } public function testAnonymizeAll(): void @@ -119,7 +128,7 @@ public function testAnonymizeAll(): void /** @var MailLog $mailLog */ $mailLog = $mailLogRepository->findAll()->getFirst(); - $this->assertModelSnapshot($mailLog); + self::assertModelSnapshot($mailLog); } protected function getNewMailLog(int $seed): MailLog @@ -206,7 +215,7 @@ protected function assertModelSnapshot(?MailLog $model): void ksort($data); } - $this->assertMatchesJsonSnapshot(json_encode($data, JSON_THROW_ON_ERROR)); + self::assertMatchesJsonSnapshot(json_encode($data, JSON_THROW_ON_ERROR)); } /** diff --git a/Tests/Functional/MailLogRepository/__snapshots__/Anonymize7daysLifeTime30daysTest__testAdd__1.json b/Tests/Functional/MailLogRepository/__snapshots__/Anonymize7daysLifeTime30daysTest__testAdd__1.json index 9ebd864..af34c51 100644 --- a/Tests/Functional/MailLogRepository/__snapshots__/Anonymize7daysLifeTime30daysTest__testAdd__1.json +++ b/Tests/Functional/MailLogRepository/__snapshots__/Anonymize7daysLifeTime30daysTest__testAdd__1.json @@ -1,16 +1,16 @@ { - "uid": 1, - "pid": 1, - "typoScriptKey": "typoscriptKey558", - "subject": "subject558", - "message": "message558", + "debug": "", + "headers": "headers558", + "mailBlindCopy": "mail558@test.test", + "mailCopy": "mail558@test.test", "mailFrom": "mail558@test.test", "mailTo": "mail558@test.test", - "mailCopy": "mail558@test.test", - "mailBlindCopy": "mail558@test.test", - "headers": "headers558", + "message": "message558", + "pid": 0, "result": "Not send until now", "status": 0, - "debug": "", - "sysLanguageUid": 0 + "subject": "subject558", + "sysLanguageUid": 0, + "typoScriptKey": "typoscriptKey558", + "uid": 1 } diff --git a/Tests/Functional/MailLogRepository/__snapshots__/Anonymize7daysLifeTime30daysTest__testAnonymizeAll__1.json b/Tests/Functional/MailLogRepository/__snapshots__/Anonymize7daysLifeTime30daysTest__testAnonymizeAll__1.json index 11b7975..63be092 100644 --- a/Tests/Functional/MailLogRepository/__snapshots__/Anonymize7daysLifeTime30daysTest__testAnonymizeAll__1.json +++ b/Tests/Functional/MailLogRepository/__snapshots__/Anonymize7daysLifeTime30daysTest__testAnonymizeAll__1.json @@ -1,16 +1,16 @@ { - "uid": 1, - "pid": 1, - "typoScriptKey": "typoscriptKey7894", - "subject": "***", - "message": "***", + "debug": "***", + "headers": "***", + "mailBlindCopy": "***", + "mailCopy": "***", "mailFrom": "***", "mailTo": "***", - "mailCopy": "***", - "mailBlindCopy": "***", - "headers": "***", + "message": "***", + "pid": 0, "result": "Not send until now", "status": 0, - "debug": "***", - "sysLanguageUid": 0 + "subject": "***", + "sysLanguageUid": 0, + "typoScriptKey": "typoscriptKey7894", + "uid": 1 } diff --git a/Tests/Functional/MailLogRepository/__snapshots__/Anonymize7daysLifeTime30daysTest__testUpdateWithDelayAnonymize__1.json b/Tests/Functional/MailLogRepository/__snapshots__/Anonymize7daysLifeTime30daysTest__testUpdateWithDelayAnonymize__1.json index 051a70d..93f5f01 100644 --- a/Tests/Functional/MailLogRepository/__snapshots__/Anonymize7daysLifeTime30daysTest__testUpdateWithDelayAnonymize__1.json +++ b/Tests/Functional/MailLogRepository/__snapshots__/Anonymize7daysLifeTime30daysTest__testUpdateWithDelayAnonymize__1.json @@ -1,16 +1,16 @@ { - "uid": 1, - "pid": 1, - "typoScriptKey": "typoscriptKey2345", - "subject": "***", - "message": "***", + "debug": "", + "headers": "***", + "mailBlindCopy": "***", + "mailCopy": "***", "mailFrom": "***", "mailTo": "***", - "mailCopy": "***", - "mailBlindCopy": "***", - "headers": "***", + "message": "***", + "pid": 0, "result": "Not send until now", "status": 0, - "debug": "", - "sysLanguageUid": 0 + "subject": "***", + "sysLanguageUid": 0, + "typoScriptKey": "typoscriptKey2345", + "uid": 1 } diff --git a/Tests/Functional/MailLogRepository/__snapshots__/Anonymize7daysLifeTime30daysTest__testUpdate__1.json b/Tests/Functional/MailLogRepository/__snapshots__/Anonymize7daysLifeTime30daysTest__testUpdate__1.json index db299cd..5ba3505 100644 --- a/Tests/Functional/MailLogRepository/__snapshots__/Anonymize7daysLifeTime30daysTest__testUpdate__1.json +++ b/Tests/Functional/MailLogRepository/__snapshots__/Anonymize7daysLifeTime30daysTest__testUpdate__1.json @@ -1,16 +1,16 @@ { - "uid": 1, - "pid": 1, - "typoScriptKey": "typoscriptKey555", - "subject": "subject555", - "message": "message555", + "debug": "", + "headers": "headers555", + "mailBlindCopy": "mail555@test.test", + "mailCopy": "mail555@test.test", "mailFrom": "mail555@test.test", "mailTo": "mail555@test.test", - "mailCopy": "mail555@test.test", - "mailBlindCopy": "mail555@test.test", - "headers": "headers555", + "message": "message555", + "pid": 0, "result": "Not send until now", "status": 0, - "debug": "", - "sysLanguageUid": 0 + "subject": "subject555", + "sysLanguageUid": 0, + "typoScriptKey": "typoscriptKey555", + "uid": 1 } diff --git a/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testAdd__1.json b/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testAdd__1.json index bf8d1d2..14fdfe8 100644 --- a/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testAdd__1.json +++ b/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testAdd__1.json @@ -1,16 +1,16 @@ { - "uid": 1, - "pid": 1, - "typoScriptKey": "typoscriptKey558", - "subject": "***", - "message": "***", + "debug": "", + "headers": "***", + "mailBlindCopy": "***", + "mailCopy": "***", "mailFrom": "***", "mailTo": "***", - "mailCopy": "***", - "mailBlindCopy": "***", - "headers": "***", + "message": "***", + "pid": 0, "result": "Not send until now", "status": 0, - "debug": "", - "sysLanguageUid": 0 + "subject": "***", + "sysLanguageUid": 0, + "typoScriptKey": "typoscriptKey558", + "uid": 1 } diff --git a/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testAnonymizeAll__1.json b/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testAnonymizeAll__1.json index 11b7975..63be092 100644 --- a/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testAnonymizeAll__1.json +++ b/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testAnonymizeAll__1.json @@ -1,16 +1,16 @@ { - "uid": 1, - "pid": 1, - "typoScriptKey": "typoscriptKey7894", - "subject": "***", - "message": "***", + "debug": "***", + "headers": "***", + "mailBlindCopy": "***", + "mailCopy": "***", "mailFrom": "***", "mailTo": "***", - "mailCopy": "***", - "mailBlindCopy": "***", - "headers": "***", + "message": "***", + "pid": 0, "result": "Not send until now", "status": 0, - "debug": "***", - "sysLanguageUid": 0 + "subject": "***", + "sysLanguageUid": 0, + "typoScriptKey": "typoscriptKey7894", + "uid": 1 } diff --git a/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testCleanupDatabase__1.json b/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testCleanupDatabase__1.json index f51e220..ba7699e 100644 --- a/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testCleanupDatabase__1.json +++ b/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testCleanupDatabase__1.json @@ -1,16 +1,16 @@ { - "uid": 1, - "pid": 1, - "typoScriptKey": "typoscriptKey789", - "subject": "***", - "message": "***", + "debug": "", + "headers": "***", + "mailBlindCopy": "***", + "mailCopy": "***", "mailFrom": "***", "mailTo": "***", - "mailCopy": "***", - "mailBlindCopy": "***", - "headers": "***", + "message": "***", + "pid": 0, "result": "Not send until now", "status": 0, - "debug": "", - "sysLanguageUid": 0 + "subject": "***", + "sysLanguageUid": 0, + "typoScriptKey": "typoscriptKey789", + "uid": 1 } diff --git a/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testUpdateWithDelayAnonymize__1.json b/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testUpdateWithDelayAnonymize__1.json index 051a70d..93f5f01 100644 --- a/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testUpdateWithDelayAnonymize__1.json +++ b/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testUpdateWithDelayAnonymize__1.json @@ -1,16 +1,16 @@ { - "uid": 1, - "pid": 1, - "typoScriptKey": "typoscriptKey2345", - "subject": "***", - "message": "***", + "debug": "", + "headers": "***", + "mailBlindCopy": "***", + "mailCopy": "***", "mailFrom": "***", "mailTo": "***", - "mailCopy": "***", - "mailBlindCopy": "***", - "headers": "***", + "message": "***", + "pid": 0, "result": "Not send until now", "status": 0, - "debug": "", - "sysLanguageUid": 0 + "subject": "***", + "sysLanguageUid": 0, + "typoScriptKey": "typoscriptKey2345", + "uid": 1 } diff --git a/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testUpdate__1.json b/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testUpdate__1.json index 6dd393d..0c76f5e 100644 --- a/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testUpdate__1.json +++ b/Tests/Functional/MailLogRepository/__snapshots__/AnonymizeDirectlyLifeTimeEmptyTest__testUpdate__1.json @@ -1,16 +1,16 @@ { - "uid": 1, - "pid": 1, - "typoScriptKey": "typoscriptKey555", - "subject": "***", - "message": "***", + "debug": "", + "headers": "***", + "mailBlindCopy": "***", + "mailCopy": "***", "mailFrom": "***", "mailTo": "***", - "mailCopy": "***", - "mailBlindCopy": "***", - "headers": "***", + "message": "***", + "pid": 0, "result": "Not send until now", "status": 0, - "debug": "", - "sysLanguageUid": 0 + "subject": "***", + "sysLanguageUid": 0, + "typoScriptKey": "typoscriptKey555", + "uid": 1 } diff --git a/Tests/Functional/MailLogRepository/__snapshots__/NoAnonymizeLifeTime1minTest__testAdd__1.json b/Tests/Functional/MailLogRepository/__snapshots__/NoAnonymizeLifeTime1minTest__testAdd__1.json index 9ebd864..af34c51 100644 --- a/Tests/Functional/MailLogRepository/__snapshots__/NoAnonymizeLifeTime1minTest__testAdd__1.json +++ b/Tests/Functional/MailLogRepository/__snapshots__/NoAnonymizeLifeTime1minTest__testAdd__1.json @@ -1,16 +1,16 @@ { - "uid": 1, - "pid": 1, - "typoScriptKey": "typoscriptKey558", - "subject": "subject558", - "message": "message558", + "debug": "", + "headers": "headers558", + "mailBlindCopy": "mail558@test.test", + "mailCopy": "mail558@test.test", "mailFrom": "mail558@test.test", "mailTo": "mail558@test.test", - "mailCopy": "mail558@test.test", - "mailBlindCopy": "mail558@test.test", - "headers": "headers558", + "message": "message558", + "pid": 0, "result": "Not send until now", "status": 0, - "debug": "", - "sysLanguageUid": 0 + "subject": "subject558", + "sysLanguageUid": 0, + "typoScriptKey": "typoscriptKey558", + "uid": 1 } diff --git a/Tests/Functional/MailLogRepository/__snapshots__/NoAnonymizeLifeTime1minTest__testAnonymizeAll__1.json b/Tests/Functional/MailLogRepository/__snapshots__/NoAnonymizeLifeTime1minTest__testAnonymizeAll__1.json index 31fa029..70aa2e0 100644 --- a/Tests/Functional/MailLogRepository/__snapshots__/NoAnonymizeLifeTime1minTest__testAnonymizeAll__1.json +++ b/Tests/Functional/MailLogRepository/__snapshots__/NoAnonymizeLifeTime1minTest__testAnonymizeAll__1.json @@ -1,16 +1,16 @@ { - "uid": 1, - "pid": 1, - "typoScriptKey": "typoscriptKey7894", - "subject": "subject7894", - "message": "message7894", + "debug": "", + "headers": "headers7894", + "mailBlindCopy": "mail7894@test.test", + "mailCopy": "mail7894@test.test", "mailFrom": "mail7894@test.test", "mailTo": "mail7894@test.test", - "mailCopy": "mail7894@test.test", - "mailBlindCopy": "mail7894@test.test", - "headers": "headers7894", + "message": "message7894", + "pid": 0, "result": "Not send until now", "status": 0, - "debug": "", - "sysLanguageUid": 0 + "subject": "subject7894", + "sysLanguageUid": 0, + "typoScriptKey": "typoscriptKey7894", + "uid": 1 } diff --git a/Tests/Functional/MailLogRepository/__snapshots__/NoAnonymizeLifeTime1minTest__testUpdateWithDelayAnonymize__1.json b/Tests/Functional/MailLogRepository/__snapshots__/NoAnonymizeLifeTime1minTest__testUpdateWithDelayAnonymize__1.json index d538665..f7b4105 100644 --- a/Tests/Functional/MailLogRepository/__snapshots__/NoAnonymizeLifeTime1minTest__testUpdateWithDelayAnonymize__1.json +++ b/Tests/Functional/MailLogRepository/__snapshots__/NoAnonymizeLifeTime1minTest__testUpdateWithDelayAnonymize__1.json @@ -1,16 +1,16 @@ { - "uid": 1, - "pid": 1, - "typoScriptKey": "typoscriptKey2345", - "subject": "subject2345", - "message": "message2345", + "debug": "", + "headers": "headers2345", + "mailBlindCopy": "mail2345@test.test", + "mailCopy": "mail2345@test.test", "mailFrom": "mail2345@test.test", "mailTo": "mail2345@test.test", - "mailCopy": "mail2345@test.test", - "mailBlindCopy": "mail2345@test.test", - "headers": "headers2345", + "message": "message2345", + "pid": 0, "result": "Not send until now", "status": 0, - "debug": "", - "sysLanguageUid": 0 + "subject": "subject2345", + "sysLanguageUid": 0, + "typoScriptKey": "typoscriptKey2345", + "uid": 1 } diff --git a/Tests/Functional/MailLogRepository/__snapshots__/NoAnonymizeLifeTime1minTest__testUpdate__1.json b/Tests/Functional/MailLogRepository/__snapshots__/NoAnonymizeLifeTime1minTest__testUpdate__1.json index db299cd..5ba3505 100644 --- a/Tests/Functional/MailLogRepository/__snapshots__/NoAnonymizeLifeTime1minTest__testUpdate__1.json +++ b/Tests/Functional/MailLogRepository/__snapshots__/NoAnonymizeLifeTime1minTest__testUpdate__1.json @@ -1,16 +1,16 @@ { - "uid": 1, - "pid": 1, - "typoScriptKey": "typoscriptKey555", - "subject": "subject555", - "message": "message555", + "debug": "", + "headers": "headers555", + "mailBlindCopy": "mail555@test.test", + "mailCopy": "mail555@test.test", "mailFrom": "mail555@test.test", "mailTo": "mail555@test.test", - "mailCopy": "mail555@test.test", - "mailBlindCopy": "mail555@test.test", - "headers": "headers555", + "message": "message555", + "pid": 0, "result": "Not send until now", "status": 0, - "debug": "", - "sysLanguageUid": 0 + "subject": "subject555", + "sysLanguageUid": 0, + "typoScriptKey": "typoscriptKey555", + "uid": 1 } diff --git a/Tests/Functional/TemplateBasedMailMessage/TemplateBasedMailMessageTest.php b/Tests/Functional/TemplateBasedMailMessage/TemplateBasedMailMessageTest.php new file mode 100644 index 0000000..2ce101d --- /dev/null +++ b/Tests/Functional/TemplateBasedMailMessage/TemplateBasedMailMessageTest.php @@ -0,0 +1,115 @@ +importCSVDataSet(__DIR__ . '/../../Fixtures/pages.csv'); + $this->importCSVDataSet(__DIR__ . '/../../Fixtures/TemplateBasedMailMessageTest/mail_templates.csv'); + $this->setUpFrontendRootPage(1, [ + 'EXT:mail_logger/Configuration/TypoScript/setup.typoscript', + 'EXT:mail_logger/Tests/Fixtures/TemplateBasedMailMessageTest/setup.typoscript', + ]); + + $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport'] = 'null'; + $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_spool_type'] = ''; + } + + public function testConfiguredTemplateIsRenderedAndLoggedWhenSentThroughNullTransport(): void + { + $mail = MailUtility::getMailByKey('functionalMail', null, [ + 'name' => 'Ada', + 'color' => 'blue', + ]); + + self::assertTrue($mail->send()); + + $mailLog = $this->getSingleMailLog(); + self::assertSame('functionalMail', $mailLog['typo_script_key']); + self::assertSame('Functional subject for Ada', $mailLog['subject']); + self::assertStringContainsString('

Rendered body for Ada with blue

', $mailLog['message']); + self::assertSame('Sender Ada ', $mailLog['mail_from']); + self::assertSame('Receiver Ada ', $mailLog['mail_to']); + self::assertSame('Email Nulled (NullTransport)', $mailLog['result']); + self::assertSame(MailStatus::NOT_SENT->value, (int)$mailLog['status']); + self::assertStringContainsString('Subject: Functional subject for Ada', $mailLog['headers']); + } + + public function testManuallyAssignedSubjectIsKeptWhenTemplateDoesNotConfigureSubject(): void + { + $mail = MailUtility::getMailByKey('directSubjectMail', null, [ + 'name' => 'Ada', + ]); + $mail->setSubject('Manual subject for Ada'); + + self::assertTrue($mail->send()); + + $mailLog = $this->getSingleMailLog(); + self::assertSame('directSubjectMail', $mailLog['typo_script_key']); + self::assertSame('Manual subject for Ada', $mailLog['subject']); + self::assertStringContainsString('

Body with manually assigned subject for Ada

', $mailLog['message']); + self::assertSame('Email Nulled (NullTransport)', $mailLog['result']); + self::assertSame(MailStatus::NOT_SENT->value, (int)$mailLog['status']); + } + + public function testEachSentMailCreatesItsOwnLogEntry(): void + { + foreach (['Ada', 'Grace'] as $name) { + $mail = MailUtility::getMailByKey('functionalMail', null, [ + 'name' => $name, + 'color' => 'blue', + ]); + + self::assertTrue($mail->send()); + } + + $connection = $this->getConnectionPool()->getConnectionForTable('tx_maillogger_domain_model_maillog'); + $mailLogs = $connection->select( + ['subject'], + 'tx_maillogger_domain_model_maillog', + [], + [], + ['uid' => 'ASC'], + )->fetchFirstColumn(); + + self::assertSame([ + 'Functional subject for Ada', + 'Functional subject for Grace', + ], $mailLogs); + } + + /** + * @return array + */ + private function getSingleMailLog(): array + { + $connection = $this->getConnectionPool()->getConnectionForTable('tx_maillogger_domain_model_maillog'); + $mailLogs = $connection->select( + ['*'], + 'tx_maillogger_domain_model_maillog', + [], + [], + ['uid' => 'ASC'], + )->fetchAllAssociative(); + + self::assertCount(1, $mailLogs); + + return $mailLogs[0]; + } +} diff --git a/Tests/Unit/Service/CleanupSettingsServiceTest.php b/Tests/Unit/Service/CleanupSettingsServiceTest.php new file mode 100644 index 0000000..7efaedd --- /dev/null +++ b/Tests/Unit/Service/CleanupSettingsServiceTest.php @@ -0,0 +1,52 @@ +createMock(ConfigurationUtility::class); + $configurationUtility->expects(self::once()) + ->method('getConfiguration') + ->with('settings') + ->willReturn([ + 'cleanup' => [ + 'lifetime' => '90 days', + 'anonymize' => false, + 'anonymizeAfter' => '14 days', + ], + ]); + + $subject = new CleanupSettingsService($configurationUtility); + + self::assertTrue($subject->isLoaded()); + self::assertSame('90 days', $subject->getLifetime()); + self::assertFalse($subject->shouldAnonymize()); + self::assertSame('14 days', $subject->getAnonymizeAfter()); + self::assertSame('***', $subject->getAnonymizeSymbol()); + } + + public function testReturnsDefaultsWhenConfigurationCannotBeLoaded(): void + { + $configurationUtility = $this->createMock(ConfigurationUtility::class); + $configurationUtility->expects(self::once()) + ->method('getConfiguration') + ->with('settings') + ->willThrowException(new RuntimeException()); + + $subject = new CleanupSettingsService($configurationUtility); + + self::assertTrue($subject->isLoaded()); + self::assertSame('30 days', $subject->getLifetime()); + self::assertTrue($subject->shouldAnonymize()); + self::assertSame('7 days', $subject->getAnonymizeAfter()); + } +} diff --git a/composer.json b/composer.json index 1de0665..6342bf7 100644 --- a/composer.json +++ b/composer.json @@ -12,19 +12,18 @@ "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-extbase": "^12.4.0 || ^13.4.0" + "typo3/cms-core": "^13.4.0", + "typo3/cms-extbase": "^13.4.0" }, "require-dev": { "ext-json": "*", "andersundsehr/no-ci": "^1.1", - "helhum/typo3-console": "^8.2.3", - "phpunit/phpunit": "^10.5.59", - "pluswerk/grumphp-config": "^7.2.0 || ^10.2.2", - "saschaegerer/phpstan-typo3": "^1.10.2 || ^2.1.1", - "spatie/phpunit-snapshot-assertions": "^5.2.3", - "ssch/typo3-rector": "^2.15.2 || ^3.9.0", - "typo3/testing-framework": "^8.3.1 || ^9.2.0" + "helhum/typo3-console": "^8.3.1", + "pluswerk/grumphp-config": "^10.2.8", + "saschaegerer/phpstan-typo3": "^2.1.1", + "spatie/phpunit-snapshot-assertions": "^5.4.0", + "ssch/typo3-rector": "^3.14.3", + "typo3/testing-framework": "^9.5.0" }, "replace": { "pluswerk/mail_logger": "self.version", @@ -63,12 +62,7 @@ "@composer bump --dev-only", "@composer normalize" ], - "post-autoload-dump": [ - "@php vendor/bin/typo3 install:fixfolderstructure -vvv", - "@no-ci @php vendor/bin/typo3 database:updateschema -vvv", - "@no-ci @php vendor/bin/typo3 cache:flush -vvv" - ], - "test": "vendor/bin/phpunit --color=always", + "test": "vendor/bin/phpunit --color=always --no-coverage", "test:update": "vendor/bin/phpunit --color=always -d --update-snapshots" }, "scripts-descriptions": { diff --git a/ext_emconf.php b/ext_emconf.php index a0d1a22..be76e4e 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -11,7 +11,7 @@ 'version' => \Composer\InstalledVersions::getPrettyVersion('pluswerk/mail-logger'), 'constraints' => [ 'depends' => [ - 'typo3' => '12.0.0-13.4.99', + 'typo3' => '13.0.0-13.4.99', ], 'conflicts' => [], 'suggests' => [], diff --git a/phpunit.xml b/phpunit.xml index d74d7e2..ed3b8bf 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -29,15 +29,14 @@ - - - - - - + + + + Tests/Unit/ + Tests/Functional/ Tests/Functional/MailLogRepository/AbstractMailLogRepositoryTest.php