A recipe for a better user experience in emails sent between users via my Rails app.
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:

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
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
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.
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.
Clearance is now a Rails engine… BLAOW!
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.
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.
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.
Users, Sessions, Passwords, and Confirmations).All knowledge pertaining to Clearance can be found on its Github wiki, where you’ll find such articles as:
.. and much more.
Enjoy!
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.