Email Subscriptions Powered by BNFW


Since the beginning of this blog I’ve relied on Jetpack Subscriptions to send out email notifications when publishing a blog post. It’s a free, quick and easy solution for handling blog email subscriptions. That said it’s not very flexible and emails are handled by the WordPress.com infrastructure which is not something you can control.

For greater control I’ve switched from Jetpack Subscriptions to Better Notifications for WordPress or BNFW for short.

BNFW is not really an email subscription plugin but rather an all purpose plugin for customizing any WordPress email notifications. There are plenty of email subscription specific plugins out there, however I’m already a fan of BNFW and wanted to attempt to have it handle blog emails. The following are steps I took to migrate from Jetpack Subscriptions to BNFW.

WARNING – This is an overly complicated solution. ⚠⚠⚠

Most people would be fine to use something like MailPoet as an alternative to Jetpack Subscriptions. That said, proceed at your own risk. 👷

Preparing for BNFW by creating a custom role for handling email subscriptions

Programmatically you can assign multiple roles per user. That said, WordPress isn’t very aware of multiple roles from the dashboard. If you ever assign multiple roles per user you’ll want to avoid changing a user’s role from the WordPress backend as it will wipe out any extra roles.

A custom user role for email subscriptions is a great way to track active subscribers. With the free User Role Editor plugin create a new role named “Email Subscriber” based on the Subscriber role.

User Role Editor adds a new button called “Grant Roles” to the user management page. That allows additional user roles added with a click of a button.

Step #1 – With BNFW create a notification for new posts published targeting the custom user role.

BNFW supports a variety of template shortcodes. The contents of the email is completely customizable. Here is what I’m using for my blog notifications. 

<h1 style="margin-bottom:0px;padding-bottom:0px;"><a href="[permalink]">[post_title]</a></h1>
<p style="margin-top:0px;padding-top:0px;">By <a href="[author_link]">[post_author]</a></p>

[post_content]

<span style="background-color: #fff;border-top: 1px solid #999999;display: block;margin: 6px 0;"></span>

Post url: <a href="[permalink]">[permalink]</a> <a href="https://anchor.host/wp-signup.php?id=[email_user_id]&email=[email_user_email]&token=[email_user_token]&action=unsubscribe" style="float:right;font-size:10px;margin-top:2px">Unsubscribe?</a>

The custom “Unsubscribe” link is something I custom coded as explained in more detail later on.

Step 2: Enabled subscription management with custom code.

If you want an out-of-box subscription management solution then check out the official BNFW Subscriptions add-on. However I personally wanted something really simple. The following custom code will handle email unsubscribe with an single unsubscribe link click.

/**
 * Manage subscription to email newsletters
 */
function anchorhost_subscription_management() {
	global $pagenow;

	// Hook into wp-signup.php page if arguments found
	if ( 'wp-signup.php' !== $pagenow || !isset( $_GET['id'] ) || !isset( $_GET['email'] ) || !isset( $_GET['token'] ) || !isset( $_GET['action'] ) ) {
		return;
	}

	$email = $_GET['email'];
	$home_url = get_home_url();

	// Find user matching ID and email address.
	$user = get_user_by( "ID", $_GET['id'] );
	$user_token = wp_hash( $user->user_registered );

	if ( !$user || $user->user_email != $email || $_GET['token'] != $user_token ) {
		wp_die( "<p style='text-align:center'>Invalid email subscription token.</p>", "Manage subscription to email newsletters" );
	}

	$user_id = $user->ID;
	$user_email = $user->user_email;

	if ( $_GET['action'] == "subscribe" ) {

		$user->add_role("email_subscriber");
		$html = <<<EOT
			<p style='text-align:center'>
				Your email <strong>$email</strong> has been subscribed. <br />
				<small>If that wasn't your intention the click here to <a href='${home_url}/wp-signup.php?id=${user_id}&email=${user_email}&token=${user_token}&action=unsubscribe'>unsubscribe</a></small>
			</p>
EOT;
		wp_die( $html, "Subscribe to email newsletters" );

	}

	if ( $_GET['action'] == "unsubscribe" ) {

		$user->remove_role("email_subscriber");
		$html = <<<EOT
			<p style='text-align:center'>
				Your email <strong>${email}</strong> has been unsubscribed. <br />
				<small>If that wasn't your intention the click here to <a href='${home_url}/wp-signup.php?id=${user_id}&email=${user_email}&token=${user_token}&action=subscribe'>resubscribe</a></small>
			</p>
EOT;
		wp_die( $html, "Unsubscribe from email newsletters" );

	}

	// If action not matched then proceed to default wp-signup.php page.

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

/**
 * Creates new shortcode [email_user_token] which is useful for generating secured unsubscribe links.
 */
function anchorhost_bnfw_recipient_token_shortcode( $message, $user_id, $prefix ) {
	$user = get_user_by( "ID", $user_id );
	$message = str_replace( '[' . $prefix . 'user_token]', wp_hash( $user->user_registered ), $message );
	return $message;
}
add_filter( 'bnfw_shortcodes_user', 'anchorhost_bnfw_recipient_token_shortcode', 10, 3 );

/**
 * Responsive html email images for BNFW.
 */
function anchorhost_bnfw_responsive_html_images( $message, $setting ) {
	$message = str_replace( "<img src=\"", "<img style=\"max-width: 100% !important;\" src=\"", $message );
	return $message;
}

add_filter( 'bnfw_notification_message', 'anchorhost_bnfw_responsive_html_images', 10, 2 );

Each email notification will includes an unsubscribe link unique to the WordPress user. When clicked it simply removes that particular WordPress user from the custom role “Email Subscribers” thus deactivating their email notifications. 

If the unsubscribe link was accidently pressed, no worries, a resubscribe link will appear which will add the custom role “Email Subscribers” back to that particular WordPress user.

The email subscription signup is handled by Gravity Forms.

As you can see it’s a simple Gravity Form which collects name and email. The magic happens all with a special activation link send to the subscriber. 

{Name (First):1.3},

Thanks for subscribing to <a href="https://anchor.host/blog/">Anchor Hosting's blog</a>. To confirm your subscription <a href="https://anchor.host/wp-signup.php?entry_email={Email:2}&entry_id={entry_id}&action=subscribe_confirm">following this link</a>.

Austin Ginder
<a href="https://anchor.host">Anchor Hosting</a>

The following code handles everything after an activation link is clicked. It will add the WordPress user, if needed, and assigning the custom role.

/**
 * Manage subscription signup to email newsletters
 */
function anchorhost_subscription_signup() {
	global $pagenow;

	// Hook into wp-signup.php page if arguments found
	if ( 'wp-signup.php' !== $pagenow || !isset( $_GET['entry_email'] ) || !isset( $_GET['entry_id'] ) || !isset( $_GET['action'] ) ) {
		return;
	}

	// Find Gravity Form submission
	$entry = GFAPI::get_entry( $_GET['entry_id'] );
	$form_id = $entry['form_id'];

	// Verifies Form ID #4
	if ( $form_id != "4" ) {
		wp_die( "<p style='text-align:center'>Invalid email subscription signup link.</p>", "Manage subscription to email newsletters" );
	}

	// Verifies email exists and matches Gravity Form entry
	if ( !$entry || !isset( $entry[2] ) || $entry[2] != $_GET['entry_email'] ) {
		wp_die( "<p style='text-align:center'>Invalid email subscription signup link.</p>", "Manage subscription to email newsletters" );
	}

	$email = $_GET['entry_email'];
	$home_url = get_home_url();

	// Find user matching ID and email address.
	$user = get_user_by( "email", $email );

	// Generate user account if needed
	if ( !$user ) {
		$user = new WP_User;
		$user->role = "email_subscriber";
		$user->user_login = $email;
		$user->user_email = $email;
		$user_id = wp_insert_user( $user ) ;
		$user = get_user_by( "ID", $user_id );
	}

	$user_token = wp_hash( $user->user_registered );
	$user_id = $user->ID;
	$user_email = $user->user_email;

	if ( $_GET['action'] == "subscribe_confirm" ) {

		$user->add_role("email_subscriber");
		$html = <<<EOT
			<p style='text-align:center'>
				Your email <strong>$email</strong> has been subscribed. <br />
				<small>If that wasn't your intention the click here to <a href='${home_url}/wp-signup.php?id=${user_id}&email=${user_email}&token=${user_token}&action=unsubscribe'>unsubscribe</a></small>
			</p>
EOT;
		wp_die( $html, "Subscribe to email newsletters" );

	}

	// If action not matched then proceed to default wp-signup.php page.

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

Step #3: Moving current subscribers from Jetpack to WordPress accounts.

There are two places on WordPress.com where you can see your current subscribers. 

  • https://wordpress.com/people/followers/<domain-name> – List of WordPress.com subscribers. There is no good way to use these with an email only subscription. That said for my site there were only a few subscribers here which I knew all personally. I simply added them as WordPress users assigned to the “Email Subscriber” user role.
  • https://wordpress.com/people/email-followers/<domain-name> – List of email addresses subscribers. There is a button “Download Data as CSV” which will export all of the emails. 

Using WP-CLI these emails can be bulk imported as WordPress users with the following bash script.

while read email; do
    echo "Generating account for $email"
    wp user create $email $email --role=email_subscriber
done < emails.csv

WP-CLI should give errors if the accounts already exists. For those manually add the “Email Subscriber” user role.

Step #4: Update the email signup

Jetpack includes a email signup widget which can be applied to a page via a Jetpack shortcode. This will need replaced with the new Gravity Forms email signup. 

Step #5: Deactivate Jetpack Subscriptions

Located under /wp-admin/admin.php?page=jetpack#/discussion, toggle the option “Allow users to subscribe to your posts and comments and receive notifications via email”. With that Jetpack Subscriptions has been disabled.