Problem/Motivation
Part of #1803948: [META] Adopt the symfony mailer component, adds symfony mailer and transports to the DIC. The scope of this issue is as follows:
Put the necessary services in place such that contrib and custom code can start using the mail delivery part of Symfony Mailer safely in test and in production code by simply retrieving / referencing a configured mailer service from / in the container.
- As a developer I want to retrieve a configured mailer service from the container in order to submit an e-mail message. Whether or not the message is queued or delivered immediately is subject to the site configuration.
- As a developer I want my service to reference a mailer service in order to submit an e-mail message. Whether or not the message is queued or delivered immediately is subject to the site configuration.
- As a developer I want my service to reference a configured transport in order to submit an e-mail message which is delivered immediately.
- As a developer I want that no mail leaves the system while executing functional and end-to-end tests.
- As a developer I want to add a third-party transport factory with its own DSN (using a module).
- As a developer I want to share modules providing third-party transports such that they can be used in combination with any Symfony Mailer based mail building contrib/custom module.
- As a developer I want to start adding message event subscriber for each
hook_mail_alter
implementation early on so as to be prepared when users start to switch to the new mail system.
The following items are out of scope in this issue:
- Service definitions for transports other than the ones included in the Transport directory of the Symfony Mailer component. In particular the transport factories provided under the Bridge are expected to be registered by custom or contrib modules and are out of scope.
- As a site owner I want to send emails via the
SMTP
protocol without installing contrib modules. - As a site owner I want to send emails using the
sendmail
binary without installing contrib modules. - As a site owner I want that mail sent by my site is using appropriate origin headers (otherwise mail will be often marked as spam or fail delivery entirely). Extracted to #3397418: [PP-1] Ensure origin headers of mails sent using the mailer.transport service comply to RFC5322
Steps to reproduce
Proposed resolution
-
Add mailer, transport and transport factory services in the same way as Symfony Framework Bundle:
Add abstract an abstract transport factory service and actual transport factories for the four concrete built-in ones (
null
,native
,sendmail
,smtp
):mailer.transport_factory.abstract: abstract: true class: Symfony\Component\Mailer\Transport\AbstractTransportFactory arguments: - '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface' - '@?Symfony\Contracts\HttpClient\HttpClientInterface' - '@logger.channel.mail' mailer.transport_factory.native: parent: mailer.transport_factory.abstract class: Symfony\Component\Mailer\Transport\NativeTransportFactory tags: - { name: mailer.transport_factory } [...]
-
Collect services tagged with
mailer.transport_factory
and use them to construct the Symfony Transport facade. Register that inmailer.transport_factory
(same service id as in Symfony Framework Bundle). Inject that intoTransportFactoryAdapter
which is used as a factory to construct the TransportInterface. Client code may retrieve / reference themailer.transport
service whenever it needs a configured transport.Note: Drupal doesn't store config in container params. Therefore an adapter is necessary which retrieves the
system.mail
mailer_dsn
config before it delegates transport construction to the Symfony Transport facade.mailer.transport_factory_collection: class: Drupal\Core\Mailer\TransportFactoryCollection public: false tags: - { name: service_collector, tag: mailer.transport_factory, call: addTransportFactory } Drupal\Core\Mailer\TransportFactoryCollection: '@mailer.transport_factory_collection' mailer.transport_factory: class: Symfony\Component\Mailer\Transport arguments: ['@Drupal\Core\Mailer\TransportFactoryCollection'] Symfony\Component\Mailer\Transport: '@mailer.transport_factory' mailer.transport_factory_adapter: class: Drupal\Core\Mailer\TransportFactoryAdapter autowire: true public: false Drupal\Core\Mailer\Transport\ConfiguredTransportFactoryInterface: '@mailer.transport_factory_adapter' mailer.transport: class: Symfony\Component\Mailer\Transport\TransportInterface factory: ['@Drupal\Core\Mailer\Transport\ConfiguredTransportFactoryInterface', 'createTransport'] Symfony\Component\Mailer\Transport\TransportInterface: '@mailer.transport'
-
Add the
mailer.mailer
service in a way which uses Symfony messenger automatically if available.mailer.messenger.message_handler: class: Symfony\Component\Mailer\Messenger\MessageHandler autowire: true public: false tags: - { name: messenger.message_handler } mailer.mailer: class: Symfony\Component\Mailer\Mailer arguments: - '@Symfony\Component\Mailer\Transport\TransportInterface' - '@?Symfony\Component\Messenger\MessageBusInterface' - '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface' Symfony\Component\Mailer\MailerInterface: '@mailer.mailer'
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.
Note: Symfony mailer provides many transports integrating with third-party providers. Products are rebranded, companies merge and split and they do certainly not respect the release cycle of Symfony or Drupal. Also some third-party transports will pull in additional dependencies (e.g. symfony/http-client
). Hence, core should avoid exposing those transports directly and instead leave that to contrib or custom code.
Remaining tasks
User interface changes
API changes
API additions:
- Service:
mailer.mailer
implementingSymfony\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. - Service:
mailer.transport
implementingSymfony\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:
mailer.transport_factory_adapter
implementingDrupal\Core\Mailer\Transport\ConfiguredTransportFactoryInterface
:
Custom and contrib modules may decorate or replace this service in order to customize construction ofmailer.transport
. This should only be necessary in advanced use cases. E.g., if certain messages are sent via dedicated transports. - Service:
mailer.transport_factory
, the Symfony\Component\Mailer\Transport facade:
Custom and contrib modules may use methods such as fromString() or fromDsnObject() to construct custom transports. This is for advanced use cases as well, the regular way of constructing transports is by retrievingmailer.mailer
service. - 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
mailer.transport_factory.abstract
and service tagmailer.transport_factory
:
Custom and contrib modules may supply third-party or custom transport factories usingmailer.transport_factory.abstract
as their parent service, tagged withmailer.transport_factory
and typically withSymfony\Component\Mailer\Transport\AbstractTransportFactory
as their parent class. - The
mailer_sendmail_commands
setting:
An array of command lines which are allowed as thecommand
option in the sendmail transport.
API examples:
There is a Sandbox with examples demonstrating how custom / contrib is supposed to use this API.
Architectural aspects
Code location (lib/Drupal/Core/Mailer vs modules/mailer)
Various options have been investigated in order to decide where to put production code.
Pro lib:
- A mailer component will very likely end up in lib. Essential core modules like
user
andsystem
require a mailer. - Moving classes from a mailer module to core once they are ready is cumbersome. Thus, let's put them in lib from the beginning.
Pro module:
- A mailer module can be marked experimental. That way it is possible to iterate on the code base without having to add BC layers from the very beginning.
Outcome:. A hybrid approach: Place mailer component directly in core and mark them internal. Register the services from within an experimental mailer module. That way the core classes don't need to be relocated. Going stable means just removing the internal flag from core classes and moving tests and service definitions into core.
Transport construction (mailer.transport service vs ad-hoc creation)
Various options have been investigated on how client code is supposed to create some instance of a TransportInterface
. All currently existing Symfony Mailer integrations in contrib create an instance of TransportInterface
in an ad-hoc manner whenever one is needed. The Symfony Framework Bundle on the other hand registers a mailer.transports
service in the container. It uses the ['@mailer.transport_factory', 'fromStrings']
as a factory method, the transport-DSN map is injected from a container parameter.
Pro mailer.transport service
- Other symfony components depend on a mailer transport being available from the container. E.g., the MessageHandler (used by the Symfony messenger component) passes an email message directly to the transport (from within a message queue).
- Some advanced transports maintain state (e.g., RoundRobinTransport). If a transport is created ad-hoc for every mail, that state is lost between invocations. If a transport instance is kept in the container, the same instance is used for subsequent mails.
Pro ad-hoc:
- Easier to implement with less services. (The Symfony mailer Transport facade doesn't work very well with the way Drupal is configured)
Outcome: Current implementation makes a TransportInterface
instance available in the container (service id mailer.transport
).