diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index c1b80b3a..0f5a4ba8 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -1,25 +1,11 @@ 'use client'; -import React from 'react'; -import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; +import React, { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; +import { Loader2, AlertCircle } from 'lucide-react'; +import { Dialog, DialogContent } from '@/components/ui/dialog'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; 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, @@ -29,2388 +15,335 @@ import { 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 components +import EmailSidebar from '@/components/email/EmailSidebar'; +import EmailList from '@/components/email/EmailList'; +import EmailContent from '@/components/email/EmailContent'; +import EmailHeader from '@/components/email/EmailHeader'; import ComposeEmail from '@/components/email/ComposeEmail'; -import { Attachment as MailParserAttachment } from 'mailparser'; -import { LoadingFix } from './loading-fix'; -// Import centralized email formatters -import { - formatForwardedEmail, - formatReplyEmail, - formatEmailForReplyOrForward, - EmailMessage as FormatterEmailMessage, - sanitizeHtml -} from '@/lib/utils/email-formatter'; +// Import the custom hook +import { useCourrier, EmailData } from '@/hooks/use-courrier'; -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/server/email-parser.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 using the centralized cleaner - const sanitizedHtml = sanitizeHtml(fullContent.content); - setContent( -
- ); - setDebugInfo('Rendered fetched HTML content with centralized formatter'); - 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 using the centralized cleaner - const sanitizedHtml = sanitizeHtml(formattedEmail); - setContent( -
- ); - setDebugInfo('Rendered existing HTML content with centralized formatter'); - } else { - // For plain text or complex formats, use the centralized formatter - const cleanedContent = sanitizeHtml(formattedEmail); - - // If it looks like HTML, render it as HTML - if (cleanedContent.includes('<') && cleanedContent.includes('>')) { - setContent( -
- ); - setDebugInfo('Rendered content as HTML using centralized formatter'); - } else { - // Otherwise, render as plain text - setContent( -
- {cleanedContent} -
- ); - setDebugInfo('Rendered content as text using centralized formatter'); - } - } - - 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... -
- ); +// Simplified version for this component +function SimplifiedLoadingFix() { + // In production, don't render anything + if (process.env.NODE_ENV === 'production') { + return null; } - - if (error) { - return ( -
-
{error}
- {debugInfo && ( -
- Debug info: {debugInfo} -
- )} -
- ); - } - + + // Simple debugging component 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)` : ''} - -
- ))} -
+
+ Debug: Email app loaded
); } -// 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) { - // Use sanitizeHtml to safely extract text from HTML - const cleanContent = sanitizeHtml(email.content); - const plainText = cleanContent.replace(/<[^>]*>/g, ' ').trim(); - if (mounted) { - setPreview(plainText.substring(0, 150) + '...'); - } - } else { - // Use the centralized cleaner instead of decodeEmail - const cleanContent = sanitizeHtml(email.content || ''); - const plainText = cleanContent.replace(/<[^>]*>/g, ' ').trim(); - - if (mounted) { - if (plainText) { - setPreview(plainText.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 ; -} - -// Add this interface before the CourrierPage component -interface EmailData { - to: string; - cc?: string; - bcc?: string; - subject: string; - body: string; - attachments?: Array<{ - name: string; - content: string; - type: string; - }>; -} - 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); + + // 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(); + + // Local state + const [showComposeModal, setShowComposeModal] = useState(false); + const [composeData, setComposeData] = useState(null); + const [composeType, setComposeType] = useState<'new' | 'reply' | 'reply-all' | 'forward'>('new'); 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); + const [showLoginNeeded, setShowLoginNeeded] = useState(false); + + // Check for more emails + const hasMoreEmails = page < totalPages; - // 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); + // Handle loading more emails on scroll + const handleLoadMore = () => { + if (hasMoreEmails && !isLoading) { + setPage(page + 1); } }; - // 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 + // Handle bulk actions 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; - } + 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); } - 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()} + break; -
- {/* 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); + case 'mark-unread': + // Mark all selected emails as unread + for (const emailId of selectedEmailIds) { + await markEmailAsRead(emailId, false); + } + break; - // 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); + case 'archive': + // Archive functionality would be implemented here + break; } }; - // 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') => { + // Handle email reply or forward + const handleReplyOrForward = (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 = ''; + const formattedEmail = formatEmailForAction(selectedEmail, type); + if (!formattedEmail) return; - 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 + setComposeData({ + to: formattedEmail.to, + cc: formattedEmail.cc, + subject: formattedEmail.subject, + body: formattedEmail.body, }); - // Show compose form with formatted content - setShowCompose(true); + setComposeType(type); + setShowComposeModal(true); + }; + + // Handle compose new email + const handleComposeNew = () => { + setComposeData({ + to: '', + subject: '', + body: '', + }); + setComposeType('new'); + setShowComposeModal(true); + }; + + // Handle sending email + const handleSend = async (emailData: EmailData) => { + await sendEmail(emailData); - if (type === 'reply' || type === 'reply-all') { - setComposeTo(formattedTo); - setComposeSubject(formattedSubject); - setComposeBody(formattedContent); - } else if (type === 'forward') { - setComposeTo(''); - setComposeSubject(formattedSubject); - setComposeBody(formattedContent); + setShowComposeModal(false); + setComposeData(null); + + // Refresh the Sent folder if we're currently viewing it + if (currentFolder.toLowerCase() === 'sent') { + loadEmails(); } }; - // Update toggleStarred to use string IDs - const toggleStarred = async (emailId: string, e?: React.MouseEvent) => { - if (e) { - e.stopPropagation(); - } + // Handle delete confirmation + const handleDeleteConfirm = async () => { + await deleteEmails(selectedEmailIds); + setShowDeleteConfirm(false); + }; - 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'); + // 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]); - // Update email in state - setEmails(emails.map(e => - e.id === emailId ? { ...e, starred: !e.starred } : e - )); - } catch (error) { - console.error('Error toggling star:', error); - } + // Go to login page + const handleGoToLogin = () => { + router.push('/courrier/login'); }; - // 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; + // If there's a critical error, show error dialog + if (error && !isLoading && emails.length === 0 && !showLoginNeeded) { 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}

- -
+
+ + + Error loading emails + {error} +
); } - if (isLoadingInitial && !emails.length) { - return ( -
-
-
-
-

Loading your emails...

-
-
-
- ); - } + // Create a properly formatted email message from composeData for the ComposeEmail component + const createEmailMessage = () => { + if (!composeData) return null; + + return { + id: 'temp-id', + messageId: '', + subject: composeData.subject || '', + from: [{ name: '', address: '' }], + to: [{ name: '', address: composeData.to || '' }], + cc: composeData.cc ? [{ name: '', address: composeData.cc }] : [], + bcc: composeData.bcc ? [{ name: '', address: composeData.bcc }] : [], + date: new Date(), + content: composeData.body || '', + html: composeData.body || '', + hasAttachments: false + }; + }; return ( - <> - {/* Main layout */} -
-
-
- {/* Sidebar */} -
- {/* Courrier Title */} -
-
- - COURRIER +
+ + + {/* Login required dialog */} + + + + Login Required + + You need to configure your email account credentials before you can access your emails. + + + + Cancel + Setup Email + + + + + {/* Delete confirmation dialog */} + + + + Confirm Deletion + + Are you sure you want to delete {selectedEmailIds.length} {selectedEmailIds.length === 1 ? 'email' : 'emails'}? + This action cannot be undone. + + + + Cancel + + Delete + + + + + + {/* Compose email dialog */} + + + {/* Using modern props format for ComposeEmail */} + setShowComposeModal(false)} + onSend={handleSend} + /> + + + + {/* Main email interface */} +
+ + +
+ loadEmails(false)} + onCompose={handleComposeNew} + isLoading={isLoading} + /> + +
+
+ +
+ + {selectedEmail && ( +
+
+
+

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

+
+ From: {selectedEmail.from?.[0]?.name || selectedEmail.from?.[0]?.address || 'Unknown'} +
+
+ +
+ + + +
+
+ +
+
- - {/* 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' && ( - - )} - +
+
); -} \ No newline at end of file +} \ No newline at end of file diff --git a/components/email/BulkActionsToolbar.tsx b/components/email/BulkActionsToolbar.tsx new file mode 100644 index 00000000..1a9b3701 --- /dev/null +++ b/components/email/BulkActionsToolbar.tsx @@ -0,0 +1,93 @@ +'use client'; + +import React from 'react'; +import { Trash, Mail, MailOpen, Archive } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from '@/components/ui/tooltip'; + +interface BulkActionsToolbarProps { + selectedCount: number; + onBulkAction: (action: 'delete' | 'mark-read' | 'mark-unread' | 'archive') => void; +} + +export default function BulkActionsToolbar({ + selectedCount, + onBulkAction +}: BulkActionsToolbarProps) { + return ( +
+
+ {selectedCount} {selectedCount === 1 ? 'message' : 'messages'} selected +
+ + + + + + + Mark as read + + + + + + + + + Mark as unread + + + + + + + + + Archive + + + + + + + + + Delete + + +
+ ); +} \ No newline at end of file diff --git a/components/email/EmailContent.tsx b/components/email/EmailContent.tsx new file mode 100644 index 00000000..907ebd9a --- /dev/null +++ b/components/email/EmailContent.tsx @@ -0,0 +1,114 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { Loader2, Paperclip, FileDown } from 'lucide-react'; +import { sanitizeHtml } from '@/lib/utils/email-formatter'; +import { Button } from '@/components/ui/button'; +import { Email } from '@/hooks/use-courrier'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; + +interface EmailContentProps { + email: Email; +} + +export default function EmailContent({ email }: EmailContentProps) { + const [content, setContent] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!email) return; + + const renderContent = async () => { + setIsLoading(true); + setError(null); + + try { + if (!email.content || email.content.length === 0) { + setContent(
Email content is empty
); + return; + } + + // Use the sanitizer from the centralized formatter + const sanitizedHtml = sanitizeHtml(email.content); + + setContent( +
+ ); + } catch (err) { + console.error('Error rendering email content:', err); + setError('Error rendering email content. Please try again.'); + setContent(null); + } finally { + setIsLoading(false); + } + }; + + renderContent(); + }, [email]); + + // Render attachments if they exist + const renderAttachments = () => { + if (!email?.attachments || email.attachments.length === 0) { + return null; + } + + return ( +
+

+ + Attachments ({email.attachments.length}) +

+
+ {email.attachments.map((attachment, index) => ( + + ))} +
+
+ ); + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( + + Error + {error} + + ); + } + + return ( +
+ {content} + {renderAttachments()} +
+ ); +} \ No newline at end of file diff --git a/components/email/EmailHeader.tsx b/components/email/EmailHeader.tsx new file mode 100644 index 00000000..114cc2c1 --- /dev/null +++ b/components/email/EmailHeader.tsx @@ -0,0 +1,109 @@ +'use client'; + +import React, { useState } from 'react'; +import { Search, X, Settings } from 'lucide-react'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from '@/components/ui/tooltip'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; + +interface EmailHeaderProps { + onSearch: (query: string) => void; + onSettingsClick?: () => void; +} + +export default function EmailHeader({ + onSearch, + onSettingsClick, +}: EmailHeaderProps) { + const [searchQuery, setSearchQuery] = useState(''); + const [isSearching, setIsSearching] = useState(false); + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + onSearch(searchQuery); + }; + + const clearSearch = () => { + setSearchQuery(''); + onSearch(''); + }; + + return ( +
+
+
+ + setSearchQuery(e.target.value)} + className="pl-8 pr-8 h-9" + /> + {searchQuery && ( + + )} + +
+ +
+ + + + + + Search + + + + + + + + + + + + Settings + + + + + Email settings + + + Configure IMAP + + + +
+
+ ); +} \ No newline at end of file diff --git a/components/email/EmailList.tsx b/components/email/EmailList.tsx new file mode 100644 index 00000000..44457b2a --- /dev/null +++ b/components/email/EmailList.tsx @@ -0,0 +1,130 @@ +'use client'; + +import React, { useState } from 'react'; +import { Loader2 } from 'lucide-react'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Email } from '@/hooks/use-courrier'; +import EmailListItem from './EmailListItem'; +import EmailListHeader from './EmailListHeader'; +import BulkActionsToolbar from './BulkActionsToolbar'; + +interface EmailListProps { + emails: Email[]; + selectedEmailIds: string[]; + selectedEmail: Email | null; + currentFolder: string; + isLoading: boolean; + totalEmails: number; + hasMoreEmails: boolean; + onSelectEmail: (emailId: string) => void; + onToggleSelect: (emailId: string) => void; + onToggleSelectAll: () => void; + onBulkAction: (action: 'delete' | 'mark-read' | 'mark-unread' | 'archive') => void; + onToggleStarred: (emailId: string) => void; + onLoadMore: () => void; +} + +export default function EmailList({ + emails, + selectedEmailIds, + selectedEmail, + currentFolder, + isLoading, + totalEmails, + hasMoreEmails, + onSelectEmail, + onToggleSelect, + onToggleSelectAll, + onBulkAction, + onToggleStarred, + onLoadMore +}: EmailListProps) { + const [scrollPosition, setScrollPosition] = useState(0); + + // Handle scroll to detect when user reaches the bottom + const handleScroll = (event: React.UIEvent) => { + const target = event.target as HTMLDivElement; + const { scrollTop, scrollHeight, clientHeight } = target; + + setScrollPosition(scrollTop); + + // If user scrolls near the bottom and we have more emails, load more + if (scrollHeight - scrollTop - clientHeight < 200 && hasMoreEmails && !isLoading) { + onLoadMore(); + } + }; + + // Render loading state + if (isLoading && emails.length === 0) { + return ( +
+ +
+ ); + } + + // Render empty state + if (emails.length === 0) { + return ( +
+

No emails found

+

+ {currentFolder === 'INBOX' + ? "Your inbox is empty. You're all caught up!" + : `The ${currentFolder} folder is empty.`} +

+
+ ); + } + + // Are all emails selected + const allSelected = selectedEmailIds.length === emails.length && emails.length > 0; + + // Are some (but not all) emails selected + const someSelected = selectedEmailIds.length > 0 && selectedEmailIds.length < emails.length; + + return ( +
+ + + {selectedEmailIds.length > 0 && ( + + )} + + +
+ {emails.map((email) => ( + onSelectEmail(email.id)} + onToggleSelect={(e) => { + e.stopPropagation(); + onToggleSelect(email.id); + }} + onToggleStarred={(e) => { + e.stopPropagation(); + onToggleStarred(email.id); + }} + /> + ))} + + {isLoading && emails.length > 0 && ( +
+ +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/email/EmailListHeader.tsx b/components/email/EmailListHeader.tsx new file mode 100644 index 00000000..40afad05 --- /dev/null +++ b/components/email/EmailListHeader.tsx @@ -0,0 +1,60 @@ +'use client'; + +import React from 'react'; +import { ChevronDown } from 'lucide-react'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; + +interface EmailListHeaderProps { + allSelected: boolean; + someSelected: boolean; + onToggleSelectAll: () => void; +} + +export default function EmailListHeader({ + allSelected, + someSelected, + onToggleSelectAll, +}: EmailListHeaderProps) { + return ( +
+
+ { + if (input) { + (input as unknown as HTMLInputElement).indeterminate = someSelected && !allSelected; + } + }} + onClick={onToggleSelectAll} + /> + + + + + + + + {allSelected ? 'Unselect all' : 'Select all'} + + + Unselect all + + + +
+ +
+ Select messages to perform actions +
+
+ ); +} \ No newline at end of file diff --git a/components/email/EmailListItem.tsx b/components/email/EmailListItem.tsx new file mode 100644 index 00000000..7b1467f2 --- /dev/null +++ b/components/email/EmailListItem.tsx @@ -0,0 +1,158 @@ +'use client'; + +import React from 'react'; +import { Star, Mail, MailOpen } from 'lucide-react'; +import { Checkbox } from '@/components/ui/checkbox'; +import { cn } from '@/lib/utils'; +import { Email } from '@/hooks/use-courrier'; +import { Badge } from '@/components/ui/badge'; +import { Avatar, AvatarFallback } from '@/components/ui/avatar'; + +interface EmailListItemProps { + email: Email; + isSelected: boolean; + isActive: boolean; + onSelect: () => void; + onToggleSelect: (e: React.MouseEvent) => void; + onToggleStarred: (e: React.MouseEvent) => void; +} + +export default function EmailListItem({ + email, + isSelected, + isActive, + onSelect, + onToggleSelect, + onToggleStarred +}: EmailListItemProps) { + // Format the date in a readable way + const formatDate = (dateString: string) => { + const date = new Date(dateString); + const now = new Date(); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + + // Check if date is today + if (date >= today) { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } + + // Check if date is yesterday + if (date >= yesterday) { + return 'Yesterday'; + } + + // Check if date is this year + if (date.getFullYear() === now.getFullYear()) { + return date.toLocaleDateString([], { month: 'short', day: 'numeric' }); + } + + // Date is from a previous year + return date.toLocaleDateString([], { year: 'numeric', month: 'short', day: 'numeric' }); + }; + + // Get the first letter of the sender's name or email for the avatar + const getSenderInitial = () => { + if (!email.from || email.from.length === 0) return '?'; + + const sender = email.from[0]; + if (sender.name && sender.name.trim()) { + return sender.name.trim()[0].toUpperCase(); + } + + if (sender.address && sender.address.trim()) { + return sender.address.trim()[0].toUpperCase(); + } + + return '?'; + }; + + // Get sender name or email + const getSenderName = () => { + if (!email.from || email.from.length === 0) return 'Unknown'; + + const sender = email.from[0]; + if (sender.name && sender.name.trim()) { + return sender.name.trim(); + } + + return sender.address || 'Unknown'; + }; + + // Generate a stable color based on the sender's email + const getAvatarColor = () => { + if (!email.from || email.from.length === 0) return 'hsl(0, 0%, 50%)'; + + const address = email.from[0].address || ''; + let hash = 0; + + for (let i = 0; i < address.length; i++) { + hash = address.charCodeAt(i) + ((hash << 5) - hash); + } + + const h = hash % 360; + return `hsl(${h}, 70%, 80%)`; + }; + + return ( +
+
+ +
+ +
+
+ + + {getSenderInitial()} + + +
+
+
+ {getSenderName()} +
+
+ {formatDate(email.date)} +
+
+ +
+
+ {email.subject || '(No subject)'} +
+ {email.hasAttachments && ( + 📎 + )} +
+ +
+ {email.preview || 'No preview available'} +
+
+ + {!email.read && ( +
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/components/email/EmailSidebar.tsx b/components/email/EmailSidebar.tsx new file mode 100644 index 00000000..8252a164 --- /dev/null +++ b/components/email/EmailSidebar.tsx @@ -0,0 +1,140 @@ +'use client'; + +import React from 'react'; +import { + Inbox, Send, Trash, Archive, Star, + File, RefreshCw, Plus, MailOpen +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Badge } from '@/components/ui/badge'; + +interface EmailSidebarProps { + currentFolder: string; + folders: string[]; + onFolderChange: (folder: string) => void; + onRefresh: () => void; + onCompose: () => void; + isLoading: boolean; +} + +export default function EmailSidebar({ + currentFolder, + folders, + onFolderChange, + onRefresh, + onCompose, + isLoading +}: EmailSidebarProps) { + // Get the appropriate icon for a folder + const getFolderIcon = (folder: string) => { + const folderLower = folder.toLowerCase(); + + switch (folderLower) { + case 'inbox': + return ; + case 'sent': + case 'sent items': + return ; + case 'drafts': + return ; + case 'trash': + case 'deleted': + case 'bin': + return ; + case 'archive': + case 'archived': + return ; + case 'starred': + case 'important': + return ; + default: + return ; + } + }; + + // Group folders into standard and custom + const standardFolders = ['INBOX', 'Sent', 'Drafts', 'Trash', 'Archive', 'Junk']; + const visibleStandardFolders = standardFolders.filter(f => + folders.includes(f) || folders.some(folder => folder.toLowerCase() === f.toLowerCase()) + ); + + const customFolders = folders.filter(f => + !standardFolders.some(sf => sf.toLowerCase() === f.toLowerCase()) + ); + + return ( + + ); +} \ No newline at end of file diff --git a/hooks/use-courrier.ts b/hooks/use-courrier.ts new file mode 100644 index 00000000..0ca12a97 --- /dev/null +++ b/hooks/use-courrier.ts @@ -0,0 +1,466 @@ +import { useState, useCallback, useEffect } from 'react'; +import { useSession } from 'next-auth/react'; +import { useToast } from './use-toast'; +import { formatEmailForReplyOrForward } from '@/lib/utils/email-formatter'; + +export interface EmailAddress { + name: string; + address: string; +} + +export interface Email { + id: string; + from: EmailAddress[]; + to: EmailAddress[]; + cc?: EmailAddress[]; + bcc?: EmailAddress[]; + subject: string; + content: string; + preview?: string; + date: string; + read: boolean; + starred: boolean; + attachments?: { filename: string; contentType: string; size: number; content?: string }[]; + folder: string; + hasAttachments: boolean; + contentFetched?: boolean; +} + +export interface EmailListResult { + emails: Email[]; + totalEmails: number; + page: number; + perPage: number; + totalPages: number; + folder: string; + mailboxes: string[]; +} + +export interface EmailData { + to: string; + cc?: string; + bcc?: string; + subject: string; + body: string; + attachments?: Array<{ + name: string; + content: string; + type: string; + }>; +} + +export type MailFolder = string; + +// Hook for managing email operations +export const useCourrier = () => { + // State for email data + const [emails, setEmails] = useState([]); + const [selectedEmail, setSelectedEmail] = useState(null); + const [selectedEmailIds, setSelectedEmailIds] = useState([]); + const [currentFolder, setCurrentFolder] = useState('INBOX'); + const [mailboxes, setMailboxes] = useState([]); + + // State for UI + const [isLoading, setIsLoading] = useState(false); + const [isSending, setIsSending] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(20); + const [totalEmails, setTotalEmails] = useState(0); + const [totalPages, setTotalPages] = useState(0); + + // Auth and notifications + const { data: session } = useSession(); + const { toast } = useToast(); + + // Load emails when folder or page changes + useEffect(() => { + if (session?.user?.id) { + loadEmails(); + } + }, [currentFolder, page, perPage, session?.user?.id]); + + // Load emails from the server + const loadEmails = useCallback(async (isLoadMore = false) => { + if (!session?.user?.id) return; + + setIsLoading(true); + setError(null); + + try { + // Build query params + const queryParams = new URLSearchParams({ + folder: currentFolder, + page: page.toString(), + perPage: perPage.toString() + }); + + if (searchQuery) { + queryParams.set('search', searchQuery); + } + + // Fetch emails from API + const response = await fetch(`/api/courrier?${queryParams.toString()}`); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to fetch emails'); + } + + const data: EmailListResult = await response.json(); + + // Update state with the fetched data + if (isLoadMore) { + setEmails(prev => [...prev, ...data.emails]); + } else { + setEmails(data.emails); + } + + setTotalEmails(data.totalEmails); + setTotalPages(data.totalPages); + + // Update available mailboxes if provided + if (data.mailboxes && data.mailboxes.length > 0) { + setMailboxes(data.mailboxes); + } + + // Clear selection if not loading more + if (!isLoadMore) { + setSelectedEmail(null); + setSelectedEmailIds([]); + } + } catch (err) { + console.error('Error loading emails:', err); + setError(err instanceof Error ? err.message : 'Failed to load emails'); + toast({ + variant: "destructive", + title: "Error", + description: err instanceof Error ? err.message : 'Failed to load emails' + }); + } finally { + setIsLoading(false); + } + }, [currentFolder, page, perPage, searchQuery, session?.user?.id, toast]); + + // Fetch a single email's content + const fetchEmailContent = useCallback(async (emailId: string) => { + try { + const response = await fetch(`/api/courrier/${emailId}?folder=${encodeURIComponent(currentFolder)}`); + + if (!response.ok) { + throw new Error(`Failed to fetch email content: ${response.status}`); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error('Error fetching email content:', error); + throw error; + } + }, [currentFolder]); + + // Select an email to view + const handleEmailSelect = useCallback(async (emailId: string) => { + setIsLoading(true); + + try { + // Find the email in the current list + const email = emails.find(e => e.id === emailId); + + if (!email) { + throw new Error('Email not found'); + } + + // If content is not fetched, get the full content + if (!email.contentFetched) { + const fullEmail = await fetchEmailContent(emailId); + + // Merge the full content with the email + const updatedEmail = { + ...email, + content: fullEmail.content, + attachments: fullEmail.attachments, + contentFetched: true + }; + + // Update the email in the list + setEmails(emails.map(e => e.id === emailId ? updatedEmail : e)); + setSelectedEmail(updatedEmail); + } else { + setSelectedEmail(email); + } + + // Mark the email as read if it's not already + if (!email.read) { + markEmailAsRead(emailId, true); + } + } catch (err) { + console.error('Error selecting email:', err); + toast({ + variant: "destructive", + title: "Error", + description: "Could not load email content" + }); + } finally { + setIsLoading(false); + } + }, [emails, fetchEmailContent, toast]); + + // Mark an email as read/unread + const markEmailAsRead = useCallback(async (emailId: string, isRead: boolean) => { + try { + const response = await fetch(`/api/courrier/${emailId}/mark-read`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + isRead, + folder: currentFolder + }) + }); + + if (!response.ok) { + throw new Error('Failed to mark email as read'); + } + + // Update the email in the list + setEmails(emails.map(email => + email.id === emailId ? { ...email, read: isRead } : email + )); + + // If the selected email is the one being marked, update it too + if (selectedEmail && selectedEmail.id === emailId) { + setSelectedEmail({ ...selectedEmail, read: isRead }); + } + + return true; + } catch (error) { + console.error('Error marking email as read:', error); + return false; + } + }, [emails, selectedEmail, currentFolder]); + + // Toggle starred status for an email + const toggleStarred = useCallback(async (emailId: string) => { + const email = emails.find(e => e.id === emailId); + if (!email) return; + + const newStarredStatus = !email.starred; + + try { + const response = await fetch(`/api/courrier/${emailId}/star`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + starred: newStarredStatus, + folder: currentFolder + }) + }); + + if (!response.ok) { + throw new Error('Failed to toggle star status'); + } + + // Update the email in the list + setEmails(emails.map(email => + email.id === emailId ? { ...email, starred: newStarredStatus } : email + )); + + // If the selected email is the one being starred, update it too + if (selectedEmail && selectedEmail.id === emailId) { + setSelectedEmail({ ...selectedEmail, starred: newStarredStatus }); + } + } catch (error) { + console.error('Error toggling star status:', error); + toast({ + variant: "destructive", + title: "Error", + description: "Could not update star status" + }); + } + }, [emails, selectedEmail, currentFolder, toast]); + + // Send an email + const sendEmail = useCallback(async (emailData: EmailData) => { + if (!session?.user?.id) { + toast({ + variant: "destructive", + title: "Error", + description: "You must be logged in to send emails" + }); + return { success: false, error: "Not authenticated" }; + } + + setIsSending(true); + + try { + const response = await fetch('/api/courrier/send', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(emailData) + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.error || 'Failed to send email'); + } + + toast({ + title: "Success", + description: "Email sent successfully" + }); + + return { success: true, messageId: result.messageId }; + } catch (error) { + console.error('Error sending email:', error); + + toast({ + variant: "destructive", + title: "Error", + description: error instanceof Error ? error.message : 'Failed to send email' + }); + + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to send email' + }; + } finally { + setIsSending(false); + } + }, [session?.user?.id, toast]); + + // Delete selected emails + const deleteEmails = useCallback(async (emailIds: string[]) => { + if (emailIds.length === 0) return; + + setIsDeleting(true); + + try { + const response = await fetch('/api/courrier/delete', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + emailIds, + folder: currentFolder + }) + }); + + if (!response.ok) { + throw new Error('Failed to delete emails'); + } + + // Remove the deleted emails from the list + setEmails(emails.filter(email => !emailIds.includes(email.id))); + + // Clear selection if the selected email was deleted + if (selectedEmail && emailIds.includes(selectedEmail.id)) { + setSelectedEmail(null); + } + + // Clear selected IDs + setSelectedEmailIds([]); + + toast({ + title: "Success", + description: `${emailIds.length} email(s) deleted` + }); + } catch (error) { + console.error('Error deleting emails:', error); + + toast({ + variant: "destructive", + title: "Error", + description: "Failed to delete emails" + }); + } finally { + setIsDeleting(false); + } + }, [emails, selectedEmail, currentFolder, toast]); + + // Toggle selection of an email + const toggleEmailSelection = useCallback((emailId: string) => { + setSelectedEmailIds(prev => { + if (prev.includes(emailId)) { + return prev.filter(id => id !== emailId); + } else { + return [...prev, emailId]; + } + }); + }, []); + + // Select all emails + const toggleSelectAll = useCallback(() => { + if (selectedEmailIds.length === emails.length) { + setSelectedEmailIds([]); + } else { + setSelectedEmailIds(emails.map(email => email.id)); + } + }, [emails, selectedEmailIds]); + + // Change the current folder + const changeFolder = useCallback((folder: MailFolder) => { + setCurrentFolder(folder); + setPage(1); + setSelectedEmail(null); + setSelectedEmailIds([]); + }, []); + + // Search emails + const searchEmails = useCallback((query: string) => { + setSearchQuery(query); + setPage(1); + loadEmails(); + }, [loadEmails]); + + // Format an email for reply or forward + const formatEmailForAction = useCallback((email: Email, type: 'reply' | 'reply-all' | 'forward') => { + if (!email) return null; + + return formatEmailForReplyOrForward(email, type); + }, []); + + // Return all the functionality and state values + return { + // Data + emails, + selectedEmail, + selectedEmailIds, + currentFolder, + mailboxes, + isLoading, + isSending, + isDeleting, + error, + searchQuery, + page, + perPage, + totalEmails, + totalPages, + + // Functions + loadEmails, + handleEmailSelect, + markEmailAsRead, + toggleStarred, + sendEmail, + deleteEmails, + toggleEmailSelection, + toggleSelectAll, + changeFolder, + searchEmails, + formatEmailForAction, + setPage, + setPerPage, + setSearchQuery, + }; +}; \ No newline at end of file