Neah/hooks/use-email-fetch.ts
2025-05-01 09:54:17 +02:00

186 lines
5.7 KiB
TypeScript

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/dom-sanitizer';
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<EmailFetchState>({
email: null,
loading: false,
error: null
});
const abortControllerRef = useRef<AbortController | null>(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
};
}