'use client'; import React from 'react'; import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import { Mail, Search, Plus as PlusIcon, Trash, Check, Forward, FolderOpen, MessageSquare, Copy, AlertOctagon, MoreHorizontal, ChevronDown, ChevronUp, X, RefreshCw, Inbox, Send, Archive, Star, Settings, Menu, Plus, Loader2, MailX, UserPlus, CheckCircle2, XCircle, Filter, Reply, ReplyAll, AlertCircle, Folder, Eye, ChevronLeft, ChevronRight, Paperclip, Trash2, CornerUpRight, Edit, MoreVertical, EyeOff, FileDown, FilePlus, Ban, MailOpen } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from '@/components/ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Label } from '@/components/ui/label'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Checkbox } from '@/components/ui/checkbox'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, } from '@/components/ui/command'; import { useSession } from 'next-auth/react'; import DOMPurify from 'isomorphic-dompurify'; import ComposeEmail from '@/components/email/ComposeEmail'; import { decodeEmail, cleanHtml } from '@/lib/mail-parser-wrapper'; import { Attachment as MailParserAttachment } from 'mailparser'; import { LoadingFix } from './loading-fix'; // Import centralized email formatters import { formatForwardedEmail, formatReplyEmail, formatEmailForReplyOrForward, EmailMessage as FormatterEmailMessage, cleanHtmlContent } from '@/lib/utils/email-formatter'; export interface Account { id: number; name: string; email: string; color: string; folders?: string[]; } export interface Email { id: string; from: string; fromName?: string; to: string; subject: string; content: string; preview?: string; // Preview content for list view body?: string; // For backward compatibility date: string; read: boolean; starred: boolean; attachments?: { name: string; url: string }[]; folder: string; cc?: string; bcc?: string; contentFetched?: boolean; // Track if full content has been fetched } interface Attachment { name: string; type: string; content: string; encoding: string; } interface ParsedEmailContent { headers: string; body: string; html?: string; text?: string; attachments?: Array<{ filename: string; content: string; contentType: string; }>; } interface ParsedEmailMetadata { subject: string; from: string; to: string; date: string; contentType: string; text: string | null; html: string | null; raw: { headers: string; body: string; }; } /** * @deprecated This function is deprecated and will be removed in future versions. * Email parsing has been centralized in lib/mail-parser-wrapper.ts and the API endpoint. */ function splitEmailHeadersAndBody(emailBody: string): { headers: string; body: string } { const [headers, ...bodyParts] = emailBody.split('\r\n\r\n'); return { headers: headers || '', body: bodyParts.join('\r\n\r\n') }; } function EmailContent({ email }: { email: Email }) { const [content, setContent] = useState(null); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); const [debugInfo, setDebugInfo] = useState(null); useEffect(() => { let mounted = true; async function loadContent() { if (!email) return; setIsLoading(true); setDebugInfo(null); try { console.log('Loading content for email:', email.id); // Check if we need to fetch full content if (!email.content || email.content.length === 0) { console.log('Fetching full content for email:', email.id); const response = await fetch(`/api/courrier/${email.id}?folder=${encodeURIComponent(email.folder || 'INBOX')}`); if (!response.ok) { throw new Error(`Failed to fetch email content: ${response.status}`); } const fullContent = await response.json(); if (mounted) { // Update the email content with the fetched full content email.content = fullContent.content; // Render the content const sanitizedHtml = DOMPurify.sanitize(fullContent.content); setContent(
); setDebugInfo('Rendered fetched HTML content'); setError(null); setIsLoading(false); } return; } // Use existing content if available console.log('Using existing content for email'); const formattedEmail = email.content.trim(); if (!formattedEmail) { console.log('Empty content for email:', email.id); if (mounted) { setContent(
Email content is empty
); setDebugInfo('Email content is empty string'); setIsLoading(false); } return; } // Check if content is already HTML if (formattedEmail.startsWith('<') && formattedEmail.endsWith('>')) { // Content is likely HTML, sanitize and display directly const sanitizedHtml = DOMPurify.sanitize(formattedEmail); setContent(
); setDebugInfo('Rendered existing HTML content'); } else { // Use mailparser for more complex formats console.log('Parsing email content'); const parsedEmail = await decodeEmail(formattedEmail); if (parsedEmail.html) { const sanitizedHtml = DOMPurify.sanitize(parsedEmail.html); setContent(
); setDebugInfo('Rendered HTML content from parser'); } else if (parsedEmail.text) { setContent(
{parsedEmail.text}
); setDebugInfo('Rendered text content from parser'); } else { setContent(
No displayable content available
); setDebugInfo('No HTML or text content in parsed email'); } } setError(null); } catch (err) { console.error('Error rendering email content:', err); if (mounted) { setError('Error rendering email content. Please try again.'); setDebugInfo(err instanceof Error ? err.message : 'Unknown error'); setContent(null); } } finally { if (mounted) { setIsLoading(false); } } } loadContent(); return () => { mounted = false; }; }, [email?.id, email?.content, email?.folder]); if (isLoading) { return (
Loading email content...
); } if (error) { return (
{error}
{debugInfo && (
Debug info: {debugInfo}
)}
); } return ( <> {content ||
No content available
} {debugInfo && process.env.NODE_ENV !== 'production' && (
Debug: {debugInfo}
)} ); } function renderEmailContent(email: Email) { return ; } function renderAttachments(attachments: MailParserAttachment[]) { if (!attachments.length) return null; return (

Attachments

{attachments.map((attachment, index) => (
{attachment.filename || 'unnamed_attachment'} {attachment.size ? `(${Math.round(attachment.size / 1024)} KB)` : ''}
))}
); } // Define the exact folder names from IMAP type MailFolder = string; // Map IMAP folders to sidebar items with icons const getFolderIcon = (folder: string) => { switch (folder.toLowerCase()) { case 'inbox': return Inbox; case 'sent': return Send; case 'drafts': return Reply; case 'trash': return Trash; case 'spam': return AlertCircle; case 'archive': case 'archives': return Archive; default: return Folder; } }; // Update the initialSidebarItems to be empty since we're using folders under Accounts now const initialSidebarItems: any[] = []; // Remove the default Inbox item function formatDate(date: Date | null): string { if (!date) return ''; return new Intl.DateTimeFormat('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }).format(date); } /** * @deprecated This function is deprecated and will be removed in future versions. * Use the ReplyContent component directly instead. */ function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward' = 'reply') { console.warn('getReplyBody is deprecated, use instead'); return ; } function ReplyContent({ email, type }: { email: Email; type: 'reply' | 'reply-all' | 'forward' }) { const [content, setContent] = useState(''); const [error, setError] = useState(null); useEffect(() => { let mounted = true; async function loadReplyContent() { try { if (!email.content) { if (mounted) setContent(''); return; } // Create a formatter-compatible email object const emailForFormatter: FormatterEmailMessage = { id: email.id, subject: email.subject || '', from: [{ name: email.fromName || email.from.split('@')[0] || '', address: email.from }], to: [{ name: '', address: email.to }], date: new Date(email.date), content: email.content, html: email.content, text: '' }; // Use centralized formatters let formattedContent = ''; if (type === 'forward') { const formatted = formatForwardedEmail(emailForFormatter); formattedContent = formatted.content; } else { const formatted = formatReplyEmail(emailForFormatter, type as 'reply' | 'reply-all'); formattedContent = formatted.content; } if (mounted) { setContent(formattedContent); setError(null); } } catch (err) { console.error('Error generating reply body:', err); if (mounted) { setError('Error generating reply content. Please try again.'); setContent(''); } } } loadReplyContent(); return () => { mounted = false; }; }, [email.content, type, email.id, email.subject, email.from, email.fromName, email.to, email.date]); if (error) { return
{error}
; } return
; } function EmailPreview({ email }: { email: Email }) { const [preview, setPreview] = useState(''); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); useEffect(() => { let mounted = true; async function loadPreview() { if (!email) { if (mounted) setPreview('No content available'); return; } // If email already has a preview, use it directly if (email.preview) { if (mounted) setPreview(email.preview); return; } setIsLoading(true); try { // If we have the content already, extract preview from it if (email.content) { const plainText = email.content.replace(/<[^>]*>/g, ' ').trim(); if (mounted) { setPreview(plainText.substring(0, 150) + '...'); } } else { // Fallback to using parser for older emails const decoded = await decodeEmail(email.content || ''); if (mounted) { if (decoded.text) { setPreview(decoded.text.substring(0, 150) + '...'); } else if (decoded.html) { const cleanText = decoded.html.replace(/<[^>]*>/g, ' ').trim(); setPreview(cleanText.substring(0, 150) + '...'); } else { setPreview('No preview available'); } } } if (mounted) { setError(null); } } catch (err) { console.error('Error generating email preview:', err); if (mounted) { setError('Error generating preview'); setPreview(''); } } finally { if (mounted) setIsLoading(false); } } loadPreview(); return () => { mounted = false; }; }, [email]); if (isLoading) { return Loading preview...; } if (error) { return {error}; } return {preview}; } /** * @deprecated This function is deprecated and will be removed in future versions. * Use the EmailPreview component directly instead. */ function generateEmailPreview(email: Email) { console.warn('generateEmailPreview is deprecated, use instead'); return ; } export default function CourrierPage() { const router = useRouter(); const { data: session } = useSession(); const [loading, setLoading] = useState(true); const [accounts, setAccounts] = useState([ { id: 0, name: 'All', email: '', color: 'bg-gray-400' }, { id: 1, name: 'Mail Account', email: '', color: 'bg-blue-500' } ]); const [selectedAccount, setSelectedAccount] = useState(null); const [currentView, setCurrentView] = useState('INBOX'); const [showCompose, setShowCompose] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [selectedEmails, setSelectedEmails] = useState([]); const [showBulkActions, setShowBulkActions] = useState(false); const [showBcc, setShowBcc] = useState(false); const [emails, setEmails] = useState([]); const [error, setError] = useState(null); const [composeSubject, setComposeSubject] = useState(''); const [composeTo, setComposeTo] = useState(''); const [composeCc, setComposeCc] = useState(''); const [composeBcc, setComposeBcc] = useState(''); const [composeBody, setComposeBody] = useState(''); const [selectedEmail, setSelectedEmail] = useState(null); const [sidebarOpen, setSidebarOpen] = useState(true); const [foldersOpen, setFoldersOpen] = useState(true); const [showSettings, setShowSettings] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [composeOpen, setComposeOpen] = useState(false); const [accountsDropdownOpen, setAccountsDropdownOpen] = useState(true); const [foldersDropdownOpen, setFoldersDropdownOpen] = useState(false); const [showAccountActions, setShowAccountActions] = useState(null); const [showEmailActions, setShowEmailActions] = useState(false); const [deleteType, setDeleteType] = useState<'email' | 'emails' | 'account'>('email'); const [itemToDelete, setItemToDelete] = useState(null); const [showCc, setShowCc] = useState(false); const [contentLoading, setContentLoading] = useState(false); const [attachments, setAttachments] = useState([]); const [folders, setFolders] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [availableFolders, setAvailableFolders] = useState([]); const [sidebarItems, setSidebarItems] = useState(initialSidebarItems); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); const [isLoadingInitial, setIsLoadingInitial] = useState(true); const [isLoadingSearch, setIsLoadingSearch] = useState(false); const [isLoadingCompose, setIsLoadingCompose] = useState(false); const [isLoadingReply, setIsLoadingReply] = useState(false); const [isLoadingForward, setIsLoadingForward] = useState(false); const [isLoadingDelete, setIsLoadingDelete] = useState(false); const [isLoadingMove, setIsLoadingMove] = useState(false); const [isLoadingStar, setIsLoadingStar] = useState(false); const [isLoadingUnstar, setIsLoadingUnstar] = useState(false); const [isLoadingMarkRead, setIsLoadingMarkRead] = useState(false); const [isLoadingMarkUnread, setIsLoadingMarkUnread] = useState(false); const [isLoadingRefresh, setIsLoadingRefresh] = useState(false); const emailsPerPage = 20; const [isSearching, setIsSearching] = useState(false); const [searchResults, setSearchResults] = useState([]); const [showSearchResults, setShowSearchResults] = useState(false); const [isComposing, setIsComposing] = useState(false); const [composeEmail, setComposeEmail] = useState({ to: '', subject: '', body: '', }); const [isSending, setIsSending] = useState(false); const [isReplying, setIsReplying] = useState(false); const [isForwarding, setIsForwarding] = useState(false); const [replyToEmail, setReplyToEmail] = useState(null); const [forwardEmail, setForwardEmail] = useState(null); const [replyBody, setReplyBody] = useState(''); const [forwardBody, setForwardBody] = useState(''); const [replyAttachments, setReplyAttachments] = useState([]); const [forwardAttachments, setForwardAttachments] = useState([]); const [isSendingReply, setIsSendingReply] = useState(false); const [isSendingForward, setIsSendingForward] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [isMoving, setIsMoving] = useState(false); const [isStarring, setIsStarring] = useState(false); const [isUnstarring, setIsUnstarring] = useState(false); const [isMarkingRead, setIsMarkingRead] = useState(false); const [isMarkingUnread, setIsMarkingUnread] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false); const composeBodyRef = useRef(null); const [originalEmail, setOriginalEmail] = useState<{ content: string; type: 'reply' | 'reply-all' | 'forward'; } | null>(null); // Debug logging for email distribution useEffect(() => { const emailsByFolder = emails.reduce((acc, email) => { acc[email.folder] = (acc[email.folder] || 0) + 1; return acc; }, {} as Record); console.log('Emails by folder:', emailsByFolder); console.log('Current view:', currentView); }, [emails, currentView]); // Move getSelectedEmail inside the component const getSelectedEmail = () => { return emails.find(email => email.id === selectedEmail?.id); }; // Add an effect to detect and fix stuck loading states useEffect(() => { let timeoutId: NodeJS.Timeout | null = null; // If we have emails but loading state is still true after emails load, it's likely stuck if (emails.length > 0 && (loading || isLoadingInitial)) { console.log('[DEBUG] Detected potential stuck loading state, setting recovery timeout'); // Set a timeout to automatically reset the loading state timeoutId = setTimeout(() => { // Double check if still stuck if (emails.length > 0 && (loading || isLoadingInitial)) { console.log('[DEBUG] Confirmed stuck loading state, auto-resetting'); setLoading(false); setIsLoadingInitial(false); } }, 3000); // 3 second timeout } // Cleanup return () => { if (timeoutId) { clearTimeout(timeoutId); } }; }, [emails.length, loading, isLoadingInitial]); // Single initialization effect that loads emails correctly on first page load useEffect(() => { const loadInitialData = async () => { try { console.log('[DEBUG] Starting initial email data loading...'); setLoading(true); setIsLoadingInitial(true); // Check credentials first console.log('[DEBUG] Checking credentials...'); const credResponse = await fetch('/api/courrier'); if (!credResponse.ok) { const errorData = await credResponse.json(); if (errorData.error === 'No stored credentials found') { console.log('[DEBUG] No credentials found, redirecting to login'); router.push('/courrier/login'); return; } throw new Error(errorData.error || 'Failed to check credentials'); } try { // Try to get user email from credentials const credsData = await credResponse.json(); console.log('[DEBUG] Credentials response:', { hasEmail: !!credsData.email, status: credResponse.status }); if (credsData.email) { console.log('[DEBUG] Got email from credentials:', credsData.email); setAccounts(prev => prev.map(account => account.id === 1 ? { ...account, name: credsData.email, email: credsData.email } : account )); } } catch (error) { console.warn('[DEBUG] Error getting email from credentials:', error); } // First do a quick request just for folders try { console.log('[DEBUG] Preloading folders...'); const folderResponse = await fetch('/api/courrier?folder=INBOX&page=1&limit=1'); if (folderResponse.ok) { const folderData = await folderResponse.json(); console.log('[DEBUG] Folder data:', { folders: folderData.folders?.length || 0, emails: folderData.emails?.length || 0 }); if (folderData.folders && folderData.folders.length > 0) { setAvailableFolders(folderData.folders); setAccounts(prev => prev.map(account => account.id === 1 ? { ...account, folders: folderData.folders } : account )); } } } catch (error) { console.warn('[DEBUG] Error preloading folders:', error); } // Then load emails (forced fetch with timestamp) console.log('[DEBUG] Fetching emails...'); const timestamp = Date.now(); const emailResponse = await fetch( `/api/courrier?folder=${encodeURIComponent(currentView)}&page=${page}&limit=${emailsPerPage}&_t=${timestamp}`, { cache: 'no-store' } ); if (!emailResponse.ok) { throw new Error('Failed to load emails'); } const data = await emailResponse.json(); console.log(`[DEBUG] Loaded ${data.emails?.length || 0} emails, response status: ${emailResponse.status}`); // Set available folders if present if (data.folders) { console.log('[DEBUG] Setting folders from initialization:', data.folders.length); setAvailableFolders(data.folders); // Update the mail account with folders setAccounts(prev => { console.log('[DEBUG] Updating accounts with folders'); return prev.map(account => account.id === 1 ? { ...account, folders: data.folders } : account ); }); } else { console.warn('[DEBUG] No folders returned from API during initialization'); } // Process emails and sort by date console.log('[DEBUG] Processing emails...'); const processedEmails = (data.emails || []) .map((email: any) => { // Add proper handling for from field which might be an array or object let fromText = ''; let fromName = ''; let toText = ''; let ccText = ''; let bccText = ''; // Handle 'from' field if (email.from) { if (Array.isArray(email.from)) { if (email.from.length > 0) { if (typeof email.from[0] === 'object') { fromText = email.from[0].address || ''; fromName = email.from[0].name || email.from[0].address?.split('@')[0] || ''; } else { fromText = email.from[0] || ''; fromName = fromText.split('@')[0] || ''; } } } else if (typeof email.from === 'object') { fromText = email.from.address || ''; fromName = email.from.name || email.from.address?.split('@')[0] || ''; } else if (typeof email.from === 'string') { fromText = email.from; fromName = email.fromName || email.from.split('@')[0] || ''; } } // Handle 'to' field if (email.to) { if (Array.isArray(email.to)) { if (email.to.length > 0) { if (typeof email.to[0] === 'object') { toText = email.to.map((t: any) => t.address || '').join(', '); } else { toText = email.to.join(', '); } } } else if (typeof email.to === 'object') { toText = email.to.address || ''; } else if (typeof email.to === 'string') { toText = email.to; } } // Handle 'cc' field if (email.cc) { if (Array.isArray(email.cc)) { if (email.cc.length > 0) { if (typeof email.cc[0] === 'object') { ccText = email.cc.map((c: any) => c.address || '').join(', '); } else { ccText = email.cc.join(', '); } } } else if (typeof email.cc === 'object') { ccText = email.cc.address || ''; } else if (typeof email.cc === 'string') { ccText = email.cc; } } // Handle 'bcc' field if (email.bcc) { if (Array.isArray(email.bcc)) { if (email.bcc.length > 0) { if (typeof email.bcc[0] === 'object') { bccText = email.bcc.map((b: any) => b.address || '').join(', '); } else { bccText = email.bcc.join(', '); } } } else if (typeof email.bcc === 'object') { bccText = email.bcc.address || ''; } else if (typeof email.bcc === 'string') { bccText = email.bcc; } } return { id: email.id, accountId: 1, from: fromText, fromName: fromName, to: toText, subject: email.subject || '(No subject)', content: email.content || '', preview: email.preview || '', date: email.date || new Date().toISOString(), read: email.read || false, starred: email.starred || false, folder: email.folder || currentView, cc: ccText, bcc: bccText, flags: email.flags || [], hasAttachments: email.hasAttachments || false }; }) .sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime()); // Set emails console.log('[DEBUG] Setting emails state with', processedEmails.length, 'emails'); setEmails(processedEmails); // Update unread count for inbox if (currentView === 'INBOX') { const unreadInboxEmails = processedEmails.filter( (email: Email) => !email.read && email.folder === 'INBOX' ).length; setUnreadCount(unreadInboxEmails); } // Update pagination setHasMore(data.hasMore); setError(null); console.log('[DEBUG] Initial load complete, setting loading states to false'); } catch (err) { console.error('[DEBUG] Error loading initial data:', err); setError(err instanceof Error ? err.message : 'Failed to load data'); } finally { console.log('[DEBUG] Setting loading state to false in finally block'); // Ensure we reset the loading state after a short delay to make sure React has processed state updates setTimeout(() => { setLoading(false); setIsLoadingInitial(false); }, 100); } }; loadInitialData(); }, [router, currentView, page, emailsPerPage]); // Fix the type issue with the session email useEffect(() => { if (session?.user?.email) { setAccounts(prev => prev.map(account => account.id === 1 ? { ...account, name: session.user.email || 'Mail Account', email: session.user.email || '' } : account )); } }, [session?.user?.email]); // Get account color const getAccountColor = (accountId: number) => { const account = accounts.find(acc => acc.id === accountId); return account ? account.color : 'bg-gray-500'; }; // Update handleEmailSelect to handle the from field correctly const handleEmailSelect = async (emailId: string) => { try { setContentLoading(true); // Find the email in the current list const selectedEmail = emails.find(email => email.id === emailId); if (!selectedEmail) { throw new Error('Email not found in list'); } // Check if we need to fetch full content if (!selectedEmail.content || selectedEmail.content.length === 0) { console.log('[DEBUG] Fetching full content for email:', emailId); try { const response = await fetch(`/api/courrier/${emailId}?folder=${encodeURIComponent(selectedEmail.folder || 'INBOX')}`); if (!response.ok) { throw new Error(`Failed to fetch email content: ${response.status}`); } const fullContent = await response.json(); // Update the email content with the fetched full content selectedEmail.content = fullContent.content; selectedEmail.contentFetched = true; // Update the email in the list too so we don't refetch setEmails(prevEmails => prevEmails.map(email => email.id === emailId ? { ...email, content: fullContent.content, contentFetched: true } : email ) ); console.log('[DEBUG] Successfully fetched full content for email:', emailId); } catch (error) { console.error('[DEBUG] Error fetching full content:', error); } } // Set selected email from our existing data (which now includes full content) setSelectedEmail(selectedEmail); // Try to mark as read in the background if not already read if (!selectedEmail.read) { try { // Use the new API endpoint await fetch(`/api/courrier/${emailId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'mark-read' }), }); // Update read status in the list setEmails(prevEmails => prevEmails.map(email => email.id === emailId ? { ...email, read: true } : email ) ); } catch (error) { console.error('Error marking email as read:', error); } } } catch (error) { console.error('Error selecting email:', error); setError('Failed to select email. Please try again.'); } finally { setContentLoading(false); } }; // Add these improved handlers const handleEmailCheckbox = (e: React.ChangeEvent, emailId: number) => { e.stopPropagation(); if (e.target.checked) { setSelectedEmails([...selectedEmails, emailId.toString()]); } else { setSelectedEmails(selectedEmails.filter(id => id !== emailId.toString())); } }; // Handles marking an individual email as read/unread const handleMarkAsRead = (emailId: string, isRead: boolean) => { setEmails(emails.map(email => email.id.toString() === emailId ? { ...email, read: isRead } : email )); }; // Handles bulk actions for selected emails const handleBulkAction = async (action: 'delete' | 'mark-read' | 'mark-unread' | 'archive') => { if (action === 'delete') { setDeleteType('emails'); setShowDeleteConfirm(true); return; } try { const response = await fetch('/api/courrier/bulk-actions', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ emailIds: selectedEmails, action: action }), }); if (!response.ok) { throw new Error('Failed to perform bulk action'); } // Update local state based on the action setEmails(emails.map(email => { if (selectedEmails.includes(email.id.toString())) { switch (action) { case 'mark-read': return { ...email, read: true }; case 'mark-unread': return { ...email, read: false }; case 'archive': return { ...email, folder: 'Archive' }; default: return email; } } return email; })); // Clear selection after successful action setSelectedEmails([]); } catch (error) { console.error('Error performing bulk action:', error); alert('Failed to perform bulk action. Please try again.'); } }; // Add handleDeleteConfirm function const handleDeleteConfirm = async () => { try { const response = await fetch('/api/courrier/bulk-actions', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ emailIds: selectedEmails, action: 'delete' }), }); if (!response.ok) { throw new Error('Failed to delete emails'); } // Remove deleted emails from state setEmails(emails.filter(email => !selectedEmails.includes(email.id.toString()))); setSelectedEmails([]); } catch (error) { console.error('Error deleting emails:', error); alert('Failed to delete emails. Please try again.'); } finally { setShowDeleteConfirm(false); } }; // Add infinite scroll handler const handleScroll = useCallback((e: React.UIEvent) => { const target = e.currentTarget; if ( target.scrollHeight - target.scrollTop === target.clientHeight && !isLoadingMore && hasMore ) { setPage(prev => prev + 1); loadEmails(true); } }, [isLoadingMore, hasMore]); // Sort emails by date (most recent first) const sortedEmails = useMemo(() => { return [...emails].sort((a, b) => { return new Date(b.date).getTime() - new Date(a.date).getTime(); }); }, [emails]); const toggleSelectAll = () => { if (selectedEmails.length === emails.length) { setSelectedEmails([]); } else { setSelectedEmails(emails.map(email => email.id.toString())); } }; // Update filtered emails to use sortedEmails const filteredEmails = useMemo(() => { if (!searchQuery) return sortedEmails; const query = searchQuery.toLowerCase(); return sortedEmails.filter(email => email.subject.toLowerCase().includes(query) || email.from.toLowerCase().includes(query) || email.to.toLowerCase().includes(query) || email.content.toLowerCase().includes(query) ); }, [sortedEmails, searchQuery]); // Update the email list to use filtered emails const renderEmailList = () => { console.log('[DEBUG] Rendering email list with state:', { loading, isLoadingInitial, emailCount: emails.length, filteredEmailCount: filteredEmails.length, searchQuery: searchQuery.length > 0 ? searchQuery : 'empty', selectedEmails: selectedEmails.length }); return (
{renderEmailListHeader()} {renderBulkActionsToolbar()}
{/* Always show emails when available, regardless of loading state */} {filteredEmails.length > 0 ? (
{filteredEmails.map((email) => renderEmailListItem(email))} {(isLoadingMore || loading) && (
Refreshing...
)}
) : isLoadingInitial || (loading && emails.length === 0) ? (

Loading emails...

) : (

{searchQuery ? 'No emails match your search' : 'No emails in this folder'}

{error && (

{error}

)}

Folder: {currentView}

Total emails: {emails.length}

)}
); }; // Update the email count in the header to show filtered count const renderEmailListHeader = () => (
setSearchQuery(e.target.value)} />
0 && selectedEmails.length === filteredEmails.length} onCheckedChange={toggleSelectAll} className="mt-0.5" />

{currentView.charAt(0).toUpperCase() + currentView.slice(1).toLowerCase()}

{searchQuery ? `${filteredEmails.length} of ${emails.length} emails` : `${emails.length} emails`}
); // Update the bulk actions toolbar to include confirmation dialog const renderBulkActionsToolbar = () => { if (selectedEmails.length === 0) return null; return (
{selectedEmails.length} selected
); }; // Keep only one renderEmailListWrapper function that includes both panels const renderEmailListWrapper = () => (
{/* Email list panel */} {renderEmailList()} {/* Preview panel - will automatically take remaining space */}
{selectedEmail ? ( <> {/* Email actions header */}

{selectedEmail.subject}

{/* Scrollable content area */} {contentLoading ? (

Loading email content...

) : ( <>
{selectedEmail.fromName?.charAt(0) || selectedEmail.from?.charAt(0) || '?'}

{selectedEmail.fromName || 'Unknown'} {selectedEmail.from && <{selectedEmail.from}>}

to {selectedEmail.to || 'No recipients'}

{selectedEmail.cc && (

cc {selectedEmail.cc}

)}
{formatDate(new Date(selectedEmail.date))}
{renderEmailContent(selectedEmail)}
)}
) : (

Select an email to view its contents

)}
); // Update the email list item to safely display the fromName const renderEmailListItem = (email: Email) => (
handleEmailSelect(email.id)} >
toggleEmailSelection(email.id)} onClick={(e) => e.stopPropagation()} className="mt-0.5" />
{email.fromName || email.from || 'Unknown'} {!email.read && ( )}
{formatDate(new Date(email.date))}

{email.subject || '(No subject)'}

{email.starred && ( )} {email.attachments && email.attachments.length > 0 && ( )}
); const handleMailboxChange = async (newMailbox: string) => { setCurrentView(newMailbox); setSelectedEmails([]); setSearchQuery(''); setEmails([]); setLoading(true); setError(null); setHasMore(true); setPage(1); try { // Optimize the request by adding a timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout const response = await fetch(`/api/courrier?folder=${encodeURIComponent(newMailbox)}&page=1&limit=${emailsPerPage}`, { signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error('Failed to fetch emails'); } const data = await response.json(); // Process emails more efficiently const processedEmails = data.emails.map((email: any) => ({ id: Number(email.id), accountId: 1, from: email.from || '', fromName: email.fromName || email.from?.split('@')[0] || '', to: email.to || '', subject: email.subject || '(No subject)', body: email.body || '', date: email.date || new Date().toISOString(), read: email.read || false, starred: email.starred || false, folder: email.folder || newMailbox, cc: email.cc || '', bcc: email.bcc || '', flags: email.flags || [], raw: email.body || '' })); setEmails(processedEmails); setHasMore(processedEmails.length === emailsPerPage); // If folders are returned, update them if (data.folders && data.folders.length > 0) { setAvailableFolders(data.folders); // Update the mail account with folders setAccounts(prev => prev.map(account => account.id === 1 ? { ...account, folders: data.folders } : account )); } // Only update unread count if we're in the Inbox folder if (newMailbox === 'INBOX') { const unreadInboxEmails = processedEmails.filter( (email: Email) => !email.read && email.folder === 'INBOX' ).length; setUnreadCount(unreadInboxEmails); } } catch (error) { console.error('Error fetching emails:', error); setError(error instanceof Error ? error.message : 'Failed to fetch emails'); } finally { setLoading(false); } }; // Update the renderSidebarNav function to handle empty items or display other navigation options const renderSidebarNav = () => ( ); // Update handleReply to include body property for backward compatibility const handleReply = async (type: 'reply' | 'reply-all' | 'forward') => { if (!selectedEmail) return; try { // If content hasn't been loaded yet, fetch it if (!selectedEmail.contentFetched) { console.log('[DEBUG] Fetching email content for reply:', selectedEmail.id); // Use the API route instead of directly calling getEmailContent const response = await fetch(`/api/courrier/${selectedEmail.id}?folder=${encodeURIComponent(selectedEmail.folder || 'INBOX')}`); if (!response.ok) { throw new Error(`Failed to fetch email content: ${response.status}`); } const content = await response.json(); if (content) { // Update the selected email with content const updatedEmail = { ...selectedEmail, content: content.content || content.html || content.text || '', html: content.html || '', text: content.text || '', contentFetched: true, // Add proper from/to/cc format for client-side formatters from: typeof content.from === 'string' ? content.from : content.from?.[0]?.address || '', fromName: typeof content.from === 'string' ? '' : content.from?.[0]?.name || '', to: typeof content.to === 'string' ? content.to : content.to?.[0]?.address || '', cc: typeof content.cc === 'string' ? content.cc : content.cc?.[0]?.address || '', bcc: typeof content.bcc === 'string' ? content.bcc : content.bcc?.[0]?.address || '', date: typeof content.date === 'string' ? content.date : (content.date ? content.date.toString() : '') }; setSelectedEmail(updatedEmail); // Format content directly here formatEmailAndShowCompose(updatedEmail, type); } } else { // Content already loaded, format directly formatEmailAndShowCompose(selectedEmail, type); } } catch (error) { console.error('[DEBUG] Error preparing email for reply/forward:', error); } }; // New helper function to directly format email content const formatEmailAndShowCompose = (email: Email, type: 'reply' | 'reply-all' | 'forward') => { // Create an EmailMessage compatible object for the ComposeEmail component const emailForCompose: FormatterEmailMessage = { id: email.id, messageId: '', subject: email.subject, from: [{ name: email.fromName || email.from.split('@')[0] || '', address: email.from }], to: [{ name: '', address: email.to }], date: new Date(email.date), content: email.content, html: email.content, text: '', hasAttachments: email.attachments ? email.attachments.length > 0 : false, folder: email.folder }; // Use centralized formatters to ensure consistent formatting let formattedContent = ''; let formattedSubject = ''; let formattedTo = ''; if (type === 'reply' || type === 'reply-all') { const formatted = formatReplyEmail(emailForCompose, type); formattedContent = formatted.content; formattedSubject = formatted.subject; formattedTo = formatted.to; } else if (type === 'forward') { const formatted = formatForwardedEmail(emailForCompose); formattedContent = formatted.content; formattedSubject = formatted.subject; } // Set state for compose form setIsReplying(true); setIsForwarding(type === 'forward'); // Set original email content for LegacyAdapter setOriginalEmail({ content: formattedContent, type: type }); // Show compose form with formatted content setShowCompose(true); if (type === 'reply' || type === 'reply-all') { setComposeTo(formattedTo); setComposeSubject(formattedSubject); setComposeBody(formattedContent); } else if (type === 'forward') { setComposeTo(''); setComposeSubject(formattedSubject); setComposeBody(formattedContent); } }; // Update toggleStarred to use string IDs const toggleStarred = async (emailId: string, e?: React.MouseEvent) => { if (e) { e.stopPropagation(); } const email = emails.find(e => e.id === emailId); if (!email) return; try { const response = await fetch('/api/courrier/toggle-star', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ emailId, starred: !email.starred }), }); if (!response.ok) { throw new Error('Failed to toggle star'); } // Update email in state setEmails(emails.map(e => e.id === emailId ? { ...e, starred: !e.starred } : e )); } catch (error) { console.error('Error toggling star:', error); } }; // Add back the handleSend function const handleSend = async () => { if (!composeTo) { alert('Please specify at least one recipient'); return; } try { const response = await fetch('/api/courrier/send', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ to: composeTo, cc: composeCc, bcc: composeBcc, subject: composeSubject, body: composeBody, attachments: attachments, }), }); const data = await response.json(); if (!response.ok) { if (data.error === 'Attachment size limit exceeded') { alert(`Error: ${data.error}\nThe following files are too large:\n${data.details.oversizedFiles.join('\n')}`); } else { alert(`Error sending email: ${data.error}`); } return; } // Clear compose form and close modal setComposeTo(''); setComposeCc(''); setComposeBcc(''); setComposeSubject(''); setComposeBody(''); setAttachments([]); setShowCompose(false); } catch (error) { console.error('Error sending email:', error); alert('Failed to send email. Please try again.'); } }; // Add back the renderDeleteConfirmDialog function const renderDeleteConfirmDialog = () => ( Delete Emails Are you sure you want to delete {selectedEmails.length} selected email{selectedEmails.length > 1 ? 's' : ''}? This action cannot be undone. Cancel Delete ); const toggleEmailSelection = (emailId: string) => { setSelectedEmails((prev) => prev.includes(emailId) ? prev.filter((id) => id !== emailId) : [...prev, emailId] ); }; const searchEmails = (query: string) => { setSearchQuery(query.trim()); }; const handleSearchChange = (e: React.ChangeEvent) => { const query = e.target.value; setSearchQuery(query); }; const renderEmailPreview = (email: Email) => { if (!email) return null; return (

{email.subject}

{renderEmailContent(email)}
); }; // Update loadEmails to store the full content from the API response const loadEmails = async (isLoadMore = false) => { try { // Skip if already loading if (isLoadingInitial || isLoadingMore) { console.log('[DEBUG] Skipping loadEmails - already loading'); return; } console.log(`[DEBUG] Loading emails for folder: ${currentView}, page: ${page}, isLoadMore: ${isLoadMore}`); if (isLoadMore) { setIsLoadingMore(true); } else { setLoading(true); } // Fetch emails with timestamp for cache busting const timestamp = Date.now(); try { const response = await fetch( `/api/courrier?folder=${encodeURIComponent(currentView)}&page=${page}&limit=${emailsPerPage}&_t=${timestamp}`, { cache: 'no-store' } ); if (!response.ok) { console.error('[DEBUG] API response error:', response.status, response.statusText); throw new Error('Failed to load emails'); } const data = await response.json(); console.log('[DEBUG] API response:', { emailCount: data.emails?.length || 0, folderCount: data.folders?.length || 0, hasMore: data.hasMore, total: data.total }); // Set available folders if (data.folders) { console.log('[DEBUG] Setting available folders:', data.folders.length); setAvailableFolders(data.folders); // Update the mail account with folders setAccounts(prev => prev.map(account => account.id === 1 ? { ...account, folders: data.folders } : account )); } else { console.warn('[DEBUG] No folders returned from API'); } // Process and sort emails const processedEmails = (data.emails || []) .map((email: any) => { // Add proper handling for from field which might be an array or object let fromText = ''; let fromName = ''; let toText = ''; let ccText = ''; let bccText = ''; // Handle 'from' field if (email.from) { if (Array.isArray(email.from)) { if (email.from.length > 0) { if (typeof email.from[0] === 'object') { fromText = email.from[0].address || ''; fromName = email.from[0].name || email.from[0].address?.split('@')[0] || ''; } else { fromText = email.from[0] || ''; fromName = fromText.split('@')[0] || ''; } } } else if (typeof email.from === 'object') { fromText = email.from.address || ''; fromName = email.from.name || email.from.address?.split('@')[0] || ''; } else if (typeof email.from === 'string') { fromText = email.from; fromName = email.fromName || email.from.split('@')[0] || ''; } } // Handle 'to' field if (email.to) { if (Array.isArray(email.to)) { if (email.to.length > 0) { if (typeof email.to[0] === 'object') { toText = email.to.map((t: any) => t.address || '').join(', '); } else { toText = email.to.join(', '); } } } else if (typeof email.to === 'object') { toText = email.to.address || ''; } else if (typeof email.to === 'string') { toText = email.to; } } // Handle 'cc' field if (email.cc) { if (Array.isArray(email.cc)) { if (email.cc.length > 0) { if (typeof email.cc[0] === 'object') { ccText = email.cc.map((c: any) => c.address || '').join(', '); } else { ccText = email.cc.join(', '); } } } else if (typeof email.cc === 'object') { ccText = email.cc.address || ''; } else if (typeof email.cc === 'string') { ccText = email.cc; } } // Handle 'bcc' field if (email.bcc) { if (Array.isArray(email.bcc)) { if (email.bcc.length > 0) { if (typeof email.bcc[0] === 'object') { bccText = email.bcc.map((b: any) => b.address || '').join(', '); } else { bccText = email.bcc.join(', '); } } } else if (typeof email.bcc === 'object') { bccText = email.bcc.address || ''; } else if (typeof email.bcc === 'string') { bccText = email.bcc; } } return { id: email.id, accountId: 1, from: fromText, fromName: fromName, to: toText, subject: email.subject || '(No subject)', content: email.content || '', preview: email.preview || '', date: email.date || new Date().toISOString(), read: email.read || false, starred: email.starred || false, folder: email.folder || currentView, cc: ccText, bcc: bccText, flags: email.flags || [], hasAttachments: email.hasAttachments || false }; }) .sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime()); // Set emails appropriately console.log('[DEBUG] Setting emails state with', processedEmails.length, 'emails'); setEmails(prev => { if (isLoadMore) { // Filter out duplicates when appending const existingIds = new Set(prev.map(email => email.id)); const uniqueNewEmails = processedEmails.filter((email: Email) => !existingIds.has(email.id)); return [...prev, ...uniqueNewEmails]; } else { return processedEmails; } }); // Update unread count if (currentView === 'INBOX') { const unreadInboxEmails = processedEmails.filter( (email: Email) => !email.read && email.folder === 'INBOX' ).length; setUnreadCount(unreadInboxEmails); } setHasMore(data.hasMore); setError(null); } catch (err) { console.error('[DEBUG] Error in fetch emails:', err); setError('Failed to load emails'); throw err; // Rethrow to ensure the finally block still runs } } catch (err) { console.error('[DEBUG] Error loading emails:', err); setError('Failed to load emails'); } finally { console.log('[DEBUG] Setting loading states to false in loadEmails finally block'); // Ensure we reset the loading state after a short delay to make sure React has processed state updates setTimeout(() => { setLoading(false); setIsLoadingMore(false); setIsLoadingInitial(false); }, 100); } }; // Add back the view change effect useEffect(() => { setPage(1); // Reset page when view changes setHasMore(true); loadEmails(); }, [currentView]); // Create a function to load folders that will be available throughout the component const loadFolders = async () => { console.log('[DEBUG] Explicitly loading folders from standalone function...'); try { // Make a specific request just to get folders const timestamp = Date.now(); // Cache busting const response = await fetch(`/api/courrier?folder=INBOX&page=1&limit=1&skipCache=true&_t=${timestamp}`, { headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache' } }); if (response.ok) { const data = await response.json(); console.log('[DEBUG] Folder response data:', JSON.stringify(data, null, 2)); // Check for mailboxes field (from IMAP) and use it if available const folders = data.mailboxes || data.folders || []; if (folders && folders.length > 0) { console.log('[DEBUG] Successfully loaded folders:', folders); setAvailableFolders(folders); // Update the mail account with folders setAccounts(prev => { console.log('[DEBUG] Updating account with folders:', folders); return prev.map(account => account.id === 1 ? { ...account, folders: folders } : account ); }); } else { console.warn('[DEBUG] No folders found in response. Response data:', data); } } else { console.error('[DEBUG] Folder request failed:', response.status); } } catch (error) { console.error('[DEBUG] Error explicitly loading folders:', error); } }; // Improve the folder loading logic with a delay and better reliability useEffect(() => { let isMounted = true; // For cleanup // Only load if we don't have folders yet and we're not already loading if ((!accounts[1]?.folders || accounts[1]?.folders?.length === 0) && !loading) { console.log('[DEBUG] Triggering folder load in useEffect...'); // Set a small delay to ensure other loading operations have completed setTimeout(() => { if (!isMounted) return; loadFolders(); }, 500); // 500ms delay } return () => { isMounted = false; }; }, [accounts, loading]); // Add a new function to fetch the IMAP credentials email const fetchImapCredentials = async () => { try { console.log("[DEBUG] Fetching IMAP credentials..."); const response = await fetch("/api/courrier/credentials", { method: "GET", headers: { "Content-Type": "application/json", "Cache-Control": "no-cache" }, }); console.log("[DEBUG] IMAP credentials response status:", response.status); if (!response.ok) { const errorData = await response.json().catch(() => ({})); console.error("[DEBUG] Error fetching IMAP credentials:", response.status, errorData); throw new Error(`Failed to fetch IMAP credentials: ${response.status}`); } const data = await response.json(); console.log("[DEBUG] IMAP credentials data:", data); if (data && data.credentials && data.credentials.email) { console.log("[DEBUG] Setting account with IMAP email:", data.credentials.email); setAccounts(prev => prev.map(account => account.id === 1 ? { ...account, name: data.credentials.email, email: data.credentials.email } : account )); // After setting the account email, explicitly load folders setTimeout(() => { console.log("[DEBUG] Triggering folder load after setting account"); loadFolders(); }, 1000); } else { console.log("[DEBUG] No valid IMAP credentials found in response:", data); } } catch (error) { console.error("[DEBUG] Error in fetchImapCredentials:", error); } }; // Call it once on component mount useEffect(() => { fetchImapCredentials(); }, []); // Add ref for tracking component mount status const isComponentMounted = useRef(true); // Add a cleanup effect to reset states on unmount/remount useEffect(() => { // Reset isComponentMounted value on mount isComponentMounted.current = true; // Reset loading states when component mounts const timeoutId = setTimeout(() => { if (isComponentMounted.current && (loading || isLoadingInitial)) { console.log('[DEBUG] Reset loading states on mount'); setLoading(false); setIsLoadingInitial(false); } }, 5000); // Longer timeout to avoid race conditions // Return cleanup function return () => { console.log('[DEBUG] Component unmounting, clearing states'); isComponentMounted.current = false; clearTimeout(timeoutId); // No need to set states during unmount as component is gone }; }, []); // Empty dependency array if (error) { return (

{error}

); } if (isLoadingInitial && !emails.length) { return (

Loading your emails...

); } return ( <> {/* Main layout */}
{/* Sidebar */}
{/* Courrier Title */}
COURRIER
{/* Compose button and refresh button */}
{/* Accounts Section */}
{accountsDropdownOpen && (
{accounts.map(account => (
{/* Show folders for email accounts (not for "All" account) without the "Folders" header */} {account.id !== 0 && (
{account.folders && account.folders.length > 0 ? ( account.folders.map((folder) => ( )) ) : (
{/* Create placeholder folder items with shimmer effect */} {Array.from({ length: 5 }).map((_, index) => (
))}
)}
)}
))}
)}
{/* Navigation */} {renderSidebarNav()}
{/* Main content area */}
{/* Email list panel */} {renderEmailListWrapper()}
{/* Compose Email Modal */} { console.log('Email sent:', emailData); setShowCompose(false); setIsReplying(false); setIsForwarding(false); }} onCancel={() => { setShowCompose(false); setComposeTo(''); setComposeCc(''); setComposeBcc(''); setComposeSubject(''); setComposeBody(''); setShowCc(false); setShowBcc(false); setAttachments([]); setIsReplying(false); setIsForwarding(false); }} /> {renderDeleteConfirmDialog()} {/* Debug tools - only shown in development mode */} {process.env.NODE_ENV !== 'production' && ( )} ); }