Repository navigation
Gmail Integration
A full Gmail client inside the shell: the Email tab of the Cheat Sheet (Super + /), a pair of desktop inbox widgets, a calendar-attachment importer for the Timetable, and read-only email tools for the AI assistant. It reads, searches, stars, trashes, replies to and sends mail for one or several Google accounts, with the Gmail API directly — no IMAP, no mail client installed, no third-party server in between.
The one thing you must bring yourself is a Google Cloud OAuth client (a free project in your own Google account). The shell does not ship shared credentials, on purpose: mail is the most sensitive thing it touches, and a key shared by every user of a dotfiles repo would be both a liability and rate-limited to death. Setup takes ten minutes once and is shared with Google Tasks, Calendar and Drive — see Google Cloud Setup.
- Create the OAuth client as described in Google Cloud Setup, enabling the Gmail API and adding these scopes to the consent screen:
-
https://www.googleapis.com/auth/gmail.modify— read, write, label, trash -
https://www.googleapis.com/auth/gmail.send— send -
https://www.googleapis.com/auth/userinfo.emailand…/userinfo.profile— show your address and avatar
-
- Open the Cheat Sheet's Email tab. It shows Gmail Setup Required: paste the Client ID and Client Secret, press Save & Apply. They are kept in the system keyring and mirrored into
ii/.env(GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET, shared with the other Google features). - Press Connect Account. Your browser opens, you sign in with the Google account you added as a test user, and approve. The tab returns to your inbox. The Welcome app has the same walkthrough as the Set up Gmail tutorial.
Publish the app. While the OAuth consent screen is in Testing, Google expires the refresh token after seven days and the tab asks you to connect again. Click Publish App ("In production") — for personal use you do not need verification; you will just see an "unverified app" warning once at sign-in.
The tab has a sidebar (Compose, a search field, your mailboxes and labels, Settings), a message list, and a reading pane.
- Mailboxes — Inbox, All Inboxes (every connected account merged), Starred, Sent, Trash, Spam, Important, Purchases and your own Gmail labels. Which of them appear, and in what order, is yours to choose in the email settings. Inbox categories (Updates, Promotions, Socials) can be switched on as well. Unread counts can be shown as badges.
- Swipe actions in the list — drag a message right to star it (in Trash, to restore it) and left to delete it (move to Trash; in Trash, delete permanently). Neighbouring cards are pulled along by a small magnetic effect, and a card turns into a pill while it is dragged. With Confirm Deletion on, a delete needs a second gesture.
-
Reading — the message body shows in the pane as rich text, with an Open in Browser button that opens a local rendered copy for mail that needs a real browser. Attachments are listed with their size; the first click downloads one to
~/Downloads(numbered if the name exists) and the second opens it. A message opened is marked read when Auto Mark as Read is on. A calendar invitation (.ics) attachment shows its event details and an add to calendar button that imports it into khal (Calendar and Timetable). - Threads — with Email Threading (beta) conversations stack into one card that expands into the whole thread, oldest first.
-
Compose — recipient, subject and a rich-text body (bold, italic, underline, strikethrough, alignment, links, images) plus file attachments from a built-in picker (Home, Documents, Downloads, Pictures). Reply quotes the message in Gmail's own style and keeps
threadId,In-Reply-ToandReferences, so the reply lands in the conversation. The draft survives closing the window and restarting the shell. The message is built as MIME and sent through the Gmail API. -
Search — the sidebar search sends your text to Gmail as a query, so Gmail's operators work (
from:,has:attachment,is:unread,label:,after:…), with paging. - Multiple accounts — Add another account in the settings runs the OAuth flow again; switching accounts is one click. Stay in settings after account switch is an option. Disconnect current account removes its tokens.
The gear in the sidebar: Accounts; Mailboxes (which mailboxes appear), Categories, Custom Tags (labels), the number of emails to load per mailbox (20 by default — higher is slower), Refresh interval (minutes), Behavior (auto mark as read, confirm deletion, threading), Display & Appearance (compact mode, avatars, snippets, body font size, semantic timestamps such as "yesterday", sidebar layout). They are stored in ~/.local/state/quickshell/user/email-settings.ini.
Mail is only refreshed while someone is looking at it. EmailService computes backgroundRefreshWanted:
- the Email tab is the visible tab of an open Cheat Sheet, or
- a desktop Email Inbox widget or the At a Glance email line is enabled.
Otherwise nothing runs: no timer, no Python. (An earlier version polled once a minute forever and spent ~150 MB of Python processes on a mailbox nobody was displaying.) When that condition turns true the service refreshes the token and syncs immediately; when it turns false it stops the fetchers. It also waits for the network: a reconnect retries a failed token refresh. Within the lazy rule, the refresh interval is the one in the settings (default every minute while visible), and an incremental historyId avoids refetching what did not change. Every mailbox is fetched in its own parallel Python process, so the inbox never waits for Sent.
- Email Inbox (1×1) and Email Inbox (2×1) — the latest messages with the unread count and a quick action to open the app, in the widget colours (see Desktop Widgets).
-
At a Glance can include an email line (
background.widgets.at_a_glance.enableEmail).
With Calendar sources and Import ICS attachments from Gmail on (see Calendar and Timetable), the active account is scanned for .ics / text/calendar parts and each one is imported once. The scanner returns no body, sender, subject or snippet — only an attachment identity and the ICS bytes, at most 20 attachments of 512 KiB per scan — and the imported keys (last 400) are remembered so a re-scan never re-applies one.
The assistant can search, get, read a thread and open in client — four tools, no write verb at all. A separate process (scripts/ai/ai_gmail.py) talks to Gmail over JSON lines on stdin/stdout; it lists metadata first, fetches a body only when asked (plainText or sanitizedHtml), returns no attachment payload, and get / thread need your approval because they expose personal content. It never touches the Email tab's models. See AI Integration.
- Own credentials, own project. Google sees an app you created; no key is shared with the developer or other users.
-
Where secrets live. Refresh tokens and account data are in the GNOME Keyring / KWallet (
gmail_accounts), the OAuth client in the keyring andii/.env. Nothing goes intoconfig.json, and presets never include them (Preset Store). -
PKCE on the local loopback flow (port 42069,
access_type=offline,prompt=consent). -
Scope and permanent delete — the app requests
gmail.modify(read, label, trash) andgmail.send, not the fullhttps://mail.google.com/scope. Moving to Trash and restoring work. Google documents that permanently deleting a message needs the full scope, so a permanent delete from Trash can be refused; if so, delete from Trash in Gmail itself (Gmail empties Trash by itself after 30 days). - Trade-off to know. The Gmail helper scripts receive the token as a command-line argument of a short-lived local process; on a shared multi-user machine another user could read it from the process list for that moment. The newer clients (Google Tasks, Calendar API, the AI Gmail tool) pass it over stdin instead.
-
Network. The only hosts contacted are
gmail.googleapis.com,oauth2.googleapis.comandaccounts.google.com.
| Symptom | Cause / fix |
|---|---|
| "Gmail Setup Required" after you configured it |
check_credentials.py found no client ID / secret; press Open .env and check the two variables |
| Browser says Access blocked: app has not completed verification | Add your account under Test users in the consent screen |
| Error 400: redirect_uri_mismatch | The client must be of type Desktop app, not Web |
| Asked to connect again after about a week | The app is in Testing; click Publish App |
Gmail API has not been used in project … |
Enable the Gmail API in that project |
| Inbox empty while the tab is open | Wait for the first sync (the header shows loading), or press the sync action; check the network |
| Mail does not update in the background | By design; open the tab or enable an Email widget |
| Where | What |
|---|---|
Config.options.cheatsheet.enableGmail |
Show the tab (default on) |
~/.local/state/quickshell/user/email-settings.ini |
Email preferences, drafts, nav order, active account |
Keyring illogical-impulse
|
gmail_accounts, apiKeys.gmail_client_id / gmail_client_secret
|
ii/.env |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET (legacy GMAIL_CLIENT_ID/SECRET still read) |
Config.options.calendar.timetable.imports.gmailIcs.* |
The attachment importer |
| Piece | Role |
|---|---|
services/EmailService.qml |
The orchestrator: accounts, tokens, one ListModel per mailbox, lazy polling, mutations (read, star, trash, restore, delete, send) |
scripts/email/ |
One Python script per operation (fetch_emails, fetch_labels, fetch_all_accounts, fetch_email_body, fetch_thread, send_email, download_email_attachment, delete_email, token_refresh, oauth_server, list_ics_attachments) — standard library only, JSON on stdout |
scripts/google/ |
The shared Google OAuth core (google_config.py — IPv4-first resolution so a dead IPv6 route cannot hang a request) |
services/KeyringStorage.qml |
Secret Service storage |
modules/ii/cheatsheet/email/ |
The tab: EmailAuth, EmailInbox, EmailContent, EmailStackedContent, EmailCompose, EmailSidebar, EmailSettings
|
services/GmailCalendarImport.qml |
The calendar importer |
modules/ii/background/widgets/utility/EmailWidget*.qml |
Desktop widgets |
- Google Cloud Setup — creating the OAuth client
- Cheat Sheet
- Calendar and Timetable
- AI Integration
- Desktop Widgets