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>
456 lines
15 KiB
Python
456 lines
15 KiB
Python
"""
|
|
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
|