fix(mail) : synchro multi-dossiers fiable contre OVH
Trois causes racines révélées par une vraie synchro complète (139 dossiers) : - contrainte UNIQUE globale sur message_id : fausse pour IMAP (un même Message-ID existe dans plusieurs dossiers) → violation → fermeture de l'EntityManager → cascade qui tuait tous les dossiers suivants. Migration : index simple à la place. - 139 connexions IMAP (une par dossier) → throttling OVH (failed to authenticate) : réutilisation d'une seule connexion (closeConnection() ajouté à l'interface). - état de connexion corrompu après un dossier en erreur (must be in SELECTED state) : reconnexion ciblée après chaque dossier en échec. - garde anti-cascade : reset du ManagerRegistry + arrêt propre si l'EM se ferme. Résultat : 456 messages sur 57 dossiers (avant : 188/30 puis crash). Les rares dossiers à encodage spécial sont skippés proprement et réessayés au cycle suivant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,19 +21,37 @@ use Webklex\PHPIMAP\IMAP;
|
||||
|
||||
final class ImapMailProvider implements MailProviderInterface
|
||||
{
|
||||
private ?Client $client = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly MailConfigurationRepository $configRepository,
|
||||
private readonly TokenEncryptor $tokenEncryptor,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Closes the reused IMAP connection. Call once at the end of a batch
|
||||
* synchronisation to release the socket; HTTP requests can ignore it
|
||||
* (the connection dies with the process).
|
||||
*/
|
||||
public function closeConnection(): void
|
||||
{
|
||||
if (null !== $this->client && $this->client->isConnected()) {
|
||||
try {
|
||||
$this->client->disconnect();
|
||||
} catch (Throwable) {
|
||||
// best effort
|
||||
}
|
||||
}
|
||||
$this->client = null;
|
||||
}
|
||||
|
||||
public function testConnection(): int
|
||||
{
|
||||
$client = $this->getClient(requireEnabled: false);
|
||||
|
||||
try {
|
||||
$folders = $client->getFolders(false);
|
||||
$client->disconnect();
|
||||
|
||||
return count($folders);
|
||||
} catch (Throwable $e) {
|
||||
@@ -69,8 +87,6 @@ final class ImapMailProvider implements MailProviderInterface
|
||||
);
|
||||
}
|
||||
|
||||
$client->disconnect();
|
||||
|
||||
return $result;
|
||||
} catch (MailProviderException $e) {
|
||||
throw $e;
|
||||
@@ -106,8 +122,6 @@ final class ImapMailProvider implements MailProviderInterface
|
||||
$result[] = $this->buildHeaderDto($message, withSnippet: false);
|
||||
}
|
||||
|
||||
$client->disconnect();
|
||||
|
||||
return $result;
|
||||
} catch (MailProviderException $e) {
|
||||
throw $e;
|
||||
@@ -148,8 +162,6 @@ final class ImapMailProvider implements MailProviderInterface
|
||||
);
|
||||
}
|
||||
|
||||
$client->disconnect();
|
||||
|
||||
return new MailMessageDetailDto(
|
||||
header: $header,
|
||||
bodyHtml: $bodyHtml,
|
||||
@@ -186,8 +198,6 @@ final class ImapMailProvider implements MailProviderInterface
|
||||
} else {
|
||||
$message->unsetFlag('Seen');
|
||||
}
|
||||
|
||||
$client->disconnect();
|
||||
} catch (MailProviderException $e) {
|
||||
throw $e;
|
||||
} catch (Throwable $e) {
|
||||
@@ -218,8 +228,6 @@ final class ImapMailProvider implements MailProviderInterface
|
||||
} else {
|
||||
$message->unsetFlag('Flagged');
|
||||
}
|
||||
|
||||
$client->disconnect();
|
||||
} catch (MailProviderException $e) {
|
||||
throw $e;
|
||||
} catch (Throwable $e) {
|
||||
@@ -246,7 +254,6 @@ final class ImapMailProvider implements MailProviderInterface
|
||||
}
|
||||
|
||||
$message->moveToFolder($targetFolder);
|
||||
$client->disconnect();
|
||||
} catch (MailProviderException $e) {
|
||||
throw $e;
|
||||
} catch (Throwable $e) {
|
||||
@@ -274,14 +281,10 @@ final class ImapMailProvider implements MailProviderInterface
|
||||
|
||||
foreach ($message->getAttachments() as $att) {
|
||||
if ((string) ($att->part_number ?? '1') === $partNumber) {
|
||||
$client->disconnect();
|
||||
|
||||
return (string) $att->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$client->disconnect();
|
||||
|
||||
throw MailProviderException::operationFailed('fetchAttachment', sprintf('Part %s not found in UID %d', $partNumber, $uid));
|
||||
} catch (MailProviderException $e) {
|
||||
throw $e;
|
||||
@@ -294,6 +297,10 @@ final class ImapMailProvider implements MailProviderInterface
|
||||
|
||||
private function getClient(bool $requireEnabled = true): Client
|
||||
{
|
||||
if (null !== $this->client && $this->client->isConnected()) {
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
$config = $this->configRepository->findSingleton();
|
||||
|
||||
if (null === $config) {
|
||||
@@ -335,6 +342,8 @@ final class ImapMailProvider implements MailProviderInterface
|
||||
}
|
||||
}
|
||||
|
||||
$this->client = $client;
|
||||
|
||||
return $client;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user