feat(mail) : MailMessagesListController - GET /api/mail/folders/{path}/messages (pagination cursor)

- MailMessageRepository::findByFolderCursor : pagination cursor sentAt DESC, id DESC
- cursor base64url(sentAt_iso:id), limit max 100
- folderPath URL-encode (requirements: .+ pour supporter les slashes nested)
- securite via MailAccessChecker

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 00:08:18 +02:00
parent b1d6303afe
commit 7fb525595e
2 changed files with 115 additions and 0 deletions
+50
View File
@@ -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<MailMessage>, 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<MailMessage> $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];
}
}