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).
Set up M365-IMAP to obtain OAuth tokens from the Microsoft identity endpoint.
Configure offlineimap to obtain email using OAuth tokens generated by M365-IMAP.
Configure mu4e to:
offlineimap to receive email with M365-IMAP under the hood,Clone the M365-IMAP repository:
git clone https://github.com/UvA-FNWI/M365-IMAP.git
cd M365-IMAPFollow their README.md instructions, using the "Use Thunderbird’s public client ID" option as the client ID, i.e not the "Use your own Azure AD app registration" option. Then follow the instructions in their "Obtaining auth tokens" section.
Once you have set this up, python3 get_token.py will create two
short-lived files: imap_smtp_refresh_token and imap_smtp_access_token.
For mu4e, you will run python3 refresh_token.py to create a new OAuth token
each time.
First install offlineimap with these instructions.
Your .offlineimaprc file needs knowledge of a Python file that will call
python3 refresh_token.py. In the .offlineimaprc file add the following in
the [general] section:
pythonfile = /path/to/offlineimaptoken.py
Next create that /path/to/offlineimaptoken.py. That file should contain the
following function that returns the refresh_token.py output:
import subprocess
def get_token():
return subprocess.getoutput("cd /path/to/M365-IMAP/; python3 refresh_token.py").split()[0]Your .offlineimaprc file should add your username and domain (replacing
USER@DOMAIN.COM below), and include an oauth2_access_token_eval entry to use
the get_token() function above to obtain the OAuth access token:
[Repository Remote]
type = IMAP
remoteuser = USER@DOMAIN.COM
remotehost = outlook.office365.com
remoteport = 993
auth_mechanisms = XOAUTH2
oauth2_request_url = https://login.microsoftonline.com/common/oauth2/v2.0/token
oauth2_client_id = 9e5f94bc-e8a4-4e73-b8be-63364c29d753
oauth2_access_token_eval = get_token()
Important: see the "Complete offlineimap configuration" section below for
the full .offlineimaprc configuration.
Now test that offlineimap uses the get_token() function to obtain an OAuth
token and checks your remote Microsoft outlook mail for new messages.
offlineimap -c /path/to/.offlineimaprc
When that works, move onto the next step.
Set up mu4e. 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 indexNext, set up mu4e to receive and send emails.
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, below is my Elisp that you should adopt in your own Emacs init file.
Remember to replace /path/to/M365-IMAP/ (appearing once) and
USER@DOMAIN.COM (appearing twice) with your own values.
;;; Generate the OAuth authentication token
(defun fetch-access-token ()
(let ((username "USER@DOMAIN.COM")
(m365 "/path/to/M365-IMAP/"))
(with-temp-buffer
(call-process-shell-command
(format "cd %s; printf \"user=%s\\x01auth=Bearer $(python3 refresh_token.py)\\x01\\x01\" | base64"
m365 username)
nil (current-buffer) nil)
(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-smtp-user "USER@DOMAIN.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 .offlineimaprc configuration file:
[general]
accounts = MyAccount
maxsyncaccounts = 1
pythonfile = /path/to/offlineimaptoken.py
[Account MyAccount]
localrepository = Local
remoterepository = Remote
[Repository Local]
type = Maildir
localfolders = /path/to/offlineimap-mail
[Repository Remote]
type = IMAP
remoteuser = USER@DOMAIN.COM
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 = 9e5f94bc-e8a4-4e73-b8be-63364c29d753
oauth2_access_token_eval = get_token()
folderfilter = lambda foldername: foldername in ["INBOX", "Sent Items"]
sslcacertfile = OS-DEFAULT
I updated this blog on 17th September 2026. It previously provided guidance on creating your own Azure AD application for OAuth token creation. Now the guidance is to use Thunderbird’s public Azure AD application, thus avoiding the requirement to create an Azure AD application.
If M365-IMAP is not creating OAuth tokens, check on the M365-IMAP website for the latest version of the Thunderbird client ID. It changes very infrequently, the client ID in this blog post was valid on 17th September 2026.