Add AgentMail plugin: paid-member mailbox client for humans + agents

@logicsrc/plugin-agentmail provides read/search/compose/send/flag/delete
over an injected MailTransport, gated to Founding Lifetime (paid) members.
Returns plain JSON-serializable domain objects so the same API serves a
human TUI, the CLI, MCP, and bots.

- domain.ts: transport-agnostic types + pure helpers (parse/format address,
  normalizeDraft, isValidEmail, snippet)
- ports.ts: MailTransport seam
- service.ts: AgentMailService (inbox/list/read/search/send/reply/flag/delete)
- access.ts: paid-member gate (assertPaid) + capability constants
- transports/memory.ts: complete in-memory backend (tests/dev/reference)
- transports/mailu.ts: self-hosted Mailu seam (mail.profullstack.com IMAP +
  smtp.profullstack.com submission) with injected IMAP/SMTP drivers
- index.ts: PluginDefinition (manifest, routes, capabilities, tuiPanels)
- registered in the root build; 20 vitest tests pass

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-06-14 15:37:17 +00:00
parent bf046ae280
commit e304eac761
14 changed files with 908 additions and 1 deletions

View file

@ -0,0 +1,40 @@
// Access control for AgentMail. Mail is a Founding Lifetime Member (paid) perk,
// so every service call is gated on a paid identity. Capability constants keep
// route/manifest wiring and runtime checks in sync.
export const AGENTMAIL_CAPABILITIES = {
mailboxList: "mailbox.list",
messageList: "message.list",
messageRead: "message.read",
messageSearch: "message.search",
messageCompose: "message.compose",
messageSend: "message.send",
messageFlag: "message.flag",
messageDelete: "message.delete",
accessGate: "access.gate"
} as const;
export type AgentMailCapability = (typeof AGENTMAIL_CAPABILITIES)[keyof typeof AGENTMAIL_CAPABILITIES];
/** Who is acting: a BBS member name and whether they hold the paid membership. */
export interface MailIdentity {
/** The member's local-part / handle, e.g. "alice". */
name: string;
/** True for a Founding Lifetime Member; mail is gated on this. */
paid: boolean;
}
/** Raised when a non-paid (or missing) identity attempts a mail action. */
export class MailAccessError extends Error {
constructor(message = "AgentMail is a Founding Lifetime Member feature ($99 one-time) — upgrade: ssh join@bbs.profullstack.com") {
super(message);
this.name = "MailAccessError";
}
}
/** Throws MailAccessError unless the identity is a paid member. */
export function assertPaid(identity: MailIdentity): void {
if (!identity || !identity.name || !identity.paid) {
throw new MailAccessError();
}
}