quorla-website/public/api/contact.php
metatroncubeswdev ed61d9057b Contact form sends through the server's mail (PHP endpoint) instead of mailto
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:00:38 -04:00

104 lines
3.8 KiB
PHP

<?php
// Contact form endpoint for quorlasocial.com.
// Astro copies this file as-is to dist/api/contact.php; HestiaCP runs it with PHP and sends through the
// server's own mail system (exim), so the message is DKIM-signed for quorlasocial.com. No passwords in code.
declare(strict_types=1);
const TO = 'support@quorlasocial.com';
const FROM = 'support@quorlasocial.com';
const ALLOWED_ORIGINS = ['https://quorlasocial.com', 'https://www.quorlasocial.com'];
const MAX_PER_HOUR = 5; // per visitor IP
const MIN_SECONDS_ON_PAGE = 3; // bots submit instantly
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
function reply(int $status, array $body): void {
http_response_code($status);
echo json_encode($body);
exit;
}
function field(string $key, int $max): string {
$v = trim((string)($_POST[$key] ?? ''));
$v = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $v) ?? '';
return mb_substr($v, 0, $max);
}
function oneLine(string $v): string {
return trim(preg_replace('/[\r\n]+/', ' ', $v) ?? '');
}
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
reply(405, ['ok' => false, 'error' => 'Use the form on quorlasocial.com/contact.']);
}
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if ($origin !== '' && !in_array($origin, ALLOWED_ORIGINS, true)) {
reply(403, ['ok' => false, 'error' => 'Not allowed.']);
}
// Honeypot and timing: pretend success so bots don't learn anything.
if (field('website_fax', 200) !== '') {
reply(200, ['ok' => true]);
}
$started = (int)($_POST['t'] ?? 0);
if ($started > 0 && (time() - intdiv($started, 1000)) < MIN_SECONDS_ON_PAGE) {
reply(200, ['ok' => true]);
}
// Rate limit per IP (small file in the system temp folder).
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$rlFile = sys_get_temp_dir() . '/quorla-contact-' . hash('sha256', $ip);
$now = time();
$hits = [];
if (is_file($rlFile)) {
$hits = array_filter(array_map('intval', explode(',', (string)file_get_contents($rlFile))), fn($t) => $t > $now - 3600);
}
if (count($hits) >= MAX_PER_HOUR) {
reply(429, ['ok' => false, 'error' => 'Too many requests. Please email support@quorlasocial.com instead.']);
}
$name = oneLine(field('name', 100));
$agency = oneLine(field('agency', 150));
$email = oneLine(field('email', 200));
$clients = oneLine(field('clients', 40));
$deployment = oneLine(field('deployment', 40));
$message = field('message', 5000);
if ($name === '' || $agency === '' || $clients === '') {
reply(400, ['ok' => false, 'error' => 'Please fill in your name, agency and number of clients.']);
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
reply(400, ['ok' => false, 'error' => 'Please enter a valid work email.']);
}
$subject = 'Quorla access request: ' . $agency . ' (' . $name . ')';
$body = "New access request from quorlasocial.com/contact\n\n"
. "Name: $name\n"
. "Agency: $agency\n"
. "Work email: $email\n"
. "Client accounts: $clients\n"
. "Preferred deployment: " . ($deployment !== '' ? $deployment : 'not given') . "\n\n"
. "Notes:\n" . ($message !== '' ? $message : '(none)') . "\n\n"
. "---\nSent " . gmdate('Y-m-d H:i') . " UTC from IP $ip. Reply to this email to answer them directly.\n";
$headers = [
'From: Quorla website <' . FROM . '>',
'Reply-To: ' . $name . ' <' . $email . '>',
'MIME-Version: 1.0',
'Content-Type: text/plain; charset=UTF-8',
'Content-Transfer-Encoding: 8bit',
'X-Mailer: quorlasocial.com contact form',
];
$sent = mail(TO, '=?UTF-8?B?' . base64_encode($subject) . '?=', $body, implode("\r\n", $headers), '-f' . FROM);
if (!$sent) {
reply(500, ['ok' => false, 'error' => 'We could not send your request. Please email support@quorlasocial.com.']);
}
$hits[] = $now;
@file_put_contents($rlFile, implode(',', $hits), LOCK_EX);
reply(200, ['ok' => true]);