OAuth for Email

OAuth for Email

Modern email providers (Gmail, Microsoft 365, Yahoo) require OAuth2 for API access — passwords are deprecated. OAuth lets a third-party app act on behalf of a user without ever seeing their password, and lets the user revoke access anytime without changing it.

For email clients specifically, the relevant OAuth flow is the authorization code flow, sometimes via the installed app variant for desktop clients.

The flow at a glance

1. Email client opens browser → "Sign in to Gmail"
2. User authenticates with Google directly (client never sees password)
3. User approves: "MyEmailClient wants to read your email"
4. Google redirects browser back to client (e.g. http://localhost:8080/callback?code=AUTH_CODE)
5. Client exchanges AUTH_CODE for tokens:
   POST https://oauth2.googleapis.com/token
   Returns: { access_token, refresh_token, expires_in: 3600 }
6. Client uses access_token in API requests:
   Authorization: Bearer <access_token>
7. When access_token expires (~1h), client uses refresh_token to get a new one:
   POST https://oauth2.googleapis.com/token (grant_type=refresh_token)
   Returns: { access_token, expires_in: 3600 }

The refresh token is long-lived (Google: indefinite unless revoked). The access token is short-lived. The client juggles both.

Why the desktop variant is awkward

Web apps have a server with a known callback URL. Desktop apps don't.

Workaround: the client spins up a temporary HTTP server on localhost:RANDOM_PORT, opens the user's browser pointed at Google's authorize URL with redirect_uri=http://localhost:RANDOM_PORT. Google redirects the browser back to localhost; the client's server captures the code and shuts down.

This is the installed app flow (Google's name) or loopback redirect. Standardised in RFC 8252 (OAuth 2.0 for Native Apps).

Variants when no browser is available:

Scopes — what permissions to ask for

Less is more. OAuth's promise is fine-grained access; using it well means asking only for what you need.

Gmail scopes:

mxr asks for readonly, modify, labels plus gmail.send if outbound is configured. Doesn't request the omnibus mail.google.com/ scope.

Refresh tokens and storage

Refresh tokens are sensitive — possession is access. Stored:

mxr stores tokens at ~/.local/share/mxr/tokens/{token_ref}.json with file mode 0600. Future work moves them to a system keychain.

Revocation: user can revoke from Google's account settings anytime. Refresh fails with a 401; client should detect and prompt for re-auth.

App verification

Google requires OAuth apps using sensitive scopes (Gmail) to be verified for production use. Unverified apps:

Verification involves:

This is why most open-source email clients (Thunderbird, mxr, K-9 Mail) have users go through the warning, or use their own OAuth credentials.

Bundled credentials in mxr

mxr ships with its own Google OAuth client ID/secret baked into the binary at compile time. Users can override in config.toml. Tradeoff:

OAuth for SMTP (XOAUTH2)

Most modern SMTP servers support OAuth2 via the XOAUTH2 mechanism. The client builds an SMTP AUTH XOAUTH2 string from the user's email + access token and submits it. Server validates the token with the OAuth provider.

mxr's crates/provider-smtp supports XOAUTH2 when the account is configured with OAuth (Gmail, Microsoft 365). Otherwise falls back to PLAIN/LOGIN with a password.

Common pitfalls

See also