- Дата публикации
E2a: почта для AI-агентов с проверкой отправителя, WebSocket и HITL-контролем
Что нового
E2a — это open source‑шлюз электронной почты, который превращает email в нормальный транспорт для AI-агентов. Не просто SMTP-сервер, а прослойка с аутентификацией, WebSocket-доставкой и контролем действий агентов.
Ключевые новинки:
-
Аутентифицированная доставка писем для агентов
- Входящие письма проходят SPF/DKIM‑проверку.
- На каждую доставку E2a добавляет HMAC‑подписанные заголовки
X-E2A-Auth-*с информацией об отправителе, домене и хеше тела письма.
-
Два режима агентов: облачный и локальный
- Cloud‑режим: доставка через HTTPS‑webhook. Требуется публичный URL.
- Local‑режим: доставка через WebSocket + REST. Публичный URL не нужен, письма копятся как «непрочитанные», пока агент офлайн.
-
Полноценный outbound API для агентов
- Агенты отправляют письма другим агентам через SMTP‑relay E2a.
- Или людям — через внешний SMTP (SES, Resend и т.п.), который вы настраиваете в
config.yaml.
-
Human-in-the-loop (HITL)
- Опциональный «стоп-кран»: исходящие письма агента попадают в статус
pending_approval. - Ревьюер одобряет или отклоняет письма через дашборд, magic-link по email или CLI.
- Есть TTL и автоматическое действие по истечении времени: отправить (
expired_approved) или выкинуть (expired_rejected).
- Опциональный «стоп-кран»: исходящие письма агента попадают в статус
-
CLI и SDK для повседневной работы
- CLI
e2aс командами для регистрации агентов, чтения и отправки писем, HITL‑модерации. - SDK на TypeScript и Python, включая WebSocket‑клиент для локальных агентов.
- CLI
-
Хостинг или self-host без урезаний
- Hosted‑версия на e2a.dev с общим доменом
agents.e2a.dev— можно завестиmy-bot@agents.e2a.devбез настройки DNS. - Self-host использует те же возможности; нужно просто направить MX вашего домена на relay E2a и прописать
shared_domainвconfig.yaml.
- Hosted‑версия на e2a.dev с общим доменом
-
Подпись заголовков и защита от подмены
- Каждый входящий email получает HMAC‑подписанные заголовки с результатами SPF/DKIM, типом сущности (
human/agent), хешем тела и ID сообщения. - SDK по умолчанию не даёт читать поля письма, пока не проверена подпись — иначе бросает
UnverifiedEmailError.
- Каждый входящий email получает HMAC‑подписанные заголовки с результатами SPF/DKIM, типом сущности (
-
Встроенный трединг переписки
- На каждый
send/replyможно передаватьconversation_id. - E2a прокидывает его адресату через
payload.conversation_idи/или заголовокX-E2A-Conversation-Id. - Для людей в Gmail/Outlook используется стандартный разбор
In-Reply-ToиReferences.
- На каждый
-
Прозрачная модель безопасности и хранения данных
- Подробная политика хранения: 30 дней для входящих тел, scrub для исходящих тел после HITL‑финала, JSONB для вложений, хеши для API‑ключей.
- Ограничения webhook‑URL для защиты от SSRF, строгая проверка timestamp в подписях и DNS‑верификация доменов агентов.
Как это работает
Общая схема
E2a стоит между обычной почтой и вашими агентами:
- Человек пишет на адрес агента (Gmail, Outlook и т.п.).
- MX‑запись домена агента указывает на SMTP‑relay E2a.
- Relay принимает письмо по SMTP, проверяет SPF/DKIM и находит нужного агента.
- E2a подписывает служебные заголовки
X-E2A-Auth-*через HMAC-SHA256. - Доставка агенту:
- в cloud‑режиме — HTTP POST на
webhook_urlагента; - в local‑режиме — сохранение сообщения + нотификация по WebSocket.
- в cloud‑режиме — HTTP POST на
В обратную сторону:
- Агент вызывает HTTP API E2a для отправки письма (
send/reply). - Если включён HITL — письмо попадает в очередь на одобрение.
- После одобрения (или авто‑решения по TTL) E2a отправляет письмо:
- агенту — через свой SMTP‑relay,
- человеку — через настроенный внешний SMTP (SES, Resend и др.).
Режимы агентов
У каждого агента есть поле agent_mode:
cloud(по умолчанию) — доставка через HTTPS‑webhook. Нужен публичный URL, в проде — только HTTPS.local— агент подключается к WebSocket.../agents/{email}/wsи получает уведомления о новых письмах. Публичный URL не нужен, можно работать из локальной сети или за корпоративным фаерволом.
В обоих режимах агент может дополнительно опрашивать сообщения через REST‑API.
Подписанные заголовки X-E2A-Auth-*
Каждое доставленное письмо (webhook или WebSocket‑fetch) содержит набор заголовков, которые E2a подписывает:
X-E2A-Auth-Verified—true, если SPF или DKIM прошли.X-E2A-Auth-Sender— проверенный email отправителя или домен агента.X-E2A-Auth-Entity-Type—humanилиagent.X-E2A-Auth-Domain-Check— строка с результатом SPF/DKIM, напримерspf=pass; dkim=none.X-E2A-Auth-Delegation—agent={id};human={id}, если есть активная делегация.X-E2A-Auth-Timestamp— время в RFC3339.X-E2A-Auth-Message-Id— внутренний ID сообщения в E2a.X-E2A-Auth-Body-Hash— hex‑SHA256 от «сырых» байт письма.X-E2A-Auth-Signature— HMAC-SHA256 от канонической строки выше.
Подпись жёстко привязана к message_id и хешу тела. Если кто-то попробует подменить тело или использовать старую подпись для другого письма, проверка не пройдёт.
SDK по умолчанию заставляет проверять подпись. В Python:
from e2a.v1 import E2AClient
client = E2AClient() # читает E2A_API_KEY
email = client.parse_webhook(request_body) # читает E2A_WEBHOOK_SECRET; бросает ошибку при неверной подписи
# здесь уже безопасно читать поля
print(email.sender, email.subject)
В TypeScript:
import { E2AClient } from "@e2a/sdk";
const email = await client.parseWebhook(req.body); // бросит ошибку при неверной подписи
По умолчанию SDK берут секрет из E2A_WEBHOOK_SECRET. Можно передать секрет вторым аргументом, если вы храните его в другом месте.
Для сообщений, полученных через client.get_message(...), подпись уже считается проверенной, так как запрос аутентифицирован API‑ключом.
Трединг переписки
E2a помогает не терять контекст диалога:
- Методы
sendиreplyпринимаютconversation_id. - При доставке E2a добавляет его в
payload.conversation_idи в заголовокX-E2A-Conversation-Idдля e2a→e2a‑трафика. X-E2A-Conversation-Idучитывается только еслиMAIL FROM— домен relay E2a, поэтому внешние отправители не могут его подделать.- Для людей в Gmail/Outlook E2a использует заголовки
In-Reply-ToиReferences, но в рамках сообщений, которые получал именно этот агент.
Первое письмо от человека приходит с conversation_id: null — агент должен сам создать новый ID перед ответом.
Human-in-the-loop
HITL включается на уровне агента (hitl_enabled: true). Тогда любое исходящее письмо этого агента:
- Получает статус
pending_approval. - API отвечает
HTTP 202 Accepted, но письмо ещё не ушло. - Ревьюер должен одобрить или отклонить письмо:
- через дашборд или API:
POST /api/v1/messages/{id}/approveили/reject; - через magic-link по email (E2a шлёт письмо с ссылками
/api/v1/approve?token=...и/reject?token=...— нуженE2A_PUBLIC_URLи настроенный outbound SMTP); - через CLI:
e2a pendingдля списка и дальнейшие команды.
- через дашборд или API:
Если TTL истёк, E2a переводит письмо в expired_approved (авто‑отправка) или expired_rejected (удаление) в зависимости от hitl_expiration_action.
API и CLI
Все основные эндпоинты живут под /api/v1. Аутентификация — Authorization: Bearer <api_key>.
Исключения без авторизации: /api/health, /api/v1/info, /api/feedback и HITL magic-link.
Поверхность API закрывает:
- регистрацию и верификацию доменов;
- CRUD по агентам;
- входящие/исходящие сообщения;
- approve/reject для HITL;
- экспорт и удаление данных пользователя (GDPR‑стиль);
- WebSocket‑канал для локальных агентов.
CLI устанавливается так:
npm install -g @e2a/cli
e2a login
Дальше доступны команды:
# регистрация агента на общем домене
e2a agents register <slug> # создаст <slug>@<shared-domain>
e2a agents list # список агентов
e2a agents update <email> # обновление режима, webhook, HITL
e2a agents delete <email> # удаление агента
e2a listen # слушать письма по WebSocket в реальном времени
e2a listen --json # выводить по одному JSON на строку
e2a listen --forward <url> # пересылать каждое сообщение POST-запросом на локальный URL
e2a inbox # список последних сообщений
e2a read <id> # прочитать сообщение
e2a reply <id> --body … # ответить на сообщение
e2a send --to … --subject … --body … # отправить письмо
e2a pending # список писем, ожидающих HITL-одобрения
e2a config # просмотр и изменение настроек CLI
Режим listen --forward умеет форматировать письма под OpenAI Responses API и автоматически отвечать с помощью модели:
e2a listen \
--forward http://localhost:18789/v1/responses \
--forward-token <token>
Архитектура и деплой
Кто что настраивает:
- Оператор сервера: Go‑бэкенд, Postgres 14+, SMTP, OAuth и опциональный shared‑домен. Настройки —
config.yaml+ переменные окруженияE2A_*. - Пользователь CLI/SDK: только URL деплоя и логин (
E2A_URL+e2a login). - Деплойер веб‑дашборда: Next.js‑фронтенд, задаёт публичный URL и брендинг через
NEXT_PUBLIC_*.
Go‑бинарник работает на любом контейнерном хосте; хранилище — чистый Postgres. Outbound‑почта — обычный SMTP.
Большинство воркеров используют SELECT … FOR UPDATE SKIP LOCKED, поэтому горизонтальное масштабирование возможно. Нужно учитывать только in‑memory‑fanout WebSocket и пер‑процессные rate‑лимиты.
Что это значит для вас
Когда E2a полезен
-
Вы пишете агентов, которые должны общаться с людьми по email
Например, ассистент поддержки, который отвечает клиентам, или бота, который обрабатывает заказы из почты. E2a даёт:- проверку отправителя через SPF/DKIM;
- подписанные заголовки, которые легко проверять в коде;
- удобный трединг через
conversation_id.
-
Вы разрабатываете агентов локально или в закрытых сетях
Локальный режим с WebSocket снимает боль с публичными URL, ngrok и пробросами портов. Агент просто держит WebSocket‑подключение и получает JSON‑уведомления. -
Вам нужен контроль над тем, что агент отправляет наружу
HITL даёт простой способ не позволить модели отправить лишнее: человек просматривает письма, одобряет, отклоняет или даёт им истечь по TTL. -
Вы хотите единый слой идентичности для разных агентов и доменов
E2a агрегирует SPF/DKIM‑результаты, подписывает заголовки, управляет делегациями и доменами. Это упрощает безопасность по сравнению с набором разрозненных webhooks. -
Вы строите B2B-интеграции агент ↔ агент по email
Через общую схемуX-E2A-Conversation-Idи HMAC‑подписи можно надёжно связывать треды и проверять, кто именно с вами общается — человек или другой агент.
Когда E2a не нужен
- Вам достаточно просто отправлять транзакционные письма и иногда принимать входящие. В этом случае SendGrid/Resend/SES + собственный парсер webhook часто проще.
- Вы не работаете с агентами и не строите вокруг них коммуникацию по email. Тогда E2a будет лишним слоем.
Доступность и инфраструктура
E2a — open source, его можно развернуть где угодно: на собственном сервере, в любом облаке, в том числе в инфраструктуре, доступной из России.
Хостинг на e2a.dev зависит от сетевых ограничений и может потребовать VPN, если ваш провайдер блокирует соответствующие ресурсы.
Для продакшена потребуется:
- домен и доступ к его DNS для настройки MX и TXT‑записей;
- SMTP‑провайдер для исходящих писем людям (SES, Resend, Postmark и др.);
- Postgres 14+;
- Docker или другой способ запускать контейнеры.
Место на рынке
E2a не конкурирует напрямую с классическими email‑провайдерами вроде SendGrid, Resend, Postmark или SES. Они решают транспортную задачу: отправить и иногда принять письмо, плюс дать webhooks и шаблоны.
E2a занимает слой выше SMTP:
-
По сравнению с SendGrid/Resend/Postmark:
- те дают только webhook для входящих; E2a добавляет локальный режим с WebSocket и REST‑polling;
- они не являются почтовыми приёмниками (MX уходит на вашу инфраструктуру), поэтому не могут прозрачно управлять тредингом и
conversation_id— это нужно строить самостоятельно; - у них нет встроенного HITL с TTL и stateless magic‑link‑одобрением.
-
По сравнению с «голым» Postfix или Postal:
- Postfix/Postal — полноценные MTA, которые вы настраиваете сами. E2a использует
go-smtpи dial‑out, но добавляет:- нормализованный SPF/DKIM‑вердикт;
- HMAC‑подписанный контракт доставки;
- WebSocket‑транспорт для агентов;
- HITL‑флоу;
- SDK и CLI, заточенные под агентов.
- Postfix/Postal — полноценные MTA, которые вы настраиваете сами. E2a использует
Если вам нужен только SMTP‑транспорт, удобнее взять Postfix/Postal или управляемый сервис. Если вы строите агентную систему, которая плотно завязана на email, E2a закрывает именно этот сценарий.
Установка
Быстрый старт (self-host)
E2a требует Docker. Базовый запуск:
git clone https://github.com/Mnexa-AI/e2a.git
cd e2a
docker compose up -d
Контейнеры поднимаются в такой последовательности:
- Postgres (миграции выполняются автоматически).
- API‑сервер.
- Дашборд.
Открытые порты хоста:
:8080— HTTP API.:2525— SMTP‑relay.:3000— дашборд (Caddy + Next.js, проксирует/api/*на API‑сервер).
Проверка здоровья API:
curl http://localhost:8080/api/health
# {"status":"ok"}
Откройте http://localhost:3000 в браузере, чтобы зайти в дашборд. Для входа нужен Google OAuth, настроенный в config.yaml. Если нужно только API — дашборд можно пропустить и использовать bootstrap‑поток ниже.
Создание пользователя и API‑ключа
docker compose exec e2a \
e2a -config /etc/e2a/config.yaml -bootstrap-email you@example.com
# User: you@example.com (id=...)
# API key: e2a_...
Сохраните ключ — он показывается один раз.
Регистрация агента
KEY=e2a_...
curl -X POST http://localhost:8080/api/v1/agents \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"slug":"my-bot","agent_mode":"local"}'
curl -H "Authorization: Bearer $KEY" \
http://localhost:8080/api/v1/agents
Подключение реального домена
Чтобы агент получал письма из внешнего мира, настройте DNS:
A: your-domain.com → server IP
A: <relay-host> → server IP
MX: your-domain.com → your-domain.com (priority 10)
После этого зарегистрируйте и верифицируйте домен через API (см. раздел Domains в репозитории). Без DNS‑настроек API всё равно работает для тестов, но внешняя почта не попадёт в relay.
Обновления и миграции
docker-compose.yml монтирует migrations/ в init‑директорию Postgres. Скрипты выполняются только при первом старте, когда volume пустой.
При обновлении E2a и появлении новых миграций нужно применить их вручную:
docker compose exec postgres sh -c \
'for f in /docker-entrypoint-initdb.d/*.sql; do \
psql -U e2a -d e2a -f "$f" -v ON_ERROR_STOP=1; \
done'
Миграции идемпотентны (CREATE TABLE IF NOT EXISTS, ALTER TABLE … ADD COLUMN IF NOT EXISTS), поэтому повторный запуск безопасен.
SDK: примеры кода
Python
Установка:
pip install e2a # webhook-режим
pip install 'e2a[ws]' # добавляет поддержку WebSocket
Webhook‑режим:
from e2a.v1 import E2AClient
client = E2AClient() # читает E2A_API_KEY
email = client.parse_webhook(request_body) # парсинг + HMAC-проверка (читает E2A_WEBHOOK_SECRET)
print(email.sender, email.subject)
email.reply("Got it!", conversation_id="conv_123")
WebSocket для локальных агентов:
from e2a.v1 import AsyncE2AClient
async with AsyncE2AClient(api_key="e2a_…") as client:
async for notif in client.listen("bot@your-domain.com"):
# notif — лёгкая метаинформация, тело можно запросить отдельно
email = await client.get_message(notif.message_id)
await email.reply("Got it!")
TypeScript
Установка:
npm install @e2a/sdk
Дальнейшие примеры — в sdks/typescript/README.md в репозитории E2a.
Безопасность и данные
Безопасность
- Идентичность доменов: для кастомных доменов регистрация агента требует DNS‑TXT‑верификации владения.
- Проверка доменов отправителей: SPF и DKIM проверяются на каждом входящем письме.
- Подпись заголовков: HMAC-SHA256 по канонической строке auth‑заголовков, запросы с timestamp старше 5 минут отклоняются.
- Защита от SSRF: в продакшене webhook‑URL должны быть HTTPS, указывать доменное имя, резолвиться в публичные IP; запрещены raw‑IP, private/loopback‑диапазоны.
- OAuth CSRF: одноразовый, ограниченный по времени nonce в параметре
state. - Режим
E2A_ENV=productionвключает жёсткие проверки; dev‑режим более мягкий.
Входящие X-E2A-Auth-* из внешней почты E2a вычищает и подписывает заново. Это защищает от попыток подделать служебные заголовки.
Обработка данных
- Входящие конверты и тела писем хранятся в Postgres по умолчанию 30 дней.
- Исходящие тела очищаются после финального HITL‑перехода.
- Вложения лежат в JSONB‑строках, без S3/GCS.
- API‑ключи хранятся в виде хешей.
- Логи содержат адреса отправителя/получателя, но не содержат тела писем, вложения, сырые ключи или HMAC‑секреты.
Пользователь сам может выгрузить свои данные (GET /users/me/export) и удалить аккаунт (DELETE /users/me) — это покрывает сценарии GDPR Art. 15/17 и CCPA.
Полная таблица сроков хранения и полей логов описана в docs/data-handling.md.
FAQ: зачем это, если уже есть почтовые сервисы
Почему не ограничиться SendGrid/Resend/Postmark?
Четыре вещи, которые сложно достроить поверх них:
-
Локальные агенты без публичного URL
Агенты аутентифицируются по API‑ключу, открывают WebSocket.../agents/{email}/wsи получают письма как JSON. Никаких webhooks, ngrok и пробросов портов. SendGrid/Resend работают только через webhooks. -
Трединг с
conversation_idдля людей и агентов
Для людей E2a разбираетIn-Reply-To/Referencesи мапит ответы к исходным сообщениям агента. Для агент↔агент E2a использует контролируемый заголовокX-E2A-Conversation-Id, который нельзя подделать снаружи. -
Slug‑провиженинг на общем домене
Оператор задаётshared_domain: agents.e2a.dev. Пользователь шлёт{"slug": "my-agent"}и сразу получаетmy-agent@agents.e2a.devбез DNS‑настроек. Классические провайдеры не управляют MX‑зоной так глубоко. -
Встроенный HITL с TTL и stateless magic-link
E2a держит письма вpending_approval, автоматически рассылает magic‑link‑email и управляет TTL. В SendGrid/Resend это пришлось бы собирать из своей БД, таймеров и собственного UI.
При этом вы всё равно можете использовать SES/Resend/SendGrid как внешний SMTP для отправки писем людям — E2a для этого и даёт outbound_smtp в config.yaml.
Почему вообще email, а не webhooks/gRPC/MCP?
У каждого человека уже есть почтовый адрес и клиент. Webhooks и gRPC хорошо работают внутри вашей системы, но не доходят до Gmail/Outlook. E2a превращает email в мост между этим миром и кодом агента.
Что мешает атакующему подделать X-E2A-Auth-*?
E2a вычищает любые входящие X-E2A-Auth-* и подписывает свои. Подпись включает отправителя, результат верификации, ID сообщения и SHA‑256 тела. SDK проверяет подпись локально, без запроса в E2a. Если секрет утёк — его можно ротировать, и старые подписи перестанут проходить.
Не похоже ли это просто на SMTP с лишними шагами?
По сути да, но «лишние шаги» — это:
- нормализованный SPF/DKIM‑вердикт;
- HMAC‑подписанный контракт доставки;
- WebSocket‑доставка для агентов;
- HITL‑флоу;
- Conversation‑Id, который переживает переход email ↔ структурированные данные;
- slug‑провиженинг на общем домене;
- пер‑агентные настройки webhooks и HITL.
Построить это поверх чистого Postfix — отдельный проект. E2a и есть этот проект, но в готовом open source‑виде.
Зачем open source, если есть hosted‑версия?
Две причины:
- Аудит. Слой идентичности для агентов лучше держать в читаемом коде, а не в чёрном ящике. Можно проверить cosign‑подпись образа
ghcr.io/mnexa-ai/e2a, воспроизвести сборку и убедиться, что крутится именно он. - Реальный self-host. Hosted‑инстанс на e2a.dev использует тот же образ, что доступен всем. Удобства hosted‑версии — это конфигурация и DNS, а не закрытые фичи.
Цены на hosted‑версию пока не включены; когда появятся, включение будет через env‑переменную, без изменений в OSS‑коде.