+
{{ $t('INBOX.LIST.TITLE') }}
diff --git a/app/javascript/dashboard/routes/dashboard/inbox/routes.js b/app/javascript/dashboard/routes/dashboard/inbox/routes.js
index 4ec1dff75..1d2b349d9 100644
--- a/app/javascript/dashboard/routes/dashboard/inbox/routes.js
+++ b/app/javascript/dashboard/routes/dashboard/inbox/routes.js
@@ -21,7 +21,7 @@ export const routes = [
},
},
{
- path: ':notification_id',
+ path: ':type/:id',
name: 'inbox_view_conversation',
component: InboxDetailView,
meta: {
diff --git a/app/jobs/inboxes/fetch_imap_email_inboxes_job.rb b/app/jobs/inboxes/fetch_imap_email_inboxes_job.rb
index 56e8c2235..ea2705955 100644
--- a/app/jobs/inboxes/fetch_imap_email_inboxes_job.rb
+++ b/app/jobs/inboxes/fetch_imap_email_inboxes_job.rb
@@ -1,5 +1,6 @@
class Inboxes::FetchImapEmailInboxesJob < ApplicationJob
queue_as :scheduled_jobs
+ include BillingHelper
def perform
email_inboxes = Inbox.where(channel_type: 'Channel::Email')
@@ -11,6 +12,13 @@ class Inboxes::FetchImapEmailInboxesJob < ApplicationJob
private
def should_fetch_emails?(inbox)
- inbox.channel.imap_enabled && !inbox.account.suspended?
+ return false if inbox.account.suspended?
+ return false unless inbox.channel.imap_enabled
+ return false if inbox.channel.reauthorization_required?
+
+ return true unless ChatwootApp.chatwoot_cloud?
+ return false if default_plan?(inbox.account)
+
+ true
end
end
diff --git a/app/mailboxes/incoming_email_validity_helper.rb b/app/mailboxes/incoming_email_validity_helper.rb
index 252ef5257..9483c9768 100644
--- a/app/mailboxes/incoming_email_validity_helper.rb
+++ b/app/mailboxes/incoming_email_validity_helper.rb
@@ -4,16 +4,17 @@ module IncomingEmailValidityHelper
def incoming_email_from_valid_email?
return false unless valid_external_email_for_active_account?
+ # Return if email doesn't have a valid sender
+ # This can happen in cases like bounce emails for invalid contact email address
+ return false unless Devise.email_regexp.match?(@processed_mail.original_sender)
+
+ # Process bounced emails, as regular emails
+ return true if @processed_mail.bounced?
+
# we skip processing auto reply emails like delivery status notifications
# out of office replies, etc.
return false if auto_reply_email?
- # return if email doesn't have a valid sender
- # This can happen in cases like bounce emails for invalid contact email address
- # TODO: Handle the bounce separately and mark the contact as invalid in case of reply bounces
- # The returned value could be "\"\"" for some email clients
- return false unless Devise.email_regexp.match?(@processed_mail.original_sender)
-
true
end
diff --git a/app/mailers/portal_instructions_mailer.rb b/app/mailers/portal_instructions_mailer.rb
index eccbd7db1..284e3e9de 100644
--- a/app/mailers/portal_instructions_mailer.rb
+++ b/app/mailers/portal_instructions_mailer.rb
@@ -15,7 +15,7 @@ class PortalInstructionsMailer < ApplicationMailer
private
def liquid_locals
- { cname_record: @cname_record }
+ super.merge({ cname_record: @cname_record })
end
def generate_cname_record
diff --git a/app/presenters/mail_presenter.rb b/app/presenters/mail_presenter.rb
index 890e97a78..e57831c96 100644
--- a/app/presenters/mail_presenter.rb
+++ b/app/presenters/mail_presenter.rb
@@ -157,6 +157,10 @@ class MailPresenter < SimpleDelegator
auto_submitted? || x_auto_reply?
end
+ def bounced?
+ @mail.bounced? || @mail['X-Failed-Recipients'].try(:value).present?
+ end
+
def notification_email_from_chatwoot?
# notification emails are send via mailer sender email address. so it should match
original_sender == Mail::Address.new(ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot
')).address
diff --git a/deployment/setup_20.04.sh b/deployment/setup_20.04.sh
index aaa928f4d..5a40ee068 100644
--- a/deployment/setup_20.04.sh
+++ b/deployment/setup_20.04.sh
@@ -2,7 +2,7 @@
# Description: Install and manage a Chatwoot installation.
# OS: Ubuntu 20.04 LTS, 22.04 LTS, 24.04 LTS
-# Script Version: 3.4.1
+# Script Version: 3.4.2
# Run this script as root
set -eu -o errexit -o pipefail -o noclobber -o nounset
@@ -990,7 +990,7 @@ EOF
# Check if CW_VERSION is 4.0 or above
if [[ "$(printf '%s\n' "$CW_VERSION" "4.0" | sort -V | head -n 1)" == "4.0" ]]; then
echo "Chatwoot v4.0 and above requires pgvector support in PostgreSQL."
- read -p "Does your postgres support pgvector and want to proceed with the upgrade? [Y/n]: " user_input
+ read -p "Does your postgres support pgvector and want to proceed with the upgrade? [y/N]: " user_input
user_input=${user_input:-Y}
if [[ "$user_input" =~ ^([yY][eE][sS]|[yY])$ ]]; then
echo "Proceeding with the upgrade..."
@@ -1005,6 +1005,7 @@ EOF
upgrade_redis
upgrade_node
get_pnpm
+
sudo -i -u chatwoot << EOF
# Navigate to the Chatwoot directory
@@ -1016,9 +1017,9 @@ EOF
# Ensure the ruby version is upto date
# Parse the latest ruby version
- latest_ruby_version="$(cat '.ruby-version')"
- rvm install "ruby-$latest_ruby_version"
- rvm use "$latest_ruby_version" --default
+ latest_ruby_version="\$(cat '.ruby-version')"
+ rvm install "ruby-\$latest_ruby_version"
+ rvm use "\$latest_ruby_version" --default
# Update dependencies
bundle
diff --git a/spec/enterprise/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb b/spec/enterprise/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb
new file mode 100644
index 000000000..ef5b9b0c7
--- /dev/null
+++ b/spec/enterprise/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb
@@ -0,0 +1,26 @@
+require 'rails_helper'
+
+RSpec.describe Inboxes::FetchImapEmailInboxesJob do
+ context 'when chatwoot_cloud is enabled' do
+ let(:account) { create(:account) }
+ let(:premium_account) { create(:account, custom_attributes: { plan_name: 'Startups' }) }
+ let(:imap_email_channel) { create(:channel_email, imap_enabled: true, account: account) }
+ let(:premium_imap_channel) { create(:channel_email, imap_enabled: true, account: premium_account) }
+
+ before do
+ premium_account.custom_attributes['plan_name'] = 'Startups'
+ InstallationConfig.where(name: 'DEPLOYMENT_ENV').first_or_create!(value: 'cloud')
+ InstallationConfig.where(name: 'CHATWOOT_CLOUD_PLANS').first_or_create!(value: [{ 'name' => 'Hacker' }])
+ end
+
+ it 'skips inboxes with default plan' do
+ expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(imap_email_channel)
+ described_class.perform_now
+ end
+
+ it 'processes inboxes with premium plan' do
+ expect(Inboxes::FetchImapEmailsJob).to receive(:perform_later).with(premium_imap_channel)
+ described_class.perform_now
+ end
+ end
+end
diff --git a/spec/fixtures/files/bounced_gmail.eml b/spec/fixtures/files/bounced_gmail.eml
new file mode 100644
index 000000000..2c45bcd4d
--- /dev/null
+++ b/spec/fixtures/files/bounced_gmail.eml
@@ -0,0 +1,120 @@
+Delivered-To: robert.smith@gmail.com
+Return-Path: <>
+Subject: Delivery Status Notification (Failure)
+From: Mail Delivery Subsystem
+To: robert.smith@gmail.com
+Content-Type: multipart/report; boundary="00000000000093475906390e1e9b"; report-type=delivery-status
+Auto-Submitted: auto-replied
+Message-ID: <686707c9.050a0220.302e7d.0cb2.GMR@mx.google.com>
+Date: Thu, 03 Jul 2025 15:44:25 -0700 (PDT)
+X-Failed-Recipients: alex.jones@fictionalcorp.com
+
+--00000000000093475906390e1e9b
+Content-Type: multipart/related; boundary="000000000000936d8406390e1ec7"
+
+--000000000000936d8406390e1ec7
+Content-Type: multipart/alternative; boundary="000000000000936d9006390e1ec8"
+
+--000000000000936d9006390e1ec8
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: quoted-printable
+
+
+** Address not found **
+
+Your message wasn't delivered to alex.jones@fictionalcorp.com because the address co=
+uldn't be found or is unable to receive email.
+
+Learn more here: https://support.google.com/mail/?p=3DNoSuchUser
+
+The response was:
+
+550 5.1.1 The email account that you tried to reach does not exist. Please =
+try double-checking the recipient's email address for typos or unnecessary =
+spaces. For more information, go to https://support.google.com/mail/?p=3DNo=
+SuchUser d2e1a72fcca58-74ce2b0525csor332154b3a.0 - gsmtp
+
+--000000000000936d9006390e1ec8
+Content-Type: text/html; charset="UTF-8"
+Content-Transfer-Encoding: quoted-printable
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ |
+
+
+The response was:
+
+550 5.1.1 The email account that you tried to reach does not exist. Please =
+try double-checking the recipient's email address for typos or unnecessary =
+spaces. For more information, go to https://support.google.com/mail/?p=3DNo=
+SuchUser d2e1a72fcca58-74ce2b0525csor332154b3a.0 - gsmtp
+
+ |
+
+
+
+
+
+--000000000000936d9006390e1ec8--
+--000000000000936d8406390e1ec7
+Content-Type: image/png; name="icon.png"
+Content-Disposition: attachment; filename="icon.png"
+Content-Transfer-Encoding: base64
+Content-ID:
+
+--000000000000936d8406390e1ec7--
+--00000000000093475906390e1e9b
+Content-Type: message/delivery-status
+
+--00000000000093475906390e1e9b
+Content-Type: message/rfc822
+
+Date: Thu, 03 Jul 2025 15:44:23 -0700
+From: Robert Smith
+Reply-To: robert.smith@gmail.com
+To: alex.jones@fictionalcorp.com
+Message-ID:
+In-Reply-To:
+Subject: Just checking in
+Mime-Version: 1.0
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+
+Hey, just checking in. Let me know if you got my earlier message.
+
+--00000000000093475906390e1e9b--
diff --git a/spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb b/spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb
index 18685a649..abcab1e8f 100644
--- a/spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb
+++ b/spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb
@@ -3,6 +3,7 @@ require 'rails_helper'
RSpec.describe Inboxes::FetchImapEmailInboxesJob do
let(:account) { create(:account) }
let(:suspended_account) { create(:account, status: 'suspended') }
+ let(:premium_account) { create(:account, custom_attributes: { plan_name: 'Startups' }) }
let(:imap_email_channel) do
create(:channel_email, imap_enabled: true, account: account)
@@ -16,6 +17,19 @@ RSpec.describe Inboxes::FetchImapEmailInboxesJob do
create(:channel_email, imap_enabled: false, account: account)
end
+ let(:reauth_required_channel) do
+ create(:channel_email, imap_enabled: true, account: account)
+ end
+
+ let(:premium_imap_channel) do
+ create(:channel_email, imap_enabled: true, account: premium_account)
+ end
+
+ before do
+ reauth_required_channel.prompt_reauthorization!
+ premium_account.custom_attributes['plan_name'] = 'Startups'
+ end
+
it 'enqueues the job' do
expect { described_class.perform_later }.to have_enqueued_job(described_class)
.on_queue('scheduled_jobs')
@@ -44,5 +58,11 @@ RSpec.describe Inboxes::FetchImapEmailInboxesJob do
described_class.perform_now
end
+
+ it 'skips channels requiring reauthorization' do
+ expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(reauth_required_channel)
+
+ described_class.perform_now
+ end
end
end
diff --git a/spec/mailboxes/imap/imap_mailbox_spec.rb b/spec/mailboxes/imap/imap_mailbox_spec.rb
index cc72be18b..fc94c98be 100644
--- a/spec/mailboxes/imap/imap_mailbox_spec.rb
+++ b/spec/mailboxes/imap/imap_mailbox_spec.rb
@@ -115,6 +115,14 @@ RSpec.describe Imap::ImapMailbox do
end
end
+ context 'when the email is bounced' do
+ let!(:bounced_mail) { create_inbound_email_from_fixture('bounced_gmail.eml') }
+
+ it 'processes the bounced email' do
+ expect { class_instance.process(bounced_mail.mail, channel) }.to change(Message, :count)
+ end
+ end
+
context 'when a reply for existing email conversation' do
let(:prev_conversation) { create(:conversation, account: account, inbox: channel.inbox, assignee: agent) }
let(:reply_mail) do