182 lines
5.6 KiB
TypeScript
182 lines
5.6 KiB
TypeScript
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
import { useToast } from './use-toast';
|
|
import { EmailMessage, EmailContent } from '@/types/email';
|
|
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<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();
|
|
|
|
// Process content based on what type it is
|
|
let processedContent: EmailContent;
|
|
|
|
if (data.content) {
|
|
if (typeof data.content === 'object') {
|
|
// If data.content is already an object, normalize it
|
|
processedContent = {
|
|
text: data.content.text || '',
|
|
html: data.content.html || undefined,
|
|
isHtml: !!data.content.html,
|
|
direction: data.content.direction || 'ltr'
|
|
};
|
|
} else if (typeof data.content === 'string') {
|
|
// If data.content is a string, determine if it's HTML
|
|
const isHtml = data.content.trim().startsWith('<') &&
|
|
(data.content.includes('<html') ||
|
|
data.content.includes('<body') ||
|
|
data.content.includes('<div') ||
|
|
data.content.includes('<p>'));
|
|
|
|
processedContent = {
|
|
text: isHtml ? data.content.replace(/<[^>]*>/g, '') : data.content,
|
|
html: isHtml ? sanitizeHtml(data.content) : undefined,
|
|
isHtml: isHtml,
|
|
direction: 'ltr'
|
|
};
|
|
} else {
|
|
// Fallback for any other case
|
|
processedContent = {
|
|
text: 'Unsupported content format',
|
|
html: undefined,
|
|
isHtml: false,
|
|
direction: 'ltr'
|
|
};
|
|
}
|
|
} else if (data.html || data.text) {
|
|
// Handle separate html/text properties
|
|
processedContent = {
|
|
text: data.text || (data.html ? data.html.replace(/<[^>]*>/g, '') : ''),
|
|
html: data.html ? sanitizeHtml(data.html) : undefined,
|
|
isHtml: !!data.html,
|
|
direction: 'ltr'
|
|
};
|
|
} else {
|
|
// No content at all
|
|
processedContent = {
|
|
text: 'No content available',
|
|
html: undefined,
|
|
isHtml: false,
|
|
direction: 'ltr'
|
|
};
|
|
}
|
|
|
|
// Create properly formatted email with all required fields
|
|
const transformedEmail: EmailMessage = {
|
|
...data,
|
|
content: processedContent
|
|
};
|
|
|
|
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) {
|
|
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
|
|
};
|
|
}
|