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:
- Create tasks.
- Receive an email about the tasks they have been assigned.
- 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.
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.
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…