Some email clients automatically set Content-Disposition to inline for
specific content types, such as images. In cases where the email body is
empty, inline attachments may not display correctly due to our previous
implementation. Our assumption was that these attachments are referenced
within text/plain or text/html parts.
Customer-reported issues, especially with Apple Mail, show emails with
attachments marked as inline but without any corresponding text parts.
This leads to missing attachments even though would have processed the
attachment.
This update introduces a check for the presence of a text part. If none
exists, inline attachments are treated as regular attachments and added
to the external attachments array, ensuring that all attachments display
properly.
<details>
<summary><b>Script to update the existing emails that are already
available in the system</b></summary>
```rb
def update_content id
message = Message.find id
conversation = message.conversation
message_id = message.source_id
channel = message.inbox.channel
authentication_type = 'XOAUTH2'
imap_password = Google::RefreshOauthTokenService.new(channel: channel).access_token
imap = Net::IMAP.new(channel.imap_address, port: channel.imap_port, ssl: true)
imap.authenticate(authentication_type, channel.imap_login, imap_password)
imap.select('INBOX')
results = imap.search(['HEADER', 'MESSAGE-ID', message_id])
message_content = imap.fetch(results.first, 'RFC822').first.attr['RFC822']
mail = MailPresenter.new(Mail.read_from_string(message_content))
mail_content = if mail.text_content.present?
mail.text_content[:reply]
elsif mail.html_content.present?
mail.html_content[:reply]
end
attachments = mail.attachments.last(Message::NUMBER_OF_PERMITTED_ATTACHMENTS)
inline_attachments = attachments.select { |attachment| attachment[:original].inline? && mail_content.present? }
regular_attachments = attachments - inline_attachments
regular_attachments.each do |mail_attachment|
attachment = message.attachments.new(
account_id: conversation.account_id,
file_type: 'file'
)
attachment.file.attach(mail_attachment[:blob])
end
message.save!
end
```
</details>
226 lines
8.6 KiB
Ruby
226 lines
8.6 KiB
Ruby
require 'rails_helper'
|
|
|
|
RSpec.describe Imap::ImapMailbox do
|
|
include ActionMailbox::TestHelper
|
|
|
|
describe '#process' do
|
|
let(:account) { create(:account) }
|
|
let(:agent) { create(:user, email: 'agent@example.com', account: account) }
|
|
let(:channel) { create(:channel_email, :imap_email) }
|
|
let(:inbox) { channel.inbox }
|
|
let!(:contact) { create(:contact, email: 'email@gmail.com', phone_number: '+919584546666', account: account, identifier: '123') }
|
|
let(:conversation) { Conversation.where(inbox_id: channel.inbox).last }
|
|
let(:class_instance) { described_class.new }
|
|
|
|
before do
|
|
create(:contact_inbox, contact_id: contact.id, inbox_id: channel.inbox.id)
|
|
end
|
|
|
|
context 'when the email is from a new contact' do
|
|
let(:inbound_mail) { create_inbound_email_from_mail(from: 'testemail@gmail.com', to: 'imap@gmail.com', subject: 'Hello!') }
|
|
|
|
it 'creates the contact and conversation with message' do
|
|
expect do
|
|
class_instance.process(inbound_mail.mail, channel)
|
|
end.to change(Conversation, :count).by(1)
|
|
|
|
expect(conversation.contact.email).to eq(inbound_mail.mail.from.first)
|
|
expect(conversation.additional_attributes['source']).to eq('email')
|
|
expect(conversation.messages.empty?).to be false
|
|
end
|
|
end
|
|
|
|
context 'when the email with has empty text content' do
|
|
let(:inbound_mail) { create_inbound_email_from_fixture('attachments_without_text.eml') }
|
|
|
|
it 'creates a converstation and a message properly' do
|
|
expect do
|
|
class_instance.process(inbound_mail.mail, channel)
|
|
end.to change(Conversation, :count).by(1)
|
|
|
|
expect(conversation.contact.email).to eq(inbound_mail.mail.from.first)
|
|
expect(conversation.messages.last.attachments.count).to be 2
|
|
end
|
|
end
|
|
|
|
context 'when the email has 15 or more attachments' do
|
|
let(:inbound_mail) { create_inbound_email_from_fixture('multiple_attachments.eml') }
|
|
|
|
it 'creates a converstation and a message properly' do
|
|
expect do
|
|
class_instance.process(inbound_mail.mail, channel)
|
|
end.to change(Conversation, :count).by(1)
|
|
|
|
expect(conversation.contact.email).to eq(inbound_mail.mail.from.first)
|
|
expect(conversation.messages.last.attachments.count).to be 15
|
|
end
|
|
end
|
|
|
|
context 'when a new email from existing contact' do
|
|
let(:inbound_mail) { create_inbound_email_from_mail(from: 'email@gmail.com', to: 'imap@gmail.com', subject: 'Hello!') }
|
|
|
|
it 'creates a new conversation with message' do
|
|
class_instance.process(inbound_mail.mail, channel)
|
|
expect(conversation.contact.email).to eq(contact.email)
|
|
expect(conversation.additional_attributes['source']).to eq('email')
|
|
expect(conversation.messages.empty?).to be false
|
|
end
|
|
end
|
|
|
|
context 'when a new email with invalid from' do
|
|
let(:inbound_mail) { create_inbound_email_from_mail(from: 'invalidemail', to: 'imap@gmail.com', subject: 'Hello!') }
|
|
|
|
it 'does not create a new conversation' do
|
|
expect { class_instance.process(inbound_mail.mail, channel) }.not_to raise_error
|
|
end
|
|
end
|
|
|
|
context 'when an auto reply email' do
|
|
let(:auto_reply_mail) { create_inbound_email_from_fixture('auto_reply.eml') }
|
|
|
|
it 'does not create a new conversation' do
|
|
expect { class_instance.process(auto_reply_mail.mail, channel) }.not_to change(Conversation, :count)
|
|
end
|
|
end
|
|
|
|
context 'when a reply for existing email conversation' do
|
|
let(:prev_conversation) { create(:conversation, account: account, inbox: channel.inbox, assignee: agent) }
|
|
let(:reply_mail) do
|
|
create_inbound_email_from_mail(from: 'email@gmail.com', to: 'imap@gmail.com', subject: 'Hello!', in_reply_to: 'test-in-reply-to')
|
|
end
|
|
|
|
it 'appends new email to the existing conversation' do
|
|
create(
|
|
:message,
|
|
content: 'Incoming Message',
|
|
message_type: 'incoming',
|
|
inbox: inbox,
|
|
account: account,
|
|
conversation: prev_conversation
|
|
)
|
|
create(
|
|
:message,
|
|
content: 'Outgoing Message',
|
|
message_type: 'outgoing',
|
|
inbox: inbox,
|
|
source_id: 'test-in-reply-to',
|
|
account: account,
|
|
conversation: prev_conversation
|
|
)
|
|
|
|
expect(prev_conversation.messages.size).to eq(2)
|
|
|
|
class_instance.process(reply_mail.mail, channel)
|
|
|
|
expect(prev_conversation.messages.size).to eq(3)
|
|
expect(prev_conversation.messages.last.content_attributes['email']['from']).to eq(reply_mail.mail.from)
|
|
expect(prev_conversation.messages.last.content_attributes['email']['to']).to eq(reply_mail.mail.to)
|
|
expect(prev_conversation.messages.last.content_attributes['email']['subject']).to eq(reply_mail.mail.subject)
|
|
expect(prev_conversation.messages.last.content_attributes['email']['in_reply_to']).to eq(reply_mail.mail.in_reply_to)
|
|
end
|
|
end
|
|
|
|
context 'when a new conversation with nil in_reply_to' do
|
|
let(:prev_conversation) { create(:conversation, account: account, inbox: channel.inbox, assignee: agent) }
|
|
let(:reply_mail) do
|
|
create_inbound_email_from_mail(from: 'email@gmail.com', to: 'imap@gmail.com', subject: 'Hello!', in_reply_to: nil)
|
|
end
|
|
|
|
it 'appends new email to the existing conversation' do
|
|
create(
|
|
:message,
|
|
content: 'Incoming Message',
|
|
message_type: 'incoming',
|
|
inbox: inbox,
|
|
account: account,
|
|
conversation: prev_conversation
|
|
)
|
|
create(
|
|
:message,
|
|
content: 'Outgoing Message',
|
|
message_type: 'outgoing',
|
|
inbox: inbox,
|
|
source_id: nil,
|
|
account: account,
|
|
conversation: prev_conversation
|
|
)
|
|
|
|
expect(prev_conversation.messages.size).to eq(2)
|
|
|
|
class_instance.process(reply_mail.mail, channel)
|
|
|
|
expect(prev_conversation.messages.size).to eq(2)
|
|
|
|
new_converstion_message = Conversation.last.messages.last.content_attributes
|
|
expect(new_converstion_message['email']['subject']).to eq('Hello!')
|
|
end
|
|
end
|
|
|
|
context 'when a reply for non existing email conversation' do
|
|
let(:reply_mail) do
|
|
create_inbound_email_from_mail(from: 'email@gmail.com', to: 'imap@gmail.com', subject: 'Hello!', in_reply_to: 'test-in-reply-to')
|
|
end
|
|
let(:references_email) { create_inbound_email_from_fixture('references.eml') }
|
|
|
|
it 'creates new email conversation with incoming in-reply-to' do
|
|
class_instance.process(reply_mail.mail, channel)
|
|
expect(conversation.additional_attributes['in_reply_to']).to eq(reply_mail.mail.in_reply_to)
|
|
end
|
|
|
|
it 'append email to conversation with references id' do
|
|
inbox = Inbox.last
|
|
message = create(
|
|
:message,
|
|
content: 'Incoming Message',
|
|
message_type: 'incoming',
|
|
inbox: inbox,
|
|
source_id: 'test-reference-id',
|
|
account: account,
|
|
conversation: conversation
|
|
)
|
|
conversation = message.conversation
|
|
|
|
expect(conversation.messages.size).to eq(1)
|
|
|
|
class_instance.process(references_email.mail, inbox.channel)
|
|
|
|
expect(conversation.messages.size).to eq(2)
|
|
expect(conversation.messages.last.content).to eq('References Email')
|
|
expect(references_email.mail.references).to include('test-reference-id')
|
|
end
|
|
|
|
it 'append email to conversation with reference id string' do
|
|
inbox = Inbox.last
|
|
message = create(
|
|
:message,
|
|
content: 'Incoming Message',
|
|
message_type: 'incoming',
|
|
inbox: inbox,
|
|
source_id: 'test-reference-id-2',
|
|
account: account,
|
|
conversation: conversation
|
|
)
|
|
conversation = message.conversation
|
|
|
|
expect(conversation.messages.size).to eq(1)
|
|
|
|
references_email.mail.references = 'test-reference-id-2'
|
|
class_instance.process(references_email.mail, inbox.channel)
|
|
|
|
expect(conversation.messages.size).to eq(2)
|
|
expect(conversation.messages.last.content).to eq('References Email')
|
|
expect(references_email.mail.references).to include('test-reference-id-2')
|
|
end
|
|
end
|
|
|
|
context 'when a reply for a conversation has multiple in_reply_to' do
|
|
let(:multiple_in_reply_to_mail) { create_inbound_email_from_fixture('multiple_in_reply_to.eml').mail }
|
|
|
|
it 'creates conversation taking the first in_reply_to email' do
|
|
class_instance.process(multiple_in_reply_to_mail, channel)
|
|
expect(conversation.additional_attributes['in_reply_to']).to eq(multiple_in_reply_to_mail.in_reply_to.first)
|
|
end
|
|
end
|
|
end
|
|
end
|