# AI 代理通过 API 发送邮件：安全控制

能够调用邮件 API 的代理，可以用机器速度联系客户、供应商和合作伙伴。这项能力适合发送例行通知和跟进运营事项。但一个小小的提示词错误、一条过时记录或一次被攻破的代理会话，都可能在任何人看到草稿前，把问题变成一次对外通信事故。

安全设计不是从改进提示词开始的，而是从让发送路径拒绝不安全的收件人开始。当邮件跨过明确的风险线时，系统必须要求审批，并保留足够证据，以便重建每个决定。如果你对“这个代理可以给谁发邮件？”的回答是“CRM 里的任何人”，那就还没有建立边界，只是把地址簿交给了代理。

## 邮件凭据绝不能决定代理的权限

调用服务商 API 的凭据，只能证明某个服务可以提交邮件。它不能证明某个代理进程应该出于某个目的联系某个人。团队经常把这两个问题混为一谈，因为服务商令牌很容易签发，泄露后却很难检查。

把服务商令牌放在负责执行发送的组件中。代理应提交意图，而不是持有可重复使用的持有者令牌。这个组件可以识别代理运行、解析收件人 ID、检查邮件内容、在必要时要求做出决定，并且只有在这些检查通过后才调用服务商。

这种区分在故障发生时尤其重要。假设代理收到一条从工单中复制来的指令：“把更新后的协议发到我的新地址。”如果代理持有邮件凭据，它可能会立即把邮件发到文本中出现的任何地址。如果代理向受控的发送组件提交请求，该组件就可以拒绝未识别的地址、要求审批，或要求人工更新联系人记录。

原始 API 令牌还会让撤销变得麻烦。你可以撤销令牌，但这可能会中断所有共享该令牌的合法工作进程。应为每个代理进程设置会话身份。进程结束时终止会话。如果进程行为异常，只撤销该会话，让其他工作继续运行。

不要把邮件 API 令牌放入代理的环境变量、项目文件、Shell 历史记录、工具配置或提示词中。事后脱敏无法修复这种设计。一旦模型或工具进程读取了秘密，就无法可靠证明它曾经被传到哪里。

对于在 Mac 上使用自主编程代理的团队，Sallyport 可以在不向代理暴露凭据的情况下执行 HTTP 邮件 API 调用。这解决了凭据保管问题，但不能代替下面介绍的收件人和内容规则。

## 收件人边界需要记录，而不是字符串匹配

收件人边界应回答这样一个问题：这个特定地址是否可以从这个发件人处接收这类邮件。仅靠域名允许列表无法回答。供应商可能使用个人邮箱，客户可能有多个联系人，而一个拼写错误仍可能指向允许域名上的真实地址。

让收件人注册表成为事实来源。每个外部收件人都应有稳定 ID 和地址，以及发送服务判断请求所需的信息：关系、负责人、允许的邮件用途、在适用情况下的同意状态，以及是否每次发送都必须经过人工审核。代理请求的是 `contact_4821`，而不是 `ap@northwind.example`。

发送组件应先验证记录，再解析 ID。请求可以携带用于渲染的显示名称，但不能覆盖注册表中保存的地址。这能避免代理集成中经常出现的一种隐蔽故障：开发人员验证了联系人 ID，却又信任同一请求中的自由填写 `to` 字段。

针对以下情况分别建立列表：

- 同意接收某类明确通知的客户
- 由指定员工负责的活跃供应商运营联系人
- 上线期间使用的内部测试收件人
- 始终需要人工决定的例外收件人

不要在面向代理的接口中，把 `to`、`cc`、`bcc` 或 reply-to 接受为不受约束的字符串。Bcc 需要特别处理。它适用于少数合规或案件管理流程，但也会形成隐藏的数据泄露路径。默认禁用 Bcc。对于允许使用的每个 Bcc 收件人，都要求提供记录在案的理由和明确审批。

Reply-to 也可能和收件人列表一样带来问题。代理可以发送一封看似无害的通知，却把 reply-to 地址设置为将客户信息转入无人值守邮箱的地址。应从有限的发件人配置列表中解析 reply-to，而不是接受代理提供的值。

联系人数据是会变化的。供应商联系人可能离职，账户可能关闭，同意状态可能改变，客户也可能要求停止接收邮件。发送路径必须在发送时检查当前状态，不能只在代理首次计划邮件时检查。缓存地址很方便，直到它让一名已离职员工收到合同更新。

## 发件人身份应让邮件用途一目了然

代理应使用专用的组织身份发送邮件，不能使用员工邮箱，更不能使用高管地址。收件人需要清楚知道是哪类邮箱联系了自己，以及回复会发送到哪里。

设置 `billing-notices`、`service-status` 或 `vendor-operations` 这样的发件人配置。每个配置都应规定发件地址、回复目的地、允许的邮件类别和可使用的模板。代理选择的是配置 ID，而不是自行填写 From 或 reply-to 标头。

这种分离既能限制损害，也能减少混淆。如果负责支持案件更新的代理也能使用 `accounts-payable`，它就可能让付款请求看起来合法。如果所有运营邮件都使用一个宽泛地址，员工就无法判断意外邮件来自受监控的进程还是人工发送者。

为每个发件身份配置身份验证。SPF 告诉接收系统哪些基础设施可以代表某个域发送邮件。DKIM 为邮件附加域签名。DMARC 发布 SPF 和 DKIM 未通过对齐检查时，域名希望接收方采取的处理方式。这些记录都不能决定代理是否选对了收件人，它们的作用是保护域名声誉，并帮助接收方判断邮件是否真实。

RFC 5322 定义了 Internet Message Format，并区分 From、Sender、Reply-To、To、Cc 和 Bcc 等字段。该标准允许许多形式，邮件客户端也能以较好的方式显示。代理接口应远比标准允许的格式更严格。邮件格式的灵活性，不等于允许代理自行制造邮箱、标头或收件人列表。

显示名称应保持克制。来自 `"Accounts Payable" <billing-notices@...>` 的邮件如果要求供应商修改敏感银行信息，可能会误导对方。名称应只描述实际运营职能。除非确有其事，否则禁止使用声称某位指定员工发送或审核过邮件的措辞。

## 审批必须检查最终邮件，而不是代理摘要

只有当审核者看到系统将要执行的准确决定时，人工审批才有效。“代理想通知客户有关发票的情况”不是一个合格的审批对象。它没有说明客户、金额、发件人、措辞、回复路径和附件集合。

先生成邮件，解析所有收件人，渲染所有模板变量，然后创建不可变的待发送记录。再把这条记录提交审核。最终发送调用必须通过 ID 引用已批准的记录，并拒绝审批后的任何修改。

待发送记录至少应包含以下结构：

```json
{
  "request_id": "req_01J...",
  "agent_session": "sess_01J...",
  "purpose": "vendor_invoice_query",
  "sender_profile": "vendor-operations",
  "to": [{"contact_id": "vendor_4821", "address": "ap@example.test"}],
  "cc": [],
  "bcc": [],
  "subject": "Question about invoice INV-1048",
  "body_sha256": "6af1...",
  "attachment_sha256": [],
  "approval_required": true
}
```

将渲染后的内容存入受保护的存储，或者在保留规则允许的持久副本下，同时保存摘要和可靠副本。只有在仍能取回被声称已发送的字节时，摘要才能证明内容没有改变。对于敏感邮件，应同时保存渲染后的 MIME 邮件及其摘要。

审批触发条件应反映潜在损害，而不是模糊的置信度分数。置信度分数看似灵活，却会让审核者难以理解为什么一封得分 0.74 的邮件发送了，而另一封得分 0.71 的邮件被拦截。应使用操作人员可以检查的明确条件。

请求出现以下任何情况时，都应要求审批：

- 为某种用途引入了以前未批准的收件人
- 向日常通知类别之外的外部对象发送邮件
- 修改付款、账户访问、合同、价格、交付或法律条款
- 包含附件或 Bcc 收件人
- 超出该邮件类别已知的收件人数

当代理根据开放式指令生成正文，而不是使用受约束的模板时，也应要求审批。“你安排的维护将于明天开始”用途明确。根据一长段支持对话草拟的邮件，则可能包含代理从错误案件中提取的主张、承诺或个人数据。

不要批准一个代理会话运行一天，然后把这称为审核。这样会授予宽泛能力，同时隐藏每次操作的后果。会话审批可以授权代理准备请求。若邮件不属于低风险、预先批准的类别，则应通过逐封邮件审批决定是否对外通信。

## 模板可以减少差异，但不能授予发送权限

模板很有用，因为它能限制措辞并让检查更容易。但如果代理可以任意选择收件人、使用未经检查的数据填充字段，或选择与事件不匹配的模板，模板并不会让邮件自动安全。

每个模板都需要明确邮件类别、允许的发件人配置、允许的收件人关系、必填数据字段和最大收件人数。模板渲染器应拒绝未知变量，而不是悄悄留下占位符或接受任意 HTML。

以服务维护通知为例。代理可以从活跃账户关联的记录中填入客户姓名、维护时间窗口和支持渠道。不应填入从工单中提取的自由文本说明，不应包含另一名客户的标识，也不应因为代理认为有帮助就添加附件。

下面这种简短的策略表示，可以让检查具备可审计性，但不会假装规则引擎能够解决所有判断：

```yaml
message_class: scheduled_maintenance
sender_profile: service-status
recipient_relationship: active_customer
max_recipients: 1
allowed_template: maintenance_notice_v3
approval:
  required_if:
    - attachment_present
    - recipient_status_not_active
    - maintenance_window_changed_after_render
```

它所防止的故障并非理论上的问题。常见流程是先渲染模板并保存草稿，然后允许代理在发送前立即更新维护时间窗口。审批界面显示的仍是旧时间。审批必须绑定渲染内容摘要，任何收件人、标头、变量或附件发生变化时，都要使审批失效。

模板也不应处于代理的权限边界之内。代理可以请求模板 ID 和结构化值。发送服务应加载模板，并根据输出上下文对值进行转义。如果代理提交完整 HTML，它可能用标记隐藏额外文本，加入未经批准的跟踪内容，或改变邮件的视觉含义。

不要用模板掩盖推广行为。交易通知和营销邮件有不同的同意、频率和退订要求。如果无法明确分类，就应转交人工，而不是强行通过一个方便的模板发送。

## 附件和引用的邮件线程会携带你忘记审核的数据

附件会把受控的文本发送变成文件披露流程。代理可能找到旧提案、导出工单，或生成包含不该分享的列的电子表格。只阅读邮件正文的审核者会错过发送中最具破坏性的部分。

要求附件进入由发送服务管理的暂存区。按照组织的安全流程扫描文件，计算摘要，标记来源，并将该确切文件绑定到待发送记录。审核者应能打开暂存副本，或在决定前看到可靠预览。

绝不能允许代理提交类似 `attach: "/Users/shared/contracts/latest.pdf"` 的请求。“最新”不是一条记录，文件系统路径也不能证明文档的受众。应由人工或经过批准的文档流程创建附件记录，其中包含分类、负责人、文件名、摘要和过期时间。

引用的邮件线程也需要同样谨慎。转发邮件线程可能暴露内部备注、先前的收件人、复制的标头和无关案件细节。如果代理需要上下文，就提供相关的结构化事实。如果必须发送之前的邮件，应把转发内容视为类似附件的对象并要求审核。

图片和生成的 PDF 需要直接检查。文本提取可以帮助审核者搜索账号或个人数据，但无法可靠发现视觉布局中的所有内容。暂存预览比盲目自动化慢，却比解释为什么一个供应商收到了另一家供应商的发票快得多。

## 投递事件是证据，不是无限重试的许可

邮件 API 的响应通常只表示服务商接受了请求。它不代表收件人邮箱接受了邮件，不代表有人阅读了邮件，也不代表回复会到达受监控的队列。保存服务商邮件 ID，并将后续事件与内部请求 ID 关联起来。

RFC 5321 描述了 SMTP 的传输行为，包括一台服务器接受邮件与之后的投递结果之间的区别。API 服务商用更友好的响应封装了这一传输过程，但不能消除这一区别。应将成功的 API 响应视为提交证据。

在服务商提供这些事件的情况下，记录投递、永久退信、临时退信、投诉、退订和服务商拒绝。使用这些事件更新收件人资格。发生永久退信的联系人应停止接收自动运营邮件，直到负责人修正记录。投诉应立即将地址从相关类别中移除，而不是等到代理下一次类似活动运行时才处理。

重试逻辑需要上限和负责人。临时故障可以支持使用同一份已批准邮件记录进行有限重试。不要让代理自行重写并重新发送被拒的邮件。重写后的重试可能绕过重复保护，把一次失败通知变成多封内容不一致的邮件。

在调用服务商之前先去重。幂等值应根据已批准的发送 ID 生成，而不是根据可变的主题行或代理当前任务生成。如果提交后发生网络超时，代理必须查询发送记录，而不是假定失败并再次提交请求。

服务商 webhook 可能延迟、重复到达或乱序到达。将其作为带服务商 ID 的事件保存，并以幂等方式处理。不要让重复的投递事件触发第二次内部流程，也不要让代理以为应该发送跟进邮件。

## 审计记录必须回答那些令人不安的问题

当客户问“你为什么把这封邮件发给我？”时，只有一个显示 `sent` 的仪表板记录远远不够。你需要知道哪个代理运行请求了发送、哪个账户或流程发起了请求、哪个收件人记录解析出了这个地址、实际发送的准确内容是什么、谁批准了它，以及服务商接受了什么。

保留两条相互关联的轨迹。运行日志记录代理进程的身份和生命周期、授权及撤销情况。调用日志记录每次待发送请求、验证决定、审批、提交给服务商的操作和投递事件。两者应通过一个标识符关联，而不是让人跨应用日志进行侦查。

让审计事件只能追加，并保护它们不受执行发送组件的影响。如果工作进程可以删除或改写自己的记录，审计轨迹会在最需要它时失效。哈希链提供了一种实用的篡改检查方式：每个事件都包含前一个事件的摘要和自己的序列化数据。应独立验证这条链。

最小事件序列如下：

```text
2025-04-03T09:12:04Z proposed  req_01J... sess_01J... digest=6af1...
2025-04-03T09:12:10Z approved  req_01J... reviewer=user_17 digest=6af1...
2025-04-03T09:12:11Z submitted req_01J... provider_id=msg_92...
2025-04-03T09:12:14Z delivered req_01J... provider_event=evt_44...
```

仅有时间戳并不能让记录值得信任。保护事件存储，为每次状态转换记录操作者，并将完整性验证与写入事件的服务分开。保留期限也很重要。应提前决定需要保存邮件内容、元数据和附件多长时间，不要等事故发生后才发现已经没有这些信息。

Sallyport 在一个加密的哈希链式审计日志中分别提供会话视图和活动视图，`sp audit verify` 可以在离线状态下检查链条，无需保险库密钥。当团队需要调查某条记录是否在代理运行后发生变化时，这种独立验证非常有用。

## 受控上线能在客户发现问题前暴露错误假设

不要一开始就让代理给所有活跃联系人发邮件。先建立内部收件人注册表，并选择一个不涉及财务、合同、访问控制或法律后果的邮件类别。目标是找出你以为拥有的记录与发送路径实际使用的记录之间的差异。

与同意接收测试邮件的人员一起进行内部试运行。刻意测试接口必须拒绝的故障：未知地址、额外的 Cc 收件人、Bcc 请求、审批后发生变化的模板变量、来自未暂存路径的附件，以及模拟超时后的重发。记录系统是否拒绝每个请求，以及审计轨迹是否说明了拒绝原因。

然后加入一个外部用例，并使用规模较小且有明确负责人的收件人集合。在检查了足够多的真实记录并了解例外模式之前，每次发送都保留审批。不要因为邮件看起来重复就取消审批。只有在收件人来源、模板、发件人配置、数据字段和重试行为都受到约束并受到监控后，才考虑取消审批。

应有人负责收件人注册表和邮件类别。自动化经常在交接边界失败：销售认为支持团队负责地址，支持认为财务负责措辞，而代理看到的只有一行联系人记录。明确的负责人可以修正过时记录，并决定新的用途是否属于自动发送路径。

为人工提供立即停止控制，既能阻止会话未来发送，也能防止排队的待发送请求通过最终提交检查。然后在队列中有待处理请求时测试它。只能在工作开始前生效的撤销按钮，只是让人安心的装饰。

第一步很简单：列出代理今天可能发送的每一封外部邮件，然后为每一封确定准确的收件人记录、发件人配置、审批规则、内容对象和审计事件。任何一项没有答案的记录，仍然是一个没有边界的 API 调用，无论代理工作流看起来多么完善。
