This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/docs/前端对接说明-邮件收发.md
T

9.3 KiB
Raw Blame History

邮件收发 —— 前端对接说明

后端已按 ADR-013 接上腾讯企业邮箱(两条路:IMAP/SMTP、企业微信邮件 API)。这份说明只讲 前端要做什么、后端给了什么;接口字段与状态码以 docs/API文档-邮件收发.md 与 packages/contracts/src/{mail.ts, api/mail.ts} 为准。鉴权、票据、SSE 的通则照《前端对接说明-统一鉴权中心》。

0. 一分钟看懂

  • 腾讯没有带内容的推送:后端每个账号按 pollIntervalSec 轮询拉信,拉到就分发给订阅方(现在只有 channel)。 界面上没有「实时收信」这回事,有的是「上次拉取时间 / 游标 / 上次错误」三个事实。
  • 账号有两种:IMAP/SMTP(任何腾讯企业邮箱地址,凭证是客户端专用密码)与企业微信应用邮箱(自建应用的 corpid + secret)。 表单按 kind 切换字段;两种都有「探测」。
  • 凭证只写不读:视图只有 secretBound;编辑时密码框留空 = 不改。
  • 探测 / 立即拉取 / 试发 的失败在 200 的 body 里(ok:false / state:'failed' / error 原话),不是接口错误;把原话原样摆出来。
  • 建议落点:运营中心 · 系统区 新增「邮箱」页(账号列表 + 详情 / 表单;来信;发信)。15 条接口全部 access: 'ops'—— 账号 / 来信 / 发信记录都只有运营中心能读能写,设计中心没有这一页、也调不通(邮箱那一屏没有设计端消费方, 入站正文又是客户来往内容,见 ADR-013 决策 10)。

1. 建议的信息架构

运营中心 / 系统 / 邮箱
├─ 账号列表(GET /api/gov/mail/accounts)              每行:名称 · kind 徽 · 地址 · 默认 ★ · 启用 · 通道 · 上次拉取 / 上次错误 · 「正在拉取」灯
│   ├─ 新建 / 编辑(POST / PUT)                          表单按 kind 切换;底部三个钮:测试连接 · 保存 · 删除
│   └─ 行内动作:立即拉取(POST …/poll)· 试发(POST …/send-test)· 探测(POST …/:id/probe)
├─ 来信(GET /api/gov/mail/inbound?accountId&limit)      列表 → 详情(正文 / 附件 / 原件 / 投递状态 / 重新投递)
└─ 发信记录(GET /api/gov/mail/outbound?accountId&limit)  列表:主题 · 收件人 · 状态 · 错误 · 来路(tool / channel / ops)· 时间

2. 账号表单(字段清单与默认值)

公共字段:name(必填,≤64)· displayName(发件人显示名)· enabled(默认开)· isDefault(默认发件账号,全局至多一个;勾上会把别人的放下)· pollIntervalSec(默认 30,10–3600)· channelId(下拉,来自 GET /api/gov/channels;空 = 来信留在运营面不进事件流,投递状态显示 skipped · 账号未绑定通道)· backfillFrom(日期,空 = 首次拉取从当下开始,不回填整箱)。

kind = 'imap-smtp':address(登录邮箱,必填)· settings.imapHost / imapPort / imapTls(默认 imap.exmail.qq.com / 993 / 开)· settings.smtpHost / smtpPort / smtpTls(默认 smtp.exmail.qq.com / 465 / 开;关 = STARTTLS,配 587)· settings.login(登录名,缺省 = 地址)· settings.folder(默认 INBOX)· secret.password(客户端专用密码;编辑时留空 = 不改)。

kind = 'wecom':settings.corpId(必填)· settings.apiBase(默认 https://qyapi.weixin.qq.com,一般不改)· secret.corpSecret(应用 secret;编辑时留空 = 不改)。 address 不让人填:探测成功后由后端回填(应用邮箱地址),探到之前摆「待探测」。

保存后建议立刻调一次 POST …/:id/probe,把报告摆在表单下方(收 / 发两栏)。

3. 四个动作的交互与「界面不撒谎」

动作 接口 怎么摆
测试连接(表单里) POST /api/gov/mail/accounts/probe?accountId=<编辑中的 id> body = 表单 收 / 发两栏各一个绿勾 / 红叉 + 原话;wecom 的发信栏是灰色「未验证」+ note(拿到 token 不等于能发);没重填密码时必须带 ?accountId=,否则 412
探测(已存) POST …/:id/probe 同上;wecom 探到地址后列表里的 address 会变,重取列表
立即拉取 POST …/:id/poll 摆 pulled / stored / dispatched;more:true 提示「还有余下,下一轮接着」;error 原话;409 摆「另一台后端正在拉取,稍后再试」;412 摆「账号已停用」
试发 POST …/:id/send-test body {to, subject, text} state:'sent' 绿、'failed' 红 + error 原话;这一笔会出现在发信记录里

列表上的「正在拉取」灯只在 status.polling 为真时亮;status.lastPollError 非空就在行上摆一条红带(原话)。

4. 来信与投递状态

  • 列表按收信时间倒序;每行:发件人 · 主题 · 收信时间 · 附件数 · 投递状态徽。
  • 投递状态徽按 deliveries[] 每个订阅方一枚:done 绿 / skipped 灰(悬停 error 里的理由)/ failed 红(原话)/ dispatching 蓝。 dispatching 超过 10 分钟 = 进程中断,摆成「投递状态未知」并给「重新投递」钮。
  • 详情:text 直接摆;html 走 @ui 的 ViewContainer 沙箱(feature 不得自行 dangerouslySetInnerHTML);references / inReplyTo 折叠在「线程」里。
  • 附件:ref !== null 的可预览 / 下载 —— GET …/attachments/:index 回 JSON + base64,与上传件同形, 预览走 fetch(带 Authorization)解 base64;「下载原件」用票据:GET /api/auth/ticket → href = …/attachments/0?ticket=…,在 onClick 里取完再跳(票据 60 秒)。 ref === null 的摆 note(「超过 8MB 上限,未存原件」),不给下载钮。
  • 「原件 .eml」同法:GET …/raw(rawAvailable:false 不给钮)。
  • 「重新投递」:POST …/redeliver body {subscriber} 或不带;先弹一句「从这一刻起是至少一次:channel 会再起一单」再发。

5. 发信记录

列表:主题 · 收件人 · state(sending 蓝 / sent 绿 / failed 红 + error)· requestedBy.kind(tool 活动放行 / channel 通道回信 / ops 运营试发)· createdAt / sentAt。 sending 久停不动 = 进程中断(没有自动重试;活动放行那一侧会按落地失败处理)。

6. SSE 失效映射

app/subscribe.ts 的 invalidateFor 加一条:touch.kind === 'system' && touch.id === 'mail' → 失效 ['gov','mail'] 前缀 (账号状态 / 来信 / 发信三组键都挂在它下面);写路由那一侧照旧走 kind:'system', id:'gov'。 reason 取值:inbound(新来信或拉取出错)· outbound(发信落地)。不带载荷,回服务端重取。

7. 腾讯侧前置(管理员执行;缺一步就是探测红叉)

IMAP/SMTP 路

  1. 企业微信管理端「协作 → 邮件 → 安全管理 → 客户端访问权限」放开 IMAP/SMTP(范围含这个邮箱)。
  2. 该邮箱登录网页端「设置 → 收发信设置」开启 IMAP/SMTP 服务。
  3. 开了安全登录 / 微信绑定的邮箱,在「设置 → 邮箱绑定」生成客户端专用密码(最多 20 个,短信下发),填进表单的密码框; 公共邮箱可在管理端生成(或经 API 建箱时 create_auth_code:1,只返回一次)。
  4. 服务器:imap.exmail.qq.com:993 / smtp.exmail.qq.com:465(表单默认值);信创环境是 xcimap / xcsmtp。

企业微信邮件 API 路

  1. 企业微信 ≥ 4.0.20;建自建应用,记下 corpid 与应用 secret。
  2. 管理端「协作 → 邮件」给该应用授邮件权限 —— 系统自动分配应用邮箱(前缀 = 应用名拼音,可改)。
  3. 企业开了 API IP 白名单的,把后端出口 IP 加进去(否则探测报 60020)。
  4. 只能收发应用邮箱自己的信,不能读成员邮箱;发出的信拿不到 Message-ID(回信的线程归属靠地址 + 时间窗)。

发信配额归企业级共享(腾讯按账号数与信誉浮动),超了 SMTP / API 都会拒 —— 摆原话即可,后端不做配额控制。

8. 错误处理

状态 何时 前端动作
400 表单不合 schema / 试发缺正文 按 detail(zod issues)标到字段
404 账号 / 来信 / 附件(超限没存)/ 原件(没存)/ 订阅方 刷新列表;附件那两种 404 不是 bug,是「没存」
409 账号名重复 / 另一台后端占着租约 原话摆给人;租约那条几秒后可重试
412 账号停用 / 没有默认账号 / 凭证空 原话摆给人;凭证空 → 引导去填密码

9. 接入完成的判据

  • 两种 kind 都能建、改、删;编辑时不填密码保存后 secretBound 仍为 true
  • 表单里「测试连接」对已存账号带 ?accountId=,不重填密码也能测;收 / 发两栏各自显示原话
  • wecom 账号探测后 address 从「待探测」变成应用邮箱地址
  • 立即拉取后来信列表多出条目;投递状态徽三色正确;skipped 悬停能看到理由
  • 附件预览 / 下载原件(票据)拿到真字节(.xlsx 能被 Excel 打开);超限附件只显示 note
  • 重新投递前有提示,投递后状态刷新
  • SSE:另一台后端拉到新信,本页 6 秒内自己刷新(不用点)
  • 试发失败时列表里看得到 failed 与原话
  • 账号停用 / 凭证空 / 名字重复的提示原话可见,没有「操作失败」这种空话

10. 本文与真实契约的核对状态

路由 15 条与 packages/contracts/src/api/mail.ts 逐条对过(全部 access: 'ops');字段名与 packages/contracts/src/mail.ts 同名。 示例响应按契约构造 —— 真实响应待账号配好后从 API 文档 §6 的 curl 序列换。