Compare 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
|
# 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
|
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_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
|
# routes as a reply to existing conversations
|
||||||
routing(
|
routing(
|
||||||
@@ -24,14 +25,27 @@ class ApplicationMailbox < ActionMailbox::Base
|
|||||||
# <account/#{@account.id}/conversation/#{@conversation.uuid}@#{@account.inbound_email_domain}>
|
# <account/#{@account.id}/conversation/#{@conversation.uuid}@#{@account.inbound_email_domain}>
|
||||||
def in_reply_to_mail?(inbound_mail)
|
def in_reply_to_mail?(inbound_mail)
|
||||||
in_reply_to = inbound_mail.mail.in_reply_to
|
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)
|
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
|
end
|
||||||
|
|
||||||
def in_reply_to_matches?(in_reply_to)
|
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
|
end
|
||||||
|
|
||||||
# checks if follow this pattern send it to reply_mailbox
|
# checks if follow this pattern send it to reply_mailbox
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ class ReplyMailbox < ApplicationMailbox
|
|||||||
def find_relative_conversation
|
def find_relative_conversation
|
||||||
if @conversation_uuid
|
if @conversation_uuid
|
||||||
find_conversation_with_uuid
|
find_conversation_with_uuid
|
||||||
elsif mail.in_reply_to.present?
|
elsif mail.in_reply_to.present? || mail.references.present?
|
||||||
find_conversation_with_in_reply_to
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -79,6 +81,55 @@ class ReplyMailbox < ApplicationMailbox
|
|||||||
find_conversation_by_message_id(in_reply_to_addresses) if @conversation.blank?
|
find_conversation_by_message_id(in_reply_to_addresses) if @conversation.blank?
|
||||||
end
|
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)
|
def validate_resource(resource)
|
||||||
Rails.logger.error "[App::Mailboxes::ReplyMailbox] Email conversation with uuid: #{conversation_uuid} not found" if resource.nil?
|
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
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user