fix(mail) : corrige le fetch IMAP réel contre OVH (listMessages cassé)

Quatre bugs révélés en testant contre une vraie boîte OVH (les tests mockaient
le provider, donc jamais exercés) :
- requête sans critère → "BAD parse error: zero-length content" : ajout de whereAll()
- getDate()/getSubject() renvoient des Attribute webklex v6, pas des scalaires : casts explicites
- séquence par défaut ST_MSGN → le peek() de webklex faisait un STORE par numéro de
  séquence rejeté par OVH ("flag could not be removed") : force ST_UID sur toutes les requêtes
- snippet via getTextBody() forçait un fetch de corps par mail (sync 179s + peek) :
  setFetchBody(false) au listing, snippet désormais optionnel

Sync INBOX : 9 messages en 1,6s (avant : échec en 179s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 07:57:17 +02:00
parent 79d3414824
commit 17b5fa2340

View File

@@ -17,6 +17,7 @@ use SodiumException;
use Throwable; use Throwable;
use Webklex\PHPIMAP\Client; use Webklex\PHPIMAP\Client;
use Webklex\PHPIMAP\ClientManager; use Webklex\PHPIMAP\ClientManager;
use Webklex\PHPIMAP\IMAP;
final class ImapMailProvider implements MailProviderInterface final class ImapMailProvider implements MailProviderInterface
{ {
@@ -90,13 +91,19 @@ final class ImapMailProvider implements MailProviderInterface
throw MailProviderException::operationFailed('listMessages', sprintf('Folder %s not found', $folderPath)); throw MailProviderException::operationFailed('listMessages', sprintf('Folder %s not found', $folderPath));
} }
$messages = $folder->query()->leaveUnread()->get(); $messages = $folder->query()
->whereAll()
->setFetchBody(false)
->leaveUnread()
->setSequence(IMAP::ST_UID)
->get()
;
$result = []; $result = [];
$items = array_slice($messages->toArray(), $offset, $limit); $items = array_slice($messages->toArray(), $offset, $limit);
foreach ($items as $message) { foreach ($items as $message) {
$result[] = $this->buildHeaderDto($message); $result[] = $this->buildHeaderDto($message, withSnippet: false);
} }
$client->disconnect(); $client->disconnect();
@@ -121,7 +128,7 @@ final class ImapMailProvider implements MailProviderInterface
throw MailProviderException::operationFailed('fetchMessage', sprintf('Folder %s not found', $folderPath)); throw MailProviderException::operationFailed('fetchMessage', sprintf('Folder %s not found', $folderPath));
} }
$message = $folder->query()->uid($uid)->leaveUnread()->get()->first(); $message = $folder->query()->uid($uid)->leaveUnread()->setSequence(IMAP::ST_UID)->get()->first();
if (null === $message) { if (null === $message) {
throw MailProviderException::operationFailed('fetchMessage', sprintf('UID %d not found in folder %s', $uid, $folderPath)); throw MailProviderException::operationFailed('fetchMessage', sprintf('UID %d not found in folder %s', $uid, $folderPath));
@@ -168,7 +175,7 @@ final class ImapMailProvider implements MailProviderInterface
throw MailProviderException::operationFailed('markRead', sprintf('Folder %s not found', $folderPath)); throw MailProviderException::operationFailed('markRead', sprintf('Folder %s not found', $folderPath));
} }
$message = $folder->query()->uid($uid)->leaveUnread()->get()->first(); $message = $folder->query()->uid($uid)->leaveUnread()->setSequence(IMAP::ST_UID)->get()->first();
if (null === $message) { if (null === $message) {
throw MailProviderException::operationFailed('markRead', sprintf('UID %d not found', $uid)); throw MailProviderException::operationFailed('markRead', sprintf('UID %d not found', $uid));
@@ -200,7 +207,7 @@ final class ImapMailProvider implements MailProviderInterface
throw MailProviderException::operationFailed('markFlagged', sprintf('Folder %s not found', $folderPath)); throw MailProviderException::operationFailed('markFlagged', sprintf('Folder %s not found', $folderPath));
} }
$message = $folder->query()->uid($uid)->leaveUnread()->get()->first(); $message = $folder->query()->uid($uid)->leaveUnread()->setSequence(IMAP::ST_UID)->get()->first();
if (null === $message) { if (null === $message) {
throw MailProviderException::operationFailed('markFlagged', sprintf('UID %d not found', $uid)); throw MailProviderException::operationFailed('markFlagged', sprintf('UID %d not found', $uid));
@@ -232,7 +239,7 @@ final class ImapMailProvider implements MailProviderInterface
throw MailProviderException::operationFailed('moveMessage', sprintf('Folder %s not found', $folderPath)); throw MailProviderException::operationFailed('moveMessage', sprintf('Folder %s not found', $folderPath));
} }
$message = $folder->query()->uid($uid)->leaveUnread()->get()->first(); $message = $folder->query()->uid($uid)->leaveUnread()->setSequence(IMAP::ST_UID)->get()->first();
if (null === $message) { if (null === $message) {
throw MailProviderException::operationFailed('moveMessage', sprintf('UID %d not found', $uid)); throw MailProviderException::operationFailed('moveMessage', sprintf('UID %d not found', $uid));
@@ -259,7 +266,7 @@ final class ImapMailProvider implements MailProviderInterface
throw MailProviderException::operationFailed('fetchAttachment', sprintf('Folder %s not found', $folderPath)); throw MailProviderException::operationFailed('fetchAttachment', sprintf('Folder %s not found', $folderPath));
} }
$message = $folder->query()->uid($uid)->leaveUnread()->get()->first(); $message = $folder->query()->uid($uid)->leaveUnread()->setSequence(IMAP::ST_UID)->get()->first();
if (null === $message) { if (null === $message) {
throw MailProviderException::operationFailed('fetchAttachment', sprintf('UID %d not found', $uid)); throw MailProviderException::operationFailed('fetchAttachment', sprintf('UID %d not found', $uid));
@@ -331,11 +338,11 @@ final class ImapMailProvider implements MailProviderInterface
return $client; return $client;
} }
private function buildHeaderDto(mixed $message): MailMessageHeaderDto private function buildHeaderDto(mixed $message, bool $withSnippet = true): MailMessageHeaderDto
{ {
$from = $message->getFrom()->first(); $from = $message->getFrom()->first();
$fromAddress = null !== $from ? (string) $from->mail : ''; $fromAddress = null !== $from ? (string) $from->mail : '';
$fromName = null !== $from ? ($from->personal ?? null) : null; $fromName = null !== $from && null !== $from->personal ? (string) $from->personal : null;
$toAddresses = []; $toAddresses = [];
foreach ($message->getTo() as $addr) { foreach ($message->getTo() as $addr) {
@@ -351,18 +358,28 @@ final class ImapMailProvider implements MailProviderInterface
} }
} }
$sentAt = $message->getDate()?->toDateTimeImmutable() ?? new DateTimeImmutable(); $sentAt = new DateTimeImmutable();
$dateAttr = $message->getDate();
if (null !== $dateAttr) {
try {
$sentAt = DateTimeImmutable::createFromInterface($dateAttr->toDate());
} catch (Throwable) {
// keep default when the header date is missing or unparsable
}
}
$snippet = null; $snippet = null;
$text = $message->getTextBody(); if ($withSnippet) {
if (null !== $text && '' !== $text) { $text = $message->getTextBody();
$snippet = mb_substr(strip_tags($text), 0, 200); if (null !== $text && '' !== $text) {
$snippet = mb_substr(strip_tags($text), 0, 200);
}
} }
return new MailMessageHeaderDto( return new MailMessageHeaderDto(
uid: (int) $message->getUid(), uid: (int) $message->getUid(),
messageId: (string) $message->getMessageId(), messageId: (string) $message->getMessageId(),
subject: $message->getSubject() ?: null, subject: '' !== (string) $message->getSubject() ? (string) $message->getSubject() : null,
fromAddress: $fromAddress, fromAddress: $fromAddress,
fromName: $fromName, fromName: $fromName,
toAddresses: $toAddresses, toAddresses: $toAddresses,