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

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, настоящая доставка, те же вебхуки.

Подключить домен на fmailer.ru →