import { useState, useEffect, useCallback, useRef } from 'react'; import { useToast } from './use-toast'; import { EmailMessage, EmailContent } from '@/types/email'; import { detectTextDirection } from '@/lib/utils/text-direction'; import { sanitizeHtml } from '@/lib/utils/email-utils'; interface EmailFetchState { email: EmailMessage | null; loading: boolean; error: string | null; } interface UseEmailFetchProps { onEmailLoaded?: (email: EmailMessage) => void; onError?: (error: string) => void; } export function useEmailFetch({ onEmailLoaded, onError }: UseEmailFetchProps = {}) { const [state, setState] = useState({ email: null, loading: false, error: null }); const abortControllerRef = useRef(null); const { toast } = useToast(); // Validate email fetch parameters const validateFetchParams = (emailId: string, accountId: string, folder: string) => { if (!emailId || typeof emailId !== 'string') { throw new Error('Invalid email ID'); } if (!accountId || typeof accountId !== 'string') { throw new Error('Invalid account ID'); } if (!folder || typeof folder !== 'string') { throw new Error('Invalid folder'); } // Validate UID format if (!/^\d+$/.test(emailId)) { throw new Error('Email ID must be a numeric UID'); } }; // Fetch email with proper error handling and cancellation const fetchEmail = useCallback(async (emailId: string, accountId: string, folder: string) => { if (!emailId || !accountId || !folder) { console.error('Missing required parameters for fetchEmail'); return; } // Abort any previous request if (abortControllerRef.current) { abortControllerRef.current.abort(); } abortControllerRef.current = new AbortController(); setState(prev => ({ ...prev, loading: true, error: null })); try { const response = await fetch( `/api/courrier/${emailId}?accountId=${encodeURIComponent(accountId)}&folder=${encodeURIComponent(folder)}`, { signal: abortControllerRef.current?.signal } ); if (!response.ok) { throw new Error(`Failed to fetch email: ${response.statusText}`); } const data = await response.json(); // Create a valid email message object with required fields const processContent = (data: any): EmailContent => { // Determine the text content - using all possible paths let textContent = ''; if (typeof data.content === 'string') { textContent = data.content; } else if (data.content?.text) { textContent = data.content.text; } else if (data.text) { textContent = data.text; } else if (data.plainText) { textContent = data.plainText; } // Determine the HTML content - using all possible paths let htmlContent = undefined; if (data.content?.html) { htmlContent = data.content.html; } else if (data.html) { htmlContent = data.html; } else if (typeof data.content === 'string' && data.content.includes('<')) { // If the content string appears to be HTML htmlContent = data.content; // We should still keep the text version, will be extracted if needed } // Clean HTML content if present if (htmlContent) { htmlContent = sanitizeHtml(htmlContent); } // Determine if content is HTML const isHtml = !!htmlContent; // Detect text direction const direction = data.content?.direction || detectTextDirection(textContent); return { text: textContent, html: htmlContent, isHtml, direction }; }; const transformedEmail: EmailMessage = { id: data.id || emailId, subject: data.subject || '', from: data.from || '', to: data.to || '', cc: data.cc, bcc: data.bcc, date: data.date || new Date().toISOString(), flags: Array.isArray(data.flags) ? data.flags : [], content: processContent(data), attachments: data.attachments }; console.log('Email processed:', transformedEmail.id, 'HTML:', !!transformedEmail.content.html, 'Text length:', transformedEmail.content.text.length); setState({ email: transformedEmail, loading: false, error: null }); onEmailLoaded?.(transformedEmail); // Mark as read if not already if (!transformedEmail.flags?.includes('seen')) { try { await fetch(`/api/courrier/${emailId}/mark-read`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'mark-read' }) }); } catch (err: any) { console.error('Error marking email as read:', err); } } } catch (err: any) { // Don't set error if request was aborted if (err.name === 'AbortError') { return; } console.error('Error fetching email:', err); const errorMessage = err instanceof Error ? err.message : 'Failed to load email'; setState(prev => ({ ...prev, loading: false, error: errorMessage })); onError?.(errorMessage); toast({ title: 'Error', description: errorMessage, variant: 'destructive' }); } }, [onEmailLoaded, onError, toast]); // Cleanup on unmount useEffect(() => { return () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, []); return { ...state, fetchEmail }; }