diff --git a/src/Controller/Mail/MailMessagesListController.php b/src/Controller/Mail/MailMessagesListController.php new file mode 100644 index 0000000..ccb4e02 --- /dev/null +++ b/src/Controller/Mail/MailMessagesListController.php @@ -0,0 +1,65 @@ + '.+'])] +#[IsGranted('IS_AUTHENTICATED_FULLY')] +class MailMessagesListController extends AbstractController +{ + public function __construct( + private readonly MailFolderRepository $folderRepository, + private readonly MailMessageRepository $messageRepository, + private readonly MailAccessChecker $accessChecker, + ) {} + + public function __invoke(Request $request, string $folderPath): JsonResponse + { + $this->accessChecker->ensureCanAccessMail($this->getUser()); + + $decodedPath = urldecode($folderPath); + + $folder = $this->folderRepository->findByPath($decodedPath); + if (null === $folder) { + throw new NotFoundHttpException(sprintf('Folder "%s" not found', $decodedPath)); + } + + $limit = min((int) $request->query->get('limit', 50), 100); + $cursor = $request->query->get('cursor'); + + $result = $this->messageRepository->findByFolderCursor($folder, $limit, $cursor ?: null); + + $messages = array_map(static fn ($m) => [ + 'id' => $m->getId(), + 'messageId' => $m->getMessageId(), + 'uid' => $m->getUid(), + 'subject' => $m->getSubject(), + 'fromAddress' => $m->getFromAddress(), + 'fromName' => $m->getFromName(), + 'toAddresses' => $m->getToAddresses(), + 'ccAddresses' => $m->getCcAddresses(), + 'sentAt' => $m->getSentAt()->format(DateTimeInterface::ATOM), + 'isRead' => $m->isRead(), + 'isFlagged' => $m->isFlagged(), + 'hasAttachments' => $m->hasAttachments(), + 'snippet' => $m->getSnippet(), + ], $result['messages']); + + return $this->json([ + 'messages' => $messages, + 'nextCursor' => $result['nextCursor'], + ]); + } +} diff --git a/src/Repository/MailMessageRepository.php b/src/Repository/MailMessageRepository.php index 2d9cf0e..78e07cd 100644 --- a/src/Repository/MailMessageRepository.php +++ b/src/Repository/MailMessageRepository.php @@ -6,6 +6,8 @@ namespace App\Repository; use App\Entity\MailFolder; use App\Entity\MailMessage; +use DateTimeImmutable; +use DateTimeInterface; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; @@ -99,4 +101,52 @@ class MailMessageRepository extends ServiceEntityRepository return array_column($rows, 'uid'); } + + /** + * Pagination cursor : retourne $limit messages apres le cursor (sentAt DESC, id DESC). + * Cursor format : base64url(sentAt_iso8601:id) - null pour la premiere page. + * + * @return array{messages: list, nextCursor: ?string} + */ + public function findByFolderCursor(MailFolder $folder, int $limit, ?string $cursor): array + { + $qb = $this->createQueryBuilder('m') + ->andWhere('m.folder = :folder') + ->setParameter('folder', $folder) + ->orderBy('m.sentAt', 'DESC') + ->addOrderBy('m.id', 'DESC') + ->setMaxResults($limit + 1) + ; + + if (null !== $cursor) { + $decoded = base64_decode(strtr($cursor, '-_', '+/'), true); + if (false !== $decoded && str_contains($decoded, ':')) { + [$sentAtStr, $idStr] = explode(':', $decoded, 2); + $cursorSentAt = DateTimeImmutable::createFromFormat(DateTimeInterface::ATOM, $sentAtStr); + $cursorId = (int) $idStr; + + if ($cursorSentAt instanceof DateTimeImmutable) { + $qb + ->andWhere('m.sentAt < :cursorSentAt OR (m.sentAt = :cursorSentAt AND m.id < :cursorId)') + ->setParameter('cursorSentAt', $cursorSentAt) + ->setParameter('cursorId', $cursorId) + ; + } + } + } + + /** @var list $results */ + $results = $qb->getQuery()->getResult(); + $hasMore = count($results) > $limit; + $messages = $hasMore ? array_slice($results, 0, $limit) : $results; + $nextCursor = null; + + if ($hasMore && [] !== $messages) { + $last = end($messages); + $raw = $last->getSentAt()->format(DateTimeInterface::ATOM).':'.$last->getId(); + $nextCursor = rtrim(strtr(base64_encode($raw), '+/', '-_'), '='); + } + + return ['messages' => $messages, 'nextCursor' => $nextCursor]; + } }