Using Action Mailbox on Rails 6

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.

Routing Emails

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

Create mailboxes

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 - Array
  • mail.recipients - Array
  • mail.subject - String
  • See body and attachments methods below for other details
class 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)

Unit testing mailboxes

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
Published 8 Jul 2020

I build mobile and web applications. Full Stack, Rails, React, Typescript, Kotlin, Swift
Pulkit Goyal on Twitter