/** * 邮件收发的路由契约(设计稿 §7.2;ADR-013)。 * * 读写都归运营中心(设计稿 §7.2 + 2026-08-26 裁决):邮箱那一屏落在运营中心,设计端没有消费方; * 而入站正文装的是客户来往内容 —— ADR-012 已把「设计端能读客户主数据」记为一处后果,不再无故加宽。 * `/api/gov/*` 的写路由由 app 中间件自动广播 `{kind:'system', id:'gov'}`。 * 探测 / 试发 / 拉取的**厂商失败不是接口错误**:200 的 body 里带原话(脱敏)。 */ import { z } from 'zod'; import type { RouteContract } from './http.ts'; import { mailAccountInputSchema, mailAccountViewSchema, mailDeliveryViewSchema, mailInboundDetailSchema, mailInboundSummarySchema, mailOutboundViewSchema, mailPollOutcomeSchema, mailProbeReportSchema, mailSendResultSchema, } from '../mail.ts'; const accountParams = z.object({ accountId: z.string().min(1) }); const mailParams = z.object({ mailId: z.string().min(1) }); const listQuery = z.object({ accountId: z.string().optional(), /** 默认 50,上限 200 */ limit: z.coerce.number().int().min(1).max(200).optional(), }); export const listMailAccountsRoute = { method: 'GET', path: '/api/gov/mail/accounts', access: 'ops', output: z.object({ accounts: z.array(mailAccountViewSchema) }), } as const satisfies RouteContract; export const createMailAccountRoute = { method: 'POST', path: '/api/gov/mail/accounts', access: 'ops', body: mailAccountInputSchema, output: mailAccountViewSchema, } as const satisfies RouteContract; /** 未存的配置探测 —— 表单上正填的那份,一个字节都不落库 */ export const probeMailDraftRoute = { method: 'POST', path: '/api/gov/mail/accounts/probe', access: 'ops', /** 改已存账号时带上 ?accountId=:表单没重填密码就借库里那份密文 */ query: z.object({ accountId: z.string().optional() }), body: mailAccountInputSchema, output: mailProbeReportSchema, } as const satisfies RouteContract; export const getMailAccountRoute = { method: 'GET', path: '/api/gov/mail/accounts/:accountId', access: 'ops', params: accountParams, output: mailAccountViewSchema, } as const satisfies RouteContract; /** secret 缺省 = 不改密文;kind / settings 变了游标清空重锚 */ export const updateMailAccountRoute = { method: 'PUT', path: '/api/gov/mail/accounts/:accountId', access: 'ops', params: accountParams, body: mailAccountInputSchema, output: mailAccountViewSchema, } as const satisfies RouteContract; export const deleteMailAccountRoute = { method: 'DELETE', path: '/api/gov/mail/accounts/:accountId', access: 'ops', params: accountParams, output: z.object({ ok: z.literal(true) }), } as const satisfies RouteContract; /** 已存账号探测(用库里凭证);凭证空 412 */ export const probeMailAccountRoute = { method: 'POST', path: '/api/gov/mail/accounts/:accountId/probe', access: 'ops', params: accountParams, output: mailProbeReportSchema, } as const satisfies RouteContract; /** 立即拉一轮;另一进程占着租约 409;停用 412 */ export const pollMailAccountRoute = { method: 'POST', path: '/api/gov/mail/accounts/:accountId/poll', access: 'ops', params: accountParams, output: mailPollOutcomeSchema, } as const satisfies RouteContract; export const sendTestMailRoute = { method: 'POST', path: '/api/gov/mail/accounts/:accountId/send-test', access: 'ops', params: accountParams, body: z.object({ to: z.email(), subject: z.string().min(1), text: z.string().min(1) }), output: mailSendResultSchema, } as const satisfies RouteContract; export const listMailInboundRoute = { method: 'GET', path: '/api/gov/mail/inbound', access: 'ops', query: listQuery, output: z.object({ mails: z.array(mailInboundSummarySchema) }), } as const satisfies RouteContract; export const getMailInboundRoute = { method: 'GET', path: '/api/gov/mail/inbound/:mailId', access: 'ops', params: mailParams, output: mailInboundDetailSchema, } as const satisfies RouteContract; /** 附件原件(JSON + base64,与 /api/gov/upload/:id 同形);可带 ?ticket= 下载(ADR-012 §6) */ export const mailAttachmentRoute = { method: 'GET', path: '/api/gov/mail/inbound/:mailId/attachments/:index', access: 'ops', params: z.object({ mailId: z.string().min(1), index: z.coerce.number().int().min(0) }), output: z.object({ name: z.string(), contentType: z.string(), dataBase64: z.string() }), } as const satisfies RouteContract; /** 原件 .eml;超限没存 404 */ export const mailRawRoute = { method: 'GET', path: '/api/gov/mail/inbound/:mailId/raw', access: 'ops', params: mailParams, output: z.object({ name: z.string(), contentType: z.literal('message/rfc822'), dataBase64: z.string() }), } as const satisfies RouteContract; /** 人按的钮:重新投给订阅方(缺省全部);从此是至少一次 */ export const redeliverMailInboundRoute = { method: 'POST', path: '/api/gov/mail/inbound/:mailId/redeliver', access: 'ops', params: mailParams, body: z.object({ subscriber: z.string().min(1).optional() }).default({}), output: z.object({ deliveries: z.array(mailDeliveryViewSchema) }), } as const satisfies RouteContract; export const listMailOutboundRoute = { method: 'GET', path: '/api/gov/mail/outbound', access: 'ops', query: listQuery, output: z.object({ mails: z.array(mailOutboundViewSchema) }), } as const satisfies RouteContract; /** 全部 15 条 —— 文档总表与覆盖测试按它数 */ export const MAIL_ROUTES: readonly RouteContract[] = [ listMailAccountsRoute, createMailAccountRoute, probeMailDraftRoute, getMailAccountRoute, updateMailAccountRoute, deleteMailAccountRoute, probeMailAccountRoute, pollMailAccountRoute, sendTestMailRoute, listMailInboundRoute, getMailInboundRoute, mailAttachmentRoute, mailRawRoute, redeliverMailInboundRoute, listMailOutboundRoute, ];