type Env = { TURNSTILE_SECRET_KEY?: string; RESEND_API_KEY?: string; CONTACT_FROM_EMAIL?: string; CONTACT_TO_EMAIL?: string; }; const jsonResponse = (data: Record, status = 200) => new Response(JSON.stringify(data), { status, headers: { "content-type": "application/json" }, }); const fetchWithTimeout = async ( input: RequestInfo | URL, init: RequestInit, timeoutMs: number, label: string, ) => { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { return await fetch(input, { ...init, signal: controller.signal }); } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; throw new Error(`${label} request failed: ${message}`); } finally { clearTimeout(timeout); } }; const readFormField = (value: FormDataEntryValue | null) => typeof value === "string" ? value.trim() : ""; const readTextField = (value: string | null) => (value ?? "").trim(); const parseFormFields = async (request: Request) => { const contentType = request.headers.get("content-type") ?? ""; if (contentType.includes("application/x-www-form-urlencoded")) { const bodyText = await request.text(); const params = new URLSearchParams(bodyText); return { name: readTextField(params.get("name")), email: readTextField(params.get("email")), message: readTextField(params.get("message")), turnstileToken: readTextField(params.get("cf-turnstile-response")), }; } const formData = await request.formData(); return { name: readFormField(formData.get("name")), email: readFormField(formData.get("email")), message: readFormField(formData.get("message")), turnstileToken: readFormField(formData.get("cf-turnstile-response")), }; }; const handlePost: PagesFunction = async ({ request, env }) => { const { name, email, message, turnstileToken } = await parseFormFields(request); if (!name || !email || !message) { return jsonResponse( { ok: false, error: "Missing required fields." }, 400, ); } if (!env.TURNSTILE_SECRET_KEY) { return jsonResponse( { ok: false, error: "Turnstile secret key not configured." }, 500, ); } if (!turnstileToken) { return jsonResponse( { ok: false, error: "Turnstile verification failed." }, 400, ); } const verifyBody = new URLSearchParams({ secret: env.TURNSTILE_SECRET_KEY, response: turnstileToken, remoteip: request.headers.get("CF-Connecting-IP") ?? "", }); const verifyResponse = await fetchWithTimeout( "https://challenges.cloudflare.com/turnstile/v0/siteverify", { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: verifyBody, }, 8000, "Turnstile", ); const verifyResult = (await verifyResponse.json()) as { success?: boolean; "error-codes"?: string[]; messages?: string[]; hostname?: string; action?: string; cdata?: string; }; if (!verifyResult.success) { const details = [ verifyResult["error-codes"]?.length ? `codes=${verifyResult["error-codes"].join(",")}` : "", verifyResult.messages?.length ? `messages=${verifyResult.messages.join(",")}` : "", verifyResult.hostname ? `hostname=${verifyResult.hostname}` : "", verifyResult.action ? `action=${verifyResult.action}` : "", ] .filter(Boolean) .join(" | ") .slice(0, 500); return jsonResponse( { ok: false, error: "Turnstile verification failed.", details: details || undefined, }, 400, ); } if (!env.RESEND_API_KEY) { return jsonResponse( { ok: false, error: "Email provider not configured." }, 500, ); } const fromEmail = env.CONTACT_FROM_EMAIL; if (!fromEmail) { return jsonResponse( { ok: false, error: "Sender email not configured." }, 500, ); } const toEmail = env.CONTACT_TO_EMAIL ?? "calum@muir.in"; const emailPayload = { from: fromEmail, to: [toEmail], reply_to: email, subject: "New contact form submission", text: `Name: ${name}\nEmail: ${email}\n\n${message}`, }; const response = await fetchWithTimeout( "https://api.resend.com/emails", { method: "POST", headers: { Authorization: `Bearer ${env.RESEND_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(emailPayload), }, 8000, "Resend", ); if (!response.ok) { const contentType = response.headers.get("content-type") ?? ""; const errorText = contentType.includes("application/json") ? JSON.stringify(await response.json().catch(() => ({}))) : await response.text(); const errorCode = response.headers.get("x-ms-error-code"); const details = [ `${response.status} ${response.statusText}`.trim(), errorCode ? `code=${errorCode}` : "", errorText.trim(), ] .filter(Boolean) .join(" | ") .slice(0, 500); return jsonResponse( { ok: false, error: "Failed to send email.", details, }, 502, ); } return jsonResponse({ ok: true }); }; export const onRequest: PagesFunction = async (context) => { try { if (context.request.method !== "POST") { return jsonResponse( { ok: false, error: "Method not allowed." }, 405, ); } return await handlePost(context); } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return jsonResponse( { ok: false, error: "Unhandled exception.", details: message }, 500, ); } };