Loading...
Loading...
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.
npx skill4agent add acedatacloud/skills google-gmailcurl + jq$GOOGLE_GMAIL_TOKENAuthorization: Bearer $GOOGLE_GMAIL_TOKENgmail.readonlyopenid email profilegmail.modifygmail.send{"error": {"code": 401|403|..., "message": "..."}}401403 insufficientPermissionsusers/me/profileformat=fullgwsgwsgoogleworkspace+gwsrawIn-Reply-ToReferencesthreadId+send / +reply / +reply-all / +forwardgwsnpm install -g @googleworkspace/cli # or: brew install googleworkspace-cli
# Pre-built binaries also at https://github.com/googleworkspace/cli/releases
gws --versiongwsGOOGLE_WORKSPACE_CLI_TOKEN$GOOGLE_GMAIL_TOKENgwsexport GOOGLE_WORKSPACE_CLI_TOKEN="$GOOGLE_GMAIL_TOKEN"gws gmail users getProfile --params '{"userId":"me"}'# 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+send+replygmail.sendgmail.readonly403 insufficientPermissionsgwscurl ... | jqcurl -sS -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
"https://gmail.googleapis.com/gmail/v1/users/me/profile" \
| jq '{email: .emailAddress, totalMessages, totalThreads, historyId}'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 // [] | .[]'.messages[]messages.list{"resultSizeEstimate": 0}.messages[]Cannot iterate over null (null).threads.labels.draftsmessages.list{id, threadId}messages.getformat=metadata&metadataHeaders=From,Subject,Dateformat=fullIDS=$(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 '.'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/nulltext/htmlTHREAD_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)}]}'# 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 // []'qfrom:to:cc:subject:label:is:unreadis:readis:starredhas:attachmentfilename:pdfnewer_than:7dolder_than:30dafter:YYYY/MM/DDbefore:in:inboxin:trashOR()-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}'INBOXSENTDRAFTIMPORTANTUNREADSTARREDSPAMTRASHCATEGORY_*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 // []'labelIdsMSG_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"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
donegmail.modifygmail.sendgmail.readonly403 insufficientPermissionsMSG_ID='18f1a2b3c4d5e6f0'
# Mark as read = remove the UNREAD label
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
-H 'Content-Type: application/json' \
--data '{"removeLabelIds":["UNREAD"]}' \
"https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/modify"
# Star it = add the STARRED label
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
-H 'Content-Type: application/json' \
--data '{"addLabelIds":["STARRED"]}' \
"https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/modify"
# Archive = remove from INBOX (keeps in All Mail)
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
-H 'Content-Type: application/json' \
--data '{"removeLabelIds":["INBOX"]}' \
"https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/modify"modifyaddLabelIdsremoveLabelIds/threads/$THREAD_ID/modify# 1. find or remember the label id from labels.list
LABEL_ID='Label_4'
MSG_ID='18f1a2b3c4d5e6f0'
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
-H 'Content-Type: application/json' \
--data "{\"addLabelIds\":[\"$LABEL_ID\"]}" \
"https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/modify"curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
-H 'Content-Type: application/json' \
--data '{"name":"Follow up","messageListVisibility":"show","labelListVisibility":"labelShow"}' \
"https://gmail.googleapis.com/gmail/v1/users/me/labels" \
| jq '{id, name}'MSG_ID='18f1a2b3c4d5e6f0'
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
"https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/trash"
# Whole thread:
THREAD_ID='18f1a2b3c4d5e6f0'
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
"https://gmail.googleapis.com/gmail/v1/users/me/threads/$THREAD_ID/trash"/untrashmessages.delete# Compose the message
TO='alice@example.com'
SUBJECT='Quick hello'
BODY='Hi Alice,
Just a quick test note from the AceDataCloud Gmail connector.
Best,
Qingcai'
# Multi-line subject lines need MIME encoded-word for non-ASCII; ASCII is fine raw.
RAW=$(printf 'To: %s\r\nSubject: %s\r\nContent-Type: text/plain; charset=UTF-8\r\nMIME-Version: 1.0\r\n\r\n%s' \
"$TO" "$SUBJECT" "$BODY" \
| base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
-H 'Content-Type: application/json' \
--data "{\"raw\":\"$RAW\"}" \
"https://gmail.googleapis.com/gmail/v1/users/me/messages/send" \
| jq '{id, threadId, labelIds}'SUBJECT_RAW='你好,季度复盘草稿'
SUBJECT_ENCODED="=?UTF-8?B?$(printf %s "$SUBJECT_RAW" | base64)?="In-Reply-ToReferencesORIG_MSG_ID='18f1a2b3c4d5e6f0'
ORIG=$(curl -sS -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
--get "https://gmail.googleapis.com/gmail/v1/users/me/messages/$ORIG_MSG_ID" \
--data-urlencode 'format=metadata' \
--data-urlencode 'metadataHeaders=Message-ID' \
--data-urlencode 'metadataHeaders=Subject' \
--data-urlencode 'metadataHeaders=From')
MID=$(echo "$ORIG" | jq -r '.payload.headers | from_entries | .["Message-ID"] // .["Message-Id"]')
FROM=$(echo "$ORIG" | jq -r '.payload.headers | from_entries | .From')
SUBJ=$(echo "$ORIG" | jq -r '.payload.headers | from_entries | .Subject')
TID=$(echo "$ORIG" | jq -r .threadId)
RAW=$(printf 'To: %s\r\nSubject: Re: %s\r\nIn-Reply-To: %s\r\nReferences: %s\r\nContent-Type: text/plain; charset=UTF-8\r\nMIME-Version: 1.0\r\n\r\n%s' \
"$FROM" "$SUBJ" "$MID" "$MID" \
'Replying inline — will follow up later today.' \
| base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
-H 'Content-Type: application/json' \
--data "{\"raw\":\"$RAW\",\"threadId\":\"$TID\"}" \
"https://gmail.googleapis.com/gmail/v1/users/me/messages/send" \
| jq '{id, threadId}'threadIdIn-Reply-Torawgmail.senddraftscurl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
-H 'Content-Type: application/json' \
--data "{\"message\":{\"raw\":\"$RAW\"}}" \
"https://gmail.googleapis.com/gmail/v1/users/me/drafts" \
| jq '{id, message: {id: .message.id, threadId: .message.threadId}}'| HTTP | meaning | what to tell the user |
|---|---|---|
| token expired / revoked | "Reconnect the Gmail connector on the Connections page." |
| scope missing | identify which scope ( |
| quota / throttling | back off ~5s, then retry once. |
| wrong message / thread / attachment id | double-check the id, or fall back to |
| malformed | print the |
$GOOGLE_GMAIL_TOKEN