fix: index email subject from conversation for outbound messages (#14122)

## Summary

Outbound email messages never populate
`content_attributes.email.subject`. The subject lives only on the parent
conversation's `additional_attributes.mail_subject`. The search index
doc built by `Messages::SearchDataPresenter` pulls from the
message-level field only, so outbound email subjects are unsearchable
for accounts on the advanced_search (Elasticsearch) path.

This change makes the presenter fall back to
`conversation.additional_attributes.mail_subject` when the message-level
subject is blank. Inbound email messages keep their existing behavior
(message-level subject takes precedence).

Closes https://linear.app/chatwoot/issue/CW-6877

## What changes

- `Messages::SearchDataPresenter#content_attributes_data` now falls back
to the conversation's `mail_subject` when the message-level subject is
blank.
- Added specs covering the fallback, precedence, and the neither-set
case.

## What does not change

- No schema changes, no migrations, no backfill of existing OpenSearch
documents.
- Searchkick's `after_commit :reindex_for_search` on `Message` will pick
up the new field for all newly created or updated messages via the
existing indexing path.
- Postgres search path (free accounts, fallback) is untouched. Broader
subject search for those users is a separate follow-up.
This commit is contained in:
Vishnu Narayanan
2026-04-22 20:36:35 +05:30
committed by GitHub
parent 475db85318
commit ce34f93917
2 changed files with 31 additions and 1 deletions
@@ -39,7 +39,8 @@ class Messages::SearchDataPresenter < SimpleDelegator
end
def content_attributes_data
email_subject = content_attributes.dig(:email, :subject)
email_subject = content_attributes.dig(:email, :subject).presence ||
conversation.additional_attributes&.dig('mail_subject').presence
return {} if email_subject.blank?
{ email: { subject: email_subject } }
@@ -58,6 +58,35 @@ RSpec.describe Messages::SearchDataPresenter do
end
end
context 'when the message has no email subject but the conversation has a mail_subject' do
before do
conversation.update!(additional_attributes: { 'mail_subject' => 'Conversation Subject' })
end
it 'falls back to the conversation mail_subject' do
content_attrs = presenter.search_data[:content_attributes]
expect(content_attrs[:email][:subject]).to eq('Conversation Subject')
end
end
context 'when both the message email subject and conversation mail_subject are set' do
before do
conversation.update!(additional_attributes: { 'mail_subject' => 'Conversation subject' })
message.update!(content_attributes: { email: { subject: 'Message subject' } })
end
it 'prefers the message-level email subject' do
content_attrs = presenter.search_data[:content_attributes]
expect(content_attrs[:email][:subject]).to eq('Message subject')
end
end
context 'when neither message nor conversation has an email subject' do
it 'omits the email subject from content_attributes' do
expect(presenter.search_data[:content_attributes]).to eq({})
end
end
context 'with campaign and automation data' do
before do
message.update(