feat(mail) : MailAttachmentDownloadController - GET /api/mail/attachments/{id} (stream, disposition: attachment)
- downloadId = base64url(messageDbId:partNumber) - Content-Disposition: attachment systematique (jamais inline pour eviter XSS via HTML attachments) - X-Content-Type-Options: nosniff - filename sanitise via basename pour eviter path traversal Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
93
src/Controller/Mail/MailAttachmentDownloadController.php
Normal file
93
src/Controller/Mail/MailAttachmentDownloadController.php
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Mail;
|
||||||
|
|
||||||
|
use App\Mail\Exception\MailProviderException;
|
||||||
|
use App\Mail\MailProviderInterface;
|
||||||
|
use App\Repository\MailMessageRepository;
|
||||||
|
use App\Security\MailAccessChecker;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[Route('/api/mail/attachments/{downloadId}', name: 'mail_attachment_download', methods: ['GET'], priority: 1)]
|
||||||
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
|
class MailAttachmentDownloadController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly MailMessageRepository $messageRepository,
|
||||||
|
private readonly MailProviderInterface $mailProvider,
|
||||||
|
private readonly MailAccessChecker $accessChecker,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(string $downloadId): Response
|
||||||
|
{
|
||||||
|
$this->accessChecker->ensureCanAccessMail($this->getUser());
|
||||||
|
|
||||||
|
$decoded = base64_decode(strtr($downloadId, '-_', '+/'), true);
|
||||||
|
if (false === $decoded || !str_contains($decoded, ':')) {
|
||||||
|
throw new BadRequestHttpException('Invalid attachment ID format');
|
||||||
|
}
|
||||||
|
|
||||||
|
[$messageDbIdStr, $partNumber] = explode(':', $decoded, 2);
|
||||||
|
$messageDbId = (int) $messageDbIdStr;
|
||||||
|
|
||||||
|
$message = $this->messageRepository->find($messageDbId);
|
||||||
|
if (null === $message) {
|
||||||
|
throw new NotFoundHttpException('Message not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$detail = $this->mailProvider->fetchMessage(
|
||||||
|
$message->getFolder()->getPath(),
|
||||||
|
$message->getUid()
|
||||||
|
);
|
||||||
|
} catch (MailProviderException) {
|
||||||
|
throw new NotFoundHttpException('Could not fetch message from IMAP server');
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetAttachment = null;
|
||||||
|
foreach ($detail->attachments as $att) {
|
||||||
|
if ($att->partNumber === $partNumber) {
|
||||||
|
$targetAttachment = $att;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $targetAttachment) {
|
||||||
|
throw new NotFoundHttpException(sprintf('Attachment part "%s" not found', $partNumber));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$content = $this->mailProvider->fetchAttachment(
|
||||||
|
$message->getFolder()->getPath(),
|
||||||
|
$message->getUid(),
|
||||||
|
$partNumber
|
||||||
|
);
|
||||||
|
} catch (MailProviderException) {
|
||||||
|
throw new NotFoundHttpException('Could not fetch attachment content');
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = basename($targetAttachment->filename);
|
||||||
|
if ('' === $filename || '.' === $filename) {
|
||||||
|
$filename = 'attachment';
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = new Response($content);
|
||||||
|
$response->headers->set('Content-Type', $targetAttachment->mimeType);
|
||||||
|
$response->headers->set(
|
||||||
|
'Content-Disposition',
|
||||||
|
sprintf('attachment; filename="%s"', addslashes($filename))
|
||||||
|
);
|
||||||
|
$response->headers->set('Content-Length', (string) strlen($content));
|
||||||
|
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user