Sending Emails with Mailer ========================== Installation ------------ Symfony's Mailer & :doc:`Mime ` components form a *powerful* system for creating and sending emails - complete with support for multipart messages, Twig integration, CSS inlining, file attachments and a lot more. Get them installed with: .. code-block:: terminal $ composer require symfony/mailer .. _mailer-transport-setup: Transport Setup --------------- Emails are delivered via a "transport". By default, you can deliver emails over SMTP by configuring the DSN in your ``.env`` file (the ``user``, ``pass`` and ``port`` parameters are optional): .. code-block:: env # .env MAILER_DSN=smtp://user:pass@smtp.example.com:port .. configuration-block:: .. code-block:: yaml # config/packages/mailer.yaml framework: mailer: dsn: '%env(MAILER_DSN)%' .. code-block:: xml .. 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()->dsn(env('MAILER_DSN')); }; .. warning:: If the username, password or host contain any character considered special in a URI (such as ``: / ? # [ ] @ ! $ & ' ( ) * + , ; =``), you must encode them. See `RFC 3986`_ for the full list of reserved characters or use the :phpfunction:`urlencode` function to encode them. Using Built-in Transports ~~~~~~~~~~~~~~~~~~~~~~~~~ ============ ======================================== ============================================================== DSN protocol Example Description ============ ======================================== ============================================================== smtp ``smtp://user:pass@smtp.example.com:25`` Mailer uses an SMTP server to send emails sendmail ``sendmail://default`` Mailer uses the local sendmail binary to send emails native ``native://default`` Mailer uses the sendmail binary and options configured in the ``sendmail_path`` setting of ``php.ini``. On Windows hosts, Mailer fallbacks to ``smtp`` and ``smtp_port`` ``php.ini`` settings when ``sendmail_path`` is not configured. ============ ======================================== ============================================================== .. warning:: When using ``native://default``, if ``php.ini`` uses the ``sendmail -t`` command, you won't have error reporting and ``Bcc`` headers won't be removed. It's highly recommended to NOT use ``native://default`` as you cannot control how sendmail is configured (prefer using ``sendmail://default`` if possible). .. _mailer_3rd_party_transport: Using a 3rd Party Transport ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Instead of using your own SMTP server or sendmail binary, you can send emails via a third-party provider: ===================== =============================================== =============== Service Install with Webhook support ===================== =============================================== =============== `AhaSend`_ ``composer require symfony/aha-send-mailer`` yes `Amazon SES`_ ``composer require symfony/amazon-mailer`` `Azure`_ ``composer require symfony/azure-mailer`` `Brevo`_ ``composer require symfony/brevo-mailer`` yes `Infobip`_ ``composer require symfony/infobip-mailer`` `Mailgun`_ ``composer require symfony/mailgun-mailer`` yes `Mailjet`_ ``composer require symfony/mailjet-mailer`` yes `Mailomat`_ ``composer require symfony/mailomat-mailer`` yes `MailPace`_ ``composer require symfony/mail-pace-mailer`` `MailerSend`_ ``composer require symfony/mailer-send-mailer`` yes `Mailtrap`_ ``composer require symfony/mailtrap-mailer`` yes `Mandrill`_ ``composer require symfony/mailchimp-mailer`` yes `Postal`_ ``composer require symfony/postal-mailer`` `Postmark`_ ``composer require symfony/postmark-mailer`` yes `Resend`_ ``composer require symfony/resend-mailer`` yes `Scaleway`_ ``composer require symfony/scaleway-mailer`` `SendGrid`_ ``composer require symfony/sendgrid-mailer`` yes `Sweego`_ ``composer require symfony/sweego-mailer`` yes ===================== =============================================== =============== .. versionadded:: 7.1 The Azure and Resend integrations were introduced in Symfony 7.1. .. versionadded:: 7.2 The Mailomat, Mailtrap, Postal and Sweego integrations were introduced in Symfony 7.2. .. versionadded:: 7.3 The AhaSend integration was introduced in Symfony 7.3. .. note:: As a convenience, Symfony also provides support for Gmail (``composer require symfony/google-mailer``), but this should not be used in production. In development, you should probably use an :ref:`email catcher ` instead. Note that most supported providers also offer a free tier. Each library includes a :ref:`Symfony Flex recipe ` that will add a configuration example to your ``.env`` file. For example, suppose you want to use SendGrid. First, install it: .. code-block:: terminal $ composer require symfony/sendgrid-mailer You'll now have a new line in your ``.env`` file that you can uncomment: .. code-block:: env # .env MAILER_DSN=sendgrid://KEY@default The ``MAILER_DSN`` isn't a *real* address: it's a convenient format that offloads most of the configuration work to mailer. The ``sendgrid`` scheme activates the SendGrid provider that you installed, which knows all about how to deliver messages via SendGrid. The *only* part you need to change is the ``KEY`` placeholder. Each provider has different environment variables that the Mailer uses to configure the *actual* protocol, address and authentication for delivery. Some also have options that can be configured with query parameters at the end of the ``MAILER_DSN`` - like ``?region=`` for Amazon SES, Mailgun or Scaleway. Some providers support sending via ``http``, ``api`` or ``smtp``. Symfony chooses the best available transport, but you can force to use one: .. code-block:: env # .env # force to use SMTP instead of HTTP (which is the default) MAILER_DSN=sendgrid+smtp://$SENDGRID_KEY@default This table shows the full list of available DSN formats for each third party provider: +------------------------+---------------------------------------------------------+ | Provider | Formats | +========================+=========================================================+ | `AhaSend`_ | - SMTP ``ahasend+smtp://USERNAME:PASSWORD@default`` | | | - HTTP n/a | | | - API ``ahasend+api://KEY@default`` | +------------------------+---------------------------------------------------------+ | `Amazon SES`_ | - SMTP ``ses+smtp://USERNAME:PASSWORD@default`` | | | - HTTP ``ses+https://ACCESS_KEY:SECRET_KEY@default`` | | | - API ``ses+api://ACCESS_KEY:SECRET_KEY@default`` | +------------------------+---------------------------------------------------------+ | `Azure`_ | - API ``azure+api://ACS_RESOURCE_NAME:KEY@default`` | +------------------------+---------------------------------------------------------+ | `Brevo`_ | - SMTP ``brevo+smtp://USERNAME:PASSWORD@default`` | | | - HTTP n/a | | | - API ``brevo+api://KEY@default`` | +------------------------+---------------------------------------------------------+ | `Google Gmail`_ | - SMTP ``gmail+smtp://USERNAME:APP-PASSWORD@default`` | | | - HTTP n/a | | | - API n/a | +------------------------+---------------------------------------------------------+ | `Infobip`_ | - SMTP ``infobip+smtp://KEY@default`` | | | - HTTP n/a | | | - API ``infobip+api://KEY@BASE_URL`` | +------------------------+---------------------------------------------------------+ | `Mandrill`_ | - SMTP ``mandrill+smtp://USERNAME:PASSWORD@default`` | | | - HTTP ``mandrill+https://KEY@default`` | | | - API ``mandrill+api://KEY@default`` | +------------------------+---------------------------------------------------------+ | `MailerSend`_ | - SMTP ``mailersend+smtp://KEY@default`` | | | - HTTP n/a | | | - API ``mailersend+api://KEY@BASE_URL`` | +------------------------+---------------------------------------------------------+ | `Mailgun`_ | - SMTP ``mailgun+smtp://USERNAME:PASSWORD@default`` | | | - HTTP ``mailgun+https://KEY:DOMAIN@default`` | | | - API ``mailgun+api://KEY:DOMAIN@default`` | +------------------------+---------------------------------------------------------+ | `Mailjet`_ | - SMTP ``mailjet+smtp://ACCESS_KEY:SECRET_KEY@default`` | | | - HTTP n/a | | | - API ``mailjet+api://ACCESS_KEY:SECRET_KEY@default`` | +------------------------+---------------------------------------------------------+ | `Mailomat`_ | - SMTP ``mailomat+smtp://USERNAME:PASSWORD@default`` | | | - HTTP n/a | | | - API ``mailomat+api://KEY@default`` | +------------------------+---------------------------------------------------------+ | `MailPace`_ | - SMTP ``mailpace+api://API_TOKEN@default`` | | | - HTTP n/a | | | - API ``mailpace+api://API_TOKEN@default`` | +------------------------+---------------------------------------------------------+ | `Mailtrap`_ | - SMTP ``mailtrap+smtp://PASSWORD@default`` | | | - HTTP n/a | | | - API ``mailtrap+api://API_TOKEN@default`` | +------------------------+---------------------------------------------------------+ | `Postal`_ | - SMTP n/a | | | - HTTP n/a | | | - API ``postal+api://API_KEY@BASE_URL`` | +------------------------+---------------------------------------------------------+ | `Postmark`_ | - SMTP ``postmark+smtp://ID@default`` | | | - HTTP n/a | | | - API ``postmark+api://KEY@default`` | +------------------------+---------------------------------------------------------+ | `Resend`_ | - SMTP ``resend+smtp://resend:API_KEY@default`` | | | - HTTP n/a | | | - API ``resend+api://API_KEY@default`` | +------------------------+---------------------------------------------------------+ | `Scaleway`_ | - SMTP ``scaleway+smtp://PROJECT_ID:API_KEY@default`` | | | - HTTP n/a | | | - API ``scaleway+api://PROJECT_ID:API_KEY@default`` | +------------------------+---------------------------------------------------------+ | `Sendgrid`_ | - SMTP ``sendgrid+smtp://KEY@default`` | | | - HTTP n/a | | | - API ``sendgrid+api://KEY@default`` | +------------------------+---------------------------------------------------------+ | `Sweego`_ | - SMTP ``sweego+smtp://LOGIN:PASSWORD@HOST:PORT`` | | | - HTTP n/a | | | - API ``sweego+api://API_KEY@default`` | +------------------------+---------------------------------------------------------+ .. warning:: If your credentials contain special characters, you must URL-encode them. For example, the DSN ``ses+smtp://ABC1234:abc+12/345@default`` should be configured as ``ses+smtp://ABC1234:abc%2B12%2F345@default`` .. warning:: If you want to use the ``ses+smtp`` transport together with :doc:`Messenger ` to :ref:`send messages in background `, you need to add the ``ping_threshold`` parameter to your ``MAILER_DSN`` with a value lower than ``10``: ``ses+smtp://USERNAME:PASSWORD@default?ping_threshold=9`` .. note:: When using SMTP, the default timeout for sending a message before throwing an exception is the value defined in the `default_socket_timeout`_ PHP.ini option. .. note:: Besides SMTP, many 3rd party transports offer a web API to send emails. To do so, you have to install (additionally to the bridge) the HttpClient component via ``composer require symfony/http-client``. .. note:: To use Google Gmail, you must have a Google Account with 2-Step-Verification (2FA) enabled and you must use `App Password`_ to authenticate. Also note that Google revokes your App Passwords when you change your Google Account password and then you need to generate a new one. Using other methods (like ``XOAUTH2`` or the ``Gmail API``) are not supported currently. You should use Gmail for testing purposes only and use a real provider in production. .. tip:: If you want to override the default host for a provider (to debug an issue using a service like ``requestbin.com``), change ``default`` by your host: .. code-block:: env # .env MAILER_DSN=mailgun+https://KEY:DOMAIN@requestbin.com Note that the protocol is *always* HTTPs and cannot be changed. .. note:: The specific transports, e.g. ``mailgun+smtp`` are designed to work without any manual configuration. Changing the port by appending it to your DSN is not supported for any of these ``+smtp`` transports. If you need to change the port, use the ``smtp`` transport instead, like so: .. code-block:: env # .env MAILER_DSN=smtp://KEY:DOMAIN@smtp.eu.mailgun.org.com:25 .. tip:: Some third party mailers, when using the API, support status callbacks via webhooks. See the :doc:`Webhook documentation ` for more details. High Availability ~~~~~~~~~~~~~~~~~ Symfony's mailer supports `high availability`_ via a technique called "failover" to ensure that emails are sent even if one mailer server fails. A failover transport is configured with two or more transports and the ``failover`` keyword: .. code-block:: env MAILER_DSN="failover(postmark+api://ID@default sendgrid+smtp://KEY@default)" The failover-transport starts using the first transport and if it fails, it will retry the same delivery with the next transports until one of them succeeds (or until all of them fail). By default, delivery is retried 60 seconds after a failed attempt. You can adjust the retry period by setting the ``retry_period`` option in the DSN: .. code-block:: env MAILER_DSN="failover(postmark+api://ID@default sendgrid+smtp://KEY@default)?retry_period=15" .. versionadded:: 7.3 The ``retry_period`` option was introduced in Symfony 7.3. Load Balancing ~~~~~~~~~~~~~~ Symfony's mailer supports `load balancing`_ via a technique called "round-robin" to distribute the mailing workload across multiple transports. A round-robin transport is configured with two or more transports and the ``roundrobin`` keyword: .. code-block:: env MAILER_DSN="roundrobin(postmark+api://ID@default sendgrid+smtp://KEY@default)" The round-robin transport starts with a *randomly* selected transport and then switches to the next available transport for each subsequent email. As with the failover transport, round-robin retries deliveries until a transport succeeds (or all fail). In contrast to the failover transport, it *spreads* the load across all its transports. By default, delivery is retried 60 seconds after a failed attempt. You can adjust the retry period by setting the ``retry_period`` option in the DSN: .. code-block:: env MAILER_DSN="roundrobin(postmark+api://ID@default sendgrid+smtp://KEY@default)?retry_period=15" .. versionadded:: 7.3 The ``retry_period`` option was introduced in Symfony 7.3. TLS Peer Verification ~~~~~~~~~~~~~~~~~~~~~ By default, SMTP transports perform TLS peer verification. This behavior is configurable with the ``verify_peer`` option. Although it's not recommended to disable this verification for security reasons, it can be useful while developing the application or when using a self-signed certificate:: $dsn = 'smtp://user:pass@smtp.example.com?verify_peer=0'; TLS Peer Fingerprint Verification ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Additional fingerprint verification can be enforced with the ``peer_fingerprint`` option. This is especially useful when a self-signed certificate is used and disabling ``verify_peer`` is needed, but security is still desired. Fingerprint may be specified as SHA1 or MD5 hash:: $dsn = 'smtp://user:pass@smtp.example.com?peer_fingerprint=6A1CF3B08D175A284C30BC10DE19162307C7286E'; Disabling Automatic TLS ~~~~~~~~~~~~~~~~~~~~~~~ .. versionadded:: 7.1 The option to disable automatic TLS was introduced in Symfony 7.1. By default, the Mailer component will use encryption when the OpenSSL extension is enabled and the SMTP server supports ``STARTTLS``. This behavior can be turned off by calling ``setAutoTls(false)`` on the ``EsmtpTransport`` instance, or by setting the ``auto_tls`` option to ``false`` in the DSN:: $dsn = 'smtp://user:pass@10.0.0.25?auto_tls=false'; .. warning:: It's not recommended to disable TLS while connecting to an SMTP server over the Internet, but it can be useful when both the application and the SMTP server are in a secured network, where there is no need for additional encryption. .. note:: This setting only works when the ``smtp://`` protocol is used. Ensure TLS ~~~~~~~~~~ You may want to ensure that TLS is used (either directly or via ``STARTTLS``) when sending mail over SMTP, regardless of other options or SMTP server support. To require TLS, call ``setRequireTls(true)`` on the ``EsmtpTransport`` instance, or set the ``require_tls`` option to ``true`` in the DSN:: $dsn = 'smtp://user:pass@10.0.0.25?require_tls=true'; When TLS is required, a :class:`Symfony\\Component\\Mailer\\Exception\\TransportException` is thrown if a TLS connection cannot be established during the initial communication with the SMTP server. .. note:: This setting only applies when using the ``smtp://`` protocol. .. versionadded:: 7.3 The ``require_tls`` option was introduced in Symfony 7.3. Binding to IPv4 or IPv6 ~~~~~~~~~~~~~~~~~~~~~~~ .. versionadded:: 7.3 The option to bind to IPv4, or IPv6, or a specific IP address was introduced in Symfony 7.3. By default, the underlying ``SocketStream`` will bind to IPv4 or IPv6 based on the available interfaces. You can enforce binding to a specific protocol or IP address by using the ``source_ip`` option. To bind to IPv4, use:: $dsn = 'smtp://smtp.example.com?source_ip=0.0.0.0'; As per RFC2732, IPv6 addresses must be enclosed in square brackets. To bind to IPv6, use:: $dsn = 'smtp://smtp.example.com?source_ip=[::]'; .. note:: This option only works when using the ``smtp://`` protocol. Overriding default SMTP authenticators ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By default, SMTP transports will try to log in using all authentication methods available on the SMTP server, one after the other. In some cases, it may be useful to redefine the supported authentication methods to ensure that the preferred method will be used first. This can be done from ``EsmtpTransport`` constructor or using the ``setAuthenticators()`` method:: use Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; // Choose one of these two options: // Option 1: pass the authenticators to the constructor $transport = new EsmtpTransport( host: 'oauth-smtp.domain.tld', authenticators: [new XOAuth2Authenticator()] ); // Option 2: call a method to redefine the authenticators $transport->setAuthenticators([new XOAuth2Authenticator()]); Other Options ~~~~~~~~~~~~~ ``command`` Command to be executed by ``sendmail`` transport:: $dsn = 'sendmail://default?command=/usr/sbin/sendmail%20-oi%20-t' ``local_domain`` The domain name to use in ``HELO`` command:: $dsn = 'smtps://smtp.example.com?local_domain=example.org' ``restart_threshold`` The maximum number of messages to send before re-starting the transport. It can be used together with ``restart_threshold_sleep``:: $dsn = 'smtps://smtp.example.com?restart_threshold=10&restart_threshold_sleep=1' ``restart_threshold_sleep`` The number of seconds to sleep between stopping and re-starting the transport. It's common to combine it with ``restart_threshold``:: $dsn = 'smtps://smtp.example.com?restart_threshold=10&restart_threshold_sleep=1' ``ping_threshold`` The minimum number of seconds between two messages required to ping the server:: $dsn = 'smtps://smtp.example.com?ping_threshold=200' ``max_per_second`` The number of messages to send per second (0 to disable this limitation):: $dsn = 'smtps://smtp.example.com?max_per_second=2' Custom Transport Factories ~~~~~~~~~~~~~~~~~~~~~~~~~~ If you want to support your own custom DSN (``acme://...``), you can create a custom transport factory. To do so, create a class that implements :class:`Symfony\\Component\\Mailer\\Transport\\TransportFactoryInterface` or, if you prefer, extend the :class:`Symfony\\Component\\Mailer\\Transport\\AbstractTransportFactory` class to save some boilerplate code:: // src/Mailer/AcmeTransportFactory.php final class AcmeTransportFactory extends AbstractTransportFactory { public function create(Dsn $dsn): TransportInterface { // parse the given DSN, extract data/credentials from it // and then, create and return the transport } protected function getSupportedSchemes(): array { // this supports DSN starting with `acme://` return ['acme']; } } After creating the custom transport class, register it as a service in your application and :doc:`tag it ` with the ``mailer.transport_factory`` tag. Creating & Sending Messages --------------------------- To send an email, get a :class:`Symfony\\Component\\Mailer\\Mailer` instance by type-hinting :class:`Symfony\\Component\\Mailer\\MailerInterface` and create an :class:`Symfony\\Component\\Mime\\Email` object:: // src/Controller/MailerController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Email; use Symfony\Component\Routing\Attribute\Route; class MailerController extends AbstractController { #[Route('/email')] public function sendEmail(MailerInterface $mailer): Response { $email = (new Email()) ->from('hello@example.com') ->to('you@example.com') //->cc('cc@example.com') //->bcc('bcc@example.com') //->replyTo('fabien@example.com') //->priority(Email::PRIORITY_HIGH) ->subject('Time for Symfony Mailer!') ->text('Sending emails is fun again!') ->html('

See Twig integration for better HTML integration!

'); $mailer->send($email); // ... } } That's it! The message will be sent immediately via the transport you configured. If you prefer to send emails asynchronously to improve performance, read the :ref:`Sending Messages Async ` section. Also, if your application has the :doc:`Messenger component ` installed, all emails will be sent asynchronously by default (but :ref:`you can change that `). Email Addresses ~~~~~~~~~~~~~~~ All the methods that require email addresses (``from()``, ``to()``, etc.) accept both strings or address objects:: // ... use Symfony\Component\Mime\Address; $email = (new Email()) // email address as a simple string ->from('fabien@example.com') // non-ASCII characters are supported both in the local part and the domain; // if the SMTP server doesn't support this feature, you'll see an exception ->from('jânë.dœ@ëxãmplę.com') // email address as an object ->from(new Address('fabien@example.com')) // defining the email address and name as an object // (email clients will display the name) ->from(new Address('fabien@example.com', 'Fabien')) // defining the email address and name as a string // (the format must match: 'Name ') ->from(Address::create('Fabien Potencier ')) // ... ; .. tip:: Instead of calling ``->from()`` *every* time you create a new email, you can :ref:`configure emails globally ` to set the same ``From`` email to all messages. .. versionadded:: 7.2 Support for non-ASCII email addresses (e.g. ``jânë.dœ@ëxãmplę.com``) was introduced in Symfony 7.2. .. note:: The local part of the address (what goes before the ``@``) can include UTF-8 characters, except for the sender address (to avoid issues with bounced emails). For example: ``föóbàr@example.com``, ``用户@example.com``, ``θσερ@example.com``, etc. Use ``addTo()``, ``addCc()``, or ``addBcc()`` methods to add more addresses:: $email = (new Email()) ->to('foo@example.com') ->addTo('bar@example.com') ->cc('cc@example.com') ->addCc('cc2@example.com') // ... ; Alternatively, you can pass multiple addresses to each method:: $toAddresses = ['foo@example.com', new Address('bar@example.com')]; $email = (new Email()) ->to(...$toAddresses) ->cc('cc1@example.com', 'cc2@example.com') // ... ; Message Headers ~~~~~~~~~~~~~~~ Messages include a number of header fields to describe their contents. Symfony sets all the required headers automatically, but you can set your own headers too. There are different types of headers (Id header, Mailbox header, Date header, etc.) but most of the times you'll set text headers:: $email = (new Email()) ->getHeaders() // this non-standard header tells compliant autoresponders ("email holiday mode") // to not reply to this message because it's an automated email ->addTextHeader('X-Auto-Response-Suppress', 'OOF, DR, RN, NRN, AutoReply') // use an array if you want to add a header with multiple values // (for example in the "References" or "In-Reply-To" header) ->addIdHeader('References', ['123@example.com', '456@example.com']) // ... ; .. tip:: Instead of calling ``->addTextHeader()`` *every* time you create a new email, you can :ref:`configure emails globally ` to set the same headers to all sent emails. Message Contents ~~~~~~~~~~~~~~~~ The text and HTML contents of the email messages can be strings (usually the result of rendering some template) or PHP resources:: $email = (new Email()) // ... // simple contents defined as a string ->text('Lorem ipsum...') ->html('

Lorem ipsum...

') // attach a file stream ->text(fopen('/path/to/emails/user_signup.txt', 'r')) ->html(fopen('/path/to/emails/user_signup.html', 'r')) ; .. tip:: You can also use Twig templates to render the HTML and text contents. Read the `Twig: HTML & CSS`_ section later in this article to learn more. File Attachments ~~~~~~~~~~~~~~~~ Use the ``addPart()`` method with a ``File`` to add files that exist on your file system:: use Symfony\Component\Mime\Part\DataPart; use Symfony\Component\Mime\Part\File; // ... $email = (new Email()) // ... ->addPart(new DataPart(new File('/path/to/documents/terms-of-use.pdf'))) // optionally you can tell email clients to display a custom name for the file ->addPart(new DataPart(new File('/path/to/documents/privacy.pdf'), 'Privacy Policy')) // optionally you can provide an explicit MIME type (otherwise it's guessed) ->addPart(new DataPart(new File('/path/to/documents/contract.doc'), 'Contract', 'application/msword')) ; Alternatively you can attach contents from a stream by passing it directly to the ``DataPart``:: $email = (new Email()) // ... ->addPart(new DataPart(fopen('/path/to/documents/contract.doc', 'r'))) ; Embedding Images ~~~~~~~~~~~~~~~~ If you want to display images inside your email, you must embed them instead of adding them as attachments. When using Twig to render the email contents, as explained :ref:`later in this article `, the images are embedded automatically. Otherwise, you need to embed them manually. First, use the ``addPart()`` method to add an image from a file or stream:: $email = (new Email()) // ... // get the image contents from a PHP resource ->addPart((new DataPart(fopen('/path/to/images/logo.png', 'r'), 'logo', 'image/png'))->asInline()) // get the image contents from an existing file ->addPart((new DataPart(new File('/path/to/images/signature.gif'), 'footer-signature', 'image/gif'))->asInline()) ; Use the ``asInline()`` method to embed the content instead of attaching it. The second optional argument of both methods is the image name ("Content-ID" in the MIME standard). Its value is an arbitrary string that must be unique in each email message and is used later to reference the images inside the HTML contents:: $email = (new Email()) // ... ->addPart((new DataPart(fopen('/path/to/images/logo.png', 'r'), 'logo', 'image/png'))->asInline()) ->addPart((new DataPart(new File('/path/to/images/signature.gif'), 'footer-signature', 'image/gif'))->asInline()) // reference images using the syntax 'cid:' + "image embed name" ->html(' ... ...') // use the same syntax for images included as HTML background images ->html('...
...
...') ; The actual Content-ID value present in the e-mail source will be randomly generated by Symfony. You can also use the :method:`DataPart::setContentId() ` method to define a custom Content-ID for the image and use it as its ``cid`` reference:: $part = new DataPart(new File('/path/to/images/signature.gif')); // according to the spec, the Content-ID value must include at least one '@' character $part->setContentId('footer-signature@my-app'); $email = (new Email()) // ... ->addPart($part->asInline()) ->html('... ...') ; .. _mailer-configure-email-globally: Configuring Emails Globally --------------------------- Instead of calling ``->from()`` on each Email you create, you can configure this value globally so that it is set on all sent emails. The same is true with ``->to()`` and headers. .. configuration-block:: .. code-block:: yaml # config/packages/mailer.yaml framework: mailer: envelope: sender: 'fabien@example.com' recipients: ['foo@example.com', 'bar@example.com'] headers: From: 'Fabien ' Bcc: 'baz@example.com' X-Custom-Header: 'foobar' .. code-block:: xml fabien@example.com foo@example.com bar@example.com Fabien <fabien@example.com> baz@example.com foobar .. code-block:: php // config/packages/mailer.php use Symfony\Config\FrameworkConfig; return static function (FrameworkConfig $framework): void { $mailer = $framework->mailer(); $mailer ->envelope() ->sender('fabien@example.com') ->recipients(['foo@example.com', 'bar@example.com']) ; $mailer->header('From')->value('Fabien '); $mailer->header('Bcc')->value('baz@example.com'); $mailer->header('X-Custom-Header')->value('foobar'); }; .. warning:: Some third-party providers don't support the usage of keywords like ``from`` in the ``headers``. Check out your provider's documentation before setting any global header. Handling Sending Failures ------------------------- Symfony Mailer considers that sending was successful when your transport (SMTP server or third-party provider) accepts the mail for further delivery. The message can later be lost or not delivered because of some problem in your provider, but that's out of reach for your Symfony application. If there's an error when handing over the email to your transport, Symfony throws a :class:`Symfony\\Component\\Mailer\\Exception\\TransportExceptionInterface`. Catch that exception to recover from the error or to display some message:: use Symfony\Component\Mailer\Exception\TransportExceptionInterface; $email = new Email(); // ... try { $mailer->send($email); } catch (TransportExceptionInterface $e) { // some error prevented the email sending; display an // error message or try to resend the message } .. _mailer-debugging-emails: Debugging Emails ---------------- The ``send()`` method of the mailer service injected when using ``MailerInterface`` doesn't return anything, so you can't access the sent email information. This is because it sends email messages **asynchronously** when the :doc:`Messenger component ` is used in the application. To access information about the sent email, update your code to replace the :class:`Symfony\\Component\\Mailer\\MailerInterface` with :class:`Symfony\\Component\\Mailer\\Transport\\TransportInterface`: .. code-block:: diff -use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mailer\Transport\TransportInterface; // ... class MailerController extends AbstractController { #[Route('/email')] - public function sendEmail(MailerInterface $mailer): Response + public function sendEmail(TransportInterface $mailer): Response { $email = (new Email()) // ... $sentEmail = $mailer->send($email); // ... } } The ``send()`` method of ``TransportInterface`` returns an object of type :class:`Symfony\\Component\\Mailer\\SentMessage`. This is because it always sends the emails **synchronously**, even if your application uses the Messenger component. The ``SentMessage`` object provides access to the original message (``getOriginalMessage()``) and to some debug information (``getDebug()``) such as the HTTP calls done by the HTTP transports, which is useful to debug errors. You can also access the :class:`Symfony\\Component\\Mailer\\SentMessage` object by listening to the :ref:`SentMessageEvent `, and retrieve ``getDebug()`` by listening to the :ref:`FailedMessageEvent `. .. note:: Some mailer providers change the ``Message-Id`` when sending the email. The ``getMessageId()`` method from ``SentMessage`` always returns the final ID of the message - whether it's the original random ID generated by Symfony or a new one generated by the provider. Exceptions related to mailer transports (those implementing :class:`Symfony\\Component\\Mailer\\Exception\\TransportException`) also provide this debug information via the ``getDebug()`` method. .. _mailer-twig: Twig: HTML & CSS ---------------- The Mime component integrates with the :ref:`Twig template engine ` 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 #} Logo

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 ACME Logo .. 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 %}

Welcome {{ email.toName }}!

{# ... #}
{% 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