feat(PROJ-26,PROJ-38): IMAP LDAP-Auth + Mail-Threading
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>)}
|
||||
|
||||
@@ -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 ? "📎" : ""}
|
||||
|
||||
@@ -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 }> {
|
||||
|
||||
Reference in New Issue
Block a user