Входящая почта · 22 сентября 2026 г.
Как получать почту через Amazon SES: S3 и Lambda
Готовый к продакшену конвейер входящей почты на Amazon SES, S3 и Lambda: безопасная обработка MIME, маршрутизация по тенантам, идемпотентность, повторные попытки и цепочки писем.
Самый короткий надёжный путь приёма почты в Amazon SES: подтвердите домен, направьте MX-запись на эндпоинт приёма SES, сохраняйте каждое принятое письмо в S3, а затем асинхронно вызывайте Lambda для разбора и сохранения. Действие S3 должно стоять перед действием Lambda. Используйте ID сообщения SES как ключ идемпотентности, маршрутизируйте по получателю из SMTP-конверта и отправляйте небезопасное содержимое в карантин, а не доверяйте заголовкам и вложениям.
Какую архитектуру строить
Используйте выделенный поддомен, например inbound.example.com, если только SES не должен принимать всю почту корневого домена. Так почта приложения отделена от ящиков сотрудников, а откат сводится к изменению DNS, а не к миграции почты.
Поток в продакшене выглядит так:
sender
-> SES inbound SMTP endpoint
-> active SES receipt rule
-> S3 raw-message object
-> asynchronous Lambda action
-> MIME parser and policy checks
-> application database and private attachment storage
Правила приёма (receipt rules) Amazon SES выполняют действия по порядку. AWS отдельно описывает паттерн «сначала S3, затем Lambda» для случаев, когда коду нужно тело письма. Прямое действие Lambda получает метаданные и отдельные заголовки, но не полное тело. Тело остаётся исходным MIME-объектом в S3 (концепции приёма почты в AWS SES).
Такое разделение полезно. Шаг, обращённый к SMTP, быстро сохраняет исходное письмо, а разбор, индексирование, уведомления и бизнес-логика выполняются уже после приёма. Временный сбой базы данных не должен заставлять почтовый сервер отправителя повторять SMTP-транзакцию.
1. Выберите поддерживаемый регион и подтвердите домен
Приём почты в SES доступен только в отдельных регионах AWS. Выберите регион из актуального списка эндпоинтов приёма SES и держите ресурсы SES, Lambda, SNS и KMS в этом регионе, если соответствующая документация AWS явно не допускает иного.
Создайте в SES доменную идентичность (domain identity) именно для того корневого домена или поддомена, который будет принимать почту. Для подтверждения домена нужно опубликовать DNS-записи, которые выдаёт SES. Подтверждение для приёма доказывает контроль над доменом; оно не связано с настройкой MX-маршрута, направляющего входящий трафик в SES (руководство AWS по подтверждению домена).
Для выделенного поддомена DNS-записи концептуально выглядят так:
inbound.example.com. MX 10 inbound-smtp.us-east-1.amazonaws.com.
Замените us-east-1 на выбранный регион. AWS указывает значение MX как 10 inbound-smtp.<region>.amazonaws.com (руководство AWS по MX-записи). Не направляйте корневой домен на SES, если люди по-прежнему получают на него почту через Google Workspace, Microsoft 365 или другого почтового провайдера.
Перед тестированием проверьте опубликованную запись через несколько резолверов:
dig MX inbound.example.com +short
Видимость в DNS доказывает лишь то, что маршрут опубликован. Прежде чем считать настройку завершённой, отправьте контрольное письмо на тестовый адрес и убедитесь, что SES его сохранил.
2. Сохраняйте исходное письмо до обработки
Создайте приватный бакет S3 с заблокированным публичным доступом, политикой жизненного цикла и максимально узкими разрешениями IAM. Затем создайте в SES правило приёма, условие получателя в котором соответствует вашему домену входящей почты или конкретным адресам.
Первое действие должно доставлять исходное письмо в S3. Префикс объектов вроде inbound/ упрощает ограничение правил хранения и политик доступа. SES сохраняет исходное, неизменённое MIME-содержимое. Сейчас AWS указывает максимум 40 МБ по умолчанию при сохранении писем в S3, тогда как у действия SNS, включающего полное письмо, лимит гораздо меньше — 150 КБ (действие приёма S3 в AWS). Именно из-за этой разницы в размере S3 — более безопасный вариант по умолчанию для реальных ответов и вложений.
Если вы включаете необязательную настройку KMS в действии приёма SES, внимательно изучите детали шифрования. Для этой функции SES использует шифрование на стороне клиента, а не обычное шифрование S3 на стороне сервера, поэтому читающий код должен расшифровывать объект совместимым клиентом. Не включайте её мимоходом, чтобы во время инцидента не выяснилось, что ваш парсер на Node не может прочитать сохранённые байты.
Разрешите SES запись только в нужный бакет и префикс. Дайте Lambda s3:GetObject только для того же места. Функции не нужны права на администрирование бакета.
3. Добавьте асинхронное действие Lambda
Поставьте действие Lambda после действия S3 в правиле приёма и используйте асинхронный вызов, если только функция не должна решать, продолжать ли SES обработку правила. AWS рекомендует асинхронное выполнение для обычной обработки, а синхронное — только для решений о потоке почты (действие приёма Lambda в AWS).
Назначенный SES mail.messageId также является ключом объекта в S3, если префикс не задан. Если префикс задан, добавьте его в начало. Приведённый ниже каркас на Node.js загружает исходное письмо и разбирает его. Включите в артефакт развёртывания @aws-sdk/client-s3 и поддерживаемый MIME-парсер, например mailparser, и закрепите их версии.
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { simpleParser } from "mailparser";
const s3 = new S3Client({});
const bucket = process.env.INBOUND_BUCKET;
const prefix = process.env.INBOUND_PREFIX || "inbound/";
export async function handler(event) {
for (const record of event.Records || []) {
const ses = record.ses;
const messageId = ses?.mail?.messageId;
const recipients = ses?.receipt?.recipients || [];
if (!messageId || !/^[A-Za-z0-9._-]+$/.test(messageId)) {
throw new Error("Missing or invalid SES message ID");
}
// claimOnce must be an atomic insert with a unique constraint.
if (!(await claimOnce(messageId))) continue;
try {
const object = await s3.send(new GetObjectCommand({
Bucket: bucket,
Key: `${prefix}${messageId}`,
}));
const raw = Buffer.from(await object.Body.transformToByteArray());
const parsed = await simpleParser(raw, {
skipHtmlToText: true,
skipTextToHtml: true,
});
await saveInboundMessage({
providerMessageId: messageId,
envelopeRecipients: recipients,
envelopeFrom: ses.mail.source,
headerMessageId: parsed.messageId || null,
inReplyTo: parsed.inReplyTo || null,
references: parsed.references || [],
subject: parsed.subject || "",
text: parsed.text || "",
html: parsed.html || null,
attachments: parsed.attachments,
receivedAt: ses.mail.timestamp,
});
await markComplete(messageId);
} catch (error) {
await releaseOrMarkFailed(messageId, String(error));
throw error;
}
}
}
Функции-заглушки обозначают хранилище конкретного приложения, но их контракт важен. claimOnce должна использовать ограничение уникальности в базе данных или условную запись по ID сообщения у провайдера. Чтение с последующей вставкой приводит к состоянию гонки. Храните состояние обработки, чтобы оператор мог различать processing, complete, quarantined и failed.
Маршрутизируйте по получателю из конверта, а не по заголовку To
Видимые поля To и Cc — это содержимое письма, заданное отправителем. В них может не оказаться фактического адресата из-за BCC, пересылки или намеренной манипуляции. Условия приёма SES используют получателей из SMTP-конверта, и AWS рекомендует последующим обработчикам брать получателей из уведомления SES, определяя, куда было доставлено письмо (концепции приёма почты в AWS).
Это различие предотвращает ошибку смешения тенантов. Если reply+tenant-a@inbound.example.com получает письмо, в видимом поле To которого указано tenant-b@example.com, маршрутизируйте его по проверенному сопоставлению адреса из конверта в приложении, а не по отображаемому заголовку.
Если адрес идентифицирует клиента или разговор, используйте случайный, неугадываемый токен ответа. Храните токен в хешированном виде, при необходимости ограничивайте срок его действия и отклоняйте адреса, не соответствующие активному рабочему пространству. Предсказуемая локальная часть вроде ticket-42 — прямое приглашение подбросить сообщения в чужую цепочку.
Разбирайте MIME как враждебный ввод
Email — это вложенный формат ввода, которому уже несколько десятилетий. RFC 5322 определяет заголовки и тело письма, а MIME добавляет составное содержимое и кодировки передачи (RFC 5322, RFC 2045). Используйте поддерживаемый парсер, а не разбивайте письмо по пустым строкам или границам самостоятельно.
Прежде чем делать содержимое доступным в продукте, введите ограничения:
- Ограничьте общий объём декодированных байтов, число вложений, размер отдельного вложения, глубину вложенности MIME и время разбора.
- Храните вложения приватно под сгенерированными именами объектов. Никогда не используйте имя файла отправителя как путь.
- Считайте заявленный тип содержимого и имя файла лишь подсказками. По возможности определяйте тип по содержимому.
- Никогда не выполняйте содержимое вложений. Проверяйте вложения антивирусом или отправляйте в карантин до скачивания.
- Очищайте HTML по строгому белому списку, по умолчанию блокируйте удалённые изображения и отображайте его в изолированном контексте. Для автоматического анализа предпочитайте текстовую версию.
- Не записывайте исходные тела писем, адреса, токены или содержимое вложений в обычные логи приложения.
SES может сообщать вердикты SPF, DKIM, DMARC, проверки на спам и вирусы, но AWS отмечает, что SES лишь предоставляет эти результаты, а не применяет автоматически вашу бизнес-политику. Решите, что делать при непройденных проверках: отклонять, отправлять в карантин или показывать с предупреждением. Успешная аутентификация идентифицирует домен по определённому механизму; она не делает содержимое безопасным и не доказывает, что письмо написал человек.
Сделайте повторы скучными
Асинхронный вызов Lambda может повторять неудачные выполнения функции, и AWS предупреждает, что повторная доставка возможна, даже если функция не вернула ошибку. Настройте назначение при сбое (on-failure destination) или очередь недоставленных сообщений (dead-letter queue) и оповещения о сбоях обработки (поведение повторов в AWS Lambda).
Идемпотентность должна охватывать каждый последующий побочный эффект:
- Вставляйте ID сообщения SES под ограничением уникальности.
- По возможности сохраняйте разобранное содержимое и связи цепочки в одной транзакции.
- Уведомления, создание тикетов или задачи для агентов помещайте в outbox с ключом из ID сообщения и типа действия.
- Помечайте запись завершённой только после успешных надёжных записей.
- Повторно обрабатывайте из исходного объекта S3, а не из записи в логе с потерями.
Если вы запускаете обработку по уведомлениям S3, а не через действие Lambda в SES, действует то же правило. Уведомления Amazon S3 рассчитаны на доставку «хотя бы один раз» и не гарантируют порядок (уведомления о событиях AWS S3).
Объединяйте письма в цепочки, не доверяя теме
Для подбора цепочки используйте разобранные поля Message-ID, In-Reply-To и References. Не объединяйте письма в цепочку только по теме, начинающейся с Re:. Кроме того, прежде чем что-либо связывать, убедитесь, что адрес из конверта или токен ответа относится к тому же рабочему пространству и разговору.
Для автоответов нужна отдельная политика. Распознавайте такие сигналы, как Auto-Submitted, и не допускайте циклов ответов. RFC 3834 рекомендует явно помечать автоматические ответы и вести себя консервативно (RFC 3834). Если ответ готовит ИИ-агент, отправка должна оставаться явным идемпотентным побочным эффектом. Требуйте подтверждения пользователя для неожиданных получателей, конфиденциального содержимого или действий за рамками исходного процесса поддержки или продукта. Получение письма не означает общего согласия на несвязанные маркетинговые рассылки.
Чек-лист для продакшена
- Receiving Region поддерживает входящую почту SES.
- Идентификатор домена подтверждён, а MX-запись разрешается корректно.
- Правило получения имеет узкое условие для получателя, а нужный набор правил активен.
- Действие S3 выполняется до асинхронной обработки Lambda.
- Бакет закрыт, доступ предоставлен по принципу наименьших привилегий, а срок хранения задокументирован.
- Для ID сообщения SES задано ограничение уникальности в базе данных.
- Маршрутизация использует получателей конверта, а не видимые заголовки
ToилиCc. - MIME, HTML, ссылки и вложения считаются недоверенным вводом.
- События с ошибкой достигают отслеживаемого назначения и могут быть воспроизведены повторно.
- Соответствия цепочек писем обеспечивают принадлежность рабочему пространству.
- Автоматические ответы имеют защиту от циклов, границы согласия и идемпотентность отправки.
- Контролируемый тест охватывает обычный текст, HTML, BCC, дублирующую доставку, большие вложения, некорректный MIME и сбой парсера.
SES напрямую хорошо подходит, если команде нужен нативный контроль в AWS и она готова отвечать за DNS, IAM, разбор MIME, изоляцию тенантов, хранение, обработку повторов и оперативные оповещения. Если вы хотите получить эти прикладные примитивы за более узким email API, SendHQ предоставляет адреса для входящей почты, хранение писем, цепочки и доступ в рамках рабочего пространства наряду с исходящими транзакционными письмами. В любом случае исходное письмо должно оставаться восстановимым, а каждое последующее действие — безопасным для повторной обработки.