feat: vérification DNS Esy-Mail et Esy-Mailer en temps réel par domaine
EsyMailService - 2 nouvelles méthodes de vérification DNS : checkDnsEsyMail(domain) — config réception (Dovecot) : - MX → doit pointer vers ESYMAIL_HOSTNAME (mail.esy-web.dev) - SPF → doit contenir le hostname mail ou include:_spf - DKIM → sélecteur dkim._domainkey.domain (TXT ou CNAME) - DMARC → _dmarc.domain doit contenir v=DMARC1 - Retourne ok=true si les 4 checks passent checkDnsEsyMailer(domain) — config envoi (AWS SES) : - SES domaine vérifié (isDomainVerified = Success) - SES DKIM activé et vérifié (getDkimStatus enabled+verified) - SPF → doit contenir include:amazonses.com - MAIL FROM → configuré et vérifié (getMailFromStatus = Success) - Retourne ok=true si les 4 checks passent Intégration : - Les checks DNS sont exécutés seulement sur l'onglet NDD (pas sur les autres onglets pour éviter les appels réseau inutiles) - Les résultats alimentent configDnsEsyMail et configDnsEsyMailer dans la sous-ligne de chaque domaine (OK vert / KO rouge) Configuration : - .env : ESYMAIL_HOSTNAME (vide par défaut) - .env.local : ESYMAIL_HOSTNAME=mail.esy-web.dev - ansible/vault.yml : esymail_hostname pour la prod Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1
.env
1
.env
@@ -119,6 +119,7 @@ DISCORD_WEBHOOK=
|
||||
|
||||
###> esymail ###
|
||||
ESYMAIL_DATABASE_URL=
|
||||
ESYMAIL_HOSTNAME=
|
||||
###< esymail ###
|
||||
|
||||
###> ovh ###
|
||||
|
||||
@@ -20,6 +20,7 @@ mailcow_api_key: DF0E7E-0FD059-16226F-8ECFF1-E558B3
|
||||
docuseal_api: pgAU116mCFmeF7WQSezHqxtZW8V1fgo31u5d2FXoaKe
|
||||
docuseal_webhooks_secret: CRM_COSLAY
|
||||
discord_webhook: https://discord.com/api/webhooks/1419573620602044518/ikAdxWxsrrTqMTb5Gh_8ylcoJHlOnq7aJZvR5udoS_fCK56Jk3qpEnJHVKdD8fwuNJF3
|
||||
esymail_hostname: mail.esy-web.dev
|
||||
ovh_key: 34bc2c2eb416b67d
|
||||
ovh_secret: 12239d273975b5ab53318907fb66d355
|
||||
ovh_customer: 56c387eb9ca4b9a2de4d4d97fd3d7f22
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Repository\CustomerRepository;
|
||||
use App\Entity\Domain;
|
||||
use App\Service\CloudflareService;
|
||||
use App\Service\DnsCheckService;
|
||||
use App\Service\EsyMailService;
|
||||
use App\Service\MailerService;
|
||||
use App\Service\MeilisearchService;
|
||||
use App\Service\OvhService;
|
||||
@@ -133,20 +134,27 @@ class ClientsController extends AbstractController
|
||||
*
|
||||
* @return array<int, array{esyMail: bool, emailCount: int, esyMailer: bool, configDnsEsyMail: bool, configDnsEsyMailer: bool}>
|
||||
*/
|
||||
private function buildDomainsInfo(array $domains, EntityManagerInterface $em): array
|
||||
private function buildDomainsInfo(array $domains, EntityManagerInterface $em, EsyMailService $esyMailService, bool $checkDns = false): array
|
||||
{
|
||||
$emailRepo = $em->getRepository(\App\Entity\DomainEmail::class);
|
||||
$info = [];
|
||||
|
||||
foreach ($domains as $domain) {
|
||||
$emailCount = $emailRepo->count(['domain' => $domain]);
|
||||
$configMail = false;
|
||||
$configMailer = false;
|
||||
|
||||
if ($checkDns) {
|
||||
$configMail = $esyMailService->checkDnsEsyMail($domain->getFqdn())['ok'];
|
||||
$configMailer = $esyMailService->checkDnsEsyMailer($domain->getFqdn())['ok'];
|
||||
}
|
||||
|
||||
$info[$domain->getId()] = [
|
||||
'esyMail' => $emailCount > 0,
|
||||
'emailCount' => $emailCount,
|
||||
'esyMailer' => false,
|
||||
'configDnsEsyMail' => false,
|
||||
'configDnsEsyMailer' => false,
|
||||
'configDnsEsyMail' => $configMail,
|
||||
'configDnsEsyMailer' => $configMailer,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -341,7 +349,7 @@ class ClientsController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route('/{id}', name: 'show')]
|
||||
public function show(Customer $customer, Request $request, EntityManagerInterface $em, OvhService $ovhService, CloudflareService $cloudflareService, DnsCheckService $dnsCheckService): Response
|
||||
public function show(Customer $customer, Request $request, EntityManagerInterface $em, OvhService $ovhService, CloudflareService $cloudflareService, DnsCheckService $dnsCheckService, EsyMailService $esyMailService): Response
|
||||
{
|
||||
$tab = $request->query->getString('tab', 'info');
|
||||
|
||||
@@ -365,7 +373,7 @@ class ClientsController extends AbstractController
|
||||
$this->ensureDefaultContact($customer, $em);
|
||||
$contacts = $em->getRepository(\App\Entity\CustomerContact::class)->findBy(['customer' => $customer], ['createdAt' => 'DESC']);
|
||||
$domains = $em->getRepository(\App\Entity\Domain::class)->findBy(['customer' => $customer]);
|
||||
$domainsInfo = $this->buildDomainsInfo($domains, $em);
|
||||
$domainsInfo = $this->buildDomainsInfo($domains, $em, $esyMailService, 'ndd' === $tab);
|
||||
|
||||
return $this->render('admin/clients/show.html.twig', [
|
||||
'customer' => $customer,
|
||||
|
||||
@@ -13,7 +13,10 @@ class EsyMailService
|
||||
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private DnsCheckService $dnsCheck,
|
||||
private AwsSesService $awsSes,
|
||||
#[Autowire(env: 'ESYMAIL_DATABASE_URL')] private string $databaseUrl = '',
|
||||
#[Autowire(env: 'ESYMAIL_HOSTNAME')] private string $mailHostname = '',
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -341,6 +344,112 @@ class EsyMailService
|
||||
}
|
||||
}
|
||||
|
||||
// ──── Vérification DNS ──────────────────────────────
|
||||
|
||||
/**
|
||||
* Vérifie la config DNS Esy-Mail (réception) pour un domaine.
|
||||
* MX → mail hostname, SPF includes, DKIM, DMARC.
|
||||
*
|
||||
* @return array{ok: bool, mx: bool, spf: bool, dkim: bool, dmarc: bool, details: array<string, string>}
|
||||
*/
|
||||
public function checkDnsEsyMail(string $domain): array
|
||||
{
|
||||
$result = ['ok' => false, 'mx' => false, 'spf' => false, 'dkim' => false, 'dmarc' => false, 'details' => []];
|
||||
|
||||
// MX → doit pointer vers le mail hostname (ex: mail.esy-web.dev)
|
||||
$mxRecords = $this->dnsCheck->getMxRecords($domain);
|
||||
foreach ($mxRecords as $mx) {
|
||||
if ('' !== $this->mailHostname && str_contains($mx['target'], $this->mailHostname)) {
|
||||
$result['mx'] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$result['details']['mx'] = implode(', ', array_map(fn ($mx) => $mx['target'], $mxRecords)) ?: 'Aucun';
|
||||
|
||||
// SPF → doit contenir le mail hostname ou l'IP
|
||||
$spfOutput = $this->dnsCheck->dig($domain, 'TXT');
|
||||
foreach (explode("\n", $spfOutput) as $line) {
|
||||
if (preg_match('/IN\s+TXT\s+"(v=spf1[^"]+)"/', $line, $m)) {
|
||||
$spf = str_replace('" "', '', $m[1]);
|
||||
if (str_contains($spf, $this->mailHostname) || str_contains($spf, 'include:_spf')) {
|
||||
$result['spf'] = true;
|
||||
}
|
||||
$result['details']['spf'] = $spf;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// DKIM → check sélecteur dkim._domainkey
|
||||
$dkimFqdn = 'dkim._domainkey.'.$domain;
|
||||
$dkimTxt = $this->dnsCheck->getDkimTxtRecord($dkimFqdn);
|
||||
$dkimCname = $this->dnsCheck->getCnameRecord($dkimFqdn);
|
||||
$result['dkim'] = null !== $dkimTxt || null !== $dkimCname;
|
||||
$result['details']['dkim'] = $dkimTxt ?? $dkimCname ?? 'Non trouve';
|
||||
|
||||
// DMARC → _dmarc.domain
|
||||
$dmarcOutput = $this->dnsCheck->dig('_dmarc.'.$domain, 'TXT');
|
||||
foreach (explode("\n", $dmarcOutput) as $line) {
|
||||
if (preg_match('/IN\s+TXT\s+"(v=DMARC1[^"]+)"/', $line, $m)) {
|
||||
$result['dmarc'] = true;
|
||||
$result['details']['dmarc'] = str_replace('" "', '', $m[1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$result['ok'] = $result['mx'] && $result['spf'] && $result['dkim'] && $result['dmarc'];
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie la config DNS Esy-Mailer (envoi AWS SES) pour un domaine.
|
||||
* SES domaine vérifié, DKIM SES, SPF include:amazonses.com, MAIL FROM.
|
||||
*
|
||||
* @return array{ok: bool, ses_verified: bool, ses_dkim: bool, spf_ses: bool, mail_from: bool, details: array<string, string>}
|
||||
*/
|
||||
public function checkDnsEsyMailer(string $domain): array
|
||||
{
|
||||
$result = ['ok' => false, 'ses_verified' => false, 'ses_dkim' => false, 'spf_ses' => false, 'mail_from' => false, 'details' => []];
|
||||
|
||||
if (!$this->awsSes->isAvailable()) {
|
||||
$result['details']['error'] = 'AWS SES non configure';
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// SES domaine vérifié
|
||||
$verif = $this->awsSes->isDomainVerified($domain);
|
||||
$result['ses_verified'] = 'Success' === $verif;
|
||||
$result['details']['ses_verified'] = $verif ?? 'Non verifie';
|
||||
|
||||
// SES DKIM
|
||||
$dkim = $this->awsSes->getDkimStatus($domain);
|
||||
$result['ses_dkim'] = $dkim['enabled'] && $dkim['verified'];
|
||||
$result['details']['ses_dkim'] = ($dkim['enabled'] ? 'Enabled' : 'Disabled').', '.($dkim['verified'] ? 'Verified' : 'Not verified');
|
||||
|
||||
// SPF → doit contenir include:amazonses.com
|
||||
$spfOutput = $this->dnsCheck->dig($domain, 'TXT');
|
||||
foreach (explode("\n", $spfOutput) as $line) {
|
||||
if (preg_match('/IN\s+TXT\s+"(v=spf1[^"]+)"/', $line, $m)) {
|
||||
$spf = str_replace('" "', '', $m[1]);
|
||||
if (str_contains($spf, 'amazonses.com')) {
|
||||
$result['spf_ses'] = true;
|
||||
}
|
||||
$result['details']['spf'] = $spf;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// MAIL FROM
|
||||
$mailFrom = $this->awsSes->getMailFromStatus($domain);
|
||||
$result['mail_from'] = 'Success' === ($mailFrom['mail_from_status'] ?? '');
|
||||
$result['details']['mail_from'] = ($mailFrom['mail_from_domain'] ?? 'Non configure').' ('.($mailFrom['mail_from_status'] ?? '?').')';
|
||||
|
||||
$result['ok'] = $result['ses_verified'] && $result['ses_dkim'] && $result['spf_ses'] && $result['mail_from'];
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// ──── Stats ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user