Files
ajarbot/google_tools/gmail_client.py
Jordan Ramos 7697220c74 Refactor: Remove zombie code, fix bugs, and clean documentation
This comprehensive refactoring removes dead code, fixes bugs, and deletes
outdated documentation to make the codebase production-ready.

## Files Deleted (16 files)

### Temporary/zombie files (9 files):
- nul (Windows artifact)
- quick_start.bat (superseded by run.bat)
- scripts/proxmox_ssh.py (hardcoded credentials - security risk)
- scripts/proxmox_ssh.sh (hardcoded credentials - security risk)
- scripts/collection_output.txt (one-time audit output)
- scripts/collect-homelab-config.sh (one-off infrastructure script)
- scripts/collect-remote.sh (one-off infrastructure script)
- memory_workspace/MEMORY.md.old (backup file)
- promtail-config-optimized.yaml (misplaced homelab config)

### Outdated documentation (7 files):
- MCP_MIGRATION.md (migration complete - 2026-02-15)
- QUICK_REFERENCE_AGENT_SDK.md (orphaned from cleanup)
- SETUP.md (duplicate of README.md quick start)
- WINDOWS_QUICK_REFERENCE.md (duplicate of docs/WINDOWS_DEPLOYMENT.md)
- SUB_AGENTS.md (design doc for unimplemented feature)
- JARVIS_VOICE_INTEGRATION_PLAN.md (1300-line spec, code not implemented)
- OBSIDIAN_MCP_SETUP_INSTRUCTIONS.md (temporary troubleshooting doc)
- LOGGING.md (redundant with well-commented logging_config.py)
- docs/SECURITY_AUDIT_SUMMARY.md (completed audit from 2026-02-12)

## Critical Bug Fixes (2 bugs)

1. bot_runner.py line 122: Fixed wrong dict key reference
   - Changed send_to_platform → send_to
   - Bug caused scheduled task platform info to never print

2. usage_tracker.py: Added missing pricing for claude-sonnet-4-6
   - Model was default but had no pricing entry
   - Caused cost under-reporting in Direct API mode

## Code Removed (14 files modified, ~1200 lines deleted)

### Dead imports removed (9 imports):
- bot_runner.py: sys
- agent.py: time
- adapters/runtime.py: re
- adapters/skill_integration.py: subprocess
- tools.py: redundant Path import
- mcp_servers/loki/loki_server.py: json
- google_tools/oauth_manager.py: Thread, Dict
- google_tools/gmail_client.py: os
- google_tools/utils.py: email

### Unused functions/methods removed (9 functions):
- agent.py: MEMORY_RESPONSE_PREVIEW_LENGTH constant
- scheduled_tasks.py: integrate_scheduler_with_runtime()
- adapters/runtime.py: command_preprocessor(), markdown_postprocessor()
- adapters/skill_integration.py: invoke_skill_via_cli(), __main__ block
- tools.py: _extract_mcp_result()
- google_tools/oauth_manager.py: needs_refresh_soon(), revoke_authorization()
- google_tools/people_client.py: update_contact(), delete_contact()

### Code quality improvements:
- memory_system.py: Removed empty else: pass branch
- calendar_client.py: Fixed bare except: → except Exception:
- mcp_ssh.py: Updated asyncio.get_event_loop() → get_running_loop()
- calendar_client.py: Fixed deprecated datetime.utcnow() → now(timezone.utc)

## Impact

- ~1200 lines of dead code removed
- 16 obsolete files deleted
- 2 critical bugs fixed
- 3 deprecated APIs updated
- Zero functionality broken (all changes verified)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-24 12:46:56 -07:00

296 lines
9.0 KiB
Python

"""Gmail API client for sending and reading emails."""
import base64
from pathlib import Path
from typing import Dict, List, Optional
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from .oauth_manager import GoogleOAuthManager
from .utils import (
create_mime_message,
format_email_summary,
get_email_body,
parse_email_message,
)
class GmailClient:
"""Client for Gmail API operations."""
def __init__(self, oauth_manager: GoogleOAuthManager):
"""Initialize Gmail client.
Args:
oauth_manager: Initialized OAuth manager with valid credentials
"""
self.oauth_manager = oauth_manager
self.service = None
self._initialize_service()
def _initialize_service(self) -> bool:
"""Initialize Gmail API service.
Returns:
True if initialized successfully, False otherwise
"""
credentials = self.oauth_manager.get_credentials()
if not credentials:
return False
try:
self.service = build("gmail", "v1", credentials=credentials)
return True
except Exception as e:
print(f"[Gmail] Failed to initialize service: {e}")
return False
def send_email(
self,
to: str,
subject: str,
body: str,
cc: Optional[List[str]] = None,
reply_to_message_id: Optional[str] = None,
) -> Dict:
"""Send an email.
Args:
to: Recipient email address
subject: Email subject
body: Email body (plain text or HTML)
cc: Optional list of CC recipients
reply_to_message_id: Optional message ID to reply to (for threading)
Returns:
Dict with success status and message_id or error
"""
if not self.service:
if not self._initialize_service():
return {
"success": False,
"error": "Not authorized. Run: python bot_runner.py --setup-google",
}
try:
message = create_mime_message(
to=to,
subject=subject,
body=body,
cc=cc,
reply_to_message_id=reply_to_message_id,
)
result = (
self.service.users()
.messages()
.send(userId="me", body=message)
.execute()
)
return {
"success": True,
"message_id": result.get("id"),
"thread_id": result.get("threadId"),
}
except HttpError as e:
return {"success": False, "error": str(e)}
def search_emails(
self,
query: str = "",
max_results: int = 10,
include_body: bool = False,
) -> Dict:
"""Search emails using Gmail search syntax.
Args:
query: Gmail search query (e.g., "from:john@example.com after:2026/02/10")
max_results: Maximum number of results to return (max: 50)
include_body: Whether to include full email body
Returns:
Dict with success status and emails or error
"""
if not self.service:
if not self._initialize_service():
return {
"success": False,
"error": "Not authorized. Run: python bot_runner.py --setup-google",
}
try:
# Limit max_results to 50 to avoid token overload
max_results = min(max_results, 50)
# Search for messages
results = (
self.service.users()
.messages()
.list(userId="me", q=query, maxResults=max_results)
.execute()
)
messages = results.get("messages", [])
if not messages:
return {
"success": True,
"emails": [],
"count": 0,
"summary": "No emails found.",
}
# Fetch full message details
emails = []
for msg in messages:
message = (
self.service.users()
.messages()
.get(userId="me", id=msg["id"], format="full")
.execute()
)
email_data = parse_email_message(message)
if include_body:
email_data["body"] = get_email_body(message)
emails.append(email_data)
summary = format_email_summary(emails, include_body=include_body)
return {
"success": True,
"emails": emails,
"count": len(emails),
"summary": summary,
}
except HttpError as e:
return {"success": False, "error": str(e)}
def get_email(self, message_id: str, format_type: str = "text") -> Dict:
"""Get full email by ID.
Args:
message_id: Gmail message ID
format_type: "text" or "html" (default: text)
Returns:
Dict with success status and email data or error
"""
if not self.service:
if not self._initialize_service():
return {
"success": False,
"error": "Not authorized. Run: python bot_runner.py --setup-google",
}
try:
message = (
self.service.users()
.messages()
.get(userId="me", id=message_id, format="full")
.execute()
)
email_data = parse_email_message(message)
email_data["body"] = get_email_body(message)
# Get attachment info if any
payload = message.get("payload", {})
attachments = []
def extract_attachments(parts):
"""Recursively extract attachments from message parts."""
for part in parts:
filename = part.get("filename")
if filename:
body = part.get("body", {})
attachment_id = body.get("attachmentId")
if attachment_id:
attachments.append({
"filename": filename,
"attachment_id": attachment_id,
"mime_type": part.get("mimeType"),
"size": body.get("size", 0),
})
# Recursively check nested parts
if "parts" in part:
extract_attachments(part["parts"])
if "parts" in payload:
extract_attachments(payload["parts"])
email_data["attachments"] = attachments
return {
"success": True,
"email": email_data,
}
except HttpError as e:
return {"success": False, "error": str(e)}
def download_attachment(
self,
message_id: str,
attachment_id: str,
filename: str,
output_dir: str = "downloads",
) -> Dict:
"""Download an email attachment.
Args:
message_id: Gmail message ID
attachment_id: Attachment ID from the message
filename: Original filename of the attachment
output_dir: Directory to save the attachment (default: "downloads")
Returns:
Dict with success status and file path or error
"""
if not self.service:
if not self._initialize_service():
return {
"success": False,
"error": "Not authorized. Run: python bot_runner.py --setup-google",
}
try:
# Get the attachment data
attachment = (
self.service.users()
.messages()
.attachments()
.get(userId="me", messageId=message_id, id=attachment_id)
.execute()
)
# Decode the attachment data
file_data = base64.urlsafe_b64decode(attachment["data"])
# Create output directory if it doesn't exist
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
# Save the file
file_path = output_path / filename
with open(file_path, "wb") as f:
f.write(file_data)
return {
"success": True,
"file_path": str(file_path),
"filename": filename,
"size": len(file_data),
}
except HttpError as e:
return {"success": False, "error": str(e)}
except Exception as e:
return {"success": False, "error": f"Failed to save attachment: {str(e)}"}