From a9202da1f2646fc298b404c889efd7ad2418bea3 Mon Sep 17 00:00:00 2001 From: Tom Giesler Date: Thu, 16 Jul 2026 17:38:01 +0200 Subject: [PATCH 1/3] feat: added logging for email dispatch (configurable via settings) --- Classes/Transport/FdMailerTransport.php | 32 ++++++++++++++++++++++++- Configuration/Settings.yaml | 4 +++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/Classes/Transport/FdMailerTransport.php b/Classes/Transport/FdMailerTransport.php index 8a3cc3b..a82ca92 100644 --- a/Classes/Transport/FdMailerTransport.php +++ b/Classes/Transport/FdMailerTransport.php @@ -3,12 +3,14 @@ namespace FormatD\Mailer\Transport; use Neos\Flow\Annotations as Flow; +use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Transport\TransportInterface; use Symfony\Component\Mime\RawMessage; +use Symfony\Component\Mime\Email; class FdMailerTransport implements TransportInterface { @@ -17,6 +19,9 @@ class FdMailerTransport implements TransportInterface protected ?TransportInterface $actualTransport = null; + #[Flow\Inject(name: "FormatD.Mailer:MailerLogger", lazy: false)] + protected LoggerInterface $logger; + /** * @throws TransportExceptionInterface|\Exception */ @@ -25,7 +30,12 @@ public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMess if ($this->actualTransport === null) { $this->actualTransport = Transport::fromDsn($this->getActualTransportDsn()); } - return $this->actualTransport->send($message); + + $sentMessage = $this->actualTransport->send($message, $envelope); + + $this->logEmailDispatch($sentMessage, $message); + + return $sentMessage; } /** @@ -66,4 +76,24 @@ public function __toString(): string { return 'fd-mailer'; } + + protected function logEmailDispatch(?SentMessage $sentMessage, RawMessage $message): void + { + $isLoggable = $sentMessage instanceof SentMessage + && $message instanceof Email + && $this->settings['logging']['emailDispatch']; + + if (!$isLoggable) { + return; + } + + $subject = $message->getSubject(); + if (mb_strlen($subject) > 50) { + $subject = mb_substr($subject, 0, 50) . ' …'; + } + $from = implode(', ', array_map(fn($a) => $a->toString(), $message->getFrom())); + $to = implode(', ', array_map(fn($a) => $a->toString(), $message->getTo())); + + $this->logger->info(vsprintf('Sent email to %s (from %s) with subject "%s"', [$to, $from, $subject])); + } } diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 859bc28..b0e8aae 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -22,4 +22,6 @@ FormatD: recipients: [] defaultFrom: address: 'example@example.com' - name: 'Example' \ No newline at end of file + name: 'Example' + logging: + emailDispatch: true From 84bda5628862b0a493922875b606628fef338f61 Mon Sep 17 00:00:00 2001 From: Tom Giesler Date: Wed, 22 Jul 2026 15:13:49 +0200 Subject: [PATCH 2/3] feat: log original recipients/subject for intercepted emails --- Classes/Transport/AbstractTransport.php | 14 +++++++++++ Classes/Transport/FdMailerTransport.php | 24 ++++++++++++------ Classes/Transport/InterceptingTransport.php | 14 ++++++++--- Configuration/Settings.yaml | 2 ++ README.md | 28 +++++++++++++++++++-- 5 files changed, 69 insertions(+), 13 deletions(-) create mode 100644 Classes/Transport/AbstractTransport.php diff --git a/Classes/Transport/AbstractTransport.php b/Classes/Transport/AbstractTransport.php new file mode 100644 index 0000000..46f237e --- /dev/null +++ b/Classes/Transport/AbstractTransport.php @@ -0,0 +1,14 @@ +getSubject(); + $interceptDataHeaderName = $this->settings['interceptAll']['interceptDataHeaderName'] ?? self::DEFAULT_INTERCEPT_DATA_HEADER_NAME; + $isIntercepted = $message->getHeaders()->has($interceptDataHeaderName); + if ($isIntercepted) { + $interceptMetaData = json_decode($message->getHeaders()->get($interceptDataHeaderName)->getBody(), true, 16, JSON_THROW_ON_ERROR); + } + + $subject = !$isIntercepted ? $message->getSubject() : $interceptMetaData['subject']; if (mb_strlen($subject) > 50) { $subject = mb_substr($subject, 0, 50) . ' …'; } - $from = implode(', ', array_map(fn($a) => $a->toString(), $message->getFrom())); - $to = implode(', ', array_map(fn($a) => $a->toString(), $message->getTo())); + $from = !$isIntercepted + ? implode(', ', array_map(fn($a) => $a->toString(), $message->getFrom())) + : implode(', ', $interceptMetaData['from']); + $to = !$isIntercepted + ? implode(', ', array_map(fn($a) => $a->toString(), $message->getTo())) + : implode(', ', $interceptMetaData['to']); $this->logger->info(vsprintf('Sent email to %s (from %s) with subject "%s"', [$to, $from, $subject])); } diff --git a/Classes/Transport/InterceptingTransport.php b/Classes/Transport/InterceptingTransport.php index ecd5fb7..936347a 100644 --- a/Classes/Transport/InterceptingTransport.php +++ b/Classes/Transport/InterceptingTransport.php @@ -9,11 +9,8 @@ use Symfony\Component\Mime\Email; use Symfony\Component\Mime\RawMessage; -class InterceptingTransport implements TransportInterface +class InterceptingTransport extends AbstractTransport { - #[Flow\InjectConfiguration(package: 'FormatD.Mailer', type: 'Settings')] - protected array $settings; - public function __construct(protected TransportInterface $actualTransport) { } @@ -55,6 +52,15 @@ protected function interceptMessage(Email $message): Email $message->getHeaders()->remove('Cc'); $message->getHeaders()->remove('Bcc'); + $interceptMetaData = [ + 'to' => array_map(fn($a) => $a->toString(), $message->getTo()), + 'from' => array_map(fn($a) => $a->toString(), $message->getFrom()), + 'subject' => $message->getSubject(), + ]; + + $interceptDataHeaderName = $this->settings['interceptAll']['interceptDataHeaderName'] ?? self::DEFAULT_INTERCEPT_DATA_HEADER_NAME; + $message->getHeaders()->addTextHeader($interceptDataHeaderName, \json_encode($interceptMetaData)); + $first = true; foreach ($this->settings['interceptAll']['recipients'] as $email) { if ($first) { diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index b0e8aae..02892f4 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -17,6 +17,8 @@ FormatD: active: false recipients: [] noInterceptPatterns: [] + interceptDataHeaderName: 'X-Intercept-Metadata' + bccAll: active: false recipients: [] diff --git a/README.md b/README.md index dd3198e..9188221 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ FormatD: Extend AbstractMailerService and add methods as needed following the example of sendTestMail(). -## intersept all mails in a dev environment +## intercept all mails in a dev environment -Configure swiftmailer to intersept all mails send by your neos installation (not only by the service). +Configure the mailer to intercept all mails send by your neos installation (not only by the service). This is an example which intercepts all mails and redirects them to example@example.com and secondexample@example.com: @@ -58,6 +58,30 @@ FormatD: recipients: [] ``` +## Logging email dispatch + +Every sent email (to, from, subject) is logged via its own logger channel `FormatD.Mailer:MailerLogger`, writing to +`Data/Logs/Mailer.log`. This can be disabled: + +``` +FormatD: + Mailer: + logging: + emailDispatch: false +``` + +If a mail was intercepted (see `interceptAll`/`bccAll` above), the log entry still shows the *original* recipient, +sender and subject — not the intercepted ones — since those are the actually relevant values for debugging. This is +achieved by stashing the original data in an `X-Intercept-Metadata` mail header before rewriting the message. The +header name can be changed if it collides with something: + +``` +FormatD: + Mailer: + interceptAll: + interceptDataHeaderName: 'X-Intercept-Metadata' +``` + ## Handling Embedded Images The method `AbstractMailerService->setMailContentFromStandaloneView()` has a parameter to embed all images into the mail From 547ea225c78d886bc63cc2a89d2c6b04d5c5156b Mon Sep 17 00:00:00 2001 From: Tom Giesler Date: Thu, 23 Jul 2026 10:55:45 +0200 Subject: [PATCH 3/3] chore: register MailerLogger PSR-3 channel (was only implicitly present on master via the Neos 9 port) --- Configuration/Objects.yaml | 8 ++++++++ Configuration/Settings.Neos.Flow.yaml | 14 ++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 Configuration/Objects.yaml create mode 100644 Configuration/Settings.Neos.Flow.yaml diff --git a/Configuration/Objects.yaml b/Configuration/Objects.yaml new file mode 100644 index 0000000..22cb4d0 --- /dev/null +++ b/Configuration/Objects.yaml @@ -0,0 +1,8 @@ +'FormatD.Mailer:MailerLogger': + className: Psr\Log\LoggerInterface + scope: singleton + factoryObjectName: Neos\Flow\Log\PsrLoggerFactoryInterface + factoryMethodName: get + arguments: + 1: + value: MailerLogger diff --git a/Configuration/Settings.Neos.Flow.yaml b/Configuration/Settings.Neos.Flow.yaml new file mode 100644 index 0000000..be02e8b --- /dev/null +++ b/Configuration/Settings.Neos.Flow.yaml @@ -0,0 +1,14 @@ +Neos: + Flow: + log: + psr3: + Neos\Flow\Log\PsrLoggerFactory: + MailerLogger: + default: + class: Neos\Flow\Log\Backend\FileBackend + options: + logFileURL: '%FLOW_PATH_DATA%Logs/Mailer.log' + severityThreshold: '%LOG_INFO%' + createParentDirectories: true + maximumLogFileSize: 1048576 + logFilesToKeep: 1