From 99d7bd9e7c3abc68dca0b57259126a6c31aeffda Mon Sep 17 00:00:00 2001 From: iamsivin Date: Fri, 2 May 2025 14:14:15 +0530 Subject: [PATCH] chore: Code clean up moves to service classes --- .../messages/forwarded_message_builder.rb | 314 ------------------ app/builders/messages/message_builder.rb | 2 +- .../forwarded_message_builder_service.rb | 114 +++++++ ...rwarded_message_content_builder_service.rb | 60 ++++ .../forwarded_message_data_handler_service.rb | 92 +++++ .../forwarded_message_formatter_service.rb | 58 ++++ .../forwarded_message_html_builder_service.rb | 68 ++++ ...forwarded_message_builder_service_spec.rb} | 166 +-------- ...ed_message_content_builder_service_spec.rb | 144 ++++++++ ...arded_message_data_handler_service_spec.rb | 180 ++++++++++ ...orwarded_message_formatter_service_spec.rb | 158 +++++++++ ...arded_message_html_builder_service_spec.rb | 156 +++++++++ 12 files changed, 1032 insertions(+), 480 deletions(-) delete mode 100644 app/builders/messages/forwarded_message_builder.rb create mode 100644 app/services/messages/forwarded_message_builder_service.rb create mode 100644 app/services/messages/forwarded_message_content_builder_service.rb create mode 100644 app/services/messages/forwarded_message_data_handler_service.rb create mode 100644 app/services/messages/forwarded_message_formatter_service.rb create mode 100644 app/services/messages/forwarded_message_html_builder_service.rb rename spec/{builders/messages/forwarded_message_builder_spec.rb => services/messages/forwarded_message_builder_service_spec.rb} (55%) create mode 100644 spec/services/messages/forwarded_message_content_builder_service_spec.rb create mode 100644 spec/services/messages/forwarded_message_data_handler_service_spec.rb create mode 100644 spec/services/messages/forwarded_message_formatter_service_spec.rb create mode 100644 spec/services/messages/forwarded_message_html_builder_service_spec.rb 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_header_html}#{forwarded_body_html}" - end - end - - def forwarded_header_html - from_value = formatted_info[:from].to_s - email = Messages::ForwardedMessageFormatter.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 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