// Package syncalert implementiert IMP-08: Benachrichtigung bei // wiederholtem Postfach-Sync-Ausfall, mit Eskalationsschwelle statt // Einzel-Alarm pro Fehlversuch. Versand ausschließlich über den // zentralen Core-Benachrichtigungs-Dispatcher (CFG-02, bereits Fertig) // — dieses Paket baut KEINEN eigenen E-Mail-Versand, sondern ruft // ausschließlich NotificationDispatcher.Enqueue auf (Akzeptanzkriterium // 1), exakt einmal je Eskalation. package syncalert import ( "bytes" "context" "encoding/json" "fmt" "net/http" ) // NotificationDispatcher ist die schmale Schnittstelle zu Core CFG-02 // (internal/notify.Dispatcher.Enqueue) — Mail ruft ausschließlich diese // EINE Methode auf, kein eigener Versandcode. type NotificationDispatcher interface { Enqueue(ctx context.Context, channel, recipient string, payload map[string]any) (id string, err error) } // HTTPNotificationDispatcher spricht CFG-02 über HTTP an — dieselbe // Service-Credential-Konvention wie mail/internal/crypto.HTTPKEKProvider // (API-02, X-Nexarch-Client-Id/Secret). type HTTPNotificationDispatcher struct { endpointURL string clientID string clientSecret string httpClient *http.Client } func NewHTTPNotificationDispatcher(endpointURL, clientID, clientSecret string, httpClient *http.Client) *HTTPNotificationDispatcher { if httpClient == nil { httpClient = http.DefaultClient } return &HTTPNotificationDispatcher{endpointURL: endpointURL, clientID: clientID, clientSecret: clientSecret, httpClient: httpClient} } type enqueueRequest struct { Channel string `json:"channel"` Recipient string `json:"recipient"` Payload map[string]any `json:"payload"` } type enqueueResponse struct { ID string `json:"id"` } func (d *HTTPNotificationDispatcher) Enqueue(ctx context.Context, channel, recipient string, payload map[string]any) (string, error) { body, err := json.Marshal(enqueueRequest{Channel: channel, Recipient: recipient, Payload: payload}) if err != nil { return "", fmt.Errorf("syncalert: anfrage serialisieren: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, d.endpointURL, bytes.NewReader(body)) if err != nil { return "", fmt.Errorf("syncalert: anfrage aufbauen: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Nexarch-Client-Id", d.clientID) req.Header.Set("X-Nexarch-Client-Secret", d.clientSecret) resp, err := d.httpClient.Do(req) if err != nil { return "", fmt.Errorf("syncalert: anfrage senden: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("syncalert: cfg-02 lehnte anfrage ab: status %d", resp.StatusCode) } var out enqueueResponse if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return "", fmt.Errorf("syncalert: antwort dekodieren: %w", err) } return out.ID, nil }