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) }