Description
Symfony version(s) affected: 5.1, 5.2
Description
A MessageListener cannot be used to add a FROM header when the symfony/mailer is used together with symfony/messenger and symfony/serializer, a LogicException is thrown: An email must have a "From" or a "Sender" header.
How to reproduce
I'm sorry, at the moment I have no reproduce code.
- create symfony project with messenger, mailer and serializer
- create a custom event listener for the MessageEvent like the documentation suggests: "Instead of calling ->from() every time you create a new email, you can create an event subscriber and listen to the MessageEvent event to set the same From email to all messages."
- or (untested): create a 5.2 project and use the MessageListener included in 5.2 (PR [FrameworkBundle][Mailer] Add a way to configure some email headers from semantic configuration #36736) and the mailer configuration to set a default sender:
framework:
mailer:
dsn: '%env(MAILER_DSN)%'
headers:
from: '[email protected]'
- configure the messenger to use the the symfony/serializer:
serializer:
default_serializer: 'messenger.transport.symfony_serializer'
- try to send an email without a sender address, relying on the listener to add it.
Result: exception with 'An email must have a "From" or a "Sender" header.'
Possible Solution
Do not clone $message
for the event in https://github.com/symfony/mailer/blob/master/Mailer.php#L48 to allow listeners to modify the message before serialization, they can check isQueued to prevent duplicate modification.
Or, probably not that easy: Don't trigger ensureValidity() when the serializer calls getBody() on Symfony\Component\Mime\Email
Additional context
relevant part of the trace:
#0 Symfony\Component\Mime\Message->ensureValidity() called at [/var/www/html/vendor/symfony/mime/Email.php:408]
#1 Symfony\Component\Mime\Email->ensureValidity() called at [/var/www/html/vendor/symfony/mime/Email.php:433]
#2 Symfony\Component\Mime\Email->generateBody() called at [/var/www/html/vendor/symfony/mime/Email.php:399]
#3 Symfony\Component\Mime\Email->getBody() called at [/var/www/html/vendor/symfony/property-access/PropertyAccessor.php:405]
#4 Symfony\Component\PropertyAccess\PropertyAccessor->readProperty() called at [/var/www/html/vendor/symfony/property-access/PropertyAccessor.php:100]
#5 Symfony\Component\PropertyAccess\PropertyAccessor->getValue() called at [/var/www/html/vendor/symfony/serializer/Normalizer/ObjectNormalizer.php:142]
#6 Symfony\Component\Serializer\Normalizer\ObjectNormalizer->getAttributeValue() called at [/var/www/html/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php:176]
#7 Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->normalize() called at [/var/www/html/vendor/symfony/serializer/Serializer.php:154]
#8 Symfony\Component\Serializer\Serializer->normalize() called at [/var/www/html/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php:201]
#9 Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->normalize() called at [/var/www/html/vendor/symfony/serializer/Serializer.php:154]
#10 Symfony\Component\Serializer\Serializer->normalize() called at [/var/www/html/vendor/symfony/serializer/Serializer.php:127]
#11 Symfony\Component\Serializer\Serializer->serialize() called at [/var/www/html/vendor/symfony/messenger/Transport/Serialization/Serializer.php:105]
#12 Symfony\Component\Messenger\Transport\Serialization\Serializer->encode() called at [/var/www/html/vendor/symfony/amqp-messenger/Transport/AmqpSender.php:42]
#13 Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpSender->send() called at [/var/www/html/vendor/symfony/amqp-messenger/Transport/AmqpTransport.php:66]
#14 Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransport->send() called at [/var/www/html/vendor/symfony/messenger/Middleware/SendMessageMiddleware.php:69]
#15 Symfony\Component\Messenger\Middleware\SendMessageMiddleware->handle() called at [/var/www/html/vendor/symfony/messenger/Middleware/FailedMessageProcessingMiddleware.php:34]
#16 Symfony\Component\Messenger\Middleware\FailedMessageProcessingMiddleware->handle() called at [/var/www/html/vendor/symfony/messenger/Middleware/DispatchAfterCurrentBusMiddleware.php:67]
#17 Symfony\Component\Messenger\Middleware\DispatchAfterCurrentBusMiddleware->handle() called at [/var/www/html/vendor/symfony/messenger/Middleware/RejectRedeliveredMessageMiddleware.php:48]
#18 Symfony\Component\Messenger\Middleware\RejectRedeliveredMessageMiddleware->handle() called at [/var/www/html/vendor/symfony/messenger/Middleware/AddBusNameStampMiddleware.php:37]
#19 Symfony\Component\Messenger\Middleware\AddBusNameStampMiddleware->handle() called at [/var/www/html/vendor/symfony/messenger/Middleware/TraceableMiddleware.php:43]
#20 Symfony\Component\Messenger\Middleware\TraceableMiddleware->handle() called at [/var/www/html/vendor/symfony/messenger/MessageBus.php:80]
#21 Symfony\Component\Messenger\MessageBus->dispatch() called at [/var/www/html/vendor/symfony/mailer/Mailer.php:54]
The normal flow in working cases is:
- no messenger
- the mailer directly calls the transport which dispatches the MessageEvent, the listener adds the FROM header -> everything working
- messenger with the default serializer
- the mailer himself dispatches the MessageEvent (with isQueued == true), but the listener cannot modify the real email message because it is cloned: https://github.com/symfony/mailer/blob/master/Mailer.php#L48
- the mail is serialized nevertheless using the PhpSerializer in the messenger
- when the SendMessage is handled the transport is called, that dispatches the MessageEvent a second time (isQueued == false, https://github.com/symfony/mailer/blob/master/Transport/AbstractTransport.php#L62), the Listener now can add the FROM -> everything works
The problem occurs when the symfony serializer is used, which triggers the ensureValidity() method when the message is sent to the bus (see trace).
Because the listeners only receive a cloned message they cannot modify the real mail message object and thus cannot add a sender address.
This also effects PR #36736: The config options to set a default sender will not work because the mentioned exception is thrown because the included Symfony\Component\Mailer\EventListener\MessageListener will not be able to set headers because of the clone().