Files
wevads-platform/scripts/smtp-tester.php
2026-02-26 04:53:11 +01:00

403 lines
25 KiB
PHP
Executable File

<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
function getDb() {
static $pdo = null;
if ($pdo === null) {
$pdo = new PDO('pgsql:host=localhost;dbname=adx_system', 'admin', 'admin123');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
return $pdo;
}
function ensureTables($pdo) {
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.smtp_servers (id SERIAL PRIMARY KEY, name VARCHAR(255), host VARCHAR(255), port INTEGER DEFAULT 587, username VARCHAR(255), password VARCHAR(255), encryption VARCHAR(20) DEFAULT 'tls', from_email VARCHAR(255), from_name VARCHAR(255), status VARCHAR(50) DEFAULT 'active', last_test TIMESTAMP, created_at TIMESTAMP DEFAULT NOW())");
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.smtp_tests (id SERIAL PRIMARY KEY, server_id INTEGER, to_email VARCHAR(255), subject VARCHAR(255), status VARCHAR(50), response TEXT, latency_ms INTEGER, created_at TIMESTAMP DEFAULT NOW())");
}
function testSMTP($host, $port, $user, $pass, $encryption, $from, $fromName, $to, $subject, $body) {
$start = microtime(true);
$result = ['success' => false, 'message' => '', 'latency' => 0];
try {
$socket = @fsockopen($encryption === 'ssl' ? "ssl://$host" : $host, $port, $errno, $errstr, 10);
if (!$socket) {
$result['message'] = "Connection failed: $errstr";
return $result;
}
stream_set_timeout($socket, 10);
$response = fgets($socket, 515);
if (substr($response, 0, 3) != '220') {
$result['message'] = "Invalid greeting: $response";
fclose($socket);
return $result;
}
// EHLO
fputs($socket, "EHLO localhost\r\n");
$response = '';
while ($line = fgets($socket, 515)) {
$response .= $line;
if (substr($line, 3, 1) == ' ') break;
}
// STARTTLS if needed
if ($encryption === 'tls') {
fputs($socket, "STARTTLS\r\n");
fgets($socket, 515);
stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
fputs($socket, "EHLO localhost\r\n");
while ($line = fgets($socket, 515)) {
if (substr($line, 3, 1) == ' ') break;
}
}
// AUTH LOGIN
fputs($socket, "AUTH LOGIN\r\n");
fgets($socket, 515);
fputs($socket, base64_encode($user) . "\r\n");
fgets($socket, 515);
fputs($socket, base64_encode($pass) . "\r\n");
$authResponse = fgets($socket, 515);
if (substr($authResponse, 0, 3) != '235') {
$result['message'] = "Auth failed: $authResponse";
fclose($socket);
return $result;
}
// MAIL FROM
fputs($socket, "MAIL FROM:<$from>\r\n");
fgets($socket, 515);
// RCPT TO
fputs($socket, "RCPT TO:<$to>\r\n");
$rcptResponse = fgets($socket, 515);
if (substr($rcptResponse, 0, 3) != '250') {
$result['message'] = "Recipient rejected: $rcptResponse";
fclose($socket);
return $result;
}
// DATA
fputs($socket, "DATA\r\n");
fgets($socket, 515);
$headers = "From: $fromName <$from>\r\n";
$headers .= "To: $to\r\n";
$headers .= "Subject: $subject\r\n";
$headers .= "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: text/html; charset=UTF-8\r\n";
$headers .= "Date: " . date('r') . "\r\n";
$headers .= "Message-ID: <" . md5(uniqid()) . "@" . parse_url($host, PHP_URL_HOST) . ">\r\n";
fputs($socket, $headers . "\r\n" . $body . "\r\n.\r\n");
$dataResponse = fgets($socket, 515);
fputs($socket, "QUIT\r\n");
fclose($socket);
$result['success'] = substr($dataResponse, 0, 3) == '250';
$result['message'] = $result['success'] ? 'Email sent successfully' : "Send failed: $dataResponse";
$result['latency'] = round((microtime(true) - $start) * 1000);
} catch (Exception $e) {
$result['message'] = 'Error: ' . $e->getMessage();
}
return $result;
}
if (isset($_GET['action'])) {
header('Content-Type: application/json');
$pdo = getDb();
ensureTables($pdo);
switch ($_GET['action']) {
case 'stats':
$servers = $pdo->query("SELECT COUNT(*) FROM admin.smtp_servers")->fetchColumn();
$tests = $pdo->query("SELECT COUNT(*) FROM admin.smtp_tests")->fetchColumn();
$success = $pdo->query("SELECT COUNT(*) FROM admin.smtp_tests WHERE status = 'success'")->fetchColumn();
$failed = $pdo->query("SELECT COUNT(*) FROM admin.smtp_tests WHERE status = 'failed'")->fetchColumn();
echo json_encode(['servers' => (int)$servers, 'tests' => (int)$tests, 'success' => (int)$success, 'failed' => (int)$failed]);
break;
case 'list_servers':
$servers = $pdo->query("SELECT id, name, host, port, username, encryption, from_email, from_name, status, last_test, created_at FROM admin.smtp_servers ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($servers);
break;
case 'add_server':
$stmt = $pdo->prepare("INSERT INTO admin.smtp_servers (name, host, port, username, password, encryption, from_email, from_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
$_POST['name'] ?? '',
$_POST['host'] ?? '',
$_POST['port'] ?? 587,
$_POST['username'] ?? '',
$_POST['password'] ?? '',
$_POST['encryption'] ?? 'tls',
$_POST['from_email'] ?? '',
$_POST['from_name'] ?? ''
]);
echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]);
break;
case 'delete_server':
$pdo->exec("DELETE FROM admin.smtp_servers WHERE id = " . intval($_POST['id'] ?? 0));
echo json_encode(['success' => true]);
break;
case 'test_server':
$serverId = intval($_POST['server_id'] ?? 0);
$toEmail = $_POST['to_email'] ?? '';
$subject = $_POST['subject'] ?? 'SMTP Test from WEVAL SEND';
$body = $_POST['body'] ?? '<h1>Test Email</h1><p>This is a test from WEVAL SEND SMTP Tester.</p><p>Time: ' . date('Y-m-d H:i:s') . '</p>';
$server = $pdo->query("SELECT * FROM admin.smtp_servers WHERE id = $serverId")->fetch(PDO::FETCH_ASSOC);
if (!$server) { echo json_encode(['error' => 'Server not found']); break; }
$result = testSMTP($server['host'], $server['port'], $server['username'], $server['password'], $server['encryption'], $server['from_email'], $server['from_name'], $toEmail, $subject, $body);
$stmt = $pdo->prepare("INSERT INTO admin.smtp_tests (server_id, to_email, subject, status, response, latency_ms) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$serverId, $toEmail, $subject, $result['success'] ? 'success' : 'failed', $result['message'], $result['latency']]);
$pdo->exec("UPDATE admin.smtp_servers SET last_test = NOW() WHERE id = $serverId");
echo json_encode($result);
break;
case 'quick_test':
$result = testSMTP(
$_POST['host'] ?? '',
$_POST['port'] ?? 587,
$_POST['username'] ?? '',
$_POST['password'] ?? '',
$_POST['encryption'] ?? 'tls',
$_POST['from_email'] ?? '',
$_POST['from_name'] ?? 'Test',
$_POST['to_email'] ?? '',
$_POST['subject'] ?? 'Quick SMTP Test',
'<h1>Quick Test</h1><p>Time: ' . date('Y-m-d H:i:s') . '</p>'
);
echo json_encode($result);
break;
case 'test_history':
$serverId = $_GET['server_id'] ?? '';
$where = $serverId ? "WHERE server_id = " . intval($serverId) : "";
$tests = $pdo->query("SELECT t.*, s.name as server_name FROM admin.smtp_tests t LEFT JOIN admin.smtp_servers s ON t.server_id = s.id $where ORDER BY t.created_at DESC LIMIT 100")->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($tests);
break;
default:
echo json_encode(['error' => 'Unknown action']);
}
exit;
}
$pdo = getDb();
ensureTables($pdo);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SMTP Tester - WEVAL SEND</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root{--bg:#0a0a0f;--card:#12121a;--card2:#1a1a25;--border:#2a2a3a;--text:#e4e4e7;--text2:#9ca3af;--primary:#6366f1;--success:#10b981;--warning:#f59e0b;--danger:#ef4444}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;padding:20px}
.container{max-width:1200px;margin:0 auto}
h1{font-size:24px;margin-bottom:24px;display:flex;align-items:center;gap:12px}
h1 i{color:var(--success)}
.stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:24px}
.stat-card{background:var(--card);border-radius:12px;padding:20px;border:1px solid var(--border)}
.stat-value{font-size:28px;font-weight:700}
.stat-label{color:var(--text2);font-size:12px;margin-top:4px}
.tabs{display:flex;gap:10px;margin-bottom:20px}
.tab{padding:10px 20px;background:var(--card);border:1px solid var(--border);border-radius:8px;cursor:pointer;font-size:13px}
.tab.active{background:var(--primary);border-color:var(--primary)}
.panel{background:var(--card);border-radius:12px;border:1px solid var(--border);margin-bottom:20px;display:none}
.panel.active{display:block}
.panel-header{padding:16px 20px;border-bottom:1px solid var(--border)}
.panel-header h2{font-size:15px}
.panel-body{padding:20px}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.form-group{margin-bottom:16px}
.form-group label{display:block;margin-bottom:6px;font-size:12px;color:var(--text2)}
.form-group input,.form-group select,.form-group textarea{width:100%;padding:10px;background:var(--card2);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px}
.btn{padding:10px 20px;border:none;border-radius:8px;cursor:pointer;font-weight:600;font-size:12px;display:inline-flex;align-items:center;gap:8px}
.btn-primary{background:var(--primary);color:white}
.btn-success{background:var(--success);color:white}
.btn-danger{background:var(--danger);color:white}
.btn-sm{padding:6px 12px;font-size:11px}
.btn:hover{filter:brightness(1.1)}
.btn:disabled{opacity:0.5}
.table{width:100%;border-collapse:collapse}
.table th,.table td{padding:10px;text-align:left;border-bottom:1px solid var(--border);font-size:12px}
.table th{background:var(--card2);color:var(--text2)}
.badge{padding:3px 8px;border-radius:12px;font-size:10px;font-weight:600}
.badge-success{background:rgba(16,185,129,0.2);color:var(--success)}
.badge-danger{background:rgba(239,68,68,0.2);color:var(--danger)}
.result-box{background:var(--card2);border-radius:8px;padding:16px;margin-top:16px}
.result-box.success{border-left:4px solid var(--success)}
.result-box.error{border-left:4px solid var(--danger)}
.toast{position:fixed;bottom:20px;right:20px;background:var(--card);border:1px solid var(--border);padding:12px 20px;border-radius:8px;display:none;font-size:13px}
.toast.show{display:flex;align-items:center;gap:10px}
</style>
</head>
<body>
<div class="container">
<h1><i class="fas fa-envelope"></i> SMTP Tester</h1>
<div class="stats-grid">
<div class="stat-card"><div class="stat-value" id="statServers">-</div><div class="stat-label">Servers</div></div>
<div class="stat-card"><div class="stat-value" id="statTests">-</div><div class="stat-label">Total Tests</div></div>
<div class="stat-card"><div class="stat-value" style="color:var(--success)" id="statSuccess">-</div><div class="stat-label">Success</div></div>
<div class="stat-card"><div class="stat-value" style="color:var(--danger)" id="statFailed">-</div><div class="stat-label">Failed</div></div>
</div>
<div class="tabs">
<div class="tab active" onclick="showTab('quick')"><i class="fas fa-bolt"></i> Quick Test</div>
<div class="tab" onclick="showTab('servers')"><i class="fas fa-server"></i> Servers</div>
<div class="tab" onclick="showTab('history')"><i class="fas fa-history"></i> History</div>
</div>
<div class="panel active" id="panel-quick">
<div class="panel-header"><h2>Quick SMTP Test</h2></div>
<div class="panel-body">
<div class="form-row">
<div class="form-group"><label>SMTP Host *</label><input type="text" id="qHost" placeholder="smtp.office365.com"></div>
<div class="form-group"><label>Port *</label><input type="number" id="qPort" value="587"></div>
</div>
<div class="form-row">
<div class="form-group"><label>Username *</label><input type="text" id="qUser" placeholder="user@domain.com"></div>
<div class="form-group"><label>Password *</label><input type="password" id="qPass"></div>
</div>
<div class="form-row">
<div class="form-group"><label>Encryption</label><select id="qEnc"><option value="tls">TLS</option><option value="ssl">SSL</option><option value="none">None</option></select></div>
<div class="form-group"><label>From Email *</label><input type="email" id="qFrom" placeholder="sender@domain.com"></div>
</div>
<div class="form-group"><label>To Email *</label><input type="email" id="qTo" placeholder="test@example.com"></div>
<button class="btn btn-success" onclick="quickTest()" id="qBtn"><i class="fas fa-paper-plane"></i> Send Test</button>
<div id="qResult"></div>
</div>
</div>
<div class="panel" id="panel-servers">
<div class="panel-header"><h2>SMTP Servers</h2></div>
<div class="panel-body">
<div class="form-row">
<div class="form-group"><label>Name</label><input type="text" id="sName" placeholder="Office 365 Main"></div>
<div class="form-group"><label>Host</label><input type="text" id="sHost" placeholder="smtp.office365.com"></div>
</div>
<div class="form-row">
<div class="form-group"><label>Port</label><input type="number" id="sPort" value="587"></div>
<div class="form-group"><label>Encryption</label><select id="sEnc"><option value="tls">TLS</option><option value="ssl">SSL</option></select></div>
</div>
<div class="form-row">
<div class="form-group"><label>Username</label><input type="text" id="sUser"></div>
<div class="form-group"><label>Password</label><input type="password" id="sPass"></div>
</div>
<div class="form-row">
<div class="form-group"><label>From Email</label><input type="email" id="sFromEmail"></div>
<div class="form-group"><label>From Name</label><input type="text" id="sFromName"></div>
</div>
<button class="btn btn-primary" onclick="addServer()"><i class="fas fa-plus"></i> Add Server</button>
<table class="table" style="margin-top:20px"><thead><tr><th>Name</th><th>Host</th><th>Port</th><th>Last Test</th><th>Actions</th></tr></thead><tbody id="serversTable"></tbody></table>
</div>
</div>
<div class="panel" id="panel-history">
<div class="panel-header"><h2>Test History</h2></div>
<div class="panel-body" style="padding:0;overflow-x:auto">
<table class="table"><thead><tr><th>Server</th><th>To</th><th>Subject</th><th>Status</th><th>Latency</th><th>Date</th></tr></thead><tbody id="historyTable"></tbody></table>
</div>
</div>
</div>
<div class="toast" id="toast"><span id="toastText"></span></div>
<script>
function showTab(t){document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));document.querySelectorAll('.tab').forEach(p=>p.classList.remove('active'));document.getElementById('panel-'+t).classList.add('active');document.querySelector('.tab[onclick*="'+t+'"]').classList.add('active');if(t==='servers')loadServers();if(t==='history')loadHistory();}
async function loadStats(){const d=await fetch('?action=stats').then(r=>r.json());document.getElementById('statServers').textContent=d.servers;document.getElementById('statTests').textContent=d.tests;document.getElementById('statSuccess').textContent=d.success;document.getElementById('statFailed').textContent=d.failed;}
async function quickTest(){const btn=document.getElementById('qBtn');btn.disabled=true;btn.innerHTML='<i class="fas fa-spinner fa-spin"></i> Testing...';const fd=new FormData();fd.append('host',document.getElementById('qHost').value);fd.append('port',document.getElementById('qPort').value);fd.append('username',document.getElementById('qUser').value);fd.append('password',document.getElementById('qPass').value);fd.append('encryption',document.getElementById(
cat >> /opt/wevads/public/smtp-tester.php << 'PART4'
<body>
<div class="container">
<h1><i class="fas fa-envelope"></i> SMTP Tester</h1>
<div class="stats-grid">
<div class="stat-card"><div class="stat-value" id="statServers">-</div><div class="stat-label">Servers</div></div>
<div class="stat-card"><div class="stat-value" id="statTests">-</div><div class="stat-label">Total Tests</div></div>
<div class="stat-card"><div class="stat-value" style="color:var(--success)" id="statSuccess">-</div><div class="stat-label">Success</div></div>
<div class="stat-card"><div class="stat-value" style="color:var(--danger)" id="statFailed">-</div><div class="stat-label">Failed</div></div>
</div>
<div class="tabs">
<div class="tab active" onclick="showTab('quick')"><i class="fas fa-bolt"></i> Quick Test</div>
<div class="tab" onclick="showTab('servers')"><i class="fas fa-server"></i> Servers</div>
<div class="tab" onclick="showTab('history')"><i class="fas fa-history"></i> History</div>
</div>
<div class="panel active" id="panel-quick">
<div class="panel-header"><h2>Quick SMTP Test</h2></div>
<div class="panel-body">
<div class="form-row">
<div class="form-group"><label>SMTP Host *</label><input type="text" id="qHost" placeholder="smtp.office365.com"></div>
<div class="form-group"><label>Port *</label><input type="number" id="qPort" value="587"></div>
</div>
<div class="form-row">
<div class="form-group"><label>Username *</label><input type="text" id="qUser" placeholder="user@domain.com"></div>
<div class="form-group"><label>Password *</label><input type="password" id="qPass"></div>
</div>
<div class="form-row">
<div class="form-group"><label>Encryption</label><select id="qEnc"><option value="tls">TLS</option><option value="ssl">SSL</option><option value="none">None</option></select></div>
<div class="form-group"><label>From Email *</label><input type="email" id="qFrom" placeholder="sender@domain.com"></div>
</div>
<div class="form-group"><label>To Email *</label><input type="email" id="qTo" placeholder="test@example.com"></div>
<button class="btn btn-success" onclick="quickTest()" id="qBtn"><i class="fas fa-paper-plane"></i> Send Test</button>
<div id="qResult"></div>
</div>
</div>
<div class="panel" id="panel-servers">
<div class="panel-header"><h2>SMTP Servers</h2></div>
<div class="panel-body">
<div class="form-row">
<div class="form-group"><label>Name</label><input type="text" id="sName" placeholder="Office 365 Main"></div>
<div class="form-group"><label>Host</label><input type="text" id="sHost" placeholder="smtp.office365.com"></div>
</div>
<div class="form-row">
<div class="form-group"><label>Port</label><input type="number" id="sPort" value="587"></div>
<div class="form-group"><label>Encryption</label><select id="sEnc"><option value="tls">TLS</option><option value="ssl">SSL</option></select></div>
</div>
<div class="form-row">
<div class="form-group"><label>Username</label><input type="text" id="sUser"></div>
<div class="form-group"><label>Password</label><input type="password" id="sPass"></div>
</div>
<div class="form-row">
<div class="form-group"><label>From Email</label><input type="email" id="sFromEmail"></div>
<div class="form-group"><label>From Name</label><input type="text" id="sFromName"></div>
</div>
<button class="btn btn-primary" onclick="addServer()"><i class="fas fa-plus"></i> Add Server</button>
<table class="table" style="margin-top:20px"><thead><tr><th>Name</th><th>Host</th><th>Port</th><th>Last Test</th><th>Actions</th></tr></thead><tbody id="serversTable"></tbody></table>
</div>
</div>
<div class="panel" id="panel-history">
<div class="panel-header"><h2>Test History</h2></div>
<div class="panel-body" style="padding:0;overflow-x:auto">
<table class="table"><thead><tr><th>Server</th><th>To</th><th>Subject</th><th>Status</th><th>Latency</th><th>Date</th></tr></thead><tbody id="historyTable"></tbody></table>
</div>
</div>
</div>
<div class="toast" id="toast"><span id="toastText"></span></div>
<script>
function showTab(t){document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));document.querySelectorAll('.tab').forEach(p=>p.classList.remove('active'));document.getElementById('panel-'+t).classList.add('active');document.querySelector('.tab[onclick*="'+t+'"]').classList.add('active');if(t==='servers')loadServers();if(t==='history')loadHistory();}
async function loadStats(){const d=await fetch('?action=stats').then(r=>r.json());document.getElementById('statServers').textContent=d.servers;document.getElementById('statTests').textContent=d.tests;document.getElementById('statSuccess').textContent=d.success;document.getElementById('statFailed').textContent=d.failed;}
async function quickTest(){const btn=document.getElementById('qBtn');btn.disabled=true;btn.innerHTML='<i class="fas fa-spinner fa-spin"></i> Testing...';const fd=new FormData();fd.append('host',document.getElementById('qHost').value);fd.append('port',document.getElementById('qPort').value);fd.append('username',document.getElementById('qUser').value);fd.append('password',document.getElementById('qPass').value);fd.append('encryption',document.getElementById('qEnc').value);fd.append('from_email',document.getElementById('qFrom').value);fd.append('to_email',document.getElementById('qTo').value);const r=await fetch('?action=quick_test',{method:'POST',body:fd}).then(r=>r.json());btn.disabled=false;btn.innerHTML='<i class="fas fa-paper-plane"></i> Send Test';document.getElementById('qResult').innerHTML='<div class="result-box '+(r.success?'success':'error')+'"><strong>'+(r.success?'SUCCESS':'FAILED')+'</strong><br>'+r.message+'<br>Latency: '+r.latency+'ms</div>';loadStats();}
async function loadServers(){const d=await fetch('?action=list_servers').then(r=>r.json());document.getElementById('serversTable').innerHTML=d.map(s=>'<tr><td>'+s.name+'</td><td>'+s.host+'</td><td>'+s.port+'</td><td>'+(s.last_test?new Date(s.last_test).toLocaleString():'-')+'</td><td><button class="btn btn-sm btn-success" onclick="testServer('+s.id+')"><i class="fas fa-play"></i></button> <button class="btn btn-sm btn-danger" onclick="delServer('+s.id+')"><i class="fas fa-trash"></i></button></td></tr>').join('')||'<tr><td colspan="5">No servers</td></tr>';}
async function addServer(){const fd=new FormData();fd.append('name',document.getElementById('sName').value);fd.append('host',document.getElementById('sHost').value);fd.append('port',document.getElementById('sPort').value);fd.append('username',document.getElementById('sUser').value);fd.append('password',document.getElementById('sPass').value);fd.append('encryption',document.getElementById('sEnc').value);fd.append('from_email',document.getElementById('sFromEmail').value);fd.append('from_name',document.getElementById('sFromName').value);await fetch('?action=add_server',{method:'POST',body:fd});toast('Server added');loadServers();loadStats();}
async function delServer(id){if(!confirm('Delete?'))return;await fetch('?action=delete_server',{method:'POST',body:new URLSearchParams({id})});toast('Deleted');loadServers();loadStats();}
async function testServer(id){const to=prompt('Enter test email:');if(!to)return;toast('Testing...');const fd=new FormData();fd.append('server_id',id);fd.append('to_email',to);const r=await fetch('?action=test_server',{method:'POST',body:fd}).then(r=>r.json());toast(r.success?'Success: '+r.latency+'ms':'Failed: '+r.message);loadServers();loadHistory();loadStats();}
async function loadHistory(){const d=await fetch('?action=test_history').then(r=>r.json());document.getElementById('historyTable').innerHTML=d.map(t=>'<tr><td>'+(t.server_name||'Quick')+'</td><td>'+t.to_email+'</td><td>'+t.subject+'</td><td><span class="badge badge-'+(t.status==='success'?'success':'danger')+'">'+t.status+'</span></td><td>'+t.latency_ms+'ms</td><td>'+new Date(t.created_at).toLocaleString()+'</td></tr>').join('')||'<tr><td colspan="6">No tests</td></tr>';}
function toast(msg){const t=document.getElementById('toast');t.className='toast show';document.getElementById('toastText').textContent=msg;setTimeout(()=>t.classList.remove('show'),3000);}
loadStats();
</script>
</body>
</html>