feat(PROJ-26,PROJ-38): IMAP LDAP-Auth + Mail-Threading

This commit is contained in:
sysops
2026-04-05 20:17:41 +02:00
parent 956b5b6d5f
commit b252172cc7
11 changed files with 382 additions and 15 deletions
+50 -1
View File
@@ -4,11 +4,13 @@ import { use, useEffect, useRef, useState } from "react";
import Link from "next/link";
import {
getMail,
getThread,
downloadMailAttachment,
downloadMailRaw,
exportMailPDF,
type MailDetail,
type MailAttachment,
type ThreadMail,
} from "@/lib/api";
import { useAuth } from "@/hooks/useAuth";
import { Navbar } from "@/components/navbar";
@@ -255,11 +257,20 @@ export default function MailViewPage({
const [loading, setLoading] = useState(true);
const [downloading, setDownloading] = useState(false);
const [pdfLoading, setPdfLoading] = useState(false);
const [thread, setThread] = useState<ThreadMail[] | null>(null);
const [threadOpen, setThreadOpen] = useState(false);
useEffect(() => {
if (!user) return;
getMail(id)
.then(setMail)
.then((m) => {
setMail(m);
if (m.thread_id) {
getThread(m.thread_id).then((t) => {
if (t.total > 1) setThread(t.mails);
}).catch(() => {});
}
})
.catch((e) =>
setError(e instanceof Error ? e.message : "Unbekannter Fehler")
)
@@ -392,6 +403,44 @@ export default function MailViewPage({
</CardContent>
</Card>
)}
{/* Thread panel */}
{thread && thread.length > 1 && (
<Card>
<CardHeader className="pb-3">
<button
onClick={() => setThreadOpen((v) => !v)}
className="flex items-center gap-2 text-sm font-medium hover:text-foreground text-muted-foreground"
>
<svg className={`w-4 h-4 transition-transform ${threadOpen ? "rotate-90" : ""}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
Konversation ({thread.length} Mails)
</button>
</CardHeader>
{threadOpen && (
<>
<Separator />
<CardContent className="pt-3 space-y-1">
{thread.map((m) => (
<Link
key={m.id}
href={`/mail/${m.id}`}
className={`flex items-center justify-between gap-3 rounded-md px-3 py-2 text-sm hover:bg-muted transition-colors ${m.id === id ? "bg-muted font-medium" : ""}`}
>
<span className="min-w-0 flex-1 truncate">
{m.subject || "(kein Betreff)"}
</span>
<span className="shrink-0 text-xs text-muted-foreground">
{m.date ? new Date(m.date).toLocaleDateString("de-DE") : ""}
</span>
</Link>
))}
</CardContent>
</>
)}
</Card>
)}
</>
)}
</>)}
+8 -1
View File
@@ -409,7 +409,14 @@ export default function SearchPage() {
: "-"}
</TableCell>
<TableCell className="max-w-[14rem] truncate text-sm">{hit.from || "-"}</TableCell>
<TableCell className="font-medium">{hit.subject || "(kein Betreff)"}</TableCell>
<TableCell className="font-medium">
<span>{hit.subject || "(kein Betreff)"}</span>
{hit.thread_size && hit.thread_size > 1 && (
<span className="ml-2 inline-flex items-center rounded-full bg-muted px-1.5 py-0.5 text-xs text-muted-foreground font-normal">
{hit.thread_size}
</span>
)}
</TableCell>
<TableCell className="max-w-[12rem] truncate text-sm text-muted-foreground">{hit.to || "-"}</TableCell>
<TableCell className="text-center text-sm">
{hit.has_attachments ? "📎" : ""}
+24
View File
@@ -11,6 +11,23 @@ export interface SearchHit {
date?: string;
size?: number;
has_attachments?: boolean;
thread_id?: string;
thread_size?: number;
}
export interface ThreadMail {
id: string;
from?: string;
to?: string;
subject: string;
date?: string;
size: number;
}
export interface ThreadResponse {
thread_id: string;
total: number;
mails: ThreadMail[];
}
export interface SearchResponse {
@@ -39,6 +56,7 @@ export interface MailDetail {
attachments: MailAttachment[];
verify_ok: boolean | null;
verified_at: string | null;
thread_id?: string;
}
export interface ImapFolder {
@@ -283,6 +301,12 @@ export async function getPop3Progress(id: number): Promise<Pop3Account> {
return request<Pop3Account>(`/api/pop3/${id}/progress`);
}
// ── Thread ────────────────────────────────────────────────────────────────────
export async function getThread(threadID: string): Promise<ThreadResponse> {
return request<ThreadResponse>(`/api/threads/${encodeURIComponent(threadID)}`);
}
// ── Export ────────────────────────────────────────────────────────────────────
export async function exportMailPDF(id: string): Promise<{ blob: Blob; filename: string }> {