{"slug": "gmail-cli-for-claude-code-multi-account-gmail-access-via-command-line", "title": "Gmail CLI for Claude Code - multi-account Gmail access via command line", "summary": "A developer created a Gmail CLI tool for Claude Code that provides multi-account Gmail access via the command line. The tool uses the Google Gmail API directly and supports multiple accounts through separate credential files, with OAuth flow for authentication.", "body_md": "| #!/usr/bin/env python3 | |\n| \"\"\" | |\n| Gmail CLI tool for Claude Code skill. | |\n| Supports multiple Gmail accounts via separate credential files. | |\n| Uses the Google Gmail API directly (no third-party wrappers). | |\n| \"\"\" | |\n| import argparse | |\n| import base64 | |\n| import json | |\n| import os | |\n| import sys | |\n| from pathlib import Path | |\n| # Config directory for storing credentials | |\n| CONFIG_DIR = Path.home() / \".claude\" / \"skills\" / \"gmail\" / \"config\" | |\n| # Account aliases mapping to credential files | |\n| # Customize these with your own email addresses | |\n| ACCOUNTS = { | |\n| \"personal\": \"you@gmail.com\", | |\n| \"work\": \"you@company.com\", | |\n| \"business\": \"you@business.io\", | |\n| } | |\n| def _resolve_account(account: str): | |\n| \"\"\"Resolve account alias to email and safe filename.\"\"\" | |\n| email = ACCOUNTS.get(account, account) | |\n| safe_name = email.replace(\"@\", \"_at_\").replace(\".\", \"_\") | |\n| return email, safe_name | |\n| def _run_oauth_flow(client_secret_file: str, token_file: str, email: str): | |\n| \"\"\"Perform OAuth flow and save token as plain JSON.\"\"\" | |\n| from google_auth_oauthlib.flow import InstalledAppFlow | |\n| SCOPES = ['https://mail.google.com/'] | |\n| print(f\"\\nAuthenticating: {email}\", file=sys.stderr) | |\n| print(f\"A browser window will open — sign in with: {email}\\n\", file=sys.stderr) | |\n| flow = InstalledAppFlow.from_client_secrets_file(client_secret_file, SCOPES) | |\n| creds = flow.run_local_server(port=0) | |\n| # Save as plain JSON (compatible with get_service) | |\n| token_data = { | |\n| \"access_token\": creds.token, | |\n| \"refresh_token\": creds.refresh_token, | |\n| \"token_uri\": creds.token_uri or \"https://oauth2.googleapis.com/token\", | |\n| \"client_id\": creds.client_id, | |\n| \"client_secret\": creds.client_secret, | |\n| } | |\n| with open(token_file, \"w\") as f: | |\n| json.dump(token_data, f, indent=2) | |\n| print(f\"Token saved for {email}\\n\", file=sys.stderr) | |\n| def get_service(account: str): | |\n| \"\"\"Get a Gmail API service client for the given account. | |\n| Triggers OAuth flow if no token exists yet.\"\"\" | |\n| from google.oauth2.credentials import Credentials | |\n| from googleapiclient.discovery import build | |\n| email, safe_name = _resolve_account(account) | |\n| client_secret = CONFIG_DIR / f\"client_secret_{safe_name}.json\" | |\n| token_path = CONFIG_DIR / f\"token_{safe_name}.json\" | |\n| if not client_secret.exists(): | |\n| print(f\"ERROR: No credentials found for {email}\", file=sys.stderr) | |\n| print(f\"Expected: {client_secret}\", file=sys.stderr) | |\n| print(f\"\\nRun 'gmail_cli.py setup' for instructions.\", file=sys.stderr) | |\n| sys.exit(1) | |\n| if not token_path.exists(): | |\n| _run_oauth_flow(str(client_secret), str(token_path), email) | |\n| with open(token_path) as f: | |\n| td = json.load(f) | |\n| creds = Credentials( | |\n| token=td.get(\"access_token\"), | |\n| refresh_token=td.get(\"refresh_token\"), | |\n| token_uri=td.get(\"token_uri\", \"https://oauth2.googleapis.com/token\"), | |\n| client_id=td.get(\"client_id\"), | |\n| client_secret=td.get(\"client_secret\"), | |\n| ) | |\n| return build(\"gmail\", \"v1\", credentials=creds, cache_discovery=False) | |\n| def _extract_body(payload): | |\n| \"\"\"Extract text/plain body from a Gmail API message payload.\"\"\" | |\n| if payload.get(\"mimeType\") == \"text/plain\": | |\n| data = payload[\"body\"].get(\"data\", \"\") | |\n| if data: | |\n| return base64.urlsafe_b64decode(data).decode(\"utf-8\", errors=\"replace\") | |\n| for part in payload.get(\"parts\", []): | |\n| result = _extract_body(part) | |\n| if result: | |\n| return result | |\n| return \"\" | |\n| def _extract_html(payload): | |\n| \"\"\"Extract text/html body from a Gmail API message payload.\"\"\" | |\n| if payload.get(\"mimeType\") == \"text/html\": | |\n| data = payload[\"body\"].get(\"data\", \"\") | |\n| if data: | |\n| return base64.urlsafe_b64decode(data).decode(\"utf-8\", errors=\"replace\") | |\n| for part in payload.get(\"parts\", []): | |\n| result = _extract_html(part) | |\n| if result: | |\n| return result | |\n| return \"\" | |\n| def _extract_attachments(payload, attachments=None): | |\n| \"\"\"Extract attachment filenames from a Gmail API message payload.\"\"\" | |\n| if attachments is None: | |\n| attachments = [] | |\n| filename = payload.get(\"filename\") | |\n| if filename: | |\n| attachments.append(filename) | |\n| for part in payload.get(\"parts\", []): | |\n| _extract_attachments(part, attachments) | |\n| return attachments | |\n| def cmd_list(args): | |\n| \"\"\"List recent emails.\"\"\" | |\n| service = get_service(args.account) | |\n| # Build query | |\n| query_parts = [\"in:inbox\"] | |\n| if args.unread: | |\n| query_parts.append(\"is:unread\") | |\n| if args.starred: | |\n| query_parts = [\"is:starred\"] | |\n| if args.label: | |\n| query_parts = [f\"label:{args.label}\"] | |\n| query_parts.append(\"newer_than:30d\") | |\n| query = \" \".join(query_parts) | |\n| try: | |\n| result = service.users().messages().list(userId=\"me\", q=query, maxResults=args.limit).execute() | |\n| except Exception as e: | |\n| print(f\"ERROR: List failed: {e}\", file=sys.stderr) | |\n| sys.exit(1) | |\n| msg_refs = result.get(\"messages\", []) | |\n| if args.json: | |\n| output = [] | |\n| for ref in msg_refs: | |\n| msg = service.users().messages().get(userId=\"me\", id=ref[\"id\"], format=\"metadata\", | |\n| metadataHeaders=[\"From\", \"To\", \"Subject\", \"Date\"]).execute() | |\n| headers = {h[\"name\"]: h[\"value\"] for h in msg.get(\"payload\", {}).get(\"headers\", [])} | |\n| labels = msg.get(\"labelIds\", []) | |\n| output.append({ | |\n| \"id\": msg[\"id\"], | |\n| \"thread_id\": msg[\"threadId\"], | |\n| \"from\": headers.get(\"From\", \"\"), | |\n| \"to\": headers.get(\"To\", \"\"), | |\n| \"subject\": headers.get(\"Subject\", \"\"), | |\n| \"date\": headers.get(\"Date\", \"\"), | |\n| \"snippet\": msg.get(\"snippet\", \"\"), | |\n| \"labels\": labels, | |\n| \"unread\": \"UNREAD\" in labels, | |\n| }) | |\n| print(json.dumps(output, indent=2, default=str)) | |\n| else: | |\n| for ref in msg_refs: | |\n| msg = service.users().messages().get(userId=\"me\", id=ref[\"id\"], format=\"metadata\", | |\n| metadataHeaders=[\"From\", \"Subject\", \"Date\"]).execute() | |\n| headers = {h[\"name\"]: h[\"value\"] for h in msg.get(\"payload\", {}).get(\"headers\", [])} | |\n| labels = msg.get(\"labelIds\", []) | |\n| status = \"[UNREAD]\" if \"UNREAD\" in labels else \"\" | |\n| print(f\"{status} {headers.get('Date', '?')} | From: {headers.get('From', '?')}\") | |\n| print(f\" Subject: {headers.get('Subject', '?')}\") | |\n| print(f\" ID: {msg['id']}\") | |\n| print() | |\n| def cmd_read(args): | |\n| \"\"\"Read a specific email by ID.\"\"\" | |\n| service = get_service(args.account) | |\n| try: | |\n| msg = service.users().messages().get(userId=\"me\", id=args.message_id, format=\"full\").execute() | |\n| except Exception as e: | |\n| print(f\"ERROR: Could not read message {args.message_id}: {e}\", file=sys.stderr) | |\n| sys.exit(1) | |\n| headers = {h[\"name\"]: h[\"value\"] for h in msg[\"payload\"][\"headers\"]} | |\n| body_plain = _extract_body(msg[\"payload\"]) | |\n| body_html = _extract_html(msg[\"payload\"]) | |\n| attachments = _extract_attachments(msg[\"payload\"]) | |\n| if args.json: | |\n| output = { | |\n| \"id\": msg[\"id\"], | |\n| \"thread_id\": msg[\"threadId\"], | |\n| \"from\": headers.get(\"From\", \"\"), | |\n| \"to\": headers.get(\"To\", \"\"), | |\n| \"cc\": headers.get(\"Cc\", \"\"), | |\n| \"subject\": headers.get(\"Subject\", \"\"), | |\n| \"date\": headers.get(\"Date\", \"\"), | |\n| \"body_plain\": body_plain, | |\n| \"body_html\": body_html, | |\n| \"attachments\": attachments, | |\n| } | |\n| print(json.dumps(output, indent=2, default=str)) | |\n| else: | |\n| print(f\"From: {headers.get('From', '?')}\") | |\n| print(f\"To: {headers.get('To', '?')}\") | |\n| cc = headers.get(\"Cc\", \"\") | |\n| if cc: | |\n| print(f\"CC: {cc}\") | |\n| print(f\"Date: {headers.get('Date', '?')}\") | |\n| print(f\"Subject: {headers.get('Subject', '?')}\") | |\n| print(\"-\" * 60) | |\n| print(body_plain or body_html or \"(no body)\") | |\n| if attachments: | |\n| print(\"-\" * 60) | |\n| print(f\"Attachments: {', '.join(attachments)}\") | |\n| def cmd_search(args): | |\n| \"\"\"Search emails with Gmail query syntax.\"\"\" | |\n| service = get_service(args.account) | |\n| try: | |\n| result = service.users().messages().list(userId=\"me\", q=args.query, maxResults=args.limit).execute() | |\n| except Exception as e: | |\n| print(f\"ERROR: Search failed: {e}\", file=sys.stderr) | |\n| sys.exit(1) | |\n| msg_refs = result.get(\"messages\", []) | |\n| if args.json: | |\n| output = [] | |\n| for ref in msg_refs: | |\n| msg = service.users().messages().get(userId=\"me\", id=ref[\"id\"], format=\"metadata\", | |\n| metadataHeaders=[\"From\", \"Subject\", \"Date\"]).execute() | |\n| headers = {h[\"name\"]: h[\"value\"] for h in msg.get(\"payload\", {}).get(\"headers\", [])} | |\n| output.append({ | |\n| \"id\": msg[\"id\"], | |\n| \"from\": headers.get(\"From\", \"\"), | |\n| \"subject\": headers.get(\"Subject\", \"\"), | |\n| \"date\": headers.get(\"Date\", \"\"), | |\n| \"snippet\": msg.get(\"snippet\", \"\"), | |\n| }) | |\n| print(json.dumps(output, indent=2, default=str)) | |\n| else: | |\n| print(f\"Found {len(msg_refs)} messages matching: {args.query}\\n\") | |\n| for ref in msg_refs: | |\n| msg = service.users().messages().get(userId=\"me\", id=ref[\"id\"], format=\"metadata\", | |\n| metadataHeaders=[\"From\", \"Subject\", \"Date\"]).execute() | |\n| headers = {h[\"name\"]: h[\"value\"] for h in msg.get(\"payload\", {}).get(\"headers\", [])} | |\n| print(f\"{headers.get('Date', '?')} | From: {headers.get('From', '?')}\") | |\n| print(f\" Subject: {headers.get('Subject', '?')}\") | |\n| print(f\" ID: {msg['id']}\") | |\n| print() | |\n| def cmd_send(args): | |\n| \"\"\"Send an email.\"\"\" | |\n| import email.mime.text | |\n| import email.mime.multipart | |\n| import email.mime.base | |\n| import mimetypes | |\n| service = get_service(args.account) | |\n| sender = ACCOUNTS.get(args.account, args.account) | |\n| # Read body from stdin if needed | |\n| body = args.body | |\n| if body == \"-\": | |\n| body = sys.stdin.read() | |\n| if args.attachments: | |\n| msg = email.mime.multipart.MIMEMultipart() | |\n| if args.html: | |\n| msg.attach(email.mime.text.MIMEText(body, \"html\")) | |\n| else: | |\n| msg.attach(email.mime.text.MIMEText(body, \"plain\")) | |\n| for filepath in args.attachments: | |\n| content_type, _ = mimetypes.guess_type(filepath) | |\n| if content_type is None: | |\n| content_type = \"application/octet-stream\" | |\n| main_type, sub_type = content_type.split(\"/\", 1) | |\n| with open(filepath, \"rb\") as f: | |\n| attachment = email.mime.base.MIMEBase(main_type, sub_type) | |\n| attachment.set_payload(f.read()) | |\n| import email.encoders | |\n| email.encoders.encode_base64(attachment) | |\n| attachment.add_header(\"Content-Disposition\", \"attachment\", filename=os.path.basename(filepath)) | |\n| msg.attach(attachment) | |\n| else: | |\n| if args.html: | |\n| msg = email.mime.text.MIMEText(body, \"html\") | |\n| else: | |\n| msg = email.mime.text.MIMEText(body, \"plain\") | |\n| msg[\"to\"] = args.to | |\n| msg[\"from\"] = sender | |\n| msg[\"subject\"] = args.subject | |\n| if args.cc: | |\n| msg[\"cc\"] = args.cc | |\n| if args.bcc: | |\n| msg[\"bcc\"] = args.bcc | |\n| raw = base64.urlsafe_b64encode(msg.as_bytes()).decode() | |\n| try: | |\n| sent = service.users().messages().send(userId=\"me\", body={\"raw\": raw}).execute() | |\n| except Exception as e: | |\n| print(f\"ERROR: Send failed: {e}\", file=sys.stderr) | |\n| sys.exit(1) | |\n| if args.json: | |\n| print(json.dumps({\"status\": \"sent\", \"id\": sent.get(\"id\", \"unknown\")}, indent=2)) | |\n| else: | |\n| print(f\"Email sent successfully. ID: {sent.get('id', '?')}\") | |\n| def cmd_reply(args): | |\n| \"\"\"Reply to an email.\"\"\" | |\n| import email.mime.text | |\n| service = get_service(args.account) | |\n| try: | |\n| original = service.users().messages().get(userId=\"me\", id=args.message_id, format=\"metadata\", | |\n| metadataHeaders=[\"From\", \"To\", \"Subject\", \"Message-ID\", \"References\", \"In-Reply-To\"]).execute() | |\n| except Exception as e: | |\n| print(f\"ERROR: Could not find message {args.message_id}: {e}\", file=sys.stderr) | |\n| sys.exit(1) | |\n| headers = {h[\"name\"]: h[\"value\"] for h in original.get(\"payload\", {}).get(\"headers\", [])} | |\n| thread_id = original[\"threadId\"] | |\n| body = args.body | |\n| if body == \"-\": | |\n| body = sys.stdin.read() | |\n| reply_to = headers.get(\"From\", \"\") | |\n| subject = headers.get(\"Subject\", \"\") | |\n| if not subject.lower().startswith(\"re:\"): | |\n| subject = f\"Re: {subject}\" | |\n| message_id = headers.get(\"Message-ID\", \"\") | |\n| references = headers.get(\"References\", \"\") | |\n| if message_id: | |\n| references = f\"{references} {message_id}\".strip() | |\n| sender = ACCOUNTS.get(args.account, args.account) | |\n| msg = email.mime.text.MIMEText(body) | |\n| msg[\"to\"] = reply_to | |\n| msg[\"from\"] = sender | |\n| msg[\"subject\"] = subject | |\n| if message_id: | |\n| msg[\"In-Reply-To\"] = message_id | |\n| if references: | |\n| msg[\"References\"] = references | |\n| raw = base64.urlsafe_b64encode(msg.as_bytes()).decode() | |\n| try: | |\n| sent = service.users().messages().send(userId=\"me\", body={\"raw\": raw, \"threadId\": thread_id}).execute() | |\n| except Exception as e: | |\n| print(f\"ERROR: Reply failed: {e}\", file=sys.stderr) | |\n| sys.exit(1) | |\n| if args.json: | |\n| print(json.dumps({\"status\": \"sent\", \"id\": sent.get(\"id\", \"unknown\")}, indent=2)) | |\n| else: | |\n| print(f\"Reply sent successfully. ID: {sent.get('id', '?')}\") | |\n| def cmd_labels(args): | |\n| \"\"\"List all labels.\"\"\" | |\n| service = get_service(args.account) | |\n| try: | |\n| result = service.users().labels().list(userId=\"me\").execute() | |\n| except Exception as e: | |\n| print(f\"ERROR: Failed to list labels: {e}\", file=sys.stderr) | |\n| sys.exit(1) | |\n| labels = result.get(\"labels\", []) | |\n| if args.json: | |\n| output = [{\"id\": l[\"id\"], \"name\": l[\"name\"]} for l in labels] | |\n| print(json.dumps(output, indent=2)) | |\n| else: | |\n| for label in sorted(labels, key=lambda l: l[\"name\"]): | |\n| print(f\"{label['name']} (ID: {label['id']})\") | |\n| def cmd_mark_read(args): | |\n| \"\"\"Mark message(s) as read.\"\"\" | |\n| service = get_service(args.account) | |\n| for msg_id in args.message_ids: | |\n| try: | |\n| service.users().messages().modify( | |\n| userId='me', id=msg_id, | |\n| body={'removeLabelIds': ['UNREAD']} | |\n| ).execute() | |\n| print(f\"Marked as read: {msg_id}\") | |\n| except Exception as e: | |\n| print(f\"ERROR marking {msg_id} as read: {e}\", file=sys.stderr) | |\n| def cmd_mark_unread(args): | |\n| \"\"\"Mark message(s) as unread.\"\"\" | |\n| service = get_service(args.account) | |\n| for msg_id in args.message_ids: | |\n| try: | |\n| service.users().messages().modify( | |\n| userId='me', id=msg_id, | |\n| body={'addLabelIds': ['UNREAD']} | |\n| ).execute() | |\n| print(f\"Marked as unread: {msg_id}\") | |\n| except Exception as e: | |\n| print(f\"ERROR marking {msg_id} as unread: {e}\", file=sys.stderr) | |\n| def cmd_archive(args): | |\n| \"\"\"Archive message(s) by removing from inbox.\"\"\" | |\n| service = get_service(args.account) | |\n| for msg_id in args.message_ids: | |\n| try: | |\n| service.users().messages().modify( | |\n| userId='me', id=msg_id, | |\n| body={'removeLabelIds': ['INBOX']} | |\n| ).execute() | |\n| print(f\"Archived: {msg_id}\") | |\n| except Exception as e: | |\n| print(f\"ERROR archiving {msg_id}: {e}\", file=sys.stderr) | |\n| def cmd_star(args): | |\n| \"\"\"Star message(s).\"\"\" | |\n| service = get_service(args.account) | |\n| for msg_id in args.message_ids: | |\n| try: | |\n| service.users().messages().modify( | |\n| userId='me', id=msg_id, | |\n| body={'addLabelIds': ['STARRED']} | |\n| ).execute() | |\n| print(f\"Starred: {msg_id}\") | |\n| except Exception as e: | |\n| print(f\"ERROR starring {msg_id}: {e}\", file=sys.stderr) | |\n| def cmd_unstar(args): | |\n| \"\"\"Remove star from message(s).\"\"\" | |\n| service = get_service(args.account) | |\n| for msg_id in args.message_ids: | |\n| try: | |\n| service.users().messages().modify( | |\n| userId='me', id=msg_id, | |\n| body={'removeLabelIds': ['STARRED']} | |\n| ).execute() | |\n| print(f\"Unstarred: {msg_id}\") | |\n| except Exception as e: | |\n| print(f\"ERROR unstarring {msg_id}: {e}\", file=sys.stderr) | |\n| def cmd_trash(args): | |\n| \"\"\"Move message(s) to trash.\"\"\" | |\n| service = get_service(args.account) | |\n| for msg_id in args.message_ids: | |\n| try: | |\n| service.users().messages().trash(userId='me', id=msg_id).execute() | |\n| print(f\"Trashed: {msg_id}\") | |\n| except Exception as e: | |\n| print(f\"ERROR trashing {msg_id}: {e}\", file=sys.stderr) | |\n| def cmd_accounts(args): | |\n| \"\"\"List configured accounts.\"\"\" | |\n| print(\"Configured account aliases:\") | |\n| for alias, email in ACCOUNTS.items(): | |\n| _, safe_name = _resolve_account(alias) | |\n| token_file = CONFIG_DIR / f\"token_{safe_name}.json\" | |\n| status = \"authenticated\" if token_file.exists() else \"NOT SET UP\" | |\n| print(f\" {alias}: {email} [{status}]\") | |\n| def cmd_setup(args): | |\n| \"\"\"Print setup instructions.\"\"\" | |\n| print(\"\"\" | |\n| Gmail Skill Setup | |\n| ================= | |\n| You only need ONE Google Cloud project for all accounts. | |\n| 1. Go to https://console.cloud.google.com/ | |\n| 2. Create a project (e.g. \"Claude Gmail Access\") | |\n| 3. Enable the Gmail API (APIs & Services > Library) | |\n| 4. Configure OAuth consent screen: | |\n| - External type | |\n| - Add ALL email addresses as test users | |\n| - Add scope: https://mail.google.com/ | |\n| 5. Create OAuth credentials: | |\n| - APIs & Services > Credentials > Create > OAuth client ID > Desktop app | |\n| - Download the JSON file | |\n| 6. Copy the SAME JSON file for each account (just rename): | |\n| \"\"\") | |\n| for alias, email in ACCOUNTS.items(): | |\n| _, safe_name = _resolve_account(alias) | |\n| print(f\" cp client_secret.json ~/.claude/skills/gmail/config/client_secret_{safe_name}.json\") | |\n| print(\"\"\" | |\n| 7. Authorize each account (browser opens for OAuth): | |\n| python3 gmail_cli.py --account personal list --limit 1 | |\n| python3 gmail_cli.py --account work list --limit 1 | |\n| (etc.) | |\n| 8. IMPORTANT: Publish your OAuth app (APIs & Services > OAuth consent screen | |\n| > Publish App) or refresh tokens expire after 7 days! | |\n| \"\"\") | |\n| def main(): | |\n| parser = argparse.ArgumentParser( | |\n| description=\"Gmail CLI for Claude Code\", | |\n| formatter_class=argparse.RawDescriptionHelpFormatter | |\n| ) | |\n| parser.add_argument( | |\n| \"--account\", \"-a\", | |\n| default=\"personal\", | |\n| help=\"Account to use: personal, work, business (or full email)\" | |\n| ) | |\n| parser.add_argument( | |\n| \"--json\", \"-j\", | |\n| action=\"store_true\", | |\n| help=\"Output in JSON format\" | |\n| ) | |\n| subparsers = parser.add_subparsers(dest=\"command\", help=\"Commands\") | |\n| # List command | |\n| list_parser = subparsers.add_parser(\"list\", help=\"List recent emails\") | |\n| list_parser.add_argument(\"--limit\", \"-n\", type=int, default=10, help=\"Number of emails\") | |\n| list_parser.add_argument(\"--unread\", \"-u\", action=\"store_true\", help=\"Only unread\") | |\n| list_parser.add_argument(\"--starred\", \"-s\", action=\"store_true\", help=\"Only starred\") | |\n| list_parser.add_argument(\"--label\", \"-l\", help=\"Filter by label\") | |\n| list_parser.set_defaults(func=cmd_list) | |\n| # Read command | |\n| read_parser = subparsers.add_parser(\"read\", help=\"Read a specific email\") | |\n| read_parser.add_argument(\"message_id\", help=\"Message ID\") | |\n| read_parser.set_defaults(func=cmd_read) | |\n| # Search command | |\n| search_parser = subparsers.add_parser(\"search\", help=\"Search emails\") | |\n| search_parser.add_argument(\"query\", help=\"Gmail search query\") | |\n| search_parser.add_argument(\"--limit\", \"-n\", type=int, default=20, help=\"Max results\") | |\n| search_parser.set_defaults(func=cmd_search) | |\n| # Send command | |\n| send_parser = subparsers.add_parser(\"send\", help=\"Send an email\") | |\n| send_parser.add_argument(\"--to\", \"-t\", required=True, help=\"Recipient\") | |\n| send_parser.add_argument(\"--subject\", \"-s\", required=True, help=\"Subject\") | |\n| send_parser.add_argument(\"--body\", \"-b\", default=\"-\", help=\"Body (use - for stdin)\") | |\n| send_parser.add_argument(\"--cc\", help=\"CC recipients\") | |\n| send_parser.add_argument(\"--bcc\", help=\"BCC recipients\") | |\n| send_parser.add_argument(\"--html\", action=\"store_true\", help=\"Body is HTML\") | |\n| send_parser.add_argument(\"--attachments\", nargs=\"*\", help=\"File paths to attach\") | |\n| send_parser.add_argument(\"--no-signature\", action=\"store_true\", help=\"Omit signature\") | |\n| send_parser.set_defaults(func=cmd_send) | |\n| # Reply command | |\n| reply_parser = subparsers.add_parser(\"reply\", help=\"Reply to an email\") | |\n| reply_parser.add_argument(\"message_id\", help=\"Message ID to reply to\") | |\n| reply_parser.add_argument(\"--body\", \"-b\", default=\"-\", help=\"Body (use - for stdin)\") | |\n| reply_parser.set_defaults(func=cmd_reply) | |\n| # Labels command | |\n| labels_parser = subparsers.add_parser(\"labels\", help=\"List all labels\") | |\n| labels_parser.set_defaults(func=cmd_labels) | |\n| # Mark read/unread commands | |\n| mark_read_parser = subparsers.add_parser(\"mark-read\", help=\"Mark messages as read\") | |\n| mark_read_parser.add_argument(\"message_ids\", nargs=\"+\", help=\"Message IDs\") | |\n| mark_read_parser.set_defaults(func=cmd_mark_read) | |\n| mark_unread_parser = subparsers.add_parser(\"mark-unread\", help=\"Mark messages as unread\") | |\n| mark_unread_parser.add_argument(\"message_ids\", nargs=\"+\", help=\"Message IDs\") | |\n| mark_unread_parser.set_defaults(func=cmd_mark_unread) | |\n| # Archive command | |\n| archive_parser = subparsers.add_parser(\"archive\", help=\"Archive messages (remove from inbox)\") | |\n| archive_parser.add_argument(\"message_ids\", nargs=\"+\", help=\"Message IDs\") | |\n| archive_parser.set_defaults(func=cmd_archive) | |\n| # Star command | |\n| star_parser = subparsers.add_parser(\"star\", help=\"Star messages\") | |\n| star_parser.add_argument(\"message_ids\", nargs=\"+\", help=\"Message IDs\") | |\n| star_parser.set_defaults(func=cmd_star) | |\n| # Unstar command | |\n| unstar_parser = subparsers.add_parser(\"unstar\", help=\"Remove star from messages\") | |\n| unstar_parser.add_argument(\"message_ids\", nargs=\"+\", help=\"Message IDs\") | |\n| unstar_parser.set_defaults(func=cmd_unstar) | |\n| # Trash command | |\n| trash_parser = subparsers.add_parser(\"trash\", help=\"Move messages to trash\") | |\n| trash_parser.add_argument(\"message_ids\", nargs=\"+\", help=\"Message IDs\") | |\n| trash_parser.set_defaults(func=cmd_trash) | |\n| # Accounts command | |\n| accounts_parser = subparsers.add_parser(\"accounts\", help=\"List configured accounts\") | |\n| accounts_parser.set_defaults(func=cmd_accounts) | |\n| # Setup command | |\n| setup_parser = subparsers.add_parser(\"setup\", help=\"Show setup instructions\") | |\n| setup_parser.set_defaults(func=cmd_setup) | |\n| args = parser.parse_args() | |\n| if not args.command: | |\n| parser.print_help() | |\n| sys.exit(1) | |\n| args.func(args) | |\n| if __name__ == \"__main__\": | |\n| main() |", "url": "https://wpnews.pro/news/gmail-cli-for-claude-code-multi-account-gmail-access-via-command-line", "canonical_source": "https://gist.github.com/jbeck1563/b829426e9724663be413ac10dd4f05e1", "published_at": "2026-07-24 05:16:27+00:00", "updated_at": "2026-07-24 05:29:28.579552+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Google Gmail API", "Claude Code"], "alternates": {"html": "https://wpnews.pro/news/gmail-cli-for-claude-code-multi-account-gmail-access-via-command-line", "markdown": "https://wpnews.pro/news/gmail-cli-for-claude-code-multi-account-gmail-access-via-command-line.md", "text": "https://wpnews.pro/news/gmail-cli-for-claude-code-multi-account-gmail-access-via-command-line.txt", "jsonld": "https://wpnews.pro/news/gmail-cli-for-claude-code-multi-account-gmail-access-via-command-line.jsonld"}}