- name
- google-gmail
- description
- Read, search, triage, label, archive and send Gmail mail / threads / labels / attachments via the Gmail v1 REST API. Use when the user mentions Gmail, "my inbox", unread mail, recent emails from someone, summarising a thread, downloading an attachment, finding mail by label / query, archiving or labelling a thread, or drafting and sending a reply / new message.
- when_to_use
- Trigger when the user wants to read, list, search, summarise,
inspect, modify or send Gmail mail — including triaging the inbox,
surfacing unread, pulling a single thread, downloading an
attachment, archiving / labelling / trashing messages, or having
the AI draft and send a reply or new message on their behalf.
The installed connector always grants `gmail.readonly`; the user
also opts in to `gmail.modify` (label / archive / trash) and
`gmail.send` (compose + send) at install time — confirm the action
is in scope before issuing it.
- connections
- ["google/gmail"]
- allowed_tools
- ["Bash"]
- license
- Apache-2.0
- metadata
- {"author":"acedatacloud","version":"1.2"}
Drive Gmail via `curl + jq`. The user's OAuth bearer token is in
`$GOOGLE_GMAIL_TOKEN`; every call needs it as
`Authorization: Bearer $GOOGLE_GMAIL_TOKEN`. At minimum the token
carries `gmail.readonly` plus the identity scopes
(`openid email profile`); if the user opted in to write at install
time it also carries `gmail.modify` (label / archive / trash) and/or
`gmail.send` (compose + send). Always assume the narrowest scope
until a write actually fails — don't ask Google for new scopes from
here.
The Gmail API returns standard JSON; failures surface as
`{"error": {"code": 401|403|..., "message": "..."}}` — show that
error verbatim. `401` means the token expired (re-install). `403
insufficientPermissions` means the user didn't grant the write scope
this call needs — explain which scope is missing and suggest
re-installing the connector with the matching write box checked.
**Before any destructive write** (trashing a thread, sending an email)
show the user the exact target / draft and ask them to confirm. Don't
fan out across many messages without an explicit go-ahead.
**Always start with `users/me/profile`** to confirm the connection works
AND learn which Gmail account you're operating against. Mailbox payloads
can be huge — fetch metadata first, only `format=full` when the user
actually wants the body of a specific message.
## Optional: Google Workspace CLI (`gws`) for outbound mail
[`gws`](https://github.com/googleworkspace/cli) is Google's official CLI
(not officially supported — community-maintained on the `googleworkspace`
org). It dynamically builds its command surface from Google's Discovery
Document, exits non-zero on API errors, and ships hand-crafted helper
commands (prefixed `+`) that handle the message-encoding boilerplate.
**Use `gws` for sending mail.** The Gmail REST API requires every
outbound message to be a fully-formed RFC 822 message, base64url-encoded
into a `raw` field, with reply / forward threading carried in
`In-Reply-To` / `References` / `threadId`. The `+send / +reply /
+reply-all / +forward` helpers do all of that for you. **For everything
else** (read, search, labels, attachments) `gws` and curl are equivalent,
so the curl recipes below are usually shorter — stay on those.
### Install
```sh
npm install -g @googleworkspace/cli # or: brew install googleworkspace-cli
# Pre-built binaries also at https://github.com/googleworkspace/cli/releases
gws --version
```
### Auth
`gws` reads its OAuth bearer token from the `GOOGLE_WORKSPACE_CLI_TOKEN`
environment variable. The Gmail token used in this skill is in
`$GOOGLE_GMAIL_TOKEN`, so re-export it once at the top of every shell
block that calls `gws`:
```sh
export GOOGLE_WORKSPACE_CLI_TOKEN="$GOOGLE_GMAIL_TOKEN"
```
You can confirm the active account with `gws gmail users getProfile
--params '{"userId":"me"}'`.
### Send / reply / forward
```sh
# New message
gws gmail +send \
--to alice@example.com \
--cc team@example.com \
--subject "Q1 status" \
--body "Numbers attached."
# Reply (handles threadId, In-Reply-To, References automatically;
# To is the original sender, Subject gets the "Re: " prefix)
gws gmail +reply --message-id MSG_ID --body "Thanks — looks good."
# Reply-all
gws gmail +reply-all --message-id MSG_ID --body "+1"
# Forward to new recipients (preserves the original message body
# inline; original headers are summarised in the forward block)
gws gmail +forward --message-id MSG_ID --to bob@example.com
```
Each helper exits with a non-zero status and a JSON error on stderr if
Google rejects the request — surface that error verbatim. `+send` /
`+reply` need the `gmail.send` scope; if the user only granted
`gmail.readonly` you'll see `403 insufficientPermissions` and should ask
them to re-install the connector with the send box checked.
All the read / list / search / label / attachment recipes below are
intentionally **not** rewritten to `gws` — a one-line `curl ... | jq` is
shorter and easier to compose with shell pipelines.
## Recipes
### Verify auth (always run first)
```sh
curl -sS -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
"https://gmail.googleapis.com/gmail/v1/users/me/profile" \
| jq '{email: .emailAddress, totalMessages, totalThreads, historyId}'
```
### List recent unread inbox
```sh
curl -sS -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
--get "https://gmail.googleapis.com/gmail/v1/users/me/messages" \
--data-urlencode 'q=is:unread in:inbox newer_than:7d' \
--data-urlencode 'maxResults=20' \
| jq '.messages // [] | .[]'
```
**Always default `.messages` to `[]`** — Gmail's `messages.list` omits the
field entirely when there are zero matches (the response is just
`{"resultSizeEstimate": 0}`), so a bare `.messages[]` will crash jq
with `Cannot iterate over null (null)` and exit 5. Same applies to
`.threads`, `.labels`, `.drafts` on their list endpoints. If the result
is empty, tell the user plainly (e.g. "No unread mail in the last 7
days") instead of retrying.
The `messages.list` endpoint returns only `{id, threadId}` — you have
to fan out to `messages.get` for headers / body. Cheap pattern: list
ids → get with `format=metadata&metadataHeaders=From,Subject,Date` for
each. Use `format=full` only if the user wants the body.
### List + enrich with headers (one-shot inbox triage)
```sh
IDS=$(curl -sS -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
--get "https://gmail.googleapis.com/gmail/v1/users/me/messages" \
--data-urlencode 'q=is:unread in:inbox' \
--data-urlencode 'maxResults=10' \
| jq -r '.messages // [] | .[].id')
# If $IDS is empty the for-loop below runs zero times — tell the user
# "no unread mail" rather than echoing an empty result.
for ID in $IDS; do
curl -sS -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
--get "https://gmail.googleapis.com/gmail/v1/users/me/messages/$ID" \
--data-urlencode 'format=metadata' \
--data-urlencode 'metadataHeaders=From' \
--data-urlencode 'metadataHeaders=Subject' \
--data-urlencode 'metadataHeaders=Date' \
| jq '{id: .id, snippet: .snippet, headers: (.payload.headers | map({(.name): .value}) | add), labels: .labelIds}'
done | jq -s '.'
```
### Read a single message body (plain text and html)
```sh
ID='18f1a2b3c4d5e6f0'
RESP=$(curl -sS -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
--get "https://gmail.googleapis.com/gmail/v1/users/me/messages/$ID" \
--data-urlencode 'format=full')
echo "$RESP" | jq '{id, snippet, headers: (.payload.headers | map({(.name): .value}) | add)}'
# Body is base64url-encoded inside payload.parts[].body.data — Gmail
# splits multipart messages, so collect every text/plain or text/html
# leaf and base64url-decode them.
echo "$RESP" | jq -r '
def walk(p):
if (p.parts // null) then (p.parts | map(walk(.)) | add) else [p] end;
walk(.payload)
| map(select(.mimeType=="text/plain" and (.body.data // "") != ""))
| .[].body.data' \
| tr '_-' '/+' | base64 -d 2>/dev/null
```
If the plain-text leaf is empty, fall back to the `text/html` leaf
(same walk, swap the mimeType filter) and tell the user it's HTML.
### Read a whole thread
```sh
THREAD_ID='18f1a2b3c4d5e6f0'
curl -sS -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
--get "https://gmail.googleapis.com/gmail/v1/users/me/threads/$THREAD_ID" \
--data-urlencode 'format=metadata' \
--data-urlencode 'metadataHeaders=From' \
--data-urlencode 'metadataHeaders=Subject' \
--data-urlencode 'metadataHeaders=Date' \
| jq '{id, historyId, messages: [(.messages // [])[] | {id, snippet, from: (.payload.headers | from_entries.From), date: (.payload.headers | from_entries.Date)}]}'
```
### Search by Gmail query
```sh
# Same query DSL the Gmail UI uses: from:, to:, subject:, has:attachment,
# is:unread, label:Work, after:2026/04/01, before:2026/05/01, …
Q='from:boss@example.com subject:OKR newer_than:30d'
curl -sS -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
--get "https://gmail.googleapis.com/gmail/v1/users/me/messages" \
--data-urlencode "q=$Q" \
--data-urlencode 'maxResults=20' \
| jq '.messages // []'
```
`q` syntax reference: <https://support.google.com/mail/answer/7190> —
the model-friendly bits are `from:`, `to:`, `cc:`, `subject:`, `label:`,
`is:unread`, `is:read`, `is:starred`, `has:attachment`, `filename:pdf`,
`newer_than:7d`, `older_than:30d`, `after:YYYY/MM/DD`, `before:`, `in:inbox`,
`in:trash`. Combine with `OR` / `()` / `-`.
### List labels (system + user-defined)
```sh
curl -sS -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
"https://gmail.googleapis.com/gmail/v1/users/me/labels" \
| jq '.labels[] | {id, name, type, color: .color.backgroundColor}'
```
The system labels are `INBOX`, `SENT`, `DRAFT`, `IMPORTANT`, `UNREAD`,
`STARRED`, `SPAM`, `TRASH`, plus `CATEGORY_*` (Personal / Social /
Promotions / Updates / Forums).
### Filter by label
```sh
LABEL_ID='Label_4' # from labels.list above
curl -sS -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
--get "https://gmail.googleapis.com/gmail/v1/users/me/messages" \
--data-urlencode "labelIds=$LABEL_ID" \
--data-urlencode 'maxResults=20' \
| jq '.messages // []'
```
Multiple `labelIds` query params behave like AND.
### Download an attachment
```sh
MSG_ID='18f1a2b3c4d5e6f0'
# 1. find the attachment leaf
RESP=$(curl -sS -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
--get "https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID" \
--data-urlencode 'format=full')
echo "$RESP" | jq '
def walk(p):
if (p.parts // null) then (p.parts | map(walk(.)) | add) else [p] end;
walk(.payload)
| map(select(.body.attachmentId? != null))
| .[] | {filename, mimeType, attachmentId: .body.attachmentId, size: .body.size}'
# 2. fetch the attachment by id
ATT_ID='ANGjdJ-abc123'
OUT=/tmp/attachment.bin
curl -sS -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
"https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/attachments/$ATT_ID" \
| jq -r .data | tr '_-' '/+' | base64 -d > "$OUT"
file "$OUT"
```
### Pagination
```sh
PAGE_TOKEN=''
while : ; do
RESP=$(curl -sS -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
--get "https://gmail.googleapis.com/gmail/v1/users/me/messages" \
--data-urlencode 'q=in:inbox' \
--data-urlencode 'maxResults=100' \
${PAGE_TOKEN:+--data-urlencode "pageToken=$PAGE_TOKEN"})
echo "$RESP" | jq -c '.messages[]?'
PAGE_TOKEN=$(echo "$RESP" | jq -r '.nextPageToken // empty')
[ -z "$PAGE_TOKEN" ] && break
done
```
## Write recipes
These all need `gmail.modify` (label / archive / trash) or
`gmail.send` (compose + send). If the user only granted
`gmail.readonly` at install you'll get `403 insufficientPermissions`
— surface that and ask them to re-install with the write boxes
checked.
### Mark a message read / unread, star it, archive it (gmail.modify)
```sh
MSG_ID='18f1a2b3c4d5e6f0'
# Mark as read = remove the UNREAD label
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
-H 'Content-Type: application/json' \
GitHubで見る