'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, RefreshCw, Menu } 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 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, LoginNeededAlert } from '@/components/email/EmailDialogs'; // Import the custom hook import { useCourrier, EmailData } from '@/hooks/use-courrier'; // Import the prefetching function import { prefetchFolderEmails } from '@/lib/services/prefetch-service'; // 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 Account { id: string; name: string; email: string; color: string; folders?: string[]; } interface EmailWithFlags { id: string; read?: boolean; flags?: { seen?: boolean; }; } export default function CourrierPage() { const router = useRouter(); const { data: session } = useSession(); // Get all the email functionality from the hook const { emails = [], selectedEmail, selectedEmailIds, currentFolder, mailboxes, isLoading, isSending, error, searchQuery, page, totalPages, loadEmails, handleEmailSelect, markEmailAsRead, toggleStarred, sendEmail, deleteEmails, toggleEmailSelection, toggleSelectAll, changeFolder, searchEmails, formatEmailForAction, setPage, } = useCourrier(); // UI state 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 [accountsDropdownOpen, setAccountsDropdownOpen] = useState(true); const [currentView, setCurrentView] = useState('INBOX'); const [unreadCount, setUnreadCount] = useState(0); const [loading, setLoading] = useState(false); const [prefetchStarted, setPrefetchStarted] = useState(false); const [showFolders, setShowFolders] = useState(true); const [showAddAccountForm, setShowAddAccountForm] = useState(false); // Email accounts for the sidebar const [accounts, setAccounts] = useState([ { id: 'all-accounts', name: 'All', email: '', color: 'bg-gray-500' }, { id: 'loading-account', name: 'Loading...', email: '', color: 'bg-blue-500', folders: [] } ]); const [selectedAccount, setSelectedAccount] = useState(null); // Track expanded folders for each account const [expandedAccounts, setExpandedAccounts] = useState>({}); // Update account folders when mailboxes change - update this to maintain account IDs useEffect(() => { console.log('Mailboxes updated:', mailboxes); setAccounts(prev => { const updated = [...prev]; if (updated.length > 1) { // Only update folders, preserve other properties including ID if (updated[1]) { updated[1] = { ...updated[1], folders: mailboxes }; } console.log('Updated accounts with new mailboxes:', updated); } return updated; }); }, [mailboxes]); // Debug accounts state useEffect(() => { console.log('Current accounts state:', accounts); }, [accounts]); // Debug selectedAccount state useEffect(() => { console.log('Selected account changed:', selectedAccount); if (selectedAccount) { console.log('Selected account folders:', selectedAccount.folders); } }, [selectedAccount]); // Add useEffect for debugging useEffect(() => { if (typeof window !== 'undefined') { console.log('[DEBUG] Rendering UI with:', { accountsCount: accounts.length, selectedAccountId: selectedAccount?.id, showFolders, currentFolder }); } }, [accounts, selectedAccount, showFolders, currentFolder]); // Calculate unread count (this would be replaced with actual data in production) useEffect(() => { // Example: counting unread emails in the inbox const unreadInInbox = (emails || []).filter(email => { // Access the 'read' property safely, handling both old and new email formats const emailWithFlags = email as unknown as EmailWithFlags; return (!emailWithFlags.read && emailWithFlags.read !== undefined) || (emailWithFlags.flags && !emailWithFlags.flags.seen) || false; }).filter(email => currentFolder === 'INBOX').length; setUnreadCount(unreadInInbox); }, [emails, currentFolder]); // Ensure accounts section is never empty useEffect(() => { // If accounts array becomes empty (bug), restore default accounts if (!accounts || accounts.length === 0) { console.warn('Accounts array is empty, restoring defaults'); setAccounts([ { id: 'all-accounts', name: 'All', email: '', color: 'bg-gray-500' }, { id: 'loading-account', name: 'Loading...', email: '', color: 'bg-blue-500', folders: mailboxes } ]); } }, [accounts, mailboxes]); // Initialize session and start prefetching useEffect(() => { // Flag to prevent multiple initialization attempts let isMounted = true; let initAttempted = false; const initSession = async () => { if (initAttempted) return; initAttempted = true; try { 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 const response = await fetch('/api/courrier/session'); const data = await response.json(); // Log the raw API response to inspect structure console.log('[DEBUG] Raw session API response:', JSON.stringify(data, null, 2)); // Add detailed logging to inspect the accounts and folders structure console.log('=== SESSION API RESPONSE DETAILED INSPECTION ==='); console.log('Session authenticated:', data.authenticated); console.log('Has email credentials:', data.hasEmailCredentials); console.log('Primary email:', data.email); console.log('Redis status:', data.redisStatus); // Log mailboxes structure - what the frontend used previously console.log('=== MAILBOXES STRUCTURE (OLD API FORMAT) ==='); console.log('Global mailboxes exists:', !!data.mailboxes); console.log('Global mailboxes is array:', Array.isArray(data.mailboxes)); console.log('Global mailboxes:', data.mailboxes); // Log allAccounts structure - the new per-account folders approach console.log('=== ALL ACCOUNTS STRUCTURE (NEW API FORMAT) ==='); console.log('allAccounts exists:', !!data.allAccounts); console.log('allAccounts is array:', Array.isArray(data.allAccounts)); console.log('allAccounts length:', data.allAccounts?.length || 0); // Inspect each account's structure if (data.allAccounts && Array.isArray(data.allAccounts)) { data.allAccounts.forEach((account: any, idx: number) => { console.log(`Account ${idx + 1}:`, { id: account.id, email: account.email, display_name: account.display_name, foldersExist: !!account.folders, foldersIsArray: Array.isArray(account.folders), foldersLength: account.folders?.length || 0, folders: account.folders }); }); } if (!isMounted) return; if (data.authenticated) { if (data.hasEmailCredentials) { console.log('Session initialized, prefetch status:', data.prefetchStarted ? 'running' : 'not started'); setPrefetchStarted(Boolean(data.prefetchStarted)); // Create a copy of the current accounts to update const updatedAccounts = [...accounts]; // Check if we have multiple accounts returned if (data.allAccounts && Array.isArray(data.allAccounts) && data.allAccounts.length > 0) { console.log('[DEBUG] Multiple accounts found:', data.allAccounts.length); // First, validate the structure of each account data.allAccounts.forEach((account: any, index: number) => { console.log(`[DEBUG] Account ${index+1} structure check:`, { id: account.id, email: account.email, display_name: account.display_name, hasFolders: !!account.folders, foldersIsArray: Array.isArray(account.folders), foldersCount: Array.isArray(account.folders) ? account.folders.length : 0, folders: account.folders || [] }); }); // Keep the All account at position 0 if (updatedAccounts.length > 0) { // Replace the loading account with the real one data.allAccounts.forEach((account: any, index: number) => { // Ensure folders are valid const accountFolders = (account.folders && Array.isArray(account.folders)) ? account.folders : (data.mailboxes && Array.isArray(data.mailboxes)) ? data.mailboxes : ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']; // If we're updating the first real account (index 0 in API, position 1 in our array) if (index === 0 && updatedAccounts.length > 1) { // Update the loading account in place to maintain references updatedAccounts[1] = { id: account.id, // Use the real account ID name: account.display_name || account.email, email: account.email, color: account.color || 'bg-blue-500', folders: accountFolders }; console.log(`[DEBUG] Updated loading account to real account: ${account.email} with ID ${account.id}`); } else { // Add additional accounts as new entries updatedAccounts.push({ id: account.id || `account-${index}`, name: account.display_name || account.email, email: account.email, color: account.color || 'bg-blue-500', folders: accountFolders }); } }); } else { // Fallback if accounts array is empty for some reason updatedAccounts.push({ id: 'all-accounts', name: 'All', email: '', color: 'bg-gray-500' }); const firstAccount = data.allAccounts[0]; const accountFolders = (firstAccount.folders && Array.isArray(firstAccount.folders)) ? firstAccount.folders : (data.mailboxes && Array.isArray(data.mailboxes)) ? data.mailboxes : ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']; updatedAccounts.push({ id: firstAccount.id, name: firstAccount.display_name || firstAccount.email, email: firstAccount.email, color: firstAccount.color || 'bg-blue-500', folders: accountFolders }); } } else if (data.email) { // Fallback to single account if allAccounts is not available console.log(`[DEBUG] Fallback to single account: ${data.email}`); // Force include some hardcoded folders if none are present const fallbackFolders = ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']; // Prioritize mailboxes from IMAP if available const folderList = (data.mailboxes && data.mailboxes.length > 0) ? data.mailboxes : fallbackFolders; // Update the loading account if it exists if (updatedAccounts.length > 1) { updatedAccounts[1] = { id: 'default-account', // Use consistent ID name: data.displayName || data.email, email: data.email, color: 'bg-blue-500', folders: folderList }; } else { // Fallback if accounts array is empty updatedAccounts.push({ id: 'all-accounts', name: 'All', email: '', color: 'bg-gray-500' }); updatedAccounts.push({ id: 'default-account', name: data.displayName || data.email, email: data.email, color: 'bg-blue-500', folders: folderList }); } } console.log('Setting accounts:', updatedAccounts); // Debug each account's folders updatedAccounts.forEach((acc: Account, idx: number) => { console.log(`Account ${idx + 1}: ${acc.name} (${acc.email}) - Folders:`, acc.folders ? acc.folders.length : 0, acc.folders || []); }); // Update the accounts state setAccounts(updatedAccounts); // Auto-select the first non-All account if available if (updatedAccounts.length > 1) { console.log('Auto-selecting account:', updatedAccounts[1]); setSelectedAccount(updatedAccounts[1]); setShowFolders(true); } // Preload first page of emails for faster initial rendering if (session?.user?.id) { await loadEmails(); // If the user hasn't opened this page recently, trigger a background refresh if (data.lastVisit && Date.now() - data.lastVisit > 5 * 60 * 1000) { // It's been more than 5 minutes, refresh in background try { const refreshResponse = await fetch('/api/courrier/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ folder: currentFolder }) }); console.log('Background refresh triggered'); } catch (error) { console.error('Failed to trigger background refresh', error); } } } } 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, loadEmails]); // 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) => { return folder.charAt(0).toUpperCase() + folder.slice(1).toLowerCase(); }; // Check for more emails const hasMoreEmails = page < totalPages; // Handle loading more emails on scroll const handleLoadMore = () => { if (hasMoreEmails && !isLoading) { // Increment the page const nextPage = page + 1; setPage(nextPage); // Also prefetch additional pages to make scrolling smoother if (session?.user?.id) { // Prefetch next 2 pages beyond the current next page prefetchFolderEmails( session.user.id, currentFolder, 2, nextPage + 1, selectedAccount?.id ).catch(err => { console.error(`Error prefetching additional pages for ${currentFolder}:`, err); }); } // Note: loadEmails will be called automatically due to the page dependency in useEffect } }; // 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; } }; // Handle 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 mailbox change with prefetching const handleMailboxChange = (folder: string, accountId?: string) => { // Reset to page 1 when changing folders setPage(1); // If we have a specific accountId, store it with the folder if (accountId) { // Store the current account ID with the folder change console.log(`Changing folder to ${folder} for account ${accountId}`); } // Change folder in the state changeFolder(folder); setCurrentView(folder); // Start prefetching additional pages for this folder if (session?.user?.id && folder) { // First two pages are most important - prefetch immediately prefetchFolderEmails(session.user.id, folder, 3, 1, accountId).catch(err => { console.error(`Error prefetching ${folder}:`, err); }); } }; // Handle sending email const handleSendEmail = async (emailData: EmailData) => { return await sendEmail(emailData); }; // Handle delete confirmation const handleDeleteConfirm = async () => { await deleteEmails(selectedEmailIds); setShowDeleteConfirm(false); }; // Check login on mount useEffect(() => { // Check if the user is logged in after a short delay const timer = setTimeout(() => { if (error?.includes('Not authenticated') || error?.includes('No email credentials found')) { setShowLoginNeeded(true); } }, 2000); return () => clearTimeout(timer); }, [error]); // Go to login page const handleGoToLogin = () => { router.push('/courrier/login'); }; // Extra debugging for folder rendering useEffect(() => { if (selectedAccount && showFolders) { console.log('Folder rendering debug:', 'account:', selectedAccount.id, 'folders:', selectedAccount.folders?.length || 0, 'showFolders:', showFolders ); } }, [selectedAccount, showFolders]); return ( <> {/* Main layout */}
{/* Panel 1: Sidebar - Always visible */}
{/* Courrier Title */}
COURRIER
{/* Compose button and refresh button */}
{/* Scrollable area for accounts and folders */}
{/* Accounts Section */}
{/* Display all accounts */}
{/* Form for adding a new account */} {showAddAccountForm && (

Add IMAP Account

{ e.preventDefault(); setLoading(true); const formData = new FormData(e.currentTarget); // 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); // If connection test is successful, save the 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'); } // Update accounts list const newAccountObj = { id: `account-${Date.now()}`, // generate unique string ID name: formValues.display_name, email: formValues.email, color: `bg-blue-500`, // Default color class folders: testResult.details.sampleFolders || ['INBOX', 'Sent', 'Drafts', 'Trash'] // Use discovered folders or defaults }; setAccounts(prev => [...prev, newAccountObj]); 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); } }}>
IMAP SMTP
Note: SMTP settings needed for sending emails
)} {accounts.map((account) => (
{/* Show folders for this account if expanded */} {expandedAccounts[account.id] && account.id !== 'all-accounts' && account.folders && (
{account.folders.map((folder) => ( ))}
)}
))}
{/* Panel 2: Email List - Always visible */}
{/* Header without search bar or profile */}
{getFolderIcon(currentFolder)} {formatFolderName(currentFolder)}
{selectedEmailIds.length > 0 && (
)}
{/* Email List - Always visible */}
{isLoading ? (

Loading emails...

) : error ? (
Error {error} {(error?.includes('Not authenticated') || error?.includes('No email credentials found')) && ( )}
) : (
{/* Email List */}
{emails.length === 0 ? (

No emails found

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

) : ( )}
)}
{/* 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)} /> setShowLoginNeeded(false)} /> {/* Compose Email Dialog */} !open && setShowComposeModal(false)}> New Message setShowComposeModal(false)} /> ); }