google-gmail

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
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.
通过
curl + jq
操作Gmail。用户的OAuth bearer token存储在
$GOOGLE_GMAIL_TOKEN
中;每次调用都需要将其作为
Authorization: Bearer $GOOGLE_GMAIL_TOKEN
传入。该令牌至少需要包含
gmail.readonly
权限以及身份范围(
openid email profile
);如果用户在安装时选择了写入权限,那么它还会包含
gmail.modify
(标记/归档/删除)和/或
gmail.send
(撰写+发送)权限。在写入操作实际失败前,始终假设使用最窄的权限范围——不要在此处向Google请求新的权限范围。
Gmail API返回标准JSON格式;失败时会返回
{"error": {"code": 401|403|..., "message": "..."}}
——直接显示该错误信息。
401
表示令牌已过期(重新安装)。
403 insufficientPermissions
表示用户未授予该调用所需的写入权限——说明缺少的权限范围,并建议用户勾选对应的写入选项重新安装连接器。
在执行任何破坏性写入操作前(删除对话、发送邮件),向用户显示确切的目标/草稿内容并请求确认。未经明确许可,不要批量操作多个消息。
**始终先调用
users/me/profile
**来确认连接正常,同时了解当前操作的Gmail账户。邮箱负载可能非常大——先获取元数据,仅当用户确实需要查看特定邮件的正文时才使用
format=full

Optional: Google Workspace CLI (
gws
) for outbound mail

可选方案:用于发送邮件的Google Workspace CLI(
gws

gws
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.
gws
是Google官方的CLI工具(非官方支持——由
googleworkspace
组织社区维护)。它从Google的Discovery Document动态构建命令界面,API错误时返回非零退出码,并提供精心设计的辅助命令(以
+
为前缀)来处理消息编码的繁琐工作。
使用
gws
发送邮件
。Gmail REST API要求每一封外发邮件都是完整的RFC 822格式消息,经过base64url编码后放入
raw
字段,回复/转发的对话关系通过
In-Reply-To
/
References
/
threadId
传递。
+send / +reply / +reply-all / +forward
辅助命令会自动处理所有这些操作。对于其他所有操作(读取、搜索、标记、附件),
gws
和curl功能相当,因此下面的curl示例通常更简洁——优先使用这些示例。

Install

安装

sh
npm install -g @googleworkspace/cli   # or: brew install googleworkspace-cli
sh
npm install -g @googleworkspace/cli   # or: brew install googleworkspace-cli
gws --version
undefined
gws --version
undefined

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"}'
.
gws
GOOGLE_WORKSPACE_CLI_TOKEN
环境变量读取OAuth bearer token。本技能中使用的Gmail token存储在
$GOOGLE_GMAIL_TOKEN
中,因此在每个调用
gws
的shell块顶部重新导出一次:
sh
export GOOGLE_WORKSPACE_CLI_TOKEN="$GOOGLE_GMAIL_TOKEN"
你可以通过
gws gmail users getProfile --params '{"userId":"me"}'
确认当前账户。

Send / reply / forward

发送/回复/转发

sh
undefined
sh
undefined

New message

New message

gws gmail +send
--to alice@example.com
--cc team@example.com
--subject "Q1 status"
--body "Numbers attached."
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;

Reply (handles threadId, In-Reply-To, References automatically;

To is the original sender, Subject gets the "Re: " prefix)

To is the original sender, Subject gets the "Re: " prefix)

gws gmail +reply --message-id MSG_ID --body "Thanks — looks good."
gws gmail +reply --message-id MSG_ID --body "Thanks — looks good."

Reply-all

Reply-all

gws gmail +reply-all --message-id MSG_ID --body "+1"
gws gmail +reply-all --message-id MSG_ID --body "+1"

Forward to new recipients (preserves the original message body

Forward to new recipients (preserves the original message body

inline; original headers are summarised in the forward block)

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.
gws gmail +forward --message-id MSG_ID --to bob@example.com

如果Google拒绝请求,每个辅助命令都会返回非零状态码并在stderr输出JSON错误——直接显示该错误信息。`+send`/`+reply`需要`gmail.send`权限;如果用户仅授予了`gmail.readonly`权限,你会看到`403 insufficientPermissions`错误,应告知用户勾选发送选项重新安装连接器。

下面所有的读取/列表/搜索/标记/附件示例都**不会**改写为`gws`命令——单行的`curl ... | jq`更短且更容易与shell管道组合使用。

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}'
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.
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 // [] | .[]'
始终将
.messages
默认设置为
[]
——当没有匹配结果时,Gmail的
messages.list
端点会完全省略该字段(响应仅为
{"resultSizeEstimate": 0}
),因此直接使用
.messages[]
会导致jq崩溃并显示
Cannot iterate over null (null)
,退出码为5。同样的规则适用于
.threads
.labels
.drafts
的列表端点。如果结果为空,直接告知用户(例如“过去7天内没有未读邮件”),不要重试。
messages.list
端点仅返回
{id, threadId}
——你需要调用
messages.get
来获取邮件头/正文。实用模式:先列出ID → 对每个ID使用
format=metadata&metadataHeaders=From,Subject,Date
获取信息。仅当用户需要查看正文时才使用
format=full

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')
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

如果$IDS为空,下面的for循环不会执行——告知用户“没有未读邮件”,而不是返回空结果。

"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 '.'
undefined
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 '.'
undefined

Read a single message body (plain text and html)

读取单封邮件正文(纯文本和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)}'
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

正文在payload.parts[].body.data中以base64url编码存储——Gmail会拆分多部分消息,因此收集所有text/plain或text/html类型的内容并进行base64url解码。

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

如果纯文本内容为空,则回退到`text/html`内容(使用相同的遍历逻辑,替换mimeType过滤器)并告知用户这是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)}]}'
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

通过Gmail查询搜索

sh
undefined
sh
undefined

Same query DSL the Gmail UI uses: from:, to:, subject:, has:attachment,

使用与Gmail界面相同的查询DSL:from:, to:, subject:, has:attachment,

is:unread, label:Work, after:2026/04/01, before:2026/05/01, …

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` / `()` / `-`.
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`语法参考:<https://support.google.com/mail/answer/7190> —— 适合模型使用的语法包括`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`。可以使用`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).
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}'
系统标签包括
INBOX
SENT
DRAFT
IMPORTANT
UNREAD
STARRED
SPAM
TRASH
,以及
CATEGORY_*
(个人/社交/推广/更新/论坛)。

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.
sh
LABEL_ID='Label_4'  # 来自labels.list的结果
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 // []'
多个
labelIds
查询参数表现为逻辑AND关系。

Download an attachment

下载附件

sh
MSG_ID='18f1a2b3c4d5e6f0'
sh
MSG_ID='18f1a2b3c4d5e6f0'

1. find the attachment leaf

1. 找到附件节点

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}'
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

2. 通过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"
undefined
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"
undefined

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
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.
这些操作都需要
gmail.modify
(标记/归档/删除)或
gmail.send
(撰写+发送)权限。如果用户在安装时仅授予了
gmail.readonly
权限,你会收到
403 insufficientPermissions
错误——显示该错误并建议用户勾选对应的写入选项重新安装连接器。

Mark a message read / unread, star it, archive it (gmail.modify)

标记邮件为已读/未读、加星标、归档(gmail.modify)

sh
MSG_ID='18f1a2b3c4d5e6f0'
sh
MSG_ID='18f1a2b3c4d5e6f0'

Mark as read = remove the UNREAD label

标记为已读 = 移除UNREAD标签

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"
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

加星标 = 添加STARRED标签

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"
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)

归档 = 从INBOX移除(保留在所有邮件中)

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"

The `modify` endpoint takes `addLabelIds` and `removeLabelIds`
together — useful for atomic "archive + label" moves. Use the same
shape on `/threads/$THREAD_ID/modify` to apply across a whole thread.
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"

`modify`端点同时支持`addLabelIds`和`removeLabelIds`——适用于原子性的“归档+标记”操作。对`/threads/$THREAD_ID/modify`使用相同的格式可应用于整个对话。

Apply a custom label

应用自定义标签

sh
undefined
sh
undefined

1. find or remember the label id from labels.list

1. 从labels.list中查找或记住标签ID

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"

Creating a brand-new label needs the same scope:

```sh
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}'
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"

创建全新标签需要相同的权限:

```sh
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}'

Trash a message or thread

删除邮件或对话

sh
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"
sh
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"

Use `/untrash` (same shape) to restore. **Never** use
`messages.delete` — it permanently deletes and needs a higher scope
that we don't request.
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"

使用`/untrash`(相同格式)恢复邮件。**切勿**使用`messages.delete`——它会永久删除邮件,且需要我们未请求的更高权限。

Send a brand-new email (gmail.send)

发送全新邮件(gmail.send)

Gmail wants the message as a base64url-encoded RFC 2822 string.
sh
undefined
Gmail要求消息为base64url编码的RFC 2822格式字符串。
sh
undefined

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'
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.

多行主题行如果包含非ASCII字符需要使用MIME编码词;ASCII字符可直接使用。

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}'

For non-ASCII subjects (Chinese / emoji), use MIME encoded-word:

```sh
SUBJECT_RAW='你好,季度复盘草稿'
SUBJECT_ENCODED="=?UTF-8?B?$(printf %s "$SUBJECT_RAW" | base64)?="
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}'

对于非ASCII主题(中文/表情符号),使用MIME编码词:

```sh
SUBJECT_RAW='你好,季度复盘草稿'
SUBJECT_ENCODED="=?UTF-8?B?$(printf %s "$SUBJECT_RAW" | base64)?="

Reply in-thread (keeps the thread together)

对话内回复(保持对话连贯)

Reply by setting the
In-Reply-To
and
References
headers to the Message-Id of the message you're replying to, and pass the Gmail thread id in the API body:
sh
ORIG_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}'
Without the
threadId
in the body Gmail starts a brand-new thread even with the right
In-Reply-To
headers.
通过将
In-Reply-To
References
邮件头设置为你要回复的邮件的Message-Id,同时在API请求体中传入Gmail对话ID来进行回复:
sh
ORIG_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}'
如果请求体中没有
threadId
,即使设置了正确的
In-Reply-To
邮件头,Gmail也会创建一个全新的对话。

Save a draft instead of sending

保存草稿而不发送

Same
raw
payload, different endpoint — still costs
gmail.send
(
drafts
shares the send scope under the hood for write):
sh
curl -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}}'
使用相同的
raw
负载,只是调用不同的端点——仍然需要
gmail.send
权限(
drafts
在底层共享发送权限用于写入操作):
sh
curl -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}}'

Common error codes

常见错误码

HTTPmeaningwhat to tell the user
401 UNAUTHENTICATED
token expired / revoked"Reconnect the Gmail connector on the Connections page."
403 insufficientPermissions
scope missingidentify which scope (
gmail.modify
for label/archive/trash,
gmail.send
for sending) and suggest re-installing the connector with that box checked.
403 userRateLimitExceeded
/
429
quota / throttlingback off ~5s, then retry once.
404 notFound
wrong message / thread / attachment iddouble-check the id, or fall back to
messages.list
with the right query.
400 invalidQuery
malformed
q
print the
q
you sent + the error back to the user.
Never log or echo
$GOOGLE_GMAIL_TOKEN
— treat it as a secret.
HTTP含义告知用户的内容
401 UNAUTHENTICATED
令牌过期/被撤销"在连接页面重新连接Gmail连接器。"
403 insufficientPermissions
缺少权限范围说明缺少的权限范围(标记/归档/删除需要
gmail.modify
,发送邮件需要
gmail.send
),并建议用户勾选对应选项重新安装连接器。
403 userRateLimitExceeded
/
429
配额/限流等待约5秒后重试一次。
404 notFound
邮件/对话/附件ID错误仔细检查ID,或使用正确的查询调用
messages.list
重新查找。
400 invalidQuery
查询格式错误将你发送的
q
参数和错误信息一起显示给用户。
切勿记录或显示
$GOOGLE_GMAIL_TOKEN
——将其视为机密信息。