Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Mailer] HTML-Mail with TWIG Template and async transport #44439

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
swnjfn opened this issue Dec 3, 2021 · 9 comments
Closed

[Mailer] HTML-Mail with TWIG Template and async transport #44439

swnjfn opened this issue Dec 3, 2021 · 9 comments

Comments

@swnjfn
Copy link

swnjfn commented Dec 3, 2021

Symfony version(s) affected

5.*

Description

Trying to send a html mail with twig template (https://symfony.com/doc/current/mailer.html#html-content) with async transport (https://symfony.com/doc/current/mailer.html#sending-messages-async).

use Symfony\Bridge\Twig\Mime\TemplatedEmail;

$email = (new TemplatedEmail())
    ->from('[email protected]')
    ->to(new Address('[email protected]'))
    ->subject('Thanks for signing up!')

    // path of the Twig template to render
    ->htmlTemplate('emails/signup.html.twig')

    // pass variables (name => value) to the template
    ->context([
        'expiration_date' => new \DateTime('+7 days'),
        'username' => 'foo',
    ])
;

How to reproduce

The Problem occure in my case when putting an entity in the context of the mail object

use Symfony\Bridge\Twig\Mime\TemplatedEmail;

$email = (new TemplatedEmail())
    ->from('[email protected]')
    ->to(new Address('[email protected]'))
    ->subject('Thanks for signing up!')

    // path of the Twig template to render
    ->htmlTemplate('emails/signup.html.twig')

    // pass variables (name => value) to the template
    ->context([
        'entity' => $this->someServiceToGetTheEnttiy->getMyImportantEntity()
    ])
;

In my case there are some ORM-Mapping (Many to...) to other Entities. Finally one of the mapped entities has an Symfony\Component\HttpFoundation\File in the property.

Symfony\Component\Messenger\Transport\Serialization\PhpSerializer:encode() uses

$body = addslashes(serialize($envelope));

which will throw an exception, because Symfony\Component\HttpFoundation\File is not allowed to serialize (could also happen with other stuff).

This Example is really just an example, but i actually will parse an entity to the twig template by mail->context().
This is not a szenario what will be often occure i think. But perhaps someone has an similar issue.

Possible Solution

  1. Only use informations and object that you really need in the context (so an Entity is a bad idea)

  2. Throw context out of the envelop object befor sending it

use Symfony\Bridge\Twig\Mime\TemplatedEmail;

$email = (new TemplatedEmail())
    ->from('[email protected]')
    ->to(new Address('[email protected]'))
    ->subject('Thanks for signing up!')

    // path of the Twig template to render
    ->htmlTemplate('emails/signup.html.twig')

    // pass variables (name => value) to the template
    ->context([
        'expiration_date' => new \DateTime('+7 days'),
        'username' => 'foo',
    ])
;

// this could be done by an mail service otherwise just do it directly without setting templates to the `TemplatedMail`-Object
// $this->twig is the `Twig\Environment` injection in this service

if ($email->getTextTemplate()) {
    $email->text($this->twig->render($mail->getTextTemplate(), $email->getContext()));
    $email->textTemplate(null); // musst be set to null or the mailer will try to render this again
}
if ($email->getHtmlTemplate()) {
    $bodyRenderer = new Symfony\Bridge\Twig\Mime\BodyRenderer($this->twig);
    $bodyRenderer->render($email);
    $email->htmlTemplate(null); // musst be set to null or the mailer will try to render this again
}

$email->context([]);

$this->mailService->send($email);






### Additional Context

_No response_
@noniagriconomie
Copy link
Contributor

IIUC, you put the entire email in the message envelope and you dispatch it?
If yes, I would recommend to compute the entire mail payload in the handler instead, and passing to the handler just the bare data :)

@swnjfn
Copy link
Author

swnjfn commented Dec 3, 2021

IIUC, ...

i am sorry but i dont know what you mean with IIUC.

... you put the entire email in the message envelope and you dispatch it?

do i have a choice? I just send the mail. What then is happen is done by Mailer.

@noniagriconomie
Copy link
Contributor

IIUC :> If I Understand Correctly

i do not know what mailer does from its side on async logic, i send async email like i've described to you :)

@maltyxx
Copy link

maltyxx commented Jan 21, 2022

I have the same problem on Symfony 4.* and 5.*

$email = (new TemplatedEmail())
->from(new Address('[email protected]', 'Company'))
->to($user->getEmail())
->priority(TemplatedEmail::PRIORITY_HIGH)
->subject($translator->trans('Your password reset request'))
->htmlTemplate('reset_password/email.html.twig')
->context([
    'resetToken' => $resetToken,
]);

$mailer->send($email);

@gubler
Copy link

gubler commented May 9, 2022

I've just run into this.

I would have expected that when I do a $mailer->send($templatedEmail), that the Mailer would render the TemplatedEmail before adding the email to the async transport.

Instead, the Mailer is serializing the TemplatedEmail and dispatching it to the queue (with the containing Envelope and Stamps) and then when the message is consumed, the Mailer is deserializing the TemplatedEmail and rendering before sending.

@maartendekeizer
Copy link

maartendekeizer commented May 16, 2022

@gubler Same issue here. I expected too that rendering is done before sending to the async transport.
Workaround:

# services.yaml
Symfony\Component\Mime\BodyRendererInterface:
        alias: twig.mime_body_renderer
# Controller/TestController.php
public function myAction(MailerInterface $mailer, BodyRendererInterface $bodyRenderer): Response
{
    $email = (new TemplatedEmail());
    $email->to('[email protected]');
    $email->htmlTemplate('emails/test.html.twig');
    $email->context(['foo' => 'bar']);
    $this->bodyRenderer->render($email);
    $this->mailer->send($email);
    new Response('OK');
}

@Heyfara
Copy link

Heyfara commented May 24, 2022

Although my issue was not exactly the same, this issue helped me find out what the problem was.

Some properties in the template were missing. It was caused by a custom __serialize() function in an object passed to the context. So to be safe, only use really simple DTOs in the context.

I hope it can help someone.

@maartendekeizer
Copy link

Calling the body renderer seems not be enough in all situations. When using custom serialize functions it may be happen that the fingerprint differs between the first call to the body renderer and the call that is made by the message handler. This will lead into a second renderer which will fail in our case.

Extending above work a round with some extra code to convert een templated email into a plain email seems to work.

    $email = (new TemplatedEmail())
        ->to(new Address($to))
        ->subject($subject)
        ->htmlTemplate($template)
        ->context($context)
    ;
    $bodyRenderer->render($email);

    $plainEmail = new Email();
    $plainEmail->setHeaders(clone $email->getHeaders());
    $plainEmail->html($email->getHtmlBody());
    $plainEmail->text($email->getTextBody());

    $mailer->send($plainEmail);

@fabpot
Copy link
Member

fabpot commented Jul 27, 2022

The mailer renders the email in the async handler to get "better" headers (like a Date header that reflects when the email was sent).
When sending an email async, you must indeed send only data that can be serialized (there is no way around it). The other proper way is to render the email yourself via the body renderer as mentioned in one of the comments above (with some caveats like for the Date header value mentioned above).

See #47075 for an easier way to render emails before sending them in 6.2
And symfony/symfony-docs#17065 for the doc PR that makes things hopefully more clear.

@fabpot fabpot closed this as completed Jul 27, 2022
javiereguiluz added a commit to symfony/symfony-docs that referenced this issue Jul 27, 2022
…(fabpot)

This PR was merged into the 6.2 branch.

Discussion
----------

[Mailer] Add more information about sending email async

The example only works as of 6.2 thanks to symfony/symfony#47075

I've written this PR because we keep having issues like symfony/symfony#44439.
So, documenting a bit more how it works internally might help people understand what to do.

Commits
-------

4fbbc16 Add more information about sending email async
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants