Netskin Logo

Auto-generate Plain-Text Emails

#ruby
#rails
by Tom Rothe on 16.07.2023

Basically every web application sends emails these days, and since we do not live in the 1980s anymore, users expect them to be colorful and well-designed. So we need to provide two templates in Rails: a plain-text version and a HTML version (more details below). Here is a quick example using the on-board ActionMailer:

# app/views/my_mailer/welcome_email.text.erb
Welcome to the party!

--- --- --- --- --- --- --- --- --- --- --- ---

Here some data:

Location | Mood
Berlin | 😀
L.A. | 🍩
# app/views/my_mailer/welcome_email.html.erb
<h1>Welcome to the party!</h1>

<hr>

Here some data:

<table>
<tr><td>Location</td><td>Mood</td></tr>
<tr><td>Berlin</td><td>😀</td></tr>
<tr><td>Stockholm</td><td>😎</td></tr>
</table>

Considering that most email clients support HTML and most people prefer it, it seems excessive to provide both versions. However, only sending HTML does not seem right and might even lead to delivery problems. So, we try to derive the plain-text version from our HTML template. Let’s look at the mailer:

class MyMailer < ApplicationMailer
  def welcome_email
    mail(subject: "Welcome") do |format|
      format.html
      format.text { render_text_for(__method__) }
    end
  end
end

We leave our HTML version in place but remove the text version. In the welcome_email method, we let Rails run its natural rendering for the HTML version. However, for the text version, we call the method render_text_for and pass in the current method’s name. This is necessary, so the converter can infer the correct template name later. We implement the method in the base class:

class ApplicationMailer < ActionMailer::Base
  using Refinements::Mailings

  protected

  def render_text_for(template_name)
    text = render_to_string(template_name, formats: :html).html_to_plain
    render(plain: text)
  end
end

So we just render the HTML version and call html_to_plain on it. This is the actual converter, which we define in a refinement:

module Refinements::Mailings
  refine String do
    def html_to_plain
      preprocessed = self
        .gsub("<hr>", "\n--- --- --- --- --- --- --- --- --- --- --- ---\n")
        .gsub(/<br\\?>/, " ") # line breaks
        .gsub(/<li[^>]*>/, "- ") # lists
        .gsub(/<\/t[hd]>\s*<t[hd][^>]*>/, " | ") # table cells (tr and table tags are removed later)

      return ActionController::Base.helpers.strip_tags(preprocessed)
        .split("\n").map(&:strip).join("\n") # fix indentation
        .gsub("\n\n\n", "\n") # remove extensive new lines
        .strip
    end
  end
end

The converter is pretty basic, but we have tried it on hundreds of emails, and it works pretty well. Sometimes, we adjust the newlines in the HTML template a little, and then it is perfect. However, please feel free to customize the replacements to your needs.

This approach is pretty basic, but it works well in most cases. It removes duplication and the risk of forgetting to adjust one of the templates. And it saves a little work. If the templates become overly complex, we can still fall back on providing both versions explicitly.

Happy coding!

Further Reading

❮ Mini-Tip: Refactor Docker Compose configs
Exploring Ruby on Rails Engines ❯
Netskin Logo