Overview
gmail-tui is a terminal Gmail client built with Go and Bubble Tea. It provides inbox browsing, search, labels, composition, replies, attachment download, and structured logging in a keyboard-first interface.
Features
- Concurrent inbox fetch with bounded workers and LRU caching.
- Compose, reply, delete, search, label browsing, and read/unread toggling.
- Safe attachment downloads with filename sanitization and deduplication.
- Configurable behavior through TOML and
GMAIL_TUI_*environment variables. - Structured JSON logging to
~/.config/gmail-tui/app.log. - Pagination, preserved search queries, and compose/reply quality-of-life improvements.
Prerequisites
- Go
1.23.3or newer. - A Google account with Gmail enabled.
- A Google Cloud project with the Gmail API enabled.
- An OAuth desktop client downloaded as
credentials.json, or pointed to byGMAIL_TUI_CREDENTIALS.
Quick Start
-
Clone the repository.
git clone https://github.com/rdx40/gmail-tui cd gmail-tui -
Create OAuth credentials in Google Cloud Console.
- Enable the Gmail API.
- Configure the OAuth consent screen.
- Create an OAuth 2.0 Desktop App client.
- Download the JSON credentials file.
-
Place the credentials file.
cp /path/to/credentials.json ./credentials.json
Or point the app to a different location:
export GMAIL_TUI_CREDENTIALS=/path/to/credentials.json -
Build the application and run it.
go build . && ./gmail-tui.exe or ./gmail-tui
-
On first launch, open the authorization URL shown in the terminal, grant access, and paste the returned code back into the app.
Configuration
Configuration is loaded in this order:
- Built-in defaults
~/.config/gmail-tui/config.tomlGMAIL_TUI_*environment variables
Example config.toml:
max_results = 10 search_max_results = 30 downloads_dir = "downloads" max_concurrent = 5 cache_max_size = 500 log_level = "INFO"
Supported environment variables:
GMAIL_TUI_MAX_RESULTSGMAIL_TUI_SEARCH_MAX_RESULTSGMAIL_TUI_DOWNLOADS_DIRGMAIL_TUI_MAX_CONCURRENTGMAIL_TUI_CACHE_MAX_SIZEGMAIL_TUI_LOG_LEVELGMAIL_TUI_CREDENTIALS
Key Bindings
Global and Inbox
| Key | Action |
|---|---|
j / k |
Move selection |
enter |
Open selected email |
c |
Compose |
d |
Move email to trash |
m |
Toggle read/unread |
l |
Open labels |
/ |
Search |
n |
Next page |
p |
Previous page |
? |
Toggle help |
q |
Quit |
Viewing
| Key | Action |
|---|---|
b / esc |
Back to inbox |
r |
Reply |
ctrl+d |
Download attachment |
Compose and Reply
| Key | Action |
|---|---|
tab |
Next field |
shift+tab |
Previous field |
ctrl+s |
Send |
ctrl+a |
Add attachment |
ctrl+x |
Remove last attachment |
esc |
Cancel or return |
Architecture Overview
The application uses a Bubble Tea Model-View-Update loop:
main.gobootstraps config, logging, the Gmail client, and the TUI.api/owns Gmail API access, OAuth, parsing, validation, and caching.ui/owns state transitions, commands, rendering, and keyboard handling.
Additional architecture notes and Mermaid diagrams live in docs/architecture.md.
Testing Instructions
Automated Checks
If make works in your environment, run the full local verification set with:
make test
make build
make coverage
make integration-test
make lintIf you are on Windows or do not have a working make setup, use direct Go commands instead.
PowerShell example:
New-Item -ItemType Directory -Force -Path .tmp\go-build, .tmp\go-tmp | Out-Null $env:GOCACHE = "$PWD\.tmp\go-build" $env:GOTMPDIR = "$PWD\.tmp\go-tmp" go test ./... go test -race -count=1 ./... go test -tags integration ./api/... go build ./... go vet ./...
Coverage:
go test -coverprofile=coverage.out ./... go test -coverprofile=coverage_api.out ./api go tool cover -func=coverage_api.out go tool cover -html=coverage.out -o coverage.html
The project expects API package statement coverage to stay at or above 80%.
Linting
If golangci-lint is installed:
Direct command:
Pre-commit hooks:
pip install pre-commit pre-commit install pre-commit run --all-files
The pre-commit configuration expects gofmt, goimports, go vet, and golangci-lint to be available locally.
Manual Smoke Test
Use a test Gmail account if possible.
-
Start the app.
-
Complete the OAuth flow on first launch.
-
Verify inbox behavior.
- Inbox loads without crashing.
j/kmoves selection.enteropens a message.nandpmove through pages when more mail exists./searches and empty results show a clear status message.lopens labels and selecting a label loads messages.
-
Verify message actions.
mtoggles read/unread.dasks for confirmation and moves the message to trash.ctrl+ddownloads an attachment and shows the resolved save path.
-
Verify compose and reply behavior.
copens compose.- Sending with an empty
Tofield showsError: recipient address is required. - Sending with an empty subject shows
Send with no subject? [y/n]. - Invalid addresses are rejected with a status-bar error.
- Missing or oversized attachments are rejected before send.
rfrom an opened email creates a reply with a normalizedRe:subject.
-
Verify logs and artifacts.
- Structured logs are written to
~/.config/gmail-tui/app.log. - Downloaded attachments stay inside the configured downloads directory.
- Structured logs are written to
Recommended Local Test Order
For day-to-day development, this is the fastest useful sequence:
go test ./...go test -tags integration ./api/...go build ./...go vet ./...golangci-lint rungo run .for a manual smoke test
OAuth Scopes
The app requests two Gmail scopes:
https://www.googleapis.com/auth/gmail.readonlyNeeded for inbox listing, message viewing, labels, and attachment download.https://www.googleapis.com/auth/gmail.sendNeeded for compose and reply operations.
If send permission is unavailable, compose and reply stay disabled and the UI shows an informational status message.
Troubleshooting
credentials file not foundConfirm thatcredentials.jsonexists or setGMAIL_TUI_CREDENTIALS.failed to decode token (corrupted file removed)Delete the token file if it still exists and re-run the app to start OAuth again.download path escapes downloads directoryThe requested attachment filename was rejected by the path-safety guard.attachment too large (max 25 MB)Gmail attachments above the 25 MB client-side limit are rejected before send.
More operational guidance lives in docs/runbook.md.
Contributing
Contribution workflow, branch naming, testing expectations, and review process are documented in CONTRIBUTING.md.