'use client'; import React, { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { useSession } from 'next-auth/react'; import { Mail, Loader2, AlertCircle, MoreVertical, Settings, Plus as PlusIcon, Trash2, Edit, Inbox, Send, Star, Trash, Plus, ChevronLeft, ChevronRight, Search, ChevronDown, Folder, ChevronUp, Reply, Forward, ReplyAll, MoreHorizontal, FolderOpen, X, Paperclip, MessageSquare, Copy, EyeOff, AlertOctagon, Archive, Menu, Check } from 'lucide-react'; import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { ScrollArea } from '@/components/ui/scroll-area'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; import { toast } from '@/components/ui/use-toast'; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu'; // Import components import EmailSidebar from '@/components/email/EmailSidebar'; import EmailList from '@/components/email/EmailList'; import EmailSidebarContent from '@/components/email/EmailSidebarContent'; import EmailDetailView from '@/components/email/EmailDetailView'; import ComposeEmail from '@/components/email/ComposeEmail'; import { DeleteConfirmDialog } from '@/components/email/EmailDialogs'; // Import the custom hooks import { useEmailState } from '@/hooks/use-email-state'; // Import the prefetching function import { prefetchFolderEmails } from '@/lib/services/prefetch-service'; // Import Account type from the reducer import { Account } from '@/lib/reducers/emailReducer'; // Add the missing EmailData import from use-courrier import { EmailData } from '@/hooks/use-courrier'; // Simplified version for this component function SimplifiedLoadingFix() { // In production, don't render anything if (process.env.NODE_ENV === 'production') { return null; } // Simple debugging component return (
Debug: Email app loaded
); } interface EmailWithFlags { id: string; read?: boolean; flags?: { seen?: boolean; }; } interface EmailMessage { id: string; from: { name: string; address: string }[]; to: { name: string; address: string }[]; subject: string; date: Date; flags: { seen: boolean; flagged: boolean; answered: boolean; draft: boolean; deleted: boolean; }; size: number; hasAttachments: boolean; folder: string; contentFetched: boolean; accountId: string; content: { text: string; html: string; }; } interface AccountData { email: string; password: string; host: string; port: number; secure: boolean; display_name: string; smtp_host?: string; smtp_port?: number; smtp_secure?: boolean; } // Define a color palette for account circles const colorPalette = [ 'bg-blue-500', 'bg-green-500', 'bg-red-500', 'bg-yellow-500', 'bg-purple-500', 'bg-pink-500', 'bg-indigo-500', 'bg-teal-500', 'bg-orange-500', 'bg-cyan-500', ]; // Helper function for consistent logging const logEmailOp = (operation: string, details: string, data?: any) => { const timestamp = new Date().toISOString().split('T')[1].substring(0, 12); console.log(`[${timestamp}][EMAIL-APP][${operation}] ${details}`); if (data) { console.log(`[${timestamp}][EMAIL-APP][DATA]`, data); } }; export default function CourrierPage() { const router = useRouter(); const { data: session } = useSession(); // Replace useCourrier with useEmailState const { // State values accounts, selectedAccount, selectedFolders, currentFolder, emails, selectedEmail, selectedEmailIds, isLoading, error, page, totalPages, totalEmails, mailboxes, unreadCountMap, showFolders, // Actions loadEmails, handleEmailSelect, toggleEmailSelection, toggleSelectAll, markEmailAsRead, toggleStarred, changeFolder, deleteEmails, sendEmail, searchEmails, formatEmailForAction, setPage, setEmails, selectAccount, handleLoadMore } = useEmailState(); // UI state (keeping only what's still needed) const [showComposeModal, setShowComposeModal] = useState(false); const [composeType, setComposeType] = useState<'new' | 'reply' | 'reply-all' | 'forward'>('new'); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showLoginNeeded, setShowLoginNeeded] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(true); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [loading, setLoading] = useState(false); const [prefetchStarted, setPrefetchStarted] = useState(false); const [showAddAccountForm, setShowAddAccountForm] = useState(false); // Add state for modals/dialogs const [showEditModal, setShowEditModal] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [accountToEdit, setAccountToEdit] = useState(null); const [accountToDelete, setAccountToDelete] = useState(null); const [newPassword, setNewPassword] = useState(''); const [editLoading, setEditLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false); const [selectedColor, setSelectedColor] = useState(''); // Use the reducer-managed values directly instead of tracked separately const [searchQuery, setSearchQuery] = useState(''); const [unreadCount, setUnreadCount] = useState(0); // Calculate unread count for the selected folder useEffect(() => { if (selectedAccount && selectedAccount.id !== 'loading-account') { const folderCounts = unreadCountMap[selectedAccount.id.toString()]; if (folderCounts) { setUnreadCount(folderCounts[currentFolder] || 0); } else { setUnreadCount(0); } } else { // For 'loading-account', sum up all unread counts for the current folder let totalUnread = 0; Object.values(unreadCountMap).forEach((folderCounts) => { totalUnread += folderCounts[currentFolder] || 0; }); setUnreadCount(totalUnread); } }, [unreadCountMap, selectedAccount, currentFolder]); // Initialize session and start prefetching useEffect(() => { // Flag to prevent multiple initialization attempts let isMounted = true; let retryCount = 0; const MAX_RETRIES = 3; const RETRY_DELAY = 1000; // 1 second const initSession = async () => { try { if (!isMounted) return; logEmailOp('SESSION', 'Initializing email session'); setLoading(true); // First check if Redis is ready before making API calls const redisStatus = await fetch('/api/redis/status') .then(res => res.json()) .catch(() => ({ ready: false })); if (!isMounted) return; // Call the session API to check email credentials and start prefetching logEmailOp('SESSION', 'Fetching session data from API'); const response = await fetch('/api/courrier/session', { credentials: 'include', headers: { 'Content-Type': 'application/json', } }); // Handle 401 Unauthorized with retry logic if (response.status === 401) { if (retryCount < MAX_RETRIES) { retryCount++; console.log(`Session request failed (attempt ${retryCount}/${MAX_RETRIES}), retrying in ${RETRY_DELAY}ms...`); await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); return initSession(); } else { console.error('Max retries reached for session request'); return; } } if (!response.ok) { throw new Error(`Session request failed with status ${response.status}`); } const data = await response.json(); // Log session response console.log('[DEBUG] Session API response details:', { authenticated: data.authenticated, hasEmailCredentials: data.hasEmailCredentials, accountsCount: data.allAccounts?.length || 0 }); // Process accounts if authenticated if (data.authenticated && data.hasEmailCredentials) { setPrefetchStarted(Boolean(data.prefetchStarted)); let updatedAccounts: Account[] = []; // Process multiple accounts if (data.allAccounts && Array.isArray(data.allAccounts) && data.allAccounts.length > 0) { console.log('[DEBUG] Processing multiple accounts:', data.allAccounts.length); data.allAccounts.forEach((account: any) => { // Use exact folders from IMAP const accountFolders = (account.folders && Array.isArray(account.folders)) ? account.folders : []; // Ensure folder names have account prefix const validFolders = accountFolders.map((folder: string) => { if (!folder.includes(':')) { return `${account.id}:${folder}`; } return folder; }); updatedAccounts.push({ id: account.id, name: account.display_name || account.email, email: account.email, color: account.color || colorPalette[(updatedAccounts.length) % colorPalette.length], folders: validFolders }); }); console.log('[DEBUG] Constructed accounts:', updatedAccounts); } else { // Fallback to single account if allAccounts is not available const folderList = (data.mailboxes && data.mailboxes.length > 0) ? data.mailboxes : []; updatedAccounts.push({ id: 'default-account', name: data.displayName || data.email, email: data.email, color: colorPalette[0], folders: folderList }); console.log('[DEBUG] Constructed single fallback account:', updatedAccounts[0]); } // Update accounts state using our reducer actions // First, set the accounts setEmails([]); // Clear any existing emails first // Log current state for debugging console.log('[DEBUG] Current state before setting accounts:', { accounts: accounts?.length || 0, selectedAccount: selectedAccount?.id || 'none', currentFolder: currentFolder || 'none' }); // Use our reducer actions instead of setState setAccounts(updatedAccounts); // Auto-select the first account if available if (updatedAccounts.length > 0) { const firstAccount = updatedAccounts[0]; console.log('[DEBUG] Auto-selecting first account:', firstAccount); // Use our new selectAccount function which handles state atomically // Add a slight delay to ensure the accounts are set first setTimeout(() => { console.log('[DEBUG] Now calling selectAccount'); selectAccount(firstAccount); }, 100); } } else { // User is authenticated but doesn't have email credentials setShowLoginNeeded(true); } } catch (error) { console.error('Error initializing session:', error); } finally { if (isMounted) { setLoading(false); } } }; if (session?.user?.id) { initSession(); } return () => { isMounted = false; }; }, [session?.user?.id, setEmails, selectAccount]); // Helper to get folder icons const getFolderIcon = (folder: string) => { const folderLower = folder.toLowerCase(); if (folderLower.includes('inbox')) { return ; } else if (folderLower.includes('sent')) { return ; } else if (folderLower.includes('trash')) { return ; } else if (folderLower.includes('archive')) { return ; } else if (folderLower.includes('draft')) { return ; } else if (folderLower.includes('spam') || folderLower.includes('junk')) { return ; } else { return ; } }; // Helper to format folder names const formatFolderName = (folder: string) => { // Extract base folder name if prefixed const baseFolderName = folder.includes(':') ? folder.split(':')[1] : folder; return baseFolderName.charAt(0).toUpperCase() + baseFolderName.slice(1).toLowerCase(); }; // Handle actions - replace with useReducer-based functions const handleMailboxChange = (folder: string, accountId?: string) => { // Simply call our new changeFolder function which handles everything atomically setLoading(true); changeFolder(folder, accountId) .finally(() => { setLoading(false); }); }; // Handle account selection - replace with reducer-based function const handleAccountSelect = (account: Account) => { // Add extensive debugging to track the process console.log('[DEBUG] handleAccountSelect called with account:', { id: account.id, email: account.email, folders: account.folders?.length }); // Skip if no valid account provided if (!account || !account.id) { console.error('Invalid account passed to handleAccountSelect'); return; } // Skip if this is already the selected account if (selectedAccount?.id === account.id) { console.log('[DEBUG] Account already selected, skipping'); return; } // Simply call our new selectAccount function which handles everything atomically setLoading(true); // Clear all existing selections first console.log('[DEBUG] Now selecting account through reducer action'); selectAccount(account); // Log what happened console.log('[DEBUG] Account selection completed'); // Give some time for the UI to update setTimeout(() => setLoading(false), 300); }; // Email actions const handleReply = () => { if (!selectedEmail) return; setComposeType('reply'); setShowComposeModal(true); }; const handleReplyAll = () => { if (!selectedEmail) return; setComposeType('reply-all'); setShowComposeModal(true); }; const handleForward = () => { if (!selectedEmail) return; setComposeType('forward'); setShowComposeModal(true); }; const handleComposeNew = () => { setComposeType('new'); setShowComposeModal(true); }; // Handle bulk actions const handleBulkAction = async (action: 'delete' | 'mark-read' | 'mark-unread' | 'archive') => { if (selectedEmailIds.length === 0) return; switch (action) { case 'delete': setShowDeleteConfirm(true); break; case 'mark-read': // Mark all selected emails as read for (const emailId of selectedEmailIds) { await markEmailAsRead(emailId, true); } break; case 'mark-unread': // Mark all selected emails as unread for (const emailId of selectedEmailIds) { await markEmailAsRead(emailId, false); } break; case 'archive': // Archive functionality would be implemented here break; } }; const handleSendEmail = async (emailData: EmailData) => { try { const result = await sendEmail(emailData); if (!result.success) { throw new Error(result.error); } return result; } catch (error) { throw error; } }; const handleDeleteConfirm = async () => { await deleteEmails(selectedEmailIds); setShowDeleteConfirm(false); // Clear selected emails after deletion // Using setEmails will reset the selection state setLoading(true); setPage(1); loadEmails(1, 20, false).finally(() => { // Selection will be cleared by loading new emails setLoading(false); }); }; const handleGoToLogin = () => { router.push('/courrier/login'); }; // Update the accounts from state - fix type issues const setAccounts = (newAccounts: Account[]) => { console.log('[DEBUG] Setting accounts:', newAccounts); // In the previous implementation, we'd dispatch an action // But since we don't have direct access to the reducer's dispatch function, // we need to use the exported actions from our hook // This dispatch function should be made available by our hook const windowWithDispatch = window as any; if (typeof windowWithDispatch.dispatchEmailAction === 'function') { // Use the global dispatch function if available windowWithDispatch.dispatchEmailAction({ type: 'SET_ACCOUNTS', payload: newAccounts }); } else { console.error('Cannot dispatch SET_ACCOUNTS action - no dispatch function available'); // Fallback: Try to directly modify the accounts array if we have access // This isn't ideal but ensures backward compatibility during transition console.log('[DEBUG] Using fallback method to update accounts'); // Our reducer should expose this action const useEmailStateDispatch = windowWithDispatch.__emailStateDispatch; if (typeof useEmailStateDispatch === 'function') { useEmailStateDispatch({ type: 'SET_ACCOUNTS', payload: newAccounts }); } else { console.error('No fallback dispatch method available either'); } } }; return ( <> {/* Main layout */}
{/* Use EmailSidebar component instead of inline sidebar */} { setLoading(true); setPage(1); loadEmails(page, 10, false).finally(() => setLoading(false)); }} onComposeNew={handleComposeNew} onAccountSelect={handleAccountSelect} onShowAddAccountForm={setShowAddAccountForm} onAddAccount={async (formData) => { setLoading(true); console.log('[DEBUG] Add account form submission:', formData); // Pull values from form with proper type handling const formValues = { email: formData.get('email')?.toString() || '', password: formData.get('password')?.toString() || '', host: formData.get('host')?.toString() || '', port: parseInt(formData.get('port')?.toString() || '993'), secure: formData.get('secure') === 'on', display_name: formData.get('display_name')?.toString() || '', smtp_host: formData.get('smtp_host')?.toString() || '', smtp_port: formData.get('smtp_port')?.toString() ? parseInt(formData.get('smtp_port')?.toString() || '587') : undefined, smtp_secure: formData.get('smtp_secure') === 'on' }; // If display_name is empty, use email if (!formValues.display_name) { formValues.display_name = formValues.email; } try { // First test the connection const testResponse = await fetch('/api/courrier/test-connection', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: formValues.email, password: formValues.password, host: formValues.host, port: formValues.port, secure: formValues.secure }) }); const testResult = await testResponse.json(); if (!testResponse.ok) { throw new Error(testResult.error || 'Connection test failed'); } console.log('Connection test successful:', testResult); // Only declare realAccounts once before using for color assignment const realAccounts = accounts.filter(a => a.id !== 'loading-account'); const saveResponse = await fetch('/api/courrier/account', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formValues) }); const saveResult = await saveResponse.json(); if (!saveResponse.ok) { throw new Error(saveResult.error || 'Failed to add account'); } const realAccount = saveResult.account; realAccount.color = colorPalette[realAccounts.length % colorPalette.length]; realAccount.folders = testResult.details.sampleFolders || ['INBOX', 'Sent', 'Drafts', 'Trash']; setAccounts([...accounts, realAccount]); setShowAddAccountForm(false); toast({ title: "Account added successfully", description: `Your email account ${formValues.email} has been added.`, duration: 5000 }); } catch (error) { console.error('Error adding account:', error); toast({ title: "Failed to add account", description: error instanceof Error ? error.message : 'Unknown error', variant: "destructive", duration: 5000 }); } finally { setLoading(false); } }} onEditAccount={async (account) => { try { // Get the latest account data from accounts array const updatedAccount = accounts.find(a => a.id === account.id); if (updatedAccount) { setAccountToEdit(updatedAccount as any); setSelectedColor(updatedAccount.color || ''); setShowEditModal(true); } else { toast({ title: "Error", description: "Could not find account data", variant: "destructive", duration: 3000 }); } } catch (error) { console.error("Error preparing account edit:", error); toast({ title: "Error", description: "Failed to load account settings", variant: "destructive", duration: 3000 }); } }} onDeleteAccount={(account) => { setAccountToDelete(account as any); setShowDeleteDialog(true); }} onSelectEmail={(emailId, accountId, folder) => { if (typeof emailId === 'string') { handleEmailSelect(emailId, accountId || '', folder || currentFolder); } }} {...({} as any)} /> {/* Panel 2: Email List - Always visible */}
{/* Header without search bar or profile */}
{getFolderIcon(currentFolder)} {/* Extract base folder and show email as prefix */} {selectedAccount?.email ? `${selectedAccount.email}: ` : ''} {formatFolderName(currentFolder.includes(':') ? currentFolder.split(':')[1] : currentFolder)}
{/* Buttons removed from here to avoid duplication with the BulkActionsToolbar */}
{/* Email List - Always visible */}
{isLoading ? (

Loading emails...

) : error ? (
Error {error}
) : (
{/* Email List */}
{ const target = e.currentTarget; const { scrollTop, scrollHeight, clientHeight } = target; const distanceToBottom = scrollHeight - scrollTop - clientHeight; const scrollPercentage = (scrollTop + clientHeight) / scrollHeight; // Store last scroll position to detect direction const lastScrollTop = target.dataset.lastScrollTop ? parseInt(target.dataset.lastScrollTop) : 0; const scrollingDown = scrollTop > lastScrollTop; // Update last scroll position target.dataset.lastScrollTop = scrollTop.toString(); // Prevent frequent log spam with a timestamp check const now = Date.now(); const lastLog = parseInt(target.dataset.lastLogTime || '0'); if (now - lastLog > 500) { // Log at most every 500ms console.log(`[DEBUG-WRAPPER-SCROLL] Distance: ${distanceToBottom}px, %: ${Math.round(scrollPercentage * 100)}%, direction: ${scrollingDown ? 'down' : 'up'}, more: ${page < totalPages}, loading: ${isLoading}`); target.dataset.lastLogTime = now.toString(); } // Check throttle to prevent multiple rapid triggers const lastTrigger = parseInt(target.dataset.lastTriggerTime || '0'); const throttleTime = 1000; // 1 second throttle // CRITICAL FIX: Only trigger loading more emails when: // 1. User is scrolling DOWN (not up) // 2. User is EXACTLY at the bottom (distance < 5px) // 3. Not currently loading // 4. More emails exist to load // 5. Not throttled (hasn't triggered in last second) if (scrollingDown && distanceToBottom < 5 && // Much stricter - truly at bottom !isLoading && page < totalPages && now - lastTrigger > throttleTime) { console.log(`[DEBUG-WRAPPER-TRIGGER] *** AT BOTTOM *** Loading more emails`); target.dataset.lastTriggerTime = now.toString(); handleLoadMore(); } }} > {emails.length === 0 ? (

No emails found

{searchQuery ? `No results found for "${searchQuery}"` : `Your ${currentFolder.toLowerCase()} is empty`}

) : ( { // Always use the email's own accountId and folder if available handleEmailSelect( emailId, emailAccountId || selectedAccount?.id || '', emailFolder || currentFolder ); }} onToggleSelect={toggleEmailSelection} onToggleSelectAll={toggleSelectAll} onToggleStarred={toggleStarred} onLoadMore={handleLoadMore} hasMoreEmails={page < totalPages} currentFolder={currentFolder} isLoading={isLoading} totalEmails={emails.length} onBulkAction={handleBulkAction} /> )}
)}
{/* Panel 3: Email Detail - Always visible */}
{/* Content for Panel 3 based on state but always visible */}
{selectedEmail ? ( { handleEmailSelect('', '', ''); // Ensure sidebar stays visible setSidebarOpen(true); }} onReply={handleReply} onReplyAll={handleReplyAll} onForward={handleForward} onToggleStar={() => toggleStarred(selectedEmail.id)} /> ) : (

Select an email to view or

)}
{/* Modals and Dialogs */} setShowDeleteConfirm(false)} /> {/* Compose Email Dialog */} !open && setShowComposeModal(false)}> New Message { try { const result = await sendEmail(emailData); return; } catch (error) { console.error('Error sending email:', error); throw error; } }} onClose={() => setShowComposeModal(false)} accounts={accounts} /> {/* Edit Password Modal */} { if (!open) { setShowEditModal(false); setEditLoading(false); setAccountToEdit(null); setNewPassword(''); setSelectedColor(''); window.location.reload(); } }}> Edit Account Settings
{ e.preventDefault(); if (!accountToEdit) return; setEditLoading(true); try { const formElement = e.target as HTMLFormElement; const displayName = (formElement.querySelector('#display-name') as HTMLInputElement).value; const color = selectedColor; // If password is changed, test the connection first if (newPassword) { try { // First get the account's connection details const accountDetailsRes = await fetch(`/api/courrier/account-details?accountId=${accountToEdit.id}`); if (!accountDetailsRes.ok) { throw new Error('Failed to fetch account connection details'); } const accountDetails = await accountDetailsRes.json(); // Test connection with new password before saving const testResponse = await fetch('/api/courrier/test-connection', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: accountToEdit.email, password: newPassword, // Use the account's connection details from the API host: accountDetails.host, port: accountDetails.port || 993, secure: accountDetails.secure || true }) }); const testResult = await testResponse.json(); if (!testResponse.ok) { throw new Error(testResult.error || 'Connection test failed with new password'); } console.log('Connection test successful with new password'); } catch (error) { console.error('Error testing connection:', error); throw new Error(`Password test failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } // Continue with the update if test passed or no password change const res = await fetch('/api/courrier/account', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ accountId: accountToEdit.id, newPassword: newPassword || undefined, display_name: displayName, color: color }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Failed to update account settings'); toast({ title: 'Account updated', description: 'Account settings updated successfully.' }); setShowEditModal(false); setNewPassword(''); // Update the local account data setAccounts(accounts.map(account => account.id === accountToEdit.id ? {...account, name: displayName, color: color} : account )); // Clear accountToEdit to ensure fresh data on next edit setAccountToEdit(null); // Force a page refresh to reset all UI states window.location.reload(); } catch (err) { toast({ title: 'Error', description: err instanceof Error ? err.message : 'Failed to update account settings', variant: 'destructive' }); } finally { setEditLoading(false); } }}>
setNewPassword(e.target.value)} className="mt-1 bg-white text-gray-800" placeholder="Leave blank to keep current password" disabled={editLoading} />
{colorPalette.map((color, index) => (
setSelectedColor(color)} className="sr-only" />
))}
{/* Delete Account Dialog */} { if (!open) setShowDeleteDialog(false); }}> Delete Account Are you sure you want to delete this account? This action cannot be undone. setShowDeleteDialog(false)}>Cancel ); }