Skip to content

Fix Reply-To problem#97

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

Fix Reply-To problem#97
ryush00 wants to merge 4 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 for:
    • non-ASCII display name
    • quoted display name
    • plain address
  • Reply-To header removal (including case variants: Reply-To, REPLY-TO, reply-to)
  • Invalid Reply-To error behavior
  • Folded/encoded invalid Reply-To form (newline folding) raises parse error

4) Integration fixture update

  • Updated ActionMailer delivery VCR cassette payload to reflect:
    • top-level reply_to
    • no headers.Reply-To duplication

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

    • Added support for reply-to headers in outgoing emails. The system now properly processes, validates, and sends reply-to addresses as structured data in API requests.
  • Tests

    • Added comprehensive tests covering various reply-to address formats, validation scenarios, and error handling cases.

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

This change adds reply-to header support to the Mailtrap mail library by adding 'replyto' to the SPECIAL_HEADERS constant and implementing extraction of reply-to values from messages. The reply-to field is now mapped to a structured object in API requests, consistent with existing from/to/cc/bcc handling, along with comprehensive test coverage for various formats and edge cases.

Changes

Cohort / File(s) Summary
Core Implementation
lib/mailtrap/mail.rb
Added 'replyto' to SPECIAL_HEADERS constant and implemented reply-to value propagation from Message objects by mapping message['reply-to'] to the reply_to field in the Base object.
Test Suite
spec/mailtrap/mail_spec.rb
Added comprehensive tests validating reply-to parsing for various formats ("Name ", plain emails), header variant handling (Reply-To, REPLY-TO, reply-to), removal of Reply-To from raw headers after parsing, error handling for invalid formats, and edge cases with folded line breaks.
Test Fixture
spec/fixtures/vcr_cassettes/Mailtrap_ActionMailer_DeliveryMethod/_deliver_/converts_the_message_and_sends_via_API.yml
Updated API request payload to include reply_to as a structured object within the JSON body, replacing the inline Reply-To header approach in request headers.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A reply-to hops into the fold,
Structured and clean, as I've been told,
From message to API, it takes its flight,
With tests that check each format 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 nature of the change; it lacks detail about what the fix entails. Consider using a more descriptive title such as 'Map Reply-To header to structured payload field' or 'Fix Reply-To handling in ActionMailer conversion' to better communicate the change.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is comprehensive and covers all key sections including motivation, detailed changes, safety considerations, testing instructions, and reviewer notes, though the images section deviates from template format.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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?

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.

2 participants