Symfony Mailer описывает весь транспорт одной переменной MAILER_DSN — без конфигурационных классов и отдельных параметров. Ниже — DSN для fmailer, письма на шаблонах Twig, вложения и асинхронная отправка через Messenger.
| Параметр | Значение |
|---|---|
| SMTP-хост | smtp.fmailer.ru |
| Порт | 587 |
| Шифрование | STARTTLS (соединение переводится в TLS до авторизации) |
| Логин | Логин SMTP-токена вашего домена |
| Пароль | Пароль этого токена — показывается один раз, при создании |
composer require symfony/mailer
composer require symfony/twig-bundle # для TemplatedEmail# .env.local — весь транспорт описывается одной строкой DSN
MAILER_DSN=smtp://token-login%40mail.example.ru:token-password@smtp.fmailer.ru:587
# Порт 465 (шифрование сразу) — другая схема
# MAILER_DSN=smtps://token-login%40mail.example.ru:token-password@smtp.fmailer.ru:465@ в логине токена записывается как %40: иначе разбор обрывается не в том месте и Symfony сообщает о хосте, которого вы не указывали. То же с :, /, #, ? и & в пароле. Отправителя по умолчанию удобно задать один раз:
# config/packages/mailer.yaml — один отправитель по умолчанию для всех писем
framework:
mailer:
envelope:
sender: 'noreply@mail.example.ru'
headers:
From: 'Магазин <noreply@mail.example.ru>' Внедрите MailerInterface и отправьте. Заполненные text() и html() дают multipart/alternative:
<?php
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
class OrderController extends AbstractController
{
#[Route('/orders', methods: ['POST'])]
public function create(MailerInterface $mailer): Response
{
$email = (new Email())
->from('Магазин <noreply@mail.example.ru>')
->to('client@example.ru')
->subject('Подтверждение заказа №4417')
->text('Заказ принят. Доставка — завтра до 18:00.')
->html('<h1>Заказ принят</h1><p>Доставка — завтра до 18:00.</p>');
$mailer->send($email);
return $this->json(['ok' => true], 201);
}
}TemplatedEmail собирает тело из шаблонов Twig, поэтому вёрстка писем лежит рядом с остальными шаблонами, а не внутри контроллера:
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
$email = (new TemplatedEmail())
->from(new Address('noreply@mail.example.ru', 'Магазин'))
->to(new Address($order->getCustomerEmail(), $order->getCustomerName()))
->subject(sprintf('Подтверждение заказа №%d', $order->getId()))
->htmlTemplate('emails/order_confirmed.html.twig')
->textTemplate('emails/order_confirmed.txt.twig')
->context(['order' => $order]);
$mailer->send($email);$email
->addPart(new DataPart(new File('/srv/invoices/2026-03.pdf'), 'invoice.pdf'))
// Картинка в теле письма — в Twig как {{ email.image('@images/logo.png') }}
->addPart((new DataPart(new File('assets/logo.png'), 'logo'))->asInline()); По умолчанию $mailer->send() обращается к SMTP прямо в запросе. Маршрутизация сообщения мейлера в асинхронный транспорт переносит доставку в воркер, не меняя ни строчки вызывающего кода:
# config/packages/messenger.yaml
framework:
messenger:
transports:
async: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
# Теперь письма уходят через очередь, а не внутри запроса.
Symfony\Component\Mailer\Messenger\SendEmailMessage: asyncphp bin/console messenger:consume async -vvSendEmailMessage направлен в транспорт, ничего не доставляется, пока не работает messenger:consume. Забытый воркер в продакшене — обычная причина того, что почта пропала сразу после включения асинхронной отправки. # .env.test / .env.dev — собирать письма вместо отправки
MAILER_DSN=null://null| Ошибка | Причина и решение |
|---|---|
The "…" mailer DSN is invalid | Незакодированный @ или спецсимвол в доступах. |
TransportException: Connection could not be established | Порт заблокирован. Используйте 8587 (smtp) или 8465 (smtps). |
Expected response code 250 but got 535 | Неверные логин или пароль в DSN. |
| После включения Messenger письма не уходят | Не запущен messenger:consume async. |
Письма зависли в messenger_messages | Воркер упал или неверно настроен транспорт — посмотрите messenger:failed:show. |
| Код ответа 550 | Домен отправителя не подтверждён. |
Его нужно закодировать: @ записывается как %40, то есть token-login@mail.example.ru превращается в token-login%40mail.example.ru. То же касается любых спецсимволов в пароле — :, /, ?, # и & тоже кодируются, иначе DSN разбирается неправильно и ошибка выглядит совершенно не связанной с причиной.
smtp:// открывает обычное соединение и поднимает его через STARTTLS — это порт 587. smtps:// открывает уже зашифрованное соединение — порт 465. Symfony выбирает поведение по схеме, поэтому отдельной настройки шифрования нет.
Направьте Symfony\Component\Mailer\Messenger\SendEmailMessage в асинхронный транспорт в messenger.yaml и запустите воркер командой messenger:consume. Вызывающий код при этом не меняется: $mailer->send() начинает класть сообщение в шину вместо прямого обращения к SMTP.
Если маршрутизация настроена, а воркер не запущен, сообщения просто копятся в очереди. Проверьте, что messenger:consume работает под супервизором, и посмотрите транспорт failed на сообщения, которые повторялись и были отброшены.
Поставьте MAILER_DSN=null://null и откройте вкладку Email в профайлере Symfony: там видно отрисованный HTML, заголовки и получателей каждого письма, созданного в запросе. В тестах по тем же собранным сообщениям работают assertEmailCount и остальные assertEmail*.
Symfony Mailer, если вы на Symfony: он интегрирован с Twig, Messenger, профайлером и тестовыми утверждениями. PHPMailer лучше подходит для отдельных скриптов и старого кода без фреймворка, где важнее одна небольшая зависимость, чем интеграция.