Created
March 9, 2026 19:49
-
-
Save Nooshu/0e1cb510f533e8ae9247f84bf9c724b3 to your computer and use it in GitHub Desktop.
This is the code that runs on my Cloudflare worker to allow sending of emails from my website contact form to my choosen email address.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { escapeHtml } from '../../_helpers/escape-html.js'; | |
| export const onRequestPost = async ({ request, env }) => { | |
| try { | |
| // Accept standard form submissions | |
| const contentType = request.headers.get('content-type') || ''; | |
| if (!contentType.includes('application/x-www-form-urlencoded') && !contentType.includes('multipart/form-data')) { | |
| return new Response(JSON.stringify({ error: 'Unsupported content type' }), { | |
| status: 415, | |
| headers: { 'content-type': 'application/json' }, | |
| }); | |
| } | |
| const form = await request.formData(); | |
| // Honeypot - reject if set | |
| if (form.get('_akjhaskjdkjhakjshdadjknjnkdsa')) { | |
| const hpUrl = new URL('/', request.url); | |
| return Response.redirect(hpUrl.toString(), 303); | |
| } | |
| const name = (form.get('name') || '').toString().trim(); | |
| const email = (form.get('email') || '').toString().trim(); | |
| const message = (form.get('message') || '').toString().trim(); | |
| const redirectUrl = (form.get('_redirect') || '/contact/thanks/').toString(); | |
| if (!name || !email || !message) { | |
| return new Response(JSON.stringify({ error: 'Missing required fields' }), { | |
| status: 400, | |
| headers: { 'content-type': 'application/json' }, | |
| }); | |
| } | |
| // Basic validation | |
| if (name.length > 200 || email.length > 320 || message.length > 5000) { | |
| return new Response(JSON.stringify({ error: 'Invalid field lengths' }), { | |
| status: 400, | |
| headers: { 'content-type': 'application/json' }, | |
| }); | |
| } | |
| // Verify reCAPTCHA v3 token - REQUIRED for all submissions | |
| const recaptchaToken = form.get('g-recaptcha-response') || form.get('h-captcha-response'); | |
| if (!recaptchaToken) { | |
| return new Response(JSON.stringify({ error: 'reCAPTCHA token is required' }), { | |
| status: 400, | |
| headers: { 'content-type': 'application/json' }, | |
| }); | |
| } | |
| const secret = env.RECAPTCHA_SECRET_KEY || env.RECAPTCHA_SECRET; | |
| if (!secret) { | |
| return new Response( | |
| JSON.stringify({ | |
| error: 'Server not configured for CAPTCHA (missing RECAPTCHA_SECRET_KEY/RECAPTCHA_SECRET)', | |
| }), | |
| { status: 500, headers: { 'content-type': 'application/json' } } | |
| ); | |
| } | |
| // Include remote IP for additional verification (best practice) | |
| const remoteIp = request.headers.get('CF-Connecting-IP') || request.cf?.clientAddress || null; | |
| const verifyParams = new URLSearchParams({ | |
| secret, | |
| response: recaptchaToken.toString(), | |
| }); | |
| if (remoteIp) { | |
| verifyParams.append('remoteip', remoteIp); | |
| } | |
| const verifyResp = await fetch('https://www.google.com/recaptcha/api/siteverify', { | |
| method: 'POST', | |
| headers: { 'content-type': 'application/x-www-form-urlencoded' }, | |
| body: verifyParams, | |
| }); | |
| if (!verifyResp.ok) { | |
| return new Response(JSON.stringify({ error: 'Failed to verify reCAPTCHA' }), { | |
| status: 502, | |
| headers: { 'content-type': 'application/json' }, | |
| }); | |
| } | |
| const verifyJson = await verifyResp.json(); | |
| // Check if verification was successful | |
| if (!verifyJson.success) { | |
| // Log error codes for debugging (in production, you might want to log this server-side) | |
| const errorCodes = verifyJson['error-codes'] || []; | |
| return new Response( | |
| JSON.stringify({ | |
| error: 'CAPTCHA verification failed', | |
| details: errorCodes.length > 0 ? errorCodes.join(', ') : 'Unknown error', | |
| }), | |
| { status: 400, headers: { 'content-type': 'application/json' } } | |
| ); | |
| } | |
| // Verify score (reCAPTCHA v3 returns scores between 0.0 and 1.0) | |
| // Lower scores indicate bot-like behavior. 0.5 is a common threshold, but 0.3 allows more legitimate traffic | |
| if (typeof verifyJson.score === 'number' && verifyJson.score < 0.3) { | |
| return new Response( | |
| JSON.stringify({ | |
| error: 'CAPTCHA verification failed', | |
| details: `Score too low: ${verifyJson.score}`, | |
| }), | |
| { status: 400, headers: { 'content-type': 'application/json' } } | |
| ); | |
| } | |
| // Optional: Verify hostname matches (helps prevent token reuse from other domains) | |
| const requestHost = new URL(request.url).hostname; | |
| if ( | |
| verifyJson.hostname && | |
| verifyJson.hostname !== requestHost && | |
| !requestHost.endsWith(`.${verifyJson.hostname}`) | |
| ) { | |
| // Allow subdomains but log mismatch | |
| console.warn(`reCAPTCHA hostname mismatch: expected ${requestHost}, got ${verifyJson.hostname}`); | |
| } | |
| // Prepare email via Resend | |
| const fromEmail = 'website@nooshu.com'; | |
| const subject = form.get('_email.subject')?.toString() || 'New Message from Nooshu.com'; | |
| const textBody = `New contact form submission on nooshu.com\n\nName: ${name}\nEmail: ${email}\n\nMessage:\n${message}`; | |
| const htmlBody = `<h2>New contact form submission on nooshu.com</h2><p><strong>Name:</strong> ${escapeHtml(name)}<br/><strong>Email:</strong> ${escapeHtml(email)}</p><p><strong>Message:</strong></p><pre style="white-space:pre-wrap">${escapeHtml(message)}</pre>`; | |
| const apiKey = env.RESEND_API_KEY; | |
| if (!apiKey) { | |
| return new Response( | |
| JSON.stringify({ | |
| error: 'Server not configured for email (missing RESEND_API_KEY)', | |
| }), | |
| { status: 500, headers: { 'content-type': 'application/json' } } | |
| ); | |
| } | |
| const resendResp = await fetch('https://api.resend.com/emails', { | |
| method: 'POST', | |
| headers: { | |
| 'content-type': 'application/json', | |
| Authorization: `Bearer ${apiKey}`, | |
| }, | |
| body: JSON.stringify({ | |
| from: `Nooshu Contact <${fromEmail}>`, | |
| to: ['website@nooshu.com'], | |
| reply_to: email, | |
| subject, | |
| text: textBody, | |
| html: htmlBody, | |
| }), | |
| }); | |
| if (!resendResp.ok) { | |
| const text = await resendResp.text(); | |
| return new Response(JSON.stringify({ error: 'Email send failed', details: text }), { | |
| status: 502, | |
| headers: { 'content-type': 'application/json' }, | |
| }); | |
| } | |
| // Redirect to thank you page (absolute URL required) | |
| const finalRedirect = new URL(redirectUrl, request.url); | |
| return Response.redirect(finalRedirect.toString(), 303); | |
| } catch (err) { | |
| return new Response(JSON.stringify({ error: 'Server error', details: String(err) }), { | |
| status: 500, | |
| headers: { 'content-type': 'application/json' }, | |
| }); | |
| } | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment