In Progress
Unit 1, Lesson 1
In Progress

Email Template

Video transcript & code

We cover a lot of slightly obscure features of Ruby and its libraries on this show. Some of these capabilities may seem like they only have very niche use cases. Today, I want to show an example of how, by putting together a bunch of little bits of Ruby knowledge, we can solve a pragmatic application programming problem.

Let's say we are working on the latest and greatest social networking site. We're at the point where we want to start inviting beta users into the site, and so we need to send some emails out. We decide to separate emailing into its own gateway class. In this class we have a method called #send_invitation_email. It takes a recipient address and some other data as parameters.

class EmailGateway
  def send_invitation_email(to:, **data)
    subject = "Invitation to join Squawker"
    body = "Hi, #{data[:name]}!\n" +
      "You're invited to join Squawker! To get started, go there: \n" +
      data[:signup_url]
    send_mail(to: to, subject: subject, body: body)
  end

  private

  def send_mail(to:, subject:, body:)
    puts "*** Sending mail to: #{to}"
    puts "Subject: #{subject}"
    puts
    puts body
  end

end

To invite a new member, we can instantiate a gateway object and send it the #send_invitation_email message, along with all the requisite extra data used to fill in the email template.

require "./email_gateway"

gw = EmailGateway.new

gw.send_invitation_email(to: "marvin@example.org",
                         name: "Marvin the Robot",
                         signup_url: "http://example.com/signup")

# >> *** Sending mail to: marvin@example.org
# >> Subject: Invitation to join Squawker
# >>
# >> Hi, Marvin the Robot!
# >> You're invited to join Squawker! To get started, go there:
# >> http://example.com/signup

With beta invitations working, we realize we now need to send out welcome messages as well. In adding another type of mailing, we realize we don't want to duplicate the ugly string-concatenation approach of our first method. We also feel like we should probably generalize our mail-sending system a bit.

But up till now this has been a very lightweight app. It's built on Sinatra rather than Rails, and we don't have an industrial-strength mailer framework all set up and ready to go.

As we think through the requirements of a generalized mail-sending subsystem, based on what we know about existing frameworks such as ActionMailer, we start to get discouraged. If we're going to be writing many more email bodies, we don't want to embed them as strings… we'll probably want to write them in their own template files… so we'll have to write some code to find the right template file for a given mailing… and then we'll need a way to personalize each template, so we'll need to hook in a templating system like ERB or Mustache

Hang on, hang on. Let's take a step back for a moment. We're still only talking about two different email templates. And we're not worried about sending both HTML and text versions of the emails or anything else fancy like that. So let's see if we can use what we know about Ruby and come up with a pragmatic solution. Something that makes it a little more pleasant to write new types of mailing, without overthinking things too much.

Here's what we come up with. First off, we bring in our unindent method from episode #249. Then we write a new method, #send_welcome_email. Like the other mailing method, it requires a to keyword, and can accept an arbitrary set of other data attributes.

We start it similarly to the last method, by defining a subject. Then we define a body. This is where we diverge from the other mailer method. Instead of a quoted string, we start a heredoc, filtered by our unindent method to get rid of unwanted leading spaces. Then we apply the format operator to the unindented heredoc. Let's complete the heredoc before we talk about the format operator and its other argument.

Inside the heredoc, we define the text of the message. Since the whole doc will be unindented, we are free to indent the body consistent with our usual coding style. Anywhere in the body where we need a personalized value, we enclose its name in curly braces preceded by a percent sign. The personalizable elements in this message are the recipient's name, and a URL for the site homepage.

Once we have the subject and body defined, we use the private send_mail method just as in the other mailer method.

Now that we are done defining the method body, let's talk a bit more about how the personalization will work. As you may recall from episode #194, Ruby strings have a format operator, the percent sign. This operator performs printf-style expansions in the string, using the supplied arguments. When we pass a hash instead of an array to the format operator, Ruby treats the hash as a source of values for named expansion keys. Everywhere the text contains a curly-braced word preceded by a percent sign, the format process looks up that word in the given hash of data and replaces the whole specifier with the value it finds there.

The upshot is that string formats give us a quick and dirty way to implement templated text without resorting to templating engines.

class EmailGateway
  def unindent(s)
    s.gsub(/^#{s.scan(/^[ \t]+(?=\S)/).min}/, "")
  end

  def send_invitation_email(to:, **data)
    subject = "Invitation to join Squawker"
    body = "Hi, #{data[:name]}!\n" +
      "You're invited to join Squawker! To get started, go there: \n" +
      data[:signup_url]
    send_mail(to: to, subject: subject, body: body)
  end

  def send_welcome_email(to:, **data)
    subject = "Welcome to Squawker!"
    body    = unindent(<<-'EOF') % data
      Hi %{name}! Welcome to Squawker!

      Your account is all set up and ready to go! Access it anytime
      at: %{home_url}
     EOF

    send_mail(to: to, subject: subject, body: body)
  end

  private

  def send_mail(to:, subject:, body:)
    puts "*** Sending mail to: #{to}"
    puts "Subject: #{subject}"
    puts
    puts body
  end

end

Let's give our new mailer method a try. We supply a recipient email address, a name, and a home URL. When we look at the output, we can see that everything has been filled in where it should be.

gw = EmailGateway.new
gw.send_welcome_email(to: "crow@example.org",
                      name: "Crow T. Robot",
                      home_url: "http://example.com")

# >> *** Sending mail to: crow@example.org
# >> Subject: Welcome to Squawker!
# >>
# >> Hi Crow T. Robot! Welcome to Squawker!
# >>
# >> Your account is all set up and ready to go! Access it anytime
# >> at: http://example.com

As it turns out, we haven't really generalized our mail gateway at all. What we've done is come up with a new convention for writing mailer methods that lets us avoid ugly and typo-prone string quoting and concatenation. It also allows us to insert personalized values into the templates by name, in a way that is decoupled from where the values came from.

Once we write a few more mailer methods, we might feel the urge to take another pass and factor out some of the commonalities between our mailer methods. If we write even more mailing types, we may eventually look for a way to push the mail templates out into their own files rather than expanding this file indefinitely. But for right now, we've found a way forward that starts us on an incremental path to greater generality, without becoming overwhelmed by all the requirements of an elaborate mailing subsystem. Happy hacking!

Responses