feat: sync temps réel Domain/DomainEmail vers Dovecot via listeners

DomainSyncListener (postPersist, postRemove sur Domain) :
- postPersist : crée le domaine dans esymail si inexistant
- postRemove : supprime le domaine dans esymail (cascade mailboxes)

DomainEmailSyncListener (postPersist, postUpdate, postRemove sur DomainEmail) :
- postPersist : crée le domaine si besoin + crée la boîte mail dans
  esymail avec mot de passe temporaire (bcrypt BLF-CRYPT)
- postUpdate : met à jour displayName, quota, isActive dans esymail
- postRemove : supprime la boîte dans esymail

Flux : Doctrine flush → Listener → EsyMailService → base esymail → Dovecot
Les listeners vérifient isAvailable() avant chaque opération (no-op si
ESYMAIL_DATABASE_URL non configuré)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-04 00:06:12 +02:00
parent 51585e33f8
commit 53364b0068
2 changed files with 122 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
<?php
namespace App\EventListener;
use App\Entity\DomainEmail;
use App\Service\EsyMailService;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
use Doctrine\ORM\Event\PostPersistEventArgs;
use Doctrine\ORM\Event\PostRemoveEventArgs;
use Doctrine\ORM\Event\PostUpdateEventArgs;
use Doctrine\ORM\Events;
use Psr\Log\LoggerInterface;
#[AsEntityListener(event: Events::postPersist, entity: DomainEmail::class)]
#[AsEntityListener(event: Events::postUpdate, entity: DomainEmail::class)]
#[AsEntityListener(event: Events::postRemove, entity: DomainEmail::class)]
class DomainEmailSyncListener
{
public function __construct(
private EsyMailService $esyMail,
private LoggerInterface $logger,
) {
}
public function postPersist(DomainEmail $email, PostPersistEventArgs $event): void
{
if (!$this->esyMail->isAvailable()) {
return;
}
$fullEmail = $email->getFullEmail();
$domain = $email->getDomain()->getFqdn();
if (!$this->esyMail->domainExists($domain)) {
$this->esyMail->createDomain($domain);
}
$tempPassword = bin2hex(random_bytes(8));
if ($this->esyMail->createMailbox($fullEmail, $tempPassword, $email->getName(), $email->getQuotaMb())) {
$this->logger->info('EsyMail sync: boite creee '.$fullEmail);
}
}
public function postUpdate(DomainEmail $email, PostUpdateEventArgs $event): void
{
if (!$this->esyMail->isAvailable()) {
return;
}
$fullEmail = $email->getFullEmail();
if (!$this->esyMail->mailboxExists($fullEmail)) {
return;
}
$this->esyMail->updateMailbox($fullEmail, $email->getName(), $email->getQuotaMb(), $email->isActive());
$this->logger->info('EsyMail sync: boite mise a jour '.$fullEmail);
}
public function postRemove(DomainEmail $email, PostRemoveEventArgs $event): void
{
if (!$this->esyMail->isAvailable()) {
return;
}
$fullEmail = $email->getFullEmail();
if ($this->esyMail->deleteMailbox($fullEmail)) {
$this->logger->info('EsyMail sync: boite supprimee '.$fullEmail);
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\EventListener;
use App\Entity\Domain;
use App\Service\EsyMailService;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
use Doctrine\ORM\Event\PostPersistEventArgs;
use Doctrine\ORM\Event\PostRemoveEventArgs;
use Doctrine\ORM\Events;
use Psr\Log\LoggerInterface;
#[AsEntityListener(event: Events::postPersist, entity: Domain::class)]
#[AsEntityListener(event: Events::postRemove, entity: Domain::class)]
class DomainSyncListener
{
public function __construct(
private EsyMailService $esyMail,
private LoggerInterface $logger,
) {
}
public function postPersist(Domain $domain, PostPersistEventArgs $event): void
{
if (!$this->esyMail->isAvailable()) {
return;
}
$fqdn = $domain->getFqdn();
if (!$this->esyMail->domainExists($fqdn)) {
$this->esyMail->createDomain($fqdn);
$this->logger->info('EsyMail sync: domaine cree '.$fqdn);
}
}
public function postRemove(Domain $domain, PostRemoveEventArgs $event): void
{
if (!$this->esyMail->isAvailable()) {
return;
}
$fqdn = $domain->getFqdn();
if ($this->esyMail->deleteDomain($fqdn)) {
$this->logger->info('EsyMail sync: domaine supprime '.$fqdn);
}
}
}