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>
296 lines
9.0 KiB
Python
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)}"}
|