[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>
This commit is contained in:
131
klausur-service/backend/mail/aggregator_smtp.py
Normal file
131
klausur-service/backend/mail/aggregator_smtp.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
Mail Aggregator SMTP — email sending via SMTP.
|
||||
|
||||
Extracted from aggregator.py for modularity.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import smtplib
|
||||
from typing import Optional, List, Dict, Any
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
from .mail_db import get_email_account
|
||||
from .models import EmailComposeRequest, EmailSendResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SMTPConnectionError(Exception):
|
||||
"""Raised when SMTP connection fails."""
|
||||
pass
|
||||
|
||||
|
||||
class SMTPMixin:
|
||||
"""SMTP-related methods for MailAggregator.
|
||||
|
||||
Provides SMTP connection testing and email sending.
|
||||
Must be mixed into a class that has ``_credentials_service``.
|
||||
"""
|
||||
|
||||
async def test_smtp_connection(
|
||||
self,
|
||||
smtp_host: str,
|
||||
smtp_port: int,
|
||||
smtp_ssl: bool,
|
||||
email_address: str,
|
||||
password: str,
|
||||
) -> tuple:
|
||||
"""Test SMTP connection. Returns (success, error)."""
|
||||
try:
|
||||
if smtp_ssl:
|
||||
smtp = smtplib.SMTP_SSL(smtp_host, smtp_port)
|
||||
else:
|
||||
smtp = smtplib.SMTP(smtp_host, smtp_port)
|
||||
smtp.starttls()
|
||||
|
||||
smtp.login(email_address, password)
|
||||
smtp.quit()
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"SMTP test failed for {email_address}: {e}")
|
||||
return False, f"SMTP Error: {str(e)}"
|
||||
|
||||
async def send_email(
|
||||
self,
|
||||
account_id: str,
|
||||
user_id: str,
|
||||
request: EmailComposeRequest,
|
||||
) -> EmailSendResult:
|
||||
"""
|
||||
Send an email via SMTP.
|
||||
|
||||
Args:
|
||||
account_id: The account to send from
|
||||
user_id: The user ID
|
||||
request: The compose request with recipients and content
|
||||
|
||||
Returns:
|
||||
EmailSendResult with success status
|
||||
"""
|
||||
account = await get_email_account(account_id, user_id)
|
||||
if not account:
|
||||
return EmailSendResult(success=False, error="Account not found")
|
||||
|
||||
# Verify the account_id matches
|
||||
if request.account_id != account_id:
|
||||
return EmailSendResult(success=False, error="Account mismatch")
|
||||
|
||||
# Get credentials
|
||||
vault_path = account.get("vault_path", "")
|
||||
creds = await self._credentials_service.get_credentials(account_id, vault_path)
|
||||
if not creds:
|
||||
return EmailSendResult(success=False, error="Credentials not found")
|
||||
|
||||
try:
|
||||
# Create message
|
||||
if request.is_html:
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg.attach(MIMEText(request.body, "html"))
|
||||
else:
|
||||
msg = MIMEText(request.body, "plain")
|
||||
|
||||
msg["Subject"] = request.subject
|
||||
msg["From"] = account["email"]
|
||||
msg["To"] = ", ".join(request.to)
|
||||
|
||||
if request.cc:
|
||||
msg["Cc"] = ", ".join(request.cc)
|
||||
|
||||
if request.reply_to_message_id:
|
||||
msg["In-Reply-To"] = request.reply_to_message_id
|
||||
msg["References"] = request.reply_to_message_id
|
||||
|
||||
# Send via SMTP
|
||||
if account["smtp_ssl"]:
|
||||
smtp = smtplib.SMTP_SSL(account["smtp_host"], account["smtp_port"])
|
||||
else:
|
||||
smtp = smtplib.SMTP(account["smtp_host"], account["smtp_port"])
|
||||
smtp.starttls()
|
||||
|
||||
smtp.login(creds.email, creds.password)
|
||||
|
||||
# All recipients
|
||||
all_recipients = list(request.to)
|
||||
if request.cc:
|
||||
all_recipients.extend(request.cc)
|
||||
if request.bcc:
|
||||
all_recipients.extend(request.bcc)
|
||||
|
||||
smtp.sendmail(account["email"], all_recipients, msg.as_string())
|
||||
smtp.quit()
|
||||
|
||||
return EmailSendResult(
|
||||
success=True,
|
||||
message_id=msg.get("Message-ID"),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send email: {e}")
|
||||
return EmailSendResult(success=False, error=str(e))
|
||||
Reference in New Issue
Block a user