Відправлення листів за допомогою Mailer

Дата оновлення перекладу 2022-12-15

Відправлення листів за допомогою Mailer

Установка

Компоненти Symfony Mailer та Mime формують потужну систему для створення та відправлення електронних листів - з підтримкою складових повідомлень, інтеграцією Twig, вбудовуванням CSS, прикріпленням файлів та багато чим іншим. Встановіть їх за допомогою:

1
$ composer require symfony/mailer

Налаштування транспорту

Листи доставляються за допомогою "транспорту". Одразу після установки, ви можете відправляти листи через SMTP, сконфігурувавши DSN у вашому файлі .env (параметри user, pass та port не обов'язкові):

1
2
# .env
MAILER_DSN=smtp://user:pass@smtp.example.com:port
  • YAML
  • XML
  • PHP
1
2
3
4
# config/packages/mailer.yaml
framework:
    mailer:
        dsn: '%env(MAILER_DSN)%'

Caution

Якщо ім'я користувача, пароль або хостинг містять в URI будь-який символ, який вважається особливим (такий як +, @, $, #, /, :, *, !), ви маєте закодувати їх. Див. RFC 3986. щоб побачити повний список зарезервованих символів або використайте функцію urlencode, щоб закодувати їх.

Використання вбудованого транспорту

DSN-???????? ??????? ????
smtp smtp://user:pass@smtp.example.com:25 Mailer ???????????? SMTP-?????? ??? ???????????? ??????
sendmail sendmail://default Mailer ???????????? ???????? ?????????? sendmail ??? ???????????? ??????
native native://default Mailer ???????????? ?????????? sendmail ?? ?????, ?????????????? ? ???????????? sendmail_path ??? php.ini. ?? ????????? Windows, Mailer ???????? ???????????? ???????????? smtp ? smtp_port ??? php.ini, ???? sendmail_path ?? ??????????????.

Caution

При використанні native://default, якщо php.ini використовує команду sendmail -t, у вас не буде звіту про помилки і заголовки Bcc не будуть видалені. Дуже рекомендовано НЕ використовувати native://default, так як ви не можете контролювати, як буде сконфігуровано sendmail (краще використайте sendmail://default, якщо це можливо).

Використання стороннього транспорту

Замість використання вашого власного SMTP-серверу або бінарності binary, ви можете відправляти листи через стороннього постачальника. Mailer підтримує декілька - встановіть той, який захочете:

?????? ????????? ?? ?????????
Amazon SES composer require symfony/amazon-mailer
Gmail composer require symfony/google-mailer
MailChimp composer require symfony/mailchimp-mailer
Mailgun composer require symfony/mailgun-mailer
Mailjet composer require symfony/mailjet-mailer
Postmark composer require symfony/postmark-mailer
SendGrid composer require symfony/sendgrid-mailer
Sendinblue composer require symfony/sendinblue-mailer
MailPace composer require symfony/mailpace-mailer
Infobip composer require symfony/infobip-mailer

6.2

Інтеграція MailPace була представлена в Symfony 6.2 (у попередніх версіях Symfony вона називалася OhMySMTP).

6.2

Інтеграція Infobip була представлена в Symfony 6.2.

Кожна бібліотека містить рецепт Symfony Flex , який буде додавати приклад конфігурації у ваш файл .env. Наприклад, уявіть, що ви хочете використати SendGrid. Спочатку встановіть його:

1
$ composer require symfony/sendgrid-mailer

Тепер у вас буде новий рядок у вашому файлі .env, який ви можете розкоментувати:

1
2
# .env
MAILER_DSN=sendgrid://KEY@default

MAILER_DSN - це не справжня адреса: це зручний формат, який віддає більшу частину роботи конфігурації поштовій програмі. Схема sendgrid активує постачальника SendGrid, який ви щойно встановили, який знає все про те, як доставляти повідомлення через SendGrid. Єдине, що вам необхідно змінити - заповнювач KEY.

Кожний постачальник має різні змінні середовища, які Mailer використовує для конфігурації справжнього протоколу, адреси та аутентифікації для відправки. Деякі також мають опції, які можна сконфігурувати з параметрами запиту в кінці MAILER_DSN - як, наприклад, ?region= для Amazon SES або Mailgun. Деякі постачальники підтримують відправлення через http, api или smtp. Symfony обирає найкращий доступний транспорт, але ви можете форсувати використання одного з них:

1
2
3
# .env
# форсувати використання SMTP замість HTTP (який стоїть за замовчуванням)
MAILER_DSN=sendgrid+smtp://$SENDGRID_KEY@default

Ця таблиця демонструє повний список доступних форматів DSN для кожного стороннього постачальника:

Caution

Якщо ваші дані безпеки містять спеціальні символи, ви маєте URL-закодувати їх. Наприклад, DSN ses+smtp://ABC1234:abc+12/345@default має бути сконфігурована як ses+smtp://ABC1234:abc%2B12%2F345@default

Caution

Якщо ви хочете використати транспорт ses+smtp разом з Messenger для фонового відправлення повідомлень , вам треба додати параметр ping_threshold до вашого= MAILER_DSN зі значенням меншим, ніж 10: ses+smtp://USERNAME:PASSWORD@default?ping_threshold=9

Note

При використанні SMTP, тайм-аут за замовчуванням для відправлення повідомлення до виклику виключення - це значення, визначене в опції PHP.ini default_socket_timeout.

Tip

Якщо ви хочете перевизначити хост провайдера за замовчуванням (щоб налагодити проблему, використовуючи сервіс на кшлталт requestbin.com), змініть default у вашому хості:

1
2
# .env
MAILER_DSN=mailgun+https://KEY:DOMAIN@requestbin.com

Відмітьте, що протокол завжди буде HTTPs, і не може бути змінений.

Висока доступність

Mailer Symfony підтримує високу доступність через техніку під назвою "відмовостійкість", щоб гарантувати, що листи будуть відправлені, навіть якщо один сервер зазнає невдачі.

Транспорт failover сконфігуровано з двома або більше транспортами та ключовим словом failover:

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

Транспорт відмовостійкості починає з використання першого транспорту, і якщо він зазнає невдачі, він повторно пробує відправлення з наступним транспортом, поки один з них не призведе до успіху (або поки вони всі не зазнають невдачі).

Балансування навантаження

Mailer Symfony підтримує балансування навантаження через техніку під назвою "round-robin" для розподілу робочого навантаження відправлення листів на декілька транспортів.

Транспорт round-robin сконфігуровано з двома або більше транспортами та ключовим словом roundrobin:

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

Транспорт round-robin починає з рандомно обраного транспорту, а потім переключається на наступний доступний трансфер для кожного наступного листа.

Як і з транспортом failover, round-robin повторно намагається зробити відправлення, поки транспорт не досягне успіху (або поки всі не зазнають невдачі). На відміну від транспорту failover, він розповсюджує навантаження по всім своїм транспортам.

Верифікація точок TLS

За замовчуванням, транспорт SMTP виконує верифікацію точок TLS. Ця поведінка конфігурується опцією verify_peer. Хоча і не рекомендується відключати верифікацію з міркувань безпеки, це може бути корисним при розробці додатку або при використанні самозавіреного сертифікату:

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

Інші опції

command

Команда для виконання транспортом sendmail:

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

Ім'я домену для використання в команді HELO:

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

Максимальна кількість повідомлень для відправки до перезавантаження транспорту. Може бути використана разом з restart_threshold_sleep:

1
$dsn = 'smtps://smtp.example.com?restart_threshold=10&restart_threshold_sleep=1'
restart_threshold_sleep

Кількість секунд сну між зупинкою та перезавантаженням транспорту. Часто поєднується з restart_threshold:

1
$dsn = 'smtps://smtp.example.com?restart_threshold=10&restart_threshold_sleep=1'
ping_threshold

Мінімальна кількість секунд між двома повідомленнями, необхідна для пінгу серверу:

1
$dsn = 'smtps://smtp.example.com?ping_threshold=200'
max_per_second
Кількість повідомлень, які відправляються за секунду (0, щоб відключити це обмеження)

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

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

6.2

Опція max_per_second була представлена в Symfony 6.2.

Створення та відправлення повідомлень

Для відправлення листа, отримайте екземпляр Mailer, використовуючи підказку MailerInterface, та створіть об'єкт Email:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// src/Controller/MailerController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Routing\Annotation\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);

        // ...
    }
}

Ось і все! Повідомлення буде відправлено через транспорт, який ви сконфігурували. Якщо транспорт сконфігуровано, щоб відправляти листи асинхронно , повідомлення не буде насправді відправлено, поки робітник його не буде споживати .

Адреси електронної пошти

Всі методи, які потребують адрес електронної пошти (from(), to(), та ін.), приймають як рядки, так і об'єкти адрес:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ...
use Symfony\Component\Mime\Address;

$email = (new Email())
    // адреса пошти як звичайний рядок
    ->from('fabien@example.com')

    // адреса пошти як об'єкт
    ->from(new Address('fabien@example.com'))

    // визначення адреси та імені пошти як об'єкту
    // (поштові клієнти відобразять ім'я)
    ->from(new Address('fabien@example.com', 'Fabien'))

    // визначення адреси та імені пошти як рядку
    // (формат має відповідати: 'Ім'я <email@example.com>')
    ->from(Address::create('Fabien Potencier <fabien@example.com>'))

    // ...
;

Tip

Замість виклику ->from() кожний раз при створенні листа, ви можете сконфігурувати листи глобально , щоб встановити одну й ту саму From листа у всіх повідомленнях.

Note

Локальная частина адреси (те, що перед @) може включати в себе символи UTF-8, окрім адреси відправника (щоб уникнути помилок з поверненими листами). Наприклад: föóbàr@example.com, 用户@example.com, θσερ@example.com, і т.д.

Використовуйте методи addTo(), addCc(), або addBcc(), щоб додати більше адрес:

1
2
3
4
5
6
7
8
$email = (new Email())
    ->to('foo@example.com')
    ->addTo('bar@example.com')
    ->cc('cc@example.com')
    ->addCc('cc2@example.com')

    // ...
;

Як варіант, ви можете передати декілька адрес кожному методу:

1
2
3
4
5
6
7
8
$toAddresses = ['foo@example.com', new Address('bar@example.com')];

$email = (new Email())
    ->to(...$toAddresses)
    ->cc('cc1@example.com', 'cc2@example.com')

    // ...
;

Заголовки повідомлень

Повідомлення містять деяку кількість полів заголовків, щоб описати їх зміст. Symfony встановлює всі заголовки автоматично, але ви також можете встановити власні заголовки. Існують різні типи заголовків (заголовок Id, заголовок Mailbox, заголовок Date і т.д.), але у більшості випадків ви будете встановлювати текстові заголовки:

1
2
3
4
5
6
7
8
9
10
11
12
$email = (new Email())
    ->getHeaders()
        // цей заголовок повідомляє авто-відповідачам ("режим відпустки електронної пошти")
        // не відповідати на це повідомлення, так як це автоматизований лист
        ->addTextHeader('X-Auto-Response-Suppress', 'OOF, DR, RN, NRN, AutoReply');

        // використати масив, якщо ви хочете додати заголовок з багатьма значеннями
        // (for example in the "References" or "In-Reply-To" header)
        ->addIdHeader('References', ['123@example.com', '456@example.com'])

    // ...
;

Tip

Замість виклику ->addTextHeader() кожний раз при створенні листа, ви можете сконфігурувати листи глобально , щоб встановити одні й ті самі заголовки у всіх відправлених листах.

Зміст повідомлення

Текстовий та HTML-зміст повідомлень може бути рядками (зазвичай у результаті відображення якогось шаблону) або PHP-джерелами:

1
2
3
4
5
6
7
8
9
10
$email = (new Email())
    // ...
    // простий зміст, визначений як рядок
    ->text('Lorem ipsum...')
    ->html('<p>Lorem ipsum...</p>')

    // прикріпити потік файлів
    ->text(fopen('/path/to/emails/user_signup.txt', 'r'))
    ->html(fopen('/path/to/emails/user_signup.html', 'r'))
;

Tip

Ви також можете використовувати шаблони Twig, щоб відобразити зміст тексту та HTML. Прочитайте розділ Twig: HTML & CSS далі в цій статті, щоб дізнатися більше.

Прикріплення файлів

Використайте метод addPart() з BodyFile, щоб додати файли, які існують у вашій файловій системі:

1
2
3
4
5
6
7
8
9
10
11
12
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')))
    // опціонально ви можете вказати email-клієнтам відображати користувацьке імʼя для цього файлу
    ->addPart(new DataPart(new File('/path/to/documents/privacy.pdf'), 'Privacy Policy'))
    // опціонально ви можете надати чіткий MIME-тип (інакше його не буде вгадано)
    ->addPart(new DataPart(new File('/path/to/documents/contract.doc'), 'Contract', 'application/msword'))
;

Як варіант, ви можете прикріпляти зміст з потоку шляхом передачі його напряму DataPart :

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

6.2

У версіях Symfony до 6.2, методи attachFromPath() та attach() могли бути використані для додавання прикріплень. Ці методи застаріли та були замінені addPart().

Вбудовування зображень

Якщо ви хочете відобразити зображення всередині вашого листа, ви маєте їх вбудувати, а не додавати як вкладення. При використанні Twig для відображення змісту листа, як пояснюється далі в цій статті , зображення вбудовуються автоматично. В іншому випадку, вам потрібно буде вбудувати їх вручну.

Спочатку, використайте метод addPart(), щоб додати зображення з файлу або потоку:

1
2
3
4
5
6
7
$email = (new Email())
    // ...
    // отримати зміст збораження з PHP-джерела
    ->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())
;

Використайте метод asInline(), щоб вбудувати зміст замість того, щоб прикріплювати його.

Другий необов'язковий аргумент обох методів - це ім'я зображення ("Content-ID" за стандартом MIME). Його значення - це довільний рядок, який використовується для того, щоб посилатися на зображення всередині HTML змісту:

1
2
3
4
5
6
7
8
9
10
11
$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())

    // посилайтеся на зображення, використовуючи синтаксис 'cid:' + "image embed name"
    ->html('<img src="cid:logo"> ... <img src="cid:footer-signature"> ...')

    // використати такий самий синтаксис для зображень, доданих як фонові зображення HTML
    ->html('... <div background="cid:footer-signature"> ... </div> ...')
;

6.1

Підтримка вбудованих зображень як фонів HTML була представлена в Symfony 6.1.

6.2

У версіях Symfony до 6.2, методи embedFromPath() та embed() могли бути використані для вбудовування зображень. Ці методи застаріли та були замінені на addPart() разом з вбудованими обʼєктами DataPart.

Конфігурація листів глобально

Замість виклику ->from() по кожному листу, який ви створюєте, ви можете сокнфігурувати це значення глобально, щоб воно було встановлене у всіх відправлених листах. Те ж саме вірно і для ->to(), і для заголовків.

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
# config/packages/dev/mailer.yaml
framework:
    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

Деякі сторонні постачальники не підтримують використання ключових слів типу from у headers. Перегляньте документацію вашого провайдера до встановлення будь-якого глобального заголовку.

Обробка помилок відправлення

Symfony Mailer вважає відправку успішною, коли ваш транспорт (SMTP-сервер або сторонній постачальник) приймає лист для подальшої доставки. Повідомлення може бути втрачене або не доставлене пізніше через проблеми у вашому постачальнику, але це виходить за межі можливостей вашого додатку Symfony.

Якщо при передачі листа вашому транспорту сталася помилка, Symfony викликає TransportExceptionInterface. Виявіть виключення, щоб відновитися після помилки або для відображення якогось повідомлення:

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;

$email = new Email();
// ...
try {
    $mailer->send($email);
} catch (TransportExceptionInterface $e) {
    // якась помилка завадила відправленню листа; відобразити повідомлення
    // про помилку або спробувати відправити повідомлення знову
}

Налагодження листів

Об'єкт SentMessage, повернений методом send() класу TransportInterface, надає доступ до початкового повідомлення (getOriginalMessage()) і до деякої інформації налагодження (getDebug()) на кшталт HTTP-викликів, зроблених HTTP-транспортом, що корисно для налагодження помилок.

Note

Деякі постачальники поштових програм змінюють Message-Id при відправленні листа. Метод getMessageId() з SentMessage завжди повертає визначений ID повідомлення (той самий рандомний ID, згенерований Symfony, або новий ID, згенерований постачальником поштової програми).

Виключення, пов'язані з транспортом поштової програми (які реалізують TransportException) також надають цю інформацію налагодження через метод getDebug().

Twig: HTML і CSS

Компонент Mime інтегрується з шаблонізатором Twig , щоб надати просунуті функції на кшталт вбудовування CSS-стилів та підтримки для фреймворків HTML/CSS, щоб створювати складні повідомлення HTML-листів. Спочатку переконайтеся, що Twig встановлено:

1
2
3
4
$ composer require symfony/twig-bundle

# або, якщо ви використовуєте компонент у додатку не на Symfony:
# composer require symfony/twig-bridge

HTML-зміст

Щоб визначити зміст вашого листа з Twig, використайте клас TemplatedEmail. Цей клас розширює нормальний клас Email, але додає деякі нові методі для шаблонів Twig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Bridge\Twig\Mime\TemplatedEmail;

$email = (new TemplatedEmail())
    ->from('fabien@example.com')
    ->to(new Address('ryan@example.com'))
    ->subject('Thanks for signing up!')

    // шлях шаблону Twig для відображення
    ->htmlTemplate('emails/signup.html.twig')

    // передайте змінні (ім'я => значення) шаблону
    ->context([
        'expiration_date' => new \DateTime('+7 days'),
        'username' => 'foo',
    ])
;

Потім створіть шаблон:

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>

Шаблон Twig має доступ до будь-яких параметрів, які передано в метод context() класу TemplatedEmail, а також спеціальної змінної під назвою email, яка є екземпляром WrappedTemplatedEmail.

Текстовий зміст

Коли текстовий зміст TemplatedEmail не визначено чітко, поштова програма автоматично згенерує його, перетворивши HTML-зміст у текст. Якщо у вас у додатку встановлено league/html-to-markdown, він використовує це для перетворення HTML у розмітку (щоб текстовий лист мав візуально привабливий вигляд). В іншому випадку, він застосовує PHP-функцію strip_tags до початкового HTML-змісту.

Якщо ви хочете визначити текстовий зміст самостійно, використайте метод text(), пояснений у попередніх розділах, або метод textTemplate(), наданий класом TemplatedEmail:

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')
    // ...
;

Вбудовування зображень

Замість того, щоб розбиратися з синтаксисом <img src="cid: ...">, який пояснювався у попердніх розділах, при використанні Twig для відображення змісту листа, ви можете послатися на файли зображень, як звичайно. Для початку, щоб все спростити, визначіть простір іменґ Twig під назвою images, який вказує на той каталог, де зберігаються ваші зображення:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
# config/packages/twig.yaml
twig:
    # ...

    paths:
        # вкажіть туди, де живуть ваші зображення
        '%kernel.project_dir%/assets/images': images

Тепер використайте спеціального помічника Twig email.image(), щоб вбудувати зображення у зміст листа:

1
2
3
4
5
{# '@images/' відноситься до простору імен Twig, визначеного раніше #}
<img src="{{ email.image('@images/logo.png') }}" alt="Logo">

<h1>Welcome {{ email.toName }}!</h1>
{# ... #}

Вбудовування CSS-стилів

Дизайн HTML-змісту листа дуже відрізняється від дизайну звичайної HTML-сторінки. Перш за все, більшість поштових клієнтів підтримують лише частину всіх CSS-функцій. Більш того, популярні поштові клієнти, на кшталт Gmail, не підтримують визначальні стилі всередині розділів <style> ... </style>, і ви маєте вбудувати всі CSS-стилі.

Вбудовування CSS означає, що кожний HTML-тег має визначати атрибут style, з усіма його CSS-стилями. Це може сильно заплутати впорядкування вашого CSS. Тому Twig надає CssInlinerExtension, який автоматизує все для вас. Встановіть його:

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

Розширення вмикається автоматично. Щоб використати його, огорніть весь шаблон у фільтр inline_css:

1
2
3
4
5
6
7
8
9
10
11
{% apply inline_css %}
    <style>
        {# тут визначте ваші СSS-стилі, як звичайно #}
        h1 {
            color: #333;
        }
    </style>

    <h1>Welcome {{ email.toName }}!</h1>
    {# ... #}
{% endapply %}

Використання зовнішніх СSS-файлів

Ви також можете визначати СSS-стилі у зовнішніх файлах та передавати їх як аргументи фільтру:

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

Ви можете передати необмежену кількість аргументів до inline_css() для завантаження декількох CSS-файлів. Для того, щоб цей приклад працював, вам також треба визначити новий простів імен Twig під назвою styles, який вказує на каталог, де живе email.css:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
# config/packages/twig.yaml
twig:
    # ...

    paths:
        # вкажіть туди, де живуть ваші css-файли
        '%kernel.project_dir%/assets/styles': styles

Відображення змісту з розміткою

Twig надає інше розширення під назвою MarkdownExtension, яке дозволяє вам визначати зміст листів з використанням синтаксису Markdown. Щоб використати його, встановіть розширення та бібліотеку конверсій Markdown (розширення сумісне з декількома популярними бібліотеками):

1
2
# замість league/commonmark, ви також можете використати erusev/parsedown або michelf/php-markdown
$ composer require twig/extra-bundle twig/markdown-extra league/commonmark

Розширення додає фільтр markdown_to_html, який ви можете використати, щоб перетворити частини або весь змісти листа з Markdown в HTML:

1
2
3
4
5
6
7
8
9
{% apply markdown_to_html %}
    Welcome {{ email.toName }}!
    ===========================

    Ви підписалися на наш сайт, використовуючи наступну адресу електронної пошти:
    `{{ email.to[0].address }}`

    [Натисніть тут, щоб активувати ваш акаунт]({{ url('...') }})
{% endapply %}

Мова шаблонізації листів Inky

Створення листів з чудовим дизайном, які працюють у кожному поштовому клієнті, настільки складне, що існують цілі фреймворки HTML/CSS, присвячені цьому. Один з найпопулярніших фреймворків називається Inky. Він визначає синтаксис, засновуючись на тегах типу HTML, які пізніше перетворюються у справжній HTML-код та відправляються користувачас:

1
2
3
4
5
6
<!-- спрощений приклад синтаксису Inky -->
<container>
    <row>
        <columns>This is a column.</columns>
    </row>
</container>

Twig надає інтеграцію з Inky через InkyExtension. Спочатку, встановіть розширення у вашому додатку:

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

Розширення додає фільтр inky_to_html, який може бути використаний для перетворення частин або всього змісту листа з Inky в 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 %}

Ви можете скомбінувати всі фільтри, щоб створювати складні повідомлення листів:

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

Це використовує простір імен стилів Twig , який ми створили раніше. Ви могли б, наприклад, завантажити файл foundation-emails.css прямо з GitHub і зберегти його в assets/styles.

Цифровий підпис та кодування повідомлень

Існує можливість цифрового підписання та/або кодування повідомлень електронних листів для посилення їхньої цілісності/безпеки. Обидві опції можна об'єднати для кодування повідомлення з електронним підписом та/або цифрофого підписання закодованого повідомлення.

Перед тим, як підписувати/закодовувати повідомлення, переконайтеся, що у вас є:

Tip

При використанні OpenSSL для генерування сертифікатів, не забудьте додати опцію команди -addtrust emailProtection.

Цифровий підпис повідомлень

При додаванні цифрового підпису до повідомлення, генерується криптографічний хеш для всього змісту повідомелння (включно зі вкладеннями). Цей хеш додається як вкладення, щоб отримувач міг валідувати цілісність отриманого повідомлення. Однак, зміст початкового повідомлення залишається читаним для поштових агентів, які не підтримують підписані повідомлення, тому ви також маєте закодувати повідомлення, якщо ви хочете приховати його зміст.

Ви можете підписувати повідомлення використовуючи S/MIME або DKIM. В обох випадках, сертифікат та приватний ключ мають бути PEM-закодовані, і можут бути або створені з використанням, наприклад, OpenSSL, або отримані в офіційной Сертифікаційної компанії (CA). Отримувач листа повинен мати сертифікат CA у списку довірених осіб, щоб верифікувати підпис.

Підпис S/MIME

S/MIME стандартна для кодування публічних ключів та підписання даних MIME. Вона вимагає використання як сертифікату, так і приватного ключа:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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');
// якщо приватний ключ має кодову фразу, передайте її як третій аргумент
// new SMimeSigner('/path/to/certificate.crt', '/path/to/certificate-private-key.key', 'the-passphrase');

$signedEmail = $signer->sign($email);
// тепер використайте компонент Mailer, щоб відправити цей $signedEmail замість початкового листа

Tip

Клас SMimeSigner визначає інші необов'язкові аргументи для передачі проміжкових сертифікатів та для конфігурації процесу підписання, використовуючи побітні опції оператору для PHP-функції openssl_pkcs7_sign.

Підпис DKIM

DKIM - це метод аутентифікації листа, який додає цифровий підпис, що посилається на основний домен, до кожного вихідного листа. Він вимагає приватний ключ, але не сертифікат:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use Symfony\Component\Mime\Crypto\DkimSigner;
use Symfony\Component\Mime\Email;

$email = (new Email())
    ->from('hello@example.com')
    // ...
    ->html('...');

// перший аргумент: такий же, як openssl_pkey_get_private(), або рядок зі змістом
// приватного ключа, або абсолютний шлях до нього (з префіксом 'file://')
// другий і третій аргументи: ім'я домену та "селектор", використовувания для виконання пошуку DNS
// (селектор - це рядок, використовуваний для вказання на конкретний запис публічного ключа DKIM у вашій DNS)
$signer = new DkimSigner('file:///path/to/private-key.key', 'example.com', 'sf');
// якщо приватний ключ має кодову фразу, передайте її як п'ятий аргумент
// new DkimSigner('file:///path/to/private-key.key', 'example.com', 'sf', [], 'the-passphrase');

$signedEmail = $signer->sign($email);
// тепер використайте компонент Mailer, щоб відправити цей $signedEmail замість початкового листа

// підпис DKIM надає багато опцій конфігурації та об'єкт помічника для їхнього конфігурування
use Symfony\Component\Mime\Crypto\DkimOptions;

$signedEmail = $signer->sign($email, (new DkimOptions())
    ->bodyCanon('relaxed')
    ->headerCanon('relaxed')
    ->headersToIgnore(['Message-ID'])
    ->toArray()
);

Кодування повідомлень

При кодуванні повідомлення, все повідомлення (включно із вкладеннями) кодується з використанням сертифікату. Отже, тільки отримувачі, які мають відповідний приватний ключ, можуть прочитати зміст початкового повідомлення:

1
2
3
4
5
6
7
8
9
10
11
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);
// тепер використайте компонент Mailer, щоб відправити цей $encryptedEmail замість початкового листа

Ви можете передати більше одного сертифікату конструктору SMimeEncrypter і він обере відповідний сертифікат, в залежності від опції To:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$firstEmail = (new Email())
    // ...
    ->to('jane@example.com');

$secondEmail = (new Email())
    // ...
    ->to('john@example.com');

// другий необов'язковий аргумент SMimeEncrypter визначає, який алгоритм кодування використовується
// (має бути однією з цих констант: https://www.php.net/manual/en/openssl.ciphers.php)
$encrypter = new SMimeEncrypter([
    // ключ = отримувач листа; значення = шлях до файлу сертифікату
    'jane@example.com' => '/path/to/first-certificate.crt',
    'john@example.com' => '/path/to/second-certificate.crt',
]);

$firstEncryptedEmail = $encrypter->encrypt($firstEmail);
$secondEncryptedEmail = $encrypter->encrypt($secondEmail);

Декілька транспортів листів

Ви можете захотіти використати більше одного транспорту поштової програми для доставки ваших повідомлень. Це можна сконфігурувати, замінивши запис конфігурації dsn на запис transports, наступним чином:

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

За замовчуванням використовується перший транспорт. Інші транспорти можуть бути використані шляхом додавання до листа текстового заголовку X-Transport:

1
2
3
4
5
6
// Відправити, використовуючи перший "головний" транспорт ...
$mailer->send($email);

// ... або використати "альтернативний"
$email->getHeaders()->addTextHeader('X-Transport', 'alternative');
$mailer->send($email);

Асинхронне відправлення повідомлень

Коли ви викликаєте $mailer->send($email), лист одразу ж відправляється транспорту. Для покращення продуктивності, ви можете використати переваги Месенджеру, щоб відправляти повідомлення пізніше через транспорт Месенджера.

Почніть, слідуючи документації Месенджеру та сконфігурувавши транспорт. Коли все буде налаштовано, при виклику $mailer->send(), повідомлення SendEmailMessage буде запущене через автобус повідомлень за замовчуванням (messenger.default_bus). Якщо припустити, що у вас є транспорт під назвою async, ви можете маршрутизувати повідомлення у нього:

  • YAML
  • XML
  • PHP
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

Завдяки цьому, замість негайної доставки, повідомлення будуть відправлені транспорту для обробки пізніше (див. ). Відмітьте, що "відображення" листа (обчислені заголовки, відображення тіла, ...) також відкладено і відбудеться прямо перед відпарвкою листа обробником Messenger.

6.2

Наступний приклад пор відображення листа перед викликом $mailer->send($email) працює починаючи з Symfony 6.2.

При відправленні листа асинхронно, його езкемпляр повинен бути серіалізовуваним. Це завжди так для екземплярів Email, але при відправці TemplatedEmail, ви повинні гарантувати, що context серіалізовуваний. Якщо у вас є несеріалізовувані змінні, типу сутностей Doctrine, або замініть їх на більш конкретні змінні, або відобразіть лист перед викликом $mailer->send($email):

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\BodyRendererInterface;

public function action(MailerInterface $mailer, BodyRendererInterface $bodyRenderer)
{
    $email = (new TemplatedEmail())
        ->htmlTemplate($template)
        ->context($context)
    ;
    $bodyRenderer->render($email);

    $mailer->send($email);
}

Ви можете сконфігурувати, який автобус використовується для запуску повідомлення, використовуючи опцію message_bus. Ви також можете встановити її як false, щоб викликати транспорт Mailer напряму та відключити асинхронну доставку.

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

Note

У випадку довготривалих скриптів і коли Mailer використовує SmtpTransport, ви можете відключитися від SMTP-сервера, щоб уникнути відкритого зʼєднання з SMTP-сервером між відправкою листів. Ви можете зробити це, використавши метод stop().

6.1

Метод stop() було оприлюднено в Symfony 6.1.

Додавання тегів та метаданих до листів

Певні сторонні транспорти підтримують теги та метадані листів, що може бути використано для групування, відслідковування і робочих процесів. Ви можете додати їх, використовуючи класи TagHeader та MetadataHeader. Якщо ваш транспорт підтримує заголовки, він перетворить їх у відповідний формат:

1
2
3
4
5
6
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'));

Якщо ваш транспорт не підтримує теги та метадані, вони будуть додані у вигляді користувацьких заголовків:

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

На даний момент, наступний транспорт підтримує теги та метадані:

  • MailChimp
  • Mailgun
  • Postmark
  • Sendgrid
  • Sendinblue

Наступний транспорт підтримує лише теги:

  • MailPace

Наступний трнаспорт підтримує лише метадані:

  • Amazon SES (відмітьте, що Amazon називає цю функцію "тегами", але Symfony називає це "метаданими" через те, що вони містять ключ та значення)

6.1

Підтримка метаданих для Amazon SES була представлена в Symfony 6.1.

Чернетки електронних листів

6.1

Symfony\Component\Mime\DraftEmail було представлено в 6.1.

DraftEmail - це сеціальний екземпляр Email. Його ціль полягає в тому, щоб побудувати електронний лист (з тілом, вкладеннями та ін.) та зробити його доступним для завантаження як .eml із заголовком X-Unsent. Багато email-клієнтів можуть відкривати ці файли та взаємодіяти з ними, як з "чернетками електронного листа". Ви можете використати їх, щоб створювати просунуті посилання mailto:.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// src/Controller/DownloadEmailController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Mime\DraftEmail;
use Symfony\Component\Routing\Annotation\Route;

class DownloadEmailController extends AbstractController
{
    #[Route('/download-email')]
    public function __invoke(): Response
    {
        $message = (new DraftEmail())
            ->html($this->renderView(/* ... */))
            ->attach(/* ... */)
        ;

        $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

Так як DraftEmail можуть бути створені без Кому/Від, вони не можуть бути відправлені без Mailer.

Події Mailer

MessageEvent

Клас події: MessageEvent

MessageEvent дозволяє змінювати повідомлення Mailer та конверт перед відправкою листа:

1
2
3
4
5
6
7
8
9
10
11
12
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;
    }
    // зробити щось з повідомленням
}

Tip

При використанні слухача MessageEvent для підписання змісту електронних листів , виконайте його якомога пізніше (наприклад, встановивши для нього відʼємний пріоритет), щоб зміст листа не було встановлено або змінено після його підписання.

Виконайте цю команду, щоб дізнатися, які слухачі зареєстровані для цієї події, та їх пріоритети:

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

QueuingMessageEvent

Клас події: QueuingMessageEvent

6.2

Клас QueuingMessageEvent було представлено в Symfony 6.2.

QueuingMessageEvent дозволяє додавати деяку логіку перед відправкою листа до автобусу Messenger (ця подія не оголошується, якщо не сконфігуровано жодного автобуса); вона розширює MessageEvent, щоб дозволити додавання штампів Messenger до повідомлення Messenger, відправленого автобусу:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\Event\QueuingMessageEvent;
use Symfony\Component\Mime\Email;

public function onMessage(QueuingMessageEvent $event): void
{
    $message = $event->getMessage();
    if (!$message instanceof Email) {
        return;
    }
    // зробити щось з повідомленням (логування, ...)

    // та/або додати якісь штампи Messenger
    $event->addStamp(new SomeMessengerStamp());
}

Ця подія дозволяє слухачам робити щось до того, як повідомлення буде відправлено у чергу (на кшталт додавання штампів або ведення логів), але будь-які зміни у повідомленні або конверті відкидаються. Щоб змінити повідомлення або конверт, натомість слухайте MessageEvent.

Виконайте цю команду, щоб дізнатися, які слухачі зареєстровані для цієї події, та їх пріоритети:

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

SentMessageEvent

Клас події: SentMessageEvent

6.2

Подія SentMessageEvent була представлена в Symfony 6.2.

SentMessageEvent дозволяє вам діяти у класі SentMessage, щоб отримати доступ до початкового повідомлення (getOriginalMessage()) та деякої інформації налагодження (getDebug()), такої як HTTP-виклики, зроблені транспортом HTTP, що корисно для налагодження помилок:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
    }

    // зробити щось з повідомленням
}

Виконайте цю команду, щоб дізнатися, які слухачі зареєстровані для цієї події, та їх пріоритети:

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

FailedMessageEvent

Клас події: FailedMessageEvent

6.2

Подія FailedMessageEvent була представлена в Symfony 6.2.

FailedMessageEvent дозволяє діяти у початковому повідомленні у випадку невдачі:

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\Event\FailedMessageEvent;

public function onMessage(FailedMessageEvent $event): void
{
    // наприклад, ви можете отримати більше інформації про цю помилку при відправці листа
    $event->getError();

    // зробити щось з повідомленням
}

Виконайте цю команду, щоб дізнатися, які слухачі зареєстровані для цієї події, та їх пріоритети:

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

Розробка та налагодження

Відправка тестових електронних листів

Symfony надає команду для відправки листів, яка корисна під час розробки, щоб тестувати, чи правильно працює відправка електронних листів:

1
2
3
# єдиний обовʼязковий аргумент - це адреса отримувача
# (перегляньте допомогу команди, щоб дізнатися про її опції)
$ php bin/console mailer:test someone@example.com

Ця команда обходить автобус Messenger, якщо його сконфігуровано, щоб полегшити тестування листів, навіть якщо споживач Messenger не працює.

6.2

Команда mailer:test була представлена в Symfony 6.2.

Відключення доставки

Під час розробки (або тестування), ви можете захотіти відключити доставку повідомлень повністю. Ви можете зробити це, використовуючи null://null як DSN поштової програми, або у ваших файлах конфігурації .env , або у файлі конфігурації поштової програми (наприклад, у середовищах dev або test):

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

Note

Якщо ви використовуєете Месенджер та маршрутизуєте його до транспорту, повідомлення всеодно буде відправлене цьому транспорту.

Постійна відправка листа за однією адресою

Замість повного відключення доставки, ви можете захотіти завжди відправляти листи за однією конкретною адресою, замість справжньої адреси:

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

Напишіть функціональний тест

Щоб функціонально протестувати відправку електронного листа, і навіть ствердити зміст або заголовки листа, ви можете використати вбудовані ствердження:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// tests/Controller/MailControllerTest.php
namespace App\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\MailerAssertionsTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class MailControllerTest extends WebTestCase
{
    use MailerAssertionsTrait;

    public function testMailIsSentAndContentIsOk()
    {
        $client = $this->createClient();
        $client->request('GET', '/mail/send');
        $this->assertResponseIsSuccessful();

        $this->assertEmailCount(1);

        $email = $this->getMailerMessage();

        $this->assertEmailHtmlBodyContains($email, 'Welcome');
        $this->assertEmailTextBodyContains($email, 'Welcome');
    }
}