内容
# proton-cal
一款用于读取和写入 Proton Calendar 事件的 Go 命令行界面(CLI)工具和 MCP 服务器,通过 Proton 未公开的内部 API 实现。由于端到端加密,Proton 没有官方的 Proton Calendar API 或 CalDAV 支持,因此该工具重现了网页客户端的端点和客户端 PGP 密钥层次结构。
基于 Proton 官方的 [`go-proton-api`](https://github.com/ProtonMail/go-proton-api)(身份验证,会话,密钥解锁)并实现了日历写入路径。逆向工程的 API 细节在 [docs/](docs/) 中有记录。
> 非官方。未经 Proton AG 认可或赞同。请自担风险 - API 未公开,可能随时更改。
## 功能
- **身份验证**:SRP 登录,TOTP 双因素身份验证,手动 CAPTCHA 备用,两密码模式,持久会话,自动令牌刷新
- **凭证**:桥接样式 - 登录后,存储派生的 *加盐密钥短语*(模式 0600),绝不会存储您的账户密码
- **日历**:列表;按 ID 或名称选择;可配置的默认值
- **事件**:列表,创建,更新,删除,完整的客户端 PGP 加密/解密
- **重复事件**:每日/每周/每月/年重复规则(或原始 `--rrule`),客户端事件扩展,单事件编辑/删除(EXDATE + 例外行),系列范围更改,过期异常清理
- **全天事件**,多地址签名
- **提醒和颜色**:设置每事件提醒(`--reminder 15m`,可重复;`email:`/`notify:` 前缀)和颜色(Proton 调色板颜色,名称或十六进制)创建/更新;使用 `--no-reminders`/`--reminders-default`/`--color default` 恢复到日历默认值
- **`get event` / `get calendar`**:完整的单个资源详情,带有 `--fields`/`--all` 以控制显示哪些字段和终端中的颜色色块。提醒和颜色反映日历的默认值,当事件没有自己的默认值时(与 Proton 应用匹配);`get calendar` 显示日历的默认提醒和事件持续时间
- **iCalendar 导出**(`get event --ics`):对于重复事件,导出整个系列在一个 `.ics` 文件中(主 `VEVENT` 及其 `RRULE`/`EXDATE` 加上每个编辑事件的 `VEVENT`,每个事件都有其 `RECURRENCE-ID`);使用 `--no-series` 只导出单个事件
- **`-o/--output json`** 在每个读写命令上(机器可读的 JSON 输出到 stdout;人类可读的消息输出到 stderr)
- **MCP 服务器**:stdio 服务器,暴露与 7 个工具相同的操作(文本 + 机器可读的结构化结果)
## 安装
需要 Go 1.26+。由于 `go-proton-api` 需要一个 `replace` 指令来替换 Proton 的 resty 分叉,因此 `go install` 不起作用 - 需要从克隆仓库构建:
```bash
git clone https://github.com/cheeseandcereal/proton-cal.git
cd proton-cal
make build # 生成 ./proton-cal
```
## 用法
```bash
# 打印版本并退出
proton-cal --version
# 身份验证(提示输入用户名,密码和 2FA;存储会话 + 派生的密钥短语在 ~/.config/proton-cal/,模式 0600)
proton-cal login
# 列出日历
proton-cal calendars
# 列出接下来的 7 天的事件(扩展重复事件)
proton-cal events --days 7
# 按名称或 ID 定位日历(默认值:帐户的默认日历,否则第一个)
proton-cal events --calendar Work
# 合并多个日历到一个时间顺序列表(重复 --calendar),或一次列出所有日历。每个事件都用其日历标记。
proton-cal events --calendar Work --calendar Personal
proton-cal events --all-calendars
# 创建事件(--end 是可选的,用于定时事件:它默认为日历的默认持续时间)
proton-cal create event "团队站会" \
--start "2026-05-26 09:00" --end "2026-05-26 09:30" \
--description "每周同步" --location "Zoom"
# 重复(每日/每周/每月/年;--every N,--count N 或 --until DATE)
proton-cal create event "团队站会" \
--start "2026-05-26 09:00" --end "2026-05-26 09:30" \
--repeat weekly --count 10
# ... 或为更复杂的重复事件使用原始 RRULE
proton-cal create event "发薪日" --start "2026-05-29 09:00" --end "2026-05-29 09:05" \
--rrule "FREQ=MONTHLY;BYSETPOS=-1;BYDAY=FR"
# 全天事件使用日期(结束日期包含且可选)
proton-cal create event "会议" --all-day --start 2026-06-10 --end 2026-06-12
# 提醒(可重复;速记 15m/1h30m/2d/1w,前缀 email: 表示电子邮件)
# 和颜色(Proton 颜色名称或其十六进制)
proton-cal create event "牙医" --start "2026-06-10 14:00" --end "2026-06-10 14:30" \
--reminder 1d --reminder email:1h --color strawberry
# 更新事件(只有您传递的标志会更改;保留重复)
proton-cal update event <event-id> --summary "重命名站会" --location "面对面"
# 提醒:替换,删除所有,或恢复到日历默认值
proton-cal update event <event-id> --reminder 30m --reminder 1d
proton-cal update event <event-id> --no-reminders # 没有提醒
proton-cal update event <event-id> --reminders-default # 使用日历默认值
proton-cal update event <event-id> --color pacific # 设置 Proton 颜色
proton-cal update event <event-id> --color default # 恢复到日历颜色
# 更改或删除重复(系列更改会删除已编辑的事件)
proton-cal update event <event-id> --repeat daily --count 5
proton-cal update event <event-id> --no-repeat
# 编辑/删除重复事件的单个事件(按其原始开始时间标识,如 `proton-cal events` 所示)
proton-cal update event <event-id> --occurrence "2026-06-02 09:00" --start "2026-06-02 10:00"
proton-cal delete event <event-id> --occurrence "2026-06-02 09:00"
# 删除事件(重复事件:整个系列,包括已编辑的事件)
proton-cal delete event <event-id>
# 更新日历的元数据,默认设置或默认状态
proton-cal update calendar Work --name "Work (2026)" --color cobalt
proton-cal update calendar Work --default-duration 60 --makes-busy
proton-cal update calendar Work --reminder 15m --full-day-reminder 1d # 替换默认提醒集
proton-cal update calendar Work --make-default # 使其成为帐户默认
# 删除日历。没有 --yes,它会解析并报告目标
# (名称 + 类型 + ID),然后拒绝 - 一个干燥的运行,防止删除错误的日历。
# 有 --yes,所有权日历提示输入登录密码
# (或传递 --password);假日日历不需要密码。
proton-cal delete calendar "旧项目" # 干燥运行:报告 + 拒绝
proton-cal delete calendar "旧项目" --yes # 实际删除
# 完整地检查一个资源(标记字段;终端中的颜色色块)
proton-cal get event <event-id>
proton-cal get event <event-id> --fields summary,location,attendees
proton-cal get event <event-id> --all # 还有 uid,calendar_id,原始 rrule
proton-cal get event <event-id> --ics # 导出 iCalendar(重复事件的整个系列)
proton-cal get event <event-id> --ics --no-series # 只导出单个事件
proton-cal get calendar # 默认日历
proton-cal get calendar Work --all # 还有 email,member/address IDs
# 机器可读输出(任何命令;JSON 输出到 stdout,消息输出到 stderr)
proton-cal events --days 7 -o json
proton-cal get event <event-id> -o json
# 禁用颜色(当 piped 或设置 NO_COLOR 时也自动禁用)
proton-cal get calendar --no-color
```
所有基于时间命令都接受 `--tz`(默认值:`~/.config/proton-cal/config.toml` 中保存的 `timezone`,在首次登录时从系统检测)。
### 配置
`~/.config/proton-cal/config.toml`:
```toml
username = "you" # 由 login 写入
timezone = "America/Los_Angeles" # 由 login 写入;可自由覆盖
# base_url = "https://mail-api.proton.me" # 可选覆盖
```
默认日历(当未给出 `--calendar` 时使用,并显示为 `[default]`)是 Proton 提供的 **帐户默认日历**,而不是本地设置;使用 `proton-cal update calendar <name> --make-default` 更改。
`session.json`(相同目录,模式 0600)保存会话令牌和派生的加盐密钥短语。它可以解锁您的日历密钥,但不能用于登录您的帐户;删除它(或运行 `proton-cal logout`)以撤销。您的帐户密码永远不会被存储。
### 引导缓存
`cache.json`(相同目录,模式 0600)缓存引导响应,否则每次调用都需要 5 个顺序 API 请求:您的用户和地址密钥(加密,30 天 TTL),每个日历的通行短语和密钥(加密,30 天 TTL)和日历列表(7 天 TTL)。**事件内容永远不会被缓存** - 每个命令都获取新鲜事件。
宽松的 TTL 是安全的,因为过时会导致自愈:错误的密钥材料会导致加密失败,从而使缓存条目失效并重试一次以获取新鲜数据;未知的日历选择器会自动刷新日历列表,而 `proton-cal calendars` 始终获取新鲜数据。缓存范围限定为会话(重新登录会丢弃它),而 `proton-cal logout` 会删除它。传递 `--no-cache` 以完全绕过它。
## MCP 服务器
`proton-cal mcp` 通过 stdio 讲话 MCP,暴露 `list_calendars`,`get_calendar`,`list_events`,`get_event`,`create_event`,`update_event`,`delete_event`,`update_calendar` 和 `delete_calendar`(大多数工具接受可选的 `calendar` 参数;`list_events` 接受 `calendars` 数组和 `all_calendars` 标志以合并跨日历的事件)。
`delete_calendar` 需要 `confirm: true` 和,对于所有者日历,一个 `password` 参数(登录密码,仅用于删除握手);没有 `create_calendar`。读取工具返回人类可读的文本块和机器可读的结构化内容(与 CLI 的 `-o json` 相同的 JSON 架构)。由于 JSON 参数无法区分省略的字符串和空字符串,`update_event` 将空字段视为“保留”;传递 `clear_fields: ["location", ...]` 以清空摘要/描述/位置(或 `"color"`,相当于 `color: "default"`,以恢复它到日历颜色)。`create_event` 和 `update_event` 还接受 `reminders`(例如 `["15m", "email:1h"]`)和 `color`(Proton 颜色名称或十六进制,或 `"default"` 表示日历颜色);`update_event` 使用 `reminders_mode`(`keep`/`inherit`/`none`/`custom`)选择提醒行为。先运行一次 `proton-cal login`。
```json
{
"mcpServers": {
"proton-calendar": {
"command": "/path/to/proton-cal",
"args": ["mcp"]
}
}
}
```
或用于 [opencode](https://opencode.ai)(`opencode.json`):
```json
{
"mcp": {
"proton-calendar": {
"type": "local",
"command": ["/path/to/proton-cal", "mcp"]
}
}
}
```
## 开发
| 目标 | 它做什么 |
|--------------------|-----------------------------------------------------------|
| `make build` | 构建 `./proton-cal` |
| `make lint` | gofmt 检查,`go vet`,golangci-lint(必需) |
| `make test` | 离线单元测试 |
| `make integration` | 针对真实 Proton 帐户的实时测试(见下文) |
### 架构
```
cmd/proton-cal 入口点
pkg/cli cobra 命令(参数绑定 + 渲染)
pkg/mcpserver MCP stdio 服务器(工具模式 + 渲染)
pkg/calsvc 共享服务层:引导,输入解析,
列表/创建/更新/删除操作
pkg/auth 登录(SRP/2FA/验证码),密钥解锁,加盐短语
pkg/papi go-proton-api 包装器 + 原始调用缺少的端点
pkg/calendar 日历列表,会员解析,日历密钥解锁
pkg/event 事件 CRUD:解密,加密,同步有效载荷,重复操作
pkg/pgp PGP 原语(分离签名,会话密钥加密/重用)
pkg/ical iCalendar 片段构建器/解析器(RFC 5545 转义)
pkg/icaltime 共享 iCalendar 日期/时间编码 + 时区帮助程序
pkg/recurrence RRULE 构建/验证 + 事件扩展
pkg/caltypes 由重复和事件共享的事件行线类型
pkg/config 配置 + 会话存储(锁定保护,原子写入)
pkg/integration 选择加入的实时测试套件(构建标签 `integration`)
```
[docs/](docs/) 目录记录了在此基础上构建的反向工程 Proton API,
分为
[概述](docs/overview.md)(密钥层次结构,会话/范围,错误代码),
[加密](docs/crypto.md)(身份验证流程,密钥解锁,事件加密模型),
[API](docs/api.md)(端点,同步有效载荷语义,读取/分页,
重复)和 [库](docs/libraries.md)(涉及的 Go 库的难点边缘)- 所有这些都通过 API 现场验证。其他关键参考:
[`go-proton-api`](https://github.com/ProtonMail/go-proton-api),
[`ProtonMail/WebClients`](https://github.com/ProtonMail/WebClients)(日历
加密在 `packages/shared/lib/calendar/` 中),和
[`proton-calendar-cli`](https://github.com/Nojuza/proton-calendar-cli)。
### 集成测试
`pkg/integration` 使用真实帐户和真实 API 进行锻炼:
1. `proton-cal login` 一次。
2. `cp pkg/integration/config.example.toml pkg/integration/config.toml`
并列出一个 **专用测试日历**(ID 或名称)- 绝不能是您的主要日历。
3. `make integration`
测试套件仅触及它创建的事件(标记摘要 ~30 天后)并在完成后清理。
没有 `config.toml` 或存储的会话,它会跳过。
## 已知限制
- **重复边缘情况**:事件扩展在客户端;不支持“此事件和未来事件”的分割(编辑单个事件或整个系列)。服务器要求系列的开始时间必须与其 RRULE 模式匹配 - 调整 `--rrule` 以移动系列的开始时间。
- **与会者/邀请**:不支持;事件不会创建与会者。
- **事件颜色**:仅接受 Proton 的固定调色板,并且无法清除为“无” - `--color default` 恢复到日历颜色(Proton 没有每事件“无颜色”状态,一旦设置)。
- **创建日历**:不支持(需要生成新的日历密钥包)。您可以更新和删除日历,但请在 Proton 应用程序中创建它们。
- **删除所有者日历是交互式的**:需要重新输入登录密码(提示,或 `--password` 用于脚本),因为 Proton 将删除操作置于提升的重新身份验证范围之后。假日日历无需密码即可删除;订阅的日历必须在应用程序中取消订阅。
- **仅 FIDO2 的 2FA**:不支持(仅 TOTP)。
- **API 日期过滤**:Proton 忽略事件列表的 `Start`/`End` 并分页处理 100/页 - CLI 分页并在客户端过滤。
- **反向工程**:Proton 可能随时更改其内部 API。
## 许可证
根据 [The Unlicense](UNLICENSE) 授予公共领域。