feat: IMAP Email Channel (#3298)

This change allows the user to configure both IMAP and SMTP for an email inbox. IMAP enables the user to see emails in Chatwoot. And user can use SMTP to reply to an email conversation.

Users can use the default settings to send and receive emails for email inboxes if both IMAP and SMTP are disabled.

Fixes #2520
This commit is contained in:
Aswin Dev P.S
2021-11-18 22:22:27 -08:00
committed by GitHub
parent 8384d0b38e
commit 24e6a92297
25 changed files with 1040 additions and 57 deletions

View File

@@ -376,6 +376,57 @@ RSpec.describe 'Inboxes API', type: :request do
expect(email_channel.reload.email).to eq('emailtest@email.test')
end
it 'updates email inbox with imap when administrator' do
email_channel = create(:channel_email, account: account)
email_inbox = create(:inbox, channel: email_channel, account: account)
imap_connection = double
allow(Mail).to receive(:connection).and_return(imap_connection)
patch "/api/v1/accounts/#{account.id}/inboxes/#{email_inbox.id}",
headers: admin.create_new_auth_token,
params: {
channel: {
imap_enabled: true,
imap_address: 'imap.gmail.com',
imap_port: 993,
imap_email: 'imaptest@gmail.com'
}
},
as: :json
expect(response).to have_http_status(:success)
expect(email_channel.reload.imap_enabled).to be true
expect(email_channel.reload.imap_address).to eq('imap.gmail.com')
expect(email_channel.reload.imap_port).to eq(993)
end
it 'updates email inbox with smtp when administrator' do
email_channel = create(:channel_email, account: account)
email_inbox = create(:inbox, channel: email_channel, account: account)
smtp_connection = double
allow(smtp_connection).to receive(:finish).and_return(true)
allow(Net::SMTP).to receive(:start).and_return(smtp_connection)
patch "/api/v1/accounts/#{account.id}/inboxes/#{email_inbox.id}",
headers: admin.create_new_auth_token,
params: {
channel: {
smtp_enabled: true,
smtp_address: 'smtp.gmail.com',
smtp_port: 587,
smtp_email: 'smtptest@gmail.com'
}
},
as: :json
expect(response).to have_http_status(:success)
expect(email_channel.reload.smtp_enabled).to be true
expect(email_channel.reload.smtp_address).to eq('smtp.gmail.com')
expect(email_channel.reload.smtp_port).to eq(587)
end
it 'updates avatar when administrator' do
# no avatar before upload
expect(inbox.avatar.attached?).to eq(false)

View File

@@ -0,0 +1,27 @@
require 'rails_helper'
RSpec.describe Inboxes::FetchImapEmailInboxesJob, type: :job do
let(:account) { create(:account) }
let(:imap_email_channel) do
create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com', imap_port: 993, imap_email: 'imap@gmail.com',
imap_password: 'password', account: account)
end
let(:email_inbox) { create(:inbox, channel: imap_email_channel, account: account) }
it 'enqueues the job' do
expect { described_class.perform_later }.to have_enqueued_job(described_class)
.on_queue('low')
end
context 'when called' do
it 'fetch all the email channels' do
imap_email_inboxes = double
allow(imap_email_inboxes).to receive(:all).and_return([email_inbox])
allow(Inbox).to receive(:where).and_return(imap_email_inboxes)
expect(Inboxes::FetchImapEmailsJob).to receive(:perform_later).with(imap_email_channel).once
described_class.perform_now
end
end
end

View File

@@ -0,0 +1,37 @@
require 'rails_helper'
RSpec.describe Inboxes::FetchImapEmailsJob, type: :job do
let(:account) { create(:account) }
let(:imap_email_channel) do
create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com', imap_port: 993, imap_email: 'imap@gmail.com',
imap_password: 'password', imap_inbox_synced_at: Time.now.utc - 10, account: account)
end
let(:email_inbox) { create(:inbox, channel: imap_email_channel, account: account) }
it 'enqueues the job' do
expect { described_class.perform_later }.to have_enqueued_job(described_class)
.on_queue('low')
end
context 'when imap fetch latest 10 emails' do
it 'check for the new emails' do
mail_date = Time.now.utc
mail = Mail.new do
to 'test@outlook.com'
from 'test@gmail.com'
subject :test.to_s
body 'hello'
date mail_date
end
allow(Mail).to receive(:find).and_return([mail])
imap_mailbox = double
allow(Imap::ImapMailbox).to receive(:new).and_return(imap_mailbox)
expect(imap_mailbox).to receive(:process).with(mail, imap_email_channel).once
described_class.perform_now(imap_email_channel)
expect(imap_email_channel.reload.imap_inbox_synced_at).to be > mail_date
end
end
end

View File

@@ -0,0 +1,93 @@
require 'rails_helper'
RSpec.describe Imap::ImapMailbox, type: :mailbox do
include ActionMailbox::TestHelper
describe 'add mail as a new conversation in the email inbox' 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_email: 'imap@gmail.com', imap_password: 'password',
account: account)
end
let(:inbox) { create(:inbox, channel: channel, account: account) }
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(:class_instance) { described_class.new }
before do
create(:contact_inbox, contact_id: contact.id, inbox_id: channel.inbox.id)
end
context 'when a new email from non existing 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(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 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!') }
it 'creates a new conversation with message' do
class_instance.process(inbound_mail.mail, channel)
expect(conversation.contact.email).to eq(contact.email)
expect(conversation.additional_attributes['source']).to eq('email')
expect(conversation.messages.empty?).to be false
end
end
context 'when a reply for existing email conversation' do
let(:prev_conversation) { create(:conversation, account: account, inbox: channel.inbox, assignee: agent) }
let(:reply_mail) do
create_inbound_email_from_mail(from: 'email@gmail.com', to: 'imap@gmail.com', subject: 'Hello!', in_reply_to: 'test-in-reply-to')
end
it 'appends new email to the existing conversation' do
create(
:message,
content: 'Incoming Message',
message_type: 'incoming',
inbox: inbox,
account: account,
conversation: prev_conversation
)
create(
:message,
content: 'Outgoing Message',
message_type: 'outgoing',
inbox: inbox,
source_id: 'test-in-reply-to',
account: account,
conversation: prev_conversation
)
expect(prev_conversation.messages.size).to eq(2)
class_instance.process(reply_mail.mail, channel)
expect(prev_conversation.messages.size).to eq(3)
expect(prev_conversation.messages.last.content_attributes['email']['from']).to eq(reply_mail.mail.from)
expect(prev_conversation.messages.last.content_attributes['email']['to']).to eq(reply_mail.mail.to)
expect(prev_conversation.messages.last.content_attributes['email']['subject']).to eq(reply_mail.mail.subject)
expect(prev_conversation.messages.last.content_attributes['email']['in_reply_to']).to eq(reply_mail.mail.in_reply_to)
end
end
context 'when a reply for non existing email conversation' do
let(:reply_mail) do
create_inbound_email_from_mail(from: 'email@gmail.com', to: 'imap@gmail.com', subject: 'Hello!', in_reply_to: 'test-in-reply-to')
end
it 'creates new email conversation with incoming in-reply-to' do
class_instance.process(reply_mail.mail, channel)
expect(conversation.additional_attributes['in_reply_to']).to eq(reply_mail.mail.in_reply_to)
end
end
end
end

View File

@@ -154,6 +154,32 @@ RSpec.describe ConversationReplyMailer, type: :mailer do
end
end
context 'when smtp enabled for email channel' do
let(:smtp_email_channel) do
create(:channel_email, smtp_enabled: true, smtp_address: 'smtp.gmail.com', smtp_port: 587, smtp_email: 'smtp@gmail.com',
smtp_password: 'password', smtp_domain: 'smtp.gmail.com', account: account)
end
let(:conversation) { create(:conversation, assignee: agent, inbox: smtp_email_channel.inbox, account: account).reload }
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
it 'use smtp mail server' do
mail = described_class.email_reply(message)
expect(mail.delivery_method.settings.empty?).to be false
expect(mail.delivery_method.settings[:address]).to eq 'smtp.gmail.com'
expect(mail.delivery_method.settings[:port]).to eq 587
end
end
context 'when smtp disabled for email channel', :test do
let(:conversation) { create(:conversation, assignee: agent, inbox: email_channel.inbox, account: account).reload }
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
it 'use default mail server' do
mail = described_class.email_reply(message)
expect(mail.delivery_method.settings).to be_empty
end
end
context 'when custom domain and email is not enabled' do
let(:inbox) { create(:inbox, account: account) }
let(:inbox_member) { create(:inbox_member, user: agent, inbox: inbox) }