import { useEffect, useMemo, useRef, useState } from "react"; import { ChevronRight, Home, Mail, Paperclip, RefreshCw } from "lucide-react"; import { Button, DismissibleAlert, ExplorerTree, LoadingIndicator, MessageDisplayPanel, formatDateTime, type ApiSettings } from "@govoplan/core-webui"; import { getMailboxMessage, listMailboxFolders, listMailboxMessages, listMailServerProfiles, type MailImapFolderResponse, type MailMailboxMessageDetail, type MailMailboxMessageSummary, type MailServerProfile } from "../../api/mail"; type MailFolderNode = { id: string; label: string; folderName: string | null; flags: string[]; children: MailFolderNode[]; }; export default function MailboxPage({ settings }: { settings: ApiSettings }) { const [profiles, setProfiles] = useState([]); const [selectedProfileId, setSelectedProfileId] = useState(""); const [folders, setFolders] = useState([]); const [selectedFolder, setSelectedFolder] = useState("INBOX"); const [foldersLoadedForProfile, setFoldersLoadedForProfile] = useState(""); const [expandedFolders, setExpandedFolders] = useState>(() => new Set()); const [messages, setMessages] = useState([]); const [messageTotalCount, setMessageTotalCount] = useState(null); const [selectedMessage, setSelectedMessage] = useState(null); const [pendingMessageKey, setPendingMessageKey] = useState(""); const [loadingProfiles, setLoadingProfiles] = useState(true); const [loadingFolders, setLoadingFolders] = useState(false); const [loadingMessages, setLoadingMessages] = useState(false); const [loadingMessage, setLoadingMessage] = useState(false); const [error, setError] = useState(""); const folderRequestRef = useRef(0); const messageListRequestRef = useRef(0); const messageDetailRequestRef = useRef(0); const selectedProfile = profiles.find((profile) => profile.id === selectedProfileId) ?? null; const imapProfiles = useMemo(() => profiles.filter((profile) => profile.is_active && profile.imap), [profiles]); const folderTree = useMemo(() => buildMailboxFolderTree(folders), [folders]); const selectedFolderNodeId = useMemo(() => findFolderNodeId(folderTree, selectedFolder) ?? "", [folderTree, selectedFolder]); const shellBusy = loadingProfiles || loadingFolders || loadingMessages; const noImapProfiles = !loadingProfiles && imapProfiles.length === 0; const foldersReady = Boolean(selectedProfileId) && foldersLoadedForProfile === selectedProfileId; const selectedMessageKey = selectedMessage ? mailboxMessageKey(selectedMessage.folder || selectedFolder, selectedMessage.uid) : pendingMessageKey; const messageCountLabel = messageListCountLabel(messages.length, messageTotalCount, loadingMessages, foldersReady); const folderEmptyText = noImapProfiles ? "No IMAP-enabled mail profiles." : loadingFolders ? "Loading folders..." : "No folders available."; const messageEmptyText = !selectedProfileId ? "Select an IMAP profile." : !foldersReady || loadingMessages ? "Loading messages..." : "No messages in this folder."; const previewEmptyText = loadingMessage ? "Loading message..." : "Select a message to inspect its content."; const loadingLabel = loadingProfiles ? "Loading mail profiles..." : loadingFolders ? "Loading folders..." : loadingMessages ? "Loading messages..." : "Loading message..."; useEffect(() => { void loadProfiles(); }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]); useEffect(() => { if (selectedProfileId) void loadFolders(selectedProfileId); }, [selectedProfileId]); useEffect(() => { if (selectedProfileId && selectedFolder && foldersReady) void loadMessages(selectedProfileId, selectedFolder); }, [foldersReady, selectedProfileId, selectedFolder]); useEffect(() => { const ancestorIds = folderAncestorIds(folderTree, selectedFolder); if (ancestorIds.length === 0) return; setExpandedFolders((current) => { const next = new Set(current); ancestorIds.forEach((id) => next.add(id)); return next; }); }, [folderTree, selectedFolder]); async function loadProfiles() { setLoadingProfiles(true); setError(""); try { const loaded = await listMailServerProfiles(settings); const usable = loaded.filter((profile) => profile.is_active && profile.imap); setProfiles(loaded); setSelectedProfileId((current) => current && usable.some((profile) => profile.id === current) ? current : usable[0]?.id ?? ""); if (usable.length === 0) { setFolders([]); setFoldersLoadedForProfile(""); setMessages([]); setMessageTotalCount(null); setSelectedMessage(null); setPendingMessageKey(""); } } catch (err) { setError(errorText(err)); } finally { setLoadingProfiles(false); } } async function loadFolders(profileId = selectedProfileId) { if (!profileId) return; const requestId = ++folderRequestRef.current; messageListRequestRef.current += 1; messageDetailRequestRef.current += 1; setLoadingFolders(true); setFoldersLoadedForProfile(""); setMessageTotalCount(null); setPendingMessageKey(""); setError(""); try { const response = await listMailboxFolders(settings, profileId); if (requestId !== folderRequestRef.current) return; if (!response.ok) throw new Error(response.message || "Mailbox folders could not be loaded."); const loaded = response.folders?.length ? response.folders : [{ name: "INBOX", flags: [] }]; setFolders(loaded); setExpandedFolders(new Set()); let nextFolder = selectedFolder; if (!nextFolder || !loaded.some((folder) => folder.name === nextFolder)) { nextFolder = loaded.some((folder) => folder.name === "INBOX") ? "INBOX" : response.detected_sent_folder || loaded[0]?.name || "INBOX"; } setSelectedFolder(nextFolder); setFoldersLoadedForProfile(profileId); } catch (err) { if (requestId !== folderRequestRef.current) return; setError(errorText(err)); setFolders([]); setFoldersLoadedForProfile(""); setMessages([]); setMessageTotalCount(null); setSelectedMessage(null); setPendingMessageKey(""); } finally { if (requestId === folderRequestRef.current) setLoadingFolders(false); } } async function loadMessages(profileId = selectedProfileId, folder = selectedFolder) { if (!profileId || !folder) return; const requestId = ++messageListRequestRef.current; messageDetailRequestRef.current += 1; setLoadingMessages(true); setMessageTotalCount(null); setSelectedMessage(null); setPendingMessageKey(""); setError(""); try { const response = await listMailboxMessages(settings, profileId, folder, 50); if (requestId !== messageListRequestRef.current) return; const loaded = response.messages ?? []; setMessages(loaded); setMessageTotalCount(response.total_count ?? loaded.length); setSelectedMessage(null); } catch (err) { if (requestId !== messageListRequestRef.current) return; setError(errorText(err)); setMessages([]); setMessageTotalCount(null); setSelectedMessage(null); setPendingMessageKey(""); } finally { if (requestId === messageListRequestRef.current) setLoadingMessages(false); } } async function openMessage(message: MailMailboxMessageSummary) { if (!selectedProfileId) return; const folderName = message.folder || selectedFolder; const requestId = ++messageDetailRequestRef.current; setPendingMessageKey(mailboxMessageKey(folderName, message.uid)); setSelectedMessage(null); setLoadingMessage(true); setError(""); try { const response = await getMailboxMessage(settings, selectedProfileId, folderName, message.uid); if (requestId !== messageDetailRequestRef.current) return; setSelectedMessage(response.message); setPendingMessageKey(""); } catch (err) { if (requestId !== messageDetailRequestRef.current) return; setError(errorText(err)); setPendingMessageKey(""); } finally { if (requestId === messageDetailRequestRef.current) setLoadingMessage(false); } } function selectProfile(profileId: string) { folderRequestRef.current += 1; messageListRequestRef.current += 1; messageDetailRequestRef.current += 1; setSelectedProfileId(profileId); setFolders([]); setFoldersLoadedForProfile(""); setMessages([]); setMessageTotalCount(null); setSelectedMessage(null); setPendingMessageKey(""); setExpandedFolders(new Set()); setLoadingFolders(false); setLoadingMessages(false); setLoadingMessage(false); } function openFolderNode(node: MailFolderNode) { if (node.folderName) { if (node.folderName === selectedFolder && foldersReady) void loadMessages(selectedProfileId, node.folderName); else setSelectedFolder(node.folderName); return; } toggleFolderNode(node); } function toggleFolderNode(node: MailFolderNode) { setExpandedFolders((current) => { const next = new Set(current); if (next.has(node.id)) next.delete(node.id); else next.add(node.id); return next; }); } return (
{error && {error}}
{selectedProfile?.imap ? transportLabel(selectedProfile) : "No IMAP profile selected"}
{messageCountLabel} {selectedFolder} {shellBusy && Working...} {loadingMessage && Loading preview...}
Subject Date Size
{messages.length === 0 &&
{messageEmptyText}
} {messages.map((message) => { const key = mailboxMessageKey(message.folder || selectedFolder, message.uid); const selected = key === selectedMessageKey; const loadingSelected = loadingMessage && key === pendingMessageKey; return (
void openMessage(message)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); void openMessage(message); } }} >
{formatDateTime(message.date, { fallback: "-" })} {message.attachment_count ? : null} {formatBytes(message.size_bytes)}
); })}
Preview
({ filename: attachment.filename, contentType: attachment.content_type, sizeBytes: attachment.size_bytes }))} emptyText={previewEmptyText} />
{shellBusy && (
{loadingLabel}
)}
); } function buildMailboxFolderTree(folders: MailImapFolderResponse[]): MailFolderNode[] { const roots: MailFolderNode[] = []; const byId = new Map(); function ensureNode(parts: string[], index: number): MailFolderNode | null { if (index < 0 || index >= parts.length) return null; const id = parts.slice(0, index + 1).join("/"); const existing = byId.get(id); if (existing) return existing; const node: MailFolderNode = { id, label: parts[index], folderName: null, flags: [], children: [] }; byId.set(id, node); if (index === 0) { roots.push(node); } else { ensureNode(parts, index - 1)?.children.push(node); } return node; } for (const folder of folders) { const parts = folderParts(folder.name); const node = ensureNode(parts, parts.length - 1); if (node) { node.folderName = folder.name; node.flags = folder.flags ?? []; } } sortFolderNodes(roots); return roots; } function folderParts(folderName: string): string[] { const normalized = folderName.trim(); if (!normalized) return []; if (normalized.includes("/")) return normalized.split("/").filter(Boolean); if (/^INBOX[.]/i.test(normalized)) return ["INBOX", ...normalized.slice(6).split(".").filter(Boolean)]; return [normalized]; } function sortFolderNodes(nodes: MailFolderNode[]) { nodes.sort((left, right) => folderSortKey(left.label).localeCompare(folderSortKey(right.label))); nodes.forEach((node) => sortFolderNodes(node.children)); } function folderSortKey(label: string): string { return label.toUpperCase() === "INBOX" ? "0" : `1:${label.toLocaleLowerCase()}`; } function findFolderNodeId(nodes: MailFolderNode[], folderName: string): string | null { for (const node of nodes) { if (node.folderName === folderName) return node.id; const child = findFolderNodeId(node.children, folderName); if (child) return child; } return null; } function folderAncestorIds(nodes: MailFolderNode[], folderName: string, parents: string[] = []): string[] { for (const node of nodes) { const path = [...parents, node.id]; if (node.folderName === folderName) return path; const child = folderAncestorIds(node.children, folderName, path); if (child.length > 0) return child; } return []; } function mailboxMessageKey(folder: string, uid: string): string { return `${folder || "INBOX"}::${uid}`; } function messageListCountLabel(visibleCount: number, totalCount: number | null, loading: boolean, ready: boolean): string { if (!ready) return "No folder loaded"; if (loading) return "Loading messages"; if (totalCount !== null && totalCount > visibleCount) return `${visibleCount} of ${totalCount} messages`; const count = totalCount ?? visibleCount; return `${count} message${count === 1 ? "" : "s"}`; } function folderFlagLabel(flags: string[]): string { const labels = flags.map(displayFolderFlag).filter((label): label is string => Boolean(label)); return Array.from(new Set(labels)).join(" "); } function displayFolderFlag(flag: string): string | null { const normalized = flag.replace(/^\\/, "").toLowerCase(); switch (normalized) { case "sent": return "Sent"; case "drafts": return "Drafts"; case "trash": return "Trash"; case "archive": return "Archive"; case "junk": return "Junk"; case "flagged": return "Flagged"; case "haschildren": case "hasnochildren": case "noselect": return null; default: return null; } } function transportLabel(profile: MailServerProfile): string { const imap = profile.imap; if (!imap?.host) return "IMAP not configured"; return `${imap.host}:${imap.port ?? "?"} ${imap.security ?? ""}`.trim(); } function formatBytes(value?: number | null): string { if (!value) return "-"; if (value < 1024) return `${value} B`; if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; return `${(value / 1024 / 1024).toFixed(1)} MB`; } function errorText(error: unknown): string { return error instanceof Error ? error.message : String(error); }