This website uses cookies to allow us to see how the site is used. If you continue to use this site, we assume that you are okay with this. If you want to use the sites without cookies, please see our privacy policy.

Use a custom post type to send notifications as Html emails to users by the user role

When I set WP Snippets til Christmas up this year I wanted to send an email to all the previous contributors just to notify them this was going to be a thing again for 2019.

As the previous contributors were already registered users and rather than subscribe to a third-party service I wanted to see how I could do this with WordPress core, can’t be that difficult right? So with a little bit of thought and rough testing, I started to formulate a plan.

I needed to think about all the pitfalls and what I wanted to do. First of all, I needed a custom post type but, how was I going to trigger the send? I couldn’t do it on the save_post action hook otherwise the post would get emailed every time it was updated. I would also need to be careful about sending on the save_post with multiple users too, waiting for a post to save after trying to send 300 users an email could take forever! How could I stagger the sending to avoid getting blocked or banned by the server? Would I need to set some post meta on the post as a ‘sent email notification’ marker just so I didn’t send it again? Perhaps I could use the WP Cron to schedule an event to check all Published posts that hadn’t been already been sent by email…

…so with all this in mind, I ended up doing it like so, first all I needed a custom post type but take it out of public visibility.

/**
 * Register a custom post type to handle all the emails notifications.
 * The labels and arguments here might be overkill but I used https://generatewp.com/post-type/
 * to generate the code so you get what is generated.
 * 
 * @source GenerateWP https://generatewp.com/post-type/
 * 
 */
function email_notification() {

	$labels = array(
		'name'                  => _x( 'Emails', 'Post Type General Name', 'wpstx-email-notification-with-post-type' ),
		'singular_name'         => _x( 'Email', 'Post Type Singular Name', 'wpstx-email-notification-with-post-type' ),
		'menu_name'             => __( 'Email Users', 'wpstx-email-notification-with-post-type' ),
		'name_admin_bar'        => __( 'Email', 'wpstx-email-notification-with-post-type' ),
		'archives'              => __( 'Item Archives', 'wpstx-email-notification-with-post-type' ),
		'attributes'            => __( 'Item Attributes', 'wpstx-email-notification-with-post-type' ),
		'parent_item_colon'     => __( 'Parent Item:', 'wpstx-email-notification-with-post-type' ),
		'all_items'             => __( 'All Items', 'wpstx-email-notification-with-post-type' ),
		'add_new_item'          => __( 'Add New Item', 'wpstx-email-notification-with-post-type' ),
		'add_new'               => __( 'Add New', 'wpstx-email-notification-with-post-type' ),
		'new_item'              => __( 'New Item', 'wpstx-email-notification-with-post-type' ),
		'edit_item'             => __( 'Edit Item', 'wpstx-email-notification-with-post-type' ),
		'update_item'           => __( 'Update Item', 'wpstx-email-notification-with-post-type' ),
		'view_item'             => __( 'View Item', 'wpstx-email-notification-with-post-type' ),
		'view_items'            => __( 'View Items', 'wpstx-email-notification-with-post-type' ),
		'search_items'          => __( 'Search Item', 'wpstx-email-notification-with-post-type' ),
		'not_found'             => __( 'Not found', 'wpstx-email-notification-with-post-type' ),
		'not_found_in_trash'    => __( 'Not found in Trash', 'wpstx-email-notification-with-post-type' ),
		'featured_image'        => __( 'Featured Image', 'wpstx-email-notification-with-post-type' ),
		'set_featured_image'    => __( 'Set featured image', 'wpstx-email-notification-with-post-type' ),
		'remove_featured_image' => __( 'Remove featured image', 'wpstx-email-notification-with-post-type' ),
		'use_featured_image'    => __( 'Use as featured image', 'wpstx-email-notification-with-post-type' ),
		'insert_into_item'      => __( 'Insert into item', 'wpstx-email-notification-with-post-type' ),
		'uploaded_to_this_item' => __( 'Uploaded to this item', 'wpstx-email-notification-with-post-type' ),
		'items_list'            => __( 'Items list', 'wpstx-email-notification-with-post-type' ),
		'items_list_navigation' => __( 'Items list navigation', 'wpstx-email-notification-with-post-type' ),
		'filter_items_list'     => __( 'Filter items list', 'wpstx-email-notification-with-post-type' ),
	);
	$args = array(
		'label'                 => __( 'Email', 'wpstx-email-notification-with-post-type' ),
		'description'           => __( 'Email notification system', 'wpstx-email-notification-with-post-type' ),
		'labels'                => $labels,
		'supports'              => array( 'title', 'editor' ),
		'hierarchical'          => false,
		'public'                => false,
		'show_ui'               => true,
		'show_in_menu'          => true,
		'menu_position'         => 25,
		'menu_icon'             => 'dashicons-email',
		'show_in_admin_bar'     => true,
		'show_in_nav_menus'     => false,
		'can_export'            => false,
		'has_archive'           => false,
		'exclude_from_search'   => true,
		'publicly_queryable'    => false,
		'capability_type'       => 'email_notification', // our own cap type
		'map_meta_cap'        	=> true,
		'capability'			=> array(
			'publish_posts'     => 'manage_email_notifications', // the capability to add to the role
		)
	);
	register_post_type( 'email_notification', $args );

}
add_action( 'init', 'email_notification' );

Add the capabilities to the Administrator so only admin can use this functionality, other roles can be added but to keep things simple we are only interested in Administrators for now.

/**
 * Add the capabilities to use this email system to the administrator only
 */
function wpstx_add_role_caps() {

	// The roles to manage this custom post type, other roles can been added to this array but we are only allowing this for Administrators
	$roles = array('administrator');
	
	// Loop through each role as it's an array and assign capabilities
	foreach($roles as $the_role) { 
		$role = get_role($the_role);
		$role->add_cap( 'read' );
		$role->add_cap( 'manage_email_notifications' );
		$role->add_cap( 'read_email_notification');
		$role->add_cap( 'read_private_email_notifications' );
		$role->add_cap( 'edit_email_notification' );
		$role->add_cap( 'edit_email_notifications' );
		$role->add_cap( 'edit_others_email_notifications' );
		$role->add_cap( 'edit_published_email_notifications' );
		$role->add_cap( 'publish_email_notifications' );
		$role->add_cap( 'delete_others_email_notifications' );
		$role->add_cap( 'delete_private_email_notifications' );
		$role->add_cap( 'delete_published_email_notifications' );

	}
}
add_action('admin_init','wpstx_add_role_caps');

Just for some additional and obvious user feedback, I wanted to change the “Publish” button to “Send” so here’s a very simple, fairly seasoned filter hook to do just that and only on my custom post type

/**
 * Change the button that says "Publish" to "Send" on the post type
 * "email_notifications" to make the UI look more obvious that the
 * user is about to send something.
 * 
 * @source https://developer.wordpress.org/reference/hooks/gettext/
 * 
 * @param 	string 	$translation 	Translated text
 * @param 	string 	$text 	Text to translate
 * @param 	string 	$domain 	Textdomain
 * @return 	string 	$translation 	Translated text
 * 
 */
function wpstx_change_publish_to_send( $translation, $text, $domain ) {
	if ( 'email_notification' == get_post_type() && ($text == 'Publish') ) {
		$translation = 'Send';
	}
	return $translation;
}
add_filter( 'gettext', 'wpstx_change_publish_to_send', 10, 3 );

I then needed to somehow save some post meta to the post that I could use later to find only published posts that hadn’t previously been sent, this can be done on the post_status auto-draft which is initiated as soon as a new post is created, I’ve also added a few other checks to stop the running of the function if its not needed.

/**
 * Save some post meta with the email_notification post type
 * on auto-draft or when the post if first initiated
 * 
 * @source https://developer.wordpress.org/reference/hooks/save_post/
 * 
 * @param 	int 		$post_id 	Auto generated post ID
 * @param 	object 		$post 		WP post object
 * @param 	boolean 	$update 	Whether this is an existing post being updated or not
 */
function wpstx_save_notification_status( $post_id, $post, $update = false ) {

	if ( ! current_user_can( 'edit_post', $post_id ) || $update) {
        return; // stop if the current user does not have capabilities to edit this or post is an update 
	}
	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return; // stop if the current post is auto saving
    }
	if( $post->post_type === 'email_notification' || $post->post_status === 'auto-draft' ) {
		add_post_meta($post_id, 'notification_sent', '0'); // add post meta that we will use in the wp cron later
	}

}
add_action( 'save_post', 'wpstx_save_notification_status', 10, 3 );

I also wanted to set the role for the email so that I could effectively segment different emails to different roles, for this I needed to add a custom meta box. This would require a function to add the meta box, a callback function renders the meta box and another function to save the post meta for that meta box, here’s all the code for just for that.

/**
 * Register some custom meta to select user role to email to.
 * 
 * @source https://developer.wordpress.org/reference/hooks/add_meta_boxes/
 */
function wpstx_register_meta_boxes() {
    add_meta_box( 'user-role', __( 'Role to Email', 'wpstx-email-notification-with-post-type' ), 'wpstx_my_display_callback', 'email_notification' );
}
add_action( 'add_meta_boxes', 'wpstx_register_meta_boxes' );

/**
 * Render the meta box to display in the backend.
 * We are also getting all the current WordPress roles with the global $wp_roles
 * here so we can isolate what role to set the email notification to send to
 *
 * @param	object	$post	Current post object.
 */
function wpstx_my_display_callback( $post ) {
	global $wp_roles;
	$roles = $wp_roles->roles;

	// for security when saving the data later
	wp_nonce_field( 'role_to_email', 'role_to_email_nonce' ); 
	// get the current value to check against
	$role_to_email = get_post_meta( $post->ID, '_role_to_email', true ); 

	?>
	<!-- set a label with translation just because it's good practice -->
	<label for='role_to_email'>
		<?php _e( 'Select the user role to email', 'wpstx-email-notification-with-post-type' ); ?>
	</label>
	<!-- add the name key '_role_to_email' for indexing the meta -->
	<select name='_role_to_email'>
		<!-- set a default of selected but disabled on render if the an option hasn't already been selected -->
		<option value='' selected disabled>Select Role</option>
		<?php foreach($roles as $k => $role) { ?>
			<!-- the currently selected option using the wp core function "selected" https://developer.wordpress.org/reference/functions/selected/ -->
			<option value='<?php echo esc_attr($k); ?>' <?php selected( $role_to_email, $k, true ); ?>><?php echo esc_attr($role['name']); ?></option>
		<?php } ?>

	</select>

	<?php
}


/**
 * Save meta box content.
 *
 * @param 	int 	$post_id 	Post ID
 * 
 * @source https://developer.wordpress.org/reference/hooks/save_post/
 */
function wpstx_save_meta_box( $post_id ) {
	if ( ! isset( $_POST['role_to_email_nonce'] ) ) {
        return; // stop if the nounce is not set
    }
    if ( ! wp_verify_nonce( $_POST['role_to_email_nonce'], 'role_to_email' ) ) {
        return; // stop if the nounce doesn't varify
    }
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return; // stop if auto saving the post
    }
    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return; // stop if the current user does not have capabilities to edit this or post
    }
    if ( isset( $_POST['post_type'] ) && 'email_notification' === $_POST['post_type'] ) {
		// only happens if the post type is "email_notification" 
		if(isset($_POST['_role_to_email'])) {
			// only happens if name key is set
			update_post_meta($post_id, '_role_to_email', esc_attr($_POST['_role_to_email']));
		}
    }
}
add_action( 'save_post', 'wpstx_save_meta_box', 10, 1 );

In the above code, a role is set so, I needed a helper function to get all the users for a particular role that will be needed during the sending of the email later in a WP Cron event, this function returns all the users from a particular role if the role is set.

/**
 * A simple helper function to get all users by role if the parameter is set
 * 
 * @param	string/array	$roles	Key for role set in WordPress roles
 */
function wpstx_get_users($roles = '') {
    $args = array();
    if($roles) {
        $args['role'] = $roles;
    }
    $users = get_users($args);
    return $users;
}

So next up comes the WP Cron event itself which is a fair chunk of code. I’ve heavily commented on it line by line to describe what is going on.

It describes what happens after an email notification is set to “Publish” or when “Send” is clicked in the UI for the email notification.

The next custom WP Cron event will get all published posts that do not have a custom meta “notification_sent” equal to “1” then look for all users of a particular role, then set up the email from the post content as a HTML email, send the email, wait 10 seconds and continue to the next user to send another email.

/**
 * This function takes care of all the sending of emails that
 * are hooked to our custom action hook scheduled by the wp cron
 */
function wpstx_send_emails() {

	// get all the posts that are set to published and that
	// do not have a meta key 'notification_sent' set to '1'
	// we will set this to '1' after the email notification is sent
	// this will ensure that we don't send it again
	// https://developer.wordpress.org/reference/functions/get_posts/
	$emails = get_posts(
		array(
			'post_type' => 'email_notification',
			'post_status' => 'publish',
			'meta_key' => 'notification_sent',
			'meta_value' => '1',
			'meta_compare' => '!='
		)
	);

	// $emails will return and array of objects so we'll use a
	// foreach to loop the email notifications ready to send
	foreach ($emails as $send_email) {

		// get role set for sending the email notification 
		$role = get_post_meta($send_email->ID, '_role_to_email', true);
		// if $role is set pass it to helper function 'wpstx_get_users()'
		if(isset($role) && $role != '') {
			$users = wpstx_get_users($role);
		} else {
			$users = wpstx_get_users();
		}

		// now we have all the users to send an email notification to
		// and we can use the $send_email post object to setup some parameters
		foreach ($users as $user) {

			// get the post title by ID and set the subject line for the email
			// https://developer.wordpress.org/reference/functions/get_the_title/
			$subject = get_the_title( $send_email->ID );
			// we only need the post_content of the post and we will filter it using the_content
			// https://developer.wordpress.org/reference/functions/get_post_field/
			// https://developer.wordpress.org/reference/hooks/the_content/
			$content = apply_filters('the_content', get_post_field('post_content', $send_email->ID));
			// load the $message variable with a personalised "Hey username" 
			$message = '<p>Hey ' . $user->user_login . '</p>';
			// concatinate the $content to the end of the "Hey..." $message variable
			$message .= $content;
			// as we are sending html we need to set an email header as an array
			$headers = array('Content-Type: text/html; charset=UTF-8');
			// Send the email to the user with the subject, message and headers.
			// https://developer.wordpress.org/reference/functions/wp_mail/
			wp_mail($user->user_email, $subject, $message, $headers);
			// IMPORTANT!!
			// Your host will have a limit on the amount of eamils you can send per minute/hour from your
			// server so it is super important to set a delay between each emails sent, otherwise
			// you could get your hosting account temporarily blocked on banded for spammy looking activity.
			// I'm simpy using a 10 second delay between each sending of an email, you might need to increase
			// this depending on the amount of users you have to send to
			sleep(10);

		}

		// Once the email has been sent now set the post with a post meta
		// key 'notification_sent' equal to '1' this will ensure we never
		// pick it up as published post to send again as set in the
		// WordPress function get_posts() above.
		update_post_meta($send_email->ID, 'notification_sent', '1');

	}

}
add_action( 'wpstx_check_scheduled_emails', 'wpstx_send_emails' );

/**
 * This sets the next event time for our action hook
 * "wpstx_check_scheduled_emails" that fires "wpstx_send_emails"
 */
wp_next_scheduled( 'wpstx_check_scheduled_emails' );

/**
 * This checks if the event is set or past and sets a new one if it has past
 * or at least that's the way I interpret the way it works :)
 */
if ( ! wp_next_scheduled( 'wpstx_check_scheduled_emails' ) ) {
	// now we can tap in to the WordPress cron schedule, there is a choice of
	// ‘hourly’, ‘twicedaily’, and ‘daily’
	// you can set your own custom time intervals as well 
	// https://developer.wordpress.org/plugins/cron/understanding-wp-cron-scheduling/
	// in this case we are firing the custom action hook "wpstx_check_scheduled_emails"
	// on the next hourly interval
    wp_schedule_event( time(), 'hourly', 'wpstx_check_scheduled_emails' );
}

The benefit of doing it this way free’s up the backend processing time and user’s interaction by pushing all the heavy work to a background cron event, you don’t have to click a button and wait for the process to finish which could be a while, for instance, if you had 1000 users with a 10 second delay between sending each email 🙂

And here is a final job to remove the schedule, this is just good practice if it is a plugin for instance and you wanted to remove the event after deactivation of the plugin.

/**
 * Setup a deactivation hook to remove this cron if this plugin is disabled
 * 
 * @source https://developer.wordpress.org/reference/functions/register_deactivation_hook/
 */
function wpstx_deactivate_email_notification_cron() {
	// retrieve the event
	$timestamp = wp_next_scheduled( 'wpstx_check_scheduled_emails' );
	// unset the event
	wp_unschedule_event( $timestamp, 'wpstx_check_scheduled_emails' );
}
register_deactivation_hook( __FILE__, 'wpstx_deactivate_email_notification_cron' );

That’s how I set up a custom post type as an Email Notification system using the WP Cron.

@github https://github.com/eirichmond/wpstx-email-notification-with-post-type

About

A geek who likes to code and be nice ^_^

3 thoughts on “Use a custom post type to send notifications as Html emails to users by the user role

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.