Skip to content

Add attachments command - #2

Open
nitsanavni wants to merge 12 commits into
saadiq:mainfrom
nitsanavni:add-attachments-command
Open

Add attachments command#2
nitsanavni wants to merge 12 commits into
saadiq:mainfrom
nitsanavni:add-attachments-command

Conversation

@nitsanavni

Copy link
Copy Markdown

Summary

  • Adds gmail attachments <message-id> [-o output-dir] command to download email attachments to disk
  • Supports saving to a custom output directory via --output flag

Test plan

  • Test downloading attachments (PDF, images, etc.)
  • Test with messages that have no attachments
  • Test with custom output directory

nitsanavni and others added 5 commits February 27, 2026 12:36
Adds gmail archive <message-id>... which removes the INBOX label,
matching Gmail archive behavior. Adds gmail.modify scope to support this.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The archive branch swapped the HTML-to-markdown dependency from
html_to_markdown to markdownify. markdownify is declared in neither
pyproject.toml nor uv.lock, so `gmail read` raised ModuleNotFoundError
on any HTML email. The swap is also unrelated to the archive feature.

Claude-Session: https://claude.ai/code/session_01XN4T96G5R4UczQpgA2nqES
An invalid message ID raised HttpError out of the loop, so IDs before it
were already archived while IDs after it silently never ran. Report the
failure, continue the batch, and exit non-zero.

Claude-Session: https://claude.ai/code/session_01XN4T96G5R4UczQpgA2nqES
Adding gmail.modify invalidates every existing token, but the old token
still refreshes cleanly, so the API rejected archive with an opaque 403.
Credentials.from_authorized_user_info() overrides a token's scopes with
the ones passed in, so creds.scopes cannot detect this - read the granted
scopes from the token file and re-consent when any required scope is absent.

Claude-Session: https://claude.ai/code/session_01XN4T96G5R4UczQpgA2nqES
@saadiq saadiq mentioned this pull request Jul 14, 2026
3 tasks
nitsanavni and others added 7 commits July 13, 2026 23:45
Adds gmail attachments <message-id> [-o output-dir] to save
attachments from an email to disk.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adding cmd_attachments pushed commands.py to 304 lines, over the project's
300-line limit. Move it to its own module, matching account_commands.py.
Pure move - behavior is unchanged.

Claude-Session: https://claude.ai/code/session_01XN4T96G5R4UczQpgA2nqES
Attachment filenames come from the email, so the sender controls them.
'output_dir / filename' honored both traversal names ('../../.ssh/authorized_keys')
and absolute names ('/etc/cron.d/pwn') - pathlib discards the left operand
entirely when the right one is absolute. Anyone who could email the user could
write a file anywhere the user could write.

Keep only the final path component, and skip names that sanitize to nothing.

Claude-Session: https://claude.ai/code/session_01XN4T96G5R4UczQpgA2nqES
The loop only scanned payload['parts'], so attachments inside a nested
multipart/related or multipart/alternative - which is how most mail clients
structure messages - reported "No attachments found." get_body() in commands.py
already recurses for the same reason; mirror it.

Claude-Session: https://claude.ai/code/session_01XN4T96G5R4UczQpgA2nqES
Three write-path problems: '-o some/new/dir' raised FileNotFoundError because
the directory was never created; two attachments sharing a filename silently
clobbered each other; and a missing 'data' field raised KeyError.

Claude-Session: https://claude.ai/code/session_01XN4T96G5R4UczQpgA2nqES
Covers the path-traversal containment, nested-part discovery, filename
collisions, and output-dir creation. Verified these fail against the
pre-fix behavior.

Claude-Session: https://claude.ai/code/session_01XN4T96G5R4UczQpgA2nqES
@saadiq
saadiq force-pushed the add-attachments-command branch from 6411372 to 315d707 Compare July 14, 2026 03:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants