diff --git a/backend-compliance/compliance/services/dsr_art11_service.py b/backend-compliance/compliance/services/dsr_art11_service.py index 1791715..8215294 100644 --- a/backend-compliance/compliance/services/dsr_art11_service.py +++ b/backend-compliance/compliance/services/dsr_art11_service.py @@ -75,33 +75,26 @@ class DSRArt11Service: if not dsr.requester_email: return try: - from compliance.services.smtp_sender import send_email - send_email( + from compliance.services.email_delivery_service import EmailDeliveryService + delivery = EmailDeliveryService(self._db) + variables = { + "requester_name": dsr.requester_name or "Antragsteller/in", + "reference_number": dsr.request_number or "", + "rejection_reason": "Identifikation nicht moeglich — Art. 11 Abs. 1 DSGVO", + "legal_basis": "Art. 11 Abs. 1 DSGVO", + "sender_name": "Datenschutzbeauftragter", + } + # Use published dsr_rejection template, fallback to inline + delivery.send( + tenant_id=str(dsr.tenant_id), + template_type="dsr_rejection", recipient=dsr.requester_email, - subject=f"Zu Ihrer Anfrage {dsr.request_number} — Art. 11 DSGVO", - body_html=f""" -
-
-

Mitteilung zu Ihrer Anfrage

-
-
-

Sehr geehrte/r Antragsteller/in,

-

wir haben Ihre Anfrage ({dsr.request_number}) gemaess Art. 15 DSGVO - erhalten und geprueft.

-

Leider koennen wir die bei uns gespeicherten Daten (anonymisierte - Cookies, IP-Hashes) keiner identifizierbaren Person zuordnen.

-

Gemaess Art. 11 Abs. 1 DSGVO sind wir nicht verpflichtet, - zusaetzliche Informationen zu erheben, um Sie zu identifizieren. - Eine Auskunftserteilung ist daher nicht moeglich.

-

Sollten Sie ueber ein Kundenkonto bei uns verfuegen, koennen Sie - die Anfrage unter Angabe Ihrer Kundennummer erneut einreichen.

-

- Mit freundlichen Gruessen
- Datenschutzbeauftragter -

-
-
- """, + variables=variables, + fallback_subject=f"Zu Ihrer Anfrage {dsr.request_number} — Art. 11 DSGVO", + fallback_html=f"""

Sehr geehrte/r {dsr.requester_name or 'Antragsteller/in'},

+

wir koennen die bei uns gespeicherten Daten keiner identifizierbaren Person zuordnen. + Gemaess Art. 11 Abs. 1 DSGVO ist eine Auskunftserteilung nicht moeglich.

+

Mit freundlichen Gruessen
Datenschutzbeauftragter

""", ) except Exception as e: logger.warning("Art. 11 notification failed: %s", e) diff --git a/backend-compliance/compliance/services/email_delivery_service.py b/backend-compliance/compliance/services/email_delivery_service.py new file mode 100644 index 0000000..313028a --- /dev/null +++ b/backend-compliance/compliance/services/email_delivery_service.py @@ -0,0 +1,122 @@ +""" +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"), + } diff --git a/backend-compliance/migrations/117_dsr_email_templates_content.sql b/backend-compliance/migrations/117_dsr_email_templates_content.sql new file mode 100644 index 0000000..42f05d7 --- /dev/null +++ b/backend-compliance/migrations/117_dsr_email_templates_content.sql @@ -0,0 +1,164 @@ +-- Migration 117: Professional DSR email template content +-- Updates published versions with proper HTML for all 5 DSR template types + +-- Note: This updates existing template versions. If no versions exist yet, +-- the email_delivery_service falls back to inline HTML. + +-- dsr_receipt: Eingangsbestaetigung +UPDATE compliance_email_template_versions SET + subject = 'Eingangsbestaetigung Ihrer Anfrage {{reference_number}} ({{request_type}})', + body_html = '
+
+

Eingangsbestaetigung

+

Ihre Anfrage nach {{request_type}}

+
+
+

Sehr geehrte/r {{requester_name}},

+

wir bestaetigen den Eingang Ihrer Anfrage auf Datenauskunft gemaess DSGVO.

+ + + + +
Vorgangsnummer:{{reference_number}}
Art der Anfrage:{{request_type}}
Frist:{{deadline}}
+

Wir werden Ihre Anfrage schnellstmoeglich bearbeiten und Ihnen die angeforderten Informationen innerhalb der gesetzlichen Frist zukommen lassen.

+

Bei Rueckfragen wenden Sie sich bitte unter Angabe der Vorgangsnummer an unseren Datenschutzbeauftragten.

+

Mit freundlichen Gruessen
{{sender_name}}

+
', + body_text = 'Eingangsbestaetigung - {{reference_number}} + +Sehr geehrte/r {{requester_name}}, + +wir bestaetigen den Eingang Ihrer Anfrage ({{reference_number}}) auf {{request_type}}. + +Frist: {{deadline}} + +Mit freundlichen Gruessen +{{sender_name}}', + updated_at = NOW() +WHERE template_id IN ( + SELECT id FROM compliance_email_templates WHERE template_type = 'dsr_receipt' +) AND status = 'published'; + +-- dsr_completion: Abschluss / Datenauskunft +UPDATE compliance_email_template_versions SET + subject = 'Ihre Datenauskunft {{reference_number}} — abgeschlossen', + body_html = '
+
+

Datenauskunft

+

Vorgangsnummer {{reference_number}}

+
+
+

Sehr geehrte/r {{requester_name}},

+

Ihre Anfrage ({{reference_number}}) vom Typ {{request_type}} wurde am {{completion_date}} abgeschlossen.

+

Die angeforderten Daten stehen Ihnen in folgenden Formaten zur Verfuegung:

+ +

Sollten Sie Fragen haben oder weitere Rechte ausueben wollen (Berichtigung Art. 16, Loeschung Art. 17, Einschraenkung Art. 18), wenden Sie sich bitte erneut an uns.

+

Mit freundlichen Gruessen
{{sender_name}}

+
', + body_text = 'Datenauskunft - {{reference_number}} + +Sehr geehrte/r {{requester_name}}, + +Ihre Anfrage ({{reference_number}}) wurde am {{completion_date}} abgeschlossen. + +Die Daten stehen als PDF, JSON und CSV zur Verfuegung. + +Mit freundlichen Gruessen +{{sender_name}}', + updated_at = NOW() +WHERE template_id IN ( + SELECT id FROM compliance_email_templates WHERE template_type = 'dsr_completion' +) AND status = 'published'; + +-- dsr_rejection: Ablehnung +UPDATE compliance_email_template_versions SET + subject = 'Zu Ihrer Anfrage {{reference_number}} — Entscheidung', + body_html = '
+
+

Mitteilung zu Ihrer Anfrage

+
+
+

Sehr geehrte/r {{requester_name}},

+

wir haben Ihre Anfrage ({{reference_number}}) geprueft und muessen Ihnen leider mitteilen, dass wir dieser nicht nachkommen koennen.

+
+

Grund:

+

{{rejection_reason}}

+

Rechtsgrundlage: {{legal_basis}}

+
+

Sie haben das Recht, eine Beschwerde bei der zustaendigen Aufsichtsbehoerde einzureichen.

+

Mit freundlichen Gruessen
{{sender_name}}

+
', + body_text = 'Mitteilung zu {{reference_number}} + +Sehr geehrte/r {{requester_name}}, + +wir koennen Ihrer Anfrage leider nicht nachkommen. + +Grund: {{rejection_reason}} +Rechtsgrundlage: {{legal_basis}} + +Mit freundlichen Gruessen +{{sender_name}}', + updated_at = NOW() +WHERE template_id IN ( + SELECT id FROM compliance_email_templates WHERE template_type = 'dsr_rejection' +) AND status = 'published'; + +-- dsr_identity_request: Identitaetspruefung +UPDATE compliance_email_template_versions SET + subject = 'Identitaetspruefung erforderlich — {{reference_number}}', + body_html = '
+
+

Identitaetspruefung erforderlich

+
+
+

Sehr geehrte/r {{requester_name}},

+

fuer die Bearbeitung Ihrer Anfrage ({{reference_number}}) benoetigen wir gemaess Art. 12 Abs. 6 DSGVO eine Bestaetigung Ihrer Identitaet.

+

Bitte senden Sie uns eines der folgenden Dokumente:

+ +

Mit freundlichen Gruessen
{{sender_name}}

+
', + body_text = 'Identitaetspruefung - {{reference_number}} + +Sehr geehrte/r {{requester_name}}, + +fuer Ihre Anfrage ({{reference_number}}) benoetigen wir eine Identitaetsbestaetigung. + +Mit freundlichen Gruessen +{{sender_name}}', + updated_at = NOW() +WHERE template_id IN ( + SELECT id FROM compliance_email_templates WHERE template_type = 'dsr_identity_request' +) AND status = 'published'; + +-- dsr_extension: Fristverlaengerung +UPDATE compliance_email_template_versions SET + subject = 'Fristverlaengerung Ihrer Anfrage {{reference_number}}', + body_html = '
+
+

Fristverlaengerung

+
+
+

Sehr geehrte/r {{requester_name}},

+

wir informieren Sie, dass die Bearbeitung Ihrer Anfrage ({{reference_number}}) mehr Zeit in Anspruch nimmt als urspruenglich geplant.

+

Gemaess Art. 12 Abs. 3 DSGVO verlaengern wir die Frist:

+ + + +
Neue Frist:{{new_deadline}}
Grund:{{extension_reason}}
+

Mit freundlichen Gruessen
{{sender_name}}

+
', + body_text = 'Fristverlaengerung - {{reference_number}} + +Sehr geehrte/r {{requester_name}}, + +die Frist fuer Ihre Anfrage wurde verlaengert. +Neue Frist: {{new_deadline}} +Grund: {{extension_reason}} + +Mit freundlichen Gruessen +{{sender_name}}', + updated_at = NOW() +WHERE template_id IN ( + SELECT id FROM compliance_email_templates WHERE template_type = 'dsr_extension' +) AND status = 'published';