Files
williammarch.xyz/app/api/contact/route.ts
2026-04-02 23:29:59 +01:00

110 lines
2.8 KiB
TypeScript

import { NextResponse } from 'next/server';
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
sendmail: true,
newline: 'unix',
path: '/usr/sbin/sendmail',
});
export async function POST(req: Request) {
try {
const {
name,
email,
message,
company, // honeypot
turnstileToken,
formStartedAt,
} = await req.json();
// Basic validation
if (!name || !email || !message) {
return NextResponse.json(
{ error: 'Missing required fields.' },
{ status: 400 },
);
}
// Honeypot: bots often fill this
if (company && String(company).trim().length > 0) {
return NextResponse.json({ error: 'Spam detected.' }, { status: 400 });
}
// Time-based spam check
const started = Number(formStartedAt);
if (!started || Date.now() - started < 3000) {
return NextResponse.json(
{ error: 'Submission too fast.' },
{ status: 400 },
);
}
// Turnstile validation
if (!turnstileToken) {
return NextResponse.json(
{ error: 'Missing captcha token.' },
{ status: 400 },
);
}
const secret = process.env.TURNSTILE_SECRET_KEY;
if (!secret) {
return NextResponse.json(
{ error: 'Turnstile secret not configured.' },
{ status: 500 },
);
}
const ip = req.headers.get('cf-connecting-ip') || '';
const verifyRes = await fetch(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
secret,
response: turnstileToken,
remoteip: ip,
}),
},
);
const verifyData = (await verifyRes.json()) as {
success: boolean;
'error-codes'?: string[];
};
if (!verifyData.success) {
return NextResponse.json(
{
error: 'Captcha verification failed.',
details: verifyData['error-codes'] || [],
},
{ status: 400 },
);
}
// Send email via local sendmail (Postfix/Sendmail/Exim)
await transporter.sendMail({
from: 'William March <contact@williammarch.xyz>',
to: 'qemuguest@protonmail.com',
replyTo: email,
subject: `New message from ${name} via williammarch.xyz`,
text: `From: ${name} <${email}>\n\n${message}`,
html: `
<p><strong>From:</strong> ${name} &lt;${email}&gt;</p>
<p><strong>Message:</strong></p>
<p>${message.replace(/\n/g, '<br/>')}</p>
`,
});
return NextResponse.json({ ok: true });
} catch (error) {
const msg =
error instanceof Error ? error.message : 'Failed to send email.';
return NextResponse.json({ error: msg }, { status: 500 });
}
}