Saturday, February 16, 2013

Easy Mandrill inbound email and webhook handling with Rails

(blogarhythm ~ Psycho Monkey - Joe Satriani)

Mandrill is the transactional email service by the same folks who do MailChimp, and I've been pretty impressed with it. For SMTP mail delivery it just works great, but where it really shines is inbound mail handling and the range of event triggers you can feed into to your application as webhooks (for example, to notify on email link clicks or bounces).

The API is very nice to use, but in a Rails application it's best to keep all the crufty details encapsulated and hidden away, right? That's what the mandrill-rails gem aims to do - make supporting Mandrill web hooks and inbound email as easy and Rails-native as possible.

I recently added some new methods to mandrill-rails to provide explicit support for inbound mail attachments (in the 0.0.3 version of the gem).

With the mandrill-rails gem installed, we simply define the routes to our webhook receiver (in this example an 'inbox' controller):
resource :inbox, :controller => 'inbox', :only => [:show,:create]
And then in the controller we provide handler implementations for any of the 9 event types we wish to consume. Here's how we might get started handling inbound email, including pulling out the attachments:
class InboxController < ApplicationController
  include Mandrill::Rails::WebHookProcessor

  # Defines our handler for the "inbound" event type. 
  # This gets called for every inbound event sent from Mandrill.  
  def handle_inbound(event_payload)
    [... do something with the event_payload here, 
         or stuff it on a background queue for later ... ]
    if attachments = event_payload.attachments.presence
      # yes, we have at least 1 attachment. Let's examine the first:
      a1 = attachments.first # => e.g. 'sample.pdf'
      a1.type # => e.g. 'application/pdf'
      # => this is the raw content provided by Mandrill, 
      #    and will be base64-encoded if not plain text
      # e.g. 'JVBERi0xLjMKJcTl8uXrp/Og0MTGCjQgMCBvY ... (etc)'
      # => this is the content decoded by Mandrill::Rails, 
      #    ready to be written as a File or whatever
      # e.g. '%PDF-1.3\n%\xC4\xE5 ... (etc)'

That's nice and easy, yes? See the Mandrill::Rails Cookbook for more tips.

If you love playing with transactional mail and haven't tried Mandrill yet, it's well worth a look!


andy jiang said...

Hey Paul,

Thanks for putting together this short tutorial on setting up parsing inbound email with Mandrill.

With your example, i'm having a hard time understanding what the route would be that I would expose to the Mandrill email configuration page.

Also, can you help shed some light on what techniques you use for testing? Localtunnel?


Paul Gallagher said...

Hi Andy,

Happy to see you got something out of it!

Regarding routes - and I'm assuming you are referring to getting Mandrill to send inbound email to your app - then if you have, say, an :inbox route in your app:

resource :inbox, :controller => 'inbox', :only => [:show,:create]

The the url you would configure in Mandrill is simply:


As for testing, I haven't gone down the path of real integration testing yet, using localtunnel or similar. It would be ideal of course - especially to protect against breaking changes that might show up in Mandrill. The async nature of Mandrill makes it a bit of a challenge though. So far I'm just using mocks with saved example responses. If you get anything better going, I'd be keen to hear about it!

Pelayo Abel said...

Here my webhook mandrill listener, perhaps someone need it:

Mandril webhook listener

Vivekananda Ponnaiyan said...

Hi this seems to have broken recently. Please let me know if you know what happening here.

Paul Gallagher said...

Hi Vivek - I just verified that it is working fine in my deployments, so if you are experiencing issues the problem is not with the basic mechanism but may be configuration, or an error you are encountering when actually processing the received mail.

If you can get more details on the error encountered, raise it as an issue on the github project page at