Files
breakpilot-lehrer/klausur-service/backend/mail/api_accounts.py
Benjamin Admin b4613e26f3 [split-required] Split 500-850 LOC files (batch 2)
backend-lehrer (10 files):
- game/database.py (785 → 5), correction_api.py (683 → 4)
- classroom_engine/antizipation.py (676 → 5)
- llm_gateway schools/edu_search already done in prior batch

klausur-service (12 files):
- orientation_crop_api.py (694 → 5), pdf_export.py (677 → 4)
- zeugnis_crawler.py (676 → 5), grid_editor_api.py (671 → 5)
- eh_templates.py (658 → 5), mail/api.py (651 → 5)
- qdrant_service.py (638 → 5), training_api.py (625 → 4)

website (6 pages):
- middleware (696 → 8), mail (733 → 6), consent (628 → 8)
- compliance/risks (622 → 5), export (502 → 5), brandbook (629 → 7)

studio-v2 (3 components):
- B2BMigrationWizard (848 → 3), CleanupPanel (765 → 2)
- dashboard-experimental (739 → 2)

admin-lehrer (4 files):
- uebersetzungen (769 → 4), manager (670 → 2)
- ChunkBrowserQA (675 → 6), dsfa/page (674 → 5)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 08:24:01 +02:00

259 lines
7.4 KiB
Python

"""
Mail API — account management and sync endpoints.
"""
import logging
from typing import Optional, List
from fastapi import APIRouter, HTTPException, Query, BackgroundTasks
from pydantic import BaseModel
from .models import AccountTestResult
from .mail_db import (
create_email_account,
get_email_accounts,
get_email_account,
delete_email_account,
log_mail_audit,
)
from .credentials import get_credentials_service
from .aggregator import get_mail_aggregator
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/mail", tags=["Mail"])
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))
@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}