Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a813a67e6 | ||
|
|
a08d44b82d | ||
|
|
5dc888c573 | ||
|
|
3817fad3fe | ||
|
|
21457368d7 |
@@ -5,6 +5,7 @@ class ApplicationMailbox < ActionMailbox::Base
|
||||
# Eg: email should be something like : reply+6bdc3f4d-0bec-4515-a284-5d916fdde489@domain.com
|
||||
REPLY_EMAIL_UUID_PATTERN = /^reply\+([0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12})$/i
|
||||
CONVERSATION_MESSAGE_ID_PATTERN = %r{conversation/([a-zA-Z0-9-]*?)/messages/(\d+?)@(\w+\.\w+)}
|
||||
CONVERSATION_FALLBACK_ID_PATTERN = %r{account/(\d+)/conversation/([a-zA-Z0-9-]+)@}
|
||||
|
||||
# routes as a reply to existing conversations
|
||||
routing(
|
||||
@@ -24,14 +25,27 @@ class ApplicationMailbox < ActionMailbox::Base
|
||||
# <account/#{@account.id}/conversation/#{@conversation.uuid}@#{@account.inbound_email_domain}>
|
||||
def in_reply_to_mail?(inbound_mail)
|
||||
in_reply_to = inbound_mail.mail.in_reply_to
|
||||
references = inbound_mail.mail.references
|
||||
|
||||
in_reply_to.present? && (
|
||||
# Check in_reply_to first
|
||||
return true if in_reply_to.present? && (
|
||||
in_reply_to_matches?(in_reply_to) || Message.exists?(source_id: in_reply_to)
|
||||
)
|
||||
|
||||
# Fallback to checking references header
|
||||
references.present? && references_match?(references)
|
||||
end
|
||||
|
||||
def in_reply_to_matches?(in_reply_to)
|
||||
Array.wrap(in_reply_to).any? { it.match?(CONVERSATION_MESSAGE_ID_PATTERN) }
|
||||
Array.wrap(in_reply_to).any? { |id| id.match?(CONVERSATION_MESSAGE_ID_PATTERN) || id.match?(CONVERSATION_FALLBACK_ID_PATTERN) }
|
||||
end
|
||||
|
||||
def references_match?(references)
|
||||
Array.wrap(references).any? do |reference|
|
||||
reference.match?(CONVERSATION_MESSAGE_ID_PATTERN) ||
|
||||
reference.match?(CONVERSATION_FALLBACK_ID_PATTERN) ||
|
||||
Message.exists?(source_id: reference)
|
||||
end
|
||||
end
|
||||
|
||||
# checks if follow this pattern send it to reply_mailbox
|
||||
|
||||
@@ -21,8 +21,10 @@ class ReplyMailbox < ApplicationMailbox
|
||||
def find_relative_conversation
|
||||
if @conversation_uuid
|
||||
find_conversation_with_uuid
|
||||
elsif mail.in_reply_to.present?
|
||||
find_conversation_with_in_reply_to
|
||||
elsif mail.in_reply_to.present? || mail.references.present?
|
||||
find_conversation_with_in_reply_to if mail.in_reply_to.present?
|
||||
# If still no conversation found and references exist, try references as fallback
|
||||
find_conversation_with_references if @conversation.blank? && mail.references.present?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -79,6 +81,55 @@ class ReplyMailbox < ApplicationMailbox
|
||||
find_conversation_by_message_id(in_reply_to_addresses) if @conversation.blank?
|
||||
end
|
||||
|
||||
# Find conversation from References header as fallback
|
||||
# Extract conversation UUID from any reference that matches our patterns
|
||||
def find_conversation_with_references
|
||||
references_addresses = mail.references
|
||||
references_addresses = [references_addresses] if references_addresses.is_a?(String)
|
||||
|
||||
references_addresses.each do |reference|
|
||||
conversation = find_conversation_from_reference(reference)
|
||||
next unless conversation && conversation_belongs_to_channel?(conversation)
|
||||
|
||||
@conversation = conversation
|
||||
@conversation_uuid = conversation.uuid
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
def find_conversation_from_reference(reference)
|
||||
# Try message-specific pattern: conversation/{uuid}/messages/{id}@domain
|
||||
message_match = reference.match(::ApplicationMailbox::CONVERSATION_MESSAGE_ID_PATTERN)
|
||||
if message_match
|
||||
uuid = message_match.captures[0]
|
||||
conversation = Conversation.find_by(uuid: uuid)
|
||||
return conversation if conversation.present?
|
||||
end
|
||||
|
||||
# Try conversation fallback pattern: account/{id}/conversation/{uuid}@domain
|
||||
fallback_match = reference.match(::ApplicationMailbox::CONVERSATION_FALLBACK_ID_PATTERN)
|
||||
if fallback_match
|
||||
uuid = fallback_match.captures[1]
|
||||
conversation = Conversation.find_by(uuid: uuid)
|
||||
return conversation if conversation.present?
|
||||
end
|
||||
|
||||
# Try finding by message source_id
|
||||
message = Message.find_by(source_id: reference)
|
||||
message&.conversation
|
||||
end
|
||||
|
||||
def conversation_belongs_to_channel?(conversation)
|
||||
return true unless conversation
|
||||
|
||||
# Get the channel from the email's To/CC addresses
|
||||
channel = EmailChannelFinder.new(mail).perform
|
||||
return false unless channel
|
||||
|
||||
# Check if the conversation's inbox matches the channel
|
||||
conversation.inbox.channel_id == channel.id
|
||||
end
|
||||
|
||||
def validate_resource(resource)
|
||||
Rails.logger.error "[App::Mailboxes::ReplyMailbox] Email conversation with uuid: #{conversation_uuid} not found" if resource.nil?
|
||||
|
||||
|
||||
@@ -248,5 +248,143 @@ RSpec.describe ReplyMailbox do
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with references header' do
|
||||
let(:reply_mail_with_references) { create_inbound_email_from_fixture('reply_mail_without_uuid.eml') }
|
||||
let(:email_channel) { create(:channel_email, email: 'test@example.com', account: account) }
|
||||
let(:conversation_1) do
|
||||
create(
|
||||
:conversation,
|
||||
assignee: agent,
|
||||
inbox: email_channel.inbox,
|
||||
account: account,
|
||||
additional_attributes: { mail_subject: "Discussion: Let's debate these attachments" }
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
conversation_1.update!(uuid: '6bdc3f4d-0bec-4515-a284-5d916fdde489')
|
||||
end
|
||||
|
||||
context 'with message-specific pattern in references' do
|
||||
before do
|
||||
reply_mail_with_references.mail['References'] = '<conversation/6bdc3f4d-0bec-4515-a284-5d916fdde489/messages/123@test.com>'
|
||||
end
|
||||
|
||||
it 'finds conversation from references header with message pattern' do
|
||||
described_class.receive reply_mail_with_references
|
||||
expect(conversation_1.messages.last.content).to include("Let's talk about these images:")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with conversation fallback pattern in references' do
|
||||
before do
|
||||
reply_mail_with_references.mail['References'] = "<account/#{account.id}/conversation/6bdc3f4d-0bec-4515-a284-5d916fdde489@test.com>"
|
||||
end
|
||||
|
||||
it 'finds conversation from references header with fallback pattern' do
|
||||
described_class.receive reply_mail_with_references
|
||||
expect(conversation_1.messages.last.content).to include("Let's talk about these images:")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple references including conversation pattern' do
|
||||
before do
|
||||
reply_mail_with_references.mail['References'] = [
|
||||
'<some-random-message-id@gmail.com>',
|
||||
"<account/#{account.id}/conversation/6bdc3f4d-0bec-4515-a284-5d916fdde489@test.com>",
|
||||
'<another-random-message-id@outlook.com>'
|
||||
].join("\r\n ")
|
||||
end
|
||||
|
||||
it 'finds conversation from any reference in the chain' do
|
||||
described_class.receive reply_mail_with_references
|
||||
expect(conversation_1.messages.last.content).to include("Let's talk about these images:")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with message source_id in references' do
|
||||
before do
|
||||
conversation_1.messages.create!(
|
||||
source_id: 'original-message-id@test.com',
|
||||
account_id: account.id,
|
||||
message_type: 'outgoing',
|
||||
inbox_id: email_channel.inbox.id,
|
||||
content: 'Original message'
|
||||
)
|
||||
reply_mail_with_references.mail['References'] = '<original-message-id@test.com>'
|
||||
end
|
||||
|
||||
it 'finds conversation from message source_id in references' do
|
||||
described_class.receive reply_mail_with_references
|
||||
expect(conversation_1.messages.last.content).to include("Let's talk about these images:")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with conversation from different channel in references' do
|
||||
let(:other_email_channel) { create(:channel_email, email: 'other@example.com', account: account) }
|
||||
let(:other_conversation) do
|
||||
create(
|
||||
:conversation,
|
||||
assignee: agent,
|
||||
inbox: other_email_channel.inbox,
|
||||
account: account
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
other_conversation.update!(uuid: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee')
|
||||
reply_mail_with_references.mail['References'] = '<conversation/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/messages/456@test.com>'
|
||||
end
|
||||
|
||||
it 'does not use conversation from different channel' do
|
||||
described_class.receive reply_mail_with_references
|
||||
expect(other_conversation.messages.count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when in_reply_to fails but references has valid conversation' do
|
||||
before do
|
||||
reply_mail_with_references.mail['In-Reply-To'] = '<non-existent-message-id@test.com>'
|
||||
reply_mail_with_references.mail['References'] = "<account/#{account.id}/conversation/6bdc3f4d-0bec-4515-a284-5d916fdde489@test.com>"
|
||||
end
|
||||
|
||||
it 'falls back to references header when in_reply_to lookup fails' do
|
||||
described_class.receive reply_mail_with_references
|
||||
expect(conversation_1.messages.last.content).to include("Let's talk about these images:")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when in_reply_to fails and references has message pattern' do
|
||||
before do
|
||||
reply_mail_with_references.mail['In-Reply-To'] = '<random-invalid-id@gmail.com>'
|
||||
reply_mail_with_references.mail['References'] = '<conversation/6bdc3f4d-0bec-4515-a284-5d916fdde489/messages/789@test.com>'
|
||||
end
|
||||
|
||||
it 'falls back to references with message pattern when in_reply_to lookup fails' do
|
||||
described_class.receive reply_mail_with_references
|
||||
expect(conversation_1.messages.last.content).to include("Let's talk about these images:")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when in_reply_to fails and references has message source_id' do
|
||||
before do
|
||||
conversation_1.messages.create!(
|
||||
source_id: 'valid-message-id@test.com',
|
||||
account_id: account.id,
|
||||
message_type: 'outgoing',
|
||||
inbox_id: email_channel.inbox.id,
|
||||
content: 'Previous message'
|
||||
)
|
||||
reply_mail_with_references.mail['In-Reply-To'] = '<wrong-message-id@test.com>'
|
||||
reply_mail_with_references.mail['References'] = '<valid-message-id@test.com>'
|
||||
end
|
||||
|
||||
it 'falls back to references with source_id when in_reply_to lookup fails' do
|
||||
described_class.receive reply_mail_with_references
|
||||
expect(conversation_1.messages.last.content).to include("Let's talk about these images:")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user