Microsoft OAuth authentication with mu4e in Emacs

Posted on January 11, 2022

This post explains how to use OAuth two factor authentication (2FA) to access Microsoft Outlook email via IMAP with the mu4e email client for Emacs on Linux (and maybe MacOS, I haven't checked).

Steps

  1. Create an application on Azure. This application will return the OAuth token.

  2. Set up oauth2ms to fetch OAuth tokens from the Microsoft identity endpoint.

  3. Configure offlineimap to obtain email after calling oauth2ms to obtain the OAuth token.

  4. Configure mu4e to call offlineimap to check email.

Create an Azure app and configure oauth2ms

Follow the "Azure app setup" and "config.json configuration" sections from this guide to configure oauth2ms for obtaining an OAuth token from a Microsoft identity endpoint. When creating the Azure app, keep a note of the application's client ID and client secret, you'll need to tell offlineimap about these later.

The config.json file should be created $XDG_CONFIG_HOME/oauth2ms/config.json, which for me is ~/.config/oauth2ms/config.json.

You can now test this, which will ask you for your Microsoft login:

After creating the config file, execute oauth2ms, it should pop up a browser window asking you to login. Once logged it, it should redirect you to a page which says “Authorization complete.”.

After this first time, the oauth2ms executable will return an OAuth token to standard output.

Subsequent fetches should use the refresh token to get the access token.

Once you have oauth2ms working (i.e. it prints your OAuth token to the terminal window), move onto the next step.

Configure offlineimap

First install offlineimap3, then install oauth2ms. The instructions below assumes Python 3 is being used, and offlineimap3 (the Python 3 fork of offlineimap).

Your .offlineimaprc file needs a filename for a Python file that will call oauth2ms:

pythonfile = ~/path/to/offlineimaptoken.py

That offlineimaptoken.py Python file should contain a function that returns the oauth2ms output:

import subprocess

def get_token():
  return subprocess.getoutput("~/path/to/oauth2ms").split()[0]

Your .offlineimaprc file should include an entry that uses get_token() to obtain the OAuth access token:

[Repository Remote]
type = IMAP
remoteuser = <my email address>
remotehost = outlook.office365.com
remoteport = 993
auth_mechanisms = XOAUTH2
oauth2_request_url = https://login.microsoftonline.com/common/oauth2/v2.0/token
oauth2_client_id = <Azure app client ID>
oauth2_client_secret = <Azure app client secret>
oauth2_access_token_eval = get_token()

Now test that offlineimap uses the get_token() function to obtain an OAuth token to authenticate against the Microsoft identity endpoint:

offlineimap -c ~/path/to/.offlineimaprc

When that works, move onto the next step. If it doesn't work, check out my entire .offlineimaprc file below and copy the appropriate parts.

Configure mu4e

Set up mu4e if you haven't yet done so. Here's the official mu4e manual. After you've initialised your mu database of emails, it's useful to test mu on its own in a terminal window outside of Emacs, before trying to test mu4e in Emacs:

mu index

Receiving emails

To integrate offlineimap into your mu4e setup, you need to set the value for mu4e-get-mail-command:

(setq mu4e-get-mail-command "offlineimap.py -c /path/to/.offlineimaprc")

Now you're ready to receive emails in Emacs with mu4e, a guide is here.

If that works, then you are using OAuth 2FA to receive Microsoft Outlook email via IMAP with the mu4e email client for Emacs.

Sending emails

To use OAuth for authentication to send emails via SMTP, here is my Elisp that you would adopt in your own Emacs init file:

;;; Call the oauth2ms program to fetch the authentication token
 (defun fetch-access-token ()
   (with-temp-buffer
      (call-process "/path/to/oauth2ms/oauth2ms" nil t nil "--encode-xoauth2")
      (buffer-string)))

 ;;; Add new authentication method for xoauth2
 (cl-defmethod smtpmail-try-auth-method
   (process (_mech (eql xoauth2)) user password)
   (let* ((access-token (fetch-access-token)))
      (smtpmail-command-or-throw
       process
       (concat "AUTH XOAUTH2 " access-token)
       235)))

 ;;; Register the method
 (with-eval-after-load 'smtpmail
   (add-to-list 'smtpmail-auth-supported 'xoauth2))

(setq message-send-mail-function 'smtpmail-send-it
      starttls-use-gnutls t
      smtpmail-starttls-credentials
      '(("smtp.office365.com" 587 nil nil))
      smtpmail-default-smtp-server "smtp.office365.com"
      smtpmail-smtp-server "smtp.office365.com"
      smtpmail-stream-type  'starttls
      smtpmail-smtp-service 587
      smtpmail-debug-info t)

You should now attempt to send an email with mu4e, to ensure that the above Elisp configuration works for you.

Complete offlineimap configuration

Here is my complete offlineimap configuration file:

[general]
accounts = MyAccount
maxsyncaccounts = 1
pythonfile = ~/path/to/offlineimaptoken.py

[Account MyAccount]
localrepository = Local
remoterepository = Remote

[Repository Local]
type = Maildir
localfolders = /home/me/path/to/offlineimap-mail

[Repository Remote]
type = IMAP
remoteuser = <my email address>
remotehost = outlook.office365.com
remoteport = 993
ssl = yes
auth_mechanisms = XOAUTH2
oauth2_request_url = https://login.microsoftonline.com/common/oauth2/v2.0/token
oauth2_client_id = <Azure app client ID>
oauth2_client_secret = <Azure app client secret>
oauth2_access_token_eval = get_token()

folderfilter = lambda foldername: foldername in ["INBOX", "Sent Items"]
sslcacertfile = OS-DEFAULT