Skip to content

fix(email): honor Reply-To on Reply and Reply-All - #5588

Closed
cameronapak wants to merge 2 commits into
macro-inc:mainfrom
cameronapak:fix/5586-honor-reply-to
Closed

fix(email): honor Reply-To on Reply and Reply-All#5588
cameronapak wants to merge 2 commits into
macro-inc:mainfrom
cameronapak:fix/5586-honor-reply-to

Conversation

@cameronapak

@cameronapak cameronapak commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Warning

PR created through AI bot. Take the meat. Spit out the bones. It won't hurt my feelings if this needs changes!

Summary

  • Honor usable Reply-To when composing Reply and Reply-All so replies go to the intended address instead of an automated From.
  • Reply: To = Reply-To address(es); Cc empty; From omitted from To. Missing/unusable Reply-To falls back to From.
  • Reply-All: To = Reply-To address(es); Cc = parent To + Cc minus the user and addresses already in To; From not added to To.
  • Multi-value Reply-To puts every parsed address in To.
  • Existing drafts keep saved To/Cc/Bcc (unchanged call-site behavior in createEmailFormState).

Approach

Interim headers_json path (not first-class API field yet):

  • Confirmed inbound ApiMessage already exposes headers_json with Gmail-style { name, value } entries, and backend search already extracts Reply-To via extract_reply_to.
  • Composer ApiMessage has no first-class reply_to: ContactInfo[] yet; regenerating OpenAPI would require a Rust openapi binary build, so this PR uses the allowed interim path.
  • Helpers prefer a future first-class reply_to field when present, then fall back to parsing headers_json.

Main seam: getReplyRecipientsFromParent / getReplyAllRecipients in recipientConversion.ts.

Closes #5586

Test plan

  • cd apps/web && bunx vitest run src/features/block-email/util/recipientConversion.test.ts --project=block-email (7 passed)
  • cd apps/web && bunx vitest run --project=block-email (40 passed)
  • cd apps/web && bun run type-check (clean)
  • Manual: open a message with distinct From vs Reply-To, click Reply / Reply-All, confirm To/Cc match the acceptance criteria
  • Manual: open an existing draft reply and confirm saved recipients are not rewritten

Use usable Reply-To addresses when composing Reply/Reply-All so replies
reach the intended recipient instead of automated From addresses.
Falls back to From when Reply-To is missing or unusable. Interim path
reads headers_json; prefers a future first-class reply_to field.

Closes macro-inc#5586
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Reply actions now recognize valid Reply-To addresses, including those provided in message headers.
    • Reply-All better preserves intended recipients while removing duplicates and excluding the current user.
    • Replies fall back to the sender when no usable Reply-To address is available.
  • Tests

    • Added coverage for Reply-To handling, fallbacks, Reply-All recipients, duplicate prevention, and empty Bcc results.

Walkthrough

Updated recipient conversion to parse Reply-To addresses from first-class data or headers_json. Single replies now prefer usable Reply-To contacts and fall back to the sender. Reply-all places Reply-To contacts in To, excludes the current user, and deduplicates remaining To and Cc recipients. Added Vitest coverage for valid, invalid, multiple, fallback, and empty recipient cases.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The implementation honors usable Reply-To addresses and falls back to From when Reply-To is missing or unusable, meeting issue #5586.
Out of Scope Changes check ✅ Passed The code and tests focus on Reply-To recipient conversion and directly support the objectives in issue #5586.
Title check ✅ Passed The title uses the conventional commits format, describes the Reply-To change, and is under 72 characters.
Description check ✅ Passed The description clearly explains the Reply and Reply-All Reply-To behavior, fallback rules, tests, and draft preservation.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/features/block-email/util/recipientConversion.ts`:
- Around line 121-126: Update recipientConversion.ts lines 121-126 to filter
withReplyTo.reply_to and return the filtered contacts only when non-empty;
otherwise call replyToFromHeadersJson(message.headers_json). In
recipientConversion.test.ts lines 82-93, add coverage for invalid first-class
reply_to entries with a valid Reply-To header and assert the header address is
returned.
- Around line 149-168: In the Reply-All branch around getUsableReplyToContacts,
filter userEmail out of the Reply-To contacts before building both to and the
toEmails exclusion set, while preserving existing deduplication and ordering.
Update apps/web/src/features/block-email/util/recipientConversion.ts lines
149-168 accordingly; in
apps/web/src/features/block-email/util/recipientConversion.test.ts lines
115-132, include USER in replyToHeader and assert it is absent from to.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 79c5c5e8-35e1-4649-8326-2d7c48a74bae

📥 Commits

Reviewing files that changed from the base of the PR and between d69a9c7 and 26cad32.

📒 Files selected for processing (2)
  • apps/web/src/features/block-email/util/recipientConversion.test.ts
  • apps/web/src/features/block-email/util/recipientConversion.ts

Comment thread apps/web/src/features/block-email/util/recipientConversion.ts
Comment thread apps/web/src/features/block-email/util/recipientConversion.ts Outdated

@cameronapak cameronapak left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Standards

praise: Small seam — only recipientConversion.ts + tests. Conventional commit / branch naming look right; no any.

suggestion (non-blocking, FE-16 / Divergent Change): parseAddressList + headers_json walking grew this conversion module with a second reason-to-change (RFC address-list parsing). Consider extracting header/address parsing to a sibling util when this interim path sticks around.

thought (non-blocking, Speculative Generality): ApiMessageWithReplyTo prefers a future first-class reply_to before headers_json — intentional interim per the lock, still a speculative surface until OpenAPI catches up.

thought (non-blocking): Mild duplication in Reply-All filters (user / To-overlap) next to the existing From path.

Hard documented-standard violations: none.

Spec

Spec sources: #5586 + locked comment (user stories / decisions) + agreed helper seam.

praise: Locked v1 acceptance looks met — Reply / Reply-All / multi-value Reply-To / unusable fallback / draft non-rewrite (createEmailFormState draft branch untouched) / no new UI / List-Post·Sender·outbound left alone / tests at getReplyRecipientsFromParent + getReplyAllRecipients.

note (non-blocking): No unit case exercises the first-class reply_to preference path (factory supports it; not in the locked required list).

Missing/wrong requirements: none material.

Summary: Standards 0 hard / judgement nits (worst: parser living in conversion util). Spec 0 open findings.

Note: COMMENT instead of APPROVE only because GitHub blocks approving your own PR as cameronapak. Spec + Standards look good to merge from this bot's perspective.

Code Reviewer bot, sent on behalf of Cam.

@@ -103,13 +219,22 @@ export const getReplyRecipientsFromParent = (
// If last message was from user, reply === replyAll
if (replyingTo?.from?.email === userEmail) {
return getReplyAllRecipients(replyingTo, userEmail);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Reply delegates into the Reply-All helper when appropriate — keeps one policy path.

Locked stories for Reply / Reply-All / draft non-rewrite stay at this seam with tests alongside. Good match to the grill lock.

Code Reviewer bot, sent on behalf of Cam.

* Parse a Reply-To / address-list header value into contacts.
* Supports `email`, `Name <email>`, and comma-separated lists.
*/
const parseAddressList = (raw: string): ContactInfo[] => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): Address-list parsing is a separate reason-to-change from recipient policy.

Fine as the interim headers_json path, but if this sticks, a sibling util (or shared email-header helper) would keep recipientConversion single-purpose (FE-16 / STYLE_GUIDE).

Code Reviewer bot, sent on behalf of Cam.

};

/** ApiMessage may gain a first-class reply_to field; prefer it when present. */
type ApiMessageWithReplyTo = ApiMessage & {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought (non-blocking): Preferring future reply_to is the right interim shape.

Just noting there's no test that hits this branch yet — not required by the lock, but cheap insurance when the OpenAPI field lands.

Code Reviewer bot, sent on behalf of Cam.

Fall back to headers_json when first-class reply_to is unusable, exclude
the current user from Reply-All Reply-To recipients, extract address-list
parsing to a sibling util, and cover the first-class preference path.
@cameronapak

Copy link
Copy Markdown
Contributor Author

Addressed the open review feedback in 39ad84d:

  • CodeRabbit (major): fall back to headers_json Reply-To when first-class reply_to is present but unusable
  • CodeRabbit (major): exclude the current user from Reply-All Reply-To to recipients
  • Nits: extracted address-list parsing to parseAddressList.ts (FE-16), added first-class reply_to preference + unusable-fallback tests, tightened Reply-All exclude/dedupe helpers

recipientConversion.test.ts: 9/9 passing.

@cameronapak

Copy link
Copy Markdown
Contributor Author

Ready for review from a Macro maintainer

@peterchinman

Copy link
Copy Markdown
Contributor

Nice! This looks like it would work, but we'd want to do this parsing server-side in our Rust instead of client-side.

Closing this and creating an internal task to implement this fix server-side.

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.

Reply composer ignores the Reply-To email header

2 participants