Skip to content

v1.3.0

Choose a tag to compare

@jiashuoz jiashuoz released this 25 Jul 03:40
72a475a

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. verified once
    inbound verification passes (needs both the ownership TXT and the inbound
    MX), pending otherwise. There is deliberately no inbound failure state:
    a missing, unreachable, or wrong MX leaves the axis pending indefinitely,
    so read pending as "not proven yet", not "in flight". Diagnose per-record
    via dns_records[].status or 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_total emission. 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 /test deliveries 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 serverghcr.io/tokencanopy/e2a-mcp-http:1.3.0, co-versioned
    with the server. Adds production hardening: /readyz with a 10s cached
    backend probe and Retry-After, /metrics Prometheus exposition, request
    correlation ids, quiet probe-route access logs.
  • CLI @e2a/cli 2.1.0 — adds e2a messages lifecycle <message-id> (beta)
    and e2a 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/sdk 5.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 to E2ANotFoundError.
  • e2a 5.4.0 on PyPI (python-v5.4.0) — unsubscribe= and wait="sent"
    on send/reply/forward, DomainCapabilities, and a fix so unsubscribe= 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