Create a new mailer service to wrap symfony mailer

Created on 18 June 2025, about 2 months ago

Problem/Motivation

Part of stage 2 of 🌱 [META] Adopt the symfony mailer component Active .

Create a new service that performs some additional Drupal-specific processing when sending an email. This mostly means copying various parts of the old mail system into the new one:

  1. Core MailManager
  2. Contrib mailsystem module
  3. Mailer plugins, such as Contrib Drupal Symfony Mailer Lite (DSM-L)
  4. plus potentially taking some new ideas from Contrib Drupal Symfony Mailer (DSM)

Proposed resolution

Class Mailer that implements MailerInterface. Functions of the mailer are:

  1. Set some defaults (currently in MailManager)
  2. Invoke events/callbacks (currently in MailManager)
  3. Switch render context to avoid pollution (currently in MailManager)
  4. Convert relative URLs to absolute (currently in MailManager)
  5. Ensure that subject is plain text (currently in MailManager)
  6. Switch theme (currently in mailsystem)
  7. Switch language (currently done in Commerce module, and would save more than half the code in user_mail())
  8. Switch account (currently done in Simplenews module, ensures security and personalisation)
  9. Render templates (currently done in DSM-L), which is a 2-stage process, first part module specific (done in simplenews, commerce) second part a generic wrapper which is generated separately for HTML and plain
  10. Convert to plain-text (currently done in DSM-L) - Symfony will do it, but in quite a different way from Drupal
  11. Inline CSS (currently done in DSM-L)
  12. Attachment access checking (some done in DSM-L)
  13. Act as a factory for Email objects (currently in DSM), which helps with dependency injection and allows Contrib code to override the Email class

The Email building pipeline proceeds in the following sequence:

  • Init phase: set language, theme, user account only
  • Switching: render context (to avoid leaking into current request), language, theme, user account (for access control and correct rendering of entities)
  • Build phase: allow modules to build the unrendered email
  • Rendering: render twig, replace tokens
  • Post-render phase: allow modules to process the rendered email
  • Send email
  • Post-send phase: allow modules to react to the sent email

The implementation can be divided between various classes, each one is independently replaceable for customisation.

  • Mailer class
  • Email class
  • Processor classes for specific function such as inline CSS

Remaining tasks

User interface changes

Introduced terminology

API changes

Possible interface for Mailer service:

  /**
   * Creates a new email.
   *
   * @param string $tag
   *   Tag used to identify the type or source of this email.
   *   @see \Drupal\symfony_mailer\EmailInterface::getTag()
   *
   * @return \Drupal\symfony_mailer\EmailInterface
   *   The new email.
   */
  public function newEmail(string $tag): EmailInterface;

  /**
   * Sends an email.
   *
   * @param \Drupal\symfony_mailer\InternalEmailInterface $email
   *   The email to send.
   *
   * @return bool
   *   Whether successful.
   */
  public function send(InternalEmailInterface $email): bool;

Example usage of the interface, taken from Contrib DSM module implementation of emails for the Core user module.


  /**
   * {@inheritdoc}
   */
  public function notify(string $op, UserInterface $user): bool {
    if ($op == 'register_pending_approval') {
      $this->newEmail("{$op}_admin")->setEntityParam($user)->send();
    }

    return $this->newEmail($op)
      ->setEntityParam($user)
      ->setTo($user)
      ->send();
  }


Data model changes

Release notes snippet

Feature request
Status

Active

Version

11.0 🔥

Component

mail system

Created by

🇬🇧United Kingdom adamps

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

Comments & Activities

  • Issue created by @adamps
  • 🇬🇧United Kingdom adamps

    Coming out of discussions at our last meeting I have been contemplating how we can progress efficiently with group agreement.

    I feel that we will move forward by making individual decisions. Each one is a specific choice in a single localised area from a defined set of options. First we make a choice regarding the existence of an EmailInterface, which if we agree, leads to an issue. Then within that issue we have separate choices about each of the fields that could be on it, leading to an actionable issue that can be coded (the coder can make any more detailed choices themselves). Probably we will make 100+ of these choices together as a group. It seems a lot, however many of them can be done in 2 minutes. I felt that @zengenuity mostly agrees (please correct me if I'm wrong).

    Lorenz says that he likes to think in code. I feel it would work well to make prototypes within one of the issues that we agreed as a group. We could have an issue for creating the component mail building code, and we could agree that for each idea someone had they would create a MR for only user module - maybe just 50 or 100 LOC. The author could explain the key points and advantages in a comment.

    However prototypes for the large parts of the system (500-1000 LOC) from my point of view are not good for making decisions because it is like making 50 choices at once, and it's too much to take in. Potentially they generate multiple issues = noise in the issue queues, sometimes duplicating other issues, and drawing the attention of outside reviewers to the prototype rather than the decisions. Personally speaking I wonder if this kind of prototyping might even be better on a personal gitlab clone or similar??

    How does anyone else feel?

Production build 0.71.5 2024