feat: Löschfristen je Datenklasse, Passwort-Zurücksetzen, DPF-Recherche
- Löschfristen (Migration 0020): pro Mandant einstellbar statt fest im Regelwerk, da die DSGVO selbst keine festen Fristen nennt (Art. 5 Abs. 1 lit. e). Jede Firma wird mit risikogestaffelten Vorschlagswerten vorbelegt, loeschfrist_max_tage wird jetzt tatsächlich hart gegen Werkzeuge gefiltert (schließt rules/OPEN.md Punkt 4). - Neues internal/mail-Paket (SMTP-Versand + Test-Doppel) und darauf aufbauend Passwort-Zurücksetzen (Migration 0019) - bisher nur als Absicht in der Rollentabelle genannt, nie gebaut. - dpf_zertifiziert für alle 20 Katalogeinträge gegen das offizielle DPF-Register recherchiert und in der Produktions-DB aktualisiert (12 zertifiziert, 5 recherchiert-nicht-gefunden, 3 nicht anwendbar). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
78
internal/mail/mail.go
Normal file
78
internal/mail/mail.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Package mail versendet E-Mails (aktuell: Passwort-Zurücksetzen-Links)
|
||||
// über einen konfigurierten SMTP-Server. Reine Versandlogik, kein
|
||||
// Template-Rendering — der Aufrufer (internal/web) baut Betreff/Text
|
||||
// selbst zusammen, damit dieses Paket unabhängig vom Web-Layer bleibt.
|
||||
package mail
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
// Mailer ist die Schnittstelle, die internal/web nutzt — austauschbar
|
||||
// gegen eine Test-Doppel (siehe FakeMailer), damit Tests keinen echten
|
||||
// SMTP-Server brauchen.
|
||||
type Mailer interface {
|
||||
Send(to, subject, body string) error
|
||||
}
|
||||
|
||||
// Config sind die SMTP-Zugangsdaten, aus Umgebungsvariablen gelesen
|
||||
// (siehe cmd/deklarix/main.go) — kein neues Geheimnis im Code.
|
||||
type Config struct {
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Password string
|
||||
From string
|
||||
}
|
||||
|
||||
// SMTPMailer versendet über einen echten SMTP-Server (STARTTLS, falls
|
||||
// vom Server angeboten — das deckt den Standard-Submission-Port 587 ab;
|
||||
// implizites TLS auf Port 465 wird von net/smtp nicht unterstützt).
|
||||
type SMTPMailer struct {
|
||||
cfg Config
|
||||
}
|
||||
|
||||
func NewSMTPMailer(cfg Config) *SMTPMailer {
|
||||
return &SMTPMailer{cfg: cfg}
|
||||
}
|
||||
|
||||
// Send verschickt eine reine Text-E-Mail. Liefert einen Fehler, wenn
|
||||
// kein SMTP-Host konfiguriert ist — bewusst kein stiller No-op, ein
|
||||
// fehlgeschlagener Versand (z. B. Passwort-Reset) muss sichtbar
|
||||
// scheitern statt eine E-Mail vorzutäuschen, die nie ankommt.
|
||||
func (m *SMTPMailer) Send(to, subject, body string) error {
|
||||
if m.cfg.Host == "" {
|
||||
return fmt.Errorf("mail: SMTP nicht konfiguriert (SMTP_HOST fehlt)")
|
||||
}
|
||||
addr := m.cfg.Host + ":" + m.cfg.Port
|
||||
msg := fmt.Sprintf(
|
||||
"From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s",
|
||||
m.cfg.From, to, subject, body,
|
||||
)
|
||||
|
||||
var auth smtp.Auth
|
||||
if m.cfg.User != "" {
|
||||
auth = smtp.PlainAuth("", m.cfg.User, m.cfg.Password, m.cfg.Host)
|
||||
}
|
||||
if err := smtp.SendMail(addr, auth, m.cfg.From, []string{to}, []byte(msg)); err != nil {
|
||||
return fmt.Errorf("mail: senden an %s: %w", to, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SentMail protokolliert einen Versand für Tests.
|
||||
type SentMail struct {
|
||||
To, Subject, Body string
|
||||
}
|
||||
|
||||
// FakeMailer ist das Test-Doppel für Mailer — sammelt gesendete Mails
|
||||
// statt sie zu verschicken.
|
||||
type FakeMailer struct {
|
||||
Sent []SentMail
|
||||
}
|
||||
|
||||
func (f *FakeMailer) Send(to, subject, body string) error {
|
||||
f.Sent = append(f.Sent, SentMail{To: to, Subject: subject, Body: body})
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user