- Add is_section_enabled() to support per-section enable/disable in config - Update Python path from 3.13 to 3.14 for gmail venv - Disable tasks section by default (enabled: false in config) - Apply code formatting improvements (black/ruff style) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
162 lines
5.1 KiB
Python
Executable File
162 lines
5.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Gmail collector using existing gmail skill."""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
|
|
def fetch_unread_emails(days: int = 7, max_results: int = 15) -> list:
|
|
"""Fetch unread emails directly using gmail_mcp library."""
|
|
# Set credentials path
|
|
os.environ.setdefault(
|
|
"GMAIL_CREDENTIALS_PATH", os.path.expanduser("~/.gmail-mcp/credentials.json")
|
|
)
|
|
|
|
try:
|
|
# Add gmail venv to path
|
|
venv_site = Path.home() / ".claude/mcp/gmail/venv/lib/python3.14/site-packages"
|
|
if str(venv_site) not in sys.path:
|
|
sys.path.insert(0, str(venv_site))
|
|
|
|
from gmail_mcp.utils.GCP.gmail_auth import get_gmail_service
|
|
|
|
service = get_gmail_service()
|
|
results = (
|
|
service.users()
|
|
.messages()
|
|
.list(
|
|
userId="me", q=f"is:unread newer_than:{days}d", maxResults=max_results
|
|
)
|
|
.execute()
|
|
)
|
|
|
|
emails = []
|
|
for msg in results.get("messages", []):
|
|
detail = (
|
|
service.users()
|
|
.messages()
|
|
.get(
|
|
userId="me",
|
|
id=msg["id"],
|
|
format="metadata",
|
|
metadataHeaders=["From", "Subject"],
|
|
)
|
|
.execute()
|
|
)
|
|
headers = {h["name"]: h["value"] for h in detail["payload"]["headers"]}
|
|
emails.append(
|
|
{
|
|
"from": headers.get("From", "Unknown"),
|
|
"subject": headers.get("Subject", "(no subject)"),
|
|
"id": msg["id"],
|
|
}
|
|
)
|
|
|
|
return emails
|
|
|
|
except Exception as e:
|
|
return [{"error": str(e)}]
|
|
|
|
|
|
def triage_with_sonnet(emails: list) -> str:
|
|
"""Use Sonnet to triage and summarize emails."""
|
|
if not emails or (len(emails) == 1 and "error" in emails[0]):
|
|
error = emails[0].get("error", "Unknown error") if emails else "No data"
|
|
return f"⚠️ Could not fetch emails: {error}"
|
|
|
|
# Build email summary for Sonnet
|
|
email_text = []
|
|
for i, e in enumerate(emails[:10], 1):
|
|
sender = e.get("from", "Unknown").split("<")[0].strip().strip('"')
|
|
subject = e.get("subject", "(no subject)")[:80]
|
|
email_text.append(f"{i}. From: {sender}\n Subject: {subject}")
|
|
|
|
email_context = "\n\n".join(email_text)
|
|
|
|
prompt = f"""You are triaging emails for a morning report. Given these unread emails, provide a brief summary.
|
|
|
|
Format:
|
|
- First line: count and any urgent items (e.g., "5 unread, 1 urgent")
|
|
- Then list top emails with [!] for urgent, or plain bullet
|
|
- Keep each email to one line: sender - subject snippet (max 50 chars)
|
|
- Maximum 5 emails shown
|
|
|
|
Emails:
|
|
{email_context}
|
|
|
|
Output the formatted email section, nothing else."""
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
[
|
|
"/home/will/.local/bin/claude",
|
|
"--print",
|
|
"--model",
|
|
"sonnet",
|
|
"-p",
|
|
prompt,
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
)
|
|
|
|
if result.returncode == 0 and result.stdout.strip():
|
|
return result.stdout.strip()
|
|
except Exception:
|
|
pass
|
|
|
|
# Fallback to basic format
|
|
lines = [f"{len(emails)} unread"]
|
|
for e in emails[:5]:
|
|
sender = e.get("from", "Unknown").split("<")[0].strip().strip('"')[:20]
|
|
subject = e.get("subject", "(no subject)")[:40]
|
|
lines.append(f" • {sender} - {subject}")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def collect(config: dict) -> dict:
|
|
"""Main collector entry point."""
|
|
email_config = config.get("email", {})
|
|
max_display = email_config.get("max_display", 5)
|
|
use_triage = email_config.get("triage", True)
|
|
|
|
emails = fetch_unread_emails(days=7, max_results=max_display + 10)
|
|
|
|
if use_triage and emails and "error" not in emails[0]:
|
|
formatted = triage_with_sonnet(emails)
|
|
else:
|
|
# Basic format or error
|
|
if emails and "error" not in emails[0]:
|
|
lines = [f"{len(emails)} unread"]
|
|
for e in emails[:max_display]:
|
|
sender = e.get("from", "Unknown").split("<")[0].strip().strip('"')[:20]
|
|
subject = e.get("subject", "(no subject)")[:40]
|
|
lines.append(f" • {sender} - {subject}")
|
|
formatted = "\n".join(lines)
|
|
else:
|
|
error = emails[0].get("error", "Unknown") if emails else "No data"
|
|
formatted = f"⚠️ Could not fetch emails: {error}"
|
|
|
|
has_error = emails and len(emails) == 1 and "error" in emails[0]
|
|
|
|
return {
|
|
"section": "Email",
|
|
"icon": "📧",
|
|
"content": formatted,
|
|
"raw": emails if not has_error else None,
|
|
"count": len(emails) if not has_error else 0,
|
|
"error": emails[0].get("error") if has_error else None,
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
config = {"email": {"max_display": 5, "triage": True}}
|
|
result = collect(config)
|
|
print(f"## {result['icon']} {result['section']}")
|
|
print(result["content"])
|