chore: Code clean up moves to service classes

This commit is contained in:
iamsivin
2025-05-02 14:14:15 +05:30
parent 7aefbba2ca
commit 99d7bd9e7c
12 changed files with 1032 additions and 480 deletions
@@ -1,314 +0,0 @@
module Messages::ForwardedMessageFormatter
def self.parse_from_field(from_field)
return '' if from_field.blank?
from_field =~ /(.*)<(.*)>/ ? Regexp.last_match(2).strip : from_field
end
def self.format_plain_text_to_html(text)
ERB::Util.html_escape(text.to_s).gsub("\n", '<br>')
end
def self.extract_email(from_field)
return '' if from_field.blank?
from_field =~ /<(.*)>/ ? Regexp.last_match(1) : from_field
end
def self.format_date_string(date_str)
return '' if date_str.blank?
begin
parsed_date = DateTime.now
parsed_date.strftime('%a, %b %-d, %Y at %-l:%M %p')
rescue StandardError
date_str
end
end
def self.strip_markdown(text)
return '' if text.blank?
# Convert markdown to HTML using CommonMarker
html = CommonMarker.render_html(text, :DEFAULT)
# Strip HTML tags to get plain text
ActionView::Base.full_sanitizer.sanitize(html)
end
def self.convert_markdown_to_html(text)
return '' if text.blank?
# Use CommonMarker with GitHub Flavored Markdown options
options = [:GITHUB_PRE_LANG, :UNSAFE]
extensions = [:table, :strikethrough, :autolink]
CommonMarker.render_html(text, options, extensions)
end
# Convert text to plain text, stripping any markdown formatting
def self.markdown_to_plain_text(text)
return '' if text.blank?
# Simply strip any markdown by converting to HTML and sanitizing
strip_markdown(text)
end
end
# Only modify the formatted_html_content method in ForwardedMessageContentBuilder
module Messages::ForwardedMessageContentBuilder
def forwarded_header_text
return '' unless formatted_info.values.any?
build_header_lines.compact.join("\n")
end
def build_header_lines
[
"\n\n---------- Forwarded message ---------",
("From: #{formatted_info[:from]}" if formatted_info[:from]),
("Date: #{formatted_info[:date]}" if formatted_info[:date]),
("Subject: #{formatted_info[:subject]}" if formatted_info[:subject]),
("To: <#{formatted_info[:to]}>" if formatted_info[:to]),
"\n"
]
end
def forwarded_body_text
return forwarded_message.content.to_s if email_data.blank?
text = email_data.dig('text_content', 'full')
html = email_data.dig('html_content', 'full')
if text.present?
text
elsif html.present?
ActionView::Base.full_sanitizer.sanitize(html)
else
forwarded_message.content.to_s
end
end
def formatted_content(original_content = '')
return original_content.to_s if forwarded_message.blank?
original_content.to_s + forwarded_header_text + forwarded_body_text
end
# Update this method to always use markdown conversion
def formatted_html_content(original_content = '')
return original_content if forwarded_message.blank?
# Always use markdown conversion since it handles plain text correctly
converted_content = Messages::ForwardedMessageFormatter.convert_markdown_to_html(original_content)
html_wrapper(converted_content)
end
end
module Messages::ForwardedMessageHtmlBuilder
def html_wrapper(content)
base = '<div dir="ltr">'
if content.blank?
"#{base}#{forwarded_header_html}#{forwarded_body_html}</div>"
else
"#{base}#{content}<br><br>#{forwarded_header_html}#{forwarded_body_html}</div>"
end
end
def forwarded_header_html
from_value = formatted_info[:from].to_s
email = Messages::ForwardedMessageFormatter.extract_email(from_value)
[
'<div class="gmail_quote gmail_quote_container">',
'<div dir="ltr" class="gmail_attr">---------- Forwarded message ---------<br>',
build_email_html(email),
build_field_html('Date', formatted_info[:date]),
build_field_html('Subject', formatted_info[:subject]),
build_to_html(formatted_info[:to]),
'</div><br><br>'
].compact.join
end
def build_email_html(email)
return nil if email.blank?
"From: &lt;<a href=\"mailto:#{email}\">#{email}</a>&gt;<br>"
end
def build_field_html(label, value)
value.present? ? "#{label}: #{value}<br>" : nil
end
def build_to_html(to_email)
return nil if to_email.blank?
"To: &lt;<a href=\"mailto:#{to_email}\">#{to_email}</a>&gt;<br>"
end
# Update this method to always use markdown conversion
def forwarded_body_html
# Return HTML content directly if available
return email_data.dig('html_content', 'full') if email_data&.dig('html_content', 'full').present?
# Otherwise format content to HTML
content = if email_data&.dig('text_content', 'full').present?
email_data.dig('text_content', 'full')
else
forwarded_message.content.to_s
end
# Always use markdown conversion since it handles plain text correctly
Messages::ForwardedMessageFormatter.convert_markdown_to_html(content)
end
end
# The rest of the modules and class remain unchanged
module Messages::ForwardedMessageDataHandler
def prepare_email_data
initialize_email_data
end
def initialize_email_data
data = base_email_data
add_content_fields(data)
add_header_fields(data)
data
end
def base_email_data
email_data.present? ? email_data.dup || {} : {}
end
def add_content_fields(data)
data['html_content'] ||= {}
data['text_content'] ||= {}
end
def add_header_fields(data)
data['from'] = [email_from_inbox] # Always overwrite with inbox email
data['to'] = Array(@params[:to_emails]) if @params && @params[:to_emails].present?
data['subject'] ||= subject
data['date'] ||= Time.zone.now.to_s
end
def formatted_info
{
from: inbox_email.to_s,
date: Messages::ForwardedMessageFormatter.format_date_string(email_date),
subject: subject.to_s,
to: recipient_email.to_s
}
end
def inbox_email
email_from_data || email_from_inbox
end
def email_from_data
return nil unless email_data_has_from?
from_field = email_data['from'].first.to_s
Messages::ForwardedMessageFormatter.parse_from_field(from_field)
end
def email_data_has_from?
email_data.present? &&
email_data['from'].present? &&
email_data['from'].first.present?
end
def email_from_inbox
inbox = forwarded_message&.conversation&.inbox
return nil if inbox.blank? || inbox.channel_type != 'Channel::Email'
email = inbox.channel&.email
return nil if email.blank?
email
end
def recipient_email
return email_data&.dig('to', 0) if email_data&.dig('to', 0).present?
forwarded_message&.content_attributes&.dig('to_emails', 0).presence
end
def subject
email_data&.dig('subject').presence ||
forwarded_message&.conversation&.additional_attributes&.dig('subject').presence ||
'No Subject'
end
def email_date
email_data&.dig('date').presence || Time.zone.now.to_s
end
end
class Messages::ForwardedMessageBuilder
include Messages::ForwardedMessageFormatter
include Messages::ForwardedMessageHtmlBuilder
include Messages::ForwardedMessageContentBuilder
include Messages::ForwardedMessageDataHandler
def initialize(message_id, params = {})
@message_id = message_id
@params = params || {}
end
def perform
return {} unless @message_id
return basic_attributes if forwarded_message.blank?
build_forwarded_attributes
end
def forwarded_email_data(original_content = '')
return {} if forwarded_message.blank?
data = prepare_email_data
full_content = formatted_content(original_content)
# Convert markdown in original_content to plain text
original_plain = Messages::ForwardedMessageFormatter.markdown_to_plain_text(original_content.to_s)
# Convert full_content to plain text if it contains markdown
full_plain = Messages::ForwardedMessageFormatter.markdown_to_plain_text(full_content)
stripped = Messages::ForwardedMessageFormatter.strip_markdown(original_content.to_s)
html_full = formatted_html_content(original_content)
data['html_content'].merge!('quoted' => stripped, 'reply' => full_content, 'full' => html_full)
data['text_content'].merge!(
'quoted' => original_plain,
'reply' => full_plain,
'full' => full_plain
)
data['attachments'] = forwarded_message.attachments.map(&:serializable_hash) if forwarded_message.attachments.present?
data
end
def forwarded_attachments
forwarded_message.attachments if forwarded_message&.attachments&.present?
end
private
def build_forwarded_attributes
{ content_attributes: { forwarded_message_id: @message_id, email: prepare_email_data } }
end
def forwarded_message
@forwarded_message ||= Message.find_by(id: @message_id)
end
def email_data
@email_data ||= forwarded_message&.content_attributes&.dig('email')
end
def basic_attributes
{ content_attributes: { forwarded_message_id: @message_id } }
end
end
+1 -1
View File
@@ -25,7 +25,7 @@ class Messages::MessageBuilder
private
def process_forwarded_message
builder = Messages::ForwardedMessageBuilder.new(@forwarded_message_id, { to_emails: @params[:to_emails] })
builder = Messages::ForwardedMessageBuilderService.new(@forwarded_message_id, { to_emails: @params[:to_emails] })
@forwarded_attributes = builder.perform
@forwarded_message_attachments = builder.forwarded_attachments
@@ -0,0 +1,114 @@
# frozen_string_literal: true
class Messages::ForwardedMessageBuilderService
attr_reader :message_id, :params
def initialize(message_id, params = {})
@message_id = message_id
@params = params || {}
end
def perform
return {} unless message_id
return basic_attributes if forwarded_message.blank?
build_forwarded_attributes
end
def formatted_content(original_content = '')
return original_content.to_s if forwarded_message.blank?
data_handler = Messages::ForwardedMessageDataHandlerService.new(forwarded_message, email_data, params)
formatted_info = data_handler.formatted_info
content_builder = Messages::ForwardedMessageContentBuilderService.new(
formatted_info,
forwarded_message,
email_data
)
content_builder.formatted_content(original_content)
end
def forwarded_email_data(original_content = '')
return {} if forwarded_message.blank?
data_handler = Messages::ForwardedMessageDataHandlerService.new(forwarded_message, email_data, params)
data = data_handler.prepare_email_data
formatted_info = data_handler.formatted_info
content_builder = Messages::ForwardedMessageContentBuilderService.new(
formatted_info,
forwarded_message,
email_data
)
process_email_content(data, content_builder, original_content)
end
def forwarded_attachments
forwarded_message.attachments if forwarded_message&.attachments&.present?
end
private
def process_email_content(data, content_builder, original_content)
full_content = content_builder.formatted_content(original_content)
process_text_content(data, original_content, full_content)
process_html_content(data, content_builder, original_content, full_content)
add_attachments(data)
data
end
def process_text_content(data, original_content, full_content)
# Convert markdown in original_content to plain text
original_plain = Messages::ForwardedMessageFormatterService.markdown_to_plain_text(original_content.to_s)
# Convert full_content to plain text if it contains markdown
full_plain = Messages::ForwardedMessageFormatterService.markdown_to_plain_text(full_content)
data['text_content'].merge!(
'quoted' => original_plain,
'reply' => full_plain,
'full' => full_plain
)
end
def process_html_content(data, content_builder, original_content, full_content)
stripped = Messages::ForwardedMessageFormatterService.strip_markdown(original_content.to_s)
html_full = content_builder.formatted_html_content(original_content)
data['html_content'].merge!(
'quoted' => stripped,
'reply' => full_content,
'full' => html_full
)
end
def add_attachments(data)
return if forwarded_message.attachments.blank?
data['attachments'] = forwarded_message.attachments.map(&:serializable_hash)
end
def build_forwarded_attributes
{ content_attributes: { forwarded_message_id: message_id, email: prepare_email_data } }
end
def prepare_email_data
data_handler = Messages::ForwardedMessageDataHandlerService.new(forwarded_message, email_data, params)
data_handler.prepare_email_data
end
def forwarded_message
@forwarded_message ||= Message.find_by(id: message_id)
end
def email_data
@email_data ||= forwarded_message&.content_attributes&.dig('email')
end
def basic_attributes
{ content_attributes: { forwarded_message_id: message_id } }
end
end
@@ -0,0 +1,60 @@
# frozen_string_literal: true
class Messages::ForwardedMessageContentBuilderService
attr_reader :formatted_info, :forwarded_message, :email_data
def initialize(formatted_info, forwarded_message, email_data)
@formatted_info = formatted_info
@forwarded_message = forwarded_message
@email_data = email_data
end
def forwarded_header_text
return '' unless formatted_info.values.any?
build_header_lines.compact.join("\n")
end
def forwarded_body_text
return forwarded_message.content.to_s if email_data.blank?
text = email_data.dig('text_content', 'full')
html = email_data.dig('html_content', 'full')
if text.present?
text
elsif html.present?
ActionView::Base.full_sanitizer.sanitize(html)
else
forwarded_message.content.to_s
end
end
def formatted_content(original_content = '')
return original_content.to_s if forwarded_message.blank?
original_content.to_s + forwarded_header_text + forwarded_body_text
end
def formatted_html_content(original_content = '')
return original_content if forwarded_message.blank?
# Always use markdown conversion since it handles plain text correctly
converted_content = Messages::ForwardedMessageFormatterService.convert_markdown_to_html(original_content)
html_builder = Messages::ForwardedMessageHtmlBuilderService.new(formatted_info, forwarded_message, email_data)
html_builder.html_wrapper(converted_content)
end
private
def build_header_lines
[
"\n\n---------- Forwarded message ---------",
("From: #{formatted_info[:from]}" if formatted_info[:from].present?),
("Date: #{formatted_info[:date]}" if formatted_info[:date].present?),
("Subject: #{formatted_info[:subject]}" if formatted_info[:subject].present?),
("To: <#{formatted_info[:to]}>" if formatted_info[:to].present?),
"\n"
]
end
end
@@ -0,0 +1,92 @@
# frozen_string_literal: true
class Messages::ForwardedMessageDataHandlerService
attr_reader :forwarded_message, :email_data, :params
def initialize(forwarded_message, email_data, params = {})
@forwarded_message = forwarded_message
@email_data = email_data
@params = params || {}
end
def prepare_email_data
initialize_email_data
end
def formatted_info
{
from: inbox_email.to_s,
date: Messages::ForwardedMessageFormatterService.format_date_string(email_date),
subject: subject.to_s,
to: recipient_email.to_s
}
end
private
def initialize_email_data
data = base_email_data
add_content_fields(data)
add_header_fields(data)
data
end
def base_email_data
email_data.present? ? email_data.dup || {} : {}
end
def add_content_fields(data)
data['html_content'] ||= {}
data['text_content'] ||= {}
end
def add_header_fields(data)
data['from'] = [email_from_inbox] # Always overwrite with inbox email
data['to'] = Array(params[:to_emails]) if params && params[:to_emails].present?
data['subject'] ||= subject
data['date'] ||= Time.zone.now.to_s
end
def inbox_email
email_from_data || email_from_inbox
end
def email_from_data
return nil unless email_data_has_from?
from_field = email_data['from'].first.to_s
Messages::ForwardedMessageFormatterService.parse_from_field(from_field)
end
def email_data_has_from?
email_data.present? &&
email_data['from'].present? &&
email_data['from'].first.present?
end
def email_from_inbox
inbox = forwarded_message&.conversation&.inbox
return nil if inbox.blank? || inbox.channel_type != 'Channel::Email'
email = inbox.channel&.email
return nil if email.blank?
email
end
def recipient_email
return email_data&.dig('to', 0) if email_data&.dig('to', 0).present?
forwarded_message&.content_attributes&.dig('to_emails', 0).presence
end
def subject
email_data&.dig('subject').presence ||
forwarded_message&.conversation&.additional_attributes&.dig('subject').presence ||
'No Subject'
end
def email_date
email_data&.dig('date').presence || Time.zone.now.to_s
end
end
@@ -0,0 +1,58 @@
# frozen_string_literal: true
class Messages::ForwardedMessageFormatterService
def self.parse_from_field(from_field)
return '' if from_field.blank?
from_field =~ /(.*)<(.*)>/ ? Regexp.last_match(2).strip : from_field
end
def self.format_plain_text_to_html(text)
ERB::Util.html_escape(text.to_s).gsub("\n", '<br>')
end
def self.extract_email(from_field)
return '' if from_field.blank?
from_field =~ /<(.*)>/ ? Regexp.last_match(1) : from_field
end
def self.format_date_string(date_str)
return '' if date_str.blank?
begin
parsed_date = DateTime.now
parsed_date.strftime('%a, %b %-d, %Y at %-l:%M %p')
rescue StandardError
date_str
end
end
def self.strip_markdown(text)
return '' if text.blank?
# Convert markdown to HTML using CommonMarker
html = CommonMarker.render_html(text, :DEFAULT)
# Strip HTML tags to get plain text
ActionView::Base.full_sanitizer.sanitize(html)
end
def self.convert_markdown_to_html(text)
return '' if text.blank?
# Use CommonMarker with GitHub Flavored Markdown options
options = [:GITHUB_PRE_LANG, :UNSAFE]
extensions = [:table, :strikethrough, :autolink]
CommonMarker.render_html(text, options, extensions)
end
# Convert text to plain text, stripping any markdown formatting
def self.markdown_to_plain_text(text)
return '' if text.blank?
# Simply strip any markdown by converting to HTML and sanitizing
strip_markdown(text)
end
end
@@ -0,0 +1,68 @@
# frozen_string_literal: true
class Messages::ForwardedMessageHtmlBuilderService
attr_reader :formatted_info, :forwarded_message, :email_data
def initialize(formatted_info, forwarded_message, email_data)
@formatted_info = formatted_info
@forwarded_message = forwarded_message
@email_data = email_data
end
def html_wrapper(content)
base = '<div dir="ltr">'
if content.blank?
"#{base}#{forwarded_header_html}#{forwarded_body_html}</div>"
else
"#{base}#{content}<br><br>#{forwarded_header_html}#{forwarded_body_html}</div>"
end
end
def forwarded_header_html
from_value = formatted_info[:from].to_s
email = Messages::ForwardedMessageFormatterService.extract_email(from_value)
[
'<div class="gmail_quote gmail_quote_container">',
'<div dir="ltr" class="gmail_attr">---------- Forwarded message ---------<br>',
build_email_html(email),
build_field_html('Date', formatted_info[:date]),
build_field_html('Subject', formatted_info[:subject]),
build_to_html(formatted_info[:to]),
'</div><br><br>'
].compact.join
end
def forwarded_body_html
# Return HTML content directly if available
return email_data.dig('html_content', 'full') if email_data&.dig('html_content', 'full').present?
# Otherwise format content to HTML
content = if email_data&.dig('text_content', 'full').present?
email_data.dig('text_content', 'full')
else
forwarded_message.content.to_s
end
# Always use markdown conversion since it handles plain text correctly
Messages::ForwardedMessageFormatterService.convert_markdown_to_html(content)
end
private
def build_email_html(email)
return nil if email.blank?
"From: &lt;<a href=\"mailto:#{email}\">#{email}</a>&gt;<br>"
end
def build_field_html(label, value)
value.present? ? "#{label}: #{value}<br>" : nil
end
def build_to_html(to_email)
return nil if to_email.blank?
"To: &lt;<a href=\"mailto:#{to_email}\">#{to_email}</a>&gt;<br>"
end
end
@@ -1,6 +1,6 @@
require 'rails_helper'
RSpec.describe Messages::ForwardedMessageBuilder do
RSpec.describe Messages::ForwardedMessageBuilderService do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:conversation) { create(:conversation, inbox: inbox, account: account) }
@@ -140,28 +140,6 @@ RSpec.describe Messages::ForwardedMessageBuilder do
end
end
describe '#formatted_html_content' do
context 'when forwarded message is blank' do
it 'returns original content' do
builder = described_class.new(999) # Non-existent message ID
expect(builder.formatted_html_content('Original')).to eq('Original')
end
end
context 'when forwarded message has email data' do
it 'returns formatted HTML content with header and body' do
builder = described_class.new(forwarded_message.id)
result = builder.formatted_html_content('**Original**')
# Updated expectation to match CommonMarker output format
expect(result).to include('<p><strong>Original</strong></p>')
expect(result).to include('---------- Forwarded message ---------')
expect(result).to include('Subject: Test Subject')
expect(result).to include('<div>HTML content</div>') # Should include the HTML content
end
end
end
describe '#forwarded_email_data' do
context 'when forwarded message is blank' do
it 'returns empty hash' do
@@ -237,146 +215,4 @@ RSpec.describe Messages::ForwardedMessageBuilder do
end
end
end
describe 'formatter methods' do
context 'when using convert_markdown_to_html' do
it 'converts bold markdown to HTML' do
formatted = Messages::ForwardedMessageFormatter.convert_markdown_to_html('**Bold Text**')
# Updated expectation to match CommonMarker output format
expect(formatted).to include('<strong>Bold Text</strong>')
end
it 'converts italic markdown to HTML' do
formatted = Messages::ForwardedMessageFormatter.convert_markdown_to_html('*Italic Text*')
expect(formatted).to include('<em>Italic Text</em>')
end
it 'converts underscore italic markdown to HTML' do
formatted = Messages::ForwardedMessageFormatter.convert_markdown_to_html('_Italic Text_')
expect(formatted).to include('<em>Italic Text</em>')
end
it 'handles multiple markdown elements' do
formatted = Messages::ForwardedMessageFormatter.convert_markdown_to_html('**Bold** and _italic_')
expect(formatted).to include('<strong>Bold</strong>')
expect(formatted).to include('<em>italic</em>')
end
it 'handles empty text' do
expect(Messages::ForwardedMessageFormatter.convert_markdown_to_html('')).to eq('')
end
it 'handles nil text' do
expect(Messages::ForwardedMessageFormatter.convert_markdown_to_html(nil)).to eq('')
end
end
context 'when using strip_markdown' do
it 'strips bold markdown' do
stripped = Messages::ForwardedMessageFormatter.strip_markdown('**Bold Text**')
expect(stripped.strip).to eq('Bold Text')
end
it 'strips italic markdown' do
stripped = Messages::ForwardedMessageFormatter.strip_markdown('*Italic Text*')
expect(stripped.strip).to eq('Italic Text')
end
it 'strips underscore italic markdown' do
stripped = Messages::ForwardedMessageFormatter.strip_markdown('_Italic Text_')
expect(stripped.strip).to eq('Italic Text')
end
it 'handles multiple markdown elements' do
stripped = Messages::ForwardedMessageFormatter.strip_markdown('**Bold** and _italic_')
# Updated expectation to handle trailing newline
expect(stripped.strip).to eq('Bold and italic')
end
it 'handles empty text' do
expect(Messages::ForwardedMessageFormatter.strip_markdown('')).to eq('')
end
it 'handles nil text' do
expect(Messages::ForwardedMessageFormatter.strip_markdown(nil)).to eq('')
end
end
context 'when using extract_email' do
it 'extracts email from format "Name <email@example.com>"' do
extracted = Messages::ForwardedMessageFormatter.extract_email('John Doe <john@example.com>')
expect(extracted).to eq('john@example.com')
end
it 'returns plain email as-is' do
extracted = Messages::ForwardedMessageFormatter.extract_email('john@example.com')
expect(extracted).to eq('john@example.com')
end
it 'handles empty string' do
expect(Messages::ForwardedMessageFormatter.extract_email('')).to eq('')
end
it 'handles nil' do
expect(Messages::ForwardedMessageFormatter.extract_email(nil)).to eq('')
end
end
context 'when using parse_from_field' do
it 'extracts email from format "Name <email@example.com>"' do
parsed = Messages::ForwardedMessageFormatter.parse_from_field('John Doe <john@example.com>')
expect(parsed).to eq('john@example.com')
end
it 'returns plain email as-is' do
parsed = Messages::ForwardedMessageFormatter.parse_from_field('john@example.com')
expect(parsed).to eq('john@example.com')
end
it 'handles empty string' do
expect(Messages::ForwardedMessageFormatter.parse_from_field('')).to eq('')
end
it 'handles nil' do
expect(Messages::ForwardedMessageFormatter.parse_from_field(nil)).to eq('')
end
end
context 'when using format_date_string' do
it 'formats the date in a readable format' do
# NOTE: We don't test the exact formatted output since it uses DateTime.now
# which would be different for each test run
result = Messages::ForwardedMessageFormatter.format_date_string('2025-04-29T14:29:07+05:30')
expect(result).to match(/\w{3}, \w{3} \d+, \d{4} at \d+:\d+ [AP]M/)
end
it 'handles empty string' do
expect(Messages::ForwardedMessageFormatter.format_date_string('')).to eq('')
end
it 'handles nil' do
expect(Messages::ForwardedMessageFormatter.format_date_string(nil)).to eq('')
end
end
context 'when using format_plain_text_to_html' do
it 'converts newlines to <br>' do
result = Messages::ForwardedMessageFormatter.format_plain_text_to_html("Line 1\nLine 2")
expect(result).to eq('Line 1<br>Line 2')
end
it 'escapes HTML special characters' do
result = Messages::ForwardedMessageFormatter.format_plain_text_to_html('<script>alert("XSS")</script>')
expect(result).to eq('&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;')
end
it 'handles empty string' do
expect(Messages::ForwardedMessageFormatter.format_plain_text_to_html('')).to eq('')
end
it 'handles nil' do
expect(Messages::ForwardedMessageFormatter.format_plain_text_to_html(nil)).to eq('')
end
end
end
end
@@ -0,0 +1,144 @@
require 'rails_helper'
RSpec.describe Messages::ForwardedMessageContentBuilderService do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:conversation) { create(:conversation, inbox: inbox, account: account) }
let(:formatted_info) do
{
from: 'sender@example.com',
date: 'Wed, Apr 29, 2025 at 2:29 PM',
subject: 'Test Subject',
to: 'recipient@example.com'
}
end
let(:base_email_content) do
{
'html_content' => {
'full' => '<div>HTML content</div>'
},
'text_content' => {
'full' => 'Text content'
}
}
end
let(:forwarded_message) do
create(:message, conversation: conversation, account: account,
content_attributes: { email: base_email_content })
end
let(:html_only_message) do
create(:message, conversation: conversation, account: account,
content_attributes: {
email: {
'html_content' => { 'full' => '<div>HTML only content</div>' },
'text_content' => {}
}
})
end
let(:content_builder) { described_class.new(formatted_info, forwarded_message, base_email_content) }
let(:html_only_builder) { described_class.new(formatted_info, html_only_message, html_only_message.content_attributes[:email]) }
let(:empty_builder) { described_class.new(formatted_info, forwarded_message, {}) }
describe '#forwarded_header_text' do
it 'returns a formatted header with all information' do
header = content_builder.forwarded_header_text
expect(header).to include('---------- Forwarded message ---------')
expect(header).to include('From: sender@example.com')
expect(header).to include('Date: Wed, Apr 29, 2025 at 2:29 PM')
expect(header).to include('Subject: Test Subject')
expect(header).to include('To: <recipient@example.com>')
end
it 'returns a formatted header without missing fields' do
empty_info = { from: '', date: '', subject: '', to: '' }
builder = described_class.new(empty_info, forwarded_message, base_email_content)
expect(builder.forwarded_header_text).to include('---------- Forwarded message ---------')
expect(builder.forwarded_header_text).not_to include('From: ')
expect(builder.forwarded_header_text).not_to include('Date: ')
expect(builder.forwarded_header_text).not_to include('Subject: ')
expect(builder.forwarded_header_text).not_to include('To: ')
end
it 'handles partial information' do
partial_info = { from: 'sender@example.com', subject: 'Test Subject' }
builder = described_class.new(partial_info, forwarded_message, base_email_content)
header = builder.forwarded_header_text
expect(header).to include('From: sender@example.com')
expect(header).to include('Subject: Test Subject')
expect(header).not_to include('Date:')
expect(header).not_to include('To:')
end
end
describe '#forwarded_body_text' do
it 'returns text content when available' do
expect(content_builder.forwarded_body_text).to eq('Text content')
end
it 'returns sanitized HTML content when only HTML is available' do
expect(html_only_builder.forwarded_body_text).to include('HTML only content')
end
it 'returns message content when no email data is available' do
expect(empty_builder.forwarded_body_text).to eq(forwarded_message.content.to_s)
end
end
describe '#formatted_content' do
it 'returns original content concatenated with header and body' do
result = content_builder.formatted_content('Original message')
expect(result).to start_with('Original message')
expect(result).to include('---------- Forwarded message ---------')
expect(result).to include('Text content')
end
it 'returns just the original content when forwarded message is blank' do
builder = described_class.new(formatted_info, nil, base_email_content)
expect(builder.formatted_content('Original message')).to eq('Original message')
end
it 'handles empty original content' do
result = content_builder.formatted_content('')
expect(result).to include('---------- Forwarded message ---------')
expect(result).to include('Text content')
end
end
describe '#formatted_html_content' do
it 'converts markdown in original content to HTML' do
html_builder = instance_double(Messages::ForwardedMessageHtmlBuilderService)
allow(Messages::ForwardedMessageHtmlBuilderService).to receive(:new)
.with(formatted_info, forwarded_message, base_email_content)
.and_return(html_builder)
allow(Messages::ForwardedMessageFormatterService).to receive(:convert_markdown_to_html)
.with('**Original** message')
.and_return('<p>Converted HTML</p>')
allow(html_builder).to receive(:html_wrapper)
.with('<p>Converted HTML</p>')
.and_return('<div>Wrapped content</div>')
result = content_builder.formatted_html_content('**Original** message')
expect(Messages::ForwardedMessageFormatterService).to have_received(:convert_markdown_to_html)
.with('**Original** message')
expect(result).to eq('<div>Wrapped content</div>')
end
it 'returns original content when forwarded message is blank' do
builder = described_class.new(formatted_info, nil, base_email_content)
expect(builder.formatted_html_content('Original message')).to eq('Original message')
end
end
end
@@ -0,0 +1,180 @@
require 'rails_helper'
RSpec.describe Messages::ForwardedMessageDataHandlerService do
let(:account) { create(:account) }
let(:email_channel) { create(:channel_email, account: account) }
let(:email_inbox) { create(:inbox, channel: email_channel, account: account) }
let(:conversation) { create(:conversation, inbox: email_inbox, account: account) }
let(:regular_inbox) { create(:inbox, account: account) }
let(:regular_conversation) { create(:conversation, inbox: regular_inbox, account: account) }
let(:base_email_content) do
{
'from' => ['John Doe <sender@example.com>'],
'to' => ['recipient@example.com'],
'subject' => 'Test Subject',
'date' => '2025-04-29T14:29:07+05:30'
}
end
# Message with email data and from an email inbox
let(:email_message) do
create(:message, conversation: conversation, account: account,
content_attributes: { email: base_email_content })
end
# Message with email data but from a regular inbox
let(:regular_message) do
create(:message, conversation: regular_conversation, account: account,
content_attributes: { email: base_email_content })
end
# Message with no email data
let(:message_no_email) { create(:message, conversation: conversation, account: account) }
describe '#prepare_email_data' do
it 'returns empty hash structure when email_data is nil' do
handler = described_class.new(message_no_email, nil)
result = handler.prepare_email_data
expect(result).to be_a(Hash)
expect(result['html_content']).to eq({})
expect(result['text_content']).to eq({})
expect(result).to have_key('date')
expect(result).to have_key('subject')
end
it 'preserves existing email data and adds required fields' do
handler = described_class.new(email_message, base_email_content)
result = handler.prepare_email_data
expect(result).to be_a(Hash)
expect(result['html_content']).to eq({})
expect(result['text_content']).to eq({})
expect(result['from']).to be_present
expect(result['to']).to eq(['recipient@example.com'])
expect(result['subject']).to eq('Test Subject')
expect(result['date']).to eq('2025-04-29T14:29:07+05:30')
end
it 'adds to_emails from params if provided' do
handler = described_class.new(email_message, base_email_content, { to_emails: 'new@example.com' })
result = handler.prepare_email_data
expect(result['to']).to eq(['new@example.com'])
end
it 'overwrites from field with inbox email' do
handler = described_class.new(email_message, base_email_content)
allow(handler).to receive(:email_from_inbox).and_return('inbox@example.com')
result = handler.prepare_email_data
expect(result['from']).to eq(['inbox@example.com'])
end
end
describe '#formatted_info' do
it 'returns hash with properly formatted fields' do
allow(Messages::ForwardedMessageFormatterService).to receive(:format_date_string).and_return('Formatted Date')
handler = described_class.new(email_message, base_email_content)
result = handler.formatted_info
expect(result[:from]).to be_present
expect(result[:date]).to eq('Formatted Date')
expect(result[:subject]).to eq('Test Subject')
expect(result[:to]).to eq('recipient@example.com')
expect(Messages::ForwardedMessageFormatterService).to have_received(:format_date_string).with('2025-04-29T14:29:07+05:30')
end
it 'works with missing email data' do
# Create instance first, then stub its methods
message_without_email = build(:message)
handler = described_class.new(message_without_email, nil)
# Stub the methods on the specific instance
allow(handler).to receive(:email_from_data).and_return(nil)
allow(handler).to receive(:email_from_inbox).and_return(nil)
# For email_date, we should expect it to use Time.zone.now.to_s when email_data is nil
# So we should mock the format_date_string to return a predictable value
allow(Messages::ForwardedMessageFormatterService).to receive(:format_date_string).with(kind_of(String)).and_return('Fri, May 2, 2025 at 12:33 PM') # rubocop:disable Layout/LineLength
result = handler.formatted_info
expect(result[:from]).to eq('')
expect(result[:date]).to eq('Fri, May 2, 2025 at 12:33 PM')
expect(result[:subject]).to eq('No Subject')
expect(result[:to]).to eq('')
end
end
context 'when private methods are called' do
describe '#inbox_email' do
it 'returns email from data when available' do
handler = described_class.new(email_message, base_email_content)
allow(handler).to receive(:email_from_data).and_return('from_data@example.com')
allow(handler).to receive(:email_from_inbox).and_return('from_inbox@example.com')
result = handler.send(:inbox_email)
expect(result).to eq('from_data@example.com')
end
it 'returns email from inbox when email data is not available' do
handler = described_class.new(email_message, {})
allow(handler).to receive(:email_from_data).and_return(nil)
allow(handler).to receive(:email_from_inbox).and_return('from_inbox@example.com')
result = handler.send(:inbox_email)
expect(result).to eq('from_inbox@example.com')
end
end
describe '#email_from_data' do
it 'parses email address from from field' do
handler = described_class.new(email_message, base_email_content)
allow(Messages::ForwardedMessageFormatterService).to receive(:parse_from_field).and_return('parsed@example.com')
result = handler.send(:email_from_data)
expect(Messages::ForwardedMessageFormatterService).to have_received(:parse_from_field).with('John Doe <sender@example.com>')
expect(result).to eq('parsed@example.com')
end
it 'returns nil when from field is not available' do
handler = described_class.new(email_message, {})
result = handler.send(:email_from_data)
expect(result).to be_nil
end
end
describe '#subject' do
it 'returns subject from email data when available' do
handler = described_class.new(email_message, base_email_content)
result = handler.send(:subject)
expect(result).to eq('Test Subject')
end
it 'returns subject from conversation when available' do
allow(conversation).to receive(:additional_attributes).and_return({ 'subject' => 'Conversation Subject' })
handler = described_class.new(email_message, {})
result = handler.send(:subject)
expect(result).to eq('Conversation Subject')
end
it 'returns "No Subject" when no subject is available' do
handler = described_class.new(message_no_email, nil)
result = handler.send(:subject)
expect(result).to eq('No Subject')
end
end
end
end
@@ -0,0 +1,158 @@
require 'rails_helper'
RSpec.describe Messages::ForwardedMessageFormatterService do
describe '.convert_markdown_to_html' do
it 'converts bold markdown to HTML' do
formatted = described_class.convert_markdown_to_html('**Bold Text**')
# Updated expectation to match CommonMarker output format
expect(formatted).to include('<strong>Bold Text</strong>')
end
it 'converts italic markdown to HTML' do
formatted = described_class.convert_markdown_to_html('*Italic Text*')
expect(formatted).to include('<em>Italic Text</em>')
end
it 'converts underscore italic markdown to HTML' do
formatted = described_class.convert_markdown_to_html('_Italic Text_')
expect(formatted).to include('<em>Italic Text</em>')
end
it 'handles multiple markdown elements' do
formatted = described_class.convert_markdown_to_html('**Bold** and _italic_')
expect(formatted).to include('<strong>Bold</strong>')
expect(formatted).to include('<em>italic</em>')
end
it 'handles empty text' do
expect(described_class.convert_markdown_to_html('')).to eq('')
end
it 'handles nil text' do
expect(described_class.convert_markdown_to_html(nil)).to eq('')
end
end
describe '.strip_markdown' do
it 'strips bold markdown' do
stripped = described_class.strip_markdown('**Bold Text**')
expect(stripped.strip).to eq('Bold Text')
end
it 'strips italic markdown' do
stripped = described_class.strip_markdown('*Italic Text*')
expect(stripped.strip).to eq('Italic Text')
end
it 'strips underscore italic markdown' do
stripped = described_class.strip_markdown('_Italic Text_')
expect(stripped.strip).to eq('Italic Text')
end
it 'handles multiple markdown elements' do
stripped = described_class.strip_markdown('**Bold** and _italic_')
# Updated expectation to handle trailing newline
expect(stripped.strip).to eq('Bold and italic')
end
it 'handles empty text' do
expect(described_class.strip_markdown('')).to eq('')
end
it 'handles nil text' do
expect(described_class.strip_markdown(nil)).to eq('')
end
end
describe '.extract_email' do
it 'extracts email from format "Name <email@example.com>"' do
extracted = described_class.extract_email('John Doe <john@example.com>')
expect(extracted).to eq('john@example.com')
end
it 'returns plain email as-is' do
extracted = described_class.extract_email('john@example.com')
expect(extracted).to eq('john@example.com')
end
it 'handles empty string' do
expect(described_class.extract_email('')).to eq('')
end
it 'handles nil' do
expect(described_class.extract_email(nil)).to eq('')
end
end
describe '.parse_from_field' do
it 'extracts email from format "Name <email@example.com>"' do
parsed = described_class.parse_from_field('John Doe <john@example.com>')
expect(parsed).to eq('john@example.com')
end
it 'returns plain email as-is' do
parsed = described_class.parse_from_field('john@example.com')
expect(parsed).to eq('john@example.com')
end
it 'handles empty string' do
expect(described_class.parse_from_field('')).to eq('')
end
it 'handles nil' do
expect(described_class.parse_from_field(nil)).to eq('')
end
end
describe '.format_date_string' do
it 'formats the date in a readable format' do
# NOTE: We don't test the exact formatted output since it uses DateTime.now
# which would be different for each test run
result = described_class.format_date_string('2025-04-29T14:29:07+05:30')
expect(result).to match(/\w{3}, \w{3} \d+, \d{4} at \d+:\d+ [AP]M/)
end
it 'handles empty string' do
expect(described_class.format_date_string('')).to eq('')
end
it 'handles nil' do
expect(described_class.format_date_string(nil)).to eq('')
end
end
describe '.format_plain_text_to_html' do
it 'converts newlines to <br>' do
result = described_class.format_plain_text_to_html("Line 1\nLine 2")
expect(result).to eq('Line 1<br>Line 2')
end
it 'escapes HTML special characters' do
result = described_class.format_plain_text_to_html('<script>alert("XSS")</script>')
expect(result).to eq('&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;')
end
it 'handles empty string' do
expect(described_class.format_plain_text_to_html('')).to eq('')
end
it 'handles nil' do
expect(described_class.format_plain_text_to_html(nil)).to eq('')
end
end
describe '.markdown_to_plain_text' do
it 'converts markdown text to plain text' do
plain_text = described_class.markdown_to_plain_text('**Bold** and _italic_ text')
expect(plain_text.strip).to eq('Bold and italic text')
end
it 'handles empty string' do
expect(described_class.markdown_to_plain_text('')).to eq('')
end
it 'handles nil' do
expect(described_class.markdown_to_plain_text(nil)).to eq('')
end
end
end
@@ -0,0 +1,156 @@
require 'rails_helper'
RSpec.describe Messages::ForwardedMessageHtmlBuilderService do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:conversation) { create(:conversation, inbox: inbox, account: account) }
let(:formatted_info) do
{
from: 'sender@example.com',
date: 'Wed, Apr 29, 2025 at 2:29 PM',
subject: 'Test Subject',
to: 'recipient@example.com'
}
end
let(:base_email_content) do
{
'html_content' => {
'full' => '<div>HTML content</div>'
},
'text_content' => {
'full' => 'Text content'
}
}
end
let(:forwarded_message) do
create(:message, conversation: conversation, account: account,
content_attributes: { email: base_email_content })
end
let(:html_builder) { described_class.new(formatted_info, forwarded_message, base_email_content) }
let(:empty_builder) { described_class.new({}, forwarded_message, {}) }
describe '#html_wrapper' do
it 'wraps content with proper HTML structure' do
result = html_builder.html_wrapper('<p>Test content</p>')
expect(result).to start_with('<div dir="ltr">')
expect(result).to include('<p>Test content</p>')
expect(result).to include('<br><br>')
expect(result).to include('---------- Forwarded message ---------')
expect(result).to end_with('</div>')
end
it 'includes header and body even when content is blank' do
result = html_builder.html_wrapper('')
expect(result).to start_with('<div dir="ltr">')
expect(result).to include('---------- Forwarded message ---------')
expect(result).to include('<br><br>')
expect(result).to end_with('</div>')
end
end
describe '#forwarded_header_html' do
it 'formats header with all fields' do
allow(Messages::ForwardedMessageFormatterService).to receive(:extract_email).and_return('sender@example.com')
result = html_builder.forwarded_header_html
expect(result).to include('gmail_quote_container')
expect(result).to include('---------- Forwarded message ---------')
expect(result).to include('From: &lt;<a href="mailto:sender@example.com">sender@example.com</a>&gt;')
expect(result).to include('Date: Wed, Apr 29, 2025 at 2:29 PM')
expect(result).to include('Subject: Test Subject')
expect(result).to include('To: &lt;<a href="mailto:recipient@example.com">recipient@example.com</a>&gt;')
expect(Messages::ForwardedMessageFormatterService).to have_received(:extract_email).with('sender@example.com')
end
it 'omits empty fields' do
empty_info = { from: '', subject: 'Test Subject' }
builder = described_class.new(empty_info, forwarded_message, base_email_content)
result = builder.forwarded_header_html
expect(result).to include('---------- Forwarded message ---------')
expect(result).to include('Subject: Test Subject')
expect(result).not_to include('From:')
expect(result).not_to include('Date:')
expect(result).not_to include('To:')
end
end
describe '#forwarded_body_html' do
it 'returns HTML content when available' do
result = html_builder.forwarded_body_html
expect(result).to eq('<div>HTML content</div>')
end
it 'converts text to HTML when HTML is not available' do
text_only_content = { 'text_content' => { 'full' => 'Text only content' } }
builder = described_class.new(formatted_info, forwarded_message, text_only_content)
allow(Messages::ForwardedMessageFormatterService).to receive(:convert_markdown_to_html).and_return('<p>Converted HTML</p>')
result = builder.forwarded_body_html
expect(Messages::ForwardedMessageFormatterService).to have_received(:convert_markdown_to_html).with('Text only content')
expect(result).to eq('<p>Converted HTML</p>')
end
it 'falls back to message content when no email data is available' do
allow(forwarded_message).to receive(:content).and_return('Message content')
result = empty_builder.forwarded_body_html
expect(Messages::ForwardedMessageFormatterService).to receive(:convert_markdown_to_html).with('Message content')
empty_builder.forwarded_body_html
end
end
describe 'private methods' do
describe '#build_email_html' do
it 'formats email with HTML markup' do
result = html_builder.send(:build_email_html, 'test@example.com')
expect(result).to eq('From: &lt;<a href="mailto:test@example.com">test@example.com</a>&gt;<br>')
end
it 'returns nil for blank email' do
expect(html_builder.send(:build_email_html, '')).to be_nil
expect(html_builder.send(:build_email_html, nil)).to be_nil
end
end
describe '#build_field_html' do
it 'formats field with label and value' do
result = html_builder.send(:build_field_html, 'Test', 'Value')
expect(result).to eq('Test: Value<br>')
end
it 'returns nil for blank value' do
expect(html_builder.send(:build_field_html, 'Test', '')).to be_nil
expect(html_builder.send(:build_field_html, 'Test', nil)).to be_nil
end
end
describe '#build_to_html' do
it 'formats to field with HTML markup' do
result = html_builder.send(:build_to_html, 'recipient@example.com')
expect(result).to eq('To: &lt;<a href="mailto:recipient@example.com">recipient@example.com</a>&gt;<br>')
end
it 'returns nil for blank email' do
expect(html_builder.send(:build_to_html, '')).to be_nil
expect(html_builder.send(:build_to_html, nil)).to be_nil
end
end
end
end