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:
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"]
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
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
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>
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
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
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
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
Add a route in Action Mailbox to send any emails with comment+123
to the CommentMailbox
:
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:
<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.
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:
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:
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:
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)