From ad1539c6cf67cec770fe7eec718958aa60e9a03f Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Tue, 17 Mar 2026 00:10:42 -0700 Subject: [PATCH] fix(email): Allow inbox OAuth replies without global SMTP (#13820) Email inbox replies now work for Google and Microsoft OAuth inboxes even when the self-hosted instance does not have global SMTP configured. This keeps agent replies working for email channels that already have valid inbox-level delivery settings. fixes: chatwoot/chatwoot#13118 closes: chatwoot/chatwoot#13118 ## Why Self-hosted email inbox replies were blocked by a global SMTP guard in the `email_reply` path. For OAuth-backed email inboxes, outbound delivery is configured at the inbox level, so the mailer returned early and the reply flow failed before sending. ## What this change does - Allows the `email_reply` path to proceed when the inbox has SMTP configured - Allows the `email_reply` path to proceed when the inbox has Google or Microsoft OAuth delivery configured - Renames the touched mailer helper predicates to `?` methods for clarity ## Validation - Configure a Google email inbox on a self-hosted instance without global `SMTP_ADDRESS` - Reply from Chatwoot to an existing email conversation - Confirm the reply is sent through the inbox OAuth SMTP configuration - Run `bundle exec rspec spec/mailers/conversation_reply_mailer_spec.rb:595` --------- Co-authored-by: Muhsin Keloth --- app/mailers/conversation_reply_mailer.rb | 3 +-- .../conversation_reply_mailer_helper.rb | 10 ++++---- .../mailers/conversation_reply_mailer_spec.rb | 24 +++++++++++++++++++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/app/mailers/conversation_reply_mailer.rb b/app/mailers/conversation_reply_mailer.rb index 7fee05596..220531221 100644 --- a/app/mailers/conversation_reply_mailer.rb +++ b/app/mailers/conversation_reply_mailer.rb @@ -35,9 +35,8 @@ class ConversationReplyMailer < ApplicationMailer end def email_reply(message) - return unless smtp_config_set_or_development? - init_conversation_attributes(message.conversation) + return unless smtp_config_set_or_development? || email_smtp_enabled? || (email_imap_enabled? && email_oauth_enabled?) @message = message prepare_mail(true) diff --git a/app/mailers/conversation_reply_mailer_helper.rb b/app/mailers/conversation_reply_mailer_helper.rb index cce236ec7..dc3e0c3fd 100644 --- a/app/mailers/conversation_reply_mailer_helper.rb +++ b/app/mailers/conversation_reply_mailer_helper.rb @@ -79,28 +79,28 @@ module ConversationReplyMailerHelper @options[:delivery_method_options] = smtp_settings end - def email_smtp_enabled + def email_smtp_enabled? @inbox.inbox_type == 'Email' && @channel.smtp_enabled end - def email_imap_enabled + def email_imap_enabled? @inbox.inbox_type == 'Email' && @channel.imap_enabled end - def email_oauth_enabled + def email_oauth_enabled? @inbox.inbox_type == 'Email' && (@channel.microsoft? || @channel.google?) end def email_from return Email::FromBuilder.new(inbox: @inbox, message: current_message).build if @account.feature_enabled?(:reply_mailer_migration) - email_oauth_enabled || email_smtp_enabled ? channel_email_with_name : from_email_with_name + email_oauth_enabled? || email_smtp_enabled? ? channel_email_with_name : from_email_with_name end def email_reply_to return Email::ReplyToBuilder.new(inbox: @inbox, message: current_message).build if @account.feature_enabled?(:reply_mailer_migration) - email_imap_enabled ? @channel.email : reply_email + email_imap_enabled? ? @channel.email : reply_email end # Use channel email domain in case of account email domain is not set for custom message_id and in_reply_to diff --git a/spec/mailers/conversation_reply_mailer_spec.rb b/spec/mailers/conversation_reply_mailer_spec.rb index 2576361fa..86a2363e9 100644 --- a/spec/mailers/conversation_reply_mailer_spec.rb +++ b/spec/mailers/conversation_reply_mailer_spec.rb @@ -591,6 +591,30 @@ RSpec.describe ConversationReplyMailer do expect(mail.delivery_method.settings[:address]).to eq 'smtp.gmail.com' expect(mail.delivery_method.settings[:port]).to eq 587 end + + it 'uses inbox oauth smtp when global smtp config is unavailable' do + allow(class_instance).to receive(:smtp_config_set_or_development?).and_return(false) + + mail = described_class.email_reply(message) + + expect(mail).not_to be_nil + expect(mail.delivery_method.settings[:address]).to eq 'smtp.gmail.com' + expect(mail.delivery_method.settings[:port]).to eq 587 + end + end + + context 'when oauth provider is set but imap is disabled' do + let(:google_channel) do + create(:channel_email, imap_enabled: false, account: account, provider: 'google', provider_config: { access_token: 'access_token' }) + end + let(:conversation) { create(:conversation, assignee: agent, inbox: google_channel.inbox, account: account).reload } + let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') } + + it 'does not build the mail without global smtp' do + allow(class_instance).to receive(:smtp_config_set_or_development?).and_return(false) + + expect(described_class.email_reply(message).deliver_now).to be_nil + end end context 'when smtp disabled for email channel', :test do