Skip to content

feat(google-workspace): add gmail attachment list/get CLI verbs (#22872) - #23465

Closed
briandevans wants to merge 1 commit into
NousResearch:mainfrom
briandevans:fix/gws-gmail-attachment-22872
Closed

feat(google-workspace): add gmail attachment list/get CLI verbs (#22872)#23465
briandevans wants to merge 1 commit into
NousResearch:mainfrom
briandevans:fix/gws-gmail-attachment-22872

Conversation

@briandevans

Copy link
Copy Markdown
Contributor

Summary

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
the SKILL.md example has:attachment filename:pdf newer_than:7d, it then
has 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.py has full
plumbing for users.messages.get, send, reply, labels, modify
but never wires users.messages.attachments.get. The Gmail message
payload references attachments by body.attachmentId, which is opaque
without a second API call to resolve to bytes.

The fix

Two new subcommands following the existing _run_gws / build_service
dual-path pattern:

$GAPI gmail attachment list MESSAGE_ID
# → [{"attachment_id": "...", "filename": "...", "mime_type": "...", "size_bytes": N, "part_id": "..."}]

$GAPI gmail attachment get MESSAGE_ID ATTACHMENT_ID --output /tmp/file.pdf
# → {"status": "saved", "path": "/tmp/file.pdf", "size_bytes": N}
  • gmail attachment list walks the message payload via a new
    _walk_attachments() helper that handles nested multipart/* parts
    (cover letter + signed/encrypted wrappers).
  • gmail attachment get base64url-decodes the payload and writes bytes
    to disk, creating parent directories as needed.
  • Both go through _run_gws when the gws binary is available and fall
    back to googleapiclient otherwise, matching every other verb in the
    file.
  • No new OAuth scopes — gmail.readonly already covers
    attachments.get.

Test plan

  • Focused: tests/skills/test_google_workspace_api.py — 5 new tests
    (12 total, all pass):
    • _walk_attachments finds attachments across nested multipart
    • _walk_attachments returns empty for text-only payloads
    • gmail attachment list issues the expected messages.get call and
      emits the JSON shape
    • gmail attachment get decodes base64url and writes the raw bytes
      to the output path, creating parent directories
    • gmail attachment get surfaces a clear stderr error and exits 1
      when the API response omits data
  • Adjacent suite: tests/skills/ (164 passed) — no regressions in
    bridge / OAuth / sibling skills tests.

Related

Copilot AI review requested due to automatic review settings May 11, 2026 00:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_ID to enumerate attachments from a message payload (including nested multipart structures).
  • Add gmail attachment get MESSAGE_ID ATTACHMENT_ID --output PATH to fetch attachment bytes and write them to disk.
  • Add/extend tests and update SKILL.md usage 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.

Comment on lines +516 to +522
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)
Comment on lines +334 to +337
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)
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have tool/skills Skills system (list, view, manage) labels May 11, 2026
@briandevans
briandevans force-pushed the fix/gws-gmail-attachment-22872 branch 3 times, most recently from df9494b to b372faf Compare May 19, 2026 23:11
@briandevans
briandevans force-pushed the fix/gws-gmail-attachment-22872 branch 3 times, most recently from d04a874 to 0b25d13 Compare May 26, 2026 03:09
@briandevans
briandevans force-pushed the fix/gws-gmail-attachment-22872 branch from 0b25d13 to 08c9bdb Compare May 28, 2026 20:13
…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>
@briandevans
briandevans force-pushed the fix/gws-gmail-attachment-22872 branch from 08c9bdb to c100d33 Compare May 30, 2026 08:28
@briandevans

Copy link
Copy Markdown
Contributor Author

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P3 Low — cosmetic, nice to have tool/skills Skills system (list, view, manage) type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

productivity/google-workspace: CLI wrapper missing attachment download

3 participants