250 lines
9.1 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|