courrier multi account restore compose
This commit is contained in:
parent
f05a982a3a
commit
9fa4f807eb
@ -1,10 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import EmailPreview from './EmailPreview';
|
||||
import ComposeEmail from './ComposeEmail';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { formatReplyEmail, EmailMessage as FormatterEmailMessage } from '@/lib/utils/email-formatter';
|
||||
import { useEmailFetch } from '@/hooks/use-email-fetch';
|
||||
import { debounce } from '@/lib/utils/debounce';
|
||||
|
||||
// Add local EmailMessage interface
|
||||
interface EmailAddress {
|
||||
@ -40,28 +42,28 @@ interface EmailMessage {
|
||||
}
|
||||
|
||||
interface EmailPanelProps {
|
||||
selectedEmail: { emailId: string; accountId: string; folder: string } | null;
|
||||
onSendEmail: (emailData: {
|
||||
to: string;
|
||||
cc?: string;
|
||||
bcc?: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
attachments?: Array<{
|
||||
name: string;
|
||||
content: string;
|
||||
type: string;
|
||||
}>;
|
||||
}) => Promise<void>;
|
||||
selectedEmail: {
|
||||
emailId: string;
|
||||
accountId: string;
|
||||
folder: string;
|
||||
} | null;
|
||||
onSendEmail: (email: any) => void;
|
||||
}
|
||||
|
||||
export default function EmailPanel({
|
||||
selectedEmail,
|
||||
onSendEmail
|
||||
}: EmailPanelProps) {
|
||||
const [email, setEmail] = useState<EmailMessage | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// Use the new email fetch hook
|
||||
const { email, loading, error, fetchEmail } = useEmailFetch({
|
||||
onEmailLoaded: (email) => {
|
||||
// Handle any post-load actions
|
||||
console.log('Email loaded:', email.id);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error loading email:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Compose mode state
|
||||
const [isComposing, setIsComposing] = useState<boolean>(false);
|
||||
@ -111,57 +113,21 @@ export default function EmailPanel({
|
||||
}
|
||||
}, [email]);
|
||||
|
||||
// Debounced email fetch
|
||||
const debouncedFetchEmail = useCallback(
|
||||
debounce((emailId: string, accountId: string, folder: string) => {
|
||||
fetchEmail(emailId, accountId, folder);
|
||||
}, 300),
|
||||
[fetchEmail]
|
||||
);
|
||||
|
||||
// Load email content when selectedEmail changes
|
||||
useEffect(() => {
|
||||
if (selectedEmail) {
|
||||
fetchEmail(selectedEmail.emailId, selectedEmail.accountId, selectedEmail.folder);
|
||||
debouncedFetchEmail(selectedEmail.emailId, selectedEmail.accountId, selectedEmail.folder);
|
||||
setIsComposing(false);
|
||||
} else {
|
||||
setEmail(null);
|
||||
}
|
||||
}, [selectedEmail]);
|
||||
|
||||
// Fetch the email content
|
||||
const fetchEmail = async (emailId: string, accountId: string, folder: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch(`/api/courrier/${emailId}?accountId=${encodeURIComponent(accountId)}&folder=${encodeURIComponent(folder)}`);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Failed to fetch email');
|
||||
}
|
||||
const data = await response.json();
|
||||
if (!data) {
|
||||
throw new Error('Email not found');
|
||||
}
|
||||
// Mark as read if not already
|
||||
if (!data.flags?.seen) {
|
||||
markAsRead(emailId);
|
||||
}
|
||||
setEmail(data);
|
||||
} catch (err) {
|
||||
console.error('Error fetching email:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load email');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Mark email as read
|
||||
const markAsRead = async (id: string) => {
|
||||
try {
|
||||
await fetch(`/api/courrier/${id}/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);
|
||||
}
|
||||
};
|
||||
}, [selectedEmail, debouncedFetchEmail]);
|
||||
|
||||
// Handle reply/forward actions
|
||||
const handleReply = (type: 'reply' | 'reply-all' | 'forward') => {
|
||||
@ -174,7 +140,7 @@ export default function EmailPanel({
|
||||
setIsComposing(false);
|
||||
setComposeType('new');
|
||||
};
|
||||
|
||||
|
||||
// If no email is selected and not composing
|
||||
if (!selectedEmail && !isComposing) {
|
||||
return (
|
||||
@ -227,7 +193,6 @@ export default function EmailPanel({
|
||||
// Show compose mode or email preview
|
||||
return (
|
||||
<div className="h-full">
|
||||
{/* Remove ComposeEmail overlay/modal logic. Only show preview or a button to trigger compose in parent. */}
|
||||
{isComposing ? (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="text-center text-muted-foreground">
|
||||
|
||||
126
hooks/use-email-fetch.ts
Normal file
126
hooks/use-email-fetch.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useToast } from './use-toast';
|
||||
|
||||
interface EmailFetchState {
|
||||
email: any | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface UseEmailFetchProps {
|
||||
onEmailLoaded?: (email: any) => 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) => {
|
||||
try {
|
||||
// Cancel any in-flight request
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
// Create new abort controller
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
// Validate parameters
|
||||
validateFetchParams(emailId, accountId, folder);
|
||||
|
||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
const response = await fetch(
|
||||
`/api/courrier/${emailId}?accountId=${encodeURIComponent(accountId)}&folder=${encodeURIComponent(folder)}`,
|
||||
{
|
||||
signal: abortControllerRef.current.signal
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Failed to fetch email');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data) {
|
||||
throw new Error('Email not found');
|
||||
}
|
||||
|
||||
setState({ email: data, loading: false, error: null });
|
||||
onEmailLoaded?.(data);
|
||||
|
||||
// Mark as read if not already
|
||||
if (!data.flags?.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) {
|
||||
// Don't set error if request was aborted
|
||||
if (err.name === 'AbortError') {
|
||||
return;
|
||||
}
|
||||
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to load email';
|
||||
setState(prev => ({ ...prev, loading: false, error: errorMessage }));
|
||||
onError?.(errorMessage);
|
||||
|
||||
// Show toast for user feedback
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: errorMessage,
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
}, [onEmailLoaded, onError, toast]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...state,
|
||||
fetchEmail
|
||||
};
|
||||
}
|
||||
@ -400,6 +400,22 @@ export async function getEmailContent(
|
||||
folder: string = 'INBOX',
|
||||
accountId?: string
|
||||
): Promise<EmailMessage> {
|
||||
// Validate parameters
|
||||
if (!userId || !emailId || !folder) {
|
||||
throw new Error('Missing required parameters');
|
||||
}
|
||||
|
||||
// Validate UID format
|
||||
if (!/^\d+$/.test(emailId)) {
|
||||
throw new Error('Invalid email ID format: must be a numeric UID');
|
||||
}
|
||||
|
||||
// Convert to number for IMAP
|
||||
const numericId = parseInt(emailId, 10);
|
||||
if (isNaN(numericId)) {
|
||||
throw new Error('Email ID must be a number');
|
||||
}
|
||||
|
||||
// Try to get from cache first
|
||||
const cachedEmail = await getCachedEmailContent(userId, accountId || folder, emailId);
|
||||
if (cachedEmail) {
|
||||
@ -414,9 +430,25 @@ export async function getEmailContent(
|
||||
try {
|
||||
// Remove accountId prefix if present
|
||||
const actualFolder = folder.includes(':') ? folder.split(':')[1] : folder;
|
||||
await client.mailboxOpen(actualFolder);
|
||||
|
||||
const message = await client.fetchOne(emailId, {
|
||||
// Log connection details
|
||||
console.log(`[DEBUG] Fetching email ${emailId} from folder ${actualFolder} for account ${accountId}`);
|
||||
|
||||
// Open mailbox with error handling
|
||||
const mailbox = await client.mailboxOpen(actualFolder);
|
||||
if (!mailbox || typeof mailbox === 'boolean') {
|
||||
throw new Error(`Failed to open mailbox: ${actualFolder}`);
|
||||
}
|
||||
|
||||
// Log mailbox status
|
||||
console.log(`[DEBUG] Mailbox ${actualFolder} opened, total messages: ${mailbox.exists}`);
|
||||
|
||||
// Validate UID exists in mailbox
|
||||
if (numericId > mailbox.uidNext) {
|
||||
throw new Error(`Email ID ${numericId} is greater than the highest UID in mailbox (${mailbox.uidNext})`);
|
||||
}
|
||||
|
||||
const message = await client.fetchOne(numericId.toString(), {
|
||||
source: true,
|
||||
envelope: true,
|
||||
flags: true,
|
||||
@ -424,7 +456,7 @@ export async function getEmailContent(
|
||||
});
|
||||
|
||||
if (!message) {
|
||||
throw new Error('Email not found');
|
||||
throw new Error(`Email not found with ID ${numericId} in folder ${actualFolder}`);
|
||||
}
|
||||
|
||||
const { source, envelope, flags, size } = message;
|
||||
@ -488,6 +520,15 @@ export async function getEmailContent(
|
||||
await cacheEmailContent(userId, accountId || folder, emailId, email);
|
||||
|
||||
return email;
|
||||
} catch (error) {
|
||||
console.error('[ERROR] Email fetch failed:', {
|
||||
userId,
|
||||
emailId,
|
||||
folder,
|
||||
accountId,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
try {
|
||||
await client.mailboxClose();
|
||||
|
||||
11
lib/utils/debounce.ts
Normal file
11
lib/utils/debounce.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export function debounce<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func(...args), wait);
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user