'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 { 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 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;
};
}
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;
};
}
// 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',
];
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: '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>({});
// Track selected folder per account
const [selectedFolders, setSelectedFolders] = useState>({});
// Track folder visibility per account
const [visibleFolders, setVisibleFolders] = useState>({});
// 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);
// Debug accounts state
useEffect(() => {
console.log('Current accounts state:', accounts);
console.log('Expanded accounts:', expandedAccounts);
console.log('Selected account:', selectedAccount);
console.log('Show folders:', showFolders);
}, [accounts, expandedAccounts, selectedAccount, showFolders]);
// 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 for each account and folder
useEffect(() => {
// Create a map to store unread counts per account and folder
const accountFolderUnreadCounts = new Map>();
// Initialize counts for all accounts and folders
accounts.forEach(account => {
if (account.id !== 'loading-account') {
const folderCounts = new Map();
account.folders.forEach(folder => {
folderCounts.set(folder, 0);
});
accountFolderUnreadCounts.set(account.id, folderCounts);
}
});
// Count unread emails for each account and folder
(emails || []).forEach(email => {
// Check if email is unread based on flags
const isUnread = email.flags && !email.flags.seen;
// Count unread emails for the specific account and folder
if (isUnread && email.accountId && email.folder) {
const folderCounts = accountFolderUnreadCounts.get(email.accountId);
if (folderCounts) {
const currentCount = folderCounts.get(email.folder) || 0;
folderCounts.set(email.folder, currentCount + 1);
}
}
});
// Update the unread count for the selected account and folder
if (selectedAccount && selectedAccount.id !== 'loading-account') {
const folderCounts = accountFolderUnreadCounts.get(selectedAccount.id);
if (folderCounts) {
setUnreadCount(folderCounts.get(currentFolder) || 0);
} else {
setUnreadCount(0);
}
} else {
// For 'loading-account', sum up all unread counts for the current folder
let totalUnread = 0;
accountFolderUnreadCounts.forEach((folderCounts: Map) => {
totalUnread += folderCounts.get(currentFolder) || 0;
});
setUnreadCount(totalUnread);
}
// Log the counts for debugging
console.log('Unread counts per account and folder:',
Object.fromEntries(
Array.from(accountFolderUnreadCounts.entries()).map(([accountId, folderCounts]) => [
accountId,
Object.fromEntries(folderCounts.entries())
])
)
);
}, [emails, selectedAccount, currentFolder, accounts]);
// 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: '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 retryCount = 0;
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000; // 1 second
const initSession = async () => {
try {
if (!isMounted) return;
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', {
credentials: 'include', // Ensure cookies are sent
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');
// Instead of throwing, redirect to login
return;
}
}
if (!response.ok) {
throw new Error(`Session request failed with status ${response.status}`);
}
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.authenticated) {
if (data.hasEmailCredentials) {
console.log('Session initialized, prefetch status:', data.prefetchStarted ? 'running' : 'not started');
setPrefetchStarted(Boolean(data.prefetchStarted));
let updatedAccounts: Account[] = [];
// 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);
// Add all accounts from the API response
data.allAccounts.forEach((account: any) => {
console.log('[DEBUG] Processing account:', {
id: account.id,
email: account.email,
folders: account.folders
});
// Use exact folders from IMAP without any mapping
const accountFolders = (account.folders && Array.isArray(account.folders))
? account.folders
: [];
// Keep the account prefix in folder names
const validFolders = accountFolders.map((folder: string) => {
// If folder doesn't have account prefix, add it
if (!folder.includes(':')) {
return `${account.id}:${folder}`;
}
return folder;
});
updatedAccounts.push({
id: account.id,
name: account.display_name || account.email,
email: account.email,
color: colorPalette[(updatedAccounts.length - 1) % colorPalette.length],
folders: validFolders
});
console.log(`[DEBUG] Added account with folders:`, {
id: account.id,
email: account.email,
folders: validFolders
});
});
} else {
// Fallback to single account if allAccounts is not available
console.log(`[DEBUG] Fallback to single account: ${data.email}`);
// Use exact folders from IMAP if 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[(updatedAccounts.length - 1) % colorPalette.length],
folders: folderList
});
}
// Update accounts state
setAccounts(updatedAccounts);
console.log('[DEBUG] Updated accounts:', 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);
// Ensure folders are visible for the selected account
setExpandedAccounts(prev => ({
...prev,
[updatedAccounts[1].id]: true
}));
// Set initial selected folder
setSelectedFolders(prev => ({
...prev,
[updatedAccounts[1].id]: 'INBOX'
}));
}
} else {
// User is authenticated but doesn't have email credentials
setShowLoginNeeded(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);
}
}
}
} 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);
};
// Update handleMailboxChange to properly handle per-account folders
const handleMailboxChange = (folder: string, accountId?: string) => {
if (accountId && accountId !== 'loading-account') {
const account = accounts.find(a => a.id === accountId);
if (!account) {
toast({
title: "Account not found",
description: `The account ${accountId} could not be found.`,
variant: "destructive",
});
return;
}
// Only allow navigation to folders in selectedAccount.folders
if (!account.folders.includes(folder)) {
toast({
title: "Folder not found",
description: `The folder ${folder} does not exist for this account.`,
variant: "destructive",
});
return;
}
setSelectedFolders(prev => ({ ...prev, [accountId]: folder }));
changeFolder(folder, accountId);
} else {
changeFolder(folder, accountId);
}
};
// Update the folder button rendering to show selected state based on account
const renderFolderButton = (folder: string, accountId: string) => {
// Get the account prefix from the folder name
const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId;
// Only show folders that belong to this account
if (folderAccountId !== accountId) return null;
const isSelected = selectedFolders[accountId] === folder;
const account = accounts.find(a => a.id === accountId);
// Get the base folder name for display
const baseFolder = folder.includes(':') ? folder.split(':')[1] : folder;
return (
);
};
// Handle sending email
const handleSendEmail = async (emailData: EmailData) => {
const result = await sendEmail(emailData);
if (!result.success) {
throw new Error(result.error);
}
return result;
};
// 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]);
const handleAccountSelect = (account: Account) => {
setSelectedAccount(account);
setShowFolders(true);
if (account.id !== 'loading-account') {
setExpandedAccounts(prev => ({
...prev,
[account.id]: true
}));
}
handleMailboxChange('INBOX', account.id);
};
const handleAddAccount = async (accountData: AccountData) => {
// ... account creation logic ...
// setAccounts(prev => [...prev, newAccount]);
// setVisibleFolders(prev => ({
// ...prev,
// [newAccount.id]: newAccount.folders
// }));
};
// Debug folder rendering
useEffect(() => {
if (selectedAccount) {
console.log('Selected account folders:', selectedAccount.folders);
console.log('Is account expanded:', expandedAccounts[selectedAccount.id]);
}
}, [selectedAccount, expandedAccounts]);
// On page load/refresh, expand all accounts so their folders are always visible
useEffect(() => {
// Expand all real accounts (not loading-account) on load/refresh
const expanded: Record = {};
accounts.forEach(a => {
if (a.id !== 'loading-account') expanded[a.id] = true;
});
setExpandedAccounts(expanded);
}, [accounts]);
return (
<>
{/* Main layout */}
{/* Panel 1: Sidebar - Always visible */}
{/* Courrier Title */}
{/* Compose button and refresh button */}
{/* Scrollable area for accounts and folders */}
{/* Accounts Section */}
Accounts
{/* Display all accounts */}
{/* Form for adding a new account */}
{showAddAccountForm && (
)}
{accounts.map((account) => (
handleAccountSelect(account)}
tabIndex={0}
role="button"
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') handleAccountSelect(account); }}
>
{account.name}
{/* More options button (⋮) */}
{account.id !== 'loading-account' && (
{ e.stopPropagation(); setAccountToEdit(account); setShowEditModal(true); }}>
Edit
{ e.stopPropagation(); setAccountToDelete(account); setShowDeleteDialog(true); }}>
Delete
)}
{/* Expand/collapse arrow */}
{account.id !== 'loading-account' && (
)}
{/* Show folders for any expanded account */}
{expandedAccounts[account.id] && account.folders && account.folders.length > 0 && (
{account.folders.map((folder) => renderFolderButton(folder, account.id))}
)}
))}
{/* 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)}
{selectedEmailIds.length > 0 && (
)}
{/* Email List - Always visible */}
{isLoading ? (
) : error ? (
) : (
{/* 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)}
/>
{/* Compose Email Dialog */}
{/* Edit Password Modal */}
{/* 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
>
);
}