tva
← Insights

为项目专属 AI 助理配置独立邮箱

你构建的项目专属 AI 助理,早晚需要一个电子邮件地址。我们在通过 Telegram 搭建项目专属 AI 助理一文中描述的 Telegram 机器人模式处理的是实时对话,但邮件是另一个维度的沟通渠道——服务通知、供应商确认、OAuth 密码重置、与不使用 Telegram 的人员进行后台往来。你可以把这些邮件转发到私人邮箱,忍受那些杂乱的噪音;也可以给项目配一个专属子域名上的独立邮箱。

本指南介绍第二种方案:在项目子域名上配置专属邮箱,并干净地完成 DKIM、SPF 和 DMARC 设置。这套方案将 DNS 和邮件系统分别交给不同的提供商——我们用 Cloudflare 管理 DNS,用 Opalstack 提供邮件服务。这也是大多数小型运营团队的实际选择:DNS 放在主域已有的服务商那里,邮件放在 IMAP 稳定、API 友好、口碑能经得住 Gmail 过滤器考验的那家。我们不会为了追邮件而迁移 DNS,也不会为了追 DNS 而迁移邮件。分开维护的额外成本,不过就是三条 DNS 记录加上一个清晰的分工模型:DNS 提供商告诉世界邮件该去哪里;邮件提供商负责接收、存储和签名。

整个配置流程只需四次 API 调用加三条 DNS 记录。做好预检后,端到端大概十分钟搞定。我们会展示具体的调用方式、验证步骤,以及第一次操作时踩过的那些坑。

本指南解决的问题

  • 项目子域名(system.example.com)尚未附加任何邮件基础设施
  • 发往项目专属地址的邮件因缺少 MX 记录而遭到退信
  • 项目对外发出的邮件无法通过 DKIM 校验,被 Gmail 投入垃圾箱
  • SPF 和 DMARC 记录已存在,但没有任何实际的签名机制与之对应
  • AI 助理需要独立的邮件渠道,而不必通过你的个人邮箱中转

所需准备

  • 你持有的主域名,DNS 托管在支持 API 操作的提供商(Cloudflare、Route53、DigitalOcean DNS 等)
  • 支持通过 API 添加域名、在域名创建时自动生成 DKIM 密钥、并提供 IMAP/SMTP 端点的邮件提供商(Opalstack、Mailcow、Migadu、Fastmail 等)
  • 两家提供商的 API token,存放在可以执行 shell 命令的地方
  • digopenssl s_client 以及一个 Python 运行环境,用于验证
  • 大约十五分钟

为何采用提供商分离的方案

默认思维是「一家搞定所有事」——网站托管、DNS 和邮箱放在同一个地方。这套逻辑能用,直到三者中的某一个需要独立演进为止。我们在自托管决策:何时 SaaS 比自建基础设施更贵一文中详细讨论了在捆绑服务和专项服务之间如何取舍,同样的逻辑在这里以更小的规模再次适用。

在我们的情况中,主域的 DNS 放在 Cloudflare,是因为我们需要它的 AnyCast 解析、公开站点的代理加速以及自动化记录管理的 API。邮件系统放在 Opalstack,是因为他们的按邮箱计费极为低廉,IMAP 服务器口碑良好,而且在欧洲邮件托管领域 API 算是比较规范的。我们不会为了追邮件迁移 DNS,也不会为了追 DNS 迁移邮件。分开运营的成本,就是三条 DNS 记录加上一个清晰的职责划分:DNS 提供商告诉世界邮件该送往何处;邮件提供商负责接收、存储和签名。

从运营层面看,这种分离能有效收窄故障影响范围。DNS 提供商故障会影响新的邮件路由决策,但已在途的邮件仍会继续送达 MX 主机。邮件提供商故障会阻断投递,但 DNS 查询依然正常响应。如果把所有环节都压在同一家提供商,两层就会同时崩掉。同样的逻辑在我们讲反向代理架构的文章生产环境中的反向代理层叠架构中也有体现——关注点分离,每一层都可以独立替换。

专属邮箱、转发别名,还是 SaaS 收件箱

在动手之前,先想清楚需要哪个层级。三种选择并不等价。

专属邮箱是真实的收件箱,有存储空间、有 IMAP 凭据,既能收邮件也能发邮件。成本:在自托管邮件提供商那里大概每月一到三欧元,外加 API 配置的工作量。这是 AI 助理需要真正读取邮件时的选择——解析供应商确认邮件、处理服务通知、与人类对接方进行多轮邮件往来。

转发别名是虚拟地址,只做重定向,把邮件转发到已有的收件箱。成本:零,纯配置。当 AI 助理只需要在项目品牌地址上接收邮件,而实际的阅读和处理都发生在人类已有的收件箱中时,这是正确选择。代价是:助理无法通过 IMAP 访问邮件、目标端无法按项目过滤、项目结束后也没有干净的清理路径——别名清理这件事往往会被遗忘。

SaaS 收件箱是指使用 Postmark、Mailgun Inbound 或 AWS SES 的接收规则等服务,按消息计费,并需要将 AI 助理接入 webhook 接收器。当助理需要处理大量邮件、按邮件头规则路由,或者每封邮件都需要触发自动化工作流时,这是正确选择。对于一个每月只处理几十到几百封邮件的项目专属助理来说,这个方案大材小用。

对于Telegram AI 助理那篇文章的使用场景——一个运营者、两位项目协作者、一个基于 Claude 的机器人——专属邮箱是最合适的。助理拥有一个可以用 IMAP 读取、用 SMTP 发送的真实地址,随项目而生,随项目而止,清清爽爽。

第一步:在邮件系统中注册子域名

邮件提供商必须先知道你的项目子域名是它的域名,才会接受发往该域名的邮件。这一步调用同时也会生成 DKIM 密钥对。

POST https://my.mailhost.example/api/v1/domain/create/
Authorization: Token <YOUR_MAIL_API_TOKEN>
Content-Type: application/json

[{"name": "system.example.com"}]

响应中有三个字段需要关注:域名 UUID(后续清理和地址映射时需要)、state(通常先是 PENDING_CREATE,几秒后变为 READY),以及 dkim_record 字段——这是一个格式完整的 TXT 值,可以直接粘贴到 DNS:

{
  "id": "<DOMAIN_UUID>",
  "state": "PENDING_CREATE",
  "name": "system.example.com",
  "dkim_record": "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4G..."
}

轮询直到 state 变为 READY,然后取出 dkim_record 的值,下一步会原封不动地填进 DNS 提供商。如果创建调用返回 invalid hostname 错误,最常见的原因是邮件提供商只支持二级域名,不接受三级子域名,这时提交工单即可——实际操作中我们还没遇到过拒绝这种请求的提供商。

继续之前,先确认 DKIM 密钥长度。dkim_record 值在 250 字节以下的是 1024 位 RSA 密钥;2048 位密钥通常在 380 到 420 字节之间。NIST 在 2013 年就已弃用 1024 位 RSA,Gmail、Microsoft、Yahoo 和 AWS SES 均将 2048 位视为 2026 年的基准标准——1024 位密钥虽然仍能通过认证,但在企业级垃圾邮件过滤器中会被视为弱信号。如果提供商允许选择密钥长度,选 2048;默认只支持 1024 位的提供商,本身就值得重新考量。

第二步:配置 DNS 记录

三条记录,全部在 DNS 提供商处为项目子域名创建。任何一条都不能通过 Cloudflare 的边缘代理——MX 和 TXT 记录必须设为 DNS-only。如果你在 API 中将 proxied: true,Cloudflare 会静默回退为 DNS-only,但明确设置会更清晰。

两条 MX 记录告诉发件方邮件应投递到哪里。大多数邮件提供商运行两台或多台 MX 主机,设置相同优先级以实现轮询故障转移:

POST https://api.dnsprovider.example/zones/<ZONE_ID>/dns_records

{"type":"MX","name":"system.example.com",
 "content":"mx1.de.mailhost.example","priority":10,
 "ttl":3600,"proxied":false}

{"type":"MX","name":"system.example.com",
 "content":"mx2.de.mailhost.example","priority":10,
 "ttl":3600,"proxied":false}

DKIM TXT 记录使用第一步中得到的 dkim_record 值,放在 dkim._domainkey.<subdomain>

{"type":"TXT",
 "name":"dkim._domainkey.system.example.com",
 "content":"v=DKIM1; k=rsa; p=MIGfMA0G...",
 "ttl":3600,"proxied":false}

如果 DKIM 公钥超过 255 字节(2048 位 RSA 密钥就是这种情况),需要将其拆分为多个带引号的片段:"chunk1" "chunk2"。大多数 DNS 提供商的 API 会自动处理分割;如果你的提供商不支持,需要在你这边手动拆分。

SPFDMARC 记录各设置一次,分别放在子域名本身和 _dmarc.<subdomain> 上。SPF 告诉接收方哪些 IP 有权为该域名发送邮件;DMARC 告诉接收方当 SPF 或 DKIM 校验失败时该如何处理:

{"type":"TXT","name":"system.example.com",
 "content":"v=spf1 include:spf.mailhost.example ~all",
 "ttl":3600,"proxied":false}

{"type":"TXT","name":"_dmarc.system.example.com",
 "content":"v=DMARC1; p=none; rua=mailto:[email protected]; aspf=r; adkim=r",
 "ttl":3600,"proxied":false}

DMARC 从 p=none 开始。这是监控模式——接收方会向 rua 地址发送汇总报告,但不会依据策略拒绝任何邮件。标准的部署节奏是:在 p=none 模式下运行两到六周,直到汇总报告显示至少 98% 的合法邮件合规通过,然后通过 pct= 标签逐步收紧——例如先设置 p=quarantine; pct=25 运行两周,再到 pct=75,再到 pct=100,最后才切换到 p=reject。在全新域名上跳过这个渐进过程直接设为 p=reject,是一种可靠地把自己的合法邮件也拦掉的方式——因为你还没验证 DKIM 签名路径是否完整。

第三步:创建邮箱和地址映射

邮件提供商的数据模型通常有两个独立资源:邮件用户(mailuser)(IMAP 账户,含凭据和配额)和地址(address)本地部分@域名 字符串,将邮件路由到某个邮件用户)。两者分离,是因为同一个邮件用户可以作为同一或不同域名上多个不同地址的目标收件方。

生成一个强密码,然后创建邮件用户。命名规范因提供商而异,常见模式是 <本地部分>_<域名下划线版>。注意长度限制——大多数提供商的邮件用户名上限是 32 个字符,较长的子域名很容易超出:

import secrets, string
alphabet = string.ascii_letters + string.digits + "!@#%^&*-_=+"
password = ''.join(secrets.choice(alphabet) for _ in range(32))

# 然后:
POST /api/v1/mailuser/create/
[{"name": "bot_system_example_com",
  "imap_server": "<IMAP_SERVER_UUID>",
  "password": password}]

邮件用户状态变为 READY 后,创建地址映射,将 本地部分@子域名 与之绑定:

POST /api/v1/address/create/
[{"source": "[email protected]",
  "destinations": ["<MAILUSER_UUID>"],
  "forwards": []}]

forwards 数组用于同时转发到其他外部地址。除非你真的需要同步转发给人工收件箱——这在项目交接阶段是个实用模式,但长期来看会带来混乱——否则留空即可。

第四步:端到端验证

按顺序进行四项检查。自回环测试不要跳过——它是唯一能确认入站路由确实送达邮箱的测试。

DNS 传播。Cloudflare 级别的 AnyCast 提供商通常在一分钟内完成传播。循环等待直到两条记录均可查询:

for i in $(seq 1 36); do
  mx=$(dig +short system.example.com MX)
  dkim=$(dig +short dkim._domainkey.system.example.com TXT)
  if [ -n "$mx" ] && [ -n "$dkim" ]; then break; fi
  sleep 5
done
echo "MX: $mx"
echo "DKIM: $dkim"

IMAP 登录。确认凭据有效,且邮箱确实关联了一个真实的收件箱:

openssl s_client -crlf -connect imap.mailhost.example:993 -quiet
a1 LOGIN bot_system_example_com <password>
a2 SELECT INBOX

预期响应:a1 OK Logged in,随后 SELECT 确认全新邮箱的 EXISTS 0

自回环测试。最佳的端到端测试:从新地址向自己发一封邮件,然后在 IMAP 中确认收到。这验证了完整链路——SMTP 认证、DKIM 签名、MX 解析、邮件接收、邮箱投递、IMAP 可见性——而不依赖任何外部邮件提供商的垃圾邮件过滤器是否配合:

import smtplib, ssl, imaplib, time
from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = "[email protected]"
msg["To"]   = "[email protected]"
msg["Subject"] = "loopback test"
msg.set_content("hello self")

with smtplib.SMTP("smtp.mailhost.example", 587) as s:
    s.starttls(context=ssl.create_default_context())
    s.login("bot_system_example_com", "<password>")
    s.send_message(msg)

time.sleep(10)
imap = imaplib.IMAP4_SSL("imap.mailhost.example", 993)
imap.login("bot_system_example_com", "<password>")
imap.select("INBOX")
typ, search = imap.search(None, "ALL")
print("inbox count:", len(search[0].split()))

在已投递邮件的邮件头中,查找 Authentication-Results 行。配置正确的情况下,应看到 spf=passsmtp.mailfrom 正确,以及带有 d=<你的子域名>s=dkimDKIM-Signature 头部字段。这两项都存在,说明整条链路工作正常;唯一的变量是外部接收方对你的看法——这将通过接下来几天的 DMARC 报告告诉你。

外发测试。向一个你能验证的外部地址发送一封邮件——私人 Gmail、独立工作邮箱,任何系统外的地址均可。确认邮件落在收件箱而非垃圾箱。如果全新域名的第一封邮件就进了垃圾箱,最常见的原因是发件域名尚无历史信誉;信誉提升是一个渐进过程,但单封测试邮件应当能干净通过——因为接收方会独立评估 DKIM 和 SPF,而不依赖历史发送量。

常见陷阱

32 字符邮件用户名限制在子域名加本地部分超出预期长度时会被触发。backoffice_dashboard_system_example_com 有三十九个字符,会被拒绝;bo_dashboard_system_example_com 只有三十一个字符,可以通过。与其让 API 在部署中途强迫你缩写,不如提前有意识地制定缩写方案。

Cloudflare 代理陷阱。Cloudflare 的界面允许你对任意记录开启橙云代理。对 Web 源站的 A/AAAA 记录几乎都该开启。对 MX 记录,永远关闭——Cloudflare 不代理邮件。陷阱在于:当你通过脚本批量启用代理并不小心包含了 MX 记录时,Cloudflare 会悄悄忽略不支持类型的代理标志,表面上看没有报错,但你会浪费几分钟排查一个根本不存在的问题。

首封入站邮件的灰名单延迟。邮件系统有时会对来自未知发件方的第一封邮件启用灰名单——返回临时 4xx 响应,要求发件方几分钟后重试。Gmail 会自动重试。如果你的第一封入站测试邮件看起来失败了,等十五分钟再下结论。

SPF DNS 查询上限。SPF 在评估过程中有严格的十次 DNS 查询上限(RFC 7208 §4.6.4)。单个 include:spf.mailhost.example 计为一次查询,即使该 include 内部解析到几十个 IP 地址。只有在开始叠加多个发件方时才会触碰上限——例如后来为了营销邮件流水线又加了 include:_spf.resend.com。制定 SPF 记录时要面向长期的发件方列表规划,而不只是当前这一个。

邮件提供商端 dkim_privkey 为空。部分提供商的域名创建接口调用成功,但实际上没有生成 DKIM 密钥,需要额外调用一个「启用 DKIM」接口。在继续下一步之前,务必确认创建响应中的 dkim_record 字段非空。如果为空,查阅提供商文档,找那个额外的调用。

对部署状态的粗放监控。如果你运营数十个域名,DKIM 记录失效或 DMARC 报告目标地址过期,并不会在日常监控中主动浮现——除非你专门去检查。这正是我们在自托管服务月度健康检查流程中专门加入邮件记录审计步骤的原因。

在支持 MTA-STS 的情况下跳过配置。MTA-STS 发布一条策略,强制发件方使用 TLS 连接你的 MX 主机并验证证书。Gmail、Microsoft 365 和 Yahoo 都会检查这条策略;全网普及率仍不足百分之一,因此有效的策略只是一个微小的信誉加分。代价在于运营层面:若策略指向的 MX 主机 TLS 验证失败,合法入站邮件会被阻断。如果你的提供商支持 MTA-STS,在 DKIM 和 DMARC 稳定运行之后再启用。

回滚方案

如果验证失败,或者你认为项目不需要独立邮箱,清理操作是幂等的,大概需要五分钟:

  1. 以地址 UUID 调用 POST /api/v1/address/delete/
  2. 以邮件用户 UUID 调用 POST /api/v1/mailuser/delete/
  3. 以邮件系统域名 UUID 调用 POST /api/v1/domain/delete/(这一步同时清除 DKIM 密钥)
  4. 针对三条 DNS 记录分别调用 DELETE /zones/<zone>/dns_records/<id>

顺序很重要,因为部分提供商在域名下仍有地址时会拒绝删除该域名。逆序操作也为你提供了合理的检查点——如果第一步因邮箱中有你忘记的邮件而失败,此时 DNS 层的路由还未被破坏。

回滚后将 SPF 和 DMARC 记录保留在子域名上没有任何害处。它们没有实际签名路径,但也向未来的接收方表明:该子域名明确不发送合法邮件——这是一个微小的防仿冒效果。我们的做法是一旦这些记录创建就保留下来。这种「安全残留」思维与我们在自托管服务灾难恢复中应用的逻辑相同:清理后的状态设计和安装时的状态设计同等重要。

小结

项目专属邮箱不是什么重型基础设施,而是一个小型运营原语,让 AI 助理——或任何其他自动化进程——能够作为一等公民参与基于邮件的工作流。上述四次 API 调用的模式可以干净地移植到不同的邮件提供商,三条 DNS 记录也可以干净地移植到不同的 DNS 提供商。真正值得做对的,是两个系统之间的边界:哪条记录属于哪一侧,故障模式长什么样,以及如何在不依赖任何外部方的垃圾邮件过滤器表现的前提下完成端到端验证。

如果你希望获得项目专属技术栈的架构建议——Telegram 助理、独立邮箱、文档接收,以及更多——tva 随时可以提供帮助


相关文章

相关文章