Adds a simple settings page that allows you to set patterns for email subjects. Uses fnmatch, so this allows the use of * wildcards
<?php
/**
* Plugin Name: SendGrid Click-Track Bypass
* Description: Disables SendGrid click tracking on outbound emails whose subject matches an admin-managed pattern list. Useful for password resets, recovery mode notices, and other security-sensitive mail where rewritten URLs cause confusion.
* Version: 1.0.0
* Author: Team51
* License: GPLv3
*/
namespace Drinkmaster\SendGridClickTrackBypass;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
const OPTION_NAME = 'drinkmaster_sendgrid_clicktrack_bypass_rules';
const SETTINGS_GROUP = 'drinkmaster_sendgrid_clicktrack_bypass';
const PAGE_SLUG = 'drinkmaster-sendgrid-clicktrack-bypass';
const SMTPAPI_HEADER = 'X-SMTPAPI: {"filters":{"clicktrack":{"settings":{"enable":0}}}}';
/**
* Default seed list — used when the option has never been saved (or was wiped).
* If an admin clears the list and saves, an empty array is stored and respected;
* the seeds only re-appear if the option row itself is missing.
*
* @return string[]
*/
function get_default_rules() {
return array(
// Fatal error and recovery mode emails.
'Technical Issue',
'Recovery Mode',
'Your Site is Experiencing',
// Security-sensitive emails.
'Password Reset',
'Reset Password',
'[Password Reset]',
'Password Changed',
// Account verification emails.
'Confirm your email',
'Verify your email',
'Email Change Request',
'New Admin Email Address',
// Privacy and data requests.
'Personal Data Export',
'Personal Data Erasure',
'Action Confirmed',
// Two-factor and login security.
'Login Confirmation',
'Two-Factor',
'2FA',
// Auto-update and site health notifications.
'Background Update',
'Some plugins have failed to update',
'Site Health',
);
}
/**
* Returns the saved rule list as a plain array of pattern strings.
* Falls back to get_default_rules() when the option row doesn't exist.
*
* @return string[]
*/
function get_rules() {
$raw = get_option( OPTION_NAME, '' );
// Option row missing → seed with defaults.
if ( '' === $raw || null === $raw ) {
return get_default_rules();
}
$decoded = json_decode( (string) $raw, true );
// Corrupted JSON → fall back to defaults rather than failing silently.
if ( ! is_array( $decoded ) ) {
return get_default_rules();
}
// Saved as [] (admin explicitly cleared) → respect that and return empty.
return array_values( array_filter( array_map( 'strval', $decoded ), 'strlen' ) );
}
/**
* Sanitize callback for the option. Accepts the repeater POST array and stores
* it as a JSON-encoded list of trimmed, deduped, non-empty pattern strings.
*
* @param mixed $input Raw POSTed value.
*
* @return string JSON-encoded array.
*/
function sanitize_rules( $input ) {
// WP calls sanitize_option twice on first save (update_option → add_option),
// so on the 2nd pass we receive our own JSON-encoded string. Decode it back
// so this function is idempotent.
if ( is_string( $input ) ) {
$decoded = json_decode( $input, true );
$input = is_array( $decoded ) ? $decoded : array();
}
if ( ! is_array( $input ) ) {
return wp_json_encode( array() );
}
$clean = array_map( 'sanitize_text_field', array_map( 'wp_unslash', $input ) );
$clean = array_values( array_unique( array_filter( $clean, 'strlen' ) ) );
return wp_json_encode( $clean );
}
/**
* Determine whether a given email subject matches any saved rule.
* Auto-detects matching mode per rule:
* - Wildcards (`*` or `?`) → fnmatch-style match.
* - Otherwise → case-insensitive substring match.
*
* @param string $subject Email subject.
* @param string[] $rules Pattern list.
*
* @return boolean
*/
function subject_matches( $subject, array $rules ) {
if ( '' === $subject || empty( $rules ) ) {
return false;
}
$subject_lc = strtolower( $subject );
foreach ( $rules as $pattern ) {
$pattern_lc = strtolower( $pattern );
if ( false !== strpos( $pattern_lc, '*' ) || false !== strpos( $pattern_lc, '?' ) ) {
if ( function_exists( 'fnmatch' ) && fnmatch( $pattern_lc, $subject_lc ) ) {
return true;
}
continue;
}
if ( false !== strpos( $subject_lc, $pattern_lc ) ) {
return true;
}
}
return false;
}
/**
* Filter on wp_mail — injects the SendGrid header that disables click tracking
* when the outgoing subject matches a configured rule.
*
* @param array $args Arguments passed to wp_mail().
*
* @return array
*/
function maybe_disable_clicktrack( $args ) {
if ( empty( $args['subject'] ) || ! is_string( $args['subject'] ) ) {
return $args;
}
$rules = get_rules();
if ( empty( $rules ) ) {
return $args;
}
if ( ! subject_matches( $args['subject'], $rules ) ) {
return $args;
}
if ( empty( $args['headers'] ) ) {
$args['headers'] = array();
} elseif ( is_string( $args['headers'] ) ) {
$args['headers'] = explode( "\n", str_replace( "\r\n", "\n", $args['headers'] ) );
} elseif ( ! is_array( $args['headers'] ) ) {
$args['headers'] = array( (string) $args['headers'] );
}
// Avoid duplicating the header if some other code already added it.
foreach ( $args['headers'] as $existing ) {
if ( is_string( $existing ) && 0 === stripos( ltrim( $existing ), 'X-SMTPAPI:' ) ) {
return $args;
}
}
$args['headers'][] = SMTPAPI_HEADER;
return $args;
}
add_filter( 'wp_mail', __NAMESPACE__ . '\\maybe_disable_clicktrack' );
/**
* Register the option with the Settings API.
*
* @return void
*/
function register_settings() {
register_setting(
SETTINGS_GROUP,
OPTION_NAME,
array(
'type' => 'string',
'sanitize_callback' => __NAMESPACE__ . '\\sanitize_rules',
'default' => wp_json_encode( array() ),
'show_in_rest' => false,
)
);
}
add_action( 'admin_init', __NAMESPACE__ . '\\register_settings' );
/**
* Add the settings page under Settings.
*
* @return void
*/
function register_settings_page() {
add_options_page(
__( 'SendGrid Click-Track Bypass', 'drinkmaster' ),
__( 'SendGrid Tracking', 'drinkmaster' ),
'manage_options',
PAGE_SLUG,
__NAMESPACE__ . '\\render_settings_page'
);
}
add_action( 'admin_menu', __NAMESPACE__ . '\\register_settings_page' );
/**
* Render the settings screen with a JS-driven repeater.
*
* @return void
*/
function render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$rules = get_rules();
if ( empty( $rules ) ) {
$rules = array( '' );
}
?>
<div class="wrap">
<h1><?php esc_html_e( 'SendGrid Click-Track Bypass', 'drinkmaster' ); ?></h1>
<p>
<?php
esc_html_e(
'Outgoing emails whose subject matches any rule below will have SendGrid click tracking disabled via the X-SMTPAPI header. Use a plain phrase for a case-insensitive substring match (e.g. "Password Reset"), or include * / ? for wildcard matching (e.g. "*Recovery Mode*").',
'drinkmaster'
);
?>
</p>
<form method="post" action="options.php">
<?php settings_fields( SETTINGS_GROUP ); ?>
<table class="widefat striped" id="drinkmaster-scb-rules" style="max-width:780px;">
<thead>
<tr>
<th style="width:100%;"><?php esc_html_e( 'Subject pattern', 'drinkmaster' ); ?></th>
<th><?php esc_html_e( 'Remove', 'drinkmaster' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $rules as $rule ) : ?>
<tr class="drinkmaster-scb-row">
<td>
<input
type="text"
name="<?php echo esc_attr( OPTION_NAME ); ?>[]"
value="<?php echo esc_attr( $rule ); ?>"
class="regular-text"
style="width:100%;"
/>
</td>
<td>
<button type="button" class="button drinkmaster-scb-remove">×</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p>
<button type="button" class="button" id="drinkmaster-scb-add">
<?php esc_html_e( '+ Add rule', 'drinkmaster' ); ?>
</button>
</p>
<?php submit_button(); ?>
</form>
<template id="drinkmaster-scb-row-template">
<tr class="drinkmaster-scb-row">
<td>
<input
type="text"
name="<?php echo esc_attr( OPTION_NAME ); ?>[]"
value=""
class="regular-text"
style="width:100%;"
/>
</td>
<td>
<button type="button" class="button drinkmaster-scb-remove">×</button>
</td>
</tr>
</template>
<script>
( function () {
var table = document.getElementById( 'drinkmaster-scb-rules' );
var tbody = table ? table.querySelector( 'tbody' ) : null;
var addBtn = document.getElementById( 'drinkmaster-scb-add' );
var template = document.getElementById( 'drinkmaster-scb-row-template' );
if ( ! tbody || ! addBtn || ! template ) {
return;
}
addBtn.addEventListener( 'click', function () {
var clone = template.content.firstElementChild.cloneNode( true );
tbody.appendChild( clone );
var input = clone.querySelector( 'input' );
if ( input ) {
input.focus();
}
} );
tbody.addEventListener( 'click', function ( e ) {
var btn = e.target.closest( '.drinkmaster-scb-remove' );
if ( ! btn ) {
return;
}
var row = btn.closest( '.drinkmaster-scb-row' );
if ( ! row ) {
return;
}
if ( tbody.querySelectorAll( '.drinkmaster-scb-row' ).length > 1 ) {
row.remove();
} else {
var input = row.querySelector( 'input' );
if ( input ) {
input.value = '';
}
}
} );
} )();
</script>
</div>
<?php
}
Developer Discussions