Sending Emails with Mailer

Sending Emails with Mailer

Installation

Symfony’s Mailer & 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:

  1. $ composer require symfony/mailer

Transport Setup

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

  1. # .env
  2. MAILER_DSN=smtp://user:[email protected]:port
  • YAML

    1. # config/packages/mailer.yaml
    2. framework:
    3. mailer:
    4. dsn: '%env(MAILER_DSN)%'
  • XML

    1. <!-- config/packages/mailer.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <container xmlns="http://symfony.com/schema/dic/services"
    4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5. xmlns:framework="http://symfony.com/schema/dic/symfony"
    6. xsi:schemaLocation="http://symfony.com/schema/dic/services
    7. https://symfony.com/schema/dic/services/services-1.0.xsd
    8. http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    9. <framework:config>
    10. <framework:mailer dsn="%env(MAILER_DSN)%"/>
    11. </framework:config>
    12. </container>
  • PHP

    1. // config/packages/mailer.php
    2. use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
    3. return static function (ContainerConfigurator $containerConfigurator): void {
    4. $containerConfigurator->extension('framework', [
    5. 'mailer' => [
    6. 'dsn' => '%env(MAILER_DSN)%',
    7. ]
    8. ]);
    9. };

Caution

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 urlencode function to encode them.

Caution

If you are migrating from Swiftmailer (and the Swiftmailer bundle), be warned that the DSN format is different.

Using Built-in Transports

New in version 5.2: The native protocol was introduced in Symfony 5.2.

DSN protocolExampleDescription
smtpsmtp://user:pass@smtp.example.com:25Mailer uses an SMTP server to send emails
sendmailsendmail://defaultMailer uses the local sendmail binary to send emails
nativenative://defaultMailer 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.

Using a 3rd Party Transport

Instead of using your own SMTP server or sendmail binary, you can send emails via a 3rd party provider. Mailer supports several - install whichever you want:

ServiceInstall with
Amazon SEScomposer require symfony/amazon-mailer
Gmailcomposer require symfony/google-mailer
MailChimpcomposer require symfony/mailchimp-mailer
Mailguncomposer require symfony/mailgun-mailer
Mailjetcomposer require symfony/mailjet-mailer
Postmarkcomposer require symfony/postmark-mailer
SendGridcomposer require symfony/sendgrid-mailer
Sendinbluecomposer require symfony/sendinblue-mailer

New in version 5.2: The Sendinblue integration was introduced in Symfony 5.2.

Each library includes a Symfony Flex recipe that will add a configuration example to your .env file. For example, suppose you want to use 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:

  1. # .env
  2. MAILER_DSN=sendgrid://[email protected]

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 just 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 or Mailgun. Some providers support sending via http, api or smtp. Symfony chooses the best available transport, but you can force to use one:

  1. # .env
  2. # force to use SMTP instead of HTTP (which is the default)
  3. MAILER_DSN=sendgrid+smtp://$SENDGRID_KEY@default

This table shows the full list of available DSN formats for each third party provider:

ProviderSMTPHTTPAPI
Amazon SESses+smtp://USERNAME:PASSWORD@defaultses+https://ACCESS_KEY:SECRET_KEY@defaultses+api://ACCESS_KEY:SECRET_KEY@default
Google Gmailgmail+smtp://USERNAME:PASSWORD@defaultn/an/a
Mailchimp Mandrillmandrill+smtp://USERNAME:PASSWORD@defaultmandrill+https://KEY@defaultmandrill+api://KEY@default
Mailgunmailgun+smtp://USERNAME:PASSWORD@defaultmailgun+https://KEY:DOMAIN@defaultmailgun+api://KEY:DOMAIN@default
Mailjetmailjet+smtp://ACCESS_KEY:SECRET_KEY@defaultn/amailjet+api://ACCESS_KEY:SECRET_KEY@default
Postmarkpostmark+smtp://ID@defaultn/apostmark+api://KEY@default
Sendgridsendgrid+smtp://KEY@defaultn/asendgrid+api://KEY@default
Sendinbluesendinblue+smtp://USERNAME:PASSWORD@defaultn/asendinblue+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 be configured as ses+smtp://ABC1234:abc%2B12%2F345@default

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.

New in version 5.1: The usage of default_socket_timeout as the default timeout was introduced in Symfony 5.1.

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:

  1. # .env
  2. MAILER_DSN=mailgun+https://KEY:[email protected]
  3. MAILER_DSN=mailgun+https://KEY:[email protected]:99

Note that the protocol is always HTTPs and cannot be changed.

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:

  1. MAILER_DSN="failover(postmark+api://[email protected] sendgrid+smtp://[email protected])"

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).

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:

  1. MAILER_DSN="roundrobin(postmark+api://[email protected] sendgrid+smtp://[email protected])"

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.

New in version 5.1: The random selection of the first transport was introduced in Symfony 5.1. In previous Symfony versions the first transport was always selected first.

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:

  1. $dsn = 'smtp://user:[email protected]?verify_peer=0';

New in version 5.1: The verify_peer option was introduced in Symfony 5.1.

Creating & Sending Messages

To send an email, get a Symfony\Component\Mailer\Mailer instance by type-hinting Symfony\Component\Mailer\MailerInterface and create an Symfony\Component\Mime\Email object:

  1. // src/Controller/MailerController.php
  2. namespace App\Controller;
  3. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  4. use Symfony\Component\HttpFoundation\Response;
  5. use Symfony\Component\Mailer\MailerInterface;
  6. use Symfony\Component\Mime\Email;
  7. class MailerController extends AbstractController
  8. {
  9. /**
  10. * @Route("/email")
  11. */
  12. public function sendEmail(MailerInterface $mailer): Response
  13. {
  14. $email = (new Email())
  15. ->from('[email protected]')
  16. ->to('[email protected]')
  17. //->cc('[email protected]')
  18. //->bcc('[email protected]')
  19. //->replyTo('[email protected]')
  20. //->priority(Email::PRIORITY_HIGH)
  21. ->subject('Time for Symfony Mailer!')
  22. ->text('Sending emails is fun again!')
  23. ->html('<p>See Twig integration for better HTML integration!</p>');
  24. $mailer->send($email);
  25. // ...
  26. }
  27. }

That’s it! The message will be sent via the transport you configured.

Email Addresses

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

  1. // ...
  2. use Symfony\Component\Mime\Address;
  3. $email = (new Email())
  4. // email address as a simple string
  5. ->from('[email protected]')
  6. // email address as an object
  7. ->from(new Address('[email protected]'))
  8. // defining the email address and name as an object
  9. // (email clients will display the name)
  10. ->from(new Address('[email protected]', 'Fabien'))
  11. // defining the email address and name as a string
  12. // (the format must match: 'Name <[email protected]>')
  13. ->from(Address::create('Fabien Potencier <[email protected]>'))
  14. // ...
  15. ;

Tip

Instead of calling ->from() *every* time you create a new email, you can [configure emails globally](#mailer-configure-email-globally) to set the sameFrom` email to all messages.

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.

New in version 5.2: Support for UTF-8 characters in email addresses was introduced in Symfony 5.2.

Use addTo(),addCc(), or `addBcc() methods to add more addresses:

  1. $email = (new Email())
  2. ->to('[email protected]')
  3. ->addTo('[email protected]')
  4. ->cc('[email protected]')
  5. ->addCc('[email protected]')
  6. // ...
  7. ;

Alternatively, you can pass multiple addresses to each method:

  1. $toAddresses = ['[email protected]', new Address('[email protected]')];
  2. $email = (new Email())
  3. ->to(...$toAddresses)
  4. ->cc('[email protected]', '[email protected]')
  5. // ...
  6. ;

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:

  1. $email = (new Email())
  2. ->getHeaders()
  3. // this header tells auto-repliers ("email holiday mode") to not
  4. // reply to this message because it's an automated email
  5. ->addTextHeader('X-Auto-Response-Suppress', 'OOF, DR, RN, NRN, AutoReply');
  6. // ...
  7. ;

Tip

Instead of calling `->addTextHeader() every time you create a new email, you can 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:

  1. $email = (new Email())
  2. // ...
  3. // simple contents defined as a string
  4. ->text('Lorem ipsum...')
  5. ->html('<p>Lorem ipsum...</p>')
  6. // attach a file stream
  7. ->text(fopen('/path/to/emails/user_signup.txt', 'r'))
  8. ->html(fopen('/path/to/emails/user_signup.html', 'r'))
  9. ;

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 `attachFromPath() method to attach files that exist on your file system:

  1. $email = (new Email())
  2. // ...
  3. ->attachFromPath('/path/to/documents/terms-of-use.pdf')
  4. // optionally you can tell email clients to display a custom name for the file
  5. ->attachFromPath('/path/to/documents/privacy.pdf', 'Privacy Policy')
  6. // optionally you can provide an explicit MIME type (otherwise it's guessed)
  7. ->attachFromPath('/path/to/documents/contract.doc', 'Contract', 'application/msword')
  8. ;

Alternatively you can use the `attach() method to attach contents from a stream:

  1. $email = (new Email())
  2. // ...
  3. ->attach(fopen('/path/to/documents/contract.doc', 'r'))
  4. ;

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 later in this article, the images are embedded automatically. Otherwise, you need to embed them manually.

First, use the embed() orembedFromPath() method to add an image from a file or stream:

  1. $email = (new Email())
  2. // ...
  3. // get the image contents from a PHP resource
  4. ->embed(fopen('/path/to/images/logo.png', 'r'), 'logo')
  5. // get the image contents from an existing file
  6. ->embedFromPath('/path/to/images/signature.gif', 'footer-signature')
  7. ;

The second optional argument of both methods is the image name (“Content-ID” in the MIME standard). Its value is an arbitrary string used later to reference the images inside the HTML contents:

  1. $email = (new Email())
  2. // ...
  3. ->embed(fopen('/path/to/images/logo.png', 'r'), 'logo')
  4. ->embedFromPath('/path/to/images/signature.gif', 'footer-signature')
  5. // reference images using the syntax 'cid:' + "image embed name"
  6. ->html('<img src="cid:logo"> ... <img src="cid:footer-signature"> ...')
  7. ;

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.

  • YAML

    1. # config/packages/dev/mailer.yaml
    2. framework:
    3. mailer:
    4. envelope:
    5. sender: '[email protected]'
    6. recipients: ['[email protected]', '[email protected]']
    7. headers:
    8. from: 'Fabien <[email protected]>'
    9. bcc: '[email protected]'
    10. X-Custom-Header: 'foobar'
  • XML

    1. <!-- config/packages/mailer.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <container xmlns="http://symfony.com/schema/dic/services"
    4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5. xmlns:framework="http://symfony.com/schema/dic/symfony"
    6. xsi:schemaLocation="http://symfony.com/schema/dic/services
    7. https://symfony.com/schema/dic/services/services-1.0.xsd
    8. http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    9. <!-- ... -->
    10. <framework:config>
    11. <framework:mailer>
    12. <framework:envelope>
    13. <framework:sender>[email protected]</framework:sender>
    14. <framework:recipients>[email protected]</framework:recipients>
    15. <framework:recipients>[email protected]</framework:recipients>
    16. </framework:envelope>
    17. <framework:header name="from">Fabien &lt;[email protected]&gt;</framework:header>
    18. <framework:header name="bcc">[email protected]</framework:header>
    19. <framework:header name="X-Custom-Header">foobar</framework:header>
    20. </framework:mailer>
    21. </framework:config>
    22. </container>
  • PHP

    1. // config/packages/mailer.php
    2. use Symfony\Config\FrameworkConfig;
    3. return static function (FrameworkConfig $framework) {
    4. $mailer = $framework->mailer();
    5. $mailer
    6. ->envelope()
    7. ->sender('[email protected]')
    8. ->recipients(['[email protected]', '[email protected]'])
    9. ;
    10. $mailer->header('from')->value('Fabien <[email protected]>');
    11. $mailer->header('bcc')->value('[email protected]');
    12. $mailer->header('X-Custom-Header')->value('foobar');
    13. };

New in version 5.2: The headers option was introduced in Symfony 5.2.

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 Symfony\Component\Mailer\Exception\TransportExceptionInterface. Catch that exception to recover from the error or to display some message:

  1. use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
  2. $email = new Email();
  3. // ...
  4. try {
  5. $mailer->send($email);
  6. } catch (TransportExceptionInterface $e) {
  7. // some error prevented the email sending; display an
  8. // error message or try to resend the message
  9. }

Debugging Emails

The Symfony\Component\Mailer\SentMessage object returned by the send() method of theSymfony\Component\Mailer\Transport\TransportInterfaceprovides 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.

Note

Some mailer providers change the Message-Id when sending the email. The getMessageId() method fromSentMessage` always returns the definitive ID of the message (being the original random ID generated by Symfony or the new ID generated by the mailer provider).

The exceptions related to mailer transports (those which implement Symfony\Component\Mailer\Exception\TransportException) also provide this debug information via the `getDebug() method.

Twig: HTML & CSS

The Mime component integrates with the 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:

  1. $ composer require symfony/twig-bundle
  2. # or if you're using the component in a non-Symfony app:
  3. # composer require symfony/twig-bridge

HTML Content

To define the contents of your email with Twig, use the Symfony\Bridge\Twig\Mime\TemplatedEmail class. This class extends the normal Symfony\Component\Mime\Email class but adds some new methods for Twig templates:

  1. use Symfony\Bridge\Twig\Mime\TemplatedEmail;
  2. $email = (new TemplatedEmail())
  3. ->from('[email protected]')
  4. ->to(new Address('[email protected]'))
  5. ->subject('Thanks for signing up!')
  6. // path of the Twig template to render
  7. ->htmlTemplate('emails/signup.html.twig')
  8. // pass variables (name => value) to the template
  9. ->context([
  10. 'expiration_date' => new \DateTime('+7 days'),
  11. 'username' => 'foo',
  12. ])
  13. ;

Then, create the template:

  1. {# templates/emails/signup.html.twig #}
  2. <h1>Welcome {{ email.toName }}!</h1>
  3. <p>
  4. You signed up as {{ username }} the following email:
  5. </p>
  6. <p><code>{{ email.to[0].address }}</code></p>
  7. <p>
  8. <a href="#">Click here to activate your account</a>
  9. (this link is valid until {{ expiration_date|date('F jS') }})
  10. </p>

The Twig template has access to any of the parameters passed in the context() method of theTemplatedEmailclass and also to a special variable calledemail, which is an instance ofSymfony\Bridge\Twig\Mime\WrappedTemplatedEmail`.

Text Content

When the text content of a TemplatedEmail is not explicitly defined, mailer will generate it automatically by converting the HTML contents into text. If you have league/html-to-markdown installed in your application, it uses that to turn HTML into Markdown (so the text email has some visual appeal). Otherwise, it applies the 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 thetextTemplate() method provided by the TemplatedEmail class:

  1. + use Symfony\Bridge\Twig\Mime\TemplatedEmail;
  2. $email = (new TemplatedEmail())
  3. // ...
  4. ->htmlTemplate('emails/signup.html.twig')
  5. + ->textTemplate('emails/signup.txt.twig')
  6. // ...
  7. ;

Embedding Images

Instead of dealing with the <img src="cid: ..."> 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:

  • YAML

    1. # config/packages/twig.yaml
    2. twig:
    3. # ...
    4. paths:
    5. # point this wherever your images live
    6. '%kernel.project_dir%/assets/images': images
  • XML

    1. <!-- config/packages/twig.xml -->
    2. <container xmlns="http://symfony.com/schema/dic/services"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xmlns:twig="http://symfony.com/schema/dic/twig"
    5. xsi:schemaLocation="http://symfony.com/schema/dic/services
    6. https://symfony.com/schema/dic/services/services-1.0.xsd
    7. http://symfony.com/schema/dic/twig https://symfony.com/schema/dic/twig/twig-1.0.xsd">
    8. <twig:config>
    9. <!-- ... -->
    10. <!-- point this wherever your images live -->
    11. <twig:path namespace="images">%kernel.project_dir%/assets/images</twig:path>
    12. </twig:config>
    13. </container>
  • PHP

    1. // config/packages/twig.php
    2. use Symfony\Config\TwigConfig;
    3. return static function (TwigConfig $twig) {
    4. // ...
    5. // point this wherever your images live
    6. $twig->path('%kernel.project_dir%/assets/images', 'images');
    7. };

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

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

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 <style> ... </style> 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:

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

  1. {% apply inline_css %}
  2. <style>
  3. {# here, define your CSS styles as usual #}
  4. h1 {
  5. color: #333;
  6. }
  7. </style>
  8. <h1>Welcome {{ email.toName }}!</h1>
  9. {# ... #}
  10. {% endapply %}

Using External CSS Files

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

  1. {% apply inline_css(source('@styles/email.css')) %}
  2. <h1>Welcome {{ username }}!</h1>
  3. {# ... #}
  4. {% 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 calledstylesthat points to the directory whereemail.css` lives:

  • YAML

    1. # config/packages/twig.yaml
    2. twig:
    3. # ...
    4. paths:
    5. # point this wherever your css files live
    6. '%kernel.project_dir%/assets/styles': styles
  • XML

    1. <!-- config/packages/twig.xml -->
    2. <container xmlns="http://symfony.com/schema/dic/services"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xmlns:twig="http://symfony.com/schema/dic/twig"
    5. xsi:schemaLocation="http://symfony.com/schema/dic/services
    6. https://symfony.com/schema/dic/services/services-1.0.xsd
    7. http://symfony.com/schema/dic/twig https://symfony.com/schema/dic/twig/twig-1.0.xsd">
    8. <twig:config>
    9. <!-- ... -->
    10. <!-- point this wherever your css files live -->
    11. <twig:path namespace="styles">%kernel.project_dir%/assets/styles</twig:path>
    12. </twig:config>
    13. </container>
  • PHP

    1. // config/packages/twig.php
    2. use Symfony\Config\TwigConfig;
    3. return static function (TwigConfig $twig) {
    4. // ...
    5. // point this wherever your css files live
    6. $twig->path('%kernel.project_dir%/assets/styles', 'styles');
    7. };

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

  1. # instead of league/commonmark, you can also use erusev/parsedown or michelf/php-markdown
  2. $ 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:

  1. {% apply markdown_to_html %}
  2. Welcome {{ email.toName }}!
  3. ===========================
  4. You signed up to our site using the following email:
  5. `{{ email.to[0].address }}`
  6. [Click here to activate your account]({{ url('...') }})
  7. {% endapply %}

Inky Email Templating Language

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:

  1. <!-- a simplified example of the Inky syntax -->
  2. <container>
  3. <row>
  4. <columns>This is a column.</columns>
  5. </row>
  6. </container>

Twig provides integration with Inky via the InkyExtension. First, install the 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 convert parts or the entire email contents from Inky to HTML:

  1. {% apply inky_to_html %}
  2. <container>
  3. <row class="header">
  4. <columns>
  5. <spacer size="16"></spacer>
  6. <h1 class="text-center">Welcome {{ email.toName }}!</h1>
  7. </columns>
  8. {# ... #}
  9. </row>
  10. </container>
  11. {% endapply %}

You can combine all filters to create complex email messages:

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

This makes use of the 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

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:

Tip

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

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.

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:

  1. use Symfony\Component\Mime\Crypto\SMimeSigner;
  2. use Symfony\Component\Mime\Email;
  3. $email = (new Email())
  4. ->from('[email protected]')
  5. // ...
  6. ->html('...');
  7. $signer = new SMimeSigner('/path/to/certificate.crt', '/path/to/certificate-private-key.key');
  8. // if the private key has a passphrase, pass it as the third argument
  9. // new SMimeSigner('/path/to/certificate.crt', '/path/to/certificate-private-key.key', 'the-passphrase');
  10. $signedEmail = $signer->sign($email);
  11. // 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 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:

  1. use Symfony\Component\Mime\Crypto\DkimSigner;
  2. use Symfony\Component\Mime\Email;
  3. $email = (new Email())
  4. ->from('[email protected]')
  5. // ...
  6. ->html('...');
  7. // first argument: same as openssl_pkey_get_private(), either a string with the
  8. // contents of the private key or the absolute path to it (prefixed with 'file://')
  9. // second and third arguments: the domain name and "selector" used to perform a DNS lookup
  10. // (the selector is a string used to point to a specific DKIM public key record in your DNS)
  11. $signer = new DkimSigner('file:///path/to/private-key.key', 'example.com', 'sf');
  12. // if the private key has a passphrase, pass it as the fifth argument
  13. // new DkimSigner('file:///path/to/private-key.key', 'example.com', 'sf', [], 'the-passphrase');
  14. $signedEmail = $signer->sign($email);
  15. // now use the Mailer component to send this $signedEmail instead of the original email
  16. // DKIM signer provides many config options and a helper object to configure them
  17. use Symfony\Component\Mime\Crypto\DkimOptions;
  18. $signedEmail = $signer->sign($email, (new DkimOptions())
  19. ->bodyCanon('relaxed')
  20. ->headerCanon('relaxed')
  21. ->headersToIgnore(['Message-ID'])
  22. ->toArray()
  23. );

New in version 5.2: The DKIM signer was introduced in Symfony 5.2.

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:

  1. use Symfony\Component\Mime\Crypto\SMimeEncrypter;
  2. use Symfony\Component\Mime\Email;
  3. $email = (new Email())
  4. ->from('[email protected]')
  5. // ...
  6. ->html('...');
  7. $encrypter = new SMimeEncrypter('/path/to/certificate.crt');
  8. $encryptedEmail = $encrypter->encrypt($email);
  9. // 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:

  1. $firstEmail = (new Email())
  2. // ...
  3. ->to('[email protected]');
  4. $secondEmail = (new Email())
  5. // ...
  6. ->to('[email protected]');
  7. // the second optional argument of SMimeEncrypter defines which encryption algorithm is used
  8. // (it must be one of these constants: https://www.php.net/manual/en/openssl.ciphers.php)
  9. $encrypter = new SMimeEncrypter([
  10. // key = email recipient; value = path to the certificate file
  11. '[email protected]' => '/path/to/first-certificate.crt',
  12. '[email protected]' => '/path/to/second-certificate.crt',
  13. ]);
  14. $firstEncryptedEmail = $encrypter->encrypt($firstEmail);
  15. $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 a transports entry, like:

  • YAML

    1. # config/packages/mailer.yaml
    2. framework:
    3. mailer:
    4. transports:
    5. main: '%env(MAILER_DSN)%'
    6. alternative: '%env(MAILER_DSN_IMPORTANT)%'
  • XML

    1. <!-- config/packages/mailer.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <container xmlns="http://symfony.com/schema/dic/services"
    4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5. xmlns:framework="http://symfony.com/schema/dic/symfony"
    6. xsi:schemaLocation="http://symfony.com/schema/dic/services
    7. https://symfony.com/schema/dic/services/services-1.0.xsd
    8. http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    9. <!-- ... -->
    10. <framework:config>
    11. <framework:mailer>
    12. <framework:transport name="main">%env(MAILER_DSN)%</framework:transport>
    13. <framework:transport name="alternative">%env(MAILER_DSN_IMPORTANT)%</framework:transport>
    14. </framework:mailer>
    15. </framework:config>
    16. </container>
  • PHP

    1. // config/packages/mailer.php
    2. use Symfony\Config\FrameworkConfig;
    3. return static function (FrameworkConfig $framework) {
    4. $framework->mailer()
    5. ->transport('main', '%env(MAILER_DSN)%')
    6. ->transport('alternative', '%env(MAILER_DSN_IMPORTANT)%')
    7. ;
    8. };

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

  1. // Send using first transport ("main"):
  2. $mailer->send($email);
  3. // ... or use the transport "alternative":
  4. $email->getHeaders()->addTextHeader('X-Transport', 'alternative');
  5. $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 send the messages later via a Messenger transport.

Start by following the Messenger documentation and configuring a transport. Once everything is set up, when you call $mailer->send(), aSymfony\Component\Mailer\Messenger\SendEmailMessagemessage will be dispatched through the default message bus (messenger.default_bus). Assuming you have a transport calledasync`, you can route the message there:

  • YAML

    1. # config/packages/messenger.yaml
    2. framework:
    3. messenger:
    4. transports:
    5. async: "%env(MESSENGER_TRANSPORT_DSN)%"
    6. routing:
    7. 'Symfony\Component\Mailer\Messenger\SendEmailMessage': async
  • XML

    1. <!-- config/packages/messenger.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <container xmlns="http://symfony.com/schema/dic/services"
    4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5. xmlns:framework="http://symfony.com/schema/dic/symfony"
    6. xsi:schemaLocation="http://symfony.com/schema/dic/services
    7. https://symfony.com/schema/dic/services/services-1.0.xsd
    8. http://symfony.com/schema/dic/symfony
    9. https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    10. <framework:config>
    11. <framework:messenger>
    12. <framework:transport name="async">%env(MESSENGER_TRANSPORT_DSN)%</framework:transport>
    13. <framework:routing message-class="Symfony\Component\Mailer\Messenger\SendEmailMessage">
    14. <framework:sender service="async"/>
    15. </framework:routing>
    16. </framework:messenger>
    17. </framework:config>
    18. </container>
  • PHP

    1. // config/packages/messenger.php
    2. use Symfony\Config\FrameworkConfig;
    3. return static function (FrameworkConfig $framework) {
    4. $framework->messenger()
    5. ->transport('async')->dsn('%env(MESSENGER_TRANSPORT_DSN)%');
    6. $framework->messenger()
    7. ->routing('Symfony\Component\Mailer\Messenger\SendEmailMessage')
    8. ->senders(['async']);
    9. };

Thanks to this, instead of being delivered immediately, messages will be sent to the transport to be handled later (see Consuming Messages (Running the Worker)).

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.

  • YAML

    1. # config/packages/mailer.yaml
    2. framework:
    3. mailer:
    4. message_bus: app.another_bus
  • XML

    1. <!-- config/packages/messenger.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <container xmlns="http://symfony.com/schema/dic/services"
    4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5. xmlns:framework="http://symfony.com/schema/dic/symfony"
    6. xsi:schemaLocation="http://symfony.com/schema/dic/services
    7. https://symfony.com/schema/dic/services/services-1.0.xsd
    8. http://symfony.com/schema/dic/symfony
    9. https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    10. <framework:config>
    11. <framework:mailer
    12. message_bus="app.another_bus"
    13. >
    14. </framework:mailer>
    15. </framework:config>
    16. </container>
  • PHP

    1. // config/packages/mailer.php
    2. use Symfony\Config\FrameworkConfig;
    3. return static function (FrameworkConfig $framework) {
    4. $framework->mailer()
    5. ->messageBus('app.another_bus');
    6. };

New in version 5.1: The message_bus option was introduced in Symfony 5.1.

Adding Tags and Metadata to Emails

New in version 5.1: The Symfony\Component\Mailer\Header\TagHeader and Symfony\Component\Mailer\Header\MetadataHeader classes were introduced in Symfony 5.1.

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 Symfony\Component\Mailer\Header\TagHeader and Symfony\Component\Mailer\Header\MetadataHeader classes. If your transport supports headers, it will convert them to their appropriate format:

  1. use Symfony\Component\Mailer\Header\MetadataHeader;
  2. use Symfony\Component\Mailer\Header\TagHeader;
  3. $email->getHeaders()->add(new TagHeader('password-reset'));
  4. $email->getHeaders()->add(new MetadataHeader('Color', 'blue'));
  5. $email->getHeaders()->add(new MetadataHeader('Client-ID', '12345'));

If your transport does not support tags and metadata, they will be added as custom headers:

  1. X-Tag: password-reset
  2. X-Metadata-Color: blue
  3. X-Metadata-Client-ID: 12345

The following transports currently support tags and metadata:

  • MailChimp
  • Mailgun
  • Postmark
  • Sendinblue

Development & Debugging

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 .env configuration files or in the mailer configuration file (e.g. in the dev or test environments):

  • YAML

    1. # config/packages/dev/mailer.yaml
    2. framework:
    3. mailer:
    4. dsn: 'null://null'
  • XML

    1. <!-- config/packages/mailer.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <container xmlns="http://symfony.com/schema/dic/services"
    4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5. xmlns:framework="http://symfony.com/schema/dic/symfony"
    6. xsi:schemaLocation="http://symfony.com/schema/dic/services
    7. https://symfony.com/schema/dic/services/services-1.0.xsd
    8. http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    9. <!-- ... -->
    10. <framework:config>
    11. <framework:mailer dsn="null://null"/>
    12. </framework:config>
    13. </container>
  • PHP

    1. // config/packages/mailer.php
    2. use Symfony\Config\FrameworkConfig;
    3. return static function (FrameworkConfig $framework) {
    4. // ...
    5. $framework->mailer()
    6. ->dsn('null://null');
    7. };

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:

  • YAML

    1. # config/packages/dev/mailer.yaml
    2. framework:
    3. mailer:
    4. envelope:
    5. recipients: ['[email protected]']
  • XML

    1. <!-- config/packages/mailer.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <container xmlns="http://symfony.com/schema/dic/services"
    4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5. xmlns:framework="http://symfony.com/schema/dic/symfony"
    6. xsi:schemaLocation="http://symfony.com/schema/dic/services
    7. https://symfony.com/schema/dic/services/services-1.0.xsd
    8. http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    9. <!-- ... -->
    10. <framework:config>
    11. <framework:mailer>
    12. <framework:envelope>
    13. <framework:recipient>[email protected]</framework:recipient>
    14. </framework:envelope>
    15. </framework:mailer>
    16. </framework:config>
    17. </container>
  • PHP

    1. // config/packages/mailer.php
    2. use Symfony\Config\FrameworkConfig;
    3. return static function (FrameworkConfig $framework) {
    4. // ...
    5. $framework->mailer()
    6. ->envelope()
    7. ->recipients(['[email protected]'])
    8. ;
    9. };

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