Skip to content

Gmail Integration

P3DROVFX edited this page Oct 12, 2026 · 1 revision

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.

Quick start

  1. 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.email and …/userinfo.profile — show your address and avatar
  2. 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).
  3. 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.


Using it

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-To and References, 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.

Settings

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.


Polling is deliberately lazy

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.

Desktop widgets and At a Glance

  • 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).

Calendar attachments

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.

AI access (read-only)

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.


Security and privacy

  • 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 and ii/.env. Nothing goes into config.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) and gmail.send, not the full https://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.com and accounts.google.com.

Troubleshooting

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

Configuration

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

Under the hood

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

Related pages

← Back to Home

Clone this wiki locally