NeahStable/app/api/courrier/[id]/attachment/[attachmentIndex]/route.ts
2026-01-18 14:45:00 +01:00

250 lines
9.1 KiB
TypeScript

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 }
);
}
}