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"