/** * routes/email.ts, the daemon actually serving `email.*`. * * These four methods shipped cataloged with `invokable: false` because the * IMAP/SMTP service that could satisfy them lived inside a single product, so * the daemon had nothing to call. The practical consequence was not a 404 on * an obscure path: it meant scheduled work, triggers and channel-driven work * could not send mail at all, with no product process attached. A reminder * that was supposed to arrive by email simply did not. * * The service is platform capability now (`platform/email`), so this module is * the thin part: it maps the descriptors' declared shapes onto a narrow * service slice. It performs no I/O, holds no credential, opens no socket, and * knows nothing about IMAP or SMTP. * * Three properties are enforced here rather than merely advertised: * * - **`confirm: true` gates the send.** `email.send` is an irreversible * outward effect. The descriptor marks it `dangerous`/`admin` and requires * `confirm` in its schema, and this module refuses without it as well, so * the guarantee does not rest on schema validation being reached by every * transport that can invoke a method. * - **A body is never echoed into an error.** Failures report the stage and * the server's own plain-language reason; the message being sent is not * part of a diagnostic. * - **Read stays read.** `inbox.list` and `inbox.read` are declared read-only * and the underlying client uses `BODY.PEEK`, so serving them over the wire * cannot mark the owner's mail as read. * * Mail read through here is attacker-controlled text, anyone who knows the * address can put words in front of whatever consumes this. The service * records an untrusted ingest for every message it returns; a caller that * feeds a body onward is responsible for keeping that provenance attached. */ import type { GatewayMethodCatalog } from '../method-catalog.js'; import type { GatewayMethodHandler } from '../method-catalog-shared.js'; import { type UntrustedContentLedger } from '../../security/untrusted-content.js'; /** One inbox message, in the shape `email.inbox.list` advertises. */ export interface EmailGatewayMessageSummary { readonly uid: number; readonly from: string; readonly subject: string; readonly date: string; readonly unread: boolean; readonly bodyPreview: string; readonly messageId: string; } export interface EmailGatewayAttachment { readonly filename: string; readonly contentType: string; readonly sizeBytes: number; } /** One message, in the shape `email.inbox.read` advertises. */ export interface EmailGatewayMessageDetail { readonly uid: number; readonly from: string; readonly subject: string; readonly date: string; readonly messageId: string; readonly bodyText: string; readonly bodyHtml?: string; readonly attachments?: readonly EmailGatewayAttachment[]; } export interface EmailGatewayListInput { readonly limit?: number | undefined; readonly since?: string | undefined; readonly unreadOnly?: boolean | undefined; } /** A FETCH response on the returned page the daemon could not read. */ export interface EmailGatewayUnreadableResponse { /** The UID the response named, or absent when it named none legibly. */ readonly uid?: number; readonly detail: string; } export interface EmailGatewayListResult { readonly messages: readonly EmailGatewayMessageSummary[]; /** Matches BEFORE `limit` truncation, so a caller can tell there is more. */ readonly total: number; /** * Answers on THIS page the daemon could not read. Absent when there were * none. * * Without it a short page is silent, and a caller reading `messages` against * `total` cannot tell "that message was deleted between the search and the * fetch" from "that message is in the mailbox and we could not read what the * server said about it". The first is ordinary; the second means the owner * is looking at a list that is missing mail and reading it as complete. */ readonly unreadable?: readonly EmailGatewayUnreadableResponse[]; } export interface EmailGatewayDraftInput { readonly to: string; readonly subject: string; readonly body: string; readonly inReplyTo?: string | undefined; readonly references?: string | undefined; } export interface EmailGatewayDraftResult { /** The draft's Message-ID. Always present. */ readonly draftId: string; /** * The APPENDUID the server assigned, when it advertises UIDPLUS. Absent * otherwise, a server that does not report one has not given us a uid, and * inventing a number a later fetch would not resolve is worse than saying so. */ readonly uid?: number; /** The Drafts folder the message actually landed in. */ readonly mailbox: string; } export interface EmailGatewaySendInput { readonly to: string; readonly subject: string; readonly body: string; readonly inReplyTo?: string | undefined; } export interface EmailGatewaySendResult { readonly messageId: string; readonly sentAt: string; } /** * What a mail backend must be able to do to serve these verbs. * * Failures are reported by throwing `GatewayVerbError` with an honest status: * an unconfigured account is a 400 naming what to configure, a rejected * credential a 401, an unknown uid a 404. */ export interface EmailGatewayService { listInbox(input: EmailGatewayListInput): Promise; readMessage(uid: number): Promise; createDraft(input: EmailGatewayDraftInput): Promise; send(input: EmailGatewaySendInput): Promise; } export declare function createEmailInboxListHandler(service: EmailGatewayService): GatewayMethodHandler; export declare function createEmailInboxReadHandler(service: EmailGatewayService): GatewayMethodHandler; export declare function createEmailDraftCreateHandler(service: EmailGatewayService): GatewayMethodHandler; export declare function createEmailSendHandler(service: EmailGatewayService, /** * The ledger the daemon's page reads and mailbox reads both record into. * Defaults to the process-wide one, which is what production wants; tests * pass their own so one case's page read cannot colour the next case's send. */ ledger?: UntrustedContentLedger, /** * The owner's own addresses, from configuration. Empty disables the * owner-destination exemption rather than widening it, see * security/owner-identity.ts. */ ownerAddresses?: ReadonlySet): GatewayMethodHandler; /** Attach the email handlers to their registered descriptors (missing = no-op). */ export declare function registerEmailGatewayMethods(catalog: GatewayMethodCatalog, service: EmailGatewayService, ledger?: UntrustedContentLedger, /** See createEmailSendHandler. Empty means the exemption cannot fire. */ ownerAddresses?: ReadonlySet): void; //# sourceMappingURL=email.d.ts.map