diff --git a/app/builders/messages/forwarded_message_builder.rb b/app/builders/messages/forwarded_message_builder.rb
deleted file mode 100644
index d5ed58f40..000000000
--- a/app/builders/messages/forwarded_message_builder.rb
+++ /dev/null
@@ -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", '
')
- 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 = '
'
- if content.blank?
- "#{base}#{forwarded_header_html}#{forwarded_body_html}
"
- else
- "#{base}#{content}',
- '
---------- Forwarded message ---------
',
- build_email_html(email),
- build_field_html('Date', formatted_info[:date]),
- build_field_html('Subject', formatted_info[:subject]),
- build_to_html(formatted_info[:to]),
- '
'
- ].compact.join
- end
-
- def build_email_html(email)
- return nil if email.blank?
-
- "From: <
#{email}>
"
- end
-
- def build_field_html(label, value)
- value.present? ? "#{label}: #{value}
" : nil
- end
-
- def build_to_html(to_email)
- return nil if to_email.blank?
-
- "To: <
#{to_email}>
"
- 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
diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb
index 806604701..c9c6bb120 100644
--- a/app/builders/messages/message_builder.rb
+++ b/app/builders/messages/message_builder.rb
@@ -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
diff --git a/app/services/messages/forwarded_message_builder_service.rb b/app/services/messages/forwarded_message_builder_service.rb
new file mode 100644
index 000000000..36208ddf6
--- /dev/null
+++ b/app/services/messages/forwarded_message_builder_service.rb
@@ -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
diff --git a/app/services/messages/forwarded_message_content_builder_service.rb b/app/services/messages/forwarded_message_content_builder_service.rb
new file mode 100644
index 000000000..315a50274
--- /dev/null
+++ b/app/services/messages/forwarded_message_content_builder_service.rb
@@ -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
diff --git a/app/services/messages/forwarded_message_data_handler_service.rb b/app/services/messages/forwarded_message_data_handler_service.rb
new file mode 100644
index 000000000..a526b2b4f
--- /dev/null
+++ b/app/services/messages/forwarded_message_data_handler_service.rb
@@ -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
diff --git a/app/services/messages/forwarded_message_formatter_service.rb b/app/services/messages/forwarded_message_formatter_service.rb
new file mode 100644
index 000000000..811c7db8c
--- /dev/null
+++ b/app/services/messages/forwarded_message_formatter_service.rb
@@ -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", '
')
+ 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
diff --git a/app/services/messages/forwarded_message_html_builder_service.rb b/app/services/messages/forwarded_message_html_builder_service.rb
new file mode 100644
index 000000000..254e1cd13
--- /dev/null
+++ b/app/services/messages/forwarded_message_html_builder_service.rb
@@ -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 = '
'
+ if content.blank?
+ "#{base}#{forwarded_header_html}#{forwarded_body_html}
"
+ else
+ "#{base}#{content}
#{forwarded_header_html}#{forwarded_body_html}
"
+ end
+ end
+
+ def forwarded_header_html
+ from_value = formatted_info[:from].to_s
+ email = Messages::ForwardedMessageFormatterService.extract_email(from_value)
+
+ [
+ '',
+ '
---------- Forwarded message ---------
',
+ build_email_html(email),
+ build_field_html('Date', formatted_info[:date]),
+ build_field_html('Subject', formatted_info[:subject]),
+ build_to_html(formatted_info[:to]),
+ '
'
+ ].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: <
#{email}>
"
+ end
+
+ def build_field_html(label, value)
+ value.present? ? "#{label}: #{value}
" : nil
+ end
+
+ def build_to_html(to_email)
+ return nil if to_email.blank?
+
+ "To: <
#{to_email}>
"
+ end
+end
diff --git a/spec/builders/messages/forwarded_message_builder_spec.rb b/spec/services/messages/forwarded_message_builder_service_spec.rb
similarity index 55%
rename from spec/builders/messages/forwarded_message_builder_spec.rb
rename to spec/services/messages/forwarded_message_builder_service_spec.rb
index 8509aa61b..f714f909d 100644
--- a/spec/builders/messages/forwarded_message_builder_spec.rb
+++ b/spec/services/messages/forwarded_message_builder_service_spec.rb
@@ -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('
Original
')
- expect(result).to include('---------- Forwarded message ---------')
- expect(result).to include('Subject: Test Subject')
- expect(result).to include('
HTML content
') # 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('
Bold Text')
- end
-
- it 'converts italic markdown to HTML' do
- formatted = Messages::ForwardedMessageFormatter.convert_markdown_to_html('*Italic Text*')
- expect(formatted).to include('
Italic Text')
- end
-
- it 'converts underscore italic markdown to HTML' do
- formatted = Messages::ForwardedMessageFormatter.convert_markdown_to_html('_Italic Text_')
- expect(formatted).to include('
Italic Text')
- end
-
- it 'handles multiple markdown elements' do
- formatted = Messages::ForwardedMessageFormatter.convert_markdown_to_html('**Bold** and _italic_')
- expect(formatted).to include('
Bold')
- expect(formatted).to include('
italic')
- 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
"' do
- extracted = Messages::ForwardedMessageFormatter.extract_email('John Doe ')
- 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 "' do
- parsed = Messages::ForwardedMessageFormatter.parse_from_field('John Doe ')
- 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
' do
- result = Messages::ForwardedMessageFormatter.format_plain_text_to_html("Line 1\nLine 2")
- expect(result).to eq('Line 1
Line 2')
- end
-
- it 'escapes HTML special characters' do
- result = Messages::ForwardedMessageFormatter.format_plain_text_to_html('')
- expect(result).to eq('<script>alert("XSS")</script>')
- 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
diff --git a/spec/services/messages/forwarded_message_content_builder_service_spec.rb b/spec/services/messages/forwarded_message_content_builder_service_spec.rb
new file mode 100644
index 000000000..d4b30d8af
--- /dev/null
+++ b/spec/services/messages/forwarded_message_content_builder_service_spec.rb
@@ -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' => 'HTML content
'
+ },
+ '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' => 'HTML only content
' },
+ '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: ')
+ 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('Converted HTML
')
+
+ allow(html_builder).to receive(:html_wrapper)
+ .with('Converted HTML
')
+ .and_return('Wrapped content
')
+
+ 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('Wrapped content
')
+ 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
diff --git a/spec/services/messages/forwarded_message_data_handler_service_spec.rb b/spec/services/messages/forwarded_message_data_handler_service_spec.rb
new file mode 100644
index 000000000..c2d5b1b4b
--- /dev/null
+++ b/spec/services/messages/forwarded_message_data_handler_service_spec.rb
@@ -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 '],
+ '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 ')
+ 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
diff --git a/spec/services/messages/forwarded_message_formatter_service_spec.rb b/spec/services/messages/forwarded_message_formatter_service_spec.rb
new file mode 100644
index 000000000..16a6013c3
--- /dev/null
+++ b/spec/services/messages/forwarded_message_formatter_service_spec.rb
@@ -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('Bold Text')
+ end
+
+ it 'converts italic markdown to HTML' do
+ formatted = described_class.convert_markdown_to_html('*Italic Text*')
+ expect(formatted).to include('Italic Text')
+ end
+
+ it 'converts underscore italic markdown to HTML' do
+ formatted = described_class.convert_markdown_to_html('_Italic Text_')
+ expect(formatted).to include('Italic Text')
+ end
+
+ it 'handles multiple markdown elements' do
+ formatted = described_class.convert_markdown_to_html('**Bold** and _italic_')
+ expect(formatted).to include('Bold')
+ expect(formatted).to include('italic')
+ 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 "' do
+ extracted = described_class.extract_email('John Doe ')
+ 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 "' do
+ parsed = described_class.parse_from_field('John Doe ')
+ 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
' do
+ result = described_class.format_plain_text_to_html("Line 1\nLine 2")
+ expect(result).to eq('Line 1
Line 2')
+ end
+
+ it 'escapes HTML special characters' do
+ result = described_class.format_plain_text_to_html('')
+ expect(result).to eq('<script>alert("XSS")</script>')
+ 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
diff --git a/spec/services/messages/forwarded_message_html_builder_service_spec.rb b/spec/services/messages/forwarded_message_html_builder_service_spec.rb
new file mode 100644
index 000000000..f7bffc8de
--- /dev/null
+++ b/spec/services/messages/forwarded_message_html_builder_service_spec.rb
@@ -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' => 'HTML content
'
+ },
+ '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('Test content
')
+
+ expect(result).to start_with('')
+ expect(result).to include('
Test content
')
+ expect(result).to include('
')
+ expect(result).to include('---------- Forwarded message ---------')
+ expect(result).to end_with('
')
+ end
+
+ it 'includes header and body even when content is blank' do
+ result = html_builder.html_wrapper('')
+
+ expect(result).to start_with('')
+ expect(result).to include('---------- Forwarded message ---------')
+ expect(result).to include('
')
+ expect(result).to end_with('
')
+ 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: <sender@example.com>')
+ 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: <recipient@example.com>')
+
+ 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('HTML content
')
+ 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('Converted HTML
')
+
+ result = builder.forwarded_body_html
+
+ expect(Messages::ForwardedMessageFormatterService).to have_received(:convert_markdown_to_html).with('Text only content')
+ expect(result).to eq('Converted HTML
')
+ 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: <test@example.com>
')
+ 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
')
+ 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: <recipient@example.com>
')
+ 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