Почта в Laravel: разбор отправки на живом проекте
Как устроена отправка писем в Laravel 11: mailable-классы, очереди, драйверы и типичные места, где письма молча теряются или блокируют HTTP-запрос.

Пользователь регистрируется, форма отправляется, а письмо с подтверждением приходит через полторы минуты — потому что SMTP-сервер тормозит, а вы отправляете письмо прямо в контроллере, синхронно. Пользователь всё это время смотрит на крутящийся спиннер. Разберём, как в Laravel 11 устроена отправка почты на самом деле: где формируется письмо, где оно уходит в сеть, и почему очередь — не опциональная оптимизация, а условие того, чтобы приложение не висело на чужом SMTP.
Из чего состоит отправка письма
В Laravel письмо — это отдельный класс, а не набор параметров в вызове хелпера. Такой класс называют mailable. Он описывает три вещи: кому и от кого письмо (envelope), что внутри (content — какой шаблон и какие данные), и вложения (attachments). Само письмо в сеть отправляет фасад Mail через выбранный драйвер (mailer).
Смысл разделения в том, что mailable не знает, как именно письмо уйдёт — через SMTP, через API Postmark или в лог. За это отвечает конфигурация config/mail.php и переменные окружения. Один и тот же mailable в тестах пишется в лог, а в проде уходит через реальный SMTP.
Минимальный рабочий mailable
Создаётся командой php artisan make:mail OrderShipped. Вот как выглядит класс для письма о том, что заказ отправлен (Laravel 11.x):
<?php
namespace App\Mail;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class OrderShipped extends Mailable
{
use Queueable, SerializesModels;
// Модель прилетает в конструктор и доступна в шаблоне
public function __construct(public Order $order)
{
}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Заказ №' . $this->order->id . ' отправлен',
);
}
public function content(): Content
{
// Шаблон лежит в resources/views/emails/orders/shipped.blade.php
return new Content(
markdown: 'emails.orders.shipped',
with: [
'trackingUrl' => $this->order->trackingUrl(),
],
);
}
}
Обратите внимание на markdown: вместо view:. Markdown-шаблоны используют готовые компоненты Laravel — кнопку, панель, таблицу — и дают адаптивную HTML-вёрстку без ручной возни с таблицами и inline-стилями. Если нужен полный контроль над HTML, используйте view: с обычным Blade-шаблоном.
Синхронная отправка против очереди
Вот два способа отправить это письмо. Разница между ними — вся статья.
use App\Mail\OrderShipped;
use Illuminate\Support\Facades\Mail;
// Синхронно: HTTP-запрос ждёт, пока SMTP примет письмо
Mail::to($order->customer->email)->send(new OrderShipped($order));
// В очередь: письмо ставится в очередь, ответ уходит пользователю сразу
Mail::to($order->customer->email)->queue(new OrderShipped($order));
При send() Laravel открывает соединение с почтовым сервером и ждёт ответа прямо внутри обработки запроса. Если SMTP-сервер отвечает за 800 мс, пользователь ждёт эти 800 мс поверх всей остальной логики. Если сервер недоступен — запрос падает с исключением, и пользователь видит 500, хотя заказ уже создан.
При queue() письмо сериализуется и кладётся в очередь. HTTP-запрос завершается мгновенно, а отправкой занимается фоновый воркер. Для этого mailable подключает трейт Queueable (он уже есть в сгенерированном классе) и настроена очередь.
Правило простое: любое письмо, отправка которого происходит в ответ на действие пользователя, должно уходить в очередь. Синхронная отправка допустима только в консольных командах и джобах, где никто не ждёт HTTP-ответа.
Почему send() внутри контроллера — частая ошибка
Проблема не только в скорости. Синхронная отправка связывает время ответа вашего приложения с доступностью чужого сервера. Postmark, SES, ваш собственный Postfix — любой из них может тормозить или отвечать таймаутом. С send() эта задержка становится задержкой ответа пользователю. С очередью — задачей воркера, который умеет ретраить.
Драйверы: чем отправлять на самом деле
Mailer выбирается переменной MAIL_MAILER в .env. От выбора зависит, куда физически уйдёт письмо и как быстро.
| Драйвер | Как отправляет | Когда уместен |
|---|---|---|
smtp | Прямое SMTP-соединение | Свой почтовый сервер, универсальный вариант. Чувствителен к таймаутам |
ses | API Amazon SES | Большие объёмы, инфраструктура в AWS |
postmark | API Postmark | Транзакционные письма, важна доставляемость |
log | Пишет письмо в лог | Локальная разработка — видно содержимое, ничего не улетает наружу |
array | Складывает в память | Тесты: Mail::fake() и проверка отправленного |
Для API-драйверов вроде ses и postmark нужны отдельные пакеты — Laravel подскажет, какой именно установить, при первой попытке отправки. Точный состав зависимостей сверяйте с разделом Mail официальной документации вашей версии: список менялся между релизами.
Что нужно, чтобы очередь заработала
Поставить письмо в очередь через queue() недостаточно — кто-то должен эту очередь разбирать. Иначе письма молча копятся и не уходят.
- Выберите драйвер очереди в
QUEUE_CONNECTION. Значениеsyncотправляет задачи немедленно и синхронно — то есть очередь не работает, всё исполняется в том же процессе. Для реальной фоновой обработки нуженdatabase,redisили другой. - Для драйвера
databaseсоздайте таблицу очереди:php artisan make:queue-table, затемphp artisan migrate. Имя команды и способ уточните в документации — в старых версиях команда называлась иначе. - Запустите воркер:
php artisan queue:work. Он крутится в фоне и разбирает задачи по мере поступления. - В проде воркер не запускают руками — им управляет supervisor или systemd, чтобы он перезапускался после падения. Иначе первая ошибка остановит отправку всех писем.
Отдельный момент: queue:work держит приложение в памяти и не подхватывает изменения кода. После деплоя обязательно перезапускайте воркер (php artisan queue:restart), иначе он продолжит работать со старым кодом mailable.
Где почта ломается на живом проекте
Письма не уходят, а ошибок нет
Классика: QUEUE_CONNECTION=sync локально всё работает, на проде стоит database, но воркер не запущен. Письма падают в таблицу jobs и лежат там. Проверяется одним запросом — если строк много и они не убывают, воркер не работает.
Сериализация модели теряет данные
При queue() Laravel сериализует mailable, чтобы положить в очередь. Трейт SerializesModels сохраняет модели по их ключу, а при выполнении задачи заново достаёт из базы. Если вы передали в письмо несохранённую модель или удалили запись до того, как воркер до неё добрался, — задача упадёт с ModelNotFoundException. Не передавайте в mailable объекты, которых может не быть в базе к моменту отправки.
Упавшие задачи уходят в никуда
Если задача превысила число попыток, она попадает в таблицу failed_jobs — если вы её создали. Без этой таблицы упавшая задача теряется без следа. Настройте таблицу failed jobs и периодически смотрите в неё: там оседают письма, которые так и не ушли.
Тесты бьют по настоящему SMTP
Если в тестах не подменить почту, они попытаются отправить реальные письма. В начале теста вызывайте Mail::fake() — после этого отправка перехватывается, а факт отправки можно проверить:
use App\Mail\OrderShipped;
use Illuminate\Support\Facades\Mail;
Mail::fake();
// ... код, который должен отправить письмо
Mail::assertQueued(OrderShipped::class, function ($mail) use ($order) {
return $mail->order->id === $order->id;
});
Обратите внимание: если код использует queue(), проверяйте через assertQueued, а не assertSent — это разные методы для разных способов отправки.
Что настроить дальше
Когда базовая отправка через очередь работает, займитесь наблюдаемостью. Три вещи стоит держать под контролем в проде:
- Размер очереди. Растущее число необработанных задач в таблице
jobsили в Redis — сигнал, что воркеров не хватает или они падают. - Таблица failed_jobs. Настройте алерт на появление новых строк — это письма, которые не дошли до пользователей.
- Доставляемость. Отправка не равна доставке. Настройте SPF, DKIM и DMARC для домена отправителя, иначе письма будут уходить в спам независимо от того, насколько чистый у вас код.
Прогрев шаблонов, локализация писем и вложения — отдельные темы; здесь мы разобрали только путь письма от контроллера до сети и места, где оно теряется.
Репетитор 1—11 класс и ВУЗ: Математика и физика методом наведения на мысль, помощь с ДЗ.
Спросить за 8 ₽Источники: Laravel Documentation — Mail, Laravel Documentation — Queues
Частые вопросы
У меня не Laravel 11, а Laravel 9. Код из статьи подойдёт?
Письмо не приходит, но в failed_jobs пусто. Что проверять?
Обязательно ли использовать очередь? У меня маленький проект.
Почему письмо уходит, но приходит в спам?
Изменил mailable, задеплоил, а письма всё равно старые. Почему?
Как посмотреть содержимое письма, не отправляя его реально?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.