--- /dev/null
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "826f515f5ef16946d3e3ee3e3205b25e",
+ "packages": [
+ {
+ "name": "phpmailer/phpmailer",
+ "version": "v7.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPMailer/PHPMailer.git",
+ "reference": "ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088",
+ "reference": "ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-filter": "*",
+ "ext-hash": "*",
+ "php": ">=5.5.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+ "doctrine/annotations": "^1.2.6 || ^1.13.3",
+ "php-parallel-lint/php-console-highlighter": "^1.0.0",
+ "php-parallel-lint/php-parallel-lint": "^1.3.2",
+ "phpcompatibility/php-compatibility": "^10.0.0@dev",
+ "squizlabs/php_codesniffer": "^3.13.5",
+ "yoast/phpunit-polyfills": "^1.0.4"
+ },
+ "suggest": {
+ "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
+ "directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
+ "ext-imap": "Needed to support advanced email address parsing according to RFC822",
+ "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
+ "ext-openssl": "Needed for secure SMTP sending and DKIM signing",
+ "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
+ "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
+ "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
+ "psr/log": "For optional PSR-3 debug logging",
+ "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
+ "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PHPMailer\\PHPMailer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1-only"
+ ],
+ "authors": [
+ {
+ "name": "Marcus Bointon",
+ "email": "phpmailer@synchromedia.co.uk"
+ },
+ {
+ "name": "Jim Jagielski",
+ "email": "jimjag@gmail.com"
+ },
+ {
+ "name": "Andy Prevost",
+ "email": "codeworxtech@users.sourceforge.net"
+ },
+ {
+ "name": "Brent R. Matzelle"
+ }
+ ],
+ "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
+ "support": {
+ "issues": "https://github.com/PHPMailer/PHPMailer/issues",
+ "source": "https://github.com/PHPMailer/PHPMailer/tree/v7.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Synchro",
+ "type": "github"
+ }
+ ],
+ "time": "2026-01-09T18:02:33+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {},
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {},
+ "platform-dev": {},
+ "plugin-api-version": "2.6.0"
+}
--- /dev/null
+<?php
+/**
+ * Contact form handler for danix.me
+ *
+ * Spam protection:
+ * 1. Honeypot field — bots fill it, humans don't see it
+ * 2. Timing check — reject submissions faster than MIN_ELAPSED seconds
+ * 3. IP rate limit — one submission per IP per RATE_WINDOW seconds
+ */
+
+ob_start();
+header('Content-Type: application/json; charset=utf-8');
+header('X-Content-Type-Options: nosniff');
+header('X-Frame-Options: DENY');
+header('Referrer-Policy: strict-origin-when-cross-origin');
+header('Cache-Control: no-store');
+
+// ── Configuration ─────────────────────────────────────────────────────────────
+// Credentials are read from server environment variables — never hardcoded here.
+// Set them in your nginx fastcgi_param, Apache SetEnv, or hosting panel.
+const SENDER_DOMAIN = 'danix.xyz'; // used in From name and subject prefix
+const ALLOWED_ORIGIN = 'https://danix.me'; // CSRF: only accept from this origin
+const MIN_ELAPSED = 4; // seconds before submission is accepted
+const RATE_WINDOW = 300; // seconds between allowed submissions per IP
+const RATE_DIR = '/var/lib/danixme/cf_rate'; // owned by www-data:www-data, mode 0700
+
+// $cfg = [
+// 'recipient' => 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.me] - ' . ($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;
+}