Admin-triggered e-mail

Here’s something that touches on lots of topics. A ‘send e-mail’ button within an admin panel, that opens up a ThickBox window with a standalone form and wp_editor.

The metabox

This requires two functions: one hooked into ‘add_meta_boxes’ which calls the second, the metabox output function:

	add_action( 'add_meta_boxes', 'wpcx_sendmail_metabox_adder' );
	function wpcx_sendmail_metabox_adder() {
		add_meta_box( 'wpcx_sendmail_admin', 'Send a message', 'wpcx_sendmail_metabox', 'post', 'side',' high' );
	}
	function wpcx_sendmail_metabox( $post ) {
		$subject = get_post_meta( $post->ID, 'example_subject', true );
		$trigger_url = get_bloginfo( 'template_directory' ) . '/my_mods/tb_sendmail_sample.php';
		// add script args:
		$trigger_url = add_query_arg( array(
			'email_subject'=>$subject
		), $trigger_url );
		// add ThickBox args:
		$trigger_url = add_query_arg( array(
			'KeepThis'	=>1,
			'width'		=>900,
			'height'	=>740,
			'TB_iframe'	=>1
		), $trigger_url );
		?><p><a class="button thickbox" href="<?php echo $trigger_url ?>">Send Email</a></p><?php
	}

Most of the second function builds the URL for the button’s href attribute. It points to a separate script file, in this example within ‘my_mods/’ inside the theme directory, followed by several query args. The args KeepThis, width, height and TB_iframe are needed by ThickBox, and must come last in the order of query args.

Other args may also be passed to the standalone script — in this example, just to illustrate, email_subject is passed. I’m pretending it was defined elsewhere and grabbing its value from an imaginary post_meta.

Notice the button must have the class “thickbox”.

The standalone script file

Let’s take this in pieces:

<?php
	define( 'WP_USE_THEMES', false );
	require( '../../../../wp-load.php' ); // be sure this points to your 'home' directory
	if ( ! current_user_can( 'edit_posts' ) ) die();
?>

This is a pretty good way to include most built-in WordPress functions in a standalone script. Certain core WP functions and classes, e.g. image editors, are in script files that must be included separately, but this covers most of the common ones.

Notice we die immediately if the user does not have the capability to edit posts — non-logged-in users, and robots, cannot view the page.

Next, output the header, and begin the body, as if it were a normal page:

<!DOCTYPE HTML>
<html>
	<head>
		<title>E-mail trigger</title>
		<?php wp_head(); ?>
		<style type="text/css" media="screen">
			.sendmail_textinput {
				margin: 1% 8% 1% 0;
				width: 92%;
			}
		</style>
	</head>
	<body>
		<Div id="wrapper" style="margin:5%">

This script will output a form that calls itself as an action, so let’s look for POSTED data now (before outputting the form). If it passes verification and executes successfully, we’ll define a variable $message_sent. Then, we can output the form only if that variable isn’t defined.

<?php 	if ( isset( $_POST['admin_mailer_nonce'] ) ) :
			$email_subject = trim( sanitize_text_field( $_POST['email_subject'] ) );
			if ( ! strlen( $email_subject ) ) :
				$email_subject = 'Message from ' . get_bloginfo('name');
			endif;
			
			if ( ! is_email( $_REQUEST['recipient'] ) ):
					?><p style="text-align:center;color:red">Sorry, that e-mail address is invalid. Please try again.</p><?php
			
			elseif ( ! wp_verify_nonce( $_POST['admin_mailer_nonce'], __FILE__ . get_current_user_id() ):
					?><p style="text-align:center;color:red">Sorry, input couldn't be verified. Please try again.</p><?php
			
			else:
				add_action( 'phpmailer_init', 'phpmailer_message_is_html' );
				$mail_sent = wp_mail( $_REQUEST['recipient'], $email_subject , stripslashes_deep( $_POST['email_message'] ) );
				if ($mail_sent): ?>
					<div style="text-align:center;margin-top:20%">Your message was sent. You may close this window.</div>
				<?php $message_sent = true;
				else: ?>
					<p style="text-align:center;color:red">Sorry, there was a problem sending the message. Please try again.</p>
				<?php endif;
			
			endif;
		endif;

Note: I’ve chosen to provide a default message subject if it’s been left blank. The only requirement is a valid e-mail address.

A couple of security features are important, so this feature can’t get co-opted as a spam broadcaster:

  • User must be logged in and have ‘edit_post’ capability.
  • A nonce field is created and verified, making it difficult for someone to spoof the form.
  • Recipient e-mail is confirmed using is_email; if there are extra characters such as line breaks, it fails.
  • Subject is sanitized to remove line breaks and anything that might cause unwanted behavior.
  • From: header is not accessible via user input. Since the PHP mail() and WordPress wp_mail() functions accept it as part of an overall ‘headers’ argument, that’s a prime place to hack additional headers.

Our form will include a wp_editor, enabling the admin to compose an HTML-formatted message. Therefore we’ll need to define an additional function phpmailer_message_is_html() (see below), and hook it prior to calling wp_mail().

Next, the form, which outputs only if $message_sent is not defined, i.e. this page is first visited or verification/sending failed. Followed by the end of the document.

		if ( ! isset( $message_sent ) ):
			?><form method="post" action="<?php echo $_SERVER['PHP_SELF'] ?>"><?php
			wp_nonce_field( __FILE__ . get_current_user_id() , 'admin_mailer_nonce' );
			if ( is_email( $_REQUEST['recipient'] ) ):
				?><p>This message will go to: <b><?php echo $_REQUEST['recipient'] ?></b>
				<input type="hidden" name="recipient" value="<?php echo esc_attr( $_REQUEST['recipient'] ) ?>" /></p><?php
			else:
				?><p><label for="id_recipient_email">Recipient e-mail address:</label><br />
				<input type="text" name="recipient" id="id_recipient_email" class="sendmail_textinput" value="<?php echo esc_attr( $_REQUEST['recipient'] ) ?>" /></p>
				<?php
			endif;
			?><p><label for="id_subject">Subject:</label><br />
			<input type="text" name="email_subject" id="id_subject" class="sendmail_textinput" value="<?php echo esc_attr( $_REQUEST['email_subject'] ) ?>" /></p>
			<p><label for="id_email_message">Message:</label><br />
			<?php
			$msg = ( isset( $_REQUEST['email_message'] ) )? stripslashes_deep( $_REQUEST['email_message'] ) : "Hello, and thank you for your inquiry.";
			wp_editor( $msg, 'id_email_message', array(
				'wpautop'			=> false,
				'media_buttons'		=> false,
				'editor_class'		=> 'sample_adminemail',
				'textarea_name'		=> 'email_message',
				'textarea_rows'		=> 10,
				'teeny'				=> true,
				'quicktags'			=> true
				
			));
			?></p>
			<p style="text-align:center"><input type="submit" value="Send" class="button primary"></p></form>
		<?php endif; /* not $message_sent. */ ?>
	</Div>
	<?php wp_footer(); ?>
	</body>
</html>

Notice we display the recipient’s address (with hidden input) if it’s been passed by the admin metabox above (and is valid), or add a text input otherwise.

Regarding the wp_editor: I’ve chosen to disable media_buttons, and use minimal markup buttons (teeny) because e-mail clients’ HTML abilities are limited — best I think to stick to basics only.

Notice the use of $_REQUEST in certain places, because this form might receive $_GET input (first visit) or $_POST input (verify/send failed).


The phpmailer init hook

This can go in your functions.php file, or, if this is the only place it’s called, at the top or bottom of the standalone script file.

function phpmailer_message_is_html($phpmailer) {
    $phpmailer->AltBody = strip_tags($phpmailer->Body);
}

When we set the AltBody property, PHPMailer automatically formats the message as HTML with a plaintext alternate (which we simply convert by stripping the tags).