giant robots smashing into other giant robots

We are thoughtbot. We make web & mobile apps.

Tagged:

Comments (View)

Recipe: Delivering email on behalf of users

A recipe for a better user experience in emails sent between users via my Rails app.

Why?

When I receive an email from an automated system like a Rails app, it is disorienting if the sender shows up in my email program as “admin” or “donotreply”.

What I want is something like this:

Ingredients

Install email-spec

I’m a fan of Ben Mabey’s email-spec gem, so I’ll install that:

group :test do
  gem 'email_spec'
end

I create a features/support/email.rb file:

require 'email_spec' # add this line if you use spork
require 'email_spec/cucumber'

Then generate some step definitions into features/step_definitions/email_steps.rb:

rails generate email_spec:steps

Feature

Now I’ll write my user story:

Scenario: Guitarist shares song with guitarist
  Given the following user exists:
    | name         | email            |
    | Eric Clapton | eric@example.com |
  And I sign in as "eric@example.com/password"
  And I am on the share page for "Layla"
  When I fill in "Share with" with "jimi@example.com"
  And I press "Share Song"
  And "jimi@example.com" opens the email
  Then he should see "Eric Clapton <admin@goodsongs.com>" in the email "From" header
  And he should see "eric@example.com" in the email "Reply-To" header

The “From” and “Reply-To” headers

I think the “From” and “Reply-To” headers can provide a better user experience.

I don’t set the author’s email as the “From” header because I hear it’s bad spam practice to send email on behalf of users in that way. ISPs use the From header (among other things) to determine if the originator is sending spam.

Making the feature pass

Ease my worried mind:

class Mailer < ActionMailer::Base
  def share_song(song, friend)
    mail :to       => friend.email,
         :from     => %{"#{song.artist.name}" <admin@goodsongs.com>},
         :reply_to => song.artist.email,
         :subject  => "Good song"
  end
end

I’ve used this format so the sender’s name shows up in the receiver’s email program:

"Name" <email@example.com>

In this case, I want Jimi to be able to reply directly to Eric, so I’ve set the “Reply-To” header to be the sender’s address. I’ve explicitly not put the sender’s name in the “Reply-To” header because that doesn’t work.

In other cases, I want the receiver to reply to the email and have that sent through the Rails app, but that’s a story for another day.

Tagged:

Comments (View)

Clearance is a Rails engine

Clearance is now a Rails engine… BLAOW!

Why

Clearance has served us well for many months. Our only complaints were shared by others:

With the re-institution of Rails engines in Rails 2.3, we decided to convert Clearance to engine. The process was relatively painless, the code is far cleaner, and we think we were able to scratch all our itches.

Philosophy

We highly recommend that you use the Cucumber features that come with Clearance to test the integration of the engine with your app:

script/generate clearance_features

You no longer run Clearance’s generated Shoulda & Factory Girl tests within your test suite. That code is unit tested internally. Use the Cucumber features to test integration. If and when you override functionality, write your own unit tests.

Read Mixing Cucumber with Test::Unit/Shoulda if you’re getting started with Cucumber and not using RSpec.

Overriding

We haven’t and probably won’t ever move Clearance beyond email and password authentication, despite frequent requests. We’re focused on “clean code that works” for the baseline authentication we’ve written over and over again for clients.

A huge part of “clean code that works” means that overriding Clearance needs to be painless. The change to an engine helps achieve that goal.

class UsersController < Clearance::UsersController
  def edit
    ...
  end
end

ActionController::Routing::Routes.draw do |map|
  map.resources :users
end

That’s it.

  • Write your tests for whatever action you want to add or override.
  • Subclass one of Clearance’s controllers (Users, Sessions, Passwords, and Confirmations).
  • Update your routes (by default, the routes will point to the namespaced Clearance controllers).

The Royal Library of Alexandria

All knowledge pertaining to Clearance can be found on its Github wiki, where you’ll find such articles as:

.. and much more.

Enjoy!

Tagged:

Comments (View)

I accidentally the whole SMTP exception

You have a slick, exclusive, invite-only Web app for sharing Tor URLs, with an Android client and specialty hardware. You use validates_email_format_of in the Invitation model, but still something slips through and your Hoptoad errors pile up, showing your user the beautifully-designed 500 page instead of an error explanation.

There are two types of exceptions that ActionMailer will raise when you attempt to deliver an email: user input problems and server problems.

User input problems are those such as incorrect or invalid email addresses; the exceptions raised are Net::SMTPFatalError and Net::SMTPSyntaxError. These are issues that the user can fix and as such the error message should indicate that everything’s fine, nothing is ruined.

Server problems could be anything from a non-existent server to an authentication issue; the exceptions raised are: TimeoutError, IOError, Net::SMTPUnknownError, Net::SMTPServerBusy, and Net::SMTPAuthenticationError. These issues are outside the power of the user and should indicate that we screwed up.

So in config/initializers/errors.rb:

SMTP_SERVER_ERRORS = [TimeoutError,
                          IOError,
                          Net::SMTPUnknownError,
                          Net::SMTPServerBusy,
                          Net::SMTPAuthenticationError]

SMTP_CLIENT_ERRORS = [Net::SMTPFatalError, Net::SMTPSyntaxError]

SMTP_ERRORS = SMTP_SERVER_ERRORS.concat(SMTP_CLIENT_ERRORS)

SMTP_CLIENT_ERROR_FLASH = 'The email address supplied is invalid.  Please check for spelling mistakes.'
SMTP_SERVER_ERROR_FLASH = 'We encountered an internal issue while attempting to deliver this email.  Please try again in a few minutes.'

We can test it with invitations_controller_test.rb:

class InvitationsController; def rescue_action(e) raise e end; end
class InvitationsControllerTest < Test::Unit::TestCase
  SMTP_CLIENT_ERRORS.each do |exn|
    should "handle #{exn}" do
      InvitationsMailer.expects(:deliver_invitation).raises(exn)
      post :create, :invitation => {:email => 'invalid email'}
      assert_match /#{SMTP_CLIENT_ERROR_FLASH}/i, @response.flash[:warning]
      assert_template 'new'
    end
  end

  SMTP_SERVER_ERRORS.each do |exn|
    should "handle #{exn}" do
      InvitationsMailer.expects(:deliver_invitation).raises(exn)
      post :create, :invitation => {:email => 'invalid email'}
      assert_match /#{SMTP_SERVER_ERROR_FLASH}/i, @response.flash[:warning]
      assert_template 'new'
    end
  end
end

And in invitations_controller.rb:

class InvitationsController < ApplicationController
  def create
    @invitation.new(params[:invitation])
    if @invitation.save
      redirect_to root_url
    else
      render :action => 'new'
    end
  rescue *SMTP_CLIENT_ERRORS
    flash[:warning] = SMTP_CLIENT_ERROR_FLASH
    render :action => 'new'
  rescue *SMTP_SERVER_ERRORS => error
    notify_hoptoad error
    flash[:warning] = SMTP_SERVER_ERROR_FLASH
    render :action => 'new'
  end
end

If you use Suspenders you’ll be pleased to find that we’ve included config/initializers/errors.rb for you pre-populated with both SMTP and HTTP exceptions.