feat(mail): INT-01 REST-API v1 für Mail-Zugriff & OpenAPI-Beschreibung
Neues Paket mail/internal/mailapi: drei v1-Endpunkte (Mail-Liste, Mail-Detail, Anhang-Download). Core API-01 (REST-Grundgerüst) und API-04 (OpenAPI-Beschreibung) haben im aktuellen Repository-Stand keinen abrufbaren Router — RegisterRoutes registriert die Endpunkte deshalb auf einem vom Aufrufer bereitgestellten *http.ServeMux mit dem dokumentierten Pfadschema /api/v1/mail/..., Core kann sich später dort einhängen, im Prüfprotokoll begründet (gleiche Situation wie ARC-06/Core TEN-01). tenant-Query-Parameter ist auf allen drei Endpunkten Pflicht (fehlender Kontext -> 400), keine eigene Login-/Session-Logik (IAM bleibt Core-Board-Sache). Anhang-Download nutzt storage.ObjectKey gegen den physisch getrennten Bucket des Mandanten (ARC-06) — ein Anhang mit identischer messageID in einem fremden Mandantenkontext ist strukturell nicht erreichbar. Neue Methode search.Client.GetByMessageID liefert das vollständige Suchdokument für Mail-Detail. openapi.yaml: vollständiger OpenAPI-3-Beitrag für alle drei Endpunkte inklusive Fehlerantworten. Als neue, gepinnte Abhängigkeit github.com/getkin/kin-openapi v0.135.0 (bewusst nicht @latest — hätte das Modul von go 1.24 auf go 1.25 gezwungen) für einen echten Standard-Validierungslauf gegen das Dokument sowie einen OpenAPI-Router, der jede implementierte Route real gegen das Dokument auflöst statt nur Pfad-Strings zu vergleichen. Alle vier Pflichtprüfungen mit echten Nachweisen: Zugriff ohne Tenant-Kontext auf allen drei Endpunkten abgelehnt, Vertragstests inkl. physischer Bucket-Trennung beim Anhang-Download, automatisiertes Code-Review bestätigt Abwesenheit IAM-naher Bezeichner, OpenAPI-Dokument validiert fehlerfrei gegen kin-openapi. go build/go vet/golangci-lint clean, go mod verify clean, gesamtes Mail-Modul regressionsfrei getestet.
This commit is contained in:
@@ -334,6 +334,52 @@ func (c *Client) Search(ctx context.Context, tenantSlug, queryText string) ([]Re
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetByMessageID liefert das vollständige Suchdokument EINER Nachricht
|
||||
// (INT-01 Akzeptanzkriterium 1: Mail-Detail braucht mehr Felder als
|
||||
// Search()s Result — insbesondere Body). ok=false, wenn keine
|
||||
// Nachricht mit dieser message_id für tenantSlug existiert
|
||||
// (Akzeptanzkriterium 2: strikt mandantengebunden — eine fremde
|
||||
// message_id liefert hier KEIN Dokument, weil tenant_slug Teil der
|
||||
// Pflichtbedingung ist, nicht nur ein optionaler Filter).
|
||||
func (c *Client) GetByMessageID(ctx context.Context, tenantSlug, messageID string) (Document, bool, error) {
|
||||
payload := map[string]any{
|
||||
"index": IndexName,
|
||||
"query": map[string]any{
|
||||
"bool": map[string]any{
|
||||
"must": []map[string]any{
|
||||
{"equals": map[string]any{FieldTenantSlug: tenantSlug}},
|
||||
{"equals": map[string]any{FieldMessageID: messageID}},
|
||||
},
|
||||
},
|
||||
},
|
||||
"limit": 1,
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return Document{}, false, fmt.Errorf("search: detailanfrage serialisieren: %w", err)
|
||||
}
|
||||
respBody, err := c.doSearchWithSwapRetry(ctx, body)
|
||||
if err != nil {
|
||||
return Document{}, false, err
|
||||
}
|
||||
var parsed documentSearchResponse
|
||||
if err := json.Unmarshal(respBody, &parsed); err != nil {
|
||||
return Document{}, false, fmt.Errorf("search: antwort parsen: %w", err)
|
||||
}
|
||||
if len(parsed.Hits.Hits) == 0 {
|
||||
return Document{}, false, nil
|
||||
}
|
||||
return parsed.Hits.Hits[0].Source, true, nil
|
||||
}
|
||||
|
||||
type documentSearchResponse struct {
|
||||
Hits struct {
|
||||
Hits []struct {
|
||||
Source Document `json:"_source"`
|
||||
} `json:"hits"`
|
||||
} `json:"hits"`
|
||||
}
|
||||
|
||||
type searchResponse struct {
|
||||
Hits struct {
|
||||
Hits []struct {
|
||||
|
||||
Reference in New Issue
Block a user