chore: Migrate mailers from the worker to jobs (#12331)

Previously, email replies were handled inside workers. There was no
execution logs. This meant if emails silently failed (as reported by a
customer), we had no way to trace where the issue happened, the only
assumption was “no error = mail sent.”

By moving email handling into jobs, we now have proper execution logs
for each attempt. This makes it easier to debug delivery issues and
would have better visibility when investigating customer reports.

Fixes
https://linear.app/chatwoot/issue/CW-5538/emails-are-not-sentdelivered-to-the-contact

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
Pranav
2025-10-21 16:36:37 -07:00
committed by GitHub
parent b4c4f328b2
commit 254d5dcf9a
13 changed files with 446 additions and 165 deletions

View File

@@ -0,0 +1,18 @@
class Email::SendOnEmailService < Base::SendOnChannelService
private
def channel_class
Channel::Email
end
def perform_reply
return unless message.email_notifiable_message?
reply_mail = ConversationReplyMailer.with(account: message.account).email_reply(message).deliver_now
Rails.logger.info("Email message #{message.id} sent with source_id: #{reply_mail.message_id}")
message.update(source_id: reply_mail.message_id)
rescue StandardError => e
ChatwootExceptionTracker.new(e, account: message.account).capture_exception
Messages::StatusUpdateService.new(message, 'failed', e.message).perform
end
end

View File

@@ -0,0 +1,38 @@
class Messages::SendEmailNotificationService
pattr_initialize [:message!]
def perform
return unless should_send_email_notification?
conversation = message.conversation
conversation_mail_key = format(::Redis::Alfred::CONVERSATION_MAILER_KEY, conversation_id: conversation.id)
# Atomically set redis key to prevent duplicate email workers. Keep the key alive longer than
# the worker delay (1 hour) so slow queues don't enqueue duplicate jobs, but let it expire if
# the worker never manages to clean up.
return unless Redis::Alfred.set(conversation_mail_key, message.id, nx: true, ex: 1.hour.to_i)
ConversationReplyEmailWorker.perform_in(2.minutes, conversation.id, message.id)
end
private
def should_send_email_notification?
return false unless message.email_notifiable_message?
return false if message.conversation.contact.email.blank?
email_reply_enabled?
end
def email_reply_enabled?
inbox = message.inbox
case inbox.channel.class.to_s
when 'Channel::WebWidget'
inbox.channel.continuity_via_email
when 'Channel::Api'
inbox.account.feature_enabled?('email_continuity_on_api_channel')
else
false
end
end
end