Implement internal mail skill MVP

This commit is contained in:
赵义仑
2026-06-06 22:28:36 +08:00
commit 097df73495
22 changed files with 4059 additions and 0 deletions

317
README.md Normal file
View File

@@ -0,0 +1,317 @@
# 内网邮箱 Agent Skill MVP
这是一个本地运行的邮箱 Skill Adapter面向 Agent 暴露结构化 HTTP 接口,底层使用 POP3 收信、SMTP 发信、SQLite 保存本地索引。
## 重要边界
- 本项目是 `POP3 + SMTP` 模式,不依赖 IMAP、网页自动化或邮箱数据库。
- POP3 不能可靠同步服务器侧已读、未读、文件夹、标签、归档、草稿状态。
- 已读、已处理、归档、删除、已回复、标签、草稿、已发送记录都是本地 SQLite 状态。
- SMTP 发出的邮件不一定会出现在 Webmail 的“已发送”里。
- 如果将来需要和 Webmail 已发送一致,可以改成调用 Webmail HTTP API或者发送时 BCC 自己。
- 默认绑定 `127.0.0.1`,不要把服务直接暴露到公网或共享网段。
- 附件真实路径不会返回给 Agent接口只返回 `attachment_id` 和附件元数据。
## 配置
复制示例配置:
```bash
cp .env.example .env
```
按内网邮箱配置 POP3 和 SMTP
```dotenv
MAIL_ACCOUNT_ID=default
POP3_HOST=mail.example.local
POP3_PORT=110
POP3_USERNAME=your-name
POP3_PASSWORD=your-password
POP3_USE_TLS=false
POP3_TIMEOUT_SECONDS=30
SMTP_HOST=mail.example.local
SMTP_PORT=25
SMTP_USERNAME=your-name
SMTP_PASSWORD=your-password
SMTP_USE_TLS=false
SMTP_USE_STARTTLS=false
SMTP_AUTH_REQUIRED=false
SMTP_ALLOW_INSECURE_AUTH=false
SMTP_FROM_ADDRESS=your-name@example.local
SMTP_FROM_NAME=Your Name
SMTP_TIMEOUT_SECONDS=30
```
不要把真实账号密码提交到 Git。服务日志不会打印密码。
如果内网 SMTP 服务器要求在 25 端口上使用明文 SMTP AUTH需要显式设置 `SMTP_ALLOW_INSECURE_AUTH=true`。这会把认证信息发送在未加密连接上,只应在可信内网里使用;公网服务商通常应使用 `SMTP_USE_TLS=true``SMTP_USE_STARTTLS=true`
## 启动
```bash
go run ./cmd/server
```
默认监听:
```text
http://127.0.0.1:8765
```
也可以指定配置文件:
```bash
CONFIG_PATH=/path/to/.env go run ./cmd/server
```
## 常用接口示例
健康检查:
```bash
curl http://127.0.0.1:8765/health
```
从 POP3 同步邮件。同步使用 `UIDL` 去重,不默认删除服务器邮件;单封邮件失败会进入 `errors`,不会中断整次同步:
```bash
curl -X POST http://127.0.0.1:8765/sync
```
搜索本地邮件,只返回摘要:
```bash
curl "http://127.0.0.1:8765/emails/search?q=合同&from=alice@example.local&has_attachment=true&limit=20"
```
读取邮件全文:
```bash
curl http://127.0.0.1:8765/emails/1
```
读取本地线程:
```bash
curl http://127.0.0.1:8765/emails/1/thread
```
修改本地状态,不会同步回 POP3 服务器:
```bash
curl -X POST http://127.0.0.1:8765/emails/1/mark \
-H 'Content-Type: application/json' \
-d '{"read":true,"processed":true}'
```
创建标签并应用到邮件:
```bash
curl -X POST http://127.0.0.1:8765/labels \
-H 'Content-Type: application/json' \
-d '{"name":"待处理","color":"#336699"}'
curl -X POST http://127.0.0.1:8765/emails/labels/apply \
-H 'Content-Type: application/json' \
-d '{"email_ids":[1],"label_ids":[1],"op":"add"}'
```
创建草稿:
```bash
curl -X POST http://127.0.0.1:8765/drafts \
-H 'Content-Type: application/json' \
-d '{"to":["bob@example.local"],"cc":[],"bcc":[],"subject":"测试","body":"你好"}'
```
列出草稿:
```bash
curl http://127.0.0.1:8765/drafts
```
修改草稿:
```bash
curl -X PATCH http://127.0.0.1:8765/drafts/1 \
-H 'Content-Type: application/json' \
-d '{"to":["bob@example.local"],"subject":"更新后的主题","body":"更新后的正文"}'
```
发送草稿。Bcc 不写入邮件头,但会参与 SMTP 实际发送;发送成功写入 `sent_messages`,回复邮件会把原邮件本地标记为 `replied=true`
```bash
curl -X POST http://127.0.0.1:8765/drafts/1/send
```
直接发送新邮件,内部等价于创建草稿并发送:
```bash
curl -X POST http://127.0.0.1:8765/send \
-H 'Content-Type: application/json' \
-d '{"to":["bob@example.local"],"bcc":["audit@example.local"],"subject":"直接发送","body":"正文"}'
```
转发邮件。MVP 暂不转发附件,`include_attachments=true` 会返回结构化错误:
```bash
curl -X POST http://127.0.0.1:8765/emails/1/forward \
-H 'Content-Type: application/json' \
-d '{"to":["bob@example.local"],"subject":"Fwd: 原邮件","body":"请看下面原文","include_attachments":false}'
```
## 数据目录
默认路径:
- SQLite: `./data/internal_mail.db`
- 原始邮件: `./data/raw`
- 附件文件: `./data/attachments`
这些运行时数据默认不提交到 Git。
## 构建与测试
运行测试:
```bash
go test ./...
```
只跑本地端到端测试:
```bash
go test ./internal/e2e -count=1 -v
```
`internal/e2e` 会在测试进程里启动最小 fake POP3 和 fake SMTP 服务,然后通过 HTTP API 验证:
- `GET /health`
- `POST /sync`
- POP3 `UIDL` 去重
- MIME 正文和附件解析
- 附件只暴露 `attachment_id`,不暴露真实路径
- `GET /emails/search`
- `GET /emails/{id}`
- 标签创建与应用
- 本地已读、已处理、已回复状态
- 草稿回复发送
- `POST /send` 直接发送
- SMTP envelope 包含 To、Cc、Bcc
- Bcc 不写入邮件头
- 发送成功写入 `sent_messages`
- 发送失败写入 `sent_messages(status=failed)`,不误标记草稿为 `sent`
本机编译:
```bash
go build ./cmd/server
```
交叉编译 Windows
```bash
mkdir -p dist
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o dist/internal-mail-skill.exe ./cmd/server
```
交叉编译 Linux
```bash
mkdir -p dist
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dist/internal-mail-skill-linux-amd64 ./cmd/server
```
也可以使用 Makefile
```bash
make test
make e2e
make build
make cross-build
```
## 本地验收建议
外部邮箱注册经常受风控影响,因此交接前建议先跑本地可重复验收:
```bash
go test ./internal/e2e -count=1 -v
go test ./...
go build ./cmd/server
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o dist/internal-mail-skill.exe ./cmd/server
```
本地 E2E 不依赖真实邮箱账号,不会发出公网邮件;它验证的是本项目对 POP3、SMTP、MIME、SQLite 和 HTTP API 的主链路处理能力。
## 已完成的公网邮箱试测记录
以下记录用于交接背景,不包含任何密码。
- VFEmailPOP3 同步成功SMTP 发送成功,自发自收闭环成功,邮件可通过搜索接口查到。
- RunboxPOP3 同步成功SMTP 587 STARTTLS 和 465 TLS 均返回 `535 Incorrect authentication data`;外部投递到该账号返回 `550 Unknown account`
- GMX、eclipso、Fastmail、Purelymail、mailbox.org公开 POP3/SMTP 端口握手可达,但注册或试用受风控、手机号、试用限制影响,未形成账号级完整闭环。
结论:代码主链路已用本地 E2E 和 VFEmail 真实账号闭环验证;内网交接时应重点用公司 POP3/SMTP 账号做最终验证。
## 内网接手测试清单
1. 复制 `.env.example``.env`,填写内网 POP3/SMTP 参数。
2. 确认 `HTTP_HOST=127.0.0.1`,不要直接暴露到公网或共享网段。
3. 启动服务:
```bash
go run ./cmd/server
```
4. 健康检查:
```bash
curl http://127.0.0.1:8765/health
```
5. 同步邮件:
```bash
curl -X POST http://127.0.0.1:8765/sync
```
6. 再次同步,确认重复邮件进入 `skipped`,不会重复入库。
7. 搜索一封已知邮件:
```bash
curl "http://127.0.0.1:8765/emails/search?q=关键字&limit=10"
```
8. 读取邮件全文,确认正文、附件元数据和本地状态正常。
9. 创建草稿并发送到测试收件人。
10. 如果是回复邮件,确认 `In-Reply-To`、`References` 尽量写入,且原邮件本地 `replied=true`。
11. 如果 SMTP 发出后 Webmail 的“已发送”里看不到,这是 POP3+SMTP 模式的正常限制;需要一致性时可 BCC 自己或后续接 Webmail HTTP API。
## 常见问题
### SMTP 25 端口能连但发不出去
确认内网服务器是否需要 SMTP AUTH。如果需要明文 AUTH必须显式设置
```dotenv
SMTP_AUTH_REQUIRED=true
SMTP_ALLOW_INSECURE_AUTH=true
```
公网邮箱通常不应使用明文 AUTH应使用 `SMTP_USE_TLS=true` 或 `SMTP_USE_STARTTLS=true`。
### 同步后 Webmail 里还是未读
这是预期行为。POP3 无法可靠同步服务器侧已读、标签、归档、文件夹等状态;本项目只维护本地状态。
### SMTP 发出的邮件没有出现在 Webmail 已发送
这是预期行为。SMTP 只负责投递,不一定写入 Webmail 已发送文件夹。需要一致性时,可让系统 BCC 自己,或者后续接入 Webmail HTTP API。
### 附件在哪里
附件真实路径只保存在 SQLite 中,不通过 API 返回给 Agent。API 只返回 `attachment_id`、文件名、类型和大小。