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 ofSymfony\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\AbstractTransportFactoryand service tagmailer.transport_factory:
Custom and contrib modules may supply third-party or custom transport factories usingSymfony\Component\Mailer\Transport\AbstractTransportFactoryas their parent service, tagged withmailer.transport_factoryand typically withSymfony\Component\Mailer\Transport\AbstractTransportFactoryas their parent class. - The
mailer_sendmail_commandssetting:
An array of command lines which are allowed as thecommandoption 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
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:
- A
Fromheader is present. Defaults to thesystem.site.mailwithsystem.site.nameas the display name. - A
Senderheader 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
See issue #3539651: Introduce email plugins
An email plugin is registered to a specific module.key pair in an {module}.emails.yml file. This file lists all emails and specifies how those are built by default:
email_tester.from_template:
label: 'Twig template email'
description: 'Email with twig template'
module: 'email_tester'
key: 'from_template'
html_body_template: 'email_tester_html_body'
text_body_template: 'email_tester_text_body'
email_tester.with_class:
label: 'Email with custom class'
description: 'Email with custom class'
module: 'email_tester'
key: 'with_class'
class: '\Drupal\email_tester\Email\CustomEmail'
For more complex cases, a plugin class can be registered using the #[Email] attribute:
#[Email(
id: 'MYMODULE.complex_email',
label: new TranslatableMarkup('Complex'),
description: new TranslatableMarkup('Complex email'),
)]
d}[startinline]{php}
class ComplexEmail extends EmailPluginDefault implements EmailPluginInterface {
public function htmlBody(): RenderableInterface|array|null {
$result [];
[...]
return $result;
}
}
Email plugins
A default email plugin covers simple cases. You can specify twig templates in your YAML declaration that will be used for the HTML and/or text bodies of the email. These twig templates can use parameters, which are mapped to twig variables for use in the templates.
If that is not enough, a custom plugin class may override whatever is necessary.
Call site
In order to send a registered email, the call site uses the email plugin manager to create an email plugin instance.
$email = $this->emailManager->createInstance('email_tester.from_template');
$email->subject(t('Test from template'))
->sender('from@example.com')
->to('to@example.com')
->langcode('en')
->params([
'name' => 'John Doe',
'bill_amount' => '$123.45',
]);
$result = $this->emailManager->send($email);
Templates and Theming
Email sending core, contrib and custom modules are responsible to declare the templates in a hook_theme needed to construct an email.
Hooks
Third parties may modify any email about to be rendered and sent using the following hooks:
- hook_email_info_alter
- Alter all the email definitions discovered by the email plugin manager.
- hook_email_pre_build_alter
- Alter the email plugin instance before the email message is built.
- hook_email_pre_render_alter
- Alter the email data array generated by the email plugin.
- hook_email_post_render_alter
- Alter the rendered Symfony email object.
- hook_email_pre_send_alter
- Alter the email plugin instance before the email message is sent.
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.');
}
}