OAuth2 for Gmail and Microsoft 365
Google and Microsoft are turning off passwords for IMAP and SMTP. With OAuth2 you do not give mutt-wizard a password at all. You authorize it once in a browser, and every sync and every sent mail asks a small helper program for a short-lived access token.
mw -a you@gmail.com -A xoauth2
mw -a you@outlook.com -A xoauth2
This replaces the app-password route for Gmail and is the only working route for Outlook.com, Hotmail, Live and Microsoft 365 once basic authentication is gone. (App passwords still work for Gmail; see the README.)
mutt-wizard ships no OAuth2 client id. You register one yourself (below) or reuse one you are allowed to use. This is deliberate: a client id belongs to an application and its owner, and the provider can block it.
What you need
- A token helper. mutt-wizard uses neomutt's
mutt_oauth2.py, which neomutt installs as/usr/share/neomutt/oauth2/mutt_oauth2.py(it is also in the neomutt source tree undercontrib/oauth2/). Python 3 is needed. To use another helper, setMW_OAUTH2_CMD(see "The helper interface"). - A client id (and, for Google, a client secret) from your provider. See "Gmail" and "Microsoft 365".
- For
mbsynconly: the Cyrus SASL XOAUTH2 plugin, a file namedlibxoauth2.soin the SASL plugin directory (/usr/lib/sasl2or/usr/lib64/sasl2). On Arch it iscyrus-sasl-xoauth2-gitfrom the AUR; elsewhere build https://github.com/moriyoshi/cyrus-sasl-xoauth2. mw warns at add time when it cannot find the plugin. Without itmbsynccannot log in, though sending withmsmtpand neomutt's online mode (-o) do not need it. passand a gpg key, as for every mw account. The helper keeps its tokens encrypted with that key.
Adding an account
Run it in a terminal:
mw -a you@gmail.com -A xoauth2
mw finds the servers as usual, then runs the helper's authorization once:
mutt_oauth2.py --authorize --verbose --email you@gmail.com --provider google ... <tokenfile>
The helper asks which flow to use, for the client id and the client secret, prints a URL, and waits for you to approve in the browser. When it finishes, mw fetches a token to test the log-on, writes the configs and tells you where the token file is. No password is stored and no pass entry is created.
Without a terminal (a script, </dev/null) mw refuses, changes nothing, and prints the helper command to run by hand. After you have run it, mw -a you@gmail.com -A xoauth2 -c finds the token file and carries on without asking anything.
If the log-on test fails after the authorization succeeded, the token file is kept, so running the same command again does not repeat the browser step. If the authorization itself fails or is interrupted, the half-written token file is removed.
-A xoauth2 cannot be combined with -x (there is no password) or -p (POP is not supported).
Gmail
You need an OAuth client of type "Desktop app" in a Google Cloud project you control:
- In the Google Cloud console create a project, configure the OAuth consent screen and create an OAuth client id of type Desktop app. The scope mw needs is
https://mail.google.com/. - While the consent screen is in "Testing", add your address as a test user. Google may expire refresh tokens of apps in testing after a short time; if syncs start failing with
invalid_grant, runmw oauth2 you@gmail.com. Moving the app to production avoids that, at the price of Google's verification warnings for a restricted scope. Check Google's current rules; they change. - Run
mw -a you@gmail.com -A xoauth2. Answer the helper's questions: flowlocalhostauthcode(Google has retired the out-of-band flow thatauthcodeuses for it), the client id, and the client secret. - IMAP must be enabled in the Gmail settings.
Google Workspace addresses on a custom domain work the same way. If mw cannot tell from the address that it is a Google account, set MW_OAUTH2_PROVIDER=google for the add.
Microsoft 365, Outlook.com, Hotmail, Live
You need an app registration in Microsoft Entra ID (the Azure portal), or a client id you are allowed to use:
- Register an application. For a personal account (Outlook.com, Hotmail, Live) it must allow personal Microsoft accounts; for a work or school account, the tenant's rules apply and an administrator may have to consent or may have disabled app registration.
- Add a "Mobile and desktop applications" platform with a redirect URI (the helper's
localhostauthcodeflow useshttp://localhost; itsauthcodeflow useshttps://login.microsoftonline.com/common/oauth2/nativeclient), and allow public client flows if you want thedevicecodeflow. - Give it the delegated permissions
IMAP.AccessAsUser.AllandSMTP.Sendfor Exchange Online, andoffline_access. - Run
mw -a you@outlook.com -A xoauth2and answer the helper's questions: the flow, the client id, and an empty client secret for a public client. - IMAP must be enabled for the mailbox, and for a work or school account SMTP AUTH (client submission) must be enabled for it too. Administrators can switch both off; mw cannot see that, only the server's refusal.
What mw writes
| Program | Setting |
|---|---|
| isync (mbsync) | AuthMechs XOAUTH2 and PassCmd "<token command>" |
| msmtp | auth xoauth2 and passwordeval "<token command>" |
| curl (the log-on test) | --login-options AUTH=XOAUTH2; the login and oauth2-bearer = "<token>" go to curl on stdin as a config (-K -), never on the command line |
neomutt, -o accounts |
imap_authenticators = "xoauth2", imap_oauth_refresh_command and smtp_oauth_refresh_command set to the token command (mw sends mail with msmtp, so the SMTP one is only for you) |
| goimapnotify | passwordCmd set to the token command and "xoauth2": true (as documented for goimapnotify; not tested here) |
<token command> is the helper plus the account's token file, for example /usr/share/neomutt/oauth2/mutt_oauth2.py --encryption-pipe 'gpg -qe -r KEYID' '/home/you/.local/share/mutt-wizard/oauth2/you@gmail.com.tokens'. It is written literally into the configs, so scheduled syncs do not depend on your environment.
msmtp supports both xoauth2 and oauthbearer. mw uses xoauth2 for every provider because Gmail and Exchange Online both document it for IMAP and SMTP, and the Cyrus plugin mbsync needs speaks only XOAUTH2.
The token file
There is one file per account:
${XDG_DATA_HOME:-~/.local/share}/mutt-wizard/oauth2/<address>.tokens
The directory has mode 700 and the file mode 600 (the default helper refuses a token file with any other mode). It holds the refresh token, so treat it like a password. mutt_oauth2.py keeps it encrypted with gpg: mw tells it to encrypt to the key ids in your password store's .gpg-id, so reading it needs the same unlocked gpg agent as pass.
mw -d (and -D) delete the account's token file with the account. To start over, delete the file and add the account again.
Several programs can ask for a token at the same moment (mbsync and msmtp in the same minute). mutt_oauth2.py does not lock its token file; a refresh that overlaps another can fail once and succeed on the next sync.
The helper interface
mw does not implement OAuth2. Any program that follows this contract works:
<cmd> <tokenfile>prints a fresh access token on the first line of stdout and exits 0. It refreshes the token itself when the stored one has expired, and it gets no input (no terminal, no stdin).<cmd> --authorize <tokenfile>runs the authorization on a terminal and creates the token file. mw also exportsMW_OAUTH2_ADDR(the address) andMW_OAUTH2_PROVIDER(googleormicrosoft, empty when unknown) to it.
Environment variables mw reads:
| Variable | Meaning |
|---|---|
MW_OAUTH2_CMD |
The helper command line, run through sh -c. It must not contain double quotes or backslashes (it is written into quoted config lines). Default: neomutt's mutt_oauth2.py, with --encryption-pipe set from your pass key. |
MW_OAUTH2_ARGS |
Extra arguments for the authorization only, for example --authflow localhostauthcode --client-id ID. With the default helper mw also passes --verbose --email ADDR --provider google|microsoft. |
MW_OAUTH2_PROVIDER |
google or microsoft, when mw cannot tell from the address or the IMAP host (default helper only). |
MW_OAUTH2_TIMEOUT |
Seconds mw waits for the helper when it tests the log-on (default 60). |
If neither MW_OAUTH2_CMD is set nor mutt_oauth2.py exists, mw stops before changing anything and says how to get one.
Renewing and checking
mw oauth2 you@gmail.comruns the authorization again for an existing OAuth2 account (the refresh token expired or was revoked), then checks that a token comes back.mw doctorruns the token command like any other secret command, then logs in to IMAP with the token. A failure namesmw oauth2as the fix.
What was not tested here
The tests use a fake helper and never touch the network. Logging in to a real Gmail or Microsoft 365 mailbox needs your own client id and account; the steps above follow the providers' public documentation and may differ from the consoles you see. Report what differs.