Status: Implemented (Inline-mode core path, 2026-07-13). Own-inbox, real-time inbound, and overfwd Portfolio/Session modes remain deferred (see below).
Built as designed — overfwd is consumed as an ordinary HTTP service via the shipped
services/email.yaml template. The three core changes the design anticipated landed as:
x-overslash-encode: base64on an apiKey security scheme (Core-change #1). The decrypted secret was base64'd before thex-overslash-prefixwas prepended, so auser:passsecret withprefix: "Basic "emittedX-Mailbox-Auth: Basic base64(user:pass). (Superseded 2026-07-20 by D35: both extensions are removed. The mailbox login is now two separate secrets —mailbox_userandmailbox_pass— joined by the scheme's jq template'"Basic " + (.mailbox_user + ":" + .mailbox_pass | @base64)', so the password rotates on its own and neither half is a usable credential alone. Same header on the wire. See credential-templates.md.)- Multi-injection via
x-overslash-secret_source: instance | org(Core-change #2).ServiceDefinition.authwas already aVec;resolve_instance_authnow emits aSecretRefper apiKey scheme.instance(default, backward-compatible) resolves the instance's boundsecret_name(the per-mailboxuser:pass);orgresolves the scheme's fixeddefault_secret_namefrom the org vault (the shared gateway key). A template may declare at most oneinstance-source apiKey scheme (validated). (Since D32 / migration 100,secret_sourceis a fallback policy: every apiKey scheme is per-instance bindable viaservice_instances.credentials[scheme], and the at-most-one-instance-scheme validation is gone.) The gateway scheme is markedx-overslash-optional: true: it is injected only when the org has stored the secret, so a self-hosted overfwd withOVERFWD_REQUIRE_API_KEY=falseneeds no gateway key (theAuthorizationheader is simply omitted rather than failing on a missing secret). A missing instance-source credential instead falls through toneeds_authentication— a partial (gateway-only) injection is never sent. - Per-instance
urloverride promoted to HTTP Mode C — the URL piece of the deferred Core-change #3, pulled forward. The HTTP resolver now prefersservice_instances.url(verbatim base, scheme + port preserved) over the template host, mirroring the MCP fork. This is how an org points its catalog entry at its own overfwd deployment. A derivedconfigurable_urlflag on the templates API drives the dashboard to reveal a "Gateway URL" field.
The rest of Core-change #3 (the generic per-instance config jsonb) is built. The
original plan held that host/port needed no per-instance surface because gateway-side
autoconfig covered the common providers; that understated the gap.
overfwd resolves the mailbox endpoint from X-Mailbox-Imap / X-Mailbox-Smtp headers, and
falls back to autoconfig (Mozilla ISPDB + RFC 6186 DNS SRV) only when they are absent. The
shipped template sent neither, so any mailbox whose domain publishes no autoconfig record was
unreachable — every self-hosted and corporate Dovecot/Cyrus deployment, i.e. a large part of
the "standard-IMAP long tail" this integration exists to serve. A login that is not an email
address has no domain to look up at all. The interim answer (fork the template per deployment)
meant issuing a capability grant to change a hostname.
services/email.yaml now declares both endpoints as optional header params marked
x-overslash-instance-config: true, so an org pins them on the instance next to the Gateway
URL, and overfwd's autoconfig remains the default path for public providers. Storage is
service_instances.config — a column that holds literals only, deliberately distinct from
credentials, which holds vault references only (D32/D33).
Note for anyone pinning these by hand: overfwd wants both headers or neither — a request
with only X-Mailbox-Imap is rejected — and it infers transport from the port number
(993/3993 and 465/3465 are implicit TLS, everything else is plaintext; there is no STARTTLS
path).
Coverage: search/get (read, auto-approvable) + send (write, gated, discloses
To/From/Subject). End-to-end test: crates/overslash-api/tests/email_overfwd.rs (API contract,
in-process mock gateway) and dashboard/tests/e2e/flows/email-configure-and-try-it.spec.ts
(full user story through the dashboard, against a real overfwd talking real IMAP to a GreenMail
container — see make mail-up).
Decision 12 below ("Cloud hosting = Cloud Run, one shared stateless service") is
built: infra/modules/cloud-run-overfwd/ serves mailbox.overslash.com, the
hostname services/email.yaml has shipped as servers[0] since day one and
which until now did not resolve. Overfolder had built a per-org deployment
(#528) and backed it out (#530) on exactly decision 7's reasoning: the
gateway is a deployment topology, and a stateless one protects nothing extra
when copied per tenant.
The one thing a shared deployment forced that a per-org one did not: the
gateway runs with OVERFWD_REQUIRE_API_KEY=true, and decision 9's "single static
Overslash-identity bearer" has nowhere to live. The gateway scheme is
secret_source: org, so the key would have to sit in every org's vault — the
same platform key, N times, unrotatable in practice. The credential cascade
therefore gains one rung below the org vault, host-pinned to the platform
deployment, so an org that stores its own key or points its instances at its own
overfwd is completely unaffected. Details and the SSRF posture: D39, plus
docs/runbooks/mailbox-gateway.md.
Original design (settled grilling session 2026-07-08/09):
Direct email integration for Overslash: let agents send and read/search mail across arbitrary providers, without adding a non-HTTP execution path to the core. The provider-facing work lives in overfwd, a separate MIT-licensed OSS Mailbox Gateway; Overslash consumes it as an ordinary HTTP service.
Mailbox Gateway: The role — a service that presents a REST facade over a remote IMAP/SMTP mailbox, translating REST calls into IMAP/SMTP against the end user's provider. In the Inline mode Overslash uses, it holds no credentials and no mail at rest. Avoid: "JMAP proxy" (retired — JMAP was dropped as an internal protocol), "mail relay", "SMTP relay" (it does more than relay; it reads too).
overfwd:
The concrete OSS implementation of the Mailbox Gateway — a standalone, MIT-licensed Rust
project (repo overspiral/overfwd) with its own mandate to disrupt EmailEngine / Nylas /
Unipile. Overslash is just one consumer.
In scope (this effort):
- (a) Send from a user's mailbox.
- (b) On-demand read / search of a mailbox (request/response).
- Coverage: the standard-IMAP long tail — providers reachable with a presented credential (Migadu, Fastmail, iCloud, Zoho, Yahoo, Proton Bridge, corporate Dovecot/Cyrus, …).
Explicitly deferred (deliberate no, not omission):
- (c) Agent's own inbox — programmatic inbox creation.
- (d) Real-time inbound — "wake the agent on new mail." Requires an inbound-event ingestion subsystem Overslash does not have today (its only inbound seam is outbound webhook dispatch). Materially larger build; independent of the decisions here.
- Big-two native APIs — Gmail already ships (
services/gmail.yaml, REST/OAuth); Microsoft Graph (msgraph.yaml) is a separate later track. The Mailbox Gateway explicitly does not cover Gmail/Outlook: Google/Microsoft now require OAuth XOAUTH2 for IMAP, so routing them through the gateway buys nothing over their REST APIs and loses fidelity (labels, threads, search operators). This carve-out is about those two, not about consumer scale — Yahoo is in scope for the gateway, not a later native track. It is the mirror image of the big two: it publishes no HTTP Mail API at all (the oldmail.yahooapis.comJSON-RPC is retired, andmail-r/mail-wOAuth is gated behind a Commercial Access Agreement), and it still accepts an app password over standard IMAP/SMTP — so there is no REST fidelity to lose and the credential the gateway needs is obtainable. See D37.
- Scope = send + on-demand read, universal via gateway. Own-inbox and real-time inbound deferred.
- Coverage boundary. Big-two on native REST (Gmail shipped, Graph later); gateway owns standard-IMAP long tail only, explicitly not Gmail/Outlook.
- Contract = REST facade. Overslash speaks a REST surface we define
(
POST /email/search,/email/get,/email/send) — not raw JMAP. This keeps the existing OpenAPI-per-action model, per-actionrisk/disclose/permission-keys, and needs no YAML-schema extension on the email critical path. JMAP is not used, even internally. - Implementation = from-scratch thin Rust gateway, no JMAP. Crates:
async-imap,lettre(SMTP),mail-parser+mail-builder(Stalwart, RFC-conformant, zero-copy). The a+b surface is small (IMAPLOGIN/SELECT/SEARCH/FETCH/APPEND+ SMTP submit). - Delivered as a separate open-source project, not an Overslash-internal module. It must stand alone as a product, with a mandate to disrupt EmailEngine / Nylas / Unipile.
- State model. Zero-persistence by default (Inline mode); an optional
credential store backs the Portfolio/Session modes. Ephemeral in-memory connection
pooling (short TTL, per-credential, bounded, LRU, lost on restart) amortizes IMAP
LOGIN/SELECTcost regardless of mode. - Multi-tenant, shared deployment. One shared gateway (horizontally scaled); tenancy is a deployment topology, not a code difference (per-instance available as an option).
- Credential & auth model — two independent axes:
- Gateway access =
api_key(Authorization: Bearer). Server configrequire_api_key(on for Cloud, optional self-host). Scoped api_keys see only their N accounts. - Mailbox credential, one of three sources:
- Inline — creds-only secret (
user:pass) presented per request asX-Mailbox-Auth: Basic base64(user:pass); the mailbox host/port are non-secret (see decision 10). Zero gateway persistence. This is the only mode Overslash uses. - Portfolio — creds stored in the gateway's own encrypted store, referenced by
X-Mailbox-Account: <account_id>. Standalone-product only; disabled in Overslash Cloud. - Session = ephemeral Portfolio account —
create_sessionmints a TTL'd (ghost) account; thesession_tokenis a scoped handle. No creds at rest.
- Inline — creds-only secret (
Authorizationis reserved for the gateway api_key; the mailbox concern lives inX-Mailbox-*headers. SMTP + IMAP are both covered by one mailbox credential (commonly a shared user/app-password, e.g. Migadu).
- Gateway access =
- Wire layout (accepted).
Authorization: Bearer <api_key>= gateway access;X-Mailbox-Auth: Basic …(creds) +X-Mailbox-Imap/X-Mailbox-Smtp(host/port) — orX-Mailbox-Accountin Portfolio mode. On the Overslash→gateway hop the api_key is a single static Overslash-identity bearer (not per-tenant); Overslash's tenancy is carried per-request by the differing mailbox credential, so the gateway never sees Overslash tenants. - Single generic template, not per-provider. One template (working name
email/mailbox-gateway) instantiated by gateway URL + auser:passsecret name + non-secret host/port config. Nothing provider-specific in the template; the secret holds onlyuser:pass. Migadu/Fastmail/etc. turnkey variants are optional org/user template forks (existing template tiers + curated catalogs), created by an org admin if wanted. - v1 facade + risk. Actions:
search(read),get(read),send(write, gated — approval discloses To/From/Subject + clamped Body;Basicheader redacted).sendscopes every recipient header under one label (scope_param: [to:recipient, cc:recipient, bcc:recipient], DECISIONS D40), so each address mints anemail:send:recipient=<addr>key: a domain-scoped grant covers a correspondent wherever they appear, a bcc to an outsider gates the send like a to, and an address on two headers is one approval, not two. Reads are ordinaryread(auto-approvable); consent boundary is whether the owner grants read permission, not per-fetch approval. Attachments (get_attachment, binary +prefer_stream) andlist_foldersare later additions. - Cloud hosting = Cloud Run, one shared stateless service alongside the API (Portfolio store disabled in Cloud). Fly/GCE unjustified without per-tenant state. Standalone = the overfwd docker image. In-memory pools are best-effort per instance under Cloud Run's lifecycle; per-request login is the correct fallback.
- Scope-outs (this effort): the generic managed-backing-container abstraction
(WhatsApp/npx-MCP hosting) and the
x-overslash-fixed-paramsYAML extension are out of scope — each its own future track. Email needs neither. - OSS project: name overfwd, repo
overspiral/overfwd, MIT license, separate top-level repo (Overslash consumes it as a third party).
secret_injection already loops over a Vec<SecretRef> (headers/query, prefix-only — no
encode, no body injection); ServiceAuth (types/service.rs:171) declares a single
injection; and a service_instance carries only url + secret_name + connection_id
(no generic per-instance config, migrations 016/048/090). (Since superseded in part:
migration 100 / D32 adds service_instances.credentials — per-scheme secret NAME bindings,
deliberately not a generic config map.) Three bounded changes — but
only the first two are built now:
encode: base64option onSecretRef— emitBasic base64(user:pass). Reusable for any Basic-auth API.- Multi-injection — Cloud needs the gateway
api_keyand the creds on one request;ServiceAuthmust express two injections. Self-host (require_api_key=false) needs only #1. Multi-injection is Cloud-only. Per-instance non-secret param overrides— deferred. Interim: host/port come from prefilled forked templates (org/user fork of the genericemailtemplate with host/port baked asdefaultparams — existing template-tier + defaulting machinery, zero core change). A genericconfig jsonbper-instance override (superseding the one-offurlfield) is a tracked later enhancement, not built now.
Two overlapping requirement sets: what Overslash needs from the gateway, and what makes the gateway a credible standalone disruptor. The overlap (statelessness, credential-free operation) is the thesis.
- Speaks the REST facade Overslash targets:
search,get,sendto start, each a clean operation with stable request/response schemas. - Credential-free / no persistence (Inline). In the mode Overslash uses, the gateway holds no long-lived credentials and no mail at rest. Overslash's vault presents the backend mailbox credential per request; the gateway uses it and forgets it.
- Per-request auth over HTTP. The
user:passcredential arrives asX-Mailbox-Auth: Basic base64(user:pass), so it maps onto Overslash's existing header secret-injection (plus theencode: base64option — see Core changes). - Stateless horizontal scale. Any gateway instance can serve any request — no account-pinned state — so it fits both per-instance deployment and a shared pool.
- Provider target in the request. IMAP/SMTP host+port travel as non-secret
X-Mailbox-*headers (sourced from the template/forked-template config), so one gateway serves many providers. - Bounded, typed errors. Auth failure, host unreachable, mailbox/message not found, TLS failure — mapped to stable machine-readable codes Overslash can gate/approve on.
- Runs identically standalone and inside Overslash Cloud. No dependency on Overslash to boot.
Competitive weaknesses to beat:
- EmailEngine: self-hosted but stateful (Redis + its own encrypted store of each account's credentials), Node runtime, $995/yr commercial license, account-pinned.
- Nylas / Unipile: cloud-only; credentials and mail content flow through a third party; per-connected-account pricing; vendor lock-in; no real self-host story.
Differentiators the gateway commits to:
- Zero-persistence by default. No credential store, no message store in Inline mode. "Your secrets never touch our disk" is the headline — the sharpest wedge against EmailEngine (stores creds) and Nylas/Unipile (store everything).
- Bring-your-own-secrets. Credentials are presented per request from your vault; the gateway is a pure function of (request + credential). Trivially audited: nothing to leak.
- Single small binary, Rust. Low footprint, no Redis/DB required for the core path — vs Node + Redis.
- Permissive OSS license (MIT). Vs EmailEngine's paid license.
- Stateless horizontal scale — vs account-pinned competitors.
- Standard-IMAP-first, provider-agnostic. No per-provider SaaS integration tax for the long tail.
- Privacy→convenience ladder. Inline (stateless) → Session (ephemeral) → Portfolio (managed) meet developers wherever they sit on the curve, in one binary.
Resolved during this session (see decision 6): the per-request-login vs. pooling tension is settled as ephemeral in-memory pooling with a short TTL — ephemeral state, never persisted — so "zero-persistence" stays honest.
- Microsoft Graph (
msgraph.yaml) — separate REST track for Outlook/M365. - Own-inbox and real-time inbound (needs an inbound-event ingestion subsystem).
Generic— built 2026-07-18 (D33). Scoped to template-declared params rather than superseding the one-offconfig jsonbper-instance param overridesurlfield, which stays a typed column.- Generic managed-backing-container hosting pattern (WhatsApp/npx-MCP).
x-overslash-fixed-paramsextension (for third-party overgeneric APIs).- overfwd Portfolio/Session modes — standalone-product surfaces, disabled in Overslash Cloud; can land after the Inline path Overslash depends on.