Matthew Lindfield Seager

Why Action Mailbox can't be used with Gmail

I’ve seen a few questions today about how to get Rails’ Action Mailbox working with Gmail so you can process Gmail messages in a Rails app. The short answer is:

  • Gmail doesn’t support it
  • Rails doesn’t support it

A longer answer, which I also posted on Stack Overflow in reply to the second question above, follows.

Action Mailbox is built around receiving email from a Mail Transfer Agent (MTA) in real time, not periodically fetching email from a mailbox. That is, it receives mail sent via SMTP, it doesn’t fetch mail (using IMAP or POP3) from another server that has already received it.

For this to work it is dependent on an external (to Rails) SMTP service receiving the email and then delivering the email to Action Mailbox. These external services are called “Ingresses” and, as at the time of writing, there are 5 available ingresses.

Of the five, four are commercial services that will run the required SMTP servers for you and then “deliver” the email to your application (usually as a JSON payload via a webhook).

You could already use those services in a Rails App and handle the webhooks yourself but Action Mailbox builds a standardised set of functionality on top. Almost like a set of rails to guide and speed the process.

In addition, the fifth ingress is the “Relay” ingress. This allows you to run your own supported MTA (SMTP server) on the same machine and for it to relay the received email to Action Mailbox (usually the raw email). The currently supported MTAs are:

To answer the specific questions about Gmail:

  1. How could they integrate that with Action Mailbox?

They couldn’t directly. They would need to also set up one of the 7 MTAs listed above and then somehow deliver the emails to that. The delivery could be accomplished with:

  • Forwarding rules managed by the user at the mailbox level
  • Dual delivery, split delivery or some other advanced routing rule managed by the admin at the domain level
  1. Would one use Gmail’s API, or would that not be appropriate for Action Mailbox?

Even if there were a way to have Gmail fire a webhook on incoming email (I’m not aware of any special delivery options outside the advanced routing rules above), there is currently no way to connect that theoretical webhook to Action Mailbox.

  1. If Gmail doesn’t work, what is different about SendGrid that makes it integrate appropriately?

Sendgrid (to use your example, the others work more or less the same way) offers an inbound mail handling API. Just as importantly, the Rails Team has built an incoming email controller to integrate with that API.

Given the lack of Gmail APIs and the lack of a Rails ingress controller, the only way I can think of that you could connect Action Mailbox to an existing Gmail mailbox would be for some other bit of code to check the mailbox, reformat the fetched email and then pose as one of the supported MTAs to deliver it to Action Mailbox.

It would be an interesting exercise, and would possibly become a popular gem, but it would very much be a kludge. A glorious kludge if done well, but a kludge nonetheless.

PayPal won’t let me login using my long, secure, unique password unless I verify using a mobile number I can’t access.

They won’t let me reset my password… without that number.

They won’t even let me use any of their 4 help methods… unless I login… with that number.


Tip for deploying a brand new Rails app

Hot tip, if you deploy a brand new Rails app to production and it doesn’t work, it might not be a problem with Ubuntu, Ansible, Capistrano, Nginx or Passenger… it might just be that it’s trying to show the “Yay! You’re on Rails!” page which only works in development 😩🙃

Before you spend hours researching and troubleshooting, throw this in your config/routes.rb file and see if it gets the site working:

  root to: -> (env) do
    [200, { 'Content-Type' => 'text/html' }, ['<h1>Hello world!</h1>']]

Goodbye Friday night ¯_(ツ)_/¯

Good rule of thumb on what software to use (from…):

Universal problem (e.g. a web server): use open source

Industry problem (e.g. SIS): use commercial off the shelf

Biz specific: build it yourself

Loving the tighter integration between MarsEdit 4.4 and, especially being able to edit pages!

The web interface is excellent but it’s still a web interface with all the usual downsides.

I just heard on a podcast that YouTube is the second biggest search engine in the world by traffic volume!!!

Before repeating, I thought I should fact check it. According to tubics it’s a myth… YouTube is “only” third biggest… behind Google and (wait for it) Google Images 🤯

I just listened to another great MicroConf talk, this time by Jason Cohen.

I’m really enjoying the MicroConf On Air podcast series. I find listening to a talk much more manageable than watching a video of one.

A Tale of Two Airlines

In February, we booked flights to Cambodia for July, on two different airlines with a few days stay in Singapore in between (this was before the whole world shut down due to COVID-19).

We are planning on moving there for two years to work in a small school and having to raise funds to support ourselves so we’re being very careful with our expenses. We booked with two budget airlines, offering cheap but generally non-transferrable and non-refundable flights. Scoot is the budget arm of Singapore Airlines while Jetstar is the budget arm of Qantas. I’ve already written in praise of Scoot and since then, Scoot has continued to exceed our expectations!!! Unfortunately we can’t say the same for Jetstar…

Scoot first contacted us in mid-March, long before our booked flight and as soon as it was clear the world was changing. They proactively offered us a free flight change (despite our fares being non-transferable). They also gave us a couple of months to decide whether to accept their offer which was VERY welcome in the midst of so much uncertainty.

In late May, Scoot contacted us again with an even more generous offer (plus multiple options to choose from). They let us choose between a 100% refund to our credit card (processed in the next 14 weeks) or a 120% refund in the form of travel vouchers (processed straight away). The travel vouchers were tempting but, given we needed to book new flights to Cambodia, we decided the refund made more sense.

In mid-June we received the full refund to our credit card, about 11 weeks earlier than promised. At this point our travel date was less than a month away and one airline had treated us wonderfully and refunded our flights whilst the other had not even made contact with us.

The next day we finally heard from Jetstar that our onward flight had been cancelled and that Jetstar was “offering all customers impacted by this announcement a Jetstar credit voucher to the full value of their untraveled booking”. Compared to Scoot, Jetstar was glacially slow, completely inflexible and totally unsympathetic.

Warning: Rant ahead

As it turns out, the Jetstar “Customer Guarantee” is worthless:

3. Our team are always here to help, 24 hours a day, 7 days a week

Nope! They’ve halved their hours and they offer no way of getting in touch other than chat. I had to wait almost 2 hours to chat to someone and then another 20 minutes to be connected to a supervisor. Overall the chat took more than 2 hours and 58 minutes.

4. We’ll let you know your choices if your flight is changed before you travel

Nope! They didn’t give us any choice and they waited months longer than a comparable airlines to let us know.

5. We’ll keep you updated and provide options if things don’t go to plan on the day

Nope! We received zero updates for three months (long after they stopped selling tickets for our flight so obviously they knew it wasn’t going to fly) and then they didn’t provide any options when our flight was cancelled.

6. You will get what you paid for


7. You can have confidence in how quickly we will respond to an issue

Nope. Not only did they not resolve my issue (did I mentionbut when I asked to register a formal complaint the supervisor refused. I later discovered he should have given me a case number and/or referred me to their call centre.

8. You can have confidence in how quickly we will refund your money

Nope. They refused to refund our money.

Fantastic talk by Joanna Wiebe on how to improve site copy!

Key takeaways for me were:
1) Listen to customers
2) Focus on each elements one job (e.g. a button’s one job is to be clicked)
3) Reduce friction and anxiety (talk about benefits, not work)

I really enjoyed this interview with… on Startups for the Rest Of Us:…)

Nearly skipped… when I saw it was about service-based architectures but I’m glad I didn’t because the principles seemed just as relevant to dealing with errors in a monolithic architecture (even if the approach might need to vary)

All human work is imperfect, because human nature is; and this intrinsic imperfection of human affairs cannot be overcome by procrastination. — Arnold Toynbee

Thank you Scoot!

Kudos to Scoot ( for their handling of flight disruptions. Early on in the pandemic they got in touch and gave us until the end of May to make free changes to our non-refundable, non-transferable flights (booked for July).

Now that the situation is clearer and it’s almost certain our flight won’t fly they have stepped up again and offered a 100% refund to our credit card or a 120% refund in the form of flight vouchers.

Heading up to the house for a break after back-to-back Zoom meetings, my path was blocked by an empty washing basket.

Yes I had forgotten to hang the washing, but I was still annoyed by the passive-aggressive reminder.

I know he means well but past me can be so condescending.

All hard problems are slow feedback loops in disguise — Andy Neely (


Enterprise Identity Management on Rails - RailsConf 2020

Reflections on the RailsConf 2020 talk of Brynn Gitt & Oliver Sanford

Brynn and Oliver shared from their experience implementing Single Sign On (SSO) and Identity Management with various protocols and providers. It was interesting thinking about this from the vendor point of view, most of my experience with SSO has been as a customer trying to implement and troubleshoot SSO integrations.

A key lesson up front was how to think about identity when building (or expanding) a business to business (B2B) application, to avoid painting yourself into a corner. Consider:

  • A single person may have multiple identities; they may be a member of multiple organisations, at the same time
  • A single “identifier”, such as an email address, may apply to different users at different times e.g. if an organisation reuses an old email address for a new member
  • A single organisation member may have multiple or mutable identifiers; e.g. for privileged accounts or in the case of name or email address change
  • Different organisations will allow or require different Identity Providers
  • Different Identity Providers have different implementations of the same protocols and standards

There is no single answer that will be correct in all circumstances but in most cases it makes sense to scope every person to an organisation. If you also need individual accounts there are two common ways to deal with that:

  • have many one-person organisations (at least under the hood, I imagine there’d be good arguments not to expose that to customers)
  • add individuals to a single “public” organisation (again, under the hood)

Within this first section Oliver briefly touched on the importance of observability and the need to log identity events. They can prove very useful when tuning a system or implementing new functionality.

The talk then went on to SAML (which is where most of my experience as a customer setting up SSO has been).

One tip was to use OmniAuth MultiProvider to make it easier to allow different customer organisations to each set up their own Identity Providers.

Another tip was to set up a flag you can turn on in production to log SAML assertions. This will allow you to assist customers as they figure out the required format and parameters while setting up SSO in their own organisation.

One last tip I want to remember is to use KeyCloak in development. It’s an open source Identity and Access Management system that can be used as an IDP in development (and can be launched simply using Docker).

After touching on just-in-time account provisioning, the rest of the talk covered SCIM - System for Cross-domain Identity Management. Implementing SCIM enables customers to create and manage accounts in your application directly from their Identity Provider (such as Okta). I’m very interested to dive into that topic a bit more, particularly with regards to how it might apply in the ed-tech space.

Thorough and well-explained post on a particular class of race condition (time of check to time of use) including examples of bugs they can cause and techniques to avoid or mitigate them:…

Calculating Dates in JavaScript

I was struggling recently to calculate dates using JavaScript. I wanted a minimum date to be two weeks from today but at the start of the day (midnight). In a Rails app (or a Ruby app with ActiveSupport) I would simply chain calculations on to the end of

minimum_date = + 2.weeks

I figured I could do something similar in JavaScript so I researched out how to calculate the start of today (new Date().setHours(0, 0, 0, 0)) and how to get 14 days from now (day.setDate(day.getDate() + 14)). Separately they both worked, but no matter what I tried I couldn’t combine the two:

// Non-example 1
let minimumDate = new Date().setHours(0, 0, 0, 0).getDate() + 14
> TypeError: new Date().setHours(0, 0, 0, 0).getDate is not a function.

// Non-example 2
let startOfToday = new Date().setHours(0, 0, 0, 0)
let minimumDate = startOfToday.setDate(startOfToday.getDate() + 14)
> TypeError: startOfToday.getDate is not a function.

// Non-example 3
let now = new Date()
let twoWeeksFromNow = now.setDate(now.getDate() + 14)
let minimumDate = twoWeeksFromNow.setHours(0, 0, 0, 0)
> TypeError: twoWeeksFromNow.setHours is not a function.

Eventually I learned that both setHours() and setDate() were updating their receivers but returning an integer representation of the adjusted date (milliseconds since the epoch), not the date itself1,2.

With this knowledge in hand, one way to set the minimum date would be to just use a single variable:

let minimumDate = new Date()
minimumDate.setDate(minimumDate.getDate() + 14)
minimumDate.setHours(0, 0, 0, 0)

It works and it’s quite explicit but I don’t love that the variable name is (temporarily) wrong for the first two lines. I like my code to be self documenting and easy for future me to decipher (hence why I tried to use interim variables in Examples 2 & 3 to spell out the steps I’m taking).

As part of my exploration I went back to Ruby to figure out how I would solve the same problem without ActiveSupport. The clearest way seemed to be to create two (accurately named) dates:

now =
minimum_date =, now.month, + 14)

Sure enough, we can do the same thing in JavaScript:

let now = new Date();
let minimumDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 14)

Again, that works but it relies on knowledge of how the Date constructor works (that is, it’s not very explicit). Using the explicit option but extracting it into its own method makes my intent even clearer:

let minimumDate = twoWeeksFromNowAtStartOfDay()

function twoWeeksFromNowAtStartOfDay() {
  let returnValue = new Date()
  returnValue.setDate(minimumDate.getDate() + 14)
  returnValue.setHours(0, 0, 0, 0)
  return returnValue

1 Ruby tries to follow the Principle of Least Surprise. Mutating a Date object but returning a number is very suprising to me. The principle in JavaScript seems to be “expect the unexpected”

2 I also learned that let is a way to declare a variable with a limited scope (whereas var creates a global variable). I had assumed that let was how you declared a constant and so I was surprised I was allowed to mutate the contents of the “constants”.

I later tested a variation of example 3 with const and it turns out I can mutate a date constant (see also footnote 1 about expecting the unexpected). I don’t want to go any further into this rabbit hole right now so the many questions this raises will remain unanswered for the time being.

// Using a "constant"
const now = new Date()
> undefined
> Sun May 17 2020 13:32:34 GMT+1000 (AEST)
now.setDate(now.getDate() + 14)
> 1590895954088
> Sun May 31 2020 13:32:34 GMT+1000 (AEST)
now.setHours(0, 0, 0, 0)
> 1590847200000
Sun May 31 2020 00:00:00 GMT+1000 (AEST) 

Can ActiveStorage Serve Images for the Modern Web? - RailsConf

Reflections on Mark Hutter’s RailsConf 2020 talk

Mark shows that ActiveStorage has some nice features which make it a good option for serving images but points out that some of those options come at a price. For example, serving variants on the fly can be expensive.

Mark has a pragmatic answer on how to measure “is it fast enough?” along with some best practices on ensuring images served through ActiveStorage are performant:

  • pre-process expected variants rather than doing it on the fly
  • better yet, pre-process in the background so as not to block the humans (or admin humans) uploading images your site
  • use eager loading to fetch associated attachment records (where appropriate) to prevent n+1 queries
  • consider monkey patching RepresentationsController if you need to cache ActiveStorage images cached using a CDN

Measure twice, cut once - RailsConf 2020

Reflections on Alec Clarke’s RailsConf 2020 talk

Using lessons from woodworking, Alec gave some practical tips on how to safely and repeatably write better code.

Safety First

Incremental (Staged) Rollouts

A simple but powerful pattern for incremental feature/version release:

  • incrementally roll out a feature over a certain period of time
  • ensure that once a user is using the new feature version they don’t flip flop between versions during the rollout period (giving a poor user experience)
  • provide an admin interface to abort the rollout without needing to re-deploy

Maintenance Jobs

A testable, less hands-on approach to writing and running one-off jobs (one less reason to console in to production servers)

  • create a MaintenanceJob class with a date and timestamped version number to ensure the job only ever gets run once
  • add a class method to run all pending jobs
  • add a line to the deploy script which triggers the pending jobs method

Solid Foundations

Proper Preparation

This lesson reminds me of the “make the change easy, and then make the easy change” principle (

Alec shows an example of improving (refactoring) the old version before trying to build the new version on top (behind the aforementioned staged rollout flag).

Quality Control

Building a Tool

Building on a previous example, Alec shows an example of creating a Rails generator to make it easy for future developers to create new MaintenanceJobs the right way:

  • Create a generator that builds a MaintenanceJob AND a failing test to implement
  • Build important logic into the generator so we don’t have to remember important steps

Combined, these techniques lower the barriers to doing things the right way (or at least a good way). I particularly like the way the staged rollout gives an easy way to fix a problem at what otherwise might be a very stressful time… when error monitoring is blowing up and I’m scrambling to figure out a fix.

Thank you to the Ruby Central team for organising the COVID-19 version of RailsConf… RailsConf 2020.2 - Couch Edition (

I plan to do short write-ups of each talk I watch to help cement what I learn.

I’ve used Zoom, Cisco WebEx, MS Teams, Slack and GoToMeeting for video calls or meetings of late. The Zoom experience was superior EVERY time.

But installing backdoors on Macs last year and exfiltrating iOS user data to Facebook this year makes me question Zoom’s company culture

I hoped to reinvent myself through this lockdown period… like maybe the change of routine and circumstance would be a catalyst for rapid change.

Cue: disappointment.

I often hope for a dramatic change, even though I know small, consistent, compounding changes are required.

Today I learned you can show hidden files and folders in the macOS finder with the keyboard shortcut Command-Shift-period (⌘ ⇧ .)

And researching it more, apparently this key combo got expanded to the Finder in Mojave, it has always worked in open/save dialog boxes 🤯

Today I learned you can use git to compare files, even if they aren’t in a repo!

git diff --no-index file1.txt file2.txt