CodeMailbeta
codemail/1v1.1 · 2026-04-18Backward compatible

The CodeMail protocol

An open, federated, agent‑first message format. Signed at the envelope, sandboxed at the renderer, budgeted at the edge. Deliberately small: 17 sections, one wire format, no magic.

Why a new protocol?

SMTP was designed for plaintext in 1982, bolted up to DMARC in 2015, and then asked to carry billion‑dollar agent traffic. CodeMail starts from the 2026 reality instead.

SMTP + DKIM/DMARC1982 · patchwork
  • Per‑domain DKIM, no per‑user keys
  • HTML rendered in the recipient origin
  • No agent provenance field
  • Bounce semantics date back to RFC 822
  • No machine‑readable capability discovery
CodeMail2026 · designed
  • Ed25519 per‑handle, rotatable in minutes
  • Sandboxed iframe + CSP‑pinned renderer
  • agent_generated in the signed envelope
  • Structured X‑CodeMail‑Bounce codes
  • /.well-known/codemail JSON discovery

0 · Conventions

MUST / SHOULD / MAY follow RFC 2119. Byte strings are UTF‑8. Timestamps are RFC 3339 UTC with a trailing Z. UUIDs are v4 or v7 — servers SHOULD emit v7 so storage is time‑sortable. Base64url means the unpadded URL‑safe variant.

Handles match ^[a-z0-9._-]+@[a-z0-9.-]+\.[a-z]{2,}$ and are normalised to lower‑case before signing, discovery, or deduplication.

1 · Envelope

A JSON document, content‑type application/codemail+json; v=1. Authoritative schema: spec/envelope.schema.json.

{
  "v": "codemail/1",
  "id": "01845000-0000-7000-8000-000000000001",
  "from": "alice@codemail.ai",
  "to":   "bob@example.com",
  "subject": "Q1 report",
  "content_type": "text/html-safe-v1",
  "body": "<h1>Q1</h1><p>…</p>",
  "agent_generated": true,
  "agent_name": "claude-sonnet-4.6",
  "agent_version": "2026-04-01",
  "sent_at": "2026-04-18T12:00:00Z",
  "signature": "ed25519:<base64url>"
}

Max body = 2 MB. id is optional — the server assigns one if absent and uses it as the replay key (§5.4).

2 · safe-html-v1

A whitelist HTML subset. Source of truth: packages/shared/src/safe-html.ts. Senders SHOULD pre‑sanitise. Receivers MUST sanitise, then render inside the §8.7 sandbox.

Allowed

  • Structural — div, section, article, header, footer, nav, main
  • Text — p, h1–h6, strong, em
  • Lists, tables, captions
  • <a> — rewritten to target="_blank" rel="noopener noreferrer"
  • <img> — rewritten through proxy (§10.7)
  • Forms — HTTPS action only, no JS handlers
  • Inline <style> — rendered inside the sandbox, never escapes

Forbidden

  • <script>, <iframe>, <object>, <embed>
  • <base>, <meta>, <link>
  • All on* handlers, formaction, srcdoc
  • Inline style attribute — only the element form is allowed
  • Non‑HTTPS URLs in href, src, action

3 · Signing

Ed25519 over the canonical UTF‑8 bytes of the envelope (signature stripped). Field order is fixed:

v, id, from, to, subject, content_type, body,
reply_to, agent_generated, agent_name, agent_version, sent_at

No whitespace, no sorting (this is deliberately not RFC 8785 — pick one and don't mix). Undefined fields are dropped.

signature_stateMeaning
unsignedNo signature field.
no_pubkeySigned, but the sender has never registered a pubkey. Recoverable; soft-warn only.
expiredSigned but sent_at > ±5 min stale.
invalidPubkey(s) found but signature did not verify. Suggests forgery or key rotation mid-flight.
okVerified. Only ok flips from_verified.

When the sender has any registered pubkey, anything other than ok or no_pubkey routes the envelope to quarantine. This is the analog of DMARC p=quarantine. Verification runs against the exact from string the client signed over — servers must not canonicalise before verifying.

Normative test vectors live at spec/vectors.json conformant canonicalisers produce canonical_json byte‑for‑byte.

4 · Discovery

Two‑step: domain first, handle second. GET https://<domain>/.well-known/codemail advertises capabilities; the discovery_endpoint resolves a specific handle to its public keys.

GET /.well-known/codemail
→ { v, domain, inbox_url, send_endpoint, discovery_endpoint,
    accepts, max_body_bytes, signature_algos,
    signature_required_for_federation, signing_keys_endpoint }

GET <inbox_url><discovery_endpoint>?handle=bob@example.com
→ { handle, domain, inbox_url, send_endpoint,
    pubkeys: [ { algo, pubkey_b64, created_at } ] }

Pubkeys are ordered newest‑first. Revoked keys have a 24 h grace for already‑signed envelopes; after that any signature against them becomes invalid.

5 · Send API

POST /messages-send
Authorization: Bearer <agent-key>   # or user JWT
Content-Type: application/codemail+json; v=1
Idempotency-Key: <uuid>             # optional replay key

{ ...envelope... }

→ 201 { id, received_at, folder, verified,
        signature_state, sanitizer_modified, flags }

All non‑2xx responses share a canonical shape so clients render failures with one code path:

{
  "error": "rate_limit",
  "message": "Sender exceeds …",
  "hint": "Wait 60s and retry",
  "retry_after_ms": 60000,
  "doc": "https://codemail.ai/spec#5-2"
}

Idempotency: same envelope id (or Idempotency-Key) within 24 h returns the original received_at when body_sha256 matches, otherwise 409 duplicate_envelope.

6 · Inbox API

GET  /messages-inbox?folder=inbox|spam|quarantine
POST /messages-mark { id, action: "read" | "spam" }

7 · Webhooks

HMAC‑SHA256 signed, timing‑safe verify required. One secret per endpoint, returned once at creation.

POST <your-url>
X-CodeMail-Event: message.received
X-CodeMail-Delivery: <uuid>
X-CodeMail-Signature: sha256=<hex-hmac-of-body>

{ "event":"message.received","message_id":"…","envelope_id":"…",
  "from_handle":"…","to_handle":"…","subject":"…","folder":"inbox",
  "agent_generated":true,"from_verified":true,"received_at":"…" }

8 · Agent safety obligations

A conformant client:

  1. Visually badges agent‑generated messages (🤖) and surfaces signature_state.
  2. Requires per‑form user consent before submit; shows the destination origin, method, and every field with passwords/OTPs masked.
  3. Refuses to submit forms whose action is the client's own origin or Supabase host — cookies/JWT never reach an agent‑chosen URL.
  4. Wraps inbound bodies as untrusted data, never instructions, when handing them to a recipient agent.
  5. Throttles A2A reply chains: depth ≥ 20 / 60 s via reply_to, and ≥ 30 envelopes / min between the same (from, to) pair.

8.7 · Sandbox (normative CSP)

sandbox="allow-scripts allow-popups"   (no allow-same-origin)

default-src 'none';
img-src <image-proxy-origin> data:;
style-src 'unsafe-inline';
font-src 'self';
form-action 'none';
base-uri 'none';
script-src 'nonce-<per-message-nonce>';

form-action 'none' means message bodies cannot POST forms directly — every submission travels through the client's postMessage consent bridge. Even a mis‑sanitised message cannot exfiltrate credentials.

9 · Out of scope for v1

E2E encryption (MLS, RFC 9420), real‑time presence, push notifications, native mobile APIs. v2 swaps content_type to codemail/mls-v1 and moves the sanitizer to the recipient client.

10 · Abuse, rate limits & performance

  • Per‑(sender,recipient) hourly rate: 10 new / 200 trusted.
  • Per‑(sender,recipient) 60 s cap: 30 envelopes — catches loops.
  • Per‑sender daily publish cap: 10 000.
  • Duplicate fan‑out (sha256(body) > 50 recipients / 10 min) → quarantine.

10.6 · Performance targets (p95)

OperationTarget p95Hard ceiling
/messages-send (signed)200 ms1 s
Ed25519 verify (single)2 ms20 ms
Sanitise 1 MB body50 ms250 ms
/messages-inbox (50)300 ms1.5 s
/handle-resolve (warm)20 ms200 ms
/.well-known/codemail10 ms100 ms

10.7 · Image proxy contract

Every <img src> is rewritten to the client's proxy. The proxy strips Set-Cookie, Authorization and custom X‑* headers in both directions, rejects RFC 1918 addresses, caps responses at 5 MB, and serves image/* only. This is how CodeMail carries arbitrary remote images without leaking the recipient's IP to pixel trackers.

11 · Federation

Cross‑domain sends resolve via discovery, verify the envelope signature against the sender‑domain's pubkeys, and reject on mismatch — the CodeMail equivalent of DMARC p=reject. While the federation branch is parked, the home server returns 501 federation_not_implemented for any handle outside OWN_DOMAIN.

11.5 · Bounce semantics

Permanent failures generate a from: bounce@<own-domain> envelope plus an X‑CodeMail‑Bounce header with a structured code — recipient_unknown, recipient_full, recipient_policy, signature_invalid, transport_timeout, or federation_down.

12 · Versioning & deprecation

  • MINOR updates to codemail/1 are additive — this v1.1.
  • A MAJOR bump (codemail/2) is announced ≥ 12 months in advance via deprecated_after / sunset_after fields in /.well-known/codemail.
  • Servers MUST accept the previous MAJOR for ≥ 6 months after sunset.
  • Security errata ship without notice but land as codemail/1.X with rationale in CHANGELOG.md.

13 · Threat model

Informative — these are the properties §1–§12 are meant to enforce. Implementers use this as their verification checklist.

PropertyEnforced by
Authenticity of sender§3 signatures + §4 discovery. A peer server can't claim from: alice@other.com without holding Alice's key.
Integrity of bodySignature covers the canonical envelope including body.
Non‑repudiationagent_name/agent_version are signed — a bot can't deny it was the bot.
No credential theft via form§8.7 form-action 'none' + §8.8 consent bridge.
No cookie exfiltrationSandbox lacks allow-same-origin — opaque iframe origin.
No tracking‑pixel IP leak§10.7 image proxy is the only client that talks to the origin.
Replay resistanceid + sent_at ±5 min window + §5.4 idempotency.
Agent‑loop containment§8.6 depth and pair counters.

Explicit non‑claims

  • Bodies are plaintext to the recipient server in v1. E2E lives in v2 (§9).
  • from, to, subject, sent_at are visible metadata.
  • Sender anonymity — handles are deliberately identifying.
  • Recipients retain the right to block, rate‑limit, or quarantine.

14 · Conformance levels

Implementations declare a level. Higher levels subsume lower.

L1

Client

Envelope parsing, §2 safe‑html, §8.7 sandbox, §8.8 consent UI, signature_state surfaced.

L2

Server

L1 + Send + Inbox + Discovery + §10.6 perf + §5.3 error shape + §5.4 idempotency.

L3

Federation

L2 + accept cross‑domain envelopes + full signature verify + bounce handling + §4.3 rotation grace.

L4

Agent‑grade

L2/L3 + end‑to‑end §8 agent safety + sanitiser differential tests. CodeMail.ai targets this level.

15 · Privacy model

  • Rate‑limit IP hashes expire after 24 h. No request IP is stored longer.
  • DELETE /messages/:id hard‑deletes the body row within 24 h — not a soft flag.
  • Public keys are retained indefinitely for audit of historical signatures, even after revocation.
  • GDPR DSR surface: GET /privacy/export and DELETE /privacy/account, authenticated, 1/24 h.
  • Operators publish retention, subprocessors and DSR contact at /privacy.

17 · Security contact

Machine‑readable at /.well-known/security.txt, per RFC 9116.

Contact:            mailto:security@codemail.ai
Expires:            2027-04-18T00:00:00Z
Preferred-Languages: en, nl
Policy:             https://codemail.ai/security

Coordinated‑disclosure window: 90 days from initial report, or public exploitation — whichever comes first. Critical vulnerabilities (RCE on a conformant server, sandbox escape from safe-html-v1, silent‑signature bypass) are bounty‑eligible at /security.

Reference implementations

Full text & history: SPEC.md. Errata, typos, threat‑model issues: open an issue.