One of the most frequent questions we are asked at CloudMailin is how can you develop your application and receives email offline, without opening ports, in a Rails app.
There are options that spring to mind. The first is to write a script that delivers a local email to your app simulating the response that you would get from an email server. The second solution is far simpler. You can perform test driven development and it can be really easy get things up and running in development and even easier to move to production.
Here we are going to create a really simple example to take the subject of an email and create a model called movie poster from that subject. In a later post we will show how we can also use the attachments to create the images for the poster.
The First Step, a Controller to Receive the Email
In a previous article I highlighted a number of options for Receiving Email in Rails 3. Using some of the approaches you don’t even need to use a controller but in that case where email is delivered to your app via an HTTP POST the controller is needed. The controller doesn’t have to be complicated though. All we really want to do is pass a mail object to the receive method of our mailer. So supposing we have a mailer called MovieMailer we can do the following:
class IncomingMailsController < ApplicationController skip_before_filter :verify_authenticity_token def create # create a Mail object from the raw message movie_poster = MovieMailer.receiver(Mail.new(params[:message])) # if the movie poster was saved then send a status of 201 else give a 422 with the errors if !movie_poster.new_record? render :text => "Success", :status => 201, :content_type => Mime::TEXT.to_s else render :text => movie_poster.errors.full_messages.join(', '), :status => 422, :content_type => Mime::TEXT.to_s end end end
The example above assumes that you are using at least Rails 3.0 but it can easily be converted to work with older versions of Rails and you could use the TMail gem rather than Mail if you need to.
Now we have this controller we can create a functional test to check this in the same way we would any other controller in our app (note I’m doing this the wrong way round for illustration you should really write your test first!). If we mock the response from the Mailer for now we can use something like this with Shoulda but you could use RSpec or any other testing framework just as easily:
class IncomingMailsControllerTest < ActionController::TestCase context "An IncomingMailsController" do context "on POST to :create with an invalid item" do setup do MoviePoster.any_instance.expects(:valid?).returns(false) post :create, :message => "From: #{@user.email}\r\nSubject: New Poster\r\n\r\nContent" end should_respond_with 422 should "not create the item" do assert assigns(:item).new_record? end end context "on POST to :create with a valid item" do setup do MoviePoster.any_instance.expects(:valid?).returns(true) post :create, :message => "From: #{@user.email}\r\nSubject: New Task\r\n\r\nContent" end should_respond_with 201 should "set the items title to be the message subject" do assert_equal "New Task", assigns(:item).title end end end end
I have also created a really simple email message to pass as the message param here as an example with the header and body separated by two carriage return new lines but this isn’t really necessary with the use of mocha.
From: example@example.com Subject: Subject content
So on to the Mailer
Now that we have our controller sorted and fully tested we can create our Mailer, which will automatically create a functional test. It’s then just a case of adding the tests to test the receive method. The first step is to create a sample email that we can use in each of our tests. I am using the Mail gem again to do this.
context "A MoveMailer" do setup do @message = Mail.new do from "test@example.com" to "test@example.com" subject "New Movie" end end
Then we can add our tests making use of the example mail we added and changing any of the parts we want to.
should "create a new movie poster with the email subject" do @message.subject = "new subject" assert_difference("Movie.count", 1) do movie_poster = MovieMailer.receive(message) assert movie_poster.persisted?, movie_poster.errors.full_messages assert_equal "new subject", movie_poster.subject end end
In this case we’re just going to add one test and we can now create our simple movie mailer to take the subject from the email and store it as a poster.
class MovieMailer < ActionMailer::Base # Called whenever a message is received on the movies controller or through the rails runner def receive(message) # For now just take the first attachment and assume there is only one attachment = message.attachments.first # Create the movie itself Movie.create do |movie| movie.title = message.subject end end end
Now when your app goes into production you just deliver the message to the controller using the HTTP Post approaches, directly to the mailer using any of the other approaches discussed in the previous article.
Although the example is a little contrived you can hopefully See how you can benefit from testing at the development machine to ensure you system works perfectly once you put your code into production. You also have the added benefit of not having to keep checking things whenever you change your code. Just re-run your tests, same as you would with any other part of your app!