dsh-gmail-plugins
dsh-gmail: Gmail plugin suite for DeepSeek Harness — 61 model-facing tools + 2 polling triggers over the Gmail & People APIs (OAuth2)
- Stars
- 0
- Language
- JavaScript
- Created
- Aug 25, 2026
- Updated
- Aug 25, 2026
Introduction
dsh-gmail · Gmail Plugin for DeepSeek Harness

A complete, production-ready Gmail plugin for DeepSeek Harness (DSH). It gives the agent typed, policy-aware access to Gmail over the official Gmail and People REST APIs — 63 model-facing tools (send, search, draft, label, filter, thread, settings, contacts) and 2 polling triggers — with automatic OAuth2 token management.
Official ecosystem keyword: this is a
dsh-plugin— add thedsh-pluginGitHub topic to this repository.
🤖 LLM-readable summary
- What: a single Cordis plugin that extends DSH agents with 63
gmail_*tools + 2 polling triggers. - Install:
dsh plugin --profile web add github:sakthiveltofficial/dsh-gmail-plugins, then add one row to your profile patch (or agent preset) — see Install. - Tools:
gmail_send_email,gmail_fetch_emails,gmail_fetch_message_by_message_id,gmail_fetch_message_by_thread_id,gmail_list_threads,gmail_reply_to_thread,gmail_create_email_draft,gmail_send_draft,gmail_forward_message, label/filter/trash/settings/contacts tools — the full list is in the tool table. - Auth: OAuth2 (
gmail.modify,gmail.settings.basic,gmail.compose,gmail.send,contacts.readonlyscopes). Credentials are never stored in config — env-var references resolved per operation viactx.credentials.gmail_authorizeruns the interactive Google sign-in and captures + stores the refresh token automatically; onlyGMAIL_CLIENT_ID/GMAIL_CLIENT_SECRETneed to be set. - Triggers:
gmail/message-received(new mail) andgmail/message-sent(sent mail) — poll-based, seeded on first activation so the mailbox is never replayed. - Runtime requirements: DeepSeek Harness, Node.js ≥ 20 (global
fetch), and a Google Cloud OAuth client with the Gmail (and People) API enabled. - Safety: permanent deletes (
gmail_delete_message,gmail_batch_delete_messages,gmail_delete_thread,gmail_delete_label) are clearly labeled and require explicit user confirmation; the agent is prompted to prefer trash over permanent deletion unless the user asked for irreversible removal. - License: MIT.
✨ What it does
- Read & search — fetch emails with Gmail query syntax, fetch a message by ID or a full thread, list threads, list/get drafts, download attachments, get profile/history.
- Compose & send — send email (with attachments from local paths, URLs, or inline base64), create/update/send drafts, forward messages, reply inside a thread (correct
In-Reply-To/Referencesthreading). - Organize — add/remove labels (single message, batch of 1,000, or whole thread), create/patch/update/delete labels, create/list/get/delete filters.
- Administer — IMAP/POP settings, auto-forwarding, vacation responder, display language, send-as aliases, S/MIME configs, CSE identities/key pairs, stop watch notifications.
- Contacts — get contacts (connections), get a person or Other Contacts, search people via the People API.
- One-click auth —
gmail_authorizeopens the Google consent page in your browser and captures + stores the refresh token automatically; no manual token generation. - Triggers — poll for new received/sent mail and emit typed Cordis events for downstream listeners.
- Resilience — automatic access-token refresh with in-memory caching, 401-invalidate-and-retry, bounded exponential backoff on 429/5xx, structured
GmailErrors with HTTP status preserved.
🚀 Install
Prerequisites
# DeepSeek Harness running (a profile, e.g. the default web profile)
# Node.js >= 20 (the host's Node — plugins run in-process)
# A Google Cloud OAuth client (see "Configure credentials" below)
1. Install the package from this GitHub repository
dsh plugin --profile web add github:sakthiveltofficial/dsh-gmail-plugins
This installs the @google-workspace/dsh-gmail plugin package into the profile (the repo root is the package — no build step needed).
2. Mount the plugin in a composition
The plugin publishes no services — it only registers tools into the host tools registry (plus the optional timer service for triggers) — so it mounts as a plain loose row, with no isolate realm required.
Option A — profile patch (host plane, tools visible to every agent). Append to your profile's cordis.patch.yml:
- insert:
- id: gmail
name: '@google-workspace/dsh-gmail'
config:
clientIdRef: GMAIL_CLIENT_ID
clientSecretRef: GMAIL_CLIENT_SECRET
refreshTokenRef: GMAIL_REFRESH_TOKEN
defaultUserId: me
timeoutMs: 30000
enableReceivedTrigger: false
enableSentTrigger: false
Option B — agent preset (tools only for agents on that preset). Add the row to the preset's agent.cordis.yml:
- id: gmail
name: '@google-workspace/dsh-gmail'
config:
clientIdRef: GMAIL_CLIENT_ID
clientSecretRef: GMAIL_CLIENT_SECRET
refreshTokenRef: GMAIL_REFRESH_TOKEN
Restart the profile (or the DSH process).
Verify
dsh --profile web --dump-config | grep -i gmail
Then ask the agent: "what gmail tools do you have?" — it should list the gmail_* tools (63 in total).
🔑 Configure credentials (OAuth2)
Gmail requires OAuth2 — there is no API-key path. Config carries only env-var references, never literal tokens; values are resolved per operation through DSH's credential service (process env → provider store → .env).
| Env var | Used for |
|---|---|
GMAIL_CLIENT_ID | OAuth client ID (e.g. ....apps.googleusercontent.com) — required |
GMAIL_CLIENT_SECRET | OAuth client secret (e.g. GOCSPX-...) — required |
GMAIL_REFRESH_TOKEN | long-lived refresh token — optional; when unset, run gmail_authorize and it is captured + stored automatically |
Google Cloud setup (5 steps, ~5 minutes)
Copy-paste this redirect URL — the plugin's OAuth callback listens on it:
http://127.0.0.1:8765/oauth2callback(configurable via the plugin's
redirectPortsetting; keep the two in lockstep)
- Create a project at https://console.cloud.google.com (or pick one).
- Enable the APIs: APIs & Services → Library → enable Gmail API and People API (People is only needed for the contacts tools).
- Configure the OAuth consent screen: APIs & Services → OAuth consent screen → User type External (or Internal for Workspace) → App name (e.g.
dsh-gmail) + your support email → Save. Keep it in Testing (add your Google account as a test user) or Publish it; both work for your own account. - Create the OAuth client: APIs & Services → Credentials → Create Credentials → OAuth client ID → Web application → under Authorized redirect URIs add exactly:
→ Create → copy the Client ID and Client secret.http://127.0.0.1:8765/oauth2callback - Export them (or configure
ctx.credentialssources for the same names):export GMAIL_CLIENT_ID='....apps.googleusercontent.com' export GMAIL_CLIENT_SECRET='GOCSPX-...'
The refresh token does not need to be exported — see the two options below.
Option A (recommended) — interactive sign-in from the harness
With GMAIL_CLIENT_ID and GMAIL_CLIENT_SECRET set, ask the agent to run gmail_authorize (or run it yourself): the plugin opens the Google consent page in your default browser, you sign in, and the refresh token is captured and stored automatically through the harness credential service — no manual token generation. One-time per account. gmail_auth_status reports whether a credential is stored and the state of any in-flight sign-in.
The plugin requests these scopes at consent time:
| Scope | Needed by |
|---|---|
https://www.googleapis.com/auth/gmail.modify | read/write mail, labels, trash (most tools) |
https://www.googleapis.com/auth/gmail.settings.basic | settings tools (IMAP/POP/forwarding/vacation/language/send-as) |
https://www.googleapis.com/auth/gmail.compose | drafts |
https://www.googleapis.com/auth/gmail.send | send/reply/forward |
https://www.googleapis.com/auth/contacts.readonly | contacts tools |
Option B — manual refresh token
Google OAuth Playground with your client id/secret: pick the scopes above, authorize, and copy the refresh token, then export it:
export GMAIL_REFRESH_TOKEN='1//0...'
Access tokens are minted from the refresh token on demand and cached for their lifetime; 401s invalidate the cache and retry once with a fresh exchange. Do not add gmail.metadata alongside content scopes (gmail.readonly/gmail.modify/mail.google.com) in the same consent request — Google treats it as a restricted scope and rejects the combination.
🧰 Tools
All tool names are snake_case gmail_* (e.g. gmail_send_email, gmail_fetch_emails), with the full parameter surface and the important warnings preserved: hexadecimal message IDs, label IDs vs display names, irreversible deletes.
| Area | Tools |
|---|---|
| Auth | gmail_authorize (interactive Google sign-in — captures + stores the refresh token), gmail_auth_status |
| Read | gmail_fetch_emails, gmail_fetch_message_by_message_id, gmail_fetch_message_by_thread_id, gmail_list_threads, gmail_list_messages (deprecated), gmail_get_draft, gmail_list_drafts, gmail_get_attachment |
| Compose | gmail_send_email, gmail_create_email_draft, gmail_update_draft, gmail_send_draft, gmail_forward_message, gmail_reply_to_thread |
| Organize | gmail_add_label_to_email, gmail_batch_modify_messages, gmail_modify_thread_labels, gmail_list_labels, gmail_get_label, gmail_create_label, gmail_patch_label, gmail_update_label, gmail_delete_label, gmail_remove_label (deprecated), gmail_create_filter, gmail_list_filters, gmail_get_filter, gmail_delete_filter |
| Delete/trash | gmail_move_to_trash, gmail_untrash_message, gmail_delete_message, gmail_batch_delete_messages, gmail_move_thread_to_trash, gmail_untrash_thread, gmail_delete_thread, gmail_delete_draft |
| Ingest | gmail_import_message, gmail_insert_message |
| Admin | gmail_get_profile, gmail_list_history, gmail_get_imap_settings, gmail_update_imap_settings, gmail_get_pop_settings, gmail_update_pop_settings, gmail_get_auto_forwarding, gmail_list_forwarding_addresses, gmail_get_vacation_settings, gmail_update_vacation_settings, gmail_get_language_settings, gmail_update_language_settings, gmail_list_send_as, gmail_get_send_as, gmail_patch_send_as, gmail_update_send_as, gmail_list_smime_info, gmail_list_cse_identities, gmail_list_cse_keypairs, gmail_stop_watch |
| Contacts | gmail_get_contacts, gmail_get_people, gmail_search_people |
Two tools are intentionally omitted:
GMAIL_CREATE_PROMPT_POSTandGMAIL_UPDATE_USER_ATTRIBUTES_VALUES, which target the Sanity Content Agent rather than Gmail.
Key conventions (the agent is told these in its prompt section)
- Message IDs are hexadecimal Gmail API IDs (e.g.
19b11732c1b578fd) — never UUIDs, thread IDs, subjects, or dates. Obtain them fromgmail_fetch_emails/gmail_list_threads. - Label parameters take label IDs, never display names: system labels use their uppercase name (
INBOX,UNREAD,STARRED,SPAM,TRASH,CATEGORY_UPDATES, ...); custom labels use their internal ID (Label_123, fromgmail_list_labels). - Draft IDs (
r99885592323229922) differ from message IDs;gmail_send_draftsends a draft exactly as-is and cannot add recipients. - Attachments accept a local file path, a public URL, or
{ name, mimetype, base64 }; total message size must stay under ~25 MB after base64 encoding. - Irreversible operations (
gmail_delete_message,gmail_batch_delete_messages,gmail_delete_thread,gmail_delete_label,gmail_delete_draft) bypass Trash — prefer the trash tools unless the user explicitly asked for permanent removal.
🔔 Triggers
Enable in config:
config:
enableReceivedTrigger: true # poll in:inbox → emit gmail/message-received
enableSentTrigger: true # poll in:sent → emit gmail/message-sent
triggerIntervalMinutes: 5
While the plugin is mounted, each poll emits a Cordis event on the plugin's scope with the trigger payload shapes (sender, subject, message_id, thread_id, message_text, message_timestamp, attachment_list, ...). The first poll only seeds the seen-set, so activation never replays the mailbox. Triggers run only while the session is live (agent-plane polling), like the harness schedule service.
ctx.on('gmail/message-received', (payload) => { /* ... */ })
ctx.on('gmail/message-sent', (payload) => { /* ... */ })
🧯 Troubleshooting
| Symptom | Cause / fix |
|---|---|
GMAIL_AUTH_FAILED (401) on tool calls | Access/refresh token invalid: user revoked access, changed password/2FA, a Workspace admin policy changed, or Google's ~50-refresh-token-per-account limit was hit. Re-authenticate with gmail_authorize (or refresh GMAIL_REFRESH_TOKEN manually). |
| "App is blocked" / unverified-app screen at consent | The OAuth client is requesting scopes Google hasn't verified. Remove extra scopes, or create your own OAuth app and submit scopes for verification. |
| "Gmail API has not been used in project" | The Gmail API is not enabled in the Cloud project owning the credentials. Enable it under APIs & Services, wait a few minutes, retry. |
Error 400: invalid_scope | Scope values are incorrect/misformatted in the authorization URL. Verify against the Google OAuth scopes docs. |
| Consent screen shows the wrong app name | Default consent uses the shared app. Create your own OAuth app and set a custom redirect URL (white-labeling). |
GMAIL_RATE_LIMITED (429/403) | Google enforces per-minute/daily quotas; a shared OAuth app shares its quota. Use your own client for a dedicated quota and apply exponential backoff (the client already retries 429/5xx up to 4 times). |
GMAIL_API_ERROR (400) "Invalid id value" | A non-hexadecimal ID (UUID, thread ID, subject, fabricated value) was passed as message_id. Use IDs from gmail_fetch_emails/gmail_list_threads. |
| Labels silently not applied | A display name was passed instead of a label ID. Run gmail_list_labels and use the returned Label_N IDs. |
| Trigger feels slow | Triggers poll on triggerIntervalMinutes (default 5); reduce the interval, or use Google Pub/Sub webhooks for sub-minute latency. |
🔒 Security
- Secrets are never stored in config — only env-var references (
GMAIL_CLIENT_ID/GMAIL_CLIENT_SECRET/GMAIL_REFRESH_TOKEN), resolved per operation throughctx.credentials(process env → provider store →.env). Literal config values are supported for quick setups but are not recommended for production. - Access tokens are cached in memory only, never persisted, and are refreshed from the refresh token on demand.
- Destructive tools are clearly labeled in their descriptions (
permanent,no recovery possible) and the agent is instructed to confirm with the user before permanent deletes and to prefer trash for reversible workflows. - Forward/reply recipients are explicit — forwarding preserves content, so the agent is instructed to verify recipients before forwarding to avoid unintended exposure.
- Minimum privilege: grant only the scopes your workflows need (drop
gmail.settings.basicorcontacts.readonlyif unused — an over-scoped client can trigger the "App is blocked" verification requirement).
📦 Package layout
lib/
├── index.js # plugin entry: name / inject / Config / apply
├── auth.js # OAuth2 credential resolution + token refresh
├── authorize.js # interactive Google sign-in (loopback OAuth flow)
├── client.js # Gmail + People REST client (401-refresh, backoff)
├── mime.js # RFC 2822 MIME builder + payload parsers
├── tools.js # tool factory over @deepseek-ai/dsh-tools
├── tools/ # 63 tool definitions in 9 modules
│ ├── messages.js # 14 tools
│ ├── drafts.js # 6 tools
│ ├── threads.js # 6 tools
│ ├── labels.js # 7 tools
│ ├── filters.js # 4 tools
│ ├── settings.js # 20 tools
│ ├── people.js # 3 tools
│ ├── attachments.js # 1 tool
│ └── authorize.js # 2 tools (gmail_authorize, gmail_auth_status)
├── triggers.js # polling triggers
├── cordis.yml # profile-patch row (host plane)
├── examples/ # agent.cordis.yml preset row
└── docs/assets/ # banner image
The plugin uses only Node built-ins plus four optional @deepseek-ai/* peer dependencies; no build step is required. Runtime requirements: Node.js ≥ 20 (global fetch), DeepSeek Harness.
🧪 Development & verification
node --check lib/index.js && for f in lib/*.js lib/tools/*.js; do node --check "$f"; done
A registration smoke test (import → Config({}) → apply on a stub ctx.tools → assert all 63 tools, no duplicates, no overlap with the expected tool set) is run before each release.