Исходящая почта на бэкенде
Play ITЗагрузка интерактивного демо…
Регистрация, сброс пароля, счета, уведомления — email остаётся надёжным каналом, когда push и мессенджеры недоступны. Бэкенд не "рисует письмо в браузере", а формирует сообщение и передаёт его почтовой инфраструктуре.
Для продукта почтовый контур одновременно технический и репутационный. Пользователь оценивает сервис по тому, дошло ли письмо вовремя, открылась ли ссылка восстановления, совпал ли контент с ожидаемым действием.
Когда email становится критичным
В большинстве продуктов почта отвечает за операции с высокой ценой ошибки:
- подтверждение владения аккаунтом;
- восстановление доступа;
- финансовые документы и юридические уведомления;
- служебные уведомления для команды и клиентов.
Поэтому для почтового контура важны наблюдаемость, повторяемость и контроль доставки.
Цепочка доставки
На практике приложение редко говорит напрямую с MX Gmail: чаще используют транзакционный SMTP (облачный релей) с API или SMTP-auth.
Эта модель отделяет бизнес-логику приложения от сложностей доставки — репутации домена, повторных попыток, обработки bounce и политики почтовых провайдеров.
Структура письма (MIME)
Письмо — текст с заголовками (RFC 5322 + MIME):
| Заголовок | Назначение |
|---|---|
From | Отправитель (должен быть разрешён SPF/DKIM домена) |
To / Cc / Bcc | Получатели |
Subject | Тема (кодировка UTF-8 → base64 в заголовке) |
Message-ID | Уникальный идентификатор (полезен для поддержки) |
Content-Type | text/plain, text/html, multipart/alternative |
Multipart — одно письмо с несколькими частями: plain + HTML + вложения. Клиент выбирает подходящую часть.
Такая структура повышает совместимость. Клиенты с ограниченным HTML-рендерингом читают текстовую часть, а современные интерфейсы показывают HTML-версию без потери смысла сообщения.
Упрощённый каркас multipart/alternative (как формирует библиотека или MTA):
From: Shop <noreply@shop.example>
To: user@example.com
Subject: =?UTF-8?B?0J7Qv9C40YHQutC+0L3RgdC60Lg=?=
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="boundary42"
--boundary42
Content-Type: text/plain; charset=utf-8
Сброс пароля: https://shop.example/reset?token=…
--boundary42
Content-Type: text/html; charset=utf-8
<p>Сброс пароля: <a href="https://shop.example/reset?token=…">перейти</a></p>
--boundary42--
На DNS домена отправителя настраивают SPF (кто может слать от имени домена), DKIM (подпись письма) и DMARC (политика при несовпадении) — без этого транзакционные письма часто попадают в спам.
Кодировки тела:
- base64 — бинарные вложения, нелатиница в старых клиентах;
- quoted-printable — текст с редкими не-ASCII символами.
Что реализует бэкенд
Почтовая подсистема на бэкенде устроена как отдельный процесс с очередью и подтверждением статусов. Такой подход сохраняет отзывчивость HTTP-слоя и делает отправку управляемой под нагрузкой.
- Шаблоны — отделение текста от кода (HTML + подстановка переменных).
- Очередь — отправка асинхронно: HTTP-запрос не ждёт SMTP 30 секунд.
# После успешной регистрации — не блокировать ответ клиенту
def register_user(email: str, password: str) -> User:
user = create_user(email, password)
queue.enqueue("send_email", template="welcome", user_id=user.id)
return user
- Идемпотентность — повтор задачи не должен слать десятое "сбрось пароль"; ключ:
userId + template + window. - Логирование — статус
queued / sent / bounced, без паролей в логах. - Unsubscribe — для маркетинга обязателен; для транзакционных писем — другие правила.
Типичные ошибки:
- синхронная отправка в обработчике HTTP → таймауты;
- нет retry с экспоненциальной паузой при 4xx/5xx релея;
- From-домен без SPF, DKIM, DMARC → письма в спаме.
Статусы доставки и реакция системы
| Статус | Смысл | Действие на бэкенде |
|---|---|---|
queued | Письмо принято в очередь | сохранить message_id и срок SLA |
sent | Релей принял письмо | закрыть задачу отправки |
deferred | Временная проблема доставки | повтор по backoff |
bounced | Постоянная ошибка адреса или политики | пометить адрес, остановить дальнейшие отправки |
complaint | Получатель пожаловался на спам | мгновенно исключить адрес из маркетинговых рассылок |
Практика: храните события доставки отдельно от бизнес-таблиц, чтобы поддержка могла быстро ответить "что именно случилось с письмом".
Эта детализация превращает отправку писем из "чёрного ящика" в наблюдаемую систему, где каждое сообщение имеет историю и понятный операционный статус.
Шаблоны и контент без хаоса
Рекомендуемая структура шаблонизации:
- Базовый layout (шапка, подвал, юридический блок).
- Контентные шаблоны по типам писем.
- Версионирование шаблонов в репозитории.
- Предпросмотр HTML и plain-варианта в CI.
Такой подход помогает безопасно менять тексты и снижает риск поломки важной рассылки.
Тестирование без риска
| Среда | Подход |
|---|---|
| Локально | Поднять песочницу SMTP: все письма перехватываются, в веб-UI видно HTML |
| CI | Mock SMTP или заглушка API провайдера |
| Staging | Отдельный домен, whitelist получателей |
Никогда не гоняйте автотесты на реальные ящики пользователей.
Безопасная тестовая среда защищает репутацию домена отправителя и снимает риски случайной рассылки. Для энциклопедического подхода это ключевой принцип: проверка механики доставки должна быть полной, а воздействие на внешних получателей — нулевым.
Проверка порта с сервера: openssl s_client -connect smtp.example:587 -starttls smtp (или аналог в терминале).
Безопасность
- Не вставляйте в HTML неэкранированные пользовательские данные (XSS в почтовике).
- Ссылки сброса пароля — одноразовые токены с коротким TTL.
- Rate limit на "отправить письмо" — защита от спама и перебора email.
Подробнее об общей модели: безопасность приложений.
Мини-checklist перед запуском в прод
- Настроены SPF, DKIM, DMARC и проверены внешним валидатором.
- Все письма отправляются асинхронно через очередь.
- Реализованы retry/backoff и дедупликация по ключу идемпотентности.
- Логи и события доставки связаны через
message_id/request_id. - Есть отдельные домены для staging и production.