feat: email channel backend (#140) (#1255)

* feat: added support mailbox to handle email channel (#140)

Added a new mailbox called 'SupportMailbox' to handle all the
incoming emails other than reply emails.

An email channel will have a support email and forward email
associated with it. So we filter for the right email inbox based on
the support email of that inbox and route this to this mailbox.

This mailbox finds the account, inbox, contact (create a new one
if it does not exist) and creates a conversation and adds the
email content as the first message in the conversation.

Other minor things handled in this commit:

* renamed the procs for routing emails in application mailbox
* renamed ConversationMailbox to ReplyMailbox
* Added a fallback content in MailPresenter
* Added a record saving (bang) versions of enabling and disabling
features in Featurable module
* added new factory for the email channel

refs: #140
This commit is contained in:
Sony Mathew
2020-09-21 22:44:22 +05:30
committed by GitHub
parent 313b2da703
commit f9b0427751
12 changed files with 892 additions and 45 deletions

View File

@@ -3,7 +3,7 @@ class ApplicationMailbox < ActionMailbox::Base
# Eg: email should be something like : reply+6bdc3f4d-0bec-4515-a284-5d916fdde489@domain.com
REPLY_EMAIL_USERNAME_PATTERN = /^reply\+([0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12})$/i.freeze
def self.reply_match_proc
def self.reply_mail?
proc do |inbound_mail_obj|
is_a_reply_email = false
inbound_mail_obj.mail.to.each do |email|
@@ -18,11 +18,26 @@ class ApplicationMailbox < ActionMailbox::Base
end
end
def self.default_mail_proc
def self.support_mail?
proc do |inbound_mail_obj|
is_a_support_email = false
inbound_mail_obj.mail.to.each do |email|
channel = Channel::Email.find_by(email: email)
if channel.present?
is_a_support_email = true
break
end
end
is_a_support_email
end
end
def self.catch_all_mail?
proc { |_mail| true }
end
# routing should be defined below the referenced procs
routing(reply_match_proc => :conversation)
routing(default_mail_proc => :default)
routing(reply_mail? => :reply)
routing(support_mail? => :support)
routing(catch_all_mail? => :default)
end

View File

@@ -0,0 +1,29 @@
module MailboxHelper
private
def create_message
@message = @conversation.messages.create(
account_id: @conversation.account_id,
sender: @conversation.contact,
content: processed_mail.text_content[:reply],
inbox_id: @conversation.inbox_id,
message_type: 'incoming',
content_type: 'incoming_email',
source_id: processed_mail.message_id,
content_attributes: {
email: processed_mail.serialized_data
}
)
end
def add_attachments_to_message
processed_mail.attachments.each do |mail_attachment|
attachment = @message.attachments.new(
account_id: @conversation.account_id,
file_type: 'file'
)
attachment.file.attach(mail_attachment[:blob])
end
@message.save!
end
end

View File

@@ -1,4 +1,6 @@
class ConversationMailbox < ApplicationMailbox
class ReplyMailbox < ApplicationMailbox
include MailboxHelper
attr_accessor :conversation_uuid, :processed_mail
# Last part is the regex for the UUID
@@ -17,32 +19,6 @@ class ConversationMailbox < ApplicationMailbox
private
def create_message
@message = @conversation.messages.create(
account_id: @conversation.account_id,
sender: @conversation.contact,
content: processed_mail.text_content[:reply],
inbox_id: @conversation.inbox_id,
message_type: 'incoming',
content_type: 'incoming_email',
source_id: processed_mail.message_id,
content_attributes: {
email: processed_mail.serialized_data
}
)
end
def add_attachments_to_message
processed_mail.attachments.each do |mail_attachment|
attachment = @message.attachments.new(
account_id: @conversation.account_id,
file_type: 'file'
)
attachment.file.attach(mail_attachment[:blob])
end
@message.save!
end
def conversation_uuid_from_to_address
mail.to.each do |email|
username = email.split('@')[0]

View File

@@ -0,0 +1,84 @@
class SupportMailbox < ApplicationMailbox
include MailboxHelper
attr_accessor :channel, :account, :inbox, :conversation, :processed_mail
before_processing :find_channel,
:load_account,
:load_inbox,
:decorate_mail
def process
find_or_create_contact
create_conversation
create_message
add_attachments_to_message
end
private
def find_channel
mail.to.each do |email|
@channel = Channel::Email.find_by(email: email)
break if @channel.present?
end
raise 'Email channel/inbox not found' if @channel.nil?
@channel
end
def load_account
@account = @channel.account
end
def load_inbox
@inbox = @channel.inbox
end
def decorate_mail
@processed_mail = MailPresenter.new(mail, @account)
end
def create_conversation
@conversation = ::Conversation.create!({
account_id: @account.id,
inbox_id: @inbox.id,
contact_id: @contact.id,
contact_inbox_id: @contact_inbox.id,
additional_attributes: {
source: 'email',
initiated_at: {
timestamp: Time.now.utc
}
}
})
end
def find_or_create_contact
@contact = @inbox.contacts.find_by(email: processed_mail.from.first)
if @contact.present?
@contact_inbox = ContactInbox.find_by(inbox: @inbox, contact: @contact)
else
create_contact
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.from.first,
additional_attributes: {
source_id: "email:#{processed_mail.message_id}"
}
}
).perform
@contact = @contact_inbox.contact
end
def identify_contact_name
processed_mail.from.first.split('@').first
end
end

View File

@@ -24,12 +24,22 @@ module Featurable
end
end
def enable_features!(*names)
enable_features(*names)
save
end
def disable_features(*names)
names.each do |name|
send("feature_#{name}=", false)
end
end
def disable_features!(*names)
disable_features(*names)
save
end
def feature_enabled?(name)
send("feature_#{name}?")
end

View File

@@ -12,7 +12,7 @@ class MailPresenter < SimpleDelegator
end
def text_content
@decoded_text_content ||= encode_to_unicode(text_part&.body&.decoded || '')
@decoded_text_content ||= encode_to_unicode(text_part&.body&.decoded || fallback_content)
@text_content ||= {
full: @decoded_text_content,
reply: extract_reply(@decoded_text_content)[:reply],
@@ -21,7 +21,7 @@ class MailPresenter < SimpleDelegator
end
def html_content
@decoded_html_content ||= encode_to_unicode(html_part&.body&.decoded || '')
@decoded_html_content ||= encode_to_unicode(html_part&.body&.decoded || fallback_content)
@html_content ||= {
full: @decoded_html_content,
reply: extract_reply(@decoded_html_content)[:reply],
@@ -29,6 +29,10 @@ class MailPresenter < SimpleDelegator
}
end
def fallback_content
body&.decoded || ''
end
def attachments
# ref : https://github.com/gorails-screencasts/action-mailbox-action-text/blob/master/app/mailboxes/posts_mailbox.rb
mail.attachments.map do |attachment|