Agent Skills: Gmail Operations Skill

Use when user says "search emails from [person]", "download email attachments", or "create draft to [recipient]". Automatically searches, reads, downloads attachments, creates drafts, and sends messages via Gmail API. Handles client communications and email processing.

UncategorizedID: tekliner/improvado-agentic-frameworks-and-skills/gmail-operations

Install this agent skill to your local

pnpm dlx add-skill https://github.com/tekliner/improvado-agentic-frameworks-and-skills/tree/HEAD/skills/gmail-operations

Skill Files

Browse the full folder contents for gmail-operations.

Download Skill

Loading file tree…

skills/gmail-operations/SKILL.md

Skill Metadata

Name
gmail-operations
Description
Use when user says "search emails from [person]", "download email attachments", or "create draft to [recipient]". Automatically searches, reads, downloads attachments, creates drafts, and sends messages via Gmail API. Handles client communications and email processing.

Gmail Operations Skill

<!-- Updated by Claude Code - Session: Current session - 2025-11-10 --> <!-- Changes: Added references to new utility scripts 02_decode_gmail_url_id.py and 03_get_gmail_thread_id_browser.py -->

This skill enables comprehensive Gmail management through the Gmail API client located at /data_sources/gmail/gmail_client.py.

★ Insight ───────────────────────────────────── Gmail Client Architecture:

  1. OAuth2 Authentication - Token-based auth with auto-refresh
  2. Full CRUD Operations - Search, read, create drafts, send, reply, forward
  3. Attachment Handling - Extract attachments with Content-ID for inline images ─────────────────────────────────────────────────

Quick Start: How This Skill Works

⚠️ CRITICAL: Gmail URL IDs Don't Work!

Gmail URL format: https://mail.google.com/mail/u/0/#inbox/FMfcgzQcqbWzsKXlVrtllTxwjvvKdPsz

  • The ID after #inbox/ is NOT the API message ID
  • You CANNOT use it directly with Gmail API

✅ How to Find Emails:

  1. Search by criteria (BEST): sender, subject, date, has:attachment
  2. Extract from browser: Use DevTools console to get real ID
  3. List recent emails: Browse through recent messages

📁 Default Save Location:

algorithms/A8_G&A_div/Daniel Personal/Daniel_communications/emails/YYYY-MM-DD/
├── 00_email_content.md          # Email body and metadata
├── original_filename.html       # Preserved attachment names
└── mermaid_diagram.md           # Extracted diagrams (if found)

🔧 Common Operations:

Search email:

python data_sources/gmail/gmail_client.py --method search_messages \
  --query "from:sender@example.com has:attachment" --limit 5

Download email with attachments:

from data_sources.gmail.gmail_client import GmailClient
import base64, os
from datetime import datetime

client = GmailClient('data_sources/gmail/credentials').authenticate()

# 1. Search for email
messages = client.search_messages("from:sender@example.com", max_results=5)

# 2. Get full message
message = client.get_message_content(messages[0]['id'], format='full')

# 3. Extract attachments
attachments = client.extract_message_attachments(message)

# 4. Download each attachment
today = datetime.now().strftime('%Y-%m-%d')
output_dir = f"algorithms/A8_G&A_div/Daniel Personal/Daniel_communications/emails/{today}"
os.makedirs(output_dir, exist_ok=True)

for att in attachments:
    if att['attachmentId']:
        att_data = client.service.users().messages().attachments().get(
            userId='me', messageId=messages[0]['id'], id=att['attachmentId']
        ).execute()

        file_data = base64.urlsafe_b64decode(att_data['data'])
        with open(f"{output_dir}/{att['filename']}", 'wb') as f:
            f.write(file_data)

📋 What Gets Saved:

  • ✅ Full email content as markdown
  • ✅ All attachments with original filenames
  • ✅ Gmail URL link for reference
  • ✅ Message ID and Thread ID
  • ✅ Sender, recipient, date metadata
  • ✅ Mermaid diagram links (if found in email body)

🚨 NOT Hardcoded:

  • ❌ No hardcoded email addresses
  • ❌ No hardcoded message IDs
  • ❌ No hardcoded folder paths (uses dated folders)
  • ✅ Dynamic search by criteria
  • ✅ Automatic folder creation by date
  • ✅ Preserves original attachment names

When to Use This Skill

Use this skill when:

  • Search emails with advanced Gmail queries
  • Read email content and extract attachments
  • Download HTML attachments from specific emails
  • Create and manage email drafts
  • Send emails or replies
  • Process email threads
  • Extract inline images with proper positioning

Quick Start Checklist

When user wants to work with Gmail:

[ ] 1. Activate claude_venv (ALWAYS required)
[ ] 2. Determine operation: search, read, download, or compose
[ ] 3. For search: build query with from:/subject:/has:attachment
[ ] 4. For download: get message_id, then extract attachments
[ ] 5. Save to dated folder: Daniel_communications/emails/YYYY-MM-DD/
[ ] 6. Preserve original filenames and create 00_email_content.md

5-Second Decision Tree:

  • User mentions email search? → search_messages() with query
  • User wants attachments? → get_message_content() + extract + download
  • User wants to compose? → create_draft() or send_message()

Practical Workflow

BEFORE any Gmail operation:

  1. Activate environment: source claude_venv/bin/activate
  2. Determine task type:
    • Search → client.search_messages(query="...")
    • Download → get_message_content() + extract_message_attachments()
    • Compose → create_draft() with HTML formatting
  3. Handle attachments properly (use attachment API, not inline data)
  4. Save to dated folders (YYYY-MM-DD structure)

Example rapid application:

User: "Download attachments from email about dashboard from james@example.com"

Agent thinks:
- Search needed: from:james@example.com subject:dashboard has:attachment
- Get message_id from search results
- Extract attachments with proper API call
- Save to: algorithms/A8_G&A_div/Daniel Personal/Daniel_communications/emails/2025-11-11/

Authentication & Setup

Required Files

# Credentials location
/data_sources/gmail/credentials/credentials.json  # OAuth2 client config
/data_sources/gmail/credentials/token.pickle      # Auto-generated auth token

Environment Setup

# ALWAYS use claude_venv
source claude_venv/bin/activate

# Or use full Python path
$PROJECT_ROOT/claude_venv/bin/python

Authentication Flow

  • First run triggers OAuth2 browser flow (port 8888)
  • Token auto-refreshes when expired
  • Token stored in token.pickle for reuse

Core Operations

1. Search and Read Emails

Search Emails by Query

# Search by sender
python data_sources/gmail/gmail_client.py --method search_messages \
  --query "from:sender@example.com" --limit 10

# Search with multiple criteria
python data_sources/gmail/gmail_client.py --method search_messages \
  --query "from:client@company.com has:attachment after:2024/11/01" --limit 5

# Find unread emails with attachments
python data_sources/gmail/gmail_client.py --method search_messages \
  --query "is:unread has:attachment"

Advanced Search Operators:

  • from:email - Sender filter
  • to:email - Recipient filter
  • subject:"text" - Subject contains
  • has:attachment - Has attachments
  • is:unread - Unread messages
  • after:YYYY/MM/DD - After date
  • before:YYYY/MM/DD - Before date
  • label:name - Specific label

Get Email Content

# Get full message with attachments info
python data_sources/gmail/gmail_client.py --method get_message_content \
  --message-id "MESSAGE_ID"

Python Usage for Attachments:

from data_sources.gmail.gmail_client import GmailClient

client = GmailClient('data_sources/gmail/credentials')
client.authenticate()

# Get message
message = client.get_message_content(message_id, format='full')

# Extract text content
content = client.extract_message_text(message)
print(content['plain_text'])
print(content['html'])

# Get attachments list
attachments = client.extract_message_attachments(message)
for att in attachments:
    print(f"File: {att['filename']}, Size: {att['size']}, Type: {att['mimeType']}")

2. Download Attachments

CRITICAL Pattern for Downloading Attachments:

from data_sources.gmail.gmail_client import GmailClient
import base64
import os

client = GmailClient('data_sources/gmail/credentials')
client.authenticate()

# Get full message
message = client.get_message_content(message_id, format='full')

# Extract attachments metadata
attachments = client.extract_message_attachments(message)

# Download each attachment
for attachment in attachments:
    if attachment['attachmentId']:
        # Download attachment data
        att_data = client.service.users().messages().attachments().get(
            userId='me',
            messageId=message_id,
            id=attachment['attachmentId']
        ).execute()

        # Decode and save
        file_data = base64.urlsafe_b64decode(att_data['data'])

        # Save to appropriate folder
        output_path = os.path.join(output_dir, attachment['filename'])
        with open(output_path, 'wb') as f:
            f.write(file_data)

        print(f"Downloaded: {attachment['filename']} ({attachment['size']} bytes)")

Example: Download HTML Attachment from Specific Email

def download_email_attachments(message_id, output_dir):
    """Download all attachments from specific email"""
    client = GmailClient('data_sources/gmail/credentials')
    client.authenticate()

    # Get message
    message = client.get_message_content(message_id, format='full')

    # Get subject for context
    headers = message['payload']['headers']
    subject = next((h['value'] for h in headers if h['name'] == 'Subject'), 'No Subject')
    print(f"Processing email: {subject}")

    # Extract attachments
    attachments = client.extract_message_attachments(message)

    # Create output directory
    os.makedirs(output_dir, exist_ok=True)

    downloaded = []
    for att in attachments:
        if att['attachmentId']:
            # Download attachment
            att_data = client.service.users().messages().attachments().get(
                userId='me',
                messageId=message_id,
                id=att['attachmentId']
            ).execute()

            # Decode and save
            file_data = base64.urlsafe_b64decode(att_data['data'])
            output_path = os.path.join(output_dir, att['filename'])

            with open(output_path, 'wb') as f:
                f.write(file_data)

            downloaded.append({
                'filename': att['filename'],
                'path': output_path,
                'size': att['size'],
                'type': att['mimeType']
            })

            print(f"✓ Downloaded: {att['filename']}")

    return downloaded, subject

# Usage
message_id = "FMfcgzQcqbWzsKXlVrtllTxwjvvKdPsz"
output_dir = "algorithms/A8_G&A_div/Daniel Personal/Daniel_communications/emails/2025-11-10"
attachments, subject = download_email_attachments(message_id, output_dir)

3. Extract Inline Images

CRITICAL: Inline Image Processing

Emails with inline images reference them via cid: (Content-ID) in HTML. You MUST:

  1. Parse HTML to find <img src="cid:..."> tags
  2. Match Content-ID from attachment headers
  3. Insert images at correct positions in markdown
from bs4 import BeautifulSoup
import html2text

def process_email_with_inline_images(client, message_id):
    """Extract email with inline images properly positioned"""

    # Get message
    message = client.get_message_content(message_id, format='full')

    # Extract text content
    content = client.extract_message_text(message)

    # Get attachments with Content-IDs
    attachments = []
    def process_parts(parts):
        for part in parts:
            if part.get('filename'):
                # Get Content-ID from headers
                content_id = None
                if 'headers' in part:
                    for header in part['headers']:
                        if header['name'].lower() == 'content-id':
                            content_id = header['value'].strip('<>')
                            break

                attachments.append({
                    'filename': part['filename'],
                    'mimeType': part.get('mimeType'),
                    'attachmentId': part['body'].get('attachmentId'),
                    'content_id': content_id
                })

            if 'parts' in part:
                process_parts(part['parts'])

    if 'payload' in message and 'parts' in message['payload']:
        process_parts(message['payload']['parts'])

    # Process HTML to insert images
    if content['html']:
        soup = BeautifulSoup(content['html'], 'html.parser')

        # Create CID map
        cid_map = {att['content_id']: att for att in attachments if att.get('content_id')}

        # Replace cid: references
        for img in soup.find_all('img'):
            src = img.get('src', '')
            if src.startswith('cid:'):
                cid = src[4:]
                if cid in cid_map:
                    # Download and save image
                    att_data = client.service.users().messages().attachments().get(
                        userId='me',
                        messageId=message_id,
                        id=cid_map[cid]['attachmentId']
                    ).execute()

                    # Save image
                    file_data = base64.urlsafe_b64decode(att_data['data'])
                    img_path = f"images/{cid_map[cid]['filename']}"
                    os.makedirs('images', exist_ok=True)
                    with open(img_path, 'wb') as f:
                        f.write(file_data)

                    # Replace with markdown
                    img.replace_with(f"\n\n![{cid_map[cid]['filename']}]({img_path})\n\n")

        # Convert to markdown preserving links
        h = html2text.HTML2Text()
        h.body_width = 0
        h.ignore_links = False
        h.protect_links = True
        markdown = h.handle(str(soup))

        return markdown, attachments

    return content['plain_text'], attachments

4. Create and Send Drafts

Create Plain Text Draft

python data_sources/gmail/gmail_client.py --method create_draft \
  --to "recipient@email.com" \
  --subject "Meeting Follow-up" \
  --body "Thank you for the meeting today..."

Create HTML Draft (Python Required)

from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from data_sources.gmail.gmail_client import GmailClient
import base64

client = GmailClient('data_sources/gmail/credentials')
client.authenticate()

# Create HTML message
message = MIMEMultipart('alternative')
message['To'] = "recipient@example.com"
message['Subject'] = "Dashboard Review: Q4 Results"

html_body = """
<html><body>
<p>Hi Team,</p>

<h3>📊 Key Metrics:</h3>
<ul>
<li><strong>Revenue:</strong> $1.2M (+15% MoM)</li>
<li><strong>Active Users:</strong> 45K (+8% MoM)</li>
</ul>

<h3>🎯 Action Items:</h3>
<ol>
<li><strong>This week:</strong> Review Q4 dashboard</li>
<li><strong>Next week:</strong> Finalize budget allocation</li>
</ol>

<p>Best regards,<br>Daniel</p>
</body></html>
"""

html_part = MIMEText(html_body, 'html')
message.attach(html_part)

# Create draft
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
draft_body = {'message': {'raw': raw_message}}

draft = client.service.users().drafts().create(
    userId='me',
    body=draft_body
).execute()

print(f"Draft created: {draft['id']}")
print(f"URL: https://mail.google.com/mail/u/0/#drafts/{draft['id']}")

Send Draft

python data_sources/gmail/gmail_client.py --method send_draft \
  --draft-id "DRAFT_ID"

5. Thread Operations

Get Thread ID from Message ID

# Message ID from Gmail URL format
python data_sources/gmail/gmail_client.py --method get_thread_id \
  --message-id "MESSAGE_ID"

Get All Messages in Thread

python data_sources/gmail/gmail_client.py --method get_thread_messages \
  --thread-id "THREAD_ID"

6. Reply and Forward

Reply to Message

python data_sources/gmail/gmail_client.py --method reply_to_message \
  --message-id "MESSAGE_ID" \
  --body "Thank you for your email. Here's my response..."

Forward Message

python data_sources/gmail/gmail_client.py --method forward_message \
  --message-id "MESSAGE_ID" \
  --to "colleague@example.com"

Common Workflows

Workflow 1: Download Email with Attachments

Task: Download specific email with HTML attachment and save to dated folder

import os
from datetime import datetime
from data_sources.gmail.gmail_client import GmailClient
import base64

def download_email_complete(message_id, base_dir):
    """Download email content and all attachments"""

    client = GmailClient('data_sources/gmail/credentials')
    client.authenticate()

    # Get message
    message = client.get_message_content(message_id, format='full')

    # Extract headers
    headers = message['payload']['headers']
    subject = next((h['value'] for h in headers if h['name'] == 'Subject'), 'No Subject')
    from_email = next((h['value'] for h in headers if h['name'] == 'From'), 'Unknown')
    date = next((h['value'] for h in headers if h['name'] == 'Date'), 'Unknown')

    # Create dated directory
    today = datetime.now().strftime('%Y-%m-%d')
    output_dir = os.path.join(base_dir, today)
    os.makedirs(output_dir, exist_ok=True)

    # Save email content
    content = client.extract_message_text(message)

    email_md = f"""# Email: {subject}

**From:** {from_email}
**Date:** {date}
**Message ID:** {message_id}

## Content

{content['plain_text'] or '(HTML only - see attachment)'}

## Attachments

"""

    # Download attachments
    attachments = client.extract_message_attachments(message)

    for att in attachments:
        if att['attachmentId']:
            # Download
            att_data = client.service.users().messages().attachments().get(
                userId='me',
                messageId=message_id,
                id=att['attachmentId']
            ).execute()

            # Save
            file_data = base64.urlsafe_b64decode(att_data['data'])
            att_path = os.path.join(output_dir, att['filename'])

            with open(att_path, 'wb') as f:
                f.write(file_data)

            email_md += f"- [{att['filename']}](./{att['filename']}) ({att['size']} bytes, {att['mimeType']})\n"

    # Save email metadata
    md_path = os.path.join(output_dir, '00_email_content.md')
    with open(md_path, 'w') as f:
        f.write(email_md)

    print(f"✓ Email saved to: {output_dir}")
    print(f"✓ Attachments: {len(attachments)}")

    return output_dir

# Usage
message_id = "FMfcgzQcqbWzsKXlVrtllTxwjvvKdPsz"
base_dir = "algorithms/A8_G&A_div/Daniel Personal/Daniel_communications/emails"
output_dir = download_email_complete(message_id, base_dir)

Workflow 2: Extract Mermaid Diagram from HTML

Task: Extract Mermaid diagram code from HTML attachment

from bs4 import BeautifulSoup
import re

def extract_mermaid_from_html(html_path):
    """Extract Mermaid diagram code from HTML file"""

    with open(html_path, 'r', encoding='utf-8') as f:
        html_content = f.read()

    soup = BeautifulSoup(html_content, 'html.parser')

    # Find mermaid code blocks
    mermaid_blocks = []

    # Method 1: Look for <div class="mermaid">
    for div in soup.find_all('div', class_='mermaid'):
        mermaid_blocks.append(div.get_text().strip())

    # Method 2: Look for <pre><code class="language-mermaid">
    for code in soup.find_all('code', class_='language-mermaid'):
        mermaid_blocks.append(code.get_text().strip())

    # Method 3: Look for script tags with mermaid content
    for script in soup.find_all('script'):
        script_text = script.get_text()
        if 'mermaid' in script_text.lower():
            # Try to extract diagram definition
            matches = re.findall(r'(?:graph|flowchart|sequenceDiagram|classDiagram)[^`]*', script_text)
            mermaid_blocks.extend(matches)

    return mermaid_blocks

# Usage
html_file = "algorithms/A8_G&A_div/Daniel Personal/Daniel_communications/emails/2025-11-10/diagram.html"
diagrams = extract_mermaid_from_html(html_file)

# Save to markdown
md_output = "algorithms/A8_G&A_div/Daniel Personal/Daniel_communications/emails/2025-11-10/diagram.md"

with open(md_output, 'w') as f:
    f.write("# Mermaid Diagrams\n\n")
    for i, diagram in enumerate(diagrams, 1):
        f.write(f"## Diagram {i}\n\n")
        f.write("```mermaid\n")
        f.write(diagram)
        f.write("\n```\n\n")

print(f"✓ Extracted {len(diagrams)} diagrams to {md_output}")

Daniel Personal Communications Folder Structure

CRITICAL: Folder Organization

algorithms/A8_G&A_div/Daniel Personal/Daniel_communications/
├── emails/
│   └── YYYY-MM-DD/          # Dated folders for emails
│       ├── 00_email_content.md
│       ├── attachment.html
│       └── diagram.md
├── calls/
│   └── YYYY-MM-DD/
└── meetings/
    └── YYYY-MM-DD/

Naming Rules:

  • Use date format: YYYY-MM-DD (e.g., 2025-11-10)
  • Email summary: 00_email_content.md
  • Preserve original attachment filenames
  • Extract diagrams to separate .md files

Best Practices

1. Always Use Existing Client

  • NEVER create custom Gmail scripts
  • ALWAYS use /data_sources/gmail/gmail_client.py
  • Reuse examples from existing folders

2. Environment

  • ALWAYS activate claude_venv first
  • Check authentication before operations
  • Token auto-refreshes when expired

3. Attachment Handling

  • Download attachments with proper error handling
  • Preserve original filenames
  • Save to dated folders in appropriate locations
  • Extract inline images at correct positions

4. HTML Processing

  • Use BeautifulSoup for HTML parsing
  • Use html2text for markdown conversion
  • Preserve all links with protect_links=True
  • Extract Mermaid diagrams separately

5. Client Communications

  • CRITICAL: Verify client isolation (no cross-contamination)
  • Use proper folder structure
  • Include email metadata in saved files
  • Link to original Gmail URL when useful

Reference: Gmail Client Methods

Search & Read

  • list_messages(max_results, query) - List messages with optional filter
  • search_messages(search_query, max_results) - Search with query
  • get_message_content(message_id, format) - Get full message
  • extract_message_text(message) - Extract plain text and HTML
  • extract_message_attachments(message) - Get attachment metadata

Drafts

  • get_drafts(max_results) - List drafts
  • create_draft(to_email, subject, body, from_email) - Create plain text draft
  • update_draft(draft_id, to_email, subject, body) - Update draft
  • send_draft(draft_id) - Send draft
  • delete_draft(draft_id) - Delete draft

Threads

  • list_threads(max_results, query) - List threads
  • get_thread_messages(thread_id) - Get all messages in thread
  • get_thread_id(message_id) - Get thread ID from message

Actions

  • send_message(to_email, subject, body, attachments) - Send new email
  • reply_to_message(message_id, body) - Reply to message
  • forward_message(message_id, to_email) - Forward message
  • mark_as_read(message_id) - Mark as read
  • mark_as_unread(message_id) - Mark as unread

Troubleshooting

Authentication Issues

# Delete token to re-authenticate
rm data_sources/gmail/credentials/token.pickle

# Run any command to trigger re-auth
python data_sources/gmail/gmail_client.py --method list_messages --limit 1

Message ID from Gmail URL

CRITICAL: Gmail URL IDs (like FMfcgzQcqbWzsKXlVrtllTxwjvvKdPsz) are NOT the same as API message IDs!

Problem:

URL: https://mail.google.com/mail/u/0/#inbox/FMfcgzQcqbWzsKXlVrtllTxwjvvKdPsz
URL ID: FMfcgzQcqbWzsKXlVrtllTxwjvvKdPsz  ❌ DOES NOT WORK with Gmail API

Utility Scripts Available:

  • /data_sources/gmail/02_decode_gmail_url_id.py - Attempts to decode Gmail URL IDs (limited success)
  • /data_sources/gmail/03_get_gmail_thread_id_browser.py - Extracts thread ID using Safari/AppleScript automation

Solutions:

  1. Search by criteria (RECOMMENDED):

    # Search by subject
    python data_sources/gmail/gmail_client.py --method search_messages \
      --query "subject:\"keyword from email\"" --limit 5
    
    # Search by sender and date
    python data_sources/gmail/gmail_client.py --method search_messages \
      --query "from:sender@example.com after:2025/11/10" --limit 10
    
    # Search by attachment type
    python data_sources/gmail/gmail_client.py --method search_messages \
      --query "has:attachment filename:html" --limit 5
    
  2. Extract from browser (if email is open):

    Manual Method:

    • Open email in Gmail
    • Open DevTools (F12)
    • Run in Console:
      document.querySelector('[data-legacy-thread-id]').getAttribute('data-legacy-thread-id')
      

    Automated Method (macOS):

    # Uses AppleScript to automate Safari and extract thread ID
    python data_sources/gmail/03_get_gmail_thread_id_browser.py
    
    • Opens URL in Safari
    • Extracts thread ID automatically
    • Falls back to search if extraction fails
  3. List recent emails and find manually:

    python data_sources/gmail/gmail_client.py --method list_messages --limit 20
    

Best Practice: Always search by email attributes (sender, subject, date) rather than trying to use URL IDs.

HTML Attachment Not Downloading

  • Check attachmentId is present
  • Verify attachment type is correct
  • Use proper base64 decoding: base64.urlsafe_b64decode()

Inline Images Not Showing

  • Check for cid: references in HTML
  • Extract Content-ID from attachment headers
  • Download images before converting HTML to markdown

Quick Reference Commands

# Search emails
python data_sources/gmail/gmail_client.py --method search_messages \
  --query "QUERY" --limit N

# Get email content
python data_sources/gmail/gmail_client.py --method get_message_content \
  --message-id "ID"

# Create draft
python data_sources/gmail/gmail_client.py --method create_draft \
  --to "email" --subject "subject" --body "body"

# List drafts
python data_sources/gmail/gmail_client.py --method get_drafts

# Send draft
python data_sources/gmail/gmail_client.py --method send_draft \
  --draft-id "ID"

Remember: Gmail operations are critical for client communication. Always verify folder structure, preserve original content, and ensure proper client data isolation.