diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb
index d09565da3..0c80d9983 100644
--- a/app/controllers/api/v1/accounts/inboxes_controller.rb
+++ b/app/controllers/api/v1/accounts/inboxes_controller.rb
@@ -1,4 +1,5 @@
class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
+ include Api::V1::InboxesHelper
before_action :fetch_inbox, except: [:index, :create]
before_action :fetch_agent_bot, only: [:set_agent_bot]
# we are already handling the authorization in fetch inbox
@@ -41,12 +42,13 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def update
@inbox.update(permitted_params.except(:channel))
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
-
channel_attributes = get_channel_attributes(@inbox.channel_type)
# Inbox update doesn't necessarily need channel attributes
return if permitted_params(channel_attributes)[:channel].blank?
+ validate_email_channel(channel_attributes) if @inbox.inbox_type == 'Email'
+
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
update_channel_feature_flags
end
diff --git a/app/helpers/api/v1/inboxes_helper.rb b/app/helpers/api/v1/inboxes_helper.rb
new file mode 100644
index 000000000..08f000cc6
--- /dev/null
+++ b/app/helpers/api/v1/inboxes_helper.rb
@@ -0,0 +1,33 @@
+module Api::V1::InboxesHelper
+ def validate_email_channel(attributes)
+ channel_data = permitted_params(attributes)[:channel]
+
+ validate_imap(channel_data)
+ validate_smtp(channel_data)
+ end
+
+ private
+
+ def validate_imap(channel_data)
+ return unless channel_data.key?('imap_enabled') && channel_data[:imap_enabled]
+
+ Mail.defaults do
+ retriever_method :imap, { address: channel_data[:imap_address],
+ port: channel_data[:imap_port],
+ user_name: channel_data[:imap_email],
+ password: channel_data[:imap_password],
+ enable_ssl: channel_data[:imap_enable_ssl] }
+ end
+
+ Mail.connection do # rubocop:disable:block
+ end
+ end
+
+ def validate_smtp(channel_data)
+ return unless channel_data.key?('smtp_enabled') && channel_data[:smtp_enabled]
+
+ smtp = Net::SMTP.start(channel_data[:smtp_address], channel_data[:smtp_port], channel_data[:smtp_domain], channel_data[:smtp_email],
+ channel_data[:smtp_password], :login)
+ smtp.finish unless smtp&.nil?
+ end
+end
diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json
index b05a730e6..774c8160d 100644
--- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json
+++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json
@@ -401,6 +401,65 @@
"VALIDATION_ERROR": "Starting time should be before closing time.",
"CHOOSE": "Choose"
}
+ },
+ "IMAP": {
+ "TITLE": "IMAP",
+ "SUBTITLE": "Set your IMAP details",
+ "UPDATE": "Update IMAP settings",
+ "TOGGLE_AVAILABILITY": "Enable IMAP configuration for this inbox",
+ "TOGGLE_HELP": "Enabling IMAP will help the user to recieve email",
+ "EDIT": {
+ "SUCCESS_MESSAGE": "IMAP settings updated successfully",
+ "ERROR_MESSAGE": "Unable to update IMAP settings"
+ },
+ "ADDRESS": {
+ "LABEL": "Address",
+ "PLACE_HOLDER": "Address (Eg: imap.gmail.com)"
+ },
+ "PORT": {
+ "LABEL": "Port",
+ "PLACE_HOLDER": "Port"
+ },
+ "EMAIL": {
+ "LABEL": "Email",
+ "PLACE_HOLDER": "Email"
+ },
+ "PASSWORD": {
+ "LABEL": "Password",
+ "PLACE_HOLDER": "Password"
+ },
+ "ENABLE_SSL": "Enable SSL"
+ },
+ "SMTP": {
+ "TITLE": "SMTP",
+ "SUBTITLE": "Set your SMTP details",
+ "UPDATE": "Update SMTP settings",
+ "TOGGLE_AVAILABILITY": "Enable SMTP configuration for this inbox",
+ "TOGGLE_HELP": "Enabling SMTP will help the user to send email",
+ "EDIT": {
+ "SUCCESS_MESSAGE": "SMTP settings updated successfully",
+ "ERROR_MESSAGE": "Unable to update SMTP settings"
+ },
+ "ADDRESS": {
+ "LABEL": "Address",
+ "PLACE_HOLDER": "Address (Eg: smtp.gmail.com)"
+ },
+ "PORT": {
+ "LABEL": "Port",
+ "PLACE_HOLDER": "Port"
+ },
+ "EMAIL": {
+ "LABEL": "Email",
+ "PLACE_HOLDER": "Email"
+ },
+ "PASSWORD": {
+ "LABEL": "Password",
+ "PLACE_HOLDER": "Password"
+ },
+ "DOMAIN": {
+ "LABEL": "Domain",
+ "PLACE_HOLDER": "Domain"
+ }
}
}
}
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue
new file mode 100644
index 000000000..801e6fa98
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue
@@ -0,0 +1,163 @@
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
index 9d63e5781..7c8db94d5 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
@@ -353,6 +353,8 @@
+
+
@@ -378,6 +380,8 @@ import FacebookReauthorize from './facebook/Reauthorize';
import PreChatFormSettings from './PreChatForm/Settings';
import WeeklyAvailability from './components/WeeklyAvailability';
import GreetingsEditor from 'shared/components/GreetingsEditor';
+import ImapSettings from './ImapSettings';
+import SmtpSettings from './SmtpSettings';
export default {
components: {
@@ -387,6 +391,8 @@ export default {
PreChatFormSettings,
WeeklyAvailability,
GreetingsEditor,
+ ImapSettings,
+ SmtpSettings,
},
mixins: [alertMixin, configMixin, inboxMixin],
data() {
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/SmtpSettings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/SmtpSettings.vue
new file mode 100644
index 000000000..009e3cbb3
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/SmtpSettings.vue
@@ -0,0 +1,163 @@
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/store/modules/inboxes.js b/app/javascript/dashboard/store/modules/inboxes.js
index 2bb554ba3..83102ba97 100644
--- a/app/javascript/dashboard/store/modules/inboxes.js
+++ b/app/javascript/dashboard/store/modules/inboxes.js
@@ -35,6 +35,8 @@ export const state = {
isUpdating: false,
isUpdatingAutoAssignment: false,
isDeleting: false,
+ isUpdatingIMAP: false,
+ isUpdatingSMTP: false,
},
};
@@ -164,6 +166,52 @@ export const actions = {
throw new Error(error);
}
},
+ updateInboxIMAP: async (
+ { commit },
+ { id, formData = true, ...inboxParams }
+ ) => {
+ commit(types.default.SET_INBOXES_UI_FLAG, {
+ isUpdatingIMAP: true,
+ });
+ try {
+ const response = await InboxesAPI.update(
+ id,
+ formData ? buildInboxData(inboxParams) : inboxParams
+ );
+ commit(types.default.EDIT_INBOXES, response.data);
+ commit(types.default.SET_INBOXES_UI_FLAG, {
+ isUpdatingIMAP: false,
+ });
+ } catch (error) {
+ commit(types.default.SET_INBOXES_UI_FLAG, {
+ isUpdatingIMAP: false,
+ });
+ throw new Error(error);
+ }
+ },
+ updateInboxSMTP: async (
+ { commit },
+ { id, formData = true, ...inboxParams }
+ ) => {
+ commit(types.default.SET_INBOXES_UI_FLAG, {
+ isUpdatingSMTP: true,
+ });
+ try {
+ const response = await InboxesAPI.update(
+ id,
+ formData ? buildInboxData(inboxParams) : inboxParams
+ );
+ commit(types.default.EDIT_INBOXES, response.data);
+ commit(types.default.SET_INBOXES_UI_FLAG, {
+ isUpdatingSMTP: false,
+ });
+ } catch (error) {
+ commit(types.default.SET_INBOXES_UI_FLAG, {
+ isUpdatingSMTP: false,
+ });
+ throw new Error(error);
+ }
+ },
delete: async ({ commit }, inboxId) => {
commit(types.default.SET_INBOXES_UI_FLAG, { isDeleting: true });
try {
diff --git a/app/javascript/dashboard/store/modules/specs/inboxes/actions.spec.js b/app/javascript/dashboard/store/modules/specs/inboxes/actions.spec.js
index da3424267..80cf92f25 100644
--- a/app/javascript/dashboard/store/modules/specs/inboxes/actions.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/inboxes/actions.spec.js
@@ -107,6 +107,66 @@ describe('#actions', () => {
});
});
+ describe('#updateInboxIMAP', () => {
+ it('sends correct actions if API is success', async () => {
+ const updatedInbox = inboxList[0];
+
+ axios.patch.mockResolvedValue({ data: updatedInbox });
+ await actions.updateInboxIMAP(
+ { commit },
+ { id: updatedInbox.id, inbox: { channel: { imap_enabled: true } } }
+ );
+ expect(commit.mock.calls).toEqual([
+ [types.default.SET_INBOXES_UI_FLAG, { isUpdatingIMAP: true }],
+ [types.default.EDIT_INBOXES, updatedInbox],
+ [types.default.SET_INBOXES_UI_FLAG, { isUpdatingIMAP: false }],
+ ]);
+ });
+ it('sends correct actions if API is error', async () => {
+ axios.patch.mockRejectedValue({ message: 'Incorrect header' });
+ await expect(
+ actions.updateInboxIMAP(
+ { commit },
+ { id: inboxList[0].id, inbox: { channel: { imap_enabled: true } } }
+ )
+ ).rejects.toThrow(Error);
+ expect(commit.mock.calls).toEqual([
+ [types.default.SET_INBOXES_UI_FLAG, { isUpdatingIMAP: true }],
+ [types.default.SET_INBOXES_UI_FLAG, { isUpdatingIMAP: false }],
+ ]);
+ });
+ });
+
+ describe('#updateInboxSMTP', () => {
+ it('sends correct actions if API is success', async () => {
+ const updatedInbox = inboxList[0];
+
+ axios.patch.mockResolvedValue({ data: updatedInbox });
+ await actions.updateInboxSMTP(
+ { commit },
+ { id: updatedInbox.id, inbox: { channel: { smtp_enabled: true } } }
+ );
+ expect(commit.mock.calls).toEqual([
+ [types.default.SET_INBOXES_UI_FLAG, { isUpdatingSMTP: true }],
+ [types.default.EDIT_INBOXES, updatedInbox],
+ [types.default.SET_INBOXES_UI_FLAG, { isUpdatingSMTP: false }],
+ ]);
+ });
+ it('sends correct actions if API is error', async () => {
+ axios.patch.mockRejectedValue({ message: 'Incorrect header' });
+ await expect(
+ actions.updateInboxSMTP(
+ { commit },
+ { id: inboxList[0].id, inbox: { channel: { smtp_enabled: true } } }
+ )
+ ).rejects.toThrow(Error);
+ expect(commit.mock.calls).toEqual([
+ [types.default.SET_INBOXES_UI_FLAG, { isUpdatingSMTP: true }],
+ [types.default.SET_INBOXES_UI_FLAG, { isUpdatingSMTP: false }],
+ ]);
+ });
+ });
+
describe('#delete', () => {
it('sends correct actions if API is success', async () => {
axios.delete.mockResolvedValue({ data: inboxList[0] });
diff --git a/app/jobs/inboxes/fetch_imap_email_inboxes_job.rb b/app/jobs/inboxes/fetch_imap_email_inboxes_job.rb
new file mode 100644
index 000000000..ec0ca7d21
--- /dev/null
+++ b/app/jobs/inboxes/fetch_imap_email_inboxes_job.rb
@@ -0,0 +1,9 @@
+class Inboxes::FetchImapEmailInboxesJob < ApplicationJob
+ queue_as :low
+
+ def perform
+ Inbox.where(channel_type: 'Channel::Email').all.each do |inbox|
+ Inboxes::FetchImapEmailsJob.perform_later(inbox.channel) if inbox.channel.imap_enabled
+ end
+ end
+end
diff --git a/app/jobs/inboxes/fetch_imap_emails_job.rb b/app/jobs/inboxes/fetch_imap_emails_job.rb
new file mode 100644
index 000000000..fa18e6599
--- /dev/null
+++ b/app/jobs/inboxes/fetch_imap_emails_job.rb
@@ -0,0 +1,24 @@
+class Inboxes::FetchImapEmailsJob < ApplicationJob
+ queue_as :low
+
+ def perform(channel)
+ Mail.defaults do
+ retriever_method :imap, address: channel.imap_address,
+ port: channel.imap_port,
+ user_name: channel.imap_email,
+ password: channel.imap_password,
+ enable_ssl: channel.imap_enable_ssl
+ end
+
+ new_mails = false
+
+ Mail.find(what: :last, count: 10, order: :desc).each do |inbound_mail|
+ if inbound_mail.date.utc >= channel.imap_inbox_synced_at
+ Imap::ImapMailbox.new.process(inbound_mail, channel)
+ new_mails = true
+ end
+ end
+
+ Channel::Email.update(channel.id, imap_inbox_synced_at: Time.now.utc) if new_mails
+ end
+end
diff --git a/app/mailboxes/imap/imap_mailbox.rb b/app/mailboxes/imap/imap_mailbox.rb
new file mode 100644
index 000000000..a9ed0a7c9
--- /dev/null
+++ b/app/mailboxes/imap/imap_mailbox.rb
@@ -0,0 +1,79 @@
+class Imap::ImapMailbox
+ include MailboxHelper
+ attr_accessor :channel, :account, :inbox, :conversation, :processed_mail
+
+ def process(mail, channel)
+ @inbound_mail = mail
+ @channel = channel
+ load_account
+ load_inbox
+ decorate_mail
+
+ # prevent loop from chatwoot notification emails
+ return if notification_email_from_chatwoot?
+
+ ActiveRecord::Base.transaction do
+ find_or_create_contact
+ find_or_create_conversation
+ create_message
+ add_attachments_to_message
+ end
+ end
+
+ private
+
+ def load_account
+ @account = @channel.account
+ end
+
+ def load_inbox
+ @inbox = @channel.inbox
+ end
+
+ def decorate_mail
+ @processed_mail = MailPresenter.new(@inbound_mail, @account)
+ end
+
+ def find_conversation_by_in_reply_to
+ return if in_reply_to.blank?
+
+ message = @inbox.messages.find_by(source_id: in_reply_to)
+ if message.nil?
+ @inbox.conversations.where("additional_attributes->>'in_reply_to' = ?", in_reply_to).first
+ else
+ @inbox.conversations.find(message.conversation_id)
+ end
+ end
+
+ def in_reply_to
+ @inbound_mail.in_reply_to
+ end
+
+ def find_or_create_conversation
+ @conversation = find_conversation_by_in_reply_to || ::Conversation.create!({ account_id: @account.id,
+ inbox_id: @inbox.id,
+ contact_id: @contact.id,
+ contact_inbox_id: @contact_inbox.id,
+ additional_attributes: {
+ source: 'email',
+ in_reply_to: in_reply_to,
+ mail_subject: @processed_mail.subject,
+ initiated_at: {
+ timestamp: Time.now.utc
+ }
+ } })
+ end
+
+ def find_or_create_contact
+ @contact = @inbox.contacts.find_by(email: @processed_mail.original_sender)
+ if @contact.present?
+ @contact_inbox = ContactInbox.find_by(inbox: @inbox, contact: @contact)
+ else
+ create_contact
+ end
+ end
+
+ def identify_contact_name
+ processed_mail.sender_name || processed_mail.from.first.split('@').first
+ end
+end
diff --git a/app/mailboxes/mailbox_helper.rb b/app/mailboxes/mailbox_helper.rb
index 15379bb86..7cdfebe30 100644
--- a/app/mailboxes/mailbox_helper.rb
+++ b/app/mailboxes/mailbox_helper.rb
@@ -29,6 +29,21 @@ module MailboxHelper
@message.save!
end
+ def create_contact
+ @contact_inbox = ::ContactBuilder.new(
+ source_id: "email:#{processed_mail.message_id}",
+ inbox: @inbox,
+ contact_attributes: {
+ name: identify_contact_name,
+ email: processed_mail.original_sender,
+ additional_attributes: {
+ source_id: "email:#{processed_mail.message_id}"
+ }
+ }
+ ).perform
+ @contact = @contact_inbox.contact
+ end
+
def notification_email_from_chatwoot?
# notification emails are send via mailer sender email address. so it should match
@processed_mail.original_sender == Mail::Address.new(ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot
')).address
diff --git a/app/mailboxes/support_mailbox.rb b/app/mailboxes/support_mailbox.rb
index 9de2f79c2..d215310e0 100644
--- a/app/mailboxes/support_mailbox.rb
+++ b/app/mailboxes/support_mailbox.rb
@@ -78,21 +78,6 @@ class SupportMailbox < ApplicationMailbox
end
end
- def create_contact
- @contact_inbox = ::ContactBuilder.new(
- source_id: "email:#{processed_mail.message_id}",
- inbox: @inbox,
- contact_attributes: {
- name: identify_contact_name,
- email: @processed_mail.original_sender,
- additional_attributes: {
- source_id: "email:#{processed_mail.message_id}"
- }
- }
- ).perform
- @contact = @contact_inbox.contact
- end
-
def identify_contact_name
processed_mail.sender_name || processed_mail.from.first.split('@').first
end
diff --git a/app/mailers/conversation_reply_mailer.rb b/app/mailers/conversation_reply_mailer.rb
index 5d4770bd7..333eb01fe 100644
--- a/app/mailers/conversation_reply_mailer.rb
+++ b/app/mailers/conversation_reply_mailer.rb
@@ -1,4 +1,5 @@
class ConversationReplyMailer < ApplicationMailer
+ include ConversationReplyMailerHelper
default from: ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot ')
layout :choose_layout
@@ -12,16 +13,7 @@ class ConversationReplyMailer < ApplicationMailer
new_messages = @conversation.messages.chat.where('id >= ?', last_queued_id)
@messages = recap_messages + new_messages
@messages = @messages.select(&:email_reply_summarizable?)
- mail({
- to: @contact&.email,
- from: from_email_with_name,
- reply_to: reply_email,
- subject: mail_subject,
- message_id: custom_message_id,
- in_reply_to: in_reply_to_email,
- cc: cc_bcc_emails[0],
- bcc: cc_bcc_emails[1]
- })
+ prepare_mail(true)
end
def reply_without_summary(conversation, last_queued_id)
@@ -34,14 +26,7 @@ class ConversationReplyMailer < ApplicationMailer
@messages = @messages.reject { |m| m.template? && !m.input_csat? }
return false if @messages.count.zero?
- mail({
- to: @contact&.email,
- from: from_email_with_name,
- reply_to: reply_email,
- subject: mail_subject,
- message_id: custom_message_id,
- in_reply_to: in_reply_to_email
- })
+ prepare_mail(false)
end
def email_reply(message)
@@ -49,17 +34,7 @@ class ConversationReplyMailer < ApplicationMailer
init_conversation_attributes(message.conversation)
@message = message
-
- reply_mail_object = mail({
- to: @contact&.email,
- from: from_email_with_name,
- reply_to: reply_email,
- subject: mail_subject,
- message_id: custom_message_id,
- in_reply_to: in_reply_to_email,
- cc: cc_bcc_emails[0],
- bcc: cc_bcc_emails[1]
- })
+ reply_mail_object = prepare_mail(true)
message.update(source_id: reply_mail_object.message_id)
end
@@ -86,6 +61,7 @@ class ConversationReplyMailer < ApplicationMailer
@contact = @conversation.contact
@agent = @conversation.assignee
@inbox = @conversation.inbox
+ @channel = @inbox.channel
end
def should_use_conversation_email_address?
diff --git a/app/mailers/conversation_reply_mailer_helper.rb b/app/mailers/conversation_reply_mailer_helper.rb
new file mode 100644
index 000000000..a9feb0c72
--- /dev/null
+++ b/app/mailers/conversation_reply_mailer_helper.rb
@@ -0,0 +1,51 @@
+module ConversationReplyMailerHelper
+ def prepare_mail(cc_bcc_enabled)
+ @options = {
+ to: @contact&.email,
+ from: email_from,
+ reply_to: email_reply_to,
+ subject: mail_subject,
+ message_id: custom_message_id,
+ in_reply_to: in_reply_to_email
+ }
+
+ if cc_bcc_enabled
+ @options[:cc] = cc_bcc_emails[0]
+ @options[:bcc] = cc_bcc_emails[1]
+ end
+
+ set_delivery_method
+ mail(@options)
+ end
+
+ private
+
+ def set_delivery_method
+ return unless @inbox.inbox_type == 'Email' && @channel.smtp_enabled
+
+ smtp_settings = {
+ address: @channel.smtp_address,
+ port: @channel.smtp_port,
+ user_name: @channel.smtp_email,
+ password: @channel.smtp_password,
+ domain: @channel.smtp_domain,
+ enable_starttls_auto: @channel.smtp_enable_starttls_auto,
+ authentication: @channel.smtp_authentication
+ }
+
+ @options[:delivery_method] = :smtp
+ @options[:delivery_method_options] = smtp_settings
+ end
+
+ def email_smtp_enabled
+ @inbox.inbox_type == 'Email' && @channel.imap_enabled
+ end
+
+ def email_from
+ email_smtp_enabled ? @channel.smtp_email : from_email_with_name
+ end
+
+ def email_reply_to
+ email_smtp_enabled ? @channel.smtp_email : reply_email
+ end
+end
diff --git a/app/models/channel/email.rb b/app/models/channel/email.rb
index fe711b01e..092a9a101 100644
--- a/app/models/channel/email.rb
+++ b/app/models/channel/email.rb
@@ -2,12 +2,27 @@
#
# Table name: channel_email
#
-# id :bigint not null, primary key
-# email :string not null
-# forward_to_email :string not null
-# created_at :datetime not null
-# updated_at :datetime not null
-# account_id :integer not null
+# id :bigint not null, primary key
+# email :string not null
+# forward_to_email :string not null
+# imap_address :string default("")
+# imap_email :string default("")
+# imap_enable_ssl :boolean default(TRUE)
+# imap_enabled :boolean default(FALSE)
+# imap_inbox_synced_at :datetime
+# imap_password :string default("")
+# imap_port :integer default(0)
+# smtp_address :string default("")
+# smtp_authentication :string default("login")
+# smtp_domain :string default("")
+# smtp_email :string default("")
+# smtp_enable_starttls_auto :boolean default(TRUE)
+# smtp_enabled :boolean default(FALSE)
+# smtp_password :string default("")
+# smtp_port :integer default(0)
+# created_at :datetime not null
+# updated_at :datetime not null
+# account_id :integer not null
#
# Indexes
#
@@ -19,7 +34,8 @@ class Channel::Email < ApplicationRecord
include Channelable
self.table_name = 'channel_email'
- EDITABLE_ATTRS = [:email].freeze
+ EDITABLE_ATTRS = [:email, :imap_enabled, :imap_email, :imap_password, :imap_address, :imap_port, :imap_enable_ssl, :imap_inbox_synced_at,
+ :smtp_enabled, :smtp_email, :smtp_password, :smtp_address, :smtp_port, :smtp_domain].freeze
validates :email, uniqueness: true
validates :forward_to_email, uniqueness: true
diff --git a/app/views/api/v1/models/_inbox.json.jbuilder b/app/views/api/v1/models/_inbox.json.jbuilder
index e27ebcac4..e4bdbcc66 100644
--- a/app/views/api/v1/models/_inbox.json.jbuilder
+++ b/app/views/api/v1/models/_inbox.json.jbuilder
@@ -45,6 +45,22 @@ json.medium resource.channel.try(:medium) if resource.twilio?
json.forward_to_email resource.channel.try(:forward_to_email)
json.email resource.channel.try(:email) if resource.email?
+## IMAP
+json.imap_email resource.channel.try(:imap_email) if resource.email?
+json.imap_password resource.channel.try(:imap_password) if resource.email?
+json.imap_address resource.channel.try(:imap_address) if resource.email?
+json.imap_port resource.channel.try(:imap_port) if resource.email?
+json.imap_enabled resource.channel.try(:imap_enabled) if resource.email?
+json.imap_enable_ssl resource.channel.try(:imap_enable_ssl) if resource.email?
+
+## SMTP
+json.smtp_email resource.channel.try(:smtp_email) if resource.email?
+json.smtp_password resource.channel.try(:smtp_password) if resource.email?
+json.smtp_address resource.channel.try(:smtp_address) if resource.email?
+json.smtp_port resource.channel.try(:smtp_port) if resource.email?
+json.smtp_enabled resource.channel.try(:smtp_enabled) if resource.email?
+json.smtp_domain resource.channel.try(:smtp_domain) if resource.email?
+
## API Channel Attributes
json.webhook_url resource.channel.try(:webhook_url) if resource.api?
json.inbox_identifier resource.channel.try(:identifier) if resource.api?
diff --git a/config/schedule.yml b/config/schedule.yml
index 4a01893a9..971ee85fd 100644
--- a/config/schedule.yml
+++ b/config/schedule.yml
@@ -3,13 +3,18 @@
# executed At 12:00 on every day-of-month.
internal_check_new_versions_job:
- cron: "0 12 */1 * *"
- class: "Internal::CheckNewVersionsJob"
+ cron: '0 12 */1 * *'
+ class: 'Internal::CheckNewVersionsJob'
queue: scheduled_jobs
# executed At every 5th minute..
trigger_scheduled_items_job:
- cron: "*/5 * * * *"
- class: "TriggerScheduledItemsJob"
+ cron: '*/5 * * * *'
+ class: 'TriggerScheduledItemsJob'
+ queue: scheduled_jobs
+
+# executed At every minute.
+trigger_scheduled_items_job:
+ cron: '*/1 * * * *'
+ class: 'Inboxes::FetchImapEmailInboxesJob'
queue: scheduled_jobs
-
\ No newline at end of file
diff --git a/db/migrate/20211027073553_add_imap_smtp_config_to_channel_email.rb b/db/migrate/20211027073553_add_imap_smtp_config_to_channel_email.rb
new file mode 100644
index 000000000..7bd7aca50
--- /dev/null
+++ b/db/migrate/20211027073553_add_imap_smtp_config_to_channel_email.rb
@@ -0,0 +1,24 @@
+class AddImapSmtpConfigToChannelEmail < ActiveRecord::Migration[6.1]
+ def change
+ change_table :channel_email, bulk: true do |t|
+ # IMAP
+ t.boolean :imap_enabled, default: false
+ t.string :imap_address, default: ''
+ t.integer :imap_port, default: 0
+ t.string :imap_email, default: ''
+ t.string :imap_password, default: ''
+ t.boolean :imap_enable_ssl, default: true
+ t.datetime :imap_inbox_synced_at
+
+ # SMTP
+ t.boolean :smtp_enabled, default: false
+ t.string :smtp_address, default: ''
+ t.integer :smtp_port, default: 0
+ t.string :smtp_email, default: ''
+ t.string :smtp_password, default: ''
+ t.string :smtp_domain, default: ''
+ t.boolean :smtp_enable_starttls_auto, default: true
+ t.string :smtp_authentication, default: 'login'
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 1e3b87636..66550552f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -181,6 +181,21 @@ ActiveRecord::Schema.define(version: 2021_11_18_100301) do
t.string "forward_to_email", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
+ t.boolean "imap_enabled", default: false
+ t.string "imap_address", default: ""
+ t.integer "imap_port", default: 0
+ t.string "imap_email", default: ""
+ t.string "imap_password", default: ""
+ t.boolean "imap_enable_ssl", default: true
+ t.datetime "imap_inbox_synced_at"
+ t.boolean "smtp_enabled", default: false
+ t.string "smtp_address", default: ""
+ t.integer "smtp_port", default: 0
+ t.string "smtp_email", default: ""
+ t.string "smtp_password", default: ""
+ t.string "smtp_domain", default: ""
+ t.boolean "smtp_enable_starttls_auto", default: true
+ t.string "smtp_authentication", default: "login"
t.index ["email"], name: "index_channel_email_on_email", unique: true
t.index ["forward_to_email"], name: "index_channel_email_on_forward_to_email", unique: true
end
diff --git a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb
index c1b8b7416..9fdceac6f 100644
--- a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb
@@ -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)
diff --git a/spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb b/spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb
new file mode 100644
index 000000000..210602ac9
--- /dev/null
+++ b/spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb
@@ -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
diff --git a/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb b/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb
new file mode 100644
index 000000000..6c1beecaf
--- /dev/null
+++ b/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb
@@ -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
diff --git a/spec/mailboxes/imap/imap_mailbox_spec.rb b/spec/mailboxes/imap/imap_mailbox_spec.rb
new file mode 100644
index 000000000..0f44e2a40
--- /dev/null
+++ b/spec/mailboxes/imap/imap_mailbox_spec.rb
@@ -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
diff --git a/spec/mailers/conversation_reply_mailer_spec.rb b/spec/mailers/conversation_reply_mailer_spec.rb
index eb9799ddf..c1d6d18dd 100644
--- a/spec/mailers/conversation_reply_mailer_spec.rb
+++ b/spec/mailers/conversation_reply_mailer_spec.rb
@@ -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) }