CMP-08: ende-zu-ende-nachweis-ediscovery-adapter-reale-drittmodul-integration #327

Closed
patrick wants to merge 1 commits from feature/cmp-08-ende-zu-ende-nachweis-ediscovery-adapter-reale-drittmodul-integration into feature/qa-03-prüfgate-compliance-und-ediscovery
pull from: feature/cmp-08-ende-zu-ende-nachweis-ediscovery-adapter-reale-drittmodul-integration
3 changed files with 480 additions and 0 deletions

No files matched your search

+125
View File
@@ -88,3 +88,128 @@ und nachgewiesen: eDiscovery-Export gegen den offiziellen Referenzdatensatz mit
Testrollen (Erfolgsfall und erneut scheiternder Alleingang); Protokoll mit Zielwert/Messwert/
Bewertung abgelegt, offene Restrisiken schriftlich benannt. Freigabe der Compliance-Funktionen aus
Sicht dieses Gates erteilt — Gesamtabnahme bleibt QA-05 vorbehalten.
## Nachtrag CMP-08 (Stand 2026-10-16): Ende-zu-Ende-Nachweis des Adapters gegen echte DMS-/Mail-Testinstanzen
Schließt den oben in Abschnitt "Restrisiken" benannten Punkt **"eDiscovery-Adapter real nur
fiktiv"**. Neue Produktivkomponente `archive/internal/ediscoveryhttp` (analog
`kekclient`/`rbacclient`): `DMSAdapter` und `MailAdapter` implementieren
`ediscovery.ModuleAdapter` über echtes HTTP gegen die realen REST-Endpunkte von DMS
(`dms/internal/searchapi` `GET /search`, `dms/internal/documentapi` `GET
/documents/{id}/content`) bzw. Mail (`mail/internal/mailapi` `GET /api/v1/mail/messages`,
`GET /api/v1/mail/messages/{id}`, mit Modul-Token `aud=mail`).
### Echte Testinstanzen (Host 192.168.1.204)
- **DMS**: echter `dms/internal/searchwiring` + neu ergänzter `dms/internal/documentapi` auf
einem frisch migrierten Schema der bestehenden Dev-Datenbank `dms_dev`
(`NEXARCH_DMS_APP_TENANT_DSN` aus `/etc/nexarch-dev/dms-dev.env`), echtes Manticore
(`TEST_MANTICORE_ADDR`), 4 synthetische PDF-Belege über den echten Upload-/Worker-Pfad
(`dms/internal/upload`, `dms/internal/workerruntime`, `dms/internal/testcorpus`) hochgeladen
und über die echte Volltextsuche auffindbar gemacht — keine Fixture-Doubles.
- **Mail**: echter `mail/internal/mailapi.Server`, mit echtem `mail/internal/search.Client`
(Manticore-Tabelle `mail_documents`, eigener Mandant `cmp08-ediscovery-mail`) und echtem
`mail/internal/storage.Service`; 4 synthetische Nachrichten über `search.Client.Index`
eingetragen. Mail hat noch keinen eigenen `cmd/app`-Produktivserver (siehe
`mail/internal/mailapi`-Paketkommentar) — Modul-Token-Verifikation läuft über die ECHTE
Verifikationslogik `mail/internal/moduleauth.Verifier`, mit einem lokal erzeugten
Test-Schlüsselpaar statt eines laufenden Core-Kontodienstes (`NewTestVerifierAndIssuer`,
dieselbe Konvention wie `mail/internal/mailapi/e2e_test.go`) — siehe Restrisiko unten.
- Beide Instanzen liefen als Wegwerf-Prüfwerkzeuge (`dms/cmd/cmp08-ediscovery-fixture`,
`mail/cmd/cmp08-ediscovery-fixture`), nach dem Testlauf wieder entfernt — nicht Teil dieses
Commits, nur die Produktivkomponente (`ediscoveryhttp`) und der Nachweistest bleiben.
- Alle Testdaten synthetisch (Suchbegriff `CmpAchtEdiscoveryFund`, Absendername
`pruefperson@cmp08.example.test`), keine echten Kunden-/Mandantendaten.
### Prüfung 1 (Akzeptanzkriterium 1): Export gegen echte, über RET-05 registrierte Instanzen
Vor dem eigentlichen Such-/Exportlauf registriert der Test beide Instanzen real über das
bestehende RET-05-Interface (`archive/internal/moduleadapter.Register`) gegen die echte
Postgres-Testdatenbank (`TEST_TENANT_DSN`): `module_name=dms/object_type=dms_document` mit
Rückruf-Adresse der echten laufenden DMS-Instanz, `module_name=mail/object_type=mail_message`
mit Rückruf-Adresse der echten laufenden Mail-Instanz.
| Prüfgröße | Zielwert | Messwert | Bewertung |
|---|---|---|---|
| Registrierungen in `module_registrations` nach Register() | 2 (DMS + Mail) | 2 | bestanden |
| Export erfolgt gegen die registrierten, laufenden Instanzen (nicht Referenzdatensatz) | ja | ja — `ediscoveryhttp.DMSAdapter`/`MailAdapter` rufen die echten Endpunkte auf | bestanden |
### Prüfung 2 (Akzeptanzkriterium 2): Treffer aus beiden Modulen, prüfsummengesichert
Test `TestEndToEnd_CMP08_RealDMSAndMailInstances`
(`archive/internal/ediscoveryhttp/e2e_test.go`): `ediscovery.Search()` über beide echten
Adapter, `ediscovery.Export()` baut das ZIP, der Test entpackt es erneut und vergleicht die
SHA-256-Prüfsumme jeder Datei gegen den `manifest.json`-Eintrag.
| Prüfgröße | Zielwert | Messwert | Bewertung |
|---|---|---|---|
| Treffer mit `module_type=dms_document` im Suchergebnis | ≥ 1 | 4 | bestanden |
| Treffer mit `module_type=mail_message` im Suchergebnis | ≥ 1 | 4 | bestanden |
| Prüfsumme nach Entpacken stimmt für jede Datei (beide Modultypen) | 100 % Übereinstimmung | 100 % (8/8 Dateien) | bestanden |
### Prüfung 3 (Pflichtprüfung 2): Export-Prüfsummen bei wiederholtem Testlauf identisch
Zwei unabhängige `Search()+Export()`-Durchläufe INNERHALB desselben Testlaufs (frischer
HTTP-Roundtrip gegen die echten Instanzen, nicht wiederverwendete Treffer), Vergleich über
`modultyp+referenz` statt Listenposition (Reihenfolge der Suchtreffer ist zwischen Läufen nicht
garantiert stabil). Zusätzlich der komplette Test zweimal als VOLLSTÄNDIG unabhängiger
`go test`-Prozessaufruf gegen dieselben laufenden Instanzen wiederholt.
| Prüfgröße | Zielwert | Messwert | Bewertung |
|---|---|---|---|
| Prüfsummen Lauf 1 vs. Lauf 2 (innerhalb des Tests, je Treffer) | identisch | identisch (8/8) | bestanden |
| Testergebnis über 2 unabhängige Prozessaufrufe hinweg | PASS, PASS | PASS, PASS | bestanden |
### Build-/Testergebnis (192.168.1.204)
```
go build ./... -> clean
go vet ./... -> clean
go test ./internal/ediscoveryhttp/... -v -> PASS (2x unabhaengiger Prozessaufruf)
go test ./... -p 1 -> alle Archive-Pakete bestanden (2x wiederholt)
```
Ein einzelner, nicht reproduzierbarer Fehlschlag in `archive/internal/reconcile`
(`TestListDBStorageKeys_ReadsRealFileRevisions`, "erwartet 1 Eintrag, habe 9") trat einmalig
während eines `go test ./... -p 1`-Laufs auf. Isoliert ausgeführt sowie in je zwei weiteren
vollständigen Suite-Läufen (mit UND ohne die CMP-08-Änderungen) bestand dieser Test
durchgehend — kein durch CMP-08 verursachter Regressionsbefund, sondern dieselbe Art
Testisolations-Lücke gegen die geteilte `TEST_TENANT_DSN`-Datenbank, wie sie für das DMS-Modul
bereits dokumentiert ist. Nicht in dieser Kachel behoben (unverändertes, fremdes Paket).
### Restrisiken (neu, durch CMP-08 gefunden)
- **Mail hat noch keinen RET-05-Rückruf-EMPFÄNGER**: DMS registriert sich bereits produktiv
selbst gegen RET-05 inkl. echtem Rückruf-Empfänger (`dms/cmd/app` `setupRetention`,
DOC-16, `POST /retention/destroy-callback`). Für Mail existiert dieses Gegenstück noch nicht
(nur der RET-13-Sperrabfrage-Client `mail/internal/retentionclient`) — die in diesem Test
registrierte Rückruf-Adresse der Mail-Instanz ist real erreichbar, aber die Instanz
verarbeitet eingehende Vernichtungs-Rückrufe fachlich noch nicht. Empfehlung: eigenes
Folgeticket, Mail-seitige Rückruf-Verarbeitung analog DOC-16 ergänzen, sobald Mail einen
eigenen `cmd/app` bekommt.
- **Mail-Modul-Token stammt aus einem Test-Schlüsselpaar, nicht vom echten Core-Kontodienst**:
weil Mail noch keinen produktiven Server-Prozess hat, an den ein echter Core-JWKS-Abruf
gebunden werden könnte (anders als bei DMS, dessen `cmd/app` dafür bereits verdrahtet ist).
Die Verifikationslogik selbst (`moduleauth.Verifier`) ist unverändert die Produktivfunktion.
Ein Nachweis mit einem vom echten Core ausgestellten Token (wie in
`mail/internal/mailapi/e2e_test.go` `TestEndToEnd_RealCoreToken`) ist erst möglich, sobald ein
Core-Kontodienst auf demselben Testhost erreichbar ist — außerhalb des Zuschnitts dieser
Kachel (keine neue eDiscovery-Funktionalität, kein Core-Deployment).
- **RET-05-Tenant-Verifikation (Core TEN-01) nicht Teil dieses Nachweises**: die Registrierung
erfolgte über den direkten Aufruf von `moduleadapter.Register` (die reale
Persistenzfunktion), nicht über den HTTP-Endpunkt `archive/cmd/moduleadapter-api` mit
`tenantregistryclient.VerifyTenant` gegen einen echten Core-TEN-01-Dienst — letzterer war auf
diesem Testhost nicht aufgesetzt (separates Infrastrukturthema, keine eDiscovery-Logik). Die
eigentliche RET-05-Speicherlogik (Register/ListRegistrations, Idempotenz) ist bereits durch
RET-05/RET-09 real geprüft und wird hier nicht erneut bewiesen.
- golangci-lint weiterhin nicht auf Host 204 installiert (siehe QA-03 oben, unverändert).
### Gesamtergebnis CMP-08
**Bestanden.** Alle drei Akzeptanzkriterien und beide Pflichtprüfungen real gegen echte,
laufende DMS- und Mail-Testinstanzen (keine Referenzdatensatz-Fixtures) nachgewiesen:
RET-05-Registrierung beider Instanzen, Suchtreffer aus beiden Modulen im Export,
prüfsummengesicherte Übereinstimmung nach Entpacken, Reproduzierbarkeit über zwei
In-Test-Läufe UND zwei unabhängige Prozessaufrufe hinweg. Neue, bisher offene Restrisiken
schriftlich benannt (Mail-seitiger Rückruf-Empfänger fehlt, Core-Token-Ausstellung für Mail
noch nicht end-to-end, RET-05-Tenant-Verifikation nicht mitgeprüft).
+190
View File
@@ -0,0 +1,190 @@
// CMP-08 Ende-zu-Ende-Nachweis: ediscovery.Search/Export real gegen
// laufende, echte DMS-/Mail-Testinstanzen (nicht gegen Referenzdatensatz-
// Fixtures wie QA-03), ueber den ediscoveryhttp-Adapter, Instanzen vorher
// real ueber RET-05 (moduleadapter.Register) registriert. Laeuft nur, wenn
// die CMP08_*-Umgebungsvariablen gesetzt sind (siehe
// scripts/cmp08-ediscovery-e2e.sh) - gleiche Konvention wie
// mail/internal/mailapi/e2e_test.go.
package ediscoveryhttp
import (
"archive/zip"
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"os"
"testing"
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/ediscovery"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/moduleadapter"
)
type cmp08Env struct {
dmsBaseURL, dmsCaller, dmsTenant string
mailBaseURL, mailToken, mailTenant string
term, tenantDSN string
}
func loadCMP08Env(t *testing.T) cmp08Env {
t.Helper()
e := cmp08Env{
dmsBaseURL: os.Getenv("CMP08_DMS_BASE_URL"),
dmsCaller: os.Getenv("CMP08_DMS_CALLER_ID"),
dmsTenant: os.Getenv("CMP08_DMS_TENANT_SLUG"),
mailBaseURL: os.Getenv("CMP08_MAIL_BASE_URL"),
mailToken: os.Getenv("CMP08_MAIL_TOKEN"),
mailTenant: os.Getenv("CMP08_MAIL_TENANT_SLUG"),
term: os.Getenv("CMP08_SEARCH_TERM"),
tenantDSN: os.Getenv("TEST_TENANT_DSN"),
}
if e.dmsBaseURL == "" || e.mailBaseURL == "" || e.tenantDSN == "" || e.term == "" {
t.Skip("CMP08_*/TEST_TENANT_DSN nicht gesetzt, Ende-zu-Ende-Nachweis uebersprungen")
}
return e
}
func requireRegistrationPool(t *testing.T, dsn string) *pgxpool.Pool {
t.Helper()
ctx := context.Background()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatalf("pool: %v", err)
}
t.Cleanup(pool.Close)
if _, err := pool.Exec(ctx, `
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE IF NOT EXISTS module_registrations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_slug TEXT NOT NULL, module_name TEXT NOT NULL,
object_type TEXT NOT NULL, retention_class TEXT NOT NULL, callback_url TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (module_name, object_type)
);
`); err != nil {
t.Fatalf("schema: %v", err)
}
t.Cleanup(func() { _, _ = pool.Exec(context.Background(), `TRUNCATE module_registrations`) })
return pool
}
// manifestChecksums liest ein ediscovery.Export-ZIP ein, prueft die
// manifest.json-Pruefsumme jeder Datei gegen den tatsaechlich entpackten
// Inhalt (Akzeptanzkriterium 2) und liefert die Pruefsummen, indiziert
// ueber modultyp+referenz statt Listenposition (Suchtreffer-Reihenfolge
// ist zwischen zwei Laeufen nicht garantiert stabil).
func manifestChecksums(t *testing.T, zipBytes []byte) map[string]string {
t.Helper()
zr, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
if err != nil {
t.Fatalf("zip lesen: %v", err)
}
var manifest []ediscovery.ManifestEntry
files := map[string][]byte{}
for _, f := range zr.File {
rc, err := f.Open()
if err != nil {
t.Fatalf("zip-eintrag %q oeffnen: %v", f.Name, err)
}
content, err := io.ReadAll(rc)
_ = rc.Close()
if err != nil {
t.Fatalf("zip-eintrag %q lesen: %v", f.Name, err)
}
if f.Name == "manifest.json" {
if err := json.Unmarshal(content, &manifest); err != nil {
t.Fatalf("manifest dekodieren: %v", err)
}
continue
}
files[f.Name] = content
}
out := map[string]string{}
for _, m := range manifest {
path := m.ModuleType + "/" + m.Filename
content, ok := files[path]
if !ok {
t.Fatalf("manifest verweist auf fehlende datei %q", path)
}
sum := sha256.Sum256(content)
if hex.EncodeToString(sum[:]) != m.ChecksumSHA256 {
t.Fatalf("pruefsumme stimmt nicht fuer %q", path)
}
out[m.ModuleType+"/"+m.Reference] = m.ChecksumSHA256
}
return out
}
func runSearchAndExport(t *testing.T, ctx context.Context, adapters []ediscovery.ModuleAdapter, byType map[string]ediscovery.ModuleAdapter, term string) map[string]string {
t.Helper()
hits, errs := ediscovery.Search(ctx, nil, "cmp08-ediscovery", "cmp08-pruefperson", term, adapters)
if len(errs) != 0 {
t.Fatalf("suche gegen reale instanzen fehlgeschlagen: %v", errs)
}
seen := map[string]bool{}
for _, h := range hits {
seen[h.ModuleType] = true
}
if !seen[ModuleTypeDMSDocument] {
t.Fatalf("keine treffer aus dms_document in: %+v", hits)
}
if !seen[ModuleTypeMailMessage] {
t.Fatalf("keine treffer aus mail_message in: %+v", hits)
}
zipBytes, err := ediscovery.Export(ctx, nil, "cmp08-ediscovery", "cmp08-pruefperson", byType, hits)
if err != nil {
t.Fatalf("export fehlgeschlagen: %v", err)
}
return manifestChecksums(t, zipBytes)
}
// TestEndToEnd_CMP08_RealDMSAndMailInstances deckt alle drei Akzeptanz-
// kriterien ab: Export gegen echte, ueber RET-05 registrierte DMS-/
// Mail-Testinstanzen (1), Treffer aus beiden Modulen pruefsummengesichert
// im Export (2), Pruefsummen bei wiederholtem Testlauf identisch (3).
func TestEndToEnd_CMP08_RealDMSAndMailInstances(t *testing.T) {
e := loadCMP08Env(t)
ctx := context.Background()
pool := requireRegistrationPool(t, e.tenantDSN)
if _, err := moduleadapter.Register(ctx, pool, e.dmsTenant, "dms", ModuleTypeDMSDocument, "klasse-ediscovery-test", e.dmsBaseURL+"/retention/destroy-callback"); err != nil {
t.Fatalf("dms via ret-05 registrieren: %v", err)
}
if _, err := moduleadapter.Register(ctx, pool, e.mailTenant, "mail", ModuleTypeMailMessage, "klasse-ediscovery-test", e.mailBaseURL+"/api/v1/mail/messages"); err != nil {
t.Fatalf("mail via ret-05 registrieren: %v", err)
}
regs, err := moduleadapter.ListRegistrations(ctx, pool)
if err != nil {
t.Fatalf("registrierungen lesen: %v", err)
}
if len(regs) != 2 {
t.Fatalf("erwartete 2 ret-05-registrierungen (dms+mail), habe %d: %+v", len(regs), regs)
}
dmsAdapter := NewDMSAdapter(e.dmsBaseURL, e.dmsCaller, nil)
mailAdapter := NewMailAdapter(e.mailBaseURL, e.mailToken, nil)
adapters := []ediscovery.ModuleAdapter{dmsAdapter, mailAdapter}
byType := map[string]ediscovery.ModuleAdapter{ModuleTypeDMSDocument: dmsAdapter, ModuleTypeMailMessage: mailAdapter}
run1 := runSearchAndExport(t, ctx, adapters, byType, e.term)
run2 := runSearchAndExport(t, ctx, adapters, byType, e.term)
if len(run1) == 0 {
t.Fatal("erster lauf lieferte keine pruefsummen")
}
if len(run1) != len(run2) {
t.Fatalf("unterschiedliche trefferzahl zwischen den laeufen: %d vs %d", len(run1), len(run2))
}
for key, sum1 := range run1 {
sum2, ok := run2[key]
if !ok {
t.Fatalf("treffer %q im zweiten lauf nicht mehr vorhanden", key)
}
if sum1 != sum2 {
t.Fatalf("pruefsumme fuer %q unterscheidet sich zwischen den laeufen: %s vs %s", key, sum1, sum2)
}
}
}
@@ -0,0 +1,165 @@
// Package ediscoveryhttp bindet archive/internal/ediscovery.ModuleAdapter
// ueber HTTP an die echten DMS-/Mail-REST-Endpunkte (gleiches Muster wie
// kekclient/rbacclient) - der in CMP-03/QA-03 offen benannte Restrisiko-Punkt
// ("ediscovery.ModuleAdapter wird in Produktion ueber HTTP gebunden, aber nie
// real getestet"), geschlossen durch CMP-08.
package ediscoveryhttp
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/ediscovery"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpclient"
)
const ModuleTypeDMSDocument = "dms_document"
const ModuleTypeMailMessage = "mail_message"
// DMSAdapter ruft DMS GET /search und GET /documents/{id}/content auf
// (dms/internal/searchapi bzw. dms/internal/documentapi).
type DMSAdapter struct {
baseURL string
caller string
http *http.Client
}
func NewDMSAdapter(baseURL, caller string, client *http.Client) *DMSAdapter {
if client == nil {
client = httpclient.Default()
}
return &DMSAdapter{baseURL: baseURL, caller: caller, http: client}
}
func (a *DMSAdapter) ModuleType() string { return ModuleTypeDMSDocument }
type dmsHit struct {
DocumentID string `json:"document_id"`
Title string `json:"title"`
}
type dmsSearchResponse struct {
Hits []dmsHit `json:"hits"`
}
func (a *DMSAdapter) Search(ctx context.Context, criteria string) ([]ediscovery.ObjectRef, error) {
q := url.Values{"q": {criteria}, "caller": {a.caller}, "limit": {"100"}}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL+"/search?"+q.Encode(), nil)
if err != nil {
return nil, fmt.Errorf("ediscoveryhttp(dms): anfrage bauen: %w", err)
}
resp, err := a.http.Do(req)
if err != nil {
return nil, fmt.Errorf("ediscoveryhttp(dms): suche fehlgeschlagen: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("ediscoveryhttp(dms): suche status %d", resp.StatusCode)
}
var body dmsSearchResponse
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
return nil, fmt.Errorf("ediscoveryhttp(dms): antwort dekodieren: %w", err)
}
refs := make([]ediscovery.ObjectRef, 0, len(body.Hits))
for _, h := range body.Hits {
refs = append(refs, ediscovery.ObjectRef{ModuleType: ModuleTypeDMSDocument, Reference: h.DocumentID, Filename: h.Title})
}
return refs, nil
}
func (a *DMSAdapter) FetchContent(ctx context.Context, reference string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL+"/documents/"+url.PathEscape(reference)+"/content", nil)
if err != nil {
return nil, fmt.Errorf("ediscoveryhttp(dms): anfrage bauen: %w", err)
}
resp, err := a.http.Do(req)
if err != nil {
return nil, fmt.Errorf("ediscoveryhttp(dms): inhalt abrufen: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("ediscoveryhttp(dms): inhalt status %d", resp.StatusCode)
}
return io.ReadAll(io.LimitReader(resp.Body, maxContentBytes))
}
// MailAdapter ruft die Mail-API v1 auf (mail/internal/mailapi), mit einem
// vom aufrufenden Kontext ausgestellten Modul-Token (aud=mail).
type MailAdapter struct {
baseURL string
token string
http *http.Client
}
func NewMailAdapter(baseURL, moduleToken string, client *http.Client) *MailAdapter {
if client == nil {
client = httpclient.Default()
}
return &MailAdapter{baseURL: baseURL, token: moduleToken, http: client}
}
func (a *MailAdapter) ModuleType() string { return ModuleTypeMailMessage }
type mailMessageListItem struct {
MessageID string `json:"messageId"`
Subject string `json:"subject"`
}
type mailListResponse struct {
Messages []mailMessageListItem `json:"messages"`
}
func (a *MailAdapter) authed(ctx context.Context, path string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL+path, nil)
if err != nil {
return nil, fmt.Errorf("ediscoveryhttp(mail): anfrage bauen: %w", err)
}
req.Header.Set("Authorization", "Bearer "+a.token)
resp, err := a.http.Do(req)
if err != nil {
return nil, fmt.Errorf("ediscoveryhttp(mail): anfrage senden: %w", err)
}
return resp, nil
}
func (a *MailAdapter) Search(ctx context.Context, criteria string) ([]ediscovery.ObjectRef, error) {
resp, err := a.authed(ctx, "/api/v1/mail/messages?q="+url.QueryEscape(criteria))
if err != nil {
return nil, err
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("ediscoveryhttp(mail): suche status %d", resp.StatusCode)
}
var body mailListResponse
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
return nil, fmt.Errorf("ediscoveryhttp(mail): antwort dekodieren: %w", err)
}
refs := make([]ediscovery.ObjectRef, 0, len(body.Messages))
for _, m := range body.Messages {
refs = append(refs, ediscovery.ObjectRef{ModuleType: ModuleTypeMailMessage, Reference: m.MessageID, Filename: m.MessageID + ".txt"})
}
return refs, nil
}
// FetchContent liefert die rohen Antwort-Bytes von GET
// /messages/{id} (Betreff+Text) als Exportinhalt - Mail-Nachrichten haben
// kein "Original-Binaerformat" wie DMS-Dokumente, der strukturierte
// JSON-Datensatz IST der Originalinhalt dieses Treffers.
func (a *MailAdapter) FetchContent(ctx context.Context, reference string) ([]byte, error) {
resp, err := a.authed(ctx, "/api/v1/mail/messages/"+url.PathEscape(reference))
if err != nil {
return nil, err
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("ediscoveryhttp(mail): inhalt status %d", resp.StatusCode)
}
return io.ReadAll(io.LimitReader(resp.Body, maxContentBytes))
}
const maxContentBytes = 50 << 20