Дата публикации
ai_products

E2a: почта для AI-агентов с проверкой отправителя, WebSocket и HITL-контролем

Что нового

E2a — это open source‑шлюз электронной почты, который превращает email в нормальный транспорт для AI-агентов. Не просто SMTP-сервер, а прослойка с аутентификацией, WebSocket-доставкой и контролем действий агентов.

Ключевые новинки:

  1. Аутентифицированная доставка писем для агентов

    • Входящие письма проходят SPF/DKIM‑проверку.
    • На каждую доставку E2a добавляет HMAC‑подписанные заголовки X-E2A-Auth-* с информацией об отправителе, домене и хеше тела письма.
  2. Два режима агентов: облачный и локальный

    • Cloud‑режим: доставка через HTTPS‑webhook. Требуется публичный URL.
    • Local‑режим: доставка через WebSocket + REST. Публичный URL не нужен, письма копятся как «непрочитанные», пока агент офлайн.
  3. Полноценный outbound API для агентов

    • Агенты отправляют письма другим агентам через SMTP‑relay E2a.
    • Или людям — через внешний SMTP (SES, Resend и т.п.), который вы настраиваете в config.yaml.
  4. Human-in-the-loop (HITL)

    • Опциональный «стоп-кран»: исходящие письма агента попадают в статус pending_approval.
    • Ревьюер одобряет или отклоняет письма через дашборд, magic-link по email или CLI.
    • Есть TTL и автоматическое действие по истечении времени: отправить (expired_approved) или выкинуть (expired_rejected).
  5. CLI и SDK для повседневной работы

    • CLI e2a с командами для регистрации агентов, чтения и отправки писем, HITL‑модерации.
    • SDK на TypeScript и Python, включая WebSocket‑клиент для локальных агентов.
  6. Хостинг или self-host без урезаний

    • Hosted‑версия на e2a.dev с общим доменом agents.e2a.dev — можно завести my-bot@agents.e2a.dev без настройки DNS.
    • Self-host использует те же возможности; нужно просто направить MX вашего домена на relay E2a и прописать shared_domain в config.yaml.
  7. Подпись заголовков и защита от подмены

    • Каждый входящий email получает HMAC‑подписанные заголовки с результатами SPF/DKIM, типом сущности (human/agent), хешем тела и ID сообщения.
    • SDK по умолчанию не даёт читать поля письма, пока не проверена подпись — иначе бросает UnverifiedEmailError.
  8. Встроенный трединг переписки

    • На каждый send/reply можно передавать conversation_id.
    • E2a прокидывает его адресату через payload.conversation_id и/или заголовок X-E2A-Conversation-Id.
    • Для людей в Gmail/Outlook используется стандартный разбор In-Reply-To и References.
  9. Прозрачная модель безопасности и хранения данных

    • Подробная политика хранения: 30 дней для входящих тел, scrub для исходящих тел после HITL‑финала, JSONB для вложений, хеши для API‑ключей.
    • Ограничения webhook‑URL для защиты от SSRF, строгая проверка timestamp в подписях и DNS‑верификация доменов агентов.

Как это работает

Общая схема

E2a стоит между обычной почтой и вашими агентами:

  1. Человек пишет на адрес агента (Gmail, Outlook и т.п.).
  2. MX‑запись домена агента указывает на SMTP‑relay E2a.
  3. Relay принимает письмо по SMTP, проверяет SPF/DKIM и находит нужного агента.
  4. E2a подписывает служебные заголовки X-E2A-Auth-* через HMAC-SHA256.
  5. Доставка агенту:
    • в cloud‑режиме — HTTP POST на webhook_url агента;
    • в local‑режиме — сохранение сообщения + нотификация по WebSocket.

В обратную сторону:

  1. Агент вызывает HTTP API E2a для отправки письма (send/reply).
  2. Если включён HITL — письмо попадает в очередь на одобрение.
  3. После одобрения (или авто‑решения по 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-Verifiedtrue, если SPF или DKIM прошли.
  • X-E2A-Auth-Sender — проверенный email отправителя или домен агента.
  • X-E2A-Auth-Entity-Typehuman или agent.
  • X-E2A-Auth-Domain-Check — строка с результатом SPF/DKIM, например spf=pass; dkim=none.
  • X-E2A-Auth-Delegationagent={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). Тогда любое исходящее письмо этого агента:

  1. Получает статус pending_approval.
  2. API отвечает HTTP 202 Accepted, но письмо ещё не ушло.
  3. Ревьюер должен одобрить или отклонить письмо:
    • через дашборд или 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 для списка и дальнейшие команды.

Если 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 полезен

  1. Вы пишете агентов, которые должны общаться с людьми по email
    Например, ассистент поддержки, который отвечает клиентам, или бота, который обрабатывает заказы из почты. E2a даёт:

    • проверку отправителя через SPF/DKIM;
    • подписанные заголовки, которые легко проверять в коде;
    • удобный трединг через conversation_id.
  2. Вы разрабатываете агентов локально или в закрытых сетях
    Локальный режим с WebSocket снимает боль с публичными URL, ngrok и пробросами портов. Агент просто держит WebSocket‑подключение и получает JSON‑уведомления.

  3. Вам нужен контроль над тем, что агент отправляет наружу
    HITL даёт простой способ не позволить модели отправить лишнее: человек просматривает письма, одобряет, отклоняет или даёт им истечь по TTL.

  4. Вы хотите единый слой идентичности для разных агентов и доменов
    E2a агрегирует SPF/DKIM‑результаты, подписывает заголовки, управляет делегациями и доменами. Это упрощает безопасность по сравнению с набором разрозненных webhooks.

  5. Вы строите 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, заточенные под агентов.

Если вам нужен только SMTP‑транспорт, удобнее взять Postfix/Postal или управляемый сервис. Если вы строите агентную систему, которая плотно завязана на email, E2a закрывает именно этот сценарий.

Установка

Быстрый старт (self-host)

E2a требует Docker. Базовый запуск:

git clone https://github.com/Mnexa-AI/e2a.git
cd e2a
docker compose up -d

Контейнеры поднимаются в такой последовательности:

  1. Postgres (миграции выполняются автоматически).
  2. API‑сервер.
  3. Дашборд.

Открытые порты хоста:

  • :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?
Четыре вещи, которые сложно достроить поверх них:

  1. Локальные агенты без публичного URL
    Агенты аутентифицируются по API‑ключу, открывают WebSocket .../agents/{email}/ws и получают письма как JSON. Никаких webhooks, ngrok и пробросов портов. SendGrid/Resend работают только через webhooks.

  2. Трединг с conversation_id для людей и агентов
    Для людей E2a разбирает In-Reply-To/References и мапит ответы к исходным сообщениям агента. Для агент↔агент E2a использует контролируемый заголовок X-E2A-Conversation-Id, который нельзя подделать снаружи.

  3. Slug‑провиженинг на общем домене
    Оператор задаёт shared_domain: agents.e2a.dev. Пользователь шлёт {"slug": "my-agent"} и сразу получает my-agent@agents.e2a.dev без DNS‑настроек. Классические провайдеры не управляют MX‑зоной так глубоко.

  4. Встроенный 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‑версия?
Две причины:

  1. Аудит. Слой идентичности для агентов лучше держать в читаемом коде, а не в чёрном ящике. Можно проверить cosign‑подпись образа ghcr.io/mnexa-ai/e2a, воспроизвести сборку и убедиться, что крутится именно он.
  2. Реальный self-host. Hosted‑инстанс на e2a.dev использует тот же образ, что доступен всем. Удобства hosted‑версии — это конфигурация и DNS, а не закрытые фичи.

Цены на hosted‑версию пока не включены; когда появятся, включение будет через env‑переменную, без изменений в OSS‑коде.


Читайте также

🔗 Источник: https://github.com/Mnexa-AI/e2a