Problem/Motivation

Part of stage 2 of #1803948: [META] Adopt the symfony mailer component. Create a new API with direct access to all the power of Symfony Mailer. A key part of the new mailsystem is to support HTML mail. We can reuse the excellent systems already in core used for creating HTML pages: render arrays, twig templates with variables, themes, various alter hooks, CSS libraries. (Let's not go back to Drupal 6 times with hooks trying to alter HTML that has been rendered and translated!)

The new API enhances the symfony code with integration into Drupal mechanisms, which mostly means copying various parts of the old mail system into the new one:

  • Core MailManagerInterface and MailInterface
  • Contrib mailsystem module
  • Mailer plugins, such as Contrib Drupal Symfony Mailer Lite (DSM-L)
  • plus potentially taking some new ideas from Contrib Drupal Symfony Mailer (DSM)

Proposed resolution

New class Email implements EmailInterface. This enhances the symfony base class with some new fields:

  • The tag/category that identifies the emails and is used by events/processors, templates etc. This corresponds to module/key in Core.
  • Parameters that define the email to send, according to its tag/category. Matches params in Core.
  • The language for the email = langcode in Core
  • Unrendered HTML body as a Drupal render array. A module that sends mail creates this then it can be altered before rendering. This roughly corresponds to body on Core MailInterface, except that it's a proper render array rather than a mix of HTML and plain text strings.
  • Variables to use in Twig template. Each module that sends email will provide their own. Corresponds to template_preprocess_symfony_mailer_lite_email(), template_preprocess_simplenews_newsletter_body(), etc.
  • Theme to use for templates and CSS. Can be set per-category in mailsystem. We could add it here to be more flexible and to bring all the parameters onto the same interface.
  • Transport. Can be set per-category in mailsystem, see reasoning in previous bullet.
  • User account. Set in Contrib module simplenews, and relevant when rendering entities. For security, recommend switching to anonymous if no specific user is requested to ensure that the access of the current admin user is not used when sending to an ordinary user.
  • CSS libraries to attach. Hard-coded in DSM-L. Contrib DSM allows modules to add libraries with their own email CSS. We could instead get them to add CSS to the single hard-coded library.

Some parts of the base Symfony email class are not ideally suited to our needs. The last 2 items in this list would likely be deferred to follow-on issues and are introduced with limited details. The point is that they provide additional reasons why it might be difficult to extend the Symfony class directly.

  • In general, Symfony can use private fields and final classes, blocking flexibility.
  • Core detects a subject that implements MarkupInterface, and converts to plain text, hence avoiding accidental tags appearing. Symfony doesn't allow that because it insists the subject is a string.
  • We might prefer to rename setter method to match Drupal standard, e.g. setTo() rather than to().
  • In contrib, we enhanced the address functions so you could pass a UserAccount directly, automatically setting the email, display name and langcode. This has worked well to simplify code and reduce bugs. Symfony accepts any class on the address setter functions, but it returns a final class on the getter functions, which prevents this from working properly
  • In contrib, we re-implemented the handling of attachments for 3 reasons: support getting and altering of attachments; add access-checking (there have already been security issues with other modules failing to sufficiently check attachment access) and to avoid forcing the calling code to pick a unique identifier (obviously difficult in the Drupal environment when attachments might come from various unrelated pieces of code or from config.

We have a choice whether to enhance the base class by extending or decorating (meaning to have a class variable $inner containing the base class). In Contrib DSM we decorated for the above reasons, and because it gave us a clean start, isolated from some of details of symfony that don't apply so much to Drupal. This has worked well.

Remaining tasks

User interface changes

None

API changes

See proposed resolution. Example usage ContactMailer::build() from DSM that builds emails for the Core contact module.

  /**
   * {@inheritdoc}
   */
  public function build(EmailInterface $email): void {
    /** @var \Drupal\symfony_mailer\Address $sender */
    $sender = $email->getParam('sender');
    $message = $email->getParam('contact_message');
    $sender_name = $sender->getDisplayName();
    $sender_email = $sender->getEmail();

    if ($account = $sender->getAccount()) {
      $sender_url = User::load($account->id())->toUrl('canonical')->toString();
    }
    else {
      // Clarify that the sender name is not verified; it could potentially
      // clash with a username on this site.
      $sender_name = $this->t('@name (not verified)', ['@name' => $sender_name]);
      $sender_url = $sender_email ? Url::fromUri("mailto:$sender_email") : '';
    }

    $email->setEntityVariable('contact_message')
      ->addLibrary('symfony_mailer/contact')
      ->setVariable('subject', $message->getSubject())
      ->setVariable('site_name', \Drupal::config('system.site')->get('name'))
      ->setVariable('sender_name', $sender_name)
      ->setVariable('sender_url', $sender_url);

    if ($message->isPersonal()) {
      $recipient = $email->getParam('recipient');
      $email->setVariable('recipient_name', $recipient->getDisplayName())
        ->setVariable('recipient_edit_url', $recipient->toUrl('edit-form')->toString());
    }
    else {
      $email->setVariable('form', $email->getParam('contact_form')->label())
        ->setVariable('form_url', Url::fromRoute('<current>')->toString());
    }

    if ($email->getTag(2) == 'mail' && $sender_email) {
      $email->setReplyTo($sender);
    }
  }

Data model changes

None

Release notes snippet

Comments

AdamPS created an issue. See original summary.

adamps’s picture

Issue summary: View changes
adamps’s picture

Issue summary: View changes
adamps’s picture

Assigned: adamps » Unassigned
adamps’s picture

Issue summary: View changes
adamps’s picture

Issue summary: View changes
adamps’s picture

Title: Create new mailer service based on symfony » Create a new email interface that extends symfony
Issue summary: View changes
adamps’s picture

Issue summary: View changes
adamps’s picture

adamps’s picture

Title: Create a new email interface that extends symfony » Create a new email interface that decorates symfony

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.