diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index 49fe325f6..481c671ce 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -8,8 +8,8 @@ > @@ -38,6 +38,7 @@ import vueActionCable from './helper/actionCable'; import WootSnackbarBox from './components/SnackbarContainer.vue'; import rtlMixin from 'shared/mixins/rtlMixin'; import { setColorTheme } from './helper/themeHelper'; +import { isOnOnboardingView } from 'v3/helpers/RouteHelper'; import { registerSubscription, verifyServiceWorkerExistence, @@ -79,6 +80,9 @@ export default { const { accounts = [] } = this.currentUser || {}; return accounts.length > 0; }, + hideOnOnboardingView() { + return !isOnOnboardingView(this.$route); + }, }, watch: { diff --git a/app/javascript/v3/helpers/RouteHelper.js b/app/javascript/v3/helpers/RouteHelper.js index db508ac13..16bc9cd41 100644 --- a/app/javascript/v3/helpers/RouteHelper.js +++ b/app/javascript/v3/helpers/RouteHelper.js @@ -48,3 +48,13 @@ export const validateRouteAccess = (to, next, chatwootConfig = {}) => { next(); }; + +export const isOnOnboardingView = route => { + const { name = '' } = route || {}; + + if (!name) { + return false; + } + + return name.includes('onboarding_'); +}; diff --git a/app/javascript/v3/helpers/specs/RouteHelper.spec.js b/app/javascript/v3/helpers/specs/RouteHelper.spec.js index 8481a8ff4..4ebeccdcb 100644 --- a/app/javascript/v3/helpers/specs/RouteHelper.spec.js +++ b/app/javascript/v3/helpers/specs/RouteHelper.spec.js @@ -1,4 +1,4 @@ -import { validateRouteAccess } from '../RouteHelper'; +import { validateRouteAccess, isOnOnboardingView } from '../RouteHelper'; import { clearBrowserSessionCookies } from 'dashboard/store/utils/api'; import { replaceRouteWithReload } from '../CommonHelper'; import Cookies from 'js-cookie'; @@ -67,3 +67,24 @@ describe('#validateRouteAccess', () => { expect(next).toHaveBeenCalledWith(); }); }); + +describe('isOnOnboardingView', () => { + test('returns true for a route with onboarding name', () => { + const route = { name: 'onboarding_welcome' }; + expect(isOnOnboardingView(route)).toBe(true); + }); + + test('returns false for a route without onboarding name', () => { + const route = { name: 'home' }; + expect(isOnOnboardingView(route)).toBe(false); + }); + + test('returns false for a route with null name', () => { + const route = { name: null }; + expect(isOnOnboardingView(route)).toBe(false); + }); + + test('returns false for an undefined route object', () => { + expect(isOnOnboardingView()).toBe(false); + }); +}); diff --git a/app/mailboxes/application_mailbox.rb b/app/mailboxes/application_mailbox.rb index c1e273e70..6a1902ce4 100644 --- a/app/mailboxes/application_mailbox.rb +++ b/app/mailboxes/application_mailbox.rb @@ -37,10 +37,24 @@ class ApplicationMailbox < ActionMailbox::Base # checks if follow this pattern send it to reply_mailbox # reply+@ def reply_uuid_mail?(inbound_mail) + validate_to_address(inbound_mail) + inbound_mail.mail.to&.any? do |email| conversation_uuid = email.split('@')[0] conversation_uuid.match?(REPLY_EMAIL_UUID_PATTERN) end end + + # if mail.to returns a string, then it is a malformed `to` header + # valid `to` header will be of type Mail::AddressContainer + # validate if the to address is of type string + def validate_to_address(inbound_mail) + to_address_class = inbound_mail.mail.to&.class + + return if to_address_class == Mail::AddressContainer + + Rails.logger.error "Email to address header is malformed `#{inbound_mail.mail.to}`" + raise StandardError, "Invalid email to address header #{inbound_mail.mail.to}" + end end end diff --git a/app/services/action_service.rb b/app/services/action_service.rb index 7a9a14d0d..79f1234a0 100644 --- a/app/services/action_service.rb +++ b/app/services/action_service.rb @@ -3,6 +3,7 @@ class ActionService def initialize(conversation) @conversation = conversation.reload + @account = @conversation.account end def mute_conversation(_params) diff --git a/db/migrate/20240216055809_add_unique_index_to_applied_slas.rb b/db/migrate/20240216055809_add_unique_index_to_applied_slas.rb new file mode 100644 index 000000000..1d64fc67d --- /dev/null +++ b/db/migrate/20240216055809_add_unique_index_to_applied_slas.rb @@ -0,0 +1,8 @@ +class AddUniqueIndexToAppliedSlas < ActiveRecord::Migration[7.0] + def change + add_index :applied_slas, + [:account_id, :sla_policy_id, :conversation_id], + unique: true, + name: 'index_applied_slas_on_account_sla_policy_conversation' + end +end diff --git a/db/schema.rb b/db/schema.rb index 138ea6835..546df444f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_02_15_065844) do +ActiveRecord::Schema[7.0].define(version: 2024_02_16_055809) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -122,6 +122,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_02_15_065844) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "sla_status", default: 0 + t.index ["account_id", "sla_policy_id", "conversation_id"], name: "index_applied_slas_on_account_sla_policy_conversation", unique: true t.index ["account_id"], name: "index_applied_slas_on_account_id" t.index ["conversation_id"], name: "index_applied_slas_on_conversation_id" t.index ["sla_policy_id"], name: "index_applied_slas_on_sla_policy_id" diff --git a/enterprise/app/jobs/sla/trigger_slas_for_accounts_job.rb b/enterprise/app/jobs/sla/trigger_slas_for_accounts_job.rb index 212c44d94..a2a142430 100644 --- a/enterprise/app/jobs/sla/trigger_slas_for_accounts_job.rb +++ b/enterprise/app/jobs/sla/trigger_slas_for_accounts_job.rb @@ -2,7 +2,7 @@ class Sla::TriggerSlasForAccountsJob < ApplicationJob queue_as :scheduled_jobs def perform - Account.find_each do |account| + Account.joins(:sla_policies).distinct.find_each do |account| Rails.logger.info "Enqueuing ProcessAccountAppliedSlasJob for account #{account.id}" Sla::ProcessAccountAppliedSlasJob.perform_later(account) end diff --git a/enterprise/app/models/applied_sla.rb b/enterprise/app/models/applied_sla.rb index 825c747fc..cb5ca508d 100644 --- a/enterprise/app/models/applied_sla.rb +++ b/enterprise/app/models/applied_sla.rb @@ -12,14 +12,17 @@ # # Indexes # -# index_applied_slas_on_account_id (account_id) -# index_applied_slas_on_conversation_id (conversation_id) -# index_applied_slas_on_sla_policy_id (sla_policy_id) +# index_applied_slas_on_account_id (account_id) +# index_applied_slas_on_account_sla_policy_conversation (account_id,sla_policy_id,conversation_id) UNIQUE +# index_applied_slas_on_conversation_id (conversation_id) +# index_applied_slas_on_sla_policy_id (sla_policy_id) # class AppliedSla < ApplicationRecord belongs_to :account belongs_to :sla_policy belongs_to :conversation + validates :account_id, uniqueness: { scope: %i[sla_policy_id conversation_id] } + enum sla_status: { active: 0, hit: 1, missed: 2 } end diff --git a/enterprise/app/services/enterprise/action_service.rb b/enterprise/app/services/enterprise/action_service.rb index 6e21031aa..1e4165b09 100644 --- a/enterprise/app/services/enterprise/action_service.rb +++ b/enterprise/app/services/enterprise/action_service.rb @@ -1,10 +1,18 @@ module Enterprise::ActionService - def add_sla(sla_policy) + def add_sla(sla_policy_id) + return if sla_policy_id.blank? + + sla_policy = @account.sla_policies.find_by(id: sla_policy_id.first) + return if sla_policy.nil? + return if @conversation.sla_policy.present? + + Rails.logger.info "SLA:: Adding SLA #{sla_policy.id} to conversation: #{@conversation.id}" @conversation.update!(sla_policy_id: sla_policy.id) create_applied_sla(sla_policy) end def create_applied_sla(sla_policy) + Rails.logger.info "SLA:: Creating Applied SLA for conversation: #{@conversation.id}" AppliedSla.create!( account_id: @conversation.account_id, sla_policy_id: sla_policy.id, diff --git a/spec/enterprise/jobs/sla/trigger_slas_for_accounts_job_spec.rb b/spec/enterprise/jobs/sla/trigger_slas_for_accounts_job_spec.rb index 95650748a..d02e543ba 100644 --- a/spec/enterprise/jobs/sla/trigger_slas_for_accounts_job_spec.rb +++ b/spec/enterprise/jobs/sla/trigger_slas_for_accounts_job_spec.rb @@ -1,16 +1,25 @@ require 'rails_helper' - RSpec.describe Sla::TriggerSlasForAccountsJob do context 'when perform is called' do - let(:account) { create(:account) } + let(:account_with_sla) { create(:account) } + let(:account_without_sla) { create(:account) } + + before do + create(:sla_policy, account: account_with_sla) + end it 'enqueues the job' do expect { described_class.perform_later }.to have_enqueued_job(described_class) .on_queue('scheduled_jobs') end - it 'calls the ProcessAccountAppliedSlasJob' do - expect(Sla::ProcessAccountAppliedSlasJob).to receive(:perform_later).with(account).and_call_original + it 'calls the ProcessAccountAppliedSlasJob for accounts with SLA' do + expect(Sla::ProcessAccountAppliedSlasJob).to receive(:perform_later).with(account_with_sla).and_call_original + described_class.perform_now + end + + it 'does not call the ProcessAccountAppliedSlasJob for accounts without SLA' do + expect(Sla::ProcessAccountAppliedSlasJob).not_to receive(:perform_later).with(account_without_sla) described_class.perform_now end end diff --git a/spec/enterprise/services/enterprise/action_service_spec.rb b/spec/enterprise/services/enterprise/action_service_spec.rb index 6efffca6d..a77a039dd 100644 --- a/spec/enterprise/services/enterprise/action_service_spec.rb +++ b/spec/enterprise/services/enterprise/action_service_spec.rb @@ -8,16 +8,42 @@ describe ActionService do let(:conversation) { create(:conversation, account: account) } let(:action_service) { described_class.new(conversation) } - it 'adds the sla policy to the conversation and create applied_sla entry' do - action_service.add_sla(sla_policy) - expect(conversation.reload.sla_policy_id).to eq(sla_policy.id) + context 'when sla_policy_id is present' do + it 'adds the sla policy to the conversation and create applied_sla entry' do + action_service.add_sla([sla_policy.id]) + expect(conversation.reload.sla_policy_id).to eq(sla_policy.id) - # check if appliedsla table entry is created with matching attributes - applied_sla = AppliedSla.last - expect(applied_sla.account_id).to eq(account.id) - expect(applied_sla.sla_policy_id).to eq(sla_policy.id) - expect(applied_sla.conversation_id).to eq(conversation.id) - expect(applied_sla.sla_status).to eq('active') + # check if appliedsla table entry is created with matching attributes + applied_sla = AppliedSla.last + expect(applied_sla.account_id).to eq(account.id) + expect(applied_sla.sla_policy_id).to eq(sla_policy.id) + expect(applied_sla.conversation_id).to eq(conversation.id) + expect(applied_sla.sla_status).to eq('active') + end + end + + context 'when sla_policy_id is not present' do + it 'does not add the sla policy to the conversation' do + action_service.add_sla(nil) + expect(conversation.reload.sla_policy_id).to be_nil + end + end + + context 'when conversation already has a sla policy' do + it 'does not add the new sla policy to the conversation' do + existing_sla_policy = sla_policy + new_sla_policy = create(:sla_policy, account: account) + conversation.update!(sla_policy_id: existing_sla_policy.id) + action_service.add_sla([new_sla_policy.id]) + expect(conversation.reload.sla_policy_id).to eq(existing_sla_policy.id) + end + end + + context 'when sla_policy is not found' do + it 'does not add the sla policy to the conversation' do + action_service.add_sla([sla_policy.id + 1]) + expect(conversation.reload.sla_policy_id).to be_nil + end end end end diff --git a/spec/fixtures/files/mail_with_invalid_to.eml b/spec/fixtures/files/mail_with_invalid_to.eml new file mode 100644 index 000000000..8dd2aabb6 --- /dev/null +++ b/spec/fixtures/files/mail_with_invalid_to.eml @@ -0,0 +1,29 @@ +X-Original-To: bd84c730a1ac7833e4d27253804516f7@reply.chatwoot.com +Received: from mail.planetmars.com (mxd [192.168.1.1]) by mx.sendgrid.net with ESMTP id AAAA-bCCCCCC5DeeeFFgg for ; Sun, 31 Dec 2023 22:32:23.586 +0000 (UTC) +From: "Mark Whatney" +To: vishnu@chatwoot.com +Subject: stranded in mars +Date: Mon, 1 Jan 2024 06:31:44 +0800 +Message-ID: <1234560e0123c05b4bbf83c828b1688a93c7@com> +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary=15688136a4ad411d82b004fae6e46549 +X-Exim-Id: 1234560e0123c05b4bbf83c828b1688a93c7 + +This is a multipart message in MIME format. + +--15688136a4ad411d82b004fae6e46549 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: 7bit + +hey v, can i get some help over here? + +--15688136a4ad411d82b004fae6e46549 +Content-Type: text/html; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +hey v, can i get some help over here? + +--15688136a4ad411d82b004fae6e46549-- diff --git a/spec/fixtures/files/mail_with_invalid_to_2.eml b/spec/fixtures/files/mail_with_invalid_to_2.eml new file mode 100644 index 000000000..4ef77cb2d --- /dev/null +++ b/spec/fixtures/files/mail_with_invalid_to_2.eml @@ -0,0 +1,29 @@ +X-Original-To: bd84c730a1ac7833e4d27253804516f7@reply.chatwoot.com +Received: from mail.planetmars.com (mxd [192.168.1.1]) by mx.sendgrid.net with ESMTP id AAAA-bCCCCCC5DeeeFFgg for ; Sun, 31 Dec 2023 22:32:23.586 +0000 (UTC) +From: "Mark Whatney" +To: vishnu@chatwoot.com www.chatwoot.com +Subject: stranded in mars +Date: Mon, 1 Jan 2024 06:31:44 +0800 +Message-ID: <1234560e0123c05b4bbf83c828b1688a93c7@com> +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary=15688136a4ad411d82b004fae6e46549 +X-Exim-Id: 1234560e0123c05b4bbf83c828b1688a93c7 + +This is a multipart message in MIME format. + +--15688136a4ad411d82b004fae6e46549 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: 7bit + +hey v, can i get some help over here? + +--15688136a4ad411d82b004fae6e46549 +Content-Type: text/html; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +hey v, can i get some help over here? + +--15688136a4ad411d82b004fae6e46549-- diff --git a/spec/mailboxes/application_mailbox_spec.rb b/spec/mailboxes/application_mailbox_spec.rb index d5e15dc46..733ec5523 100644 --- a/spec/mailboxes/application_mailbox_spec.rb +++ b/spec/mailboxes/application_mailbox_spec.rb @@ -10,6 +10,8 @@ RSpec.describe ApplicationMailbox do let(:reply_mail_without_uuid) { create_inbound_email_from_fixture('reply.eml') } let(:reply_mail_with_in_reply_to) { create_inbound_email_from_fixture('in_reply_to.eml') } let(:support_mail) { create_inbound_email_from_fixture('support.eml') } + let(:mail_with_invalid_to_address) { create_inbound_email_from_fixture('mail_with_invalid_to.eml') } + let(:mail_with_invalid_to_address_2) { create_inbound_email_from_fixture('mail_with_invalid_to_2.eml') } describe 'Default' do it 'catchall mails route to Default Mailbox' do @@ -65,5 +67,21 @@ RSpec.describe ApplicationMailbox do described_class.route reply_cc_mail end end + + describe 'Invalid Mail To Address' do + it 'raises error when mail.to header is malformed' do + expect do + described_class.route mail_with_invalid_to_address + end.to raise_error(StandardError, + 'Invalid email to address header vishnu@chatwoot.com') + end + + it 'raises another error when mail.to header is malformed' do + expect do + described_class.route mail_with_invalid_to_address_2 + end.to raise_error(StandardError, + 'Invalid email to address header vishnu@chatwoot.com www.chatwoot.com') + end + end end end diff --git a/yarn.lock b/yarn.lock index 1ab16149d..cc9a6f34c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12597,14 +12597,14 @@ ip-regex@^2.1.0: integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= ip@^1.1.0, ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + version "1.1.9" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.9.tgz#8dfbcc99a754d07f425310b86a99546b1151e396" + integrity sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ== ip@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" - integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.1.tgz#e8f3595d33a3ea66490204234b77636965307105" + integrity sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ== ipaddr.js@1.9.1, ipaddr.js@^1.9.0: version "1.9.1"