robotstxt-smtp-newsletter/admin/class-settings-page.php
2026-01-29 10:02:03 +00:00

579 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Settings page for admin.
*
* @package ROBOTSTXT_SMTP_Newsletter
*/
namespace Robotstxt_SMTP_Newsletter\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Settings page class.
*
* Manages plugin configuration in WordPress admin.
*/
class Settings_Page {
/**
* Singleton instance.
*
* @var Settings_Page|null
*/
private static $instance = null;
/**
* Option name.
*
* @var string
*/
private const OPTION_NAME = 'robotstxt_smtp_newsletter_options';
/**
* Get singleton instance.
*
* @return Settings_Page
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
*/
private function __construct() {
add_action( 'admin_init', array( $this, 'register_settings' ) );
// Note: We don't add admin_menu here because NewsletterMailerAddon parent handles menu creation.
// The parent expects admin/index.php to render the settings page.
}
/**
* Static method to register settings without creating instance.
* Used when parent addon handles menu creation.
*
* @return void
*/
public static function register_settings_static() {
// Just ensure the instance is created so register_settings hook is added.
self::get_instance();
}
/**
* Register settings.
*
* @return void
*/
public function register_settings() {
register_setting(
'robotstxt_smtp_newsletter',
self::OPTION_NAME,
array(
'sanitize_callback' => array( $this, 'sanitize_options' ),
)
);
add_settings_section(
'robotstxt_smtp_newsletter_main',
__( 'SMTP (by ROBOTSTXT) Integration', 'robotstxt-smtp-newsletter' ),
array( $this, 'render_section' ),
'robotstxt_smtp_newsletter'
);
add_settings_field(
'enabled',
__( 'Enable Integration', 'robotstxt-smtp-newsletter' ),
array( $this, 'render_enabled_field' ),
'robotstxt_smtp_newsletter',
'robotstxt_smtp_newsletter_main'
);
add_settings_field(
'batch_size',
__( 'Batch Size', 'robotstxt-smtp-newsletter' ),
array( $this, 'render_batch_size_field' ),
'robotstxt_smtp_newsletter',
'robotstxt_smtp_newsletter_main'
);
add_settings_field(
'max_per_connection',
__( 'Max Emails Per Connection', 'robotstxt-smtp-newsletter' ),
array( $this, 'render_max_per_connection_field' ),
'robotstxt_smtp_newsletter',
'robotstxt_smtp_newsletter_main'
);
}
/**
* Add settings page to admin menu.
*
* @return void
*/
public function add_settings_page() {
add_submenu_page(
'newsletter_main_index',
__( 'SMTP (by ROBOTSTXT)', 'robotstxt-smtp-newsletter' ),
__( 'SMTP (by ROBOTSTXT)', 'robotstxt-smtp-newsletter' ),
'manage_options',
'robotstxt-smtp-newsletter',
array( $this, 'render_page' )
);
}
/**
* Render settings page.
*
* @return void
*/
public function render_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'robotstxt-smtp-newsletter' ) );
}
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form action="options.php" method="post">
<?php
settings_fields( 'robotstxt_smtp_newsletter' );
do_settings_sections( 'robotstxt_smtp_newsletter' );
submit_button();
?>
</form>
</div>
<?php
}
/**
* Render section description.
*
* @return void
*/
public function render_section() {
echo '<p>';
esc_html_e( 'Configure how Newsletter uses SMTP (by ROBOTSTXT) for email delivery.', 'robotstxt-smtp-newsletter' );
echo '</p>';
// Show current SMTP configuration.
if ( ! class_exists( '\Robotstxt_SMTP\Plugin' ) ) {
echo '<div class="notice notice-error inline"><p>';
esc_html_e( 'ROBOTSTXT SMTP plugin is not active. Please activate it first.', 'robotstxt-smtp-newsletter' );
echo '</p></div>';
return;
}
$plugin = \Robotstxt_SMTP_Newsletter\Plugin::get_instance();
$settings = $plugin->get_smtp_settings();
echo '<div class="notice notice-info inline"><p>';
echo '<strong>' . esc_html__( 'Current SMTP Configuration:', 'robotstxt-smtp-newsletter' ) . '</strong><br>';
echo '<ul style="margin: 10px 0 0 20px;">';
echo '<li>' . sprintf(
esc_html__( 'Host: %s:%d', 'robotstxt-smtp-newsletter' ),
esc_html( $settings['host'] ?? '' ),
absint( $settings['port'] ?? 0 )
) . '</li>';
$encryption = $settings['encryption'] ?? '';
if ( ! empty( $encryption ) ) {
echo '<li>' . sprintf(
esc_html__( 'Encryption: %s', 'robotstxt-smtp-newsletter' ),
esc_html( strtoupper( $encryption ) )
) . '</li>';
}
if ( \Robotstxt_SMTP\Plugin::is_amazon_ses_integration_active() ) {
echo '<li><strong>' . esc_html__( 'Amazon SES: Active', 'robotstxt-smtp-newsletter' ) . '</strong></li>';
}
echo '</ul>';
echo '</p></div>';
// Show optimal batch size recommendations.
$this->render_recommendations();
}
/**
* Render settings recommendations.
*
* @return void
*/
private function render_recommendations() {
$optimal = $this->calculate_optimal_settings();
$current_options = \Robotstxt_SMTP_Newsletter\Plugin::get_instance()->get_settings();
$current_batch = absint( $current_options['batch_size'] ?? 10 );
$current_max_conn = absint( $current_options['max_per_connection'] ?? 50 );
// Determine batch size status.
$batch_status = 'optimal';
if ( $current_batch < $optimal['batch_size'] * 0.5 ) {
$batch_status = 'too_low';
} elseif ( $current_batch > $optimal['batch_size'] * 1.5 ) {
$batch_status = 'too_high';
} elseif ( abs( $current_batch - $optimal['batch_size'] ) > 5 ) {
$batch_status = 'suboptimal';
}
// Determine max_per_connection status (only for SMTP).
$max_conn_status = 'optimal';
if ( ! $optimal['is_ses'] && $optimal['max_per_connection'] > 0 ) {
if ( $current_max_conn < $optimal['max_per_connection'] * 0.5 ) {
$max_conn_status = 'too_low';
} elseif ( $current_max_conn > $optimal['max_per_connection'] * 1.5 ) {
$max_conn_status = 'too_high';
} elseif ( abs( $current_max_conn - $optimal['max_per_connection'] ) > 20 ) {
$max_conn_status = 'suboptimal';
}
}
// Overall status.
$overall_status = 'optimal';
if ( 'too_low' === $batch_status || 'too_high' === $batch_status || 'too_low' === $max_conn_status || 'too_high' === $max_conn_status ) {
$overall_status = 'warning';
} elseif ( 'suboptimal' === $batch_status || 'suboptimal' === $max_conn_status ) {
$overall_status = 'info';
}
// Status styling.
$notice_class = 'notice-info';
$status_icon = '&#9432;'; // Info icon.
if ( 'optimal' === $overall_status ) {
$notice_class = 'notice-success';
$status_icon = '&#10004;'; // Checkmark.
} elseif ( 'warning' === $overall_status ) {
$notice_class = 'notice-warning';
$status_icon = '&#9888;'; // Warning icon.
}
echo '<div class="notice ' . esc_attr( $notice_class ) . ' inline" style="margin-top: 15px;"><p>';
echo '<strong>' . $status_icon . ' ' . esc_html__( 'Settings Recommendations', 'robotstxt-smtp-newsletter' ) . '</strong><br>';
echo '<ul style="margin: 10px 0 0 20px;">';
// Show cron interval.
echo '<li>' . sprintf(
/* translators: %d: seconds */
esc_html__( 'Newsletter cron runs every: %d seconds', 'robotstxt-smtp-newsletter' ),
$optimal['cron_interval']
) . '</li>';
// Show delivery method.
$delivery_method = $optimal['is_ses']
? __( 'Amazon SES API (REST)', 'robotstxt-smtp-newsletter' )
: __( 'SMTP with SMTPKeepAlive', 'robotstxt-smtp-newsletter' );
echo '<li>' . sprintf(
/* translators: %s: delivery method */
esc_html__( 'Delivery method: %s', 'robotstxt-smtp-newsletter' ),
esc_html( $delivery_method )
) . '</li>';
// Show calculation reasoning.
if ( ! empty( $optimal['reasoning'] ) ) {
foreach ( $optimal['reasoning'] as $reason ) {
echo '<li>' . esc_html( $reason ) . '</li>';
}
}
echo '</ul>';
// Show recommendations.
echo '<p style="margin-top: 10px;"><strong>';
// Batch size recommendation.
if ( 'optimal' === $batch_status ) {
echo '&#10004; ' . sprintf(
/* translators: %d: batch size */
esc_html__( 'Batch size: Your current setting (%d) is optimal!', 'robotstxt-smtp-newsletter' ),
$current_batch
);
} else {
echo '&#9658; ' . sprintf(
/* translators: %d: recommended batch size */
esc_html__( 'Recommended batch size: %d emails/batch', 'robotstxt-smtp-newsletter' ),
$optimal['batch_size']
);
if ( 'too_low' === $batch_status ) {
echo '<br>&nbsp;&nbsp;&nbsp;' . sprintf(
/* translators: %d: current batch size */
esc_html__( 'Current (%d) is significantly lower than optimal. Increase it to send faster.', 'robotstxt-smtp-newsletter' ),
$current_batch
);
} elseif ( 'too_high' === $batch_status ) {
echo '<br>&nbsp;&nbsp;&nbsp;' . sprintf(
/* translators: %d: current batch size */
esc_html__( 'Current (%d) is too high and may trigger rate limits. Reduce it to avoid errors.', 'robotstxt-smtp-newsletter' ),
$current_batch
);
} elseif ( 'suboptimal' === $batch_status ) {
echo '<br>&nbsp;&nbsp;&nbsp;' . sprintf(
/* translators: 1: current batch size, 2: recommended batch size */
esc_html__( 'Current (%1$d) could be optimized. Consider changing to %2$d.', 'robotstxt-smtp-newsletter' ),
$current_batch,
$optimal['batch_size']
);
}
}
// Max per connection recommendation (only for SMTP).
if ( ! $optimal['is_ses'] && $optimal['max_per_connection'] > 0 ) {
echo '<br>';
if ( 'optimal' === $max_conn_status ) {
echo '&#10004; ' . sprintf(
/* translators: %d: max per connection */
esc_html__( 'Max per connection: Your current setting (%d) is optimal!', 'robotstxt-smtp-newsletter' ),
$current_max_conn
);
} else {
echo '&#9658; ' . sprintf(
/* translators: %d: recommended max per connection */
esc_html__( 'Recommended max per connection: %d emails', 'robotstxt-smtp-newsletter' ),
$optimal['max_per_connection']
);
if ( 'too_low' === $max_conn_status ) {
echo '<br>&nbsp;&nbsp;&nbsp;' . sprintf(
/* translators: %d: current max per connection */
esc_html__( 'Current (%d) is too low. Increase to reduce connection overhead.', 'robotstxt-smtp-newsletter' ),
$current_max_conn
);
} elseif ( 'too_high' === $max_conn_status ) {
echo '<br>&nbsp;&nbsp;&nbsp;' . sprintf(
/* translators: %d: current max per connection */
esc_html__( 'Current (%d) is too high and may cause SMTP timeouts. Reduce it.', 'robotstxt-smtp-newsletter' ),
$current_max_conn
);
} elseif ( 'suboptimal' === $max_conn_status ) {
echo '<br>&nbsp;&nbsp;&nbsp;' . sprintf(
/* translators: 1: current max per connection, 2: recommended max per connection */
esc_html__( 'Current (%1$d) could be optimized. Consider changing to %2$d.', 'robotstxt-smtp-newsletter' ),
$current_max_conn,
$optimal['max_per_connection']
);
}
}
}
echo '</strong></p>';
echo '</p></div>';
}
/**
* Render enabled field.
*
* @return void
*/
public function render_enabled_field() {
$options = get_option( self::OPTION_NAME, array() );
$enabled = ! empty( $options['enabled'] );
?>
<label>
<input type="checkbox" name="<?php echo esc_attr( self::OPTION_NAME ); ?>[enabled]" value="1" <?php checked( $enabled ); ?>>
<?php esc_html_e( 'Use SMTP (by ROBOTSTXT) for Newsletter delivery', 'robotstxt-smtp-newsletter' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'When enabled, Newsletter will use SMTP (by ROBOTSTXT) configuration with SMTPKeepAlive for efficient bulk sending.', 'robotstxt-smtp-newsletter' ); ?>
</p>
<?php
}
/**
* Render batch size field.
*
* @return void
*/
public function render_batch_size_field() {
$options = get_option( self::OPTION_NAME, array() );
$batch_size = $options['batch_size'] ?? 10;
?>
<input type="number" name="<?php echo esc_attr( self::OPTION_NAME ); ?>[batch_size]" value="<?php echo esc_attr( $batch_size ); ?>" min="1" max="10000" class="small-text">
<p class="description">
<?php esc_html_e( 'Number of emails to send in each batch. Higher values improve performance with SMTPKeepAlive.', 'robotstxt-smtp-newsletter' ); ?><br>
<?php esc_html_e( 'See calculated recommendations below based on your rate limits and cron interval.', 'robotstxt-smtp-newsletter' ); ?>
</p>
<?php
}
/**
* Render max per connection field.
*
* @return void
*/
public function render_max_per_connection_field() {
$options = get_option( self::OPTION_NAME, array() );
$max_per_connection = $options['max_per_connection'] ?? 50;
// Hide for Amazon SES.
if ( class_exists( '\Robotstxt_SMTP\Plugin' ) && \Robotstxt_SMTP\Plugin::is_amazon_ses_integration_active() ) {
?>
<p class="description">
<?php esc_html_e( 'Not applicable for Amazon SES (uses API instead of SMTP connections).', 'robotstxt-smtp-newsletter' ); ?>
</p>
<input type="hidden" name="<?php echo esc_attr( self::OPTION_NAME ); ?>[max_per_connection]" value="<?php echo esc_attr( $max_per_connection ); ?>">
<?php
return;
}
?>
<input type="number" name="<?php echo esc_attr( self::OPTION_NAME ); ?>[max_per_connection]" value="<?php echo esc_attr( $max_per_connection ); ?>" min="1" max="1000" class="small-text">
<p class="description">
<?php esc_html_e( 'Maximum emails to send before reconnecting to SMTP server. Prevents timeouts.', 'robotstxt-smtp-newsletter' ); ?><br>
<?php esc_html_e( 'See calculated recommendations below based on your rate limits.', 'robotstxt-smtp-newsletter' ); ?>
</p>
<?php
}
/**
* Calculate optimal batch size and max per connection based on rate limits and cron interval.
*
* @return array<string, mixed> Array with 'batch_size', 'max_per_connection', 'reasoning', and 'status'.
*/
private function calculate_optimal_settings() {
$plugin = \Robotstxt_SMTP_Newsletter\Plugin::get_instance();
$settings = $plugin->get_smtp_settings();
// Get Newsletter cron interval (default 300 seconds).
$cron_interval = defined( 'NEWSLETTER_CRON_INTERVAL' ) ? NEWSLETTER_CRON_INTERVAL : 300;
// Get rate limits.
$rate_per_second = absint( $settings['rate_limit_per_second'] ?? 0 );
$rate_per_hour = absint( $settings['rate_limit_per_hour'] ?? 0 );
$rate_per_day = absint( $settings['rate_limit_per_day'] ?? 0 );
// Check if Amazon SES is active.
$is_ses = \Robotstxt_SMTP\Plugin::is_amazon_ses_integration_active();
$optimal_batch = 10; // Default fallback.
$optimal_max_conn = 50; // Default fallback.
$reasoning = array();
$limiting_factor = '';
// If no rate limits are set.
if ( 0 === $rate_per_second && 0 === $rate_per_hour && 0 === $rate_per_day ) {
$optimal_batch = $is_ses ? 50 : 20;
$optimal_max_conn = $is_ses ? 0 : 100; // SES doesn't need max per connection.
$reasoning[] = __( 'No rate limits configured.', 'robotstxt-smtp-newsletter' );
$reasoning[] = $is_ses
? __( 'Using default for Amazon SES (50 emails/batch).', 'robotstxt-smtp-newsletter' )
: __( 'Using default for SMTP (20 emails/batch, 100 max per connection).', 'robotstxt-smtp-newsletter' );
$limiting_factor = 'default';
} else {
// Calculate based on rate limits.
$candidates = array();
// Per-second limit: Calculate how many emails we can send in one cron cycle.
if ( $rate_per_second > 0 ) {
// Reserve time for connection overhead (10s for SMTP, 2s for SES).
$overhead = $is_ses ? 2 : 10;
$available_time = max( 1, $cron_interval - $overhead );
$max_emails = floor( $rate_per_second * $available_time );
$candidates['per_second'] = $max_emails;
$reasoning[] = sprintf(
/* translators: 1: emails per second, 2: cron interval, 3: overhead, 4: calculated max */
__( 'Per-second limit: %1$d emails/sec × (%2$d sec - %3$d sec overhead) = %4$d emails/batch', 'robotstxt-smtp-newsletter' ),
$rate_per_second,
$cron_interval,
$overhead,
$max_emails
);
}
// Per-hour limit: Divide by number of cron executions per hour.
if ( $rate_per_hour > 0 ) {
$batches_per_hour = floor( 3600 / $cron_interval );
$max_emails = floor( $rate_per_hour / $batches_per_hour );
$candidates['per_hour'] = $max_emails;
$reasoning[] = sprintf(
/* translators: 1: emails per hour, 2: batches per hour, 3: calculated max */
__( 'Per-hour limit: %1$d emails/hour ÷ %2$d batches/hour = %3$d emails/batch', 'robotstxt-smtp-newsletter' ),
$rate_per_hour,
$batches_per_hour,
$max_emails
);
}
// Per-day limit: Divide by number of cron executions per day.
if ( $rate_per_day > 0 ) {
$batches_per_day = floor( 86400 / $cron_interval );
$max_emails = floor( $rate_per_day / $batches_per_day );
$candidates['per_day'] = $max_emails;
$reasoning[] = sprintf(
/* translators: 1: emails per day, 2: batches per day, 3: calculated max */
__( 'Per-day limit: %1$d emails/day ÷ %2$d batches/day = %3$d emails/batch', 'robotstxt-smtp-newsletter' ),
$rate_per_day,
$batches_per_day,
$max_emails
);
}
// The optimal batch size is the minimum of all candidates.
if ( ! empty( $candidates ) ) {
$optimal_batch = min( $candidates );
$limiting_factor = array_search( $optimal_batch, $candidates, true );
// Apply safety margin (90%).
$optimal_batch = max( 1, floor( $optimal_batch * 0.9 ) );
$reasoning[] = sprintf(
/* translators: 1: limiting factor, 2: final batch size */
__( 'Most restrictive limit: %1$s. Recommended batch size (with 10%% safety margin): %2$d', 'robotstxt-smtp-newsletter' ),
$limiting_factor,
$optimal_batch
);
}
// Calculate optimal max_per_connection for SMTP (not needed for SES).
if ( ! $is_ses ) {
// Max per connection should be 2-5x the batch size, or based on hourly limits.
if ( $rate_per_hour > 0 ) {
// Conservative: use 10 minutes of the hourly limit.
$optimal_max_conn = floor( ( $rate_per_hour / 6 ) * 0.9 );
} else {
$optimal_max_conn = min( 500, max( 50, $optimal_batch * 3 ) );
}
$reasoning[] = sprintf(
/* translators: %d: max per connection */
__( 'Max per connection (SMTP): %d emails before reconnecting to prevent timeouts.', 'robotstxt-smtp-newsletter' ),
$optimal_max_conn
);
}
}
return array(
'batch_size' => $optimal_batch,
'max_per_connection' => $optimal_max_conn,
'reasoning' => $reasoning,
'limiting_factor' => $limiting_factor,
'cron_interval' => $cron_interval,
'is_ses' => $is_ses,
);
}
/**
* Sanitize options.
*
* @param array<string, mixed> $options Raw options.
*
* @return array<string, mixed> Sanitized options.
*/
public function sanitize_options( $options ) {
$clean = array();
$clean['enabled'] = ! empty( $options['enabled'] );
// Batch size: Allow up to 10,000 for high-throughput scenarios (SES, high rate limits).
$clean['batch_size'] = max( 1, min( 10000, (int) ( $options['batch_size'] ?? 10 ) ) );
// Max per connection: Allow up to 1,000 for providers that support it.
$clean['max_per_connection'] = max( 1, min( 1000, (int) ( $options['max_per_connection'] ?? 50 ) ) );
return $clean;
}
}