Files
nexarch/mail/internal/syncalert/dispatcher.go
T
sysopsandClaude Sonnet 5 56d31c9176 IMP-08: fehler-benachrichtigung-bei-postfach-sync-ausfall
Benachrichtigung bei wiederholtem Postfach-Sync-Ausfall, mit
Eskalationsschwelle statt Einzel-Alarm pro Fehlversuch. Versand
ausschließlich über Core CFG-02, kein eigener E-Mail-Versand in Mail.

- dispatcher.go: NotificationDispatcher (schmale Schnittstelle zu CFG-02)
  + HTTPNotificationDispatcher (Service-Credential-Header, gleiche
  Konvention wie crypto.HTTPKEKProvider). Core exponiert internal/notify.
  Dispatcher.Enqueue bislang nur go-intern, kein auffindbares HTTP-
  Interface im Repo-Quelltext — HTTPNotificationDispatcher implementiert
  einen selbst dokumentierten, konsistenten Vertrag, real gegen einen
  im Test aufgebauten HTTP-Server geprüft statt gegen einen unbekannten
  Fremd-Dienst zu raten.
- monitor.go: Monitor.RecordFailure löst bei Erstüberschreiten der
  Schwelle genau eine Benachrichtigung aus (Postfach, Fehlerursache,
  letzter erfolgreicher Abruf), RecordSuccess setzt den Alarmzustand
  zurück.

Prüfungen (alle real durchgeführt, siehe mail/docs/IMP-08-PRUEFPROTOKOLL.md):
1. TestRecordFailure_NConsecutiveFailuresTriggerExactlyOneNotification:
   3 Fehlschläge real genau 1 Benachrichtigung, weitere real keine.
2. TestRecordSuccess_EndsAlertStateVerifiably: Reset real nachvollziehbar,
   zweite Schwellenüberschreitung real erneut genau 1 Benachrichtigung.
3. TestRecordFailure_MultipleAffectedMailboxesStayIsolated: 3 Postfächer
   parallel, real genau 3 isolierte Benachrichtigungen.

Kein Umbau: imapimport (IMP-01/IMP-04) unverändert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
2026-09-01 00:11:42 +02:00

81 lines
2.8 KiB
Go

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