google-gmail
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDrive Gmail via . The user's OAuth bearer token is in
; every call needs it as
. At minimum the token
carries plus the identity scopes
(); if the user opted in to write at install
time it also carries (label / archive / trash) and/or
(compose + send). Always assume the narrowest scope
until a write actually fails — don't ask Google for new scopes from
here.
curl + jq$GOOGLE_GMAIL_TOKENAuthorization: Bearer $GOOGLE_GMAIL_TOKENgmail.readonlyopenid email profilegmail.modifygmail.sendThe Gmail API returns standard JSON; failures surface as
— show that
error verbatim. means the token expired (re-install). 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.
{"error": {"code": 401|403|..., "message": "..."}}401403 insufficientPermissionsBefore 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 to confirm the connection works
AND learn which Gmail account you're operating against. Mailbox payloads
can be huge — fetch metadata first, only when the user
actually wants the body of a specific message.
users/me/profileformat=full通过操作Gmail。用户的OAuth bearer token存储在中;每次调用都需要将其作为传入。该令牌至少需要包含权限以及身份范围();如果用户在安装时选择了写入权限,那么它还会包含(标记/归档/删除)和/或(撰写+发送)权限。在写入操作实际失败前,始终假设使用最窄的权限范围——不要在此处向Google请求新的权限范围。
curl + jq$GOOGLE_GMAIL_TOKENAuthorization: Bearer $GOOGLE_GMAIL_TOKENgmail.readonlyopenid email profilegmail.modifygmail.sendGmail API返回标准JSON格式;失败时会返回——直接显示该错误信息。表示令牌已过期(重新安装)。表示用户未授予该调用所需的写入权限——说明缺少的权限范围,并建议用户勾选对应的写入选项重新安装连接器。
{"error": {"code": 401|403|..., "message": "..."}}401403 insufficientPermissions在执行任何破坏性写入操作前(删除对话、发送邮件),向用户显示确切的目标/草稿内容并请求确认。未经明确许可,不要批量操作多个消息。
**始终先调用**来确认连接正常,同时了解当前操作的Gmail账户。邮箱负载可能非常大——先获取元数据,仅当用户确实需要查看特定邮件的正文时才使用。
users/me/profileformat=fullOptional: Google Workspace CLI (gws
) for outbound mail
gws可选方案:用于发送邮件的Google Workspace CLI(gws
)
gwsgwsgoogleworkspace+Use for sending mail. The Gmail REST API requires every
outbound message to be a fully-formed RFC 822 message, base64url-encoded
into a field, with reply / forward threading carried in
/ / . The helpers do all of that for you. For everything
else (read, search, labels, attachments) and curl are equivalent,
so the curl recipes below are usually shorter — stay on those.
gwsrawIn-Reply-ToReferencesthreadId+send / +reply / +reply-all / +forwardgwsgwsgoogleworkspace+使用发送邮件。Gmail REST API要求每一封外发邮件都是完整的RFC 822格式消息,经过base64url编码后放入字段,回复/转发的对话关系通过//传递。辅助命令会自动处理所有这些操作。对于其他所有操作(读取、搜索、标记、附件),和curl功能相当,因此下面的curl示例通常更简洁——优先使用这些示例。
gwsrawIn-Reply-ToReferencesthreadId+send / +reply / +reply-all / +forwardgwsInstall
安装
sh
npm install -g @googleworkspace/cli # or: brew install googleworkspace-clish
npm install -g @googleworkspace/cli # or: brew install googleworkspace-cliPre-built binaries also at https://github.com/googleworkspace/cli/releases
Pre-built binaries also at https://github.com/googleworkspace/cli/releases
gws --version
undefinedgws --version
undefinedAuth
认证
gwsGOOGLE_WORKSPACE_CLI_TOKEN$GOOGLE_GMAIL_TOKENgwssh
export GOOGLE_WORKSPACE_CLI_TOKEN="$GOOGLE_GMAIL_TOKEN"You can confirm the active account with .
gws gmail users getProfile --params '{"userId":"me"}'gwsGOOGLE_WORKSPACE_CLI_TOKEN$GOOGLE_GMAIL_TOKENgwssh
export GOOGLE_WORKSPACE_CLI_TOKEN="$GOOGLE_GMAIL_TOKEN"你可以通过确认当前账户。
gws gmail users getProfile --params '{"userId":"me"}'Send / reply / forward
发送/回复/转发
sh
undefinedsh
undefinedNew message
New message
gws gmail +send
--to alice@example.com
--cc team@example.com
--subject "Q1 status"
--body "Numbers attached."
--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."
--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 to — Gmail's omits the
field entirely when there are zero matches (the response is just
), so a bare will crash jq
with and exit 5. Same applies to
, , 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.
.messages[]messages.list{"resultSizeEstimate": 0}.messages[]Cannot iterate over null (null).threads.labels.draftsThe endpoint returns only — you have
to fan out to for headers / body. Cheap pattern: list
ids → get with for
each. Use only if the user wants the body.
messages.list{id, threadId}messages.getformat=metadata&metadataHeaders=From,Subject,Dateformat=fullsh
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 // [] | .[]'始终将默认设置为——当没有匹配结果时,Gmail的端点会完全省略该字段(响应仅为),因此直接使用会导致jq崩溃并显示,退出码为5。同样的规则适用于、、的列表端点。如果结果为空,直接告知用户(例如“过去7天内没有未读邮件”),不要重试。
.messages[]messages.list{"resultSizeEstimate": 0}.messages[]Cannot iterate over null (null).threads.labels.draftsmessages.list{id, threadId}messages.getformat=metadata&metadataHeaders=From,Subject,Dateformat=fullList + 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 '.'
--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 '.'
undefinedfor 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 '.'
--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 '.'
undefinedRead 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
| 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
| 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
undefinedsh
undefinedSame 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 // []'
--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 // []'
--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 , , , , ,
, , , plus (Personal / Social /
Promotions / Updates / Forums).
INBOXSENTDRAFTIMPORTANTUNREADSTARREDSPAMTRASHCATEGORY_*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}'系统标签包括、、、、、、、,以及(个人/社交/推广/更新/论坛)。
INBOXSENTDRAFTIMPORTANTUNREADSTARREDSPAMTRASHCATEGORY_*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 query params behave like AND.
labelIdssh
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 // []'多个查询参数表现为逻辑AND关系。
labelIdsDownload 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')
--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')
--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"
"https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/attachments/$ATT_ID"
| jq -r .data | tr '_-' '/+' | base64 -d > "$OUT" file "$OUT"
undefinedATT_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"
"https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/attachments/$ATT_ID"
| jq -r .data | tr '_-' '/+' | base64 -d > "$OUT" file "$OUT"
undefinedPagination
分页
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
donesh
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
doneWrite recipes
写入操作示例脚本
These all need (label / archive / trash) or
(compose + send). If the user only granted
at install you'll get
— surface that and ask them to re-install with the write boxes
checked.
gmail.modifygmail.sendgmail.readonly403 insufficientPermissions这些操作都需要(标记/归档/删除)或(撰写+发送)权限。如果用户在安装时仅授予了权限,你会收到错误——显示该错误并建议用户勾选对应的写入选项重新安装连接器。
gmail.modifygmail.sendgmail.readonly403 insufficientPermissionsMark 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"
-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"
-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"
-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"
-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"
-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"
-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
undefinedsh
undefined1. 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"
-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"
-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"
"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"
"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
undefinedGmail要求消息为base64url编码的RFC 2822格式字符串。
sh
undefinedCompose 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 '=')
"$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}'
-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 '=')
"$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}'
-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 and headers to the
Message-Id of the message you're replying to, and pass the
Gmail thread id in the API body:
In-Reply-ToReferencessh
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 in the body Gmail starts a brand-new thread
even with the right headers.
threadIdIn-Reply-To通过将和邮件头设置为你要回复的邮件的Message-Id,同时在API请求体中传入Gmail对话ID来进行回复:
In-Reply-ToReferencessh
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}'如果请求体中没有,即使设置了正确的邮件头,Gmail也会创建一个全新的对话。
threadIdIn-Reply-ToSave a draft instead of sending
保存草稿而不发送
Same payload, different endpoint — still costs
( shares the send scope under the hood for write):
rawgmail.senddraftssh
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}}'使用相同的负载,只是调用不同的端点——仍然需要权限(在底层共享发送权限用于写入操作):
rawgmail.senddraftssh
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
常见错误码
| 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 |
Never log or echo — treat it as a secret.
$GOOGLE_GMAIL_TOKEN| HTTP | 含义 | 告知用户的内容 |
|---|---|---|
| 令牌过期/被撤销 | "在连接页面重新连接Gmail连接器。" |
| 缺少权限范围 | 说明缺少的权限范围(标记/归档/删除需要 |
| 配额/限流 | 等待约5秒后重试一次。 |
| 邮件/对话/附件ID错误 | 仔细检查ID,或使用正确的查询调用 |
| 查询格式错误 | 将你发送的 |
切勿记录或显示——将其视为机密信息。
$GOOGLE_GMAIL_TOKEN