diff --git a/src/Controller/Mail/MailMessageDetailController.php b/src/Controller/Mail/MailMessageDetailController.php new file mode 100644 index 0000000..c6a7e29 --- /dev/null +++ b/src/Controller/Mail/MailMessageDetailController.php @@ -0,0 +1,87 @@ + '\d+'])] +#[IsGranted('IS_AUTHENTICATED_FULLY')] +class MailMessageDetailController extends AbstractController +{ + public function __construct( + private readonly MailMessageRepository $messageRepository, + private readonly MailProviderInterface $mailProvider, + private readonly MailAccessChecker $accessChecker, + private readonly CacheItemPoolInterface $cache, + ) {} + + public function __invoke(int $id): JsonResponse + { + $this->accessChecker->ensureCanAccessMail($this->getUser()); + + $message = $this->messageRepository->find($id); + if (null === $message) { + throw new NotFoundHttpException('Message not found'); + } + + $cacheKey = 'mail_body_'.md5($message->getMessageId()); + $item = $this->cache->getItem($cacheKey); + + if (!$item->isHit()) { + try { + $detail = $this->mailProvider->fetchMessage( + $message->getFolder()->getPath(), + $message->getUid() + ); + $item->set($detail); + $item->expiresAfter(300); + $this->cache->save($item); + } catch (MailProviderException) { + throw new ServiceUnavailableHttpException(null, 'IMAP unavailable: could not fetch message body'); + } + } + + $detail = $item->get(); + + $messageId = $message->getId(); + $attachments = array_map(static fn ($att) => [ + 'partNumber' => $att->partNumber, + 'filename' => $att->filename, + 'mimeType' => $att->mimeType, + 'size' => $att->size, + 'downloadId' => rtrim(strtr(base64_encode($messageId.':'.$att->partNumber), '+/', '-_'), '='), + ], $detail->attachments); + + return $this->json([ + 'id' => $message->getId(), + 'messageId' => $message->getMessageId(), + 'uid' => $message->getUid(), + 'folderPath' => $message->getFolder()->getPath(), + 'subject' => $detail->header->subject, + 'fromAddress' => $detail->header->fromAddress, + 'fromName' => $detail->header->fromName, + 'toAddresses' => $detail->header->toAddresses, + 'ccAddresses' => $detail->header->ccAddresses, + 'sentAt' => $detail->header->sentAt->format(DateTimeInterface::ATOM), + 'isRead' => $message->isRead(), + 'isFlagged' => $message->isFlagged(), + 'hasAttachments' => $message->hasAttachments(), + 'bodyHtml' => $detail->bodyHtml, + 'bodyText' => $detail->bodyText, + 'attachments' => $attachments, + ]); + } +} diff --git a/tests/Functional/Controller/Mail/MailMessagesControllerTest.php b/tests/Functional/Controller/Mail/MailMessagesControllerTest.php new file mode 100644 index 0000000..f82e4f1 --- /dev/null +++ b/tests/Functional/Controller/Mail/MailMessagesControllerTest.php @@ -0,0 +1,48 @@ +request('GET', '/api/mail/messages/999'); + + self::assertResponseStatusCodeSame(401); + } + + public function testGetMessageDetailReturns403ForRoleClient(): void + { + $client = static::createClient(); + $container = static::getContainer(); + $em = $container->get('doctrine.orm.entity_manager'); + + $clientUser = $em->getRepository(User::class)->findOneBy(['username' => 'client-liot']); + $client->loginUser($clientUser); + $client->request('GET', '/api/mail/messages/999'); + + self::assertResponseStatusCodeSame(403); + } + + public function testGetMessageDetailReturns404WhenMessageNotFound(): void + { + $client = static::createClient(); + $container = static::getContainer(); + $em = $container->get('doctrine.orm.entity_manager'); + + $user = $em->getRepository(User::class)->findOneBy(['username' => 'alice']); + $client->loginUser($user); + $client->request('GET', '/api/mail/messages/99999'); + + self::assertResponseStatusCodeSame(404); + } +}