mcpField
M
mailwarden

mailwarden

本地服务
github.com/csitte通信协作01 天前更新

关于 mailwarden

mailwarden 是面向 AI 助手的 Gmail MCP 服务,用于整理邮箱。它直接查询 Gmail 实时数据并按当前标签复核搜索结果,提供邮箱侧延后处理及批量操作;有意不提供邮件发送工具。

主要功能

  1. 01按实时标签复核 Gmail 搜索
  2. 02邮箱侧延后处理
  3. 03批量归档和标签操作
  4. 04退订管理
  5. 05发件人认证结果
  6. 06结构化工具输出

适用场景

  1. 01查找未读或特定类别邮件
  2. 02归档或标记匹配查询的邮件
  3. 03按日期将邮件线程移回收件箱
  4. 04查看发件人认证信息

MCP 相关

它将 Gmail 邮箱整理能力作为 MCP 工具提供,并声明输出架构、返回结构化结果。工具支持搜索、查看邮件线程、标签、归档、延后处理和退订管理,但不能发送邮件。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"csitte-mailwarden": {
"command": "npx",
"args": [
"-y",
"mailwarden"
],
"env": {
"MAILWARDEN_DIR": "<MAILWARDEN_DIR>",
"MAILWARDEN_CREDENTIALS": "<MAILWARDEN_CREDENTIALS>",
"MAILWARDEN_AUTO_SWEEP": "<MAILWARDEN_AUTO_SWEEP>",
"MAILWARDEN_DOWNLOAD_DIR": "<MAILWARDEN_DOWNLOAD_DIR>"
}
}
}
}
启动命令npx -y mailwarden
需要配置的环境变量
MAILWARDEN_DIRConfig directory (default ~/.mailwarden), holds credentials.json and token.json
MAILWARDEN_CREDENTIALS密钥Path to the Google OAuth credentials.json (Desktop app client)
MAILWARDEN_AUTO_SWEEPSet to 1 to sweep due snoozes at startup and hourly while running
MAILWARDEN_DOWNLOAD_DIRRestrict download_attachment and export_thread writes to this directory (recommended for HTTP hosting)

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
npmmailwarden0.25.0

工具

26

收录的工具列表 · 更新于 1 天前

参数类型必填说明
querystring是
pageTokenstring否
maxResultsinteger否

详细介绍

mailwarden

npm license Node Website Smithery

可靠的原生 Gmail MCP 服务——为 AI 助手提供完整的邮箱整理功能,并具备其他 Gmail MCP 服务都不支持的功能:邮箱端延后处理。

亮点

  • 延后处理——Gmail MCP 服务中唯一的邮箱端延后处理功能。 现在归档会话,让它在指定日期重新出现在收件箱中。此功能基于带日期的标签和定期扫描,因此可在任何客户端中使用,在 Gmail 中也能直接看到,并且在重启后仍然有效。(其他服务所谓的“延后处理”只是本地提醒列表——邮件既不会离开收件箱,也不会重新进入收件箱。)
  • 值得信赖的搜索。 Gmail 的 threads.list——所有会话搜索都会调用此接口——可能会根据过时的会话级已读状态响应 is:unread:在一个真实邮箱中测得,它返回的会话中有 86% 实际上没有任何未读邮件;而在另一个邮箱中,完全没有偏差。你无法不查看就知道自己使用的是哪个邮箱,因此 search 会根据邮件的实时标签重新验证每个结果。支持通过 pageToken/nextPageToken 分页。(测量结果)
  • 验证发件人身份,而非只看发件人名称。 get_thread 会报告接收服务器记录的 SPF/DKIM/DMARC 结果,因此“这真的是我的银行发来的吗?”可以根据邮件自身的标头来判断,而不是看域名像不像。它只读取 Gmail 自身的报告——即 mx.google.com 写入的报告;邮件本身也可能携带伪造的报告——并说明通过验证的 DKIM 签名是否属于 From 域名;如果无人检查,则标记为 unchecked,因为没有检查不等于通过验证。
  • 可扩展的批量操作。 bulk_modify 每次 API 请求最多处理 1000 封邮件,可归档或为所有符合查询条件的邮件添加标签,并针对每个分块报告部分成功,而非非成即败。延后处理扫描也使用同一批处理路径。
  • 结构化输出。 每个工具都声明 outputSchema,并在围栏 JSON 文本之外返回经过验证的 structuredContent——客户端无需猜测如何解析。失败也采用结构化格式:包含 code 和 retryable 标志,客户端无需阅读说明文字即可判断是“稍后重试”还是“重新授权”。
  • 精简的攻击面。 不提供发送工具(避免通过提示词注入邮件进行数据外泄),可选只读模式,不收集遥测数据,默认不开放端口,下载路径防护可安全应对符号链接,输出经过注入防护。也不存在任何可能发送邮件的代码路径:每个 Gmail 请求都会经过出口检查点,拒绝 messages.send、所有草稿端点、永久删除和转发设置,无论被攻陷或粗心的调用方提出什么请求。唯一的例外是: unsubscribe / bulk_unsubscribe(manage 层级)会联系邮件自身标头中指定的退订端点——这是 mailwarden 唯一会访问的非 Google 主机;而部署在 read 层级时,完全不会发出外部请求。详情请参阅安全与隐私和退订——唯一的外部请求。
  • 适用于真实世界的邮件。 解码 RFC 2047 标头(=?UTF-8?B?…?= → 可读文本),根据字节内容和声明的字符集解码邮件正文(Gmail 已转换为 UTF-8 的文本一律按 UTF-8 读取,不受标头影响;其他内容则使用所声明的字符集;ISO-8859-1/Shift_JIS 邮件不会出现乱码),对 429/5xx 错误进行指数退避重试。

原因

同步或缓存邮箱的连接器可能会落后于邮箱本身,而且 Gmail 自己的搜索索引有时也不太准确(见下文)。mailwarden 直接调用实时 Gmail API(不使用缓存快照),并重新核验索引返回的结果,因此你看到的就是邮箱中实际存在的内容。它是一个通用的 Gmail 能力层——请在自己的 AI 客户端中实现规则和逻辑,而不是放在服务端。

search 比原始 API 更进一步:Gmail 的 threads.list 索引可能会根据过期副本中的状态来处理已读状态运算符,因此 is:unread 会返回你几周前就已读完的会话——在一个经过测量的邮箱中,返回结果中的大多数都是这种情况。由于每个匹配项都会实时获取,search 会根据每个会话的真实标签,重新检查明确无歧义的谓词(is:unread/is:read/is:starred/in:inbox/category:…,包括否定条件),并剔除索引误报的结果。

与其他 Gmail MCP 服务的对比

大多数 Gmail MCP 服务都支持类似的读取、标签和发送功能。据我们所知,mailwarden 仍有两项独有能力(邮箱端的延后处理、搜索结果重新核验),而一项有意为之的功能缺失则是安全特性,并非功能不足。第三项功能——发件人身份验证——在推出时曾是独有能力,但现在已不再独有:@agentcomms/gmail-mcp 自 2026 年 9 月 18 日起会读取 Authentication-Results,并且只信任 authserv-id 为 mx.google.com 的报告。自 2026 年 10 月 13 日起,下方也为它单独设置了一列。Google 自己的服务支持范围也比表面看起来窄:仅支持草稿,不支持垃圾箱、过滤器或取消订阅。

能力mailwardenGoogle 官方taylorwilsdona-bonusaaronsbklodragentcommsgmail-tidy
邮箱端延后处理 — 立即归档,到指定日期/时间或预设时间后重新显示在收件箱中✅———————
重新验证搜索结果 — 根据实时标签剔除会话索引中的误报✅———————
发件人身份验证 — 对每封邮件显示接收服务器记录的 SPF/DKIM/DMARC 信息✅ 仅提供 Gmail 自身的报告,值经过令牌验证,并检查 DKIM 对齐—————✅ 仅提供 Gmail 自身的报告,并检查 DKIM 对齐⚠️ 从第一个标头获取值,未检查签发方
批量处理查询结果 — 对搜索返回的每个会话执行同一操作✅ 每次请求最多 1000 项,支持部分成功—⚠️ 按明确指定的 ID 批量处理;筛选器 apply 会对查询结果重新贴标签,但不重新检查—✅ 查询结果预览,最多 10,000 项,不重新检查⚠️ 按明确指定的 ID 批量处理⚠️ 按明确指定的 ID 批量处理,可精确撤销每封邮件的操作⚠️ 对查询结果执行,最多 5,000 项,不预览或重新检查
退订 — 按发件人汇总 + RFC 8058 一键退订,无需发送权限✅—⚠️ 显示标头,不提供操作————⚠️ 显示链接,不提供操作
收件箱分拣概览 — 一次调用即可归类待处理邮件✅ 发件人/标签/邮件年龄 + 标头信号——✅ 启发式标记 + 统计数据⚠️ 未读邮件列表,不分类——⚠️ 发件人统计 + 新闻邮件列表,不分类
服务器端筛选器 — 无需助手介入即可持续执行分拣的规则✅ 绝不转发—✅—✅ 支持预览;⚠️ 除非采用仅草稿策略,否则允许转发✅ 绝不转发—✅ 绝不转发或移至垃圾箱
按设计不提供发送工具 — 即使邮件中包含提示词注入,也没有数据外泄途径✅ 完全没有撰写功能⚠️ 仅草稿❌ 可发送❌ 可发送❌ 可发送;⚠️ 可选择仅草稿策略❌ 可发送❌ 可发送;⚠️ 需逐封邮件审批,默认在聊天中进行⚠️ 仅草稿
最小权限工具层级 — 根据启用的工具推导 OAuth 作用域✅⚠️ 作用域分级⚠️ --permissions 可按服务缩小作用域(累积或 + 并集);层级仅限制工具—⚠️ 每个账户可设为只读/读写,在调用时检查⚠️ 相反:根据已授予的作用域限制工具⚠️ 每个邮箱的层级决定作用域;在调用时根据授权检查写入操作—
静态令牌加密✅ 默认使用 AES-256-GCM,密钥存放在操作系统密钥库中;若系统没有密钥库,则使用密码短语不适用(托管服务)⚠️ 文件权限设为 0600;GCS 存储桶使用 CMEK———✅ 默认使用操作系统钥匙串;也可选择仅限所有者访问的文件✅ 默认使用 AES-256-GCM,密钥存放在操作系统密钥库中
无供应商云服务 — 由你运行服务✅❌ 由 Google 托管✅✅✅✅✅✅
结构化输出 — 每个工具都声明 outputSchema✅✅ 在每个工具的参考文档中均有说明———⚠️ 两个工具(download_email、download_all_attachments),计划增加更多✅ 每个工具均有—
截至 2026 年 10 月 13 日的快照——这是七列检查中最早的日期(Google 的参考资料和 a-bonus 都在当天最后一次查阅),因此也是整张表唯一可以声称的日期。每当 npm run table-sources 报告有变动时,我们都会读取与 docs/comparison-sources.json 中记录的版本之间的差异,以更新各仓库对应的列。2026 年 10 月 25 日,其中四个仓库同时更新。aaronsb 更新至 9c9f8033(v4.8.0),它的两个单元格也随之更新:自 2026 年 10 月 16 日起,它可以对查询匹配的所有内容应用标签;执行前会先预览匹配数量、将 ID 保留在服务端,并返回一个三十分钟内有效且只能使用一次的令牌(最多 10,000 封邮件;不会重新检查匹配项);它还可以创建、更新和删除过滤器,同样需要先预览——包括转发,除非其策略设为仅草稿。taylorwilsdon 更新至 fcbf5a2c(v2.2.0);现在 --permissions 还接受类似 gmail:readonly+send 的权限并集,只授予指定的权限级别,最小权限单元格现已注明这一点。klodr 更新至 e5c080c3(3.0.0);现在无论使用什么作用域,create_filter 都会拒绝转发操作,其过滤器单元格已注明这一点。agentcomms 更新至 261cae4f(0.28.0,新增了针对所发送内容的收件人规则);没有单元格需要更新。a-bonus 自 2026 年 10 月 13 日以来没有更新。Google 的工具参考资料最后一次查阅于 2026 年 10 月 13 日,其中为它的十一个工具都记录了输出架构;它仍处于 Developer Preview 阶段,也没有发送、移至垃圾箱、过滤、稍后处理、取消订阅或基于查询的工具。— = 未提供 / 未记录。表中的列对应读者最可能使用的服务——Google 的第一方服务,加上仍在维护的三个最大社区服务,以及最接近 mailwarden 自身最小权限设计的 klodr。agentcomms(@agentcomms/gmail-mcp,于 2026 年 10 月 13 日查阅,版本为 01cee751)也在当天加入,原因有二:据我们所知,它是唯一另一个会读取 Authentication-Results 的服务,而且它会将读取范围限定为 Gmail 自己生成的报告,比 mailwarden 当时采用的做法更严格,这也是 mailwarden 现在采用相同做法的原因;并且它在首次发布后一个月内,npm 的下载量就超过了 klodr。它的批量操作按 ID 执行,而不是按查询执行,但每次更改都会返回针对每封邮件的精确撤销操作——mailwarden 的 bulk_modify 也采用了这一设计,设置为 undo: true。它可以发送邮件,但每封邮件都需要审批;在对话中的默认审批结果是“是”。它自己的 README 明确指出,软件无法分辨这个“是”究竟来自用户还是 Agent,并提供了终端确认(confirm)或完全禁止发送(never)两种替代方案。gmail-tidy(gmail-tidy-mcp,于 2026 年 10 月 25 日查阅,版本为 6df48c69)在当天加入,因为它明确采取的立场与本项目最接近:没有发送工具,也不能移至垃圾箱;构建测试会在出现其中任何一项时失败;过滤器既不能转发,也不能移至垃圾箱。它确实可以创建草稿,因此在发送一行中与 Google 的服务并列,而不是与本项目并列——它自己的 README 明确说明,其令牌可以发送邮件。它曾在一项上领先于 mailwarden,而 mailwarden 此后也采用了相同设计:默认加密令牌,并将密钥保存在操作系统密钥库中。它的批量操作以查询为范围,最多处理 5,000 封邮件,既无预览,也不重新检查;它会从第一个 Authentication-Results 标头读取 SPF/DKIM/DMARC 失败信息,但不会确认该标头由哪个服务写入。它首次发布后十天内,npm 下载量就追平了 mailwarden。aaronsb 于 2026 年 9 月 8 日加入,之所以之前未被纳入,值得明确说明:当时的筛选标准是功能重叠,因此,一个规模比表中最小列大一个数量级的服务被排除在表外,而那个最小列却保留了下来。它入选的原因是发送功能,而不是规模。它是这里除 mailwarden 之外第一个可以被设置为完全拒绝发送的服务——GWS_SAFETY_POLICY=draft-only-email 会阻止 send、reply、replyAll 和 forward;agentcomms 此后也通过 never 发送策略加入其中——而决定这一行是否准确的关键,在于承诺体现在哪里:该服务默认允许发送,拒绝发送依赖运营者设置环境变量;而本项目从未编写过任何发送工具。它的 no-delete 策略与本项目仅允许移至垃圾箱的规则也有同样的关系。过滤器一行从相反的方向提出了同一个问题,另一个项目则说明了为什么这个问题很重要:c0webster/hardened-google-workspace-mcp 是 taylorwilsdon 一列对应项目的分支,只为解决这一问题而创建;它直接移除了 create_gmail_filter 和 delete_gmail_filter,因为过滤器可以转发——这正是风险所在,而它通过移除该功能来规避风险。mailwarden 保留了该功能,同时消除了风险:create_filter 无法指定转发,而 list_filters 会显示已有过滤器,便于审计。这个分支还展示了这类承诺通常会从哪里出现漏洞,因此上一行关注的是工具接口和出口防护,而不是作用域:它移除了 gmail.send,却保留了 gmail.compose——注释说明“仅用于创建/编辑草稿(不用于发送)”——以及 gmail.modify。Google 将前者描述为“管理草稿并发送邮件”,将后者描述为“读取、撰写和发送邮件”,所以这项承诺并非仅仅因为多了一个作用域而无法成立,而是因为两个作用域;仅 gmail.modify 就足以发送邮件——而这正是本项目在 manage 权限级别使用的作用域,因此该行关注工具接口的原因也正在于此。它的令牌可以发送邮件;只有它的工具列表不可以。最常安装的 Gmail 服务因此没有列入,而非疏忽所致:GongRzhe/Gmail-MCP-Server 已归档,最后一次提交在 2025 年 8 月,但截至 2026 年 8 月 29 日的那个月,它仍有 112,163 次 npm 下载量。使用范围和当前状态是两个不同的问题,而比较某个服务现在能做什么,只能回答后者。发送能力被列为一项安全属性:mailwarden 没有发送能力是有意为之(见安全与隐私)。加密一行关注的是谁持有密钥:默认情况下,mailwarden 会将随机密钥保存在操作系统密钥库中,gmail-tidy 也是如此;无法访问密钥库时,则使用你设置的密码短语;如果你没有设置密码短语,则退回到明文文件,因此单元格中两种情况都有注明。taylorwilsdon 在本地依赖文件权限,在托管于 GCS 时则依赖存储桶自身的 CMEK——前者可防止文件被盗,后者可防止磁盘被盗。最后一行关注的是谁在运营服务,而不是服务运行在哪里:自行托管是这里的共同点,表中的每个社区服务都支持某种远程部署,只有 klodr 和 gmail-tidy 例外(仅支持 stdio)——mailwarden 使用 --http,taylorwilsdon 通过支持流式传输的 HTTP 配合 OAuth 2.1,a-bonus 则运行在 Cloud Run 上。在自己的主机上运行其中一个服务,不等于云端副本;在供应商的主机上运行才是。
真正的护城河不在于任何单项,而在于暂缓处理 + 实时重新验证的结合:这是一个真正的收件箱工作流层,依据邮箱的当前状态执行操作,而不是使用缓存快照。其他工具已经追上来的部分,我们已在上文如实注明:静态数据加密(taylorwilsdon;默认启用且密钥存放在 OS 密钥库中的 agentcomms 和 gmail-tidy)、由权限范围驱动的工具门控(klodr 方向相反;taylorwilsdon 与我们的方向一致,而且比此处原先所述更进一步,后来我们才更正——除了 --read-only 会将 OAuth 流程切换到只读权限范围映射外,自 2026 年 2 月 25 日起,他还支持按服务设置权限级别:--permissions gmail:organize 会根据累积级别构建所请求的权限集合——readonly、organize、drafts、send、full——随后他的工具注册表会禁用所有声明的权限范围不在其中的工具,并刻意跳过权限范围层级扩展,因此 organize 确实会排除 gmail.send。这与此处说法的区别在于方向,而非强度:权限级别是按服务选择的,并非根据启用的工具推导而来,因此除非指定级别,否则 --tool-tier core --tools gmail 仍会请求完整的 Gmail 权限范围;而且这两种模式都没有在请求本身设置防护。我们 8 月的检查只看了 tier 路径,从 8 月 26 日起在此处低估了他的能力;我们于 2026 年 9 月 7 日重新检查了他仓库当时所处修订版 54b1c56 中的 auth/permissions.py、auth/scopes.py 和 core/tool_registry.py——也就是我们阅读代码那天的版本。tier 部分于 2026 年 8 月 26 日检查,并在同一天由 csitte.at 更正;他对照自己的克隆仓库进行了验证,而不是只听信我们的说法)、更丰富的逐邮件分类启发式(a-bonus),以及对整个邮箱进行批量整理(托管服务 mcpemails.com,它也没有暂缓处理功能)。他们都没有做到的是根据查询执行操作,并在操作前检查邮箱返回的结果——taylorwilsdon 的筛选器 apply、aaronsb 的预览式 modifyMatching 和 gmail-tidy 的 organize_emails 现在都做到了前半部分,且都基于 messages.list;但都没有做到后半部分。
与 Workspace 服务搭配运行

mailwarden 是 Gmail 服务,而非 Workspace 套件——如果你想在一个地方使用 Calendar、Drive、Docs 和 Sheets,像 taylorwilsdon/google_workspace_mcp 这样的综合服务能覆盖本服务无法覆盖的范围,而且两者并不冲突。两者同时使用是合理的配置,原因在于令牌,而非工具数量:能够发送邮件的套件服务持有的凭据也能发送邮件,且适用于它所连接的每个邮箱。改由 mailwarden 处理 Gmail,则意味着你的配置中的邮件部分完全没有撰写、回复、转发或发送工具。此承诺如何落实取决于权限级别,区别很重要:在 read 级别,Google 会在令牌层面强制执行(gmail.readonly,发送端点会拒绝此权限);而在 manage 级别,保障则依赖工具层——Gmail 确实接受 gmail.modify 来发送邮件,因此仅靠权限范围并不能保证安全。两种情况下,出站防护都会在服务本身拒绝 messages.send 和所有草稿端点,因此收件箱中即使有注入的邮件,也没有可调用的工具或可访问的端点。

实际配置方式:将套件服务指向你需要的服务,并禁用其 Gmail 工具(使用 --disabled-tools,或选择不包含这些工具的 tier),同时运行 mailwarden 来处理邮件。请牢记权限级别规则——每份客户端配置最多只能让一个邮箱使用可写权限级别。

为什么重新验证很重要——一个具体例子

让助手*"归档那些已跳过我的收件箱、且尚未读过的促销邮件"*,它就会使用显而易见的查询:category:updates is:unread -in:inbox。如果服务信任 Gmail 的索引,就会归档你已经读过的邮件——这些邮件你本无意处理,却在批量操作中被移走,而且很难轻易恢复。 有测量为证,而非空口断言。 在一个真实邮箱(约 70,000 封邮件)中,category:updates is:unread 通过 threads.list 返回了 131 个会话,其中只有 17 个会话包含未读邮件——87% 已过时。对同一个邮箱,在同一分钟通过 messages.list 执行相同查询:19 封邮件,没有一封已过时。所以问题不是「Gmail 搜索不可靠」——而是已读状态的 会话 视图会滞后,逐封邮件视图则不会,且 search 会经过 threads.list。同一天用相同方式测量的第二个邮箱,完全没有偏差。

测量方法、全部三个查询、对照项,以及这项发现 不是什么(不是索引丢弃了谓词,也不是罕见运算符组合造成的怪异现象):Gmail 的会话索引可能依据过时的已读状态回答 is:unread——一份独立报告,每个数字都有记录在案的测量结果可追溯。

这正是重点:服务端无法知道自己面对的是哪种邮箱。 没有偏差时,重新验证没有成本;出现偏差时,重新验证能帮上忙——在上述测量中,search 丢弃的每个会话都确实已读,而且没有丢弃任何确实未读的邮件。

但批量工具并非没有成本。 search 会重新验证,因为无论如何它都要获取每个结果;bulk_modify(以及 create_filter 的 applyToExisting 扫描)处理的邮件数量以千计,每个结果都获取一次,成本就完全不同了。两者都会处理索引返回的内容——bulk_modify 会报告 unverifiedPredicates,即查询中被视为索引所言而未加验证的条件(+UNREAD、-INBOX……)。为空表示没有需要存疑的条件。若非空,且结果必须精确反映已读状态?先用 search 确定这组结果,再对这些会话 ID 执行操作。dryRun 无法弥补这一差距:它会重新读取同一个索引,因此只能确认结果集有多大,无法确认它是否正确。而它的 previewToken 能弥补的是另一种差距——展示一组结果与修改它之间的差距:没有该令牌,写入操作会重新运行查询,并修改届时匹配的内容。

有意不报告筛选器扫描的这个字段。 它的查询并非由你提供——create_filter 会根据条件构建查询,对 query 条件加上括号,并为每个 from/to/subject 值加上双引号;这两种做法都会关闭谓词推导。因此,无论定义什么筛选器,列表都会为空;而这里的空列表会被理解为「没有需要存疑的内容」,实际含义却是「无法判断」。所以我们会在说明文字中明确提示,而且始终适用:请将扫描视为按索引的回答执行,无论它匹配了什么。 使用 verify: true 了解扫描实际修改了什么;如果必须在写入前确保结果集正确,则使用 search。

更省成本的折中方案,并如实标注。 bulk_modify 还支持 crossCheck: true,它会在写入前以第二种方式向 Gmail 提出相同问题:每个推导出的谓词都会作为标签筛选器(labelIds)重新执行,而不是作为查询运算符执行;两种途径结果不一致的邮件会保持不变,并被报告。成本是每个谓词多一次列表调用——固定不变,与匹配的邮件数量无关;而重新验证的成本是每个结果获取一次。这种方式的效果有限,值得明确说明:结果不一致是真实证据,但结果一致不构成任何证据,因为两种途径读取的是同一个索引,而索引可能始终出错。因此,无论是否进行交叉核对,unverifiedPredicates 仍会一如既往地报告;只有 search 会针对邮箱本身重新检查。两种途径在实际使用中是否会出现差异,目前尚无测量结果——运行 node scripts/probe-crosscheck.mjs 即可在你自己的邮箱中精确测量,只读且仅使用 ID。 mailwarden 无论如何都会实时获取每个命中结果,因此 search 会针对每个会话的真实标签重新检查明确无歧义的条件(is:unread、is:read、in:inbox、category:…,包括否定条件),并在任何工具看到结果前剔除索引产生的误报。随后,批量操作会恰好作用于你指定的集合。这就是基于 Gmail 索引的内容采取操作与基于当前邮箱中的实际内容采取操作之间的区别,也是可以放心让助手执行暂缓和清扫操作的原因:清扫只会重新显示暂缓时间确实已到的会话,并会在运行时根据实时标签进行验证。

亲自试试看——无需 Gmail 账号。 在克隆的仓库中运行(此演示是仅供仓库使用的验证脚本,不属于 npm 包):

git clone https://github.com/csitte/mailwarden && cd mailwarden
npm install && npm run build
node scripts/demo-reverify.mjs

旁边还有第二个脚本:node scripts/probe-reverify.mjs。它会在你的邮箱而非虚构邮箱中测量同一项数据——只读,仅限元数据(不会获取主题、发件人或正文),并输出计数和标签名称。上文中的数字就是通过它得出的,你也可以用它检查邮箱是否存在偏差。

此演示会通过伪造的 Gmail API 调用真实的 search()。该 API 的索引被刻意设为过时状态(对 is:unread 查询返回一个已读会话,正如 Gmail 实际会做的那样),并展示 mailwarden 如何剔除误报。演示会断言结果,因此如果此行为出现回归,就会以非零状态码退出。同一案例也由 test/gmail.test.ts 中的单元测试锁定("drops index false positives via live-label re-verify")。

无需获取所有内容,就能再次查询

定期检查——我上次查看后有什么新内容——对于实时服务来说成本很高:显而易见的做法是重新搜索整个范围并进行比较。what_changed(读取级别)则会直接从 Gmail 自己的事件日志中回答。传入先前调用或 get_profile 返回的 historyId,它就会返回新增内容、已移除内容,以及添加或移除的标签,并附上供你保留的下一个 ID。

这不是缓存,这一区别正是整个设计的核心。 调用之间唯一会保留的是一个数字,而且它保留在调用方。mailwarden 仍不会存储邮箱相关信息,不会保留镜像或索引,并且每次调用仍会实时访问 Gmail API——这与这里其他所有功能遵循的规则相同。

在依赖它之前,有两点值得了解。它报告的是事件,而非状态:某条消息先被标记为未读、再标记为已读时,会同时出现在两类结果中,而且两者都属实——若要了解邮箱当前的状态,请使用 search。此外,Gmail 大约只保留一周的历史记录,之后就会拒绝某个 ID;mailwarden 会将此拒绝转为错误,而不是返回空结果,因为没有任何变化和我已无法告知你发生了什么变化需要截然相反的处理方式,只有其中一种可以安全地用于后续操作。

判断发件方——authentication 能回答什么,不能回答什么

get_thread 返回的每条消息都带有一个 authentication 对象:其中包含接收服务器记录的 SPF、DKIM 和 DMARC 结果,以及每项检查实际验证的域名。

{
  "spf": "pass",   "mailedBy":   "forwarder.example",        // envelope sender SPF checked
  "dkim": "pass",  "signedBy":   "routing.example",           // domain whose key signed it
  "dmarc": "pass", "headerFrom": "authority.example",        // the From domain DMARC evaluated
  "dkimAligned": false,                                      // the signer is not the From domain
  "authservId": "mx.google.com",                             // who asserts all of the above
  "returnPath": "srs0=…=authority.example=…@forwarder.example"
}

先看 dmarc。 在这三项检查中,只有它会将通过检查与人类看到的 From 地址关联起来。单独的 spf: "pass" 只说明某个信封发件人获准发送邮件——仿冒域名只需几分钟就能做到。

这三个域名不必一致,不一致也不代表有问题。 上面的对象来自一封真实的公共机构邮件:它经由邮件路由服务上的自定义域名转发,之后才抵达邮箱。三个域名各不相同,但邮件确实是真的:转发会改写信封发件人(mailedBy 会变成转发方),转发方会用自己的密钥签名(signedBy),只有 headerFrom 仍保留原始发件方的名称——这正是 DMARC 而非 SPF 在这里有意义的原因。应将这些域名视为结果的解释,而不是需要单独检验的内容。

通过检查不代表什么。 这并不意味着邮件真的来自该域名——更不代表该域名值得信任。钓鱼者刚注册了仿冒域名,就能让 SPF、DKIM 和 DMARC 全部完美通过;身份验证告诉你是谁发的,答案可能是“确实是它声称的发件方,而这正是问题所在”。

dkimAligned 回答的是签名引出的问题。 任何人都能用自己拥有的域名对应的密钥为邮件签名,因此单独的 dkim: "pass" 只说明有人签了名。dkimAligned 则说明这个发件方是否就是 From 域名,或是它的父域名或子域名。对于两个同级子域名,它无法给出结论:news.bank.example 和 mail.bank.example 是对齐的,a.co.uk 和 b.co.uk 则不是;没有公共后缀列表时,这两种情况看起来完全相同,因此该字段会被省略,而不是返回错误结果。在上面的转发示例中,该字段为 false,而且结果正确:签名者是转发方。DKIM 未通过时,该字段会被省略。与域名一样,false 只是解释,不是判定——应始终先看 dmarc。

unchecked: true 的含义。 这封邮件没有 Gmail 提供的 Authentication-Results 标头——无人检查。这既不代表失败,也不代表通过。该标头由接收邮件的服务器写入,因此任何从外部从未抵达的邮件——例如你自己发出的邮件——都应预期不带此标头。

伪造的报告。 邮件可以带有自己的 Authentication-Results 标头——攻击者想在自己发送的邮件中写什么都行。系统只读取 Gmail 自己的报告:authserv-id 为 mx.google.com 的最顶部报告;RFC 8601 告诉读者应信任这条边界。其他任何 ID 下的报告都不会被读取,无论它看起来多么可信;即使邮件带有其他 ID 下的伪造报告,只要没有 Gmail 的报告,就会标记为 unchecked。限制在于:如果邮件未经过 Gmail 的入站邮件服务器就抵达邮箱——例如通过 POP 从另一个帐户获取,或由应用插入——它可能完全没有 Gmail 报告;此时,声称来自 mx.google.com 的伪造报告便无法与真实报告区分。Gmail 自行接收的邮件始终会在顶部附上自己的报告,因此其中的伪造报告绝不会被读取;otherReports 会统计所有未读取的报告。(在 0.22.1 及之前的版本中,系统会读取第一份报告,无论其 ID 是什么——这在 Gmail 写入报告时是对的,在 Gmail 未写入报告时则恰好是错的。)系统会将值验证为令牌,而非直接透传,因此看起来像判定结果的字段无法包含完整句子。如果同一方法的两个结果相互矛盾——例如第二个 DKIM 签名验证失败——这种矛盾会显示在 alsoReported 中,而不会被忽略。

工具

工具功能
searchGmail 查询语法 → 线程摘要(第一封邮件的发件人、主题和日期,最后一封邮件的 lastMessageId/lastMessageDate/lastMessageFrom,以及当最后一封邮件由你发送时的 lastMessageSent;unread 是布尔值;attachments 按名称、类型和大小列出;标签;Gmail 的摘要片段——即最后一封邮件的摘要片段);读取状态/类别谓词会根据每条命中结果的实时标签重新验证;通过 pageToken/nextPageToken 分页。每条命中结果都带有 signals:newsletter(List-Id / List-Unsubscribe / Precedence bulk、junk 或 list)、automated(Auto-Submitted、自动回复/抑制标头、类似 no-reply 的发件人)、calendar(text/calendar 或 .ics 部分)、replyToMismatch(Reply-To 的域与 From 不同;同一域的子域视为相同)、displayNameSpoof(From 显示名称中出现了邮件来源域以外的地址或 URL——"[email protected]" <[email protected]>)、lookalikeDomain(From 域名中的某个标签混用了拉丁字母与西里尔字母、希腊字母或亚美尼亚字母,类似同形异义域名)——这些信息直接从第一封邮件的标头/MIME 中读取,无需额外调用。除非查询中包含 in:spam / in:trash,否则会排除垃圾邮件和回收站中的邮件——参见搜索垃圾邮件
get_thread完整线程:标头、纯文本 + HTML 正文、附件元数据。每封邮件还包含 authentication——接收服务器报告的 SPF/DKIM/DMARC 结果,以及每项检查验证过的域名(signedBy/mailedBy/headerFrom)、声明结果的主体(authservId)、returnPath、表示相互矛盾结果的 alsoReported、表示未读取报告的 otherReports、表示 DKIM 签名者是否为 From 域的 dkimAligned,以及邮件未携带 Gmail 报告时的 unchecked: true——参见如何判断发件人——身份验证能说明什么,不能说明什么。full: false 仅获取标头和标签——此时会省略 plaintextBody/htmlBody/attachments 并设置 metadataOnly: true,而不是把未读取的内容报告为空;此选项已弃用,建议改用 bodies: "none"(配额相同,且会返回实际附件列表),与 bodies 同时使用会被拒绝并返回 invalid_input。对于较长或 HTML 内容较多的线程,可以缩减返回内容:bodies: "text" 仅保留纯文本(仅含 HTML 的邮件会转换为文本,并标记 bodyFromHtml);bodies: "new" 相当于 text,但会移除回复末尾引用的历史内容(末尾带归属说明行的 > 区块,或 Outlook 的 Original Message 区块;行内回复、引用内容下方的回复和转发邮件保持完整,包括主题以 Fwd/FW/WG 开头的 Outlook 转发;位于 Outlook 纯文本标头区块下方的回复会连同该区块保留),并将被截断的邮件标记为 quotedStripped;bodies: "none" 保留标头、身份验证信息和实际附件列表,不含正文;lastN 仅返回最新的 N 封邮件,afterMessageId 仅返回已读取邮件之后的邮件。无论保留哪些内容,配额都相同;messageCount 和 omittedMessages 始终说明返回内容涵盖了线程的多少部分
read_attachment读取附件中的文本,不保存文件:纯文本(txt、csv、xml、json、ics 等)、gzip 文件或 ZIP 压缩包中的文本条目——DMARC 汇总报告、导出文件、日志。格式由字节内容决定,绝不依据声明的名称或类型;非 UTF-8 文本(例如 Windows 上的 Excel CSV)会按 windows-1252 读取,并标记 guessedCharset;二进制格式会被拒绝,并提示使用 download_attachment。所有大小都在处理前受限(每个条目解压后 5 MiB、每个压缩包 20 MiB、最多 50 个条目),因此 ZIP 炸弹会在达到限制时停止。一次响应中所有条目的文本总计最多包含 maxChars 个字符(默认 30,000,最多 100,000);被截断的条目会返回 nextOffset,可用 offset 继续读取。对于压缩包,list: true 会先显示名称、大小和文本长度;entry(名称或 *.csv 之类的模式)仅读取所需内容,并在 notSelectedNames 中列出其余条目;二进制、加密、超大和嵌套压缩包条目只会列为 skipped,不会打开。不会写入磁盘,且文本会用代码围栏括起,并剔除邮件正文中那样的隐藏字符
list_labels列出所有标签(系统标签 + 用户标签)
get_profile已连接账户的地址、邮件/线程总数,以及邮箱当前的 historyId——执行操作前先确认连接的是哪个邮箱。还会显示回答请求的服务:serverVersion(仅发布构建时为纯版本号,否则附带提交信息,例如 0.23.0+a5ff4a7)、build、serverPath(加载运行代码的目录)和 account,适用于文档提到的工具似乎缺失、需要确认实际运行的是否为你认为的构建版本的情况
what_changed自你持有的 historyId 以来的邮箱事件——邮件到达、移除、标签添加和移除,一次调用即可获取
triage_digest邮箱片段的结构化概览,用于决策:主要发件人(各自线程所带的信号)、标签和时间段分组、未读和附件数量,以及新闻通讯/自动邮件/日历邀请/回复地址不匹配线程的数量——而非原始线程列表
follow_ups谁在等谁:最后一封邮件由你发送、且数天未获回复的线程(发给自己地址的备注会排除;To 或 Cc 中有其他收件人时则保留),以及最后一封邮件来自个人而非新闻通讯或 no-reply 通知的收件箱线程。根据每个线程最后一封邮件及其实时标签进行判断,因此索引仍将已归档线程列在收件箱中的情况会被排除
list_unsubscribe查看线程提供的退订选项(List-Unsubscribe);若未提供,则列出正文链接——不会联系任何人
list_subscriptions按发件人分组的邮箱片段:线程/未读数量、每个发件人对应邮件的日期范围,以及各自的退订选项——每位发件人仅获取一次标头,不会联系任何人。sendersFound 会报告 topN 截断列表前的发件人数量
create_label创建用户标签(幂等;可通过 Parent/Child 创建嵌套标签)并返回其 ID;可选的 backgroundColor/textColor 配对用于设置颜色,也适用于已存在的标签
modify_labels按名称或 ID 添加/移除标签——add 中未知的名称会自动创建(归档 = 移除 INBOX,标为已读 = 移除 UNREAD)。接收 threadId,或接收 threadIds(最多 100 个的列表),以便一次调用对经批准的列表应用同一更改;之后响应会按线程报告 changed、failed 和 notAttempted
bulk_modify对所有匹配查询的邮件批量更改标签——每个 API 请求最多 1000 封邮件,按批次报告部分成功情况(线程 ID 列表最多 500 个,submittedThreadCount 为总数)。计数使用 submitted,因为 messages.batchModify 会返回不带正文的 204,且会静默忽略未知 ID;verify: true 会重新读取标签并返回 verified {applied, notApplied, unverifiable}——这是唯一可用的实际结果。操作基于原始索引,因此 unverifiedPredicates 会列出无法保证成立的条件(见下文)。dryRun: true 会解析查询并报告匹配的线程及将要创建的标签,不做任何更改;如果有任何匹配项,还会返回 previewToken:服务端会将这一确切的邮件集合保留半小时,之后回传该令牌并使用相同查询和标签更改的写入操作仅会针对该集合,不会涉及其他邮件。期间新开始匹配的邮件会被忽略,并计入 fromPreview.newMatches;改写查询或更改内容会在首次写入前被拒绝;令牌只能使用一次。undo: true 会在写入前实时读取每个受影响线程的标签,并返回 undo,其中包含每封邮件的精确反向操作(最多 500 封邮件;使用 undo: true 时,除非可以构建撤销计划,否则不会写入任何内容)
undo_bulk_modify重放 bulk_modify 返回的 undo 计划:每封邮件会恢复其恰好丢失的标签,并移除其恰好新增的标签,因此原本已处于目标状态的邮件会保持原样——交换 add 和 remove 无法做到这一点。仅接受标签 ID,因为计划中携带的是 ID;批量更改创建的标签仍会保留在邮箱中,但为空
archive / mark_read / mark_unread便捷封装工具;与 modify_labels 类似,接受 threadIds 列表
trash / untrash移至回收站/从回收站恢复
download_attachment将附件保存到本地路径(绝不覆盖文件——发生重名时会添加数字后缀)
export_thread将完整线程写入本地文件,而不是直接返回——供人阅读的 Markdown 格式(标头和附件名称用代码格式括起,因此预览不会加载图片,也不会跟随发件人写入的链接;包含发件人身份验证信息;每封邮件的正文都用邮件文本无法闭合的围栏括起),或按 get_thread 返回形式输出 JSON。路径规则与附件相同
unsubscribe一键退订(RFC 8058),使用邮件自身标头中的端点——唯一会联系非 Google 主机的工具(详情)
bulk_unsubscribe对多个线程执行相同操作,按顺序处理,且每位发件人最多请求一次(在服务运行期间跨调用记忆,因此重试不会对同一发件人再次发起请求);按线程报告部分成功情况。dryRun: true 会执行相同的标头读取和去重,并报告每个线程的 wouldCall 端点——不会联系任何人
snooze立即归档,并在某个日期(YYYY-MM-DD)、日期 + 时间(2026-06-20 9am)或预设时间(tomorrow、tomorrow 9am、weekend、next week、星期名称、in N days、in N hours)或之后重新显示
unsnooze取消稍后处理,立即移回收件箱
list_snoozed列出所有稍后处理的线程及到期日期
sweep_snoozed重新显示已到期的稍后处理线程(按需、通过 cron 或守护进程运行);分批处理,并报告部分失败情况。dryRun: true 会回答“现在有哪些已到期?”(dueLabels/dueThreads),不会唤醒任何邮件
list_filters列出所有 Gmail 过滤器(条件 + 标签操作);显示现有过滤器中的任何 forward 地址,供审计使用
preview_filter创建过滤器前先检查:当前有多少封邮件符合其条件(currentMatches,使用与 applyToExisting 扫描相同的索引)、哪些现有过滤器的条件完全相同(sameCriteria),哪些过滤器与其共享 from/to/subject/query 值(overlapping,并注明共享字段),哪些 addLabels 名称将被创建,以及该操作是否会丢弃所有未来的匹配邮件。只读;验证规则与 create_filter 相同
create_filter创建服务端自动分类规则(条件 → 仅标签操作;不转发——见下文)。可选 applyToExisting,同时扫描邮箱中已有的匹配邮件
delete_filter按 ID 删除过滤器
所有工具都声明 outputSchema,并返回结构化内容(经过验证、机器可读),同时附上相同内容的围栏 JSON 文本——客户端无需解析散文。

失败也采用结构化形式:isError 加上围栏 JSON 正文,其中包含 code (not_authorized、needs_reauth、insufficient_scope、forbidden_operation、not_found、 rate_limited、upstream_unavailable、network_error、invalid_input、declined_by_user、internal_error)和 retryable 标记,同时附上供人阅读的句子。因此,“等待后重试”还是“重新运行 mailwarden --auth”,是客户端可以自行决定的事情,而不是需要从可能在下个版本改写的措辞中推断出来的。(错误不会包含 structuredContent:它要通过工具的 outputSchema 验证,而该架构描述的是成功响应。)rate_limited 错误还会携带 retryAfterSeconds,因为此时等待就是解决办法。insufficient_scope 错误会指出具体缺少的权限范围,而非罗列各种可能性——缺少哪个权限范围、它涵盖什么,以及已保存的授权实际授予了什么。这些信息是在令牌到达时读取其自身权限范围获得的,服务也会在启动时将同一行内容打印到 stderr。它会解释拒绝原因,而非阻止拒绝发生: 调用仍会发送到 Google,由 Google 拒绝请求。这在使用加密令牌时尤其重要:注册阶段的层级门禁无法解密令牌,因此会完整公布功能,而缺少权限的问题原本会在任务进行到一半时才以一个简单的 403 暴露出来。

配额:唯一一种会自行恢复的故障

Gmail 会将每次调用计入每用户每分钟 6,000 个单位的预算,而且单位并非一概而论——无论以何种格式获取,获取会话都是最耗费配额的操作:

工具每次调用的单位数
search每个列表页 10 个单位,每获取一个候选会话另加 40 个单位——默认大小约为 1,000 个单位;查询包含已读状态或类别条件时,最高约为 4,000 个单位
get_thread(完整和元数据模式均适用)40 个单位
trash、download_attachment、read_attachment20 个单位
export_thread40 个单位
archive、mark_read、untrash每个会话 10 个单位
modify_labels、snooze10 个单位;按名称查找每个标签另加 1 个单位,每个需要创建的标签另加 5 个单位
bulk_modify每 500 条匹配邮件 5 个单位,每修改 1,000 条另加 50 个单位;verify: true 每个会话另加 40 个单位,crossCheck 每个条件和 500 条匹配邮件另加 5 个单位,undo: true 每个会话另加 40 个单位;使用 previewToken 执行写入时,会再列出一次以报告 fromPreview
undo_bulk_modify每个步骤 50 个单位,使用 verify: true 时每个会话另加 40 个单位
follow_ups个人资料 1 个单位,每个列表页 10 个单位,每扫描一个会话另加 40 个单位——默认最多扫描 50 个会话,约为 2,000 个单位
list_labels、get_profile1 个单位

mark_unread 的费用与 mark_read 相同。

在基于这些工具构建功能之前,有两个重要事项值得了解。并行 Agent 共用一个预算——它们通过同一个服务进程,以同一个用户身份访问 Gmail,因此四个 Agent 同时读取会话时,预算消耗速度会变为四倍。另一个事项是,从 6 个会话开始,使用 bulk_modify 比循环调用更省配额:匹配邮件最多为 500 条时,费用为 55 个单位,相当于五次半 archive 调用的费用;邮件数量增加时,费用按千条邮件递增,而不是按会话递增。verify: true 则会反过来——读回标签每个会话要花 40 个单位,比它取代的循环操作还贵。

如果超出配额,mailwarden 会以 rate_limited 和 retryAfterSeconds: 60 使调用失败。这种失败是暂时的,等待确实能解决问题——预算按分钟计算。mailwarden 会自行等待一次(约 20 秒)再报告;超过这段时间后,它会把决定权交还给调用方,而不会让工具调用一直处于打开状态。对于一次调用中会获取多个会话的工具,这一规则同样适用:search 会以 rate_limited 失败,而不会返回被配额截断的列表;bulk_modify(使用 verify 时)和 list_subscriptions 会停止获取,并将未能处理的部分分别报告为 unverifiable 和 unknown(对于 bulk_modify,更改已经完成,如果调用失败,反而会掩盖这一点)。请注意,Gmail 同样可能用 403 而非 429 报告此问题,因此仅根据状态码判断的客户端会误将其视为权限问题——应根据 code 判断。

贪睡功能的工作原理(Gmail API 没有贪睡功能——我们自行实现)

snooze 会移除 INBOX 并应用带日期的标签 MCP/Snoozed/<key>,其中 key 可以是 YYYY-MM-DD(当天到期)或 YYYY-MM-DDTHHMM(在当地时间的这一分钟到期)。until 参数可以接受明确日期、日期和时间(2026-06-20 9am、…T17:00)或由服务端解析的预设值——today、tomorrow、weekend(下一个星期六)、next week(下一个星期一)、星期几名称(monday–sunday,取下一个对应日期)、in N days 或 in N hours;日期预设值还可以带结尾时间(tomorrow 9am、monday 8:30),因此调用方无需自行计算时间点。sweep_snoozed 会查找已到期的标签,并将对应会话移回收件箱(标记为未读);定时贪睡会在其分钟数当时或之后的首次扫描时唤醒,因此唤醒延迟等于扫描间隔。运行扫描:

  • 按需运行(sweep_snoozed 工具),
  • 通过 cron 运行:mailwarden --sweep,
  • 或自动运行:设置 MAILWARDEN_AUTO_SWEEP=1(服务运行期间每小时扫描一次)。
筛选器(持久化自动分流规则)

create_filter 会设置 Gmail 服务端规则:符合条件的邮件会自动执行指定的标签操作——无需助手介入,邮箱便会自行分流。

  • 条件: from、to、subject、query(完整的 Gmail 搜索语法)、negatedQuery、 hasAttachment、excludeChats,以及 size + sizeComparison(smaller/larger,须同时提供)。 至少需要一项。
  • 操作(仅标签): 按名称或 ID 使用 addLabels / removeLabels(addLabels 中的未知名称会自动创建,可通过 / 嵌套)。常见用法:跳过收件箱 → removeLabels: ["INBOX"]; 自动标为已读 → removeLabels: ["UNREAD"];自动移至垃圾箱 → addLabels: ["TRASH"]; 加星标 → addLabels: ["STARRED"];永不标为垃圾邮件 → removeLabels: ["SPAM"];归档到某个标签下 → addLabels: ["Receipts"]。
  • 现有邮件: 筛选器仅适用于创建后收到的邮件。传入 applyToExisting: true,还可对邮箱中已有邮件执行一次相同操作——mailwarden 会根据条件构建 Gmail 搜索并批量修改(最多 maxMessages 封,默认 1000;与 bulk_modify 一样有未经验证的索引这一注意事项,且此单次处理会排除垃圾邮件和废纸篓邮件)。 这要求至少有一项正向条件(from/to/subject/query/hasAttachment:true/size): 仅包含排除条件的规则(negatedQuery 或 hasAttachment:false)会因可能匹配几乎整个邮箱,而被拒绝用于 applyToExisting——不带此标志创建此类筛选器即可。 结果会返回在 applied 中(所用的 query、matchedMessages/submittedMessages/submittedThreadCount 计数、匹配集达到 maxMessages 时的 capped、每个分块的 failed,以及整个处理失败时的 error 字符串);未设置 applyToExisting 时,该值为 null。submittedMessages 表示提交给 API 的数量,并不代表实际发生了更改——在设置 applyToExisting 的同时传入 verify: true,即可读取标签并获取 applied.verified {applied, notApplied, unverifiable},其检查方式与 bulk_modify 的 verify 相同(每个受影响的会话额外读取一次,因此默认关闭)。筛选器会先创建,因此积压邮件的处理即使部分完成或失败,也只会在 applied 中报告,不会抛出错误——规则仍会生效。
  • 不支持转发——请参阅安全与隐私。
  • 需要 gmail.settings.basic 范围;如果授权的是旧版本,请重新运行一次 --auth。 只读模式下不可用。
取消订阅——唯一的出站请求

list_unsubscribe(读取级别)会报告发件方提供的选项,不会联系任何人。它会读取最新一封实际带有 List-Unsubscribe 标头的邮件——线程中接在新闻邮件后面的回复位于末尾,且不会公布任何选项,否则就会被误判为“此邮件列表没有退出方式”。 当线程中完全没有公布退出标头时,list_unsubscribe 会检查邮件正文,并将找到的退订链接作为 bodyCandidates 报告——很多发件方只在页脚放置链接,对他们回答“没有退出选项”虽然符合标头情况,却不符合邮件实际内容。这些链接只会报告,不会获取,不能传给 unsubscribe,且其 hasUnsubscribe 仍为 false,因为该标志一直描述的是标头。此检查需要完整获取一次线程,且仅在这种情况下进行。 list_subscriptions(读取级别)会对整批邮件执行相同操作,并按发件方分组,让你查看哪些人一直在写信,以及其中哪些人确实可以退订——每位发件方获取一次标头,而不是每个线程获取一次。unsubscribe 和 bulk_unsubscribe(管理级别)会据此执行操作——而且这是 mailwarden 唯一会联系 Google 以外主机的地方,因此规则非常严格:

  • 没有 URL 参数。 端点仅来自邮件自身的标头,不会从其他地方获取。 URL 参数会让遭到提示词注入的邮件将工具变成数据外泄通道 (把邮箱内容放进查询字符串);标头无法携带模型自行选择的数据。这也是为什么正文中的链接只会被报告:它是发件人写入文本的 URL,而抓取它会以一种完全绕过防护的方式,重新引入这条规则旨在防止的行为。 另一个 Gmail 服务 navbuildz/gmail-mcp-server 会在标头缺失时抓取这些链接,并跟随重定向。
  • 仅执行 RFC 8058 一键退订——发件人必须通过 List-Unsubscribe-Post 明确选择加入。 普通的 https: 链接是供人类在浏览器中使用的,只会返回给你,不会被抓取。
  • 绝不执行 mailto: 退订。 这需要发送邮件,而 mailwarden 无法做到。 会报告该地址,供你自行处理。
  • 请求固定,响应丢弃。 POST 正文始终为 List-Unsubscribe=One-Click,绝不会根据任何内容生成;响应正文会在未读取的情况下取消。返回给模型的是状态码和实际调用的 URL——不包含端点的任何内容,因此端点无法通过响应提供指令。(301/302/303 重定向会以 GET 方式跟随,也就是完全不带正文。)
  • 每位发件人只发送一次请求,按顺序执行,并受单次调用预算限制。 bulk_unsubscribe 接受线程 ID(绝不接受查询——由查询驱动的批量操作会在任何人查看之前,向每个匹配的发件人发送请求)。对于已发送过请求的发件人,其线程会以 duplicateOf 报告,不会再发送第二次请求:同一列表的两个线程共享一次退订操作,调用两次只会让对方再次确认你的地址。只有请求实际到达端点后,才会记录该发件人;因此,即使请求遭到拒绝或连接中断,下一个线程仍会获得自己的尝试机会——如果被跳过的线程提供了不同的端点,原因中也会说明,因为一位发件人可能运营多个列表。此记录会在服务运行期间跨调用保留,并且 unsubscribe 也会共享该记录:超时的调用可以安全重试,对同一份新闻简报退订两次也只会联系发件人一次。若要有意进行第二次尝试,例如端点返回 500 后,请在 unsubscribe 中传入 force: true。记录仅保存在内存中:若将其持久化,就会在令牌之外引入第二种本地状态,而此服务有意不保留这类状态,因此重启后记录会清空。 每次调用最多处理 25 个线程,最长 60 秒;预算无法覆盖的部分会以 skippedOutOfTime 返回,而不会被悄悄撤销。所有这些操作都无法撤销,因此才设置了这三项限制。
  • SSRF 防护。 仅允许 https、仅允许默认端口、URL 中不得包含凭据;每一跳(包括最多跟随 3 次的重定向)都必须解析为仅包含全局可达地址。检查会将每个地址解析为字节,并根据 IANA 特殊用途注册表进行匹配,因此同一地址的所有写法都会得到相同判定(::1 和 0:0:0:0:0:0:0:1 也是如此);无法解析的地址会被拒绝。DNS 解析和所有跳转共用 10 秒预算。此防护无法抵御 DNS 重绑定(连接时 fetch 会再次解析)——详见 SECURITY.md;即使如此,留下的风险也只是一个盲目 POST,其响应绝不会被读取。

请先用自己的邮件进行检查,再决定是否信任它。 从仓库克隆版运行(仅仓库版提供,不包含在 npm 包中),先执行 npm run build 和 mailwarden --auth:

node scripts/probe-unsubscribe.mjs --vet          # category:promotions, 25 threads
node scripts/probe-unsubscribe.mjs "from:substack.com" --max 50 --vet

它会并排打印每个实际的 List-Unsubscribe 标头及解析器对其的解析结果;--vet 还会使用 URL 验证和地址防护检查端点——让你同时看到解析器是否理解该标头,以及防护是否会允许执行该退订操作。严格只读:绝不会向发件人发送请求,也不会更改邮箱中的任何内容。 它无法撤销的操作:该请求会让发件人知道你的地址有效。无视自身退订选项的发件人,任何客户端都无能为力——对这类发件人,请将 unsubscribe 与 create_filter 或 trash 配合使用。对于无法自动执行的选项,会报告 unsubscribed:false 并提供替代方案,而不是报错。仅具备 read 权限的部署会提供 list_unsubscribe 和 list_subscriptions,绝不会发出退订请求。

查找垃圾邮件

未指定位置的查询永远不会查找垃圾邮件或垃圾箱。 Gmail 会从所有未包含 in:spam / in:trash 的搜索中排除这两个位置,因此,对于一封位于垃圾邮件文件夹中的邮件,from:someone 不会返回任何结果——而答案中也不会说明原因。使用真实邮箱进行测试发现:同一个 from: 查询默认返回 0 条结果,包含垃圾邮件后则返回 1 条。

这很重要,因为邮件被错误分类的原因就在于此。垃圾邮件过滤器会单独判断每封邮件;它不知道你刚注册了某项服务、请求了密码重置,或下了订单——因此,你正在等待的确认邮件恰恰是最容易进入垃圾邮件文件夹的邮件。你知道自己刚刚做了什么,过滤器却不知道。

因此,如果预期收到的邮件没有出现,请指定位置再查一次:

search("in:spam newer_than:2d")          # what got filed as spam recently
search("in:spam from:example.com")       # the confirmation that never arrived

使用 modify_labels(移除 SPAM,添加 INBOX)可将整个邮件会话移回收件箱。对于总是被误判的发件人,最好使用永不标记为垃圾邮件的规则,从根本上解决问题——通过 create_filter 设置 removeLabels: ["SPAM"](参见过滤器)。

本服务刻意不做两件事。它不会扫描垃圾邮件文件夹并判断哪些邮件该留在那里:对一个真实垃圾邮件文件夹的测试发现,其中 89% 的邮件完全没有邮件列表机制,因此,“看起来不像群发邮件”的规则会标记几乎整个文件夹,却无法过滤任何邮件。它也不会自行根据该判断采取行动——将邮件从垃圾邮件中移出是一项需要作出决定的操作,而让这一决定显而易见的上下文(“我刚在那个网站注册”)存在于对话中,而非邮箱里。

安全与隐私

完整的威胁模型——包括信任边界、针对各类威胁的缓解措施、明确不处理的事项,以及漏洞报告方式——请参阅 SECURITY.md。以下是重点:

  • 无遥测。 不会向外发送任何数据——没有分析、崩溃报告或跟踪。
  • 默认不开放端口。 仅使用 stdio。可选的 --http 监听器绑定到 127.0.0.1 (而非局域网),并且没有 MAILWARDEN_TOKEN 持有者令牌就拒绝启动——在可信的隔离网络上,可设置 MAILWARDEN_ALLOW_NO_TOKEN=1 覆盖此限制。绑定到环回地址时,它还会验证 Host 标头(防御 DNS 重绑定)。要远程托管,请设置 MAILWARDEN_HOST 并在前面配置 TLS。
  • 按设计不提供发送工具。 mailwarden 无法撰写、回复或转发邮件。邮件中的提示词注入指令无法通过此服务外泄数据。create_filter 遵循相同规则:它可以为邮件添加标签、归档、移至垃圾箱、加星标或标记邮件,但绝不会创建转发过滤器(这会成为数据外泄途径)。list_filters 仍会显示账户上已有的任何转发过滤器,以便你发现它们。此保证成立,是因为不存在此类工具,且无法在运行时注册此类工具;要了解更严格的变体——由 Google 拒绝发送,而非由 mailwarden 拒绝发送——请参见下文的只读模式。**请谨慎理解这项保证,因为它的范围很窄:**它只封堵了通过此服务对外传输的路径,而非助手的其他路径——仍能获取 URL、写入同步文件夹或运行代码的客户端,依然具备注入邮件所需的一切能力,而此服务正是将那封邮件呈现在它面前的途径。请参见非目标。
  • 仅访问一个外部主机,不使用模型选定的 URL。 unsubscribe 工具是唯一会联系非 Google 主机的代码路径。其端点从邮件的 List-Unsubscribe 标头中读取,绝不从工具参数中读取;请求正文固定不变,响应正文会被丢弃,因此无法成为数据通道。仅允许 https/默认端口;重定向会重新验证,任何解析到私有、环回、链路本地或元数据地址的跳转都会被拒绝。请参见取消订阅。
  • 工具层级(渐进式披露 + 最小权限范围)。 MAILWARDEN_TOOLS 仅公布你指定的层级——read(读取工具)、manage(邮箱修改、延后处理、下载)、filters(服务端过滤器的增删改查,这是唯一需要 gmail.settings.basic 的层级)。默认启用全部三个层级;例如,read,manage 可提供完整的邮件分拣功能,但不提供过滤器管理。--auth 请求的 OAuth 权限范围由启用的层级决定——read 部署只请求 gmail.readonly,仅当启用 filters 层级时才会请求 gmail.settings.basic。如果存储的令牌不包含 gmail.settings.basic(例如,在启用该层级前授权的令牌),过滤器工具也会自动隐藏——请重新运行 --auth 授予此权限。没有记录权限范围的旧令牌仍会像以前一样公布工具,并以运行时的权限不足提示作为后备。
  • 只读模式。 设置 MAILWARDEN_READONLY=1(即 MAILWARDEN_TOOLS=read 的简写)后,只会注册读取工具(search、get_thread、read_attachment、list_labels、list_snoozed、get_profile、what_changed、triage_digest、 follow_ups、list_unsubscribe、list_subscriptions) ——不会向客户端公布任何能更改邮箱或写入文件的工具(需要更广泛的 gmail.settings.basic 权限范围的过滤器工具也会排除在外)。推荐用于仅进行邮件分拣的共享/HTTP 部署。 这也是唯一一种由 Google 强制执行禁止发送保证的层级:它持有 gmail.readonly 令牌,Gmail 的发送端点会直接拒绝该令牌。manage 需要 gmail.modify,而 Gmail 确实接受该权限范围发送邮件——mailwarden 只是没有提供相应工具。因此,即使替换此二进制文件,read 部署也无法发送;manage 部署则是因为没有可调用的工具而无法发送。(不存在不包含发送权限的写入权限范围可供切换——请参见 SECURITY.md,威胁 1。)
  • 出口防护。 “没有可调用的工具”不再只是对工具列表的描述。每个经过身份验证的 Gmail 请求都会通过同一个检查点,该检查点仅允许 mailwarden 使用的端点,并拒绝其他端点——先检查拒绝列表中的 messages.send、drafts.*、messages.import/insert、永久删除,以及所有非过滤器的 settings 端点,避免后续修改允许列表时意外重新开放这些端点——还包括 /upload/gmail/v1/... 路由,Google 客户端在某个方法传入 media 时会使用该路由。如果请求主机被重写(GOOGLE_CLOUD_UNIVERSE_DOMAIN、rootUrl 选项),则会在令牌离开进程前拒绝该请求。Gmail 发现文档中的每个方法都会针对防护机制进行测试。它保护的是此服务,而非令牌:被盗的 gmail.modify 令牌仍可在其他地方用于发送邮件。
  • 限制下载范围。 设置 MAILWARDEN_DOWNLOAD_DIR 后,文件写入(附件和邮件线程导出)仅限于该目录内(规范化真实路径并识别符号链接),且不会覆盖已有文件。未设置时,由于没有可用于校验客户端提供路径的目标目录,download_attachment 和 export_thread 可以写入此进程有权访问的任何位置——在 --http 模式下尤其需要注意,因为客户端位于远程。
    因此,在未设置此限制的情况下启动 --http 会打印一条指出此风险的警告(但不会拒绝启动:与缺少持有者令牌不同,这需要经过授权的客户端,而现有部署依赖当前行为)。read 层级部署不会打印警告,因为它从不注册该工具。
  • 不可信内容防护。 每个工具结果都会用 <untrusted-tool-output> 标记包裹,并移除不可见字符/双向文本覆盖字符,让客户端能够区分引用的邮件内容和指令。移除操作还会处理 Unicode 标签字符和变体选择符补充区(用于隐藏 ASCII 信息),并且同样应用于 structuredContent 和文本副本——读取机器可读部分的客户端也会获得相同的净化内容。
  • 实时 API,不存副本。 不会在任何位置存储邮箱镜像或搜索索引。唯一的本地状态是 ~/.mailwarden/ 中的 OAuth 令牌。
  • 默认启用令牌静态加密。 token.json 保存刷新令牌;在磁盘上仅通过 mode 0o600 保护(在 Windows 上无效)。因此,mailwarden --auth 会使用 AES-256-GCM 加密令牌,并将随机密钥保存在操作系统密钥库中——Windows Credential Manager、macOS Keychain、Linux 上的 Secret Service(GNOME Keyring、KWallet)——这样,单独获取该文件的副本(备份、同步文件夹中的副本或另一台机器上的副本)也毫无用处。无法访问密钥库时(没有桌面会话的 Linux 主机、没有预构建绑定的平台),请设置 MAILWARDEN_TOKEN_PASSPHRASE,密钥将改为从该密码派生(scrypt);两者都不可用时,令牌会像以前一样以明文存储,且 --auth 和 --check 会对此作出提示。设置密码后,它的优先级高于密钥库。现有的明文令牌仍可使用,并会在下次运行 --auth 时加密——绝不会在你不知情的情况下重写。请注意防护边界:这能防止文件被盗,但无法防御以你的用户身份运行的恶意软件,因为恶意软件可以向密钥库请求密钥,或读取环境变量中的密码,mailwarden 也正是这样访问这些信息的。

快速开始

claude mcp add mailwarden -- npx -y mailwarden

安装就这么简单——npx 会获取并运行已发布的软件包,无需克隆或构建。你只需配置一次 Google OAuth 凭据(见下文)。

设置

第一次设置 Google OAuth 应用?请按照**分步设置指南**操作——指南会逐步介绍 Google Cloud Console 中的具体点击路径,解释“未经验证的应用”页面,并说明导致令牌在 7 天后失效的陷阱。简而言之:

  1. Google Cloud:创建项目 → 启用 Gmail API → 配置 OAuth 同意屏幕并将其发布到正式版(如果处于测试状态,Google 会在 7 天后使刷新令牌过期)→ 创建类型为桌面应用的 OAuth 客户端 ID → 将其下载为 credentials.json。
  2. 将 credentials.json 放入 ~/.mailwarden/(或设置 MAILWARDEN_CREDENTIALS=/path/to/credentials.json)。
  3. 授权一次——这会打开浏览器,并将刷新令牌保存到 ~/.mailwarden/token.json:
    npx -y mailwarden --auth
    
    请求的范围:gmail.modify(读取 + 添加标签/归档/移至垃圾箱)和 gmail.settings.basic (仅用于过滤器管理)。如果你在支持过滤器之前授权了某个版本,请重新运行一次 --auth 以授予新增的范围。若要持有 Gmail 本身拒绝发送邮件的令牌,请使用 MAILWARDEN_TOOLS=read 进行授权——参见上文的只读模式。
  4. 随时使用内置诊断工具验证设置:
    npx -y mailwarden --check
    
    它会检查 credentials.json、令牌是否存在(以及是否已加密)、已授予的范围是否涵盖你启用的层级,并调用一次 Gmail 在线接口以确认令牌仍然有效——对于任何问题,它都会打印具体的修复方法;如果存在问题,则以非零状态码退出(适用于 CI/运行状况检查)。它能诊断常见陷阱:凭据文件不存在/错误、从未授权、令牌已加密但未设置 MAILWARDEN_TOKEN_PASSPHRASE 或其密钥不在本机密钥库中、缺少范围,或“测试”状态同意令牌在 7 天后过期。

连接

Claude Code(本地 stdio):

claude mcp add mailwarden -- npx -y mailwarden

Claude Code 插件 — 同一服务,加上两个技能:/mailwarden:setup 会引导你完成 OAuth 设置并诊断故障;/mailwarden:triage 则包含助手处理邮箱时需要遵循的操作规则 — 最重要的是,search 会重新验证,而 bulk_modify 不会,因此如果 unverifiedPredicates 非空,就必须先用 search 解析集合,再对其执行操作。只有助手遵循这条规则,才能保护用户;工具描述在每次调用时读取,而技能会在制定计划前读取。仓库根目录就是插件(.claude-plugin/plugin.json),因此从克隆的仓库开始:

claude --plugin-dir /path/to/mailwarden

该插件已提交至 Anthropic 的社区市场;上架后,运行 /plugin marketplace add anthropics/claude-plugins-community,再运行 /plugin install mailwarden@claude-community,无需克隆即可完成相同操作。插件会启动精确的已发布版本(npx -y mailwarden@<version>;发布脚本会同时更新 server.json 中的版本),因为目录审核的是特定修订版,并拒绝使用会运行 npm 日后提供的任意版本的启动器。插件会运行完整的工具集 — 如需使用更精简的层级(MAILWARDEN_TOOLS=read)或第二个账户,请改用带有所需环境变量的 claude mcp add(参见配置和多个账户)。 这种方式会获取服务,但不包含技能:npm 包只包含服务,技能位于本仓库的 skills/ 文件夹中 — 若要在 claude mcp add 配置中保留 triage,请将克隆仓库中的 skills/triage 复制到 ~/.claude/skills/。

Claude Desktop — 添加到 claude_desktop_config.json:

{
  "mcpServers": {
    "mailwarden": { "command": "npx", "args": ["-y", "mailwarden"] }
  }
}

或者安装 MCPB 捆绑包(mailwarden-<version>.mcpb,自 0.10.0 版本起附在 GitHub 发布页面中)作为 Desktop 扩展 — Settings → Extensions → Install extension… — 其中包含同一服务,运行时无需其他依赖(无需 npx;Claude Desktop 会提供 Node 运行时),工具层级可通过设置选择。该捆绑包由打包后的 npm 包构建(文件集合与已发布版本相同;npm run mcpb,并在 CI 中验证:完成校验、解包和启动),还包含其声明支持的各个平台对应的 OS 密钥库绑定,且由锁文件固定版本并校验哈希。 一次性运行 npx -y mailwarden --auth 仍然适用(需要 Node 运行一次)— 捆绑包会读取相同的 ~/.mailwarden/ 令牌。

Smithery — 在目录中的条目为 csitte/mailwarden。Smithery 将于 2026 年 10 月 31 日关闭其 CLI、工具箱、网关和账户,仅保留公共目录(公告),因此不再提供 Smithery 安装方式:请使用上文的 npx 或 .mcpb 安装。该日期之后是否保留此条目由 Smithery 决定。

远程(Streamable HTTP) — 用于 VPS / claude.ai 自定义连接器:

# Loopback + token required by default. For real hosting, bind outward and keep the token:
MAILWARDEN_TOKEN=<secret> MAILWARDEN_HOST=0.0.0.0 npx -y mailwarden --http   # :8787/mcp

然后在 claude.ai 中:Settings → Connectors → Add custom connector → 输入你的 https://your-host/mcp URL。在 Claude Code 中:claude mcp add --transport http mailwarden https://your-host/mcp。

多个账号

一个 OAuth 应用(一个 credentials.json)可以授权多个 Gmail 账号。每个账号都会将自己的刷新令牌保存在单独的文件中,由 MAILWARDEN_ACCOUNT 选择:

mailwarden --auth --account work        # stores token.work.json
mailwarden --auth --account personal    # stores token.personal.json

每个账号注册一次 MCP 服务,即可让它们并行运行,并为每个实例设置各自的 MAILWARDEN_ACCOUNT。每个实例都有自己的令牌、获批的作用域和工具集;工具调用只会作用于发起调用的实例所对应的账号,不会作用于其他账号:

{
  "mcpServers": {
    "gmail-work":     { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "work" } },
    "gmail-personal": { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "personal" } }
  }
}

账号名称不区分大小写——名称会成为文件名,因此在 Windows/macOS 上,Work 和 work 会指向同一个文件。mailwarden 会将名称转换为小写(--account Work → token.work.json),确保一个名称始终只对应一个邮箱。

--auth 写入哪个文件只取决于 --account / MAILWARDEN_ACCOUNT,与您在浏览器中选择的账号无关。因此,如果不指定 --account 就授权第二个邮箱,目标会是第一个邮箱的令牌文件;所以 --auth 会先进行检查,并拒绝覆盖其他邮箱的令牌;也可以使用 --force 明确覆盖。两种设置不能互换:如果要从一个配置目录管理多个邮箱,应使用 MAILWARDEN_ACCOUNT(它会选择 token.<name>.json);如果要将整个目录移到别处,则应使用 MAILWARDEN_DIR——这适合完全隔离不同配置,但无法在同一个目录中添加第二个账号。克隆仓库后运行 npm run auth 时,这两个变量都不会传入,也就是说它始终使用默认账号。

mailwarden --check 会显示当前账号,并列出它找到的其他账号。如果没有设置 MAILWARDEN_ACCOUNT,所有操作都会像以前一样使用默认的 token.json——完全向后兼容。

两个以上的账号

两个条目是最简单的情况。账号再多一些时,这种设计的两个特性就会变得重要。

每个实例都会提供自己的工具。 权限层级分为 11 个 read 工具、16 个 manage 工具和 4 个 filters 工具,因此完整实例会公布 31 个工具,四个实例则会公布 124 个。按需搜索工具集的客户端可以应对这一规模;将所有定义都放入上下文的客户端则不行。

同一个客户端中的多个账号共用一个模型上下文。 账号边界会将一次调用绑定到一个邮箱,但不会阻止模型读取一个邮箱中的文本后,发起针对另一个邮箱的调用,因为所有工具集都面向同一个模型。这是边界本身的局限,并非缺陷;请参阅 SECURITY.md 中的威胁 8。

一个做法就能同时解决这两个问题:只为一个邮箱提供写入工具,其余邮箱仅提供 read 工具。

{
  "mcpServers": {
    "gmail-main":   { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "main" } },
    "gmail-work":   { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "work",   "MAILWARDEN_TOOLS": "read" } },
    "gmail-club":   { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "club",   "MAILWARDEN_TOOLS": "read" } },
    "gmail-archive":{ "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "archive","MAILWARDEN_TOOLS": "read" } }
  }
}

这样做会同时带来三项好处:read 条目只会请求 gmail.readonly,这是 Google 能够强制禁止发送邮件的唯一作用域,而不是依靠 mailwarden 的工具集来限制;四个实例合计提供 64 个工具,而不是 124 个;并且,即使向其中任一实例注入指令,也找不到可用于操作其他邮箱的写入工具。如果某个只读邮箱需要清理,只需在清理期间为对应条目启用 manage,而不必永久启用。

使用独立客户端或独立会话可以彻底避免共享上下文,但代价是无法同时查看两个邮箱。如果确实需要为多个邮箱提供写入工具,这种方式值得考虑;否则,划分权限层级是成本更低的边界。

没有任何工具会跨邮箱读取数据。 search、triage_digest 和 list_subscriptions 都只会处理其所属实例配置的那个账号,因此,像“哪些简报会同时发送到这四个邮箱?”这样的问题需要调用四次,再由调用方汇总答案。在这种规模的配置中,首次执行任何更改操作前调用 get_profile 值得考虑——它会显示实际操作的邮箱。

源码

git clone https://github.com/csitte/mailwarden && cd mailwarden
npm install && npm run build
node dist/index.js --auth

克隆仓库时不会附带 CLAUDE.md:本仓库的工作说明是一份内部文档,并未纳入版本控制。构建、测试和 npm run smoke 都不需要这份文档。贡献者——或编码 Agent——实际需要了解的规则,见 CONTRIBUTING.md(不可更改的设计规则、构建/测试流程)和 SECURITY.md(这些规则所依据的威胁模型)。

配置(环境变量)

变量含义
MAILWARDEN_DIR配置目录(默认为 ~/.mailwarden)
MAILWARDEN_CREDENTIALScredentials.json 的路径
MAILWARDEN_ACCOUNT选择一个命名账户(其令牌文件为 token.<name>.json;名称会转换为小写);未设置 = 默认的 token.json。参见多个账户
MAILWARDEN_TOKEN_PASSPHRASE使用派生自该密码短语的密钥(AES-256-GCM)加密静态存储的 token.json,而不是使用密钥库密钥;优先于密钥库;设置后需重新运行 --auth
MAILWARDEN_KEYSTOREauto(默认):在可用的操作系统密钥库中用密钥加密新令牌。off:永不写入密钥库——此时新令牌会使用密码短语存储,否则以明文存储。此设置仅控制写入:已使用密钥库密钥存储的令牌仍需要该密钥
MAILWARDEN_AUTO_SWEEP1 → 启动时以及运行期间每小时执行一次延后处理扫描(会写入标签——需要 manage/gmail.modify 范围;仅授予 read 权限时无法扫描)
MAILWARDEN_DOWNLOAD_DIR将 download_attachment 和 export_thread 限制在此目录内。若未设置,客户端会选择此进程可写入的任意路径——除非 manage 层级已关闭,否则 --http 会在启动时发出警告
MAILWARDEN_CONFIRM在 create_filter、unsubscribe 和 bulk_unsubscribe 执行前,通过 MCP 征询对话框向客户端用户确认——答案不会经过模型,因此遭到提示词注入的邮件无法提供答案。ask:在客户端支持征询时询问,不支持时继续执行。require:不支持时拒绝执行。off(默认):从不询问——否则无人查看对话框的无人值守运行会失败。用户拒绝时会返回错误 declined_by_user,且不会执行任何操作。不会询问是否执行试运行
MAILWARDEN_READONLY1 → 仅注册读取工具(search/get_thread/read_attachment/list_labels/list_snoozed/get_profile/what_changed/triage_digest/follow_ups/list_unsubscribe/list_subscriptions)。这是 MAILWARDEN_TOOLS=read 的简写
MAILWARDEN_TOOLS以逗号分隔的工具层级,用于发布工具:read、manage、filters(默认:全部)。同时决定 --auth 请求的 OAuth 范围。例如,read,manage 会移除过滤器工具及其 gmail.settings.basic 范围
MAILWARDEN_DEBUG1 → 打印完整错误和堆栈跟踪,而不是单行消息(用于提交错误报告)
PORTHTTP 端口(默认 8787)
MAILWARDEN_HOSTHTTP 绑定地址(默认 127.0.0.1;例如,设置为 0.0.0.0 可进行远程托管)
MAILWARDEN_TOKENHTTP 端点的 bearer 令牌——除非另行覆盖,否则 --http 必需
MAILWARDEN_ALLOW_NO_TOKEN1 → 允许 --http 不使用令牌(仅限可信/隔离网络)
MAILWARDEN_ALLOWED_HOSTS额外允许的、以逗号分隔的 host:port 值,适用于环回 Host 许可名单

状态

项目已可正常使用,并用于日常邮箱自动化。核心 Gmail 工具和延后处理功能基于 @googleapis/gmail 实现,并由 vitest 测试套件覆盖(1709 项测试——npm run coverage)。当前版本:请参见上方的 npm 徽章、更新日志或发布版本。欢迎提交 PR——CONTRIBUTING.md 介绍了构建/测试流程以及不可更改的设计规则。

许可证

MIT © C.Sitte Softwaretechnik

评论

支持 Markdown

第一次接触 MCP?

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

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