feat(api): expose Reply-To as a first-class field on inbound payloads#81
Open
jiashuoz wants to merge 1 commit into
Open
feat(api): expose Reply-To as a first-class field on inbound payloads#81jiashuoz wants to merge 1 commit into
jiashuoz wants to merge 1 commit into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
reply_to: list[str]to the inbound webhook payload, REST list/detail responses, and the Python SDK'sInboundEmail/AsyncInboundEmail/MessageSummary. Empty list when the header is absent — never falls back silently tosender.messages.reply_to text[](migration005_reply_to.sql), populated server-side fromReply-To:using the samemail.ParseAddressListpath asto/ccso display names get stripped uniformly. RFC 5322 § 3.6.2 allows multiple addresses, hence the list shape.displaySender(first Reply-To wins) behavior preserved — the webhookfromfield is unchanged for backwards compatibility. The newreply_tofield is purely additive.Motivating case
Granola sends meeting-summary emails with
From: notifications@mail.granola.aiandReply-To: <real-user>. Today consumers re-parseraw_messagewith stdlibemail.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 behindverify_signature().DKIM-coverage decision
verify_signature()in this SDK is HMAC overbody_hash, not real DKIM verification. SinceReply-To:lives insideraw_message, tampering breaks the HMAC —reply_toenjoys the same end-to-end integrity guarantee asto/cc. Picked option (a) from the design question: keepreply_topopulated whenever present, noreply_to_signedflag. Rationale: a per-header DKIM-coverage check would require wiring data out ofinternal/emailauthinto 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 insdks/python/CHANGELOG.md. Callers doing high-stakes routing should also confirmis_verifiedand the sender domain.Consumer awareness
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.005_reply_to.sqlmust run before the upgraded binary on existing deployments.ADD COLUMN IF NOT EXISTSis idempotent. Existing inbound rows havereply_to IS NULLand coerce to[]stringon read.fromhoisting unchanged — when Reply-To is present,fromstill equalsReply-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— passesmake test-integration— passes against local Postgres on :5433 (coversinternal/identityandinternal/agenttests that touch the new column and the new arg onCreateInboundMessage)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, andunverified_payloadsurfacinguvx mypy sdks/python/src— clean on new code (9 pre-existing errors unrelated toreply_to)go build ./...+go vet ./...— cleanmake test-e2e— pre-existing build break onorigin/main(internal/e2e/e2e_test.go:71comparesp.Body.To []stringto a string literal). Unrelated to this branch; verified by stashing my changes and reproducing onc66f3a7.🤖 Generated with Claude Code