Feat: onglet SMTP dans l'administration

- Formulaire d'édition des paramètres SMTP (serveur, port, chiffrement,
  utilisateur, mot de passe, expéditeur) stockés dans data/smtp_settings.json
  (écrit par www-data, contrairement au .env en lecture seule)
- Test de connexion SMTP avec logs PHPMailer complets (DEBUG_SERVER)
- Envoi d'email de test avec contenu personnalisé anti-spam
- src/SmtpSettings.php : lecture/écriture smtp_settings.json avec fallback env()
- mailer.php : lit les paramètres depuis SmtpSettings en priorité
- admin.js : indicateurs spinner sur les boutons pendant le traitement
This commit is contained in:
Cedric Abonnel
2026-05-13 10:53:03 +02:00
parent 4cc4a01534
commit 26ada9b54e
5 changed files with 377 additions and 8 deletions
+28
View File
@@ -1,4 +1,5 @@
document.addEventListener('DOMContentLoaded', function () {
// Sélection globale articles
var checkAll = document.getElementById('check-all');
if (checkAll) {
checkAll.addEventListener('change', function () {
@@ -7,4 +8,31 @@ document.addEventListener('DOMContentLoaded', function () {
});
});
}
// Indicateurs de traitement formulaire SMTP (config + tester connexion)
var smtpForm = document.getElementById('smtp-config-form');
if (smtpForm) {
smtpForm.addEventListener('submit', function (e) {
var clicked = e.submitter;
if (!clicked) return;
smtpForm.querySelectorAll('button[type="submit"]').forEach(function (btn) {
btn.disabled = true;
});
var isSave = clicked.id === 'smtp-save-btn';
clicked.innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>'
+ (isSave ? 'Enregistrement…' : 'En cours…');
});
}
// Indicateur de traitement envoi email de test
var smtpTestForm = document.getElementById('smtp-test-form');
if (smtpTestForm) {
smtpTestForm.addEventListener('submit', function () {
var btn = document.getElementById('smtp-send-btn');
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>En cours…';
}
});
}
});
+129
View File
@@ -1991,9 +1991,138 @@ switch ($action) {
}
}
if ($tab === 'smtp') {
if (!isAdmin()) {
http_response_code(403);
exit;
}
require_once BASE_PATH . '/src/SmtpSettings.php';
$adminData['smtp_config'] = [
'host' => smtpCfg('host', 'SMTP_HOST'),
'port' => smtpCfg('port', 'SMTP_PORT'),
'secure' => smtpCfg('secure', 'SMTP_SECURE'),
'user' => smtpCfg('user', 'SMTP_USER'),
'has_pass' => smtpCfg('pass', 'SMTP_PASS') !== '',
'from' => smtpCfg('from', 'SMTP_FROM'),
'from_name' => smtpCfg('from_name', 'SMTP_FROM_NAME'),
];
$adminData['smtp_test'] = $_SESSION['smtp_test_result'] ?? null;
unset($_SESSION['smtp_test_result']);
}
include BASE_PATH . '/templates/admin.php';
break;
case 'admin_smtp_save':
requireAuth();
if (!isAdmin()) {
http_response_code(403);
exit;
}
require_once BASE_PATH . '/src/SmtpSettings.php';
saveSmtpSettings([
'host' => $_POST['smtp_host'] ?? '',
'port' => $_POST['smtp_port'] ?? '',
'secure' => $_POST['smtp_secure'] ?? '',
'user' => $_POST['smtp_user'] ?? '',
'pass' => $_POST['smtp_pass'] ?? '',
'from' => $_POST['smtp_from'] ?? '',
'from_name' => $_POST['smtp_from_name'] ?? '',
]);
header('Location: /admin/smtp?saved=1');
exit;
case 'admin_smtp_test':
requireAuth();
if (!isAdmin()) {
http_response_code(403);
exit;
}
require_once BASE_PATH . '/src/SmtpSettings.php';
require_once BASE_PATH . '/src/mailer.php';
$mode = in_array($_POST['mode'] ?? '', ['connect', 'send'], true) ? $_POST['mode'] : 'connect';
$testEmail = trim($_POST['test_email'] ?? '');
if ($testEmail !== '' && !filter_var($testEmail, FILTER_VALIDATE_EMAIL)) {
$testEmail = '';
}
$smtpLogs = [];
$smtpOk = false;
$smtpErrMsg = '';
try {
$mail = new \PHPMailer\PHPMailer\PHPMailer(true);
$mail->isSMTP();
$mail->Host = smtpCfg('host', 'SMTP_HOST', 'localhost');
$mail->Port = (int)smtpCfg('port', 'SMTP_PORT', '587');
$_stUser = smtpCfg('user', 'SMTP_USER');
$_stPass = smtpCfg('pass', 'SMTP_PASS');
$mail->SMTPAuth = ($_stUser !== '' || $_stPass !== '');
$mail->Username = $_stUser;
$mail->Password = $_stPass;
$smtpSecure = strtolower(smtpCfg('secure', 'SMTP_SECURE', 'tls'));
if ($smtpSecure === 'ssl') {
$mail->SMTPSecure = \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS;
} elseif ($smtpSecure === 'tls') {
$mail->SMTPSecure = \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
}
$mail->Timeout = 15;
$mail->SMTPOptions = ['ssl' => ['verify_peer' => true, 'verify_peer_name' => true, 'allow_self_signed' => false]];
$mail->SMTPDebug = \PHPMailer\PHPMailer\SMTP::DEBUG_SERVER;
$mail->Debugoutput = static function (string $str) use (&$smtpLogs): void {
$smtpLogs[] = rtrim($str);
};
if ($mode === 'send' && $testEmail !== '') {
$mail->CharSet = 'UTF-8';
$mail->isHTML(true);
$_smtpFrom = smtpCfg('from', 'SMTP_FROM', 'no-reply@varlog.a5l.fr');
$_smtpFromName = smtpCfg('from_name', 'SMTP_FROM_NAME', 'varlog');
$mail->setFrom($_smtpFrom, $_smtpFromName);
$mail->addAddress($testEmail);
$_siteName = siteTitle();
$_siteUrl = rtrim(APP_URL, '/');
$_sentAt = date('d/m/Y à H\hi', time());
$mail->Subject = 'Vérification de la configuration email — ' . $_siteName;
$mail->Body = '<!DOCTYPE html><html lang="fr"><head><meta charset="UTF-8">'
. '<meta name="viewport" content="width=device-width,initial-scale=1"></head>'
. '<body style="font-family:sans-serif;color:#1a1a1a;max-width:520px;margin:0 auto;padding:32px 16px">'
. '<p>Bonjour,</p>'
. '<p>Cet email confirme que la configuration SMTP de <strong>' . htmlspecialchars($_siteName) . '</strong> fonctionne correctement.</p>'
. '<p>Envoyé le ' . $_sentAt . ' depuis <a href="' . htmlspecialchars($_siteUrl) . '">' . htmlspecialchars($_siteUrl) . '</a>.</p>'
. '<hr style="border:none;border-top:1px solid #e5e7eb;margin:28px 0">'
. '<p style="color:#6b7280;font-size:0.82em">Vous recevez cet email car un administrateur a effectué un test de configuration depuis l\'interface d\'administration de ' . htmlspecialchars($_siteName) . '.'
. ' Si vous n\'attendiez pas cet email, vous pouvez l\'ignorer.</p>'
. '</body></html>';
$mail->AltBody = "Bonjour,\r\n\r\n"
. "Cet email confirme que la configuration SMTP de {$_siteName} fonctionne correctement.\r\n\r\n"
. "Envoyé le {$_sentAt} depuis {$_siteUrl}.\r\n\r\n"
. "--\r\n"
. "Vous recevez cet email car un administrateur a effectué un test de configuration depuis l'interface d'administration de {$_siteName}."
. " Si vous n'attendiez pas cet email, vous pouvez l'ignorer.";
$mail->send();
} else {
$mail->smtpConnect();
$mail->smtpClose();
}
$smtpOk = true;
} catch (\Exception $e) {
$smtpErrMsg = $e->getMessage();
}
$_SESSION['smtp_test_result'] = [
'success' => $smtpOk,
'error' => $smtpErrMsg,
'logs' => $smtpLogs,
'mode' => $mode,
'email' => $testEmail,
'ts' => date('d/m/Y H:i:s'),
];
header('Location: /admin/smtp');
exit;
case 'admin_bulk_delete':
requireAuth();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+53
View File
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
function smtpSettingsPath(): string
{
return BASE_PATH . '/data/smtp_settings.json';
}
function smtpSettings(): array
{
static $s = null;
if ($s !== null) {
return $s;
}
$path = smtpSettingsPath();
if (is_file($path)) {
$data = @json_decode((string)file_get_contents($path), true);
if (is_array($data)) {
$s = $data;
return $s;
}
}
$s = [];
return $s;
}
function smtpCfg(string $key, string $envKey, string $default = ''): string
{
$s = smtpSettings();
if (isset($s[$key]) && (string)$s[$key] !== '') {
return (string)$s[$key];
}
$v = $_ENV[$envKey] ?? getenv($envKey);
return ($v !== false && $v !== '') ? (string)$v : $default;
}
function saveSmtpSettings(array $data): void
{
$current = smtpSettings();
foreach (['host', 'port', 'secure', 'user', 'from', 'from_name'] as $key) {
if (array_key_exists($key, $data)) {
$current[$key] = trim((string)$data[$key]);
}
}
if (!empty($data['pass']) && trim((string)$data['pass']) !== '') {
$current['pass'] = trim((string)$data['pass']);
}
file_put_contents(
smtpSettingsPath(),
json_encode($current, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
);
}
+15 -8
View File
@@ -10,6 +10,10 @@ use PHPMailer\PHPMailer\Exception;
require_once dirname(__DIR__) . '/vendor/autoload.php';
if (defined('BASE_PATH') && !function_exists('smtpCfg')) {
require_once dirname(__DIR__) . '/src/SmtpSettings.php';
}
if (!function_exists('env')) {
function env(string $key, ?string $default = null): ?string
{
@@ -115,12 +119,15 @@ function envoyer_mail_smtp(string $to, string $subject, string $html, ?string $t
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = (string)env('SMTP_HOST', 'localhost');
$mail->Port = (int)env('SMTP_PORT', '587');
$mail->SMTPAuth = (env('SMTP_USER') || env('SMTP_PASS')) ? true : false;
$mail->Username = (string)env('SMTP_USER', '');
$mail->Password = (string)env('SMTP_PASS', '');
$secure = strtolower((string)env('SMTP_SECURE', 'tls'));
$_smtpRead = function_exists('smtpCfg');
$mail->Host = $_smtpRead ? smtpCfg('host', 'SMTP_HOST', 'localhost') : (string)env('SMTP_HOST', 'localhost');
$mail->Port = (int)($_smtpRead ? smtpCfg('port', 'SMTP_PORT', '587') : env('SMTP_PORT', '587'));
$_smtpUser = $_smtpRead ? smtpCfg('user', 'SMTP_USER') : (string)env('SMTP_USER', '');
$_smtpPass = $_smtpRead ? smtpCfg('pass', 'SMTP_PASS') : (string)env('SMTP_PASS', '');
$mail->SMTPAuth = ($_smtpUser !== '' || $_smtpPass !== '');
$mail->Username = $_smtpUser;
$mail->Password = $_smtpPass;
$secure = strtolower($_smtpRead ? smtpCfg('secure', 'SMTP_SECURE', 'tls') : (string)env('SMTP_SECURE', 'tls'));
if ($secure === 'ssl') {
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
} elseif ($secure === 'tls') {
@@ -135,8 +142,8 @@ function envoyer_mail_smtp(string $to, string $subject, string $html, ?string $t
$mail->isHTML(true);
// Expéditeur
$from = (string)env('SMTP_FROM', 'no-reply@mug.a5l.fr');
$fromName = (string)env('SMTP_FROM_NAME', 'MUG');
$from = $_smtpRead ? smtpCfg('from', 'SMTP_FROM', 'no-reply@varlog.a5l.fr') : (string)env('SMTP_FROM', 'no-reply@varlog.a5l.fr');
$fromName = $_smtpRead ? smtpCfg('from_name', 'SMTP_FROM_NAME', 'varlog') : (string)env('SMTP_FROM_NAME', 'varlog');
$mail->setFrom($from, $fromName);
// Reply-To
+152
View File
@@ -52,6 +52,10 @@ function adminStatusBadge(array $a, int $now): string
<a class="nav-link <?= $tab === 'comments' ? 'active' : '' ?>"
href="/admin/comments">Commentaires</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'smtp' ? 'active' : '' ?>"
href="/admin/smtp">SMTP</a>
</li>
<?php endif; ?>
</ul>
@@ -461,6 +465,154 @@ function adminStatusBadge(array $a, int $now): string
<?php endif; ?>
<!-- ─────────────────────────── SMTP ─────────────────────────────── -->
<?php if ($tab === 'smtp' && isAdmin()): ?>
<?php $sc = $adminData['smtp_config']; ?>
<?php if (isset($_GET['saved'])): ?>
<div class="alert alert-success py-2 mb-3">Paramètres SMTP enregistrés.</div>
<?php endif; ?>
<div class="row g-4">
<!-- Formulaire config + actions -->
<div class="col-lg-5">
<div class="card">
<div class="card-header">Configuration SMTP</div>
<div class="card-body">
<form method="post" id="smtp-config-form">
<div class="mb-3">
<label for="smtp-host" class="form-label small fw-semibold">Serveur</label>
<input type="text" id="smtp-host" name="smtp_host"
class="form-control form-control-sm font-monospace"
value="<?= htmlspecialchars($sc['host']) ?>"
placeholder="smtp.exemple.fr">
</div>
<div class="row g-2 mb-3">
<div class="col-5">
<label for="smtp-port" class="form-label small fw-semibold">Port</label>
<input type="number" id="smtp-port" name="smtp_port"
class="form-control form-control-sm"
value="<?= htmlspecialchars($sc['port']) ?>"
placeholder="587" min="1" max="65535">
</div>
<div class="col-7">
<label for="smtp-secure" class="form-label small fw-semibold">Chiffrement</label>
<select id="smtp-secure" name="smtp_secure" class="form-select form-select-sm">
<option value="" <?= $sc['secure'] === '' ? 'selected' : '' ?>>Aucun</option>
<option value="tls" <?= strtolower($sc['secure']) === 'tls' ? 'selected' : '' ?>>STARTTLS (587)</option>
<option value="ssl" <?= strtolower($sc['secure']) === 'ssl' ? 'selected' : '' ?>>SSL/TLS (465)</option>
</select>
</div>
</div>
<div class="mb-3">
<label for="smtp-user" class="form-label small fw-semibold">Utilisateur</label>
<input type="text" id="smtp-user" name="smtp_user"
class="form-control form-control-sm"
value="<?= htmlspecialchars($sc['user']) ?>"
placeholder="user@exemple.fr"
autocomplete="off">
</div>
<div class="mb-3">
<label for="smtp-pass" class="form-label small fw-semibold">Mot de passe</label>
<input type="password" id="smtp-pass" name="smtp_pass"
class="form-control form-control-sm"
placeholder="<?= $sc['has_pass'] ? '(inchangé si vide)' : '' ?>"
autocomplete="new-password">
<?php if ($sc['has_pass']): ?>
<div class="form-text">Laisser vide pour conserver le mot de passe actuel.</div>
<?php endif; ?>
</div>
<div class="mb-3">
<label for="smtp-from" class="form-label small fw-semibold">Email expéditeur</label>
<input type="email" id="smtp-from" name="smtp_from"
class="form-control form-control-sm"
value="<?= htmlspecialchars($sc['from']) ?>"
placeholder="no-reply@exemple.fr">
</div>
<div class="mb-3">
<label for="smtp-from-name" class="form-label small fw-semibold">Nom expéditeur</label>
<input type="text" id="smtp-from-name" name="smtp_from_name"
class="form-control form-control-sm"
value="<?= htmlspecialchars($sc['from_name']) ?>"
placeholder="Mon Site">
</div>
<div class="d-flex flex-wrap gap-2">
<button type="submit" formaction="/?action=admin_smtp_save"
class="btn btn-primary btn-sm" id="smtp-save-btn">
Enregistrer
</button>
<button type="submit" formaction="/?action=admin_smtp_test"
name="mode" value="connect"
class="btn btn-outline-secondary btn-sm">
Tester la connexion
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Envoi de test + résultats -->
<div class="col-lg-7">
<div class="card mb-3">
<div class="card-body">
<form method="post" action="/?action=admin_smtp_test" id="smtp-test-form">
<input type="hidden" name="mode" value="send">
<label for="smtp-email" class="form-label small fw-semibold">Envoyer un email de test</label>
<div class="d-flex gap-2">
<input type="email" id="smtp-email" name="test_email"
class="form-control form-control-sm"
placeholder="destinataire@exemple.fr"
value="<?= htmlspecialchars($adminData['smtp_test']['email'] ?? '') ?>"
required>
<button type="submit" class="btn btn-outline-secondary btn-sm text-nowrap" id="smtp-send-btn">
Envoyer
</button>
</div>
</form>
</div>
</div>
<?php if (isset($adminData['smtp_test'])): ?>
<?php $st = $adminData['smtp_test']; ?>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>
<?php if ($st['success']): ?>
<span class="text-success fw-semibold"> Succès</span>
<?php else: ?>
<span class="text-danger fw-semibold"> Échec</span>
<?php endif; ?>
<?= $st['mode'] === 'send'
? 'Envoi vers ' . htmlspecialchars($st['email'])
: 'Test de connexion' ?>
</span>
<span class="text-muted small"><?= htmlspecialchars($st['ts']) ?></span>
</div>
<?php if (!$st['success'] && $st['error'] !== ''): ?>
<div class="alert alert-danger mb-0 rounded-0 border-0 border-bottom py-2 px-3 small">
<?= htmlspecialchars($st['error']) ?>
</div>
<?php endif; ?>
<?php if (!empty($st['logs'])): ?>
<pre class="p-3 mb-0 small" style="max-height:420px;overflow-y:auto;font-size:0.75rem;background:var(--vl-code-bg,#f8f9fa);border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)"><?php
foreach ($st['logs'] as $line) {
echo htmlspecialchars($line) . "\n";
}
?></pre>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<script src="/assets/js/admin.js" defer></script>
<?php endif; ?>
<!-- ─────────────────────────── COMMENTAIRES ──────────────────────── -->
<?php if ($tab === 'comments' && isAdmin()): ?>