902 lines
30 KiB
PHP
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
|
|
}
|