Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
455
klausur-service/backend/mail/models.py
Normal file
455
klausur-service/backend/mail/models.py
Normal file
@@ -0,0 +1,455 @@
|
||||
"""
|
||||
Unified Inbox Mail Models
|
||||
|
||||
Pydantic models for API requests/responses and internal data structures.
|
||||
Database schema is defined in mail_db.py.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional, List, Dict, Any
|
||||
from pydantic import BaseModel, Field, EmailStr
|
||||
import uuid
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Enums
|
||||
# =============================================================================
|
||||
|
||||
class AccountStatus(str, Enum):
|
||||
"""Status of an email account connection."""
|
||||
ACTIVE = "active"
|
||||
INACTIVE = "inactive"
|
||||
ERROR = "error"
|
||||
PENDING = "pending"
|
||||
|
||||
|
||||
class TaskStatus(str, Enum):
|
||||
"""Status of an inbox task."""
|
||||
PENDING = "pending"
|
||||
IN_PROGRESS = "in_progress"
|
||||
COMPLETED = "completed"
|
||||
ARCHIVED = "archived"
|
||||
|
||||
|
||||
class TaskPriority(str, Enum):
|
||||
"""Priority level for tasks."""
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
URGENT = "urgent"
|
||||
|
||||
|
||||
class EmailCategory(str, Enum):
|
||||
"""AI-detected email categories."""
|
||||
DIENSTLICH = "dienstlich" # Official government/authority
|
||||
PERSONAL = "personal" # Staff/HR matters
|
||||
FINANZEN = "finanzen" # Finance/budget
|
||||
ELTERN = "eltern" # Parent communication
|
||||
SCHUELER = "schueler" # Student matters
|
||||
KOLLEGIUM = "kollegium" # Teacher colleagues
|
||||
FORTBILDUNG = "fortbildung" # Professional development
|
||||
VERANSTALTUNG = "veranstaltung" # Events
|
||||
SICHERHEIT = "sicherheit" # Safety/security
|
||||
TECHNIK = "technik" # IT/technical
|
||||
NEWSLETTER = "newsletter" # Newsletters
|
||||
WERBUNG = "werbung" # Advertising/spam
|
||||
SONSTIGES = "sonstiges" # Other
|
||||
|
||||
|
||||
class SenderType(str, Enum):
|
||||
"""Type of sender for classification."""
|
||||
KULTUSMINISTERIUM = "kultusministerium"
|
||||
LANDESSCHULBEHOERDE = "landesschulbehoerde"
|
||||
RLSB = "rlsb" # Regionales Landesamt für Schule und Bildung
|
||||
SCHULAMT = "schulamt"
|
||||
NIBIS = "nibis"
|
||||
SCHULTRAEGER = "schultraeger"
|
||||
ELTERNVERTRETER = "elternvertreter"
|
||||
GEWERKSCHAFT = "gewerkschaft"
|
||||
FORTBILDUNGSINSTITUT = "fortbildungsinstitut"
|
||||
PRIVATPERSON = "privatperson"
|
||||
UNTERNEHMEN = "unternehmen"
|
||||
UNBEKANNT = "unbekannt"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Known Authority Domains (Niedersachsen)
|
||||
# =============================================================================
|
||||
|
||||
KNOWN_AUTHORITIES_NI = {
|
||||
"@mk.niedersachsen.de": {"type": SenderType.KULTUSMINISTERIUM, "name": "Kultusministerium Niedersachsen"},
|
||||
"@rlsb.de": {"type": SenderType.RLSB, "name": "Regionales Landesamt für Schule und Bildung"},
|
||||
"@rlsb-bs.niedersachsen.de": {"type": SenderType.RLSB, "name": "RLSB Braunschweig"},
|
||||
"@rlsb-h.niedersachsen.de": {"type": SenderType.RLSB, "name": "RLSB Hannover"},
|
||||
"@rlsb-lg.niedersachsen.de": {"type": SenderType.RLSB, "name": "RLSB Lüneburg"},
|
||||
"@rlsb-os.niedersachsen.de": {"type": SenderType.RLSB, "name": "RLSB Osnabrück"},
|
||||
"@landesschulbehoerde-nds.de": {"type": SenderType.LANDESSCHULBEHOERDE, "name": "Landesschulbehörde"},
|
||||
"@nibis.de": {"type": SenderType.NIBIS, "name": "NiBiS"},
|
||||
"@schule.niedersachsen.de": {"type": SenderType.LANDESSCHULBEHOERDE, "name": "Schulnetzwerk NI"},
|
||||
"@nlq.nibis.de": {"type": SenderType.FORTBILDUNGSINSTITUT, "name": "NLQ"},
|
||||
"@gew-nds.de": {"type": SenderType.GEWERKSCHAFT, "name": "GEW Niedersachsen"},
|
||||
"@vbe-nds.de": {"type": SenderType.GEWERKSCHAFT, "name": "VBE Niedersachsen"},
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Email Account Models
|
||||
# =============================================================================
|
||||
|
||||
class EmailAccountBase(BaseModel):
|
||||
"""Base model for email account."""
|
||||
email: EmailStr = Field(..., description="Email address")
|
||||
display_name: str = Field(..., description="Display name for the account")
|
||||
account_type: str = Field("personal", description="Type: personal, schulleitung, personal, verwaltung")
|
||||
imap_host: str = Field(..., description="IMAP server hostname")
|
||||
imap_port: int = Field(993, description="IMAP port (default: 993 for SSL)")
|
||||
imap_ssl: bool = Field(True, description="Use SSL for IMAP")
|
||||
smtp_host: str = Field(..., description="SMTP server hostname")
|
||||
smtp_port: int = Field(465, description="SMTP port (default: 465 for SSL)")
|
||||
smtp_ssl: bool = Field(True, description="Use SSL for SMTP")
|
||||
|
||||
|
||||
class EmailAccountCreate(EmailAccountBase):
|
||||
"""Model for creating a new email account."""
|
||||
password: str = Field(..., description="Password (will be stored in Vault)")
|
||||
|
||||
|
||||
class EmailAccountUpdate(BaseModel):
|
||||
"""Model for updating an email account."""
|
||||
display_name: Optional[str] = None
|
||||
account_type: Optional[str] = None
|
||||
imap_host: Optional[str] = None
|
||||
imap_port: Optional[int] = None
|
||||
smtp_host: Optional[str] = None
|
||||
smtp_port: Optional[int] = None
|
||||
password: Optional[str] = None # Only if changing
|
||||
|
||||
|
||||
class EmailAccount(EmailAccountBase):
|
||||
"""Full email account model (without password)."""
|
||||
id: str
|
||||
user_id: str
|
||||
tenant_id: str
|
||||
status: AccountStatus = AccountStatus.PENDING
|
||||
last_sync: Optional[datetime] = None
|
||||
sync_error: Optional[str] = None
|
||||
email_count: int = 0
|
||||
unread_count: int = 0
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class AccountTestResult(BaseModel):
|
||||
"""Result of testing email account connection."""
|
||||
success: bool
|
||||
imap_connected: bool = False
|
||||
smtp_connected: bool = False
|
||||
error_message: Optional[str] = None
|
||||
folders_found: List[str] = []
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Aggregated Email Models
|
||||
# =============================================================================
|
||||
|
||||
class AggregatedEmailBase(BaseModel):
|
||||
"""Base model for an aggregated email."""
|
||||
subject: str
|
||||
sender_email: str
|
||||
sender_name: Optional[str] = None
|
||||
recipients: List[str] = []
|
||||
cc: List[str] = []
|
||||
body_preview: Optional[str] = None
|
||||
has_attachments: bool = False
|
||||
|
||||
|
||||
class AggregatedEmail(AggregatedEmailBase):
|
||||
"""Full aggregated email model."""
|
||||
id: str
|
||||
account_id: str
|
||||
message_id: str # Original IMAP message ID
|
||||
folder: str = "INBOX"
|
||||
is_read: bool = False
|
||||
is_starred: bool = False
|
||||
is_deleted: bool = False
|
||||
body_text: Optional[str] = None
|
||||
body_html: Optional[str] = None
|
||||
attachments: List[Dict[str, Any]] = []
|
||||
headers: Dict[str, str] = {}
|
||||
date_sent: datetime
|
||||
date_received: datetime
|
||||
|
||||
# AI-enriched fields
|
||||
category: Optional[EmailCategory] = None
|
||||
sender_type: Optional[SenderType] = None
|
||||
sender_authority_name: Optional[str] = None
|
||||
detected_deadlines: List[Dict[str, Any]] = []
|
||||
suggested_priority: Optional[TaskPriority] = None
|
||||
ai_summary: Optional[str] = None
|
||||
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class EmailSearchParams(BaseModel):
|
||||
"""Parameters for searching emails."""
|
||||
query: Optional[str] = None
|
||||
account_ids: Optional[List[str]] = None
|
||||
categories: Optional[List[EmailCategory]] = None
|
||||
is_read: Optional[bool] = None
|
||||
is_starred: Optional[bool] = None
|
||||
has_deadline: Optional[bool] = None
|
||||
date_from: Optional[datetime] = None
|
||||
date_to: Optional[datetime] = None
|
||||
limit: int = Field(50, ge=1, le=200)
|
||||
offset: int = Field(0, ge=0)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Inbox Task Models (Arbeitsvorrat)
|
||||
# =============================================================================
|
||||
|
||||
class TaskBase(BaseModel):
|
||||
"""Base model for inbox task."""
|
||||
title: str = Field(..., description="Task title")
|
||||
description: Optional[str] = None
|
||||
priority: TaskPriority = TaskPriority.MEDIUM
|
||||
deadline: Optional[datetime] = None
|
||||
|
||||
|
||||
class TaskCreate(TaskBase):
|
||||
"""Model for creating a task manually."""
|
||||
email_id: Optional[str] = None # Link to source email
|
||||
|
||||
|
||||
class TaskUpdate(BaseModel):
|
||||
"""Model for updating a task."""
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
priority: Optional[TaskPriority] = None
|
||||
status: Optional[TaskStatus] = None
|
||||
deadline: Optional[datetime] = None
|
||||
completed_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class InboxTask(TaskBase):
|
||||
"""Full inbox task model."""
|
||||
id: str
|
||||
user_id: str
|
||||
tenant_id: str
|
||||
email_id: Optional[str] = None
|
||||
account_id: Optional[str] = None
|
||||
status: TaskStatus = TaskStatus.PENDING
|
||||
|
||||
# Source information
|
||||
source_email_subject: Optional[str] = None
|
||||
source_sender: Optional[str] = None
|
||||
source_sender_type: Optional[SenderType] = None
|
||||
|
||||
# AI-extracted information
|
||||
ai_extracted: bool = False
|
||||
confidence_score: Optional[float] = None
|
||||
|
||||
completed_at: Optional[datetime] = None
|
||||
reminder_at: Optional[datetime] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TaskDashboardStats(BaseModel):
|
||||
"""Dashboard statistics for tasks."""
|
||||
total_tasks: int = 0
|
||||
pending_tasks: int = 0
|
||||
in_progress_tasks: int = 0
|
||||
completed_tasks: int = 0
|
||||
overdue_tasks: int = 0
|
||||
due_today: int = 0
|
||||
due_this_week: int = 0
|
||||
by_priority: Dict[str, int] = {}
|
||||
by_sender_type: Dict[str, int] = {}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AI Analysis Models
|
||||
# =============================================================================
|
||||
|
||||
class SenderClassification(BaseModel):
|
||||
"""Result of AI sender classification."""
|
||||
sender_type: SenderType
|
||||
authority_name: Optional[str] = None
|
||||
confidence: float = Field(..., ge=0, le=1)
|
||||
domain_matched: bool = False
|
||||
ai_classified: bool = False
|
||||
|
||||
|
||||
class DeadlineExtraction(BaseModel):
|
||||
"""Extracted deadline from email."""
|
||||
deadline_date: datetime
|
||||
description: str
|
||||
confidence: float = Field(..., ge=0, le=1)
|
||||
source_text: str # Original text containing the deadline
|
||||
is_firm: bool = True # True for "bis zum", False for "etwa"
|
||||
|
||||
|
||||
class EmailAnalysisResult(BaseModel):
|
||||
"""Complete AI analysis result for an email."""
|
||||
email_id: str
|
||||
category: EmailCategory
|
||||
category_confidence: float
|
||||
sender_classification: SenderClassification
|
||||
deadlines: List[DeadlineExtraction] = []
|
||||
suggested_priority: TaskPriority
|
||||
summary: Optional[str] = None
|
||||
suggested_actions: List[str] = []
|
||||
auto_create_task: bool = False
|
||||
|
||||
|
||||
class ResponseSuggestion(BaseModel):
|
||||
"""AI-generated response suggestion."""
|
||||
template_type: str # "acknowledgment", "request_info", "delegation", etc.
|
||||
subject: str
|
||||
body: str
|
||||
confidence: float
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Email Template Models
|
||||
# =============================================================================
|
||||
|
||||
class EmailTemplateBase(BaseModel):
|
||||
"""Base model for email template."""
|
||||
name: str
|
||||
category: str # "acknowledgment", "request", "forwarding", etc.
|
||||
subject_template: str
|
||||
body_template: str
|
||||
variables: List[str] = [] # e.g., ["sender_name", "deadline", "topic"]
|
||||
|
||||
|
||||
class EmailTemplateCreate(EmailTemplateBase):
|
||||
"""Model for creating a template."""
|
||||
pass
|
||||
|
||||
|
||||
class EmailTemplate(EmailTemplateBase):
|
||||
"""Full email template model."""
|
||||
id: str
|
||||
user_id: Optional[str] = None # None = system template
|
||||
tenant_id: Optional[str] = None
|
||||
is_system: bool = False
|
||||
usage_count: int = 0
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Compose Email Models
|
||||
# =============================================================================
|
||||
|
||||
class EmailComposeRequest(BaseModel):
|
||||
"""Request to compose/send an email."""
|
||||
account_id: str = Field(..., description="Account to send from")
|
||||
to: List[EmailStr]
|
||||
cc: Optional[List[EmailStr]] = []
|
||||
bcc: Optional[List[EmailStr]] = []
|
||||
subject: str
|
||||
body: str
|
||||
is_html: bool = False
|
||||
reply_to_message_id: Optional[str] = None
|
||||
attachments: Optional[List[Dict[str, Any]]] = None
|
||||
|
||||
|
||||
class EmailSendResult(BaseModel):
|
||||
"""Result of sending an email."""
|
||||
success: bool
|
||||
message_id: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Statistics & Health Models
|
||||
# =============================================================================
|
||||
|
||||
class MailStats(BaseModel):
|
||||
"""Overall mail system statistics."""
|
||||
total_accounts: int = 0
|
||||
active_accounts: int = 0
|
||||
error_accounts: int = 0
|
||||
total_emails: int = 0
|
||||
unread_emails: int = 0
|
||||
total_tasks: int = 0
|
||||
pending_tasks: int = 0
|
||||
overdue_tasks: int = 0
|
||||
emails_today: int = 0
|
||||
ai_analyses_today: int = 0
|
||||
per_account: List[Dict[str, Any]] = []
|
||||
|
||||
|
||||
class MailHealthCheck(BaseModel):
|
||||
"""Health check for mail system."""
|
||||
status: str # "healthy", "degraded", "unhealthy"
|
||||
database_connected: bool = False
|
||||
vault_connected: bool = False
|
||||
accounts_checked: int = 0
|
||||
accounts_healthy: int = 0
|
||||
last_sync: Optional[datetime] = None
|
||||
errors: List[str] = []
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Helper Functions
|
||||
# =============================================================================
|
||||
|
||||
def generate_id() -> str:
|
||||
"""Generate a new UUID."""
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def classify_sender_by_domain(email: str) -> Optional[SenderClassification]:
|
||||
"""
|
||||
Classify sender by known authority domains.
|
||||
Returns None if domain is not recognized.
|
||||
"""
|
||||
email_lower = email.lower()
|
||||
for domain, info in KNOWN_AUTHORITIES_NI.items():
|
||||
if domain in email_lower:
|
||||
return SenderClassification(
|
||||
sender_type=info["type"],
|
||||
authority_name=info["name"],
|
||||
confidence=0.95,
|
||||
domain_matched=True,
|
||||
ai_classified=False,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def get_priority_from_sender_type(sender_type: SenderType) -> TaskPriority:
|
||||
"""Get suggested priority based on sender type."""
|
||||
high_priority = {
|
||||
SenderType.KULTUSMINISTERIUM,
|
||||
SenderType.LANDESSCHULBEHOERDE,
|
||||
SenderType.RLSB,
|
||||
SenderType.SCHULAMT,
|
||||
}
|
||||
medium_priority = {
|
||||
SenderType.NIBIS,
|
||||
SenderType.SCHULTRAEGER,
|
||||
SenderType.ELTERNVERTRETER,
|
||||
}
|
||||
|
||||
if sender_type in high_priority:
|
||||
return TaskPriority.HIGH
|
||||
elif sender_type in medium_priority:
|
||||
return TaskPriority.MEDIUM
|
||||
return TaskPriority.LOW
|
||||
Reference in New Issue
Block a user