- 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>
79 lines
2.4 KiB
Go
79 lines
2.4 KiB
Go
// 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
|
|
}
|