gmail
Read, search, send, reply, label, and trash Gmail over OAuth2, and receive new-email events in real time — authorized through Cremind Connect, tokens stay local.
The gmail skill is a Python CLI plus an event listener for Gmail over OAuth2. It can read, search, send, reply to, label, and trash messages, and it can receive new-email events in real time. Authorization goes through the Cremind Connect service, so you never touch a Google Cloud project — and the tokens it mints stay on your machine. It runs via uv (PEP 723 inline metadata), so there's no environment to set up.
This page distills the skill's shipped SKILL.md. The skill is one of the built-ins Cremind copies into every profile, so it's available out of the box.
How it works (token-less relay)
The skill separates actions from events, and the relay never sees your data:
- Actions (list, send, …) call the Gmail API directly with your local token.
- Events: the listener calls Gmail
users.watch()into the org's Pub/Sub topic (from the relay's discovery doc), then connects a WebSocket to the relay and proves account control with a short-lived Google ID token. When mail arrives, the relay sends a content-freeresyncnudge; the listener then runshistory.list()locally and writes the new message toevents/new_email/. - The same account linked in two Cremind apps receives events in both — the relay fans out to every connected app for that account.
The OAuth code-to-token exchange happens locally (loopback PKCE), and the token is stored only on this machine at scripts/.google_token.json. See Local Tokens & Secrets.
Setup
No configuration is required by default. CREMIND_CONNECT_URL defaults to https://connect.cremind.io, and the OAuth GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET are fetched dynamically from Cremind Connect (GET /credentials/google) so the org can rotate them without a client update. Set any of these in scripts/.env (or via the Settings UI) only to override:
CREMIND_CONNECT_URL=https://connect.cremind.io # optional; this is the default
GOOGLE_CLIENT_ID= # optional; otherwise fetched from Cremind Connect
GOOGLE_CLIENT_SECRET= # optional; otherwise fetched from Cremind ConnectLinking the account
Link the account with the link subcommand:
uv run scripts/__main__.py linklink prints a Google consent URL and then waits in the background for consent to complete. Surface that URL to the user and ask them to open it and approve access. The consent redirect is received by the always-running Cremind backend (a persistent loopback listener), so linking completes even though the command keeps running in the background. Once the user confirms they've approved, check status:
uv run scripts/__main__.py status--no-browser only affects the standalone fallback used when the Cremind backend isn't running; under the app the URL is always printed for the user.
CLI commands
Run uv run scripts/__main__.py <subcommand>. Output is JSON (human-readable on a TTY; force JSON with --json).
| Subcommand | Required | Optional |
|---|---|---|
link | — | --no-browser |
status | — | — |
list | — | --query, --max-results (10), --detail summary|full |
search | --query | --max-results (10), --detail summary|full |
get | --id | — |
send | --to (repeatable), --subject | --cc, --bcc (repeatable), --body/--body-file/stdin |
reply | --id | --cc, --bcc, body via --body/--body-file/stdin |
trash | --id | — |
watch | — | establish the Gmail watch once; the listener does this automatically |
unwatch | — | — |
--id is the Gmail message id (from list/search). --query uses Gmail search syntax, e.g. from:alice newer_than:7d.
Examples
uv run scripts/__main__.py status
uv run scripts/__main__.py list --max-results 5
uv run scripts/__main__.py search --query "from:boss is:unread"
uv run scripts/__main__.py get --id 1923abc...
uv run scripts/__main__.py send --to a@b.com --subject "Hi" --body "Hello there"
uv run scripts/__main__.py reply --id 1923abc... --body "Thanks!"
uv run scripts/__main__.py trash --id 1923abc...The new_email event
The skill declares one event type, new_email, fired when a new message arrives in the Gmail INBOX. The listener:
uv run scripts/event_listener.pyBehavior:
- Baseline on first run — records the current
historyId; emits nothing for existing mail. - Live — on each relay
resyncnudge, runs incrementalhistory.list()and writes new INBOX messages toevents/new_email/<YYYY-MM-DDTHH-MM-SS> <subject>.md. - Catch-up — on startup it also syncs anything that arrived while offline.
- Watch renewal — re-calls
users.watch()well within Google's 7-day limit. - Offline more than ~7 days — if the
historyIdis too old the cursor is reset, and the bounded gap is not replayed (by design — no full-mailbox dump). - State —
scripts/.listener_state.json(gitignored). Shuts down onSIGINT/SIGTERM.
Start and monitor the listener with the event CLI (see Event Subscriptions):
cremind skill-events listener-start gmail
cremind skill-events listener-status gmailEvent markdown schema
Each new email is dropped as a markdown file with structured frontmatter and a plain-text body:
---
id: "1923abc..."
thread_id: "1923a..."
message_id: "<CABc...@mail.gmail.com>"
from: "Alice <alice@example.com>"
to: "you@gmail.com"
cc: ""
subject: "Lunch?"
date: "Fri, 06 Jun 2026 09:00:00 +0000"
labels: ["INBOX", "UNREAD"]
event_type: "new_email"
received_at: "2026-06-06T09:00:05+00:00"
---
<plain-text body>Troubleshooting
| Symptom | Fix |
|---|---|
Account not linked | Run uv run scripts/__main__.py link. |
No GOOGLE_CLIENT_SECRET available | Cremind Connect must be reachable (it serves the secret), or set it in scripts/.env to override. |
Google did not return a refresh token | Revoke at myaccount.google.com/permissions and re-link. |
| No events arriving | Confirm the listener is running, that link used openid email scopes, and that the relay is reachable (curl $CREMIND_CONNECT_URL/.well-known/cremind-connect). |
| Can't link a restricted scope | While the org's consent screen is in "Testing", only added test users can link. |
Module layout
gmail/
├── SKILL.md
├── events/new_email/ # markdown drop-zone
└── scripts/
├── .env # optional overrides (creds fetched from Cremind Connect by default)
├── __main__.py # CLI entry
├── event_listener.py # listener entry
└── app/
├── config.py # env + paths + logging
├── gmail_api.py # Gmail API wrapper (watch/history/list/get/send/...)
├── formatter.py # message parsing + markdown
├── listener.py # watch lifecycle + relay client + incremental sync
├── cli.py # argparse + dispatch
└── google/ # shared: account_key, discovery, auth (PKCE), relay_clientWriting Documents for RAG
The in-app documents/*.md format that feeds the documentation_search tool — how the frontmatter description is indexed and the body fetched on demand.
gcalendar
List, view, create, update, and delete Google Calendar events over OAuth2, with real-time calendar-change events — authorized through Cremind Connect.