mail.send 工具契约 2.0.0(to 数组 / html / 附件 / inReplyTo / dryRun;保留 v1 的 body 与 queued);契约快照刷新,五道门禁绿

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
This commit is contained in:
xcs
2026-08-26 02:23:24 +08:00
co-authored by Claude Fable 5
parent b6ec37a231
commit e0fc16db33
10 changed files with 1340 additions and 8 deletions
+62
View File
@@ -0,0 +1,62 @@
/**
* module: mail —— 对外类型(编码规范 §10.1;设计稿 §5.1)。
*/
import type { MailAccountInput, MailAccountView, MailDeliveryView, MailInboundDetail, MailInboundSummary, MailOutboundView, MailPollOutcome, MailProbeReport, MailSendInput, MailSendResult } from '@contracts';
export type { MailAccountInput, MailAccountView, MailDeliveryView, MailInboundDetail, MailInboundSummary, MailOutboundView, MailPollOutcome, MailProbeReport, MailSendInput, MailSendResult, };
export { MailAccountDisabledError, MailAccountNameTakenError, MailAccountNotFoundError, MailAttachmentNotFoundError, MailInboundNotFoundError, MailNoDefaultAccountError, MailPollBusyError, MailSecretMissingError, MailSendInvalidError, MailSubscriberTakenError, MailSubscriberUnknownError, } from './internal/domain/errors.ts';
/**
* 订阅方回调 —— 「注册回调读到邮件内容」的落点。
* 返回 `{skipped}` 表示这封信与我无关(留痕 skipped,不算失败);抛错留痕 failed(原话脱敏)。
* **自动只投一次,不自动重试**(设计稿 §9.3);人按「重新投递」才再来。
*/
export type InboundHandler = (mail: MailInboundDetail, ctx: {
readonly accountId: string;
readonly channelId: string | undefined;
}) => Promise<void | {
readonly skipped: string;
}>;
export type MailService = {
listAccounts(): Promise<MailAccountView[]>;
getAccount(accountId: string): Promise<MailAccountView>;
createAccount(input: MailAccountInput): Promise<MailAccountView>;
/** secret 缺省 = 不改密文;kind / settings 变了游标清空重锚 */
updateAccount(accountId: string, input: MailAccountInput): Promise<MailAccountView>;
/** 留痕不删(入站 / 出站表对账号不设外键) */
deleteAccount(accountId: string): Promise<void>;
/** 已存账号用库里凭证;未存用入参里的;一个字节都不落库(wecom 探到地址的已存账号除外:回填 address) */
probe(target: {
accountId: string;
} | {
input: MailAccountInput;
accountId?: string | undefined;
}): Promise<MailProbeReport>;
/** 工作者用:到期且未被占的账号各拉一轮(账号间并行) */
pollDue(now: Date): Promise<MailPollOutcome[]>;
/** 路由用:租约被占 → MailPollBusyError;停用 → MailAccountDisabledError */
pollNow(accountId: string): Promise<MailPollOutcome>;
/** 同名重复注册 → MailSubscriberTakenError;返回注销函数 */
subscribe(name: string, handler: InboundHandler): () => void;
listInbound(query: {
accountId?: string | undefined;
limit?: number | undefined;
}): Promise<MailInboundSummary[]>;
getInbound(mailId: string): Promise<MailInboundDetail>;
attachment(mailId: string, index: number): Promise<{
name: string;
contentType: string;
bytes: Uint8Array;
}>;
/** 原件 .eml;超限没存 → MailAttachmentNotFoundError(index = -1) */
raw(mailId: string): Promise<{
name: string;
bytes: Uint8Array;
}>;
/** 人按的钮:指定订阅方或全部;正在 dispatching 的那条跳过 */
redeliver(mailId: string, subscriber?: string): Promise<MailDeliveryView[]>;
/** 先落 sending 行再经 provider 发;厂商失败落 failed 并以 state='failed' 返回,不抛 */
send(input: MailSendInput): Promise<MailSendResult>;
listOutbound(query: {
accountId?: string | undefined;
limit?: number | undefined;
}): Promise<MailOutboundView[]>;
};