关于 Apple Mail
Apple Mail 是用于访问已同步到 macOS Mail 应用的邮件的 MCP 服务,无需邮件凭据,也不进行网络调用。它结合索引搜索、邮件读取和操作;默认不注册写入工具。
主要功能
- 01列出账户、邮箱和邮件
- 02搜索邮件及正文
- 03读取邮件会话和附件
- 04统计邮件总数与未读数
- 05默认禁用写入操作
适用场景
- 01按筛选条件或正文查找邮件
- 02阅读邮件会话及其附件
- 03查看邮箱和未读邮件数量
- 04启用写入前保持邮件操作不可用
MCP 相关
向 MCP 客户端提供 Apple Mail 账户、邮件、搜索、读取和操作工具。只有启用 `APPLE_MAIL_ALLOW_WRITES` 后才会注册修改类工具。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
npx -y @mgcrea/mcp-apple-mailAPPLE_MAIL_ALLOW_WRITESRegister the mutating tools. Off by default, and the gate decides whether the write tools exist at all rather than merely refusing — an agent with writes off cannot see them.APPLE_MAIL_ACCOUNTSRestrict every read and write to these Mail accounts, by name. Bounds reading, not just writing.APPLE_MAIL_ALLOW_FILE_SENDLet the compose tools attach a local file and send in the same call. Attaching to a draft needs only writes; this is off by default because an immediate send puts the file in someone's inbox with nobody reviewing it.本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
@mgcrea/mcp-apple-mail
适用于 macOS Apple Mail 应用的 Model Context Protocol 服务。读取、搜索和操作已同步到 Mac 的邮件——无需 IMAP 凭据、OAuth,也不会有邮件离开本机。
非官方。 与 Apple 无关。它通过 Mac 上已有的 Mail 应用执行操作。
状态
搜索、列表、计数、会话串联、正文读取、正文_搜索_和所有修改操作均已实现。
工作原理
Apple Mail 提供了两种截然不同的接口,本服务会各取所长。采用这种划分并非出于偏好,而是基于对一个包含 29,617 封邮件的真实邮箱进行测量得出的结果:
| 操作 | Apple Events (AppleScript/JXA) | 结论 |
|---|---|---|
| 列出 4 个账户和所有邮箱 | 0.6 s | 足够快 |
| 邮箱总数 / 未读数 | 295 ms / 76 ms | 足够快 |
| 在包含 29 k 封邮件的邮箱中按 id 查找一封邮件 | 0.10 s | 足够快 |
| 获取 N 封邮件的各个字段 | ~130 ms + 每封邮件 42 ms | 约 50 封以内可用 |
| 循环读取每封邮件的一个属性 | 每封约 250 ms | 绝不要这样做 |
messages whose read status is false | 74 秒 | 无法使用 |
因此,搜索完全不能通过 Apple Events 实现。它必须读取 Mail 自己的 SQLite 索引(~/Library/Mail/V10/MailData/Envelope Index),也就是 Mail 自身进行搜索所用的索引。这形成了三条路径:
- AppleScript 路径 — 账户、邮箱、计数、按 id 查找邮件,以及所有修改操作。这也是_权威_路径:写入后,结果以 Mail 重新读取的内容为准,而非索引中的内容。
- 索引路径 — 只读 SQLite,用于搜索和筛选。需要完全磁盘访问权限。
- 正文路径 — 从磁盘上的
.emlx文件读取邮件正文和附件。需要完全磁盘访问权限,并会在必要时回退到逐封邮件的 Apple Event。
绝不会写入索引。索引由 Mail 管理并保持打开,且会与 IMAP 同步;写入索引会造成延迟显现的损坏。
权限
macOS 中有两种独立权限,各司其职。两种权限都不需要授予 Mail.app——需要权限的是_读取器_,而不是 Mail。
| 权限 | 用途 | 未授予时 |
|---|---|---|
| 自动化 → Mail | 所有操作 | 服务无法执行任何操作;你会收到 -1743 错误 |
| 完全磁盘访问权限 | 搜索、邮件正文、附件 | 服务仍可运行:账户、邮箱、计数和数量受限的列表功能都可正常使用 |
自动化权限需要授予启动服务的应用(Terminal、iTerm、VS Code、Claude 等)。macOS 会在首次触发 Apple Event 时提示,通常只需点击“允许”。
完全磁盘访问权限比较棘手。
为什么不应直接授予编辑器此权限
macOS 会将进程的文件访问权限归属到其_负责进程_——也就是启动链顶端的应用。当 Claude 启动此服务时,启动链如下:
launchd → Visual Studio Code.app → Code Helper → claude → node dist/cli.js
因此必须将权限授予 VS Code;随之而来的是,该编辑器运行的所有扩展、任务和终端命令都将获得读取整个磁盘的权限:Messages、Safari 历史记录、SSH 密钥,以及其他应用的容器。这远远超出了“可以搜索我的邮件”的权限范围,也会使 APPLE_MAIL_ACCOUNTS 失去作用,而该设置正是为了限制访问范围。
你无法通过将权限授予 .mcp.json(它是数据,不是代码)或 node(它不是负责进程,而且机器上的所有 node 程序共用它)来绕过这一点。
替代方案
安装 Cupertino.app,这个带签名的菜单栏应用正是本软件包的开发环境。它持有一项“完全磁盘访问”授权,并在其下运行此服务,因此权限授予的是经过公证的二进制文件,而不是你的编辑器:
brew install --cask mgcrea/tap/cupertino
实测结果:授予带签名 .app 的“完全磁盘访问”和“自动化”权限会向下继承两层,传递给它启动的进程。因此,作为孙进程的 node 可以读取 Envelope Index,而 tccd 会将孙进程 osascript 解析为该应用,而不是启动它的程序。
此软件包的早期版本曾介绍一个小型编译启动器,它通过私有 API 让自身成为自己的责任进程。现在已不再构建或分发该启动器——已经没有需要逃离的东西了——不过 native/launcher.c 仍保留在仓库中,作为对这个问题最清晰的说明。
如果你不想安装该应用,向编辑器授予“完全磁盘访问”权限也确实有效。操作前,请先阅读上面的段落,了解该授权实际涵盖的内容。
授予权限后,请重启 MCP 主机。apple_mail_diagnostics 会报告缺少哪项权限,以及它会造成什么阻碍。
还有一点值得了解:对受 TCC 保护的文件调用 stat() 会成功——即使没有“完全磁盘访问”权限,你也能查看索引的大小和修改时间,只有读取操作会被拒绝。因此,“文件存在”并不能证明已授予权限;access(R_OK) 才能。
这种安装方式只是权宜之计。docs/distribution.md 介绍了后续计划——发布经过签名和公证的应用,让授权在更新后仍然有效,也无需任何人安装编译器——以及为什么 App Store 无法上架这些内容。
快速开始
# A. from npm
npx -y @mgcrea/mcp-apple-mail
# B. from source
pnpm install && pnpm build && node dist/cli.js
将其接入 Claude Code(.mcp.json)或 Claude Desktop:
{
"mcpServers": {
"apple-mail": {
"command": "npx",
"args": ["-y", "@mgcrea/mcp-apple-mail"],
"env": {
// Off by default. With it off the write tools are not registered at all.
"APPLE_MAIL_ALLOW_WRITES": "0",
},
},
},
}
直接检查这些工具:
npx @modelcontextprotocol/inspector node dist/cli.js
工具
| 工具 | 功能 |
|---|---|
apple_mail_diagnostics | 查看服务当前能做什么以及原因。遇到任何异常时,请先调用此工具——它会指出要打开的具体“系统设置”面板。 |
apple_mail_list_accounts | 列出账户 UUID、地址和邮箱名称。从这里开始。 |
apple_mail_list_mailboxes | 列出邮箱,可选包含邮件数量——一次从索引读取;不使用索引时每个约需 0.3 秒。 |
apple_mail_list_messages | 列出某个邮箱中最新的 N 封邮件,每封邮件都有一个 ref。 |
apple_mail_search_messages | 支持任意组合的筛选条件。body 可搜索邮件正文——参见正文搜索。 |
apple_mail_count_messages | 按数据来源标注总数和未读数。 |
apple_mail_list_open_drafts | 列出当前在 Mail 中打开的草稿;每封草稿都有一个 composer 句柄、收件人和独立于引用内容的正文,正文由应用的 apple_desktop_mail_composers 动词读取。 |
每封邮件都有一个不透明的 ref(m1:<accountUuid>/<mailbox>#<id>),读取和操作工具都使用它。该标识带有版本信息并包含邮箱,因此行 ID 永远不会被用于错误的邮箱。请勿手动构造。
附件
邮件几乎总是将附件正文存储在邮件文件之外的伴随目录树中——在真实的邮件存储中,抽样检查的附件没有一个是内嵌的。因此,仅解析邮件无法确定附件的大小,也无法判断附件是否可获取,而两个看似合理的捷径都不正确:
- MIME 部分非空不代表字节数据存在。剥离内容后会留下一字节的分隔符空白,看起来就像存在一个 1 字节的附件。
X-Apple-Content-Length不是字节大小。它记录的是 base64 编码后的长度,所以一个 164,156 字节的 PDF 显示为 224,634。
因此,list_attachments 会报告磁盘上文件的 sizeBytes,并提供一个 retrievable 标志,说明 save_attachment 是否确实能够成功,而不是根据邮件进行猜测。
安全性
影响范围。 此服务可以读取你的整个邮件归档;启用写入后,还可以用你的身份发送邮件。这里有两项相互独立的控制措施:
APPLE_MAIL_ALLOW_WRITES(默认关闭)控制所有变更操作。关闭时,写入工具不会注册——对模型不可见,而不仅仅是拒绝执行。APPLE_MAIL_ACCOUNTS限制哪些帐户可见。这是读取侧的控制,ALLOW_WRITES无法覆盖它。该限制集中在一处强制执行,因此任何查询路径都无法绕过。
实现后,发送操作默认会保留一封草稿供你审核;要实际发送,必须同时启用 ALLOW_WRITES 并明确指定 confirm: true。
不含机密信息。 此服务不持有任何凭据——它通过你授予宿主应用的 macOS 权限进行访问。这里没有任何信息可泄露,也不会向任何地方发送任何内容:完全不会进行网络调用。
附件。 save_attachment 只能写入 APPLE_MAIL_ATTACHMENT_DIR 目录中,并且会先将文件名转换为基本文件名——即使发件人将附件命名为 ../../../.ssh/authorized_keys,你下载文件夹中也只会得到一个名为 authorized_keys 的文件,仅此而已。除非你明确要求,否则绝不会覆盖现有文件。
反过来,send_message、reply_to_message 和 forward_message 接受 attachments,即 Mail 会读取并附加的绝对路径。默认情况下,邮件会先保存为草稿,你可以在发送前查看文件。除非设置了 APPLE_MAIL_ALLOW_FILE_SEND,否则不允许在一次调用中附加并发送(sendNow),因为这样无人审核,而路径可能让此服务能够读取的任何内容都被邮寄出去。
不使用 shell。 此软件包中唯一会生成进程的地方使用 execFile,绝不使用 exec,也绝不会将调用方输入插入脚本文本。脚本是通过 stdin 传给 osascript 的静态常量;每个值都以 JSON 参数的形式传递。名为 "; do shell script "touch /tmp/pwned"; // 的邮箱名称只是数据,不是语法——对此有专门的测试,并且还有一道防护机制,会拒绝运行任何包含 ${ 的脚本。
依赖项。 两项:MCP SDK 和 zod。SQLite 来自 Node 24 内置的 node:sqlite。
配置
| 变量 | 默认值 | 说明 |
|---|---|---|
APPLE_MAIL_ALLOW_WRITES | 0 | 注册会修改数据的工具。 |
APPLE_MAIL_ACCOUNTS | all | 以逗号分隔的账户名称或 UUID。 |
APPLE_MAIL_DEBUG | off | 将详细日志输出到 stderr。 |
APPLE_MAIL_INDEX_MODE | auto | auto | ro | immutable | off。off 会完全禁用索引通道。 |
APPLE_MAIL_ROOT | auto | 覆盖 Mail 的数据根目录。通常会从 Mail 本身发现。 |
APPLE_MAIL_ENVELOPE_INDEX | auto | 显式指定索引路径,用于测试和取证副本。 |
APPLE_MAIL_OSASCRIPT_TIMEOUT_MS | 30000 | 为首次运行时会阻塞的权限提示设置的超时时间。 |
APPLE_MAIL_DEGRADED_MAX_MESSAGES | 50 | Apple Events 列表通道的上限。 |
APPLE_MAIL_MAX_RESULTS | 200 | 搜索结果上限。 |
APPLE_MAIL_BODY_MAX_BYTES | 262144 | 正文截断限制,用于保护上下文窗口。 |
APPLE_MAIL_ATTACHMENT_DIR | ~/Downloads | 附件只能保存到此目录。 |
APPLE_MAIL_ALLOW_FILE_SEND | off | 允许撰写工具在同一次调用中附加本地文件并发送。 |
APPLE_MAIL_ALLOW_WATCHES | off | 注册 watch 和 unwatch:供 Agent 读取、供主机订阅的资源。 |
APPLE_MAIL_ALLOW_EVENTS | off | 提供实验性的 mail.received 事件(仅限 2026-07-28 的 MCP Events 扩展草案)。 |
APPLE_MAIL_MAILBOX_CACHE_TTL_MS | 60000 | 账户/邮箱映射的缓存时长。 |
注意事项
这些问题很容易让人踩坑,因此在此记录,免得别人再重新发现:
- Mail 的
unread count可能完全不准确。 在这台机器上,一个邮箱报告的unreadCount = 0,但messages whose read status is false的计数是 1618,而 Mail 自己的徽标 显示 37——同一个邮箱出现了三个数字。这是一个缓存值。因此,报告计数时会注明其来源,而不会合并。 - Gmail 账户会将所有内容保存在
[Gmail]/All Mail中。 INBOX 成员资格是一个 标签,因此 直观的索引查询(WHERE mailbox = ?)会返回空收件箱。邮箱名称会通过逐级剥离[Gmail]/前缀来解析。 - 磁盘上的目录布局也有嵌套,而 ref 无法识别这些嵌套。
All Mail并不在<account>/All Mail.mbox中,而是在<account>/[Gmail].mbox/All Mail.mbox中;像Work/Projects这样的标签会嵌套两层。索引记录了完整路径,但上面的逐级剥离会在生成 ref 之前将其 简化为叶节点,因此文件通道只能获取All Mail,而没有其他信息。因此,它会遍历账户根目录下的*.mbox目录(绝不进入Data/),并在叶节点名称不唯一时,选择实际包含该 rowid 的容器。此处 使用扁平连接会悄无声息地禁用 Gmail 账户上的所有邮件文件功能。 - 在受 TCC 保护的路径上,
existsSync也会成功。 它基于 stat,因此回答的是“是否存在”,而不是“是否 可以读取”——返回EPERM的是readdirSync和readFileSync。是否存在与是否可读是两个不同的问题, 因此apple_mail_diagnostics会读取真实邮件文件的一个字节,而不是对其执行 stat;查找操作也能区分 “路径错误”和“没有权限”,而不会将两者都归咎于“完全磁盘访问权限”。 - 用
immutable=1打开索引是错误的做法,尽管这是常见建议。它会告诉 SQLite 忽略-wal文件,而 Mail 运行在 WAL 模式下,因此读取时可能会漏掉尚未检查点写入的内容——恰好是 Agent 通常会被询问的最新邮件。 请注意,这是一个竞态,而非必然如此:在一个正在运行的 437 MB 索引上,同时探测两种模式,且存在一个 1 MB 的-wal文件时,返回了相同的 MAX(ROWID),因为最新邮件恰好已经完成检查点写入。默认使用mode=ro是为了避免这个问题,而不是因为已经观察到数据过时。 - 读取邮件不会启动 Mail。 如果 Mail 未运行,读取工具会正常报错,而不会启动它,因为启动 Mail 会抢占 焦点并开始同步。
正文搜索
search_messages 接受一个 body 词条,这是唯一没有索引支持的筛选条件。
没有可供索引的内容。Envelope Index 不包含 FTS 表,而 Spotlight 卷索引完全排除了 ~/Library——无论查询如何措辞,mdfind 都不会返回该路径下的任何结果。这是 Spotlight 的覆盖范围所致,与邮件无关。Mail 自身的正文搜索依赖 CoreSpotlight 提供的数据,由提供数据的应用通过 CSSearchQuery 查询,我们无法在此访问。测量数据见
docs/mail-body.md。
因此,索引会缩小范围,扫描只读取剩余候选项。耗时与候选项数量呈线性关系,在一个包含 181,734 封邮件的存储中测得,每封邮件耗时 0.48 ms:
| 其他筛选条件之后的候选项 | 耗时 |
|---|---|
| 100 — 筛选范围较小 | 48 ms |
| 500 | 242 ms |
| 1,932 — 一个邮箱的 90 天邮件 | 0.9 s |
| 6,566 — 所有邮箱的 90 天邮件 | 3.2 s |
| 182,329 — 未设置任何筛选条件 | 88 s,无法使用 |
因此,限制会明确告知,而非暗中执行。超过 APPLE_MAIL_BODY_SCAN_MAX(2,000)时,搜索会返回 degraded,指出候选项数量和上限,并且不执行扫描。如果静默地只检查最新的 N 封邮件,那么对于较旧邮件中的内容,返回“未找到”就无法与确实不存在区分开来;模型无法判断两者的区别,而它可以读取拒绝信息并据此采取行动。
需要了解的两个影响:
- 将
body与缩小范围的筛选条件结合使用。mailbox、sender、dateFrom——任选其一即可。对数百个候选项进行正文搜索几乎是即时完成的。 - 扫描会读取每个文件开头的
APPLE_MAIL_BODY_SCAN_BYTES(64 KB)。 实际存储中 79% 的字节是 base64 编码内容,文本搜索不会匹配;而 MIME 会将文本部分放在附件之前——因此,这种做法舍弃了没人需要的文件尾部内容,换取每个候选项都能降低开销。若词条藏在读取上限之后,就会被漏掉;如果这比延迟更重要,可以调高上限。
开发
pnpm install
pnpm dev # tsdown --watch
pnpm test # vitest, fully offline
pnpm typecheck
pnpm lint && pnpm format:check
pnpm probe # the phase 0 spike (needs Full Disk Access)
测试离线运行且完全隔离:进程边界是注入的,因此队列、注入探针和信封处理都能真实运行,而无需启动 osascript 或触碰 Mail。
许可证
MIT
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。