feat: création boîte mail Esy-Mail lors de la création client

EsyMailService :
- createMailbox(email, password, quotaMb) : INSERT dans la table mailbox
  de la base esymail avec hash bcrypt (BLF-CRYPT compatible Dovecot)
- mailboxExists(email) : vérifie si l'adresse existe déjà
- isAvailable() : vérifie si ESYMAIL_DATABASE_URL est configuré
- Connexion DBAL directe vers la base esymail (séparée de l'EntityManager)

ClientsController :
- Ajout paramètre EsyMailService dans create()
- Ajout méthode createMailboxIfRequested() : vérifie checkbox, valide
  email/password, vérifie existence, crée la boîte avec quota choisi
- Flash success/error selon le résultat

Template admin/clients/create.html.twig :
- Section "Messagerie Esy-Mail" avec checkbox toggle
- Champs : adresse email, mot de passe (min 8 chars), quota (1/2/5/10 Go)
- Masqué par défaut, affiché au clic sur la checkbox

Configuration :
- .env : ajout ESYMAIL_DATABASE_URL (vide par défaut)
- .env.local : connexion vers database:5432/esymail

Tests mis à jour avec EsyMailService stubé dans tous les appels create()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-03 16:50:50 +02:00
parent 5c4576ca27
commit 7a7796c090
5 changed files with 177 additions and 3 deletions

4
.env
View File

@@ -116,3 +116,7 @@ DOCUSEAL_WEBHOOKS_SECRET=
###> discord ###
DISCORD_WEBHOOK=
###< discord ###
###> esymail ###
ESYMAIL_DATABASE_URL=
###< esymail ###

View File

@@ -5,6 +5,7 @@ namespace App\Controller\Admin;
use App\Entity\Customer;
use App\Entity\User;
use App\Repository\CustomerRepository;
use App\Service\EsyMailService;
use App\Service\MeilisearchService;
use App\Service\UserManagementService;
use Doctrine\ORM\EntityManagerInterface;
@@ -39,6 +40,7 @@ class ClientsController extends AbstractController
MeilisearchService $meilisearch,
UserManagementService $userService,
LoggerInterface $logger,
EsyMailService $esyMailService,
#[Autowire(env: 'STRIPE_SK')] string $stripeSecretKey,
): Response {
if ('POST' !== $request->getMethod()) {
@@ -66,6 +68,7 @@ class ClientsController extends AbstractController
$em->flush();
$this->indexInMeilisearch($meilisearch, $customer, $logger);
$this->createMailboxIfRequested($request, $esyMailService);
$this->addFlash('success', 'Client '.$customer->getFullName().' cree.');
@@ -134,6 +137,35 @@ class ClientsController extends AbstractController
}
}
private function createMailboxIfRequested(Request $request, EsyMailService $esyMailService): void
{
if (!$request->request->getBoolean('createMailbox')) {
return;
}
$mailboxEmail = trim($request->request->getString('mailboxEmail'));
$mailboxPassword = $request->request->getString('mailboxPassword');
$quota = $request->request->getInt('mailboxQuota', 5120);
if ('' === $mailboxEmail || '' === $mailboxPassword) {
$this->addFlash('error', 'Esy-Mail : email et mot de passe requis.');
return;
}
if ($esyMailService->mailboxExists($mailboxEmail)) {
$this->addFlash('error', 'Esy-Mail : la boite '.$mailboxEmail.' existe deja.');
return;
}
if ($esyMailService->createMailbox($mailboxEmail, $mailboxPassword, $quota)) {
$this->addFlash('success', 'Boite mail '.$mailboxEmail.' creee.');
} else {
$this->addFlash('error', 'Esy-Mail : erreur lors de la creation de la boite mail.');
}
}
#[Route('/search', name: 'search', methods: ['GET'])]
public function search(Request $request, MeilisearchService $meilisearch): JsonResponse
{

View File

@@ -0,0 +1,94 @@
<?php
namespace App\Service;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
class EsyMailService
{
private ?Connection $conn = null;
public function __construct(
private EntityManagerInterface $em,
private LoggerInterface $logger,
#[Autowire(env: 'ESYMAIL_DATABASE_URL')] private string $databaseUrl = '',
) {
}
public function isAvailable(): bool
{
return '' !== $this->databaseUrl;
}
/**
* Crée une boîte mail dans la base esymail.
*/
public function createMailbox(string $email, string $password, int $quotaMb = 5120): bool
{
if (!$this->isAvailable()) {
return false;
}
$parts = explode('@', $email);
if (2 !== \count($parts)) {
return false;
}
$domain = $parts[1];
$hashedPassword = password_hash($password, \PASSWORD_BCRYPT);
try {
$conn = $this->getConnection();
$conn->insert('mailbox', [
'email' => $email,
'password' => $hashedPassword,
'domain' => $domain,
'quota_mb' => $quotaMb,
'is_active' => true,
'created_at' => (new \DateTimeImmutable())->format('Y-m-d H:i:sP'),
], [
'is_active' => 'boolean',
]);
$this->logger->info('EsyMail: boite mail creee pour '.$email);
return true;
} catch (\Throwable $e) {
$this->logger->error('EsyMail: erreur creation boite '.$email.': '.$e->getMessage());
return false;
}
}
/**
* Vérifie si une boîte mail existe.
*/
public function mailboxExists(string $email): bool
{
if (!$this->isAvailable()) {
return false;
}
try {
$result = $this->getConnection()->fetchOne('SELECT COUNT(*) FROM mailbox WHERE email = ?', [$email]);
return (int) $result > 0;
} catch (\Throwable) {
return false;
}
}
private function getConnection(): Connection
{
if (null === $this->conn) {
$this->conn = DriverManager::getConnection(['url' => $this->databaseUrl]);
}
return $this->conn;
}
}

View File

@@ -115,6 +115,46 @@
</div>
</section>
<section class="glass p-6">
<h2 class="text-sm font-bold uppercase tracking-wider mb-4 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-[#fabf04]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
Messagerie Esy-Mail
<span class="text-xs font-normal text-gray-400 normal-case tracking-normal">(optionnel)</span>
</h2>
<div class="flex items-center gap-3 mb-4">
<label for="createMailbox" class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="createMailbox" name="createMailbox" value="1" class="accent-[#fabf04]"
onchange="document.getElementById('mailbox-fields').classList.toggle('hidden', !this.checked)">
<span class="text-sm font-bold">Creer une boite mail pour ce client</span>
</label>
</div>
<div id="mailbox-fields" class="hidden">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="mailboxEmail" class="block text-xs font-bold uppercase tracking-wider mb-2">Adresse email Esy-Mail *</label>
<input type="email" id="mailboxEmail" name="mailboxEmail" placeholder="client@siteconseil.fr"
class="w-full px-4 py-3 input-glass text-sm font-medium">
</div>
<div>
<label for="mailboxPassword" class="block text-xs font-bold uppercase tracking-wider mb-2">Mot de passe messagerie *</label>
<input type="password" id="mailboxPassword" name="mailboxPassword" placeholder="Min. 8 caracteres"
class="w-full px-4 py-3 input-glass text-sm font-medium" minlength="8">
</div>
<div>
<label for="mailboxQuota" class="block text-xs font-bold uppercase tracking-wider mb-2">Quota</label>
<select id="mailboxQuota" name="mailboxQuota" class="w-full px-4 py-3 glass text-sm font-bold">
<option value="1024">1 Go</option>
<option value="2048">2 Go</option>
<option value="5120" selected>5 Go</option>
<option value="10240">10 Go</option>
</select>
</div>
</div>
</div>
</section>
<div>
<p class="text-xs text-gray-500 mb-4">Le client sera automatiquement cree chez Stripe. Un mot de passe temporaire sera genere pour son acces a l'Espace Client.</p>
<button type="submit"

View File

@@ -6,6 +6,7 @@ use App\Controller\Admin\ClientsController;
use App\Entity\Customer;
use App\Entity\User;
use App\Repository\CustomerRepository;
use App\Service\EsyMailService;
use App\Service\MeilisearchService;
use App\Service\UserManagementService;
use Doctrine\ORM\EntityManagerInterface;
@@ -73,7 +74,7 @@ class ClientsControllerTest extends TestCase
$userService = $this->createStub(UserManagementService::class);
$logger = $this->createStub(LoggerInterface::class);
$response = $controller->create($request, $repo, $em, $meilisearch, $userService, $logger, 'sk_test_***');
$response = $controller->create($request, $repo, $em, $meilisearch, $userService, $logger, $this->createStub(EsyMailService::class), 'sk_test_***');
$this->assertInstanceOf(Response::class, $response);
}
@@ -93,7 +94,7 @@ class ClientsControllerTest extends TestCase
$userService->method('createBaseUser')->willThrowException(new \InvalidArgumentException('Champs requis'));
$logger = $this->createStub(LoggerInterface::class);
$response = $controller->create($request, $repo, $em, $meilisearch, $userService, $logger, 'sk_test_***');
$response = $controller->create($request, $repo, $em, $meilisearch, $userService, $logger, $this->createStub(EsyMailService::class), 'sk_test_***');
$this->assertInstanceOf(Response::class, $response);
}
@@ -113,7 +114,7 @@ class ClientsControllerTest extends TestCase
$userService->method('createBaseUser')->willThrowException(new \RuntimeException('DB error'));
$logger = $this->createStub(LoggerInterface::class);
$response = $controller->create($request, $repo, $em, $meilisearch, $userService, $logger, 'sk_test_***');
$response = $controller->create($request, $repo, $em, $meilisearch, $userService, $logger, $this->createStub(EsyMailService::class), 'sk_test_***');
$this->assertInstanceOf(Response::class, $response);
}
@@ -249,6 +250,7 @@ class ClientsControllerTest extends TestCase
$this->createStub(MeilisearchService::class),
$userService,
$this->createStub(LoggerInterface::class),
$this->createStub(EsyMailService::class),
'',
);
$this->assertSame(302, $response->getStatusCode());
@@ -286,6 +288,7 @@ class ClientsControllerTest extends TestCase
$this->createStub(MeilisearchService::class),
$userService,
$this->createStub(LoggerInterface::class),
$this->createStub(EsyMailService::class),
'sk_test_***',
);
$this->assertSame(302, $response->getStatusCode());
@@ -326,6 +329,7 @@ class ClientsControllerTest extends TestCase
$meilisearch,
$userService,
$this->createStub(LoggerInterface::class),
$this->createStub(EsyMailService::class),
'',
);
$this->assertSame(302, $response->getStatusCode());