mcpField
G
google-gmail

google-gmail

github.com/acedatacloud生产力17221 次安装今天更新
代码仓库

关于 google-gmail

通过 curl 和 jq 调用 Gmail API 的技能,供处理邮箱任务的助手使用。它强调按授权范围操作、先读取元数据,并在破坏性操作前确认。

主要功能

  1. 01搜索和列出邮件
  2. 02读取邮件及会话内容
  3. 03管理标签、归档和移至垃圾箱
  4. 04使用 gws 撰写、回复和转发
  5. 05遵循 OAuth 范围并请求确认

适用场景

  1. 01整理未读收件箱
  2. 02按发件人、主题或标签查找邮件
  3. 03添加标签或归档邮件
  4. 04起草并发送经确认的回复

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/acedatacloud/skills --skill google-gmail

安装后在 Claude Code 中输入/google-gmail确认已加载。

详细介绍

通过 curl + jq 操作 Gmail。用户的 OAuth bearer token 位于 $GOOGLE_GMAIL_TOKEN;每次调用都需要在请求中加入 Authorization: Bearer $GOOGLE_GMAIL_TOKEN。该 token 至少包含 gmail.readonly 和身份验证范围 (openid email profile);如果用户在安装时选择启用写入操作,还会包含 gmail.modify(标签 / 归档 / 移至垃圾箱)和/或 gmail.send(撰写并发送)。在写入操作实际失败之前,始终按最窄权限范围处理——不要在这里向 Google 请求新权限范围。

Gmail API 返回标准 JSON;失败时会返回 {"error": {"code": 401|403|..., "message": "..."}}——请原样显示该错误。401 表示 token 已过期(请重新安装)。403 insufficientPermissions 表示用户未授予此次调用所需的写入权限范围——说明缺少哪个权限范围,并建议用户重新安装连接器,同时勾选对应的写入选项。

执行任何破坏性写入操作之前(将会话移至垃圾箱、发送电子邮件),先向用户展示确切的目标 / 草稿并征求确认。未经明确同意,不要对大量邮件批量操作。

始终先调用 users/me/profile,确认连接正常,并了解当前操作的 Gmail 账号。邮箱数据可能很大——先获取元数据,只有当用户确实需要查看特定邮件的正文时才使用 format=full。

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

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 示例通常更简短——请继续使用这些示例。

安装
npm install -g @googleworkspace/cli   # or: brew install googleworkspace-cli
# Pre-built binaries also at https://github.com/googleworkspace/cli/releases
gws --version
身份验证

gws 从环境变量 GOOGLE_WORKSPACE_CLI_TOKEN 读取 OAuth bearer token。本技能使用的 Gmail token 位于 $GOOGLE_GMAIL_TOKEN,因此每个调用 gws 的 shell 代码块开头都要重新导出一次:

export GOOGLE_WORKSPACE_CLI_TOKEN="$GOOGLE_GMAIL_TOKEN"

你可以通过 gws gmail users getProfile --params '{"userId":"me"}' 确认当前账号。

发送 / 回复 / 转发
# New message
gws gmail +send \
  --to [email protected] \
  --cc [email protected] \
  --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 [email protected]

如果 Google 拒绝请求,每个辅助命令都会以非零状态码退出,并在 stderr 上输出 JSON 错误——请原样显示该错误。+send / +reply 需要 gmail.send 权限范围;如果用户只授予了 gmail.readonly,你会看到 403 insufficientPermissions,此时应请用户重新安装连接器并勾选发送选项。

下面所有读取 / 列表 / 搜索 / 标签 / 附件示例都有意不改写为 gws——一行 curl ... | jq 更简短,也更容易与 shell 管道组合使用。

示例

验证身份验证(始终首先运行)
curl -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 的默认值设为 []——当没有匹配项时,Gmail 的 messages.list 会完全省略此字段(响应仅为 {"resultSizeEstimate": 0}),因此直接使用 .messages[] 会导致 jq 因 Cannot iterate over null (null) 崩溃并以状态码 5 退出。列表端点中的 .threads、.labels、.drafts 也一样。如果结果为空,请直接告知用户(例如“过去 7 天内没有未读邮件”),不要重试。

messages.list 端点只返回 {id, threadId}——你必须对每条邮件调用 messages.get,才能获取邮件头 / 正文。简便的做法:先列出 ID,再对每条邮件使用 format=metadata&metadataHeaders=From,Subject,Date 获取信息。只有当用户需要查看正文时才使用 format=full。

列出并补充标头(一次性收件箱分拣)
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 '.'
阅读单封邮件正文(纯文本和 HTML)
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

如果纯文本叶节点为空,请回退到 text/html 叶节点 (采用相同的遍历方式,将 mimeType 筛选条件替换为 HTML),并告知用户内容是 HTML。

阅读整个邮件会话
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)}]}'
按 Gmail 查询语法搜索
# 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:[email protected] 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 / () / -。

列出标签(系统标签和用户自定义标签)
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_*(个人 / 社交 / 促销 / 更新 / 论坛)。

按标签筛选
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 // []'

多个 labelIds 查询参数的行为类似于 AND。

下载附件
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"
分页
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

写入操作示例

这些操作需要 gmail.modify(添加标签 / 归档 / 移至垃圾箱)或 gmail.send(撰写并发送)。如果用户安装时只授权了 gmail.readonly,你会收到 403 insufficientPermissions 错误——请告知用户此问题,并让他们重新安装,同时勾选写入权限选项。

将邮件标记为已读 / 未读、加星标或归档(gmail.modify)
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' \
  --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"

modify 端点可同时接收 addLabelIds 和 removeLabelIds ——适用于原子化执行“归档 + 添加标签”操作。使用相同的 结构调用 /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"

使用 /untrash(结构相同)即可恢复。绝不要使用 messages.delete——它会永久删除邮件,且需要我们未申请的更高权限范围。

发送全新邮件(gmail.send)

Gmail 要求邮件内容为经过 base64url 编码的 RFC 2822 字符串。

# Compose the message
TO='[email protected]'
SUBJECT='Quick hello'
BODY='Hi Alice,

Just a quick test note from the AceDataCloud Gmail connector.

Best,
YOUR_NAME'

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

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

SUBJECT_RAW='你好,季度复盘草稿'
SUBJECT_ENCODED="=?UTF-8?B?$(printf %s "$SUBJECT_RAW" | base64)?="
在邮件会话中回复(保持会话完整)

回复时,将 In-Reply-To 和 References 标头设为所回复邮件的 Message-Id,并且在 API 请求正文中传入 Gmail 会话 ID:

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 仍会开启一个全新的邮件会话。

保存为草稿而非发送

raw 负载相同,但端点不同——仍需 gmail.send (写入时,drafts 在底层也使用发送权限范围):

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

常见错误代码

HTTP含义应告知用户的内容
401 UNAUTHENTICATED令牌已过期 / 已撤销“请在 Connections 页面重新连接 Gmail 连接器。”
403 insufficientPermissions缺少权限范围指明缺少的权限范围(添加标签 / 归档 / 移至垃圾箱需要 gmail.modify,发送邮件需要 gmail.send),并建议用户重新安装连接器并勾选相应选项。
403 userRateLimitExceeded / 429配额限制 / 请求限流等待约 5 秒后重试一次。
404 notFound邮件 / 邮件会话 / 附件 ID 错误仔细检查 ID,或使用正确的查询条件回退到 messages.list。
400 invalidQueryq 格式错误将你发送的 q 和错误信息一并告知用户。

切勿记录或回显 $GOOGLE_GMAIL_TOKEN——请将其视为机密。

评论

支持 Markdown