Files
chatwoot/spec/mailers/conversation_reply_mailer_spec.rb
T
Sony MathewandGitHub c420edce58 feat: branded email layouts for email inboxes (#14936)
## Description

Adds an API-only branded email layout feature for Email inbox replies.
Administrators can configure an account-level fallback layout and
per-email-inbox overrides with Liquid HTML using `{{ content_for_layout
}}`, and eligible outbound email replies/transcripts render through the
scoped layout when the account feature flag `branded_email_templates` is
enabled.

The feature is disabled by default and is manually controlled through
the normal account feature flag mechanism.

Fixes
https://linear.app/chatwoot/issue/CW-7514/branded-html-email-templates-per-inboxbrand

## Type of change

- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update

## How to test

1. Start Chatwoot locally and sign in as an administrator.
2. Enable the account feature flag for the account you are testing:

   ```ruby
   account = Account.find(<account_id>)
   account.enable_features!(:branded_email_templates)
   ```

3. Create or pick an Email inbox, then note the `account_id` and
`inbox_id`.
4. Configure an account-level fallback layout through the API using
authenticated admin headers:

   ```http
   PATCH /api/v1/accounts/:account_id/branded_email_layout
   Content-Type: application/json

   {
"branded_email_layout": "<html><body><header>Account Brand</header>{{
content_for_layout }}<footer>Account footer</footer></body></html>"
   }
   ```

5. Confirm `GET /api/v1/accounts/:account_id/branded_email_layout`
returns the saved account layout.
6. Configure an inbox-level override for the Email inbox:

   ```http
   PATCH /api/v1/accounts/:account_id/inboxes/:inbox_id
   Content-Type: application/json

   {
"branded_email_layout": "<html><body><header>Inbox Brand</header>{{
content_for_layout }}<footer>Inbox footer</footer></body></html>"
   }
   ```

7. Confirm `GET /api/v1/accounts/:account_id/inboxes/:inbox_id` returns
the inbox `branded_email_layout`.
8. Send an Email inbox reply and verify the outbound email body is
wrapped with the inbox layout around the generated reply content.
9. Clear the inbox layout by sending a blank value, then send another
reply and verify it falls back to the account layout:

   ```http
   PATCH /api/v1/accounts/:account_id/inboxes/:inbox_id
   Content-Type: application/json

   {
     "branded_email_layout": ""
   }
   ```

10. Clear the account layout with a blank value and verify Email replies
return to the existing no-layout behavior.
11. Verify validation behavior:
- Updating either API with a layout that omits `{{ content_for_layout
}}` returns `422`.
    - Updating either API with invalid Liquid returns `422`.
- Updating a non-Email inbox with `branded_email_layout` returns `422`.
- Disabling `branded_email_templates` and updating a layout returns
`422`.

## How Has This Been Tested?

Validation:

- `bundle exec rspec spec/models/email_template_spec.rb
spec/controllers/api/v1/accounts/branded_email_layouts_controller_spec.rb
spec/controllers/api/v1/accounts/inboxes_controller_spec.rb
spec/lib/email_templates/db_resolver_service_spec.rb
spec/mailers/conversation_reply_mailer_spec.rb`
- `bundle exec rspec
spec/enterprise/services/enterprise/billing/handle_stripe_event_service_spec.rb
spec/enterprise/services/internal/reconcile_plan_config_service_spec.rb`
- `bundle exec rubocop` on changed Ruby files, excluding generated
`db/schema.rb`
- `git diff --check` and `git diff --cached --check`
- YAML parsing for changed config/Swagger files
- `bundle exec rails routes -g branded_email_layout`

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have made corresponding changes to the documentation
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] My changes generate no new warnings
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-07-17 15:25:55 +05:30

853 lines
39 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 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