mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-04 21:18:02 +00:00
fix(refactor): Cleanup the specs and the logic for FetchIMAP job (#8766)
This commit is contained in:
@@ -7,6 +7,7 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob
|
|||||||
return unless should_fetch_email?(channel)
|
return unless should_fetch_email?(channel)
|
||||||
|
|
||||||
key = format(::Redis::Alfred::EMAIL_MESSAGE_MUTEX, inbox_id: channel.inbox.id)
|
key = format(::Redis::Alfred::EMAIL_MESSAGE_MUTEX, inbox_id: channel.inbox.id)
|
||||||
|
|
||||||
with_lock(key, 5.minutes) do
|
with_lock(key, 5.minutes) do
|
||||||
process_email_for_channel(channel)
|
process_email_for_channel(channel)
|
||||||
end
|
end
|
||||||
@@ -28,126 +29,14 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob
|
|||||||
end
|
end
|
||||||
|
|
||||||
def process_email_for_channel(channel)
|
def process_email_for_channel(channel)
|
||||||
if channel.microsoft?
|
inbound_emails = if channel.microsoft?
|
||||||
fetch_mail_for_ms_provider(channel)
|
Imap::MicrosoftFetchEmailService.new(channel: channel).perform
|
||||||
else
|
else
|
||||||
fetch_mail_for_channel(channel)
|
Imap::FetchEmailService.new(channel: channel).perform
|
||||||
end
|
end
|
||||||
# clearing old failures like timeouts since the mail is now successfully processed
|
inbound_emails.map do |inbound_mail|
|
||||||
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)
|
process_mail(inbound_mail, channel)
|
||||||
end
|
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 <message set>
|
|
||||||
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?
|
|
||||||
|
|
||||||
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
|
end
|
||||||
|
|
||||||
def process_mail(inbound_mail, channel)
|
def process_mail(inbound_mail, channel)
|
||||||
@@ -157,13 +46,4 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob
|
|||||||
Rails.logger.error("
|
Rails.logger.error("
|
||||||
#{channel.provider} Email dropped: #{inbound_mail.from} and message_source_id: #{inbound_mail.message_id}")
|
#{channel.provider} Email dropped: #{inbound_mail.from} and message_source_id: #{inbound_mail.message_id}")
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
115
app/services/imap/base_fetch_email_service.rb
Normal file
115
app/services/imap/base_fetch_email_service.rb
Normal file
@@ -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 <message set>
|
||||||
|
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
|
||||||
15
app/services/imap/fetch_email_service.rb
Normal file
15
app/services/imap/fetch_email_service.rb
Normal file
@@ -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
|
||||||
17
app/services/imap/microsoft_fetch_email_service.rb
Normal file
17
app/services/imap/microsoft_fetch_email_service.rb
Normal file
@@ -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
|
||||||
@@ -25,5 +25,14 @@ FactoryBot.define do
|
|||||||
end
|
end
|
||||||
provider { 'microsoft' }
|
provider { 'microsoft' }
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,161 +1,106 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Inboxes::FetchImapEmailsJob do
|
RSpec.describe Inboxes::FetchImapEmailsJob do
|
||||||
|
include ActiveJob::TestHelper
|
||||||
include ActionMailbox::TestHelper
|
include ActionMailbox::TestHelper
|
||||||
|
|
||||||
let(:account) { create(:account) }
|
let(:account) { create(:account) }
|
||||||
let(:imap_email_channel) do
|
let(:imap_email_channel) { create(:channel_email, :imap_email, account: account) }
|
||||||
create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com', imap_port: 993, imap_login: 'imap@gmail.com',
|
let(:channel_with_imap_disabled) { create(:channel_email, :imap_email, imap_enabled: false, account: account) }
|
||||||
imap_password: 'password', account: account)
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:microsoft_imap_email_channel) { create(:channel_email, :microsoft_email) }
|
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') }
|
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
it 'enqueues the job' do
|
it 'enqueues the job' do
|
||||||
expect { described_class.perform_later }.to have_enqueued_job(described_class)
|
expect do
|
||||||
.on_queue('scheduled_jobs')
|
described_class.perform_later
|
||||||
|
end.to have_enqueued_job(described_class).on_queue('scheduled_jobs')
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when imap fetch new emails' do
|
context 'when IMAP is disabled' do
|
||||||
it 'process the email' do
|
it 'does not fetch emails' do
|
||||||
email = Mail.new do
|
expect(Imap::FetchEmailService).not_to receive(:new)
|
||||||
to 'test@outlook.com'
|
expect(Imap::MicrosoftFetchEmailService).not_to receive(:new)
|
||||||
from 'test@gmail.com'
|
described_class.perform_now(channel_with_imap_disabled)
|
||||||
subject :test.to_s
|
end
|
||||||
body 'hello'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
imap_fetch_mail = Net::IMAP::FetchData.new
|
context 'when IMAP reauthorization is required' do
|
||||||
imap_fetch_mail.attr = { seqno: 1, RFC822: email }.with_indifferent_access
|
it 'does not fetch emails' do
|
||||||
|
10.times do
|
||||||
imap = double
|
imap_email_channel.authorization_error!
|
||||||
|
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'process the email with no date' do
|
expect(Imap::FetchEmailService).not_to receive(:new)
|
||||||
fixture_path = Rails.root.join('spec/fixtures/files/mail_with_no_date.eml')
|
# Confirm the imap_enabled flag is true to avoid false positives.
|
||||||
eml_content = File.read(fixture_path)
|
expect(imap_email_channel.imap_enabled?).to be true
|
||||||
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)
|
described_class.perform_now(imap_email_channel)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when imap fetch new emails with more than 15 attachments' do
|
context 'when the channel is regular imap' do
|
||||||
it 'process the email' do
|
it 'calls the imap fetch service' do
|
||||||
email = Mail.new do
|
fetch_service = double
|
||||||
to 'test@outlook.com'
|
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel).and_return(fetch_service)
|
||||||
from 'test@gmail.com'
|
allow(fetch_service).to receive(:perform).and_return([])
|
||||||
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])
|
|
||||||
|
|
||||||
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)
|
described_class.perform_now(imap_email_channel)
|
||||||
expect(Message.last.attachments.count).to eq(15)
|
expect(fetch_service).to have_received(:perform)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when imap fetch new emails for microsoft mailer' do
|
context 'when the channel is Microsoft' do
|
||||||
it 'fetch and process all emails' do
|
it 'calls the Microsoft fetch service' do
|
||||||
email = Mail.new do
|
fetch_service = double
|
||||||
to 'test@outlook.com'
|
allow(Imap::MicrosoftFetchEmailService).to receive(:new).with(channel: microsoft_imap_email_channel).and_return(fetch_service)
|
||||||
from 'test@gmail.com'
|
allow(fetch_service).to receive(:perform).and_return([])
|
||||||
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)
|
described_class.perform_now(microsoft_imap_email_channel)
|
||||||
|
expect(fetch_service).to have_received(:perform)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when imap fetch existing emails' do
|
context 'when IMAP connection errors out' do
|
||||||
it 'does not process the email' do
|
it 'mark the connection for authorization required' do
|
||||||
email = Mail.new do
|
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel).and_raise(Errno::ECONNREFUSED)
|
||||||
to 'test@outlook.com'
|
allow(Redis::Alfred).to receive(:incr)
|
||||||
from 'test@gmail.com'
|
|
||||||
subject :test.to_s
|
expect(Redis::Alfred).to receive(:incr).with("AUTHORIZATION_ERROR_COUNT:channel_email:#{imap_email_channel.id}")
|
||||||
body 'hello'
|
described_class.perform_now(imap_email_channel)
|
||||||
message_id '<messageId@example.com>'
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
create(:message, message_type: 'incoming', source_id: email.message_id, account: account, inbox: imap_email_channel.inbox,
|
context 'when the fetch service returns the email objects' do
|
||||||
conversation: conversation)
|
let(:inbound_mail) { create_inbound_email_from_fixture('welcome.eml').mail }
|
||||||
|
let(:mailbox) { double }
|
||||||
|
let(:exception_tracker) { double }
|
||||||
|
let(:fetch_service) { double }
|
||||||
|
|
||||||
allow(Mail).to receive(:find).and_return([email])
|
before do
|
||||||
imap_mailbox = double
|
allow(Imap::ImapMailbox).to receive(:new).and_return(mailbox)
|
||||||
allow(Imap::ImapMailbox).to receive(:new).and_return(imap_mailbox)
|
allow(ChatwootExceptionTracker).to receive(:new).and_return(exception_tracker)
|
||||||
expect(imap_mailbox).not_to receive(:process).with(email, imap_email_channel)
|
|
||||||
|
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
|
||||||
|
|
||||||
|
it 'calls the mailbox to create emails' do
|
||||||
|
allow(mailbox).to receive(:process)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
described_class.perform_now(imap_email_channel)
|
described_class.perform_now(imap_email_channel)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'logs errors if mailbox returns errors' do
|
||||||
|
allow(mailbox).to receive(:process).and_raise(StandardError)
|
||||||
|
|
||||||
|
expect(exception_tracker).to receive(:capture_exception)
|
||||||
|
|
||||||
|
described_class.perform_now(imap_email_channel)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,14 +3,10 @@ require 'rails_helper'
|
|||||||
RSpec.describe Imap::ImapMailbox do
|
RSpec.describe Imap::ImapMailbox do
|
||||||
include ActionMailbox::TestHelper
|
include ActionMailbox::TestHelper
|
||||||
|
|
||||||
describe 'add mail as a new conversation in the email inbox' do
|
describe '#process' do
|
||||||
let(:account) { create(:account) }
|
let(:account) { create(:account) }
|
||||||
let(:agent) { create(:user, email: 'agent@example.com', account: account) }
|
let(:agent) { create(:user, email: 'agent@example.com', account: account) }
|
||||||
let(:channel) do
|
let(:channel) { create(:channel_email, :imap_email) }
|
||||||
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(:inbox) { channel.inbox }
|
let(:inbox) { channel.inbox }
|
||||||
let!(:contact) { create(:contact, email: 'email@gmail.com', phone_number: '+919584546666', account: account, identifier: '123') }
|
let!(:contact) { create(:contact, email: 'email@gmail.com', phone_number: '+919584546666', account: account, identifier: '123') }
|
||||||
let(:conversation) { Conversation.where(inbox_id: channel.inbox).last }
|
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)
|
create(:contact_inbox, contact_id: contact.id, inbox_id: channel.inbox.id)
|
||||||
end
|
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!') }
|
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
|
it 'creates the contact and conversation with message' do
|
||||||
|
expect do
|
||||||
class_instance.process(inbound_mail.mail, channel)
|
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.contact.email).to eq(inbound_mail.mail.from.first)
|
||||||
expect(conversation.additional_attributes['source']).to eq('email')
|
expect(conversation.additional_attributes['source']).to eq('email')
|
||||||
expect(conversation.messages.empty?).to be false
|
expect(conversation.messages.empty?).to be false
|
||||||
end
|
end
|
||||||
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
|
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!') }
|
let(:inbound_mail) { create_inbound_email_from_mail(from: 'email@gmail.com', to: 'imap@gmail.com', subject: 'Hello!') }
|
||||||
|
|
||||||
|
|||||||
65
spec/services/imap/fetch_email_service_spec.rb
Normal file
65
spec/services/imap/fetch_email_service_spec.rb
Normal file
@@ -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
|
||||||
53
spec/services/imap/microsoft_fetch_email_service_spec.rb
Normal file
53
spec/services/imap/microsoft_fetch_email_service_spec.rb
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user