The internet is by far an evil place. Spam bots crawled my website even before Google did and I received my first spam message just before I finished securing my contact form. This tutorial will help you to avoid 99.9% of spam coming your way. The main parts of this tutorial include:

  • setup php PEAR::Mail with Google's SMTP server for message delivery
  • protect your php message processing script from direct access
  • prevent php mail form injection attacks
  • verify the form inputs
  • use tricks to confuse spam bots and set a honeypot
  • use a CAPTCHA and php-sessions for highest security

Setup PHP and use Google's SMTP server

Warning! This entire tutorial comes with an overall exclusion of liability!

I'm running my own small webserver on a Raspberry Pi over my home broadband connection. (Read everything about my experience with the RaspberryPi as webserver here.) This means I have to setup PHP to relay emails over Google's free SMTPserver. This is by far the simplest solution.

  1. Install PEAR, the php extension and application repository on your webserver. On Arch Linux this is done via pacman -S php-pear
  2. Install Mail and Net_SMTP from the PEAR repository
    pear install Mail
    pear install Net_SMTP
    
  3. Create a PHP script which sends your messages using the Google SMTP servers. One first step of spam prevention is obscurity! If you name the script contact.php it will be soon found by spam bots through search engines. So why not name it something less obvious. Of course the later steps of this tutorial will prevent the abuse of your form but this way you also reduce malicious traffic from spam bots. Finally, this is all you need to use PEAR::Mail to send emails through Google's SMTP servers:
    include('Mail.php');
    $smtp = Mail::factory('smtp', 
        array ('host' => 'ssl://smtp.gmail.com', 
        'port' => '465',
        'auth' => true, 
        'username' => '<your_login>@gmail.com', 
        'password' => '<your_pwd>')); 
          
    $mail = $smtp->send(<recipients>, <headers>, <email_content>);
    if (PEAR::isError($mail)) die($mail->getMessage());
    
    I highly recommend you to create an application password for your Google account. In case it gets compromised you can simply cancel it and you remain your access and security of your Google account. 
    Important Obviously, you have to secure the script from direct access as it contains your password! How to do that depends on your server configuration. I use Nginx and the internal directive.

SPAM protected Contact Form

There are different levels of protection against SPAM. There are simple measures which don't effect the usability of your contact form and then there are more secure precautions which mean also mean extra hurdles for legitimate users. You have to trade off security against usability as it seems reasonable for your application. First, the simple but essential steps which will already sort most of your spam problems and don't even bother your visitors:

  1. Obscurity all over again: use less obvious/common names for your value fields
    <input name="p1" placeholder="Your Name" type="text" > 
    <input name="p2" placeholder="Your email address" type="text" >
    
    This sorts out the simple spam bots which try the most common POST strings against your contact form. It also prevents spam bots from guessing where the email address etc. goes in your form.
  2. Use a hidden value field to prevent direct access to your php script processing the contact form. This will catch spam bots which never loaded your contact form but simply try against the php script directly. In the HTML form you add:
    <input name="p3" type="hidden" value="sometext" >
    
    And in the php script processing the form you check whether the hidden value was set:
    if (!isset($_POST["p3"]) || $_POST["p3"] != "sometext") {  
        header("Location: contact_form.html");
        exit;  
    }  
    
    When the check fails it sends the user to the contact form instead. Replace the location with the name of the website which holds the contact form.
  3. In your php script, check whether the POST method was used:
    if ("POST" != getenv("REQUEST_METHOD")) {  
        header("Location: contact_form.html");
        exit;  
    } 
    
  4. In your php script, check for user-agent and http-referer:
    if ("" == getenv("HTTP_USER_AGENT") || "" == getenv("HTTP_REFERER")) {
        header("Location: contact_form.html");
        exit;  
    } 
    
  5. The next step is to check for a valid email address and prevent injection attacks. We called the email field p2 in this example:
    $email = $_POST["p2"];
    if (!preg_match("/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$/", $email))
        exit;
    
  6. A last measure is to trick the spam bot into identifying itself using a honeypot. Add an input field to the form which you hide from normal users with CSS which has the alluring name email. Spam bots will post an email into that field for sure. Add the following to your HTML form
    <input name="email" style="display:none;" type="text">
    
    And check whether a spam bot took the bait in your php script processing your contact form:
    if (!empty($_POST["email"])) 
        exit;  
    

Add a CAPTCHA for best protection

The best way to get rid of spam bots is to use a CAPTCHA. Unfortunately, this adds an extra annoyance for your visitors. But the internet is full of them so that they are widely tolerated. There are professional CAPTCHA solutions which especially try to address the problem of barrier-free access. This script is perfect if you look for a simple solution for a smaller homepage.

  1. Create an additional script called captcha.php with the following code to create your CAPTCHA image:
    <?php
    session_start();
    $string = '';
    for ($i = 0; $i < 5; $i++) {
       $string .= chr(rand(97, 122));
    }
    $_SESSION['captcha'] = $string;
    $image = imagecreatetruecolor(180, 60);
    $black = imagecolorallocate($image, 0, 0, 0);
    $white = imagecolorallocate($image, 255, 255, 255);
    imagefilledrectangle($image,0,0,180,60,$white);
    imagettftext ($image, 35, 0, 10, 45, $black, "font/zxx_noise.ttf", $_SESSION['captcha']);
    header("Content-type: image/png");
    imagepng($image);
    ?>
    
    This small script creates a CAPTCHA of 5 random letters. To foul text recognition I use the ZXX font. You can download it from http://z-x-x.org/. Use one of the free websites to convert the font file into a TrueType Font (.ttf) like www.freefontconverter.com and place the .ttf file in a folder called font on your webserver.
  2. Add the following line to your HTML form:
    <img class="img-responsive" src="captcha.php" />
    <input name="captcha" placeholder="Type the 5 letters from the image" type="text" />
    
    and to your php script processing the form you add
    session_start(); 
    if ($_POST['captcha'] != $_SESSION['captcha'])
       exit;
    
    That's already all!

All the pieces falling together

Important! All code comes with an overall exclusion of liability! Please don't just copy and paste code. Please read what the different bits are doing and change the scripts to your own needs. Most importantly, you have to secure the submission script from external access as it will hold your Google password! The simple measures taken in the script itself won't guaranty full protection. You have to secure the scripts on the webserver level, too.

Firstly, the form itself:

<form action="submission.php" class="well span7" method="POST">
 <div class="row">
  <div class="span3">
   <label>Name</label>
   <input class="span3" name="p1" placeholder="Your Name" type="text" />
   <label>Email Address</label> 
   <input class="span3" name="p2" placeholder="Your email address" type="text" /> 
   <img class="img-responsive" src="captcha.php" /> 
   <label>CAPTCHA</label> 
   <input class="span3" name="p3" placeholder="Type the 5 letters from the image" type="text" />
  </div>
  <div class="span4">
   <label>Message</label>
   <textarea class="input-xlarge span4" id="message" name="p4" rows="10"></textarea>
  </div>
  <input name="email" style="display:none;" type="text" />
  <input name="p5" type="hidden" value="contact" />
  <button class="btn btn-primary pull-right" type="submit">Send</button>
 </div>
</form>

Secondly, the captcha script captcha.php:

<?php
session_start();
$string = '';
for ($i = 0; $i < 5; $i++) {
   $string .= chr(rand(97, 122));
}
$_SESSION['captcha'] = $string;
$image = imagecreatetruecolor(180, 60);
$black = imagecolorallocate($image, 0, 0, 0);
$white = imagecolorallocate($image, 255, 255, 255);
imagefilledrectangle($image,0,0,180,60,$white);
imagettftext ($image, 35, 0, 10, 45, $black, "font/zxx_noise.ttf", $_SESSION['captcha']);
header("Content-type: image/png");
imagepng($image);
?>

Last but not least, the processing script submission.php

<?php  
session_start(); 
include('Mail.php');
  
if (!isset($_POST["p5"]) || $_POST["p5"] != "contact" || "POST" != getenv("REQUEST_METHOD")) {  
    header("Location: /contact"); exit;  
}  
if (!empty($_POST["email"])) {
    header("Location: contact"); exit;  
}  
if ("" == getenv("HTTP_USER_AGENT") || "" == getenv("HTTP_REFERER")) {
    header("Location: /contact"); exit;  
} 
         
$name = $_POST["p1"];  
$email_address = $_POST["p2"];  
$message = $_POST["p4"];  
      
if ($_POST['p3'] != $_SESSION['captcha'])
    $error = "Please type the 5 letters from the image!";
elseif (empty ($name))  
    $error = "You must enter your name.";  
elseif (empty ($email_address))   
    $error = "You must enter your email address.";  
elseif (!preg_match("/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$/", $email_address))  
    $error = "You must enter a valid email address.";  
elseif (empty ($message))  
    $error = "You must enter a message.";  
elseif (preg_match("/http/i", $message) )
    $error = "No URLs in the message allowed for SPAM prevention!";      
elseif (preg_match("/http/i", $name) )
    $error = "No URLs as Name allowed for SPAM prevention!";          
if (isset($error)) {  
    header("Location: /contact?e=".urlencode($error)); exit;  
}  
          
$email_content = "Name: $name \n";  
$email_content .= "Email Address: $email_address\n";  
$email_content .= "Message:\n\n$message";  
      
$recipients = '#####';
$headers['From']    = $email_address;
$headers['To']      = '#####';
$headers['Subject'] = '#####';
$smtp = Mail::factory('smtp', 
        array ('host' => 'ssl://smtp.gmail.com', 
        'port' => '465',
        'auth' => true, 
        'username' => '#####', 
        'password' => '#####'));     
$mail = $smtp->send($recipients, $headers, $email_content);
if (PEAR::isError($mail)) die($mail->getMessage());     
header("Location: /contact");
exit;  
?>