SMTP, REST API или SDK: что выбрать для отправки писем

Вопрос «SMTP или API» звучит почти в каждом подключении, и почти всегда за ним стоит другой вопрос: сколько кода придётся написать сейчас и о чём придётся жалеть через полгода. В FMailer доступны три способа отправки — SMTP-релей, REST API и официальный Python SDK, — и все три работают через один и тот же токен домена, попадают в один и тот же журнал и порождают одни и те же вебхуки.
Разница не в доставке. Письмо, отправленное по SMTP, и письмо, отправленное POST-запросом, подписываются одним и тем же ключом DKIM вашего домена, уходят через одну очередь и получают один и тот же ответ принимающего сервера. Разница в том, что вы можете сказать о письме при отправке и что узнаете о нём в ответе.
Ниже — короткий ответ для тех, кто выбирает прямо сейчас, а затем разбор каждого способа: что он умеет, чего не умеет, как выглядит код и в каких проектах он оказывается правильным. В конце — сравнительная таблица, частые ошибки и ответы на типичные вопросы.
Короткий ответ
- SMTP — если приложение уже отправляет почту. Меняются хост, логин и пароль в конфиге, код не трогается вообще. Самый быстрый переезд: обычно час вместе с DNS.
- REST API — если вы пишете интеграцию с нуля или вам нужно то, чего в SMTP нет: серверные шаблоны,
idempotency_key, метки, пакетная отправка, явный флаг рассылки,message_idв ответе, отслеживание кликов. - SDK — тот же REST API, но на Python и в две строки, с асинхронной отправкой через пул потоков. Разумный старт для Django- и FastAPI-бэкендов, где не хочется собирать HTTP-запрос руками.
Можно пользоваться всеми тремя одновременно: способ отправки записывается у каждого письма отдельным полем, статистика и вебхуки от этого не меняются.
Один токен на все три способа
Начать стоит с того, что объединяет все варианты. В FMailer учётные данные выдаются на домен, а не на аккаунт: у токена есть логин и пароль, и это ровно те же логин и пароль, которые:
- подставляются в SMTP-клиент как имя пользователя и пароль;
- уходят в объекте
authв теле POST-запроса к REST API; - передаются как HTTP Basic при чтении отправленного через
GET-методы.
Отдельный токен на каждый домен — это не формальность. Разработка, продакшен и маркетинговый домен разводятся разными учётными данными, и утечка одного из них не открывает остальные. Токен при этом открывает только /external/ — это не ключ от аккаунта: домены, биллинг и команда ему недоступны.
Ещё одна общая вещь: отправитель проверяется по подключённому домену. Адрес в From должен принадлежать домену, который вы подтвердили, — иначе письмо будет отклонено, каким бы способом вы его ни отправляли. Это защита от подделки отправителя, и она одинакова для SMTP и API.
И третья: DNS не нужен, чтобы увидеть первое письмо. У каждого аккаунта есть песочница — учётные данные на общем домене с настоящим DKIM и настоящей доставкой; отправлять из неё можно только на адрес своего аккаунта. Интеграцию можно собрать и проверить, пока кто-то ещё ищет доступ к DNS-панели.
SMTP-релей: переезд без единой строки кода
SMTP — протокол 1982 года, и это его главное достоинство: его умеет всё. Django, Laravel, Rails, Spring Boot, ASP.NET, WordPress, 1С-Битрикс, самописный скрипт на PHP, который никто не открывал четыре года, — везде есть форма с полями «сервер», «порт», «логин», «пароль».
Параметры подключения
host: smtp.fmailer.ru
port: 587, 8587 — STARTTLS
465, 8465 — SSL/TLS
25, 8025 — без шифрования
login: логин токена домена
pass: пароль токена домена
Дублирующие порты (8587, 8465, 8025) нужны, когда хостинг-провайдер блокирует стандартные — это встречается чаще, чем хотелось бы. Порты без шифрования оставлены для окружений, где TLS невозможен; в продакшене их использовать не надо: по ним в открытом виде уезжает пароль токена.
Как это выглядит в коде
import smtplib
from email.message import EmailMessage
msg = EmailMessage()
msg["From"] = "Магазин <noreply@mail.example.ru>"
msg["To"] = "client@example.ru"
msg["Subject"] = "Подтверждение заказа №4417"
msg.set_content("Заказ принят. Доставка — завтра до 18:00.")
with smtplib.SMTP("smtp.fmailer.ru", 587) as smtp:
smtp.starttls()
smtp.login("<логин токена>", "<пароль токена>")
smtp.send_message(msg)
В реальном проекте этого кода обычно нет вовсе — есть четыре строки в настройках. Для Django, Laravel, Ruby on Rails, Express.js, Spring Boot, ASP.NET, NestJS, Next.js, FastAPI, Flask, Symfony и популярных CMS готовые конфигурации лежат в документации отдельными страницами.
Что стоит знать про SMTP заранее
Несколько получателей — это несколько писем. Письмо, адресованное нескольким людям, превращается в системе в отдельное письмо на каждого адресата, и для To, и для Cc, и для Bcc. У каждого свой Message-ID, свой вебхук доставки и своё место в лимитах тарифа — именно поэтому отказ, открытие или отписку можно отнести к конкретному человеку. Потолок — 100 получателей на письмо; следующему адресу сервер ответит 452 4.5.3 Too many recipients, и это просьба отправить остальных отдельной транзакцией, а не отказ.
Заголовок Bcc удаляется при приёме. Так и положено серверу отправки (RFC 5321 §7.2): скрытые получатели уже есть в конверте, а уцелевший Bcc сообщил бы каждому читателю, кто ещё был в письме. Большинство почтовых библиотек убирают его сами — поэтому те, что не убирают, и остаются незамеченными.
Адрес конверта по умолчанию совпадает с вашим From. Это необычно и сделано намеренно: SPF проверяется по вашему домену и выравнивается с From для DMARC. Обратная сторона — отложенные отказы приходят на адрес из From, а если это noreply@, их никто не прочитает. Для такого случая домену задаётся return path — адрес, который вы действительно читаете, обязательно на самом домене, а не на поддомене.
Тип письма определяется эвристикой. По SMTP невозможно сказать «это рассылка» явно — нет такого поля в протоколе. FMailer классифицирует письмо сам и на массовом проставляет заголовки отписки в один клик, но эвристика есть эвристика: она может ошибиться в обе стороны. Если вы отправляете маркетинговые письма, это первый аргумент в пользу API, где флаг mass_mail ставится руками.
Отслеживание кликов по SMTP не работает. Переписывание ссылок через ваш хост stats.<домен> доступно только для писем, отправленных через API: по SMTP приходит уже готовый MIME, части которого могут быть закодированы, и правка адресов внутри них — верный способ испортить тело письма. Пиксель открытий при этом работает в обоих случаях.
Ответ сервера — это код SMTP, а не JSON. Вы узнаете, что письмо принято, но message_id, идемпотентность и машинный разбор результата остаются за бортом.
Когда SMTP — правильный выбор
- Приложение уже отправляет почту, и надо просто сменить провайдера.
- Письма отправляет коробочный продукт или CMS, куда своего кода не добавить.
- Стек не Python и не JavaScript, а писать HTTP-клиент ради писем не хочется.
- Нужен самый быстрый способ увидеть первое доставленное письмо.
Проверить, что соединение вообще устанавливается, можно до всякого кода:
openssl s_client -starttls smtp -crlf -connect smtp.fmailer.ru:587
Если соединение не устанавливается — почти всегда дело в том, что исходящий SMTP закрыт на стороне вашего хостинга. Это, кстати, второй по частоте аргумент в пользу API: HTTPS наружу открыт всегда.
REST API: когда письму нужно больше, чем текст и адрес
REST API живёт на https://api.fmailer.ru, все запросы — POST с телом в JSON, логин и пароль токена передаются в объекте auth.
Отправка готового письма
curl -X POST https://api.fmailer.ru/external/send_email_simple/ \
-H "Content-Type: application/json" \
-d '{
"recipient": "client@example.ru",
"subject": "Подтверждение заказа №4417",
"body": "<p>Заказ принят. Доставка — завтра до 18:00.</p>",
"text": "Заказ принят. Доставка — завтра до 18:00.",
"sender": "Магазин <noreply@mail.example.ru>",
"idempotency_key": "order-4417",
"tags": {"stream": "order", "lang": "ru"},
"auth": {"username": "<логин токена>", "password": "<пароль токена>"}
}'
Ответ — не «принято», а перечисление того, что именно принято:
{
"ok": true,
"emails": [
{
"ok": true,
"message_id": "<uuid@mail.example.ru>",
"uuid": "uuid",
"recipient": "client@example.ru",
"kind": "to",
"idempotency_key": "order-4417",
"replayed": false
}
]
}
message_id — это тот же идентификатор, который придёт потом в вебхуке доставки. Сохранив его рядом с заказом, вы связываете событие с сущностью в своей базе, не запрашивая ничего дополнительно. По SMTP такой связи в ответе нет.
Что API умеет, а SMTP — нет
idempotency_key. Ваш собственный идентификатор события — номер заказа, идентификатор попытки входа. Повторный запрос с тем же ключом не создаёт второе письмо, поэтому ретрай после таймаута безопасен. В ответе такое письмо приходит с replayed: true — без второй отправки и без повторного расхода лимитов. Это самое ценное, что даёт API, и первое, что стоит поставить.
Серверные шаблоны. Вместо темы и тела передаётся tpl — slug шаблона из панели, lang и params с данными для подстановки:
{
"recipient": "client@example.ru",
"tpl": "order-confirmed",
"lang": "ru",
"sender": "Магазин <noreply@mail.example.ru>",
"idempotency_key": "order-4417",
"params": {"order": "4417", "delivery": "завтра до 18:00"},
"auth": {"username": "<логин токена>", "password": "<пароль токена>"}
}
Вёрстка письма перестаёт быть частью релиза приложения: у одного шаблона может быть русская и английская версия, а оформление настраивается под ваш бренд.
Явный флаг рассылки. mass_mail: true добавляет заголовки List-Unsubscribe и List-Unsubscribe-Post — ту самую отписку в одно нажатие, которую Gmail, Yahoo и Apple ожидают от массовых отправителей. Флаг работает в одну сторону: true помечает письмо рассылкой, false просто оставляет решение эвристике.
Метки. tags — объект вида {"stream": "password-reset", "lang": "en"}. В письмо они не попадают и на доставку не влияют; нужны они для чтения почты обратно: фильтр в журнале, поле в вебхуке, разбивка в аналитике. Именно метки отвечают на вопрос, на который не отвечает ничто другое: к какому потоку относится письмо. До 10 меток на письмо.
Вложения. Массив attachments с именем файла и байтами в base64: до 20 файлов, по 10 МБ каждый и 10 МБ суммарно, всё тело запроса — не больше 16 МБ. Исполняемые типы отклоняются по расширению и заявленному типу — тот же список, который применяет к вашей почте Gmail на приёме.
Пакетная отправка. POST /external/send_email_batch/ принимает до 100 писем в одном запросе: auth читается один раз на всю пачку, обычные и шаблонные письма можно смешивать. Ошибка в одном элементе отклоняет всю пачку с указанием индекса — это лучше, чем наполовину ушедшая рассылка. Ответ плоский и упорядоченный, по элементу на письмо.
Темы подписки. Поле topic привязывает письмо к теме («Новости продукта», «Статусы заказов»), и получатель, отказавшийся от темы, продолжает получать всё остальное, включая сброс пароля. Неизвестный slug — это ошибка 400, а не тихое игнорирование.
Чтение отправленного. Тот же токен, которым вы отправляете, умеет читать — через HTTP Basic:
curl https://api.fmailer.ru/external/emails/ \
-u "$TOKEN_USERNAME:$TOKEN_PASSWORD"
curl "https://api.fmailer.ru/external/stats/?start=2026-08-01&end=2026-08-28" \
-u "$TOKEN_USERNAME:$TOKEN_PASSWORD"
Пять GET-методов: список писем, одно письмо вместе с ушедшим HTML, его события, весь поток событий домена и итоги за период. Все ограничены доменом токена. Методы событий — платная возможность тарифа.
Понятные отказы. Превышение часового или месячного объёма — это HTTP 400 с текстом Hourly limit exceeded или Monthly limit exceeded, и такой запрос имеет смысл повторить с выдержкой. Остановленная отправка с домена — HTTP 403 с code: domain_banned; здесь повторы не помогут. Разница между «подожди» и «не пытайся» видна в коде, а не в логе.
Когда REST API — правильный выбор
- Новый бэкенд, где отправка пишется с нуля.
- Письма отправляются в ответ на события, которые могут повториться при ретрае, — оплата, регистрация, код подтверждения.
- Нужны рассылки с корректной отпиской в один клик.
- Вёрстку письма меняет маркетинг, а не разработчик.
- Нужна аналитика по потокам писем или отслеживание кликов.
SDK: тот же API, но короче
Для Python есть официальный SDK — обёртка над теми же двумя методами отправки. Он опубликован в PyPI под именем postwing.
pip install postwing
from postwing import PostwingSdk
sdk = PostwingSdk(
username="<логин токена>",
password="<пароль токена>",
)
# Для fmailer.ru базовый адрес указывается явно:
sdk.SERVER_URL = "https://api.fmailer.ru"
sdk.send_simple(
recipient="client@example.ru",
sender="Магазин <noreply@mail.example.ru>",
subject="Подтверждение заказа №4417",
body="<p>Заказ принят. Доставка — завтра до 18:00.</p>",
text="Заказ принят. Доставка — завтра до 18:00.",
idempotency_key="order-4417",
)
Отправка по шаблону — метод send:
sdk.send(
tpl="order-confirmed",
recipient="client@example.ru",
sender="Магазин <noreply@mail.example.ru>",
lang="ru",
params={"order": "4417", "delivery": "завтра до 18:00"},
idempotency_key="order-4417",
)
Асинхронная отправка
У обоих методов есть _async-версии, работающие через пул потоков. Они возвращают Future, поэтому отправку можно сделать неблокирующей, а результат — забрать позже или обработать колбэком:
def on_complete(success, error):
if error:
logger.error("Письмо не ушло: %s", error)
sdk.send_simple_async(
recipient="client@example.ru",
sender="Магазин <noreply@mail.example.ru>",
subject="Подтверждение заказа №4417",
body="<p>Заказ принят.</p>",
idempotency_key="order-4417",
callback=on_complete,
)
Размер пула задаётся параметром max_workers, а fail_silently=True превращает исключение в False — для случаев, когда неотправленное письмо не должен уронить запрос пользователя. Логирование настраивается уровнем: на DEBUG в лог попадают URL, полезная нагрузка (с замаскированным auth) и ответ сервера — этого обычно достаточно, чтобы понять, почему письмо не ушло, не поднимая прокси.
Границы SDK
SDK намеренно небольшой, и это стоит знать заранее.
- Он покрывает два метода отправки — простое письмо и шаблонное. Пакетной отправки, вложений, меток и
GET-методов чтения в нём нет: для них нужен обычный HTTP-запрос. - Методы возвращают
Trueили бросаютPostwingSdkException.message_idони не возвращают — если вы связываете письмо с сущностью в своей базе, обращайтесь к REST API напрямую. - Таймаут запроса фиксированный, пятисекундный.
- Асинхронные методы — это пул потоков внутри вашего процесса, а не очередь. Перезапуск приложения теряет то, что не успело уйти.
Иначе говоря, SDK хорош там, где письмо простое и его надо отправить в две строки. Как только письму нужны метки, вложения или пачка — вы всё равно окажетесь на уровне HTTP.
Сравнение возможностей
| Возможность | SMTP | REST API | Python SDK |
|---|---|---|---|
| Подключение без изменений в коде | да | нет | нет |
| Работает с любым стеком | да | да | только Python |
| Требует открытого исходящего SMTP | да | нет | нет |
idempotency_key |
нет | да | да |
| Серверные шаблоны | нет | да | да |
Явный флаг рассылки mass_mail |
нет (эвристика) | да | да |
message_id в ответе |
нет | да | нет |
Метки (tags) |
нет | да | нет |
| Вложения | да (MIME) | да (base64) | нет |
| Пакетная отправка до 100 писем | нет | да | нет |
Темы подписки (topic) |
нет | да | нет |
| Отслеживание кликов | нет | да | да |
| Пиксель открытий | да | да | да |
| Вебхуки о событиях | да | да | да |
| Чтение журнала тем же токеном | нет | да | нет |
Что одинаково во всех трёх колонках: ключ DKIM вашего домена, SPF и DMARC, разделение транзакционного и массового потоков, журнал с настоящим ответом принимающего сервера, репутация домена и лимиты тарифа.
Как выбрать: пять типичных ситуаций
У вас Django-приложение, которое уже шлёт почту через чужой SMTP. Меняете четыре настройки, отправляете тестовое письмо, смотрите журнал. Переезд занимает час, и это правильный первый шаг даже если в итоге вы хотите API: сначала письма идут, потом улучшается интеграция.
Вы пишете новый сервис, где письмо порождает событие. Сразу REST API с idempotency_key. Ретрай очереди, повтор webhook'а от платёжного провайдера, двойной клик пользователя — всё это в какой-то момент случится, и ключ идемпотентности превратит второе письмо в replayed: true.
Вам нужна рассылка по своей базе. Только API: mass_mail: true, topic, пакетная отправка. Без явного флага вы полагаетесь на эвристику там, где ошибка стоит жалобы на спам, а жалоба — репутации домена.
Стек не Python — Go, PHP, C#, Node.js. SMTP для быстрого старта, REST API для всего остального: это обычный POST с JSON, клиента для него писать не нужно ни в одном языке.
Коробочный продукт, CMS, 1С, оборудование. SMTP, и других вариантов обычно нет. Зато он есть везде.
Смешивать — нормально
Способ отправки записывается у каждого письма, и в журнале по нему можно фильтровать. Вполне рабочая схема: легаси-часть продолжает ходить по SMTP, новый код отправляет через API, рассылки уходят пачками, а вебхуки и статистика при этом общие. Начать с SMTP и перейти на API позже — это не переделка, а добавление.
Частые ошибки
Отправка письма прямо в HTTP-запросе пользователя. Любой способ отправки — это сетевой вызов, и он может занять секунды. Ставьте отправку в очередь (Celery, RQ, что угодно) и отвечайте пользователю сразу. Асинхронные методы SDK — это пул потоков в вашем же процессе, а не замена очереди.
Ретраи без idempotency_key. Очередь, которая перезапускает упавшую задачу, — это правильно. Очередь, которая перезапускает задачу «отправить код подтверждения» без ключа идемпотентности, — это два разных кода в почте у пользователя и звонок в поддержку.
Пустой text. Письмо всегда уходит как multipart/alternative. Если текстовую версию не передать, FMailer соберёт её из HTML сам, но вывод не знает, какие части вёрстки были украшением, и не увидит фразы, которую вы написали бы для читателя без стилей. Почтовые провайдеры сравнивают обе части при оценке письма.
Рассылка без mass_mail. Эвристика может не распознать в вашем письме массовое, и тогда в нём не окажется заголовков отписки. Дальше человек, которому надоела рассылка, нажимает «Спам». Отписка стоит одного адреса, жалоба — репутации домена.
Порт 25 в продакшене. По незашифрованному соединению пароль токена уезжает в открытом виде. Дублирующие порты 8587 и 8465 существуют ровно для того, чтобы не приходилось выбирать между шифрованием и заблокированным провайдером портом.
Токен в репозитории или в конфиге фронтенда. Токен домена позволяет отправлять письма от вашего имени и читать журнал отправки. Его место — в переменных окружения или в хранилище секретов, а сама отправка — всегда на сервере, никогда из браузера или мобильного приложения.
Игнорирование разницы между 400 и 403. Hourly limit exceeded — это «повтори позже», и повтор с выдержкой сработает. domain_banned — постоянный отказ, и повторы только сожгут очередь. Обрабатывайте их по-разному.
Частые вопросы
Что быстрее — SMTP или API?
На практике разница незаметна на фоне самой доставки: и то, и другое ставит письмо в очередь за миллисекунды. Заметная разница возникает на объёме: одно письмо по SMTP — это несколько сетевых обменов и обычно новое соединение, тогда как пакетный метод API принимает до 100 писем одним запросом. Для потока в несколько писем в минуту это неважно; для рассылки — важно.
Доставляемость через API лучше, чем через SMTP?
Нет. Письмо подписывается тем же ключом DKIM, отправляется той же инфраструктурой и получает тот же ответ принимающего сервера. На доставляемость влияет аутентификация домена, качество базы, доля жалоб и содержание письма — а не транспорт, которым письмо попало к отправляющему сервису.
Можно ли пользоваться SMTP и API одновременно?
Да, и это распространённая схема. Токен один, домен один, журнал общий; у каждого письма записан способ отправки, по которому можно фильтровать.
Нужен ли SDK, если есть REST API?
Нет, это удобство, а не необходимость. SDK экономит несколько строк на Python и даёт готовую асинхронную отправку. Как только нужны вложения, метки, пакетная отправка или message_id в ответе, вы всё равно возвращаетесь к обычному HTTP-запросу.
Есть ли SDK для других языков?
Официальный SDK сейчас один — Python. Для остальных языков API остаётся обычным POST с JSON: любой HTTP-клиент справится без дополнительной библиотеки.
Как отправлять письма, если хостинг блокирует исходящий SMTP?
Двумя способами: попробовать дублирующие порты 8587 и 8465, которые для этого и заведены, либо перейти на REST API — исходящий HTTPS открыт практически везде. Проверить доступность порта можно командой openssl s_client -starttls smtp -crlf -connect smtp.fmailer.ru:587.
Нужно ли настраивать SPF, DKIM и DMARC, если я отправляю через API?
Да, и это не зависит от способа отправки. Панель генерирует три записи, которые нужно опубликовать у регистратора; дальше FMailer сам опрашивает DNS и следит, чтобы они не пропали при переезде на другой хостинг. Проверить текущее состояние домена можно бесплатными чекерами в разделе «Инструменты», ещё до регистрации.
Как узнать, что письмо дошло, при отправке по SMTP?
Так же, как и при отправке через API: вебхуками и журналом. По каждому письму виден настоящий SMTP-ответ принимающего сервера — с кодом и текстом, а не «ошибка доставки». Разница только в том, что при отправке через API вы получаете message_id сразу в ответе и можете сохранить его рядом с заказом.
Можно ли начать с SMTP и перейти на API потом?
Да, и это самый частый путь. SMTP снимает срочность — письма идут, — а API добавляется там, где нужны шаблоны, идемпотентность или рассылки. Учётные данные при этом не меняются: токен домена один и тот же.
Итог
SMTP отвечает на вопрос «как начать отправлять сегодня». REST API отвечает на вопрос «как сделать так, чтобы отправка была надёжной через полгода»: идемпотентность, шаблоны, метки, пачки, message_id и понятные коды отказа. SDK — короткий путь к API для Python-проектов, у которого есть свои границы.
Правильный ответ для большинства проектов состоит из двух шагов: подключиться по SMTP за час, а затем перевести на API те письма, где повтор запроса, шаблон или рассылка действительно что-то значат. Один токен, один домен, один журнал — переход ничего не ломает.
Попробовать FMailer
Подключите домен на fmailer.ru: панель сгенерирует SPF, DKIM и DMARC и проверит их сама, а первое письмо можно отправить через SMTP, не меняя код, — или через API, если начинаете с нуля. Пока DNS расходится, интеграцию можно собрать в песочнице: настоящий DKIM, настоящая доставка, те же вебхуки.