Introduce EmailTemplate config entity and plugin type

Created on 17 July 2025, 27 days ago

Problem/Motivation

Steps to reproduce

Proposed resolution

Introduce a new plugin type and a config entity type which work in tandem. I.e., use plugins to realize a strategy pattern (substitutable implementations) and the config entity to select one of them for each case. The combination of plugin type and config entity type is a known pattern in Drupal core. Examples for this architecture are Action, Block and Editor.

A sensible name for the plugin type and config entity in the mailer realm could be EmailTemplate.

The parts should roughly fit together as follows:

  • Each implementation (email template plugin) declares which type of email it is capable of rendering. The responsibility of an email template plugin is to turn input data of a specified type into an email data object.
  • Each call site (email sending code) declares the type of email and its specific variation it wishes to dispatch.
  • A config (email template entity) links the email type+variation combination to a specific template implementation.

Call site

The call site (email sending code) interacts with the email template config entity. Config entities are easy to deal with in both procedural and object oriented code. The render() call is forwarded to the plugin implementation.

Procedural example (error handling removed):

$account = User::load(1);
$template = EmailTemplate::load('user.password_reset');
$email = $template->render($account, $account->getPreferredLangcode());
$mailer->send($email->to($account->getEmail()));

EmailTemplate config entity

Config entities can track their dependencies. Config which is shipped in a config/optional directory is checked whenever a module is enabled or uninstalled. Optional config is installed as soon as all of its dependencies are met and removed whenever one of its dependencies breaks. It follows that the experimental mailer module can safely provide config for all mail sending core modules. Whenever the experimental mailer module is enabled, the subset of email template config entities having all dependencies met is installed. Whenever the experimental mailer module is disabled, all the email template config entities are removed and the system is config wise back to the point before enabling the mailer module.

Contrib and custom code can list the email template config entities. This represents an exhaustive list of all transactional mails potentially sent out from a site.

EmailTemplate plugin

Core may initially ship rudimentary mail template plugins mirroring the current behavior of core hook_mail implementations. Better implementations (e.g. based on entity view modes) can be added later in the development process.

Contrib mail sending modules (e.g., Commerce, ECA, Simplenews, Webform) can add their own mail template plugins and config entities for their own mail types.

Contrib mail building modules (e.g., DSM+, Easy Email) can ship additional mail template plugins for existing mail types and substitute the default implementations.

Remaining tasks

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

📌 Task
Status

Active

Version

11.0 🔥

Component

base system

Created by

🇨🇭Switzerland znerol

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @znerol
  • @znerol opened merge request.
  • 🇨🇭Switzerland znerol

    From #3534136-21: Requirements and strategy for Core Mailer by @berdir:

    Catching up on the discussion and merge requests. Long comment incoming...

    I don't think LOC is the primary factor to consider for this. Also because the current examples are way more complicated than they need to be due to feature flags, the experimental module and BC. The actual implementation would be a lot less.

    That said, I do agree that the config entity approach seems complex. Yes, config entity + plugin are a very common pattern, but sometimes also overused. This feels similar to action plugins and config entities, which are often pretty awkward as it's typically a 1:1 relationship and tedious for modules to maintain the config. In general, code that is hardcoding specific config entity ID's is a bit of a code smell to me. As @adamps said, it's unclear what would happen if that config is missing. For example now with recipes, which pretty much *encourages* installing modules without their config ( I don't really agree with that, but that's another topic). Deleting the password reset template

    Naming: I think the template part is misleading. That makes me think of twig templates, HTML and customizing their output. Looking at DSM, it uses build() instead of render(), which makes a lot more sense to me. We build an Email object, which might or might not involve rendering something or using templates, but it's not the main thing. It's also similar to terminology used in simplenews.

    An issue with the current architecture is that there's a bidirectional dependency between the plugin and the config entity, as it's passed to render. If you compare with block plugins for example, they don't know about block.module block config entities and can be used in other contexts as well. Similar for field formatters, which are also used by views. That's what $configuration is for, the config entity is responsible for storing and passing that in.

    Now, there are cases where a fully implemented config entity + plugin could be useful. For example with user, the plugin would be provide a subject and body setting which would make it self-contained instead of accessing global config (which currently bypasses your attempt at making not just mails but also their data and arguments documented and discoverable). and status is built-in, so the code could check that. But it would mean that the UI would move from user settings to generic mail configuration, which is likely harder to find. It's also not as flexible as you'd be used to from plugins. you can't make up your own notifications, you can only enable/disable and edit the specific settings of them. In the end, pretty limited benefit over the current config and some drawbacks.

    contact, update are examples that don't really benefit from there being a config entity, and that's going to be a lot of contrib and custom examples. That's combined with the "problem" that the mail rendering/preparing sits entirely on top of the actual mail send API and is there fore optional. People looking at this and trying to send mails will just directly create the Email object and send it directly. And if they do that, we lose the ability to use a mail theme because rendering of stuff possibly already happened. I think we should make it as easy as possible to use the mail API without losing features like language. And then optional features on top. If you look at commerce_mail(), they essentially bypass the whole thing by just passing in the body and rendering it there. I wonder if we should actually allow to use a callback based approach for those minimal cases, similar to what renderInContext() essentially already is.

    I'm not sure how important discoverability and reusability of mail "templates" really is, it might in some cases even be undesired. We recently discussed that ECA explicitly doesn't support those special user password reset mails, to not allow to send them out in the wrong context, that could even be a security issue.

    On the other side of the spectrum, use cases that actually have generic and complex use cases around mails might struggle to fit into this architecture. Simplenews has multiple things that make up a newsletter, specifically a entity that sent to a subscriber and there's a lot of rendering and logic going on that should happen within the mail render context. And ECA would need to redefine its mails that sit in its configuration or have a super generic one that would almost become recursive.

    I think I get the intent with the types and data, but that really feels complex to use. the user confirm thing requires a data type plugin, a definition class, it's passed in as a raw value, converted to typed data and then used. It values (theoretical) reusability over DX, despite all that complexity, what you work with when writing the code is an array with arbitrary keys. I see that DSM has the concept of mail params on the object. That's still keys and not types that can be enforced, but I think that's an improvement over the single magic mixed type that this has. With mulitple params, we can still document them and their type.

    I've even been thinking about going entirely with plain value objects implementing a basic interface, but haven't fully thought that through. But overall I'd clearly recommend keeping the actual mail builder interface/API as plain and un-opinionated as possible and start with that.

  • 🇨🇭Switzerland znerol

    We decided against the config + plugin approach in our last meeting. Due to the risks of config going missing and due to the fact that recipes do not encourage installing default config.

    An issue with the current architecture is that there's a bidirectional dependency between the plugin and the config entity, as it's passed to render. If you compare with block plugins for example, they don't know about block.module block config entities and can be used in other contexts as well. Similar for field formatters, which are also used by views. That's what $configuration is for, the config entity is responsible for storing and passing that in.

    That is crucial and it has bothered me a lot. I think I'll try to decouple the config from the plugin and then use that to branch off for coming experiments.

  • 🇨🇭Switzerland znerol

    Approach abandoned in 📌 [meeting] 2025-07-29 core mailer beta planning Active .

Production build 0.71.5 2024