Gestion des Emails Invalides (Bounces)¶
Version 1.0.0
Système de détection et gestion des emails rejetés par les serveurs SMTP avec dashboard super-administrateur.
Vue d'ensemble¶
Le système de gestion des emails invalides (bounces) permet de détecter automatiquement les rejets SMTP lors de l'envoi de notifications aux mairies et offre un dashboard dédié pour corriger ces problèmes.
Fonctionnalités principales¶
- ✅ Détection automatique des codes d'erreur SMTP (5xx) — au
RCPT TOet auDATA - ✅ Cooldown 7 jours : aucune tentative SMTP vers une adresse connue invalide
- ✅ Classification des erreurs (user unknown, mailbox full, domain not found)
- ✅ Dashboard superadmin pour visualiser et corriger
- ✅ Historique complet des tentatives d'envoi
- ✅ Rate limiting (5 minutes entre renvois)
Architecture¶
Base de données¶
Table mairies - Colonnes ajoutées¶
email_invalid BOOLEAN DEFAULT FALSE
email_invalid_since DATETIME NULL
email_invalid_reason TEXT NULL
email_last_checked DATETIME NULL
Table email_delivery_logs - Nouvelle table¶
CREATE TABLE email_delivery_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
mairie_id INT NOT NULL,
incident_id INT NULL,
recipient_email VARCHAR(255) NOT NULL,
smtp_command VARCHAR(50) NOT NULL,
smtp_response TEXT NOT NULL,
smtp_code INT NOT NULL,
status ENUM('success', 'failure') NOT NULL,
error_type VARCHAR(50) NULL,
attempt_number INT DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Rôle superadmin¶
-- ENUM étendu
ALTER TABLE users MODIFY COLUMN role
ENUM('admin', 'gestionnaire', 'lecteur', 'superadmin');
-- Compte par défaut
Email: superadmin@example.com
Mot de passe: <défini à l'installation, à changer immédiatement>
Flux de détection¶
Classification des erreurs¶
| Type | Code SMTP | Description | Action |
|---|---|---|---|
user_unknown |
550 | Adresse inexistante | Corriger email |
mailbox_full |
552 | Boîte mail pleine | Attendre ou corriger |
domain_not_found |
551 | Domaine introuvable | Corriger domaine |
permanent_error |
5xx | Erreur permanente générique | Vérifier email |
temporary_error |
4xx | Erreur temporaire | Réessayer plus tard |
Erreurs temporaires (4xx)
Les codes 4xx ne marquent pas l'email comme invalide car le problème est temporaire.
EmailService¶
Propriétés ajoutées¶
Méthode logEmailDelivery()¶
private function logEmailDelivery(
$mairieId,
$incidentId,
$recipientEmail,
$smtpCommand,
$smtpResponse,
$smtpCode,
$success
) {
// 1. Classifier l'erreur
$errorType = $this->classifyError($smtpCode, $smtpResponse);
// 2. Logger dans email_delivery_logs
$db->execute("INSERT INTO email_delivery_logs ...", [...]);
// 3. Mettre à jour mairie
if (!$success && $smtpCode >= 500) {
$db->execute("UPDATE mairies SET email_invalid = 1 ...");
} elseif ($success) {
$db->execute("UPDATE mairies SET email_invalid = 0 ...");
}
}
Logging dans sendEmailSMTPMultipart()¶
Deux points de capture dans la conversation SMTP :
1. RCPT TO — rejet avant envoi du corps :
fputs($socket, "RCPT TO: <{$to}>\r\n");
$response = $this->readResponse($socket);
$smtpCode = intval(substr($response, 0, 3));
if (strpos($response, '250') === false && strpos($response, '2') !== 0) {
if (isset($this->currentMairieId)) {
$this->logEmailDelivery(
$this->currentMairieId, $this->currentIncidentId ?? null,
$to, 'RCPT TO', $response, $smtpCode, false
);
}
fclose($socket);
return false;
}
// Succès RCPT TO
$this->logEmailDelivery(..., 'RCPT TO', ..., true);
2. DATA — rejet après envoi du corps du message :
fputs($socket, $message);
$response = $this->readResponse($socket);
$smtpCodeData = intval(substr($response, 0, 3));
if (strpos($response, '250') === false && strpos($response, '2') !== 0) {
if (isset($this->currentMairieId)) {
$this->logEmailDelivery(
$this->currentMairieId, $this->currentIncidentId ?? null,
$to, 'DATA', $response, $smtpCodeData, false
);
}
fclose($socket);
return false;
}
Méthode legacy sendEmailSMTP()
Utilisée uniquement par sendTestEmail(). Ne dispose pas du logging DATA-level.
Le logging RCPT TO y est présent mais sans impact opérationnel.
Cooldown 7 jours dans submit_incident.php¶
// Avant toute tentative d'envoi
$emailInvalidRecemment = !empty($mairie['email_invalid'])
&& !empty($mairie['email_invalid_since'])
&& strtotime($mairie['email_invalid_since']) > strtotime('-7 days');
if ($hasAdminAccount['count'] == 0 && !empty($mairie['email']) && !$emailInvalidRecemment) {
// → envoi normal
} elseif ($emailInvalidRecemment) {
// → court-circuit : aucune connexion SMTP ouverte
error_log("Email non envoyé: mairie invalide depuis {$mairie['email_invalid_since']}");
}
Comportement du cooldown :
| Situation | Action |
|---|---|
email_invalid = 0 |
Envoi normal |
email_invalid = 1 depuis < 7 jours |
Court-circuit, log error_log uniquement |
email_invalid = 1 depuis ≥ 7 jours |
Nouvelle tentative automatique |
| Tentative réussie après cooldown | email_invalid remis à 0 |
Dashboard Superadmin¶
URL : /superadmin_emails.php
Accès : $auth->requireRole(['superadmin'])
Section 1: Mairies avec emails invalides¶

Affichage de: - Nom et localisation de la mairie - Email actuel (invalide) - Date depuis laquelle invalide + nombre de jours - Raison du rejet (code + message SMTP) - Nombre d'échecs totaux - Nombre d'incidents pour cette mairie
Actions disponibles:
- ✏️ Corriger: Ouvre un modal pour modifier l'email
- 📤 Renvoyer: Renvoie la notification du dernier incident
- ✅ Marquer valide: Réinitialise manuellement le statut
Section 2: Historique des tentatives¶
Journal des 100 dernières tentatives avec: - Date/heure - Mairie et email destinataire - ID incident associé - Commande SMTP - Code de réponse - Statut (succès/échec) - Type d'erreur - Réponse complète du serveur
Utilisation¶
Connexion¶
# URL
http://localhost:8080/login.php
# Identifiants superadmin
Email: superadmin@example.com
Mot de passe: <défini à l'installation, à changer immédiatement>
Workflow de correction¶
- Accéder au dashboard : Menu "📧 Emails Invalides"
- Identifier la mairie : Consulter la liste des emails invalides
- Corriger l'email :
- Cliquer sur "✏️ Corriger"
- Saisir la nouvelle adresse
- Valider
- Tester l'envoi :
- Cliquer sur "📤 Renvoyer"
- Attendre la confirmation
- Vérifier : L'email est automatiquement marqué valide si l'envoi réussit
Rate limiting
Un délai de 5 minutes minimum est requis entre deux tentatives de renvoi pour la même mairie.
Sécurité¶
Protection d'accès¶
// Restriction stricte dans toutes les pages superadmin
$auth = new Auth();
$auth->requireRole(['superadmin']);
Validation des entrées¶
// Validation email
filter_var($newEmail, FILTER_VALIDATE_EMAIL)
// Échappement HTML
htmlspecialchars($value, ENT_QUOTES)
// Prepared statements
$db->execute($sql, $params)
Rate limiting¶
// Vérification de la dernière tentative
$lastAttempt = $db->fetchOne(
"SELECT created_at FROM email_delivery_logs
WHERE mairie_id = ? AND incident_id = ?
ORDER BY created_at DESC LIMIT 1",
[$mairieId, $incidentId]
);
if ($lastAttempt && (time() - strtotime($lastAttempt['created_at'])) < 300) {
// Erreur: attendre 5 minutes
}
Migration¶
Exécution¶
# Backup avant migration
docker-compose exec db mysqldump -u urbafix_user -p \
urbafix > backup_pre_superadmin.sql
# Exécuter la migration
docker-compose exec -T db mysql -u urbafix_user -p \
urbafix < /srv/urbafix/database/migration_superadmin_emails.sql
Vérifications¶
-- Vérifier le compte superadmin
SELECT * FROM users WHERE email = 'superadmin@example.com';
-- Vérifier les colonnes
DESCRIBE mairies;
DESCRIBE email_delivery_logs;
Monitoring¶
Requêtes utiles¶
-- Nombre de mairies avec emails invalides
SELECT COUNT(*) FROM mairies WHERE email_invalid = 1;
-- Top 10 des types d'erreurs (7 derniers jours)
SELECT error_type, COUNT(*) as nb
FROM email_delivery_logs
WHERE status = 'failure'
AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY error_type
ORDER BY nb DESC
LIMIT 10;
-- Mairies avec le plus d'échecs
SELECT m.nom, m.ville, COUNT(*) as nb_echecs
FROM email_delivery_logs l
JOIN mairies m ON l.mairie_id = m.id
WHERE l.status = 'failure'
GROUP BY m.id
ORDER BY nb_echecs DESC
LIMIT 10;
Alertes recommandées¶
- 🚨 Taux d'échecs > 20% sur 24h
- 🚨 Mairie avec > 5 échecs consécutifs
- 🚨 Table
email_delivery_logs> 100k lignes (nettoyage requis)
Tests¶
Scénarios de test¶
Fichiers¶
Créés¶
/srv/urbafix/database/migration_superadmin_emails.sql- Migration complète/srv/urbafix/public/superadmin_emails.php- Dashboard (430 lignes)
Modifiés¶
/srv/urbafix/src/EmailService.php- Ajout logging SMTP/srv/urbafix/public/src/views/header.php- Menu superadmin
Roadmap¶
v1.1¶
- Export CSV des emails invalides
- Notification email au superadmin
- Statistiques graphiques
v1.2¶
- Correction en masse
- Import CSV d'emails corrigés
- Validation DNS MX avant envoi
v2.0¶
- Système de bounce asynchrone (VERP) — seule solution pour capturer les NDR différés
- Webhook notifications temps réel
- API REST pour consultation logs
Limite connue : bounces asynchrones
Si le serveur SMTP accepte RCPT TO et DATA (code 250) mais envoie un NDR (Non-Delivery Report) ultérieurement, ce bounce n'est pas capturé. Nécessite une boîte dédiée avec parsing automatique des messages de bounce (implémentation VERP prévue en v2.0).
Support¶
Besoin d'aide ?
- Documentation complète:
/srv/urbafix/EMAIL_BOUNCES_SUPERADMIN.md - Guide admin:
GUIDE_ADMIN.md - Contact: https://urbafix.fr/#contact