From b11775ebda55f00c8fc545598b6d712345e22393 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sun, 22 Mar 2026 18:01:12 +0200 Subject: [PATCH] remove premailer --- Gemfile | 2 +- Gemfile.lock | 13 +-- app/mailers/application_mailer.rb | 2 + lib/html_to_plain_text.rb | 119 ++++++++++++++++++++++++++ lib/html_to_plain_text_interceptor.rb | 65 ++++++++++++++ 5 files changed, 188 insertions(+), 13 deletions(-) create mode 100644 lib/html_to_plain_text.rb create mode 100644 lib/html_to_plain_text_interceptor.rb diff --git a/Gemfile b/Gemfile index d8503573..4e2fd78e 100644 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,7 @@ source 'https://rubygems.org' ruby '4.0.1' +gem 'addressable' gem 'arabic-letter-connector', require: false gem 'aws-sdk-s3', require: false gem 'aws-sdk-secretsmanager', require: false @@ -28,7 +29,6 @@ gem 'oj' gem 'onnxruntime', require: false gem 'pagy' gem 'pg', require: false -gem 'premailer-rails' gem 'pretender' gem 'puma', require: false gem 'rack' diff --git a/Gemfile.lock b/Gemfile.lock index a20c37db..ce504fee 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -150,8 +150,6 @@ GEM bigdecimal rexml crass (1.0.6) - css_parser (1.21.1) - addressable csv (3.3.5) csv-safe (3.3.1) csv (~> 3.0) @@ -264,7 +262,6 @@ GEM geom2d (~> 0.4, >= 0.4.1) openssl (>= 2.2.1) strscan (>= 3.1.2) - htmlentities (4.4.2) i18n (1.14.8) concurrent-ruby (~> 1.0) image_processing (1.14.0) @@ -370,14 +367,6 @@ GEM pg (1.6.3-x86_64-linux-musl) pp (0.6.3) prettyprint - premailer (1.27.0) - addressable - css_parser (>= 1.19.0) - htmlentities (>= 4.0.0) - premailer-rails (1.12.0) - actionmailer (>= 3) - net-smtp - premailer (~> 1.7, >= 1.7.9) pretender (0.6.0) actionpack (>= 7.1) prettyprint (0.2.0) @@ -607,6 +596,7 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES + addressable annotaterb arabic-letter-connector aws-sdk-s3 @@ -643,7 +633,6 @@ DEPENDENCIES onnxruntime pagy pg - premailer-rails pretender pry-rails puma diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 945994ed..c5ed556d 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -5,6 +5,8 @@ class ApplicationMailer < ActionMailer::Base layout 'mailer' register_interceptor ActionMailerConfigsInterceptor + register_interceptor HtmlToPlainTextInterceptor + register_preview_interceptor HtmlToPlainTextInterceptor register_observer ActionMailerEventsObserver diff --git a/lib/html_to_plain_text.rb b/lib/html_to_plain_text.rb new file mode 100644 index 00000000..1001f40c --- /dev/null +++ b/lib/html_to_plain_text.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +module HtmlToPlainText + module_function + + def call(html, line_length = 65) + return '' if html.nil? || html.strip.empty? + + cleaned = html.gsub(%r{.*?}m, '') + + doc = Nokogiri::HTML.fragment(cleaned) + + doc.css('script').each(&:remove) + + result = process_nodes(doc, line_length) + + result.gsub!(/\r\n?/, "\n") + result.gsub!(/[ \t]*\u00A0+[ \t]*/, ' ') + result.gsub!(/\n[ \t]+/, "\n") + result.gsub!(/[ \t]+\n/, "\n") + result.gsub!(/\n{3,}/, "\n\n") + + result = word_wrap(result, line_length) + + result.gsub!(/\(([ \n])(http[^)]+)([\n ])\)/) do + "#{"\n" if ::Regexp.last_match(1) == "\n"}( #{::Regexp.last_match(2)} )#{"\n" if ::Regexp.last_match(3) == "\n"}" + end + + result.strip + end + + def process_nodes(node, line_length) + result = +'' + + node.children.each do |child| + case child + when Nokogiri::XML::Text + result << child.text + when Nokogiri::XML::Comment + next + when Nokogiri::XML::Element + result << process_element(child, line_length) + end + end + + result + end + + def process_element(node, line_length) + case node.name + when 'br' + "\n" + when 'p', 'div' + inner = process_nodes(node, line_length) + inner.strip.empty? ? '' : "#{inner}\n\n" + when 'img' + node['alt'] || '' + when 'a' + process_link(node, line_length) + when /\Ah([1-6])\z/ + process_heading(node, ::Regexp.last_match(1).to_i, line_length) + when 'li' + inner = process_nodes(node, line_length) + "* #{inner.strip}\n" + else + process_nodes(node, line_length) + end + end + + def process_link(node, line_length) + text = process_nodes(node, line_length).strip + return '' if text.empty? + + href = node['href'] + href = href.sub(/\Amailto:/i, '') if href + + if href.nil? || text.casecmp(href.strip) == 0 + text + else + "#{text} ( #{href.strip} )" + end + end + + def process_heading(node, level, line_length) + text = +'' + node.children.each do |child| + text << if child.name == 'br' + "\n" + else + child.text + end + end + text.strip! + + hlength = text.each_line.map { |l| l.strip.length }.max || 0 + hlength = line_length if hlength > line_length + + decorated = case level + when 1 + "#{'*' * hlength}\n#{text}\n#{'*' * hlength}" + when 2 + "#{'-' * hlength}\n#{text}\n#{'-' * hlength}" + else + "#{text}\n#{'-' * hlength}" + end + + "\n\n#{decorated}\n\n" + end + + def word_wrap(txt, line_length) + txt.split("\n").map do |line| + if line.length > line_length + line.gsub(/(.{1,#{line_length}})(\s+|$)/, "\\1\n").strip + else + line + end + end.join("\n") + end +end diff --git a/lib/html_to_plain_text_interceptor.rb b/lib/html_to_plain_text_interceptor.rb new file mode 100644 index 00000000..9041d8ad --- /dev/null +++ b/lib/html_to_plain_text_interceptor.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module HtmlToPlainTextInterceptor + module_function + + def delivering_email(message) + process(message) + end + + def previewing_email(message) + process(message) + end + + def process(message) + return message unless html_part(message) + return message if message.text_part + + add_text_part(message) + + message + end + + def add_text_part(message) + html = html_part(message).decoded + text = HtmlToPlainText.call(html) + + text_part = Mail::Part.new do + content_type 'text/plain; charset=UTF-8' + body text + end + + if pure_html_message?(message) + message.body = nil + message.content_type = 'multipart/alternative' + message.add_part(text_part) + message.add_part(Mail::Part.new do + content_type 'text/html; charset=UTF-8' + body html + end) + else + alternative = Mail::Part.new(content_type: 'multipart/alternative') + alternative.add_part(text_part) + alternative.add_part(message.html_part) + replace_part(message.parts, message.html_part, alternative) + end + end + + def pure_html_message?(message) + message.content_type.to_s.include?('text/html') + end + + def html_part(message) + pure_html_message?(message) ? message : message.html_part + end + + def replace_part(parts, old_part, new_part) + if (index = parts.index(old_part)) + parts[index] = new_part + else + parts.each do |part| + replace_part(part.parts, old_part, new_part) if part.respond_to?(:parts) + end + end + end +end