Implementing PHPMailer With Ajax & SMTP

I’ve been implementing a lot of small websites lately that use Bootstrap templates. These templates generally include a contact form, and most also include Javascript and PHP scripts to submit the form.

The majority of these scripts rely on the PHP ‘mail()’ function to send email. I prefer to use the PHPMailer class. To keep things simple, I also prefer to send email using the SMTP protocol on small projects.

This tutorial describes how to implement a simple, secure, highly-performant contact form using PHPMailer(), Ajax, and SMTP.


Table of Contents

  1. Starting Assumptions
  2. Install PHPMailer
  3. Create the Contact Form
  4. Include the Client-side Validator
  5. Include the Server-side Validator
  6. Create the Form Processor Script
  7. Create the Form Submission Script
  8. Implement and Test
  9. Conclusion

1: Starting Assumptions

Before starting, be sure the following is true for your project:

  • PHP 5.6 or higher is installed
  • jQuery is installed
  • An email hosting account is already set up to send emails using SMTP *
  • You are comfortable working with PHP, Javascript, jQuery, and HTML
  • You are familiar with command-line basics **

* I like using Zoho Mail as well as SparkPost, but any SMTP-capable hosting service will do.

** Command-line instructions are shown for Linux Ubuntu 16.04. You may need to make adjustments if you are using a different OS, or if you prefer to use an IDE or GUI-based text-editor (i.e SublimeText).


2: Install PHPMailer

Go to the command line, navigate to the main folder for the website project, and type the following code to install the PHPMailer class:

composer require phpmailer/phpmailer

Be patient – it takes a minute or two for Composer to get everything organized.


3: Create the Contact Form

The contact form can be placed almost anywhere in your website project. I often include mine on the index page within a modal. Here’s the code I use:

<!-- Modal -->
<div class="modal fade" id="contactModal" tabindex="-1" role="dialog" aria-labelledby="contactModalLabel">
    <div class="modal-dialog" role="document">
        <div class="modal-content">

            <style type="text/css">#contactForm .control-label {display: none; visibility: hidden;}</style>
            <form method="post" accept-charset="utf-8" id="contactForm" class="async" role="form" name="sentMessage" action="assets/php/contact.php">

                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                    <h4 class="modal-title" id="contactModalLabel">Send me a message</h4>
                </div>

                <div class="modal-body">
                    <div class="row">
                        <div class="col-sm-12">
                            <input type="text" name="Title" id="Title" type="hidden" style="display: none;" />
                            <div class="form-group text required">
                                <label class="control-label" for="name">Name</label>
                                <input class="form-control" type="text" name="name" id="name" placeholder="Your Name (required)" required="required"/>
                            </div>

                            <div class="form-group email required">
                                <label class="control-label" for="email">Email</label>
                                <input class="form-control" type="email" name="email" id="email" placeholder="Your Email (required)" required="required"/>
                            </div>

                            <div class="form-group tel">
                                <label class="control-label" for="phone">Phone</label>
                                <input class="form-control" type="tel" name="phone" id="phone" placeholder="Your Phone# (optional)"/>
                            </div>

                            <div class="form-group textarea required">
                                <label class="control-label" for="message">Message</label>
                                <textarea class="form-control" name="message" id="message" required="required" rows="5"></textarea>
                            </div>

                        </div>
                    </div>
                </div>

                <div class="modal-footer">
                    <div id="responseMessage"></div>
                    <button type="submit" class="btn btn-xl btn-primary submit"><i class="fa fa-paper-plane"></i> Send Message</button>
                </div>

            </form>

        </div>
    </div>
</div>

Add this code to whichever page you want in your website project. For this tutorial, I’m using index.php. If you prefer not to include your form in a modal or to use Bootstrap, just strip out the related markup and you’re good to go.

Note that the markup for this form includes a novalidate attribute. This tells the client not to apply native HTML5 validation rules. Instead, we’ll add a custom validator to take care of this in the next step.


4: Include the Client-side Validator

NOTE: For the moment, I’m skipping this step

In a browser, go to http://reactiveraven.github.io/jqBootstrapValidation/ and download the latest version of the validator.

Now unzip this file into the directory where you store JS files for your project, then shorten the plugin folder’s name to jqBootstrapValidation.

This folder contains several files in addition to the validation script, including the readme.md and license files. Here’s how this looks for my projects:

../assets/js/jsBootstrapValidation/jsBootstrapValidation.js
../assets/js/jsBootstrapValidation/readme.md
../etc...

Now include jqBootstrapValidation.js into your project’s page template. If you’re following this tutorial, add the following to the bottom of the index.php document:

<script type="text/javascript" src="assets/js/jqBootstrapValidation/jqBootstrapValidation.js"></script>

Make sure to add this into the load sequence after jQuery and Bootstrap.


5: Include the Server-Side Validator

I like to use a validator on the server-side as well as client-side. For this tutorial, I’ll use this email validation script from Lars Moelleken.

Navigate to the folder where the project’s PHP scripts are stored, and create a new file called check_email.php. Here’s an example:

../assets/php/check_email.php

In a browser, go to http://gist.suckup.de/#sec_b88f0538d0dbcb2fc189, and copy the code for checkEmail.php to the clipboard. Open check_mail.php, and paste in the code you just copied.

You should end up with something that looks like this:

<?php
/**
 * checkEmail
 * allows for international email addresses
 * http://gist.suckup.de/#sec_b88f0538d0dbcb2fc189
 * @param String  $email
 * @param Boolean $mxCheck   (do not use, if you don't need it)
 *
 * @return Boolean
 */
function checkEmail($email, $mxCheck = false)
{
  if (!is_string($email) || strlen($email) >= 320) {
    $valid = false;
  } elseif (!preg_match('/^(.*<?)(.*)@(.*)(>?)$/', $email, $matches)) {
    $valid = false;
  } else {
    $domain = $matches[3];
    if (function_exists('idn_to_ascii')) {
      $email = $matches[1] . $matches[2] . '@' . idn_to_ascii($domain) . $matches[4];
    } else {
      $email = $matches[1] . $matches[2] . '@' . $domain . $matches[4];
    }
    if (!$domain || !$email) {
      $valid = false;
    } else {
      if (function_exists('filter_var') && function_exists('idn_to_ascii')) {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
          $valid = false;
        } else {
          $valid = true;
        }
      } else {
        if (function_exists('idn_to_ascii')) {
          $regEx = "/^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$/i";
        } else {
          $regEx = "/^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([öäüa-z0-9]{1}[öäüa-z0-9\-]{0,62}[öäüa-z0-9]{1})|[öäüa-z])\.)+[öäüa-z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$/i";
        }
        if (!preg_match($regEx, $email)) {
          $valid = false;
        } else {
          $valid = true;
        }
      }
      if ($valid && $mxCheck && function_exists('checkdnsrr')) {
        $valid = checkdnsrr($domain . '.', 'MX') || checkdnsrr($domain, 'A');
      }
    }
  }
  return $valid;
}

This server-side script goes deeper than the client-side validator to make sure the contact form’s sender has used a safe and valid email address.


6: Create the Form Processor Script

In the folder where the project’s PHP scripts are stored, create a new file called contact.php, as so:

../assets/php/contact.php

Open this new file and add the following code:

<?php
// Set the PHP time zone for SMTP to work. Do this in php.ini, or here if no access
date_default_timezone_set('Etc/UTC');
// load up PHPMailer
require_once('../../vendor/phpmailer/phpmailer/PHPMailerAutoload.php');
// load up the email validator
require_once('check_email.php');
// confirm this is a POST request
if ($_SERVER["REQUEST_METHOD"] == "POST") 
{
    // test for bots
    if(isset($_POST['Title']) && !empty($_POST['Title'])) 
    {
        die();
    } 
    else 
    {
        // set the default return message
        $mailsent = [
            'status'=>'error'
            ,'message'=>"OOPS, we couldn't validate the information you sent"
            ,'alert'=>'danger'];

        $errors = [];

        // cleanup method
        function clean_inputs($data) 
        {
            $data = trim($data);
            $data = stripslashes($data);
            $data = htmlspecialchars($data);
            return $data;
        }    

        // clean the submitted form fields data
        $fields = array(
            'name'         => clean_inputs($_POST['name']),
            'email'     => clean_inputs($_POST['email']),
            'phone'     => clean_inputs($_POST['phone']),
            'message'    => clean_inputs($_POST['message'])
        );

        // validate the name
        if (empty($fields["name"]))
        {
            // string is empty
            $errors['name']='Please provide your name';
        } 
        else
        {
            if (!preg_match("/^[-a-zA-Z ]*$/",$fields["name"])) 
            {
                // string contains invalid characters
                $errors['name']="Your name contains characters we can't recognize";
            }

            if (strlen($fields["name"]) < 2 )
            {
                // string too short
                $errors['name']='Your name must be at least 2 characters long';
            }

            if (strlen($fields["name"]) > 50 )
            {
                // string too long
                $errors['name']='Your name must be less than 50 characters long';
            }
        }

        // validate the email address
        if (empty($fields["email"]))
        {
            // string is empty
            $errors['email']='Please provide your email address';
        } 
        else
        {
            if (!checkEmail($fields["email"])) 
            {
                // string is not a valid email
                $errors['email']='Your email address is not properly formed';
            }
        }

        /**
         * @TODO: implement Google's libphonenumber client-side
         * Can also be implemented server-side with "https://github.com/giggsey/libphonenumber-for-php"
         */
        // validate the phone number, if provided
        if (!empty($fields["phone"]))
        {
            if (strlen($fields["phone"]) < 7)
            {
                // string too short
                $errors['phone']='Your phone# must be at least 7 characters long';
            }

            if (strlen($fields["phone"]) > 30)
            {
                // string too long
                $errors['phone']='Your phone# must not be more than 30 characters long';
            }
        } 

        // validate the message
        if (empty($fields["message"]))
        {
            // string is empty
            $errors['message']='Please include a message';
        } 
        else 
        {
            if (strlen($fields["message"]) <= 4) 
            {
                // string too short
                $errors['message']='Your message must be at least 4 characters long';
            }
            if (strlen($fields["message"]) >= 2048) 
            {
                // string too long
                $errors['message']='Your message cannot be more than 2048 characters long';
            }
        }

        // if no validation errors, send the email
        if (empty($errors))
        {
            // process the submitted data
            $from = $fields['email'];
            $from_name = $fields['name'];
            $content = $fields["message"];
            $phone = $fields["phone"];
            $to = "you@yourdomain.com";
            $to_name = "Your Name";
            $time =  strftime("%Y-%m-%d %H:%M:%S", time());
            $subject = "Contact form submitted from my website.";
            $message = "You have received a new message from your website contact form at $time.<br>";
            $message .= "Here are the details:<br>";
            $message .= "Name: $from_name<br>Email: $from<br>Phone: $phone<br>Message:<br>$content";
            $message = wordwrap($message, 70,"<br>");

            // Instantiate a Mailer entity
            $mail = new PHPMailer;

            // Set the email transport type
            $mail->isSMTP(); 

            // Enable SMTP debugging - careful, this can throw off AJAX testing!
            // 0 = off (for production use)
            // 1 = client messages
            // 2 = client and server messages
            $mail->SMTPDebug = 0;
            $mail->Debugoutput = 'html'; // ask for browser-friendly debugging output

            // Set the email server account details
            $mail->Host = "your mailserver address"; // mail server
            $mail->SMTPAuth = true; // Enable SMTP authentication
            $mail->Username = 'your mailserver username'; // SMTP username
            $mail->Password = 'your mailserver password'; // SMTP password
            $mail->SMTPSecure = 'tls';// Enable TLS encryption, `ssl` also accepted
            $mail->Port = 587; // the SMTP port number: likely to be 25, 465, or 587

            // prepare the email
            $mail->setFrom($from, $from_name);
            $mail->addReplyTo($from, $from_name);
            $mail->addAddress($to, $to_name); // Add a recipient
            $mail->isHTML(true); // Set email format to HTML
            $mail->Subject = $subject;
            $mail->Body = $message;
            //$mail->AltBody = 'This is a plain-text message body'; 

            // send the email
            if($mail->send()) 
            {
                $mailsent = [
                    'status'=>'sent'
                    ,'message'=>"Thanks for contacting me. I'll get back to you ASAP!"
                    ,'alert'=>'success'];
            } 
            else
            {
                $mailsent["message"]="OOPS, something went wrong with our server. Your email could not be sent.";
            }
        }
        else 
        {
            $mailsent['message'] = $mailsent['message'] . ' &mdash; ' . 'Please correct any errors and try again.';
            // include an array of specific errors in the response
            foreach ($errors as $key => $value) 
            {
                $mailsent['errors'][$key] = $value;
            }
        }
    }
    // return the response
    echo json_encode($mailsent, JSON_FORCE_OBJECT);
}
else // this is a GET request
{
    die;
}

This script handles all of the server-side functionality for creating, validating, and sending an email based on the data submitted via the contact form.

Don’t forget to replace the bolded items with appropriate values for your SMTP mail hosting service.


7: Create the Form Submission Script

Next, navigate to the folder containing the project’s javascript assets and create a new file called contact.js, like so:

../assets/js/contact.js

Open this new empty file, and paste in the following script:

$(function() {
    "use strict";

    var contentTarget = $("div#responseMessage");

    // handle form submissions
    $("#contactForm").submit(function(e)
    {
        // prevent the form from submitting 
        e.preventDefault();

        // set a convenience variable for the form
        var form = $(this);

        // disable the submit button to prevent repeated clicks
        form.find('.submit').prop('disabled', true);

        // create and post a temporary spinner
        var spinner = '<div class="alert alert-info" role="alert" style="text-align: center;">';
        spinner += '<p class="lead"><span class="fa fa-spinner fa-spin"></span></p>';
        spinner += '</div>';
        contentTarget.html(spinner);

        // create a text string of form variables in standard URL-encoded notation
        var data = form.serialize(); 

        // set up the Ajax submission defaults
        var type = "POST";
        var url = './'+form.attr('action');
//        var url = './assets/php/contact.php';
        var dataType =  'json';

        // submit the form
        makeAjaxCall(data,type,url,form,dataType);
    });

    // makeAjaxCall function
    function makeAjaxCall(data,type,url,form,dataType)
    {
        $.ajax({
            type    : type,
            url     : url,
            dataType: dataType,
            data    : data,
            cache    : false,
        }).done(successFunction)
          .fail(failFunction)
          .always(alwaysFunction(form));
    }

    // success function (when the server responds)
    function successFunction(data,textStatus,jqXHR) 
    {
        // create a JS object from the returned string
        var jsonData = JSON.parse(data); // best practice: use native JS instead of jQuery

        // create the server response message
/*
        var message = '<div class="alert alert-'+jsonData.alert+' alert-dismissible" role="alert" style="text-align: center;">';
        message += '<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>';
        message += '<p class="lead">'+jsonData.message+'</p>';
        message += '</div>';
*/
        var message = '<div class="alert alert-'+jsonData.alert+'" role="alert" style="text-align: center;">';
        message += '<p class="lead">'+jsonData.message+'</p>';
        message += '</div>';

        // process the server response
         if (jsonData.status == 'sent')
         {
            // reset the form fields back to their defaults
            $(".async")[0].reset(); // best practice: using [0] will return the native html element, not the DOM element
        }
        else // aka. status is 'error'
        {
            /**
             * @TODO: on (re)submit, clear any old error messages that were appended to the inputs
             */
            // append error messages to matching inputs 
            $.each(jsonData.errors, function(input,error)
                {
                    var notice = '<p class="help-block text-danger"><strong><span class="fa fa-times"></span> '+error+'</strong></p>';
                    var errorTarget = "#"+input;
                    $(errorTarget).parent().append(notice);
                });
        }
        // post the server response message
        contentTarget.html(message);
    }

    // fail function (when Ajax fails to make the connection to the server)
    function failFunction(request, textStatus, errorThrown) 
    {
        // create & post the 'failed request' message
        var message = '<div class="alert alert-danger" role="alert" style="text-align: center;">';
        message += '<p class="lead">OOPS! An error occurred during your request: ' + request.status + ' ' + textStatus + ': ' + errorThrown+'</p>';
        message += '</div>';
        // post the fail message
        contentTarget.html(message);
    }

    // always function, executes regardless of success or failure
    function alwaysFunction(form) 
    {
        // re-enable the submit button
        form.find('.submit').prop('disabled', false);
    }
});

This script will trigger the browser’s native client-side validation whenever the form is submitted. On success, it then submits the form data to the server to be processed. It then posts the server’s response to the page.

Make sure this script is included in the project’s template file by adding the following just after the include for the client-side validator script, like so:

<script type="text/javascript" src="assets/js/contact.js"></script>

Fwiw, this script was built purely for demonstration purposes. In real life, your project may already have Ajax scripting in place that should be used instead. If not, feel free to adapt the above as needed.


8: Implement and Test

At this point, the coding is done and you should be ready to go. All that remains is to upload and test what you’ve done.

I purposely avoided using a specific testing regime in this tutorial because almost everyone has their own approach and toolset. Simply adjust the instructions provided to suit your preferred methodology.


Conclusion

Assuming all has gone well, you should now have in hand a simple, robust, and highly-performant contact form that uses PHPMailer() with Ajax, jQuery, and JSON.


Next Steps:

Phone# Validation

The above code checks server-side if ‘something’ was submitted for the phone# field. This should be updated to use Google’s libphonenumber to validate client-side, and this script from Giggsey as a PHP wrapper for Google’s stuff to validate server-side.

Unfortunately, Google’s JavaScript version of the script is embedded within a large utility library. Here’s a shortened JS-only version that looks like it might do the trick.

Meantime, I’ll just go with enforcing a non-blank field, then stripping out anything offensive with trim(), stripslashed(), and htmlspecialchars().

Client-side Validation

Revisit Step 4 above and sort out implementing a custom client-side validator.

Meantime, the server-side validation is strong enough to pick up anything that might slip past the browser’s native HTML5 validation.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.