Skip to content

Latest commit

 

History

History
551 lines (374 loc) · 12.3 KB

emailkata.md

File metadata and controls

551 lines (374 loc) · 12.3 KB

An Email Code Kata for Rails

50,000 foot view

What we need:

  • A Mailer (which is like a model)
  • Email template (the view)
  • An implementation, how the emails get sent.

Some background

Sending emails from Rails isn't difficult, just takes a little practice to figure out and get working.

Here is that practice, develped as a code kata.

  • If possible, have a localhost email server running
  • Consider using a temporary gemset
  • Get a personal Sendgrid account (testing development)
  • Set up Rails locally
  • Create a heroku application, cedar stack.
  • Add Sendgrid to the heroku application (testing production)
  • Set up Rails email framework
  • Configure SMTP for Sendgrid
  • Drive out desired emails via test-first

Development operations first

Setting up

First, we need to set the stage:

$ rails new emailkata
$ rm public/index.html

Build out the Gemfile

Now, add haml and rspec to the Gemfile:

@@@ ruby
source 'https://rubygems.org'

gem 'rails', '3.2.1'
gem 'haml-rails'
gem 'jquery-rails'

group :assets do
  gem 'sass-rails',   '~> 3.2.3'
  gem 'coffee-rails', '~> 3.2.1'
  gem 'uglifier', '>= 1.0.3'
end

group :development, :test do
  gem 'sqlite3'
  gem 'rspec-rails'
  gem 'capybara'
  gem 'letter_opener'
  gem 'email_spec'
end

We're moving the sqlite3 gem into :test and :development groups to keep heroku cedar stack from sqawking at us.

Don't forget to run the following:

@@@ sh
$ bundle install
$ rails generate rspec:install

Do the git thing...

Let's go ahead and do the obvious next:

@@@ sh
$ git init
$ git add .
$ git commit -m"first commit"

We're going to need this next, when we fire up heroku.

Heroku because we're going there anyway, probably

We want a new application on the celadon cedar stack.

Do it like this:

$ heroku apps:create --stack cedar

New we can push to heroku:

$ git push heroku master

Building application email infrastructure

What we're driving for

An application email infrastructure consists of the following:

  • Web pages (views) which have links or forms for sending emails
  • Web pages (views) which are redirect targets after email is sent
  • Email templates (mailers), what the user gets sent.
  • All the models, controllers, etc.

Test driving the email setup

We really want to do this test-first, but rails generate saves a lot of time and hassle, so let's generate what we need to get started, then go from there.

@@@ sh
$ rails generate mailer AlertMailer pop
      create  app/mailers/alert_mailer.rb
      invoke  haml
      create    app/views/alert_mailer
      create    app/views/alert_mailer/pop.text.haml
      invoke  rspec
      create    spec/mailers/alert_mailer_spec.rb
      create    spec/fixtures/alert_mailer/pop

git add .; git commit -m"generated mailer files"

What is an "ActionMailer", anyway?

From the Rails ActionMailer API description, we have this explanation (paraphrased):

Emails are defined by creating methods within the model which are then used:

  1. to set variables to be used in the mail template,
  2. to change options on the mail, or
  3. to add attachments.

Not too difficult, but as usual, there are a lot of moving parts.

Starting into the email spec

Here's what we get for the alert_mailer_spec.rb:

@@@ ruby 
require "spec_helper"

describe AlertMailer do
  describe "pop" do
    let(:mail) { AlertMailer.pop }

    it "renders the headers" do
      mail.subject.should eq("Pop")
      mail.to.should eq(["[email protected]"])
      mail.from.should eq(["[email protected]"])
    end

    it "renders the body" do
      mail.body.encoded.should match("Hi")
    end
  end
end

Those parameters aren't useful. We want this emailer to actually work. I'm going to use my real email address for testing it out. You should use your own.

Controlling delivery

@@@ sh
$ rails generate controller AlertPages sender thankyou
      create  app/controllers/alert_pages_controller.rb
       route  get "alert_pages/thankyou"
       route  get "alert_pages/sender"
      invoke  haml
      create    app/views/alert_pages
      create    app/views/alert_pages/sender.html.haml
      create    app/views/alert_pages/thankyou.html.haml
      invoke  rspec
      create    spec/controllers/alert_pages_controller_spec.rb
      create    spec/views/alert_pages
      create    spec/views/alert_pages/sender.html.haml_spec.rb
      create    spec/views/alert_pages/thankyou.html.haml_spec.rb
      invoke  helper
      create    app/helpers/alert_pages_helper.rb
      invoke    rspec
      create      spec/helpers/alert_pages_helper_spec.rb
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/alert_pages.js.coffee
      invoke    scss
      create      app/assets/stylesheets/alert_pages.css.scss

Bonus: why not send instead of sender?

Note we invoked generate with sender argument:

$ rails generate controller AlertPages sender thankyou

Why not send which is what we're intended to do?

Let's take a look at the routing first

And clean it up while we're at it.

This is what mine looks like now:

@@@ ruby
Testemail::Application.routes.draw do
  get "alert_pages/sender"
  get "alert_pages/thankyou"
end

Let's not bother with setting up a root route. It's not necessary.

And rake routes just for fun

@@@ sh
  alert_pages_sender GET /alert_pages/sender(.:format)   alert_pages#sender
alert_pages_thankyou GET /alert_pages/thankyou(.:format) alert_pages#thankyou

We're going to use these later.

Web pages supporting email

  • Sender page
  • Thank you page
  • Helpers

Sender page

This is the page where the user clicks to get an email sent.

We're going to test for the link to send the email.

Add the following to spec/views/alert_pages/sender.html.haml_spec.rb:

@@@ ruby

require 'spec_helper'
describe "alert_pages/sender" do
  it "has a send link on the page for sending email" do    
    render    
    rendered.should have_selector('a')
  end
end

Make it pass

@@@ haml
%h1 AlertPages#sender
%p Find me in app/views/alert_pages/sender.html.haml
%p
  %a.alert-email{ :href => "#" } Send email

Thank you page

Add to cat spec/views/alert_pages/thankyou.html.haml_spec.rb:

@@@ ruby 
require 'spec_helper'

describe "alert_pages/thankyou.html.haml" do
  it "thanks the user for sending him or herself email" do
    render
    rendered.should =~ /thank you/i
  end
end

rspec spec is red

Go green

Add the following to app/views/alert_pages/thankyou.html.haml:

@@@ haml
%h1 AlertPages#thankyou
%p Find me in app/views/alert_pages/thankyou.html.haml
%p Thank you for sending yourself email
%a { :href => alert_pages_sender_path }

Pages helper

@@@ ruby
spec/helpers/alert_pages_helper_spec.rb 
require 'spec_helper'

describe AlertPagesHelper do
  it "provides a small footer element" do
    helper.footer_small.should =~ /small/
  end
end

And the implementation

@@@ ruby
module AlertPagesHelper
  def footer_small
    'This is the small footer'  end
end

Ok, that's kind of lame, I agree. But...

But but but...

The point is testing first.

It's a habit, and it's not an easy habit to create.

If it were easy, everyone would do it.

Do we even need a helper?

Maybe, maybe not.

Once you start thinking about split testing user behavior, you need to think about how your code will support split testing.

At that point, helpers start to make a lot more sense.

Controller specs

Getting some help with specs

Using the handy email_spec gem, put the following into spec/spec_helper.rb:

@@@ ruby
require "email_spec"

RSpec.configure do |config|
  config.include(EmailSpec::Helpers)
  config.include(EmailSpec::Matchers)
end

Getting down and dirty with email specs...

Now we add in some of the handy spec with matchers from email_spec gem.

Add more to spec/mailers/alert_mailer_spec.rb:

@@@ ruby
require "spec_helper"

describe AlertMailer do
  describe "pop" do
    # you already have this...
  end

  # from https://github.com/bmabey/email-spec
  before(:all) do
    @email = AlertMailer.pop
  end

  it "should be set to be delivered to the email passed in" do
    @email.should deliver_to("[email protected]")
  end

  it "should contain the user's message in the mail body" do
    @email.should have_body_text(/Hi/)
  end

  it "should have the correct subject" do
    @email.should have_subject(/Pop/)
  end
end

Sending email

You have several options:

  1. Run your own server
  2. Leech from someone else's server
  3. Take a chance with Google gmail smtp
  4. Pay for smtp services such as Sendgrid

Key point

''Ensure that your remote environment variables are set.''

This is a huge pain and will result in hours of entertainment attempting to get all the pieces of the puzzle fit together properly.

Email text view (with haml)

@@@ haml
%p
  Your StormSavvy Alert

%p 
  #{@greeting}, find me in app/views/app/views/alert_mailer/pop.text.haml

Going live with email

Up until now, we've been speccing out without having to actually run an email server. We've just had Rails doing it's Rails thing, and that's a good thing.

But now we make these emails actually get to where we want them.

  1. localhost with postfix
  2. personal Sendgrid account for development
  3. Heroku managed Sendgrid account for production

Here's the command:

heroku addons:add sendgrid:starter

It should be possible to use the heroku managed Sendgrid locally on development. We'll check that out later using the heroku configuration command heroku config. Might even work for testing.

Configure application email

Using the Sendgrid guidelines, emplace the following code into config/environment.rb:

@@@ ruby
ActionMailer::Base.smtp_settings = {
  :user_name => ENV['SENDGRID_USERNAME'],
  :password => ENV['SENDGRID_PASSWORD'],
  :domain => "heroku.com",
  :address => "smtp.sendgrid.net",
  :port => 587,
  :authentication => :plain,
  :enable_starttls_auto => true
}

If you use the environment variables SENDGRID_USERNAME and SENDGRID_PASSWORD, you will be able to leverage Heroku's automatically set environment variables. This is convenient for a number of reasons, mainly because it's convenient.

Checklist

In approximate order of implementation:

  1. App a spec for the mailer template file.
  2. Add the email template to /app/mailers.

For simple testing:

  1. Add a view template spec checking for a link to an action which fires an email.
  2. Add the view.
  3. Add a controller spec on that action
  4. Add the controller action.

Troubleshooting

There are an countable infinity of ways to configure email systems.

  • If you added Sendgrid via Heroku, ensure your environment variables are correct.

  • Precompiling assets may be necessary.

  • $ heroku logs is your new best friend.

Helpful links

Some good RSpec links

Postfix

  618  postconf -n
  619  sudo launchctl load org.postfix.master.plist 
  620  sudo port install sendmail
  621  sudo port install mail
  622  sudo port install postfix
  623  sudo port uninstall postfix
  627  tail -f /var/log/mail.log
  628  date | mail -s test [email protected]
  629  tail -f /var/log/mail.log
  635  less /etc/postfix/main.cf
  636  sudo postfix stop
  637  sudo postfix start
  638  date | mail -s test [email protected]
  639  telnet localhost 25
  644  sudo less /System/Library/LaunchDaemons/org.postfix.master.plist
  645  sudo /bin/launchctl unload -w /System/Library/LaunchDaemons/org.postfix.master.plist
  646  tail -f /var/log/mail.log
  647  sudo port install postfix
  648  sudo port unload postfix
  649  tail -f /var/log/mail.log
  651  sudo /bin/launchctl load -w /System/Library/LaunchDaemons/org.postfix.master.plist
  652  tail -f /var/log/mail.log
  653  date | mail -s test [email protected]