Sending mail from a Rails application has been covered by hundreds or thousands of articles, however, there is not a ton of articles about receiving, parsing and using the new . ActionMailbox The following is an excerpt from my book and is used in a new product I am building . Thus, I am standing by the tutorial here with real world usage! Build A SaaS App in Rails 6 Mail Buffer Lastly, as this is an excerpt, this takes into account the chapter leading up to this section where the reader will have created an application, models for a , / / (which are ) and a few mailers for outgoing mail. Standup Todo Did Blockers Tasks Here goes nothing... Another aspect of email within a SaaS application is receiving mail. While this is far less normal or used in comparison to sending, it can be a great way to make end user's responses to email or action items quicker. At a high level, there are a few different layers to this. The topmost layer is the email service, and this book's case, Sendgrid. This service handles sending outbound emails, as well as routing incoming emails to an address/domain name specified in their interface. Once the email is routed, it will be redirected to a route and processor file in your application. This file will be responsible for parsing the incoming email address and using logic to decide what to do with it. In the case of the Standup App we are building, we can have directions to respond to an Email Reminder to create a new standup right from their email response! Some of the tools that we will be using are Sendgrid's email routing service and ngrok, an HTTP tunneling service. `ngrok` is very useful for a few solutions throughout the remainder of this book. It allows you to have a web-accessible URL, which tunnels(connects) to ports within your local machine. Meaning, that you will have a that will forward to your computer and a specific port specified when you start . http://somesubdomain.ngrok.com ngrok This allows you to test with external services such as Sendgrid, Stripe(later), Github(later), and more! Let's get started with some setup: 1. Download and use ngrok Download ngrok Unzip and move the executable file to where you would like. In \*nix OS's, open ngrok with: . This will be dependent on the port you are using for your Rails server. ngrok will now fire up a tunnel service with a randomly generated URL. path/to/ngrok HTTP start 3000 Add to config.hosts << "yoursubdomain.ngrok.io" config/environments/development.rb Optionally, if you upgrade to a paid version of , you can set a subdomain, so you do not have to change your settings elsewhere every time you restart . ngrok ngrok 2. Follow your domain's DNS provider's instructions for adding an MX record for the subdomain or domain name you have chosen to use for inbound email. The MX record will be , with a priority of . mx.sendgrid.net 10 Now, before we head to the next part, we need to create and store a password for the incoming mail. You can use any method you like to create a password, and if you just can't think of any way to do so, you can run in Rails console. SecureRandom.alphanumeric Now, to store this password you just came up with, we will need to introduce a new small, but powerful, feature, . Since Rails 5.2, Rails provides a built-in way for storing secure credentials, that are encrypted. Rails Credentials To start using Rails Credentials, you will want to fire off the edit command: = rails credentials:edit EDITOR "atom --wait" The editor I am using in this example is Atom. However, you can use any editor you like, and it's command line caller. Also, is used to make sure that the editor will wait for Rails to finish decrypting the file before opening it in the editor. --wait Now that we have the encrypted file open, add the following lines: action_mailbox: ingress_password: *password you just made* Save the file and close the tab/editor to make sure Rails saves the file and encrypts it. You will notice when Rails first opened the file, there was some output text in your terminal. Which includes the master key, DO NOT LOSE THIS KEY. It will need to be used a the only text in a to be able to encrypt and decrypt the credentials. config/master.key Now, with that out of the way, there is a little bit more setup we need to do for Action Mailbox. First, two commands to get Rails and some tables set up for inbound email: bin/rails action_mailbox:install bin/rails db:migrate Lastly, add the following line to the file to let Rails know we're using Sendgrid: config/application.rb config.action_mailbox.ingress = :sendgrid Ok, back to Sendgrid's setup: In Sendgrid, go to the link, under settings, to create the email route that will send the email. Enter the following settings: Inbound Parse Enter for the domain field. You can optionally choose an additional subdomain to receive emails within the Inbound Parse. app.yourdomain.com Enter as the destination URL. http://actionmailbox:THEPASSWORDFROMBEFORE@yoursubdomain.ngrok.io/rails/action_mailbox/sendgrid/inbound_emails Check , before pressing the button. POST the raw, full MIME message submit Ok, we're getting nearly done with the setup here. Next, we will need to set up the applications inbound email route and run a command to generate that mailbox class. First, in the file that was created just a few commands ago, you will add . application_mailbox.rb routing /standups\.[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}@/i => :standups This says when an incoming mail matches that email address syntax, it will be routed to the mailbox class for processing. standups After that, to create the mailbox file, you will just need to run a Rails generator: bin/rails generate mailbox forwards A few changes will be needed to allow Action Mailbox and its files to capture text from incoming replies. First, we will update the to create a unique reply-to address and include that email address as part of the outgoing email: EmailReminderMailer @user = user @team = team reply_to = make_bootstrap_mail( @user.email, , reply_to ) < ApplicationMailer class EmailReminderMailer def reminder_email (user, team) "'Standups App' <standups. @app.yourdomain.com>" #{@user.id} to: subject: " Standup Reminder!" #{team.name} reply_to: end end Here we are adding the string to so when someone replies to a reminder email, the reply will be routed through Sendgrid, to Action Mailbox, and then finally by the Standup Mailbox class. reply_to Next, we will update the mailer template to have and some text letting the email recipient know they can add a standup by replying: ##- Please type your reply above this line -## <%= %> <%= %> <%= %> ##- Please type your reply above this line -## Standup App < = > div class "container" < = = > span class "text-secondary text-center" style "font-size:8px" </ > span < = > h1 class "text-center mb-4" </ > h1 < = > div class "card mb-4" < = > div class "card-body" < = > h3 class "text-center mb-4" @team.name Reminder! Just wanted to remind you to add your standup for the team: </ > h3 < = > p class "mb-4" @team.name </ > p link_to , new_standup_url(), { "Add Your Standup" : " - - - -2", : " :95%"} class btn btn primary btn lg mx auto mt style width </ > div </ > div </ > div Lastly, we will need to add an extra column to the table to track the coming from the Mailgun routed emails. As you can not count on an email service to provide "just once" delivery, we will need to track these unique IDs ourselves on the Standup table. Standups Message-ID g migration message_id rails AddMessageIdToStandups Next, before the end of the newly created migrations method, you will want to add . This index will allow quick lookups as the table grows. change add_index :standups, :message_id Standups Finally, migrate the actual change: rails db:migrate Lastly, to make sure sidekiq picks up the queues that Rails uses for Action Mailbox, we will need to adjust the in the to: worker Procfile.dev worker: bundle exec sidekiq -q -q mailers -q action_mailbox_routing -q active_storage_analysis default With those changes out of the way we can now add the new class that will parse the incoming email: StandupsMailbox TASK_TYPE_HASH = { => , => , => } reply_user = mail.to.first&.split( )&.last&.split( )&.first&.split( )&.last reply_user.blank? user = User.find_by( reply_user) user. ? Standup.exists?( inbound_email.message_id) today = Date.today.iso8601 Standup.exists?( today) safe_body = Rails::Html::WhiteListSanitizer.new.sanitize(mail_body) tasks_from_body = safe_body.scan( ) tasks_from_body.blank? tasks_from_body.empty? build_and_create_standup( user, tasks_from_body, today, inbound_email.message_id ) private standup = Standup.new( user.id, date, message_id ) tasks.each task_type, task_body = task.first.scan( ).flatten standup.tasks << Task.new( TASK_TYPE_HASH[task_type], task_body) standup.save @mail_body = mail.multipart? mail.parts[ ].body.decoded mail.decoded < ApplicationMailbox class StandupsMailbox '[d]' 'Did' '[t]' 'Todo' '[b]' 'Blocker' def process # Get a user id from reply-to or bail '<' '@' '.' return if # Find a user by the id or bail id: return if nil # Bail if standup with incoming message-id exists return if message_id: # Bail if a standup for today exists return if standup_date: # Get content or bail /(\[[dtb]{1}\].*)$/ return if || user: tasks: date: message_id: end def build_and_create_standup ( , , , ) user: tasks: date: message_id: user_id: standup_date: message_id: do |task| /(\[[dtb]\])(.*)$/ type: title: end end def mail_body || begin if 0 else end end end end The class is relatively simple, but let's go over it section by section: ruby TASK_TYPE_HASH = { => , => , => } ... < ApplicationMailbox class StandupsMailbox '[d]' 'Did' '[t]' 'Todo' '[b]' 'Blocker' Here we are setting up the to inherit from the class, which handles the routing. Additionally, we are creating a hash to later use in the text content to type conversion. StandupsMailbox ApplicationMailbox Task ... reply_user = mail.to.first&.split( )&.last&.split( )&.first&.split( )&.last reply_user.blank? user = User.find_by( reply_user) user. ? Standup.exists?( inbound_email.message_id) today = Date.today.iso8601 Standup.exists?( today) safe_body = Rails::Html::WhiteListSanitizer.new.sanitize(mail_body) tasks_from_body = safe_body.scan( ) tasks_from_body.blank? tasks_from_body.empty? ... def process # Get a user id from reply-to or bail '<' '@' '.' return if # Find a user by the id or bail id: return if nil # Bail if standup with incoming message-id exists return if message_id: # Bail if a standup for today exists return if standup_date: # Get content or bail /(\[[dtb]{1}\].*)$/ return if || Here we are grabbing some information used in the parsing, as well as giving the method chances to exit early if the incoming email is not sufficient for processing and creation. In the first section, the incoming email address is parsed to find the user's . That string is then used to find a User. If there is no user, the method returns without adding a Standup. process Standup id The next line will exit the method early if there is already a standup with the current Message-ID. Again, this is making sure to guard against email providers, not guaranteeing "just once" delivery. That is followed up by generating a variable for the current date and making sure there is no standup with the and . current_user current_date To get the mail's content, we will want to do two things, make sure we get the correct body from the mail object as email clients can send both an HTML and plain text version. Then we will want to make sure we only grab the content above the string by using the method (which splits a string into as many parts based on the separator specified in the argument): ##- Please type your reply above this line -## split @mail_body = body = mail.multipart? mail.parts[ ].body.decoded mail.decoded body.split( ).first def mail_body || begin if 0 else end '##- Please type your reply above this line -##' end end Finally, the actual email content is parsed with a regular expression. Regular Expression is a programming language that allows you to pattern match on a string and even capture parts of the pattern matching. This particular pattern(which you can get a more thorough syntax explanation searches for lines that begin with , or . If those are present, it captures the content to the end of the line. here [d] [t] [r] The method on the content's body allows it to catch all occurrences of the above pattern. If the scan's output is empty, the method exits. .scan process build_and_create_standup( user, tasks_from_body, today, email.headers[ ] ) private standup = Standup.new( user.id, date, message_id ) tasks.each task_type, task_body = task.first.scan( ).flatten standup.tasks << Task.new( TASK_TYPE_HASH[task_type], task_body) standup.save user: tasks: date: message_id: "Message-ID" end def build_and_create_standup ( , , , ) user: tasks: date: message_id: user_id: standup_date: message_id: do |task| /(\[[dtb]\])(.*)$/ type: title: end end end The last section here is a culmination of all of the stored information so far to be saved into a new . The , , , and are all passed into a method that will hand the actual save. The method creates a new , with the user's ID, date, and . Standup user tasks_from_body today message_id build_and_create_standup Standup message_id Once the object is created, the task's strings are iterated over to build the with a type and assigned as child objects with the syntax. Finally the new object with children will be saved with Task << Standup Tasks standup.save Lastly, you can test this all works if you reply back to an email(that was sent through Sendgrid SMTP and not letter_opener) with the following text: [d] Did a thing [d] And Another [t] Something to [b] Something the way. Some really long line about something or another do in Testing this will require a new spec file with a few blocks to test all the branches the may encounter. it StandupMailbox First, we will need to add one small change to the file to make sure RSpec has access to the Rails Action Mailbox test helpers. rails_helper.rb I'll add the new lines and a few lines above each, so you know where to put the new stuff: ... Warden::Test::Helpers ... RSpec.configure config. Devise::Test::ControllerHelpers, config. Devise::Test::ControllerHelpers, config. ActionMailbox::TestHelper, ... require 'support/system_macros' include require 'action_mailbox/test_helper' # <-- new line do |config| include type: :controller include type: :view include type: :mailbox # <-- new line ...and now the actual spec file itself: ActiveJob::TestHelper RSpec.describe StandupsMailbox, let( ) { FactoryBot.create( ) } let( ) { } let( ) subject receive_inbound_email_from_mail( user.email, to_email, , body ) before ActiveJob::Base.queue_adapter = it expect { subject }.to change(Standup, ).by( ) it expect { subject }.to change(Task, ).by( ) context let( ) { } it expect { subject }.to_not change(Standup, ) context let( ) { } it expect { subject }.to_not change(Standup, ) context let( ) { } it expect { subject }.to_not change(Standup, ) context let( ) { } it expect { subject }.to_not change(Standup, ) require 'rails_helper' include type: :mailbox do :user :user :to_email "standups. @app.buildasaasappinrails.com" #{user.id} :body do %Q( [d] Did a thing [d] And Another [t] Something to do [b] Something in the way. Some really long line about something or another ##- Please type your reply above this line -## ) end do from: to: subject: 'Re: Reminder' body: end do :test end 'saves standup record' do :count 1 end 'saves task records' do :count 4 end 'fails on bad to email' do :to_email "standups@app.buildasaasappinrails.com" 'does not save standup record' do rescue nil :count end end 'fails on no user' do :to_email "standups.9a859af8-30ca-4473-b073-47105352d936@app.buildasaasappinrails.com" 'does not save standup record' do rescue nil :count end end 'fails on useless body' do :body 'asdj;oisaduaskd' 'does not save standup record' do rescue nil :count end end 'fails on empty body' do :body '' 'does not save standup record' do rescue nil :count end end end While long and containing six examples, this spec is actually pretty straightforward. It is first testing the happy path where everything is set up and working. Then checks each failing path that doesn't create a standup, in order as those paths appear in the 's method. StandupsMailbox .process Previously published at https://robrace.dev/using-action-mailbox-in-rails-6-to-receive-mail/