package channels import ( "context" "fmt" "net/smtp" "os" "gitea.perlbach24.de/scripte/nexarch/internal/notify" ) // EmailSender implementiert notify.Sender fuer den E-Mail-Kanal // (Akzeptanzkriterium 1). SMTP-Zugangsdaten kommen ausschliesslich aus // Umgebungsvariablen, nie aus Code/DB. type EmailSender struct { templates *TemplateStore host string port string from string username string password string } // NewEmailSenderFromEnv liest NEXARCH_SMTP_HOST/PORT/FROM (Pflicht) sowie // optional NEXARCH_SMTP_USER/PASSWORD. func NewEmailSenderFromEnv(templates *TemplateStore) (*EmailSender, error) { host := os.Getenv("NEXARCH_SMTP_HOST") port := os.Getenv("NEXARCH_SMTP_PORT") from := os.Getenv("NEXARCH_SMTP_FROM") if host == "" || port == "" || from == "" { return nil, fmt.Errorf("channels: NEXARCH_SMTP_HOST/PORT/FROM muessen gesetzt sein") } return &EmailSender{ templates: templates, host: host, port: port, from: from, username: os.Getenv("NEXARCH_SMTP_USER"), password: os.Getenv("NEXARCH_SMTP_PASSWORD"), }, nil } // NewEmailSender erlaubt Tests, Host/Port explizit zu setzen (z.B. einen // absichtlich nicht erreichbaren Host fuer den Retry-Nachweis), ohne // Umgebungsvariablen zu benoetigen. func NewEmailSender(templates *TemplateStore, host, port, from string) *EmailSender { return &EmailSender{templates: templates, host: host, port: port, from: from} } // Send rendert zuerst die Vorlage — schlaegt das fehl (z.B. fehlender // Platzhalter), wird NIE eine SMTP-Verbindung aufgebaut // (Akzeptanzkriterium 1 / Pruefung 1: kontrollierter Abbruch statt // fehlerhafter Mail). Ein danach fehlschlagender SMTP-Versand liefert einen // Fehler zurueck, den CFG-02s Dispatcher fuer die bereits getestete // Wiederholungslogik nutzt (Akzeptanzkriterium 3 / Pruefung 3) — kein // zweiter Retry-Mechanismus hier. func (e *EmailSender) Send(ctx context.Context, n notify.Notification) error { templateKey, _ := n.Payload["template_key"].(string) tenantSlug, _ := n.Payload["tenant_slug"].(string) tmpl, err := e.templates.Resolve(ctx, tenantSlug, templateKey) if err != nil { return fmt.Errorf("vorlage aufloesen: %w", err) } subject, body, err := Render(tmpl, n.Payload) if err != nil { return err } msg := []byte("Subject: " + subject + "\r\n\r\n" + body) var auth smtp.Auth if e.username != "" { auth = smtp.PlainAuth("", e.username, e.password, e.host) } addr := e.host + ":" + e.port if err := smtp.SendMail(addr, auth, e.from, []string{n.Recipient}, msg); err != nil { return fmt.Errorf("smtp-versand fehlgeschlagen: %w", err) } return nil }