fix(mailbox): handle malformed sender address headers (#13486)

## How to reproduce
When an inbound email has malformed sender headers (for example `From:
McDonald <info@example.com` without a closing `>`), mailbox
processing can raise `Mail::Field::IncompleteParseError` while resolving
sender data in `MailPresenter`.

## What changed
This PR hardens sender parsing in `MailPresenter` with a small, readable
implementation:
- Added/used a safe parser (`parse_mail_address`) that rescues
`Mail::Field::ParseError` and `Mail::Field::IncompleteParseError`.
- `sender_name` now uses the same safe parser path.
- `original_sender` now resolves candidates in order via a compact
`filter_map` flow:
  - `Reply-To`
  - `X-Original-Sender`
  - `From`
- All three candidates are parsed as email addresses before use
(including `X-Original-Sender`), and invalid values are ignored.
- `notification_email_from_chatwoot?` now compares sender addresses
case-insensitively (`casecmp?`) to avoid case-only mismatches.

## Test coverage
Added focused presenter specs for:
- malformed `From` header returns nil sender values and does not
classify as notification sender
- malformed `Reply-To` falls back to valid `From`
- valid `X-Original-Sender` is used when present
- invalid `X-Original-Sender` falls back to valid `From`
- mixed-case sender address still matches configured
`MAILER_SENDER_EMAIL`

## How this was tested
Ran:
- `bundle exec rspec spec/presenters/mail_presenter_spec.rb`
- `bundle exec rubocop app/presenters/mail_presenter.rb
spec/presenters/mail_presenter_spec.rb`

Sentry issue:
[CHATWOOT-B9Y](https://chatwoot-p3.sentry.io/issues/7005483640/)
This commit is contained in:
Sojan Jose
2026-02-11 11:02:38 -08:00
committed by GitHub
parent b2cb3717e5
commit d272a64ff7
2 changed files with 96 additions and 7 deletions

View File

@@ -178,5 +178,85 @@ RSpec.describe MailPresenter do
expect(decorated_mail.serialized_data[:auto_reply]).to be_falsey
end
end
describe 'malformed sender headers' do
let(:mail_with_malformed_from) do
Mail.new do
header['From'] = 'Kevin McDonald <info@example.com'
to 'Inbox <inbox@example.com>'
subject :header
body 'Hi'
end
end
let(:mail_with_malformed_reply_to) do
Mail.new do
from 'Sender <sender@example.com>'
to 'Inbox <inbox@example.com>'
subject :header
body 'Hi'
header['Reply-To'] = 'Reply User <reply@example.com'
end
end
let(:mail_with_original_sender_header) do
Mail.new do
from 'Sender <sender@example.com>'
to 'Inbox <inbox@example.com>'
subject :header
body 'Hi'
header['Reply-To'] = 'Reply User <reply@example.com'
header['X-Original-Sender'] = 'Forwarded Sender <forwarded.sender@example.com>'
end
end
let(:mail_with_invalid_original_sender_header) do
Mail.new do
from 'Sender <sender@example.com>'
to 'Inbox <inbox@example.com>'
subject :header
body 'Hi'
header['Reply-To'] = 'Reply User <reply@example.com'
header['X-Original-Sender'] = 'not an email address'
end
end
it 'returns nil sender values when from header is malformed' do
presenter = described_class.new(mail_with_malformed_from)
expect(presenter.original_sender).to be_nil
expect(presenter.sender_name).to be_nil
expect(presenter.notification_email_from_chatwoot?).to be(false)
end
it 'falls back to from header when reply_to is malformed' do
presenter = described_class.new(mail_with_malformed_reply_to)
expect(presenter.original_sender).to eq('sender@example.com')
end
it 'uses parsed X-Original-Sender value when available' do
presenter = described_class.new(mail_with_original_sender_header)
expect(presenter.original_sender).to eq('forwarded.sender@example.com')
end
it 'falls back to from when X-Original-Sender is invalid' do
presenter = described_class.new(mail_with_invalid_original_sender_header)
expect(presenter.original_sender).to eq('sender@example.com')
end
it 'matches notification sender emails case-insensitively' do
mail_with_uppercase_sender = Mail.new do
from 'Chatwoot <ACCOUNTS@CHATWOOT.COM>'
to 'Inbox <inbox@example.com>'
subject :header
body 'Hi'
end
with_modified_env MAILER_SENDER_EMAIL: 'Chatwoot <accounts@chatwoot.com>' do
presenter = described_class.new(mail_with_uppercase_sender)
expect(presenter.notification_email_from_chatwoot?).to be(true)
end
end
end
end
end