diff --git a/app/finders/email_channel_finder.rb b/app/finders/email_channel_finder.rb index 41cd8e910..1b6d6f844 100644 --- a/app/finders/email_channel_finder.rb +++ b/app/finders/email_channel_finder.rb @@ -6,19 +6,54 @@ class EmailChannelFinder end def perform - channel = nil - - recipient_mails.each do |email| - normalized_email = normalize_email_with_plus_addressing(email) - channel = Channel::Email.find_by('lower(email) = ? OR lower(forward_to_email) = ?', normalized_email, normalized_email) - - break if channel.present? - end - channel + channel_from_primary_recipients || channel_from_bcc_recipients end - def recipient_mails - recipient_addresses = @email_object.to.to_a + @email_object.cc.to_a + @email_object.bcc.to_a + [@email_object['X-Original-To'].try(:value)] - recipient_addresses.flatten.compact + private + + def channel_from_primary_recipients + primary_recipient_emails.each do |email| + channel = channel_from_email(email) + return channel if channel.present? + end + + nil + end + + def channel_from_bcc_recipients + bcc_recipient_emails.each do |email| + channel = channel_from_email(email) + + # Skip if BCC processing is disabled for this account + next if channel && !allow_bcc_processing?(channel.account_id) + + return channel if channel.present? + end + + nil + end + + def primary_recipient_emails + (@email_object.to.to_a + @email_object.cc.to_a + [@email_object['X-Original-To'].try(:value)]).flatten.compact + end + + def bcc_recipient_emails + @email_object.bcc.to_a.flatten.compact + end + + def channel_from_email(email) + normalized_email = normalize_email_with_plus_addressing(email) + Channel::Email.find_by('lower(email) = ? OR lower(forward_to_email) = ?', normalized_email, normalized_email) + end + + def bcc_processing_skipped_accounts + config_value = GlobalConfigService.load('SKIP_INCOMING_BCC_PROCESSING', '') + return [] if config_value.blank? + + config_value.split(',').map(&:to_i) + end + + def allow_bcc_processing?(account_id) + bcc_processing_skipped_accounts.exclude?(account_id) end end diff --git a/app/javascript/dashboard/store/modules/conversationStats.js b/app/javascript/dashboard/store/modules/conversationStats.js index bae365f30..1b3844b08 100644 --- a/app/javascript/dashboard/store/modules/conversationStats.js +++ b/app/javascript/dashboard/store/modules/conversationStats.js @@ -27,10 +27,18 @@ const fetchMetaData = async (commit, params) => { const debouncedFetchMetaData = debounce(fetchMetaData, 500, false, 1000); const longDebouncedFetchMetaData = debounce(fetchMetaData, 500, false, 5000); +const superLongDebouncedFetchMetaData = debounce( + fetchMetaData, + 2000, + false, + 5000 +); export const actions = { get: async ({ commit, state: $state }, params) => { - if ($state.allCount > 100) { + if ($state.allCount > 10000) { + superLongDebouncedFetchMetaData(commit, params); + } else if ($state.allCount > 100) { longDebouncedFetchMetaData(commit, params); } else { debouncedFetchMetaData(commit, params); diff --git a/app/services/contacts/filter_service.rb b/app/services/contacts/filter_service.rb index 7f2d6a0b8..9d017ea75 100644 --- a/app/services/contacts/filter_service.rb +++ b/app/services/contacts/filter_service.rb @@ -21,7 +21,7 @@ class Contacts::FilterService < FilterService def filter_values(query_hash) current_val = query_hash['values'][0] if query_hash['attribute_key'] == 'phone_number' - "+#{current_val}" + "+#{current_val&.delete('+')}" elsif query_hash['attribute_key'] == 'country_code' current_val.downcase else diff --git a/config/installation_config.yml b/config/installation_config.yml index db907b7ad..9eb6af14f 100644 --- a/config/installation_config.yml +++ b/config/installation_config.yml @@ -236,6 +236,10 @@ display_title: 'Blocked Email Domains' description: 'Add a domain per line to block them from signing up, accepts Regex' type: code +- name: SKIP_INCOMING_BCC_PROCESSING + value: + display_title: 'Skip BCC Processing For' + description: 'Comma-separated list of account IDs that should be skipped from incoming BCC processing' - name: INACTIVE_WHATSAPP_NUMBERS value: '' display_title: 'Inactive WhatsApp Numbers' diff --git a/enterprise/app/controllers/enterprise/super_admin/app_configs_controller.rb b/enterprise/app/controllers/enterprise/super_admin/app_configs_controller.rb index 5e70d2d79..934462b93 100644 --- a/enterprise/app/controllers/enterprise/super_admin/app_configs_controller.rb +++ b/enterprise/app/controllers/enterprise/super_admin/app_configs_controller.rb @@ -33,7 +33,7 @@ module Enterprise::SuperAdmin::AppConfigsController def internal_config_options %w[CHATWOOT_INBOX_TOKEN CHATWOOT_INBOX_HMAC_KEY ANALYTICS_TOKEN CLEARBIT_API_KEY DASHBOARD_SCRIPTS INACTIVE_WHATSAPP_NUMBERS BLOCKED_EMAIL_DOMAINS - CAPTAIN_CLOUD_PLAN_LIMITS ACCOUNT_SECURITY_NOTIFICATION_WEBHOOK_URL CHATWOOT_INSTANCE_ADMIN_EMAIL + SKIP_INCOMING_BCC_PROCESSING CAPTAIN_CLOUD_PLAN_LIMITS ACCOUNT_SECURITY_NOTIFICATION_WEBHOOK_URL CHATWOOT_INSTANCE_ADMIN_EMAIL OG_IMAGE_CDN_URL OG_IMAGE_CLIENT_REF CLOUDFLARE_API_KEY CLOUDFLARE_ZONE_ID] end diff --git a/spec/finders/email_channel_finder_spec.rb b/spec/finders/email_channel_finder_spec.rb index fe57dec0b..d56d97008 100644 --- a/spec/finders/email_channel_finder_spec.rb +++ b/spec/finders/email_channel_finder_spec.rb @@ -2,6 +2,7 @@ require 'rails_helper' describe EmailChannelFinder do include ActionMailbox::TestHelper + let!(:channel_email) { create(:channel_email) } describe '#perform' do @@ -48,6 +49,75 @@ describe EmailChannelFinder do expect(channel).to eq(channel_email) end + it 'skip bcc email when account is configured to skip BCC processing' do + channel_email.update(email: 'test@example.com') + reply_mail.mail['to'] = nil + reply_mail.mail['bcc'] = 'test@example.com' + + allow(GlobalConfigService).to receive(:load) + .with('SKIP_INCOMING_BCC_PROCESSING', '') + .and_return(channel_email.account_id.to_s) + + channel = described_class.new(reply_mail.mail).perform + expect(channel).to be_nil + end + + it 'skip bcc email when account is in multiple account ids config' do + channel_email.update(email: 'test@example.com') + reply_mail.mail['to'] = nil + reply_mail.mail['bcc'] = 'test@example.com' + + # Include this account along with other account IDs + other_account_ids = [123, 456, channel_email.account_id, 789] + allow(GlobalConfigService).to receive(:load) + .with('SKIP_INCOMING_BCC_PROCESSING', '') + .and_return(other_account_ids.join(',')) + + channel = described_class.new(reply_mail.mail).perform + expect(channel).to be_nil + end + + it 'process bcc email when account is not in skip config' do + channel_email.update(email: 'test@example.com') + reply_mail.mail['to'] = nil + reply_mail.mail['bcc'] = 'test@example.com' + + # Configure other account IDs but not this one + other_account_ids = [123, 456, 789] + allow(GlobalConfigService).to receive(:load) + .with('SKIP_INCOMING_BCC_PROCESSING', '') + .and_return(other_account_ids.join(',')) + + channel = described_class.new(reply_mail.mail).perform + expect(channel).to eq(channel_email) + end + + it 'process bcc email when skip config is empty' do + channel_email.update(email: 'test@example.com') + reply_mail.mail['to'] = nil + reply_mail.mail['bcc'] = 'test@example.com' + + allow(GlobalConfigService).to receive(:load) + .with('SKIP_INCOMING_BCC_PROCESSING', '') + .and_return('') + + channel = described_class.new(reply_mail.mail).perform + expect(channel).to eq(channel_email) + end + + it 'process bcc email when skip config is nil' do + channel_email.update(email: 'test@example.com') + reply_mail.mail['to'] = nil + reply_mail.mail['bcc'] = 'test@example.com' + + allow(GlobalConfigService).to receive(:load) + .with('SKIP_INCOMING_BCC_PROCESSING', '') + .and_return(nil) + + channel = described_class.new(reply_mail.mail).perform + expect(channel).to eq(channel_email) + end + it 'return channel with X-Original-To email' do channel_email.update(email: 'test@example.com') reply_mail.mail['to'] = nil @@ -55,6 +125,19 @@ describe EmailChannelFinder do channel = described_class.new(reply_mail.mail).perform expect(channel).to eq(channel_email) end + + it 'process X-Original-To email even when account is configured to skip BCC processing' do + channel_email.update(email: 'test@example.com') + reply_mail.mail['to'] = nil + reply_mail.mail['X-Original-To'] = 'test@example.com' + + allow(GlobalConfigService).to receive(:load) + .with('SKIP_INCOMING_BCC_PROCESSING', '') + .and_return(channel_email.account_id.to_s) + + channel = described_class.new(reply_mail.mail).perform + expect(channel).to eq(channel_email) + end end end end diff --git a/spec/mailboxes/application_mailbox_spec.rb b/spec/mailboxes/application_mailbox_spec.rb index f4f28d811..33bbf9de8 100644 --- a/spec/mailboxes/application_mailbox_spec.rb +++ b/spec/mailboxes/application_mailbox_spec.rb @@ -66,6 +66,20 @@ RSpec.describe ApplicationMailbox do expect(dbl).to receive(:perform_processing).and_return(true) described_class.route reply_cc_mail end + + it 'skips routing when BCC processing is disabled for account' do + allow(GlobalConfigService).to receive(:load).with('SKIP_INCOMING_BCC_PROCESSING', '').and_return(channel_email.account_id.to_s) + + # Create a BCC-only email scenario + bcc_mail = create_inbound_email_from_fixture('support.eml') + bcc_mail.mail['to'] = nil + bcc_mail.mail['bcc'] = 'care@example.com' + + channel_email.update(email: 'care@example.com') + + expect(DefaultMailbox).to receive(:new).and_return(double.tap { |d| expect(d).to receive(:perform_processing) }) + described_class.route bcc_mail + end end describe 'Invalid Mail To Address' do diff --git a/spec/mailboxes/support_mailbox_spec.rb b/spec/mailboxes/support_mailbox_spec.rb index f6964285d..0dbfbbe3b 100644 --- a/spec/mailboxes/support_mailbox_spec.rb +++ b/spec/mailboxes/support_mailbox_spec.rb @@ -334,5 +334,19 @@ RSpec.describe SupportMailbox do expect(conversation.messages.last.content_attributes['email']['subject']).to eq('attachment with html') end end + + describe 'when BCC processing is disabled for account' do + before do + allow(GlobalConfigService).to receive(:load).with('SKIP_INCOMING_BCC_PROCESSING', '').and_return(account.id.to_s) + end + + it 'does not process BCC-only emails' do + bcc_mail = create_inbound_email_from_fixture('support.eml') + bcc_mail.mail['to'] = nil + bcc_mail.mail['bcc'] = 'care@example.com' + + expect { described_class.receive bcc_mail }.to raise_error('Email channel/inbox not found') + end + end end end diff --git a/spec/services/contacts/filter_service_spec.rb b/spec/services/contacts/filter_service_spec.rb index 22882ae81..77d3a49f3 100644 --- a/spec/services/contacts/filter_service_spec.rb +++ b/spec/services/contacts/filter_service_spec.rb @@ -9,7 +9,7 @@ describe Contacts::FilterService do let!(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) } let!(:en_contact) { create(:contact, account: account, additional_attributes: { 'country_code': 'uk' }) } let!(:el_contact) { create(:contact, account: account, additional_attributes: { 'country_code': 'gr' }) } - let!(:cs_contact) { create(:contact, account: account, additional_attributes: { 'country_code': 'cz' }) } + let!(:cs_contact) { create(:contact, :with_phone_number, account: account, additional_attributes: { 'country_code': 'cz' }) } before do create(:inbox_member, user: first_user, inbox: inbox) @@ -65,6 +65,42 @@ describe Contacts::FilterService do end end + context 'with standard attributes - phone' do + it 'filter contacts by name' do + params[:payload] = [ + { + attribute_key: 'phone_number', + filter_operator: 'equal_to', + values: [cs_contact.phone_number], + query_operator: nil + }.with_indifferent_access + ] + + result = filter_service.new(account, first_user, params).perform + expect(result[:count]).to be 1 + expect(result[:contacts].length).to be 1 + expect(result[:contacts].first.name).to eq(cs_contact.name) + end + end + + context 'with standard attributes - phone (without +)' do + it 'filter contacts by name' do + params[:payload] = [ + { + attribute_key: 'phone_number', + filter_operator: 'equal_to', + values: [cs_contact.phone_number[1..]], + query_operator: nil + }.with_indifferent_access + ] + + result = filter_service.new(account, first_user, params).perform + expect(result[:count]).to be 1 + expect(result[:contacts].length).to be 1 + expect(result[:contacts].first.name).to eq(cs_contact.name) + end + end + context 'with standard attributes - blocked' do it 'filter contacts by blocked' do blocked_contact = create(:contact, account: account, blocked: true)