Popup Email Form Without a Plugin

WordPress popup emailer tutorial

Learn how to create a popup email contact form on your WordPress site without using a plugin. We’ll include a GDPR/privacy consent checkbox, and we’ll rinse messages through Spam Shield (an online anti-spam filter) to block junk messages.

Before you start, make sure you’re using a child theme so you can edit functions.php. You’ll also need a free Spam Shield account, which will give you an API Key.

Break Down the Problem

There’s quite a bit of code in this tutorial. The logic starts with some PHP code, then hands-over to JavaScript, and goes back to PHP code via AJAX to send the email. Instead of tackling the whole thing in one go, we need to break it down into smaller chunks…

  • We want to create a button that can trigger a modal popup “send us an email” form. We can add the button with a simple shortcode.
  • The modal popup needs to work well on mobile and desktop/laptop – it needs to be responsive.
  • Spam Shield is an online service, so all we need to do there is call an API from WordPress.
  • We need a bit of code in there to stop bots from battering the server with hundreds/thousands of email submissions every second – flood protection.

Scaffold the Project

Start by creating a folder in your custom child theme called “wpt-popup-mailer”. In this folder, create empty text files called “wpt-popup-mailer.css” and “wpt-popup-mailer.js”. Grab one of Sam Herbert’s loading spinners and save it in the “wpt-popup-mailer” folder as “spinner.svg”.

Back in your child theme’s main folder, create a file called wpt-popup-mailer.php and paste the following into it:

<?php

/**
 * WP Tutorials : Popup mailer with anti-spam filter (WPTEMLR)
 *
 * https://wp-tutorials.tech/add-functionality/popup-email-signup-for-wordpress/
 */

defined('WPINC') || die();

const WPTEMLR_API_REQUEST_TIMEOUT = 5;
const WPTEMLR_ACTION_SEND_EMAIL = 'wptemlr_send_email';
const WPTEMLR_ENABLE_FLOOD_PROTECTION = true;
const FLOOD_PROTECTION_DELAY_SECS = 10; // seconds

// Repalce this with your Spam Shield API Key
const WPTEMLR_SPAM_SHIELD_API_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';

/**
 * This registers our Ajax handler, so when we detect the "wptemlr_send_email"
 * action, WordPress will call the wptemlr_send_email() function for us.
 * One of these actions applies to logged-in users, and the "norpiv" action
 * applies to non-logged-in users.
 */
add_action('wp_ajax_' . WPTEMLR_ACTION_SEND_EMAIL, 'wptemlr_send_email');
add_action('wp_ajax_nopriv_' . WPTEMLR_ACTION_SEND_EMAIL, 'wptemlr_send_email');

/**
 * Enqueue the JavaScript and CSS to render the popup emailer.
 */
function wptemlr_enqueue_assets() {
	global $wptemlr_have_assets_been_enqueued;

	if (is_null($wptemlr_have_assets_been_enqueued)) {
		$base_url = get_stylesheet_directory_uri();
		$version = wp_get_theme()->get('Version');

		wp_enqueue_style('wptemlr', $base_url . '/wpt-popup-mailer/wpt-popup-mailer.css', null, $version);
		wp_enqueue_script('wptemlr', $base_url . '/wpt-popup-mailer/wpt-popup-mailer.js', array('jquery'), $version);

		$wptemlr_have_assets_been_enqueued = true;
	}
}

/**
 * Render the button that displays the popup when clicked.
 */
function wptemlr_do_shortcode_popup_mailer($atts) {
	$html = '';

	if (is_admin() || wp_doing_ajax()) {
		// Don't do anything.
	} else {
		// TODO: Render the button here...
	}

	return $html;
}
add_shortcode('popup_mailer', 'wptemlr_do_shortcode_popup_mailer');

/**
 * A useful little function to get the browser's IP address,
 * even if we're behind a proxy.
 */
function wptemlr_get_browser_ip() {
	$browser_ip = '';

	// Scan these members of $_SERVER looking for the
	// visitor's IP address.
	$server_vars = array(
		'HTTP_X_REAL_IP',
		'HTTP_CLIENT_IP',
		'HTTP_X_FORWARDED_FOR',
		'REMOTE_ADDR',
	);

	foreach ($server_vars as $server_var) {
		if (!array_key_exists($server_var, $_SERVER)) {
			// The server variable is not specified.
		} elseif (empty($browser_ip = filter_var($_SERVER[$server_var], FILTER_VALIDATE_IP))) {
			// The IP address is not valid or is empty.
		} else {
			// We've found a valid IP address, so break out of the foreach
			// loop now.
			break;
		}
	}

	return $browser_ip;
}

/**
 * A reusable function to call the Spam Shield API.
 * If the API call fails for some reason, the function returns null.
 * For information about what to put into the $request array, see the official
 * Spam Shield docs: https://spamshield.cloud/doc/spam-shield-api/
 */
function wptemlr_call_spam_shield_api(string $path, string $method = 'GET', $request = []) {
	$response = null;

	$args = array(
		'method' => $method,
		'body' => $request,
		'sslverify' => true,
		'timeout' => WPTEMLR_API_REQUEST_TIMEOUT,
		'headers' => array(
			'X-ApiKey' => WPTEMLR_SPAM_SHIELD_API_KEY,
			'X-Caller' => site_url('/'),
		),
	);

	$url = sprintf('https://api.spamshield.cloud/%s', $path);

	if (empty($raw_response = wp_remote_post($url, $args))) {
		error_log(__FUNCTION__ . ': Empty response from API server: ' . $url);
	} elseif (is_wp_error($raw_response)) {
		error_log(__FUNCTION__ . ': ' . $raw_response->get_error_message());
	} elseif (empty($response = json_decode(wp_remote_retrieve_body($raw_response), true))) {
		error_log(__FUNCTION__ . ' : Bad/empty response from the API server.');
	} elseif (!array_key_exists('queryTime', $response)) {
		error_log(__FUNCTION__ . ' : Missing queryTime in response body.');
		$response = null;
	} else {
		// OK
	}

	return $response;
}

/**
 * Validate user inputs (POST data), send the email to the admin email address
 * and return a message to the browser.
 */
function wptemlr_send_email() {
	// This is what we send back to the browser as JSON.
	$response = array(
		'message' => '',
		'isSent' => false,
	);

	// TODO: Capture the user's name, email and message.
	// Send the email to the site admin using wp_mail()

	// Send the response back to the browser as JSON.
	wp_send_json(
		$response,
		200// HTTP Response code
	);
}

The functions in here are either utility-functions, or placeholders that we’ll fill-in later. The interesting bits are commented.

Next, open your child theme’s functions.php and add the following couple of lines:

// WPT Mailer Popup.
require_once dirname(__FILE__) . '/wpt-popup-mailer.php';

That’s the basics in-place, so save everything and make sure your site is not broken.

Add the Button

To get the main button working, we need to add code into wptemlr_do_shortcode_popup_mailer() to render the button’s HTML. The button will be a simple <div><button>...</button></div> arrangement, and we’ll attach some parameters in the data-mailer-args="{...}" property – we can pick these up in the JavaScript code later.

Open wpt-popup-mailer.php and replace the contents of the wptemlr_do_shortcode_popup_mailer() function with the following:

function wptemlr_do_shortcode_popup_mailer($atts) {
	$html = '';

	if (is_admin() || wp_doing_ajax()) {
		// Don't do anything.
	} else {
		wptemlr_enqueue_assets();

		$args = shortcode_atts(
			array(
				'button' => 'Send us an email',
				'heading' => 'Send us an email',
				'send' => 'Send',
			),
			$atts
		);

		$base_url = get_stylesheet_directory_uri();

		$frontend_args = array(
			'action' => WPTEMLR_ACTION_SEND_EMAIL,
			'ajaxUrl' => admin_url('admin-ajax.php'),
			'heading' => $args['heading'],
			'sendButtonText' => $args['send'],
			'fromNameLabel' => 'Your name',
			'fromEmailLabel' => 'Your email',
			'privacyConsentLabel' => 'I agree to the handling of my data by this website',
			'messageBodyPlaceholder' => 'Your message',
			'dismissWindowHtml' => '<i class="fas fa-times-circle"></i>',
			'spinnerUrl' => $base_url . '/wpt-popup-mailer/spinner.svg',
		);

		$html .= sprintf(
			'<div class="wptemlr-popup-button" data-mailer-args="%s">',
			esc_attr(json_encode($frontend_args))
		);

		// Customise the button's content with whatever you want.
		$html .= sprintf(
			'<button class="button"><i class="fas fa-at"></i> %s</button>',
			$args['button']
		);

		$html .= '</div>'; // .wptemlr-popup-button
	}

	return $html;
}

Save your changes then add the shortcode to some content to make sure it works. Notice how we’ve created a couple of optional parameters, so you can specify the label for the button and some labels for the popup mailer itself.

info When building WordPress sites, being able to create custom shortcodes like this is a powerful tool. It’s a way to inject specific functionality into your site without installing a big/bloated plugin that doesn’t quite do what you want.

Now we need to create the pop in the browser with JavaScript & CSS.

Popup email button shortcode
Popup mailer shortcode

Create the Mailer Popup

The JavaScript code works like this…

  • Show the popup mailer when one of our [data-mailer-args] buttons is clicked.
  • When the user clicks “Send”, create a request object with the user’s name, email and message.
  • Send the request object to the server as an Ajax POST using jQuery.post().
  • Check the response from the server – maybe show an error message to the user?
  • If the email was sent successfully, hide the modal popup form.

Open wpt-popup-mailer/wpt-popup-mailer.js and paste the following lump into it:

/**
 * WP Tutorials : Popup mailer with anti-spam filter (WPTEMLR)
 *
 * https://wp-tutorials.tech/add-functionality/popup-email-signup-for-wordpress/
 */

(function($) {
	'use strict';
	$(window).on('load', function() {
		console.log('WPT Emailer : load');

		var visibleEmailContainer;

		// Find all instances of our mailer popup buttons and listen for the
		// "click" event.
		$('[data-mailer-args] button').click(function(event) {
			const container = $(this).closest('[data-mailer-args]');
			showEmailer(container);
		});

		// Pass in an element that's got data-mailer-args="{...}", ehich defines
		// the popup mailer's properties.
		function showEmailer(container) {
			// If there's already a popup mailer visible, close it now, so we
			// don't end up with multiple popup mailers in the DOM.
			if (visibleEmailContainer) {
				closeEmailer();
			}

			visibleEmailContainer = container;

			// Get the $frontend_args that we passed forward from the back-end.
			const args = $(container).data('mailer-args');

			// Create an overlay DIV that will be display:fixed; and not visible (yet).
			const overlay = $('<div class="wptemlr-overlay" style="display:none;"></div>');

			// Create the elements that represent the popup itself. Simple HTML.
			overlay.append($(`
<div class="wptemlr-emailer">
	<div class="wptemlr-title-bar">
		<h2>${args.heading}</h2>
		<div class="wptemlr-dismiss">${args.dismissWindowHtml}</div>
	</div>
	<div class="wptemlr-meta-fields">
		<p class="wptemlr-row">
			<label for="emailer_from_name">${args.fromNameLabel}</label>
			<input id="emailer_from_name" type="text" />
		</p>
		<p class="wptemlr-row">
			<label for="emailer_from_email">${args.fromEmailLabel}</label>
			<input id="emailer_from_email" type="email" />
		</p>
	</div>
	<p class="wptemlr-row">
		<textarea id="email_message_body" placeholder="${args.messageBodyPlaceholder}"></textarea>
	</p>
	<p class="wptemlr-row wptemlr-checkbox">
		<input id="email_privacy_consent" type="checkbox" />
		<label for="email_privacy_consent">${args.privacyConsentLabel}</label>
	</p>
	<button class="wptemlr-send">${args.sendButtonText}</button>
	<div class="wptemlr-spinner" style="display:none;"><img src="${args.spinnerUrl}" /></div>
</div>`));

			// When the Dismiss/Close button is clicked, close the mailer.
			overlay.find('.wptemlr-dismiss').click(function(event) {
				closeEmailer();
			});

			overlay.find('.wptemlr-send').click(function(event) {
				sendEmailNow();
			});

			// Add the overlay to the end of our page's body element.
			$('body').append(overlay);

			// ...and then fade it in.
			overlay.fadeIn();
		}

		function sendEmailNow() {
			if (visibleEmailContainer) {
				const container = visibleEmailContainer;
				const args = $(container).data('mailer-args');
				const spinner = $('.wptemlr-spinner');

				var consent = '';
				if ($('#email_privacy_consent').prop('checked')) {
					consent = $('label[for="email_privacy_consent"]').text();
				}

				// Create our AJax request object,
				const request = {
					action: args.action,
					senderName: $('#emailer_from_name').val(),
					senderEmail: $('#emailer_from_email').val(),
					messageBody: $('#email_message_body').val(),
					privacyConsent: consent
				};

				// Uncomment this if you want to see the request that we're
				// POSTing to the server.
				// console.log(request);

				spinner.show();

				// Ajax POST the request to the server and wait for it to respond.
				$.post(args.ajaxUrl, request)
					.done((response) => {
						// This is only called when the Ajax call returns
						// HTTP Response 200.

						// Uncomment this to see the raw resposne from the server.
						// console.log(response);

						if (response.message) {
							alert(response.message);
						}

						if (response.isSent) {
							closeEmailer();
						}
					})
					.always(() => {
						// THis is called whether the Ajax call passes or fails.
						$(spinner).fadeOut();
					});
			}
		}

		// If the mailer is visible, fade it out and then remove it from the DOM.
		function closeEmailer() {
			if (visibleEmailContainer) {
				const overlay = $('.wptemlr-overlay');
				$(overlay).fadeOut(400, () => {
					$(overlay).remove();
				});

				visibleEmailContainer = null;
			}
		}
	});
})(jQuery);

We also need some CSS to support the popup. Paste the following into “wpt-popup-mailer/wpt-popup-mailer.css“. It should be easy enough to customise to your requirements.

/**
 * WP Tutorials : Popup mailer with anti-spam filter (WPTEMLR)
 *
 * https://wp-tutorials.tech/add-functionality/popup-email-signup-for-wordpress/
 */

.wptemlr-popup-button button {
	display: block;
	margin-left: auto;
	margin-right: auto;
	padding: 1em 1.5em;
	font-size: 16pt;
	border-radius: 0.5em;
}

.wptemlr-popup-button button .fas {
	margin-right: 0.5em;
}

.wptemlr-overlay {
	position: fixed;
	left: 0;
	top: 0;
	width: 100%;
	height: 100%;
	background-color: #fffc;
	z-index: 3000;
}

.wptemlr-emailer {
	background-color: purple;
	color: white;
	border-radius: 1em;
	box-shadow: 0 0 2em #0008;
	overflow: hidden;
	position: absolute;
	left: 50%;
	top: 50%;
	transform: translate(-50%, -50%);
	width: 90%;
	max-width: 40em;
}

.wptemlr-title-bar {
	display: flex;
	flex-direction: row;
	align-items: center;
	background-color: #0004;
	margin-bottom: 1rem;
}

.wptemlr-emailer h2 {
	color: white;
	font-size: 18pt;
	margin-bottom: 1.5rem;
	flex-grow: 1;
	margin: 0;
	padding: 1.8rem;
}

.wptemlr-dismiss {
	/* background-color: pink; */
	padding: 0 1.8rem;
	font-size: 24pt;
	cursor: pointer;
	transition: 0.3s text-shadow;
}

.wptemlr-dismiss:hover {
	text-shadow: 0 0 0.5em white;
}

.wptemlr-emailer .wptemlr-row {
	padding: 0 2rem 2rem 2rem;
	margin: 0;
}

.wptemlr-emailer label,
.wptemlr-emailer input[type="text"],
.wptemlr-emailer input[type="email"],
.wptemlr-emailer textarea {
	display: block;
	width: 100%;
	font-size: 16pt;
}

.wptemlr-emailer input {
	padding: 0.5em;
}

.wptemlr-emailer .wptemlr-checkbox {
	display: flex;
	align-items: flex-start;
	gap: 0.5rem;
}

.wptemlr-emailer .wptemlr-checkbox input[type="checkbox"] {
	margin-top: 0.8em;
}

.wptemlr-send {
	font-size: 18pt;
	display: block;
	width: 100%;
	padding: 1.5rem;
}

.wptemlr-spinner {
	position: absolute;
	left: 0;
	bottom: 0;
	width: 100%;
	padding: 3rem;
	background-image: linear-gradient(to bottom, transparent, black 125%);
	text-align: center;
}

@media(min-width: 768px) {
	.wptemlr-meta-fields {
		display: flex;
		flex-direction: row;
	}

	.wptemlr-meta-fields .wptemlr-row {
		flex-grow: 1;
	}
}

Save all that and test it. When you click the button, you should get a popup window. Result!

Capture the AJAX POST and Send the Email

The final stage is to add the PHP code that responds to the AJAX POST when the user clicks the “Send” button. We’ve already registered the AJAX handler that’ll get called when WordPress detects our wptemlr_send_email AJAX action. All we need to do is validate the $_POST[...] fields, then return either success or fail to the browser. The new function is really just a series of if() statements, followed by a call to wp_mail(), which sends the actual email.

Open wpt-popup-mailer.php and replace the contents of wptemlr_send_email() with the following:

function wptemlr_send_email() {
	// This is what we send back to the browser as JSON.
	$response = array(
		'message' => '',
		'isSent' => false,
	);

	$browser_ip = wptemlr_get_browser_ip();

	// Extract and sanitise the POSTed form data.
	$browser_key = !empty($browser_ip) ? ('wptemlr_browser_' . $browser_ip) : '';
	$sender_name = array_key_exists('senderName', $_POST) ? sanitize_text_field($_POST['senderName']) : '';
	$sender_email = array_key_exists('senderEmail', $_POST) ? sanitize_email($_POST['senderEmail']) : '';
	$message_body = array_key_exists('messageBody', $_POST) ? sanitize_textarea_field($_POST['messageBody']) : '';
	$privacy_consent = array_key_exists('privacyConsent', $_POST) ? sanitize_textarea_field($_POST['privacyConsent']) : '';

	if (empty($browser_key)) {
		$response['message'] = 'Invalid browser IP address';
	} elseif (WPTEMLR_ENABLE_FLOOD_PROTECTION && !empty(get_transient($browser_key))) {
		$response['message'] = 'You already sent an email recently';
	} elseif (empty($privacy_consent)) {
		$response['message'] = 'You have not consented to the privacy requirements';
	} elseif (empty($sender_name)) {
		$response['message'] = 'You have not entered your name';
	} elseif (empty($sender_email)) {
		$response['message'] = 'Invalid email address';
	} elseif (empty($message_body)) {
		$response['message'] = 'You have not typed a message';
	} else {
		// Reference: https://spamshield.cloud/doc/spam-shield-api/check-for-spam/
		$spam_shield_request = array(
			'ip' => $browser_ip,
			'message' => $message_body,
			'fields' => array(
				'sender-name' => $sender_name,
				'sender-email' => $sender_email,
			),
		);

		// Check message against Spam Shield
		$spam_shield_result = wptemlr_call_spam_shield_api('spam/check', 'POST', $spam_shield_request);

		if (empty($spam_shield_result)) {
			$response['message'] = 'Failed to check the message against our spam filter';
		} elseif ($spam_shield_result['result']['isSpam']) {
			$response['message'] = 'Sorry, but your message looks like spam';
		} else {
			$email_subject = 'Enquiry from ' . get_option('blogname');
			$email_body = 'From: ' . $sender_name . ' ' . $sender_email . PHP_EOL;
			$email_body .= 'Privacy: ' . $privacy_consent . PHP_EOL;
			$email_body .= 'Message:' . PHP_EOL . $message_body . PHP_EOL;
			$email_body .= 'Checked by Spam Shield' . PHP_EOL; // <<< You can get rid of this line if you want.

			// Send an email to the site admin now.
			$response['isSent'] = wp_mail(
				get_option('admin_email'),
				$email_subject,
				$email_body
			);
		}
	}

	if ($response['isSent']) {
		$response['message'] = 'Thank you for your email';

		if (WPTEMLR_ENABLE_FLOOD_PROTECTION && !empty(get_transient($browser_key))) {
			set_transient($browser_key, '1', FLOOD_PROTECTION_DELAY_SECS);
		}
	} elseif (!empty($response['message'])) {
		// We've already set an error message.
	} else {
		$response['message'] = 'Unknown internal error';
	}

	// Send the response back to the browser as JSON.
	wp_send_json(
		$response,
		200// HTTP Response code
	);
}

Save the final changes and… that’s it. Everything is in place.

Flood Protection: We create a unique $browser_key based on the user’s IP address. When an email is sent, we set a transient – a short-lived object that expires after 10 seconds (FLOOD_PROTECTION_DELAY_SECS). If a user tries to send an email, but a transient with their IP address already exists, they won’t be allowed to send it.

Oh, and before anybody leaves a comment saying that wptemlr_send_email() can be written in half the lines of code… I’ve written it like this to make it very easy to read – and not just because this is a tutorial. The top half of the function is sanity-checking POST inputs, so it pays to be absolutely clear about what the code is doing. It makes it much easier to spot bugs and potential security holes.

That’s it. Happy popping-up and emailing! 😎 👍

Like This Tutorial?

Let us know

WordPress plugins for developers

Leave a comment