feat(google-workspace): add gmail attachment list/get CLI verbs (#22872) - #23465
feat(google-workspace): add gmail attachment list/get CLI verbs (#22872)#23465briandevans wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR extends the bundled productivity/google-workspace skill’s Gmail CLI wrapper to support discovering and downloading Gmail attachments, closing the gap where the agent could find attachment-bearing emails but had no skill-supported way to fetch the bytes.
Changes:
- Add
gmail attachment list MESSAGE_IDto enumerate attachments from a message payload (including nested multipart structures). - Add
gmail attachment get MESSAGE_ID ATTACHMENT_ID --output PATHto fetch attachment bytes and write them to disk. - Add/extend tests and update
SKILL.mdusage examples for the new attachment commands.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
skills/productivity/google-workspace/scripts/google_api.py |
Adds _walk_attachments() plus gmail attachment list/get subcommands and wires them into argparse. |
tests/skills/test_google_workspace_api.py |
Adds tests for attachment walking, listing, downloading, and an error path. |
skills/productivity/google-workspace/SKILL.md |
Documents the new gmail attachment commands in the Gmail usage section. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| data = att.get("data") if isinstance(att, dict) else None | ||
| if not data: | ||
| print("ERROR: Attachment payload missing 'data' field.", file=sys.stderr) | ||
| sys.exit(1) | ||
|
|
||
| raw = base64.urlsafe_b64decode(data) | ||
| output_path = Path(args.output) |
| def test_api_gmail_attachment_get_decodes_and_writes(api_module, tmp_path, capsys): | ||
| """gmail attachment get base64url-decodes the payload and writes it to disk.""" | ||
| raw_bytes = b"%PDF-1.4 fake content\x00\xff" | ||
| encoded = base64.urlsafe_b64encode(raw_bytes).decode() |
| api_module.gmail_attachment_get(args) | ||
|
|
||
| assert excinfo.value.code == 1 | ||
| assert not output.exists() |
| @@ -196,6 +196,13 @@ $GAPI gmail reply MESSAGE_ID --from '"Support Bot" <user@example.com>' --body "T | |||
| $GAPI gmail labels | |||
| $GAPI gmail modify MESSAGE_ID --add-labels LABEL_ID | |||
| $GAPI gmail modify MESSAGE_ID --remove-labels UNREAD | |||
|
|
|||
| # Attachments | |||
| # List attachments in a message (returns JSON with attachment_id, filename, mime_type, size_bytes) | |||
df9494b to
b372faf
Compare
d04a874 to
0b25d13
Compare
0b25d13 to
08c9bdb
Compare
…Research#22872) The bundled productivity/google-workspace skill ships a CLI wrapper for Gmail (search, get, send, reply, labels, modify) but exposes no surface for downloading attachments. When the agent surfaces emails matching SKILL.md's own `has:attachment filename:pdf newer_than:7d` example, it then has no skill-supported way to fetch them — it falls back to ad-hoc Python or asks the user to download manually. Add two subcommands under `gmail attachment`: gmail attachment list MESSAGE_ID Walks the message payload (including nested multipart) and returns JSON with {attachment_id, filename, mime_type, size_bytes, part_id} per attachment. gmail attachment get MESSAGE_ID ATTACHMENT_ID --output PATH Calls users.messages.attachments.get and base64url-decodes the payload to the output path. Creates parent directories as needed. Both follow the established pattern: `gws` CLI when available, falls back to googleapiclient when not. No new scopes — covered by existing gmail.readonly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
08c9bdb to
c100d33
Compare
|
Housekeeping: closing to keep my open-PR set focused on actively-reviewed work. This has been open ~21d without maintainer review and the surrounding code has continued to move, so it's unlikely to land as-is. The underlying fix still stands — happy to reopen and rebase if it would be useful. Thanks! |
Summary
The bundled
productivity/google-workspaceskill ships a CLI wrapper forGmail (
search,get,send,reply,labels,modify) but exposes nosurface for downloading attachments. When the agent surfaces emails matching
the SKILL.md example
has:attachment filename:pdf newer_than:7d, it thenhas no skill-supported way to fetch them — it falls back to ad-hoc Python
or asks the user to download manually.
This adds two subcommands under
gmail attachment.The gap
skills/productivity/google-workspace/scripts/google_api.pyhas fullplumbing for
users.messages.get,send,reply,labels,modify—but never wires
users.messages.attachments.get. The Gmail messagepayload references attachments by
body.attachmentId, which is opaquewithout a second API call to resolve to bytes.
The fix
Two new subcommands following the existing
_run_gws/build_servicedual-path pattern:
gmail attachment listwalks the message payload via a new_walk_attachments()helper that handles nestedmultipart/*parts(cover letter + signed/encrypted wrappers).
gmail attachment getbase64url-decodes the payload and writes bytesto disk, creating parent directories as needed.
_run_gwswhen the gws binary is available and fallback to
googleapiclientotherwise, matching every other verb in thefile.
gmail.readonlyalready coversattachments.get.Test plan
tests/skills/test_google_workspace_api.py— 5 new tests(12 total, all pass):
_walk_attachmentsfinds attachments across nested multipart_walk_attachmentsreturns empty for text-only payloadsgmail attachment listissues the expectedmessages.getcall andemits the JSON shape
gmail attachment getdecodes base64url and writes the raw bytesto the output path, creating parent directories
gmail attachment getsurfaces a clear stderr error and exits 1when the API response omits
datatests/skills/(164 passed) — no regressions inbridge / OAuth / sibling skills tests.
Related
no overlap with that diff.