refactor: finalize PHP handler architecture and API integration

This commit is contained in:
Chneemann 2026-07-25 10:19:22 +02:00
parent 6e2c6644df
commit c02af434dc
5 changed files with 118 additions and 49 deletions

7
backend/config.php Normal file
View file

@ -0,0 +1,7 @@
<?php
return [
'recipient_email' => 'dev@andre-kempf.com',
'from_email' => 'noreply@andre-kempf.com',
'app_name' => 'Portfolio Contact',
];

67
backend/sendMail.php Normal file
View file

@ -0,0 +1,67 @@
<?php
$config = require_once __DIR__ . '/config.php';
require_once __DIR__ . '/templates/emailTemplate.php';
switch ($_SERVER['REQUEST_METHOD']) {
case ("OPTIONS"): // Allow preflight requests
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Allow-Headers: Content-Type");
exit;
case ("POST"): // Handle and send the email
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
// Read raw JSON payload
$json = file_get_contents('php://input');
$params = json_decode($json);
// 1. Honeypot check for spam protection
if (!empty($params->website)) {
http_response_code(200);
echo json_encode(["status" => "success"]);
exit;
}
// 2. Sanitize inputs & set fallbacks
$email = filter_var($params->email ?? '', FILTER_VALIDATE_EMAIL);
$name = htmlspecialchars(trim($params->name ?? 'Anonymous'), ENT_QUOTES, 'UTF-8');
$text = htmlspecialchars(trim($params->message ?? ''), ENT_QUOTES, 'UTF-8');
// Validate required fields
if (!$email || empty($text)) {
http_response_code(400);
echo json_encode(["status" => "error", "message" => "Invalid input"]);
exit;
}
// 3. Render template
$emailBody = renderEmailTemplate($name, $email, $text, $params->message ?? '');
$subject = "{$config['app_name']}: $name";
// 4. Set headers
$headers = [
'MIME-Version: 1.0',
'Content-type: text/html; charset=utf-8',
"From: {$config['from_email']}",
"Reply-To: $email"
];
// Send email
$success = mail($config['recipient_email'], $subject, $emailBody, implode("\n", $headers));
if ($success) {
http_response_code(200);
echo json_encode(["status" => "success"]);
} else {
http_response_code(500);
echo json_encode(["status" => "error"]);
}
break;
default: // Reject any other HTTP method
header("Allow: POST", true, 405);
exit;
}

View file

@ -0,0 +1,35 @@
<?php
function renderEmailTemplate(string $name, string $email, string $text, string $paramsMessage): string {
// Pre-fill reply parameters using rawurlencode (%20 for spaces)
$replySubject = rawurlencode("Re: Portfolio Contact from $name");
$replyBody = rawurlencode("\n\n---\nOriginal message from $name ($email):\n" . $paramsMessage);
$mailtoLink = "mailto:$email?subject=$replySubject&body=$replyBody";
// Cleaned message body to avoid double breaks
$formattedText = nl2br(trim($text));
return "
<div style='width: 100%; height: 100%; background-color: #fff; font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;'>
<div style='background-color: #020617; color: #f8fafc; padding: 28px; max-width: 800px; margin: 0 auto; border: 1px solid #1e293b; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);'>
<div style='margin-bottom: 20px;'>
<span style='font-family: monospace; color: #3b82f6; font-size: 12px;'>// Portfolio Contact Message</span>
<h2 style='margin: 6px 0 0 0; color: #ffffff; font-size: 20px;'>New message from $name</h2>
</div>
<div style='background-color: #0f172a; border: 1px solid #1e293b; padding: 18px; border-radius: 8px; margin-bottom: 24px;'>
<div style='font-family: monospace; font-size: 12px; color: #64748b; margin-bottom: 12px;'>
<span style='color: #3b82f6;'>From:</span> $name ($email)
</div>
<div style='font-size: 14px; line-height: 1.6; color: #cbd5e1;'>{$formattedText}</div>
</div>
<div style='border-top: 1px solid #1e293b; padding-top: 20px; text-align: center;'>
<a href='$mailtoLink' style='display: inline-block; padding: 12px 24px; background-color: #2563eb; color: #ffffff; font-weight: 500; text-decoration: none; border-radius: 8px; font-size: 13px; font-family: monospace;'>
Quick Reply
</a>
</div>
</div>
</div>
";
}

View file

@ -1,43 +0,0 @@
<?php
switch ($_SERVER['REQUEST_METHOD']) {
case ("OPTIONS"): // Allow preflighting to take place.
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Allow-Headers: content-type");
exit;
case ("POST"): // Send the email
header("Access-Control-Allow-Origin: *");
// Payload is sent to php://input as raw JSON text
$json = file_get_contents('php://input');
$params = json_decode($json);
// Honeypot check
if (!empty($params->website)) {
http_response_code(200);
exit;
}
// Extract values
$email = $params->email;
$name = $params->name;
$message = $params->message;
$recipient = 'dev@andre-kempf.com';
$subject = "Contact Form <$email>";
$message = "From: $name<br> Email: $email <br><br>$message";
$headers = array();
$headers[] = 'MIME-Version: 1.0';
$headers[] = 'Content-type: text/html; charset=utf-8';
$headers[] = "From: noreply@andre-kempf.com";
mail($recipient, $subject, $message, implode("\r\n", $headers));
break;
default: // Reject non-POST or OPTIONS requests.
header("Allow: POST", true, 405);
exit;
}

View file

@ -34,13 +34,16 @@ export default function Contact() {
setError(false);
try {
const response = await fetch("https://andre-kempf.com/sendMail.php", {
method: "POST",
headers: {
"Content-Type": "application/json",
const response = await fetch(
"https://andre-kempf.com/backend/sendMail.php",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name, email, message, honeypot }),
},
body: JSON.stringify({ name, email, message, honeypot }),
});
);
if (response.ok) {
setSubmitted(true);