Terminal email with Mutt: Gmail, OAuth 2.0, IMAP, and SMTP
January 14, 2025This write-up summarizes a CLI-first email stack built around Mutt on Linux, with Google Gmail over IMAP/SMTP and OAuth 2.0 for authentication—relevant when Google Workspace or policy blocks legacy app passwords.
For a personal Gmail account where app passwords remain available, mutt-wizard plus an app password is the fastest path: generate credentials in the Google Account security UI, install the wizard from GitHub, and follow its prompts. That flow assumes comfort with the shell and a working muttrc layout.
The harder case—and the one this document focuses on—is Workspace / school Gmail where app passwords are disabled. The baseline approach follows Red Hat’s Mutt + OAuth2 guidance (article). As of April 2024, the bundled mutt_oauth2.py flow in that article returned short-lived access tokens (~1 hour) without reliably persisting a refresh token from Google’s token endpoint. After expiry, the only recovery was repeating the interactive OAuth dance: copy an authorization URL to a browser, complete consent, paste the verification code back into the terminal.
The workable alternative was Google’s own oauth2.py from gmail-oauth2-tools, using its first mode of operation so the tool returns a durable refresh token alongside the access token.
Initial token generation (placeholders shown; substitute your OAuth client and secret from the Google Cloud Console):
oauth2 --user=xxx@gmail.com \
--client_id=1038[...].apps.googleusercontent.com \
--client_secret=VWF[...]OplZ \
--generate_oauth2_token
After pressing Enter, the script prints an authorization URL; completing the browser flow and pasting the verification code finishes the exchange.
A successful run on stdin/stdout looks like:
$ ./oauth2.py --user=your.gmail --client_id=your.client.id --client_secret=your.client.secret --generate_oauth2_token
To authorize token, visit this url and follow the directions:
https://accounts.google.com/long.foo.bar.url.string
Enter verification code: code.you.get.back
Refresh Token: your.refresh.token.is.here
Access Token: your.access.token.is.here
Access Token Expiration Seconds: 3599
With a refresh token stored securely, Mutt can refresh access tokens non-interactively. The relevant ~/.mutt/muttrc directives wire SASL OAUTHBEARER / XOAUTH2 on both IMAP and SMTP, delegating token refresh to the same script:
set editor = "vim"
set charset = "utf-8"
set record = ''
set imap_authenticators="oauthbearer:xoauth2"
set imap_oauth_refresh_command="/you/path/to/oauth2.py --quiet \
--user=xxx@gmail.com \
--client_id=1038[...].apps.googleusercontent.com \
--client_secret=VWF[...]OplZ \
--refresh_token=1/Yzm6M[...]oyTum4YA"
set smtp_authenticators=${imap_authenticators}
set smtp_oauth_refresh_command=${imap_oauth_refresh_command}
Mailbox and transport settings complete the integration (still no static password in the config when OAuth refresh is configured):
set from = "your@gmail.com"
set realname = "your name"
# Imap settings
set imap_user = "your@gmail.com"
# set imap_pass = "<mutt-app-specific-password>" # do not need this since we have oauth 2
# Smtp settings
set smtp_url = "smtps://your@smtp.gmail.com"
# set smtp_pass = "<mutt-app-specific-password>" # do not need this
# Remote gmail folders
set folder = "imaps://imap.gmail.com/"
set spoolfile = "+INBOX"
set postponed = "+[Gmail]/Drafts"
set record = "+[Gmail]/Sent Mail"
set trash = "+[Gmail]/Trash"
Replace placeholders with the live mailbox identity and OAuth client material; keep the refresh token out of version control.
After configuration, mutt becomes a fully terminal-native mail client on top of standard IMAP folders and SMTPS submission—useful for SSH sessions, minimal remote environments, and workflows where a GUI mail stack is undesirable.