117 lines · 9.1 KB · Markdown
RawDownload

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

  1. 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 under contrib/oauth2/). Python 3 is needed. To use another helper, set MW_OAUTH2_CMD (see "The helper interface").
  2. A client id (and, for Google, a client secret) from your provider. See "Gmail" and "Microsoft 365".
  3. For mbsync only: the Cyrus SASL XOAUTH2 plugin, a file named libxoauth2.so in the SASL plugin directory (/usr/lib/sasl2 or /usr/lib64/sasl2). On Arch it is cyrus-sasl-xoauth2-git from the AUR; elsewhere build https://github.com/moriyoshi/cyrus-sasl-xoauth2. mw warns at add time when it cannot find the plugin. Without it mbsync cannot log in, though sending with msmtp and neomutt's online mode (-o) do not need it.
  4. pass and 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:

  1. 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/.
  2. 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, run mw 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.
  3. Run mw -a you@gmail.com -A xoauth2. Answer the helper's questions: flow localhostauthcode (Google has retired the out-of-band flow that authcode uses for it), the client id, and the client secret.
  4. 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:

  1. 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.
  2. Add a "Mobile and desktop applications" platform with a redirect URI (the helper's localhostauthcode flow uses http://localhost; its authcode flow uses https://login.microsoftonline.com/common/oauth2/nativeclient), and allow public client flows if you want the devicecode flow.
  3. Give it the delegated permissions IMAP.AccessAsUser.All and SMTP.Send for Exchange Online, and offline_access.
  4. Run mw -a you@outlook.com -A xoauth2 and answer the helper's questions: the flow, the client id, and an empty client secret for a public client.
  5. 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 exports MW_OAUTH2_ADDR (the address) and MW_OAUTH2_PROVIDER (google or microsoft, 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.com runs the authorization again for an existing OAuth2 account (the refresh token expired or was revoked), then checks that a token comes back.
  • mw doctor runs the token command like any other secret command, then logs in to IMAP with the token. A failure names mw oauth2 as 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.