- 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
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.
- Ed25519 per‑handle, rotatable in minutes
- Sandboxed iframe + CSP‑pinned renderer
agent_generatedin the signed envelope- Structured
X‑CodeMail‑Bouncecodes /.well-known/codemailJSON 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 totarget="_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
styleattribute — 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_state | Meaning |
|---|---|
unsigned | No signature field. |
no_pubkey | Signed, but the sender has never registered a pubkey. Recoverable; soft-warn only. |
expired | Signed but sent_at > ±5 min stale. |
invalid | Pubkey(s) found but signature did not verify. Suggests forgery or key rotation mid-flight. |
ok | Verified. 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:
- Visually badges agent‑generated messages (🤖) and surfaces
signature_state. - Requires per‑form user consent before submit; shows the destination origin, method, and every field with passwords/OTPs masked.
- Refuses to submit forms whose action is the client's own origin or Supabase host — cookies/JWT never reach an agent‑chosen URL.
- Wraps inbound bodies as untrusted data, never instructions, when handing them to a recipient agent.
- 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)
| Operation | Target p95 | Hard ceiling |
|---|---|---|
/messages-send (signed) | 200 ms | 1 s |
| Ed25519 verify (single) | 2 ms | 20 ms |
| Sanitise 1 MB body | 50 ms | 250 ms |
/messages-inbox (50) | 300 ms | 1.5 s |
/handle-resolve (warm) | 20 ms | 200 ms |
/.well-known/codemail | 10 ms | 100 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/1are additive — this v1.1. - A MAJOR bump (
codemail/2) is announced ≥ 12 months in advance viadeprecated_after/sunset_afterfields 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.Xwith rationale inCHANGELOG.md.
13 · Threat model
Informative — these are the properties §1–§12 are meant to enforce. Implementers use this as their verification checklist.
| Property | Enforced 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 body | Signature covers the canonical envelope including body. |
| Non‑repudiation | agent_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 exfiltration | Sandbox 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 resistance | id + 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_atare 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.
Client
Envelope parsing, §2 safe‑html, §8.7 sandbox, §8.8 consent UI, signature_state surfaced.
Server
L1 + Send + Inbox + Discovery + §10.6 perf + §5.3 error shape + §5.4 idempotency.
Federation
L2 + accept cross‑domain envelopes + full signature verify + bounce handling + §4.3 rotation grace.
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/:idhard‑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/exportandDELETE /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
@codemail/client— JS SDK (Node + browser, auto‑federation)@codemail/mcp-server— MCP server for Claude/GPT/Geminispec/vectors.json— canonical‑form test vectors; verify withnpx tsx scripts/spec-vectors.ts
Full text & history: SPEC.md. Errata, typos, threat‑model issues: open an issue.