Blog

Receiving and Processing Inbound Emails with Action Mailbox and Rails 6

Placeholder Avatar
Sameera Gayan
September 13, 2019

The vast majority of web applications these days have some sort of a notification system to communicate with their users. Typically, this is by sending them emails. Given emails are a major part of modern-day communication, application developers try to utilise email workflows for users to interact with their apps.

One example would be: when a user receives an email from an app (e.g issue tracker), they can then reply to that email with a comment and that comment will automatically be added to the app database. This makes the user experience seamless as users now don’t have to switch between apps to provide data. In this case, the email client and a ticketing app.

Take a project management application as an example. Imagine a workflow of creating a task and assigning it to the team. Each member will receive an email and they can reply to the email with a status update. Allowing them to reply to the same email with their status update will make their work easier compared to them having to log in to a separate project management app and updating the status there.

From a project management aspect, the above task should have all the status updates listed under the task, so it is easy to read and understand by anyone who logs in to the system. All the data in one place.

For Rails, historically there was no straight-forward way to handle incoming emails. To achieve something like this, you would have had to either use a gem like griddler or set up your own webhooks from a provider like cloudmailin.

But with new Rails 6 release, receiving emails has never been easier. Rails 6 has introduced Action Mailbox, a built-in mechanism to receive emails. In this post, let’s have a look at how we can set up Action Mailbox to receive emails in Rails 6.

Using the above project management example, we will create a small project management app. In this example, users of this app should be able to:

  1. Create tasks.
  2. Receive an email about the tasks they have been assigned.
  3. Reply to that email with their status update, with the data from the status update listed in the app under the given task.

Disclaimer: since we are focusing on Action Mailbox functionality, I will skip discussing points 1 and 2 in this blog. There are many tutorials out there on how to set up sending emails with Rails. Also, we’ll skip user creation and authentication. Now, let’s get into the fun part, coding…

Setting up the Rails Project

First things first, let’s create a Rails 6 project, Let’s call it, Projectmanager. 1 - Make sure you have Rails 6 installed on your machine. # Checking the Rails version in the command line. $ rails -v #=> Rails 6.0.0 2 - We’ll create a Rails 6 project and skip mini-test and later add rspec. $ rails new Projectmanager -T 3 - Now we’ll create a simple Task object. For simplicity, Task will only have one field called ‘name’. $ rails generate model Task name:string 4 - Next up is the comments model for tasks. comments will have only one field called note, and an association to a task. $ rails generate model Comment task:references note:text 5 - Now to set up the Active Record associations. You will see that the Comment model already has belongs_to but we need to add has_many to Task. ruby # app/models/task.rb class Task < ApplicationRecord has_many :comments end # app/models/comment.rb class Comment < ApplicationRecord belongs_to :task end 6 - Don’t forget to migrate the database. $ rails db:migrate 7 - Now, just to make sure that we have some data, let’s create a Task and a comment for it in the Rails console. task = Task.create(name: 'My Awesome task') comment = Comment.create(task: task, note: 'This is the first update') 8 - Now let’s create a controller with one action (show) to list the task and its comments. $ rails generate controller tasks show 9 - And to set the root url, change get 'tasks/show' to root 'tasks#show'. ruby # config/routes.rb Rails.application.routes.draw do root 'tasks#show' end 10 - Now set up the show method to get the first task and its comments. ruby # app/controllers/tasks_controller.rb class TasksController < ApplicationController def show @task = Task.includes(:comments).first end end 11 - Let’s update the generated view to show the comments and the task. Please note, we are just adding basic HTML and not focusing on styling up the page here. ```ruby # app/views/tasks/show.html.erb

<%= @task.name %>

    <% @task.comments.find_each do |comment| %>
  • <%= comment.note %>
  • <% end %>

``` Now comes the fun part. Since we have set up the basic Rails project, let’s dive into the actual focus of this blog post. Adding Action Mailbox.

Adding Action Mailbox

Let’s quickly go through the basic concepts of Action Mailbox. All the inbound emails received by Action Mailbox are converted to an InboundEmail record using Active Record. So, the ApplicationMailbox class acts as a base class just like ApplicationRecord does for models. This instance of the InboundEmail record is stored in the action_mailbox_inbound_emails table, hence the *_create_action_mailbox_tables.action_mailbox.rb migration. If you open up the below *_create_action_mailbox_tables.action_mailbox.rb migration, you’ll notice that it has a few columns, such as status: # https://github.com/rails/rails/blob/master/actionmailbox/app/models/action_mailbox/inbound_email.rb#L9 - Pending: Just received by one of the ingress controllers and scheduled for routing. - Processing: During active processing, while a specific mailbox is running its #process method. - Delivered: Successfully processed by the specific mailbox. - Failed: An exception was raised during the specific mailbox's execution of the +#process+ method. - Bounced: Rejected processing by the specific mailbox and bounced to sender. Also, it keeps the message id and the message_checksum to filter out duplicate messages. Another migration *_create_active_storage_tables.active_storage is generated to keep the email details. This is done via rails ActiveStorage. One more thing to note is that by default, once a mail is processed, it will be queued for incineration. This is to make sure that the application is not holding any sensitive data within the emails. By default, the cleanup will happen after 30 days. However, if you want to change it, you can do it via config.action_mailbox.incinerate_after in the application configuration. Okay, after that basic understanding on how Action Mailbox works, let’s continue with our code. 1 - To start, we need to add action_mailbox to our project. $ rails action_mailbox:install You’ll see the following in the command line: Copying application_mailbox.rb to app/mailboxes create app/mailboxes/application_mailbox.rb Copied migration 20190906012037_create_active_storage_tables.active_storage.rb from active_storage Copied migration 20190906012038_create_action_mailbox_tables.action_mailbox.rb from action_mailbox 2 - Next, let’s create the tables. $ rails db:migrate For this example, let’s assume we are receiving the comments for the tasks in the following format. - Address: replies@projectmanager.com - Subject: comment-<taskid> - Body: Comment text for the task 3 - Now, you need to set up something called the mailbox route. Think of this as a normal route, but for inbound emails. Open ApplicationMailbox and add the following. class ApplicationMailbox < ActionMailbox::Base routing :all => :comments end Note that we can use a regular expression for the routing, meaning you can easily handle multiple types of inbound emails, if needed. The second part is => :comments. This is where we tell Action Mailbox which mailbox to route to. In this case, we are routing all the inbound email to the comments mailbox. But ideally, as above, you would want to direct these using a regular expression. 4 - Now let’s create the comments mailbox. $ rails generate mailbox comments Note: Your mailbox name should match the name you’ve given it in the routing params. In our example, our routing param is: ruby routing :all => :comments So, our mailbox class is CommentsMailbox. The reason behind this is, "#{mailbox_name.to_s.camelize}Mailbox".constantize is how Action Mailbox finds the mailbox class, see the source code if you are interested.

5 - Once that is done, let’s open up the CommentsMailbox class.

ruby class CommentsMailbox < ApplicationMailbox def process end end

Here, the process method is the one we are interested in. This is where all the processing happens and we write our own logic to handle the inbound emails.

6 - Now, before going any further, let’s have a look at what we have available to us within the process method. To do so, let’s add a breakpoint to the process method.

ruby class CommentsMailbox < ApplicationMailbox def process byebug end end

One cool thing about Action Mailbox is that it ships with a UI to test inbound emails in the development environment. To access this, fire up the Rails server

$ rails server

And go to <your host>/rails/conductor/action_mailbox/inbound_emails and click on Deliver new inbound email. Fill in all the details as per the above email format (e.g subject to comment-<taskid>) and then click send. email_details Since we have a breakpoint at the process method, now the execution of the server process should stop at the process method.

7 - At this point, we have two main objects to work with 1 - inbound_email The current inbound email record as an InboundEmail instance, for example: ```

<ActionMailbox::InboundEmail id: 1, status: “processing”, message_id: “5d71ef414d844_7a33ff4d312d9e885c0@192-168-1-8.tpgi…”,

message_checksum: “c2e141423e6733925ed8ce8581beb28cde973552”, created_at: “2019-09-06 05:31:45”, updated_at: “2019-09-06 05:31:48”> ```

2 - Mail object This is probably the most important one. This is a Ruby Mail object. Since it is a standard Ruby Mail object, we can now perform actions like getting the subject, finding the sender, etc. Since we know the subject has the task ID, we can split the subject, find the task and update the comment from the email body.

These steps can vary based on how you want to process the inbound email and required validations such as “is the sender email is already a registered email?”, etc. However, for the scope of this blog post, I’ll keep it simple and just find the Task by the ID. Since this is just simple Ruby/Rails code, I’m not going to explain it in detail. The following is the final code:

ruby # app/mailboxes/comments_mailbox.rb class CommentsMailbox < ApplicationMailbox def process task = find_task(mail.subject) update_comments(task, mail.decoded) rescue # Proper error handler here. end private def update_comments(task, comment_string) task.comments.create!(note: comment_string) end def find_task(subject) task_id = subject.split("-").last.to_i Task.find(task_id) end end

Now when you submit the form, you should see the comment is added from the inbound email. via_inbound_email 8 - Cool, almost done. To wrap things up, let’s add a test. If you’re following this tutorial, you might remember that we opted-out of the default Minitest library by passing -T when we created the project. That is because we want to add RSpec. If you are familiar with Minitest over RSpec, you’re more than welcome to go with Minitest. To add RSpec, open your Gemfile and add rspec-rails to your :development, :test group block. ruby # Gemfile group :development, :test do ... gem 'rspec-rails', '~> 3.8' end

And then install rspec-rails.

$ bundle install $ rails generate rspec:install ActionMailbox comes with a test helper ActionMailbox::TestHelper with some helper methods for easy testing. At the time of this blog post, RSpec doesn’t have a way of accessing it out-of-the-box, but adding it to RSpec is easy. You can add it as a support helper by creating a file in spec/support/action_mailbox.rb. ruby # spec/support/action_mailbox.rb require 'action_mailbox/test_helper' RSpec.configure do |config| config.include ActionMailbox::TestHelper end

9 - Don’t forget to uncomment this line from the rails_helper, if it isn’t already.

ruby Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }

With this, we can access helper methods such as create_inbound_email_from_mail.

10 - Let’s write a quick spec. It’s pretty self-explanatory if you are familiar with testing, so I will not explain the spec; it simply checks if the comment from an inbound email is being added to the task.

ruby # spec/mailboxes/comments_mailbox_spec.rb require 'rails_helper' describe CommentsMailbox do let(:task) { Task.create!(name: "Test Task") } subject do receive_inbound_email_from_mail( from: 'test@user.com', to: 'comments@taskmanager.com', subject: email_subject, body: "Body text" ) end context 'email subject with a correct task ID' do let(:email_subject) { "comment-#{task.id}" } it 'adds a comment to the task' do expect { subject }.to change(task.comments, :count).by(1) end end context 'email subject with an invalid task ID' do let(:email_subject) { "comment-#{task.id}1" } it 'does not add a comment to the task' do expect { subject }.to change(task.comments, :count).by(0) end end end

That’s it. Now when you run the spec, you should see that it’s all working. ```

$ rspec # => 2 examples, 0 failures ```

Deploying to Production

Clearly, everything that we have done above is for the local environment. But once you get everything working locally, deploying it to production is just a matter of setting up the relevant configurations with your inbound email provider such as Mailgun. You can read the official documentation on setting up Action Mailbox with an inbound email provider here. Happy coding…