The great 2025 email yak-shave: O365 + mbsync + mu + neomutt + msmtp

For years I was a happy user of mu4e in Emacs. But then a few years ago my employer turned off password-based IMAP auth and broke my (Office 365-based) work email, so I had to make alternative email arrangements.

I’ve recently rebuilt my entire email setup around neomutt (in Zed’s built-in terminal). I always knew that there was some way to do the Office365 OAuth2 dance and hook things back up, so I took the plug and shaved the email yak again. And here, dear reader, are the results—may you not waste as many hours messing around as I did.

The moving parts

The new setup consists of:

  • mbsync (built from source with SASL support) for IMAP sync with OAuth2
  • cyrus-sasl-xoauth2 mbsync plugin to handle the OAuth dance
  • mu for fast email search and indexing
  • neomutt as the email client
  • msmtp for SMTP sending
  • macOS Keychain for secure token storage

Each tool does one thing well, which is the Unix way—even if it means more configuration files to maintain.

OAuth2: the tricky bit

Getting OAuth2 working with Office365 was the gnarliest part. You need to use the mutt_oauth2.py script with Thunderbird’s client ID (9e5f94bc-e8a4-4e73-b8be-63364c29d753) and the devicecode flow, since localhostauthcode doesn’t work with the way my O365 exchange server is set up.

Here’s a snippet from my mbsyncrc showing how the OAuth token gets passed:

IMAPAccount anu
Host outlook.office365.com
Port 993
AuthMech XOAUTH2
User [email protected]
PassCmd "/Users/ben/.dotfiles/mail/mutt_oauth2.py \
  --decryption-pipe 'security find-generic-password -a [email protected] -s mutt_oauth2_anu -w' \
  --encryption-pipe '/Users/ben/.dotfiles/mail/keychain-store.sh [email protected] mutt_oauth2_anu' \
  /Users/ben/.dotfiles/mail/anu_oauth2_keychain_stub"

The keychain-store.sh wrapper script ensures tokens are stored securely in macOS Keychain rather than sitting around in plaintext files. If you’re on Linux you can switch my macOS-specific approach with suitable pass or gpg invocations.

I needed to build mbsync and the cyrus-sasl-xoauth2 plugin from source with XOAUTH2 support (something I plan to upstream to the homebrew formula when I get a chance).

Running in Zed

Since I’m a Zed user, I run neomutt in a fullscreen terminal task (same approach as my Claude Code setup). Add this to your tasks.json:

{
  "label": "mutt",
  "command": "neomutt",
  "reveal": "always",
  "use_new_terminal": true,
  "allow_concurrent_runs": false
}

I bind this task to a keyboard shortcut, then I’m one key command away from a fullscreen email client with all the Zed terminal niceties.

The payoff

Yes, it was a yak-shave. But now I have:

  • full control over my email workflow
  • lightning-fast search with mu (I tried notmuch, but the mu setup allows me to use normal IMAP folders—and that’s important because I have to check my email from multiple devices)
  • OAuth2 working seamlessly with Office365
  • everything running in my preferred editor
  • the tantalising prospect of replacing all the email parts of my job with a series of shell scripts (and claude code invocations)

For the full config files and detailed setup instructions, check out my full email config on GitHub. Fair warning: you’ll probably need to tweak things for your specific setup, but that’s half the fun.

github bluesky vimeo graduation-cap rss envelope magnifying-glass vial mug-hot heart creative-commons creative-commons-by creative-commons-nc creative-commons-sa