// 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 }