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 Mailboxes. 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)
endTo 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
endIf 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
endThe 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