diff --git a/app/jobs/inboxes/fetch_imap_emails_job.rb b/app/jobs/inboxes/fetch_imap_emails_job.rb index ec5717f3b..8936e1dd9 100644 --- a/app/jobs/inboxes/fetch_imap_emails_job.rb +++ b/app/jobs/inboxes/fetch_imap_emails_job.rb @@ -6,26 +6,30 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob def perform(channel, interval = 1) return unless should_fetch_email?(channel) - key = format(::Redis::Alfred::EMAIL_MESSAGE_MUTEX, inbox_id: channel.inbox.id) - - with_lock(key, 5.minutes) do - process_email_for_channel(channel, interval) - end - rescue *ExceptionList::IMAP_EXCEPTIONS => e - Rails.logger.error "Authorization error for email channel - #{channel.inbox.id} : #{e.message}" - rescue EOFError, OpenSSL::SSL::SSLError, Net::IMAP::NoResponseError, Net::IMAP::BadResponseError, Net::IMAP::InvalidResponseError, - Net::IMAP::ResponseParseError, Net::IMAP::ResponseReadError, Net::IMAP::ResponseTooLargeError => e - Rails.logger.error "Error for email channel - #{channel.inbox.id} : #{e.message}" - rescue LockAcquisitionError - Rails.logger.error "Lock failed for #{channel.inbox.id}" + fetch_emails_with_backoff(channel, interval) rescue StandardError => e ChatwootExceptionTracker.new(e, account: channel.account).capture_exception end private + def fetch_emails_with_backoff(channel, interval) + key = format(::Redis::Alfred::EMAIL_MESSAGE_MUTEX, inbox_id: channel.inbox.id) + with_lock(key, 5.minutes) { process_email_for_channel(channel, interval) } + channel.clear_imap_backoff! + rescue *ExceptionList::IMAP_EXCEPTIONS => e + Rails.logger.error "Authorization error for email channel - #{channel.inbox.id} : #{e.message}" + channel.apply_imap_backoff! + rescue EOFError, OpenSSL::SSL::SSLError, Net::IMAP::NoResponseError, Net::IMAP::BadResponseError, Net::IMAP::InvalidResponseError, + Net::IMAP::ResponseParseError, Net::IMAP::ResponseReadError, Net::IMAP::ResponseTooLargeError => e + Rails.logger.error "Error for email channel - #{channel.inbox.id} : #{e.message}" + channel.apply_imap_backoff! + rescue LockAcquisitionError + Rails.logger.error "Lock failed for #{channel.inbox.id}" + end + def should_fetch_email?(channel) - channel.imap_enabled? && !channel.reauthorization_required? + channel.imap_enabled? && !channel.reauthorization_required? && !channel.in_backoff? end def process_email_for_channel(channel, interval) diff --git a/app/models/channel/email.rb b/app/models/channel/email.rb index b1124dd75..893f2bdac 100644 --- a/app/models/channel/email.rb +++ b/app/models/channel/email.rb @@ -39,6 +39,8 @@ class Channel::Email < ApplicationRecord include Reauthorizable AUTHORIZATION_ERROR_THRESHOLD = 10 + MAX_IMAP_RETRIES = 5 + IMAP_BACKOFF_BASE_SECONDS = 60 # TODO: Remove guard once encryption keys become mandatory (target 3-4 releases out). if Chatwoot.encryption_configured? @@ -72,6 +74,27 @@ class Channel::Email < ApplicationRecord imap_enabled && imap_address == 'imap.gmail.com' end + def in_backoff? + imap_retry_after.present? && imap_retry_after > Time.current + end + + def apply_imap_backoff! + new_count = imap_retry_count + 1 + if new_count >= MAX_IMAP_RETRIES + update!(imap_retry_count: 0, imap_retry_after: nil) + prompt_reauthorization! + else + backoff_seconds = IMAP_BACKOFF_BASE_SECONDS * (2**new_count) + update!(imap_retry_count: new_count, imap_retry_after: backoff_seconds.seconds.from_now) + end + end + + def clear_imap_backoff! + return unless imap_retry_count.positive? || imap_retry_after.present? + + update!(imap_retry_count: 0, imap_retry_after: nil) + end + private def ensure_forward_to_email diff --git a/db/migrate/20260223100000_add_imap_backoff_to_channel_email.rb b/db/migrate/20260223100000_add_imap_backoff_to_channel_email.rb new file mode 100644 index 000000000..304395ab9 --- /dev/null +++ b/db/migrate/20260223100000_add_imap_backoff_to_channel_email.rb @@ -0,0 +1,6 @@ +class AddImapBackoffToChannelEmail < ActiveRecord::Migration[7.2] + def change + add_column :channel_email, :imap_retry_count, :integer, default: 0, null: false + add_column :channel_email, :imap_retry_after, :datetime + end +end