Files
moreminimore-chat/spec/mailers/conversation_reply_mailer_spec.rb
Vaibhav Dewangan ecb7a44f07 fix(email): resolve reply recipients from the message being sent (#15194)
When an agent loops a new address into an ongoing email thread and then
adds a private note, the reply currently goes out with no Cc, and with
To falling back to the conversation contact. The newly added person
never receives the mail. This change makes an outgoing email keep the
To/Cc/Bcc that were entered on that reply, no matter what is added to
the conversation afterwards.

Closes #15193

## How to reproduce

1. Open a conversation on an Email inbox.
2. Add a new address to **Cc** and send a reply.
3. Immediately add a private note to the same conversation.
4. Inspect the delivered mail: `Cc` is empty and `To` is the
conversation contact instead of the addresses entered on the reply.

## What changed

`ConversationReplyMailer#cc_bcc_emails` and
`#to_emails_from_content_attributes` read the addresses from
`@conversation.messages.outgoing.last` rather than from the message they
were handed. Replies are delivered asynchronously
(`SendReplyJob.perform_later`, and `wait: 2.seconds` when the message
has attachments), so any outgoing message created in that window
replaces the recipients of a mail that is already queued. A private note
is the easiest way to hit it: private notes are outgoing messages, and
on an Email inbox `Messages::MessageBuilder#process_emails` stores them
with empty `to_emails` / `cc_emails` / `bcc_emails`.

Both lookups now go through the existing `current_message` helper
(`@message || @conversation.messages.outgoing.last`), which the `from`
and `reply_to` builders already use. `email_reply` sets `@message`, so
it resolves the recipients of the message being delivered;
`reply_with_summary` and `reply_without_summary` leave `@message` nil
and keep their current behaviour.

Added a spec covering the private-note case in
`spec/mailers/conversation_reply_mailer_spec.rb`.

## Note

I could not run the Ruby test suite locally — there is no Ruby toolchain
in the environment I worked in, so the new spec has not been executed on
my side. The change and the spec were verified by reading the code paths
(`Message#send_reply`, `Email::SendOnEmailService`,
`Messages::MessageBuilder#process_emails`,
`ConversationReplyMailerHelper#prepare_mail`). Deferring to CI for the
actual run.
2026-07-30 17:53:11 -07:00

872 lines
40 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ConversationReplyMailer do
describe 'reply' do
let!(:account) { create(:account) }
let!(:agent) { create(:user, email: 'agent1@example.com', account: account) }
let(:class_instance) { described_class.new }
let(:email_channel) { create(:channel_email, account: account) }
before do
allow(described_class).to receive(:new).and_return(class_instance)
allow(class_instance).to receive(:smtp_config_set_or_development?).and_return(true)
end
context 'with summary' do
let(:conversation) { create(:conversation, account: account, assignee: agent) }
let(:message) do
create(:message,
account: account,
conversation: conversation,
content_attributes: {
cc_emails: 'agent_cc1@example.com',
bcc_emails: 'agent_bcc1@example.com'
})
end
let(:new_message) do
create(:message,
account: account,
conversation: conversation,
content_attributes: {
cc_emails: 'agent_cc2@example.com',
bcc_emails: 'agent_bcc2@example.com'
})
end
let(:cc_message) do
create(:message,
account: account,
message_type: :outgoing,
conversation: conversation,
content_attributes: {
cc_emails: 'agent_cc1@example.com',
bcc_emails: 'agent_bcc1@example.com'
})
end
let(:private_message) { create(:message, account: account, content: 'This is a private message', conversation: conversation) }
let(:mail) { described_class.reply_with_summary(message.conversation, message.id).deliver_now }
let(:cc_mail) { described_class.reply_with_summary(cc_message.conversation, message.id).deliver_now }
it 'renders the default subject' do
expect(mail.subject).to eq("[##{message.conversation.display_id}] New messages on this conversation")
end
it 'renders the subject in conversation as reply' do
conversation.additional_attributes = { 'mail_subject': 'Mail Subject' }
conversation.save!
new_message.save!
expect(mail.subject).to eq('Re: Mail Subject')
end
it 'not have private notes' do
# make the message private
private_message.private = true
private_message.save!
expect(mail.body.decoded).not_to include(private_message.content)
expect(mail.body.decoded).to include(message.content)
end
it 'will not send email if conversation is already viewed by contact' do
create(:message, message_type: 'outgoing', account: account, conversation: conversation)
conversation.update(contact_last_seen_at: Time.zone.now)
expect(mail).to be_nil
end
it 'will send email to cc and bcc email addresses' do
expect(cc_mail.cc.first).to eq(cc_message.content_attributes[:cc_emails])
expect(cc_mail.bcc.first).to eq(cc_message.content_attributes[:bcc_emails])
end
end
context 'without assignee' do
let(:conversation) { create(:conversation, assignee: nil) }
let(:message) { create(:message, message_type: :outgoing, conversation: conversation) }
let(:mail) { described_class.reply_with_summary(message.conversation, message.id).deliver_now }
it 'has correct name' do
expect(mail[:from].display_names).to eq(["#{message.sender.available_name} from #{message.conversation.inbox.sanitized_name}"])
end
end
context 'without summary' do
let(:conversation) { create(:conversation, assignee: agent, account: account).reload }
let(:message_1) { create(:message, conversation: conversation, account: account, content: 'Outgoing Message 1').reload }
let(:message_2) { build(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
let(:private_message) do
create(:message,
content: 'This is a private message',
conversation: conversation,
account: account,
message_type: 'outgoing').reload
end
let(:mail) { described_class.reply_without_summary(message_2.conversation, message_2.id).deliver_now }
before do
message_2.save!
end
it 'renders the default subject' do
expect(mail.subject).to eq("[##{message_2.conversation.display_id}] New messages on this conversation")
end
it 'renders the subject in conversation' do
conversation.additional_attributes = { 'mail_subject': 'Mail Subject' }
conversation.save!
expect(mail.subject).to eq('Mail Subject')
end
it 'not have private notes' do
# make the message private
private_message.private = true
private_message.save!
expect(mail.body.decoded).not_to include(private_message.content)
end
it 'onlies have the messages sent by the agent' do
expect(mail.body.decoded).not_to include(message_1.content)
expect(mail.body.decoded).to include(message_2.content)
end
it 'will not send email if conversation is already viewed by contact' do
create(:message, message_type: 'outgoing', account: account, conversation: conversation)
conversation.update(contact_last_seen_at: Time.zone.now)
expect(mail).to be_nil
end
end
context 'without summary for a non-email inbox' do
let(:inbox) { create(:inbox, account: account, channel: create(:channel_widget, account: account)) }
let(:conversation) { create(:conversation, assignee: agent, account: account, inbox: inbox) }
let!(:incoming_email_message) do
create(:message, conversation: conversation, account: account, message_type: :incoming, content_type: :incoming_email)
end
let!(:outgoing_message) do
create(:message, conversation: conversation, account: account, message_type: :outgoing, content: 'Outgoing email reply')
end
let(:mail) { described_class.reply_without_summary(conversation, incoming_email_message.id).deliver_now }
it 'applies the account branded email layout' do
account.enable_features!(:branded_email_templates)
create(:email_template, :layout, account: account, body: '<html><body>Account Brand {{ content_for_layout }}</body></html>')
expect(mail.decoded).to include('Account Brand')
expect(mail.decoded).to include(outgoing_message.content)
end
it 'does not apply an installation layout without an account override' do
account.enable_features!(:branded_email_templates)
create(:email_template, :layout, body: '<html><body>Installation Brand {{ content_for_layout }}</body></html>')
expect(mail.decoded).not_to include('Installation Brand')
expect(mail.decoded).to include(outgoing_message.content)
end
end
context 'with references header' do
let(:conversation) { create(:conversation, assignee: agent, inbox: email_channel.inbox, account: account).reload }
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
let(:mail) { described_class.email_reply(message).deliver_now }
context 'when starting a new conversation' do
let(:first_outgoing_message) do
create(:message,
conversation: conversation,
account: account,
message_type: 'outgoing',
content: 'First outgoing message')
end
let(:mail) { described_class.email_reply(first_outgoing_message).deliver_now }
it 'has only the conversation reference' do
# When starting a conversation, references will have the default conversation ID
# Extract domain from the actual references header to handle dynamic domain selection
actual_domain = mail.references.split('@').last
expected_reference = "account/#{account.id}/conversation/#{conversation.uuid}@#{actual_domain}"
expect(mail.references).to eq(expected_reference)
end
end
context 'when replying to a message with no references' do
let(:incoming_message) do
create(:message,
conversation: conversation,
account: account,
message_type: 'incoming',
source_id: '<incoming-123@example.com>',
content: 'Incoming message',
content_attributes: {
'email' => {
'message_id' => 'incoming-123@example.com'
}
})
end
let(:reply_message) do
create(:message,
conversation: conversation,
account: account,
message_type: 'outgoing',
content: 'Reply to incoming')
end
let(:mail) { described_class.email_reply(reply_message).deliver_now }
before do
incoming_message
end
it 'includes only the in_reply_to id in references' do
# References should only have the incoming message ID when no prior references exist
expect(mail.references).to eq('incoming-123@example.com')
end
end
context 'when replying to a message that has references' do
let(:incoming_message_with_refs) do
create(:message,
conversation: conversation,
account: account,
message_type: 'incoming',
source_id: '<incoming-456@example.com>',
content: 'Incoming with references',
content_attributes: {
'email' => {
'message_id' => 'incoming-456@example.com',
'references' => ['<ref-1@example.com>', '<ref-2@example.com>']
}
})
end
let(:reply_message) do
create(:message,
conversation: conversation,
account: account,
message_type: 'outgoing',
content: 'Reply to message with refs')
end
let(:mail) { described_class.email_reply(reply_message).deliver_now }
before do
incoming_message_with_refs
end
it 'includes existing references plus the in_reply_to id' do
# Rails returns references as an array when multiple values are present
expected_references = ['ref-1@example.com', 'ref-2@example.com', 'incoming-456@example.com']
expect(mail.references).to eq(expected_references)
end
end
end
context 'with email reply' do
let(:conversation) { create(:conversation, assignee: agent, inbox: email_channel.inbox, account: account).reload }
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
let(:mail) { described_class.email_reply(message).deliver_now }
it 'renders the subject' do
expect(mail.subject).to eq("[##{message.conversation.display_id}] New messages on this conversation")
end
it 'renders the body' do
expect(mail.decoded).to include message.content
end
it 'does not apply branded email layout when feature is disabled' do
create(
:email_template,
:layout,
account: account,
inbox: conversation.inbox,
body: '<html><body>Inbox Brand {{ content_for_layout }}</body></html>'
)
expect(mail.decoded).not_to include('Inbox Brand')
expect(mail.decoded).to include(message.content)
end
it 'exposes the reply sender in inbox branded email layouts' do
account.enable_features!(:branded_email_templates)
conversation.inbox.update!(business_name: 'Acme Support')
create(
:email_template,
:layout,
account: account,
inbox: conversation.inbox,
body: [
'<html><body><header>{{ inbox.business_name }}</header>',
'{{ content_for_layout }}',
'<span>{{ agent.email }}</span>',
'<footer>{{ message.sender_display_name }}</footer></body></html>'
].join
)
expect(mail.decoded).to include('Acme Support')
expect(mail.decoded).to include(message.content)
expect(message.sender).not_to eq(agent)
expect(mail.decoded).to include(message.sender.email)
expect(mail.decoded).to include(message.sender.available_name)
end
it 'falls back to account branded email layout when inbox layout is absent' do
account.enable_features!(:branded_email_templates)
create(
:email_template,
:layout,
account: account,
body: '<html><body>Account Brand {{ content_for_layout }}</body></html>'
)
expect(mail.decoded).to include('Account Brand')
expect(mail.decoded).to include(message.content)
end
it 'applies inbox branded email layout to template messages' do
account.enable_features!(:branded_email_templates)
create(
:email_template,
:layout,
account: account,
inbox: conversation.inbox,
body: '<html><body>Template Brand {{ content_for_layout }}</body></html>'
)
template_message = create(:message, conversation: conversation, account: account, message_type: :template, content_type: :text,
content: 'Automation template response', sender: agent)
template_mail = described_class.email_reply(template_message).deliver_now
expect(template_mail.decoded).to include('Template Brand')
expect(template_mail.decoded).to include('Automation template response')
end
it 'builds messageID properly' do
expect(mail.message_id).to eq("conversation/#{conversation.uuid}/messages/#{message.id}@#{conversation.account.domain}")
end
context 'when a newer outgoing message exists in the conversation' do
let!(:message) do
create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Looping in the vendor',
content_attributes: { to_emails: ['customer@example.com'], cc_emails: ['vendor@example.com'],
bcc_emails: ['audit@example.com'] })
end
it 'sends to the recipients of the message being delivered' do
# a private note added right after the reply carries empty recipient lists
create(:message, conversation: conversation, account: account, message_type: 'outgoing', private: true,
content: 'Vendor has been looped in',
content_attributes: { to_emails: [], cc_emails: [], bcc_emails: [] })
expect(mail.to).to eq(message.content_attributes[:to_emails])
expect(mail.cc).to eq(message.content_attributes[:cc_emails])
expect(mail.bcc).to eq(message.content_attributes[:bcc_emails])
end
end
context 'when message is a CSAT survey' do
let(:csat_message) do
create(:message, conversation: conversation, account: account, message_type: 'template',
content_type: 'input_csat', content: 'How would you rate our support?', sender: agent)
end
it 'includes CSAT survey URL in outgoing_content' do
with_modified_env 'FRONTEND_URL' => 'https://app.chatwoot.com' do
mail = described_class.email_reply(csat_message).deliver_now
expect(mail.decoded).to include "https://app.chatwoot.com/survey/responses/#{conversation.uuid}"
end
end
it 'uses outgoing_content for CSAT message body' do
with_modified_env 'FRONTEND_URL' => 'https://app.chatwoot.com' do
mail = described_class.email_reply(csat_message).deliver_now
expect(mail.decoded).to include csat_message.outgoing_content
end
end
end
context 'with email attachments' do
it 'includes small attachments as email attachments' do
message_with_attachment = create(:message, conversation: conversation, account: account, message_type: 'outgoing',
content: 'Message with small attachment')
attachment = message_with_attachment.attachments.new(account_id: account.id, file_type: :file)
attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
attachment.save!
mail = described_class.email_reply(message_with_attachment).deliver_now
# Should be attached to the email
expect(mail.attachments.map(&:filename).map(&:to_s)).to include('avatar.png')
# Should not be in large_attachments
expect(mail.body.encoded).not_to include('Attachments:')
end
it 'renders large attachments as links in the email body' do
message_with_large_attachment = create(:message, conversation: conversation, account: account, message_type: 'outgoing',
content: 'Message with large attachment')
attachment = message_with_large_attachment.attachments.new(account_id: account.id, file_type: :file)
attachment.file.attach(io: Rails.root.join('spec/assets/large_file.pdf').open, filename: 'large_file.pdf', content_type: 'application/pdf')
attachment.save!
mail = described_class.email_reply(message_with_large_attachment).deliver_now
# Should NOT be attached to the email
expect(mail.attachments.map(&:filename).map(&:to_s)).not_to include('large_file.pdf')
# Should be rendered as a link in the body
expect(mail.body.encoded).to include('Attachments:')
expect(mail.body.encoded).to include('large_file.pdf')
# Should render a link with large_file.pdf as the link text
expect(mail.body.encoded).to match(%r{<a [^>]*>large_file\.pdf</a>})
# Small file should not be rendered as a link in the body
expect(mail.body.encoded).not_to match(%r{<a [^>]*>avatar\.png</a>})
end
it 'handles both small and large attachments correctly' do
message_with_mixed_attachments = create(:message, conversation: conversation, account: account, message_type: 'outgoing',
content: 'Message with mixed attachments')
# Small attachment
small_attachment = message_with_mixed_attachments.attachments.new(account_id: account.id, file_type: :file)
small_attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
small_attachment.save!
# Large attachment
large_attachment = message_with_mixed_attachments.attachments.new(account_id: account.id, file_type: :file)
large_attachment.file.attach(io: Rails.root.join('spec/assets/large_file.pdf').open, filename: 'large_file.pdf',
content_type: 'application/pdf')
large_attachment.save!
mail = described_class.email_reply(message_with_mixed_attachments).deliver_now
# Small file should be attached
expect(mail.attachments.map(&:filename).map(&:to_s)).to include('avatar.png')
# Large file should NOT be attached
expect(mail.attachments.map(&:filename).map(&:to_s)).not_to include('large_file.pdf')
# Large file should be rendered as a link in the body
expect(mail.body.encoded).to include('Attachments:')
expect(mail.body.encoded).to include('large_file.pdf')
# Should render a link with large_file.pdf as the link text
expect(mail.body.encoded).to match(%r{<a [^>]*>large_file\.pdf</a>})
# Small file should not be rendered as a link in the body
expect(mail.body.encoded).not_to match(%r{<a [^>]*>avatar\.png</a>})
end
end
context 'with custom email content' do
it 'uses custom HTML content when available and creates multipart email' do
message_with_custom_content = create(:message,
conversation: conversation,
account: account,
message_type: 'outgoing',
content: 'Regular message content',
content_attributes: {
email: {
html_content: {
reply: '<p>Custom <strong>HTML</strong> content for email</p>'
},
text_content: {
reply: 'Custom text content for email'
}
}
})
mail = described_class.email_reply(message_with_custom_content).deliver_now
# Check HTML part contains custom HTML content
html_part = mail.html_part || mail
expect(html_part.body.encoded).to include('<p>Custom <strong>HTML</strong> content for email</p>')
expect(html_part.body.encoded).not_to include('Regular message content')
# Check text part contains custom text content
text_part = mail.text_part
if text_part
expect(text_part.body.encoded).to include('Custom text content for email')
expect(text_part.body.encoded).not_to include('Regular message content')
end
end
it 'falls back to markdown rendering when custom HTML content is not available' do
message_without_custom_content = create(:message,
conversation: conversation,
account: account,
message_type: 'outgoing',
content: 'Regular **markdown** content')
mail = described_class.email_reply(message_without_custom_content).deliver_now
html_part = mail.html_part || mail
expect(html_part.body.encoded).to include('<strong>markdown</strong>')
expect(html_part.body.encoded).to include('Regular')
end
it 'handles empty custom HTML content gracefully' do
message_with_empty_content = create(:message,
conversation: conversation,
account: account,
message_type: 'outgoing',
content: 'Regular **markdown** content',
content_attributes: {
email: {
html_content: {
reply: ''
}
}
})
mail = described_class.email_reply(message_with_empty_content).deliver_now
html_part = mail.html_part || mail
expect(html_part.body.encoded).to include('<strong>markdown</strong>')
expect(html_part.body.encoded).to include('Regular')
end
it 'handles nil custom HTML content gracefully' do
message_with_nil_content = create(:message,
conversation: conversation,
account: account,
message_type: 'outgoing',
content: 'Regular **markdown** content',
content_attributes: {
email: {
html_content: {
reply: nil
}
}
})
mail = described_class.email_reply(message_with_nil_content).deliver_now
expect(mail.body.encoded).to include('<strong>markdown</strong>')
expect(mail.body.encoded).to include('Regular')
end
it 'uses custom text content in text part when only text is provided' do
message_with_text_only = create(:message,
conversation: conversation,
account: account,
message_type: 'outgoing',
content: 'Regular message content',
content_attributes: {
email: {
text_content: {
reply: 'Custom text content only'
}
}
})
mail = described_class.email_reply(message_with_text_only).deliver_now
text_part = mail.text_part
if text_part
expect(text_part.body.encoded).to include('Custom text content only')
expect(text_part.body.encoded).not_to include('Regular message content')
end
end
end
end
context 'when smtp enabled for email channel' do
let(:smtp_channel) do
create(:channel_email, smtp_enabled: true, smtp_address: 'smtp.gmail.com', smtp_port: 587, smtp_login: 'smtp@gmail.com',
smtp_password: 'password', smtp_domain: 'smtp.gmail.com', account: account)
end
let(:conversation) { create(:conversation, assignee: agent, inbox: smtp_channel.inbox, account: account).reload }
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
it 'use smtp mail server' do
mail = described_class.email_reply(message)
expect(mail.delivery_method.settings.empty?).to be false
expect(mail.delivery_method.settings[:address]).to eq 'smtp.gmail.com'
expect(mail.delivery_method.settings[:port]).to eq 587
expect(mail.delivery_method.settings[:open_timeout]).to eq 15
expect(mail.delivery_method.settings[:read_timeout]).to eq 30
end
it 'uses configured smtp timeout values' do
with_modified_env SMTP_OPEN_TIMEOUT: '10', SMTP_READ_TIMEOUT: '30' do
mail = described_class.email_reply(message)
expect(mail.delivery_method.settings[:open_timeout]).to eq 10
expect(mail.delivery_method.settings[:read_timeout]).to eq 30
end
end
it 'uses default smtp timeout values when env values are blank' do
with_modified_env SMTP_OPEN_TIMEOUT: '', SMTP_READ_TIMEOUT: '' do
mail = described_class.email_reply(message)
expect(mail.delivery_method.settings[:open_timeout]).to eq 15
expect(mail.delivery_method.settings[:read_timeout]).to eq 30
end
end
it 'renders sender name in the from address' do
mail = described_class.email_reply(message)
expect(mail['from'].value).to eq "#{message.sender.available_name} from #{smtp_channel.inbox.sanitized_name} <#{smtp_channel.email}>"
end
it 'renders sender name even when assignee is not present' do
conversation.update(assignee_id: nil)
mail = described_class.email_reply(message)
expect(mail['from'].value).to eq "#{message.sender.available_name} from #{smtp_channel.inbox.sanitized_name} <#{smtp_channel.email}>"
end
it 'renders assignee name in the from address when sender_name not available' do
message.update(sender_id: nil)
mail = described_class.email_reply(message)
expect(mail['from'].value).to eq "#{conversation.assignee.available_name} from #{smtp_channel.inbox.sanitized_name} <#{smtp_channel.email}>"
end
it 'renders inbox name as sender and assignee or business_name not present' do
message.update(sender_id: nil)
conversation.update(assignee_id: nil)
mail = described_class.email_reply(message)
expect(mail['from'].value).to eq "Notifications from #{smtp_channel.inbox.sanitized_name} <#{smtp_channel.email}>"
end
context 'when friendly name enabled' do
before do
conversation.inbox.update(sender_name_type: 0)
conversation.inbox.update(business_name: 'Business Name')
end
it 'renders sender name as sender and assignee and business_name not present' do
message.update(sender_id: nil)
conversation.update(assignee_id: nil)
conversation.inbox.update(business_name: nil)
mail = described_class.email_reply(message)
expect(mail['from'].value).to eq "Notifications from #{conversation.inbox.sanitized_name} <#{smtp_channel.email}>"
end
it 'renders sender name as sender and assignee nil and business_name present' do
message.update(sender_id: nil)
conversation.update(assignee_id: nil)
mail = described_class.email_reply(message)
expect(mail['from'].value).to eq(
"Notifications from #{conversation.inbox.business_name} <#{smtp_channel.email}>"
)
end
it 'renders sender name as sender nil and assignee and business_name present' do
message.update(sender_id: nil)
conversation.update(assignee_id: agent.id)
mail = described_class.email_reply(message)
expect(mail['from'].value).to eq "#{agent.available_name} from #{conversation.inbox.business_name} <#{smtp_channel.email}>"
end
it 'renders sender name as sender and assignee and business_name present' do
agent_2 = create(:user, email: 'agent2@example.com', account: account)
message.update(sender_id: agent_2.id)
conversation.update(assignee_id: agent.id)
mail = described_class.email_reply(message)
expect(mail['from'].value).to eq "#{agent_2.available_name} from #{conversation.inbox.business_name} <#{smtp_channel.email}>"
end
end
context 'when friendly name disabled' do
before do
conversation.inbox.update(sender_name_type: 1)
conversation.inbox.update(business_name: 'Business Name')
end
it 'renders sender name as business_name not present' do
message.update(sender_id: nil)
conversation.update(assignee_id: nil)
conversation.inbox.update(business_name: nil)
mail = described_class.email_reply(message)
expect(mail['from'].value).to eq "#{conversation.inbox.sanitized_name} <#{smtp_channel.email}>"
end
it 'renders sender name as business_name present' do
message.update(sender_id: nil)
conversation.update(assignee_id: nil)
mail = described_class.email_reply(message)
expect(mail['from'].value).to eq "#{conversation.inbox.business_name} <#{smtp_channel.email}>"
end
end
end
context 'when smtp enabled for microsoft email channel' do
let(:ms_smtp_channel) do
create(:channel_email, imap_login: 'smtp@outlook.com',
imap_enabled: true, account: account, provider: 'microsoft', provider_config: { access_token: 'access_token' })
end
let(:conversation) { create(:conversation, assignee: agent, inbox: ms_smtp_channel.inbox, account: account).reload }
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
it 'use smtp mail server' do
mail = described_class.email_reply(message)
expect(mail.delivery_method.settings.empty?).to be false
expect(mail.delivery_method.settings[:address]).to eq 'smtp.office365.com'
expect(mail.delivery_method.settings[:port]).to eq 587
end
end
context 'when smtp enabled for google email channel' do
let(:ms_smtp_channel) do
create(:channel_email, imap_login: 'smtp@gmail.com',
imap_enabled: true, account: account, provider: 'google', provider_config: { access_token: 'access_token' })
end
let(:conversation) { create(:conversation, assignee: agent, inbox: ms_smtp_channel.inbox, account: account).reload }
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
it 'use smtp mail server' do
mail = described_class.email_reply(message)
expect(mail.delivery_method.settings.empty?).to be false
expect(mail.delivery_method.settings[:address]).to eq 'smtp.gmail.com'
expect(mail.delivery_method.settings[:port]).to eq 587
end
it 'uses inbox oauth smtp when global smtp config is unavailable' do
allow(class_instance).to receive(:smtp_config_set_or_development?).and_return(false)
mail = described_class.email_reply(message)
expect(mail).not_to be_nil
expect(mail.delivery_method.settings[:address]).to eq 'smtp.gmail.com'
expect(mail.delivery_method.settings[:port]).to eq 587
end
end
context 'when oauth provider is set but imap is disabled' do
let(:google_channel) do
create(:channel_email, imap_enabled: false, account: account, provider: 'google', provider_config: { access_token: 'access_token' })
end
let(:conversation) { create(:conversation, assignee: agent, inbox: google_channel.inbox, account: account).reload }
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
it 'does not build the mail without global smtp' do
allow(class_instance).to receive(:smtp_config_set_or_development?).and_return(false)
expect(described_class.email_reply(message).deliver_now).to be_nil
end
end
context 'when smtp disabled for email channel', :test do
let(:conversation) { create(:conversation, assignee: agent, inbox: email_channel.inbox, account: account).reload }
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
it 'use default mail server' do
mail = described_class.email_reply(message)
expect(mail.delivery_method.settings).to be_empty
end
end
context 'when custom domain and email is not enabled' do
let(:inbox) { create(:inbox, account: account) }
let(:inbox_member) { create(:inbox_member, user: agent, inbox: inbox) }
let(:conversation) { create(:conversation, assignee: agent, inbox: inbox_member.inbox, account: account) }
let!(:message) { create(:message, conversation: conversation, account: account) }
let(:mail) { described_class.reply_with_summary(message.conversation, message.id).deliver_now }
let(:domain) { account.inbound_email_domain }
it 'renders the receiver email' do
expect(mail.to).to eq([message&.conversation&.contact&.email])
end
it 'renders the reply to email' do
expect(mail.reply_to).to eq([message&.conversation&.assignee&.email])
end
it 'sets the correct custom message id' do
expect(mail.message_id).to eq("conversation/#{conversation.uuid}/messages/#{message.id}@#{domain}")
end
it 'sets the correct in reply to id' do
expect(mail.in_reply_to).to eq("account/#{conversation.account.id}/conversation/#{conversation.uuid}@#{domain}")
end
end
context 'when inbox email address is available' do
let(:inbox) { create(:inbox, account: account, email_address: 'noreply@chatwoot.com') }
let(:conversation) { create(:conversation, assignee: agent, inbox: inbox, account: account) }
let!(:message) { create(:message, conversation: conversation, account: account) }
let(:mail) { described_class.reply_with_summary(message.conversation, message.id).deliver_now }
it 'set reply to email address as inbox email address' do
expect(mail.from).to eq([inbox.email_address])
expect(mail.reply_to).to eq([inbox.email_address])
end
end
context 'when the custom domain emails are enabled' do
let(:account) { create(:account) }
let(:conversation) { create(:conversation, assignee: agent, account: account).reload }
let(:message) { create(:message, message_type: :outgoing, conversation: conversation, account: account, inbox: conversation.inbox) }
let(:mail) { described_class.reply_with_summary(message.conversation, message.id).deliver_now }
before do
account = conversation.account
account.domain = 'example.com'
account.support_email = 'support@example.com'
account.enable_features('inbound_emails')
account.save!
end
it 'sets reply to email to be based on the domain' do
reply_to_email = "reply+#{message.conversation.uuid}@#{conversation.account.domain}"
reply_to = "#{message.sender.available_name} from #{conversation.inbox.sanitized_name} <#{reply_to_email}>"
expect(mail['REPLY-TO'].value).to eq(reply_to)
expect(mail.reply_to).to eq([reply_to_email])
end
it 'sets the from email to be the support email' do
expect(mail['FROM'].value).to eq("#{conversation.messages.last.sender.available_name} from Inbox <#{conversation.account.support_email}>")
expect(mail.from).to eq([conversation.account.support_email])
end
it 'sets the correct custom message id' do
expect(mail.message_id).to eq("conversation/#{conversation.uuid}/messages/#{message.id}@#{conversation.account.domain}")
end
it 'sets the correct in reply to id' do
expect(mail.in_reply_to).to eq("account/#{conversation.account.id}/conversation/#{conversation.uuid}@#{conversation.account.domain}")
end
end
context 'when inbound email domain is not enabled' do
let(:new_account) { create(:account, domain: nil) }
let!(:email_channel) { create(:channel_email, account: new_account) }
let!(:inbox) { create(:inbox, channel: email_channel, account: new_account) }
let(:inbox_member) { create(:inbox_member, user: agent, inbox: inbox) }
let(:conversation) { create(:conversation, assignee: agent, inbox: inbox_member.inbox, account: new_account) }
let!(:message) { create(:message, conversation: conversation, account: new_account) }
let(:mail) { described_class.reply_with_summary(message.conversation, message.id).deliver_now }
let(:domain) { inbox.channel.email.split('@').last }
it 'sets the correct custom message id' do
expect(mail.message_id).to eq("conversation/#{conversation.uuid}/messages/#{message.id}@#{domain}")
end
it 'sets the correct in reply to id' do
expect(mail.in_reply_to).to eq("account/#{conversation.account.id}/conversation/#{conversation.uuid}@#{domain}")
end
it 'applies inbox branded email layout to conversation transcript' do
new_account.enable_features!(:branded_email_templates)
create(
:email_template,
:layout,
account: new_account,
inbox: conversation.inbox,
body: '<html><body>Transcript Brand {{ content_for_layout }}</body></html>'
)
transcript = described_class.conversation_transcript(conversation, 'customer@example.com').deliver_now
expect(transcript.decoded).to include('Transcript Brand')
expect(transcript.decoded).to include(message.content)
end
end
end
end