Zum Inhalt springen

AA_EMAIL

Status: Entwurf · Spec-Kandidat: ja

Zweck

Stammdaten und Protokolle für den Email-Versand. Kapselt drei Verantwortlichkeiten:

  • AA_EMAIL_TEMPLATE — versionierte, mehrsprachige Vorlage (Subject + Plain-Text + HTML + Variablen-Whitelist).
  • AA_EMAIL_SUPPRESSION — Sperrliste pro Tenant (Bounces, Complaints, Unsubscribe, manuell). Wird vor jedem Versand geprüft.
  • AA_EMAIL_LOG — Append-Log eines Versand-Versuchs inkl. Provider-Antwort, Bounce-/Delivery-Status. Langfristiges Gedächtnis, unabhängig von der Lebensdauer der AA_JOB-Zeile.

Der Versand selbst läuft als Job in AA_JOB mit job_kind='email_send'. Der Worker rendert anhand dieser Tabellen.

AA_EMAIL_TEMPLATE

FeldTypPflichtHinweise
iduuidjaPK
tenant_iduuidjaFK → AA_TENANT. Templates sind tenant-scoped (Default-Templates werden beim Tenant-Onboarding instanziiert)
keytextjalogischer Name, z. B. case_created, password_reset, welcome
localetextjaBCP-47-Tag, z. B. de, en, de-CH. Whitelist je Tenant siehe Tenant-Konfig
versionintegerjadefault 1, monoton steigend je (tenant_id, key, locale)
subjecttextjaMustache-/Handlebars-kompatible Platzhalter {{var}}
body_texttextjaPlain-Text-Variante, Pflicht (Fallback für Reader ohne HTML)
body_htmltextneinoptionale HTML-Variante
variablesjsonbjadefault [] — Liste erlaubter Platzhalter mit Typ-Hints ([{ "name": "case_code", "type": "string", "required": true }, …])
is_activebooleanjadefault true. Inaktive Versionen bleiben für historische Jobs verfügbar (siehe Pinning)
metadatajsonbjadefault '{}'
Audit-/Soft-Delete-Felder

Constraints

  • email_template_key_format_chk: key ~ '^[a-z][a-z0-9_]*$'
  • email_template_locale_format_chk: locale ~ '^[a-z]{2}(-[A-Z]{2})?$' (BCP-47 vereinfacht)
  • email_template_variables_is_array_chk: jsonb_typeof(variables) = 'array'
  • email_template_metadata_is_object_chk
  • email_template_uk: UNIQUE (tenant_id, key, locale, version)

Versionierung & Pin

  • Beim Enqueue eines Email-Jobs hält der Service die zu verwendende template_version im Job-Payload fest (Pin). Verzögert versendete Mails (Backoff, Wartung) nutzen exakt die zur Enqueue-Zeit aktive Version, auch wenn parallel eine neue Version aktiv geschaltet wurde.
  • Eine neue Version wird per INSERT mit version = max(version)+1 und is_active=true angelegt; ältere Versionen werden auf is_active=false gesetzt (Service-Operation, atomar).
  • Hard-Delete einer Version ist nicht vorgesehen — sie würde Logs unleserlich machen. Deaktivierung reicht.

Default-Templates beim Tenant-Onboarding

Analog zu Default-Rollen werden Default-Templates beim Anlegen eines Tenants in den neuen Tenant kopiert (siehe Tenant-Onboarding). Das initiale Default-Set bleibt klein und wird mit konkreten Use Cases erweitert; die Liste lebt im Migrations-Code, nicht hier.

AA_EMAIL_SUPPRESSION

FeldTypPflichtHinweise
iduuidjaPK
tenant_iduuidjaFK → AA_TENANT. Suppression ist pro Tenant, nicht global
emailtextjanormalisiert (lowercase, trim)
reasontextjabounce, complaint, unsubscribe, manual
provider_event_idtextneinProvider-Referenz (z. B. SES-Bounce-Message-ID)
metadatajsonbjadefault '{}' (Provider-Original-Payload, Bounce-Subtyp, etc.)
created_at / created_by

Constraints

  • email_suppression_email_format_chk: lockere Validierung email LIKE '%@%' plus Service-Layer-Prüfung
  • email_suppression_reason_chk: reason IN ('bounce','complaint','unsubscribe','manual')
  • email_suppression_uk: UNIQUE (tenant_id, email) — pro Tenant höchstens ein aktiver Eintrag je Adresse

Verhalten

  • Worker prüft vor jedem Send: SELECT 1 FROM AA_EMAIL_SUPPRESSION WHERE tenant_id=? AND email=?. Treffer → Job wird mit Status done und Log-Status suppressed abgeschlossen, kein Provider-Aufruf.
  • Bounce-/Complaint-Webhooks (siehe Email-Schnittstelle) führen zum INSERT … ON CONFLICT (tenant_id, email) DO UPDATE SET reason=EXCLUDED.reason, ….
  • Unsubscribe-Links sind ein dedizierter Endpoint, der ebenfalls in dieser Tabelle landet. Wiederaktivierung (DELETE) ist Admin-Aktion mit Audit-Zeile.

AA_EMAIL_LOG

FeldTypPflichtHinweise
iduuidjaPK
tenant_iduuidja
job_iduuidneinFK → AA_JOB(id) ON DELETE SET NULL (Job-Cleanup darf Log nicht zerreißen)
to_emailtextjanormalisiert
from_emailtextjaaufgelöst aus Tenant-Config oder payload.from_override
template_keytextneinNULL bei Ad-hoc-Versand ohne Template
template_versionintegerneinexakt verwendete Version (Pin)
localetextneintatsächlich gerenderte Locale (nach Fallback-Resolution)
subjecttextneingerenderter Subject (für Audit; PII-Konzept beachten)
providertextjasmtp, ses, sendgrid, dryrun
provider_message_idtextneinProvider-Antwort-ID
statustextjaqueued, sent, delivered, bounced, failed, suppressed
sent_attimestamptzneingesetzt nach 2xx vom Provider
delivered_attimestamptzneingesetzt durch Delivery-Webhook
bounced_attimestamptzneingesetzt durch Bounce-Webhook
errortextneinletzter Fehler bei failed
attachment_countsmallintjadefault 0
created_attimestamptzjadefault now()

Constraints

  • email_log_status_chk: status IN ('queued','sent','delivered','bounced','failed','suppressed')
  • Keine Soft-Delete; AA_EMAIL_LOG ist append-only.

Indizes

  • ix_email_log_tenant_recent: (tenant_id, created_at DESC) — Admin-/Audit-Sichten
  • ix_email_log_job partial: (job_id) WHERE job_id IS NOT NULL
  • ix_email_log_to_email: (tenant_id, to_email, created_at DESC) — Pro-Adresse-Historie
  • ix_email_log_provider_msg partial: (provider, provider_message_id) WHERE provider_message_id IS NOT NULL — schneller Webhook-Match

Beziehung zu AA_AUDIT_LOG

AA_EMAIL_LOG ist die operative Quelle. Zusätzlich schreibt der Worker AA_AUDIT_LOG-Zeilen mit action='email_sent' | 'email_bounced' | 'email_suppressed' und payload->>'email_log_id', damit Email-Outcomes auch in der zentralen Audit-Sicht auftauchen.

Verwandte Dokumente