// Package mail defines the MailBackend abstraction (ARCHITECTURE.md §0.2): all // inbound delivery and outbound sending sit behind one interface so the core // (API, MCP, store, threading) never depends on *how* mail moves. Concrete // backends — relay, imap_smtp, embedded — live in subpackages and are added in // later milestones. Milestone 1 ships only NullBackend. package mail import ( "context" "errors" ) // OutgoingMessage is a message the core wants sent. The backend is responsible // for building/serializing and (where applicable) DKIM-signing it. type OutgoingMessage struct { InboxID string From string To []string Cc []string Bcc []string Subject string Text string HTML string InReplyTo string References []string } // SendResult reports the outcome of a Send. type SendResult struct { MessageIDHdr string // RFC 5322 Message-ID assigned to the sent message Accepted bool } // InboundSink receives raw RFC 5322 messages a backend has accepted for an // address. The core implements this (parse → thread → store → events). type InboundSink interface { Deliver(ctx context.Context, inboxAddr string, raw []byte) error } // Caps advertises what a backend can do, so the API/MCP can expose accurate // capabilities (e.g. whether throwaway addresses or custom domains are possible). type Caps struct { SelfHost bool // runs its own MTA in-process InboundPush bool // delivers inbound without polling (webhook or :25) CustomDomain bool // can own an arbitrary domain ThrowawayAddrs bool // can mint addresses on demand } // MailBackend abstracts where mail comes from and how it leaves. type MailBackend interface { // Send dispatches an outgoing message. Send(ctx context.Context, msg *OutgoingMessage) (SendResult, error) // Start delivers inbound messages to sink until ctx is cancelled. Start(ctx context.Context, sink InboundSink) error // Capabilities describes what this backend supports. Capabilities() Caps } // ErrNotSupported is returned by backends for operations they cannot perform. var ErrNotSupported = errors.New("mail: operation not supported by this backend") // NullBackend satisfies MailBackend without moving any mail. It is the // milestone-1 default: messages enter only via the ingest API (core acts as its // own InboundSink), and sending is unavailable until a real backend is wired. type NullBackend struct{} func (NullBackend) Send(context.Context, *OutgoingMessage) (SendResult, error) { return SendResult{}, ErrNotSupported } // Start blocks until cancelled; the null backend never produces inbound mail. func (NullBackend) Start(ctx context.Context, _ InboundSink) error { <-ctx.Done() return ctx.Err() } func (NullBackend) Capabilities() Caps { return Caps{} }