replyto/replyto.php
2026-08-18 11:29:29 +00:00

902 lines
30 KiB
PHP

<?php
/**
* Plugin Name: Reply-To for WP_Mail
* Description: Configure your "Reply-To:" for WP_Mail with validation, admin settings, and context-based routing.
* Plugin URI: https://www.robotstxt.software/plugins/replyto/
* Requires at least: 4.7
* Requires PHP: 5.6
* Version: 2.0.3
* Author: ROBOTSTXT
* Author URI: https://www.robotstxt.software/
* License: GPL-3.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-3.0.txt
* Text Domain: replyto
* Domain Path: /languages
*
* @package replyto
*
* @version 2.0.3
*/
defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
register_activation_hook( __FILE__, 'wp_mail_replyto_migrate_to_v200' );
/**
* Loads the plugin text domain.
*
* @since 2.0.0
*
* @return void
*/
function wp_mail_replyto_load_textdomain() {
load_plugin_textdomain( 'replyto', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
}
add_action( 'plugins_loaded', 'wp_mail_replyto_load_textdomain' );
add_action( 'plugins_loaded', 'wp_mail_replyto_migrate_to_v200' );
/**
* Performs automatic migration from v1.0.x to v2.0.0.
*
* Migrates old single email/name options to new context-based structure.
* Runs on plugin activation, every request via plugins_loaded (idempotent).
*
* @since 2.0.0
*
* @return void
*/
function wp_mail_replyto_migrate_to_v200() {
// Check if migration has already been performed.
$migration_done = get_option( 'wp_mail_replyto_migration_v200', false );
if ( $migration_done ) {
return;
}
// Get old settings.
$old_email_raw = get_option( 'wp_mail_replyto_email', '' );
$old_name_raw = get_option( 'wp_mail_replyto_name', '' );
$old_email = is_string( $old_email_raw ) ? $old_email_raw : '';
$old_name = is_string( $old_name_raw ) ? $old_name_raw : '';
// Check if old settings exist.
if ( ! empty( $old_email ) || ! empty( $old_name ) ) {
// Create new context structure with old values in 'default' context.
$contexts = array(
'default' => array(
'email' => $old_email,
'name' => $old_name,
'enabled' => true,
),
'authentication' => array(
'email' => '',
'name' => '',
'enabled' => false,
),
'comments' => array(
'email' => '',
'name' => '',
'enabled' => false,
),
'users' => array(
'email' => '',
'name' => '',
'enabled' => false,
),
'system' => array(
'email' => '',
'name' => '',
'enabled' => false,
),
);
// Add WooCommerce context only if WooCommerce is active.
if ( class_exists( 'WooCommerce' ) ) {
$contexts['woocommerce'] = array(
'email' => '',
'name' => '',
'enabled' => false,
);
}
// Save new structure.
update_option( 'wp_mail_replyto_contexts', $contexts );
// Keep old options for rollback safety (will be removed on uninstall).
}
// Mark migration as complete.
update_option( 'wp_mail_replyto_migration_v200', true );
}
/**
* Detects email context based on WordPress backtrace.
*
* Analyzes the call stack to determine what type of email is being sent.
* Uses DEBUG_BACKTRACE_IGNORE_ARGS with a 20-frame limit for production safety.
* The detected value is cached in a static variable; pass $reset = true to clear
* the cache between emails (called automatically by wp_mail_replyto_reset_context).
*
* @since 1.3.0
*
* @param bool $reset If true, clears the static cache and returns without detecting.
* @return string Context: 'authentication', 'comments', 'users', 'system', 'woocommerce', or 'default'.
*/
function wp_mail_replyto_detect_context( $reset = false ) {
static $context = null;
if ( $reset ) {
$context = null;
return '';
}
// Use cached value if available (for performance within the same email).
if ( null !== $context ) {
return $context;
}
// Get backtrace with minimal overhead.
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace -- Not debug code: debug_backtrace() is essential for production context detection. It analyzes the call stack to identify which WordPress function is sending the email (e.g., retrieve_password, wp_notify_postauthor). This is the core functionality of the plugin, not debugging. The function is optimized with DEBUG_BACKTRACE_IGNORE_ARGS and limited to 20 frames for performance.
$backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 20 );
$detected = 'default';
foreach ( $backtrace as $trace ) {
if ( empty( $trace['function'] ) ) {
continue;
}
$function = $trace['function'];
$class = isset( $trace['class'] ) ? $trace['class'] : '';
// Authentication context.
if ( in_array(
$function,
array(
'retrieve_password',
'reset_password',
'wp_password_change_notification',
),
true
) ) {
$detected = 'authentication';
break;
}
// Comments context.
if ( in_array(
$function,
array(
'wp_notify_postauthor',
'wp_notify_moderator',
'wp_new_comment_notify_postauthor',
'wp_new_comment_notify_moderator',
),
true
) ) {
$detected = 'comments';
break;
}
// Users context.
if ( in_array(
$function,
array(
'wp_new_user_notification',
'wp_send_new_user_notifications',
'register_new_user',
),
true
) ) {
$detected = 'users';
break;
}
// System context.
if ( 'WP_Automatic_Updater' === $class ||
'WP_Recovery_Mode' === $class ||
in_array(
$function,
array(
'wp_maybe_auto_update',
'send_core_update_notification_email',
),
true
) ) {
$detected = 'system';
break;
}
// WooCommerce context.
if ( ! empty( $class ) && false !== strpos( $class, 'WC_Email' ) ) {
$detected = 'woocommerce';
break;
}
}
/**
* Filters the detected email context.
*
* Allows overriding the context before the Reply-To address is resolved.
* Return a custom context key to apply a different Reply-To configuration,
* or 'default' to use the fallback.
*
* @since 2.0.0
*
* @param string $detected Detected context: 'authentication', 'comments', 'users',
* 'system', 'woocommerce', or 'default'.
*/
$context = (string) apply_filters( 'wp_mail_replyto_context', $detected );
return $context;
}
/**
* Resets the context cache after email is sent.
*
* Ensures each email detection starts fresh when multiple emails are sent
* within the same PHP process (e.g., WP-Cron, REST batch).
*
* @since 1.3.0
*
* @param array{to: string|string[], subject: string, message: string, headers: string|string[], attachments: string[]} $args Email arguments.
* @return array{to: string|string[], subject: string, message: string, headers: string|string[], attachments: string[]} Unmodified email arguments.
*/
function wp_mail_replyto_reset_context( $args ) {
wp_mail_replyto_detect_context( true );
return $args;
}
add_filter( 'wp_mail', 'wp_mail_replyto_reset_context', 999 );
/**
* Modifies the "Reply-To" header in emails sent using wp_mail().
*
* Detects email context and applies appropriate Reply-To based on configuration.
* Falls back to default context if specific context is not configured.
* Removes any existing Reply-To header before injecting the configured value.
*
* @since 1.3.0 Updated to support context-based Reply-To.
*
* @param array{to: string|string[], subject: string, message: string, headers: string|string[], attachments: string[]} $args The email arguments passed to wp_mail().
* @return array{to: string|string[], subject: string, message: string, headers: string|string[], attachments: string[]} Modified email arguments with adjusted "Reply-To" header.
*/
function wp_mail_replyto( $args ) {
// Detect email context.
$detected_context = wp_mail_replyto_detect_context();
// Get all contexts configuration.
$contexts_raw = get_option( 'wp_mail_replyto_contexts', array() );
$contexts = is_array( $contexts_raw ) ? $contexts_raw : array();
// Normalize the detected context entry.
$detected_ctx = isset( $contexts[ $detected_context ] ) && is_array( $contexts[ $detected_context ] )
? $contexts[ $detected_context ]
: array();
// Normalize the default context entry.
$default_ctx = isset( $contexts['default'] ) && is_array( $contexts['default'] )
? $contexts['default']
: array();
// Fallback chain: specific context -> default -> legacy.
$reply_to_email = '';
$reply_to_name = '';
$detected_email = isset( $detected_ctx['email'] ) && is_string( $detected_ctx['email'] ) ? $detected_ctx['email'] : '';
$detected_enabled = ! empty( $detected_ctx['enabled'] );
if ( $detected_enabled && ! empty( $detected_email ) ) {
// Specific context is configured and enabled.
$reply_to_email = $detected_email;
$reply_to_name = isset( $detected_ctx['name'] ) && is_string( $detected_ctx['name'] ) ? $detected_ctx['name'] : '';
} else {
$default_email = isset( $default_ctx['email'] ) && is_string( $default_ctx['email'] ) ? $default_ctx['email'] : '';
if ( ! empty( $default_email ) ) {
// Fall back to default context.
$reply_to_email = $default_email;
$reply_to_name = isset( $default_ctx['name'] ) && is_string( $default_ctx['name'] ) ? $default_ctx['name'] : '';
} else {
// Final fallback: legacy single option (for migration period).
$legacy_email = get_option( 'wp_mail_replyto_email', '' );
$legacy_name = get_option( 'wp_mail_replyto_name', '' );
$reply_to_email = is_string( $legacy_email ) ? $legacy_email : '';
$reply_to_name = is_string( $legacy_name ) ? $legacy_name : '';
}
}
// Explicit header injection prevention - defense in depth.
if ( ! empty( $reply_to_email ) ) {
$reply_to_email = str_replace( array( "\r", "\n", '%0a', '%0d', "\0" ), '', $reply_to_email );
}
if ( ! empty( $reply_to_name ) ) {
$reply_to_name = str_replace( array( "\r", "\n", '%0a', '%0d', "\0" ), '', $reply_to_name );
}
// Construct the new "Reply-To" header if a valid email address is set.
$new_reply_to = '';
if ( ! empty( $reply_to_email ) && is_email( $reply_to_email ) ) {
$safe_email = sanitize_email( $reply_to_email );
if ( ! empty( $reply_to_name ) ) {
$display_name = sanitize_text_field( $reply_to_name );
// Strip any surrounding double-quotes the user may have typed manually.
$display_name = trim( $display_name, '"' );
// RFC 5322 specials that require a quoted-string: ( ) < > [ ] : ; @ backslash comma period and double-quote.
if ( preg_match( '/[()<>\[\]:;@\\\\,."\\r\\n]/', $display_name ) ) {
$safe_name = str_replace( array( '\\', '"' ), array( '\\\\', '\\"' ), $display_name );
$new_reply_to = 'Reply-To: "' . $safe_name . '" <' . $safe_email . '>';
} else {
// Simple phrase: no quoting needed.
$new_reply_to = 'Reply-To: ' . $display_name . ' <' . $safe_email . '>';
}
} else {
// Email only.
$new_reply_to = 'Reply-To: <' . $safe_email . '>';
}
}
/**
* Filters the Reply-To header string before injection.
*
* Return an empty string to suppress Reply-To injection for this email.
*
* @since 2.0.0
*
* @param string $new_reply_to The constructed Reply-To header (e.g. 'Reply-To: <user@example.com>').
* @param array{to: string|string[], subject: string, message: string, headers: string|string[], attachments: string[]} $args The wp_mail arguments.
*/
$new_reply_to = (string) apply_filters( 'wp_mail_replyto_header', $new_reply_to, $args );
// Check if email headers exist.
if ( ! empty( $args['headers'] ) ) {
// Normalize headers into an array if they are not already.
if ( ! is_array( $args['headers'] ) ) {
$args['headers'] = array_filter(
explode( "\n", str_replace( array( "\r\n", "\r" ), "\n", $args['headers'] ) )
);
}
if ( ! empty( $new_reply_to ) ) {
// Remove any existing Reply-To header unconditionally (RFC 5322 allows only one).
$args['headers'] = array_values(
array_filter(
$args['headers'],
function ( $header ) {
return stripos( $header, 'reply-to:' ) !== 0;
}
)
);
$args['headers'][] = $new_reply_to;
}
} elseif ( ! empty( $new_reply_to ) ) {
// If no headers exist at all, create a new headers array with the "Reply-To".
$args['headers'] = array( $new_reply_to );
}
// Return the modified email arguments.
return $args;
}
add_filter( 'wp_mail', 'wp_mail_replyto' );
/**
* Registers a settings page for the plugin in the WordPress admin menu.
*
* Adds a new page under the "Settings" menu where administrators can configure
* the "Reply-To" email address used in outgoing emails.
*
* @since 1.3.0
*
* @return void
*/
function wp_mail_replyto_add_settings_page() {
add_options_page(
esc_html__( 'WP Mail Reply-To Settings', 'replyto' ),
esc_html__( 'Reply-To', 'replyto' ),
'manage_options',
'replyto',
'wp_mail_replyto_render_settings_page'
);
}
add_action( 'admin_menu', 'wp_mail_replyto_add_settings_page' );
/**
* Validates email address with strict RFC 5322 format checking.
*
* Performs additional validation beyond WordPress's is_email() function
* to ensure strict compliance with email standards.
*
* @since 1.1.0
*
* @param string $email The email address to validate.
* @return bool True if email is valid, false otherwise.
*/
function wp_mail_replyto_validate_email_strict( $email ) {
// Basic WordPress email validation.
if ( ! is_email( $email ) ) {
return false;
}
// Ensure no angle brackets in the email address itself.
if ( false !== strpos( $email, '<' ) || false !== strpos( $email, '>' ) ) {
return false;
}
// Additional validation for special characters that could cause issues.
if ( preg_match( '/[\r\n\0]/', $email ) ) {
return false;
}
return true;
}
/**
* Registers the plugin settings, section, and field in the WordPress Settings API.
*
* This function defines the context-based configuration structure.
*
* @since 1.3.0 Updated to support multiple contexts.
*
* @return void
*/
function wp_mail_replyto_register_settings() {
// Register the contexts setting.
register_setting(
'wp_mail_replyto_settings_group',
'wp_mail_replyto_contexts',
array(
'type' => 'array',
'sanitize_callback' => 'wp_mail_replyto_sanitize_contexts',
'default' => array(),
)
);
// Legacy settings kept for backward compatibility during migration.
register_setting(
'wp_mail_replyto_settings_group',
'wp_mail_replyto_email',
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_email',
'default' => '',
)
);
register_setting(
'wp_mail_replyto_settings_group',
'wp_mail_replyto_name',
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
)
);
}
add_action( 'admin_init', 'wp_mail_replyto_register_settings' );
/**
* Sanitizes and validates the contexts configuration.
*
* Validates all context email addresses and names, performs DNS validation,
* provides appropriate error messages, and logs changes.
*
* @since 1.3.0
*
* @param array<string, mixed> $input The input contexts array.
* @return array<string, array{email: string, name: string, enabled: bool}> The sanitized contexts array.
*/
function wp_mail_replyto_sanitize_contexts( $input ) {
$sanitized = array();
// Define available contexts.
$available_contexts = array(
'default' => __( 'Default', 'replyto' ),
'authentication' => __( 'Authentication', 'replyto' ),
'comments' => __( 'Comments', 'replyto' ),
'users' => __( 'Users', 'replyto' ),
'system' => __( 'System', 'replyto' ),
);
// Add WooCommerce context only if WooCommerce is active.
if ( class_exists( 'WooCommerce' ) ) {
$available_contexts['woocommerce'] = __( 'WooCommerce', 'replyto' );
}
foreach ( $available_contexts as $context_key => $context_label ) {
// Get input for this context.
$ctx_input = isset( $input[ $context_key ] ) && is_array( $input[ $context_key ] ) ? $input[ $context_key ] : array();
$email = isset( $ctx_input['email'] ) && is_string( $ctx_input['email'] ) ? sanitize_email( $ctx_input['email'] ) : '';
$name = isset( $ctx_input['name'] ) && is_string( $ctx_input['name'] ) ? sanitize_text_field( $ctx_input['name'] ) : '';
$enabled_raw = isset( $ctx_input['enabled'] ) ? $ctx_input['enabled'] : '';
$enabled = '1' === $enabled_raw || true === $enabled_raw;
// Header injection prevention.
if ( ! empty( $email ) ) {
$email = str_replace( array( "\r", "\n", '%0a', '%0d', "\0" ), '', $email );
}
if ( ! empty( $name ) ) {
$name = str_replace( array( "\r", "\n", '%0a', '%0d', "\0" ), '', $name );
}
// Validate email if provided.
if ( ! empty( $email ) && ! wp_mail_replyto_validate_email_strict( $email ) ) {
add_settings_error(
'wp_mail_replyto_messages',
'wp_mail_replyto_invalid_email_' . $context_key,
/* translators: %s: context name */
sprintf( esc_html__( 'Invalid email address for %s context.', 'replyto' ), $context_label ),
'error'
);
$email = '';
}
// Check domain existence (optional validation with warning only).
if ( ! empty( $email ) && function_exists( 'checkdnsrr' ) ) {
$domain_str = strrchr( $email, '@' );
if ( false !== $domain_str ) {
$domain = substr( $domain_str, 1 );
if ( ! empty( $domain ) && ! checkdnsrr( $domain, 'MX' ) && ! checkdnsrr( $domain, 'A' ) ) {
add_settings_error(
'wp_mail_replyto_messages',
'wp_mail_replyto_domain_warning_' . $context_key,
/* translators: %s: context name */
sprintf( esc_html__( 'Warning: The email domain for %s context does not appear to have valid DNS records.', 'replyto' ), $context_label ),
'warning'
);
}
}
}
// Limit name length (multibyte-aware).
if ( function_exists( 'mb_strlen' ) ) {
if ( mb_strlen( $name, 'UTF-8' ) > 255 ) {
$name = mb_substr( $name, 0, 255, 'UTF-8' );
add_settings_error(
'wp_mail_replyto_messages',
'wp_mail_replyto_name_long_' . $context_key,
/* translators: %s: context name */
sprintf( esc_html__( 'Name for %s context was truncated to 255 characters.', 'replyto' ), $context_label ),
'warning'
);
}
} elseif ( strlen( $name ) > 255 ) {
$name = substr( $name, 0, 255 );
add_settings_error(
'wp_mail_replyto_messages',
'wp_mail_replyto_name_long_' . $context_key,
/* translators: %s: context name */
sprintf( esc_html__( 'Name for %s context was truncated to 255 characters.', 'replyto' ), $context_label ),
'warning'
);
}
// Default context is always enabled if it has an email.
if ( 'default' === $context_key && ! empty( $email ) ) {
$enabled = true;
}
// Store sanitized values.
$sanitized[ $context_key ] = array(
'email' => $email,
'name' => $name,
'enabled' => $enabled,
);
}
// Log changes if in debug mode.
$old_value = get_option( 'wp_mail_replyto_contexts', array() );
if ( $old_value !== $sanitized ) {
$user = wp_get_current_user();
$remote_addr = isset( $_SERVER['REMOTE_ADDR'] ) && is_string( $_SERVER['REMOTE_ADDR'] )
? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) )
: 'unknown';
if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Intentional: error_log() is used for security audit logging, not debugging. Only active when WP_DEBUG_LOG is enabled. Records context configuration changes with user info and IP for security compliance and troubleshooting. This is a production feature for administrators who enable logging in wp-config.php.
error_log(
sprintf(
'[Reply-To Plugin] Contexts configuration updated by user %s (ID: %d) from IP: %s',
$user->user_login,
$user->ID,
$remote_addr
)
);
}
add_settings_error(
'wp_mail_replyto_messages',
'wp_mail_replyto_updated',
esc_html__( 'Reply-To configuration updated successfully.', 'replyto' ),
'success'
);
}
return $sanitized;
}
/**
* Enqueues admin styles for the settings page.
*
* @since 1.3.0
*
* @param string $hook The current admin page hook.
* @return void
*/
function wp_mail_replyto_admin_styles( $hook ) {
if ( 'settings_page_replyto' !== $hook ) {
return;
}
wp_register_style( 'replyto-admin', false, array(), '2.0.3' );
wp_enqueue_style( 'replyto-admin' );
wp_add_inline_style(
'replyto-admin',
'.replyto-context-description {
margin: 10px 0 20px;
padding: 10px;
background: #f0f6fc;
border-left: 4px solid #2271b1;
}
.replyto-field-group {
margin-bottom: 20px;
}
.replyto-field-group label {
display: block;
font-weight: 600;
margin-bottom: 5px;
}
.replyto-field-group input[type="email"],
.replyto-field-group input[type="text"] {
width: 100%;
max-width: 400px;
}
.replyto-field-group .description {
margin-top: 5px;
color: #646970;
}
.replyto-toggle-wrapper {
margin-bottom: 20px;
padding: 15px;
background: #fff9e5;
border-left: 4px solid #dba617;
}
.replyto-tab-content {
margin-top: 20px;
}
.replyto-status-legend {
font-size: 12px;
color: #646970;
margin: 10px 0 0 0;
padding: 0;
}
.replyto-status-legend span {
margin-right: 15px;
}'
);
}
add_action( 'admin_enqueue_scripts', 'wp_mail_replyto_admin_styles' );
/**
* Renders the plugin's settings page with tab-based interface.
*
* Uses WordPress native nav-tab-wrapper for consistent admin UI.
*
* @since 1.3.0 Completely rewritten with tabs interface.
*
* @return void
*/
function wp_mail_replyto_render_settings_page() {
// Check if the current user has the required capability.
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Get current contexts configuration.
$contexts_raw = get_option( 'wp_mail_replyto_contexts', array() );
$contexts = is_array( $contexts_raw ) ? $contexts_raw : array();
// Get active tab from URL, default to 'default'.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Tab parameter is for display only, not data modification.
$active_tab_raw = isset( $_GET['tab'] ) && is_string( $_GET['tab'] ) ? $_GET['tab'] : 'default';
$active_tab = sanitize_key( $active_tab_raw );
// Define contexts with descriptions.
$context_config = array(
'default' => array(
'label' => __( 'Default', 'replyto' ),
'description' => __( 'Default Reply-To used for all emails that do not match a specific context. This is the fallback when other contexts are not configured.', 'replyto' ),
'examples' => __( 'Newsletter confirmations, general notifications, and any emails not covered by other contexts.', 'replyto' ),
),
'authentication' => array(
'label' => __( 'Authentication & Security', 'replyto' ),
'description' => __( 'Reply-To for password resets, password changes, and email address changes.', 'replyto' ),
'examples' => __( 'Password reset requests, password change confirmations, email address verifications.', 'replyto' ),
),
'comments' => array(
'label' => __( 'Comments & Moderation', 'replyto' ),
'description' => __( 'Reply-To for comment notifications and moderation alerts.', 'replyto' ),
'examples' => __( 'New comment notifications, comment moderation alerts, comment replies.', 'replyto' ),
),
'users' => array(
'label' => __( 'Users & Registration', 'replyto' ),
'description' => __( 'Reply-To for new user registrations, role changes, and user management emails.', 'replyto' ),
'examples' => __( 'New user welcome emails, user role changes, account activations.', 'replyto' ),
),
'system' => array(
'label' => __( 'System & Updates', 'replyto' ),
'description' => __( 'Reply-To for automatic updates, system alerts, and critical site health notifications.', 'replyto' ),
'examples' => __( 'WordPress core updates, plugin updates, theme updates, recovery mode, fatal error notifications.', 'replyto' ),
),
);
// Add WooCommerce context only if WooCommerce is active.
if ( class_exists( 'WooCommerce' ) ) {
$context_config['woocommerce'] = array(
'label' => __( 'WooCommerce', 'replyto' ),
'description' => __( 'Reply-To for WooCommerce order notifications, invoices, and customer communications.', 'replyto' ),
'examples' => __( 'Order confirmations, shipping notifications, invoices, customer notes.', 'replyto' ),
);
}
// Validate active tab exists.
if ( ! isset( $context_config[ $active_tab ] ) ) {
$active_tab = 'default';
}
// Display settings errors, if any.
settings_errors( 'wp_mail_replyto_messages' );
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<p><?php esc_html_e( 'Configure different Reply-To addresses based on the type of email being sent. Each context can have its own email address and display name.', 'replyto' ); ?></p>
<h2 class="nav-tab-wrapper">
<?php
foreach ( $context_config as $key => $config ) {
$tab_url = add_query_arg(
array(
'page' => 'replyto',
'tab' => $key,
),
admin_url( 'options-general.php' )
);
// Determine status indicator.
$ctx_data = isset( $contexts[ $key ] ) && is_array( $contexts[ $key ] ) ? $contexts[ $key ] : array();
$ctx_email = isset( $ctx_data['email'] ) && is_string( $ctx_data['email'] ) ? $ctx_data['email'] : '';
$ctx_enabled = ! empty( $ctx_data['enabled'] );
// For Default context: only check if email exists (always enabled).
// For other contexts: check if enabled AND email exists.
if ( 'default' === $key ) {
$status_indicator = ! empty( $ctx_email ) ? '🟢 ' : '🔴 ';
} else {
$status_indicator = ( $ctx_enabled && ! empty( $ctx_email ) ) ? '🟢 ' : '🔴 ';
}
printf(
'<a href="%s" class="nav-tab%s">%s%s</a>',
esc_url( $tab_url ),
$active_tab === $key ? ' nav-tab-active' : '',
esc_html( $status_indicator ),
esc_html( $config['label'] )
);
}
?>
</h2>
<p class="replyto-status-legend">
<span>🟢 <?php esc_html_e( 'Active with email configured', 'replyto' ); ?></span>
<span>🔴 <?php esc_html_e( 'Inactive or no email configured', 'replyto' ); ?></span>
</p>
<form action="options.php" method="post">
<?php settings_fields( 'wp_mail_replyto_settings_group' ); ?>
<div class="replyto-tab-content">
<?php
$config = isset( $context_config[ $active_tab ] ) ? $context_config[ $active_tab ] : $context_config['default'];
$active_data = isset( $contexts[ $active_tab ] ) && is_array( $contexts[ $active_tab ] ) ? $contexts[ $active_tab ] : array();
$email = isset( $active_data['email'] ) && is_string( $active_data['email'] ) ? $active_data['email'] : '';
$name = isset( $active_data['name'] ) && is_string( $active_data['name'] ) ? $active_data['name'] : '';
$enabled = ! empty( $active_data['enabled'] );
?>
<div class="replyto-context-description">
<p><strong><?php esc_html_e( 'What this context covers:', 'replyto' ); ?></strong><br>
<?php echo esc_html( $config['description'] ); ?></p>
<p><strong><?php esc_html_e( 'Examples:', 'replyto' ); ?></strong><br>
<?php echo esc_html( $config['examples'] ); ?></p>
</div>
<?php if ( 'default' !== $active_tab ) : ?>
<div class="replyto-toggle-wrapper">
<label>
<input type="checkbox"
name="wp_mail_replyto_contexts[<?php echo esc_attr( $active_tab ); ?>][enabled]"
value="1"
<?php checked( $enabled ); ?> />
<strong><?php esc_html_e( 'Enable this context', 'replyto' ); ?></strong>
</label>
<p class="description">
<?php esc_html_e( 'When disabled, emails in this context will use the Default Reply-To address.', 'replyto' ); ?>
</p>
</div>
<?php endif; ?>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row">
<label for="replyto_<?php echo esc_attr( $active_tab ); ?>_email">
<?php esc_html_e( 'Reply-To Email Address', 'replyto' ); ?>
</label>
</th>
<td>
<input type="email"
id="replyto_<?php echo esc_attr( $active_tab ); ?>_email"
name="wp_mail_replyto_contexts[<?php echo esc_attr( $active_tab ); ?>][email]"
value="<?php echo esc_attr( $email ); ?>"
class="regular-text" />
<p class="description">
<?php esc_html_e( 'Enter the email address where replies should be sent.', 'replyto' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="replyto_<?php echo esc_attr( $active_tab ); ?>_name">
<?php esc_html_e( 'Reply-To Display Name (Optional)', 'replyto' ); ?>
</label>
</th>
<td>
<input type="text"
id="replyto_<?php echo esc_attr( $active_tab ); ?>_name"
name="wp_mail_replyto_contexts[<?php echo esc_attr( $active_tab ); ?>][name]"
value="<?php echo esc_attr( $name ); ?>"
class="regular-text" />
<p class="description">
<?php esc_html_e( 'Optional: Enter a name to display with the email (e.g., "Support Team").', 'replyto' ); ?>
</p>
</td>
</tr>
</tbody>
</table>
<?php if ( 'default' === $active_tab ) : ?>
<p class="description">
<strong><?php esc_html_e( 'Note:', 'replyto' ); ?></strong>
<?php esc_html_e( 'The Default context is always active and acts as a fallback for all emails.', 'replyto' ); ?>
</p>
<?php endif; ?>
<?php
// Add hidden fields for all other contexts to preserve their values.
foreach ( $context_config as $key => $config_item ) {
if ( $key === $active_tab ) {
continue;
}
$ctx_data = isset( $contexts[ $key ] ) && is_array( $contexts[ $key ] ) ? $contexts[ $key ] : array();
$ctx_email = isset( $ctx_data['email'] ) && is_string( $ctx_data['email'] ) ? $ctx_data['email'] : '';
$ctx_name = isset( $ctx_data['name'] ) && is_string( $ctx_data['name'] ) ? $ctx_data['name'] : '';
$ctx_enabled = ! empty( $ctx_data['enabled'] );
?>
<input type="hidden" name="wp_mail_replyto_contexts[<?php echo esc_attr( $key ); ?>][email]" value="<?php echo esc_attr( $ctx_email ); ?>" />
<input type="hidden" name="wp_mail_replyto_contexts[<?php echo esc_attr( $key ); ?>][name]" value="<?php echo esc_attr( $ctx_name ); ?>" />
<?php if ( $ctx_enabled ) : ?>
<input type="hidden" name="wp_mail_replyto_contexts[<?php echo esc_attr( $key ); ?>][enabled]" value="1" />
<?php endif; ?>
<?php
}
?>
</div>
<?php submit_button( esc_html__( 'Save Settings', 'replyto' ) ); ?>
</form>
</div>
<?php
}