Files
breakpilot-lehrer/klausur-service/backend/mail/aggregator.py
Benjamin Admin bd4b956e3c [split-required] Split final 43 files (500-668 LOC) to complete refactoring
klausur-service (11 files):
- cv_gutter_repair, ocr_pipeline_regression, upload_api
- ocr_pipeline_sessions, smart_spell, nru_worksheet_generator
- ocr_pipeline_overlays, mail/aggregator, zeugnis_api
- cv_syllable_detect, self_rag

backend-lehrer (17 files):
- classroom_engine/suggestions, generators/quiz_generator
- worksheets_api, llm_gateway/comparison, state_engine_api
- classroom/models (→ 4 submodules), services/file_processor
- alerts_agent/api/wizard+digests+routes, content_generators/pdf
- classroom/routes/sessions, llm_gateway/inference
- classroom_engine/analytics, auth/keycloak_auth
- alerts_agent/processing/rule_engine, ai_processor/print_versions

agent-core (5 files):
- brain/memory_store, brain/knowledge_graph, brain/context_manager
- orchestrator/supervisor, sessions/session_manager

admin-lehrer (5 components):
- GridOverlay, StepGridReview, DevOpsPipelineSidebar
- DataFlowDiagram, sbom/wizard/page

website (2 files):
- DependencyMap, lehrer/abitur-archiv

Other: nibis_ingestion, grid_detection_service, export-doclayout-onnx

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

156 lines
4.4 KiB
Python

"""
Mail Aggregator Service — barrel re-export.
All implementation split into:
aggregator_imap — IMAP connection, sync, email parsing
aggregator_smtp — SMTP connection, email sending
Multi-account IMAP aggregation with async support.
"""
import asyncio
import logging
from typing import Optional, List, Dict, Any
from .credentials import get_credentials_service
from .mail_db import get_email_accounts, get_unified_inbox
from .models import AccountTestResult
from .aggregator_imap import IMAPMixin, IMAPConnectionError
from .aggregator_smtp import SMTPMixin, SMTPConnectionError
logger = logging.getLogger(__name__)
class MailAggregator(IMAPMixin, SMTPMixin):
"""
Aggregates emails from multiple IMAP accounts into a unified inbox.
Features:
- Connect to multiple IMAP accounts
- Fetch and cache emails in PostgreSQL
- Send emails via SMTP
- Handle connection pooling
"""
def __init__(self):
self._credentials_service = get_credentials_service()
self._imap_connections: Dict[str, Any] = {}
self._sync_lock = asyncio.Lock()
async def test_account_connection(
self,
imap_host: str,
imap_port: int,
imap_ssl: bool,
smtp_host: str,
smtp_port: int,
smtp_ssl: bool,
email_address: str,
password: str,
) -> AccountTestResult:
"""
Test IMAP and SMTP connection with provided credentials.
Returns:
AccountTestResult with connection status
"""
result = AccountTestResult(
success=False,
imap_connected=False,
smtp_connected=False,
)
# Test IMAP
imap_ok, imap_err, folders = await self.test_imap_connection(
imap_host, imap_port, imap_ssl, email_address, password
)
result.imap_connected = imap_ok
if folders:
result.folders_found = folders
if imap_err:
result.error_message = imap_err
# Test SMTP
smtp_ok, smtp_err = await self.test_smtp_connection(
smtp_host, smtp_port, smtp_ssl, email_address, password
)
result.smtp_connected = smtp_ok
if smtp_err:
if result.error_message:
result.error_message += f"; {smtp_err}"
else:
result.error_message = smtp_err
result.success = result.imap_connected and result.smtp_connected
return result
async def sync_all_accounts(self, user_id: str, tenant_id: Optional[str] = None) -> Dict[str, Any]:
"""
Sync all accounts for a user.
Returns:
Dict with sync results per account
"""
async with self._sync_lock:
accounts = await get_email_accounts(user_id, tenant_id)
results = {}
for account in accounts:
account_id = account["id"]
try:
new_count, total_count = await self.sync_account(
account_id, user_id, max_emails=50
)
results[account_id] = {
"status": "success",
"new_emails": new_count,
"total_emails": total_count,
}
except Exception as e:
results[account_id] = {
"status": "error",
"error": str(e),
}
return results
async def get_unified_inbox_emails(
self,
user_id: str,
account_ids: Optional[List[str]] = None,
categories: Optional[List[str]] = None,
is_read: Optional[bool] = None,
is_starred: Optional[bool] = None,
limit: int = 50,
offset: int = 0,
) -> List[Dict]:
"""
Get unified inbox with all filters.
Returns:
List of email dictionaries
"""
return await get_unified_inbox(
user_id=user_id,
account_ids=account_ids,
categories=categories,
is_read=is_read,
is_starred=is_starred,
limit=limit,
offset=offset,
)
# Global instance
_aggregator: Optional[MailAggregator] = None
def get_mail_aggregator() -> MailAggregator:
"""Get or create the global MailAggregator instance."""
global _aggregator
if _aggregator is None:
_aggregator = MailAggregator()
return _aggregator