qqmail-mcp
by claude89757
README.md
# qqmail-mcp
单个 QQ 邮箱的远程 Model Context Protocol 服务,运行于 Cloudflare Workers,通过 IMAP/SMTP 访问邮箱,使用 D1 保存 OAuth 状态。Cloudflare Access 验证所有者身份,服务自己的确认页面负责批准具体客户端及权限。
**当前版本:2.0.4。** 作者实例:`https://qqmail.zacks.vision/mcp`。
[2026-11-01 发布与验收记录](docs/acceptance-2026-11-01.md)
## 工具与权限
| 工具 | 权限 | 行为与重要限制 |
| --- | --- | --- |
| `check_mail_connection` | `mail:read` | IMAP 登录、文件夹/收件箱统计;可选 SMTP 登录,不发送邮件 |
| `list_folders` | `mail:read` | 中文文件夹、特殊用途、总数与未读数 |
| `search_emails` | `mail:read` | 解码后主题/发件人/收件人、日期、标记、文本和 HTML 正文;条件 AND 组合;检查 `partial` |
| `read_email` | `mail:read` | 正文、附件、状态与回复关联;不标记已读;空白纯文本部分可回退到 HTML |
| `list_attachments` | `mail:read` | 部件编号、中文文件名、类型与估算大小 |
| `download_attachment` | `mail:read` | 小附件内联,或限时链接流式下载;列表大小是估算值,不是内容完整性校验 |
| `set_email_flags` | `mail:modify` | 已读/未读、星标/取消星标、已回复 |
| `move_emails` | `mail:modify` | 优先服务器 MOVE,否则 COPY 后标记源邮件删除;不能安全定向清理时返回 `pending_expunge` |
| `delete_emails` | `mail:modify` | 默认移入垃圾箱;请求永久删除也不能绕过安全清理限制 |
| `save_draft` | `mail:modify` | 创建或替换草稿;新草稿与旧草稿的物理清理是不同结果 |
| `send_email` | `mail:send` | To/Cc/Bcc、纯文本/HTML、附件;Bcc 不出现在发送的正文头部 |
| `reply_email` | `mail:send` | 回复/回复全部、可指定 To、可取消引用;返回 `original_marked_answered` |
| `forward_email` | `mail:send` | 带/不带原附件转发;附件读取失败时取消发送,而不是静默漏附件 |
| `send_draft` | `mail:send` | 默认发送后尝试清理;可保留草稿;同一草稿重复提交有数据库防重保护 |
文件夹参数可以用 `inbox`、`sent`、`drafts`、`trash`、`junk`,或 `list_folders` 返回的中文显示名称。UID 只在所属文件夹内有效,移动后目标 UID 可能改变;请重新搜索目标文件夹。
## 首次连接
在支持远程 MCP OAuth 的客户端中使用上述 `/mcp` 地址,认证选择 OAuth。客户端需要动态注册,使用 PKCE S256;登录 Cloudflare Access 后,还须在 QQ 邮箱授权确认页面核对应用、回调地址和权限,再点击确认。
仅接受已注册客户端和完全匹配的回调地址。当前不支持 Client ID Metadata Document,元数据明确返回 `client_id_metadata_document_supported:false`;HTTPS 形式的客户端 ID 本身不是可信凭证。
Cursor 配置示例:
```json
{"mcpServers":{"qqmail":{"url":"https://qqmail.zacks.vision/mcp"}}}
```
升级并不主动撤销已有可用令牌。旧客户端若仅有 `mail:read`,需重新授权才能获得修改/发送权限。客户端若缓存旧工具定义,需要刷新定义后才能使用新增的 `scan_before_uid` 参数。
## 搜索结果如何读取
所有给出的条件保持 AND 语义;中文、英文都在解码后的文本上匹配。`since` 包含当天,`before` 不包含当天,按 IMAP INTERNALDATE 的日期筛选。正文搜索包括文本和 HTML 转文本,不包括附件文件内的文字。
单次最多扫描 500 个候选,正文最多读取 64 封,每个文本部分最多 256 KiB,并设有工作时间预算,避免把邮件任务无限拉长。返回:
- `partial:false`、`total_is_exact:true`:本次候选范围已完整检查,`total` 有效。
- `partial:true`、`total:null`:不完整,不能解释成“全邮箱没有更多结果”。`matched_in_scan` 仅是已扫描范围内的匹配数。
- 同一已扫描窗口内用 `offset` 翻页;`has_more` 表示该窗口尚有匹配结果。
- 有 `next_scan_before_uid` 时,将它作为下一次 `scan_before_uid`,并将 `offset` 重置为 0,继续较旧候选。
- `truncated_bodies` 大于 0 时,未扫描的正文尾部不能通过普通候选翻页补齐;应定位邮件后使用 `read_email` 进一步检查。
默认隐藏 `\\Deleted` 邮件;只有显式 `include_deleted:true` 才包含它们。文件夹总数可能包括这些待清理邮件,所以与默认搜索数量不同不是必然的数据丢失。
## QQ 删除限制:不以误删风险换取“成功”
有些 QQ IMAP 会话不提供 MOVE/UIDPLUS。**服务绝不执行整个文件夹级别的 EXPUNGE 或 CLOSE,也不临时修改其他邮件的删除标记。** 无安全定向清理能力时,源邮件可能仅被标记删除、从 MCP 默认列表隐藏;其他邮箱客户端可能仍显示它。
请检查 `source_expunged`、`pending_expunge`、`draft_removed` 和 `draft_hidden`,不要仅根据 `moved`、`moved_to_trash` 或 `sent` 推断物理删除成功。`permanently_deleted:0` 就是未完成永久清理,需要通过 QQ 邮箱自身界面处理。替换草稿时,`saved:true` 可以与 `replaced_previous:false` 同时出现,表示新草稿已保存但旧草稿尚未物理移除。
发送草稿使用 D1 原子占位,防止同一草稿重复提交。SMTP 结果不确定时保留占位而不自动重发;先检查“已发送”和收件情况。确实需要再次发送时,应明确创建一封新草稿。`keep_draft:true` 不表示可反复发送同一草稿。防重记录保留 30 天;其他直接发送工具不提供跨调用全局幂等性,客户端不要盲目重试结果不确定的发信请求。
## 安全设计
QQ 授权码只保存在 Worker secret 中,不提交到仓库或返回给客户端。Access JWT 校验签名、issuer、audience、时间和可选邮箱白名单;缺少配置时拒绝新授权。授权确认使用绑定身份/站点的一次性 nonce、同源 Origin 检查及 HttpOnly/SameSite Cookie,禁止嵌入页面。
授权码通过单条数据库语句原子消费。刷新令牌原子标记已用,检测重放后撤销关联令牌家族;校验资源 audience,禁止刷新时提升权限。访问令牌有效期 24 小时,刷新令牌 30 天。OAuth 令牌及下载令牌以哈希索引保存。
`ENABLE_MODIFY=false`、`ENABLE_SEND=false` 是服务端权限开关,对现有令牌同样生效。邮件内容始终不可信,不得按照邮件里的指令擅自发送、转发、删除或泄露信息。
下载链接默认 15 分钟有效,最长 1 小时;它是临时访问凭证,不要写入公开日志。下载使用附件处置、安全 CSP 和 nosniff,活动类型按二进制下载。错误日志仅包含事件、工具名和错误类型,不记录邮件正文、令牌或完整堆栈;当前配置关闭请求级 invocation logs,避免记录含凭证的 URL。
## 部署
需要 Cloudflare 账号、域名、D1,以及已启用 IMAP/SMTP 的 QQ 邮箱授权码。不是 QQ 登录密码,不需要额外的 OWNER_PASSWORD。
```bash
git clone https://github.com/claude89757/qqmail-mcp
cd qqmail-mcp
npm ci
npm run check
# 首次部署才创建数据库;把返回的 ID 写入 wrangler.jsonc
npx wrangler d1 create qqmail-mcp-oauth
npm run db:migrate
npx wrangler secret put QQ_EMAIL
npx wrangler secret put QQ_APP_PASSWORD
npx wrangler secret put ACCESS_TEAM_DOMAIN
npx wrangler secret put ACCESS_AUD
npx wrangler secret put ACCESS_ALLOWED_EMAILS
npm run deploy
```
Cloudflare Access 自托管应用只保护 `<your-domain>/authorize`,策略只允许所有者邮箱。不要把整个域名都套在 Access 登录页后面,否则 MCP 发现、令牌交换和限时下载会受影响。Worker 内部仍会重新验证 Access JWT,未经 Access 认证的其他入口不能签发授权。
更新已有实例时不要重建 D1,不要重置密钥。核对 Worker 名称、路由、D1 和密钥绑定;保留现有绑定。项目 `keep_vars:true` 保留已有变量。通过 API 发布时应使用严格绑定继承并在发布后读回核对。
### 变量
| 名称 | 用途 / 默认值 |
| --- | --- |
| `QQ_EMAIL` | 邮箱地址,支持 qq.com / vip.qq.com / foxmail.com |
| `QQ_APP_PASSWORD` | QQ 邮箱 IMAP/SMTP 授权码,必须作为 secret |
| `QQ_DISPLAY_NAME` | 发信显示名,可选 |
| `ACCESS_TEAM_DOMAIN` | `<team>.cloudflareaccess.com` |
| `ACCESS_AUD` | Access 应用 audience |
| `ACCESS_ALLOWED_EMAILS` | 建议设置所有者邮箱白名单,逗号分隔 |
| `ENABLE_MODIFY` / `ENABLE_SEND` | 默认 true;可立即关闭相关功能 |
| `SAVE_SENT_COPY` | 默认 false;当前作者实例 true。避免与 QQ 自动保存重复配置 |
| `MAX_INLINE_ATTACHMENT_BYTES` | 默认 4 MiB |
| `DOWNLOAD_TTL_SECONDS` | 默认 900,最大 3600 |
| `ALLOW_INSECURE_LOCAL_AUTHORIZE` | 仅 localhost/127.0.0.1 本地开发;生产不要启用 |
读取正文每部分最多 2 MiB,单 IMAP 会话有内存/大小限制。发信附件总计最多 20 MiB、10 个附件、50 个收件人;批量 UID 最多 100。链接采用流式下载,不代表任意大小、网络和时长均经过验收。
## 开发、验收与发布检查
```bash
npm ci
npm test
npm run check # 测试 + Wrangler dry-run,不部署
cp .dev.vars.example .dev.vars # 只在本地填写;禁止提交
npm run db:migrate:local
npm run dev
npm run smoke # 本地 DCR/显式确认/令牌/MCP 流程
npm run smoke -- --live # 另检查本地配置的真实 IMAP/SMTP
```
GitHub CI 对 push/PR 执行测试和打包检查。`scripts/apply-2.0.3.py`、`scripts/apply-2.0.4.py` 是本次升级的哈希保护一次性补丁,不是常规启动或安装步骤,不应在新版本上重复运行。
常规发布顺序:固定提交 → 测试通过 → 备份版本号/核对绑定 → 部署同名 Worker → `/health` 检查版本及 ready → 未授权请求应为 401 → QQ 自发自收验收 → 对照“已发送”和收件正文/附件 → 仅清理测试样本。记录 Worker version ID 和源提交,回滚优先选择最近安全且可用的版本,不回滚到已知授权/误删风险版本。
`src/index.js` 路由;`oauth.js`/`access.js`/`store.js` 授权;`mcp.js`/`tools.js` MCP;`mail.js` 业务;`imap.js`/`smtp.js` 协议;`mime.js`/`imap-parse.js`/`bytes.js` 编解码;`download.js` 限时下载。具体发布证据和未覆盖的边界场景见验收记录。
## License
[MIT](LICENSE)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues