Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
0
research/chatwoot/spec/mailers/.keep
Normal file
0
research/chatwoot/spec/mailers/.keep
Normal file
@@ -0,0 +1,34 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AdministratorNotifications::AccountComplianceMailer do
|
||||
let(:account) do
|
||||
create(:account, custom_attributes: { 'marked_for_deletion_at' => 1.day.ago.iso8601, 'marked_for_deletion_reason' => 'user_requested' })
|
||||
end
|
||||
let(:soft_deleted_users) do
|
||||
[
|
||||
{ id: 1, original_email: 'user1@example.com' },
|
||||
{ id: 2, original_email: 'user2@example.com' }
|
||||
]
|
||||
end
|
||||
|
||||
describe 'account_deleted' do
|
||||
it 'has the right subject format' do
|
||||
subject = described_class.new.send(:subject_for, account)
|
||||
expect(subject).to eq("Account Deletion Notice for #{account.id} - #{account.name}")
|
||||
end
|
||||
|
||||
it 'includes soft deleted users in meta when provided' do
|
||||
mailer_instance = described_class.new
|
||||
allow(mailer_instance).to receive(:params).and_return(
|
||||
{ soft_deleted_users: soft_deleted_users }
|
||||
)
|
||||
|
||||
meta = mailer_instance.send(:build_meta, account)
|
||||
|
||||
expect(meta['deleted_user_count']).to eq(2)
|
||||
expect(meta['soft_deleted_users'].size).to eq(2)
|
||||
expect(meta['soft_deleted_users'].first['user_id']).to eq('1')
|
||||
expect(meta['soft_deleted_users'].first['user_email']).to eq('user1@example.com')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AdministratorNotifications::AccountNotificationMailer do
|
||||
let(:account) { create(:account, name: 'Test Account') }
|
||||
let(:mailer) { described_class.with(account: account) }
|
||||
let(:class_instance) { described_class.new }
|
||||
|
||||
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)
|
||||
account.custom_attributes['marked_for_deletion_at'] = 7.days.from_now.iso8601
|
||||
account.save!
|
||||
end
|
||||
|
||||
describe '#account_deletion_user_initiated' do
|
||||
it 'sets the correct subject for user-initiated deletion' do
|
||||
mail = mailer.account_deletion_user_initiated(account, 'manual_deletion')
|
||||
expect(mail.subject).to eq('Your Chatwoot account deletion has been scheduled')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#account_deletion_for_inactivity' do
|
||||
it 'sets the correct subject for system-initiated deletion' do
|
||||
mail = mailer.account_deletion_for_inactivity(account, 'Account Inactive')
|
||||
expect(mail.subject).to eq('Your Chatwoot account is scheduled for deletion due to inactivity')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#format_deletion_date' do
|
||||
it 'formats a valid date string' do
|
||||
date_str = '2024-12-31T12:00:00Z'
|
||||
formatted = described_class.new.send(:format_deletion_date, date_str)
|
||||
expect(formatted).to eq('December 31, 2024')
|
||||
end
|
||||
|
||||
it 'handles blank dates' do
|
||||
formatted = described_class.new.send(:format_deletion_date, nil)
|
||||
expect(formatted).to eq('Unknown')
|
||||
end
|
||||
|
||||
it 'handles invalid dates' do
|
||||
formatted = described_class.new.send(:format_deletion_date, 'invalid-date')
|
||||
expect(formatted).to eq('Unknown')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,74 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AdministratorNotifications::BaseMailer do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:admin1) { create(:user, account: account, role: :administrator) }
|
||||
let!(:admin2) { create(:user, account: account, role: :administrator) }
|
||||
let!(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:mailer) { described_class.new }
|
||||
let!(:inbox) { create(:inbox, account: account) }
|
||||
|
||||
before do
|
||||
Current.account = account
|
||||
end
|
||||
|
||||
describe 'admin_emails' do
|
||||
it 'returns emails of all administrators' do
|
||||
# Call the private method
|
||||
admin_emails = mailer.send(:admin_emails)
|
||||
|
||||
expect(admin_emails).to contain_exactly(admin1.email, admin2.email)
|
||||
expect(admin_emails).not_to include(agent.email)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'helper methods' do
|
||||
it 'generates correct inbox URL' do
|
||||
url = mailer.inbox_url(inbox)
|
||||
expected_url = "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/inboxes/#{inbox.id}"
|
||||
expect(url).to eq(expected_url)
|
||||
end
|
||||
|
||||
it 'generates correct settings URL' do
|
||||
url = mailer.settings_url('automation/list')
|
||||
expected_url = "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/automation/list"
|
||||
expect(url).to eq(expected_url)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'send_notification' do
|
||||
before do
|
||||
allow(mailer).to receive(:smtp_config_set_or_development?).and_return(true)
|
||||
end
|
||||
|
||||
it 'sends email with correct parameters' do
|
||||
subject = 'Test Subject'
|
||||
action_url = 'https://example.com'
|
||||
meta = { 'key' => 'value' }
|
||||
|
||||
# Mock the send_mail_with_liquid method
|
||||
expect(mailer).to receive(:send_mail_with_liquid).with(
|
||||
to: contain_exactly(admin1.email, admin2.email),
|
||||
subject: subject
|
||||
).and_return(true)
|
||||
|
||||
mailer.send_notification(subject, action_url: action_url, meta: meta)
|
||||
|
||||
# Check that instance variables are set correctly
|
||||
expect(mailer.instance_variable_get(:@action_url)).to eq(action_url)
|
||||
expect(mailer.instance_variable_get(:@meta)).to eq(meta)
|
||||
end
|
||||
|
||||
it 'uses provided email addresses when specified' do
|
||||
subject = 'Test Subject'
|
||||
custom_email = 'custom@example.com'
|
||||
|
||||
expect(mailer).to receive(:send_mail_with_liquid).with(
|
||||
to: custom_email,
|
||||
subject: subject
|
||||
).and_return(true)
|
||||
|
||||
mailer.send_notification(subject, to: custom_email)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
require Rails.root.join 'spec/mailers/administrator_notifications/shared/smtp_config_shared.rb'
|
||||
|
||||
RSpec.describe AdministratorNotifications::ChannelNotificationsMailer do
|
||||
include_context 'with smtp config'
|
||||
|
||||
let(:class_instance) { described_class.new }
|
||||
let!(:account) { create(:account) }
|
||||
let!(:administrator) { create(:user, :administrator, email: 'agent1@example.com', account: account) }
|
||||
let!(:another_administrator) { create(:user, :administrator, email: 'agent2@example.com', account: account) }
|
||||
|
||||
describe 'facebook_disconnect' do
|
||||
before do
|
||||
stub_request(:post, /graph.facebook.com/)
|
||||
end
|
||||
|
||||
let!(:facebook_channel) { create(:channel_facebook_page, account: account) }
|
||||
let!(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: account) }
|
||||
|
||||
context 'when sending the actual email' do
|
||||
let(:mail) { described_class.with(account: account).facebook_disconnect(facebook_inbox).deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq('Your Facebook page connection has expired')
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to contain_exactly(administrator.email, another_administrator.email)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'whatsapp_disconnect' do
|
||||
let!(:whatsapp_channel) { create(:channel_whatsapp, provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false) }
|
||||
let!(:whatsapp_inbox) { create(:inbox, channel: whatsapp_channel, account: account) }
|
||||
let(:mail) { described_class.with(account: account).whatsapp_disconnect(whatsapp_inbox).deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq('Your Whatsapp connection has expired')
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to contain_exactly(administrator.email, another_administrator.email)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'instagram_disconnect' do
|
||||
let!(:instagram_channel) { create(:channel_instagram, account: account) }
|
||||
let!(:instagram_inbox) { create(:inbox, channel: instagram_channel, account: account) }
|
||||
let(:mail) { described_class.with(account: account).instagram_disconnect(instagram_inbox).deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq('Your Instagram connection has expired')
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to contain_exactly(administrator.email, another_administrator.email)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,42 @@
|
||||
require 'rails_helper'
|
||||
require Rails.root.join 'spec/mailers/administrator_notifications/shared/smtp_config_shared.rb'
|
||||
|
||||
RSpec.describe AdministratorNotifications::IntegrationsNotificationMailer do
|
||||
include_context 'with smtp config'
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
let!(:administrator) { create(:user, :administrator, email: 'admin@example.com', account: account) }
|
||||
let!(:another_administrator) { create(:user, :administrator, email: 'owner@example.com', account: account) }
|
||||
|
||||
describe 'slack_disconnect' do
|
||||
let(:mail) { described_class.with(account: account).slack_disconnect.deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq('Your Slack integration has expired')
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to contain_exactly(administrator.email, another_administrator.email)
|
||||
end
|
||||
|
||||
it 'includes reconnect instructions in the body' do
|
||||
expect(mail.body.encoded).to include('To continue receiving messages on Slack, please delete the integration and connect your workspace again')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'dialogflow_disconnect' do
|
||||
let(:mail) { described_class.with(account: account).dialogflow_disconnect.deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq('Your Dialogflow integration was disconnected')
|
||||
end
|
||||
|
||||
it 'renders the content' do
|
||||
expect(mail.body.encoded).to include('Your Dialogflow integration was disconnected because of permission issues')
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to contain_exactly(administrator.email, another_administrator.email)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_context 'with smtp config' do
|
||||
before do
|
||||
# We need to use allow_any_instance_of here because smtp_config_set_or_development?
|
||||
# is defined in ApplicationMailer and needs to be stubbed for all mailer instances
|
||||
# rubocop:disable RSpec/AnyInstance
|
||||
allow_any_instance_of(ApplicationMailer).to receive(:smtp_config_set_or_development?).and_return(true)
|
||||
# rubocop:enable RSpec/AnyInstance
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,108 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AgentNotifications::ConversationNotificationsMailer do
|
||||
let(:class_instance) { described_class.new }
|
||||
let!(:account) { create(:account) }
|
||||
let(:agent) { create(:user, email: 'agent1@example.com', account: account) }
|
||||
let(:conversation) { create(:conversation, assignee: agent, 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
|
||||
|
||||
describe 'conversation_creation' do
|
||||
let(:mail) { described_class.with(account: account).conversation_creation(conversation, agent, nil).deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq("#{agent.available_name}, A new conversation [ID - #{conversation
|
||||
.display_id}] has been created in #{conversation.inbox&.sanitized_name}.")
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([agent.email])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'conversation_assignment' do
|
||||
let(:mail) { described_class.with(account: account).conversation_assignment(conversation, agent, nil).deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq("#{agent.available_name}, A new conversation [ID - #{conversation.display_id}] has been assigned to you.")
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([agent.email])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'conversation_mention' do
|
||||
let(:contact) { create(:contact, name: nil, account: account) }
|
||||
let(:another_agent) { create(:user, email: 'agent2@example.com', account: account) }
|
||||
let(:message) { create(:message, conversation: conversation, account: account, sender: another_agent) }
|
||||
let(:mail) { described_class.with(account: account).conversation_mention(conversation, agent, message).deliver_now }
|
||||
let(:contact_inbox) { create(:contact_inbox, account: account, inbox: conversation.inbox) }
|
||||
|
||||
before do
|
||||
create(:message, conversation: conversation, account: account, sender: contact)
|
||||
create(:message, conversation: conversation, account: account, sender: contact)
|
||||
create(:message, conversation: conversation, account: account, sender: contact)
|
||||
end
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq("#{agent.available_name}, You have been mentioned in conversation [ID - #{conversation.display_id}]")
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([agent.email])
|
||||
end
|
||||
|
||||
it 'renders the senders name' do
|
||||
expect(mail.body.encoded).to match("You've been mentioned in a conversation. <b>#{another_agent.display_name}</b> wrote:")
|
||||
end
|
||||
|
||||
it 'renders Customer if contacts name not available in the conversation' do
|
||||
expect(contact.name).to be_nil
|
||||
expect(conversation.recent_messages).not_to be_empty
|
||||
expect(mail.body.encoded).to match('Incoming Message')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'assigned_conversation_new_message' do
|
||||
let(:message) { create(:message, conversation: conversation, account: account) }
|
||||
let(:mail) { described_class.with(account: account).assigned_conversation_new_message(conversation, agent, message).deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq("#{agent.available_name}, New message in your assigned conversation [ID - #{message.conversation.display_id}].")
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([agent.email])
|
||||
end
|
||||
|
||||
it 'will not send email if agent is online' do
|
||||
OnlineStatusTracker.update_presence(conversation.account.id, 'User', agent.id)
|
||||
expect(mail).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'participating_conversation_new_message' do
|
||||
let(:message) { create(:message, conversation: conversation, account: account) }
|
||||
let(:mail) { described_class.with(account: account).participating_conversation_new_message(conversation, agent, message).deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq("#{agent.available_name}, New message in your participating conversation [ID - #{message.conversation.display_id}].")
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
expect(mail.to).to eq([agent.email])
|
||||
end
|
||||
|
||||
it 'will not send email if agent is online' do
|
||||
OnlineStatusTracker.update_presence(conversation.account.id, 'User', agent.id)
|
||||
expect(mail).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,95 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Devise::Mailer' do
|
||||
describe 'notify' do
|
||||
let(:account) { create(:account) }
|
||||
let!(:confirmable_user) { create(:user, inviter: inviter_val, account: account) }
|
||||
let(:inviter_val) { nil }
|
||||
let(:mail) { Devise::Mailer.confirmation_instructions(confirmable_user.reload, nil, {}) }
|
||||
|
||||
before do
|
||||
# to verify the token in email
|
||||
confirmable_user.update!(confirmed_at: nil)
|
||||
confirmable_user.send(:generate_confirmation_token)
|
||||
end
|
||||
|
||||
it 'has the correct header data' do
|
||||
expect(mail.reply_to).to contain_exactly('accounts@chatwoot.com')
|
||||
expect(mail.to).to contain_exactly(confirmable_user.email)
|
||||
expect(mail.subject).to eq('Confirmation Instructions')
|
||||
end
|
||||
|
||||
it 'uses the user\'s name' do
|
||||
expect(mail.body).to match("Hi #{CGI.escapeHTML(confirmable_user.name)},")
|
||||
end
|
||||
|
||||
it 'does not refer to the inviter and their account' do
|
||||
expect(mail.body).not_to match('has invited you to try out Chatwoot!')
|
||||
expect(mail.body).to match('We have a suite of powerful tools ready for you to explore.')
|
||||
end
|
||||
|
||||
it 'sends a confirmation link' do
|
||||
expect(mail.body).to include("app/auth/confirmation?confirmation_token=#{confirmable_user.confirmation_token}")
|
||||
expect(mail.body).not_to include('app/auth/password/edit')
|
||||
end
|
||||
|
||||
context 'when there is an inviter' do
|
||||
let(:inviter_val) { create(:user, :administrator, skip_confirmation: true, account: account) }
|
||||
|
||||
it 'refers to the inviter and their account' do
|
||||
expect(mail.body).to match(
|
||||
"#{CGI.escapeHTML(inviter_val.name)}, with #{CGI.escapeHTML(account.name)}, has invited you to try out Chatwoot."
|
||||
)
|
||||
expect(mail.body).not_to match('We have a suite of powerful tools ready for you to explore.')
|
||||
end
|
||||
|
||||
it 'sends a password reset link' do
|
||||
expect(mail.body).to include('app/auth/password/edit?reset_password_token')
|
||||
expect(mail.body).not_to include('app/auth/confirmation')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user updates the email' do
|
||||
before do
|
||||
confirmable_user.update!(email: 'user@example.com')
|
||||
end
|
||||
|
||||
it 'sends a confirmation link' do
|
||||
confirmation_mail = Devise::Mailer.confirmation_instructions(confirmable_user.reload, nil, {})
|
||||
|
||||
expect(confirmation_mail.body).to include('app/auth/confirmation?confirmation_token')
|
||||
expect(confirmation_mail.body).not_to include('app/auth/password/edit')
|
||||
expect(confirmable_user.unconfirmed_email.blank?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is confirmed and updates the email' do
|
||||
before do
|
||||
confirmable_user.confirm
|
||||
confirmable_user.update!(email: 'user@example.com')
|
||||
end
|
||||
|
||||
it 'sends a confirmation link' do
|
||||
confirmation_mail = Devise::Mailer.confirmation_instructions(confirmable_user.reload, nil, {})
|
||||
|
||||
expect(confirmation_mail.body).to include('app/auth/confirmation?confirmation_token')
|
||||
expect(confirmation_mail.body).not_to include('app/auth/password/edit')
|
||||
expect(confirmable_user.unconfirmed_email.blank?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user already confirmed' do
|
||||
before do
|
||||
confirmable_user.confirm
|
||||
confirmable_user.account_users.last.destroy!
|
||||
end
|
||||
|
||||
it 'send instructions with the link to login' do
|
||||
confirmation_mail = Devise::Mailer.confirmation_instructions(confirmable_user.reload, nil, {})
|
||||
expect(confirmation_mail.body).to include('/auth/sign_in')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
697
research/chatwoot/spec/mailers/conversation_reply_mailer_spec.rb
Normal file
697
research/chatwoot/spec/mailers/conversation_reply_mailer_spec.rb
Normal file
@@ -0,0 +1,697 @@
|
||||
# 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 '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 '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
|
||||
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
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PortalInstructionsMailer do
|
||||
describe 'send_cname_instructions' do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:portal) { create(:portal, account: account, custom_domain: 'help.example.com') }
|
||||
let(:recipient_email) { 'admin@example.com' }
|
||||
let(:class_instance) { described_class.new }
|
||||
|
||||
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 'when target domain is available' do
|
||||
it 'sends email with cname instructions' do
|
||||
with_modified_env HELPCENTER_URL: 'https://help.chatwoot.com' do
|
||||
mail = described_class.send_cname_instructions(portal: portal, recipient_email: recipient_email).deliver_now
|
||||
|
||||
expect(mail.to).to eq([recipient_email])
|
||||
expect(mail.subject).to eq("Finish setting up #{portal.custom_domain}")
|
||||
expect(mail.body.encoded).to include('help.example.com CNAME help.chatwoot.com')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when helpcenter url is not available but frontend url is' do
|
||||
it 'uses frontend url as target domain' do
|
||||
with_modified_env HELPCENTER_URL: '', FRONTEND_URL: 'https://app.chatwoot.com' do
|
||||
mail = described_class.send_cname_instructions(portal: portal, recipient_email: recipient_email).deliver_now
|
||||
|
||||
expect(mail.to).to eq([recipient_email])
|
||||
expect(mail.body.encoded).to include('help.example.com CNAME app.chatwoot.com')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no target domain is available' do
|
||||
it 'does not send email' do
|
||||
with_modified_env HELPCENTER_URL: '', FRONTEND_URL: '' do
|
||||
mail = described_class.send_cname_instructions(portal: portal, recipient_email: recipient_email).deliver_now
|
||||
|
||||
expect(mail).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
164
research/chatwoot/spec/mailers/references_header_builder_spec.rb
Normal file
164
research/chatwoot/spec/mailers/references_header_builder_spec.rb
Normal file
@@ -0,0 +1,164 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ReferencesHeaderBuilder do
|
||||
include described_class
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:email_channel) { create(:channel_email, account: account) }
|
||||
let(:inbox) { create(:inbox, channel: email_channel, account: account) }
|
||||
let(:conversation) { create(:conversation, account: account, inbox: inbox) }
|
||||
|
||||
describe '#build_references_header' do
|
||||
let(:in_reply_to_message_id) { '<reply-to-123@example.com>' }
|
||||
|
||||
context 'when no message is found with the in_reply_to_message_id' do
|
||||
it 'returns only the in_reply_to message ID' do
|
||||
result = build_references_header(conversation, in_reply_to_message_id)
|
||||
expect(result).to eq('<reply-to-123@example.com>')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a message is found with matching source_id' do
|
||||
context 'with stored References' do
|
||||
let(:original_message) do
|
||||
create(:message, conversation: conversation, account: account,
|
||||
source_id: '<reply-to-123@example.com>',
|
||||
content_attributes: {
|
||||
'email' => {
|
||||
'references' => ['<thread-001@example.com>', '<thread-002@example.com>']
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
before do
|
||||
original_message
|
||||
end
|
||||
|
||||
it 'includes stored References plus in_reply_to' do
|
||||
result = build_references_header(conversation, in_reply_to_message_id)
|
||||
expect(result).to eq("<thread-001@example.com>\r\n <thread-002@example.com>\r\n <reply-to-123@example.com>")
|
||||
end
|
||||
|
||||
it 'removes duplicates while preserving order' do
|
||||
# If in_reply_to is already in the References, it should appear only once at the end
|
||||
original_message.content_attributes['email']['references'] = ['<thread-001@example.com>', '<reply-to-123@example.com>']
|
||||
original_message.save!
|
||||
|
||||
result = build_references_header(conversation, in_reply_to_message_id)
|
||||
message_ids = result.split("\r\n ").map(&:strip)
|
||||
expect(message_ids).to eq(['<thread-001@example.com>', '<reply-to-123@example.com>'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'without stored References' do
|
||||
let(:original_message) do
|
||||
create(:message, conversation: conversation, account: account,
|
||||
source_id: 'reply-to-123@example.com', # without angle brackets
|
||||
content_attributes: { 'email' => {} })
|
||||
end
|
||||
|
||||
before do
|
||||
original_message
|
||||
end
|
||||
|
||||
it 'returns only the in_reply_to message ID (no rebuild)' do
|
||||
result = build_references_header(conversation, in_reply_to_message_id)
|
||||
expect(result).to eq('<reply-to-123@example.com>')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with folding multiple References' do
|
||||
let(:original_message) do
|
||||
create(:message, conversation: conversation, account: account,
|
||||
source_id: '<reply-to-123@example.com>',
|
||||
content_attributes: {
|
||||
'email' => {
|
||||
'references' => ['<msg-001@example.com>', '<msg-002@example.com>', '<msg-003@example.com>']
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
before do
|
||||
original_message
|
||||
end
|
||||
|
||||
it 'folds the header with CRLF between message IDs' do
|
||||
result = build_references_header(conversation, in_reply_to_message_id)
|
||||
|
||||
expect(result).to include("\r\n")
|
||||
lines = result.split("\r\n")
|
||||
|
||||
# First line has no leading space, continuation lines do
|
||||
expect(lines.first).not_to start_with(' ')
|
||||
expect(lines[1..]).to all(start_with(' '))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with source_id in different formats' do
|
||||
it 'finds message with source_id without angle brackets' do
|
||||
create(:message, conversation: conversation, account: account,
|
||||
source_id: 'test-123@example.com',
|
||||
content_attributes: {
|
||||
'email' => {
|
||||
'references' => ['<ref-1@example.com>']
|
||||
}
|
||||
})
|
||||
|
||||
result = build_references_header(conversation, '<test-123@example.com>')
|
||||
expect(result).to eq("<ref-1@example.com>\r\n <test-123@example.com>")
|
||||
end
|
||||
|
||||
it 'finds message with source_id with angle brackets' do
|
||||
create(:message, conversation: conversation, account: account,
|
||||
source_id: '<test-456@example.com>',
|
||||
content_attributes: {
|
||||
'email' => {
|
||||
'references' => ['<ref-2@example.com>']
|
||||
}
|
||||
})
|
||||
|
||||
result = build_references_header(conversation, 'test-456@example.com')
|
||||
expect(result).to eq("<ref-2@example.com>\r\n test-456@example.com")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#fold_references_header' do
|
||||
it 'returns single message ID without folding' do
|
||||
single_array = ['<msg1@example.com>']
|
||||
result = fold_references_header(single_array)
|
||||
|
||||
expect(result).to eq('<msg1@example.com>')
|
||||
expect(result).not_to include("\r\n")
|
||||
end
|
||||
|
||||
it 'folds multiple message IDs with CRLF + space' do
|
||||
multiple_array = ['<msg1@example.com>', '<msg2@example.com>', '<msg3@example.com>']
|
||||
result = fold_references_header(multiple_array)
|
||||
|
||||
expect(result).to eq("<msg1@example.com>\r\n <msg2@example.com>\r\n <msg3@example.com>")
|
||||
end
|
||||
|
||||
it 'ensures RFC 5322 compliance with continuation line spacing' do
|
||||
multiple_array = ['<msg1@example.com>', '<msg2@example.com>']
|
||||
result = fold_references_header(multiple_array)
|
||||
lines = result.split("\r\n")
|
||||
|
||||
# First line has no leading space (not a continuation line)
|
||||
expect(lines.first).to eq('<msg1@example.com>')
|
||||
expect(lines.first).not_to start_with(' ')
|
||||
|
||||
# Second line starts with space (continuation line)
|
||||
expect(lines[1]).to eq(' <msg2@example.com>')
|
||||
expect(lines[1]).to start_with(' ')
|
||||
end
|
||||
|
||||
it 'handles empty array' do
|
||||
result = fold_references_header([])
|
||||
expect(result).to eq('')
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user