"}'
# HTTP/2 202
# { "id": "msg_01J...", "status": "queued" }
``
Один RTT на прогретом соединении — и у вас на руках 202 Accepted с message-id. HTTP/2 добавляет мультиплексирование: сотни писем идут по одному TLS-соединению без нового рукопожатия на каждое, keep-alive держит канал открытым между запросами. Плата за простоту прямая — формат payload и SDK свои у каждого провайдера, стандарта тут нет.
## Скорость: где утекают миллисекунды
Ключевой вопрос не «какой протокол быстрее», а **сколько раз вы платите за хендшейк**. Разложим бюджет.
**Холодное соединение.** Serverless-функция просыпается, шлёт одно письмо, умирает. У SMTP это TCP + TLS + EHLO + STARTTLS + повторный EHLO + AUTH — весь диалог с нуля, 5–7 RTT. У HTTP — TCP + TLS + один POST. На одиночном холодном письме HTTP стабильно быстрее, и это структурная разница, а не оптимизация.
**Тёплое соединение.** Долгоживущий воркер с пулом коннектов рассылает поток. Здесь SMTP отыгрывается: хендшейк уже оплачен, PIPELINING склеивает команды, стоимость письма падает до одного-двух RTT. При переиспользовании коннекта разрыв между транспортами почти исчезает.
Вывод практичный. Если письмо уходит из эфемерного окружения, где коннект каждый раз новый, HTTP API выигрывает по латентности просто потому, что делает меньше кругов. Если у вас свой воркер с пулом и очередью — оба варианта близки, и решает уже не скорость, а обратная связь и связанность.
## Обратная связь: синхронный код против вебхука
SMTP отдаёт результат прямо в сессии, и это его сильная сторона: трёхзначный код приходит синхронно, не отходя от сокета.
- **250** — принято в очередь релея.
- **4xx** (421, 451) — временный отказ: greylisting, лимиты скорости, «повтори позже». Сигнал поставить письмо в очередь и ретраить с backoff, а не потерять.
- **5xx** (550 no such user, 554 policy reject) — постоянный отказ, ретраить бессмысленно.
Но здесь зарыта главная ловушка: **принятие релеем не равно доставке**. Код 250 означает лишь «я взял письмо в очередь». Финальный отказ придёт асинхронно — как DSN (delivery status notification) на адрес в Return-Path, в формате multipart/report (RFC 3464). Чтобы узнать про отскок, вам нужен ящик под Return-Path и парсер этих отчётов — отдельная инфраструктура, которую придётся строить и поддерживать.
HTTP API решает это иначе. Синхронно вы получаете тот же «принято» — 202 плюс message-id. А статусы delivered, bounced, complained, opened, clicked прилетают **подписанными вебхуками**, которые вы сразу пишете в БД по message-id. DSN-парсинг не нужен: провайдер уже разобрал отчёт за вас и прислал структурированное событие.
Две петли обратной связи
сплошная = синхронно в сессии · пунктир = асинхронно, отдельным каналом
SMTP relay
250 / 4xx / 5xx
принято в очередь
DSN → Return-Path, парсить самому (RFC 3464)
HTTP API
202 + message-id
принято в очередь
Webhook
delivered · bounced
complained · opened
подписанный вебхук → запись в БД по message-id
Есть и вопрос **идемпотентности**. SMTP не дедуплицирует ничего. Классический сценарий двойной отправки: клиент отправил финальную точку, письмо уже в очереди релея, но ответ 250 не дочитался из-за сетевого таймаута. Клиент считает отправку неудачной, ретраит — и абонент получает два письма. HTTP закрывает это заголовком **Idempotency-Key**: провайдер запоминает ключ, и повтор с тем же ключом не создаёт второе письмо, а возвращает исходный message-id.
## Сложность интеграции и связанность
SMTP — переносимый стандарт, и это его главный козырь в долгую. Один и тот же nodemailer или smtplib работает с любым релеем: сменили host и порт в конфиге — переехали к другому провайдеру за пять минут, ноль изменений в коде. Vendor lock-in отсутствует по определению.
javascript
const t = nodemailer.createTransport({
host: 'smtp.evilmail.pro',
port: 587,
secure: false, // STARTTLS, не implicit
requireTLS: true, // не отдавать AUTH по открытому каналу
auth: { user: '[email protected]', pass: process.env.SMTP_PASS },
pool: true, // переиспользование коннектов — ключ к скорости
maxConnections: 5,
});
await t.sendMail({ from: '[email protected]', to, subject, html });
Обратите внимание на pool: true. Без пула nodemailer открывает новое соединение на каждое письмо, и вы платите за полный хендшейк каждый раз. Именно это чаще всего делает «медленный SMTP» медленным, а не сам протокол.
HTTP API даёт быстрый старт и богатые события из коробки, но payload и обработчик вебхуков привязаны к провайдеру. Переезд означает переписывание отправки и приёмника событий. Взамен вы получаете открытия, клики и статусы доставки без единой строчки инфраструктуры на своей стороне.
Операционные различия, которые бьют на проде:
- **Сетевые блокировки.** Firewall и облачные политики режут 25, иногда 587; HTTPS проходит везде — часто решающий аргумент для serverless и managed-платформ.
- **Секреты.** SMTP-пароль обычно один на всё и меняется редко. API-ключ ротируется, скоупится по правам и отзывается независимо — хранить оба нужно в секрет-менеджере, а не в коде.
- **DNS одинаков.** В обоих случаях в зоне лежат одни и те же записи, и настраиваются они раз:
text
evilmail.pro. TXT "v=spf1 include:_spf.evilmail.pro -all"
mail._domainkey.evilmail.pro. TXT "v=DKIM1; k=rsa; p=MIGf..."
_dmarc.evilmail.pro. TXT "v=DMARC1; p=quarantine; rua=mailto:[email protected]; adkim=s; aspf=s"
Ещё раз, чтобы закрыть исходный миф: транспорт не участвует в аутентификации домена. SPF проверяет отправляющий IP, DKIM — подпись, DMARC — их выравнивание с From. Всё это идентично, идёт письмо по SMTP или по HTTP.
## Что выбирать под задачу
- **Транзакционные письма из serverless или облака, где 25/587 зарезан** → HTTP API. Холодные коннекты, HTTPS проходит сквозь любой firewall, события из коробки.
- **Массовая или регулируемая рассылка через собственный MTA с очередью, ретраями и контролем над цепочкой Received** → SMTP-релей. Пул соединений нивелирует латентность, полный контроль над заголовками.
- **Нужна максимальная переносимость и минимум зависимостей** → SMTP. Стандарт переживёт любого провайдера.
- **Нужны открытия, клики и вебхуки без своей инфраструктуры** → HTTP API. DSN-парсинг не строить.
И честный тезис, который не любят вендоры: продовые системы часто держат **оба**. HTTP как основной путь ради событий и скорости на холодную, SMTP как совместимый фолбэк, если API-эндпоинт лёг или нужен срочный переезд. Это не компромисс, а нормальная отказоустойчивая архитектура.
## Чеклист перед выбором
- Проверьте, не зарезан ли исходящий 25/587 у вашего хостера — telnet relay 587 или nc -vz из целевого окружения, а не с ноутбука.
- Замерьте реальный RTT до релея и прикиньте среднее число писем на одно соединение — это определяет, окупается ли пул.
- Решите заранее, нужны ли открытия и клики: если да — это HTTP, SMTP их не даёт.
- Настройте приём DSN (ящик под Return-Path) или обработчик вебхуков **до** запуска, а не после первого отскока.
- Внедрите идемпотентность на ретраях: Idempotency-Key для HTTP, дедуп-ключ на своей стороне для SMTP.
- Пропишите минимум SPF include релея, DKIM selector._domainkey и DMARC p=quarantine` — без этого транспорт не спасёт.
- Храните SMTP-пароль и API-ключ в секрет-менеджере, ключу задайте скоуп и ротацию, не держите в коде.
- Заложите очередь и экспоненциальный backoff на 4xx — временный отказ это норма, а не ошибка приложения.
- Проверяйте подпись вебхуков (constant-time сравнение) и валидируйте источник — эндпоинт публичный.
- Логируйте message-id сквозняком: от момента отправки до финального вебхука или DSN, чтобы отладка сводилась к одному grep по ID.