This repository provides instructions and small helper scripts to obtain and refresh OAuth2 tokens for Microsoft 365.
You can then use these with:
- offlineimap3 to read/sync your mailbox via IMAP
- msmtp (or similar) to send mail via SMTP
- mutt to do both
- a small Python demo script (
demo.py) that shows both.
The basic idea:
- Use
get_token.pyonce to log in via a browser and obtain an initial refresh token. - Use
refresh_token.pywhenever you need a new access token. - Configure your IMAP/SMTP tools to authenticate with XOAUTH2 using that access token.
Your Microsoft 365 tenant must be configured to allow IMAP/SMTP with modern authentication, and your admin must grant permissions for the chosen client ID.
There are two options.
Thunderbird uses a public Azure AD application with:
Client ID: 9e5f94bc-e8a4-4e73-b8be-63364c29d753
Note that the updated version doesn't rely on a "client secret" any longer. Your organisation’s admin must once approve this app for your tenant, granting at least:
IMAP.AccessAsUser.AllSMTP.Sendoffline_access
Note that there isn't a hard connection to, for instance, Thunderbird's Azure Client ID, meaning that you could use the same ID for other tools as well (offlineimap3, msmtp, scripts).
If you manage Azure AD yourself, you can create a dedicated app registration and grant it the same permissions.
In that case, set in config.py:
ClientId = "<your client id>"
ClientSecret = "<your client secret>"The rest of the instructions stay the same. Again, notice that not all applications need both (like Thunderbird).
Modify config.py so ClientId (and optionally ClientSecret) match the client you want to use.
Then run:
pip install -r requirements.txt
python3 get_token.pyWhat happens:
- A browser window/tab opens to the Microsoft 365 login page.
- You sign in with your M365 account.
- After login you are redirected to
https://localhost:7598/. get_token.pycaptures the authorization code and exchanges it for tokens (see the terminal as well).
Note that you could see some irrelevant warnings related to certificates in the terminal.
The script writes two files in the repository directory:
imap_smtp_refresh_token– refresh token, long‑livedimap_smtp_access_token– access token, short‑lived
If the automatic browser/open redirect does not work (e.g. over SSH), the script asks you to paste the final URL manually.
You normally only need to run get_token.py once per account/client combination.
When you need a fresh access token (for SMTP or your own scripts), run:
python3 refresh_token.pyThis script:
- reads the refresh token from
imap_smtp_refresh_token - requests a new access token
- updates
imap_smtp_refresh_tokenwith the new refresh token - prints the new access token to stdout
IMAP/SMTP clients can call this script as a passwordeval/passwordcmd to always send a valid access token.
Install offlineimap3 (e.g. via pip or your distro):
pip install offlineimap3Create or edit ~/.offlineimaprc with a minimal configuration like:
[general]
accounts = M365
[Account M365]
localrepository = Local
remoterepository = Remote
[Repository Local]
type = Maildir
localfolders = ~/Maildir
[Repository Remote]
type = IMAP
remotehost = outlook.office365.com
remoteuser = <your M365 email>
ssl = yes
# OAuth2 settings
auth_mechanisms = XOAUTH2
oauth2_request_url = https://login.microsoftonline.com/common/oauth2/v2.0/token
# Thunderbird client ID (no secret)
oauth2_client_id = 9e5f94bc-e8a4-4e73-b8be-63364c29d753
oauth2_client_secret =
# Contents of the file written by get_token.py
oauth2_refresh_token = <contents of imap_smtp_refresh_token>
# Optional: skip non‑mail folders
# folderfilter = lambda folder: not folder.startswith('Calendar') and not folder.startswith('Contacts')Then run:
offlineimap3to synchronise your mailbox into ~/Maildir.
If you use your own Azure app instead of Thunderbird’s, just replace oauth2_client_id and oauth2_client_secret accordingly.
Install msmtp and create ~/.msmtprc:
account m365
host smtp.office365.com
port 587
tls on
tls_starttls on
from <your M365 email>
user <your M365 email>
auth xoauth2
# Always get a fresh access token
passwordeval "python3 /path/to/refresh_token.py"Send a test message with:
echo "Test" | msmtp -a m365 someone@example.comAny other SMTP client that can run an external command to obtain the password can use the same pattern: call refresh_token.py and treat the printed access token as the XOAUTH2 password.
Install mutt and config ~/.muttrc:
set imap_user = <your M365 email>
set folder = imaps://${imap_user}@outlook.office365.com:993/
set imap_authenticators = "xoauth2"
set imap_oauth_refresh_command = "python3 /path/to/refresh_token.py"
set smtp_url = smtp://${imap_user}@smtp.office365.com:587
set smtp_authenticators = "xoauth2"
set smtp_oauth_refresh_command = ${imap_oauth_refresh_command}demo.py is a small example script that reuses the same tokens (scripts) to access your mailbox from the local terminal.
Usage:
python3 demo.pyThe script will:
- Ask you to choose
inboxormessage. - For
inbox: fetch and print the 15 most recent emails from your INBOX using IMAP + XOAUTH2. - For
message: prompt for recipients, subject and body, then send the mail via SMTP + XOAUTH2.
Internally it uses the same refresh‑token mechanism as refresh_token.py and connects directly to:
outlook.office365.com(IMAP)smtp.office365.com(SMTP)
This is meant as a simple, readable example of how to apply the access and refresh tokens from AD.
- The refresh token in
imap_smtp_refresh_tokengrants full access to your mailbox for the configured app. Protect this file using the right permissions, encryption, keyring, etc. - Treat access tokens like passwords; they are short‑lived but still sensitive.
- If a token is compromised, revoke access by removing the app’s consent in Azure AD and re‑running
get_token.py.
config.py– client ID/secret and scope configurationget_token.py– run once to obtain initial refresh/access tokensrefresh_token.py– refreshes the access token and updates the refresh tokendemo.py– simple IMAP/SMTP demo (inbox listing + send mail)imap_smtp_refresh_token/imap_smtp_access_token– token storage filesrequirements.txt– Python dependencies (MSAL)