руководство · email api

Как продуктовой команде безопасно внедрить email API?

Внедряйте email API как асинхронный процесс с проверкой прав, а не как прямой вызов провайдера из формы. Аутентифицируйте вызывающую сторону, подтвердите, что клиенту принадлежит подтверждённый домен From, проверьте письмо и его размер, присвойте стабильный идентификатор задачи приложения, поставьте её в очередь один раз и отправляйте из воркера. При приёме сохраняйте идентификатор письма у провайдера, идемпотентно обрабатывайте события доставки и добавляйте в стоп-лист адреса с постоянными отказами и жалобами. Используйте ограниченные повторы только там, где риск дублирования под контролем. Храните учётные данные на сервере, минимизируйте данные писем в журналах и различайте приём через API, доставку на почтовый сервер и попадание во «Входящие».

Определите границы API до выбора провайдера

Email API должен выражать намерения приложения, не протаскивая каждую деталь провайдера в код продукта. Определите ресурсы для писем, доменов отправки, API-ключей, событий и стоп-листа. Решите, какие поля могут задавать вызывающие стороны, включая From, To, Reply-To, тему, текст, HTML и небольшой белый список заголовков. Отклоняйте переданные клиентом транспортные заголовки, которые могут конфликтовать с подписью или маршрутизацией у провайдера. Считайте отправку значимой операцией записи: ответ должен указывать на ресурс письма в приложении и его текущее состояние, а не намекать на результат в почтовом ящике. Скрывайте аккаунт провайдера, регион, набор конфигурации и транспортные идентификаторы за адаптером. Такая граница делает возможной смену провайдера и даёт авторизации, хранению данных и защите от злоупотреблений стабильное место.

Аутентифицируйте вызывающих и авторизуйте каждый домен отправителя

Храните API-ключи только в виде односторонних хешей и показывайте секрет целиком один раз. У каждого ключа должны быть рабочее пространство-владелец, статус, время создания и способ отзыва; добавьте более узкие права, если интеграция должна только отправлять письма или только читать события. Аутентификация отвечает на вопрос, кто предъявил учётные данные, а авторизация решает, может ли этот субъект использовать запрошенный домен From и ресурс письма. Проверяйте владение доменом при каждой отправке, включая пакетные эндпоинты, а не доверяйте идентификатору домена, переданному клиентом. Требуйте подтверждения у провайдера, прежде чем включать трафик в продакшене. Никогда не размещайте учётные данные провайдера или API-ключи рабочего пространства в JavaScript браузера, строках запроса, аналитике или сообщениях об ошибках. Авторизация на уровне объектов особенно важна для идентификаторов писем, событий, стоп-листа, почтовых ящиков и доменов в многоклиентском API.

Подтвердите домен и выровняйте аутентификацию

Домену отправки нужно больше, чем флаг в базе данных. Пройдите проверку владения у провайдера и опубликуйте требуемые DKIM-записи. SPF авторизует хосты для идентификатора SMTP MAIL FROM или HELO, а DKIM связывает домен подписи с криптографической подписью письма. DMARC проверяет, выровнен ли успешно прошедший SPF- или DKIM-идентификатор с видимым доменом From по RFC 5322, и позволяет владельцу домена публиковать политику обработки и отчётности. Если у домена уже есть SPF, добавьте требуемый механизм в существующую запись: по RFC 7208 домен не должен публиковать несколько записей, из-за которых будет выбрано больше одной SPF-записи. Ужесточайте политику DMARC только после того, как контрольные письма и агрегированные отчёты покажут, что все легитимные отправители выровнены. Аутентификация снижает несанкционированное использование домена, но не гарантирует попадания во «Входящие».

Проверяйте структуру письма и минимизируйте принимаемые данные

RFC 5322 определяет интернет-сообщение как набор полей заголовка, за которыми следует необязательное тело, а спецификации MIME расширяют содержимое за пределы простого текста. API может скрывать большую часть деталей формата передачи, но всё равно должен их соблюдать. Нормализуйте массивы получателей, ограничивайте число получателей и общий закодированный размер, требуйте хотя бы одно тело — текстовое или HTML — и проверяйте адреса, не делая вид, что корректный синтаксис доказывает существование почтового ящика. Удаляйте символы возврата каретки и перевода строки из полей, которые становятся заголовками. Генерируйте Message-ID сами или доверьте это провайдеру; не используйте его повторно как идентификатор задачи приложения, потому что новая версия письма вполне может получить новый идентификатор. Разрешайте только задокументированные пользовательские заголовки, отклоняйте дубликаты защищённых полей и рендерите шаблоны до отправки провайдеру, чтобы отсутствующие переменные приводили к ошибке в контролируемом состоянии приложения.

Ставьте в очередь один раз и используйте стабильные идентификаторы приложения

Запрос пользователя должен создавать одну надёжно сохранённую задачу письма в рамках транзакции, а вызов провайдера должен выполнять воркер. Дайте задаче стабильный идентификатор и сохраняйте отпечаток запроса или переданный клиентом ключ идемпотентности, если контракт это поддерживает. HTTP определяет POST как по умолчанию неидемпотентный и предостерегает от автоматических повторов, если клиент не знает, что операция фактически идемпотентна, или что исходный запрос не был применён. Для почты это важно: тайм-аут может произойти после того, как провайдер принял письмо, но до того, как воркер получил ответ. При неоднозначном сбое сначала сверьте сохранённую задачу и состояние у провайдера, а не создавайте новую отправку. Используйте паттерн outbox, когда состояние приложения и публикация в очередь должны меняться вместе, и поставьте ограничение уникальности на границе идемпотентности.

Проектируйте повторы с учётом классов сбоев

Разделяйте ошибки валидации, авторизации, ограничения частоты, отклонения провайдером, временные транспортные сбои и сбои доставки получателю. Некорректные входные данные и неавторизованный домен From должны завершаться ошибкой без повтора. Лимиты запросов провайдера и временные ошибки сервиса можно повторять с ограниченной экспоненциальной задержкой (exponential backoff), случайным разбросом (jitter), максимальным числом попыток и тайм-аутом видимости в очереди, превышающим дедлайн запроса воркера. Неоднозначный сетевой тайм-аут требует сверки с учётом дубликатов, а не безусловного нового запроса. Сам SMTP различает временные ответы 4xx и постоянные 5xx, но приложению, работающему через API провайдера, следует опираться на задокументированную семантику ошибок этого провайдера. Задачи, исчерпавшие попытки, переводите в состояние dead-letter для разбора и сохраняйте очищенную причину. Не повторяйте постоянный отказ получателя так, будто это сбой API, и не превращайте жалобу в очередную попытку отправки.

Фиксируйте приём и обрабатывайте события доставки

Сохраняйте идентификатор сообщения провайдера сразу после принятия и сопоставляйте его с ID сообщения приложения. Тогда события провайдера смогут обновлять правильный ресурс, даже если отчёт о жалобе скрывает сведения о получателе. Amazon SES, например, различает успешную отправку и доставку на почтовый сервер получателя и может публиковать события delivery, bounce, complaint, reject, delivery delay, rendering failure, open и click. Проверяйте подлинность вебхука документированным провайдером способом, валидируйте схему события, устраняйте дубликаты по идентификатору события провайдера или детерминированному отпечатку и допускайте повторную доставку того же события без повтора побочных эффектов. Храните исходные полезные нагрузки (payload) только при необходимости, в зашифрованном виде, с контролем доступа и ограниченным сроком хранения. Нормализованное состояние должно различать результаты accepted, delivered-to-server, bounced, complained, delayed, rejected и suppressed.

Сделайте стоп-лист проверкой в момент отправки

Запись в стоп-листе нужно проверять перед каждой отправкой провайдеру, а не только показывать в панели управления. Адреса с постоянными отказами и жалобами обычно нужно добавлять в стоп-лист; для временных задержек доставки нужна другая политика. Осознанно выбирайте область действия стоп-листа. Список на уровне всего аккаунта защищает общую репутацию, но может позволить результату доставки одного клиента блокировать другого. Список на уровне клиента уменьшает такую связанность, но всё равно требует слоя защиты от злоупотреблений и безопасности платформы. Фиксируйте причину, исходное событие, клиента, время создания и контролируемый способ удаления. Удаление из стоп-листа после жалобы или постоянного отказа — значимое действие, которое должно требовать осознанной проверки и доказательств того, что адрес действителен и получатель ждёт письма. Не копируйте исходные адреса получателей в общие журналы или эксперименты: операционное хранилище может обеспечивать политику отправки, а аналитика — использовать агрегированные счётчики.

Защищайте пакетные отправки и чувствительные бизнес-процессы

Пакетный эндпоинт умножает последствия ошибки авторизации или валидации. Применяйте к каждому элементу те же проверки владения доменом, стоп-листа, размера и содержимого, установите строгий максимум размера пакета и возвращайте результаты по каждому элементу, не раскрывая данные другого клиента. Лимиты запросов должны действовать на уровне учётных данных, рабочего пространства, домена и провайдера, с отдельным контролем всплесков и скользящего объёма. Одного глобального лимита запросов в секунду недостаточно, потому что один запрос может содержать множество получателей. В инструментах, которыми управляют агенты, требуйте осознанного подтверждения перед отправкой пакета с серьёзными последствиями. Разделяйте права на транзакционные и маркетинговые письма, если к ним применяются разные правила согласия и эксплуатации. Отслеживайте необычный рост числа получателей, повторяющиеся отклонённые домены, резкие изменения доли отказов или жалоб и быстрое создание ключей. Ограничение частоты запросов помогает безопасности, но не заменяет аутентификацию, авторизацию на уровне объектов, подтверждённое согласие и реагирование на злоупотребления.

Тестируйте сценарии сбоев до продакшена

Используйте симуляторы провайдера или контролируемые почтовые ящики, чтобы протестировать приём, доставку на сервер получателя, жёсткий отказ, жалобу, задержку, некорректный домен, отозванный ключ, ограничение частоты, тайм-аут провайдера, повторный вебхук и повторную доставку из очереди. Убедитесь, что один и тот же ключ идемпотентности создаёт одно письмо в приложении, повторно воспроизведённое событие не вызывает дублирующих побочных эффектов, а один клиент не может читать или отправлять письма с доменом или идентификатором письма другого клиента. Изучите реально полученное письмо: From, Return-Path, DKIM, SPF, DMARC-выравнивание, отображение текста и HTML, поведение отписки там, где она применима, и ссылки. Проводите нагрузочное тестирование очереди ниже одобренных лимитов провайдера и проверяйте работу обратного давления (backpressure), а не обходите его. Добавьте оповещения о возрасте очереди, исчерпанных повторах, сбоях приёма событий, запасе квоты, изменениях доли отказов и жалоб и отсутствующих обратных вызовах провайдера. В чек-листе запуска для каждого оповещения и действия по восстановлению должен быть указан ответственный.

Применяйте этот подход в SendHQ осторожно

SendHQ предоставляет bearer-ключи в рамках рабочего пространства, проверку подтверждённого домена From, создание одиночных и пакетных писем, входящие почтовые ящики, события писем и ресурсы стоп-листа. Эти возможности поддерживают архитектуру из этого руководства: храните ключ на сервере, создавайте ресурс письма, сохраняйте его ID и читайте последующие события, а не считайте начальный ответ окончательной доставкой. Независимо от платформы отправители остаются ответственны за предполагаемых получателей, законную и ожидаемую почту, точность содержимого и аккуратное одобрение значимых отправок.

Частые вопросы

Должен ли email API отправлять письмо синхронно из веб-запроса?

Как правило, нет. Создайте надёжно сохранённое письмо в приложении, поставьте его в очередь и дайте воркеру вызвать провайдера. Это изолирует задержки, позволяет делать ограниченные повторы и упрощает сверку при неоднозначных результатах у провайдера.

Как избежать дублирующих писем при тайм-ауте запроса?

Используйте стабильный идентификатор задачи приложения и границу идемпотентности с ограничением уникальности. При неоднозначном тайм-ауте сначала сверьте существующую задачу, а не отправляйте провайдеру новое письмо с новым идентификатором.

Означает ли успешный ответ email API, что письмо доставлено?

Нет. Обычно он означает, что API или провайдер принял запрос. Используйте последующие события, чтобы отличать доставку на сервер получателя, отказ, жалобу, задержку, отклонение и добавление в стоп-лист от первоначального приёма.

Какие DNS-записи нужны для email API?

Точный набор записей зависит от провайдера, но для отправки в продакшене обычно нужны подтверждение домена и DKIM, а также корректная стратегия SPF и политика DMARC, согласованная с легитимными потоками отправки.

Можно ли хранить API-ключи в коде для браузера?

Нет. Храните учётные данные рабочего пространства и провайдера в серверном хранилище секретов, по возможности храните API-ключи приложения в виде хешей, показывайте секреты целиком один раз и обеспечьте быстрый отзыв и ротацию.

Как email API должен обрабатывать постоянные отказы?

Нормализуйте событие провайдера, свяжите его с письмом в приложении и добавьте получателя в стоп-лист для обычных будущих отправок в пределах заданной области. Удаление из стоп-листа должно быть осознанным и подкреплённым доказательствами.

Источники