DEV Community

Cover image for Integrate and Troubleshoot Inbound Emails with Action Mailbox in Rails
John Beatty for AppSignal

Posted on • Originally published at blog.appsignal.com

Integrate and Troubleshoot Inbound Emails with Action Mailbox in Rails

If you’ve ever looked at the Request for Comments (RFCs) around sending and receiving emails, you’ll see the technical complications involved when hitting send in your email inbox.

Thankfully, many existing tools provide the Simple Mail Transfer Protocol (SMTP) service for us developers — from a Postfix server you manage to a fully scalable sending service such as SendGrid, Amazon SES, or Postmark. However, moving between providers for deliverability or pricing reasons means rewriting or refactoring our apps to meet the peculiarities of each service.

Rails helps us out here by providing Action Mailbox. In this post, we'll dive into how you can use Action Mailbox to integrate and troubleshoot inbound emails.

But first, let's quickly define what Action Mailbox is.

What Is Action Mailbox for Rails?

Action Mailbox uses conceptual compression for receiving email in Ruby on Rails. Conceptual compression means it encapsulates all the small differences between every SMTP service and writes your inbound processing code just once. You can even write a provider for a new service.

The key concept of ActionMailbox is routing based on email recipients. By setting up an inbound email provider, mail going to a domain will be routed into your app. You can look at the recipient address to determine how each mail message should be processed.

If you go through the rails conductor action to send a test email, as I have here:

Rails Conductor Screenshot

Then your inbound email will have recipients from the To, CC, BCC, and X-Original-To fields.

> inbound_email = ActionMailbox::InboundEmail.last
> inbound_email.mail.recipients
["to@john.onrails.blog", "cc@john.onrails.blog", "bcc@john.onrails.blog", "original@john.onrails.blog"]
Enter fullscreen mode Exit fullscreen mode

Each address is tested to determine where it will be routed, but the mail message is only routed once.

One key aspect of development is actually testing email from your system. Rails has a set of development pages under the routes /rails/conductor/ that allow you to input emails locally into your development setup.

You can enter the email manually, like I did in the above example, or you can upload an email complete with all the headers.

A great way to get a complete email (with headers, a message body, and attachments) is to use an email client like Thunderbird. Save the individual email in a .eml, open the file with a text editor, and copy the complete contents into the conductor page.

Now you can test more complicated email processing.

Posting and Commenting Demo for a Rails App

Let’s put together a small demo to show how this all works. I’m a great admirer of 37Signals, and I especially like their blogging with Hey World. But they don’t allow comments, so let’s create a clone that includes comments for each blog post.

Follow along with the code in this post.

Create a new app (I’m using Tailwind CSS, but you can pick what makes sense to you). I’ll also add Action Text for the Post and Comment models.

$ rails new BlogWorld -c tailwind
$ cd BlogWorld
$ bin/rails action_text:install
$ rails g scaffold Post title:string author:string content:rich_text
$ rails g scaffold Comment author:string content:rich_text post:references
Enter fullscreen mode Exit fullscreen mode

The scaffolding gives us a quick way to see the Posts and Comments. Add the association in post.rb, and the post will display related comments:

class Post < ApplicationRecord
  has_many :comments
  has_rich_text :content
end
Enter fullscreen mode Exit fullscreen mode

In posts/_post.html.erb, let's add the comments partial:

<div class="ml-12 pl-4 my-4 border-l-2 border-green-500">
  <%= render post.comments %>
</div>
Enter fullscreen mode Exit fullscreen mode

We now have a sparse post and comment view. Set up an inbound email for posting to the blog:

$ bin/rails action_mailbox:install
$ bin/rails g mailbox Post
Enter fullscreen mode Exit fullscreen mode

This will generate the ApplicationMailbox. We’ll set up a route so that anything for blog@ goes to our Post Mailbox and creates a post.

class ApplicationMailbox < ActionMailbox::Base
  routing /blog@/i => :post
end
Enter fullscreen mode Exit fullscreen mode

You can test this quickly by going to http://localhost:3000/rails/conductor/action_mailbox/inbound_emails and sending some emails to your service. If you send something to blog@whatever.com, the email should be delivered to an inbox on our app. If you email any other address, the message will bounce.

Receive Email with Post Mailbox

Let’s set up the Post Mailbox to receive the email and post it to the blog. Each Mailbox has access to the original inbound_email and mail objects. The InboundEmail is a wrapper around the mail class used throughout rails.

For our purposes, we’re interested in who the email is from, its subject, and its body copy. We can extract these and create a Post record that will show up on our blog's front page.

class PostMailbox < ApplicationMailbox
  def process
    Post.create title: mail['subject'].to_s, author: mail['from'].to_s, content: mail.body.to_s
  end
end
Enter fullscreen mode Exit fullscreen mode

Send another email to your blog address and then refresh the index page. You should see the post!

Add Comments for a Post in Action Mailbox

Now to add comments for a post. First, any email commenter needs to refer to the correct post when sending an email. A simple way to do this is to encode the post ID in the inbound email address (like comment+123@whatever.com, where the 123 in the email address refers to a Post element).

Generate the CommentMailbox:

$ bin/rails g mailbox Comment
Enter fullscreen mode Exit fullscreen mode

Add a route in Action Mailbox to send any emails with comment+123 to the CommentMailbox:

routing /^comment\+\d+@/i => :comment
Enter fullscreen mode Exit fullscreen mode

In the _post.html.erb, add a link to generate the email address, so someone can open their email app and send an email:

<div class="ml-12 pl-4 my-4 border-l-2 border-green-500">
  <%= render post.comments %>
  <%= mail_to "comment+#{post.id}@whatever.com", class: "rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
</div>
Enter fullscreen mode Exit fullscreen mode

The incoming email will be routed to the CommentMailbox and parsed into a comment attached to the correct blog post.

class CommentMailbox < ApplicationMailbox
  def process
    Comment.create author: mail["from"].to_s, content: mail.body.to_s, post: post
  end

  def post
    return @post unless @post.nil?
    email = mail.recipients.reject { |address| address.blank? }.first
    match = email.match(/^comment\+(.*)@/i)
    token = match[1]

    begin
      if token
        @post = Post.find_by_id(token)
      else
        bounced!
      end
    rescue RecordNotFound
      bounced!
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

The process method creates a comment from the email body and sender email. It references the Post queried in the post method. This method gets the first recipient's email address and uses a regular expression to pull out the post ID.

If the Post doesn’t exist or a token can’t be parsed, the email bounces, which stops the processing.

Now go to the Rails conductor form and send a comment to the address for each Post. The comment will appear underneath the post on the index page!

A More Complex Example Using Action Mailbox

Emails are actually really complicated. Imagine you've got an application monitoring tool set up, you deploy something like this to your app, and you start seeing errors in your APM dashboard.

You may see a parsing error, or that posts/comments have a lot of weird formatting errors.

Your app receives HTML emails, and you take the raw body source and post it to the website. The mail gem allows us to see if the incoming email has an HTML body, and we can pull whatever parts from the message we need.

Let’s change the CommentMailbox and PostMailbox to check for multipart emails and pull out the HTML part, falling back to text if that’s the only thing left.

Each email either has no parts or multiple parts. The preferred order is to see if there is an HTML part and use it, and if not, try to get and use the text part. If there aren’t parsed HTML or text sections, we’ll use the email body as before.

The PostMailbox is now a little more complicated:

class PostMailbox < ApplicationMailbox
  def process
    post = Post.new title: mail["subject"].to_s, author: mail["from"].to_s
    post.content = if mail.html_part
      mail.html_part.decoded
    elsif mail.text_part
      mail.text_part.decoded
    else
      mail.decoded
    end
    post.save
  end
end
Enter fullscreen mode Exit fullscreen mode

The CommentMailbox also has a different process method:

def process
  comment = Comment.new author: mail["from"].to_s, post: post
  comment.content = if mail.html_part
    mail.html_part.decoded
  elsif mail.text_part
    mail.text_part.decoded
  else
    mail.decoded
  end
  comment.save
end
Enter fullscreen mode Exit fullscreen mode

Now we can handle emails coming from someone’s phone.

Adding Action Mailbox to Your Rails App

Thanks to Action Mailbox, we can consider emails as another I/O avenue for our Rails app. We can write code independent of email service providers using conceptual compression. I’ve even been able to move email providers with minimal work since I don’t have to worry about the underlying infrastructure.

APM tools like AppSignal also provide a convenient dashboard to monitor all your outgoing ActionMailers and keep an eye on deliverability.

Here’s an example, showing one of my apps that sends and receives lots of emails:

AppSignal ActionMailer Dashboard

This gives you more visibility into what’s happening inside your app.

Wrapping Up

In this post, we first defined the capabilities of Action Mailer for Rails. We then set up a demo project where we integrated inbound emails and parsed them to create posts for a blog.

I hope you've found this useful. Happy coding!

P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

Top comments (0)