Action Mailbox Basics
This guide provides you with all you need to get started in receiving emails toyour application.
After reading this guide, you will know:
- How to receive email within a Rails application.
- How to configure Action Mailbox.
- How to generate and route emails to a mailbox.
- How to test incoming emails.
1. What is Action Mailbox?
Action Mailbox routes incoming emails to controller-like mailboxes forprocessing in your Rails application. Action Mailbox is for receiving email,whileAction Mailer is forsending them.
The inbound emails are routed asynchronously usingActiveJob to one or several dedicated mailboxes. These emailsare turned intoInboundEmailrecords usingActive Record, which are capable ofinteracting directly with the rest of your domain model.
InboundEmail records also provide lifecycle tracking, storage of the originalemail viaActive Storage, and responsible datahandling with on-by-defaultincineration.
Action Mailbox ships with ingresses which enable your application to receiveemails from external email providers such as Mailgun, Mandrill, Postmark, andSendGrid. You can also handle inbound emails directly via the built-in Exim,Postfix, and Qmail ingresses.
2. Setup
Action Mailbox has a few moving parts. First, you'll run the installer. Next,you'll choose and configure an ingress for handling incoming email. You're thenready to add Action Mailbox routing, create mailboxes, and start processingincoming emails.
To start, let's install Action Mailbox:
$bin/railsaction_mailbox:installThis will create anapplication_mailbox.rb file and copy over migrations.
$bin/railsdb:migrateThis will run the Action Mailbox and Active Storage migrations.
The Action Mailbox tableaction_mailbox_inbound_emails stores incomingmessages and their processing status.
At this point, you can start your Rails server and check outhttp://localhost:3000/rails/conductor/action_mailbox/inbound_emails. SeeLocal Development and Testing for more.
The next step is to configure an ingress in your Rails application to specifyhow incoming emails should be received.
3. Ingress Configuration
Configuring ingress involves setting up credentials and endpoint information forthe chosen email service. Here are the steps for each of the supportedingresses.
3.1. Exim
Tell Action Mailbox to accept emails from an SMTP relay:
# config/environments/production.rbconfig.action_mailbox.ingress=:relayGenerate a strong password that Action Mailbox can use to authenticate requeststo the relay ingress.
Usebin/rails credentials:edit to add the password to your application'sencrypted credentials underaction_mailbox.ingress_password, where ActionMailbox will automatically find it:
action_mailbox:ingress_password:...Alternatively, provide the password in theRAILS_INBOUND_EMAIL_PASSWORDenvironment variable.
Configure Exim to pipe inbound emails tobin/railsaction_mailbox:ingress:exim, providing theURL of the relay ingress and theINGRESS_PASSWORD you previously generated. If your application lived athttps://example.com, the full command would look like this:
$bin/railsaction_mailbox:ingress:eximURL=https://example.com/rails/action_mailbox/relay/inbound_emailsINGRESS_PASSWORD=...3.2. Mailgun
Give Action Mailbox your Mailgun Signing key (which you can find under Settings-> Security & Users -> API security in Mailgun), so it can authenticate requeststo the Mailgun ingress.
Usebin/rails credentials:edit to add your Signing key to your application'sencrypted credentials underaction_mailbox.mailgun_signing_key, where ActionMailbox will automatically find it:
action_mailbox:mailgun_signing_key:...Alternatively, provide your Signing key in theMAILGUN_INGRESS_SIGNING_KEYenvironment variable.
Tell Action Mailbox to accept emails from Mailgun:
# config/environments/production.rbconfig.action_mailbox.ingress=:mailgunConfigureMailgunto forward inbound emails to/rails/action_mailbox/mailgun/inbound_emails/mime. If your application livedathttps://example.com, you would specify the fully-qualified URLhttps://example.com/rails/action_mailbox/mailgun/inbound_emails/mime.
3.3. Mandrill
Give Action Mailbox your Mandrill API key, so it can authenticate requests tothe Mandrill ingress.
Usebin/rails credentials:edit to add your API key to your application'sencrypted credentials underaction_mailbox.mandrill_api_key, where ActionMailbox will automatically find it:
action_mailbox:mandrill_api_key:...Alternatively, provide your API key in theMANDRILL_INGRESS_API_KEYenvironment variable.
Tell Action Mailbox to accept emails from Mandrill:
# config/environments/production.rbconfig.action_mailbox.ingress=:mandrillConfigureMandrillto route inbound emails to/rails/action_mailbox/mandrill/inbound_emails. Ifyour application lived athttps://example.com, you would specify thefully-qualified URLhttps://example.com/rails/action_mailbox/mandrill/inbound_emails.
3.4. Postfix
Tell Action Mailbox to accept emails from an SMTP relay:
# config/environments/production.rbconfig.action_mailbox.ingress=:relayGenerate a strong password that Action Mailbox can use to authenticate requeststo the relay ingress.
Usebin/rails credentials:edit to add the password to your application'sencrypted credentials underaction_mailbox.ingress_password, where ActionMailbox will automatically find it:
action_mailbox:ingress_password:...Alternatively, provide the password in theRAILS_INBOUND_EMAIL_PASSWORDenvironment variable.
ConfigurePostfixto pipe inbound emails tobin/rails action_mailbox:ingress:postfix, providingtheURL of the Postfix ingress and theINGRESS_PASSWORD you previouslygenerated. If your application lived athttps://example.com, the full commandwould look like this:
$bin/railsaction_mailbox:ingress:postfixURL=https://example.com/rails/action_mailbox/relay/inbound_emailsINGRESS_PASSWORD=...3.5. Postmark
Tell Action Mailbox to accept emails from Postmark:
# config/environments/production.rbconfig.action_mailbox.ingress=:postmarkGenerate a strong password that Action Mailbox can use to authenticate requeststo the Postmark ingress.
Usebin/rails credentials:edit to add the password to your application'sencrypted credentials underaction_mailbox.ingress_password, where ActionMailbox will automatically find it:
action_mailbox:ingress_password:...Alternatively, provide the password in theRAILS_INBOUND_EMAIL_PASSWORDenvironment variable.
Configure Postmark inboundwebhook toforward inbound emails to/rails/action_mailbox/postmark/inbound_emails withthe usernameactionmailbox and the password you previously generated. If yourapplication lived athttps://example.com, you would configure Postmark withthe following fully-qualified URL:
https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/postmark/inbound_emailsWhen configuring your Postmark inbound webhook, be sure to check the boxlabeled"Include raw email content in JSON payload". Action Mailbox needsthe raw email content to work.
3.6. Qmail
Tell Action Mailbox to accept emails from an SMTP relay:
# config/environments/production.rbconfig.action_mailbox.ingress=:relayGenerate a strong password that Action Mailbox can use to authenticate requeststo the relay ingress.
Usebin/rails credentials:edit to add the password to your application'sencrypted credentials underaction_mailbox.ingress_password, where ActionMailbox will automatically find it:
action_mailbox:ingress_password:...Alternatively, provide the password in theRAILS_INBOUND_EMAIL_PASSWORDenvironment variable.
Configure Qmail to pipe inbound emails tobin/railsaction_mailbox:ingress:qmail, providing theURL of the relay ingress and theINGRESS_PASSWORD you previously generated. If your application lived athttps://example.com, the full command would look like this:
$bin/railsaction_mailbox:ingress:qmailURL=https://example.com/rails/action_mailbox/relay/inbound_emailsINGRESS_PASSWORD=...3.7. SendGrid
Tell Action Mailbox to accept emails from SendGrid:
# config/environments/production.rbconfig.action_mailbox.ingress=:sendgridGenerate a strong password that Action Mailbox can use to authenticate requeststo the SendGrid ingress.
Usebin/rails credentials:edit to add the password to your application'sencrypted credentials underaction_mailbox.ingress_password, where ActionMailbox will automatically find it:
action_mailbox:ingress_password:...Alternatively, provide the password in theRAILS_INBOUND_EMAIL_PASSWORDenvironment variable.
Configure SendGrid InboundParseto forward inbound emails to/rails/action_mailbox/sendgrid/inbound_emailswith the usernameactionmailbox and the password you previously generated. Ifyour application lived athttps://example.com, you would configure SendGridwith the following URL:
https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/sendgrid/inbound_emailsWhen configuring your SendGrid Inbound Parse webhook, be sure to check thebox labeled“Post the raw, full MIME message.” Action Mailbox needs the rawMIME message to work.
4. Processing Incoming Email
Processing incoming emails usually entails using the email content to createmodels, update views, queue background work, etc. in your Rails application.
Before you can start processing incoming emails, you'll need to setup ActionMailbox routing and create mailboxes.
4.1. Configure Routing
After an incoming email is received via the configured ingress, it needs to beforwarded to a mailbox for actual processing by your application. Much like theRails router that dispatches URLs to controllers, routing inAction Mailbox defines which emails go to which mailboxes for processing. Routesare added to theapplication_mailbox.rb file using regular expressions:
# app/mailboxes/application_mailbox.rbclassApplicationMailbox<ActionMailbox::Baserouting(/^save@/i=>:forwards)routing(/@replies\./i=>:replies)endThe regular expression matches the incoming email'sto,cc, orbcc fields.For example, the above will match any email sent tosave@ to a "forwards"mailbox. There are other ways to route an email, seeActionMailbox::Basefor more.
We need to create that "forwards" mailbox next.
4.2. Create a Mailbox
# Generate new mailbox$bin/railsgenerate mailbox forwardsThis createsapp/mailboxes/forwards_mailbox.rb, with aForwardsMailbox classand aprocess method.
4.3. Process Email
When processing anInboundEmail, you can get the parsed version of the emailas aMail object withInboundEmail#mail.You can also get the raw source directly using the#source method. With theMail object, you can access the relevant fields, such asmail.to,mail.body.decoded, etc.
irb>mail=> #<Mail::Message:33780, Multipart: false, Headers: <Date: Wed, 31 Jan 2024 22:18:40 -0600>, <From: someone@hey.com>, <To: save@example.com>, <Message-ID: <65bb1ba066830_50303a70397e@Bhumis-MacBook-Pro.local.mail>>,<In-Reply-To:>,<Subject:HelloActionMailbox>,<Mime-Version:1.0>,<Content-Type:text/plain;charset=UTF-8>,<Content-Transfer-Encoding:7bit>,<x-original-to:>>irb>mail.to=>["save@example.com"]irb>mail.from=>["someone@hey.com"]irb>mail.date=>Wed,31Jan202422:18:40-0600irb>mail.subject=>"Hello Action Mailbox"irb>mail.body.decoded=>"This is the body of the email message."# mail.decoded, a shorthand for mail.body.decoded, also worksirb>mail.decoded=>"This is the body of the email message."irb>mail.body=> <Mail::Body:0x00007fc74cbf46c0 @boundary=nil, @preamble=nil, @epilogue=nil, @charset="US-ASCII", @part_sort_order=["text/plain", "text/enriched", "text/html", "multipart/alternative"], @parts=[], @raw_source="This is the body of the email message.", @ascii_only=true, @encoding="7bit">4.4. Inbound Email Status
While the email is being routed to a matching mailbox and processed, ActionMailbox updates the email status stored inaction_mailbox_inbound_emails tablewith one of the following values:
pending: Received by one of the ingress controllers and scheduled forrouting.processing: During active processing, while a specific mailbox is runningitsprocessmethod.delivered: Successfully processed by the specific mailbox.failed: An exception was raised during the specific mailbox’s execution oftheprocessmethod.bounced: Rejected processing by the specific mailbox and bounced to sender.
If the email is marked eitherdelivered,failed, orbounced it'sconsidered "processed" and marked forincineration.
5. Example
Here is an example of an Action Mailbox that processes emails to create"forwards" for the user's project.
Thebefore_processing callback is used to ensure that certain conditions aremet beforeprocess method is called. In this case,before_processing checksthat the user has at least one project. Other supportedAction Mailboxcallbacks areafter_processing andaround_processing.
The email can be bounced usingbounced_with if the "forwarder" has noprojects. The "forwarder" is aUser with the same email asmail.from.
If the "forwarder" does have at least one project, therecord_forward methodcreates an Active Record model in the application using the email datamail.subject andmail.decoded. Otherwise, it sends an email, using ActionMailer, requesting the "forwarder" to choose a project.
# app/mailboxes/forwards_mailbox.rbclassForwardsMailbox<ApplicationMailbox# Callbacks specify prerequisites to processingbefore_processing:require_projectsdefprocess# Record the forward on the one project, or…ifforwarder.projects.one?record_forwardelse# …involve a second Action Mailer to ask which project to forward into.request_forwarding_projectendendprivatedefrequire_projectsifforwarder.projects.none?# Use Action Mailers to bounce incoming emails back to sender – this halts processingbounce_withForwards::BounceMailer.no_projects(inbound_email,forwarder:forwarder)endenddefrecord_forwardforwarder.forwards.createsubject:mail.subject,content:mail.decodedenddefrequest_forwarding_projectForwards::RoutingMailer.choose_project(inbound_email,forwarder:forwarder).deliver_nowenddefforwarder@forwarder||=User.find_by(email_address:mail.from)endend6. Local Development and Testing
It's helpful to be able to test incoming emails in development without actuallysending and receiving real emails. To accomplish this, there's a conductorcontroller mounted at/rails/conductor/action_mailbox/inbound_emails, whichgives you an index of all the InboundEmails in the system, their state ofprocessing, and a form to create a new InboundEmail as well.
Here is and example of testing an inbound email with Action Mailbox TestHelpers.
classForwardsMailboxTest<ActionMailbox::TestCasetest"directly recording a client forward for a forwarder and forwardee corresponding to one project"doassert_difference->{people(:david).buckets.first.recordings.count}doreceive_inbound_email_from_mail\to:"save@example.com",from:people(:david).email_address,subject:"Fwd: Status update?",body:<<~BODY --- Begin forwarded message --- From: Frank Holland <frank@microsoft.com> What's the status? BODYendrecording=people(:david).buckets.first.recordings.lastassert_equalpeople(:david),recording.creatorassert_equal"Status update?",recording.forward.subjectassert_match"What's the status?",recording.forward.content.to_sendendPlease refer to theActionMailbox::TestHelperAPI forfurther test helper methods.
7. Incineration of InboundEmails
By default, anInboundEmail that has been processed will be incinerated after30 days. TheInboundEmail is considered as processed when its status changestodelivered,failed, orbounced.
The actual incineration is done via theIncinerationJobthat's scheduled to run afterconfig.action_mailbox.incinerate_aftertime. This value is set to30.days by default, but you can change it in yourproduction.rb configuration. (Note that this far-future incineration schedulingrelies on your job queue being able to hold jobs for that long.)
Default data incineration ensures that you're not holding on to people's dataunnecessarily after they may have canceled their accounts or deleted theircontent.
The intention with Action Mailbox processing is that as you process an email,you should extract all the data you need from the email and persist it intodomain models in your application. TheInboundEmail stays in the system forthe configured time to allow for debugging and forensics and then will bedeleted.