Secure Website Contact Forms: Honeypots, Rate Limits, Send via API
Secure your website contact form and the email it sends: honeypot fields, rate limits, header stripping, why PHP mail() fails DMARC, and sending through an API.
A contact form is a public door that sends email on your behalf. Left unguarded, it fills your inbox with spam, gets abused to relay junk to strangers using your domain, and, when it sends through the web server's built-in mail function, produces messages that fail SPF and DKIM and land in spam even when they are genuine. Fixing all three takes an hour and does not require a paid anti-spam service.
Quick answer
Add a hidden honeypot field and a minimum-time check, rate-limit submissions per IP, validate and strip headers from every input, and send the resulting notification through an authenticated channel (an SMTP submission account on port 587 or a transactional API) from a fixed address on your own domain, with the visitor's address in Reply-To rather than From. Never call PHP's mail() or its equivalents directly on a production site.
Stop the bots before they send anything
- Honeypot field. Add an input that is hidden with CSS (not type=hidden) and named something plausible like website or fax. Humans never see it; bots fill it. Reject any submission where it is non-empty.
- Time check. Record the time the form was rendered in a signed token; reject submissions that arrive in under three seconds, which is faster than any human types a message.
- Rate limit. Allow a small number of submissions per IP per hour, for example five, and return a polite error above that. Most frameworks have middleware for this.
- Only add a CAPTCHA if the first three fail to keep spam down. CAPTCHAs cost you real enquiries from real people.
Stop the form being used against others
- Never let the visitor set the To address. The notification goes to a fixed address you control.
- Strip line breaks and header-like strings (anything matching To:, Cc:, Bcc:, Content-Type:) from the name, email, and subject fields. Header injection through these fields is how an old form becomes a spam relay.
- Do not echo the visitor's message back to the address they typed as a "copy of your enquiry". Attackers use that feature to send their text to any address from your domain.
- Limit message length and reject messages that are mostly links.
Why sending from the web server fails
PHP mail(), Python's smtplib pointed at localhost, and similar functions hand the message to whatever mail agent is on the web server. That server's IP is not in your SPF record, has no DKIM key for your domain, and often has no reverse DNS. Under a DMARC policy of quarantine or reject, the notification to your own inbox fails your own policy. Worse, many forms set the From header to the visitor's address, which fails the visitor's domain policy as well. The mail is technically sent and silently lost.
Send it properly: fixed From, Reply-To visitor, authenticated channel
Send from a fixed address on your domain such as website@yourdomain.com, put the visitor's address in Reply-To, and deliver through a channel that authenticates as your domain. With OquMail that is either SMTP submission with a mailbox on mail.oqumail.com, port 587, STARTTLS, or the send API using a Bearer API key. Mail sent either way is signed with your DKIM key and passes SPF, so it passes DMARC and arrives.
// Node.js example: contact form handler sending via the OquMail API
const res = await fetch("https://api.oqumail.com/api/v1/emails", {
method: "POST",
headers: {
"Authorization": "Bearer " + process.env.OQUMAIL_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
from: "website@yourdomain.com",
to: "hello@yourdomain.com",
subject: "Contact form: " + safeSubject,
html: "<p><strong>From:</strong> " + escapeHtml(name) + " <" + escapeHtml(email) + "></p>" +
"<p>" + escapeHtml(message).replace(/\n/g, "<br>") + "</p>"
})
});Keep the API key in an environment variable on the server, never in front-end JavaScript. If the site is static, put the handler in a serverless function so the key stays server-side. Escape every field before placing it in HTML so a visitor cannot inject markup into the email your team opens.
Checklist before you go live
- Submit the form yourself and open the notification in Gmail with Show original: SPF (Sender Policy Framework — a list of servers allowed to send email as your domain)=pass, DKIM (a digital signature that proves your email was not tampered with)=pass, DMARC (a policy that tells providers what to do if someone fakes your domain)=pass.
- Submit with the honeypot filled (use browser dev tools) and confirm it is rejected silently.
- Submit six times in a minute and confirm the rate limit triggers.
- Put a line break followed by Bcc: someone@example.com in the name field and confirm it is stripped.
- Check the notification's Reply-To is the visitor and the From is your fixed address.
Common questions
My form plugin only offers "send with PHP mail" or SMTP. Which?
SMTP, with a real mailbox on your domain, port 587, STARTTLS, and the mailbox's password stored in the plugin's settings. Popular WordPress SMTP plugins do exactly this. Never leave it on PHP mail.
Should the visitor get an automatic confirmation?
Only if it is a fixed template with no visitor-supplied text, and only after rate limits and validation. Otherwise the confirmation becomes a relay for spam sent from your domain.
Is the honeypot enough on its own?
It stops most automated submissions. Combined with the time check and rate limit it stops nearly all, and the remaining trickle is usually humans, which you can handle by hand.
Free business email on your own domain
OquMail gives you up to 15 mailboxes on your domain — free — with guided SPF/DKIM/DMARC, webmail, IMAP/SMTP for any mail app, and a send API. Most teams are live in under fifteen minutes. Start at oqumail.com.
Get started free