Mit unserem leistungsstarken All-in-One PHP Mailer Script erhältst du ein modernes Kontaktformular, das Sicherheit, Komfort und professionelles Design vereint.
Features im Überblick
- Sichere E-Mail-Übertragung mit PHPMailer und SMTP-Unterstützung
- Google reCAPTCHA Integration (sichtbar und clientseitig eingebunden) gegen Spam und Bots
- Admin-Login mit verschlüsselter Anmeldung für Konfiguration und Einsicht
- IP-Sperre & Missbrauchsschutz – blockiert Spammer automatisch
- Zähler für gesendete Nachrichten direkt unter dem Formular
- AJAX-Integration – versenden ohne Seitenreload, nutzerfreundlich und schnell
- Responsives Premium-Design – optimiert für Desktop, Tablet & Smartphone
- Einfach anpassbar – Texte, Farben und Layout direkt in einer Datei editierbar
Sicherheit inklusive
Dank reCAPTCHA, IP-Blockade und verschlüsseltem Login ist dein Formular bestens gegen Spam und Missbrauch geschützt.
Installation
- Datei hochladen, SMTP-Daten eintragen, reCAPTCHA-Keys einfügen – fertig!
- Keine zusätzlichen Abhängigkeiten notwendig – alles in einer Datei enthalten.
Ideal für Webseiten, Agenturen, Startups und alle, die ein sicheres und modernes Kontaktformular einsetzen möchten.
<?php
// mailer.php
// Ein Datei Email Mailer, AJAX, Captcha, Zähler, IP Sperre, Admin Login, PHPMailer optional
session_start();
date_default_timezone_set('Europe/Berlin');
header('X-Content-Type-Options: nosniff');
// ========== Konfiguration ==========
const USE_PHPMailer = true; // true wenn du composer und phpmailer installiert hast
const SMTP_HOST = 'smtp.example.com';
const SMTP_PORT = 587;
const SMTP_USER = 'user@example.com';
const SMTP_PASS = 'password';
const SMTP_FROM = 'no-reply@example.com';
const SMTP_NAME = 'Mailer';
const ADMIN_USER = 'admin'; // Admin Benutzername
// Falls du bereits ein Passwort-Hash hast, trage ihn hier ein. Ansonsten wird beim ersten Aufruf eine Setupseite angeboten
const ADMIN_PW_HASH = ''; // z. B. password_hash('DeinSicheresPasswort', PASSWORD_DEFAULT);
// reCAPTCHA Konfiguration trage deine keys ein
const RECAPTCHA_SITE_KEY = ''; // z. B. '6Lc...'
const RECAPTCHA_SECRET = ''; // z. B. '6Lc...'
// Limits
const MAX_SENDS_PER_IP_PER_HOUR = 20;
const BLOCK_AFTER_FAILED_CAPTCHA = 5;
const BLOCK_DURATION_SECONDS = 3600 * 6; // 6 Stunden
// Dateien
const DATA_DIR = __DIR__ . '/data_mailer';
const LOG_FILE = DATA_DIR . '/mail_log.json';
const IP_FILE = DATA_DIR . '/ip_block.json';
const COUNTER_FILE = DATA_DIR . '/counter.json';
const ADMIN_FILE = DATA_DIR . '/admin.json';
if(!is_dir(DATA_DIR)) {
mkdir(DATA_DIR, 0755, true);
file_put_contents(LOG_FILE, json_encode([]));
file_put_contents(IP_FILE, json_encode([]));
file_put_contents(COUNTER_FILE, json_encode(['total'=>0]));
file_put_contents(ADMIN_FILE, json_encode(['user'=>ADMIN_USER,'hash'=>ADMIN_PW_HASH]));
}
// PHPMailer Autoload falls verwendet
if(USE_PHPMailer && file_exists(__DIR__ . '/vendor/autoload.php')){
require_once __DIR__ . '/vendor/autoload.php';
}
// Hilfsfunktionen
function ipAddress(){
if(!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) return $_SERVER['HTTP_CF_CONNECTING_IP'];
if(!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) return explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
function readJsonFile($f){
if(!file_exists($f)) return null;
$c = file_get_contents($f);
return $c ? json_decode($c, true) : null;
}
function writeJsonFile($f, $data){
file_put_contents($f, json_encode($data, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT));
}
function incrementCounter(){
$c = readJsonFile(COUNTER_FILE) ?: ['total'=>0];
$c['total'] = ($c['total'] ?? 0) + 1;
writeJsonFile(COUNTER_FILE, $c);
return $c['total'];
}
function logMailEvent($entry){
$log = readJsonFile(LOG_FILE) ?: [];
$log[] = $entry;
if(count($log) > 4000) $log = array_slice($log, -4000);
writeJsonFile(LOG_FILE, $log);
}
function isBlocked($ip){
$blocks = readJsonFile(IP_FILE) ?: [];
if(isset($blocks[$ip])){
$b = $blocks[$ip];
if($b['blocked_until'] > time()) return $b;
unset($blocks[$ip]);
writeJsonFile(IP_FILE, $blocks);
return false;
}
return false;
}
function recordFailedCaptcha($ip){
$blocks = readJsonFile(IP_FILE) ?: [];
if(!isset($blocks[$ip])) $blocks[$ip] = ['fails'=>0, 'blocked_until'=>0, 'sends'=>[]];
$blocks[$ip]['fails'] = ($blocks[$ip]['fails'] ?? 0) + 1;
if($blocks[$ip]['fails'] >= BLOCK_AFTER_FAILED_CAPTCHA){
$blocks[$ip]['blocked_until'] = time() + BLOCK_DURATION_SECONDS;
}
writeJsonFile(IP_FILE, $blocks);
}
function recordSend($ip){
$blocks = readJsonFile(IP_FILE) ?: [];
if(!isset($blocks[$ip])) $blocks[$ip] = ['fails'=>0, 'blocked_until'=>0, 'sends'=>[]];
$blocks[$ip]['sends'][] = time();
$blocks[$ip]['sends'] = array_slice($blocks[$ip]['sends'], -500);
writeJsonFile(IP_FILE, $blocks);
}
function countSendsLastHour($ip){
$blocks = readJsonFile(IP_FILE) ?: [];
if(!isset($blocks[$ip]['sends'])) return 0;
$cut = time() - 3600;
$arr = array_filter($blocks[$ip]['sends'], function($t) use($cut){ return $t >= $cut; });
return count($arr);
}
function verifyRecaptcha($token){
if(!RECAPTCHA_SECRET) return true; // kein key gesetzt, akzeptieren
$url = 'https://www.google.com/recaptcha/api/siteverify';
$data = http_build_query(['secret'=>RECAPTCHA_SECRET,'response'=>$token,'remoteip'=>ipAddress()]);
$opts = ['http'=>['method'=>'POST','header'=>"Content-type: application/x-www-form-urlencoded\r\n",'content'=>$data,'timeout'=>10]];
$context = stream_context_create($opts);
$resp = @file_get_contents($url, false, $context);
if(!$resp) return false;
$j = json_decode($resp, true);
return !empty($j['success']);
}
// Einfacher text filter
function sanitize($s){
return trim(htmlspecialchars((string)$s, ENT_QUOTES));
}
// Send mail helper
function sendMail($to, $subject, $html, $plain=''){
if(USE_PHPMailer && class_exists('PHPMailer\PHPMailer\PHPMailer')){
try{
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
$mail->isSMTP();
$mail->Host = SMTP_HOST;
$mail->SMTPAuth = true;
$mail->Username = SMTP_USER;
$mail->Password = SMTP_PASS;
$mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = SMTP_PORT;
$mail->setFrom(SMTP_FROM, SMTP_NAME);
$mail->addAddress($to);
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $html;
if($plain) $mail->AltBody = $plain;
$mail->send();
return true;
}catch(Exception $e){
error_log('PHPMailer error ' . $e->getMessage());
}
}
$headers = "From: " . SMTP_NAME . " <" . SMTP_FROM . ">\r\n";
$headers .= "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: text/html; charset=UTF-8\r\n";
return @mail($to, $subject, $html, $headers);
}
// ====== Admin Passwort Management ======
function getAdminData(){
$a = readJsonFile(ADMIN_FILE) ?: ['user'=>ADMIN_USER,'hash'=>ADMIN_PW_HASH];
if(empty($a['user'])) $a['user'] = ADMIN_USER;
return $a;
}
function setAdminHash($hash){
$a = getAdminData();
$a['hash'] = $hash;
writeJsonFile(ADMIN_FILE, $a);
}
function adminVerify($user, $pw){
$a = getAdminData();
if($user !== $a['user']) return false;
if(empty($a['hash'])) return false;
return password_verify($pw, $a['hash']);
}
function adminIsLogged(){
return !empty($_SESSION['admin_logged']) && $_SESSION['admin_logged'] === true;
}
// CSRF token
if(empty($_SESSION['csrf_token'])) $_SESSION['csrf_token'] = bin2hex(random_bytes(16));
function checkCsrf($token){
return !empty($token) && hash_equals($_SESSION['csrf_token'], $token);
}
// ========== AJAX Endpoints & Admin Actions ==========
if(isset($_GET['action']) && $_GET['action'] === 'send' && $_SERVER['REQUEST_METHOD'] === 'POST'){
header('Content-Type: application/json; charset=utf-8');
$ip = ipAddress();
if($b = isBlocked($ip)){
echo json_encode(['ok'=>false, 'error'=>'ip_blocked', 'blocked_until'=>$b['blocked_until']]);
exit;
}
$data = json_decode(file_get_contents('php://input'), true);
$to = filter_var($data['to'] ?? '', FILTER_VALIDATE_EMAIL) ? $data['to'] : '';
$subject = sanitize($data['subject'] ?? '');
$message = sanitize($data['message'] ?? '');
$name = sanitize($data['name'] ?? 'Anonym');
$recaptcha = $data['recaptcha'] ?? '';
if(!$to || !$subject || !$message){
echo json_encode(['ok'=>false,'error'=>'missing']);
exit;
}
$recOk = false;
if(RECAPTCHA_SECRET){
$recOk = verifyRecaptcha($recaptcha);
} else {
if(!empty($_SESSION['math_captcha_answer']) && isset($data['math_answer']) && intval($data['math_answer']) === intval($_SESSION['math_captcha_answer'])){
$recOk = true;
} else {
$recOk = false;
}
}
if(!$recOk){
recordFailedCaptcha($ip);
echo json_encode(['ok'=>false,'error'=>'captcha_failed']);
exit;
}
if(countSendsLastHour($ip) >= MAX_SENDS_PER_IP_PER_HOUR){
$blocks = readJsonFile(IP_FILE) ?: [];
$blocks[$ip] = $blocks[$ip] ?? ['fails'=>0,'blocked_until'=>0,'sends'=>[]];
$blocks[$ip]['blocked_until'] = time() + BLOCK_DURATION_SECONDS;
writeJsonFile(IP_FILE, $blocks);
echo json_encode(['ok'=>false,'error'=>'rate_limit_exceeded']);
exit;
}
$html = "<html><body>";
$html .= "<h2>" . htmlspecialchars($subject) . "</h2>";
$html .= "<p><b>Von</b>: " . htmlspecialchars($name) . " (" . htmlspecialchars($ip) . ")</p>";
$html .= "<hr>";
$html .= "<div>" . nl2br(htmlspecialchars($message)) . "</div>";
$html .= "<hr>";
$html .= "<p>Gesendet über Mailer</p>";
$html .= "</body></html>";
$plain = strip_tags(str_replace(['<br>','<br/>','<br />'], "\n", $html));
$mailOk = sendMail($to, $subject, $html, $plain);
$entry = [
'time' => time(),
'ip' => $ip,
'to' => $to,
'subject' => $subject,
'name' => $name,
'ok' => $mailOk,
];
logMailEvent($entry);
if($mailOk){
recordSend($ip);
$total = incrementCounter();
echo json_encode(['ok'=>true,'total'=>$total]);
} else {
echo json_encode(['ok'=>false,'error'=>'send_failed']);
}
exit;
}
// Status endpoint
if(isset($_GET['action']) && $_GET['action'] === 'status'){
header('Content-Type: application/json; charset=utf-8');
$c = readJsonFile(COUNTER_FILE) ?: ['total'=>0];
$ip = ipAddress();
$blocked = isBlocked($ip);
echo json_encode(['ok'=>true, 'total'=>$c['total'], 'blocked' => $blocked ? true : false, 'blocked_until' => $blocked ? $blocked['blocked_until'] : null]);
exit;
}
// Admin AJAX
if(isset($_GET['action']) && in_array($_GET['action'], ['admin_login','admin_logout','admin_logs','admin_clear_logs','admin_export_csv','admin_block_ip','admin_unblock_ip','admin_set_password','admin_stats'])){
header('Content-Type: application/json; charset=utf-8');
$act = $_GET['action'];
// read body for post
$data = $_SERVER['REQUEST_METHOD']==='POST' ? json_decode(file_get_contents('php://input'), true) : $_GET;
if($act === 'admin_login'){
$user = $data['user'] ?? '';
$pw = $data['pw'] ?? '';
if(adminVerify($user,$pw)){
$_SESSION['admin_logged'] = true;
echo json_encode(['ok'=>true]);
} else {
echo json_encode(['ok'=>false,'error'=>'invalid']);
}
exit;
}
if($act === 'admin_logout'){
session_destroy();
echo json_encode(['ok'=>true]);
exit;
}
// require login for following
if(!adminIsLogged()){
echo json_encode(['ok'=>false,'error'=>'auth_required']);
exit;
}
if($act === 'admin_logs'){
$log = readJsonFile(LOG_FILE) ?: [];
echo json_encode(['ok'=>true,'log'=>array_reverse(array_slice($log, -100))]);
exit;
}
if($act === 'admin_clear_logs'){
writeJsonFile(LOG_FILE, []);
echo json_encode(['ok'=>true]);
exit;
}
if($act === 'admin_export_csv'){
$log = readJsonFile(LOG_FILE) ?: [];
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="mailer_logs_' . date('Ymd_His') . '.csv"');
$out = fopen('php://output','w');
fputcsv($out, ['time','ip','to','subject','name','ok']);
foreach($log as $row){
fputcsv($out, [date('c',$row['time']), $row['ip'], $row['to'], $row['subject'], $row['name'], $row['ok'] ? '1' : '0']);
}
fclose($out);
exit;
}
if($act === 'admin_block_ip'){
$ip = $data['ip'] ?? '';
if($ip){
$blocks = readJsonFile(IP_FILE) ?: [];
$blocks[$ip] = $blocks[$ip] ?? ['fails'=>0,'blocked_until'=>0,'sends'=>[]];
$blocks[$ip]['blocked_until'] = time() + BLOCK_DURATION_SECONDS;
writeJsonFile(IP_FILE, $blocks);
echo json_encode(['ok'=>true]);
} else echo json_encode(['ok'=>false,'error'=>'missing']);
exit;
}
if($act === 'admin_unblock_ip'){
$ip = $data['ip'] ?? '';
if($ip){
$blocks = readJsonFile(IP_FILE) ?: [];
unset($blocks[$ip]);
writeJsonFile(IP_FILE, $blocks);
echo json_encode(['ok'=>true]);
} else echo json_encode(['ok'=>false,'error'=>'missing']);
exit;
}
if($act === 'admin_set_password'){
$newpw = $data['newpw'] ?? '';
if(strlen($newpw) < 6){ echo json_encode(['ok'=>false,'error'=>'short']); exit; }
$hash = password_hash($newpw, PASSWORD_DEFAULT);
setAdminHash($hash);
echo json_encode(['ok'=>true]);
exit;
}
if($act === 'admin_stats'){
$c = readJsonFile(COUNTER_FILE) ?: ['total'=>0];
$blocks = readJsonFile(IP_FILE) ?: [];
$log = readJsonFile(LOG_FILE) ?: [];
echo json_encode(['ok'=>true,'total'=>$c['total'],'blocked_count'=>count(array_filter($blocks,function($b){return $b['blocked_until']>time();})),'log_recent'=>array_slice($log,-20)]);
exit;
}
}
// Admin setup page create initial password if none set
if(isset($_GET['setup_admin']) && $_SERVER['REQUEST_METHOD']==='POST'){
$pw = $_POST['newpw'] ?? '';
if(strlen($pw) >= 6){
setAdminHash(password_hash($pw, PASSWORD_DEFAULT));
header('Location: ?admin=1');
exit;
} else {
$err = 'Passwort zu kurz';
}
}
// small endpoint to set math captcha answer in session from client
if(isset($_GET['set_math'])){
$_SESSION['math_captcha_answer'] = intval($_GET['set_math']);
echo 'ok';
exit;
}
// ========== Frontend HTML, CSS, JS ==========
?><!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Professioneller E Mail Mailer</title>
<style>
:root{
--bg:#071026;
--card:#0b1a2b;
--muted:#9fb4d6;
--accent:#1fb6ff;
--success:#34d399;
--danger:#ff7b7b;
--glass: rgba(255,255,255,0.03);
}
html,body{height:100%;margin:0;font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial;color:#e6f0fb;background:linear-gradient(180deg,var(--bg),#021226)}
.wrap{max-width:980px;margin:28px auto;padding:20px}
.card{background:linear-gradient(180deg,var(--card),#071428);border-radius:12px;padding:18px;box-shadow:0 8px 30px rgba(2,6,23,0.7);border:1px solid rgba(255,255,255,0.04)}
h1{margin:0 0 8px;font-weight:700}
label{display:block;margin-top:10px;color:var(--muted);font-size:13px}
input[type=text], input[type=email], textarea, select{width:100%;padding:10px;border-radius:8px;border:1px solid rgba(255,255,255,0.06);background:rgba(255,255,255,0.02);color:inherit;box-sizing:border-box}
textarea{min-height:120px;resize:vertical}
.row{display:flex;gap:12px;flex-wrap:wrap}
.col{flex:1;min-width:120px}
.btn{background:var(--accent);color:#042431;padding:10px 14px;border-radius:10px;border:none;font-weight:700;cursor:pointer}
.btn.alt{background:transparent;border:1px solid rgba(255,255,255,0.06);color:var(--muted)}
.btn:disabled{opacity:0.6;cursor:default}
.info{margin-top:10px;color:var(--muted);font-size:13px}
.meta{display:flex;justify-content:space-between;align-items:center;margin-top:12px;font-size:13px;color:var(--muted)}
.muted{color:var(--muted)}
.small{font-size:12px;color:var(--muted)}
.status{padding:8px;border-radius:8px;margin-top:12px}
.status.ok{background:rgba(52,211,153,0.06);border:1px solid rgba(52,211,153,0.12);color:var(--success)}
.status.err{background:rgba(255,123,123,0.06);border:1px solid rgba(255,123,123,0.12);color:var(--danger)}
.flex{display:flex;gap:10px;align-items:center}
.captcha-box{display:flex;gap:10px;align-items:center;margin-top:10px;flex-wrap:wrap}
.counter{font-weight:700;color:var(--accent)}
footer{margin-top:18px;text-align:center;color:var(--muted);font-size:12px}
.kbd{padding:6px 8px;background:rgba(255,255,255,0.02);border-radius:8px;border:1px solid rgba(255,255,255,0.03);font-size:12px}
@media(max-width:720px){ .row{flex-direction:column} .meta{flex-direction:column;align-items:flex-start} }
</style>
<?php if(RECAPTCHA_SITE_KEY): ?>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<?php endif; ?>
</head>
<body>
<div class="wrap">
<div class="card">
<h1>Professioneller E Mail Mailer</h1>
<p class="small muted">Sicher, mit Captcha, Limitierung und IP Sperre</p>
<form id="mailerForm" onsubmit="return false" autocomplete="off">
<input type="hidden" id="csrfToken" value="<?=htmlspecialchars($_SESSION['csrf_token'])?>" />
<label for="to">Empfänger Email</label>
<input id="to" type="email" placeholder="empfaenger@domain.tld" required />
<div class="row">
<div class="col">
<label for="name">Dein Name</label>
<input id="name" type="text" placeholder="Name" />
</div>
<div style="width:180px">
<label for="subject">Betreff</label>
<input id="subject" type="text" placeholder="Betreff" />
</div>
</div>
<label for="message">Nachricht</label>
<textarea id="message" placeholder="Deine Nachricht"></textarea>
<div id="captchaArea" class="captcha-box">
<?php if(RECAPTCHA_SITE_KEY): ?>
<div class="g-recaptcha" data-sitekey="<?=htmlspecialchars(RECAPTCHA_SITE_KEY)?>"></div>
<?php else: ?>
<div id="mathCaptcha" class="small muted">Berechne</div>
<?php endif; ?>
</div>
<div style="display:flex;gap:10px;margin-top:14px;align-items:center;flex-wrap:wrap">
<button id="sendBtn" class="btn">Senden</button>
<button id="resetBtn" class="btn alt" type="button">Zurücksetzen</button>
<div style="margin-left:auto" class="meta">
<div>Gesendet insgesamt <span id="totalCounter" class="counter">0</span></div>
</div>
</div>
<div id="statusBox" class="status" style="display:none"></div>
<div class="info">
<p class="small muted">Hinweis: Bei wiederholtem fehlschlagen des Captcha oder zu vielen Versendungen wird die IP temporär gesperrt</p>
</div>
</form>
</div>
<div style="height:14px"></div>
<div class="card" id="adminCard">
<div style="display:flex;align-items:center;gap:12px">
<h2 style="margin:0">Admin Bereich</h2>
<div style="margin-left:auto" id="adminControls"></div>
</div>
<p class="small muted">Logs, Export und IP Verwaltung</p>
<div id="adminArea" style="margin-top:12px;display:none">
<div style="display:flex;gap:10px;flex-wrap:wrap">
<button id="btnFetchLogs" class="btn alt">Logs aktualisieren</button>
<button id="btnClearLogs" class="btn alt">Logs löschen</button>
<button id="btnExportCSV" class="btn">CSV export</button>
<button id="btnStats" class="btn alt">Statistik</button>
</div>
<div style="margin-top:12px">
<h3 style="margin:8px 0">Letzte Einträge</h3>
<pre id="logPreview" style="background:rgba(255,255,255,0.02);padding:12px;border-radius:8px;max-height:260px;overflow:auto"></pre>
</div>
<div style="margin-top:12px">
<h3 style="margin:8px 0">IP Verwaltung</h3>
<div style="display:flex;gap:8px">
<input id="ipInput" placeholder="IP Adresse" style="padding:8px;border-radius:8px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.04)"></input>
<button id="btnBlockIP" class="btn alt">Sperren</button>
<button id="btnUnblockIP" class="btn alt">Entsperren</button>
</div>
<div id="blockedList" style="margin-top:8px"></div>
</div>
<div style="margin-top:12px">
<h3 style="margin:8px 0">Admin Passwort ändern</h3>
<div style="display:flex;gap:8px">
<input id="newPw" type="password" placeholder="neues Passwort" style="padding:8px;border-radius:8px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.04)">
<button id="btnSetPw" class="btn">Setzen</button>
</div>
<div id="pwMsg" class="small muted" style="margin-top:6px"></div>
</div>
</div>
<div id="loginArea" style="margin-top:12px">
<h3 style="margin:8px 0">Admin Login</h3>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<input id="adminUser" placeholder="Benutzer" value="<?=htmlspecialchars(getAdminData()['user'] ?? ADMIN_USER)?>" style="padding:8px;border-radius:8px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.04)">
<input id="adminPw" type="password" placeholder="Passwort" style="padding:8px;border-radius:8px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.04)">
<button id="btnLogin" class="btn">Anmelden</button>
</div>
<div id="loginMsg" class="small muted" style="margin-top:8px"></div>
</div>
</div>
<footer>© <?=date('Y')?> <a href="http://www.dreamcodes.net" target="_blank" rel="noopener">Dreamcodes</a> — Mailer</footer>
</div>
<script>
(async function(){
const recaptchaKey = "<?=htmlspecialchars(RECAPTCHA_SITE_KEY)?>";
const csrfToken = document.getElementById('csrfToken').value;
const SEND_URL = '?action=send';
const STATUS_URL = '?action=status';
const form = document.getElementById('mailerForm');
const toEl = document.getElementById('to');
const nameEl = document.getElementById('name');
const subjectEl = document.getElementById('subject');
const messageEl = document.getElementById('message');
const sendBtn = document.getElementById('sendBtn');
const resetBtn = document.getElementById('resetBtn');
const statusBox = document.getElementById('statusBox');
const totalCounterEl = document.getElementById('totalCounter');
const captchaArea = document.getElementById('captchaArea');
const adminControls = document.getElementById('adminControls');
const adminArea = document.getElementById('adminArea');
const loginArea = document.getElementById('loginArea');
const btnLogin = document.getElementById('btnLogin');
const btnFetchLogs = document.getElementById('btnFetchLogs');
const btnClearLogs = document.getElementById('btnClearLogs');
const btnExportCSV = document.getElementById('btnExportCSV');
const btnStats = document.getElementById('btnStats');
const logPreview = document.getElementById('logPreview');
const btnBlockIP = document.getElementById('btnBlockIP');
const btnUnblockIP = document.getElementById('btnUnblockIP');
const ipInput = document.getElementById('ipInput');
const blockedList = document.getElementById('blockedList');
const newPw = document.getElementById('newPw');
const btnSetPw = document.getElementById('btnSetPw');
const pwMsg = document.getElementById('pwMsg');
const loginMsg = document.getElementById('loginMsg');
let mathAnswer = null;
function showStatus(ok, txt){
statusBox.style.display = 'block';
statusBox.className = 'status ' + (ok ? 'ok' : 'err');
statusBox.textContent = txt;
}
function hideStatus(){
statusBox.style.display = 'none';
}
function setLoading(v){
sendBtn.disabled = v; resetBtn.disabled = v;
}
async function fetchStatus(){
try{
const r = await fetch(STATUS_URL);
const j = await r.json();
totalCounterEl.textContent = j.total || 0;
if(j.blocked) showStatus(false, 'Deine IP ist temporär gesperrt');
else hideStatus();
}catch(e){ console.error(e); }
}
async function loadLogs(){
// fetch admin logs via admin ajax
const res = await fetch('?action=admin_logs');
if(res.status === 200){
const j = await res.json();
if(j.ok){
const recent = j.log || [];
logPreview.textContent = recent.map(x => {
const t = new Date(x.time*1000).toLocaleString();
return `[${t}] ${x.ip} → ${x.to} ${x.ok ? 'OK' : 'FEHLER'} ${x.subject}`;
}).join("\n");
} else {
if(j.error === 'auth_required'){ adminArea.style.display='none'; loginArea.style.display='block'; }
}
} else console.error('logs fetch status', res.status);
}
async function loadBlocked(){
const res = await fetch('?action=admin_stats');
if(res.status === 200){
const j = await res.json();
if(j.ok){
const blocks = j.log_recent || [];
// show blocked count
blockedList.innerHTML = 'Geblockte IPs ' + (j.blocked_count || 0);
}
}
}
function generateMathCaptcha(){
const a = Math.floor(Math.random()*9)+1;
const b = Math.floor(Math.random()*9)+1;
mathAnswer = a + b;
fetch('?set_math=' + mathAnswer).catch(()=>{});
const el = document.getElementById('mathCaptcha');
if(el) el.textContent = 'Bitte lösen: ' + a + ' + ' + b + ' =';
// add small input
if(!document.getElementById('mathAnswer')){
const inp = document.createElement('input'); inp.id='mathAnswer'; inp.style.width='80px';
captchaArea.appendChild(inp);
}
}
if(!recaptchaKey) generateMathCaptcha();
sendBtn.addEventListener('click', async function(){
setLoading(true);
hideStatus();
const to = toEl.value.trim();
const subject = subjectEl.value.trim();
const message = messageEl.value.trim();
const name = nameEl.value.trim() || 'Anonym';
if(!to || !subject || !message){ showStatus(false,'Bitte fülle alle Felder aus'); setLoading(false); return; }
let recaptchaToken = '';
let mathVal = '';
if(recaptchaKey){
// try to get token if grecaptcha available
if(window.grecaptcha){
try{
recaptchaToken = await grecaptcha.execute(recaptchaKey, {action: 'submit'});
}catch(e){ console.warn('grecaptcha execute failed', e); }
}
} else {
const ma = document.getElementById('mathAnswer'); mathVal = ma ? ma.value.trim() : '';
}
const payload = { to, subject, message, name, recaptcha: recaptchaToken, math_answer: mathVal, csrf: csrfToken };
try{
const res = await fetch('?action=send', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(payload)
});
const j = await res.json();
if(j.ok){
showStatus(true, 'Mail erfolgreich gesendet. Insgesamt gesendet: ' + (j.total || '?'));
await fetchStatus();
if(!recaptchaKey) generateMathCaptcha();
} else {
let msg = 'Fehler beim Senden';
if(j.error === 'captcha_failed') msg = 'Captcha ungültig';
if(j.error === 'missing') msg = 'Eingabefelder fehlen';
if(j.error === 'rate_limit_exceeded') msg = 'Zu viele Versuche, IP temporär gesperrt';
if(j.error === 'ip_blocked') msg = 'Deine IP ist gesperrt';
if(j.error === 'send_failed') msg = 'Versand fehlgeschlagen';
showStatus(false, msg);
}
}catch(e){
console.error(e);
showStatus(false, 'Netzwerkfehler');
}
setLoading(false);
});
resetBtn.addEventListener('click', ()=>{ toEl.value=''; nameEl.value=''; subjectEl.value=''; messageEl.value=''; if(!recaptchaKey){ generateMathCaptcha(); document.getElementById('mathAnswer').value=''; } });
// Admin login
btnLogin.addEventListener('click', async ()=>{
const user = document.getElementById('adminUser').value.trim();
const pw = document.getElementById('adminPw').value;
const res = await fetch('?action=admin_login', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({user,pw})});
const j = await res.json();
if(j.ok){ loginArea.style.display='none'; adminArea.style.display='block'; adminControls.innerHTML = '<button id=\"logoutBtn\" class=\"btn alt\">Abmelden</button>'; document.getElementById('logoutBtn').addEventListener('click', adminLogout); loadLogs(); loadBlocked(); }
else loginMsg.textContent = 'Login fehlgeschlagen';
});
async function adminLogout(){
const res = await fetch('?action=admin_logout');
const j = await res.json();
if(j.ok){ adminArea.style.display='none'; loginArea.style.display='block'; adminControls.innerHTML=''; }
}
btnFetchLogs.addEventListener('click', ()=>loadLogs());
btnClearLogs.addEventListener('click', async ()=>{
if(!confirm('Logs löschen?')) return;
const res = await fetch('?action=admin_clear_logs');
const j = await res.json();
if(j.ok) { logPreview.textContent=''; alert('Logs gelöscht'); }
});
btnExportCSV.addEventListener('click', ()=>{ window.location.href='?action=admin_export_csv'; });
btnBlockIP.addEventListener('click', async ()=>{
const ip = ipInput.value.trim(); if(!ip) return alert('IP eingeben');
const res = await fetch('?action=admin_block_ip', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ip})});
const j = await res.json(); if(j.ok) { alert('IP gesperrt'); loadBlocked(); }
});
btnUnblockIP.addEventListener('click', async ()=>{
const ip = ipInput.value.trim(); if(!ip) return alert('IP eingeben');
const res = await fetch('?action=admin_unblock_ip', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ip})});
const j = await res.json(); if(j.ok) { alert('IP entsperrt'); loadBlocked(); }
});
btnSetPw.addEventListener('click', async ()=>{
const v = newPw.value.trim();
if(v.length < 6){ pwMsg.textContent='Passwort zu kurz minimal 6 Zeichen'; return; }
const res = await fetch('?action=admin_set_password', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({newpw:v})});
const j = await res.json();
if(j.ok){ pwMsg.textContent='Passwort gesetzt'; newPw.value=''; } else pwMsg.textContent='Fehler';
});
btnStats.addEventListener('click', async ()=>{
const res = await fetch('?action=admin_stats');
const j = await res.json();
if(j.ok){ alert('Total gesendet: ' + j.total + '\nGeblockte IPs: ' + j.blocked_count); }
});
// Initial
await fetchStatus();
// try to fetch logs only if admin already logged in server side
await loadLogs();
// If no admin password set then show setup notice
const adminDataRes = await fetch('?action=admin_stats');
if(adminDataRes.status === 200){
const jd = await adminDataRes.json();
if(jd && jd.ok) { /* ok */ }
} else {
// ignore
}
})();
</script>
</body>
</html>