Commit 68c6dd05 authored by Oliver Bartsch's avatar Oliver Bartsch
Browse files

[FEATURE] Add PSR-14 events for Mailer

TYPO3's Mailer implementation does now dispatch
two new PSR-14 events.

The `BeforeMailerSentMessageEvent` can be
used to manipulate the message and the envelope
before being sent.

The `AfterMailerSentMessageEvent` can be used
to add further processing after the message has
been sent.

Resolves: #93689
Releases: main
Change-Id: I02ec5ad5b7a18f9f6f4f7146b676f7ce773d3b29
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/74825

Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Stefan Bürk's avatarStefan Bürk <stefan@buerk.tech>
Tested-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Reviewed-by: André Buchmann's avatarAndré Buchmann <andy.schliesser@gmail.com>
Reviewed-by: Oliver Klee's avatarOliver Klee <typo3-coding@oliverklee.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Stefan Bürk's avatarStefan Bürk <stefan@buerk.tech>
Reviewed-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
parent 8cf1f942
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Mail\Event;
use Symfony\Component\Mailer\MailerInterface;
/**
* This event is fired once a Mailer has sent a message and allows listeners to execute
* further code afterwards, depending on the result, e.g. the SentMessage.
*
* Note: Usually TYPO3\CMS\Core\Mail\Mailer is given to the event. This implementation
* allows to retrieve the SentMessage using the getSentMessage() method. Depending
* on the Transport, used to send the message, this might also be NULL.
*/
final class AfterMailerSentMessageEvent
{
public function __construct(private readonly MailerInterface $mailer)
{
}
public function getMailer(): MailerInterface
{
return $this->mailer;
}
}
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Mail\Event;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\RawMessage;
/**
* This event is fired before the Mailer has sent a message and
* allows listeners to manipulate the RawMessage and the Envelope.
*
* Note: Usually TYPO3\CMS\Core\Mail\Mailer is given to the event. This implementation
* allows to retrieve the TransportInterface using the getTransport() method.
*/
final class BeforeMailerSentMessageEvent
{
public function __construct(
private readonly MailerInterface $mailer,
private RawMessage $message,
private ?Envelope $envelope = null,
) {
}
public function getMessage(): RawMessage
{
return $this->message;
}
public function setMessage(RawMessage $message): void
{
$this->message = $message;
}
public function getEnvelope(): ?Envelope
{
return $this->envelope;
}
public function setEnvelope(?Envelope $envelope = null): void
{
$this->envelope = $envelope;
}
public function getMailer(): MailerInterface
{
return $this->mailer;
}
}
......@@ -25,6 +25,8 @@ use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\RawMessage;
use TYPO3\CMS\Core\Exception as CoreException;
use TYPO3\CMS\Core\Mail\Event\AfterMailerInitializationEvent;
use TYPO3\CMS\Core\Mail\Event\AfterMailerSentMessageEvent;
use TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MailUtility;
......@@ -100,7 +102,15 @@ class Mailer implements MailerInterface
$message->getHeaders()->addTextHeader('X-Mailer', $this->mailerHeader);
}
$this->sentMessage = $this->transport->send($message, $envelope);
// After static enrichment took place, allow listeners to further manipulate message and envelope
$event = new BeforeMailerSentMessageEvent($this, $message, $envelope);
$this->eventDispatcher?->dispatch($event);
// Send message using the defined transport, with message and envelope from the event
$this->sentMessage = $this->transport->send($event->getMessage(), $event->getEnvelope());
// Finally, allow further processing by listeners after the message has been sent
$this->eventDispatcher?->dispatch(new AfterMailerSentMessageEvent($this));
}
public function getSentMessage(): ?SentMessage
......
.. include:: /Includes.rst.txt
.. _feature-93689-1654629861
===============================================================
Feature: #93689 - PSR-14 Events on sending messages with Mailer
===============================================================
See :issue:`93689`
Description
===========
TYPO3's :php:`MailerInterface` implementation :php:`Mailer` is used for sending
messages, e.g. in the EXT:form email finishers. To allow further handling and
manipulation of the message sending process, two new PSR-14 Events have been
introduced.
The :php:`BeforeMailerSentMessageEvent` is dispatched before the message
is sent by the mailer and can be used to manipulate the :php:`RawMessage`
and the :php:`Envelope`. Usually an :php:`Email` or `FluidEmail` instance
is given as :php:`RawMessage`. Additionally the :php:`MailerInstance` is
given, which depending on the implementation - usually
:php:`TYPO3\CMS\Core\Mail\Mailer` - contains the :php:`Transport` object,
which can be retrieved using the :php:`getTransport()` method.
The :php:`AfterMailerSentMessageEvent` is dispatched as soon as the
message has been sent via the corresponding :php:`TransportInterface`.
The Event receives the current :php:`MailerInstance`, which depending
on the implementation - usually :php:`TYPO3\CMS\Core\Mail\Mailer` -
contains the :php:`SentMessage` object, which can be retrieved using
the :php:`getSentMessage()` method.
Registration of the Event listeners in your extensions' :file:`Services.yaml`:
.. code-block:: yaml
MyVendor\MyPackage\EventListener\MailerSentMessageEventListener:
tags:
- name: event.listener
identifier: 'my-package/modify-message'
method: 'modifyMessage'
- name: event.listener
identifier: 'my-package/process-sent-message'
method: 'processSentMessage'
The corresponding event listener class:
.. code-block:: php
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use TYPO3\CMS\Core\Mail\Event\AfterMailerSentMessageEvent;
use TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent;
use TYPO3\CMS\Core\Mail\Mailer;
final class MailerSentMessageEventListener implementsLoggerAwareInterface
{
use LoggerAwareTrait;
public function modifyMessage(BeforeMailerSentMessageEvent $event): void
{
$message = $event->getMessage();
// If $message is an Email implementation, add an additional recipient
if ($message instanceof Email) {
$message->addCc(new Address('kasperYYYY@typo3.org'));
}
}
public function processSentMessage(AfterMailerSentMessageEvent $event): void
{
$mailer = $event->getMailer();
if ($mailer instanceof Mailer) {
$sentMessage = $mailer->getSentMessage();
if ($sentMessage !== null) {
$this->logger->debug($sentMessage->getDebug());
}
}
}
}
Impact
======
With the new PSR-14 events, it's now possible to manipulate messages before
they are sent by the mailer. Additionally, after the mailer has sent messages,
further processing can be performed.
.. index:: PHP-API, ext:core
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Tests\Functional\Mail;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Transport\NullTransport;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\RawMessage;
use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
use TYPO3\CMS\Core\Mail\Event\AfterMailerInitializationEvent;
use TYPO3\CMS\Core\Mail\Event\AfterMailerSentMessageEvent;
use TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent;
use TYPO3\CMS\Core\Mail\Mailer;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
class MailerTest extends FunctionalTestCase
{
/**
* @test
*/
public function mailerEventsAreTriggered(): void
{
$afterMailerInitializedEvent = null;
$beforeMailerSentMessageEvent = null;
$afterMailerSentMessageEvent = null;
/** @var Container $container */
$container = $this->getContainer();
$container->set(
'after-mailer-initialized-listener',
static function (AfterMailerInitializationEvent $event) use (&$afterMailerInitializedEvent) {
$afterMailerInitializedEvent = $event;
}
);
$container->set(
'before-mailer-sent-message-listener',
static function (BeforeMailerSentMessageEvent $event) use (&$beforeMailerSentMessageEvent) {
$beforeMailerSentMessageEvent = $event;
}
);
$container->set(
'after-mailer-sent-message-listener',
static function (AfterMailerSentMessageEvent $event) use (&$afterMailerSentMessageEvent) {
$afterMailerSentMessageEvent = $event;
}
);
$eventListener = $container->get(ListenerProvider::class);
$eventListener->addListener(AfterMailerInitializationEvent::class, 'after-mailer-initialized-listener');
$eventListener->addListener(BeforeMailerSentMessageEvent::class, 'before-mailer-sent-message-listener');
$eventListener->addListener(AfterMailerSentMessageEvent::class, 'after-mailer-sent-message-listener');
$message = new RawMessage('some message');
$envelope = new Envelope(new Address('kasperYYYY@typo3.org'), [new Address('acme@example.com')]);
$mailer = (new Mailer(new NullTransport(), $container->get(EventDispatcherInterface::class)));
$mailer->send($message, $envelope);
self::assertInstanceOf(AfterMailerInitializationEvent::class, $afterMailerInitializedEvent);
self::assertEquals($mailer, $afterMailerInitializedEvent->getMailer());
self::assertInstanceOf(BeforeMailerSentMessageEvent::class, $beforeMailerSentMessageEvent);
self::assertEquals($message, $beforeMailerSentMessageEvent->getMessage());
self::assertEquals($envelope, $beforeMailerSentMessageEvent->getEnvelope());
self::assertEquals($mailer, $beforeMailerSentMessageEvent->getMailer());
self::assertInstanceOf(AfterMailerSentMessageEvent::class, $afterMailerSentMessageEvent);
self::assertEquals($mailer, $afterMailerSentMessageEvent->getMailer());
}
}
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Tests\Unit\Mail\Event;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Mailer\Transport\SendmailTransport;
use TYPO3\CMS\Core\Mail\Event\AfterMailerSentMessageEvent;
use TYPO3\CMS\Core\Mail\Mailer;
use TYPO3\CMS\Core\Mail\TransportFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
class AfterMailerSentMessageEventTest extends UnitTestCase
{
use ProphecyTrait;
protected bool $resetSingletonInstances = true;
protected function setUp(): void
{
parent::setUp();
$transportFactory = $this->prophesize(TransportFactory::class);
$transportFactory->get(Argument::any())->willReturn($this->prophesize(SendmailTransport::class));
GeneralUtility::setSingletonInstance(TransportFactory::class, $transportFactory->reveal());
}
/**
* @test
*/
public function gettersReturnInitializedObjects(): void
{
$mailer = (new Mailer());
$event = new AfterMailerSentMessageEvent($mailer);
self::assertEquals($mailer, $event->getMailer());
}
}
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Tests\Unit\Mail\Event;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Transport\SendmailTransport;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent;
use TYPO3\CMS\Core\Mail\Mailer;
use TYPO3\CMS\Core\Mail\TransportFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
class BeforeMailerSentMessageEventTest extends UnitTestCase
{
use ProphecyTrait;
protected bool $resetSingletonInstances = true;
protected function setUp(): void
{
parent::setUp();
$transportFactory = $this->prophesize(TransportFactory::class);
$transportFactory->get(Argument::any())->willReturn($this->prophesize(SendmailTransport::class));
GeneralUtility::setSingletonInstance(TransportFactory::class, $transportFactory->reveal());
}
/**
* @test
*/
public function gettersReturnInitializedObjects(): void
{
$mailer = (new Mailer());
$rawMessage = (new Email())->subject('some subject');
$envelope = (new Envelope(new Address('kasperYYYY@typo3.org'), [new Address('acme@example.com')]));
$event = new BeforeMailerSentMessageEvent($mailer, $rawMessage, $envelope);
self::assertEquals($mailer, $event->getMailer());
self::assertEquals($rawMessage, $event->getMessage());
self::assertEquals($envelope, $event->getEnvelope());
}
/**
* @test
*/
public function modifyingInitializedObjects(): void
{
$mailer = (new Mailer());
$rawMessage = (new Email())->subject('some subject');
$envelope = (new Envelope(new Address('kasperYYYY@typo3.org'), [new Address('acme@example.com')]));
$event = new BeforeMailerSentMessageEvent($mailer, $rawMessage, $envelope);
// can modify message
$modifiedMessage = $rawMessage->subject('modified subject');
$event->setMessage($modifiedMessage);
self::assertEquals($modifiedMessage, $event->getMessage());
// can modify envelope
$envelope->setSender(new Address('modified.sender@typo3.org'));
$event->setEnvelope($envelope);
self::assertEquals($envelope, $event->getEnvelope());
// can unset envelope
$event->setEnvelope();
self::assertNull($event->getEnvelope());
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment