Archived
邮件路由契约 15 条:账号 CRUD / 探测 / 立即拉取 / 试发 / 入站出站列表 / 附件与原件 / 重新投递
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 邮件路由的归属:15 条全部进契约,注册表建得起来(字面 / 参数模版互不撞),
|
||||
* 读标两端、写归运营中心(设计稿 §7.2)。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import * as contracts from '@contracts';
|
||||
import { MAIL_ROUTES } from '@contracts';
|
||||
import { buildRouteRegistry, routesOf } from '../route-registry.ts';
|
||||
|
||||
describe('邮件路由契约', () => {
|
||||
it('15 条全在契约命名空间里,注册表能建起来', () => {
|
||||
const all = routesOf(Object.values(contracts));
|
||||
const registry = buildRouteRegistry(all);
|
||||
expect(MAIL_ROUTES).toHaveLength(15);
|
||||
for (const route of MAIL_ROUTES) {
|
||||
expect(registry.has(route.method, route.path)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('读标两端,写归 ops', () => {
|
||||
for (const route of MAIL_ROUTES) {
|
||||
if (route.method === 'GET') expect(route.access).toEqual(['design', 'ops']);
|
||||
else expect(route.access).toBe('ops');
|
||||
}
|
||||
});
|
||||
|
||||
it('未存草稿探测与已存账号探测是两条不同的路', () => {
|
||||
const registry = buildRouteRegistry(routesOf(Object.values(contracts)));
|
||||
expect(registry.lookup('POST', '/api/gov/mail/accounts/probe')).toBe('ops');
|
||||
expect(registry.lookup('POST', '/api/gov/mail/accounts/acc-1/probe')).toBe('ops');
|
||||
expect(registry.lookup('GET', '/api/gov/mail/inbound/m-1/attachments/0')).toEqual(['design', 'ops']);
|
||||
});
|
||||
});
|
||||
@@ -2,3 +2,4 @@ export * from './http.ts';
|
||||
export * from './workbench.ts';
|
||||
export * from './governance.ts';
|
||||
export * from './ingest.ts';
|
||||
export * from './mail.ts';
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 邮件收发的路由契约(设计稿 §7.2;ADR-013)。
|
||||
*
|
||||
* 读标两端(设计中心要看通道对上了哪个信箱,运营中心管配置);写归运营中心。
|
||||
* `/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: ['design', '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: ['design', '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: ['design', '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: ['design', '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: ['design', '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: ['design', '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: ['design', '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,
|
||||
];
|
||||
Reference in New Issue
Block a user