Skip to content

feat(api): expose Reply-To as a first-class field on inbound payloads#81

Open
jiashuoz wants to merge 1 commit into
mainfrom
feat/reply-to-field
Open

feat(api): expose Reply-To as a first-class field on inbound payloads#81
jiashuoz wants to merge 1 commit into
mainfrom
feat/reply-to-field

Conversation

@jiashuoz
Copy link
Copy Markdown
Contributor

Summary

  • Adds reply_to: list[str] to the inbound webhook payload, REST list/detail responses, and the Python SDK's InboundEmail / AsyncInboundEmail / MessageSummary. Empty list when the header is absent — never falls back silently to sender.
  • New DB column messages.reply_to text[] (migration 005_reply_to.sql), populated server-side from Reply-To: using the same mail.ParseAddressList path as to/cc so display names get stripped uniformly. RFC 5322 § 3.6.2 allows multiple addresses, hence the list shape.
  • Existing displaySender (first Reply-To wins) behavior preserved — the webhook from field is unchanged for backwards compatibility. The new reply_to field is purely additive.

Motivating case

Granola sends meeting-summary emails with From: notifications@mail.granola.ai and Reply-To: <real-user>. Today consumers re-parse raw_message with stdlib email.message_from_bytes() to recover the intended correspondent — slow and bypasses e2a's signature trust path. With this change the field is structured and gated behind verify_signature().

DKIM-coverage decision

verify_signature() in this SDK is HMAC over body_hash, not real DKIM verification. Since Reply-To: lives inside raw_message, tampering breaks the HMAC — reply_to enjoys the same end-to-end integrity guarantee as to/cc. Picked option (a) from the design question: keep reply_to populated whenever present, no reply_to_signed flag. Rationale: a per-header DKIM-coverage check would require wiring data out of internal/emailauth into the SDK, which crosses the "no Authentication-Results / SPF / DMARC surface changes" line called out as out-of-scope. The trust limitation (upstream MITM can rewrite Reply-To if the original sender's DKIM doesn't cover it) is documented on the property docstring and in sdks/python/CHANGELOG.md. Callers doing high-stakes routing should also confirm is_verified and the sender domain.

Consumer awareness

  • Webhook payload schema — new optional reply_to: string[] key. Old SDKs that ignore unknown keys are unaffected; new SDKs talking to an old server simply see []. Additive in both directions.
  • DB migration005_reply_to.sql must run before the upgraded binary on existing deployments. ADD COLUMN IF NOT EXISTS is idempotent. Existing inbound rows have reply_to IS NULL and coerce to []string on read.
  • from hoisting unchanged — when Reply-To is present, from still equals Reply-To[0]. A separate change can revisit that conflation; this PR is scoped to adding the new field, not breaking the old one.

Test plan

  • make test-unit — passes
  • make test-integration — passes against local Postgres on :5433 (covers internal/identity and internal/agent tests that touch the new column and the new arg on CreateInboundMessage)
  • Python pytest sdks/python/tests/ — 178 passed, 0 failed. Adds 7 new tests covering: absent Reply-To → empty list, single, multi-address, display-name-stripped (server-normalized), gated-until-verified, HMAC-trust path, and unverified_payload surfacing
  • uvx mypy sdks/python/src — clean on new code (9 pre-existing errors unrelated to reply_to)
  • go build ./... + go vet ./... — clean
  • make test-e2e — pre-existing build break on origin/main (internal/e2e/e2e_test.go:71 compares p.Body.To []string to a string literal). Unrelated to this branch; verified by stashing my changes and reproducing on c66f3a7.

🤖 Generated with Claude Code

Surface the parsed Reply-To: header as reply_to (list[str]) on the
webhook payload, the REST list/detail responses, and the Python SDK's
InboundEmail / AsyncInboundEmail / MessageSummary. Empty list when the
header is absent — callers decide whether to fall back to sender. Trust
path is e2a's HMAC over raw_message body_hash, same as to/cc; upstream
DKIM coverage of Reply-To is not separately surfaced (documented).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant