Neues Paket mail/internal/notifyclient: Mail-seitige Kopplung an Core CFG-02/CFG-05 (POST /notify, service-token-authentifiziert). Core CFG-02/CFG-05 stehen auf core-kanban zwar auf "Fertig", haben im aktuellen Repository-Stand aber keinen abrufbaren Endpunkt — dieselbe Situation wie ARC-06/Core TEN-01 und INT-01/Core API-01, im Prüfprotokoll begründet. Client richtet sich nach dem in CFG-05s eigener Beschreibung dokumentierten Vertrag. 204 wird bewusst nicht als Fehler behandelt (CFG-05 wrappt laut Beschreibung bereits notifyprefs.EnqueueIfAllowed — die Zustellentscheidung nach Benutzerpräferenz liegt vollständig bei Core, Mail dupliziert diese Logik nicht). Neues Paket mail/internal/importnotify: NotifyBatch löst am Ende EINES imapimport.RunOnce-Laufs höchstens EINEN Notify-Aufruf aus — es gibt strukturell keinen Codepfad für mehr als einen Aufruf je Lauf (Bündelung statt Flut bei Massenimport). Alle drei Pflichtprüfungen mit echten Nachweisen: eine neue Nachricht löst genau eine Benachrichtigung aus, 50 neue Nachrichten weiterhin genau eine gebündelte Benachrichtigung (Count: 50), ein echter HTTP-Server bildet den CFG-05-204-Unterdrückungsvertrag nach und bestätigt keine Zustellung ohne Fehler. Ergänzt um echte Fehlerpfade (5xx, nicht erreichbarer Endpunkt mit Timeout statt unbegrenztem Warten). go build/go vet/golangci-lint clean, gesamtes Mail-Modul regressionsfrei getestet.
88 lines
3.2 KiB
Go
88 lines
3.2 KiB
Go
// Package notifyclient implementiert die Mail-seitige Kopplung an Core
|
|
// CFG-02/CFG-05 (INT-05): ein zentraler Dispatcher übernimmt Warteschlange,
|
|
// Wiederholungslogik, Kanal-Abstraktion UND — laut CFG-05s eigener
|
|
// Beschreibung ("internal/notifyprefs.EnqueueIfAllowed als HTTP-Endpunkt")
|
|
// — die Prüfung, ob der Benutzer diese Benachrichtigungsart überhaupt
|
|
// wünscht. Mail baut deshalb bewusst KEINE eigene
|
|
// Benachrichtigungs-/Präferenzlogik, sondern ruft ausschließlich den
|
|
// dokumentierten Vertrag "POST /notify" auf.
|
|
//
|
|
// Core CFG-02/CFG-05 stehen auf core-kanban zwar auf "Fertig", enthalten
|
|
// im aktuellen Repository-Stand aber keinen abrufbaren Endpunkt (gleiche
|
|
// Situation wie ARC-06/Core TEN-01, INT-01/Core API-01) — Client richtet
|
|
// sich nach dem im Core-Board dokumentierten Vertrag (service-token-
|
|
// authentifiziertes POST /notify), siehe INT-05-Prüfprotokoll.
|
|
package notifyclient
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// Event ist eine einzelne Benachrichtigung an den Core-Dispatcher.
|
|
type Event struct {
|
|
TenantSlug string `json:"tenant_slug"`
|
|
EventType string `json:"event_type"`
|
|
Summary string `json:"summary"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
// Client ruft Core CFG-05s "POST /notify" auf.
|
|
type Client struct {
|
|
baseURL string
|
|
serviceToken string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// NewClient erstellt einen Client. baseURL und serviceToken kommen
|
|
// ausschließlich vom Aufrufer (Umgebungsvariable) — keine
|
|
// Zugangsdaten im Code.
|
|
func NewClient(baseURL, serviceToken string) *Client {
|
|
return &Client{
|
|
baseURL: baseURL,
|
|
serviceToken: serviceToken,
|
|
httpClient: &http.Client{Timeout: 5 * time.Second},
|
|
}
|
|
}
|
|
|
|
// Notify sendet EIN Ereignis. Ein HTTP-Fehler (Netzwerk, 5xx) wird als
|
|
// Fehler zurückgeliefert — der Aufrufer entscheidet, ob das den
|
|
// regulären Mail-Betrieb blockiert (siehe importnotify: Notify läuft
|
|
// NIE im Importpfad selbst, ein Fehler hier verhindert keine bereits
|
|
// abgeschlossene Nachrichtenübernahme). Ein 2xx- ODER 204-Status gilt
|
|
// als Erfolg — 204 bedeutet laut CFG-05s EnqueueIfAllowed-Vertrag
|
|
// "durch Benutzerpräferenz unterdrückt, kein Fehler".
|
|
func (c *Client) Notify(ctx context.Context, ev Event) error {
|
|
body, err := json.Marshal(ev)
|
|
if err != nil {
|
|
return fmt.Errorf("notifyclient: ereignis serialisieren: %w", err)
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/notify", bytes.NewReader(body))
|
|
if err != nil {
|
|
return fmt.Errorf("notifyclient: anfrage bauen: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+c.serviceToken)
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("notifyclient: /notify aufrufen: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
|
|
// 2xx (inkl. 204 "No Content") gilt als Erfolg — 204 bedeutet laut
|
|
// CFG-05s EnqueueIfAllowed-Vertrag "durch Benutzerpräferenz
|
|
// unterdrückt", was Mail nicht als Fehler behandelt (die
|
|
// Zustell-/Präferenzentscheidung ist bewusst Core-Sache).
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("notifyclient: /notify status %d: %s", resp.StatusCode, string(respBody))
|
|
}
|