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).
Create an application on Azure. This application will return the OAuth token.
Set up oauth2ms
to fetch OAuth tokens from the Microsoft identity
endpoint.
Configure offlineimap to obtain email after calling oauth2ms
to
obtain the OAuth token.
Configure mu4e
to call offlineimap
to check email.
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.
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.
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
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.
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
;; remove once things are working
smtpmail-debug-info t
;; force smtpmail to use use authentication
smtpmail-servers-requiring-authorization ".*"
)
You should now attempt to send an email with mu4e, to ensure that the above Elisp configuration works for you.
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