PHP contact form
For twenty years the answer to a contact form was a PHP script calling mail(). It still works, and it still has the same three problems it always had. Here is the script, an honest account of what breaks, and the version with no PHP in it at all.
Replace YOUR_FORM_ID with a form of your own and this sends to your inbox.
<?php
// The classic version. It needs PHP hosting, a configured MTA, and it will
// land in spam more often than not because the mail is unauthenticated.
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$name = filter_input(INPUT_POST, "name", FILTER_SANITIZE_SPECIAL_CHARS);
$email = filter_input(INPUT_POST, "email", FILTER_VALIDATE_EMAIL);
$message = filter_input(INPUT_POST, "message", FILTER_SANITIZE_SPECIAL_CHARS);
if (!$email) {
http_response_code(422);
exit("Invalid email address");
}
// Header injection lives here. Never interpolate a raw field into a header.
$headers = "From: site@example.com\r\n" .
"Reply-To: " . $email . "\r\n";
mail("you@example.com", "New enquiry from " . $name, $message, $headers);
header("Location: /thank-you.html");
exit;
}<form action="https://submit.formpost.ai/YOUR_FORM_ID" method="POST">
<h2>Contact us</h2>
<label for="name">Name</label>
<input id="name" type="text" name="name" required>
<label for="email">Email</label>
<input id="email" type="email" name="email" required>
<label for="message">Message</label>
<textarea id="message" name="message" rows="4" required></textarea>
<button type="submit">Send message</button>
</form>What actually goes wrong with mail()
- Deliverability. mail() hands the message to the local MTA, which sends from your web server's IP with no SPF alignment and no DKIM signature. Gmail and Outlook increasingly treat that as spam — and you never find out, because mail() returns true regardless.
- It returns true for queued, not delivered. The function tells you the MTA accepted the message. Everything after that — rejection, bounce, silent drop — is invisible to your script.
- Header injection. Interpolating a raw field into the headers lets somebody put a newline and a Bcc: in your email field and use your server to send mail. The example above validates the address first, which is the minimum.
- It needs PHP hosting. Which rules out GitHub Pages, Netlify, Vercel static output, S3, and every other place a static site would rather live.
The replacement is the HTML you already have
Delete send.php, change the action to an endpoint, and the form is done. The sending happens somewhere that is set up for it — authenticated domains, retries, bounce handling — and your site goes back to being static files.
<form action="https://submit.formpost.ai/YOUR_FORM_ID" method="POST">Everything the PHP was doing has an equivalent that needs no code. The redirect after success is a hidden field or a form setting. Validation is the required and type attributes. Reply-to is set automatically from the field named email. Spam filtering is a honeypot and an optional captcha, configured rather than written.
If you are keeping the PHP
Use a proper mail library — PHPMailer or Symfony Mailer — and send through an authenticated SMTP provider rather than the local MTA. That fixes deliverability and gives you a real error when sending fails. Keep filter_input with FILTER_VALIDATE_EMAIL on anything that reaches a header, and never interpolate user input into one directly.
Questions about this contact form
- Do I need PHP for a contact form?
- No. PHP was only ever there to send the email. Point the form's action at an endpoint that sends it for you and the page can be a static HTML file on any host.
- Why do emails from PHP mail() go to spam?
- They are sent from your web server's IP with no SPF alignment and no DKIM signature, so receiving providers cannot verify them. This gets worse every year as bulk-sender requirements tighten.
- What is email header injection?
- Putting a newline plus an extra header into a field that gets interpolated into the mail headers — typically to add a Bcc and relay spam through your server. Validate the address before it touches a header, and never build headers from raw input.
- Is mail() returning true enough to confirm delivery?
- No. It means the local mail transfer agent accepted the message for queuing. Rejections, bounces and silent drops all happen after that point and your script never hears about any of them.
Related examples
HTML contact form
Pure HTML that sends email. No JavaScript, no PHP, nothing to install.
Contact form without a backend
For static sites: no server, no database, no serverless function.
Contact form with captcha
Turnstile, hCaptcha or reCAPTCHA — plus the honeypot that does most of the work.
Or go back to all 21 contact form examples.
Give this form an endpoint
Formpost takes the submission, emails it to you and keeps a searchable copy. Free for 250 messages a month, with unlimited forms and no card.