Show HN: E2a – 面向 AI 智能体的开源邮件网关

Hacker News Top 工具

摘要

E2a 是一款开源邮件网关,支持 AI 智能体通过 webhook、WebSocket 或 HTTP API 安全地收发邮件。它具备 SPF/DKIM 验证功能,提供 TypeScript 和 Python SDK,并支持可选的人工介入审批。

我们在构建智能体系统时,希望将邮件作为触发器。于是我们决定将其抽离出来,做成一个独立的服务。<p>我们为自己的智能体系统所需并使用的主要邮件功能包括:<p>1. 邮件会话线程与智能体对话线程保持一致<p>2. 对外发邮件进行人工介入审核(尤其在测试阶段)<p>3. 几分钟内即可为智能体快速绑定或解绑邮箱地址<p>4. 本地智能体使用 WebSocket,云端智能体使用至少一次(at-least-once)交付的 webhook<p>尚未支持:DMARC(目前仅支持 SPF&#x2F;DKIM)、带作用域的 API 密钥、高可用&#x2F;多区域部署(目前为单 VM + 单 Postgres)、应用层邮件数据加密、合规认证(SOC 2&#x2F;HIPAA)。<p>GitHub: <a href="https:&#x2F;&#x2F;github.com&#x2F;Mnexa-AI&#x2F;e2a" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;Mnexa-AI&#x2F;e2a</a><p>托管版: <a href="https:&#x2F;&#x2F;e2a.dev&#x2F;" rel="nofollow">https:&#x2F;&#x2F;e2a.dev&#x2F;</a><p>欢迎任何反馈与贡献。
查看原文
查看缓存全文

缓存时间: 2026/05/11 21:55

Mnexa-AI/e2a 源码:https://github.com/Mnexa-AI/e2a

e2a — 面向 AI 代理的电子邮件

测试 (https://github.com/Mnexa-AI/e2a/actions/workflows/test.yml) 构建镜像 (https://github.com/Mnexa-AI/e2a/actions/workflows/build-image.yml) 许可证 npm @e2a/sdk (https://www.npmjs.com/package/@e2a/sdk) PyPI e2a (https://pypi.org/project/e2a/)

面向 AI 代理的已认证电子邮件网关。通过 Webhook 或 WebSocket 接收邮件,通过 HTTP API 发送邮件,并验证每个发件人的身份——无论是人类还是其他代理。

  • 已认证传输 — 入站邮件经过 SPF/DKIM 验证;每次投递均附带 HMAC 签名的 X-E2A-Auth-* 标头
  • 两种投递模式 — Webhook(云代理)或 WebSocket(本地代理,无需公开 URL)
  • 出站 API — 代理可发送给其他代理(SMTP 中继)或人类(上游 SMTP,如 SES、Resend)
  • 人在回路(Human in the Loop) — 可选择加入的审批网关,在审查者通过仪表板、魔法链接邮件或 CLI 批准前,暂存出站邮件
  • CLI + SDK — 提供 TypeScript 和 Python SDK,以及用于日常代理操作的 e2a CLI

使用方法

您可以使用托管实例或自行部署。

  • 托管版 — 在 e2a.dev (https://e2a.dev) 注册。包含共享域名 agents.e2a.dev,支持基于 slug 的快速入驻(无需配置 DNS)、仪表板及托管的送达率管理服务。
  • 自托管 — 请参阅 快速入门部署。所有功能均保持一致;共享域名的 slug 快捷方式仅需您将邮件域名指向您的中继,并在 config.yaml 中设置 shared_domain

工作原理

Human (Gmail/Outlook) │ ▼ SMTP ┌──────────────┐ │ e2a relay │ ← MX record for your agent domain points here │ │ │ 1. Verify │ ← SPF/DKIM check on the inbound message │ 2. Sign │ ← HMAC-signed X-E2A-Auth-* headers │ 3. Deliver │ └──────────────┘ │ ├──▶ Cloud-mode agent: HTTPS webhook POST │ └──▶ Local-mode agent: store + WebSocket notification │ ▼ e2a listen (CLI) or client.listen() (SDK) 入站流程:SMTP → SPF/DKIM 检查 → 代理查找 → HMAC 签名认证标头 → Webhook 或 WebSocket 投递。 出站流程:API 调用 → 可选的 HITL 暂存 → SMTP 中继(代理到代理)或上游 SMTP(代理到人类)。

快速入门

需要 Docker。

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

Postgres 会首先启动(自动运行迁移),接着是 API 服务器,最后是仪表板。 暴露的三个主机端口:

  • :8080 — HTTP API
  • :2525 — SMTP 中继
  • :3000 — 仪表板(Caddy + Next.js,将 /api/* 代理到 API 服务器)

健康检查:

curl http://localhost:8080/api/health
# {"status":"ok"}

在浏览器中打开 http://localhost:3000 查看仪表板。登录需要在 config.yaml 中配置 Google OAuth 凭据;若仅需进行 API 冒烟测试,可跳过仪表板并使用下方的引导流程。 创建您的第一个用户和 API 密钥(无需 OAuth):

docker compose exec e2a e2a -config /etc/e2a/config.yaml -bootstrap-email [email protected]
# User: [email protected] (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

要接收真实的入站邮件,请将域名的 MX 记录指向您的中继主机:

  • A: your-domain.com → 服务器 IP
  • MX: your-domain.comyour-domain.com(优先级 10) 然后通过 API 注册并验证该域名(参见 域名)。 即使没有 DNS,API 仍可用于测试——但外部邮件将无法到达您的中继。

升级与迁移。 Compose 文件将 migrations/ 挂载到 Postgres 的初始化目录,该目录仅在首次启动时运行(数据卷为空时)。当您升级 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 EXISTSALTER TABLE ... ADD COLUMN IF NOT EXISTS),因此重复运行是安全的。

核心概念

代理模式

代理在注册时通过 agent_mode 设置为以下两种模式之一:

模式投递方式是否需要公开 URL?
cloud(默认)HTTPS Webhook POST 至 webhook_url
localWebSocket 通知 + REST 拉取

本地模式代理在断开连接期间会累积“未读”消息;重新连接时,服务器会以 WebSocket 通知的形式将它们下发。 两种模式均可通过 REST API 轮询消息。

认证标头

通过 e2a 投递的每封邮件(Webhook 或 WebSocket 获取)均携带签名标头:

标头描述
X-E2A-Auth-Verified如果域名级认证(SPF 或 DKIM)通过,则为 true
X-E2A-Auth-Sender已验证的发件人电子邮件或代理域名
X-E2A-Auth-Entity-Typehuman(人类)或 agent(代理)
X-E2A-Auth-Domain-CheckSPF/DKIM 结果字符串(例如 spf=pass; dkim=none
X-E2A-Auth-Delegation如果存在活跃的分权绑定,则为 agent={id};human={id}
X-E2A-Auth-TimestampRFC3339 时间戳
X-E2A-Auth-Message-Id本次投递对应的内部 e2a 消息 ID
X-E2A-Auth-Body-Hash原始消息字节的十六进制 SHA-256 哈希
X-E2A-Auth-Signature对上述规范字符串的 HMAC-SHA256 签名

签名覆盖以下内容: verified \n sender \n entity_type \n domain_check \n delegation \n timestamp \n message_id \n body_hash MAC 同时绑定 message_id 和原始消息正文的 SHA-256 哈希。替换其中任何一项都会使签名失效,因此捕获了某次投递的攻击者无法在其他消息或修改后的正文上重放该认证声明。

验证签名

X-E2A-Auth-Verified 字段是 服务器的声明 ——任何能够访问您 Webhook URL 的人都可以设置它。为做出安全决策,请使用您账户的签名密钥之一 验证签名(可在仪表板的 Settings → Webhook signing secrets 管理,或通过 /api/v1/users/me/signing-secrets 管理)。 SDK 默认在验证后才开放字段访问权限——在未验证的 Webhook 载荷上访问 email.senderemail.subject 等会抛出 UnverifiedEmailError,从而避免您意外信任攻击者可控的字段。 一步到位的快捷方式:

from e2a.v1 import E2AClient
client = E2AClient() # reads E2A_API_KEY
email = client.parse_webhook(request_body) # reads E2A_WEBHOOK_SECRET; raises on bad signature
# safe to use email.sender, email.subject, ...
import { E2AClient } from "@e2a/sdk";
const email = await client.parseWebhook(req.body); // throws on bad signature

两种形式默认都从 E2A_WEBHOOK_SECRET 读取密钥;如果您将其存储在其他位置,可显式作为第二个参数传入。 底层验证步骤按顺序检查:body_hash 是否与原始消息字节匹配、HMAC 是否与规范认证字符串匹配,以及时间戳是否在 5 分钟的重放窗口内。 client.get_message(...) 返回的邮件已预先验证——承载令牌已对通道进行认证——因此字段访问可直接使用,无需验证步骤。(get_messages / listMessages 等列表端点返回轻量级摘要而非 InboundEmail,因此不适用此限制。)

会话线程

sendreply 均接受一个不透明的 conversation_id。e2a 在投递时通过 payload.conversation_id 将其传播给收件人,优先级顺序如下:

  1. X-E2A-Conversation-Id 标头 — e2a 到 e2a 通信的事实标准。仅当 SMTP 信封 MAIL FROM 源自本中继时才会被采纳,因此外部发件人无法伪造。
  2. In-Reply-To / References 查找 — 标准 RFC 5322 线程机制,范围限定在收件人代理自身的消息内。覆盖从 Gmail/Outlook 回复的人类用户。

人类首次联系时携带 conversation_id: null——代理在回复前应分配一个新的 ID。

人在回路(HITL)

当代理启用 HITL 时,出站 sendreply 调用不会立即发送。消息将以 pending_approval 状态存储,API 返回 HTTP 202 Accepted。 在投递前必须由审查者批准;否则,在可配置的 TTL 过后,消息将过期为 expired_approved(自动发送)或 expired_rejected(丢弃),具体取决于代理的 hitl_expiration_action。 审查者可通过以下方式批准或拒绝:

  • 仪表板 / APIPOST /api/v1/messages/{id}/approve/reject
  • 魔法链接邮件 — HITL 触发时自动发送;包含一键 GET /api/v1/approve?token=.../reject?token=... URL(需配置 E2A_PUBLIC_URL 和出站 SMTP)
  • CLIe2a pending 列出暂存的消息

通过 PUT /api/v1/agents/{email} 并设置 hitl_enabled: true 以及可选的 hitl_expiration_action 和 TTL 为代理启用 HITL。

API

除非另有说明,所有端点均位于 /api/v1 下。 认证方式为 Authorization: Bearer /api/health/api/v1/info/api/feedback 以及 HITL 魔法链接路由除外。 包含 @ 的路径参数(代理电子邮件)必须进行 URL 编码。 功能范围涵盖域名注册与验证、代理 CRUD、入站/出站消息、HITL 批准/拒绝(API 密钥或签名的魔法链接令牌)、GDPR 风格的导出与删除,以及供本地模式代理使用的 WebSocket 通道。 完整端点参考请参阅 docs/api.md,机器可读规范请参阅 web/public/openapi.yaml

CLI

npm install -g @e2a/cli
e2a login
命令描述
e2a agents register <email>注册 <email>e2a login 后会自动发现部署的共享域名并缓存至 ~/.e2a/config.json
e2a agents list列出您的代理
e2a agents update <email>更新代理(Webhook URL、模式、HITL)
e2a agents delete <email>删除代理
e2a listen通过 WebSocket 监听邮件(实时)
e2a listen --json每行输出一个完整消息的 JSON
e2a listen --forward <url>将每条消息作为 HTTP POST 转发至本地 URL
e2a inbox列出近期消息
e2a read <id>阅读消息
e2a reply --body ...回复消息
e2a send --to ... --subject ... --body ...发送邮件
e2a pending列出等待批准的 HITL 消息
e2a config查看或更新 CLI 配置

listen --forward 模式还支持通过 --forward-token 转发至 OpenAI Responses API,该模式将每封入站邮件格式化为 Responses 载荷,并使用模型的输出自动回复:

e2a listen --forward http://localhost:18789/v1/responses --forward-token <token>

完整参考请参阅 cli/README.md

SDK

Python

pip install e2a
# webhook mode
pip install 'e2a[ws]' # adds WebSocket support
from e2a.v1 import E2AClient
client = E2AClient() # reads E2A_API_KEY
email = client.parse_webhook(request_body) # parse + HMAC-verify (reads 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("[email protected]"):
        # notif is lightweight metadata — fetch the body when you want it
        email = await client.get_message(notif.message_id)
        await email.reply("Got it!")

请参阅 sdks/python/README.md

TypeScript

npm install @e2a/sdk

请参阅 sdks/typescript/README.md

部署

三类受众分别配置不同的界面:

受众配置内容位置
服务器运维人员 — 运行 Go 后端数据库、签名密钥、SMTP、OAuth、可选的共享域名config.yaml + E2A_* 环境变量
CLI / SDK 用户 — 从本机调用 API仅需部署 URL(及登录凭据)E2A_URL + e2a login
Web 仪表板部署者 — 托管 Next.js 仪表板公开站点 URL + 品牌信息NEXT_PUBLIC_* 构建时环境变量

Go 二进制文件可在任何容器主机上运行;存储采用原生 Postgres 14+;出站邮件通过标准 SMTP 发送。 大多数工作进程通过 SELECT ... FOR UPDATE SKIP LOCKED 进行协调,因此多副本部署是安全的——真正的水平扩展限制仅在于内存中的 WebSocket 扇出和每进程速率限制。 完整环境变量参考、共享域名 DNS 配置以及扩展/限制说明,请参阅 docs/deployment.md

安全性

  • 身份 — 代理注册需要 DNS TXT 验证域名所有权(自定义域名)
  • 域名认证 — 每封入站邮件均检查 SPF 和 DKIM
  • 标头签名 — 对规范认证标头字符串进行 HMAC-SHA256 签名;若时间戳超过 5 分钟则拒绝
  • SSRF 防护 — Webhook URL 必须使用 HTTPS(生产环境),解析为公共 IP,并使用域名(禁止原始 IP、私有/回环地址段)
  • OAuth CSRF 防护state 参数中包含单次使用、限时有效的 nonce
  • 生产模式 (E2A_ENV=production) 强制执行上述策略,而开发模式则更为宽松

请私下报告安全问题——披露流程及范围请参见 SECURITY.md请勿在 GitHub 上公开发布漏洞 Issue。

数据处理

消息信封和入站正文默认在 Postgres 中保留 30 天;出站正文在最终 HITL 转换时被清除;API 密钥以哈希形式存储;附件存入 JSONB 行(不使用 S3/GCS)。 应用日志包含发件人/收件人地址(标准 MTA 做法),但绝不包含正文、附件、原始密钥或 HMAC 密钥。 用户可自助导出(GET /users/me/export)和自助删除(DELETE /users/me),以符合 GDPR 第 15 条/第 17 条及 CCPA 要求。 完整保留策略表、日志字段、用户权利端点以及运维方责任(备份、TLS、静态加密、日志脱敏、合规),请参阅 docs/data-handling.md

常见问题

为什么不直接使用 SendGrid / Resend / Postmark 进行发送,并利用它们的入站解析功能进行接收?

以下四点无法在不进行大量重构的情况下简单附加实现:

  1. 无需公开 URL 的本地模式代理。 代理使用 API 密钥认证,打开指向 /api/v1/agents/{email}/ws 的 WebSocket,入站邮件通过该连接以 JSON 形式到达——无需 Webhook URL、ngrok 或端口转发。适用于部署在开发者笔记本、边缘设备或企业防火墙后的代理。SendGrid/Resend 设计上仅支持 Webhook。可作为备用方案提供轮询 REST API。
  2. 每次回复均保持会话线程。 无论人类是从 Gmail 回复,还是其他 e2a 代理通过 API 回复,入站消息到达代理时都已附带稳定映射到原始线程的 conversation_id。对于人类发件人,中继会在收件人代理自身的消息范围内执行标准的 In-Reply-To / References 查找。对于双方均使用 e2a 的代理间通信,它还会信任其控制的 X-E2A-Conversation-Id 标头(信封发件人为其自身域名),该标头不会因客户端重写线程标头而失效。SendGrid/Resend 从不接触入站邮件——它们并非接收方——因此若不自行构建,这两种路径均不可用。
  3. 共享域名上的 slug 分配。 运维人员设置 shared_domain: agents.e2a.dev,用户通过 POST {"slug": "my-agent"} 即可立即获得 [email protected],无需任何 DNS 配置。这之所以可行,是因为 e2a 本身 就是声明该域名的 SMTP 中继——Resend / SendGrid 是服务提供商而非平台,若不自行运行中继,它们无法在共享地址空间中进行多租户划分。
  4. 内置 HITL 暂存 + 自动过期。 每个代理可配置 hitl_e

相似文章

潜客外联代理

YouTube AI Channels

OpenAI 展示了 Spark,一个由 ChatGPT 驱动的办公代理,可自主调研、评分并通过邮件跟进销售线索,同时自动更新 CRM。