import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/app/api/auth/options"; import { getEmailContent, shouldUseGraphAPI } from '@/lib/services/email-service'; import { logger } from '@/lib/logger'; /** * Route API pour télécharger une pièce jointe d'un email * GET /api/courrier/[id]/attachment/[attachmentIndex] */ export async function GET( request: Request, context: { params: Promise<{ id: string; attachmentIndex: string }> } ) { try { const params = await context.params; const { id: emailId, attachmentIndex } = params; // Authenticate user const session = await getServerSession(authOptions); if (!session || !session.user?.id) { return NextResponse.json( { error: "Not authenticated" }, { status: 401 } ); } // Validate parameters if (!emailId || !attachmentIndex) { return NextResponse.json( { error: "Missing email ID or attachment index" }, { status: 400 } ); } const attachmentIdx = parseInt(attachmentIndex, 10); if (isNaN(attachmentIdx) || attachmentIdx < 0) { return NextResponse.json( { error: "Invalid attachment index" }, { status: 400 } ); } // Get query parameters const { searchParams } = new URL(request.url); const folder = searchParams.get("folder") || "INBOX"; const accountId = searchParams.get("accountId") || undefined; logger.debug('[ATTACHMENT] Fetching attachment', { userIdHash: Buffer.from(session.user.id).toString('base64').slice(0, 12), emailId, attachmentIndex: attachmentIdx, folder, accountIdHash: accountId ? Buffer.from(accountId).toString('base64').slice(0, 12) : 'default', }); // Check if this is a Microsoft Graph account const graphCheck = await shouldUseGraphAPI(session.user.id, accountId); let attachmentBuffer: Buffer; let contentType: string; let filename: string; if (graphCheck.useGraph && graphCheck.mailCredentialId) { // Use Microsoft Graph API to fetch attachment logger.debug('[ATTACHMENT] Fetching attachment via Microsoft Graph API', { emailId, attachmentIndex: attachmentIdx, mailCredentialIdHash: graphCheck.mailCredentialId ? Buffer.from(graphCheck.mailCredentialId).toString('base64').slice(0, 12) : null, }); try { // First, get the email to find the attachment ID const email = await getEmailContent(session.user.id, emailId, folder, accountId); if (!email.attachments || email.attachments.length === 0) { return NextResponse.json( { error: "Email has no attachments" }, { status: 404 } ); } if (attachmentIdx >= email.attachments.length) { return NextResponse.json( { error: "Attachment index out of range" }, { status: 404 } ); } // For Graph API, we need to fetch the attachment separately // The attachment should have an ID or we need to fetch it from the message const attachment = email.attachments[attachmentIdx]; filename = attachment.filename || `attachment-${attachmentIdx}`; contentType = attachment.contentType || 'application/octet-stream'; // If attachment has content (from cache), use it if (attachment.content) { attachmentBuffer = Buffer.from(attachment.content, 'base64'); } else { // Need to fetch from Graph API - try to get attachment ID from email metadata // First, re-fetch the email to get attachment IDs if available logger.debug('[ATTACHMENT] Attachment content not cached, fetching from Graph API', { emailId, attachmentIndex: attachmentIdx, mailCredentialIdHash: graphCheck.mailCredentialId ? Buffer.from(graphCheck.mailCredentialId).toString('base64').slice(0, 12) : null, }); try { // Re-fetch the email with full attachment data const { fetchGraphEmail, fetchGraphAttachment } = await import('@/lib/services/microsoft-graph-mail'); const graphMessage = await fetchGraphEmail(graphCheck.mailCredentialId, emailId); if (!graphMessage.attachments || attachmentIdx >= graphMessage.attachments.length) { return NextResponse.json( { error: "Attachment not found" }, { status: 404 } ); } const graphAttachment = graphMessage.attachments[attachmentIdx]; // If still no contentBytes, try fetching by attachment ID if (!graphAttachment.contentBytes && graphAttachment.id) { const attachmentData = await fetchGraphAttachment( graphCheck.mailCredentialId, emailId, graphAttachment.id ); attachmentBuffer = Buffer.from(attachmentData.contentBytes, 'base64'); } else if (graphAttachment.contentBytes) { attachmentBuffer = Buffer.from(graphAttachment.contentBytes, 'base64'); } else { logger.error('[ATTACHMENT] Graph API attachment has no content and no ID', { emailId, attachmentIndex: attachmentIdx, }); return NextResponse.json( { error: "Attachment content not available" }, { status: 404 } ); } } catch (fetchError) { logger.error('[ATTACHMENT] Error fetching attachment from Graph API', { emailId, attachmentIndex: attachmentIdx, error: fetchError instanceof Error ? fetchError.message : String(fetchError), }); return NextResponse.json( { error: "Failed to fetch attachment from Microsoft Graph API" }, { status: 500 } ); } } } catch (error) { logger.error('[ATTACHMENT] Error fetching Graph API attachment', { emailId, attachmentIndex: attachmentIdx, error: error instanceof Error ? error.message : String(error), }); return NextResponse.json( { error: "Failed to fetch attachment from Microsoft Graph API" }, { status: 500 } ); } } else { // Use IMAP (standard flow) // Fetch the email content (which includes attachments) const email = await getEmailContent(session.user.id, emailId, folder, accountId); // Check if email has attachments if (!email.attachments || email.attachments.length === 0) { return NextResponse.json( { error: "Email has no attachments" }, { status: 404 } ); } // Check if attachment index is valid if (attachmentIdx >= email.attachments.length) { return NextResponse.json( { error: "Attachment index out of range" }, { status: 404 } ); } const attachment = email.attachments[attachmentIdx]; filename = attachment.filename || `attachment-${attachmentIdx}`; contentType = attachment.contentType || 'application/octet-stream'; // Check if attachment has content if (!attachment.content) { logger.error('[ATTACHMENT] Attachment has no content', { emailId, attachmentIndex: attachmentIdx, filename: attachment.filename, }); return NextResponse.json( { error: "Attachment content not available" }, { status: 404 } ); } // Decode base64 content try { attachmentBuffer = Buffer.from(attachment.content, 'base64'); } catch (error) { logger.error('[ATTACHMENT] Error decoding base64 content', { emailId, attachmentIndex: attachmentIdx, error: error instanceof Error ? error.message : String(error), }); return NextResponse.json( { error: "Failed to decode attachment content" }, { status: 500 } ); } } // Get filename, sanitize it for safe download const sanitizedFilename = filename.replace(/[^a-zA-Z0-9._-]/g, '_'); logger.debug('[ATTACHMENT] Serving attachment', { emailId, attachmentIndex: attachmentIdx, filename: sanitizedFilename, contentType, size: attachmentBuffer.length, }); // Return the attachment as a downloadable file return new NextResponse(attachmentBuffer, { status: 200, headers: { 'Content-Type': contentType, 'Content-Disposition': `attachment; filename="${sanitizedFilename}"`, 'Content-Length': attachmentBuffer.length.toString(), 'Cache-Control': 'private, max-age=3600', // Cache for 1 hour }, }); } catch (error) { logger.error('[ATTACHMENT] Error fetching attachment', { error: error instanceof Error ? error.message : String(error), }); return NextResponse.json( { error: "Failed to fetch attachment", message: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 } ); } }