Files
nexarch/web/mail-postfach/lib/api.ts
Claude Sonnet 5andClaude Sonnet 5 deed1b1af8
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Mail-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
UX-02: e-mail-detailansicht-thread-darstellung
Detailansicht fuer web/mail-postfach (UX-01-App erweitert, nicht neu
gebaut): Header (Von/Betreff/Ordner/Datum/Nachricht-ID), Nachrichtentext,
Anhangsliste mit Download (Dateiname nicht verfuegbar -- System
persistiert keinen Originalnamen, daher Anhang N + aus dem
Content-Type geratene Endung). Thread-Gruppierung ueber normalisierten
Betreff (keine In-Reply-To/References-Header im System), Navigation
zwischen Thread-Nachrichten als reiner Komponentenzustand derselben
Seite (kein Routenwechsel -- Ordner-/Suchkontext bleibt dadurch
strukturell erhalten, Akzeptanzkriterium 3).

UX-03 (eigener offener Branch, eigenes Frontend web/mail-search) wird
NICHT wiederverwendet -- getrennte Apps ohne Komponentenfreigabe, siehe
UX-02-PRUEFPROTOKOLL.md fuer die Abgrenzung und die Begruendung der
Backend-Erweiterung.

Tests: lib/threading, lib/threadNavigation, lib/attachmentNaming,
components/MessageDetail (Komponententest je Akzeptanzkriterium),
__tests__/a11y (jest-axe, Pruefung 3), e2e/ux02 gegen die echte
mailpostfach-fixture (Pruefungen 1+2, Mandantentrennung). Bestehender
e2e/ux01-Satz bleibt unveraendert gruen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-16 07:18:57 +00:00

99 lines
3.4 KiB
TypeScript

// UX-01: dünner Client für mail/internal/mailapi (INT-01, bereits
// fertig) — keine eigene Sortier-/Filterlogik, die läuft in
// lib/sorting.ts rein clientseitig auf der bereits mandantengebundenen
// Antwort weiter.
import { apiFetch, statusMessage } from "./http";
export interface MailboxDTO {
name: string;
messageCount: number;
}
export interface MessageListItemDTO {
messageId: string;
subject: string;
sender: string;
mailbox: string;
sentAt: number;
}
// Fetcher erlaubt es Tests, einen echten Bearer-Token direkt gegen eine
// echte mailapi-Instanz zu senden, ohne den vollen Core-Anmeldefluss
// (lib/http.ts) nachzubauen — der Tenant-Isolationscode läuft dabei
// unverändert auf dem echten Server, nur die Token-Beschaffung ist
// ausgetauscht.
export type Fetcher = (input: string, init?: RequestInit) => Promise<Response>;
function apiBase(): string {
const base = process.env.NEXT_PUBLIC_MAIL_API_URL;
if (!base) {
throw new Error("NEXT_PUBLIC_MAIL_API_URL ist nicht gesetzt (Umgebungsvariable erforderlich)");
}
return base.replace(/\/$/, "");
}
async function get<T>(path: string, fetcher: Fetcher): Promise<T> {
const res = await fetcher(`${apiBase()}${path}`);
if (!res.ok) {
throw new Error(statusMessage(res, "Anfrage fehlgeschlagen"));
}
return res.json();
}
export async function fetchMailboxes(fetcher: Fetcher = apiFetch): Promise<MailboxDTO[]> {
const body = await get<{ mailboxes: MailboxDTO[] }>("/mailboxes", fetcher);
return body.mailboxes;
}
export interface MessagesQuery {
mailbox?: string;
q?: string;
}
export async function fetchMessages(query: MessagesQuery = {}, fetcher: Fetcher = apiFetch): Promise<MessageListItemDTO[]> {
const params = new URLSearchParams();
if (query.mailbox) params.set("mailbox", query.mailbox);
if (query.q) params.set("q", query.q);
const qs = params.toString() ? `?${params.toString()}` : "";
const body = await get<{ messages: MessageListItemDTO[] }>(`/messages${qs}`, fetcher);
return body.messages;
}
// UX-02: Detailansicht. Felder entsprechen bereits den "relevanten
// Headern" (Von/Betreff/Ordner/Datum/Nachricht-ID) — mailapi hat keine
// To/Cc-Felder, weil der Ingestion-Pfad sie nirgends persistiert.
export interface MessageDetailDTO {
messageId: string;
subject: string;
body: string;
sender: string;
mailbox: string;
sentAt: number;
}
export async function fetchMessageDetail(messageId: string, fetcher: Fetcher = apiFetch): Promise<MessageDetailDTO> {
return get<MessageDetailDTO>(`/messages/${encodeURIComponent(messageId)}`, fetcher);
}
// AttachmentDTO hat bewusst keinen Dateinamen -- die API liefert keinen
// (siehe mailapi.attachmentListItem), nur Index/ContentType/Size aus
// den tatsaechlich gespeicherten Bytes.
export interface AttachmentDTO {
index: number;
contentType: string;
size: number;
}
export async function fetchAttachments(messageId: string, fetcher: Fetcher = apiFetch): Promise<AttachmentDTO[]> {
const body = await get<{ attachments: AttachmentDTO[] }>(`/messages/${encodeURIComponent(messageId)}/attachments`, fetcher);
return body.attachments;
}
export async function fetchAttachmentContent(messageId: string, index: number, fetcher: Fetcher = apiFetch): Promise<Blob> {
const res = await fetcher(`${apiBase()}/messages/${encodeURIComponent(messageId)}/attachments/${index}`);
if (!res.ok) {
throw new Error(statusMessage(res, "Anhang-Abruf fehlgeschlagen"));
}
return res.blob();
}