diff --git a/app/jobs/inboxes/fetch_imap_emails_job.rb b/app/jobs/inboxes/fetch_imap_emails_job.rb index c32e6eee2..5722e76a7 100644 --- a/app/jobs/inboxes/fetch_imap_emails_job.rb +++ b/app/jobs/inboxes/fetch_imap_emails_job.rb @@ -7,6 +7,7 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob 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) end @@ -28,128 +29,16 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob end def process_email_for_channel(channel) - if channel.microsoft? - fetch_mail_for_ms_provider(channel) - else - fetch_mail_for_channel(channel) - end - # clearing old failures like timeouts since the mail is now successfully processed - channel.reauthorized! - end - - def fetch_mail_for_channel(channel) - imap_client = build_imap_client(channel, channel.imap_password, 'PLAIN') - - message_ids_with_seq = fetch_message_ids_with_sequence(imap_client, channel) - message_ids_with_seq.each do |message_id_with_seq| - process_message_id(channel, imap_client, message_id_with_seq) - end - end - - def process_message_id(channel, imap_client, message_id_with_seq) - seq_no, message_id = message_id_with_seq - - return if email_already_present?(channel, message_id) - - if message_id.blank? - Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Empty message id for #{channel.email} with seq no. <#{seq_no}>." - return - end - - # Fetch the original mail content using the sequence no - mail_str = imap_client.fetch(seq_no, 'RFC822')[0].attr['RFC822'] - - 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(channel, inbound_mail, seq_no) - process_mail(inbound_mail, channel) - 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(imap_client, channel) - seq_nums = fetch_available_mail_sequence_numbers(imap_client) - - Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Fetching mails from #{channel.email}, found #{seq_nums.length}." - - message_ids_with_seq = [] - seq_nums.each_slice(10).each do |batch| - # Fetch only message-id only without mail body or contents. - batch_message_ids = imap_client.fetch(batch, 'BODY.PEEK[HEADER]') - - # .fetch returns an array of Net::IMAP::FetchData or nil - # (instead of an empty array) if there is no matching message. - # Check - if batch_message_ids.blank? - Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Fetching the batch failed for #{channel.email}." - next - end - - batch_message_ids.each do |data| - message_id = build_mail_from_string(data.attr['BODY[HEADER]']).message_id - message_ids_with_seq.push([data.seqno, message_id]) - end - end - - message_ids_with_seq - end - - # Sends a SEARCH command to search the mailbox for messages that were - # created between yesterday and today and returns message sequence numbers. - # Return - def fetch_available_mail_sequence_numbers(imap_client) - imap_client.search(['SINCE', yesterday]) - end - - def fetch_mail_for_ms_provider(channel) - return if channel.provider_config['access_token'].blank? - - access_token = valid_access_token channel - - return unless access_token - - imap_client = build_imap_client(channel, access_token, 'XOAUTH2') - process_mails(imap_client, channel) - end - - def process_mails(imap_client, channel) - fetch_available_mail_sequence_numbers(imap_client).each do |seq_no| - inbound_mail = Mail.read_from_string imap_client.fetch(seq_no, 'RFC822')[0].attr['RFC822'] - - mail_info_logger(channel, inbound_mail, seq_no) - - next if channel.inbox.messages.find_by(source_id: inbound_mail.message_id).present? - + inbound_emails = if channel.microsoft? + Imap::MicrosoftFetchEmailService.new(channel: channel).perform + else + Imap::FetchEmailService.new(channel: channel).perform + end + inbound_emails.map do |inbound_mail| process_mail(inbound_mail, channel) end end - def mail_info_logger(channel, inbound_mail, uid) - return if Rails.env.test? - - Rails.logger.info(" - #{channel.provider} Email id: #{inbound_mail.from} - message_source_id: #{inbound_mail.message_id} - sequence id: #{uid}") - end - - def build_imap_client(channel, access_token, auth_method) - imap = Net::IMAP.new(channel.imap_address, channel.imap_port, true) - imap.authenticate(auth_method, channel.imap_login, access_token) - imap.select('INBOX') - imap - end - - def email_already_present?(channel, message_id) - channel.inbox.messages.find_by(source_id: message_id).present? - end - - def build_mail_from_string(raw_email_content) - Mail.read_from_string(raw_email_content) - end - def process_mail(inbound_mail, channel) Imap::ImapMailbox.new.process(inbound_mail, channel) rescue StandardError => e @@ -157,13 +46,4 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob Rails.logger.error(" #{channel.provider} Email dropped: #{inbound_mail.from} and message_source_id: #{inbound_mail.message_id}") end - - # Making sure the access token is valid for microsoft provider - def valid_access_token(channel) - Microsoft::RefreshOauthTokenService.new(channel: channel).access_token - end - - def yesterday - (Time.zone.today - 1).strftime('%d-%b-%Y') - end end diff --git a/app/services/imap/base_fetch_email_service.rb b/app/services/imap/base_fetch_email_service.rb new file mode 100644 index 000000000..4f49dc980 --- /dev/null +++ b/app/services/imap/base_fetch_email_service.rb @@ -0,0 +1,115 @@ +require 'net/imap' + +class Imap::BaseFetchEmailService + pattr_initialize [:channel!] + + def perform + # Override this method + 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) + channel.inbox.messages.find_by(source_id: message_id).present? + 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 + mail_str = imap_client.fetch(seq_no, 'RFC822')[0].attr['RFC822'] + + 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(10).each do |batch| + # Fetch only message-id only without mail body or contents. + batch_message_ids = imap_client.fetch(batch, 'BODY.PEEK[HEADER]') + + # .fetch returns an array of Net::IMAP::FetchData or nil + # (instead of an empty array) if there is no matching message. + # Check + if batch_message_ids.blank? + Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Fetching the batch failed for #{channel.email}." + next + end + + batch_message_ids.each do |data| + message_id = build_mail_from_string(data.attr['BODY[HEADER]']).message_id + message_ids_with_seq.push([data.seqno, message_id]) + end + end + + message_ids_with_seq + end + + # Sends a SEARCH command to search the mailbox for messages that were + # created between yesterday and today and returns message sequence numbers. + # Return + def fetch_available_mail_sequence_numbers + imap_client.search(['SINCE', yesterday]) + end + + def build_imap_client + imap = Net::IMAP.new(channel.imap_address, port: channel.imap_port, ssl: true) + imap.authenticate(authentication_type, channel.imap_login, imap_password) + imap.select('INBOX') + imap + end + + def build_mail_from_string(raw_email_content) + Mail.read_from_string(raw_email_content) + end + + def yesterday + (Time.zone.today - 1).strftime('%d-%b-%Y') + end +end diff --git a/app/services/imap/fetch_email_service.rb b/app/services/imap/fetch_email_service.rb new file mode 100644 index 000000000..f6d8de774 --- /dev/null +++ b/app/services/imap/fetch_email_service.rb @@ -0,0 +1,15 @@ +class Imap::FetchEmailService < Imap::BaseFetchEmailService + def perform + fetch_mail_for_channel + end + + private + + def authentication_type + 'PLAIN' + end + + def imap_password + channel.imap_password + end +end diff --git a/app/services/imap/microsoft_fetch_email_service.rb b/app/services/imap/microsoft_fetch_email_service.rb new file mode 100644 index 000000000..b6d3c03b8 --- /dev/null +++ b/app/services/imap/microsoft_fetch_email_service.rb @@ -0,0 +1,17 @@ +class Imap::MicrosoftFetchEmailService < Imap::BaseFetchEmailService + def perform + return if channel.provider_config['access_token'].blank? + + fetch_mail_for_channel + end + + private + + def authentication_type + 'XOAUTH2' + end + + def imap_password + Microsoft::RefreshOauthTokenService.new(channel: channel).access_token + end +end diff --git a/spec/factories/channel/channel_email.rb b/spec/factories/channel/channel_email.rb index 3ec96aaa4..32cdb0488 100644 --- a/spec/factories/channel/channel_email.rb +++ b/spec/factories/channel/channel_email.rb @@ -25,5 +25,14 @@ FactoryBot.define do end provider { 'microsoft' } end + + trait :imap_email do + imap_enabled { true } + imap_address { 'imap.gmail.com' } + imap_port { 993 } + imap_login { 'email@example.com' } + imap_password { 'random-password' } + imap_enable_ssl { true } + end end end diff --git a/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb b/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb index 12828bf68..f40e39151 100644 --- a/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb +++ b/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb @@ -1,161 +1,106 @@ require 'rails_helper' RSpec.describe Inboxes::FetchImapEmailsJob do + include ActiveJob::TestHelper include ActionMailbox::TestHelper let(:account) { create(:account) } - let(:imap_email_channel) do - create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com', imap_port: 993, imap_login: 'imap@gmail.com', - imap_password: 'password', account: account) - end - + let(:imap_email_channel) { create(:channel_email, :imap_email, account: account) } + let(:channel_with_imap_disabled) { create(:channel_email, :imap_email, imap_enabled: false, account: account) } let(:microsoft_imap_email_channel) { create(:channel_email, :microsoft_email) } - let(:ms_email_inbox) { create(:inbox, channel: microsoft_imap_email_channel, account: account) } - let!(:conversation) { create(:conversation, inbox: imap_email_channel.inbox, account: account) } - let(:inbound_mail) { create_inbound_email_from_mail(from: 'testemail@gmail.com', to: 'imap@outlook.com', subject: 'Hello!') } - let(:inbound_mail_with_attachments) { create_inbound_email_from_fixture('multiple_attachments.eml') } - it 'enqueues the job' do - expect { described_class.perform_later }.to have_enqueued_job(described_class) - .on_queue('scheduled_jobs') - end - - context 'when imap fetch new emails' do - it 'process the email' do - email = Mail.new do - to 'test@outlook.com' - from 'test@gmail.com' - subject :test.to_s - body 'hello' - end - - imap_fetch_mail = Net::IMAP::FetchData.new - imap_fetch_mail.attr = { seqno: 1, RFC822: email }.with_indifferent_access - - imap = double - - allow(Net::IMAP).to receive(:new).and_return(imap) - allow(imap).to receive(:authenticate) - allow(imap).to receive(:select) - allow(imap).to receive(:search).and_return([1]) - allow(imap).to receive(:fetch).and_return([imap_fetch_mail]) - - read_mail = Mail::Message.new(date: DateTime.now, from: 'testemail@gmail.com', to: 'imap@outlook.com', subject: 'Hello!') - allow(Mail).to receive(:read_from_string).and_return(inbound_mail.mail) - - imap_mailbox = double - - allow(Imap::ImapMailbox).to receive(:new).and_return(imap_mailbox) - expect(imap_mailbox).to receive(:process).with(read_mail, imap_email_channel).once - - described_class.perform_now(imap_email_channel) + describe '#perform' do + it 'enqueues the job' do + expect do + described_class.perform_later + end.to have_enqueued_job(described_class).on_queue('scheduled_jobs') end - it 'process the email with no date' do - fixture_path = Rails.root.join('spec/fixtures/files/mail_with_no_date.eml') - eml_content = File.read(fixture_path) - inbound_mail_with_no_date = create_inbound_email_from_fixture('mail_with_no_date.eml') - email_header = Net::IMAP::FetchData.new(1, 'BODY[HEADER]' => eml_content) - imap_fetch_mail = Net::IMAP::FetchData.new(1, 'RFC822' => eml_content) - - imap = double - allow(Net::IMAP).to receive(:new).and_return(imap) - allow(imap).to receive(:authenticate) - allow(imap).to receive(:select) - allow(imap).to receive(:search).and_return([1]) - allow(imap).to receive(:fetch).with([1], 'BODY.PEEK[HEADER]').and_return([email_header]) - allow(imap).to receive(:fetch).with(1, 'RFC822').and_return([imap_fetch_mail]) - - imap_mailbox = double - - allow(Imap::ImapMailbox).to receive(:new).and_return(imap_mailbox) - expect(imap_mailbox).to receive(:process).with(inbound_mail_with_no_date.mail, imap_email_channel).once - - described_class.perform_now(imap_email_channel) + context 'when IMAP is disabled' do + it 'does not fetch emails' do + expect(Imap::FetchEmailService).not_to receive(:new) + expect(Imap::MicrosoftFetchEmailService).not_to receive(:new) + described_class.perform_now(channel_with_imap_disabled) + end end - end - context 'when imap fetch new emails with more than 15 attachments' do - it 'process the email' do - email = Mail.new do - to 'test@outlook.com' - from 'test@gmail.com' - subject :test.to_s - body 'hello' + context 'when IMAP reauthorization is required' do + it 'does not fetch emails' do + 10.times do + imap_email_channel.authorization_error! + end + + expect(Imap::FetchEmailService).not_to receive(:new) + # Confirm the imap_enabled flag is true to avoid false positives. + expect(imap_email_channel.imap_enabled?).to be true + + described_class.perform_now(imap_email_channel) + end + end + + context 'when the channel is regular imap' do + it 'calls the imap fetch service' do + fetch_service = double + allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel).and_return(fetch_service) + allow(fetch_service).to receive(:perform).and_return([]) + + described_class.perform_now(imap_email_channel) + expect(fetch_service).to have_received(:perform) + end + end + + context 'when the channel is Microsoft' do + it 'calls the Microsoft fetch service' do + fetch_service = double + allow(Imap::MicrosoftFetchEmailService).to receive(:new).with(channel: microsoft_imap_email_channel).and_return(fetch_service) + allow(fetch_service).to receive(:perform).and_return([]) + + described_class.perform_now(microsoft_imap_email_channel) + expect(fetch_service).to have_received(:perform) + end + end + + context 'when IMAP connection errors out' do + it 'mark the connection for authorization required' do + allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel).and_raise(Errno::ECONNREFUSED) + allow(Redis::Alfred).to receive(:incr) + + expect(Redis::Alfred).to receive(:incr).with("AUTHORIZATION_ERROR_COUNT:channel_email:#{imap_email_channel.id}") + described_class.perform_now(imap_email_channel) + end + end + + context 'when the fetch service returns the email objects' do + let(:inbound_mail) { create_inbound_email_from_fixture('welcome.eml').mail } + let(:mailbox) { double } + let(:exception_tracker) { double } + let(:fetch_service) { double } + + before do + allow(Imap::ImapMailbox).to receive(:new).and_return(mailbox) + allow(ChatwootExceptionTracker).to receive(:new).and_return(exception_tracker) + + allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel).and_return(fetch_service) + allow(fetch_service).to receive(:perform).and_return([inbound_mail]) end - imap_fetch_mail = Net::IMAP::FetchData.new - imap_fetch_mail.attr = { seqno: 1, RFC822: email }.with_indifferent_access + it 'calls the mailbox to create emails' do + allow(mailbox).to receive(:process) - imap = double + expect(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel).and_return(fetch_service) + expect(fetch_service).to receive(:perform).and_return([inbound_mail]) + expect(mailbox).to receive(:process).with(inbound_mail, imap_email_channel) - allow(Net::IMAP).to receive(:new).and_return(imap) - allow(imap).to receive(:authenticate) - allow(imap).to receive(:select) - allow(imap).to receive(:search).and_return([1]) - allow(imap).to receive(:fetch).and_return([imap_fetch_mail]) - - inbound_mail_with_attachments.mail.date = DateTime.now - - allow(Mail).to receive(:read_from_string).and_return(inbound_mail_with_attachments.mail) - - imap_mailbox = Imap::ImapMailbox.new - - allow(Imap::ImapMailbox).to receive(:new).and_return(imap_mailbox) - - described_class.perform_now(imap_email_channel) - expect(Message.last.attachments.count).to eq(15) - end - end - - context 'when imap fetch new emails for microsoft mailer' do - it 'fetch and process all emails' do - email = Mail.new do - to 'test@outlook.com' - from 'test@gmail.com' - subject :test.to_s - body 'hello' - end - imap_fetch_mail = Net::IMAP::FetchData.new - imap_fetch_mail.attr = { RFC822: email }.with_indifferent_access - - ms_imap = double - - allow(Net::IMAP).to receive(:new).and_return(ms_imap) - allow(ms_imap).to receive(:authenticate) - allow(ms_imap).to receive(:select) - allow(ms_imap).to receive(:search).and_return([1]) - allow(ms_imap).to receive(:fetch).and_return([imap_fetch_mail]) - allow(Mail).to receive(:read_from_string).and_return(inbound_mail) - - ms_imap_email_inbox = double - - allow(Imap::ImapMailbox).to receive(:new).and_return(ms_imap_email_inbox) - expect(ms_imap_email_inbox).to receive(:process).with(inbound_mail, microsoft_imap_email_channel).once - - described_class.perform_now(microsoft_imap_email_channel) - end - end - - context 'when imap fetch existing emails' do - it 'does not process the email' do - email = Mail.new do - to 'test@outlook.com' - from 'test@gmail.com' - subject :test.to_s - body 'hello' - message_id '' + described_class.perform_now(imap_email_channel) end - create(:message, message_type: 'incoming', source_id: email.message_id, account: account, inbox: imap_email_channel.inbox, - conversation: conversation) + it 'logs errors if mailbox returns errors' do + allow(mailbox).to receive(:process).and_raise(StandardError) - allow(Mail).to receive(:find).and_return([email]) - imap_mailbox = double - allow(Imap::ImapMailbox).to receive(:new).and_return(imap_mailbox) - expect(imap_mailbox).not_to receive(:process).with(email, imap_email_channel) + expect(exception_tracker).to receive(:capture_exception) - described_class.perform_now(imap_email_channel) + described_class.perform_now(imap_email_channel) + end end end end diff --git a/spec/mailboxes/imap/imap_mailbox_spec.rb b/spec/mailboxes/imap/imap_mailbox_spec.rb index 33302629d..42942a548 100644 --- a/spec/mailboxes/imap/imap_mailbox_spec.rb +++ b/spec/mailboxes/imap/imap_mailbox_spec.rb @@ -3,14 +3,10 @@ require 'rails_helper' RSpec.describe Imap::ImapMailbox do include ActionMailbox::TestHelper - describe 'add mail as a new conversation in the email inbox' do + describe '#process' do let(:account) { create(:account) } let(:agent) { create(:user, email: 'agent@example.com', account: account) } - let(:channel) do - create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com', - imap_port: 993, imap_login: 'imap@gmail.com', imap_password: 'password', - account: account) - end + let(:channel) { create(:channel_email, :imap_email) } let(:inbox) { channel.inbox } let!(:contact) { create(:contact, email: 'email@gmail.com', phone_number: '+919584546666', account: account, identifier: '123') } let(:conversation) { Conversation.where(inbox_id: channel.inbox).last } @@ -20,17 +16,33 @@ RSpec.describe Imap::ImapMailbox do create(:contact_inbox, contact_id: contact.id, inbox_id: channel.inbox.id) end - context 'when a new email from non existing contact' do + context 'when the email is from a new contact' do let(:inbound_mail) { create_inbound_email_from_mail(from: 'testemail@gmail.com', to: 'imap@gmail.com', subject: 'Hello!') } it 'creates the contact and conversation with message' do - class_instance.process(inbound_mail.mail, channel) + expect do + class_instance.process(inbound_mail.mail, channel) + end.to change(Conversation, :count).by(1) + expect(conversation.contact.email).to eq(inbound_mail.mail.from.first) expect(conversation.additional_attributes['source']).to eq('email') expect(conversation.messages.empty?).to be false end end + context 'when the email has 15 or more attachments' do + let(:inbound_mail) { create_inbound_email_from_fixture('multiple_attachments.eml') } + + it 'creates a converstation and a message properly' do + expect do + class_instance.process(inbound_mail.mail, channel) + end.to change(Conversation, :count).by(1) + + expect(conversation.contact.email).to eq(inbound_mail.mail.from.first) + expect(conversation.messages.last.attachments.count).to be 15 + end + end + context 'when a new email from existing contact' do let(:inbound_mail) { create_inbound_email_from_mail(from: 'email@gmail.com', to: 'imap@gmail.com', subject: 'Hello!') } diff --git a/spec/services/imap/fetch_email_service_spec.rb b/spec/services/imap/fetch_email_service_spec.rb new file mode 100644 index 000000000..28a173951 --- /dev/null +++ b/spec/services/imap/fetch_email_service_spec.rb @@ -0,0 +1,65 @@ +require 'rails_helper' + +RSpec.describe Imap::FetchEmailService do + include ActionMailbox::TestHelper + let(:logger) { instance_double(ActiveSupport::Logger, info: true, error: true) } + let(:account) { create(:account) } + let(:imap_email_channel) { create(:channel_email, :imap_email, account: account) } + let(:imap) { instance_double(Net::IMAP) } + let(:eml_content_with_message_id) { Rails.root.join('spec/fixtures/files/only_text.eml').read } + + describe '#perform' do + before do + allow(Rails).to receive(:logger).and_return(logger) + allow(Net::IMAP).to receive(:new).with( + imap_email_channel.imap_address, port: imap_email_channel.imap_port, ssl: true + ).and_return(imap) + allow(imap).to receive(:authenticate).with( + 'PLAIN', imap_email_channel.imap_login, imap_email_channel.imap_password + ) + allow(imap).to receive(:select).with('INBOX') + end + + context 'when new emails are available in the mailbox' do + it 'fetches the emails and returns the emails that are not present in the db' do + travel_to '26.10.2020 10:00'.to_datetime do + email_object = create_inbound_email_from_fixture('only_text.eml') + email_header = Net::IMAP::FetchData.new(1, 'BODY[HEADER]' => eml_content_with_message_id) + imap_fetch_mail = Net::IMAP::FetchData.new(1, 'RFC822' => eml_content_with_message_id) + + allow(imap).to receive(:search).with(%w[SINCE 25-Oct-2020]).and_return([1]) + allow(imap).to receive(:fetch).with([1], 'BODY.PEEK[HEADER]').and_return([email_header]) + allow(imap).to receive(:fetch).with(1, 'RFC822').and_return([imap_fetch_mail]) + + result = described_class.new(channel: imap_email_channel).perform + + expect(result.length).to eq 1 + expect(result[0].message_id).to eq email_object.message_id + expect(imap).to have_received(:search).with(%w[SINCE 25-Oct-2020]) + expect(imap).to have_received(:fetch).with([1], 'BODY.PEEK[HEADER]') + expect(imap).to have_received(:fetch).with(1, 'RFC822') + expect(logger).to have_received(:info).with("[IMAP::FETCH_EMAIL_SERVICE] Fetching mails from #{imap_email_channel.email}, found 1.") + end + end + + it 'fetches the emails and returns the mail objects that are not present in the db' do + travel_to '26.10.2020 10:00'.to_datetime do + email_object = create_inbound_email_from_fixture('only_text.eml') + create(:message, source_id: email_object.message_id, account: account, inbox: imap_email_channel.inbox) + + email_header = Net::IMAP::FetchData.new(1, 'BODY[HEADER]' => eml_content_with_message_id) + + allow(imap).to receive(:search).with(%w[SINCE 25-Oct-2020]).and_return([1]) + allow(imap).to receive(:fetch).with([1], 'BODY.PEEK[HEADER]').and_return([email_header]) + + result = described_class.new(channel: imap_email_channel).perform + + expect(result.length).to eq 0 + expect(imap).to have_received(:search).with(%w[SINCE 25-Oct-2020]) + expect(imap).to have_received(:fetch).with([1], 'BODY.PEEK[HEADER]') + expect(imap).not_to have_received(:fetch).with(1, 'RFC822') + end + end + end + end +end diff --git a/spec/services/imap/microsoft_fetch_email_service_spec.rb b/spec/services/imap/microsoft_fetch_email_service_spec.rb new file mode 100644 index 000000000..8f79b86b5 --- /dev/null +++ b/spec/services/imap/microsoft_fetch_email_service_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +RSpec.describe Imap::MicrosoftFetchEmailService do + include ActionMailbox::TestHelper + let(:logger) { instance_double(ActiveSupport::Logger, info: true, error: true) } + let(:account) { create(:account) } + let(:microsoft_channel) { create(:channel_email, :microsoft_email, account: account) } + let(:imap) { instance_double(Net::IMAP) } + let(:refresh_token_service) { double } + let(:eml_content_with_message_id) { Rails.root.join('spec/fixtures/files/only_text.eml').read } + + describe '#perform' do + before do + allow(Rails).to receive(:logger).and_return(logger) + + allow(Net::IMAP).to receive(:new).with( + microsoft_channel.imap_address, port: microsoft_channel.imap_port, ssl: true + ).and_return(imap) + allow(imap).to receive(:authenticate).with( + 'XOAUTH2', microsoft_channel.imap_login, microsoft_channel.provider_config['access_token'] + ) + allow(imap).to receive(:select).with('INBOX') + + allow(Microsoft::RefreshOauthTokenService).to receive(:new).and_return(refresh_token_service) + allow(refresh_token_service).to receive(:access_token).and_return(microsoft_channel.provider_config['access_token']) + end + + context 'when new emails are available in the mailbox' do + it 'fetches the emails and returns the emails that are not present in the db' do + travel_to '26.10.2020 10:00'.to_datetime do + email_object = create_inbound_email_from_fixture('only_text.eml') + email_header = Net::IMAP::FetchData.new(1, 'BODY[HEADER]' => eml_content_with_message_id) + imap_fetch_mail = Net::IMAP::FetchData.new(1, 'RFC822' => eml_content_with_message_id) + + allow(imap).to receive(:search).with(%w[SINCE 25-Oct-2020]).and_return([1]) + allow(imap).to receive(:fetch).with([1], 'BODY.PEEK[HEADER]').and_return([email_header]) + allow(imap).to receive(:fetch).with(1, 'RFC822').and_return([imap_fetch_mail]) + + result = described_class.new(channel: microsoft_channel).perform + + expect(refresh_token_service).to have_received(:access_token) + + expect(result.length).to eq 1 + expect(result[0].message_id).to eq email_object.message_id + expect(imap).to have_received(:search).with(%w[SINCE 25-Oct-2020]) + expect(imap).to have_received(:fetch).with([1], 'BODY.PEEK[HEADER]') + expect(imap).to have_received(:fetch).with(1, 'RFC822') + expect(logger).to have_received(:info).with("[IMAP::FETCH_EMAIL_SERVICE] Fetching mails from #{microsoft_channel.email}, found 1.") + end + end + end + end +end