一个只读的本地邮箱代理:把你的求职邮件,变成聊天软件里一张能一键完成的行动清单。
Read-only IMAP · Local-first · PII-masked before LLM · Interactive cards
秋招投了五六十家公司之后,邮箱会变成一团乱麻:
- 我到底投了哪些公司?哪些是主动投的,哪些是内推?
- 谁回我了?谁已读不回?谁的邮件被我漏掉了?
- 有没有"需要我在某个日期前做点什么"的邮件,静静躺在收件箱里过期了?
- 一封测评邀请里给了通行证号、一封面试通知里给了确认链接,三天后翻出来找不到了
普通邮件客户端不解决这些——它们是"收信工具",不是"办事工具"。
MailRecon 只做一件事:盯着你的邮箱,把"需要你动手的事"抽出来,变成一张能点一下就完成的任务卡,推到飞书上。
每条待办一个「✅ 完成」按钮,点完可以撤销:
📋 求职任务 · 已完成 2/8
▰▰▱▱▱▱▱▱ 6 待办 🔥 24h 内 1
🔥 24 小时内截止(1)
1. 完成星辰科技在线测评,点击链接作答
星辰科技邀你参加 2027 校招在线测评
📍 点邮件里的测评链接作答 · ⏰ 09-25 16:50 · 🔗 bs.example.com
[ ✅ 完成 ]
[ 🔗 去做 ]
📌 进行中(7)
2. 重投示例工程师:原信未送达 hr@acme.example.com
你投给 hr@acme.example.com 的邮件没送到:该地址不存在
📍 邮箱搜 postmaster@qq.com 看原始退信 · 📭 邮件里没写截止时间
[ ✅ 完成 ]
点「✅ 完成」之后会回来一张确认卡,上面带一个「↩️ 撤销」,误点可以直接反悔。
字数不多,但每条待办都回答四个问题:谁 · 发生了什么 · 要做什么 · 去哪做。
MailRecon 从不修改你的邮箱。整个代码库里没有任何一处标记已读、移动、删除、回复或投递。
具体做法:
- IMAP 用
EXAMINE而不是SELECT—— 只读方式打开邮箱 - 取正文用
BODY.PEEK[]而不是BODY[]——BODY[]会顺带把邮件标记成已读,PEEK不会 - 增量同步靠
UID+UIDVALIDITY水位线,不靠"删除标记"之类有副作用的操作
这不是能力缺失,是设计取舍:一个会动你邮箱的助手,你不敢让它常驻。
大多数邮件工具处理的是"一封信"。MailRecon 处理的是一次投递的生命周期:
我发出的申请 ──┐
├──► 对账 ──► 已回复 / 已退信 / 无回音
对方回来的信 ──┘
它分别读收件箱和已发送,按"公司在域名层面"配对——hr@acme.example.com 和 campus@acme.example.com 算同一家;但 10000@qq.com 这种公共服务邮箱算独立个体,不会因为同域就被误判成 HR 回复。
邮件里全是隐私:姓名、手机号、证件号、学校、住址。
这些在你机器上用规则先抹掉,才发去调模型(mask.py)。同时保留对任务有用的信息——日期、金额、编号、通行证——否则提取出来的待办就没用了。
还有一道独立护栏:身份配置文件没加载成功时,整个分析环节直接拒绝调用模型,而不是"降级成不脱敏地发出去"。
送模型之前还会先做本地分诊(triage.py):把邮件分成 drop / template / llm / local_only 四类。实测 999 封收件箱邮件里,94% 不需要模型参与就能判掉(营销、验证码、平台通知)。既省钱,也少暴露。
这套系统不会替你打开测评链接、不会替你提交表单、不会替你回复邮件。
原因很实际:那些链接大多是一次性的、带签名的,而且服务端会拦截脚本 UA——实测某招聘系统对 curl 直接 302 到拦截页,对浏览器 UA 放行 200。与其做一个注定失败的自动化,不如把"该做什么"准确地送到你手上,让你自己点。
- 每天最多 50 次模型调用,硬上限
- 单轮最多处理 20 封
- 本地分诊先过滤掉约 94% 的邮件
三个数字合起来:即使邮箱被轰炸,账单也是封顶的。
┌──────────── 每小时 ────────────┐
│ │
QQ 邮箱 ◄──只读 IMAP──► sync.py ──► reconcile.py ──► triage.py
│ 取信 对账 本地分诊
│ │
│ analyze.py
│ 脱敏 → 调模型 → 提取字段
│ │
│ ┌───────────────────────┤
│ │ │
│ push_actions.py taskboard.py
│ 行动项卡片 任务台账
│ │ │
└────────┴──────► 飞书 ◄─────────┘
▲ │
poller.py ◄──────┘
处理「完成 3」/ 按钮回调
取信/分析/推送走小时级批量;你的操作(点按钮、发指令)走秒级。两条线分开,所以点一下按钮是即时的,不用等下一轮。
mail-agent/ 取信 → 对账 → 分诊 → 分析 → 推送
sync.py 增量取信(EXAMINE + BODY.PEEK,UID 水位线)
reconcile.py 投递对账
triage.py 本地分诊(省掉 94% 的模型调用)
analyze.py 脱敏 → 调模型 → 落库
prompt_final.py 提取用的 prompt 与输出校验
mask.py 本地脱敏
links.py 从原始正文里挑"该点的那一个"链接
push_actions.py 行动项卡片推送
taskboard.py 任务台账视图
notify.py 回复 / 退信通知
feishu.py 飞书 API 客户端
run.sh / daemon.sh 小时级调度
mail-agent-interact/ 交互层
poller.py 指令泵:轮询 + 校验 + 执行 + 回执
cmds.py 指令解析(完成 / 放弃 / 撤销 / 状态 …)
board.py 常驻看板卡
infer.py 从后续邮件推断任务是否已完成
fsapi.py 飞书收发(含卡片原地刷新)
hermes/ 聊天网关侧的桥接(可选)
task-cmd-bridge/ 把飞书卡片回调转成指令
skill-求职台账/ 让 AI 助手能读懂脱敏后的台账
都是实测踩出来的,不是理论:
cron 在这个环境里不触发。 WSL 里 cron 进程活着、crontab 也配对,但整点任务和每分钟探针都不执行。最后换成了常驻循环脚本(daemon.sh)。并且:脚本必须自己在每次执行前检查可执行位——我们遇到过一次 chmod 丢失,导致守护进程每小时都被拒绝执行、错误只进 stderr、日志一片安静,系统静默停摆两个半小时才发现。
UID n:* 的语义有个坑。 按 RFC 3501,当邮箱里的最大 UID 低于你请求的起始 UID 时,搜索仍会返回最后一封邮件。不客户端过滤的话,你会反复处理同一封信。
卡片按钮回传的不是消息 ID。 飞书的卡片回调给的是回调 token,拿它去查消息会 404。所以卡片点击需要一条独立的信任通道,不能复用"拿 message_id 回飞书核对正文"那套强校验。
卡片只能改 14 天内的消息。 刷新历史看板时超过 14 天的会失败,得按"单张失败忽略、全失败才发新卡"处理。
一次性链接会在提交瞬间失效。 招聘系统的测评/简历链接大多带签名,提交后服务端立刻作废。所以"链接打不开"经常不是链接错了,而是你已经做完了。
过滤和截断的顺序会咬人。 我们写过 list[:2] 再过滤条件,结果前两条里只要有一条不满足条件,真正符合条件的第三条就永远轮不到。看起来能跑,实际是静默少功能。
这套东西是为个人环境部署的,不是开箱即用的 SaaS。路径、飞书自建应用、模型接口都需要按你的环境改。
# 1. 配置
cp .env.example .env # QQ 邮箱(用授权码,不是登录密码)+ IMAP 地址
cp identity.example.json identity.json # 你的身份信息,用于本地脱敏,不会上传
# 2. 依赖
pip install -r requirements.txt
# 3. 跑一轮(只读,不会改你的邮箱)
./mail-agent/run.sh
# 4. 常驻
./mail-agent/daemon.sh & # 每小时取信 / 分析 / 推送
./mail-agent-interact/poller_daemon.sh & # 每 60 秒处理你的指令QQ 邮箱的 IMAP 要用「授权码」,不是登录密码:设置 → 账户 → POP3/IMAP/SMTP 服务 → 生成授权码。这套访问方式是官方支持的;风控针对的是异常行为(高频重连、密码登录),不是 IMAP 本身。
- 路径是硬编码的(
/root/mail-agent、/opt/mail-agent-interact),可移植性改造还没做 - 只在 08:00–20:00 运行,晚上到的邮件要等第二天早上
- 依赖飞书:想换别处(Telegram / 企业微信 / 邮件日报)需要改
feishu.py那一层 - 模型调用有额度,用完了当天不再分析
triage.py的平台域名列表需要按自己的邮箱情况填(见platform_domains.txt.example),内置只有示例
MIT