Bricolage-Histoires

Symfony : gestion avancée de l’envoi d’e-mails

Symfony : gestion avancée de l’envoi d’e-mails

Symfony : gestion avancée de l’envoi d’e-mails

 

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:

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

NOTE

Some mailer providers change the Message-Id when sending the email. The getMessageId() method from SentMessage 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:

 composer require symfony/twig-bundle

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:

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

    // pass variables (name => value) to the template
    ->context([
        'expiration_date' => new \DateTime('+7 days'),
        'username' => 'foo',
    ])
;

Then, create the template:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{# 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="#">Click here to 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 called email, which is an instance of Symfony\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 the textTemplate() method provided by the TemplatedEmail class:

1
2
3
4
5
6
7
8
9
+ 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  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:

1
2
3
4
5
6
7
# config/packages/twig.yaml
twig:
    # ...

    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 inside the email contents:

1
2
3
4
5
{# '@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 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:

 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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{% 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 as arguments to the filter:

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

1
2
3
4
5
6
7
# config/packages/twig.yaml
twig:
    # ...

    paths:
        # point this wherever your css files live
        '%kernel.project_dir%/assets/css': css

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

 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
2
3
4
5
6
7
8
9
{% apply markdown_to_html %}
    Welcome {{ email.toName }}!
    ===========================

    You signed up to our site using the following email:
    `{{ email.to[0].address }}`

    [Click here to activate your account]({{ url('...') }})
{% 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 simple tags which are later transformed into the real HTML code sent to users:

1
2
3
4
5
6
<container>
    <row>
        <columns>This is a column.</columns>
    </row>
</container>

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

 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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{% 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:

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

This makes use of the css Twig namespace we created earlier. You could, for example, download the foundation-emails.css file directly from GitHub and save it in assets/css.

Signing and Encrypting Messages

It’s possible to sign and/or encrypt email messages applying the S/MIME standard 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:

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:

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

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.

TIP

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

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.

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

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:

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

By default the first transport is used. The other transports can be used by adding a text header X-Transport to an email:

// Send using first "main" transport ...
$mailer->send($email);

// ... or use the "alternative" one
$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 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(), a 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:

    • YAML
      1
      2
      3
      4
      5
      6
      7
      8
      # config/packages/messenger.yaml
      framework:
          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 sent to the transport to be handled later (see Consuming Messages (Running the Worker)).

Development & Debugging

Disabling Delivery

While developing (or testing), you may want to disable delivery of messages entirely. You can do this by forcing Mailer to use the NullTransport in only the dev environment:

1
2
3
4
# config/packages/dev/mailer.yaml
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:

1
2
3
4
5
# config/packages/dev/mailer.yaml
framework:
    mailer:
        envelope:
            recipients: ['youremail@example.com']

 

Traduction de l’article Source : https://symfony.com/doc/current/mailer.html

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