From 91994cd7e22087c614c767f426f6bd5a4b265cbc Mon Sep 17 00:00:00 2001 From: alma Date: Fri, 25 Apr 2025 18:06:37 +0200 Subject: [PATCH] panel 2 courier api restore --- app/api/courrier/login/route.ts | 105 ------ app/api/courrier/route.ts | 30 ++ app/courrier/page.tsx | 558 ++++++++------------------------ 3 files changed, 158 insertions(+), 535 deletions(-) diff --git a/app/api/courrier/login/route.ts b/app/api/courrier/login/route.ts index 5fc6d930..6f153ef4 100644 --- a/app/api/courrier/login/route.ts +++ b/app/api/courrier/login/route.ts @@ -11,111 +11,6 @@ const emailContentCache = new LRUCache({ ttl: 1000 * 60 * 15, // 15 minutes }); -export async function POST(request: Request) { - try { - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); - } - - // Verify user exists - const user = await prisma.user.findUnique({ - where: { id: session.user.id } - }); - - if (!user) { - return NextResponse.json( - { error: 'User not found' }, - { status: 404 } - ); - } - - const { email, password, host, port } = await request.json(); - - if (!email || !password || !host || !port) { - return NextResponse.json( - { error: 'Missing required fields' }, - { status: 400 } - ); - } - - // Test IMAP connection - const client = new ImapFlow({ - host: host, - port: parseInt(port), - secure: true, - auth: { - user: email, - pass: password, - }, - logger: false, - emitLogs: false, - tls: { - rejectUnauthorized: false // Allow self-signed certificates - } - }); - - try { - await client.connect(); - await client.mailboxOpen('INBOX'); - - // Store or update credentials in database - await prisma.mailCredentials.upsert({ - where: { - userId: session.user.id - }, - update: { - email, - password, - host, - port: parseInt(port) - }, - create: { - userId: session.user.id, - email, - password, - host, - port: parseInt(port) - } - }); - - return NextResponse.json({ success: true }); - } catch (error) { - if (error instanceof Error) { - if (error.message.includes('Invalid login')) { - return NextResponse.json( - { error: 'Invalid login or password' }, - { status: 401 } - ); - } - return NextResponse.json( - { error: `IMAP connection error: ${error.message}` }, - { status: 500 } - ); - } - return NextResponse.json( - { error: 'Failed to connect to email server' }, - { status: 500 } - ); - } finally { - try { - await client.logout(); - } catch (e) { - console.error('Error during logout:', e); - } - } - } catch (error) { - console.error('Error in login handler:', error); - return NextResponse.json( - { error: 'An unexpected error occurred' }, - { status: 500 } - ); - } -} - export async function GET( request: Request, { params }: { params: { id: string } } diff --git a/app/api/courrier/route.ts b/app/api/courrier/route.ts index f3f8a5f3..a886a51d 100644 --- a/app/api/courrier/route.ts +++ b/app/api/courrier/route.ts @@ -52,13 +52,16 @@ async function getCredentialsWithCache(userId: string) { export async function GET(request: Request) { try { + console.log('Courrier API call received'); const session = await getServerSession(authOptions); if (!session?.user?.id) { + console.log('No authenticated session found'); return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); } + console.log('User authenticated:', session.user.id); // Get URL parameters const url = new URL(request.url); @@ -67,6 +70,7 @@ export async function GET(request: Request) { const limit = parseInt(url.searchParams.get('limit') || '20'); const preview = url.searchParams.get('preview') === 'true'; const skipCache = url.searchParams.get('skipCache') === 'true'; + console.log('Request parameters:', { folder, page, limit, preview, skipCache }); // Generate cache key based on request parameters const cacheKey = `${session.user.id}:${folder}:${page}:${limit}:${preview}`; @@ -76,14 +80,17 @@ export async function GET(request: Request) { const { data, timestamp } = emailListCache[cacheKey]; // Return cached data if it's fresh (less than 1 minute old) if (Date.now() - timestamp < 60000) { + console.log('Returning cached email data'); return NextResponse.json(data); } } // Get credentials from cache or database const credentials = await getCredentialsWithCache(session.user.id); + console.log('Credentials retrieved:', credentials ? 'yes' : 'no'); if (!credentials) { + console.log('No mail credentials found for user'); return NextResponse.json( { error: 'No mail credentials found. Please configure your email account.' }, { status: 401 } @@ -93,8 +100,10 @@ export async function GET(request: Request) { // Calculate start and end sequence numbers const start = (page - 1) * limit + 1; const end = start + limit - 1; + console.log('Fetching emails from range:', { start, end }); // Connect to IMAP server + console.log('Connecting to IMAP server:', credentials.host, credentials.port); const client = new ImapFlow({ host: credentials.host, port: credentials.port, @@ -112,13 +121,21 @@ export async function GET(request: Request) { try { await client.connect(); + console.log('Connected to IMAP server'); // Get list of all mailboxes first const mailboxes = await client.list(); const availableFolders = mailboxes.map(box => box.path); + console.log('Available folders:', availableFolders); // Open the requested mailbox + console.log('Opening mailbox:', folder); const mailbox = await client.mailboxOpen(folder); + console.log('Mailbox stats:', { + exists: mailbox.exists, + name: mailbox.path, + flags: mailbox.flags + }); const result = []; @@ -127,6 +144,7 @@ export async function GET(request: Request) { // Adjust start and end to be within bounds const adjustedStart = Math.min(start, mailbox.exists); const adjustedEnd = Math.min(end, mailbox.exists); + console.log('Adjusted fetch range:', { adjustedStart, adjustedEnd }); // Fetch both metadata and preview content const fetchOptions: any = { @@ -140,9 +158,11 @@ export async function GET(request: Request) { fetchOptions.bodyParts = ['TEXT']; } + console.log('Fetching messages with options:', fetchOptions); const messages = await client.fetch(`${adjustedStart}:${adjustedEnd}`, fetchOptions); for await (const message of messages) { + console.log('Processing message ID:', message.uid); const emailData: any = { id: message.uid, from: message.envelope.from?.[0]?.address || '', @@ -164,6 +184,8 @@ export async function GET(request: Request) { result.push(emailData); } + } else { + console.log('No messages in mailbox'); } const responseData = { @@ -173,6 +195,13 @@ export async function GET(request: Request) { hasMore: end < mailbox.exists }; + console.log('Response summary:', { + emailCount: result.length, + folderCount: availableFolders.length, + total: mailbox.exists, + hasMore: end < mailbox.exists + }); + // Cache the result emailListCache[cacheKey] = { data: responseData, @@ -183,6 +212,7 @@ export async function GET(request: Request) { } finally { try { await client.logout(); + console.log('IMAP client logged out'); } catch (e) { console.error('Error during logout:', e); } diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index 91b8b126..cd5bf718 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -98,34 +98,20 @@ function splitEmailHeadersAndBody(emailBody: string): { headers: string; body: s }; } -// Email content parsing cache to prevent redundant API calls -const parsedEmailCache = new Map(); - 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); - const didParseRef = useRef(false); useEffect(() => { let mounted = true; - - // Check if we've already parsed this email content before - const cacheKey = email?.id + (email?.content?.substring(0, 50) || ''); - const cachedResult = parsedEmailCache.get(cacheKey); async function loadContent() { if (!email) return; - // If this is the same email, don't reload unless we need to - if (didParseRef.current && content) { - return; - } - setIsLoading(true); setDebugInfo(null); - try { console.log('Loading content for email:', email.id); console.log('Email content length:', email.content?.length || 0); @@ -150,58 +136,6 @@ function EmailContent({ email }: { email: Email }) { } return; } - - // Use cached result if available - if (cachedResult) { - console.log('Using cached parsed email for:', email.id); - if (mounted) { - if (cachedResult.html) { - const sanitizedHtml = DOMPurify.sanitize(cachedResult.html); - setContent( -
- ); - setDebugInfo('Rendered HTML content from cache'); - } else if (cachedResult.text) { - setContent( -
- {cachedResult.text} -
- ); - setDebugInfo('Rendered text content from cache'); - } - setError(null); - setIsLoading(false); - didParseRef.current = true; - } - return; - } - - // Check if the content is already HTML - if (formattedEmail.includes(' - ); - setDebugInfo('Rendered pre-existing HTML content'); - - // Cache the result - parsedEmailCache.set(cacheKey, { html: formattedEmail }); - - setError(null); - setIsLoading(false); - didParseRef.current = true; - } - return; - } console.log('Parsing email content:', formattedEmail.substring(0, 100) + '...'); const parsedEmail = await decodeEmail(formattedEmail); @@ -212,12 +146,6 @@ function EmailContent({ email }: { email: Email }) { textLength: parsedEmail.text?.length || 0 }); - // Cache the result for future use - parsedEmailCache.set(cacheKey, { - html: parsedEmail.html || undefined, - text: parsedEmail.text || undefined - }); - if (mounted) { if (parsedEmail.html) { const sanitizedHtml = DOMPurify.sanitize(parsedEmail.html); @@ -241,7 +169,6 @@ function EmailContent({ email }: { email: Email }) { } setError(null); setIsLoading(false); - didParseRef.current = true; } } catch (err) { console.error('Error rendering email content:', err); @@ -259,7 +186,7 @@ function EmailContent({ email }: { email: Email }) { return () => { mounted = false; }; - }, [email?.id, email?.content, content]); + }, [email?.id, email?.content]); if (isLoading) { return ( @@ -453,15 +380,6 @@ function EmailPreview({ email }: { email: Email }) { setIsLoading(true); try { - // Check if content is already a simple preview string (less than 1000 chars) - if (email.content.length < 1000 && !email.content.includes('Content-Type:')) { - setPreview(email.content.substring(0, 150) + '...'); - setError(null); - setIsLoading(false); - return; - } - - // Otherwise try to decode it const decoded = await decodeEmail(email.content); if (mounted) { if (decoded.text) { @@ -477,9 +395,8 @@ function EmailPreview({ email }: { email: Email }) { } catch (err) { console.error('Error generating email preview:', err); if (mounted) { - // Fallback to displaying the raw content if decoding fails - setPreview(email.content.substring(0, 150) + '...'); - setError(null); + setError('Error generating preview'); + setPreview(''); } } finally { if (mounted) setIsLoading(false); @@ -600,9 +517,6 @@ export default function CourrierPage() { type: 'reply' | 'reply-all' | 'forward'; } | null>(null); - // Email content cache to prevent redundant API calls - const emailContentCache = useRef>(new Map()); - // Debug logging for email distribution useEffect(() => { const emailsByFolder = emails.reduce((acc, email) => { @@ -625,7 +539,6 @@ export default function CourrierPage() { try { console.log('Checking for stored credentials...'); const response = await fetch('/api/courrier'); - if (!response.ok) { const errorData = await response.json(); console.log('API response error:', errorData); @@ -636,66 +549,9 @@ export default function CourrierPage() { } throw new Error(errorData.error || 'Failed to check credentials'); } - - // Process API response to get folders and emails in one go - const data = await response.json(); - - // Update folder list first - if (data.folders && data.folders.length > 0) { - setAvailableFolders(data.folders); - - // Update sidebar items based on folders - const standardFolders = ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']; - const customFolders = data.folders.filter( - (folder: string) => !standardFolders.includes(folder) - ); - - setFolders(customFolders); - - // Update sidebar items with standard and custom folders - const updatedSidebarItems = [ - ...sidebarItems.filter(item => standardFolders.includes(item.view)), - ...customFolders.map((folder: string) => ({ - view: folder, - label: folder, - icon: getFolderIcon(folder) - })) - ]; - - setSidebarItems(updatedSidebarItems); - } - - // Process emails immediately if available - if (data.emails && data.emails.length > 0) { - const processedEmails = data.emails.map((email: any) => ({ - id: email.id, - accountId: 1, - from: email.from || '', - fromName: email.fromName || email.from?.split('@')[0] || '', - to: email.to || '', - subject: email.subject || '(No subject)', - content: email.preview || '', // Store preview as initial content - date: email.date || new Date().toISOString(), - read: email.read || false, - starred: email.starred || false, - folder: email.folder || currentView, - cc: email.cc, - bcc: email.bcc, - flags: email.flags || [], - hasAttachments: email.hasAttachments || false - })); - - setEmails(processedEmails); - - // Calculate unread count - const unreadInboxEmails = processedEmails.filter( - (email: any) => !email.read && email.folder === 'INBOX' - ).length; - setUnreadCount(unreadInboxEmails); - } - - console.log('Credentials verified, loading complete'); + console.log('Credentials verified, loading emails...'); setLoading(false); + loadEmails(); } catch (err) { console.error('Error checking credentials:', err); setError(err instanceof Error ? err.message : 'Failed to check credentials'); @@ -705,114 +561,36 @@ export default function CourrierPage() { checkCredentials(); }, [router]); - - // Add a refreshEmails function to handle fresh data loading - const refreshEmails = async () => { - // If we're already refreshing, don't start another refresh - if (isLoadingRefresh) return; - - setIsLoadingRefresh(true); - - // Clear selected email to avoid UI inconsistencies - setSelectedEmail(null); - - try { - // Force timestamp to bypass cache - const timestamp = Date.now(); - - // Include skipCache parameter to bypass any server-side caching - const response = await fetch( - `/api/courrier?folder=${encodeURIComponent(currentView)}&page=1&limit=${emailsPerPage}&_t=${timestamp}&skipCache=true&preview=true`, - { - cache: 'no-store', - headers: { - 'Cache-Control': 'no-cache, no-store, must-revalidate', - 'Pragma': 'no-cache' - } - } - ); - - if (!response.ok) { - throw new Error('Failed to refresh emails'); - } - - const data = await response.json(); - - // Create a Set of standard folder names for more efficient lookup - const standardFoldersSet = new Set(['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']); - - // Always update folders on refresh - if (data.folders && data.folders.length > 0) { - setAvailableFolders(data.folders); - - // Filter custom folders more efficiently - const customFolders = data.folders.filter((folder: string) => !standardFoldersSet.has(folder)); - setFolders(customFolders); - - // Create a map of existing standard sidebar items for quick lookup - const standardSidebarItems = initialSidebarItems.filter(item => - standardFoldersSet.has(item.view) - ); - - // Only create new items for custom folders - const customSidebarItems = customFolders.map((folder: string) => ({ - view: folder, - label: folder, - icon: getFolderIcon(folder) - })); - - // Update sidebar items efficiently - setSidebarItems([...standardSidebarItems, ...customSidebarItems]); - } - - // Process emails with a more efficient mapping approach - if (data.emails && data.emails.length > 0) { - const processedEmails = data.emails.map((email: any) => ({ - id: email.id, - accountId: 1, - from: email.from || '', - fromName: email.fromName || email.from?.split('@')[0] || '', - to: email.to || '', - subject: email.subject || '(No subject)', - content: email.preview || email.content || '', - date: email.date || new Date().toISOString(), - read: email.read || false, - starred: email.starred || false, - folder: email.folder || currentView, - cc: email.cc, - bcc: email.bcc, - flags: email.flags || [], - hasAttachments: email.hasAttachments || false - })); - - setEmails(processedEmails); - setHasMore(data.hasMore); - - // Calculate unread count more efficiently by directly counting during the map phase - if (currentView === 'INBOX') { - const unreadCount = processedEmails.reduce((count: number, email: Email) => - (!email.read && email.folder === 'INBOX') ? count + 1 : count, 0 - ); - setUnreadCount(unreadCount); - } - } else { - // Clear emails if none were returned - setEmails([]); - setHasMore(false); - } - } catch (error) { - console.error('Error refreshing emails:', error); - setError(error instanceof Error ? error.message : 'Failed to refresh emails'); - } finally { - setIsLoadingRefresh(false); - } - }; - // Update the loadEmails function to prevent redundant API calls + // Check for email credentials + useEffect(() => { + async function checkMailCredentials() { + try { + const response = await fetch('/api/courrier/login'); + const data = await response.json(); + + if (response.status === 404 || !response.ok) { + console.log('Mail credentials not found'); + setError('Please configure your email account first'); + // Optionally redirect to login page + // router.push('/courrier/login'); + } else { + console.log('Mail credentials found:', data.email); + } + } catch (error) { + console.error('Error checking mail credentials:', error); + } + } + + checkMailCredentials(); + }, []); + + // Update the loadEmails function with better debugging const loadEmails = async (isLoadMore = false) => { try { // Don't reload if we're already loading if (isLoadingInitial || isLoadingMore) { + console.log('Skipping email load - already loading'); return; } @@ -825,12 +603,13 @@ export default function CourrierPage() { // Create a cache key for this request const cacheKey = `${currentView}-${page}-${emailsPerPage}`; + console.log('Loading emails with params:', { currentView, page, emailsPerPage }); // Add timestamp parameter to force fresh data when needed const timestamp = isLoadMore || page > 1 ? '' : `&_t=${Date.now()}`; const response = await fetch( - `/api/courrier?folder=${encodeURIComponent(currentView)}&page=${page}&limit=${emailsPerPage}${timestamp}&preview=true`, + `/api/courrier?folder=${encodeURIComponent(currentView)}&page=${page}&limit=${emailsPerPage}${timestamp}`, { cache: 'no-store' } ); @@ -841,29 +620,8 @@ export default function CourrierPage() { const data = await response.json(); // Get available folders from the API response - if (data.folders && data.folders.length > 0) { + if (data.folders) { setAvailableFolders(data.folders); - - // Update sidebar items based on folders - const standardFolders = ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']; - const customFolders = data.folders.filter( - (folder: string) => !standardFolders.includes(folder) - ); - - // Set folders for display in sidebar - setFolders(customFolders); - - // Update sidebar items with standard and custom folders - const updatedSidebarItems = [ - ...sidebarItems.filter(item => standardFolders.includes(item.view)), - ...customFolders.map((folder: string) => ({ - view: folder, - label: folder, - icon: getFolderIcon(folder) - })) - ]; - - setSidebarItems(updatedSidebarItems); } // Process emails keeping exact folder names and sort by date @@ -875,7 +633,7 @@ export default function CourrierPage() { fromName: email.fromName || email.from?.split('@')[0] || '', to: email.to || '', subject: email.subject || '(No subject)', - content: email.preview || email.content || '', // Ensure we use preview if available + content: email.preview || '', // Store preview as initial content date: email.date || new Date().toISOString(), read: email.read || false, starred: email.starred || false, @@ -942,141 +700,67 @@ export default function CourrierPage() { // Update handleEmailSelect to set selectedEmail correctly and ensure content is loaded const handleEmailSelect = async (emailId: string) => { - // Find the email in the current list - const email = emails.find(e => e.id === emailId); - if (!email) return; - - // Check if we have the content cached - const cachedContent = emailContentCache.current.get(emailId); - - // Set selected email immediately to show the UI - setSelectedEmail(cachedContent || email); - - // If we already have the full content cached, we don't need to fetch it again - if (cachedContent?.content && cachedContent.content.length > 100) { - // Just update read status if needed - if (!email.read) { - try { - fetch(`/api/courrier/${emailId}/mark-read`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - } - }); - - // Update the email in state optimistically - setEmails(emails.map(e => - e.id === emailId ? { ...e, read: true } : e - )); - } catch (error) { - console.error('Error marking as read:', error); - } - } - return; - } - - // Set loading state - setContentLoading(true); - try { - // Fetch the full email content in the background - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout + setContentLoading(true); - // Mark as read immediately for better UX - if (!email.read) { - try { - fetch(`/api/courrier/${emailId}/mark-read`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - } - }); - - // Update the email in state optimistically - setEmails(emails.map(e => - e.id === emailId ? { ...e, read: true } : e - )); - } catch (error) { - console.error('Error marking as read:', error); - } + // Find the email in the current list first + const emailInList = emails.find(email => email.id === emailId); + + // Set selected email immediately with what we have + if (emailInList) { + setSelectedEmail(emailInList); } // Fetch the full email content - const response = await fetch(`/api/courrier/${emailId}`, { - signal: controller.signal, - cache: 'force-cache' - }); - - clearTimeout(timeoutId); + const response = await fetch(`/api/courrier/${emailId}`); if (!response.ok) { - throw new Error('Failed to fetch email content'); + throw new Error('Failed to fetch full email content'); } - const emailData = await response.json(); + const fullEmail = await response.json(); + console.log('Fetched email content:', fullEmail); - // Pre-parse the email content on the client side to avoid additional API calls - let parsedContent = emailData.content; - - // Only make parse-email call if we need to (large emails or complex formats) - if (emailData.content && emailData.content.length > 0 && !emailData.content.includes(' - e.id === emailId - ? fullEmail - : e + // Update the email in the list + setEmails(prevEmails => prevEmails.map(email => + email.id === emailId ? completeEmail : email )); + + // Update the selected email + setSelectedEmail(completeEmail); + + // Try to mark as read in the background + try { + await fetch(`/api/courrier/${emailId}/mark-read`, { + method: 'POST', + }); + + // 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 fetching email content:', error); - setError(error instanceof Error ? error.message : 'Failed to fetch email content'); + console.error('Error fetching email:', error); + setError('Failed to load email content. Please try again.'); } finally { setContentLoading(false); } }; - - // Add or update the refresh handler - const handleRefresh = () => { - // Clear selected email to avoid stale data - setSelectedEmail(null); - // Use the new refresh function - refreshEmails(); - }; // Add these improved handlers const handleEmailCheckbox = (e: React.ChangeEvent, emailId: number) => { @@ -1215,39 +899,55 @@ export default function CourrierPage() { }, [sortedEmails, searchQuery]); // Update the email list to use filtered emails - const renderEmailList = () => ( -
- {renderEmailListHeader()} - {renderBulkActionsToolbar()} - -
- {loading ? ( -
-
-
- ) : filteredEmails.length === 0 ? ( -
- -

- {searchQuery ? 'No emails match your search' : 'No emails in this folder'} -

-
- ) : ( -
- {filteredEmails.map((email) => renderEmailListItem(email))} - {isLoadingMore && ( -
-
-
- )} -
- )} + const renderEmailList = () => { + console.log('Rendering email list with state:', { + loading, + emailCount: emails.length, + filteredEmailCount: filteredEmails.length, + searchQuery: searchQuery.length > 0 ? searchQuery : 'empty', + selectedEmails: selectedEmails.length + }); + + return ( +
+ {renderEmailListHeader()} + {renderBulkActionsToolbar()} + +
+ {loading ? ( +
+
+
+ ) : filteredEmails.length === 0 ? ( +
+ +

+ {searchQuery ? 'No emails match your search' : 'No emails in this folder'} +

+ {error && ( +

{error}

+ )} +

Folder: {currentView}

+

Total emails: {emails.length}

+

Available folders: {availableFolders.length}

+
+ ) : ( +
+ {filteredEmails.map((email) => renderEmailListItem(email))} + {isLoadingMore && ( +
+
+
+ )} +
+ )} +
-
- ); + ); + }; // Update the email count in the header to show filtered count const renderEmailListHeader = () => ( @@ -1531,7 +1231,7 @@ export default function CourrierPage() { 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}&preview=true`, { + const response = await fetch(`/api/courrier?folder=${encodeURIComponent(newMailbox)}&page=1&limit=${emailsPerPage}`, { signal: controller.signal }); @@ -1552,7 +1252,6 @@ export default function CourrierPage() { to: email.to || '', subject: email.subject || '(No subject)', body: email.body || '', - content: email.preview || email.content || '', date: email.date || new Date().toISOString(), read: email.read || false, starred: email.starred || false, @@ -1851,11 +1550,10 @@ export default function CourrierPage() {