Skip to content

Navigation Menu

Sign in
Appearance settings
Sign up
Appearance settings

[Mailer] Add RemoteTemplateEmail to send emails rendered from provide… - #64782

#64782
Open
Orkin wants to merge 1 commit into
symfony:8.2symfony/symfony:8.2from
Orkin:mailer-remote-templatesOrkin/symfony:mailer-remote-templatesCopy head branch name to clipboard
Open

[Mailer] Add RemoteTemplateEmail to send emails rendered from provide…#64782
Orkin wants to merge 1 commit into
symfony:8.2symfony/symfony:8.2from
Orkin:mailer-remote-templatesOrkin/symfony:mailer-remote-templatesCopy head branch name to clipboard

Conversation

@Orkin

@Orkin Orkin commented Jul 2, 2026

Copy link
Copy Markdown
Contributor
Q A
Branch? 8.2
Bug fix? no
New feature? yes
Deprecations? yes
Issues -
License MIT

Motivation

Most transactional email providers can render an email from a template hosted on their side, configured with a template reference and a set of variables. Symfony Mailer currently has no abstraction for this, which has led to inconsistent, bridge-specific hacks:

Previous attempts to extend this header-based approach to other bridges (#63196, #63223) were rightfully rejected: repurposing header names inside a bridge creates hidden restrictions and breaks the transport abstraction.

However, the underlying feature is interoperable — virtually every provider with a Symfony bridge exposes the exact same pair {template reference, variables map}:

Provider Template reference Variables
Brevo templateId (int) params
Mailjet TemplateID (int) Variables
Mailgun template (name) t:variables
SendGrid template_id dynamic_template_data
Postmark TemplateId / TemplateAlias TemplateModel
Mandrill template_name merge_vars
MailerSend template_id personalization
Mailtrap template_uuid template_variables
Amazon SES v2 Template.TemplateName TemplateData
Resend template.id template.variables

This is the same situation that led to TagHeader / MetadataHeader: one common concept, provider-specific payload mappings. This PR follows that precedent, but without headers.

Design

use Symfony\Component\Mailer\RemoteTemplateEmail;

$email = (new RemoteTemplateEmail())
    ->from('hello@example.com')
    ->to('user@example.com')
    ->template('welcome-v2', ['firstName' => 'Fabien', 'orderId' => 42]);

$mailer->send($email);
  • RemoteTemplateEmail extends Email carries the template reference (a string; each bridge casts to its native type) and the variables. Since the body is rendered by the provider, ensureValidity() allows sending without a local text/HTML part — and rejects an email that defines both a local body and a template. A local subject remains allowed (providers support overriding it). Serialization is preserved so async sending via Messenger keeps the template.
  • RemoteTemplateTransportInterface is a marker interface implemented by transports that support the feature. Each one maps the reference/variables to its native API payload.
  • Explicit failure instead of hidden behavior: AbstractTransport::send() throws a LogicException when a RemoteTemplateEmail with a template is sent through a transport that does not implement the interface (SMTP transports, sendmail, not-yet-migrated bridges…). No header name is reserved, nothing is silently dropped. NullTransport implements the interface so null:// setups keep working.

This addresses the concerns raised on the previous attempts:

  1. "Special meaning for header names" → no headers involved; the feature is part of the component's API surface.
  2. "Bridges are not SDKs" → bridges only map one interoperable concept, exactly like they already do for tags and metadata.
  3. "No interoperable common case" → see the table above; the abstraction is limited to the {reference, variables} pair that all providers share. Anything provider-specific beyond that stays out of scope.

What this PR does

  • Add RemoteTemplateEmail and RemoteTemplateTransportInterface to symfony/mailer, with the fail-fast check in AbstractTransport;
  • change Email::ensureBodyValid() from private to protected in symfony/mime (BC-safe) so the subclass can adjust body validation;
  • implement the interface in three bridges with different payload styles to demonstrate the mapping: Resend (template.id / template.variables), Brevo (templateId / params), SendGrid (template_id / dynamic_template_data);
  • deprecate the templateid and params magic headers of the Brevo bridge in favor of the new abstraction.

Out of scope (by design):

  • per-recipient variables / batch personalizations: one message = one set of variables;
  • any other provider-specific template option.

Other bridges (Mailjet, Postmark, Mailtrap, MailerSend, Mailgun, SES…) can follow in separate PRs, including SMTP transports of providers whose relay supports templates through documented headers (e.g. Mailjet's X-MJ-TemplateID).

{
/**
* @param string $reference The provider-side reference of the template (an id, uuid, name or alias, depending on the provider)
* @param array $variables The variables used by the provider to render the template

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should define the proper type for the array keys and values (I assume keys are strings here, not sure about values)


/**
* @param string|null $template The provider-side reference of the template (an id, uuid, name or alias, depending on the provider)
* @param array $variables The variables used by the provider to render the template

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should also define the proper type

8.2
---

* Add support for sending emails rendered from a remote template via `RemoteTemplateEmail`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this changelog entry is confusing IMO, as this is implemented only for one transport of the bridge, not for all of them.

}

private function ensureBodyValid(): void
protected function ensureBodyValid(): void

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding a new extension point should probably have a changelog entry in the component.

…r-hosted templates

Add a first-class abstraction for emails rendered by the mail provider
from a template hosted on its side:

* `RemoteTemplateEmail` carries a template reference and its variables,
  and allows sending without a local text/HTML body;
* transports opt in by implementing `RemoteTemplateTransportInterface`
  and mapping the template to their native API payload; other transports
  fail with an explicit exception instead of silently dropping the
  template;
* implement the interface in the Brevo, Resend and Sendgrid bridges;
* deprecate the "templateid" and "params" magic headers of the Brevo
  bridge in favor of the new abstraction.
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

Morty Proxy This is a proxified and sanitized view of the page, visit original site.