feat: auto-refresh AJAX, bouton transfert entre comptes, webhook par compte
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -91,5 +91,29 @@ function process_account(array $account, MailMimeParser $parser): void {
|
|||||||
|
|
||||||
// Déplace le mail vers cur/ (marque comme lu)
|
// Déplace le mail vers cur/ (marque comme lu)
|
||||||
rename($filePath, "/var/mail/vhosts/{$domain}/{$account['username']}/Maildir/cur/{$fileName}:2,S");
|
rename($filePath, "/var/mail/vhosts/{$domain}/{$account['username']}/Maildir/cur/{$fileName}:2,S");
|
||||||
|
|
||||||
|
// Webhook
|
||||||
|
if (!empty($account['webhook_url'])) {
|
||||||
|
fire_webhook($account['webhook_url'], [
|
||||||
|
'account' => $account['username'],
|
||||||
|
'email' => $account['email'],
|
||||||
|
'subject' => $subject,
|
||||||
|
'from' => $from,
|
||||||
|
'source_id' => $sourceId,
|
||||||
|
'files' => $attachments ? count($attachments) : 0,
|
||||||
|
'received_at'=> date('c'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fire_webhook(string $url, array $payload): void {
|
||||||
|
$ctx = stream_context_create(['http' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'header' => "Content-Type: application/json\r\n",
|
||||||
|
'content' => json_encode($payload),
|
||||||
|
'timeout' => 5,
|
||||||
|
'ignore_errors' => true,
|
||||||
|
]]);
|
||||||
|
@file_get_contents($url, false, $ctx);
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS accounts (
|
|||||||
email VARCHAR(200) NOT NULL UNIQUE,
|
email VARCHAR(200) NOT NULL UNIQUE,
|
||||||
display_name VARCHAR(100),
|
display_name VARCHAR(100),
|
||||||
smb_path VARCHAR(255) NOT NULL,
|
smb_path VARCHAR(255) NOT NULL,
|
||||||
|
webhook_url VARCHAR(500) NULL,
|
||||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
created_at DATETIME DEFAULT NOW()
|
created_at DATETIME DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,9 +29,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|||||||
if ($st->fetch()) {
|
if ($st->fetch()) {
|
||||||
$error = "Le compte « {$username} » existe déjà.";
|
$error = "Le compte « {$username} » existe déjà.";
|
||||||
} else {
|
} else {
|
||||||
|
$webhook = trim($_POST['webhook_url'] ?? '');
|
||||||
db()->prepare(
|
db()->prepare(
|
||||||
'INSERT INTO accounts (username, email, display_name, smb_path) VALUES (?, ?, ?, ?)'
|
'INSERT INTO accounts (username, email, display_name, smb_path, webhook_url) VALUES (?, ?, ?, ?, ?)'
|
||||||
)->execute([$username, $email, $display, $smbPath]);
|
)->execute([$username, $email, $display, $smbPath, $webhook ?: null]);
|
||||||
|
|
||||||
// Crée le dossier, l'utilisateur Dovecot et le share Samba
|
// Crée le dossier, l'utilisateur Dovecot et le share Samba
|
||||||
$cmd = sprintf(
|
$cmd = sprintf(
|
||||||
@@ -114,6 +115,11 @@ html_navbar('accounts');
|
|||||||
<label class="form-label">Nom affiché <span class="text-muted">(optionnel)</span></label>
|
<label class="form-label">Nom affiché <span class="text-muted">(optionnel)</span></label>
|
||||||
<input type="text" name="display_name" class="form-control" placeholder="Copy Caisse">
|
<input type="text" name="display_name" class="form-control" placeholder="Copy Caisse">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Webhook URL <span class="text-muted">(optionnel)</span></label>
|
||||||
|
<input type="url" name="webhook_url" class="form-control" placeholder="https://…/webhook">
|
||||||
|
<div class="form-text">POST JSON à chaque réception de mail.</div>
|
||||||
|
</div>
|
||||||
<button type="submit" class="btn btn-primary w-100">
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
<i class="bi bi-plus-circle me-1"></i>Créer le compte
|
<i class="bi bi-plus-circle me-1"></i>Créer le compte
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
34
web/api/mails.php
Normal file
34
web/api/mails.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../includes/db.php';
|
||||||
|
require_once __DIR__ . '/../includes/auth.php';
|
||||||
|
|
||||||
|
auth_check();
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$rows = db()->query(
|
||||||
|
'SELECT sf.id, sf.title, sf.sender, sf.date_processing, a.username,
|
||||||
|
GROUP_CONCAT(f.id, ":", f.file_name, ":", f.path ORDER BY f.id SEPARATOR "|") as files
|
||||||
|
FROM source_file sf
|
||||||
|
LEFT JOIN accounts a ON sf.account_id = a.id
|
||||||
|
LEFT JOIN files f ON sf.id = f.source_file_id
|
||||||
|
GROUP BY sf.id
|
||||||
|
ORDER BY sf.date_processing DESC'
|
||||||
|
)->fetchAll();
|
||||||
|
|
||||||
|
// Vérifie l'existence physique des fichiers
|
||||||
|
foreach ($rows as &$r) {
|
||||||
|
$r['files_list'] = [];
|
||||||
|
if ($r['files']) {
|
||||||
|
foreach (explode('|', $r['files']) as $f) {
|
||||||
|
[$fid, $fname, $fpath] = array_pad(explode(':', $f, 3), 3, '');
|
||||||
|
$r['files_list'][] = [
|
||||||
|
'id' => (int)$fid,
|
||||||
|
'name' => $fname,
|
||||||
|
'exists' => file_exists($fpath),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($r['files']);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($rows);
|
||||||
60
web/api/transfer.php
Normal file
60
web/api/transfer.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../includes/db.php';
|
||||||
|
require_once __DIR__ . '/../includes/auth.php';
|
||||||
|
|
||||||
|
auth_check();
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405); echo json_encode(['error' => 'Method not allowed']); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceId = (int)($_POST['source_id'] ?? 0);
|
||||||
|
$targetId = (int)($_POST['target_id'] ?? 0);
|
||||||
|
|
||||||
|
if (!$sourceId || !$targetId) {
|
||||||
|
http_response_code(400); echo json_encode(['error' => 'Paramètres manquants']); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupère le compte destination
|
||||||
|
$target = db()->prepare('SELECT * FROM accounts WHERE id = ? AND active = 1');
|
||||||
|
$target->execute([$targetId]);
|
||||||
|
$targetAccount = $target->fetch();
|
||||||
|
if (!$targetAccount) {
|
||||||
|
http_response_code(404); echo json_encode(['error' => 'Compte destination introuvable']); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupère les fichiers à transférer
|
||||||
|
$files = db()->prepare('SELECT * FROM files WHERE source_file_id = ?');
|
||||||
|
$files->execute([$sourceId]);
|
||||||
|
$fileList = $files->fetchAll();
|
||||||
|
|
||||||
|
$destDir = $targetAccount['smb_path'];
|
||||||
|
if (!is_dir($destDir)) mkdir($destDir, 0750, true);
|
||||||
|
|
||||||
|
$moved = 0;
|
||||||
|
foreach ($fileList as $file) {
|
||||||
|
$newPath = $destDir . '/' . basename($file['path']);
|
||||||
|
// Évite collision
|
||||||
|
if (file_exists($newPath)) {
|
||||||
|
$ext = pathinfo($newPath, PATHINFO_EXTENSION);
|
||||||
|
$base = pathinfo($newPath, PATHINFO_FILENAME);
|
||||||
|
$newPath = $destDir . '/' . $base . '_' . time() . '.' . $ext;
|
||||||
|
}
|
||||||
|
if (file_exists($file['path'])) {
|
||||||
|
rename($file['path'], $newPath);
|
||||||
|
db()->prepare('UPDATE files SET path = ? WHERE id = ?')
|
||||||
|
->execute([$newPath, $file['id']]);
|
||||||
|
$moved++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Met à jour le compte source_file
|
||||||
|
db()->prepare('UPDATE source_file SET account_id = ? WHERE id = ?')
|
||||||
|
->execute([$targetId, $sourceId]);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'moved' => $moved,
|
||||||
|
'target' => $targetAccount['username'],
|
||||||
|
]);
|
||||||
221
web/index.php
221
web/index.php
@@ -5,12 +5,10 @@ require_once __DIR__ . '/includes/layout.php';
|
|||||||
|
|
||||||
auth_check();
|
auth_check();
|
||||||
|
|
||||||
$message = '';
|
// Suppression
|
||||||
|
|
||||||
// Suppression d'un email traité
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_source'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_source'])) {
|
||||||
$id = (int)$_POST['delete_source'];
|
$id = (int)$_POST['delete_source'];
|
||||||
$st = db()->prepare('SELECT sf.title, CONCAT(sf.file_name) as fname, GROUP_CONCAT(f.path) as fpaths
|
$st = db()->prepare('SELECT sf.title, GROUP_CONCAT(f.path) as fpaths
|
||||||
FROM source_file sf LEFT JOIN files f ON sf.id = f.source_file_id
|
FROM source_file sf LEFT JOIN files f ON sf.id = f.source_file_id
|
||||||
WHERE sf.id = ? GROUP BY sf.id');
|
WHERE sf.id = ? GROUP BY sf.id');
|
||||||
$st->execute([$id]);
|
$st->execute([$id]);
|
||||||
@@ -22,21 +20,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_source'])) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
db()->prepare('DELETE FROM source_file WHERE id = ?')->execute([$id]);
|
db()->prepare('DELETE FROM source_file WHERE id = ?')->execute([$id]);
|
||||||
flash('success', "Entrée « " . htmlspecialchars($row['title']) . " » supprimée.");
|
flash('success', "Entrée supprimée.");
|
||||||
}
|
}
|
||||||
header('Location: /');
|
header('Location: /');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$rows = db()->query(
|
// Comptes pour le dropdown transfert
|
||||||
'SELECT sf.id, sf.title, sf.sender, sf.date_processing, a.username,
|
$otherAccounts = db()->query('SELECT id, username, display_name FROM accounts WHERE active = 1 ORDER BY username')->fetchAll();
|
||||||
GROUP_CONCAT(f.id, ":", f.file_name, ":", f.path ORDER BY f.id SEPARATOR "|") as files
|
|
||||||
FROM source_file sf
|
|
||||||
LEFT JOIN accounts a ON sf.account_id = a.id
|
|
||||||
LEFT JOIN files f ON sf.id = f.source_file_id
|
|
||||||
GROUP BY sf.id
|
|
||||||
ORDER BY sf.date_processing DESC'
|
|
||||||
)->fetchAll();
|
|
||||||
|
|
||||||
html_head('Courriers');
|
html_head('Courriers');
|
||||||
html_navbar('mail');
|
html_navbar('mail');
|
||||||
@@ -45,15 +36,13 @@ html_navbar('mail');
|
|||||||
<?php flash_render(); ?>
|
<?php flash_render(); ?>
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h5 class="mb-0"><i class="bi bi-envelope-fill me-2"></i>Courriers traités</h5>
|
<h5 class="mb-0"><i class="bi bi-envelope-fill me-2"></i>Courriers traités</h5>
|
||||||
<span class="badge bg-secondary"><?= count($rows) ?> entrée(s)</span>
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span id="mail-count" class="badge bg-secondary">—</span>
|
||||||
|
<span class="text-muted small"><i class="bi bi-arrow-clockwise"></i> <span id="refresh-countdown">10</span>s</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (empty($rows)): ?>
|
<div class="card shadow-sm" id="mail-card">
|
||||||
<div class="alert alert-info">
|
|
||||||
<i class="bi bi-info-circle me-2"></i>Aucun courrier traité pour l'instant.
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="card shadow-sm">
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
@@ -63,60 +52,158 @@ html_navbar('mail');
|
|||||||
<th>Sujet</th>
|
<th>Sujet</th>
|
||||||
<th>Expéditeur</th>
|
<th>Expéditeur</th>
|
||||||
<th>Pièces jointes</th>
|
<th>Pièces jointes</th>
|
||||||
<th class="text-center">Action</th>
|
<th class="text-center" style="width:110px">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody id="mail-tbody">
|
||||||
<?php foreach ($rows as $r): ?>
|
<tr><td colspan="6" class="text-center text-muted py-4">Chargement…</td></tr>
|
||||||
<tr>
|
|
||||||
<td class="text-nowrap text-muted small">
|
|
||||||
<?= htmlspecialchars(date('d/m/Y H:i', strtotime($r['date_processing']))) ?>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<?php if ($r['username']): ?>
|
|
||||||
<span class="badge bg-primary"><?= htmlspecialchars($r['username']) ?></span>
|
|
||||||
<?php else: ?>
|
|
||||||
<span class="text-muted">—</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td><?= htmlspecialchars($r['title'] ?? '(sans sujet)') ?></td>
|
|
||||||
<td class="small text-muted"><?= htmlspecialchars($r['sender'] ?? '') ?></td>
|
|
||||||
<td>
|
|
||||||
<?php if ($r['files']): ?>
|
|
||||||
<?php foreach (explode('|', $r['files']) as $f): ?>
|
|
||||||
<?php [$fid, $fname, $fpath] = array_pad(explode(':', $f, 3), 3, ''); ?>
|
|
||||||
<?php if (file_exists($fpath)): ?>
|
|
||||||
<a href="/mail/download.php?id=<?= (int)$fid ?>" class="badge bg-info text-decoration-none me-1">
|
|
||||||
<i class="bi bi-download me-1"></i><?= htmlspecialchars(substr($fname, 0, 20)) ?>
|
|
||||||
</a>
|
|
||||||
<?php else: ?>
|
|
||||||
<span class="badge bg-secondary me-1" title="Fichier introuvable">
|
|
||||||
<?= htmlspecialchars(substr($fname, 0, 20)) ?>
|
|
||||||
</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php else: ?>
|
|
||||||
<span class="text-muted small">Aucune pièce jointe</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<form method="post" onsubmit="return confirm('Supprimer cette entrée ?')">
|
|
||||||
<input type="hidden" name="delete_source" value="<?= $r['id'] ?>">
|
|
||||||
<button class="btn btn-sm btn-outline-danger">
|
|
||||||
<i class="bi bi-trash"></i>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<div id="empty-msg" class="alert alert-info mt-3" style="display:none">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>Aucun courrier traité pour l'instant.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast -->
|
||||||
|
<div class="position-fixed bottom-0 end-0 p-3" style="z-index:9999">
|
||||||
|
<div id="toast-el" class="toast align-items-center border-0" role="alert">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body" id="toast-body"></div>
|
||||||
|
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
setTimeout(() => location.reload(), 30000);
|
const ALL_ACCOUNTS = <?= json_encode($otherAccounts) ?>;
|
||||||
|
let knownIds = null;
|
||||||
|
let countdown = 10;
|
||||||
|
let countdownTimer = null;
|
||||||
|
|
||||||
|
function showToast(msg, type = 'success') {
|
||||||
|
const el = document.getElementById('toast-el');
|
||||||
|
el.className = `toast align-items-center text-bg-${type} border-0`;
|
||||||
|
document.getElementById('toast-body').textContent = msg;
|
||||||
|
bootstrap.Toast.getOrCreateInstance(el, {delay: 4000}).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTransferDropdown(sourceId, currentUsername) {
|
||||||
|
const others = ALL_ACCOUNTS.filter(a => a.username !== currentUsername);
|
||||||
|
if (!others.length) return '';
|
||||||
|
const items = others.map(a =>
|
||||||
|
`<li><a class="dropdown-item" href="#" onclick="doTransfer(event,${sourceId},${a.id},'${a.username}')">
|
||||||
|
<i class="bi bi-arrow-right-circle me-1 text-primary"></i>${a.username}${a.display_name ? ' <span class=text-muted>— '+a.display_name+'</span>' : ''}
|
||||||
|
</a></li>`
|
||||||
|
).join('');
|
||||||
|
return `<div class="btn-group">
|
||||||
|
<button class="btn btn-sm btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown" title="Transférer vers…">
|
||||||
|
<i class="bi bi-share"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end shadow">${items}</ul>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function doTransfer(e, sourceId, targetId, targetName) {
|
||||||
|
e.preventDefault();
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('source_id', sourceId);
|
||||||
|
fd.append('target_id', targetId);
|
||||||
|
fetch('/api/transfer.php', {method:'POST', body: fd})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast(`Transféré vers ${data.target} (${data.moved} fichier(s))`);
|
||||||
|
loadMails();
|
||||||
|
} else {
|
||||||
|
showToast(data.error || 'Erreur lors du transfert', 'danger');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => showToast('Erreur réseau', 'danger'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMails(rows) {
|
||||||
|
const tbody = document.getElementById('mail-tbody');
|
||||||
|
const card = document.getElementById('mail-card');
|
||||||
|
const empty = document.getElementById('empty-msg');
|
||||||
|
document.getElementById('mail-count').textContent = rows.length + ' entrée(s)';
|
||||||
|
|
||||||
|
// Détecte nouveaux mails
|
||||||
|
const newIds = new Set(rows.map(r => r.id));
|
||||||
|
if (knownIds !== null && [...newIds].some(id => !knownIds.has(id))) {
|
||||||
|
showToast('Nouveau courrier reçu !');
|
||||||
|
}
|
||||||
|
knownIds = newIds;
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
card.style.display = 'none';
|
||||||
|
empty.style.display = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
card.style.display = '';
|
||||||
|
empty.style.display = 'none';
|
||||||
|
|
||||||
|
tbody.innerHTML = rows.map(r => {
|
||||||
|
const date = new Date(r.date_processing).toLocaleString('fr-FR', {
|
||||||
|
day:'2-digit', month:'2-digit', year:'numeric', hour:'2-digit', minute:'2-digit'
|
||||||
|
});
|
||||||
|
let filesHtml = '<span class="text-muted small">Aucune pièce jointe</span>';
|
||||||
|
if (r.files_list && r.files_list.length) {
|
||||||
|
filesHtml = r.files_list.map(f =>
|
||||||
|
f.exists
|
||||||
|
? `<a href="/mail/download.php?id=${f.id}" class="badge bg-info text-decoration-none me-1"><i class="bi bi-download me-1"></i>${escHtml(f.name.substring(0,22))}</a>`
|
||||||
|
: `<span class="badge bg-secondary me-1" title="Fichier introuvable">${escHtml(f.name.substring(0,22))}</span>`
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
const transferBtn = buildTransferDropdown(r.id, r.username);
|
||||||
|
return `<tr id="row-${r.id}">
|
||||||
|
<td class="text-nowrap text-muted small">${date}</td>
|
||||||
|
<td>${r.username ? `<span class="badge bg-primary">${escHtml(r.username)}</span>` : '<span class="text-muted">—</span>'}</td>
|
||||||
|
<td>${escHtml(r.title || '(sans sujet)')}</td>
|
||||||
|
<td class="small text-muted">${escHtml(r.sender || '')}</td>
|
||||||
|
<td>${filesHtml}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<div class="d-flex gap-1 justify-content-center">
|
||||||
|
${transferBtn}
|
||||||
|
<form method="post" class="d-inline" onsubmit="return confirm('Supprimer cette entrée ?')">
|
||||||
|
<input type="hidden" name="delete_source" value="${r.id}">
|
||||||
|
<button class="btn btn-sm btn-outline-danger" title="Supprimer"><i class="bi bi-trash"></i></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escHtml(s) {
|
||||||
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMails() {
|
||||||
|
fetch('/api/mails.php')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(renderMails)
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCountdown() {
|
||||||
|
if (countdownTimer) clearInterval(countdownTimer);
|
||||||
|
countdown = 10;
|
||||||
|
const el = document.getElementById('refresh-countdown');
|
||||||
|
countdownTimer = setInterval(() => {
|
||||||
|
countdown--;
|
||||||
|
if (el) el.textContent = countdown;
|
||||||
|
if (countdown <= 0) {
|
||||||
|
clearInterval(countdownTimer);
|
||||||
|
loadMails();
|
||||||
|
startCountdown();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMails();
|
||||||
|
startCountdown();
|
||||||
</script>
|
</script>
|
||||||
<?php html_foot(); ?>
|
<?php html_foot(); ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user