Aller au contenu

services

⬇️ Télécharger cette page en Markdown


EmailService

Fichier : src/EmailService.php

Service d'envoi des notifications email aux mairies lors d'un nouveau signalement.

Responsabilités

  • Envoi SMTP multipart (HTML + texte) via sendEmailSMTPMultipart()
  • Rendu des templates incident_notification_html et incident_notification_text
  • Gestion dynamique du mode test/production via app_settings
  • Logging des succès et échecs dans les logs PHP

Mode test vs production (v3.0.1)

Le mode d'envoi est lu depuis la table app_settings à chaque appel — plus de valeur hardcodée :

$isTestMode = true;
try {
    $row = Database::getInstance()->fetchOne(
        "SELECT `value` FROM app_settings WHERE `key` = 'email_test_mode'"
    );
    $isTestMode = $row ? (bool)(int)$row['value'] : true;
} catch (Exception $e) {
    error_log("EmailService: impossible de lire email_test_mode, fallback TEST: " . $e->getMessage());
}

$recipient  = $isTestMode ? $testEmail : $mairieEmail;
$modeLabel  = $isTestMode ? '[MODE TEST]' : '[PROD]';
Mode app_settings.value Destinataire Préfixe log
TEST '1' l'adresse de test [MODE TEST]
PROD '0' Email réel de la mairie [PROD]

Le fallback en cas d'erreur DB est toujours MODE TEST (fail-safe : aucun email ne part vers une mairie en cas de panne de la table de configuration).

Conditions d'envoi (submit_incident.php)

L'email est envoyé uniquement si toutes ces conditions sont vraies :

  1. La mairie n'a pas de compte administrateur actif — SELECT COUNT(*) FROM users WHERE mairie_id = ? AND actif = 1 → 0
  2. La mairie a une adresse email renseignée (mairies.email non vide)
  3. L'email n'est pas invalide depuis moins de 7 jours (email_invalid + timestamp de moins de 7 jours)
  4. incidents.email_notification_sent = 0 — l'email n'a pas déjà été envoyé pour cet incident

La condition 4 couvre le retry WorkManager : si l'incident est déjà en base suite à un crash 500, le retry détecte le doublon et n'envoie l'email que si email_notification_sent = 0.

Gestion des bounces SMTP

  • Rejet au RCPT TO ou DATA : loggé dans email_delivery_logs + mairies.email_invalid = 1
  • Cooldown 7 jours : aucune tentative SMTP si email_invalid = 1 depuis moins de 7 jours
  • Après 7 jours : nouvelle tentative automatique — un succès remet email_invalid = 0

Utilisation

// Dans submit_incident.php après insertion de l'incident
$emailService = EmailService::getInstance();
$sent = $emailService->sendNewIncidentNotification($incidentId, $mairieEmail);
if ($sent) {
    $db->execute("UPDATE incidents SET email_notification_sent = 1 WHERE id = ?", [$incidentId]);
}

Méthode principale

public function sendNewIncidentNotification(int $incidentId, string $mairieEmail): bool

Charge les données de l'incident + les photos, rend les templates, envoie via SMTP. Retourne true si l'envoi a réussi.


EncryptionManager

Fichier : src/EncryptionManager.php

Chiffrement AES-256-GCM des données sensibles (stacktraces de bug reports, etc.).

Utilisation (singleton uniquement)

// ✅ Correct
$encryption = EncryptionManager::getInstance();

// ❌ Incorrect — constructeur privé (fatal error)
$encryption = new EncryptionManager();

La clé AES est chargée depuis le secret Docker monté en /run/secrets/app_encryption_key (lecture seule).


api/get_photo.php — Déchiffrement à la volée (v3.1, 2026-08-25)

Fichier : public/api/get_photo.php

Sert une photo (GET ?id=<photo_id>), en la déchiffrant automatiquement si photos_incident.encrypted = 1. Détermine le Content-Type par analyse du contenu (finfo_buffer), pas par l'extension du fichier.

Deux bugs corrigés (endpoint jusque-là non fonctionnel sur les photos chiffrées) :

  1. Résolution de chemin fausse : realpath(__DIR__ . '/../uploads') visait public/uploads/ (dossier vide resté dans le dépôt) au lieu de uploads/ à la racine du projet. Corrigé en __DIR__ . '/../../uploads'.
  2. new EncryptionManager() au lieu de EncryptionManager::getInstance() — fatal error non catché (Error, pas Exception) sur toute photo chiffrée. Voir l'avertissement ci-dessus.

incident_detail.php utilise désormais systématiquement cet endpoint pour l'affichage des photos déclarant (<img src="/api/get_photo.php?id=...">), plutôt que le chemin filepath brut — nécessaire pour les photos chiffrées, sans effet sur les autres.

src/ExifHelper.php

Extraction EXIF (date de prise de vue + GPS) depuis des octets image bruts, à appeler avant chiffrement/floutage — voir Face Blurring — EXIF et floutage.

$exif = ExifHelper::extract($imageData); // ['taken_at' => 'Y-m-d H:i:s'|null, 'latitude' => float|null, 'longitude' => float|null]