Change record status: 
Project: 
Introduced in branch: 
11.x
Description: 

An experimental mailer module has been added which puts the necessary services in place such that bleeding-edge contrib and custom code can start using the mail delivery part of Symfony Mailer by simply retrieving / referencing a configured mailer service from / in the container.

The new mail API is subject to breaking changes. This change record will be updated with every iteration.

Setup

Enable the experimental mailer module. Then configure the transport DSN by specifying its components in the mailer_dsn config. The following examples may serve as a starting point:

  • For the default sendmail transport:
    $config['system.mail']['mailer_dsn'] = [
      'scheme' => 'sendmail',
      'host' => 'default',
    ];
    
  • For mailpit on localhost:
    $config['system.mail']['mailer_dsn'] = [
      'scheme' => 'smtp',
      'host' => 'localhost',
      'port' => 1025,
    ];
    
  • For authenticated SMTP:
    $config['system.mail']['mailer_dsn'] = [
      'scheme' => 'smtp',
      'host' => 'smtp.example.com',
      'user' => 'some-username@example.com',
      'password' => 'correct horse battery staple',
      'options' => [
        'local_domain' => 'example.org',
      ],
    ];
    

Preliminary Mail Delivery API

See issue #3379794: Add symfony mailer transports to Dependency Injection Container (mail delivery layer).

  • Service: Symfony\Component\Mailer\MailerInterface:
    Custom and contrib modules may use this service to pass mails to the mail delivery layer. This is the main entry point for the mail delivery layer.
  • Service: Symfony\Component\Mailer\Transport\TransportInterface:
    Custom and contrib modules may use this service to directly inject mails to the configured mail transport for delivery. This will skip Symfony messenger (if configured). This should only be necessary in advanced use cases.
  • Service: Drupal\Core\Mailer\TransportServiceFactoryInterface:
    Custom and contrib modules may decorate or replace this service in order to customize construction of Symfony\Component\Mailer\Transport\TransportInterface. This should only be necessary in advanced use cases. E.g., if certain messages are sent via dedicated transports.
  • Events MessageEvent, SentMessageEvent and FailedMessageEvent:
    Custom and contrib modules may register event subscribers to act on emails before and after they are sent.
  • Abstract service Symfony\Component\Mailer\Transport\AbstractTransportFactory and service tag mailer.transport_factory:
    Custom and contrib modules may supply third-party or custom transport factories using Symfony\Component\Mailer\Transport\AbstractTransportFactory as their parent service, tagged with mailer.transport_factory and typically with Symfony\Component\Mailer\Transport\AbstractTransportFactory as their parent class.
  • The mailer_sendmail_commands setting:
    An array of command lines which are allowed as the command option in the sendmail transport.

Example: Send a Message

The following example can serve as a starting point for experiments with the Symfony Mailer mail delivery API:

// Get the mailer instance from the container.
$mailer = $container->get(\Symfony\Component\Mailer\MailerInterface::class);

// Create a new message.
$email = new \Symfony\Component\Mime\Email();
$email->subject("Test message")
  ->from('test@localhost.localdomain')
  ->text('Hello test runner!');

try {
  $mailer->send($email->to('admin@localhost.localdomain'));
  // $messenger->addStatus($this->t('Sent test message'));
}
catch (RuntimeException $e) {
  // $messenger->addError($this->t('Failed to send test message'));
}

Example: Register a Third-Party Transport

Contrib and custom modules providing third-party transports supply their own service tagged with the mailer.transport_factory tag, deriving from the abstract transport class:

services:
  # Example of third-party service integration, requires symfony/google-mailer
  # https://packagist.org/packages/symfony/google-mailer
  Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory:
    parent: Symfony\Component\Mailer\Transport\AbstractTransportFactory
    tags:
      - { name: mailer.transport_factory }

Core Event Subscribers

OriginatorSubscriber

See issue #3397418: Ensure origin headers of mails sent using the symfony-based mailer service comply to RFC5322

The Drupal\Core\Mailer\EventSubscriber\OriginatorSubscriber acts on the MessageEvent with a very low priority of -255. It ensures that the From and Sender headers are set correctly according to RFC 5322 section 3.6.2. Especially:

  1. A From header is present. Defaults to the system.site.mail with system.site.name as the display name.
  2. A Sender header is present if-and-only-if necessary.

It doesn't change the From and/or the Sender header if it is already present on the message. It only removes the Sender if it is redundant according to the rules described in the RFC.

Preliminary Mail Building API

Not designed/implemented yet.

Preliminary Mail Capturing while Testing

See issue #3397420: Add a way to capture mails sent through the mailer transport service during tests.

Mails sent through the Symfony\Component\Mailer\MailerInterface and Symfony\Component\Mailer\Transport\TransportInterface services are discarded during tests. Enable the mailer_capture module in order to capture them form within a test case.

Mails captured by the mailer_capture module can be retrieved using the getMails() method from the Drupal\Core\Test\MailerCaptureTrait trait.

Example: Capture emails during test

class MailFormTest extends BrowserTestBase {

  use MailerCaptureTrait;

  protected static $modules = ['mailer', 'mailer_capture'];

  public function testMailForm(): void {
    // Before we send the email, getEmails should return an empty array.
    $capturedEmails = $this->getEmails();
    $this->assertCount(0, $capturedEmails, 'The captured emails queue is empty.');

    $this->drupalGet('/mailer-capture-test/send-mail');
    $this->submitForm([], 'Send Mail');

    // Ensure that there is one email in the captured emails array.
    $capturedEmails = $this->getEmails();

    $this->assertCount(1, $capturedEmails, 'One email was captured.');
  }
}
Impacts: 
Module developers