Sending Emails with Mailer (Symfony Docs) (2024)

Edit this page

Installation

Symfony's Mailer & Mime components form a powerful systemfor creating and sending emails - complete with support for multipart messages, Twigintegration, CSS inlining, file attachments and a lot more. Get them installed with:

1
$ composer require symfony/mailer

Transport Setup

Emails are delivered via a "transport". Out of the box, you can deliver emailsover SMTP by configuring the DSN in your .env file (the user,pass and port parameters are optional):

12
# .envMAILER_DSN=smtp://user:pass@smtp.example.com:port
1234
# config/packages/mailer.yamlframework: mailer: dsn: '%env(MAILER_DSN)%'

Caution

If the username, password or host contain any character considered special in aURI (such as : / ? # [ ] @ ! $ & ' ( ) * + , ; =), you mustencode them. See RFC 3986 for the full list of reserved characters or use theurlencode 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 configuredin the sendmail_path setting of php.ini. On Windowshosts, Mailer fallbacks to smtp and smtp_portphp.ini settings when sendmail_path is not configured.

Caution

When using native://default, if php.ini uses the sendmail -tcommand, 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 controlhow sendmail is configured (prefer using sendmail://default if possible).

Using a 3rd Party Transport

Instead of using your own SMTP server or sendmail binary, you can send emailsvia a third-party provider:

Service Install with Webhook support
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
MailPace composer require symfony/mail-pace-mailer
MailerSend composer require symfony/mailer-send-mailer
Mandrill composer require symfony/mailchimp-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

7.1

The Azure and Resend integrations were introduced in Symfony 7.1.

Note

As a convenience, Symfony also provides support for Gmail (composerrequire symfony/google-mailer), but this should not be used inproduction. In development, you should probably use an email catcher instead. Note that most supported providers also offer afree tier.

Each library includes a Symfony Flex recipe that will adda configuration example to your .env file. For example, suppose you want touse SendGrid. First, install it:

1
$ composer require symfony/sendgrid-mailer

You'll now have a new line in your .env file that you can uncomment:

12
# .envMAILER_DSN=sendgrid://KEY@default

The MAILER_DSN isn't a real address: it's a convenient format thatoffloads most of the configuration work to mailer. The sendgrid schemeactivates the SendGrid provider that you just installed, which knows all abouthow to deliver messages via SendGrid. The only part you need to change is theKEY placeholder.

Each provider has different environment variables that the Mailer uses toconfigure the actual protocol, address and authentication for delivery. Somealso have options that can be configured with query parameters at the end of theMAILER_DSN - like ?region= for Amazon SES, Mailgun or Scaleway. Some providers supportsending via http, api or smtp. Symfony chooses the best availabletransport, but you can force to use one:

123
# .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 thirdparty provider:

Provider Formats
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
MailPace
  • SMTP mailpace+api://API_TOKEN@default
  • HTTP n/a
  • API mailpace+api://API_TOKEN@default
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

Caution

If your credentials contain special characters, you must URL-encode them.For example, the DSN ses+smtp://ABC1234:abc+12/345@default should beconfigured as ses+smtp://ABC1234:abc%2B12%2F345@default

Caution

If you want to use the ses+smtp transport together with Messengerto send messages in background,you need to add the ping_threshold parameter to your MAILER_DSN witha value lower than 10: ses+smtp://USERNAME:PASSWORD@default?ping_threshold=9

Caution

If you send custom headers when using the Amazon SES transport (to receivethem later via a webhook), make sure to use the ses+https provider becauseit's the only one that supports them.

Note

When using SMTP, the default timeout for sending a message before throwing anexception 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 Googlerevokes your App Passwords when you change your Google Account password and thenyou 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 usinga service like requestbin.com), change default by your host:

12
# .envMAILER_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 <provider>+smtp transports.If you need to change the port, use the smtp transport instead, like so:

12
# .envMAILER_DSN=smtp://KEY:DOMAIN@smtp.eu.mailgun.org.com:25

Tip

Some third party mailers, when using the API, support status callbacksvia webhooks. See the Webhook documentation for moredetails.

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 thefailover keyword:

1
MAILER_DSN="failover(postmark+api://ID@default sendgrid+smtp://KEY@default)"

The failover-transport starts using the first transport and if it fails, itwill retry the same delivery with the next transports until one of them succeeds(or until all of them fail).

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 theroundrobin keyword:

1
MAILER_DSN="roundrobin(postmark+api://ID@default sendgrid+smtp://KEY@default)"

The round-robin transport starts with a randomly selected transport andthen switches to the next available transport for each subsequent email.

As with the failover transport, round-robin retries deliveries untila transport succeeds (or all fail). In contrast to the failover transport,it spreads the load across all its transports.

TLS Peer Verification

By default, SMTP transports perform TLS peer verification. This behavior isconfigurable with the verify_peer option. Although it's not recommended todisable this verification for security reasons, it can be useful while developingthe application or when using a self-signed certificate:

1
$dsn = 'smtp://user:pass@smtp.example.com?verify_peer=0';

TLS Peer Fingerprint Verification

Additional fingerprint verification can be enforced with the peer_fingerprintoption. This is especially useful when a self-signed certificate is used anddisabling verify_peer is needed, but security is still desired. Fingerprintmay be specified as SHA1 or MD5 hash:

1
$dsn = 'smtp://user:pass@smtp.example.com?peer_fingerprint=6A1CF3B08D175A284C30BC10DE19162307C7286E';

Disabling Automatic TLS

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 extensionis enabled and the SMTP server supports STARTTLS. This behavior can be turnedoff by calling setAutoTls(false) on the EsmtpTransport instance, or bysetting the auto_tls option to false in the DSN:

1
$dsn = 'smtp://user:pass@10.0.0.25?auto_tls=false';

Caution

It's not recommended to disable TLS while connecting to an SMTP server overthe Internet, but it can be useful when both the application and the SMTPserver are in a secured network, where there is no need for additional encryption.

Note

This setting only works when the smtp:// protocol is used.

Overriding default SMTP authenticators

By default, SMTP transports will try to login using all authentication methodsavailable on the SMTP server, one after the other. In some cases, it may beuseful to redefine the supported authentication methods to ensure that thepreferred method will be used first.

This can be done from EsmtpTransport constructor or using thesetAuthenticators() method:

12345678910111213
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:

1
$dsn = 'sendmail://default?command=/usr/sbin/sendmail%20-oi%20-t'
local_domain

The domain name to use in HELO command:

1
$dsn = 'smtps://smtp.example.com?local_domain=example.org'
restart_threshold

The maximum number of messages to send before re-starting the transport. Itcan be used together with restart_threshold_sleep:

1
$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:

1
$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:

1
$dsn = 'smtps://smtp.example.com?ping_threshold=200'
max_per_second

The number of messages to send per second (0 to disable this limitation):

1
$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 acustom transport factory. To do so, create a class that implementsTransportFactoryInterface or, ifyou prefer, extend the AbstractTransportFactoryclass to save some boilerplate code:

123456789101112131415
// src/Mailer/AcmeTransportFactory.phpfinal 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 yourapplication and tag it with themailer.transport_factory tag.

To send an email, get a Mailerinstance by type-hinting MailerInterfaceand create an Email object:

123456789101112131415161718192021222324252627282930
// src/Controller/MailerController.phpnamespace 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('<p>See Twig integration for better HTML integration!</p>'); $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 theSending Messages Async section. Also, ifyour application has the Messenger component installed, allemails will be sent asynchronously by default(but you can change that).

Email Addresses

All the methods that require email addresses (from(), to(), etc.) acceptboth strings or address objects:

1234567891011121314151617181920
// ...use Symfony\Component\Mime\Address;$email = (new Email()) // email address as a simple string ->from('fabien@example.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 <email@example.com>') ->from(Address::create('Fabien Potencier <fabien@example.com>')) // ...;

Tip

Instead of calling ->from() every time you create a new email, you canconfigure emails globally to set thesame From email to all messages.

Note

The local part of the address (what goes before the @) can include UTF-8characters, 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:

12345678
$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:

12345678
$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. Symfonysets all the required headers automatically, but you can set your own headerstoo. There are different types of headers (Id header, Mailbox header, Dateheader, etc.) but most of the times you'll set text headers:

123456789101112
$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 canconfigure emails globally to set the sameheaders to all sent emails.

Message Contents

The text and HTML contents of the email messages can be strings (usually theresult of rendering some template) or PHP resources:

12345678910
$email = (new Email()) // ... // simple contents defined as a string ->text('Lorem ipsum...') ->html('<p>Lorem ipsum...</p>') // 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. Readthe section later in this article tolearn more.

File Attachments

Use the addPart() method with a File to add files that exist on yourfile system:

123456789101112
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 tothe DataPart:

1234
$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 theminstead of adding them as attachments. When using Twig to render the emailcontents, as explained 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 afile or stream:

1234567
$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" inthe MIME standard). Its value is an arbitrary string that must be unique in eachemail message and is used later to reference the images inside the HTML contents:

1234567891011
$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('<img src="cid:logo"> ... <img src="cid:footer-signature"> ...') // use the same syntax for images included as HTML background images ->html('... <div background="cid:footer-signature"> ... </div> ...');

The actual Content-ID value present in the e-mail source will be randomly generated by Symfony.You can also use the DataPart::setContentId()method to define a custom Content-ID for the image and use it as its cid reference:

123456789
$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('... <img src="cid:footer-signature@my-app"> ...');

Configuring Emails Globally

Instead of calling ->from() on each Email you create, you can configure thisvalue globally so that it is set on all sent emails. The same is true with ->to()and headers.

12345678910
# config/packages/mailer.yamlframework: mailer: envelope: sender: 'fabien@example.com' recipients: ['foo@example.com', 'bar@example.com'] headers: From: 'Fabien <fabien@example.com>' Bcc: 'baz@example.com' X-Custom-Header: 'foobar'

Caution

Some third-party providers don't support the usage of keywords like fromin the headers. Check out your provider's documentation before settingany global header.

Handling Sending Failures

Symfony Mailer considers that sending was successful when your transport (SMTPserver or third-party provider) accepts the mail for further delivery. The messagecan later be lost or not delivered because of some problem in your provider, butthat's out of reach for your Symfony application.

If there's an error when handing over the email to your transport, Symfony throwsa TransportExceptionInterface.Catch that exception to recover from the error or to display some message:

12345678910
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}

Debugging Emails

The SentMessage object returned by thesend() method of the TransportInterfaceprovides access to the original message (getOriginalMessage()) and to somedebug information (getDebug()) such as the HTTP calls done by the HTTPtransports, which is useful to debug errors.

Note

If your code used MailerInterface, youneed to replace it by TransportInterfaceto have the SentMessage object returned.

Note

Some mailer providers change the Message-Id when sending the email. ThegetMessageId() method from SentMessage always returns the definitiveID of the message (being the original random ID generated by Symfony or thenew ID generated by the mailer provider).

The exceptions related to mailer transports (those which implementTransportException) also providethis debug information via the getDebug() method.

The Mime component integrates with the Twig template engineto provide advanced features such as CSS style inlining and support for HTML/CSSframeworks to create complex HTML email messages. First, make sure Twig is installed:

1234
$ 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 theTemplatedEmail class. This class extendsthe normal Email class but adds some new methodsfor Twig templates:

12345678910111213141516171819
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:

123456789101112
{# templates/emails/signup.html.twig #}<h1>Welcome {{ email.toName }}!</h1><p> You signed up as {{ username }} the following email:</p><p><code>{{ email.to[0].address }}</code></p><p> <a href="#">Activate your account</a> (this link is valid until {{ expiration_date|date('F jS') }})</p>

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 calledemail, which is an instance ofWrappedTemplatedEmail.

Text Content

When the text content of a TemplatedEmail is not explicitly defined, it isautomatically generated from the HTML contents.

Symfony uses the following strategy when generating the text version of anemail:

  • If an explicit HTML to text converter has been configured (seetwig.mailer.html_to_text_converter), it calls it;
  • If not, and if you have league/html-to-markdown installed in yourapplication, it uses it to turn HTML into Markdown (so the text email hassome visual appeal);
  • Otherwise, it applies the strip_tags PHP function to theoriginal HTML contents.

If you want to define the text content yourself, use the text() methodexplained in the previous sections or the textTemplate() method provided bythe TemplatedEmail class:

123456789
+use Symfony\Bridge\Twig\Mime\TemplatedEmail; $email = (new TemplatedEmail()) // ... ->htmlTemplate('emails/signup.html.twig')+ ->textTemplate('emails/signup.txt.twig') // ... ;

Embedding Images

Instead of dealing with the <img src="cid: ..."> syntax explained in theprevious sections, when using Twig to render email contents you can refer toimage files as usual. First, to simplify things, define a Twig namespace calledimages that points to whatever directory your images are stored in:

1234567
# config/packages/twig.yamltwig: # ... paths: # point this wherever your images live '%kernel.project_dir%/assets/images': images

Now, use the special email.image() Twig helper to embed the images insidethe email contents:

12345
{# '@images/' refers to the Twig namespace defined earlier #}<img src="{{ email.image('@images/logo.png') }}" alt="Logo"><h1>Welcome {{ email.toName }}!</h1>{# ... #}

Inlining CSS Styles

Designing the HTML contents of an email is very different from designing anormal HTML page. For starters, most email clients only support a subset of allCSS features. In addition, popular email clients like Gmail don't supportdefining styles inside <style> ... </style> sections and you must inlineall the CSS styles.

CSS inlining means that every HTML tag must define a style attribute withall its CSS styles. This can make organizing your CSS a mess. That's why Twigprovides a CssInlinerExtension that automates everything for you. Installit with:

1
$ composer require twig/extra-bundle twig/cssinliner-extra

The extension is enabled automatically. To use it, wrap the entire templatewith the inline_css filter:

1234567891011
{% apply inline_css %} <style> {# here, define your CSS styles as usual #} h1 { color: #333; } </style> <h1>Welcome {{ email.toName }}!</h1> {# ... #}{% endapply %}

Using External CSS Files

You can also define CSS styles in external files and pass them asarguments to the filter:

1234
{% apply inline_css(source('@styles/email.css')) %} <h1>Welcome {{ username }}!</h1> {# ... #}{% endapply %}

You can pass unlimited number of arguments to inline_css() to load multipleCSS files. For this example to work, you also need to define a new Twig namespacecalled styles that points to the directory where email.css lives:

1234567
# config/packages/twig.yamltwig: # ... paths: # point this wherever your css files live '%kernel.project_dir%/assets/styles': styles

Rendering Markdown Content

Twig provides another extension called MarkdownExtension that lets youdefine the email contents using Markdown syntax. To use this, install theextension and a Markdown conversion library (the extension is compatible withseveral popular libraries):

12
# 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 orthe entire email contents from Markdown to HTML:

123456789
{% 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 %}

Inky Email Templating Language

Creating beautifully designed emails that work on every email client is socomplex that there are HTML/CSS frameworks dedicated to that. One of the mostpopular frameworks is called Inky. It defines a syntax based on some HTML-liketags which are later transformed into the real HTML code sent to users:

123456
<!-- a simplified example of the Inky syntax --><container> <row> <columns>This is a column.</columns> </row></container>

Twig provides integration with Inky via the InkyExtension. First, installthe extension in your application:

1
$ composer require twig/extra-bundle twig/inky-extra

The extension adds an inky_to_html filter, which can be used to convertparts or the entire email contents from Inky to HTML:

123456789101112
{% apply inky_to_html %} <container> <row class="header"> <columns> <spacer size="16"></spacer> <h1 class="text-center">Welcome {{ email.toName }}!</h1> </columns> {# ... #} </row> </container>{% endapply %}

You can combine all filters to create complex email messages:

123
{% apply inky_to_html|inline_css(source('@styles/foundation-emails.css')) %} {# ... #}{% endapply %}

This makes use of the styles Twig namespace we createdearlier. You could, for example, download the foundation-emails.css filedirectly from GitHub and save it in assets/styles.

Signing and Encrypting Messages

It's possible to sign and/or encrypt email messages to increase theirintegrity/security. Both options can be combined to encrypt a signed messageand/or to sign an encrypted message.

Before signing/encrypting messages, make sure to have:

Tip

When using OpenSSL to generate certificates, make sure to add the-addtrust emailProtection command option.

Caution

Signing and encrypting messages require their contents to be fully rendered.For example, the content of templated emails is renderedby a MessageListener.So, if you want to sign and/or encrypt such a message, you need to do it ina MessageEvent listener run after it (you need to seta negative priority to your listener).

Signing Messages

When signing a message, a cryptographic hash is generated for the entire contentof the message (including attachments). This hash is added as an attachment sothe recipient can validate the integrity of the received message. However, thecontents of the original message are still readable for mailing agents notsupporting signed messages, so you must also encrypt the message if you want tohide its contents.

You can sign messages using either S/MIME or DKIM. In both cases, thecertificate and private key must be PEM encoded, and can be either createdusing for example OpenSSL or obtained at an official Certificate Authority (CA).The email recipient must have the CA certificate in the list of trusted issuersin order to verify the signature.

Caution

If you use message signature, sending to Bcc will be removed from themessage. If you need to send a message to multiple recipients, you needto compute a new signature for each recipient.

S/MIME Signer

S/MIME is a standard for public key encryption and signing of MIME data. Itrequires using both a certificate and a private key:

1234567891011121314
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 passintermediate certificates and to configure the signing process using abitwise operator options for 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 privatekey but not a certificate:

12345678910111213141516171819202122232425262728
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 themuse Symfony\Component\Mime\Crypto\DkimOptions;$signedEmail = $signer->sign($email, (new DkimOptions()) ->bodyCanon('relaxed') ->headerCanon('relaxed') ->headersToIgnore(['Message-ID']) ->toArray());

Encrypting Messages

When encrypting a message, the entire message (including attachments) isencrypted using a certificate. Therefore, only the recipients that have thecorresponding private key can read the original message contents:

1234567891011
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 constructorand it will select the appropriate certificate depending on the To option:

123456789101112131415161718
$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);

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 atransports entry, like:

123456
# config/packages/mailer.yamlframework: mailer: transports: main: '%env(MAILER_DSN)%' alternative: '%env(MAILER_DSN_IMPORTANT)%'

By default the first transport is used. The other transports can be selected byadding an X-Transport header (which Mailer will remove automatically fromthe final email):

123456
// Send using first transport ("main"):$mailer->send($email);// ... or use the transport "alternative":$email->getHeaders()->addTextHeader('X-Transport', 'alternative');$mailer->send($email);

Sending Messages Async

When you call $mailer->send($email), the email is sent to the transport immediately.To improve performance, you can leverage Messenger to sendthe messages later via a Messenger transport.

Start by following the Messenger documentation and configuringa transport. Once everything is set up, when you call $mailer->send(), aSendEmailMessage message willbe dispatched through the default message bus (messenger.default_bus). Assumingyou have a transport called async, you can route the message there:

12345678
# config/packages/messenger.yamlframework: messenger: transports: async: "%env(MESSENGER_TRANSPORT_DSN)%" routing: 'Symfony\Component\Mailer\Messenger\SendEmailMessage': async

Thanks to this, instead of being delivered immediately, messages will be sentto the transport to be handled later (see ). Note thatthe "rendering" of the email (computed headers, body rendering, ...) is alsodeferred and will only happen just before the email is sent by the Messengerhandler.

When sending an email asynchronously, its instance must be serializable.This is always the case for Mailerinstances, but when sending aTemplatedEmail, you must ensure thatthe context is serializable. If you have non-serializable variables,like Doctrine entities, either replace them with more specific variables orrender the email before calling $mailer->send($email):

12345678910111213
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 anddisable asynchronous delivery.

1234
# config/packages/mailer.yamlframework: mailer: message_bus: app.another_bus

Note

In cases of long-running scripts, and when Mailer uses theSmtpTransportyou may manually disconnect from the SMTP server to avoid keepingan 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 (whichwill be removed automatically from the final message):

123
// Use the bus transport "app.another_bus":$email->getHeaders()->addTextHeader('X-Bus-Transport', 'app.another_bus');$mailer->send($email);

Certain 3rd party transports support email tags and metadata, which can be usedfor grouping, tracking and workflows. You can add those by using theTagHeader andMetadataHeader classes. If your transportsupports headers, it will convert them to their appropriate format:

123456
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:

123
X-Tag: password-resetX-Metadata-Color: blueX-Metadata-Client-ID: 12345

The following transports currently support tags and metadata:

  • Brevo
  • Mailgun
  • 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 Symfonycalls it "metadata" because it contains a key and a value)

Draft Emails

DraftEmail is a special instance ofEmail. Its purpose is to build up an email(with body, attachments, etc) and make available to download as an .eml withthe X-Unsent header. Many email clients can open these files and interpretthem as draft emails. You can use these to create advanced mailto: links.

Here's an example of making one available to download:

123456789101112131415161718192021222324252627282930
// src/Controller/DownloadEmailController.phpnamespace 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 DraftEmail's to be createdwithout a To/From they cannot be sent with the mailer.

Mailer Events

MessageEvent

Event Class: MessageEvent

MessageEvent allows to change the Mailer message and the envelope beforethe email is sent:

123456789101112131415
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 willalso stop the event propagation):

123456
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 eventand their priorities:

1
$ php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\MessageEvent"

SentMessageEvent

Event Class: SentMessageEvent

SentMessageEvent allows you to act on the SentMessageclass to access the original message (getOriginalMessage()) and some debugginginformation (getDebug()) such as the HTTP calls made by the HTTP transports,which is useful for debugging errors:

12345678910111213
use Symfony\Component\EventDispatcher\EventSubscriberInterface;use Symfony\Component\Mailer\Event\SentMessageEvent;use Symfony\Component\Mailer\SentMessage;public function onMessage(SentMessageEvent $event): void{ $message = $event->getMessage(); if (!$message instanceof SentMessage) { return; } // do something with the message}

Execute this command to find out which listeners are registered for this eventand their priorities:

1
$ php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\SentMessageEvent"

FailedMessageEvent

Event Class: FailedMessageEvent

FailedMessageEvent allows acting on the the initial message in case of a failure:

12345678910
use Symfony\Component\EventDispatcher\EventSubscriberInterface;use Symfony\Component\Mailer\Event\FailedMessageEvent;public function onMessage(FailedMessageEvent $event): void{ // e.g you can get more information on this error when sending an email $event->getError(); // do something with the message}

Execute this command to find out which listeners are registered for this eventand their priorities:

1
$ php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\FailedMessageEvent"

Enabling an Email Catcher

When developing locally, it is recommended to use an email catcher. If you haveenabled Docker support via Symfony recipes, an email catcher is automaticallyconfigured. In addition, if you are using the Symfony local web server, the mailer DSN is automatically exposed via thesymfony binary Docker integration.

Sending Test Emails

Symfony provides a command to send emails, which is useful during developmentto test if sending emails works correctly:

123
# 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 Messenger bus, if configured, toease testing emails even when the Messenger consumer is not running.

Disabling Delivery

While developing (or testing), you may want to disable delivery of messagesentirely. You can do this by using null://null as the mailer DSN, either inyour .env configuration files or inthe mailer configuration file (e.g. in the dev or test environments):

12345
# config/packages/mailer.yamlwhen@dev: framework: mailer: dsn: 'null://null'

Note

If you're using Messenger and routing to a transport, the message will stillbe sent to that transport.

Always Send to the same Address

Instead of disabling delivery entirely, you might want to always send emails toa specific address, instead of the real address:

123456
# config/packages/mailer.yamlwhen@dev: framework: mailer: envelope: recipients: ['youremail@example.com']

Use the allowed_recipients option to specify exceptions to the behavior definedin the recipients option; allowing emails directed to these specific recipientsto maintain their original destination:

12345678910
# config/packages/mailer.yamlwhen@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)'

With this configuration, all emails will be sent to youremail@example.com,except for those sent to internal@example.com, internal-monitoring@example.fr,etc., which will receive emails as usual.

7.1

The allowed_recipients option was introduced in Symfony 7.1.

Write a Functional Test

Symfony provides lots of built-in mailer assertionsto functionally test that an email was sent, its contents or headers, etc.They are available in test classes extendingKernelTestCase or when usingthe MailerAssertionsTrait:

123456789101112131415161718192021
// tests/Controller/MailControllerTest.phpnamespace 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, makesure to have your client not follow redirects. The kernel is rebooted afterfollowing the redirection and the message will be lost from the mailer eventhandler.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.

TOC
    Version
    Sending Emails with Mailer (Symfony Docs) (2024)
    Top Articles
    Latest Posts
    Article information

    Author: Dean Jakubowski Ret

    Last Updated:

    Views: 6139

    Rating: 5 / 5 (50 voted)

    Reviews: 89% of readers found this page helpful

    Author information

    Name: Dean Jakubowski Ret

    Birthday: 1996-05-10

    Address: Apt. 425 4346 Santiago Islands, Shariside, AK 38830-1874

    Phone: +96313309894162

    Job: Legacy Sales Designer

    Hobby: Baseball, Wood carving, Candle making, Jigsaw puzzles, Lacemaking, Parkour, Drawing

    Introduction: My name is Dean Jakubowski Ret, I am a enthusiastic, friendly, homely, handsome, zealous, brainy, elegant person who loves writing and wants to share my knowledge and understanding with you.