/** * Mail page — folder tree + mail list + reading pane + compose. */ import React, { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { useToast } from '@/components/ui/Toast'; import { EmptyState } from '@/components/ui/EmptyState'; import { MailFolderTree } from '@/components/mail/MailFolderTree'; import { MailList } from '@/components/mail/MailList'; import { MailDetail } from '@/components/mail/MailDetail'; import { ComposeModal, type ComposeMode } from '@/components/mail/ComposeModal'; import { SharedMailboxSelector } from '@/components/mail/SharedMailboxSelector'; import { MailSearchBar } from '@/components/mail/MailSearchBar'; import { fetchAccounts, fetchFolders, fetchMails, getMail, sendMail, replyMail, forwardMail, updateFlags, createEventFromMail, downloadAttachment, fetchSignatures, searchMails, type MailAccount, type MailFolder, type Mail, type MailSignature, type MailAttachment, type SendMailPayload, type ReplyPayload, type ForwardPayload, type CreateEventFromMailPayload, } from '@/api/mail'; const PAGE_SIZE = 25; export function MailPage() { const { t } = useTranslation(); const toast = useToast(); const [accounts, setAccounts] = useState([]); const [selectedAccountId, setSelectedAccountId] = useState(''); const [folders, setFolders] = useState([]); const [selectedFolderId, setSelectedFolderId] = useState(null); const [mails, setMails] = useState([]); const [mailsTotal, setMailsTotal] = useState(0); const [mailsPage, setMailsPage] = useState(1); const [selectedMail, setSelectedMail] = useState(null); const [loadingMail, setLoadingMail] = useState(false); const [loadingAccounts, setLoadingAccounts] = useState(true); const [loadingFolders, setLoadingFolders] = useState(false); const [loadingMails, setLoadingMails] = useState(false); const [error, setError] = useState(null); const [signatures, setSignatures] = useState([]); const [composeOpen, setComposeOpen] = useState(false); const [composeMode, setComposeMode] = useState('new'); const [replyToMail, setReplyToMail] = useState(null); const [forwardMailState, setForwardMailState] = useState(null); const [downloadingAttachmentId, setDownloadingAttachmentId] = useState(null); const [searchQuery, setSearchQuery] = useState(''); // Load accounts const loadAccounts = useCallback(async () => { setLoadingAccounts(true); try { const accs = await fetchAccounts(); setAccounts(accs); if (accs.length > 0 && !selectedAccountId) { setSelectedAccountId(accs[0].id); } } catch (err) { const msg = err instanceof Error ? err.message : String(err); setError(msg); } setLoadingAccounts(false); }, [selectedAccountId]); // Load signatures const loadSignatures = useCallback(async () => { try { const sigs = await fetchSignatures(); setSignatures(sigs); } catch { // non-critical } }, []); useEffect(() => { loadAccounts(); loadSignatures(); }, [loadAccounts, loadSignatures]); // Load folders when account changes const loadFolders = useCallback(async () => { if (!selectedAccountId) return; setLoadingFolders(true); try { const folderList = await fetchFolders(selectedAccountId); setFolders(folderList); // Auto-select first folder if (folderList.length > 0 && !selectedFolderId) { setSelectedFolderId(folderList[0].id); } } catch (err) { const msg = err instanceof Error ? err.message : String(err); setError(msg); } setLoadingFolders(false); }, [selectedAccountId, selectedFolderId]); useEffect(() => { loadFolders(); }, [loadFolders]); // Load mails when folder or page changes const loadMails = useCallback(async () => { if (!selectedFolderId) return; setLoadingMails(true); try { if (searchQuery.trim()) { const result = await searchMails(searchQuery); setMails(result.mails); setMailsTotal(result.total); } else { const result = await fetchMails(selectedFolderId, mailsPage); setMails(result.mails); setMailsTotal(result.total); } } catch (err) { const msg = err instanceof Error ? err.message : String(err); setError(msg); setMails([]); } setLoadingMails(false); }, [selectedFolderId, mailsPage, searchQuery]); useEffect(() => { loadMails(); }, [loadMails]); // Handle folder selection const handleSelectFolder = useCallback((folderId: string) => { setSelectedFolderId(folderId); setMailsPage(1); setSearchQuery(''); setSelectedMail(null); }, []); // Handle mail selection const handleSelectMail = useCallback(async (mail: Mail) => { setLoadingMail(true); try { const detail = await getMail(mail.id); setSelectedMail(detail); // Mark as seen if not seen if (!detail.is_seen) { await updateFlags(mail.id, { seen: true }); setMails((prev) => prev.map((m) => (m.id === mail.id ? { ...m, is_seen: true } : m))); } } catch (err) { const msg = err instanceof Error ? err.message : String(err); toast.error(msg); setSelectedMail(mail); } setLoadingMail(false); }, [toast]); // Handle compose const handleCompose = useCallback(() => { setComposeMode('new'); setReplyToMail(null); setForwardMailState(null); setComposeOpen(true); }, []); // Handle reply const handleReply = useCallback((mail: Mail) => { setComposeMode('reply'); setReplyToMail(mail); setForwardMailState(null); setComposeOpen(true); }, []); // Handle forward const handleForward = useCallback((mail: Mail) => { setComposeMode('forward'); setForwardMailState(mail); setReplyToMail(null); setComposeOpen(true); }, []); // Handle toggle flag const handleToggleFlag = useCallback(async (mail: Mail) => { try { await updateFlags(mail.id, { flagged: !mail.is_flagged }); setSelectedMail((prev) => prev ? { ...prev, is_flagged: !mail.is_flagged } : prev); setMails((prev) => prev.map((m) => (m.id === mail.id ? { ...m, is_flagged: !mail.is_flagged } : m))); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } }, [toast]); // Handle create event from mail const handleCreateEvent = useCallback(async (mail: Mail) => { const title = mail.subject || t('mail.noSubject'); const now = new Date(); const start = now.toISOString(); const end = new Date(now.getTime() + 60 * 60 * 1000).toISOString(); const payload: CreateEventFromMailPayload = { title, start, end, description: mail.body_text.slice(0, 500), }; try { await createEventFromMail(mail.id, payload); toast.success(t('mail.eventCreated')); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } }, [toast, t]); // Handle attachment download const handleDownloadAttachment = useCallback(async (mailId: string, attachment: MailAttachment) => { setDownloadingAttachmentId(attachment.id); try { const blob = await downloadAttachment(mailId, attachment.id); const url = window.URL.createObjectURL(blob); const a = window.document.createElement('a'); a.href = url; a.download = attachment.filename; window.document.body.appendChild(a); a.click(); window.document.body.removeChild(a); window.URL.revokeObjectURL(url); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } finally { setDownloadingAttachmentId(null); } }, [toast]); // Handle send (compose) const handleSend = useCallback(async (payload: SendMailPayload | ReplyPayload | ForwardPayload, mode: ComposeMode) => { try { if (mode === 'reply' && replyToMail) { await replyMail(replyToMail.id, payload as ReplyPayload); } else if (mode === 'forward' && forwardMailState) { await forwardMail(forwardMailState.id, payload as ForwardPayload); } else { await sendMail(payload as SendMailPayload); } toast.success(t('mail.sent')); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); throw err; } }, [replyToMail, forwardMailState, toast, t]); // Handle search const handleSearch = useCallback((query: string) => { setSearchQuery(query); setMailsPage(1); }, []); // Handle account switch const handleAccountSwitch = useCallback((accountId: string) => { setSelectedAccountId(accountId); setSelectedFolderId(null); setSelectedMail(null); setMails([]); setMailsPage(1); setSearchQuery(''); }, []); if (loadingAccounts) { return (
{t('common.loading')}
); } if (accounts.length === 0) { return (
{t('mail.configureAccount')}} />
); } return (
{/* Top bar */}
{t('mail.settings')}
{error && (

{error}

)} {/* Three-pane layout */}
{/* Folder tree */}

{t('mail.folders')}

{/* Mail list */}
{/* Reading pane */}
setComposeOpen(false)} />
); }