mcpField
M
mail-muncher

mail-muncher

本地服务
craigjmidwinter.github.io通信协作310 周前更新

关于 mail-muncher

mail-muncher 将 IMAP 或 Gmail 邮件筛选后归档到磁盘,供自动化程序和 Agent 使用。它支持定时或轮询投递,也可通过 MCP 查询同一归档。

主要功能

  1. 01只读获取 IMAP 和 Gmail 邮件
  2. 02有序规则与实时文件筛选
  3. 03归档为 EML 和 Markdown
  4. 04增量且去重的投递
  5. 05支持定时、守护进程和 MCP 模式

适用场景

  1. 01按发件域名归档求职回复
  2. 02为客服机器人收集供应商邮件
  3. 03按会话或日期查询归档邮件

MCP 相关

可作为 stdio MCP 服务运行,提供规则和邮件查看、搜索、会话读取及同步工具。访问范围限制在配置的归档目录;同步只会添加文件。

配置

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

mcp.json · 通用配置
{
"mcpServers": {
"craigjmidwinter-mail-muncher": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/craigjmidwinter/mail-muncher:0.4.0"
]
}
}
}
启动命令docker run -i --rm ghcr.io/craigjmidwinter/mail-muncher:0.4.0

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

安装包
ocighcr.io/craigjmidwinter/mail-muncher:0.4.0

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

邮件吞噬兽咬着一封信的档案图案
mail-muncher

mail-muncher

CI Go Reference Go version Release License: MIT

为程序提供专属的只读邮箱,精准筛选出它请求的邮件,并将邮件作为文件写入磁盘。

mail-muncher 从邮件服务商获取邮件,按顺序应用规则进行筛选,并将匹配的邮件写入目录——保留字节原样的 .eml 文件,以及可选的 Markdown 格式文件,其中邮件头以 YAML 前置元数据呈现,正文以文本形式呈现,附件则提取到同一目录中。规则可以从一个由其他程序管理的纯文本文件中读取筛选条件,而 mail-muncher 会在每个周期开始时重新读取该文件。该程序只需更改文件中的一行,紧接着的下一个周期就会投递不同的邮件——无需编辑配置、重启或重新部署。

它可以从任何 IMAP 邮箱读取邮件——Gmail、Fastmail、iCloud、Proton Bridge、工作邮箱或你自己的服务端——也可以通过 Gmail API 和只读 OAuth 作用域读取邮件。它既可以单次运行以配合 cron,也可以作为轮询守护进程运行,还可以作为 stdio MCP 服务运行,让 Agent 直接查询。所有运行模式都会生成相同的机器可读操作清单,并且都不会向你的邮箱写入任何内容。

连接邮箱的两种方式

安装任何内容之前,先选一种。两种方式都受支持,后续的一切——规则、格式、文件名、归档布局、MCP 工具——都完全相同。

provider: imapprovider: gmail
设置时间约 2 分钟约 10 分钟,需在 Google Cloud Console 中操作
注册内容无你自己的 Google Cloud 项目和桌面应用 OAuth 客户端
凭据从邮箱服务商自己的设置页面获取的应用密码OAuth 令牌,作用域为 gmail.readonly
凭据的权限范围完整的邮箱凭据。 应用密码可以发送和删除邮件只读,仅此而已
谁来强制只读mail-muncher 自身的代码Google
有效期无测试模式的 OAuth 授权屏幕下每 7 天过期;必须每周重新运行 mail-muncher auth
密钥存放位置密码管理器原本存放密码的位置:运行 password_cmd,其标准输出即为密码。这里刻意不提供 password 键token.json,权限模式为 0600,由 mail-muncher auth 写入
支持的邮箱mailboxes: 中列出的文件夹;默认是 [INBOX]整个 Gmail 账号,除非主动启用垃圾邮件和废纸篓,否则不包含这两者
适用服务Gmail、Fastmail、iCloud、Proton Bridge、工作账号、可自托管服务仅 Gmail
额外步骤无。这种方式不需要 auth 命令按照 docs/gmail-setup.md 操作后运行 mail-muncher auth

上文的约 2 分钟、约 10 分钟和 7 天,与 mail-muncher init 以及未配置时显示的指导信息中的数字相同,因为这些数字正是决定因素。

两种方式都能真正保证只读,但保证的方式并不相同,混为一谈是不诚实的。

  • Gmail:由 Google 强制执行。 唯一请求的作用域是 gmail.readonly。返回的令牌无法发送、删除、添加标签或修改邮件——这不是因为 mail-muncher 不愿执行,而是因为 Google 会拒绝调用。这个程序中的漏洞也无法触及你的邮箱。
  • IMAP:由 mail-muncher 强制执行。 IMAP 没有可申请的只读凭据。应用密码是完整的邮箱凭据;该协议会允许持有者删除文件夹。mail-muncher 的做法则是拒绝这样做:每个文件夹都使用 EXAMINE 打开,从不使用 SELECT;每封邮件都使用 BODY.PEEK[] 获取,从不使用 BODY[](因此邮件不会被标记为已读);并且此服务提供方的代码路径中,任何地方都不会发出 STORE、APPEND 或 EXPUNGE。之所以双重防护,是因为服务端没有义务保护客户端免受自身操作的影响。这是强有力且可审计的保证——只是由本程序提供的保证,而非邮箱服务商提供的保证。

如果没有特别需要使用 Gmail API 的理由,请从 IMAP 开始。它也适用于 Gmail 账号,而且快速入门使用的就是这种方式。

问题

自动化流程需要读取一些邮件。求职跟踪器想获取你申请过的公司的回复。客服机器人想获取来自某个供应商域名的邮件。研究 Agent 想把三家出版商的所有新闻邮件都以可实际阅读的文本形式获取。

常见的解决办法都不理想。把邮箱凭据交给流程,它就能读取(也能发送和删除)所有邮件。为它接入邮件 API,你就得在每个需要邮件的流程中维护 OAuth 流程、同步游标、MIME 解析和去重机制。或者把筛选条件硬编码到配置文件中,这样每次更改它要获取的内容,都得修改配置并重新部署。

mail-muncher 将这些工作一分为二。它负责凭据、增量同步、解析和去重。使用它的程序只需维护一个列出所需内容的文本文件,以及一个读取结果的目录——如果更喜欢主动查询而非持续监听,还可以使用一组针对同一目录的 MCP 工具。

Agent 工作流

有两种受支持的模式,并且可以组合使用。根据你的 Agent 是自行循环运行,还是等待被调用来选择。

  • 文件投递 — mail-muncher 按计划运行并写入文件;Agent 读取目录。两者之间没有调用关系。下文介绍这种模式。
  • 工具调用 — Agent 通过 MCP 与 mail-muncher mcp 通信,直接提问:我订阅了什么、收到了什么、这个会话讲了什么、现在获取。参见模式 2:工具调用。

两种模式读取同一份归档,同时运行也很常见:守护进程填充目录,而 MCP 服务回答相关问题。

模式 1:文件投递

这个循环完全解耦:mail-muncher 从不调用 Agent,Agent 也不必调用 mail-muncher。两者共享磁盘上的两个路径。

1. Agent 声明它需要什么。 追加到它拥有的文件中:

mkdir -p ~/.local/share/agent
cat >> ~/.local/share/agent/domains.txt <<'EOF'
# domains this agent is currently interested in
acme.com
globex.io
EOF

2. mail-muncher 订阅该声明。 添加一条指向该文件的规则:

rules:
  - name: agent-inbox
    match:
      from_domains_file: ~/.local/share/agent/domains.txt
    dest: ~/mail/agent-inbox
    formats: [eml, markdown]

3. 每个周期都会重新读取该文件。 通过 cron 运行,或让守护进程持续运行:

mail-muncher run                 # one cycle — the cron entrypoint
mail-muncher daemon --interval 5m  # poll forever

4. 匹配的邮件会以文件形式写入 dest,供 Agent 读取。

~/mail/agent-inbox/
└── 2026/
    └── 07/
        ├── 1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.eml
        ├── 1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.md
        └── 1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.attachments/
            └── offer.pdf

.md 是可供使用的呈现形式——解析 frontmatter,将正文提供给模型,并从同级目录打开附件:

---
subject: 'Re: Your application for Senior Engineer'
from: Jane Doe <[email protected]>
from_address: [email protected]
from_addresses: [[email protected]]
to: [[email protected]]
to_addresses: [[email protected]]
date: 2026-07-28T09:15:00Z
message_id: <[email protected]>
thread_id: 18fe9c0d1a2b3c4d
thread_id_source: provider
in_reply_to: <[email protected]>
account: personal
rule: job-search
labels: [INBOX]
attachments: [offer.pdf]
---

Hi there,

Thanks for applying.

## Attachments

- [offer.pdf](1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.attachments/offer.pdf)

每条消息都有 thread_id,且它绝不会为空,因此只需按一个字段 sort,就能将目录整理为各个对话——无需重新组装引用链。

5. 也可以选择使用清单,而不是遍历目录树。 --json 会将本周期的机器可读记录写入 stdout,每个账户一个对象;每条日志行则写入 stderr:

mail-muncher run --json 2>/dev/null | jq -r '.stored[].path'

完整约定:参见 docs/manifest.md。

以下三项特性使其适合安全地用于自主循环:

  • 从设计上只读。 mail-muncher 中没有任何操作会写入邮箱。对于 Gmail,这是由 Google 强制执行 gmail.readonly 范围;对于 IMAP,则使用 EXAMINE 和 BODY.PEEK[],完全没有写入路径。无论哪种情况,使用输出的程序——以及其中可能存在的任何错误——都无法发送、删除或修改邮件。关于你会获得这两种保证中的哪一种,请参见上文的比较。
  • 幂等投递。 消息文件名包含 account + ":" + message id 的摘要,因此其目标路径完全由消息身份决定。文件已存在表示“较早的周期已存储此文件”,而接收端不会写入任何内容。重新运行、丢失状态后重放、在周期中途崩溃,或两个 cron 调用重叠运行:目录树最终都会收敛,且不会重复处理任何内容。
  • 确定性路由。 规则按顺序排列,并采用首条匹配规则,因此每条消息只会由一条规则写入。为每个使用方配置自己的规则和 dest,每个使用方都会拥有一个不会被其他程序写入的独立邮箱。

投递内容是磁盘上的文件,这里没有任何服务监听网络。约定以目录为准,清单则是对变更内容的可选机器可读记录。

形态 2:工具调用

mail-muncher mcp 是一个基于已归档邮件运行的 stdio MCP 服务。Agent 发起请求;不会安排任何任务。

{
  "mcpServers": {
    "mail-muncher": {
      "command": "/usr/local/bin/mail-muncher",
      "args": ["mcp", "--config", "/Users/you/.config/mail-muncher/config.yml"]
    }
  }
}

五个工具:

工具解答的问题
list_rules我在收集什么?目前订阅了哪些发件人?每次调用时都会重新读取各个 from_domains_file。
list_messages收到了什么?可按规则、账户、线程或日期筛选;也可以选择按对话分组。
search_messages提及 X 的邮件在哪里?在主题、发件人、收件人、标签、附件名称和正文中进行子串搜索。
read_message获取一封邮件的完整内容——元数据、正文、附件名称和大小;还可以按顺序获取其整个线程。
sync获取一次新邮件,并返回与 run --json 写入的相同清单。

它对邮件只有只读权限:没有工具会发送、删除或修改任何内容;sync 是唯一会进行任何更改的工具,但它只能添加文件。 文件系统访问仅限于配置的规则 dest 根目录,因此无法访问或获知配置、已存储的凭据和状态目录。

即使未经配置,mcp 服务也会启动,这是有意为之。 如果客户端在尚无配置时启动 mail-muncher mcp,服务不会退出——它会完成握手、注册同样的五个工具名称,并将设置指引作为工具错误返回给每次调用,让 Agent 有内容可以转述,而不是只收到“服务启动失败”。如果你正在为操作人员接入此服务,这是预期行为,不是需要提交的错误。启动时也会将指引写入 stderr,客户端会在此处转存服务日志。

完整参考、客户端接入方式以及每个参数和返回字段: docs/mcp.md。

list_rules 是闭环的关键。Agent 将某个域名写入自己的文件,然后调用 list_rules,就能看到自己的订阅已反映在结果中——下一轮周期也会匹配同一份列表。

替代方案

采用前请先阅读。许多工具都能很好地完成获取、筛选和投递的工作,其中一些比这个工具更适合你的需求。

工具适合改用它的情况
getmail6你需要成熟且广泛打包的邮件获取工具。它支持 IMAP 和 Gmail OAuth2,可投递到 Maildir/MDAs,并通过外部程序筛选。如果使用者是人(或 mutt、notmuch),它会是更强的工具。
fdm你需要按规则投递到 Maildir 目录,且配置简洁、经过充分测试——这正是本工具的形态,只是没有外部筛选源。Gmail 通过应用专用密码 IMAP 接入。
lieer你想将整个 Gmail 邮箱双向同步到本地 Maildir,供 notmuch 使用,而不是从中筛选并提取部分邮件。
gmail-archive它几乎就是这个工具——通过 Gmail 查询增量写入 Maildir——如果仍在维护,本来会是显而易见的选择。但它自 2018 年以来就没有维护过。
gmail-exporter你需要一次性的、基于标签、适合电子表格的导出,而不是增量同步。
mbsync / offlineimap你需要复制整个邮箱,之后再在本地筛选。

这些工具都做不到,而本工具正是为此而存在:从其他程序拥有的文件中读取筛选条件,并在每个周期重新读取;输出适合程序消费,而不是供邮件客户端显示的内容。如果你不需要这两项功能,上面某个工具会更适合你,而且经过了更多年的实践检验。

安装

前两种方式都不需要 Go 工具链。

Homebrew
brew install craigjmidwinter/tap/mail-muncher

此操作会添加 craigjmidwinter/homebrew-tap tap,并安装预编译二进制文件。运行 brew upgrade mail-muncher 即可跟进新版本。

下载二进制文件

每个发行版 都提供适用于 macOS 和 Linux、支持 amd64 和 arm64 的压缩包,以及 checksums.txt 和对应的签名。

# Latest release, without the leading v. Set this by hand to pin a version.
VERSION=$(curl -fsSL https://api.github.com/repos/craigjmidwinter/mail-muncher/releases/latest \
  | sed -n 's/.*"tag_name": *"v\{0,1\}\([^"]*\)".*/\1/p')

OS=$(uname -s | tr '[:upper:]' '[:lower:]')     # darwin | linux
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')

curl -fsSLO "https://github.com/craigjmidwinter/mail-muncher/releases/download/v${VERSION}/mail-muncher_${VERSION}_${OS}_${ARCH}.tar.gz"
tar xzf "mail-muncher_${VERSION}_${OS}_${ARCH}.tar.gz" mail-muncher
sudo install -m 0755 mail-muncher /usr/local/bin/mail-muncher

如果二进制文件随后完全无法运行 —

bash: mail-muncher: cannot execute binary file: Exec format error

— 说明你下载的压缩包架构不匹配。这条消息来自内核,与 mail-muncher 无关,因此值得了解它的样子。将 uname -m 的结果与下载文件名中的 _amd64 / _arm64 进行比较;上面的 ARCH= 行会为你计算正确的架构,因此只有手动设置名称时才会遇到这个问题。

没有 root 权限? /usr/local/bin 需要 root 权限;~/.local/bin 不需要。去掉 sudo,改为安装到那里即可——mail-muncher 并不需要安装在系统级目录中:

install -d ~/.local/bin
install -m 0755 mail-muncher ~/.local/bin/mail-muncher

如果随后提示 mail-muncher“找不到命令”,说明 ~/.local/bin 不在你的 PATH 中;请在 shell 配置文件中添加它。

如果不更改目标目录就跳过 sudo,install 本身会报错 Permission denied——在 macOS 上,报错中提到的是临时文件而不是 mail-muncher,第一次看到时可能会让人困惑:

install: /usr/local/bin/INS@LPh1Hz: Permission denied     # macOS
install: cannot create regular file '/usr/local/bin/mail-muncher': Permission denied   # GNU

两种提示都表示同一件事:选择上面的 ~/.local/bin 方式,或者加回 sudo。

在 macOS 上,你自行下载的二进制文件会被 Gatekeeper 隔离。运行 xattr -d com.apple.quarantine /usr/local/bin/mail-muncher 清除隔离标记,或者使用上面的 Homebrew 安装方式,它会自动完成这一步。

验证下载的文件

这个工具会读取你的邮件。请确认压缩包是由发行工作流构建的。先验证校验和:

curl -fsSLO "https://github.com/craigjmidwinter/mail-muncher/releases/download/v${VERSION}/checksums.txt"

# Linux
sha256sum --check --ignore-missing checksums.txt
# macOS
shasum -a 256 --check --ignore-missing checksums.txt

然后验证 checksums.txt 上的签名。发行版使用 cosign 进行无密钥签名—— 无需获取公钥,也不需要任何人保管私钥。签名证书由发行工作流自己的 GitHub OIDC 身份颁发,并记录在公开的 Rekor 透明日志中,因此你验证的是“此文件由此仓库中的 release.yml 从某个标签构建”:

cosign 并非任何平台的默认安装项,也不在常见的发行版软件仓库中,因此此处出现 cosign: command not found 表示“尚未安装”,而不是“验证失败”。先安装它——运行 brew install cosign,或 go install github.com/sigstore/cosign/v2/cmd/cosign@latest,或从安装 文档获取发行版二进制文件。

curl -fsSLO "https://github.com/craigjmidwinter/mail-muncher/releases/download/v${VERSION}/checksums.txt.sig"
curl -fsSLO "https://github.com/craigjmidwinter/mail-muncher/releases/download/v${VERSION}/checksums.txt.pem"

cosign verify-blob \
  --certificate checksums.txt.pem \
  --signature checksums.txt.sig \
  --certificate-identity-regexp '^https://github\.com/craigjmidwinter/mail-muncher/\.github/workflows/release\.yml@refs/tags/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  checksums.txt

Verified OK 表示校验和文件真实可信;接着,sha256sum 步骤会验证你的压缩包与之匹配。cosign 3 会针对 --certificate 和 --signature 输出弃用提示——验证仍会继续执行,而且这些分离文件也能被 cosign 2 识别。

go install

如果你已经安装 Go 1.25 或更高版本,这是合适的方式:

go install github.com/craigjmidwinter/mail-muncher/cmd/mail-muncher@latest

请注意,使用 go install 构建的程序在 --version 中会报告 dev,因为版本信息是在链接时写入的,而 go 工具不会执行这一步。发行版二进制文件和 make build 会报告实际标签。如果你针对 go install 构建版本提交 bug,请说明安装的是哪个提交。

从源代码构建
git clone https://github.com/craigjmidwinter/mail-muncher
cd mail-muncher
make build          # -> ./mail-muncher, version stamped from git describe

如果你想检查发行版会包含哪些内容,可以运行 make snapshot 在本地构建完整的发行版压缩包(需要 goreleaser)。

下文提到的示例配置位于 examples/ 中—— imap.yml、minimal.yml 和 job-search.yml。它们也随每个发行版压缩包一起打包,因此下载二进制文件时也会获得这些配置。不过,入门并不需要它们:mail-muncher init 会从头创建配置。

容器镜像
docker pull ghcr.io/craigjmidwinter/mail-muncher:latest

支持 linux/amd64 和 linux/arm64,由与发布归档相同的二进制文件构建。镜像的默认命令是 mcp,因为通过 stdio 提供归档服务是容器适合的用法:客户端启动它、与它通信,然后停止它。run 和 daemon 也能运行——覆盖默认命令即可——但在主机上,它们对应的是 cron 条目和 launchd/systemd 单元,更适合直接在主机上使用。

需要挂载两个目录,缺一不可:

# -e IMAP_PASSWORD forwards the variable, it does not invent it: export it
# first, from wherever you actually keep the secret.
export IMAP_PASSWORD="$(security find-generic-password -s mail-muncher -w)"

docker run -i --rm \
  -e IMAP_PASSWORD \
  -v ~/.config/mail-muncher:/home/muncher/.config/mail-muncher:ro \
  -v ~/.local/share/mail-muncher:/home/muncher/archive \
  ghcr.io/craigjmidwinter/mail-muncher:latest mcp

配置中的 password_cmd: printenv IMAP_PASSWORD 与这条 export 配对使用——关于主机密码管理器为何无法从容器内部访问,请参阅下方说明。

config.yml 中的每个路径都必须是容器可见的路径。 dest: 设置为 ~/Mail/receipts 时,会相对于容器的主目录解析,而不是你的主目录,因此邮件会写入容器退出后就会消失的层。将 dest: 指向挂载目录——对于上面的挂载,即 /home/muncher/archive/receipts——否则你就会把归档写入虚空,而清单还会欣然告诉你操作成功。

password_cmd 在容器内部运行,使用的是 /bin/sh,也就是说,容器里没有你的主机密码管理器。pass show mail/fastmail 无法运行。请使用容器实际能访问的机密信息:

password_cmd: printenv IMAP_PASSWORD          # -e IMAP_PASSWORD
password_cmd: cat /run/secrets/imap-password  # docker secret or a mounted file

这是容器方案确实不如主机安装的一点:它会将凭据从密码管理器移到容器的环境中。如果你觉得这种取舍不值得,就安装二进制文件——password_cmd 是为主机环境设计的,而容器中的做法只是妥协,并非初衷。

MCP Registry 中的条目也由此镜像提供支持;server.json 就是该条目,其中的 name 必须与镜像中内置的 io.modelcontextprotocol.server.name 标签一致。

该条目的发布过程是自动的。为发布打标签时,系统会构建并推送镜像,然后由第二个任务根据 git 标签改写 server.json 中的 version 和镜像标签,并使用工作流自身的 OIDC 身份(而非存储的令牌)进行身份验证,最后发布到注册表。

因此,提交在 server.json 中的 version 是上次发布的版本,并且会有意落后一个标签。 标签才是唯一可信来源;该文件是由 CI 填写的模板。手动更新它毫无意义。

作为 Claude Code 技能

仓库在 skills/ 下提供了技能和插件包,可将 mail-muncher 安装为由 Agent 为你设置和操作的工具——编写配置、运行 auth,并将 MCP 服务接入客户端。如果你想以这种方式使用它,请从这里开始,而不要看下面的快速入门。

该技能以 provider: imap 为起点,并会运行 mail-muncher init,因此它和本 README 一样,只需两分钟即可完成,不会把你引向 Google Cloud Console。

Windows

没有适用于 Windows 的构建版本,上述选项也都无法在后台绕过这一问题。Homebrew 无法在 Windows 上运行。发布归档仅包含 darwin 和 linux,上面的下载代码段是基于 uname 的 POSIX shell 脚本,PowerShell 和 cmd 都完全无法运行。

go install 是唯一能生成可执行文件的途径,而这正是需要直言说明的问题。Go 可以干净地交叉编译此模块——没有 cgo,测试文件之外也没有平台构建标签——因此你会得到一个可以启动的 mail-muncher.exe,mail-muncher init 也会毫无异议地写入配置。但运行到第一个 run 就会停止。IMAP 提供方是本 README 首推的路径(约 2 分钟),它会将 imap.password_cmd 交给 /bin/sh -c 执行(internal/provider/imap/password.go),而标准 Windows 机器上没有 /bin/sh。init 足够谨慎,不会在 Windows 配置中预置 macOS 或 Linux 的密钥工具,但它预置的命令仍会交给一个不存在的 shell,因此问题会到很晚才出现,而且报错指向错误的原因。

Gmail 提供方没有这种依赖——它的 OAuth 流程已会在 Windows 上选择 rundll32——因此可能可以端到端运行。但它未经 Windows 测试,也不受支持。

在 Windows 上可行的做法:在 Docker Desktop 下运行容器镜像,或者在 WSL2 中使用任一安装方式,因为那里同时具备 Linux 二进制文件和 /bin/sh。

升级
brew upgrade mail-muncher            # Homebrew
go install github.com/craigjmidwinter/mail-muncher/cmd/mail-muncher@latest

如果使用下载的二进制文件,请重复上述下载步骤——install 步骤会就地覆盖文件。其他内容都不需要更改:在 0.x 版本中,配置架构和磁盘布局保持稳定,state_dir 中的同步游标也能被任何较新版本读取。CHANGELOG.md 会记录任何可能导致上述情况不再成立的变更,目前还没有此类变更。

使用 mail-muncher --version 查看安装的版本。如果构建时没有使用 make,则会显示 dev——这是 go install 的版本标记注意事项,并不代表安装损坏。

卸载

移除二进制文件不会影响其他内容,因此以下步骤按优先移除最敏感材料的顺序排列。这里没有任何操作会自动替你完成:

# 1. Stop it, if you scheduled it.
launchctl unload ~/Library/LaunchAgents/com.craigjmidwinter.mail-muncher.plist
rm ~/Library/LaunchAgents/com.craigjmidwinter.mail-muncher.plist
rm -f ~/Library/Logs/mail-muncher.out.log ~/Library/Logs/mail-muncher.err.log
# or, if you used cron: crontab -e and delete the line.

# 2. The credential. This is the part nothing else will clean up.
security delete-generic-password -s mail-muncher      # macOS Keychain, IMAP
# Gmail instead: revoke the app at https://myaccount.google.com/permissions

# 3. Config, credentials and the OAuth token.
rm -rf ~/.config/mail-muncher

# 4. Sync cursors, both lockfiles, and the quarantine directory.
rm -rf ~/.local/state/mail-muncher

# 5. The binary.
brew uninstall mail-muncher          # Homebrew
rm -f /usr/local/bin/mail-muncher    # downloaded binary
rm -f "$(go env GOPATH)/bin/mail-muncher"   # go install

你归档的邮件有意不在此列表中。 它们存放在规则中指定的 dest 位置——如果使用了 init 的默认值,则为 ~/Mail/mail-muncher——这些普通文件会在工具卸载后继续存在,这正是该格式的设计目的。如果想查看路径,请在执行第 3 步前运行 grep -n 'dest:' ~/.config/mail-muncher/config.yml;如果想删除邮件,请自行删除这些文件。

Homebrew cask 没有 zap 段落,因此 brew uninstall 只会移除二进制文件,不会删除主目录中的任何内容。这是有意为之——此工具已写入的邮件属于你,而卸载程序不是发现这一点的好地方。

快速入门

大约 5 分钟,无需浏览器,也无需克隆仓库。以下是 IMAP 路径;如果要使用 Gmail API,请在开始前阅读下方的 快速入门:Gmail,因为这需要在 Google Cloud Console 中操作约 10 分钟,而且令牌每周都会过期。

0. 检查安装。 安装后立即执行,尚未创建任何配置时:

mail-muncher run

这实际上是很有用的冒烟测试,并非错误。它会以状态码 1 退出,并准确告诉你它查找的位置、接下来要运行什么,以及各提供方的开销:

mail-muncher is not configured.
  missing config file: /Users/you/.config/mail-muncher/config.yml
  next command:        mail-muncher init
  then:                mail-muncher validate && mail-muncher run --dry-run

init asks which provider to use. Both are supported; the costs differ.
  provider: imap   ~2 min. Gmail, Fastmail, Proton Bridge, work accounts,
    self-hosted. Needs an app password, which is a broader credential than a
    read-only OAuth token; mail-muncher only ever issues BODY.PEEK.
  provider: gmail  ~10 min in the Google Cloud Console: gmail.readonly is a
    Google restricted scope, so mail-muncher ships no OAuth client and you
    register your own. Google enforces read-only, but on a Testing-mode
    consent screen the refresh token expires every 7 days, so
    "mail-muncher auth" must be re-run weekly. Read docs/gmail-setup.md.
Docs: https://craigjmidwinter.github.io/mail-muncher/

所有需要配置的命令都会显示这条信息,因此安装损坏和尚未配置不会混淆。

1. 获取应用专用密码。 前往邮件服务提供方自己的设置页面——Gmail、Fastmail、iCloud、Proton 或你的工作账户。该密码仅用于这一用途,你可以撤销它,而不影响其他内容。将它存放在你平时保存密钥的地方:

# macOS Keychain
security add-generic-password -s mail-muncher -a "$USER" -w

# or pass, 1Password, secret-tool, gpg — anything that prints it on stdout

mail-muncher 从不存储此密码。它会运行你指定的命令,并从该命令的标准输出中读取密码,因此密钥会留在你的密码管理器中。配置架构中有意不设 password: 键。

2. 编写配置。

mail-muncher init --provider imap
Account name [personal]:
Write matched mail to [~/Mail/mail-muncher]:
IMAP host: imap.fastmail.com
IMAP username: [email protected]
Password command [security find-generic-password -s mail-muncher -w]:
Wrote /Users/you/.config/mail-muncher/config.yml

Next, for provider imap:
  1. Run that password_cmd in a shell and check it prints the password and
     nothing else, for example:
       security find-generic-password -s mail-muncher -w | cat -A
     Anything else on stdout - a prompt, a warning, a trailing blank line -
     becomes part of the password and the login fails. If it is not there
     yet, create an app password with your mail provider first and store it
     where password_cmd can read it.
  2. mail-muncher validate
  3. mail-muncher run --dry-run     then     mail-muncher run
Matched mail lands in ~/Mail/mail-muncher
Docs: https://craigjmidwinter.github.io/mail-muncher/

无需编辑步骤。 init 会询问 IMAP 流程所需的所有信息,并写入一份首次即可通过验证的配置。密码命令会预先填入你平台的默认值——macOS 上为 Keychain,Linux 上为 secret-tool,其他平台上为 pass——因此一路按 Enter 就能给出真实答案,而不是占位符。

每个问题都有对应的标志,方便提前回答或编写脚本完成整个流程:

mail-muncher init --provider imap --yes \
  --host imap.fastmail.com --username [email protected]

--yes 会为所有确实有默认值的问题采用默认值,因此仍需要你提供 --provider、--host 和 --username。这三项都没有值得猜测的默认值,程序会明确说明,而不是写入占位符:

error: --host and --username required with --yes --provider imap; host and
username have no honest default to take. Run `mail-muncher init --provider
imap` without --yes to be prompted instead

添加 --account NAME、--dest DIR 和 --password-cmd CMD,即可回答其余问题。除非使用 --force,否则绝不会覆盖现有配置。

~/.config/mail-muncher/config.yml 是默认路径;--config 可在所有地方覆盖此路径,包括 init。

3. 检查密码命令是否只输出密码,不包含其他内容。

security find-generic-password -s mail-muncher -w | cat -A

| cat -A 会让多余的提示、警告或尾随空行清晰可见。stdout 上的任何额外内容都会成为密码的一部分,导致登录失败——这是首次运行无法通过身份验证的最常见原因。

以下是 init 写入的内容,供参考;文件中有详细注释,完整示例请参阅 examples/imap.yml:

accounts:
  - name: personal
    provider: imap
    imap:
      host: imap.fastmail.com
      username: [email protected]
      password_cmd: security find-generic-password -s mail-muncher -w
      mailboxes: [INBOX]

4. 检查配置。

mail-muncher validate
config: /Users/you/.config/mail-muncher/config.yml
1 account(s), 1 rule(s), state_dir /Users/you/.local/state/mail-muncher
OK

IMAP 账户可通过干净的验证:无需查找凭据文件,也无需预先写入令牌,磁盘上什么都不需要有。预期结果是没有警告的 OK。validate 会解析配置、编译每条规则的匹配树,并检查规则引用的文件。由其他程序管理的缺失文件——例如 from_domains_file,或 Gmail 流程中的 OAuth 凭据和令牌——会产生警告,而非错误:

warning: rules[0].match.any[0].from_domains_file: file does not exist yet: /Users/you/.local/share/jobsearch/domains.txt (it is maintained by another program; the rule matches nothing until it appears)
OK with 1 warning

5. 查看实际运行会执行什么操作。

mail-muncher run --dry-run

试运行会像真实运行一样连接、获取并评估邮件,同时报告每个匹配项将会写入的路径。它不会写入文件,也不会保存同步状态,因此你可以随意多次运行。这也能准确指出主机、用户名或 password_cmd 错误之处:

error: account "personal": imap: password_cmd "security find-generic-password -s mail-muncher -w" failed: exit status 44: security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.

6. 运行。

mail-muncher run

init 写入的配置包含一条匹配所有 72h 内新邮件的入门规则,因此首次运行一定会存储内容——如果运行后什么都没存储,就无法分辨是没有匹配项还是安装出了问题。然后再运行一次:磁盘上已有的内容都会标记为 skipped,增量游标也意味着第二次运行几乎不会与服务通信。

成功后,将入门规则缩小到你实际需要的范围(docs/filters.md),然后设置定时运行(参见定时运行)。docs/configuration.md 列出了所有配置项。

快速入门:Gmail

如果你特别需要 Gmail API,并希望由 Google 而非此程序强制保证只读,请选择此路径。开始之前,请了解两项成本,因为它们都是结构性的,且都无法消除:

  • 需要预先花约十分钟在 Google Cloud Console 中进行设置。 gmail.readonly 是 Google 的受限范围,因此 mail-muncher 不会提供 OAuth 客户端,将来也不会——你需要注册自己的项目和桌面应用客户端,并下载其 JSON 文件。
  • 令牌每 7 天过期一次。 Google 会对所有仍处于测试模式的同意屏幕应用此规则,而你的应用就会处于这种模式。你必须每周重新运行 mail-muncher auth。没有任何设置可以取消此限制; docs/gmail-setup.md 解释了原因以及替代方案的代价。

如果你认为这两项成本都不值得,那么上面的 IMAP 路径也适用于 Gmail 帐户。

mail-muncher init --provider gmail        # prints the cost warning, then writes the config
# → follow docs/gmail-setup.md: project, Gmail API, consent screen,
#   Desktop app OAuth client, save its JSON as
#   ~/.config/mail-muncher/credentials.json
mail-muncher auth --account personal      # browser consent; writes token.json 0600
mail-muncher validate
mail-muncher run --dry-run
mail-muncher run

auth 会打印同意 URL(并尝试打开浏览器),监听回环端口上的重定向,并以 0600 权限将令牌写入帐户的 token_file。此命令仅适用于 Gmail 帐户——如果是 IMAP 帐户,它会拒绝执行,因为无需授权。上文 IMAP 快速入门中的第 4 至第 6 步仍然适用;在凭据文件和令牌文件存在之前,validate 会报告两条警告。

外部管理的筛选文件

这是此工具的核心功能,因此有必要准确说明其语义。

from_domains_file 指定的文件归 mail-muncher 所有;mail-muncher 不会创建或写入此文件:

match:
  from_domains_file: ~/.local/share/jobsearch/domains.txt
# ~/.local/share/jobsearch/domains.txt
# written by the job-search tracker

acme.com
globex.io          # inline comments are fine
@initech.com       # a leading @ is stripped
MAIL.Umbrella.COM  # case is irrelevant
  • 每个周期读取一次,在首次使用时读取。 不是每个进程读取一次,也不是每条邮件读取一次。run 会重新读取该文件;守护进程的每次定时运行也会重新读取。多个规则引用同一文件时,该文件只读取一次,且内容会共享。
  • 文件缺失或无法读取不会导致致命错误。 此谓词只会匹配不到任何内容,并针对该文件在该周期记录一条警告。所属程序可能尚未创建该文件,mail-muncher 不应因此失败。
  • 宽松解析。 每行一个条目;# 表示注释;跳过空行;去除首尾空格;去掉开头的 @ 和末尾的 .;全部转换为小写;重复项会合并。没有点号的条目会保留,并记录为可疑条目,而不会被丢弃,因为该文件属于其他程序,错误猜测而静默丢弃条目并不可取。
  • 匹配域名或其子域名。 acme.com 匹配 acme.com 和 careers.acme.com,但不匹配 notacme.com。

相同的匹配规则也适用于内联的 from_domains: 谓词;唯一的区别在于列表的所有者。

域名列表无法表达的情况

有些发件人无法提前枚举。某家公司的邮件可能来自 wagepoint.teamtailor.com、mail.wagepoint.com 和 [email protected] ——域名列表只能列出你已经知道的主机。from_regex_file 则可以用模式实现同样的功能:

match:
  from_regex_file: ~/.local/share/jobsearch/companies.txt
# one RE2 pattern per line, unanchored
wagepoint
(?i)^careers@acme\.io$
teamtailor\.com$

生命周期完全相同——每个周期读取一次,文件缺失绝不会导致致命错误, on_degraded_filter 决定游标的处理方式。与域名格式有两处刻意的差异:

  • 不会将任何内容转换为小写,因为正则表达式本身区分大小写。 需要忽略大小写时,请写入 (?i)。
  • 只有行首的 # 才表示注释。 在行中途遇到 # 就截断模式, 会悄无声息地改变匹配结果。

两种格式的故障模式恰好相反,因此防护方式也不同。 域名列表中的拼写错误会导致什么也匹配不到——代价是静默失效。模式列表中的拼写错误则可能导致匹配所有内容:.* 或一个多余的空行就会匹配整个邮箱。因此,空模式或任何能匹配空字符串的模式都会被直接拒绝;无法编译的模式会被单独拒绝,其余文件内容仍然生效;每个周期都会记录已加载的模式数量,因此,如果文件中的模式从十二条降到一条兜底模式,你在运行输出中就能看到这个数字,而不必等到整个磁盘都被占满才发现。

配置

完整参考:docs/configuration.md。可运行文件: examples/imap.yml、 examples/minimal.yml、 examples/job-search.yml。

只有账户配置块会因提供商而异。IMAP:

accounts:
  - name: personal
    provider: imap
    imap:
      host: imap.fastmail.com
      port: 993                  # default
      tls: true                  # default
      username: [email protected]
      password_cmd: pass show mail/fastmail   # stdout is the password
      mailboxes: [INBOX, Archive]             # default [INBOX]
      initial_lookback: 720h                  # default

Gmail:

state_dir: ~/.local/state/mail-muncher

on_message_failure: quarantine   # or: abort
on_degraded_filter: hold         # or: fail, proceed

accounts:
  - name: personal
    provider: gmail
    gmail:
      credentials_file: ~/.config/mail-muncher/credentials.json
      token_file: ~/.config/mail-muncher/token.json
      query: "-in:chats"
      initial_lookback: 2160h

rules:
  - name: job-search
    account: personal
    match:
      any:
        - from_domains_file: ~/.local/share/jobsearch/domains.txt
        - subject_regex: "(?i)your application"
    dest: ~/Mail/job-search
    formats: [eml, markdown]
Key类型默认值描述
state_dir路径~/.local/state/mail-muncher同步游标(每个账户一个 JSON 文件)、周期锁、实例锁和隔离目录。
on_message_failurequarantine、abortquarantine遇到无法解析的邮件或导致接收端失败的邮件时该怎么处理。见下文。
on_degraded_filterhold、fail、proceedhold规则的 from_domains_file 或 from_regex_file 无法读取时该怎么处理。见下文。
quarantine_dir路径<state_dir>/quarantine存放隔离邮件的位置。
accounts列表—要拉取邮件的邮箱。至少需要一个。
accounts[].name字符串—必填,且必须唯一。用作状态文件名,也是 rules[].account 引用的名称。
accounts[].providerimap、gmail—必填;没有默认值。指定用于获取邮件的后端。见连接邮箱的两种方式。
accounts[].imap映射—仅当 provider 为 imap 时必填,且仅允许在此时设置。
accounts[].imap.host字符串—必填。imap.fastmail.com、imap.gmail.com;Proton Bridge 使用 127.0.0.1。
accounts[].imap.port整数993993 表示隐式 TLS(IMAPS),与默认值 tls: true 配套使用。
accounts[].imap.username字符串—必填。通常是完整地址;有些提供商要求填写不带域名的本地部分。
accounts[].imap.password_cmdshell 命令—必填。在 /bin/sh -c 下运行;其标准输出即为密码。特意不提供 password 键——密钥保存在你的密码管理器中。
accounts[].imap.mailboxes字符串列表[INBOX]要获取的文件夹,每个文件夹都有自己的游标。文件夹名称也会作为 label 谓词值。服务器上不存在的文件夹会报错,而不会被视为空文件夹。
accounts[].imap.tls布尔值true连接时使用隐式 TLS。设为 false 会以明文发送密码和所有邮件正文;validate 会发出警告。仅适用于回环地址或位于 stunnel 后方的情况。
accounts[].imap.initial_lookbackGo 时长720h每个邮箱首次同步时回溯的时间范围;UIDVALIDITY 发生变化后也会按此范围回溯。必须为正数。
accounts[].gmail映射—仅当 provider 为 gmail 时必填,且仅允许在此时设置。
accounts[].gmail.credentials_file路径—必填。从 Google Cloud 下载的 OAuth 客户端 JSON 文件。
accounts[].gmail.token_file路径—必填。auth 缓存 OAuth 令牌的位置,文件权限为 0600。
accounts[].gmail.query字符串none可选的 Gmail 搜索表达式。仅用于优化首次扫描的成本——见下文。
accounts[].gmail.initial_lookbackGo 时长720h首次扫描时回溯的时间范围。必须为正数。见回填。
accounts[].gmail.include_spam_trash布尔值false获取垃圾邮件和垃圾箱中的邮件。两个 Gmail 同步路径对此的处理方式相同。设为 true 时,validate 会发出警告。见垃圾邮件和垃圾箱(Gmail)。
rules列表—按顺序对每封邮件进行评估;第一个匹配项生效。
rules[].name字符串—必填,且必须唯一。显示在日志和 Markdown frontmatter 中。
rules[].account字符串所有账户将规则限制为一个账户。
rules[].match匹配节点—必填。见筛选器。
rules[].dest路径—必填。目标目录;需要时会自动创建。
rules[].formatseml、markdown 列表[eml]要写入的格式。

容易踩坑的注意事项:

  • 未知键会直接导致错误。 拼写错误会导致加载失败,而不是被忽略,因此 validate 能在运行前捕获 initial_lookbak。
  • 所有路径字段中的 ~ 和 $VAR 都会展开,包括匹配树中 from_domains_file 和 from_regex_file 的值。不支持 ~user 形式。未定义的变量会展开为空字符串,与 shell 中的行为相同。
  • gmail.query 不会筛选最终保留的内容,而且作用范围比你想象的更窄。 它只会在某个账户首次扫描时发送给 Gmail,除此之外不会在其他情况下使用——增量周期不会使用,历史游标过期后的恢复扫描也不会使用。它绝不会在本地重新应用。决定存储哪些内容的唯一依据是你的规则。请保持查询条件宽泛,或省略它。
  • 默认不会获取垃圾邮件和已删除邮件(Gmail)。 两种 Gmail 同步路径对此一致:完整扫描会传入 includeSpamTrash=false,增量路径则会在消息进入流水线之前丢弃标记为 SPAM 或 TRASH 的邮件。设置 gmail.include_spam_trash: true 即可获取这些邮件——请参阅垃圾邮件和已删除邮件。IMAP 没有对应的键:你只会获取 mailboxes: 中列出的文件夹,因此不列出垃圾邮件文件夹就是全部做法。
  • gmail: 和 imap: 块不能同时使用。 如果设置的块与 provider: 不匹配,就会直接报错,而不是被静默忽略。因此,在 Gmail 账户下设置 imap: 块,不会让你误以为正在通过 IMAP 获取邮件。
两种可能故障的处理策略

这两个键都位于顶层,与 state_dir 同级。默认值是安全选项;只有在确定更愿意遇到哪种故障时,才需要更改它们。

on_message_failure — 消息无法解析,或者规则要求的所有渲染结果都写入失败时的处理方式。

值行为
quarantine(默认)将原始字节写入 <quarantine_dir>/<account>/<id>.eml,并附带一个 .json 文件说明失败原因,然后让游标越过该消息。不会丢失内容,单条有问题的消息也无法卡住流水线。摘要和清单会将其计为 quarantined;运行仍以状态码 0 退出。
abort返回失败,因此游标不会前移,该消息会在下一个周期重新获取。取舍很明确:永久无法解析的消息会卡住该账户,直到有人处理为止。

如果隔离写入本身失败,则该消息会回退到 abort 语义——拒绝前移游标可以恢复,丢失消息则不行。

on_degraded_filter — 规则的 from_domains_file 或 from_regex_file 缺失、不可读或被截断时的处理方式。此类文件不会匹配任何内容,因此如果没有处理策略,该周期中的每条消息都会针对空列表进行评估,被判定为不匹配,然后被处理掉。

值行为
hold(默认)运行周期并存储所有匹配的内容,以错误级别记录降级情况,但不保存前移后的游标——因此文件恢复后会重新评估同一批邮件。清单会报告 degraded 和 state_held。以状态码 0 退出。
fail在获取任何内容之前结束周期。不会存储内容,也不会前移游标,并以非零状态码退出。
proceed将不可读的列表视为空列表并继续前移游标。这是旧行为,也是唯一会接受静默丢失所需邮件的选项——validate 会对此发出警告。

在 hold 模式下已存储的文件会继续保留:目标位置具有幂等性,因此重新运行时会跳过这些文件。

筛选器

完整参考和使用指南:docs/filters.md。

match: 的值是一个映射,且必须恰好包含一个键——组合器或谓词。同一个映射中出现两个键会导致编译错误,并提示你使用 all: 或 any: 将它们组合起来。正则表达式和持续时间会在配置加载时编译,因此错误的模式会导致 validate 失败,而不会在凌晨 3 点带来意外。

组合器
KeyValue匹配条件
all节点列表每个子节点都匹配(至少需要一个子节点)
any节点列表至少一个子节点匹配(至少需要一个子节点)
not单个节点子节点不匹配
match:
  all:
    - any:
        - from_domains: [acme.com]
        - from_domains_file: ~/.local/share/agent/domains.txt
    - not:
        subject_regex: "(?i)^\\[newsletter\\]"
谓词
KeyValue匹配条件
from_domains域名列表任意 From 地址的域名与所列域名相同,或是其子域名
from_domains_file路径同上,但每个周期都会从外部管理的文件读取列表
from_regexRE2 模式模式匹配任意 From addr-spec(不含显示名称)
from_regex_file路径同上,但每个周期都会从外部管理的文件读取模式
to_regexRE2 模式模式匹配任意 To 或 Cc addr-spec
subject_regexRE2 模式模式匹配解码后的 Subject
header{name: X-Foo, regex: ...}模式匹配该标头的任意值
has_attachmenttrue / false邮件是否包含真实附件
label标签名称邮件包含指定的提供方标签,按完全匹配比较。在 Gmail 上,这是 Gmail 标签;在 IMAP 上,这是邮件所在的邮箱名称
older_thanGo 时长邮件的 Date 时间早于当前时间,且超过该时长
newer_thanGo 时长邮件的 Date 时间晚于当前时间,且在该时长范围内

各举一个完整示例:

# Mail from a company or any of its subdomains.
- from_domains: [acme.com, globex.io]

# The same list, owned and updated by another program.
- from_domains_file: ~/.local/share/jobsearch/domains.txt

# A specific sender, however they capitalize it.
- from_regex: "(?i)^no-?reply@acme\\.com$"

# Patterns owned and updated by another program, for senders whose host cannot
# be enumerated in advance (wagepoint.teamtailor.com, mail.wagepoint.com).
- from_regex_file: ~/.local/share/jobsearch/companies.txt

# Anything addressed to a plus-alias you hand out to vendors.
- to_regex: "(?i)^me\\+vendors@example\\.com$"

# Application acknowledgements, case-insensitively.
- subject_regex: "(?i)(your application|application received)"

# Everything a mailing list tags for you.
- header: {name: List-Id, regex: "golang-nuts"}

# Only messages that actually carry a file.
- has_attachment: true

# On Gmail: labels exactly as shown in the UI. Nested labels use "Parent/Child";
# system labels are upper case (INBOX, SENT, UNREAD, STARRED).
# On IMAP: the mailbox the message came from, verbatim as the server names it,
# including its hierarchy separator ("Lists/golang", "Lists.golang").
- label: INBOX

# Message Date older than 90 days / newer than a day.
- older_than: 2160h
- newer_than: 24h

值得了解的细节:

  • from_regex 和 to_regex 匹配裸地址([email protected]),不会匹配显示名称。若要匹配包含显示名称的原始标头,请使用 header: {name: From, regex: ...}。
  • has_attachment 会统计标记为 Content-Disposition: attachment 的部分。通过 cid: 引用的内嵌图片不算附件。
  • label 区分大小写且必须完全匹配——label: inbox 不会匹配 INBOX。在 IMAP 账户上,可用值是你在 imap.mailboxes 下列出的邮箱名称,因此邮件只会带有提取它时所在邮箱对应的标签。
  • older_than / newer_than 会根据邮件的 Date 标头进行比较;如果标头缺失或无法解析,则回退到提供方的内部日期。没有可用日期的邮件不会匹配这两项中的任何一项。
  • 模式使用 Go RE2:不支持反向引用和环视。添加前缀 (?i) 可忽略大小写。在 YAML 中,建议使用双引号并转义反斜杠("\\."),或者在无需转义时使用单引号。
  • has_attachment 请使用 true / false。YAML 1.2 会将 yes 和 no 视为字符串,mail-muncher 会拒绝它们。
垃圾邮件和已删除邮件(Gmail)

**默认情况下不会获取垃圾邮件和已删除邮件。**这些文件夹中的内容不会进入规则,也不会写入磁盘。垃圾邮件最有可能包含由攻击者撰写的恶意文本,并进入最终会出现在模型上下文窗口中的处理流程,因此默认做法是将其留在 Gmail 中。

本节全部针对 Gmail 提供方。IMAP 没有对应的键,因为它不需要:IMAP 账户只会获取 imap.mailboxes 中指定的文件夹,因此只有在你按名称请求垃圾邮件时,它们才会被获取。

如果你确实需要获取它们——例如,有一封合法邮件被误判为垃圾邮件,或者你需要一个真正完整的归档——请为每个账户设置此键:

accounts:
  - name: personal
    gmail:
      include_spam_trash: true   # validate warns; that is deliberate

这两项设置的作用不同,你可能两者都需要:

决定
gmail.include_spam_trash是否获取这些邮件
针对 SPAM / TRASH 标签的规则获取后如何处理这些邮件

gmail.query 无法完成这两项工作。它只会在首次扫描时发送,因此其中的 -in:spam 对之后的任何周期都不起作用。设置 include_spam_trash: true 后,请使用规则进行筛选——规则引擎是唯一能看到每封已获取邮件的地方:

rules:
  - name: job-search
    match:
      all:
        - from_domains_file: ~/.local/share/jobsearch/domains.txt
        - not:
            any:
              - label: SPAM
              - label: TRASH
    dest: ~/Mail/job-search

Gmail 的系统标签区分大小写,且必须使用大写。如果你希望所有规则都排除垃圾邮件和已删除邮件,就必须在每条规则中加入 not:——这是有意设计的,没有全局排除机制:规则是决定存储内容的唯一依据。

磁盘上的布局

**mail-muncher 写入的文件就是它的公共 API。**本节将带你了解其布局;docs/output-format.md 是相关约定,涵盖每个 frontmatter 键、为何 frontmatter 需要真正的 YAML 解析器,以及安全枚举传递目录树的规则。编写使用这些文件的程序前,请先阅读该文档;正确示例请参阅 examples/read_delivered.py。

每个 sink 都会按邮件日期(UTC),将邮件写入规则的 dest 目录下:

~/Mail/job-search/
└── 2026/
    └── 07/
        ├── 1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.eml
        ├── 1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.md
        └── 1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.attachments/
            ├── offer.pdf
            └── R-sum-2026.docx

所有格式共用同一个基本文件名,因此同一封邮件的不同呈现形式会排列在一起:

<unix-seconds>-<sha256(account + ":" + message-id)[:16]>-<subject-slug>

摘要片段为 16 个十六进制字符,即 64 位。在邮箱可能产生的任何规模下,两封邮件都具有相同摘要片段的情况都不可能发生;归档读取程序会将其解析为邮件 ID,因此请将其位数视为布局的一部分。

  • 时间戳可使目录按时间顺序排列。
  • 摘要是幂等性键。它仅取决于账户名称和提供方邮件 ID,因此路径是邮件身份的纯函数。
  • slug 是转换为小写的主题,其中所有不属于 [a-z0-9] 的字符都会合并为单个 -,然后去除首尾的 -,并截断至 40 个字符。

关于 slug 有两点需要注意,且都是有意如此:

  • **它仅使用 ASCII 字符。**完全由非拉丁文字或表情符号组成的主题,其 slug 会变成 no-subject。非 ASCII 文件名会受到文件系统 Unicode 规范化的影响(HFS+ 使用 NFD),这可能导致写入的名称与下一周期检查的名称不同——而该存在性检查就是整个幂等性机制。摘要仍能区分这些邮件。
  • **它仅用于外观。**只有摘要携带身份信息。主题相同的两封邮件绝不会因此冲突。
文件的写入方式

消息文件会先写入目标目录中的临时文件并执行 fsync,然后通过 link(2) 硬链接到目标位置。有三点值得注意:

  • 不会发布不完整的文件。 文件写完后,文件名才会出现。
  • 不会覆盖已有文件。 与 rename(2) 不同,link(2) 会因 EEXIST 而失败,不会覆盖文件。这种失败本身就是幂等性检查——内核会在占用文件名的瞬间判断该名称是否空闲,因此不存在其他写入者插入文件并被静默替换的时间窗口。已存在的文件会报告为 skipped。
  • 拒绝符号链接,不会跟随它们。 如果消息最终路径上存在符号链接,或 dest 下作为 <YYYY> 或 <MM> 目录的符号链接,系统会将该消息计数并记录为错误。目录结构中的任何内容都不应是链接,因此出现链接就说明有其他东西将它们放在那里。规则自身的 dest: 除外——将其指向另一个卷是常见做法。

在不支持硬链接的文件系统上(FAT、某些网络挂载),会回退为就地使用 O_CREAT|O_EXCL 写入:仍可原子地避免覆盖,也能防止符号链接,但不再保证文件不会部分写入。

附件是唯一的例外:它们通过临时文件和 rename(2) 写入,因为附件名称并非幂等性标记——上方的 .md 文件才是。

目录权限为 0700,文件权限为 0600。 归档邮件属于私人通信,解码后的附件也同样如此,因此它们会受到与同步游标和 OAuth 令牌相同的保护:其他本地用户无法读取。以你本人身份运行的工具不受影响。

.eml 文件

model.Message.Raw,逐字节保留,与服务提供方传送的内容完全一致。不进行重新编码、重新换行或规范化,因此可以通过任何邮件工具往返处理,并且仍能通过 DKIM 签名验证。这是用于保真保存的副本。

.md 文件

YAML frontmatter,然后是正文,最后是指向所有附件的链接。

  • 正文选择:如果有 text/plain 部分,就使用它;否则将 text/html 部分转换为 Markdown;否则使用字面内容 *(no body)*。 行尾会统一为 LF,每行末尾的空白会被去除,首尾空行会被裁掉。
  • Frontmatter 始终包含 subject、from、from_address、 from_addresses、to、to_addresses、date、message_id、thread_id、 thread_id_source、account、rule。cc、cc_addresses、in_reply_to、 labels 和 attachments 为空时会省略。解析地址时要使用 *_address / *_addresses 字段,绝不要从 from/to/cc 解析——这些是 显示字符串;发件人选择的显示名称中若包含 <、> 或 ,,就会产生歧义。 机器可读字段包含不带显示名称的 addr-spec,无法通过这种方式伪造。内容由 YAML 编码器生成,而非字符串格式化,因此即使主题中满是引号和冒号,也不会破坏解析; 这也意味着读取时需要使用真正的 YAML 解析器。 带表情符号的主题会以双引号括起,并包含 \U0001F389 转义;包含换行符的主题则会以 |- 块标量形式出现。按 key: value 拆分会把这两种情况都处理错。请参阅 docs/output-format.md。
  • 线程信息由三个字段组成,而非四个。thread_id 是连接键,永不为空:若有 提供方自己的对话 ID,就使用该 ID;否则根据消息的 References 链生成一个。 可直接按它对目录分组,无需特殊处理。thread_id_source 说明应在多大程度上信任 该分组——取值为 provider、references、in_reply_to 或 self——因为重建只能尽力而为, 如果邮件客户端破坏了链,就会拆分线程。in_reply_to 指明父消息。完整的 References 链会有意省略:它长度不受限制,而同目录下的 .eml 文件中保留了 原始内容。
  • 附件会写入 .md 旁边的 <basename>.attachments/,文件名会经过清理(不含目录组件, 不允许路径遍历);.md 或 .eml 扩展名会分别替换为 .md.attachment / .eml.attachment;发生重名时会去重为 name-2.pdf、name-3.pdf。附件会先于 .md 文件写入,因此文档不会链接到不存在的文件。 附件内容由发件人选择——请参阅下文的枚举规则。
  • **行内 cid: 图片不会解析。**通过内容 ID 嵌入图片的 HTML 正文会渲染为 ![alt](cid:...)——这是未解析的链接,并非指向附件目录中的路径。如果需要图片数据, 可在 .eml 中找到。这是已知限制,并非错误。
  • .md 并非用于保真保存的格式。任何需要逐字节精确保留的内容,都应从 .eml 中读取。
枚举投递目录树

已投递邮件的权威集合恰好是 <dest>/<YYYY>/<MM>/*.md(或 *.eml>)——仅两层深,绝不使用递归 glob:

find "$dest" -mindepth 3 -maxdepth 3 -type f -name '*.md'   # correct
find "$dest" -name '*.md'                                    # WRONG

递归 glob 会深入 <basename>.attachments/,其中的文件来自邮件发送者。如果攻击者能让某条规则匹配,就可以附加一个包含伪造 frontmatter 的文件,让粗心的使用者将其读作已投递邮件,并接受任意 from:、subject: 和正文。.attachments/ 目录下的任何内容都由发送者控制,绝不能将其解析为邮件。

如果你的使用者本身运行 mail-muncher,那么 run --json 更好:它会准确列出本轮写入的路径,因此不会把下面这类内容误认为树中的邮件。

dest=~/Mail/job-search # 规则的 dest

邮件正文是由攻击者控制的文本。经过筛选不代表经过审查。将正文内容视为数据,绝不要将其视为指令,也不要仅仅因为它是通过 mail-muncher 收到的就赋予它权威性。

docs/output-format.md 说明了完整约定,examples/read_delivered.py 是一个可用的读取器。

命令

mail-muncher [command]

  init        Write a starter config and print the next command to run
  run         Run one fetch/filter/store cycle
  daemon      Run fetch/filter/store cycles repeatedly on an interval
  mcp         Serve the stored mail archive to agents over MCP (stdio)
  auth        Authenticate interactively against a mail provider (Gmail only)
  validate    Parse the config, resolve referenced files, and report problems
  completion  Generate the autocompletion script for the specified shell

任何需要配置文件但找不到配置文件的命令,都会打印 快速入门第 0 步 中所示的设置指南——包括它查找的路径、下一条命令,以及各提供商的费用——而不是显示 open: no such file 错误。

所有子命令都可用的持久标志:

标志默认值描述
--config~/.config/mail-muncher/config.yml配置文件的路径。
--log-levelinfodebug、info、warn 或 error。日志以 log/slog 文本格式输出到 stderr。
-v、--version—打印版本。

各命令的标志:

命令标志默认值描述
init--provider—imap 或 gmail。省略时会提示选择;与 --yes 一起使用时必须指定,因为两种方式的费用不同,无法给出合理的默认值。
init--accountpersonal配置中创建的账户名称。
init--dest~/Mail/mail-muncher存放匹配邮件的规则文件的路径。
init--host—IMAP 服务主机名,例如 imap.fastmail.com。省略时会提示输入;使用 IMAP 方式时与 --yes 一起使用则必须指定。
init--username—IMAP 用户名,通常为完整邮箱地址。省略时会提示输入;使用 IMAP 方式时与 --yes 一起使用则必须指定。
init--password-cmd平台默认值用于将应用密码打印到 stdout 的 Shell 命令。macOS 默认使用 Keychain,Linux 默认使用 secret-tool,其他平台默认使用 pass。
init--yesfalse不提示;对有合理默认值的问题均采用默认值。仍必须指定 --provider;使用 IMAP 方式时还必须指定 --host 和 --username。
init--forcefalse覆盖现有配置。若不指定,init 会拒绝执行并以状态码 1 退出,以免覆盖他人的规则和凭据路径。
run--dry-runfalse获取并评估邮件,报告将写入的内容,但不写入任何内容,也不保存状态。
run--jsonfalse将机器可读的清单写入 stdout,每个账户对应一个 JSON 对象。
daemon--interval5m循环之间的间隔,最短为 30s。每次休眠时长会随机调整,幅度最高为 ±10%。
daemon--dry-runfalse每次轮询都按 run --dry-run 的方式运行。
daemon--jsonfalse每个循环结束后将清单写入 stdout:以换行符分隔的 JSON,每个账户在每次轮询中对应一个对象。
auth--account—要进行身份验证的账户。配置中有多个账户时必须指定。仅适用于 Gmail 账户——对 IMAP 账户运行 auth 会被拒绝,因为无需授权。

mcp 不接受任何自身的标志;其配置完全由 --config 指定。请注意,init 会写入 --config 指定的位置,而不是从该位置读取,并会按需创建目录(0700)和文件(0600)。

--log-level debug 会记录每封邮件的规则判定——邮件 ID、主题,以及胜出的规则名称或 no match——这是查明规则为何未触发的最快方式。Debug 级别还会记录每条 already stored 跳过记录及其路径。所有日志都输出到 stderr,因此不会干扰 stdout 上的 --json 输出。

小于 30s 的间隔会在启动任何操作前被拒绝:

error: --interval 10s is below the 30s minimum
退出代码
代码含义
0成功。即使某个周期遇到单条消息错误(消息无法解析,或一次目标写入失败),也仍会以 0 退出——这些错误会计入摘要,而不会升级处理。
1配置或验证错误:文件无法解析、规则无法编译、缺少必需的键,或者根本没有配置文件。未获取任何内容。如果配置已存在且未提供 --force,init 也会返回此代码。
2提供方或身份验证失败:Gmail 令牌从未写入,或被 Google 拒绝;IMAP password_cmd 执行失败,或服务器拒绝了登录;重试后仍无法连接服务器。
3另一个实例持有周期锁。退出代码 3 表示重叠的 cron 调用报告“上一次调用仍在运行”。

隔离的消息不会改变退出状态:这是已处理并计为 quarantined、且已记录在清单中的消息级故障。请根据计数器而非退出代码触发告警。

daemon 永远不会以 2 退出。失败的周期会记录日志和连续失败次数,并在下一次计时触发时重试——如果 daemon 因令牌过期而立即退出,就需要有人恰好在最不方便的时候介入。它的状态码为 0(收到 SIGINT 或 SIGTERM 后停止,等待正在执行的周期完成并保存状态)、1(配置或验证错误,包括 --interval 低于最小值)和 3(启动时另一个实例已持有实例锁或周期锁)。如果后续计时触发时锁已被持有——例如 cron run 与 daemon 周期重叠——则会记录日志并跳过,不会视为致命错误。

运行摘要

每个周期都会为每个账户记录并打印一行摘要:

personal: fetched=128 matched=6 stored=6 skipped=0 parse_errors=0 sink_errors=0 quarantined=0 duration=4.1s
字段计数单位含义
fetched消息提供方在此周期交付的消息。
matched消息被某条规则认领的消息。
stored渲染结果实际写入的渲染结果。
skipped渲染结果因目标位置已存在而未写入的渲染结果。
parse_errors消息无法解析的消息;记录日志并跳过。
sink_errors渲染结果写入失败;记录日志并计数,周期继续执行。
quarantined消息因无法交付而存放在隔离目录中的消息。
vanished消息提供方曾列出、但之后发现已不存在的消息——即在列出和下载之间被删除的消息。此类消息会被跳过,游标也会越过它们。仅在非零时打印。

字段列表从 fetched= 到 duration= 连续排列,因此周期中的异常情况会标记在账户标签上:

personal (dry-run): fetched=42 ...
personal (degraded, state held): fetched=42 ...
personal (stopped): fetched=12 ...

vanished=N 是唯一会有条件追加的计数器。只有在某条消息于周期中途消失时,它才会显示在 quarantined= 之后;其他情况下都会省略,因此其他所有运行的已发布字段列表保持不变。使用 --json 时,对应字段为 summary.vanished,同样在值为零时省略——如果要据此触发告警,请将其解析为 .summary.vanished // 0。

稳定状态下的 cron 运行会显示 fetched=0。对同一时间窗口重新运行则会显示 matched=N stored=0 skipped=N——这表示幂等性正在正常工作。

JSON 清单

run --json 和 daemon --json 会将该行替换为每个账户一个机器可读对象,其中包含 stored[]、skipped[]、quarantined[]、各项计数器,以及游标是否前移。所有日志行仍会输出到 stderr,因此 run --json 2>/dev/null 输出纯 JSON,而 daemon --json 输出 NDJSON 流。

mail-muncher run --json 2>/dev/null | jq -r '.stored[] | "\(.rule)\t\(.path)"'

完整的逐字段契约请参阅:docs/manifest.md。

状态与锁定

~/.local/state/mail-muncher/
├── personal.json                # one per account, mode 0600
├── mail-muncher.lock            # cycle lock, shared by run, daemon and mcp sync
├── instance/
│   └── mail-muncher.lock        # daemon lifetime lock — one daemon per state dir
└── quarantine/
    └── personal/
        ├── 18f2a1b2c3.eml       # raw bytes of an undeliverable message
        └── 18f2a1b2c3.json      # why it is here

每个账户的状态文件都是 JSON。其中的游标属于对应的提供方,因此结构各不相同。Gmail:

{
  "history_id": 918273,
  "last_sync_time": "2026-07-28T09:15:00Z",
  "seen_ids": ["18f2a...", "18f2b..."]
}
  • history_id 是 Gmail 的增量游标。设置后,每个周期都会向 Gmail 查询自该游标以来的变更。Gmail 保留历史记录的时间大约为一周;游标过期后,API 会返回 404,mail-muncher 会记录一条警告、清除游标,并在同一个周期内回退到完整扫描。
  • last_sync_time 用于限定完整扫描中 after: 条件的范围。恢复扫描会比已存储的水位线再往前覆盖 24 小时,因此不会漏掉上个周期列出邮件期间到达的邮件。重复扫描无妨:写入时会跳过磁盘上已有的所有邮件。
  • seen_ids 是包含最近 2000 个已投递邮件 ID 的先进先出集合,与各目标位置使用的幂等文件名构成双重保障。

IMAP 会在 extra 下,以 UIDVALIDITY/UID 对的形式独立跟踪每个邮箱:

{
  "last_sync_time": "2026-07-28T09:15:00Z",
  "extra": {
    "imap.INBOX.uidvalidity": "1650000000",
    "imap.INBOX.last_uid": "48213"
  },
  "seen_ids": ["personal:INBOX:1650000000:48213"]
}

如果存储的 UIDVALIDITY 仍与服务器上的值匹配,周期任务会请求 UID FETCH 48214:*——仅获取此后到达的邮件。如果发现 UIDVALIDITY 已更改,周期任务就会丢弃已存储的 UID,并从 initial_lookback 开始重新同步,因为 UIDVALIDITY 的变更意味着协议在声明:mail-muncher 记住的每个 UID 现在都指向另一封邮件。重新同步会使用新的文件名,将覆盖范围内的邮件重新归档,因为此路径上的邮件标识为 <account>:<mailbox>:<uidvalidity>:<uid>。这是有意为之的取舍:重复邮件可以删除,而悄无声息地漏掉的邮件则无法找回。详细说明:docs/configuration.md。

状态目录会以 0700 权限创建,状态文件则为 0600:哪些账户存在以及已查看过哪些邮件 ID,都不应公开。

删除账户的状态文件会强制执行完整重新扫描,扫描范围由 initial_lookback 限定。这是安全的——目标位置会跳过磁盘上已有的所有内容——也是从损坏的游标中恢复的受支持方式。

两种锁
  • 周期锁 <state_dir>/mail-muncher.lock 基于 flock,由 run、每次守护进程轮询以及 MCP sync 工具在整个周期内持有。它可以防止 cron 调用与正在运行的守护进程使用相同游标时发生竞争。如果发现锁已被持有,表示“现在不行”,而不是“出错了”:run 会以 3 退出,守护进程则会记录日志并跳过本次轮询。

  • 实例锁 <state_dir>/instance/mail-muncher.lock 由守护进程在其整个进程生命周期内持有。周期锁无法承担这个作用——它会在轮询间隔期间释放,因此第二个守护进程若在第一个休眠时启动,就能绕过该锁,随后与第一个守护进程并行地无限轮询,使它们使用相同游标并将 API 流量加倍。第二个守护进程会立即以 3 退出:

    another mail-muncher daemon is already running against this state directory; not starting
    

如果进程崩溃,操作系统会释放这两种锁。

隔离区

在默认设置 on_message_failure: quarantine 下,无法投递的邮件会写入 <quarantine_dir>/<account>/<id>.eml——原始字节原样保存——旁边还会附带一个 .json 文件,其中记录规则、失败阶段(parse 或 sink)、错误和时间。这个附带文件会包含所有必要信息,以便检查该目录时无需查阅本次运行的日志。

邮件不会自动重新投递。修复原因后,手动将 .eml 文件重新输入。该目录的权限为 0700,文件为 0600,与状态目录中的其他内容一致——隔离邮件是一封完整的电子邮件,存放在其目标目录树之外。

调度

cron — 每十分钟运行一次。若上一次调用仍在运行,run 会以状态码 3 退出,因此重叠运行无害:

*/10 * * * * /usr/local/bin/mail-muncher run --config /home/you/.config/mail-muncher/config.yml >> /home/you/.local/state/mail-muncher/cron.log 2>&1

请使用二进制文件和配置文件的绝对路径:cron 的环境与 shell 不同,而且配置中的 ~ 展开取决于是否设置了 $HOME。

更完整的示例(包含 cron 不会提供给你的环境变量)见 contrib/crontab.sample。

launchd(macOS) — 在 launchd 下运行守护进程,使其在注销后继续运行,并在故障时重启。可直接编辑的 plist 文件位于 contrib/launchd/。将其复制到目标位置,编辑副本中的四个绝对路径——launchd 不会展开 ~ 或 $HOME——然后加载:

cp contrib/launchd/com.craigjmidwinter.mail-muncher.plist ~/Library/LaunchAgents/
# edit the four paths in the copy, then:
launchctl load ~/Library/LaunchAgents/com.craigjmidwinter.mail-muncher.plist

systemd — 目前尚未提供 unit 文件;mail-muncher daemon --interval 5m 可直接作为 Type=simple 服务运行,也可以将 mail-muncher run 与计时器配合使用。

回填:首次运行

首次大范围运行是预期的使用方式,并非滥用。默认的 initial_lookback 为 720h(30 天),这意味着你最希望在第一天获得的内容——你要收集的全部历史记录,一次性保存在磁盘上——默认情况下恰恰不会发生。

首次运行时请将其设为较大的值。两个提供方都有此配置项,放在你的账户所使用的配置块下:

要停止它,请对同一路径运行 launchctl unload。若要彻底移除,还需删除该文件——参见卸载。

accounts:
  - name: personal
    gmail:
      credentials_file: ~/.config/mail-muncher/credentials.json
      token_file: ~/.config/mail-muncher/token.json
      initial_lookback: 13140h   # ~18 months — first run only
accounts:
  - name: personal
    provider: imap
    imap:
      host: imap.fastmail.com
      username: [email protected]
      password_cmd: pass show mail/fastmail
      initial_lookback: 13140h   # ~18 months — first run only, per mailbox

然后运行一次,并在之后将该值调回原值:

      initial_lookback: 720h

将其调回原值是安全的,因为 initial_lookback 只会限制首次扫描范围——Gmail 账户的首次扫描,或 IMAP 下每个邮箱的首次扫描。之后的每个周期都会从已存储的游标继续运行,并完全忽略该配置项。只有删除账户的状态文件,或 IMAP 服务器更改 UIDVALIDITY 时,才会重新启用该配置项。

预期情况:

  • 这是一次大型全量扫描,会进行速率限制和重试,可能需要一些时间。在 Gmail 上,下载会同时进行,每次四封邮件,每页 500 封;这两项都不可配置。在 IMAP 上,它会转为对每个邮箱执行 SINCE UID 搜索。

  • 提交前先检查。 mail-muncher run --dry-run 会像真实运行一样获取并评估内容,并报告所有将要写入的路径,但不会触碰目标目录树。添加 --json 可统计数量:

    mail-muncher run --dry-run --json 2>/dev/null | jq '.summary'
    
  • 之后的运行应报告所有内容都已 skipped。这是确认回填确实完成的检查方法。

Go 的时长不支持天或年的单位——请换算成小时。18 个月是 13140h,一年是 8760h,90 天是 2160h。

2000 条目的已见集合上限不会限制回填。原因见 docs/architecture.md:确定性文件名加上存在即跳过才是真正的幂等性标识,而已见集合只是双重保险。

状态与范围

尚未达到 1.0。目前的版本是 v0.4.0,每个版本的变更记录见 CHANGELOG.md;不使用 make 构建时,版本会显示为 dev。配置架构已稳定到可以据此编写配置,但在 1.0 之前仍可能发生变化。

范围状态
IMAP 提供方(password_cmd、每个邮箱的 UID 游标、EXAMINE + BODY.PEEK[])已实现
Gmail 提供方(OAuth、全量扫描、增量历史同步、RAW 下载)已实现
init — 为任一提供方写入经过验证的配置,支持交互式或脚本化操作已实现
针对所有未配置或配置不完整的命令提供设置指南已实现
未配置时仍可按协议规范提供服务的 mcp已实现
配置加载与验证已实现
筛选引擎(上述所有组合器和谓词)已实现
.eml 和 Markdown 输出目标已实现
run、daemon、周期锁、实例锁已实现
--json 运行清单已实现
mcp — stdio MCP 服务,提供五种工具已实现
隔离区和两种故障策略已实现
明确不在范围内
  • 写入邮箱。 只读是设计约束,不是阶段目标。 不会添加标签、删除、发送或创建草稿;对于 IMAP,甚至不会将 邮件标记为已读。
  • 充当邮件客户端。 没有界面,也没有索引。search_messages 会对存储文件 进行不区分大小写的子字符串扫描,并非搜索引擎: 不做词干提取、不排序,也不支持查询语言。邮件线程根据每封邮件携带的 ID 分组, 不会重新组装成对话模型。
  • 网络 API。 没有任何进程监听套接字。mcp 通过 stdio 协议与将其作为子进程 启动的客户端通信——没有端口、没有 HTTP 端点,也没有网络接口。唯一会绑定端口的 是在 Gmail 流程中,auth 为 OAuth 重定向打开几秒钟的本地环回端口。
  • 捆绑 OAuth 客户端。 gmail.readonly 是 Google 的受限范围,因此 Gmail 流程始终需要自行注册——这正是 IMAP 作为低门槛接入方式存在的原因。
已知限制
  • Markdown 输出中,内联 cid: 图片会保留为无法解析的链接。
  • 主题 slug 仅支持 ASCII;非拉丁字符主题会归档为 no-subject。
  • 没有任何谓词会查看邮件正文;匹配依据是邮件头、标签和日期。

Gmail:

  • 在测试模式的授权屏幕下,Gmail 令牌每 7 天过期一次,因此 mail-muncher auth 每周都要运行一次。这是 Google 的政策,不是可配置项。
  • gmail.query 只应用于账户首次扫描,不适用于增量周期,也不适用于游标过期后的恢复扫描。
  • 除非设置 gmail.include_spam_trash: true,否则根本不会抓取垃圾邮件和回收站邮件。 抓取后,由针对 SPAM 和 TRASH 标签的规则决定如何处理。
  • Gmail 的下载并发数(4)和页面大小(500)无法配置。

IMAP:

  • 应用专用密码是一种完整的邮件凭据。只读限制由 mail-muncher 的代码强制执行,而非由你的服务提供商强制执行——请参阅 连接邮箱的两种方式。
  • 服务器更改 UIDVALIDITY 后,会使用新文件名重新归档 initial_lookback 时间范围内的邮件。 这是有意为之:重复邮件可以恢复,漏掉的邮件则无法恢复。
  • IMAP 没有服务端对话 ID,因此 thread_id 始终根据 References/In-Reply-To 链合成, 且 thread_id_source 永远不会是 provider。
  • 服务器上不存在的邮箱会报错,而不是视为空文件夹——因此 mailboxes: 中的拼写错误会导致首次运行失败, 而不是看起来像没有新邮件。
  • 只会抓取 mailboxes: 中列出的文件夹;没有“全部”选项,也不支持服务端搜索。

文档

可在 https://craigjmidwinter.github.io/mail-muncher/ 浏览,也可在此代码库中查看:

  • docs/configuration.md — 两种提供方的每个配置项、验证规则及失败情况。accounts[].imap(见 docs/configuration.md#accountsimap)部分是 IMAP 参考;没有单独的设置页面,因为除了应用专用密码外无需进行其他设置。
  • docs/gmail-setup.md — 仅介绍 Gmail:Google Cloud 操作指南、七天有效期,以及每条 OAuth 错误消息及其解决方法。本文档内容不适用于 IMAP 账户。
  • docs/filters.md — 完整的匹配树语言,以及包含真实规则的实用指南。
  • docs/output-format.md — 磁盘上的契约:目录布局、文件名、每个 frontmatter 字段,以及枚举投递目录树时的安全规则。编写使用方前请先阅读。
  • docs/manifest.md — --json 清单契约,逐字段说明。
  • docs/mcp.md — MCP 服务:客户端接入方式,以及每个工具的参数和返回格式。
  • docs/architecture.md — 流水线、各个衔接点,以及应将更改放在哪里。
  • CONTRIBUTING.md — 构建、测试,以及代码库遵循的约定。

可运行的配置:examples/imap.yml(IMAP 配置,含大量注释)、examples/minimal.yml(最精简且实用的 Gmail 配置)、examples/job-search.yml(实际使用的外部管理筛选器文件),以及 examples/read_delivered.py(正确的使用方示例)。这三个配置都能通过 mail-muncher validate --config <file> 验证。

致谢

字标采用 Jason Kottke 设计的 Silkscreen 字体,依据 SIL Open Font License 1.1 使用。品牌资源、配色方案和使用规则见 branding/BRAND.md。

许可证

MIT。请参阅 LICENSE。

评论

支持 Markdown

第一次接触 MCP?

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

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