`
to provide advanced features such as CSS style inlining and support for HTML/CSS
frameworks to create complex HTML email messages. First, make sure Twig is installed:
.. code-block:: terminal
$ composer require symfony/twig-bundle
# or if you're using the component in a non-Symfony app:
# composer require symfony/twig-bridge
HTML Content
~~~~~~~~~~~~
To define the contents of your email with Twig, use the
:class:`Symfony\\Bridge\\Twig\\Mime\\TemplatedEmail` class. This class extends
the normal :class:`Symfony\\Component\\Mime\\Email` class but adds some new methods
for Twig templates::
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
$email = (new TemplatedEmail())
->from('fabien@example.com')
->to(new Address('ryan@example.com'))
->subject('Thanks for signing up!')
// path of the Twig template to render
->htmlTemplate('emails/signup.html.twig')
// change locale used in the template, e.g. to match user's locale
->locale('de')
// pass variables (name => value) to the template
->context([
'expiration_date' => new \DateTime('+7 days'),
'username' => 'foo',
])
;
Then, create the template:
.. code-block:: html+twig
{# templates/emails/signup.html.twig #}
Welcome {{ email.toName }}!
You signed up as {{ username }} the following email:
{{ email.to[0].address }}
Activate your account
(this link is valid until {{ expiration_date|date('F jS') }})
The Twig template has access to any of the parameters passed in the ``context()``
method of the ``TemplatedEmail`` class and also to a special variable called
``email``, which is an instance of
:class:`Symfony\\Bridge\\Twig\\Mime\\WrappedTemplatedEmail`.
Text Content
~~~~~~~~~~~~
When the text content of a ``TemplatedEmail`` is not explicitly defined, it is
automatically generated from the HTML contents.
Symfony uses the following strategy when generating the text version of an
email:
* If an explicit HTML to text converter has been configured (see
:ref:`twig.mailer.html_to_text_converter
`), it calls it;
* If not, and if you have `league/html-to-markdown`_ installed in your
application, it uses it to turn HTML into Markdown (so the text email has
some visual appeal);
* Otherwise, it applies the :phpfunction:`strip_tags` PHP function to the
original HTML contents.
If you want to define the text content yourself, use the ``text()`` method
explained in the previous sections or the ``textTemplate()`` method provided by
the ``TemplatedEmail`` class:
.. code-block:: diff
+use Symfony\Bridge\Twig\Mime\TemplatedEmail;
$email = (new TemplatedEmail())
// ...
->htmlTemplate('emails/signup.html.twig')
+ ->textTemplate('emails/signup.txt.twig')
// ...
;
.. _mailer-twig-embedding-images:
Embedding Images
~~~~~~~~~~~~~~~~
Instead of dealing with the ``
`` syntax explained in the
previous sections, when using Twig to render email contents you can refer to
image files as usual. First, to simplify things, define a Twig namespace called
``images`` that points to whatever directory your images are stored in:
.. configuration-block::
.. code-block:: yaml
# config/packages/twig.yaml
twig:
# ...
paths:
# point this wherever your images live
'%kernel.project_dir%/assets/images': images
.. code-block:: xml
%kernel.project_dir%/assets/images
.. code-block:: php
// config/packages/twig.php
use Symfony\Config\TwigConfig;
return static function (TwigConfig $twig): void {
// ...
// point this wherever your images live
$twig->path('%kernel.project_dir%/assets/images', 'images');
};
Now, use the special ``email.image()`` Twig helper to embed the images inside
the email contents:
.. code-block:: html+twig
{# '@images/' refers to the Twig namespace defined earlier #}
Welcome {{ email.toName }}!
{# ... #}
By default this will create an attachment using the file path as file name:
``Content-Disposition: inline; name="cid..."; filename="@images/logo.png"``.
This behavior can be overridden by passing a custom file name as the third argument:
.. code-block:: html+twig
.. versionadded:: 7.3
The third argument of ``email.image()`` was introduced in Symfony 7.3.
.. _mailer-inline-css:
Inlining CSS Styles
~~~~~~~~~~~~~~~~~~~
Designing the HTML contents of an email is very different from designing a
normal HTML page. For starters, most email clients only support a subset of all
CSS features. In addition, popular email clients like Gmail don't support
defining styles inside ```` sections and you must **inline
all the CSS styles**.
CSS inlining means that every HTML tag must define a ``style`` attribute with
all its CSS styles. This can make organizing your CSS a mess. That's why Twig
provides a ``CssInlinerExtension`` that automates everything for you. Install
it with:
.. code-block:: terminal
$ composer require twig/extra-bundle twig/cssinliner-extra
The extension is enabled automatically. To use it, wrap the entire template
with the ``inline_css`` filter:
.. code-block:: html+twig
{% apply inline_css %}
Welcome {{ email.toName }}!
{# ... #}
{% endapply %}
Using External CSS Files
........................
You can also define CSS styles in external files and pass them as
arguments to the filter:
.. code-block:: html+twig
{% apply inline_css(source('@styles/email.css')) %}
Welcome {{ username }}!
{# ... #}
{% endapply %}
You can pass unlimited number of arguments to ``inline_css()`` to load multiple
CSS files. For this example to work, you also need to define a new Twig namespace
called ``styles`` that points to the directory where ``email.css`` lives:
.. _mailer-css-namespace:
.. configuration-block::
.. code-block:: yaml
# config/packages/twig.yaml
twig:
# ...
paths:
# point this wherever your css files live
'%kernel.project_dir%/assets/styles': styles
.. code-block:: xml
%kernel.project_dir%/assets/styles
.. code-block:: php
// config/packages/twig.php
use Symfony\Config\TwigConfig;
return static function (TwigConfig $twig): void {
// ...
// point this wherever your css files live
$twig->path('%kernel.project_dir%/assets/styles', 'styles');
};
.. _mailer-markdown:
Rendering Markdown Content
~~~~~~~~~~~~~~~~~~~~~~~~~~
Twig provides another extension called ``MarkdownExtension`` that lets you
define the email contents using `Markdown syntax`_. To use this, install the
extension and a Markdown conversion library (the extension is compatible with
several popular libraries):
.. code-block:: terminal
# instead of league/commonmark, you can also use erusev/parsedown or michelf/php-markdown
$ composer require twig/extra-bundle twig/markdown-extra league/commonmark
The extension adds a ``markdown_to_html`` filter, which you can use to convert parts or
the entire email contents from Markdown to HTML:
.. code-block:: twig
{% apply markdown_to_html %}
Welcome {{ email.toName }}!
===========================
You signed up to our site using the following email:
`{{ email.to[0].address }}`
[Activate your account]({{ url('...') }})
{% endapply %}
.. _mailer-inky:
Inky Email Templating Language
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. tip::
Symfony recommends Inky, but you can also use `MJML`_, a more actively
maintained alternative for responsive email templates.
Creating beautifully designed emails that work on every email client is so
complex that there are HTML/CSS frameworks dedicated to that. One of the most
popular frameworks is called `Inky`_. It defines a syntax based on some HTML-like
tags which are later transformed into the real HTML code sent to users:
.. code-block:: html
This is a column.
Twig provides integration with Inky via the ``InkyExtension``. First, install
the extension in your application:
.. code-block:: terminal
$ composer require twig/extra-bundle twig/inky-extra
The extension adds an ``inky_to_html`` filter, which can be used to convert
parts or the entire email contents from Inky to HTML:
.. code-block:: html+twig
{% apply inky_to_html %}
{% endapply %}
You can combine all filters to create complex email messages:
.. code-block:: twig
{% apply inky_to_html|inline_css(source('@styles/foundation-emails.css')) %}
{# ... #}
{% endapply %}
This makes use of the :ref:`styles Twig namespace ` we created
earlier. You could, for example, `download the foundation-emails.css file`_
directly from GitHub and save it in ``assets/styles``.
.. _signing-and-encrypting-messages:
Signing and Encrypting Messages
-------------------------------
It's possible to sign and/or encrypt email messages to increase their
integrity/security. Both options can be combined to encrypt a signed message
and/or to sign an encrypted message.
Before signing/encrypting messages, make sure to have:
* The `OpenSSL PHP extension`_ properly installed and configured;
* A valid `S/MIME`_ security certificate.
.. tip::
When using OpenSSL to generate certificates, make sure to add the
``-addtrust emailProtection`` command option.
.. warning::
Signing and encrypting messages require their contents to be fully rendered.
For example, the content of :ref:`templated emails ` is rendered
by a :class:`Symfony\\Component\\Mailer\\EventListener\\MessageListener`.
So, if you want to sign and/or encrypt such a message, you need to do it in
a :ref:`MessageEvent ` listener run after it (you need to set
a negative priority to your listener).
Signing Messages
~~~~~~~~~~~~~~~~
When signing a message, a cryptographic hash is generated for the entire content
of the message (including attachments). This hash is added as an attachment so
the recipient can validate the integrity of the received message. However, the
contents of the original message are still readable for mailing agents not
supporting signed messages, so you must also encrypt the message if you want to
hide its contents.
You can sign messages using either ``S/MIME`` or ``DKIM``. In both cases, the
certificate and private key must be `PEM encoded`_, and can be either created
using for example OpenSSL or obtained at an official Certificate Authority (CA).
The email recipient must have the CA certificate in the list of trusted issuers
in order to verify the signature.
.. warning::
If you use message signature, sending to ``Bcc`` will be removed from the
message. If you need to send a message to multiple recipients, you need
to compute a new signature for each recipient.
S/MIME Signer
.............
`S/MIME`_ is a standard for public key encryption and signing of MIME data. It
requires using both a certificate and a private key::
use Symfony\Component\Mime\Crypto\SMimeSigner;
use Symfony\Component\Mime\Email;
$email = (new Email())
->from('hello@example.com')
// ...
->html('...');
$signer = new SMimeSigner('/path/to/certificate.crt', '/path/to/certificate-private-key.key');
// if the private key has a passphrase, pass it as the third argument
// new SMimeSigner('/path/to/certificate.crt', '/path/to/certificate-private-key.key', 'the-passphrase');
$signedEmail = $signer->sign($email);
// now use the Mailer component to send this $signedEmail instead of the original email
.. tip::
The ``SMimeSigner`` class defines other optional arguments to pass
intermediate certificates and to configure the signing process using a
bitwise operator options for :phpfunction:`openssl_pkcs7_sign` PHP function.
DKIM Signer
...........
`DKIM`_ is an email authentication method that affixes a digital signature,
linked to a domain name, to each outgoing email messages. It requires a private
key but not a certificate::
use Symfony\Component\Mime\Crypto\DkimSigner;
use Symfony\Component\Mime\Email;
$email = (new Email())
->from('hello@example.com')
// ...
->html('...');
// first argument: same as openssl_pkey_get_private(), either a string with the
// contents of the private key or the absolute path to it (prefixed with 'file://')
// second and third arguments: the domain name and "selector" used to perform a DNS lookup
// (the selector is a string used to point to a specific DKIM public key record in your DNS)
$signer = new DkimSigner('file:///path/to/private-key.key', 'example.com', 'sf');
// if the private key has a passphrase, pass it as the fifth argument
// new DkimSigner('file:///path/to/private-key.key', 'example.com', 'sf', [], 'the-passphrase');
$signedEmail = $signer->sign($email);
// now use the Mailer component to send this $signedEmail instead of the original email
// DKIM signer provides many config options and a helper object to configure them
use Symfony\Component\Mime\Crypto\DkimOptions;
$signedEmail = $signer->sign($email, (new DkimOptions())
->bodyCanon('relaxed')
->headerCanon('relaxed')
->headersToIgnore(['Message-ID'])
->toArray()
);
Signing Messages Globally
.........................
Instead of creating a signer instance for each email, you can configure a global
signer that automatically applies to all outgoing messages. This approach
minimizes repetition and centralizes your configuration for DKIM and S/MIME signing.
.. configuration-block::
.. code-block:: yaml
# config/packages/mailer.yaml
framework:
mailer:
dkim_signer:
key: 'file://%kernel.project_dir%/var/certificates/dkim.pem'
domain: 'symfony.com'
select: 's1'
smime_signer:
key: '%kernel.project_dir%/var/certificates/smime.key'
certificate: '%kernel.project_dir%/var/certificates/smime.crt'
passphrase: ''
.. code-block:: xml
file://%kernel.project_dir%/var/certificates/dkim.pem
symfony.com
s1
%kernel.project_dir%/var/certificates/smime.pem
%kernel.project_dir%/var/certificates/smime.crt
.. code-block:: php
// config/packages/mailer.php
use Symfony\Config\FrameworkConfig;
return static function (FrameworkConfig $framework): void {
$mailer = $framework->mailer();
$mailer->dsn('%env(MAILER_DSN)%');
$mailer->dkimSigner()
->key('file://%kernel.project_dir%/var/certificates/dkim.pem')
->domain('symfony.com')
->select('s1');
$mailer->smimeSigner()
->key('%kernel.project_dir%/var/certificates/smime.key')
->certificate('%kernel.project_dir%/var/certificates/smime.crt')
->passphrase('')
;
};
.. versionadded:: 7.3
Global message signing was introduced in Symfony 7.3.
Encrypting Messages
~~~~~~~~~~~~~~~~~~~
When encrypting a message, the entire message (including attachments) is
encrypted using a certificate. Therefore, only the recipients that have the
corresponding private key can read the original message contents::
use Symfony\Component\Mime\Crypto\SMimeEncrypter;
use Symfony\Component\Mime\Email;
$email = (new Email())
->from('hello@example.com')
// ...
->html('...');
$encrypter = new SMimeEncrypter('/path/to/certificate.crt');
$encryptedEmail = $encrypter->encrypt($email);
// now use the Mailer component to send this $encryptedEmail instead of the original email
You can pass more than one certificate to the ``SMimeEncrypter`` constructor
and it will select the appropriate certificate depending on the ``To`` option::
$firstEmail = (new Email())
// ...
->to('jane@example.com');
$secondEmail = (new Email())
// ...
->to('john@example.com');
// the second optional argument of SMimeEncrypter defines which encryption algorithm is used
// (it must be one of these constants: https://www.php.net/manual/en/openssl.ciphers.php)
$encrypter = new SMimeEncrypter([
// key = email recipient; value = path to the certificate file
'jane@example.com' => '/path/to/first-certificate.crt',
'john@example.com' => '/path/to/second-certificate.crt',
]);
$firstEncryptedEmail = $encrypter->encrypt($firstEmail);
$secondEncryptedEmail = $encrypter->encrypt($secondEmail);
Encrypting Messages Globally
............................
Instead of creating a new encrypter for each email, you can configure a global S/MIME
encrypter that automatically applies to all outgoing messages:
.. configuration-block::
.. code-block:: yaml
# config/packages/mailer.yaml
framework:
mailer:
smime_encrypter:
repository: App\Security\LocalFileCertificateRepository
.. code-block:: xml
App\Security\LocalFileCertificateRepository
.. code-block:: php
// config/packages/mailer.php
use App\Security\LocalFileCertificateRepository;
use Symfony\Config\FrameworkConfig;
return static function (FrameworkConfig $framework): void {
$mailer = $framework->mailer();
$mailer->smimeEncrypter()
->repository(LocalFileCertificateRepository::class)
;
};
The ``repository`` option is the ID of a service that implements
:class:`Symfony\\Component\\Mailer\\EventListener\\SmimeCertificateRepositoryInterface`.
This interface requires only one method: ``findCertificatePathFor()``, which must
return the file path to the certificate associated with the given email address::
namespace App\Security;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface;
class LocalFileCertificateRepository implements SmimeCertificateRepositoryInterface
{
public function __construct(
#[Autowire(param: 'kernel.project_dir')]
private readonly string $projectDir
){}
public function findCertificatePathFor(string $email): ?string
{
$hash = hash('sha256', strtolower(trim($email)));
$path = sprintf('%s/storage/%s.crt', $this->projectDir, $hash);
return file_exists($path) ? $path : null;
}
}
.. versionadded:: 7.3
Global message encryption configuration was introduced in Symfony 7.3.
.. _multiple-email-transports:
Multiple Email Transports
-------------------------
You may want to use more than one mailer transport for delivery of your messages.
This can be configured by replacing the ``dsn`` configuration entry with a
``transports`` entry, like:
.. configuration-block::
.. code-block:: yaml
# config/packages/mailer.yaml
framework:
mailer:
transports:
main: '%env(MAILER_DSN)%'
alternative: '%env(MAILER_DSN_IMPORTANT)%'
.. code-block:: xml
%env(MAILER_DSN)%
%env(MAILER_DSN_IMPORTANT)%
.. code-block:: php
// config/packages/mailer.php
use Symfony\Config\FrameworkConfig;
use function Symfony\Component\DependencyInjection\Loader\Configurator\env;
return static function (FrameworkConfig $framework): void {
$framework->mailer()
->transport('main', env('MAILER_DSN'))
->transport('alternative', env('MAILER_DSN_IMPORTANT'))
;
};
By default the first transport is used. The other transports can be selected by
adding an ``X-Transport`` header (which Mailer will remove automatically from
the final email)::
// Send using first transport ("main"):
$mailer->send($email);
// ... or use the transport "alternative":
$email->getHeaders()->addTextHeader('X-Transport', 'alternative');
$mailer->send($email);
.. _mailer-sending-messages-async:
Sending Messages Async
----------------------
When you call ``$mailer->send($email)``, the email is sent to the transport immediately.
To improve performance, you can leverage :doc:`Messenger ` to send
the messages later via a Messenger transport.
Start by following the :doc:`Messenger ` documentation and configuring
a transport. Once everything is set up, when you call ``$mailer->send()``, a
:class:`Symfony\\Component\\Mailer\\Messenger\\SendEmailMessage` message will
be dispatched through the default message bus (``messenger.default_bus``). Assuming
you have a transport called ``async``, you can route the message there:
.. configuration-block::
.. code-block:: yaml
# config/packages/messenger.yaml
framework:
messenger:
transports:
async: "%env(MESSENGER_TRANSPORT_DSN)%"
routing:
'Symfony\Component\Mailer\Messenger\SendEmailMessage': async
.. code-block:: xml
%env(MESSENGER_TRANSPORT_DSN)%
.. code-block:: php
// config/packages/messenger.php
use Symfony\Config\FrameworkConfig;
return static function (FrameworkConfig $framework): void {
$framework->messenger()
->transport('async')->dsn(env('MESSENGER_TRANSPORT_DSN'));
$framework->messenger()
->routing('Symfony\Component\Mailer\Messenger\SendEmailMessage')
->senders(['async']);
};
Thanks to this, instead of being delivered immediately, messages will be sent
to the transport to be handled later (see :ref:`messenger-worker`). Note that
the "rendering" of the email (computed headers, body rendering, ...) is also
deferred and will only happen just before the email is sent by the Messenger
handler.
When sending an email asynchronously, its instance must be serializable.
This is always the case for :class:`Symfony\\Component\\Mailer\\Mailer`
instances, but when sending a
:class:`Symfony\\Bridge\\Twig\\Mime\\TemplatedEmail`, you must ensure that
the ``context`` is serializable. If you have non-serializable variables,
like Doctrine entities, either replace them with more specific variables or
render the email before calling ``$mailer->send($email)``::
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\BodyRendererInterface;
public function action(MailerInterface $mailer, BodyRendererInterface $bodyRenderer): void
{
$email = (new TemplatedEmail())
->htmlTemplate($template)
->context($context)
;
$bodyRenderer->render($email);
$mailer->send($email);
}
You can configure which bus is used to dispatch the message using the ``message_bus`` option.
You can also set this to ``false`` to call the Mailer transport directly and
disable asynchronous delivery.
.. configuration-block::
.. code-block:: yaml
# config/packages/mailer.yaml
framework:
mailer:
message_bus: app.another_bus
.. code-block:: xml
.. code-block:: php
// config/packages/mailer.php
use Symfony\Config\FrameworkConfig;
return static function (FrameworkConfig $framework): void {
$framework->mailer()
->messageBus('app.another_bus');
};
.. note::
In cases of long-running scripts, and when Mailer uses the
:class:`Symfony\\Component\\Mailer\\Transport\\Smtp\\SmtpTransport`
you may manually disconnect from the SMTP server to avoid keeping
an open connection to the SMTP server in between sending emails.
You can do so by using the ``stop()`` method.
You can also select the transport by adding an ``X-Bus-Transport`` header (which
will be removed automatically from the final message)::
// Use the bus transport "app.another_bus":
$email->getHeaders()->addTextHeader('X-Bus-Transport', 'app.another_bus');
$mailer->send($email);
Adding Tags and Metadata to Emails
----------------------------------
Certain 3rd party transports support email *tags* and *metadata*, which can be used
for grouping, tracking and workflows. You can add those by using the
:class:`Symfony\\Component\\Mailer\\Header\\TagHeader` and
:class:`Symfony\\Component\\Mailer\\Header\\MetadataHeader` classes. If your transport
supports headers, it will convert them to their appropriate format::
use Symfony\Component\Mailer\Header\MetadataHeader;
use Symfony\Component\Mailer\Header\TagHeader;
$email->getHeaders()->add(new TagHeader('password-reset'));
$email->getHeaders()->add(new MetadataHeader('Color', 'blue'));
$email->getHeaders()->add(new MetadataHeader('Client-ID', '12345'));
If your transport does not support tags and metadata, they will be added as custom headers:
.. code-block:: text
X-Tag: password-reset
X-Metadata-Color: blue
X-Metadata-Client-ID: 12345
The following transports currently support tags and metadata:
* Brevo
* Mailgun
* Mailtrap
* Mandrill
* Postmark
* Sendgrid
The following transports only support tags:
* MailPace
* Resend
The following transports only support metadata:
* Amazon SES (note that Amazon refers to this feature as "tags", but Symfony
calls it "metadata" because it contains a key and a value)
Draft Emails
------------
:class:`Symfony\\Component\\Mime\\DraftEmail` is a special instance of
:class:`Symfony\\Component\\Mime\\Email`. Its purpose is to build up an email
(with body, attachments, etc) and make available to download as an ``.eml`` with
the ``X-Unsent`` header. Many email clients can open these files and interpret
them as *draft emails*. You can use these to create advanced ``mailto:`` links.
Here's an example of making one available to download::
// src/Controller/DownloadEmailController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Mime\DraftEmail;
use Symfony\Component\Routing\Attribute\Route;
class DownloadEmailController extends AbstractController
{
#[Route('/download-email')]
public function __invoke(): Response
{
$message = (new DraftEmail())
->html($this->renderView(/* ... */))
->addPart(/* ... */)
;
$response = new Response($message->toString());
$contentDisposition = $response->headers->makeDisposition(
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
'download.eml'
);
$response->headers->set('Content-Type', 'message/rfc822');
$response->headers->set('Content-Disposition', $contentDisposition);
return $response;
}
}
.. note::
As it's possible for :class:`Symfony\\Component\\Mime\\DraftEmail`'s to be created
without a To/From they cannot be sent with the mailer.
Mailer Events
-------------
MessageEvent
~~~~~~~~~~~~
**Event Class**: :class:`Symfony\\Component\\Mailer\\Event\\MessageEvent`
``MessageEvent`` allows changing the Mailer message and the envelope before
the email is sent::
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mime\Email;
public function onMessage(MessageEvent $event): void
{
$message = $event->getMessage();
if (!$message instanceof Email) {
return;
}
// do something with the message (logging, ...)
// and/or add some Messenger stamps
$event->addStamp(new SomeMessengerStamp());
}
If you want to stop the Message from being sent, call ``reject()`` (it will
also stop the event propagation)::
use Symfony\Component\Mailer\Event\MessageEvent;
public function onMessage(MessageEvent $event): void
{
$event->reject();
}
Execute this command to find out which listeners are registered for this event
and their priorities:
.. code-block:: terminal
$ php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\MessageEvent"
.. _mailer-sent-message-event:
SentMessageEvent
~~~~~~~~~~~~~~~~
**Event Class**: :class:`Symfony\\Component\\Mailer\\Event\\SentMessageEvent`
``SentMessageEvent`` allows you to act on the :class:`Symfony\\Component\\Mailer\\SentMessage`
class to access the original message (``getOriginalMessage()``) and some
:ref:`debugging information ` (``getDebug()``) such as
the HTTP calls made by the HTTP transports, which is useful for debugging errors::
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\Event\SentMessageEvent;
public function onMessage(SentMessageEvent $event): void
{
$message = $event->getMessage();
// do something with the message (e.g. get its id)
}
Execute this command to find out which listeners are registered for this event
and their priorities:
.. code-block:: terminal
$ php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\SentMessageEvent"
.. _mailer-failed-message-event:
FailedMessageEvent
~~~~~~~~~~~~~~~~~~
**Event Class**: :class:`Symfony\\Component\\Mailer\\Event\\FailedMessageEvent`
``FailedMessageEvent`` allows acting on the initial message in case of a failure
and some :ref:`debugging information ` (``getDebug()``)
such as the HTTP calls made by the HTTP transports, which is useful for debugging errors::
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\Event\FailedMessageEvent;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
public function onMessage(FailedMessageEvent $event): void
{
// e.g you can get more information on this error when sending an email
$error = $event->getError();
if ($error instanceof TransportExceptionInterface) {
$error->getDebug();
}
// do something with the message
}
Execute this command to find out which listeners are registered for this event
and their priorities:
.. code-block:: terminal
$ php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\FailedMessageEvent"
Development & Debugging
-----------------------
.. _mail-catcher:
Enabling an Email Catcher
~~~~~~~~~~~~~~~~~~~~~~~~~
When developing locally, it is recommended to use an email catcher. If you have
enabled Docker support via Symfony recipes, an email catcher is automatically
configured. In addition, if you are using the :doc:`Symfony CLI `
tool, the mailer DSN is automatically exposed via the
:ref:`symfony binary Docker integration `.
Sending Test Emails
~~~~~~~~~~~~~~~~~~~
Symfony provides a command to send emails, which is useful during development
to test if sending emails works correctly:
.. code-block:: terminal
# the only mandatory argument is the recipient address
# (check the command help to learn about its options)
$ php bin/console mailer:test someone@example.com
This command bypasses the :doc:`Messenger bus `, if configured, to
ease testing emails even when the Messenger consumer is not running.
Disabling Delivery
~~~~~~~~~~~~~~~~~~
While developing (or testing), you may want to disable delivery of messages
entirely. You can do this by using ``null://null`` as the mailer DSN, either in
your :ref:`.env configuration files ` or in
the mailer configuration file (e.g. in the ``dev`` or ``test`` environments):
.. configuration-block::
.. code-block:: yaml
# config/packages/mailer.yaml
when@dev:
framework:
mailer:
dsn: 'null://null'
.. code-block:: xml
.. code-block:: php
// config/packages/mailer.php
use Symfony\Config\FrameworkConfig;
return static function (FrameworkConfig $framework): void {
// ...
$framework->mailer()
->dsn('null://null');
};
.. note::
If you're using Messenger and routing to a transport, the message will *still*
be sent to that transport.
Always Send to the same Address
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Instead of disabling delivery entirely, you might want to *always* send emails to
a specific address, instead of the *real* address:
.. configuration-block::
.. code-block:: yaml
# config/packages/mailer.yaml
when@dev:
framework:
mailer:
envelope:
recipients: ['youremail@example.com']
.. code-block:: xml
youremail@example.com
.. code-block:: php
// config/packages/mailer.php
use Symfony\Config\FrameworkConfig;
return static function (FrameworkConfig $framework): void {
// ...
$framework->mailer()
->envelope()
->recipients(['youremail@example.com'])
;
};
Use the ``allowed_recipients`` option to define specific addresses that should
still receive their original emails. These messages will also be sent to the
address(es) defined in ``recipients``, as with all other emails:
.. configuration-block::
.. code-block:: yaml
# config/packages/mailer.yaml
when@dev:
framework:
mailer:
envelope:
recipients: ['youremail@example.com']
allowed_recipients:
- 'internal@example.com'
# you can also use regular expression to define allowed recipients
- 'internal-.*@example.(com|fr)'
.. code-block:: xml
youremail@example.com
internal@example.com
internal-.*@example.(com|fr)
.. code-block:: php
// config/packages/mailer.php
use Symfony\Config\FrameworkConfig;
return static function (FrameworkConfig $framework): void {
// ...
$framework->mailer()
->envelope()
->recipients(['youremail@example.com'])
->allowedRecipients([
'internal@example.com',
// you can also use regular expression to define allowed recipients
'internal-.*@example.(com|fr)',
])
;
};
With this configuration, all emails will be sent to ``youremail@example.com``.
Additionally, emails sent to ``internal@example.com``, ``internal-monitoring@example.fr``,
etc., will also be delivered to those addresses.
.. versionadded:: 7.1
The ``allowed_recipients`` option was introduced in Symfony 7.1.
Write a Functional Test
~~~~~~~~~~~~~~~~~~~~~~~
Symfony provides lots of :ref:`built-in mailer assertions `
to functionally test that an email was sent, its contents or headers, etc.
They are available in test classes extending
:class:`Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase` or when using
the :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\MailerAssertionsTrait`::
// tests/Controller/MailControllerTest.php
namespace App\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class MailControllerTest extends WebTestCase
{
public function testMailIsSentAndContentIsOk(): void
{
$client = static::createClient();
$client->request('GET', '/mail/send');
$this->assertResponseIsSuccessful();
$this->assertEmailCount(1); // use assertQueuedEmailCount() when using Messenger
$email = $this->getMailerMessage();
$this->assertEmailHtmlBodyContains($email, 'Welcome');
$this->assertEmailTextBodyContains($email, 'Welcome');
}
}
.. tip::
If your controller returns a redirect response after sending the email, make
sure to have your client *not* follow redirects. The kernel is rebooted after
following the redirection and the message will be lost from the mailer event
handler.
.. _`AhaSend`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/AhaSend/README.md
.. _`Amazon SES`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Amazon/README.md
.. _`Azure`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Azure/README.md
.. _`App Password`: https://support.google.com/accounts/answer/185833
.. _`Brevo`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Brevo/README.md
.. _`default_socket_timeout`: https://www.php.net/manual/en/filesystem.configuration.php#ini.default-socket-timeout
.. _`DKIM`: https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail
.. _`download the foundation-emails.css file`: https://github.com/foundation/foundation-emails/blob/develop/dist/foundation-emails.css
.. _`Google Gmail`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Google/README.md
.. _`high availability`: https://en.wikipedia.org/wiki/High_availability
.. _`Infobip`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Infobip/README.md
.. _`Inky`: https://get.foundation/emails/docs/inky.html
.. _`league/html-to-markdown`: https://github.com/thephpleague/html-to-markdown
.. _`load balancing`: https://en.wikipedia.org/wiki/Load_balancing_(computing)
.. _`MailerSend`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/MailerSend/README.md
.. _`Mandrill`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Mailchimp/README.md
.. _`Mailgun`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Mailgun/README.md
.. _`Mailjet`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Mailjet/README.md
.. _`Markdown syntax`: https://commonmark.org/
.. _`Mailomat`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Mailomat/README.md
.. _`MailPace`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/MailPace/README.md
.. _`OpenSSL PHP extension`: https://www.php.net/manual/en/book.openssl.php
.. _`PEM encoded`: https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail
.. _`Postal`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Postal/README.md
.. _`Postmark`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Postmark/README.md
.. _`Mailtrap`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Mailtrap/README.md
.. _`Resend`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Resend/README.md
.. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt
.. _`S/MIME`: https://en.wikipedia.org/wiki/S/MIME
.. _`Scaleway`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Scaleway/README.md
.. _`SendGrid`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Sendgrid/README.md
.. _`MJML`: https://github.com/mjmlio/mjml
.. _`Sweego`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Sweego/README.md