""" Unified Inbox Mail API FastAPI router for the mail system. """ import logging from typing import Optional, List from datetime import datetime from fastapi import APIRouter, HTTPException, Depends, Query, BackgroundTasks from pydantic import BaseModel from .models import ( EmailAccountCreate, EmailAccountUpdate, EmailAccount, AccountTestResult, AggregatedEmail, EmailSearchParams, TaskCreate, TaskUpdate, InboxTask, TaskDashboardStats, EmailComposeRequest, EmailSendResult, MailStats, MailHealthCheck, EmailAnalysisResult, ResponseSuggestion, TaskStatus, TaskPriority, EmailCategory, ) from .mail_db import ( init_mail_tables, create_email_account, get_email_accounts, get_email_account, delete_email_account, get_unified_inbox, get_email, mark_email_read, mark_email_starred, get_mail_stats, log_mail_audit, ) from .credentials import get_credentials_service from .aggregator import get_mail_aggregator from .ai_service import get_ai_email_service from .task_service import get_task_service logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1/mail", tags=["Mail"]) # ============================================================================= # Health & Init # ============================================================================= @router.get("/health", response_model=MailHealthCheck) async def health_check(): """Health check for the mail system.""" # TODO: Implement full health check return MailHealthCheck( status="healthy", database_connected=True, vault_connected=True, ) @router.post("/init") async def initialize_mail_system(): """Initialize mail database tables.""" success = await init_mail_tables() if not success: raise HTTPException(status_code=500, detail="Failed to initialize mail tables") return {"status": "initialized"} # ============================================================================= # Account Management # ============================================================================= class AccountCreateRequest(BaseModel): """Request to create an email account.""" email: str display_name: str account_type: str = "personal" imap_host: str imap_port: int = 993 imap_ssl: bool = True smtp_host: str smtp_port: int = 465 smtp_ssl: bool = True password: str @router.post("/accounts", response_model=dict) async def create_account( request: AccountCreateRequest, user_id: str = Query(..., description="User ID"), tenant_id: str = Query(..., description="Tenant ID"), ): """Create a new email account.""" credentials_service = get_credentials_service() # Store credentials securely vault_path = await credentials_service.store_credentials( account_id=f"{user_id}_{request.email}", email=request.email, password=request.password, imap_host=request.imap_host, imap_port=request.imap_port, smtp_host=request.smtp_host, smtp_port=request.smtp_port, ) # Create account in database account_id = await create_email_account( user_id=user_id, tenant_id=tenant_id, email=request.email, display_name=request.display_name, account_type=request.account_type, imap_host=request.imap_host, imap_port=request.imap_port, imap_ssl=request.imap_ssl, smtp_host=request.smtp_host, smtp_port=request.smtp_port, smtp_ssl=request.smtp_ssl, vault_path=vault_path, ) if not account_id: raise HTTPException(status_code=500, detail="Failed to create account") # Log audit await log_mail_audit( user_id=user_id, action="account_created", entity_type="account", entity_id=account_id, details={"email": request.email}, tenant_id=tenant_id, ) return {"id": account_id, "status": "created"} @router.get("/accounts", response_model=List[dict]) async def list_accounts( user_id: str = Query(..., description="User ID"), tenant_id: Optional[str] = Query(None, description="Tenant ID"), ): """List all email accounts for a user.""" accounts = await get_email_accounts(user_id, tenant_id) # Remove sensitive fields for account in accounts: account.pop("vault_path", None) return accounts @router.get("/accounts/{account_id}", response_model=dict) async def get_account( account_id: str, user_id: str = Query(..., description="User ID"), ): """Get a single email account.""" account = await get_email_account(account_id, user_id) if not account: raise HTTPException(status_code=404, detail="Account not found") account.pop("vault_path", None) return account @router.delete("/accounts/{account_id}") async def remove_account( account_id: str, user_id: str = Query(..., description="User ID"), ): """Delete an email account.""" account = await get_email_account(account_id, user_id) if not account: raise HTTPException(status_code=404, detail="Account not found") # Delete credentials credentials_service = get_credentials_service() vault_path = account.get("vault_path", "") if vault_path: await credentials_service.delete_credentials(account_id, vault_path) # Delete from database (cascades to emails) success = await delete_email_account(account_id, user_id) if not success: raise HTTPException(status_code=500, detail="Failed to delete account") await log_mail_audit( user_id=user_id, action="account_deleted", entity_type="account", entity_id=account_id, ) return {"status": "deleted"} @router.post("/accounts/{account_id}/test", response_model=AccountTestResult) async def test_account_connection( account_id: str, user_id: str = Query(..., description="User ID"), ): """Test connection for an email account.""" account = await get_email_account(account_id, user_id) if not account: raise HTTPException(status_code=404, detail="Account not found") # Get credentials credentials_service = get_credentials_service() vault_path = account.get("vault_path", "") creds = await credentials_service.get_credentials(account_id, vault_path) if not creds: return AccountTestResult( success=False, error_message="Credentials not found" ) # Test connection aggregator = get_mail_aggregator() result = await aggregator.test_account_connection( imap_host=account["imap_host"], imap_port=account["imap_port"], imap_ssl=account["imap_ssl"], smtp_host=account["smtp_host"], smtp_port=account["smtp_port"], smtp_ssl=account["smtp_ssl"], email_address=creds.email, password=creds.password, ) return result class ConnectionTestRequest(BaseModel): """Request to test connection before saving account.""" email: str imap_host: str imap_port: int = 993 imap_ssl: bool = True smtp_host: str smtp_port: int = 465 smtp_ssl: bool = True password: str @router.post("/accounts/test-connection", response_model=AccountTestResult) async def test_connection_before_save(request: ConnectionTestRequest): """ Test IMAP/SMTP connection before saving an account. This allows the wizard to verify credentials are correct before creating the account in the database. """ aggregator = get_mail_aggregator() result = await aggregator.test_account_connection( imap_host=request.imap_host, imap_port=request.imap_port, imap_ssl=request.imap_ssl, smtp_host=request.smtp_host, smtp_port=request.smtp_port, smtp_ssl=request.smtp_ssl, email_address=request.email, password=request.password, ) return result @router.post("/accounts/{account_id}/sync") async def sync_account( account_id: str, user_id: str = Query(..., description="User ID"), max_emails: int = Query(100, ge=1, le=500), background_tasks: BackgroundTasks = None, ): """Sync emails from an account.""" aggregator = get_mail_aggregator() try: new_count, total_count = await aggregator.sync_account( account_id=account_id, user_id=user_id, max_emails=max_emails, ) return { "status": "synced", "new_emails": new_count, "total_emails": total_count, } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # ============================================================================= # Unified Inbox # ============================================================================= @router.get("/inbox", response_model=List[dict]) async def get_inbox( user_id: str = Query(..., description="User ID"), account_ids: Optional[str] = Query(None, description="Comma-separated account IDs"), categories: Optional[str] = Query(None, description="Comma-separated categories"), is_read: Optional[bool] = Query(None), is_starred: Optional[bool] = Query(None), limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0), ): """Get unified inbox with all accounts aggregated.""" # Parse comma-separated values account_id_list = account_ids.split(",") if account_ids else None category_list = categories.split(",") if categories else None emails = await get_unified_inbox( user_id=user_id, account_ids=account_id_list, categories=category_list, is_read=is_read, is_starred=is_starred, limit=limit, offset=offset, ) return emails @router.get("/inbox/{email_id}", response_model=dict) async def get_email_detail( email_id: str, user_id: str = Query(..., description="User ID"), ): """Get a single email with full details.""" email_data = await get_email(email_id, user_id) if not email_data: raise HTTPException(status_code=404, detail="Email not found") # Mark as read await mark_email_read(email_id, user_id, is_read=True) return email_data @router.post("/inbox/{email_id}/read") async def mark_read( email_id: str, user_id: str = Query(..., description="User ID"), is_read: bool = Query(True), ): """Mark email as read/unread.""" success = await mark_email_read(email_id, user_id, is_read) if not success: raise HTTPException(status_code=500, detail="Failed to update email") return {"status": "updated", "is_read": is_read} @router.post("/inbox/{email_id}/star") async def mark_starred( email_id: str, user_id: str = Query(..., description="User ID"), is_starred: bool = Query(True), ): """Mark email as starred/unstarred.""" success = await mark_email_starred(email_id, user_id, is_starred) if not success: raise HTTPException(status_code=500, detail="Failed to update email") return {"status": "updated", "is_starred": is_starred} # ============================================================================= # Send Email # ============================================================================= @router.post("/send", response_model=EmailSendResult) async def send_email( request: EmailComposeRequest, user_id: str = Query(..., description="User ID"), ): """Send an email.""" aggregator = get_mail_aggregator() result = await aggregator.send_email( account_id=request.account_id, user_id=user_id, request=request, ) if result.success: await log_mail_audit( user_id=user_id, action="email_sent", entity_type="email", details={ "account_id": request.account_id, "to": request.to, "subject": request.subject, }, ) return result # ============================================================================= # AI Analysis # ============================================================================= @router.post("/analyze/{email_id}", response_model=EmailAnalysisResult) async def analyze_email( email_id: str, user_id: str = Query(..., description="User ID"), ): """Run AI analysis on an email.""" email_data = await get_email(email_id, user_id) if not email_data: raise HTTPException(status_code=404, detail="Email not found") ai_service = get_ai_email_service() result = await ai_service.analyze_email( email_id=email_id, sender_email=email_data.get("sender_email", ""), sender_name=email_data.get("sender_name"), subject=email_data.get("subject", ""), body_text=email_data.get("body_text"), body_preview=email_data.get("body_preview"), ) return result @router.get("/suggestions/{email_id}", response_model=List[ResponseSuggestion]) async def get_response_suggestions( email_id: str, user_id: str = Query(..., description="User ID"), ): """Get AI-generated response suggestions for an email.""" email_data = await get_email(email_id, user_id) if not email_data: raise HTTPException(status_code=404, detail="Email not found") ai_service = get_ai_email_service() # Use stored analysis if available from .models import SenderType, EmailCategory as EC sender_type = SenderType(email_data.get("sender_type", "unbekannt")) category = EC(email_data.get("category", "sonstiges")) suggestions = await ai_service.suggest_response( subject=email_data.get("subject", ""), body_text=email_data.get("body_text", ""), sender_type=sender_type, category=category, ) return suggestions # ============================================================================= # Tasks (Arbeitsvorrat) # ============================================================================= @router.get("/tasks", response_model=List[dict]) async def list_tasks( user_id: str = Query(..., description="User ID"), status: Optional[str] = Query(None, description="Filter by status"), priority: Optional[str] = Query(None, description="Filter by priority"), include_completed: bool = Query(False), limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0), ): """Get all tasks for a user.""" task_service = get_task_service() status_enum = TaskStatus(status) if status else None priority_enum = TaskPriority(priority) if priority else None tasks = await task_service.get_user_tasks( user_id=user_id, status=status_enum, priority=priority_enum, include_completed=include_completed, limit=limit, offset=offset, ) return tasks @router.post("/tasks", response_model=dict) async def create_task( request: TaskCreate, user_id: str = Query(..., description="User ID"), tenant_id: str = Query(..., description="Tenant ID"), ): """Create a new task manually.""" task_service = get_task_service() task_id = await task_service.create_manual_task( user_id=user_id, tenant_id=tenant_id, task_data=request, ) if not task_id: raise HTTPException(status_code=500, detail="Failed to create task") return {"id": task_id, "status": "created"} @router.get("/tasks/dashboard", response_model=TaskDashboardStats) async def get_task_dashboard( user_id: str = Query(..., description="User ID"), ): """Get dashboard statistics for tasks.""" task_service = get_task_service() return await task_service.get_dashboard_stats(user_id) @router.get("/tasks/{task_id}", response_model=dict) async def get_task( task_id: str, user_id: str = Query(..., description="User ID"), ): """Get a single task.""" task_service = get_task_service() task = await task_service.get_task(task_id, user_id) if not task: raise HTTPException(status_code=404, detail="Task not found") return task @router.put("/tasks/{task_id}") async def update_task( task_id: str, request: TaskUpdate, user_id: str = Query(..., description="User ID"), ): """Update a task.""" task_service = get_task_service() success = await task_service.update_task(task_id, user_id, request) if not success: raise HTTPException(status_code=500, detail="Failed to update task") return {"status": "updated"} @router.post("/tasks/{task_id}/complete") async def complete_task( task_id: str, user_id: str = Query(..., description="User ID"), ): """Mark a task as completed.""" task_service = get_task_service() success = await task_service.mark_completed(task_id, user_id) if not success: raise HTTPException(status_code=500, detail="Failed to complete task") return {"status": "completed"} @router.post("/tasks/from-email/{email_id}") async def create_task_from_email( email_id: str, user_id: str = Query(..., description="User ID"), tenant_id: str = Query(..., description="Tenant ID"), ): """Create a task from an email (after analysis).""" email_data = await get_email(email_id, user_id) if not email_data: raise HTTPException(status_code=404, detail="Email not found") # Get deadlines from stored analysis deadlines_raw = email_data.get("detected_deadlines", []) from .models import DeadlineExtraction, SenderType deadlines = [] for d in deadlines_raw: try: deadlines.append(DeadlineExtraction( deadline_date=datetime.fromisoformat(d["date"]), description=d.get("description", "Frist"), confidence=0.8, source_text="", is_firm=d.get("is_firm", True), )) except (KeyError, ValueError): continue sender_type = None if email_data.get("sender_type"): try: sender_type = SenderType(email_data["sender_type"]) except ValueError: pass task_service = get_task_service() task_id = await task_service.create_task_from_email( user_id=user_id, tenant_id=tenant_id, email_id=email_id, deadlines=deadlines, sender_type=sender_type, ) if not task_id: raise HTTPException(status_code=500, detail="Failed to create task") return {"id": task_id, "status": "created"} # ============================================================================= # Statistics # ============================================================================= @router.get("/stats", response_model=MailStats) async def get_statistics( user_id: str = Query(..., description="User ID"), ): """Get overall mail statistics for a user.""" stats = await get_mail_stats(user_id) return MailStats(**stats) # ============================================================================= # Sync All # ============================================================================= @router.post("/sync-all") async def sync_all_accounts( user_id: str = Query(..., description="User ID"), tenant_id: Optional[str] = Query(None), ): """Sync all email accounts for a user.""" aggregator = get_mail_aggregator() results = await aggregator.sync_all_accounts(user_id, tenant_id) return {"status": "synced", "results": results}