Workflows: handling inbound email on Fastmail with regular expressions (now featuring ChatGPT)

I've been using Fastmail for years now and have explored a number of different approaches to handling mail. I've approached it by creating rules targeting lists of top level domains, I've gone with no rules at all and a heavy-handed approach to unsubscribing from messages (operating under the idea that everything warrants being seen and triaged) and I've even used HEY.[1]

For now, I've approached filtering my mail by applying regular expressions to reasonably broad categories of incoming mail[2]. My thinking with this approach is that will scale better over the long term by applying heuristics to common phrases and patterns in incoming mail without the need to apply rules to senders on a per address or domain basis.

A diagram of my Fastmail workflow

Alias-specific rules

I have four aliases that I regularly provide to different services. One is for newsletters and routes them to Matter's app, another routes directly to my saved articles in the same app, another routes different messages to my Todoist inbox and a final one serves as the recovery email on my grandfather's accounts (in the event anything goes awry).

These work by checking that the To/CC/BCC matches the appropriate alias before filing them off to Archive/Newsletters, Archive/Saves or Notifications, respectively. These folders are configured to auto-purge their contents at regular intervals as they are typically consumed in the context of the application that they're forwarded to (and are only filed into folders for reference in the event something goes wrong in said applications).

A quick failsafe

In the event I've failed to tune a regular expression properly or an actual person triggers a match I have a rule that is executed after the aforementioned alias-specific rules that stops all rule evaluations for any address in my contacts.

Update: I've run every regular expression and glob pattern I apply to my messages through ChatGPT to see if it could simplify, combine and otherwise improve them (namely reducing false positives). This has worked quite well (outside the time required to coax ChatGPT to the best possible answer). Further, my deliveries rule that forwards to Parcel now also requires both a subject and body match before forwarding.

I also have a rule containing regular expressions that also skips evaluations for login pin codes, meeting/appointment reminders and common security notices.

{
    "conditions": [
      {
        "lookHow": "regexp",
        "lookFor": "(?i)\\b(PIN|Verify|Verification|Confirm|One-Time|Single(-|\\s)Use)\\b.*?(passcode|number|code.*$)",
        "lookIn": "subject"
      },
      {
        "lookHow": "regexp",
        "lookFor": "(?i)^.*upcoming (appointment|visit).*",
        "lookIn": "subject"
      },
      {
        "lookFor": "(?i)^.*new.*(sign(in|-in|ed)|(log(in|-in|ged)))",
        "lookIn": "subject",
        "lookHow": "regexp"
      },
      {
        "lookFor": "(?i)^.*(meeting|visit|appointment|event).*\\b(reminder|notification)",
        "lookIn": "subject",
        "lookHow": "regexp"
      },
      {
        "lookFor": "(?i)^.*verify.*(device|email|phone)",
        "lookIn": "subject",
        "lookHow": "regexp"
      },
      {
        "lookHow": "regexp",
        "lookFor": "(?i)^.*Apple.*(ID was used to sign in)",
        "lookIn": "subject"
      },
      {
        "lookFor": "(?i)^.*(computer|phone|device).*(added)",
        "lookIn": "subject",
        "lookHow": "regexp"
      },
      {
        "lookHow": "regexp",
        "lookFor": "(?i)^2FA.*(turned on)",
        "lookIn": "subject"
      },
      {
        "lookIn": "subject",
        "lookFor": "(?i)^.*confirm.*(you)",
        "lookHow": "regexp"
      },
      {
        "lookFor": "(?i)^.*you.*((log|sign)\\s?-?\\s?in).*$",
        "lookIn": "subject",
        "lookHow": "regexp"
      },
      {
        "lookHow": "is",
        "lookFor": "notifications@savvycal.com",
        "lookIn": "fromEmail"
      },
      {
        "lookIn": "subject",
        "lookFor": "\\b(?:RSVP|invitation|event|attend)\\b",
        "lookHow": "regexp"
      }

Mapping categories as folders

I've tailored these rules to align with folders on a per-topic basis. I have a broad Financial folder for things like receipts, bank statements and bills. That folder contains a few granular sub-folders like Deliveries, Media, Medical, Promotions and so forth. All multistep rules are set to filter messages when any of the tabled criteria matches.

The top level Financial rule looks like this.

"conditions": [
      {
        "lookFor": "([Ee]quifax.*$|[Ee]xperian.*$|[Tt]ransunion.*$|[Aa]mazon[Kk]ids.*$|[Vv]isa[Pp]repaid[Pp]rocessing.*$|americanexpress.*$|paddle.*$|instacart.*$|^.*discover.*$|^.*aaa.*$)",
        "lookIn": "fromEmail",
        "lookHow": "regexp"
      },
      {
        "lookFor": "([Gg]andi.*$|[Hh]over.*$|[Tt]ucows.*$|[Gg]o[Dd]addy.*$|[Nn]ame[Cc]heap.*$|[Vv]enmo.*$|[Pp]ay[Pp]al.*$|[Aa][Cc][Ii]payonline.*$|[Uu]se[Ff]athom.*$)",
        "lookIn": "fromEmail",
        "lookHow": "regexp"
      },
      {
        "lookHow": "regexp",
        "lookFor": "(?i)you(?:r)?[\\s-]*(?:pre[\\s-]?order|pre[\\s-]?order(?:ed))",
        "lookIn": "body"
      },
      {
        "lookIn": "toCcBccName",
        "lookFor": "*[Aa][Pp][Pp][Ll][Ee] [Cc][Aa][Rr][Dd]*[Ss][Uu][Pp][Pp][Oo][Rr][Tt]*",
        "lookHow": "glob"
      },
      {
        "lookHow": "regexp",
        "lookIn": "subject",
        "lookFor": "\\b(?i)(receipt|bill|invoice|transaction|statement|payment|order|subscription|authorized|booking|renew(al|ing)?|expir(e|ed|ing)?|deposit|withdrawl|purchased)\\b.*"
      },
      {
        "lookFor": "(?i)\\b(receipt|bill|invoice|transaction|statement|payment|order|subscription|authorized|booking|renew(al|ing)?|expir(e|ed|ing)?|deposit|withdrawl|purchased|(itunes|apple) store|credit (score|report)|manage (account|loan))\\b.*",
        "lookIn": "subject",
        "lookHow": "regexp"
      },
      {
        "lookHow": "regexp",
        "lookFor": "(?i)\\b(gift (card|certificate)|zelle|new plan|autopay|reward certificate)\\b.*",
        "lookIn": "subject"
      }
    ],

Deliveries follow a similar pattern with rule sets intended to capture messages with package tracking information or other details. I kickstarted this rule by, naturally, referencing this answer from StackOverflow.

All the regular expressions contained in this answer are matched against the Body of inbound messages before being forwarded to Parcel Email[3]. These rules are supplemented by a few edge case rules targeted at the Subject field:

"conditions": [
      {
        "lookHow": "regexp",
        "lookIn": "body",
        "lookFor": "\\b(?:1Z[\\dA-Z]{16}|[\\d]{20}|[\\d]{22}|[\\d]{26}|[\\d]{15}|E\\D{1}[\\d]{9}|[\\d]{9}[ ]?[\\d]{4})\\b"
      },
      {
        "lookIn": "subject",
        "lookHow": "regexp",
        "lookFor": "^.*[Aa] shipment (from|to).*([Ww]as|[Hh]as|is on the way).*?$"
      }
    ],

Finally, I have a rule intended to catch anything that falls through the cracks[4]:

"conditions": [
      {
        "lookFor": "usps|fedex|narvar|shipment-tracking|getconvey",
        "lookHow": "regexp",
        "lookIn": "fromEmail"
      },
      {
        "lookFor": "?(ed*x delivery manager|*ed*x.com|tracking*updates*)",
        "lookHow": "glob",
        "lookIn": "fromName"
      },
      {
        "lookFor": "(?i)^.*package (has been?|was) delivered.*$",
        "lookHow": "regexp",
        "lookIn": "subject"
      }
    ],

My medical and media rules follow a basic pattern that could be approximated using a per-line sender TLD match[5]:

"conditions": [
      {
        "lookFor": "^(?i:Disneyplus.*$|Netflix.*$|^.*hulu.*$|HBOmax.*$|MoviesAnywhere.*$|iTunes.*$|7digital.*$|Bandcamp.*$|Roku.*$|Plex.*$|Peacock.*$)",
        "lookHow": "regexp",
        "lookIn": "fromEmail"
      }
    ],

I'd recommend paring this down to match whichever media and medical providers email you.

This pattern of filtering and filing continues for several additional categories.

Financial/Tickets

"conditions": [
      {
        "lookFor": "\\b(?i)(concert|event|show|performance|ticket|admission|venue|registration)\\b",
        "lookHow": "regexp",
        "lookIn": "subject"
      }
    ],

Travel (non-forwarding)

"conditions": [
      {
        "lookHow": "regexp",
        "lookFor": "\\b(?i)(hotel|reservation|booking|dining|restaurant|travel)(s)?( |-)?(confirmation|reservations?|bookings?|details)\\b",
        "lookIn": "subject"
      },
      {
        "lookFor": "\\b(?i)(uber|lyft|rideshare)(s)?( |-)?(receipt|confirmation|ride summary|your ride with)\\b",
        "lookHow": "regexp",
        "lookIn": "subject"
      }
    ],

Travel (forwarding) These are designed to capture confirmations sent by Southwest and are sent off to Flighty before being sorted.

"conditions": [
      {
        "lookIn": "subject",
        "lookHow": "regexp",
        "lookFor": "\\b(?i)(flight|confirmation|you're going to).*\\b(reservation|on)\\b"
      }
    ],

Annoying customer support follow-ups

"conditions": [
      {
        "lookHow": "glob",
        "lookFor": "*customer*?(are|uccess|upport)",
        "lookIn": "fromName"
      }
    ],

Promotional messages (that you haven't unsubscribed from)

"conditions": [
      {
        "lookHow": "regexp",
        "lookIn": "fromEmail",
        "lookFor": "(^.*store-news.*$|^.*axxess.*$)(\\b.*?|$)"
      },
      {
        "lookFor": "^(?=.*\\b(?i)(final offer|limited time|last chance|black friday|cyber monday|holiday|christmas|free shipping|send (gift|present))\\b).*\\b(?i)(discount|save|\\d+% off|free)\\b",
        "lookIn": "subject",
        "lookHow": "regexp"
      },
      {
        "lookIn": "body",
        "lookFor": "\\b\\d{1,2}(?:\\.\\d+)?% off\\b",
        "lookHow": "regexp"
      },
      {
        "lookIn": "subject",
        "lookFor": "\\b(?:new|updated|special|limited-time)\\s+(?:offers|deals|discounts|promotions|sales)\\b",
        "lookHow": "regexp"
      }
    ],

Social networking messages These I've left as a simple list wherein any included top level domain is filed away as I don't belong to many social networks, and they change fairly infrequently.

DMARC notifications (depending on how you have your policy record configured)

"conditions": [
      {
        "lookIn": "subject",
        "lookHow": "regexp",
        "lookFor": "((^.*dmarc.*$)(\\b.*?|$))"
      },
      {
        "lookIn": "fromEmail",
        "lookHow": "regexp",
        "lookFor": "((^.*dmarc.*$)(\\b.*?|$))"
      }
    ],

That covers most of what I use to manage my mail (outside of anything particularly personal). I fully expect the regular expressions I'm using could stand to be refined and I plan on continuing to do just that. But, with that said, things have worked better than I expected so far and false positives/mis-categorizations have been infrequent.

If you have any questions or suggestions I'm all ears. Feel free to email me or ping me on Mastodon.


  1. Before, well, all that. ↩︎

  2. Fastmail has some helpful tips on regular expression rules here ↩︎

  3. Fun fact, this is, apparently, no longer being actively developed — presumably because email, as we all know, is an absolute pleasure to parse and deal with. ↩︎

  4. This rule doesn't forward over to Parcel as it typically captures secondary notices that either don't contain or duplicate the original tracking info. ↩︎

  5. I know, I called this inefficient earlier. ↩︎

Cory Dransfeldt
Cory Dransfeldt

I'm a software developer in Camarillo, California. I enjoy hanging out with my beautiful family and 4 rescue dogs, technology, automation, music, writing, reading and tv and movies.