Files
chatwoot/spec/mailboxes/mailbox_helper_spec.rb
d1c482cb64 fix(email): strip null bytes from inbound mailbox messages (#14546)
Inbound emails containing null bytes could fail while Chatwoot persisted
the incoming message, causing IMAP sync to drop and repeatedly retry the
same malformed email. This strips null bytes at the inbound mailbox
message persistence boundary so the rest of the email can be saved
normally.

Fixes
https://linear.app/chatwoot/issue/CW-7165/argumenterror-string-contains-null-byte-argumenterror
Sentry: https://chatwoot-p3.sentry.io/issues/7405946944/

## Test
- [x] added specs
- [x] manually tested the logic on prod for the inbox(email) which was
throwing the error

## What changed

- Sanitizes null bytes only while building attributes for inbound
mailbox message persistence.
- Uses the sanitized source ID for both duplicate lookup and message
creation.
- Adds regression coverage for the mailbox message creation path.

---------

Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-06-08 17:12:25 +05:30

162 lines
5.8 KiB
Ruby

require 'rails_helper'
RSpec.describe MailboxHelper do
include ActionMailbox::TestHelper
# Setup anonymous class
let(:mailbox_helper_obj) do
Class.new do
include MailboxHelper
attr_accessor :conversation, :processed_mail
def initialize(conversation, processed_mail)
@conversation = conversation
@processed_mail = processed_mail
end
end
end
let(:mail) { create_inbound_email_from_fixture('welcome.eml').mail }
let(:processed_mail) { MailPresenter.new(mail) }
let(:conversation) { create(:conversation) }
let(:dummy_message) { create(:message) }
describe '#create_message' do
before do
create_list(:message, 5, conversation: conversation)
end
context 'when message already exist' do
it 'creates a new message' do
helper_instance = mailbox_helper_obj.new(conversation, processed_mail)
expect(conversation.messages).to receive(:find_by).with(source_id: processed_mail.message_id).and_return(dummy_message)
expect(conversation.messages).not_to receive(:create!)
helper_instance.send(:create_message)
end
end
context 'when message does not exist' do
it 'creates a new message' do
helper_instance = mailbox_helper_obj.new(conversation, processed_mail)
expect(conversation.messages).to receive(:find_by).with(source_id: processed_mail.message_id).and_return(nil)
expect(conversation.messages).to receive(:create!)
helper_instance.send(:create_message)
end
end
context 'when message data contains null bytes' do
let(:mail) do
mail = Mail.new
mail.from = 'Sender <sender@example.com>'
mail.to = 'Inbox <inbox@example.com>'
mail.subject = "Hello\u0000"
mail.message_id = "message\u0000@example.com"
mail.content_type = 'text/plain'
mail.body = "Body\u0000 text"
mail
end
it 'creates the message with sanitized values' do
helper_instance = mailbox_helper_obj.new(conversation, processed_mail)
expect { helper_instance.send(:create_message) }.to change(conversation.messages, :count).by(1)
message = conversation.messages.last
expect(message.source_id).to eq('message@example.com')
expect(message.content).to eq('Body text')
expect(message.content_attributes.dig('email', 'message_id')).to eq('message@example.com')
expect(message.content_attributes.to_json).not_to include('\u0000')
end
end
end
describe '#embed_plain_text_email_with_inline_image' do
let(:mail_attachment) do
{
original: OpenStruct.new(filename: 'image.png'),
blob: get_blob_for('spec/assets/avatar.png', 'image/png')
}
end
let(:helper_instance) { mailbox_helper_obj.new(conversation, processed_mail) }
it 'replaces the image tag in the text content' do
helper_instance.instance_variable_set(:@text_content, 'Hello [image: image.png] World')
helper_instance.send(:embed_plain_text_email_with_inline_image, mail_attachment)
text_content = helper_instance.instance_variable_get(:@text_content)
expect(text_content).to include(Rails.application.routes.url_helpers.url_for(mail_attachment[:blob]))
expect(text_content).not_to include('[image: avatar.png]')
end
it 'replaces the image tag in the text content even if there is not tag to replace' do
helper_instance.instance_variable_set(:@text_content, 'Hello World')
helper_instance.send(:embed_plain_text_email_with_inline_image, mail_attachment)
text_content = helper_instance.instance_variable_get(:@text_content)
expect(text_content).to include(Rails.application.routes.url_helpers.url_for(mail_attachment[:blob]))
end
end
describe '#body_references_cid?' do
let(:helper_instance) { mailbox_helper_obj.new(conversation, processed_mail) }
it 'detects percent-encoded CID references in HTML content' do
helper_instance.instance_variable_set(:@html_content, '<img src="cid:image001.jpg%40test">')
expect(helper_instance.send(:body_references_cid?, 'image001.jpg@test')).to be true
end
end
describe '#upload_inline_image' do
let(:mail_attachment) do
{
original: OpenStruct.new(cid: 'image001.jpg@test'),
blob: get_blob_for('spec/assets/avatar.png', 'image/png')
}
end
let(:helper_instance) { mailbox_helper_obj.new(conversation, processed_mail) }
it 'replaces percent-encoded CID references in HTML content' do
allow(Rails.application.routes.url_helpers).to receive(:url_for).and_return('/fake-image-url')
helper_instance.instance_variable_set(:@html_content, '<img src="cid:image001.jpg%40test">')
helper_instance.send(:upload_inline_image, mail_attachment)
html_content = helper_instance.instance_variable_get(:@html_content)
expect(html_content).to include('/fake-image-url"')
expect(html_content).not_to include('cid:')
end
end
describe '#add_attachments_to_message' do
let(:mail) { create_inbound_email_from_fixture('cid_inline_images_without_disposition.eml').mail }
let(:processed_mail) { MailPresenter.new(mail) }
let(:conversation) { create(:conversation) }
let(:helper_instance) { mailbox_helper_obj.new(conversation, processed_mail) }
before do
helper_instance.send(:create_message)
end
it 'detects inline image attachment by cid reference when Content-Disposition is missing' do
allow(Rails.application.routes.url_helpers).to receive(:url_for).and_return('/fake-image-url')
helper_instance.send(:add_attachments_to_message)
message = conversation.messages[0]
expect(message.attachments.count).to eq(0)
html_content = message.content_attributes[:email][:html_content][:full]
expect(html_content).to include('/fake-image-url"')
expect(html_content).not_to include('cid:')
end
end
end