ruby

Integrate and Troubleshoot Inbound Emails with Action Mailbox in Rails

John Beatty

John Beatty on

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.

Ruby
> 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"]

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.

Shell
$ 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

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:

Ruby
class Post < ApplicationRecord has_many :comments has_rich_text :content end

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

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

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

Shell
$ bin/rails action_mailbox:install $ bin/rails g mailbox Post

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.

Ruby
class ApplicationMailbox < ActionMailbox::Base routing /blog@/i => :post end

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.

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

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:

Shell
$ bin/rails g mailbox Comment

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

Ruby
routing /^comment\+\d+@/i => :comment

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

erb
<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>

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

Ruby
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

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:

Ruby
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

The CommentMailbox also has a different process method:

Ruby
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

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!

John Beatty

John Beatty

Our guest author John learned Rails to build APIs for iPhone apps in 2010, and he hasn’t looked back. He currently teaches programming to High School students, builds custom software for the school, and blogs.

All articles by John Beatty

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps