From 3655f4cedc09885e7150f6c174f4e3a9c979a100 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 22 Sep 2025 17:19:12 +0530 Subject: [PATCH 1/3] feat: Add superlong debounce condition for meta endpoint (#12486) --- .../dashboard/store/modules/conversationStats.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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); From 8764ade161b427491ff40ad34129ffb22bf6fe9c Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 22 Sep 2025 17:52:56 +0530 Subject: [PATCH 2/3] feat: add `SKIP_INCOMING_BCC_PROCESSING` as internal config (#12484) Co-authored-by: Muhsin Keloth --- app/finders/email_channel_finder.rb | 59 ++++++++++--- config/installation_config.yml | 4 + .../super_admin/app_configs_controller.rb | 2 +- spec/finders/email_channel_finder_spec.rb | 83 +++++++++++++++++++ spec/mailboxes/application_mailbox_spec.rb | 14 ++++ spec/mailboxes/support_mailbox_spec.rb | 14 ++++ 6 files changed, 163 insertions(+), 13 deletions(-) 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/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 From 8162473eb6c2d4895d75bfd65e92b46587e61b48 Mon Sep 17 00:00:00 2001 From: Honza Sterba Date: Mon, 22 Sep 2025 15:29:30 +0200 Subject: [PATCH 3/3] fix: Contact search by phone number (#10386) # Pull Request Template ## Description when filtering contacts by phone number a + is always added to the begining of the query, this means that the filtering breaks if the complete phone number with international code and + is entered ## Type of change Please delete options that are not relevant. - [X] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. Updated automated tests Tested manually with contact filtering UI ## Checklist: - [X] My code follows the style guidelines of this project - [X] I have performed a self-review of my code - [X] I have commented on my code, particularly in hard-to-understand areas - [X] I have made corresponding changes to the documentation - [X] My changes generate no new warnings - [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 - [X] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Muhsin Keloth --- app/services/contacts/filter_service.rb | 2 +- spec/services/contacts/filter_service_spec.rb | 38 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) 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/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)