Action Mailbox is a framework available with Rails 6 to handle incoming emails with Rails. It integrates with a variety of service providers out of the box including SendGrid, Mandrill, Mailgun, or self hosted Exim, Postfix or Qmail. It stores all emails in the database and also provides declarative routing for mails to custom Mailbox
es. A great usecase we have for this feature is to allow imports (or file uploads, for example) to certain users directly through email.
The first step is to install action_mailbox
to the application. Run rails action_mailbox:install
. If everything went well, this will create some new migrations and a file app/mailboxes/application_mailbox.rb
. Run the migrations to create the required tables on your DB.
The routes for the emails are defined in application_mailbox
. The rules are processed from top to bottom and the mail is delivered to the first matched mailbox. Here’s a sample route definition that forwards all emails coming to import-{key}@example.com
to ImportMailbox
and all others to a BounceMailbox
.
class ApplicationMailbox < ActionMailbox::Base
routing(/import-(\w+)@#{ENV.fetch("INBOUND_EMAIL_DOMAIN", "example.com")}\Z/i => :import)
# bounce all other emails
routing(:all => :bounce)
end
To handle our incoming emails, we will need to create custom mailboxes. We can have Rails generate them for us using rails g mailbox <<name>>
.
From our routing through ApplicationMailbox
, the emails coming with import-xyz@example.com
will automatically call ImportMailbox#process
. We can then access mail
details using:
mail.from
- Arraymail.recipients
- Arraymail.subject
- Stringbody
and attachments
methods below for other detailsclass ImportMailbox < ApplicationMailbox
def process
# Handle mail here
end
def body
mail.body.decoded
end
def attachments
mail.attachments.map do |attachment|
# access attachment.fiilename, attachment.decoded, attachment.content_type
# You can also add the attachments to a model (e.g. `Resource` here) connected to ActiveStorage
Resource.new(:name => attachment.filename).tap do |r|
r.attachment.attach(:io => StringIO.new(attachment.decoded),
:filename => attachment.filename,
:content_type => attachment.content_type)
r.save!
end
end
end
end
If you need to bounce emails (for example, if they don’t match any user’s address), you can use bounce_with
with reference to an email (can be generated using ActionMailer
):
bounce_with BounceMailer.invalid_to_address(mail.from.first)
The official ActionMailbox
docs are very light about the topic of unit testing mailboxes. ActionMailbox::TestHelper
provides some utility methods to test emails. The core methods of importance are receive_inbound_email_from_fixture
(which reads from an eml
file fixture) and receive_inbound_email_from_mail
which accepts a block to configure the email.
receive_inbound_email_from_mail do |mail|
mail.to "To Name <to@example.com>"
mail.from "From Name <from@bagend.com>"
mail.subject "Test subject"
mail.text_part do |part|
part.body "test body"
end
mail.html_part do |part|
part.body "<h1>test body</h1>"
end
end
The core of the testing depends on the ruby mail
library. So if you find that the test helper isn’t working for you or has some limitations (for example, I couldn’t get it to work with attachments), you can always fall back to the Mail
implementation. Here is my sample implementation:
mail = Mail.new do
from "from@example.com"
to "to@example.com"
cc "cc@example.com"
subject "Test subject"
content_type "multipart/mixed"
part(:content_type => "multipart/alternative") do |p|
p.part "text/html" do |p|
p.body = "test body"
end
p.part "text/plain" do |p|
p.body = "test body"
end
end
end
mail.attachments["attachment.csv"] = { :mime_type => "text/csv", :content => "a,b,c" }
inbound = ActionMailbox::InboundEmail.create_and_extract_message_id!(mail.to_s, :status => :processing)
inbound.route