""" Email Template Delivery Service — the missing integration layer. Combines: template loading → published version → variable rendering → SMTP → audit log. Used by DSR workflow, document reviews, and other modules that need to send templated emails. """ import logging import uuid from typing import Any, Optional from sqlalchemy.orm import Session from compliance.db.email_template_models import ( EmailSendLogDB, EmailTemplateDB, EmailTemplateVersionDB, ) logger = logging.getLogger(__name__) def _render(html: str, variables: dict[str, str]) -> str: """Replace {{variable}} placeholders with values.""" result = html for key, value in variables.items(): result = result.replace(f"{{{{{key}}}}}", str(value)) return result class EmailDeliveryService: """Load template → render → send via SMTP → log.""" def __init__(self, db: Session) -> None: self.db = db def get_published_version( self, tenant_id: str, template_type: str, ) -> Optional[EmailTemplateVersionDB]: """Get the latest published version of a template by type.""" tid = uuid.UUID(tenant_id) template = ( self.db.query(EmailTemplateDB) .filter(EmailTemplateDB.tenant_id == tid, EmailTemplateDB.template_type == template_type) .first() ) if not template: return None return ( self.db.query(EmailTemplateVersionDB) .filter( EmailTemplateVersionDB.template_id == template.id, EmailTemplateVersionDB.status == "published", ) .order_by(EmailTemplateVersionDB.created_at.desc()) .first() ) def send( self, tenant_id: str, template_type: str, recipient: str, variables: dict[str, str], fallback_subject: Optional[str] = None, fallback_html: Optional[str] = None, ) -> dict[str, Any]: """Send a templated email. Falls back to inline HTML if no published template. Args: tenant_id: Tenant UUID string. template_type: E.g. 'dsr_receipt', 'dsr_completion'. recipient: Email address. variables: Dict of {{key}}: value for rendering. fallback_subject: Subject if no template found. fallback_html: HTML body if no template found. """ from compliance.services.smtp_sender import send_email tid = uuid.UUID(tenant_id) version = self.get_published_version(tenant_id, template_type) if version: subject = _render(version.subject, variables) body_html = _render(version.body_html, variables) version_id = version.id elif fallback_subject and fallback_html: subject = _render(fallback_subject, variables) body_html = _render(fallback_html, variables) version_id = None else: logger.warning("No published template for '%s' and no fallback provided", template_type) return {"success": False, "error": f"No template for {template_type}"} result = send_email(recipient=recipient, subject=subject, body_html=body_html) # Audit log try: log = EmailSendLogDB( tenant_id=tid, template_type=template_type, version_id=version_id, recipient=recipient, subject=subject, status=result.get("status", "unknown"), variables=variables, error_message=result.get("error"), ) self.db.add(log) self.db.commit() except Exception as e: logger.warning("Failed to log email send: %s", e) return { "success": result.get("status") == "sent", "template_type": template_type, "recipient": recipient, "subject": subject, "used_template": version is not None, "status": result.get("status"), }