v1.3.0
Additive release — no breaking changes to the frozen /v1 contract, no config
changes required, one non-blocking migration.
API
Per-axis domain capabilities (#683). DomainView gains capabilities,
splitting the two independent axes that the legacy verified boolean and the
sending_status rollup expressed separately:
capabilities.inbound— can this domain receive mail.verifiedonce
inbound verification passes (needs both the ownership TXT and the inbound
MX),pendingotherwise. There is deliberately no inbound failure state:
a missing, unreachable, or wrong MX leaves the axispendingindefinitely,
so readpendingas "not proven yet", not "in flight". Diagnose per-record
viadns_records[].statusor the live probe on
POST /v1/domains/{domain}/verify.capabilities.outbound— can agents on this domain send as their own
address.none|pending|verified|failed, with detail in
sending_error.
Both are open string sets — tolerate unknown values. Existing verified and
sending_status fields are unchanged.
Security
Inbound protection now runs on the loopback self-send inbound leg (#681).
The inbound leg of a self-send previously bypassed all inbound protection —
performSelfSend and finalizeLocalDeliveryTx created the inbound row with a
zero-value screening, so an agent's inbound_gate_policy / allowlist and
inbound_scan sensitivity were silently ignored whenever an agent emailed
itself. All three loopback delivery paths now screen the composed MIME with
relay-identical outcome semantics: allow → delivered as before; flag →
delivered + annotated + email.flagged; review → hidden pending_review
hold (email.review_requested, email.received suppressed); block →
accept-then-quarantine as review_rejected. email.sent still always fires
for the delivered outbound leg.
The evaluation core was extracted verbatim from internal/relay into the new
leaf package internal/inboundscreen; the SMTP path is behavior-neutral.
Operator note: if you rely on agents emailing themselves and have a
non-default inbound_gate_policy or inbound_scan sensitivity, those self-sends
are now subject to it and may be held or blocked where they previously were not.
Agents on default protection see no change.
Webhook delivery reliability
- Rescue dead-job strands; guard the delivery TTL janitor (#679). Pending
rows whose stamped River job went terminal or was pruned are now reconciled
instead of stranding. - Retry transient webhook-lookup errors (#682) rather than mislabeling them
as a deleted webhook. - Terminal-write rows whose load fails on the final attempt (#684), so a
final-attempt load failure records a terminal outcome instead of leaving the
row pending. - Keep infrastructure detail out of the customer-facing
last_error.
Observability
SLO instrumentation for the three adopted targets (#676) — the instruments
docs/observability.md previously marked "target adopted; instrument pending":
e2a_outbound_terminal_latency_seconds— acceptance→terminal histogram,
observed exactly once per message, co-located with every
e2a_outbound_terminal_totalemission. Long buckets cover the 72h retry tail.e2a_webhook_first_attempt_latency_seconds— event→first-attempt histogram,
observed only on a first-delivery row's first HTTP attempt. Retries, no-POST
outcomes, replay rows, and eventless/testdeliveries never observe.e2a_ws_handshake_rejected_total{reason}— pre-upgrade handshake rejections
(unauthorized,not_found,forbidden,upgrade_failed). The SLI excludes
the client-fault reasons from its denominator.
Migrations
080_wsd_pending_with_job_idx.sql — a partial index backing the webhook
delivery reconciler's dead-job rescue scan, which runs every minute and would
otherwise seq-scan the whole delivery table each tick.
Built with CREATE INDEX CONCURRENTLY under the runner's e2a:no-transaction
directive, so it takes no write lock on the table. Ops note: if the
concurrent build is interrupted, Postgres leaves an INVALID index of that name;
the IF NOT EXISTS guard then skips the rebuild on the next startup and marks
the migration applied over a broken index. That is a performance regression
only — stranded rows are still rescued. Verify with:
SELECT indisvalid FROM pg_index WHERE indexrelid = 'idx_wsd_pending_with_job'::regclass;and recover by dropping the index concurrently and re-running the statement.
Clients
Published from this tag:
- MCP HTTP server —
ghcr.io/tokencanopy/e2a-mcp-http:1.3.0, co-versioned
with the server. Adds production hardening:/readyzwith a 10s cached
backend probe andRetry-After,/metricsPrometheus exposition, request
correlation ids, quiet probe-route access logs. - CLI
@e2a/cli2.1.0 — addse2a messages lifecycle <message-id>(beta)
ande2a doctor, a read-only production email-path diagnostic (config,
credential scope, API connectivity, agent access, live domain DNS checks, MCP
reachability, webhook config and delivery history, outbound SMTP visibility).
It never sends mail and never mutates anything. Note that 2.0.0 was published
at v1.2.0, so this release carries everything CLI-side since then.
Published from their own tags alongside this release:
@e2a/sdk5.4.0 (ts-sdk-v5.4.0) —wait: "sent"on
send/reply/forward,agents.delete(email, { permanent: true }),
apiKeys.create(body, { idempotencyKey }),DomainCapabilities, and a fix
mapping HTTP 410 toE2ANotFoundError.e2a5.4.0 on PyPI (python-v5.4.0) —unsubscribe=andwait="sent"
on send/reply/forward,DomainCapabilities, and a fix sounsubscribe=no
longer mutates the caller's request model.
See each package's CHANGELOG.md for the full list.
Testing and CI
No runtime impact: per-package test databases with parallelized DB-backed runs
(#674, #675), web UI tests plus a web coverage gate (#677, #678), TS/Python SDK
and CLI coverage gates, and previously orphaned suites wired into CI.
Full changelog: v1.2.2...v1.3.0