Skip to content
0

Connecting Gmail

A comprehensive guide to configuring NeoMutt with Gmail using OAuth2 authentication and GPG encryption. This step-by-step tutorial covers Google Cloud Console setup, token management, and NeoMutt configuration for the post-App Password era.

Background

I have successfully configured NeoMutt to work with two separate email accounts:

  • twine (primary): Uses maildir
  • biaget (occasional): Uses IMAP connection

However, setting up NeoMutt with my soundfreaq.com Gmail-hosted account proved challenging due to Google's security restrictions. After App Passwords were deprecated and OAuth2 became mandatory, the landscape changed significantly.

What I've Tried

Overview

Setting up NeoMutt with Gmail in the post-App Password era is a rite of passage for terminal users. This guide consolidates everything needed to configure NeoMutt + Gmail (soundfreaq.com) using OAuth2 and GPG.

The setup involves four phases:

PhaseTopicGoal
1Google Cloud ConsoleCreate OAuth2 credentials and publish the app
2GPG EncryptionSecure your tokens locally
3OAuth2 HandshakeObtain the refresh token
4NeoMutt ConfigurationWire everything together

Phase 1: Google Cloud Console Setup

This is where most people get stuck due to Google's "Verification" warnings.

1.1 Create Your Project

  1. Go to Google Cloud Console
  2. Create a new project (e.g., "MyNeoMutt")
  3. Enable the Gmail API

1.2 Create OAuth2 Credentials

  1. Navigate to APIs & Services > Credentials

  2. Click Create credentialsOAuth client ID

  3. Select Desktop App as the application type

  4. Download the JSON file and save it to:

    $HOME/.config/secrets/client_secret_1091889733448-e3tfetj4pg7nfambq2cibcpbutehqp4k.apps.googleusercontent.com.json
    Q: What is this JSON file and what role does it play?

    A: This file contains your OAuth2 application credentials—specifically the client_id and client_secret. These identify your NeoMutt installation to Google's OAuth2 server. During Phase 3, you'll provide both values to mutt_oauth2.py, which exchanges them (along with your browser grant) for a refresh token. Think of it as your app's "username and password" to Google's API.

    Q: Should I version-control this file? Is it dangerous if leaked?

    A: No, never version-control this file. While the client_id is public, the client_secret is sensitive. If leaked, an attacker could impersonate your OAuth2 application. Add *.json to ~/.config/secrets/.gitignore. If accidentally leaked, revoke it in Google Cloud Console and generate new credentials.

  1. Navigate to APIs & Services > OAuth consent screen
  2. The Problem: By default, your app is in "Testing" mode, which causes "Access blocked" errors
  3. The Solution (choose one, but publishing is recommended):
    • Add alowree@soundfreaq.com to Test Users, OR
    • Navigate to Publishing status and click Publish App

Note: Google will warn about "Verification." You can ignore this. Verification is only required if you want other people to use your app. For personal use, an "Unverified" production app works indefinitely.

Q: Why does publishing matter? What's the difference between Testing and Published mode?

A: Google requires OAuth app verification to protect users from malicious applications, but this is designed for public apps. For personal use only:

ModeToken Behavior
TestingRefresh tokens expire every 7 days, forcing weekly re-authorization
Published (Unverified)Tokens remain valid indefinitely for your single user account
Published (Verified)Required only when accessing data for multiple external users

Publishing as "Unverified" bypasses the 7-day token expiration without needing Google's security review.

Q: What happens if I skip both options (no test users, no publishing)?

A: Your app remains in "Testing" mode with no authorized users. When attempting OAuth2 authorization:

  • You'll encounter "Access blocked" or Error 403: access_denied
  • Google will refuse to issue a refresh token
  • The OAuth2 handshake cannot complete

To recover, return to Google Cloud Console and either add your email as a test user or publish the app.

Phase 2: GPG Setup

The OAuth2 token file contains sensitive credentials. GPG encryption ensures they aren't stored in plain text.

Q: Is the OAuth2 token file the same as the JSON file from 1.2?

A: No, they are two different files:

FileCreated InContentsPurpose
client_secret_*.jsonPhase 1.2 (Google Cloud Console)client_id and client_secretIdentifies your OAuth2 application to Google
.tokens (e.g., soundfreaq.tokens)Phase 3 (OAuth2 handshake)access_token, refresh_token, expiryStores your personal refresh token

The JSON file is your app's credentials (used during authorization). The .tokens file is generated by mutt_oauth2.py after browser authorization—it contains your personal refresh token that NeoMutt uses daily.

Q: Why is GPG encryption necessary?

A: The token file contains a refresh token—a long-lived credential that can generate new access tokens. If stored in plain text:

  • Theft risk: Malware or attackers can read your token and gain full Gmail access
  • No password protection: Tokens remain valid until revoked (unlike passwords you can change)
  • Bearer token: Whoever holds the token "bears" the identity—no additional auth needed

GPG encryption ensures that even if someone steals the .tokens file, they cannot use it without your GPG passphrase.

2.1 Generate a GPG Key

bash
gpg --full-generate-key

Follow the prompts with these settings:

SettingValue
Key TypeRSA and RSA (default)
Keysize3072 or 4096
Expiration0 (doesn't expire) or your preference
Real Namealowree
Emailalowree@soundfreaq.com
Commentpassphrase is your cell number
PassphraseEnter and confirm a strong passphrase
Q: Is generating a GPG key the same as "GPG encryption"?

A: Not exactly—they're related but distinct:

StepWhat It DoesAnalogy
Generate GPG key (2.1)Creates a key pair (public + private) for encryption/decryptionBuying a safe and its key
GPG encryptionUses the key to encrypt the .tokens fileLocking your tokens inside the safe

You need the GPG key first. The mutt_oauth2.py script will automatically encrypt the .tokens file using your GPG public key during Phase 3.

2.2 Configure GPG TTY

Open your ~/.zshrc (or ~/.bashrc) and add:

bash
export GPG_TTY=$(tty)
Q: Why is GPG_TTY needed?

A: Without this, GPG cannot prompt for your passphrase when NeoMutt runs in the background. This tells GPG which terminal to use for interactive prompts.

2.3 Understanding the GPG Passphrase

Q: With GPG_TTY set, why doesn't GPG ask for the passphrase every time?

A: The passphrase protects your private GPG key. Here's how it works:

  1. First use: When NeoMutt starts and needs to decrypt the token file, gpg-agent prompts for your passphrase
  2. Cached in memory: After correct entry, gpg-agent stores the unlocked private key in RAM for a configurable timeout (default: 10 minutes)
  3. Subsequent uses: During the cache window, GPG doesn't ask again—the agent handles decryption silently
  4. Session persistence: This is why you might only see the prompt once per terminal session

Security model:

  • The passphrase itself is never saved to disk
  • It lives only in volatile memory via gpg-agent
  • Once the agent cache expires or you log out, the passphrase is forgotten
  • The encrypted .tokens file remains secure on disk
Q: Does entering the passphrase mean I can access the encrypted token file?

A: Yes—entering the passphrase proves you're authorized. GPG decrypts the file in memory and passes the token to NeoMutt. The decrypted token is never written to disk.

Phase 3: OAuth2 Handshake

NeoMutt uses an external Python script to handle the "OAuth2 dance."

3.1 Understanding Refresh Tokens

Q: What is a Refresh Token and why do we need it?

A: OAuth2 uses a two-token system for security:

Token TypeLifespanPurpose
Access Token~1 hourGrants access to Gmail API; used for every email operation
Refresh TokenIndefiniteUsed only to obtain new access tokens when they expire

Why this design?

  • If an access token is stolen, it expires quickly (1 hour max)
  • The refresh token never directly accesses Gmail—it only generates new access tokens
  • You can revoke the refresh token anytime (via Google Account settings) to cut off all access
  • NeoMutt automatically runs the OAuth2 script in the background to refresh tokens before they expire

3.2 Download the Script

bash
curl -O https://raw.githubusercontent.com/neomutt/neomutt/master/contrib/oauth2/mutt_oauth2.py
chmod +x mutt_oauth2.py
mv mutt_oauth2.py ~/.local/bin/

3.3 Authorize Your Account

bash
~/.local/bin/mutt_oauth2.py --authorize --email alowree@soundfreaq.com ~/.config/secrets/soundfreaq.tokens

The script will prompt for:

PromptValue
OAuth2 registrationgoogle
Preferred OAuth2 flowlocalhostauthcode
Client ID(from your downloaded JSON file)
Client Secret(from your downloaded JSON file)

It will then provide a URL. Copy and paste this into your browser.

3.4 Complete Browser Authorization

  1. Log in with your alowree@soundfreaq.com account

  2. You'll likely see a "Google hasn't verified this app" warning. Click AdvancedGo to [Project Name] (unsafe)

  3. Grant permission

    Error Message You Might See:

    Access blocked: NeoMutt has not completed the Google verification process
    
    NeoMutt has not completed the Google verification process. The app is currently
    being tested, and can only be accessed by developer-approved testers.
    
    Error 403: access_denied

    This message appears if you haven't published the app (Phase 1.3). Return to Google Cloud Console and publish the app.

  4. After granting permission, you'll be redirected to a localhost page

  5. Copy the code= value from the browser's address bar and paste it back into your terminal

Phase 3 Summary: What You Should Have Now

Q: What should I have after correctly completing Phase 3?

A: If Phase 3 completed successfully, you should have:

ItemLocationStatus
Encrypted token file~/.config/secrets/soundfreaq.tokens (GPG-encrypted)Contains your refresh token
OAuth2 authorizationGoogle Cloud ConsoleYour app has exchanged client_id + client_secret + auth code for a refresh token
GPG decryption workinggpg-agent running with cached passphraseNeoMutt can decrypt tokens silently
Ready for Phase 4Only NeoMutt configuration remains

Verification: Run gpg -d ~/.config/secrets/soundfreaq.tokens — if it prompts for your passphrase and displays JSON with refresh_token, you're ready.

Phase 4: NeoMutt Configuration

Update your account configuration file (e.g., ~/.config/neomutt/accounts/soundfreaq):

bash
# Identity
set from = "alowree@soundfreaq.com"
set real_name = "Alowree Xu - Soundfreaq"

# OAuth2 Core Config
set imap_user = "alowree@soundfreaq.com"
set imap_authenticators = "oauthbearer"
set smtp_authenticators = "oauthbearer"

# Token Script (Use absolute paths, not tilde)
set imap_oauth_refresh_command = "/Users/alowree/.local/bin/mutt_oauth2.py /Users/alowree/.config/secrets/soundfreaq.tokens"
set smtp_oauth_refresh_command = "/Users/alowree/.local/bin/mutt_oauth2.py /Users/alowree/.config/secrets/soundfreaq.tokens"

# Connection Strings (Note the :oauth2 flag for SMTP)
set folder = "imaps://imap.gmail.com:993/"
set spool_file = "+INBOX"
set smtp_url = "smtps://alowree@soundfreaq.com:oauth2@smtp.gmail.com:465/"

# Gmail Folder Mapping
set postponed = "+[Gmail]/Drafts"
set record = "+[Gmail]/Sent Mail"
set trash = "+[Gmail]/Trash"

# Sidebar / Mailboxes
unmailboxes *
mailboxes "+INBOX" "+[Gmail]/Sent Mail" "+[Gmail]/Drafts" "+[Gmail]/Trash"

Verification & Testing

After completing all four phases, verify your setup:

bash
# Test IMAP connection
neomutt -f "imaps://imap.gmail.com:993/"

# Or simply launch NeoMutt and check if emails load
neomutt

If successful, you should see your Gmail inbox without any authentication errors.

Troubleshooting

Forgot GPG Passphrase

If you forget the GPG passphrase:

  1. Delete the .tokens file: rm ~/.config/secrets/soundfreaq.tokens
  2. Delete the GPG key: gpg --delete-secret-keys alowree@soundfreaq.com
  3. Repeat Phase 2 and Phase 3

Silent Login Behavior

With GPG_TTY configured:

  • Your OS may ask for the GPG passphrase once when you open NeoMutt
  • After that, it stays in gpg-agent memory for the rest of your session
  • Subsequent NeoMutt launches within the cache window won't prompt again

Token Expiration Issues

If tokens stop working:

  1. Verify the app is Published in Google Cloud Console (not in Testing mode)
  2. Re-run the authorization command in Phase 3.3
  3. Check that gpg-agent is running: gpgconf --list-dirs agent-socket

Alternative: Why "vygrant"?

vygrant is a modern wrapper/alternative to mutt_oauth2.py. It:

  • Automates token management
  • Integrates with system keyrings (Apple Keychain, GNOME Keyring)
  • Eliminates manual GPG file handling

Recommendation

If you're comfortable with Python scripts, stick with the mutt_oauth2.py method because:

  • It's the standard approach documented by NeoMutt
  • More transparent for troubleshooting
  • Better understanding of what's happening under the hood

Use vygrant if you prefer seamless keychain integration and don't need to understand the underlying mechanics.

Summary Checklist

  • Google Cloud project created with Gmail API enabled
  • OAuth2 credentials downloaded to ~/.config/secrets/
  • OAuth consent screen published (not in Testing mode)
  • GPG key generated with passphrase
  • GPG_TTY=$(tty) added to shell config
  • mutt_oauth2.py downloaded and made executable
  • Refresh token obtained via browser authorization
  • NeoMutt account config updated with OAuth2 settings
  • Test connection with neomutt -f imaps://imap.gmail.com:993/
最近更新