Skip to content

Fix Reply-To problem#97

Open
ryush00 wants to merge 6 commits intomailtrap:mainfrom
MineList:main
Open

Fix Reply-To problem#97
ryush00 wants to merge 6 commits intomailtrap:mainfrom
MineList:main

Conversation

@ryush00
Copy link

@ryush00 ryush00 commented Mar 8, 2026

Motivation

Some users sending to Yahoo reported bounces like:

Full Response: 554 Message not accepted. Email address contains exotic or forbidden characters. See https://senders.yahooinc.com/smtp-error-codes/#other-policy-errors
Bounce Category: Bad recipient

In our ActionMailer conversion path, Reply-To could remain in headers instead of being represented as structured reply_to.
This risks inconsistent header serialization/parsing for edge cases (including folded/encoded header forms).

image

What changed

1) Map Reply-To into structured payload field

  • Mailtrap::Mail.from_message now parses message['reply-to'] and sets:
    • reply_to: prepare_addresses(message['reply-to']).first
  • This aligns Reply-To handling with other structured address fields at payload build time.

2) Remove Reply-To from custom headers

  • Added replyto to the headers-removal list (SPECIAL_HEADERSHEADERS_TO_REMOVE path),
    so Reply-To does not remain duplicated in headers.

3) Test coverage improvements

Updated spec/mailtrap/mail_spec.rb to verify:

  • Structured reply_to mapping
  • Reply-To header removal (including case variants: Reply-To, REPLY-TO, reply-to)
  • Invalid Reply-To error behavior

Why this is safe

  • Existing behavior for from/to/cc/bcc/subject/text/html/attachments is unchanged.
  • reply_to type remains compatible with current Base payload shape (single object).
  • Header de-duplication reduces conflicting representations of the same semantic field.
  • We have deployed it to our stage environment and tested to send Mailtrap .

How to test

bundle exec rspec spec/mailtrap/mail_spec.rb
bundle exec rspec spec/mailtrap/action_mailer/delivery_method_spec.rb
bundle exec rubocop spec/mailtrap/mail_spec.rb

Notes for reviewers

  • This PR intentionally keeps reply_to as a single structured object (first parsed address), matching current gem payload schema.
  • Multi-address Reply-To policy can be discussed separately if needed

Summary by CodeRabbit

  • New Features

    • Reply-to is now recognized, validated, and sent as structured reply-to data in outgoing emails (removed from raw headers).
  • Tests

    • Added comprehensive tests for multiple reply-to formats, header-variant handling, validation errors, and correct mapping to the structured reply-to field.

ryush00 added 4 commits March 8, 2026 00:58
…bug-in-mailtrap-gem

Map Reply-To into structured field and strip Reply-To header from payload
…bug-in-mailtrap-gem-itmsm1

test: cover folded line-break reply-to parsing error
@coderabbitai
Copy link

coderabbitai bot commented Mar 8, 2026

📝 Walkthrough

Walkthrough

Adds Reply-To handling: includes 'replyto' in Mailtrap::Mail::SPECIAL_HEADERS and maps message['reply-to'] into the mail object's structured reply_to field; tests and a VCR fixture updated so reply_to is sent in JSON payload rather than as a raw header.

Changes

Cohort / File(s) Summary
Core Implementation
lib/mailtrap/mail.rb
Added 'replyto' to SPECIAL_HEADERS and map message['reply-to'] to the Base object's reply_to (first parsed address), matching existing from/to/cc/bcc behavior.
Tests
spec/mailtrap/mail_spec.rb
Updated/added specs to assert Reply-To is parsed into structured reply_to, removed from raw headers, handles case variants and folded lines, and raises on invalid Reply-To.
VCR Fixture / API Payload
spec/fixtures/vcr_cassettes/.../converts_the_message_and_sends_via_API.yml
Updated recorded request body to include reply_to as a structured JSON object and removed the Reply-To header from request headers.

Sequence Diagram(s)

sequenceDiagram
    participant Sender as Client (Message)
    participant Mail as Mailtrap::Mail (Base)
    participant API as Mailtrap API

    Sender->>Mail: provide headers (including Reply-To)
    Mail->>Mail: parse headers (from,to,cc,bcc, reply-to)
    Note right of Mail: `'replyto'` in SPECIAL_HEADERS\nreply_to set to first parsed address
    Mail->>API: POST /send with JSON body including reply_to: { email, name }
    API-->>Mail: 200 OK
    Mail-->>Sender: delivery result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 A little header, whisked and true,
I nibbled space and stitched it through.
From message burrow to API light,
Reply-to hops on, clean and right. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Fix Reply-To problem' is vague and does not clearly convey the specific change, using generic phrasing that lacks meaningful detail about the fix. Consider a more descriptive title such as 'Map Reply-To header to structured payload field' or 'Fix Reply-To handling by removing duplicate headers and using structured field' to better communicate the specific change.
✅ Passed checks (2 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The pull request description comprehensively covers motivation, changes, testing instructions, and safety considerations, aligning well with the required template structure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

@ryush00 ryush00 marked this pull request as ready for review March 8, 2026 05:09
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
spec/mailtrap/mail_spec.rb (1)

186-197: Duplicate test contexts for invalid reply-to.

The test at lines 186-197 and lines 212-224 both test the same scenario: an invalid 'Reply-To': 'invalid email@example.com' raising Mailtrap::Error with the same message. Consider consolidating these into a single context block with the folded line break case nested within it.

♻️ Suggested consolidation

Remove lines 186-197 and keep only the block at lines 212-241, which already covers both the basic invalid case and the folded line break edge case.

-    context "when 'reply-to' is invalid" do
-      before do
-        message.header['Reply-To'] = 'invalid email@example.com'
-      end
-
-      it 'raises an error' do
-        expect { mail }.to raise_error(
-          Mailtrap::Error,
-          "failed to parse 'Reply-To': 'invalid email@example.com'"
-        )
-      end
-    end
-
     %i[from to cc bcc].each do |header|

Also applies to: 212-224

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/mailtrap/mail_spec.rb` around lines 186 - 197, There are duplicate RSpec
contexts named "when 'reply-to' is invalid" testing the same invalid Reply-To
value; remove the earlier duplicate context (the one that sets
message.header['Reply-To'] = 'invalid email@example.com' and its expect) and
keep the later context that already covers both the basic invalid case and the
folded line-break edge case (the nested case that manipulates
message.header['Reply-To'] and expects Mailtrap::Error with the failure
message); alternatively, consolidate by merging the simple invalid-value example
into the later context as a nested example so only one context named "when
'reply-to' is invalid" remains and both assertions (simple invalid and folded
line break) live under it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@spec/mailtrap/mail_spec.rb`:
- Around line 186-197: There are duplicate RSpec contexts named "when 'reply-to'
is invalid" testing the same invalid Reply-To value; remove the earlier
duplicate context (the one that sets message.header['Reply-To'] = 'invalid
email@example.com' and its expect) and keep the later context that already
covers both the basic invalid case and the folded line-break edge case (the
nested case that manipulates message.header['Reply-To'] and expects
Mailtrap::Error with the failure message); alternatively, consolidate by merging
the simple invalid-value example into the later context as a nested example so
only one context named "when 'reply-to' is invalid" remains and both assertions
(simple invalid and folded line break) live under it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 274dddc9-cd03-4644-9a18-84c358f9ebd6

📥 Commits

Reviewing files that changed from the base of the PR and between af396c8 and 7ff1068.

📒 Files selected for processing (3)
  • lib/mailtrap/mail.rb
  • spec/fixtures/vcr_cassettes/Mailtrap_ActionMailer_DeliveryMethod/_deliver_/converts_the_message_and_sends_via_API.yml
  • spec/mailtrap/mail_spec.rb

@i7an i7an self-requested a review March 9, 2026 16:01
@i7an
Copy link
Contributor

i7an commented Mar 9, 2026

@ryush00 I see the problem. Will have to test the changes. Thanks.

body:
encoding: UTF-8
string: '{"from":{"email":"mailtrap@mailtrap.io","name":"Mailtrap Test"},"to":[{"email":"to_1@railsware.com","name":"To 1"},{"email":"to_2@railsware.com"}],"cc":[{"email":"cc_1@railsware.com"},{"email":"cc_2@railsware.com","name":"Cc 2"}],"bcc":[{"email":"bcc_1@railsware.com"},{"email":"bcc_2@railsware.com"}],"subject":"You are awesome!","text":"Some text","html":"<div>HTML part</div>","attachments":[{"content":"VGhpcyBpcyBhIHRleHQgZmlsZQo=","type":"text/plain","filename":"file.txt","disposition":"attachment","content_id":"txt_content_id@test.mail"},{"content":"iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABiElEQVRIid3VSW5UMRCA4U8d0gIx7AjhAghBTgFICAkxRFHukDDDIRiuwI7xPNAMISQRV4CABFmkWbie2nrt9muaFZTkxavhr7Jfucz/Ln2s4hU28B0/sBO6VczPCl/GNoYd6wsuZ3ELWKuBe3iSAQa4g7M4jEM4jRt4g5+4lMEHEbc+KUED/xVOvY5iThXgg/gek+UMfq4CbstU8L7RmU/c3qxwUkc0TnN/CT9Ycn4djrenhB/H24j5iMXQX8TTUsCncD6T6daUtzyp8hNSV30uJfgWAUfje70AqMFF7FC6kGOy20rQPoKTBd1ii3EsbF9LCUpH1K62q1uWwr5RSvAyjHdb+rzqSZU38iB8npWMTZu+M96mzU5qfT6H98FYKTn0sRUONwv2hQqcNK+G2FSZsNeNRsX5CqwtF7CHfVzpcn6cJbmlfqsPSJXvRczDaarpZUmaf3JP6pAjsZZw3+jM9/FIffKOyTXpRnY9OJu4+ifgXOaljnghtedurA94HraZn8x/Q34DYaON8Fk9Z1IAAAAASUVORK5CYII=","type":"image/png","filename":"file.png","disposition":"inline","content_id":"png_content_id@test.mail"}],"headers":{"Reply-To":"reply-to@railsware.com","X-Special-Domain-Specific-Header":"SecretValue","One-more-custom-header":"CustomValue"},"category":"Module Test"}'
string: '{"from":{"email":"mailtrap@mailtrap.io","name":"Mailtrap Test"},"to":[{"email":"to_1@railsware.com","name":"To 1"},{"email":"to_2@railsware.com"}],"reply_to":{"email":"reply-to@railsware.com"},"cc":[{"email":"cc_1@railsware.com"},{"email":"cc_2@railsware.com","name":"Cc 2"}],"bcc":[{"email":"bcc_1@railsware.com"},{"email":"bcc_2@railsware.com"}],"subject":"You are awesome!","text":"Some text","html":"<div>HTML part</div>","attachments":[{"content":"VGhpcyBpcyBhIHRleHQgZmlsZQo=","type":"text/plain","filename":"file.txt","disposition":"attachment","content_id":"txt_content_id@test.mail"},{"content":"iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABiElEQVRIid3VSW5UMRCA4U8d0gIx7AjhAghBTgFICAkxRFHukDDDIRiuwI7xPNAMISQRV4CABFmkWbie2nrt9muaFZTkxavhr7Jfucz/Ln2s4hU28B0/sBO6VczPCl/GNoYd6wsuZ3ELWKuBe3iSAQa4g7M4jEM4jRt4g5+4lMEHEbc+KUED/xVOvY5iThXgg/gek+UMfq4CbstU8L7RmU/c3qxwUkc0TnN/CT9Ycn4djrenhB/H24j5iMXQX8TTUsCncD6T6daUtzyp8hNSV30uJfgWAUfje70AqMFF7FC6kGOy20rQPoKTBd1ii3EsbF9LCUpH1K62q1uWwr5RSvAyjHdb+rzqSZU38iB8npWMTZu+M96mzU5qfT6H98FYKTn0sRUONwv2hQqcNK+G2FSZsNeNRsX5CqwtF7CHfVzpcn6cJbmlfqsPSJXvRczDaarpZUmaf3JP6pAjsZZw3+jM9/FIffKOyTXpRnY9OJu4+ifgXOaljnghtedurA94HraZn8x/Q34DYaON8Fk9Z1IAAAAASUVORK5CYII=","type":"image/png","filename":"file.png","disposition":"inline","content_id":"png_content_id@test.mail"}],"headers":{"X-Special-Domain-Specific-Header":"SecretValue","One-more-custom-header":"CustomValue"},"category":"Module Test"}'
Copy link
Contributor

Choose a reason for hiding this comment

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

reply-to address was moved from headers to the body

to: prepare_addresses(message['to']),
cc: prepare_addresses(message['cc']),
bcc: prepare_addresses(message['bcc']),
reply_to: prepare_addresses(message['reply-to']).first,
Copy link
Contributor

Choose a reason for hiding this comment

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

Reply-to in the raw email.

Before:

 >=?UTF-8?q?"=EB=A7=88=EC=9D=B8=EB=A6=AC=EC=8A=A4=ED=8A=B8"_<support@mailtr?==?UTF-8?q?ap.io>?=

After:

=?utf-8?q?=EB=A7=88=EC=9D=B8=EB=A6=AC=EC=8A=A4=ED=8A=B8?=<support@mailtrap.io> 

@i7an i7an requested a review from mklocek March 9, 2026 16:45
its(:template_variables) { is_expected.to eq('first_name' => 'John') }
end

context "when 'reply-to' is invalid" do
Copy link
Contributor

@i7an i7an Mar 9, 2026

Choose a reason for hiding this comment

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

the tests should be refactored. I can do that once merged.

  • this should be merged with the next block.

end

context 'when address contains a folded line break' do
let(:invalid_reply_to) do
Copy link
Contributor

Choose a reason for hiding this comment

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

could you please explain this case?

Copy link
Author

Choose a reason for hiding this comment

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

I misunderstood RFC 2047. It doesn’t seem necessary here. Instead, after reading the part that says the address part must not be RFC 2047-encoded, I’ll remove this test and update it to strengthen coverage around that rule.

@ryush00
Copy link
Author

ryush00 commented Mar 10, 2026

I initially misunderstood the issue and assumed the folded / domain-splitting encoding was being generated in this gem.
After tracing the flow again, I realized the old behavior here was simply passing Reply-To through the custom headers
path.

Because of that, I removed the folding test I had added earlier.

If Reply-To was becoming malformed while going through the headers path, that seems more like a backend-side issue than
something this gem itself was generating. Since that is outside the scope of this gem change, I left that case out of
the tests here.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
spec/mailtrap/mail_spec.rb (2)

89-99: Consider clarifying test setup to avoid implicit header accumulation.

In this nested context, the outer before block (line 82) already sets message.reply_to, so when the inner before runs, there are actually four Reply-To headers total. The test passes because the Mail gem's message['reply-to'] returns the last header added ('lower-reply-to@railsware.com').

This behavior is valid to test, but the implicit header accumulation might confuse future readers. Consider either:

  1. Adding a comment explaining the test verifies "last header wins" behavior
  2. Or resetting reply_to in the inner before to make the test self-contained
Option: Add clarifying comment
       context 'when reply-to header variants are present' do
         before do
+          # Note: outer before already set one reply-to; Mail gem returns last header added
           message.header['Reply-To'] = 'Reply To <reply-to@railsware.com>'
           message.header['REPLY-TO'] = 'Upper Case <upper-reply-to@railsware.com>'
           message.header['reply-to'] = 'Lower Case <lower-reply-to@railsware.com>'
         end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/mailtrap/mail_spec.rb` around lines 89 - 99, The test's inner before
block adds three Reply-To headers on top of the outer before's message.reply_to,
causing header accumulation; either make the nested context self-contained by
clearing/resetting reply_to before adding the three headers (e.g., call
message.header['Reply-To'] = nil or reset message.reply_to) or add a concise
comment above the inner before explaining this test intentionally exercises
"last header wins" behavior; update references in the spec around
message.reply_to, message.header['Reply-To'], and mail.reply_to accordingly so
future readers understand why multiple headers are present.

56-64: Minor: Test description could be more specific.

The context description says "varying formats" but only tests a single format (quoted non-ASCII display name). Consider renaming to something more specific like 'when reply-to has non-ASCII display name', or expand to test multiple formats within this context if "varying formats" coverage is intended.

That said, the test correctly verifies that Reply-To is stripped from custom headers regardless of encoding.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/mailtrap/mail_spec.rb` around lines 56 - 64, The context description is
misleading—rename the RSpec context from 'when reply-to is added in varying
formats' to a specific description like 'when reply-to has non-ASCII display
name' (the surrounding context block and its before hook that sets
message.reply_to = '"메일트랩" <support@mailtrap.io>') or alternatively add
additional examples within the same context to cover other formats (e.g., plain
address and encoded formats) so the description matches coverage; update only
the context string (and/or add extra it blocks) to keep test intent clear.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@spec/mailtrap/mail_spec.rb`:
- Around line 89-99: The test's inner before block adds three Reply-To headers
on top of the outer before's message.reply_to, causing header accumulation;
either make the nested context self-contained by clearing/resetting reply_to
before adding the three headers (e.g., call message.header['Reply-To'] = nil or
reset message.reply_to) or add a concise comment above the inner before
explaining this test intentionally exercises "last header wins" behavior; update
references in the spec around message.reply_to, message.header['Reply-To'], and
mail.reply_to accordingly so future readers understand why multiple headers are
present.
- Around line 56-64: The context description is misleading—rename the RSpec
context from 'when reply-to is added in varying formats' to a specific
description like 'when reply-to has non-ASCII display name' (the surrounding
context block and its before hook that sets message.reply_to = '"메일트랩"
<support@mailtrap.io>') or alternatively add additional examples within the same
context to cover other formats (e.g., plain address and encoded formats) so the
description matches coverage; update only the context string (and/or add extra
it blocks) to keep test intent clear.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9561ded7-6e00-4673-9c92-b32a8d8fe295

📥 Commits

Reviewing files that changed from the base of the PR and between 7ff1068 and 41ffc54.

📒 Files selected for processing (1)
  • spec/mailtrap/mail_spec.rb

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
spec/mailtrap/mail_spec.rb (1)

89-99: Clarify intent of multi-header variant test.

This test documents behavior when multiple Reply-To headers with different cases coexist. The expectation that the lowercase variant ('reply-to') wins depends on how the Mail gem handles duplicate headers internally.

Consider adding a brief comment explaining why this edge case matters (e.g., potential for malformed messages from external sources) or whether this is documenting a contract you intend to maintain vs. incidental Mail gem behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/mailtrap/mail_spec.rb` around lines 89 - 99, Add a short clarifying
comment inside the context 'when reply-to header variants are present'
describing why the test asserts that the lowercase 'reply-to' header wins (e.g.,
whether this is a deliberate contract your code must preserve versus an
incidental Mail gem behavior or to document handling of malformed external
messages), placing the comment near the message.header assignments (the context
block and the message.header[...] lines) and referencing the expected outcome
asserted by expect(mail.reply_to) to make the intent explicit for future
readers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@spec/mailtrap/mail_spec.rb`:
- Around line 89-99: Add a short clarifying comment inside the context 'when
reply-to header variants are present' describing why the test asserts that the
lowercase 'reply-to' header wins (e.g., whether this is a deliberate contract
your code must preserve versus an incidental Mail gem behavior or to document
handling of malformed external messages), placing the comment near the
message.header assignments (the context block and the message.header[...] lines)
and referencing the expected outcome asserted by expect(mail.reply_to) to make
the intent explicit for future readers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c4ec97a4-1fdd-4ff7-8998-296790bc7664

📥 Commits

Reviewing files that changed from the base of the PR and between 41ffc54 and 0ec6367.

📒 Files selected for processing (1)
  • spec/mailtrap/mail_spec.rb

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.

3 participants