mcpField
A
Agent Email List — Free SMTP Relay for AI Agents

Agent Email List — Free SMTP Relay for AI Agents

远程服务
ai.agentemaillist.com通信协作16 周前更新

关于 Agent Email List — Free SMTP Relay for AI Agents

面向 AI Agent 和开发者的邮件服务,提供 SMTP 中继、REST API、收件功能及 MCP 服务。Agent 可通过 MCP 无需凭据创建账户并取得 API 密钥;发送邮件前须验证域名 DNS,并遵循自动预热计划。

主要功能

  1. 01无需凭据即可通过 MCP 创建账户
  2. 02通过 SMTP 或 REST API 发送邮件
  3. 03将收件路由至 Webhook
  4. 04为每个域名签署 DKIM
  5. 05自动发送预热
  6. 06邮件投递事件与签名 Webhook

适用场景

  1. 01让 Agent 创建账户并获取 API 密钥
  2. 02通过 SMTP 或 REST 发送事务邮件
  3. 03将收到的邮件路由至 Webhook
  4. 04跟踪邮件投递事件

MCP 相关

远程 MCP 服务提供账户创建、发信域名管理、邮件发送、额度查询以及邮件和投递事件查看等工具。`create_account` 无需凭据,后续工具需使用返回的 API 密钥。

配置

远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。

服务地址https://ai.agentemaillist.com/mcp
一键复制安装命令
Claude CodeTerminal
Cursor.cursor/mcp.json
VS Code.vscode/mcp.json
Claude Desktopclaude_desktop_config.json
Windsurf~/.codeium/windsurf/mcp_config.json

命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。

工具

8
参数类型必填说明
namestring否可选的显示名称。
emailstring是账户的联系地址。
passwordstring是至少 12 个字符。仅用于登录网页控制台。

详细介绍

面向 AI Agent 的免费 SMTP 中继与电子邮件 API

AI Agent 可自行注册的电子邮件服务。 无需信用卡、无需销售人员致电,也无需拥有企业账户的人类。免费提供,并配有 MCP 服务、Mailgun 风格的 REST API、DKIM 签名、自动发送预热和收件功能。

现已上线:ai.agentemaillist.com · 源码可用 · 使用 Elixir 和 Phoenix 构建


面向 AI Agent:从这里开始

这是一个远程 MCP 服务。添加后,整个服务就会以工具的形式提供:

https://ai.agentemaillist.com/mcp

通过 HTTP POST 使用 JSON-RPC 2.0。重要的是:

create_account 无需凭据。 刚发现此服务的 Agent 可以调用它,获取 API 密钥,然后开始发送邮件。其他所有事务性电子邮件服务提供商都会让 Agent 卡在付款表单或身份验证环节,而这个服务不会。

在决定使用之前,先看看有哪些功能:

curl -X POST https://ai.agentemaillist.com/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
工具
工具功能是否需要密钥
create_account开设账户并获取 API 密钥否
add_domain注册发送域名并获取其 DNS 记录是
verify_domain重新读取 DNS 并激活域名是
send_email发送邮件,提供零成本的测试模式是
list_domains查看域名及每个域名今天可发送的邮件数量是
get_sending_limits查看今天的发送上限及提升上限的条件是
list_messages查看已发送和已接收的邮件是
get_delivery_events查看邮件实际发生的情况是

首次调用之后,每次调用都要在 Authorization: Bearer <key> 中发送密钥。

两件会让你无法继续的事

域名必须先在 DNS 中验证,才能发送邮件。 发布 DNS 通常需要拥有注册商访问权限的人类操作,因此要尽早开始。在此之前,每次发送都会被拒绝,并明确说明原因。

新验证的域名每天最初只能发送 10 封邮件,之后会随着信誉建立而逐步提升,因为一开始就以满负荷发送的域名会被接收方过滤。规划批量发送前,请先调用 get_sending_limits,不要等发送到一半才发现这一点。

首次发送时请使用 test_mode: true。它会运行完整流程,包括筛查,但不会发送任何邮件,也不会消耗每日额度。

了解限制,避免措手不及
限制数值
每个 IP 地址的账户数每小时 5 个,每天 20 个
每个账户的域名数3 个;任意一个域名验证后可增至 50 个
每个账户的 API 请求数每分钟 600 次——这是请求频率限制,而非发送限制
每个域名的发送量每天从 10 封起步,并沿预热阶梯逐步提升
筛查拒绝次数24 小时内达到 8 次时暂停发送,并自动解除

get_sending_limits 会实时报告以上所有限制,无需提供域名参数。每个 429 响应都会附带 Retry-After。开设第二个账户不会让你获得更多额度:账户按地址限制,因此两个账户会共用同一份额度。


面向开发者:REST API

采用 Mailgun 风格,因此只需更改基础 URL,大多数 Mailgun 客户端库就能直接使用。

# An account and an API key, in one request
curl -X POST https://ai.agentemaillist.com/v1/accounts \
  -d '[email protected]' -d 'password=a sufficiently long password'

# Send
curl -X POST https://ai.agentemaillist.com/v3/yourdomain.com/messages \
  --user 'api:YOUR_KEY' \
  -F from='[email protected]' \
  -F [email protected] \
  -F subject='Hello' \
  -F text='Hello there.'

完整 API 参考文档专为机器阅读而编写: ai.agentemaillist.com/llms.txt。 其中的发送限制由运行中的服务生成,因此反映的是你实际会遇到的限制,而不是一次性写定的数字。

此外还有一个 Web 控制台,可通过 ai.agentemaillist.com/signup 手动设置域名。


功能

  • 免费的 SMTP 中继和电子邮件 API。 没有试用期限,也无需信用卡。
  • 通过 SMTP 或 REST 发送。 添加域名即可获取 SMTP 凭据;REST API 采用 Mailgun 风格。
  • 也能接收邮件。 在端口 25 上运行真正的 SMTP 服务器,并将收到的邮件路由到你的 webhook。不只是发送邮件。
  • DKIM 签名,采用 RSA-SHA256 和宽松规范化,并为每个域名生成一对密钥。
  • 自动发送预热,公开的阶梯式额度从每天 10 封逐步提升至无限,让新域名建立信誉,而不是被过滤。
  • 双向内容筛查,拒绝外发邮件,并将收到的垃圾邮件归档。
  • 每封邮件的投递事件,以及签名 webhook。
  • 随着你证明自身可信度而逐步放开的限额,而不是固定上限:验证域名即可获得更多额度,因为这是一次性账号无法大规模伪造的事情。

与其他方案对比

本服务MailgunSendGridAmazon SES
Agent 可以独立注册可以不可以不可以不可以
MCP 服务有没有没有没有
免费额度免费,无期限有限试用后付费按邮件计费
接收邮件可以可以可以通过 S3
可自托管可以不可以不可以不可以

更详细的对比(包括 Brevo、Resend 和 SMTP2GO)请见 ai.agentemaillist.com/free-smtp-relay。

自托管

它由一个 Phoenix 应用和一个 Postgres 数据库组成。运行一条命令即可部署:

sudo bash deploy/deploy.sh --domain mail.yourcompany.com --email [email protected]

该命令会安装 Postgres 和 nginx、构建发布版本、签发 TLS 证书,并配置 systemd 和防火墙。DEPLOY.md 说明了它执行的每个步骤,以及某一步失败时该怎么做。

许可证

目前未授权,这意味着保留所有权利。如果你想使用或贡献此项目,请告知我们,我们会添加许可证。

由 Logan Besecker 维护。欢迎通过 [email protected] 或 [email protected] 联系我们,提出问题、报告错误或进行业务联系。


运行和运维

以下内容适用于运行自己副本的用户。

运行

mix setup            # deps, database, migrations
mix phx.server       # http://localhost:4005
mix test

默认监听端口 4005,因为与它一同运行的机器上,4000 到 4003 端口已被占用。可通过 PORT 覆盖。

数据库

在项目根目录的 .env 文件中设置一次凭据:

cp .env.example .env

每条 mix 命令都会读取该文件,因此无需在每条命令中重复输入;实际环境变量仍会覆盖它:DATABASE_URL=... mix test 的效果正如其表面所示。.env 已加入 gitignore;.env.example 列出了可写入其中的所有内容。

如果没有该文件,只要设置了 DATABASE_URL,就会优先使用它。这种方式在任何环境中都适用:

DATABASE_URL=ecto://user:pass@localhost/email_provider_dev mix setup

否则会依次使用标准变量 PGUSER、PGPASSWORD、PGHOST、PGPORT 和 PGDATABASE,最后才尝试使用与操作系统用户名同名的角色;标准 Homebrew Postgres 就是这样配置的。

如果操作系统用户名是 root,则会跳过该用户名。在服务器上,你通常是 root 用户,而 Postgres 中很少有名为 root 的角色。此时返回的错误——password authentication failed for user "root"——看起来像是凭据问题,实际原因却是没有人指定要使用哪些凭据。如果你在新服务器上遇到这种情况,可以设置 DATABASE_URL,或者创建该角色:

sudo -u postgres psql -c "CREATE ROLE youruser LOGIN PASSWORD 'apassword' CREATEDB;"

在实际部署中,请使用 MIX_ENV=prod 和 DATABASE_URL 运行发布版本,而不要运行 mix setup,因为它是开发任务。一条命令即可完成全部操作,DEPLOY.md 说明了它执行的每个步骤:

sudo bash deploy/deploy.sh --domain ai.agentemaillist.com --email [email protected]
在还运行其他服务的机器上

mix setup 是开发者命令,不适合与其他服务共用机器。请改用以下命令:

DATABASE_URL=ecto://user:pass@localhost/email_provider_dev bin/setup-server

这里有三处不同,每一处都能避免 mix setup 干扰机器上的其他服务。

限制构建资源。 编译 35 个依赖和两个 C NIF 时,Elixir 编译器会为每个调度器启动一个进程,make 则会为每个核心启动一个作业。在小型 VPS 上,构建会成为机器上占用内存最多的进程。内存耗尽时,内核不会终止构建;OOM killer 会挑选占用内存最大的进程,通常是正在运行的应用。该脚本会串行编译,并在以 root 身份通过 systemd 运行时,将构建放入具有硬性 MemoryMax 限制的 scope 中,因此因内存不足而被终止的只会是构建本身。可通过 MEMORY_MAX=1G 覆盖此设置。

只打开两个连接,而不是十个。 Postgres 的 max_connections 是固定的;连接数耗尽时,它会向所有客户端回复“抱歉,客户端连接数已满”,包括已经连接的服务。开发环境的连接池现在默认为 5,并读取 POOL_SIZE;该脚本将其设为 2。

绝不启动应用。 mix setup 会启动整个监督树来运行 priv/repo/seeds.exs,这会打开一个连接池并启动投递队列。mix setup.server 会创建并迁移数据库,但不会启动任何内容。

如果运行 mix setup 时已有服务下线,以下命令可以指出是哪一种情况:

sudo dmesg -T | grep -i -A2 'killed process'
sudo grep -i "too many clients" /var/log/postgresql/*.log | tail
关于 PGDATABASE 的说明

主机、用户名和密码从环境中读取,数据库名称则不是。数据库名称不是凭据,而是用于指定这些数据属于哪个应用;PGDATABASE 是标准的 libpq 变量,在共享机器上可能已为其他服务导出。若使用该变量,mix ecto.migrate 就会连接到该服务的数据库,并在其中创建本应用的表。要使用其他数据库,请在 DATABASE_URL 中指定其名称。

环境
变量含义默认值
OPENAI_API_KEY内容审核端点的密钥无 — 跳过筛查
SSA_UID用于采用情况报告的 SeriouslySimpleAnalytics 账户 ID未设置 — 不报告任何内容
MODERATION_ENABLED完全关闭筛查true
MODERATION_ON_ERROR筛查服务无法访问时,设为 block 以默认拒绝allow
SMTP_RELAY外发邮件的 SMTP 中继主机未设置 — 邮件写入 priv/local_mail
SMTP_PORT / SMTP_USERNAME / SMTP_PASSWORDSMTP 中继凭据587、无
SPF_HOST客户在 SPF include 中填写的主机mail.example.com
MX_HOST客户的 MX 记录指向的主机mx.example.com

未设置 SMTP_RELAY 时,不会有任何内容发送到互联网:本地发件器会将每封邮件写入磁盘并记录日志。这是有意设置的默认行为。

账户和密钥

curl -s localhost:4005/v1/accounts \
  -d [email protected] -d password='a sufficiently long password'

响应中包含一个 API 密钥。该密钥仅显示一次——系统只存储 SHA-256 哈希,因此密钥丢失后需要轮换,而不是查询找回。可以通过以下任一方式进行身份验证:

curl -s --user 'api:ep_live_...' localhost:4005/v3/domains     # Mailgun style
curl -s -H 'Authorization: Bearer ep_live_...' localhost:4005/v3/domains

密钥包含作用域(messages:send、events:read、domains:write 等)。每个控制器都会在各个操作中声明作用域,并紧邻受其保护的代码。

自定义域名

添加域名时会生成一对 RSA-2048 DKIM 密钥。私钥保留在数据库中;客户发布的是公钥。

curl -s --user 'api:KEY' localhost:4005/v3/domains -d name=mail.yourcompany.com

响应会列出需要发布的记录,然后:

curl -s -X PUT --user 'api:KEY' localhost:4005/v3/domains/mail.yourcompany.com/verify

在 DNS 中确认 SPF 和 DKIM 记录之前,域名处于 unverified 状态;只有状态变为 active 后才能发送邮件。如果之后这些记录消失,域名会恢复为 unverified 状态,并停止发送。系统会报告 MX 记录,但不会将其设为必需项——只有接收邮件时才需要 MX 记录。

外发邮件使用 RSA-SHA256 签名,并采用 relaxed/relaxed 规范化,因此中继即使重新折叠邮件头或删除尾随空格,也不会破坏签名。

发送

curl -s --user 'api:KEY' localhost:4005/v3/mail.yourcompany.com/messages \
  -F from='Ada <[email protected]>' \
  -F [email protected] \
  -F subject='Hello' \
  -F text='Hello there.' \
  -F o:tag=welcome \
  -F h:X-Campaign=spring \
  -F v:customer_id=42

from 地址必须属于该密钥发送邮件所用的域名。

端点

消息
POST /v3/:domain/messages发送。字段如下。
POST /v3/:domain/messages.mime发送预先构建的 MIME 文档。仍会经过筛查,仍会计数。
GET /v3/:domain/messages列出消息,可按 folder 和 direction 筛选。
GET /v3/domains/:domain/messages/:key获取一条已存储的消息。

发送字段:from、to、cc、bcc、subject、text、html、 o:tag、o:deliverytime、o:testmode、o:tracking-opens、 o:tracking-clicks、h:*(要发送的标头)、v:*(随消息传递并在事件中返回的变量)、recipient-variables、template、t:version、 t:variables。

o:deliverytime 接受 RFC 2822 或 ISO 8601 格式,且最晚只能安排在三天后。 o:testmode 会接受并存储消息,但不会发送,也不会消耗预热额度。recipient-variables 会将一个请求拆分为每位收件人一条消息,每条消息都有独立的正文和 Message-ID,并替换 %recipient.name%。

域名

GET|POST /v3/domains · GET|PUT|DELETE /v3/domains/:domain · PUT /v3/domains/:domain/verify

报告

GET /v3/:domain/events(可按 event、recipient、tag、begin、 limit 筛选)· GET /v3/:domain/stats/total · GET /v3/:domain/tags · GET /v3/:domain/limits(预热进度)

抑制列表

GET|POST /v3/:domain/bounces · GET|DELETE /v3/:domain/bounces/:address, unsubscribes 和 complaints 也采用相同的路径结构。每次发送时都会强制执行;发生永久退信时,会将该地址加入列表。

模板

GET|POST /v3/:domain/templates · GET|DELETE /v3/:domain/templates/:name · POST /v3/:domain/templates/:name/versions

替换语法只有 {{name}}——不支持表达式、包含或循环。存储的模板是多租户服务中由攻击者控制的输入,因此,模板引擎越简单,影响范围就越小。未知占位符会原样显示,而不会被置空,因此,拼写错误会显示为 {{frist_name}},而不是悄然消失。

路由(入站)

GET|POST /v3/routes · GET|PUT|DELETE /v3/routes/:id

表达式:match_recipient("regex")、match_header("name", "regex")、 catch_all()。操作:forward("https://…")、store()、stop()。按优先级从高到低执行,遇到 stop() 即停止。

Webhook

GET|POST /v3/domains/:domain/webhooks · GET|DELETE /v3/domains/:domain/webhooks/:id

负载使用 HMAC-SHA256(timestamp <> token, signing_key) 签名,并为每个域名配置独立密钥,因此,一个泄露的密钥无法伪造其他客户的回调。密钥仅在创建时返回一次。

地址验证

GET /v4/address/validate?address=… — 检查语法并实时查询 MX。它不会通过不完整的 SMTP 会话探测收件人的服务器:这正是验证结果准确的原因,也使其无法与目录枚举的侦察步骤区分开来。

运维仪表板

GET /admin — 服务级统计数据:账户、域名、收发消息数、永久退信、退订、投诉、筛查结果,以及已验证域名在各个预热阶段的分布情况。

使用 ADMIN_TOKEN 进行保护,而不是使用 API 密钥,因为这些数据涵盖所有账户,不应允许任何客户凭证访问。未设置令牌时,仪表板会拒绝打开;对于缺失的环境变量,这正是恰当的失败方式。页面本身无需令牌即可访问,且不包含统计数据;它会携带令牌请求 GET /admin/stats,因此,未经授权访问该 URL 时会看到提示,而不会看到任何统计数据。

有一项数据不在其中:被内容筛查拒绝的出站消息。它们会在存储前被拒绝,因此不会留下记录或事件。仪表板会对此明确说明,而不是悄然省略。

面向用户

/signup、/login,然后是 /domains、/send、/messages、/account。这些是普通的服务端渲染页面,每个页面都有独立 URL,方便链接和加入书签。

它们使用的上下文与 JSON API 相同,并非另行实现:通过表单添加域名和调用 POST /v3/domains 会执行相同的代码。会话中只保存用户 ID,不存储其他信息;每次请求都会查询用户,因此暂停账户后会立即生效,而不必等到会话过期。

这里使用 HEEx,而非着陆页使用的字符串模板,因为这些页面会渲染地址、域名和主题行。所有这些内容都由客户提供,~H 会在输出时对其进行转义。

面向 Agent

GET /llms.txt — 面向 Agent 的 API 说明,无需密钥。该文件由运行中的服务生成,因此其中的发送阶梯就是实际执行的阶梯,而不是写下一次后便逐渐过时的数字。测试会断言公布的阶梯与 EmailProvider.Warmup.stages/0 一致,并确认文件中列出的每个端点都确实已路由。

/robots.txt 指向该文件。

入站

POST /v1/inbound/:domain — MTA 将收到的邮件交给我们的入口,邮件可以是原始 message,也可以是已解析的字段。GET /v1/inbound/:domain/spam 会列出筛查后归档的内容。

自动预热

新域名如果一开始就以满负荷发送,邮件会被过滤,因此每个域名都要逐级攀升。阶段在 config/config.exs 中配置,而非硬编码:

阶段每日上限达到晋级条件
110已在 5 个不同日期发送过邮件
220在此阶梯发送了 1,000 封邮件
3100在此阶梯又发送了 1,000 封邮件
41,000在此阶梯又发送了 10,000 封邮件
5不限—

有两点值得注意:

“发送天数”指实际发送过邮件的天数,而非域名创建至今的天数。闲置一周的域名并没有预热一周。

每个阶梯的数量都是该阶梯自身的配额,而非累计总量。 域名在以每天 20 封的速度发送了 1,000 封后,才会离开每天 20 封的阶梯;之后在每天 100 封的阶梯再发送 1,000 封 才会继续晋级。达到这些数量时的累计总量,取决于域名在最初五天里发送得有多快:在第 1 阶梯达到上限的域名,已经发送了 50 封,因此各阶梯的晋级总量依次为 50、1,050、2,050 和 12,050;而发送较慢的域名则会更早达到每个阶梯。GET /v3/:domain/limits 会报告当前阶梯的进度,也就是回答“按当前上限还要多久?”的数字,同时还会报告晋级时的累计总量。

对于应在达到绝对总量时晋级的阶梯,请在 config/config.exs 中使用 {:lifetime_sent, n},而非 {:stage_volume, n}。

发送前会预留容量;如果发送未发生,则会释放预留。因此,在两者之间发生崩溃只会损失少量发送额度,而不会导致域名超额发送。预留通过单次原子 upsert 完成:两个同时到达的请求不会都读到“今天已发送 9 封”,然后都获准发送。GET /v3/:domain/limits 会报告当前阶梯、今天的剩余发送额度,以及晋级条件;被拒绝的发送会返回 429,并附带相同的详细信息和 retry_after_seconds。

可通过 warmup_enabled 按域名禁用预热。

内容筛查

每条消息都会通过 OpenAI 的审核端点进行筛查,该端点可免费调用。根据方向不同,处理方式也不同:

  • 发出 — 被标记的内容会在到达传输线路之前、且尚未消耗任何预热额度之前,以 403 拒绝。审核结果会存储在消息上,因此之后可以解释拒绝原因,而不是只在日志中留下一个 403。
  • 收到 — 被标记的内容仍会投递,但会存入 spam,而不是 inbox。直接丢弃收到的邮件会因误报而丢失真实消息;将其归档则不会。

如果无法访问审核服务,默认会放行并将消息标记为未筛查,这样第三方服务中断不会连带导致本服务停摆。设置 MODERATION_ON_ERROR=block 则会改为默认拒绝。“未筛查”与“已筛查且内容正常”是不同状态——前者没有检查时间戳,因此下游不会将两者混淆。

SMTP

本服务通过 SMTP 收发邮件。两个监听器默认都不会启用:接收邮件需要端口 25,直接发送邮件也需要出站端口 25;这两项都需要显式决定,而非默认开启。

发送

按优先级从高到低,有三种发送方式。

设置路由
SMTP_RELAY通过你选择的智能主机发送。要求使用 STARTTLS 并验证证书。
DIRECT_DELIVERY=true直接向每位收件人的 MX 发送,不经过智能主机。
两者皆无写入 priv/local_mail。邮件不会离开这台机器。

直接投递会按域名对收件人分组,查询每个域名的 MX 记录,并按优先级依次尝试;如果域名没有发布 MX 记录,则按 RFC 5321 的要求回退到该域名自己的 A 记录。目标服务器返回 5xx 时会终止尝试;其他情况则继续尝试下一台主机,因为这通常表示该主机无法访问,而非对方不想接收邮件。

这里的 TLS 安全要求有意低于连接智能主机时的要求:采用机会式加密,且不验证证书。智能主机是你选择的一台服务器,你可以要求它达到一定标准。开放互联网中有许多接收服务器使用自签名或不匹配的证书,而且事先并未约定要对这些证书进行验证;因此,强制验证并不会让投递更安全,只会导致投递无法正常工作。所有其他 MTA 都采用这种方式,RFC 7435 将其称为机会式安全。

大多数主机都会封锁出站端口 25,Vultr 也不例外。 在为这台机器解除封锁之前,所有直接投递都会超时。请联系支持团队解除封锁,或使用智能主机。

接收

SMTP_RECEIVE_ENABLED=true 会开放端口 25,并接收发往托管域名的邮件。SMTP_SUBMISSION_ENABLED=true 会开放端口 587,客户自己的软件可在此使用添加域名时发放的凭据进行身份验证,然后通过我们发送邮件。邮件提交会使用与 REST API 相同的处理流程,因此筛查、抑制列表和预热阶梯仍然适用。

防止成为开放中继的关键在于一个名为 handle_RCPT/2 的函数。在端口 25 上,只有当收件人所属域名由我们托管且处于启用状态时,才会接受该收件人;其他情况一律返回 550 5.7.1。尚未验证的托管域名则会返回 450,这样,客户完成 DNS 配置后,合法发件人可以重试,而不会被永久拒绝。只有会话通过身份验证后,才允许中继;这正是提交端口存在的全部目的,也是它绝不能使用端口 25 的原因。只有提交端口会公布 AUTH,因为在端口 25 上提供该功能,会让任何人都能从任何地方尝试猜测每位客户的 SMTP 密码。

端口 25 需要 root 权限或 CAP_NET_BIND_SERVICE。如果监听器无法绑定端口,会记录日志并跳过,而不会因此阻止应用启动:今天无法接收邮件的服务提供商仍应能够提供 API 并继续发送邮件。

此部署需要的 DNS 记录

ai.agentemaillist.com 已解析到这台机器。以下记录尚不存在,是域名验证和投递正常工作的必要条件:

类型名称值用途
TXTai.agentemaillist.comv=spf1 ip4:155.138.220.76 ~all让客户的 SPF 中的 include:ai.agentemaillist.com 授权这台机器
PTR155.138.220.76ai.agentemaillist.com在 Vultr 控制面板中设置,而非在 DNS 中设置;接收服务器会将其与 HELO 进行比对

根域名现有的 MX 指向 Namecheap 转发,用于将普通邮件转发到 @agentemaillist.com。此处所有内容都位于 ai. 子域名下,因此无需更改现有设置。

投递工作原理

投递会并发运行,并受 DELIVERY_CONCURRENCY 限制。投递过程几乎都在等待 DNS、TCP 连接,以及与互联网另一端服务器的通信。如果逐一处理,队列的速度就会受最慢的收件人限制;而一台服务器耗时 30 秒,就会让后续所有任务都停滞。并发上限不会超过 POOL_SIZE,因为每个正在进行的投递都会占用一个数据库连接。

消息表本身就是队列,无需单独的代理。这会带来每隔几秒轮询一次的开销,但能确保每条消息只有一个处理者:认领操作是受当前状态保护的 UPDATE,因此两个节点同时争抢同一条消息时,不可能同时成功;如果节点在发送过程中崩溃,过期认领清理任务会将相应记录放回队列。5xx 会永久发送失败、抑制该地址并退还其预热额度;4xx 最多重试 5 次。

Bcc 会写入信封,但绝不会写入邮件头,因此密送收件人不会被公开。

目录结构

lib/email_provider/
  accounts.ex          users, API keys, scopes
  domains.ex           registration, DKIM keys, DNS records, verification
  mail.ex              the send and receive pipeline
  warmup.ex            the ladder and its counters
  moderation.ex        OpenAI screening client
  suppressions.ex      bounces, unsubscribes, complaints
  templates.ex         stored bodies and substitution
  routes.ex            inbound matching and actions
  webhooks.ex          signed callbacks
  delivery/
    mime.ex            render RFC 5322
    dkim.ex            relaxed/relaxed RSA-SHA256 signing
    inbound.ex         parse MIME, decode RFC 2047 headers
    sender.ex          SMTP and local adapters
    queue.ex           claim and dispatch

测试

mix test

测试套件中的任何内容都无法访问网络:筛查已关闭,分发循环未运行,且发送器会将内容写入临时目录。测试时,config/runtime.exs 会跳过由环境变量驱动的配置,因此开发者机器上遗留的环境变量不会意外将测试套件指向正在运行的中继服务或收费 API。

账户档案

每个账户持有人都会获得一份约 200 字的滚动描述,每当邮件向任一方向流动时都会重写。描述由以下两项输入生成:

  • CSuiteFinder 丰富化查询:针对账户本身的地址,通过 /csuitefinder/email/enrich 发起。响应会缓存在档案记录中,有效期为 30 天,因为服务提供商按结果收费,而且一个人的职位不会在两封邮件之间发生变化。
  • 邮件活动摘要:邮件量和方向、首次和最近活动时间、每个活跃日的频率、主要通信对象、近期主题行、发件域名、标签,以及设有严格长度上限的近期邮件正文摘录。
curl -s --user 'api:KEY' localhost:4005/v1/profile
curl -s --user 'api:KEY' -X POST localhost:4005/v1/profile/refresh
curl -s --user 'api:KEY' localhost:4005/v1/profile/signals

存储的记录会保留描述,以及生成描述时所依据的丰富化记录和活动摘要;API 也会返回这三项。无法说明来源的描述,就无法用于采取行动、纠正或辩护。

两种生成器

EmailProvider.Profiles.Generator 会根据配置选择生成器。如果配置了 OpenAI 密钥,就会使用聊天模型,并提示模型以证据为依据:不得编造雇主或职位,对不确定的内容标注出处,也不得推断受保护特征。没有密钥时,则会退回到确定性构建器,直接根据事实组合段落,不使用模型,也不访问网络。测试套件运行的就是这种模式,因此测试覆盖的是实际路径而非模拟路径,而且该功能开箱即用。

确定性生成器只陈述数据所支持的内容,不作延伸,因此记录信息较少时,生成的描述也会较短,而不是为了凑长度而填充内容。只有模型适配器会以 200 字为目标。两种生成器的输出都会截断至 200 字;如果存在句子边界,则会在句子边界处截断。

成本

min_interval_seconds 默认值为 0,即每条消息重写一次;使用模型适配器时,也就是每条消息调用一次模型。这正是所要求的设置,也是成本最高的设置。在 config/config.exs 中调高该值,可合并繁忙账户的刷新操作;每条消息仍会计数,因此下次重写时会知道遗漏了多少内容。

这项工作在发送流程之外的任务中运行,因此无论是增强调用还是模型,都不会增加 API 请求的延迟或阻塞消息投递。失败会记录在对应行中,且不会向上抛出:无法重写的个人资料会保留原有描述。

范围

GET /v1/profile 仅返回调用账户自己的描述,不返回其他内容。没有任何端点会返回他人的描述,而且增强查询针对的是账户持有者自己的地址,而不是其通信对象的地址。

之前这里的说明提到我还没有实现这一功能。你确认需要后,我便按要求实现了。有两件事值得审慎决定,而不是默认处理,因为它们属于策略问题,而非代码问题:你的条款是否告知账户持有者此功能的存在,以及他们能否查看或关闭关于自己的信息。如果你需要,EmailProvider.Profiles.enabled?/0 和 /v1/profile 端点可用于实现后者。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。