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 derAA_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
| Feld | Typ | Pflicht | Hinweise |
|---|---|---|---|
id | uuid | ja | PK |
tenant_id | uuid | ja | FK → AA_TENANT. Templates sind tenant-scoped (Default-Templates werden beim Tenant-Onboarding instanziiert) |
key | text | ja | logischer Name, z. B. case_created, password_reset, welcome |
locale | text | ja | BCP-47-Tag, z. B. de, en, de-CH. Whitelist je Tenant siehe Tenant-Konfig |
version | integer | ja | default 1, monoton steigend je (tenant_id, key, locale) |
subject | text | ja | Mustache-/Handlebars-kompatible Platzhalter {{var}} |
body_text | text | ja | Plain-Text-Variante, Pflicht (Fallback für Reader ohne HTML) |
body_html | text | nein | optionale HTML-Variante |
variables | jsonb | ja | default [] — Liste erlaubter Platzhalter mit Typ-Hints ([{ "name": "case_code", "type": "string", "required": true }, …]) |
is_active | boolean | ja | default true. Inaktive Versionen bleiben für historische Jobs verfügbar (siehe Pinning) |
metadata | jsonb | ja | default '{}' |
| 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_chkemail_template_uk:UNIQUE (tenant_id, key, locale, version)
Versionierung & Pin
- Beim Enqueue eines Email-Jobs hält der Service die zu verwendende
template_versionim 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
INSERTmitversion = max(version)+1undis_active=trueangelegt; ältere Versionen werden aufis_active=falsegesetzt (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
| Feld | Typ | Pflicht | Hinweise |
|---|---|---|---|
id | uuid | ja | PK |
tenant_id | uuid | ja | FK → AA_TENANT. Suppression ist pro Tenant, nicht global |
email | text | ja | normalisiert (lowercase, trim) |
reason | text | ja | bounce, complaint, unsubscribe, manual |
provider_event_id | text | nein | Provider-Referenz (z. B. SES-Bounce-Message-ID) |
metadata | jsonb | ja | default '{}' (Provider-Original-Payload, Bounce-Subtyp, etc.) |
created_at / created_by | – | – |
Constraints
email_suppression_email_format_chk: lockere Validierungemail LIKE '%@%'plus Service-Layer-Prüfungemail_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 Statusdoneund Log-Statussuppressedabgeschlossen, 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
| Feld | Typ | Pflicht | Hinweise |
|---|---|---|---|
id | uuid | ja | PK |
tenant_id | uuid | ja | |
job_id | uuid | nein | FK → AA_JOB(id) ON DELETE SET NULL (Job-Cleanup darf Log nicht zerreißen) |
to_email | text | ja | normalisiert |
from_email | text | ja | aufgelöst aus Tenant-Config oder payload.from_override |
template_key | text | nein | NULL bei Ad-hoc-Versand ohne Template |
template_version | integer | nein | exakt verwendete Version (Pin) |
locale | text | nein | tatsächlich gerenderte Locale (nach Fallback-Resolution) |
subject | text | nein | gerenderter Subject (für Audit; PII-Konzept beachten) |
provider | text | ja | smtp, ses, sendgrid, dryrun |
provider_message_id | text | nein | Provider-Antwort-ID |
status | text | ja | queued, sent, delivered, bounced, failed, suppressed |
sent_at | timestamptz | nein | gesetzt nach 2xx vom Provider |
delivered_at | timestamptz | nein | gesetzt durch Delivery-Webhook |
bounced_at | timestamptz | nein | gesetzt durch Bounce-Webhook |
error | text | nein | letzter Fehler bei failed |
attachment_count | smallint | ja | default 0 |
created_at | timestamptz | ja | default now() |
Constraints
email_log_status_chk:status IN ('queued','sent','delivered','bounced','failed','suppressed')- Keine Soft-Delete;
AA_EMAIL_LOGist append-only.
Indizes
ix_email_log_tenant_recent:(tenant_id, created_at DESC)— Admin-/Audit-Sichtenix_email_log_jobpartial:(job_id)WHERE job_id IS NOT NULLix_email_log_to_email:(tenant_id, to_email, created_at DESC)— Pro-Adresse-Historieix_email_log_provider_msgpartial:(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.