Inbound Mail
Three features that turn a mailbox into an event-sourced inbound
channel: inbound-mail-foundation owns the pipeline, the
inbound-provider-* features are swappable mailbox backends. Your app
attaches business processes (ticketing, AI triage, document intake) as
consumers — it never talks IMAP.
inbound-mail-foundation
Section titled “inbound-mail-foundation”Status: ✅ Stable
What: The inbound pipeline: mail accounts (shared team inboxes and
personal mailboxes), exactly-once ingest with dedup, thread rollup, and
PII-encrypted storage. Every inbound mail becomes a domain event
(inbound-message-received) with a full replayable history — a consumer
you build next year can process this year’s mail.
How it works:
- A tenant connects a mailbox — via credentials form (IMAP) or the
built-in OAuth connect/callback routes (
createInboundMailConnectRoutes). - The watch-supervisor (
createInboundMailSupervisor) keeps a live push connection per account (IMAP IDLE — mail arrives in seconds) and runs a reconciliation poll as the correctness anchor. - Each mail is ingested exactly once: deterministic aggregate ids + a dedup anchor make provider replays no-ops.
- Sender, recipients, subject and snippet are envelope-encrypted per tenant before anything touches storage — tenant destroy crypto-shreds the key, making event log and projections unreadable.
Read models: read_mail_accounts, read_inbound_messages,
read_mail_threads — list queries decrypt on read and enforce mailbox
scoping (personal mailboxes are visible to their owner and tenant
admins only).
inbound-provider-imap
Section titled “inbound-provider-imap”Status: ✅ Stable
What: IMAP backend using password / app-password auth — covers
classic mail hosts without any OAuth setup. Live push via IMAP IDLE,
incremental sync via a UIDVALIDITY:lastUid cursor (a mailbox reset
triggers a clean full resync inside the backfill window).
Connection data lives as a JSON document in the per-account secret slot (host, port, user, password) — one tenant can connect mailboxes on different hosts side by side.
inbound-provider-inmemory
Section titled “inbound-provider-inmemory”Status: ✅ Stable
What: Scriptable in-process provider for tests, demos and sample apps. Seed messages, inject errors, simulate a dying IDLE connection — your consumer code runs against the same contract as production.
Attach your business process
Section titled “Attach your business process”Consumers hook onto the ingest write-handler; the job receives the plaintext normalized mail:
import { InboundMailFoundationHandlers } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
r.job( "triage-inbound", { trigger: { on: InboundMailFoundationHandlers.ingestMessage }, runIn: "worker" }, async (payload) => { // create a ticket, run AI classification, extract an invoice … // key side-effects by payload.providerMessageId — the trigger also // fires on idempotent replays. },);Recipe:
samples/recipes/inbound-mail-triage
shows the full flow — connect, ingest, worker-lane triage job, replay
idempotency — verified end-to-end.
Outlook
Section titled “Outlook”Microsoft 365 (Graph delta queries) and Gmail (history API) ship as
enterprise providers against the same plugin contract — the oauth
block and provider-opaque cursors are already part of it. Consumer code
does not change when a tenant switches providers.