Документация / Symfony

Отправка email в Symfony через SMTP

Symfony Mailer описывает весь транспорт одной переменной MAILER_DSN — без конфигурационных классов и отдельных параметров. Ниже — DSN для fmailer, письма на шаблонах Twig, вложения и асинхронная отправка через Messenger.

Логин и пароль можно получить на странице управления токенами домена.

Параметры подключения

ПараметрЗначение
SMTP-хостsmtp.fmailer.ru
Порт587
ШифрованиеSTARTTLS (соединение переводится в TLS до авторизации)
ЛогинЛогин SMTP-токена вашего домена
ПарольПароль этого токена — показывается один раз, при создании
Каждый режим доступен и на высоком порту: 8465 (SSL/TLS), 8587 (STARTTLS) и 8025 (без шифрования). Многие хостинги и облака блокируют исходящие 25, 465 и 587 — если соединение отваливается по таймауту, используйте соответствующий высокий порт

Установка

bash
composer require symfony/mailer
composer require symfony/twig-bundle   # для TemplatedEmail

Настройка MAILER_DSN

.env.localproperties
# .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
DSN — это URL, поэтому @ в логине токена записывается как %40: иначе разбор обрывается не в том месте и Symfony сообщает о хосте, которого вы не указывали. То же с :, /, #, ? и & в пароле.

Отправителя по умолчанию удобно задать один раз:

config/packages/mailer.yamlproperties
# config/packages/mailer.yaml — один отправитель по умолчанию для всех писем
framework:
  mailer:
    envelope:
      sender: 'noreply@mail.example.ru'
    headers:
      From: 'Магазин <noreply@mail.example.ru>'

Первое письмо

Внедрите MailerInterface и отправьте. Заполненные text() и html() дают multipart/alternative:

php
<?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);
    }
}

Письмо на шаблоне Twig

TemplatedEmail собирает тело из шаблонов Twig, поэтому вёрстка писем лежит рядом с остальными шаблонами, а не внутри контроллера:

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

Вложения и картинки в теле письма

php
$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());

Асинхронная отправка через Messenger

По умолчанию $mailer->send() обращается к SMTP прямо в запросе. Маршрутизация сообщения мейлера в асинхронный транспорт переносит доставку в воркер, не меняя ни строчки вызывающего кода:

config/packages/messenger.yamlproperties
# config/packages/messenger.yaml
framework:
  messenger:
    transports:
      async: '%env(MESSENGER_TRANSPORT_DSN)%'
    routing:
      # Теперь письма уходят через очередь, а не внутри запроса.
      Symfony\Component\Mailer\Messenger\SendEmailMessage: async
bash
php bin/console messenger:consume async -vv
Как только SendEmailMessage направлен в транспорт, ничего не доставляется, пока не работает messenger:consume. Забытый воркер в продакшене — обычная причина того, что почта пропала сразу после включения асинхронной отправки.

Проверка без реальной отправки

properties
# .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Домен отправителя не подтверждён.

Частые вопросы

Как записать MAILER_DSN, если в логине есть символ @?

Его нужно закодировать: @ записывается как %40, то есть token-login@mail.example.ru превращается в token-login%40mail.example.ru. То же касается любых спецсимволов в пароле — :, /, ?, # и & тоже кодируются, иначе DSN разбирается неправильно и ошибка выглядит совершенно не связанной с причиной.

Чем отличаются схемы smtp и smtps?

smtp:// открывает обычное соединение и поднимает его через STARTTLS — это порт 587. smtps:// открывает уже зашифрованное соединение — порт 465. Symfony выбирает поведение по схеме, поэтому отдельной настройки шифрования нет.

Как отправлять письма асинхронно в Symfony?

Направьте Symfony\Component\Mailer\Messenger\SendEmailMessage в асинхронный транспорт в messenger.yaml и запустите воркер командой messenger:consume. Вызывающий код при этом не меняется: $mailer->send() начинает класть сообщение в шину вместо прямого обращения к SMTP.

Почему после включения Messenger письма перестали уходить?

Если маршрутизация настроена, а воркер не запущен, сообщения просто копятся в очереди. Проверьте, что messenger:consume работает под супервизором, и посмотрите транспорт failed на сообщения, которые повторялись и были отброшены.

Как посмотреть письмо, не отправляя его?

Поставьте MAILER_DSN=null://null и откройте вкладку Email в профайлере Symfony: там видно отрисованный HTML, заголовки и получателей каждого письма, созданного в запросе. В тестах по тем же собранным сообщениям работают assertEmailCount и остальные assertEmail*.

Symfony Mailer или PHPMailer?

Symfony Mailer, если вы на Symfony: он интегрирован с Twig, Messenger, профайлером и тестовыми утверждениями. PHPMailer лучше подходит для отдельных скриптов и старого кода без фреймворка, где важнее одна небольшая зависимость, чем интеграция.

Что дальше