Files
moreminimore-chat/app/services/imap/base_fetch_email_service.rb
Shivam Mishra 6ed0f11e6b chore: add diagnostic logs to imap email fetch pipeline (#15187)
Adds lifecycle log lines across the IMAP email fetch pipeline so we can
trace where a fetch run spends time or silently stops. Some IMAP inboxes
showed increasingly sparse fetch runs in production without matching
errors; existing logs did not cover job start, lock acquisition,
connection setup, or early-exit paths.

## What changed

- `Inboxes::FetchImapEmailInboxesJob`: log when a fetch job is enqueued
per inbox (temporary instrumentation).
- `Inboxes::FetchImapEmailsJob`: log job start, skip reason on early
return, lock attempt/acquisition, fetched count, processing completion,
job completion, and unexpected errors.
- `Imap::BaseFetchEmailService`: log successful IMAP connection and the
start of each header batch fetch.

All lines share the `[IMAP::FETCH_EMAIL_SERVICE]` tag for searchability.
No behavior changes.

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-07-30 16:27:59 +05:30

164 lines
5.2 KiB
Ruby

require 'net/imap'
class Imap::BaseFetchEmailService
MAX_MESSAGES_PER_SYNC = 500
pattr_initialize [:channel!, :interval]
def fetch_emails
# Override this method
end
def perform
inbound_emails = fetch_emails
terminate_imap_connection
inbound_emails
end
private
def authentication_type
# Override this method
end
def imap_password
# Override this method
end
def imap_client
@imap_client ||= build_imap_client
end
def mail_info_logger(inbound_mail, seq_no)
return if Rails.env.test?
Rails.logger.info("
#{channel.provider} Email id: #{inbound_mail.from} - message_source_id: #{inbound_mail.message_id} - sequence id: #{seq_no}")
end
def email_already_present?(channel, message_id)
# exists? avoids Message's default_scope ORDER BY, which full-scans large inboxes
channel.inbox.messages.exists?(source_id: message_id) || deleted_message_tracker.deleted?(message_id)
end
def deleted_message_tracker
@deleted_message_tracker ||= Imap::DeletedMessageTracker.new(inbox: channel.inbox)
end
def fetch_mail_for_channel
message_ids_with_seq = fetch_message_ids_with_sequence
message_ids_with_seq.filter_map do |message_id_with_seq|
process_message_id(message_id_with_seq)
end
end
def process_message_id(message_id_with_seq)
seq_no, message_id = message_id_with_seq
if message_id.blank?
Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Empty message id for #{channel.email} with seq no. <#{seq_no}>."
return
end
return if email_already_present?(channel, message_id)
# Fetch the original mail content using the sequence no.
# BODY.PEEK[] avoids RFC822 parser failures seen with some IMAP servers.
mail_str = imap_client.fetch(seq_no, 'BODY.PEEK[]')[0].attr['BODY[]']
if mail_str.blank?
Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Fetch failed for #{channel.email} with message-id <#{message_id}>."
return
end
inbound_mail = build_mail_from_string(mail_str)
mail_info_logger(inbound_mail, seq_no)
inbound_mail
end
# Sends a FETCH command to retrieve data associated with a message in the mailbox.
# You can send batches of message sequence number in `.fetch` method.
def fetch_message_ids_with_sequence
seq_nums = fetch_available_mail_sequence_numbers
Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Fetching mails from #{channel.email}, found #{seq_nums.length}."
message_ids_with_seq = []
seq_nums.each_slice(MAX_MESSAGES_PER_SYNC).each do |batch|
append_message_ids_for_batch(batch, message_ids_with_seq)
if message_ids_with_seq.length >= MAX_MESSAGES_PER_SYNC
Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Reached MAX_MESSAGES_PER_SYNC=#{MAX_MESSAGES_PER_SYNC} for #{channel.email}, stopping sync."
break
end
end
message_ids_with_seq
end
def append_message_ids_for_batch(batch, message_ids_with_seq)
# Fetch only message-id only without mail body or contents.
Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Starting header batch of #{batch.length} for #{channel.email}"
batch_message_ids = imap_client.fetch(batch, 'BODY.PEEK[HEADER]')
Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Fetching the batch for #{channel.email}. Found #{batch_message_ids&.length} messages."
# .fetch returns an array of Net::IMAP::FetchData or nil
# (instead of an empty array) if there is no matching message.
if batch_message_ids.blank?
Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Fetching the batch failed for #{channel.email}."
return
end
batch_message_ids.each do |data|
entry = build_message_id_entry(data)
next if entry.nil?
message_ids_with_seq.push(entry)
break if message_ids_with_seq.length >= MAX_MESSAGES_PER_SYNC
end
end
def build_message_id_entry(data)
mail = build_mail_from_string(data.attr['BODY[HEADER]'])
return nil if MailPresenter.new(mail, channel.account).notification_email_from_chatwoot?
message_id = mail.message_id
return nil if message_id.blank?
return nil if email_already_present?(channel, message_id)
[data.seqno, message_id]
end
# Sends a SEARCH command to search the mailbox for messages that were
# created between yesterday (or given date) and today and returns message sequence numbers.
# Return <message set>
def fetch_available_mail_sequence_numbers
imap_client.search(['SINCE', since])
end
def build_imap_client
imap = Net::IMAP.new(channel.imap_address, port: channel.imap_port, ssl: channel.imap_enable_ssl)
Imap::Authentication.authenticate!(imap, authentication_type, channel.imap_login, imap_password)
imap.select('INBOX')
Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] IMAP connection established for #{channel.email}"
imap
end
def terminate_imap_connection
imap_client.logout
rescue Net::IMAP::Error => e
Rails.logger.info "Logout failed for #{channel.email} - #{e.message}."
imap_client.disconnect
end
def build_mail_from_string(raw_email_content)
Mail.read_from_string(raw_email_content)
end
def since
previous_day = Time.zone.today - (interval || 1).to_i
previous_day.strftime('%d-%b-%Y')
end
end