GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

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 = [
  IOError,
  Net::SMTPAuthenticationError
  Net::SMTPServerBusy,
  Net::SMTPUnknownError,
  TimeoutError,
]

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.