Skip to content

Commit

Permalink
feat: email channel backend (#140) (#1255)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sony-mathew authored Sep 21, 2020
1 parent 313b2da commit f9b0427
Show file tree
Hide file tree
Showing 12 changed files with 892 additions and 45 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
*.log
# Ignore application configuration
node_modules

master.key
*.rdb

# Ignore env files
Expand Down
23 changes: 19 additions & 4 deletions app/mailboxes/application_mailbox.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ class ApplicationMailbox < ActionMailbox::Base
# Eg: email should be something like : [email protected]
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|
Expand All @@ -18,11 +18,26 @@ def self.reply_match_proc
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
29 changes: 29 additions & 0 deletions app/mailboxes/mailbox_helper.rb
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,32 +19,6 @@ def process

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]
Expand Down
84 changes: 84 additions & 0 deletions app/mailboxes/support_mailbox.rb
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions app/models/concerns/featurable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,22 @@ def enable_features(*names)
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
Expand Down
8 changes: 6 additions & 2 deletions app/presenters/mail_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def subject
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],
Expand All @@ -21,14 +21,18 @@ def text_content
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],
quoted: extract_reply(@decoded_html_content)[:quoted_text]
}
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|
Expand Down
12 changes: 12 additions & 0 deletions spec/factories/channel/channel_email.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

FactoryBot.define do
factory :channel_email, class: 'Channel::Email' do
sequence(:email) { |n| "care-#{n}@example.com" }
sequence(:forward_to_address) { |n| "forward-#{n}@chatwoot.com" }
account
after(:create) do |channel_email|
create(:inbox, channel: channel_email, account: channel_email.account)
end
end
end
Loading

0 comments on commit f9b0427

Please sign in to comment.