# WpywMail 客户端 给自建邮件服务器 **WpywMail**(`mail.example.com`)写的桌面客户端。 收信走 **IMAP4rev1(993 隐式 TLS)**,发信走 **SMTP 提交(587 STARTTLS + AUTH)**。 界面用 **shadcn/ui + Tailwind CSS v4 + Vite(React 19)** —— 结构与视觉以 shadcn 官方的 Mail 示例为基线,控件不自己发明。后端是**零依赖的 Node 服务**,负责讲 IMAP/SMTP 与 MIME。 ## 怎么启动 双击 **`start.cmd`**。它会: 1. 若 `ui/dist` 尚未构建,自动 `npm install` + `npm run build`(首次约一两分钟); 2. 起一个只监听 `127.0.0.1:8788` 的本地服务(界面与邮件协议之间的桥); 3. 自动打开浏览器指向 `http://127.0.0.1:8788/`。 关闭那个黑窗口就等于退出客户端。需要 Node.js 20+(当前机器 v24)。 改界面时用热更新:`npm --prefix ui run dev`(已配好把 `/api` 代理到 8788)。 首次启动若 `data\account.json` 里没有账号,界面会要求登录;登录成功后凭据写回该文件(只在本机)。 ## 现在能做什么 | 能力 | 说明 | |---|---| | 收信 | 6 个文件夹(收件箱/草稿/已发送/归档/垃圾邮件/废纸篓),带未读数 | | 邮件列表 | 头像、发件人、主题、时间、大小、未读圆点、旗标、已回复/草稿标记 | | 阅读区 | 中文主题与正文正确解码、附件下载、发件人与收件人信息、完整时间 | | 撰写 / 回复 / 转发 | 弹窗式;中文头部自动 RFC 2047 编码;多附件;回复带 `In-Reply-To`/`References` | | 快速回复 | 阅读区底部直接写,`Ctrl+Enter` 发送 | | 草稿 | 「存草稿」写入 Drafts | | 搜索 | 主题 / 发件人 / 收件人 / UID,**支持中文**(见「已知限制」) | | 管理动作 | 已读未读、旗标、归档、删除(默认进废纸篓)、切换文件夹 | | 主题 | 深色/浅色双主题,切换后记忆(默认深色) | ### 键盘 | 键 | 动作 | |---|---| | `J` / `K`(或 ↑↓) | 上下选邮件 | | `Enter` | 打开 | | `C` | 撰写 | | `R` / `F` | 回复 / 转发 | | `S` | 加/取消旗标 | | `U` | 已读/未读切换 | | `E` | 归档 | | `#` | 删除 | | `/` | 聚焦搜索框 | | `Esc` | 关闭弹窗 / 取消焦点 | ## 架构 ``` start.cmd └─ node server/index.js 本地服务(仅 127.0.0.1:8788):托管界面 + JSON API ├─ server/imap.js IMAP4rev1 客户端(自己实现,含字面量感知解析器) ├─ server/smtp.js SMTP 提交客户端(STARTTLS + AUTH PLAIN,DATA 按字节写出) ├─ server/mime.js MIME 编解码(RFC 5322/2047/2231/2045) ├─ server/account.js 账户配置读写(data/account.json) ├─ ui/ 界面源码(shadcn/ui + Tailwind + Vite)→ 构建到 ui/dist └─ web/ 旧版纯手写界面(兜底;ui/dist 不存在时才会被发出去) ``` **为什么后端零依赖**:IMAP/SMTP/MIME 是这块的硬骨头,自己实现能对症下药 —— 实测用它挖出了服务端 三个真实缺陷(见 `E:\deepseek\artifacts\wpywmail-server-findings.md`),并且不引入供应链风险。 **为什么界面用现成组件库**:控件观感不必自己发明。shadcn 是「把源码复制进项目」的组件(不是黑盒依赖), 随手可改,同时白拿 Radix 的可访问性底座与 Tailwind 的迭代速度。 ## 设计系统 结构实例化 **shadcn/ui 官方 Mail 示例**(左:账号 + 文件夹导航;中:邮件列表;右:阅读与操作)。 配色、圆角、阴影、暗色全部用 **shadcn 默认(new-york / neutral)主题 token**,原样写在 `ui/src/styles.css`(`oklch()` 变量 + `.dark` 覆盖),所以 light/dark 都是"官方观感"。 换主题只改那一个文件的变量块。 字体走系统栈(Segoe UI / 微软雅黑 / PingFang)—— 邮件客户端要的是"读起来像系统原生",不是品牌表达。 ## 验收记录 | 套件 | 结果 | 报告 | |---|---|---| | MIME 单元测试 `node tools/mime-test.js` | **25/25** | `E:\deepseek\artifacts\client-mime-test.txt` | | 协议闭环自测 `node tools/smoke.js` | **20/20** | `E:\deepseek\artifacts\client-smoke.txt` | | 后端 API 链路 `node tools/api-check.js` | **10/10** | `E:\deepseek\artifacts\client-api-check.txt` | | 界面运行时(新 UI)`node tools/ui-check-v2.js` | **8/8** | `E:\deepseek\artifacts\client-ui-check-v2.txt` | `smoke.js` 是真正的闭环:**用本项目的代码发一封中文信 → 服务器投递 → 再用本项目的 IMAP 代码 把它从收件箱取回并解析**,断言主题与正文(含全角标点)往返无损。 `ui-check-v2.js` 用 **Edge 无头 + CDP(Node 自带 WebSocket,零依赖)** 真开页面:收集 JS 异常与 控制台报错、断言文件夹/列表/阅读区/撰写弹窗都渲染、验证主题切换真的改变背景色、检查无横向溢出。 `api-check.js` 覆盖界面所用的那条链路:发送 → 到达 → 旗标 → 未读 → 移动(归档/移回)→ 删除 → 清理痕迹。 ## 已知限制(都是真的,没藏) 1. **搜索是「服务端取回摘要 + 本地过滤」**,窗口上限 500 封,**不含正文全文**。 原因:服务端 `SEARCH` 没实现 `BODY`/`OR`,且命令行按 `Encoding.ASCII` 解码 → 中文检索词到服务端 会变成 `??`(详见服务端缺陷记录)。客户端绕过了它,所以中文搜索可用。 2. **不渲染 HTML 邮件正文**,只渲染解析出的纯文本(避免 XSS)。只有 HTML 的邮件会退化成去标签文本。 3. **附件只支持「添加并随信发出」与「下载」**,不做内联图片渲染。 4. **密码以明文存在 `data\account.json`**(本机、便携)。要更稳妥可以用 Windows DPAPI 包一层。 5. 未实现:IDLE 实时推送(目前 30 秒轮询文件夹计数)、会话视图、离线缓存、富文本撰写、多账号。 6. 服务端目前**没有 `SPECIAL-USE`**,文件夹靠名字映射。 7. `web/`(旧手写界面)已退役但保留作兜底;日常运行的是 `ui/dist`。 ## 排障 - **界面显示"正在连接邮件服务器…"不动**:看黑窗口里的报错,多半是服务器 993 连不上或密码变了。 - **发送失败**:报错原样显示服务端返回(如 `550 Relay denied`)。家用 DNS 偶发瞬时解析失败 (`EAI_AGAIN`)已加一次自动重试。 - **改了界面没生效**:`npm --prefix ui run build` 重新构建(`ui/dist` 是静态产物,改了源码不会自动更新)。 - **端口被占**:设环境变量 `WPYWMAIL_PORT` 换端口。