getenv('MAIL_RECIPIENT') ?: '', // 'smtp_host' => getenv('MAIL_SMTP_HOST') ?: '', // 'smtp_port' => (int)(getenv('MAIL_SMTP_PORT') ?: 587), // 'smtp_secure' => getenv('MAIL_SMTP_SECURE') ?: 'tls', // 'smtp_user' => getenv('MAIL_SMTP_USER') ?: '', // 'smtp_pass' => getenv('MAIL_SMTP_PASS') ?: '', // ]; $cfg = require dirname(__DIR__, 2) . '/mail-config.php'; if ($cfg['recipient'] === '' || $cfg['smtp_host'] === '' || $cfg['smtp_user'] === '') { out(500, 'Mail not configured'); } // ────────────────────────────────────────────────────────────────────────────── // Only accept POST if ($_SERVER['REQUEST_METHOD'] !== 'POST') { out(405, 'Method not allowed'); } // ── CSRF: verify request originates from our own domain ─────────────────────── $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; $referer = $_SERVER['HTTP_REFERER'] ?? ''; if ($origin !== '') { if ($origin !== ALLOWED_ORIGIN) { out(403, 'Forbidden'); } } elseif (strpos($referer, ALLOWED_ORIGIN . '/') !== 0) { out(403, 'Forbidden'); } // ───────────────────────────────────────────────────────────────────────────── // ── Honeypot ────────────────────────────────────────────────────────────────── // If the hidden "website" field is filled, it's a bot — silently pretend success. if (!empty($_POST['website'])) { out(200, null, true); } // ── Timing check ────────────────────────────────────────────────────────────── $loadedAt = (int) filter_input(INPUT_POST, '_t', FILTER_SANITIZE_NUMBER_INT); $elapsed = time() - $loadedAt; if ($loadedAt === 0 || $elapsed < MIN_ELAPSED || $elapsed > 7200) { out(400, 'Invalid submission'); } // ── Rate limiting ───────────────────────────────────────────────────────────── $ip = preg_replace('/[^a-f0-9:.]/', '', $_SERVER['REMOTE_ADDR'] ?? ''); $lockDir = rtrim(RATE_DIR, '/'); if (!is_dir($lockDir)) { @mkdir($lockDir, 0700, true); } $lockFile = $lockDir . '/' . md5($ip) . '.lock'; // Atomic check+write using exclusive lock — prevents TOCTOU race condition $fp = fopen($lockFile, 'c+'); if ($fp === false || !flock($fp, LOCK_EX)) { out(500, 'Server error'); } $lastTime = (int) fread($fp, 20); if ($lastTime > 0 && (time() - $lastTime) < RATE_WINDOW) { flock($fp, LOCK_UN); fclose($fp); out(429, 'Too many requests'); } // ── Sanitize & validate ─────────────────────────────────────────────────────── $name = clean(INPUT_POST, 'name', 100); $email = trim(filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL) ?? ''); $subject = clean(INPUT_POST, 'subject', 200); $message = clean(INPUT_POST, 'message', 5000); if ($name === '' || $email === '' || $message === '') { flock($fp, LOCK_UN); fclose($fp); out(400, 'Missing required fields'); } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { flock($fp, LOCK_UN); fclose($fp); out(400, 'Invalid email address'); } // ── Build and send the email via PHPMailer ──────────────────────────────────── require __DIR__ . '/vendor/autoload.php'; use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\SMTP; use PHPMailer\PHPMailer\Exception as MailerException; $subjectLine = '[contact from danix.xyz] - ' . ($subject !== '' ? $subject : 'New message from ' . $name); $body = implode("\n", [ 'Name: ' . $name, 'Email: ' . $email, 'Subject: ' . ($subject ?: '—'), '', str_repeat('─', 48), '', $message, ]); $mail = new PHPMailer(true); try { $mail->isSMTP(); $mail->Host = $cfg['smtp_host']; $mail->SMTPAuth = true; $mail->Username = $cfg['smtp_user']; $mail->Password = $cfg['smtp_pass']; $mail->SMTPSecure = $cfg['smtp_secure'] === 'ssl' ? PHPMailer::ENCRYPTION_SMTPS : PHPMailer::ENCRYPTION_STARTTLS; $mail->Port = $cfg['smtp_port']; $mail->CharSet = 'UTF-8'; $mail->setFrom('danix@' . SENDER_DOMAIN, SENDER_DOMAIN); $mail->addReplyTo($email, $name); $mail->addAddress($cfg['recipient']); $mail->Subject = $subjectLine; $mail->Body = $body; $mail->send(); rewind($fp); ftruncate($fp, 0); fwrite($fp, (string) time()); flock($fp, LOCK_UN); fclose($fp); out(200, null, true); } catch (MailerException $e) { flock($fp, LOCK_UN); fclose($fp); out(500, 'Failed to send email'); } // ── Helpers ─────────────────────────────────────────────────────────────────── function clean(int $type, string $key, int $maxLen): string { $val = filter_input($type, $key, FILTER_UNSAFE_RAW) ?? ''; $val = strip_tags(trim((string) $val)); return mb_substr($val, 0, $maxLen); } function out(int $code, ?string $error, bool $success = false): void { ob_end_clean(); http_response_code($code); echo json_encode(['success' => $success, 'error' => $error]); exit; }