Skip to content

[Mime] use isRendered method to ensure we can avoid rendering an email twice #59596

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

Merged
merged 1 commit into from
Feb 4, 2025

Conversation

walva
Copy link
Contributor

@walva walva commented Jan 23, 2025

Q A
Branch? 6.4
Bug fix? yes? Not sure
New feature? no
Deprecations? no
Issues
License MIT

Description:

After upgrading my project from Symfony 5.4 to 6.4, due this change, I encountered an issue where asynchronous email sending failed due to serialization problems with TemplatedEmail. According to the Symfony documentation, when sending an email asynchronously, the email instance must be serializable. While Mailer instances are inherently serializable, TemplatedEmail requires that its context be serializable. If the context contains non-serializable variables, such as Doctrine entities, it's recommended to either replace them with serializable variables or render the email before calling $mailer->send($email).

To address this, I decorated the Symfony Mailer to force the rendering of emails using the BodyRendererInterface::render(Message $message): void method. However, this approach led to an exception indicating that the context was empty. The method successfully rendered the email, clearing its context, but the same TemplatedEmail object was rendered again later in the execution process, specifically in the MessageListener.

To prevent this double rendering, I utilized the TemplatedEmail::isRendered(): bool method, as seen in the AbstractTransport. This ensures that if an email is already rendered, it won't be rendered again, preserving the context and preventing serialization issues.

Solution

The proposed fix involves checking if the TemplatedEmail has already been rendered before attempting to render it again. By using the isRendered() method, we can avoid unnecessary re-rendering, which can lead to empty contexts and serialization problems during asynchronous email sending.

Impact

This correction allows classes extending TemplatedEmail to be sent asynchronously without encountering errors related to double rendering or context loss due to serialization issues, as outlined in the documentation.

References

Symfony Mailer Documentation
MessageListener Implementation
AbstractTransport Implementation
This pull request aims to enhance the robustness of asynchronous email handling in Symfony by ensuring that TemplatedEmail instances are not rendered multiple times, thereby maintaining their context and serializability.

Original message:

Following up my comment on https://github.com/symfony/symfony/pull/47075/files.

I would recommend using the method TemplatedEmail::isRendered() the same way it is used in AbstractTransport.php.

Keeping like this prevents subclass of TemplatedEmail with computed html template to not be rendered twice, such as NotificationEmail.

This change will allow developers to create a subclass of template emails and render them before sending them async, without rendering them twice.

@nicolas-grekas
Copy link
Member

Would there be a way to test this?

@walva
Copy link
Contributor Author

walva commented Jan 28, 2025

@nicolas-grekas I need to take a closer look to it.

@OskarStark OskarStark changed the title [Mime] use isRendered method to ensure we can avoid rendering an email twice [Mime] use isRendered method to ensure we can avoid rendering an email twice Jan 29, 2025
@walva walva force-pushed the fix/cant-render-email-manually branch 4 times, most recently from c6c75a6 to 234c657 Compare January 30, 2025 09:17
@walva
Copy link
Contributor Author

walva commented Jan 30, 2025

Hey 👋

I reviewed the existing tests and found that the testRenderedOnce test already covers the core logic behind this fix. However, I added some additional checks to verify that the isRendered method behaves as expected.

Since the test below is very similar to testRenderedOnce, I feel like adding it might be redundant. However, I can still submit it if you think it adds value. Let me know what you prefer!

public function testEmailIsNotRenderedTwice()
{
    $twig = new Environment(new ArrayLoader([
        'text' => 'Initial Text',
        'html' => '<b>Initial HTML</b>',
    ]));
    $renderer = new BodyRenderer($twig);
    
    $email = new TemplatedEmail();
    $email->textTemplate('text');
    $email->htmlTemplate('html');

    // First render
    $renderer->render($email);
    $this->assertTrue($email->isRendered());
    $this->assertEquals('Initial Text', $email->getTextBody());
    $this->assertEquals('<b>Initial HTML</b>', $email->getHtmlBody());

    // Manually setting the text and HTML content to simulate a previous render
    $email->text('Manually Set Text');
    $email->html('Manually Set HTML');

    // Second render should not override manually set values
    $renderer->render($email);

    $this->assertEquals('Manually Set Text', $email->getTextBody());
    $this->assertEquals('Manually Set HTML', $email->getHtmlBody());
}

@walva walva force-pushed the fix/cant-render-email-manually branch from 234c657 to 2d37c79 Compare January 30, 2025 09:53
@nicolas-grekas
Copy link
Member

Thank you @walva.

@nicolas-grekas nicolas-grekas merged commit 0beb17a into symfony:6.4 Feb 4, 2025
9 of 11 checks passed
@walva walva deleted the fix/cant-render-email-manually branch February 10, 2025 07:38
This was referenced Feb 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants