Plugin Directory

Changeset 3762759


Ignore:
Timestamp:
10/24/2026 05:26:32 AM (2 hours ago)
Author:
bytecorestack
Message:

Release 1.3.0: redesigned admin, full email viewer, multi-channel alerts, always-on health checks

  • New Overview, Connection, Alerts, Reports and Email log screens; responsive dark admin with bundled fonts
  • Provider presets with one-minute setup and test email beside the connection form
  • Email log keeps 90 days with body, headers and attachment names; open any email on its own page
  • Alerts to Slack, Teams, Google Chat, Discord and a custom webhook at once; one alert per outage
  • Health checks every 15 minutes, including a sender check
  • Monthly report on the 1st of each month; redesigned report and test emails
  • Fixes: SMTP password survives salt rotation, honest health status, early translation notice, CSV formula injection, multisite uninstall
  • Removed bundled Chart.js, the Settings tab and resend
  • Assets: new screenshots 1-6, old screenshots 7-9 removed
Location:
bcs-smtp-manager
Files:
14 added
5 deleted
30 edited
1 copied

Legend:

Unmodified
Added
Removed
  • bcs-smtp-manager/assets/screenshot-1.png

    • Property svn:mime-type changed from application/octet-stream to image/png
  • bcs-smtp-manager/assets/screenshot-2.png

    • Property svn:mime-type changed from application/octet-stream to image/png
  • bcs-smtp-manager/assets/screenshot-3.png

    • Property svn:mime-type changed from application/octet-stream to image/png
  • bcs-smtp-manager/assets/screenshot-4.png

    • Property svn:mime-type changed from application/octet-stream to image/png
  • bcs-smtp-manager/assets/screenshot-5.png

    • Property svn:mime-type changed from application/octet-stream to image/png
  • bcs-smtp-manager/assets/screenshot-6.png

    • Property svn:mime-type changed from application/octet-stream to image/png
  • bcs-smtp-manager/tags/1.3.0/admin/class-bcs-smtp-admin.php

    r3652946 r3762759  
    146146                wp_enqueue_style(  'bcs-smtp-admin', BCS_SMTP_PLUGIN_URL . 'assets/css/admin.css', array(), BCS_SMTP_VERSION );
    147147
    148                 wp_enqueue_script( 'chartjs', BCS_SMTP_PLUGIN_URL . 'assets/js/chart.umd.min.js', array(), '4.4.0', true );
    149                 wp_enqueue_script( 'bcs-smtp-admin', BCS_SMTP_PLUGIN_URL . 'assets/js/admin.js', array( 'jquery', 'chartjs' ), BCS_SMTP_VERSION, true );
     148                wp_enqueue_script( 'bcs-smtp-admin', BCS_SMTP_PLUGIN_URL . 'assets/js/admin.js', array( 'jquery' ), BCS_SMTP_VERSION, true );
    150149
    151150                wp_localize_script( 'bcs-smtp-admin', 'bcsSmtpData', array(
    … …  
    158157                        'smtpAuth'             => (bool) get_option( 'bcs_smtp_auth', true ),
    159158                        'smtpEncryption'       => get_option( 'bcs_smtp_encryption', 'tls' ),
    160                         'monitorEnabled'       => (bool) get_option( BCS_SMTP_OPT_MONITOR_ENABLED, false ),
    161159                        'alertChannelEnabled'  => (bool) get_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED, false ),
    162                         'alertChannel'         => get_option( BCS_SMTP_OPT_ALERT_CHANNEL, 'none' ),
    163160                        'passwordIsSet'        => ! empty( get_option( 'bcs_smtp_password', '' ) ),
     161                        'firstRun'             => '' === trim( (string) get_option( 'bcs_smtp_host', '' ) ),
     162                        'i18n'                 => array(
     163                                'chReady'         => __( 'Active', 'bcs-smtp-manager' ),
     164                                'chAddUrl'        => __( 'Needs URL', 'bcs-smtp-manager' ),
     165                                'chOff'           => __( 'Off', 'bcs-smtp-manager' ),
     166                                /* translators: 1: channels switched on, 2: all channels. */
     167                                'chCount'         => __( '%1$s of %2$s on', 'bcs-smtp-manager' ),
     168                                'chCheckUrl'      => __( 'Check URL', 'bcs-smtp-manager' ),
     169                                'urlHttps'        => __( 'Paste the full address, starting with https://', 'bcs-smtp-manager' ),
     170                                /* translators: %s: alert channel name, e.g. Slack. */
     171                                'urlHost'         => __( 'This doesn’t look like a %s webhook URL.', 'bcs-smtp-manager' ),
     172                                'urlPublic'       => __( 'Use a public https:// address, not localhost or an IP address.', 'bcs-smtp-manager' ),
     173                                'saveFirst'       => __( 'Save your changes first. The test uses your saved settings.', 'bcs-smtp-manager' ),
     174                                /* translators: %s: number of alert channels. */
     175                                'nChannels'       => __( '%s channels', 'bcs-smtp-manager' ),
     176                                'saving'          => __( 'Saving…', 'bcs-smtp-manager' ),
     177                                'savingTesting'   => __( 'Saving and testing…', 'bcs-smtp-manager' ),
     178                                'sending'         => __( 'Sending…', 'bcs-smtp-manager' ),
     179                                'checking'        => __( 'Checking…', 'bcs-smtp-manager' ),
     180                                'saved'           => __( 'Saved.', 'bcs-smtp-manager' ),
     181                                'saveFailed'      => __( 'Couldn’t save. Please try again.', 'bcs-smtp-manager' ),
     182                                'requestFailed'   => __( 'The request didn’t reach WordPress. Check your connection and try again.', 'bcs-smtp-manager' ),
     183                                /* translators: %s: HTTP status code. */
     184                                'requestFailedCode' => __( 'WordPress returned an error (HTTP %s). Check the server error log or turn on WP_DEBUG.', 'bcs-smtp-manager' ),
     185                                'connFailed'      => __( 'Connection failed:', 'bcs-smtp-manager' ),
     186                                'enterRecipient'  => __( 'Enter an email address to send the test to.', 'bcs-smtp-manager' ),
     187                                'testFailed'      => __( 'The test email wasn’t sent.', 'bcs-smtp-manager' ),
     188                                'checkFailed'     => __( 'The health check couldn’t run.', 'bcs-smtp-manager' ),
     189                                'resent'          => __( 'Resent', 'bcs-smtp-manager' ),
     190                                'pwSaved'         => __( 'Saved and encrypted.', 'bcs-smtp-manager' ),
     191                                'pwChanging'      => __( 'Enter the new password. Leave it empty to keep the saved one.', 'bcs-smtp-manager' ),
     192                                'showPw'          => __( 'Show password', 'bcs-smtp-manager' ),
     193                                'hidePw'          => __( 'Hide password', 'bcs-smtp-manager' ),
     194                                'copy'            => __( 'Copy', 'bcs-smtp-manager' ),
     195                                'copied'          => __( 'Copied', 'bcs-smtp-manager' ),
     196                                'loadError'       => __( 'The email log couldn’t be loaded. Reload the page to try again.', 'bcs-smtp-manager' ),
     197                                /* translators: 1: current page, 2: total pages, 3: number of emails. */
     198                                'pageInfo'        => __( 'Page %1$s of %2$s · %3$s emails', 'bcs-smtp-manager' ),
     199                                'pickServer'      => __( 'Filled in when you pick a provider', 'bcs-smtp-manager' ),
     200                                'sent'            => __( 'Sent', 'bcs-smtp-manager' ),
     201                                'failed'          => __( 'Failed', 'bcs-smtp-manager' ),
     202                                'unconfirmed'     => __( 'Unconfirmed', 'bcs-smtp-manager' ),
     203                                /* translators: %s: date the week starts, e.g. "Oct 1". */
     204                                'weekOf'          => __( 'Week of %s', 'bcs-smtp-manager' ),
     205                                /* translators: %s: date the two-week period starts, e.g. "Oct 1". */
     206                                'twoWeeksOf'      => __( '2 weeks from %s', 'bcs-smtp-manager' ),
     207                                /* translators: %s: number of emails. */
     208                                'emailsCount'     => __( '%s emails', 'bcs-smtp-manager' ),
     209                                /* translators: %s: number of days in the period. */
     210                                'ofDays'          => __( 'of %s days', 'bcs-smtp-manager' ),
     211                                /* translators: %s: period length, e.g. "30 days". */
     212                                'lastPeriod'      => __( 'Last %s', 'bcs-smtp-manager' ),
     213                                'lessThanOne'     => __( 'Less than 1', 'bcs-smtp-manager' ),
     214                                /* translators: %s: the entries that are not valid email addresses. */
     215                                'badEmails'       => __( 'These aren’t valid email addresses: %s', 'bcs-smtp-manager' ),
     216                                'needEmail'       => __( 'Enter at least one email address to send the report to.', 'bcs-smtp-manager' ),
     217                                /* translators: %s: email address. */
     218                                'removeEmail'     => __( 'Remove %s', 'bcs-smtp-manager' ),
     219                                'notAnEmail'      => __( 'Not a valid email address', 'bcs-smtp-manager' ),
     220                                'oneRecipient'    => _x( 'person', 'count label: 1 person', 'bcs-smtp-manager' ),
     221                                'manyRecipients'  => _x( 'people', 'count label: 2 or more people', 'bcs-smtp-manager' ),
     222                                'oneDay'          => _x( 'day', 'streak length: 1 day', 'bcs-smtp-manager' ),
     223                                'manyDays'        => _x( 'days', 'streak length: 2 or more days', 'bcs-smtp-manager' ),
     224                                /* translators: %s: email provider name, e.g. Microsoft 365 / Outlook. */
     225                                'needsAuth'       => __( '%s needs a username and password.', 'bcs-smtp-manager' ),
     226                        ),
    164227                ) );
    165228        }
    … …  
    226289                update_option( 'bcs_smtp_username',   $username );
    227290
     291                // Keep the "Other SMTP server" details on their own, so switching to a preset provider
     292                // and saving doesn't lose them — choosing "Other" again brings them back.
     293                $provider = isset( $_POST['provider'] ) ? sanitize_key( wp_unslash( $_POST['provider'] ) ) : '';
     294                if ( 'other' === $provider && '' !== $host ) {
     295                        update_option( 'bcs_smtp_custom_server', array( 'host' => $host, 'port' => $port, 'encryption' => $encryption ), false );
     296                }
     297
    228298                // Only overwrite the stored password if the user submitted a non-empty value.
    229299                // Encrypted at rest — see BCS_SMTP_Mailer::encrypt_password()/get_smtp_password().
    … …  
    241311                                update_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, 0 );
    242312                        }
     313                        // This test counts as a check, so the next scheduled one is counted from it.
     314                        $this->monitor->schedule_next_check();
    243315                }
    244316
    … …  
    254326                                        /* translators: 1: SMTP host, 2: SMTP port. */
    255327                                        ? sprintf( __( 'Connection test passed — SMTP server is reachable on %1$s:%2$s.', 'bcs-smtp-manager' ), esc_html( $host ), esc_html( (string) $port ) )
    256                                         : ( isset( $test_result['error_msg'] ) ? $test_result['error_msg'] : __( 'Connection test failed.', 'bcs-smtp-manager' ) ),
     328                                        : ( isset( $test_result['error_msg'] ) ? esc_html( $test_result['error_msg'] ) : __( 'Connection test failed.', 'bcs-smtp-manager' ) ),
    257329                        );
    258330                }
    … …  
    271343                check_ajax_referer( 'bcs_smtp_nonce', 'nonce' );
    272344                if ( ! current_user_can( 'manage_options' ) ) { ob_end_clean(); wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); }
    273                 if ( $this->is_rate_limited( 'send_test', 60 ) ) { ob_end_clean(); wp_send_json_error( __( 'Please wait 60 seconds before sending another test email.', 'bcs-smtp-manager' ) ); }
    274345
    275346                $to = isset( $_POST['to'] ) ? sanitize_email( wp_unslash( $_POST['to'] ) ) : '';
    … …  
    278349                        wp_send_json_error( __( 'Invalid recipient email address.', 'bcs-smtp-manager' ) );
    279350                }
     351                if ( $this->is_rate_limited( 'send_test', 15 ) ) { ob_end_clean(); wp_send_json_error( __( 'Please wait 15 seconds before sending another test email.', 'bcs-smtp-manager' ) ); }
    280352
    281353                $result = $this->mailer->send_test_email( $to );
    … …  
    296368                                        $hint = ' ' . __( 'SMTP is configured but PHPMailer is still using PHP mail(). Try deactivating and reactivating this plugin, or check if another email plugin is overriding the settings.', 'bcs-smtp-manager' );
    297369                                } else {
    298                                         $hint = ' ' . __( 'Configure and enable SMTP in the Settings tab — PHP mail() is not available on this server.', 'bcs-smtp-manager' );
     370                                        $hint = ' ' . __( 'Set up and enable SMTP on the Connection tab — PHP mail() is not available on this server.', 'bcs-smtp-manager' );
    299371                                }
    300372                        } elseif ( 'not_configured' === $result['failure_type'] ) {
    301                                 $hint = ' ' . __( 'Go to Settings, enter your SMTP details, and save before sending a test.', 'bcs-smtp-manager' );
     373                                $hint = ' ' . __( 'Enter your server details on the Connection tab and save before sending a test.', 'bcs-smtp-manager' );
    302374                        }
    303375                        wp_send_json_error( array(
    304                                 'message'      => $result['error_msg'] . $hint,
     376                                'message'      => esc_html( $result['error_msg'] ) . $hint,
    305377                                'failure_type' => $type,
    306378                        ) );
    … …  
    317389                if ( $this->is_rate_limited( 'check_now', 30 ) ) { wp_send_json_error( __( 'Please wait 30 seconds before running another health check.', 'bcs-smtp-manager' ) ); }
    318390
    319                 $this->monitor->run_health_check();
    320 
    321                 $success  = (bool) get_option( BCS_SMTP_OPT_LAST_CHECK_SUCCESS );
    322                 $failures = (int)  get_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, 0 );
     391                $result   = $this->monitor->run_health_check( true );
     392                $success  = ! empty( $result['success'] );
     393                $failures = (int) get_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, 0 );
     394
     395                if ( $success ) {
     396                        $message = __( 'Health check passed — connected and authenticated with the SMTP server.', 'bcs-smtp-manager' );
     397                } else {
     398                        /* translators: 1: number of consecutive failures, 2: error detail. */
     399                        $message = sprintf( __( 'Health check failed (%1$d consecutive failure(s)): %2$s', 'bcs-smtp-manager' ), $failures, esc_html( isset( $result['error_msg'] ) ? $result['error_msg'] : '' ) );
     400                }
    323401
    324402                wp_send_json_success( array(
    325403                        'success'  => $success,
    326404                        'failures' => $failures,
    327                         'message'  => $success
    328                                 ? __( 'Health check passed — SMTP connection successful.', 'bcs-smtp-manager' )
    329                                 /* translators: %d: number of consecutive failures. */
    330                                 : sprintf( __( 'Health check failed — %d consecutive failure(s).', 'bcs-smtp-manager' ), $failures ),
     405                        'message'  => $message,
    331406                ) );
    332407        }
    … …  
    334409        /* ─────────────────────────────────────────────────────────────────── */
    335410        /* AJAX — Save monitor settings                                          */
    336         /* ─────────────────────────────────────────────────────────────────── */
    337 
    338         public function ajax_save_monitor() {
    339                 check_ajax_referer( 'bcs_smtp_nonce', 'nonce' );
    340                 if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); }
    341 
    342                 $enabled        = ! empty( $_POST['monitor_enabled'] );
    343                 $interval       = isset( $_POST['check_interval'] )    ? absint( $_POST['check_interval'] )    : 5;
    344                 $threshold      = isset( $_POST['failure_threshold'] ) ? absint( $_POST['failure_threshold'] ) : 2;
    345                 $max_alert      = isset( $_POST['max_alert_count'] )   ? absint( $_POST['max_alert_count'] )   : 0;
    346 
    347                 $interval  = max( 1, min( 1440, $interval ) );
    348                 $threshold = max( 1, min( 10, $threshold ) );
    349                 $max_alert = min( 100, $max_alert );
    350 
    351                 $old_interval = (int) get_option( BCS_SMTP_OPT_CHECK_INTERVAL, 5 );
    352 
    353                 update_option( BCS_SMTP_OPT_MONITOR_ENABLED,   $enabled );
    354                 update_option( BCS_SMTP_OPT_CHECK_INTERVAL,    $interval );
    355                 update_option( BCS_SMTP_OPT_FAILURE_THRESHOLD, $threshold );
    356                 update_option( BCS_SMTP_OPT_MAX_ALERT_COUNT,   $max_alert );
    357 
    358                 if ( $enabled && $interval !== $old_interval ) {
    359                         $next = wp_next_scheduled( 'bcs_smtp_health_check' );
    360                         if ( $next ) {
    361                                 wp_unschedule_event( $next, 'bcs_smtp_health_check' );
    362                         }
    363                 }
    364 
    365                 $this->monitor->maybe_schedule_event();
    366 
    367                 wp_send_json_success( array( 'message' => __( 'Monitor settings saved.', 'bcs-smtp-manager' ) ) );
    368         }
    369 
    370411        /* ─────────────────────────────────────────────────────────────────── */
    371412        /* AJAX — Save alert channel settings                                    */
    … …  
    377418
    378419                $channel_enabled = ! empty( $_POST['alert_channel_enabled'] );
    379                 $allowed         = array( 'none', 'slack', 'teams', 'google_chat', 'discord', 'custom' );
    380                 $channel         = isset( $_POST['alert_channel'] ) ? sanitize_key( wp_unslash( $_POST['alert_channel'] ) ) : 'none';
    381                 $channel         = in_array( $channel, $allowed, true ) ? $channel : 'none';
     420                $labels          = BCS_SMTP_Monitor::channel_labels();
     421                $posted          = isset( $_POST['alert_channels'] ) ? array_map( 'sanitize_key', (array) wp_unslash( $_POST['alert_channels'] ) ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized by array_map.
     422                $channels        = array_values( array_intersect( array_keys( $labels ), $posted ) );
    382423                $slack_url       = isset( $_POST['slack_webhook_url'] ) ? esc_url_raw( wp_unslash( $_POST['slack_webhook_url'] ) ) : '';
    383424                $teams_url       = isset( $_POST['teams_webhook_url'] ) ? esc_url_raw( wp_unslash( $_POST['teams_webhook_url'] ) ) : '';
    … …  
    391432                }
    392433                if ( '' !== $teams_url && ! BCS_SMTP_Monitor::is_allowed_webhook_host( $teams_url, 'teams' ) ) {
    393                         wp_send_json_error( __( 'Teams webhook URL must be a Microsoft Office/Logic Apps https:// URL.', 'bcs-smtp-manager' ) );
     434                        wp_send_json_error( __( 'Teams webhook URL must be a Microsoft Teams Workflows (Power Automate / Logic Apps) or Office 365 connector https:// URL.', 'bcs-smtp-manager' ) );
    394435                }
    395436                if ( '' !== $google_chat_url && ! BCS_SMTP_Monitor::is_allowed_webhook_host( $google_chat_url, 'google_chat' ) ) {
    … …  
    403444                }
    404445
     446                $channel_urls = array( 'slack' => $slack_url, 'teams' => $teams_url, 'google_chat' => $google_chat_url, 'discord' => $discord_url, 'custom' => $custom_url );
     447                if ( $channel_enabled && ! $channels ) {
     448                        wp_send_json_error( __( 'Choose at least one place to send alerts.', 'bcs-smtp-manager' ) );
     449                }
     450                foreach ( $channel_enabled ? $channels : array() as $ch ) {
     451                        if ( '' === $channel_urls[ $ch ] ) {
     452                                /* translators: %s: alert channel name, e.g. Slack. */
     453                                wp_send_json_error( sprintf( __( 'Enter the %s webhook URL.', 'bcs-smtp-manager' ), $labels[ $ch ] ) );
     454                        }
     455                }
     456
    405457                update_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED,    $channel_enabled );
    406                 update_option( BCS_SMTP_OPT_ALERT_CHANNEL,            $channel );
     458                update_option( BCS_SMTP_OPT_ALERT_CHANNEL,            $channels );
    407459                update_option( BCS_SMTP_OPT_SLACK_WEBHOOK_URL,        $slack_url );
    408460                update_option( BCS_SMTP_OPT_TEAMS_WEBHOOK_URL,        $teams_url );
    … …  
    427479                }
    428480
    429                 $channel = get_option( BCS_SMTP_OPT_ALERT_CHANNEL, 'none' );
    430                 if ( 'none' === $channel ) {
    431                         wp_send_json_error( __( 'No alert channel configured.', 'bcs-smtp-manager' ) );
    432                 }
    433481
    434482                $mock_result = array(
    … …  
    442490                );
    443491
    444                 $this->monitor->send_alert( $mock_result, 2, false );
    445                 $channel_labels = array( 'slack' => 'Slack', 'teams' => 'Teams', 'google_chat' => 'Google Chat', 'discord' => 'Discord', 'custom' => 'Custom Webhook' );
    446                 $channel_label  = isset( $channel_labels[ $channel ] ) ? $channel_labels[ $channel ] : ucfirst( $channel );
    447                 /* translators: %s: alert channel name (e.g. Slack, Teams, Google Chat). */
    448                 wp_send_json_success( array( 'message' => sprintf( __( 'Test alert dispatched via %s channel.', 'bcs-smtp-manager' ), $channel_label ) ) );
     492                $results = $this->monitor->send_alert( $mock_result, 2, false, true );
     493                if ( is_wp_error( $results ) ) {
     494                        wp_send_json_error( esc_html( $results->get_error_message() ) );
     495                }
     496
     497                $labels = BCS_SMTP_Monitor::channel_labels();
     498                $ok     = array_keys( array_filter( $results, function ( $r ) { return true === $r; } ) );
     499                $errors = array();
     500                foreach ( $results as $ch => $r ) {
     501                        if ( is_wp_error( $r ) ) {
     502                                /* translators: 1: alert channel name, 2: error message. */
     503                                $errors[] = sprintf( __( '%1$s: %2$s', 'bcs-smtp-manager' ), $labels[ $ch ], $r->get_error_message() );
     504                        }
     505                }
     506                /* translators: %s: list of alert channels, e.g. "Slack and Discord". */
     507                $delivered = $ok ? sprintf( __( 'Test alert delivered to %s.', 'bcs-smtp-manager' ), BCS_SMTP_Monitor::channel_list_label( $ok ) ) : '';
     508                if ( ! $errors ) {
     509                        wp_send_json_success( array( 'message' => $delivered ) );
     510                }
     511                /* translators: %s: one or more "Channel: error" entries. */
     512                $failed = sprintf( __( 'Not delivered — %s', 'bcs-smtp-manager' ), implode( '; ', $errors ) );
     513                wp_send_json_error( esc_html( trim( $delivered . ' ' . $failed ) ) );
    449514        }
    450515
    … …  
    469534                ob_start();
    470535                if ( empty( $result['rows'] ) ) {
    471                         echo '<tr><td colspan="7" style="text-align:center;padding:24px;color:var(--bcs-smtp-text-3)">' . esc_html__( 'No emails found matching your filters.', 'bcs-smtp-manager' ) . '</td></tr>';
     536                        $filtered = '' !== $args['search'] || '' !== $args['status'];
     537                        echo '<tr><td colspan="5" class="bcs-smtp-table-empty"><strong>' . esc_html( $filtered ? __( 'No emails match these filters', 'bcs-smtp-manager' ) : __( 'No emails in this period', 'bcs-smtp-manager' ) ) . '</strong><span>' . esc_html( $filtered ? __( 'Try a wider date range or clear the search.', 'bcs-smtp-manager' ) : __( 'Every email WordPress sends will be listed here with its delivery status.', 'bcs-smtp-manager' ) ) . '</span></td></tr>';
    472538                } else {
     539                        $last_day = '';
    473540                        foreach ( $result['rows'] as $row ) {
     541                                $day = mysql2date( 'Y-m-d', $row['sent_at'] );
     542                                if ( $day !== $last_day ) {
     543                                        echo '<tr class="bcs-smtp-day-row"><th colspan="5" scope="colgroup">' . esc_html( $this->log_day_label( $day ) ) . '</th></tr>';
     544                                        $last_day = $day;
     545                                }
    474546                                $this->render_email_log_row( $row );
    475547                        }
    … …  
    478550
    479551                wp_send_json_success( array(
    480                         'html'  => $html,
    481                         'total' => $result['total'],
    482                         'pages' => $result['pages'],
    483                         'page'  => $result['page'],
     552                        'html'   => $html,
     553                        'counts' => $this->db->count_email_log_by_status( $args['date_range'], $args['search'] ),
     554                        'total'  => $result['total'],
     555                        'pages'  => $result['pages'],
     556                        'page'   => $result['page'],
    484557                ) );
    485558        }
    … …  
    495568                $date_range = isset( $_POST['date_range'] ) ? sanitize_key( wp_unslash( $_POST['date_range'] ) ) : '30days';
    496569
    497                 $summary    = $this->db->get_email_summary( $date_range );
     570                list( $own, $own_domains ) = $this->internal_recipients();
     571                $summary    = $this->db->get_email_summary_with_recipients( $date_range, $own, $own_domains );
    498572                $chart_data = $this->db->get_email_chart_data( $date_range );
    499573
    … …  
    509583        /* ─────────────────────────────────────────────────────────────────── */
    510584
     585        /**
     586         * A CSV cell that spreadsheet apps won't run as a formula. Subjects and addresses can come
     587         * from visitors (contact forms), so a value starting with =, +, -, @, tab or CR gets a
     588         * leading apostrophe, which Excel and Google Sheets show as plain text.
     589         *
     590         * @param  mixed $value
     591         * @return string
     592         */
     593        private static function csv_cell( $value ) {
     594                $value = (string) $value;
     595                return ( '' !== $value && false !== strpos( "=+-@\t\r", $value[0] ) ) ? "'" . $value : $value;
     596        }
     597
    511598        public function handle_export_csv() {
    512599                if ( ! isset( $_GET['bcs_smtp_action'] ) || 'export_csv' !== $_GET['bcs_smtp_action'] ) {
    513600                        return;
    514601                }
    515                 if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['nonce'] ) ), 'bcs_smtp_export' ) ) {
    516                         wp_die( esc_html__( 'Security check failed.', 'bcs-smtp-manager' ) );
    517                 }
    518602                if ( ! current_user_can( 'manage_options' ) ) {
    519                         wp_die( esc_html__( 'Permission denied.', 'bcs-smtp-manager' ) );
     603                        wp_die( esc_html__( 'Permission denied.', 'bcs-smtp-manager' ), '', array( 'response' => 403 ) );
     604                }
     605                if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'bcs_smtp_export' ) ) {
     606                        wp_die( esc_html__( 'This export link has expired. Reload the page and try again.', 'bcs-smtp-manager' ), '', array( 'response' => 403 ) );
    520607                }
    521608
    … …  
    537624                $out = fopen( 'php://output', 'w' );
    538625                fwrite( $out, "\xEF\xBB\xBF" ); // UTF-8 BOM for Excel compatibility
    539                 fputcsv( $out, array( 'ID', 'Date', 'Subject', 'From', 'To', 'Status', 'Error' ) );
     626                // Delimiter, enclosure and escape are passed explicitly: PHP 8.4 deprecates relying on the default escape.
     627                fputcsv( $out, array( 'ID', 'Date', 'Subject', 'From', 'To', 'Status', 'Error' ), ',', '"', '\\' );
    540628                foreach ( $rows as $row ) {
    541629                        fputcsv( $out, array(
    542630                                $row['id'],
    543631                                $row['sent_at'],
    544                                 $row['subject'],
    545                                 $row['from_address'],
    546                                 $row['to_address'],
     632                                self::csv_cell( $row['subject'] ),
     633                                self::csv_cell( $row['from_address'] ),
     634                                self::csv_cell( $row['to_address'] ),
    547635                                $row['status'],
    548                                 $row['error_msg'],
    549                         ) );
     636                                self::csv_cell( $row['error_msg'] ),
     637                        ), ',', '"', '\\' );
    550638                }
    551639                fclose( $out );
    … …  
    555643
    556644        /* ─────────────────────────────────────────────────────────────────── */
    557         /* AJAX — Save log retention / auto-prune settings                      */
    558         /* ─────────────────────────────────────────────────────────────────── */
    559 
    560         public function ajax_save_log_prune() {
    561                 check_ajax_referer( 'bcs_smtp_nonce', 'nonce' );
    562                 if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); }
    563 
    564                 $allowed_modes = array( 'days', 'records' );
    565                 $mode = isset( $_POST['prune_mode'] ) ? sanitize_key( wp_unslash( $_POST['prune_mode'] ) ) : 'days';
    566 
    567                 if ( ! in_array( $mode, $allowed_modes, true ) ) { $mode = 'days'; }
    568 
    569                 // Values are locked — 30 days or 1000 records. Server always enforces these limits.
    570                 $value = ( 'records' === $mode ) ? 1000 : 30;
    571 
    572                 update_option( 'bcs_log_prune_mode',  $mode );
    573                 update_option( 'bcs_log_prune_value', $value );
    574 
    575                 wp_send_json_success( array( 'message' => __( 'Log retention settings saved.', 'bcs-smtp-manager' ) ) );
    576         }
    577 
    578         /* ─────────────────────────────────────────────────────────────────── */
    579         /* Cron callback — run auto-prune                                        */
     645        /* Cron callback — prune the email log                                   */
    580646        /* ─────────────────────────────────────────────────────────────────── */
    581647
    582648        public function run_log_pruning() {
    583                 $mode  = get_option( 'bcs_log_prune_mode',  'days' );
    584                 $value = (int) get_option( 'bcs_log_prune_value', 30 );
    585                 $this->db->prune_email_log( $mode, $value );
    586 
    587                 // Failed-email bodies get their own, typically shorter, retention window.
    588                 $this->db->clear_old_failed_bodies( (int) get_option( BCS_SMTP_OPT_FAILED_BODY_RETENTION_DAYS, 7 ) );
    589         }
    590 
    591         /* ─────────────────────────────────────────────────────────────────── */
    592         /* AJAX — Resend a failed email                                          */
    593         /* ─────────────────────────────────────────────────────────────────── */
    594 
    595         public function ajax_resend_email() {
    596                 check_ajax_referer( 'bcs_smtp_nonce', 'nonce' );
    597                 if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); }
    598 
    599                 $log_id = isset( $_POST['log_id'] ) ? absint( $_POST['log_id'] ) : 0;
    600                 if ( ! $log_id ) { wp_send_json_error( __( 'Missing log entry ID.', 'bcs-smtp-manager' ) ); }
    601 
    602                 if ( $this->is_rate_limited( 'resend_' . $log_id, 30 ) ) {
    603                         wp_send_json_error( __( 'Please wait 30 seconds before resending this email again.', 'bcs-smtp-manager' ) );
    604                 }
    605 
    606                 $result = $this->mailer->resend_stored_email( $log_id );
    607                 if ( $result['success'] ) {
    608                         wp_send_json_success( array( 'message' => $result['message'] ) );
    609                 }
    610                 wp_send_json_error( $result['message'] );
    611         }
    612 
    613         /* ─────────────────────────────────────────────────────────────────── */
    614         /* AJAX — Save Resend settings                                           */
    615         /* ─────────────────────────────────────────────────────────────────── */
    616 
    617         public function ajax_save_resend_settings() {
    618                 check_ajax_referer( 'bcs_smtp_nonce', 'nonce' );
    619                 if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); }
    620 
    621                 $enabled = ! empty( $_POST['store_failed_body'] );
    622                 $days    = isset( $_POST['retention_days'] ) ? absint( $_POST['retention_days'] ) : 7;
    623                 $days    = max( 1, min( 30, $days ) );
    624 
    625                 update_option( BCS_SMTP_OPT_STORE_FAILED_BODY, $enabled );
    626                 update_option( BCS_SMTP_OPT_FAILED_BODY_RETENTION_DAYS, $days );
    627 
    628                 wp_send_json_success( array( 'message' => __( 'Resend settings saved.', 'bcs-smtp-manager' ) ) );
    629         }
    630 
    631         /* ─────────────────────────────────────────────────────────────────── */
    632         /* AJAX — Save report schedule                                           */
    633         /* ─────────────────────────────────────────────────────────────────── */
     649                // After updating from a version that kept everything, wait out the grace period first.
     650                $grace_until = (int) get_option( 'bcs_smtp_prune_after', 0 );
     651                if ( $grace_until > time() ) {
     652                        return;
     653                }
     654                if ( $grace_until ) {
     655                        delete_option( 'bcs_smtp_prune_after' );
     656                }
     657                // The log keeps the last 90 days; the reports and charts never look further back.
     658                $this->db->prune_email_log( 'days', BCS_SMTP_LOG_RETENTION_DAYS );
     659        }
     660
     661        /* ─────────────────────────────────────────────────────────────────── */
     662        /* Report recipients + AJAX — Save report schedule                       */
     663        /* ─────────────────────────────────────────────────────────────────── */
     664
     665        /**
     666         * Split a comma-separated recipient list into valid and invalid entries.
     667         *
     668         * @param string $list Raw input.
     669         * @return array { valid: string[] (lower-cased, de-duplicated), invalid: string[] }
     670         */
     671        private function parse_report_recipients( $list ) {
     672                $valid   = array();
     673                $invalid = array();
     674                foreach ( explode( ',', (string) $list ) as $part ) {
     675                        $part = trim( $part );
     676                        if ( '' === $part ) {
     677                                continue;
     678                        }
     679                        if ( is_email( $part ) ) {
     680                                $valid[ strtolower( $part ) ] = strtolower( $part );
     681                        } else {
     682                                $invalid[] = $part;
     683                        }
     684                }
     685                return array( 'valid' => array_values( $valid ), 'invalid' => $invalid );
     686        }
     687
     688        /**
     689         * Error text naming the entries that are not email addresses.
     690         *
     691         * @param string[] $invalid Bad entries.
     692         * @return string
     693         */
     694        private function invalid_recipients_message( $invalid ) {
     695                return sprintf(
     696                        /* translators: %s: the entries that are not valid email addresses, comma-separated. */
     697                        _n( 'This isn’t a valid email address: %s', 'These aren’t valid email addresses: %s', count( $invalid ), 'bcs-smtp-manager' ),
     698                        esc_html( implode( ', ', $invalid ) )
     699                );
     700        }
    634701
    635702        public function ajax_save_report_schedule() {
    … …  
    637704                if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); }
    638705
    639                 $enabled   = ! empty( $_POST['enabled'] );
    640                 $emails    = isset( $_POST['emails'] )    ? sanitize_text_field( wp_unslash( $_POST['emails'] ) )    : '';
    641                 $frequency = isset( $_POST['frequency'] ) ? sanitize_key( wp_unslash( $_POST['frequency'] ) )        : 'weekly';
    642 
    643                 if ( ! in_array( $frequency, array( 'daily', 'weekly', 'monthly' ), true ) ) {
    644                         $frequency = 'weekly';
    645                 }
     706                $enabled = ! empty( $_POST['enabled'] );
     707                $parsed  = $this->parse_report_recipients( isset( $_POST['emails'] ) ? sanitize_text_field( wp_unslash( $_POST['emails'] ) ) : '' );
     708
     709                if ( ! empty( $parsed['invalid'] ) ) {
     710                        wp_send_json_error( array( 'message' => $this->invalid_recipients_message( $parsed['invalid'] ), 'field' => 'emails' ) );
     711                }
     712                if ( $enabled && empty( $parsed['valid'] ) ) {
     713                        wp_send_json_error( array( 'message' => __( 'Enter at least one email address to send the report to.', 'bcs-smtp-manager' ), 'field' => 'emails' ) );
     714                }
     715                $emails = implode( ', ', $parsed['valid'] );
    646716
    647717                update_option( 'bcs_report_schedule_enabled', $enabled );
    648718                update_option( 'bcs_report_emails',           $emails );
    649                 update_option( 'bcs_report_frequency',        $frequency );
    650 
    651                 // Re-schedule the cron event.
    652                 $timestamp = wp_next_scheduled( 'bcs_smtp_send_report' );
    653                 if ( $timestamp ) {
    654                         wp_unschedule_event( $timestamp, 'bcs_smtp_send_report' );
    655                 }
    656                 if ( $enabled && ! empty( $emails ) ) {
    657                         $recurrence = ( 'daily' === $frequency ) ? 'daily' : ( ( 'monthly' === $frequency ) ? 'bcs_smtp_monthly' : 'weekly' );
    658                         wp_schedule_event( time() + DAY_IN_SECONDS, $recurrence, 'bcs_smtp_send_report' );
    659                 }
    660 
    661                 wp_send_json_success( array( 'message' => __( 'Report schedule saved.', 'bcs-smtp-manager' ) ) );
     719                // Reports are monthly, on the 1st at 9:00; options from the old schedule picker are retired.
     720
     721                $next = $this->schedule_next_report();
     722
     723                wp_send_json_success( array(
     724                        'message' => __( 'Report schedule saved.', 'bcs-smtp-manager' ),
     725                        'next'    => $next ? $this->describe_next_report( $next ) : '',
     726                        'emails'  => $emails,
     727                ) );
    662728        }
    663729
    … …  
    669735                check_ajax_referer( 'bcs_smtp_nonce', 'nonce' );
    670736                if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); }
     737                $parsed = $this->parse_report_recipients( isset( $_POST['emails'] ) ? sanitize_text_field( wp_unslash( $_POST['emails'] ) ) : '' );
     738                if ( ! empty( $parsed['invalid'] ) ) {
     739                        wp_send_json_error( array( 'message' => $this->invalid_recipients_message( $parsed['invalid'] ), 'field' => 'emails' ) );
     740                }
     741                if ( empty( $parsed['valid'] ) ) {
     742                        wp_send_json_error( array( 'message' => __( 'Enter at least one email address to send the report to.', 'bcs-smtp-manager' ), 'field' => 'emails' ) );
     743                }
    671744                if ( $this->is_rate_limited( 'send_report', 120 ) ) { wp_send_json_error( __( 'Please wait 2 minutes before sending another report.', 'bcs-smtp-manager' ) ); }
    672745
    673                 $emails    = isset( $_POST['emails'] ) ? sanitize_text_field( wp_unslash( $_POST['emails'] ) ) : '';
    674                 $frequency = get_option( 'bcs_report_frequency', 'weekly' );
    675                 $range_map = array( 'daily' => 'today', 'weekly' => '7days', 'monthly' => 'this_month' );
    676                 $date_range = isset( $range_map[ $frequency ] ) ? $range_map[ $frequency ] : '7days';
    677 
    678                 if ( empty( $emails ) ) {
    679                         wp_send_json_error( __( 'No recipient email addresses entered.', 'bcs-smtp-manager' ) );
    680                 }
     746                $emails     = implode( ', ', $parsed['valid'] );
     747                // "Send now" shows the current month so far; the scheduled report on the 1st covers the full previous month.
     748                $date_range = 'this_month';
    681749
    682750                $result = $this->send_report_to( $emails, $date_range );
    … …  
    699767        public function send_scheduled_report() {
    700768                if ( ! get_option( 'bcs_report_schedule_enabled', false ) ) {
     769                        wp_clear_scheduled_hook( 'bcs_smtp_send_report' );
    701770                        return;
    702771                }
    703                 $emails    = get_option( 'bcs_report_emails', '' );
    704                 $frequency = get_option( 'bcs_report_frequency', 'weekly' );
    705                 $range_map = array( 'daily' => 'today', 'weekly' => '7days', 'monthly' => 'this_month' );
    706                 $date_range = isset( $range_map[ $frequency ] ) ? $range_map[ $frequency ] : '7days';
     772                $emails = get_option( 'bcs_report_emails', '' );
    707773                if ( ! empty( $emails ) ) {
    708                         $this->send_report_to( $emails, $date_range );
    709                 }
     774                        $this->send_report_to( $emails, $this->report_range_for() );
     775                }
     776                // One-off events chained at the chosen time; this also replaces the interval-based
     777                // recurring event older versions created, which drifted from the save time.
     778                $this->schedule_next_report();
     779        }
     780
     781        /**
     782         * The period a report covers: reports go out on the 1st, so they cover the whole previous month.
     783         *
     784         * @return string Date-range key understood by BCS_SMTP_DB.
     785         */
     786        private function report_range_for() {
     787                return 'last_month';
     788        }
     789
     790        /**
     791         * Next send time for the saved schedule, in the site's timezone.
     792         *
     793         * @return int|null Unix timestamp, or null when no schedule is active.
     794         */
     795        private function next_report_timestamp() {
     796                if ( ! get_option( 'bcs_report_schedule_enabled', false ) || '' === trim( (string) get_option( 'bcs_report_emails', '' ) ) ) {
     797                        return null;
     798                }
     799                $now = new DateTimeImmutable( 'now', wp_timezone() );
     800                $at  = $now->modify( 'first day of this month' )->setTime( 9, 0 );
     801                if ( $at <= $now ) {
     802                        $at = $at->modify( 'first day of next month' );
     803                }
     804                return $at->getTimestamp();
     805        }
     806
     807        /**
     808         * Replace any pending report event with one at the next scheduled time.
     809         *
     810         * @return int|null The booked timestamp.
     811         */
     812        /**
     813         * Keep the monthly report booked while reports are on: the 1.3 upgrade replaces the old
     814         * daily/weekly event, and a cron run that never happened can leave nothing queued.
     815         */
     816        public function ensure_report_scheduled() {
     817                if ( get_option( 'bcs_report_schedule_enabled', false ) && ! wp_next_scheduled( 'bcs_smtp_send_report' ) ) {
     818                        $this->schedule_next_report();
     819                }
     820        }
     821
     822        private function schedule_next_report() {
     823                wp_clear_scheduled_hook( 'bcs_smtp_send_report' );
     824                $next = $this->next_report_timestamp();
     825                if ( $next ) {
     826                        wp_schedule_single_event( $next, 'bcs_smtp_send_report' );
     827                }
     828                return $next;
     829        }
     830
     831        /**
     832         * "Next report: Mon, Oct 27 at 9:00 am" in the site's date/time format.
     833         *
     834         * @param int $timestamp Unix timestamp.
     835         * @return string
     836         */
     837        private function describe_next_report( $timestamp ) {
     838                /* translators: 1: date, e.g. "Mon, Oct 27", 2: time, e.g. "9:00 am". */
     839                return sprintf( __( 'Next report: %1$s at %2$s', 'bcs-smtp-manager' ), wp_date( 'D, M j', $timestamp ), wp_date( get_option( 'time_format', 'g:i a' ), $timestamp ) );
    710840        }
    711841
    … …  
    723853                $summary    = $this->db->get_email_summary( $date_range );
    724854                $chart_data = $this->db->get_email_chart_data( $date_range );
    725                 $subject    = sprintf( '[%s] Email Delivery Report', get_bloginfo( 'name' ) );
     855                /* translators: 1: site name, 2: period, e.g. "October 2026". */
     856                $subject    = sprintf( __( '%1$s email report: %2$s', 'bcs-smtp-manager' ), get_bloginfo( 'name' ), $this->report_period_label( $date_range ) );
    726857                $body       = $this->build_report_html_email( $summary, $chart_data, $date_range );
    727858
    … …  
    739870        }
    740871
     872        /**
     873         * "October 2026", or "October 2026 (so far)" for the current month.
     874         *
     875         * @param string $date_range Date-range key.
     876         * @return string
     877         */
     878        private function report_period_label( $date_range ) {
     879                if ( 'last_month' === $date_range ) {
     880                        return wp_date( 'F Y', strtotime( 'first day of last month', current_time( 'timestamp' ) ) );
     881                }
     882                if ( 'this_month' === $date_range ) {
     883                        /* translators: %s: month and year, e.g. "October 2026". */
     884                        return sprintf( __( '%s (so far)', 'bcs-smtp-manager' ), wp_date( 'F Y' ) );
     885                }
     886                return __( 'Last 30 days', 'bcs-smtp-manager' );
     887        }
     888
     889        /**
     890         * Days the report chart covers, as Y-m-d keys in site time, plus whether each is still to come.
     891         * A month report shows every day of that month, so the shape reads the same every time.
     892         *
     893         * @param string $date_range Date-range key.
     894         * @param array  $labels     Day keys the chart data has.
     895         * @return array List of array( 'Y-m-d', is_future ).
     896         */
     897        private function report_days( $date_range, $labels ) {
     898                $now = current_time( 'timestamp' );
     899                if ( 'last_month' === $date_range || 'this_month' === $date_range ) {
     900                        $first = 'last_month' === $date_range ? strtotime( 'first day of last month', $now ) : $now;
     901                        $today = gmdate( 'Y-m-d', $now );
     902                        $days  = array();
     903                        for ( $d = 1, $n = (int) gmdate( 't', $first ); $d <= $n; $d++ ) {
     904                                $key    = gmdate( 'Y-m-', $first ) . sprintf( '%02d', $d );
     905                                $days[] = array( $key, $key > $today );
     906                        }
     907                        return $days;
     908                }
     909                return array_map( function ( $key ) { return array( $key, false ); }, array_slice( $labels, -31 ) );
     910        }
     911
    741912        private function build_report_html_email( $summary, $chart_data, $date_range ) {
    742                 $site_name = get_bloginfo( 'name' );
    743                 $site_url  = get_bloginfo( 'url' );
    744                 $timestamp = current_time( 'M j, Y \a\t g:i A' );
    745                 $range_labels = array(
    746                         'today'      => 'Today',
    747                         '7days'      => 'Last 7 Days',
    748                         '30days'     => 'Last 30 Days',
    749                         'this_week'  => 'This Week',
    750                         'this_month' => 'This Month',
    751                         '3months'    => 'Last 3 Months',
    752                         '6months'    => 'Last 6 Months',
    753                         'year'       => 'Last Year',
    754                         'all'        => 'All Time',
     913                $T       = 'BCS_SMTP_Email_Template';
     914                $period  = $this->report_period_label( $date_range );
     915                $total   = (int) $summary['total'];
     916                $sent    = (int) $summary['success'];
     917                $failed  = (int) $summary['failed'];
     918                $unconf  = (int) $summary['unconfirmed'];
     919                $rate    = $total > 0 ? round( ( $sent / $total ) * 100, 1 ) : 0;
     920                $rate_tx = rtrim( rtrim( number_format_i18n( $rate, 1 ), '0' ), '.,' );
     921                $colors  = array( 'sent' => '#16a36a', 'failed' => '#e5484d', 'unconfirmed' => '#f0a020' );
     922
     923                // Headline verdict for the period.
     924                if ( 0 === $total ) {
     925                        $tone    = 'info';
     926                        $verdict = __( 'No email was sent', 'bcs-smtp-manager' );
     927                        $note    = __( 'Nothing went out in this period. If your site has contact forms or a shop, send a test email to make sure the connection still works.', 'bcs-smtp-manager' );
     928                } elseif ( 0 === $failed ) {
     929                        $tone    = 'ok';
     930                        $verdict = __( 'Every email was delivered', 'bcs-smtp-manager' );
     931                        /* translators: %s: number of emails. */
     932                        $note    = sprintf( _n( '%s email went out without a single failure.', 'All %s emails went out without a single failure.', $total, 'bcs-smtp-manager' ), number_format_i18n( $total ) );
     933                } else {
     934                        $tone    = $rate >= 90 ? 'warn' : 'bad';
     935                        /* translators: %s: number of failed emails. */
     936                        $verdict = sprintf( _n( '%s email failed', '%s emails failed', $failed, 'bcs-smtp-manager' ), number_format_i18n( $failed ) );
     937                        /* translators: 1: delivered count, 2: total count. */
     938                        $note    = sprintf( __( '%1$s of %2$s emails were delivered. The failed ones never reached their inbox.', 'bcs-smtp-manager' ), number_format_i18n( $sent ), number_format_i18n( $total ) );
     939                }
     940
     941                // Share-of-total bar.
     942                $bar = '';
     943                if ( $total > 0 ) {
     944                        foreach ( array( 'sent' => $sent, 'failed' => $failed, 'unconfirmed' => $unconf ) as $k => $v ) {
     945                                if ( $v > 0 ) {
     946                                        $bar .= '<td width="' . max( 1, round( $v / $total * 100 ) ) . '%" bgcolor="' . $colors[ $k ] . '" style="background:' . $colors[ $k ] . ';height:8px;font-size:0;line-height:0">&nbsp;</td>';
     947                                }
     948                        }
     949                } else {
     950                        $bar = '<td class="bcs-track" bgcolor="#e8ecf2" style="background:#e8ecf2;height:8px;font-size:0;line-height:0">&nbsp;</td>';
     951                }
     952
     953                // Stat columns.
     954                $stats = '';
     955                foreach ( array(
     956                        array( __( 'Delivered', 'bcs-smtp-manager' ), $sent, $colors['sent'] ),
     957                        array( __( 'Failed', 'bcs-smtp-manager' ), $failed, $colors['failed'] ),
     958                        array( __( 'Unconfirmed', 'bcs-smtp-manager' ), $unconf, $colors['unconfirmed'] ),
     959                ) as $i => $st ) {
     960                        $stats .= '<td width="33%" valign="top" class="bcs-line" style="padding:16px 0 0 ' . ( $i ? '16px;border-left:1px solid #edf0f5' : '0' ) . '">
     961          <div class="bcs-ink3" style="font-size:12px;color:#64748b"><span style="color:' . $st[2] . '">&#9679;</span>&nbsp;' . esc_html( $st[0] ) . '</div>
     962          <div class="bcs-ink" style="margin-top:4px;font-size:24px;font-weight:700;color:#0f172a;letter-spacing:-.3px">' . esc_html( number_format_i18n( $st[1] ) ) . '</div>
     963        </td>';
     964                }
     965
     966                // Daily column chart: every day of the period, stacked delivered / failed / unconfirmed.
     967                $by_day = array();
     968                foreach ( $chart_data['labels'] as $i => $key ) {
     969                        $by_day[ $key ] = array( (int) $chart_data['success'][ $i ], (int) $chart_data['failed'][ $i ], (int) $chart_data['unconfirmed'][ $i ] );
     970                }
     971                $days = $this->report_days( $date_range, $chart_data['labels'] );
     972                $max  = 1;
     973                foreach ( $days as $day ) {
     974                        if ( isset( $by_day[ $day[0] ] ) ) { $max = max( $max, array_sum( $by_day[ $day[0] ] ) ); }
     975                }
     976                $plot  = 96;
     977                $cols  = '';
     978                $width = round( 100 / max( 1, count( $days ) ), 2 );
     979                foreach ( $days as $day ) {
     980                        list( $s, $f, $u ) = isset( $by_day[ $day[0] ] ) ? $by_day[ $day[0] ] : array( 0, 0, 0 );
     981                        $stack = '';
     982                        if ( $day[1] ) {
     983                                $stack = '';
     984                        } elseif ( $s + $f + $u > 0 ) {
     985                                foreach ( array( array( $u, $colors['unconfirmed'] ), array( $f, $colors['failed'] ), array( $s, $colors['sent'] ) ) as $seg ) {
     986                                        if ( $seg[0] > 0 ) {
     987                                                $h      = max( 3, (int) round( $seg[0] / $max * $plot ) );
     988                                                $stack .= '<tr><td height="' . $h . '" bgcolor="' . $seg[1] . '" style="background:' . $seg[1] . ';height:' . $h . 'px;font-size:0;line-height:0">&nbsp;</td></tr>';
     989                                        }
     990                                }
     991                        } else {
     992                                $stack = '<tr><td height="2" class="bcs-track" bgcolor="#e3e8ef" style="background:#e3e8ef;height:2px;font-size:0;line-height:0">&nbsp;</td></tr>';
     993                        }
     994                        /* translators: 1: date, 2: delivered count, 3: failed count. */
     995                        $tip   = sprintf( __( '%1$s: %2$s delivered, %3$s failed', 'bcs-smtp-manager' ), wp_date( 'M j', strtotime( $day[0] . ' 12:00:00' ) ), $s, $f );
     996                        $cols .= '<td width="' . $width . '%" valign="bottom" title="' . esc_attr( $tip ) . '" style="padding:0 1px;height:' . $plot . 'px">'
     997                                . ( $stack ? '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">' . $stack . '</table>' : '' ) . '</td>';
     998                }
     999                $first_day = $days ? wp_date( 'M j', strtotime( $days[0][0] . ' 12:00:00' ) ) : '';
     1000                $mid_day   = $days ? wp_date( 'M j', strtotime( $days[ (int) floor( count( $days ) / 2 ) ][0] . ' 12:00:00' ) ) : '';
     1001                $last_day  = $days ? wp_date( 'M j', strtotime( $days[ count( $days ) - 1 ][0] . ' 12:00:00' ) ) : '';
     1002                $legend    = '';
     1003                foreach ( array( 'sent' => __( 'Delivered', 'bcs-smtp-manager' ), 'failed' => __( 'Failed', 'bcs-smtp-manager' ), 'unconfirmed' => __( 'Unconfirmed', 'bcs-smtp-manager' ) ) as $k => $label ) {
     1004                        $legend .= '<span style="white-space:nowrap;margin-left:12px"><span style="color:' . $colors[ $k ] . '">&#9632;</span>&nbsp;' . esc_html( $label ) . '</span>';
     1005                }
     1006
     1007                $body = '
     1008      <tr><td class="bcs-pad" style="padding:34px 40px 0">
     1009        ' . $T::pill( $verdict, $tone ) . '
     1010        <h1 class="bcs-ink bcs-h1" style="margin:16px 0 8px;font-size:28px;line-height:1.2;font-weight:700;letter-spacing:-.4px;color:#0f172a">' . esc_html( $period ) . '</h1>
     1011        <p class="bcs-ink2" style="margin:0;font-size:15px;line-height:1.65;color:#475569">' . esc_html( $note ) . '</p>
     1012      </td></tr>
     1013
     1014      <tr><td class="bcs-pad" style="padding:28px 40px 0">
     1015        <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
     1016          <td valign="bottom">
     1017            <div class="bcs-ink3" style="font-size:12px;color:#64748b">' . esc_html__( 'Delivery rate', 'bcs-smtp-manager' ) . '</div>
     1018            <div class="bcs-ink bcs-big" style="margin-top:2px;font-size:46px;line-height:1.05;font-weight:700;letter-spacing:-1.2px;color:#0f172a">' . esc_html( $total ? $rate_tx . '%' : '—' ) . '</div>
     1019          </td>
     1020          <td valign="bottom" align="right" class="bcs-ink3" style="font-size:13px;color:#64748b;padding-bottom:6px">' .
     1021                        /* translators: %s: number of emails. */
     1022                        esc_html( sprintf( _n( '%s email in total', '%s emails in total', $total, 'bcs-smtp-manager' ), number_format_i18n( $total ) ) ) . '</td>
     1023        </tr></table>
     1024        <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-top:14px;border-radius:4px;overflow:hidden"><tr>' . $bar . '</tr></table>
     1025        <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>' . $stats . '</tr></table>
     1026      </td></tr>
     1027
     1028      <tr><td class="bcs-pad" style="padding:32px 40px 0">
     1029        <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
     1030          <td class="bcs-ink" style="font-size:15px;font-weight:700;color:#0f172a">' . esc_html__( 'Day by day', 'bcs-smtp-manager' ) . '</td>
     1031          <td align="right" class="bcs-ink3 bcs-hide-sm" style="font-size:12px;color:#64748b">' . $legend . '</td>
     1032        </tr></table>
     1033        <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" class="bcs-line" style="margin-top:16px;border-bottom:1px solid #e3e8ef"><tr>' . $cols . '</tr></table>
     1034        <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-top:8px"><tr>
     1035          <td class="bcs-ink3" style="font-size:11px;color:#8a94a6">' . esc_html( $first_day ) . '</td>
     1036          <td class="bcs-ink3" align="center" style="font-size:11px;color:#8a94a6">' . esc_html( $mid_day ) . '</td>
     1037          <td class="bcs-ink3" align="right" style="font-size:11px;color:#8a94a6">' . esc_html( $last_day ) . '</td>
     1038        </tr></table>
     1039      </td></tr>
     1040
     1041      <tr><td class="bcs-pad" style="padding:28px 40px 36px">
     1042        ' . $T::button( $T::settings_url( $failed ? 'email-log' : 'reports' ), $failed ? __( 'Review failed emails', 'bcs-smtp-manager' ) : __( 'Open the full report', 'bcs-smtp-manager' ) ) . '
     1043      </td></tr>';
     1044
     1045                return $T::document( array(
     1046                        /* translators: %s: period, e.g. "October 2026". */
     1047                        'title'     => sprintf( __( 'Email report: %s', 'bcs-smtp-manager' ), $period ),
     1048                        /* translators: 1: period, 2: delivery-rate verdict, e.g. "3 emails failed". */
     1049                        'preheader' => sprintf( __( '%1$s: %2$s.', 'bcs-smtp-manager' ), $period, $total ? $verdict . ' · ' . $rate_tx . '% ' . __( 'delivered', 'bcs-smtp-manager' ) : $verdict ),
     1050                        'label'     => __( 'Monthly email report', 'bcs-smtp-manager' ),
     1051                        'body'      => $body,
     1052                        'footer'    => $T::footer( $T::settings_url( 'reports' ), __( 'Change or stop these reports', 'bcs-smtp-manager' ), esc_html__( 'You get this because you’re on the report list for this site.', 'bcs-smtp-manager' ) ),
     1053                ) );
     1054        }
     1055
     1056        /* ─────────────────────────────────────────────────────────────────── */
     1057        /* Helpers: status chips, provider presets                              */
     1058        /* ─────────────────────────────────────────────────────────────────── */
     1059
     1060        /**
     1061         * Status vocabulary shared by the log table, the overview and the JS.
     1062         *
     1063         * @return array status => array( 'tone' => css modifier, 'label' => visible word )
     1064         */
     1065        private function status_marks() {
     1066                return array(
     1067                        'success'     => array( 'tone' => 'ok',   'label' => __( 'Sent', 'bcs-smtp-manager' ) ),
     1068                        'failed'      => array( 'tone' => 'bad',  'label' => __( 'Failed', 'bcs-smtp-manager' ) ),
     1069                        'unconfirmed' => array( 'tone' => 'warn', 'label' => __( 'Unconfirmed', 'bcs-smtp-manager' ) ),
    7551070                );
    756                 $range_label = isset( $range_labels[ $date_range ] ) ? $range_labels[ $date_range ] : $date_range;
    757 
    758                 // Build simple HTML bar chart rows.
    759                 $max_total = 1;
    760                 foreach ( $chart_data['labels'] as $i => $day ) {
    761                         $day_total = $chart_data['success'][ $i ] + $chart_data['failed'][ $i ] + $chart_data['unconfirmed'][ $i ];
    762                         if ( $day_total > $max_total ) { $max_total = $day_total; }
    763                 }
    764 
    765                 // Build bar chart rows — skip days with zero activity; show last 14 days with data.
    766                 $active_days = array();
    767                 foreach ( $chart_data['labels'] as $i => $label ) {
    768                         if ( $chart_data['success'][ $i ] > 0 || $chart_data['failed'][ $i ] > 0 || $chart_data['unconfirmed'][ $i ] > 0 ) {
    769                                 $active_days[] = $i;
     1071        }
     1072
     1073        /**
     1074         * Status chip for one email's delivery status.
     1075         *
     1076         * @param string $status success|failed|unconfirmed.
     1077         * @return string Escaped HTML.
     1078         */
     1079        /**
     1080         * Whole minutes since the last health check (rounded down) or until the next (rounded up),
     1081         * so the two always add up to the check interval.
     1082         *
     1083         * @param  int  $timestamp
     1084         * @param  bool $until True for a time in the future.
     1085         * @return string
     1086         */
     1087        private function check_minutes( $timestamp, $until = false ) {
     1088                $seconds = $until ? $timestamp - time() : time() - $timestamp;
     1089                $mins    = $until ? (int) ceil( $seconds / 60 ) : (int) floor( $seconds / 60 );
     1090                if ( $mins < 1 ) {
     1091                        return __( 'less than a minute', 'bcs-smtp-manager' );
     1092                }
     1093                /* translators: %d: number of minutes. */
     1094                return sprintf( _n( '%d minute', '%d minutes', $mins, 'bcs-smtp-manager' ), $mins );
     1095        }
     1096
     1097        private function status_html( $status ) {
     1098                $marks = $this->status_marks();
     1099                $mark  = isset( $marks[ $status ] ) ? $marks[ $status ] : array( 'tone' => 'off', 'label' => $status );
     1100                return '<span class="bcs-smtp-status bcs-smtp-status--' . esc_attr( $mark['tone'] ) . '"><span class="bcs-smtp-status-dot" aria-hidden="true"></span>' . esc_html( $mark['label'] ) . '</span>';
     1101        }
     1102
     1103        /**
     1104         * Provider presets for guided setup. Host/port/encryption are standard published SMTP settings.
     1105         *
     1106         * @return array
     1107         */
     1108        private function provider_presets() {
     1109                // Last saved "Other SMTP server" details. Older installs that saved a custom host
     1110                // before this option existed fall back to the current host when it isn't a preset.
     1111                $custom = wp_parse_args( (array) get_option( 'bcs_smtp_custom_server', array() ), array( 'host' => '', 'port' => 587, 'encryption' => 'tls' ) );
     1112                $saved  = strtolower( trim( (string) get_option( 'bcs_smtp_host', '' ) ) );
     1113                $known  = array( 'smtp.gmail.com', 'smtp.office365.com', 'smtp-mail.outlook.com', 'smtp.sendgrid.net', 'smtp.mailgun.org', 'smtp-relay.brevo.com', 'smtp.zoho.com', 'smtp.zoho.eu', 'smtp.zoho.in', 'smtp.mail.yahoo.com' );
     1114                if ( '' === $custom['host'] && '' !== $saved && ! in_array( $saved, $known, true ) && ! preg_match( '/^email-smtp\.[a-z0-9-]+\.amazonaws\.com$/', $saved ) ) {
     1115                        $custom = array( 'host' => $saved, 'port' => (int) get_option( 'bcs_smtp_port', 587 ), 'encryption' => get_option( 'bcs_smtp_encryption', 'tls' ) );
     1116                }
     1117
     1118                return array(
     1119                        'gmail'     => array(
     1120                                'label' => __( 'Gmail / Google Workspace', 'bcs-smtp-manager' ),
     1121                                'short' => 'G',
     1122                                'host'  => 'smtp.gmail.com',
     1123                                'port'  => 587,
     1124                                'enc'   => 'tls',
     1125                                'user'  => '',
     1126                                'user_hint' => __( 'Your full Gmail or Workspace address', 'bcs-smtp-manager' ),
     1127                                'pass_label' => __( 'App password', 'bcs-smtp-manager' ),
     1128                                'note'  => __( 'Google needs a 16-character App Password here, not your normal password.', 'bcs-smtp-manager' ),
     1129                                'link'  => 'https://myaccount.google.com/apppasswords',
     1130                                'link_label' => __( 'Create an App Password', 'bcs-smtp-manager' ),
     1131                        ),
     1132                        'microsoft' => array(
     1133                                'label' => __( 'Microsoft 365 / Outlook', 'bcs-smtp-manager' ),
     1134                                'short' => 'M',
     1135                                'host'  => 'smtp.office365.com',
     1136                                'port'  => 587,
     1137                                'enc'   => 'tls',
     1138                                'user'  => '',
     1139                                'user_hint' => __( 'Your full Microsoft 365 email address', 'bcs-smtp-manager' ),
     1140                                'pass_label' => __( 'Password or app password', 'bcs-smtp-manager' ),
     1141                                'note'  => __( 'SMTP sign-in (SMTP AUTH) must be allowed for this mailbox in Microsoft 365 admin.', 'bcs-smtp-manager' ),
     1142                                'link'  => 'https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission',
     1143                                'link_label' => __( 'How to allow SMTP AUTH', 'bcs-smtp-manager' ),
     1144                        ),
     1145                        'sendgrid'  => array(
     1146                                'label' => __( 'SendGrid', 'bcs-smtp-manager' ),
     1147                                'short' => 'SG',
     1148                                'host'  => 'smtp.sendgrid.net',
     1149                                'port'  => 587,
     1150                                'enc'   => 'tls',
     1151                                'user'  => 'apikey',
     1152                                'user_hint' => __( 'Always the word apikey', 'bcs-smtp-manager' ),
     1153                                'pass_label' => __( 'API key', 'bcs-smtp-manager' ),
     1154                                'note'  => __( 'Paste a SendGrid API key with Mail Send permission as the password.', 'bcs-smtp-manager' ),
     1155                                'link'  => 'https://docs.sendgrid.com/for-developers/sending-email/getting-started-smtp',
     1156                                'link_label' => __( 'SendGrid SMTP guide', 'bcs-smtp-manager' ),
     1157                        ),
     1158                        'ses'       => array(
     1159                                'label' => __( 'Amazon SES', 'bcs-smtp-manager' ),
     1160                                'short' => 'SES',
     1161                                // SES servers are per region, so there's no single right default: the user pastes theirs.
     1162                                'host'  => '',
     1163                                'port'  => 587,
     1164                                'enc'   => 'tls',
     1165                                'user'  => '',
     1166                                'user_hint' => __( 'SMTP username from the SES console', 'bcs-smtp-manager' ),
     1167                                'pass_label' => __( 'SMTP password', 'bcs-smtp-manager' ),
     1168                                'note'  => __( 'Copy the SMTP endpoint for your region from the SES console (SMTP settings) into Server details. Use SES SMTP credentials, not your AWS access keys.', 'bcs-smtp-manager' ),
     1169                                'link'  => 'https://docs.aws.amazon.com/ses/latest/dg/send-email-smtp.html',
     1170                                'link_label' => __( 'SES SMTP guide', 'bcs-smtp-manager' ),
     1171                        ),
     1172                        'mailgun'   => array(
     1173                                'label' => __( 'Mailgun', 'bcs-smtp-manager' ),
     1174                                'short' => 'MG',
     1175                                'host'  => 'smtp.mailgun.org',
     1176                                'port'  => 587,
     1177                                'enc'   => 'tls',
     1178                                'user'  => '',
     1179                                'user_hint' => __( 'For example postmaster@mg.yourdomain.com', 'bcs-smtp-manager' ),
     1180                                'pass_label' => __( 'SMTP password', 'bcs-smtp-manager' ),
     1181                                'note'  => __( 'Find the SMTP login and password under your sending domain in Mailgun.', 'bcs-smtp-manager' ),
     1182                                'link'  => 'https://documentation.mailgun.com/docs/mailgun/user-manual/sending-messages/',
     1183                                'link_label' => __( 'Mailgun SMTP guide', 'bcs-smtp-manager' ),
     1184                        ),
     1185                        'brevo'     => array(
     1186                                'label' => __( 'Brevo', 'bcs-smtp-manager' ),
     1187                                'short' => 'B',
     1188                                'host'  => 'smtp-relay.brevo.com',
     1189                                'port'  => 587,
     1190                                'enc'   => 'tls',
     1191                                'user'  => '',
     1192                                'user_hint' => __( 'Your Brevo SMTP login', 'bcs-smtp-manager' ),
     1193                                'pass_label' => __( 'SMTP key', 'bcs-smtp-manager' ),
     1194                                'note'  => __( 'Use an SMTP key from Brevo, not your account password.', 'bcs-smtp-manager' ),
     1195                                'link'  => 'https://help.brevo.com/hc/en-us/articles/209462765',
     1196                                'link_label' => __( 'Brevo SMTP guide', 'bcs-smtp-manager' ),
     1197                        ),
     1198                        'zoho'      => array(
     1199                                'label' => __( 'Zoho Mail', 'bcs-smtp-manager' ),
     1200                                'short' => 'Z',
     1201                                'host'  => 'smtp.zoho.com',
     1202                                'port'  => 465,
     1203                                'enc'   => 'ssl',
     1204                                'user'  => '',
     1205                                'user_hint' => __( 'Your full Zoho email address', 'bcs-smtp-manager' ),
     1206                                'pass_label' => __( 'Password or app password', 'bcs-smtp-manager' ),
     1207                                'note'  => __( 'EU accounts use smtp.zoho.eu. Use an app-specific password if two-factor sign-in is on.', 'bcs-smtp-manager' ),
     1208                                'link'  => 'https://www.zoho.com/mail/help/zoho-smtp.html',
     1209                                'link_label' => __( 'Zoho SMTP guide', 'bcs-smtp-manager' ),
     1210                        ),
     1211                        'yahoo'     => array(
     1212                                'label' => __( 'Yahoo Mail', 'bcs-smtp-manager' ),
     1213                                'short' => 'Y',
     1214                                'host'  => 'smtp.mail.yahoo.com',
     1215                                'port'  => 465,
     1216                                'enc'   => 'ssl',
     1217                                'user'  => '',
     1218                                'user_hint' => __( 'Your full Yahoo email address', 'bcs-smtp-manager' ),
     1219                                'pass_label' => __( 'App password', 'bcs-smtp-manager' ),
     1220                                'note'  => __( 'Generate an app password under Yahoo Account Security.', 'bcs-smtp-manager' ),
     1221                                'link'  => 'https://help.yahoo.com/kb/SLN15241.html',
     1222                                'link_label' => __( 'Yahoo app passwords', 'bcs-smtp-manager' ),
     1223                        ),
     1224                        'other'     => array(
     1225                                'label' => __( 'Other SMTP server', 'bcs-smtp-manager' ),
     1226                                'short' => '',
     1227                                'host'  => $custom['host'],
     1228                                'port'  => $custom['port'],
     1229                                'enc'   => $custom['encryption'],
     1230                                'user'  => '',
     1231                                'user_hint' => __( 'Usually your full email address', 'bcs-smtp-manager' ),
     1232                                'pass_label' => __( 'Password', 'bcs-smtp-manager' ),
     1233                                'note'  => __( 'Your host or email provider lists these details. Port 587 with TLS works for most servers.', 'bcs-smtp-manager' ),
     1234                                'link'  => '',
     1235                                'link_label' => '',
     1236                        ),
     1237                );
     1238        }
     1239
     1240        /**
     1241         * Which preset matches the saved host. Empty string when nothing is saved yet.
     1242         *
     1243         * @param string $host Saved SMTP host.
     1244         * @return string
     1245         */
     1246        private function detect_provider( $host ) {
     1247                $host = strtolower( trim( $host ) );
     1248                if ( '' === $host ) {
     1249                        return '';
     1250                }
     1251                if ( preg_match( '/^email-smtp\.[a-z0-9-]+\.amazonaws\.com$/', $host ) ) {
     1252                        return 'ses';
     1253                }
     1254                if ( 'smtp.zoho.eu' === $host || 'smtp.zoho.in' === $host ) {
     1255                        return 'zoho';
     1256                }
     1257                if ( 'smtp-mail.outlook.com' === $host ) {
     1258                        return 'microsoft';
     1259                }
     1260                foreach ( $this->provider_presets() as $key => $preset ) {
     1261                        if ( $preset['host'] && $preset['host'] === $host ) {
     1262                                return $key;
    7701263                        }
    7711264                }
    772                 $active_days = array_slice( $active_days, -14 ); // cap at 14
    773 
    774                 $chart_rows = '';
    775                 foreach ( $active_days as $i ) {
    776                         $s   = $chart_data['success'][ $i ];
    777                         $f   = $chart_data['failed'][ $i ];
    778                         $u   = $chart_data['unconfirmed'][ $i ];
    779                         $tot = $s + $f + $u;
    780                         $sw  = $max_total > 0 ? round( ( $s / $max_total ) * 100 ) : 0;
    781                         $fw  = $max_total > 0 ? round( ( $f / $max_total ) * 100 ) : 0;
    782                         $uw  = $max_total > 0 ? round( ( $u / $max_total ) * 100 ) : 0;
    783                         $day = substr( $chart_data['labels'][ $i ], 5 ); // MM-DD
    784                         $chart_rows .= '<tr>
    785                                 <td style="font-size:10px;color:#64748b;width:44px;text-align:right;padding-right:8px;white-space:nowrap">' . esc_html( $day ) . '</td>
    786                                 <td style="padding:2px 0">
    787                                         <table cellpadding="0" cellspacing="0" border="0" width="100%"><tr>
    788                                                 ' . ( $sw > 0 ? '<td style="background:#10b981;width:' . $sw . '%;height:14px;border-radius:2px"></td>' : '' ) . '
    789                                                 ' . ( $fw > 0 ? '<td style="background:#ef4444;width:' . $fw . '%;height:14px;border-radius:2px"></td>' : '' ) . '
    790                                                 ' . ( $uw > 0 ? '<td style="background:#f59e0b;width:' . $uw . '%;height:14px;border-radius:2px"></td>' : '' ) . '
    791                                                 <td></td>
    792                                         </tr></table>
    793                                 </td>
    794                                 <td style="font-size:10px;color:#64748b;width:32px;text-align:right;padding-left:6px">' . $tot . '</td>
    795                         </tr>';
    796                 }
    797 
    798                 $success_rate = $summary['total'] > 0 ? round( ( $summary['success'] / $summary['total'] ) * 100 ) : 0;
    799 
    800                 return BCS_SMTP_Email_Template::wrapper_open( 'Email Report', 620, 16, '.09' ) . '
    801   <!-- Header — bgcolor attr = Outlook fallback; background-color = Yahoo/other fallback; background-image = gradient for supporting clients -->
    802   <tr><td bgcolor="#6366f1" style="background-color:#6366f1;background-image:linear-gradient(135deg,#6366f1 0%,#4f46e5 100%);padding:36px 48px">
    803     <p style="margin:0;font-size:11px;color:#c7d2fe;letter-spacing:1.5px;text-transform:uppercase;font-weight:600">Email Delivery Report</p>
    804     <h1 style="margin:8px 0 4px;font-size:26px;color:#ffffff;font-weight:700;letter-spacing:-0.3px">' . esc_html( $site_name ) . '</h1>
    805     <p style="margin:0;font-size:13px;color:#e0e7ff">' . esc_html( $range_label ) . ' &nbsp;&bull;&nbsp; Generated ' . esc_html( $timestamp ) . '</p>
    806   </td></tr>
    807 
    808   <!-- Delivery rate banner -->
    809   <tr><td bgcolor="#4f46e5" style="background-color:#4f46e5;padding:14px 48px">
    810     <p style="margin:0;font-size:13px;color:#e0e7ff">
    811       <strong style="color:#ffffff">Delivery Rate: ' . $success_rate . '%</strong>
    812       &nbsp;&mdash;&nbsp;' . intval( $summary['success'] ) . ' of ' . intval( $summary['total'] ) . ' emails delivered successfully
    813     </p>
    814   </td></tr>
    815 
    816   <!-- Stat boxes -->
    817   <tr><td style="padding:36px 48px 0">
    818     <table width="100%" cellpadding="0" cellspacing="0" border="0">
    819     <tr>
    820       <td width="25%" style="padding:0 5px 0 0">
    821         <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px">
    822           <tr><td style="padding:18px 12px;text-align:center">
    823             <div style="font-size:32px;font-weight:700;color:#15803d;line-height:1">' . intval( $summary['success'] ) . '</div>
    824             <div style="font-size:11px;color:#16a34a;margin-top:6px;font-weight:600;text-transform:uppercase;letter-spacing:.5px">Sent</div>
    825           </td></tr>
    826         </table>
    827       </td>
    828       <td width="25%" style="padding:0 5px">
    829         <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#fef2f2;border:1px solid #fecaca;border-radius:10px">
    830           <tr><td style="padding:18px 12px;text-align:center">
    831             <div style="font-size:32px;font-weight:700;color:#dc2626;line-height:1">' . intval( $summary['failed'] ) . '</div>
    832             <div style="font-size:11px;color:#ef4444;margin-top:6px;font-weight:600;text-transform:uppercase;letter-spacing:.5px">Failed</div>
    833           </td></tr>
    834         </table>
    835       </td>
    836       <td width="25%" style="padding:0 5px">
    837         <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#fffbeb;border:1px solid #fde68a;border-radius:10px">
    838           <tr><td style="padding:18px 12px;text-align:center">
    839             <div style="font-size:32px;font-weight:700;color:#d97706;line-height:1">' . intval( $summary['unconfirmed'] ) . '</div>
    840             <div style="font-size:11px;color:#f59e0b;margin-top:6px;font-weight:600;text-transform:uppercase;letter-spacing:.5px">Unconfirmed</div>
    841           </td></tr>
    842         </table>
    843       </td>
    844       <td width="25%" style="padding:0 0 0 5px">
    845         <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px">
    846           <tr><td style="padding:18px 12px;text-align:center">
    847             <div style="font-size:32px;font-weight:700;color:#334155;line-height:1">' . intval( $summary['total'] ) . '</div>
    848             <div style="font-size:11px;color:#64748b;margin-top:6px;font-weight:600;text-transform:uppercase;letter-spacing:.5px">Total</div>
    849           </td></tr>
    850         </table>
    851       </td>
    852     </tr>
    853     </table>
    854   </td></tr>
    855 
    856   <!-- Chart -->
    857   <tr><td style="padding:32px 48px 0">
    858     <p style="margin:0 0 16px;font-size:14px;font-weight:700;color:#1e293b;letter-spacing:-0.2px">Daily Activity</p>
    859     <table width="100%" cellpadding="0" cellspacing="0" border="0">
    860       ' . $chart_rows . '
    861     </table>
    862     <!-- Legend -->
    863     <table cellpadding="0" cellspacing="0" border="0" style="margin-top:16px">
    864       <tr>
    865         <td style="width:10px;height:10px;background:#10b981;border-radius:2px;vertical-align:middle"></td>
    866         <td style="font-size:11px;color:#64748b;padding:0 16px 0 6px;vertical-align:middle">Sent</td>
    867         <td style="width:10px;height:10px;background:#ef4444;border-radius:2px;vertical-align:middle"></td>
    868         <td style="font-size:11px;color:#64748b;padding:0 16px 0 6px;vertical-align:middle">Failed</td>
    869         <td style="width:10px;height:10px;background:#f59e0b;border-radius:2px;vertical-align:middle"></td>
    870         <td style="font-size:11px;color:#64748b;padding:0 0 0 6px;vertical-align:middle">Unconfirmed</td>
    871       </tr>
    872     </table>
    873   </td></tr>
    874 
    875   <!-- Footer -->
    876   <tr><td style="padding:32px 48px;margin-top:32px;border-top:1px solid #e2e8f0">
    877     <table width="100%" cellpadding="0" cellspacing="0" border="0">
    878       <tr>
    879         <td style="text-align:center">
    880           <p style="margin:0 0 4px;font-size:12px;color:#64748b">
    881             <a href="' . esc_url( $site_url ) . '" style="color:#6366f1;text-decoration:none;font-weight:600">' . esc_html( $site_name ) . '</a>
    882           </p>
    883           <p style="margin:0;font-size:11px;color:#94a3b8">Automated report by <strong>ByteCoreStack SMTP</strong> &bull; <a href="' . esc_url( $site_url ) . '/wp-admin/options-general.php?page=bcs-smtp-manager" style="color:#94a3b8;text-decoration:none">Manage settings</a></p>
    884         </td>
    885       </tr>
    886     </table>
    887   </td></tr>
    888 ' . BCS_SMTP_Email_Template::wrapper_close();
     1265                return 'other';
    8891266        }
    8901267
    … …  
    8931270        /* ─────────────────────────────────────────────────────────────────── */
    8941271
     1272        /**
     1273         * Heading for a day in the email log: Today, Yesterday, or the date.
     1274         *
     1275         * @param  string $ymd Site-local date, Y-m-d.
     1276         * @return string
     1277         */
     1278        private function log_day_label( $ymd ) {
     1279                $today = current_time( 'Y-m-d' );
     1280                if ( $ymd === $today ) {
     1281                        return __( 'Today', 'bcs-smtp-manager' );
     1282                }
     1283                if ( gmdate( 'Y-m-d', strtotime( $today . ' -1 day' ) ) === $ymd ) {
     1284                        return __( 'Yesterday', 'bcs-smtp-manager' );
     1285                }
     1286                return mysql2date( 'l, F j', $ymd . ' 00:00:00' );
     1287        }
     1288
     1289        /**
     1290         * Link to the full view of one logged email.
     1291         *
     1292         * @param  int $id Log row ID.
     1293         * @return string
     1294         */
     1295        private function email_view_url( $id ) {
     1296                return add_query_arg( array( 'page' => 'bcs-smtp-manager', 'email' => (int) $id ), admin_url( 'options-general.php' ) );
     1297        }
     1298
     1299        /**
     1300         * Plain-text version of an HTML email: line breaks kept, links shown as "text (url)".
     1301         *
     1302         * @param  string $html
     1303         * @return string
     1304         */
     1305        private function email_text_version( $html ) {
     1306                $html = preg_replace( '#<(script|style|head|title)\b[^>]*>.*?</\1>#is', '', $html );
     1307                $html = preg_replace( '/\s+/u', ' ', $html ); // Line breaks in the source aren't real ones; tags decide below.
     1308                $html = preg_replace( '#<a\b[^>]*href=["\']([^"\']+)["\'][^>]*>(.*?)</a>#is', '$2 ($1)', $html );
     1309                $html = preg_replace( '#<br\s*/?>#i', "\n", $html );
     1310                $html = preg_replace( '#<li\b[^>]*>#i', "\n• ", $html );
     1311                $html = preg_replace( '#</(p|div|tr|table|h[1-6]|li|ul|ol|blockquote)>#i', "\n", $html );
     1312                $text = html_entity_decode( wp_strip_all_tags( $html ), ENT_QUOTES, 'UTF-8' );
     1313                $text = implode( "\n", array_map( 'trim', explode( "\n", str_replace( "\r", '', $text ) ) ) );
     1314                return trim( preg_replace( "/\n{3,}/", "\n\n", $text ) );
     1315        }
     1316
     1317        /**
     1318         * Full view of one logged email, laid out like a mail client: who it was from and to,
     1319         * the message (preview, HTML source or text) and, alongside, how it was delivered.
     1320         * HTML messages render in a sandboxed frame with scripts and forms off.
     1321         *
     1322         * @param int $id Log row ID.
     1323         */
     1324        private function render_email_view( $id ) {
     1325                $row  = $id ? $this->db->get_email_log_row( $id ) : null;
     1326                $back = admin_url( 'options-general.php?page=bcs-smtp-manager#email-log' );
     1327                ?>
     1328                <div class="bcs-smtp-wrap wrap bcs-smtp-mailview">
     1329                        <a class="bcs-smtp-back" href="<?php echo esc_url( $back ); ?>"><span class="dashicons dashicons-arrow-left-alt2" aria-hidden="true"></span><?php esc_html_e( 'Email log', 'bcs-smtp-manager' ); ?></a>
     1330                <?php
     1331                if ( ! $row ) {
     1332                        ?>
     1333                        <div class="bcs-smtp-panel bcs-smtp-mailview-missing">
     1334                                <h1><?php esc_html_e( 'This email isn’t in the log', 'bcs-smtp-manager' ); ?></h1>
     1335                                <p><?php esc_html_e( 'It may be more than 90 days old, or the log was cleared.', 'bcs-smtp-manager' ); ?></p>
     1336                        </div>
     1337                </div>
     1338                        <?php
     1339                        return;
     1340                }
     1341
     1342                $subject = $row['subject'] ? $row['subject'] : __( '(no subject)', 'bcs-smtp-manager' );
     1343                $headers = $row['mail_headers'] ? (array) json_decode( $row['mail_headers'], true ) : array();
     1344                $files   = $row['attachments'] ? (array) json_decode( $row['attachments'], true ) : array();
     1345                $named   = array();
     1346                foreach ( $headers as $header ) {
     1347                        list( $name, $value ) = array_pad( explode( ':', (string) $header, 2 ), 2, '' );
     1348                        $key = strtolower( trim( $name ) );
     1349                        if ( in_array( $key, array( 'from', 'cc', 'bcc', 'reply-to' ), true ) ) {
     1350                                $named[ $key ][] = trim( $value );
     1351                        }
     1352                }
     1353                $has_body = isset( $row['body'] ) && '' !== (string) $row['body'];
     1354                $is_html  = 'html' === $row['body_format'];
     1355                $adjacent = $this->db->get_adjacent_log_ids( (int) $row['id'] );
     1356                // sent_at is stored in site-local time.
     1357                $sent     = mysql2date( get_option( 'date_format', 'F j, Y' ) . ' ' . get_option( 'time_format', 'g:i a' ), $row['sent_at'] );
     1358
     1359                // "Name <address>" → name and address, for the sender line.
     1360                $from       = (string) $row['from_address'];
     1361                $from_email = preg_match( '/<([^>]+)>/', $from, $m ) ? $m[1] : $from;
     1362                $from_name  = trim( preg_replace( '/<[^>]*>/', '', $from ), " \"'" );
     1363                $from_name  = '' !== $from_name && $from_name !== $from_email ? $from_name : '';
     1364                $initial    = strtoupper( function_exists( 'mb_substr' ) ? mb_substr( $from_name ? $from_name : $from_email, 0, 1 ) : substr( $from_name ? $from_name : $from_email, 0, 1 ) );
     1365
     1366                // The address the sending code asked for, when the mail server sent as someone else.
     1367                $requested = isset( $named['from'] ) ? $named['from'][0] : '';
     1368                if ( $requested && false !== stripos( $from, preg_match( '/<([^>]+)>/', $requested, $rm ) ? $rm[1] : $requested ) ) {
     1369                        $requested = '';
     1370                }
     1371
     1372                $recipients = array(
     1373                        __( 'to', 'bcs-smtp-manager' )       => $row['to_address'],
     1374                        __( 'cc', 'bcs-smtp-manager' )       => isset( $named['cc'] ) ? implode( ', ', $named['cc'] ) : '',
     1375                        __( 'bcc', 'bcs-smtp-manager' )      => isset( $named['bcc'] ) ? implode( ', ', $named['bcc'] ) : '',
     1376                        __( 'reply to', 'bcs-smtp-manager' ) => isset( $named['reply-to'] ) ? implode( ', ', $named['reply-to'] ) : '',
     1377                );
     1378                $explain = array(
     1379                        'success'     => __( 'Your mail server accepted this email for delivery.', 'bcs-smtp-manager' ),
     1380                        'failed'      => __( 'This email was not sent. The server’s reply above says why.', 'bcs-smtp-manager' ),
     1381                        'unconfirmed' => __( 'WordPress handed this email over, but the server didn’t confirm it.', 'bcs-smtp-manager' ),
     1382                );
     1383                ?>
     1384                        <header class="bcs-smtp-mailview-head">
     1385                                <div class="bcs-smtp-mailview-title">
     1386                                        <p class="bcs-smtp-mailview-when"><?php echo $this->status_html( $row['status'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in status_html(). ?><span><?php echo esc_html( $sent ); ?></span></p>
     1387                                        <h1><?php echo esc_html( $subject ); ?></h1>
     1388                                </div>
     1389                                <nav class="bcs-smtp-mailview-nav" aria-label="<?php esc_attr_e( 'Other emails', 'bcs-smtp-manager' ); ?>">
     1390                                        <?php
     1391                                        $steps = array(
     1392                                                'newer' => array( __( 'Newer', 'bcs-smtp-manager' ), 'arrow-left-alt2' ),
     1393                                                'older' => array( __( 'Older', 'bcs-smtp-manager' ), 'arrow-right-alt2' ),
     1394                                        );
     1395                                        foreach ( $steps as $dir => $step ) :
     1396                                                $icon  = '<span class="dashicons dashicons-' . esc_attr( $step[1] ) . '" aria-hidden="true"></span>';
     1397                                                $label = 'newer' === $dir ? $icon . esc_html( $step[0] ) : esc_html( $step[0] ) . $icon;
     1398                                                if ( $adjacent[ $dir ] ) :
     1399                                                        ?>
     1400                                                        <a class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm" href="<?php echo esc_url( $this->email_view_url( $adjacent[ $dir ] ) ); ?>"><?php echo $label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built from escaped parts. ?></a>
     1401                                                <?php else : ?>
     1402                                                        <span class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm is-disabled" aria-disabled="true"><?php echo $label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built from escaped parts. ?></span>
     1403                                                        <?php
     1404                                                endif;
     1405                                        endforeach;
     1406                                        ?>
     1407                                </nav>
     1408                        </header>
     1409
     1410                        <?php if ( ! empty( $row['error_msg'] ) ) : ?>
     1411                                <div class="bcs-smtp-log-error bcs-smtp-mailview-error">
     1412                                        <div class="bcs-smtp-log-error-head">
     1413                                                <span><?php esc_html_e( 'Server reply', 'bcs-smtp-manager' ); ?></span>
     1414                                                <button type="button" class="bcs-smtp-copy-btn"><span class="dashicons dashicons-admin-page" aria-hidden="true"></span><span class="bcs-smtp-copy-label"><?php esc_html_e( 'Copy', 'bcs-smtp-manager' ); ?></span></button>
     1415                                        </div>
     1416                                        <pre class="bcs-smtp-mono"><?php echo esc_html( $row['error_msg'] ); ?></pre>
     1417                                </div>
     1418                        <?php endif; ?>
     1419
     1420                        <div class="bcs-smtp-mailview-grid">
     1421                                <section class="bcs-smtp-panel bcs-smtp-mailview-message" aria-label="<?php esc_attr_e( 'Message', 'bcs-smtp-manager' ); ?>">
     1422                                        <div class="bcs-smtp-envelope">
     1423                                                <span class="bcs-smtp-envelope-avatar" aria-hidden="true"><?php echo esc_html( $initial ); ?></span>
     1424                                                <div class="bcs-smtp-envelope-lines">
     1425                                                        <p class="bcs-smtp-envelope-from">
     1426                                                                <?php if ( $from_name ) : ?>
     1427                                                                        <strong><?php echo esc_html( $from_name ); ?></strong>
     1428                                                                        <span>&lt;<?php echo esc_html( $from_email ); ?>&gt;</span>
     1429                                                                <?php else : ?>
     1430                                                                        <strong><?php echo esc_html( $from_email ); ?></strong>
     1431                                                                <?php endif; ?>
     1432                                                        </p>
     1433                                                        <?php foreach ( $recipients as $label => $value ) : ?>
     1434                                                                <?php if ( '' !== trim( (string) $value ) ) : ?>
     1435                                                                        <p class="bcs-smtp-envelope-to"><span><?php echo esc_html( $label ); ?></span> <?php echo esc_html( $value ); ?></p>
     1436                                                                <?php endif; ?>
     1437                                                        <?php endforeach; ?>
     1438                                                </div>
     1439                                        </div>
     1440
     1441                                        <?php if ( $has_body ) : ?>
     1442                                                <div class="bcs-smtp-mailview-toolbar">
     1443                                                        <div class="bcs-smtp-segmented" role="radiogroup" aria-label="<?php esc_attr_e( 'Show the message as', 'bcs-smtp-manager' ); ?>">
     1444                                                                <?php if ( $is_html ) : ?>
     1445                                                                        <label><input type="radio" name="bcs_smtp_mailview_mode" value="preview" checked><span><?php esc_html_e( 'Preview', 'bcs-smtp-manager' ); ?></span></label>
     1446                                                                        <label><input type="radio" name="bcs_smtp_mailview_mode" value="source"><span><?php esc_html_e( 'HTML', 'bcs-smtp-manager' ); ?></span></label>
     1447                                                                <?php endif; ?>
     1448                                                                <label><input type="radio" name="bcs_smtp_mailview_mode" value="text" <?php checked( ! $is_html ); ?>><span><?php esc_html_e( 'Text', 'bcs-smtp-manager' ); ?></span></label>
     1449                                                        </div>
     1450                                                        <?php if ( $is_html ) : ?>
     1451                                                                <div class="bcs-smtp-segmented bcs-smtp-device" role="radiogroup" aria-label="<?php esc_attr_e( 'Preview width', 'bcs-smtp-manager' ); ?>" data-mode="preview">
     1452                                                                        <label title="<?php esc_attr_e( 'Desktop', 'bcs-smtp-manager' ); ?>"><input type="radio" name="bcs_smtp_mailview_device" value="desktop" checked><span><svg class="bcs-smtp-device-icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><rect x="2.5" y="3.5" width="15" height="10" rx="1.5"/><path d="M7 17h6M10 13.5V17"/></svg><span class="screen-reader-text"><?php esc_html_e( 'Desktop', 'bcs-smtp-manager' ); ?></span></span></label>
     1453                                                                        <label title="<?php esc_attr_e( 'Mobile', 'bcs-smtp-manager' ); ?>"><input type="radio" name="bcs_smtp_mailview_device" value="mobile"><span><svg class="bcs-smtp-device-icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><rect x="6" y="2.5" width="8" height="15" rx="1.8"/><path d="M9 15h2"/></svg><span class="screen-reader-text"><?php esc_html_e( 'Mobile', 'bcs-smtp-manager' ); ?></span></span></label>
     1454                                                                </div>
     1455                                                        <?php endif; ?>
     1456                                                </div>
     1457                                        <?php endif; ?>
     1458
     1459                                        <div class="bcs-smtp-mailview-body">
     1460                                                <?php if ( $has_body && $is_html ) : ?>
     1461                                                        <div class="bcs-smtp-mailframe-stage" data-mode="preview">
     1462                                                                <div class="bcs-smtp-mailframe-wrap">
     1463                                                                        <?php // Full encoding (double_encode on) keeps entities in the email exactly as sent. ?>
     1464                                                                        <iframe class="bcs-smtp-mailframe" scrolling="no" title="<?php esc_attr_e( 'Email preview', 'bcs-smtp-manager' ); ?>" sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox" referrerpolicy="no-referrer" srcdoc="<?php echo htmlspecialchars( '<base target="_blank">' . $row['body'], ENT_QUOTES, 'UTF-8', true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fully encoded for the attribute. ?>"></iframe>
     1465                                                                </div>
     1466                                                        </div>
     1467                                                        <div class="bcs-smtp-mailview-source" data-mode="source" hidden>
     1468                                                                <div class="bcs-smtp-mailview-source-head">
     1469                                                                        <span><?php esc_html_e( 'HTML source', 'bcs-smtp-manager' ); ?></span>
     1470                                                                        <button type="button" class="bcs-smtp-copy-btn"><span class="dashicons dashicons-admin-page" aria-hidden="true"></span><span class="bcs-smtp-copy-label"><?php esc_html_e( 'Copy', 'bcs-smtp-manager' ); ?></span></button>
     1471                                                                </div>
     1472                                                                <pre class="bcs-smtp-mono"><?php echo esc_html( $row['body'] ); ?></pre>
     1473                                                        </div>
     1474                                                <?php endif; ?>
     1475                                                <?php if ( $has_body ) : ?>
     1476                                                        <div class="bcs-smtp-mailview-source bcs-smtp-mailview-plain" data-mode="text" <?php echo $is_html ? 'hidden' : ''; ?>>
     1477                                                                <div class="bcs-smtp-mailview-source-head">
     1478                                                                        <span><?php echo $is_html ? esc_html__( 'Text version', 'bcs-smtp-manager' ) : esc_html__( 'Plain text', 'bcs-smtp-manager' ); ?></span>
     1479                                                                        <button type="button" class="bcs-smtp-copy-btn"><span class="dashicons dashicons-admin-page" aria-hidden="true"></span><span class="bcs-smtp-copy-label"><?php esc_html_e( 'Copy', 'bcs-smtp-manager' ); ?></span></button>
     1480                                                                </div>
     1481                                                                <pre><?php echo esc_html( $is_html ? $this->email_text_version( $row['body'] ) : $row['body'] ); ?></pre>
     1482                                                        </div>
     1483                                                <?php else : ?>
     1484                                                        <div class="bcs-smtp-mailview-empty">
     1485                                                                <span class="dashicons dashicons-hidden" aria-hidden="true"></span>
     1486                                                                <strong><?php esc_html_e( 'The message wasn’t saved', 'bcs-smtp-manager' ); ?></strong>
     1487                                                                <p><?php esc_html_e( 'This email was sent before the log started keeping message content.', 'bcs-smtp-manager' ); ?></p>
     1488                                                        </div>
     1489                                                <?php endif; ?>
     1490                                        </div>
     1491                                </section>
     1492
     1493                                <aside class="bcs-smtp-panel bcs-smtp-mailview-meta" aria-labelledby="bcs-smtp-mailview-meta-title">
     1494                                        <h2 id="bcs-smtp-mailview-meta-title"><?php esc_html_e( 'Delivery', 'bcs-smtp-manager' ); ?></h2>
     1495                                        <div class="bcs-smtp-mailview-verdict bcs-smtp-mailview-verdict--<?php echo esc_attr( $row['status'] ); ?>">
     1496                                                <?php echo $this->status_html( $row['status'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in status_html(). ?>
     1497                                                <p><?php echo esc_html( isset( $explain[ $row['status'] ] ) ? $explain[ $row['status'] ] : '' ); ?></p>
     1498                                        </div>
     1499                                        <dl class="bcs-smtp-mailview-facts">
     1500                                                <div><dt><?php esc_html_e( 'Sent', 'bcs-smtp-manager' ); ?></dt><dd><?php echo esc_html( $sent ); ?></dd></div>
     1501                                                <?php if ( $requested ) : ?>
     1502                                                        <div>
     1503                                                                <dt><?php esc_html_e( 'Requested sender', 'bcs-smtp-manager' ); ?></dt>
     1504                                                                <dd class="bcs-smtp-mono"><?php echo esc_html( $requested ); ?></dd>
     1505                                                                <dd class="bcs-smtp-mailview-note"><?php esc_html_e( 'Your mail server sent it from your connected address instead.', 'bcs-smtp-manager' ); ?></dd>
     1506                                                        </div>
     1507                                                <?php endif; ?>
     1508                                                <?php if ( $has_body ) : ?>
     1509                                                        <div>
     1510                                                                <dt><?php esc_html_e( 'Format', 'bcs-smtp-manager' ); ?></dt>
     1511                                                                <dd>
     1512                                                                        <?php
     1513                                                                        /* translators: 1: HTML or Plain text, 2: size such as "8 KB". */
     1514                                                                        printf( esc_html__( '%1$s · %2$s', 'bcs-smtp-manager' ), $is_html ? esc_html__( 'HTML', 'bcs-smtp-manager' ) : esc_html__( 'Plain text', 'bcs-smtp-manager' ), esc_html( size_format( strlen( $row['body'] ) ) ) );
     1515                                                                        ?>
     1516                                                                </dd>
     1517                                                        </div>
     1518                                                <?php endif; ?>
     1519                                                <?php if ( ! empty( $row['provider'] ) ) : ?>
     1520                                                        <div><dt><?php esc_html_e( 'Provider', 'bcs-smtp-manager' ); ?></dt><dd><?php echo esc_html( $row['provider'] ); ?></dd></div>
     1521                                                <?php endif; ?>
     1522                                                <?php if ( ! empty( $row['message_id'] ) ) : ?>
     1523                                                        <div><dt><?php esc_html_e( 'Tracking ID', 'bcs-smtp-manager' ); ?></dt><dd class="bcs-smtp-mono"><?php echo esc_html( $row['message_id'] ); ?></dd></div>
     1524                                                <?php endif; ?>
     1525                                        </dl>
     1526
     1527                                        <?php if ( $files ) : ?>
     1528                                                <h3><?php esc_html_e( 'Attachments', 'bcs-smtp-manager' ); ?></h3>
     1529                                                <ul class="bcs-smtp-mailview-atts">
     1530                                                        <?php foreach ( $files as $file ) : ?>
     1531                                                                <li><span class="dashicons dashicons-media-default" aria-hidden="true"></span><?php echo esc_html( $file ); ?></li>
     1532                                                        <?php endforeach; ?>
     1533                                                </ul>
     1534                                                <p class="bcs-smtp-hint"><?php esc_html_e( 'File names only. The files themselves aren’t stored.', 'bcs-smtp-manager' ); ?></p>
     1535                                        <?php endif; ?>
     1536
     1537                                        <?php if ( $headers ) : ?>
     1538                                                <details class="bcs-smtp-mailview-headers">
     1539                                                        <summary>
     1540                                                                <?php
     1541                                                                /* translators: %d: number of email headers. */
     1542                                                                printf( esc_html( _n( 'All headers (%d)', 'All headers (%d)', count( $headers ), 'bcs-smtp-manager' ) ), (int) count( $headers ) );
     1543                                                                ?>
     1544                                                        </summary>
     1545                                                        <pre class="bcs-smtp-mono"><?php echo esc_html( implode( "\n", $headers ) ); ?></pre>
     1546                                                </details>
     1547                                        <?php endif; ?>
     1548                                </aside>
     1549                        </div>
     1550                </div>
     1551                <?php
     1552        }
     1553
    8951554        private function render_email_log_row( $row ) {
    896                 $status_map = array(
    897                         'success'     => array( 'class' => 'bcs-smtp-badge-success', 'label' => __( '✓ Sent', 'bcs-smtp-manager' ) ),
    898                         'failed'      => array( 'class' => 'bcs-smtp-badge-error',   'label' => __( '✗ Failed', 'bcs-smtp-manager' ) ),
    899                         'unconfirmed' => array( 'class' => 'bcs-smtp-badge-warning', 'label' => __( '? Unconfirmed', 'bcs-smtp-manager' ) ),
    900                 );
    901                 $st = isset( $status_map[ $row['status'] ] ) ? $status_map[ $row['status'] ] : array( 'class' => '', 'label' => esc_html( $row['status'] ) );
    902 
    903                 $is_failed    = 'failed' === $row['status'];
    904                 $has_body     = ! empty( $row['body'] );
    905                 $resend_count = isset( $row['resend_count'] ) ? (int) $row['resend_count'] : 0;
     1555                $is_failed = 'failed' === $row['status'];
     1556                $subject   = $row['subject'] ? $row['subject'] : __( '(no subject)', 'bcs-smtp-manager' );
     1557                // sent_at is stored in site-local time.
     1558                $time      = mysql2date( get_option( 'time_format', 'g:i a' ), $row['sent_at'] );
    9061559                ?>
    907                 <tr>
    908                         <td style="font-size:11px;white-space:nowrap;font-family:monospace"><?php echo esc_html( wp_date( 'M j, Y g:i A', strtotime( $row['sent_at'] ) ) ); ?></td>
    909                         <td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="<?php echo esc_attr( $row['subject'] ); ?>"><?php echo esc_html( $row['subject'] ?: __( '(no subject)', 'bcs-smtp-manager' ) ); ?></td>
    910                         <td style="font-size:11px;font-family:monospace;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><?php echo esc_html( $row['from_address'] ); ?></td>
    911                         <td style="font-size:11px;font-family:monospace;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><?php echo esc_html( $row['to_address'] ); ?></td>
    912                         <td><span class="bcs-smtp-badge <?php echo esc_attr( $st['class'] ); ?>"><?php echo esc_html( $st['label'] ); ?></span></td>
    913                         <td style="font-size:10px;color:var(--bcs-smtp-text-3);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="<?php echo esc_attr( $row['error_msg'] ); ?>"><?php echo esc_html( $row['error_msg'] ?: '—' ); ?></td>
    914                         <td style="white-space:nowrap">
    915                                 <?php if ( $is_failed ) : ?>
    916                                         <button type="button"
    917                                                 class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm bcs-smtp-resend-btn"
    918                                                 data-log-id="<?php echo esc_attr( $row['id'] ); ?>"
    919                                                 <?php echo $has_body ? '' : 'disabled title="' . esc_attr__( 'No stored content — enable "Store failed email content" in the Config tab before this failure occurred.', 'bcs-smtp-manager' ) . '"'; ?>>
    920                                                 <span class="dashicons dashicons-controls-repeat"></span><?php esc_html_e( 'Resend', 'bcs-smtp-manager' ); ?>
    921                                         </button>
    922                                         <?php if ( $resend_count > 0 ) : ?>
    923                                                 <div style="font-size:10px;color:var(--bcs-smtp-text-3);margin-top:4px">
    924                                                         <?php
    925                                                         printf(
    926                                                                 /* translators: %d: number of times this email has been resent. */
    927                                                                 esc_html( _n( 'Resent %d time', 'Resent %d times', $resend_count, 'bcs-smtp-manager' ) ),
    928                                                                 esc_html( $resend_count )
    929                                                         );
    930                                                         ?>
    931                                                 </div>
    932                                         <?php endif; ?>
    933                                 <?php else : ?>
    934                                         <span style="color:var(--bcs-smtp-text-3)">—</span>
    935                                 <?php endif; ?>
     1560                <tr class="<?php echo $is_failed ? 'is-failed' : ''; ?>">
     1561                        <td class="bcs-smtp-col-date bcs-smtp-mono"><?php echo esc_html( $time ); ?></td>
     1562                        <td class="bcs-smtp-col-subject">
     1563                                <span class="bcs-smtp-cell-main"><?php echo esc_html( $subject ); ?></span>
     1564                                <span class="bcs-smtp-cell-to bcs-smtp-mono" aria-hidden="true"><?php echo esc_html( $row['to_address'] ); ?></span>
     1565                        </td>
     1566                        <td class="bcs-smtp-col-to bcs-smtp-mono"><?php echo esc_html( $row['to_address'] ); ?></td>
     1567                        <td class="bcs-smtp-col-status"><?php echo $this->status_html( $row['status'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in status_html(). ?></td>
     1568                        <td class="bcs-smtp-col-more">
     1569                                <a class="bcs-smtp-view-btn" href="<?php echo esc_url( $this->email_view_url( (int) $row['id'] ) ); ?>">
     1570                                        <?php esc_html_e( 'View', 'bcs-smtp-manager' ); ?>
     1571                                        <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1572                                        <span class="screen-reader-text">
     1573                                                <?php
     1574                                                /* translators: %s: email subject. */
     1575                                                printf( esc_html__( 'email: %s', 'bcs-smtp-manager' ), esc_html( $subject ) );
     1576                                                ?>
     1577                                        </span>
     1578                                </a>
    9361579                        </td>
    9371580                </tr>
    … …  
    9391582        }
    9401583
     1584        /**
     1585         * Overview health banner. Reads the saved state every time, so it can be re-rendered
     1586         * over AJAX after a save, test or health check and never shows a stale verdict.
     1587         */
     1588        public function render_health_banner() {
     1589                $smtp_host    = get_option( 'bcs_smtp_host', '' );
     1590                $is_first_run = '' === trim( (string) $smtp_host );
     1591                $smtp_enabled = (bool) get_option( 'bcs_smtp_enabled', false );
     1592                $smtp_auth    = (bool) get_option( 'bcs_smtp_auth', true );
     1593                $smtp_port    = (int) get_option( 'bcs_smtp_port', 587 );
     1594                $from_email   = get_option( 'bcs_from_email', get_option( 'admin_email' ) );
     1595                $monitor_on   = BCS_SMTP_Monitor::is_active();
     1596                $last_check   = (int) get_option( BCS_SMTP_OPT_LAST_CHECK, 0 );
     1597                $last_success = get_option( BCS_SMTP_OPT_LAST_CHECK_SUCCESS, null );
     1598                $failures     = (int) get_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, 0 );
     1599                $next_check   = $monitor_on ? wp_next_scheduled( 'bcs_smtp_health_check' ) : false;
     1600                $alert_on     = (bool) get_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED, false );
     1601                $alert_chs    = BCS_SMTP_Monitor::get_channels();
     1602                $ch_labels    = array(
     1603                        'slack'       => __( 'Slack', 'bcs-smtp-manager' ),
     1604                        'teams'       => __( 'Microsoft Teams', 'bcs-smtp-manager' ),
     1605                        'google_chat' => __( 'Google Chat', 'bcs-smtp-manager' ),
     1606                        'discord'     => __( 'Discord', 'bcs-smtp-manager' ),
     1607                        'custom'      => __( 'Custom webhook', 'bcs-smtp-manager' ),
     1608                );
     1609
     1610                if ( ! $smtp_enabled || $is_first_run ) {
     1611                        $health = array( 'tone' => 'off', 'title' => __( 'WordPress is not using SMTP yet', 'bcs-smtp-manager' ), 'text' => __( 'Emails go out through PHP mail(), which inboxes often reject. Connect a mail server to fix that.', 'bcs-smtp-manager' ) );
     1612                } elseif ( null === $last_success ) {
     1613                        $health = array( 'tone' => 'warn', 'title' => __( 'Connection not checked yet', 'bcs-smtp-manager' ), 'text' => __( 'Run a health check to confirm WordPress can send through your mail server.', 'bcs-smtp-manager' ) );
     1614                } elseif ( $last_success ) {
     1615                        $health = array( 'tone' => 'ok', 'title' => __( 'Mail is flowing', 'bcs-smtp-manager' ), 'text' => $smtp_auth ? __( 'WordPress signed in to your mail server and it agreed to send on the last check.', 'bcs-smtp-manager' ) : __( 'Your mail server agreed to send without signing in on the last check.', 'bcs-smtp-manager' ) );
     1616                } else {
     1617                        $health = array(
     1618                                'tone'  => 'bad',
     1619                                'title' => __( 'Mail is not getting through', 'bcs-smtp-manager' ),
     1620                                /* translators: %d: number of consecutive failed health checks. */
     1621                                'text'  => sprintf( _n( 'The last health check failed (%d in a row). Check your sign-in details on the Connection tab.', 'The last health checks failed (%d in a row). Check your sign-in details on the Connection tab.', max( 1, $failures ), 'bcs-smtp-manager' ), max( 1, $failures ) ),
     1622                        );
     1623                }
     1624                ?>
     1625                <section class="bcs-smtp-hero bcs-smtp-hero--<?php echo esc_attr( $health['tone'] ); ?>" aria-labelledby="bcs-smtp-hero-title">
     1626                        <div class="bcs-smtp-hero-main">
     1627                                <span class="bcs-smtp-beacon" aria-hidden="true"><span></span></span>
     1628                                <div class="bcs-smtp-hero-copy">
     1629                                        <h2 id="bcs-smtp-hero-title"><?php echo esc_html( $health['title'] ); ?></h2>
     1630                                        <p><?php echo esc_html( $health['text'] ); ?></p>
     1631                                </div>
     1632                                <div class="bcs-smtp-hero-actions">
     1633                                        <?php if ( 'ok' !== $health['tone'] && 'warn' !== $health['tone'] ) : ?>
     1634                                                <button type="button" class="bcs-smtp-btn bcs-smtp-btn-primary" data-goto="connection">
     1635                                                        <?php echo $is_first_run ? esc_html__( 'Set up in a minute', 'bcs-smtp-manager' ) : esc_html__( 'Fix the connection', 'bcs-smtp-manager' ); ?>
     1636                                                </button>
     1637                                        <?php endif; ?>
     1638                                        <?php if ( ! $is_first_run ) : ?>
     1639                                                <button type="button" class="bcs-smtp-btn bcs-smtp-btn-secondary" data-check-now data-msg="#bcs-smtp-overview-msg">
     1640                                                        <?php esc_html_e( 'Run health check', 'bcs-smtp-manager' ); ?>
     1641                                                </button>
     1642                                        <?php endif; ?>
     1643                                </div>
     1644                        </div>
     1645                        <?php if ( ! $is_first_run ) : ?>
     1646                        <dl class="bcs-smtp-hero-facts">
     1647                                <div><dt><?php esc_html_e( 'Server', 'bcs-smtp-manager' ); ?></dt><dd class="bcs-smtp-mono"><?php echo esc_html( $smtp_host . ':' . $smtp_port ); ?></dd></div>
     1648                                <div><dt><?php esc_html_e( 'Sending as', 'bcs-smtp-manager' ); ?></dt><dd class="bcs-smtp-mono"><?php echo esc_html( $from_email ? $from_email : '—' ); ?></dd></div>
     1649                                <div><dt><?php esc_html_e( 'Last check', 'bcs-smtp-manager' ); ?></dt><dd>
     1650                                        <?php
     1651                                        /* translators: %s: human-readable time difference, e.g. "5 mins". */
     1652                                        echo $last_check ? esc_html( sprintf( __( '%s ago', 'bcs-smtp-manager' ), $this->check_minutes( $last_check ) ) ) : esc_html__( 'Not yet', 'bcs-smtp-manager' );
     1653                                        ?>
     1654                                </dd></div>
     1655                                <div><dt><?php esc_html_e( 'Next check', 'bcs-smtp-manager' ); ?></dt><dd>
     1656                                        <?php
     1657                                        if ( ! $monitor_on ) {
     1658                                                esc_html_e( 'Automatic checks off', 'bcs-smtp-manager' );
     1659                                        } elseif ( $next_check ) {
     1660                                                /* translators: %s: human-readable time difference, e.g. "3 mins". */
     1661                                                echo esc_html( $next_check > time() ? sprintf( __( 'in %s', 'bcs-smtp-manager' ), $this->check_minutes( $next_check, true ) ) : __( 'due now', 'bcs-smtp-manager' ) );
     1662                                        } else {
     1663                                                esc_html_e( 'Scheduling…', 'bcs-smtp-manager' );
     1664                                        }
     1665                                        ?>
     1666                                </dd></div>
     1667                                <div><dt><?php esc_html_e( 'Alerts go to', 'bcs-smtp-manager' ); ?></dt><dd><?php echo ( $alert_on && $alert_chs ) ? esc_html( BCS_SMTP_Monitor::channel_list_label( $alert_chs ) ) : esc_html__( 'Nowhere yet', 'bcs-smtp-manager' ); ?></dd></div>
     1668                        </dl>
     1669                        <?php endif; ?>
     1670                </section>
     1671                <?php
     1672        }
     1673
     1674        /**
     1675         * AJAX — return the freshly rendered health banner.
     1676         */
     1677        public function ajax_get_health_banner() {
     1678                check_ajax_referer( 'bcs_smtp_nonce', 'nonce' );
     1679                if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); }
     1680                ob_start();
     1681                $this->render_health_banner();
     1682                wp_send_json_success( array( 'html' => ob_get_clean() ) );
     1683        }
     1684
     1685        /**
     1686         * The site's own addresses, left out of "People reached" so it counts visitors and
     1687         * customers only: admin email and administrators, the sending address, report recipients,
     1688         * Contact Form 7 notification ("Mail") recipients, and anyone on the sending domain.
     1689         *
     1690         * @return array { 0: string[] addresses, 1: string[] domains }
     1691         */
     1692        private function internal_recipients() {
     1693                $emails   = array( get_option( 'admin_email' ), get_option( 'bcs_from_email', '' ) );
     1694                $emails   = array_merge( $emails, explode( ',', (string) get_option( 'bcs_report_emails', '' ) ) );
     1695                $emails   = array_merge( $emails, wp_list_pluck( get_users( array( 'role' => 'administrator', 'fields' => array( 'user_email' ) ) ), 'user_email' ) );
     1696
     1697                if ( class_exists( 'WPCF7_ContactForm' ) ) {
     1698                        foreach ( WPCF7_ContactForm::find() as $form ) {
     1699                                $mail = $form->prop( 'mail' );
     1700                                if ( ! empty( $mail['recipient'] ) ) {
     1701                                        $to = str_replace( '[_site_admin_email]', get_option( 'admin_email' ), $mail['recipient'] );
     1702                                        // Form-field tags like [your-email] are the visitor, not the team.
     1703                                        $emails = array_merge( $emails, explode( ',', preg_replace( '/\[[^\]]*\]/', '', $to ) ) );
     1704                                }
     1705                        }
     1706                }
     1707
     1708                $emails  = array_values( array_unique( array_filter( array_map( 'strtolower', array_map( 'trim', $emails ) ), 'is_email' ) ) );
     1709                $from    = strtolower( (string) get_option( 'bcs_from_email', '' ) );
     1710                $domains = ( is_email( $from ) && false !== strpos( $from, '@' ) ) ? array( substr( strrchr( $from, '@' ), 1 ) ) : array();
     1711
     1712                return apply_filters( 'bcs_smtp_internal_recipients', array( $emails, $domains ) );
     1713        }
     1714
    9411715        /* ═══════════════════════════════════════════════════════════════════
    9421716           PAGE RENDER
    … …  
    9461720                if ( ! current_user_can( 'manage_options' ) ) { return; }
    9471721
    948                 $info          = $this->mailer->detect_smtp_plugin();
     1722                // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only view, capability checked above.
     1723                if ( isset( $_GET['email'] ) ) {
     1724                        $this->render_email_view( absint( $_GET['email'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
     1725                        return;
     1726                }
     1727
    9491728
    9501729                // SMTP settings
     1730                $smtp_host     = get_option( 'bcs_smtp_host', '' );
     1731                $is_first_run  = '' === trim( $smtp_host );
    9511732                $smtp_enabled  = (bool) get_option( 'bcs_smtp_enabled', false );
    9521733                $from_email    = get_option( 'bcs_from_email', get_option( 'admin_email' ) );
    9531734                $from_name     = get_option( 'bcs_from_name',  get_option( 'blogname' ) );
    954                 $smtp_host     = get_option( 'bcs_smtp_host', '' );
    9551735                $smtp_port     = (int) get_option( 'bcs_smtp_port', 587 );
    9561736                $smtp_enc      = get_option( 'bcs_smtp_encryption', 'tls' );
    … …  
    9581738                $smtp_auth     = (bool) get_option( 'bcs_smtp_auth', true );
    9591739                $smtp_user     = get_option( 'bcs_smtp_username', '' );
     1740                $has_pw        = ! empty( get_option( 'bcs_smtp_password', '' ) );
     1741                $pw_unreadable = $has_pw && $this->mailer->is_password_unreadable();
     1742                $presets       = $this->provider_presets();
     1743                $provider      = $this->detect_provider( $smtp_host );
     1744                $preset        = $provider ? $presets[ $provider ] : $presets['other'];
    9601745
    9611746                // Monitor settings
    962                 $monitor_on    = (bool) get_option( BCS_SMTP_OPT_MONITOR_ENABLED, false );
    963                 $check_interval = (int) get_option( BCS_SMTP_OPT_CHECK_INTERVAL, 5 );
    964                 $threshold     = (int) get_option( BCS_SMTP_OPT_FAILURE_THRESHOLD, 2 );
    965                 $last_check    = (int) get_option( BCS_SMTP_OPT_LAST_CHECK, 0 );
    966                 $last_success  = get_option( BCS_SMTP_OPT_LAST_CHECK_SUCCESS, null );
     1747                $monitor_on     = BCS_SMTP_Monitor::is_active();
     1748                $last_check     = (int) get_option( BCS_SMTP_OPT_LAST_CHECK, 0 );
     1749                $last_success   = get_option( BCS_SMTP_OPT_LAST_CHECK_SUCCESS, null );
     1750                $failures       = (int) get_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, 0 );
     1751                $next_check     = $monitor_on ? wp_next_scheduled( 'bcs_smtp_health_check' ) : false;
    9671752
    9681753                // Alert settings
    969                 $alert_enabled = (bool) get_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED, false );
    970                 $alert_channel = get_option( BCS_SMTP_OPT_ALERT_CHANNEL, 'none' );
    971 
    972                 // Summary for stat row
    973                 $smtp_status   = is_null( $last_success ) ? 'unknown' : ( $last_success ? 'ok' : 'fail' );
    974 
    975                 // Report defaults (30-day)
    976                 $summary_30d   = $this->db->get_email_summary( '30days' );
     1754                $alert_enabled  = (bool) get_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED, false );
     1755                $alert_chs      = BCS_SMTP_Monitor::get_channels();
     1756                $channels       = array(
     1757                        'slack'       => array( 'label' => __( 'Slack', 'bcs-smtp-manager' ),          'short' => 'S' ),
     1758                        'teams'       => array( 'label' => __( 'Microsoft Teams', 'bcs-smtp-manager' ), 'short' => 'T' ),
     1759                        'google_chat' => array( 'label' => __( 'Google Chat', 'bcs-smtp-manager' ),     'short' => 'GC' ),
     1760                        'discord'     => array( 'label' => __( 'Discord', 'bcs-smtp-manager' ),         'short' => 'D' ),
     1761                        'custom'      => array( 'label' => __( 'Custom webhook', 'bcs-smtp-manager' ),  'short' => '{ }' ),
     1762                );
     1763
     1764                // Overview data
     1765                $summary_30d  = $this->db->get_email_summary( '30days' );
     1766                $rate         = $summary_30d['total'] > 0 ? round( ( $summary_30d['success'] / $summary_30d['total'] ) * 100, 1 ) : null;
     1767                $chart        = $this->db->get_email_chart_data( '30days' );
    9771768
    9781769                // Scheduled report settings
    9791770                $report_schedule_enabled = (bool) get_option( 'bcs_report_schedule_enabled', false );
    9801771                $report_emails           = get_option( 'bcs_report_emails', '' );
    981                 $report_frequency        = get_option( 'bcs_report_frequency', 'weekly' );
    982 
    983                 // Resend settings
    984                 $store_failed_body     = (bool) get_option( BCS_SMTP_OPT_STORE_FAILED_BODY, false );
    985                 $failed_body_retention = (int) get_option( BCS_SMTP_OPT_FAILED_BODY_RETENTION_DAYS, 7 );
     1772
     1773
     1774                $setup_items = array(
     1775                        array( 'done' => $smtp_enabled && ! $is_first_run, 'label' => __( 'Connect your mail server', 'bcs-smtp-manager' ), 'goto' => 'connection' ),
     1776                        array( 'done' => $alert_enabled && $alert_chs, 'label' => __( 'Choose where failure alerts go', 'bcs-smtp-manager' ), 'goto' => 'alerts' ),
     1777                        array( 'done' => $report_schedule_enabled,          'label' => __( 'Get a delivery report by email', 'bcs-smtp-manager' ), 'goto' => 'reports' ),
     1778                );
     1779                $setup_left = count( array_filter( $setup_items, function ( $i ) { return ! $i['done']; } ) );
     1780
     1781                $tabs = array(
     1782                        'overview'   => __( 'Overview', 'bcs-smtp-manager' ),
     1783                        'connection' => __( 'Connection', 'bcs-smtp-manager' ),
     1784                        'alerts'     => __( 'Alerts', 'bcs-smtp-manager' ),
     1785                        'reports'    => __( 'Reports', 'bcs-smtp-manager' ),
     1786                        'email-log'  => __( 'Email log', 'bcs-smtp-manager' ),
     1787                );
     1788                $date_ranges = array(
     1789                        'today'      => __( 'Today', 'bcs-smtp-manager' ),
     1790                        'yesterday'  => __( 'Yesterday', 'bcs-smtp-manager' ),
     1791                        '7days'      => __( 'Last 7 days', 'bcs-smtp-manager' ),
     1792                        '30days'     => __( 'Last 30 days', 'bcs-smtp-manager' ),
     1793                        'this_week'  => __( 'This week', 'bcs-smtp-manager' ),
     1794                        'last_week'  => __( 'Last week', 'bcs-smtp-manager' ),
     1795                        'this_month' => __( 'This month', 'bcs-smtp-manager' ),
     1796                        '3months'    => __( 'Last 3 months', 'bcs-smtp-manager' ),
     1797                );
     1798                $time_fmt = get_option( 'time_format', 'g:i a' );
    9861799                ?>
    987                 <div class="bcs-smtp-wrap wrap">
    988 
    989                         <!-- ═══ HEADER ═══════════════════════════════════════════════════ -->
    990                         <div class="bcs-smtp-header">
    991                                 <div class="bcs-smtp-header-left">
    992                                         <div class="bcs-smtp-header-icon">
    993                                                 <span class="dashicons dashicons-email-alt"></span>
    994                                         </div>
     1800                <div class="bcs-smtp-wrap wrap" data-first-run="<?php echo $is_first_run ? '1' : '0'; ?>">
     1801
     1802                        <header class="bcs-smtp-header">
     1803                                <div class="bcs-smtp-brand">
     1804                                        <span class="bcs-smtp-brand-mark" aria-hidden="true">
     1805                                                <svg viewBox="0 0 24 24" focusable="false"><rect x="3.5" y="6" width="17" height="12.5" rx="2.5"/><path d="m4.5 7.5 7.5 5.75 7.5-5.75"/></svg>
     1806                                        </span>
    9951807                                        <div>
    996                                                 <h1><?php esc_html_e( 'ByteCoreStack - SMTP & Health Monitor', 'bcs-smtp-manager' ); ?></h1>
    997                                                 <p class="bcs-smtp-header-sub"><?php esc_html_e( 'WordPress email monitoring & alerting by ByteCore Stack', 'bcs-smtp-manager' ); ?></p>
     1808                                                <h1><?php esc_html_e( 'SMTP & Health Monitor', 'bcs-smtp-manager' ); ?></h1>
     1809                                                <p class="bcs-smtp-header-sub">
     1810                                                        <?php echo esc_html( wp_parse_url( home_url(), PHP_URL_HOST ) ); ?>
     1811                                                        <span aria-hidden="true">·</span>
     1812                                                        <?php
     1813                                                        /* translators: %s: plugin version number. */
     1814                                                        printf( esc_html__( 'ByteCoreStack · v%s', 'bcs-smtp-manager' ), esc_html( BCS_SMTP_VERSION ) );
     1815                                                        ?>
     1816                                                </p>
    9981817                                        </div>
    9991818                                </div>
    1000                                 <div class="bcs-smtp-header-right">
    1001                                         <span class="bcs-smtp-header-badge">v<?php echo esc_html( BCS_SMTP_VERSION ); ?></span>
     1819                                <div class="bcs-smtp-header-actions">
     1820                                        <button type="button" class="bcs-smtp-btn bcs-smtp-btn-primary" data-goto="connection" data-focus="#bcs-smtp-test-to">
     1821                                                <?php esc_html_e( 'Send test email', 'bcs-smtp-manager' ); ?>
     1822                                        </button>
    10021823                                </div>
    1003                         </div>
    1004 
    1005                         <!-- ═══ STAT ROW ══════════════════════════════════════════════════ -->
    1006                         <div class="bcs-smtp-stat-row">
    1007                                 <div class="bcs-smtp-stat">
    1008                                         <div class="bcs-smtp-stat-icon <?php echo 'ok' === $smtp_status ? 'good' : ( 'fail' === $smtp_status ? 'bad' : '' ); ?>">
    1009                                                 <span class="dashicons dashicons-yes-alt"></span>
    1010                                         </div>
    1011                                         <div class="bcs-smtp-stat-body">
    1012                                                 <div class="bcs-smtp-stat-value <?php echo 'ok' === $smtp_status ? 'good' : ( 'fail' === $smtp_status ? 'bad' : '' ); ?>">
    1013                                                         <?php echo 'ok' === $smtp_status ? '✓' : ( 'fail' === $smtp_status ? '✗' : '—' ); ?>
    1014                                                 </div>
    1015                                                 <div class="bcs-smtp-stat-label"><?php esc_html_e( 'SMTP Status', 'bcs-smtp-manager' ); ?></div>
     1824                        </header>
     1825
     1826                        <nav class="bcs-smtp-tabs" role="tablist" aria-label="<?php esc_attr_e( 'SMTP Manager sections', 'bcs-smtp-manager' ); ?>">
     1827                                <span class="bcs-smtp-tab-pill" aria-hidden="true"></span>
     1828                                <?php
     1829                                $tab_icons = array(
     1830                                        'overview'   => 'dashboard',
     1831                                        'connection' => 'admin-plugins',
     1832                                        'alerts'     => 'bell',
     1833                                        'reports'    => 'chart-bar',
     1834                                        'email-log'  => 'email-alt',
     1835                                );
     1836                                foreach ( $tabs as $key => $label ) :
     1837                                        ?>
     1838                                        <button type="button" role="tab" class="bcs-smtp-tab" id="bcs-smtp-tab-<?php echo esc_attr( $key ); ?>" data-panel="<?php echo esc_attr( $key ); ?>" aria-controls="bcs-smtp-panel-<?php echo esc_attr( $key ); ?>" aria-selected="false" tabindex="-1">
     1839                                                <span class="dashicons dashicons-<?php echo esc_attr( $tab_icons[ $key ] ); ?>" aria-hidden="true"></span>
     1840                                                <?php echo esc_html( $label ); ?>
     1841                                                <?php if ( 'connection' === $key ) : ?>
     1842                                                        <span class="bcs-smtp-tab-dot" id="bcs-smtp-conn-dot" aria-hidden="true"></span>
     1843                                                        <span class="screen-reader-text" id="bcs-smtp-conn-dot-label"></span>
     1844                                                <?php endif; ?>
     1845                                                <?php if ( 'email-log' === $key && $summary_30d['failed'] > 0 ) : ?>
     1846                                                        <span class="bcs-smtp-tab-count" title="<?php esc_attr_e( 'Failed in the last 30 days', 'bcs-smtp-manager' ); ?>"><?php echo esc_html( number_format_i18n( $summary_30d['failed'] ) ); ?></span>
     1847                                                <?php endif; ?>
     1848                                        </button>
     1849                                <?php endforeach; ?>
     1850                        </nav>
     1851
     1852                        <div id="bcs-smtp-toast" class="bcs-smtp-toast" role="status" aria-live="polite"></div>
     1853
     1854                        <!-- ═══ PANEL: Overview ═══════════════════════════════════════════ -->
     1855                        <section id="bcs-smtp-panel-overview" class="bcs-smtp-tab-panel" role="tabpanel" aria-labelledby="bcs-smtp-tab-overview">
     1856
     1857                                <div id="bcs-smtp-health-banner"><?php $this->render_health_banner(); ?></div>
     1858                                <div id="bcs-smtp-overview-msg" class="bcs-smtp-msg" role="status" aria-live="polite"></div>
     1859
     1860
     1861                                <div class="bcs-smtp-overview-grid">
     1862                                        <section class="bcs-smtp-panel bcs-smtp-activity" aria-labelledby="bcs-smtp-activity-title">
     1863                                                <div class="bcs-smtp-panel-head">
     1864                                                        <h2 id="bcs-smtp-activity-title"><?php esc_html_e( 'Email activity', 'bcs-smtp-manager' ); ?></h2>
     1865                                                        <div class="bcs-smtp-segmented bcs-smtp-segmented--sm" role="radiogroup" aria-label="<?php esc_attr_e( 'Time range', 'bcs-smtp-manager' ); ?>">
     1866                                                                <?php foreach ( array( '7days' => __( '7 days', 'bcs-smtp-manager' ), '30days' => __( '30 days', 'bcs-smtp-manager' ), '3months' => __( '90 days', 'bcs-smtp-manager' ) ) as $val => $label ) : ?>
     1867                                                                        <label><input type="radio" name="bcs_smtp_ov_range" class="bcs-smtp-ov-range" autocomplete="off" value="<?php echo esc_attr( $val ); ?>" <?php checked( '30days', $val ); ?>><span><?php echo esc_html( $label ); ?></span></label>
     1868                                                                <?php endforeach; ?>
     1869                                                        </div>
     1870                                                </div>
     1871                                                <dl class="bcs-smtp-figures" id="bcs-smtp-ov-figures">
     1872                                                        <div><dt><span class="bcs-smtp-key bcs-smtp-key--sent" aria-hidden="true"></span><?php esc_html_e( 'Sent', 'bcs-smtp-manager' ); ?></dt><dd data-fig="success"><?php echo esc_html( number_format_i18n( $summary_30d['success'] ) ); ?></dd></div>
     1873                                                        <div><dt><span class="bcs-smtp-key bcs-smtp-key--bad" aria-hidden="true"></span><?php esc_html_e( 'Failed', 'bcs-smtp-manager' ); ?></dt><dd data-fig="failed" class="<?php echo $summary_30d['failed'] > 0 ? 'is-bad' : ''; ?>"><?php echo esc_html( number_format_i18n( $summary_30d['failed'] ) ); ?></dd></div>
     1874                                                        <div><dt><span class="bcs-smtp-key bcs-smtp-key--warn" aria-hidden="true"></span><?php esc_html_e( 'Unconfirmed', 'bcs-smtp-manager' ); ?></dt><dd data-fig="unconfirmed"><?php echo esc_html( number_format_i18n( $summary_30d['unconfirmed'] ) ); ?></dd></div>
     1875                                                        <div><dt><?php esc_html_e( 'Delivered', 'bcs-smtp-manager' ); ?></dt><dd data-fig="rate"><?php echo null === $rate ? '—' : esc_html( number_format_i18n( $rate, 1 ) ) . '<small>%</small>'; ?></dd></div>
     1876                                                </dl>
     1877                                                <div class="bcs-smtp-linechart" id="bcs-smtp-ov-chart" tabindex="0"
     1878                                                        role="img" aria-label="<?php esc_attr_e( 'Emails per day: sent, failed and unconfirmed. Use the left and right arrow keys to read each day.', 'bcs-smtp-manager' ); ?>"
     1879                                                        data-chart="<?php echo esc_attr( wp_json_encode( $chart ) ); ?>">
     1880                                                        <div class="bcs-smtp-linechart-tip" aria-live="polite" hidden></div>
     1881                                                </div>
     1882                                                <p class="bcs-smtp-chart-empty" id="bcs-smtp-ov-empty" <?php echo $summary_30d['total'] > 0 ? 'hidden' : ''; ?>><?php esc_html_e( 'No email was sent in this period.', 'bcs-smtp-manager' ); ?></p>
     1883                                                <?php if ( $summary_30d['failed'] > 0 ) : ?>
     1884                                                        <p class="bcs-smtp-activity-foot">
     1885                                                                <?php
     1886                                                                /* translators: %s: number of failed emails in the last 30 days. */
     1887                                                                printf( esc_html( _n( '%s email failed in the last 30 days.', '%s emails failed in the last 30 days.', $summary_30d['failed'], 'bcs-smtp-manager' ) ), esc_html( number_format_i18n( $summary_30d['failed'] ) ) );
     1888                                                                ?>
     1889                                                                <button type="button" class="bcs-smtp-link" data-goto="email-log" data-log-status="failed"><?php esc_html_e( 'Review failed emails', 'bcs-smtp-manager' ); ?></button>
     1890                                                        </p>
     1891                                                <?php endif; ?>
     1892                                        </section>
     1893
     1894                                        <div class="bcs-smtp-side">
     1895                                        <?php if ( $setup_left > 0 ) : ?>
     1896                                        <section class="bcs-smtp-panel bcs-smtp-setup-card" aria-labelledby="bcs-smtp-setup-title">
     1897                                                <div class="bcs-smtp-panel-head">
     1898                                                        <h2 id="bcs-smtp-setup-title"><?php esc_html_e( 'Finish setting up', 'bcs-smtp-manager' ); ?></h2>
     1899                                                        <span class="bcs-smtp-panel-sub">
     1900                                                                <?php
     1901                                                                /* translators: 1: completed setup steps, 2: total setup steps. */
     1902                                                                printf( esc_html__( '%1$d of %2$d done', 'bcs-smtp-manager' ), count( $setup_items ) - $setup_left, count( $setup_items ) );
     1903                                                                ?>
     1904                                                        </span>
     1905                                                </div>
     1906                                                <div class="bcs-smtp-progress" aria-hidden="true">
     1907                                                        <?php foreach ( $setup_items as $item ) : ?>
     1908                                                                <span class="<?php echo $item['done'] ? 'is-done' : ''; ?>"></span>
     1909                                                        <?php endforeach; ?>
     1910                                                </div>
     1911                                                <ul class="bcs-smtp-checklist">
     1912                                                        <?php foreach ( $setup_items as $item ) : ?>
     1913                                                                <li class="<?php echo $item['done'] ? 'is-done' : ''; ?>">
     1914                                                                        <span class="bcs-smtp-check-dot dashicons <?php echo $item['done'] ? 'dashicons-yes' : ''; ?>" aria-hidden="true"></span>
     1915                                                                        <?php if ( $item['done'] ) : ?>
     1916                                                                                <span><?php echo esc_html( $item['label'] ); ?></span>
     1917                                                                                <span class="screen-reader-text"><?php esc_html_e( '(done)', 'bcs-smtp-manager' ); ?></span>
     1918                                                                        <?php else : ?>
     1919                                                                                <button type="button" class="bcs-smtp-link bcs-smtp-link--plain" data-goto="<?php echo esc_attr( $item['goto'] ); ?>"><?php echo esc_html( $item['label'] ); ?><span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span></button>
     1920                                                                        <?php endif; ?>
     1921                                                                </li>
     1922                                                        <?php endforeach; ?>
     1923                                                </ul>
     1924                                        </section>
     1925                                        <?php endif; ?>
     1926                                        <section class="bcs-smtp-panel bcs-smtp-quick" aria-labelledby="bcs-smtp-quick-title">
     1927                                                <div class="bcs-smtp-panel-head">
     1928                                                        <h2 id="bcs-smtp-quick-title"><?php esc_html_e( 'Quick actions', 'bcs-smtp-manager' ); ?></h2>
     1929                                                </div>
     1930                                                <ul class="bcs-smtp-quick-list">
     1931                                                        <li><button type="button" data-goto="connection" data-focus="#bcs-smtp-test-to">
     1932                                                                <span class="bcs-smtp-quick-icon dashicons dashicons-email-alt" aria-hidden="true"></span>
     1933                                                                <span class="bcs-smtp-quick-text"><strong><?php esc_html_e( 'Send a test email', 'bcs-smtp-manager' ); ?></strong></span>
     1934                                                                <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1935                                                        </button></li>
     1936                                                        <li><button type="button" data-goto="email-log" data-log-status="failed">
     1937                                                                <span class="bcs-smtp-quick-icon bcs-smtp-quick-icon--bad dashicons dashicons-warning" aria-hidden="true"></span>
     1938                                                                <span class="bcs-smtp-quick-text"><strong><?php esc_html_e( 'Review failed emails', 'bcs-smtp-manager' ); ?></strong></span>
     1939                                                                <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1940                                                        </button></li>
     1941                                                        <li><button type="button" data-goto="alerts">
     1942                                                                <span class="bcs-smtp-quick-icon dashicons dashicons-bell" aria-hidden="true"></span>
     1943                                                                <span class="bcs-smtp-quick-text"><strong><?php esc_html_e( 'Failure alerts', 'bcs-smtp-manager' ); ?></strong></span>
     1944                                                                <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1945                                                        </button></li>
     1946                                                        <li><button type="button" data-export-log>
     1947                                                                <span class="bcs-smtp-quick-icon dashicons dashicons-download" aria-hidden="true"></span>
     1948                                                                <span class="bcs-smtp-quick-text"><strong><?php esc_html_e( 'Export email log', 'bcs-smtp-manager' ); ?></strong></span>
     1949                                                                <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1950                                                        </button></li>
     1951                                                        <?php if ( 0 === $setup_left ) : // The setup card is gone, so this card takes the whole column. ?>
     1952                                                        <li><button type="button" data-check-now data-msg="#bcs-smtp-overview-msg">
     1953                                                                <span class="bcs-smtp-quick-icon bcs-smtp-quick-icon--ok dashicons dashicons-heart" aria-hidden="true"></span>
     1954                                                                <span class="bcs-smtp-quick-text"><strong><?php esc_html_e( 'Run health check', 'bcs-smtp-manager' ); ?></strong></span>
     1955                                                                <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1956                                                        </button></li>
     1957                                                        <li><button type="button" data-goto="reports">
     1958                                                                <span class="bcs-smtp-quick-icon dashicons dashicons-chart-area" aria-hidden="true"></span>
     1959                                                                <span class="bcs-smtp-quick-text"><strong><?php esc_html_e( 'Delivery report', 'bcs-smtp-manager' ); ?></strong></span>
     1960                                                                <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1961                                                        </button></li>
     1962                                                        <li><button type="button" data-goto="connection">
     1963                                                                <span class="bcs-smtp-quick-icon dashicons dashicons-admin-network" aria-hidden="true"></span>
     1964                                                                <span class="bcs-smtp-quick-text"><strong><?php esc_html_e( 'Connection settings', 'bcs-smtp-manager' ); ?></strong></span>
     1965                                                                <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1966                                                        </button></li>
     1967                                                        <?php endif; ?>
     1968                                                </ul>
     1969                                        </section>
    10161970                                        </div>
    10171971                                </div>
    1018                                 <div class="bcs-smtp-stat">
    1019                                         <div class="bcs-smtp-stat-icon <?php echo $smtp_enabled ? 'good' : ''; ?>">
    1020                                                 <span class="dashicons dashicons-admin-plugins"></span>
    1021                                         </div>
    1022                                         <div class="bcs-smtp-stat-body">
    1023                                                 <div class="bcs-smtp-stat-value <?php echo $smtp_enabled ? 'good' : ''; ?>">
    1024                                                         <?php echo $smtp_enabled ? esc_html__( 'On', 'bcs-smtp-manager' ) : esc_html__( 'Off', 'bcs-smtp-manager' ); ?>
    1025                                                 </div>
    1026                                                 <div class="bcs-smtp-stat-label"><?php esc_html_e( 'SMTP Active', 'bcs-smtp-manager' ); ?></div>
    1027                                         </div>
    1028                                 </div>
    1029                                 <div class="bcs-smtp-stat">
    1030                                         <div class="bcs-smtp-stat-icon">
    1031                                                 <span class="dashicons dashicons-email-alt"></span>
    1032                                         </div>
    1033                                         <div class="bcs-smtp-stat-body">
    1034                                                 <div class="bcs-smtp-stat-value" style="font-size:18px"><?php echo esc_html( number_format( $summary_30d['total'] ) ); ?></div>
    1035                                                 <div class="bcs-smtp-stat-label"><?php esc_html_e( 'Emails (30d)', 'bcs-smtp-manager' ); ?></div>
    1036                                         </div>
    1037                                 </div>
    1038                                 <div class="bcs-smtp-stat">
    1039                                         <div class="bcs-smtp-stat-icon good">
    1040                                                 <span class="dashicons dashicons-yes"></span>
    1041                                         </div>
    1042                                         <div class="bcs-smtp-stat-body">
    1043                                                 <div class="bcs-smtp-stat-value good" style="font-size:22px"><?php echo esc_html( number_format( $summary_30d['success'] ) ); ?></div>
    1044                                                 <div class="bcs-smtp-stat-label"><?php esc_html_e( 'Sent (30d)', 'bcs-smtp-manager' ); ?></div>
    1045                                         </div>
    1046                                 </div>
    1047                                 <div class="bcs-smtp-stat">
    1048                                         <div class="bcs-smtp-stat-icon <?php echo $summary_30d['failed'] > 0 ? 'bad' : ( $last_check ? 'good' : '' ); ?>">
    1049                                                 <span class="dashicons dashicons-warning"></span>
    1050                                         </div>
    1051                                         <div class="bcs-smtp-stat-body">
    1052                                                 <div class="bcs-smtp-stat-value <?php echo $summary_30d['failed'] > 0 ? 'bad' : ( $last_check ? 'good' : '' ); ?>" style="font-size:22px">
    1053                                                         <?php echo esc_html( number_format( $summary_30d['failed'] ) ); ?>
    1054                                                 </div>
    1055                                                 <div class="bcs-smtp-stat-label"><?php esc_html_e( 'Failed (30d)', 'bcs-smtp-manager' ); ?></div>
    1056                                         </div>
    1057                                 </div>
    1058                         </div>
    1059 
    1060                         <!-- ═══ TABS ══════════════════════════════════════════════════════ -->
    1061                         <div class="bcs-smtp-tabs">
    1062                                 <button type="button" class="bcs-smtp-tab active" data-panel="settings">
    1063                                         <span class="dashicons dashicons-admin-settings"></span><?php esc_html_e( 'Settings', 'bcs-smtp-manager' ); ?>
    1064                                 </button>
    1065                                 <button type="button" class="bcs-smtp-tab" data-panel="email-log">
    1066                                         <span class="dashicons dashicons-list-view"></span><?php esc_html_e( 'Email Log', 'bcs-smtp-manager' ); ?>
    1067                                 </button>
    1068                                 <button type="button" class="bcs-smtp-tab" data-panel="email-report">
    1069                                         <span class="dashicons dashicons-chart-bar"></span><?php esc_html_e( 'Email Report', 'bcs-smtp-manager' ); ?>
    1070                                 </button>
    1071                                 <button type="button" class="bcs-smtp-tab" data-panel="test">
    1072                                         <span class="dashicons dashicons-email"></span><?php esc_html_e( 'Send Test Email', 'bcs-smtp-manager' ); ?>
    1073                                 </button>
    1074                                 <button type="button" class="bcs-smtp-tab" data-panel="alerts">
    1075                                         <span class="dashicons dashicons-bell"></span><?php esc_html_e( 'Alerts', 'bcs-smtp-manager' ); ?>
    1076                                 </button>
    1077                                 <button type="button" class="bcs-smtp-tab" data-panel="config">
    1078                                         <span class="dashicons dashicons-admin-generic"></span><?php esc_html_e( 'Config', 'bcs-smtp-manager' ); ?>
    1079                                 </button>
    1080                         </div>
    1081 
    1082                         <!-- ═══════════════════════════════════════════════════════════════
    1083                              PANEL: Settings (SMTP connection)
    1084                              ═══════════════════════════════════════════════════════════════ -->
    1085                         <div id="bcs-smtp-panel-settings" class="bcs-smtp-tab-panel active">
    1086                                 <div class="bcs-smtp-grid">
    1087                                 <div>
    1088                                         <div class="bcs-smtp-card">
    1089                                                 <div class="bcs-smtp-card-header">
    1090                                                         <p class="bcs-smtp-card-title"><span class="dashicons dashicons-email-alt"></span><?php esc_html_e( 'SMTP Connection', 'bcs-smtp-manager' ); ?></p>
    1091                                                         <p class="bcs-smtp-card-desc"><?php esc_html_e( 'Configure this plugin to send all WordPress emails via your SMTP server. Leave disabled to use PHP mail or another SMTP plugin.', 'bcs-smtp-manager' ); ?></p>
    1092                                                 </div>
    1093                                                 <div class="bcs-smtp-card-body">
    1094                                                         <!-- Enable toggle -->
    1095                                                         <div class="bcs-smtp-toggle-row" style="margin-bottom:0">
    1096                                                                 <div class="bcs-smtp-toggle-row-info">
    1097                                                                         <div class="bcs-smtp-toggle-row-label"><?php esc_html_e( 'Enable SMTP', 'bcs-smtp-manager' ); ?></div>
    1098                                                                         <div class="bcs-smtp-toggle-row-desc"><?php esc_html_e( 'Override WordPress default email with your SMTP settings below', 'bcs-smtp-manager' ); ?></div>
     1972                        </section>
     1973
     1974                        <!-- ═══ PANEL: Connection (guided setup) ══════════════════════════ -->
     1975                        <section id="bcs-smtp-panel-connection" class="bcs-smtp-tab-panel" role="tabpanel" aria-labelledby="bcs-smtp-tab-connection">
     1976                                <div class="bcs-smtp-split">
     1977                                        <div class="bcs-smtp-panel bcs-smtp-setup">
     1978
     1979                                                <div class="bcs-smtp-setup-head">
     1980                                                        <span class="bcs-smtp-setup-badge dashicons dashicons-email-alt" aria-hidden="true"></span>
     1981                                                        <div>
     1982                                                                <h2><label for="bcs-smtp-smtp-enabled"><?php esc_html_e( 'SMTP connection', 'bcs-smtp-manager' ); ?></label></h2>
     1983                                                                <p><?php esc_html_e( 'Send all WordPress email through your mail server.', 'bcs-smtp-manager' ); ?></p>
     1984                                                        </div>
     1985                                                        <span class="bcs-smtp-toggle">
     1986                                                                <input type="checkbox" id="bcs-smtp-smtp-enabled" role="switch" aria-controls="bcs-smtp-setup-body" <?php checked( $smtp_enabled || $is_first_run ); ?>>
     1987                                                                <span class="bcs-smtp-toggle-slider" aria-hidden="true"></span>
     1988                                                        </span>
     1989                                                </div>
     1990
     1991                                                <?php $smtp_on = $smtp_enabled || $is_first_run; ?>
     1992                                                <div class="bcs-smtp-alerts-off bcs-smtp-smtp-off" id="bcs-smtp-smtp-off" <?php echo $smtp_on ? 'hidden' : ''; ?>>
     1993                                                        <span class="dashicons dashicons-email-alt" aria-hidden="true"></span>
     1994                                                        <p><?php esc_html_e( 'SMTP is off, so WordPress sends email with PHP mail(), which often lands in spam. Turn it on to send through your mail server. Your settings are kept.', 'bcs-smtp-manager' ); ?></p>
     1995                                                </div>
     1996
     1997                                                <div class="bcs-smtp-setup-body" id="bcs-smtp-setup-body" <?php echo $smtp_on ? '' : 'hidden'; ?>>
     1998                                                        <div class="bcs-smtp-field" id="bcs-smtp-step-provider">
     1999                                                                <span class="bcs-smtp-field-label" id="bcs-smtp-provider-label"><?php esc_html_e( 'Email provider', 'bcs-smtp-manager' ); ?></span>
     2000                                                                <?php
     2001                                                                // Once a provider is chosen, show just that choice with a Change link; the full list opens on demand.
     2002                                                                $picked = ( $provider && isset( $presets[ $provider ] ) ) ? $presets[ $provider ] : null;
     2003                                                                ?>
     2004                                                                <div class="bcs-smtp-provider-picked" id="bcs-smtp-provider-picked" <?php echo $picked ? '' : 'hidden'; ?>>
     2005                                                                        <span class="bcs-smtp-provider-mark" id="bcs-smtp-picked-mark" aria-hidden="true"></span>
     2006                                                                        <span class="bcs-smtp-picked-name" id="bcs-smtp-picked-name"><?php echo $picked ? esc_html( $picked['label'] ) : ''; ?></span>
     2007                                                                        <button type="button" class="bcs-smtp-link" id="bcs-smtp-provider-change" aria-expanded="false" aria-controls="bcs-smtp-provider-chips"><?php esc_html_e( 'Change', 'bcs-smtp-manager' ); ?></button>
    10992008                                                                </div>
    1100                                                                 <label class="bcs-smtp-toggle">
    1101                                                                         <input type="checkbox" id="bcs-smtp-smtp-enabled" <?php checked( $smtp_enabled ); ?>>
    1102                                                                         <span class="bcs-smtp-toggle-slider"></span>
    1103                                                                 </label>
     2009                                                                <div class="bcs-smtp-provider-chips" id="bcs-smtp-provider-chips" role="radiogroup" aria-labelledby="bcs-smtp-provider-label" <?php echo $picked ? 'hidden' : ''; ?>>
     2010                                                                        <?php
     2011                                                                        // Each provider's own colour tints its badge, so the list scans by colour as well as name.
     2012                                                                        $provider_tones = array(
     2013                                                                                'gmail'     => '#ea4335',
     2014                                                                                'microsoft' => '#2b88d8',
     2015                                                                                'sendgrid'  => '#1a82e2',
     2016                                                                                'ses'       => '#ff9900',
     2017                                                                                'mailgun'   => '#f06b66',
     2018                                                                                'brevo'     => '#0fae7b',
     2019                                                                                'zoho'      => '#f0b400',
     2020                                                                                'yahoo'     => '#8f4bff',
     2021                                                                        );
     2022                                                                        $provider_short = array(
     2023                                                                                'gmail'     => __( 'Gmail', 'bcs-smtp-manager' ),
     2024                                                                                'microsoft' => __( 'Microsoft 365', 'bcs-smtp-manager' ),
     2025                                                                                'zoho'      => __( 'Zoho', 'bcs-smtp-manager' ),
     2026                                                                                'yahoo'     => __( 'Yahoo', 'bcs-smtp-manager' ),
     2027                                                                                'other'     => __( 'Other', 'bcs-smtp-manager' ),
     2028                                                                        );
     2029                                                                        foreach ( $presets as $key => $p ) :
     2030                                                                                ?>
     2031                                                                                <label class="bcs-smtp-provider" title="<?php echo esc_attr( $p['label'] ); ?>" style="--tone:<?php echo esc_attr( isset( $provider_tones[ $key ] ) ? $provider_tones[ $key ] : '#8a97b0' ); ?>">
     2032                                                                                        <input type="radio" name="bcs_smtp_provider" class="bcs-smtp-provider-radio" value="<?php echo esc_attr( $key ); ?>"
     2033                                                                                                data-host="<?php echo esc_attr( $p['host'] ); ?>"
     2034                                                                                                data-port="<?php echo esc_attr( $p['port'] ); ?>"
     2035                                                                                                data-enc="<?php echo esc_attr( $p['enc'] ); ?>"
     2036                                                                                                data-user="<?php echo esc_attr( $p['user'] ); ?>"
     2037                                                                                                data-user-hint="<?php echo esc_attr( $p['user_hint'] ); ?>"
     2038                                                                                                data-pass-label="<?php echo esc_attr( $p['pass_label'] ); ?>"
     2039                                                                                                data-note="<?php echo esc_attr( $p['note'] ); ?>"
     2040                                                                                                data-link="<?php echo esc_url( $p['link'] ); ?>"
     2041                                                                                                data-link-label="<?php echo esc_attr( $p['link_label'] ); ?>"
     2042                                                                                                <?php checked( $provider, $key ); ?>>
     2043                                                                                        <span class="bcs-smtp-provider-mark" aria-hidden="true"><?php echo $p['short'] ? esc_html( $p['short'] ) : '<span class="dashicons dashicons-admin-generic"></span>'; ?></span>
     2044                                                                                        <span class="bcs-smtp-provider-name"><?php echo esc_html( isset( $provider_short[ $key ] ) ? $provider_short[ $key ] : $p['label'] ); ?></span>
     2045                                                                                </label>
     2046                                                                        <?php endforeach; ?>
     2047                                                                </div>
     2048                                                                <p class="bcs-smtp-provider-note" id="bcs-smtp-provider-note" <?php echo $provider ? '' : 'hidden'; ?>>
     2049                                                                        <span class="dashicons dashicons-info-outline" aria-hidden="true"></span>
     2050                                                                        <span>
     2051                                                                                <span id="bcs-smtp-provider-note-text"><?php echo esc_html( $preset['note'] ); ?></span>
     2052                                                                                <a id="bcs-smtp-provider-note-link" href="<?php echo esc_url( $preset['link'] ); ?>" target="_blank" rel="noopener noreferrer" <?php echo $preset['link'] ? '' : 'hidden'; ?>><span id="bcs-smtp-provider-note-link-text"><?php echo esc_html( $preset['link_label'] ); ?></span><span class="screen-reader-text"> <?php esc_html_e( '(opens in a new tab)', 'bcs-smtp-manager' ); ?></span></a>
     2053                                                                        </span>
     2054                                                                </p>
    11042055                                                        </div>
    11052056
    1106                                                         <!-- SMTP settings accordion -->
    1107                                                         <div id="bcs-smtp-smtp-accordion" class="bcs-smtp-accordion <?php echo $smtp_enabled ? 'open' : ''; ?>" style="margin-top:12px">
    1108                                                                 <button type="button" class="bcs-smtp-accordion-head">
    1109                                                                         <div class="bcs-smtp-accordion-head-left">
    1110                                                                                 <div class="bcs-smtp-accordion-head-icon" style="background:rgba(99,102,241,.15);color:var(--bcs-smtp-indigo)">
    1111                                                                                         <span class="dashicons dashicons-admin-network"></span>
     2057                                                        <div class="bcs-smtp-field-row">
     2058                                                                <div class="bcs-smtp-field">
     2059                                                                        <label class="bcs-smtp-field-label" for="bcs-smtp-from-email"><?php esc_html_e( 'From address', 'bcs-smtp-manager' ); ?></label>
     2060                                                                        <input type="email" id="bcs-smtp-from-email" class="bcs-smtp-input" value="<?php echo esc_attr( $from_email ); ?>" spellcheck="false">
     2061                                                                </div>
     2062                                                                <div class="bcs-smtp-field">
     2063                                                                        <label class="bcs-smtp-field-label" for="bcs-smtp-from-name"><?php esc_html_e( 'From name', 'bcs-smtp-manager' ); ?></label>
     2064                                                                        <input type="text" id="bcs-smtp-from-name" class="bcs-smtp-input" value="<?php echo esc_attr( $from_name ); ?>">
     2065                                                                </div>
     2066                                                        </div>
     2067
     2068                                                        <label class="bcs-smtp-inline-switch bcs-smtp-auth-switch" for="bcs-smtp-smtp-auth">
     2069                                                                <span class="bcs-smtp-toggle bcs-smtp-toggle--sm">
     2070                                                                        <input type="checkbox" id="bcs-smtp-smtp-auth" role="switch" aria-controls="bcs-smtp-auth-fields" <?php checked( $smtp_auth ); ?>>
     2071                                                                        <span class="bcs-smtp-toggle-slider" aria-hidden="true"></span>
     2072                                                                </span>
     2073                                                                <span><?php esc_html_e( 'My server needs a username and password', 'bcs-smtp-manager' ); ?></span>
     2074                                                        </label>
     2075
     2076                                                        <div id="bcs-smtp-auth-fields" <?php echo $smtp_auth ? '' : 'hidden'; ?>>
     2077                                                                <div class="bcs-smtp-field-row">
     2078                                                                        <div class="bcs-smtp-field">
     2079                                                                                <label class="bcs-smtp-field-label" for="bcs-smtp-smtp-username"><?php esc_html_e( 'Username', 'bcs-smtp-manager' ); ?></label>
     2080                                                                                <input type="text" id="bcs-smtp-smtp-username" class="bcs-smtp-input" value="<?php echo esc_attr( $smtp_user ); ?>" placeholder="<?php echo esc_attr( $preset['user_hint'] ); ?>" autocomplete="off" spellcheck="false" aria-describedby="bcs-smtp-user-hint">
     2081                                                                                <span class="screen-reader-text" id="bcs-smtp-user-hint"><?php echo esc_html( $preset['user_hint'] ); ?></span>
     2082                                                                        </div>
     2083                                                                        <div class="bcs-smtp-field">
     2084                                                                                <label class="bcs-smtp-field-label" for="bcs-smtp-smtp-password" id="bcs-smtp-pass-label"><?php echo esc_html( $preset['pass_label'] ); ?></label>
     2085                                                                                <div id="bcs-smtp-pw-display" class="bcs-smtp-pw-saved" <?php echo $has_pw ? '' : 'hidden'; ?>>
     2086                                                                                        <span class="bcs-smtp-pw-dots" aria-hidden="true">••••••••••••</span>
     2087                                                                                        <span class="screen-reader-text"><?php esc_html_e( 'A password is saved.', 'bcs-smtp-manager' ); ?></span>
     2088                                                                                        <button type="button" id="bcs-smtp-change-pw-btn" class="bcs-smtp-link"><?php esc_html_e( 'Change', 'bcs-smtp-manager' ); ?></button>
    11122089                                                                                </div>
    1113                                                                                 <div>
    1114                                                                                         <div class="bcs-smtp-accordion-head-title"><?php esc_html_e( 'SMTP Server Settings', 'bcs-smtp-manager' ); ?></div>
    1115                                                                                         <div class="bcs-smtp-accordion-head-desc"><?php esc_html_e( 'Host, port, encryption & authentication', 'bcs-smtp-manager' ); ?></div>
     2090                                                                                <div id="bcs-smtp-pw-input" <?php echo $has_pw ? 'hidden' : ''; ?>>
     2091                                                                                        <div class="bcs-smtp-pw-wrap">
     2092                                                                                                <input type="password" id="bcs-smtp-smtp-password" class="bcs-smtp-input" value="" autocomplete="new-password" spellcheck="false" aria-describedby="bcs-smtp-pw-status">
     2093                                                                                                <button type="button" id="bcs-smtp-toggle-pw-btn" class="bcs-smtp-pw-eye" aria-label="<?php esc_attr_e( 'Show password', 'bcs-smtp-manager' ); ?>" aria-controls="bcs-smtp-smtp-password">
     2094                                                                                                        <span class="dashicons dashicons-visibility" aria-hidden="true"></span>
     2095                                                                                                </button>
     2096                                                                                        </div>
     2097                                                                                        <button type="button" id="bcs-smtp-cancel-pw-btn" class="bcs-smtp-link bcs-smtp-hint" hidden><?php esc_html_e( 'Keep the saved password', 'bcs-smtp-manager' ); ?></button>
     2098                                                                                </div>
     2099                                                                                <?php if ( $pw_unreadable ) : ?>
     2100                                                                                        <span class="bcs-smtp-hint is-bad" id="bcs-smtp-pw-status"><?php esc_html_e( 'The saved password can no longer be read because your site’s security keys changed. Click Change and enter it again.', 'bcs-smtp-manager' ); ?></span>
     2101                                                                                <?php else : ?>
     2102                                                                                        <span class="bcs-smtp-hint <?php echo $has_pw ? 'is-good' : ''; ?>" id="bcs-smtp-pw-status"></span>
     2103                                                                                <?php endif; ?>
     2104                                                                        </div>
     2105                                                                </div>
     2106                                                        </div>
     2107
     2108                                                        <div class="bcs-smtp-alert bcs-smtp-alert-warning bcs-smtp-auth-warning" id="bcs-smtp-auth-warning" role="alert" <?php echo ( ! $smtp_auth && $provider && 'other' !== $provider ) ? '' : 'hidden'; ?>>
     2109                                                                <span class="dashicons dashicons-warning" aria-hidden="true"></span>
     2110                                                                <div class="bcs-smtp-alert-content">
     2111                                                                        <strong id="bcs-smtp-auth-warning-title">
     2112                                                                                <?php
     2113                                                                                /* translators: %s: email provider name, e.g. Microsoft 365. */
     2114                                                                                printf( esc_html__( '%s needs a username and password.', 'bcs-smtp-manager' ), esc_html( isset( $provider_short[ $provider ] ) ? $provider_short[ $provider ] : $preset['label'] ) );
     2115                                                                                ?>
     2116                                                                        </strong>
     2117                                                                        <?php esc_html_e( 'Turn on “My server needs a username and password” above and save, or every email will be rejected.', 'bcs-smtp-manager' ); ?>
     2118                                                                </div>
     2119                                                        </div>
     2120
     2121                                                        <div class="bcs-smtp-disclosure <?php echo ( 'other' === $provider ) ? 'is-open' : ''; ?>" id="bcs-smtp-server-details">
     2122                                                                <button type="button" class="bcs-smtp-disclosure-head" aria-expanded="<?php echo ( 'other' === $provider ) ? 'true' : 'false'; ?>" aria-controls="bcs-smtp-server-details-body">
     2123                                                                        <span>
     2124                                                                                <strong><?php esc_html_e( 'Server details', 'bcs-smtp-manager' ); ?></strong>
     2125                                                                                <span class="bcs-smtp-mono" id="bcs-smtp-server-summary"><?php echo $smtp_host ? esc_html( $smtp_host . ' · ' . $smtp_port . ' · ' . strtoupper( $smtp_enc ) ) : esc_html__( 'Filled in when you pick a provider', 'bcs-smtp-manager' ); ?></span>
     2126                                                                        </span>
     2127                                                                        <span class="dashicons dashicons-arrow-down-alt2" aria-hidden="true"></span>
     2128                                                                </button>
     2129                                                                <div class="bcs-smtp-disclosure-body" id="bcs-smtp-server-details-body">
     2130                                                                        <div class="bcs-smtp-field-row">
     2131                                                                                <div class="bcs-smtp-field bcs-smtp-field--grow">
     2132                                                                                        <label class="bcs-smtp-field-label" for="bcs-smtp-smtp-host"><?php esc_html_e( 'SMTP server', 'bcs-smtp-manager' ); ?></label>
     2133                                                                                        <input type="text" id="bcs-smtp-smtp-host" class="bcs-smtp-input bcs-smtp-mono" value="<?php echo esc_attr( $smtp_host ); ?>" placeholder="smtp.example.com" spellcheck="false" autocomplete="off">
     2134                                                                                </div>
     2135                                                                                <div class="bcs-smtp-field bcs-smtp-field--port">
     2136                                                                                        <label class="bcs-smtp-field-label" for="bcs-smtp-smtp-port"><?php esc_html_e( 'Port', 'bcs-smtp-manager' ); ?></label>
     2137                                                                                        <input type="number" id="bcs-smtp-smtp-port" class="bcs-smtp-input bcs-smtp-mono" value="<?php echo esc_attr( $smtp_port ); ?>" min="1" max="65535" inputmode="numeric">
    11162138                                                                                </div>
    11172139                                                                        </div>
    1118                                                                         <span class="dashicons dashicons-arrow-down-alt2 bcs-smtp-accordion-chevron"></span>
    1119                                                                 </button>
    1120                                                                 <div class="bcs-smtp-accordion-body">
    1121                                                                         <div class="bcs-smtp-field-row">
    1122                                                                                 <div class="bcs-smtp-field">
    1123                                                                                         <label class="bcs-smtp-field-label" for="bcs-smtp-from-email"><?php esc_html_e( 'Force From Email', 'bcs-smtp-manager' ); ?></label>
    1124                                                                                         <input type="email" id="bcs-smtp-from-email" class="bcs-smtp-input" value="<?php echo esc_attr( $from_email ); ?>" placeholder="no-reply@yoursite.com">
    1125                                                                                 </div>
    1126                                                                                 <div class="bcs-smtp-field">
    1127                                                                                         <label class="bcs-smtp-field-label" for="bcs-smtp-from-name"><?php esc_html_e( 'Force From Name', 'bcs-smtp-manager' ); ?></label>
    1128                                                                                         <input type="text" id="bcs-smtp-from-name" class="bcs-smtp-input" value="<?php echo esc_attr( $from_name ); ?>" placeholder="Your Site Name">
    1129                                                                                 </div>
    1130                                                                         </div>
    1131 
    1132                                                                         <div class="bcs-smtp-field-row">
    1133                                                                                 <div class="bcs-smtp-field">
    1134                                                                                         <label class="bcs-smtp-field-label" for="bcs-smtp-smtp-host"><?php esc_html_e( 'SMTP Host', 'bcs-smtp-manager' ); ?></label>
    1135                                                                                         <input type="text" id="bcs-smtp-smtp-host" class="bcs-smtp-input" value="<?php echo esc_attr( $smtp_host ); ?>" placeholder="smtp.gmail.com">
    1136                                                                                 </div>
    1137                                                                                 <div class="bcs-smtp-field">
    1138                                                                                         <label class="bcs-smtp-field-label" for="bcs-smtp-smtp-port"><?php esc_html_e( 'SMTP Port', 'bcs-smtp-manager' ); ?></label>
    1139                                                                                         <input type="number" id="bcs-smtp-smtp-port" class="bcs-smtp-input" value="<?php echo esc_attr( $smtp_port ); ?>" min="1" max="65535">
    1140                                                                                         <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block"><?php esc_html_e( 'SSL = 465 • TLS = 587 • None = 25', 'bcs-smtp-manager' ); ?></small>
    1141                                                                                 </div>
    1142                                                                         </div>
    1143 
    1144                                                                         <!-- Encryption -->
    11452140                                                                        <div class="bcs-smtp-field">
    1146                                                                                 <label class="bcs-smtp-field-label"><?php esc_html_e( 'Encryption', 'bcs-smtp-manager' ); ?></label>
    1147                                                                                 <div class="bcs-smtp-check-list" style="flex-direction:row;gap:8px">
    1148                                                                                         <?php foreach ( array( 'none' => __( 'None', 'bcs-smtp-manager' ), 'ssl' => __( 'SSL', 'bcs-smtp-manager' ), 'tls' => __( 'TLS', 'bcs-smtp-manager' ) ) as $val => $label ) : ?>
    1149                                                                                         <label class="bcs-smtp-check-item" style="flex-direction:row;align-items:center;gap:6px;padding:8px 14px;min-width:70px">
    1150                                                                                                 <input type="radio" name="bcs_smtp_encryption" class="bcs-smtp-enc-radio" value="<?php echo esc_attr( $val ); ?>" <?php checked( $smtp_enc, $val ); ?> style="width:20px;height:20px;flex-shrink:0">
    1151                                                                                                 <span class="bcs-smtp-check-label-title"><?php echo esc_html( $label ); ?></span>
     2141                                                                                <span class="bcs-smtp-field-label" id="bcs-smtp-enc-label"><?php esc_html_e( 'Encryption', 'bcs-smtp-manager' ); ?></span>
     2142                                                                                <div class="bcs-smtp-segmented" role="radiogroup" aria-labelledby="bcs-smtp-enc-label">
     2143                                                                                        <?php foreach ( array( 'tls' => __( 'TLS · 587', 'bcs-smtp-manager' ), 'ssl' => __( 'SSL · 465', 'bcs-smtp-manager' ), 'none' => __( 'None · 25', 'bcs-smtp-manager' ) ) as $val => $label ) : ?>
     2144                                                                                        <label>
     2145                                                                                                <input type="radio" name="bcs_smtp_encryption" class="bcs-smtp-enc-radio" value="<?php echo esc_attr( $val ); ?>" <?php checked( $smtp_enc, $val ); ?>>
     2146                                                                                                <span><?php echo esc_html( $label ); ?></span>
    11522147                                                                                        </label>
    11532148                                                                                        <?php endforeach; ?>
    11542149                                                                                </div>
    11552150                                                                        </div>
    1156 
    1157                                                                         <!-- Auto TLS toggle -->
    1158                                                                         <div class="bcs-smtp-toggle-row" style="padding:10px 0">
    1159                                                                                 <div class="bcs-smtp-toggle-row-info">
    1160                                                                                         <div class="bcs-smtp-toggle-row-label" style="font-size:13px"><?php esc_html_e( 'Auto TLS', 'bcs-smtp-manager' ); ?></div>
    1161                                                                                         <div class="bcs-smtp-toggle-row-desc"><?php esc_html_e( 'Automatically upgrade to TLS if supported by the server', 'bcs-smtp-manager' ); ?></div>
    1162                                                                                 </div>
    1163                                                                                 <label class="bcs-smtp-toggle">
    1164                                                                                         <input type="checkbox" id="bcs-smtp-auto-tls" <?php checked( $auto_tls ); ?>>
    1165                                                                                         <span class="bcs-smtp-toggle-slider"></span>
    1166                                                                                 </label>
    1167                                                                         </div>
    1168 
    1169                                                                         <!-- Authentication toggle -->
    1170                                                                         <div class="bcs-smtp-toggle-row" style="padding:10px 0">
    1171                                                                                 <div class="bcs-smtp-toggle-row-info">
    1172                                                                                         <div class="bcs-smtp-toggle-row-label" style="font-size:13px"><?php esc_html_e( 'Authentication', 'bcs-smtp-manager' ); ?></div>
    1173                                                                                         <div class="bcs-smtp-toggle-row-desc"><?php esc_html_e( 'Authenticate with the SMTP server using username & password', 'bcs-smtp-manager' ); ?></div>
    1174                                                                                 </div>
    1175                                                                                 <label class="bcs-smtp-toggle">
    1176                                                                                         <input type="checkbox" id="bcs-smtp-smtp-auth" <?php checked( $smtp_auth ); ?>>
    1177                                                                                         <span class="bcs-smtp-toggle-slider"></span>
    1178                                                                                 </label>
    1179                                                                         </div>
    1180 
    1181                                                                         <!-- Username + Password (shown when auth enabled) -->
    1182                                                                         <div id="bcs-smtp-auth-fields" style="<?php echo $smtp_auth ? '' : 'display:none'; ?>">
    1183                                                                         <div class="bcs-smtp-field-row">
    1184                                                                                 <div class="bcs-smtp-field" style="flex:0 0 50%;max-width:50%">
    1185                                                                                         <label class="bcs-smtp-field-label" for="bcs-smtp-smtp-username"><?php esc_html_e( 'Username', 'bcs-smtp-manager' ); ?></label>
    1186                                                                                         <input type="text" id="bcs-smtp-smtp-username" class="bcs-smtp-input" value="<?php echo esc_attr( $smtp_user ); ?>" placeholder="your@email.com" autocomplete="off">
    1187                                                                                 </div>
    1188                                                                         </div>
    1189                                                                         <?php $has_pw = ! empty( get_option( 'bcs_smtp_password', '' ) ); ?>
    1190                                                                         <div class="bcs-smtp-field-row">
    1191                                                                                 <div class="bcs-smtp-field" style="flex:0 0 50%;max-width:50%">
    1192                                                                                         <label class="bcs-smtp-field-label" for="bcs-smtp-smtp-password"><?php esc_html_e( 'Password / App Password', 'bcs-smtp-manager' ); ?></label>
    1193 
    1194                                                                                         <!-- Masked display — shown when a password is already saved -->
    1195                                                                                         <div id="bcs-smtp-pw-display" style="<?php echo $has_pw ? 'display:flex' : 'display:none'; ?>;gap:8px;align-items:center">
    1196                                                                                                 <input type="password" class="bcs-smtp-input" value="placeholder_pw"
    1197                                                                                                    disabled style="flex:1" aria-label="<?php esc_attr_e( 'Saved password (masked)', 'bcs-smtp-manager' ); ?>">
    1198                                                                                                 <button type="button" id="bcs-smtp-change-pw-btn"
    1199                                                                                                                 class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm"
    1200                                                                                                                 style="flex-shrink:0;white-space:nowrap">
    1201                                                                                                         <span class="dashicons dashicons-edit"></span> <?php esc_html_e( 'Change', 'bcs-smtp-manager' ); ?>
    1202                                                                                                 </button>
    1203                                                                                         </div>
    1204 
    1205                                                                                         <!-- Actual input — visible when no password yet, or after Change is clicked -->
    1206                                                                                         <div id="bcs-smtp-pw-input" style="<?php echo $has_pw ? 'display:none' : ''; ?>">
    1207                                                                                                 <div class="bcs-smtp-pw-wrap">
    1208                                                                                                         <input type="password" id="bcs-smtp-smtp-password" class="bcs-smtp-input"
    1209                                                                                                                    value="" placeholder="<?php esc_attr_e( 'Enter password or app password…', 'bcs-smtp-manager' ); ?>"
    1210                                                                                                                    autocomplete="new-password" style="width:100%">
    1211                                                                                                         <button type="button" id="bcs-smtp-toggle-pw-btn" class="bcs-smtp-pw-eye" aria-label="<?php esc_attr_e( 'Show password', 'bcs-smtp-manager' ); ?>" tabindex="-1">
    1212                                                                                                                 <span class="dashicons dashicons-visibility"></span>
    1213                                                                                                         </button>
    1214                                                                                                 </div>
    1215                                                                                                 <!-- Cancel link — shown after Change is clicked -->
    1216                                                                                                 <div id="bcs-smtp-pw-cancel-row" style="display:none;margin-top:6px">
    1217                                                                                                         <a href="#" id="bcs-smtp-cancel-pw-btn" style="font-size:12px;color:var(--bcs-smtp-text-3);text-decoration:none">
    1218                                                                                                                 &#8592; <?php esc_html_e( 'Cancel — keep existing password', 'bcs-smtp-manager' ); ?>
    1219                                                                                                         </a>
    1220                                                                                                 </div>
    1221                                                                                         </div>
    1222 
    1223                                                                                         <small style="font-size:11px;margin-top:6px;display:block">
    1224                                                                                                 <span id="bcs-smtp-pw-status" style="color:<?php echo $has_pw ? 'var(--bcs-smtp-emerald)' : 'var(--bcs-smtp-text-3)'; ?>">
    1225                                                                                                         <?php echo $has_pw ? esc_html__( '✓ Password saved — leave blank to keep it.', 'bcs-smtp-manager' ) : esc_html__( 'No password saved yet.', 'bcs-smtp-manager' ); ?>
    1226                                                                                                 </span><br>
    1227                                                                                                 <span style="color:var(--bcs-smtp-text-3)">
    1228                                                                                                         <?php esc_html_e( 'Gmail / Workspace: use a 16-char', 'bcs-smtp-manager' ); ?> <strong><?php esc_html_e( 'App Password', 'bcs-smtp-manager' ); ?></strong>
    1229                                                                                                         (<a href="https://myaccount.google.com/apppasswords" target="_blank" rel="noopener"><?php esc_html_e( 'generate ↗', 'bcs-smtp-manager' ); ?></a>).
    1230                                                                                                         <?php esc_html_e( 'Outlook / Microsoft 365: use your account or app password.', 'bcs-smtp-manager' ); ?>
    1231                                                                                                         <?php esc_html_e( 'Spaces are stripped automatically.', 'bcs-smtp-manager' ); ?>
    1232                                                                                                 </span>
    1233                                                                                         </small>
    1234                                                                                 </div>
    1235                                                                         </div>
    1236                                                                         </div>
     2151                                                                        <label class="bcs-smtp-inline-switch" for="bcs-smtp-auto-tls">
     2152                                                                                <span class="bcs-smtp-toggle bcs-smtp-toggle--sm">
     2153                                                                                        <input type="checkbox" id="bcs-smtp-auto-tls" role="switch" <?php checked( $auto_tls ); ?>>
     2154                                                                                        <span class="bcs-smtp-toggle-slider" aria-hidden="true"></span>
     2155                                                                                </span>
     2156                                                                                <span><?php esc_html_e( 'Upgrade to TLS when the server offers it', 'bcs-smtp-manager' ); ?></span>
     2157                                                                        </label>
    12372158                                                                </div>
    12382159                                                        </div>
    1239 
    1240                                                         <div style="margin-top:16px">
    1241                                                                 <button type="button" id="bcs-smtp-save-smtp-btn" class="bcs-smtp-btn bcs-smtp-btn-primary">
    1242                                                                         <span class="dashicons dashicons-saved"></span><?php esc_html_e( 'Save SMTP Settings', 'bcs-smtp-manager' ); ?>
    1243                                                                 </button>
    1244                                                                 <div id="bcs-smtp-smtp-msg" role="status" aria-live="polite" style="display:none;margin-top:10px;width:100%"></div>
     2160                                                </div>
     2161
     2162                                                <div class="bcs-smtp-setup-foot">
     2163                                                        <button type="button" id="bcs-smtp-save-smtp-btn" class="bcs-smtp-btn bcs-smtp-btn-primary">
     2164                                                                <?php esc_html_e( 'Save and test connection', 'bcs-smtp-manager' ); ?>
     2165                                                        </button>
     2166                                                        <span class="bcs-smtp-unsaved" id="bcs-smtp-smtp-unsaved" hidden><?php esc_html_e( 'Unsaved changes', 'bcs-smtp-manager' ); ?></span>
     2167                                                        <div id="bcs-smtp-smtp-msg" class="bcs-smtp-msg" role="status" aria-live="polite"></div>
     2168                                                </div>
     2169                                        </div>
     2170
     2171                                        <aside class="bcs-smtp-panel bcs-smtp-test" aria-labelledby="bcs-smtp-test-title">
     2172                                                <div class="bcs-smtp-test-head">
     2173                                                        <span class="bcs-smtp-test-badge dashicons dashicons-yes-alt" aria-hidden="true"></span>
     2174                                                        <div>
     2175                                                                <h2 id="bcs-smtp-test-title"><?php esc_html_e( 'Check it works', 'bcs-smtp-manager' ); ?></h2>
     2176                                                                <p><?php esc_html_e( 'Send a real email through your connection. The result shows here and in the email log.', 'bcs-smtp-manager' ); ?></p>
    12452177                                                        </div>
    12462178                                                </div>
     2179                                                <div class="bcs-smtp-field">
     2180                                                        <label class="bcs-smtp-field-label" for="bcs-smtp-test-to"><?php esc_html_e( 'Send a test email to', 'bcs-smtp-manager' ); ?></label>
     2181                                                        <input type="email" id="bcs-smtp-test-to" class="bcs-smtp-input" value="<?php echo esc_attr( get_option( 'admin_email' ) ); ?>" spellcheck="false">
     2182                                                </div>
     2183                                                <div class="bcs-smtp-actions">
     2184                                                        <button type="button" id="bcs-smtp-send-test-btn" class="bcs-smtp-btn bcs-smtp-btn-primary"><?php esc_html_e( 'Send test email', 'bcs-smtp-manager' ); ?></button>
     2185                                                        <button type="button" id="bcs-smtp-check-now-btn" class="bcs-smtp-btn bcs-smtp-btn-secondary" data-check-now data-msg="#bcs-smtp-test-msg"><?php esc_html_e( 'Run health check', 'bcs-smtp-manager' ); ?></button>
     2186                                                </div>
     2187                                                <div id="bcs-smtp-test-msg" class="bcs-smtp-msg" role="status" aria-live="polite"></div>
     2188                                        </aside>
     2189                                </div>
     2190                        </section>
     2191
     2192                        <!-- ═══ PANEL: Email log ══════════════════════════════════════════ -->
     2193                        <section id="bcs-smtp-panel-email-log" class="bcs-smtp-tab-panel" role="tabpanel" aria-labelledby="bcs-smtp-tab-email-log">
     2194                                <?php
     2195                                $grace_until = (int) get_option( 'bcs_smtp_prune_after', 0 );
     2196                                $older_count = $grace_until > time() ? $this->db->count_email_log_older_than( BCS_SMTP_LOG_RETENTION_DAYS ) : 0;
     2197                                if ( $older_count > 0 ) :
     2198                                        $export_all = add_query_arg(
     2199                                                array(
     2200                                                        'bcs_smtp_action' => 'export_csv',
     2201                                                        'nonce'           => wp_create_nonce( 'bcs_smtp_export' ),
     2202                                                        'date_range'      => 'all',
     2203                                                        'status'          => 'all',
     2204                                                ),
     2205                                                admin_url( 'options-general.php?page=bcs-smtp-manager' )
     2206                                        );
     2207                                        ?>
     2208                                        <div class="bcs-smtp-alert bcs-smtp-alert-warning bcs-smtp-log-retention" role="status">
     2209                                                <span class="dashicons dashicons-info-outline" aria-hidden="true"></span>
     2210                                                <div class="bcs-smtp-alert-content">
     2211                                                        <?php
     2212                                                        printf(
     2213                                                                /* translators: 1: number of log entries, 2: date. */
     2214                                                                esc_html( _n( 'The email log now keeps the last 90 days. %1$s older entry will be removed on %2$s.', 'The email log now keeps the last 90 days. %1$s older entries will be removed on %2$s.', $older_count, 'bcs-smtp-manager' ) ),
     2215                                                                esc_html( number_format_i18n( $older_count ) ),
     2216                                                                esc_html( wp_date( get_option( 'date_format', 'F j, Y' ), $grace_until ) )
     2217                                                        );
     2218                                                        ?>
     2219                                                        <a href="<?php echo esc_url( $export_all ); ?>"><?php esc_html_e( 'Export all entries (CSV)', 'bcs-smtp-manager' ); ?></a>
     2220                                                </div>
    12472221                                        </div>
    1248                                 </div>
    1249 
    1250                                 <div>
    1251                                         <div class="bcs-smtp-card">
    1252                                                 <div class="bcs-smtp-card-header">
    1253                                                         <p class="bcs-smtp-card-title"><span class="dashicons dashicons-info-outline"></span><?php esc_html_e( 'Common SMTP Providers', 'bcs-smtp-manager' ); ?></p>
    1254                                                         <p class="bcs-smtp-card-desc"><?php esc_html_e( 'Reference host, port & encryption settings for popular providers.', 'bcs-smtp-manager' ); ?></p>
    1255                                                 </div>
    1256                                                 <div class="bcs-smtp-card-body">
    1257                                                         <div class="bcs-smtp-field-group" style="gap:10px">
    1258                                                                 <div class="bcs-smtp-server-row">
    1259                                                                         <span class="bcs-smtp-server-key"><?php esc_html_e( 'Gmail / Google Workspace', 'bcs-smtp-manager' ); ?></span>
    1260                                                                         <span class="bcs-smtp-server-val bcs-smtp-mono">smtp.gmail.com &bull; 587 &bull; TLS</span>
    1261                                                                 </div>
    1262                                                                 <div class="bcs-smtp-server-row">
    1263                                                                         <span class="bcs-smtp-server-key"><?php esc_html_e( 'Outlook / Microsoft 365', 'bcs-smtp-manager' ); ?></span>
    1264                                                                         <span class="bcs-smtp-server-val bcs-smtp-mono">smtp.office365.com &bull; 587 &bull; TLS</span>
    1265                                                                 </div>
    1266                                                                 <div class="bcs-smtp-server-row">
    1267                                                                         <span class="bcs-smtp-server-key"><?php esc_html_e( 'Yahoo Mail', 'bcs-smtp-manager' ); ?></span>
    1268                                                                         <span class="bcs-smtp-server-val bcs-smtp-mono">smtp.mail.yahoo.com &bull; 587 &bull; TLS</span>
    1269                                                                 </div>
    1270                                                                 <div class="bcs-smtp-server-row">
    1271                                                                         <span class="bcs-smtp-server-key"><?php esc_html_e( 'SendGrid', 'bcs-smtp-manager' ); ?></span>
    1272                                                                         <span class="bcs-smtp-server-val bcs-smtp-mono">smtp.sendgrid.net &bull; 587 &bull; TLS</span>
    1273                                                                 </div>
    1274                                                                 <div class="bcs-smtp-server-row">
    1275                                                                         <span class="bcs-smtp-server-key"><?php esc_html_e( 'Amazon SES', 'bcs-smtp-manager' ); ?></span>
    1276                                                                         <span class="bcs-smtp-server-val bcs-smtp-mono">email-smtp.[region].amazonaws.com &bull; 587 &bull; TLS</span>
    1277                                                                 </div>
    1278                                                                 <div class="bcs-smtp-server-row">
    1279                                                                         <span class="bcs-smtp-server-key"><?php esc_html_e( 'Mailgun', 'bcs-smtp-manager' ); ?></span>
    1280                                                                         <span class="bcs-smtp-server-val bcs-smtp-mono">smtp.mailgun.org &bull; 587 &bull; TLS</span>
    1281                                                                 </div>
    1282                                                         </div>
    1283                                                         <div class="bcs-smtp-alert bcs-smtp-alert-info" style="margin-top:14px;margin-bottom:0">
    1284                                                                 <span class="dashicons dashicons-info-outline"></span>
    1285                                                                 <div class="bcs-smtp-alert-content" style="font-size:11.5px"><?php esc_html_e( 'Gmail and Microsoft 365 require an app password when 2-factor authentication is enabled — your normal account password won’t work.', 'bcs-smtp-manager' ); ?></div>
    1286                                                         </div>
    1287                                                 </div>
    1288                                         </div>
    1289                                 </div>
    1290 
    1291                                 </div>
    1292 
    1293                         </div>
    1294 
    1295                         <!-- ═══════════════════════════════════════════════════════════════
    1296                              PANEL: Email Log
    1297                              ═══════════════════════════════════════════════════════════════ -->
    1298                         <div id="bcs-smtp-panel-email-log" class="bcs-smtp-tab-panel">
    1299                                 <div class="bcs-smtp-card">
    1300                                         <div class="bcs-smtp-card-header">
    1301                                                 <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:14px">
    1302                                                         <p class="bcs-smtp-card-title" style="margin:0"><span class="dashicons dashicons-list-view"></span><?php esc_html_e( 'Email Log', 'bcs-smtp-manager' ); ?></p>
    1303                                                         <div style="display:flex;gap:8px;flex-wrap:wrap">
    1304                                                                 <a id="bcs-smtp-export-csv-btn" href="#" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm">
    1305                                                                         <span class="dashicons dashicons-download"></span><?php esc_html_e( 'Export CSV', 'bcs-smtp-manager' ); ?>
    1306                                                                 </a>
    1307                                                         </div>
    1308                                                 </div>
    1309                                                 <p class="bcs-smtp-card-desc"><?php esc_html_e( 'Every email sent via', 'bcs-smtp-manager' ); ?> <code>wp_mail()</code> <?php esc_html_e( 'is logged here with its delivery status.', 'bcs-smtp-manager' ); ?></p>
    1310                                         </div>
    1311                                         <div class="bcs-smtp-card-body">
    1312                                                 <!-- Filters -->
    1313                                         <div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:14px;align-items:center">
    1314                                                 <select id="bcs-smtp-log-date-range" class="bcs-smtp-input" style="width:25%;min-width:130px">
    1315                                                         <option value="today"><?php esc_html_e( 'Today', 'bcs-smtp-manager' ); ?></option>
    1316                                                         <option value="yesterday"><?php esc_html_e( 'Yesterday', 'bcs-smtp-manager' ); ?></option>
    1317                                                         <option value="7days"><?php esc_html_e( 'Last 7 days', 'bcs-smtp-manager' ); ?></option>
    1318                                                         <option value="30days" selected><?php esc_html_e( 'Last 30 days', 'bcs-smtp-manager' ); ?></option>
    1319                                                         <option value="this_week"><?php esc_html_e( 'This week', 'bcs-smtp-manager' ); ?></option>
    1320                                                         <option value="last_week"><?php esc_html_e( 'Last week', 'bcs-smtp-manager' ); ?></option>
    1321                                                         <option value="this_month"><?php esc_html_e( 'This month', 'bcs-smtp-manager' ); ?></option>
    1322                                                         <option value="3months"><?php esc_html_e( 'Last 3 months', 'bcs-smtp-manager' ); ?></option>
    1323                                                         <option value="6months"><?php esc_html_e( 'Last 6 months', 'bcs-smtp-manager' ); ?></option>
    1324                                                         <option value="year"><?php esc_html_e( 'This year', 'bcs-smtp-manager' ); ?></option>
    1325                                                         <option value="all"><?php esc_html_e( 'All time', 'bcs-smtp-manager' ); ?></option>
     2222                                <?php endif; ?>
     2223                                <div class="bcs-smtp-panel bcs-smtp-panel--flush">
     2224                                        <div class="bcs-smtp-filters">
     2225                                                <div class="bcs-smtp-search">
     2226                                                        <span class="dashicons dashicons-search" aria-hidden="true"></span>
     2227                                                        <label class="screen-reader-text" for="bcs-smtp-log-search"><?php esc_html_e( 'Search email log', 'bcs-smtp-manager' ); ?></label>
     2228                                                        <input type="search" id="bcs-smtp-log-search" class="bcs-smtp-input" placeholder="<?php esc_attr_e( 'Search subject or address', 'bcs-smtp-manager' ); ?>">
     2229                                                </div>
     2230                                                <label class="screen-reader-text" for="bcs-smtp-log-date-range"><?php esc_html_e( 'Date range', 'bcs-smtp-manager' ); ?></label>
     2231                                                <select id="bcs-smtp-log-date-range" class="bcs-smtp-input bcs-smtp-select">
     2232                                                        <?php foreach ( $date_ranges as $val => $label ) : ?>
     2233                                                                <option value="<?php echo esc_attr( $val ); ?>" <?php selected( $val, '30days' ); ?>><?php echo esc_html( $label ); ?></option>
     2234                                                        <?php endforeach; ?>
    13262235                                                </select>
    1327                                                 <select id="bcs-smtp-log-status" class="bcs-smtp-input" style="width:25%;min-width:120px">
    1328                                                         <option value=""><?php esc_html_e( 'All statuses', 'bcs-smtp-manager' ); ?></option>
    1329                                                         <option value="success"><?php esc_html_e( 'Sent', 'bcs-smtp-manager' ); ?></option>
    1330                                                         <option value="failed"><?php esc_html_e( 'Failed', 'bcs-smtp-manager' ); ?></option>
    1331                                                         <option value="unconfirmed"><?php esc_html_e( 'Unconfirmed', 'bcs-smtp-manager' ); ?></option>
    1332                                                 </select>
    1333                                                 <input type="text" id="bcs-smtp-log-search" class="bcs-smtp-input" style="width:50%;min-width:200px;flex:1" placeholder="<?php esc_attr_e( 'Search subject, from, or to email…', 'bcs-smtp-manager' ); ?>">
     2236                                                <div class="bcs-smtp-segmented bcs-smtp-status-filter" role="radiogroup" aria-label="<?php esc_attr_e( 'Status', 'bcs-smtp-manager' ); ?>">
     2237                                                        <?php foreach ( array( '' => __( 'All', 'bcs-smtp-manager' ), 'success' => __( 'Sent', 'bcs-smtp-manager' ), 'failed' => __( 'Failed', 'bcs-smtp-manager' ), 'unconfirmed' => __( 'Unconfirmed', 'bcs-smtp-manager' ) ) as $val => $label ) : ?>
     2238                                                                <label><input type="radio" name="bcs_smtp_log_status" class="bcs-smtp-log-status-radio" value="<?php echo esc_attr( $val ); ?>" <?php checked( '', $val ); ?>><span><?php echo esc_html( $label ); ?><b class="bcs-smtp-seg-count" data-count="<?php echo esc_attr( '' === $val ? 'all' : $val ); ?>"></b></span></label>
     2239                                                        <?php endforeach; ?>
     2240                                                </div>
     2241                                                <input type="hidden" id="bcs-smtp-log-status" value="">
     2242                                                <a id="bcs-smtp-export-csv-btn" href="#" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm">
     2243                                                        <span class="dashicons dashicons-download" aria-hidden="true"></span><?php esc_html_e( 'Export CSV', 'bcs-smtp-manager' ); ?>
     2244                                                </a>
    13342245                                        </div>
    13352246
    13362247                                        <div class="bcs-smtp-table-wrap">
    13372248                                                <table class="bcs-smtp-table">
     2249                                                        <caption class="screen-reader-text"><?php esc_html_e( 'Emails sent by WordPress', 'bcs-smtp-manager' ); ?></caption>
    13382250                                                        <thead>
    13392251                                                                <tr>
    1340                                                                         <th><?php esc_html_e( 'Date', 'bcs-smtp-manager' ); ?></th>
    1341                                                                         <th><?php esc_html_e( 'Subject', 'bcs-smtp-manager' ); ?></th>
    1342                                                                         <th><?php esc_html_e( 'From', 'bcs-smtp-manager' ); ?></th>
    1343                                                                         <th><?php esc_html_e( 'To', 'bcs-smtp-manager' ); ?></th>
    1344                                                                         <th><?php esc_html_e( 'Status', 'bcs-smtp-manager' ); ?></th>
    1345                                                                         <th><?php esc_html_e( 'Error', 'bcs-smtp-manager' ); ?></th>
    1346                                                                         <th><?php esc_html_e( 'Actions', 'bcs-smtp-manager' ); ?></th>
     2252                                                                        <th scope="col" class="bcs-smtp-col-date"><?php esc_html_e( 'Time', 'bcs-smtp-manager' ); ?></th>
     2253                                                                        <th scope="col" class="bcs-smtp-col-subject"><?php esc_html_e( 'Subject', 'bcs-smtp-manager' ); ?></th>
     2254                                                                        <th scope="col" class="bcs-smtp-col-to"><?php esc_html_e( 'To', 'bcs-smtp-manager' ); ?></th>
     2255                                                                        <th scope="col" class="bcs-smtp-col-status"><?php esc_html_e( 'Status', 'bcs-smtp-manager' ); ?></th>
     2256                                                                        <th scope="col" class="bcs-smtp-col-more"><span class="screen-reader-text"><?php esc_html_e( 'Open', 'bcs-smtp-manager' ); ?></span></th>
    13472257                                                                </tr>
    13482258                                                        </thead>
    13492259                                                        <tbody id="bcs-smtp-log-tbody">
    1350                                                                 <tr><td colspan="7" style="text-align:center;padding:24px;color:var(--bcs-smtp-text-3)"><?php esc_html_e( 'Loading…', 'bcs-smtp-manager' ); ?></td></tr>
     2260                                                                <tr><td colspan="5" class="bcs-smtp-table-empty"><?php esc_html_e( 'Loading…', 'bcs-smtp-manager' ); ?></td></tr>
    13512261                                                        </tbody>
    13522262                                                </table>
    13532263                                        </div>
    13542264
    1355                                         <!-- Pagination + per-page at bottom -->
    1356                                         <div style="margin-top:12px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px">
    1357                                                 <div style="display:flex;align-items:center;gap:8px">
    1358                                                         <label style="font-size:12px;color:var(--bcs-smtp-text-2)"><?php esc_html_e( 'Show per page:', 'bcs-smtp-manager' ); ?></label>
    1359                                                         <select id="bcs-smtp-log-per-page" class="bcs-smtp-input" style="width:auto;min-width:80px">
    1360                                                                 <option value="10" selected>10</option>
    1361                                                                 <option value="20">20</option>
     2265                                        <div class="bcs-smtp-pager">
     2266                                                <div class="bcs-smtp-pager-size">
     2267                                                        <label for="bcs-smtp-log-per-page"><?php esc_html_e( 'Rows', 'bcs-smtp-manager' ); ?></label>
     2268                                                        <select id="bcs-smtp-log-per-page" class="bcs-smtp-input bcs-smtp-select bcs-smtp-select--sm">
     2269                                                                <option value="10">10</option>
     2270                                                                <option value="25" selected>25</option>
    13622271                                                                <option value="50">50</option>
    13632272                                                                <option value="100">100</option>
    13642273                                                        </select>
    13652274                                                </div>
    1366                                                 <div id="bcs-smtp-log-pagination" style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
    1367                                                         <button type="button" id="bcs-smtp-log-prev" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm" disabled>&larr; <?php esc_html_e( 'Prev', 'bcs-smtp-manager' ); ?></button>
    1368                                                         <span id="bcs-smtp-log-page-info" style="font-size:12px;color:var(--bcs-smtp-text-2)"><?php esc_html_e( 'Page 1', 'bcs-smtp-manager' ); ?></span>
    1369                                                         <button type="button" id="bcs-smtp-log-next" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm"><?php esc_html_e( 'Next', 'bcs-smtp-manager' ); ?> &rarr;</button>
    1370                                                 </div>
    1371                                         </div>
     2275                                                <div id="bcs-smtp-log-pagination" class="bcs-smtp-pager-nav">
     2276                                                        <button type="button" id="bcs-smtp-log-prev" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm" disabled><?php esc_html_e( 'Previous', 'bcs-smtp-manager' ); ?></button>
     2277                                                        <span id="bcs-smtp-log-page-info" aria-live="polite"></span>
     2278                                                        <button type="button" id="bcs-smtp-log-next" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm"><?php esc_html_e( 'Next', 'bcs-smtp-manager' ); ?></button>
     2279                                                </div>
    13722280                                        </div>
    13732281                                </div>
    1374 
    1375                                 <!-- Log Retention accordion -->
     2282                        </section>
     2283
     2284                        <!-- ═══ PANEL: Alerts ═════════════════════════════════════════════ -->
     2285                        <section id="bcs-smtp-panel-alerts" class="bcs-smtp-tab-panel" role="tabpanel" aria-labelledby="bcs-smtp-tab-alerts">
     2286
     2287                                <div class="bcs-smtp-alerts-grid">
     2288                                        <section class="bcs-smtp-panel bcs-smtp-alerts-card" aria-labelledby="bcs-smtp-alerts-title">
     2289                                                <div class="bcs-smtp-alerts-head">
     2290                                                        <span class="bcs-smtp-alerts-badge dashicons dashicons-bell" aria-hidden="true"></span>
     2291                                                        <div class="bcs-smtp-alerts-intro">
     2292                                                                <h2 id="bcs-smtp-alerts-title"><label for="bcs-smtp-alert-channel-enabled"><?php esc_html_e( 'Failure alerts', 'bcs-smtp-manager' ); ?></label></h2>
     2293                                                                <p><?php esc_html_e( 'A message in your team chat the moment email stops sending.', 'bcs-smtp-manager' ); ?></p>
     2294                                                        </div>
     2295                                                        <span class="bcs-smtp-toggle">
     2296                                                                <input type="checkbox" id="bcs-smtp-alert-channel-enabled" role="switch" aria-controls="bcs-smtp-alert-channel-accordion" <?php checked( $alert_enabled ); ?>>
     2297                                                                <span class="bcs-smtp-toggle-slider" aria-hidden="true"></span>
     2298                                                        </span>
     2299                                                </div>
     2300
     2301                                                <div id="bcs-smtp-alert-channel-accordion" class="bcs-smtp-reveal <?php echo $alert_enabled ? 'open' : ''; ?>">
     2302                                                        <div class="bcs-smtp-ch-list-head">
     2303                                                                <span id="bcs-smtp-channel-label"><?php esc_html_e( 'Channels', 'bcs-smtp-manager' ); ?></span>
     2304                                                                <span class="bcs-smtp-ch-count" id="bcs-smtp-ch-count" aria-live="polite"></span>
     2305                                                        </div>
     2306                                                        <?php
     2307                                                        $channel_icons = array(
     2308                                                                'slack'       => 'format-chat',
     2309                                                                'teams'       => 'groups',
     2310                                                                'google_chat' => 'format-status',
     2311                                                                'discord'     => 'games',
     2312                                                                'custom'      => 'admin-links',
     2313                                                        );
     2314                                                        $webhooks = array(
     2315                                                                'slack'       => array( 'opt' => BCS_SMTP_OPT_SLACK_WEBHOOK_URL,       'id' => 'slack',       'sub' => __( 'Incoming webhook', 'bcs-smtp-manager' ),                 'ph' => 'https://hooks.slack.com/services/…', 'how' => __( 'In Slack: Apps → Incoming Webhooks → Add to a channel.', 'bcs-smtp-manager' ), 'link' => 'https://api.slack.com/messaging/webhooks' ),
     2316                                                                'teams'       => array( 'opt' => BCS_SMTP_OPT_TEAMS_WEBHOOK_URL,       'id' => 'teams',       'sub' => __( 'Workflows webhook', 'bcs-smtp-manager' ),                'ph' => 'https://…powerplatform.com/…/workflows/…', 'how' => __( 'In Teams: channel → Workflows → “Post to a channel when a webhook request is received”.', 'bcs-smtp-manager' ), 'link' => 'https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook' ),
     2317                                                                'google_chat' => array( 'opt' => BCS_SMTP_OPT_GOOGLE_CHAT_WEBHOOK_URL, 'id' => 'google-chat', 'sub' => __( 'Space webhook', 'bcs-smtp-manager' ),                    'ph' => 'https://chat.googleapis.com/v1/spaces/…', 'how' => __( 'In Google Chat: space → Apps & integrations → Webhooks.', 'bcs-smtp-manager' ), 'link' => 'https://developers.google.com/workspace/chat/quickstart/webhooks' ),
     2318                                                                'discord'     => array( 'opt' => BCS_SMTP_OPT_DISCORD_WEBHOOK_URL,     'id' => 'discord',     'sub' => __( 'Channel webhook', 'bcs-smtp-manager' ),                  'ph' => 'https://discord.com/api/webhooks/…', 'how' => __( 'In Discord: Server Settings → Integrations → Webhooks.', 'bcs-smtp-manager' ), 'link' => 'https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks' ),
     2319                                                                'custom'      => array( 'opt' => BCS_SMTP_OPT_CUSTOM_WEBHOOK_URL,      'id' => 'custom',      'sub' => __( 'JSON for Zapier, Make or n8n', 'bcs-smtp-manager' ),      'ph' => 'https://example.com/webhook', 'how' => __( 'Receives a JSON payload with the event, site, error and failure count.', 'bcs-smtp-manager' ), 'link' => '' ),
     2320                                                        );
     2321                                                        ?>
     2322                                                        <ul class="bcs-smtp-ch-list" aria-labelledby="bcs-smtp-channel-label">
     2323                                                                <?php
     2324                                                                foreach ( $channels as $val => $ch ) :
     2325                                                                        $w = $webhooks[ $val ];
     2326                                                                        ?>
     2327                                                                        <li class="bcs-smtp-ch" data-channel="<?php echo esc_attr( $val ); ?>">
     2328                                                                                <div class="bcs-smtp-ch-row">
     2329                                                                                        <button type="button" class="bcs-smtp-ch-main" aria-expanded="false" aria-controls="bcs-smtp-<?php echo esc_attr( $w['id'] ); ?>-webhook-row">
     2330                                                                                                <span class="bcs-smtp-ch-icon dashicons dashicons-<?php echo esc_attr( $channel_icons[ $val ] ); ?>" aria-hidden="true"></span>
     2331                                                                                                <span class="bcs-smtp-ch-text">
     2332                                                                                                        <span class="bcs-smtp-ch-name"><?php echo esc_html( $ch['label'] ); ?></span>
     2333                                                                                                        <span class="bcs-smtp-ch-sub"><?php echo esc_html( $w['sub'] ); ?></span>
     2334                                                                                                </span>
     2335                                                                                                <span class="bcs-smtp-ch-pill" aria-live="polite"></span>
     2336                                                                                                <span class="bcs-smtp-ch-chevron dashicons dashicons-arrow-down-alt2" aria-hidden="true"></span>
     2337                                                                                        </button>
     2338                                                                                        <span class="bcs-smtp-toggle">
     2339                                                                                                <input type="checkbox" name="bcs_alert_channels[]" class="bcs-smtp-channel-radio" role="switch" autocomplete="off" value="<?php echo esc_attr( $val ); ?>" data-label="<?php echo esc_attr( $ch['label'] ); ?>" aria-label="<?php echo esc_attr( sprintf( /* translators: %s: alert channel name, e.g. Slack. */ __( 'Send alerts to %s', 'bcs-smtp-manager' ), $ch['label'] ) ); ?>" <?php checked( in_array( $val, $alert_chs, true ) ); ?>>
     2340                                                                                                <span class="bcs-smtp-toggle-slider" aria-hidden="true"></span>
     2341                                                                                        </span>
     2342                                                                                </div>
     2343                                                                                <div id="bcs-smtp-<?php echo esc_attr( $w['id'] ); ?>-webhook-row" class="bcs-smtp-webhook" data-channel="<?php echo esc_attr( $val ); ?>" hidden>
     2344                                                                                        <div class="bcs-smtp-webhook-head">
     2345                                                                                                <label class="bcs-smtp-field-label" for="bcs-smtp-<?php echo esc_attr( $w['id'] ); ?>-webhook-url"><?php esc_html_e( 'Webhook URL', 'bcs-smtp-manager' ); ?></label>
     2346                                                                                                <?php if ( $w['link'] ) : ?>
     2347                                                                                                        <a class="bcs-smtp-webhook-guide" href="<?php echo esc_url( $w['link'] ); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Setup guide', 'bcs-smtp-manager' ); ?><span class="dashicons dashicons-external" aria-hidden="true"></span><span class="screen-reader-text"> <?php esc_html_e( '(opens in a new tab)', 'bcs-smtp-manager' ); ?></span></a>
     2348                                                                                                <?php endif; ?>
     2349                                                                                        </div>
     2350                                                                                        <input type="url" id="bcs-smtp-<?php echo esc_attr( $w['id'] ); ?>-webhook-url" class="bcs-smtp-input bcs-smtp-mono" value="<?php echo esc_attr( get_option( $w['opt'], '' ) ); ?>" placeholder="<?php echo esc_attr( $w['ph'] ); ?>" spellcheck="false" autocomplete="off" aria-describedby="bcs-smtp-<?php echo esc_attr( $w['id'] ); ?>-webhook-err bcs-smtp-<?php echo esc_attr( $w['id'] ); ?>-webhook-how">
     2351                                                                                        <p class="bcs-smtp-webhook-err" id="bcs-smtp-<?php echo esc_attr( $w['id'] ); ?>-webhook-err" hidden></p>
     2352                                                                                        <p class="bcs-smtp-webhook-how" id="bcs-smtp-<?php echo esc_attr( $w['id'] ); ?>-webhook-how"><?php echo esc_html( $w['how'] ); ?></p>
     2353                                                                                </div>
     2354                                                                        </li>
     2355                                                                <?php endforeach; ?>
     2356                                                        </ul>
     2357                                                </div>
     2358
     2359                                                <div class="bcs-smtp-alerts-off" id="bcs-smtp-alerts-off" <?php echo $alert_enabled ? 'hidden' : ''; ?>>
     2360                                                        <span class="dashicons dashicons-bell" aria-hidden="true"></span>
     2361                                                        <p><?php esc_html_e( 'Alerts are off. Health checks still run, but you’ll only see a problem when you open this page.', 'bcs-smtp-manager' ); ?></p>
     2362                                                </div>
     2363
     2364                                                <div class="bcs-smtp-actions bcs-smtp-card-foot">
     2365                                                        <button type="button" id="bcs-smtp-save-alerts-btn" class="bcs-smtp-btn bcs-smtp-btn-primary"><?php esc_html_e( 'Save alerts', 'bcs-smtp-manager' ); ?></button>
     2366                                                        <button type="button" id="bcs-smtp-test-alert-btn" class="bcs-smtp-btn bcs-smtp-btn-secondary" <?php disabled( ! $alert_enabled || ! $alert_chs ); ?>><?php esc_html_e( 'Send test alert', 'bcs-smtp-manager' ); ?></button>
     2367                                                        <span class="bcs-smtp-unsaved" id="bcs-smtp-alerts-unsaved" hidden><?php esc_html_e( 'Unsaved changes', 'bcs-smtp-manager' ); ?></span>
     2368                                                </div>
     2369                                                <div id="bcs-smtp-alerts-msg" class="bcs-smtp-msg" role="status" aria-live="polite"></div>
     2370                                        </section>
     2371
     2372                                        <aside class="bcs-smtp-panel bcs-smtp-health-side" aria-labelledby="bcs-smtp-monitor-title">
     2373                                                <div class="bcs-smtp-health-head">
     2374                                                        <span class="bcs-smtp-health-icon dashicons dashicons-heart" aria-hidden="true"></span>
     2375                                                        <div>
     2376                                                                <h2 id="bcs-smtp-monitor-title"><?php esc_html_e( 'Health checks run automatically', 'bcs-smtp-manager' ); ?></h2>
     2377                                                                <p><?php esc_html_e( 'Each check signs in to your mail server without sending an email.', 'bcs-smtp-manager' ); ?></p>
     2378                                                        </div>
     2379                                                </div>
     2380
     2381                                                <?php if ( $monitor_on ) : ?>
     2382                                                        <dl class="bcs-smtp-health-times">
     2383                                                                <div>
     2384                                                                        <dt><?php esc_html_e( 'Last check', 'bcs-smtp-manager' ); ?></dt>
     2385                                                                        <dd>
     2386                                                                                <?php
     2387                                                                                /* translators: %s: time since the last check, e.g. "5 minutes". */
     2388                                                                                echo $last_check ? esc_html( sprintf( __( '%s ago', 'bcs-smtp-manager' ), $this->check_minutes( $last_check ) ) ) : esc_html__( 'Not checked yet', 'bcs-smtp-manager' );
     2389                                                                                ?>
     2390                                                                        </dd>
     2391                                                                </div>
     2392                                                                <div>
     2393                                                                        <dt><?php esc_html_e( 'Next check', 'bcs-smtp-manager' ); ?></dt>
     2394                                                                        <dd>
     2395                                                                                <?php
     2396                                                                                if ( ! $next_check ) {
     2397                                                                                        esc_html_e( 'Being scheduled', 'bcs-smtp-manager' );
     2398                                                                                } elseif ( $next_check > time() ) {
     2399                                                                                        /* translators: %s: time until the next check, e.g. "10 minutes". */
     2400                                                                                        echo esc_html( sprintf( __( 'in %s', 'bcs-smtp-manager' ), $this->check_minutes( $next_check, true ) ) );
     2401                                                                                } else {
     2402                                                                                        esc_html_e( 'Due now', 'bcs-smtp-manager' );
     2403                                                                                }
     2404                                                                                ?>
     2405                                                                        </dd>
     2406                                                                </div>
     2407                                                        </dl>
     2408                                                <?php else : ?>
     2409                                                        <p class="bcs-smtp-health-pending"><?php esc_html_e( 'Health checks start once your connection is set up.', 'bcs-smtp-manager' ); ?></p>
     2410                                                <?php endif; ?>
     2411
     2412                                                <ul class="bcs-smtp-health-rules">
     2413                                                        <li><span class="dashicons dashicons-clock" aria-hidden="true"></span>
     2414                                                                <?php
     2415                                                                /* translators: %d: minutes between health checks. */
     2416                                                                printf( esc_html__( 'Every %d minutes', 'bcs-smtp-manager' ), (int) BCS_SMTP_CHECK_INTERVAL_MINUTES );
     2417                                                                ?>
     2418                                                        </li>
     2419                                                        <li><span class="dashicons dashicons-bell" aria-hidden="true"></span><?php esc_html_e( 'Alert on the failed check', 'bcs-smtp-manager' ); ?></li>
     2420                                                        <li><span class="dashicons dashicons-shield" aria-hidden="true"></span><?php esc_html_e( 'One alert per outage, never a flood', 'bcs-smtp-manager' ); ?></li>
     2421                                                        <li><span class="dashicons dashicons-update" aria-hidden="true"></span><?php esc_html_e( 'Resets when mail recovers', 'bcs-smtp-manager' ); ?></li>
     2422                                                </ul>
     2423                                        </aside>
     2424                                </div>
     2425                        </section>
     2426
     2427                        <!-- ═══ PANEL: Reports ════════════════════════════════════════════ -->
     2428                        <section id="bcs-smtp-panel-reports" class="bcs-smtp-tab-panel" role="tabpanel" aria-labelledby="bcs-smtp-tab-reports">
    13762429                                <?php
    1377                                 $prune_mode = get_option( 'bcs_log_prune_mode', 'days' );
     2430                                $report_ranges = array(
     2431                                        '7days'   => __( '7 days', 'bcs-smtp-manager' ),
     2432                                        '30days'  => __( '30 days', 'bcs-smtp-manager' ),
     2433                                        '3months' => __( '90 days', 'bcs-smtp-manager' ),
     2434                                );
    13782435                                ?>
    1379                                 <div class="bcs-smtp-accordion" style="margin-top:12px">
    1380                                         <button type="button" class="bcs-smtp-accordion-header" aria-expanded="false">
    1381                                                 <span class="dashicons dashicons-trash"></span>
    1382                                                 <span><?php esc_html_e( 'Log Retention', 'bcs-smtp-manager' ); ?></span>
    1383                                                 <span class="bcs-smtp-accordion-arrow dashicons dashicons-arrow-down-alt2" style="margin-left:auto"></span>
    1384                                         </button>
    1385                                         <div class="bcs-smtp-accordion-body" style="display:none">
    1386                                                 <p style="font-size:13px;color:var(--bcs-smtp-text-2);margin:0 0 16px"><?php esc_html_e( 'Automatically delete old log entries to keep the database lean. Runs daily via WP-Cron.', 'bcs-smtp-manager' ); ?></p>
    1387                                                 <div class="bcs-smtp-check-list" style="flex-direction:column;gap:6px;margin-bottom:16px">
    1388                                                         <label class="bcs-smtp-check-item" style="flex-direction:row;align-items:center;gap:10px;padding:10px 14px">
    1389                                                                 <input type="radio" name="bcs_smtp_prune_mode" class="bcs-smtp-prune-radio" value="days" <?php checked( $prune_mode, 'days' ); ?> style="width:20px;height:20px;flex-shrink:0">
    1390                                                                 <span class="bcs-smtp-check-label-title"><?php esc_html_e( 'Keep last', 'bcs-smtp-manager' ); ?></span>
    1391                                                                 <input type="number" class="bcs-smtp-input" value="30" readonly disabled
    1392                                                                         style="width:80px;display:inline-block;opacity:.7;cursor:not-allowed">
    1393                                                                 <span style="font-size:13px;color:var(--bcs-smtp-text-2)"><?php esc_html_e( 'days', 'bcs-smtp-manager' ); ?> <span style="font-size:11px;color:var(--bcs-smtp-text-3)"><?php esc_html_e( '(fixed)', 'bcs-smtp-manager' ); ?></span></span>
    1394                                                         </label>
    1395                                                         <label class="bcs-smtp-check-item" style="flex-direction:row;align-items:center;gap:10px;padding:10px 14px">
    1396                                                                 <input type="radio" name="bcs_smtp_prune_mode" class="bcs-smtp-prune-radio" value="records" <?php checked( $prune_mode, 'records' ); ?> style="width:20px;height:20px;flex-shrink:0">
    1397                                                                 <span class="bcs-smtp-check-label-title"><?php esc_html_e( 'Keep last', 'bcs-smtp-manager' ); ?></span>
    1398                                                                 <input type="number" class="bcs-smtp-input" value="1000" readonly disabled
    1399                                                                         style="width:80px;display:inline-block;opacity:.7;cursor:not-allowed">
    1400                                                                 <span style="font-size:13px;color:var(--bcs-smtp-text-2)"><?php esc_html_e( 'records', 'bcs-smtp-manager' ); ?> <span style="font-size:11px;color:var(--bcs-smtp-text-3)"><?php esc_html_e( '(fixed)', 'bcs-smtp-manager' ); ?></span></span>
    1401                                                         </label>
    1402                                                 </div>
    1403                                                 <button type="button" id="bcs-smtp-save-log-prune-btn" class="bcs-smtp-btn bcs-smtp-btn-primary">
    1404                                                         <span class="dashicons dashicons-saved"></span><?php esc_html_e( 'Save Retention Settings', 'bcs-smtp-manager' ); ?>
    1405                                                 </button>
    1406                                                 <div id="bcs-smtp-log-prune-msg" role="status" aria-live="polite" style="display:none;margin-top:10px"></div>
     2436                                <section class="bcs-smtp-panel bcs-smtp-report" aria-labelledby="bcs-smtp-report-title">
     2437                                        <div class="bcs-smtp-panel-head">
     2438                                                <div>
     2439                                                        <h2 id="bcs-smtp-report-title"><?php esc_html_e( 'Email delivery', 'bcs-smtp-manager' ); ?></h2>
     2440                                                        <p class="bcs-smtp-panel-sub" id="bcs-smtp-report-period"><?php esc_html_e( 'Last 30 days', 'bcs-smtp-manager' ); ?></p>
     2441                                                </div>
     2442                                                <div class="bcs-smtp-report-tools">
     2443                                                        <div class="bcs-smtp-segmented bcs-smtp-segmented--sm" role="radiogroup" aria-label="<?php esc_attr_e( 'Time range', 'bcs-smtp-manager' ); ?>">
     2444                                                                <?php foreach ( $report_ranges as $val => $label ) : ?>
     2445                                                                        <label><input type="radio" name="bcs_smtp_rp_range" class="bcs-smtp-rp-range" autocomplete="off" value="<?php echo esc_attr( $val ); ?>" data-label="<?php echo esc_attr( $label ); ?>" <?php checked( '30days', $val ); ?>><span><?php echo esc_html( $label ); ?></span></label>
     2446                                                                <?php endforeach; ?>
     2447                                                        </div>
     2448                                                        <button type="button" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm" id="bcs-smtp-report-export">
     2449                                                                <span class="dashicons dashicons-download" aria-hidden="true"></span><?php esc_html_e( 'Export CSV', 'bcs-smtp-manager' ); ?>
     2450                                                        </button>
     2451                                                </div>
    14072452                                        </div>
     2453
     2454                                        <dl class="bcs-smtp-figures bcs-smtp-figures--5 bcs-smtp-report-stats">
     2455                                                <div><dt><?php esc_html_e( 'Total', 'bcs-smtp-manager' ); ?></dt><dd id="bcs-smtp-report-total"><?php echo esc_html( number_format_i18n( $summary_30d['total'] ) ); ?></dd></div>
     2456                                                <div><dt><span class="bcs-smtp-key bcs-smtp-key--sent" aria-hidden="true"></span><?php esc_html_e( 'Sent', 'bcs-smtp-manager' ); ?></dt><dd id="bcs-smtp-report-success"><?php echo esc_html( number_format_i18n( $summary_30d['success'] ) ); ?></dd></div>
     2457                                                <div><dt><span class="bcs-smtp-key bcs-smtp-key--bad" aria-hidden="true"></span><?php esc_html_e( 'Failed', 'bcs-smtp-manager' ); ?></dt><dd id="bcs-smtp-report-failed" class="<?php echo $summary_30d['failed'] > 0 ? 'is-bad' : ''; ?>"><?php echo esc_html( number_format_i18n( $summary_30d['failed'] ) ); ?></dd></div>
     2458                                                <div><dt><span class="bcs-smtp-key bcs-smtp-key--warn" aria-hidden="true"></span><?php esc_html_e( 'Unconfirmed', 'bcs-smtp-manager' ); ?></dt><dd id="bcs-smtp-report-unconfirmed"><?php echo esc_html( number_format_i18n( $summary_30d['unconfirmed'] ) ); ?></dd></div>
     2459                                                <div class="bcs-smtp-figure-rate">
     2460                                                        <dt><?php esc_html_e( 'Delivered', 'bcs-smtp-manager' ); ?></dt>
     2461                                                        <dd id="bcs-smtp-report-rate"><?php echo null === $rate ? '—' : esc_html( number_format_i18n( $rate, 1 ) ) . '<small>%</small>'; ?></dd>
     2462                                                        <span class="bcs-smtp-ratebar" aria-hidden="true"><span id="bcs-smtp-report-ratebar" style="width:<?php echo esc_attr( null === $rate ? 0 : $rate ); ?>%"></span></span>
     2463                                                </div>
     2464                                        </dl>
     2465
     2466                                        <div class="bcs-smtp-linechart bcs-smtp-linechart--lg" id="bcs-smtp-report-chart" tabindex="0"
     2467                                                role="img" aria-label="<?php esc_attr_e( 'Emails over time: sent, failed and unconfirmed. Use the left and right arrow keys to read each point.', 'bcs-smtp-manager' ); ?>"
     2468                                                data-chart="<?php echo esc_attr( wp_json_encode( $chart ) ); ?>">
     2469                                                <div class="bcs-smtp-linechart-tip" aria-live="polite" hidden></div>
     2470                                        </div>
     2471                                        <p class="bcs-smtp-chart-empty" id="bcs-smtp-chart-empty" <?php echo $summary_30d['total'] > 0 ? 'hidden' : ''; ?>><?php esc_html_e( 'No email was sent in this period.', 'bcs-smtp-manager' ); ?></p>
     2472                                </section>
     2473
     2474                                <div class="bcs-smtp-report-grid">
     2475                                        <section class="bcs-smtp-panel" aria-labelledby="bcs-smtp-highlights-title">
     2476                                                <div class="bcs-smtp-panel-head">
     2477                                                        <h2 id="bcs-smtp-highlights-title"><?php esc_html_e( 'Highlights', 'bcs-smtp-manager' ); ?></h2>
     2478                                                </div>
     2479                                                <dl class="bcs-smtp-highlights" id="bcs-smtp-highlights">
     2480                                                        <div><dt><span class="bcs-smtp-hl-icon dashicons dashicons-chart-line" aria-hidden="true"></span><?php esc_html_e( 'Average per day', 'bcs-smtp-manager' ); ?></dt><dd data-hl="avg">—</dd></div>
     2481                                                        <div><dt><span class="bcs-smtp-hl-icon dashicons dashicons-calendar-alt" aria-hidden="true"></span><?php esc_html_e( 'Busiest day', 'bcs-smtp-manager' ); ?></dt><dd data-hl="peak">—</dd></div>
     2482                                                        <div><dt><span class="bcs-smtp-hl-icon bcs-smtp-hl-icon--ok dashicons dashicons-yes-alt" aria-hidden="true"></span><?php esc_html_e( 'Failure-free days', 'bcs-smtp-manager' ); ?></dt><dd data-hl="cleandays">—</dd></div>
     2483                                                        <div><dt><span class="bcs-smtp-hl-icon bcs-smtp-hl-icon--ok dashicons dashicons-awards" aria-hidden="true"></span><?php esc_html_e( 'Longest failure-free run', 'bcs-smtp-manager' ); ?></dt><dd data-hl="beststreak">—</dd></div>
     2484                                                        <div><dt><span class="bcs-smtp-hl-icon bcs-smtp-hl-icon--ok dashicons dashicons-groups" aria-hidden="true"></span><span><?php esc_html_e( 'People reached', 'bcs-smtp-manager' ); ?><small class="bcs-smtp-hl-sub"><?php esc_html_e( 'Visitors and customers who received your email', 'bcs-smtp-manager' ); ?></small></span></dt><dd data-hl="recipients" data-initial="<?php list( $own, $own_domains ) = $this->internal_recipients(); echo esc_attr( $this->db->count_unique_recipients( '30days', $own, $own_domains ) ); ?>">—</dd></div>
     2485                                                </dl>
     2486                                        </section>
     2487
     2488                                        <section class="bcs-smtp-panel bcs-smtp-schedule" aria-labelledby="bcs-smtp-schedule-title">
     2489                                                <div class="bcs-smtp-toggle-row bcs-smtp-toggle-row--head">
     2490                                                        <div class="bcs-smtp-toggle-row-info">
     2491                                                                <h2 id="bcs-smtp-schedule-title"><label for="bcs-smtp-report-schedule-enabled"><?php esc_html_e( 'Email me this report', 'bcs-smtp-manager' ); ?></label></h2>
     2492                                                                <p class="bcs-smtp-toggle-row-desc"><?php esc_html_e( 'On the 1st of every month at 9:00 am, a summary of last month’s sent, failed and unconfirmed email with a daily chart.', 'bcs-smtp-manager' ); ?></p>
     2493                                                        </div>
     2494                                                        <span class="bcs-smtp-toggle">
     2495                                                                <input type="checkbox" id="bcs-smtp-report-schedule-enabled" role="switch" aria-controls="bcs-smtp-report-body" <?php checked( $report_schedule_enabled ); ?>>
     2496                                                                <span class="bcs-smtp-toggle-slider" aria-hidden="true"></span>
     2497                                                        </span>
     2498                                                </div>
     2499                                                <div id="bcs-smtp-report-body" <?php echo $report_schedule_enabled ? '' : 'hidden'; ?>>
     2500                                                <div class="bcs-smtp-field">
     2501                                                        <label class="bcs-smtp-field-label" for="bcs-smtp-report-emails-entry"><?php esc_html_e( 'Send to', 'bcs-smtp-manager' ); ?></label>
     2502                                                        <div class="bcs-smtp-chips" id="bcs-smtp-report-chips">
     2503                                                                <ul class="bcs-smtp-chip-list" aria-label="<?php esc_attr_e( 'Report recipients', 'bcs-smtp-manager' ); ?>"></ul>
     2504                                                                <input type="text" inputmode="email" id="bcs-smtp-report-emails-entry" class="bcs-smtp-chips-input" placeholder="<?php esc_attr_e( 'Add an email address', 'bcs-smtp-manager' ); ?>" autocomplete="off" spellcheck="false">
     2505                                                        </div>
     2506                                                        <input type="hidden" id="bcs-smtp-report-emails" value="<?php echo esc_attr( $report_emails ? $report_emails : get_option( 'admin_email' ) ); ?>">
     2507                                                </div>
     2508                                                <?php $next_report = $this->next_report_timestamp(); ?>
     2509                                                <p class="bcs-smtp-report-next" id="bcs-smtp-report-next" <?php echo $next_report ? '' : 'hidden'; ?>>
     2510                                                        <span class="dashicons dashicons-clock" aria-hidden="true"></span>
     2511                                                        <span id="bcs-smtp-report-next-text"><?php echo $next_report ? esc_html( $this->describe_next_report( $next_report ) ) : ''; ?></span>
     2512                                                </p>
     2513                                                </div>
     2514                                                <div class="bcs-smtp-alerts-off" id="bcs-smtp-report-off" <?php echo $report_schedule_enabled ? 'hidden' : ''; ?>>
     2515                                                        <span class="dashicons dashicons-email-alt" aria-hidden="true"></span>
     2516                                                        <p><?php esc_html_e( 'Monthly reports are off. Your stats stay on this page.', 'bcs-smtp-manager' ); ?></p>
     2517                                                </div>
     2518                                                <div class="bcs-smtp-actions">
     2519                                                        <button type="button" id="bcs-smtp-save-report-schedule-btn" class="bcs-smtp-btn bcs-smtp-btn-primary"><?php esc_html_e( 'Save schedule', 'bcs-smtp-manager' ); ?></button>
     2520                                                        <button type="button" id="bcs-smtp-send-report-now-btn" class="bcs-smtp-btn bcs-smtp-btn-secondary" <?php echo $report_schedule_enabled ? '' : 'hidden'; ?>><?php esc_html_e( 'Send a report now', 'bcs-smtp-manager' ); ?></button>
     2521                                                </div>
     2522                                                <div id="bcs-smtp-report-schedule-msg" class="bcs-smtp-msg" role="status" aria-live="polite"></div>
     2523                                        </section>
    14082524                                </div>
    1409                         </div>
    1410 
    1411                         <!-- ═══════════════════════════════════════════════════════════════
    1412                              PANEL: Email Report
    1413                              ═══════════════════════════════════════════════════════════════ -->
    1414                         <div id="bcs-smtp-panel-email-report" class="bcs-smtp-tab-panel">
    1415                                 <div class="bcs-smtp-card">
    1416                                         <div class="bcs-smtp-card-header">
    1417                                                 <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px">
    1418                                                         <p class="bcs-smtp-card-title" style="margin:0"><span class="dashicons dashicons-chart-bar"></span><?php esc_html_e( 'Email Report', 'bcs-smtp-manager' ); ?></p>
    1419                                                         <select id="bcs-smtp-report-date-range" class="bcs-smtp-input" style="max-width:160px">
    1420                                                                 <option value="today"><?php esc_html_e( 'Today', 'bcs-smtp-manager' ); ?></option>
    1421                                                                 <option value="yesterday"><?php esc_html_e( 'Yesterday', 'bcs-smtp-manager' ); ?></option>
    1422                                                                 <option value="7days"><?php esc_html_e( 'Last 7 days', 'bcs-smtp-manager' ); ?></option>
    1423                                                                 <option value="30days" selected><?php esc_html_e( 'Last 30 days', 'bcs-smtp-manager' ); ?></option>
    1424                                                                 <option value="this_week"><?php esc_html_e( 'This week', 'bcs-smtp-manager' ); ?></option>
    1425                                                                 <option value="last_week"><?php esc_html_e( 'Last week', 'bcs-smtp-manager' ); ?></option>
    1426                                                                 <option value="this_month"><?php esc_html_e( 'This month', 'bcs-smtp-manager' ); ?></option>
    1427                                                                 <option value="3months"><?php esc_html_e( 'Last 3 months', 'bcs-smtp-manager' ); ?></option>
    1428                                                                 <option value="6months"><?php esc_html_e( 'Last 6 months', 'bcs-smtp-manager' ); ?></option>
    1429                                                                 <option value="year"><?php esc_html_e( 'This year', 'bcs-smtp-manager' ); ?></option>
    1430                                                                 <option value="all"><?php esc_html_e( 'All time', 'bcs-smtp-manager' ); ?></option>
    1431                                                         </select>
    1432                                                 </div>
    1433                                         </div>
    1434                                         <div class="bcs-smtp-card-body">
    1435                                                 <!-- Summary stat boxes -->
    1436                                                 <div class="bcs-smtp-report-stats" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:20px">
    1437                                                         <div class="bcs-smtp-stat" style="border:1px solid var(--bcs-smtp-border);border-radius:var(--bcs-smtp-radius);padding:16px">
    1438                                                                 <div class="bcs-smtp-stat-icon good">
    1439                                                                         <span class="dashicons dashicons-yes"></span>
    1440                                                                 </div>
    1441                                                                 <div class="bcs-smtp-stat-body">
    1442                                                                         <div class="bcs-smtp-stat-value good" id="bcs-smtp-report-success" style="font-size:28px"><?php echo esc_html( $summary_30d['success'] ); ?></div>
    1443                                                                         <div class="bcs-smtp-stat-label"><?php esc_html_e( 'Sent', 'bcs-smtp-manager' ); ?></div>
    1444                                                                 </div>
    1445                                                         </div>
    1446                                                         <div class="bcs-smtp-stat" style="border:1px solid var(--bcs-smtp-border);border-radius:var(--bcs-smtp-radius);padding:16px">
    1447                                                                 <div class="bcs-smtp-stat-icon bad">
    1448                                                                         <span class="dashicons dashicons-warning"></span>
    1449                                                                 </div>
    1450                                                                 <div class="bcs-smtp-stat-body">
    1451                                                                         <div class="bcs-smtp-stat-value bad" id="bcs-smtp-report-failed" style="font-size:28px"><?php echo esc_html( $summary_30d['failed'] ); ?></div>
    1452                                                                         <div class="bcs-smtp-stat-label"><?php esc_html_e( 'Failed', 'bcs-smtp-manager' ); ?></div>
    1453                                                                 </div>
    1454                                                         </div>
    1455                                                         <div class="bcs-smtp-stat" style="border:1px solid var(--bcs-smtp-border);border-radius:var(--bcs-smtp-radius);padding:16px">
    1456                                                                 <div class="bcs-smtp-stat-icon">
    1457                                                                         <span class="dashicons dashicons-editor-help"></span>
    1458                                                                 </div>
    1459                                                                 <div class="bcs-smtp-stat-body">
    1460                                                                         <div class="bcs-smtp-stat-value" id="bcs-smtp-report-unconfirmed" style="font-size:28px"><?php echo esc_html( $summary_30d['unconfirmed'] ); ?></div>
    1461                                                                         <div class="bcs-smtp-stat-label"><?php esc_html_e( 'Unconfirmed', 'bcs-smtp-manager' ); ?></div>
    1462                                                                 </div>
    1463                                                         </div>
    1464                                                         <div class="bcs-smtp-stat" style="border:1px solid var(--bcs-smtp-border);border-radius:var(--bcs-smtp-radius);padding:16px">
    1465                                                                 <div class="bcs-smtp-stat-icon">
    1466                                                                         <span class="dashicons dashicons-chart-bar"></span>
    1467                                                                 </div>
    1468                                                                 <div class="bcs-smtp-stat-body">
    1469                                                                         <div class="bcs-smtp-stat-value" id="bcs-smtp-report-total" style="font-size:28px"><?php echo esc_html( $summary_30d['total'] ); ?></div>
    1470                                                                         <div class="bcs-smtp-stat-label"><?php esc_html_e( 'Total', 'bcs-smtp-manager' ); ?></div>
    1471                                                                 </div>
    1472                                                         </div>
    1473                                                 </div>
    1474 
    1475                                                 <!-- Chart -->
    1476                                                 <div style="position:relative;height:320px;width:100%;margin-bottom:0">
    1477                                                         <canvas id="bcs-smtp-report-chart"></canvas>
    1478                                                 </div>
    1479                                         </div>
    1480                                 </div>
    1481 
    1482                                 <!-- Schedule Report accordion — inside the panel, outside the card -->
    1483                                 <div class="bcs-smtp-accordion" style="margin-top:12px">
    1484                                         <button type="button" class="bcs-smtp-accordion-header" aria-expanded="false">
    1485                                                 <span class="dashicons dashicons-calendar-alt"></span>
    1486                                                 <span><?php esc_html_e( 'Schedule Report', 'bcs-smtp-manager' ); ?></span>
    1487                                                 <span class="bcs-smtp-accordion-arrow dashicons dashicons-arrow-down-alt2" style="margin-left:auto"></span>
    1488                                         </button>
    1489                                         <div class="bcs-smtp-accordion-body" style="display:none">
    1490                                                 <div class="bcs-smtp-toggle-row" style="padding:4px 0 16px">
    1491                                                         <div class="bcs-smtp-toggle-row-info">
    1492                                                                 <div class="bcs-smtp-toggle-row-label"><?php esc_html_e( 'Enable automated report emails', 'bcs-smtp-manager' ); ?></div>
    1493                                                                 <div class="bcs-smtp-toggle-row-desc"><?php esc_html_e( 'Automatically send email reports on the schedule below', 'bcs-smtp-manager' ); ?></div>
    1494                                                         </div>
    1495                                                         <label class="bcs-smtp-toggle">
    1496                                                                 <input type="checkbox" id="bcs-smtp-report-schedule-enabled" <?php checked( $report_schedule_enabled ); ?>>
    1497                                                                 <span class="bcs-smtp-toggle-slider"></span>
    1498                                                         </label>
    1499                                                 </div>
    1500                                                 <div class="bcs-smtp-field-row" style="margin-bottom:20px">
    1501                                                         <div class="bcs-smtp-field">
    1502                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-report-emails"><?php esc_html_e( 'Recipients', 'bcs-smtp-manager' ); ?> <span style="color:var(--bcs-smtp-text-3);font-weight:400"><?php esc_html_e( '(comma-separated)', 'bcs-smtp-manager' ); ?></span></label>
    1503                                                                 <input type="text" id="bcs-smtp-report-emails" class="bcs-smtp-input" value="<?php echo esc_attr( $report_emails ); ?>" placeholder="admin@example.com, manager@example.com">
    1504                                                         </div>
    1505                                                         <div class="bcs-smtp-field">
    1506                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-report-frequency"><?php esc_html_e( 'Frequency', 'bcs-smtp-manager' ); ?></label>
    1507                                                                 <select id="bcs-smtp-report-frequency" class="bcs-smtp-input">
    1508                                                                         <option value="daily"   <?php selected( $report_frequency, 'daily' ); ?>><?php esc_html_e( 'Daily', 'bcs-smtp-manager' ); ?></option>
    1509                                                                         <option value="weekly"  <?php selected( $report_frequency, 'weekly' ); ?>><?php esc_html_e( 'Weekly', 'bcs-smtp-manager' ); ?></option>
    1510                                                                         <option value="monthly" <?php selected( $report_frequency, 'monthly' ); ?>><?php esc_html_e( 'Monthly', 'bcs-smtp-manager' ); ?></option>
    1511                                                                 </select>
    1512                                                         </div>
    1513                                                 </div>
    1514                                                 <div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
    1515                                                         <button type="button" id="bcs-smtp-save-report-schedule-btn" class="bcs-smtp-btn bcs-smtp-btn-primary">
    1516                                                                 <span class="dashicons dashicons-saved"></span><?php esc_html_e( 'Save Schedule', 'bcs-smtp-manager' ); ?>
    1517                                                         </button>
    1518                                                         <button type="button" id="bcs-smtp-send-report-now-btn" class="bcs-smtp-btn bcs-smtp-btn-secondary">
    1519                                                                 <span class="dashicons dashicons-email-alt"></span><?php esc_html_e( 'Send Report Now', 'bcs-smtp-manager' ); ?>
    1520                                                         </button>
    1521                                                 </div>
    1522                                                 <div id="bcs-smtp-report-schedule-msg" role="status" aria-live="polite" style="display:none;margin-top:12px"></div>
    1523                                         </div>
    1524                                 </div>
    1525 
    1526                         </div>
    1527 
    1528                         <!-- ═══════════════════════════════════════════════════════════════
    1529                              PANEL: Send Test Email
    1530                              ═══════════════════════════════════════════════════════════════ -->
    1531                         <div id="bcs-smtp-panel-test" class="bcs-smtp-tab-panel">
    1532                                 <div class="bcs-smtp-grid">
    1533                                         <div>
    1534                                                 <div class="bcs-smtp-card">
    1535                                                         <div class="bcs-smtp-card-header">
    1536                                                                 <p class="bcs-smtp-card-title"><span class="dashicons dashicons-email"></span><?php esc_html_e( 'Send Test Email', 'bcs-smtp-manager' ); ?></p>
    1537                                                                 <p class="bcs-smtp-card-desc"><?php esc_html_e( "Trigger a real email send via WordPress's", 'bcs-smtp-manager' ); ?> <code>wp_mail()</code>. <?php esc_html_e( 'This validates the full SMTP stack and logs the result.', 'bcs-smtp-manager' ); ?></p>
    1538                                                         </div>
    1539                                                         <div class="bcs-smtp-card-body">
    1540                                                                 <div class="bcs-smtp-field">
    1541                                                                         <label class="bcs-smtp-field-label" for="bcs-smtp-test-to"><?php esc_html_e( 'Recipient address', 'bcs-smtp-manager' ); ?></label>
    1542                                                                         <input type="email" id="bcs-smtp-test-to" class="bcs-smtp-input" value="<?php echo esc_attr( get_option( 'admin_email' ) ); ?>" placeholder="recipient@example.com">
    1543                                                                 </div>
    1544 
    1545                                                                 <div style="margin-top:16px;display:flex;align-items:center;gap:12px;flex-wrap:wrap">
    1546                                                                         <button type="button" id="bcs-smtp-send-test-btn" class="bcs-smtp-btn bcs-smtp-btn-primary">
    1547                                                                                 <span class="dashicons dashicons-email-alt"></span><?php esc_html_e( 'Send Test Email', 'bcs-smtp-manager' ); ?>
    1548                                                                         </button>
    1549                                                                         <?php if ( $monitor_on ) : ?>
    1550                                                                         <button type="button" id="bcs-smtp-check-now-btn" class="bcs-smtp-btn bcs-smtp-btn-secondary">
    1551                                                                                 <span class="dashicons dashicons-update"></span><?php esc_html_e( 'Run Health Check Now', 'bcs-smtp-manager' ); ?>
    1552                                                                         </button>
    1553                                                                         <?php endif; ?>
    1554                                                                 </div>
    1555                                                                 <div id="bcs-smtp-test-msg" role="status" aria-live="polite" style="display:none;margin-top:12px"></div>
    1556                                                         </div>
    1557                                                 </div>
    1558                                         </div>
    1559 
    1560                                         <div>
    1561                                                 <div class="bcs-smtp-card">
    1562                                                         <div class="bcs-smtp-card-header">
    1563                                                                 <p class="bcs-smtp-card-title"><span class="dashicons dashicons-info-outline"></span><?php esc_html_e( 'What this tests', 'bcs-smtp-manager' ); ?></p>
    1564                                                                 <p class="bcs-smtp-card-desc"><?php esc_html_e( 'A real email is sent via', 'bcs-smtp-manager' ); ?> <code>wp_mail()</code> <?php esc_html_e( 'through your configured SMTP. The result is logged to the Email Log tab.', 'bcs-smtp-manager' ); ?></p>
    1565                                                         </div>
    1566                                                         <div class="bcs-smtp-card-body">
    1567                                                                 <div class="bcs-smtp-alert bcs-smtp-alert-info" style="margin-bottom:0">
    1568                                                                         <span class="dashicons dashicons-info-outline"></span>
    1569                                                                         <div class="bcs-smtp-alert-content" style="font-size:11.5px">
    1570                                                                                 <strong><?php esc_html_e( 'Failure types detected:', 'bcs-smtp-manager' ); ?></strong>
    1571                                                                                 <ul style="margin:6px 0 0 14px;padding:0">
    1572                                                                                         <li><strong><?php esc_html_e( 'Authentication Failure', 'bcs-smtp-manager' ); ?></strong> — <?php esc_html_e( 'Bad credentials / OAuth token', 'bcs-smtp-manager' ); ?></li>
    1573                                                                                         <li><strong><?php esc_html_e( 'SMTP Rejection', 'bcs-smtp-manager' ); ?></strong> — <?php esc_html_e( 'Server rejects the message', 'bcs-smtp-manager' ); ?></li>
    1574                                                                                         <li><strong><?php esc_html_e( 'Plugin / System Failure', 'bcs-smtp-manager' ); ?></strong> — <?php esc_html_e( 'Timeout, DNS, PHP error', 'bcs-smtp-manager' ); ?></li>
    1575                                                                                 </ul>
    1576                                                                         </div>
    1577                                                                 </div>
    1578                                                         </div>
    1579                                                 </div>
    1580                                         </div>
    1581                                 </div>
    1582                         </div>
    1583 
    1584                         <!-- ═══════════════════════════════════════════════════════════════
    1585                              PANEL: Alerts
    1586                              ═══════════════════════════════════════════════════════════════ -->
    1587                         <div id="bcs-smtp-panel-alerts" class="bcs-smtp-tab-panel">
    1588                                 <div class="bcs-smtp-grid">
    1589                                         <div>
    1590                                                 <!-- Section 1: Enable Automated Monitoring -->
    1591                                                 <div class="bcs-smtp-card">
    1592                                                 <div class="bcs-smtp-card-body">
    1593                                                         <div class="bcs-smtp-toggle-row" style="margin-bottom:0">
    1594                                                                 <div class="bcs-smtp-toggle-row-info">
    1595                                                                         <div class="bcs-smtp-toggle-row-label"><?php esc_html_e( 'Enable Automated Monitoring', 'bcs-smtp-manager' ); ?></div>
    1596                                                                         <div class="bcs-smtp-toggle-row-desc"><?php esc_html_e( 'Tests SMTP connectivity on a schedule and alerts on failure', 'bcs-smtp-manager' ); ?></div>
    1597                                                                 </div>
    1598                                                                 <label class="bcs-smtp-toggle">
    1599                                                                         <input type="checkbox" id="bcs-smtp-monitor-enabled" <?php checked( $monitor_on ); ?>>
    1600                                                                         <span class="bcs-smtp-toggle-slider"></span>
    1601                                                                 </label>
    1602                                                         </div>
    1603 
    1604                                                         <div id="bcs-smtp-monitor-accordion" class="bcs-smtp-accordion <?php echo $monitor_on ? 'open' : ''; ?>" style="margin-top:12px">
    1605                                                                 <button type="button" class="bcs-smtp-accordion-head">
    1606                                                                         <div class="bcs-smtp-accordion-head-left">
    1607                                                                                 <div class="bcs-smtp-accordion-head-icon" style="background:rgba(99,102,241,.15);color:var(--bcs-smtp-indigo)">
    1608                                                                                         <span class="dashicons dashicons-clock"></span>
    1609                                                                                 </div>
    1610                                                                                 <div>
    1611                                                                                         <div class="bcs-smtp-accordion-head-title"><?php esc_html_e( 'Monitor Settings', 'bcs-smtp-manager' ); ?></div>
    1612                                                                                         <div class="bcs-smtp-accordion-head-desc"><?php esc_html_e( 'Check interval & failure threshold', 'bcs-smtp-manager' ); ?></div>
    1613                                                                                 </div>
    1614                                                                         </div>
    1615                                                                         <span class="dashicons dashicons-arrow-down-alt2 bcs-smtp-accordion-chevron"></span>
    1616                                                                 </button>
    1617                                                                 <div class="bcs-smtp-accordion-body">
    1618                                                                         <div class="bcs-smtp-field">
    1619                                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-check-interval"><?php esc_html_e( 'Check every', 'bcs-smtp-manager' ); ?> <abbr title="<?php esc_attr_e( 'Minutes between automated SMTP tests', 'bcs-smtp-manager' ); ?>">N</abbr> <?php esc_html_e( 'minutes', 'bcs-smtp-manager' ); ?></label>
    1620                                                                                 <input type="number" id="bcs-smtp-check-interval" class="bcs-smtp-input" value="<?php echo esc_attr( $check_interval ); ?>" min="1" max="1440" style="max-width:100px">
    1621                                                                                 <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block"><?php esc_html_e( 'Default: 5 min. Changing this reschedules the cron event immediately.', 'bcs-smtp-manager' ); ?></small>
    1622                                                                         </div>
    1623                                                                         <div class="bcs-smtp-field">
    1624                                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-failure-threshold"><?php esc_html_e( 'Alert after', 'bcs-smtp-manager' ); ?> <abbr title="<?php esc_attr_e( 'Consecutive failures before alerting', 'bcs-smtp-manager' ); ?>">N</abbr> <?php esc_html_e( 'consecutive failures', 'bcs-smtp-manager' ); ?></label>
    1625                                                                                 <input type="number" id="bcs-smtp-failure-threshold" class="bcs-smtp-input" value="<?php echo esc_attr( $threshold ); ?>" min="1" max="10" style="max-width:100px">
    1626                                                                         </div>
    1627                                                                         <div class="bcs-smtp-field">
    1628                                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-max-alert-count"><?php esc_html_e( 'Stop after', 'bcs-smtp-manager' ); ?> <abbr title="<?php esc_attr_e( 'Maximum notifications per failure cycle. 0 = unlimited.', 'bcs-smtp-manager' ); ?>">N</abbr> <?php esc_html_e( 'notifications', 'bcs-smtp-manager' ); ?></label>
    1629                                                                                 <input type="number" id="bcs-smtp-max-alert-count" class="bcs-smtp-input" value="<?php echo esc_attr( (int) get_option( BCS_SMTP_OPT_MAX_ALERT_COUNT, 0 ) ); ?>" min="0" max="100" style="max-width:100px">
    1630                                                                                 <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block"><?php esc_html_e( '0 = unlimited — resets automatically when SMTP recovers', 'bcs-smtp-manager' ); ?></small>
    1631                                                                         </div>
    1632                                                                 </div>
    1633                                                         </div>
    1634 
    1635                                                         <div style="margin-top:14px">
    1636                                                                 <button type="button" id="bcs-smtp-save-monitor-btn" class="bcs-smtp-btn bcs-smtp-btn-primary">
    1637                                                                         <span class="dashicons dashicons-saved"></span><?php esc_html_e( 'Save Monitor Settings', 'bcs-smtp-manager' ); ?>
    1638                                                                 </button>
    1639                                                                 <div id="bcs-smtp-monitor-msg" role="status" aria-live="polite" style="display:none;margin-top:10px"></div>
    1640                                                         </div>
    1641                                                 </div>
    1642                                                 </div>
    1643 
    1644                                                 <!-- Section 2: Alert Channel -->
    1645                                                 <div class="bcs-smtp-card" style="margin-top:0">
    1646                                                 <div class="bcs-smtp-card-body">
    1647                                                         <div class="bcs-smtp-toggle-row" style="margin-bottom:0">
    1648                                                                 <div class="bcs-smtp-toggle-row-info">
    1649                                                                         <div class="bcs-smtp-toggle-row-label"><?php esc_html_e( 'Alert Channel', 'bcs-smtp-manager' ); ?></div>
    1650                                                                         <div class="bcs-smtp-toggle-row-desc"><?php esc_html_e( 'Send failure alerts via an independent notification channel', 'bcs-smtp-manager' ); ?></div>
    1651                                                                 </div>
    1652                                                                 <label class="bcs-smtp-toggle">
    1653                                                                         <input type="checkbox" id="bcs-smtp-alert-channel-enabled" <?php checked( $alert_enabled ); ?>>
    1654                                                                         <span class="bcs-smtp-toggle-slider"></span>
    1655                                                                 </label>
    1656                                                         </div>
    1657 
    1658                                                         <div id="bcs-smtp-alert-channel-accordion" class="bcs-smtp-accordion <?php echo $alert_enabled ? 'open' : ''; ?>" style="margin-top:12px">
    1659                                                                 <button type="button" class="bcs-smtp-accordion-head">
    1660                                                                         <div class="bcs-smtp-accordion-head-left">
    1661                                                                                 <div class="bcs-smtp-accordion-head-icon" style="background:rgba(239,68,68,.12);color:var(--bcs-smtp-rose)">
    1662                                                                                         <span class="dashicons dashicons-megaphone"></span>
    1663                                                                                 </div>
    1664                                                                                 <div>
    1665                                                                                         <div class="bcs-smtp-accordion-head-title"><?php esc_html_e( 'Channel Settings', 'bcs-smtp-manager' ); ?></div>
    1666                                                                                         <div class="bcs-smtp-accordion-head-desc"><?php esc_html_e( 'Slack or Microsoft Teams webhook', 'bcs-smtp-manager' ); ?></div>
    1667                                                                                 </div>
    1668                                                                         </div>
    1669                                                                         <span class="dashicons dashicons-arrow-down-alt2 bcs-smtp-accordion-chevron"></span>
    1670                                                                 </button>
    1671                                                                 <div class="bcs-smtp-accordion-body">
    1672                                                                         <div class="bcs-smtp-field">
    1673                                                                                 <label class="bcs-smtp-field-label"><?php esc_html_e( 'Alert delivery method', 'bcs-smtp-manager' ); ?></label>
    1674                                                                                 <div class="bcs-smtp-check-list" style="flex-direction:row;flex-wrap:wrap;gap:8px">
    1675                                                                                         <?php
    1676                                                                                         $channels = array(
    1677                                                                                                 'slack'       => array( 'icon' => '💬', 'label' => __( 'Slack', 'bcs-smtp-manager' ) ),
    1678                                                                                                 'teams'       => array( 'icon' => '🟦', 'label' => __( 'Teams', 'bcs-smtp-manager' ) ),
    1679                                                                                                 'google_chat' => array( 'icon' => '🟢', 'label' => __( 'Google Chat', 'bcs-smtp-manager' ) ),
    1680                                                                                                 'discord'     => array( 'icon' => '🟣', 'label' => __( 'Discord', 'bcs-smtp-manager' ) ),
    1681                                                                                                 'custom'      => array( 'icon' => '🔗', 'label' => __( 'Custom Webhook', 'bcs-smtp-manager' ) ),
    1682                                                                                         );
    1683                                                                                         foreach ( $channels as $val => $ch ) : ?>
    1684                                                                                         <label class="bcs-smtp-check-item" style="flex-direction:row;align-items:center;gap:8px;padding:10px 16px;min-width:90px">
    1685                                                                                                 <input type="radio" name="bcs_alert_channel" class="bcs-smtp-channel-radio" value="<?php echo esc_attr( $val ); ?>" <?php checked( $alert_channel, $val ); ?> style="width:20px;height:20px;flex-shrink:0">
    1686                                                                                                 <span style="font-size:16px"><?php echo esc_html( $ch['icon'] ); ?></span>
    1687                                                                                                 <div class="bcs-smtp-check-label-title"><?php echo esc_html( $ch['label'] ); ?></div>
    1688                                                                                         </label>
    1689                                                                                         <?php endforeach; ?>
    1690                                                                                 </div>
    1691                                                                         </div>
    1692 
    1693                                                                         <div id="bcs-smtp-slack-webhook-row" class="bcs-smtp-field" style="display:none">
    1694                                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-slack-webhook-url"><?php esc_html_e( 'Incoming Webhook URL', 'bcs-smtp-manager' ); ?></label>
    1695                                                                                 <input type="url" id="bcs-smtp-slack-webhook-url" class="bcs-smtp-input"
    1696                                                                                         value="<?php echo esc_attr( get_option( BCS_SMTP_OPT_SLACK_WEBHOOK_URL, '' ) ); ?>"
    1697                                                                                         placeholder="https://hooks.slack.com/services/…">
    1698                                                                                 <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block">
    1699                                                                                         <em><?php esc_html_e( 'Apps → Incoming Webhooks', 'bcs-smtp-manager' ); ?></em> —
    1700                                                                                         <a href="https://api.slack.com/messaging/webhooks" target="_blank" rel="noopener"><?php esc_html_e( 'Setup guide ↗', 'bcs-smtp-manager' ); ?></a>
    1701                                                                                 </small>
    1702                                                                         </div>
    1703                                                                         <div id="bcs-smtp-teams-webhook-row" class="bcs-smtp-field" style="display:none">
    1704                                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-teams-webhook-url"><?php esc_html_e( 'Incoming Webhook URL', 'bcs-smtp-manager' ); ?></label>
    1705                                                                                 <input type="url" id="bcs-smtp-teams-webhook-url" class="bcs-smtp-input"
    1706                                                                                         value="<?php echo esc_attr( get_option( BCS_SMTP_OPT_TEAMS_WEBHOOK_URL, '' ) ); ?>"
    1707                                                                                         placeholder="https://outlook.office.com/webhook/…">
    1708                                                                                 <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block">
    1709                                                                                         <em><?php esc_html_e( 'Channel → Connectors → Incoming Webhook', 'bcs-smtp-manager' ); ?></em> —
    1710                                                                                         <a href="https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook" target="_blank" rel="noopener"><?php esc_html_e( 'Setup guide ↗', 'bcs-smtp-manager' ); ?></a>
    1711                                                                                 </small>
    1712                                                                         </div>
    1713                                                                         <div id="bcs-smtp-google-chat-webhook-row" class="bcs-smtp-field" style="display:none">
    1714                                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-google-chat-webhook-url"><?php esc_html_e( 'Incoming Webhook URL', 'bcs-smtp-manager' ); ?></label>
    1715                                                                                 <input type="url" id="bcs-smtp-google-chat-webhook-url" class="bcs-smtp-input"
    1716                                                                                         value="<?php echo esc_attr( get_option( BCS_SMTP_OPT_GOOGLE_CHAT_WEBHOOK_URL, '' ) ); ?>"
    1717                                                                                         placeholder="https://chat.googleapis.com/v1/spaces/…/messages?key=…&token=…">
    1718                                                                                 <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block">
    1719                                                                                         <em><?php esc_html_e( 'Space → Apps & integrations → Webhooks', 'bcs-smtp-manager' ); ?></em> —
    1720                                                                                         <a href="https://developers.google.com/workspace/chat/quickstart/webhooks" target="_blank" rel="noopener"><?php esc_html_e( 'Setup guide ↗', 'bcs-smtp-manager' ); ?></a>
    1721                                                                                 </small>
    1722                                                                         </div>
    1723                                                                         <div id="bcs-smtp-discord-webhook-row" class="bcs-smtp-field" style="display:none">
    1724                                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-discord-webhook-url"><?php esc_html_e( 'Incoming Webhook URL', 'bcs-smtp-manager' ); ?></label>
    1725                                                                                 <input type="url" id="bcs-smtp-discord-webhook-url" class="bcs-smtp-input"
    1726                                                                                         value="<?php echo esc_attr( get_option( BCS_SMTP_OPT_DISCORD_WEBHOOK_URL, '' ) ); ?>"
    1727                                                                                         placeholder="https://discord.com/api/webhooks/…/…">
    1728                                                                                 <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block">
    1729                                                                                         <em><?php esc_html_e( 'Server Settings → Integrations → Webhooks', 'bcs-smtp-manager' ); ?></em> —
    1730                                                                                         <a href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks" target="_blank" rel="noopener"><?php esc_html_e( 'Setup guide ↗', 'bcs-smtp-manager' ); ?></a>
    1731                                                                                 </small>
    1732                                                                         </div>
    1733                                                                         <div id="bcs-smtp-custom-webhook-row" class="bcs-smtp-field" style="display:none">
    1734                                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-custom-webhook-url"><?php esc_html_e( 'Webhook URL', 'bcs-smtp-manager' ); ?></label>
    1735                                                                                 <input type="url" id="bcs-smtp-custom-webhook-url" class="bcs-smtp-input"
    1736                                                                                         value="<?php echo esc_attr( get_option( BCS_SMTP_OPT_CUSTOM_WEBHOOK_URL, '' ) ); ?>"
    1737                                                                                         placeholder="https://example.com/your-webhook-endpoint">
    1738                                                                                 <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block">
    1739                                                                                         <?php esc_html_e( 'Sends a structured JSON payload (event, site, error details, failure count) — use this with Zapier, Make, n8n, or your own endpoint.', 'bcs-smtp-manager' ); ?>
    1740                                                                                 </small>
    1741                                                                         </div>
    1742                                                                 </div>
    1743                                                         </div>
    1744 
    1745                                                         <div style="margin-top:14px;display:flex;gap:10px;flex-wrap:wrap;align-items:center">
    1746                                                                 <button type="button" id="bcs-smtp-save-alerts-btn" class="bcs-smtp-btn bcs-smtp-btn-primary">
    1747                                                                         <span class="dashicons dashicons-saved"></span><?php esc_html_e( 'Save Alert Settings', 'bcs-smtp-manager' ); ?>
    1748                                                                 </button>
    1749                                                                 <button type="button" id="bcs-smtp-test-alert-btn" class="bcs-smtp-btn bcs-smtp-btn-secondary">
    1750                                                                         <span class="dashicons dashicons-megaphone"></span><?php esc_html_e( 'Send Test Alert', 'bcs-smtp-manager' ); ?>
    1751                                                                 </button>
    1752                                                         </div>
    1753                                                         <div id="bcs-smtp-alerts-msg" role="status" aria-live="polite" style="display:none;margin-top:10px"></div>
    1754                                                 </div>
    1755                                                 </div>
    1756                                         </div>
    1757 
    1758                                         <div>
    1759                                                 <div class="bcs-smtp-card">
    1760                                                         <div class="bcs-smtp-card-header">
    1761                                                                 <p class="bcs-smtp-card-title"><span class="dashicons dashicons-info-outline"></span><?php esc_html_e( 'How monitoring works', 'bcs-smtp-manager' ); ?></p>
    1762                                                         </div>
    1763                                                         <div class="bcs-smtp-card-body">
    1764                                                                 <div class="bcs-smtp-field-group" style="gap:14px">
    1765                                                                         <div class="bcs-smtp-server-row" style="border-radius:var(--bcs-smtp-radius);padding:12px 14px">
    1766                                                                                 <span class="bcs-smtp-server-key" style="font-size:12px;font-weight:600;color:var(--bcs-smtp-indigo-h)">⏱ <?php esc_html_e( 'TCP-only test', 'bcs-smtp-manager' ); ?></span>
    1767                                                                                 <span class="bcs-smtp-server-val" style="font-size:12px;color:var(--bcs-smtp-text-2);font-family:var(--bcs-smtp-font);margin-top:4px"><?php esc_html_e( 'WP-Cron connects to your SMTP server via TCP — no email is sent on successful checks.', 'bcs-smtp-manager' ); ?></span>
    1768                                                                         </div>
    1769                                                                         <div class="bcs-smtp-server-row" style="border-radius:var(--bcs-smtp-radius);padding:12px 14px">
    1770                                                                                 <span class="bcs-smtp-server-key" style="font-size:12px;font-weight:600;color:var(--bcs-smtp-indigo-h)">🔔 <?php esc_html_e( 'Failure-only alerts', 'bcs-smtp-manager' ); ?></span>
    1771                                                                                 <span class="bcs-smtp-server-val" style="font-size:12px;color:var(--bcs-smtp-text-2);font-family:var(--bcs-smtp-font);margin-top:4px"><?php esc_html_e( 'Alerts only fire when consecutive failures hit your threshold. No noise on healthy checks.', 'bcs-smtp-manager' ); ?></span>
    1772                                                                         </div>
    1773                                                                         <div class="bcs-smtp-server-row" style="border-radius:var(--bcs-smtp-radius);padding:12px 14px">
    1774                                                                                 <span class="bcs-smtp-server-key" style="font-size:12px;font-weight:600;color:var(--bcs-smtp-indigo-h)">🔁 <?php esc_html_e( 'Auto-reset', 'bcs-smtp-manager' ); ?></span>
    1775                                                                                 <span class="bcs-smtp-server-val" style="font-size:12px;color:var(--bcs-smtp-text-2);font-family:var(--bcs-smtp-font);margin-top:4px"><?php esc_html_e( 'When delivery recovers, the failure counter resets automatically.', 'bcs-smtp-manager' ); ?></span>
    1776                                                                         </div>
    1777                                                                 </div>
    1778                                                                 <div class="bcs-smtp-alert bcs-smtp-alert-info" style="margin-top:14px;margin-bottom:0">
    1779                                                                         <span class="dashicons dashicons-info-outline"></span>
    1780                                                                         <div class="bcs-smtp-alert-content" style="font-size:11.5px"><?php esc_html_e( 'WP-Cron fires on page load. For accurate intervals, point a real server', 'bcs-smtp-manager' ); ?> <code>crontab</code> <?php esc_html_e( 'at', 'bcs-smtp-manager' ); ?> <code>wp-cron.php</code>.</div>
    1781                                                                 </div>
    1782                                                         </div>
    1783                                                 </div>
    1784                                                 <div class="bcs-smtp-card">
    1785                                                         <div class="bcs-smtp-card-header">
    1786                                                                 <p class="bcs-smtp-card-title"><span class="dashicons dashicons-shield"></span><?php esc_html_e( 'Alert message includes', 'bcs-smtp-manager' ); ?></p>
    1787                                                         </div>
    1788                                                         <div class="bcs-smtp-card-body">
    1789                                                                 <div class="bcs-smtp-check-list" style="gap:6px">
    1790                                                                         <?php
    1791                                                                         $fields = array(
    1792                                                                                 __( 'Date and time of failure', 'bcs-smtp-manager' ),
    1793                                                                                 __( 'Sending mailbox (from address)', 'bcs-smtp-manager' ),
    1794                                                                                 __( 'SMTP host & port', 'bcs-smtp-manager' ),
    1795                                                                                 __( 'SMTP plugin detected', 'bcs-smtp-manager' ),
    1796                                                                                 __( 'Raw error message', 'bcs-smtp-manager' ),
    1797                                                                                 __( 'Source IP address', 'bcs-smtp-manager' ),
    1798                                                                                 __( 'Failure classification', 'bcs-smtp-manager' ),
    1799                                                                                 __( 'Consecutive failure count', 'bcs-smtp-manager' ),
    1800                                                                                 __( 'External provider status note', 'bcs-smtp-manager' ),
    1801                                                                         );
    1802                                                                         foreach ( $fields as $f ) : ?>
    1803                                                                         <div style="display:flex;align-items:center;gap:8px;font-size:12px;color:var(--bcs-smtp-text-2);padding:4px 0">
    1804                                                                                 <span style="color:var(--bcs-smtp-emerald);font-size:14px">✓</span><?php echo wp_kses_post( $f ); ?>
    1805                                                                         </div>
    1806                                                                         <?php endforeach; ?>
    1807                                                                 </div>
    1808                                                         </div>
    1809                                                 </div>
    1810                                         </div>
    1811                                 </div>
    1812                         </div>
    1813 
    1814                         <!-- ═══════════════════════════════════════════════════════════════
    1815                              PANEL: Config
    1816                              ═══════════════════════════════════════════════════════════════ -->
    1817                         <div id="bcs-smtp-panel-config" class="bcs-smtp-tab-panel">
    1818                                 <div class="bcs-smtp-grid">
    1819                                         <div>
    1820                                                 <div class="bcs-smtp-card">
    1821                                                         <div class="bcs-smtp-card-header">
    1822                                                                 <p class="bcs-smtp-card-title"><span class="dashicons dashicons-admin-generic"></span><?php esc_html_e( 'Detected SMTP Configuration', 'bcs-smtp-manager' ); ?></p>
    1823                                                                 <p class="bcs-smtp-card-desc"><?php esc_html_e( 'Auto-detected from your active SMTP configuration. Credentials are never stored by this plugin beyond what you enter in Settings.', 'bcs-smtp-manager' ); ?></p>
    1824                                                         </div>
    1825                                                         <div class="bcs-smtp-card-body">
    1826                                                                 <div class="bcs-smtp-server-grid">
    1827                                                                         <div class="bcs-smtp-server-row">
    1828                                                                                 <span class="bcs-smtp-server-key"><?php esc_html_e( 'Plugin', 'bcs-smtp-manager' ); ?></span>
    1829                                                                                 <span class="bcs-smtp-server-val"><?php echo esc_html( $info['plugin'] ); ?></span>
    1830                                                                         </div>
    1831                                                                         <div class="bcs-smtp-server-row">
    1832                                                                                 <span class="bcs-smtp-server-key"><?php esc_html_e( 'Version', 'bcs-smtp-manager' ); ?></span>
    1833                                                                                 <span class="bcs-smtp-server-val"><?php echo esc_html( $info['version'] ); ?></span>
    1834                                                                         </div>
    1835                                                                         <div class="bcs-smtp-server-row">
    1836                                                                                 <span class="bcs-smtp-server-key"><?php esc_html_e( 'SMTP Host', 'bcs-smtp-manager' ); ?></span>
    1837                                                                                 <span class="bcs-smtp-server-val bcs-smtp-mono"><?php echo esc_html( $info['host'] ?: '—' ); ?></span>
    1838                                                                         </div>
    1839                                                                         <div class="bcs-smtp-server-row">
    1840                                                                                 <span class="bcs-smtp-server-key"><?php esc_html_e( 'Port', 'bcs-smtp-manager' ); ?></span>
    1841                                                                                 <span class="bcs-smtp-server-val bcs-smtp-mono"><?php echo esc_html( $info['port'] ?: '—' ); ?></span>
    1842                                                                         </div>
    1843                                                                         <div class="bcs-smtp-server-row">
    1844                                                                                 <span class="bcs-smtp-server-key"><?php esc_html_e( 'Encryption', 'bcs-smtp-manager' ); ?></span>
    1845                                                                                 <span class="bcs-smtp-server-val"><?php echo esc_html( strtoupper( $info['encryption'] ?: '—' ) ); ?></span>
    1846                                                                         </div>
    1847                                                                         <div class="bcs-smtp-server-row">
    1848                                                                                 <span class="bcs-smtp-server-key"><?php esc_html_e( 'From mailbox', 'bcs-smtp-manager' ); ?></span>
    1849                                                                                 <span class="bcs-smtp-server-val bcs-smtp-mono" style="font-size:11px;word-break:break-all"><?php echo esc_html( $info['mailbox'] ); ?></span>
    1850                                                                         </div>
    1851                                                                         <div class="bcs-smtp-server-row">
    1852                                                                                 <span class="bcs-smtp-server-key"><?php esc_html_e( 'Auth method', 'bcs-smtp-manager' ); ?></span>
    1853                                                                                 <span class="bcs-smtp-server-val"><?php echo esc_html( $info['auth_type'] ); ?></span>
    1854                                                                         </div>
    1855                                                                         <div class="bcs-smtp-server-row">
    1856                                                                                 <span class="bcs-smtp-server-key"><?php esc_html_e( 'WordPress TZ', 'bcs-smtp-manager' ); ?></span>
    1857                                                                                 <span class="bcs-smtp-server-val"><?php echo esc_html( wp_timezone_string() ); ?></span>
    1858                                                                         </div>
    1859                                                                 </div>
    1860                                                         </div>
    1861                                                 </div>
    1862                                         </div>
    1863 
    1864                                         <div>
    1865                                                 <div class="bcs-smtp-card">
    1866                                                         <div class="bcs-smtp-card-header">
    1867                                                                 <p class="bcs-smtp-card-title"><span class="dashicons dashicons-shield"></span><?php esc_html_e( 'What is tracked', 'bcs-smtp-manager' ); ?></p>
    1868                                                         </div>
    1869                                                         <div class="bcs-smtp-card-body">
    1870                                                                 <div class="bcs-smtp-field-group" style="gap:6px">
    1871                                                                         <?php
    1872                                                                         $tracked = array(
    1873                                                                                 sprintf(
    1874                                                                                         /* translators: %s: wp_mail() function name wrapped in <code> tags. */
    1875                                                                                         __( 'All %s sends (subject, from, to, status)', 'bcs-smtp-manager' ),
    1876                                                                                         '<code>wp_mail()</code>'
    1877                                                                                 ),
    1878                                                                                 __( 'SMTP plugin detected', 'bcs-smtp-manager' ),
    1879                                                                                 __( 'SMTP hostname & port', 'bcs-smtp-manager' ),
    1880                                                                                 __( 'Encryption method (TLS / SSL)', 'bcs-smtp-manager' ),
    1881                                                                                 __( 'From mailbox (sender address)', 'bcs-smtp-manager' ),
    1882                                                                                 __( 'Authentication type', 'bcs-smtp-manager' ),
    1883                                                                         );
    1884                                                                         foreach ( $tracked as $t ) : ?>
    1885                                                                         <div style="display:flex;align-items:center;gap:8px;font-size:12px;color:var(--bcs-smtp-text-2);padding:4px 0">
    1886                                                                                 <span style="color:var(--bcs-smtp-indigo-h)">◈</span><?php echo wp_kses_post( $t ); ?>
    1887                                                                         </div>
    1888                                                                         <?php endforeach; ?>
    1889                                                                 </div>
    1890                                                                 <div class="bcs-smtp-alert bcs-smtp-alert-warning" style="margin-top:14px;margin-bottom:0">
    1891                                                                         <span class="dashicons dashicons-warning"></span>
    1892                                                                         <div class="bcs-smtp-alert-content" style="font-size:11.5px"><?php esc_html_e( 'Email body content is', 'bcs-smtp-manager' ); ?> <strong><?php esc_html_e( 'never', 'bcs-smtp-manager' ); ?></strong> <?php esc_html_e( 'stored by default — only metadata. The optional Resend feature below is the sole exception.', 'bcs-smtp-manager' ); ?></div>
    1893                                                                 </div>
    1894                                                         </div>
    1895                                                 </div>
    1896 
    1897                                                 <div class="bcs-smtp-card">
    1898                                                         <div class="bcs-smtp-card-header">
    1899                                                                 <p class="bcs-smtp-card-title"><span class="dashicons dashicons-controls-repeat"></span><?php esc_html_e( 'Failed Email Resend', 'bcs-smtp-manager' ); ?></p>
    1900                                                                 <p class="bcs-smtp-card-desc"><?php esc_html_e( 'When enabled, the content of an email is stored only if it fails to send, so you can resend it from the Email Log. Off by default.', 'bcs-smtp-manager' ); ?></p>
    1901                                                         </div>
    1902                                                         <div class="bcs-smtp-card-body">
    1903                                                                 <div class="bcs-smtp-toggle-row" style="margin-bottom:14px">
    1904                                                                         <div class="bcs-smtp-toggle-row-info">
    1905                                                                                 <div class="bcs-smtp-toggle-row-label"><?php esc_html_e( 'Store failed email content', 'bcs-smtp-manager' ); ?></div>
    1906                                                                                 <div class="bcs-smtp-toggle-row-desc"><?php esc_html_e( 'Required for the Resend button to work — has no effect on emails that send successfully', 'bcs-smtp-manager' ); ?></div>
    1907                                                                         </div>
    1908                                                                         <label class="bcs-smtp-toggle">
    1909                                                                                 <input type="checkbox" id="bcs-smtp-store-failed-body" <?php checked( $store_failed_body ); ?>>
    1910                                                                                 <span class="bcs-smtp-toggle-slider"></span>
    1911                                                                         </label>
    1912                                                                 </div>
    1913                                                                 <div class="bcs-smtp-field">
    1914                                                                         <label class="bcs-smtp-field-label" for="bcs-smtp-failed-body-retention"><?php esc_html_e( 'Retention (days)', 'bcs-smtp-manager' ); ?></label>
    1915                                                                         <input type="number" id="bcs-smtp-failed-body-retention" class="bcs-smtp-input" style="max-width:120px" value="<?php echo esc_attr( $failed_body_retention ); ?>" min="1" max="30">
    1916                                                                         <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block"><?php esc_html_e( 'Stored content is auto-purged after this many days, independent of the overall log retention setting.', 'bcs-smtp-manager' ); ?></small>
    1917                                                                 </div>
    1918                                                                 <div style="margin-top:14px">
    1919                                                                         <button type="button" id="bcs-smtp-save-resend-btn" class="bcs-smtp-btn bcs-smtp-btn-primary bcs-smtp-btn-sm">
    1920                                                                                 <span class="dashicons dashicons-saved"></span><?php esc_html_e( 'Save', 'bcs-smtp-manager' ); ?>
    1921                                                                         </button>
    1922                                                                         <div id="bcs-smtp-resend-settings-msg" role="status" aria-live="polite" style="display:none;margin-top:10px;width:100%"></div>
    1923                                                                 </div>
    1924                                                         </div>
    1925                                                 </div>
    1926 
    1927                                                 <div class="bcs-smtp-card">
    1928                                                         <div class="bcs-smtp-card-header">
    1929                                                                 <p class="bcs-smtp-card-title"><span class="dashicons dashicons-admin-links"></span><?php esc_html_e( 'Quick links', 'bcs-smtp-manager' ); ?></p>
    1930                                                         </div>
    1931                                                         <div class="bcs-smtp-card-body">
    1932                                                                 <div class="bcs-smtp-field-group">
    1933                                                                         <a href="https://admin.microsoft.com/" target="_blank" rel="noopener" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm bcs-smtp-btn-full">
    1934                                                                                 <span class="dashicons dashicons-external"></span> <?php esc_html_e( 'Microsoft 365 Admin', 'bcs-smtp-manager' ); ?>
    1935                                                                         </a>
    1936                                                                         <a href="https://status.office365.com/" target="_blank" rel="noopener" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm bcs-smtp-btn-full">
    1937                                                                                 <span class="dashicons dashicons-external"></span> <?php esc_html_e( 'Microsoft 365 Status', 'bcs-smtp-manager' ); ?>
    1938                                                                         </a>
    1939                                                                         <a href="https://api.slack.com/messaging/webhooks" target="_blank" rel="noopener" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm bcs-smtp-btn-full">
    1940                                                                                 <span class="dashicons dashicons-external"></span> <?php esc_html_e( 'Slack Webhook Guide', 'bcs-smtp-manager' ); ?>
    1941                                                                         </a>
    1942                                                                         <a href="https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook" target="_blank" rel="noopener" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm bcs-smtp-btn-full">
    1943                                                                                 <span class="dashicons dashicons-external"></span> <?php esc_html_e( 'Teams Webhook Guide', 'bcs-smtp-manager' ); ?>
    1944                                                                         </a>
    1945                                                                 </div>
    1946                                                         </div>
    1947                                                 </div>
    1948                                         </div>
    1949                                 </div>
    1950                         </div>
     2525                        </section>
    19512526
    19522527                </div><!-- /.bcs-smtp-wrap -->
  • bcs-smtp-manager/tags/1.3.0/assets/css/admin.css

    r3652946 r3762759  
    11/* =====================================================================
    2    CSS Variables - Dark Theme (default)
    3    ===================================================================== */
    4 :root,
    5 [data-bcs-smtp-theme="dark"] {
    6   --bcs-smtp-bg:#0f1117;--bcs-smtp-surface:#181c27;--bcs-smtp-surface-2:#1e2333;--bcs-smtp-surface-3:#252a3d;
    7   --bcs-smtp-border:rgba(255,255,255,.07);--bcs-smtp-border-light:rgba(255,255,255,.12);
    8   --bcs-smtp-indigo:#6366f1;--bcs-smtp-indigo-h:#818cf8;--bcs-smtp-indigo-dim:rgba(99,102,241,.15);--bcs-smtp-indigo-glow:rgba(99,102,241,.25);
    9   --bcs-smtp-emerald:#10b981;--bcs-smtp-emerald-dim:rgba(16,185,129,.15);
    10   --bcs-smtp-amber:#f59e0b;--bcs-smtp-amber-dim:rgba(245,158,11,.12);
    11   --bcs-smtp-rose:#f43f5e;--bcs-smtp-rose-dim:rgba(244,63,94,.12);
    12   --bcs-smtp-sky:#38bdf8;
    13   --bcs-smtp-text:#e2e8f0;--bcs-smtp-text-2:#94a3b8;--bcs-smtp-text-3:#64748b;
    14   --bcs-smtp-font:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;--bcs-smtp-mono:ui-monospace,'Courier New',monospace;
    15   --bcs-smtp-radius:10px;--bcs-smtp-radius-lg:16px;--bcs-smtp-radius-sm:6px;
    16   --bcs-smtp-shadow:0 4px 24px rgba(0,0,0,.4);--bcs-smtp-t:.18s cubic-bezier(.4,0,.2,1);
    17   --bcs-smtp-admin-bg:#0f1117;
    18 }
    19 
    20 html.wp-toolbar,body.wp-admin,#wpwrap,#wpcontent,#wpbody,#wpbody-content,#wpfooter { background:var(--bcs-smtp-admin-bg)!important; }
     2   ByteCoreStack - SMTP & Health Monitor — admin UI
     3   Dark, layered surfaces; #1450c4 for the primary action and active state.
     4   ===================================================================== */
     5
     6@font-face{font-family:"BCS Outfit";src:url(../fonts/outfit-variable.woff2) format("woff2");font-weight:100 900;font-style:normal;font-display:swap}
     7@font-face{font-family:"BCS Geist Mono";src:url(../fonts/geist-mono-variable.woff2) format("woff2");font-weight:100 900;font-style:normal;font-display:swap}
     8
     9:root{
     10  --bcs-bg:#0a0e17;
     11  --bcs-panel:#111725;
     12  --bcs-raised:#161d2d;
     13  --bcs-field:#0d1320;
     14  --bcs-field-hover:#1a2234;
     15  --bcs-seam:rgba(255,255,255,.07);
     16  --bcs-seam-strong:rgba(255,255,255,.12);
     17  --bcs-ink:#eef2f9;
     18  --bcs-ink-2:#a7b2c7;
     19  --bcs-ink-3:#808ca4;
     20  --bcs-brand:#1450c4;
     21  --bcs-brand-hover:#1c5fdc;
     22  --bcs-brand-press:#0f3f9e;
     23  --bcs-brand-ink:#8db4ff;
     24  --bcs-brand-wash:rgba(20,80,196,.16);
     25  --bcs-ok:#34d399;
     26  --bcs-bad:#fb7185;
     27  --bcs-warn:#fbbf24;
     28  --bcs-off:#808ca4;
     29  /* legacy aliases used by the other tabs */
     30  --bcs-sent:var(--bcs-ok);--bcs-back:var(--bcs-bad);--bcs-held:var(--bcs-warn);
     31  --bcs-font:"BCS Outfit",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
     32  --bcs-mono:"BCS Geist Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
     33  --bcs-r:14px;
     34  --bcs-r-sm:8px;
     35  --bcs-shadow:inset 0 1px 0 rgba(255,255,255,.045),0 1px 2px rgba(0,0,0,.35),0 12px 32px -16px rgba(0,0,0,.65);
     36  --bcs-ease:cubic-bezier(.22,1,.36,1);
     37  --bcs-t:.18s var(--bcs-ease);
     38}
     39
     40/* WordPress admin chrome on this screen only (stylesheet is enqueued for this page). */
     41html.wp-toolbar,body.wp-admin,#wpwrap,#wpcontent,#wpbody,#wpbody-content{background:var(--bcs-bg)!important}
     42#wpfooter{color:var(--bcs-ink-3)}
     43#wpfooter a{color:var(--bcs-brand-ink)}
    2144
    2245.bcs-smtp-wrap,.bcs-smtp-wrap *{box-sizing:border-box}
    23 .bcs-smtp-wrap{font-family:var(--bcs-smtp-font);color:var(--bcs-smtp-text);max-width:none;width:100%;margin:0;padding:20px 20px 20px 0;overflow-x:hidden}
    24 .bcs-smtp-wrap h1,.bcs-smtp-wrap h2,.bcs-smtp-wrap h3{font-family:var(--bcs-smtp-font);color:var(--bcs-smtp-text)}
     46.bcs-smtp-wrap{
     47  font-family:var(--bcs-font);font-size:14px;line-height:1.55;color:var(--bcs-ink);
     48  max-width:1320px;margin:0;padding:28px 24px 56px 8px;
     49  font-variant-numeric:tabular-nums;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
     50  accent-color:var(--bcs-brand);color-scheme:dark;
     51}
    2552.bcs-smtp-wrap .notice{display:none!important}
    26 .bcs-smtp-mono{font-family:var(--bcs-smtp-mono)}
     53.bcs-smtp-wrap ::selection{background:rgba(141,180,255,.32);color:#fff}
     54.bcs-smtp-wrap h1,.bcs-smtp-wrap h2,.bcs-smtp-wrap h3{font-family:var(--bcs-font);color:var(--bcs-ink);margin:0;padding:0;letter-spacing:-.015em}
     55.bcs-smtp-wrap p{margin:0;font-size:14px}
     56.bcs-smtp-wrap a{color:var(--bcs-brand-ink);text-underline-offset:3px}
     57.bcs-smtp-wrap a:hover{color:#b3ccff}
     58.bcs-smtp-wrap code{font-family:var(--bcs-mono);font-size:.86em;background:var(--bcs-field);color:var(--bcs-ink);padding:1px 5px;border-radius:4px}
     59.bcs-smtp-wrap [hidden]{display:none!important}
     60.bcs-smtp-mono{font-family:var(--bcs-mono);font-size:12.5px;letter-spacing:-.01em}
     61.bcs-smtp-wrap :focus{outline:none;box-shadow:none}
     62.bcs-smtp-wrap :focus-visible{outline:2px solid var(--bcs-brand-ink);outline-offset:2px;border-radius:var(--bcs-r-sm)}
     63.bcs-smtp-wrap *{scrollbar-width:thin;scrollbar-color:var(--bcs-seam-strong) transparent}
    2764
    2865/* =====================================================================
    2966   Header
    3067   ===================================================================== */
    31 .bcs-smtp-header{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;padding:20px 24px;background:linear-gradient(135deg,var(--bcs-smtp-surface-2) 0%,#1a1f33 100%);border:1px solid var(--bcs-smtp-border);border-radius:var(--bcs-smtp-radius-lg);margin-bottom:24px}
    32 .bcs-smtp-header-left{display:flex;align-items:center;gap:14px;min-width:0}
    33 .bcs-smtp-header-right{display:flex;align-items:center;gap:10px}
    34 .bcs-smtp-header-icon{width:44px;height:44px;flex-shrink:0;background:var(--bcs-smtp-indigo-dim);border:1px solid var(--bcs-smtp-indigo);border-radius:var(--bcs-smtp-radius);display:flex;align-items:center;justify-content:center;box-shadow:0 0 20px var(--bcs-smtp-indigo-glow)}
    35 .bcs-smtp-header-icon .dashicons{color:var(--bcs-smtp-indigo);font-size:22px;width:22px;height:22px}
    36 .bcs-smtp-header h1{font-size:22px;font-weight:600;margin:0;color:var(--bcs-smtp-text);line-height:1}
    37 .bcs-smtp-header-sub{font-size:13px;color:var(--bcs-smtp-text-2);margin:3px 0 0;font-weight:400}
    38 .bcs-smtp-header-badge{font-size:11px;font-family:var(--bcs-smtp-mono);background:var(--bcs-smtp-surface-3);border:1px solid var(--bcs-smtp-border);color:var(--bcs-smtp-text-3);padding:4px 10px;border-radius:100px;white-space:nowrap}
    39 @media(max-width:600px){
    40   .bcs-smtp-header-right{width:100%;justify-content:flex-start;padding-left:58px}
    41 }
    42 
    43 /* =====================================================================
    44    Nav Tabs
    45    ===================================================================== */
    46 .bcs-smtp-tabs{display:flex;gap:4px;margin-bottom:24px;border-bottom:1px solid var(--bcs-smtp-border);overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;scrollbar-width:thin;scrollbar-color:var(--bcs-smtp-surface-3) transparent}
    47 .bcs-smtp-tabs::-webkit-scrollbar{height:6px}
    48 .bcs-smtp-tabs::-webkit-scrollbar-track{background:transparent}
    49 .bcs-smtp-tabs::-webkit-scrollbar-thumb{background:var(--bcs-smtp-surface-3);border-radius:10px}
    50 .bcs-smtp-tab{display:inline-flex;align-items:center;gap:7px;padding:13px 18px;font-size:13px;font-weight:500;color:var(--bcs-smtp-text-2);border:none;background:transparent;border-bottom:4px solid transparent;margin-bottom:-1px;cursor:pointer;transition:color var(--bcs-smtp-t),border-color var(--bcs-smtp-t);font-family:var(--bcs-smtp-font);text-decoration:none;white-space:nowrap;flex-shrink:0}
    51 .bcs-smtp-tab:hover{color:var(--bcs-smtp-text)}
    52 .bcs-smtp-tab.active{color:var(--bcs-smtp-indigo-h);font-weight:600;border-bottom:4px solid var(--bcs-smtp-indigo)}
    53 .bcs-smtp-tab .dashicons{font-size:16px;width:16px;height:16px}
     68.bcs-smtp-header{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:22px}
     69.bcs-smtp-brand{display:flex;align-items:center;gap:14px;min-width:0}
     70.bcs-smtp-brand-mark{display:grid;place-items:center;width:42px;height:42px;flex-shrink:0;border-radius:11px;background:var(--bcs-brand);box-shadow:inset 0 1px 0 rgba(255,255,255,.18),0 6px 16px -6px rgba(20,80,196,.7)}
     71.bcs-smtp-brand-mark svg{width:22px;height:22px;fill:none;stroke:#fff;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
     72.bcs-smtp-header h1{font-size:21px;font-weight:600;line-height:1.2;letter-spacing:-.02em}
     73.bcs-smtp-header-sub{font-size:13px!important;color:var(--bcs-ink-3);margin-top:2px!important}
     74.bcs-smtp-header-actions{display:flex;gap:10px}
     75
     76/* =====================================================================
     77   Tabs
     78   ===================================================================== */
     79.bcs-smtp-tabs{position:relative;display:flex;gap:4px;width:fit-content;max-width:100%;padding:5px;margin-bottom:24px;background:var(--bcs-panel);border:1px solid var(--bcs-seam);border-radius:12px;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;scrollbar-width:none}
     80.bcs-smtp-tabs::-webkit-scrollbar{display:none}
     81.bcs-smtp-tab{
     82  appearance:none;border:0;background:none;cursor:pointer;flex-shrink:0;white-space:nowrap;
     83  display:inline-flex;align-items:center;gap:8px;height:38px;padding:0 14px;margin:0;
     84  font:500 14px/1 var(--bcs-font);color:var(--bcs-ink-3);border-radius:8px;
     85  transition:color var(--bcs-t),background var(--bcs-t),box-shadow var(--bcs-t);
     86}
     87.bcs-smtp-tab .dashicons{font-size:17px;width:17px;height:17px;color:var(--bcs-ink-3);transition:color var(--bcs-t)}
     88.bcs-smtp-tab:hover{color:var(--bcs-ink);background:var(--bcs-field)}
     89.bcs-smtp-tab:hover .dashicons{color:var(--bcs-ink-2)}
     90.bcs-smtp-tab[aria-selected="true"]{color:var(--bcs-ink);background:var(--bcs-brand-wash);box-shadow:inset 0 0 0 1px rgba(77,134,240,.38)}
     91.bcs-smtp-tab[aria-selected="true"] .dashicons{color:var(--bcs-brand-ink)}
     92.bcs-smtp-tab:focus-visible{outline:2px solid var(--bcs-brand-ink);outline-offset:1px}
     93.bcs-smtp-tab{position:relative;z-index:1}
     94.bcs-smtp-tab-pill{position:absolute;top:0;left:0;width:0;height:0;border-radius:8px;background:var(--bcs-brand-wash);box-shadow:inset 0 0 0 1px rgba(77,134,240,.38);opacity:0;pointer-events:none;transition:transform .32s var(--bcs-ease),width .32s var(--bcs-ease)}
     95.bcs-smtp-tabs.has-pill .bcs-smtp-tab-pill{opacity:1}
     96.bcs-smtp-tabs.is-instant .bcs-smtp-tab-pill{transition:none}
     97.bcs-smtp-tabs.has-pill .bcs-smtp-tab[aria-selected="true"]{background:transparent;box-shadow:none}
     98.bcs-smtp-tabs.has-pill .bcs-smtp-tab:not([aria-selected="true"]):hover{background:var(--bcs-field)}
     99.bcs-smtp-tab-dot{width:7px;height:7px;border-radius:50%;background:var(--bcs-ink-3);margin-left:-2px}
     100.bcs-smtp-tab-dot[data-tone="ok"]{background:var(--bcs-ok)}
     101.bcs-smtp-tab-dot[data-tone="bad"]{background:var(--bcs-bad);box-shadow:0 0 0 3px rgba(251,113,133,.18)}
     102.bcs-smtp-tab-dot[data-tone="warn"]{background:var(--bcs-warn)}
     103.bcs-smtp-tab-dot:not([data-tone]){display:none}
     104.bcs-smtp-tab-count{min-width:20px;text-align:center;font:600 11px/1 var(--bcs-font);font-variant-numeric:tabular-nums;padding:3px 6px;border-radius:99px;background:rgba(251,113,133,.16);color:var(--bcs-bad)}
    54105.bcs-smtp-tab-panel{display:none}
    55 .bcs-smtp-tab-panel.active{display:block}
    56 
    57 /* =====================================================================
    58    Card
    59    ===================================================================== */
    60 .bcs-smtp-card{background:var(--bcs-smtp-surface);border:1px solid var(--bcs-smtp-border);border-radius:var(--bcs-smtp-radius-lg);margin-bottom:20px;position:relative;overflow:hidden}
    61 .bcs-smtp-card::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(99,102,241,.3),transparent)}
    62 .bcs-smtp-card-header{padding:20px 24px 16px;border-bottom:1px solid var(--bcs-smtp-border)}
    63 .bcs-smtp-card-body{padding:24px}
    64 .bcs-smtp-card-title{font-size:14px;font-weight:600;color:var(--bcs-smtp-text);margin:0 0 4px;display:flex;align-items:center;gap:8px}
    65 .bcs-smtp-card-title .dashicons{color:var(--bcs-smtp-indigo);font-size:16px;width:16px;height:16px}
    66 .bcs-smtp-card-desc{font-size:13px;color:var(--bcs-smtp-text-2);margin:0;line-height:1.6}
    67 
    68 /* =====================================================================
    69    Grid
    70    ===================================================================== */
    71 .bcs-smtp-grid{display:grid;grid-template-columns:2fr 1fr;gap:20px;align-items:start;min-width:0}
    72 .bcs-smtp-grid>div{min-width:0}
    73 @media(max-width:900px){.bcs-smtp-grid{grid-template-columns:1fr}}
    74 
    75 /* =====================================================================
    76    Stat Row
    77    ===================================================================== */
    78 .bcs-smtp-stat-row{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:28px;padding-bottom:20px;border-bottom:1px solid var(--bcs-smtp-border)}
    79 .bcs-smtp-stat{flex:1;min-width:160px;padding:14px 16px;background:var(--bcs-smtp-surface-2);border:1px solid var(--bcs-smtp-border);border-radius:var(--bcs-smtp-radius);display:flex;align-items:center;gap:14px}
    80 .bcs-smtp-stat-icon{width:36px;height:36px;border-radius:var(--bcs-smtp-radius-sm);display:flex;align-items:center;justify-content:center;flex-shrink:0;background:var(--bcs-smtp-indigo-dim);color:var(--bcs-smtp-indigo-h);border:1px solid rgba(99,102,241,.3)}
    81 .bcs-smtp-stat-icon .dashicons{font-size:18px;width:18px;height:18px}
    82 .bcs-smtp-stat-icon.good{background:var(--bcs-smtp-emerald-dim);color:var(--bcs-smtp-emerald);border-color:rgba(16,185,129,.3)}
    83 .bcs-smtp-stat-icon.bad{background:var(--bcs-smtp-rose-dim);color:var(--bcs-smtp-rose);border-color:rgba(244,63,94,.3)}
    84 .bcs-smtp-stat-body{min-width:0}
    85 .bcs-smtp-stat-value{font-size:20px;font-weight:600;color:var(--bcs-smtp-text);line-height:1;font-family:var(--bcs-smtp-mono)}
    86 .bcs-smtp-stat-label{font-size:11px;color:var(--bcs-smtp-text-3);margin-top:4px;text-transform:uppercase;letter-spacing:.06em}
    87 .bcs-smtp-stat-value.good{color:var(--bcs-smtp-emerald)}
    88 .bcs-smtp-stat-value.bad{color:var(--bcs-smtp-rose)}
     106.bcs-smtp-tab-panel.active{display:block;animation:bcs-panel-in .24s var(--bcs-ease)}
     107@keyframes bcs-panel-in{from{opacity:.35;transform:translateY(6px)}to{opacity:1;transform:none}}
     108
     109/* =====================================================================
     110   Panels & layout
     111   ===================================================================== */
     112.bcs-smtp-panel{background:var(--bcs-panel);border:1px solid var(--bcs-seam);border-radius:var(--bcs-r);padding:24px;min-width:0;box-shadow:var(--bcs-shadow)}
     113.bcs-smtp-panel + .bcs-smtp-panel{margin-top:20px}
     114.bcs-smtp-panel--flush{padding:0;overflow:hidden}
     115.bcs-smtp-panel h2{font-size:16px;font-weight:600;line-height:1.3;margin-bottom:6px}
     116.bcs-smtp-panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px 12px;margin-bottom:18px;flex-wrap:wrap}
     117.bcs-smtp-panel-head h2{margin:0;display:flex;align-items:center;gap:10px}
     118.bcs-smtp-panel-sub{font-size:13px!important;color:var(--bcs-ink-3)}
     119.bcs-smtp-stack{display:flex;flex-direction:column;gap:20px;min-width:0}
     120.bcs-smtp-stack > .bcs-smtp-panel + .bcs-smtp-panel{margin-top:0}
     121.bcs-smtp-split{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(280px,1fr);gap:20px;align-items:start}
     122.bcs-smtp-split > .bcs-smtp-panel + .bcs-smtp-panel{margin-top:0}
     123.bcs-smtp-split > .bcs-smtp-test{position:sticky;top:52px}
     124.bcs-smtp-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:20px}
     125.bcs-smtp-count{display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:22px;padding:0 7px;border-radius:99px;font:600 12px/1 var(--bcs-font);background:var(--bcs-raised);color:var(--bcs-ink-2)}
     126.bcs-smtp-count--bad{background:rgba(251,113,133,.15);color:var(--bcs-bad)}
    89127
    90128/* =====================================================================
    91129   Buttons
    92130   ===================================================================== */
    93 .bcs-smtp-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:12px 24px;border-radius:var(--bcs-smtp-radius);font-size:14px;font-weight:600;font-family:var(--bcs-smtp-font);cursor:pointer;border:none;transition:all var(--bcs-smtp-t);text-decoration:none;white-space:nowrap;line-height:1}
    94 .bcs-smtp-btn-primary{background:linear-gradient(135deg,var(--bcs-smtp-indigo),#7c3aed);color:#fff;box-shadow:0 4px 14px var(--bcs-smtp-indigo-glow)}
    95 .bcs-smtp-btn-primary:hover{transform:translateY(-1px);box-shadow:0 6px 20px var(--bcs-smtp-indigo-glow);color:#fff}
    96 .bcs-smtp-btn-primary:active{transform:translateY(0)}
    97 .bcs-smtp-btn-primary:disabled{opacity:.5;cursor:not-allowed;transform:none}
    98 .bcs-smtp-btn-secondary{background:var(--bcs-smtp-surface-2);border:1px solid var(--bcs-smtp-border-light);color:var(--bcs-smtp-text-2)}
    99 .bcs-smtp-btn-secondary:hover{background:var(--bcs-smtp-surface-3);color:var(--bcs-smtp-text)}
    100 .bcs-smtp-btn-danger{background:var(--bcs-smtp-rose-dim);border:1px solid rgba(244,63,94,.3);color:var(--bcs-smtp-rose)}
    101 .bcs-smtp-btn-danger:hover{background:var(--bcs-smtp-rose);color:#fff}
    102 .bcs-smtp-btn-sm{padding:7px 14px;font-size:12px}
     131.bcs-smtp-btn{
     132  appearance:none;display:inline-flex;align-items:center;justify-content:center;gap:8px;
     133  min-height:38px;padding:0 16px;border-radius:var(--bcs-r-sm);border:1px solid transparent;
     134  font:600 14px/1 var(--bcs-font);text-decoration:none;white-space:nowrap;cursor:pointer;
     135  transition:background var(--bcs-t),border-color var(--bcs-t),color var(--bcs-t),transform .08s ease;
     136}
     137.bcs-smtp-btn:active:not(:disabled){transform:translateY(1px)}
     138.bcs-smtp-btn:disabled,.bcs-smtp-btn[aria-disabled="true"]{opacity:.5;cursor:not-allowed}
    103139.bcs-smtp-btn .dashicons{font-size:16px;width:16px;height:16px}
    104 .bcs-smtp-btn-full{width:100%}
    105 
    106 /* =====================================================================
    107    Toggle (on/off switch)
    108    ===================================================================== */
    109 .bcs-smtp-toggle{position:relative;display:inline-block;width:44px;height:24px;flex-shrink:0}
    110 .bcs-smtp-toggle input{display:none}
    111 .bcs-smtp-toggle-slider{position:absolute;inset:0;cursor:pointer;background:var(--bcs-smtp-surface-3);border-radius:100px;border:1px solid var(--bcs-smtp-border-light);transition:all var(--bcs-smtp-t)}
    112 .bcs-smtp-toggle-slider::before{content:'';position:absolute;width:16px;height:16px;left:3px;top:50%;transform:translateY(-50%);background:var(--bcs-smtp-text-3);border-radius:50%;transition:all var(--bcs-smtp-t)}
    113 .bcs-smtp-toggle input:checked+.bcs-smtp-toggle-slider{background:var(--bcs-smtp-indigo);border-color:var(--bcs-smtp-indigo)}
    114 .bcs-smtp-toggle input:checked+.bcs-smtp-toggle-slider::before{transform:translate(20px,-50%);background:#fff}
    115 
    116 .bcs-smtp-toggle-row{display:flex;align-items:center;justify-content:space-between;gap:12px}
    117 .bcs-smtp-toggle-row-info{flex:1}
    118 .bcs-smtp-toggle-row-label{font-size:14px;font-weight:600;color:var(--bcs-smtp-text)}
    119 .bcs-smtp-toggle-row-desc{font-size:12px;color:var(--bcs-smtp-text-3);margin-top:2px}
    120 
    121 /* =====================================================================
    122    Accordion
    123    ===================================================================== */
    124 .bcs-smtp-accordion{border:1px solid var(--bcs-smtp-border);border-radius:var(--bcs-smtp-radius);margin-top:16px;overflow:hidden}
    125 .bcs-smtp-accordion-head{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;background:var(--bcs-smtp-surface-2);cursor:pointer;user-select:none;transition:background var(--bcs-smtp-t);border:none;width:100%;text-align:left;font-family:var(--bcs-smtp-font)}
    126 .bcs-smtp-accordion-head:hover{background:var(--bcs-smtp-surface-3)}
    127 .bcs-smtp-accordion-head-left{display:flex;align-items:center;gap:10px}
    128 .bcs-smtp-accordion-head-icon{width:32px;height:32px;border-radius:var(--bcs-smtp-radius-sm);display:flex;align-items:center;justify-content:center;flex-shrink:0}
    129 .bcs-smtp-accordion-head-icon .dashicons{font-size:16px;width:16px;height:16px}
    130 .bcs-smtp-accordion-head-title{font-size:13px;font-weight:600;color:var(--bcs-smtp-text)}
    131 .bcs-smtp-accordion-head-desc{font-size:12px;color:var(--bcs-smtp-text-2);margin-top:1px}
    132 .bcs-smtp-accordion-chevron{color:var(--bcs-smtp-text-3);transition:transform var(--bcs-smtp-t);font-size:16px;width:16px;height:16px}
    133 .bcs-smtp-accordion.open .bcs-smtp-accordion-chevron{transform:rotate(180deg)}
    134 .bcs-smtp-accordion-body{display:none;padding:18px;border-top:1px solid var(--bcs-smtp-border);background:var(--bcs-smtp-bg)}
    135 .bcs-smtp-accordion.open .bcs-smtp-accordion-body{display:block}
    136 .bcs-smtp-accordion-header{display:flex;align-items:center;gap:10px;padding:14px 18px;background:var(--bcs-smtp-surface-2);cursor:pointer;user-select:none;transition:background var(--bcs-smtp-t);border:none;width:100%;text-align:left;font-family:var(--bcs-smtp-font);font-size:13px;font-weight:600;color:var(--bcs-smtp-text)}
    137 .bcs-smtp-accordion-header:hover{background:var(--bcs-smtp-surface-3)}
    138 .bcs-smtp-accordion-arrow{transition:transform var(--bcs-smtp-t);color:var(--bcs-smtp-text-3);font-size:16px;width:16px;height:16px}
    139 
    140 /* =====================================================================
    141    Fields / Inputs
    142    ===================================================================== */
    143 .bcs-smtp-field{margin-top:14px}
    144 .bcs-smtp-field-label{font-size:12px;color:var(--bcs-smtp-text-2);display:block;margin-bottom:6px}
    145 .bcs-smtp-input{width:100%;padding:10px 14px;background:var(--bcs-smtp-surface-2)!important;border:1px solid var(--bcs-smtp-border-light)!important;border-radius:var(--bcs-smtp-radius-sm);color:var(--bcs-smtp-text)!important;font-size:13px;font-family:var(--bcs-smtp-mono);transition:border-color var(--bcs-smtp-t),box-shadow var(--bcs-smtp-t);outline:none;box-sizing:border-box;box-shadow:none!important}
    146 input.bcs-smtp-input,select.bcs-smtp-input{height:38px!important;min-height:38px}
    147 .bcs-smtp-input:focus{border-color:var(--bcs-smtp-indigo)!important;box-shadow:0 0 0 3px var(--bcs-smtp-indigo-dim)!important}
    148 textarea.bcs-smtp-input{font-family:var(--bcs-smtp-font);resize:vertical;min-height:60px;height:auto!important}
    149 select.bcs-smtp-input{appearance:none!important;-webkit-appearance:none!important;padding-right:36px!important;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E")!important;background-repeat:no-repeat!important;background-position:right 12px center!important;background-size:12px!important;cursor:pointer}
    150 select.bcs-smtp-input option{background:var(--bcs-smtp-surface-2);color:var(--bcs-smtp-text)}
    151 .bcs-smtp-field-group{display:flex;flex-direction:column;gap:12px}
    152 
    153 /* Two-up field row — e.g. From Email/Name, Host/Port, Username/Password, Recipients/Frequency */
    154 .bcs-smtp-field-row{display:flex;gap:16px;margin-top:14px}
    155 .bcs-smtp-field-row .bcs-smtp-field{flex:1;min-width:0;margin-top:0}
    156 @media(max-width:600px){
    157   .bcs-smtp-field-row{flex-direction:column;gap:14px}
    158 }
    159 
    160 /* Password field — show/hide eye toggle */
     140.bcs-smtp-btn-primary{background:var(--bcs-brand);color:#fff}
     141.bcs-smtp-btn-primary:hover:not(:disabled){background:var(--bcs-brand-hover);color:#fff}
     142.bcs-smtp-btn-primary:active:not(:disabled){background:var(--bcs-brand-press)}
     143.bcs-smtp-btn-secondary{background:var(--bcs-field);border-color:var(--bcs-seam-strong);color:var(--bcs-ink)}
     144.bcs-smtp-btn-secondary:hover:not(:disabled){background:var(--bcs-field-hover);color:#fff;border-color:#40507a}
     145.bcs-smtp-btn-sm{min-height:30px;padding:0 12px;font-size:13px}
     146.bcs-smtp-btn-lg{min-height:44px;padding:0 22px;font-size:15px}
     147.bcs-smtp-btn.is-busy{cursor:progress}
     148.bcs-smtp-btn.is-busy::before{content:"";width:14px;height:14px;border-radius:50%;border:2px solid currentColor;border-right-color:transparent;animation:bcs-spin .7s linear infinite}
     149@keyframes bcs-spin{to{transform:rotate(360deg)}}
     150.bcs-smtp-link{appearance:none;background:none;border:0;padding:0;cursor:pointer;font:600 13px/1.4 var(--bcs-font);color:var(--bcs-brand-ink);text-decoration:underline;text-decoration-color:rgba(141,180,255,.35);text-underline-offset:3px}
     151.bcs-smtp-link:hover{color:#b3ccff;text-decoration-color:currentColor}
     152
     153/* =====================================================================
     154   Status chips — one vocabulary for every email's delivery state
     155   ===================================================================== */
     156.bcs-smtp-status{
     157  --tone:var(--bcs-off);
     158  display:inline-flex;align-items:center;gap:7px;height:24px;padding:0 10px 0 8px;border-radius:7px;white-space:nowrap;
     159  font:500 12.5px/1 var(--bcs-font);color:var(--bcs-ink);
     160  background:color-mix(in srgb,var(--tone) 12%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--tone) 24%,transparent);
     161}
     162.bcs-smtp-status-dot{width:7px;height:7px;border-radius:50%;background:var(--tone);flex-shrink:0}
     163.bcs-smtp-status--ok{--tone:var(--bcs-ok)}
     164.bcs-smtp-status--bad{--tone:var(--bcs-bad)}
     165.bcs-smtp-status--warn{--tone:var(--bcs-warn)}
     166.bcs-smtp-status.is-updated{animation:bcs-chip-in .32s var(--bcs-ease)}
     167@keyframes bcs-chip-in{from{transform:scale(.9);opacity:.2}to{transform:none;opacity:1}}
     168
     169.bcs-smtp-key{display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--bcs-off);flex-shrink:0}
     170.bcs-smtp-key--ok{background:var(--bcs-ok)}
     171.bcs-smtp-key--sent{background:var(--bcs-brand-ink)}
     172.bcs-smtp-key--bad{background:var(--bcs-bad)}
     173.bcs-smtp-key--warn{background:var(--bcs-warn)}
     174
     175/* =====================================================================
     176   Overview
     177   ===================================================================== */
     178
     179/* Health banner — the one thing to read first */
     180.bcs-smtp-hero{
     181  --tone:var(--bcs-off);
     182  position:relative;overflow:hidden;border-radius:var(--bcs-r);padding:28px 28px 0;
     183  background:color-mix(in srgb,var(--tone) 8%,var(--bcs-panel));
     184  border:1px solid color-mix(in srgb,var(--tone) 38%,transparent);box-shadow:var(--bcs-shadow);
     185}
     186.bcs-smtp-hero--ok{--tone:var(--bcs-ok)}
     187.bcs-smtp-hero--bad{--tone:var(--bcs-bad)}
     188.bcs-smtp-hero--warn{--tone:var(--bcs-warn)}
     189.bcs-smtp-hero--off{--tone:var(--bcs-off)}
     190.bcs-smtp-hero-main{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:20px;padding-bottom:26px}
     191.bcs-smtp-hero-copy h2{font-size:26px;font-weight:600;line-height:1.15;letter-spacing:-.025em;margin-bottom:6px}
     192.bcs-smtp-hero-copy p{color:var(--bcs-ink-2);font-size:14.5px;max-width:64ch}
     193.bcs-smtp-hero-actions{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}
     194
     195.bcs-smtp-beacon{position:relative;display:grid;place-items:center;width:52px;height:52px;border-radius:50%;background:color-mix(in srgb,var(--tone) 14%,transparent);border:1px solid color-mix(in srgb,var(--tone) 30%,transparent)}
     196.bcs-smtp-beacon span{width:16px;height:16px;border-radius:50%;background:var(--tone);box-shadow:inset 0 -2px 0 rgba(0,0,0,.18)}
     197.bcs-smtp-beacon::after{content:"";position:absolute;inset:-1px;border-radius:50%;border:1px solid var(--tone);opacity:0}
     198.bcs-smtp-hero--ok .bcs-smtp-beacon::after{animation:bcs-beacon 2.8s var(--bcs-ease) infinite}
     199@keyframes bcs-beacon{0%{transform:scale(1);opacity:.55}70%,100%{transform:scale(1.55);opacity:0}}
     200
     201.bcs-smtp-hero-facts{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));margin:0 -28px;border-top:1px solid color-mix(in srgb,var(--tone) 22%,transparent);background:color-mix(in srgb,var(--tone) 4%,var(--bcs-panel))}
     202.bcs-smtp-hero-facts > div{padding:14px 28px;min-width:0;border-left:1px solid var(--bcs-seam)}
     203.bcs-smtp-hero-facts > div:first-child{border-left:0}
     204.bcs-smtp-hero-facts dt{font-size:12px;color:var(--bcs-ink-3);margin-bottom:3px}
     205.bcs-smtp-hero-facts dd{margin:0;font-size:13.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     206.bcs-smtp-hero-msg{margin:0;padding:0 0 18px}
     207.bcs-smtp-hero-msg:empty{display:none}
     208
     209/* Grid: activity chart + setup checklist */
     210.bcs-smtp-overview-grid{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:20px;margin-top:20px;align-items:start}
     211.bcs-smtp-overview-grid{align-items:stretch}
     212.bcs-smtp-overview-grid > .bcs-smtp-activity{display:flex;flex-direction:column}
     213.bcs-smtp-overview-grid > .bcs-smtp-activity .bcs-smtp-linechart{flex:1 1 auto;min-height:200px;height:auto}
     214.bcs-smtp-side{display:flex;flex-direction:column;gap:20px;min-width:0}
     215.bcs-smtp-side > .bcs-smtp-panel + .bcs-smtp-panel{margin-top:0}
     216.bcs-smtp-side > .bcs-smtp-quick{flex:1 1 auto;display:flex;flex-direction:column}
     217.bcs-smtp-quick .bcs-smtp-quick-list{flex:1 1 auto;justify-content:space-evenly}
     218.bcs-smtp-side .bcs-smtp-panel{padding:20px 22px}
     219.bcs-smtp-side .bcs-smtp-panel-head{margin-bottom:14px}
     220
     221/* Quick actions */
     222.bcs-smtp-quick-list{list-style:none;margin:0 -12px;padding:0;display:flex;flex-direction:column;gap:2px}
     223.bcs-smtp-quick-list li{margin:0}
     224.bcs-smtp-quick-list button{appearance:none;width:100%;display:flex;align-items:center;gap:12px;padding:7px 12px;border:0;border-radius:10px;background:none;cursor:pointer;text-align:left;color:var(--bcs-ink);font-family:var(--bcs-font);transition:background var(--bcs-t)}
     225.bcs-smtp-quick-list button:hover{background:rgba(255,255,255,.04)}
     226.bcs-smtp-quick-icon{display:grid!important;place-items:center;width:32px!important;height:32px!important;flex-shrink:0;border-radius:9px;background:var(--bcs-brand-wash);color:var(--bcs-brand-ink);font-size:18px!important}
     227.bcs-smtp-quick-icon--bad{background:rgba(251,113,133,.13);color:var(--bcs-bad)}
     228.bcs-smtp-quick-icon--ok{background:rgba(52,211,153,.13);color:var(--bcs-ok)}
     229.bcs-smtp-quick-text{display:flex;flex-direction:column;min-width:0;flex:1}
     230.bcs-smtp-quick-text strong{font-weight:500;font-size:14px}
     231.bcs-smtp-quick-text small{font-size:12.5px;color:var(--bcs-ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     232.bcs-smtp-quick-list button > .dashicons:last-child{color:var(--bcs-ink-3);font-size:16px;width:16px;height:16px;transition:transform var(--bcs-t),color var(--bcs-t)}
     233.bcs-smtp-quick-list button:hover > .dashicons:last-child{color:var(--bcs-ink);transform:translateX(2px)}
     234.bcs-smtp-overview-grid > .bcs-smtp-panel + .bcs-smtp-panel{margin-top:0}
     235
     236/* Email activity — line chart */
     237.bcs-smtp-segmented--sm{padding:2px}
     238.bcs-smtp-segmented--sm span{min-height:28px;padding:0 11px;font-size:12.5px}
     239.bcs-smtp-linechart{position:relative;height:260px;margin:0 -6px;border-radius:var(--bcs-r-sm);cursor:crosshair}
     240.bcs-smtp-linechart:focus-visible{outline-offset:4px}
     241.bcs-smtp-linechart svg{display:block;width:100%;height:100%;overflow:visible}
     242.bcs-smtp-lc-grid line{stroke:var(--bcs-seam);stroke-width:1}
     243.bcs-smtp-lc-grid line.is-base{stroke:var(--bcs-seam-strong)}
     244.bcs-smtp-lc-label{fill:var(--bcs-ink-3);font:500 11.5px var(--bcs-font)}
     245.bcs-smtp-lc-area{fill:var(--bcs-brand-ink);opacity:.1}
     246.bcs-smtp-lc-line{fill:none;stroke-width:2.25;stroke-linecap:round;stroke-linejoin:round}
     247.bcs-smtp-lc-line.is-sent{stroke:var(--bcs-brand-ink)}
     248.bcs-smtp-lc-line.is-failed{stroke:var(--bcs-bad)}
     249.bcs-smtp-lc-line.is-unconfirmed{stroke:var(--bcs-warn);stroke-dasharray:4 4}
     250.bcs-smtp-lc-line{stroke-dasharray:var(--len);stroke-dashoffset:var(--len);animation:bcs-draw .9s var(--bcs-ease) forwards}
     251.bcs-smtp-lc-line.is-unconfirmed{animation:none;stroke-dashoffset:0}
     252@keyframes bcs-draw{to{stroke-dashoffset:0}}
     253.bcs-smtp-lc-cursor{stroke:var(--bcs-ink-3);stroke-width:1;stroke-dasharray:3 3;opacity:0;transition:opacity .12s}
     254.bcs-smtp-lc-dot{stroke:var(--bcs-panel);stroke-width:2.5;opacity:0;transition:opacity .12s}
     255.bcs-smtp-linechart.is-hover .bcs-smtp-lc-cursor,.bcs-smtp-linechart.is-hover .bcs-smtp-lc-dot{opacity:1}
     256.bcs-smtp-linechart-tip{position:absolute;top:0;z-index:2;min-width:168px;padding:10px 12px;border-radius:10px;pointer-events:none;
     257  background:#0a0e17;border:1px solid var(--bcs-seam-strong);box-shadow:0 10px 28px -8px rgba(0,0,0,.7);font-size:13px;line-height:1.5;transform:translateX(-50%)}
     258.bcs-smtp-linechart-tip strong{display:block;font-weight:600;margin-bottom:4px}
     259.bcs-smtp-linechart-tip span{display:flex;align-items:center;gap:8px;color:var(--bcs-ink-2)}
     260.bcs-smtp-linechart-tip b{margin-left:auto;color:var(--bcs-ink);font-weight:600}
     261.bcs-smtp-activity-foot{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;margin:16px -24px -24px!important;padding:14px 24px;border-top:1px solid var(--bcs-seam);border-radius:0 0 var(--bcs-r) var(--bcs-r);background:rgba(0,0,0,.14);font-size:13.5px!important;color:var(--bcs-ink-2)}
     262.bcs-smtp-activity .bcs-smtp-chart-empty{margin-top:-140px!important;text-align:center;color:var(--bcs-ink-3);position:relative}
     263
     264/* Finish setting up */
     265.bcs-smtp-progress{display:grid;grid-auto-flow:column;grid-auto-columns:1fr;gap:4px;margin:-6px 0 18px}
     266.bcs-smtp-progress span{height:4px;border-radius:2px;background:var(--bcs-seam-strong)}
     267.bcs-smtp-progress span.is-done{background:var(--bcs-brand)}
     268.bcs-smtp-checklist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
     269.bcs-smtp-checklist li{display:flex;align-items:center;gap:12px;margin:0;padding:8px 0;border-top:1px solid var(--bcs-seam);font-size:14px}
     270.bcs-smtp-checklist li:first-child{border-top:0;padding-top:0}
     271.bcs-smtp-checklist li.is-done{color:var(--bcs-ink-3)}
     272.bcs-smtp-check-dot{display:grid!important;place-items:center;width:20px!important;height:20px!important;border-radius:50%;border:1.5px solid var(--bcs-seam-strong);font-size:15px!important;flex-shrink:0}
     273.is-done .bcs-smtp-check-dot{border-color:transparent;background:rgba(52,211,153,.16);color:var(--bcs-ok)}
     274.bcs-smtp-link--plain{display:inline-flex;align-items:center;gap:4px;text-decoration:none;color:var(--bcs-ink)!important;font-weight:500;font-size:14px}
     275.bcs-smtp-link--plain .dashicons{font-size:15px;width:15px;height:15px;color:var(--bcs-brand-ink);transition:transform var(--bcs-t)}
     276.bcs-smtp-link--plain:hover .dashicons{transform:translateX(2px)}
     277
     278/* Last 30 days */
     279.bcs-smtp-figures{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px;margin:0 0 22px}
     280.bcs-smtp-figures > div{min-width:0}
     281.bcs-smtp-figures dt{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--bcs-ink-3)}
     282.bcs-smtp-figures dd{margin:6px 0 0;font:600 28px/1 var(--bcs-font);letter-spacing:-.03em;color:var(--bcs-ink);white-space:nowrap}
     283.bcs-smtp-figures dd.is-bad{color:var(--bcs-bad)}
     284.bcs-smtp-figures dd small{font-size:16px;font-weight:500;color:var(--bcs-ink-3);margin-left:1px;letter-spacing:0}
     285
     286
     287/* Fact grid (Settings → What WordPress is using) */
     288.bcs-smtp-facts{display:grid;grid-template-columns:1fr 1fr;gap:0;margin:0;border-top:1px solid var(--bcs-seam)}
     289.bcs-smtp-facts > div{padding:11px 0;border-bottom:1px solid var(--bcs-seam);min-width:0}
     290.bcs-smtp-facts > div:nth-child(odd){padding-right:14px}
     291.bcs-smtp-facts dt{font-size:12px;color:var(--bcs-ink-3)}
     292.bcs-smtp-facts dd{margin:2px 0 0;font-size:13.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     293.bcs-smtp-facts--plain{margin-top:14px}
     294
     295/* =====================================================================
     296   Guided setup — providers, steps, fields
     297   ===================================================================== */
     298
     299.bcs-smtp-provider-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(168px,1fr));gap:8px}
     300.bcs-smtp-provider-grid--compact{grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
     301.bcs-smtp-provider{
     302  position:relative;display:flex;align-items:center;gap:10px;min-height:52px;padding:8px 12px;cursor:pointer;
     303  background:var(--bcs-field);border:1px solid var(--bcs-seam);border-radius:var(--bcs-r-sm);
     304  font:500 13.5px/1.25 var(--bcs-font);color:var(--bcs-ink);text-align:left;
     305  transition:border-color var(--bcs-t),background var(--bcs-t);
     306}
     307button.bcs-smtp-provider{appearance:none;width:100%}
     308.bcs-smtp-provider:hover{border-color:var(--bcs-seam-strong);background:var(--bcs-field-hover)}
     309.bcs-smtp-provider input{position:absolute;opacity:0;width:1px;height:1px;margin:0;pointer-events:none}
     310.bcs-smtp-provider-mark{display:grid;place-items:center;width:32px;height:32px;flex-shrink:0;border-radius:6px;background:var(--bcs-panel);border:1px solid var(--bcs-seam-strong);font:700 12px/1 var(--bcs-font);letter-spacing:.02em;color:var(--bcs-ink-2);transition:background var(--bcs-t),color var(--bcs-t),border-color var(--bcs-t)}
     311.bcs-smtp-provider .bcs-smtp-provider-mark{background:color-mix(in srgb,var(--tone) 15%,var(--bcs-panel));border-color:color-mix(in srgb,var(--tone) 38%,transparent);color:color-mix(in srgb,var(--tone) 75%,#fff)}
     312.bcs-smtp-provider-mark .dashicons{font-size:16px;width:16px;height:16px}
     313.bcs-smtp-provider:has(input:checked){border-color:var(--bcs-brand);background:var(--bcs-brand-wash)}
     314.bcs-smtp-provider:has(input:checked) .bcs-smtp-provider-mark{background:var(--tone);border-color:var(--tone);color:#fff}
     315.bcs-smtp-provider:has(input:focus-visible){outline:2px solid var(--bcs-brand-ink);outline-offset:2px}
     316
     317.bcs-smtp-provider-note{display:flex;gap:10px;align-items:flex-start;margin-top:14px!important;padding:12px 14px;border-radius:var(--bcs-r-sm);background:var(--bcs-field);border:1px solid var(--bcs-seam);font-size:13.5px!important;color:var(--bcs-ink-2)}
     318.bcs-smtp-provider-note .dashicons{color:var(--bcs-brand-ink);font-size:18px;width:18px;height:18px;flex-shrink:0}
     319.bcs-smtp-provider-note a{font-weight:600;margin-left:4px}
     320
     321.bcs-smtp-field{display:flex;flex-direction:column;min-width:0;margin-top:16px}
     322.bcs-smtp-field-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
     323.bcs-smtp-field-row .bcs-smtp-field{margin-top:16px}
     324.bcs-smtp-field-row:has(.bcs-smtp-field--port){grid-template-columns:minmax(0,1fr) 120px}
     325.bcs-smtp-field-row:has(> .bcs-smtp-field--grow:first-child):not(:has(.bcs-smtp-field--port)){grid-template-columns:minmax(0,1fr) auto}
     326.bcs-smtp-field-label{font-size:13px;font-weight:600;color:var(--bcs-ink);margin-bottom:6px;display:block}
     327.bcs-smtp-hint{display:block;font-size:12.5px;color:var(--bcs-ink-3);margin-top:6px;line-height:1.45}
     328.bcs-smtp-hint.is-good{color:var(--bcs-sent)}
     329.bcs-smtp-hint.is-bad{color:var(--bcs-back)}
     330
     331.bcs-smtp-wrap .bcs-smtp-input{
     332  width:100%;min-height:40px;height:40px;padding:0 12px;margin:0;
     333  background:var(--bcs-field);border:1px solid var(--bcs-seam-strong);border-radius:var(--bcs-r-sm);
     334  color:var(--bcs-ink);font:400 14px/1.4 var(--bcs-font);box-shadow:none;
     335  transition:border-color var(--bcs-t),box-shadow var(--bcs-t),background var(--bcs-t);
     336}
     337.bcs-smtp-wrap .bcs-smtp-input.bcs-smtp-mono{font-family:var(--bcs-mono);font-size:13px}
     338.bcs-smtp-wrap .bcs-smtp-input::placeholder{color:#6f7c97}
     339.bcs-smtp-wrap .bcs-smtp-input:hover{border-color:#40507a}
     340.bcs-smtp-wrap .bcs-smtp-input:focus{border-color:var(--bcs-brand-ink);box-shadow:0 0 0 3px rgba(141,180,255,.18);outline:none}
     341.bcs-smtp-wrap .bcs-smtp-input:disabled{opacity:.6}
     342.bcs-smtp-wrap .bcs-smtp-input.is-invalid{border-color:var(--bcs-bad);box-shadow:0 0 0 3px rgba(251,113,133,.18)}
     343.bcs-smtp-wrap .bcs-smtp-input--num{width:76px;text-align:center;padding:0 6px}
     344.bcs-smtp-wrap .bcs-smtp-select{
     345  appearance:none;-webkit-appearance:none;width:auto;min-width:150px;padding-right:34px;cursor:pointer;
     346  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23aab5ca' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
     347  background-repeat:no-repeat;background-position:right 12px center;background-size:12px;
     348}
     349.bcs-smtp-wrap .bcs-smtp-select--sm{min-width:72px;min-height:30px;height:30px;font-size:13px}
     350.bcs-smtp-wrap select.bcs-smtp-input option{background:var(--bcs-field);color:var(--bcs-ink)}
     351/* WordPress core darkens select text on hover/focus (#1e1e1e), which vanishes on this theme. */
     352.wp-core-ui .bcs-smtp-wrap select.bcs-smtp-input:hover,.wp-core-ui .bcs-smtp-wrap select.bcs-smtp-input:focus{color:var(--bcs-ink)}
     353
    161354.bcs-smtp-pw-wrap{position:relative}
    162 .bcs-smtp-pw-wrap input{padding-right:40px!important}
    163 .bcs-smtp-pw-eye{position:absolute;top:50%;right:6px;transform:translateY(-50%);display:flex;align-items:center;justify-content:center;width:28px;height:28px;background:transparent;border:none;cursor:pointer;color:var(--bcs-smtp-text-3);border-radius:4px;transition:color var(--bcs-smtp-t),background var(--bcs-smtp-t)}
    164 .bcs-smtp-pw-eye:hover{color:var(--bcs-smtp-text);background:var(--bcs-smtp-surface-3)}
    165 .bcs-smtp-pw-eye .dashicons{font-size:16px;width:16px;height:16px}
    166 
    167 /* Cancel-password-change link — pulses briefly each time it's revealed */
    168 @keyframes bcs-smtp-pw-cancel-pulse{
    169   0%{background:rgba(99,102,241,.3);box-shadow:0 0 0 4px rgba(99,102,241,.2)}
    170   100%{background:transparent;box-shadow:0 0 0 0 rgba(99,102,241,0)}
    171 }
    172 #bcs-smtp-pw-cancel-row{animation:bcs-smtp-pw-cancel-pulse 1.6s ease-out;border-radius:6px;padding:4px 8px;margin-left:-8px}
    173 
    174 /* =====================================================================
    175    Checkboxes / Radio
    176    ===================================================================== */
    177 .bcs-smtp-check-list{display:flex;flex-direction:column;gap:10px}
    178 .bcs-smtp-check-item{display:flex;align-items:flex-start;gap:10px;cursor:pointer;padding:10px 12px;border-radius:var(--bcs-smtp-radius-sm);border:1px solid transparent;transition:border-color var(--bcs-smtp-t),background var(--bcs-smtp-t)}
    179 .bcs-smtp-check-item:hover{background:var(--bcs-smtp-surface);border-color:var(--bcs-smtp-border)}
    180 .bcs-smtp-check-item input[type="checkbox"]{appearance:none;-webkit-appearance:none;width:16px;height:16px;border-radius:4px;flex-shrink:0;margin-top:2px;border:1.5px solid var(--bcs-smtp-border-light);background:var(--bcs-smtp-surface-2);cursor:pointer;transition:all var(--bcs-smtp-t);position:relative}
    181 .bcs-smtp-check-item input[type="checkbox"]:checked{background:var(--bcs-smtp-indigo);border-color:var(--bcs-smtp-indigo)}
    182 .bcs-smtp-check-item input[type="checkbox"]:checked::after{content:'';position:absolute;left:4px;top:1px;width:5px;height:9px;border:2px solid #fff;border-top:none;border-left:none;transform:rotate(45deg)}
    183 .bcs-smtp-check-item input[type="radio"]{appearance:none;-webkit-appearance:none;width:20px;height:20px;border-radius:50%;flex-shrink:0;border:2px solid var(--bcs-smtp-border-light);background:var(--bcs-smtp-surface-2);cursor:pointer;transition:all var(--bcs-smtp-t);position:relative}
    184 .bcs-smtp-check-item input[type="radio"]:checked{border-color:var(--bcs-smtp-indigo);background:var(--bcs-smtp-indigo);box-shadow:inset 0 0 0 3px var(--bcs-smtp-surface-2)}
    185 .bcs-smtp-check-label-title{font-size:13px;color:var(--bcs-smtp-text);font-weight:500}
    186 .bcs-smtp-check-label-desc{font-size:11.5px;color:var(--bcs-smtp-text-3);margin-top:1px}
    187 
    188 /* =====================================================================
    189    Alert
    190    ===================================================================== */
    191 .bcs-smtp-alert{display:flex;gap:12px;padding:14px 16px;border-radius:var(--bcs-smtp-radius);margin-bottom:16px;font-size:13px;line-height:1.6}
    192 .bcs-smtp-alert .dashicons{flex-shrink:0;font-size:18px;width:18px;height:18px;margin-top:1px}
    193 .bcs-smtp-alert-content{flex:1}
    194 .bcs-smtp-alert-title{font-weight:600;margin-bottom:4px}
    195 .bcs-smtp-alert-warning{background:var(--bcs-smtp-amber-dim);border:1px solid rgba(245,158,11,.2)}
    196 .bcs-smtp-alert-warning .dashicons{color:var(--bcs-smtp-amber)}
    197 .bcs-smtp-alert-error{background:var(--bcs-smtp-rose-dim);border:1px solid rgba(244,63,94,.25)}
    198 .bcs-smtp-alert-error .dashicons{color:var(--bcs-smtp-rose)}
    199 .bcs-smtp-alert-success{background:var(--bcs-smtp-emerald-dim);border:1px solid rgba(16,185,129,.25)}
    200 .bcs-smtp-alert-success .dashicons{color:var(--bcs-smtp-emerald)}
    201 .bcs-smtp-alert-info{background:var(--bcs-smtp-indigo-dim);border:1px solid rgba(99,102,241,.2)}
    202 .bcs-smtp-alert-info .dashicons{color:var(--bcs-smtp-indigo-h)}
    203 
    204 /* =====================================================================
    205    Server / Config Grid
    206    ===================================================================== */
    207 .bcs-smtp-server-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
    208 .bcs-smtp-server-row{display:flex;flex-direction:column;gap:3px;padding:10px 12px;background:var(--bcs-smtp-surface-2);border-radius:var(--bcs-smtp-radius-sm)}
    209 .bcs-smtp-server-key{font-size:11px;color:var(--bcs-smtp-text-3)}
    210 .bcs-smtp-server-val{font-size:12px;font-family:var(--bcs-smtp-mono);color:var(--bcs-smtp-text)}
    211 .bcs-smtp-server-val.good{color:var(--bcs-smtp-emerald)}
    212 .bcs-smtp-server-val.bad{color:var(--bcs-smtp-rose)}
    213 
    214 /* =====================================================================
    215    Badge
    216    ===================================================================== */
    217 .bcs-smtp-badge{display:inline-flex;align-items:center;gap:5px;font-size:11px;padding:3px 10px;border-radius:100px;font-weight:500;font-family:var(--bcs-smtp-mono);white-space:nowrap}
    218 .bcs-smtp-badge-success{background:var(--bcs-smtp-emerald-dim);color:var(--bcs-smtp-emerald);border:1px solid rgba(16,185,129,.3)}
    219 .bcs-smtp-badge-error{background:var(--bcs-smtp-rose-dim);color:var(--bcs-smtp-rose);border:1px solid rgba(244,63,94,.3)}
    220 .bcs-smtp-badge-warning{background:var(--bcs-smtp-amber-dim);color:var(--bcs-smtp-amber);border:1px solid rgba(245,158,11,.3)}
    221 
    222 /* =====================================================================
    223    Tables — test log / incidents / change log
    224    ===================================================================== */
    225 .bcs-smtp-table-wrap{overflow-x:auto;margin-top:4px}
    226 .bcs-smtp-table{width:100%;border-collapse:collapse;font-size:12px}
    227 .bcs-smtp-table th{text-align:left;padding:8px 10px;font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--bcs-smtp-text-3);border-bottom:1px solid var(--bcs-smtp-border);white-space:nowrap}
    228 .bcs-smtp-table td{padding:9px 10px;border-bottom:1px solid var(--bcs-smtp-border);color:var(--bcs-smtp-text);vertical-align:middle}
    229 .bcs-smtp-table tr:last-child td{border-bottom:none}
    230 .bcs-smtp-table tr:hover td{background:var(--bcs-smtp-surface-2)}
    231 .bcs-smtp-table tbody tr{transition:background var(--bcs-smtp-t)}
    232 
    233 /* Skeleton loader — Email Log table while an AJAX filter request is in flight */
    234 @keyframes bcs-smtp-shimmer{0%{background-position:-400px 0}100%{background-position:400px 0}}
    235 .bcs-smtp-skel{display:inline-block;height:10px;border-radius:4px;background:linear-gradient(90deg,var(--bcs-smtp-surface-2) 25%,rgba(255,255,255,.07) 50%,var(--bcs-smtp-surface-2) 75%);background-size:800px 100%;animation:bcs-smtp-shimmer 1.4s infinite linear}
    236 .bcs-smtp-skeleton-row td{padding:9px 10px}
    237 
    238 /* =====================================================================
    239    Misc
    240    ===================================================================== */
    241 .bcs-smtp-divider{height:1px;background:var(--bcs-smtp-border);margin:20px 0}
    242 .bcs-smtp-hidden{display:none!important}
    243 @keyframes bcs-smtp-spin{to{transform:rotate(360deg)}}
    244 .bcs-smtp-spinning{animation:bcs-smtp-spin .7s linear infinite;display:inline-block}
    245 details summary{list-style:none}
    246 details summary::-webkit-details-marker{display:none}
    247 
    248 /* Tab link in alerts */
    249 .bcs-smtp-tab-link{color:var(--bcs-smtp-indigo-h);text-decoration:underline;cursor:pointer}
    250 .bcs-smtp-tab-link:hover{color:var(--bcs-smtp-indigo)}
    251 
    252 /* =====================================================================
    253    Mobile-only overrides (all panels)
    254    ===================================================================== */
    255 @media(max-width:600px){
    256   /* All buttons full-width on mobile, every tab */
    257   .bcs-smtp-btn{width:100%}
    258 
    259   /* Settings — saved-password row: input full width, Change button drops to its own line */
    260   #bcs-smtp-pw-display{flex-wrap:wrap}
    261   #bcs-smtp-pw-display input{flex:1 1 100%!important;width:100%!important}
    262 
    263   /* Email Log filters — date range & status dropdowns full width */
    264   #bcs-smtp-log-date-range,
    265   #bcs-smtp-log-status{width:100%!important;min-width:0!important}
    266 
    267   /* Email Log pagination — Prev/Next stay on one row, left & right */
    268   #bcs-smtp-log-pagination{justify-content:space-between;flex-wrap:nowrap}
    269   #bcs-smtp-log-prev,
    270   #bcs-smtp-log-next{width:auto!important;flex-shrink:0}
    271 
    272   /* Email Report — 4 summary stat boxes as a 2-column (50%) grid, without overflowing the card */
    273   .bcs-smtp-report-stats{grid-template-columns:repeat(2,1fr)!important}
    274   .bcs-smtp-report-stats .bcs-smtp-stat{min-width:0}
    275 
    276   /* Schedule Report — frequency select full-width */
    277   #bcs-smtp-report-frequency{max-width:none!important;width:100%!important}
    278 }
    279 
    280 /* "Failure types detected" list — ensure bullet markers render, on every device */
    281 .bcs-smtp-alert-content ul{list-style:disc;list-style-position:inside;margin:6px 0 0;padding:0}
    282 .bcs-smtp-alert-content li{margin-bottom:4px}
     355.bcs-smtp-pw-wrap .bcs-smtp-input{padding-right:44px}
     356.bcs-smtp-pw-eye{position:absolute;top:50%;right:4px;transform:translateY(-50%);display:grid;place-items:center;width:34px;height:32px;background:none;border:0;border-radius:5px;cursor:pointer;color:var(--bcs-ink-3)}
     357.bcs-smtp-pw-eye:hover{color:var(--bcs-ink);background:var(--bcs-field-hover)}
     358.bcs-smtp-pw-saved{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:40px;padding:0 12px;border-radius:var(--bcs-r-sm);background:var(--bcs-field);border:1px dashed var(--bcs-seam-strong)}
     359.bcs-smtp-pw-dots{letter-spacing:.12em;color:var(--bcs-ink-2)}
     360
     361.bcs-smtp-segmented{display:inline-flex;flex-wrap:wrap;padding:3px;gap:2px;background:var(--bcs-field);border:1px solid var(--bcs-seam-strong);border-radius:var(--bcs-r-sm)}
     362.bcs-smtp-segmented label{position:relative;cursor:pointer;margin:0}
     363.bcs-smtp-segmented input{position:absolute;opacity:0;width:1px;height:1px;margin:0}
     364.bcs-smtp-segmented span{display:flex;align-items:center;min-height:32px;padding:0 12px;border-radius:5px;font:500 13px/1 var(--bcs-font);color:var(--bcs-ink-2);white-space:nowrap;transition:background var(--bcs-t),color var(--bcs-t)}
     365.bcs-smtp-segmented label:hover span{color:var(--bcs-ink)}
     366.bcs-smtp-segmented input:checked + span{background:var(--bcs-brand);color:#fff;font-weight:600}
     367.bcs-smtp-segmented input:focus-visible + span{outline:2px solid var(--bcs-brand-ink);outline-offset:1px}
     368
     369.bcs-smtp-toggle-row{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;margin-top:4px}
     370.bcs-smtp-toggle-row + #bcs-smtp-auth-fields{margin-top:4px}
     371.bcs-smtp-auth-warning{margin-top:14px}
     372.bcs-smtp-toggle-row-info{min-width:0}
     373.bcs-smtp-toggle-row-label{display:block;font-size:14.5px;font-weight:600;color:var(--bcs-ink);cursor:pointer}
     374.bcs-smtp-toggle-row--head .bcs-smtp-toggle-row-label{font-size:17px}
     375.bcs-smtp-toggle-row-desc{font-size:13.5px!important;color:var(--bcs-ink-2);margin-top:3px!important;max-width:62ch}
     376.bcs-smtp-toggle{position:relative;display:inline-block;width:42px;height:24px;flex-shrink:0;margin-top:2px}
     377.bcs-smtp-toggle input{position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;cursor:pointer;z-index:1}
     378.bcs-smtp-toggle-slider{position:absolute;inset:0;border-radius:99px;background:var(--bcs-seam-strong);transition:background var(--bcs-t)}
     379.bcs-smtp-toggle-slider::before{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#c5cddd;box-shadow:0 1px 2px rgba(0,0,0,.35);transition:transform var(--bcs-t),background var(--bcs-t)}
     380.bcs-smtp-toggle input:checked + .bcs-smtp-toggle-slider{background:var(--bcs-brand)}
     381.bcs-smtp-toggle input:checked + .bcs-smtp-toggle-slider::before{transform:translateX(18px);background:#fff}
     382.bcs-smtp-toggle input:focus-visible + .bcs-smtp-toggle-slider{outline:2px solid var(--bcs-brand-ink);outline-offset:2px}
     383
     384.bcs-smtp-disclosure{margin-top:24px;border:1px solid var(--bcs-seam);border-radius:var(--bcs-r-sm);background:var(--bcs-bg)}
     385.bcs-smtp-disclosure-head{appearance:none;width:100%;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px;background:none;border:0;cursor:pointer;text-align:left;color:var(--bcs-ink);font-family:var(--bcs-font)}
     386.bcs-smtp-disclosure-head strong{display:block;font-size:14px;font-weight:600}
     387.bcs-smtp-disclosure-head .bcs-smtp-mono{display:block;color:var(--bcs-ink-3);margin-top:2px;font-size:12px}
     388.bcs-smtp-disclosure-head .dashicons{color:var(--bcs-ink-3);transition:transform var(--bcs-t)}
     389.bcs-smtp-disclosure.is-open .bcs-smtp-disclosure-head .dashicons{transform:rotate(180deg)}
     390.bcs-smtp-disclosure-body{display:none;padding:0 16px 18px;border-top:1px solid var(--bcs-seam)}
     391.bcs-smtp-disclosure.is-open .bcs-smtp-disclosure-body{display:block}
     392.bcs-smtp-disclosure-body .bcs-smtp-toggle-row{margin-top:18px}
     393
     394
     395.bcs-smtp-reveal{display:none;padding-top:6px}
     396.bcs-smtp-reveal.open{display:block}
     397.bcs-smtp-sentence{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:16px;font-size:14px;color:var(--bcs-ink)}
     398.bcs-smtp-sentence label{font-weight:500}
     399.bcs-smtp-sentence .bcs-smtp-hint{flex-basis:100%;margin-top:0}
     400
     401/* Test / aside */
     402.bcs-smtp-test h2{font-size:17px}
     403.bcs-smtp-test > p,.bcs-smtp-aside > p{color:var(--bcs-ink-2);font-size:13.5px}
     404.bcs-smtp-aside > .bcs-smtp-hint{margin-top:18px}
     405
     406/* =====================================================================
     407   Email log
     408   ===================================================================== */
     409.bcs-smtp-filters{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:16px 20px;border-bottom:1px solid var(--bcs-seam)}
     410.bcs-smtp-search{position:relative;flex:1 1 260px;min-width:200px}
     411.bcs-smtp-search .dashicons{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--bcs-ink-3);font-size:18px;width:18px;height:18px;pointer-events:none}
     412.bcs-smtp-wrap .bcs-smtp-search .bcs-smtp-input{padding-left:36px}
     413.bcs-smtp-filters .bcs-smtp-btn{margin-left:auto}
     414
     415.bcs-smtp-table-wrap{overflow-x:auto}
     416.bcs-smtp-table{width:100%;border-collapse:collapse;font-size:13.5px;table-layout:fixed}
     417.bcs-smtp-table th{text-align:left;padding:10px 12px;font:600 11px/1.2 var(--bcs-font);letter-spacing:.09em;text-transform:uppercase;color:var(--bcs-ink-3);background:var(--bcs-bg);border-bottom:1px solid var(--bcs-seam);white-space:nowrap}
     418.bcs-smtp-table td{padding:9px 12px;border-bottom:1px solid var(--bcs-seam);vertical-align:middle;color:var(--bcs-ink);overflow:hidden}
     419.bcs-smtp-table th:first-child,.bcs-smtp-table td:first-child{padding-left:20px}
     420.bcs-smtp-table th:last-child,.bcs-smtp-table td:last-child{padding-right:20px}
     421.bcs-smtp-table tbody tr{transition:background var(--bcs-t)}
     422.bcs-smtp-table tbody tr:hover td{background:rgba(255,255,255,.025)}
     423.bcs-smtp-table tbody tr:last-child td{border-bottom:0}
     424.bcs-smtp-col-date{width:112px;color:var(--bcs-ink-2);white-space:nowrap}
     425td.bcs-smtp-col-date{font-size:12px}
     426.bcs-smtp-col-to{width:28%}
     427.bcs-smtp-col-status{width:128px}
     428.bcs-smtp-col-more{width:84px;text-align:right}
     429.bcs-smtp-table .bcs-smtp-view-btn{display:inline-flex;align-items:center;gap:2px;height:30px;padding:0 6px 0 10px;border-radius:7px;font-size:13px;font-weight:600;color:var(--bcs-ink-2);text-decoration:none;transition:color var(--bcs-t),background var(--bcs-t)}
     430.bcs-smtp-view-btn .dashicons{font-size:15px;width:15px;height:15px;transition:transform var(--bcs-t)}
     431.bcs-smtp-table tbody tr:hover .bcs-smtp-view-btn,.bcs-smtp-table .bcs-smtp-view-btn:focus-visible{color:var(--bcs-brand-ink);background:var(--bcs-brand-wash)}
     432.bcs-smtp-view-btn:hover .dashicons{transform:translateX(2px)}
     433.bcs-smtp-view-btn:focus-visible{outline:2px solid var(--bcs-brand-ink);outline-offset:1px}
     434.bcs-smtp-table tbody tr.bcs-smtp-day-row th{padding:16px 20px 8px;font:600 12px/1.2 var(--bcs-font);letter-spacing:.02em;text-transform:none;color:var(--bcs-ink-2);background:var(--bcs-panel);border-bottom:1px solid var(--bcs-seam)}
     435.bcs-smtp-table tbody tr.bcs-smtp-day-row:hover th{background:var(--bcs-panel)}
     436.bcs-smtp-log-error{margin-top:12px;border-radius:10px;border:1px solid rgba(251,113,133,.25);background:rgba(251,113,133,.06);overflow:hidden}
     437.bcs-smtp-log-error-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 10px 8px 16px;border-bottom:1px solid rgba(251,113,133,.16);font-size:12px;font-weight:600;color:var(--bcs-bad)}
     438.bcs-smtp-copy-btn{display:inline-flex;align-items:center;gap:5px;height:28px;padding:0 10px;border:1px solid var(--bcs-seam-strong);border-radius:7px;background:var(--bcs-panel);color:var(--bcs-ink);font:inherit;font-size:12.5px;font-weight:500;cursor:pointer}
     439.bcs-smtp-copy-btn:hover{border-color:var(--bcs-ink-3)}
     440.bcs-smtp-copy-btn .dashicons{font-size:14px;width:14px;height:14px}
     441.bcs-smtp-log-error pre{margin:0;padding:12px 16px;font-size:12.5px;line-height:1.6;color:var(--bcs-ink);white-space:pre-wrap;overflow-wrap:anywhere;background:none}
     442.bcs-smtp-seg-count{margin-left:6px;font-weight:500;font-size:12px;opacity:.7;font-variant-numeric:tabular-nums}
     443.bcs-smtp-seg-count:empty{display:none}
     444.bcs-smtp-table tr[hidden]{display:none!important}
     445td.bcs-smtp-col-to,td.bcs-smtp-col-from{white-space:nowrap;text-overflow:ellipsis;color:var(--bcs-ink-2);font-size:12px}
     446.bcs-smtp-cell-main{display:block;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     447.bcs-smtp-cell-note{display:block;margin-top:4px;font-size:11.5px;color:var(--bcs-ink-3)}
     448.bcs-smtp-table-empty{text-align:center;padding:44px 20px!important;color:var(--bcs-ink-2)}
     449.bcs-smtp-table-empty strong{display:block;color:var(--bcs-ink);font-weight:600;margin-bottom:4px}
     450.bcs-smtp-skel{display:block;height:10px;border-radius:4px;background:var(--bcs-field-hover);animation:bcs-skel-pulse 1.2s ease-in-out infinite}
     451@keyframes bcs-skel-pulse{0%,100%{opacity:.45}50%{opacity:1}}
     452
     453.bcs-smtp-pager{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:12px 20px;border-top:1px solid var(--bcs-seam);background:var(--bcs-bg)}
     454.bcs-smtp-pager-size{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--bcs-ink-2)}
     455.bcs-smtp-pager-nav{display:flex;align-items:center;gap:12px;font-size:13px;color:var(--bcs-ink-2)}
     456
     457/* =====================================================================
     458   Reports
     459   ===================================================================== */
     460.bcs-smtp-chart{position:relative;height:300px;margin-top:20px}
     461.bcs-smtp-chart-empty{position:absolute;inset:0;display:grid;place-items:center;color:var(--bcs-ink-3)}
     462
     463/* Reports */
     464.bcs-smtp-report-tools{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
     465.bcs-smtp-report .bcs-smtp-panel-head{align-items:flex-start;margin-bottom:16px}
     466.bcs-smtp-report .bcs-smtp-panel-head h2{font-size:18px}
     467.bcs-smtp-report .bcs-smtp-panel-sub{margin-top:2px!important}
     468.bcs-smtp-figures--5{grid-template-columns:repeat(5,minmax(0,1fr));gap:0;margin:0 -24px 16px;border-top:1px solid var(--bcs-seam);border-bottom:1px solid var(--bcs-seam)}
     469.bcs-smtp-figures--5 > div{padding:12px 24px;border-left:1px solid var(--bcs-seam)}
     470.bcs-smtp-figures--5 dd{font-size:24px;margin-top:4px}
     471.bcs-smtp-figures--5 > div:first-child{border-left:0}
     472.bcs-smtp-figure-rate dd{color:var(--bcs-ink)}
     473.bcs-smtp-ratebar{display:block;height:4px;margin-top:8px;border-radius:2px;background:color-mix(in srgb,var(--bcs-bad) 35%,var(--bcs-seam-strong));overflow:hidden}
     474.bcs-smtp-ratebar span{display:block;height:100%;background:var(--bcs-ok);border-radius:2px;transition:width .5s var(--bcs-ease)}
     475.bcs-smtp-linechart--lg{height:220px}
     476.bcs-smtp-report .bcs-smtp-chart-empty{margin-top:-120px!important;text-align:center;color:var(--bcs-ink-3);position:relative}
     477.bcs-smtp-report-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.25fr);gap:20px;margin-top:20px;align-items:stretch}
     478.bcs-smtp-report-grid > .bcs-smtp-panel + .bcs-smtp-panel{margin-top:0}
     479.bcs-smtp-highlights{margin:0;display:flex;flex-direction:column}
     480.bcs-smtp-highlights > div{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 0;border-top:1px solid var(--bcs-seam)}
     481.bcs-smtp-highlights > div:first-child{border-top:0;padding-top:0}
     482.bcs-smtp-highlights dt{display:flex;align-items:center;gap:12px;color:var(--bcs-ink-2);font-size:14px}
     483.bcs-smtp-highlights dd{margin:0;font-weight:600;font-size:16px;text-align:right;white-space:nowrap}
     484.bcs-smtp-highlights dd small{font-size:12.5px;font-weight:400;color:var(--bcs-ink-3);margin-left:4px}
     485.bcs-smtp-highlights dd.is-bad{color:var(--bcs-bad)}
     486.bcs-smtp-highlights dd.is-good{color:var(--bcs-ok)}
     487.bcs-smtp-highlights dt > span{display:flex;flex-direction:column}
     488.bcs-smtp-hl-sub{font-size:12px;color:var(--bcs-ink-3);margin-top:1px}
     489.bcs-smtp-hl-icon{display:grid!important;place-items:center;width:32px!important;height:32px!important;flex-shrink:0;border-radius:9px;background:var(--bcs-brand-wash);color:var(--bcs-brand-ink);font-size:17px!important}
     490.bcs-smtp-hl-icon--bad{background:rgba(251,113,133,.13);color:var(--bcs-bad)}
     491.bcs-smtp-hl-icon--ok{background:rgba(52,211,153,.13);color:var(--bcs-ok)}
     492.bcs-smtp-schedule h2{font-size:16px;margin:0}
     493.bcs-smtp-schedule h2 label{cursor:pointer}
     494.bcs-smtp-schedule .bcs-smtp-field{margin-top:18px}
     495.bcs-smtp-field > .bcs-smtp-segmented{align-self:flex-start}
     496.bcs-smtp-report-next{display:flex;align-items:center;gap:8px;margin-top:14px!important;padding:10px 12px;border-radius:var(--bcs-r-sm);background:var(--bcs-brand-wash);color:var(--bcs-ink);font-size:13.5px!important;font-weight:500}
     497.bcs-smtp-report-next .dashicons{color:var(--bcs-brand-ink);font-size:17px;width:17px;height:17px}
     498
     499/* Recipient chips */
     500.bcs-smtp-chips{display:flex;flex-wrap:wrap;align-items:center;gap:6px;min-height:44px;padding:6px 8px;cursor:text;
     501  background:var(--bcs-field);border:1px solid var(--bcs-seam-strong);border-radius:var(--bcs-r-sm);transition:border-color var(--bcs-t),box-shadow var(--bcs-t)}
     502.bcs-smtp-chips:hover{border-color:#40507a}
     503.bcs-smtp-chips:focus-within{border-color:var(--bcs-brand-ink);box-shadow:0 0 0 3px rgba(141,180,255,.18)}
     504.bcs-smtp-chips.is-invalid{border-color:var(--bcs-bad);box-shadow:0 0 0 3px rgba(251,113,133,.18)}
     505.bcs-smtp-chip-list{display:contents;list-style:none;margin:0;padding:0}
     506.bcs-smtp-chip{display:inline-flex;align-items:center;gap:2px;max-width:100%;height:28px;margin:0;padding:0 2px 0 10px;border-radius:99px;
     507  background:var(--bcs-brand-wash);border:1px solid rgba(141,180,255,.28);color:var(--bcs-ink);font-size:13px;line-height:1}
     508.bcs-smtp-chip-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     509.bcs-smtp-chip.is-invalid{background:rgba(251,113,133,.12);border-color:rgba(251,113,133,.45);color:var(--bcs-bad)}
     510.bcs-smtp-chip.is-armed{border-color:var(--bcs-brand-ink);box-shadow:0 0 0 2px rgba(141,180,255,.35)}
     511.bcs-smtp-chip-x{appearance:none;display:grid;place-items:center;width:22px;height:22px;padding:0;border:0;border-radius:50%;background:none;color:var(--bcs-ink-3);cursor:pointer;flex-shrink:0}
     512.bcs-smtp-chip-x:hover{background:rgba(255,255,255,.1);color:var(--bcs-ink)}
     513.bcs-smtp-chip-x .dashicons{font-size:16px;width:16px;height:16px}
     514.bcs-smtp-wrap .bcs-smtp-chips-input{flex:1 1 160px;min-width:120px;height:30px;margin:0;padding:0 4px;border:0;background:none;box-shadow:none;outline:none;color:var(--bcs-ink);font:400 14px/1.4 var(--bcs-font)}
     515.bcs-smtp-wrap .bcs-smtp-chips-input:focus{box-shadow:none;outline:none}
     516.bcs-smtp-wrap .bcs-smtp-chips-input::placeholder{color:#6f7c97}
     517
     518
     519/* Alerts — channel picker, webhook, health checks */
     520.bcs-smtp-alerts-grid{display:grid;grid-template-columns:minmax(0,1fr) 380px;gap:20px;margin-top:20px;align-items:stretch}
     521.bcs-smtp-alerts-grid > .bcs-smtp-panel + .bcs-smtp-panel{margin-top:0}
     522.bcs-smtp-alerts-grid{margin-top:0}
     523.bcs-smtp-health-side{padding:22px;position:sticky;top:52px}
     524.bcs-smtp-health-head{display:flex;gap:14px;align-items:flex-start}
     525.bcs-smtp-health-icon{display:grid!important;place-items:center;width:42px!important;height:42px!important;flex-shrink:0;border-radius:11px;background:rgba(52,211,153,.13);color:var(--bcs-ok);font-size:20px!important}
     526.bcs-smtp-health-head h2{margin:0 0 4px;font-size:16px;line-height:1.3}
     527.bcs-smtp-health-head p{font-size:13px!important;line-height:1.5;color:var(--bcs-ink-2)}
     528.bcs-smtp-health-times{display:grid;grid-template-columns:1fr 1fr;margin:18px 0 0;border:1px solid var(--bcs-seam);border-radius:10px;background:var(--bcs-field)}
     529.bcs-smtp-health-times > div{padding:12px 14px}
     530.bcs-smtp-health-times > div + div{border-left:1px solid var(--bcs-seam)}
     531.bcs-smtp-health-times dt{font-size:12px;color:var(--bcs-ink-3);margin-bottom:4px}
     532.bcs-smtp-health-times dd{margin:0;font-size:14px;font-weight:600;color:var(--bcs-ink)}
     533.bcs-smtp-health-pending{margin-top:16px!important;padding:12px 14px;border-radius:10px;background:var(--bcs-field);border:1px solid var(--bcs-seam);font-size:13px!important;color:var(--bcs-ink-2)}
     534.bcs-smtp-health-rules{list-style:none;margin:18px 0 0;padding:18px 0 0;border-top:1px solid var(--bcs-seam);display:flex;flex-direction:column;gap:12px}
     535.bcs-smtp-health-rules li{display:flex;align-items:center;gap:10px;margin:0;font-size:13.5px;color:var(--bcs-ink)}
     536.bcs-smtp-health-rules .dashicons{display:grid;place-items:center;width:28px;height:28px;flex-shrink:0;border-radius:8px;background:var(--bcs-brand-wash);color:var(--bcs-brand-ink);font-size:15px}
     537.bcs-smtp-webhook{margin-top:20px}
     538.bcs-smtp-webhook-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
     539.bcs-smtp-webhook-guide{display:inline-flex;align-items:center;gap:3px;font-size:13px;font-weight:600;text-decoration:none;white-space:nowrap}
     540.bcs-smtp-webhook-guide .dashicons{font-size:14px;width:14px;height:14px}
     541.bcs-smtp-webhook-guide:hover{text-decoration:underline}
     542.bcs-smtp-webhook input[aria-invalid="true"]{border-color:var(--bcs-bad)}
     543.bcs-smtp-webhook-err{margin-top:8px!important;font-size:13px!important;color:var(--bcs-bad)}
     544.bcs-smtp-alerts-off{display:flex;gap:12px;align-items:flex-start;margin:18px 0 24px;padding:14px 16px;border-radius:10px;background:var(--bcs-field);border:1px solid var(--bcs-seam)}
     545.bcs-smtp-alerts-off[hidden]{display:none}
     546.bcs-smtp-alerts-off .dashicons{color:var(--bcs-warn);font-size:18px;width:18px;height:18px;margin-top:1px}
     547.bcs-smtp-alerts-off p{font-size:13.5px;color:var(--bcs-ink-2)}
     548.bcs-smtp-unsaved{margin-left:auto;display:inline-flex;align-items:center;gap:7px;font-size:13px;color:var(--bcs-warn)}
     549.bcs-smtp-unsaved::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor}
     550.bcs-smtp-unsaved[hidden]{display:none}
     551.bcs-smtp-field-aside{font-weight:400;color:var(--bcs-ink-3);margin-left:6px}
     552.bcs-smtp-webhook-how{margin-top:10px!important;font-size:13px!important;color:var(--bcs-ink-3);line-height:1.5}
     553
     554.bcs-smtp-alerts-card{display:flex;flex-direction:column}
     555.bcs-smtp-card-foot{margin-top:auto;padding-top:20px;border-top:1px solid var(--bcs-seam)}
     556
     557
     558
     559/* Full email view */
     560.bcs-smtp-back{display:inline-flex;align-items:center;gap:4px;margin:6px 0 18px;font-size:13.5px;font-weight:600;color:var(--bcs-ink-2);text-decoration:none}
     561.bcs-smtp-back:hover,.bcs-smtp-back:focus{color:var(--bcs-ink)}
     562.bcs-smtp-back .dashicons{font-size:16px;width:16px;height:16px}
     563.bcs-smtp-mailview [hidden]{display:none!important}
     564.bcs-smtp-mailview-head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:20px;flex-wrap:wrap}
     565.bcs-smtp-mailview-title{min-width:0;flex:1 1 420px}
     566.bcs-smtp-mailview-when{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:10px!important;font-size:13px;color:var(--bcs-ink-2)}
     567.bcs-smtp-mailview h1{margin:0;padding:0;font-size:24px;line-height:1.3;font-weight:600;color:var(--bcs-ink);overflow-wrap:anywhere}
     568.bcs-smtp-mailview-nav{display:flex;gap:8px}
     569.bcs-smtp-mailview-nav .dashicons{font-size:15px;width:15px;height:15px}
     570.bcs-smtp-mailview-nav .is-disabled{opacity:.4;pointer-events:none}
     571.bcs-smtp-mailview-error{margin:0 0 20px}
     572.bcs-smtp-mailview-grid{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:20px;align-items:start}
     573.bcs-smtp-mailview-grid > .bcs-smtp-panel + .bcs-smtp-panel{margin-top:0}
     574.bcs-smtp-mailview-message .bcs-smtp-panel-head h2,.bcs-smtp-mailview-meta h2{margin:0;font-size:16px}
     575.bcs-smtp-mailframe-wrap{border-radius:10px;overflow:hidden;background:#fff;border:1px solid var(--bcs-seam)}
     576.bcs-smtp-mailframe{display:block;width:100%;height:480px;border:0;background:#fff;overflow:hidden}
     577.bcs-smtp-mailview-source{border-radius:10px;border:1px solid var(--bcs-seam);background:var(--bcs-bg);overflow:hidden}
     578.bcs-smtp-mailview-source-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 10px 8px 16px;border-bottom:1px solid var(--bcs-seam);font-size:12px;font-weight:600;color:var(--bcs-ink-2)}
     579.bcs-smtp-mailview-source pre,.bcs-smtp-mailview-text{margin:0;padding:16px;font-size:12.5px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--bcs-ink);max-height:70vh;overflow:auto;background:none}
     580.bcs-smtp-mailview-text{border-radius:10px;border:1px solid var(--bcs-seam);background:var(--bcs-bg);font-family:var(--bcs-mono)}
     581.bcs-smtp-mailview-empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:48px 24px;text-align:center;border-radius:10px;border:1px dashed var(--bcs-seam-strong)}
     582.bcs-smtp-mailview-empty .dashicons{font-size:28px;width:28px;height:28px;color:var(--bcs-ink-3)}
     583.bcs-smtp-mailview-empty strong{font-size:15px;color:var(--bcs-ink)}
     584.bcs-smtp-mailview-empty p{max-width:46ch;font-size:13.5px;color:var(--bcs-ink-2)}
     585.bcs-smtp-mailview-meta{padding:20px 22px}
     586.bcs-smtp-mailview-meta h2{margin-bottom:14px}
     587.bcs-smtp-mailview-facts{display:flex;flex-direction:column;margin:0}
     588.bcs-smtp-mailview-facts > div{padding:10px 0;border-top:1px solid var(--bcs-seam)}
     589.bcs-smtp-mailview-facts > div:first-child{padding-top:0;border-top:0}
     590.bcs-smtp-mailview-facts dt{margin-bottom:3px;font-size:11.5px;color:var(--bcs-ink-3)}
     591.bcs-smtp-mailview-facts dd{margin:0;font-size:13px;color:var(--bcs-ink);overflow-wrap:anywhere}
     592.bcs-smtp-mailview-meta h3{margin:18px 0 8px;font-size:13px;color:var(--bcs-ink)}
     593.bcs-smtp-mailview-atts{display:flex;flex-direction:column;gap:6px;margin:0 0 8px;padding:0;list-style:none}
     594.bcs-smtp-mailview-atts li{display:flex;align-items:center;gap:8px;margin:0;padding:8px 10px;border-radius:8px;background:var(--bcs-field);border:1px solid var(--bcs-seam);font-size:13px;overflow-wrap:anywhere}
     595.bcs-smtp-mailview-atts .dashicons{color:var(--bcs-ink-3);font-size:16px;width:16px;height:16px;flex-shrink:0}
     596.bcs-smtp-mailview-headers{margin-top:18px;padding-top:14px;border-top:1px solid var(--bcs-seam)}
     597.bcs-smtp-mailview-headers summary{cursor:pointer;font-size:13px;font-weight:600;color:var(--bcs-ink)}
     598.bcs-smtp-mailview-headers pre{margin:10px 0 0;padding:12px;border-radius:8px;background:var(--bcs-bg);border:1px solid var(--bcs-seam);font-size:12px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--bcs-ink-2)}
     599.bcs-smtp-mailview-missing{padding:48px 24px;text-align:center}
     600.bcs-smtp-mailview-missing p{margin-top:8px!important;color:var(--bcs-ink-2)}
     601.bcs-smtp-mailview-message{padding:0;overflow:hidden}
     602.bcs-smtp-envelope{display:flex;gap:14px;align-items:flex-start;padding:20px 24px;border-bottom:1px solid var(--bcs-seam)}
     603.bcs-smtp-envelope-avatar{display:grid;place-items:center;width:42px;height:42px;flex-shrink:0;border-radius:50%;background:var(--bcs-brand);color:#fff;font-size:17px;font-weight:600}
     604.bcs-smtp-envelope-lines{min-width:0;flex:1}
     605.bcs-smtp-envelope-from{font-size:14.5px!important;color:var(--bcs-ink);overflow-wrap:anywhere}
     606.bcs-smtp-envelope-from strong{font-weight:600;margin-right:6px}
     607.bcs-smtp-envelope-from span{color:var(--bcs-ink-3);font-size:13px}
     608.bcs-smtp-envelope-to{margin-top:3px!important;font-size:13px!important;color:var(--bcs-ink-2);overflow-wrap:anywhere}
     609.bcs-smtp-envelope-to span{color:var(--bcs-ink-3);margin-right:4px}
     610.bcs-smtp-mailview-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:12px 24px;border-bottom:1px solid var(--bcs-seam);background:rgba(0,0,0,.14)}
     611.bcs-smtp-device-icon{display:block;width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
     612.bcs-smtp-mailview-body{padding:20px 24px 24px}
     613.bcs-smtp-mailframe-stage{border-radius:10px;background:var(--bcs-bg);border:1px solid var(--bcs-seam);padding:0;transition:padding var(--bcs-t)}
     614.bcs-smtp-mailframe-stage.is-mobile{padding:20px 0}
     615.bcs-smtp-mailframe-stage .bcs-smtp-mailframe-wrap{margin:0 auto;border:0;border-radius:10px;transition:max-width var(--bcs-t)}
     616.bcs-smtp-mailframe-stage.is-mobile .bcs-smtp-mailframe-wrap{max-width:390px;border-radius:18px;box-shadow:0 0 0 6px var(--bcs-panel),0 0 0 7px var(--bcs-seam-strong)}
     617.bcs-smtp-mailview-plain pre{font-family:var(--bcs-font);font-size:13.5px;line-height:1.65}
     618.bcs-smtp-mailview-meta{position:sticky;top:52px}
     619.bcs-smtp-mailview-verdict{display:flex;flex-direction:column;align-items:flex-start;gap:8px;margin-bottom:16px;padding:12px 14px;border-radius:10px;background:var(--bcs-field);border:1px solid var(--bcs-seam)}
     620.bcs-smtp-mailview-verdict p{font-size:13px!important;color:var(--bcs-ink-2);line-height:1.5}
     621.bcs-smtp-mailview-note{margin-top:4px!important;font-size:12px!important;color:var(--bcs-ink-3)!important}
     622@media (max-width:1100px){
     623  .bcs-smtp-mailview-meta{position:static}
     624}
     625@media (max-width:600px){
     626  .bcs-smtp-wrap .bcs-smtp-mailview-message{padding:0}
     627  .bcs-smtp-envelope,.bcs-smtp-mailview-toolbar,.bcs-smtp-mailview-body{padding-left:16px;padding-right:16px}
     628}
     629@media (max-width:1100px){
     630  .bcs-smtp-mailview-grid{grid-template-columns:minmax(0,1fr)}
     631}
     632@media (max-width:600px){
     633  .bcs-smtp-mailview h1{font-size:20px}
     634  .bcs-smtp-mailview-nav{width:100%}
     635  .bcs-smtp-mailview-nav > *{flex:1;justify-content:center}
     636}
     637
     638/* =====================================================================
     639   Messages
     640   ===================================================================== */
     641.bcs-smtp-msg:empty{display:none}
     642.bcs-smtp-msg{margin-top:14px}
     643.bcs-smtp-alert{--tone:var(--bcs-brand-ink);display:flex;gap:10px;align-items:flex-start;padding:12px 14px;border-radius:var(--bcs-r-sm);border:1px solid color-mix(in srgb,var(--tone) 35%,transparent);background:color-mix(in srgb,var(--tone) 9%,var(--bcs-panel));font-size:13.5px;line-height:1.5;color:var(--bcs-ink)}
     644.bcs-smtp-alert .dashicons{color:var(--tone);flex-shrink:0;font-size:18px;width:18px;height:18px;margin-top:1px}
     645.bcs-smtp-alert-content{min-width:0;overflow-wrap:anywhere}
     646.bcs-smtp-alert-content strong{font-weight:600}
     647.bcs-smtp-alert-success{--tone:var(--bcs-sent)}
     648.bcs-smtp-alert-error{--tone:var(--bcs-back)}
     649.bcs-smtp-alert-warning{--tone:var(--bcs-held)}
     650.bcs-smtp-alert .is-good{color:var(--bcs-sent)}
     651.bcs-smtp-alert .is-bad{color:var(--bcs-back)}
     652
     653.bcs-smtp-toast{position:fixed;right:24px;bottom:24px;z-index:100000;max-width:min(420px,calc(100vw - 32px));pointer-events:none}
     654.bcs-smtp-toast .bcs-smtp-alert{pointer-events:auto;box-shadow:0 12px 32px rgba(0,0,0,.45),0 2px 6px rgba(0,0,0,.3);animation:bcs-panel-in .22s var(--bcs-ease)}
     655
     656/* =====================================================================
     657   Responsive
     658   ===================================================================== */
     659@media (max-width:1200px){
     660  .bcs-smtp-col-from{display:none}
     661  .bcs-smtp-hero-facts{grid-template-columns:repeat(3,minmax(0,1fr))}
     662  .bcs-smtp-hero-facts > div:nth-child(4){border-left:0}
     663  .bcs-smtp-hero-facts > div:nth-child(n+4){border-top:1px solid var(--bcs-seam)}
     664}
     665@media (max-width:1100px){
     666  .bcs-smtp-overview-grid,.bcs-smtp-split,.bcs-smtp-alerts-grid{grid-template-columns:minmax(0,1fr)}
     667  .bcs-smtp-split > .bcs-smtp-test,.bcs-smtp-health-side{position:static}
     668}
     669@media (max-width:900px){
     670  .bcs-smtp-hero-main{grid-template-columns:auto minmax(0,1fr)}
     671  .bcs-smtp-hero-actions{grid-column:1/-1;justify-content:flex-start}
     672  .bcs-smtp-figures{grid-template-columns:repeat(2,minmax(0,1fr));row-gap:18px}
     673  .bcs-smtp-figures--5{grid-template-columns:repeat(3,minmax(0,1fr));row-gap:0}
     674  .bcs-smtp-figures--5 > div:nth-child(4){border-left:0}
     675  .bcs-smtp-figures--5 > div:nth-child(n+4){border-top:1px solid var(--bcs-seam)}
     676  .bcs-smtp-report-grid{grid-template-columns:minmax(0,1fr)}
     677}
     678@media (max-width:782px){
     679  .bcs-smtp-wrap{padding:16px 12px 40px 0;font-size:15px}
     680  .bcs-smtp-header h1{font-size:19px}
     681  .bcs-smtp-header-actions{width:100%}
     682  .bcs-smtp-header-actions .bcs-smtp-btn{flex:1}
     683  .bcs-smtp-tabs{width:100%;gap:2px;padding:4px}
     684  .bcs-smtp-tab{height:36px;padding:0 11px;font-size:13.5px}
     685  .bcs-smtp-tab .dashicons{font-size:16px;width:16px;height:16px}
     686  .bcs-smtp-panel{padding:18px}
     687  .bcs-smtp-panel--flush{padding:0}
     688  .bcs-smtp-hero{padding:20px 18px 0}
     689  .bcs-smtp-hero-main{gap:14px 16px;padding-bottom:20px}
     690  .bcs-smtp-hero-copy h2{font-size:21px}
     691  .bcs-smtp-beacon{width:42px;height:42px}
     692  .bcs-smtp-beacon span{width:13px;height:13px}
     693  .bcs-smtp-hero-actions .bcs-smtp-btn{flex:1 1 auto}
     694  .bcs-smtp-hero-facts{grid-template-columns:repeat(2,minmax(0,1fr));margin:0 -18px}
     695  .bcs-smtp-hero-facts > div{padding:12px 18px;border-left:0;border-top:1px solid var(--bcs-seam)}
     696  .bcs-smtp-hero-facts > div:nth-child(-n+2){border-top:0}
     697  .bcs-smtp-hero-facts > div:nth-child(even){border-left:1px solid var(--bcs-seam)}
     698  .bcs-smtp-hero-facts > div:nth-child(n+4){border-top:1px solid var(--bcs-seam)}
     699  .bcs-smtp-hero-facts > div:last-child:nth-child(odd){grid-column:1/-1}
     700  .bcs-smtp-hero-facts dd{white-space:normal;overflow-wrap:anywhere}
     701  .bcs-smtp-linechart{height:200px}
     702  .bcs-smtp-linechart--lg{height:190px}
     703  .bcs-smtp-figures--5{grid-template-columns:repeat(2,minmax(0,1fr));margin:0 -18px 20px}
     704  .bcs-smtp-figures--5 > div{padding:14px 18px}
     705  .bcs-smtp-figures--5 > div:nth-child(odd){border-left:0}
     706  .bcs-smtp-figures--5 > div:nth-child(4){border-left:1px solid var(--bcs-seam)}
     707  .bcs-smtp-figures--5 > div:nth-child(n+3){border-top:1px solid var(--bcs-seam)}
     708  .bcs-smtp-figures--5 > div:last-child{grid-column:1/-1;border-left:0}
     709  .bcs-smtp-report-tools{width:100%}
     710  .bcs-smtp-report-tools .bcs-smtp-segmented{width:100%}
     711  .bcs-smtp-activity-foot{margin:16px -18px -18px!important;padding:14px 18px}
     712  .bcs-smtp-field-row,.bcs-smtp-field-row:has(.bcs-smtp-field--port),.bcs-smtp-field-row:has(> .bcs-smtp-field--grow:first-child):not(:has(.bcs-smtp-field--port)){grid-template-columns:minmax(0,1fr);gap:0}
     713  .bcs-smtp-provider-grid,.bcs-smtp-provider-grid--compact{grid-template-columns:repeat(2,minmax(0,1fr))}
     714  .bcs-smtp-wrap .bcs-smtp-input{min-height:44px;height:44px;font-size:16px}
     715  .bcs-smtp-btn{min-height:44px}
     716  .bcs-smtp-btn-sm{min-height:36px}
     717  .bcs-smtp-facts{grid-template-columns:minmax(0,1fr)}
     718  .bcs-smtp-facts > div:nth-child(odd){padding-right:0}
     719  .bcs-smtp-filters{padding:14px}
     720  .bcs-smtp-filters .bcs-smtp-btn{margin-left:0}
     721  .bcs-smtp-filters .bcs-smtp-select{flex:1 1 140px}
     722  .bcs-smtp-segmented{width:100%}
     723  .bcs-smtp-segmented label{flex:1}
     724  .bcs-smtp-segmented span{justify-content:center;padding:0 8px}
     725
     726  /* Email log becomes stacked rows */
     727  .bcs-smtp-table{table-layout:auto}
     728  .bcs-smtp-table thead{display:none}
     729  .bcs-smtp-table tbody tr{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:4px 8px;padding:12px 14px;border-bottom:1px solid var(--bcs-seam)}
     730  .bcs-smtp-table tbody tr:last-child{border-bottom:0}
     731  .bcs-smtp-table td{display:block;padding:0!important;border:0;width:auto!important;text-align:left}
     732  .bcs-smtp-table td.bcs-smtp-col-date{grid-column:1;grid-row:1}
     733  .bcs-smtp-table td.bcs-smtp-col-status{grid-column:2;grid-row:1;justify-self:end}
     734  .bcs-smtp-table td.bcs-smtp-col-more{grid-column:3;grid-row:1;align-self:center}
     735  .bcs-smtp-table .bcs-smtp-view-btn{height:26px;padding:0 2px 0 6px}
     736  .bcs-smtp-table tbody tr.bcs-smtp-day-row{display:block;padding:0}
     737  .bcs-smtp-table tbody tr.bcs-smtp-day-row th{display:block;padding:14px 14px 8px}
     738  .bcs-smtp-table td.bcs-smtp-col-subject{grid-column:1/-1}
     739  .bcs-smtp-table td.bcs-smtp-col-to{grid-column:1/-1}
     740  .bcs-smtp-table td.bcs-smtp-table-empty{grid-column:1/-1;text-align:center}
     741  .bcs-smtp-pager{padding:12px 14px}
     742  .bcs-smtp-pager-nav{width:100%;justify-content:space-between}
     743  .bcs-smtp-toast{left:16px;right:16px;bottom:16px}
     744}
     745@media (max-width:480px){
     746  .bcs-smtp-figures dd{font-size:24px}
     747}
     748
     749/* Tablet: five-item rows fill as 3 + 2 (two halves) instead of leaving an empty cell. */
     750@media (min-width:783px) and (max-width:1200px){
     751  .bcs-smtp-hero-facts:has(> div:nth-child(5):last-child){grid-template-columns:repeat(6,minmax(0,1fr))}
     752  .bcs-smtp-hero-facts:has(> div:nth-child(5):last-child) > div:nth-child(-n+3){grid-column:span 2}
     753  .bcs-smtp-hero-facts:has(> div:nth-child(5):last-child) > div:nth-child(n+4){grid-column:span 3}
     754}
     755@media (min-width:783px) and (max-width:900px){
     756  .bcs-smtp-figures--5{grid-template-columns:repeat(6,minmax(0,1fr))}
     757  .bcs-smtp-figures--5 > div:nth-child(-n+3){grid-column:span 2}
     758  .bcs-smtp-figures--5 > div:nth-child(n+4){grid-column:span 3}
     759}
     760/* Tablet: the four activity numbers fit on one row; the two side cards sit side by side. */
     761@media (min-width:601px) and (max-width:900px){
     762  .bcs-smtp-figures:not(.bcs-smtp-figures--5){grid-template-columns:repeat(4,minmax(0,1fr))}
     763}
     764@media (min-width:601px) and (max-width:1100px){
     765  .bcs-smtp-side{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:stretch}
     766  .bcs-smtp-side > :only-child{grid-column:1/-1}
     767}
     768/* Tablet email log: the recipient moves under the subject so subjects get the room. */
     769.bcs-smtp-cell-to{display:none}
     770@media (min-width:783px) and (max-width:1100px){
     771  /* Collapsed, not display:none: the day rows span five columns, so all five must still exist. */
     772  .bcs-smtp-table .bcs-smtp-col-to{width:0!important;padding:0!important;font-size:0;border-left:0;border-right:0;overflow:hidden}
     773  .bcs-smtp-cell-to{display:block;margin-top:3px;font-size:12px;color:var(--bcs-ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     774}
     775/* Phone: one fact per line, label left and value right, so values don't break mid-word;
     776   and no desktop/mobile preview switch on a screen that is already phone-sized. */
     777@media (max-width:600px){
     778  .bcs-smtp-hero-facts{grid-template-columns:minmax(0,1fr)}
     779  .bcs-smtp-hero-facts > div{display:flex;align-items:baseline;justify-content:space-between;gap:16px;grid-column:auto!important;border-left:0!important;border-top:1px solid var(--bcs-seam)!important}
     780  .bcs-smtp-hero-facts > div:first-child{border-top:0!important}
     781  .bcs-smtp-hero-facts dt{margin:0;flex-shrink:0}
     782  .bcs-smtp-hero-facts dd{text-align:right;min-width:0}
     783  .bcs-smtp-device{display:none!important}
     784}
     785
     786@media (prefers-reduced-motion:reduce){
     787  .bcs-smtp-tab-pill{transition:none}
     788  .bcs-smtp-wrap *,.bcs-smtp-wrap *::after,.bcs-smtp-toast *{animation:none!important;transition:none!important}
     789}
     790.bcs-smtp-log-retention{margin-bottom:16px}
     791.bcs-smtp-log-retention a{margin-left:6px;font-weight:600}
     792
     793/* Failure alerts card — header and channel list */
     794.bcs-smtp-alerts-card{padding:0;overflow:hidden}
     795.bcs-smtp-alerts-head{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:14px;padding:22px 24px}
     796.bcs-smtp-alerts-badge{display:grid!important;place-items:center;width:42px!important;height:42px!important;border-radius:11px;background:var(--bcs-brand-wash);color:var(--bcs-brand-ink);font-size:20px!important}
     797.bcs-smtp-alerts-intro h2{margin:0 0 3px;font-size:16px;line-height:1.3}
     798.bcs-smtp-alerts-intro h2 label{cursor:pointer}
     799.bcs-smtp-alerts-intro p{font-size:13px!important;color:var(--bcs-ink-2)}
     800.bcs-smtp-alerts-card .bcs-smtp-reveal{padding:0 24px}
     801.bcs-smtp-ch-list-head{display:flex;align-items:center;justify-content:space-between;padding:14px 0 10px;border-top:1px solid var(--bcs-seam);font-size:12px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--bcs-ink-3)}
     802.bcs-smtp-ch-count{font-weight:500;letter-spacing:0;text-transform:none;font-size:12.5px}
     803.bcs-smtp-ch-list{list-style:none;margin:0 0 20px;padding:0;border:1px solid var(--bcs-seam);border-radius:12px;overflow:hidden;background:var(--bcs-bg)}
     804.bcs-smtp-ch{margin:0;border-top:1px solid var(--bcs-seam)}
     805.bcs-smtp-ch:first-child{border-top:0}
     806.bcs-smtp-ch-row{display:flex;align-items:center;gap:12px;padding-right:16px;transition:background var(--bcs-t)}
     807.bcs-smtp-ch-row:hover{background:rgba(255,255,255,.02)}
     808.bcs-smtp-ch.is-open .bcs-smtp-ch-row{background:var(--bcs-field)}
     809.bcs-smtp-ch-main{flex:1;min-width:0;display:flex;align-items:center;gap:12px;padding:12px 4px 12px 16px;border:0;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer}
     810.bcs-smtp-ch-main:focus-visible{outline:2px solid var(--bcs-brand-ink);outline-offset:-2px;border-radius:10px}
     811.bcs-smtp-ch-icon{display:grid!important;place-items:center;width:34px!important;height:34px!important;flex-shrink:0;border-radius:9px;background:var(--bcs-panel);border:1px solid var(--bcs-seam-strong);color:var(--bcs-ink-3);font-size:17px!important;transition:all var(--bcs-t)}
     812.bcs-smtp-ch:not([data-state="off"]) .bcs-smtp-ch-icon{background:var(--bcs-brand);border-color:var(--bcs-brand);color:#fff}
     813.bcs-smtp-ch-text{min-width:0;flex:1;display:flex;flex-direction:column;gap:1px}
     814.bcs-smtp-ch-name{font-size:14px;font-weight:600;color:var(--bcs-ink)}
     815.bcs-smtp-ch-sub{font-size:12.5px;color:var(--bcs-ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     816.bcs-smtp-ch-pill{flex-shrink:0;padding:3px 9px;border-radius:99px;font-size:11.5px;font-weight:600;line-height:1.4;color:var(--bcs-ink-3);background:var(--bcs-field);border:1px solid var(--bcs-seam)}
     817.bcs-smtp-ch[data-state="ready"] .bcs-smtp-ch-pill{color:var(--bcs-ok);background:rgba(52,211,153,.1);border-color:rgba(52,211,153,.28)}
     818.bcs-smtp-ch[data-state="missing"] .bcs-smtp-ch-pill{color:var(--bcs-warn);background:rgba(251,191,36,.1);border-color:rgba(251,191,36,.28)}
     819.bcs-smtp-ch[data-state="bad"] .bcs-smtp-ch-pill{color:var(--bcs-bad);background:rgba(251,113,133,.1);border-color:rgba(251,113,133,.3)}
     820.bcs-smtp-ch-chevron{flex-shrink:0;color:var(--bcs-ink-3);font-size:16px!important;width:16px!important;height:16px!important;transition:transform var(--bcs-t),opacity var(--bcs-t)}
     821.bcs-smtp-ch[data-state="off"] .bcs-smtp-ch-chevron{opacity:0}
     822.bcs-smtp-ch.is-open .bcs-smtp-ch-chevron{transform:rotate(180deg)}
     823.bcs-smtp-ch .bcs-smtp-webhook{margin:0;padding:4px 16px 16px 62px;background:var(--bcs-field)}
     824.bcs-smtp-ch .bcs-smtp-webhook .bcs-smtp-field-label{font-size:12.5px}
     825.bcs-smtp-alerts-card .bcs-smtp-alerts-off{margin:0 24px 20px}
     826.bcs-smtp-alerts-card .bcs-smtp-card-foot{margin:auto 0 0;padding:16px 24px;border-top:1px solid var(--bcs-seam);background:rgba(0,0,0,.14)}
     827.bcs-smtp-alerts-card .bcs-smtp-msg{margin:0 24px 16px}
     828@media (max-width:600px){
     829  .bcs-smtp-alerts-head{padding:18px}
     830  .bcs-smtp-alerts-card .bcs-smtp-reveal{padding:0 18px}
     831  .bcs-smtp-ch-sub{display:none}
     832  .bcs-smtp-ch .bcs-smtp-webhook{padding:4px 14px 14px}
     833  .bcs-smtp-alerts-card .bcs-smtp-card-foot{padding:14px 18px}
     834  .bcs-smtp-alerts-card .bcs-smtp-alerts-off,.bcs-smtp-alerts-card .bcs-smtp-msg{margin-left:18px;margin-right:18px}
     835}
     836
     837/* Connection — header, labelled rows, provider chips, footer */
     838.bcs-smtp-setup{padding:0;overflow:hidden;display:flex;flex-direction:column}
     839.bcs-smtp-setup-head{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:14px;padding:22px 24px}
     840.bcs-smtp-setup-badge,.bcs-smtp-test-badge{display:grid!important;place-items:center;width:42px!important;height:42px!important;border-radius:11px;background:var(--bcs-brand-wash);color:var(--bcs-brand-ink);font-size:20px!important}
     841.bcs-smtp-setup-head h2{margin:0 0 3px;font-size:16px;line-height:1.3}
     842.bcs-smtp-setup-head h2 label{cursor:pointer}
     843.bcs-smtp-setup-head p{font-size:13px!important;color:var(--bcs-ink-2)}
     844.bcs-smtp-provider-chips{display:grid;grid-template-columns:repeat(auto-fill,minmax(138px,1fr));gap:6px}
     845.bcs-smtp-provider-chips .bcs-smtp-provider{min-height:40px;padding:6px 10px;gap:9px;border-radius:9px}
     846.bcs-smtp-provider-chips .bcs-smtp-provider-mark{width:24px;height:24px;border-radius:6px;font-size:10px}
     847.bcs-smtp-provider-chips .bcs-smtp-provider-mark .dashicons{font-size:14px;width:14px;height:14px}
     848.bcs-smtp-provider-chips .bcs-smtp-provider-name{font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     849.bcs-smtp-inline-switch{display:flex;align-items:flex-start;gap:10px;margin:14px 0 0;font-size:13.5px;color:var(--bcs-ink);cursor:pointer;line-height:1.45}
     850.bcs-smtp-toggle--sm{width:34px;height:20px;margin-top:1px}
     851.bcs-smtp-toggle--sm .bcs-smtp-toggle-slider::before{width:14px;height:14px}
     852.bcs-smtp-toggle--sm input:checked + .bcs-smtp-toggle-slider::before{transform:translateX(14px)}
     853.bcs-smtp-setup-foot{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin-top:auto;padding:16px 24px;border-top:1px solid var(--bcs-seam);background:rgba(0,0,0,.14)}
     854.bcs-smtp-setup-foot .bcs-smtp-msg{flex-basis:100%;margin-top:0}
     855.bcs-smtp-test-head{display:flex;gap:14px;align-items:flex-start;margin-bottom:6px}
     856.bcs-smtp-test .bcs-smtp-test-badge{flex-shrink:0;background:rgba(52,211,153,.13);color:var(--bcs-ok)}
     857.bcs-smtp-test-head h2{margin:0 0 3px;font-size:16px}
     858.bcs-smtp-test-head p{font-size:13px!important;color:var(--bcs-ink-2);line-height:1.5}
     859@media (max-width:782px){
     860  .bcs-smtp-setup-head{padding:18px}
     861  .bcs-smtp-setup-body{padding:4px 18px 20px}
     862  .bcs-smtp-setup-body .bcs-smtp-field-row .bcs-smtp-field + .bcs-smtp-field{margin-top:14px!important}
     863  .bcs-smtp-setup-foot{padding:14px 18px}
     864  .bcs-smtp-setup-foot .bcs-smtp-btn{flex:1 1 100%}
     865  .bcs-smtp-provider-chips{grid-template-columns:repeat(2,minmax(0,1fr))}
     866}
     867
     868/* Connection — one compact column */
     869.bcs-smtp-setup-body{display:flex;flex-direction:column;gap:20px;padding:4px 24px 24px}
     870.bcs-smtp-setup-body .bcs-smtp-field,.bcs-smtp-setup-body .bcs-smtp-field-row .bcs-smtp-field{margin-top:0}
     871.bcs-smtp-setup-body .bcs-smtp-field-row{gap:14px}
     872.bcs-smtp-setup-body .bcs-smtp-field-label{margin-bottom:7px}
     873.bcs-smtp-setup-body .bcs-smtp-provider-chips{grid-template-columns:repeat(auto-fill,minmax(118px,1fr))}
     874.bcs-smtp-setup-body .bcs-smtp-provider-note{margin-top:8px!important;padding:0;background:none;border:0;font-size:12.5px!important;color:var(--bcs-ink-3)}
     875.bcs-smtp-setup-body .bcs-smtp-provider-note .dashicons{font-size:15px;width:15px;height:15px;margin-top:1px}
     876.bcs-smtp-setup-body .bcs-smtp-auth-warning{margin:0}
     877.bcs-smtp-setup-body .bcs-smtp-disclosure{margin:0}
     878.bcs-smtp-setup-body .bcs-smtp-disclosure-body .bcs-smtp-field{margin-top:14px}
     879.bcs-smtp-setup-body .bcs-smtp-disclosure-body .bcs-smtp-field-row .bcs-smtp-field{margin-top:0}
     880#bcs-smtp-pw-status:empty,#bcs-smtp-pw-status.is-good{display:none}
     881.bcs-smtp-setup-head{border-bottom:0}
     882.bcs-smtp-setup-body .bcs-smtp-auth-switch{margin:0 0 -6px}
     883.bcs-smtp-provider-picked{display:flex;align-items:center;gap:11px;min-height:44px;padding:7px 14px 7px 9px;border-radius:10px;background:var(--bcs-field);border:1px solid var(--bcs-seam-strong)}
     884.bcs-smtp-provider-picked[hidden]{display:none}
     885.bcs-smtp-provider-picked .bcs-smtp-provider-mark{width:28px;height:28px;border-radius:7px;font-size:10.5px;background:var(--tone);border-color:var(--tone);color:#fff}
     886.bcs-smtp-picked-name{flex:1;min-width:0;font-size:14px;font-weight:600;color:var(--bcs-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     887.bcs-smtp-provider-picked .bcs-smtp-link{font-weight:600}
     888.bcs-smtp-setup .bcs-smtp-smtp-off{margin:0 24px 24px}
     889.bcs-smtp-setup .bcs-smtp-smtp-off .dashicons{color:var(--bcs-warn)}
     890.bcs-smtp-setup-body[hidden]{display:none}
  • bcs-smtp-manager/tags/1.3.0/assets/js/admin.js

    r3652946 r3762759  
    1 /* global bcsSmtpData, jQuery, Chart */
     1/* global bcsSmtpData, jQuery */
    22( function ( $ ) {
    33'use strict';
    44
    5 /* Always dark mode */
    6 document.documentElement.setAttribute( 'data-bcs-smtp-theme', 'dark' );
     5var i18n = ( bcsSmtpData && bcsSmtpData.i18n ) || {};
     6var T = function ( key, fallback ) { return i18n[ key ] || fallback || ''; };
     7
     8/* Minimal sprintf for %s / %1$s placeholders in translated strings. */
     9function fmt( str ) {
     10        var args = Array.prototype.slice.call( arguments, 1 );
     11        var i = 0;
     12        return String( str ).replace( /%(\d+)\$s|%s/g, function ( m, n ) {
     13                return n ? args[ n - 1 ] : args[ i++ ];
     14        } );
     15}
     16
     17/* Storage can throw in private windows or with blocked site data. */
     18var store = {
     19        get: function ( k ) { try { return window.localStorage.getItem( k ); } catch ( e ) { return null; } },
     20        set: function ( k, v ) { try { window.localStorage.setItem( k, v ); } catch ( e ) {} }
     21};
    722
    823/* =====================================================================
    9    Tab Navigation — with persistence
     24   Busy-state + message helpers
    1025   ===================================================================== */
    11 var TAB_KEY = 'bcs_smtp_active_tab';
    12 
    13 function getStoredTab() { return localStorage.getItem( TAB_KEY ) || 'settings'; }
    14 
    15 /* Scroll the tab bar so $tab's left edge aligns with the container's left edge —
    16    keeps the active tab pinned to the start instead of wherever it happened to land. */
    17 function scrollTabIntoViewLeft( $tab, $container ) {
    18         if ( ! $tab.length || ! $container.length ) { return; }
    19         var containerEl = $container[ 0 ];
    20         var delta = $tab[ 0 ].getBoundingClientRect().left - containerEl.getBoundingClientRect().left;
    21         containerEl.scrollLeft += delta;
    22 }
    23 
    24 function setActiveTab( panel ) {
    25         localStorage.setItem( TAB_KEY, panel );
    26         $( '.bcs-smtp-tab' ).removeClass( 'active' );
    27         var $activeTab = $( '.bcs-smtp-tab[data-panel="' + panel + '"]' ).addClass( 'active' );
    28         scrollTabIntoViewLeft( $activeTab, $( '.bcs-smtp-tabs' ) );
     26function setBusy( $btn, label ) {
     27        if ( ! $btn.data( 'bcsLabel' ) ) { $btn.data( 'bcsLabel', $btn.html() ); }
     28        $btn.prop( 'disabled', true ).addClass( 'is-busy' ).attr( 'aria-busy', 'true' ).text( label );
     29}
     30function clearBusy( $btn ) {
     31        $btn.prop( 'disabled', false ).removeClass( 'is-busy' ).removeAttr( 'aria-busy' );
     32        if ( $btn.data( 'bcsLabel' ) ) { $btn.html( $btn.data( 'bcsLabel' ) ); }
     33}
     34
     35function alertHtml( type, msg ) {
     36        var icons = { success: 'yes-alt', error: 'dismiss', warning: 'warning', info: 'info-outline' };
     37        return '<div class="bcs-smtp-alert bcs-smtp-alert-' + type + '">' +
     38                '<span class="dashicons dashicons-' + ( icons[ type ] || icons.info ) + '" aria-hidden="true"></span>' +
     39                '<div class="bcs-smtp-alert-content">' + msg + '</div></div>';
     40}
     41
     42function showInlineMsg( selector, type, msg ) {
     43        var $el = $( selector );
     44        clearTimeout( $el.data( 'bcsHideTimer' ) );
     45        $el.html( alertHtml( type, msg ) );
     46        // Errors and warnings stay until the next message so they can actually be read.
     47        if ( type !== 'success' ) { return; }
     48        $el.data( 'bcsHideTimer', setTimeout( function () { $el.empty(); }, 7000 ) );
     49}
     50
     51function toast( type, msg ) {
     52        var $t = $( '#bcs-smtp-toast' );
     53        clearTimeout( $t.data( 'bcsHideTimer' ) );
     54        $t.html( alertHtml( type, msg ) );
     55        $t.data( 'bcsHideTimer', setTimeout( function () { $t.empty(); }, type === 'success' ? 4000 : 9000 ) );
     56}
     57
     58function errorText( r, fallback ) {
     59        if ( r && r.data ) {
     60                if ( typeof r.data === 'string' ) { return r.data; }
     61                if ( r.data.message ) { return r.data.message; }
     62        }
     63        return fallback;
     64}
     65function failText( xhr ) {
     66        return xhr && xhr.status ? fmt( T( 'requestFailedCode' ), xhr.status ) : T( 'requestFailed' );
     67}
     68
     69/* =====================================================================
     70   Overview health banner — re-rendered from saved state after anything that
     71   changes it (save, health check, test email), so it never shows a stale verdict.
     72   ===================================================================== */
     73var healthDirty = false;
     74var overviewShown = false;
     75
     76function refreshHealthBanner() {
     77        healthDirty = false;
     78        $.post( bcsSmtpData.ajaxurl, { action: 'bcs_smtp_get_health_banner', nonce: bcsSmtpData.nonce }, function ( r ) {
     79                if ( r && r.success && r.data && r.data.html ) {
     80                        $( '#bcs-smtp-health-banner' ).html( r.data.html );
     81                        syncConnectionDot();
     82                }
     83        } );
     84}
     85
     86/* After a save, test or check the banner is re-rendered right away, even from another tab,
     87   so the Connection tab's status dot never shows an old result. */
     88function markHealthDirty() {
     89        refreshHealthBanner();
     90}
     91
     92/* The Connection tab's dot mirrors the Overview banner: green flowing, red failing, amber otherwise */
     93function syncConnectionDot() {
     94        var $hero = $( '#bcs-smtp-health-banner .bcs-smtp-hero' );
     95        if ( ! $hero.length ) { return; }
     96        var tone = ( /bcs-smtp-hero--(ok|bad|warn|off)/.exec( $hero.attr( 'class' ) ) || [] )[ 1 ] || 'off';
     97        $( '#bcs-smtp-conn-dot' ).attr( 'data-tone', 'off' === tone ? 'warn' : tone );
     98        $( '#bcs-smtp-conn-dot-label' ).text( ' (' + $.trim( $hero.find( '#bcs-smtp-hero-title' ).text() ) + ')' );
     99}
     100
     101/* The active tab's highlight slides between tabs instead of jumping */
     102function movePill( instant ) {
     103        var $nav = $( '.bcs-smtp-tabs' );
     104        var tab  = $nav.find( '.bcs-smtp-tab[aria-selected="true"]' )[ 0 ];
     105        var pill = $nav.find( '.bcs-smtp-tab-pill' )[ 0 ];
     106        if ( ! tab || ! pill ) { return; }
     107        $nav.toggleClass( 'is-instant', !! instant ).addClass( 'has-pill' );
     108        pill.style.width     = tab.offsetWidth + 'px';
     109        pill.style.height    = tab.offsetHeight + 'px';
     110        pill.style.transform = 'translate(' + tab.offsetLeft + 'px,' + tab.offsetTop + 'px)';
     111        if ( instant ) { setTimeout( function () { $nav.removeClass( 'is-instant' ); }, 50 ); }
     112}
     113$( window ).on( 'resize', function () { movePill( true ); } );
     114if ( document.fonts && document.fonts.ready ) { document.fonts.ready.then( function () { movePill( true ); } ); }
     115
     116/* =====================================================================
     117   Tabs — ARIA tablist with arrow-key support and persistence
     118   ===================================================================== */
     119var TAB_KEY = 'bcs_smtp_tab_v2';
     120var emailLogLoaded = false;
     121
     122function setActiveTab( panel, focusTab ) {
     123        var $tab = $( '.bcs-smtp-tab[data-panel="' + panel + '"]' );
     124        if ( ! $tab.length ) { panel = 'overview'; $tab = $( '.bcs-smtp-tab[data-panel="overview"]' ); }
     125        store.set( TAB_KEY, panel );
     126        // Keep the URL fragment in step with the active tab, so a refresh (or a copied link)
     127        // opens the tab you were on instead of whichever one the URL happened to name.
     128        if ( window.history && history.replaceState && window.location.hash !== '#' + panel ) {
     129                history.replaceState( null, '', window.location.pathname + window.location.search + '#' + panel );
     130        }
     131
     132        var firstPaint = ! $( '.bcs-smtp-tabs' ).hasClass( 'has-pill' );
     133        $( '.bcs-smtp-tab' ).attr( { 'aria-selected': 'false', tabindex: '-1' } );
     134        $tab.attr( { 'aria-selected': 'true', tabindex: '0' } );
     135        movePill( firstPaint );
     136        if ( focusTab ) { $tab.trigger( 'focus' ); }
     137
     138        var tabsEl = $( '.bcs-smtp-tabs' )[ 0 ];
     139        if ( tabsEl && $tab[ 0 ] ) {
     140                var delta = $tab[ 0 ].getBoundingClientRect().left - tabsEl.getBoundingClientRect().left;
     141                if ( delta < 0 || delta + $tab[ 0 ].offsetWidth > tabsEl.clientWidth ) { tabsEl.scrollLeft += delta - 8; }
     142        }
     143
    29144        $( '.bcs-smtp-tab-panel' ).removeClass( 'active' );
    30145        $( '#bcs-smtp-panel-' + panel ).addClass( 'active' );
    31146
    32         // Lazy-load data on first visit to these panels
    33         if ( panel === 'email-log' && ! emailLogLoaded ) {
    34                 emailLogLoaded = true;
    35                 loadEmailLog();
    36         }
    37         if ( panel === 'email-report' && ! emailReportLoaded ) {
    38                 emailReportLoaded = true;
    39                 loadEmailReport();
    40         }
     147        // Background health checks can change the verdict too, so redraw on every return to Overview.
     148        if ( panel === 'overview' ) { initOverviewChart(); if ( healthDirty || overviewShown ) { refreshHealthBanner(); } overviewShown = true; }
     149        if ( panel === 'email-log' && ! emailLogLoaded ) { emailLogLoaded = true; loadEmailLog(); }
     150        if ( panel === 'reports' ) { initReportChart(); }
    41151}
    42152
    … …  
    45155} );
    46156
    47 $( function () {
    48         var stored = getStoredTab();
    49         if ( stored && $( '.bcs-smtp-tab[data-panel="' + stored + '"]' ).length ) {
    50                 setActiveTab( stored );
    51         }
     157$( document ).on( 'keydown', '.bcs-smtp-tab', function ( e ) {
     158        var $tabs = $( '.bcs-smtp-tab' );
     159        var idx   = $tabs.index( this );
     160        var next  = null;
     161        if ( e.key === 'ArrowRight' ) { next = ( idx + 1 ) % $tabs.length; }
     162        if ( e.key === 'ArrowLeft' )  { next = ( idx - 1 + $tabs.length ) % $tabs.length; }
     163        if ( e.key === 'Home' )       { next = 0; }
     164        if ( e.key === 'End' )        { next = $tabs.length - 1; }
     165        if ( next === null ) { return; }
     166        e.preventDefault();
     167        setActiveTab( $tabs.eq( next ).data( 'panel' ), true );
     168} );
     169
     170/* Any [data-goto] control jumps to a tab; optional data-focus moves focus, data-provider-pick preselects. */
     171$( document ).on( 'click', '[data-goto]', function ( e ) {
     172        e.preventDefault();
     173        var $el = $( this );
     174        var logStatus = $el.data( 'log-status' );
     175        if ( logStatus !== undefined ) {
     176                $( '.bcs-smtp-log-status-radio[value="' + logStatus + '"]' ).prop( 'checked', true );
     177                $( '#bcs-smtp-log-status' ).val( logStatus );
     178                if ( emailLogLoaded ) { loadEmailLog( 1 ); }
     179        }
     180        setActiveTab( $el.data( 'goto' ) );
     181        var pick = $el.data( 'provider-pick' );
     182        if ( pick ) {
     183                $( '.bcs-smtp-provider-radio[value="' + pick + '"]' ).prop( 'checked', true ).trigger( 'change' );
     184        }
     185        var focus = $el.data( 'focus' );
     186        window.scrollTo( { top: 0, behavior: 'auto' } );
     187        if ( focus ) { setTimeout( function () { $( focus ).trigger( 'focus' ); }, 30 ); }
    52188} );
    53189
    54190/* =====================================================================
    55    Accordion toggle
     191   CONNECTION: provider presets
    56192   ===================================================================== */
    57 $( document ).on( 'click', '.bcs-smtp-accordion-head', function () {
    58         $( this ).closest( '.bcs-smtp-accordion' ).toggleClass( 'open' );
    59 } );
    60 
    61 /* =====================================================================
    62    SETTINGS TAB: SMTP toggle → accordion
    63    ===================================================================== */
     193var ENC_PORTS = { ssl: 465, tls: 587, none: 25 };
     194var PRESET_HOSTS = $( '.bcs-smtp-provider-radio' ).not( '[value="other"]' ).map( function () { return $( this ).data( 'host' ); } ).get().filter( Boolean );
     195var PRESET_USERS = $( '.bcs-smtp-provider-radio' ).map( function () { return $( this ).data( 'user' ); } ).get().filter( Boolean );
     196
     197function updateServerSummary() {
     198        var host = $.trim( $( '#bcs-smtp-smtp-host' ).val() );
     199        var port = $( '#bcs-smtp-smtp-port' ).val();
     200        var enc  = ( $( '.bcs-smtp-enc-radio:checked' ).val() || '' ).toUpperCase();
     201        $( '#bcs-smtp-server-summary' ).text( host ? host + ' · ' + port + ' · ' + enc : T( 'pickServer' ) );
     202}
     203
     204function setDisclosure( $d, open ) {
     205        $d.toggleClass( 'is-open', open ).find( '.bcs-smtp-disclosure-head' ).attr( 'aria-expanded', open ? 'true' : 'false' );
     206}
     207
     208$( document ).on( 'click', '.bcs-smtp-disclosure-head', function () {
     209        var $d = $( this ).closest( '.bcs-smtp-disclosure' );
     210        setDisclosure( $d, ! $d.hasClass( 'is-open' ) );
     211} );
     212
     213/* Remember what was typed for each provider while the user clicks around, so switching
     214   to another provider and back never loses a server, port, encryption or username. */
     215var providerMemory  = {};
     216var currentProvider = $( '.bcs-smtp-provider-radio:checked' ).val() || '';
     217
     218function readConnectionFields() {
     219        return {
     220                host : $( '#bcs-smtp-smtp-host' ).val(),
     221                port : $( '#bcs-smtp-smtp-port' ).val(),
     222                enc  : $( '.bcs-smtp-enc-radio:checked' ).val(),
     223                user : $( '#bcs-smtp-smtp-username' ).val()
     224        };
     225}
     226
     227function writeConnectionFields( f ) {
     228        $( '#bcs-smtp-smtp-host' ).val( f.host );
     229        $( '#bcs-smtp-smtp-port' ).val( f.port );
     230        if ( f.enc ) { $( '.bcs-smtp-enc-radio[value="' + f.enc + '"]' ).prop( 'checked', true ); }
     231        $( '#bcs-smtp-smtp-username' ).val( f.user );
     232}
     233
     234/* SMTP switch off: the form folds away, like the Alerts card, and a note says what that means */
    64235$( document ).on( 'change', '#bcs-smtp-smtp-enabled', function () {
    65         $( '#bcs-smtp-smtp-accordion' ).toggleClass( 'open', this.checked );
    66 } );
    67 
    68 /* Encryption radio → auto-fill port */
    69 var ENC_PORTS = { ssl: 465, tls: 587, none: 25 };
     236        $( '#bcs-smtp-setup-body' ).prop( 'hidden', ! this.checked );
     237        $( '#bcs-smtp-smtp-off' ).prop( 'hidden', this.checked );
     238} );
     239
     240/* Provider picker: after a choice, show just that provider with a Change link */
     241function syncPickedProvider() {
     242        var $r = $( '.bcs-smtp-provider-radio:checked' );
     243        if ( ! $r.length ) { return; }
     244        var $chip = $r.closest( '.bcs-smtp-provider' );
     245        $( '#bcs-smtp-picked-mark' ).html( $chip.find( '.bcs-smtp-provider-mark' ).html() ).attr( 'style', $chip.attr( 'style' ) );
     246        $( '#bcs-smtp-picked-name' ).text( $chip.attr( 'title' ) );
     247}
     248function setProviderListOpen( open ) {
     249        if ( ! $( '.bcs-smtp-provider-radio:checked' ).length ) { open = true; } // Nothing chosen yet: the list stays.
     250        $( '#bcs-smtp-provider-chips' ).prop( 'hidden', ! open );
     251        $( '#bcs-smtp-provider-picked' ).prop( 'hidden', open );
     252        $( '#bcs-smtp-provider-change' ).attr( 'aria-expanded', open ? 'true' : 'false' );
     253}
     254$( document ).on( 'click', '#bcs-smtp-provider-change', function () {
     255        setProviderListOpen( true );
     256        $( '.bcs-smtp-provider-radio:checked' ).trigger( 'focus' );
     257} );
     258/* A pointer pick closes the list straight away; keyboard users can arrow through it and it closes when they move on.
     259   Pressing the mouse on a chip moves focus off the current one before the click lands, so focus leaving
     260   during a press inside the list must not close it. */
     261var providerPress = false;
     262$( document ).on( 'mousedown touchstart', '#bcs-smtp-provider-chips', function () { providerPress = true; } );
     263$( document ).on( 'click', '.bcs-smtp-provider', function ( e ) {
     264        if ( e.target.tagName === 'INPUT' ) { return; } // The label's own click already handled this pick.
     265        providerPress = false;
     266        setTimeout( function () { syncPickedProvider(); setProviderListOpen( false ); }, 0 );
     267} );
     268$( document ).on( 'mouseup touchend', function () {
     269        // A press that started in the list but ended outside it picked nothing; forget it.
     270        setTimeout( function () { providerPress = false; }, 0 );
     271} );
     272$( document ).on( 'mousedown touchstart', function ( e ) {
     273        // Clicking anywhere else closes an open list, even when nothing inside it holds focus.
     274        if ( $( '#bcs-smtp-provider-chips' ).prop( 'hidden' ) ) { return; }
     275        if ( $( e.target ).closest( '#bcs-smtp-provider-chips, #bcs-smtp-provider-change' ).length ) { return; }
     276        syncPickedProvider();
     277        setProviderListOpen( false );
     278} );
     279$( document ).on( 'focusout', '#bcs-smtp-provider-chips', function ( e ) {
     280        if ( providerPress ) { return; }
     281        if ( e.relatedTarget && $( e.relatedTarget ).closest( '#bcs-smtp-provider-chips' ).length ) { return; }
     282        syncPickedProvider();
     283        setProviderListOpen( false );
     284} );
     285
     286$( document ).on( 'change', '.bcs-smtp-provider-radio', function () {
     287        var $r      = $( this );
     288        var d       = $r.data();
     289        var key     = $r.val();
     290        var isOther = key === 'other';
     291        var $user   = $( '#bcs-smtp-smtp-username' );
     292
     293        if ( currentProvider && currentProvider !== key ) {
     294                providerMemory[ currentProvider ] = readConnectionFields();
     295        }
     296
     297        if ( providerMemory[ key ] ) {
     298                // Been here before in this visit: put back exactly what was there.
     299                writeConnectionFields( providerMemory[ key ] );
     300        } else {
     301                var f = readConnectionFields();
     302                if ( isOther ) {
     303                        // Bring back the last saved custom server; never carry another provider's server over.
     304                        if ( d.host ) {
     305                                f.host = d.host;
     306                                f.port = d.port;
     307                                f.enc  = d.enc;
     308                        } else if ( PRESET_HOSTS.indexOf( $.trim( f.host ) ) !== -1 ) {
     309                                f.host = '';
     310                        }
     311                } else {
     312                        f.host = d.host;
     313                        f.port = d.port;
     314                        f.enc  = d.enc;
     315                }
     316                // Fixed usernames (SendGrid's "apikey") are set; one left over from such a preset is cleared.
     317                if ( d.user ) {
     318                        f.user = d.user;
     319                } else if ( PRESET_USERS.indexOf( $.trim( f.user ) ) !== -1 ) {
     320                        f.user = '';
     321                }
     322                writeConnectionFields( f );
     323        }
     324        currentProvider = key;
     325
     326        $( '#bcs-smtp-user-hint' ).text( d.userHint || '' );
     327        $user.attr( 'placeholder', d.userHint || '' );
     328        $( '#bcs-smtp-pass-label' ).text( d.passLabel || '' );
     329        $( '#bcs-smtp-provider-note-text' ).text( d.note || '' );
     330        var $link = $( '#bcs-smtp-provider-note-link' );
     331        if ( d.link ) {
     332                $link.attr( 'href', d.link ).prop( 'hidden', false );
     333                $( '#bcs-smtp-provider-note-link-text' ).text( d.linkLabel || '' );
     334        } else {
     335                $link.prop( 'hidden', true );
     336        }
     337        $( '#bcs-smtp-provider-note' ).prop( 'hidden', false );
     338
     339        // "Other", and presets without a fixed server (Amazon SES is per region), need the server typed in.
     340        var needsHost = isOther || ! d.host;
     341        setDisclosure( $( '#bcs-smtp-server-details' ), needsHost || ! $.trim( $( '#bcs-smtp-smtp-host' ).val() ) );
     342        updateServerSummary();
     343
     344        if ( needsHost && ! $.trim( $( '#bcs-smtp-smtp-host' ).val() ) ) {
     345                $( '#bcs-smtp-smtp-host' ).trigger( 'focus' );
     346        } else if ( ! $.trim( $user.val() ) ) {
     347                $user.trigger( 'focus' );
     348        }
     349} );
     350
     351/* Encryption radio → auto-fill port (only when the port is still a standard one) */
    70352$( document ).on( 'change', '.bcs-smtp-enc-radio', function () {
    71         var enc  = $( this ).val();
    72         var $port = $( '#bcs-smtp-smtp-port' );
    73         // Only auto-fill if the user hasn't customised the port
     353        var $port   = $( '#bcs-smtp-smtp-port' );
    74354        var current = parseInt( $port.val(), 10 );
    75         var knownPorts = [ 25, 465, 587 ];
    76         if ( knownPorts.indexOf( current ) !== -1 || ! current ) {
    77                 $port.val( ENC_PORTS[ enc ] || 587 );
    78         }
    79 } );
     355        if ( [ 25, 465, 587 ].indexOf( current ) !== -1 || ! current ) {
     356                $port.val( ENC_PORTS[ $( this ).val() ] || 587 );
     357        }
     358        updateServerSummary();
     359} );
     360$( document ).on( 'input', '#bcs-smtp-smtp-host, #bcs-smtp-smtp-port', updateServerSummary );
    80361
    81362/* Auth toggle → show/hide username + password */
     363/* Warn when sign-in is off for a provider that always requires it */
     364function syncAuthWarning() {
     365        var $p   = $( '.bcs-smtp-provider-radio:checked' );
     366        var need = $p.length && $p.val() !== 'other' && ! $( '#bcs-smtp-smtp-auth' ).is( ':checked' );
     367        if ( need ) {
     368                $( '#bcs-smtp-auth-warning-title' ).text( fmt( T( 'needsAuth', '%s needs a username and password.' ), $.trim( $p.closest( 'label' ).find( '.bcs-smtp-provider-name' ).text() ) ) );
     369        }
     370        $( '#bcs-smtp-auth-warning' ).prop( 'hidden', ! need );
     371}
     372
    82373$( document ).on( 'change', '#bcs-smtp-smtp-auth', function () {
    83         $( '#bcs-smtp-auth-fields' ).toggle( this.checked );
     374        $( '#bcs-smtp-auth-fields' ).prop( 'hidden', ! this.checked );
     375        syncAuthWarning();
     376} );
     377$( document ).on( 'change', '.bcs-smtp-provider-radio', syncAuthWarning );
     378
     379/* Password: change / keep / show */
     380function showSavedPassword() {
     381        $( '#bcs-smtp-pw-input' ).prop( 'hidden', true );
     382        $( '#bcs-smtp-cancel-pw-btn' ).prop( 'hidden', true );
     383        $( '#bcs-smtp-pw-display' ).prop( 'hidden', false );
     384        $( '#bcs-smtp-smtp-password' ).val( '' );
     385        $( '#bcs-smtp-pw-status' ).removeClass( 'is-bad' ).addClass( 'is-good' ).text( T( 'pwSaved' ) );
     386}
     387$( document ).on( 'click', '#bcs-smtp-change-pw-btn', function () {
     388        $( '#bcs-smtp-pw-display' ).prop( 'hidden', true );
     389        $( '#bcs-smtp-pw-input' ).prop( 'hidden', false );
     390        $( '#bcs-smtp-cancel-pw-btn' ).prop( 'hidden', false );
     391        $( '#bcs-smtp-smtp-password' ).val( '' ).trigger( 'focus' );
     392        $( '#bcs-smtp-pw-status' ).removeClass( 'is-good is-bad' ).text( T( 'pwChanging' ) );
     393} );
     394$( document ).on( 'click', '#bcs-smtp-cancel-pw-btn', function ( e ) {
     395        e.preventDefault();
     396        showSavedPassword();
     397        $( '#bcs-smtp-change-pw-btn' ).trigger( 'focus' );
     398} );
     399$( document ).on( 'click', '#bcs-smtp-toggle-pw-btn', function () {
     400        var $input = $( '#bcs-smtp-smtp-password' );
     401        var hidden = $input.attr( 'type' ) === 'password';
     402        $input.attr( 'type', hidden ? 'text' : 'password' );
     403        $( this ).attr( 'aria-label', hidden ? T( 'hidePw' ) : T( 'showPw' ) )
     404                .find( '.dashicons' ).toggleClass( 'dashicons-visibility', ! hidden ).toggleClass( 'dashicons-hidden', hidden );
    84405} );
    85406
    86407/* Save SMTP settings */
     408/* Unsaved changes on the Connection card: only real edits count, not values the page fills in itself */
     409$( document ).on( 'input change', '.bcs-smtp-setup input', function ( e ) {
     410        if ( e.originalEvent ) { $( '#bcs-smtp-smtp-unsaved' ).prop( 'hidden', false ); }
     411} );
     412
    87413$( document ).on( 'click', '#bcs-smtp-save-smtp-btn', function () {
    88414        var $btn      = $( this );
    89         var hasHost   = $( '#bcs-smtp-smtp-host' ).val().trim() !== '';
     415        var hasHost   = $.trim( $( '#bcs-smtp-smtp-host' ).val() ) !== '';
    90416        var isEnabled = $( '#bcs-smtp-smtp-enabled' ).is( ':checked' );
    91         var btnLabel  = '<span class="dashicons dashicons-saved"></span> Save SMTP Settings';
    92 
    93         $btn.prop( 'disabled', true ).html(
    94                 '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> ' +
    95                 ( isEnabled && hasHost ? 'Saving & testing connection…' : 'Saving…' )
    96         );
     417        var newPw     = $( '#bcs-smtp-smtp-password' ).val();
     418
     419        setBusy( $btn, isEnabled && hasHost ? T( 'savingTesting' ) : T( 'saving' ) );
    97420
    98421        $.post( bcsSmtpData.ajaxurl, {
    … …  
    108431                smtp_auth       : $( '#bcs-smtp-smtp-auth' ).is( ':checked' ) ? 1 : 0,
    109432                smtp_username   : $( '#bcs-smtp-smtp-username' ).val(),
    110                 smtp_password   : $( '#bcs-smtp-smtp-password' ).val(),
     433                smtp_password   : newPw,
     434                provider        : $( '.bcs-smtp-provider-radio:checked' ).val() || '',
    111435        }, function ( r ) {
    112                 $btn.prop( 'disabled', false ).html( btnLabel );
    113 
     436                clearBusy( $btn );
    114437                if ( ! r || ! r.success ) {
    115                         var errMsg = ( r && r.data ) ? r.data : 'Save failed — check server error logs.';
    116                         showInlineMsg( '#bcs-smtp-smtp-msg', 'error', errMsg );
     438                        showInlineMsg( '#bcs-smtp-smtp-msg', 'error', errorText( r, T( 'saveFailed' ) ) );
    117439                        return;
    118440                }
    119 
    120                 // Update password display after save
    121                 var newPw = $( '#bcs-smtp-smtp-password' ).val();
    122                 if ( newPw !== '' && r.data.password_set ) {
    123                         // New password saved — switch back to masked display + Change button
    124                         $( '#bcs-smtp-pw-display' )
    125                                 .css( 'display', 'flex' )
    126                                 .html(
    127                                         '<input type="password" class="bcs-smtp-input" value="placeholder_pw" disabled style="flex:1" aria-label="Saved password (masked)">' +
    128                                         '<button type="button" id="bcs-smtp-change-pw-btn" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm" style="flex-shrink:0;white-space:nowrap">' +
    129                                         '<span class="dashicons dashicons-edit"></span> Change</button>'
    130                                 );
    131                         $( '#bcs-smtp-pw-input' ).hide();
    132                         $( '#bcs-smtp-smtp-password' ).val( '' );
    133                         $( '#bcs-smtp-pw-status' )
    134                                 .text( '✓ Password saved — leave blank to keep it.' )
    135                                 .css( 'color', 'var(--bcs-smtp-emerald)' );
    136                 }
    137 
    138                 // Show save result
     441                if ( newPw !== '' && r.data.password_set ) { showSavedPassword(); }
     442                $( '#bcs-smtp-smtp-unsaved' ).prop( 'hidden', true );
     443
    139444                var msg = '<strong>' + r.data.message + '</strong>';
    140 
    141                 // Show connection test result if returned
    142445                if ( r.data.connection ) {
    143446                        var conn = r.data.connection;
    144                         if ( conn.success ) {
    145                                 msg += '<br><span style="color:var(--bcs-smtp-emerald)">✓ ' + conn.message + '</span>';
    146                         } else {
    147                                 msg += '<br><span style="color:var(--bcs-smtp-rose)">✗ Connection failed: ' + conn.message + '</span>';
    148                         }
    149                         // Use warning type if connection failed (settings saved but not reachable)
     447                        msg += conn.success
     448                                ? '<br><span class="is-good">' + conn.message + '</span>'
     449                                : '<br><span class="is-bad">' + T( 'connFailed' ) + ' ' + conn.message + '</span>';
    150450                        showInlineMsg( '#bcs-smtp-smtp-msg', conn.success ? 'success' : 'warning', msg );
     451                        markHealthDirty();
     452                        if ( conn.success ) { $( '#bcs-smtp-send-test-btn' ).trigger( 'focus' ); }
    151453                } else {
    152454                        showInlineMsg( '#bcs-smtp-smtp-msg', 'success', msg );
    153455                }
    154456        } ).fail( function ( xhr ) {
    155                 $btn.prop( 'disabled', false ).html( btnLabel );
    156                 var detail = xhr.status ? ' (HTTP ' + xhr.status + ')' : '';
    157                 showInlineMsg( '#bcs-smtp-smtp-msg', 'error', 'Request failed' + detail + ' — check server error logs or enable WP_DEBUG.' );
     457                clearBusy( $btn );
     458                showInlineMsg( '#bcs-smtp-smtp-msg', 'error', failText( xhr ) );
    158459        } );
    159460} );
    160461
    161462/* =====================================================================
    162    EMAIL LOG TAB
     463   Test email + health check
    163464   ===================================================================== */
    164 var emailLogLoaded = false;
     465$( document ).on( 'click', '#bcs-smtp-send-test-btn', function () {
     466        var $btn = $( this );
     467        var to   = $.trim( $( '#bcs-smtp-test-to' ).val() );
     468        if ( ! to ) {
     469                showInlineMsg( '#bcs-smtp-test-msg', 'error', T( 'enterRecipient' ) );
     470                $( '#bcs-smtp-test-to' ).trigger( 'focus' );
     471                return;
     472        }
     473        setBusy( $btn, T( 'sending' ) );
     474        $.post( bcsSmtpData.ajaxurl, { action: 'bcs_smtp_send_test', nonce: bcsSmtpData.nonce, to: to }, function ( r ) {
     475                clearBusy( $btn );
     476                if ( r && r.success ) {
     477                        showInlineMsg( '#bcs-smtp-test-msg', 'success', r.data.message );
     478                        return;
     479                }
     480                // Error payload is either { message, failure_type } or a plain string (validation / rate limit).
     481                var d   = r ? r.data : null;
     482                var msg = ! d ? T( 'testFailed' )
     483                        : ( typeof d === 'string' ? d
     484                        : ( d.failure_type ? '<strong>' + d.failure_type + ':</strong> ' + d.message : d.message ) );
     485                showInlineMsg( '#bcs-smtp-test-msg', 'error', msg );
     486        } ).fail( function ( xhr ) {
     487                clearBusy( $btn );
     488                showInlineMsg( '#bcs-smtp-test-msg', 'error', failText( xhr ) );
     489        } );
     490} );
     491
     492$( document ).on( 'click', '[data-check-now]', function () {
     493        var $btn  = $( this );
     494        var msgEl = $btn.data( 'msg' );
     495        setBusy( $btn, T( 'checking' ) );
     496        $.post( bcsSmtpData.ajaxurl, { action: 'bcs_smtp_check_now', nonce: bcsSmtpData.nonce }, function ( r ) {
     497                clearBusy( $btn );
     498                if ( r && r.success ) {
     499                        showInlineMsg( msgEl, r.data.success ? 'success' : 'error', r.data.message );
     500                        markHealthDirty();
     501                } else {
     502                        showInlineMsg( msgEl, 'error', errorText( r, T( 'checkFailed' ) ) );
     503                }
     504        } ).fail( function ( xhr ) {
     505                clearBusy( $btn );
     506                showInlineMsg( msgEl, 'error', failText( xhr ) );
     507        } );
     508} );
     509
     510/* =====================================================================
     511   EMAIL LOG
     512   ===================================================================== */
    165513var emailLogPage      = 1;
    166514var emailLogSearchTmr = null;
    167515
    168516function emailLogSkeletonHtml() {
    169         var widths = [ 85, 150, 120, 120, 64, 90, 70 ];
    170         var row    = '<tr class="bcs-smtp-skeleton-row">' + widths.map( function ( w ) {
    171                 return '<td><span class="bcs-smtp-skel" style="width:' + w + 'px"></span></td>';
     517        var widths = [ 60, 78, 66, 54, 30 ];
     518        var row = '<tr class="bcs-smtp-skeleton-row" aria-hidden="true">' + widths.map( function ( w ) {
     519                return '<td><span class="bcs-smtp-skel" style="width:' + w + '%"></span></td>';
    172520        } ).join( '' ) + '</tr>';
    173521        return new Array( 6 ).fill( row ).join( '' );
    … …  
    178526        emailLogPage = page;
    179527        var $tbody = $( '#bcs-smtp-log-tbody' );
    180         $tbody.html( emailLogSkeletonHtml() );
     528        $tbody.attr( 'aria-busy', 'true' ).html( emailLogSkeletonHtml() );
    181529
    182530        $.post( bcsSmtpData.ajaxurl, {
    183                 action      : 'bcs_smtp_get_email_log',
    184                 nonce       : bcsSmtpData.nonce,
    185                 page        : page,
    186                 per_page    : parseInt( $( '#bcs-smtp-log-per-page' ).val(), 10 ) || 10,
    187                 status      : $( '#bcs-smtp-log-status' ).val(),
    188                 search      : $( '#bcs-smtp-log-search' ).val(),
    189                 date_range  : $( '#bcs-smtp-log-date-range' ).val(),
     531                action     : 'bcs_smtp_get_email_log',
     532                nonce      : bcsSmtpData.nonce,
     533                page       : page,
     534                per_page   : parseInt( $( '#bcs-smtp-log-per-page' ).val(), 10 ) || 25,
     535                status     : $( '#bcs-smtp-log-status' ).val(),
     536                search     : $( '#bcs-smtp-log-search' ).val(),
     537                date_range : $( '#bcs-smtp-log-date-range' ).val(),
    190538        }, function ( r ) {
     539                $tbody.removeAttr( 'aria-busy' );
    191540                if ( r && r.success ) {
    192541                        $tbody.html( r.data.html );
    193                         var info = 'Page ' + r.data.page + ' of ' + ( r.data.pages || 1 ) + ' — ' + r.data.total + ' email(s)';
    194                         $( '#bcs-smtp-log-page-info' ).text( info );
     542                        $.each( r.data.counts || {}, function ( key, n ) {
     543                                $( '.bcs-smtp-seg-count[data-count="' + key + '"]' ).text( n );
     544                        } );
     545                        $( '#bcs-smtp-log-page-info' ).text( fmt( T( 'pageInfo' ), r.data.page, r.data.pages || 1, r.data.total ) );
    195546                        $( '#bcs-smtp-log-prev' ).prop( 'disabled', page <= 1 );
    196547                        $( '#bcs-smtp-log-next' ).prop( 'disabled', page >= r.data.pages );
    197548                } else {
    198                         $tbody.html( '<tr><td colspan="7" style="text-align:center;padding:20px;color:var(--bcs-smtp-text-3)">Error loading log.</td></tr>' );
     549                        $tbody.html( '<tr><td colspan="5" class="bcs-smtp-table-empty">' + T( 'loadError' ) + '</td></tr>' );
    199550                }
    200         } ).fail( function () {
    201                 $tbody.html( '<tr><td colspan="7" style="text-align:center;padding:20px;color:var(--bcs-smtp-text-3)">Request failed.</td></tr>' );
    202         } );
    203 }
     551        } ).fail( function ( xhr ) {
     552                $tbody.removeAttr( 'aria-busy' ).html( '<tr><td colspan="5" class="bcs-smtp-table-empty">' + failText( xhr ) + '</td></tr>' );
     553        } );
     554}
     555
     556/* Full email view: size the preview to its content; switch between preview and HTML source */
     557function fitMailFrame( frame ) {
     558        try {
     559                frame.contentDocument.documentElement.style.overflow = 'hidden'; // The frame grows to fit, so no inner scrollbar.
     560                frame.style.height = '0px'; // Collapse first, or a short email reports the frame's own height.
     561                frame.style.height = Math.max( 160, frame.contentDocument.documentElement.scrollHeight ) + 'px';
     562        } catch ( e ) {} // Not readable: keep the default height.
     563}
     564$( '.bcs-smtp-mailframe' ).each( function () {
     565        var frame = this;
     566        frame.addEventListener( 'load', function () { fitMailFrame( frame ); } );
     567        if ( frame.contentDocument && 'complete' === frame.contentDocument.readyState ) { fitMailFrame( frame ); }
     568} );
     569$( window ).on( 'resize', function () { $( '.bcs-smtp-mailframe' ).each( function () { fitMailFrame( this ); } ); } );
     570$( document ).on( 'change', 'input[name="bcs_smtp_mailview_mode"]', function () {
     571        var mode = this.value;
     572        $( '.bcs-smtp-mailview-message [data-mode]' ).each( function () {
     573                $( this ).prop( 'hidden', $( this ).data( 'mode' ) !== mode );
     574        } );
     575        if ( 'preview' === mode ) { $( '.bcs-smtp-mailframe' ).each( function () { fitMailFrame( this ); } ); }
     576} );
     577$( document ).on( 'change', 'input[name="bcs_smtp_mailview_device"]', function () {
     578        $( '.bcs-smtp-mailframe-stage' ).toggleClass( 'is-mobile', 'mobile' === this.value );
     579        $( '.bcs-smtp-mailframe' ).each( function () { fitMailFrame( this ); } );
     580} );
     581
     582$( document ).on( 'click', '.bcs-smtp-copy-btn', function () {
     583        var $btn = $( this );
     584        var text = $btn.closest( '.bcs-smtp-log-error, .bcs-smtp-mailview-source' ).find( 'pre' ).text();
     585        var done = function () {
     586                $btn.find( '.bcs-smtp-copy-label' ).text( T( 'copied' ) );
     587                setTimeout( function () { $btn.find( '.bcs-smtp-copy-label' ).text( T( 'copy' ) ); }, 1600 );
     588        };
     589        if ( navigator.clipboard && window.isSecureContext ) {
     590                navigator.clipboard.writeText( text ).then( done );
     591        } else {
     592                var ta = $( '<textarea readonly>' ).val( text ).css( { position: 'fixed', opacity: 0 } ).appendTo( 'body' );
     593                ta[ 0 ].select();
     594                document.execCommand( 'copy' );
     595                ta.remove();
     596                done();
     597        }
     598} );
    204599
    205600$( document ).on( 'input', '#bcs-smtp-log-search', function () {
    206601        clearTimeout( emailLogSearchTmr );
    207         emailLogSearchTmr = setTimeout( function () { loadEmailLog( 1 ); }, 400 );
     602        emailLogSearchTmr = setTimeout( function () { loadEmailLog( 1 ); }, 350 );
    208603} );
    209604$( document ).on( 'keydown', '#bcs-smtp-log-search', function ( e ) {
    … …  
    214609        }
    215610} );
    216 $( document ).on( 'change', '#bcs-smtp-log-date-range, #bcs-smtp-log-status, #bcs-smtp-log-per-page', function () {
     611$( document ).on( 'change', '.bcs-smtp-log-status-radio', function () {
     612        $( '#bcs-smtp-log-status' ).val( $( this ).val() );
    217613        loadEmailLog( 1 );
    218614} );
    219 $( document ).on( 'click', '#bcs-smtp-log-prev', function () {
    220         if ( emailLogPage > 1 ) { loadEmailLog( emailLogPage - 1 ); }
    221 } );
    222 $( document ).on( 'click', '#bcs-smtp-log-next', function () {
    223         loadEmailLog( emailLogPage + 1 );
    224 } );
    225 
     615$( document ).on( 'change', '#bcs-smtp-log-date-range, #bcs-smtp-log-per-page', function () { loadEmailLog( 1 ); } );
     616$( document ).on( 'click', '#bcs-smtp-log-prev', function () { if ( emailLogPage > 1 ) { loadEmailLog( emailLogPage - 1 ); } } );
     617$( document ).on( 'click', '#bcs-smtp-log-next', function () { loadEmailLog( emailLogPage + 1 ); } );
     618
     619$( document ).on( 'click', '[data-export-log]', function () {
     620        window.location.href = bcsSmtpData.exportUrl + '&nonce=' + encodeURIComponent( bcsSmtpData.exportNonce ) + '&date_range=30days&status=all&search=';
     621} );
     622
     623$( document ).on( 'click', '#bcs-smtp-export-csv-btn', function ( e ) {
     624        e.preventDefault();
     625        window.location.href = bcsSmtpData.exportUrl
     626                + '&nonce='      + encodeURIComponent( bcsSmtpData.exportNonce )
     627                + '&date_range=' + encodeURIComponent( $( '#bcs-smtp-log-date-range' ).val() )
     628                + '&status='     + encodeURIComponent( $( '#bcs-smtp-log-status' ).val() || 'all' )
     629                + '&search='     + encodeURIComponent( $( '#bcs-smtp-log-search' ).val() );
     630} );
    226631
    227632/* =====================================================================
    228    EMAIL REPORT TAB
     633   OVERVIEW — email activity line chart (dependency-free SVG)
    229634   ===================================================================== */
    230 var emailReportLoaded = false;
    231 var reportChart       = null;
     635var SVGNS = 'http://www.w3.org/2000/svg';
     636
     637function dayLabel( ymd, opts ) {
     638        var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec( ymd );
     639        if ( ! m ) { return ymd; }
     640        return new Date( +m[ 1 ], m[ 2 ] - 1, +m[ 3 ] ).toLocaleDateString( document.documentElement.lang || undefined, opts || { month: 'short', day: 'numeric' } );
     641}
     642
     643/* A "nice" axis: 4 steps of 1, 2, 2.5 or 5 × 10^n, never below 1 per step. */
     644function niceStep( max ) {
     645        var raw = Math.max( 1, max / 4 );
     646        var pow = Math.pow( 10, Math.floor( Math.log10( raw ) ) );
     647        var n   = raw / pow;
     648        var m   = n <= 1 ? 1 : n <= 2 ? 2 : n <= 2.5 ? 2.5 : n <= 5 ? 5 : 10;
     649        return Math.max( 1, Math.ceil( m * pow ) );
     650}
     651
     652/* Monotone cubic interpolation (Fritsch–Carlson) — smooth without overshooting below zero. */
     653function smoothPath( pts ) {
     654        var n = pts.length;
     655        if ( n < 2 ) { return n ? 'M' + pts[ 0 ][ 0 ] + ',' + pts[ 0 ][ 1 ] : ''; }
     656        var dx = [], dy = [], s = [], t = [];
     657        for ( var i = 0; i < n - 1; i++ ) {
     658                dx[ i ] = pts[ i + 1 ][ 0 ] - pts[ i ][ 0 ];
     659                dy[ i ] = pts[ i + 1 ][ 1 ] - pts[ i ][ 1 ];
     660                s[ i ]  = dy[ i ] / dx[ i ];
     661        }
     662        t[ 0 ] = s[ 0 ];
     663        t[ n - 1 ] = s[ n - 2 ];
     664        for ( i = 1; i < n - 1; i++ ) {
     665                t[ i ] = ( s[ i - 1 ] * s[ i ] <= 0 ) ? 0 : 3 * ( dx[ i - 1 ] + dx[ i ] ) / ( ( 2 * dx[ i ] + dx[ i - 1 ] ) / s[ i - 1 ] + ( dx[ i ] + 2 * dx[ i - 1 ] ) / s[ i ] );
     666        }
     667        var d = 'M' + pts[ 0 ][ 0 ].toFixed( 1 ) + ',' + pts[ 0 ][ 1 ].toFixed( 1 );
     668        for ( i = 0; i < n - 1; i++ ) {
     669                var h = dx[ i ] / 3;
     670                d += 'C' + ( pts[ i ][ 0 ] + h ).toFixed( 1 ) + ',' + ( pts[ i ][ 1 ] + h * t[ i ] ).toFixed( 1 ) + ' ' +
     671                        ( pts[ i + 1 ][ 0 ] - h ).toFixed( 1 ) + ',' + ( pts[ i + 1 ][ 1 ] - h * t[ i + 1 ] ).toFixed( 1 ) + ' ' +
     672                        pts[ i + 1 ][ 0 ].toFixed( 1 ) + ',' + pts[ i + 1 ][ 1 ].toFixed( 1 );
     673        }
     674        return d;
     675}
     676
     677function svgEl( name, attrs, parent ) {
     678        var el = document.createElementNS( SVGNS, name );
     679        for ( var k in attrs ) { if ( attrs.hasOwnProperty( k ) ) { el.setAttribute( k, attrs[ k ] ); } }
     680        if ( parent ) { parent.appendChild( el ); }
     681        return el;
     682}
     683
     684function LineChart( el ) {
     685        this.el   = el;
     686        this.tip  = el.querySelector( '.bcs-smtp-linechart-tip' );
     687        this.idx  = -1;
     688        this.data = null;
     689        var self  = this;
     690
     691        el.addEventListener( 'mousemove', function ( e ) { self.pointAt( e.clientX ); } );
     692        el.addEventListener( 'mouseleave', function () { self.hide(); } );
     693        el.addEventListener( 'blur', function () { self.hide(); } );
     694        el.addEventListener( 'keydown', function ( e ) {
     695                if ( ! self.data ) { return; }
     696                var n = self.data.labels.length;
     697                if ( e.key === 'ArrowRight' || e.key === 'ArrowLeft' ) {
     698                        e.preventDefault();
     699                        var i = self.idx < 0 ? n - 1 : self.idx + ( e.key === 'ArrowRight' ? 1 : -1 );
     700                        self.show( Math.max( 0, Math.min( n - 1, i ) ) );
     701                } else if ( e.key === 'Escape' ) {
     702                        self.hide();
     703                }
     704        } );
     705        if ( window.ResizeObserver ) {
     706                var lastW = 0;
     707                new ResizeObserver( function () {
     708                        var w = el.clientWidth;
     709                        if ( self.data && w && Math.abs( w - lastW ) > 2 ) { lastW = w; self.render( self.data, true ); }
     710                } ).observe( el );
     711        }
     712}
     713
     714/* Long ranges read better per week than as daily spikes. */
     715function bucketWeekly( data ) {
     716        if ( data.labels.length <= 45 ) { return data; }
     717        var size = data.labels.length > 200 ? 14 : 7;
     718        var out = { labels: [], success: [], failed: [], unconfirmed: [], weekly: true, span: size };
     719        for ( var i = 0; i < data.labels.length; i += size ) {
     720                out.labels.push( data.labels[ i ] );
     721                [ 'success', 'failed', 'unconfirmed' ].forEach( function ( k ) {
     722                        out[ k ].push( data[ k ].slice( i, i + size ).reduce( function ( a, b ) { return a + ( +b ); }, 0 ) );
     723                } );
     724        }
     725        return out;
     726}
     727
     728LineChart.prototype.render = function ( data, quiet ) {
     729        data = bucketWeekly( data );
     730        this.data = data;
     731        this.idx  = -1;
     732        var el = this.el;
     733        var old = el.querySelector( 'svg' );
     734        if ( old ) { el.removeChild( old ); }
     735
     736        var W = el.clientWidth, H = el.clientHeight;
     737        if ( ! W || ! H ) { return; }
     738        var pad = { l: 40, r: 12, t: 12, b: 28 };
     739        var n   = data.labels.length;
     740        var series = [ { key: 'success', cls: 'is-sent', color: 'var(--bcs-brand-ink)' } ];
     741        if ( data.failed.some( function ( v ) { return v > 0; } ) ) { series.push( { key: 'failed', cls: 'is-failed', color: 'var(--bcs-bad)' } ); }
     742        if ( data.unconfirmed.some( function ( v ) { return v > 0; } ) ) { series.push( { key: 'unconfirmed', cls: 'is-unconfirmed', color: 'var(--bcs-warn)' } ); }
     743
     744        var max = 0;
     745        series.forEach( function ( s ) { data[ s.key ].forEach( function ( v ) { max = Math.max( max, v ); } ); } );
     746        var step = niceStep( max ), top = step * 4;
     747        var iw = W - pad.l - pad.r, ih = H - pad.t - pad.b;
     748        var x = function ( i ) { return pad.l + ( n > 1 ? i * iw / ( n - 1 ) : iw / 2 ); };
     749        var y = function ( v ) { return pad.t + ih - ( v / top ) * ih; };
     750        this.geom = { x: x, y: y, pad: pad, iw: iw, n: n, series: series };
     751
     752        var svg = svgEl( 'svg', { viewBox: '0 0 ' + W + ' ' + H, 'aria-hidden': 'true', focusable: 'false' } );
     753        if ( quiet ) { svg.setAttribute( 'class', 'is-quiet' ); }
     754
     755        var grid = svgEl( 'g', { 'class': 'bcs-smtp-lc-grid' }, svg );
     756        for ( var g = 0; g <= 4; g++ ) {
     757                var gy = y( step * g );
     758                svgEl( 'line', { x1: pad.l, x2: W - pad.r, y1: gy, y2: gy, 'class': g === 0 ? 'is-base' : '' }, grid );
     759                var lbl = svgEl( 'text', { x: pad.l - 10, y: gy + 4, 'text-anchor': 'end', 'class': 'bcs-smtp-lc-label' }, grid );
     760                lbl.textContent = ( step * g ).toLocaleString();
     761        }
     762        var ticks = n > 1 ? [ 0, Math.round( ( n - 1 ) / 4 ), Math.round( ( n - 1 ) / 2 ), Math.round( 3 * ( n - 1 ) / 4 ), n - 1 ] : [ 0 ];
     763        ticks.filter( function ( v, i, a ) { return a.indexOf( v ) === i; } ).forEach( function ( i, k, arr ) {
     764                var anchor = k === 0 ? 'start' : ( k === arr.length - 1 ? 'end' : 'middle' );
     765                var t = svgEl( 'text', { x: x( i ), y: H - 6, 'text-anchor': anchor, 'class': 'bcs-smtp-lc-label' }, grid );
     766                t.textContent = dayLabel( data.labels[ i ] );
     767        } );
     768
     769        // Area under "sent", then each line on top.
     770        var sentPts = data.success.map( function ( v, i ) { return [ x( i ), y( v ) ]; } );
     771        var line0 = smoothPath( sentPts );
     772        if ( n > 1 ) {
     773                svgEl( 'path', { d: line0 + 'L' + x( n - 1 ) + ',' + y( 0 ) + 'L' + x( 0 ) + ',' + y( 0 ) + 'Z', 'class': 'bcs-smtp-lc-area' }, svg );
     774        }
     775        var paths = [];
     776        series.slice().reverse().forEach( function ( s ) {
     777                var pts = data[ s.key ].map( function ( v, i ) { return [ x( i ), y( v ) ]; } );
     778                paths.push( svgEl( 'path', { d: smoothPath( pts ), 'class': 'bcs-smtp-lc-line ' + s.cls }, svg ) );
     779        } );
     780
     781        this.cursor = svgEl( 'line', { x1: 0, x2: 0, y1: pad.t, y2: pad.t + ih, 'class': 'bcs-smtp-lc-cursor' }, svg );
     782        this.dots = series.map( function ( s ) { return svgEl( 'circle', { r: 4.5, cx: 0, cy: 0, fill: s.color, 'class': 'bcs-smtp-lc-dot' }, svg ); } );
     783
     784        el.insertBefore( svg, el.firstChild );
     785        paths.forEach( function ( p ) {
     786                var len = p.getTotalLength ? Math.ceil( p.getTotalLength() ) : 0;
     787                p.style.setProperty( '--len', len );
     788                if ( quiet ) { p.style.animation = 'none'; p.style.strokeDashoffset = 0; }
     789        } );
     790        this.hide();
     791};
     792
     793LineChart.prototype.pointAt = function ( clientX ) {
     794        if ( ! this.geom ) { return; }
     795        var r = this.el.getBoundingClientRect();
     796        var g = this.geom;
     797        var i = g.n > 1 ? Math.round( ( clientX - r.left - g.pad.l ) / ( g.iw / ( g.n - 1 ) ) ) : 0;
     798        this.show( Math.max( 0, Math.min( g.n - 1, i ) ) );
     799};
     800
     801LineChart.prototype.show = function ( i ) {
     802        var g = this.geom, d = this.data;
     803        if ( ! g ) { return; }
     804        this.idx = i;
     805        var cx = g.x( i );
     806        this.cursor.setAttribute( 'x1', cx );
     807        this.cursor.setAttribute( 'x2', cx );
     808        g.series.forEach( function ( s, k ) {
     809                this.dots[ k ].setAttribute( 'cx', cx );
     810                this.dots[ k ].setAttribute( 'cy', g.y( d[ s.key ][ i ] ) );
     811        }, this );
     812        this.el.classList.add( 'is-hover' );
     813
     814        var rows = [
     815                [ 'sent', T( 'sent', 'Sent' ), d.success[ i ] ],
     816                [ 'bad', T( 'failed', 'Failed' ), d.failed[ i ] ],
     817                [ 'warn', T( 'unconfirmed', 'Unconfirmed' ), d.unconfirmed[ i ] ],
     818        ];
     819        var when = d.weekly ? fmt( d.span === 14 ? T( 'twoWeeksOf', '2 weeks from %s' ) : T( 'weekOf', 'Week of %s' ), dayLabel( d.labels[ i ] ) ) : dayLabel( d.labels[ i ], { weekday: 'short', month: 'short', day: 'numeric' } );
     820        var html = '<strong>' + when + '</strong>';
     821        rows.forEach( function ( r ) {
     822                html += '<span><i class="bcs-smtp-key bcs-smtp-key--' + r[ 0 ] + '"></i>' + r[ 1 ] + '<b>' + Number( r[ 2 ] ).toLocaleString() + '</b></span>';
     823        } );
     824        this.tip.innerHTML = html;
     825        this.tip.hidden = false;
     826        var w = this.tip.offsetWidth, W = this.el.clientWidth;
     827        this.tip.style.left = Math.max( w / 2, Math.min( W - w / 2, cx ) ) + 'px';
     828};
     829
     830LineChart.prototype.hide = function () {
     831        this.idx = -1;
     832        this.el.classList.remove( 'is-hover' );
     833        if ( this.tip ) { this.tip.hidden = true; }
     834};
     835
     836var ovChart = null;
     837
     838function updateOverviewFigures( sum ) {
     839        var nf = function ( v ) { return Number( v ).toLocaleString(); };
     840        var $f = $( '#bcs-smtp-ov-figures' );
     841        $f.find( '[data-fig="success"]' ).text( nf( sum.success ) );
     842        $f.find( '[data-fig="failed"]' ).text( nf( sum.failed ) ).toggleClass( 'is-bad', sum.failed > 0 );
     843        $f.find( '[data-fig="unconfirmed"]' ).text( nf( sum.unconfirmed ) );
     844        $f.find( '[data-fig="rate"]' ).html( sum.total > 0
     845                ? ( Math.round( sum.success / sum.total * 1000 ) / 10 ).toLocaleString() + '<small>%</small>'
     846                : '—' );
     847        $( '#bcs-smtp-ov-empty' ).prop( 'hidden', sum.total > 0 );
     848}
     849
     850$( document ).on( 'change', '.bcs-smtp-ov-range', function () {
     851        var range = $( this ).val();
     852        $( '#bcs-smtp-ov-chart' ).attr( 'aria-busy', 'true' );
     853        $.post( bcsSmtpData.ajaxurl, { action: 'bcs_smtp_get_email_report', nonce: bcsSmtpData.nonce, date_range: range }, function ( r ) {
     854                $( '#bcs-smtp-ov-chart' ).removeAttr( 'aria-busy' );
     855                if ( ! r || ! r.success ) { return; }
     856                updateOverviewFigures( r.data.summary );
     857                if ( ovChart ) { ovChart.render( r.data.chart ); }
     858        } );
     859} );
     860
     861function initOverviewChart() {
     862        var el = document.getElementById( 'bcs-smtp-ov-chart' );
     863        if ( ! el || ovChart ) { return; }
     864        var data;
     865        try { data = JSON.parse( el.getAttribute( 'data-chart' ) ); } catch ( e ) { return; }
     866        ovChart = new LineChart( el );
     867        ovChart.render( data, window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches );
     868}
     869
     870/* =====================================================================
     871   REPORTS
     872   ===================================================================== */
     873var reportChart = null;
     874var reportRange = '30days';
     875
     876/* Facts derived from the daily series: no extra queries needed. */
     877var RANGE_DAYS = { '7days': 7, '30days': 30, '3months': 90 };
     878
     879function showRecipients( count ) {
     880        var $dd = $( '#bcs-smtp-highlights [data-hl="recipients"]' );
     881        if ( count === undefined || count === null || isNaN( count ) ) { $dd.text( '—' ); return; }
     882        count = +count;
     883        $dd.html( count.toLocaleString() + ' <small>' + ( 1 === count ? T( 'oneRecipient', 'recipient' ) : T( 'manyRecipients', 'recipients' ) ) + '</small>' )
     884                .toggleClass( 'is-good', count > 0 );
     885}
     886
     887function updateHighlights( raw ) {
     888        var n = raw.labels.length;
     889        // The series runs from the start date through today, so it holds one more calendar day
     890        // than the range's name; describe the period by its nominal length.
     891        var days = RANGE_DAYS[ reportRange ] || n;
     892        var total = 0, peak = -1, peakIdx = -1;
     893        for ( var i = 0; i < n; i++ ) {
     894                var day = ( +raw.success[ i ] ) + ( +raw.failed[ i ] ) + ( +raw.unconfirmed[ i ] );
     895                total  += day;
     896                if ( day > peak ) { peak = day; peakIdx = i; }
     897        }
     898        var $h = $( '#bcs-smtp-highlights' );
     899        var avg = days ? total / days : 0;
     900        $h.find( '[data-hl="avg"]' ).text( ! days ? '—' : ( total > 0 && avg < 0.5 ? T( 'lessThanOne', 'Less than 1' ) : Math.round( avg ).toLocaleString() ) );
     901        $h.find( '[data-hl="peak"]' ).html( peak > 0
     902                ? dayLabel( raw.labels[ peakIdx ] ) + ' <small>' + fmt( T( 'emailsCount', '%s emails' ), peak.toLocaleString() ) + '</small>'
     903                : '—' );
     904        // Failure-free counts use exactly the period's days (the most recent ones, through today).
     905        var start = Math.max( 0, n - days );
     906        var clean = 0, run = 0, best = 0;
     907        for ( var d = start; d < n; d++ ) {
     908                if ( +raw.failed[ d ] > 0 ) {
     909                        run = 0;
     910                } else {
     911                        clean++;
     912                        run++;
     913                        best = Math.max( best, run );
     914                }
     915        }
     916        var daysText = function ( v ) { return v.toLocaleString() + ' <small>' + ( 1 === v ? T( 'oneDay', 'day' ) : T( 'manyDays', 'days' ) ) + '</small>'; };
     917        $h.find( '[data-hl="cleandays"]' ).html( n ? clean.toLocaleString() + ' <small>' + fmt( T( 'ofDays', 'of %s days' ), Math.min( days, n - start ).toLocaleString() ) + '</small>' : '—' )
     918                .toggleClass( 'is-good', clean > 0 );
     919        $h.find( '[data-hl="beststreak"]' ).html( n ? daysText( best ) : '—' ).toggleClass( 'is-good', best > 0 );
     920
     921}
     922
     923function renderReport( sum, chart ) {
     924        var nf = function ( v ) { return Number( v ).toLocaleString(); };
     925        $( '#bcs-smtp-report-total' ).text( nf( sum.total ) );
     926        $( '#bcs-smtp-report-success' ).text( nf( sum.success ) );
     927        $( '#bcs-smtp-report-failed' ).text( nf( sum.failed ) ).toggleClass( 'is-bad', sum.failed > 0 );
     928        $( '#bcs-smtp-report-unconfirmed' ).text( nf( sum.unconfirmed ) );
     929        var rate = sum.total > 0 ? Math.round( sum.success / sum.total * 1000 ) / 10 : null;
     930        $( '#bcs-smtp-report-rate' ).html( null === rate ? '—' : rate.toLocaleString() + '<small>%</small>' );
     931        $( '#bcs-smtp-report-ratebar' ).css( 'width', ( rate || 0 ) + '%' );
     932        $( '#bcs-smtp-chart-empty' ).prop( 'hidden', sum.total > 0 );
     933        if ( reportChart ) { reportChart.render( chart ); }
     934        updateHighlights( chart );
     935        showRecipients( sum.recipients );
     936}
    232937
    233938function loadEmailReport() {
     939        var $chart = $( '#bcs-smtp-report-chart' ).attr( 'aria-busy', 'true' );
    234940        $.post( bcsSmtpData.ajaxurl, {
    235941                action     : 'bcs_smtp_get_email_report',
    236942                nonce      : bcsSmtpData.nonce,
    237                 date_range : $( '#bcs-smtp-report-date-range' ).val(),
     943                date_range : reportRange,
    238944        }, function ( r ) {
    239                 if ( ! r || ! r.success ) { return; }
    240                 var d = r.data;
    241 
    242                 $( '#bcs-smtp-report-success' ).text( d.summary.success );
    243                 $( '#bcs-smtp-report-failed' ).text( d.summary.failed );
    244                 $( '#bcs-smtp-report-unconfirmed' ).text( d.summary.unconfirmed );
    245                 $( '#bcs-smtp-report-total' ).text( d.summary.total );
    246 
    247                 updateReportChart( d.chart );
    248         } );
    249 }
    250 
    251 function updateReportChart( chartData ) {
    252         var ctx = document.getElementById( 'bcs-smtp-report-chart' );
    253         if ( ! ctx ) { return; }
    254 
    255         // Filter out days where all values are 0 (no email activity).
    256         var filteredLabels = [], filteredSuccess = [], filteredFailed = [], filteredUnconf = [];
    257         for ( var fi = 0; fi < chartData.labels.length; fi++ ) {
    258                 if ( chartData.success[ fi ] > 0 || chartData.failed[ fi ] > 0 || chartData.unconfirmed[ fi ] > 0 ) {
    259                         filteredLabels.push( chartData.labels[ fi ] );
    260                         filteredSuccess.push( chartData.success[ fi ] );
    261                         filteredFailed.push( chartData.failed[ fi ] );
    262                         filteredUnconf.push( chartData.unconfirmed[ fi ] );
     945                $chart.removeAttr( 'aria-busy' );
     946                if ( r && r.success ) { renderReport( r.data.summary, r.data.chart ); }
     947        } );
     948}
     949
     950function initReportChart() {
     951        var el = document.getElementById( 'bcs-smtp-report-chart' );
     952        if ( ! el || reportChart ) { return; }
     953        var data;
     954        try { data = JSON.parse( el.getAttribute( 'data-chart' ) ); } catch ( e ) { return; }
     955        reportChart = new LineChart( el );
     956        reportChart.render( data, window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches );
     957        updateHighlights( data );
     958        showRecipients( $( '#bcs-smtp-highlights [data-hl="recipients"]' ).data( 'initial' ) );
     959}
     960
     961$( document ).on( 'change', '.bcs-smtp-rp-range', function () {
     962        reportRange = $( this ).val();
     963        $( '#bcs-smtp-report-period' ).text( fmt( T( 'lastPeriod', 'Last %s' ), $( this ).data( 'label' ) ) );
     964        loadEmailReport();
     965} );
     966
     967$( document ).on( 'click', '#bcs-smtp-report-export', function () {
     968        window.location.href = bcsSmtpData.exportUrl + '&nonce=' + encodeURIComponent( bcsSmtpData.exportNonce ) + '&date_range=' + encodeURIComponent( reportRange ) + '&status=all&search=';
     969} );
     970
     971/* Same rule as the server: comma-separated, every entry a valid address. */
     972var EMAIL_RE = /^[^\s@,]+@[^\s@,]+\.[^\s@,]{2,}$/;
     973
     974/* =====================================================================
     975   Recipient chips (Gmail-style) — the hidden #bcs-smtp-report-emails keeps the
     976   comma-separated value the save handlers and validation read.
     977   ===================================================================== */
     978var $chipsWrap  = $( '#bcs-smtp-report-chips' );
     979var $chipList   = $chipsWrap.find( '.bcs-smtp-chip-list' );
     980var $chipEntry  = $( '#bcs-smtp-report-emails-entry' );
     981var $chipHidden = $( '#bcs-smtp-report-emails' );
     982var CHIP_PLACEHOLDER = $chipEntry.attr( 'placeholder' ) || '';
     983
     984function chipValues() {
     985        return String( $chipHidden.val() || '' ).split( ',' ).map( function ( v ) { return $.trim( v ); } ).filter( Boolean );
     986}
     987
     988function renderChips() {
     989        $chipList.empty();
     990        chipValues().forEach( function ( email, i ) {
     991                var ok    = EMAIL_RE.test( email );
     992                var $chip = $( '<li class="bcs-smtp-chip" />' ).toggleClass( 'is-invalid', ! ok ).attr( 'data-i', i );
     993                $( '<span class="bcs-smtp-chip-text" />' ).text( email ).attr( 'title', ok ? email : T( 'notAnEmail', 'Not a valid email address' ) ).appendTo( $chip );
     994                $( '<button type="button" class="bcs-smtp-chip-x" />' )
     995                        .attr( 'aria-label', fmt( T( 'removeEmail', 'Remove %s' ), email ) )
     996                        .html( '<span class="dashicons dashicons-no-alt" aria-hidden="true"></span>' )
     997                        .appendTo( $chip );
     998                $chipList.append( $chip );
     999        } );
     1000        $chipEntry.attr( 'placeholder', chipValues().length ? '' : CHIP_PLACEHOLDER );
     1001}
     1002
     1003function setChipValues( list ) {
     1004        var seen = {};
     1005        list = list.filter( function ( v ) {
     1006                var k = v.toLowerCase();
     1007                if ( seen[ k ] ) { return false; }
     1008                seen[ k ] = true;
     1009                return true;
     1010        } );
     1011        $chipHidden.val( list.join( ', ' ) );
     1012        $chipsWrap.removeClass( 'is-invalid' );
     1013        renderChips();
     1014}
     1015
     1016/* Turn whatever is typed or pasted into chips. */
     1017function commitChipEntry() {
     1018        var raw = $chipEntry.val();
     1019        if ( ! $.trim( raw ) ) { return; }
     1020        var added = raw.split( /[\s,;]+/ ).map( function ( v ) { return $.trim( v ); } ).filter( Boolean );
     1021        $chipEntry.val( '' );
     1022        setChipValues( chipValues().concat( added ) );
     1023}
     1024
     1025$chipEntry.on( 'keydown', function ( e ) {
     1026        if ( e.key === 'Enter' || e.key === ',' || e.key === ';' || ( e.key === ' ' && $.trim( this.value ) ) || ( e.key === 'Tab' && $.trim( this.value ) ) ) {
     1027                e.preventDefault();
     1028                commitChipEntry();
     1029                return;
     1030        }
     1031        if ( e.key === 'Backspace' && ! this.value ) {
     1032                var $last = $chipList.children().last();
     1033                if ( ! $last.length ) { return; }
     1034                if ( $last.hasClass( 'is-armed' ) ) {
     1035                        var vals = chipValues();
     1036                        vals.pop();
     1037                        setChipValues( vals );
     1038                } else {
     1039                        $last.addClass( 'is-armed' );
    2631040                }
    264         }
    265         // Fall back to full data if nothing has activity (avoids empty chart).
    266         if ( filteredLabels.length === 0 ) {
    267                 filteredLabels  = chartData.labels;
    268                 filteredSuccess = chartData.success;
    269                 filteredFailed  = chartData.failed;
    270                 filteredUnconf  = chartData.unconfirmed;
    271         }
    272         chartData = { labels: filteredLabels, success: filteredSuccess, failed: filteredFailed, unconfirmed: filteredUnconf };
    273 
    274         // Resolve colours based on active theme
    275         var isDark    = document.documentElement.getAttribute( 'data-bcs-smtp-theme' ) === 'dark';
    276         var gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)';
    277         var textColor = isDark ? '#94a3b8' : '#64748b';
    278 
    279         var datasets = [
    280                 {
    281                         label           : 'Sent',
    282                         data            : chartData.success,
    283                         backgroundColor : 'rgba(16,185,129,0.75)',
    284                         borderColor     : '#10b981',
    285                         borderWidth     : 1,
    286                         borderRadius    : 4,
    287                 },
    288                 {
    289                         label           : 'Failed',
    290                         data            : chartData.failed,
    291                         backgroundColor : 'rgba(239,68,68,0.75)',
    292                         borderColor     : '#ef4444',
    293                         borderWidth     : 1,
    294                         borderRadius    : 4,
    295                 },
    296                 {
    297                         label           : 'Unconfirmed',
    298                         data            : chartData.unconfirmed,
    299                         backgroundColor : 'rgba(245,158,11,0.75)',
    300                         borderColor     : '#f59e0b',
    301                         borderWidth     : 1,
    302                         borderRadius    : 4,
    303                 },
    304         ];
    305 
    306         if ( reportChart ) {
    307                 reportChart.data.labels   = chartData.labels;
    308                 reportChart.data.datasets = datasets;
    309                 reportChart.update();
    3101041                return;
    3111042        }
    312 
    313         reportChart = new Chart( ctx, {
    314                 type : 'bar',
    315                 data : {
    316                         labels   : chartData.labels,
    317                         datasets : datasets,
    318                 },
    319                 options : {
    320                         responsive          : true,
    321                         maintainAspectRatio : false,
    322                         interaction         : { mode: 'index', intersect: false },
    323                         plugins : {
    324                                 legend  : { labels : { color: textColor, font: { size: 12 } } },
    325                                 tooltip : { mode: 'index' },
    326                         },
    327                         scales : {
    328                                 x : {
    329                                         stacked   : false,
    330                                         grid      : { color: gridColor },
    331                                         ticks     : { color: textColor, font: { size: 11 } },
    332                                 },
    333                                 y : {
    334                                         stacked    : false,
    335                                         beginAtZero: true,
    336                                         grid       : { color: gridColor },
    337                                         ticks      : { color: textColor, font: { size: 11 }, precision: 0 },
    338                                 },
    339                         },
    340                 },
    341         } );
    342 }
    343 
    344 $( document ).on( 'change', '#bcs-smtp-report-date-range', function () {
    345         loadEmailReport();
    346 } );
    347 
    348 /* Accordion toggle */
    349 $( document ).on( 'click', '.bcs-smtp-accordion-header', function () {
    350         var $header = $( this );
    351         var $body   = $header.next( '.bcs-smtp-accordion-body' );
    352         var open    = $body.is( ':visible' );
    353         $body.slideToggle( 200 );
    354         $header.attr( 'aria-expanded', open ? 'false' : 'true' );
    355         $header.find( '.bcs-smtp-accordion-arrow' )
    356                 .toggleClass( 'dashicons-arrow-down-alt2', open )
    357                 .toggleClass( 'dashicons-arrow-up-alt2', ! open );
    358 } );
    359 
    360 /* Save schedule */
     1043        $chipList.children().removeClass( 'is-armed' );
     1044} );
     1045$chipEntry.on( 'paste', function () { setTimeout( commitChipEntry, 0 ); } );
     1046$chipEntry.on( 'blur', commitChipEntry );
     1047$chipEntry.on( 'input', function () { $chipsWrap.removeClass( 'is-invalid' ); } );
     1048
     1049$chipList.on( 'click', '.bcs-smtp-chip-x', function () {
     1050        var i    = +$( this ).closest( '.bcs-smtp-chip' ).attr( 'data-i' );
     1051        var vals = chipValues();
     1052        vals.splice( i, 1 );
     1053        setChipValues( vals );
     1054        $chipEntry.trigger( 'focus' );
     1055} );
     1056$chipsWrap.on( 'click', function ( e ) {
     1057        if ( e.target === this || $( e.target ).is( '.bcs-smtp-chip-list' ) ) { $chipEntry.trigger( 'focus' ); }
     1058} );
     1059
     1060
     1061function checkReportEmails( required ) {
     1062        commitChipEntry();
     1063        var $f    = $( '#bcs-smtp-report-emails' );
     1064        var parts = $f.val().split( ',' ).map( function ( v ) { return $.trim( v ); } ).filter( Boolean );
     1065        var bad   = parts.filter( function ( v ) { return ! EMAIL_RE.test( v ); } );
     1066        var msg   = '';
     1067        if ( bad.length ) {
     1068                msg = fmt( T( 'badEmails', 'These aren’t valid email addresses: %s' ), $( '<span>' ).text( bad.join( ', ' ) ).html() );
     1069        } else if ( required && ! parts.length ) {
     1070                msg = T( 'needEmail', 'Enter at least one email address to send the report to.' );
     1071        }
     1072        $chipsWrap.toggleClass( 'is-invalid', !! msg );
     1073        $chipEntry.attr( 'aria-invalid', msg ? 'true' : null );
     1074        if ( msg ) {
     1075                showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', msg );
     1076                $chipEntry.trigger( 'focus' );
     1077        }
     1078        return ! msg;
     1079}
     1080
     1081
     1082$( document ).on( 'change', '#bcs-smtp-report-schedule-enabled', function () {
     1083        $( '#bcs-smtp-report-body, #bcs-smtp-send-report-now-btn' ).prop( 'hidden', ! this.checked );
     1084        $( '#bcs-smtp-report-off' ).prop( 'hidden', this.checked );
     1085} );
     1086
    3611087$( document ).on( 'click', '#bcs-smtp-save-report-schedule-btn', function () {
    362         var $btn    = $( this );
    363         var btnHtml = '<span class="dashicons dashicons-saved"></span>Save Schedule';
    364         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Saving…' );
     1088        var $btn = $( this );
     1089        if ( ! checkReportEmails( $( '#bcs-smtp-report-schedule-enabled' ).is( ':checked' ) ) ) { return; }
     1090        setBusy( $btn, T( 'saving' ) );
    3651091        $.post( bcsSmtpData.ajaxurl, {
    3661092                action    : 'bcs_smtp_save_report_schedule',
    … …  
    3681094                enabled   : $( '#bcs-smtp-report-schedule-enabled' ).is( ':checked' ) ? 1 : 0,
    3691095                emails    : $( '#bcs-smtp-report-emails' ).val(),
    370                 frequency : $( '#bcs-smtp-report-frequency' ).val(),
    3711096        }, function ( r ) {
    372                 $btn.prop( 'disabled', false ).html( btnHtml );
     1097                clearBusy( $btn );
    3731098                if ( r && r.success ) {
    374                         showInlineMsg( '#bcs-smtp-report-schedule-msg', 'success', r.data.message || 'Schedule saved.' );
     1099                        showInlineMsg( '#bcs-smtp-report-schedule-msg', 'success', r.data.message || T( 'saved' ) );
     1100                        $( '#bcs-smtp-report-next-text' ).text( r.data.next || '' );
     1101                        if ( r.data.emails !== undefined ) { $chipHidden.val( r.data.emails ); renderChips(); }
     1102                        $( '#bcs-smtp-report-next' ).prop( 'hidden', ! r.data.next );
    3751103                } else {
    376                         showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', ( r && r.data ) ? r.data : 'Save failed.' );
    377                 }
    378         } ).fail( function () {
    379                 $btn.prop( 'disabled', false ).html( btnHtml );
    380                 showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', 'Request failed.' );
    381         } );
    382 } );
    383 
    384 /* Send report now — date_range is derived server-side from saved frequency */
    385 $( document ).on( 'click', '#bcs-smtp-send-report-now-btn', function () {
    386         var $btn    = $( this );
    387         var btnHtml = '<span class="dashicons dashicons-email-alt"></span>Send Report Now';
    388         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Sending…' );
    389         $.post( bcsSmtpData.ajaxurl, {
    390                 action  : 'bcs_smtp_send_report_now',
    391                 nonce   : bcsSmtpData.nonce,
    392                 emails  : $( '#bcs-smtp-report-emails' ).val(),
    393         }, function ( r ) {
    394                 $btn.prop( 'disabled', false ).html( btnHtml );
    395                 if ( r && r.success ) {
    396                         showInlineMsg( '#bcs-smtp-report-schedule-msg', 'success', r.data.message || 'Report sent.' );
    397                 } else {
    398                         showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', ( r && r.data ) ? r.data : 'Send failed.' );
    399                 }
    400         } ).fail( function () {
    401                 $btn.prop( 'disabled', false ).html( btnHtml );
    402                 showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', 'Request failed.' );
    403         } );
    404 } );
    405 
    406 /* =====================================================================
    407    SEND TEST EMAIL TAB
    408    ===================================================================== */
    409 $( document ).on( 'click', '#bcs-smtp-send-test-btn', function () {
    410         var $btn = $( this );
    411         var to   = $( '#bcs-smtp-test-to' ).val().trim();
    412         if ( ! to ) {
    413                 showInlineMsg( '#bcs-smtp-test-msg', 'error', 'Please enter a recipient email address.' );
    414                 return;
    415         }
    416         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Sending…' );
    417 
    418         $.post( bcsSmtpData.ajaxurl, {
    419                 action : 'bcs_smtp_send_test',
    420                 nonce  : bcsSmtpData.nonce,
    421                 to     : to,
    422         }, function ( r ) {
    423                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-email-alt"></span> Send Test Email' );
    424                 if ( r && r.success ) {
    425                         showInlineMsg( '#bcs-smtp-test-msg', 'success', r.data.message );
    426                 } else {
    427                         var msg = ( r && r.data )
    428                                 ? ( r.data.failure_type ? '<strong>' + r.data.failure_type + ':</strong> ' + r.data.message : r.data.message )
    429                                 : 'Test failed.';
    430                         showInlineMsg( '#bcs-smtp-test-msg', 'error', msg );
     1104                        showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', errorText( r, T( 'saveFailed' ) ) );
    4311105                }
    4321106        } ).fail( function ( xhr ) {
    433                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-email-alt"></span> Send Test Email' );
    434                 showInlineMsg( '#bcs-smtp-test-msg', 'error', 'Request failed (HTTP ' + xhr.status + ').' );
    435         } );
    436 } );
    437 
    438 $( document ).on( 'click', '#bcs-smtp-check-now-btn', function () {
     1107                clearBusy( $btn );
     1108                showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', failText( xhr ) );
     1109        } );
     1110} );
     1111
     1112/* Send report now — date_range is derived server-side from the saved frequency */
     1113$( document ).on( 'click', '#bcs-smtp-send-report-now-btn', function () {
    4391114        var $btn = $( this );
    440         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Running…' );
    441 
     1115        if ( ! checkReportEmails( true ) ) { return; }
     1116        setBusy( $btn, T( 'sending' ) );
    4421117        $.post( bcsSmtpData.ajaxurl, {
    443                 action : 'bcs_smtp_check_now',
     1118                action : 'bcs_smtp_send_report_now',
    4441119                nonce  : bcsSmtpData.nonce,
     1120                emails : $( '#bcs-smtp-report-emails' ).val(),
    4451121        }, function ( r ) {
    446                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-update"></span> Run Health Check Now' );
     1122                clearBusy( $btn );
    4471123                if ( r && r.success ) {
    448                         var type = r.data.success ? 'success' : 'error';
    449                         showInlineMsg( '#bcs-smtp-test-msg', type, r.data.message );
     1124                        showInlineMsg( '#bcs-smtp-report-schedule-msg', 'success', r.data.message );
     1125                } else {
     1126                        showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', errorText( r, T( 'saveFailed' ) ) );
    4501127                }
    4511128        } ).fail( function ( xhr ) {
    452                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-update"></span> Run Health Check Now' );
    453                 showInlineMsg( '#bcs-smtp-test-msg', 'error', 'Request failed (HTTP ' + xhr.status + ').' );
     1129                clearBusy( $btn );
     1130                showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', failText( xhr ) );
    4541131        } );
    4551132} );
    4561133
    4571134/* =====================================================================
    458    ALERTS TAB: toggles + channel
     1135   ALERTS
    4591136   ===================================================================== */
    460 
    461 /* Monitor toggle → accordion */
    462 $( document ).on( 'change', '#bcs-smtp-monitor-enabled', function () {
    463         $( '#bcs-smtp-monitor-accordion' ).toggleClass( 'open', this.checked );
    464 } );
    465 
    466 /* Alert channel toggle → accordion */
    4671137$( document ).on( 'change', '#bcs-smtp-alert-channel-enabled', function () {
    4681138        $( '#bcs-smtp-alert-channel-accordion' ).toggleClass( 'open', this.checked );
    469 } );
    470 
    471 /* Channel radio → show only the matching webhook field, preserve both values */
     1139        $( '#bcs-smtp-alerts-off' ).prop( 'hidden', this.checked );
     1140} );
     1141
     1142/* Channel list: each channel has its own switch; its webhook field opens under its row.
     1143   One row is open at a time. Every URL is kept and saved, even for channels that are off. */
     1144var WEBHOOK_ROWS  = { slack: 'slack', teams: 'teams', google_chat: 'google-chat', discord: 'discord', custom: 'custom' };
     1145var activeWebhook = null;
     1146
     1147function webhookInput( key ) {
     1148        return $( '#bcs-smtp-' + WEBHOOK_ROWS[ key ] + '-webhook-url' );
     1149}
     1150
     1151function channelState( box ) {
     1152        if ( ! box.checked ) { return 'off'; }
     1153        var url = $.trim( webhookInput( box.value ).val() );
     1154        return ! url ? 'missing' : ( webhookProblem( box.value, url ) ? 'bad' : 'ready' );
     1155}
     1156
    4721157function syncWebhookFields() {
    473         var ch = $( '.bcs-smtp-channel-radio:checked' ).val();
    474         $( '#bcs-smtp-slack-webhook-row' ).toggle( ch === 'slack' );
    475         $( '#bcs-smtp-teams-webhook-row' ).toggle( ch === 'teams' );
    476         $( '#bcs-smtp-google-chat-webhook-row' ).toggle( ch === 'google_chat' );
    477         $( '#bcs-smtp-discord-webhook-row' ).toggle( ch === 'discord' );
    478         $( '#bcs-smtp-custom-webhook-row' ).toggle( ch === 'custom' );
    479 }
    480 $( document ).on( 'change', '.bcs-smtp-channel-radio', syncWebhookFields );
    481 
    482 
    483 /* Save monitor settings */
    484 $( document ).on( 'click', '#bcs-smtp-save-monitor-btn', function () {
    485         var $btn = $( this );
    486         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Saving…' );
    487 
    488         $.post( bcsSmtpData.ajaxurl, {
    489                 action            : 'bcs_smtp_save_monitor',
    490                 nonce             : bcsSmtpData.nonce,
    491                 monitor_enabled   : $( '#bcs-smtp-monitor-enabled' ).is( ':checked' ) ? 1 : 0,
    492                 check_interval    : $( '#bcs-smtp-check-interval' ).val(),
    493                 failure_threshold : $( '#bcs-smtp-failure-threshold' ).val(),
    494                 max_alert_count   : $( '#bcs-smtp-max-alert-count' ).val(),
    495         }, function ( r ) {
    496                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-saved"></span> Save Monitor Settings' );
    497                 if ( r && r.success ) {
    498                         showInlineMsg( '#bcs-smtp-monitor-msg', 'success', r.data.message );
    499                 } else {
    500                         showInlineMsg( '#bcs-smtp-monitor-msg', 'error', ( r && r.data ) ? r.data : 'Save failed.' );
    501                 }
    502         } ).fail( function ( xhr ) {
    503                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-saved"></span> Save Monitor Settings' );
    504                 showInlineMsg( '#bcs-smtp-monitor-msg', 'error', 'Request failed (HTTP ' + xhr.status + ').' );
    505         } );
    506 } );
    507 
    508 /* Save alert channel settings */
    509 $( document ).on( 'click', '#bcs-smtp-save-alerts-btn', function () {
    510         var $btn = $( this );
    511         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Saving…' );
    512 
    513         $.post( bcsSmtpData.ajaxurl, {
    514                 action               : 'bcs_smtp_save_alerts',
    515                 nonce                : bcsSmtpData.nonce,
    516                 alert_channel_enabled: $( '#bcs-smtp-alert-channel-enabled' ).is( ':checked' ) ? 1 : 0,
    517                 alert_channel        : $( '.bcs-smtp-channel-radio:checked' ).val(),
    518                 slack_webhook_url    : $( '#bcs-smtp-slack-webhook-url' ).val(),
    519                 teams_webhook_url    : $( '#bcs-smtp-teams-webhook-url' ).val(),
    520                 google_chat_webhook_url: $( '#bcs-smtp-google-chat-webhook-url' ).val(),
    521                 discord_webhook_url  : $( '#bcs-smtp-discord-webhook-url' ).val(),
    522                 custom_webhook_url   : $( '#bcs-smtp-custom-webhook-url' ).val(),
    523         }, function ( r ) {
    524                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-saved"></span> Save Alert Settings' );
    525                 if ( r && r.success ) {
    526                         showInlineMsg( '#bcs-smtp-alerts-msg', 'success', r.data.message );
    527                 } else {
    528                         showInlineMsg( '#bcs-smtp-alerts-msg', 'error', ( r && r.data ) ? r.data : 'Save failed.' );
    529                 }
    530         } ).fail( function ( xhr ) {
    531                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-saved"></span> Save Alert Settings' );
    532                 showInlineMsg( '#bcs-smtp-alerts-msg', 'error', 'Request failed (HTTP ' + xhr.status + ').' );
    533         } );
    534 } );
    535 
    536 /* Send test alert */
     1158        var $boxes = $( '.bcs-smtp-channel-radio' );
     1159        // An open row stays open only while its channel is on.
     1160        if ( activeWebhook && ! $boxes.filter( '[value="' + activeWebhook + '"]' ).is( ':checked' ) ) {
     1161                activeWebhook = null;
     1162        }
     1163        var on = 0;
     1164        $boxes.each( function () {
     1165                var state = channelState( this );
     1166                var open  = this.value === activeWebhook;
     1167                var $row  = $( this ).closest( '.bcs-smtp-ch' );
     1168                on += this.checked ? 1 : 0;
     1169                $row.attr( 'data-state', state ).toggleClass( 'is-open', open );
     1170                $row.find( '.bcs-smtp-ch-main' ).attr( 'aria-expanded', open ? 'true' : 'false' );
     1171                $row.find( '.bcs-smtp-ch-pill' ).text( { ready: T( 'chReady' ), missing: T( 'chAddUrl' ), bad: T( 'chCheckUrl' ), off: T( 'chOff' ) }[ state ] );
     1172                $( '#bcs-smtp-' + WEBHOOK_ROWS[ this.value ] + '-webhook-row' ).prop( 'hidden', ! open );
     1173        } );
     1174        $( '#bcs-smtp-ch-count' ).text( fmt( T( 'chCount' ), on, $boxes.length ) );
     1175}
     1176
     1177/* Same host rules the server enforces, so a wrong paste is caught before saving */
     1178var WEBHOOK_HOSTS = {
     1179        slack       : /^hooks\.slack\.com$/,
     1180        teams       : /(^|\.)(office\.com|logic\.azure\.com|api\.powerplatform\.com)$/,
     1181        google_chat : /^chat\.googleapis\.com$/,
     1182        discord     : /(^|\.)discord(app)?\.com$/
     1183};
     1184
     1185function webhookProblem( key, url ) {
     1186        var u;
     1187        try { u = new URL( url ); } catch ( e ) { return T( 'urlHttps' ); }
     1188        if ( 'https:' !== u.protocol ) { return T( 'urlHttps' ); }
     1189        var host = u.hostname.toLowerCase();
     1190        if ( 'custom' === key ) {
     1191                return ( 'localhost' === host || /^[\d.]+$/.test( host ) || -1 !== host.indexOf( ':' ) || '[' === host.charAt( 0 ) ) ? T( 'urlPublic' ) : '';
     1192        }
     1193        return WEBHOOK_HOSTS[ key ].test( host ) ? '' : fmt( T( 'urlHost' ), $( '.bcs-smtp-channel-radio[value="' + key + '"]' ).data( 'label' ) );
     1194}
     1195
     1196/* Show a field's problem; live once shown, so it clears the moment the URL is fixed */
     1197function checkWebhookField( input, force ) {
     1198        var $row = $( input ).closest( '.bcs-smtp-webhook' );
     1199        var $err = $row.find( '.bcs-smtp-webhook-err' );
     1200        if ( ! force && $err.prop( 'hidden' ) ) { return; }
     1201        var url  = $.trim( input.value );
     1202        var msg  = url ? webhookProblem( $row.data( 'channel' ), url ) : '';
     1203        $err.text( msg ).prop( 'hidden', ! msg );
     1204        $( input ).attr( 'aria-invalid', msg ? 'true' : null );
     1205}
     1206$( document ).on( 'blur', '.bcs-smtp-webhook input', function () { checkWebhookField( this, true ); } );
     1207$( document ).on( 'input', '.bcs-smtp-webhook input', function () { checkWebhookField( this, false ); } );
     1208
     1209/* Unsaved changes: the test alert uses saved settings, so it waits for a save */
     1210function setAlertsDirty( dirty ) {
     1211        var live = $( '#bcs-smtp-alert-channel-enabled' ).is( ':checked' ) && $( '.bcs-smtp-channel-radio:checked' ).length > 0;
     1212        $( '#bcs-smtp-alerts-unsaved' ).prop( 'hidden', ! dirty );
     1213        $( '#bcs-smtp-test-alert-btn' ).prop( 'disabled', dirty || ! live ).attr( 'title', dirty ? T( 'saveFirst' ) : null );
     1214}
     1215$( document ).on( 'change input', '.bcs-smtp-alerts-card input', function () { setAlertsDirty( true ); } );
     1216
     1217/* Edits live only in the page until saved, so warn before leaving with unsaved changes */
     1218$( window ).on( 'beforeunload', function ( e ) {
     1219        if ( $( '.bcs-smtp-unsaved' ).filter( function () { return ! this.hidden; } ).length ) {
     1220                e.preventDefault();
     1221                return '';
     1222        }
     1223} );
     1224
     1225function showWebhook( key, focus ) {
     1226        activeWebhook = key;
     1227        syncWebhookFields();
     1228        if ( focus ) { webhookInput( key ).trigger( 'focus' ); }
     1229}
     1230
     1231/* The row itself: a channel that's off is switched on; one that's on opens or closes its field */
     1232$( document ).on( 'click', '.bcs-smtp-ch-main', function () {
     1233        var box = $( this ).closest( '.bcs-smtp-ch' ).find( '.bcs-smtp-channel-radio' )[ 0 ];
     1234        if ( ! box.checked ) {
     1235                box.checked = true;
     1236                $( box ).trigger( 'change' );
     1237                setAlertsDirty( true );
     1238                return;
     1239        }
     1240        if ( activeWebhook === box.value ) {
     1241                activeWebhook = null;
     1242                syncWebhookFields();
     1243        } else {
     1244                showWebhook( box.value, false );
     1245        }
     1246} );
     1247
     1248/* Typing a URL updates its row's status straight away */
     1249$( document ).on( 'input', '.bcs-smtp-webhook input', syncWebhookFields );
     1250
     1251$( document ).on( 'change', '.bcs-smtp-channel-radio', function () {
     1252        if ( this.checked ) {
     1253                showWebhook( this.value, ! $.trim( webhookInput( this.value ).val() ) );
     1254        } else {
     1255                syncWebhookFields();
     1256        }
     1257} );
     1258
     1259function simpleSave( btnSel, msgSel, data, onSaved ) {
     1260        $( document ).on( 'click', btnSel, function () {
     1261                var $btn = $( this );
     1262                setBusy( $btn, T( 'saving' ) );
     1263                $.post( bcsSmtpData.ajaxurl, $.extend( { nonce: bcsSmtpData.nonce }, data() ) )
     1264                        .done( function ( r ) {
     1265                                if ( r && r.success ) {
     1266                                        showInlineMsg( msgSel, 'success', ( r.data && r.data.message ) || T( 'saved' ) );
     1267                                        if ( onSaved ) { onSaved( r ); }
     1268                                } else {
     1269                                        showInlineMsg( msgSel, 'error', errorText( r, T( 'saveFailed' ) ) );
     1270                                }
     1271                        } )
     1272                        .fail( function ( xhr ) { showInlineMsg( msgSel, 'error', failText( xhr ) ); } )
     1273                        .always( function () { clearBusy( $btn ); } );
     1274        } );
     1275}
     1276
     1277simpleSave( '#bcs-smtp-save-alerts-btn', '#bcs-smtp-alerts-msg', function () {
     1278        if ( $( '#bcs-smtp-alert-channel-enabled' ).is( ':checked' ) ) {
     1279                $( '.bcs-smtp-channel-radio:checked' ).each( function () {
     1280                        var url = $.trim( webhookInput( this.value ).val() );
     1281                        if ( ! url || webhookProblem( this.value, url ) ) {
     1282                                showWebhook( this.value, true );
     1283                                checkWebhookField( webhookInput( this.value )[0], true );
     1284                                return false;
     1285                        }
     1286                } );
     1287        }
     1288        return {
     1289                action                  : 'bcs_smtp_save_alerts',
     1290                alert_channel_enabled   : $( '#bcs-smtp-alert-channel-enabled' ).is( ':checked' ) ? 1 : 0,
     1291                alert_channels          : $( '.bcs-smtp-channel-radio:checked' ).map( function () { return this.value; } ).get(),
     1292                slack_webhook_url       : $( '#bcs-smtp-slack-webhook-url' ).val(),
     1293                teams_webhook_url       : $( '#bcs-smtp-teams-webhook-url' ).val(),
     1294                google_chat_webhook_url : $( '#bcs-smtp-google-chat-webhook-url' ).val(),
     1295                discord_webhook_url     : $( '#bcs-smtp-discord-webhook-url' ).val(),
     1296                custom_webhook_url      : $( '#bcs-smtp-custom-webhook-url' ).val(),
     1297        };
     1298}, function () { setAlertsDirty( false ); } );
     1299
    5371300$( document ).on( 'click', '#bcs-smtp-test-alert-btn', function () {
    5381301        var $btn = $( this );
    539         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Sending…' );
    540 
    541         $.post( bcsSmtpData.ajaxurl, {
    542                 action : 'bcs_smtp_send_test_alert',
    543                 nonce  : bcsSmtpData.nonce,
    544         }, function ( r ) {
    545                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-megaphone"></span> Send Test Alert' );
    546                 if ( r && r.success ) {
    547                         showInlineMsg( '#bcs-smtp-alerts-msg', 'success', r.data.message );
    548                 } else {
    549                         showInlineMsg( '#bcs-smtp-alerts-msg', 'error', ( r && r.data ) ? r.data : 'Alert dispatch failed.' );
    550                 }
    551         } ).fail( function ( xhr ) {
    552                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-megaphone"></span> Send Test Alert' );
    553                 showInlineMsg( '#bcs-smtp-alerts-msg', 'error', 'Request failed (HTTP ' + xhr.status + ').' );
    554         } );
     1302        setBusy( $btn, T( 'sending' ) );
     1303        $.post( bcsSmtpData.ajaxurl, { action: 'bcs_smtp_send_test_alert', nonce: bcsSmtpData.nonce } )
     1304                .done( function ( r ) {
     1305                        if ( r && r.success ) {
     1306                                showInlineMsg( '#bcs-smtp-alerts-msg', 'success', r.data.message );
     1307                        } else {
     1308                                showInlineMsg( '#bcs-smtp-alerts-msg', 'error', errorText( r, T( 'saveFailed' ) ) );
     1309                        }
     1310                } )
     1311                .fail( function ( xhr ) { showInlineMsg( '#bcs-smtp-alerts-msg', 'error', failText( xhr ) ); } )
     1312                .always( function () { clearBusy( $btn ); } );
    5551313} );
    5561314
    5571315/* =====================================================================
    558    SETTINGS TAB: Change Password button
     1316   SETTINGS
    5591317   ===================================================================== */
    560 $( document ).on( 'click', '#bcs-smtp-change-pw-btn', function () {
    561         $( '#bcs-smtp-pw-display' ).hide();
    562         $( '#bcs-smtp-pw-cancel-row' ).show();
    563         $( '#bcs-smtp-pw-input' ).show();
    564         $( '#bcs-smtp-smtp-password' ).val( '' ).focus();
    565         $( '#bcs-smtp-pw-status' )
    566                 .text( 'Enter a new password — leave blank to keep the existing one.' )
    567                 .css( 'color', 'var(--bcs-smtp-text-3)' );
    568 } );
    569 
    570 $( document ).on( 'click', '#bcs-smtp-cancel-pw-btn', function ( e ) {
    571         e.preventDefault();
    572         $( '#bcs-smtp-pw-input' ).hide();
    573         $( '#bcs-smtp-pw-cancel-row' ).hide();
    574         $( '#bcs-smtp-pw-display' ).css( 'display', 'flex' );
    575         $( '#bcs-smtp-smtp-password' ).val( '' );
    576         $( '#bcs-smtp-pw-status' )
    577                 .text( '✓ Password saved — leave blank to keep it.' )
    578                 .css( 'color', 'var(--bcs-smtp-emerald)' );
    579 } );
    580 
    581 /* Show/hide the typed password */
    582 $( document ).on( 'click', '#bcs-smtp-toggle-pw-btn', function () {
    583         var $btn   = $( this );
    584         var $input = $( '#bcs-smtp-smtp-password' );
    585         var $icon  = $btn.find( '.dashicons' );
    586         var hidden = $input.attr( 'type' ) === 'password';
    587 
    588         $input.attr( 'type', hidden ? 'text' : 'password' );
    589         $icon.toggleClass( 'dashicons-visibility', ! hidden ).toggleClass( 'dashicons-hidden', hidden );
    590         $btn.attr( 'aria-label', hidden ? 'Hide password' : 'Show password' );
    591 } );
    592 
    593 /* =====================================================================
    594    RESEND FAILED EMAIL (Email Log tab)
    595    ===================================================================== */
    596 $( document ).on( 'click', '.bcs-smtp-resend-btn', function () {
    597         var $btn   = $( this );
    598         var logId  = $btn.data( 'log-id' );
    599         var orig   = $btn.html();
    600 
    601         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Resending…' );
    602 
    603         $.post( bcsSmtpData.ajaxurl, {
    604                 action: 'bcs_smtp_resend_email',
    605                 nonce : bcsSmtpData.nonce,
    606                 log_id: logId,
    607         } ).done( function ( r ) {
    608                 if ( r && r.success ) {
    609                         // Remove the button in place rather than reloading the table — the original
    610                         // row's status stays 'failed' (the resend created a separate log entry), so a
    611                         // full reload would just show the same Resend button again.
    612                         $btn.replaceWith( '<span class="bcs-smtp-badge bcs-smtp-badge-success">' +
    613                                 '<span class="dashicons dashicons-yes"></span> Resent</span>' );
    614                 } else {
    615                         $btn.prop( 'disabled', false ).html( orig );
    616                         window.alert( ( r && r.data ) ? r.data : 'Resend failed.' );
    617                 }
    618         } ).fail( function () {
    619                 $btn.prop( 'disabled', false ).html( orig );
    620                 window.alert( 'Request failed.' );
    621         } );
    622 } );
    623 
    624 /* =====================================================================
    625    RESEND SETTINGS (Config tab)
    626    ===================================================================== */
    627 $( document ).on( 'click', '#bcs-smtp-save-resend-btn', function () {
    628         var $btn = $( this );
    629         var orig = $btn.html();
    630 
    631         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Saving…' );
    632         $.post( bcsSmtpData.ajaxurl, {
    633                 action            : 'bcs_smtp_save_resend_settings',
    634                 nonce             : bcsSmtpData.nonce,
    635                 store_failed_body : $( '#bcs-smtp-store-failed-body' ).is( ':checked' ) ? 1 : 0,
    636                 retention_days    : $( '#bcs-smtp-failed-body-retention' ).val(),
    637         } ).done( function ( r ) {
    638                 showInlineMsg( '#bcs-smtp-resend-settings-msg', r && r.success ? 'success' : 'error', ( r && r.data && r.data.message ) ? r.data.message : ( ( r && r.data ) ? r.data : 'Save failed.' ) );
    639         } ).fail( function () {
    640                 showInlineMsg( '#bcs-smtp-resend-settings-msg', 'error', 'Request failed.' );
    641         } ).always( function () {
    642                 $btn.prop( 'disabled', false ).html( orig );
    643         } );
    644 } );
    645 
    646 /* =====================================================================
    647    Inline message helper
    648    ===================================================================== */
    649 function showInlineMsg( selector, type, msg ) {
    650         var icons = { success: 'yes-alt', error: 'dismiss', warning: 'warning', info: 'info-outline' };
    651         var $el   = $( selector );
    652         $el.css( { display: 'block', width: '100%' } ).html(
    653                 '<div class="bcs-smtp-alert bcs-smtp-alert-' + type + '" style="margin-top:0;width:100%;">' +
    654                 '<span class="dashicons dashicons-' + icons[ type ] + '"></span>' +
    655                 '<div class="bcs-smtp-alert-content">' + msg + '</div></div>'
    656         );
    657         setTimeout( function () {
    658                 $el.fadeOut( 400, function () { $el.empty().show().css( 'display', 'none' ); } );
    659         }, 5000 );
    660 }
    661 
    662 
    663 /* =====================================================================
    664    EMAIL LOG — Export CSV
    665    ===================================================================== */
    666 $( document ).on( 'click', '#bcs-smtp-export-csv-btn', function ( e ) {
    667         e.preventDefault();
    668         var url = bcsSmtpData.exportUrl
    669                 + '&nonce='      + encodeURIComponent( bcsSmtpData.exportNonce )
    670                 + '&date_range=' + encodeURIComponent( $( '#bcs-smtp-log-date-range' ).val() )
    671                 + '&status='     + encodeURIComponent( $( '#bcs-smtp-log-status' ).val() )
    672                 + '&search='     + encodeURIComponent( $( '#bcs-smtp-log-search' ).val() );
    673         window.location.href = url;
    674 } );
    675 
    676 /* =====================================================================
    677    EMAIL LOG — Log Retention / Auto-prune
    678    ===================================================================== */
    679 $( document ).on( 'click', '#bcs-smtp-save-log-prune-btn', function () {
    680         var $btn    = $( this );
    681         var btnHtml = '<span class="dashicons dashicons-saved"></span>Save Retention Settings';
    682         var mode    = $( '.bcs-smtp-prune-radio:checked' ).val();
    683         var value   = mode === 'days'    ? $( '#bcs-smtp-prune-days' ).val()
    684                     : mode === 'records' ? $( '#bcs-smtp-prune-records' ).val()
    685                     : 30;
    686 
    687         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Saving…' );
    688 
    689         $.post( bcsSmtpData.ajaxurl, {
    690                 action      : 'bcs_smtp_save_log_prune',
    691                 nonce       : bcsSmtpData.nonce,
    692                 prune_mode  : mode,
    693                 prune_value : value,
    694         }, function ( r ) {
    695                 $btn.prop( 'disabled', false ).html( btnHtml );
    696                 if ( r && r.success ) {
    697                         showInlineMsg( '#bcs-smtp-log-prune-msg', 'success', r.data.message || 'Saved.' );
    698                 } else {
    699                         showInlineMsg( '#bcs-smtp-log-prune-msg', 'error', ( r && r.data ) ? r.data : 'Save failed.' );
    700                 }
    701         } ).fail( function () {
    702                 $btn.prop( 'disabled', false ).html( btnHtml );
    703                 showInlineMsg( '#bcs-smtp-log-prune-msg', 'error', 'Request failed.' );
    704         } );
    705 } );
    706 
    707 
    7081318/* =====================================================================
    7091319   Init
    7101320   ===================================================================== */
    7111321$( function () {
     1322        // Browsers restore form state on reload, which can show a switch or field that no longer
     1323        // matches what is saved. Put every control back to the value the server rendered.
     1324        $( '.bcs-smtp-wrap' ).find( 'input, select, textarea' ).each( function () {
     1325                if ( this.type === 'checkbox' || this.type === 'radio' ) {
     1326                        this.checked = this.defaultChecked;
     1327                } else if ( this.tagName === 'SELECT' ) {
     1328                        var def = $( this ).find( 'option' ).filter( function () { return this.defaultSelected; } );
     1329                        this.selectedIndex = def.length ? def.index() : 0;
     1330                } else if ( this.type !== 'hidden' && this.type !== 'password' ) {
     1331                        this.value = this.defaultValue;
     1332                }
     1333        } );
    7121334        syncWebhookFields();
    713 
    714         // Load the active panel's data if it's a data panel
    715         var activePanel = getStoredTab();
    716         if ( activePanel === 'email-log' ) {
    717                 emailLogLoaded = true;
    718                 loadEmailLog();
    719         }
    720         if ( activePanel === 'email-report' ) {
    721                 emailReportLoaded = true;
    722                 loadEmailReport();
     1335        updateServerSummary();
     1336        renderChips();
     1337        syncConnectionDot();
     1338        syncPickedProvider();
     1339        // The single-email page has no tabs; leave the remembered tab and its URL alone there.
     1340        if ( $( '.bcs-smtp-tab' ).length ) {
     1341                var hash   = ( window.location.hash || '' ).replace( '#', '' );
     1342                var stored = store.get( TAB_KEY );
     1343                setActiveTab( hash && $( '.bcs-smtp-tab[data-panel="' + hash + '"]' ).length ? hash : ( stored || 'overview' ) );
    7231344        }
    7241345} );
  • bcs-smtp-manager/tags/1.3.0/bcs-smtp-manager.php

    r3652946 r3762759  
    44 * Plugin URI: https://bytecorestack.com/plugins/smtp-manager/
    55 * Description: Send WordPress email through any authenticated SMTP server, log every message, and get an alert on Slack, Microsoft Teams, Google Chat, or Discord the moment delivery fails.
    6  * Version: 1.2.1
     6 * Version: 1.3.0
    77 * Author: ByteCore Stack
    88 * Author URI: https://bytecorestack.com/
    … …  
    2121
    2222// Define plugin constants
    23 define( 'BCS_SMTP_VERSION',         '1.2.1' );
     23define( 'BCS_SMTP_VERSION',         '1.3.0' );
    2424define( 'BCS_SMTP_PLUGIN_DIR',      plugin_dir_path( __FILE__ ) );
    2525define( 'BCS_SMTP_PLUGIN_URL',      plugin_dir_url( __FILE__ ) );
    2626define( 'BCS_SMTP_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
    2727
    28 // Monitor/alert option names — defined once so the read side and write side can
    29 // never drift apart (a literal-string mismatch caused the monitor toggle to silently
    30 // no-op in 1.1.0; see class-bcs-smtp-monitor.php).
    31 define( 'BCS_SMTP_OPT_MONITOR_ENABLED',       'bcs_monitor_enabled' );
    32 define( 'BCS_SMTP_OPT_CHECK_INTERVAL',        'bcs_check_interval' );
    33 define( 'BCS_SMTP_OPT_FAILURE_THRESHOLD',     'bcs_failure_threshold' );
     28// Monitor/alert option names — defined once so the read side and write side can never drift apart.
    3429define( 'BCS_SMTP_OPT_CONSECUTIVE_FAILURES',  'bcs_consecutive_failures' );
    3530define( 'BCS_SMTP_OPT_LAST_CHECK',            'bcs_last_check' );
    … …  
    4237define( 'BCS_SMTP_OPT_DISCORD_WEBHOOK_URL',     'bcs_discord_webhook_url' );
    4338define( 'BCS_SMTP_OPT_CUSTOM_WEBHOOK_URL',      'bcs_custom_webhook_url' );
    44 define( 'BCS_SMTP_OPT_MAX_ALERT_COUNT',       'bcs_max_alert_count' );
    4539define( 'BCS_SMTP_OPT_ALERT_SENT_COUNT',      'bcs_alert_sent_count' );
    4640
    47 // Resend feature — failed-email body storage is opt-in and retained only briefly.
    48 define( 'BCS_SMTP_OPT_STORE_FAILED_BODY',           'bcs_store_failed_body' );
    49 define( 'BCS_SMTP_OPT_FAILED_BODY_RETENTION_DAYS',  'bcs_failed_body_retention_days' );
     41// The email log keeps this many days; reports and charts are limited to the same window.
     42define( 'BCS_SMTP_LOG_RETENTION_DAYS', 90 );
     43
     44// Bumped whenever the log table's columns change, so dbDelta runs again on existing sites.
     45define( 'BCS_SMTP_DB_VERSION', '1.3.0.1' );
     46
     47// Health checks are always on once SMTP is set up, with a fixed policy.
     48define( 'BCS_SMTP_CHECK_INTERVAL_MINUTES', 15 ); // Minutes between checks.
     49define( 'BCS_SMTP_FAILURE_THRESHOLD', 1 );       // Alert on the first failed check.
     50define( 'BCS_SMTP_MAX_ALERTS_PER_OUTAGE', 1 );   // One alert until mail recovers.
     51
     52// Random secret the SMTP password is encrypted with (see BCS_SMTP_Mailer::get_crypto_key()).
     53define( 'BCS_SMTP_OPT_CRYPTO_KEY', 'bcs_smtp_crypto_key' );
    5054
    5155// Load required files
    … …  
    6266}
    6367
    64 // Settings link on the Plugins page.
    65 if ( ! function_exists( 'bcs_smtp_plugin_action_links' ) ) {
    66         function bcs_smtp_plugin_action_links( $links ) {
    67                 $settings = '<a href="' . esc_url( admin_url( 'options-general.php?page=bcs-smtp-manager' ) ) . '">' . __( 'Settings', 'bcs-smtp-manager' ) . '</a>';
    68                 array_unshift( $links, $settings );
    69                 return $links;
    70         }
    71         add_filter( 'plugin_action_links_' . BCS_SMTP_PLUGIN_BASENAME, 'bcs_smtp_plugin_action_links' );
    72 }
    73 
    74 if ( ! function_exists( 'bcs_smtp_init' ) ) {
    75         /**
    76          * Initialize the plugin.
    77          */
    78         function bcs_smtp_init() {
    79                 $plugin = new BCS_SMTP_Core();
    80                 $plugin->run();
    81         }
    82         add_action( 'plugins_loaded', 'bcs_smtp_init' );
    83 }
    84 
    85 if ( ! function_exists( 'bcs_smtp_activate' ) ) {
    86         /**
    87          * Activation hook — creates tables and sets default options.
    88          *
    89          * @since 1.0
    90          */
    91         function bcs_smtp_activate() {
    92                 $db = new BCS_SMTP_DB();
    93                 $db->create_tables();
    94 
    95                 // SMTP connection settings
    96                 add_option( 'bcs_smtp_enabled',           false );
    97                 add_option( 'bcs_from_email',             get_option( 'admin_email' ) );
    98                 add_option( 'bcs_from_name',              get_option( 'blogname' ) );
    99                 add_option( 'bcs_smtp_host',              '' );
    100                 add_option( 'bcs_smtp_encryption',        'tls' );
    101                 add_option( 'bcs_smtp_port',              587 );
    102                 add_option( 'bcs_auto_tls',               true );
    103                 add_option( 'bcs_smtp_auth',              true );
    104                 add_option( 'bcs_smtp_username',          '' );
    105                 add_option( 'bcs_smtp_password',          '' );
    106 
    107                 // Monitoring settings
    108                 add_option( BCS_SMTP_OPT_MONITOR_ENABLED,      false );
    109                 add_option( BCS_SMTP_OPT_CHECK_INTERVAL,       5 );
    110                 add_option( BCS_SMTP_OPT_FAILURE_THRESHOLD,    2 );
    111                 add_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, 0 );
    112                 add_option( BCS_SMTP_OPT_LAST_CHECK,           0 );
    113                 add_option( BCS_SMTP_OPT_LAST_CHECK_SUCCESS,   null );
    114 
    115                 // Alert channel settings
    116                 add_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED, false );
    117                 add_option( BCS_SMTP_OPT_ALERT_CHANNEL,         'none' );
    118                 add_option( 'bcs_alert_webhook_url',            '' );
    119                 add_option( BCS_SMTP_OPT_SLACK_WEBHOOK_URL,        '' );
    120                 add_option( BCS_SMTP_OPT_TEAMS_WEBHOOK_URL,        '' );
    121                 add_option( BCS_SMTP_OPT_GOOGLE_CHAT_WEBHOOK_URL,  '' );
    122                 add_option( BCS_SMTP_OPT_DISCORD_WEBHOOK_URL,      '' );
    123                 add_option( BCS_SMTP_OPT_CUSTOM_WEBHOOK_URL,       '' );
    124                 add_option( BCS_SMTP_OPT_MAX_ALERT_COUNT,       0 );
    125                 add_option( BCS_SMTP_OPT_ALERT_SENT_COUNT,      0 );
    126 
    127                 // Migrate old single webhook URL to per-channel options.
    128                 $old_url = get_option( 'bcs_alert_webhook_url', '' );
    129                 $channel = get_option( 'bcs_alert_channel', 'none' );
    130                 if ( ! empty( $old_url ) ) {
    131                         if ( 'slack' === $channel && ! get_option( 'bcs_slack_webhook_url' ) ) {
    132                                 update_option( 'bcs_slack_webhook_url', $old_url );
    133                         } elseif ( 'teams' === $channel && ! get_option( 'bcs_teams_webhook_url' ) ) {
    134                                 update_option( 'bcs_teams_webhook_url', $old_url );
    135                         }
     68/**
     69 * Settings link on the Plugins page.
     70 *
     71 * @param  string[] $links
     72 * @return string[]
     73 */
     74function bcs_smtp_plugin_action_links( $links ) {
     75        $settings = '<a href="' . esc_url( admin_url( 'options-general.php?page=bcs-smtp-manager' ) ) . '">' . esc_html__( 'Settings', 'bcs-smtp-manager' ) . '</a>';
     76        array_unshift( $links, $settings );
     77        return $links;
     78}
     79add_filter( 'plugin_action_links_' . BCS_SMTP_PLUGIN_BASENAME, 'bcs_smtp_plugin_action_links' );
     80
     81/**
     82 * Initialize the plugin.
     83 */
     84function bcs_smtp_init() {
     85        $plugin = new BCS_SMTP_Core();
     86        $plugin->run();
     87}
     88add_action( 'plugins_loaded', 'bcs_smtp_init' );
     89
     90/**
     91 * Activation hook — creates tables and sets default options.
     92 *
     93 * @since 1.0
     94 */
     95function bcs_smtp_activate() {
     96        $db = new BCS_SMTP_DB();
     97        $db->create_tables();
     98
     99        // SMTP connection settings
     100        add_option( 'bcs_smtp_enabled',           false );
     101        add_option( 'bcs_from_email',             get_option( 'admin_email' ) );
     102        add_option( 'bcs_from_name',              get_option( 'blogname' ) );
     103        add_option( 'bcs_smtp_host',              '' );
     104        add_option( 'bcs_smtp_encryption',        'tls' );
     105        add_option( 'bcs_smtp_port',              587 );
     106        add_option( 'bcs_auto_tls',               true );
     107        add_option( 'bcs_smtp_auth',              true );
     108        add_option( 'bcs_smtp_username',          '' );
     109        add_option( 'bcs_smtp_password',          '' );
     110
     111        // Monitoring settings
     112        add_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, 0 );
     113        add_option( BCS_SMTP_OPT_LAST_CHECK,           0 );
     114        add_option( BCS_SMTP_OPT_LAST_CHECK_SUCCESS,   null );
     115
     116        // Alert channel settings
     117        add_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED, false );
     118        add_option( BCS_SMTP_OPT_ALERT_CHANNEL,         'none' );
     119        add_option( BCS_SMTP_OPT_SLACK_WEBHOOK_URL,        '' );
     120        add_option( BCS_SMTP_OPT_TEAMS_WEBHOOK_URL,        '' );
     121        add_option( BCS_SMTP_OPT_GOOGLE_CHAT_WEBHOOK_URL,  '' );
     122        add_option( BCS_SMTP_OPT_DISCORD_WEBHOOK_URL,      '' );
     123        add_option( BCS_SMTP_OPT_CUSTOM_WEBHOOK_URL,       '' );
     124        add_option( BCS_SMTP_OPT_ALERT_SENT_COUNT,      0 );
     125
     126        // Scheduled report settings (sent on the 1st of each month).
     127        add_option( 'bcs_report_schedule_enabled',    false );
     128        add_option( 'bcs_report_emails',              '' );
     129}
     130register_activation_hook( __FILE__, 'bcs_smtp_activate' );
     131
     132/**
     133 * Deactivation hook — stops the scheduled checks, pruning and reports. They are booked again
     134 * on the next page load after the plugin is reactivated.
     135 */
     136function bcs_smtp_deactivate() {
     137        foreach ( array( 'bcs_smtp_health_check', 'bcs_smtp_prune_log', 'bcs_smtp_send_report' ) as $bcs_smtp_hook ) {
     138                wp_clear_scheduled_hook( $bcs_smtp_hook );
     139        }
     140}
     141register_deactivation_hook( __FILE__, 'bcs_smtp_deactivate' );
     142
     143/**
     144 * One-time migration from 1.0: copy the old single webhook URL into the per-channel options.
     145 */
     146function bcs_smtp_migrate_webhook_options() {
     147        if ( get_option( 'bcs_smtp_migrated_v2' ) ) {
     148                return;
     149        }
     150        $bcs_smtp_old_url = get_option( 'bcs_alert_webhook_url', '' );
     151        $bcs_smtp_channel = get_option( 'bcs_alert_channel', 'none' );
     152        if ( ! empty( $bcs_smtp_old_url ) ) {
     153                if ( 'slack' === $bcs_smtp_channel && ! get_option( 'bcs_slack_webhook_url' ) ) {
     154                        update_option( 'bcs_slack_webhook_url', $bcs_smtp_old_url );
     155                } elseif ( 'teams' === $bcs_smtp_channel && ! get_option( 'bcs_teams_webhook_url' ) ) {
     156                        update_option( 'bcs_teams_webhook_url', $bcs_smtp_old_url );
    136157                }
    137 
    138                 // Scheduled report settings
    139                 add_option( 'bcs_report_schedule_enabled',    false );
    140                 add_option( 'bcs_report_emails',              '' );
    141                 add_option( 'bcs_report_frequency',           'weekly' );
    142 
    143                 add_option( 'bcs_smtp_snapshot',              array() );
    144 
    145                 // Log retention / auto-prune settings
    146                 add_option( 'bcs_log_prune_mode',  'disabled' );
    147                 add_option( 'bcs_log_prune_value', 30 );
    148 
    149                 // Resend feature — opt-in, off by default.
    150                 add_option( BCS_SMTP_OPT_STORE_FAILED_BODY,          false );
    151                 add_option( BCS_SMTP_OPT_FAILED_BODY_RETENTION_DAYS, 7 );
    152 
    153                 flush_rewrite_rules();
    154         }
    155         register_activation_hook( __FILE__, 'bcs_smtp_activate' );
    156 }
    157 
    158 if ( ! function_exists( 'bcs_smtp_deactivate' ) ) {
    159         /**
    160          * Deactivation hook.
    161          */
    162         function bcs_smtp_deactivate() {
    163                 $timestamp = wp_next_scheduled( 'bcs_smtp_health_check' );
    164                 if ( $timestamp ) {
    165                         wp_unschedule_event( $timestamp, 'bcs_smtp_health_check' );
    166                 }
    167                 flush_rewrite_rules();
    168         }
    169         register_deactivation_hook( __FILE__, 'bcs_smtp_deactivate' );
    170 }
    171 
    172 // One-time migration: copy old single webhook URL into per-channel options if not yet done.
    173 if ( ! function_exists( 'bcs_smtp_migrate_webhook_options' ) ) {
    174         function bcs_smtp_migrate_webhook_options() {
    175                 if ( get_option( 'bcs_smtp_migrated_v2' ) ) {
    176                         return;
    177                 }
    178                 $bcs_smtp_old_url = get_option( 'bcs_alert_webhook_url', '' );
    179                 $bcs_smtp_channel = get_option( 'bcs_alert_channel', 'none' );
    180                 if ( ! empty( $bcs_smtp_old_url ) ) {
    181                         if ( 'slack' === $bcs_smtp_channel ) {
    182                                 update_option( 'bcs_slack_webhook_url', $bcs_smtp_old_url );
    183                         } elseif ( 'teams' === $bcs_smtp_channel ) {
    184                                 update_option( 'bcs_teams_webhook_url', $bcs_smtp_old_url );
    185                         }
    186                 }
    187                 if ( false === get_option( 'bcs_max_alert_count', false ) ) {
    188                         add_option( 'bcs_max_alert_count', 0 );
    189                 }
    190                 if ( false === get_option( 'bcs_alert_sent_count', false ) ) {
    191                         add_option( 'bcs_alert_sent_count', 0 );
    192                 }
    193                 update_option( 'bcs_smtp_migrated_v2', true );
    194 
    195                 // Resend feature defaults for sites that installed before 1.2 (add_option is a no-op if already set).
    196                 add_option( BCS_SMTP_OPT_STORE_FAILED_BODY,          false );
    197                 add_option( BCS_SMTP_OPT_FAILED_BODY_RETENTION_DAYS, 7 );
    198         }
    199         add_action( 'plugins_loaded', 'bcs_smtp_migrate_webhook_options' );
    200 }
     158        }
     159        delete_option( 'bcs_alert_webhook_url' );
     160        add_option( 'bcs_alert_sent_count', 0 );
     161        update_option( 'bcs_smtp_migrated_v2', true );
     162}
     163add_action( 'plugins_loaded', 'bcs_smtp_migrate_webhook_options', 4 );
     164
     165/**
     166 * One-time cleanup for 1.3: health checks, log retention and the report schedule became fixed,
     167 * and the resend feature is gone. Drops the settings that no longer do anything, and the old
     168 * daily/weekly report event; the monthly one is booked on init (BCS_SMTP_Core::schedule_events()).
     169 * Saved email content is kept: 1.3 shows it on each email's page.
     170 */
     171function bcs_smtp_migrate_1_3() {
     172        if ( get_option( 'bcs_smtp_migrated_1_3' ) ) {
     173                return;
     174        }
     175        $bcs_smtp_retired = array(
     176                'bcs_store_failed_body',
     177                'bcs_failed_body_retention_days',
     178                'bcs_log_prune_mode',
     179                'bcs_log_prune_value',
     180                'bcs_monitor_enabled',
     181                'bcs_check_interval',
     182                'bcs_failure_threshold',
     183                'bcs_max_alert_count',
     184                'bcs_report_frequency',
     185                'bcs_smtp_snapshot',
     186                'bcs_smtp_monitor_enabled', // 1.0.0
     187        );
     188        foreach ( $bcs_smtp_retired as $bcs_smtp_old ) {
     189                delete_option( $bcs_smtp_old );
     190        }
     191
     192        // The single webhook URL from the earliest builds: make sure it reached its channel, then drop it.
     193        $bcs_smtp_old_url = (string) get_option( 'bcs_alert_webhook_url', '' );
     194        $bcs_smtp_channel = get_option( 'bcs_alert_channel', 'none' );
     195        if ( '' !== $bcs_smtp_old_url && in_array( $bcs_smtp_channel, array( 'slack', 'teams' ), true ) && ! get_option( 'bcs_' . $bcs_smtp_channel . '_webhook_url' ) ) {
     196                update_option( 'bcs_' . $bcs_smtp_channel . '_webhook_url', $bcs_smtp_old_url );
     197        }
     198        delete_option( 'bcs_alert_webhook_url' );
     199
     200        wp_clear_scheduled_hook( 'bcs_smtp_health_check' ); // Rescheduled on the fixed 15-minute interval.
     201        wp_clear_scheduled_hook( 'bcs_smtp_send_report' );  // Rebooked for the 1st of the month.
     202
     203        // Earlier versions could keep the log forever; 1.3 keeps 90 days. Nothing is pruned for
     204        // 30 days after updating, so older entries can be exported first (the Email log says so).
     205        add_option( 'bcs_smtp_prune_after', time() + 30 * DAY_IN_SECONDS, '', false );
     206
     207        // 1.0.0 stored the SMTP password as plain text; encrypt it now rather than on the next save.
     208        $bcs_smtp_pw = (string) get_option( 'bcs_smtp_password', '' );
     209        if ( '' !== $bcs_smtp_pw && 0 !== strpos( $bcs_smtp_pw, 'enc:' ) && 0 !== strpos( $bcs_smtp_pw, 'enc2:' ) ) {
     210                $bcs_smtp_mailer = new BCS_SMTP_Mailer();
     211                update_option( 'bcs_smtp_password', $bcs_smtp_mailer->encrypt_password( $bcs_smtp_pw ) );
     212        }
     213
     214        update_option( 'bcs_smtp_migrated_1_3', true );
     215}
     216add_action( 'plugins_loaded', 'bcs_smtp_migrate_1_3', 5 ); // After the 1.0 migration, before bcs_smtp_init().
  • bcs-smtp-manager/tags/1.3.0/includes/class-bcs-smtp-core.php

    r3652946 r3762759  
    5555                add_action( 'wp_ajax_bcs_smtp_send_test',          array( $this->admin, 'ajax_send_test' ) );
    5656                add_action( 'wp_ajax_bcs_smtp_check_now',          array( $this->admin, 'ajax_check_now' ) );
    57                 add_action( 'wp_ajax_bcs_smtp_save_monitor',       array( $this->admin, 'ajax_save_monitor' ) );
     57                add_action( 'wp_ajax_bcs_smtp_get_health_banner', array( $this->admin, 'ajax_get_health_banner' ) );
    5858                add_action( 'wp_ajax_bcs_smtp_save_alerts',        array( $this->admin, 'ajax_save_alerts' ) );
    5959                add_action( 'wp_ajax_bcs_smtp_send_test_alert',    array( $this->admin, 'ajax_send_test_alert' ) );
    … …  
    6262                add_action( 'wp_ajax_bcs_smtp_save_report_schedule', array( $this->admin, 'ajax_save_report_schedule' ) );
    6363                add_action( 'wp_ajax_bcs_smtp_send_report_now',    array( $this->admin, 'ajax_send_report_now' ) );
    64                 add_action( 'wp_ajax_bcs_smtp_save_log_prune',     array( $this->admin, 'ajax_save_log_prune' ) );
    65                 add_action( 'wp_ajax_bcs_smtp_resend_email',       array( $this->admin, 'ajax_resend_email' ) );
    66                 add_action( 'wp_ajax_bcs_smtp_save_resend_settings', array( $this->admin, 'ajax_save_resend_settings' ) );
    6764        }
    6865
    … …  
    7572        public function run() {
    7673                $this->db->maybe_create_tables();
     74
     75                // Scheduling reads the cron schedules, whose labels are translated, so it waits for init:
     76                // translations loaded earlier trigger a "_load_textdomain_just_in_time" notice (WP 6.7+).
     77                add_action( 'init', array( $this, 'schedule_events' ) );
     78        }
     79
     80        public function schedule_events() {
    7781                $this->monitor->maybe_schedule_event();
     82                $this->admin->ensure_report_scheduled();
    7883
    79                 // Schedule daily log pruning cron (runs even when mode is 'disabled' — no-op in that case).
     84                // Daily pruning keeps the log to the last BCS_SMTP_LOG_RETENTION_DAYS days.
    8085                if ( ! wp_next_scheduled( 'bcs_smtp_prune_log' ) ) {
    8186                        wp_schedule_event( time() + DAY_IN_SECONDS, 'daily', 'bcs_smtp_prune_log' );
  • bcs-smtp-manager/tags/1.3.0/includes/class-bcs-smtp-db.php

    r3652946 r3762759  
    6969        public function maybe_create_tables() {
    7070                $this->migrate_table_names();
    71                 if ( get_option( 'bcs_db_version' ) !== BCS_SMTP_VERSION ) {
     71                if ( get_option( 'bcs_db_version' ) !== BCS_SMTP_DB_VERSION ) {
    7272                        $this->create_tables();
    73                         update_option( 'bcs_db_version', BCS_SMTP_VERSION );
     73                        update_option( 'bcs_db_version', BCS_SMTP_DB_VERSION );
    7474                }
    7575        }
    … …  
    8080                require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    8181
    82                 // Email log — every wp_mail() call is recorded here. Message body/headers are
    83                 // intentionally NOT in this schema by default — only delivery metadata is retained
    84                 // (see readme.txt). The `body`/`body_format` columns below are the sole exception:
    85                 // populated only for FAILED sends, only when the opt-in "Store failed email content"
    86                 // setting is enabled, and purged after a configurable retention window — see
    87                 // BCS_SMTP_Mailer::on_email_failed() and BCS_SMTP_Admin::run_log_pruning().
     82                // Email log — every wp_mail() call is recorded here with its delivery details and message:
     83                // `body`, `body_format`, `mail_headers` and `attachments` (file names only, never the files).
     84                // Rows are pruned after BCS_SMTP_LOG_RETENTION_DAYS — see BCS_SMTP_Mailer::capture_outgoing_email().
    8885                $sql = "CREATE TABLE {$this->email_log_table()} (
    8986                        id             bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    … …  
    9895                        body           longtext,
    9996                        body_format    varchar(10),
     97                        mail_headers   text,
     98                        attachments    text,
    10099                        resend_count   smallint(5) unsigned NOT NULL DEFAULT 0,
    101100                        last_resend_at datetime NULL,
    … …  
    114113                // Migration: drop the legacy `headers` column from installs that pre-date 1.2 (dbDelta
    115114                // never drops columns). Note: `body` is deliberately NOT in this list from 1.2 onward —
    116                 // it was dropped in 1.1 but is re-added (conditionally populated) by the CREATE TABLE above
    117                 // to support the opt-in "Resend failed emails" feature.
     115                // it was dropped in 1.1 and re-added for the resend feature (removed in 1.3; the column is
     116                // kept so existing tables need no destructive change, and is cleared on upgrade).
    118117                $table = esc_sql( $this->email_log_table() );
    119118                foreach ( array( 'headers' ) as $col ) {
    … …  
    158157                                'message_id'   => isset( $data['message_id'] )   ? $data['message_id']   : null,
    159158                                'provider'     => isset( $data['provider'] )     ? $data['provider']     : null,
     159                                'body'         => isset( $data['body'] )         ? $data['body']         : null,
     160                                'body_format'  => isset( $data['body_format'] )  ? $data['body_format']  : null,
     161                                'mail_headers' => isset( $data['mail_headers'] ) ? $data['mail_headers'] : null,
     162                                'attachments'  => isset( $data['attachments'] )  ? $data['attachments']  : null,
    160163                        ),
    161                         array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
     164                        array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
    162165                );
    163166                return (int) $wpdb->insert_id;
    … …  
    258261        }
    259262
    260         /**
    261          * Store the message body for a failed send (opt-in, see BCS_SMTP_OPT_STORE_FAILED_BODY).
    262          * Never called for successful/unconfirmed sends.
    263          *
    264          * @param int    $id
    265          * @param string $body
    266          * @param string $format 'html'|'text'
    267          */
    268         public function update_email_log_body( $id, $body, $format = 'text' ) {
    269                 global $wpdb;
    270                 $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    271                         $this->email_log_table(),
    272                         array( 'body' => $body, 'body_format' => $format ),
    273                         array( 'id' => (int) $id ),
    274                         array( '%s', '%s' ),
    275                         array( '%d' )
    276                 );
    277         }
    278 
    279         /**
    280          * Bump the resend counter and timestamp on the original log row.
    281          * Called regardless of whether the resend attempt itself succeeds.
    282          *
    283          * @param int $id
    284          */
    285         public function increment_resend_count( $id ) {
    286                 global $wpdb;
    287                 $table = esc_sql( $this->email_log_table() );
    288                 $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    289                         "UPDATE {$table} SET resend_count = resend_count + 1, last_resend_at = %s WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    290                         current_time( 'mysql' ),
    291                         (int) $id
    292                 ) );
    293         }
    294 
    295         /**
    296          * Clear stored bodies of failed emails older than $days — independent of (and typically
    297          * shorter than) the overall log retention window, since bodies are more sensitive than metadata.
    298          *
    299          * @param int $days
    300          */
    301         public function clear_old_failed_bodies( $days ) {
    302                 global $wpdb;
    303                 $table = esc_sql( $this->email_log_table() );
    304                 $days  = max( 1, (int) $days );
    305                 $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    306                         "UPDATE {$table} SET body = NULL, body_format = NULL WHERE status = 'failed' AND body IS NOT NULL AND sent_at < DATE_SUB(NOW(), INTERVAL %d DAY)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    307                         $days
    308                 ) );
    309         }
    310 
    311263        /* ─────────────────────────────────────────────────────────────────── */
    312264        /* Email log — read                                                     */
    … …  
    324276         *   per_page   int     Rows per page
    325277         * }
     278         * Rows carry the delivery columns only, never message bodies, so a page of the log stays light.
     279         *
    326280         * @return array { rows, total, pages, page, per_page }
    327281         */
    … …  
    367321                                $wpdb->prepare(
    368322                                        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    369                                         "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY sent_at DESC LIMIT %d OFFSET %d",
     323                                        "SELECT id, sent_at, subject, from_address, to_address, status, error_msg, message_id, provider FROM {$table} WHERE {$where_sql} ORDER BY sent_at DESC LIMIT %d OFFSET %d",
    370324                                        (int) $args['per_page'],
    371325                                        $offset
    … …  
    381335                                $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
    382336                                        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    383                                         "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY sent_at DESC LIMIT %d OFFSET %d",
     337                                        "SELECT id, sent_at, subject, from_address, to_address, status, error_msg, message_id, provider FROM {$table} WHERE {$where_sql} ORDER BY sent_at DESC LIMIT %d OFFSET %d",
    384338                                        array_merge( $params, array( (int) $args['per_page'], $offset ) )
    385339                                ),
    … …  
    395349                        'per_page' => (int) $args['per_page'],
    396350                );
     351        }
     352
     353
     354        /**
     355         * IDs of the next newer and older emails, for stepping through the log one email at a time.
     356         *
     357         * @param  int $id
     358         * @return array{newer:int,older:int} 0 when there is none.
     359         */
     360        public function get_adjacent_log_ids( $id ) {
     361                global $wpdb;
     362                $table = esc_sql( $this->email_log_table() );
     363                // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
     364                $newer = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE id > %d ORDER BY id ASC LIMIT 1", (int) $id ) );
     365                $older = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE id < %d ORDER BY id DESC LIMIT 1", (int) $id ) );
     366                // phpcs:enable
     367                return array( 'newer' => (int) $newer, 'older' => (int) $older );
     368        }
     369
     370        /**
     371         * How many log entries are older than the given number of days.
     372         *
     373         * @param  int $days
     374         * @return int
     375         */
     376        public function count_email_log_older_than( $days ) {
     377                global $wpdb;
     378                $table  = esc_sql( $this->email_log_table() );
     379                $cutoff = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - (int) $days * DAY_IN_SECONDS );
     380                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
     381                return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE sent_at < %s", $cutoff ) );
     382        }
     383
     384        /**
     385         * Emails per status for a date range and search, for the log's status filter.
     386         *
     387         * @param  string $date_range
     388         * @param  string $search
     389         * @return array{all:int,success:int,failed:int,unconfirmed:int}
     390         */
     391        public function count_email_log_by_status( $date_range = '30days', $search = '' ) {
     392                global $wpdb;
     393                $table  = esc_sql( $this->email_log_table() );
     394                $where  = array( '1=1' );
     395                $params = array();
     396
     397                $date_sql = $this->build_date_where( $date_range );
     398                if ( $date_sql ) {
     399                        $where[] = $date_sql;
     400                }
     401                if ( '' !== $search ) {
     402                        $like     = '%' . $wpdb->esc_like( $search ) . '%';
     403                        $where[]  = '( subject LIKE %s OR from_address LIKE %s OR to_address LIKE %s )';
     404                        $params[] = $like;
     405                        $params[] = $like;
     406                        $params[] = $like;
     407                }
     408                $sql = "SELECT status, COUNT(*) AS n FROM {$table} WHERE " . implode( ' AND ', $where ) . ' GROUP BY status';
     409                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
     410                $rows = $wpdb->get_results( $params ? $wpdb->prepare( $sql, $params ) : $sql, ARRAY_A );
     411
     412                $counts = array( 'all' => 0, 'success' => 0, 'failed' => 0, 'unconfirmed' => 0 );
     413                foreach ( (array) $rows as $r ) {
     414                        if ( isset( $counts[ $r['status'] ] ) ) {
     415                                $counts[ $r['status'] ] = (int) $r['n'];
     416                        }
     417                        $counts['all'] += (int) $r['n'];
     418                }
     419                return $counts;
     420        }
     421
     422        /**
     423         * How many different people actually received email in the range: unique recipient
     424         * addresses on successfully sent emails only (failed and unconfirmed sends don't count).
     425         *
     426         * @param string $date_range       Range key.
     427         * @param array  $exclude          Addresses to leave out (the site's own team).
     428         * @param array  $exclude_domains  Domains whose addresses are left out, e.g. "example.com".
     429         * @return int
     430         */
     431        public function count_unique_recipients( $date_range = '30days', $exclude = array(), $exclude_domains = array() ) {
     432                $exclude         = array_flip( array_map( 'strtolower', array_map( 'trim', (array) $exclude ) ) );
     433                $exclude_domains = array_map( 'strtolower', array_filter( array_map( 'trim', (array) $exclude_domains ) ) );
     434                global $wpdb;
     435                $table    = esc_sql( $this->email_log_table() );
     436                $date_sql = $this->build_date_where( $date_range );
     437                $where    = "WHERE status = 'success'" . ( $date_sql ? " AND {$date_sql}" : '' );
     438
     439                $rows = $wpdb->get_col( "SELECT to_address FROM {$table} {$where}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
     440
     441                // A row can hold several comma-separated addresses; count each person once.
     442                $seen = array();
     443                foreach ( (array) $rows as $list ) {
     444                        foreach ( explode( ',', (string) $list ) as $addr ) {
     445                                $addr = strtolower( trim( $addr ) );
     446                                if ( preg_match( '/<([^>]+)>/', $addr, $m ) ) {
     447                                        $addr = trim( $m[1] );
     448                                }
     449                                if ( '' === $addr || isset( $exclude[ $addr ] ) ) {
     450                                        continue;
     451                                }
     452                                $domain = substr( strrchr( $addr, '@' ), 1 );
     453                                if ( $domain && in_array( $domain, $exclude_domains, true ) ) {
     454                                        continue;
     455                                }
     456                                $seen[ $addr ] = true;
     457                        }
     458                }
     459                return count( $seen );
     460        }
     461
     462        /**
     463         * Return success/failed/unconfirmed/total counts for the given date range.
     464         *
     465         * @param  string $date_range
     466         * @return array  { success, failed, unconfirmed, total, recipients }
     467         */
     468        public function get_email_summary_with_recipients( $date_range = '30days', $exclude = array(), $exclude_domains = array() ) {
     469                $summary               = $this->get_email_summary( $date_range );
     470                $summary['recipients'] = $this->count_unique_recipients( $date_range, $exclude, $exclude_domains );
     471                return $summary;
    397472        }
    398473
    … …  
    426501
    427502        /**
    428          * Return daily counts per status, suitable for a Chart.js dataset.
     503         * Return daily counts per status, used by the admin line charts and the report email.
    429504         *
    430505         * @param  string $date_range
    … …  
    591666                                $s = gmdate( 'Y-m-01 00:00:00', $now );
    592667                                return "sent_at >= '{$s}'";
     668                        case 'last_month':
     669                                $s = gmdate( 'Y-m-01 00:00:00', strtotime( 'first day of last month', $now ) );
     670                                $e = gmdate( 'Y-m-t 23:59:59', strtotime( 'first day of last month', $now ) );
     671                                return "sent_at BETWEEN '{$s}' AND '{$e}'";
    593672                        case '3months':
    594673                                $s = gmdate( 'Y-m-d 00:00:00', strtotime( '-3 months', $now ) );
    … …  
    615694                        'last_week'  => 14,
    616695                        'this_month' => 30,
     696                        'last_month' => 62,
    617697                        '3months'    => 90,
    618698                        '6months'    => 180,
  • bcs-smtp-manager/tags/1.3.0/includes/class-bcs-smtp-email-template.php

    r3652946 r3762759  
    11<?php
    22/**
    3  * Shared HTML chrome for outgoing plugin emails (test email + scheduled report).
    4  * Holds only the markup that is genuinely identical between templates — each
    5  * template still builds its own header banner and body content.
     3 * Shared layout for the plugin's own emails (test email + monthly report).
     4 *
     5 * Table-based and inline-styled so it holds up in Outlook and Gmail; fluid up to 600px
     6 * so it fits phones; with a hidden preheader for the inbox preview line and dark-mode
     7 * overrides for clients that support them (Apple Mail, Outlook for Mac/iOS).
    68 *
    79 * @package BCS_SMTP_Manager
    … …  
    1315class BCS_SMTP_Email_Template {
    1416
     17        const BRAND = '#1450c4';
     18        const FONT  = "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif";
     19        const MONO  = "ui-monospace,SFMono-Regular,Menlo,Consolas,'Liberation Mono',monospace";
     20
     21        /** Status tones: text colour and soft background. */
     22        const TONES = array(
     23                'ok'   => array( '#0f7a4a', '#e6f6ee' ),
     24                'bad'  => array( '#c62828', '#fdecec' ),
     25                'warn' => array( '#a15c00', '#fdf3e1' ),
     26                'info' => array( '#1450c4', '#e8effc' ),
     27        );
     28
    1529        /**
    16          * Opening half of the email: DOCTYPE/head/body + the outer centering table
    17          * + the inner content card table (open only — caller fills in everything else).
     30         * A whole email document.
    1831         *
    19          * @param string $title          <title> text.
    20          * @param int    $width          Inner card width in px.
    21          * @param int    $radius         Inner card border-radius in px.
    22          * @param string $shadow_opacity Box-shadow alpha, e.g. '.08'.
     32         * @param array $args {
     33         *     @type string $title     Document title.
     34         *     @type string $preheader Inbox preview text (plain).
     35         *     @type string $label     Small label shown above the card, right of the site name (plain).
     36         *     @type string $body      Card rows: one or more escaped `<tr>` elements.
     37         *     @type string $footer    Escaped HTML for the line under the card.
     38         * }
     39         * @return string
    2340         */
    24         public static function wrapper_open( $title, $width = 600, $radius = 14, $shadow_opacity = '.08' ) {
     41        public static function document( $args ) {
     42                $args = wp_parse_args( $args, array( 'title' => '', 'preheader' => '', 'label' => '', 'body' => '', 'footer' => '' ) );
     43                $site = get_bloginfo( 'name' );
     44                // Pads the preview so clients don't fill it with the first lines of the body.
     45                $pad = str_repeat( '&#847;&zwnj;&nbsp;', 40 );
     46
    2547                return '<!DOCTYPE html>
    26 <html lang="en">
    27 <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>' . esc_html( $title ) . '</title></head>
    28 <body style="margin:0;padding:0;background:#f1f5f9;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif">
    29 <table width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f1f5f9" style="background-color:#f1f5f9;padding:40px 16px">
    30 <tr><td align="center">
    31 <table width="' . (int) $width . '" cellpadding="0" cellspacing="0" border="0" bgcolor="#ffffff" style="background-color:#ffffff;border-radius:' . (int) $radius . 'px;overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,' . esc_attr( $shadow_opacity ) . ')">
    32 ';
    33         }
    34 
    35         /** Closing half of the email — matches wrapper_open(). */
    36         public static function wrapper_close() {
    37                 return '
     48<html lang="' . esc_attr( str_replace( '_', '-', get_locale() ) ) . '">
     49<head>
     50<meta charset="UTF-8">
     51<meta name="viewport" content="width=device-width,initial-scale=1">
     52<meta name="color-scheme" content="light dark">
     53<meta name="supported-color-schemes" content="light dark">
     54<title>' . esc_html( $args['title'] ) . '</title>
     55<style>
     56body{margin:0;padding:0;-webkit-text-size-adjust:100%}
     57a{color:' . self::BRAND . '}
     58@media (max-width:620px){
     59  .bcs-pad{padding-left:22px!important;padding-right:22px!important}
     60  .bcs-h1{font-size:24px!important}
     61  .bcs-big{font-size:40px!important}
     62  .bcs-hide-sm{display:none!important}
     63}
     64@media (prefers-color-scheme:dark){
     65  .bcs-bg{background:#0b1120!important}
     66  .bcs-card{background:#111a2e!important;border-color:#1e2a44!important}
     67  .bcs-ink{color:#e8edf6!important}
     68  .bcs-ink2{color:#a7b3c8!important}
     69  .bcs-ink3{color:#7d8aa1!important}
     70  .bcs-line{border-color:#1e2a44!important}
     71  .bcs-soft{background:#0d1526!important}
     72  .bcs-track{background:#1e2a44!important}
     73}
     74</style>
     75</head>
     76<body class="bcs-bg" style="margin:0;padding:0;background:#eef1f6">
     77<div style="display:none;max-height:0;max-width:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#eef1f6">' . esc_html( $args['preheader'] ) . $pad . '</div>
     78<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" class="bcs-bg" bgcolor="#eef1f6" style="background:#eef1f6">
     79<tr><td align="center" style="padding:32px 12px 40px">
     80<!--[if mso]><table role="presentation" width="600" cellpadding="0" cellspacing="0" border="0"><tr><td><![endif]-->
     81<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:600px;font-family:' . self::FONT . '">
     82  <tr><td style="padding:0 6px 14px">
     83    <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
     84      <td class="bcs-ink" style="font-size:14px;font-weight:700;color:#0f172a;letter-spacing:-.1px">' . esc_html( $site ) . '</td>
     85      <td class="bcs-ink3" align="right" style="font-size:12px;color:#8a94a6">' . esc_html( $args['label'] ) . '</td>
     86    </tr></table>
     87  </td></tr>
     88  <tr><td class="bcs-card" bgcolor="#ffffff" style="background:#ffffff;border:1px solid #e2e7ef;border-radius:14px;overflow:hidden">
     89    <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
     90      <tr><td height="4" bgcolor="' . self::BRAND . '" style="background:' . self::BRAND . ';height:4px;font-size:0;line-height:0">&nbsp;</td></tr>
     91' . $args['body'] . '
     92    </table>
     93  </td></tr>
     94  <tr><td class="bcs-ink3" align="center" style="padding:22px 16px 0;font-size:12px;line-height:1.6;color:#8a94a6">' . $args['footer'] . '</td></tr>
    3895</table>
     96<!--[if mso]></td></tr></table><![endif]-->
    3997</td></tr>
    4098</table>
    … …  
    44102
    45103        /**
    46          * The small "Sent from / via ByteCoreStack SMTP" branding line
    47          * used in every plugin email footer. $extra is appended after the brand name
    48          * (e.g. a "Manage settings" link) so each template can still differ slightly.
     104         * Small rounded status pill with a dot.
     105         *
     106         * @param string $text Plain text.
     107         * @param string $tone ok|bad|warn|info.
    49108         */
    50         public static function brand_footer_line( $site_url, $site_name, $extra = '' ) {
    51                 return '<p style="margin:0;font-size:11px;color:#94a3b8">' .
    52                         'Sent from <a href="' . esc_url( $site_url ) . '" style="color:#6366f1;text-decoration:none">' . esc_html( $site_name ) . '</a> ' .
    53                         'via <strong style="color:#64748b">ByteCoreStack SMTP</strong>' . $extra .
    54                         '</p>';
     109        public static function pill( $text, $tone = 'info' ) {
     110                list( $ink, $bg ) = isset( self::TONES[ $tone ] ) ? self::TONES[ $tone ] : self::TONES['info'];
     111                return '<span style="display:inline-block;padding:5px 11px 5px 10px;border-radius:999px;background:' . $bg . ';color:' . $ink . ';font-size:12px;font-weight:600;line-height:1.2">'
     112                        . '<span style="color:' . $ink . '">&#9679;</span>&nbsp;' . esc_html( $text ) . '</span>';
     113        }
     114
     115        /**
     116         * Solid brand button that renders as a button in Outlook too.
     117         *
     118         * @param string $url   Link.
     119         * @param string $label Plain text.
     120         */
     121        public static function button( $url, $label ) {
     122                return '<table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr>
     123  <td bgcolor="' . self::BRAND . '" style="background:' . self::BRAND . ';border-radius:9px">
     124    <a href="' . esc_url( $url ) . '" style="display:inline-block;padding:13px 22px;font-family:' . self::FONT . ';font-size:14px;font-weight:600;color:#ffffff;text-decoration:none;border-radius:9px">' . esc_html( $label ) . '&nbsp;&rarr;</a>
     125  </td></tr></table>';
     126        }
     127
     128        /**
     129         * Label / value rows separated by hairlines.
     130         *
     131         * @param array $rows List of array( label, value, mono? ). Plain text.
     132         */
     133        public static function details( $rows ) {
     134                $html = '';
     135                foreach ( $rows as $i => $row ) {
     136                        $border = $i ? 'border-top:1px solid #edf0f5;' : '';
     137                        $font   = ! empty( $row[2] ) ? 'font-family:' . self::MONO . ';font-size:13px;' : 'font-size:14px;';
     138                        $html  .= '<tr>
     139  <td class="bcs-ink3 bcs-line" width="34%" valign="top" style="' . $border . 'padding:12px 12px 12px 0;font-size:13px;color:#64748b">' . esc_html( $row[0] ) . '</td>
     140  <td class="bcs-ink bcs-line" valign="top" style="' . $border . 'padding:12px 0;' . $font . 'color:#0f172a;word-break:break-word">' . esc_html( $row[1] ) . '</td>
     141</tr>';
     142                }
     143                return '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">' . $html . '</table>';
     144        }
     145
     146        /**
     147         * Footer line: "Sent from {site} by ByteCoreStack SMTP · {link}".
     148         *
     149         * @param string $link_url   Settings link.
     150         * @param string $link_label Plain text.
     151         * @param string $before     Escaped HTML shown on its own line above, optional.
     152         */
     153        public static function footer( $link_url, $link_label, $before = '' ) {
     154                /* translators: 1: site name (linked), 2: plugin name. */
     155                $sent = sprintf( esc_html__( 'Sent from %1$s by %2$s', 'bcs-smtp-manager' ), '<a href="' . esc_url( home_url( '/' ) ) . '" style="color:#64748b;text-decoration:underline">' . esc_html( get_bloginfo( 'name' ) ) . '</a>', 'ByteCoreStack SMTP' );
     156                return ( $before ? $before . '<br>' : '' ) . $sent . ' &middot; <a href="' . esc_url( $link_url ) . '" style="color:#64748b;text-decoration:underline">' . esc_html( $link_label ) . '</a>';
     157        }
     158
     159        /**
     160         * Link to a tab of the plugin's settings page.
     161         *
     162         * @param string $tab overview|connection|alerts|reports|email-log.
     163         */
     164        public static function settings_url( $tab = '' ) {
     165                return admin_url( 'options-general.php?page=bcs-smtp-manager' ) . ( $tab ? '#' . $tab : '' );
    55166        }
    56167}
  • bcs-smtp-manager/tags/1.3.0/includes/class-bcs-smtp-mailer.php

    r3652946 r3762759  
    1717        public $pending_log_id = null;
    1818
    19         /** Body/format of the email currently being sent — held in memory only, never persisted unless the send fails. */
    20         private $pending_body        = '';
    21         private $pending_body_format = 'text';
     19        /** Server-side SMTP transcript lines for the email currently being sent (credentials never included). */
     20        private $smtp_transcript = array();
    2221
    2322        public function __construct( $db = null ) {
    … …  
    7675         */
    7776        public function capture_outgoing_email( $args ) {
     77                $this->smtp_transcript = array();
     78
    7879                if ( ! $this->db ) {
    7980                        return $args;
    … …  
    108109                $args['headers'] = $headers;
    109110
    110                 // Note: message body/headers are NOT persisted to the log table for successful/unconfirmed
    111                 // sends — only delivery metadata is stored (see readme.txt "What is tracked"). The body is
    112                 // held here in memory only; it's written to the DB by on_email_failed() below, and only
    113                 // then, only when the send actually fails, and only if the opt-in retention setting is on.
    114                 $this->pending_body        = isset( $args['message'] ) ? (string) $args['message'] : '';
    115                 $this->pending_body_format = ( false !== stripos( implode( ' ', $headers ), 'text/html' ) ) ? 'html' : 'text';
    116 
    117                 $this->pending_log_id = $this->db->log_email( array(
     111                $row = array(
    118112                        'subject'      => isset( $args['subject'] ) ? $args['subject'] : '',
    119113                        'from_address' => $from_address,
    … …  
    121115                        'status'       => 'pending',
    122116                        'message_id'   => $tracking_id,
    123                 ) );
     117                );
     118
     119                // Keep the message itself so the log can show the full email.
     120                $row = array_merge( $row, $this->email_content_for_log( $args, $headers ) );
     121
     122                $this->pending_log_id = $this->db->log_email( $row );
    124123
    125124                return $args;
    … …  
    132131         * @param \PHPMailer\PHPMailer\PHPMailer $phpmailer
    133132         */
     133        /** Most bytes of a message body kept per email, so one huge newsletter can't bloat the table. */
     134        const MAX_LOGGED_BODY = 1048576;
     135
     136        /**
     137         * Body, headers and attachment names to save with a log row.
     138         *
     139         * @param  array    $args    wp_mail() arguments.
     140         * @param  string[] $headers Normalized header lines, including our tracking header.
     141         * @return array
     142         */
     143        private function email_content_for_log( $args, $headers ) {
     144                $body = isset( $args['message'] ) ? (string) $args['message'] : '';
     145                if ( strlen( $body ) > self::MAX_LOGGED_BODY ) {
     146                        $body = function_exists( 'mb_strcut' ) ? mb_strcut( $body, 0, self::MAX_LOGGED_BODY, 'UTF-8' ) : substr( $body, 0, self::MAX_LOGGED_BODY );
     147                }
     148
     149                $kept         = array();
     150                $content_type = '';
     151                foreach ( $headers as $header ) {
     152                        if ( 0 === stripos( $header, 'X-BPTSM-ID:' ) ) {
     153                                continue;
     154                        }
     155                        $kept[] = $header;
     156                        if ( 0 === stripos( $header, 'Content-Type:' ) ) {
     157                                $content_type = $header;
     158                        }
     159                }
     160                if ( '' === $content_type ) {
     161                        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- reading a WordPress core filter.
     162                        $content_type = (string) apply_filters( 'wp_mail_content_type', 'text/plain' );
     163                }
     164                $is_html = false !== stripos( $content_type, 'text/html' ) || preg_match( '/<(html|body|table|div|p|br)\b/i', $body );
     165
     166                // Attachment file names only; the files themselves are never copied.
     167                $attachments = isset( $args['attachments'] ) ? $args['attachments'] : array();
     168                if ( is_string( $attachments ) ) {
     169                        $attachments = explode( "\n", str_replace( "\r\n", "\n", $attachments ) );
     170                }
     171                $names = array();
     172                foreach ( (array) $attachments as $name => $file ) {
     173                        if ( '' !== trim( (string) $file ) ) {
     174                                $names[] = is_string( $name ) ? $name : wp_basename( (string) $file );
     175                        }
     176                }
     177
     178                return array(
     179                        'body'         => $body,
     180                        'body_format'  => $is_html ? 'html' : 'text',
     181                        'mail_headers' => $kept ? wp_json_encode( $kept ) : null,
     182                        'attachments'  => $names ? wp_json_encode( $names ) : null,
     183                );
     184        }
     185
    134186        public function on_phpmailer_init( $phpmailer ) {
    135187                $smtp_enabled = (bool) get_option( 'bcs_smtp_enabled' );
    … …  
    137189                if ( $smtp_enabled ) {
    138190                        $this->configure_phpmailer( $phpmailer );
     191                        $this->attach_transcript_capture( $phpmailer );
    139192                }
    140193
    … …  
    165218        public function on_email_failed( $error ) {
    166219                if ( $this->db && $this->pending_log_id ) {
    167                         $msg = is_wp_error( $error ) ? $error->get_error_message() : 'Unknown error';
     220                        $msg = $this->describe_failure( is_wp_error( $error ) ? $error->get_error_message() : 'Unknown error' );
    168221                        $this->db->update_email_log_status( $this->pending_log_id, 'failed', $msg );
    169222
    170                         if ( get_option( BCS_SMTP_OPT_STORE_FAILED_BODY, false ) && '' !== $this->pending_body ) {
    171                                 $this->db->update_email_log_body( $this->pending_log_id, $this->pending_body, $this->pending_body_format );
    172                         }
    173 
    174                         $this->pending_log_id     = null;
    175                         $this->pending_body        = '';
    176                         $this->pending_body_format = 'text';
    177                 }
    178         }
    179 
    180         /* ─────────────────────────────────────────────────────────────────── */
    181         /* Resend a failed email                                                */
    182         /* ─────────────────────────────────────────────────────────────────── */
    183 
    184         /**
    185          * Resend a previously logged, failed email using its stored body.
    186          * Re-enters the normal wp_mail()/capture/log hook chain, so a fresh log row
    187          * is created for the resend attempt — the original row is untouched other
    188          * than its resend_count/last_resend_at audit fields.
    189          *
    190          * @param  int $log_id
    191          * @return array { success: bool, message: string }
    192          */
    193         public function resend_stored_email( $log_id ) {
    194                 if ( ! $this->db ) {
    195                         return array( 'success' => false, 'message' => __( 'Internal error — database unavailable.', 'bcs-smtp-manager' ) );
    196                 }
    197 
    198                 $row = $this->db->get_email_log_row( $log_id );
    199                 if ( ! $row ) {
    200                         return array( 'success' => false, 'message' => __( 'Log entry not found.', 'bcs-smtp-manager' ) );
    201                 }
    202                 if ( 'failed' !== $row['status'] ) {
    203                         return array( 'success' => false, 'message' => __( 'Only failed emails can be resent.', 'bcs-smtp-manager' ) );
    204                 }
    205                 if ( empty( $row['body'] ) ) {
    206                         return array( 'success' => false, 'message' => __( 'No stored content to resend — enable "Store failed email content" in the Config tab before the failure occurs.', 'bcs-smtp-manager' ) );
    207                 }
    208                 if ( empty( $row['to_address'] ) ) {
    209                         return array( 'success' => false, 'message' => __( 'Log entry has no recipient address.', 'bcs-smtp-manager' ) );
    210                 }
    211 
    212                 $headers = array();
    213                 if ( 'html' === $row['body_format'] ) {
    214                         $headers[] = 'Content-Type: text/html; charset=UTF-8';
    215                 }
    216 
    217                 $this->db->increment_resend_count( $log_id );
    218 
    219                 $sent = wp_mail( $row['to_address'], $row['subject'], $row['body'], $headers );
    220 
    221                 return array(
    222                         'success' => (bool) $sent,
    223                         'message' => $sent
    224                                 ? __( 'Resend attempted — check the Email Log for the new entry\'s delivery status.', 'bcs-smtp-manager' )
    225                                 : __( 'wp_mail() returned false when resending — check the new log entry for details.', 'bcs-smtp-manager' ),
    226                 );
     223                        $this->pending_log_id = null;
     224                }
     225        }
     226
     227        /* ─────────────────────────────────────────────────────────────────── */
     228        /* SMTP transcript capture (failure diagnostics)                       */
     229        /* ─────────────────────────────────────────────────────────────────── */
     230
     231        /** @var callable|null The Debugoutput closure we installed, so we can recognise our own. */
     232        private $transcript_cb = null;
     233
     234        /**
     235         * Capture PHPMailer's debug output so a failure can report the server's actual reply
     236         * (e.g. "535 5.7.139 Authentication unsuccessful, SmtpClientAuthentication is disabled")
     237         * instead of PHPMailer's generic "Could not authenticate.". Client lines are discarded, so
     238         * nothing the site sends — including credentials — is ever retained.
     239         *
     240         * @param \PHPMailer\PHPMailer\PHPMailer $phpmailer
     241         */
     242        private function attach_transcript_capture( $phpmailer ) {
     243                if ( null === $this->transcript_cb ) {
     244                        $this->transcript_cb = function ( $str ) {
     245                                $str = trim( (string) $str );
     246                                if ( '' === $str || 0 === strpos( $str, 'CLIENT -> SERVER' ) || count( $this->smtp_transcript ) >= 200 ) {
     247                                        return;
     248                                }
     249                                $this->smtp_transcript[] = $str;
     250                        };
     251                }
     252                // Respect a debug level another plugin or developer set deliberately.
     253                if ( 0 !== (int) $phpmailer->SMTPDebug && $phpmailer->Debugoutput !== $this->transcript_cb ) {
     254                        return;
     255                }
     256                $phpmailer->SMTPDebug   = 3; // SMTP::DEBUG_CONNECTION — includes connection-level errors.
     257                $phpmailer->Debugoutput = $this->transcript_cb;
     258        }
     259
     260        /**
     261         * Most relevant error line from the last SMTP conversation, or '' if none.
     262         *
     263         * @return string
     264         */
     265        public function get_last_smtp_error_reply() {
     266                $lines = array_reverse( $this->smtp_transcript );
     267                // Prefer the server's own 4xx/5xx reply — it carries the actionable reason.
     268                foreach ( $lines as $line ) {
     269                        if ( preg_match( '/^SERVER -> CLIENT:\s*([45]\d\d[\s-].*)$/', $line, $m ) ) {
     270                                return mb_substr( trim( $m[1] ), 0, 500 );
     271                        }
     272                }
     273                // Otherwise a connection-level error from PHPMailer's SMTP class (upper-case "SMTP ERROR:",
     274                // unlike PHPMailer's generic "SMTP Error: …" message that is already in the error text).
     275                foreach ( $lines as $line ) {
     276                        if ( 0 === strpos( $line, 'SMTP ERROR:' ) || false !== stripos( $line, 'Connection failed' ) ) {
     277                                return mb_substr( $line, 0, 500 );
     278                        }
     279                }
     280                return '';
     281        }
     282
     283        /**
     284         * Enrich a PHPMailer error message with the server's reply and, when relevant, the
     285         * unreadable-password explanation.
     286         *
     287         * @param  string $msg
     288         * @return string
     289         */
     290        public function describe_failure( $msg ) {
     291                $reply = $this->get_last_smtp_error_reply();
     292                if ( '' !== $reply && false === strpos( $msg, $reply ) ) {
     293                        $msg .= ' — Server reply: ' . $reply;
     294                }
     295                if ( get_option( 'bcs_smtp_enabled' ) && get_option( 'bcs_smtp_auth' ) && $this->is_password_unreadable() ) {
     296                        $msg .= ' ' . $this->unreadable_password_message();
     297                }
     298                return $msg;
    227299        }
    228300
    … …  
    232304
    233305        /**
    234          * Encrypt a plaintext password for storage in wp_options.
    235          * Keyed off this install's AUTH salt, so the ciphertext is unusable outside this site.
     306         * 32-byte key used to encrypt the SMTP password.
     307         *
     308         * Deliberately NOT derived from wp_salt(): security plugins rotate salts (AIOS "salt postfix"
     309         * changes them weekly; Solid Security / Sucuri regenerate them on demand), which silently made
     310         * the stored password undecryptable and every send fail with "Could not authenticate".
     311         * A BCS_SMTP_CRYPTO_KEY constant in wp-config.php takes precedence; otherwise a random key is
     312         * generated once and kept in its own non-autoloaded option.
     313         *
     314         * @return string Raw binary key.
     315         */
     316        private function get_crypto_key() {
     317                if ( defined( 'BCS_SMTP_CRYPTO_KEY' ) && '' !== (string) BCS_SMTP_CRYPTO_KEY ) {
     318                        return hash( 'sha256', (string) BCS_SMTP_CRYPTO_KEY, true );
     319                }
     320                $secret = get_option( BCS_SMTP_OPT_CRYPTO_KEY, '' );
     321                if ( '' === $secret ) {
     322                        add_option( BCS_SMTP_OPT_CRYPTO_KEY, wp_generate_password( 64, true, true ), '', false );
     323                        $secret = get_option( BCS_SMTP_OPT_CRYPTO_KEY, '' );
     324                }
     325                return hash( 'sha256', $secret, true );
     326        }
     327
     328        /**
     329         * Encrypt a plaintext password for storage in wp_options (AES-256-CBC + HMAC-SHA256).
    236330         *
    237331         * @param  string $plain
    238          * @return string Encrypted value (prefixed 'enc:'), or the plain value if openssl is unavailable.
     332         * @return string Encrypted value (prefixed 'enc2:'), or the plain value if openssl is unavailable.
    239333         */
    240334        public function encrypt_password( $plain ) {
    … …  
    242336                        return $plain;
    243337                }
    244                 $key    = hash( 'sha256', wp_salt( 'auth' ), true );
     338                $key    = $this->get_crypto_key();
    245339                $iv     = random_bytes( 16 );
    246340                $cipher = openssl_encrypt( $plain, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );
    … …  
    248342                        return $plain;
    249343                }
    250                 return 'enc:' . base64_encode( $iv . $cipher ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- ciphertext encoding, not obfuscation.
     344                $mac = hash_hmac( 'sha256', $iv . $cipher, $key, true );
     345                return 'enc2:' . base64_encode( $iv . $mac . $cipher ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- ciphertext encoding, not obfuscation.
    251346        }
    252347
    253348        /**
    254349         * Decrypt a password previously stored via encrypt_password().
    255          * Values without the 'enc:' prefix are treated as legacy plaintext (pre-encryption upgrade)
    256          * and returned as-is, so existing installs keep working until their next save.
     350         *
     351         * 'enc2:' — current format. 'enc:' — pre-1.2.2 format keyed off wp_salt('auth'); decrypted with
     352         * that legacy key and transparently re-encrypted to 'enc2:' so later salt changes can't break it.
     353         * Values with neither prefix are legacy plaintext and returned as-is.
    257354         *
    258355         * @param  string $stored
    259          * @return string
     356         * @return string Plaintext, or '' when the value can't be decrypted.
    260357         */
    261358        public function decrypt_password( $stored ) {
    262                 if ( '' === $stored || 0 !== strpos( $stored, 'enc:' ) ) {
     359                if ( '' === $stored ) {
     360                        return '';
     361                }
     362                if ( 0 === strpos( $stored, 'enc2:' ) ) {
     363                        if ( ! function_exists( 'openssl_decrypt' ) ) {
     364                                return '';
     365                        }
     366                        $raw = base64_decode( substr( $stored, 5 ), true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- ciphertext decoding, not obfuscation.
     367                        if ( false === $raw || strlen( $raw ) <= 48 ) {
     368                                return '';
     369                        }
     370                        $key    = $this->get_crypto_key();
     371                        $iv     = substr( $raw, 0, 16 );
     372                        $mac    = substr( $raw, 16, 32 );
     373                        $cipher = substr( $raw, 48 );
     374                        if ( ! hash_equals( hash_hmac( 'sha256', $iv . $cipher, $key, true ), $mac ) ) {
     375                                return '';
     376                        }
     377                        $plain = openssl_decrypt( $cipher, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );
     378                        return false === $plain ? '' : $plain;
     379                }
     380                if ( 0 !== strpos( $stored, 'enc:' ) ) {
    263381                        return $stored;
    264382                }
    … …  
    274392                $key    = hash( 'sha256', wp_salt( 'auth' ), true );
    275393                $plain  = openssl_decrypt( $cipher, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );
    276                 return false === $plain ? '' : $plain;
     394                if ( false === $plain ) {
     395                        return '';
     396                }
     397                // Migrate to the salt-independent format while the legacy key still works.
     398                if ( get_option( 'bcs_smtp_password', '' ) === $stored ) {
     399                        update_option( 'bcs_smtp_password', $this->encrypt_password( $plain ) );
     400                }
     401                return $plain;
    277402        }
    278403
    … …  
    280405        public function get_smtp_password() {
    281406                return $this->decrypt_password( get_option( 'bcs_smtp_password', '' ) );
     407        }
     408
     409        /**
     410         * True when a password is saved but can no longer be decrypted (e.g. it was saved under
     411         * security keys that have since changed, or the database was migrated to another site).
     412         */
     413        public function is_password_unreadable() {
     414                $stored = get_option( 'bcs_smtp_password', '' );
     415                return '' !== $stored && '' === $this->decrypt_password( $stored );
     416        }
     417
     418        /** User-facing explanation for is_password_unreadable(). */
     419        public function unreadable_password_message() {
     420                return __( 'The saved SMTP password can no longer be decrypted — this happens when WordPress security keys/salts change (e.g. a security plugin rotated them) or the database was moved to another site. Re-enter the password in SMTP Manager → Settings and save.', 'bcs-smtp-manager' );
    282421        }
    283422
    … …  
    307446                $phpmailer->Host = $host;
    308447                $phpmailer->Port = $port;
     448
     449                // PHPMailer's default 300s timeout outlives PHP's max_execution_time, so a firewalled port
     450                // kills the request with a fatal error instead of returning a readable failure.
     451                if ( 300 === (int) $phpmailer->Timeout ) {
     452                        $phpmailer->Timeout = 20;
     453                }
    309454
    310455                switch ( $encryption ) {
    … …  
    339484
    340485        /* ─────────────────────────────────────────────────────────────────── */
    341         /* SMTP Plugin Detection (used by Config tab)                           */
     486        /* SMTP Plugin Detection (used by the Settings tab)                     */
    342487        /* ─────────────────────────────────────────────────────────────────── */
    343488
    … …  
    503648                        return array_merge( $base, array(
    504649                                'success'      => false,
    505                                 'error_msg'    => 'No SMTP host configured. Enter your SMTP server details in the Settings tab and save first.',
     650                                'error_msg'    => __( 'No SMTP server is set up yet. Enter your server details on the Connection tab and save.', 'bcs-smtp-manager' ),
    506651                                'failure_type' => 'not_configured',
    507652                        ) );
    … …  
    513658                                return array_merge( $base, array(
    514659                                        'success'      => false,
    515                                         'error_msg'    => "PHP mail() is not available on this server. Configure SMTP in the Settings tab for reliable email delivery.",
     660                                        'error_msg'    => __( 'PHP mail() is not available on this server. Set up SMTP on the Connection tab for reliable email delivery.', 'bcs-smtp-manager' ),
    516661                                        'failure_type' => 'mail_unavailable',
    517662                                ) );
    … …  
    524669                }
    525670
     671                // Built-in SMTP: run the exact connect/STARTTLS/AUTH sequence a real send uses.
     672                if ( get_option( 'bcs_smtp_enabled' ) ) {
     673                        return array_merge( $base, $this->test_builtin_smtp() );
     674                }
     675
     676                // Another SMTP plugin is in charge — we don't hold its credentials, so only
     677                // reachability and the TLS handshake are checked below.
    526678                $encryption = strtolower( $info['encryption'] ?: 'tls' );
    527679                $timeout    = 8;
    … …  
    630782                }
    631783
    632                 // Test SMTP AUTH LOGIN when credentials are configured. Reachable for SSL, successfully-upgraded
    633                 // TLS, and also plain/'none' connections — matching exactly what a real send would attempt, so a
    634                 // bad username/password doesn't get silently reported as a healthy connection.
    635                 $auth     = (bool) get_option( 'bcs_smtp_auth', false );
    636                 $username = get_option( 'bcs_smtp_username', '' );
    637                 $password = $this->get_smtp_password();
    638 
    639                 if ( $auth && $username && $password ) {
    640                         fwrite( $socket, "AUTH LOGIN\r\n" );
    641                         $auth_resp = fgets( $socket, 1024 );
    642                         if ( '334' === substr( trim( $auth_resp ), 0, 3 ) ) {
    643                                 fwrite( $socket, base64_encode( $username ) . "\r\n" );
    644                                 $user_resp = fgets( $socket, 1024 );
    645                                 if ( '334' === substr( trim( $user_resp ), 0, 3 ) ) {
    646                                         fwrite( $socket, base64_encode( $password ) . "\r\n" );
    647                                         $pass_resp = fgets( $socket, 1024 );
    648                                         fwrite( $socket, "QUIT\r\n" );
    649                                         fclose( $socket );
    650                                         if ( '235' !== substr( trim( $pass_resp ), 0, 3 ) ) {
    651                                                 return array_merge( $base, array(
    652                                                         'success'      => false,
    653                                                         'error_msg'    => 'Authentication failed: ' . trim( $pass_resp ) . ' — Verify your username and password (or app password).',
    654                                                         'failure_type' => 'auth_failure',
    655                                                 ) );
    656                                         }
    657                                         return array_merge( $base, array(
    658                                                 'success'      => true,
    659                                                 'error_msg'    => null,
    660                                                 'failure_type' => null,
    661                                         ) );
    662                                 }
    663                         }
    664                         // AUTH LOGIN not accepted — server may use a different mechanism.
    665                         fwrite( $socket, "QUIT\r\n" );
    666                         fclose( $socket );
    667                         return array_merge( $base, array(
    668                                 'success'      => true,
    669                                 'error_msg'    => null,
    670                                 'failure_type' => null,
    671                                 'auth_note'    => 'Connection OK — AUTH LOGIN not supported by this server; credentials will be verified on send.',
    672                         ) );
    673                 }
    674 
    675784                fwrite( $socket, "QUIT\r\n" );
    676785                fclose( $socket );
    … …  
    684793        }
    685794
     795        /**
     796         * Connection + authentication test for the built-in SMTP config, using PHPMailer itself so
     797         * every AUTH mechanism, TLS option and phpmailer_init customisation matches a real send.
     798         * Nothing is sent — the session ends right after authentication.
     799         *
     800         * @return array {success, error_msg, failure_type}
     801         */
     802        private function test_builtin_smtp() {
     803                $fail = function ( $msg, $type ) {
     804                        return array( 'success' => false, 'error_msg' => $msg, 'failure_type' => $type );
     805                };
     806
     807                if ( get_option( 'bcs_smtp_auth', false ) ) {
     808                        if ( $this->is_password_unreadable() ) {
     809                                return $fail( $this->unreadable_password_message(), 'auth_failure' );
     810                        }
     811                        if ( '' === (string) get_option( 'bcs_smtp_username', '' ) || '' === $this->get_smtp_password() ) {
     812                                return $fail( __( 'Sign-in is turned on but the username or password is empty. Enter both on the Connection tab and save.', 'bcs-smtp-manager' ), 'auth_failure' );
     813                        }
     814                }
     815
     816                $phpmailer = $this->create_phpmailer();
     817                if ( ! $phpmailer ) {
     818                        return $fail( __( 'PHPMailer is not available — WordPress 5.5 or newer is required for the connection test.', 'bcs-smtp-manager' ), 'plugin_failure' );
     819                }
     820
     821                // Apply phpmailer_init exactly as wp_mail() does, so a conflicting plugin shows up here too.
     822                // Our own handler runs as part of it; park the pending log row so it isn't touched.
     823                $pending              = $this->pending_log_id;
     824                $this->pending_log_id = null;
     825                do_action_ref_array( 'phpmailer_init', array( &$phpmailer ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- firing the core hook, as wp_mail() does.
     826                $this->pending_log_id = $pending;
     827
     828                if ( 'smtp' !== $phpmailer->Mailer ) {
     829                        return $fail( __( 'Another plugin switched PHPMailer away from SMTP during phpmailer_init, so these SMTP settings are not being used for delivery.', 'bcs-smtp-manager' ), 'plugin_failure' );
     830                }
     831
     832                $this->smtp_transcript = array();
     833                $this->attach_transcript_capture( $phpmailer );
     834                $phpmailer->Timeout = min( (int) $phpmailer->Timeout, 15 );
     835
     836                $error = '';
     837                try {
     838                        $ok = $phpmailer->smtpConnect();
     839                } catch ( \Exception $e ) {
     840                        $ok    = false;
     841                        $error = $e->getMessage();
     842                }
     843
     844                // Connecting (and signing in, when enabled) is not proof that mail can be sent:
     845                // e.g. Microsoft 365 accepts the connection without AUTH but rejects every message
     846                // with "530 5.7.57 Client not authenticated". Walk the envelope a real send uses —
     847                // MAIL FROM and RCPT TO — then RSET before DATA, so nothing is delivered.
     848                if ( $ok ) {
     849                        $envelope_error = $this->test_envelope( $phpmailer );
     850                        if ( '' !== $envelope_error ) {
     851                                $ok    = false;
     852                                $error = $envelope_error;
     853                        }
     854                }
     855                $phpmailer->smtpClose();
     856
     857                if ( $ok ) {
     858                        return array( 'success' => true, 'error_msg' => null, 'failure_type' => null );
     859                }
     860
     861                $msg = $this->describe_failure( $error ?: ( $phpmailer->ErrorInfo ?: __( 'Could not connect to the SMTP server.', 'bcs-smtp-manager' ) ) );
     862                return $fail( $msg, $this->classify_failure( $msg ) );
     863        }
     864
     865        /**
     866         * Ask the server to accept a sender and a recipient on an open connection, then reset
     867         * the transaction before any message data is sent.
     868         *
     869         * @param \PHPMailer\PHPMailer\PHPMailer $phpmailer Connected (and authenticated, if enabled) mailer.
     870         * @return string Empty when the server accepts the envelope, otherwise an error with the server's reply.
     871         */
     872        private function test_envelope( $phpmailer ) {
     873                $smtp = $phpmailer->getSMTPInstance();
     874                if ( ! $smtp || ! $smtp->connected() ) {
     875                        return '';
     876                }
     877
     878                $from = $phpmailer->From;
     879                if ( ! is_email( $from ) ) {
     880                        $from = get_option( 'bcs_from_email', '' );
     881                }
     882                if ( ! is_email( $from ) ) {
     883                        $from = get_option( 'admin_email' );
     884                }
     885                $to = get_option( 'admin_email' );
     886
     887                $no_auth = ! get_option( 'bcs_smtp_auth', false );
     888                $prefix  = $no_auth
     889                        ? __( 'The server refused to send mail without signing in. Turn on "My server needs a username and password" and enter your credentials.', 'bcs-smtp-manager' ) . ' '
     890                        : '';
     891
     892                if ( ! $smtp->mail( $from ) ) {
     893                        $reply = trim( $smtp->getLastReply() );
     894                        $smtp->reset();
     895                        /* translators: 1: sender email address, 2: the SMTP server's reply. */
     896                        return $prefix . sprintf( __( 'The server would not accept mail from %1$s. Server reply: %2$s', 'bcs-smtp-manager' ), $from, $reply ? $reply : __( 'no response', 'bcs-smtp-manager' ) );
     897                }
     898                if ( is_email( $to ) && ! $smtp->recipient( $to ) ) {
     899                        $reply = trim( $smtp->getLastReply() );
     900                        $smtp->reset();
     901                        /* translators: 1: recipient email address, 2: the SMTP server's reply. */
     902                        return $prefix . sprintf( __( 'The server would not deliver to %1$s. Server reply: %2$s', 'bcs-smtp-manager' ), $to, $reply ? $reply : __( 'no response', 'bcs-smtp-manager' ) );
     903                }
     904                $smtp->reset();
     905                return '';
     906        }
     907
     908        /** @return \PHPMailer\PHPMailer\PHPMailer|null A fresh, exception-throwing PHPMailer instance. */
     909        private function create_phpmailer() {
     910                if ( ! class_exists( '\PHPMailer\PHPMailer\PHPMailer' ) && file_exists( ABSPATH . WPINC . '/PHPMailer/PHPMailer.php' ) ) {
     911                        require_once ABSPATH . WPINC . '/PHPMailer/PHPMailer.php';
     912                        require_once ABSPATH . WPINC . '/PHPMailer/SMTP.php';
     913                        require_once ABSPATH . WPINC . '/PHPMailer/Exception.php';
     914                }
     915                return class_exists( '\PHPMailer\PHPMailer\PHPMailer' ) ? new \PHPMailer\PHPMailer\PHPMailer( true ) : null;
     916        }
     917
    686918        /* ─────────────────────────────────────────────────────────────────── */
    687919        /* Manual Test Email Send                                               */
    … …  
    711943
    712944                $headers = array( 'Content-Type: text/html; charset=UTF-8' );
     945
     946                // Fail fast with the real reason instead of PHPMailer's generic "Could not authenticate."
     947                if ( get_option( 'bcs_smtp_enabled' ) && get_option( 'bcs_smtp_auth' ) && $this->is_password_unreadable() ) {
     948                        return array(
     949                                'success'      => false,
     950                                'error_msg'    => $this->unreadable_password_message(),
     951                                'failure_type' => 'auth_failure',
     952                                'smtp_host'    => $info['host'],
     953                                'smtp_port'    => $info['port'],
     954                                'mailbox'      => $info['mailbox'],
     955                                'source_ip'    => $this->get_source_ip(),
     956                        );
     957                }
    713958
    714959                $captured_error = '';
    … …  
    745990                }
    746991
     992                $error_msg = $captured_error ? $this->describe_failure( $captured_error ) : 'wp_mail() returned false — no additional error details.';
    747993                return array(
    748994                        'success'      => false,
    749                         'error_msg'    => $captured_error ?: 'wp_mail() returned false — no additional error details.',
    750                         'failure_type' => $this->classify_failure( $captured_error ),
     995                        'error_msg'    => $error_msg,
     996                        'failure_type' => $this->classify_failure( $error_msg ),
    751997                        'smtp_host'    => $info['host'],
    752998                        'smtp_port'    => $info['port'],
    … …  
    7971043
    7981044        private function build_test_email_html( $site_name, $site_url, $timestamp, $info, $to ) {
    799                 return BCS_SMTP_Email_Template::wrapper_open( 'SMTP Test', 580, 14, '.08' ) . '
    800   <!-- Header — bgcolor attr = Outlook fallback; background-color = Yahoo/other fallback; background-image = gradient for supporting clients -->
    801   <tr><td bgcolor="#10b981" style="background-color:#10b981;background-image:linear-gradient(135deg,#10b981 0%,#059669 100%);padding:36px 40px;text-align:center">
    802     <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center"><![endif]-->
    803     <div style="width:56px;height:56px;background:#34d399;border-radius:50%;margin:0 auto 16px">
    804       <span style="font-size:28px;line-height:56px;display:block;text-align:center">✓</span>
    805     </div>
    806     <!--[if mso]></td></tr></table><![endif]-->
    807     <h1 style="margin:0;font-size:24px;font-weight:700;color:#ffffff;letter-spacing:-0.3px">Email Delivery Working</h1>
    808     <p style="margin:8px 0 0;font-size:14px;color:#d1fae5">Your SMTP configuration is set up correctly</p>
    809   </td></tr>
    810 
    811   <!-- Body -->
    812   <tr><td style="padding:36px 40px">
    813     <p style="margin:0 0 24px;font-size:15px;color:#374151;line-height:1.6">
    814       This is a test email from <strong>' . esc_html( $site_name ) . '</strong> to confirm that email delivery is working as expected through ByteCoreStack SMTP.
    815     </p>
    816 
    817     <!-- Info grid -->
    818     <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden;margin-bottom:24px">
    819       <tr style="border-bottom:1px solid #e2e8f0">
    820         <td style="padding:13px 18px;font-size:12px;color:#6b7280;font-weight:600;text-transform:uppercase;letter-spacing:.5px;width:38%;background:#f1f5f9">Recipient</td>
    821         <td style="padding:13px 18px;font-size:13px;color:#1e293b;font-family:monospace">' . esc_html( $to ) . '</td>
    822       </tr>
    823       <tr style="border-bottom:1px solid #e2e8f0">
    824         <td style="padding:13px 18px;font-size:12px;color:#6b7280;font-weight:600;text-transform:uppercase;letter-spacing:.5px;background:#f1f5f9">SMTP Host</td>
    825         <td style="padding:13px 18px;font-size:13px;color:#1e293b;font-family:monospace">' . esc_html( $info['host'] ?: 'PHP mail()' ) . ( $info['port'] ? ':' . esc_html( $info['port'] ) : '' ) . '</td>
    826       </tr>
    827       <tr style="border-bottom:1px solid #e2e8f0">
    828         <td style="padding:13px 18px;font-size:12px;color:#6b7280;font-weight:600;text-transform:uppercase;letter-spacing:.5px;background:#f1f5f9">Plugin</td>
    829         <td style="padding:13px 18px;font-size:13px;color:#1e293b">' . esc_html( $info['plugin'] ) . '</td>
    830       </tr>
    831       <tr>
    832         <td style="padding:13px 18px;font-size:12px;color:#6b7280;font-weight:600;text-transform:uppercase;letter-spacing:.5px;background:#f1f5f9">Sent At</td>
    833         <td style="padding:13px 18px;font-size:13px;color:#1e293b">' . esc_html( $timestamp ) . '</td>
    834       </tr>
    835     </table>
    836 
    837     <div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:14px 18px;margin-bottom:0">
    838       <p style="margin:0;font-size:13px;color:#15803d;line-height:1.5">
    839         <strong>All good!</strong> If you received this email, your WordPress site can send emails successfully. Check the Email Log tab in ByteCoreStack SMTP for delivery history.
    840       </p>
    841     </div>
    842   </td></tr>
    843 
    844   <!-- Footer -->
    845   <tr><td style="padding:20px 40px 28px;border-top:1px solid #f1f5f9;text-align:center">
    846     ' . BCS_SMTP_Email_Template::brand_footer_line( $site_url, $site_name ) . '
    847   </td></tr>
    848 ' . BCS_SMTP_Email_Template::wrapper_close();
     1045                $T      = 'BCS_SMTP_Email_Template';
     1046                $server = $info['host'] ? $info['host'] . ( $info['port'] ? ':' . $info['port'] : '' ) : __( 'PHP mail() — no SMTP server', 'bcs-smtp-manager' );
     1047
     1048                $body = '
     1049      <tr><td class="bcs-pad" style="padding:36px 40px 0">
     1050        ' . $T::pill( __( 'Delivered', 'bcs-smtp-manager' ), 'ok' ) . '
     1051        <h1 class="bcs-ink bcs-h1" style="margin:18px 0 10px;font-size:28px;line-height:1.2;font-weight:700;letter-spacing:-.4px;color:#0f172a">' . esc_html__( 'Your site can send email', 'bcs-smtp-manager' ) . '</h1>
     1052        <p class="bcs-ink2" style="margin:0;font-size:15px;line-height:1.65;color:#475569">' .
     1053                        /* translators: %s: site name. */
     1054                        esc_html( sprintf( __( '%s sent this message through your mail server. Contact forms, password resets and every other WordPress email go out the same way.', 'bcs-smtp-manager' ), $site_name ) ) . '</p>
     1055      </td></tr>
     1056      <tr><td class="bcs-pad" style="padding:26px 40px 0">
     1057        ' . $T::details( array(
     1058                        array( __( 'Sent to', 'bcs-smtp-manager' ), $to, true ),
     1059                        array( __( 'Mail server', 'bcs-smtp-manager' ), $server, true ),
     1060                        array( __( 'Sent by', 'bcs-smtp-manager' ), $info['plugin'] ),
     1061                        array( __( 'Time', 'bcs-smtp-manager' ), $timestamp ),
     1062                ) ) . '
     1063      </td></tr>
     1064      <tr><td class="bcs-pad" style="padding:28px 40px 36px">
     1065        ' . $T::button( $T::settings_url( 'email-log' ), __( 'Open the email log', 'bcs-smtp-manager' ) ) . '
     1066      </td></tr>';
     1067
     1068                return $T::document( array(
     1069                        'title'     => __( 'Test email', 'bcs-smtp-manager' ),
     1070                        /* translators: %s: site name. */
     1071                        'preheader' => sprintf( __( 'Good news: %s can send email. This test arrived through your mail server.', 'bcs-smtp-manager' ), $site_name ),
     1072                        'label'     => __( 'Test email', 'bcs-smtp-manager' ),
     1073                        'body'      => $body,
     1074                        'footer'    => $T::footer( $T::settings_url( 'connection' ), __( 'Connection settings', 'bcs-smtp-manager' ) ),
     1075                ) );
    8491076        }
    8501077
    … …  
    8621089                }
    8631090                // Loopback or missing SERVER_ADDR — try external lookup (e.g. local dev / Docker).
     1091                // Cached for a day so health checks every 15 minutes don't each make a request.
     1092                $cached = get_transient( 'bcs_smtp_source_ip' );
     1093                if ( false !== $cached ) {
     1094                        return $cached;
     1095                }
     1096                $ip       = 'unknown';
    8641097                $response = wp_remote_get( 'https://api.ipify.org', array( 'timeout' => 5 ) );
    8651098                if ( ! is_wp_error( $response ) ) {
    866                         $ip = trim( wp_remote_retrieve_body( $response ) );
    867                         if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) { return $ip; }
    868                 }
    869                 return 'unknown';
     1099                        $body = trim( wp_remote_retrieve_body( $response ) );
     1100                        if ( filter_var( $body, FILTER_VALIDATE_IP ) ) { $ip = $body; }
     1101                }
     1102                set_transient( 'bcs_smtp_source_ip', $ip, DAY_IN_SECONDS );
     1103                return $ip;
    8701104        }
    8711105}
  • bcs-smtp-manager/tags/1.3.0/includes/class-bcs-smtp-monitor.php

    r3579822 r3762759  
    4444
    4545        private function get_interval_minutes() {
    46                 $mins = (int) get_option( BCS_SMTP_OPT_CHECK_INTERVAL, 5 );
    47                 return max( 1, min( 1440, $mins ) );
     46                return (int) BCS_SMTP_CHECK_INTERVAL_MINUTES;
     47        }
     48
     49        /**
     50         * Health checks run whenever the plugin is sending through a configured SMTP server.
     51         *
     52         * @return bool
     53         */
     54        public static function is_active() {
     55                return (bool) get_option( 'bcs_smtp_enabled', false ) && '' !== trim( (string) get_option( 'bcs_smtp_host', '' ) );
     56        }
     57
     58        /**
     59         * Channels that receive failure alerts. Older versions stored a single channel as a string.
     60         *
     61         * @return string[]
     62         */
     63        public static function get_channels() {
     64                $saved = get_option( BCS_SMTP_OPT_ALERT_CHANNEL, array() );
     65                $saved = is_array( $saved ) ? $saved : array( $saved );
     66                return array_values( array_intersect( array_keys( self::channel_labels() ), $saved ) );
     67        }
     68
     69        /**
     70         * @return array Channel key => display name.
     71         */
     72        public static function channel_labels() {
     73                return array(
     74                        'slack'       => __( 'Slack', 'bcs-smtp-manager' ),
     75                        'teams'       => __( 'Microsoft Teams', 'bcs-smtp-manager' ),
     76                        'google_chat' => __( 'Google Chat', 'bcs-smtp-manager' ),
     77                        'discord'     => __( 'Discord', 'bcs-smtp-manager' ),
     78                        'custom'      => __( 'Custom webhook', 'bcs-smtp-manager' ),
     79                );
     80        }
     81
     82        /**
     83         * Human list of the given channels, e.g. "Slack and Discord".
     84         *
     85         * @param  string[] $channels
     86         * @return string
     87         */
     88        public static function channel_list_label( $channels ) {
     89                $labels = self::channel_labels();
     90                $names  = array();
     91                foreach ( $channels as $ch ) {
     92                        if ( isset( $labels[ $ch ] ) ) {
     93                                $names[] = $labels[ $ch ];
     94                        }
     95                }
     96                return wp_sprintf_l( '%l', $names );
     97        }
     98
     99        /**
     100         * Book the next scheduled check one interval after a check that just ran. Every place that
     101         * records a check calls this (cron runs late, and manual checks happen any time), so
     102         * "last check" and "next check" always add up to the interval.
     103         */
     104        public function schedule_next_check() {
     105                wp_clear_scheduled_hook( 'bcs_smtp_health_check' );
     106                if ( self::is_active() ) {
     107                        wp_schedule_event( time() + $this->get_interval_minutes() * 60, 'bcs_smtp_monitor', 'bcs_smtp_health_check' );
     108                }
    48109        }
    49110
    50111        public function maybe_schedule_event() {
    51                 $enabled = get_option( BCS_SMTP_OPT_MONITOR_ENABLED, false );
     112                $enabled = self::is_active();
    52113                $next    = wp_next_scheduled( 'bcs_smtp_health_check' );
    53114
    … …  
    64125        /* ─────────────────────────────────────────────────────────────────── */
    65126
    66         public function run_health_check() {
    67                 if ( ! get_option( BCS_SMTP_OPT_MONITOR_ENABLED, false ) ) {
    68                         return;
     127        /**
     128         * @param  bool $force True for a manual "Run Health Check Now" — runs even when scheduled
     129         *                     monitoring is off or a cron check holds the lock.
     130         * @return array|null  The connection-test result, or null when the check was skipped.
     131         */
     132        public function run_health_check( $force = false ) {
     133                if ( ! $force && ! self::is_active() ) {
     134                        return null;
    69135                }
    70136
    71137                // Prevent concurrent runs (WP-Cron + manual AJAX can overlap).
    72                 if ( get_transient( 'bcs_smtp_check_lock' ) ) {
    73                         return;
     138                if ( ! $force && get_transient( 'bcs_smtp_check_lock' ) ) {
     139                        return null;
    74140                }
    75141                $mins = $this->get_interval_minutes();
    … …  
    81147                update_option( BCS_SMTP_OPT_LAST_CHECK, time() );
    82148                update_option( BCS_SMTP_OPT_LAST_CHECK_SUCCESS, $result['success'] );
     149                $this->schedule_next_check();
    83150
    84151                if ( $result['success'] ) {
    … …  
    89156
    90157                delete_transient( 'bcs_smtp_check_lock' );
     158                return $result;
    91159        }
    92160
    … …  
    104172                update_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, $failures );
    105173
    106                 $threshold = (int) get_option( BCS_SMTP_OPT_FAILURE_THRESHOLD, 2 );
     174                $threshold = (int) BCS_SMTP_FAILURE_THRESHOLD;
    107175
    108176                if ( $failures >= $threshold ) {
    … …  
    149217        /* ─────────────────────────────────────────────────────────────────── */
    150218
    151         public function send_alert( $result, $failures, $external_down = false ) {
     219        /**
     220         * Dispatch a failure alert to every selected channel.
     221         *
     222         * @param  array $result
     223         * @param  int   $failures
     224         * @param  bool  $external_down
     225         * @param  bool  $is_test True for "Send test alert": ignores the one-per-outage limit and doesn't count toward it.
     226         * @return array|\WP_Error Channel key => true or WP_Error, or a WP_Error when nothing was attempted.
     227         */
     228        public function send_alert( $result, $failures, $external_down = false, $is_test = false ) {
    152229                if ( ! get_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED, false ) ) {
    153                         return;
    154                 }
    155 
    156                 $channel = get_option( BCS_SMTP_OPT_ALERT_CHANNEL, 'none' );
    157                 if ( 'none' === $channel ) {
    158                         return;
    159                 }
    160 
    161                 // Respect the max notification cap (0 = unlimited).
    162                 $max_count  = (int) get_option( BCS_SMTP_OPT_MAX_ALERT_COUNT, 0 );
     230                        return new \WP_Error( 'bcs_alert_disabled', __( 'Failure alerts are turned off.', 'bcs-smtp-manager' ) );
     231                }
     232
     233                $channels = self::get_channels();
     234                if ( ! $channels ) {
     235                        return new \WP_Error( 'bcs_alert_no_channel', __( 'No alert channel selected.', 'bcs-smtp-manager' ) );
     236                }
     237
     238                // One alert per outage; the counter resets when a check passes again.
     239                $max_count  = (int) BCS_SMTP_MAX_ALERTS_PER_OUTAGE;
    163240                $sent_count = (int) get_option( BCS_SMTP_OPT_ALERT_SENT_COUNT, 0 );
    164                 if ( $max_count > 0 && $sent_count >= $max_count ) {
    165                         return;
    166                 }
    167 
    168                 $payload = $this->build_alert_payload( $result, $failures, $external_down );
    169 
    170                 switch ( $channel ) {
    171                         case 'slack':
    172                                 $this->send_slack( $payload );
    173                                 break;
    174                         case 'teams':
    175                                 $this->send_teams( $payload );
    176                                 break;
    177                         case 'google_chat':
    178                                 $this->send_google_chat( $payload );
    179                                 break;
    180                         case 'discord':
    181                                 $this->send_discord( $payload );
    182                                 break;
    183                         case 'custom':
    184                                 $this->send_custom_webhook( $payload );
    185                                 break;
    186                 }
    187 
    188                 update_option( BCS_SMTP_OPT_ALERT_SENT_COUNT, $sent_count + 1 );
     241                if ( ! $is_test && $max_count > 0 && $sent_count >= $max_count ) {
     242                        return new \WP_Error( 'bcs_alert_capped', __( 'Notification limit reached for this failure cycle.', 'bcs-smtp-manager' ) );
     243                }
     244
     245                $payload = $this->build_alert_payload( $result, $failures, $external_down, $is_test );
     246                $senders = array(
     247                        'slack'       => 'send_slack',
     248                        'teams'       => 'send_teams',
     249                        'google_chat' => 'send_google_chat',
     250                        'discord'     => 'send_discord',
     251                        'custom'      => 'send_custom_webhook',
     252                );
     253
     254                $results   = array();
     255                $delivered = false;
     256                foreach ( $channels as $channel ) {
     257                        $results[ $channel ] = $this->{ $senders[ $channel ] }( $payload );
     258                        $delivered           = $delivered || true === $results[ $channel ];
     259                }
     260
     261                // Only a delivered alert counts toward the limit, so a webhook outage doesn't use it up.
     262                if ( $delivered && ! $is_test ) {
     263                        update_option( BCS_SMTP_OPT_ALERT_SENT_COUNT, $sent_count + 1 );
     264                }
     265                return $results;
     266        }
     267
     268        /**
     269         * POST a JSON body to a webhook and turn the response into true or a descriptive WP_Error.
     270         *
     271         * @param  string $webhook
     272         * @param  array  $body
     273         * @return true|\WP_Error
     274         */
     275        private function post_webhook( $webhook, $body ) {
     276                $response = wp_safe_remote_post( $webhook, array(
     277                        'body'    => wp_json_encode( $body ),
     278                        'headers' => array( 'Content-Type' => 'application/json' ),
     279                        'timeout' => 10,
     280                ) );
     281                if ( is_wp_error( $response ) ) {
     282                        /* translators: %s: transport error message. */
     283                        return new \WP_Error( 'bcs_alert_transport', sprintf( __( 'Webhook request failed: %s', 'bcs-smtp-manager' ), $response->get_error_message() ) );
     284                }
     285                $code = (int) wp_remote_retrieve_response_code( $response );
     286                if ( $code < 200 || $code >= 300 ) {
     287                        $detail = trim( wp_strip_all_tags( (string) wp_remote_retrieve_body( $response ) ) );
     288                        /* translators: 1: HTTP status code, 2: response body excerpt. */
     289                        return new \WP_Error( 'bcs_alert_http', sprintf( __( 'Webhook returned HTTP %1$d. %2$s', 'bcs-smtp-manager' ), $code, mb_substr( $detail, 0, 200 ) ) );
     290                }
     291                return true;
     292        }
     293
     294        /** @return \WP_Error For a channel whose webhook URL is missing or not on the provider's domain. */
     295        private function missing_webhook_error( $label ) {
     296                /* translators: %s: channel name, e.g. Slack. */
     297                return new \WP_Error( 'bcs_alert_no_url', sprintf( __( 'No valid %s webhook URL is saved — enter it in the Alerts tab and click Save Alert Settings.', 'bcs-smtp-manager' ), $label ) );
    189298        }
    190299
    … …  
    213322                if ( 'teams' === $channel ) {
    214323                        // Microsoft has used several domains for Incoming Webhooks / Workflows over time.
    215                         foreach ( array( 'webhook.office.com', 'office.com', 'logic.azure.com' ) as $suffix ) {
     324                        foreach ( array( 'webhook.office.com', 'office.com', 'logic.azure.com', 'api.powerplatform.com' ) as $suffix ) {
    216325                                if ( $host === $suffix || ( strlen( $host ) > strlen( $suffix ) + 1 && '.' . $suffix === substr( $host, -( strlen( $suffix ) + 1 ) ) ) ) {
    217326                                        return true;
    … …  
    250359        /* ─────────────────────────────────────────────────────────────────── */
    251360
    252         public function build_alert_payload( $result, $failures, $external_down = false ) {
     361        /**
     362         * Everything an alert needs, in one place. The chat channels all show the same message
     363         * (title, explanation, fields, link); the custom webhook keeps its machine-readable keys.
     364         *
     365         * @param  array $result        Connection-test result.
     366         * @param  int   $failures      Consecutive failed checks.
     367         * @param  bool  $external_down True when the provider's status page reports a problem.
     368         * @param  bool  $is_test       True for "Send test alert".
     369         * @return array
     370         */
     371        public function build_alert_payload( $result, $failures, $external_down = false, $is_test = false ) {
    253372                $info       = $this->mailer->detect_smtp_plugin();
    254373                $site_name  = get_bloginfo( 'name' );
    255374                $site_url   = get_bloginfo( 'url' );
    256                 $timestamp  = current_time( 'M j, Y \a\t g:i A' );
    257                 $fail_label = $this->mailer->label_failure_type( isset( $result['failure_type'] ) ? $result['failure_type'] : '' );
    258                 $ext_note   = $external_down ? ' (External SMTP service may be down — check provider status.)' : '';
     375                $type       = isset( $result['failure_type'] ) ? $result['failure_type'] : '';
     376                $mailbox    = isset( $result['mailbox'] ) ? $result['mailbox'] : $info['mailbox'];
     377                $smtp_host  = isset( $result['smtp_host'] ) ? $result['smtp_host'] : $info['host'];
     378                $smtp_port  = isset( $result['smtp_port'] ) ? $result['smtp_port'] : $info['port'];
     379                $error_msg  = isset( $result['error_msg'] ) ? $result['error_msg'] : __( 'No details available.', 'bcs-smtp-manager' );
     380                $when       = wp_date( get_option( 'date_format', 'M j, Y' ) . ' ' . get_option( 'time_format', 'g:i a' ) );
     381                $server     = $smtp_host ? $smtp_host . ( $smtp_port ? ':' . $smtp_port : '' ) : __( 'Not set', 'bcs-smtp-manager' );
     382
     383                if ( $is_test ) {
     384                        /* translators: %s: site name. */
     385                        $title   = sprintf( __( 'Test alert from %s', 'bcs-smtp-manager' ), $site_name );
     386                        $summary = __( 'Your email alerts are working. If email stops sending on this site, an alert like this one will arrive here. No action needed.', 'bcs-smtp-manager' );
     387                        $fields  = array(
     388                                __( 'Mail server', 'bcs-smtp-manager' ) => $server,
     389                                __( 'Sending as', 'bcs-smtp-manager' )  => $mailbox,
     390                                __( 'Sent', 'bcs-smtp-manager' )        => $when,
     391                        );
     392                } else {
     393                        /* translators: %s: site name. */
     394                        $title   = sprintf( __( 'Email is failing on %s', 'bcs-smtp-manager' ), $site_name );
     395                        $summary = __( 'WordPress can’t send email through your mail server, so emails from your site may not arrive. Check the error below.', 'bcs-smtp-manager' );
     396                        $fields  = array(
     397                                __( 'Problem', 'bcs-smtp-manager' )     => $this->problem_label( $type ),
     398                                __( 'Error', 'bcs-smtp-manager' )       => $error_msg,
     399                                __( 'Mail server', 'bcs-smtp-manager' ) => $server,
     400                                __( 'Sending as', 'bcs-smtp-manager' )  => $mailbox,
     401                                __( 'Detected', 'bcs-smtp-manager' )    => $when,
     402                        );
     403                }
    259404
    260405                return array(
    261                         'site_name'     => $site_name,
    262                         'site_url'      => $site_url,
    263                         'timestamp'     => $timestamp,
    264                         'mailbox'       => isset( $result['mailbox'] )   ? $result['mailbox']   : $info['mailbox'],
    265                         'smtp_host'     => isset( $result['smtp_host'] ) ? $result['smtp_host'] : $info['host'],
    266                         'smtp_port'     => isset( $result['smtp_port'] ) ? $result['smtp_port'] : $info['port'],
    267                         'plugin'        => $info['plugin'],
    268                         'error_msg'     => isset( $result['error_msg'] ) ? $result['error_msg'] : 'No details available.',
    269                         'failure_type'  => $fail_label,
    270                         'source_ip'     => isset( $result['source_ip'] ) ? $result['source_ip'] : '—',
    271                         'failures'      => $failures,
    272                         'external_down' => $external_down,
    273                         'ext_note'      => $ext_note,
    274                 );
     406                        // Shared message for the chat channels.
     407                        'is_test'        => $is_test,
     408                        'title'          => $title,
     409                        'summary'        => $summary,
     410                        'fields'         => $fields,
     411                        'note'           => $external_down ? __( 'Your email provider’s status page also reports a problem, so this may be an outage on their side.', 'bcs-smtp-manager' ) : '',
     412                        'footer'         => sprintf( '%s · %s', $site_name, preg_replace( '#^https?://#', '', $site_url ) ),
     413                        // Machine-readable values for the custom webhook (keys unchanged since 1.1).
     414                        'site_name'      => $site_name,
     415                        'site_url'       => $site_url,
     416                        'timestamp'      => current_time( 'M j, Y \a\t g:i A' ),
     417                        'mailbox'        => $mailbox,
     418                        'smtp_host'      => $smtp_host,
     419                        'smtp_port'      => $smtp_port,
     420                        'plugin'         => $info['plugin'],
     421                        'error_msg'      => $error_msg,
     422                        'failure_type'   => $this->mailer->label_failure_type( $type ),
     423                        'source_ip'      => isset( $result['source_ip'] ) ? $result['source_ip'] : '',
     424                        'failures'       => $failures,
     425                        'external_down'  => $external_down,
     426                );
     427        }
     428
     429        /**
     430         * What went wrong, in plain words, for the Problem line of an alert.
     431         *
     432         * @param  string $type Failure type from the connection test.
     433         * @return string
     434         */
     435        private function problem_label( $type ) {
     436                $labels = array(
     437                        'auth_failure'     => __( 'Sign-in failed: the username or password wasn’t accepted', 'bcs-smtp-manager' ),
     438                        'smtp_rejection'   => __( 'The mail server rejected the email', 'bcs-smtp-manager' ),
     439                        'plugin_failure'   => __( 'Couldn’t connect to the mail server', 'bcs-smtp-manager' ),
     440                        'mail_unavailable' => __( 'PHP mail() isn’t available on this server', 'bcs-smtp-manager' ),
     441                        'not_configured'   => __( 'SMTP isn’t set up yet', 'bcs-smtp-manager' ),
     442                );
     443                return isset( $labels[ $type ] ) ? $labels[ $type ] : __( 'Email couldn’t be sent', 'bcs-smtp-manager' );
     444        }
     445
     446        /**
     447         * Slack treats <, > and & as control characters in message text.
     448         *
     449         * @param  string $text
     450         * @return string
     451         */
     452        private static function slack_escape( $text ) {
     453                return str_replace( array( '&', '<', '>' ), array( '&amp;', '&lt;', '&gt;' ), (string) $text );
    275454        }
    276455
    … …  
    282461                $webhook = get_option( BCS_SMTP_OPT_SLACK_WEBHOOK_URL, '' );
    283462                if ( empty( $webhook ) || ! self::is_allowed_webhook_host( $webhook, 'slack' ) ) {
    284                         return;
    285                 }
    286 
    287                 $text = sprintf(
    288                         ":rotating_light: *WordPress SMTP Alert — %s*\n" .
    289                         ">*Time:* %s\n" .
    290                         ">*Site:* <%s|%s>\n" .
    291                         ">*Sending mailbox:* `%s`\n" .
    292                         ">*SMTP host:* `%s:%s`\n" .
    293                         ">*Plugin:* %s\n" .
    294                         ">*Error:* %s\n" .
    295                         ">*Failure type:* %s\n" .
    296                         ">*Source IP:* %s\n" .
    297                         ">*Consecutive failures:* %d%s",
    298                         $payload['site_name'],
    299                         $payload['timestamp'],
    300                         $payload['site_url'], $payload['site_name'],
    301                         $payload['mailbox'],
    302                         $payload['smtp_host'], $payload['smtp_port'],
    303                         $payload['plugin'],
    304                         $payload['error_msg'],
    305                         $payload['failure_type'],
    306                         $payload['source_ip'],
    307                         $payload['failures'],
    308                         $payload['ext_note']
    309                 );
    310 
    311                 wp_safe_remote_post( $webhook, array(
    312                         'body'    => wp_json_encode( array( 'text' => $text ) ),
    313                         'headers' => array( 'Content-Type' => 'application/json' ),
    314                         'timeout' => 10,
     463                        return $this->missing_webhook_error( 'Slack' );
     464                }
     465
     466                $icon  = $payload['is_test'] ? '✅' : '🔴';
     467                $lines = array();
     468                foreach ( $payload['fields'] as $label => $value ) {
     469                        $lines[] = '*' . self::slack_escape( $label ) . ':* ' . self::slack_escape( $value );
     470                }
     471
     472                $blocks = array(
     473                        array( 'type' => 'header', 'text' => array( 'type' => 'plain_text', 'text' => $icon . ' ' . $payload['title'], 'emoji' => true ) ),
     474                        array( 'type' => 'section', 'text' => array( 'type' => 'mrkdwn', 'text' => self::slack_escape( $payload['summary'] ) ) ),
     475                        array( 'type' => 'divider' ),
     476                        array( 'type' => 'section', 'text' => array( 'type' => 'mrkdwn', 'text' => implode( "\n", $lines ) ) ),
     477                );
     478                if ( $payload['note'] ) {
     479                        $blocks[] = array( 'type' => 'section', 'text' => array( 'type' => 'mrkdwn', 'text' => '⚠️ ' . self::slack_escape( $payload['note'] ) ) );
     480                }
     481                $blocks[] = array( 'type' => 'context', 'elements' => array( array( 'type' => 'mrkdwn', 'text' => self::slack_escape( $payload['footer'] ) ) ) );
     482
     483                // "text" is the notification preview and the fallback for clients without blocks.
     484                return $this->post_webhook( $webhook, array(
     485                        'text'   => $icon . ' ' . $payload['title'],
     486                        'blocks' => $blocks,
    315487                ) );
    316488        }
    … …  
    323495                $webhook = get_option( BCS_SMTP_OPT_TEAMS_WEBHOOK_URL, '' );
    324496                if ( empty( $webhook ) || ! self::is_allowed_webhook_host( $webhook, 'teams' ) ) {
    325                         return;
    326                 }
    327 
    328                 $card = array(
    329                         '@type'      => 'MessageCard',
    330                         '@context'   => 'http://schema.org/extensions',
    331                         'themeColor' => 'FF0000',
    332                         'summary'    => 'WordPress SMTP Failure Alert',
    333                         'sections'   => array(
     497                        return $this->missing_webhook_error( 'Teams' );
     498                }
     499
     500                // Adaptive Card in a message envelope — accepted by Power Automate "Workflows" webhooks
     501                // (the replacement for the retired Office 365 connectors) and by legacy connector URLs alike.
     502                $facts = array();
     503                foreach ( $payload['fields'] as $title => $value ) {
     504                        $facts[] = array( 'title' => (string) $title, 'value' => (string) $value );
     505                }
     506
     507                $body = array(
     508                        array(
     509                                'type'   => 'TextBlock',
     510                                'text'   => ( $payload['is_test'] ? '✅ ' : '🔴 ' ) . $payload['title'],
     511                                'weight' => 'Bolder',
     512                                'size'   => 'Large',
     513                                'color'  => $payload['is_test'] ? 'Good' : 'Attention',
     514                                'wrap'   => true,
     515                        ),
     516                        array( 'type' => 'TextBlock', 'text' => $payload['summary'], 'wrap' => true, 'spacing' => 'Small' ),
     517                        array( 'type' => 'FactSet', 'facts' => $facts, 'spacing' => 'Medium' ),
     518                );
     519                if ( $payload['note'] ) {
     520                        $body[] = array( 'type' => 'TextBlock', 'text' => '⚠️ ' . $payload['note'], 'wrap' => true, 'color' => 'Warning' );
     521                }
     522                $body[] = array( 'type' => 'TextBlock', 'text' => $payload['footer'], 'isSubtle' => true, 'size' => 'Small', 'wrap' => true );
     523
     524                return $this->post_webhook( $webhook, array(
     525                        'type'        => 'message',
     526                        'attachments' => array(
    334527                                array(
    335                                         'activityTitle'    => sprintf( '🚨 SMTP Failure — %s', $payload['site_name'] ),
    336                                         'activitySubtitle' => $payload['timestamp'],
    337                                         'facts'            => array(
    338                                                 array( 'name' => 'Site',                 'value' => $payload['site_url'] ),
    339                                                 array( 'name' => 'Sending Mailbox',      'value' => $payload['mailbox'] ),
    340                                                 array( 'name' => 'SMTP Host',            'value' => $payload['smtp_host'] . ':' . $payload['smtp_port'] ),
    341                                                 array( 'name' => 'Plugin Used',          'value' => $payload['plugin'] ),
    342                                                 array( 'name' => 'Error',                'value' => $payload['error_msg'] ),
    343                                                 array( 'name' => 'Failure Type',         'value' => $payload['failure_type'] ),
    344                                                 array( 'name' => 'Source IP',            'value' => $payload['source_ip'] ),
    345                                                 array( 'name' => 'Consecutive Failures', 'value' => (string) $payload['failures'] ),
     528                                        'contentType' => 'application/vnd.microsoft.card.adaptive',
     529                                        'contentUrl'  => null,
     530                                        'content'     => array(
     531                                                '$schema' => 'http://adaptivecards.io/schemas/adaptive-card.json',
     532                                                'type'    => 'AdaptiveCard',
     533                                                'version' => '1.4',
     534                                                'msteams' => array( 'width' => 'Full' ),
     535                                                'body'    => $body,
    346536                                        ),
    347                                         'markdown' => true,
    348537                                ),
    349538                        ),
    350                 );
    351 
    352                 if ( $payload['external_down'] ) {
    353                         $card['sections'][] = array(
    354                                 'text' => '⚠️ External SMTP service may be down — check your provider\'s status page before escalating.',
    355                         );
    356                 }
    357 
    358                 wp_safe_remote_post( $webhook, array(
    359                         'body'    => wp_json_encode( $card ),
    360                         'headers' => array( 'Content-Type' => 'application/json' ),
    361                         'timeout' => 10,
    362539                ) );
    363540        }
    … …  
    370547                $webhook = get_option( BCS_SMTP_OPT_GOOGLE_CHAT_WEBHOOK_URL, '' );
    371548                if ( empty( $webhook ) || ! self::is_allowed_webhook_host( $webhook, 'google_chat' ) ) {
    372                         return;
    373                 }
    374 
    375                 $text = sprintf(
    376                         "🚨 *WordPress SMTP Alert — %s*\n" .
    377                         "*Time:* %s\n" .
    378                         "*Site:* <%s|%s>\n" .
    379                         "*Sending mailbox:* `%s`\n" .
    380                         "*SMTP host:* `%s:%s`\n" .
    381                         "*Plugin:* %s\n" .
    382                         "*Error:* %s\n" .
    383                         "*Failure type:* %s\n" .
    384                         "*Source IP:* %s\n" .
    385                         "*Consecutive failures:* %d%s",
    386                         $payload['site_name'],
    387                         $payload['timestamp'],
    388                         $payload['site_url'], $payload['site_name'],
    389                         $payload['mailbox'],
    390                         $payload['smtp_host'], $payload['smtp_port'],
    391                         $payload['plugin'],
    392                         $payload['error_msg'],
    393                         $payload['failure_type'],
    394                         $payload['source_ip'],
    395                         $payload['failures'],
    396                         $payload['ext_note']
    397                 );
    398 
    399                 wp_safe_remote_post( $webhook, array(
    400                         'body'    => wp_json_encode( array( 'text' => $text ) ),
    401                         'headers' => array( 'Content-Type' => 'application/json' ),
    402                         'timeout' => 10,
    403                 ) );
     549                        return $this->missing_webhook_error( 'Google Chat' );
     550                }
     551
     552                $lines = array( ( $payload['is_test'] ? '✅ ' : '🔴 ' ) . '*' . $payload['title'] . '*', $payload['summary'], '' );
     553                foreach ( $payload['fields'] as $label => $value ) {
     554                        $lines[] = '*' . $label . ':* ' . $value;
     555                }
     556                if ( $payload['note'] ) {
     557                        $lines[] = '';
     558                        $lines[] = '⚠️ ' . $payload['note'];
     559                }
     560                $lines[] = '';
     561                $lines[] = $payload['footer'];
     562
     563                return $this->post_webhook( $webhook, array( 'text' => implode( "\n", $lines ) ) );
    404564        }
    405565
    … …  
    411571                $webhook = get_option( BCS_SMTP_OPT_DISCORD_WEBHOOK_URL, '' );
    412572                if ( empty( $webhook ) || ! self::is_allowed_webhook_host( $webhook, 'discord' ) ) {
    413                         return;
    414                 }
    415 
    416                 // Discord messages don't support Slack-style <url|text> link aliasing — the
    417                 // site URL is appended plainly so it still autolinks.
    418                 $content = sprintf(
    419                         "🚨 **WordPress SMTP Alert — %s**\n" .
    420                         "**Time:** %s\n" .
    421                         "**Site:** %s\n" .
    422                         "**Sending mailbox:** `%s`\n" .
    423                         "**SMTP host:** `%s:%s`\n" .
    424                         "**Plugin:** %s\n" .
    425                         "**Error:** %s\n" .
    426                         "**Failure type:** %s\n" .
    427                         "**Source IP:** %s\n" .
    428                         "**Consecutive failures:** %d%s",
    429                         $payload['site_name'],
    430                         $payload['timestamp'],
    431                         $payload['site_url'],
    432                         $payload['mailbox'],
    433                         $payload['smtp_host'], $payload['smtp_port'],
    434                         $payload['plugin'],
    435                         $payload['error_msg'],
    436                         $payload['failure_type'],
    437                         $payload['source_ip'],
    438                         $payload['failures'],
    439                         $payload['ext_note']
    440                 );
    441 
    442                 wp_safe_remote_post( $webhook, array(
    443                         'body'    => wp_json_encode( array( 'content' => $content ) ),
    444                         'headers' => array( 'Content-Type' => 'application/json' ),
    445                         'timeout' => 10,
     573                        return $this->missing_webhook_error( 'Discord' );
     574                }
     575
     576                $fields = array();
     577                foreach ( $payload['fields'] as $label => $value ) {
     578                        $fields[] = array( 'name' => (string) $label, 'value' => (string) $value, 'inline' => false );
     579                }
     580                $description = $payload['summary'] . ( $payload['note'] ? "\n\n⚠️ " . $payload['note'] : '' );
     581
     582                // An embed shows as a card with a coloured edge: red for a failure, green for a test.
     583                return $this->post_webhook( $webhook, array(
     584                        'embeds' => array(
     585                                array(
     586                                        'title'       => ( $payload['is_test'] ? '✅ ' : '🔴 ' ) . $payload['title'],
     587                                        'description' => $description,
     588                                        'color'       => $payload['is_test'] ? 0x2EA44F : 0xE5484D,
     589                                        'fields'      => $fields,
     590                                        'footer'      => array( 'text' => $payload['footer'] ),
     591                                ),
     592                        ),
    446593                ) );
    447594        }
    … …  
    459606                $webhook = get_option( BCS_SMTP_OPT_CUSTOM_WEBHOOK_URL, '' );
    460607                if ( empty( $webhook ) || ! self::is_allowed_webhook_host( $webhook, 'custom' ) ) {
    461                         return;
     608                        return $this->missing_webhook_error( 'custom' );
    462609                }
    463610
    464611                $body = array(
    465612                        'event'                 => 'smtp_failure',
     613                        'test'                  => $payload['is_test'],
    466614                        'site_name'             => $payload['site_name'],
    467615                        'site_url'              => $payload['site_url'],
    … …  
    478626                );
    479627
    480                 wp_safe_remote_post( $webhook, array(
    481                         'body'    => wp_json_encode( $body ),
    482                         'headers' => array( 'Content-Type' => 'application/json' ),
    483                         'timeout' => 10,
    484                 ) );
     628                return $this->post_webhook( $webhook, $body );
    485629        }
    486630}
  • bcs-smtp-manager/tags/1.3.0/includes/class-bcs-smtp-wp-cli.php

    r3652946 r3762759  
    1010
    1111/**
    12  * Manage ByteCoreStack SMTP email sending, resending, and the email log.
     12 * Manage ByteCoreStack SMTP email sending and the email log.
    1313 */
    1414class BCS_SMTP_WP_CLI_Command {
    … …  
    3838                // registered the real mailer's wp_mail/phpmailer_init/wp_mail_failed hooks, which is what
    3939                // actually logs this send. Calling init_hooks() again on a second instance would register
    40                 // a duplicate set of hooks and log two rows per send (see resend() below for the same note).
     40                // a duplicate set of hooks and log two rows per send.
    4141                $mailer = new BCS_SMTP_Mailer();
    4242                $result = $mailer->send_test_email( $to );
    … …  
    5050
    5151        /**
    52          * Resend a previously logged, failed email using its stored content.
    53          *
    54          * Requires "Store failed email content" to have been enabled (Config tab)
    55          * at the time the original email failed — otherwise no content was stored.
     52         * List recent email log entries.
    5653         *
    5754         * ## OPTIONS
    5855         *
    59          * <log_id>
    60          * : The email log entry ID to resend (see `wp bcs-smtp log list`).
    61          *
    62          * ## EXAMPLES
    63          *
    64          *     wp bcs-smtp resend 482
    65          *
    66          * @when after_wp_load
    67          */
    68         public function resend( $args ) {
    69                 $log_id = isset( $args[0] ) ? absint( $args[0] ) : 0;
    70                 if ( ! $log_id ) {
    71                         WP_CLI::error( 'Please provide a log entry ID.' );
    72                         return;
    73                 }
    74 
    75                 // No init_hooks() here either — see the note in test() above. resend_stored_email()
    76                 // only needs $db directly (to read/update the log row); the actual send is logged
    77                 // by the already-registered global mailer instance's hooks, same as any other wp_mail() call.
    78                 $db     = new BCS_SMTP_DB();
    79                 $mailer = new BCS_SMTP_Mailer( $db );
    80                 $result = $mailer->resend_stored_email( $log_id );
    81 
    82                 if ( $result['success'] ) {
    83                         WP_CLI::success( $result['message'] );
    84                 } else {
    85                         WP_CLI::error( $result['message'] );
    86                 }
    87         }
    88 
    89         /**
    90          * List recent email log entries.
    91          *
    92          * ## OPTIONS
     56         * [<action>]
     57         * : What to do with the log.
     58         * ---
     59         * default: list
     60         * options:
     61         *   - list
     62         * ---
    9363         *
    9464         * [--status=<status>]
  • bcs-smtp-manager/tags/1.3.0/readme.txt

    r3652946 r3762759  
    11=== ByteCoreStack - SMTP & Health Monitor ===
    22Contributors: bytecorestack
    3 Tags: smtp, wordpress smtp, email log, smtp alerts, email monitoring
     3Tags: smtp, email log, email, email alerts, email monitoring
    44Requires at least: 5.3
    5 Tested up to: 7.0
     5Tested up to: 7.1
    66Requires PHP: 7.2
    7 Stable tag: 1.2.1
     7Stable tag: 1.3.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1010
    11 The WordPress SMTP plugin that logs every email, monitors delivery health, and alerts you on Slack, Teams, Discord & Google Chat when it fails.
     11Fix WordPress email with SMTP, log every email with its full message, and get Slack, Teams, Discord or Google Chat alerts when sending fails.
    1212
    1313== Description ==
    1414
    15 **Is your WordPress site silently failing to send emails?** Order confirmations, password resets, contact form notifications, shipping updates — when they don't land in the inbox, you're usually the last person to find out, often from an angry customer. ByteCoreStack - SMTP & Health Monitor fixes email deliverability at the source, logs every single send, and alerts your team the instant delivery breaks again — on Slack, Microsoft Teams, Google Chat, or Discord, at no extra cost.
    16 
    17 This plugin doesn't stop the moment your email starts sending again. It keeps going: continuous SMTP health monitoring, a fully searchable email log with CSV export, scheduled delivery reports, and real-time failure alerts, all included.
    18 
    19 = Why your WordPress emails are failing =
    20 
    21 By default, WordPress sends email through your server's PHP `mail()` function. PHP mail has no authentication — no SPF, no DKIM, nothing that proves to Gmail, Outlook, or Yahoo that the message is legitimate. Most inbox providers either bin it as spam or drop it outright. That's why "WordPress not sending email" is one of the most common WordPress problems there is, and why every serious site needs proper SMTP — authenticated, encrypted mail delivery through a real mail server or transactional email provider.
    22 
    23 = How this plugin fixes it =
    24 
    25 The plugin reconfigures `wp_mail()` to send through the authenticated SMTP server you configure — your hosting provider, Gmail, Microsoft 365, or any transactional email service (SendGrid, Mailgun, Amazon SES, Brevo, Zoho, SMTP.com, and others all work over standard SMTP). Every email is logged with its delivery status, and an automated health check keeps testing your connection in the background — so instead of finding out about a broken mail server from an angry customer, you find out from a Slack message.
    26 
    27 = ✅ Core features =
    28 
    29 * **Universal SMTP connection** — host, port, and encryption (SSL / TLS / none) for any standard SMTP provider, with auto-TLS upgrade support
    30 * **Built-in quick-reference for popular providers** — Gmail / Google Workspace, Microsoft 365 / Outlook, Yahoo Mail, SendGrid, Amazon SES, and Mailgun host/port/encryption settings, right inside the settings page
    31 * **Full email log** — date, subject, sender, recipient, delivery status, and the actual error message for every email `wp_mail()` sends
    32 * **AJAX-driven search and filtering** — live search-as-you-type plus date-range presets (today through all time), no page reloads
    33 * **Resend failed emails** — an opt-in setting stores the content of an email only if it fails, so you can resend it straight from the Email Log instead of it being a dead end. Off by default; stored content auto-purges after a configurable number of days
    34 * **CSV export** of the filtered log for record-keeping or support tickets
    35 * **One-click "Send Test Email"** to verify your configuration instantly, with failure-type classification (authentication failure, SMTP rejection, or plugin/system error) so you know exactly what's wrong
    36 * **Scheduled HTML delivery reports** — daily, weekly, or monthly summary emails with a visual chart of sent/failed/unconfirmed volume
    37 * **Automatic log retention** — keep the last 30 days or the last 1,000 records, pruned automatically
    38 * **WordPress Dashboard widget** — SMTP health and 30-day send volume at a glance, without opening the plugin's settings page
    39 * **WP-CLI support** — `wp bcs-smtp test`, `wp bcs-smtp resend <id>`, and `wp bcs-smtp log list` for scripting test sends, resends, and log lookups from the command line or a CI/deploy pipeline
    40 * **Encrypted password storage** — your SMTP password is encrypted at rest, not stored as plaintext
    41 * **Dark-mode admin UI** built for clarity, not just decoration
    42 
    43 = 🔔 Real-time failure alerts =
    44 
    45 Included at no extra cost:
    46 
    47 * **Automated SMTP health checks** via WordPress Cron — a real connection + EHLO + STARTTLS + AUTH test on the interval you choose, with no test email sent on success
    48 * **Five alert channels**: Slack, Microsoft Teams, Google Chat, Discord, and a Custom Webhook option that POSTs a structured JSON payload (`event`, `site_name`, `error_msg`, `consecutive_failures`, and more) for Zapier, Make, n8n, or your own endpoint
    49 * **Configurable failure threshold** — alert after 1 failure or wait for N consecutive failures, your choice
    50 * **Notification cap** so a prolonged outage doesn't spam your channel forever
    51 * **External provider status check** — when an alert fires, the plugin checks whether the failure looks like an outage on your provider's end before notifying you
    52 * All webhook URLs are validated against the provider's real domain and sent over `wp_safe_remote_post()`, so a misconfigured webhook can't be used to reach internal network addresses
    53 
    54 = 🔌 Works with everything that uses wp_mail() =
    55 
    56 WooCommerce, Contact Form 7, WPForms, Gravity Forms, Elementor Forms, user registration/password reset emails, and literally any other plugin or theme that sends mail through WordPress's standard `wp_mail()` function — because that's exactly what this plugin hooks into. Nothing to reconfigure on the form/store plugin side.
    57 
    58 = 🌐 Supported SMTP providers =
    59 
    60 Anything that speaks standard SMTP with host/port/username/password authentication works, because the plugin talks SMTP directly rather than locking you into a specific provider's proprietary API:
    61 
    62 * [Gmail / Google Workspace](https://myaccount.google.com/apppasswords) (App Passwords, or [Workspace SMTP relay](https://support.google.com/a/answer/2956491) for higher volume)
    63 * [Microsoft 365 / Outlook](https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/how-to-set-up-a-multifunction-device-or-application-to-send-email-using-microsoft-365-or-office-365)
     15**Is your WordPress site quietly failing to send email?** Order confirmations, password resets and contact form messages that never arrive usually go unnoticed until a customer complains. ByteCoreStack - SMTP & Health Monitor sends your WordPress email through a real, authenticated SMTP server, keeps a searchable log of every email with its full message, and tells your team on Slack, Microsoft Teams, Google Chat or Discord the moment sending breaks.
     16
     17= Why WordPress emails fail =
     18
     19Out of the box, WordPress sends email with PHP `mail()`. It has no authentication, so Gmail, Outlook and Yahoo often send those messages to spam or reject them, and many hosts disable it entirely. That's why "WordPress not sending email" is one of the most common WordPress problems. The fix is SMTP: sending through a real mail server that signs in, such as Gmail, Microsoft 365, your host's mail server or a transactional email service.
     20
     21= Set up in about a minute =
     22
     231. Pick your provider. The server, port and encryption fill in for you, with sign-in tips for that provider.
     242. Enter your username and password (or app password / API key) and click **Save and test connection**. The plugin signs in to your server right away and tells you whether it worked.
     253. Send a test email to confirm a real message arrives.
     26
     27= Features =
     28
     29**Reliable sending**
     30
     31* Send all WordPress email through any SMTP server: SSL, TLS or no encryption, with automatic TLS upgrade
     32* Ready-made settings for Gmail / Google Workspace, Microsoft 365 / Outlook, SendGrid, Amazon SES, Mailgun, Brevo, Zoho Mail and Yahoo Mail, plus any other SMTP server
     33* Test email with a plain explanation when it fails: sign-in failed, rejected by the server, or couldn't connect, along with the server's own reply
     34* SMTP password encrypted at rest
     35
     36**Email log**
     37
     38* Every email WordPress sends, grouped by day, with its delivery status and the server's reason for any failure
     39* Open any email on its own page: a safe preview of the message, its HTML source, a text version, every header and attachment names, with a phone-width preview to check mobile layout
     40* Search, filter by status (with counts) and date range, and export to CSV
     41* Keeps the last 90 days and removes older entries automatically
     42
     43**Health checks and failure alerts**
     44
     45* Automatic health check every 15 minutes once SMTP is set up: a real sign-in and sender check, with no email sent
     46* Alerts to Slack, Microsoft Teams, Google Chat, Discord and a custom webhook (for Zapier, Make or n8n), as many at once as you like
     47* An alert on the first failed check, and one alert per outage, so a long outage never floods your channel
     48* Checks whether Microsoft 365 reports an outage before alerting, when you send through Microsoft 365
     49
     50**Reports**
     51
     52* Delivery chart and totals for the last 7, 30 or 90 days, with highlights such as your busiest day, failure-free days and how many people your email reached
     53* Monthly summary email on the 1st of every month at 9:00, covering the previous month, sent to whoever you choose
     54
     55**Built for WordPress**
     56
     57* Overview screen showing whether mail is flowing, when the last and next checks run, and what's left to set up
     58* Dashboard widget with SMTP health and 30-day totals
     59* WP-CLI commands: `wp bcs-smtp test` and `wp bcs-smtp log list`
     60* Dark, responsive admin that works on phones and tablets, with keyboard-accessible controls and bundled fonts (no external requests)
     61
     62= Works with any plugin that sends email =
     63
     64WooCommerce, Contact Form 7, WPForms, Gravity Forms, Elementor Forms, Easy Digital Downloads, membership and booking plugins, and WordPress's own password reset and new user emails all send through `wp_mail()`, so they use your SMTP connection automatically. There's nothing to change in those plugins.
     65
     66= Supported SMTP providers =
     67
     68Any provider that offers standard SMTP sign-in works:
     69
     70* [Gmail / Google Workspace](https://myaccount.google.com/apppasswords) (app password, or the [Workspace SMTP relay](https://support.google.com/a/answer/2956491) for higher volume)
     71* [Microsoft 365 / Outlook](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission)
    6472* [SendGrid](https://docs.sendgrid.com/for-developers/sending-email/getting-started-smtp)
     73* [Amazon SES](https://docs.aws.amazon.com/ses/latest/dg/send-email-smtp.html)
    6574* [Mailgun](https://documentation.mailgun.com/docs/mailgun/user-manual/sending-messages/)
    66 * [Amazon SES](https://docs.aws.amazon.com/ses/latest/dg/send-email-smtp.html)
    6775* [Brevo](https://help.brevo.com/hc/en-us/articles/209462765)
    68 * [Zoho Mail](https://www.zoho.com/mail/help/)
    69 * Yahoo Mail, SMTP.com, or your own hosting provider's mail server
    70 
    71 = 🆚 What sets this plugin apart =
    72 
    73 Getting your email sending again is only half the job. If the connection breaks next month — expired app password, host changed something, provider outage — you need to know before a customer tells you. ByteCoreStack - SMTP & Health Monitor treats failure alerting as core functionality, included for every user, because a delivery plugin that can't tell you when delivery breaks isn't finished doing its job.
    74 
    75 = Documentation & support =
    76 
    77 Full plugin details: [bytecorestack.com/plugins/smtp-manager](https://bytecorestack.com/plugins/smtp-manager/). For SMTP protocol background, see the [WordPress wp_mail() developer reference](https://developer.wordpress.org/reference/functions/wp_mail/).
     76* [Zoho Mail](https://www.zoho.com/mail/help/zoho-smtp.html)
     77* [Yahoo Mail](https://help.yahoo.com/kb/SLN15241.html), SMTP.com, or your web host's mail server
     78
     79= Privacy =
     80
     81Everything the plugin stores stays in your own WordPress database: your SMTP settings, and the email log (each email's date, subject, sender, recipients, delivery status, message body, headers and attachment file names). Attached files themselves are never copied. Log entries are deleted automatically after 90 days, only administrators can see them, and uninstalling the plugin removes all of its data. Nothing is sent to ByteCore Stack. See **External services** below for the services the plugin contacts.
     82
     83= Documentation and support =
     84
     85Plugin home page: [bytecorestack.com/plugins/smtp-manager](https://bytecorestack.com/plugins/smtp-manager/). For questions or bug reports, use the support forum on this plugin's WordPress.org page.
     86
     87= Credits =
     88
     89The admin screens use two fonts bundled with the plugin, both under the SIL Open Font License 1.1 (license files included in `assets/fonts`): [Outfit](https://github.com/Outfitio/Outfit-Fonts) and [Geist Mono](https://github.com/vercel/geist-font).
    7890
    7991== Installation ==
    8092
    81 1. Upload the `bcs-smtp-manager` folder to `/wp-content/plugins/`, or install directly from the WordPress Plugins screen.
    82 2. Activate the plugin through the **Plugins** menu in WordPress.
    83 3. Go to **Settings → SMTP Manager** and enter your SMTP host, port, encryption, and credentials (use the built-in provider quick-reference if you're not sure what to enter).
    84 4. Click **Save Settings** — the plugin runs a connection test immediately and tells you if it worked.
    85 5. Use **Send Test Email** to confirm an actual message is delivered end-to-end.
    86 6. Open the **Alerts** tab, enable a channel (Slack, Teams, Google Chat, Discord, or a custom webhook), and turn on automated health monitoring so you're notified the next time something breaks.
     931. In WordPress, go to **Plugins → Add New**, search for "ByteCoreStack SMTP" and click **Install Now**, or upload the `bcs-smtp-manager` folder to `/wp-content/plugins/`.
     942. Activate the plugin.
     953. Go to **Settings → SMTP Manager** and open the **Connection** tab. Pick your provider (Gmail, Microsoft 365, SendGrid, Amazon SES, Mailgun, Brevo, Zoho, Yahoo or another SMTP server).
     964. Enter your username and password (or app password / API key), then click **Save and test connection**.
     975. Click **Send test email** to confirm a real message arrives.
     986. Optional: open the **Alerts** tab and choose where failure alerts go, and turn on the monthly report on the **Reports** tab. Health checks start automatically.
    8799
    88100== Frequently Asked Questions ==
    89101
    90 = What is SMTP and why do I need it for WordPress? =
    91 SMTP (Simple Mail Transfer Protocol) is the standard, authenticated way to send email. WordPress's default `mail()` function has no authentication, which is why its messages are frequently marked as spam or rejected outright by Gmail, Outlook, and Yahoo. Routing through a real SMTP connection — your host, Gmail, Microsoft 365, or a transactional provider — fixes that.
    92 
    93 = Why isn't my WordPress site sending emails right now? =
    94 The most common causes are: your host has disabled the PHP `mail()` function, your SMTP credentials are wrong or expired (Gmail/Microsoft App Passwords expire or get revoked), the wrong port/encryption combination is selected, or your host blocks outbound SMTP ports entirely. Use **Send Test Email** in this plugin — it classifies the failure (authentication, rejection, or system error) so you know which of these it is instead of guessing.
    95 
    96 = Does this work with Gmail? =
    97 Yes. You must use a Gmail App Password — Google no longer allows regular account passwords for third-party SMTP clients. Enable 2-Step Verification on your Google account, then generate an App Password under [Security → App Passwords](https://myaccount.google.com/apppasswords).
    98 
    99 = Does this work with Outlook / Microsoft 365? =
    100 Yes. Use your Microsoft 365 account password or an app password. Set the host to `smtp.office365.com`, port `587`, encryption `TLS`. See Microsoft's [official SMTP relay setup guide](https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/how-to-set-up-a-multifunction-device-or-application-to-send-email-using-microsoft-365-or-office-365) if you need app-specific credentials or hit a sending limit.
    101 
    102 = Does this work with Google Workspace (not just regular Gmail)? =
    103 Yes. Google Workspace also supports a [dedicated SMTP relay service](https://support.google.com/a/answer/2956491) for higher sending limits than a personal Gmail App Password allows — useful if you're sending a high volume of transactional email.
    104 
    105 = Does this work with SendGrid, Mailgun, Amazon SES, Brevo, or Zoho? =
    106 Yes — any provider that offers SMTP relay credentials works, since this plugin speaks standard SMTP rather than a provider-specific API. Grab your SMTP host/port/username/password from each provider's own setup docs:
    107 * [SendGrid SMTP setup](https://docs.sendgrid.com/for-developers/sending-email/getting-started-smtp)
    108 * [Mailgun SMTP setup](https://documentation.mailgun.com/docs/mailgun/user-manual/sending-messages/)
    109 * [Amazon SES SMTP setup](https://docs.aws.amazon.com/ses/latest/dg/send-email-smtp.html)
    110 * [Brevo SMTP relay](https://help.brevo.com/hc/en-us/articles/209462765)
    111 * [Zoho Mail help center](https://www.zoho.com/mail/help/)
    112 
    113 = Does this work with WooCommerce? =
    114 Yes. WooCommerce order emails, and any other plugin that sends email through `wp_mail()`, automatically use the SMTP settings configured here — no separate configuration needed on the WooCommerce side.
    115 
    116 = Does this work with Contact Form 7, WPForms, Gravity Forms, or Elementor Forms? =
    117 Yes. Any plugin that uses the standard WordPress `wp_mail()` function routes through your configured SMTP settings automatically.
    118 
    119 = Can I use this for bulk email marketing or newsletters? =
    120 No. This plugin is built for transactional email — order confirmations, password resets, contact form notifications, account emails. For bulk marketing sends, use a dedicated service such as Mailchimp, Klaviyo, or Brevo's marketing product.
    121 
    122 = Can I switch from another SMTP plugin to this one? =
    123 Yes, and there's nothing destructive about it. Deactivate your current SMTP plugin (or leave it active and just enable this plugin's "Enable SMTP" toggle — whichever hooks `phpmailer_init` last wins), enter your SMTP credentials here, and send a test email to confirm. Nothing about switching SMTP plugins affects your site's actual email content or past history.
    124 
    125 = How does the free real-time alerting work? =
    126 Enable monitoring in the Alerts tab and pick a channel — Slack, Microsoft Teams, Google Chat, Discord, or a Custom Webhook. WordPress Cron then opens a real connection to your SMTP server at the interval you set (EHLO, STARTTLS, and AUTH are all tested, not just a TCP port check) with no email actually sent on a successful check. If it fails for as many consecutive checks as your configured threshold, an alert goes out immediately.
    127 
    128 = How do I set up Slack alerts? =
    129 In the Alerts tab, enable the Alert Channel toggle, select Slack, paste your Incoming Webhook URL, and save. Create the webhook at [api.slack.com/messaging/webhooks](https://api.slack.com/messaging/webhooks).
    130 
    131 = How do I set up Microsoft Teams alerts? =
    132 In the Alerts tab, enable the Alert Channel toggle, select Microsoft Teams, paste your Incoming Webhook URL, and save. Follow Microsoft's [official guide to adding an Incoming Webhook to a Teams channel](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook) to create the URL.
    133 
    134 = How do I set up Google Chat alerts? =
    135 In the Alerts tab, enable the Alert Channel toggle, select Google Chat, paste your Incoming Webhook URL, and save. Follow Google's [official Chat webhooks quickstart](https://developers.google.com/workspace/chat/quickstart/webhooks) to create the URL from your Chat space's Apps & Integrations menu.
    136 
    137 = How do I set up Discord alerts? =
    138 In the Alerts tab, enable the Alert Channel toggle, select Discord, paste your Incoming Webhook URL, and save. Follow Discord's [official "Intro to Webhooks" guide](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) to create the URL from your channel's Server Settings → Integrations → Webhooks.
    139 
    140 = How do I set up a custom webhook for Zapier, Make, or n8n? =
    141 In the Alerts tab, enable the Alert Channel toggle, select Custom Webhook, and paste the URL of your endpoint. Unlike the chat-app channels, this sends a structured JSON payload — `event`, `site_name`, `error_msg`, `consecutive_failures`, and more — built for automation tools to parse rather than for a human to read directly. Docs for the most common automation tools:
    142 * [Webhooks by Zapier](https://zapier.com/page/webhooks/) — trigger a Zap from this plugin's alert payload
    143 * [Make (Integromat) webhooks](https://www.make.com/en/help/tools/webhooks) — use a "Custom webhook" trigger module
    144 * [n8n Webhook node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/) — for self-hosted automation
    145 
    146 = Does this support multisite networks? =
    147 The plugin activates and works on individual sites within a multisite network, but there's currently no centralized network-wide settings screen — each site is configured independently.
    148 
    149 = How does log pruning work? =
    150 Log entries are pruned automatically once per day via WordPress Cron. Choose to keep either the last 30 days or the last 1,000 records — older entries beyond your choice are removed automatically so the table doesn't grow unbounded.
    151 
    152 = Can I resend a failed email? =
    153 Yes, if you've enabled "Store failed email content" in the Config tab first. This is off by default — email bodies are never stored otherwise, matching the plugin's normal metadata-only logging. When enabled, the content of an email is stored *only if it fails to send*, so a "Resend" button appears on that row in the Email Log. Resending creates a fresh log entry with its own delivery status; stored content is auto-purged after the number of days you configure (default 7), independent of your overall log retention setting.
    154 
    155 = Can I send test emails, resend failed emails, or check the log from the command line? =
    156 Yes. The plugin includes WP-CLI support: `wp bcs-smtp test you@example.com` sends a test email, `wp bcs-smtp resend <id>` resends a failed email by its log ID (same requirements as the admin Resend button — "Store failed email content" must have been enabled when it failed), and `wp bcs-smtp log list` lists recent log entries with `--status`, `--days`, and `--format` options. Handy for CI/deploy pipelines or server automation.
     102= Why is my WordPress site not sending emails? =
     103
     104The usual causes: your host has disabled PHP `mail()`, your SMTP password is wrong or has expired (Gmail and Microsoft app passwords can be revoked), the port and encryption don't match, or your host blocks outgoing SMTP ports. Send a test email from this plugin. It tells you which of these it is and shows the mail server's own reply.
     105
     106= What is SMTP and why do I need it? =
     107
     108SMTP is the standard, authenticated way to send email. WordPress's default PHP `mail()` doesn't sign in to anything, so its messages are often marked as spam or rejected. Sending through a real SMTP server (your host, Gmail, Microsoft 365 or a transactional email service) fixes that.
     109
     110= Does it work with Gmail? =
     111
     112Yes. Use a Gmail app password: Google no longer accepts your normal account password for SMTP. Turn on 2-Step Verification, then create an app password under [Security → App passwords](https://myaccount.google.com/apppasswords). Google Workspace accounts can also use the [SMTP relay service](https://support.google.com/a/answer/2956491) for higher sending limits.
     113
     114= Does it work with Microsoft 365 / Outlook? =
     115
     116Yes. Pick Microsoft 365 / Outlook and the server (`smtp.office365.com`, port 587, TLS) fills in. SMTP sign-in (SMTP AUTH) must be allowed for the mailbox in your Microsoft 365 admin center; see Microsoft's [authenticated SMTP guide](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission).
     117
     118= Does it work with SendGrid, Mailgun, Amazon SES, Brevo or Zoho? =
     119
     120Yes. Any service that gives you SMTP credentials works. For Amazon SES, copy the SMTP endpoint for your region from the SES console, and use your SES SMTP credentials rather than your AWS access keys.
     121
     122= Does it work with WooCommerce, Contact Form 7, WPForms or Gravity Forms? =
     123
     124Yes. Any plugin that sends email through WordPress's `wp_mail()` function uses your SMTP connection automatically, with nothing to configure on their side.
     125
     126= Can I see the full content of an email that was sent? =
     127
     128Yes. Click **View** on any row of the Email log to open the email: a safe preview of the message, its HTML source, a plain-text version, every header and the names of any attachments. Failed emails also show the mail server's complete reply.
     129
     130= What does the email log store, and for how long? =
     131
     132Each entry keeps the date, subject, sender, recipients, delivery status, any error, the message body, headers and attachment file names (never the files themselves). Entries are deleted automatically after 90 days. Saved emails can include sensitive details such as password reset links or form submissions, so only administrators can view the log.
     133
     134= How do failure alerts work? =
     135
     136Health checks run automatically every 15 minutes once SMTP is set up. Each check signs in to your mail server and checks your sender address, without sending an email. If a check fails, one alert goes to every channel you picked on the Alerts tab. You won't get another until mail has recovered and broken again.
     137
     138= How do I set up Slack, Microsoft Teams, Google Chat or Discord alerts? =
     139
     140On the Alerts tab, turn on failure alerts, tick the channel and paste its incoming webhook URL. Each channel has a setup guide link next to its field:
     141* [Slack incoming webhooks](https://api.slack.com/messaging/webhooks)
     142* [Microsoft Teams incoming webhooks](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook)
     143* [Google Chat webhooks](https://developers.google.com/workspace/chat/quickstart/webhooks)
     144* [Discord webhooks](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks)
     145
     146= Can I send alerts to Zapier, Make or n8n? =
     147
     148Yes. Use the custom webhook channel. It sends a JSON payload (`event`, `site_name`, `error_msg`, `consecutive_failures` and more) that automation tools can read: [Zapier webhooks](https://zapier.com/page/webhooks/), [Make webhooks](https://www.make.com/en/help/tools/webhooks), [n8n Webhook node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/).
     149
     150= When is the monthly report sent? =
     151
     152On the 1st of every month at 9:00 (your site's time zone), covering the whole previous month. "Send a report now" sends the current month so far.
     153
     154= Can I use it for newsletters or bulk email? =
     155
     156No. It's built for transactional email such as orders, password resets, form notifications and account emails. Use a dedicated email marketing service for newsletters.
     157
     158= Can I switch from another SMTP plugin? =
     159
     160Yes. Set up your connection here and send a test email, then deactivate the other SMTP plugin. Switching doesn't affect your emails or other plugins.
    157161
    158162= Is my SMTP password stored securely? =
    159 Yes. The password is encrypted at rest using a key derived from your WordPress installation's own salts before it's saved, not stored as plaintext.
    160 
    161 = Where are my SMTP credentials stored, and are they sent anywhere? =
    162 Credentials are stored in the WordPress options table (`wp_options`) on your own server using the standard `update_option()` API. They are never transmitted to ByteCore Stack or any third party — the only network connection your credentials are used for is the SMTP server you configured them for.
    163 
    164 = What external services does this plugin use? =
    165 See the "External Services" section below for a full breakdown — your SMTP server, and optionally whichever alert channel(s) you choose to enable.
    166 
    167 = I found a bug or have a feature request — where do I report it? =
    168 Use the Support forum on this plugin's WordPress.org page, or reach out via [bytecorestack.com](https://bytecorestack.com/).
    169 
    170 == External Services ==
    171 
    172 This plugin may contact the following external services. All connections are either user-initiated or require explicit configuration by the site administrator.
    173 
    174 **1. Your SMTP server (user-configured)**
    175 The plugin routes all WordPress emails through the SMTP server credentials you enter in Settings → SMTP Manager. The hostname, port, username, and password are provided entirely by you. No data is sent to any ByteCore Stack server.
    176 
    177 **2. Slack Incoming Webhooks (optional)**
    178 If you enable Slack alerts, the plugin will POST a JSON failure notification to the Slack Incoming Webhook URL you provide when consecutive SMTP failures are detected. This URL is specific to your Slack workspace and is created by you.
     163
     164Yes. It's encrypted before it's saved, using a random key the plugin creates for your site (or a `BCS_SMTP_CRYPTO_KEY` constant you can define in `wp-config.php`), and it's never sent anywhere except your own SMTP server.
     165
     166= Does it work on multisite? =
     167
     168Yes, on each site of a network. Each site is set up separately; there's no network-wide settings screen yet.
     169
     170= Can I use it from the command line? =
     171
     172Yes. `wp bcs-smtp test you@example.com` sends a test email, and `wp bcs-smtp log list` lists recent log entries, with `--status`, `--days` and `--format` options.
     173
     174== External services ==
     175
     176The plugin contacts the services below. None of them receive data from ByteCore Stack, and nothing is sent to ByteCore Stack.
     177
     178**1. Your SMTP server (set up by you)**
     179All email WordPress sends goes through the SMTP server you enter on the Connection tab, using the credentials you provide. Health checks, run every 15 minutes once SMTP is set up, sign in to the same server without sending an email.
     180
     181**2. Slack (optional)**
     182If you turn on Slack alerts, the plugin sends a failure notification (site name and address, time, the problem and the mail server's error, mail server and sending address) to the Slack incoming webhook URL you provide, only when a health check fails or you click "Send test alert".
    179183* Service: Slack Technologies, LLC
     184* Terms of service: https://slack.com/terms-of-service
    180185* Privacy policy: https://slack.com/privacy-policy
    181 * Terms of service: https://slack.com/terms-of-service
    182 
    183 **3. Microsoft Teams Incoming Webhooks (optional)**
    184 If you enable Teams alerts, the plugin will POST a JSON failure notification to the Teams Incoming Webhook URL you provide when consecutive SMTP failures are detected. This URL is specific to your Teams channel and is created by you.
     186
     187**3. Microsoft Teams (optional)**
     188If you turn on Teams alerts, the same notification is sent to the Teams webhook URL you provide, only when a health check fails or you click "Send test alert".
    185189* Service: Microsoft Corporation
     190* Terms of service: https://www.microsoft.com/en-us/servicesagreement
    186191* Privacy policy: https://privacy.microsoft.com/en-us/privacystatement
     192
     193**4. Google Chat (optional)**
     194If you turn on Google Chat alerts, the same notification is sent to the Google Chat webhook URL you provide, only when a health check fails or you click "Send test alert".
     195* Service: Google LLC
     196* Terms of service: https://policies.google.com/terms
     197* Privacy policy: https://policies.google.com/privacy
     198
     199**5. Discord (optional)**
     200If you turn on Discord alerts, the same notification is sent to the Discord webhook URL you provide, only when a health check fails or you click "Send test alert".
     201* Service: Discord Inc.
     202* Terms of service: https://discord.com/terms
     203* Privacy policy: https://discord.com/privacy
     204
     205**6. Custom webhook (optional)**
     206If you turn on the custom webhook, the notification is sent as JSON to the URL you provide (the same details, plus your server's IP address and the number of failed checks), only when a health check fails or you click "Send test alert". The URL is entirely your choice, such as Zapier, Make, n8n or your own server.
     207
     208**7. ipify (automatic, only on some servers)**
     209The server's outgoing IP address is included in diagnostics and alerts. If your server doesn't report its own address (common on local, Docker or some proxied setups), the plugin asks the ipify public API for it, at most once a day. The request carries no site or email data; the reply is just an IP address.
     210* Service: ipify
     211* Website and terms: https://www.ipify.org/
     212
     213**8. Microsoft 365 service status (automatic, only for Microsoft 365)**
     214When a health check fails and your SMTP server is Microsoft 365 / Outlook, the plugin reads Microsoft's public service status page to tell you whether Microsoft is reporting an outage. No site or email data is sent.
     215* Service: Microsoft Corporation
    187216* Terms of service: https://www.microsoft.com/en-us/servicesagreement
    188 
    189 **4. Google Chat Incoming Webhooks (optional)**
    190 If you enable Google Chat alerts, the plugin will POST a JSON failure notification to the Google Chat Incoming Webhook URL you provide when consecutive SMTP failures are detected. This URL is specific to your Chat space and is created by you.
    191 * Service: Google LLC
    192 * Privacy policy: https://policies.google.com/privacy
    193 * Terms of service: https://policies.google.com/terms
    194 
    195 **5. Discord Incoming Webhooks (optional)**
    196 If you enable Discord alerts, the plugin will POST a JSON failure notification to the Discord Incoming Webhook URL you provide when consecutive SMTP failures are detected. This URL is specific to your Discord channel and is created by you.
    197 * Service: Discord Inc.
    198 * Privacy policy: https://discord.com/privacy
    199 * Terms of service: https://discord.com/terms
    200 
    201 **6. Custom Webhook (optional)**
    202 If you enable the Custom Webhook channel, the plugin will POST a structured JSON failure notification to the URL you provide when consecutive SMTP failures are detected. This URL is entirely user-supplied — point it at Zapier, Make, n8n, or your own server. No data is sent unless you configure this channel.
    203 
    204 **7. ipify.org (automatic, no configuration)**
    205 When determining the server's outbound IP address for connection diagnostics (used only if the server's own `SERVER_ADDR` is unavailable, e.g. in some local/Docker/proxy environments), the plugin makes a GET request to the ipify.org public IP-lookup API. No data is sent beyond the request itself; the response is just an IP address string.
    206 * Service: ipify (Randomize.io)
    207 * Privacy policy: https://www.ipify.org/
    208 
    209 **8. Microsoft 365 Service Status API (automatic, only when your configured SMTP host is Microsoft 365/Outlook)**
    210 When the automated SMTP health check (see "Real-time failure alerts") detects a connection failure and your configured SMTP host is `office365` or `outlook`, the plugin queries Microsoft's public service status API to check whether the failure is a known Microsoft-side outage before sending you an alert. No site or email data is sent — only an outbound GET request is made.
    211 * Service: Microsoft Corporation
    212217* Privacy policy: https://privacy.microsoft.com/en-us/privacystatement
    213218
    214 No data is collected, transmitted to, or processed by ByteCore Stack servers.
    215 
    216219== Screenshots ==
    217220
    218 1. Dashboard overview — SMTP authentication status and recent email log activity at a glance
    219 2. SMTP server connection panel — host, port, encryption, and credentials in one place
    220 3. Email log — full delivery history with search, status filters, and automatic log retention
    221 4. Email report — visual delivery chart with scheduled report configuration
    222 5. Send Test Email — verify your SMTP configuration and delivery instantly
    223 6. Alerts tab — enabling automated SMTP health monitoring
    224 7. Alert channel setup — Slack, Microsoft Teams, Google Chat, Discord, or a custom webhook
    225 8. SMTP configuration — encryption, authentication, and provider-specific settings
    226 9. A live failure alert delivered to a chat channel — real-time SMTP notification in action
    227 10. Email Log — a Resend button on a failed row
     2211. Overview — whether mail is flowing, last and next health check, email activity chart and what's left to set up
     2222. Connection — pick your provider and the server details fill in; send a test email right alongside
     2233. Alerts — automatic health checks and failure alerts to Slack, Teams, Google Chat, Discord or a webhook
     2244. Reports — delivery chart, totals, highlights and the monthly report email
     2255. Email log — grouped by day, with status filters, counts and search; open any email to see the full message
     2266. Email details — the full message as it was sent, with delivery status, recipients and headers
    228227
    229228== Changelog ==
     229
     230= 1.3.0 =
     231**Redesigned admin.** A new Overview shows whether mail is flowing, which server and sender address are in use, when the last and next health checks run, where alerts go, a 7, 30 or 90-day activity chart, quick actions and what's left to set up. Tabs are now Overview, Connection, Alerts, Reports and Email log, and every screen works on phones and tablets.
     232
     233**One-minute setup.** Pick your provider (Gmail / Google Workspace, Microsoft 365, SendGrid, Amazon SES, Mailgun, Brevo, Zoho, Yahoo or any other SMTP server) and the server, port and encryption fill in, with sign-in tips for that provider. The password field is labelled for that provider (for example "App password" for Gmail). Testing sits right next to the connection form, and you're warned before leaving with unsaved changes.
     234
     235**View the full email.** Every email is now saved with its body, headers and attachment names for 90 days. Click **View** on any row of the Email log to open it on its own page: a safe preview, the HTML source, a text version, a phone-width preview, every header and the mail server's reply. Emails saved by 1.2.1's "store failed emails" option are kept and can be viewed too.
     236
     237**Email log.** Grouped by day (Today, Yesterday, then by date), with status filters that show counts, search, date ranges, 10 to 100 rows per page and CSV export. Rows stay short and easy to scan; the server's full reason for a failure is on the email's own page. The log keeps the last 90 days. If you're updating from a version that kept everything, older entries stay for 30 days after the update, and the Email log offers an export of all entries before they go.
     238
     239**Clearer alerts.** Every alert now opens with a plain title ("Email is failing on your site"), and says what's wrong in plain words. Slack, Teams and Discord get properly formatted cards, and test alerts say clearly that they're a test.
     240
     241**Alerts to several channels at once.** Pick any mix of Slack, Microsoft Teams, Google Chat, Discord and a custom webhook. Each one gets the alert, and "Send test alert" reports how each did. Webhook URLs are checked as you type.
     242
     243**Always-on health checks.** Checks run every 15 minutes once SMTP is set up, alert on the first failure and send one alert per outage. The interval, threshold and alert-limit settings are gone. The health check now also checks your sender address, so a server that accepts the connection but rejects your email is reported as failing.
     244
     245**Monthly report.** The report email now goes out on the 1st of every month at 9:00 (site time) and covers the whole previous month, instead of 24 hours after saving and drifting from there. Your daily or weekly report is switched to monthly when you update. "Send a report now" sends the current month so far. Recipients are entered as tags, the next send date is shown, and turning "Email me this report" off hides the report options.
     246
     247**Redesigned emails.** The test email and the monthly report have a cleaner layout that fits phones and supports dark mode. The report leads with a clear verdict and the delivery rate, then shows sent, failed and unconfirmed totals and a day-by-day chart for every day of the month, with a button to open the full report. The test email confirms which server and sender it went through and when.
     248
     249**Reports.** Delivery chart for 7, 30 or 90 days with totals and highlights: average per day, busiest day, failure-free days, longest failure-free run and how many people your email reached (not counting your own team). Charts are drawn by the plugin, so the bundled Chart.js library (about 200 KB) is no longer loaded.
     250
     251**Fixes** (including fixes prepared for 1.2.2, which wasn't released separately):
     252* Fixed "Could not authenticate" errors when a security plugin rotates the WordPress salts (All-In-One Security, Solid Security, Sucuri) or the site moves: the SMTP password now uses its own encryption key, or a `BCS_SMTP_CRYPTO_KEY` constant in `wp-config.php`. Existing passwords are moved over automatically.
     253* Fixed the health check reporting "healthy" when the password was empty, unreadable or not accepted.
     254* Fixed a "translation loading was triggered too early" notice on WordPress 6.7 and later.
     255* Errors now include the mail server's actual reply, such as Microsoft 365's `535 5.7.139` codes, and a blocked SMTP port times out after 20 seconds instead of hanging.
     256* Microsoft Teams alerts are sent as Adaptive Cards and accept Teams Workflows (Power Automate) webhook URLs.
     257* The "last check" and "next check" times always add up to the 15-minute interval.
     258* Amazon SES no longer pre-fills a region: paste the SMTP endpoint for your own region.
     259* CSV export now protects against spreadsheet formula injection: a subject or address starting with =, +, - or @ (which can come from a contact form) is exported as plain text.
     260* Uninstalling on a multisite network now removes the plugin's data from every site, not just the main one.
     261* A password saved by 1.0.0, which was stored as plain text, is encrypted when you update.
     262
     263**Removed.** The Settings tab, the opt-in "resend failed emails" feature and `wp bcs-smtp resend`.
     264
     265**Under the hood.** Bundled Outfit and Geist Mono fonts (no external requests), the outgoing IP lookup is cached for a day, the email log loads only the columns it shows, uninstalling also removes the plugin's temporary data, and the readme now documents every external service.
    230266
    231267= 1.2.1 =
    … …  
    234270**Renamed** from "SMTP Manager – Email Logs, Monitoring & Alerts" to "ByteCoreStack - SMTP & Health Monitor" for a clearer, more distinctive plugin name. Display name only — the plugin slug (`bcs-smtp-manager`) and all your settings are unchanged.
    235271
    236 **Resend failed emails** — a new opt-in "Store failed email content" setting (Config tab, off by default) retains the body of an email only when it fails to send, so a "Resend" button now appears on failed rows in the Email Log. Resending creates a fresh log entry with its own delivery status; stored content auto-purges after a configurable number of days (default 7), independently of your overall log retention setting. Successful and unconfirmed sends are never affected — this plugin's metadata-only logging is unchanged unless you explicitly enable this setting.
     272**Resend failed emails** — a new opt-in "Store failed email content" setting (Config tab, off by default) retains the body of an email only when it fails to send, so a "Resend" button now appears on failed rows in the Email Log. Resending creates a fresh log entry with its own delivery status; stored content auto-purges after a configurable number of days (default 7), independently of your overall log retention setting.
    237273
    238274**WP-CLI support** — `wp bcs-smtp test <email>`, `wp bcs-smtp resend <log_id>`, and `wp bcs-smtp log list` for scripted test sends, resends, and log lookups from the command line or a CI/deploy pipeline.
    239275
    240 **Directory compliance** — rewrote parts of the description and admin UI to remove comparative claims about other SMTP plugins, and confirmed the plugin's use of external services (SMTP server, alert webhooks, ipify.org, Microsoft 365 status API) is fully documented in the External Services section below, per WordPress.org directory guidelines.
    241 
    242 **Self-hosted admin fonts** — the admin UI no longer loads DM Sans/DM Mono from the Google Fonts CDN (`@import` in `admin.css`); it now uses the system font stack instead. Fixes a Plugin Check "offloaded content" error and removes an external request that was previously made on every visit to the settings page.
     276**Directory compliance** — rewrote parts of the description and admin UI to remove comparative claims about other SMTP plugins, and documented the plugin's use of external services (SMTP server, alert webhooks, ipify.org, Microsoft 365 status API) in the External Services section, per WordPress.org directory guidelines.
     277
     278**Self-hosted admin fonts** — the admin UI no longer loads DM Sans/DM Mono from the Google Fonts CDN; it uses the system font stack instead. Fixes a Plugin Check "offloaded content" error and removes an external request made on every visit to the settings page.
    243279
    244280**Readme fix** — `Tested up to` now uses major.minor format (`7.0`) instead of including a patch version, per the WordPress.org readme spec.
    245281
    246 = 1.1.0 =
    247 **Renamed** from "SMTP Manager by ByteCore Stack" to "SMTP Manager – Email Logs, Monitoring & Alerts" — display name only, the plugin slug and all your settings are unchanged.
    248 
    249 **New alert channels** — Google Chat and Discord join Slack and Microsoft Teams, plus a Custom Webhook channel for Zapier/Make/n8n/your own endpoint. The Custom Webhook channel POSTs a structured JSON payload (`event`, `site_name`, `error_msg`, `consecutive_failures`, etc.) instead of a formatted chat message. All webhook URLs are validated against the provider's real domain and sent via `wp_safe_remote_post()`.
    250 
    251 **New WordPress Dashboard widget** — SMTP health status and a 30-day sent/failed/unconfirmed volume summary, visible without opening the plugin's settings page.
    252 
    253 **Admin UI redesign** — full-width layout, boxed/gradient header, cards split into header/body sections, icon tiles on stat boxes, a "Common SMTP Providers" quick-reference card, full mobile/tablet responsive pass (dropdowns, pagination, button widths, tab bar scrolling), a show/hide eye icon on the password field, and AJAX-driven Email Log filtering with a skeleton loading state.
    254 
    255 **Security**
    256 * The SMTP password is now encrypted at rest instead of stored in plaintext (existing installs migrate automatically on next save).
    257 * All alert webhook URLs are validated against the provider's real domain and sent via `wp_safe_remote_post()`, closing an SSRF vector where a malicious/misconfigured webhook URL could make the server POST to internal addresses.
    258 * Stopped storing full email message bodies and headers in the Email Log table — only delivery metadata is retained, matching what the UI has always claimed.
    259 * Added a 30-second rate limit to "Run Health Check Now" to match the other test/send actions.
    260 
    261 **Reliability**
    262 * Fixed a bug where the automated SMTP health check could report "healthy" after a bare EHLO even when the server's STARTTLS upgrade failed, TLS was selected but STARTTLS wasn't offered, or SMTP auth was never actually verified over an unencrypted connection.
    263 * Scheduled/manual delivery reports now report exactly which recipients succeeded and which failed, instead of one pass/fail flag for the whole batch.
    264 
    265 **Performance**: composite database index on (status, sent_at) for the common Email Log filter combination; CSV export capped at 50,000 rows.
    266 
    267 **Compatibility**: bumped minimum WordPress version to 5.3 (`wp_timezone_string()`/`wp_date()` require it — the plugin previously declared 5.0 but used functions that didn't exist yet).
    268 
    269 **Accessibility & i18n**: AJAX status messages use `role="status" aria-live="polite"`; nearly every user-facing string is now wrapped for translation (previously ~2 out of hundreds, despite declaring a text domain).
    270 
    271 = 1.0.0 =
    272 * Initial release.
     282Older versions are listed in changelog.txt, included with the plugin.
    273283
    274284== Upgrade Notice ==
     285
     286= 1.3.0 =
     287Redesigned admin, full email viewer, multi-channel alerts and automatic health checks. Your settings and email log are kept. The log now keeps 90 days: older entries stay for 30 days after updating so you can export them.
    275288
    276289= 1.2.1 =
    277290First update since 1.1.0. Renamed to "ByteCoreStack - SMTP & Health Monitor" (slug and settings unchanged), adds opt-in "Resend failed emails" and WP-CLI support (`wp bcs-smtp test|resend|log`). No settings change automatically.
    278 
    279 = 1.1.0 =
    280 Major update: full admin UI redesign, renamed to "SMTP Manager – Email Logs, Monitoring & Alerts" (slug unchanged), 3 new alert channels (Google Chat, Discord, Custom Webhook), a Dashboard widget, password encryption, and SSRF-hardened webhooks. Settings migrate automatically.
    281 
    282 = 1.0.0 =
    283 Initial release.
  • bcs-smtp-manager/tags/1.3.0/uninstall.php

    r3652946 r3762759  
    11<?php
    22/**
    3  * Fired when the plugin is uninstalled — removes all data.
     3 * Fired when the plugin is uninstalled — removes all data, on every site of a network.
    44 *
    55 * @package BCS_SMTP_Manager
    … …  
    1010}
    1111
    12 // Unschedule cron
    13 $bcs_smtp_timestamp = wp_next_scheduled( 'bcs_smtp_health_check' );
    14 if ( $bcs_smtp_timestamp ) {
    15         wp_unschedule_event( $bcs_smtp_timestamp, 'bcs_smtp_health_check' );
     12/**
     13 * Remove the plugin's scheduled events, tables, options and temporary values from the current site.
     14 */
     15function bcs_smtp_uninstall_site() {
     16        global $wpdb;
     17
     18        // Unschedule cron
     19        foreach ( array( 'bcs_smtp_health_check', 'bcs_smtp_prune_log', 'bcs_smtp_send_report' ) as $bcs_smtp_hook ) {
     20                wp_clear_scheduled_hook( $bcs_smtp_hook );
     21        }
     22
     23        // Drop custom tables
     24        // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,PluginCheck.Security.DirectDB.UnescapedDBParameter -- uninstall routine; table names are hardcoded plugin prefixes, not user input.
     25        $bcs_smtp_tables = array(
     26                $wpdb->prefix . 'bcs_smtp_email_log',
     27                $wpdb->prefix . 'bch_smtp_email_log',
     28                $wpdb->prefix . 'bptsm_incidents',
     29        );
     30        foreach ( $bcs_smtp_tables as $bcs_smtp_table ) {
     31                $wpdb->query( "DROP TABLE IF EXISTS {$bcs_smtp_table}" );
     32        }
     33        // phpcs:enable
     34
     35        // Delete all options
     36        $bcs_smtp_options = array(
     37                'bcs_smtp_enabled',
     38                'bcs_from_email',
     39                'bcs_from_name',
     40                'bcs_smtp_host',
     41                'bcs_smtp_port',
     42                'bcs_smtp_encryption',
     43                'bcs_auto_tls',
     44                'bcs_smtp_auth',
     45                'bcs_smtp_username',
     46                'bcs_smtp_custom_server',
     47                'bcs_smtp_password',
     48                'bcs_smtp_crypto_key',
     49                'bcs_smtp_snapshot',
     50                'bcs_monitor_enabled',
     51                'bcs_check_interval',
     52                'bcs_failure_threshold',
     53                'bcs_consecutive_failures',
     54                'bcs_last_check',
     55                'bcs_last_check_success',
     56                'bcs_alert_channel_enabled',
     57                'bcs_alert_channel',
     58                'bcs_alert_webhook_url',
     59                'bcs_slack_webhook_url',
     60                'bcs_teams_webhook_url',
     61                'bcs_google_chat_webhook_url',
     62                'bcs_discord_webhook_url',
     63                'bcs_custom_webhook_url',
     64                'bcs_max_alert_count',
     65                'bcs_alert_sent_count',
     66                'bcs_report_schedule_enabled',
     67                'bcs_report_emails',
     68                'bcs_report_frequency',
     69                'bcs_log_prune_mode',
     70                'bcs_log_prune_value',
     71                'bcs_smtp_migrated_v2',
     72                'bcs_db_version',
     73                'bcs_store_failed_body',
     74                'bcs_failed_body_retention_days',
     75                'bcs_smtp_migrated_1_3',
     76                'bcs_smtp_prune_after',
     77                'bcs_smtp_monitor_enabled',
     78        );
     79        foreach ( $bcs_smtp_options as $bcs_smtp_option ) {
     80                delete_option( $bcs_smtp_option );
     81        }
     82
     83        // Temporary values: the health-check lock, the cached server IP and per-user rate limits.
     84        delete_transient( 'bcs_smtp_check_lock' );
     85        delete_transient( 'bcs_smtp_source_ip' );
     86        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- uninstall routine; removes this plugin's own rate-limit transients.
     87        $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", $wpdb->esc_like( '_transient_bcs_smtp_rl_' ) . '%', $wpdb->esc_like( '_transient_timeout_bcs_smtp_rl_' ) . '%' ) );
    1688}
    1789
    18 // Drop custom tables
    19 global $wpdb;
    20 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,PluginCheck.Security.DirectDB.UnescapedDBParameter -- uninstall routine; table names are hardcoded plugin prefixes, not user input.
    21 $bcs_smtp_tables = array(
    22         $wpdb->prefix . 'bcs_smtp_email_log',
    23         $wpdb->prefix . 'bch_smtp_email_log',
    24         $wpdb->prefix . 'bptsm_incidents',
    25 );
    26 foreach ( $bcs_smtp_tables as $bcs_smtp_table ) {
    27         $wpdb->query( "DROP TABLE IF EXISTS {$bcs_smtp_table}" );
     90// On a network each site has its own log table and settings, so clean every one of them.
     91if ( is_multisite() ) {
     92        foreach ( get_sites( array( 'fields' => 'ids', 'number' => 0 ) ) as $bcs_smtp_site_id ) {
     93                switch_to_blog( $bcs_smtp_site_id );
     94                bcs_smtp_uninstall_site();
     95                restore_current_blog();
     96        }
     97} else {
     98        bcs_smtp_uninstall_site();
    2899}
    29 // phpcs:enable
    30 
    31 // Delete all options
    32 $bcs_smtp_options = array(
    33         'bcs_smtp_enabled',
    34         'bcs_from_email',
    35         'bcs_from_name',
    36         'bcs_smtp_host',
    37         'bcs_smtp_port',
    38         'bcs_smtp_encryption',
    39         'bcs_auto_tls',
    40         'bcs_smtp_auth',
    41         'bcs_smtp_username',
    42         'bcs_smtp_password',
    43         'bcs_monitor_enabled',
    44         'bcs_check_interval',
    45         'bcs_failure_threshold',
    46         'bcs_consecutive_failures',
    47         'bcs_last_check',
    48         'bcs_last_check_success',
    49         'bcs_alert_channel_enabled',
    50         'bcs_alert_channel',
    51         'bcs_alert_webhook_url',
    52         'bcs_slack_webhook_url',
    53         'bcs_teams_webhook_url',
    54         'bcs_google_chat_webhook_url',
    55         'bcs_discord_webhook_url',
    56         'bcs_custom_webhook_url',
    57         'bcs_max_alert_count',
    58         'bcs_alert_sent_count',
    59         'bcs_report_schedule_enabled',
    60         'bcs_report_emails',
    61         'bcs_report_frequency',
    62         'bcs_log_prune_mode',
    63         'bcs_log_prune_value',
    64         'bcs_smtp_migrated_v2',
    65         'bcs_db_version',
    66         'bcs_store_failed_body',
    67         'bcs_failed_body_retention_days',
    68 );
    69 foreach ( $bcs_smtp_options as $bcs_smtp_option ) {
    70         delete_option( $bcs_smtp_option );
    71 }
  • bcs-smtp-manager/trunk/admin/class-bcs-smtp-admin.php

    r3652946 r3762759  
    146146                wp_enqueue_style(  'bcs-smtp-admin', BCS_SMTP_PLUGIN_URL . 'assets/css/admin.css', array(), BCS_SMTP_VERSION );
    147147
    148                 wp_enqueue_script( 'chartjs', BCS_SMTP_PLUGIN_URL . 'assets/js/chart.umd.min.js', array(), '4.4.0', true );
    149                 wp_enqueue_script( 'bcs-smtp-admin', BCS_SMTP_PLUGIN_URL . 'assets/js/admin.js', array( 'jquery', 'chartjs' ), BCS_SMTP_VERSION, true );
     148                wp_enqueue_script( 'bcs-smtp-admin', BCS_SMTP_PLUGIN_URL . 'assets/js/admin.js', array( 'jquery' ), BCS_SMTP_VERSION, true );
    150149
    151150                wp_localize_script( 'bcs-smtp-admin', 'bcsSmtpData', array(
    … …  
    158157                        'smtpAuth'             => (bool) get_option( 'bcs_smtp_auth', true ),
    159158                        'smtpEncryption'       => get_option( 'bcs_smtp_encryption', 'tls' ),
    160                         'monitorEnabled'       => (bool) get_option( BCS_SMTP_OPT_MONITOR_ENABLED, false ),
    161159                        'alertChannelEnabled'  => (bool) get_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED, false ),
    162                         'alertChannel'         => get_option( BCS_SMTP_OPT_ALERT_CHANNEL, 'none' ),
    163160                        'passwordIsSet'        => ! empty( get_option( 'bcs_smtp_password', '' ) ),
     161                        'firstRun'             => '' === trim( (string) get_option( 'bcs_smtp_host', '' ) ),
     162                        'i18n'                 => array(
     163                                'chReady'         => __( 'Active', 'bcs-smtp-manager' ),
     164                                'chAddUrl'        => __( 'Needs URL', 'bcs-smtp-manager' ),
     165                                'chOff'           => __( 'Off', 'bcs-smtp-manager' ),
     166                                /* translators: 1: channels switched on, 2: all channels. */
     167                                'chCount'         => __( '%1$s of %2$s on', 'bcs-smtp-manager' ),
     168                                'chCheckUrl'      => __( 'Check URL', 'bcs-smtp-manager' ),
     169                                'urlHttps'        => __( 'Paste the full address, starting with https://', 'bcs-smtp-manager' ),
     170                                /* translators: %s: alert channel name, e.g. Slack. */
     171                                'urlHost'         => __( 'This doesn’t look like a %s webhook URL.', 'bcs-smtp-manager' ),
     172                                'urlPublic'       => __( 'Use a public https:// address, not localhost or an IP address.', 'bcs-smtp-manager' ),
     173                                'saveFirst'       => __( 'Save your changes first. The test uses your saved settings.', 'bcs-smtp-manager' ),
     174                                /* translators: %s: number of alert channels. */
     175                                'nChannels'       => __( '%s channels', 'bcs-smtp-manager' ),
     176                                'saving'          => __( 'Saving…', 'bcs-smtp-manager' ),
     177                                'savingTesting'   => __( 'Saving and testing…', 'bcs-smtp-manager' ),
     178                                'sending'         => __( 'Sending…', 'bcs-smtp-manager' ),
     179                                'checking'        => __( 'Checking…', 'bcs-smtp-manager' ),
     180                                'saved'           => __( 'Saved.', 'bcs-smtp-manager' ),
     181                                'saveFailed'      => __( 'Couldn’t save. Please try again.', 'bcs-smtp-manager' ),
     182                                'requestFailed'   => __( 'The request didn’t reach WordPress. Check your connection and try again.', 'bcs-smtp-manager' ),
     183                                /* translators: %s: HTTP status code. */
     184                                'requestFailedCode' => __( 'WordPress returned an error (HTTP %s). Check the server error log or turn on WP_DEBUG.', 'bcs-smtp-manager' ),
     185                                'connFailed'      => __( 'Connection failed:', 'bcs-smtp-manager' ),
     186                                'enterRecipient'  => __( 'Enter an email address to send the test to.', 'bcs-smtp-manager' ),
     187                                'testFailed'      => __( 'The test email wasn’t sent.', 'bcs-smtp-manager' ),
     188                                'checkFailed'     => __( 'The health check couldn’t run.', 'bcs-smtp-manager' ),
     189                                'resent'          => __( 'Resent', 'bcs-smtp-manager' ),
     190                                'pwSaved'         => __( 'Saved and encrypted.', 'bcs-smtp-manager' ),
     191                                'pwChanging'      => __( 'Enter the new password. Leave it empty to keep the saved one.', 'bcs-smtp-manager' ),
     192                                'showPw'          => __( 'Show password', 'bcs-smtp-manager' ),
     193                                'hidePw'          => __( 'Hide password', 'bcs-smtp-manager' ),
     194                                'copy'            => __( 'Copy', 'bcs-smtp-manager' ),
     195                                'copied'          => __( 'Copied', 'bcs-smtp-manager' ),
     196                                'loadError'       => __( 'The email log couldn’t be loaded. Reload the page to try again.', 'bcs-smtp-manager' ),
     197                                /* translators: 1: current page, 2: total pages, 3: number of emails. */
     198                                'pageInfo'        => __( 'Page %1$s of %2$s · %3$s emails', 'bcs-smtp-manager' ),
     199                                'pickServer'      => __( 'Filled in when you pick a provider', 'bcs-smtp-manager' ),
     200                                'sent'            => __( 'Sent', 'bcs-smtp-manager' ),
     201                                'failed'          => __( 'Failed', 'bcs-smtp-manager' ),
     202                                'unconfirmed'     => __( 'Unconfirmed', 'bcs-smtp-manager' ),
     203                                /* translators: %s: date the week starts, e.g. "Oct 1". */
     204                                'weekOf'          => __( 'Week of %s', 'bcs-smtp-manager' ),
     205                                /* translators: %s: date the two-week period starts, e.g. "Oct 1". */
     206                                'twoWeeksOf'      => __( '2 weeks from %s', 'bcs-smtp-manager' ),
     207                                /* translators: %s: number of emails. */
     208                                'emailsCount'     => __( '%s emails', 'bcs-smtp-manager' ),
     209                                /* translators: %s: number of days in the period. */
     210                                'ofDays'          => __( 'of %s days', 'bcs-smtp-manager' ),
     211                                /* translators: %s: period length, e.g. "30 days". */
     212                                'lastPeriod'      => __( 'Last %s', 'bcs-smtp-manager' ),
     213                                'lessThanOne'     => __( 'Less than 1', 'bcs-smtp-manager' ),
     214                                /* translators: %s: the entries that are not valid email addresses. */
     215                                'badEmails'       => __( 'These aren’t valid email addresses: %s', 'bcs-smtp-manager' ),
     216                                'needEmail'       => __( 'Enter at least one email address to send the report to.', 'bcs-smtp-manager' ),
     217                                /* translators: %s: email address. */
     218                                'removeEmail'     => __( 'Remove %s', 'bcs-smtp-manager' ),
     219                                'notAnEmail'      => __( 'Not a valid email address', 'bcs-smtp-manager' ),
     220                                'oneRecipient'    => _x( 'person', 'count label: 1 person', 'bcs-smtp-manager' ),
     221                                'manyRecipients'  => _x( 'people', 'count label: 2 or more people', 'bcs-smtp-manager' ),
     222                                'oneDay'          => _x( 'day', 'streak length: 1 day', 'bcs-smtp-manager' ),
     223                                'manyDays'        => _x( 'days', 'streak length: 2 or more days', 'bcs-smtp-manager' ),
     224                                /* translators: %s: email provider name, e.g. Microsoft 365 / Outlook. */
     225                                'needsAuth'       => __( '%s needs a username and password.', 'bcs-smtp-manager' ),
     226                        ),
    164227                ) );
    165228        }
    … …  
    226289                update_option( 'bcs_smtp_username',   $username );
    227290
     291                // Keep the "Other SMTP server" details on their own, so switching to a preset provider
     292                // and saving doesn't lose them — choosing "Other" again brings them back.
     293                $provider = isset( $_POST['provider'] ) ? sanitize_key( wp_unslash( $_POST['provider'] ) ) : '';
     294                if ( 'other' === $provider && '' !== $host ) {
     295                        update_option( 'bcs_smtp_custom_server', array( 'host' => $host, 'port' => $port, 'encryption' => $encryption ), false );
     296                }
     297
    228298                // Only overwrite the stored password if the user submitted a non-empty value.
    229299                // Encrypted at rest — see BCS_SMTP_Mailer::encrypt_password()/get_smtp_password().
    … …  
    241311                                update_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, 0 );
    242312                        }
     313                        // This test counts as a check, so the next scheduled one is counted from it.
     314                        $this->monitor->schedule_next_check();
    243315                }
    244316
    … …  
    254326                                        /* translators: 1: SMTP host, 2: SMTP port. */
    255327                                        ? sprintf( __( 'Connection test passed — SMTP server is reachable on %1$s:%2$s.', 'bcs-smtp-manager' ), esc_html( $host ), esc_html( (string) $port ) )
    256                                         : ( isset( $test_result['error_msg'] ) ? $test_result['error_msg'] : __( 'Connection test failed.', 'bcs-smtp-manager' ) ),
     328                                        : ( isset( $test_result['error_msg'] ) ? esc_html( $test_result['error_msg'] ) : __( 'Connection test failed.', 'bcs-smtp-manager' ) ),
    257329                        );
    258330                }
    … …  
    271343                check_ajax_referer( 'bcs_smtp_nonce', 'nonce' );
    272344                if ( ! current_user_can( 'manage_options' ) ) { ob_end_clean(); wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); }
    273                 if ( $this->is_rate_limited( 'send_test', 60 ) ) { ob_end_clean(); wp_send_json_error( __( 'Please wait 60 seconds before sending another test email.', 'bcs-smtp-manager' ) ); }
    274345
    275346                $to = isset( $_POST['to'] ) ? sanitize_email( wp_unslash( $_POST['to'] ) ) : '';
    … …  
    278349                        wp_send_json_error( __( 'Invalid recipient email address.', 'bcs-smtp-manager' ) );
    279350                }
     351                if ( $this->is_rate_limited( 'send_test', 15 ) ) { ob_end_clean(); wp_send_json_error( __( 'Please wait 15 seconds before sending another test email.', 'bcs-smtp-manager' ) ); }
    280352
    281353                $result = $this->mailer->send_test_email( $to );
    … …  
    296368                                        $hint = ' ' . __( 'SMTP is configured but PHPMailer is still using PHP mail(). Try deactivating and reactivating this plugin, or check if another email plugin is overriding the settings.', 'bcs-smtp-manager' );
    297369                                } else {
    298                                         $hint = ' ' . __( 'Configure and enable SMTP in the Settings tab — PHP mail() is not available on this server.', 'bcs-smtp-manager' );
     370                                        $hint = ' ' . __( 'Set up and enable SMTP on the Connection tab — PHP mail() is not available on this server.', 'bcs-smtp-manager' );
    299371                                }
    300372                        } elseif ( 'not_configured' === $result['failure_type'] ) {
    301                                 $hint = ' ' . __( 'Go to Settings, enter your SMTP details, and save before sending a test.', 'bcs-smtp-manager' );
     373                                $hint = ' ' . __( 'Enter your server details on the Connection tab and save before sending a test.', 'bcs-smtp-manager' );
    302374                        }
    303375                        wp_send_json_error( array(
    304                                 'message'      => $result['error_msg'] . $hint,
     376                                'message'      => esc_html( $result['error_msg'] ) . $hint,
    305377                                'failure_type' => $type,
    306378                        ) );
    … …  
    317389                if ( $this->is_rate_limited( 'check_now', 30 ) ) { wp_send_json_error( __( 'Please wait 30 seconds before running another health check.', 'bcs-smtp-manager' ) ); }
    318390
    319                 $this->monitor->run_health_check();
    320 
    321                 $success  = (bool) get_option( BCS_SMTP_OPT_LAST_CHECK_SUCCESS );
    322                 $failures = (int)  get_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, 0 );
     391                $result   = $this->monitor->run_health_check( true );
     392                $success  = ! empty( $result['success'] );
     393                $failures = (int) get_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, 0 );
     394
     395                if ( $success ) {
     396                        $message = __( 'Health check passed — connected and authenticated with the SMTP server.', 'bcs-smtp-manager' );
     397                } else {
     398                        /* translators: 1: number of consecutive failures, 2: error detail. */
     399                        $message = sprintf( __( 'Health check failed (%1$d consecutive failure(s)): %2$s', 'bcs-smtp-manager' ), $failures, esc_html( isset( $result['error_msg'] ) ? $result['error_msg'] : '' ) );
     400                }
    323401
    324402                wp_send_json_success( array(
    325403                        'success'  => $success,
    326404                        'failures' => $failures,
    327                         'message'  => $success
    328                                 ? __( 'Health check passed — SMTP connection successful.', 'bcs-smtp-manager' )
    329                                 /* translators: %d: number of consecutive failures. */
    330                                 : sprintf( __( 'Health check failed — %d consecutive failure(s).', 'bcs-smtp-manager' ), $failures ),
     405                        'message'  => $message,
    331406                ) );
    332407        }
    … …  
    334409        /* ─────────────────────────────────────────────────────────────────── */
    335410        /* AJAX — Save monitor settings                                          */
    336         /* ─────────────────────────────────────────────────────────────────── */
    337 
    338         public function ajax_save_monitor() {
    339                 check_ajax_referer( 'bcs_smtp_nonce', 'nonce' );
    340                 if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); }
    341 
    342                 $enabled        = ! empty( $_POST['monitor_enabled'] );
    343                 $interval       = isset( $_POST['check_interval'] )    ? absint( $_POST['check_interval'] )    : 5;
    344                 $threshold      = isset( $_POST['failure_threshold'] ) ? absint( $_POST['failure_threshold'] ) : 2;
    345                 $max_alert      = isset( $_POST['max_alert_count'] )   ? absint( $_POST['max_alert_count'] )   : 0;
    346 
    347                 $interval  = max( 1, min( 1440, $interval ) );
    348                 $threshold = max( 1, min( 10, $threshold ) );
    349                 $max_alert = min( 100, $max_alert );
    350 
    351                 $old_interval = (int) get_option( BCS_SMTP_OPT_CHECK_INTERVAL, 5 );
    352 
    353                 update_option( BCS_SMTP_OPT_MONITOR_ENABLED,   $enabled );
    354                 update_option( BCS_SMTP_OPT_CHECK_INTERVAL,    $interval );
    355                 update_option( BCS_SMTP_OPT_FAILURE_THRESHOLD, $threshold );
    356                 update_option( BCS_SMTP_OPT_MAX_ALERT_COUNT,   $max_alert );
    357 
    358                 if ( $enabled && $interval !== $old_interval ) {
    359                         $next = wp_next_scheduled( 'bcs_smtp_health_check' );
    360                         if ( $next ) {
    361                                 wp_unschedule_event( $next, 'bcs_smtp_health_check' );
    362                         }
    363                 }
    364 
    365                 $this->monitor->maybe_schedule_event();
    366 
    367                 wp_send_json_success( array( 'message' => __( 'Monitor settings saved.', 'bcs-smtp-manager' ) ) );
    368         }
    369 
    370411        /* ─────────────────────────────────────────────────────────────────── */
    371412        /* AJAX — Save alert channel settings                                    */
    … …  
    377418
    378419                $channel_enabled = ! empty( $_POST['alert_channel_enabled'] );
    379                 $allowed         = array( 'none', 'slack', 'teams', 'google_chat', 'discord', 'custom' );
    380                 $channel         = isset( $_POST['alert_channel'] ) ? sanitize_key( wp_unslash( $_POST['alert_channel'] ) ) : 'none';
    381                 $channel         = in_array( $channel, $allowed, true ) ? $channel : 'none';
     420                $labels          = BCS_SMTP_Monitor::channel_labels();
     421                $posted          = isset( $_POST['alert_channels'] ) ? array_map( 'sanitize_key', (array) wp_unslash( $_POST['alert_channels'] ) ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized by array_map.
     422                $channels        = array_values( array_intersect( array_keys( $labels ), $posted ) );
    382423                $slack_url       = isset( $_POST['slack_webhook_url'] ) ? esc_url_raw( wp_unslash( $_POST['slack_webhook_url'] ) ) : '';
    383424                $teams_url       = isset( $_POST['teams_webhook_url'] ) ? esc_url_raw( wp_unslash( $_POST['teams_webhook_url'] ) ) : '';
    … …  
    391432                }
    392433                if ( '' !== $teams_url && ! BCS_SMTP_Monitor::is_allowed_webhook_host( $teams_url, 'teams' ) ) {
    393                         wp_send_json_error( __( 'Teams webhook URL must be a Microsoft Office/Logic Apps https:// URL.', 'bcs-smtp-manager' ) );
     434                        wp_send_json_error( __( 'Teams webhook URL must be a Microsoft Teams Workflows (Power Automate / Logic Apps) or Office 365 connector https:// URL.', 'bcs-smtp-manager' ) );
    394435                }
    395436                if ( '' !== $google_chat_url && ! BCS_SMTP_Monitor::is_allowed_webhook_host( $google_chat_url, 'google_chat' ) ) {
    … …  
    403444                }
    404445
     446                $channel_urls = array( 'slack' => $slack_url, 'teams' => $teams_url, 'google_chat' => $google_chat_url, 'discord' => $discord_url, 'custom' => $custom_url );
     447                if ( $channel_enabled && ! $channels ) {
     448                        wp_send_json_error( __( 'Choose at least one place to send alerts.', 'bcs-smtp-manager' ) );
     449                }
     450                foreach ( $channel_enabled ? $channels : array() as $ch ) {
     451                        if ( '' === $channel_urls[ $ch ] ) {
     452                                /* translators: %s: alert channel name, e.g. Slack. */
     453                                wp_send_json_error( sprintf( __( 'Enter the %s webhook URL.', 'bcs-smtp-manager' ), $labels[ $ch ] ) );
     454                        }
     455                }
     456
    405457                update_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED,    $channel_enabled );
    406                 update_option( BCS_SMTP_OPT_ALERT_CHANNEL,            $channel );
     458                update_option( BCS_SMTP_OPT_ALERT_CHANNEL,            $channels );
    407459                update_option( BCS_SMTP_OPT_SLACK_WEBHOOK_URL,        $slack_url );
    408460                update_option( BCS_SMTP_OPT_TEAMS_WEBHOOK_URL,        $teams_url );
    … …  
    427479                }
    428480
    429                 $channel = get_option( BCS_SMTP_OPT_ALERT_CHANNEL, 'none' );
    430                 if ( 'none' === $channel ) {
    431                         wp_send_json_error( __( 'No alert channel configured.', 'bcs-smtp-manager' ) );
    432                 }
    433481
    434482                $mock_result = array(
    … …  
    442490                );
    443491
    444                 $this->monitor->send_alert( $mock_result, 2, false );
    445                 $channel_labels = array( 'slack' => 'Slack', 'teams' => 'Teams', 'google_chat' => 'Google Chat', 'discord' => 'Discord', 'custom' => 'Custom Webhook' );
    446                 $channel_label  = isset( $channel_labels[ $channel ] ) ? $channel_labels[ $channel ] : ucfirst( $channel );
    447                 /* translators: %s: alert channel name (e.g. Slack, Teams, Google Chat). */
    448                 wp_send_json_success( array( 'message' => sprintf( __( 'Test alert dispatched via %s channel.', 'bcs-smtp-manager' ), $channel_label ) ) );
     492                $results = $this->monitor->send_alert( $mock_result, 2, false, true );
     493                if ( is_wp_error( $results ) ) {
     494                        wp_send_json_error( esc_html( $results->get_error_message() ) );
     495                }
     496
     497                $labels = BCS_SMTP_Monitor::channel_labels();
     498                $ok     = array_keys( array_filter( $results, function ( $r ) { return true === $r; } ) );
     499                $errors = array();
     500                foreach ( $results as $ch => $r ) {
     501                        if ( is_wp_error( $r ) ) {
     502                                /* translators: 1: alert channel name, 2: error message. */
     503                                $errors[] = sprintf( __( '%1$s: %2$s', 'bcs-smtp-manager' ), $labels[ $ch ], $r->get_error_message() );
     504                        }
     505                }
     506                /* translators: %s: list of alert channels, e.g. "Slack and Discord". */
     507                $delivered = $ok ? sprintf( __( 'Test alert delivered to %s.', 'bcs-smtp-manager' ), BCS_SMTP_Monitor::channel_list_label( $ok ) ) : '';
     508                if ( ! $errors ) {
     509                        wp_send_json_success( array( 'message' => $delivered ) );
     510                }
     511                /* translators: %s: one or more "Channel: error" entries. */
     512                $failed = sprintf( __( 'Not delivered — %s', 'bcs-smtp-manager' ), implode( '; ', $errors ) );
     513                wp_send_json_error( esc_html( trim( $delivered . ' ' . $failed ) ) );
    449514        }
    450515
    … …  
    469534                ob_start();
    470535                if ( empty( $result['rows'] ) ) {
    471                         echo '<tr><td colspan="7" style="text-align:center;padding:24px;color:var(--bcs-smtp-text-3)">' . esc_html__( 'No emails found matching your filters.', 'bcs-smtp-manager' ) . '</td></tr>';
     536                        $filtered = '' !== $args['search'] || '' !== $args['status'];
     537                        echo '<tr><td colspan="5" class="bcs-smtp-table-empty"><strong>' . esc_html( $filtered ? __( 'No emails match these filters', 'bcs-smtp-manager' ) : __( 'No emails in this period', 'bcs-smtp-manager' ) ) . '</strong><span>' . esc_html( $filtered ? __( 'Try a wider date range or clear the search.', 'bcs-smtp-manager' ) : __( 'Every email WordPress sends will be listed here with its delivery status.', 'bcs-smtp-manager' ) ) . '</span></td></tr>';
    472538                } else {
     539                        $last_day = '';
    473540                        foreach ( $result['rows'] as $row ) {
     541                                $day = mysql2date( 'Y-m-d', $row['sent_at'] );
     542                                if ( $day !== $last_day ) {
     543                                        echo '<tr class="bcs-smtp-day-row"><th colspan="5" scope="colgroup">' . esc_html( $this->log_day_label( $day ) ) . '</th></tr>';
     544                                        $last_day = $day;
     545                                }
    474546                                $this->render_email_log_row( $row );
    475547                        }
    … …  
    478550
    479551                wp_send_json_success( array(
    480                         'html'  => $html,
    481                         'total' => $result['total'],
    482                         'pages' => $result['pages'],
    483                         'page'  => $result['page'],
     552                        'html'   => $html,
     553                        'counts' => $this->db->count_email_log_by_status( $args['date_range'], $args['search'] ),
     554                        'total'  => $result['total'],
     555                        'pages'  => $result['pages'],
     556                        'page'   => $result['page'],
    484557                ) );
    485558        }
    … …  
    495568                $date_range = isset( $_POST['date_range'] ) ? sanitize_key( wp_unslash( $_POST['date_range'] ) ) : '30days';
    496569
    497                 $summary    = $this->db->get_email_summary( $date_range );
     570                list( $own, $own_domains ) = $this->internal_recipients();
     571                $summary    = $this->db->get_email_summary_with_recipients( $date_range, $own, $own_domains );
    498572                $chart_data = $this->db->get_email_chart_data( $date_range );
    499573
    … …  
    509583        /* ─────────────────────────────────────────────────────────────────── */
    510584
     585        /**
     586         * A CSV cell that spreadsheet apps won't run as a formula. Subjects and addresses can come
     587         * from visitors (contact forms), so a value starting with =, +, -, @, tab or CR gets a
     588         * leading apostrophe, which Excel and Google Sheets show as plain text.
     589         *
     590         * @param  mixed $value
     591         * @return string
     592         */
     593        private static function csv_cell( $value ) {
     594                $value = (string) $value;
     595                return ( '' !== $value && false !== strpos( "=+-@\t\r", $value[0] ) ) ? "'" . $value : $value;
     596        }
     597
    511598        public function handle_export_csv() {
    512599                if ( ! isset( $_GET['bcs_smtp_action'] ) || 'export_csv' !== $_GET['bcs_smtp_action'] ) {
    513600                        return;
    514601                }
    515                 if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['nonce'] ) ), 'bcs_smtp_export' ) ) {
    516                         wp_die( esc_html__( 'Security check failed.', 'bcs-smtp-manager' ) );
    517                 }
    518602                if ( ! current_user_can( 'manage_options' ) ) {
    519                         wp_die( esc_html__( 'Permission denied.', 'bcs-smtp-manager' ) );
     603                        wp_die( esc_html__( 'Permission denied.', 'bcs-smtp-manager' ), '', array( 'response' => 403 ) );
     604                }
     605                if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'bcs_smtp_export' ) ) {
     606                        wp_die( esc_html__( 'This export link has expired. Reload the page and try again.', 'bcs-smtp-manager' ), '', array( 'response' => 403 ) );
    520607                }
    521608
    … …  
    537624                $out = fopen( 'php://output', 'w' );
    538625                fwrite( $out, "\xEF\xBB\xBF" ); // UTF-8 BOM for Excel compatibility
    539                 fputcsv( $out, array( 'ID', 'Date', 'Subject', 'From', 'To', 'Status', 'Error' ) );
     626                // Delimiter, enclosure and escape are passed explicitly: PHP 8.4 deprecates relying on the default escape.
     627                fputcsv( $out, array( 'ID', 'Date', 'Subject', 'From', 'To', 'Status', 'Error' ), ',', '"', '\\' );
    540628                foreach ( $rows as $row ) {
    541629                        fputcsv( $out, array(
    542630                                $row['id'],
    543631                                $row['sent_at'],
    544                                 $row['subject'],
    545                                 $row['from_address'],
    546                                 $row['to_address'],
     632                                self::csv_cell( $row['subject'] ),
     633                                self::csv_cell( $row['from_address'] ),
     634                                self::csv_cell( $row['to_address'] ),
    547635                                $row['status'],
    548                                 $row['error_msg'],
    549                         ) );
     636                                self::csv_cell( $row['error_msg'] ),
     637                        ), ',', '"', '\\' );
    550638                }
    551639                fclose( $out );
    … …  
    555643
    556644        /* ─────────────────────────────────────────────────────────────────── */
    557         /* AJAX — Save log retention / auto-prune settings                      */
    558         /* ─────────────────────────────────────────────────────────────────── */
    559 
    560         public function ajax_save_log_prune() {
    561                 check_ajax_referer( 'bcs_smtp_nonce', 'nonce' );
    562                 if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); }
    563 
    564                 $allowed_modes = array( 'days', 'records' );
    565                 $mode = isset( $_POST['prune_mode'] ) ? sanitize_key( wp_unslash( $_POST['prune_mode'] ) ) : 'days';
    566 
    567                 if ( ! in_array( $mode, $allowed_modes, true ) ) { $mode = 'days'; }
    568 
    569                 // Values are locked — 30 days or 1000 records. Server always enforces these limits.
    570                 $value = ( 'records' === $mode ) ? 1000 : 30;
    571 
    572                 update_option( 'bcs_log_prune_mode',  $mode );
    573                 update_option( 'bcs_log_prune_value', $value );
    574 
    575                 wp_send_json_success( array( 'message' => __( 'Log retention settings saved.', 'bcs-smtp-manager' ) ) );
    576         }
    577 
    578         /* ─────────────────────────────────────────────────────────────────── */
    579         /* Cron callback — run auto-prune                                        */
     645        /* Cron callback — prune the email log                                   */
    580646        /* ─────────────────────────────────────────────────────────────────── */
    581647
    582648        public function run_log_pruning() {
    583                 $mode  = get_option( 'bcs_log_prune_mode',  'days' );
    584                 $value = (int) get_option( 'bcs_log_prune_value', 30 );
    585                 $this->db->prune_email_log( $mode, $value );
    586 
    587                 // Failed-email bodies get their own, typically shorter, retention window.
    588                 $this->db->clear_old_failed_bodies( (int) get_option( BCS_SMTP_OPT_FAILED_BODY_RETENTION_DAYS, 7 ) );
    589         }
    590 
    591         /* ─────────────────────────────────────────────────────────────────── */
    592         /* AJAX — Resend a failed email                                          */
    593         /* ─────────────────────────────────────────────────────────────────── */
    594 
    595         public function ajax_resend_email() {
    596                 check_ajax_referer( 'bcs_smtp_nonce', 'nonce' );
    597                 if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); }
    598 
    599                 $log_id = isset( $_POST['log_id'] ) ? absint( $_POST['log_id'] ) : 0;
    600                 if ( ! $log_id ) { wp_send_json_error( __( 'Missing log entry ID.', 'bcs-smtp-manager' ) ); }
    601 
    602                 if ( $this->is_rate_limited( 'resend_' . $log_id, 30 ) ) {
    603                         wp_send_json_error( __( 'Please wait 30 seconds before resending this email again.', 'bcs-smtp-manager' ) );
    604                 }
    605 
    606                 $result = $this->mailer->resend_stored_email( $log_id );
    607                 if ( $result['success'] ) {
    608                         wp_send_json_success( array( 'message' => $result['message'] ) );
    609                 }
    610                 wp_send_json_error( $result['message'] );
    611         }
    612 
    613         /* ─────────────────────────────────────────────────────────────────── */
    614         /* AJAX — Save Resend settings                                           */
    615         /* ─────────────────────────────────────────────────────────────────── */
    616 
    617         public function ajax_save_resend_settings() {
    618                 check_ajax_referer( 'bcs_smtp_nonce', 'nonce' );
    619                 if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); }
    620 
    621                 $enabled = ! empty( $_POST['store_failed_body'] );
    622                 $days    = isset( $_POST['retention_days'] ) ? absint( $_POST['retention_days'] ) : 7;
    623                 $days    = max( 1, min( 30, $days ) );
    624 
    625                 update_option( BCS_SMTP_OPT_STORE_FAILED_BODY, $enabled );
    626                 update_option( BCS_SMTP_OPT_FAILED_BODY_RETENTION_DAYS, $days );
    627 
    628                 wp_send_json_success( array( 'message' => __( 'Resend settings saved.', 'bcs-smtp-manager' ) ) );
    629         }
    630 
    631         /* ─────────────────────────────────────────────────────────────────── */
    632         /* AJAX — Save report schedule                                           */
    633         /* ─────────────────────────────────────────────────────────────────── */
     649                // After updating from a version that kept everything, wait out the grace period first.
     650                $grace_until = (int) get_option( 'bcs_smtp_prune_after', 0 );
     651                if ( $grace_until > time() ) {
     652                        return;
     653                }
     654                if ( $grace_until ) {
     655                        delete_option( 'bcs_smtp_prune_after' );
     656                }
     657                // The log keeps the last 90 days; the reports and charts never look further back.
     658                $this->db->prune_email_log( 'days', BCS_SMTP_LOG_RETENTION_DAYS );
     659        }
     660
     661        /* ─────────────────────────────────────────────────────────────────── */
     662        /* Report recipients + AJAX — Save report schedule                       */
     663        /* ─────────────────────────────────────────────────────────────────── */
     664
     665        /**
     666         * Split a comma-separated recipient list into valid and invalid entries.
     667         *
     668         * @param string $list Raw input.
     669         * @return array { valid: string[] (lower-cased, de-duplicated), invalid: string[] }
     670         */
     671        private function parse_report_recipients( $list ) {
     672                $valid   = array();
     673                $invalid = array();
     674                foreach ( explode( ',', (string) $list ) as $part ) {
     675                        $part = trim( $part );
     676                        if ( '' === $part ) {
     677                                continue;
     678                        }
     679                        if ( is_email( $part ) ) {
     680                                $valid[ strtolower( $part ) ] = strtolower( $part );
     681                        } else {
     682                                $invalid[] = $part;
     683                        }
     684                }
     685                return array( 'valid' => array_values( $valid ), 'invalid' => $invalid );
     686        }
     687
     688        /**
     689         * Error text naming the entries that are not email addresses.
     690         *
     691         * @param string[] $invalid Bad entries.
     692         * @return string
     693         */
     694        private function invalid_recipients_message( $invalid ) {
     695                return sprintf(
     696                        /* translators: %s: the entries that are not valid email addresses, comma-separated. */
     697                        _n( 'This isn’t a valid email address: %s', 'These aren’t valid email addresses: %s', count( $invalid ), 'bcs-smtp-manager' ),
     698                        esc_html( implode( ', ', $invalid ) )
     699                );
     700        }
    634701
    635702        public function ajax_save_report_schedule() {
    … …  
    637704                if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); }
    638705
    639                 $enabled   = ! empty( $_POST['enabled'] );
    640                 $emails    = isset( $_POST['emails'] )    ? sanitize_text_field( wp_unslash( $_POST['emails'] ) )    : '';
    641                 $frequency = isset( $_POST['frequency'] ) ? sanitize_key( wp_unslash( $_POST['frequency'] ) )        : 'weekly';
    642 
    643                 if ( ! in_array( $frequency, array( 'daily', 'weekly', 'monthly' ), true ) ) {
    644                         $frequency = 'weekly';
    645                 }
     706                $enabled = ! empty( $_POST['enabled'] );
     707                $parsed  = $this->parse_report_recipients( isset( $_POST['emails'] ) ? sanitize_text_field( wp_unslash( $_POST['emails'] ) ) : '' );
     708
     709                if ( ! empty( $parsed['invalid'] ) ) {
     710                        wp_send_json_error( array( 'message' => $this->invalid_recipients_message( $parsed['invalid'] ), 'field' => 'emails' ) );
     711                }
     712                if ( $enabled && empty( $parsed['valid'] ) ) {
     713                        wp_send_json_error( array( 'message' => __( 'Enter at least one email address to send the report to.', 'bcs-smtp-manager' ), 'field' => 'emails' ) );
     714                }
     715                $emails = implode( ', ', $parsed['valid'] );
    646716
    647717                update_option( 'bcs_report_schedule_enabled', $enabled );
    648718                update_option( 'bcs_report_emails',           $emails );
    649                 update_option( 'bcs_report_frequency',        $frequency );
    650 
    651                 // Re-schedule the cron event.
    652                 $timestamp = wp_next_scheduled( 'bcs_smtp_send_report' );
    653                 if ( $timestamp ) {
    654                         wp_unschedule_event( $timestamp, 'bcs_smtp_send_report' );
    655                 }
    656                 if ( $enabled && ! empty( $emails ) ) {
    657                         $recurrence = ( 'daily' === $frequency ) ? 'daily' : ( ( 'monthly' === $frequency ) ? 'bcs_smtp_monthly' : 'weekly' );
    658                         wp_schedule_event( time() + DAY_IN_SECONDS, $recurrence, 'bcs_smtp_send_report' );
    659                 }
    660 
    661                 wp_send_json_success( array( 'message' => __( 'Report schedule saved.', 'bcs-smtp-manager' ) ) );
     719                // Reports are monthly, on the 1st at 9:00; options from the old schedule picker are retired.
     720
     721                $next = $this->schedule_next_report();
     722
     723                wp_send_json_success( array(
     724                        'message' => __( 'Report schedule saved.', 'bcs-smtp-manager' ),
     725                        'next'    => $next ? $this->describe_next_report( $next ) : '',
     726                        'emails'  => $emails,
     727                ) );
    662728        }
    663729
    … …  
    669735                check_ajax_referer( 'bcs_smtp_nonce', 'nonce' );
    670736                if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); }
     737                $parsed = $this->parse_report_recipients( isset( $_POST['emails'] ) ? sanitize_text_field( wp_unslash( $_POST['emails'] ) ) : '' );
     738                if ( ! empty( $parsed['invalid'] ) ) {
     739                        wp_send_json_error( array( 'message' => $this->invalid_recipients_message( $parsed['invalid'] ), 'field' => 'emails' ) );
     740                }
     741                if ( empty( $parsed['valid'] ) ) {
     742                        wp_send_json_error( array( 'message' => __( 'Enter at least one email address to send the report to.', 'bcs-smtp-manager' ), 'field' => 'emails' ) );
     743                }
    671744                if ( $this->is_rate_limited( 'send_report', 120 ) ) { wp_send_json_error( __( 'Please wait 2 minutes before sending another report.', 'bcs-smtp-manager' ) ); }
    672745
    673                 $emails    = isset( $_POST['emails'] ) ? sanitize_text_field( wp_unslash( $_POST['emails'] ) ) : '';
    674                 $frequency = get_option( 'bcs_report_frequency', 'weekly' );
    675                 $range_map = array( 'daily' => 'today', 'weekly' => '7days', 'monthly' => 'this_month' );
    676                 $date_range = isset( $range_map[ $frequency ] ) ? $range_map[ $frequency ] : '7days';
    677 
    678                 if ( empty( $emails ) ) {
    679                         wp_send_json_error( __( 'No recipient email addresses entered.', 'bcs-smtp-manager' ) );
    680                 }
     746                $emails     = implode( ', ', $parsed['valid'] );
     747                // "Send now" shows the current month so far; the scheduled report on the 1st covers the full previous month.
     748                $date_range = 'this_month';
    681749
    682750                $result = $this->send_report_to( $emails, $date_range );
    … …  
    699767        public function send_scheduled_report() {
    700768                if ( ! get_option( 'bcs_report_schedule_enabled', false ) ) {
     769                        wp_clear_scheduled_hook( 'bcs_smtp_send_report' );
    701770                        return;
    702771                }
    703                 $emails    = get_option( 'bcs_report_emails', '' );
    704                 $frequency = get_option( 'bcs_report_frequency', 'weekly' );
    705                 $range_map = array( 'daily' => 'today', 'weekly' => '7days', 'monthly' => 'this_month' );
    706                 $date_range = isset( $range_map[ $frequency ] ) ? $range_map[ $frequency ] : '7days';
     772                $emails = get_option( 'bcs_report_emails', '' );
    707773                if ( ! empty( $emails ) ) {
    708                         $this->send_report_to( $emails, $date_range );
    709                 }
     774                        $this->send_report_to( $emails, $this->report_range_for() );
     775                }
     776                // One-off events chained at the chosen time; this also replaces the interval-based
     777                // recurring event older versions created, which drifted from the save time.
     778                $this->schedule_next_report();
     779        }
     780
     781        /**
     782         * The period a report covers: reports go out on the 1st, so they cover the whole previous month.
     783         *
     784         * @return string Date-range key understood by BCS_SMTP_DB.
     785         */
     786        private function report_range_for() {
     787                return 'last_month';
     788        }
     789
     790        /**
     791         * Next send time for the saved schedule, in the site's timezone.
     792         *
     793         * @return int|null Unix timestamp, or null when no schedule is active.
     794         */
     795        private function next_report_timestamp() {
     796                if ( ! get_option( 'bcs_report_schedule_enabled', false ) || '' === trim( (string) get_option( 'bcs_report_emails', '' ) ) ) {
     797                        return null;
     798                }
     799                $now = new DateTimeImmutable( 'now', wp_timezone() );
     800                $at  = $now->modify( 'first day of this month' )->setTime( 9, 0 );
     801                if ( $at <= $now ) {
     802                        $at = $at->modify( 'first day of next month' );
     803                }
     804                return $at->getTimestamp();
     805        }
     806
     807        /**
     808         * Replace any pending report event with one at the next scheduled time.
     809         *
     810         * @return int|null The booked timestamp.
     811         */
     812        /**
     813         * Keep the monthly report booked while reports are on: the 1.3 upgrade replaces the old
     814         * daily/weekly event, and a cron run that never happened can leave nothing queued.
     815         */
     816        public function ensure_report_scheduled() {
     817                if ( get_option( 'bcs_report_schedule_enabled', false ) && ! wp_next_scheduled( 'bcs_smtp_send_report' ) ) {
     818                        $this->schedule_next_report();
     819                }
     820        }
     821
     822        private function schedule_next_report() {
     823                wp_clear_scheduled_hook( 'bcs_smtp_send_report' );
     824                $next = $this->next_report_timestamp();
     825                if ( $next ) {
     826                        wp_schedule_single_event( $next, 'bcs_smtp_send_report' );
     827                }
     828                return $next;
     829        }
     830
     831        /**
     832         * "Next report: Mon, Oct 27 at 9:00 am" in the site's date/time format.
     833         *
     834         * @param int $timestamp Unix timestamp.
     835         * @return string
     836         */
     837        private function describe_next_report( $timestamp ) {
     838                /* translators: 1: date, e.g. "Mon, Oct 27", 2: time, e.g. "9:00 am". */
     839                return sprintf( __( 'Next report: %1$s at %2$s', 'bcs-smtp-manager' ), wp_date( 'D, M j', $timestamp ), wp_date( get_option( 'time_format', 'g:i a' ), $timestamp ) );
    710840        }
    711841
    … …  
    723853                $summary    = $this->db->get_email_summary( $date_range );
    724854                $chart_data = $this->db->get_email_chart_data( $date_range );
    725                 $subject    = sprintf( '[%s] Email Delivery Report', get_bloginfo( 'name' ) );
     855                /* translators: 1: site name, 2: period, e.g. "October 2026". */
     856                $subject    = sprintf( __( '%1$s email report: %2$s', 'bcs-smtp-manager' ), get_bloginfo( 'name' ), $this->report_period_label( $date_range ) );
    726857                $body       = $this->build_report_html_email( $summary, $chart_data, $date_range );
    727858
    … …  
    739870        }
    740871
     872        /**
     873         * "October 2026", or "October 2026 (so far)" for the current month.
     874         *
     875         * @param string $date_range Date-range key.
     876         * @return string
     877         */
     878        private function report_period_label( $date_range ) {
     879                if ( 'last_month' === $date_range ) {
     880                        return wp_date( 'F Y', strtotime( 'first day of last month', current_time( 'timestamp' ) ) );
     881                }
     882                if ( 'this_month' === $date_range ) {
     883                        /* translators: %s: month and year, e.g. "October 2026". */
     884                        return sprintf( __( '%s (so far)', 'bcs-smtp-manager' ), wp_date( 'F Y' ) );
     885                }
     886                return __( 'Last 30 days', 'bcs-smtp-manager' );
     887        }
     888
     889        /**
     890         * Days the report chart covers, as Y-m-d keys in site time, plus whether each is still to come.
     891         * A month report shows every day of that month, so the shape reads the same every time.
     892         *
     893         * @param string $date_range Date-range key.
     894         * @param array  $labels     Day keys the chart data has.
     895         * @return array List of array( 'Y-m-d', is_future ).
     896         */
     897        private function report_days( $date_range, $labels ) {
     898                $now = current_time( 'timestamp' );
     899                if ( 'last_month' === $date_range || 'this_month' === $date_range ) {
     900                        $first = 'last_month' === $date_range ? strtotime( 'first day of last month', $now ) : $now;
     901                        $today = gmdate( 'Y-m-d', $now );
     902                        $days  = array();
     903                        for ( $d = 1, $n = (int) gmdate( 't', $first ); $d <= $n; $d++ ) {
     904                                $key    = gmdate( 'Y-m-', $first ) . sprintf( '%02d', $d );
     905                                $days[] = array( $key, $key > $today );
     906                        }
     907                        return $days;
     908                }
     909                return array_map( function ( $key ) { return array( $key, false ); }, array_slice( $labels, -31 ) );
     910        }
     911
    741912        private function build_report_html_email( $summary, $chart_data, $date_range ) {
    742                 $site_name = get_bloginfo( 'name' );
    743                 $site_url  = get_bloginfo( 'url' );
    744                 $timestamp = current_time( 'M j, Y \a\t g:i A' );
    745                 $range_labels = array(
    746                         'today'      => 'Today',
    747                         '7days'      => 'Last 7 Days',
    748                         '30days'     => 'Last 30 Days',
    749                         'this_week'  => 'This Week',
    750                         'this_month' => 'This Month',
    751                         '3months'    => 'Last 3 Months',
    752                         '6months'    => 'Last 6 Months',
    753                         'year'       => 'Last Year',
    754                         'all'        => 'All Time',
     913                $T       = 'BCS_SMTP_Email_Template';
     914                $period  = $this->report_period_label( $date_range );
     915                $total   = (int) $summary['total'];
     916                $sent    = (int) $summary['success'];
     917                $failed  = (int) $summary['failed'];
     918                $unconf  = (int) $summary['unconfirmed'];
     919                $rate    = $total > 0 ? round( ( $sent / $total ) * 100, 1 ) : 0;
     920                $rate_tx = rtrim( rtrim( number_format_i18n( $rate, 1 ), '0' ), '.,' );
     921                $colors  = array( 'sent' => '#16a36a', 'failed' => '#e5484d', 'unconfirmed' => '#f0a020' );
     922
     923                // Headline verdict for the period.
     924                if ( 0 === $total ) {
     925                        $tone    = 'info';
     926                        $verdict = __( 'No email was sent', 'bcs-smtp-manager' );
     927                        $note    = __( 'Nothing went out in this period. If your site has contact forms or a shop, send a test email to make sure the connection still works.', 'bcs-smtp-manager' );
     928                } elseif ( 0 === $failed ) {
     929                        $tone    = 'ok';
     930                        $verdict = __( 'Every email was delivered', 'bcs-smtp-manager' );
     931                        /* translators: %s: number of emails. */
     932                        $note    = sprintf( _n( '%s email went out without a single failure.', 'All %s emails went out without a single failure.', $total, 'bcs-smtp-manager' ), number_format_i18n( $total ) );
     933                } else {
     934                        $tone    = $rate >= 90 ? 'warn' : 'bad';
     935                        /* translators: %s: number of failed emails. */
     936                        $verdict = sprintf( _n( '%s email failed', '%s emails failed', $failed, 'bcs-smtp-manager' ), number_format_i18n( $failed ) );
     937                        /* translators: 1: delivered count, 2: total count. */
     938                        $note    = sprintf( __( '%1$s of %2$s emails were delivered. The failed ones never reached their inbox.', 'bcs-smtp-manager' ), number_format_i18n( $sent ), number_format_i18n( $total ) );
     939                }
     940
     941                // Share-of-total bar.
     942                $bar = '';
     943                if ( $total > 0 ) {
     944                        foreach ( array( 'sent' => $sent, 'failed' => $failed, 'unconfirmed' => $unconf ) as $k => $v ) {
     945                                if ( $v > 0 ) {
     946                                        $bar .= '<td width="' . max( 1, round( $v / $total * 100 ) ) . '%" bgcolor="' . $colors[ $k ] . '" style="background:' . $colors[ $k ] . ';height:8px;font-size:0;line-height:0">&nbsp;</td>';
     947                                }
     948                        }
     949                } else {
     950                        $bar = '<td class="bcs-track" bgcolor="#e8ecf2" style="background:#e8ecf2;height:8px;font-size:0;line-height:0">&nbsp;</td>';
     951                }
     952
     953                // Stat columns.
     954                $stats = '';
     955                foreach ( array(
     956                        array( __( 'Delivered', 'bcs-smtp-manager' ), $sent, $colors['sent'] ),
     957                        array( __( 'Failed', 'bcs-smtp-manager' ), $failed, $colors['failed'] ),
     958                        array( __( 'Unconfirmed', 'bcs-smtp-manager' ), $unconf, $colors['unconfirmed'] ),
     959                ) as $i => $st ) {
     960                        $stats .= '<td width="33%" valign="top" class="bcs-line" style="padding:16px 0 0 ' . ( $i ? '16px;border-left:1px solid #edf0f5' : '0' ) . '">
     961          <div class="bcs-ink3" style="font-size:12px;color:#64748b"><span style="color:' . $st[2] . '">&#9679;</span>&nbsp;' . esc_html( $st[0] ) . '</div>
     962          <div class="bcs-ink" style="margin-top:4px;font-size:24px;font-weight:700;color:#0f172a;letter-spacing:-.3px">' . esc_html( number_format_i18n( $st[1] ) ) . '</div>
     963        </td>';
     964                }
     965
     966                // Daily column chart: every day of the period, stacked delivered / failed / unconfirmed.
     967                $by_day = array();
     968                foreach ( $chart_data['labels'] as $i => $key ) {
     969                        $by_day[ $key ] = array( (int) $chart_data['success'][ $i ], (int) $chart_data['failed'][ $i ], (int) $chart_data['unconfirmed'][ $i ] );
     970                }
     971                $days = $this->report_days( $date_range, $chart_data['labels'] );
     972                $max  = 1;
     973                foreach ( $days as $day ) {
     974                        if ( isset( $by_day[ $day[0] ] ) ) { $max = max( $max, array_sum( $by_day[ $day[0] ] ) ); }
     975                }
     976                $plot  = 96;
     977                $cols  = '';
     978                $width = round( 100 / max( 1, count( $days ) ), 2 );
     979                foreach ( $days as $day ) {
     980                        list( $s, $f, $u ) = isset( $by_day[ $day[0] ] ) ? $by_day[ $day[0] ] : array( 0, 0, 0 );
     981                        $stack = '';
     982                        if ( $day[1] ) {
     983                                $stack = '';
     984                        } elseif ( $s + $f + $u > 0 ) {
     985                                foreach ( array( array( $u, $colors['unconfirmed'] ), array( $f, $colors['failed'] ), array( $s, $colors['sent'] ) ) as $seg ) {
     986                                        if ( $seg[0] > 0 ) {
     987                                                $h      = max( 3, (int) round( $seg[0] / $max * $plot ) );
     988                                                $stack .= '<tr><td height="' . $h . '" bgcolor="' . $seg[1] . '" style="background:' . $seg[1] . ';height:' . $h . 'px;font-size:0;line-height:0">&nbsp;</td></tr>';
     989                                        }
     990                                }
     991                        } else {
     992                                $stack = '<tr><td height="2" class="bcs-track" bgcolor="#e3e8ef" style="background:#e3e8ef;height:2px;font-size:0;line-height:0">&nbsp;</td></tr>';
     993                        }
     994                        /* translators: 1: date, 2: delivered count, 3: failed count. */
     995                        $tip   = sprintf( __( '%1$s: %2$s delivered, %3$s failed', 'bcs-smtp-manager' ), wp_date( 'M j', strtotime( $day[0] . ' 12:00:00' ) ), $s, $f );
     996                        $cols .= '<td width="' . $width . '%" valign="bottom" title="' . esc_attr( $tip ) . '" style="padding:0 1px;height:' . $plot . 'px">'
     997                                . ( $stack ? '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">' . $stack . '</table>' : '' ) . '</td>';
     998                }
     999                $first_day = $days ? wp_date( 'M j', strtotime( $days[0][0] . ' 12:00:00' ) ) : '';
     1000                $mid_day   = $days ? wp_date( 'M j', strtotime( $days[ (int) floor( count( $days ) / 2 ) ][0] . ' 12:00:00' ) ) : '';
     1001                $last_day  = $days ? wp_date( 'M j', strtotime( $days[ count( $days ) - 1 ][0] . ' 12:00:00' ) ) : '';
     1002                $legend    = '';
     1003                foreach ( array( 'sent' => __( 'Delivered', 'bcs-smtp-manager' ), 'failed' => __( 'Failed', 'bcs-smtp-manager' ), 'unconfirmed' => __( 'Unconfirmed', 'bcs-smtp-manager' ) ) as $k => $label ) {
     1004                        $legend .= '<span style="white-space:nowrap;margin-left:12px"><span style="color:' . $colors[ $k ] . '">&#9632;</span>&nbsp;' . esc_html( $label ) . '</span>';
     1005                }
     1006
     1007                $body = '
     1008      <tr><td class="bcs-pad" style="padding:34px 40px 0">
     1009        ' . $T::pill( $verdict, $tone ) . '
     1010        <h1 class="bcs-ink bcs-h1" style="margin:16px 0 8px;font-size:28px;line-height:1.2;font-weight:700;letter-spacing:-.4px;color:#0f172a">' . esc_html( $period ) . '</h1>
     1011        <p class="bcs-ink2" style="margin:0;font-size:15px;line-height:1.65;color:#475569">' . esc_html( $note ) . '</p>
     1012      </td></tr>
     1013
     1014      <tr><td class="bcs-pad" style="padding:28px 40px 0">
     1015        <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
     1016          <td valign="bottom">
     1017            <div class="bcs-ink3" style="font-size:12px;color:#64748b">' . esc_html__( 'Delivery rate', 'bcs-smtp-manager' ) . '</div>
     1018            <div class="bcs-ink bcs-big" style="margin-top:2px;font-size:46px;line-height:1.05;font-weight:700;letter-spacing:-1.2px;color:#0f172a">' . esc_html( $total ? $rate_tx . '%' : '—' ) . '</div>
     1019          </td>
     1020          <td valign="bottom" align="right" class="bcs-ink3" style="font-size:13px;color:#64748b;padding-bottom:6px">' .
     1021                        /* translators: %s: number of emails. */
     1022                        esc_html( sprintf( _n( '%s email in total', '%s emails in total', $total, 'bcs-smtp-manager' ), number_format_i18n( $total ) ) ) . '</td>
     1023        </tr></table>
     1024        <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-top:14px;border-radius:4px;overflow:hidden"><tr>' . $bar . '</tr></table>
     1025        <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>' . $stats . '</tr></table>
     1026      </td></tr>
     1027
     1028      <tr><td class="bcs-pad" style="padding:32px 40px 0">
     1029        <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
     1030          <td class="bcs-ink" style="font-size:15px;font-weight:700;color:#0f172a">' . esc_html__( 'Day by day', 'bcs-smtp-manager' ) . '</td>
     1031          <td align="right" class="bcs-ink3 bcs-hide-sm" style="font-size:12px;color:#64748b">' . $legend . '</td>
     1032        </tr></table>
     1033        <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" class="bcs-line" style="margin-top:16px;border-bottom:1px solid #e3e8ef"><tr>' . $cols . '</tr></table>
     1034        <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-top:8px"><tr>
     1035          <td class="bcs-ink3" style="font-size:11px;color:#8a94a6">' . esc_html( $first_day ) . '</td>
     1036          <td class="bcs-ink3" align="center" style="font-size:11px;color:#8a94a6">' . esc_html( $mid_day ) . '</td>
     1037          <td class="bcs-ink3" align="right" style="font-size:11px;color:#8a94a6">' . esc_html( $last_day ) . '</td>
     1038        </tr></table>
     1039      </td></tr>
     1040
     1041      <tr><td class="bcs-pad" style="padding:28px 40px 36px">
     1042        ' . $T::button( $T::settings_url( $failed ? 'email-log' : 'reports' ), $failed ? __( 'Review failed emails', 'bcs-smtp-manager' ) : __( 'Open the full report', 'bcs-smtp-manager' ) ) . '
     1043      </td></tr>';
     1044
     1045                return $T::document( array(
     1046                        /* translators: %s: period, e.g. "October 2026". */
     1047                        'title'     => sprintf( __( 'Email report: %s', 'bcs-smtp-manager' ), $period ),
     1048                        /* translators: 1: period, 2: delivery-rate verdict, e.g. "3 emails failed". */
     1049                        'preheader' => sprintf( __( '%1$s: %2$s.', 'bcs-smtp-manager' ), $period, $total ? $verdict . ' · ' . $rate_tx . '% ' . __( 'delivered', 'bcs-smtp-manager' ) : $verdict ),
     1050                        'label'     => __( 'Monthly email report', 'bcs-smtp-manager' ),
     1051                        'body'      => $body,
     1052                        'footer'    => $T::footer( $T::settings_url( 'reports' ), __( 'Change or stop these reports', 'bcs-smtp-manager' ), esc_html__( 'You get this because you’re on the report list for this site.', 'bcs-smtp-manager' ) ),
     1053                ) );
     1054        }
     1055
     1056        /* ─────────────────────────────────────────────────────────────────── */
     1057        /* Helpers: status chips, provider presets                              */
     1058        /* ─────────────────────────────────────────────────────────────────── */
     1059
     1060        /**
     1061         * Status vocabulary shared by the log table, the overview and the JS.
     1062         *
     1063         * @return array status => array( 'tone' => css modifier, 'label' => visible word )
     1064         */
     1065        private function status_marks() {
     1066                return array(
     1067                        'success'     => array( 'tone' => 'ok',   'label' => __( 'Sent', 'bcs-smtp-manager' ) ),
     1068                        'failed'      => array( 'tone' => 'bad',  'label' => __( 'Failed', 'bcs-smtp-manager' ) ),
     1069                        'unconfirmed' => array( 'tone' => 'warn', 'label' => __( 'Unconfirmed', 'bcs-smtp-manager' ) ),
    7551070                );
    756                 $range_label = isset( $range_labels[ $date_range ] ) ? $range_labels[ $date_range ] : $date_range;
    757 
    758                 // Build simple HTML bar chart rows.
    759                 $max_total = 1;
    760                 foreach ( $chart_data['labels'] as $i => $day ) {
    761                         $day_total = $chart_data['success'][ $i ] + $chart_data['failed'][ $i ] + $chart_data['unconfirmed'][ $i ];
    762                         if ( $day_total > $max_total ) { $max_total = $day_total; }
    763                 }
    764 
    765                 // Build bar chart rows — skip days with zero activity; show last 14 days with data.
    766                 $active_days = array();
    767                 foreach ( $chart_data['labels'] as $i => $label ) {
    768                         if ( $chart_data['success'][ $i ] > 0 || $chart_data['failed'][ $i ] > 0 || $chart_data['unconfirmed'][ $i ] > 0 ) {
    769                                 $active_days[] = $i;
     1071        }
     1072
     1073        /**
     1074         * Status chip for one email's delivery status.
     1075         *
     1076         * @param string $status success|failed|unconfirmed.
     1077         * @return string Escaped HTML.
     1078         */
     1079        /**
     1080         * Whole minutes since the last health check (rounded down) or until the next (rounded up),
     1081         * so the two always add up to the check interval.
     1082         *
     1083         * @param  int  $timestamp
     1084         * @param  bool $until True for a time in the future.
     1085         * @return string
     1086         */
     1087        private function check_minutes( $timestamp, $until = false ) {
     1088                $seconds = $until ? $timestamp - time() : time() - $timestamp;
     1089                $mins    = $until ? (int) ceil( $seconds / 60 ) : (int) floor( $seconds / 60 );
     1090                if ( $mins < 1 ) {
     1091                        return __( 'less than a minute', 'bcs-smtp-manager' );
     1092                }
     1093                /* translators: %d: number of minutes. */
     1094                return sprintf( _n( '%d minute', '%d minutes', $mins, 'bcs-smtp-manager' ), $mins );
     1095        }
     1096
     1097        private function status_html( $status ) {
     1098                $marks = $this->status_marks();
     1099                $mark  = isset( $marks[ $status ] ) ? $marks[ $status ] : array( 'tone' => 'off', 'label' => $status );
     1100                return '<span class="bcs-smtp-status bcs-smtp-status--' . esc_attr( $mark['tone'] ) . '"><span class="bcs-smtp-status-dot" aria-hidden="true"></span>' . esc_html( $mark['label'] ) . '</span>';
     1101        }
     1102
     1103        /**
     1104         * Provider presets for guided setup. Host/port/encryption are standard published SMTP settings.
     1105         *
     1106         * @return array
     1107         */
     1108        private function provider_presets() {
     1109                // Last saved "Other SMTP server" details. Older installs that saved a custom host
     1110                // before this option existed fall back to the current host when it isn't a preset.
     1111                $custom = wp_parse_args( (array) get_option( 'bcs_smtp_custom_server', array() ), array( 'host' => '', 'port' => 587, 'encryption' => 'tls' ) );
     1112                $saved  = strtolower( trim( (string) get_option( 'bcs_smtp_host', '' ) ) );
     1113                $known  = array( 'smtp.gmail.com', 'smtp.office365.com', 'smtp-mail.outlook.com', 'smtp.sendgrid.net', 'smtp.mailgun.org', 'smtp-relay.brevo.com', 'smtp.zoho.com', 'smtp.zoho.eu', 'smtp.zoho.in', 'smtp.mail.yahoo.com' );
     1114                if ( '' === $custom['host'] && '' !== $saved && ! in_array( $saved, $known, true ) && ! preg_match( '/^email-smtp\.[a-z0-9-]+\.amazonaws\.com$/', $saved ) ) {
     1115                        $custom = array( 'host' => $saved, 'port' => (int) get_option( 'bcs_smtp_port', 587 ), 'encryption' => get_option( 'bcs_smtp_encryption', 'tls' ) );
     1116                }
     1117
     1118                return array(
     1119                        'gmail'     => array(
     1120                                'label' => __( 'Gmail / Google Workspace', 'bcs-smtp-manager' ),
     1121                                'short' => 'G',
     1122                                'host'  => 'smtp.gmail.com',
     1123                                'port'  => 587,
     1124                                'enc'   => 'tls',
     1125                                'user'  => '',
     1126                                'user_hint' => __( 'Your full Gmail or Workspace address', 'bcs-smtp-manager' ),
     1127                                'pass_label' => __( 'App password', 'bcs-smtp-manager' ),
     1128                                'note'  => __( 'Google needs a 16-character App Password here, not your normal password.', 'bcs-smtp-manager' ),
     1129                                'link'  => 'https://myaccount.google.com/apppasswords',
     1130                                'link_label' => __( 'Create an App Password', 'bcs-smtp-manager' ),
     1131                        ),
     1132                        'microsoft' => array(
     1133                                'label' => __( 'Microsoft 365 / Outlook', 'bcs-smtp-manager' ),
     1134                                'short' => 'M',
     1135                                'host'  => 'smtp.office365.com',
     1136                                'port'  => 587,
     1137                                'enc'   => 'tls',
     1138                                'user'  => '',
     1139                                'user_hint' => __( 'Your full Microsoft 365 email address', 'bcs-smtp-manager' ),
     1140                                'pass_label' => __( 'Password or app password', 'bcs-smtp-manager' ),
     1141                                'note'  => __( 'SMTP sign-in (SMTP AUTH) must be allowed for this mailbox in Microsoft 365 admin.', 'bcs-smtp-manager' ),
     1142                                'link'  => 'https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission',
     1143                                'link_label' => __( 'How to allow SMTP AUTH', 'bcs-smtp-manager' ),
     1144                        ),
     1145                        'sendgrid'  => array(
     1146                                'label' => __( 'SendGrid', 'bcs-smtp-manager' ),
     1147                                'short' => 'SG',
     1148                                'host'  => 'smtp.sendgrid.net',
     1149                                'port'  => 587,
     1150                                'enc'   => 'tls',
     1151                                'user'  => 'apikey',
     1152                                'user_hint' => __( 'Always the word apikey', 'bcs-smtp-manager' ),
     1153                                'pass_label' => __( 'API key', 'bcs-smtp-manager' ),
     1154                                'note'  => __( 'Paste a SendGrid API key with Mail Send permission as the password.', 'bcs-smtp-manager' ),
     1155                                'link'  => 'https://docs.sendgrid.com/for-developers/sending-email/getting-started-smtp',
     1156                                'link_label' => __( 'SendGrid SMTP guide', 'bcs-smtp-manager' ),
     1157                        ),
     1158                        'ses'       => array(
     1159                                'label' => __( 'Amazon SES', 'bcs-smtp-manager' ),
     1160                                'short' => 'SES',
     1161                                // SES servers are per region, so there's no single right default: the user pastes theirs.
     1162                                'host'  => '',
     1163                                'port'  => 587,
     1164                                'enc'   => 'tls',
     1165                                'user'  => '',
     1166                                'user_hint' => __( 'SMTP username from the SES console', 'bcs-smtp-manager' ),
     1167                                'pass_label' => __( 'SMTP password', 'bcs-smtp-manager' ),
     1168                                'note'  => __( 'Copy the SMTP endpoint for your region from the SES console (SMTP settings) into Server details. Use SES SMTP credentials, not your AWS access keys.', 'bcs-smtp-manager' ),
     1169                                'link'  => 'https://docs.aws.amazon.com/ses/latest/dg/send-email-smtp.html',
     1170                                'link_label' => __( 'SES SMTP guide', 'bcs-smtp-manager' ),
     1171                        ),
     1172                        'mailgun'   => array(
     1173                                'label' => __( 'Mailgun', 'bcs-smtp-manager' ),
     1174                                'short' => 'MG',
     1175                                'host'  => 'smtp.mailgun.org',
     1176                                'port'  => 587,
     1177                                'enc'   => 'tls',
     1178                                'user'  => '',
     1179                                'user_hint' => __( 'For example postmaster@mg.yourdomain.com', 'bcs-smtp-manager' ),
     1180                                'pass_label' => __( 'SMTP password', 'bcs-smtp-manager' ),
     1181                                'note'  => __( 'Find the SMTP login and password under your sending domain in Mailgun.', 'bcs-smtp-manager' ),
     1182                                'link'  => 'https://documentation.mailgun.com/docs/mailgun/user-manual/sending-messages/',
     1183                                'link_label' => __( 'Mailgun SMTP guide', 'bcs-smtp-manager' ),
     1184                        ),
     1185                        'brevo'     => array(
     1186                                'label' => __( 'Brevo', 'bcs-smtp-manager' ),
     1187                                'short' => 'B',
     1188                                'host'  => 'smtp-relay.brevo.com',
     1189                                'port'  => 587,
     1190                                'enc'   => 'tls',
     1191                                'user'  => '',
     1192                                'user_hint' => __( 'Your Brevo SMTP login', 'bcs-smtp-manager' ),
     1193                                'pass_label' => __( 'SMTP key', 'bcs-smtp-manager' ),
     1194                                'note'  => __( 'Use an SMTP key from Brevo, not your account password.', 'bcs-smtp-manager' ),
     1195                                'link'  => 'https://help.brevo.com/hc/en-us/articles/209462765',
     1196                                'link_label' => __( 'Brevo SMTP guide', 'bcs-smtp-manager' ),
     1197                        ),
     1198                        'zoho'      => array(
     1199                                'label' => __( 'Zoho Mail', 'bcs-smtp-manager' ),
     1200                                'short' => 'Z',
     1201                                'host'  => 'smtp.zoho.com',
     1202                                'port'  => 465,
     1203                                'enc'   => 'ssl',
     1204                                'user'  => '',
     1205                                'user_hint' => __( 'Your full Zoho email address', 'bcs-smtp-manager' ),
     1206                                'pass_label' => __( 'Password or app password', 'bcs-smtp-manager' ),
     1207                                'note'  => __( 'EU accounts use smtp.zoho.eu. Use an app-specific password if two-factor sign-in is on.', 'bcs-smtp-manager' ),
     1208                                'link'  => 'https://www.zoho.com/mail/help/zoho-smtp.html',
     1209                                'link_label' => __( 'Zoho SMTP guide', 'bcs-smtp-manager' ),
     1210                        ),
     1211                        'yahoo'     => array(
     1212                                'label' => __( 'Yahoo Mail', 'bcs-smtp-manager' ),
     1213                                'short' => 'Y',
     1214                                'host'  => 'smtp.mail.yahoo.com',
     1215                                'port'  => 465,
     1216                                'enc'   => 'ssl',
     1217                                'user'  => '',
     1218                                'user_hint' => __( 'Your full Yahoo email address', 'bcs-smtp-manager' ),
     1219                                'pass_label' => __( 'App password', 'bcs-smtp-manager' ),
     1220                                'note'  => __( 'Generate an app password under Yahoo Account Security.', 'bcs-smtp-manager' ),
     1221                                'link'  => 'https://help.yahoo.com/kb/SLN15241.html',
     1222                                'link_label' => __( 'Yahoo app passwords', 'bcs-smtp-manager' ),
     1223                        ),
     1224                        'other'     => array(
     1225                                'label' => __( 'Other SMTP server', 'bcs-smtp-manager' ),
     1226                                'short' => '',
     1227                                'host'  => $custom['host'],
     1228                                'port'  => $custom['port'],
     1229                                'enc'   => $custom['encryption'],
     1230                                'user'  => '',
     1231                                'user_hint' => __( 'Usually your full email address', 'bcs-smtp-manager' ),
     1232                                'pass_label' => __( 'Password', 'bcs-smtp-manager' ),
     1233                                'note'  => __( 'Your host or email provider lists these details. Port 587 with TLS works for most servers.', 'bcs-smtp-manager' ),
     1234                                'link'  => '',
     1235                                'link_label' => '',
     1236                        ),
     1237                );
     1238        }
     1239
     1240        /**
     1241         * Which preset matches the saved host. Empty string when nothing is saved yet.
     1242         *
     1243         * @param string $host Saved SMTP host.
     1244         * @return string
     1245         */
     1246        private function detect_provider( $host ) {
     1247                $host = strtolower( trim( $host ) );
     1248                if ( '' === $host ) {
     1249                        return '';
     1250                }
     1251                if ( preg_match( '/^email-smtp\.[a-z0-9-]+\.amazonaws\.com$/', $host ) ) {
     1252                        return 'ses';
     1253                }
     1254                if ( 'smtp.zoho.eu' === $host || 'smtp.zoho.in' === $host ) {
     1255                        return 'zoho';
     1256                }
     1257                if ( 'smtp-mail.outlook.com' === $host ) {
     1258                        return 'microsoft';
     1259                }
     1260                foreach ( $this->provider_presets() as $key => $preset ) {
     1261                        if ( $preset['host'] && $preset['host'] === $host ) {
     1262                                return $key;
    7701263                        }
    7711264                }
    772                 $active_days = array_slice( $active_days, -14 ); // cap at 14
    773 
    774                 $chart_rows = '';
    775                 foreach ( $active_days as $i ) {
    776                         $s   = $chart_data['success'][ $i ];
    777                         $f   = $chart_data['failed'][ $i ];
    778                         $u   = $chart_data['unconfirmed'][ $i ];
    779                         $tot = $s + $f + $u;
    780                         $sw  = $max_total > 0 ? round( ( $s / $max_total ) * 100 ) : 0;
    781                         $fw  = $max_total > 0 ? round( ( $f / $max_total ) * 100 ) : 0;
    782                         $uw  = $max_total > 0 ? round( ( $u / $max_total ) * 100 ) : 0;
    783                         $day = substr( $chart_data['labels'][ $i ], 5 ); // MM-DD
    784                         $chart_rows .= '<tr>
    785                                 <td style="font-size:10px;color:#64748b;width:44px;text-align:right;padding-right:8px;white-space:nowrap">' . esc_html( $day ) . '</td>
    786                                 <td style="padding:2px 0">
    787                                         <table cellpadding="0" cellspacing="0" border="0" width="100%"><tr>
    788                                                 ' . ( $sw > 0 ? '<td style="background:#10b981;width:' . $sw . '%;height:14px;border-radius:2px"></td>' : '' ) . '
    789                                                 ' . ( $fw > 0 ? '<td style="background:#ef4444;width:' . $fw . '%;height:14px;border-radius:2px"></td>' : '' ) . '
    790                                                 ' . ( $uw > 0 ? '<td style="background:#f59e0b;width:' . $uw . '%;height:14px;border-radius:2px"></td>' : '' ) . '
    791                                                 <td></td>
    792                                         </tr></table>
    793                                 </td>
    794                                 <td style="font-size:10px;color:#64748b;width:32px;text-align:right;padding-left:6px">' . $tot . '</td>
    795                         </tr>';
    796                 }
    797 
    798                 $success_rate = $summary['total'] > 0 ? round( ( $summary['success'] / $summary['total'] ) * 100 ) : 0;
    799 
    800                 return BCS_SMTP_Email_Template::wrapper_open( 'Email Report', 620, 16, '.09' ) . '
    801   <!-- Header — bgcolor attr = Outlook fallback; background-color = Yahoo/other fallback; background-image = gradient for supporting clients -->
    802   <tr><td bgcolor="#6366f1" style="background-color:#6366f1;background-image:linear-gradient(135deg,#6366f1 0%,#4f46e5 100%);padding:36px 48px">
    803     <p style="margin:0;font-size:11px;color:#c7d2fe;letter-spacing:1.5px;text-transform:uppercase;font-weight:600">Email Delivery Report</p>
    804     <h1 style="margin:8px 0 4px;font-size:26px;color:#ffffff;font-weight:700;letter-spacing:-0.3px">' . esc_html( $site_name ) . '</h1>
    805     <p style="margin:0;font-size:13px;color:#e0e7ff">' . esc_html( $range_label ) . ' &nbsp;&bull;&nbsp; Generated ' . esc_html( $timestamp ) . '</p>
    806   </td></tr>
    807 
    808   <!-- Delivery rate banner -->
    809   <tr><td bgcolor="#4f46e5" style="background-color:#4f46e5;padding:14px 48px">
    810     <p style="margin:0;font-size:13px;color:#e0e7ff">
    811       <strong style="color:#ffffff">Delivery Rate: ' . $success_rate . '%</strong>
    812       &nbsp;&mdash;&nbsp;' . intval( $summary['success'] ) . ' of ' . intval( $summary['total'] ) . ' emails delivered successfully
    813     </p>
    814   </td></tr>
    815 
    816   <!-- Stat boxes -->
    817   <tr><td style="padding:36px 48px 0">
    818     <table width="100%" cellpadding="0" cellspacing="0" border="0">
    819     <tr>
    820       <td width="25%" style="padding:0 5px 0 0">
    821         <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px">
    822           <tr><td style="padding:18px 12px;text-align:center">
    823             <div style="font-size:32px;font-weight:700;color:#15803d;line-height:1">' . intval( $summary['success'] ) . '</div>
    824             <div style="font-size:11px;color:#16a34a;margin-top:6px;font-weight:600;text-transform:uppercase;letter-spacing:.5px">Sent</div>
    825           </td></tr>
    826         </table>
    827       </td>
    828       <td width="25%" style="padding:0 5px">
    829         <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#fef2f2;border:1px solid #fecaca;border-radius:10px">
    830           <tr><td style="padding:18px 12px;text-align:center">
    831             <div style="font-size:32px;font-weight:700;color:#dc2626;line-height:1">' . intval( $summary['failed'] ) . '</div>
    832             <div style="font-size:11px;color:#ef4444;margin-top:6px;font-weight:600;text-transform:uppercase;letter-spacing:.5px">Failed</div>
    833           </td></tr>
    834         </table>
    835       </td>
    836       <td width="25%" style="padding:0 5px">
    837         <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#fffbeb;border:1px solid #fde68a;border-radius:10px">
    838           <tr><td style="padding:18px 12px;text-align:center">
    839             <div style="font-size:32px;font-weight:700;color:#d97706;line-height:1">' . intval( $summary['unconfirmed'] ) . '</div>
    840             <div style="font-size:11px;color:#f59e0b;margin-top:6px;font-weight:600;text-transform:uppercase;letter-spacing:.5px">Unconfirmed</div>
    841           </td></tr>
    842         </table>
    843       </td>
    844       <td width="25%" style="padding:0 0 0 5px">
    845         <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px">
    846           <tr><td style="padding:18px 12px;text-align:center">
    847             <div style="font-size:32px;font-weight:700;color:#334155;line-height:1">' . intval( $summary['total'] ) . '</div>
    848             <div style="font-size:11px;color:#64748b;margin-top:6px;font-weight:600;text-transform:uppercase;letter-spacing:.5px">Total</div>
    849           </td></tr>
    850         </table>
    851       </td>
    852     </tr>
    853     </table>
    854   </td></tr>
    855 
    856   <!-- Chart -->
    857   <tr><td style="padding:32px 48px 0">
    858     <p style="margin:0 0 16px;font-size:14px;font-weight:700;color:#1e293b;letter-spacing:-0.2px">Daily Activity</p>
    859     <table width="100%" cellpadding="0" cellspacing="0" border="0">
    860       ' . $chart_rows . '
    861     </table>
    862     <!-- Legend -->
    863     <table cellpadding="0" cellspacing="0" border="0" style="margin-top:16px">
    864       <tr>
    865         <td style="width:10px;height:10px;background:#10b981;border-radius:2px;vertical-align:middle"></td>
    866         <td style="font-size:11px;color:#64748b;padding:0 16px 0 6px;vertical-align:middle">Sent</td>
    867         <td style="width:10px;height:10px;background:#ef4444;border-radius:2px;vertical-align:middle"></td>
    868         <td style="font-size:11px;color:#64748b;padding:0 16px 0 6px;vertical-align:middle">Failed</td>
    869         <td style="width:10px;height:10px;background:#f59e0b;border-radius:2px;vertical-align:middle"></td>
    870         <td style="font-size:11px;color:#64748b;padding:0 0 0 6px;vertical-align:middle">Unconfirmed</td>
    871       </tr>
    872     </table>
    873   </td></tr>
    874 
    875   <!-- Footer -->
    876   <tr><td style="padding:32px 48px;margin-top:32px;border-top:1px solid #e2e8f0">
    877     <table width="100%" cellpadding="0" cellspacing="0" border="0">
    878       <tr>
    879         <td style="text-align:center">
    880           <p style="margin:0 0 4px;font-size:12px;color:#64748b">
    881             <a href="' . esc_url( $site_url ) . '" style="color:#6366f1;text-decoration:none;font-weight:600">' . esc_html( $site_name ) . '</a>
    882           </p>
    883           <p style="margin:0;font-size:11px;color:#94a3b8">Automated report by <strong>ByteCoreStack SMTP</strong> &bull; <a href="' . esc_url( $site_url ) . '/wp-admin/options-general.php?page=bcs-smtp-manager" style="color:#94a3b8;text-decoration:none">Manage settings</a></p>
    884         </td>
    885       </tr>
    886     </table>
    887   </td></tr>
    888 ' . BCS_SMTP_Email_Template::wrapper_close();
     1265                return 'other';
    8891266        }
    8901267
    … …  
    8931270        /* ─────────────────────────────────────────────────────────────────── */
    8941271
     1272        /**
     1273         * Heading for a day in the email log: Today, Yesterday, or the date.
     1274         *
     1275         * @param  string $ymd Site-local date, Y-m-d.
     1276         * @return string
     1277         */
     1278        private function log_day_label( $ymd ) {
     1279                $today = current_time( 'Y-m-d' );
     1280                if ( $ymd === $today ) {
     1281                        return __( 'Today', 'bcs-smtp-manager' );
     1282                }
     1283                if ( gmdate( 'Y-m-d', strtotime( $today . ' -1 day' ) ) === $ymd ) {
     1284                        return __( 'Yesterday', 'bcs-smtp-manager' );
     1285                }
     1286                return mysql2date( 'l, F j', $ymd . ' 00:00:00' );
     1287        }
     1288
     1289        /**
     1290         * Link to the full view of one logged email.
     1291         *
     1292         * @param  int $id Log row ID.
     1293         * @return string
     1294         */
     1295        private function email_view_url( $id ) {
     1296                return add_query_arg( array( 'page' => 'bcs-smtp-manager', 'email' => (int) $id ), admin_url( 'options-general.php' ) );
     1297        }
     1298
     1299        /**
     1300         * Plain-text version of an HTML email: line breaks kept, links shown as "text (url)".
     1301         *
     1302         * @param  string $html
     1303         * @return string
     1304         */
     1305        private function email_text_version( $html ) {
     1306                $html = preg_replace( '#<(script|style|head|title)\b[^>]*>.*?</\1>#is', '', $html );
     1307                $html = preg_replace( '/\s+/u', ' ', $html ); // Line breaks in the source aren't real ones; tags decide below.
     1308                $html = preg_replace( '#<a\b[^>]*href=["\']([^"\']+)["\'][^>]*>(.*?)</a>#is', '$2 ($1)', $html );
     1309                $html = preg_replace( '#<br\s*/?>#i', "\n", $html );
     1310                $html = preg_replace( '#<li\b[^>]*>#i', "\n• ", $html );
     1311                $html = preg_replace( '#</(p|div|tr|table|h[1-6]|li|ul|ol|blockquote)>#i', "\n", $html );
     1312                $text = html_entity_decode( wp_strip_all_tags( $html ), ENT_QUOTES, 'UTF-8' );
     1313                $text = implode( "\n", array_map( 'trim', explode( "\n", str_replace( "\r", '', $text ) ) ) );
     1314                return trim( preg_replace( "/\n{3,}/", "\n\n", $text ) );
     1315        }
     1316
     1317        /**
     1318         * Full view of one logged email, laid out like a mail client: who it was from and to,
     1319         * the message (preview, HTML source or text) and, alongside, how it was delivered.
     1320         * HTML messages render in a sandboxed frame with scripts and forms off.
     1321         *
     1322         * @param int $id Log row ID.
     1323         */
     1324        private function render_email_view( $id ) {
     1325                $row  = $id ? $this->db->get_email_log_row( $id ) : null;
     1326                $back = admin_url( 'options-general.php?page=bcs-smtp-manager#email-log' );
     1327                ?>
     1328                <div class="bcs-smtp-wrap wrap bcs-smtp-mailview">
     1329                        <a class="bcs-smtp-back" href="<?php echo esc_url( $back ); ?>"><span class="dashicons dashicons-arrow-left-alt2" aria-hidden="true"></span><?php esc_html_e( 'Email log', 'bcs-smtp-manager' ); ?></a>
     1330                <?php
     1331                if ( ! $row ) {
     1332                        ?>
     1333                        <div class="bcs-smtp-panel bcs-smtp-mailview-missing">
     1334                                <h1><?php esc_html_e( 'This email isn’t in the log', 'bcs-smtp-manager' ); ?></h1>
     1335                                <p><?php esc_html_e( 'It may be more than 90 days old, or the log was cleared.', 'bcs-smtp-manager' ); ?></p>
     1336                        </div>
     1337                </div>
     1338                        <?php
     1339                        return;
     1340                }
     1341
     1342                $subject = $row['subject'] ? $row['subject'] : __( '(no subject)', 'bcs-smtp-manager' );
     1343                $headers = $row['mail_headers'] ? (array) json_decode( $row['mail_headers'], true ) : array();
     1344                $files   = $row['attachments'] ? (array) json_decode( $row['attachments'], true ) : array();
     1345                $named   = array();
     1346                foreach ( $headers as $header ) {
     1347                        list( $name, $value ) = array_pad( explode( ':', (string) $header, 2 ), 2, '' );
     1348                        $key = strtolower( trim( $name ) );
     1349                        if ( in_array( $key, array( 'from', 'cc', 'bcc', 'reply-to' ), true ) ) {
     1350                                $named[ $key ][] = trim( $value );
     1351                        }
     1352                }
     1353                $has_body = isset( $row['body'] ) && '' !== (string) $row['body'];
     1354                $is_html  = 'html' === $row['body_format'];
     1355                $adjacent = $this->db->get_adjacent_log_ids( (int) $row['id'] );
     1356                // sent_at is stored in site-local time.
     1357                $sent     = mysql2date( get_option( 'date_format', 'F j, Y' ) . ' ' . get_option( 'time_format', 'g:i a' ), $row['sent_at'] );
     1358
     1359                // "Name <address>" → name and address, for the sender line.
     1360                $from       = (string) $row['from_address'];
     1361                $from_email = preg_match( '/<([^>]+)>/', $from, $m ) ? $m[1] : $from;
     1362                $from_name  = trim( preg_replace( '/<[^>]*>/', '', $from ), " \"'" );
     1363                $from_name  = '' !== $from_name && $from_name !== $from_email ? $from_name : '';
     1364                $initial    = strtoupper( function_exists( 'mb_substr' ) ? mb_substr( $from_name ? $from_name : $from_email, 0, 1 ) : substr( $from_name ? $from_name : $from_email, 0, 1 ) );
     1365
     1366                // The address the sending code asked for, when the mail server sent as someone else.
     1367                $requested = isset( $named['from'] ) ? $named['from'][0] : '';
     1368                if ( $requested && false !== stripos( $from, preg_match( '/<([^>]+)>/', $requested, $rm ) ? $rm[1] : $requested ) ) {
     1369                        $requested = '';
     1370                }
     1371
     1372                $recipients = array(
     1373                        __( 'to', 'bcs-smtp-manager' )       => $row['to_address'],
     1374                        __( 'cc', 'bcs-smtp-manager' )       => isset( $named['cc'] ) ? implode( ', ', $named['cc'] ) : '',
     1375                        __( 'bcc', 'bcs-smtp-manager' )      => isset( $named['bcc'] ) ? implode( ', ', $named['bcc'] ) : '',
     1376                        __( 'reply to', 'bcs-smtp-manager' ) => isset( $named['reply-to'] ) ? implode( ', ', $named['reply-to'] ) : '',
     1377                );
     1378                $explain = array(
     1379                        'success'     => __( 'Your mail server accepted this email for delivery.', 'bcs-smtp-manager' ),
     1380                        'failed'      => __( 'This email was not sent. The server’s reply above says why.', 'bcs-smtp-manager' ),
     1381                        'unconfirmed' => __( 'WordPress handed this email over, but the server didn’t confirm it.', 'bcs-smtp-manager' ),
     1382                );
     1383                ?>
     1384                        <header class="bcs-smtp-mailview-head">
     1385                                <div class="bcs-smtp-mailview-title">
     1386                                        <p class="bcs-smtp-mailview-when"><?php echo $this->status_html( $row['status'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in status_html(). ?><span><?php echo esc_html( $sent ); ?></span></p>
     1387                                        <h1><?php echo esc_html( $subject ); ?></h1>
     1388                                </div>
     1389                                <nav class="bcs-smtp-mailview-nav" aria-label="<?php esc_attr_e( 'Other emails', 'bcs-smtp-manager' ); ?>">
     1390                                        <?php
     1391                                        $steps = array(
     1392                                                'newer' => array( __( 'Newer', 'bcs-smtp-manager' ), 'arrow-left-alt2' ),
     1393                                                'older' => array( __( 'Older', 'bcs-smtp-manager' ), 'arrow-right-alt2' ),
     1394                                        );
     1395                                        foreach ( $steps as $dir => $step ) :
     1396                                                $icon  = '<span class="dashicons dashicons-' . esc_attr( $step[1] ) . '" aria-hidden="true"></span>';
     1397                                                $label = 'newer' === $dir ? $icon . esc_html( $step[0] ) : esc_html( $step[0] ) . $icon;
     1398                                                if ( $adjacent[ $dir ] ) :
     1399                                                        ?>
     1400                                                        <a class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm" href="<?php echo esc_url( $this->email_view_url( $adjacent[ $dir ] ) ); ?>"><?php echo $label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built from escaped parts. ?></a>
     1401                                                <?php else : ?>
     1402                                                        <span class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm is-disabled" aria-disabled="true"><?php echo $label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built from escaped parts. ?></span>
     1403                                                        <?php
     1404                                                endif;
     1405                                        endforeach;
     1406                                        ?>
     1407                                </nav>
     1408                        </header>
     1409
     1410                        <?php if ( ! empty( $row['error_msg'] ) ) : ?>
     1411                                <div class="bcs-smtp-log-error bcs-smtp-mailview-error">
     1412                                        <div class="bcs-smtp-log-error-head">
     1413                                                <span><?php esc_html_e( 'Server reply', 'bcs-smtp-manager' ); ?></span>
     1414                                                <button type="button" class="bcs-smtp-copy-btn"><span class="dashicons dashicons-admin-page" aria-hidden="true"></span><span class="bcs-smtp-copy-label"><?php esc_html_e( 'Copy', 'bcs-smtp-manager' ); ?></span></button>
     1415                                        </div>
     1416                                        <pre class="bcs-smtp-mono"><?php echo esc_html( $row['error_msg'] ); ?></pre>
     1417                                </div>
     1418                        <?php endif; ?>
     1419
     1420                        <div class="bcs-smtp-mailview-grid">
     1421                                <section class="bcs-smtp-panel bcs-smtp-mailview-message" aria-label="<?php esc_attr_e( 'Message', 'bcs-smtp-manager' ); ?>">
     1422                                        <div class="bcs-smtp-envelope">
     1423                                                <span class="bcs-smtp-envelope-avatar" aria-hidden="true"><?php echo esc_html( $initial ); ?></span>
     1424                                                <div class="bcs-smtp-envelope-lines">
     1425                                                        <p class="bcs-smtp-envelope-from">
     1426                                                                <?php if ( $from_name ) : ?>
     1427                                                                        <strong><?php echo esc_html( $from_name ); ?></strong>
     1428                                                                        <span>&lt;<?php echo esc_html( $from_email ); ?>&gt;</span>
     1429                                                                <?php else : ?>
     1430                                                                        <strong><?php echo esc_html( $from_email ); ?></strong>
     1431                                                                <?php endif; ?>
     1432                                                        </p>
     1433                                                        <?php foreach ( $recipients as $label => $value ) : ?>
     1434                                                                <?php if ( '' !== trim( (string) $value ) ) : ?>
     1435                                                                        <p class="bcs-smtp-envelope-to"><span><?php echo esc_html( $label ); ?></span> <?php echo esc_html( $value ); ?></p>
     1436                                                                <?php endif; ?>
     1437                                                        <?php endforeach; ?>
     1438                                                </div>
     1439                                        </div>
     1440
     1441                                        <?php if ( $has_body ) : ?>
     1442                                                <div class="bcs-smtp-mailview-toolbar">
     1443                                                        <div class="bcs-smtp-segmented" role="radiogroup" aria-label="<?php esc_attr_e( 'Show the message as', 'bcs-smtp-manager' ); ?>">
     1444                                                                <?php if ( $is_html ) : ?>
     1445                                                                        <label><input type="radio" name="bcs_smtp_mailview_mode" value="preview" checked><span><?php esc_html_e( 'Preview', 'bcs-smtp-manager' ); ?></span></label>
     1446                                                                        <label><input type="radio" name="bcs_smtp_mailview_mode" value="source"><span><?php esc_html_e( 'HTML', 'bcs-smtp-manager' ); ?></span></label>
     1447                                                                <?php endif; ?>
     1448                                                                <label><input type="radio" name="bcs_smtp_mailview_mode" value="text" <?php checked( ! $is_html ); ?>><span><?php esc_html_e( 'Text', 'bcs-smtp-manager' ); ?></span></label>
     1449                                                        </div>
     1450                                                        <?php if ( $is_html ) : ?>
     1451                                                                <div class="bcs-smtp-segmented bcs-smtp-device" role="radiogroup" aria-label="<?php esc_attr_e( 'Preview width', 'bcs-smtp-manager' ); ?>" data-mode="preview">
     1452                                                                        <label title="<?php esc_attr_e( 'Desktop', 'bcs-smtp-manager' ); ?>"><input type="radio" name="bcs_smtp_mailview_device" value="desktop" checked><span><svg class="bcs-smtp-device-icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><rect x="2.5" y="3.5" width="15" height="10" rx="1.5"/><path d="M7 17h6M10 13.5V17"/></svg><span class="screen-reader-text"><?php esc_html_e( 'Desktop', 'bcs-smtp-manager' ); ?></span></span></label>
     1453                                                                        <label title="<?php esc_attr_e( 'Mobile', 'bcs-smtp-manager' ); ?>"><input type="radio" name="bcs_smtp_mailview_device" value="mobile"><span><svg class="bcs-smtp-device-icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><rect x="6" y="2.5" width="8" height="15" rx="1.8"/><path d="M9 15h2"/></svg><span class="screen-reader-text"><?php esc_html_e( 'Mobile', 'bcs-smtp-manager' ); ?></span></span></label>
     1454                                                                </div>
     1455                                                        <?php endif; ?>
     1456                                                </div>
     1457                                        <?php endif; ?>
     1458
     1459                                        <div class="bcs-smtp-mailview-body">
     1460                                                <?php if ( $has_body && $is_html ) : ?>
     1461                                                        <div class="bcs-smtp-mailframe-stage" data-mode="preview">
     1462                                                                <div class="bcs-smtp-mailframe-wrap">
     1463                                                                        <?php // Full encoding (double_encode on) keeps entities in the email exactly as sent. ?>
     1464                                                                        <iframe class="bcs-smtp-mailframe" scrolling="no" title="<?php esc_attr_e( 'Email preview', 'bcs-smtp-manager' ); ?>" sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox" referrerpolicy="no-referrer" srcdoc="<?php echo htmlspecialchars( '<base target="_blank">' . $row['body'], ENT_QUOTES, 'UTF-8', true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fully encoded for the attribute. ?>"></iframe>
     1465                                                                </div>
     1466                                                        </div>
     1467                                                        <div class="bcs-smtp-mailview-source" data-mode="source" hidden>
     1468                                                                <div class="bcs-smtp-mailview-source-head">
     1469                                                                        <span><?php esc_html_e( 'HTML source', 'bcs-smtp-manager' ); ?></span>
     1470                                                                        <button type="button" class="bcs-smtp-copy-btn"><span class="dashicons dashicons-admin-page" aria-hidden="true"></span><span class="bcs-smtp-copy-label"><?php esc_html_e( 'Copy', 'bcs-smtp-manager' ); ?></span></button>
     1471                                                                </div>
     1472                                                                <pre class="bcs-smtp-mono"><?php echo esc_html( $row['body'] ); ?></pre>
     1473                                                        </div>
     1474                                                <?php endif; ?>
     1475                                                <?php if ( $has_body ) : ?>
     1476                                                        <div class="bcs-smtp-mailview-source bcs-smtp-mailview-plain" data-mode="text" <?php echo $is_html ? 'hidden' : ''; ?>>
     1477                                                                <div class="bcs-smtp-mailview-source-head">
     1478                                                                        <span><?php echo $is_html ? esc_html__( 'Text version', 'bcs-smtp-manager' ) : esc_html__( 'Plain text', 'bcs-smtp-manager' ); ?></span>
     1479                                                                        <button type="button" class="bcs-smtp-copy-btn"><span class="dashicons dashicons-admin-page" aria-hidden="true"></span><span class="bcs-smtp-copy-label"><?php esc_html_e( 'Copy', 'bcs-smtp-manager' ); ?></span></button>
     1480                                                                </div>
     1481                                                                <pre><?php echo esc_html( $is_html ? $this->email_text_version( $row['body'] ) : $row['body'] ); ?></pre>
     1482                                                        </div>
     1483                                                <?php else : ?>
     1484                                                        <div class="bcs-smtp-mailview-empty">
     1485                                                                <span class="dashicons dashicons-hidden" aria-hidden="true"></span>
     1486                                                                <strong><?php esc_html_e( 'The message wasn’t saved', 'bcs-smtp-manager' ); ?></strong>
     1487                                                                <p><?php esc_html_e( 'This email was sent before the log started keeping message content.', 'bcs-smtp-manager' ); ?></p>
     1488                                                        </div>
     1489                                                <?php endif; ?>
     1490                                        </div>
     1491                                </section>
     1492
     1493                                <aside class="bcs-smtp-panel bcs-smtp-mailview-meta" aria-labelledby="bcs-smtp-mailview-meta-title">
     1494                                        <h2 id="bcs-smtp-mailview-meta-title"><?php esc_html_e( 'Delivery', 'bcs-smtp-manager' ); ?></h2>
     1495                                        <div class="bcs-smtp-mailview-verdict bcs-smtp-mailview-verdict--<?php echo esc_attr( $row['status'] ); ?>">
     1496                                                <?php echo $this->status_html( $row['status'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in status_html(). ?>
     1497                                                <p><?php echo esc_html( isset( $explain[ $row['status'] ] ) ? $explain[ $row['status'] ] : '' ); ?></p>
     1498                                        </div>
     1499                                        <dl class="bcs-smtp-mailview-facts">
     1500                                                <div><dt><?php esc_html_e( 'Sent', 'bcs-smtp-manager' ); ?></dt><dd><?php echo esc_html( $sent ); ?></dd></div>
     1501                                                <?php if ( $requested ) : ?>
     1502                                                        <div>
     1503                                                                <dt><?php esc_html_e( 'Requested sender', 'bcs-smtp-manager' ); ?></dt>
     1504                                                                <dd class="bcs-smtp-mono"><?php echo esc_html( $requested ); ?></dd>
     1505                                                                <dd class="bcs-smtp-mailview-note"><?php esc_html_e( 'Your mail server sent it from your connected address instead.', 'bcs-smtp-manager' ); ?></dd>
     1506                                                        </div>
     1507                                                <?php endif; ?>
     1508                                                <?php if ( $has_body ) : ?>
     1509                                                        <div>
     1510                                                                <dt><?php esc_html_e( 'Format', 'bcs-smtp-manager' ); ?></dt>
     1511                                                                <dd>
     1512                                                                        <?php
     1513                                                                        /* translators: 1: HTML or Plain text, 2: size such as "8 KB". */
     1514                                                                        printf( esc_html__( '%1$s · %2$s', 'bcs-smtp-manager' ), $is_html ? esc_html__( 'HTML', 'bcs-smtp-manager' ) : esc_html__( 'Plain text', 'bcs-smtp-manager' ), esc_html( size_format( strlen( $row['body'] ) ) ) );
     1515                                                                        ?>
     1516                                                                </dd>
     1517                                                        </div>
     1518                                                <?php endif; ?>
     1519                                                <?php if ( ! empty( $row['provider'] ) ) : ?>
     1520                                                        <div><dt><?php esc_html_e( 'Provider', 'bcs-smtp-manager' ); ?></dt><dd><?php echo esc_html( $row['provider'] ); ?></dd></div>
     1521                                                <?php endif; ?>
     1522                                                <?php if ( ! empty( $row['message_id'] ) ) : ?>
     1523                                                        <div><dt><?php esc_html_e( 'Tracking ID', 'bcs-smtp-manager' ); ?></dt><dd class="bcs-smtp-mono"><?php echo esc_html( $row['message_id'] ); ?></dd></div>
     1524                                                <?php endif; ?>
     1525                                        </dl>
     1526
     1527                                        <?php if ( $files ) : ?>
     1528                                                <h3><?php esc_html_e( 'Attachments', 'bcs-smtp-manager' ); ?></h3>
     1529                                                <ul class="bcs-smtp-mailview-atts">
     1530                                                        <?php foreach ( $files as $file ) : ?>
     1531                                                                <li><span class="dashicons dashicons-media-default" aria-hidden="true"></span><?php echo esc_html( $file ); ?></li>
     1532                                                        <?php endforeach; ?>
     1533                                                </ul>
     1534                                                <p class="bcs-smtp-hint"><?php esc_html_e( 'File names only. The files themselves aren’t stored.', 'bcs-smtp-manager' ); ?></p>
     1535                                        <?php endif; ?>
     1536
     1537                                        <?php if ( $headers ) : ?>
     1538                                                <details class="bcs-smtp-mailview-headers">
     1539                                                        <summary>
     1540                                                                <?php
     1541                                                                /* translators: %d: number of email headers. */
     1542                                                                printf( esc_html( _n( 'All headers (%d)', 'All headers (%d)', count( $headers ), 'bcs-smtp-manager' ) ), (int) count( $headers ) );
     1543                                                                ?>
     1544                                                        </summary>
     1545                                                        <pre class="bcs-smtp-mono"><?php echo esc_html( implode( "\n", $headers ) ); ?></pre>
     1546                                                </details>
     1547                                        <?php endif; ?>
     1548                                </aside>
     1549                        </div>
     1550                </div>
     1551                <?php
     1552        }
     1553
    8951554        private function render_email_log_row( $row ) {
    896                 $status_map = array(
    897                         'success'     => array( 'class' => 'bcs-smtp-badge-success', 'label' => __( '✓ Sent', 'bcs-smtp-manager' ) ),
    898                         'failed'      => array( 'class' => 'bcs-smtp-badge-error',   'label' => __( '✗ Failed', 'bcs-smtp-manager' ) ),
    899                         'unconfirmed' => array( 'class' => 'bcs-smtp-badge-warning', 'label' => __( '? Unconfirmed', 'bcs-smtp-manager' ) ),
    900                 );
    901                 $st = isset( $status_map[ $row['status'] ] ) ? $status_map[ $row['status'] ] : array( 'class' => '', 'label' => esc_html( $row['status'] ) );
    902 
    903                 $is_failed    = 'failed' === $row['status'];
    904                 $has_body     = ! empty( $row['body'] );
    905                 $resend_count = isset( $row['resend_count'] ) ? (int) $row['resend_count'] : 0;
     1555                $is_failed = 'failed' === $row['status'];
     1556                $subject   = $row['subject'] ? $row['subject'] : __( '(no subject)', 'bcs-smtp-manager' );
     1557                // sent_at is stored in site-local time.
     1558                $time      = mysql2date( get_option( 'time_format', 'g:i a' ), $row['sent_at'] );
    9061559                ?>
    907                 <tr>
    908                         <td style="font-size:11px;white-space:nowrap;font-family:monospace"><?php echo esc_html( wp_date( 'M j, Y g:i A', strtotime( $row['sent_at'] ) ) ); ?></td>
    909                         <td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="<?php echo esc_attr( $row['subject'] ); ?>"><?php echo esc_html( $row['subject'] ?: __( '(no subject)', 'bcs-smtp-manager' ) ); ?></td>
    910                         <td style="font-size:11px;font-family:monospace;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><?php echo esc_html( $row['from_address'] ); ?></td>
    911                         <td style="font-size:11px;font-family:monospace;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><?php echo esc_html( $row['to_address'] ); ?></td>
    912                         <td><span class="bcs-smtp-badge <?php echo esc_attr( $st['class'] ); ?>"><?php echo esc_html( $st['label'] ); ?></span></td>
    913                         <td style="font-size:10px;color:var(--bcs-smtp-text-3);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="<?php echo esc_attr( $row['error_msg'] ); ?>"><?php echo esc_html( $row['error_msg'] ?: '—' ); ?></td>
    914                         <td style="white-space:nowrap">
    915                                 <?php if ( $is_failed ) : ?>
    916                                         <button type="button"
    917                                                 class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm bcs-smtp-resend-btn"
    918                                                 data-log-id="<?php echo esc_attr( $row['id'] ); ?>"
    919                                                 <?php echo $has_body ? '' : 'disabled title="' . esc_attr__( 'No stored content — enable "Store failed email content" in the Config tab before this failure occurred.', 'bcs-smtp-manager' ) . '"'; ?>>
    920                                                 <span class="dashicons dashicons-controls-repeat"></span><?php esc_html_e( 'Resend', 'bcs-smtp-manager' ); ?>
    921                                         </button>
    922                                         <?php if ( $resend_count > 0 ) : ?>
    923                                                 <div style="font-size:10px;color:var(--bcs-smtp-text-3);margin-top:4px">
    924                                                         <?php
    925                                                         printf(
    926                                                                 /* translators: %d: number of times this email has been resent. */
    927                                                                 esc_html( _n( 'Resent %d time', 'Resent %d times', $resend_count, 'bcs-smtp-manager' ) ),
    928                                                                 esc_html( $resend_count )
    929                                                         );
    930                                                         ?>
    931                                                 </div>
    932                                         <?php endif; ?>
    933                                 <?php else : ?>
    934                                         <span style="color:var(--bcs-smtp-text-3)">—</span>
    935                                 <?php endif; ?>
     1560                <tr class="<?php echo $is_failed ? 'is-failed' : ''; ?>">
     1561                        <td class="bcs-smtp-col-date bcs-smtp-mono"><?php echo esc_html( $time ); ?></td>
     1562                        <td class="bcs-smtp-col-subject">
     1563                                <span class="bcs-smtp-cell-main"><?php echo esc_html( $subject ); ?></span>
     1564                                <span class="bcs-smtp-cell-to bcs-smtp-mono" aria-hidden="true"><?php echo esc_html( $row['to_address'] ); ?></span>
     1565                        </td>
     1566                        <td class="bcs-smtp-col-to bcs-smtp-mono"><?php echo esc_html( $row['to_address'] ); ?></td>
     1567                        <td class="bcs-smtp-col-status"><?php echo $this->status_html( $row['status'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in status_html(). ?></td>
     1568                        <td class="bcs-smtp-col-more">
     1569                                <a class="bcs-smtp-view-btn" href="<?php echo esc_url( $this->email_view_url( (int) $row['id'] ) ); ?>">
     1570                                        <?php esc_html_e( 'View', 'bcs-smtp-manager' ); ?>
     1571                                        <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1572                                        <span class="screen-reader-text">
     1573                                                <?php
     1574                                                /* translators: %s: email subject. */
     1575                                                printf( esc_html__( 'email: %s', 'bcs-smtp-manager' ), esc_html( $subject ) );
     1576                                                ?>
     1577                                        </span>
     1578                                </a>
    9361579                        </td>
    9371580                </tr>
    … …  
    9391582        }
    9401583
     1584        /**
     1585         * Overview health banner. Reads the saved state every time, so it can be re-rendered
     1586         * over AJAX after a save, test or health check and never shows a stale verdict.
     1587         */
     1588        public function render_health_banner() {
     1589                $smtp_host    = get_option( 'bcs_smtp_host', '' );
     1590                $is_first_run = '' === trim( (string) $smtp_host );
     1591                $smtp_enabled = (bool) get_option( 'bcs_smtp_enabled', false );
     1592                $smtp_auth    = (bool) get_option( 'bcs_smtp_auth', true );
     1593                $smtp_port    = (int) get_option( 'bcs_smtp_port', 587 );
     1594                $from_email   = get_option( 'bcs_from_email', get_option( 'admin_email' ) );
     1595                $monitor_on   = BCS_SMTP_Monitor::is_active();
     1596                $last_check   = (int) get_option( BCS_SMTP_OPT_LAST_CHECK, 0 );
     1597                $last_success = get_option( BCS_SMTP_OPT_LAST_CHECK_SUCCESS, null );
     1598                $failures     = (int) get_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, 0 );
     1599                $next_check   = $monitor_on ? wp_next_scheduled( 'bcs_smtp_health_check' ) : false;
     1600                $alert_on     = (bool) get_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED, false );
     1601                $alert_chs    = BCS_SMTP_Monitor::get_channels();
     1602                $ch_labels    = array(
     1603                        'slack'       => __( 'Slack', 'bcs-smtp-manager' ),
     1604                        'teams'       => __( 'Microsoft Teams', 'bcs-smtp-manager' ),
     1605                        'google_chat' => __( 'Google Chat', 'bcs-smtp-manager' ),
     1606                        'discord'     => __( 'Discord', 'bcs-smtp-manager' ),
     1607                        'custom'      => __( 'Custom webhook', 'bcs-smtp-manager' ),
     1608                );
     1609
     1610                if ( ! $smtp_enabled || $is_first_run ) {
     1611                        $health = array( 'tone' => 'off', 'title' => __( 'WordPress is not using SMTP yet', 'bcs-smtp-manager' ), 'text' => __( 'Emails go out through PHP mail(), which inboxes often reject. Connect a mail server to fix that.', 'bcs-smtp-manager' ) );
     1612                } elseif ( null === $last_success ) {
     1613                        $health = array( 'tone' => 'warn', 'title' => __( 'Connection not checked yet', 'bcs-smtp-manager' ), 'text' => __( 'Run a health check to confirm WordPress can send through your mail server.', 'bcs-smtp-manager' ) );
     1614                } elseif ( $last_success ) {
     1615                        $health = array( 'tone' => 'ok', 'title' => __( 'Mail is flowing', 'bcs-smtp-manager' ), 'text' => $smtp_auth ? __( 'WordPress signed in to your mail server and it agreed to send on the last check.', 'bcs-smtp-manager' ) : __( 'Your mail server agreed to send without signing in on the last check.', 'bcs-smtp-manager' ) );
     1616                } else {
     1617                        $health = array(
     1618                                'tone'  => 'bad',
     1619                                'title' => __( 'Mail is not getting through', 'bcs-smtp-manager' ),
     1620                                /* translators: %d: number of consecutive failed health checks. */
     1621                                'text'  => sprintf( _n( 'The last health check failed (%d in a row). Check your sign-in details on the Connection tab.', 'The last health checks failed (%d in a row). Check your sign-in details on the Connection tab.', max( 1, $failures ), 'bcs-smtp-manager' ), max( 1, $failures ) ),
     1622                        );
     1623                }
     1624                ?>
     1625                <section class="bcs-smtp-hero bcs-smtp-hero--<?php echo esc_attr( $health['tone'] ); ?>" aria-labelledby="bcs-smtp-hero-title">
     1626                        <div class="bcs-smtp-hero-main">
     1627                                <span class="bcs-smtp-beacon" aria-hidden="true"><span></span></span>
     1628                                <div class="bcs-smtp-hero-copy">
     1629                                        <h2 id="bcs-smtp-hero-title"><?php echo esc_html( $health['title'] ); ?></h2>
     1630                                        <p><?php echo esc_html( $health['text'] ); ?></p>
     1631                                </div>
     1632                                <div class="bcs-smtp-hero-actions">
     1633                                        <?php if ( 'ok' !== $health['tone'] && 'warn' !== $health['tone'] ) : ?>
     1634                                                <button type="button" class="bcs-smtp-btn bcs-smtp-btn-primary" data-goto="connection">
     1635                                                        <?php echo $is_first_run ? esc_html__( 'Set up in a minute', 'bcs-smtp-manager' ) : esc_html__( 'Fix the connection', 'bcs-smtp-manager' ); ?>
     1636                                                </button>
     1637                                        <?php endif; ?>
     1638                                        <?php if ( ! $is_first_run ) : ?>
     1639                                                <button type="button" class="bcs-smtp-btn bcs-smtp-btn-secondary" data-check-now data-msg="#bcs-smtp-overview-msg">
     1640                                                        <?php esc_html_e( 'Run health check', 'bcs-smtp-manager' ); ?>
     1641                                                </button>
     1642                                        <?php endif; ?>
     1643                                </div>
     1644                        </div>
     1645                        <?php if ( ! $is_first_run ) : ?>
     1646                        <dl class="bcs-smtp-hero-facts">
     1647                                <div><dt><?php esc_html_e( 'Server', 'bcs-smtp-manager' ); ?></dt><dd class="bcs-smtp-mono"><?php echo esc_html( $smtp_host . ':' . $smtp_port ); ?></dd></div>
     1648                                <div><dt><?php esc_html_e( 'Sending as', 'bcs-smtp-manager' ); ?></dt><dd class="bcs-smtp-mono"><?php echo esc_html( $from_email ? $from_email : '—' ); ?></dd></div>
     1649                                <div><dt><?php esc_html_e( 'Last check', 'bcs-smtp-manager' ); ?></dt><dd>
     1650                                        <?php
     1651                                        /* translators: %s: human-readable time difference, e.g. "5 mins". */
     1652                                        echo $last_check ? esc_html( sprintf( __( '%s ago', 'bcs-smtp-manager' ), $this->check_minutes( $last_check ) ) ) : esc_html__( 'Not yet', 'bcs-smtp-manager' );
     1653                                        ?>
     1654                                </dd></div>
     1655                                <div><dt><?php esc_html_e( 'Next check', 'bcs-smtp-manager' ); ?></dt><dd>
     1656                                        <?php
     1657                                        if ( ! $monitor_on ) {
     1658                                                esc_html_e( 'Automatic checks off', 'bcs-smtp-manager' );
     1659                                        } elseif ( $next_check ) {
     1660                                                /* translators: %s: human-readable time difference, e.g. "3 mins". */
     1661                                                echo esc_html( $next_check > time() ? sprintf( __( 'in %s', 'bcs-smtp-manager' ), $this->check_minutes( $next_check, true ) ) : __( 'due now', 'bcs-smtp-manager' ) );
     1662                                        } else {
     1663                                                esc_html_e( 'Scheduling…', 'bcs-smtp-manager' );
     1664                                        }
     1665                                        ?>
     1666                                </dd></div>
     1667                                <div><dt><?php esc_html_e( 'Alerts go to', 'bcs-smtp-manager' ); ?></dt><dd><?php echo ( $alert_on && $alert_chs ) ? esc_html( BCS_SMTP_Monitor::channel_list_label( $alert_chs ) ) : esc_html__( 'Nowhere yet', 'bcs-smtp-manager' ); ?></dd></div>
     1668                        </dl>
     1669                        <?php endif; ?>
     1670                </section>
     1671                <?php
     1672        }
     1673
     1674        /**
     1675         * AJAX — return the freshly rendered health banner.
     1676         */
     1677        public function ajax_get_health_banner() {
     1678                check_ajax_referer( 'bcs_smtp_nonce', 'nonce' );
     1679                if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); }
     1680                ob_start();
     1681                $this->render_health_banner();
     1682                wp_send_json_success( array( 'html' => ob_get_clean() ) );
     1683        }
     1684
     1685        /**
     1686         * The site's own addresses, left out of "People reached" so it counts visitors and
     1687         * customers only: admin email and administrators, the sending address, report recipients,
     1688         * Contact Form 7 notification ("Mail") recipients, and anyone on the sending domain.
     1689         *
     1690         * @return array { 0: string[] addresses, 1: string[] domains }
     1691         */
     1692        private function internal_recipients() {
     1693                $emails   = array( get_option( 'admin_email' ), get_option( 'bcs_from_email', '' ) );
     1694                $emails   = array_merge( $emails, explode( ',', (string) get_option( 'bcs_report_emails', '' ) ) );
     1695                $emails   = array_merge( $emails, wp_list_pluck( get_users( array( 'role' => 'administrator', 'fields' => array( 'user_email' ) ) ), 'user_email' ) );
     1696
     1697                if ( class_exists( 'WPCF7_ContactForm' ) ) {
     1698                        foreach ( WPCF7_ContactForm::find() as $form ) {
     1699                                $mail = $form->prop( 'mail' );
     1700                                if ( ! empty( $mail['recipient'] ) ) {
     1701                                        $to = str_replace( '[_site_admin_email]', get_option( 'admin_email' ), $mail['recipient'] );
     1702                                        // Form-field tags like [your-email] are the visitor, not the team.
     1703                                        $emails = array_merge( $emails, explode( ',', preg_replace( '/\[[^\]]*\]/', '', $to ) ) );
     1704                                }
     1705                        }
     1706                }
     1707
     1708                $emails  = array_values( array_unique( array_filter( array_map( 'strtolower', array_map( 'trim', $emails ) ), 'is_email' ) ) );
     1709                $from    = strtolower( (string) get_option( 'bcs_from_email', '' ) );
     1710                $domains = ( is_email( $from ) && false !== strpos( $from, '@' ) ) ? array( substr( strrchr( $from, '@' ), 1 ) ) : array();
     1711
     1712                return apply_filters( 'bcs_smtp_internal_recipients', array( $emails, $domains ) );
     1713        }
     1714
    9411715        /* ═══════════════════════════════════════════════════════════════════
    9421716           PAGE RENDER
    … …  
    9461720                if ( ! current_user_can( 'manage_options' ) ) { return; }
    9471721
    948                 $info          = $this->mailer->detect_smtp_plugin();
     1722                // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only view, capability checked above.
     1723                if ( isset( $_GET['email'] ) ) {
     1724                        $this->render_email_view( absint( $_GET['email'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
     1725                        return;
     1726                }
     1727
    9491728
    9501729                // SMTP settings
     1730                $smtp_host     = get_option( 'bcs_smtp_host', '' );
     1731                $is_first_run  = '' === trim( $smtp_host );
    9511732                $smtp_enabled  = (bool) get_option( 'bcs_smtp_enabled', false );
    9521733                $from_email    = get_option( 'bcs_from_email', get_option( 'admin_email' ) );
    9531734                $from_name     = get_option( 'bcs_from_name',  get_option( 'blogname' ) );
    954                 $smtp_host     = get_option( 'bcs_smtp_host', '' );
    9551735                $smtp_port     = (int) get_option( 'bcs_smtp_port', 587 );
    9561736                $smtp_enc      = get_option( 'bcs_smtp_encryption', 'tls' );
    … …  
    9581738                $smtp_auth     = (bool) get_option( 'bcs_smtp_auth', true );
    9591739                $smtp_user     = get_option( 'bcs_smtp_username', '' );
     1740                $has_pw        = ! empty( get_option( 'bcs_smtp_password', '' ) );
     1741                $pw_unreadable = $has_pw && $this->mailer->is_password_unreadable();
     1742                $presets       = $this->provider_presets();
     1743                $provider      = $this->detect_provider( $smtp_host );
     1744                $preset        = $provider ? $presets[ $provider ] : $presets['other'];
    9601745
    9611746                // Monitor settings
    962                 $monitor_on    = (bool) get_option( BCS_SMTP_OPT_MONITOR_ENABLED, false );
    963                 $check_interval = (int) get_option( BCS_SMTP_OPT_CHECK_INTERVAL, 5 );
    964                 $threshold     = (int) get_option( BCS_SMTP_OPT_FAILURE_THRESHOLD, 2 );
    965                 $last_check    = (int) get_option( BCS_SMTP_OPT_LAST_CHECK, 0 );
    966                 $last_success  = get_option( BCS_SMTP_OPT_LAST_CHECK_SUCCESS, null );
     1747                $monitor_on     = BCS_SMTP_Monitor::is_active();
     1748                $last_check     = (int) get_option( BCS_SMTP_OPT_LAST_CHECK, 0 );
     1749                $last_success   = get_option( BCS_SMTP_OPT_LAST_CHECK_SUCCESS, null );
     1750                $failures       = (int) get_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, 0 );
     1751                $next_check     = $monitor_on ? wp_next_scheduled( 'bcs_smtp_health_check' ) : false;
    9671752
    9681753                // Alert settings
    969                 $alert_enabled = (bool) get_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED, false );
    970                 $alert_channel = get_option( BCS_SMTP_OPT_ALERT_CHANNEL, 'none' );
    971 
    972                 // Summary for stat row
    973                 $smtp_status   = is_null( $last_success ) ? 'unknown' : ( $last_success ? 'ok' : 'fail' );
    974 
    975                 // Report defaults (30-day)
    976                 $summary_30d   = $this->db->get_email_summary( '30days' );
     1754                $alert_enabled  = (bool) get_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED, false );
     1755                $alert_chs      = BCS_SMTP_Monitor::get_channels();
     1756                $channels       = array(
     1757                        'slack'       => array( 'label' => __( 'Slack', 'bcs-smtp-manager' ),          'short' => 'S' ),
     1758                        'teams'       => array( 'label' => __( 'Microsoft Teams', 'bcs-smtp-manager' ), 'short' => 'T' ),
     1759                        'google_chat' => array( 'label' => __( 'Google Chat', 'bcs-smtp-manager' ),     'short' => 'GC' ),
     1760                        'discord'     => array( 'label' => __( 'Discord', 'bcs-smtp-manager' ),         'short' => 'D' ),
     1761                        'custom'      => array( 'label' => __( 'Custom webhook', 'bcs-smtp-manager' ),  'short' => '{ }' ),
     1762                );
     1763
     1764                // Overview data
     1765                $summary_30d  = $this->db->get_email_summary( '30days' );
     1766                $rate         = $summary_30d['total'] > 0 ? round( ( $summary_30d['success'] / $summary_30d['total'] ) * 100, 1 ) : null;
     1767                $chart        = $this->db->get_email_chart_data( '30days' );
    9771768
    9781769                // Scheduled report settings
    9791770                $report_schedule_enabled = (bool) get_option( 'bcs_report_schedule_enabled', false );
    9801771                $report_emails           = get_option( 'bcs_report_emails', '' );
    981                 $report_frequency        = get_option( 'bcs_report_frequency', 'weekly' );
    982 
    983                 // Resend settings
    984                 $store_failed_body     = (bool) get_option( BCS_SMTP_OPT_STORE_FAILED_BODY, false );
    985                 $failed_body_retention = (int) get_option( BCS_SMTP_OPT_FAILED_BODY_RETENTION_DAYS, 7 );
     1772
     1773
     1774                $setup_items = array(
     1775                        array( 'done' => $smtp_enabled && ! $is_first_run, 'label' => __( 'Connect your mail server', 'bcs-smtp-manager' ), 'goto' => 'connection' ),
     1776                        array( 'done' => $alert_enabled && $alert_chs, 'label' => __( 'Choose where failure alerts go', 'bcs-smtp-manager' ), 'goto' => 'alerts' ),
     1777                        array( 'done' => $report_schedule_enabled,          'label' => __( 'Get a delivery report by email', 'bcs-smtp-manager' ), 'goto' => 'reports' ),
     1778                );
     1779                $setup_left = count( array_filter( $setup_items, function ( $i ) { return ! $i['done']; } ) );
     1780
     1781                $tabs = array(
     1782                        'overview'   => __( 'Overview', 'bcs-smtp-manager' ),
     1783                        'connection' => __( 'Connection', 'bcs-smtp-manager' ),
     1784                        'alerts'     => __( 'Alerts', 'bcs-smtp-manager' ),
     1785                        'reports'    => __( 'Reports', 'bcs-smtp-manager' ),
     1786                        'email-log'  => __( 'Email log', 'bcs-smtp-manager' ),
     1787                );
     1788                $date_ranges = array(
     1789                        'today'      => __( 'Today', 'bcs-smtp-manager' ),
     1790                        'yesterday'  => __( 'Yesterday', 'bcs-smtp-manager' ),
     1791                        '7days'      => __( 'Last 7 days', 'bcs-smtp-manager' ),
     1792                        '30days'     => __( 'Last 30 days', 'bcs-smtp-manager' ),
     1793                        'this_week'  => __( 'This week', 'bcs-smtp-manager' ),
     1794                        'last_week'  => __( 'Last week', 'bcs-smtp-manager' ),
     1795                        'this_month' => __( 'This month', 'bcs-smtp-manager' ),
     1796                        '3months'    => __( 'Last 3 months', 'bcs-smtp-manager' ),
     1797                );
     1798                $time_fmt = get_option( 'time_format', 'g:i a' );
    9861799                ?>
    987                 <div class="bcs-smtp-wrap wrap">
    988 
    989                         <!-- ═══ HEADER ═══════════════════════════════════════════════════ -->
    990                         <div class="bcs-smtp-header">
    991                                 <div class="bcs-smtp-header-left">
    992                                         <div class="bcs-smtp-header-icon">
    993                                                 <span class="dashicons dashicons-email-alt"></span>
    994                                         </div>
     1800                <div class="bcs-smtp-wrap wrap" data-first-run="<?php echo $is_first_run ? '1' : '0'; ?>">
     1801
     1802                        <header class="bcs-smtp-header">
     1803                                <div class="bcs-smtp-brand">
     1804                                        <span class="bcs-smtp-brand-mark" aria-hidden="true">
     1805                                                <svg viewBox="0 0 24 24" focusable="false"><rect x="3.5" y="6" width="17" height="12.5" rx="2.5"/><path d="m4.5 7.5 7.5 5.75 7.5-5.75"/></svg>
     1806                                        </span>
    9951807                                        <div>
    996                                                 <h1><?php esc_html_e( 'ByteCoreStack - SMTP & Health Monitor', 'bcs-smtp-manager' ); ?></h1>
    997                                                 <p class="bcs-smtp-header-sub"><?php esc_html_e( 'WordPress email monitoring & alerting by ByteCore Stack', 'bcs-smtp-manager' ); ?></p>
     1808                                                <h1><?php esc_html_e( 'SMTP & Health Monitor', 'bcs-smtp-manager' ); ?></h1>
     1809                                                <p class="bcs-smtp-header-sub">
     1810                                                        <?php echo esc_html( wp_parse_url( home_url(), PHP_URL_HOST ) ); ?>
     1811                                                        <span aria-hidden="true">·</span>
     1812                                                        <?php
     1813                                                        /* translators: %s: plugin version number. */
     1814                                                        printf( esc_html__( 'ByteCoreStack · v%s', 'bcs-smtp-manager' ), esc_html( BCS_SMTP_VERSION ) );
     1815                                                        ?>
     1816                                                </p>
    9981817                                        </div>
    9991818                                </div>
    1000                                 <div class="bcs-smtp-header-right">
    1001                                         <span class="bcs-smtp-header-badge">v<?php echo esc_html( BCS_SMTP_VERSION ); ?></span>
     1819                                <div class="bcs-smtp-header-actions">
     1820                                        <button type="button" class="bcs-smtp-btn bcs-smtp-btn-primary" data-goto="connection" data-focus="#bcs-smtp-test-to">
     1821                                                <?php esc_html_e( 'Send test email', 'bcs-smtp-manager' ); ?>
     1822                                        </button>
    10021823                                </div>
    1003                         </div>
    1004 
    1005                         <!-- ═══ STAT ROW ══════════════════════════════════════════════════ -->
    1006                         <div class="bcs-smtp-stat-row">
    1007                                 <div class="bcs-smtp-stat">
    1008                                         <div class="bcs-smtp-stat-icon <?php echo 'ok' === $smtp_status ? 'good' : ( 'fail' === $smtp_status ? 'bad' : '' ); ?>">
    1009                                                 <span class="dashicons dashicons-yes-alt"></span>
    1010                                         </div>
    1011                                         <div class="bcs-smtp-stat-body">
    1012                                                 <div class="bcs-smtp-stat-value <?php echo 'ok' === $smtp_status ? 'good' : ( 'fail' === $smtp_status ? 'bad' : '' ); ?>">
    1013                                                         <?php echo 'ok' === $smtp_status ? '✓' : ( 'fail' === $smtp_status ? '✗' : '—' ); ?>
    1014                                                 </div>
    1015                                                 <div class="bcs-smtp-stat-label"><?php esc_html_e( 'SMTP Status', 'bcs-smtp-manager' ); ?></div>
     1824                        </header>
     1825
     1826                        <nav class="bcs-smtp-tabs" role="tablist" aria-label="<?php esc_attr_e( 'SMTP Manager sections', 'bcs-smtp-manager' ); ?>">
     1827                                <span class="bcs-smtp-tab-pill" aria-hidden="true"></span>
     1828                                <?php
     1829                                $tab_icons = array(
     1830                                        'overview'   => 'dashboard',
     1831                                        'connection' => 'admin-plugins',
     1832                                        'alerts'     => 'bell',
     1833                                        'reports'    => 'chart-bar',
     1834                                        'email-log'  => 'email-alt',
     1835                                );
     1836                                foreach ( $tabs as $key => $label ) :
     1837                                        ?>
     1838                                        <button type="button" role="tab" class="bcs-smtp-tab" id="bcs-smtp-tab-<?php echo esc_attr( $key ); ?>" data-panel="<?php echo esc_attr( $key ); ?>" aria-controls="bcs-smtp-panel-<?php echo esc_attr( $key ); ?>" aria-selected="false" tabindex="-1">
     1839                                                <span class="dashicons dashicons-<?php echo esc_attr( $tab_icons[ $key ] ); ?>" aria-hidden="true"></span>
     1840                                                <?php echo esc_html( $label ); ?>
     1841                                                <?php if ( 'connection' === $key ) : ?>
     1842                                                        <span class="bcs-smtp-tab-dot" id="bcs-smtp-conn-dot" aria-hidden="true"></span>
     1843                                                        <span class="screen-reader-text" id="bcs-smtp-conn-dot-label"></span>
     1844                                                <?php endif; ?>
     1845                                                <?php if ( 'email-log' === $key && $summary_30d['failed'] > 0 ) : ?>
     1846                                                        <span class="bcs-smtp-tab-count" title="<?php esc_attr_e( 'Failed in the last 30 days', 'bcs-smtp-manager' ); ?>"><?php echo esc_html( number_format_i18n( $summary_30d['failed'] ) ); ?></span>
     1847                                                <?php endif; ?>
     1848                                        </button>
     1849                                <?php endforeach; ?>
     1850                        </nav>
     1851
     1852                        <div id="bcs-smtp-toast" class="bcs-smtp-toast" role="status" aria-live="polite"></div>
     1853
     1854                        <!-- ═══ PANEL: Overview ═══════════════════════════════════════════ -->
     1855                        <section id="bcs-smtp-panel-overview" class="bcs-smtp-tab-panel" role="tabpanel" aria-labelledby="bcs-smtp-tab-overview">
     1856
     1857                                <div id="bcs-smtp-health-banner"><?php $this->render_health_banner(); ?></div>
     1858                                <div id="bcs-smtp-overview-msg" class="bcs-smtp-msg" role="status" aria-live="polite"></div>
     1859
     1860
     1861                                <div class="bcs-smtp-overview-grid">
     1862                                        <section class="bcs-smtp-panel bcs-smtp-activity" aria-labelledby="bcs-smtp-activity-title">
     1863                                                <div class="bcs-smtp-panel-head">
     1864                                                        <h2 id="bcs-smtp-activity-title"><?php esc_html_e( 'Email activity', 'bcs-smtp-manager' ); ?></h2>
     1865                                                        <div class="bcs-smtp-segmented bcs-smtp-segmented--sm" role="radiogroup" aria-label="<?php esc_attr_e( 'Time range', 'bcs-smtp-manager' ); ?>">
     1866                                                                <?php foreach ( array( '7days' => __( '7 days', 'bcs-smtp-manager' ), '30days' => __( '30 days', 'bcs-smtp-manager' ), '3months' => __( '90 days', 'bcs-smtp-manager' ) ) as $val => $label ) : ?>
     1867                                                                        <label><input type="radio" name="bcs_smtp_ov_range" class="bcs-smtp-ov-range" autocomplete="off" value="<?php echo esc_attr( $val ); ?>" <?php checked( '30days', $val ); ?>><span><?php echo esc_html( $label ); ?></span></label>
     1868                                                                <?php endforeach; ?>
     1869                                                        </div>
     1870                                                </div>
     1871                                                <dl class="bcs-smtp-figures" id="bcs-smtp-ov-figures">
     1872                                                        <div><dt><span class="bcs-smtp-key bcs-smtp-key--sent" aria-hidden="true"></span><?php esc_html_e( 'Sent', 'bcs-smtp-manager' ); ?></dt><dd data-fig="success"><?php echo esc_html( number_format_i18n( $summary_30d['success'] ) ); ?></dd></div>
     1873                                                        <div><dt><span class="bcs-smtp-key bcs-smtp-key--bad" aria-hidden="true"></span><?php esc_html_e( 'Failed', 'bcs-smtp-manager' ); ?></dt><dd data-fig="failed" class="<?php echo $summary_30d['failed'] > 0 ? 'is-bad' : ''; ?>"><?php echo esc_html( number_format_i18n( $summary_30d['failed'] ) ); ?></dd></div>
     1874                                                        <div><dt><span class="bcs-smtp-key bcs-smtp-key--warn" aria-hidden="true"></span><?php esc_html_e( 'Unconfirmed', 'bcs-smtp-manager' ); ?></dt><dd data-fig="unconfirmed"><?php echo esc_html( number_format_i18n( $summary_30d['unconfirmed'] ) ); ?></dd></div>
     1875                                                        <div><dt><?php esc_html_e( 'Delivered', 'bcs-smtp-manager' ); ?></dt><dd data-fig="rate"><?php echo null === $rate ? '—' : esc_html( number_format_i18n( $rate, 1 ) ) . '<small>%</small>'; ?></dd></div>
     1876                                                </dl>
     1877                                                <div class="bcs-smtp-linechart" id="bcs-smtp-ov-chart" tabindex="0"
     1878                                                        role="img" aria-label="<?php esc_attr_e( 'Emails per day: sent, failed and unconfirmed. Use the left and right arrow keys to read each day.', 'bcs-smtp-manager' ); ?>"
     1879                                                        data-chart="<?php echo esc_attr( wp_json_encode( $chart ) ); ?>">
     1880                                                        <div class="bcs-smtp-linechart-tip" aria-live="polite" hidden></div>
     1881                                                </div>
     1882                                                <p class="bcs-smtp-chart-empty" id="bcs-smtp-ov-empty" <?php echo $summary_30d['total'] > 0 ? 'hidden' : ''; ?>><?php esc_html_e( 'No email was sent in this period.', 'bcs-smtp-manager' ); ?></p>
     1883                                                <?php if ( $summary_30d['failed'] > 0 ) : ?>
     1884                                                        <p class="bcs-smtp-activity-foot">
     1885                                                                <?php
     1886                                                                /* translators: %s: number of failed emails in the last 30 days. */
     1887                                                                printf( esc_html( _n( '%s email failed in the last 30 days.', '%s emails failed in the last 30 days.', $summary_30d['failed'], 'bcs-smtp-manager' ) ), esc_html( number_format_i18n( $summary_30d['failed'] ) ) );
     1888                                                                ?>
     1889                                                                <button type="button" class="bcs-smtp-link" data-goto="email-log" data-log-status="failed"><?php esc_html_e( 'Review failed emails', 'bcs-smtp-manager' ); ?></button>
     1890                                                        </p>
     1891                                                <?php endif; ?>
     1892                                        </section>
     1893
     1894                                        <div class="bcs-smtp-side">
     1895                                        <?php if ( $setup_left > 0 ) : ?>
     1896                                        <section class="bcs-smtp-panel bcs-smtp-setup-card" aria-labelledby="bcs-smtp-setup-title">
     1897                                                <div class="bcs-smtp-panel-head">
     1898                                                        <h2 id="bcs-smtp-setup-title"><?php esc_html_e( 'Finish setting up', 'bcs-smtp-manager' ); ?></h2>
     1899                                                        <span class="bcs-smtp-panel-sub">
     1900                                                                <?php
     1901                                                                /* translators: 1: completed setup steps, 2: total setup steps. */
     1902                                                                printf( esc_html__( '%1$d of %2$d done', 'bcs-smtp-manager' ), count( $setup_items ) - $setup_left, count( $setup_items ) );
     1903                                                                ?>
     1904                                                        </span>
     1905                                                </div>
     1906                                                <div class="bcs-smtp-progress" aria-hidden="true">
     1907                                                        <?php foreach ( $setup_items as $item ) : ?>
     1908                                                                <span class="<?php echo $item['done'] ? 'is-done' : ''; ?>"></span>
     1909                                                        <?php endforeach; ?>
     1910                                                </div>
     1911                                                <ul class="bcs-smtp-checklist">
     1912                                                        <?php foreach ( $setup_items as $item ) : ?>
     1913                                                                <li class="<?php echo $item['done'] ? 'is-done' : ''; ?>">
     1914                                                                        <span class="bcs-smtp-check-dot dashicons <?php echo $item['done'] ? 'dashicons-yes' : ''; ?>" aria-hidden="true"></span>
     1915                                                                        <?php if ( $item['done'] ) : ?>
     1916                                                                                <span><?php echo esc_html( $item['label'] ); ?></span>
     1917                                                                                <span class="screen-reader-text"><?php esc_html_e( '(done)', 'bcs-smtp-manager' ); ?></span>
     1918                                                                        <?php else : ?>
     1919                                                                                <button type="button" class="bcs-smtp-link bcs-smtp-link--plain" data-goto="<?php echo esc_attr( $item['goto'] ); ?>"><?php echo esc_html( $item['label'] ); ?><span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span></button>
     1920                                                                        <?php endif; ?>
     1921                                                                </li>
     1922                                                        <?php endforeach; ?>
     1923                                                </ul>
     1924                                        </section>
     1925                                        <?php endif; ?>
     1926                                        <section class="bcs-smtp-panel bcs-smtp-quick" aria-labelledby="bcs-smtp-quick-title">
     1927                                                <div class="bcs-smtp-panel-head">
     1928                                                        <h2 id="bcs-smtp-quick-title"><?php esc_html_e( 'Quick actions', 'bcs-smtp-manager' ); ?></h2>
     1929                                                </div>
     1930                                                <ul class="bcs-smtp-quick-list">
     1931                                                        <li><button type="button" data-goto="connection" data-focus="#bcs-smtp-test-to">
     1932                                                                <span class="bcs-smtp-quick-icon dashicons dashicons-email-alt" aria-hidden="true"></span>
     1933                                                                <span class="bcs-smtp-quick-text"><strong><?php esc_html_e( 'Send a test email', 'bcs-smtp-manager' ); ?></strong></span>
     1934                                                                <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1935                                                        </button></li>
     1936                                                        <li><button type="button" data-goto="email-log" data-log-status="failed">
     1937                                                                <span class="bcs-smtp-quick-icon bcs-smtp-quick-icon--bad dashicons dashicons-warning" aria-hidden="true"></span>
     1938                                                                <span class="bcs-smtp-quick-text"><strong><?php esc_html_e( 'Review failed emails', 'bcs-smtp-manager' ); ?></strong></span>
     1939                                                                <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1940                                                        </button></li>
     1941                                                        <li><button type="button" data-goto="alerts">
     1942                                                                <span class="bcs-smtp-quick-icon dashicons dashicons-bell" aria-hidden="true"></span>
     1943                                                                <span class="bcs-smtp-quick-text"><strong><?php esc_html_e( 'Failure alerts', 'bcs-smtp-manager' ); ?></strong></span>
     1944                                                                <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1945                                                        </button></li>
     1946                                                        <li><button type="button" data-export-log>
     1947                                                                <span class="bcs-smtp-quick-icon dashicons dashicons-download" aria-hidden="true"></span>
     1948                                                                <span class="bcs-smtp-quick-text"><strong><?php esc_html_e( 'Export email log', 'bcs-smtp-manager' ); ?></strong></span>
     1949                                                                <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1950                                                        </button></li>
     1951                                                        <?php if ( 0 === $setup_left ) : // The setup card is gone, so this card takes the whole column. ?>
     1952                                                        <li><button type="button" data-check-now data-msg="#bcs-smtp-overview-msg">
     1953                                                                <span class="bcs-smtp-quick-icon bcs-smtp-quick-icon--ok dashicons dashicons-heart" aria-hidden="true"></span>
     1954                                                                <span class="bcs-smtp-quick-text"><strong><?php esc_html_e( 'Run health check', 'bcs-smtp-manager' ); ?></strong></span>
     1955                                                                <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1956                                                        </button></li>
     1957                                                        <li><button type="button" data-goto="reports">
     1958                                                                <span class="bcs-smtp-quick-icon dashicons dashicons-chart-area" aria-hidden="true"></span>
     1959                                                                <span class="bcs-smtp-quick-text"><strong><?php esc_html_e( 'Delivery report', 'bcs-smtp-manager' ); ?></strong></span>
     1960                                                                <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1961                                                        </button></li>
     1962                                                        <li><button type="button" data-goto="connection">
     1963                                                                <span class="bcs-smtp-quick-icon dashicons dashicons-admin-network" aria-hidden="true"></span>
     1964                                                                <span class="bcs-smtp-quick-text"><strong><?php esc_html_e( 'Connection settings', 'bcs-smtp-manager' ); ?></strong></span>
     1965                                                                <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1966                                                        </button></li>
     1967                                                        <?php endif; ?>
     1968                                                </ul>
     1969                                        </section>
    10161970                                        </div>
    10171971                                </div>
    1018                                 <div class="bcs-smtp-stat">
    1019                                         <div class="bcs-smtp-stat-icon <?php echo $smtp_enabled ? 'good' : ''; ?>">
    1020                                                 <span class="dashicons dashicons-admin-plugins"></span>
    1021                                         </div>
    1022                                         <div class="bcs-smtp-stat-body">
    1023                                                 <div class="bcs-smtp-stat-value <?php echo $smtp_enabled ? 'good' : ''; ?>">
    1024                                                         <?php echo $smtp_enabled ? esc_html__( 'On', 'bcs-smtp-manager' ) : esc_html__( 'Off', 'bcs-smtp-manager' ); ?>
    1025                                                 </div>
    1026                                                 <div class="bcs-smtp-stat-label"><?php esc_html_e( 'SMTP Active', 'bcs-smtp-manager' ); ?></div>
    1027                                         </div>
    1028                                 </div>
    1029                                 <div class="bcs-smtp-stat">
    1030                                         <div class="bcs-smtp-stat-icon">
    1031                                                 <span class="dashicons dashicons-email-alt"></span>
    1032                                         </div>
    1033                                         <div class="bcs-smtp-stat-body">
    1034                                                 <div class="bcs-smtp-stat-value" style="font-size:18px"><?php echo esc_html( number_format( $summary_30d['total'] ) ); ?></div>
    1035                                                 <div class="bcs-smtp-stat-label"><?php esc_html_e( 'Emails (30d)', 'bcs-smtp-manager' ); ?></div>
    1036                                         </div>
    1037                                 </div>
    1038                                 <div class="bcs-smtp-stat">
    1039                                         <div class="bcs-smtp-stat-icon good">
    1040                                                 <span class="dashicons dashicons-yes"></span>
    1041                                         </div>
    1042                                         <div class="bcs-smtp-stat-body">
    1043                                                 <div class="bcs-smtp-stat-value good" style="font-size:22px"><?php echo esc_html( number_format( $summary_30d['success'] ) ); ?></div>
    1044                                                 <div class="bcs-smtp-stat-label"><?php esc_html_e( 'Sent (30d)', 'bcs-smtp-manager' ); ?></div>
    1045                                         </div>
    1046                                 </div>
    1047                                 <div class="bcs-smtp-stat">
    1048                                         <div class="bcs-smtp-stat-icon <?php echo $summary_30d['failed'] > 0 ? 'bad' : ( $last_check ? 'good' : '' ); ?>">
    1049                                                 <span class="dashicons dashicons-warning"></span>
    1050                                         </div>
    1051                                         <div class="bcs-smtp-stat-body">
    1052                                                 <div class="bcs-smtp-stat-value <?php echo $summary_30d['failed'] > 0 ? 'bad' : ( $last_check ? 'good' : '' ); ?>" style="font-size:22px">
    1053                                                         <?php echo esc_html( number_format( $summary_30d['failed'] ) ); ?>
    1054                                                 </div>
    1055                                                 <div class="bcs-smtp-stat-label"><?php esc_html_e( 'Failed (30d)', 'bcs-smtp-manager' ); ?></div>
    1056                                         </div>
    1057                                 </div>
    1058                         </div>
    1059 
    1060                         <!-- ═══ TABS ══════════════════════════════════════════════════════ -->
    1061                         <div class="bcs-smtp-tabs">
    1062                                 <button type="button" class="bcs-smtp-tab active" data-panel="settings">
    1063                                         <span class="dashicons dashicons-admin-settings"></span><?php esc_html_e( 'Settings', 'bcs-smtp-manager' ); ?>
    1064                                 </button>
    1065                                 <button type="button" class="bcs-smtp-tab" data-panel="email-log">
    1066                                         <span class="dashicons dashicons-list-view"></span><?php esc_html_e( 'Email Log', 'bcs-smtp-manager' ); ?>
    1067                                 </button>
    1068                                 <button type="button" class="bcs-smtp-tab" data-panel="email-report">
    1069                                         <span class="dashicons dashicons-chart-bar"></span><?php esc_html_e( 'Email Report', 'bcs-smtp-manager' ); ?>
    1070                                 </button>
    1071                                 <button type="button" class="bcs-smtp-tab" data-panel="test">
    1072                                         <span class="dashicons dashicons-email"></span><?php esc_html_e( 'Send Test Email', 'bcs-smtp-manager' ); ?>
    1073                                 </button>
    1074                                 <button type="button" class="bcs-smtp-tab" data-panel="alerts">
    1075                                         <span class="dashicons dashicons-bell"></span><?php esc_html_e( 'Alerts', 'bcs-smtp-manager' ); ?>
    1076                                 </button>
    1077                                 <button type="button" class="bcs-smtp-tab" data-panel="config">
    1078                                         <span class="dashicons dashicons-admin-generic"></span><?php esc_html_e( 'Config', 'bcs-smtp-manager' ); ?>
    1079                                 </button>
    1080                         </div>
    1081 
    1082                         <!-- ═══════════════════════════════════════════════════════════════
    1083                              PANEL: Settings (SMTP connection)
    1084                              ═══════════════════════════════════════════════════════════════ -->
    1085                         <div id="bcs-smtp-panel-settings" class="bcs-smtp-tab-panel active">
    1086                                 <div class="bcs-smtp-grid">
    1087                                 <div>
    1088                                         <div class="bcs-smtp-card">
    1089                                                 <div class="bcs-smtp-card-header">
    1090                                                         <p class="bcs-smtp-card-title"><span class="dashicons dashicons-email-alt"></span><?php esc_html_e( 'SMTP Connection', 'bcs-smtp-manager' ); ?></p>
    1091                                                         <p class="bcs-smtp-card-desc"><?php esc_html_e( 'Configure this plugin to send all WordPress emails via your SMTP server. Leave disabled to use PHP mail or another SMTP plugin.', 'bcs-smtp-manager' ); ?></p>
    1092                                                 </div>
    1093                                                 <div class="bcs-smtp-card-body">
    1094                                                         <!-- Enable toggle -->
    1095                                                         <div class="bcs-smtp-toggle-row" style="margin-bottom:0">
    1096                                                                 <div class="bcs-smtp-toggle-row-info">
    1097                                                                         <div class="bcs-smtp-toggle-row-label"><?php esc_html_e( 'Enable SMTP', 'bcs-smtp-manager' ); ?></div>
    1098                                                                         <div class="bcs-smtp-toggle-row-desc"><?php esc_html_e( 'Override WordPress default email with your SMTP settings below', 'bcs-smtp-manager' ); ?></div>
     1972                        </section>
     1973
     1974                        <!-- ═══ PANEL: Connection (guided setup) ══════════════════════════ -->
     1975                        <section id="bcs-smtp-panel-connection" class="bcs-smtp-tab-panel" role="tabpanel" aria-labelledby="bcs-smtp-tab-connection">
     1976                                <div class="bcs-smtp-split">
     1977                                        <div class="bcs-smtp-panel bcs-smtp-setup">
     1978
     1979                                                <div class="bcs-smtp-setup-head">
     1980                                                        <span class="bcs-smtp-setup-badge dashicons dashicons-email-alt" aria-hidden="true"></span>
     1981                                                        <div>
     1982                                                                <h2><label for="bcs-smtp-smtp-enabled"><?php esc_html_e( 'SMTP connection', 'bcs-smtp-manager' ); ?></label></h2>
     1983                                                                <p><?php esc_html_e( 'Send all WordPress email through your mail server.', 'bcs-smtp-manager' ); ?></p>
     1984                                                        </div>
     1985                                                        <span class="bcs-smtp-toggle">
     1986                                                                <input type="checkbox" id="bcs-smtp-smtp-enabled" role="switch" aria-controls="bcs-smtp-setup-body" <?php checked( $smtp_enabled || $is_first_run ); ?>>
     1987                                                                <span class="bcs-smtp-toggle-slider" aria-hidden="true"></span>
     1988                                                        </span>
     1989                                                </div>
     1990
     1991                                                <?php $smtp_on = $smtp_enabled || $is_first_run; ?>
     1992                                                <div class="bcs-smtp-alerts-off bcs-smtp-smtp-off" id="bcs-smtp-smtp-off" <?php echo $smtp_on ? 'hidden' : ''; ?>>
     1993                                                        <span class="dashicons dashicons-email-alt" aria-hidden="true"></span>
     1994                                                        <p><?php esc_html_e( 'SMTP is off, so WordPress sends email with PHP mail(), which often lands in spam. Turn it on to send through your mail server. Your settings are kept.', 'bcs-smtp-manager' ); ?></p>
     1995                                                </div>
     1996
     1997                                                <div class="bcs-smtp-setup-body" id="bcs-smtp-setup-body" <?php echo $smtp_on ? '' : 'hidden'; ?>>
     1998                                                        <div class="bcs-smtp-field" id="bcs-smtp-step-provider">
     1999                                                                <span class="bcs-smtp-field-label" id="bcs-smtp-provider-label"><?php esc_html_e( 'Email provider', 'bcs-smtp-manager' ); ?></span>
     2000                                                                <?php
     2001                                                                // Once a provider is chosen, show just that choice with a Change link; the full list opens on demand.
     2002                                                                $picked = ( $provider && isset( $presets[ $provider ] ) ) ? $presets[ $provider ] : null;
     2003                                                                ?>
     2004                                                                <div class="bcs-smtp-provider-picked" id="bcs-smtp-provider-picked" <?php echo $picked ? '' : 'hidden'; ?>>
     2005                                                                        <span class="bcs-smtp-provider-mark" id="bcs-smtp-picked-mark" aria-hidden="true"></span>
     2006                                                                        <span class="bcs-smtp-picked-name" id="bcs-smtp-picked-name"><?php echo $picked ? esc_html( $picked['label'] ) : ''; ?></span>
     2007                                                                        <button type="button" class="bcs-smtp-link" id="bcs-smtp-provider-change" aria-expanded="false" aria-controls="bcs-smtp-provider-chips"><?php esc_html_e( 'Change', 'bcs-smtp-manager' ); ?></button>
    10992008                                                                </div>
    1100                                                                 <label class="bcs-smtp-toggle">
    1101                                                                         <input type="checkbox" id="bcs-smtp-smtp-enabled" <?php checked( $smtp_enabled ); ?>>
    1102                                                                         <span class="bcs-smtp-toggle-slider"></span>
    1103                                                                 </label>
     2009                                                                <div class="bcs-smtp-provider-chips" id="bcs-smtp-provider-chips" role="radiogroup" aria-labelledby="bcs-smtp-provider-label" <?php echo $picked ? 'hidden' : ''; ?>>
     2010                                                                        <?php
     2011                                                                        // Each provider's own colour tints its badge, so the list scans by colour as well as name.
     2012                                                                        $provider_tones = array(
     2013                                                                                'gmail'     => '#ea4335',
     2014                                                                                'microsoft' => '#2b88d8',
     2015                                                                                'sendgrid'  => '#1a82e2',
     2016                                                                                'ses'       => '#ff9900',
     2017                                                                                'mailgun'   => '#f06b66',
     2018                                                                                'brevo'     => '#0fae7b',
     2019                                                                                'zoho'      => '#f0b400',
     2020                                                                                'yahoo'     => '#8f4bff',
     2021                                                                        );
     2022                                                                        $provider_short = array(
     2023                                                                                'gmail'     => __( 'Gmail', 'bcs-smtp-manager' ),
     2024                                                                                'microsoft' => __( 'Microsoft 365', 'bcs-smtp-manager' ),
     2025                                                                                'zoho'      => __( 'Zoho', 'bcs-smtp-manager' ),
     2026                                                                                'yahoo'     => __( 'Yahoo', 'bcs-smtp-manager' ),
     2027                                                                                'other'     => __( 'Other', 'bcs-smtp-manager' ),
     2028                                                                        );
     2029                                                                        foreach ( $presets as $key => $p ) :
     2030                                                                                ?>
     2031                                                                                <label class="bcs-smtp-provider" title="<?php echo esc_attr( $p['label'] ); ?>" style="--tone:<?php echo esc_attr( isset( $provider_tones[ $key ] ) ? $provider_tones[ $key ] : '#8a97b0' ); ?>">
     2032                                                                                        <input type="radio" name="bcs_smtp_provider" class="bcs-smtp-provider-radio" value="<?php echo esc_attr( $key ); ?>"
     2033                                                                                                data-host="<?php echo esc_attr( $p['host'] ); ?>"
     2034                                                                                                data-port="<?php echo esc_attr( $p['port'] ); ?>"
     2035                                                                                                data-enc="<?php echo esc_attr( $p['enc'] ); ?>"
     2036                                                                                                data-user="<?php echo esc_attr( $p['user'] ); ?>"
     2037                                                                                                data-user-hint="<?php echo esc_attr( $p['user_hint'] ); ?>"
     2038                                                                                                data-pass-label="<?php echo esc_attr( $p['pass_label'] ); ?>"
     2039                                                                                                data-note="<?php echo esc_attr( $p['note'] ); ?>"
     2040                                                                                                data-link="<?php echo esc_url( $p['link'] ); ?>"
     2041                                                                                                data-link-label="<?php echo esc_attr( $p['link_label'] ); ?>"
     2042                                                                                                <?php checked( $provider, $key ); ?>>
     2043                                                                                        <span class="bcs-smtp-provider-mark" aria-hidden="true"><?php echo $p['short'] ? esc_html( $p['short'] ) : '<span class="dashicons dashicons-admin-generic"></span>'; ?></span>
     2044                                                                                        <span class="bcs-smtp-provider-name"><?php echo esc_html( isset( $provider_short[ $key ] ) ? $provider_short[ $key ] : $p['label'] ); ?></span>
     2045                                                                                </label>
     2046                                                                        <?php endforeach; ?>
     2047                                                                </div>
     2048                                                                <p class="bcs-smtp-provider-note" id="bcs-smtp-provider-note" <?php echo $provider ? '' : 'hidden'; ?>>
     2049                                                                        <span class="dashicons dashicons-info-outline" aria-hidden="true"></span>
     2050                                                                        <span>
     2051                                                                                <span id="bcs-smtp-provider-note-text"><?php echo esc_html( $preset['note'] ); ?></span>
     2052                                                                                <a id="bcs-smtp-provider-note-link" href="<?php echo esc_url( $preset['link'] ); ?>" target="_blank" rel="noopener noreferrer" <?php echo $preset['link'] ? '' : 'hidden'; ?>><span id="bcs-smtp-provider-note-link-text"><?php echo esc_html( $preset['link_label'] ); ?></span><span class="screen-reader-text"> <?php esc_html_e( '(opens in a new tab)', 'bcs-smtp-manager' ); ?></span></a>
     2053                                                                        </span>
     2054                                                                </p>
    11042055                                                        </div>
    11052056
    1106                                                         <!-- SMTP settings accordion -->
    1107                                                         <div id="bcs-smtp-smtp-accordion" class="bcs-smtp-accordion <?php echo $smtp_enabled ? 'open' : ''; ?>" style="margin-top:12px">
    1108                                                                 <button type="button" class="bcs-smtp-accordion-head">
    1109                                                                         <div class="bcs-smtp-accordion-head-left">
    1110                                                                                 <div class="bcs-smtp-accordion-head-icon" style="background:rgba(99,102,241,.15);color:var(--bcs-smtp-indigo)">
    1111                                                                                         <span class="dashicons dashicons-admin-network"></span>
     2057                                                        <div class="bcs-smtp-field-row">
     2058                                                                <div class="bcs-smtp-field">
     2059                                                                        <label class="bcs-smtp-field-label" for="bcs-smtp-from-email"><?php esc_html_e( 'From address', 'bcs-smtp-manager' ); ?></label>
     2060                                                                        <input type="email" id="bcs-smtp-from-email" class="bcs-smtp-input" value="<?php echo esc_attr( $from_email ); ?>" spellcheck="false">
     2061                                                                </div>
     2062                                                                <div class="bcs-smtp-field">
     2063                                                                        <label class="bcs-smtp-field-label" for="bcs-smtp-from-name"><?php esc_html_e( 'From name', 'bcs-smtp-manager' ); ?></label>
     2064                                                                        <input type="text" id="bcs-smtp-from-name" class="bcs-smtp-input" value="<?php echo esc_attr( $from_name ); ?>">
     2065                                                                </div>
     2066                                                        </div>
     2067
     2068                                                        <label class="bcs-smtp-inline-switch bcs-smtp-auth-switch" for="bcs-smtp-smtp-auth">
     2069                                                                <span class="bcs-smtp-toggle bcs-smtp-toggle--sm">
     2070                                                                        <input type="checkbox" id="bcs-smtp-smtp-auth" role="switch" aria-controls="bcs-smtp-auth-fields" <?php checked( $smtp_auth ); ?>>
     2071                                                                        <span class="bcs-smtp-toggle-slider" aria-hidden="true"></span>
     2072                                                                </span>
     2073                                                                <span><?php esc_html_e( 'My server needs a username and password', 'bcs-smtp-manager' ); ?></span>
     2074                                                        </label>
     2075
     2076                                                        <div id="bcs-smtp-auth-fields" <?php echo $smtp_auth ? '' : 'hidden'; ?>>
     2077                                                                <div class="bcs-smtp-field-row">
     2078                                                                        <div class="bcs-smtp-field">
     2079                                                                                <label class="bcs-smtp-field-label" for="bcs-smtp-smtp-username"><?php esc_html_e( 'Username', 'bcs-smtp-manager' ); ?></label>
     2080                                                                                <input type="text" id="bcs-smtp-smtp-username" class="bcs-smtp-input" value="<?php echo esc_attr( $smtp_user ); ?>" placeholder="<?php echo esc_attr( $preset['user_hint'] ); ?>" autocomplete="off" spellcheck="false" aria-describedby="bcs-smtp-user-hint">
     2081                                                                                <span class="screen-reader-text" id="bcs-smtp-user-hint"><?php echo esc_html( $preset['user_hint'] ); ?></span>
     2082                                                                        </div>
     2083                                                                        <div class="bcs-smtp-field">
     2084                                                                                <label class="bcs-smtp-field-label" for="bcs-smtp-smtp-password" id="bcs-smtp-pass-label"><?php echo esc_html( $preset['pass_label'] ); ?></label>
     2085                                                                                <div id="bcs-smtp-pw-display" class="bcs-smtp-pw-saved" <?php echo $has_pw ? '' : 'hidden'; ?>>
     2086                                                                                        <span class="bcs-smtp-pw-dots" aria-hidden="true">••••••••••••</span>
     2087                                                                                        <span class="screen-reader-text"><?php esc_html_e( 'A password is saved.', 'bcs-smtp-manager' ); ?></span>
     2088                                                                                        <button type="button" id="bcs-smtp-change-pw-btn" class="bcs-smtp-link"><?php esc_html_e( 'Change', 'bcs-smtp-manager' ); ?></button>
    11122089                                                                                </div>
    1113                                                                                 <div>
    1114                                                                                         <div class="bcs-smtp-accordion-head-title"><?php esc_html_e( 'SMTP Server Settings', 'bcs-smtp-manager' ); ?></div>
    1115                                                                                         <div class="bcs-smtp-accordion-head-desc"><?php esc_html_e( 'Host, port, encryption & authentication', 'bcs-smtp-manager' ); ?></div>
     2090                                                                                <div id="bcs-smtp-pw-input" <?php echo $has_pw ? 'hidden' : ''; ?>>
     2091                                                                                        <div class="bcs-smtp-pw-wrap">
     2092                                                                                                <input type="password" id="bcs-smtp-smtp-password" class="bcs-smtp-input" value="" autocomplete="new-password" spellcheck="false" aria-describedby="bcs-smtp-pw-status">
     2093                                                                                                <button type="button" id="bcs-smtp-toggle-pw-btn" class="bcs-smtp-pw-eye" aria-label="<?php esc_attr_e( 'Show password', 'bcs-smtp-manager' ); ?>" aria-controls="bcs-smtp-smtp-password">
     2094                                                                                                        <span class="dashicons dashicons-visibility" aria-hidden="true"></span>
     2095                                                                                                </button>
     2096                                                                                        </div>
     2097                                                                                        <button type="button" id="bcs-smtp-cancel-pw-btn" class="bcs-smtp-link bcs-smtp-hint" hidden><?php esc_html_e( 'Keep the saved password', 'bcs-smtp-manager' ); ?></button>
     2098                                                                                </div>
     2099                                                                                <?php if ( $pw_unreadable ) : ?>
     2100                                                                                        <span class="bcs-smtp-hint is-bad" id="bcs-smtp-pw-status"><?php esc_html_e( 'The saved password can no longer be read because your site’s security keys changed. Click Change and enter it again.', 'bcs-smtp-manager' ); ?></span>
     2101                                                                                <?php else : ?>
     2102                                                                                        <span class="bcs-smtp-hint <?php echo $has_pw ? 'is-good' : ''; ?>" id="bcs-smtp-pw-status"></span>
     2103                                                                                <?php endif; ?>
     2104                                                                        </div>
     2105                                                                </div>
     2106                                                        </div>
     2107
     2108                                                        <div class="bcs-smtp-alert bcs-smtp-alert-warning bcs-smtp-auth-warning" id="bcs-smtp-auth-warning" role="alert" <?php echo ( ! $smtp_auth && $provider && 'other' !== $provider ) ? '' : 'hidden'; ?>>
     2109                                                                <span class="dashicons dashicons-warning" aria-hidden="true"></span>
     2110                                                                <div class="bcs-smtp-alert-content">
     2111                                                                        <strong id="bcs-smtp-auth-warning-title">
     2112                                                                                <?php
     2113                                                                                /* translators: %s: email provider name, e.g. Microsoft 365. */
     2114                                                                                printf( esc_html__( '%s needs a username and password.', 'bcs-smtp-manager' ), esc_html( isset( $provider_short[ $provider ] ) ? $provider_short[ $provider ] : $preset['label'] ) );
     2115                                                                                ?>
     2116                                                                        </strong>
     2117                                                                        <?php esc_html_e( 'Turn on “My server needs a username and password” above and save, or every email will be rejected.', 'bcs-smtp-manager' ); ?>
     2118                                                                </div>
     2119                                                        </div>
     2120
     2121                                                        <div class="bcs-smtp-disclosure <?php echo ( 'other' === $provider ) ? 'is-open' : ''; ?>" id="bcs-smtp-server-details">
     2122                                                                <button type="button" class="bcs-smtp-disclosure-head" aria-expanded="<?php echo ( 'other' === $provider ) ? 'true' : 'false'; ?>" aria-controls="bcs-smtp-server-details-body">
     2123                                                                        <span>
     2124                                                                                <strong><?php esc_html_e( 'Server details', 'bcs-smtp-manager' ); ?></strong>
     2125                                                                                <span class="bcs-smtp-mono" id="bcs-smtp-server-summary"><?php echo $smtp_host ? esc_html( $smtp_host . ' · ' . $smtp_port . ' · ' . strtoupper( $smtp_enc ) ) : esc_html__( 'Filled in when you pick a provider', 'bcs-smtp-manager' ); ?></span>
     2126                                                                        </span>
     2127                                                                        <span class="dashicons dashicons-arrow-down-alt2" aria-hidden="true"></span>
     2128                                                                </button>
     2129                                                                <div class="bcs-smtp-disclosure-body" id="bcs-smtp-server-details-body">
     2130                                                                        <div class="bcs-smtp-field-row">
     2131                                                                                <div class="bcs-smtp-field bcs-smtp-field--grow">
     2132                                                                                        <label class="bcs-smtp-field-label" for="bcs-smtp-smtp-host"><?php esc_html_e( 'SMTP server', 'bcs-smtp-manager' ); ?></label>
     2133                                                                                        <input type="text" id="bcs-smtp-smtp-host" class="bcs-smtp-input bcs-smtp-mono" value="<?php echo esc_attr( $smtp_host ); ?>" placeholder="smtp.example.com" spellcheck="false" autocomplete="off">
     2134                                                                                </div>
     2135                                                                                <div class="bcs-smtp-field bcs-smtp-field--port">
     2136                                                                                        <label class="bcs-smtp-field-label" for="bcs-smtp-smtp-port"><?php esc_html_e( 'Port', 'bcs-smtp-manager' ); ?></label>
     2137                                                                                        <input type="number" id="bcs-smtp-smtp-port" class="bcs-smtp-input bcs-smtp-mono" value="<?php echo esc_attr( $smtp_port ); ?>" min="1" max="65535" inputmode="numeric">
    11162138                                                                                </div>
    11172139                                                                        </div>
    1118                                                                         <span class="dashicons dashicons-arrow-down-alt2 bcs-smtp-accordion-chevron"></span>
    1119                                                                 </button>
    1120                                                                 <div class="bcs-smtp-accordion-body">
    1121                                                                         <div class="bcs-smtp-field-row">
    1122                                                                                 <div class="bcs-smtp-field">
    1123                                                                                         <label class="bcs-smtp-field-label" for="bcs-smtp-from-email"><?php esc_html_e( 'Force From Email', 'bcs-smtp-manager' ); ?></label>
    1124                                                                                         <input type="email" id="bcs-smtp-from-email" class="bcs-smtp-input" value="<?php echo esc_attr( $from_email ); ?>" placeholder="no-reply@yoursite.com">
    1125                                                                                 </div>
    1126                                                                                 <div class="bcs-smtp-field">
    1127                                                                                         <label class="bcs-smtp-field-label" for="bcs-smtp-from-name"><?php esc_html_e( 'Force From Name', 'bcs-smtp-manager' ); ?></label>
    1128                                                                                         <input type="text" id="bcs-smtp-from-name" class="bcs-smtp-input" value="<?php echo esc_attr( $from_name ); ?>" placeholder="Your Site Name">
    1129                                                                                 </div>
    1130                                                                         </div>
    1131 
    1132                                                                         <div class="bcs-smtp-field-row">
    1133                                                                                 <div class="bcs-smtp-field">
    1134                                                                                         <label class="bcs-smtp-field-label" for="bcs-smtp-smtp-host"><?php esc_html_e( 'SMTP Host', 'bcs-smtp-manager' ); ?></label>
    1135                                                                                         <input type="text" id="bcs-smtp-smtp-host" class="bcs-smtp-input" value="<?php echo esc_attr( $smtp_host ); ?>" placeholder="smtp.gmail.com">
    1136                                                                                 </div>
    1137                                                                                 <div class="bcs-smtp-field">
    1138                                                                                         <label class="bcs-smtp-field-label" for="bcs-smtp-smtp-port"><?php esc_html_e( 'SMTP Port', 'bcs-smtp-manager' ); ?></label>
    1139                                                                                         <input type="number" id="bcs-smtp-smtp-port" class="bcs-smtp-input" value="<?php echo esc_attr( $smtp_port ); ?>" min="1" max="65535">
    1140                                                                                         <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block"><?php esc_html_e( 'SSL = 465 • TLS = 587 • None = 25', 'bcs-smtp-manager' ); ?></small>
    1141                                                                                 </div>
    1142                                                                         </div>
    1143 
    1144                                                                         <!-- Encryption -->
    11452140                                                                        <div class="bcs-smtp-field">
    1146                                                                                 <label class="bcs-smtp-field-label"><?php esc_html_e( 'Encryption', 'bcs-smtp-manager' ); ?></label>
    1147                                                                                 <div class="bcs-smtp-check-list" style="flex-direction:row;gap:8px">
    1148                                                                                         <?php foreach ( array( 'none' => __( 'None', 'bcs-smtp-manager' ), 'ssl' => __( 'SSL', 'bcs-smtp-manager' ), 'tls' => __( 'TLS', 'bcs-smtp-manager' ) ) as $val => $label ) : ?>
    1149                                                                                         <label class="bcs-smtp-check-item" style="flex-direction:row;align-items:center;gap:6px;padding:8px 14px;min-width:70px">
    1150                                                                                                 <input type="radio" name="bcs_smtp_encryption" class="bcs-smtp-enc-radio" value="<?php echo esc_attr( $val ); ?>" <?php checked( $smtp_enc, $val ); ?> style="width:20px;height:20px;flex-shrink:0">
    1151                                                                                                 <span class="bcs-smtp-check-label-title"><?php echo esc_html( $label ); ?></span>
     2141                                                                                <span class="bcs-smtp-field-label" id="bcs-smtp-enc-label"><?php esc_html_e( 'Encryption', 'bcs-smtp-manager' ); ?></span>
     2142                                                                                <div class="bcs-smtp-segmented" role="radiogroup" aria-labelledby="bcs-smtp-enc-label">
     2143                                                                                        <?php foreach ( array( 'tls' => __( 'TLS · 587', 'bcs-smtp-manager' ), 'ssl' => __( 'SSL · 465', 'bcs-smtp-manager' ), 'none' => __( 'None · 25', 'bcs-smtp-manager' ) ) as $val => $label ) : ?>
     2144                                                                                        <label>
     2145                                                                                                <input type="radio" name="bcs_smtp_encryption" class="bcs-smtp-enc-radio" value="<?php echo esc_attr( $val ); ?>" <?php checked( $smtp_enc, $val ); ?>>
     2146                                                                                                <span><?php echo esc_html( $label ); ?></span>
    11522147                                                                                        </label>
    11532148                                                                                        <?php endforeach; ?>
    11542149                                                                                </div>
    11552150                                                                        </div>
    1156 
    1157                                                                         <!-- Auto TLS toggle -->
    1158                                                                         <div class="bcs-smtp-toggle-row" style="padding:10px 0">
    1159                                                                                 <div class="bcs-smtp-toggle-row-info">
    1160                                                                                         <div class="bcs-smtp-toggle-row-label" style="font-size:13px"><?php esc_html_e( 'Auto TLS', 'bcs-smtp-manager' ); ?></div>
    1161                                                                                         <div class="bcs-smtp-toggle-row-desc"><?php esc_html_e( 'Automatically upgrade to TLS if supported by the server', 'bcs-smtp-manager' ); ?></div>
    1162                                                                                 </div>
    1163                                                                                 <label class="bcs-smtp-toggle">
    1164                                                                                         <input type="checkbox" id="bcs-smtp-auto-tls" <?php checked( $auto_tls ); ?>>
    1165                                                                                         <span class="bcs-smtp-toggle-slider"></span>
    1166                                                                                 </label>
    1167                                                                         </div>
    1168 
    1169                                                                         <!-- Authentication toggle -->
    1170                                                                         <div class="bcs-smtp-toggle-row" style="padding:10px 0">
    1171                                                                                 <div class="bcs-smtp-toggle-row-info">
    1172                                                                                         <div class="bcs-smtp-toggle-row-label" style="font-size:13px"><?php esc_html_e( 'Authentication', 'bcs-smtp-manager' ); ?></div>
    1173                                                                                         <div class="bcs-smtp-toggle-row-desc"><?php esc_html_e( 'Authenticate with the SMTP server using username & password', 'bcs-smtp-manager' ); ?></div>
    1174                                                                                 </div>
    1175                                                                                 <label class="bcs-smtp-toggle">
    1176                                                                                         <input type="checkbox" id="bcs-smtp-smtp-auth" <?php checked( $smtp_auth ); ?>>
    1177                                                                                         <span class="bcs-smtp-toggle-slider"></span>
    1178                                                                                 </label>
    1179                                                                         </div>
    1180 
    1181                                                                         <!-- Username + Password (shown when auth enabled) -->
    1182                                                                         <div id="bcs-smtp-auth-fields" style="<?php echo $smtp_auth ? '' : 'display:none'; ?>">
    1183                                                                         <div class="bcs-smtp-field-row">
    1184                                                                                 <div class="bcs-smtp-field" style="flex:0 0 50%;max-width:50%">
    1185                                                                                         <label class="bcs-smtp-field-label" for="bcs-smtp-smtp-username"><?php esc_html_e( 'Username', 'bcs-smtp-manager' ); ?></label>
    1186                                                                                         <input type="text" id="bcs-smtp-smtp-username" class="bcs-smtp-input" value="<?php echo esc_attr( $smtp_user ); ?>" placeholder="your@email.com" autocomplete="off">
    1187                                                                                 </div>
    1188                                                                         </div>
    1189                                                                         <?php $has_pw = ! empty( get_option( 'bcs_smtp_password', '' ) ); ?>
    1190                                                                         <div class="bcs-smtp-field-row">
    1191                                                                                 <div class="bcs-smtp-field" style="flex:0 0 50%;max-width:50%">
    1192                                                                                         <label class="bcs-smtp-field-label" for="bcs-smtp-smtp-password"><?php esc_html_e( 'Password / App Password', 'bcs-smtp-manager' ); ?></label>
    1193 
    1194                                                                                         <!-- Masked display — shown when a password is already saved -->
    1195                                                                                         <div id="bcs-smtp-pw-display" style="<?php echo $has_pw ? 'display:flex' : 'display:none'; ?>;gap:8px;align-items:center">
    1196                                                                                                 <input type="password" class="bcs-smtp-input" value="placeholder_pw"
    1197                                                                                                    disabled style="flex:1" aria-label="<?php esc_attr_e( 'Saved password (masked)', 'bcs-smtp-manager' ); ?>">
    1198                                                                                                 <button type="button" id="bcs-smtp-change-pw-btn"
    1199                                                                                                                 class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm"
    1200                                                                                                                 style="flex-shrink:0;white-space:nowrap">
    1201                                                                                                         <span class="dashicons dashicons-edit"></span> <?php esc_html_e( 'Change', 'bcs-smtp-manager' ); ?>
    1202                                                                                                 </button>
    1203                                                                                         </div>
    1204 
    1205                                                                                         <!-- Actual input — visible when no password yet, or after Change is clicked -->
    1206                                                                                         <div id="bcs-smtp-pw-input" style="<?php echo $has_pw ? 'display:none' : ''; ?>">
    1207                                                                                                 <div class="bcs-smtp-pw-wrap">
    1208                                                                                                         <input type="password" id="bcs-smtp-smtp-password" class="bcs-smtp-input"
    1209                                                                                                                    value="" placeholder="<?php esc_attr_e( 'Enter password or app password…', 'bcs-smtp-manager' ); ?>"
    1210                                                                                                                    autocomplete="new-password" style="width:100%">
    1211                                                                                                         <button type="button" id="bcs-smtp-toggle-pw-btn" class="bcs-smtp-pw-eye" aria-label="<?php esc_attr_e( 'Show password', 'bcs-smtp-manager' ); ?>" tabindex="-1">
    1212                                                                                                                 <span class="dashicons dashicons-visibility"></span>
    1213                                                                                                         </button>
    1214                                                                                                 </div>
    1215                                                                                                 <!-- Cancel link — shown after Change is clicked -->
    1216                                                                                                 <div id="bcs-smtp-pw-cancel-row" style="display:none;margin-top:6px">
    1217                                                                                                         <a href="#" id="bcs-smtp-cancel-pw-btn" style="font-size:12px;color:var(--bcs-smtp-text-3);text-decoration:none">
    1218                                                                                                                 &#8592; <?php esc_html_e( 'Cancel — keep existing password', 'bcs-smtp-manager' ); ?>
    1219                                                                                                         </a>
    1220                                                                                                 </div>
    1221                                                                                         </div>
    1222 
    1223                                                                                         <small style="font-size:11px;margin-top:6px;display:block">
    1224                                                                                                 <span id="bcs-smtp-pw-status" style="color:<?php echo $has_pw ? 'var(--bcs-smtp-emerald)' : 'var(--bcs-smtp-text-3)'; ?>">
    1225                                                                                                         <?php echo $has_pw ? esc_html__( '✓ Password saved — leave blank to keep it.', 'bcs-smtp-manager' ) : esc_html__( 'No password saved yet.', 'bcs-smtp-manager' ); ?>
    1226                                                                                                 </span><br>
    1227                                                                                                 <span style="color:var(--bcs-smtp-text-3)">
    1228                                                                                                         <?php esc_html_e( 'Gmail / Workspace: use a 16-char', 'bcs-smtp-manager' ); ?> <strong><?php esc_html_e( 'App Password', 'bcs-smtp-manager' ); ?></strong>
    1229                                                                                                         (<a href="https://myaccount.google.com/apppasswords" target="_blank" rel="noopener"><?php esc_html_e( 'generate ↗', 'bcs-smtp-manager' ); ?></a>).
    1230                                                                                                         <?php esc_html_e( 'Outlook / Microsoft 365: use your account or app password.', 'bcs-smtp-manager' ); ?>
    1231                                                                                                         <?php esc_html_e( 'Spaces are stripped automatically.', 'bcs-smtp-manager' ); ?>
    1232                                                                                                 </span>
    1233                                                                                         </small>
    1234                                                                                 </div>
    1235                                                                         </div>
    1236                                                                         </div>
     2151                                                                        <label class="bcs-smtp-inline-switch" for="bcs-smtp-auto-tls">
     2152                                                                                <span class="bcs-smtp-toggle bcs-smtp-toggle--sm">
     2153                                                                                        <input type="checkbox" id="bcs-smtp-auto-tls" role="switch" <?php checked( $auto_tls ); ?>>
     2154                                                                                        <span class="bcs-smtp-toggle-slider" aria-hidden="true"></span>
     2155                                                                                </span>
     2156                                                                                <span><?php esc_html_e( 'Upgrade to TLS when the server offers it', 'bcs-smtp-manager' ); ?></span>
     2157                                                                        </label>
    12372158                                                                </div>
    12382159                                                        </div>
    1239 
    1240                                                         <div style="margin-top:16px">
    1241                                                                 <button type="button" id="bcs-smtp-save-smtp-btn" class="bcs-smtp-btn bcs-smtp-btn-primary">
    1242                                                                         <span class="dashicons dashicons-saved"></span><?php esc_html_e( 'Save SMTP Settings', 'bcs-smtp-manager' ); ?>
    1243                                                                 </button>
    1244                                                                 <div id="bcs-smtp-smtp-msg" role="status" aria-live="polite" style="display:none;margin-top:10px;width:100%"></div>
     2160                                                </div>
     2161
     2162                                                <div class="bcs-smtp-setup-foot">
     2163                                                        <button type="button" id="bcs-smtp-save-smtp-btn" class="bcs-smtp-btn bcs-smtp-btn-primary">
     2164                                                                <?php esc_html_e( 'Save and test connection', 'bcs-smtp-manager' ); ?>
     2165                                                        </button>
     2166                                                        <span class="bcs-smtp-unsaved" id="bcs-smtp-smtp-unsaved" hidden><?php esc_html_e( 'Unsaved changes', 'bcs-smtp-manager' ); ?></span>
     2167                                                        <div id="bcs-smtp-smtp-msg" class="bcs-smtp-msg" role="status" aria-live="polite"></div>
     2168                                                </div>
     2169                                        </div>
     2170
     2171                                        <aside class="bcs-smtp-panel bcs-smtp-test" aria-labelledby="bcs-smtp-test-title">
     2172                                                <div class="bcs-smtp-test-head">
     2173                                                        <span class="bcs-smtp-test-badge dashicons dashicons-yes-alt" aria-hidden="true"></span>
     2174                                                        <div>
     2175                                                                <h2 id="bcs-smtp-test-title"><?php esc_html_e( 'Check it works', 'bcs-smtp-manager' ); ?></h2>
     2176                                                                <p><?php esc_html_e( 'Send a real email through your connection. The result shows here and in the email log.', 'bcs-smtp-manager' ); ?></p>
    12452177                                                        </div>
    12462178                                                </div>
     2179                                                <div class="bcs-smtp-field">
     2180                                                        <label class="bcs-smtp-field-label" for="bcs-smtp-test-to"><?php esc_html_e( 'Send a test email to', 'bcs-smtp-manager' ); ?></label>
     2181                                                        <input type="email" id="bcs-smtp-test-to" class="bcs-smtp-input" value="<?php echo esc_attr( get_option( 'admin_email' ) ); ?>" spellcheck="false">
     2182                                                </div>
     2183                                                <div class="bcs-smtp-actions">
     2184                                                        <button type="button" id="bcs-smtp-send-test-btn" class="bcs-smtp-btn bcs-smtp-btn-primary"><?php esc_html_e( 'Send test email', 'bcs-smtp-manager' ); ?></button>
     2185                                                        <button type="button" id="bcs-smtp-check-now-btn" class="bcs-smtp-btn bcs-smtp-btn-secondary" data-check-now data-msg="#bcs-smtp-test-msg"><?php esc_html_e( 'Run health check', 'bcs-smtp-manager' ); ?></button>
     2186                                                </div>
     2187                                                <div id="bcs-smtp-test-msg" class="bcs-smtp-msg" role="status" aria-live="polite"></div>
     2188                                        </aside>
     2189                                </div>
     2190                        </section>
     2191
     2192                        <!-- ═══ PANEL: Email log ══════════════════════════════════════════ -->
     2193                        <section id="bcs-smtp-panel-email-log" class="bcs-smtp-tab-panel" role="tabpanel" aria-labelledby="bcs-smtp-tab-email-log">
     2194                                <?php
     2195                                $grace_until = (int) get_option( 'bcs_smtp_prune_after', 0 );
     2196                                $older_count = $grace_until > time() ? $this->db->count_email_log_older_than( BCS_SMTP_LOG_RETENTION_DAYS ) : 0;
     2197                                if ( $older_count > 0 ) :
     2198                                        $export_all = add_query_arg(
     2199                                                array(
     2200                                                        'bcs_smtp_action' => 'export_csv',
     2201                                                        'nonce'           => wp_create_nonce( 'bcs_smtp_export' ),
     2202                                                        'date_range'      => 'all',
     2203                                                        'status'          => 'all',
     2204                                                ),
     2205                                                admin_url( 'options-general.php?page=bcs-smtp-manager' )
     2206                                        );
     2207                                        ?>
     2208                                        <div class="bcs-smtp-alert bcs-smtp-alert-warning bcs-smtp-log-retention" role="status">
     2209                                                <span class="dashicons dashicons-info-outline" aria-hidden="true"></span>
     2210                                                <div class="bcs-smtp-alert-content">
     2211                                                        <?php
     2212                                                        printf(
     2213                                                                /* translators: 1: number of log entries, 2: date. */
     2214                                                                esc_html( _n( 'The email log now keeps the last 90 days. %1$s older entry will be removed on %2$s.', 'The email log now keeps the last 90 days. %1$s older entries will be removed on %2$s.', $older_count, 'bcs-smtp-manager' ) ),
     2215                                                                esc_html( number_format_i18n( $older_count ) ),
     2216                                                                esc_html( wp_date( get_option( 'date_format', 'F j, Y' ), $grace_until ) )
     2217                                                        );
     2218                                                        ?>
     2219                                                        <a href="<?php echo esc_url( $export_all ); ?>"><?php esc_html_e( 'Export all entries (CSV)', 'bcs-smtp-manager' ); ?></a>
     2220                                                </div>
    12472221                                        </div>
    1248                                 </div>
    1249 
    1250                                 <div>
    1251                                         <div class="bcs-smtp-card">
    1252                                                 <div class="bcs-smtp-card-header">
    1253                                                         <p class="bcs-smtp-card-title"><span class="dashicons dashicons-info-outline"></span><?php esc_html_e( 'Common SMTP Providers', 'bcs-smtp-manager' ); ?></p>
    1254                                                         <p class="bcs-smtp-card-desc"><?php esc_html_e( 'Reference host, port & encryption settings for popular providers.', 'bcs-smtp-manager' ); ?></p>
    1255                                                 </div>
    1256                                                 <div class="bcs-smtp-card-body">
    1257                                                         <div class="bcs-smtp-field-group" style="gap:10px">
    1258                                                                 <div class="bcs-smtp-server-row">
    1259                                                                         <span class="bcs-smtp-server-key"><?php esc_html_e( 'Gmail / Google Workspace', 'bcs-smtp-manager' ); ?></span>
    1260                                                                         <span class="bcs-smtp-server-val bcs-smtp-mono">smtp.gmail.com &bull; 587 &bull; TLS</span>
    1261                                                                 </div>
    1262                                                                 <div class="bcs-smtp-server-row">
    1263                                                                         <span class="bcs-smtp-server-key"><?php esc_html_e( 'Outlook / Microsoft 365', 'bcs-smtp-manager' ); ?></span>
    1264                                                                         <span class="bcs-smtp-server-val bcs-smtp-mono">smtp.office365.com &bull; 587 &bull; TLS</span>
    1265                                                                 </div>
    1266                                                                 <div class="bcs-smtp-server-row">
    1267                                                                         <span class="bcs-smtp-server-key"><?php esc_html_e( 'Yahoo Mail', 'bcs-smtp-manager' ); ?></span>
    1268                                                                         <span class="bcs-smtp-server-val bcs-smtp-mono">smtp.mail.yahoo.com &bull; 587 &bull; TLS</span>
    1269                                                                 </div>
    1270                                                                 <div class="bcs-smtp-server-row">
    1271                                                                         <span class="bcs-smtp-server-key"><?php esc_html_e( 'SendGrid', 'bcs-smtp-manager' ); ?></span>
    1272                                                                         <span class="bcs-smtp-server-val bcs-smtp-mono">smtp.sendgrid.net &bull; 587 &bull; TLS</span>
    1273                                                                 </div>
    1274                                                                 <div class="bcs-smtp-server-row">
    1275                                                                         <span class="bcs-smtp-server-key"><?php esc_html_e( 'Amazon SES', 'bcs-smtp-manager' ); ?></span>
    1276                                                                         <span class="bcs-smtp-server-val bcs-smtp-mono">email-smtp.[region].amazonaws.com &bull; 587 &bull; TLS</span>
    1277                                                                 </div>
    1278                                                                 <div class="bcs-smtp-server-row">
    1279                                                                         <span class="bcs-smtp-server-key"><?php esc_html_e( 'Mailgun', 'bcs-smtp-manager' ); ?></span>
    1280                                                                         <span class="bcs-smtp-server-val bcs-smtp-mono">smtp.mailgun.org &bull; 587 &bull; TLS</span>
    1281                                                                 </div>
    1282                                                         </div>
    1283                                                         <div class="bcs-smtp-alert bcs-smtp-alert-info" style="margin-top:14px;margin-bottom:0">
    1284                                                                 <span class="dashicons dashicons-info-outline"></span>
    1285                                                                 <div class="bcs-smtp-alert-content" style="font-size:11.5px"><?php esc_html_e( 'Gmail and Microsoft 365 require an app password when 2-factor authentication is enabled — your normal account password won’t work.', 'bcs-smtp-manager' ); ?></div>
    1286                                                         </div>
    1287                                                 </div>
    1288                                         </div>
    1289                                 </div>
    1290 
    1291                                 </div>
    1292 
    1293                         </div>
    1294 
    1295                         <!-- ═══════════════════════════════════════════════════════════════
    1296                              PANEL: Email Log
    1297                              ═══════════════════════════════════════════════════════════════ -->
    1298                         <div id="bcs-smtp-panel-email-log" class="bcs-smtp-tab-panel">
    1299                                 <div class="bcs-smtp-card">
    1300                                         <div class="bcs-smtp-card-header">
    1301                                                 <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:14px">
    1302                                                         <p class="bcs-smtp-card-title" style="margin:0"><span class="dashicons dashicons-list-view"></span><?php esc_html_e( 'Email Log', 'bcs-smtp-manager' ); ?></p>
    1303                                                         <div style="display:flex;gap:8px;flex-wrap:wrap">
    1304                                                                 <a id="bcs-smtp-export-csv-btn" href="#" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm">
    1305                                                                         <span class="dashicons dashicons-download"></span><?php esc_html_e( 'Export CSV', 'bcs-smtp-manager' ); ?>
    1306                                                                 </a>
    1307                                                         </div>
    1308                                                 </div>
    1309                                                 <p class="bcs-smtp-card-desc"><?php esc_html_e( 'Every email sent via', 'bcs-smtp-manager' ); ?> <code>wp_mail()</code> <?php esc_html_e( 'is logged here with its delivery status.', 'bcs-smtp-manager' ); ?></p>
    1310                                         </div>
    1311                                         <div class="bcs-smtp-card-body">
    1312                                                 <!-- Filters -->
    1313                                         <div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:14px;align-items:center">
    1314                                                 <select id="bcs-smtp-log-date-range" class="bcs-smtp-input" style="width:25%;min-width:130px">
    1315                                                         <option value="today"><?php esc_html_e( 'Today', 'bcs-smtp-manager' ); ?></option>
    1316                                                         <option value="yesterday"><?php esc_html_e( 'Yesterday', 'bcs-smtp-manager' ); ?></option>
    1317                                                         <option value="7days"><?php esc_html_e( 'Last 7 days', 'bcs-smtp-manager' ); ?></option>
    1318                                                         <option value="30days" selected><?php esc_html_e( 'Last 30 days', 'bcs-smtp-manager' ); ?></option>
    1319                                                         <option value="this_week"><?php esc_html_e( 'This week', 'bcs-smtp-manager' ); ?></option>
    1320                                                         <option value="last_week"><?php esc_html_e( 'Last week', 'bcs-smtp-manager' ); ?></option>
    1321                                                         <option value="this_month"><?php esc_html_e( 'This month', 'bcs-smtp-manager' ); ?></option>
    1322                                                         <option value="3months"><?php esc_html_e( 'Last 3 months', 'bcs-smtp-manager' ); ?></option>
    1323                                                         <option value="6months"><?php esc_html_e( 'Last 6 months', 'bcs-smtp-manager' ); ?></option>
    1324                                                         <option value="year"><?php esc_html_e( 'This year', 'bcs-smtp-manager' ); ?></option>
    1325                                                         <option value="all"><?php esc_html_e( 'All time', 'bcs-smtp-manager' ); ?></option>
     2222                                <?php endif; ?>
     2223                                <div class="bcs-smtp-panel bcs-smtp-panel--flush">
     2224                                        <div class="bcs-smtp-filters">
     2225                                                <div class="bcs-smtp-search">
     2226                                                        <span class="dashicons dashicons-search" aria-hidden="true"></span>
     2227                                                        <label class="screen-reader-text" for="bcs-smtp-log-search"><?php esc_html_e( 'Search email log', 'bcs-smtp-manager' ); ?></label>
     2228                                                        <input type="search" id="bcs-smtp-log-search" class="bcs-smtp-input" placeholder="<?php esc_attr_e( 'Search subject or address', 'bcs-smtp-manager' ); ?>">
     2229                                                </div>
     2230                                                <label class="screen-reader-text" for="bcs-smtp-log-date-range"><?php esc_html_e( 'Date range', 'bcs-smtp-manager' ); ?></label>
     2231                                                <select id="bcs-smtp-log-date-range" class="bcs-smtp-input bcs-smtp-select">
     2232                                                        <?php foreach ( $date_ranges as $val => $label ) : ?>
     2233                                                                <option value="<?php echo esc_attr( $val ); ?>" <?php selected( $val, '30days' ); ?>><?php echo esc_html( $label ); ?></option>
     2234                                                        <?php endforeach; ?>
    13262235                                                </select>
    1327                                                 <select id="bcs-smtp-log-status" class="bcs-smtp-input" style="width:25%;min-width:120px">
    1328                                                         <option value=""><?php esc_html_e( 'All statuses', 'bcs-smtp-manager' ); ?></option>
    1329                                                         <option value="success"><?php esc_html_e( 'Sent', 'bcs-smtp-manager' ); ?></option>
    1330                                                         <option value="failed"><?php esc_html_e( 'Failed', 'bcs-smtp-manager' ); ?></option>
    1331                                                         <option value="unconfirmed"><?php esc_html_e( 'Unconfirmed', 'bcs-smtp-manager' ); ?></option>
    1332                                                 </select>
    1333                                                 <input type="text" id="bcs-smtp-log-search" class="bcs-smtp-input" style="width:50%;min-width:200px;flex:1" placeholder="<?php esc_attr_e( 'Search subject, from, or to email…', 'bcs-smtp-manager' ); ?>">
     2236                                                <div class="bcs-smtp-segmented bcs-smtp-status-filter" role="radiogroup" aria-label="<?php esc_attr_e( 'Status', 'bcs-smtp-manager' ); ?>">
     2237                                                        <?php foreach ( array( '' => __( 'All', 'bcs-smtp-manager' ), 'success' => __( 'Sent', 'bcs-smtp-manager' ), 'failed' => __( 'Failed', 'bcs-smtp-manager' ), 'unconfirmed' => __( 'Unconfirmed', 'bcs-smtp-manager' ) ) as $val => $label ) : ?>
     2238                                                                <label><input type="radio" name="bcs_smtp_log_status" class="bcs-smtp-log-status-radio" value="<?php echo esc_attr( $val ); ?>" <?php checked( '', $val ); ?>><span><?php echo esc_html( $label ); ?><b class="bcs-smtp-seg-count" data-count="<?php echo esc_attr( '' === $val ? 'all' : $val ); ?>"></b></span></label>
     2239                                                        <?php endforeach; ?>
     2240                                                </div>
     2241                                                <input type="hidden" id="bcs-smtp-log-status" value="">
     2242                                                <a id="bcs-smtp-export-csv-btn" href="#" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm">
     2243                                                        <span class="dashicons dashicons-download" aria-hidden="true"></span><?php esc_html_e( 'Export CSV', 'bcs-smtp-manager' ); ?>
     2244                                                </a>
    13342245                                        </div>
    13352246
    13362247                                        <div class="bcs-smtp-table-wrap">
    13372248                                                <table class="bcs-smtp-table">
     2249                                                        <caption class="screen-reader-text"><?php esc_html_e( 'Emails sent by WordPress', 'bcs-smtp-manager' ); ?></caption>
    13382250                                                        <thead>
    13392251                                                                <tr>
    1340                                                                         <th><?php esc_html_e( 'Date', 'bcs-smtp-manager' ); ?></th>
    1341                                                                         <th><?php esc_html_e( 'Subject', 'bcs-smtp-manager' ); ?></th>
    1342                                                                         <th><?php esc_html_e( 'From', 'bcs-smtp-manager' ); ?></th>
    1343                                                                         <th><?php esc_html_e( 'To', 'bcs-smtp-manager' ); ?></th>
    1344                                                                         <th><?php esc_html_e( 'Status', 'bcs-smtp-manager' ); ?></th>
    1345                                                                         <th><?php esc_html_e( 'Error', 'bcs-smtp-manager' ); ?></th>
    1346                                                                         <th><?php esc_html_e( 'Actions', 'bcs-smtp-manager' ); ?></th>
     2252                                                                        <th scope="col" class="bcs-smtp-col-date"><?php esc_html_e( 'Time', 'bcs-smtp-manager' ); ?></th>
     2253                                                                        <th scope="col" class="bcs-smtp-col-subject"><?php esc_html_e( 'Subject', 'bcs-smtp-manager' ); ?></th>
     2254                                                                        <th scope="col" class="bcs-smtp-col-to"><?php esc_html_e( 'To', 'bcs-smtp-manager' ); ?></th>
     2255                                                                        <th scope="col" class="bcs-smtp-col-status"><?php esc_html_e( 'Status', 'bcs-smtp-manager' ); ?></th>
     2256                                                                        <th scope="col" class="bcs-smtp-col-more"><span class="screen-reader-text"><?php esc_html_e( 'Open', 'bcs-smtp-manager' ); ?></span></th>
    13472257                                                                </tr>
    13482258                                                        </thead>
    13492259                                                        <tbody id="bcs-smtp-log-tbody">
    1350                                                                 <tr><td colspan="7" style="text-align:center;padding:24px;color:var(--bcs-smtp-text-3)"><?php esc_html_e( 'Loading…', 'bcs-smtp-manager' ); ?></td></tr>
     2260                                                                <tr><td colspan="5" class="bcs-smtp-table-empty"><?php esc_html_e( 'Loading…', 'bcs-smtp-manager' ); ?></td></tr>
    13512261                                                        </tbody>
    13522262                                                </table>
    13532263                                        </div>
    13542264
    1355                                         <!-- Pagination + per-page at bottom -->
    1356                                         <div style="margin-top:12px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px">
    1357                                                 <div style="display:flex;align-items:center;gap:8px">
    1358                                                         <label style="font-size:12px;color:var(--bcs-smtp-text-2)"><?php esc_html_e( 'Show per page:', 'bcs-smtp-manager' ); ?></label>
    1359                                                         <select id="bcs-smtp-log-per-page" class="bcs-smtp-input" style="width:auto;min-width:80px">
    1360                                                                 <option value="10" selected>10</option>
    1361                                                                 <option value="20">20</option>
     2265                                        <div class="bcs-smtp-pager">
     2266                                                <div class="bcs-smtp-pager-size">
     2267                                                        <label for="bcs-smtp-log-per-page"><?php esc_html_e( 'Rows', 'bcs-smtp-manager' ); ?></label>
     2268                                                        <select id="bcs-smtp-log-per-page" class="bcs-smtp-input bcs-smtp-select bcs-smtp-select--sm">
     2269                                                                <option value="10">10</option>
     2270                                                                <option value="25" selected>25</option>
    13622271                                                                <option value="50">50</option>
    13632272                                                                <option value="100">100</option>
    13642273                                                        </select>
    13652274                                                </div>
    1366                                                 <div id="bcs-smtp-log-pagination" style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
    1367                                                         <button type="button" id="bcs-smtp-log-prev" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm" disabled>&larr; <?php esc_html_e( 'Prev', 'bcs-smtp-manager' ); ?></button>
    1368                                                         <span id="bcs-smtp-log-page-info" style="font-size:12px;color:var(--bcs-smtp-text-2)"><?php esc_html_e( 'Page 1', 'bcs-smtp-manager' ); ?></span>
    1369                                                         <button type="button" id="bcs-smtp-log-next" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm"><?php esc_html_e( 'Next', 'bcs-smtp-manager' ); ?> &rarr;</button>
    1370                                                 </div>
    1371                                         </div>
     2275                                                <div id="bcs-smtp-log-pagination" class="bcs-smtp-pager-nav">
     2276                                                        <button type="button" id="bcs-smtp-log-prev" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm" disabled><?php esc_html_e( 'Previous', 'bcs-smtp-manager' ); ?></button>
     2277                                                        <span id="bcs-smtp-log-page-info" aria-live="polite"></span>
     2278                                                        <button type="button" id="bcs-smtp-log-next" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm"><?php esc_html_e( 'Next', 'bcs-smtp-manager' ); ?></button>
     2279                                                </div>
    13722280                                        </div>
    13732281                                </div>
    1374 
    1375                                 <!-- Log Retention accordion -->
     2282                        </section>
     2283
     2284                        <!-- ═══ PANEL: Alerts ═════════════════════════════════════════════ -->
     2285                        <section id="bcs-smtp-panel-alerts" class="bcs-smtp-tab-panel" role="tabpanel" aria-labelledby="bcs-smtp-tab-alerts">
     2286
     2287                                <div class="bcs-smtp-alerts-grid">
     2288                                        <section class="bcs-smtp-panel bcs-smtp-alerts-card" aria-labelledby="bcs-smtp-alerts-title">
     2289                                                <div class="bcs-smtp-alerts-head">
     2290                                                        <span class="bcs-smtp-alerts-badge dashicons dashicons-bell" aria-hidden="true"></span>
     2291                                                        <div class="bcs-smtp-alerts-intro">
     2292                                                                <h2 id="bcs-smtp-alerts-title"><label for="bcs-smtp-alert-channel-enabled"><?php esc_html_e( 'Failure alerts', 'bcs-smtp-manager' ); ?></label></h2>
     2293                                                                <p><?php esc_html_e( 'A message in your team chat the moment email stops sending.', 'bcs-smtp-manager' ); ?></p>
     2294                                                        </div>
     2295                                                        <span class="bcs-smtp-toggle">
     2296                                                                <input type="checkbox" id="bcs-smtp-alert-channel-enabled" role="switch" aria-controls="bcs-smtp-alert-channel-accordion" <?php checked( $alert_enabled ); ?>>
     2297                                                                <span class="bcs-smtp-toggle-slider" aria-hidden="true"></span>
     2298                                                        </span>
     2299                                                </div>
     2300
     2301                                                <div id="bcs-smtp-alert-channel-accordion" class="bcs-smtp-reveal <?php echo $alert_enabled ? 'open' : ''; ?>">
     2302                                                        <div class="bcs-smtp-ch-list-head">
     2303                                                                <span id="bcs-smtp-channel-label"><?php esc_html_e( 'Channels', 'bcs-smtp-manager' ); ?></span>
     2304                                                                <span class="bcs-smtp-ch-count" id="bcs-smtp-ch-count" aria-live="polite"></span>
     2305                                                        </div>
     2306                                                        <?php
     2307                                                        $channel_icons = array(
     2308                                                                'slack'       => 'format-chat',
     2309                                                                'teams'       => 'groups',
     2310                                                                'google_chat' => 'format-status',
     2311                                                                'discord'     => 'games',
     2312                                                                'custom'      => 'admin-links',
     2313                                                        );
     2314                                                        $webhooks = array(
     2315                                                                'slack'       => array( 'opt' => BCS_SMTP_OPT_SLACK_WEBHOOK_URL,       'id' => 'slack',       'sub' => __( 'Incoming webhook', 'bcs-smtp-manager' ),                 'ph' => 'https://hooks.slack.com/services/…', 'how' => __( 'In Slack: Apps → Incoming Webhooks → Add to a channel.', 'bcs-smtp-manager' ), 'link' => 'https://api.slack.com/messaging/webhooks' ),
     2316                                                                'teams'       => array( 'opt' => BCS_SMTP_OPT_TEAMS_WEBHOOK_URL,       'id' => 'teams',       'sub' => __( 'Workflows webhook', 'bcs-smtp-manager' ),                'ph' => 'https://…powerplatform.com/…/workflows/…', 'how' => __( 'In Teams: channel → Workflows → “Post to a channel when a webhook request is received”.', 'bcs-smtp-manager' ), 'link' => 'https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook' ),
     2317                                                                'google_chat' => array( 'opt' => BCS_SMTP_OPT_GOOGLE_CHAT_WEBHOOK_URL, 'id' => 'google-chat', 'sub' => __( 'Space webhook', 'bcs-smtp-manager' ),                    'ph' => 'https://chat.googleapis.com/v1/spaces/…', 'how' => __( 'In Google Chat: space → Apps & integrations → Webhooks.', 'bcs-smtp-manager' ), 'link' => 'https://developers.google.com/workspace/chat/quickstart/webhooks' ),
     2318                                                                'discord'     => array( 'opt' => BCS_SMTP_OPT_DISCORD_WEBHOOK_URL,     'id' => 'discord',     'sub' => __( 'Channel webhook', 'bcs-smtp-manager' ),                  'ph' => 'https://discord.com/api/webhooks/…', 'how' => __( 'In Discord: Server Settings → Integrations → Webhooks.', 'bcs-smtp-manager' ), 'link' => 'https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks' ),
     2319                                                                'custom'      => array( 'opt' => BCS_SMTP_OPT_CUSTOM_WEBHOOK_URL,      'id' => 'custom',      'sub' => __( 'JSON for Zapier, Make or n8n', 'bcs-smtp-manager' ),      'ph' => 'https://example.com/webhook', 'how' => __( 'Receives a JSON payload with the event, site, error and failure count.', 'bcs-smtp-manager' ), 'link' => '' ),
     2320                                                        );
     2321                                                        ?>
     2322                                                        <ul class="bcs-smtp-ch-list" aria-labelledby="bcs-smtp-channel-label">
     2323                                                                <?php
     2324                                                                foreach ( $channels as $val => $ch ) :
     2325                                                                        $w = $webhooks[ $val ];
     2326                                                                        ?>
     2327                                                                        <li class="bcs-smtp-ch" data-channel="<?php echo esc_attr( $val ); ?>">
     2328                                                                                <div class="bcs-smtp-ch-row">
     2329                                                                                        <button type="button" class="bcs-smtp-ch-main" aria-expanded="false" aria-controls="bcs-smtp-<?php echo esc_attr( $w['id'] ); ?>-webhook-row">
     2330                                                                                                <span class="bcs-smtp-ch-icon dashicons dashicons-<?php echo esc_attr( $channel_icons[ $val ] ); ?>" aria-hidden="true"></span>
     2331                                                                                                <span class="bcs-smtp-ch-text">
     2332                                                                                                        <span class="bcs-smtp-ch-name"><?php echo esc_html( $ch['label'] ); ?></span>
     2333                                                                                                        <span class="bcs-smtp-ch-sub"><?php echo esc_html( $w['sub'] ); ?></span>
     2334                                                                                                </span>
     2335                                                                                                <span class="bcs-smtp-ch-pill" aria-live="polite"></span>
     2336                                                                                                <span class="bcs-smtp-ch-chevron dashicons dashicons-arrow-down-alt2" aria-hidden="true"></span>
     2337                                                                                        </button>
     2338                                                                                        <span class="bcs-smtp-toggle">
     2339                                                                                                <input type="checkbox" name="bcs_alert_channels[]" class="bcs-smtp-channel-radio" role="switch" autocomplete="off" value="<?php echo esc_attr( $val ); ?>" data-label="<?php echo esc_attr( $ch['label'] ); ?>" aria-label="<?php echo esc_attr( sprintf( /* translators: %s: alert channel name, e.g. Slack. */ __( 'Send alerts to %s', 'bcs-smtp-manager' ), $ch['label'] ) ); ?>" <?php checked( in_array( $val, $alert_chs, true ) ); ?>>
     2340                                                                                                <span class="bcs-smtp-toggle-slider" aria-hidden="true"></span>
     2341                                                                                        </span>
     2342                                                                                </div>
     2343                                                                                <div id="bcs-smtp-<?php echo esc_attr( $w['id'] ); ?>-webhook-row" class="bcs-smtp-webhook" data-channel="<?php echo esc_attr( $val ); ?>" hidden>
     2344                                                                                        <div class="bcs-smtp-webhook-head">
     2345                                                                                                <label class="bcs-smtp-field-label" for="bcs-smtp-<?php echo esc_attr( $w['id'] ); ?>-webhook-url"><?php esc_html_e( 'Webhook URL', 'bcs-smtp-manager' ); ?></label>
     2346                                                                                                <?php if ( $w['link'] ) : ?>
     2347                                                                                                        <a class="bcs-smtp-webhook-guide" href="<?php echo esc_url( $w['link'] ); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Setup guide', 'bcs-smtp-manager' ); ?><span class="dashicons dashicons-external" aria-hidden="true"></span><span class="screen-reader-text"> <?php esc_html_e( '(opens in a new tab)', 'bcs-smtp-manager' ); ?></span></a>
     2348                                                                                                <?php endif; ?>
     2349                                                                                        </div>
     2350                                                                                        <input type="url" id="bcs-smtp-<?php echo esc_attr( $w['id'] ); ?>-webhook-url" class="bcs-smtp-input bcs-smtp-mono" value="<?php echo esc_attr( get_option( $w['opt'], '' ) ); ?>" placeholder="<?php echo esc_attr( $w['ph'] ); ?>" spellcheck="false" autocomplete="off" aria-describedby="bcs-smtp-<?php echo esc_attr( $w['id'] ); ?>-webhook-err bcs-smtp-<?php echo esc_attr( $w['id'] ); ?>-webhook-how">
     2351                                                                                        <p class="bcs-smtp-webhook-err" id="bcs-smtp-<?php echo esc_attr( $w['id'] ); ?>-webhook-err" hidden></p>
     2352                                                                                        <p class="bcs-smtp-webhook-how" id="bcs-smtp-<?php echo esc_attr( $w['id'] ); ?>-webhook-how"><?php echo esc_html( $w['how'] ); ?></p>
     2353                                                                                </div>
     2354                                                                        </li>
     2355                                                                <?php endforeach; ?>
     2356                                                        </ul>
     2357                                                </div>
     2358
     2359                                                <div class="bcs-smtp-alerts-off" id="bcs-smtp-alerts-off" <?php echo $alert_enabled ? 'hidden' : ''; ?>>
     2360                                                        <span class="dashicons dashicons-bell" aria-hidden="true"></span>
     2361                                                        <p><?php esc_html_e( 'Alerts are off. Health checks still run, but you’ll only see a problem when you open this page.', 'bcs-smtp-manager' ); ?></p>
     2362                                                </div>
     2363
     2364                                                <div class="bcs-smtp-actions bcs-smtp-card-foot">
     2365                                                        <button type="button" id="bcs-smtp-save-alerts-btn" class="bcs-smtp-btn bcs-smtp-btn-primary"><?php esc_html_e( 'Save alerts', 'bcs-smtp-manager' ); ?></button>
     2366                                                        <button type="button" id="bcs-smtp-test-alert-btn" class="bcs-smtp-btn bcs-smtp-btn-secondary" <?php disabled( ! $alert_enabled || ! $alert_chs ); ?>><?php esc_html_e( 'Send test alert', 'bcs-smtp-manager' ); ?></button>
     2367                                                        <span class="bcs-smtp-unsaved" id="bcs-smtp-alerts-unsaved" hidden><?php esc_html_e( 'Unsaved changes', 'bcs-smtp-manager' ); ?></span>
     2368                                                </div>
     2369                                                <div id="bcs-smtp-alerts-msg" class="bcs-smtp-msg" role="status" aria-live="polite"></div>
     2370                                        </section>
     2371
     2372                                        <aside class="bcs-smtp-panel bcs-smtp-health-side" aria-labelledby="bcs-smtp-monitor-title">
     2373                                                <div class="bcs-smtp-health-head">
     2374                                                        <span class="bcs-smtp-health-icon dashicons dashicons-heart" aria-hidden="true"></span>
     2375                                                        <div>
     2376                                                                <h2 id="bcs-smtp-monitor-title"><?php esc_html_e( 'Health checks run automatically', 'bcs-smtp-manager' ); ?></h2>
     2377                                                                <p><?php esc_html_e( 'Each check signs in to your mail server without sending an email.', 'bcs-smtp-manager' ); ?></p>
     2378                                                        </div>
     2379                                                </div>
     2380
     2381                                                <?php if ( $monitor_on ) : ?>
     2382                                                        <dl class="bcs-smtp-health-times">
     2383                                                                <div>
     2384                                                                        <dt><?php esc_html_e( 'Last check', 'bcs-smtp-manager' ); ?></dt>
     2385                                                                        <dd>
     2386                                                                                <?php
     2387                                                                                /* translators: %s: time since the last check, e.g. "5 minutes". */
     2388                                                                                echo $last_check ? esc_html( sprintf( __( '%s ago', 'bcs-smtp-manager' ), $this->check_minutes( $last_check ) ) ) : esc_html__( 'Not checked yet', 'bcs-smtp-manager' );
     2389                                                                                ?>
     2390                                                                        </dd>
     2391                                                                </div>
     2392                                                                <div>
     2393                                                                        <dt><?php esc_html_e( 'Next check', 'bcs-smtp-manager' ); ?></dt>
     2394                                                                        <dd>
     2395                                                                                <?php
     2396                                                                                if ( ! $next_check ) {
     2397                                                                                        esc_html_e( 'Being scheduled', 'bcs-smtp-manager' );
     2398                                                                                } elseif ( $next_check > time() ) {
     2399                                                                                        /* translators: %s: time until the next check, e.g. "10 minutes". */
     2400                                                                                        echo esc_html( sprintf( __( 'in %s', 'bcs-smtp-manager' ), $this->check_minutes( $next_check, true ) ) );
     2401                                                                                } else {
     2402                                                                                        esc_html_e( 'Due now', 'bcs-smtp-manager' );
     2403                                                                                }
     2404                                                                                ?>
     2405                                                                        </dd>
     2406                                                                </div>
     2407                                                        </dl>
     2408                                                <?php else : ?>
     2409                                                        <p class="bcs-smtp-health-pending"><?php esc_html_e( 'Health checks start once your connection is set up.', 'bcs-smtp-manager' ); ?></p>
     2410                                                <?php endif; ?>
     2411
     2412                                                <ul class="bcs-smtp-health-rules">
     2413                                                        <li><span class="dashicons dashicons-clock" aria-hidden="true"></span>
     2414                                                                <?php
     2415                                                                /* translators: %d: minutes between health checks. */
     2416                                                                printf( esc_html__( 'Every %d minutes', 'bcs-smtp-manager' ), (int) BCS_SMTP_CHECK_INTERVAL_MINUTES );
     2417                                                                ?>
     2418                                                        </li>
     2419                                                        <li><span class="dashicons dashicons-bell" aria-hidden="true"></span><?php esc_html_e( 'Alert on the failed check', 'bcs-smtp-manager' ); ?></li>
     2420                                                        <li><span class="dashicons dashicons-shield" aria-hidden="true"></span><?php esc_html_e( 'One alert per outage, never a flood', 'bcs-smtp-manager' ); ?></li>
     2421                                                        <li><span class="dashicons dashicons-update" aria-hidden="true"></span><?php esc_html_e( 'Resets when mail recovers', 'bcs-smtp-manager' ); ?></li>
     2422                                                </ul>
     2423                                        </aside>
     2424                                </div>
     2425                        </section>
     2426
     2427                        <!-- ═══ PANEL: Reports ════════════════════════════════════════════ -->
     2428                        <section id="bcs-smtp-panel-reports" class="bcs-smtp-tab-panel" role="tabpanel" aria-labelledby="bcs-smtp-tab-reports">
    13762429                                <?php
    1377                                 $prune_mode = get_option( 'bcs_log_prune_mode', 'days' );
     2430                                $report_ranges = array(
     2431                                        '7days'   => __( '7 days', 'bcs-smtp-manager' ),
     2432                                        '30days'  => __( '30 days', 'bcs-smtp-manager' ),
     2433                                        '3months' => __( '90 days', 'bcs-smtp-manager' ),
     2434                                );
    13782435                                ?>
    1379                                 <div class="bcs-smtp-accordion" style="margin-top:12px">
    1380                                         <button type="button" class="bcs-smtp-accordion-header" aria-expanded="false">
    1381                                                 <span class="dashicons dashicons-trash"></span>
    1382                                                 <span><?php esc_html_e( 'Log Retention', 'bcs-smtp-manager' ); ?></span>
    1383                                                 <span class="bcs-smtp-accordion-arrow dashicons dashicons-arrow-down-alt2" style="margin-left:auto"></span>
    1384                                         </button>
    1385                                         <div class="bcs-smtp-accordion-body" style="display:none">
    1386                                                 <p style="font-size:13px;color:var(--bcs-smtp-text-2);margin:0 0 16px"><?php esc_html_e( 'Automatically delete old log entries to keep the database lean. Runs daily via WP-Cron.', 'bcs-smtp-manager' ); ?></p>
    1387                                                 <div class="bcs-smtp-check-list" style="flex-direction:column;gap:6px;margin-bottom:16px">
    1388                                                         <label class="bcs-smtp-check-item" style="flex-direction:row;align-items:center;gap:10px;padding:10px 14px">
    1389                                                                 <input type="radio" name="bcs_smtp_prune_mode" class="bcs-smtp-prune-radio" value="days" <?php checked( $prune_mode, 'days' ); ?> style="width:20px;height:20px;flex-shrink:0">
    1390                                                                 <span class="bcs-smtp-check-label-title"><?php esc_html_e( 'Keep last', 'bcs-smtp-manager' ); ?></span>
    1391                                                                 <input type="number" class="bcs-smtp-input" value="30" readonly disabled
    1392                                                                         style="width:80px;display:inline-block;opacity:.7;cursor:not-allowed">
    1393                                                                 <span style="font-size:13px;color:var(--bcs-smtp-text-2)"><?php esc_html_e( 'days', 'bcs-smtp-manager' ); ?> <span style="font-size:11px;color:var(--bcs-smtp-text-3)"><?php esc_html_e( '(fixed)', 'bcs-smtp-manager' ); ?></span></span>
    1394                                                         </label>
    1395                                                         <label class="bcs-smtp-check-item" style="flex-direction:row;align-items:center;gap:10px;padding:10px 14px">
    1396                                                                 <input type="radio" name="bcs_smtp_prune_mode" class="bcs-smtp-prune-radio" value="records" <?php checked( $prune_mode, 'records' ); ?> style="width:20px;height:20px;flex-shrink:0">
    1397                                                                 <span class="bcs-smtp-check-label-title"><?php esc_html_e( 'Keep last', 'bcs-smtp-manager' ); ?></span>
    1398                                                                 <input type="number" class="bcs-smtp-input" value="1000" readonly disabled
    1399                                                                         style="width:80px;display:inline-block;opacity:.7;cursor:not-allowed">
    1400                                                                 <span style="font-size:13px;color:var(--bcs-smtp-text-2)"><?php esc_html_e( 'records', 'bcs-smtp-manager' ); ?> <span style="font-size:11px;color:var(--bcs-smtp-text-3)"><?php esc_html_e( '(fixed)', 'bcs-smtp-manager' ); ?></span></span>
    1401                                                         </label>
    1402                                                 </div>
    1403                                                 <button type="button" id="bcs-smtp-save-log-prune-btn" class="bcs-smtp-btn bcs-smtp-btn-primary">
    1404                                                         <span class="dashicons dashicons-saved"></span><?php esc_html_e( 'Save Retention Settings', 'bcs-smtp-manager' ); ?>
    1405                                                 </button>
    1406                                                 <div id="bcs-smtp-log-prune-msg" role="status" aria-live="polite" style="display:none;margin-top:10px"></div>
     2436                                <section class="bcs-smtp-panel bcs-smtp-report" aria-labelledby="bcs-smtp-report-title">
     2437                                        <div class="bcs-smtp-panel-head">
     2438                                                <div>
     2439                                                        <h2 id="bcs-smtp-report-title"><?php esc_html_e( 'Email delivery', 'bcs-smtp-manager' ); ?></h2>
     2440                                                        <p class="bcs-smtp-panel-sub" id="bcs-smtp-report-period"><?php esc_html_e( 'Last 30 days', 'bcs-smtp-manager' ); ?></p>
     2441                                                </div>
     2442                                                <div class="bcs-smtp-report-tools">
     2443                                                        <div class="bcs-smtp-segmented bcs-smtp-segmented--sm" role="radiogroup" aria-label="<?php esc_attr_e( 'Time range', 'bcs-smtp-manager' ); ?>">
     2444                                                                <?php foreach ( $report_ranges as $val => $label ) : ?>
     2445                                                                        <label><input type="radio" name="bcs_smtp_rp_range" class="bcs-smtp-rp-range" autocomplete="off" value="<?php echo esc_attr( $val ); ?>" data-label="<?php echo esc_attr( $label ); ?>" <?php checked( '30days', $val ); ?>><span><?php echo esc_html( $label ); ?></span></label>
     2446                                                                <?php endforeach; ?>
     2447                                                        </div>
     2448                                                        <button type="button" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm" id="bcs-smtp-report-export">
     2449                                                                <span class="dashicons dashicons-download" aria-hidden="true"></span><?php esc_html_e( 'Export CSV', 'bcs-smtp-manager' ); ?>
     2450                                                        </button>
     2451                                                </div>
    14072452                                        </div>
     2453
     2454                                        <dl class="bcs-smtp-figures bcs-smtp-figures--5 bcs-smtp-report-stats">
     2455                                                <div><dt><?php esc_html_e( 'Total', 'bcs-smtp-manager' ); ?></dt><dd id="bcs-smtp-report-total"><?php echo esc_html( number_format_i18n( $summary_30d['total'] ) ); ?></dd></div>
     2456                                                <div><dt><span class="bcs-smtp-key bcs-smtp-key--sent" aria-hidden="true"></span><?php esc_html_e( 'Sent', 'bcs-smtp-manager' ); ?></dt><dd id="bcs-smtp-report-success"><?php echo esc_html( number_format_i18n( $summary_30d['success'] ) ); ?></dd></div>
     2457                                                <div><dt><span class="bcs-smtp-key bcs-smtp-key--bad" aria-hidden="true"></span><?php esc_html_e( 'Failed', 'bcs-smtp-manager' ); ?></dt><dd id="bcs-smtp-report-failed" class="<?php echo $summary_30d['failed'] > 0 ? 'is-bad' : ''; ?>"><?php echo esc_html( number_format_i18n( $summary_30d['failed'] ) ); ?></dd></div>
     2458                                                <div><dt><span class="bcs-smtp-key bcs-smtp-key--warn" aria-hidden="true"></span><?php esc_html_e( 'Unconfirmed', 'bcs-smtp-manager' ); ?></dt><dd id="bcs-smtp-report-unconfirmed"><?php echo esc_html( number_format_i18n( $summary_30d['unconfirmed'] ) ); ?></dd></div>
     2459                                                <div class="bcs-smtp-figure-rate">
     2460                                                        <dt><?php esc_html_e( 'Delivered', 'bcs-smtp-manager' ); ?></dt>
     2461                                                        <dd id="bcs-smtp-report-rate"><?php echo null === $rate ? '—' : esc_html( number_format_i18n( $rate, 1 ) ) . '<small>%</small>'; ?></dd>
     2462                                                        <span class="bcs-smtp-ratebar" aria-hidden="true"><span id="bcs-smtp-report-ratebar" style="width:<?php echo esc_attr( null === $rate ? 0 : $rate ); ?>%"></span></span>
     2463                                                </div>
     2464                                        </dl>
     2465
     2466                                        <div class="bcs-smtp-linechart bcs-smtp-linechart--lg" id="bcs-smtp-report-chart" tabindex="0"
     2467                                                role="img" aria-label="<?php esc_attr_e( 'Emails over time: sent, failed and unconfirmed. Use the left and right arrow keys to read each point.', 'bcs-smtp-manager' ); ?>"
     2468                                                data-chart="<?php echo esc_attr( wp_json_encode( $chart ) ); ?>">
     2469                                                <div class="bcs-smtp-linechart-tip" aria-live="polite" hidden></div>
     2470                                        </div>
     2471                                        <p class="bcs-smtp-chart-empty" id="bcs-smtp-chart-empty" <?php echo $summary_30d['total'] > 0 ? 'hidden' : ''; ?>><?php esc_html_e( 'No email was sent in this period.', 'bcs-smtp-manager' ); ?></p>
     2472                                </section>
     2473
     2474                                <div class="bcs-smtp-report-grid">
     2475                                        <section class="bcs-smtp-panel" aria-labelledby="bcs-smtp-highlights-title">
     2476                                                <div class="bcs-smtp-panel-head">
     2477                                                        <h2 id="bcs-smtp-highlights-title"><?php esc_html_e( 'Highlights', 'bcs-smtp-manager' ); ?></h2>
     2478                                                </div>
     2479                                                <dl class="bcs-smtp-highlights" id="bcs-smtp-highlights">
     2480                                                        <div><dt><span class="bcs-smtp-hl-icon dashicons dashicons-chart-line" aria-hidden="true"></span><?php esc_html_e( 'Average per day', 'bcs-smtp-manager' ); ?></dt><dd data-hl="avg">—</dd></div>
     2481                                                        <div><dt><span class="bcs-smtp-hl-icon dashicons dashicons-calendar-alt" aria-hidden="true"></span><?php esc_html_e( 'Busiest day', 'bcs-smtp-manager' ); ?></dt><dd data-hl="peak">—</dd></div>
     2482                                                        <div><dt><span class="bcs-smtp-hl-icon bcs-smtp-hl-icon--ok dashicons dashicons-yes-alt" aria-hidden="true"></span><?php esc_html_e( 'Failure-free days', 'bcs-smtp-manager' ); ?></dt><dd data-hl="cleandays">—</dd></div>
     2483                                                        <div><dt><span class="bcs-smtp-hl-icon bcs-smtp-hl-icon--ok dashicons dashicons-awards" aria-hidden="true"></span><?php esc_html_e( 'Longest failure-free run', 'bcs-smtp-manager' ); ?></dt><dd data-hl="beststreak">—</dd></div>
     2484                                                        <div><dt><span class="bcs-smtp-hl-icon bcs-smtp-hl-icon--ok dashicons dashicons-groups" aria-hidden="true"></span><span><?php esc_html_e( 'People reached', 'bcs-smtp-manager' ); ?><small class="bcs-smtp-hl-sub"><?php esc_html_e( 'Visitors and customers who received your email', 'bcs-smtp-manager' ); ?></small></span></dt><dd data-hl="recipients" data-initial="<?php list( $own, $own_domains ) = $this->internal_recipients(); echo esc_attr( $this->db->count_unique_recipients( '30days', $own, $own_domains ) ); ?>">—</dd></div>
     2485                                                </dl>
     2486                                        </section>
     2487
     2488                                        <section class="bcs-smtp-panel bcs-smtp-schedule" aria-labelledby="bcs-smtp-schedule-title">
     2489                                                <div class="bcs-smtp-toggle-row bcs-smtp-toggle-row--head">
     2490                                                        <div class="bcs-smtp-toggle-row-info">
     2491                                                                <h2 id="bcs-smtp-schedule-title"><label for="bcs-smtp-report-schedule-enabled"><?php esc_html_e( 'Email me this report', 'bcs-smtp-manager' ); ?></label></h2>
     2492                                                                <p class="bcs-smtp-toggle-row-desc"><?php esc_html_e( 'On the 1st of every month at 9:00 am, a summary of last month’s sent, failed and unconfirmed email with a daily chart.', 'bcs-smtp-manager' ); ?></p>
     2493                                                        </div>
     2494                                                        <span class="bcs-smtp-toggle">
     2495                                                                <input type="checkbox" id="bcs-smtp-report-schedule-enabled" role="switch" aria-controls="bcs-smtp-report-body" <?php checked( $report_schedule_enabled ); ?>>
     2496                                                                <span class="bcs-smtp-toggle-slider" aria-hidden="true"></span>
     2497                                                        </span>
     2498                                                </div>
     2499                                                <div id="bcs-smtp-report-body" <?php echo $report_schedule_enabled ? '' : 'hidden'; ?>>
     2500                                                <div class="bcs-smtp-field">
     2501                                                        <label class="bcs-smtp-field-label" for="bcs-smtp-report-emails-entry"><?php esc_html_e( 'Send to', 'bcs-smtp-manager' ); ?></label>
     2502                                                        <div class="bcs-smtp-chips" id="bcs-smtp-report-chips">
     2503                                                                <ul class="bcs-smtp-chip-list" aria-label="<?php esc_attr_e( 'Report recipients', 'bcs-smtp-manager' ); ?>"></ul>
     2504                                                                <input type="text" inputmode="email" id="bcs-smtp-report-emails-entry" class="bcs-smtp-chips-input" placeholder="<?php esc_attr_e( 'Add an email address', 'bcs-smtp-manager' ); ?>" autocomplete="off" spellcheck="false">
     2505                                                        </div>
     2506                                                        <input type="hidden" id="bcs-smtp-report-emails" value="<?php echo esc_attr( $report_emails ? $report_emails : get_option( 'admin_email' ) ); ?>">
     2507                                                </div>
     2508                                                <?php $next_report = $this->next_report_timestamp(); ?>
     2509                                                <p class="bcs-smtp-report-next" id="bcs-smtp-report-next" <?php echo $next_report ? '' : 'hidden'; ?>>
     2510                                                        <span class="dashicons dashicons-clock" aria-hidden="true"></span>
     2511                                                        <span id="bcs-smtp-report-next-text"><?php echo $next_report ? esc_html( $this->describe_next_report( $next_report ) ) : ''; ?></span>
     2512                                                </p>
     2513                                                </div>
     2514                                                <div class="bcs-smtp-alerts-off" id="bcs-smtp-report-off" <?php echo $report_schedule_enabled ? 'hidden' : ''; ?>>
     2515                                                        <span class="dashicons dashicons-email-alt" aria-hidden="true"></span>
     2516                                                        <p><?php esc_html_e( 'Monthly reports are off. Your stats stay on this page.', 'bcs-smtp-manager' ); ?></p>
     2517                                                </div>
     2518                                                <div class="bcs-smtp-actions">
     2519                                                        <button type="button" id="bcs-smtp-save-report-schedule-btn" class="bcs-smtp-btn bcs-smtp-btn-primary"><?php esc_html_e( 'Save schedule', 'bcs-smtp-manager' ); ?></button>
     2520                                                        <button type="button" id="bcs-smtp-send-report-now-btn" class="bcs-smtp-btn bcs-smtp-btn-secondary" <?php echo $report_schedule_enabled ? '' : 'hidden'; ?>><?php esc_html_e( 'Send a report now', 'bcs-smtp-manager' ); ?></button>
     2521                                                </div>
     2522                                                <div id="bcs-smtp-report-schedule-msg" class="bcs-smtp-msg" role="status" aria-live="polite"></div>
     2523                                        </section>
    14082524                                </div>
    1409                         </div>
    1410 
    1411                         <!-- ═══════════════════════════════════════════════════════════════
    1412                              PANEL: Email Report
    1413                              ═══════════════════════════════════════════════════════════════ -->
    1414                         <div id="bcs-smtp-panel-email-report" class="bcs-smtp-tab-panel">
    1415                                 <div class="bcs-smtp-card">
    1416                                         <div class="bcs-smtp-card-header">
    1417                                                 <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px">
    1418                                                         <p class="bcs-smtp-card-title" style="margin:0"><span class="dashicons dashicons-chart-bar"></span><?php esc_html_e( 'Email Report', 'bcs-smtp-manager' ); ?></p>
    1419                                                         <select id="bcs-smtp-report-date-range" class="bcs-smtp-input" style="max-width:160px">
    1420                                                                 <option value="today"><?php esc_html_e( 'Today', 'bcs-smtp-manager' ); ?></option>
    1421                                                                 <option value="yesterday"><?php esc_html_e( 'Yesterday', 'bcs-smtp-manager' ); ?></option>
    1422                                                                 <option value="7days"><?php esc_html_e( 'Last 7 days', 'bcs-smtp-manager' ); ?></option>
    1423                                                                 <option value="30days" selected><?php esc_html_e( 'Last 30 days', 'bcs-smtp-manager' ); ?></option>
    1424                                                                 <option value="this_week"><?php esc_html_e( 'This week', 'bcs-smtp-manager' ); ?></option>
    1425                                                                 <option value="last_week"><?php esc_html_e( 'Last week', 'bcs-smtp-manager' ); ?></option>
    1426                                                                 <option value="this_month"><?php esc_html_e( 'This month', 'bcs-smtp-manager' ); ?></option>
    1427                                                                 <option value="3months"><?php esc_html_e( 'Last 3 months', 'bcs-smtp-manager' ); ?></option>
    1428                                                                 <option value="6months"><?php esc_html_e( 'Last 6 months', 'bcs-smtp-manager' ); ?></option>
    1429                                                                 <option value="year"><?php esc_html_e( 'This year', 'bcs-smtp-manager' ); ?></option>
    1430                                                                 <option value="all"><?php esc_html_e( 'All time', 'bcs-smtp-manager' ); ?></option>
    1431                                                         </select>
    1432                                                 </div>
    1433                                         </div>
    1434                                         <div class="bcs-smtp-card-body">
    1435                                                 <!-- Summary stat boxes -->
    1436                                                 <div class="bcs-smtp-report-stats" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:20px">
    1437                                                         <div class="bcs-smtp-stat" style="border:1px solid var(--bcs-smtp-border);border-radius:var(--bcs-smtp-radius);padding:16px">
    1438                                                                 <div class="bcs-smtp-stat-icon good">
    1439                                                                         <span class="dashicons dashicons-yes"></span>
    1440                                                                 </div>
    1441                                                                 <div class="bcs-smtp-stat-body">
    1442                                                                         <div class="bcs-smtp-stat-value good" id="bcs-smtp-report-success" style="font-size:28px"><?php echo esc_html( $summary_30d['success'] ); ?></div>
    1443                                                                         <div class="bcs-smtp-stat-label"><?php esc_html_e( 'Sent', 'bcs-smtp-manager' ); ?></div>
    1444                                                                 </div>
    1445                                                         </div>
    1446                                                         <div class="bcs-smtp-stat" style="border:1px solid var(--bcs-smtp-border);border-radius:var(--bcs-smtp-radius);padding:16px">
    1447                                                                 <div class="bcs-smtp-stat-icon bad">
    1448                                                                         <span class="dashicons dashicons-warning"></span>
    1449                                                                 </div>
    1450                                                                 <div class="bcs-smtp-stat-body">
    1451                                                                         <div class="bcs-smtp-stat-value bad" id="bcs-smtp-report-failed" style="font-size:28px"><?php echo esc_html( $summary_30d['failed'] ); ?></div>
    1452                                                                         <div class="bcs-smtp-stat-label"><?php esc_html_e( 'Failed', 'bcs-smtp-manager' ); ?></div>
    1453                                                                 </div>
    1454                                                         </div>
    1455                                                         <div class="bcs-smtp-stat" style="border:1px solid var(--bcs-smtp-border);border-radius:var(--bcs-smtp-radius);padding:16px">
    1456                                                                 <div class="bcs-smtp-stat-icon">
    1457                                                                         <span class="dashicons dashicons-editor-help"></span>
    1458                                                                 </div>
    1459                                                                 <div class="bcs-smtp-stat-body">
    1460                                                                         <div class="bcs-smtp-stat-value" id="bcs-smtp-report-unconfirmed" style="font-size:28px"><?php echo esc_html( $summary_30d['unconfirmed'] ); ?></div>
    1461                                                                         <div class="bcs-smtp-stat-label"><?php esc_html_e( 'Unconfirmed', 'bcs-smtp-manager' ); ?></div>
    1462                                                                 </div>
    1463                                                         </div>
    1464                                                         <div class="bcs-smtp-stat" style="border:1px solid var(--bcs-smtp-border);border-radius:var(--bcs-smtp-radius);padding:16px">
    1465                                                                 <div class="bcs-smtp-stat-icon">
    1466                                                                         <span class="dashicons dashicons-chart-bar"></span>
    1467                                                                 </div>
    1468                                                                 <div class="bcs-smtp-stat-body">
    1469                                                                         <div class="bcs-smtp-stat-value" id="bcs-smtp-report-total" style="font-size:28px"><?php echo esc_html( $summary_30d['total'] ); ?></div>
    1470                                                                         <div class="bcs-smtp-stat-label"><?php esc_html_e( 'Total', 'bcs-smtp-manager' ); ?></div>
    1471                                                                 </div>
    1472                                                         </div>
    1473                                                 </div>
    1474 
    1475                                                 <!-- Chart -->
    1476                                                 <div style="position:relative;height:320px;width:100%;margin-bottom:0">
    1477                                                         <canvas id="bcs-smtp-report-chart"></canvas>
    1478                                                 </div>
    1479                                         </div>
    1480                                 </div>
    1481 
    1482                                 <!-- Schedule Report accordion — inside the panel, outside the card -->
    1483                                 <div class="bcs-smtp-accordion" style="margin-top:12px">
    1484                                         <button type="button" class="bcs-smtp-accordion-header" aria-expanded="false">
    1485                                                 <span class="dashicons dashicons-calendar-alt"></span>
    1486                                                 <span><?php esc_html_e( 'Schedule Report', 'bcs-smtp-manager' ); ?></span>
    1487                                                 <span class="bcs-smtp-accordion-arrow dashicons dashicons-arrow-down-alt2" style="margin-left:auto"></span>
    1488                                         </button>
    1489                                         <div class="bcs-smtp-accordion-body" style="display:none">
    1490                                                 <div class="bcs-smtp-toggle-row" style="padding:4px 0 16px">
    1491                                                         <div class="bcs-smtp-toggle-row-info">
    1492                                                                 <div class="bcs-smtp-toggle-row-label"><?php esc_html_e( 'Enable automated report emails', 'bcs-smtp-manager' ); ?></div>
    1493                                                                 <div class="bcs-smtp-toggle-row-desc"><?php esc_html_e( 'Automatically send email reports on the schedule below', 'bcs-smtp-manager' ); ?></div>
    1494                                                         </div>
    1495                                                         <label class="bcs-smtp-toggle">
    1496                                                                 <input type="checkbox" id="bcs-smtp-report-schedule-enabled" <?php checked( $report_schedule_enabled ); ?>>
    1497                                                                 <span class="bcs-smtp-toggle-slider"></span>
    1498                                                         </label>
    1499                                                 </div>
    1500                                                 <div class="bcs-smtp-field-row" style="margin-bottom:20px">
    1501                                                         <div class="bcs-smtp-field">
    1502                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-report-emails"><?php esc_html_e( 'Recipients', 'bcs-smtp-manager' ); ?> <span style="color:var(--bcs-smtp-text-3);font-weight:400"><?php esc_html_e( '(comma-separated)', 'bcs-smtp-manager' ); ?></span></label>
    1503                                                                 <input type="text" id="bcs-smtp-report-emails" class="bcs-smtp-input" value="<?php echo esc_attr( $report_emails ); ?>" placeholder="admin@example.com, manager@example.com">
    1504                                                         </div>
    1505                                                         <div class="bcs-smtp-field">
    1506                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-report-frequency"><?php esc_html_e( 'Frequency', 'bcs-smtp-manager' ); ?></label>
    1507                                                                 <select id="bcs-smtp-report-frequency" class="bcs-smtp-input">
    1508                                                                         <option value="daily"   <?php selected( $report_frequency, 'daily' ); ?>><?php esc_html_e( 'Daily', 'bcs-smtp-manager' ); ?></option>
    1509                                                                         <option value="weekly"  <?php selected( $report_frequency, 'weekly' ); ?>><?php esc_html_e( 'Weekly', 'bcs-smtp-manager' ); ?></option>
    1510                                                                         <option value="monthly" <?php selected( $report_frequency, 'monthly' ); ?>><?php esc_html_e( 'Monthly', 'bcs-smtp-manager' ); ?></option>
    1511                                                                 </select>
    1512                                                         </div>
    1513                                                 </div>
    1514                                                 <div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
    1515                                                         <button type="button" id="bcs-smtp-save-report-schedule-btn" class="bcs-smtp-btn bcs-smtp-btn-primary">
    1516                                                                 <span class="dashicons dashicons-saved"></span><?php esc_html_e( 'Save Schedule', 'bcs-smtp-manager' ); ?>
    1517                                                         </button>
    1518                                                         <button type="button" id="bcs-smtp-send-report-now-btn" class="bcs-smtp-btn bcs-smtp-btn-secondary">
    1519                                                                 <span class="dashicons dashicons-email-alt"></span><?php esc_html_e( 'Send Report Now', 'bcs-smtp-manager' ); ?>
    1520                                                         </button>
    1521                                                 </div>
    1522                                                 <div id="bcs-smtp-report-schedule-msg" role="status" aria-live="polite" style="display:none;margin-top:12px"></div>
    1523                                         </div>
    1524                                 </div>
    1525 
    1526                         </div>
    1527 
    1528                         <!-- ═══════════════════════════════════════════════════════════════
    1529                              PANEL: Send Test Email
    1530                              ═══════════════════════════════════════════════════════════════ -->
    1531                         <div id="bcs-smtp-panel-test" class="bcs-smtp-tab-panel">
    1532                                 <div class="bcs-smtp-grid">
    1533                                         <div>
    1534                                                 <div class="bcs-smtp-card">
    1535                                                         <div class="bcs-smtp-card-header">
    1536                                                                 <p class="bcs-smtp-card-title"><span class="dashicons dashicons-email"></span><?php esc_html_e( 'Send Test Email', 'bcs-smtp-manager' ); ?></p>
    1537                                                                 <p class="bcs-smtp-card-desc"><?php esc_html_e( "Trigger a real email send via WordPress's", 'bcs-smtp-manager' ); ?> <code>wp_mail()</code>. <?php esc_html_e( 'This validates the full SMTP stack and logs the result.', 'bcs-smtp-manager' ); ?></p>
    1538                                                         </div>
    1539                                                         <div class="bcs-smtp-card-body">
    1540                                                                 <div class="bcs-smtp-field">
    1541                                                                         <label class="bcs-smtp-field-label" for="bcs-smtp-test-to"><?php esc_html_e( 'Recipient address', 'bcs-smtp-manager' ); ?></label>
    1542                                                                         <input type="email" id="bcs-smtp-test-to" class="bcs-smtp-input" value="<?php echo esc_attr( get_option( 'admin_email' ) ); ?>" placeholder="recipient@example.com">
    1543                                                                 </div>
    1544 
    1545                                                                 <div style="margin-top:16px;display:flex;align-items:center;gap:12px;flex-wrap:wrap">
    1546                                                                         <button type="button" id="bcs-smtp-send-test-btn" class="bcs-smtp-btn bcs-smtp-btn-primary">
    1547                                                                                 <span class="dashicons dashicons-email-alt"></span><?php esc_html_e( 'Send Test Email', 'bcs-smtp-manager' ); ?>
    1548                                                                         </button>
    1549                                                                         <?php if ( $monitor_on ) : ?>
    1550                                                                         <button type="button" id="bcs-smtp-check-now-btn" class="bcs-smtp-btn bcs-smtp-btn-secondary">
    1551                                                                                 <span class="dashicons dashicons-update"></span><?php esc_html_e( 'Run Health Check Now', 'bcs-smtp-manager' ); ?>
    1552                                                                         </button>
    1553                                                                         <?php endif; ?>
    1554                                                                 </div>
    1555                                                                 <div id="bcs-smtp-test-msg" role="status" aria-live="polite" style="display:none;margin-top:12px"></div>
    1556                                                         </div>
    1557                                                 </div>
    1558                                         </div>
    1559 
    1560                                         <div>
    1561                                                 <div class="bcs-smtp-card">
    1562                                                         <div class="bcs-smtp-card-header">
    1563                                                                 <p class="bcs-smtp-card-title"><span class="dashicons dashicons-info-outline"></span><?php esc_html_e( 'What this tests', 'bcs-smtp-manager' ); ?></p>
    1564                                                                 <p class="bcs-smtp-card-desc"><?php esc_html_e( 'A real email is sent via', 'bcs-smtp-manager' ); ?> <code>wp_mail()</code> <?php esc_html_e( 'through your configured SMTP. The result is logged to the Email Log tab.', 'bcs-smtp-manager' ); ?></p>
    1565                                                         </div>
    1566                                                         <div class="bcs-smtp-card-body">
    1567                                                                 <div class="bcs-smtp-alert bcs-smtp-alert-info" style="margin-bottom:0">
    1568                                                                         <span class="dashicons dashicons-info-outline"></span>
    1569                                                                         <div class="bcs-smtp-alert-content" style="font-size:11.5px">
    1570                                                                                 <strong><?php esc_html_e( 'Failure types detected:', 'bcs-smtp-manager' ); ?></strong>
    1571                                                                                 <ul style="margin:6px 0 0 14px;padding:0">
    1572                                                                                         <li><strong><?php esc_html_e( 'Authentication Failure', 'bcs-smtp-manager' ); ?></strong> — <?php esc_html_e( 'Bad credentials / OAuth token', 'bcs-smtp-manager' ); ?></li>
    1573                                                                                         <li><strong><?php esc_html_e( 'SMTP Rejection', 'bcs-smtp-manager' ); ?></strong> — <?php esc_html_e( 'Server rejects the message', 'bcs-smtp-manager' ); ?></li>
    1574                                                                                         <li><strong><?php esc_html_e( 'Plugin / System Failure', 'bcs-smtp-manager' ); ?></strong> — <?php esc_html_e( 'Timeout, DNS, PHP error', 'bcs-smtp-manager' ); ?></li>
    1575                                                                                 </ul>
    1576                                                                         </div>
    1577                                                                 </div>
    1578                                                         </div>
    1579                                                 </div>
    1580                                         </div>
    1581                                 </div>
    1582                         </div>
    1583 
    1584                         <!-- ═══════════════════════════════════════════════════════════════
    1585                              PANEL: Alerts
    1586                              ═══════════════════════════════════════════════════════════════ -->
    1587                         <div id="bcs-smtp-panel-alerts" class="bcs-smtp-tab-panel">
    1588                                 <div class="bcs-smtp-grid">
    1589                                         <div>
    1590                                                 <!-- Section 1: Enable Automated Monitoring -->
    1591                                                 <div class="bcs-smtp-card">
    1592                                                 <div class="bcs-smtp-card-body">
    1593                                                         <div class="bcs-smtp-toggle-row" style="margin-bottom:0">
    1594                                                                 <div class="bcs-smtp-toggle-row-info">
    1595                                                                         <div class="bcs-smtp-toggle-row-label"><?php esc_html_e( 'Enable Automated Monitoring', 'bcs-smtp-manager' ); ?></div>
    1596                                                                         <div class="bcs-smtp-toggle-row-desc"><?php esc_html_e( 'Tests SMTP connectivity on a schedule and alerts on failure', 'bcs-smtp-manager' ); ?></div>
    1597                                                                 </div>
    1598                                                                 <label class="bcs-smtp-toggle">
    1599                                                                         <input type="checkbox" id="bcs-smtp-monitor-enabled" <?php checked( $monitor_on ); ?>>
    1600                                                                         <span class="bcs-smtp-toggle-slider"></span>
    1601                                                                 </label>
    1602                                                         </div>
    1603 
    1604                                                         <div id="bcs-smtp-monitor-accordion" class="bcs-smtp-accordion <?php echo $monitor_on ? 'open' : ''; ?>" style="margin-top:12px">
    1605                                                                 <button type="button" class="bcs-smtp-accordion-head">
    1606                                                                         <div class="bcs-smtp-accordion-head-left">
    1607                                                                                 <div class="bcs-smtp-accordion-head-icon" style="background:rgba(99,102,241,.15);color:var(--bcs-smtp-indigo)">
    1608                                                                                         <span class="dashicons dashicons-clock"></span>
    1609                                                                                 </div>
    1610                                                                                 <div>
    1611                                                                                         <div class="bcs-smtp-accordion-head-title"><?php esc_html_e( 'Monitor Settings', 'bcs-smtp-manager' ); ?></div>
    1612                                                                                         <div class="bcs-smtp-accordion-head-desc"><?php esc_html_e( 'Check interval & failure threshold', 'bcs-smtp-manager' ); ?></div>
    1613                                                                                 </div>
    1614                                                                         </div>
    1615                                                                         <span class="dashicons dashicons-arrow-down-alt2 bcs-smtp-accordion-chevron"></span>
    1616                                                                 </button>
    1617                                                                 <div class="bcs-smtp-accordion-body">
    1618                                                                         <div class="bcs-smtp-field">
    1619                                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-check-interval"><?php esc_html_e( 'Check every', 'bcs-smtp-manager' ); ?> <abbr title="<?php esc_attr_e( 'Minutes between automated SMTP tests', 'bcs-smtp-manager' ); ?>">N</abbr> <?php esc_html_e( 'minutes', 'bcs-smtp-manager' ); ?></label>
    1620                                                                                 <input type="number" id="bcs-smtp-check-interval" class="bcs-smtp-input" value="<?php echo esc_attr( $check_interval ); ?>" min="1" max="1440" style="max-width:100px">
    1621                                                                                 <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block"><?php esc_html_e( 'Default: 5 min. Changing this reschedules the cron event immediately.', 'bcs-smtp-manager' ); ?></small>
    1622                                                                         </div>
    1623                                                                         <div class="bcs-smtp-field">
    1624                                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-failure-threshold"><?php esc_html_e( 'Alert after', 'bcs-smtp-manager' ); ?> <abbr title="<?php esc_attr_e( 'Consecutive failures before alerting', 'bcs-smtp-manager' ); ?>">N</abbr> <?php esc_html_e( 'consecutive failures', 'bcs-smtp-manager' ); ?></label>
    1625                                                                                 <input type="number" id="bcs-smtp-failure-threshold" class="bcs-smtp-input" value="<?php echo esc_attr( $threshold ); ?>" min="1" max="10" style="max-width:100px">
    1626                                                                         </div>
    1627                                                                         <div class="bcs-smtp-field">
    1628                                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-max-alert-count"><?php esc_html_e( 'Stop after', 'bcs-smtp-manager' ); ?> <abbr title="<?php esc_attr_e( 'Maximum notifications per failure cycle. 0 = unlimited.', 'bcs-smtp-manager' ); ?>">N</abbr> <?php esc_html_e( 'notifications', 'bcs-smtp-manager' ); ?></label>
    1629                                                                                 <input type="number" id="bcs-smtp-max-alert-count" class="bcs-smtp-input" value="<?php echo esc_attr( (int) get_option( BCS_SMTP_OPT_MAX_ALERT_COUNT, 0 ) ); ?>" min="0" max="100" style="max-width:100px">
    1630                                                                                 <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block"><?php esc_html_e( '0 = unlimited — resets automatically when SMTP recovers', 'bcs-smtp-manager' ); ?></small>
    1631                                                                         </div>
    1632                                                                 </div>
    1633                                                         </div>
    1634 
    1635                                                         <div style="margin-top:14px">
    1636                                                                 <button type="button" id="bcs-smtp-save-monitor-btn" class="bcs-smtp-btn bcs-smtp-btn-primary">
    1637                                                                         <span class="dashicons dashicons-saved"></span><?php esc_html_e( 'Save Monitor Settings', 'bcs-smtp-manager' ); ?>
    1638                                                                 </button>
    1639                                                                 <div id="bcs-smtp-monitor-msg" role="status" aria-live="polite" style="display:none;margin-top:10px"></div>
    1640                                                         </div>
    1641                                                 </div>
    1642                                                 </div>
    1643 
    1644                                                 <!-- Section 2: Alert Channel -->
    1645                                                 <div class="bcs-smtp-card" style="margin-top:0">
    1646                                                 <div class="bcs-smtp-card-body">
    1647                                                         <div class="bcs-smtp-toggle-row" style="margin-bottom:0">
    1648                                                                 <div class="bcs-smtp-toggle-row-info">
    1649                                                                         <div class="bcs-smtp-toggle-row-label"><?php esc_html_e( 'Alert Channel', 'bcs-smtp-manager' ); ?></div>
    1650                                                                         <div class="bcs-smtp-toggle-row-desc"><?php esc_html_e( 'Send failure alerts via an independent notification channel', 'bcs-smtp-manager' ); ?></div>
    1651                                                                 </div>
    1652                                                                 <label class="bcs-smtp-toggle">
    1653                                                                         <input type="checkbox" id="bcs-smtp-alert-channel-enabled" <?php checked( $alert_enabled ); ?>>
    1654                                                                         <span class="bcs-smtp-toggle-slider"></span>
    1655                                                                 </label>
    1656                                                         </div>
    1657 
    1658                                                         <div id="bcs-smtp-alert-channel-accordion" class="bcs-smtp-accordion <?php echo $alert_enabled ? 'open' : ''; ?>" style="margin-top:12px">
    1659                                                                 <button type="button" class="bcs-smtp-accordion-head">
    1660                                                                         <div class="bcs-smtp-accordion-head-left">
    1661                                                                                 <div class="bcs-smtp-accordion-head-icon" style="background:rgba(239,68,68,.12);color:var(--bcs-smtp-rose)">
    1662                                                                                         <span class="dashicons dashicons-megaphone"></span>
    1663                                                                                 </div>
    1664                                                                                 <div>
    1665                                                                                         <div class="bcs-smtp-accordion-head-title"><?php esc_html_e( 'Channel Settings', 'bcs-smtp-manager' ); ?></div>
    1666                                                                                         <div class="bcs-smtp-accordion-head-desc"><?php esc_html_e( 'Slack or Microsoft Teams webhook', 'bcs-smtp-manager' ); ?></div>
    1667                                                                                 </div>
    1668                                                                         </div>
    1669                                                                         <span class="dashicons dashicons-arrow-down-alt2 bcs-smtp-accordion-chevron"></span>
    1670                                                                 </button>
    1671                                                                 <div class="bcs-smtp-accordion-body">
    1672                                                                         <div class="bcs-smtp-field">
    1673                                                                                 <label class="bcs-smtp-field-label"><?php esc_html_e( 'Alert delivery method', 'bcs-smtp-manager' ); ?></label>
    1674                                                                                 <div class="bcs-smtp-check-list" style="flex-direction:row;flex-wrap:wrap;gap:8px">
    1675                                                                                         <?php
    1676                                                                                         $channels = array(
    1677                                                                                                 'slack'       => array( 'icon' => '💬', 'label' => __( 'Slack', 'bcs-smtp-manager' ) ),
    1678                                                                                                 'teams'       => array( 'icon' => '🟦', 'label' => __( 'Teams', 'bcs-smtp-manager' ) ),
    1679                                                                                                 'google_chat' => array( 'icon' => '🟢', 'label' => __( 'Google Chat', 'bcs-smtp-manager' ) ),
    1680                                                                                                 'discord'     => array( 'icon' => '🟣', 'label' => __( 'Discord', 'bcs-smtp-manager' ) ),
    1681                                                                                                 'custom'      => array( 'icon' => '🔗', 'label' => __( 'Custom Webhook', 'bcs-smtp-manager' ) ),
    1682                                                                                         );
    1683                                                                                         foreach ( $channels as $val => $ch ) : ?>
    1684                                                                                         <label class="bcs-smtp-check-item" style="flex-direction:row;align-items:center;gap:8px;padding:10px 16px;min-width:90px">
    1685                                                                                                 <input type="radio" name="bcs_alert_channel" class="bcs-smtp-channel-radio" value="<?php echo esc_attr( $val ); ?>" <?php checked( $alert_channel, $val ); ?> style="width:20px;height:20px;flex-shrink:0">
    1686                                                                                                 <span style="font-size:16px"><?php echo esc_html( $ch['icon'] ); ?></span>
    1687                                                                                                 <div class="bcs-smtp-check-label-title"><?php echo esc_html( $ch['label'] ); ?></div>
    1688                                                                                         </label>
    1689                                                                                         <?php endforeach; ?>
    1690                                                                                 </div>
    1691                                                                         </div>
    1692 
    1693                                                                         <div id="bcs-smtp-slack-webhook-row" class="bcs-smtp-field" style="display:none">
    1694                                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-slack-webhook-url"><?php esc_html_e( 'Incoming Webhook URL', 'bcs-smtp-manager' ); ?></label>
    1695                                                                                 <input type="url" id="bcs-smtp-slack-webhook-url" class="bcs-smtp-input"
    1696                                                                                         value="<?php echo esc_attr( get_option( BCS_SMTP_OPT_SLACK_WEBHOOK_URL, '' ) ); ?>"
    1697                                                                                         placeholder="https://hooks.slack.com/services/…">
    1698                                                                                 <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block">
    1699                                                                                         <em><?php esc_html_e( 'Apps → Incoming Webhooks', 'bcs-smtp-manager' ); ?></em> —
    1700                                                                                         <a href="https://api.slack.com/messaging/webhooks" target="_blank" rel="noopener"><?php esc_html_e( 'Setup guide ↗', 'bcs-smtp-manager' ); ?></a>
    1701                                                                                 </small>
    1702                                                                         </div>
    1703                                                                         <div id="bcs-smtp-teams-webhook-row" class="bcs-smtp-field" style="display:none">
    1704                                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-teams-webhook-url"><?php esc_html_e( 'Incoming Webhook URL', 'bcs-smtp-manager' ); ?></label>
    1705                                                                                 <input type="url" id="bcs-smtp-teams-webhook-url" class="bcs-smtp-input"
    1706                                                                                         value="<?php echo esc_attr( get_option( BCS_SMTP_OPT_TEAMS_WEBHOOK_URL, '' ) ); ?>"
    1707                                                                                         placeholder="https://outlook.office.com/webhook/…">
    1708                                                                                 <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block">
    1709                                                                                         <em><?php esc_html_e( 'Channel → Connectors → Incoming Webhook', 'bcs-smtp-manager' ); ?></em> —
    1710                                                                                         <a href="https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook" target="_blank" rel="noopener"><?php esc_html_e( 'Setup guide ↗', 'bcs-smtp-manager' ); ?></a>
    1711                                                                                 </small>
    1712                                                                         </div>
    1713                                                                         <div id="bcs-smtp-google-chat-webhook-row" class="bcs-smtp-field" style="display:none">
    1714                                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-google-chat-webhook-url"><?php esc_html_e( 'Incoming Webhook URL', 'bcs-smtp-manager' ); ?></label>
    1715                                                                                 <input type="url" id="bcs-smtp-google-chat-webhook-url" class="bcs-smtp-input"
    1716                                                                                         value="<?php echo esc_attr( get_option( BCS_SMTP_OPT_GOOGLE_CHAT_WEBHOOK_URL, '' ) ); ?>"
    1717                                                                                         placeholder="https://chat.googleapis.com/v1/spaces/…/messages?key=…&token=…">
    1718                                                                                 <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block">
    1719                                                                                         <em><?php esc_html_e( 'Space → Apps & integrations → Webhooks', 'bcs-smtp-manager' ); ?></em> —
    1720                                                                                         <a href="https://developers.google.com/workspace/chat/quickstart/webhooks" target="_blank" rel="noopener"><?php esc_html_e( 'Setup guide ↗', 'bcs-smtp-manager' ); ?></a>
    1721                                                                                 </small>
    1722                                                                         </div>
    1723                                                                         <div id="bcs-smtp-discord-webhook-row" class="bcs-smtp-field" style="display:none">
    1724                                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-discord-webhook-url"><?php esc_html_e( 'Incoming Webhook URL', 'bcs-smtp-manager' ); ?></label>
    1725                                                                                 <input type="url" id="bcs-smtp-discord-webhook-url" class="bcs-smtp-input"
    1726                                                                                         value="<?php echo esc_attr( get_option( BCS_SMTP_OPT_DISCORD_WEBHOOK_URL, '' ) ); ?>"
    1727                                                                                         placeholder="https://discord.com/api/webhooks/…/…">
    1728                                                                                 <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block">
    1729                                                                                         <em><?php esc_html_e( 'Server Settings → Integrations → Webhooks', 'bcs-smtp-manager' ); ?></em> —
    1730                                                                                         <a href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks" target="_blank" rel="noopener"><?php esc_html_e( 'Setup guide ↗', 'bcs-smtp-manager' ); ?></a>
    1731                                                                                 </small>
    1732                                                                         </div>
    1733                                                                         <div id="bcs-smtp-custom-webhook-row" class="bcs-smtp-field" style="display:none">
    1734                                                                                 <label class="bcs-smtp-field-label" for="bcs-smtp-custom-webhook-url"><?php esc_html_e( 'Webhook URL', 'bcs-smtp-manager' ); ?></label>
    1735                                                                                 <input type="url" id="bcs-smtp-custom-webhook-url" class="bcs-smtp-input"
    1736                                                                                         value="<?php echo esc_attr( get_option( BCS_SMTP_OPT_CUSTOM_WEBHOOK_URL, '' ) ); ?>"
    1737                                                                                         placeholder="https://example.com/your-webhook-endpoint">
    1738                                                                                 <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block">
    1739                                                                                         <?php esc_html_e( 'Sends a structured JSON payload (event, site, error details, failure count) — use this with Zapier, Make, n8n, or your own endpoint.', 'bcs-smtp-manager' ); ?>
    1740                                                                                 </small>
    1741                                                                         </div>
    1742                                                                 </div>
    1743                                                         </div>
    1744 
    1745                                                         <div style="margin-top:14px;display:flex;gap:10px;flex-wrap:wrap;align-items:center">
    1746                                                                 <button type="button" id="bcs-smtp-save-alerts-btn" class="bcs-smtp-btn bcs-smtp-btn-primary">
    1747                                                                         <span class="dashicons dashicons-saved"></span><?php esc_html_e( 'Save Alert Settings', 'bcs-smtp-manager' ); ?>
    1748                                                                 </button>
    1749                                                                 <button type="button" id="bcs-smtp-test-alert-btn" class="bcs-smtp-btn bcs-smtp-btn-secondary">
    1750                                                                         <span class="dashicons dashicons-megaphone"></span><?php esc_html_e( 'Send Test Alert', 'bcs-smtp-manager' ); ?>
    1751                                                                 </button>
    1752                                                         </div>
    1753                                                         <div id="bcs-smtp-alerts-msg" role="status" aria-live="polite" style="display:none;margin-top:10px"></div>
    1754                                                 </div>
    1755                                                 </div>
    1756                                         </div>
    1757 
    1758                                         <div>
    1759                                                 <div class="bcs-smtp-card">
    1760                                                         <div class="bcs-smtp-card-header">
    1761                                                                 <p class="bcs-smtp-card-title"><span class="dashicons dashicons-info-outline"></span><?php esc_html_e( 'How monitoring works', 'bcs-smtp-manager' ); ?></p>
    1762                                                         </div>
    1763                                                         <div class="bcs-smtp-card-body">
    1764                                                                 <div class="bcs-smtp-field-group" style="gap:14px">
    1765                                                                         <div class="bcs-smtp-server-row" style="border-radius:var(--bcs-smtp-radius);padding:12px 14px">
    1766                                                                                 <span class="bcs-smtp-server-key" style="font-size:12px;font-weight:600;color:var(--bcs-smtp-indigo-h)">⏱ <?php esc_html_e( 'TCP-only test', 'bcs-smtp-manager' ); ?></span>
    1767                                                                                 <span class="bcs-smtp-server-val" style="font-size:12px;color:var(--bcs-smtp-text-2);font-family:var(--bcs-smtp-font);margin-top:4px"><?php esc_html_e( 'WP-Cron connects to your SMTP server via TCP — no email is sent on successful checks.', 'bcs-smtp-manager' ); ?></span>
    1768                                                                         </div>
    1769                                                                         <div class="bcs-smtp-server-row" style="border-radius:var(--bcs-smtp-radius);padding:12px 14px">
    1770                                                                                 <span class="bcs-smtp-server-key" style="font-size:12px;font-weight:600;color:var(--bcs-smtp-indigo-h)">🔔 <?php esc_html_e( 'Failure-only alerts', 'bcs-smtp-manager' ); ?></span>
    1771                                                                                 <span class="bcs-smtp-server-val" style="font-size:12px;color:var(--bcs-smtp-text-2);font-family:var(--bcs-smtp-font);margin-top:4px"><?php esc_html_e( 'Alerts only fire when consecutive failures hit your threshold. No noise on healthy checks.', 'bcs-smtp-manager' ); ?></span>
    1772                                                                         </div>
    1773                                                                         <div class="bcs-smtp-server-row" style="border-radius:var(--bcs-smtp-radius);padding:12px 14px">
    1774                                                                                 <span class="bcs-smtp-server-key" style="font-size:12px;font-weight:600;color:var(--bcs-smtp-indigo-h)">🔁 <?php esc_html_e( 'Auto-reset', 'bcs-smtp-manager' ); ?></span>
    1775                                                                                 <span class="bcs-smtp-server-val" style="font-size:12px;color:var(--bcs-smtp-text-2);font-family:var(--bcs-smtp-font);margin-top:4px"><?php esc_html_e( 'When delivery recovers, the failure counter resets automatically.', 'bcs-smtp-manager' ); ?></span>
    1776                                                                         </div>
    1777                                                                 </div>
    1778                                                                 <div class="bcs-smtp-alert bcs-smtp-alert-info" style="margin-top:14px;margin-bottom:0">
    1779                                                                         <span class="dashicons dashicons-info-outline"></span>
    1780                                                                         <div class="bcs-smtp-alert-content" style="font-size:11.5px"><?php esc_html_e( 'WP-Cron fires on page load. For accurate intervals, point a real server', 'bcs-smtp-manager' ); ?> <code>crontab</code> <?php esc_html_e( 'at', 'bcs-smtp-manager' ); ?> <code>wp-cron.php</code>.</div>
    1781                                                                 </div>
    1782                                                         </div>
    1783                                                 </div>
    1784                                                 <div class="bcs-smtp-card">
    1785                                                         <div class="bcs-smtp-card-header">
    1786                                                                 <p class="bcs-smtp-card-title"><span class="dashicons dashicons-shield"></span><?php esc_html_e( 'Alert message includes', 'bcs-smtp-manager' ); ?></p>
    1787                                                         </div>
    1788                                                         <div class="bcs-smtp-card-body">
    1789                                                                 <div class="bcs-smtp-check-list" style="gap:6px">
    1790                                                                         <?php
    1791                                                                         $fields = array(
    1792                                                                                 __( 'Date and time of failure', 'bcs-smtp-manager' ),
    1793                                                                                 __( 'Sending mailbox (from address)', 'bcs-smtp-manager' ),
    1794                                                                                 __( 'SMTP host & port', 'bcs-smtp-manager' ),
    1795                                                                                 __( 'SMTP plugin detected', 'bcs-smtp-manager' ),
    1796                                                                                 __( 'Raw error message', 'bcs-smtp-manager' ),
    1797                                                                                 __( 'Source IP address', 'bcs-smtp-manager' ),
    1798                                                                                 __( 'Failure classification', 'bcs-smtp-manager' ),
    1799                                                                                 __( 'Consecutive failure count', 'bcs-smtp-manager' ),
    1800                                                                                 __( 'External provider status note', 'bcs-smtp-manager' ),
    1801                                                                         );
    1802                                                                         foreach ( $fields as $f ) : ?>
    1803                                                                         <div style="display:flex;align-items:center;gap:8px;font-size:12px;color:var(--bcs-smtp-text-2);padding:4px 0">
    1804                                                                                 <span style="color:var(--bcs-smtp-emerald);font-size:14px">✓</span><?php echo wp_kses_post( $f ); ?>
    1805                                                                         </div>
    1806                                                                         <?php endforeach; ?>
    1807                                                                 </div>
    1808                                                         </div>
    1809                                                 </div>
    1810                                         </div>
    1811                                 </div>
    1812                         </div>
    1813 
    1814                         <!-- ═══════════════════════════════════════════════════════════════
    1815                              PANEL: Config
    1816                              ═══════════════════════════════════════════════════════════════ -->
    1817                         <div id="bcs-smtp-panel-config" class="bcs-smtp-tab-panel">
    1818                                 <div class="bcs-smtp-grid">
    1819                                         <div>
    1820                                                 <div class="bcs-smtp-card">
    1821                                                         <div class="bcs-smtp-card-header">
    1822                                                                 <p class="bcs-smtp-card-title"><span class="dashicons dashicons-admin-generic"></span><?php esc_html_e( 'Detected SMTP Configuration', 'bcs-smtp-manager' ); ?></p>
    1823                                                                 <p class="bcs-smtp-card-desc"><?php esc_html_e( 'Auto-detected from your active SMTP configuration. Credentials are never stored by this plugin beyond what you enter in Settings.', 'bcs-smtp-manager' ); ?></p>
    1824                                                         </div>
    1825                                                         <div class="bcs-smtp-card-body">
    1826                                                                 <div class="bcs-smtp-server-grid">
    1827                                                                         <div class="bcs-smtp-server-row">
    1828                                                                                 <span class="bcs-smtp-server-key"><?php esc_html_e( 'Plugin', 'bcs-smtp-manager' ); ?></span>
    1829                                                                                 <span class="bcs-smtp-server-val"><?php echo esc_html( $info['plugin'] ); ?></span>
    1830                                                                         </div>
    1831                                                                         <div class="bcs-smtp-server-row">
    1832                                                                                 <span class="bcs-smtp-server-key"><?php esc_html_e( 'Version', 'bcs-smtp-manager' ); ?></span>
    1833                                                                                 <span class="bcs-smtp-server-val"><?php echo esc_html( $info['version'] ); ?></span>
    1834                                                                         </div>
    1835                                                                         <div class="bcs-smtp-server-row">
    1836                                                                                 <span class="bcs-smtp-server-key"><?php esc_html_e( 'SMTP Host', 'bcs-smtp-manager' ); ?></span>
    1837                                                                                 <span class="bcs-smtp-server-val bcs-smtp-mono"><?php echo esc_html( $info['host'] ?: '—' ); ?></span>
    1838                                                                         </div>
    1839                                                                         <div class="bcs-smtp-server-row">
    1840                                                                                 <span class="bcs-smtp-server-key"><?php esc_html_e( 'Port', 'bcs-smtp-manager' ); ?></span>
    1841                                                                                 <span class="bcs-smtp-server-val bcs-smtp-mono"><?php echo esc_html( $info['port'] ?: '—' ); ?></span>
    1842                                                                         </div>
    1843                                                                         <div class="bcs-smtp-server-row">
    1844                                                                                 <span class="bcs-smtp-server-key"><?php esc_html_e( 'Encryption', 'bcs-smtp-manager' ); ?></span>
    1845                                                                                 <span class="bcs-smtp-server-val"><?php echo esc_html( strtoupper( $info['encryption'] ?: '—' ) ); ?></span>
    1846                                                                         </div>
    1847                                                                         <div class="bcs-smtp-server-row">
    1848                                                                                 <span class="bcs-smtp-server-key"><?php esc_html_e( 'From mailbox', 'bcs-smtp-manager' ); ?></span>
    1849                                                                                 <span class="bcs-smtp-server-val bcs-smtp-mono" style="font-size:11px;word-break:break-all"><?php echo esc_html( $info['mailbox'] ); ?></span>
    1850                                                                         </div>
    1851                                                                         <div class="bcs-smtp-server-row">
    1852                                                                                 <span class="bcs-smtp-server-key"><?php esc_html_e( 'Auth method', 'bcs-smtp-manager' ); ?></span>
    1853                                                                                 <span class="bcs-smtp-server-val"><?php echo esc_html( $info['auth_type'] ); ?></span>
    1854                                                                         </div>
    1855                                                                         <div class="bcs-smtp-server-row">
    1856                                                                                 <span class="bcs-smtp-server-key"><?php esc_html_e( 'WordPress TZ', 'bcs-smtp-manager' ); ?></span>
    1857                                                                                 <span class="bcs-smtp-server-val"><?php echo esc_html( wp_timezone_string() ); ?></span>
    1858                                                                         </div>
    1859                                                                 </div>
    1860                                                         </div>
    1861                                                 </div>
    1862                                         </div>
    1863 
    1864                                         <div>
    1865                                                 <div class="bcs-smtp-card">
    1866                                                         <div class="bcs-smtp-card-header">
    1867                                                                 <p class="bcs-smtp-card-title"><span class="dashicons dashicons-shield"></span><?php esc_html_e( 'What is tracked', 'bcs-smtp-manager' ); ?></p>
    1868                                                         </div>
    1869                                                         <div class="bcs-smtp-card-body">
    1870                                                                 <div class="bcs-smtp-field-group" style="gap:6px">
    1871                                                                         <?php
    1872                                                                         $tracked = array(
    1873                                                                                 sprintf(
    1874                                                                                         /* translators: %s: wp_mail() function name wrapped in <code> tags. */
    1875                                                                                         __( 'All %s sends (subject, from, to, status)', 'bcs-smtp-manager' ),
    1876                                                                                         '<code>wp_mail()</code>'
    1877                                                                                 ),
    1878                                                                                 __( 'SMTP plugin detected', 'bcs-smtp-manager' ),
    1879                                                                                 __( 'SMTP hostname & port', 'bcs-smtp-manager' ),
    1880                                                                                 __( 'Encryption method (TLS / SSL)', 'bcs-smtp-manager' ),
    1881                                                                                 __( 'From mailbox (sender address)', 'bcs-smtp-manager' ),
    1882                                                                                 __( 'Authentication type', 'bcs-smtp-manager' ),
    1883                                                                         );
    1884                                                                         foreach ( $tracked as $t ) : ?>
    1885                                                                         <div style="display:flex;align-items:center;gap:8px;font-size:12px;color:var(--bcs-smtp-text-2);padding:4px 0">
    1886                                                                                 <span style="color:var(--bcs-smtp-indigo-h)">◈</span><?php echo wp_kses_post( $t ); ?>
    1887                                                                         </div>
    1888                                                                         <?php endforeach; ?>
    1889                                                                 </div>
    1890                                                                 <div class="bcs-smtp-alert bcs-smtp-alert-warning" style="margin-top:14px;margin-bottom:0">
    1891                                                                         <span class="dashicons dashicons-warning"></span>
    1892                                                                         <div class="bcs-smtp-alert-content" style="font-size:11.5px"><?php esc_html_e( 'Email body content is', 'bcs-smtp-manager' ); ?> <strong><?php esc_html_e( 'never', 'bcs-smtp-manager' ); ?></strong> <?php esc_html_e( 'stored by default — only metadata. The optional Resend feature below is the sole exception.', 'bcs-smtp-manager' ); ?></div>
    1893                                                                 </div>
    1894                                                         </div>
    1895                                                 </div>
    1896 
    1897                                                 <div class="bcs-smtp-card">
    1898                                                         <div class="bcs-smtp-card-header">
    1899                                                                 <p class="bcs-smtp-card-title"><span class="dashicons dashicons-controls-repeat"></span><?php esc_html_e( 'Failed Email Resend', 'bcs-smtp-manager' ); ?></p>
    1900                                                                 <p class="bcs-smtp-card-desc"><?php esc_html_e( 'When enabled, the content of an email is stored only if it fails to send, so you can resend it from the Email Log. Off by default.', 'bcs-smtp-manager' ); ?></p>
    1901                                                         </div>
    1902                                                         <div class="bcs-smtp-card-body">
    1903                                                                 <div class="bcs-smtp-toggle-row" style="margin-bottom:14px">
    1904                                                                         <div class="bcs-smtp-toggle-row-info">
    1905                                                                                 <div class="bcs-smtp-toggle-row-label"><?php esc_html_e( 'Store failed email content', 'bcs-smtp-manager' ); ?></div>
    1906                                                                                 <div class="bcs-smtp-toggle-row-desc"><?php esc_html_e( 'Required for the Resend button to work — has no effect on emails that send successfully', 'bcs-smtp-manager' ); ?></div>
    1907                                                                         </div>
    1908                                                                         <label class="bcs-smtp-toggle">
    1909                                                                                 <input type="checkbox" id="bcs-smtp-store-failed-body" <?php checked( $store_failed_body ); ?>>
    1910                                                                                 <span class="bcs-smtp-toggle-slider"></span>
    1911                                                                         </label>
    1912                                                                 </div>
    1913                                                                 <div class="bcs-smtp-field">
    1914                                                                         <label class="bcs-smtp-field-label" for="bcs-smtp-failed-body-retention"><?php esc_html_e( 'Retention (days)', 'bcs-smtp-manager' ); ?></label>
    1915                                                                         <input type="number" id="bcs-smtp-failed-body-retention" class="bcs-smtp-input" style="max-width:120px" value="<?php echo esc_attr( $failed_body_retention ); ?>" min="1" max="30">
    1916                                                                         <small style="color:var(--bcs-smtp-text-3);font-size:11px;margin-top:4px;display:block"><?php esc_html_e( 'Stored content is auto-purged after this many days, independent of the overall log retention setting.', 'bcs-smtp-manager' ); ?></small>
    1917                                                                 </div>
    1918                                                                 <div style="margin-top:14px">
    1919                                                                         <button type="button" id="bcs-smtp-save-resend-btn" class="bcs-smtp-btn bcs-smtp-btn-primary bcs-smtp-btn-sm">
    1920                                                                                 <span class="dashicons dashicons-saved"></span><?php esc_html_e( 'Save', 'bcs-smtp-manager' ); ?>
    1921                                                                         </button>
    1922                                                                         <div id="bcs-smtp-resend-settings-msg" role="status" aria-live="polite" style="display:none;margin-top:10px;width:100%"></div>
    1923                                                                 </div>
    1924                                                         </div>
    1925                                                 </div>
    1926 
    1927                                                 <div class="bcs-smtp-card">
    1928                                                         <div class="bcs-smtp-card-header">
    1929                                                                 <p class="bcs-smtp-card-title"><span class="dashicons dashicons-admin-links"></span><?php esc_html_e( 'Quick links', 'bcs-smtp-manager' ); ?></p>
    1930                                                         </div>
    1931                                                         <div class="bcs-smtp-card-body">
    1932                                                                 <div class="bcs-smtp-field-group">
    1933                                                                         <a href="https://admin.microsoft.com/" target="_blank" rel="noopener" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm bcs-smtp-btn-full">
    1934                                                                                 <span class="dashicons dashicons-external"></span> <?php esc_html_e( 'Microsoft 365 Admin', 'bcs-smtp-manager' ); ?>
    1935                                                                         </a>
    1936                                                                         <a href="https://status.office365.com/" target="_blank" rel="noopener" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm bcs-smtp-btn-full">
    1937                                                                                 <span class="dashicons dashicons-external"></span> <?php esc_html_e( 'Microsoft 365 Status', 'bcs-smtp-manager' ); ?>
    1938                                                                         </a>
    1939                                                                         <a href="https://api.slack.com/messaging/webhooks" target="_blank" rel="noopener" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm bcs-smtp-btn-full">
    1940                                                                                 <span class="dashicons dashicons-external"></span> <?php esc_html_e( 'Slack Webhook Guide', 'bcs-smtp-manager' ); ?>
    1941                                                                         </a>
    1942                                                                         <a href="https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook" target="_blank" rel="noopener" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm bcs-smtp-btn-full">
    1943                                                                                 <span class="dashicons dashicons-external"></span> <?php esc_html_e( 'Teams Webhook Guide', 'bcs-smtp-manager' ); ?>
    1944                                                                         </a>
    1945                                                                 </div>
    1946                                                         </div>
    1947                                                 </div>
    1948                                         </div>
    1949                                 </div>
    1950                         </div>
     2525                        </section>
    19512526
    19522527                </div><!-- /.bcs-smtp-wrap -->
  • bcs-smtp-manager/trunk/assets/css/admin.css

    r3652946 r3762759  
    11/* =====================================================================
    2    CSS Variables - Dark Theme (default)
    3    ===================================================================== */
    4 :root,
    5 [data-bcs-smtp-theme="dark"] {
    6   --bcs-smtp-bg:#0f1117;--bcs-smtp-surface:#181c27;--bcs-smtp-surface-2:#1e2333;--bcs-smtp-surface-3:#252a3d;
    7   --bcs-smtp-border:rgba(255,255,255,.07);--bcs-smtp-border-light:rgba(255,255,255,.12);
    8   --bcs-smtp-indigo:#6366f1;--bcs-smtp-indigo-h:#818cf8;--bcs-smtp-indigo-dim:rgba(99,102,241,.15);--bcs-smtp-indigo-glow:rgba(99,102,241,.25);
    9   --bcs-smtp-emerald:#10b981;--bcs-smtp-emerald-dim:rgba(16,185,129,.15);
    10   --bcs-smtp-amber:#f59e0b;--bcs-smtp-amber-dim:rgba(245,158,11,.12);
    11   --bcs-smtp-rose:#f43f5e;--bcs-smtp-rose-dim:rgba(244,63,94,.12);
    12   --bcs-smtp-sky:#38bdf8;
    13   --bcs-smtp-text:#e2e8f0;--bcs-smtp-text-2:#94a3b8;--bcs-smtp-text-3:#64748b;
    14   --bcs-smtp-font:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;--bcs-smtp-mono:ui-monospace,'Courier New',monospace;
    15   --bcs-smtp-radius:10px;--bcs-smtp-radius-lg:16px;--bcs-smtp-radius-sm:6px;
    16   --bcs-smtp-shadow:0 4px 24px rgba(0,0,0,.4);--bcs-smtp-t:.18s cubic-bezier(.4,0,.2,1);
    17   --bcs-smtp-admin-bg:#0f1117;
    18 }
    19 
    20 html.wp-toolbar,body.wp-admin,#wpwrap,#wpcontent,#wpbody,#wpbody-content,#wpfooter { background:var(--bcs-smtp-admin-bg)!important; }
     2   ByteCoreStack - SMTP & Health Monitor — admin UI
     3   Dark, layered surfaces; #1450c4 for the primary action and active state.
     4   ===================================================================== */
     5
     6@font-face{font-family:"BCS Outfit";src:url(../fonts/outfit-variable.woff2) format("woff2");font-weight:100 900;font-style:normal;font-display:swap}
     7@font-face{font-family:"BCS Geist Mono";src:url(../fonts/geist-mono-variable.woff2) format("woff2");font-weight:100 900;font-style:normal;font-display:swap}
     8
     9:root{
     10  --bcs-bg:#0a0e17;
     11  --bcs-panel:#111725;
     12  --bcs-raised:#161d2d;
     13  --bcs-field:#0d1320;
     14  --bcs-field-hover:#1a2234;
     15  --bcs-seam:rgba(255,255,255,.07);
     16  --bcs-seam-strong:rgba(255,255,255,.12);
     17  --bcs-ink:#eef2f9;
     18  --bcs-ink-2:#a7b2c7;
     19  --bcs-ink-3:#808ca4;
     20  --bcs-brand:#1450c4;
     21  --bcs-brand-hover:#1c5fdc;
     22  --bcs-brand-press:#0f3f9e;
     23  --bcs-brand-ink:#8db4ff;
     24  --bcs-brand-wash:rgba(20,80,196,.16);
     25  --bcs-ok:#34d399;
     26  --bcs-bad:#fb7185;
     27  --bcs-warn:#fbbf24;
     28  --bcs-off:#808ca4;
     29  /* legacy aliases used by the other tabs */
     30  --bcs-sent:var(--bcs-ok);--bcs-back:var(--bcs-bad);--bcs-held:var(--bcs-warn);
     31  --bcs-font:"BCS Outfit",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
     32  --bcs-mono:"BCS Geist Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
     33  --bcs-r:14px;
     34  --bcs-r-sm:8px;
     35  --bcs-shadow:inset 0 1px 0 rgba(255,255,255,.045),0 1px 2px rgba(0,0,0,.35),0 12px 32px -16px rgba(0,0,0,.65);
     36  --bcs-ease:cubic-bezier(.22,1,.36,1);
     37  --bcs-t:.18s var(--bcs-ease);
     38}
     39
     40/* WordPress admin chrome on this screen only (stylesheet is enqueued for this page). */
     41html.wp-toolbar,body.wp-admin,#wpwrap,#wpcontent,#wpbody,#wpbody-content{background:var(--bcs-bg)!important}
     42#wpfooter{color:var(--bcs-ink-3)}
     43#wpfooter a{color:var(--bcs-brand-ink)}
    2144
    2245.bcs-smtp-wrap,.bcs-smtp-wrap *{box-sizing:border-box}
    23 .bcs-smtp-wrap{font-family:var(--bcs-smtp-font);color:var(--bcs-smtp-text);max-width:none;width:100%;margin:0;padding:20px 20px 20px 0;overflow-x:hidden}
    24 .bcs-smtp-wrap h1,.bcs-smtp-wrap h2,.bcs-smtp-wrap h3{font-family:var(--bcs-smtp-font);color:var(--bcs-smtp-text)}
     46.bcs-smtp-wrap{
     47  font-family:var(--bcs-font);font-size:14px;line-height:1.55;color:var(--bcs-ink);
     48  max-width:1320px;margin:0;padding:28px 24px 56px 8px;
     49  font-variant-numeric:tabular-nums;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
     50  accent-color:var(--bcs-brand);color-scheme:dark;
     51}
    2552.bcs-smtp-wrap .notice{display:none!important}
    26 .bcs-smtp-mono{font-family:var(--bcs-smtp-mono)}
     53.bcs-smtp-wrap ::selection{background:rgba(141,180,255,.32);color:#fff}
     54.bcs-smtp-wrap h1,.bcs-smtp-wrap h2,.bcs-smtp-wrap h3{font-family:var(--bcs-font);color:var(--bcs-ink);margin:0;padding:0;letter-spacing:-.015em}
     55.bcs-smtp-wrap p{margin:0;font-size:14px}
     56.bcs-smtp-wrap a{color:var(--bcs-brand-ink);text-underline-offset:3px}
     57.bcs-smtp-wrap a:hover{color:#b3ccff}
     58.bcs-smtp-wrap code{font-family:var(--bcs-mono);font-size:.86em;background:var(--bcs-field);color:var(--bcs-ink);padding:1px 5px;border-radius:4px}
     59.bcs-smtp-wrap [hidden]{display:none!important}
     60.bcs-smtp-mono{font-family:var(--bcs-mono);font-size:12.5px;letter-spacing:-.01em}
     61.bcs-smtp-wrap :focus{outline:none;box-shadow:none}
     62.bcs-smtp-wrap :focus-visible{outline:2px solid var(--bcs-brand-ink);outline-offset:2px;border-radius:var(--bcs-r-sm)}
     63.bcs-smtp-wrap *{scrollbar-width:thin;scrollbar-color:var(--bcs-seam-strong) transparent}
    2764
    2865/* =====================================================================
    2966   Header
    3067   ===================================================================== */
    31 .bcs-smtp-header{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;padding:20px 24px;background:linear-gradient(135deg,var(--bcs-smtp-surface-2) 0%,#1a1f33 100%);border:1px solid var(--bcs-smtp-border);border-radius:var(--bcs-smtp-radius-lg);margin-bottom:24px}
    32 .bcs-smtp-header-left{display:flex;align-items:center;gap:14px;min-width:0}
    33 .bcs-smtp-header-right{display:flex;align-items:center;gap:10px}
    34 .bcs-smtp-header-icon{width:44px;height:44px;flex-shrink:0;background:var(--bcs-smtp-indigo-dim);border:1px solid var(--bcs-smtp-indigo);border-radius:var(--bcs-smtp-radius);display:flex;align-items:center;justify-content:center;box-shadow:0 0 20px var(--bcs-smtp-indigo-glow)}
    35 .bcs-smtp-header-icon .dashicons{color:var(--bcs-smtp-indigo);font-size:22px;width:22px;height:22px}
    36 .bcs-smtp-header h1{font-size:22px;font-weight:600;margin:0;color:var(--bcs-smtp-text);line-height:1}
    37 .bcs-smtp-header-sub{font-size:13px;color:var(--bcs-smtp-text-2);margin:3px 0 0;font-weight:400}
    38 .bcs-smtp-header-badge{font-size:11px;font-family:var(--bcs-smtp-mono);background:var(--bcs-smtp-surface-3);border:1px solid var(--bcs-smtp-border);color:var(--bcs-smtp-text-3);padding:4px 10px;border-radius:100px;white-space:nowrap}
    39 @media(max-width:600px){
    40   .bcs-smtp-header-right{width:100%;justify-content:flex-start;padding-left:58px}
    41 }
    42 
    43 /* =====================================================================
    44    Nav Tabs
    45    ===================================================================== */
    46 .bcs-smtp-tabs{display:flex;gap:4px;margin-bottom:24px;border-bottom:1px solid var(--bcs-smtp-border);overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;scrollbar-width:thin;scrollbar-color:var(--bcs-smtp-surface-3) transparent}
    47 .bcs-smtp-tabs::-webkit-scrollbar{height:6px}
    48 .bcs-smtp-tabs::-webkit-scrollbar-track{background:transparent}
    49 .bcs-smtp-tabs::-webkit-scrollbar-thumb{background:var(--bcs-smtp-surface-3);border-radius:10px}
    50 .bcs-smtp-tab{display:inline-flex;align-items:center;gap:7px;padding:13px 18px;font-size:13px;font-weight:500;color:var(--bcs-smtp-text-2);border:none;background:transparent;border-bottom:4px solid transparent;margin-bottom:-1px;cursor:pointer;transition:color var(--bcs-smtp-t),border-color var(--bcs-smtp-t);font-family:var(--bcs-smtp-font);text-decoration:none;white-space:nowrap;flex-shrink:0}
    51 .bcs-smtp-tab:hover{color:var(--bcs-smtp-text)}
    52 .bcs-smtp-tab.active{color:var(--bcs-smtp-indigo-h);font-weight:600;border-bottom:4px solid var(--bcs-smtp-indigo)}
    53 .bcs-smtp-tab .dashicons{font-size:16px;width:16px;height:16px}
     68.bcs-smtp-header{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:22px}
     69.bcs-smtp-brand{display:flex;align-items:center;gap:14px;min-width:0}
     70.bcs-smtp-brand-mark{display:grid;place-items:center;width:42px;height:42px;flex-shrink:0;border-radius:11px;background:var(--bcs-brand);box-shadow:inset 0 1px 0 rgba(255,255,255,.18),0 6px 16px -6px rgba(20,80,196,.7)}
     71.bcs-smtp-brand-mark svg{width:22px;height:22px;fill:none;stroke:#fff;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
     72.bcs-smtp-header h1{font-size:21px;font-weight:600;line-height:1.2;letter-spacing:-.02em}
     73.bcs-smtp-header-sub{font-size:13px!important;color:var(--bcs-ink-3);margin-top:2px!important}
     74.bcs-smtp-header-actions{display:flex;gap:10px}
     75
     76/* =====================================================================
     77   Tabs
     78   ===================================================================== */
     79.bcs-smtp-tabs{position:relative;display:flex;gap:4px;width:fit-content;max-width:100%;padding:5px;margin-bottom:24px;background:var(--bcs-panel);border:1px solid var(--bcs-seam);border-radius:12px;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;scrollbar-width:none}
     80.bcs-smtp-tabs::-webkit-scrollbar{display:none}
     81.bcs-smtp-tab{
     82  appearance:none;border:0;background:none;cursor:pointer;flex-shrink:0;white-space:nowrap;
     83  display:inline-flex;align-items:center;gap:8px;height:38px;padding:0 14px;margin:0;
     84  font:500 14px/1 var(--bcs-font);color:var(--bcs-ink-3);border-radius:8px;
     85  transition:color var(--bcs-t),background var(--bcs-t),box-shadow var(--bcs-t);
     86}
     87.bcs-smtp-tab .dashicons{font-size:17px;width:17px;height:17px;color:var(--bcs-ink-3);transition:color var(--bcs-t)}
     88.bcs-smtp-tab:hover{color:var(--bcs-ink);background:var(--bcs-field)}
     89.bcs-smtp-tab:hover .dashicons{color:var(--bcs-ink-2)}
     90.bcs-smtp-tab[aria-selected="true"]{color:var(--bcs-ink);background:var(--bcs-brand-wash);box-shadow:inset 0 0 0 1px rgba(77,134,240,.38)}
     91.bcs-smtp-tab[aria-selected="true"] .dashicons{color:var(--bcs-brand-ink)}
     92.bcs-smtp-tab:focus-visible{outline:2px solid var(--bcs-brand-ink);outline-offset:1px}
     93.bcs-smtp-tab{position:relative;z-index:1}
     94.bcs-smtp-tab-pill{position:absolute;top:0;left:0;width:0;height:0;border-radius:8px;background:var(--bcs-brand-wash);box-shadow:inset 0 0 0 1px rgba(77,134,240,.38);opacity:0;pointer-events:none;transition:transform .32s var(--bcs-ease),width .32s var(--bcs-ease)}
     95.bcs-smtp-tabs.has-pill .bcs-smtp-tab-pill{opacity:1}
     96.bcs-smtp-tabs.is-instant .bcs-smtp-tab-pill{transition:none}
     97.bcs-smtp-tabs.has-pill .bcs-smtp-tab[aria-selected="true"]{background:transparent;box-shadow:none}
     98.bcs-smtp-tabs.has-pill .bcs-smtp-tab:not([aria-selected="true"]):hover{background:var(--bcs-field)}
     99.bcs-smtp-tab-dot{width:7px;height:7px;border-radius:50%;background:var(--bcs-ink-3);margin-left:-2px}
     100.bcs-smtp-tab-dot[data-tone="ok"]{background:var(--bcs-ok)}
     101.bcs-smtp-tab-dot[data-tone="bad"]{background:var(--bcs-bad);box-shadow:0 0 0 3px rgba(251,113,133,.18)}
     102.bcs-smtp-tab-dot[data-tone="warn"]{background:var(--bcs-warn)}
     103.bcs-smtp-tab-dot:not([data-tone]){display:none}
     104.bcs-smtp-tab-count{min-width:20px;text-align:center;font:600 11px/1 var(--bcs-font);font-variant-numeric:tabular-nums;padding:3px 6px;border-radius:99px;background:rgba(251,113,133,.16);color:var(--bcs-bad)}
    54105.bcs-smtp-tab-panel{display:none}
    55 .bcs-smtp-tab-panel.active{display:block}
    56 
    57 /* =====================================================================
    58    Card
    59    ===================================================================== */
    60 .bcs-smtp-card{background:var(--bcs-smtp-surface);border:1px solid var(--bcs-smtp-border);border-radius:var(--bcs-smtp-radius-lg);margin-bottom:20px;position:relative;overflow:hidden}
    61 .bcs-smtp-card::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(99,102,241,.3),transparent)}
    62 .bcs-smtp-card-header{padding:20px 24px 16px;border-bottom:1px solid var(--bcs-smtp-border)}
    63 .bcs-smtp-card-body{padding:24px}
    64 .bcs-smtp-card-title{font-size:14px;font-weight:600;color:var(--bcs-smtp-text);margin:0 0 4px;display:flex;align-items:center;gap:8px}
    65 .bcs-smtp-card-title .dashicons{color:var(--bcs-smtp-indigo);font-size:16px;width:16px;height:16px}
    66 .bcs-smtp-card-desc{font-size:13px;color:var(--bcs-smtp-text-2);margin:0;line-height:1.6}
    67 
    68 /* =====================================================================
    69    Grid
    70    ===================================================================== */
    71 .bcs-smtp-grid{display:grid;grid-template-columns:2fr 1fr;gap:20px;align-items:start;min-width:0}
    72 .bcs-smtp-grid>div{min-width:0}
    73 @media(max-width:900px){.bcs-smtp-grid{grid-template-columns:1fr}}
    74 
    75 /* =====================================================================
    76    Stat Row
    77    ===================================================================== */
    78 .bcs-smtp-stat-row{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:28px;padding-bottom:20px;border-bottom:1px solid var(--bcs-smtp-border)}
    79 .bcs-smtp-stat{flex:1;min-width:160px;padding:14px 16px;background:var(--bcs-smtp-surface-2);border:1px solid var(--bcs-smtp-border);border-radius:var(--bcs-smtp-radius);display:flex;align-items:center;gap:14px}
    80 .bcs-smtp-stat-icon{width:36px;height:36px;border-radius:var(--bcs-smtp-radius-sm);display:flex;align-items:center;justify-content:center;flex-shrink:0;background:var(--bcs-smtp-indigo-dim);color:var(--bcs-smtp-indigo-h);border:1px solid rgba(99,102,241,.3)}
    81 .bcs-smtp-stat-icon .dashicons{font-size:18px;width:18px;height:18px}
    82 .bcs-smtp-stat-icon.good{background:var(--bcs-smtp-emerald-dim);color:var(--bcs-smtp-emerald);border-color:rgba(16,185,129,.3)}
    83 .bcs-smtp-stat-icon.bad{background:var(--bcs-smtp-rose-dim);color:var(--bcs-smtp-rose);border-color:rgba(244,63,94,.3)}
    84 .bcs-smtp-stat-body{min-width:0}
    85 .bcs-smtp-stat-value{font-size:20px;font-weight:600;color:var(--bcs-smtp-text);line-height:1;font-family:var(--bcs-smtp-mono)}
    86 .bcs-smtp-stat-label{font-size:11px;color:var(--bcs-smtp-text-3);margin-top:4px;text-transform:uppercase;letter-spacing:.06em}
    87 .bcs-smtp-stat-value.good{color:var(--bcs-smtp-emerald)}
    88 .bcs-smtp-stat-value.bad{color:var(--bcs-smtp-rose)}
     106.bcs-smtp-tab-panel.active{display:block;animation:bcs-panel-in .24s var(--bcs-ease)}
     107@keyframes bcs-panel-in{from{opacity:.35;transform:translateY(6px)}to{opacity:1;transform:none}}
     108
     109/* =====================================================================
     110   Panels & layout
     111   ===================================================================== */
     112.bcs-smtp-panel{background:var(--bcs-panel);border:1px solid var(--bcs-seam);border-radius:var(--bcs-r);padding:24px;min-width:0;box-shadow:var(--bcs-shadow)}
     113.bcs-smtp-panel + .bcs-smtp-panel{margin-top:20px}
     114.bcs-smtp-panel--flush{padding:0;overflow:hidden}
     115.bcs-smtp-panel h2{font-size:16px;font-weight:600;line-height:1.3;margin-bottom:6px}
     116.bcs-smtp-panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px 12px;margin-bottom:18px;flex-wrap:wrap}
     117.bcs-smtp-panel-head h2{margin:0;display:flex;align-items:center;gap:10px}
     118.bcs-smtp-panel-sub{font-size:13px!important;color:var(--bcs-ink-3)}
     119.bcs-smtp-stack{display:flex;flex-direction:column;gap:20px;min-width:0}
     120.bcs-smtp-stack > .bcs-smtp-panel + .bcs-smtp-panel{margin-top:0}
     121.bcs-smtp-split{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(280px,1fr);gap:20px;align-items:start}
     122.bcs-smtp-split > .bcs-smtp-panel + .bcs-smtp-panel{margin-top:0}
     123.bcs-smtp-split > .bcs-smtp-test{position:sticky;top:52px}
     124.bcs-smtp-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:20px}
     125.bcs-smtp-count{display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:22px;padding:0 7px;border-radius:99px;font:600 12px/1 var(--bcs-font);background:var(--bcs-raised);color:var(--bcs-ink-2)}
     126.bcs-smtp-count--bad{background:rgba(251,113,133,.15);color:var(--bcs-bad)}
    89127
    90128/* =====================================================================
    91129   Buttons
    92130   ===================================================================== */
    93 .bcs-smtp-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:12px 24px;border-radius:var(--bcs-smtp-radius);font-size:14px;font-weight:600;font-family:var(--bcs-smtp-font);cursor:pointer;border:none;transition:all var(--bcs-smtp-t);text-decoration:none;white-space:nowrap;line-height:1}
    94 .bcs-smtp-btn-primary{background:linear-gradient(135deg,var(--bcs-smtp-indigo),#7c3aed);color:#fff;box-shadow:0 4px 14px var(--bcs-smtp-indigo-glow)}
    95 .bcs-smtp-btn-primary:hover{transform:translateY(-1px);box-shadow:0 6px 20px var(--bcs-smtp-indigo-glow);color:#fff}
    96 .bcs-smtp-btn-primary:active{transform:translateY(0)}
    97 .bcs-smtp-btn-primary:disabled{opacity:.5;cursor:not-allowed;transform:none}
    98 .bcs-smtp-btn-secondary{background:var(--bcs-smtp-surface-2);border:1px solid var(--bcs-smtp-border-light);color:var(--bcs-smtp-text-2)}
    99 .bcs-smtp-btn-secondary:hover{background:var(--bcs-smtp-surface-3);color:var(--bcs-smtp-text)}
    100 .bcs-smtp-btn-danger{background:var(--bcs-smtp-rose-dim);border:1px solid rgba(244,63,94,.3);color:var(--bcs-smtp-rose)}
    101 .bcs-smtp-btn-danger:hover{background:var(--bcs-smtp-rose);color:#fff}
    102 .bcs-smtp-btn-sm{padding:7px 14px;font-size:12px}
     131.bcs-smtp-btn{
     132  appearance:none;display:inline-flex;align-items:center;justify-content:center;gap:8px;
     133  min-height:38px;padding:0 16px;border-radius:var(--bcs-r-sm);border:1px solid transparent;
     134  font:600 14px/1 var(--bcs-font);text-decoration:none;white-space:nowrap;cursor:pointer;
     135  transition:background var(--bcs-t),border-color var(--bcs-t),color var(--bcs-t),transform .08s ease;
     136}
     137.bcs-smtp-btn:active:not(:disabled){transform:translateY(1px)}
     138.bcs-smtp-btn:disabled,.bcs-smtp-btn[aria-disabled="true"]{opacity:.5;cursor:not-allowed}
    103139.bcs-smtp-btn .dashicons{font-size:16px;width:16px;height:16px}
    104 .bcs-smtp-btn-full{width:100%}
    105 
    106 /* =====================================================================
    107    Toggle (on/off switch)
    108    ===================================================================== */
    109 .bcs-smtp-toggle{position:relative;display:inline-block;width:44px;height:24px;flex-shrink:0}
    110 .bcs-smtp-toggle input{display:none}
    111 .bcs-smtp-toggle-slider{position:absolute;inset:0;cursor:pointer;background:var(--bcs-smtp-surface-3);border-radius:100px;border:1px solid var(--bcs-smtp-border-light);transition:all var(--bcs-smtp-t)}
    112 .bcs-smtp-toggle-slider::before{content:'';position:absolute;width:16px;height:16px;left:3px;top:50%;transform:translateY(-50%);background:var(--bcs-smtp-text-3);border-radius:50%;transition:all var(--bcs-smtp-t)}
    113 .bcs-smtp-toggle input:checked+.bcs-smtp-toggle-slider{background:var(--bcs-smtp-indigo);border-color:var(--bcs-smtp-indigo)}
    114 .bcs-smtp-toggle input:checked+.bcs-smtp-toggle-slider::before{transform:translate(20px,-50%);background:#fff}
    115 
    116 .bcs-smtp-toggle-row{display:flex;align-items:center;justify-content:space-between;gap:12px}
    117 .bcs-smtp-toggle-row-info{flex:1}
    118 .bcs-smtp-toggle-row-label{font-size:14px;font-weight:600;color:var(--bcs-smtp-text)}
    119 .bcs-smtp-toggle-row-desc{font-size:12px;color:var(--bcs-smtp-text-3);margin-top:2px}
    120 
    121 /* =====================================================================
    122    Accordion
    123    ===================================================================== */
    124 .bcs-smtp-accordion{border:1px solid var(--bcs-smtp-border);border-radius:var(--bcs-smtp-radius);margin-top:16px;overflow:hidden}
    125 .bcs-smtp-accordion-head{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;background:var(--bcs-smtp-surface-2);cursor:pointer;user-select:none;transition:background var(--bcs-smtp-t);border:none;width:100%;text-align:left;font-family:var(--bcs-smtp-font)}
    126 .bcs-smtp-accordion-head:hover{background:var(--bcs-smtp-surface-3)}
    127 .bcs-smtp-accordion-head-left{display:flex;align-items:center;gap:10px}
    128 .bcs-smtp-accordion-head-icon{width:32px;height:32px;border-radius:var(--bcs-smtp-radius-sm);display:flex;align-items:center;justify-content:center;flex-shrink:0}
    129 .bcs-smtp-accordion-head-icon .dashicons{font-size:16px;width:16px;height:16px}
    130 .bcs-smtp-accordion-head-title{font-size:13px;font-weight:600;color:var(--bcs-smtp-text)}
    131 .bcs-smtp-accordion-head-desc{font-size:12px;color:var(--bcs-smtp-text-2);margin-top:1px}
    132 .bcs-smtp-accordion-chevron{color:var(--bcs-smtp-text-3);transition:transform var(--bcs-smtp-t);font-size:16px;width:16px;height:16px}
    133 .bcs-smtp-accordion.open .bcs-smtp-accordion-chevron{transform:rotate(180deg)}
    134 .bcs-smtp-accordion-body{display:none;padding:18px;border-top:1px solid var(--bcs-smtp-border);background:var(--bcs-smtp-bg)}
    135 .bcs-smtp-accordion.open .bcs-smtp-accordion-body{display:block}
    136 .bcs-smtp-accordion-header{display:flex;align-items:center;gap:10px;padding:14px 18px;background:var(--bcs-smtp-surface-2);cursor:pointer;user-select:none;transition:background var(--bcs-smtp-t);border:none;width:100%;text-align:left;font-family:var(--bcs-smtp-font);font-size:13px;font-weight:600;color:var(--bcs-smtp-text)}
    137 .bcs-smtp-accordion-header:hover{background:var(--bcs-smtp-surface-3)}
    138 .bcs-smtp-accordion-arrow{transition:transform var(--bcs-smtp-t);color:var(--bcs-smtp-text-3);font-size:16px;width:16px;height:16px}
    139 
    140 /* =====================================================================
    141    Fields / Inputs
    142    ===================================================================== */
    143 .bcs-smtp-field{margin-top:14px}
    144 .bcs-smtp-field-label{font-size:12px;color:var(--bcs-smtp-text-2);display:block;margin-bottom:6px}
    145 .bcs-smtp-input{width:100%;padding:10px 14px;background:var(--bcs-smtp-surface-2)!important;border:1px solid var(--bcs-smtp-border-light)!important;border-radius:var(--bcs-smtp-radius-sm);color:var(--bcs-smtp-text)!important;font-size:13px;font-family:var(--bcs-smtp-mono);transition:border-color var(--bcs-smtp-t),box-shadow var(--bcs-smtp-t);outline:none;box-sizing:border-box;box-shadow:none!important}
    146 input.bcs-smtp-input,select.bcs-smtp-input{height:38px!important;min-height:38px}
    147 .bcs-smtp-input:focus{border-color:var(--bcs-smtp-indigo)!important;box-shadow:0 0 0 3px var(--bcs-smtp-indigo-dim)!important}
    148 textarea.bcs-smtp-input{font-family:var(--bcs-smtp-font);resize:vertical;min-height:60px;height:auto!important}
    149 select.bcs-smtp-input{appearance:none!important;-webkit-appearance:none!important;padding-right:36px!important;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E")!important;background-repeat:no-repeat!important;background-position:right 12px center!important;background-size:12px!important;cursor:pointer}
    150 select.bcs-smtp-input option{background:var(--bcs-smtp-surface-2);color:var(--bcs-smtp-text)}
    151 .bcs-smtp-field-group{display:flex;flex-direction:column;gap:12px}
    152 
    153 /* Two-up field row — e.g. From Email/Name, Host/Port, Username/Password, Recipients/Frequency */
    154 .bcs-smtp-field-row{display:flex;gap:16px;margin-top:14px}
    155 .bcs-smtp-field-row .bcs-smtp-field{flex:1;min-width:0;margin-top:0}
    156 @media(max-width:600px){
    157   .bcs-smtp-field-row{flex-direction:column;gap:14px}
    158 }
    159 
    160 /* Password field — show/hide eye toggle */
     140.bcs-smtp-btn-primary{background:var(--bcs-brand);color:#fff}
     141.bcs-smtp-btn-primary:hover:not(:disabled){background:var(--bcs-brand-hover);color:#fff}
     142.bcs-smtp-btn-primary:active:not(:disabled){background:var(--bcs-brand-press)}
     143.bcs-smtp-btn-secondary{background:var(--bcs-field);border-color:var(--bcs-seam-strong);color:var(--bcs-ink)}
     144.bcs-smtp-btn-secondary:hover:not(:disabled){background:var(--bcs-field-hover);color:#fff;border-color:#40507a}
     145.bcs-smtp-btn-sm{min-height:30px;padding:0 12px;font-size:13px}
     146.bcs-smtp-btn-lg{min-height:44px;padding:0 22px;font-size:15px}
     147.bcs-smtp-btn.is-busy{cursor:progress}
     148.bcs-smtp-btn.is-busy::before{content:"";width:14px;height:14px;border-radius:50%;border:2px solid currentColor;border-right-color:transparent;animation:bcs-spin .7s linear infinite}
     149@keyframes bcs-spin{to{transform:rotate(360deg)}}
     150.bcs-smtp-link{appearance:none;background:none;border:0;padding:0;cursor:pointer;font:600 13px/1.4 var(--bcs-font);color:var(--bcs-brand-ink);text-decoration:underline;text-decoration-color:rgba(141,180,255,.35);text-underline-offset:3px}
     151.bcs-smtp-link:hover{color:#b3ccff;text-decoration-color:currentColor}
     152
     153/* =====================================================================
     154   Status chips — one vocabulary for every email's delivery state
     155   ===================================================================== */
     156.bcs-smtp-status{
     157  --tone:var(--bcs-off);
     158  display:inline-flex;align-items:center;gap:7px;height:24px;padding:0 10px 0 8px;border-radius:7px;white-space:nowrap;
     159  font:500 12.5px/1 var(--bcs-font);color:var(--bcs-ink);
     160  background:color-mix(in srgb,var(--tone) 12%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--tone) 24%,transparent);
     161}
     162.bcs-smtp-status-dot{width:7px;height:7px;border-radius:50%;background:var(--tone);flex-shrink:0}
     163.bcs-smtp-status--ok{--tone:var(--bcs-ok)}
     164.bcs-smtp-status--bad{--tone:var(--bcs-bad)}
     165.bcs-smtp-status--warn{--tone:var(--bcs-warn)}
     166.bcs-smtp-status.is-updated{animation:bcs-chip-in .32s var(--bcs-ease)}
     167@keyframes bcs-chip-in{from{transform:scale(.9);opacity:.2}to{transform:none;opacity:1}}
     168
     169.bcs-smtp-key{display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--bcs-off);flex-shrink:0}
     170.bcs-smtp-key--ok{background:var(--bcs-ok)}
     171.bcs-smtp-key--sent{background:var(--bcs-brand-ink)}
     172.bcs-smtp-key--bad{background:var(--bcs-bad)}
     173.bcs-smtp-key--warn{background:var(--bcs-warn)}
     174
     175/* =====================================================================
     176   Overview
     177   ===================================================================== */
     178
     179/* Health banner — the one thing to read first */
     180.bcs-smtp-hero{
     181  --tone:var(--bcs-off);
     182  position:relative;overflow:hidden;border-radius:var(--bcs-r);padding:28px 28px 0;
     183  background:color-mix(in srgb,var(--tone) 8%,var(--bcs-panel));
     184  border:1px solid color-mix(in srgb,var(--tone) 38%,transparent);box-shadow:var(--bcs-shadow);
     185}
     186.bcs-smtp-hero--ok{--tone:var(--bcs-ok)}
     187.bcs-smtp-hero--bad{--tone:var(--bcs-bad)}
     188.bcs-smtp-hero--warn{--tone:var(--bcs-warn)}
     189.bcs-smtp-hero--off{--tone:var(--bcs-off)}
     190.bcs-smtp-hero-main{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:20px;padding-bottom:26px}
     191.bcs-smtp-hero-copy h2{font-size:26px;font-weight:600;line-height:1.15;letter-spacing:-.025em;margin-bottom:6px}
     192.bcs-smtp-hero-copy p{color:var(--bcs-ink-2);font-size:14.5px;max-width:64ch}
     193.bcs-smtp-hero-actions{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}
     194
     195.bcs-smtp-beacon{position:relative;display:grid;place-items:center;width:52px;height:52px;border-radius:50%;background:color-mix(in srgb,var(--tone) 14%,transparent);border:1px solid color-mix(in srgb,var(--tone) 30%,transparent)}
     196.bcs-smtp-beacon span{width:16px;height:16px;border-radius:50%;background:var(--tone);box-shadow:inset 0 -2px 0 rgba(0,0,0,.18)}
     197.bcs-smtp-beacon::after{content:"";position:absolute;inset:-1px;border-radius:50%;border:1px solid var(--tone);opacity:0}
     198.bcs-smtp-hero--ok .bcs-smtp-beacon::after{animation:bcs-beacon 2.8s var(--bcs-ease) infinite}
     199@keyframes bcs-beacon{0%{transform:scale(1);opacity:.55}70%,100%{transform:scale(1.55);opacity:0}}
     200
     201.bcs-smtp-hero-facts{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));margin:0 -28px;border-top:1px solid color-mix(in srgb,var(--tone) 22%,transparent);background:color-mix(in srgb,var(--tone) 4%,var(--bcs-panel))}
     202.bcs-smtp-hero-facts > div{padding:14px 28px;min-width:0;border-left:1px solid var(--bcs-seam)}
     203.bcs-smtp-hero-facts > div:first-child{border-left:0}
     204.bcs-smtp-hero-facts dt{font-size:12px;color:var(--bcs-ink-3);margin-bottom:3px}
     205.bcs-smtp-hero-facts dd{margin:0;font-size:13.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     206.bcs-smtp-hero-msg{margin:0;padding:0 0 18px}
     207.bcs-smtp-hero-msg:empty{display:none}
     208
     209/* Grid: activity chart + setup checklist */
     210.bcs-smtp-overview-grid{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:20px;margin-top:20px;align-items:start}
     211.bcs-smtp-overview-grid{align-items:stretch}
     212.bcs-smtp-overview-grid > .bcs-smtp-activity{display:flex;flex-direction:column}
     213.bcs-smtp-overview-grid > .bcs-smtp-activity .bcs-smtp-linechart{flex:1 1 auto;min-height:200px;height:auto}
     214.bcs-smtp-side{display:flex;flex-direction:column;gap:20px;min-width:0}
     215.bcs-smtp-side > .bcs-smtp-panel + .bcs-smtp-panel{margin-top:0}
     216.bcs-smtp-side > .bcs-smtp-quick{flex:1 1 auto;display:flex;flex-direction:column}
     217.bcs-smtp-quick .bcs-smtp-quick-list{flex:1 1 auto;justify-content:space-evenly}
     218.bcs-smtp-side .bcs-smtp-panel{padding:20px 22px}
     219.bcs-smtp-side .bcs-smtp-panel-head{margin-bottom:14px}
     220
     221/* Quick actions */
     222.bcs-smtp-quick-list{list-style:none;margin:0 -12px;padding:0;display:flex;flex-direction:column;gap:2px}
     223.bcs-smtp-quick-list li{margin:0}
     224.bcs-smtp-quick-list button{appearance:none;width:100%;display:flex;align-items:center;gap:12px;padding:7px 12px;border:0;border-radius:10px;background:none;cursor:pointer;text-align:left;color:var(--bcs-ink);font-family:var(--bcs-font);transition:background var(--bcs-t)}
     225.bcs-smtp-quick-list button:hover{background:rgba(255,255,255,.04)}
     226.bcs-smtp-quick-icon{display:grid!important;place-items:center;width:32px!important;height:32px!important;flex-shrink:0;border-radius:9px;background:var(--bcs-brand-wash);color:var(--bcs-brand-ink);font-size:18px!important}
     227.bcs-smtp-quick-icon--bad{background:rgba(251,113,133,.13);color:var(--bcs-bad)}
     228.bcs-smtp-quick-icon--ok{background:rgba(52,211,153,.13);color:var(--bcs-ok)}
     229.bcs-smtp-quick-text{display:flex;flex-direction:column;min-width:0;flex:1}
     230.bcs-smtp-quick-text strong{font-weight:500;font-size:14px}
     231.bcs-smtp-quick-text small{font-size:12.5px;color:var(--bcs-ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     232.bcs-smtp-quick-list button > .dashicons:last-child{color:var(--bcs-ink-3);font-size:16px;width:16px;height:16px;transition:transform var(--bcs-t),color var(--bcs-t)}
     233.bcs-smtp-quick-list button:hover > .dashicons:last-child{color:var(--bcs-ink);transform:translateX(2px)}
     234.bcs-smtp-overview-grid > .bcs-smtp-panel + .bcs-smtp-panel{margin-top:0}
     235
     236/* Email activity — line chart */
     237.bcs-smtp-segmented--sm{padding:2px}
     238.bcs-smtp-segmented--sm span{min-height:28px;padding:0 11px;font-size:12.5px}
     239.bcs-smtp-linechart{position:relative;height:260px;margin:0 -6px;border-radius:var(--bcs-r-sm);cursor:crosshair}
     240.bcs-smtp-linechart:focus-visible{outline-offset:4px}
     241.bcs-smtp-linechart svg{display:block;width:100%;height:100%;overflow:visible}
     242.bcs-smtp-lc-grid line{stroke:var(--bcs-seam);stroke-width:1}
     243.bcs-smtp-lc-grid line.is-base{stroke:var(--bcs-seam-strong)}
     244.bcs-smtp-lc-label{fill:var(--bcs-ink-3);font:500 11.5px var(--bcs-font)}
     245.bcs-smtp-lc-area{fill:var(--bcs-brand-ink);opacity:.1}
     246.bcs-smtp-lc-line{fill:none;stroke-width:2.25;stroke-linecap:round;stroke-linejoin:round}
     247.bcs-smtp-lc-line.is-sent{stroke:var(--bcs-brand-ink)}
     248.bcs-smtp-lc-line.is-failed{stroke:var(--bcs-bad)}
     249.bcs-smtp-lc-line.is-unconfirmed{stroke:var(--bcs-warn);stroke-dasharray:4 4}
     250.bcs-smtp-lc-line{stroke-dasharray:var(--len);stroke-dashoffset:var(--len);animation:bcs-draw .9s var(--bcs-ease) forwards}
     251.bcs-smtp-lc-line.is-unconfirmed{animation:none;stroke-dashoffset:0}
     252@keyframes bcs-draw{to{stroke-dashoffset:0}}
     253.bcs-smtp-lc-cursor{stroke:var(--bcs-ink-3);stroke-width:1;stroke-dasharray:3 3;opacity:0;transition:opacity .12s}
     254.bcs-smtp-lc-dot{stroke:var(--bcs-panel);stroke-width:2.5;opacity:0;transition:opacity .12s}
     255.bcs-smtp-linechart.is-hover .bcs-smtp-lc-cursor,.bcs-smtp-linechart.is-hover .bcs-smtp-lc-dot{opacity:1}
     256.bcs-smtp-linechart-tip{position:absolute;top:0;z-index:2;min-width:168px;padding:10px 12px;border-radius:10px;pointer-events:none;
     257  background:#0a0e17;border:1px solid var(--bcs-seam-strong);box-shadow:0 10px 28px -8px rgba(0,0,0,.7);font-size:13px;line-height:1.5;transform:translateX(-50%)}
     258.bcs-smtp-linechart-tip strong{display:block;font-weight:600;margin-bottom:4px}
     259.bcs-smtp-linechart-tip span{display:flex;align-items:center;gap:8px;color:var(--bcs-ink-2)}
     260.bcs-smtp-linechart-tip b{margin-left:auto;color:var(--bcs-ink);font-weight:600}
     261.bcs-smtp-activity-foot{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;margin:16px -24px -24px!important;padding:14px 24px;border-top:1px solid var(--bcs-seam);border-radius:0 0 var(--bcs-r) var(--bcs-r);background:rgba(0,0,0,.14);font-size:13.5px!important;color:var(--bcs-ink-2)}
     262.bcs-smtp-activity .bcs-smtp-chart-empty{margin-top:-140px!important;text-align:center;color:var(--bcs-ink-3);position:relative}
     263
     264/* Finish setting up */
     265.bcs-smtp-progress{display:grid;grid-auto-flow:column;grid-auto-columns:1fr;gap:4px;margin:-6px 0 18px}
     266.bcs-smtp-progress span{height:4px;border-radius:2px;background:var(--bcs-seam-strong)}
     267.bcs-smtp-progress span.is-done{background:var(--bcs-brand)}
     268.bcs-smtp-checklist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
     269.bcs-smtp-checklist li{display:flex;align-items:center;gap:12px;margin:0;padding:8px 0;border-top:1px solid var(--bcs-seam);font-size:14px}
     270.bcs-smtp-checklist li:first-child{border-top:0;padding-top:0}
     271.bcs-smtp-checklist li.is-done{color:var(--bcs-ink-3)}
     272.bcs-smtp-check-dot{display:grid!important;place-items:center;width:20px!important;height:20px!important;border-radius:50%;border:1.5px solid var(--bcs-seam-strong);font-size:15px!important;flex-shrink:0}
     273.is-done .bcs-smtp-check-dot{border-color:transparent;background:rgba(52,211,153,.16);color:var(--bcs-ok)}
     274.bcs-smtp-link--plain{display:inline-flex;align-items:center;gap:4px;text-decoration:none;color:var(--bcs-ink)!important;font-weight:500;font-size:14px}
     275.bcs-smtp-link--plain .dashicons{font-size:15px;width:15px;height:15px;color:var(--bcs-brand-ink);transition:transform var(--bcs-t)}
     276.bcs-smtp-link--plain:hover .dashicons{transform:translateX(2px)}
     277
     278/* Last 30 days */
     279.bcs-smtp-figures{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px;margin:0 0 22px}
     280.bcs-smtp-figures > div{min-width:0}
     281.bcs-smtp-figures dt{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--bcs-ink-3)}
     282.bcs-smtp-figures dd{margin:6px 0 0;font:600 28px/1 var(--bcs-font);letter-spacing:-.03em;color:var(--bcs-ink);white-space:nowrap}
     283.bcs-smtp-figures dd.is-bad{color:var(--bcs-bad)}
     284.bcs-smtp-figures dd small{font-size:16px;font-weight:500;color:var(--bcs-ink-3);margin-left:1px;letter-spacing:0}
     285
     286
     287/* Fact grid (Settings → What WordPress is using) */
     288.bcs-smtp-facts{display:grid;grid-template-columns:1fr 1fr;gap:0;margin:0;border-top:1px solid var(--bcs-seam)}
     289.bcs-smtp-facts > div{padding:11px 0;border-bottom:1px solid var(--bcs-seam);min-width:0}
     290.bcs-smtp-facts > div:nth-child(odd){padding-right:14px}
     291.bcs-smtp-facts dt{font-size:12px;color:var(--bcs-ink-3)}
     292.bcs-smtp-facts dd{margin:2px 0 0;font-size:13.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     293.bcs-smtp-facts--plain{margin-top:14px}
     294
     295/* =====================================================================
     296   Guided setup — providers, steps, fields
     297   ===================================================================== */
     298
     299.bcs-smtp-provider-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(168px,1fr));gap:8px}
     300.bcs-smtp-provider-grid--compact{grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
     301.bcs-smtp-provider{
     302  position:relative;display:flex;align-items:center;gap:10px;min-height:52px;padding:8px 12px;cursor:pointer;
     303  background:var(--bcs-field);border:1px solid var(--bcs-seam);border-radius:var(--bcs-r-sm);
     304  font:500 13.5px/1.25 var(--bcs-font);color:var(--bcs-ink);text-align:left;
     305  transition:border-color var(--bcs-t),background var(--bcs-t);
     306}
     307button.bcs-smtp-provider{appearance:none;width:100%}
     308.bcs-smtp-provider:hover{border-color:var(--bcs-seam-strong);background:var(--bcs-field-hover)}
     309.bcs-smtp-provider input{position:absolute;opacity:0;width:1px;height:1px;margin:0;pointer-events:none}
     310.bcs-smtp-provider-mark{display:grid;place-items:center;width:32px;height:32px;flex-shrink:0;border-radius:6px;background:var(--bcs-panel);border:1px solid var(--bcs-seam-strong);font:700 12px/1 var(--bcs-font);letter-spacing:.02em;color:var(--bcs-ink-2);transition:background var(--bcs-t),color var(--bcs-t),border-color var(--bcs-t)}
     311.bcs-smtp-provider .bcs-smtp-provider-mark{background:color-mix(in srgb,var(--tone) 15%,var(--bcs-panel));border-color:color-mix(in srgb,var(--tone) 38%,transparent);color:color-mix(in srgb,var(--tone) 75%,#fff)}
     312.bcs-smtp-provider-mark .dashicons{font-size:16px;width:16px;height:16px}
     313.bcs-smtp-provider:has(input:checked){border-color:var(--bcs-brand);background:var(--bcs-brand-wash)}
     314.bcs-smtp-provider:has(input:checked) .bcs-smtp-provider-mark{background:var(--tone);border-color:var(--tone);color:#fff}
     315.bcs-smtp-provider:has(input:focus-visible){outline:2px solid var(--bcs-brand-ink);outline-offset:2px}
     316
     317.bcs-smtp-provider-note{display:flex;gap:10px;align-items:flex-start;margin-top:14px!important;padding:12px 14px;border-radius:var(--bcs-r-sm);background:var(--bcs-field);border:1px solid var(--bcs-seam);font-size:13.5px!important;color:var(--bcs-ink-2)}
     318.bcs-smtp-provider-note .dashicons{color:var(--bcs-brand-ink);font-size:18px;width:18px;height:18px;flex-shrink:0}
     319.bcs-smtp-provider-note a{font-weight:600;margin-left:4px}
     320
     321.bcs-smtp-field{display:flex;flex-direction:column;min-width:0;margin-top:16px}
     322.bcs-smtp-field-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
     323.bcs-smtp-field-row .bcs-smtp-field{margin-top:16px}
     324.bcs-smtp-field-row:has(.bcs-smtp-field--port){grid-template-columns:minmax(0,1fr) 120px}
     325.bcs-smtp-field-row:has(> .bcs-smtp-field--grow:first-child):not(:has(.bcs-smtp-field--port)){grid-template-columns:minmax(0,1fr) auto}
     326.bcs-smtp-field-label{font-size:13px;font-weight:600;color:var(--bcs-ink);margin-bottom:6px;display:block}
     327.bcs-smtp-hint{display:block;font-size:12.5px;color:var(--bcs-ink-3);margin-top:6px;line-height:1.45}
     328.bcs-smtp-hint.is-good{color:var(--bcs-sent)}
     329.bcs-smtp-hint.is-bad{color:var(--bcs-back)}
     330
     331.bcs-smtp-wrap .bcs-smtp-input{
     332  width:100%;min-height:40px;height:40px;padding:0 12px;margin:0;
     333  background:var(--bcs-field);border:1px solid var(--bcs-seam-strong);border-radius:var(--bcs-r-sm);
     334  color:var(--bcs-ink);font:400 14px/1.4 var(--bcs-font);box-shadow:none;
     335  transition:border-color var(--bcs-t),box-shadow var(--bcs-t),background var(--bcs-t);
     336}
     337.bcs-smtp-wrap .bcs-smtp-input.bcs-smtp-mono{font-family:var(--bcs-mono);font-size:13px}
     338.bcs-smtp-wrap .bcs-smtp-input::placeholder{color:#6f7c97}
     339.bcs-smtp-wrap .bcs-smtp-input:hover{border-color:#40507a}
     340.bcs-smtp-wrap .bcs-smtp-input:focus{border-color:var(--bcs-brand-ink);box-shadow:0 0 0 3px rgba(141,180,255,.18);outline:none}
     341.bcs-smtp-wrap .bcs-smtp-input:disabled{opacity:.6}
     342.bcs-smtp-wrap .bcs-smtp-input.is-invalid{border-color:var(--bcs-bad);box-shadow:0 0 0 3px rgba(251,113,133,.18)}
     343.bcs-smtp-wrap .bcs-smtp-input--num{width:76px;text-align:center;padding:0 6px}
     344.bcs-smtp-wrap .bcs-smtp-select{
     345  appearance:none;-webkit-appearance:none;width:auto;min-width:150px;padding-right:34px;cursor:pointer;
     346  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23aab5ca' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
     347  background-repeat:no-repeat;background-position:right 12px center;background-size:12px;
     348}
     349.bcs-smtp-wrap .bcs-smtp-select--sm{min-width:72px;min-height:30px;height:30px;font-size:13px}
     350.bcs-smtp-wrap select.bcs-smtp-input option{background:var(--bcs-field);color:var(--bcs-ink)}
     351/* WordPress core darkens select text on hover/focus (#1e1e1e), which vanishes on this theme. */
     352.wp-core-ui .bcs-smtp-wrap select.bcs-smtp-input:hover,.wp-core-ui .bcs-smtp-wrap select.bcs-smtp-input:focus{color:var(--bcs-ink)}
     353
    161354.bcs-smtp-pw-wrap{position:relative}
    162 .bcs-smtp-pw-wrap input{padding-right:40px!important}
    163 .bcs-smtp-pw-eye{position:absolute;top:50%;right:6px;transform:translateY(-50%);display:flex;align-items:center;justify-content:center;width:28px;height:28px;background:transparent;border:none;cursor:pointer;color:var(--bcs-smtp-text-3);border-radius:4px;transition:color var(--bcs-smtp-t),background var(--bcs-smtp-t)}
    164 .bcs-smtp-pw-eye:hover{color:var(--bcs-smtp-text);background:var(--bcs-smtp-surface-3)}
    165 .bcs-smtp-pw-eye .dashicons{font-size:16px;width:16px;height:16px}
    166 
    167 /* Cancel-password-change link — pulses briefly each time it's revealed */
    168 @keyframes bcs-smtp-pw-cancel-pulse{
    169   0%{background:rgba(99,102,241,.3);box-shadow:0 0 0 4px rgba(99,102,241,.2)}
    170   100%{background:transparent;box-shadow:0 0 0 0 rgba(99,102,241,0)}
    171 }
    172 #bcs-smtp-pw-cancel-row{animation:bcs-smtp-pw-cancel-pulse 1.6s ease-out;border-radius:6px;padding:4px 8px;margin-left:-8px}
    173 
    174 /* =====================================================================
    175    Checkboxes / Radio
    176    ===================================================================== */
    177 .bcs-smtp-check-list{display:flex;flex-direction:column;gap:10px}
    178 .bcs-smtp-check-item{display:flex;align-items:flex-start;gap:10px;cursor:pointer;padding:10px 12px;border-radius:var(--bcs-smtp-radius-sm);border:1px solid transparent;transition:border-color var(--bcs-smtp-t),background var(--bcs-smtp-t)}
    179 .bcs-smtp-check-item:hover{background:var(--bcs-smtp-surface);border-color:var(--bcs-smtp-border)}
    180 .bcs-smtp-check-item input[type="checkbox"]{appearance:none;-webkit-appearance:none;width:16px;height:16px;border-radius:4px;flex-shrink:0;margin-top:2px;border:1.5px solid var(--bcs-smtp-border-light);background:var(--bcs-smtp-surface-2);cursor:pointer;transition:all var(--bcs-smtp-t);position:relative}
    181 .bcs-smtp-check-item input[type="checkbox"]:checked{background:var(--bcs-smtp-indigo);border-color:var(--bcs-smtp-indigo)}
    182 .bcs-smtp-check-item input[type="checkbox"]:checked::after{content:'';position:absolute;left:4px;top:1px;width:5px;height:9px;border:2px solid #fff;border-top:none;border-left:none;transform:rotate(45deg)}
    183 .bcs-smtp-check-item input[type="radio"]{appearance:none;-webkit-appearance:none;width:20px;height:20px;border-radius:50%;flex-shrink:0;border:2px solid var(--bcs-smtp-border-light);background:var(--bcs-smtp-surface-2);cursor:pointer;transition:all var(--bcs-smtp-t);position:relative}
    184 .bcs-smtp-check-item input[type="radio"]:checked{border-color:var(--bcs-smtp-indigo);background:var(--bcs-smtp-indigo);box-shadow:inset 0 0 0 3px var(--bcs-smtp-surface-2)}
    185 .bcs-smtp-check-label-title{font-size:13px;color:var(--bcs-smtp-text);font-weight:500}
    186 .bcs-smtp-check-label-desc{font-size:11.5px;color:var(--bcs-smtp-text-3);margin-top:1px}
    187 
    188 /* =====================================================================
    189    Alert
    190    ===================================================================== */
    191 .bcs-smtp-alert{display:flex;gap:12px;padding:14px 16px;border-radius:var(--bcs-smtp-radius);margin-bottom:16px;font-size:13px;line-height:1.6}
    192 .bcs-smtp-alert .dashicons{flex-shrink:0;font-size:18px;width:18px;height:18px;margin-top:1px}
    193 .bcs-smtp-alert-content{flex:1}
    194 .bcs-smtp-alert-title{font-weight:600;margin-bottom:4px}
    195 .bcs-smtp-alert-warning{background:var(--bcs-smtp-amber-dim);border:1px solid rgba(245,158,11,.2)}
    196 .bcs-smtp-alert-warning .dashicons{color:var(--bcs-smtp-amber)}
    197 .bcs-smtp-alert-error{background:var(--bcs-smtp-rose-dim);border:1px solid rgba(244,63,94,.25)}
    198 .bcs-smtp-alert-error .dashicons{color:var(--bcs-smtp-rose)}
    199 .bcs-smtp-alert-success{background:var(--bcs-smtp-emerald-dim);border:1px solid rgba(16,185,129,.25)}
    200 .bcs-smtp-alert-success .dashicons{color:var(--bcs-smtp-emerald)}
    201 .bcs-smtp-alert-info{background:var(--bcs-smtp-indigo-dim);border:1px solid rgba(99,102,241,.2)}
    202 .bcs-smtp-alert-info .dashicons{color:var(--bcs-smtp-indigo-h)}
    203 
    204 /* =====================================================================
    205    Server / Config Grid
    206    ===================================================================== */
    207 .bcs-smtp-server-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
    208 .bcs-smtp-server-row{display:flex;flex-direction:column;gap:3px;padding:10px 12px;background:var(--bcs-smtp-surface-2);border-radius:var(--bcs-smtp-radius-sm)}
    209 .bcs-smtp-server-key{font-size:11px;color:var(--bcs-smtp-text-3)}
    210 .bcs-smtp-server-val{font-size:12px;font-family:var(--bcs-smtp-mono);color:var(--bcs-smtp-text)}
    211 .bcs-smtp-server-val.good{color:var(--bcs-smtp-emerald)}
    212 .bcs-smtp-server-val.bad{color:var(--bcs-smtp-rose)}
    213 
    214 /* =====================================================================
    215    Badge
    216    ===================================================================== */
    217 .bcs-smtp-badge{display:inline-flex;align-items:center;gap:5px;font-size:11px;padding:3px 10px;border-radius:100px;font-weight:500;font-family:var(--bcs-smtp-mono);white-space:nowrap}
    218 .bcs-smtp-badge-success{background:var(--bcs-smtp-emerald-dim);color:var(--bcs-smtp-emerald);border:1px solid rgba(16,185,129,.3)}
    219 .bcs-smtp-badge-error{background:var(--bcs-smtp-rose-dim);color:var(--bcs-smtp-rose);border:1px solid rgba(244,63,94,.3)}
    220 .bcs-smtp-badge-warning{background:var(--bcs-smtp-amber-dim);color:var(--bcs-smtp-amber);border:1px solid rgba(245,158,11,.3)}
    221 
    222 /* =====================================================================
    223    Tables — test log / incidents / change log
    224    ===================================================================== */
    225 .bcs-smtp-table-wrap{overflow-x:auto;margin-top:4px}
    226 .bcs-smtp-table{width:100%;border-collapse:collapse;font-size:12px}
    227 .bcs-smtp-table th{text-align:left;padding:8px 10px;font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--bcs-smtp-text-3);border-bottom:1px solid var(--bcs-smtp-border);white-space:nowrap}
    228 .bcs-smtp-table td{padding:9px 10px;border-bottom:1px solid var(--bcs-smtp-border);color:var(--bcs-smtp-text);vertical-align:middle}
    229 .bcs-smtp-table tr:last-child td{border-bottom:none}
    230 .bcs-smtp-table tr:hover td{background:var(--bcs-smtp-surface-2)}
    231 .bcs-smtp-table tbody tr{transition:background var(--bcs-smtp-t)}
    232 
    233 /* Skeleton loader — Email Log table while an AJAX filter request is in flight */
    234 @keyframes bcs-smtp-shimmer{0%{background-position:-400px 0}100%{background-position:400px 0}}
    235 .bcs-smtp-skel{display:inline-block;height:10px;border-radius:4px;background:linear-gradient(90deg,var(--bcs-smtp-surface-2) 25%,rgba(255,255,255,.07) 50%,var(--bcs-smtp-surface-2) 75%);background-size:800px 100%;animation:bcs-smtp-shimmer 1.4s infinite linear}
    236 .bcs-smtp-skeleton-row td{padding:9px 10px}
    237 
    238 /* =====================================================================
    239    Misc
    240    ===================================================================== */
    241 .bcs-smtp-divider{height:1px;background:var(--bcs-smtp-border);margin:20px 0}
    242 .bcs-smtp-hidden{display:none!important}
    243 @keyframes bcs-smtp-spin{to{transform:rotate(360deg)}}
    244 .bcs-smtp-spinning{animation:bcs-smtp-spin .7s linear infinite;display:inline-block}
    245 details summary{list-style:none}
    246 details summary::-webkit-details-marker{display:none}
    247 
    248 /* Tab link in alerts */
    249 .bcs-smtp-tab-link{color:var(--bcs-smtp-indigo-h);text-decoration:underline;cursor:pointer}
    250 .bcs-smtp-tab-link:hover{color:var(--bcs-smtp-indigo)}
    251 
    252 /* =====================================================================
    253    Mobile-only overrides (all panels)
    254    ===================================================================== */
    255 @media(max-width:600px){
    256   /* All buttons full-width on mobile, every tab */
    257   .bcs-smtp-btn{width:100%}
    258 
    259   /* Settings — saved-password row: input full width, Change button drops to its own line */
    260   #bcs-smtp-pw-display{flex-wrap:wrap}
    261   #bcs-smtp-pw-display input{flex:1 1 100%!important;width:100%!important}
    262 
    263   /* Email Log filters — date range & status dropdowns full width */
    264   #bcs-smtp-log-date-range,
    265   #bcs-smtp-log-status{width:100%!important;min-width:0!important}
    266 
    267   /* Email Log pagination — Prev/Next stay on one row, left & right */
    268   #bcs-smtp-log-pagination{justify-content:space-between;flex-wrap:nowrap}
    269   #bcs-smtp-log-prev,
    270   #bcs-smtp-log-next{width:auto!important;flex-shrink:0}
    271 
    272   /* Email Report — 4 summary stat boxes as a 2-column (50%) grid, without overflowing the card */
    273   .bcs-smtp-report-stats{grid-template-columns:repeat(2,1fr)!important}
    274   .bcs-smtp-report-stats .bcs-smtp-stat{min-width:0}
    275 
    276   /* Schedule Report — frequency select full-width */
    277   #bcs-smtp-report-frequency{max-width:none!important;width:100%!important}
    278 }
    279 
    280 /* "Failure types detected" list — ensure bullet markers render, on every device */
    281 .bcs-smtp-alert-content ul{list-style:disc;list-style-position:inside;margin:6px 0 0;padding:0}
    282 .bcs-smtp-alert-content li{margin-bottom:4px}
     355.bcs-smtp-pw-wrap .bcs-smtp-input{padding-right:44px}
     356.bcs-smtp-pw-eye{position:absolute;top:50%;right:4px;transform:translateY(-50%);display:grid;place-items:center;width:34px;height:32px;background:none;border:0;border-radius:5px;cursor:pointer;color:var(--bcs-ink-3)}
     357.bcs-smtp-pw-eye:hover{color:var(--bcs-ink);background:var(--bcs-field-hover)}
     358.bcs-smtp-pw-saved{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:40px;padding:0 12px;border-radius:var(--bcs-r-sm);background:var(--bcs-field);border:1px dashed var(--bcs-seam-strong)}
     359.bcs-smtp-pw-dots{letter-spacing:.12em;color:var(--bcs-ink-2)}
     360
     361.bcs-smtp-segmented{display:inline-flex;flex-wrap:wrap;padding:3px;gap:2px;background:var(--bcs-field);border:1px solid var(--bcs-seam-strong);border-radius:var(--bcs-r-sm)}
     362.bcs-smtp-segmented label{position:relative;cursor:pointer;margin:0}
     363.bcs-smtp-segmented input{position:absolute;opacity:0;width:1px;height:1px;margin:0}
     364.bcs-smtp-segmented span{display:flex;align-items:center;min-height:32px;padding:0 12px;border-radius:5px;font:500 13px/1 var(--bcs-font);color:var(--bcs-ink-2);white-space:nowrap;transition:background var(--bcs-t),color var(--bcs-t)}
     365.bcs-smtp-segmented label:hover span{color:var(--bcs-ink)}
     366.bcs-smtp-segmented input:checked + span{background:var(--bcs-brand);color:#fff;font-weight:600}
     367.bcs-smtp-segmented input:focus-visible + span{outline:2px solid var(--bcs-brand-ink);outline-offset:1px}
     368
     369.bcs-smtp-toggle-row{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;margin-top:4px}
     370.bcs-smtp-toggle-row + #bcs-smtp-auth-fields{margin-top:4px}
     371.bcs-smtp-auth-warning{margin-top:14px}
     372.bcs-smtp-toggle-row-info{min-width:0}
     373.bcs-smtp-toggle-row-label{display:block;font-size:14.5px;font-weight:600;color:var(--bcs-ink);cursor:pointer}
     374.bcs-smtp-toggle-row--head .bcs-smtp-toggle-row-label{font-size:17px}
     375.bcs-smtp-toggle-row-desc{font-size:13.5px!important;color:var(--bcs-ink-2);margin-top:3px!important;max-width:62ch}
     376.bcs-smtp-toggle{position:relative;display:inline-block;width:42px;height:24px;flex-shrink:0;margin-top:2px}
     377.bcs-smtp-toggle input{position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;cursor:pointer;z-index:1}
     378.bcs-smtp-toggle-slider{position:absolute;inset:0;border-radius:99px;background:var(--bcs-seam-strong);transition:background var(--bcs-t)}
     379.bcs-smtp-toggle-slider::before{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#c5cddd;box-shadow:0 1px 2px rgba(0,0,0,.35);transition:transform var(--bcs-t),background var(--bcs-t)}
     380.bcs-smtp-toggle input:checked + .bcs-smtp-toggle-slider{background:var(--bcs-brand)}
     381.bcs-smtp-toggle input:checked + .bcs-smtp-toggle-slider::before{transform:translateX(18px);background:#fff}
     382.bcs-smtp-toggle input:focus-visible + .bcs-smtp-toggle-slider{outline:2px solid var(--bcs-brand-ink);outline-offset:2px}
     383
     384.bcs-smtp-disclosure{margin-top:24px;border:1px solid var(--bcs-seam);border-radius:var(--bcs-r-sm);background:var(--bcs-bg)}
     385.bcs-smtp-disclosure-head{appearance:none;width:100%;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px;background:none;border:0;cursor:pointer;text-align:left;color:var(--bcs-ink);font-family:var(--bcs-font)}
     386.bcs-smtp-disclosure-head strong{display:block;font-size:14px;font-weight:600}
     387.bcs-smtp-disclosure-head .bcs-smtp-mono{display:block;color:var(--bcs-ink-3);margin-top:2px;font-size:12px}
     388.bcs-smtp-disclosure-head .dashicons{color:var(--bcs-ink-3);transition:transform var(--bcs-t)}
     389.bcs-smtp-disclosure.is-open .bcs-smtp-disclosure-head .dashicons{transform:rotate(180deg)}
     390.bcs-smtp-disclosure-body{display:none;padding:0 16px 18px;border-top:1px solid var(--bcs-seam)}
     391.bcs-smtp-disclosure.is-open .bcs-smtp-disclosure-body{display:block}
     392.bcs-smtp-disclosure-body .bcs-smtp-toggle-row{margin-top:18px}
     393
     394
     395.bcs-smtp-reveal{display:none;padding-top:6px}
     396.bcs-smtp-reveal.open{display:block}
     397.bcs-smtp-sentence{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:16px;font-size:14px;color:var(--bcs-ink)}
     398.bcs-smtp-sentence label{font-weight:500}
     399.bcs-smtp-sentence .bcs-smtp-hint{flex-basis:100%;margin-top:0}
     400
     401/* Test / aside */
     402.bcs-smtp-test h2{font-size:17px}
     403.bcs-smtp-test > p,.bcs-smtp-aside > p{color:var(--bcs-ink-2);font-size:13.5px}
     404.bcs-smtp-aside > .bcs-smtp-hint{margin-top:18px}
     405
     406/* =====================================================================
     407   Email log
     408   ===================================================================== */
     409.bcs-smtp-filters{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:16px 20px;border-bottom:1px solid var(--bcs-seam)}
     410.bcs-smtp-search{position:relative;flex:1 1 260px;min-width:200px}
     411.bcs-smtp-search .dashicons{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--bcs-ink-3);font-size:18px;width:18px;height:18px;pointer-events:none}
     412.bcs-smtp-wrap .bcs-smtp-search .bcs-smtp-input{padding-left:36px}
     413.bcs-smtp-filters .bcs-smtp-btn{margin-left:auto}
     414
     415.bcs-smtp-table-wrap{overflow-x:auto}
     416.bcs-smtp-table{width:100%;border-collapse:collapse;font-size:13.5px;table-layout:fixed}
     417.bcs-smtp-table th{text-align:left;padding:10px 12px;font:600 11px/1.2 var(--bcs-font);letter-spacing:.09em;text-transform:uppercase;color:var(--bcs-ink-3);background:var(--bcs-bg);border-bottom:1px solid var(--bcs-seam);white-space:nowrap}
     418.bcs-smtp-table td{padding:9px 12px;border-bottom:1px solid var(--bcs-seam);vertical-align:middle;color:var(--bcs-ink);overflow:hidden}
     419.bcs-smtp-table th:first-child,.bcs-smtp-table td:first-child{padding-left:20px}
     420.bcs-smtp-table th:last-child,.bcs-smtp-table td:last-child{padding-right:20px}
     421.bcs-smtp-table tbody tr{transition:background var(--bcs-t)}
     422.bcs-smtp-table tbody tr:hover td{background:rgba(255,255,255,.025)}
     423.bcs-smtp-table tbody tr:last-child td{border-bottom:0}
     424.bcs-smtp-col-date{width:112px;color:var(--bcs-ink-2);white-space:nowrap}
     425td.bcs-smtp-col-date{font-size:12px}
     426.bcs-smtp-col-to{width:28%}
     427.bcs-smtp-col-status{width:128px}
     428.bcs-smtp-col-more{width:84px;text-align:right}
     429.bcs-smtp-table .bcs-smtp-view-btn{display:inline-flex;align-items:center;gap:2px;height:30px;padding:0 6px 0 10px;border-radius:7px;font-size:13px;font-weight:600;color:var(--bcs-ink-2);text-decoration:none;transition:color var(--bcs-t),background var(--bcs-t)}
     430.bcs-smtp-view-btn .dashicons{font-size:15px;width:15px;height:15px;transition:transform var(--bcs-t)}
     431.bcs-smtp-table tbody tr:hover .bcs-smtp-view-btn,.bcs-smtp-table .bcs-smtp-view-btn:focus-visible{color:var(--bcs-brand-ink);background:var(--bcs-brand-wash)}
     432.bcs-smtp-view-btn:hover .dashicons{transform:translateX(2px)}
     433.bcs-smtp-view-btn:focus-visible{outline:2px solid var(--bcs-brand-ink);outline-offset:1px}
     434.bcs-smtp-table tbody tr.bcs-smtp-day-row th{padding:16px 20px 8px;font:600 12px/1.2 var(--bcs-font);letter-spacing:.02em;text-transform:none;color:var(--bcs-ink-2);background:var(--bcs-panel);border-bottom:1px solid var(--bcs-seam)}
     435.bcs-smtp-table tbody tr.bcs-smtp-day-row:hover th{background:var(--bcs-panel)}
     436.bcs-smtp-log-error{margin-top:12px;border-radius:10px;border:1px solid rgba(251,113,133,.25);background:rgba(251,113,133,.06);overflow:hidden}
     437.bcs-smtp-log-error-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 10px 8px 16px;border-bottom:1px solid rgba(251,113,133,.16);font-size:12px;font-weight:600;color:var(--bcs-bad)}
     438.bcs-smtp-copy-btn{display:inline-flex;align-items:center;gap:5px;height:28px;padding:0 10px;border:1px solid var(--bcs-seam-strong);border-radius:7px;background:var(--bcs-panel);color:var(--bcs-ink);font:inherit;font-size:12.5px;font-weight:500;cursor:pointer}
     439.bcs-smtp-copy-btn:hover{border-color:var(--bcs-ink-3)}
     440.bcs-smtp-copy-btn .dashicons{font-size:14px;width:14px;height:14px}
     441.bcs-smtp-log-error pre{margin:0;padding:12px 16px;font-size:12.5px;line-height:1.6;color:var(--bcs-ink);white-space:pre-wrap;overflow-wrap:anywhere;background:none}
     442.bcs-smtp-seg-count{margin-left:6px;font-weight:500;font-size:12px;opacity:.7;font-variant-numeric:tabular-nums}
     443.bcs-smtp-seg-count:empty{display:none}
     444.bcs-smtp-table tr[hidden]{display:none!important}
     445td.bcs-smtp-col-to,td.bcs-smtp-col-from{white-space:nowrap;text-overflow:ellipsis;color:var(--bcs-ink-2);font-size:12px}
     446.bcs-smtp-cell-main{display:block;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     447.bcs-smtp-cell-note{display:block;margin-top:4px;font-size:11.5px;color:var(--bcs-ink-3)}
     448.bcs-smtp-table-empty{text-align:center;padding:44px 20px!important;color:var(--bcs-ink-2)}
     449.bcs-smtp-table-empty strong{display:block;color:var(--bcs-ink);font-weight:600;margin-bottom:4px}
     450.bcs-smtp-skel{display:block;height:10px;border-radius:4px;background:var(--bcs-field-hover);animation:bcs-skel-pulse 1.2s ease-in-out infinite}
     451@keyframes bcs-skel-pulse{0%,100%{opacity:.45}50%{opacity:1}}
     452
     453.bcs-smtp-pager{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:12px 20px;border-top:1px solid var(--bcs-seam);background:var(--bcs-bg)}
     454.bcs-smtp-pager-size{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--bcs-ink-2)}
     455.bcs-smtp-pager-nav{display:flex;align-items:center;gap:12px;font-size:13px;color:var(--bcs-ink-2)}
     456
     457/* =====================================================================
     458   Reports
     459   ===================================================================== */
     460.bcs-smtp-chart{position:relative;height:300px;margin-top:20px}
     461.bcs-smtp-chart-empty{position:absolute;inset:0;display:grid;place-items:center;color:var(--bcs-ink-3)}
     462
     463/* Reports */
     464.bcs-smtp-report-tools{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
     465.bcs-smtp-report .bcs-smtp-panel-head{align-items:flex-start;margin-bottom:16px}
     466.bcs-smtp-report .bcs-smtp-panel-head h2{font-size:18px}
     467.bcs-smtp-report .bcs-smtp-panel-sub{margin-top:2px!important}
     468.bcs-smtp-figures--5{grid-template-columns:repeat(5,minmax(0,1fr));gap:0;margin:0 -24px 16px;border-top:1px solid var(--bcs-seam);border-bottom:1px solid var(--bcs-seam)}
     469.bcs-smtp-figures--5 > div{padding:12px 24px;border-left:1px solid var(--bcs-seam)}
     470.bcs-smtp-figures--5 dd{font-size:24px;margin-top:4px}
     471.bcs-smtp-figures--5 > div:first-child{border-left:0}
     472.bcs-smtp-figure-rate dd{color:var(--bcs-ink)}
     473.bcs-smtp-ratebar{display:block;height:4px;margin-top:8px;border-radius:2px;background:color-mix(in srgb,var(--bcs-bad) 35%,var(--bcs-seam-strong));overflow:hidden}
     474.bcs-smtp-ratebar span{display:block;height:100%;background:var(--bcs-ok);border-radius:2px;transition:width .5s var(--bcs-ease)}
     475.bcs-smtp-linechart--lg{height:220px}
     476.bcs-smtp-report .bcs-smtp-chart-empty{margin-top:-120px!important;text-align:center;color:var(--bcs-ink-3);position:relative}
     477.bcs-smtp-report-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.25fr);gap:20px;margin-top:20px;align-items:stretch}
     478.bcs-smtp-report-grid > .bcs-smtp-panel + .bcs-smtp-panel{margin-top:0}
     479.bcs-smtp-highlights{margin:0;display:flex;flex-direction:column}
     480.bcs-smtp-highlights > div{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 0;border-top:1px solid var(--bcs-seam)}
     481.bcs-smtp-highlights > div:first-child{border-top:0;padding-top:0}
     482.bcs-smtp-highlights dt{display:flex;align-items:center;gap:12px;color:var(--bcs-ink-2);font-size:14px}
     483.bcs-smtp-highlights dd{margin:0;font-weight:600;font-size:16px;text-align:right;white-space:nowrap}
     484.bcs-smtp-highlights dd small{font-size:12.5px;font-weight:400;color:var(--bcs-ink-3);margin-left:4px}
     485.bcs-smtp-highlights dd.is-bad{color:var(--bcs-bad)}
     486.bcs-smtp-highlights dd.is-good{color:var(--bcs-ok)}
     487.bcs-smtp-highlights dt > span{display:flex;flex-direction:column}
     488.bcs-smtp-hl-sub{font-size:12px;color:var(--bcs-ink-3);margin-top:1px}
     489.bcs-smtp-hl-icon{display:grid!important;place-items:center;width:32px!important;height:32px!important;flex-shrink:0;border-radius:9px;background:var(--bcs-brand-wash);color:var(--bcs-brand-ink);font-size:17px!important}
     490.bcs-smtp-hl-icon--bad{background:rgba(251,113,133,.13);color:var(--bcs-bad)}
     491.bcs-smtp-hl-icon--ok{background:rgba(52,211,153,.13);color:var(--bcs-ok)}
     492.bcs-smtp-schedule h2{font-size:16px;margin:0}
     493.bcs-smtp-schedule h2 label{cursor:pointer}
     494.bcs-smtp-schedule .bcs-smtp-field{margin-top:18px}
     495.bcs-smtp-field > .bcs-smtp-segmented{align-self:flex-start}
     496.bcs-smtp-report-next{display:flex;align-items:center;gap:8px;margin-top:14px!important;padding:10px 12px;border-radius:var(--bcs-r-sm);background:var(--bcs-brand-wash);color:var(--bcs-ink);font-size:13.5px!important;font-weight:500}
     497.bcs-smtp-report-next .dashicons{color:var(--bcs-brand-ink);font-size:17px;width:17px;height:17px}
     498
     499/* Recipient chips */
     500.bcs-smtp-chips{display:flex;flex-wrap:wrap;align-items:center;gap:6px;min-height:44px;padding:6px 8px;cursor:text;
     501  background:var(--bcs-field);border:1px solid var(--bcs-seam-strong);border-radius:var(--bcs-r-sm);transition:border-color var(--bcs-t),box-shadow var(--bcs-t)}
     502.bcs-smtp-chips:hover{border-color:#40507a}
     503.bcs-smtp-chips:focus-within{border-color:var(--bcs-brand-ink);box-shadow:0 0 0 3px rgba(141,180,255,.18)}
     504.bcs-smtp-chips.is-invalid{border-color:var(--bcs-bad);box-shadow:0 0 0 3px rgba(251,113,133,.18)}
     505.bcs-smtp-chip-list{display:contents;list-style:none;margin:0;padding:0}
     506.bcs-smtp-chip{display:inline-flex;align-items:center;gap:2px;max-width:100%;height:28px;margin:0;padding:0 2px 0 10px;border-radius:99px;
     507  background:var(--bcs-brand-wash);border:1px solid rgba(141,180,255,.28);color:var(--bcs-ink);font-size:13px;line-height:1}
     508.bcs-smtp-chip-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     509.bcs-smtp-chip.is-invalid{background:rgba(251,113,133,.12);border-color:rgba(251,113,133,.45);color:var(--bcs-bad)}
     510.bcs-smtp-chip.is-armed{border-color:var(--bcs-brand-ink);box-shadow:0 0 0 2px rgba(141,180,255,.35)}
     511.bcs-smtp-chip-x{appearance:none;display:grid;place-items:center;width:22px;height:22px;padding:0;border:0;border-radius:50%;background:none;color:var(--bcs-ink-3);cursor:pointer;flex-shrink:0}
     512.bcs-smtp-chip-x:hover{background:rgba(255,255,255,.1);color:var(--bcs-ink)}
     513.bcs-smtp-chip-x .dashicons{font-size:16px;width:16px;height:16px}
     514.bcs-smtp-wrap .bcs-smtp-chips-input{flex:1 1 160px;min-width:120px;height:30px;margin:0;padding:0 4px;border:0;background:none;box-shadow:none;outline:none;color:var(--bcs-ink);font:400 14px/1.4 var(--bcs-font)}
     515.bcs-smtp-wrap .bcs-smtp-chips-input:focus{box-shadow:none;outline:none}
     516.bcs-smtp-wrap .bcs-smtp-chips-input::placeholder{color:#6f7c97}
     517
     518
     519/* Alerts — channel picker, webhook, health checks */
     520.bcs-smtp-alerts-grid{display:grid;grid-template-columns:minmax(0,1fr) 380px;gap:20px;margin-top:20px;align-items:stretch}
     521.bcs-smtp-alerts-grid > .bcs-smtp-panel + .bcs-smtp-panel{margin-top:0}
     522.bcs-smtp-alerts-grid{margin-top:0}
     523.bcs-smtp-health-side{padding:22px;position:sticky;top:52px}
     524.bcs-smtp-health-head{display:flex;gap:14px;align-items:flex-start}
     525.bcs-smtp-health-icon{display:grid!important;place-items:center;width:42px!important;height:42px!important;flex-shrink:0;border-radius:11px;background:rgba(52,211,153,.13);color:var(--bcs-ok);font-size:20px!important}
     526.bcs-smtp-health-head h2{margin:0 0 4px;font-size:16px;line-height:1.3}
     527.bcs-smtp-health-head p{font-size:13px!important;line-height:1.5;color:var(--bcs-ink-2)}
     528.bcs-smtp-health-times{display:grid;grid-template-columns:1fr 1fr;margin:18px 0 0;border:1px solid var(--bcs-seam);border-radius:10px;background:var(--bcs-field)}
     529.bcs-smtp-health-times > div{padding:12px 14px}
     530.bcs-smtp-health-times > div + div{border-left:1px solid var(--bcs-seam)}
     531.bcs-smtp-health-times dt{font-size:12px;color:var(--bcs-ink-3);margin-bottom:4px}
     532.bcs-smtp-health-times dd{margin:0;font-size:14px;font-weight:600;color:var(--bcs-ink)}
     533.bcs-smtp-health-pending{margin-top:16px!important;padding:12px 14px;border-radius:10px;background:var(--bcs-field);border:1px solid var(--bcs-seam);font-size:13px!important;color:var(--bcs-ink-2)}
     534.bcs-smtp-health-rules{list-style:none;margin:18px 0 0;padding:18px 0 0;border-top:1px solid var(--bcs-seam);display:flex;flex-direction:column;gap:12px}
     535.bcs-smtp-health-rules li{display:flex;align-items:center;gap:10px;margin:0;font-size:13.5px;color:var(--bcs-ink)}
     536.bcs-smtp-health-rules .dashicons{display:grid;place-items:center;width:28px;height:28px;flex-shrink:0;border-radius:8px;background:var(--bcs-brand-wash);color:var(--bcs-brand-ink);font-size:15px}
     537.bcs-smtp-webhook{margin-top:20px}
     538.bcs-smtp-webhook-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
     539.bcs-smtp-webhook-guide{display:inline-flex;align-items:center;gap:3px;font-size:13px;font-weight:600;text-decoration:none;white-space:nowrap}
     540.bcs-smtp-webhook-guide .dashicons{font-size:14px;width:14px;height:14px}
     541.bcs-smtp-webhook-guide:hover{text-decoration:underline}
     542.bcs-smtp-webhook input[aria-invalid="true"]{border-color:var(--bcs-bad)}
     543.bcs-smtp-webhook-err{margin-top:8px!important;font-size:13px!important;color:var(--bcs-bad)}
     544.bcs-smtp-alerts-off{display:flex;gap:12px;align-items:flex-start;margin:18px 0 24px;padding:14px 16px;border-radius:10px;background:var(--bcs-field);border:1px solid var(--bcs-seam)}
     545.bcs-smtp-alerts-off[hidden]{display:none}
     546.bcs-smtp-alerts-off .dashicons{color:var(--bcs-warn);font-size:18px;width:18px;height:18px;margin-top:1px}
     547.bcs-smtp-alerts-off p{font-size:13.5px;color:var(--bcs-ink-2)}
     548.bcs-smtp-unsaved{margin-left:auto;display:inline-flex;align-items:center;gap:7px;font-size:13px;color:var(--bcs-warn)}
     549.bcs-smtp-unsaved::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor}
     550.bcs-smtp-unsaved[hidden]{display:none}
     551.bcs-smtp-field-aside{font-weight:400;color:var(--bcs-ink-3);margin-left:6px}
     552.bcs-smtp-webhook-how{margin-top:10px!important;font-size:13px!important;color:var(--bcs-ink-3);line-height:1.5}
     553
     554.bcs-smtp-alerts-card{display:flex;flex-direction:column}
     555.bcs-smtp-card-foot{margin-top:auto;padding-top:20px;border-top:1px solid var(--bcs-seam)}
     556
     557
     558
     559/* Full email view */
     560.bcs-smtp-back{display:inline-flex;align-items:center;gap:4px;margin:6px 0 18px;font-size:13.5px;font-weight:600;color:var(--bcs-ink-2);text-decoration:none}
     561.bcs-smtp-back:hover,.bcs-smtp-back:focus{color:var(--bcs-ink)}
     562.bcs-smtp-back .dashicons{font-size:16px;width:16px;height:16px}
     563.bcs-smtp-mailview [hidden]{display:none!important}
     564.bcs-smtp-mailview-head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:20px;flex-wrap:wrap}
     565.bcs-smtp-mailview-title{min-width:0;flex:1 1 420px}
     566.bcs-smtp-mailview-when{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:10px!important;font-size:13px;color:var(--bcs-ink-2)}
     567.bcs-smtp-mailview h1{margin:0;padding:0;font-size:24px;line-height:1.3;font-weight:600;color:var(--bcs-ink);overflow-wrap:anywhere}
     568.bcs-smtp-mailview-nav{display:flex;gap:8px}
     569.bcs-smtp-mailview-nav .dashicons{font-size:15px;width:15px;height:15px}
     570.bcs-smtp-mailview-nav .is-disabled{opacity:.4;pointer-events:none}
     571.bcs-smtp-mailview-error{margin:0 0 20px}
     572.bcs-smtp-mailview-grid{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:20px;align-items:start}
     573.bcs-smtp-mailview-grid > .bcs-smtp-panel + .bcs-smtp-panel{margin-top:0}
     574.bcs-smtp-mailview-message .bcs-smtp-panel-head h2,.bcs-smtp-mailview-meta h2{margin:0;font-size:16px}
     575.bcs-smtp-mailframe-wrap{border-radius:10px;overflow:hidden;background:#fff;border:1px solid var(--bcs-seam)}
     576.bcs-smtp-mailframe{display:block;width:100%;height:480px;border:0;background:#fff;overflow:hidden}
     577.bcs-smtp-mailview-source{border-radius:10px;border:1px solid var(--bcs-seam);background:var(--bcs-bg);overflow:hidden}
     578.bcs-smtp-mailview-source-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 10px 8px 16px;border-bottom:1px solid var(--bcs-seam);font-size:12px;font-weight:600;color:var(--bcs-ink-2)}
     579.bcs-smtp-mailview-source pre,.bcs-smtp-mailview-text{margin:0;padding:16px;font-size:12.5px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--bcs-ink);max-height:70vh;overflow:auto;background:none}
     580.bcs-smtp-mailview-text{border-radius:10px;border:1px solid var(--bcs-seam);background:var(--bcs-bg);font-family:var(--bcs-mono)}
     581.bcs-smtp-mailview-empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:48px 24px;text-align:center;border-radius:10px;border:1px dashed var(--bcs-seam-strong)}
     582.bcs-smtp-mailview-empty .dashicons{font-size:28px;width:28px;height:28px;color:var(--bcs-ink-3)}
     583.bcs-smtp-mailview-empty strong{font-size:15px;color:var(--bcs-ink)}
     584.bcs-smtp-mailview-empty p{max-width:46ch;font-size:13.5px;color:var(--bcs-ink-2)}
     585.bcs-smtp-mailview-meta{padding:20px 22px}
     586.bcs-smtp-mailview-meta h2{margin-bottom:14px}
     587.bcs-smtp-mailview-facts{display:flex;flex-direction:column;margin:0}
     588.bcs-smtp-mailview-facts > div{padding:10px 0;border-top:1px solid var(--bcs-seam)}
     589.bcs-smtp-mailview-facts > div:first-child{padding-top:0;border-top:0}
     590.bcs-smtp-mailview-facts dt{margin-bottom:3px;font-size:11.5px;color:var(--bcs-ink-3)}
     591.bcs-smtp-mailview-facts dd{margin:0;font-size:13px;color:var(--bcs-ink);overflow-wrap:anywhere}
     592.bcs-smtp-mailview-meta h3{margin:18px 0 8px;font-size:13px;color:var(--bcs-ink)}
     593.bcs-smtp-mailview-atts{display:flex;flex-direction:column;gap:6px;margin:0 0 8px;padding:0;list-style:none}
     594.bcs-smtp-mailview-atts li{display:flex;align-items:center;gap:8px;margin:0;padding:8px 10px;border-radius:8px;background:var(--bcs-field);border:1px solid var(--bcs-seam);font-size:13px;overflow-wrap:anywhere}
     595.bcs-smtp-mailview-atts .dashicons{color:var(--bcs-ink-3);font-size:16px;width:16px;height:16px;flex-shrink:0}
     596.bcs-smtp-mailview-headers{margin-top:18px;padding-top:14px;border-top:1px solid var(--bcs-seam)}
     597.bcs-smtp-mailview-headers summary{cursor:pointer;font-size:13px;font-weight:600;color:var(--bcs-ink)}
     598.bcs-smtp-mailview-headers pre{margin:10px 0 0;padding:12px;border-radius:8px;background:var(--bcs-bg);border:1px solid var(--bcs-seam);font-size:12px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--bcs-ink-2)}
     599.bcs-smtp-mailview-missing{padding:48px 24px;text-align:center}
     600.bcs-smtp-mailview-missing p{margin-top:8px!important;color:var(--bcs-ink-2)}
     601.bcs-smtp-mailview-message{padding:0;overflow:hidden}
     602.bcs-smtp-envelope{display:flex;gap:14px;align-items:flex-start;padding:20px 24px;border-bottom:1px solid var(--bcs-seam)}
     603.bcs-smtp-envelope-avatar{display:grid;place-items:center;width:42px;height:42px;flex-shrink:0;border-radius:50%;background:var(--bcs-brand);color:#fff;font-size:17px;font-weight:600}
     604.bcs-smtp-envelope-lines{min-width:0;flex:1}
     605.bcs-smtp-envelope-from{font-size:14.5px!important;color:var(--bcs-ink);overflow-wrap:anywhere}
     606.bcs-smtp-envelope-from strong{font-weight:600;margin-right:6px}
     607.bcs-smtp-envelope-from span{color:var(--bcs-ink-3);font-size:13px}
     608.bcs-smtp-envelope-to{margin-top:3px!important;font-size:13px!important;color:var(--bcs-ink-2);overflow-wrap:anywhere}
     609.bcs-smtp-envelope-to span{color:var(--bcs-ink-3);margin-right:4px}
     610.bcs-smtp-mailview-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:12px 24px;border-bottom:1px solid var(--bcs-seam);background:rgba(0,0,0,.14)}
     611.bcs-smtp-device-icon{display:block;width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
     612.bcs-smtp-mailview-body{padding:20px 24px 24px}
     613.bcs-smtp-mailframe-stage{border-radius:10px;background:var(--bcs-bg);border:1px solid var(--bcs-seam);padding:0;transition:padding var(--bcs-t)}
     614.bcs-smtp-mailframe-stage.is-mobile{padding:20px 0}
     615.bcs-smtp-mailframe-stage .bcs-smtp-mailframe-wrap{margin:0 auto;border:0;border-radius:10px;transition:max-width var(--bcs-t)}
     616.bcs-smtp-mailframe-stage.is-mobile .bcs-smtp-mailframe-wrap{max-width:390px;border-radius:18px;box-shadow:0 0 0 6px var(--bcs-panel),0 0 0 7px var(--bcs-seam-strong)}
     617.bcs-smtp-mailview-plain pre{font-family:var(--bcs-font);font-size:13.5px;line-height:1.65}
     618.bcs-smtp-mailview-meta{position:sticky;top:52px}
     619.bcs-smtp-mailview-verdict{display:flex;flex-direction:column;align-items:flex-start;gap:8px;margin-bottom:16px;padding:12px 14px;border-radius:10px;background:var(--bcs-field);border:1px solid var(--bcs-seam)}
     620.bcs-smtp-mailview-verdict p{font-size:13px!important;color:var(--bcs-ink-2);line-height:1.5}
     621.bcs-smtp-mailview-note{margin-top:4px!important;font-size:12px!important;color:var(--bcs-ink-3)!important}
     622@media (max-width:1100px){
     623  .bcs-smtp-mailview-meta{position:static}
     624}
     625@media (max-width:600px){
     626  .bcs-smtp-wrap .bcs-smtp-mailview-message{padding:0}
     627  .bcs-smtp-envelope,.bcs-smtp-mailview-toolbar,.bcs-smtp-mailview-body{padding-left:16px;padding-right:16px}
     628}
     629@media (max-width:1100px){
     630  .bcs-smtp-mailview-grid{grid-template-columns:minmax(0,1fr)}
     631}
     632@media (max-width:600px){
     633  .bcs-smtp-mailview h1{font-size:20px}
     634  .bcs-smtp-mailview-nav{width:100%}
     635  .bcs-smtp-mailview-nav > *{flex:1;justify-content:center}
     636}
     637
     638/* =====================================================================
     639   Messages
     640   ===================================================================== */
     641.bcs-smtp-msg:empty{display:none}
     642.bcs-smtp-msg{margin-top:14px}
     643.bcs-smtp-alert{--tone:var(--bcs-brand-ink);display:flex;gap:10px;align-items:flex-start;padding:12px 14px;border-radius:var(--bcs-r-sm);border:1px solid color-mix(in srgb,var(--tone) 35%,transparent);background:color-mix(in srgb,var(--tone) 9%,var(--bcs-panel));font-size:13.5px;line-height:1.5;color:var(--bcs-ink)}
     644.bcs-smtp-alert .dashicons{color:var(--tone);flex-shrink:0;font-size:18px;width:18px;height:18px;margin-top:1px}
     645.bcs-smtp-alert-content{min-width:0;overflow-wrap:anywhere}
     646.bcs-smtp-alert-content strong{font-weight:600}
     647.bcs-smtp-alert-success{--tone:var(--bcs-sent)}
     648.bcs-smtp-alert-error{--tone:var(--bcs-back)}
     649.bcs-smtp-alert-warning{--tone:var(--bcs-held)}
     650.bcs-smtp-alert .is-good{color:var(--bcs-sent)}
     651.bcs-smtp-alert .is-bad{color:var(--bcs-back)}
     652
     653.bcs-smtp-toast{position:fixed;right:24px;bottom:24px;z-index:100000;max-width:min(420px,calc(100vw - 32px));pointer-events:none}
     654.bcs-smtp-toast .bcs-smtp-alert{pointer-events:auto;box-shadow:0 12px 32px rgba(0,0,0,.45),0 2px 6px rgba(0,0,0,.3);animation:bcs-panel-in .22s var(--bcs-ease)}
     655
     656/* =====================================================================
     657   Responsive
     658   ===================================================================== */
     659@media (max-width:1200px){
     660  .bcs-smtp-col-from{display:none}
     661  .bcs-smtp-hero-facts{grid-template-columns:repeat(3,minmax(0,1fr))}
     662  .bcs-smtp-hero-facts > div:nth-child(4){border-left:0}
     663  .bcs-smtp-hero-facts > div:nth-child(n+4){border-top:1px solid var(--bcs-seam)}
     664}
     665@media (max-width:1100px){
     666  .bcs-smtp-overview-grid,.bcs-smtp-split,.bcs-smtp-alerts-grid{grid-template-columns:minmax(0,1fr)}
     667  .bcs-smtp-split > .bcs-smtp-test,.bcs-smtp-health-side{position:static}
     668}
     669@media (max-width:900px){
     670  .bcs-smtp-hero-main{grid-template-columns:auto minmax(0,1fr)}
     671  .bcs-smtp-hero-actions{grid-column:1/-1;justify-content:flex-start}
     672  .bcs-smtp-figures{grid-template-columns:repeat(2,minmax(0,1fr));row-gap:18px}
     673  .bcs-smtp-figures--5{grid-template-columns:repeat(3,minmax(0,1fr));row-gap:0}
     674  .bcs-smtp-figures--5 > div:nth-child(4){border-left:0}
     675  .bcs-smtp-figures--5 > div:nth-child(n+4){border-top:1px solid var(--bcs-seam)}
     676  .bcs-smtp-report-grid{grid-template-columns:minmax(0,1fr)}
     677}
     678@media (max-width:782px){
     679  .bcs-smtp-wrap{padding:16px 12px 40px 0;font-size:15px}
     680  .bcs-smtp-header h1{font-size:19px}
     681  .bcs-smtp-header-actions{width:100%}
     682  .bcs-smtp-header-actions .bcs-smtp-btn{flex:1}
     683  .bcs-smtp-tabs{width:100%;gap:2px;padding:4px}
     684  .bcs-smtp-tab{height:36px;padding:0 11px;font-size:13.5px}
     685  .bcs-smtp-tab .dashicons{font-size:16px;width:16px;height:16px}
     686  .bcs-smtp-panel{padding:18px}
     687  .bcs-smtp-panel--flush{padding:0}
     688  .bcs-smtp-hero{padding:20px 18px 0}
     689  .bcs-smtp-hero-main{gap:14px 16px;padding-bottom:20px}
     690  .bcs-smtp-hero-copy h2{font-size:21px}
     691  .bcs-smtp-beacon{width:42px;height:42px}
     692  .bcs-smtp-beacon span{width:13px;height:13px}
     693  .bcs-smtp-hero-actions .bcs-smtp-btn{flex:1 1 auto}
     694  .bcs-smtp-hero-facts{grid-template-columns:repeat(2,minmax(0,1fr));margin:0 -18px}
     695  .bcs-smtp-hero-facts > div{padding:12px 18px;border-left:0;border-top:1px solid var(--bcs-seam)}
     696  .bcs-smtp-hero-facts > div:nth-child(-n+2){border-top:0}
     697  .bcs-smtp-hero-facts > div:nth-child(even){border-left:1px solid var(--bcs-seam)}
     698  .bcs-smtp-hero-facts > div:nth-child(n+4){border-top:1px solid var(--bcs-seam)}
     699  .bcs-smtp-hero-facts > div:last-child:nth-child(odd){grid-column:1/-1}
     700  .bcs-smtp-hero-facts dd{white-space:normal;overflow-wrap:anywhere}
     701  .bcs-smtp-linechart{height:200px}
     702  .bcs-smtp-linechart--lg{height:190px}
     703  .bcs-smtp-figures--5{grid-template-columns:repeat(2,minmax(0,1fr));margin:0 -18px 20px}
     704  .bcs-smtp-figures--5 > div{padding:14px 18px}
     705  .bcs-smtp-figures--5 > div:nth-child(odd){border-left:0}
     706  .bcs-smtp-figures--5 > div:nth-child(4){border-left:1px solid var(--bcs-seam)}
     707  .bcs-smtp-figures--5 > div:nth-child(n+3){border-top:1px solid var(--bcs-seam)}
     708  .bcs-smtp-figures--5 > div:last-child{grid-column:1/-1;border-left:0}
     709  .bcs-smtp-report-tools{width:100%}
     710  .bcs-smtp-report-tools .bcs-smtp-segmented{width:100%}
     711  .bcs-smtp-activity-foot{margin:16px -18px -18px!important;padding:14px 18px}
     712  .bcs-smtp-field-row,.bcs-smtp-field-row:has(.bcs-smtp-field--port),.bcs-smtp-field-row:has(> .bcs-smtp-field--grow:first-child):not(:has(.bcs-smtp-field--port)){grid-template-columns:minmax(0,1fr);gap:0}
     713  .bcs-smtp-provider-grid,.bcs-smtp-provider-grid--compact{grid-template-columns:repeat(2,minmax(0,1fr))}
     714  .bcs-smtp-wrap .bcs-smtp-input{min-height:44px;height:44px;font-size:16px}
     715  .bcs-smtp-btn{min-height:44px}
     716  .bcs-smtp-btn-sm{min-height:36px}
     717  .bcs-smtp-facts{grid-template-columns:minmax(0,1fr)}
     718  .bcs-smtp-facts > div:nth-child(odd){padding-right:0}
     719  .bcs-smtp-filters{padding:14px}
     720  .bcs-smtp-filters .bcs-smtp-btn{margin-left:0}
     721  .bcs-smtp-filters .bcs-smtp-select{flex:1 1 140px}
     722  .bcs-smtp-segmented{width:100%}
     723  .bcs-smtp-segmented label{flex:1}
     724  .bcs-smtp-segmented span{justify-content:center;padding:0 8px}
     725
     726  /* Email log becomes stacked rows */
     727  .bcs-smtp-table{table-layout:auto}
     728  .bcs-smtp-table thead{display:none}
     729  .bcs-smtp-table tbody tr{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:4px 8px;padding:12px 14px;border-bottom:1px solid var(--bcs-seam)}
     730  .bcs-smtp-table tbody tr:last-child{border-bottom:0}
     731  .bcs-smtp-table td{display:block;padding:0!important;border:0;width:auto!important;text-align:left}
     732  .bcs-smtp-table td.bcs-smtp-col-date{grid-column:1;grid-row:1}
     733  .bcs-smtp-table td.bcs-smtp-col-status{grid-column:2;grid-row:1;justify-self:end}
     734  .bcs-smtp-table td.bcs-smtp-col-more{grid-column:3;grid-row:1;align-self:center}
     735  .bcs-smtp-table .bcs-smtp-view-btn{height:26px;padding:0 2px 0 6px}
     736  .bcs-smtp-table tbody tr.bcs-smtp-day-row{display:block;padding:0}
     737  .bcs-smtp-table tbody tr.bcs-smtp-day-row th{display:block;padding:14px 14px 8px}
     738  .bcs-smtp-table td.bcs-smtp-col-subject{grid-column:1/-1}
     739  .bcs-smtp-table td.bcs-smtp-col-to{grid-column:1/-1}
     740  .bcs-smtp-table td.bcs-smtp-table-empty{grid-column:1/-1;text-align:center}
     741  .bcs-smtp-pager{padding:12px 14px}
     742  .bcs-smtp-pager-nav{width:100%;justify-content:space-between}
     743  .bcs-smtp-toast{left:16px;right:16px;bottom:16px}
     744}
     745@media (max-width:480px){
     746  .bcs-smtp-figures dd{font-size:24px}
     747}
     748
     749/* Tablet: five-item rows fill as 3 + 2 (two halves) instead of leaving an empty cell. */
     750@media (min-width:783px) and (max-width:1200px){
     751  .bcs-smtp-hero-facts:has(> div:nth-child(5):last-child){grid-template-columns:repeat(6,minmax(0,1fr))}
     752  .bcs-smtp-hero-facts:has(> div:nth-child(5):last-child) > div:nth-child(-n+3){grid-column:span 2}
     753  .bcs-smtp-hero-facts:has(> div:nth-child(5):last-child) > div:nth-child(n+4){grid-column:span 3}
     754}
     755@media (min-width:783px) and (max-width:900px){
     756  .bcs-smtp-figures--5{grid-template-columns:repeat(6,minmax(0,1fr))}
     757  .bcs-smtp-figures--5 > div:nth-child(-n+3){grid-column:span 2}
     758  .bcs-smtp-figures--5 > div:nth-child(n+4){grid-column:span 3}
     759}
     760/* Tablet: the four activity numbers fit on one row; the two side cards sit side by side. */
     761@media (min-width:601px) and (max-width:900px){
     762  .bcs-smtp-figures:not(.bcs-smtp-figures--5){grid-template-columns:repeat(4,minmax(0,1fr))}
     763}
     764@media (min-width:601px) and (max-width:1100px){
     765  .bcs-smtp-side{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:stretch}
     766  .bcs-smtp-side > :only-child{grid-column:1/-1}
     767}
     768/* Tablet email log: the recipient moves under the subject so subjects get the room. */
     769.bcs-smtp-cell-to{display:none}
     770@media (min-width:783px) and (max-width:1100px){
     771  /* Collapsed, not display:none: the day rows span five columns, so all five must still exist. */
     772  .bcs-smtp-table .bcs-smtp-col-to{width:0!important;padding:0!important;font-size:0;border-left:0;border-right:0;overflow:hidden}
     773  .bcs-smtp-cell-to{display:block;margin-top:3px;font-size:12px;color:var(--bcs-ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     774}
     775/* Phone: one fact per line, label left and value right, so values don't break mid-word;
     776   and no desktop/mobile preview switch on a screen that is already phone-sized. */
     777@media (max-width:600px){
     778  .bcs-smtp-hero-facts{grid-template-columns:minmax(0,1fr)}
     779  .bcs-smtp-hero-facts > div{display:flex;align-items:baseline;justify-content:space-between;gap:16px;grid-column:auto!important;border-left:0!important;border-top:1px solid var(--bcs-seam)!important}
     780  .bcs-smtp-hero-facts > div:first-child{border-top:0!important}
     781  .bcs-smtp-hero-facts dt{margin:0;flex-shrink:0}
     782  .bcs-smtp-hero-facts dd{text-align:right;min-width:0}
     783  .bcs-smtp-device{display:none!important}
     784}
     785
     786@media (prefers-reduced-motion:reduce){
     787  .bcs-smtp-tab-pill{transition:none}
     788  .bcs-smtp-wrap *,.bcs-smtp-wrap *::after,.bcs-smtp-toast *{animation:none!important;transition:none!important}
     789}
     790.bcs-smtp-log-retention{margin-bottom:16px}
     791.bcs-smtp-log-retention a{margin-left:6px;font-weight:600}
     792
     793/* Failure alerts card — header and channel list */
     794.bcs-smtp-alerts-card{padding:0;overflow:hidden}
     795.bcs-smtp-alerts-head{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:14px;padding:22px 24px}
     796.bcs-smtp-alerts-badge{display:grid!important;place-items:center;width:42px!important;height:42px!important;border-radius:11px;background:var(--bcs-brand-wash);color:var(--bcs-brand-ink);font-size:20px!important}
     797.bcs-smtp-alerts-intro h2{margin:0 0 3px;font-size:16px;line-height:1.3}
     798.bcs-smtp-alerts-intro h2 label{cursor:pointer}
     799.bcs-smtp-alerts-intro p{font-size:13px!important;color:var(--bcs-ink-2)}
     800.bcs-smtp-alerts-card .bcs-smtp-reveal{padding:0 24px}
     801.bcs-smtp-ch-list-head{display:flex;align-items:center;justify-content:space-between;padding:14px 0 10px;border-top:1px solid var(--bcs-seam);font-size:12px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--bcs-ink-3)}
     802.bcs-smtp-ch-count{font-weight:500;letter-spacing:0;text-transform:none;font-size:12.5px}
     803.bcs-smtp-ch-list{list-style:none;margin:0 0 20px;padding:0;border:1px solid var(--bcs-seam);border-radius:12px;overflow:hidden;background:var(--bcs-bg)}
     804.bcs-smtp-ch{margin:0;border-top:1px solid var(--bcs-seam)}
     805.bcs-smtp-ch:first-child{border-top:0}
     806.bcs-smtp-ch-row{display:flex;align-items:center;gap:12px;padding-right:16px;transition:background var(--bcs-t)}
     807.bcs-smtp-ch-row:hover{background:rgba(255,255,255,.02)}
     808.bcs-smtp-ch.is-open .bcs-smtp-ch-row{background:var(--bcs-field)}
     809.bcs-smtp-ch-main{flex:1;min-width:0;display:flex;align-items:center;gap:12px;padding:12px 4px 12px 16px;border:0;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer}
     810.bcs-smtp-ch-main:focus-visible{outline:2px solid var(--bcs-brand-ink);outline-offset:-2px;border-radius:10px}
     811.bcs-smtp-ch-icon{display:grid!important;place-items:center;width:34px!important;height:34px!important;flex-shrink:0;border-radius:9px;background:var(--bcs-panel);border:1px solid var(--bcs-seam-strong);color:var(--bcs-ink-3);font-size:17px!important;transition:all var(--bcs-t)}
     812.bcs-smtp-ch:not([data-state="off"]) .bcs-smtp-ch-icon{background:var(--bcs-brand);border-color:var(--bcs-brand);color:#fff}
     813.bcs-smtp-ch-text{min-width:0;flex:1;display:flex;flex-direction:column;gap:1px}
     814.bcs-smtp-ch-name{font-size:14px;font-weight:600;color:var(--bcs-ink)}
     815.bcs-smtp-ch-sub{font-size:12.5px;color:var(--bcs-ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     816.bcs-smtp-ch-pill{flex-shrink:0;padding:3px 9px;border-radius:99px;font-size:11.5px;font-weight:600;line-height:1.4;color:var(--bcs-ink-3);background:var(--bcs-field);border:1px solid var(--bcs-seam)}
     817.bcs-smtp-ch[data-state="ready"] .bcs-smtp-ch-pill{color:var(--bcs-ok);background:rgba(52,211,153,.1);border-color:rgba(52,211,153,.28)}
     818.bcs-smtp-ch[data-state="missing"] .bcs-smtp-ch-pill{color:var(--bcs-warn);background:rgba(251,191,36,.1);border-color:rgba(251,191,36,.28)}
     819.bcs-smtp-ch[data-state="bad"] .bcs-smtp-ch-pill{color:var(--bcs-bad);background:rgba(251,113,133,.1);border-color:rgba(251,113,133,.3)}
     820.bcs-smtp-ch-chevron{flex-shrink:0;color:var(--bcs-ink-3);font-size:16px!important;width:16px!important;height:16px!important;transition:transform var(--bcs-t),opacity var(--bcs-t)}
     821.bcs-smtp-ch[data-state="off"] .bcs-smtp-ch-chevron{opacity:0}
     822.bcs-smtp-ch.is-open .bcs-smtp-ch-chevron{transform:rotate(180deg)}
     823.bcs-smtp-ch .bcs-smtp-webhook{margin:0;padding:4px 16px 16px 62px;background:var(--bcs-field)}
     824.bcs-smtp-ch .bcs-smtp-webhook .bcs-smtp-field-label{font-size:12.5px}
     825.bcs-smtp-alerts-card .bcs-smtp-alerts-off{margin:0 24px 20px}
     826.bcs-smtp-alerts-card .bcs-smtp-card-foot{margin:auto 0 0;padding:16px 24px;border-top:1px solid var(--bcs-seam);background:rgba(0,0,0,.14)}
     827.bcs-smtp-alerts-card .bcs-smtp-msg{margin:0 24px 16px}
     828@media (max-width:600px){
     829  .bcs-smtp-alerts-head{padding:18px}
     830  .bcs-smtp-alerts-card .bcs-smtp-reveal{padding:0 18px}
     831  .bcs-smtp-ch-sub{display:none}
     832  .bcs-smtp-ch .bcs-smtp-webhook{padding:4px 14px 14px}
     833  .bcs-smtp-alerts-card .bcs-smtp-card-foot{padding:14px 18px}
     834  .bcs-smtp-alerts-card .bcs-smtp-alerts-off,.bcs-smtp-alerts-card .bcs-smtp-msg{margin-left:18px;margin-right:18px}
     835}
     836
     837/* Connection — header, labelled rows, provider chips, footer */
     838.bcs-smtp-setup{padding:0;overflow:hidden;display:flex;flex-direction:column}
     839.bcs-smtp-setup-head{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:14px;padding:22px 24px}
     840.bcs-smtp-setup-badge,.bcs-smtp-test-badge{display:grid!important;place-items:center;width:42px!important;height:42px!important;border-radius:11px;background:var(--bcs-brand-wash);color:var(--bcs-brand-ink);font-size:20px!important}
     841.bcs-smtp-setup-head h2{margin:0 0 3px;font-size:16px;line-height:1.3}
     842.bcs-smtp-setup-head h2 label{cursor:pointer}
     843.bcs-smtp-setup-head p{font-size:13px!important;color:var(--bcs-ink-2)}
     844.bcs-smtp-provider-chips{display:grid;grid-template-columns:repeat(auto-fill,minmax(138px,1fr));gap:6px}
     845.bcs-smtp-provider-chips .bcs-smtp-provider{min-height:40px;padding:6px 10px;gap:9px;border-radius:9px}
     846.bcs-smtp-provider-chips .bcs-smtp-provider-mark{width:24px;height:24px;border-radius:6px;font-size:10px}
     847.bcs-smtp-provider-chips .bcs-smtp-provider-mark .dashicons{font-size:14px;width:14px;height:14px}
     848.bcs-smtp-provider-chips .bcs-smtp-provider-name{font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     849.bcs-smtp-inline-switch{display:flex;align-items:flex-start;gap:10px;margin:14px 0 0;font-size:13.5px;color:var(--bcs-ink);cursor:pointer;line-height:1.45}
     850.bcs-smtp-toggle--sm{width:34px;height:20px;margin-top:1px}
     851.bcs-smtp-toggle--sm .bcs-smtp-toggle-slider::before{width:14px;height:14px}
     852.bcs-smtp-toggle--sm input:checked + .bcs-smtp-toggle-slider::before{transform:translateX(14px)}
     853.bcs-smtp-setup-foot{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin-top:auto;padding:16px 24px;border-top:1px solid var(--bcs-seam);background:rgba(0,0,0,.14)}
     854.bcs-smtp-setup-foot .bcs-smtp-msg{flex-basis:100%;margin-top:0}
     855.bcs-smtp-test-head{display:flex;gap:14px;align-items:flex-start;margin-bottom:6px}
     856.bcs-smtp-test .bcs-smtp-test-badge{flex-shrink:0;background:rgba(52,211,153,.13);color:var(--bcs-ok)}
     857.bcs-smtp-test-head h2{margin:0 0 3px;font-size:16px}
     858.bcs-smtp-test-head p{font-size:13px!important;color:var(--bcs-ink-2);line-height:1.5}
     859@media (max-width:782px){
     860  .bcs-smtp-setup-head{padding:18px}
     861  .bcs-smtp-setup-body{padding:4px 18px 20px}
     862  .bcs-smtp-setup-body .bcs-smtp-field-row .bcs-smtp-field + .bcs-smtp-field{margin-top:14px!important}
     863  .bcs-smtp-setup-foot{padding:14px 18px}
     864  .bcs-smtp-setup-foot .bcs-smtp-btn{flex:1 1 100%}
     865  .bcs-smtp-provider-chips{grid-template-columns:repeat(2,minmax(0,1fr))}
     866}
     867
     868/* Connection — one compact column */
     869.bcs-smtp-setup-body{display:flex;flex-direction:column;gap:20px;padding:4px 24px 24px}
     870.bcs-smtp-setup-body .bcs-smtp-field,.bcs-smtp-setup-body .bcs-smtp-field-row .bcs-smtp-field{margin-top:0}
     871.bcs-smtp-setup-body .bcs-smtp-field-row{gap:14px}
     872.bcs-smtp-setup-body .bcs-smtp-field-label{margin-bottom:7px}
     873.bcs-smtp-setup-body .bcs-smtp-provider-chips{grid-template-columns:repeat(auto-fill,minmax(118px,1fr))}
     874.bcs-smtp-setup-body .bcs-smtp-provider-note{margin-top:8px!important;padding:0;background:none;border:0;font-size:12.5px!important;color:var(--bcs-ink-3)}
     875.bcs-smtp-setup-body .bcs-smtp-provider-note .dashicons{font-size:15px;width:15px;height:15px;margin-top:1px}
     876.bcs-smtp-setup-body .bcs-smtp-auth-warning{margin:0}
     877.bcs-smtp-setup-body .bcs-smtp-disclosure{margin:0}
     878.bcs-smtp-setup-body .bcs-smtp-disclosure-body .bcs-smtp-field{margin-top:14px}
     879.bcs-smtp-setup-body .bcs-smtp-disclosure-body .bcs-smtp-field-row .bcs-smtp-field{margin-top:0}
     880#bcs-smtp-pw-status:empty,#bcs-smtp-pw-status.is-good{display:none}
     881.bcs-smtp-setup-head{border-bottom:0}
     882.bcs-smtp-setup-body .bcs-smtp-auth-switch{margin:0 0 -6px}
     883.bcs-smtp-provider-picked{display:flex;align-items:center;gap:11px;min-height:44px;padding:7px 14px 7px 9px;border-radius:10px;background:var(--bcs-field);border:1px solid var(--bcs-seam-strong)}
     884.bcs-smtp-provider-picked[hidden]{display:none}
     885.bcs-smtp-provider-picked .bcs-smtp-provider-mark{width:28px;height:28px;border-radius:7px;font-size:10.5px;background:var(--tone);border-color:var(--tone);color:#fff}
     886.bcs-smtp-picked-name{flex:1;min-width:0;font-size:14px;font-weight:600;color:var(--bcs-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
     887.bcs-smtp-provider-picked .bcs-smtp-link{font-weight:600}
     888.bcs-smtp-setup .bcs-smtp-smtp-off{margin:0 24px 24px}
     889.bcs-smtp-setup .bcs-smtp-smtp-off .dashicons{color:var(--bcs-warn)}
     890.bcs-smtp-setup-body[hidden]{display:none}
  • bcs-smtp-manager/trunk/assets/js/admin.js

    r3652946 r3762759  
    1 /* global bcsSmtpData, jQuery, Chart */
     1/* global bcsSmtpData, jQuery */
    22( function ( $ ) {
    33'use strict';
    44
    5 /* Always dark mode */
    6 document.documentElement.setAttribute( 'data-bcs-smtp-theme', 'dark' );
     5var i18n = ( bcsSmtpData && bcsSmtpData.i18n ) || {};
     6var T = function ( key, fallback ) { return i18n[ key ] || fallback || ''; };
     7
     8/* Minimal sprintf for %s / %1$s placeholders in translated strings. */
     9function fmt( str ) {
     10        var args = Array.prototype.slice.call( arguments, 1 );
     11        var i = 0;
     12        return String( str ).replace( /%(\d+)\$s|%s/g, function ( m, n ) {
     13                return n ? args[ n - 1 ] : args[ i++ ];
     14        } );
     15}
     16
     17/* Storage can throw in private windows or with blocked site data. */
     18var store = {
     19        get: function ( k ) { try { return window.localStorage.getItem( k ); } catch ( e ) { return null; } },
     20        set: function ( k, v ) { try { window.localStorage.setItem( k, v ); } catch ( e ) {} }
     21};
    722
    823/* =====================================================================
    9    Tab Navigation — with persistence
     24   Busy-state + message helpers
    1025   ===================================================================== */
    11 var TAB_KEY = 'bcs_smtp_active_tab';
    12 
    13 function getStoredTab() { return localStorage.getItem( TAB_KEY ) || 'settings'; }
    14 
    15 /* Scroll the tab bar so $tab's left edge aligns with the container's left edge —
    16    keeps the active tab pinned to the start instead of wherever it happened to land. */
    17 function scrollTabIntoViewLeft( $tab, $container ) {
    18         if ( ! $tab.length || ! $container.length ) { return; }
    19         var containerEl = $container[ 0 ];
    20         var delta = $tab[ 0 ].getBoundingClientRect().left - containerEl.getBoundingClientRect().left;
    21         containerEl.scrollLeft += delta;
    22 }
    23 
    24 function setActiveTab( panel ) {
    25         localStorage.setItem( TAB_KEY, panel );
    26         $( '.bcs-smtp-tab' ).removeClass( 'active' );
    27         var $activeTab = $( '.bcs-smtp-tab[data-panel="' + panel + '"]' ).addClass( 'active' );
    28         scrollTabIntoViewLeft( $activeTab, $( '.bcs-smtp-tabs' ) );
     26function setBusy( $btn, label ) {
     27        if ( ! $btn.data( 'bcsLabel' ) ) { $btn.data( 'bcsLabel', $btn.html() ); }
     28        $btn.prop( 'disabled', true ).addClass( 'is-busy' ).attr( 'aria-busy', 'true' ).text( label );
     29}
     30function clearBusy( $btn ) {
     31        $btn.prop( 'disabled', false ).removeClass( 'is-busy' ).removeAttr( 'aria-busy' );
     32        if ( $btn.data( 'bcsLabel' ) ) { $btn.html( $btn.data( 'bcsLabel' ) ); }
     33}
     34
     35function alertHtml( type, msg ) {
     36        var icons = { success: 'yes-alt', error: 'dismiss', warning: 'warning', info: 'info-outline' };
     37        return '<div class="bcs-smtp-alert bcs-smtp-alert-' + type + '">' +
     38                '<span class="dashicons dashicons-' + ( icons[ type ] || icons.info ) + '" aria-hidden="true"></span>' +
     39                '<div class="bcs-smtp-alert-content">' + msg + '</div></div>';
     40}
     41
     42function showInlineMsg( selector, type, msg ) {
     43        var $el = $( selector );
     44        clearTimeout( $el.data( 'bcsHideTimer' ) );
     45        $el.html( alertHtml( type, msg ) );
     46        // Errors and warnings stay until the next message so they can actually be read.
     47        if ( type !== 'success' ) { return; }
     48        $el.data( 'bcsHideTimer', setTimeout( function () { $el.empty(); }, 7000 ) );
     49}
     50
     51function toast( type, msg ) {
     52        var $t = $( '#bcs-smtp-toast' );
     53        clearTimeout( $t.data( 'bcsHideTimer' ) );
     54        $t.html( alertHtml( type, msg ) );
     55        $t.data( 'bcsHideTimer', setTimeout( function () { $t.empty(); }, type === 'success' ? 4000 : 9000 ) );
     56}
     57
     58function errorText( r, fallback ) {
     59        if ( r && r.data ) {
     60                if ( typeof r.data === 'string' ) { return r.data; }
     61                if ( r.data.message ) { return r.data.message; }
     62        }
     63        return fallback;
     64}
     65function failText( xhr ) {
     66        return xhr && xhr.status ? fmt( T( 'requestFailedCode' ), xhr.status ) : T( 'requestFailed' );
     67}
     68
     69/* =====================================================================
     70   Overview health banner — re-rendered from saved state after anything that
     71   changes it (save, health check, test email), so it never shows a stale verdict.
     72   ===================================================================== */
     73var healthDirty = false;
     74var overviewShown = false;
     75
     76function refreshHealthBanner() {
     77        healthDirty = false;
     78        $.post( bcsSmtpData.ajaxurl, { action: 'bcs_smtp_get_health_banner', nonce: bcsSmtpData.nonce }, function ( r ) {
     79                if ( r && r.success && r.data && r.data.html ) {
     80                        $( '#bcs-smtp-health-banner' ).html( r.data.html );
     81                        syncConnectionDot();
     82                }
     83        } );
     84}
     85
     86/* After a save, test or check the banner is re-rendered right away, even from another tab,
     87   so the Connection tab's status dot never shows an old result. */
     88function markHealthDirty() {
     89        refreshHealthBanner();
     90}
     91
     92/* The Connection tab's dot mirrors the Overview banner: green flowing, red failing, amber otherwise */
     93function syncConnectionDot() {
     94        var $hero = $( '#bcs-smtp-health-banner .bcs-smtp-hero' );
     95        if ( ! $hero.length ) { return; }
     96        var tone = ( /bcs-smtp-hero--(ok|bad|warn|off)/.exec( $hero.attr( 'class' ) ) || [] )[ 1 ] || 'off';
     97        $( '#bcs-smtp-conn-dot' ).attr( 'data-tone', 'off' === tone ? 'warn' : tone );
     98        $( '#bcs-smtp-conn-dot-label' ).text( ' (' + $.trim( $hero.find( '#bcs-smtp-hero-title' ).text() ) + ')' );
     99}
     100
     101/* The active tab's highlight slides between tabs instead of jumping */
     102function movePill( instant ) {
     103        var $nav = $( '.bcs-smtp-tabs' );
     104        var tab  = $nav.find( '.bcs-smtp-tab[aria-selected="true"]' )[ 0 ];
     105        var pill = $nav.find( '.bcs-smtp-tab-pill' )[ 0 ];
     106        if ( ! tab || ! pill ) { return; }
     107        $nav.toggleClass( 'is-instant', !! instant ).addClass( 'has-pill' );
     108        pill.style.width     = tab.offsetWidth + 'px';
     109        pill.style.height    = tab.offsetHeight + 'px';
     110        pill.style.transform = 'translate(' + tab.offsetLeft + 'px,' + tab.offsetTop + 'px)';
     111        if ( instant ) { setTimeout( function () { $nav.removeClass( 'is-instant' ); }, 50 ); }
     112}
     113$( window ).on( 'resize', function () { movePill( true ); } );
     114if ( document.fonts && document.fonts.ready ) { document.fonts.ready.then( function () { movePill( true ); } ); }
     115
     116/* =====================================================================
     117   Tabs — ARIA tablist with arrow-key support and persistence
     118   ===================================================================== */
     119var TAB_KEY = 'bcs_smtp_tab_v2';
     120var emailLogLoaded = false;
     121
     122function setActiveTab( panel, focusTab ) {
     123        var $tab = $( '.bcs-smtp-tab[data-panel="' + panel + '"]' );
     124        if ( ! $tab.length ) { panel = 'overview'; $tab = $( '.bcs-smtp-tab[data-panel="overview"]' ); }
     125        store.set( TAB_KEY, panel );
     126        // Keep the URL fragment in step with the active tab, so a refresh (or a copied link)
     127        // opens the tab you were on instead of whichever one the URL happened to name.
     128        if ( window.history && history.replaceState && window.location.hash !== '#' + panel ) {
     129                history.replaceState( null, '', window.location.pathname + window.location.search + '#' + panel );
     130        }
     131
     132        var firstPaint = ! $( '.bcs-smtp-tabs' ).hasClass( 'has-pill' );
     133        $( '.bcs-smtp-tab' ).attr( { 'aria-selected': 'false', tabindex: '-1' } );
     134        $tab.attr( { 'aria-selected': 'true', tabindex: '0' } );
     135        movePill( firstPaint );
     136        if ( focusTab ) { $tab.trigger( 'focus' ); }
     137
     138        var tabsEl = $( '.bcs-smtp-tabs' )[ 0 ];
     139        if ( tabsEl && $tab[ 0 ] ) {
     140                var delta = $tab[ 0 ].getBoundingClientRect().left - tabsEl.getBoundingClientRect().left;
     141                if ( delta < 0 || delta + $tab[ 0 ].offsetWidth > tabsEl.clientWidth ) { tabsEl.scrollLeft += delta - 8; }
     142        }
     143
    29144        $( '.bcs-smtp-tab-panel' ).removeClass( 'active' );
    30145        $( '#bcs-smtp-panel-' + panel ).addClass( 'active' );
    31146
    32         // Lazy-load data on first visit to these panels
    33         if ( panel === 'email-log' && ! emailLogLoaded ) {
    34                 emailLogLoaded = true;
    35                 loadEmailLog();
    36         }
    37         if ( panel === 'email-report' && ! emailReportLoaded ) {
    38                 emailReportLoaded = true;
    39                 loadEmailReport();
    40         }
     147        // Background health checks can change the verdict too, so redraw on every return to Overview.
     148        if ( panel === 'overview' ) { initOverviewChart(); if ( healthDirty || overviewShown ) { refreshHealthBanner(); } overviewShown = true; }
     149        if ( panel === 'email-log' && ! emailLogLoaded ) { emailLogLoaded = true; loadEmailLog(); }
     150        if ( panel === 'reports' ) { initReportChart(); }
    41151}
    42152
    … …  
    45155} );
    46156
    47 $( function () {
    48         var stored = getStoredTab();
    49         if ( stored && $( '.bcs-smtp-tab[data-panel="' + stored + '"]' ).length ) {
    50                 setActiveTab( stored );
    51         }
     157$( document ).on( 'keydown', '.bcs-smtp-tab', function ( e ) {
     158        var $tabs = $( '.bcs-smtp-tab' );
     159        var idx   = $tabs.index( this );
     160        var next  = null;
     161        if ( e.key === 'ArrowRight' ) { next = ( idx + 1 ) % $tabs.length; }
     162        if ( e.key === 'ArrowLeft' )  { next = ( idx - 1 + $tabs.length ) % $tabs.length; }
     163        if ( e.key === 'Home' )       { next = 0; }
     164        if ( e.key === 'End' )        { next = $tabs.length - 1; }
     165        if ( next === null ) { return; }
     166        e.preventDefault();
     167        setActiveTab( $tabs.eq( next ).data( 'panel' ), true );
     168} );
     169
     170/* Any [data-goto] control jumps to a tab; optional data-focus moves focus, data-provider-pick preselects. */
     171$( document ).on( 'click', '[data-goto]', function ( e ) {
     172        e.preventDefault();
     173        var $el = $( this );
     174        var logStatus = $el.data( 'log-status' );
     175        if ( logStatus !== undefined ) {
     176                $( '.bcs-smtp-log-status-radio[value="' + logStatus + '"]' ).prop( 'checked', true );
     177                $( '#bcs-smtp-log-status' ).val( logStatus );
     178                if ( emailLogLoaded ) { loadEmailLog( 1 ); }
     179        }
     180        setActiveTab( $el.data( 'goto' ) );
     181        var pick = $el.data( 'provider-pick' );
     182        if ( pick ) {
     183                $( '.bcs-smtp-provider-radio[value="' + pick + '"]' ).prop( 'checked', true ).trigger( 'change' );
     184        }
     185        var focus = $el.data( 'focus' );
     186        window.scrollTo( { top: 0, behavior: 'auto' } );
     187        if ( focus ) { setTimeout( function () { $( focus ).trigger( 'focus' ); }, 30 ); }
    52188} );
    53189
    54190/* =====================================================================
    55    Accordion toggle
     191   CONNECTION: provider presets
    56192   ===================================================================== */
    57 $( document ).on( 'click', '.bcs-smtp-accordion-head', function () {
    58         $( this ).closest( '.bcs-smtp-accordion' ).toggleClass( 'open' );
    59 } );
    60 
    61 /* =====================================================================
    62    SETTINGS TAB: SMTP toggle → accordion
    63    ===================================================================== */
     193var ENC_PORTS = { ssl: 465, tls: 587, none: 25 };
     194var PRESET_HOSTS = $( '.bcs-smtp-provider-radio' ).not( '[value="other"]' ).map( function () { return $( this ).data( 'host' ); } ).get().filter( Boolean );
     195var PRESET_USERS = $( '.bcs-smtp-provider-radio' ).map( function () { return $( this ).data( 'user' ); } ).get().filter( Boolean );
     196
     197function updateServerSummary() {
     198        var host = $.trim( $( '#bcs-smtp-smtp-host' ).val() );
     199        var port = $( '#bcs-smtp-smtp-port' ).val();
     200        var enc  = ( $( '.bcs-smtp-enc-radio:checked' ).val() || '' ).toUpperCase();
     201        $( '#bcs-smtp-server-summary' ).text( host ? host + ' · ' + port + ' · ' + enc : T( 'pickServer' ) );
     202}
     203
     204function setDisclosure( $d, open ) {
     205        $d.toggleClass( 'is-open', open ).find( '.bcs-smtp-disclosure-head' ).attr( 'aria-expanded', open ? 'true' : 'false' );
     206}
     207
     208$( document ).on( 'click', '.bcs-smtp-disclosure-head', function () {
     209        var $d = $( this ).closest( '.bcs-smtp-disclosure' );
     210        setDisclosure( $d, ! $d.hasClass( 'is-open' ) );
     211} );
     212
     213/* Remember what was typed for each provider while the user clicks around, so switching
     214   to another provider and back never loses a server, port, encryption or username. */
     215var providerMemory  = {};
     216var currentProvider = $( '.bcs-smtp-provider-radio:checked' ).val() || '';
     217
     218function readConnectionFields() {
     219        return {
     220                host : $( '#bcs-smtp-smtp-host' ).val(),
     221                port : $( '#bcs-smtp-smtp-port' ).val(),
     222                enc  : $( '.bcs-smtp-enc-radio:checked' ).val(),
     223                user : $( '#bcs-smtp-smtp-username' ).val()
     224        };
     225}
     226
     227function writeConnectionFields( f ) {
     228        $( '#bcs-smtp-smtp-host' ).val( f.host );
     229        $( '#bcs-smtp-smtp-port' ).val( f.port );
     230        if ( f.enc ) { $( '.bcs-smtp-enc-radio[value="' + f.enc + '"]' ).prop( 'checked', true ); }
     231        $( '#bcs-smtp-smtp-username' ).val( f.user );
     232}
     233
     234/* SMTP switch off: the form folds away, like the Alerts card, and a note says what that means */
    64235$( document ).on( 'change', '#bcs-smtp-smtp-enabled', function () {
    65         $( '#bcs-smtp-smtp-accordion' ).toggleClass( 'open', this.checked );
    66 } );
    67 
    68 /* Encryption radio → auto-fill port */
    69 var ENC_PORTS = { ssl: 465, tls: 587, none: 25 };
     236        $( '#bcs-smtp-setup-body' ).prop( 'hidden', ! this.checked );
     237        $( '#bcs-smtp-smtp-off' ).prop( 'hidden', this.checked );
     238} );
     239
     240/* Provider picker: after a choice, show just that provider with a Change link */
     241function syncPickedProvider() {
     242        var $r = $( '.bcs-smtp-provider-radio:checked' );
     243        if ( ! $r.length ) { return; }
     244        var $chip = $r.closest( '.bcs-smtp-provider' );
     245        $( '#bcs-smtp-picked-mark' ).html( $chip.find( '.bcs-smtp-provider-mark' ).html() ).attr( 'style', $chip.attr( 'style' ) );
     246        $( '#bcs-smtp-picked-name' ).text( $chip.attr( 'title' ) );
     247}
     248function setProviderListOpen( open ) {
     249        if ( ! $( '.bcs-smtp-provider-radio:checked' ).length ) { open = true; } // Nothing chosen yet: the list stays.
     250        $( '#bcs-smtp-provider-chips' ).prop( 'hidden', ! open );
     251        $( '#bcs-smtp-provider-picked' ).prop( 'hidden', open );
     252        $( '#bcs-smtp-provider-change' ).attr( 'aria-expanded', open ? 'true' : 'false' );
     253}
     254$( document ).on( 'click', '#bcs-smtp-provider-change', function () {
     255        setProviderListOpen( true );
     256        $( '.bcs-smtp-provider-radio:checked' ).trigger( 'focus' );
     257} );
     258/* A pointer pick closes the list straight away; keyboard users can arrow through it and it closes when they move on.
     259   Pressing the mouse on a chip moves focus off the current one before the click lands, so focus leaving
     260   during a press inside the list must not close it. */
     261var providerPress = false;
     262$( document ).on( 'mousedown touchstart', '#bcs-smtp-provider-chips', function () { providerPress = true; } );
     263$( document ).on( 'click', '.bcs-smtp-provider', function ( e ) {
     264        if ( e.target.tagName === 'INPUT' ) { return; } // The label's own click already handled this pick.
     265        providerPress = false;
     266        setTimeout( function () { syncPickedProvider(); setProviderListOpen( false ); }, 0 );
     267} );
     268$( document ).on( 'mouseup touchend', function () {
     269        // A press that started in the list but ended outside it picked nothing; forget it.
     270        setTimeout( function () { providerPress = false; }, 0 );
     271} );
     272$( document ).on( 'mousedown touchstart', function ( e ) {
     273        // Clicking anywhere else closes an open list, even when nothing inside it holds focus.
     274        if ( $( '#bcs-smtp-provider-chips' ).prop( 'hidden' ) ) { return; }
     275        if ( $( e.target ).closest( '#bcs-smtp-provider-chips, #bcs-smtp-provider-change' ).length ) { return; }
     276        syncPickedProvider();
     277        setProviderListOpen( false );
     278} );
     279$( document ).on( 'focusout', '#bcs-smtp-provider-chips', function ( e ) {
     280        if ( providerPress ) { return; }
     281        if ( e.relatedTarget && $( e.relatedTarget ).closest( '#bcs-smtp-provider-chips' ).length ) { return; }
     282        syncPickedProvider();
     283        setProviderListOpen( false );
     284} );
     285
     286$( document ).on( 'change', '.bcs-smtp-provider-radio', function () {
     287        var $r      = $( this );
     288        var d       = $r.data();
     289        var key     = $r.val();
     290        var isOther = key === 'other';
     291        var $user   = $( '#bcs-smtp-smtp-username' );
     292
     293        if ( currentProvider && currentProvider !== key ) {
     294                providerMemory[ currentProvider ] = readConnectionFields();
     295        }
     296
     297        if ( providerMemory[ key ] ) {
     298                // Been here before in this visit: put back exactly what was there.
     299                writeConnectionFields( providerMemory[ key ] );
     300        } else {
     301                var f = readConnectionFields();
     302                if ( isOther ) {
     303                        // Bring back the last saved custom server; never carry another provider's server over.
     304                        if ( d.host ) {
     305                                f.host = d.host;
     306                                f.port = d.port;
     307                                f.enc  = d.enc;
     308                        } else if ( PRESET_HOSTS.indexOf( $.trim( f.host ) ) !== -1 ) {
     309                                f.host = '';
     310                        }
     311                } else {
     312                        f.host = d.host;
     313                        f.port = d.port;
     314                        f.enc  = d.enc;
     315                }
     316                // Fixed usernames (SendGrid's "apikey") are set; one left over from such a preset is cleared.
     317                if ( d.user ) {
     318                        f.user = d.user;
     319                } else if ( PRESET_USERS.indexOf( $.trim( f.user ) ) !== -1 ) {
     320                        f.user = '';
     321                }
     322                writeConnectionFields( f );
     323        }
     324        currentProvider = key;
     325
     326        $( '#bcs-smtp-user-hint' ).text( d.userHint || '' );
     327        $user.attr( 'placeholder', d.userHint || '' );
     328        $( '#bcs-smtp-pass-label' ).text( d.passLabel || '' );
     329        $( '#bcs-smtp-provider-note-text' ).text( d.note || '' );
     330        var $link = $( '#bcs-smtp-provider-note-link' );
     331        if ( d.link ) {
     332                $link.attr( 'href', d.link ).prop( 'hidden', false );
     333                $( '#bcs-smtp-provider-note-link-text' ).text( d.linkLabel || '' );
     334        } else {
     335                $link.prop( 'hidden', true );
     336        }
     337        $( '#bcs-smtp-provider-note' ).prop( 'hidden', false );
     338
     339        // "Other", and presets without a fixed server (Amazon SES is per region), need the server typed in.
     340        var needsHost = isOther || ! d.host;
     341        setDisclosure( $( '#bcs-smtp-server-details' ), needsHost || ! $.trim( $( '#bcs-smtp-smtp-host' ).val() ) );
     342        updateServerSummary();
     343
     344        if ( needsHost && ! $.trim( $( '#bcs-smtp-smtp-host' ).val() ) ) {
     345                $( '#bcs-smtp-smtp-host' ).trigger( 'focus' );
     346        } else if ( ! $.trim( $user.val() ) ) {
     347                $user.trigger( 'focus' );
     348        }
     349} );
     350
     351/* Encryption radio → auto-fill port (only when the port is still a standard one) */
    70352$( document ).on( 'change', '.bcs-smtp-enc-radio', function () {
    71         var enc  = $( this ).val();
    72         var $port = $( '#bcs-smtp-smtp-port' );
    73         // Only auto-fill if the user hasn't customised the port
     353        var $port   = $( '#bcs-smtp-smtp-port' );
    74354        var current = parseInt( $port.val(), 10 );
    75         var knownPorts = [ 25, 465, 587 ];
    76         if ( knownPorts.indexOf( current ) !== -1 || ! current ) {
    77                 $port.val( ENC_PORTS[ enc ] || 587 );
    78         }
    79 } );
     355        if ( [ 25, 465, 587 ].indexOf( current ) !== -1 || ! current ) {
     356                $port.val( ENC_PORTS[ $( this ).val() ] || 587 );
     357        }
     358        updateServerSummary();
     359} );
     360$( document ).on( 'input', '#bcs-smtp-smtp-host, #bcs-smtp-smtp-port', updateServerSummary );
    80361
    81362/* Auth toggle → show/hide username + password */
     363/* Warn when sign-in is off for a provider that always requires it */
     364function syncAuthWarning() {
     365        var $p   = $( '.bcs-smtp-provider-radio:checked' );
     366        var need = $p.length && $p.val() !== 'other' && ! $( '#bcs-smtp-smtp-auth' ).is( ':checked' );
     367        if ( need ) {
     368                $( '#bcs-smtp-auth-warning-title' ).text( fmt( T( 'needsAuth', '%s needs a username and password.' ), $.trim( $p.closest( 'label' ).find( '.bcs-smtp-provider-name' ).text() ) ) );
     369        }
     370        $( '#bcs-smtp-auth-warning' ).prop( 'hidden', ! need );
     371}
     372
    82373$( document ).on( 'change', '#bcs-smtp-smtp-auth', function () {
    83         $( '#bcs-smtp-auth-fields' ).toggle( this.checked );
     374        $( '#bcs-smtp-auth-fields' ).prop( 'hidden', ! this.checked );
     375        syncAuthWarning();
     376} );
     377$( document ).on( 'change', '.bcs-smtp-provider-radio', syncAuthWarning );
     378
     379/* Password: change / keep / show */
     380function showSavedPassword() {
     381        $( '#bcs-smtp-pw-input' ).prop( 'hidden', true );
     382        $( '#bcs-smtp-cancel-pw-btn' ).prop( 'hidden', true );
     383        $( '#bcs-smtp-pw-display' ).prop( 'hidden', false );
     384        $( '#bcs-smtp-smtp-password' ).val( '' );
     385        $( '#bcs-smtp-pw-status' ).removeClass( 'is-bad' ).addClass( 'is-good' ).text( T( 'pwSaved' ) );
     386}
     387$( document ).on( 'click', '#bcs-smtp-change-pw-btn', function () {
     388        $( '#bcs-smtp-pw-display' ).prop( 'hidden', true );
     389        $( '#bcs-smtp-pw-input' ).prop( 'hidden', false );
     390        $( '#bcs-smtp-cancel-pw-btn' ).prop( 'hidden', false );
     391        $( '#bcs-smtp-smtp-password' ).val( '' ).trigger( 'focus' );
     392        $( '#bcs-smtp-pw-status' ).removeClass( 'is-good is-bad' ).text( T( 'pwChanging' ) );
     393} );
     394$( document ).on( 'click', '#bcs-smtp-cancel-pw-btn', function ( e ) {
     395        e.preventDefault();
     396        showSavedPassword();
     397        $( '#bcs-smtp-change-pw-btn' ).trigger( 'focus' );
     398} );
     399$( document ).on( 'click', '#bcs-smtp-toggle-pw-btn', function () {
     400        var $input = $( '#bcs-smtp-smtp-password' );
     401        var hidden = $input.attr( 'type' ) === 'password';
     402        $input.attr( 'type', hidden ? 'text' : 'password' );
     403        $( this ).attr( 'aria-label', hidden ? T( 'hidePw' ) : T( 'showPw' ) )
     404                .find( '.dashicons' ).toggleClass( 'dashicons-visibility', ! hidden ).toggleClass( 'dashicons-hidden', hidden );
    84405} );
    85406
    86407/* Save SMTP settings */
     408/* Unsaved changes on the Connection card: only real edits count, not values the page fills in itself */
     409$( document ).on( 'input change', '.bcs-smtp-setup input', function ( e ) {
     410        if ( e.originalEvent ) { $( '#bcs-smtp-smtp-unsaved' ).prop( 'hidden', false ); }
     411} );
     412
    87413$( document ).on( 'click', '#bcs-smtp-save-smtp-btn', function () {
    88414        var $btn      = $( this );
    89         var hasHost   = $( '#bcs-smtp-smtp-host' ).val().trim() !== '';
     415        var hasHost   = $.trim( $( '#bcs-smtp-smtp-host' ).val() ) !== '';
    90416        var isEnabled = $( '#bcs-smtp-smtp-enabled' ).is( ':checked' );
    91         var btnLabel  = '<span class="dashicons dashicons-saved"></span> Save SMTP Settings';
    92 
    93         $btn.prop( 'disabled', true ).html(
    94                 '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> ' +
    95                 ( isEnabled && hasHost ? 'Saving & testing connection…' : 'Saving…' )
    96         );
     417        var newPw     = $( '#bcs-smtp-smtp-password' ).val();
     418
     419        setBusy( $btn, isEnabled && hasHost ? T( 'savingTesting' ) : T( 'saving' ) );
    97420
    98421        $.post( bcsSmtpData.ajaxurl, {
    … …  
    108431                smtp_auth       : $( '#bcs-smtp-smtp-auth' ).is( ':checked' ) ? 1 : 0,
    109432                smtp_username   : $( '#bcs-smtp-smtp-username' ).val(),
    110                 smtp_password   : $( '#bcs-smtp-smtp-password' ).val(),
     433                smtp_password   : newPw,
     434                provider        : $( '.bcs-smtp-provider-radio:checked' ).val() || '',
    111435        }, function ( r ) {
    112                 $btn.prop( 'disabled', false ).html( btnLabel );
    113 
     436                clearBusy( $btn );
    114437                if ( ! r || ! r.success ) {
    115                         var errMsg = ( r && r.data ) ? r.data : 'Save failed — check server error logs.';
    116                         showInlineMsg( '#bcs-smtp-smtp-msg', 'error', errMsg );
     438                        showInlineMsg( '#bcs-smtp-smtp-msg', 'error', errorText( r, T( 'saveFailed' ) ) );
    117439                        return;
    118440                }
    119 
    120                 // Update password display after save
    121                 var newPw = $( '#bcs-smtp-smtp-password' ).val();
    122                 if ( newPw !== '' && r.data.password_set ) {
    123                         // New password saved — switch back to masked display + Change button
    124                         $( '#bcs-smtp-pw-display' )
    125                                 .css( 'display', 'flex' )
    126                                 .html(
    127                                         '<input type="password" class="bcs-smtp-input" value="placeholder_pw" disabled style="flex:1" aria-label="Saved password (masked)">' +
    128                                         '<button type="button" id="bcs-smtp-change-pw-btn" class="bcs-smtp-btn bcs-smtp-btn-secondary bcs-smtp-btn-sm" style="flex-shrink:0;white-space:nowrap">' +
    129                                         '<span class="dashicons dashicons-edit"></span> Change</button>'
    130                                 );
    131                         $( '#bcs-smtp-pw-input' ).hide();
    132                         $( '#bcs-smtp-smtp-password' ).val( '' );
    133                         $( '#bcs-smtp-pw-status' )
    134                                 .text( '✓ Password saved — leave blank to keep it.' )
    135                                 .css( 'color', 'var(--bcs-smtp-emerald)' );
    136                 }
    137 
    138                 // Show save result
     441                if ( newPw !== '' && r.data.password_set ) { showSavedPassword(); }
     442                $( '#bcs-smtp-smtp-unsaved' ).prop( 'hidden', true );
     443
    139444                var msg = '<strong>' + r.data.message + '</strong>';
    140 
    141                 // Show connection test result if returned
    142445                if ( r.data.connection ) {
    143446                        var conn = r.data.connection;
    144                         if ( conn.success ) {
    145                                 msg += '<br><span style="color:var(--bcs-smtp-emerald)">✓ ' + conn.message + '</span>';
    146                         } else {
    147                                 msg += '<br><span style="color:var(--bcs-smtp-rose)">✗ Connection failed: ' + conn.message + '</span>';
    148                         }
    149                         // Use warning type if connection failed (settings saved but not reachable)
     447                        msg += conn.success
     448                                ? '<br><span class="is-good">' + conn.message + '</span>'
     449                                : '<br><span class="is-bad">' + T( 'connFailed' ) + ' ' + conn.message + '</span>';
    150450                        showInlineMsg( '#bcs-smtp-smtp-msg', conn.success ? 'success' : 'warning', msg );
     451                        markHealthDirty();
     452                        if ( conn.success ) { $( '#bcs-smtp-send-test-btn' ).trigger( 'focus' ); }
    151453                } else {
    152454                        showInlineMsg( '#bcs-smtp-smtp-msg', 'success', msg );
    153455                }
    154456        } ).fail( function ( xhr ) {
    155                 $btn.prop( 'disabled', false ).html( btnLabel );
    156                 var detail = xhr.status ? ' (HTTP ' + xhr.status + ')' : '';
    157                 showInlineMsg( '#bcs-smtp-smtp-msg', 'error', 'Request failed' + detail + ' — check server error logs or enable WP_DEBUG.' );
     457                clearBusy( $btn );
     458                showInlineMsg( '#bcs-smtp-smtp-msg', 'error', failText( xhr ) );
    158459        } );
    159460} );
    160461
    161462/* =====================================================================
    162    EMAIL LOG TAB
     463   Test email + health check
    163464   ===================================================================== */
    164 var emailLogLoaded = false;
     465$( document ).on( 'click', '#bcs-smtp-send-test-btn', function () {
     466        var $btn = $( this );
     467        var to   = $.trim( $( '#bcs-smtp-test-to' ).val() );
     468        if ( ! to ) {
     469                showInlineMsg( '#bcs-smtp-test-msg', 'error', T( 'enterRecipient' ) );
     470                $( '#bcs-smtp-test-to' ).trigger( 'focus' );
     471                return;
     472        }
     473        setBusy( $btn, T( 'sending' ) );
     474        $.post( bcsSmtpData.ajaxurl, { action: 'bcs_smtp_send_test', nonce: bcsSmtpData.nonce, to: to }, function ( r ) {
     475                clearBusy( $btn );
     476                if ( r && r.success ) {
     477                        showInlineMsg( '#bcs-smtp-test-msg', 'success', r.data.message );
     478                        return;
     479                }
     480                // Error payload is either { message, failure_type } or a plain string (validation / rate limit).
     481                var d   = r ? r.data : null;
     482                var msg = ! d ? T( 'testFailed' )
     483                        : ( typeof d === 'string' ? d
     484                        : ( d.failure_type ? '<strong>' + d.failure_type + ':</strong> ' + d.message : d.message ) );
     485                showInlineMsg( '#bcs-smtp-test-msg', 'error', msg );
     486        } ).fail( function ( xhr ) {
     487                clearBusy( $btn );
     488                showInlineMsg( '#bcs-smtp-test-msg', 'error', failText( xhr ) );
     489        } );
     490} );
     491
     492$( document ).on( 'click', '[data-check-now]', function () {
     493        var $btn  = $( this );
     494        var msgEl = $btn.data( 'msg' );
     495        setBusy( $btn, T( 'checking' ) );
     496        $.post( bcsSmtpData.ajaxurl, { action: 'bcs_smtp_check_now', nonce: bcsSmtpData.nonce }, function ( r ) {
     497                clearBusy( $btn );
     498                if ( r && r.success ) {
     499                        showInlineMsg( msgEl, r.data.success ? 'success' : 'error', r.data.message );
     500                        markHealthDirty();
     501                } else {
     502                        showInlineMsg( msgEl, 'error', errorText( r, T( 'checkFailed' ) ) );
     503                }
     504        } ).fail( function ( xhr ) {
     505                clearBusy( $btn );
     506                showInlineMsg( msgEl, 'error', failText( xhr ) );
     507        } );
     508} );
     509
     510/* =====================================================================
     511   EMAIL LOG
     512   ===================================================================== */
    165513var emailLogPage      = 1;
    166514var emailLogSearchTmr = null;
    167515
    168516function emailLogSkeletonHtml() {
    169         var widths = [ 85, 150, 120, 120, 64, 90, 70 ];
    170         var row    = '<tr class="bcs-smtp-skeleton-row">' + widths.map( function ( w ) {
    171                 return '<td><span class="bcs-smtp-skel" style="width:' + w + 'px"></span></td>';
     517        var widths = [ 60, 78, 66, 54, 30 ];
     518        var row = '<tr class="bcs-smtp-skeleton-row" aria-hidden="true">' + widths.map( function ( w ) {
     519                return '<td><span class="bcs-smtp-skel" style="width:' + w + '%"></span></td>';
    172520        } ).join( '' ) + '</tr>';
    173521        return new Array( 6 ).fill( row ).join( '' );
    … …  
    178526        emailLogPage = page;
    179527        var $tbody = $( '#bcs-smtp-log-tbody' );
    180         $tbody.html( emailLogSkeletonHtml() );
     528        $tbody.attr( 'aria-busy', 'true' ).html( emailLogSkeletonHtml() );
    181529
    182530        $.post( bcsSmtpData.ajaxurl, {
    183                 action      : 'bcs_smtp_get_email_log',
    184                 nonce       : bcsSmtpData.nonce,
    185                 page        : page,
    186                 per_page    : parseInt( $( '#bcs-smtp-log-per-page' ).val(), 10 ) || 10,
    187                 status      : $( '#bcs-smtp-log-status' ).val(),
    188                 search      : $( '#bcs-smtp-log-search' ).val(),
    189                 date_range  : $( '#bcs-smtp-log-date-range' ).val(),
     531                action     : 'bcs_smtp_get_email_log',
     532                nonce      : bcsSmtpData.nonce,
     533                page       : page,
     534                per_page   : parseInt( $( '#bcs-smtp-log-per-page' ).val(), 10 ) || 25,
     535                status     : $( '#bcs-smtp-log-status' ).val(),
     536                search     : $( '#bcs-smtp-log-search' ).val(),
     537                date_range : $( '#bcs-smtp-log-date-range' ).val(),
    190538        }, function ( r ) {
     539                $tbody.removeAttr( 'aria-busy' );
    191540                if ( r && r.success ) {
    192541                        $tbody.html( r.data.html );
    193                         var info = 'Page ' + r.data.page + ' of ' + ( r.data.pages || 1 ) + ' — ' + r.data.total + ' email(s)';
    194                         $( '#bcs-smtp-log-page-info' ).text( info );
     542                        $.each( r.data.counts || {}, function ( key, n ) {
     543                                $( '.bcs-smtp-seg-count[data-count="' + key + '"]' ).text( n );
     544                        } );
     545                        $( '#bcs-smtp-log-page-info' ).text( fmt( T( 'pageInfo' ), r.data.page, r.data.pages || 1, r.data.total ) );
    195546                        $( '#bcs-smtp-log-prev' ).prop( 'disabled', page <= 1 );
    196547                        $( '#bcs-smtp-log-next' ).prop( 'disabled', page >= r.data.pages );
    197548                } else {
    198                         $tbody.html( '<tr><td colspan="7" style="text-align:center;padding:20px;color:var(--bcs-smtp-text-3)">Error loading log.</td></tr>' );
     549                        $tbody.html( '<tr><td colspan="5" class="bcs-smtp-table-empty">' + T( 'loadError' ) + '</td></tr>' );
    199550                }
    200         } ).fail( function () {
    201                 $tbody.html( '<tr><td colspan="7" style="text-align:center;padding:20px;color:var(--bcs-smtp-text-3)">Request failed.</td></tr>' );
    202         } );
    203 }
     551        } ).fail( function ( xhr ) {
     552                $tbody.removeAttr( 'aria-busy' ).html( '<tr><td colspan="5" class="bcs-smtp-table-empty">' + failText( xhr ) + '</td></tr>' );
     553        } );
     554}
     555
     556/* Full email view: size the preview to its content; switch between preview and HTML source */
     557function fitMailFrame( frame ) {
     558        try {
     559                frame.contentDocument.documentElement.style.overflow = 'hidden'; // The frame grows to fit, so no inner scrollbar.
     560                frame.style.height = '0px'; // Collapse first, or a short email reports the frame's own height.
     561                frame.style.height = Math.max( 160, frame.contentDocument.documentElement.scrollHeight ) + 'px';
     562        } catch ( e ) {} // Not readable: keep the default height.
     563}
     564$( '.bcs-smtp-mailframe' ).each( function () {
     565        var frame = this;
     566        frame.addEventListener( 'load', function () { fitMailFrame( frame ); } );
     567        if ( frame.contentDocument && 'complete' === frame.contentDocument.readyState ) { fitMailFrame( frame ); }
     568} );
     569$( window ).on( 'resize', function () { $( '.bcs-smtp-mailframe' ).each( function () { fitMailFrame( this ); } ); } );
     570$( document ).on( 'change', 'input[name="bcs_smtp_mailview_mode"]', function () {
     571        var mode = this.value;
     572        $( '.bcs-smtp-mailview-message [data-mode]' ).each( function () {
     573                $( this ).prop( 'hidden', $( this ).data( 'mode' ) !== mode );
     574        } );
     575        if ( 'preview' === mode ) { $( '.bcs-smtp-mailframe' ).each( function () { fitMailFrame( this ); } ); }
     576} );
     577$( document ).on( 'change', 'input[name="bcs_smtp_mailview_device"]', function () {
     578        $( '.bcs-smtp-mailframe-stage' ).toggleClass( 'is-mobile', 'mobile' === this.value );
     579        $( '.bcs-smtp-mailframe' ).each( function () { fitMailFrame( this ); } );
     580} );
     581
     582$( document ).on( 'click', '.bcs-smtp-copy-btn', function () {
     583        var $btn = $( this );
     584        var text = $btn.closest( '.bcs-smtp-log-error, .bcs-smtp-mailview-source' ).find( 'pre' ).text();
     585        var done = function () {
     586                $btn.find( '.bcs-smtp-copy-label' ).text( T( 'copied' ) );
     587                setTimeout( function () { $btn.find( '.bcs-smtp-copy-label' ).text( T( 'copy' ) ); }, 1600 );
     588        };
     589        if ( navigator.clipboard && window.isSecureContext ) {
     590                navigator.clipboard.writeText( text ).then( done );
     591        } else {
     592                var ta = $( '<textarea readonly>' ).val( text ).css( { position: 'fixed', opacity: 0 } ).appendTo( 'body' );
     593                ta[ 0 ].select();
     594                document.execCommand( 'copy' );
     595                ta.remove();
     596                done();
     597        }
     598} );
    204599
    205600$( document ).on( 'input', '#bcs-smtp-log-search', function () {
    206601        clearTimeout( emailLogSearchTmr );
    207         emailLogSearchTmr = setTimeout( function () { loadEmailLog( 1 ); }, 400 );
     602        emailLogSearchTmr = setTimeout( function () { loadEmailLog( 1 ); }, 350 );
    208603} );
    209604$( document ).on( 'keydown', '#bcs-smtp-log-search', function ( e ) {
    … …  
    214609        }
    215610} );
    216 $( document ).on( 'change', '#bcs-smtp-log-date-range, #bcs-smtp-log-status, #bcs-smtp-log-per-page', function () {
     611$( document ).on( 'change', '.bcs-smtp-log-status-radio', function () {
     612        $( '#bcs-smtp-log-status' ).val( $( this ).val() );
    217613        loadEmailLog( 1 );
    218614} );
    219 $( document ).on( 'click', '#bcs-smtp-log-prev', function () {
    220         if ( emailLogPage > 1 ) { loadEmailLog( emailLogPage - 1 ); }
    221 } );
    222 $( document ).on( 'click', '#bcs-smtp-log-next', function () {
    223         loadEmailLog( emailLogPage + 1 );
    224 } );
    225 
     615$( document ).on( 'change', '#bcs-smtp-log-date-range, #bcs-smtp-log-per-page', function () { loadEmailLog( 1 ); } );
     616$( document ).on( 'click', '#bcs-smtp-log-prev', function () { if ( emailLogPage > 1 ) { loadEmailLog( emailLogPage - 1 ); } } );
     617$( document ).on( 'click', '#bcs-smtp-log-next', function () { loadEmailLog( emailLogPage + 1 ); } );
     618
     619$( document ).on( 'click', '[data-export-log]', function () {
     620        window.location.href = bcsSmtpData.exportUrl + '&nonce=' + encodeURIComponent( bcsSmtpData.exportNonce ) + '&date_range=30days&status=all&search=';
     621} );
     622
     623$( document ).on( 'click', '#bcs-smtp-export-csv-btn', function ( e ) {
     624        e.preventDefault();
     625        window.location.href = bcsSmtpData.exportUrl
     626                + '&nonce='      + encodeURIComponent( bcsSmtpData.exportNonce )
     627                + '&date_range=' + encodeURIComponent( $( '#bcs-smtp-log-date-range' ).val() )
     628                + '&status='     + encodeURIComponent( $( '#bcs-smtp-log-status' ).val() || 'all' )
     629                + '&search='     + encodeURIComponent( $( '#bcs-smtp-log-search' ).val() );
     630} );
    226631
    227632/* =====================================================================
    228    EMAIL REPORT TAB
     633   OVERVIEW — email activity line chart (dependency-free SVG)
    229634   ===================================================================== */
    230 var emailReportLoaded = false;
    231 var reportChart       = null;
     635var SVGNS = 'http://www.w3.org/2000/svg';
     636
     637function dayLabel( ymd, opts ) {
     638        var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec( ymd );
     639        if ( ! m ) { return ymd; }
     640        return new Date( +m[ 1 ], m[ 2 ] - 1, +m[ 3 ] ).toLocaleDateString( document.documentElement.lang || undefined, opts || { month: 'short', day: 'numeric' } );
     641}
     642
     643/* A "nice" axis: 4 steps of 1, 2, 2.5 or 5 × 10^n, never below 1 per step. */
     644function niceStep( max ) {
     645        var raw = Math.max( 1, max / 4 );
     646        var pow = Math.pow( 10, Math.floor( Math.log10( raw ) ) );
     647        var n   = raw / pow;
     648        var m   = n <= 1 ? 1 : n <= 2 ? 2 : n <= 2.5 ? 2.5 : n <= 5 ? 5 : 10;
     649        return Math.max( 1, Math.ceil( m * pow ) );
     650}
     651
     652/* Monotone cubic interpolation (Fritsch–Carlson) — smooth without overshooting below zero. */
     653function smoothPath( pts ) {
     654        var n = pts.length;
     655        if ( n < 2 ) { return n ? 'M' + pts[ 0 ][ 0 ] + ',' + pts[ 0 ][ 1 ] : ''; }
     656        var dx = [], dy = [], s = [], t = [];
     657        for ( var i = 0; i < n - 1; i++ ) {
     658                dx[ i ] = pts[ i + 1 ][ 0 ] - pts[ i ][ 0 ];
     659                dy[ i ] = pts[ i + 1 ][ 1 ] - pts[ i ][ 1 ];
     660                s[ i ]  = dy[ i ] / dx[ i ];
     661        }
     662        t[ 0 ] = s[ 0 ];
     663        t[ n - 1 ] = s[ n - 2 ];
     664        for ( i = 1; i < n - 1; i++ ) {
     665                t[ i ] = ( s[ i - 1 ] * s[ i ] <= 0 ) ? 0 : 3 * ( dx[ i - 1 ] + dx[ i ] ) / ( ( 2 * dx[ i ] + dx[ i - 1 ] ) / s[ i - 1 ] + ( dx[ i ] + 2 * dx[ i - 1 ] ) / s[ i ] );
     666        }
     667        var d = 'M' + pts[ 0 ][ 0 ].toFixed( 1 ) + ',' + pts[ 0 ][ 1 ].toFixed( 1 );
     668        for ( i = 0; i < n - 1; i++ ) {
     669                var h = dx[ i ] / 3;
     670                d += 'C' + ( pts[ i ][ 0 ] + h ).toFixed( 1 ) + ',' + ( pts[ i ][ 1 ] + h * t[ i ] ).toFixed( 1 ) + ' ' +
     671                        ( pts[ i + 1 ][ 0 ] - h ).toFixed( 1 ) + ',' + ( pts[ i + 1 ][ 1 ] - h * t[ i + 1 ] ).toFixed( 1 ) + ' ' +
     672                        pts[ i + 1 ][ 0 ].toFixed( 1 ) + ',' + pts[ i + 1 ][ 1 ].toFixed( 1 );
     673        }
     674        return d;
     675}
     676
     677function svgEl( name, attrs, parent ) {
     678        var el = document.createElementNS( SVGNS, name );
     679        for ( var k in attrs ) { if ( attrs.hasOwnProperty( k ) ) { el.setAttribute( k, attrs[ k ] ); } }
     680        if ( parent ) { parent.appendChild( el ); }
     681        return el;
     682}
     683
     684function LineChart( el ) {
     685        this.el   = el;
     686        this.tip  = el.querySelector( '.bcs-smtp-linechart-tip' );
     687        this.idx  = -1;
     688        this.data = null;
     689        var self  = this;
     690
     691        el.addEventListener( 'mousemove', function ( e ) { self.pointAt( e.clientX ); } );
     692        el.addEventListener( 'mouseleave', function () { self.hide(); } );
     693        el.addEventListener( 'blur', function () { self.hide(); } );
     694        el.addEventListener( 'keydown', function ( e ) {
     695                if ( ! self.data ) { return; }
     696                var n = self.data.labels.length;
     697                if ( e.key === 'ArrowRight' || e.key === 'ArrowLeft' ) {
     698                        e.preventDefault();
     699                        var i = self.idx < 0 ? n - 1 : self.idx + ( e.key === 'ArrowRight' ? 1 : -1 );
     700                        self.show( Math.max( 0, Math.min( n - 1, i ) ) );
     701                } else if ( e.key === 'Escape' ) {
     702                        self.hide();
     703                }
     704        } );
     705        if ( window.ResizeObserver ) {
     706                var lastW = 0;
     707                new ResizeObserver( function () {
     708                        var w = el.clientWidth;
     709                        if ( self.data && w && Math.abs( w - lastW ) > 2 ) { lastW = w; self.render( self.data, true ); }
     710                } ).observe( el );
     711        }
     712}
     713
     714/* Long ranges read better per week than as daily spikes. */
     715function bucketWeekly( data ) {
     716        if ( data.labels.length <= 45 ) { return data; }
     717        var size = data.labels.length > 200 ? 14 : 7;
     718        var out = { labels: [], success: [], failed: [], unconfirmed: [], weekly: true, span: size };
     719        for ( var i = 0; i < data.labels.length; i += size ) {
     720                out.labels.push( data.labels[ i ] );
     721                [ 'success', 'failed', 'unconfirmed' ].forEach( function ( k ) {
     722                        out[ k ].push( data[ k ].slice( i, i + size ).reduce( function ( a, b ) { return a + ( +b ); }, 0 ) );
     723                } );
     724        }
     725        return out;
     726}
     727
     728LineChart.prototype.render = function ( data, quiet ) {
     729        data = bucketWeekly( data );
     730        this.data = data;
     731        this.idx  = -1;
     732        var el = this.el;
     733        var old = el.querySelector( 'svg' );
     734        if ( old ) { el.removeChild( old ); }
     735
     736        var W = el.clientWidth, H = el.clientHeight;
     737        if ( ! W || ! H ) { return; }
     738        var pad = { l: 40, r: 12, t: 12, b: 28 };
     739        var n   = data.labels.length;
     740        var series = [ { key: 'success', cls: 'is-sent', color: 'var(--bcs-brand-ink)' } ];
     741        if ( data.failed.some( function ( v ) { return v > 0; } ) ) { series.push( { key: 'failed', cls: 'is-failed', color: 'var(--bcs-bad)' } ); }
     742        if ( data.unconfirmed.some( function ( v ) { return v > 0; } ) ) { series.push( { key: 'unconfirmed', cls: 'is-unconfirmed', color: 'var(--bcs-warn)' } ); }
     743
     744        var max = 0;
     745        series.forEach( function ( s ) { data[ s.key ].forEach( function ( v ) { max = Math.max( max, v ); } ); } );
     746        var step = niceStep( max ), top = step * 4;
     747        var iw = W - pad.l - pad.r, ih = H - pad.t - pad.b;
     748        var x = function ( i ) { return pad.l + ( n > 1 ? i * iw / ( n - 1 ) : iw / 2 ); };
     749        var y = function ( v ) { return pad.t + ih - ( v / top ) * ih; };
     750        this.geom = { x: x, y: y, pad: pad, iw: iw, n: n, series: series };
     751
     752        var svg = svgEl( 'svg', { viewBox: '0 0 ' + W + ' ' + H, 'aria-hidden': 'true', focusable: 'false' } );
     753        if ( quiet ) { svg.setAttribute( 'class', 'is-quiet' ); }
     754
     755        var grid = svgEl( 'g', { 'class': 'bcs-smtp-lc-grid' }, svg );
     756        for ( var g = 0; g <= 4; g++ ) {
     757                var gy = y( step * g );
     758                svgEl( 'line', { x1: pad.l, x2: W - pad.r, y1: gy, y2: gy, 'class': g === 0 ? 'is-base' : '' }, grid );
     759                var lbl = svgEl( 'text', { x: pad.l - 10, y: gy + 4, 'text-anchor': 'end', 'class': 'bcs-smtp-lc-label' }, grid );
     760                lbl.textContent = ( step * g ).toLocaleString();
     761        }
     762        var ticks = n > 1 ? [ 0, Math.round( ( n - 1 ) / 4 ), Math.round( ( n - 1 ) / 2 ), Math.round( 3 * ( n - 1 ) / 4 ), n - 1 ] : [ 0 ];
     763        ticks.filter( function ( v, i, a ) { return a.indexOf( v ) === i; } ).forEach( function ( i, k, arr ) {
     764                var anchor = k === 0 ? 'start' : ( k === arr.length - 1 ? 'end' : 'middle' );
     765                var t = svgEl( 'text', { x: x( i ), y: H - 6, 'text-anchor': anchor, 'class': 'bcs-smtp-lc-label' }, grid );
     766                t.textContent = dayLabel( data.labels[ i ] );
     767        } );
     768
     769        // Area under "sent", then each line on top.
     770        var sentPts = data.success.map( function ( v, i ) { return [ x( i ), y( v ) ]; } );
     771        var line0 = smoothPath( sentPts );
     772        if ( n > 1 ) {
     773                svgEl( 'path', { d: line0 + 'L' + x( n - 1 ) + ',' + y( 0 ) + 'L' + x( 0 ) + ',' + y( 0 ) + 'Z', 'class': 'bcs-smtp-lc-area' }, svg );
     774        }
     775        var paths = [];
     776        series.slice().reverse().forEach( function ( s ) {
     777                var pts = data[ s.key ].map( function ( v, i ) { return [ x( i ), y( v ) ]; } );
     778                paths.push( svgEl( 'path', { d: smoothPath( pts ), 'class': 'bcs-smtp-lc-line ' + s.cls }, svg ) );
     779        } );
     780
     781        this.cursor = svgEl( 'line', { x1: 0, x2: 0, y1: pad.t, y2: pad.t + ih, 'class': 'bcs-smtp-lc-cursor' }, svg );
     782        this.dots = series.map( function ( s ) { return svgEl( 'circle', { r: 4.5, cx: 0, cy: 0, fill: s.color, 'class': 'bcs-smtp-lc-dot' }, svg ); } );
     783
     784        el.insertBefore( svg, el.firstChild );
     785        paths.forEach( function ( p ) {
     786                var len = p.getTotalLength ? Math.ceil( p.getTotalLength() ) : 0;
     787                p.style.setProperty( '--len', len );
     788                if ( quiet ) { p.style.animation = 'none'; p.style.strokeDashoffset = 0; }
     789        } );
     790        this.hide();
     791};
     792
     793LineChart.prototype.pointAt = function ( clientX ) {
     794        if ( ! this.geom ) { return; }
     795        var r = this.el.getBoundingClientRect();
     796        var g = this.geom;
     797        var i = g.n > 1 ? Math.round( ( clientX - r.left - g.pad.l ) / ( g.iw / ( g.n - 1 ) ) ) : 0;
     798        this.show( Math.max( 0, Math.min( g.n - 1, i ) ) );
     799};
     800
     801LineChart.prototype.show = function ( i ) {
     802        var g = this.geom, d = this.data;
     803        if ( ! g ) { return; }
     804        this.idx = i;
     805        var cx = g.x( i );
     806        this.cursor.setAttribute( 'x1', cx );
     807        this.cursor.setAttribute( 'x2', cx );
     808        g.series.forEach( function ( s, k ) {
     809                this.dots[ k ].setAttribute( 'cx', cx );
     810                this.dots[ k ].setAttribute( 'cy', g.y( d[ s.key ][ i ] ) );
     811        }, this );
     812        this.el.classList.add( 'is-hover' );
     813
     814        var rows = [
     815                [ 'sent', T( 'sent', 'Sent' ), d.success[ i ] ],
     816                [ 'bad', T( 'failed', 'Failed' ), d.failed[ i ] ],
     817                [ 'warn', T( 'unconfirmed', 'Unconfirmed' ), d.unconfirmed[ i ] ],
     818        ];
     819        var when = d.weekly ? fmt( d.span === 14 ? T( 'twoWeeksOf', '2 weeks from %s' ) : T( 'weekOf', 'Week of %s' ), dayLabel( d.labels[ i ] ) ) : dayLabel( d.labels[ i ], { weekday: 'short', month: 'short', day: 'numeric' } );
     820        var html = '<strong>' + when + '</strong>';
     821        rows.forEach( function ( r ) {
     822                html += '<span><i class="bcs-smtp-key bcs-smtp-key--' + r[ 0 ] + '"></i>' + r[ 1 ] + '<b>' + Number( r[ 2 ] ).toLocaleString() + '</b></span>';
     823        } );
     824        this.tip.innerHTML = html;
     825        this.tip.hidden = false;
     826        var w = this.tip.offsetWidth, W = this.el.clientWidth;
     827        this.tip.style.left = Math.max( w / 2, Math.min( W - w / 2, cx ) ) + 'px';
     828};
     829
     830LineChart.prototype.hide = function () {
     831        this.idx = -1;
     832        this.el.classList.remove( 'is-hover' );
     833        if ( this.tip ) { this.tip.hidden = true; }
     834};
     835
     836var ovChart = null;
     837
     838function updateOverviewFigures( sum ) {
     839        var nf = function ( v ) { return Number( v ).toLocaleString(); };
     840        var $f = $( '#bcs-smtp-ov-figures' );
     841        $f.find( '[data-fig="success"]' ).text( nf( sum.success ) );
     842        $f.find( '[data-fig="failed"]' ).text( nf( sum.failed ) ).toggleClass( 'is-bad', sum.failed > 0 );
     843        $f.find( '[data-fig="unconfirmed"]' ).text( nf( sum.unconfirmed ) );
     844        $f.find( '[data-fig="rate"]' ).html( sum.total > 0
     845                ? ( Math.round( sum.success / sum.total * 1000 ) / 10 ).toLocaleString() + '<small>%</small>'
     846                : '—' );
     847        $( '#bcs-smtp-ov-empty' ).prop( 'hidden', sum.total > 0 );
     848}
     849
     850$( document ).on( 'change', '.bcs-smtp-ov-range', function () {
     851        var range = $( this ).val();
     852        $( '#bcs-smtp-ov-chart' ).attr( 'aria-busy', 'true' );
     853        $.post( bcsSmtpData.ajaxurl, { action: 'bcs_smtp_get_email_report', nonce: bcsSmtpData.nonce, date_range: range }, function ( r ) {
     854                $( '#bcs-smtp-ov-chart' ).removeAttr( 'aria-busy' );
     855                if ( ! r || ! r.success ) { return; }
     856                updateOverviewFigures( r.data.summary );
     857                if ( ovChart ) { ovChart.render( r.data.chart ); }
     858        } );
     859} );
     860
     861function initOverviewChart() {
     862        var el = document.getElementById( 'bcs-smtp-ov-chart' );
     863        if ( ! el || ovChart ) { return; }
     864        var data;
     865        try { data = JSON.parse( el.getAttribute( 'data-chart' ) ); } catch ( e ) { return; }
     866        ovChart = new LineChart( el );
     867        ovChart.render( data, window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches );
     868}
     869
     870/* =====================================================================
     871   REPORTS
     872   ===================================================================== */
     873var reportChart = null;
     874var reportRange = '30days';
     875
     876/* Facts derived from the daily series: no extra queries needed. */
     877var RANGE_DAYS = { '7days': 7, '30days': 30, '3months': 90 };
     878
     879function showRecipients( count ) {
     880        var $dd = $( '#bcs-smtp-highlights [data-hl="recipients"]' );
     881        if ( count === undefined || count === null || isNaN( count ) ) { $dd.text( '—' ); return; }
     882        count = +count;
     883        $dd.html( count.toLocaleString() + ' <small>' + ( 1 === count ? T( 'oneRecipient', 'recipient' ) : T( 'manyRecipients', 'recipients' ) ) + '</small>' )
     884                .toggleClass( 'is-good', count > 0 );
     885}
     886
     887function updateHighlights( raw ) {
     888        var n = raw.labels.length;
     889        // The series runs from the start date through today, so it holds one more calendar day
     890        // than the range's name; describe the period by its nominal length.
     891        var days = RANGE_DAYS[ reportRange ] || n;
     892        var total = 0, peak = -1, peakIdx = -1;
     893        for ( var i = 0; i < n; i++ ) {
     894                var day = ( +raw.success[ i ] ) + ( +raw.failed[ i ] ) + ( +raw.unconfirmed[ i ] );
     895                total  += day;
     896                if ( day > peak ) { peak = day; peakIdx = i; }
     897        }
     898        var $h = $( '#bcs-smtp-highlights' );
     899        var avg = days ? total / days : 0;
     900        $h.find( '[data-hl="avg"]' ).text( ! days ? '—' : ( total > 0 && avg < 0.5 ? T( 'lessThanOne', 'Less than 1' ) : Math.round( avg ).toLocaleString() ) );
     901        $h.find( '[data-hl="peak"]' ).html( peak > 0
     902                ? dayLabel( raw.labels[ peakIdx ] ) + ' <small>' + fmt( T( 'emailsCount', '%s emails' ), peak.toLocaleString() ) + '</small>'
     903                : '—' );
     904        // Failure-free counts use exactly the period's days (the most recent ones, through today).
     905        var start = Math.max( 0, n - days );
     906        var clean = 0, run = 0, best = 0;
     907        for ( var d = start; d < n; d++ ) {
     908                if ( +raw.failed[ d ] > 0 ) {
     909                        run = 0;
     910                } else {
     911                        clean++;
     912                        run++;
     913                        best = Math.max( best, run );
     914                }
     915        }
     916        var daysText = function ( v ) { return v.toLocaleString() + ' <small>' + ( 1 === v ? T( 'oneDay', 'day' ) : T( 'manyDays', 'days' ) ) + '</small>'; };
     917        $h.find( '[data-hl="cleandays"]' ).html( n ? clean.toLocaleString() + ' <small>' + fmt( T( 'ofDays', 'of %s days' ), Math.min( days, n - start ).toLocaleString() ) + '</small>' : '—' )
     918                .toggleClass( 'is-good', clean > 0 );
     919        $h.find( '[data-hl="beststreak"]' ).html( n ? daysText( best ) : '—' ).toggleClass( 'is-good', best > 0 );
     920
     921}
     922
     923function renderReport( sum, chart ) {
     924        var nf = function ( v ) { return Number( v ).toLocaleString(); };
     925        $( '#bcs-smtp-report-total' ).text( nf( sum.total ) );
     926        $( '#bcs-smtp-report-success' ).text( nf( sum.success ) );
     927        $( '#bcs-smtp-report-failed' ).text( nf( sum.failed ) ).toggleClass( 'is-bad', sum.failed > 0 );
     928        $( '#bcs-smtp-report-unconfirmed' ).text( nf( sum.unconfirmed ) );
     929        var rate = sum.total > 0 ? Math.round( sum.success / sum.total * 1000 ) / 10 : null;
     930        $( '#bcs-smtp-report-rate' ).html( null === rate ? '—' : rate.toLocaleString() + '<small>%</small>' );
     931        $( '#bcs-smtp-report-ratebar' ).css( 'width', ( rate || 0 ) + '%' );
     932        $( '#bcs-smtp-chart-empty' ).prop( 'hidden', sum.total > 0 );
     933        if ( reportChart ) { reportChart.render( chart ); }
     934        updateHighlights( chart );
     935        showRecipients( sum.recipients );
     936}
    232937
    233938function loadEmailReport() {
     939        var $chart = $( '#bcs-smtp-report-chart' ).attr( 'aria-busy', 'true' );
    234940        $.post( bcsSmtpData.ajaxurl, {
    235941                action     : 'bcs_smtp_get_email_report',
    236942                nonce      : bcsSmtpData.nonce,
    237                 date_range : $( '#bcs-smtp-report-date-range' ).val(),
     943                date_range : reportRange,
    238944        }, function ( r ) {
    239                 if ( ! r || ! r.success ) { return; }
    240                 var d = r.data;
    241 
    242                 $( '#bcs-smtp-report-success' ).text( d.summary.success );
    243                 $( '#bcs-smtp-report-failed' ).text( d.summary.failed );
    244                 $( '#bcs-smtp-report-unconfirmed' ).text( d.summary.unconfirmed );
    245                 $( '#bcs-smtp-report-total' ).text( d.summary.total );
    246 
    247                 updateReportChart( d.chart );
    248         } );
    249 }
    250 
    251 function updateReportChart( chartData ) {
    252         var ctx = document.getElementById( 'bcs-smtp-report-chart' );
    253         if ( ! ctx ) { return; }
    254 
    255         // Filter out days where all values are 0 (no email activity).
    256         var filteredLabels = [], filteredSuccess = [], filteredFailed = [], filteredUnconf = [];
    257         for ( var fi = 0; fi < chartData.labels.length; fi++ ) {
    258                 if ( chartData.success[ fi ] > 0 || chartData.failed[ fi ] > 0 || chartData.unconfirmed[ fi ] > 0 ) {
    259                         filteredLabels.push( chartData.labels[ fi ] );
    260                         filteredSuccess.push( chartData.success[ fi ] );
    261                         filteredFailed.push( chartData.failed[ fi ] );
    262                         filteredUnconf.push( chartData.unconfirmed[ fi ] );
     945                $chart.removeAttr( 'aria-busy' );
     946                if ( r && r.success ) { renderReport( r.data.summary, r.data.chart ); }
     947        } );
     948}
     949
     950function initReportChart() {
     951        var el = document.getElementById( 'bcs-smtp-report-chart' );
     952        if ( ! el || reportChart ) { return; }
     953        var data;
     954        try { data = JSON.parse( el.getAttribute( 'data-chart' ) ); } catch ( e ) { return; }
     955        reportChart = new LineChart( el );
     956        reportChart.render( data, window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches );
     957        updateHighlights( data );
     958        showRecipients( $( '#bcs-smtp-highlights [data-hl="recipients"]' ).data( 'initial' ) );
     959}
     960
     961$( document ).on( 'change', '.bcs-smtp-rp-range', function () {
     962        reportRange = $( this ).val();
     963        $( '#bcs-smtp-report-period' ).text( fmt( T( 'lastPeriod', 'Last %s' ), $( this ).data( 'label' ) ) );
     964        loadEmailReport();
     965} );
     966
     967$( document ).on( 'click', '#bcs-smtp-report-export', function () {
     968        window.location.href = bcsSmtpData.exportUrl + '&nonce=' + encodeURIComponent( bcsSmtpData.exportNonce ) + '&date_range=' + encodeURIComponent( reportRange ) + '&status=all&search=';
     969} );
     970
     971/* Same rule as the server: comma-separated, every entry a valid address. */
     972var EMAIL_RE = /^[^\s@,]+@[^\s@,]+\.[^\s@,]{2,}$/;
     973
     974/* =====================================================================
     975   Recipient chips (Gmail-style) — the hidden #bcs-smtp-report-emails keeps the
     976   comma-separated value the save handlers and validation read.
     977   ===================================================================== */
     978var $chipsWrap  = $( '#bcs-smtp-report-chips' );
     979var $chipList   = $chipsWrap.find( '.bcs-smtp-chip-list' );
     980var $chipEntry  = $( '#bcs-smtp-report-emails-entry' );
     981var $chipHidden = $( '#bcs-smtp-report-emails' );
     982var CHIP_PLACEHOLDER = $chipEntry.attr( 'placeholder' ) || '';
     983
     984function chipValues() {
     985        return String( $chipHidden.val() || '' ).split( ',' ).map( function ( v ) { return $.trim( v ); } ).filter( Boolean );
     986}
     987
     988function renderChips() {
     989        $chipList.empty();
     990        chipValues().forEach( function ( email, i ) {
     991                var ok    = EMAIL_RE.test( email );
     992                var $chip = $( '<li class="bcs-smtp-chip" />' ).toggleClass( 'is-invalid', ! ok ).attr( 'data-i', i );
     993                $( '<span class="bcs-smtp-chip-text" />' ).text( email ).attr( 'title', ok ? email : T( 'notAnEmail', 'Not a valid email address' ) ).appendTo( $chip );
     994                $( '<button type="button" class="bcs-smtp-chip-x" />' )
     995                        .attr( 'aria-label', fmt( T( 'removeEmail', 'Remove %s' ), email ) )
     996                        .html( '<span class="dashicons dashicons-no-alt" aria-hidden="true"></span>' )
     997                        .appendTo( $chip );
     998                $chipList.append( $chip );
     999        } );
     1000        $chipEntry.attr( 'placeholder', chipValues().length ? '' : CHIP_PLACEHOLDER );
     1001}
     1002
     1003function setChipValues( list ) {
     1004        var seen = {};
     1005        list = list.filter( function ( v ) {
     1006                var k = v.toLowerCase();
     1007                if ( seen[ k ] ) { return false; }
     1008                seen[ k ] = true;
     1009                return true;
     1010        } );
     1011        $chipHidden.val( list.join( ', ' ) );
     1012        $chipsWrap.removeClass( 'is-invalid' );
     1013        renderChips();
     1014}
     1015
     1016/* Turn whatever is typed or pasted into chips. */
     1017function commitChipEntry() {
     1018        var raw = $chipEntry.val();
     1019        if ( ! $.trim( raw ) ) { return; }
     1020        var added = raw.split( /[\s,;]+/ ).map( function ( v ) { return $.trim( v ); } ).filter( Boolean );
     1021        $chipEntry.val( '' );
     1022        setChipValues( chipValues().concat( added ) );
     1023}
     1024
     1025$chipEntry.on( 'keydown', function ( e ) {
     1026        if ( e.key === 'Enter' || e.key === ',' || e.key === ';' || ( e.key === ' ' && $.trim( this.value ) ) || ( e.key === 'Tab' && $.trim( this.value ) ) ) {
     1027                e.preventDefault();
     1028                commitChipEntry();
     1029                return;
     1030        }
     1031        if ( e.key === 'Backspace' && ! this.value ) {
     1032                var $last = $chipList.children().last();
     1033                if ( ! $last.length ) { return; }
     1034                if ( $last.hasClass( 'is-armed' ) ) {
     1035                        var vals = chipValues();
     1036                        vals.pop();
     1037                        setChipValues( vals );
     1038                } else {
     1039                        $last.addClass( 'is-armed' );
    2631040                }
    264         }
    265         // Fall back to full data if nothing has activity (avoids empty chart).
    266         if ( filteredLabels.length === 0 ) {
    267                 filteredLabels  = chartData.labels;
    268                 filteredSuccess = chartData.success;
    269                 filteredFailed  = chartData.failed;
    270                 filteredUnconf  = chartData.unconfirmed;
    271         }
    272         chartData = { labels: filteredLabels, success: filteredSuccess, failed: filteredFailed, unconfirmed: filteredUnconf };
    273 
    274         // Resolve colours based on active theme
    275         var isDark    = document.documentElement.getAttribute( 'data-bcs-smtp-theme' ) === 'dark';
    276         var gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)';
    277         var textColor = isDark ? '#94a3b8' : '#64748b';
    278 
    279         var datasets = [
    280                 {
    281                         label           : 'Sent',
    282                         data            : chartData.success,
    283                         backgroundColor : 'rgba(16,185,129,0.75)',
    284                         borderColor     : '#10b981',
    285                         borderWidth     : 1,
    286                         borderRadius    : 4,
    287                 },
    288                 {
    289                         label           : 'Failed',
    290                         data            : chartData.failed,
    291                         backgroundColor : 'rgba(239,68,68,0.75)',
    292                         borderColor     : '#ef4444',
    293                         borderWidth     : 1,
    294                         borderRadius    : 4,
    295                 },
    296                 {
    297                         label           : 'Unconfirmed',
    298                         data            : chartData.unconfirmed,
    299                         backgroundColor : 'rgba(245,158,11,0.75)',
    300                         borderColor     : '#f59e0b',
    301                         borderWidth     : 1,
    302                         borderRadius    : 4,
    303                 },
    304         ];
    305 
    306         if ( reportChart ) {
    307                 reportChart.data.labels   = chartData.labels;
    308                 reportChart.data.datasets = datasets;
    309                 reportChart.update();
    3101041                return;
    3111042        }
    312 
    313         reportChart = new Chart( ctx, {
    314                 type : 'bar',
    315                 data : {
    316                         labels   : chartData.labels,
    317                         datasets : datasets,
    318                 },
    319                 options : {
    320                         responsive          : true,
    321                         maintainAspectRatio : false,
    322                         interaction         : { mode: 'index', intersect: false },
    323                         plugins : {
    324                                 legend  : { labels : { color: textColor, font: { size: 12 } } },
    325                                 tooltip : { mode: 'index' },
    326                         },
    327                         scales : {
    328                                 x : {
    329                                         stacked   : false,
    330                                         grid      : { color: gridColor },
    331                                         ticks     : { color: textColor, font: { size: 11 } },
    332                                 },
    333                                 y : {
    334                                         stacked    : false,
    335                                         beginAtZero: true,
    336                                         grid       : { color: gridColor },
    337                                         ticks      : { color: textColor, font: { size: 11 }, precision: 0 },
    338                                 },
    339                         },
    340                 },
    341         } );
    342 }
    343 
    344 $( document ).on( 'change', '#bcs-smtp-report-date-range', function () {
    345         loadEmailReport();
    346 } );
    347 
    348 /* Accordion toggle */
    349 $( document ).on( 'click', '.bcs-smtp-accordion-header', function () {
    350         var $header = $( this );
    351         var $body   = $header.next( '.bcs-smtp-accordion-body' );
    352         var open    = $body.is( ':visible' );
    353         $body.slideToggle( 200 );
    354         $header.attr( 'aria-expanded', open ? 'false' : 'true' );
    355         $header.find( '.bcs-smtp-accordion-arrow' )
    356                 .toggleClass( 'dashicons-arrow-down-alt2', open )
    357                 .toggleClass( 'dashicons-arrow-up-alt2', ! open );
    358 } );
    359 
    360 /* Save schedule */
     1043        $chipList.children().removeClass( 'is-armed' );
     1044} );
     1045$chipEntry.on( 'paste', function () { setTimeout( commitChipEntry, 0 ); } );
     1046$chipEntry.on( 'blur', commitChipEntry );
     1047$chipEntry.on( 'input', function () { $chipsWrap.removeClass( 'is-invalid' ); } );
     1048
     1049$chipList.on( 'click', '.bcs-smtp-chip-x', function () {
     1050        var i    = +$( this ).closest( '.bcs-smtp-chip' ).attr( 'data-i' );
     1051        var vals = chipValues();
     1052        vals.splice( i, 1 );
     1053        setChipValues( vals );
     1054        $chipEntry.trigger( 'focus' );
     1055} );
     1056$chipsWrap.on( 'click', function ( e ) {
     1057        if ( e.target === this || $( e.target ).is( '.bcs-smtp-chip-list' ) ) { $chipEntry.trigger( 'focus' ); }
     1058} );
     1059
     1060
     1061function checkReportEmails( required ) {
     1062        commitChipEntry();
     1063        var $f    = $( '#bcs-smtp-report-emails' );
     1064        var parts = $f.val().split( ',' ).map( function ( v ) { return $.trim( v ); } ).filter( Boolean );
     1065        var bad   = parts.filter( function ( v ) { return ! EMAIL_RE.test( v ); } );
     1066        var msg   = '';
     1067        if ( bad.length ) {
     1068                msg = fmt( T( 'badEmails', 'These aren’t valid email addresses: %s' ), $( '<span>' ).text( bad.join( ', ' ) ).html() );
     1069        } else if ( required && ! parts.length ) {
     1070                msg = T( 'needEmail', 'Enter at least one email address to send the report to.' );
     1071        }
     1072        $chipsWrap.toggleClass( 'is-invalid', !! msg );
     1073        $chipEntry.attr( 'aria-invalid', msg ? 'true' : null );
     1074        if ( msg ) {
     1075                showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', msg );
     1076                $chipEntry.trigger( 'focus' );
     1077        }
     1078        return ! msg;
     1079}
     1080
     1081
     1082$( document ).on( 'change', '#bcs-smtp-report-schedule-enabled', function () {
     1083        $( '#bcs-smtp-report-body, #bcs-smtp-send-report-now-btn' ).prop( 'hidden', ! this.checked );
     1084        $( '#bcs-smtp-report-off' ).prop( 'hidden', this.checked );
     1085} );
     1086
    3611087$( document ).on( 'click', '#bcs-smtp-save-report-schedule-btn', function () {
    362         var $btn    = $( this );
    363         var btnHtml = '<span class="dashicons dashicons-saved"></span>Save Schedule';
    364         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Saving…' );
     1088        var $btn = $( this );
     1089        if ( ! checkReportEmails( $( '#bcs-smtp-report-schedule-enabled' ).is( ':checked' ) ) ) { return; }
     1090        setBusy( $btn, T( 'saving' ) );
    3651091        $.post( bcsSmtpData.ajaxurl, {
    3661092                action    : 'bcs_smtp_save_report_schedule',
    … …  
    3681094                enabled   : $( '#bcs-smtp-report-schedule-enabled' ).is( ':checked' ) ? 1 : 0,
    3691095                emails    : $( '#bcs-smtp-report-emails' ).val(),
    370                 frequency : $( '#bcs-smtp-report-frequency' ).val(),
    3711096        }, function ( r ) {
    372                 $btn.prop( 'disabled', false ).html( btnHtml );
     1097                clearBusy( $btn );
    3731098                if ( r && r.success ) {
    374                         showInlineMsg( '#bcs-smtp-report-schedule-msg', 'success', r.data.message || 'Schedule saved.' );
     1099                        showInlineMsg( '#bcs-smtp-report-schedule-msg', 'success', r.data.message || T( 'saved' ) );
     1100                        $( '#bcs-smtp-report-next-text' ).text( r.data.next || '' );
     1101                        if ( r.data.emails !== undefined ) { $chipHidden.val( r.data.emails ); renderChips(); }
     1102                        $( '#bcs-smtp-report-next' ).prop( 'hidden', ! r.data.next );
    3751103                } else {
    376                         showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', ( r && r.data ) ? r.data : 'Save failed.' );
    377                 }
    378         } ).fail( function () {
    379                 $btn.prop( 'disabled', false ).html( btnHtml );
    380                 showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', 'Request failed.' );
    381         } );
    382 } );
    383 
    384 /* Send report now — date_range is derived server-side from saved frequency */
    385 $( document ).on( 'click', '#bcs-smtp-send-report-now-btn', function () {
    386         var $btn    = $( this );
    387         var btnHtml = '<span class="dashicons dashicons-email-alt"></span>Send Report Now';
    388         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Sending…' );
    389         $.post( bcsSmtpData.ajaxurl, {
    390                 action  : 'bcs_smtp_send_report_now',
    391                 nonce   : bcsSmtpData.nonce,
    392                 emails  : $( '#bcs-smtp-report-emails' ).val(),
    393         }, function ( r ) {
    394                 $btn.prop( 'disabled', false ).html( btnHtml );
    395                 if ( r && r.success ) {
    396                         showInlineMsg( '#bcs-smtp-report-schedule-msg', 'success', r.data.message || 'Report sent.' );
    397                 } else {
    398                         showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', ( r && r.data ) ? r.data : 'Send failed.' );
    399                 }
    400         } ).fail( function () {
    401                 $btn.prop( 'disabled', false ).html( btnHtml );
    402                 showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', 'Request failed.' );
    403         } );
    404 } );
    405 
    406 /* =====================================================================
    407    SEND TEST EMAIL TAB
    408    ===================================================================== */
    409 $( document ).on( 'click', '#bcs-smtp-send-test-btn', function () {
    410         var $btn = $( this );
    411         var to   = $( '#bcs-smtp-test-to' ).val().trim();
    412         if ( ! to ) {
    413                 showInlineMsg( '#bcs-smtp-test-msg', 'error', 'Please enter a recipient email address.' );
    414                 return;
    415         }
    416         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Sending…' );
    417 
    418         $.post( bcsSmtpData.ajaxurl, {
    419                 action : 'bcs_smtp_send_test',
    420                 nonce  : bcsSmtpData.nonce,
    421                 to     : to,
    422         }, function ( r ) {
    423                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-email-alt"></span> Send Test Email' );
    424                 if ( r && r.success ) {
    425                         showInlineMsg( '#bcs-smtp-test-msg', 'success', r.data.message );
    426                 } else {
    427                         var msg = ( r && r.data )
    428                                 ? ( r.data.failure_type ? '<strong>' + r.data.failure_type + ':</strong> ' + r.data.message : r.data.message )
    429                                 : 'Test failed.';
    430                         showInlineMsg( '#bcs-smtp-test-msg', 'error', msg );
     1104                        showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', errorText( r, T( 'saveFailed' ) ) );
    4311105                }
    4321106        } ).fail( function ( xhr ) {
    433                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-email-alt"></span> Send Test Email' );
    434                 showInlineMsg( '#bcs-smtp-test-msg', 'error', 'Request failed (HTTP ' + xhr.status + ').' );
    435         } );
    436 } );
    437 
    438 $( document ).on( 'click', '#bcs-smtp-check-now-btn', function () {
     1107                clearBusy( $btn );
     1108                showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', failText( xhr ) );
     1109        } );
     1110} );
     1111
     1112/* Send report now — date_range is derived server-side from the saved frequency */
     1113$( document ).on( 'click', '#bcs-smtp-send-report-now-btn', function () {
    4391114        var $btn = $( this );
    440         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Running…' );
    441 
     1115        if ( ! checkReportEmails( true ) ) { return; }
     1116        setBusy( $btn, T( 'sending' ) );
    4421117        $.post( bcsSmtpData.ajaxurl, {
    443                 action : 'bcs_smtp_check_now',
     1118                action : 'bcs_smtp_send_report_now',
    4441119                nonce  : bcsSmtpData.nonce,
     1120                emails : $( '#bcs-smtp-report-emails' ).val(),
    4451121        }, function ( r ) {
    446                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-update"></span> Run Health Check Now' );
     1122                clearBusy( $btn );
    4471123                if ( r && r.success ) {
    448                         var type = r.data.success ? 'success' : 'error';
    449                         showInlineMsg( '#bcs-smtp-test-msg', type, r.data.message );
     1124                        showInlineMsg( '#bcs-smtp-report-schedule-msg', 'success', r.data.message );
     1125                } else {
     1126                        showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', errorText( r, T( 'saveFailed' ) ) );
    4501127                }
    4511128        } ).fail( function ( xhr ) {
    452                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-update"></span> Run Health Check Now' );
    453                 showInlineMsg( '#bcs-smtp-test-msg', 'error', 'Request failed (HTTP ' + xhr.status + ').' );
     1129                clearBusy( $btn );
     1130                showInlineMsg( '#bcs-smtp-report-schedule-msg', 'error', failText( xhr ) );
    4541131        } );
    4551132} );
    4561133
    4571134/* =====================================================================
    458    ALERTS TAB: toggles + channel
     1135   ALERTS
    4591136   ===================================================================== */
    460 
    461 /* Monitor toggle → accordion */
    462 $( document ).on( 'change', '#bcs-smtp-monitor-enabled', function () {
    463         $( '#bcs-smtp-monitor-accordion' ).toggleClass( 'open', this.checked );
    464 } );
    465 
    466 /* Alert channel toggle → accordion */
    4671137$( document ).on( 'change', '#bcs-smtp-alert-channel-enabled', function () {
    4681138        $( '#bcs-smtp-alert-channel-accordion' ).toggleClass( 'open', this.checked );
    469 } );
    470 
    471 /* Channel radio → show only the matching webhook field, preserve both values */
     1139        $( '#bcs-smtp-alerts-off' ).prop( 'hidden', this.checked );
     1140} );
     1141
     1142/* Channel list: each channel has its own switch; its webhook field opens under its row.
     1143   One row is open at a time. Every URL is kept and saved, even for channels that are off. */
     1144var WEBHOOK_ROWS  = { slack: 'slack', teams: 'teams', google_chat: 'google-chat', discord: 'discord', custom: 'custom' };
     1145var activeWebhook = null;
     1146
     1147function webhookInput( key ) {
     1148        return $( '#bcs-smtp-' + WEBHOOK_ROWS[ key ] + '-webhook-url' );
     1149}
     1150
     1151function channelState( box ) {
     1152        if ( ! box.checked ) { return 'off'; }
     1153        var url = $.trim( webhookInput( box.value ).val() );
     1154        return ! url ? 'missing' : ( webhookProblem( box.value, url ) ? 'bad' : 'ready' );
     1155}
     1156
    4721157function syncWebhookFields() {
    473         var ch = $( '.bcs-smtp-channel-radio:checked' ).val();
    474         $( '#bcs-smtp-slack-webhook-row' ).toggle( ch === 'slack' );
    475         $( '#bcs-smtp-teams-webhook-row' ).toggle( ch === 'teams' );
    476         $( '#bcs-smtp-google-chat-webhook-row' ).toggle( ch === 'google_chat' );
    477         $( '#bcs-smtp-discord-webhook-row' ).toggle( ch === 'discord' );
    478         $( '#bcs-smtp-custom-webhook-row' ).toggle( ch === 'custom' );
    479 }
    480 $( document ).on( 'change', '.bcs-smtp-channel-radio', syncWebhookFields );
    481 
    482 
    483 /* Save monitor settings */
    484 $( document ).on( 'click', '#bcs-smtp-save-monitor-btn', function () {
    485         var $btn = $( this );
    486         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Saving…' );
    487 
    488         $.post( bcsSmtpData.ajaxurl, {
    489                 action            : 'bcs_smtp_save_monitor',
    490                 nonce             : bcsSmtpData.nonce,
    491                 monitor_enabled   : $( '#bcs-smtp-monitor-enabled' ).is( ':checked' ) ? 1 : 0,
    492                 check_interval    : $( '#bcs-smtp-check-interval' ).val(),
    493                 failure_threshold : $( '#bcs-smtp-failure-threshold' ).val(),
    494                 max_alert_count   : $( '#bcs-smtp-max-alert-count' ).val(),
    495         }, function ( r ) {
    496                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-saved"></span> Save Monitor Settings' );
    497                 if ( r && r.success ) {
    498                         showInlineMsg( '#bcs-smtp-monitor-msg', 'success', r.data.message );
    499                 } else {
    500                         showInlineMsg( '#bcs-smtp-monitor-msg', 'error', ( r && r.data ) ? r.data : 'Save failed.' );
    501                 }
    502         } ).fail( function ( xhr ) {
    503                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-saved"></span> Save Monitor Settings' );
    504                 showInlineMsg( '#bcs-smtp-monitor-msg', 'error', 'Request failed (HTTP ' + xhr.status + ').' );
    505         } );
    506 } );
    507 
    508 /* Save alert channel settings */
    509 $( document ).on( 'click', '#bcs-smtp-save-alerts-btn', function () {
    510         var $btn = $( this );
    511         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Saving…' );
    512 
    513         $.post( bcsSmtpData.ajaxurl, {
    514                 action               : 'bcs_smtp_save_alerts',
    515                 nonce                : bcsSmtpData.nonce,
    516                 alert_channel_enabled: $( '#bcs-smtp-alert-channel-enabled' ).is( ':checked' ) ? 1 : 0,
    517                 alert_channel        : $( '.bcs-smtp-channel-radio:checked' ).val(),
    518                 slack_webhook_url    : $( '#bcs-smtp-slack-webhook-url' ).val(),
    519                 teams_webhook_url    : $( '#bcs-smtp-teams-webhook-url' ).val(),
    520                 google_chat_webhook_url: $( '#bcs-smtp-google-chat-webhook-url' ).val(),
    521                 discord_webhook_url  : $( '#bcs-smtp-discord-webhook-url' ).val(),
    522                 custom_webhook_url   : $( '#bcs-smtp-custom-webhook-url' ).val(),
    523         }, function ( r ) {
    524                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-saved"></span> Save Alert Settings' );
    525                 if ( r && r.success ) {
    526                         showInlineMsg( '#bcs-smtp-alerts-msg', 'success', r.data.message );
    527                 } else {
    528                         showInlineMsg( '#bcs-smtp-alerts-msg', 'error', ( r && r.data ) ? r.data : 'Save failed.' );
    529                 }
    530         } ).fail( function ( xhr ) {
    531                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-saved"></span> Save Alert Settings' );
    532                 showInlineMsg( '#bcs-smtp-alerts-msg', 'error', 'Request failed (HTTP ' + xhr.status + ').' );
    533         } );
    534 } );
    535 
    536 /* Send test alert */
     1158        var $boxes = $( '.bcs-smtp-channel-radio' );
     1159        // An open row stays open only while its channel is on.
     1160        if ( activeWebhook && ! $boxes.filter( '[value="' + activeWebhook + '"]' ).is( ':checked' ) ) {
     1161                activeWebhook = null;
     1162        }
     1163        var on = 0;
     1164        $boxes.each( function () {
     1165                var state = channelState( this );
     1166                var open  = this.value === activeWebhook;
     1167                var $row  = $( this ).closest( '.bcs-smtp-ch' );
     1168                on += this.checked ? 1 : 0;
     1169                $row.attr( 'data-state', state ).toggleClass( 'is-open', open );
     1170                $row.find( '.bcs-smtp-ch-main' ).attr( 'aria-expanded', open ? 'true' : 'false' );
     1171                $row.find( '.bcs-smtp-ch-pill' ).text( { ready: T( 'chReady' ), missing: T( 'chAddUrl' ), bad: T( 'chCheckUrl' ), off: T( 'chOff' ) }[ state ] );
     1172                $( '#bcs-smtp-' + WEBHOOK_ROWS[ this.value ] + '-webhook-row' ).prop( 'hidden', ! open );
     1173        } );
     1174        $( '#bcs-smtp-ch-count' ).text( fmt( T( 'chCount' ), on, $boxes.length ) );
     1175}
     1176
     1177/* Same host rules the server enforces, so a wrong paste is caught before saving */
     1178var WEBHOOK_HOSTS = {
     1179        slack       : /^hooks\.slack\.com$/,
     1180        teams       : /(^|\.)(office\.com|logic\.azure\.com|api\.powerplatform\.com)$/,
     1181        google_chat : /^chat\.googleapis\.com$/,
     1182        discord     : /(^|\.)discord(app)?\.com$/
     1183};
     1184
     1185function webhookProblem( key, url ) {
     1186        var u;
     1187        try { u = new URL( url ); } catch ( e ) { return T( 'urlHttps' ); }
     1188        if ( 'https:' !== u.protocol ) { return T( 'urlHttps' ); }
     1189        var host = u.hostname.toLowerCase();
     1190        if ( 'custom' === key ) {
     1191                return ( 'localhost' === host || /^[\d.]+$/.test( host ) || -1 !== host.indexOf( ':' ) || '[' === host.charAt( 0 ) ) ? T( 'urlPublic' ) : '';
     1192        }
     1193        return WEBHOOK_HOSTS[ key ].test( host ) ? '' : fmt( T( 'urlHost' ), $( '.bcs-smtp-channel-radio[value="' + key + '"]' ).data( 'label' ) );
     1194}
     1195
     1196/* Show a field's problem; live once shown, so it clears the moment the URL is fixed */
     1197function checkWebhookField( input, force ) {
     1198        var $row = $( input ).closest( '.bcs-smtp-webhook' );
     1199        var $err = $row.find( '.bcs-smtp-webhook-err' );
     1200        if ( ! force && $err.prop( 'hidden' ) ) { return; }
     1201        var url  = $.trim( input.value );
     1202        var msg  = url ? webhookProblem( $row.data( 'channel' ), url ) : '';
     1203        $err.text( msg ).prop( 'hidden', ! msg );
     1204        $( input ).attr( 'aria-invalid', msg ? 'true' : null );
     1205}
     1206$( document ).on( 'blur', '.bcs-smtp-webhook input', function () { checkWebhookField( this, true ); } );
     1207$( document ).on( 'input', '.bcs-smtp-webhook input', function () { checkWebhookField( this, false ); } );
     1208
     1209/* Unsaved changes: the test alert uses saved settings, so it waits for a save */
     1210function setAlertsDirty( dirty ) {
     1211        var live = $( '#bcs-smtp-alert-channel-enabled' ).is( ':checked' ) && $( '.bcs-smtp-channel-radio:checked' ).length > 0;
     1212        $( '#bcs-smtp-alerts-unsaved' ).prop( 'hidden', ! dirty );
     1213        $( '#bcs-smtp-test-alert-btn' ).prop( 'disabled', dirty || ! live ).attr( 'title', dirty ? T( 'saveFirst' ) : null );
     1214}
     1215$( document ).on( 'change input', '.bcs-smtp-alerts-card input', function () { setAlertsDirty( true ); } );
     1216
     1217/* Edits live only in the page until saved, so warn before leaving with unsaved changes */
     1218$( window ).on( 'beforeunload', function ( e ) {
     1219        if ( $( '.bcs-smtp-unsaved' ).filter( function () { return ! this.hidden; } ).length ) {
     1220                e.preventDefault();
     1221                return '';
     1222        }
     1223} );
     1224
     1225function showWebhook( key, focus ) {
     1226        activeWebhook = key;
     1227        syncWebhookFields();
     1228        if ( focus ) { webhookInput( key ).trigger( 'focus' ); }
     1229}
     1230
     1231/* The row itself: a channel that's off is switched on; one that's on opens or closes its field */
     1232$( document ).on( 'click', '.bcs-smtp-ch-main', function () {
     1233        var box = $( this ).closest( '.bcs-smtp-ch' ).find( '.bcs-smtp-channel-radio' )[ 0 ];
     1234        if ( ! box.checked ) {
     1235                box.checked = true;
     1236                $( box ).trigger( 'change' );
     1237                setAlertsDirty( true );
     1238                return;
     1239        }
     1240        if ( activeWebhook === box.value ) {
     1241                activeWebhook = null;
     1242                syncWebhookFields();
     1243        } else {
     1244                showWebhook( box.value, false );
     1245        }
     1246} );
     1247
     1248/* Typing a URL updates its row's status straight away */
     1249$( document ).on( 'input', '.bcs-smtp-webhook input', syncWebhookFields );
     1250
     1251$( document ).on( 'change', '.bcs-smtp-channel-radio', function () {
     1252        if ( this.checked ) {
     1253                showWebhook( this.value, ! $.trim( webhookInput( this.value ).val() ) );
     1254        } else {
     1255                syncWebhookFields();
     1256        }
     1257} );
     1258
     1259function simpleSave( btnSel, msgSel, data, onSaved ) {
     1260        $( document ).on( 'click', btnSel, function () {
     1261                var $btn = $( this );
     1262                setBusy( $btn, T( 'saving' ) );
     1263                $.post( bcsSmtpData.ajaxurl, $.extend( { nonce: bcsSmtpData.nonce }, data() ) )
     1264                        .done( function ( r ) {
     1265                                if ( r && r.success ) {
     1266                                        showInlineMsg( msgSel, 'success', ( r.data && r.data.message ) || T( 'saved' ) );
     1267                                        if ( onSaved ) { onSaved( r ); }
     1268                                } else {
     1269                                        showInlineMsg( msgSel, 'error', errorText( r, T( 'saveFailed' ) ) );
     1270                                }
     1271                        } )
     1272                        .fail( function ( xhr ) { showInlineMsg( msgSel, 'error', failText( xhr ) ); } )
     1273                        .always( function () { clearBusy( $btn ); } );
     1274        } );
     1275}
     1276
     1277simpleSave( '#bcs-smtp-save-alerts-btn', '#bcs-smtp-alerts-msg', function () {
     1278        if ( $( '#bcs-smtp-alert-channel-enabled' ).is( ':checked' ) ) {
     1279                $( '.bcs-smtp-channel-radio:checked' ).each( function () {
     1280                        var url = $.trim( webhookInput( this.value ).val() );
     1281                        if ( ! url || webhookProblem( this.value, url ) ) {
     1282                                showWebhook( this.value, true );
     1283                                checkWebhookField( webhookInput( this.value )[0], true );
     1284                                return false;
     1285                        }
     1286                } );
     1287        }
     1288        return {
     1289                action                  : 'bcs_smtp_save_alerts',
     1290                alert_channel_enabled   : $( '#bcs-smtp-alert-channel-enabled' ).is( ':checked' ) ? 1 : 0,
     1291                alert_channels          : $( '.bcs-smtp-channel-radio:checked' ).map( function () { return this.value; } ).get(),
     1292                slack_webhook_url       : $( '#bcs-smtp-slack-webhook-url' ).val(),
     1293                teams_webhook_url       : $( '#bcs-smtp-teams-webhook-url' ).val(),
     1294                google_chat_webhook_url : $( '#bcs-smtp-google-chat-webhook-url' ).val(),
     1295                discord_webhook_url     : $( '#bcs-smtp-discord-webhook-url' ).val(),
     1296                custom_webhook_url      : $( '#bcs-smtp-custom-webhook-url' ).val(),
     1297        };
     1298}, function () { setAlertsDirty( false ); } );
     1299
    5371300$( document ).on( 'click', '#bcs-smtp-test-alert-btn', function () {
    5381301        var $btn = $( this );
    539         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Sending…' );
    540 
    541         $.post( bcsSmtpData.ajaxurl, {
    542                 action : 'bcs_smtp_send_test_alert',
    543                 nonce  : bcsSmtpData.nonce,
    544         }, function ( r ) {
    545                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-megaphone"></span> Send Test Alert' );
    546                 if ( r && r.success ) {
    547                         showInlineMsg( '#bcs-smtp-alerts-msg', 'success', r.data.message );
    548                 } else {
    549                         showInlineMsg( '#bcs-smtp-alerts-msg', 'error', ( r && r.data ) ? r.data : 'Alert dispatch failed.' );
    550                 }
    551         } ).fail( function ( xhr ) {
    552                 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-megaphone"></span> Send Test Alert' );
    553                 showInlineMsg( '#bcs-smtp-alerts-msg', 'error', 'Request failed (HTTP ' + xhr.status + ').' );
    554         } );
     1302        setBusy( $btn, T( 'sending' ) );
     1303        $.post( bcsSmtpData.ajaxurl, { action: 'bcs_smtp_send_test_alert', nonce: bcsSmtpData.nonce } )
     1304                .done( function ( r ) {
     1305                        if ( r && r.success ) {
     1306                                showInlineMsg( '#bcs-smtp-alerts-msg', 'success', r.data.message );
     1307                        } else {
     1308                                showInlineMsg( '#bcs-smtp-alerts-msg', 'error', errorText( r, T( 'saveFailed' ) ) );
     1309                        }
     1310                } )
     1311                .fail( function ( xhr ) { showInlineMsg( '#bcs-smtp-alerts-msg', 'error', failText( xhr ) ); } )
     1312                .always( function () { clearBusy( $btn ); } );
    5551313} );
    5561314
    5571315/* =====================================================================
    558    SETTINGS TAB: Change Password button
     1316   SETTINGS
    5591317   ===================================================================== */
    560 $( document ).on( 'click', '#bcs-smtp-change-pw-btn', function () {
    561         $( '#bcs-smtp-pw-display' ).hide();
    562         $( '#bcs-smtp-pw-cancel-row' ).show();
    563         $( '#bcs-smtp-pw-input' ).show();
    564         $( '#bcs-smtp-smtp-password' ).val( '' ).focus();
    565         $( '#bcs-smtp-pw-status' )
    566                 .text( 'Enter a new password — leave blank to keep the existing one.' )
    567                 .css( 'color', 'var(--bcs-smtp-text-3)' );
    568 } );
    569 
    570 $( document ).on( 'click', '#bcs-smtp-cancel-pw-btn', function ( e ) {
    571         e.preventDefault();
    572         $( '#bcs-smtp-pw-input' ).hide();
    573         $( '#bcs-smtp-pw-cancel-row' ).hide();
    574         $( '#bcs-smtp-pw-display' ).css( 'display', 'flex' );
    575         $( '#bcs-smtp-smtp-password' ).val( '' );
    576         $( '#bcs-smtp-pw-status' )
    577                 .text( '✓ Password saved — leave blank to keep it.' )
    578                 .css( 'color', 'var(--bcs-smtp-emerald)' );
    579 } );
    580 
    581 /* Show/hide the typed password */
    582 $( document ).on( 'click', '#bcs-smtp-toggle-pw-btn', function () {
    583         var $btn   = $( this );
    584         var $input = $( '#bcs-smtp-smtp-password' );
    585         var $icon  = $btn.find( '.dashicons' );
    586         var hidden = $input.attr( 'type' ) === 'password';
    587 
    588         $input.attr( 'type', hidden ? 'text' : 'password' );
    589         $icon.toggleClass( 'dashicons-visibility', ! hidden ).toggleClass( 'dashicons-hidden', hidden );
    590         $btn.attr( 'aria-label', hidden ? 'Hide password' : 'Show password' );
    591 } );
    592 
    593 /* =====================================================================
    594    RESEND FAILED EMAIL (Email Log tab)
    595    ===================================================================== */
    596 $( document ).on( 'click', '.bcs-smtp-resend-btn', function () {
    597         var $btn   = $( this );
    598         var logId  = $btn.data( 'log-id' );
    599         var orig   = $btn.html();
    600 
    601         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Resending…' );
    602 
    603         $.post( bcsSmtpData.ajaxurl, {
    604                 action: 'bcs_smtp_resend_email',
    605                 nonce : bcsSmtpData.nonce,
    606                 log_id: logId,
    607         } ).done( function ( r ) {
    608                 if ( r && r.success ) {
    609                         // Remove the button in place rather than reloading the table — the original
    610                         // row's status stays 'failed' (the resend created a separate log entry), so a
    611                         // full reload would just show the same Resend button again.
    612                         $btn.replaceWith( '<span class="bcs-smtp-badge bcs-smtp-badge-success">' +
    613                                 '<span class="dashicons dashicons-yes"></span> Resent</span>' );
    614                 } else {
    615                         $btn.prop( 'disabled', false ).html( orig );
    616                         window.alert( ( r && r.data ) ? r.data : 'Resend failed.' );
    617                 }
    618         } ).fail( function () {
    619                 $btn.prop( 'disabled', false ).html( orig );
    620                 window.alert( 'Request failed.' );
    621         } );
    622 } );
    623 
    624 /* =====================================================================
    625    RESEND SETTINGS (Config tab)
    626    ===================================================================== */
    627 $( document ).on( 'click', '#bcs-smtp-save-resend-btn', function () {
    628         var $btn = $( this );
    629         var orig = $btn.html();
    630 
    631         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Saving…' );
    632         $.post( bcsSmtpData.ajaxurl, {
    633                 action            : 'bcs_smtp_save_resend_settings',
    634                 nonce             : bcsSmtpData.nonce,
    635                 store_failed_body : $( '#bcs-smtp-store-failed-body' ).is( ':checked' ) ? 1 : 0,
    636                 retention_days    : $( '#bcs-smtp-failed-body-retention' ).val(),
    637         } ).done( function ( r ) {
    638                 showInlineMsg( '#bcs-smtp-resend-settings-msg', r && r.success ? 'success' : 'error', ( r && r.data && r.data.message ) ? r.data.message : ( ( r && r.data ) ? r.data : 'Save failed.' ) );
    639         } ).fail( function () {
    640                 showInlineMsg( '#bcs-smtp-resend-settings-msg', 'error', 'Request failed.' );
    641         } ).always( function () {
    642                 $btn.prop( 'disabled', false ).html( orig );
    643         } );
    644 } );
    645 
    646 /* =====================================================================
    647    Inline message helper
    648    ===================================================================== */
    649 function showInlineMsg( selector, type, msg ) {
    650         var icons = { success: 'yes-alt', error: 'dismiss', warning: 'warning', info: 'info-outline' };
    651         var $el   = $( selector );
    652         $el.css( { display: 'block', width: '100%' } ).html(
    653                 '<div class="bcs-smtp-alert bcs-smtp-alert-' + type + '" style="margin-top:0;width:100%;">' +
    654                 '<span class="dashicons dashicons-' + icons[ type ] + '"></span>' +
    655                 '<div class="bcs-smtp-alert-content">' + msg + '</div></div>'
    656         );
    657         setTimeout( function () {
    658                 $el.fadeOut( 400, function () { $el.empty().show().css( 'display', 'none' ); } );
    659         }, 5000 );
    660 }
    661 
    662 
    663 /* =====================================================================
    664    EMAIL LOG — Export CSV
    665    ===================================================================== */
    666 $( document ).on( 'click', '#bcs-smtp-export-csv-btn', function ( e ) {
    667         e.preventDefault();
    668         var url = bcsSmtpData.exportUrl
    669                 + '&nonce='      + encodeURIComponent( bcsSmtpData.exportNonce )
    670                 + '&date_range=' + encodeURIComponent( $( '#bcs-smtp-log-date-range' ).val() )
    671                 + '&status='     + encodeURIComponent( $( '#bcs-smtp-log-status' ).val() )
    672                 + '&search='     + encodeURIComponent( $( '#bcs-smtp-log-search' ).val() );
    673         window.location.href = url;
    674 } );
    675 
    676 /* =====================================================================
    677    EMAIL LOG — Log Retention / Auto-prune
    678    ===================================================================== */
    679 $( document ).on( 'click', '#bcs-smtp-save-log-prune-btn', function () {
    680         var $btn    = $( this );
    681         var btnHtml = '<span class="dashicons dashicons-saved"></span>Save Retention Settings';
    682         var mode    = $( '.bcs-smtp-prune-radio:checked' ).val();
    683         var value   = mode === 'days'    ? $( '#bcs-smtp-prune-days' ).val()
    684                     : mode === 'records' ? $( '#bcs-smtp-prune-records' ).val()
    685                     : 30;
    686 
    687         $btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update bcs-smtp-spinning"></span> Saving…' );
    688 
    689         $.post( bcsSmtpData.ajaxurl, {
    690                 action      : 'bcs_smtp_save_log_prune',
    691                 nonce       : bcsSmtpData.nonce,
    692                 prune_mode  : mode,
    693                 prune_value : value,
    694         }, function ( r ) {
    695                 $btn.prop( 'disabled', false ).html( btnHtml );
    696                 if ( r && r.success ) {
    697                         showInlineMsg( '#bcs-smtp-log-prune-msg', 'success', r.data.message || 'Saved.' );
    698                 } else {
    699                         showInlineMsg( '#bcs-smtp-log-prune-msg', 'error', ( r && r.data ) ? r.data : 'Save failed.' );
    700                 }
    701         } ).fail( function () {
    702                 $btn.prop( 'disabled', false ).html( btnHtml );
    703                 showInlineMsg( '#bcs-smtp-log-prune-msg', 'error', 'Request failed.' );
    704         } );
    705 } );
    706 
    707 
    7081318/* =====================================================================
    7091319   Init
    7101320   ===================================================================== */
    7111321$( function () {
     1322        // Browsers restore form state on reload, which can show a switch or field that no longer
     1323        // matches what is saved. Put every control back to the value the server rendered.
     1324        $( '.bcs-smtp-wrap' ).find( 'input, select, textarea' ).each( function () {
     1325                if ( this.type === 'checkbox' || this.type === 'radio' ) {
     1326                        this.checked = this.defaultChecked;
     1327                } else if ( this.tagName === 'SELECT' ) {
     1328                        var def = $( this ).find( 'option' ).filter( function () { return this.defaultSelected; } );
     1329                        this.selectedIndex = def.length ? def.index() : 0;
     1330                } else if ( this.type !== 'hidden' && this.type !== 'password' ) {
     1331                        this.value = this.defaultValue;
     1332                }
     1333        } );
    7121334        syncWebhookFields();
    713 
    714         // Load the active panel's data if it's a data panel
    715         var activePanel = getStoredTab();
    716         if ( activePanel === 'email-log' ) {
    717                 emailLogLoaded = true;
    718                 loadEmailLog();
    719         }
    720         if ( activePanel === 'email-report' ) {
    721                 emailReportLoaded = true;
    722                 loadEmailReport();
     1335        updateServerSummary();
     1336        renderChips();
     1337        syncConnectionDot();
     1338        syncPickedProvider();
     1339        // The single-email page has no tabs; leave the remembered tab and its URL alone there.
     1340        if ( $( '.bcs-smtp-tab' ).length ) {
     1341                var hash   = ( window.location.hash || '' ).replace( '#', '' );
     1342                var stored = store.get( TAB_KEY );
     1343                setActiveTab( hash && $( '.bcs-smtp-tab[data-panel="' + hash + '"]' ).length ? hash : ( stored || 'overview' ) );
    7231344        }
    7241345} );
  • bcs-smtp-manager/trunk/bcs-smtp-manager.php

    r3652946 r3762759  
    44 * Plugin URI: https://bytecorestack.com/plugins/smtp-manager/
    55 * Description: Send WordPress email through any authenticated SMTP server, log every message, and get an alert on Slack, Microsoft Teams, Google Chat, or Discord the moment delivery fails.
    6  * Version: 1.2.1
     6 * Version: 1.3.0
    77 * Author: ByteCore Stack
    88 * Author URI: https://bytecorestack.com/
    … …  
    2121
    2222// Define plugin constants
    23 define( 'BCS_SMTP_VERSION',         '1.2.1' );
     23define( 'BCS_SMTP_VERSION',         '1.3.0' );
    2424define( 'BCS_SMTP_PLUGIN_DIR',      plugin_dir_path( __FILE__ ) );
    2525define( 'BCS_SMTP_PLUGIN_URL',      plugin_dir_url( __FILE__ ) );
    2626define( 'BCS_SMTP_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
    2727
    28 // Monitor/alert option names — defined once so the read side and write side can
    29 // never drift apart (a literal-string mismatch caused the monitor toggle to silently
    30 // no-op in 1.1.0; see class-bcs-smtp-monitor.php).
    31 define( 'BCS_SMTP_OPT_MONITOR_ENABLED',       'bcs_monitor_enabled' );
    32 define( 'BCS_SMTP_OPT_CHECK_INTERVAL',        'bcs_check_interval' );
    33 define( 'BCS_SMTP_OPT_FAILURE_THRESHOLD',     'bcs_failure_threshold' );
     28// Monitor/alert option names — defined once so the read side and write side can never drift apart.
    3429define( 'BCS_SMTP_OPT_CONSECUTIVE_FAILURES',  'bcs_consecutive_failures' );
    3530define( 'BCS_SMTP_OPT_LAST_CHECK',            'bcs_last_check' );
    … …  
    4237define( 'BCS_SMTP_OPT_DISCORD_WEBHOOK_URL',     'bcs_discord_webhook_url' );
    4338define( 'BCS_SMTP_OPT_CUSTOM_WEBHOOK_URL',      'bcs_custom_webhook_url' );
    44 define( 'BCS_SMTP_OPT_MAX_ALERT_COUNT',       'bcs_max_alert_count' );
    4539define( 'BCS_SMTP_OPT_ALERT_SENT_COUNT',      'bcs_alert_sent_count' );
    4640
    47 // Resend feature — failed-email body storage is opt-in and retained only briefly.
    48 define( 'BCS_SMTP_OPT_STORE_FAILED_BODY',           'bcs_store_failed_body' );
    49 define( 'BCS_SMTP_OPT_FAILED_BODY_RETENTION_DAYS',  'bcs_failed_body_retention_days' );
     41// The email log keeps this many days; reports and charts are limited to the same window.
     42define( 'BCS_SMTP_LOG_RETENTION_DAYS', 90 );
     43
     44// Bumped whenever the log table's columns change, so dbDelta runs again on existing sites.
     45define( 'BCS_SMTP_DB_VERSION', '1.3.0.1' );
     46
     47// Health checks are always on once SMTP is set up, with a fixed policy.
     48define( 'BCS_SMTP_CHECK_INTERVAL_MINUTES', 15 ); // Minutes between checks.
     49define( 'BCS_SMTP_FAILURE_THRESHOLD', 1 );       // Alert on the first failed check.
     50define( 'BCS_SMTP_MAX_ALERTS_PER_OUTAGE', 1 );   // One alert until mail recovers.
     51
     52// Random secret the SMTP password is encrypted with (see BCS_SMTP_Mailer::get_crypto_key()).
     53define( 'BCS_SMTP_OPT_CRYPTO_KEY', 'bcs_smtp_crypto_key' );
    5054
    5155// Load required files
    … …  
    6266}
    6367
    64 // Settings link on the Plugins page.
    65 if ( ! function_exists( 'bcs_smtp_plugin_action_links' ) ) {
    66         function bcs_smtp_plugin_action_links( $links ) {
    67                 $settings = '<a href="' . esc_url( admin_url( 'options-general.php?page=bcs-smtp-manager' ) ) . '">' . __( 'Settings', 'bcs-smtp-manager' ) . '</a>';
    68                 array_unshift( $links, $settings );
    69                 return $links;
    70         }
    71         add_filter( 'plugin_action_links_' . BCS_SMTP_PLUGIN_BASENAME, 'bcs_smtp_plugin_action_links' );
    72 }
    73 
    74 if ( ! function_exists( 'bcs_smtp_init' ) ) {
    75         /**
    76          * Initialize the plugin.
    77          */
    78         function bcs_smtp_init() {
    79                 $plugin = new BCS_SMTP_Core();
    80                 $plugin->run();
    81         }
    82         add_action( 'plugins_loaded', 'bcs_smtp_init' );
    83 }
    84 
    85 if ( ! function_exists( 'bcs_smtp_activate' ) ) {
    86         /**
    87          * Activation hook — creates tables and sets default options.
    88          *
    89          * @since 1.0
    90          */
    91         function bcs_smtp_activate() {
    92                 $db = new BCS_SMTP_DB();
    93                 $db->create_tables();
    94 
    95                 // SMTP connection settings
    96                 add_option( 'bcs_smtp_enabled',           false );
    97                 add_option( 'bcs_from_email',             get_option( 'admin_email' ) );
    98                 add_option( 'bcs_from_name',              get_option( 'blogname' ) );
    99                 add_option( 'bcs_smtp_host',              '' );
    100                 add_option( 'bcs_smtp_encryption',        'tls' );
    101                 add_option( 'bcs_smtp_port',              587 );
    102                 add_option( 'bcs_auto_tls',               true );
    103                 add_option( 'bcs_smtp_auth',              true );
    104                 add_option( 'bcs_smtp_username',          '' );
    105                 add_option( 'bcs_smtp_password',          '' );
    106 
    107                 // Monitoring settings
    108                 add_option( BCS_SMTP_OPT_MONITOR_ENABLED,      false );
    109                 add_option( BCS_SMTP_OPT_CHECK_INTERVAL,       5 );
    110                 add_option( BCS_SMTP_OPT_FAILURE_THRESHOLD,    2 );
    111                 add_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, 0 );
    112                 add_option( BCS_SMTP_OPT_LAST_CHECK,           0 );
    113                 add_option( BCS_SMTP_OPT_LAST_CHECK_SUCCESS,   null );
    114 
    115                 // Alert channel settings
    116                 add_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED, false );
    117                 add_option( BCS_SMTP_OPT_ALERT_CHANNEL,         'none' );
    118                 add_option( 'bcs_alert_webhook_url',            '' );
    119                 add_option( BCS_SMTP_OPT_SLACK_WEBHOOK_URL,        '' );
    120                 add_option( BCS_SMTP_OPT_TEAMS_WEBHOOK_URL,        '' );
    121                 add_option( BCS_SMTP_OPT_GOOGLE_CHAT_WEBHOOK_URL,  '' );
    122                 add_option( BCS_SMTP_OPT_DISCORD_WEBHOOK_URL,      '' );
    123                 add_option( BCS_SMTP_OPT_CUSTOM_WEBHOOK_URL,       '' );
    124                 add_option( BCS_SMTP_OPT_MAX_ALERT_COUNT,       0 );
    125                 add_option( BCS_SMTP_OPT_ALERT_SENT_COUNT,      0 );
    126 
    127                 // Migrate old single webhook URL to per-channel options.
    128                 $old_url = get_option( 'bcs_alert_webhook_url', '' );
    129                 $channel = get_option( 'bcs_alert_channel', 'none' );
    130                 if ( ! empty( $old_url ) ) {
    131                         if ( 'slack' === $channel && ! get_option( 'bcs_slack_webhook_url' ) ) {
    132                                 update_option( 'bcs_slack_webhook_url', $old_url );
    133                         } elseif ( 'teams' === $channel && ! get_option( 'bcs_teams_webhook_url' ) ) {
    134                                 update_option( 'bcs_teams_webhook_url', $old_url );
    135                         }
     68/**
     69 * Settings link on the Plugins page.
     70 *
     71 * @param  string[] $links
     72 * @return string[]
     73 */
     74function bcs_smtp_plugin_action_links( $links ) {
     75        $settings = '<a href="' . esc_url( admin_url( 'options-general.php?page=bcs-smtp-manager' ) ) . '">' . esc_html__( 'Settings', 'bcs-smtp-manager' ) . '</a>';
     76        array_unshift( $links, $settings );
     77        return $links;
     78}
     79add_filter( 'plugin_action_links_' . BCS_SMTP_PLUGIN_BASENAME, 'bcs_smtp_plugin_action_links' );
     80
     81/**
     82 * Initialize the plugin.
     83 */
     84function bcs_smtp_init() {
     85        $plugin = new BCS_SMTP_Core();
     86        $plugin->run();
     87}
     88add_action( 'plugins_loaded', 'bcs_smtp_init' );
     89
     90/**
     91 * Activation hook — creates tables and sets default options.
     92 *
     93 * @since 1.0
     94 */
     95function bcs_smtp_activate() {
     96        $db = new BCS_SMTP_DB();
     97        $db->create_tables();
     98
     99        // SMTP connection settings
     100        add_option( 'bcs_smtp_enabled',           false );
     101        add_option( 'bcs_from_email',             get_option( 'admin_email' ) );
     102        add_option( 'bcs_from_name',              get_option( 'blogname' ) );
     103        add_option( 'bcs_smtp_host',              '' );
     104        add_option( 'bcs_smtp_encryption',        'tls' );
     105        add_option( 'bcs_smtp_port',              587 );
     106        add_option( 'bcs_auto_tls',               true );
     107        add_option( 'bcs_smtp_auth',              true );
     108        add_option( 'bcs_smtp_username',          '' );
     109        add_option( 'bcs_smtp_password',          '' );
     110
     111        // Monitoring settings
     112        add_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, 0 );
     113        add_option( BCS_SMTP_OPT_LAST_CHECK,           0 );
     114        add_option( BCS_SMTP_OPT_LAST_CHECK_SUCCESS,   null );
     115
     116        // Alert channel settings
     117        add_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED, false );
     118        add_option( BCS_SMTP_OPT_ALERT_CHANNEL,         'none' );
     119        add_option( BCS_SMTP_OPT_SLACK_WEBHOOK_URL,        '' );
     120        add_option( BCS_SMTP_OPT_TEAMS_WEBHOOK_URL,        '' );
     121        add_option( BCS_SMTP_OPT_GOOGLE_CHAT_WEBHOOK_URL,  '' );
     122        add_option( BCS_SMTP_OPT_DISCORD_WEBHOOK_URL,      '' );
     123        add_option( BCS_SMTP_OPT_CUSTOM_WEBHOOK_URL,       '' );
     124        add_option( BCS_SMTP_OPT_ALERT_SENT_COUNT,      0 );
     125
     126        // Scheduled report settings (sent on the 1st of each month).
     127        add_option( 'bcs_report_schedule_enabled',    false );
     128        add_option( 'bcs_report_emails',              '' );
     129}
     130register_activation_hook( __FILE__, 'bcs_smtp_activate' );
     131
     132/**
     133 * Deactivation hook — stops the scheduled checks, pruning and reports. They are booked again
     134 * on the next page load after the plugin is reactivated.
     135 */
     136function bcs_smtp_deactivate() {
     137        foreach ( array( 'bcs_smtp_health_check', 'bcs_smtp_prune_log', 'bcs_smtp_send_report' ) as $bcs_smtp_hook ) {
     138                wp_clear_scheduled_hook( $bcs_smtp_hook );
     139        }
     140}
     141register_deactivation_hook( __FILE__, 'bcs_smtp_deactivate' );
     142
     143/**
     144 * One-time migration from 1.0: copy the old single webhook URL into the per-channel options.
     145 */
     146function bcs_smtp_migrate_webhook_options() {
     147        if ( get_option( 'bcs_smtp_migrated_v2' ) ) {
     148                return;
     149        }
     150        $bcs_smtp_old_url = get_option( 'bcs_alert_webhook_url', '' );
     151        $bcs_smtp_channel = get_option( 'bcs_alert_channel', 'none' );
     152        if ( ! empty( $bcs_smtp_old_url ) ) {
     153                if ( 'slack' === $bcs_smtp_channel && ! get_option( 'bcs_slack_webhook_url' ) ) {
     154                        update_option( 'bcs_slack_webhook_url', $bcs_smtp_old_url );
     155                } elseif ( 'teams' === $bcs_smtp_channel && ! get_option( 'bcs_teams_webhook_url' ) ) {
     156                        update_option( 'bcs_teams_webhook_url', $bcs_smtp_old_url );
    136157                }
    137 
    138                 // Scheduled report settings
    139                 add_option( 'bcs_report_schedule_enabled',    false );
    140                 add_option( 'bcs_report_emails',              '' );
    141                 add_option( 'bcs_report_frequency',           'weekly' );
    142 
    143                 add_option( 'bcs_smtp_snapshot',              array() );
    144 
    145                 // Log retention / auto-prune settings
    146                 add_option( 'bcs_log_prune_mode',  'disabled' );
    147                 add_option( 'bcs_log_prune_value', 30 );
    148 
    149                 // Resend feature — opt-in, off by default.
    150                 add_option( BCS_SMTP_OPT_STORE_FAILED_BODY,          false );
    151                 add_option( BCS_SMTP_OPT_FAILED_BODY_RETENTION_DAYS, 7 );
    152 
    153                 flush_rewrite_rules();
    154         }
    155         register_activation_hook( __FILE__, 'bcs_smtp_activate' );
    156 }
    157 
    158 if ( ! function_exists( 'bcs_smtp_deactivate' ) ) {
    159         /**
    160          * Deactivation hook.
    161          */
    162         function bcs_smtp_deactivate() {
    163                 $timestamp = wp_next_scheduled( 'bcs_smtp_health_check' );
    164                 if ( $timestamp ) {
    165                         wp_unschedule_event( $timestamp, 'bcs_smtp_health_check' );
    166                 }
    167                 flush_rewrite_rules();
    168         }
    169         register_deactivation_hook( __FILE__, 'bcs_smtp_deactivate' );
    170 }
    171 
    172 // One-time migration: copy old single webhook URL into per-channel options if not yet done.
    173 if ( ! function_exists( 'bcs_smtp_migrate_webhook_options' ) ) {
    174         function bcs_smtp_migrate_webhook_options() {
    175                 if ( get_option( 'bcs_smtp_migrated_v2' ) ) {
    176                         return;
    177                 }
    178                 $bcs_smtp_old_url = get_option( 'bcs_alert_webhook_url', '' );
    179                 $bcs_smtp_channel = get_option( 'bcs_alert_channel', 'none' );
    180                 if ( ! empty( $bcs_smtp_old_url ) ) {
    181                         if ( 'slack' === $bcs_smtp_channel ) {
    182                                 update_option( 'bcs_slack_webhook_url', $bcs_smtp_old_url );
    183                         } elseif ( 'teams' === $bcs_smtp_channel ) {
    184                                 update_option( 'bcs_teams_webhook_url', $bcs_smtp_old_url );
    185                         }
    186                 }
    187                 if ( false === get_option( 'bcs_max_alert_count', false ) ) {
    188                         add_option( 'bcs_max_alert_count', 0 );
    189                 }
    190                 if ( false === get_option( 'bcs_alert_sent_count', false ) ) {
    191                         add_option( 'bcs_alert_sent_count', 0 );
    192                 }
    193                 update_option( 'bcs_smtp_migrated_v2', true );
    194 
    195                 // Resend feature defaults for sites that installed before 1.2 (add_option is a no-op if already set).
    196                 add_option( BCS_SMTP_OPT_STORE_FAILED_BODY,          false );
    197                 add_option( BCS_SMTP_OPT_FAILED_BODY_RETENTION_DAYS, 7 );
    198         }
    199         add_action( 'plugins_loaded', 'bcs_smtp_migrate_webhook_options' );
    200 }
     158        }
     159        delete_option( 'bcs_alert_webhook_url' );
     160        add_option( 'bcs_alert_sent_count', 0 );
     161        update_option( 'bcs_smtp_migrated_v2', true );
     162}
     163add_action( 'plugins_loaded', 'bcs_smtp_migrate_webhook_options', 4 );
     164
     165/**
     166 * One-time cleanup for 1.3: health checks, log retention and the report schedule became fixed,
     167 * and the resend feature is gone. Drops the settings that no longer do anything, and the old
     168 * daily/weekly report event; the monthly one is booked on init (BCS_SMTP_Core::schedule_events()).
     169 * Saved email content is kept: 1.3 shows it on each email's page.
     170 */
     171function bcs_smtp_migrate_1_3() {
     172        if ( get_option( 'bcs_smtp_migrated_1_3' ) ) {
     173                return;
     174        }
     175        $bcs_smtp_retired = array(
     176                'bcs_store_failed_body',
     177                'bcs_failed_body_retention_days',
     178                'bcs_log_prune_mode',
     179                'bcs_log_prune_value',
     180                'bcs_monitor_enabled',
     181                'bcs_check_interval',
     182                'bcs_failure_threshold',
     183                'bcs_max_alert_count',
     184                'bcs_report_frequency',
     185                'bcs_smtp_snapshot',
     186                'bcs_smtp_monitor_enabled', // 1.0.0
     187        );
     188        foreach ( $bcs_smtp_retired as $bcs_smtp_old ) {
     189                delete_option( $bcs_smtp_old );
     190        }
     191
     192        // The single webhook URL from the earliest builds: make sure it reached its channel, then drop it.
     193        $bcs_smtp_old_url = (string) get_option( 'bcs_alert_webhook_url', '' );
     194        $bcs_smtp_channel = get_option( 'bcs_alert_channel', 'none' );
     195        if ( '' !== $bcs_smtp_old_url && in_array( $bcs_smtp_channel, array( 'slack', 'teams' ), true ) && ! get_option( 'bcs_' . $bcs_smtp_channel . '_webhook_url' ) ) {
     196                update_option( 'bcs_' . $bcs_smtp_channel . '_webhook_url', $bcs_smtp_old_url );
     197        }
     198        delete_option( 'bcs_alert_webhook_url' );
     199
     200        wp_clear_scheduled_hook( 'bcs_smtp_health_check' ); // Rescheduled on the fixed 15-minute interval.
     201        wp_clear_scheduled_hook( 'bcs_smtp_send_report' );  // Rebooked for the 1st of the month.
     202
     203        // Earlier versions could keep the log forever; 1.3 keeps 90 days. Nothing is pruned for
     204        // 30 days after updating, so older entries can be exported first (the Email log says so).
     205        add_option( 'bcs_smtp_prune_after', time() + 30 * DAY_IN_SECONDS, '', false );
     206
     207        // 1.0.0 stored the SMTP password as plain text; encrypt it now rather than on the next save.
     208        $bcs_smtp_pw = (string) get_option( 'bcs_smtp_password', '' );
     209        if ( '' !== $bcs_smtp_pw && 0 !== strpos( $bcs_smtp_pw, 'enc:' ) && 0 !== strpos( $bcs_smtp_pw, 'enc2:' ) ) {
     210                $bcs_smtp_mailer = new BCS_SMTP_Mailer();
     211                update_option( 'bcs_smtp_password', $bcs_smtp_mailer->encrypt_password( $bcs_smtp_pw ) );
     212        }
     213
     214        update_option( 'bcs_smtp_migrated_1_3', true );
     215}
     216add_action( 'plugins_loaded', 'bcs_smtp_migrate_1_3', 5 ); // After the 1.0 migration, before bcs_smtp_init().
  • bcs-smtp-manager/trunk/includes/class-bcs-smtp-core.php

    r3652946 r3762759  
    5555                add_action( 'wp_ajax_bcs_smtp_send_test',          array( $this->admin, 'ajax_send_test' ) );
    5656                add_action( 'wp_ajax_bcs_smtp_check_now',          array( $this->admin, 'ajax_check_now' ) );
    57                 add_action( 'wp_ajax_bcs_smtp_save_monitor',       array( $this->admin, 'ajax_save_monitor' ) );
     57                add_action( 'wp_ajax_bcs_smtp_get_health_banner', array( $this->admin, 'ajax_get_health_banner' ) );
    5858                add_action( 'wp_ajax_bcs_smtp_save_alerts',        array( $this->admin, 'ajax_save_alerts' ) );
    5959                add_action( 'wp_ajax_bcs_smtp_send_test_alert',    array( $this->admin, 'ajax_send_test_alert' ) );
    … …  
    6262                add_action( 'wp_ajax_bcs_smtp_save_report_schedule', array( $this->admin, 'ajax_save_report_schedule' ) );
    6363                add_action( 'wp_ajax_bcs_smtp_send_report_now',    array( $this->admin, 'ajax_send_report_now' ) );
    64                 add_action( 'wp_ajax_bcs_smtp_save_log_prune',     array( $this->admin, 'ajax_save_log_prune' ) );
    65                 add_action( 'wp_ajax_bcs_smtp_resend_email',       array( $this->admin, 'ajax_resend_email' ) );
    66                 add_action( 'wp_ajax_bcs_smtp_save_resend_settings', array( $this->admin, 'ajax_save_resend_settings' ) );
    6764        }
    6865
    … …  
    7572        public function run() {
    7673                $this->db->maybe_create_tables();
     74
     75                // Scheduling reads the cron schedules, whose labels are translated, so it waits for init:
     76                // translations loaded earlier trigger a "_load_textdomain_just_in_time" notice (WP 6.7+).
     77                add_action( 'init', array( $this, 'schedule_events' ) );
     78        }
     79
     80        public function schedule_events() {
    7781                $this->monitor->maybe_schedule_event();
     82                $this->admin->ensure_report_scheduled();
    7883
    79                 // Schedule daily log pruning cron (runs even when mode is 'disabled' — no-op in that case).
     84                // Daily pruning keeps the log to the last BCS_SMTP_LOG_RETENTION_DAYS days.
    8085                if ( ! wp_next_scheduled( 'bcs_smtp_prune_log' ) ) {
    8186                        wp_schedule_event( time() + DAY_IN_SECONDS, 'daily', 'bcs_smtp_prune_log' );
  • bcs-smtp-manager/trunk/includes/class-bcs-smtp-db.php

    r3652946 r3762759  
    6969        public function maybe_create_tables() {
    7070                $this->migrate_table_names();
    71                 if ( get_option( 'bcs_db_version' ) !== BCS_SMTP_VERSION ) {
     71                if ( get_option( 'bcs_db_version' ) !== BCS_SMTP_DB_VERSION ) {
    7272                        $this->create_tables();
    73                         update_option( 'bcs_db_version', BCS_SMTP_VERSION );
     73                        update_option( 'bcs_db_version', BCS_SMTP_DB_VERSION );
    7474                }
    7575        }
    … …  
    8080                require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    8181
    82                 // Email log — every wp_mail() call is recorded here. Message body/headers are
    83                 // intentionally NOT in this schema by default — only delivery metadata is retained
    84                 // (see readme.txt). The `body`/`body_format` columns below are the sole exception:
    85                 // populated only for FAILED sends, only when the opt-in "Store failed email content"
    86                 // setting is enabled, and purged after a configurable retention window — see
    87                 // BCS_SMTP_Mailer::on_email_failed() and BCS_SMTP_Admin::run_log_pruning().
     82                // Email log — every wp_mail() call is recorded here with its delivery details and message:
     83                // `body`, `body_format`, `mail_headers` and `attachments` (file names only, never the files).
     84                // Rows are pruned after BCS_SMTP_LOG_RETENTION_DAYS — see BCS_SMTP_Mailer::capture_outgoing_email().
    8885                $sql = "CREATE TABLE {$this->email_log_table()} (
    8986                        id             bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    … …  
    9895                        body           longtext,
    9996                        body_format    varchar(10),
     97                        mail_headers   text,
     98                        attachments    text,
    10099                        resend_count   smallint(5) unsigned NOT NULL DEFAULT 0,
    101100                        last_resend_at datetime NULL,
    … …  
    114113                // Migration: drop the legacy `headers` column from installs that pre-date 1.2 (dbDelta
    115114                // never drops columns). Note: `body` is deliberately NOT in this list from 1.2 onward —
    116                 // it was dropped in 1.1 but is re-added (conditionally populated) by the CREATE TABLE above
    117                 // to support the opt-in "Resend failed emails" feature.
     115                // it was dropped in 1.1 and re-added for the resend feature (removed in 1.3; the column is
     116                // kept so existing tables need no destructive change, and is cleared on upgrade).
    118117                $table = esc_sql( $this->email_log_table() );
    119118                foreach ( array( 'headers' ) as $col ) {
    … …  
    158157                                'message_id'   => isset( $data['message_id'] )   ? $data['message_id']   : null,
    159158                                'provider'     => isset( $data['provider'] )     ? $data['provider']     : null,
     159                                'body'         => isset( $data['body'] )         ? $data['body']         : null,
     160                                'body_format'  => isset( $data['body_format'] )  ? $data['body_format']  : null,
     161                                'mail_headers' => isset( $data['mail_headers'] ) ? $data['mail_headers'] : null,
     162                                'attachments'  => isset( $data['attachments'] )  ? $data['attachments']  : null,
    160163                        ),
    161                         array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
     164                        array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
    162165                );
    163166                return (int) $wpdb->insert_id;
    … …  
    258261        }
    259262
    260         /**
    261          * Store the message body for a failed send (opt-in, see BCS_SMTP_OPT_STORE_FAILED_BODY).
    262          * Never called for successful/unconfirmed sends.
    263          *
    264          * @param int    $id
    265          * @param string $body
    266          * @param string $format 'html'|'text'
    267          */
    268         public function update_email_log_body( $id, $body, $format = 'text' ) {
    269                 global $wpdb;
    270                 $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    271                         $this->email_log_table(),
    272                         array( 'body' => $body, 'body_format' => $format ),
    273                         array( 'id' => (int) $id ),
    274                         array( '%s', '%s' ),
    275                         array( '%d' )
    276                 );
    277         }
    278 
    279         /**
    280          * Bump the resend counter and timestamp on the original log row.
    281          * Called regardless of whether the resend attempt itself succeeds.
    282          *
    283          * @param int $id
    284          */
    285         public function increment_resend_count( $id ) {
    286                 global $wpdb;
    287                 $table = esc_sql( $this->email_log_table() );
    288                 $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    289                         "UPDATE {$table} SET resend_count = resend_count + 1, last_resend_at = %s WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    290                         current_time( 'mysql' ),
    291                         (int) $id
    292                 ) );
    293         }
    294 
    295         /**
    296          * Clear stored bodies of failed emails older than $days — independent of (and typically
    297          * shorter than) the overall log retention window, since bodies are more sensitive than metadata.
    298          *
    299          * @param int $days
    300          */
    301         public function clear_old_failed_bodies( $days ) {
    302                 global $wpdb;
    303                 $table = esc_sql( $this->email_log_table() );
    304                 $days  = max( 1, (int) $days );
    305                 $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    306                         "UPDATE {$table} SET body = NULL, body_format = NULL WHERE status = 'failed' AND body IS NOT NULL AND sent_at < DATE_SUB(NOW(), INTERVAL %d DAY)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    307                         $days
    308                 ) );
    309         }
    310 
    311263        /* ─────────────────────────────────────────────────────────────────── */
    312264        /* Email log — read                                                     */
    … …  
    324276         *   per_page   int     Rows per page
    325277         * }
     278         * Rows carry the delivery columns only, never message bodies, so a page of the log stays light.
     279         *
    326280         * @return array { rows, total, pages, page, per_page }
    327281         */
    … …  
    367321                                $wpdb->prepare(
    368322                                        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    369                                         "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY sent_at DESC LIMIT %d OFFSET %d",
     323                                        "SELECT id, sent_at, subject, from_address, to_address, status, error_msg, message_id, provider FROM {$table} WHERE {$where_sql} ORDER BY sent_at DESC LIMIT %d OFFSET %d",
    370324                                        (int) $args['per_page'],
    371325                                        $offset
    … …  
    381335                                $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
    382336                                        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    383                                         "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY sent_at DESC LIMIT %d OFFSET %d",
     337                                        "SELECT id, sent_at, subject, from_address, to_address, status, error_msg, message_id, provider FROM {$table} WHERE {$where_sql} ORDER BY sent_at DESC LIMIT %d OFFSET %d",
    384338                                        array_merge( $params, array( (int) $args['per_page'], $offset ) )
    385339                                ),
    … …  
    395349                        'per_page' => (int) $args['per_page'],
    396350                );
     351        }
     352
     353
     354        /**
     355         * IDs of the next newer and older emails, for stepping through the log one email at a time.
     356         *
     357         * @param  int $id
     358         * @return array{newer:int,older:int} 0 when there is none.
     359         */
     360        public function get_adjacent_log_ids( $id ) {
     361                global $wpdb;
     362                $table = esc_sql( $this->email_log_table() );
     363                // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
     364                $newer = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE id > %d ORDER BY id ASC LIMIT 1", (int) $id ) );
     365                $older = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE id < %d ORDER BY id DESC LIMIT 1", (int) $id ) );
     366                // phpcs:enable
     367                return array( 'newer' => (int) $newer, 'older' => (int) $older );
     368        }
     369
     370        /**
     371         * How many log entries are older than the given number of days.
     372         *
     373         * @param  int $days
     374         * @return int
     375         */
     376        public function count_email_log_older_than( $days ) {
     377                global $wpdb;
     378                $table  = esc_sql( $this->email_log_table() );
     379                $cutoff = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - (int) $days * DAY_IN_SECONDS );
     380                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
     381                return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE sent_at < %s", $cutoff ) );
     382        }
     383
     384        /**
     385         * Emails per status for a date range and search, for the log's status filter.
     386         *
     387         * @param  string $date_range
     388         * @param  string $search
     389         * @return array{all:int,success:int,failed:int,unconfirmed:int}
     390         */
     391        public function count_email_log_by_status( $date_range = '30days', $search = '' ) {
     392                global $wpdb;
     393                $table  = esc_sql( $this->email_log_table() );
     394                $where  = array( '1=1' );
     395                $params = array();
     396
     397                $date_sql = $this->build_date_where( $date_range );
     398                if ( $date_sql ) {
     399                        $where[] = $date_sql;
     400                }
     401                if ( '' !== $search ) {
     402                        $like     = '%' . $wpdb->esc_like( $search ) . '%';
     403                        $where[]  = '( subject LIKE %s OR from_address LIKE %s OR to_address LIKE %s )';
     404                        $params[] = $like;
     405                        $params[] = $like;
     406                        $params[] = $like;
     407                }
     408                $sql = "SELECT status, COUNT(*) AS n FROM {$table} WHERE " . implode( ' AND ', $where ) . ' GROUP BY status';
     409                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
     410                $rows = $wpdb->get_results( $params ? $wpdb->prepare( $sql, $params ) : $sql, ARRAY_A );
     411
     412                $counts = array( 'all' => 0, 'success' => 0, 'failed' => 0, 'unconfirmed' => 0 );
     413                foreach ( (array) $rows as $r ) {
     414                        if ( isset( $counts[ $r['status'] ] ) ) {
     415                                $counts[ $r['status'] ] = (int) $r['n'];
     416                        }
     417                        $counts['all'] += (int) $r['n'];
     418                }
     419                return $counts;
     420        }
     421
     422        /**
     423         * How many different people actually received email in the range: unique recipient
     424         * addresses on successfully sent emails only (failed and unconfirmed sends don't count).
     425         *
     426         * @param string $date_range       Range key.
     427         * @param array  $exclude          Addresses to leave out (the site's own team).
     428         * @param array  $exclude_domains  Domains whose addresses are left out, e.g. "example.com".
     429         * @return int
     430         */
     431        public function count_unique_recipients( $date_range = '30days', $exclude = array(), $exclude_domains = array() ) {
     432                $exclude         = array_flip( array_map( 'strtolower', array_map( 'trim', (array) $exclude ) ) );
     433                $exclude_domains = array_map( 'strtolower', array_filter( array_map( 'trim', (array) $exclude_domains ) ) );
     434                global $wpdb;
     435                $table    = esc_sql( $this->email_log_table() );
     436                $date_sql = $this->build_date_where( $date_range );
     437                $where    = "WHERE status = 'success'" . ( $date_sql ? " AND {$date_sql}" : '' );
     438
     439                $rows = $wpdb->get_col( "SELECT to_address FROM {$table} {$where}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
     440
     441                // A row can hold several comma-separated addresses; count each person once.
     442                $seen = array();
     443                foreach ( (array) $rows as $list ) {
     444                        foreach ( explode( ',', (string) $list ) as $addr ) {
     445                                $addr = strtolower( trim( $addr ) );
     446                                if ( preg_match( '/<([^>]+)>/', $addr, $m ) ) {
     447                                        $addr = trim( $m[1] );
     448                                }
     449                                if ( '' === $addr || isset( $exclude[ $addr ] ) ) {
     450                                        continue;
     451                                }
     452                                $domain = substr( strrchr( $addr, '@' ), 1 );
     453                                if ( $domain && in_array( $domain, $exclude_domains, true ) ) {
     454                                        continue;
     455                                }
     456                                $seen[ $addr ] = true;
     457                        }
     458                }
     459                return count( $seen );
     460        }
     461
     462        /**
     463         * Return success/failed/unconfirmed/total counts for the given date range.
     464         *
     465         * @param  string $date_range
     466         * @return array  { success, failed, unconfirmed, total, recipients }
     467         */
     468        public function get_email_summary_with_recipients( $date_range = '30days', $exclude = array(), $exclude_domains = array() ) {
     469                $summary               = $this->get_email_summary( $date_range );
     470                $summary['recipients'] = $this->count_unique_recipients( $date_range, $exclude, $exclude_domains );
     471                return $summary;
    397472        }
    398473
    … …  
    426501
    427502        /**
    428          * Return daily counts per status, suitable for a Chart.js dataset.
     503         * Return daily counts per status, used by the admin line charts and the report email.
    429504         *
    430505         * @param  string $date_range
    … …  
    591666                                $s = gmdate( 'Y-m-01 00:00:00', $now );
    592667                                return "sent_at >= '{$s}'";
     668                        case 'last_month':
     669                                $s = gmdate( 'Y-m-01 00:00:00', strtotime( 'first day of last month', $now ) );
     670                                $e = gmdate( 'Y-m-t 23:59:59', strtotime( 'first day of last month', $now ) );
     671                                return "sent_at BETWEEN '{$s}' AND '{$e}'";
    593672                        case '3months':
    594673                                $s = gmdate( 'Y-m-d 00:00:00', strtotime( '-3 months', $now ) );
    … …  
    615694                        'last_week'  => 14,
    616695                        'this_month' => 30,
     696                        'last_month' => 62,
    617697                        '3months'    => 90,
    618698                        '6months'    => 180,
  • bcs-smtp-manager/trunk/includes/class-bcs-smtp-email-template.php

    r3652946 r3762759  
    11<?php
    22/**
    3  * Shared HTML chrome for outgoing plugin emails (test email + scheduled report).
    4  * Holds only the markup that is genuinely identical between templates — each
    5  * template still builds its own header banner and body content.
     3 * Shared layout for the plugin's own emails (test email + monthly report).
     4 *
     5 * Table-based and inline-styled so it holds up in Outlook and Gmail; fluid up to 600px
     6 * so it fits phones; with a hidden preheader for the inbox preview line and dark-mode
     7 * overrides for clients that support them (Apple Mail, Outlook for Mac/iOS).
    68 *
    79 * @package BCS_SMTP_Manager
    … …  
    1315class BCS_SMTP_Email_Template {
    1416
     17        const BRAND = '#1450c4';
     18        const FONT  = "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif";
     19        const MONO  = "ui-monospace,SFMono-Regular,Menlo,Consolas,'Liberation Mono',monospace";
     20
     21        /** Status tones: text colour and soft background. */
     22        const TONES = array(
     23                'ok'   => array( '#0f7a4a', '#e6f6ee' ),
     24                'bad'  => array( '#c62828', '#fdecec' ),
     25                'warn' => array( '#a15c00', '#fdf3e1' ),
     26                'info' => array( '#1450c4', '#e8effc' ),
     27        );
     28
    1529        /**
    16          * Opening half of the email: DOCTYPE/head/body + the outer centering table
    17          * + the inner content card table (open only — caller fills in everything else).
     30         * A whole email document.
    1831         *
    19          * @param string $title          <title> text.
    20          * @param int    $width          Inner card width in px.
    21          * @param int    $radius         Inner card border-radius in px.
    22          * @param string $shadow_opacity Box-shadow alpha, e.g. '.08'.
     32         * @param array $args {
     33         *     @type string $title     Document title.
     34         *     @type string $preheader Inbox preview text (plain).
     35         *     @type string $label     Small label shown above the card, right of the site name (plain).
     36         *     @type string $body      Card rows: one or more escaped `<tr>` elements.
     37         *     @type string $footer    Escaped HTML for the line under the card.
     38         * }
     39         * @return string
    2340         */
    24         public static function wrapper_open( $title, $width = 600, $radius = 14, $shadow_opacity = '.08' ) {
     41        public static function document( $args ) {
     42                $args = wp_parse_args( $args, array( 'title' => '', 'preheader' => '', 'label' => '', 'body' => '', 'footer' => '' ) );
     43                $site = get_bloginfo( 'name' );
     44                // Pads the preview so clients don't fill it with the first lines of the body.
     45                $pad = str_repeat( '&#847;&zwnj;&nbsp;', 40 );
     46
    2547                return '<!DOCTYPE html>
    26 <html lang="en">
    27 <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>' . esc_html( $title ) . '</title></head>
    28 <body style="margin:0;padding:0;background:#f1f5f9;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif">
    29 <table width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f1f5f9" style="background-color:#f1f5f9;padding:40px 16px">
    30 <tr><td align="center">
    31 <table width="' . (int) $width . '" cellpadding="0" cellspacing="0" border="0" bgcolor="#ffffff" style="background-color:#ffffff;border-radius:' . (int) $radius . 'px;overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,' . esc_attr( $shadow_opacity ) . ')">
    32 ';
    33         }
    34 
    35         /** Closing half of the email — matches wrapper_open(). */
    36         public static function wrapper_close() {
    37                 return '
     48<html lang="' . esc_attr( str_replace( '_', '-', get_locale() ) ) . '">
     49<head>
     50<meta charset="UTF-8">
     51<meta name="viewport" content="width=device-width,initial-scale=1">
     52<meta name="color-scheme" content="light dark">
     53<meta name="supported-color-schemes" content="light dark">
     54<title>' . esc_html( $args['title'] ) . '</title>
     55<style>
     56body{margin:0;padding:0;-webkit-text-size-adjust:100%}
     57a{color:' . self::BRAND . '}
     58@media (max-width:620px){
     59  .bcs-pad{padding-left:22px!important;padding-right:22px!important}
     60  .bcs-h1{font-size:24px!important}
     61  .bcs-big{font-size:40px!important}
     62  .bcs-hide-sm{display:none!important}
     63}
     64@media (prefers-color-scheme:dark){
     65  .bcs-bg{background:#0b1120!important}
     66  .bcs-card{background:#111a2e!important;border-color:#1e2a44!important}
     67  .bcs-ink{color:#e8edf6!important}
     68  .bcs-ink2{color:#a7b3c8!important}
     69  .bcs-ink3{color:#7d8aa1!important}
     70  .bcs-line{border-color:#1e2a44!important}
     71  .bcs-soft{background:#0d1526!important}
     72  .bcs-track{background:#1e2a44!important}
     73}
     74</style>
     75</head>
     76<body class="bcs-bg" style="margin:0;padding:0;background:#eef1f6">
     77<div style="display:none;max-height:0;max-width:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#eef1f6">' . esc_html( $args['preheader'] ) . $pad . '</div>
     78<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" class="bcs-bg" bgcolor="#eef1f6" style="background:#eef1f6">
     79<tr><td align="center" style="padding:32px 12px 40px">
     80<!--[if mso]><table role="presentation" width="600" cellpadding="0" cellspacing="0" border="0"><tr><td><![endif]-->
     81<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:600px;font-family:' . self::FONT . '">
     82  <tr><td style="padding:0 6px 14px">
     83    <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
     84      <td class="bcs-ink" style="font-size:14px;font-weight:700;color:#0f172a;letter-spacing:-.1px">' . esc_html( $site ) . '</td>
     85      <td class="bcs-ink3" align="right" style="font-size:12px;color:#8a94a6">' . esc_html( $args['label'] ) . '</td>
     86    </tr></table>
     87  </td></tr>
     88  <tr><td class="bcs-card" bgcolor="#ffffff" style="background:#ffffff;border:1px solid #e2e7ef;border-radius:14px;overflow:hidden">
     89    <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
     90      <tr><td height="4" bgcolor="' . self::BRAND . '" style="background:' . self::BRAND . ';height:4px;font-size:0;line-height:0">&nbsp;</td></tr>
     91' . $args['body'] . '
     92    </table>
     93  </td></tr>
     94  <tr><td class="bcs-ink3" align="center" style="padding:22px 16px 0;font-size:12px;line-height:1.6;color:#8a94a6">' . $args['footer'] . '</td></tr>
    3895</table>
     96<!--[if mso]></td></tr></table><![endif]-->
    3997</td></tr>
    4098</table>
    … …  
    44102
    45103        /**
    46          * The small "Sent from / via ByteCoreStack SMTP" branding line
    47          * used in every plugin email footer. $extra is appended after the brand name
    48          * (e.g. a "Manage settings" link) so each template can still differ slightly.
     104         * Small rounded status pill with a dot.
     105         *
     106         * @param string $text Plain text.
     107         * @param string $tone ok|bad|warn|info.
    49108         */
    50         public static function brand_footer_line( $site_url, $site_name, $extra = '' ) {
    51                 return '<p style="margin:0;font-size:11px;color:#94a3b8">' .
    52                         'Sent from <a href="' . esc_url( $site_url ) . '" style="color:#6366f1;text-decoration:none">' . esc_html( $site_name ) . '</a> ' .
    53                         'via <strong style="color:#64748b">ByteCoreStack SMTP</strong>' . $extra .
    54                         '</p>';
     109        public static function pill( $text, $tone = 'info' ) {
     110                list( $ink, $bg ) = isset( self::TONES[ $tone ] ) ? self::TONES[ $tone ] : self::TONES['info'];
     111                return '<span style="display:inline-block;padding:5px 11px 5px 10px;border-radius:999px;background:' . $bg . ';color:' . $ink . ';font-size:12px;font-weight:600;line-height:1.2">'
     112                        . '<span style="color:' . $ink . '">&#9679;</span>&nbsp;' . esc_html( $text ) . '</span>';
     113        }
     114
     115        /**
     116         * Solid brand button that renders as a button in Outlook too.
     117         *
     118         * @param string $url   Link.
     119         * @param string $label Plain text.
     120         */
     121        public static function button( $url, $label ) {
     122                return '<table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr>
     123  <td bgcolor="' . self::BRAND . '" style="background:' . self::BRAND . ';border-radius:9px">
     124    <a href="' . esc_url( $url ) . '" style="display:inline-block;padding:13px 22px;font-family:' . self::FONT . ';font-size:14px;font-weight:600;color:#ffffff;text-decoration:none;border-radius:9px">' . esc_html( $label ) . '&nbsp;&rarr;</a>
     125  </td></tr></table>';
     126        }
     127
     128        /**
     129         * Label / value rows separated by hairlines.
     130         *
     131         * @param array $rows List of array( label, value, mono? ). Plain text.
     132         */
     133        public static function details( $rows ) {
     134                $html = '';
     135                foreach ( $rows as $i => $row ) {
     136                        $border = $i ? 'border-top:1px solid #edf0f5;' : '';
     137                        $font   = ! empty( $row[2] ) ? 'font-family:' . self::MONO . ';font-size:13px;' : 'font-size:14px;';
     138                        $html  .= '<tr>
     139  <td class="bcs-ink3 bcs-line" width="34%" valign="top" style="' . $border . 'padding:12px 12px 12px 0;font-size:13px;color:#64748b">' . esc_html( $row[0] ) . '</td>
     140  <td class="bcs-ink bcs-line" valign="top" style="' . $border . 'padding:12px 0;' . $font . 'color:#0f172a;word-break:break-word">' . esc_html( $row[1] ) . '</td>
     141</tr>';
     142                }
     143                return '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">' . $html . '</table>';
     144        }
     145
     146        /**
     147         * Footer line: "Sent from {site} by ByteCoreStack SMTP · {link}".
     148         *
     149         * @param string $link_url   Settings link.
     150         * @param string $link_label Plain text.
     151         * @param string $before     Escaped HTML shown on its own line above, optional.
     152         */
     153        public static function footer( $link_url, $link_label, $before = '' ) {
     154                /* translators: 1: site name (linked), 2: plugin name. */
     155                $sent = sprintf( esc_html__( 'Sent from %1$s by %2$s', 'bcs-smtp-manager' ), '<a href="' . esc_url( home_url( '/' ) ) . '" style="color:#64748b;text-decoration:underline">' . esc_html( get_bloginfo( 'name' ) ) . '</a>', 'ByteCoreStack SMTP' );
     156                return ( $before ? $before . '<br>' : '' ) . $sent . ' &middot; <a href="' . esc_url( $link_url ) . '" style="color:#64748b;text-decoration:underline">' . esc_html( $link_label ) . '</a>';
     157        }
     158
     159        /**
     160         * Link to a tab of the plugin's settings page.
     161         *
     162         * @param string $tab overview|connection|alerts|reports|email-log.
     163         */
     164        public static function settings_url( $tab = '' ) {
     165                return admin_url( 'options-general.php?page=bcs-smtp-manager' ) . ( $tab ? '#' . $tab : '' );
    55166        }
    56167}
  • bcs-smtp-manager/trunk/includes/class-bcs-smtp-mailer.php

    r3652946 r3762759  
    1717        public $pending_log_id = null;
    1818
    19         /** Body/format of the email currently being sent — held in memory only, never persisted unless the send fails. */
    20         private $pending_body        = '';
    21         private $pending_body_format = 'text';
     19        /** Server-side SMTP transcript lines for the email currently being sent (credentials never included). */
     20        private $smtp_transcript = array();
    2221
    2322        public function __construct( $db = null ) {
    … …  
    7675         */
    7776        public function capture_outgoing_email( $args ) {
     77                $this->smtp_transcript = array();
     78
    7879                if ( ! $this->db ) {
    7980                        return $args;
    … …  
    108109                $args['headers'] = $headers;
    109110
    110                 // Note: message body/headers are NOT persisted to the log table for successful/unconfirmed
    111                 // sends — only delivery metadata is stored (see readme.txt "What is tracked"). The body is
    112                 // held here in memory only; it's written to the DB by on_email_failed() below, and only
    113                 // then, only when the send actually fails, and only if the opt-in retention setting is on.
    114                 $this->pending_body        = isset( $args['message'] ) ? (string) $args['message'] : '';
    115                 $this->pending_body_format = ( false !== stripos( implode( ' ', $headers ), 'text/html' ) ) ? 'html' : 'text';
    116 
    117                 $this->pending_log_id = $this->db->log_email( array(
     111                $row = array(
    118112                        'subject'      => isset( $args['subject'] ) ? $args['subject'] : '',
    119113                        'from_address' => $from_address,
    … …  
    121115                        'status'       => 'pending',
    122116                        'message_id'   => $tracking_id,
    123                 ) );
     117                );
     118
     119                // Keep the message itself so the log can show the full email.
     120                $row = array_merge( $row, $this->email_content_for_log( $args, $headers ) );
     121
     122                $this->pending_log_id = $this->db->log_email( $row );
    124123
    125124                return $args;
    … …  
    132131         * @param \PHPMailer\PHPMailer\PHPMailer $phpmailer
    133132         */
     133        /** Most bytes of a message body kept per email, so one huge newsletter can't bloat the table. */
     134        const MAX_LOGGED_BODY = 1048576;
     135
     136        /**
     137         * Body, headers and attachment names to save with a log row.
     138         *
     139         * @param  array    $args    wp_mail() arguments.
     140         * @param  string[] $headers Normalized header lines, including our tracking header.
     141         * @return array
     142         */
     143        private function email_content_for_log( $args, $headers ) {
     144                $body = isset( $args['message'] ) ? (string) $args['message'] : '';
     145                if ( strlen( $body ) > self::MAX_LOGGED_BODY ) {
     146                        $body = function_exists( 'mb_strcut' ) ? mb_strcut( $body, 0, self::MAX_LOGGED_BODY, 'UTF-8' ) : substr( $body, 0, self::MAX_LOGGED_BODY );
     147                }
     148
     149                $kept         = array();
     150                $content_type = '';
     151                foreach ( $headers as $header ) {
     152                        if ( 0 === stripos( $header, 'X-BPTSM-ID:' ) ) {
     153                                continue;
     154                        }
     155                        $kept[] = $header;
     156                        if ( 0 === stripos( $header, 'Content-Type:' ) ) {
     157                                $content_type = $header;
     158                        }
     159                }
     160                if ( '' === $content_type ) {
     161                        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- reading a WordPress core filter.
     162                        $content_type = (string) apply_filters( 'wp_mail_content_type', 'text/plain' );
     163                }
     164                $is_html = false !== stripos( $content_type, 'text/html' ) || preg_match( '/<(html|body|table|div|p|br)\b/i', $body );
     165
     166                // Attachment file names only; the files themselves are never copied.
     167                $attachments = isset( $args['attachments'] ) ? $args['attachments'] : array();
     168                if ( is_string( $attachments ) ) {
     169                        $attachments = explode( "\n", str_replace( "\r\n", "\n", $attachments ) );
     170                }
     171                $names = array();
     172                foreach ( (array) $attachments as $name => $file ) {
     173                        if ( '' !== trim( (string) $file ) ) {
     174                                $names[] = is_string( $name ) ? $name : wp_basename( (string) $file );
     175                        }
     176                }
     177
     178                return array(
     179                        'body'         => $body,
     180                        'body_format'  => $is_html ? 'html' : 'text',
     181                        'mail_headers' => $kept ? wp_json_encode( $kept ) : null,
     182                        'attachments'  => $names ? wp_json_encode( $names ) : null,
     183                );
     184        }
     185
    134186        public function on_phpmailer_init( $phpmailer ) {
    135187                $smtp_enabled = (bool) get_option( 'bcs_smtp_enabled' );
    … …  
    137189                if ( $smtp_enabled ) {
    138190                        $this->configure_phpmailer( $phpmailer );
     191                        $this->attach_transcript_capture( $phpmailer );
    139192                }
    140193
    … …  
    165218        public function on_email_failed( $error ) {
    166219                if ( $this->db && $this->pending_log_id ) {
    167                         $msg = is_wp_error( $error ) ? $error->get_error_message() : 'Unknown error';
     220                        $msg = $this->describe_failure( is_wp_error( $error ) ? $error->get_error_message() : 'Unknown error' );
    168221                        $this->db->update_email_log_status( $this->pending_log_id, 'failed', $msg );
    169222
    170                         if ( get_option( BCS_SMTP_OPT_STORE_FAILED_BODY, false ) && '' !== $this->pending_body ) {
    171                                 $this->db->update_email_log_body( $this->pending_log_id, $this->pending_body, $this->pending_body_format );
    172                         }
    173 
    174                         $this->pending_log_id     = null;
    175                         $this->pending_body        = '';
    176                         $this->pending_body_format = 'text';
    177                 }
    178         }
    179 
    180         /* ─────────────────────────────────────────────────────────────────── */
    181         /* Resend a failed email                                                */
    182         /* ─────────────────────────────────────────────────────────────────── */
    183 
    184         /**
    185          * Resend a previously logged, failed email using its stored body.
    186          * Re-enters the normal wp_mail()/capture/log hook chain, so a fresh log row
    187          * is created for the resend attempt — the original row is untouched other
    188          * than its resend_count/last_resend_at audit fields.
    189          *
    190          * @param  int $log_id
    191          * @return array { success: bool, message: string }
    192          */
    193         public function resend_stored_email( $log_id ) {
    194                 if ( ! $this->db ) {
    195                         return array( 'success' => false, 'message' => __( 'Internal error — database unavailable.', 'bcs-smtp-manager' ) );
    196                 }
    197 
    198                 $row = $this->db->get_email_log_row( $log_id );
    199                 if ( ! $row ) {
    200                         return array( 'success' => false, 'message' => __( 'Log entry not found.', 'bcs-smtp-manager' ) );
    201                 }
    202                 if ( 'failed' !== $row['status'] ) {
    203                         return array( 'success' => false, 'message' => __( 'Only failed emails can be resent.', 'bcs-smtp-manager' ) );
    204                 }
    205                 if ( empty( $row['body'] ) ) {
    206                         return array( 'success' => false, 'message' => __( 'No stored content to resend — enable "Store failed email content" in the Config tab before the failure occurs.', 'bcs-smtp-manager' ) );
    207                 }
    208                 if ( empty( $row['to_address'] ) ) {
    209                         return array( 'success' => false, 'message' => __( 'Log entry has no recipient address.', 'bcs-smtp-manager' ) );
    210                 }
    211 
    212                 $headers = array();
    213                 if ( 'html' === $row['body_format'] ) {
    214                         $headers[] = 'Content-Type: text/html; charset=UTF-8';
    215                 }
    216 
    217                 $this->db->increment_resend_count( $log_id );
    218 
    219                 $sent = wp_mail( $row['to_address'], $row['subject'], $row['body'], $headers );
    220 
    221                 return array(
    222                         'success' => (bool) $sent,
    223                         'message' => $sent
    224                                 ? __( 'Resend attempted — check the Email Log for the new entry\'s delivery status.', 'bcs-smtp-manager' )
    225                                 : __( 'wp_mail() returned false when resending — check the new log entry for details.', 'bcs-smtp-manager' ),
    226                 );
     223                        $this->pending_log_id = null;
     224                }
     225        }
     226
     227        /* ─────────────────────────────────────────────────────────────────── */
     228        /* SMTP transcript capture (failure diagnostics)                       */
     229        /* ─────────────────────────────────────────────────────────────────── */
     230
     231        /** @var callable|null The Debugoutput closure we installed, so we can recognise our own. */
     232        private $transcript_cb = null;
     233
     234        /**
     235         * Capture PHPMailer's debug output so a failure can report the server's actual reply
     236         * (e.g. "535 5.7.139 Authentication unsuccessful, SmtpClientAuthentication is disabled")
     237         * instead of PHPMailer's generic "Could not authenticate.". Client lines are discarded, so
     238         * nothing the site sends — including credentials — is ever retained.
     239         *
     240         * @param \PHPMailer\PHPMailer\PHPMailer $phpmailer
     241         */
     242        private function attach_transcript_capture( $phpmailer ) {
     243                if ( null === $this->transcript_cb ) {
     244                        $this->transcript_cb = function ( $str ) {
     245                                $str = trim( (string) $str );
     246                                if ( '' === $str || 0 === strpos( $str, 'CLIENT -> SERVER' ) || count( $this->smtp_transcript ) >= 200 ) {
     247                                        return;
     248                                }
     249                                $this->smtp_transcript[] = $str;
     250                        };
     251                }
     252                // Respect a debug level another plugin or developer set deliberately.
     253                if ( 0 !== (int) $phpmailer->SMTPDebug && $phpmailer->Debugoutput !== $this->transcript_cb ) {
     254                        return;
     255                }
     256                $phpmailer->SMTPDebug   = 3; // SMTP::DEBUG_CONNECTION — includes connection-level errors.
     257                $phpmailer->Debugoutput = $this->transcript_cb;
     258        }
     259
     260        /**
     261         * Most relevant error line from the last SMTP conversation, or '' if none.
     262         *
     263         * @return string
     264         */
     265        public function get_last_smtp_error_reply() {
     266                $lines = array_reverse( $this->smtp_transcript );
     267                // Prefer the server's own 4xx/5xx reply — it carries the actionable reason.
     268                foreach ( $lines as $line ) {
     269                        if ( preg_match( '/^SERVER -> CLIENT:\s*([45]\d\d[\s-].*)$/', $line, $m ) ) {
     270                                return mb_substr( trim( $m[1] ), 0, 500 );
     271                        }
     272                }
     273                // Otherwise a connection-level error from PHPMailer's SMTP class (upper-case "SMTP ERROR:",
     274                // unlike PHPMailer's generic "SMTP Error: …" message that is already in the error text).
     275                foreach ( $lines as $line ) {
     276                        if ( 0 === strpos( $line, 'SMTP ERROR:' ) || false !== stripos( $line, 'Connection failed' ) ) {
     277                                return mb_substr( $line, 0, 500 );
     278                        }
     279                }
     280                return '';
     281        }
     282
     283        /**
     284         * Enrich a PHPMailer error message with the server's reply and, when relevant, the
     285         * unreadable-password explanation.
     286         *
     287         * @param  string $msg
     288         * @return string
     289         */
     290        public function describe_failure( $msg ) {
     291                $reply = $this->get_last_smtp_error_reply();
     292                if ( '' !== $reply && false === strpos( $msg, $reply ) ) {
     293                        $msg .= ' — Server reply: ' . $reply;
     294                }
     295                if ( get_option( 'bcs_smtp_enabled' ) && get_option( 'bcs_smtp_auth' ) && $this->is_password_unreadable() ) {
     296                        $msg .= ' ' . $this->unreadable_password_message();
     297                }
     298                return $msg;
    227299        }
    228300
    … …  
    232304
    233305        /**
    234          * Encrypt a plaintext password for storage in wp_options.
    235          * Keyed off this install's AUTH salt, so the ciphertext is unusable outside this site.
     306         * 32-byte key used to encrypt the SMTP password.
     307         *
     308         * Deliberately NOT derived from wp_salt(): security plugins rotate salts (AIOS "salt postfix"
     309         * changes them weekly; Solid Security / Sucuri regenerate them on demand), which silently made
     310         * the stored password undecryptable and every send fail with "Could not authenticate".
     311         * A BCS_SMTP_CRYPTO_KEY constant in wp-config.php takes precedence; otherwise a random key is
     312         * generated once and kept in its own non-autoloaded option.
     313         *
     314         * @return string Raw binary key.
     315         */
     316        private function get_crypto_key() {
     317                if ( defined( 'BCS_SMTP_CRYPTO_KEY' ) && '' !== (string) BCS_SMTP_CRYPTO_KEY ) {
     318                        return hash( 'sha256', (string) BCS_SMTP_CRYPTO_KEY, true );
     319                }
     320                $secret = get_option( BCS_SMTP_OPT_CRYPTO_KEY, '' );
     321                if ( '' === $secret ) {
     322                        add_option( BCS_SMTP_OPT_CRYPTO_KEY, wp_generate_password( 64, true, true ), '', false );
     323                        $secret = get_option( BCS_SMTP_OPT_CRYPTO_KEY, '' );
     324                }
     325                return hash( 'sha256', $secret, true );
     326        }
     327
     328        /**
     329         * Encrypt a plaintext password for storage in wp_options (AES-256-CBC + HMAC-SHA256).
    236330         *
    237331         * @param  string $plain
    238          * @return string Encrypted value (prefixed 'enc:'), or the plain value if openssl is unavailable.
     332         * @return string Encrypted value (prefixed 'enc2:'), or the plain value if openssl is unavailable.
    239333         */
    240334        public function encrypt_password( $plain ) {
    … …  
    242336                        return $plain;
    243337                }
    244                 $key    = hash( 'sha256', wp_salt( 'auth' ), true );
     338                $key    = $this->get_crypto_key();
    245339                $iv     = random_bytes( 16 );
    246340                $cipher = openssl_encrypt( $plain, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );
    … …  
    248342                        return $plain;
    249343                }
    250                 return 'enc:' . base64_encode( $iv . $cipher ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- ciphertext encoding, not obfuscation.
     344                $mac = hash_hmac( 'sha256', $iv . $cipher, $key, true );
     345                return 'enc2:' . base64_encode( $iv . $mac . $cipher ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- ciphertext encoding, not obfuscation.
    251346        }
    252347
    253348        /**
    254349         * Decrypt a password previously stored via encrypt_password().
    255          * Values without the 'enc:' prefix are treated as legacy plaintext (pre-encryption upgrade)
    256          * and returned as-is, so existing installs keep working until their next save.
     350         *
     351         * 'enc2:' — current format. 'enc:' — pre-1.2.2 format keyed off wp_salt('auth'); decrypted with
     352         * that legacy key and transparently re-encrypted to 'enc2:' so later salt changes can't break it.
     353         * Values with neither prefix are legacy plaintext and returned as-is.
    257354         *
    258355         * @param  string $stored
    259          * @return string
     356         * @return string Plaintext, or '' when the value can't be decrypted.
    260357         */
    261358        public function decrypt_password( $stored ) {
    262                 if ( '' === $stored || 0 !== strpos( $stored, 'enc:' ) ) {
     359                if ( '' === $stored ) {
     360                        return '';
     361                }
     362                if ( 0 === strpos( $stored, 'enc2:' ) ) {
     363                        if ( ! function_exists( 'openssl_decrypt' ) ) {
     364                                return '';
     365                        }
     366                        $raw = base64_decode( substr( $stored, 5 ), true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- ciphertext decoding, not obfuscation.
     367                        if ( false === $raw || strlen( $raw ) <= 48 ) {
     368                                return '';
     369                        }
     370                        $key    = $this->get_crypto_key();
     371                        $iv     = substr( $raw, 0, 16 );
     372                        $mac    = substr( $raw, 16, 32 );
     373                        $cipher = substr( $raw, 48 );
     374                        if ( ! hash_equals( hash_hmac( 'sha256', $iv . $cipher, $key, true ), $mac ) ) {
     375                                return '';
     376                        }
     377                        $plain = openssl_decrypt( $cipher, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );
     378                        return false === $plain ? '' : $plain;
     379                }
     380                if ( 0 !== strpos( $stored, 'enc:' ) ) {
    263381                        return $stored;
    264382                }
    … …  
    274392                $key    = hash( 'sha256', wp_salt( 'auth' ), true );
    275393                $plain  = openssl_decrypt( $cipher, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );
    276                 return false === $plain ? '' : $plain;
     394                if ( false === $plain ) {
     395                        return '';
     396                }
     397                // Migrate to the salt-independent format while the legacy key still works.
     398                if ( get_option( 'bcs_smtp_password', '' ) === $stored ) {
     399                        update_option( 'bcs_smtp_password', $this->encrypt_password( $plain ) );
     400                }
     401                return $plain;
    277402        }
    278403
    … …  
    280405        public function get_smtp_password() {
    281406                return $this->decrypt_password( get_option( 'bcs_smtp_password', '' ) );
     407        }
     408
     409        /**
     410         * True when a password is saved but can no longer be decrypted (e.g. it was saved under
     411         * security keys that have since changed, or the database was migrated to another site).
     412         */
     413        public function is_password_unreadable() {
     414                $stored = get_option( 'bcs_smtp_password', '' );
     415                return '' !== $stored && '' === $this->decrypt_password( $stored );
     416        }
     417
     418        /** User-facing explanation for is_password_unreadable(). */
     419        public function unreadable_password_message() {
     420                return __( 'The saved SMTP password can no longer be decrypted — this happens when WordPress security keys/salts change (e.g. a security plugin rotated them) or the database was moved to another site. Re-enter the password in SMTP Manager → Settings and save.', 'bcs-smtp-manager' );
    282421        }
    283422
    … …  
    307446                $phpmailer->Host = $host;
    308447                $phpmailer->Port = $port;
     448
     449                // PHPMailer's default 300s timeout outlives PHP's max_execution_time, so a firewalled port
     450                // kills the request with a fatal error instead of returning a readable failure.
     451                if ( 300 === (int) $phpmailer->Timeout ) {
     452                        $phpmailer->Timeout = 20;
     453                }
    309454
    310455                switch ( $encryption ) {
    … …  
    339484
    340485        /* ─────────────────────────────────────────────────────────────────── */
    341         /* SMTP Plugin Detection (used by Config tab)                           */
     486        /* SMTP Plugin Detection (used by the Settings tab)                     */
    342487        /* ─────────────────────────────────────────────────────────────────── */
    343488
    … …  
    503648                        return array_merge( $base, array(
    504649                                'success'      => false,
    505                                 'error_msg'    => 'No SMTP host configured. Enter your SMTP server details in the Settings tab and save first.',
     650                                'error_msg'    => __( 'No SMTP server is set up yet. Enter your server details on the Connection tab and save.', 'bcs-smtp-manager' ),
    506651                                'failure_type' => 'not_configured',
    507652                        ) );
    … …  
    513658                                return array_merge( $base, array(
    514659                                        'success'      => false,
    515                                         'error_msg'    => "PHP mail() is not available on this server. Configure SMTP in the Settings tab for reliable email delivery.",
     660                                        'error_msg'    => __( 'PHP mail() is not available on this server. Set up SMTP on the Connection tab for reliable email delivery.', 'bcs-smtp-manager' ),
    516661                                        'failure_type' => 'mail_unavailable',
    517662                                ) );
    … …  
    524669                }
    525670
     671                // Built-in SMTP: run the exact connect/STARTTLS/AUTH sequence a real send uses.
     672                if ( get_option( 'bcs_smtp_enabled' ) ) {
     673                        return array_merge( $base, $this->test_builtin_smtp() );
     674                }
     675
     676                // Another SMTP plugin is in charge — we don't hold its credentials, so only
     677                // reachability and the TLS handshake are checked below.
    526678                $encryption = strtolower( $info['encryption'] ?: 'tls' );
    527679                $timeout    = 8;
    … …  
    630782                }
    631783
    632                 // Test SMTP AUTH LOGIN when credentials are configured. Reachable for SSL, successfully-upgraded
    633                 // TLS, and also plain/'none' connections — matching exactly what a real send would attempt, so a
    634                 // bad username/password doesn't get silently reported as a healthy connection.
    635                 $auth     = (bool) get_option( 'bcs_smtp_auth', false );
    636                 $username = get_option( 'bcs_smtp_username', '' );
    637                 $password = $this->get_smtp_password();
    638 
    639                 if ( $auth && $username && $password ) {
    640                         fwrite( $socket, "AUTH LOGIN\r\n" );
    641                         $auth_resp = fgets( $socket, 1024 );
    642                         if ( '334' === substr( trim( $auth_resp ), 0, 3 ) ) {
    643                                 fwrite( $socket, base64_encode( $username ) . "\r\n" );
    644                                 $user_resp = fgets( $socket, 1024 );
    645                                 if ( '334' === substr( trim( $user_resp ), 0, 3 ) ) {
    646                                         fwrite( $socket, base64_encode( $password ) . "\r\n" );
    647                                         $pass_resp = fgets( $socket, 1024 );
    648                                         fwrite( $socket, "QUIT\r\n" );
    649                                         fclose( $socket );
    650                                         if ( '235' !== substr( trim( $pass_resp ), 0, 3 ) ) {
    651                                                 return array_merge( $base, array(
    652                                                         'success'      => false,
    653                                                         'error_msg'    => 'Authentication failed: ' . trim( $pass_resp ) . ' — Verify your username and password (or app password).',
    654                                                         'failure_type' => 'auth_failure',
    655                                                 ) );
    656                                         }
    657                                         return array_merge( $base, array(
    658                                                 'success'      => true,
    659                                                 'error_msg'    => null,
    660                                                 'failure_type' => null,
    661                                         ) );
    662                                 }
    663                         }
    664                         // AUTH LOGIN not accepted — server may use a different mechanism.
    665                         fwrite( $socket, "QUIT\r\n" );
    666                         fclose( $socket );
    667                         return array_merge( $base, array(
    668                                 'success'      => true,
    669                                 'error_msg'    => null,
    670                                 'failure_type' => null,
    671                                 'auth_note'    => 'Connection OK — AUTH LOGIN not supported by this server; credentials will be verified on send.',
    672                         ) );
    673                 }
    674 
    675784                fwrite( $socket, "QUIT\r\n" );
    676785                fclose( $socket );
    … …  
    684793        }
    685794
     795        /**
     796         * Connection + authentication test for the built-in SMTP config, using PHPMailer itself so
     797         * every AUTH mechanism, TLS option and phpmailer_init customisation matches a real send.
     798         * Nothing is sent — the session ends right after authentication.
     799         *
     800         * @return array {success, error_msg, failure_type}
     801         */
     802        private function test_builtin_smtp() {
     803                $fail = function ( $msg, $type ) {
     804                        return array( 'success' => false, 'error_msg' => $msg, 'failure_type' => $type );
     805                };
     806
     807                if ( get_option( 'bcs_smtp_auth', false ) ) {
     808                        if ( $this->is_password_unreadable() ) {
     809                                return $fail( $this->unreadable_password_message(), 'auth_failure' );
     810                        }
     811                        if ( '' === (string) get_option( 'bcs_smtp_username', '' ) || '' === $this->get_smtp_password() ) {
     812                                return $fail( __( 'Sign-in is turned on but the username or password is empty. Enter both on the Connection tab and save.', 'bcs-smtp-manager' ), 'auth_failure' );
     813                        }
     814                }
     815
     816                $phpmailer = $this->create_phpmailer();
     817                if ( ! $phpmailer ) {
     818                        return $fail( __( 'PHPMailer is not available — WordPress 5.5 or newer is required for the connection test.', 'bcs-smtp-manager' ), 'plugin_failure' );
     819                }
     820
     821                // Apply phpmailer_init exactly as wp_mail() does, so a conflicting plugin shows up here too.
     822                // Our own handler runs as part of it; park the pending log row so it isn't touched.
     823                $pending              = $this->pending_log_id;
     824                $this->pending_log_id = null;
     825                do_action_ref_array( 'phpmailer_init', array( &$phpmailer ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- firing the core hook, as wp_mail() does.
     826                $this->pending_log_id = $pending;
     827
     828                if ( 'smtp' !== $phpmailer->Mailer ) {
     829                        return $fail( __( 'Another plugin switched PHPMailer away from SMTP during phpmailer_init, so these SMTP settings are not being used for delivery.', 'bcs-smtp-manager' ), 'plugin_failure' );
     830                }
     831
     832                $this->smtp_transcript = array();
     833                $this->attach_transcript_capture( $phpmailer );
     834                $phpmailer->Timeout = min( (int) $phpmailer->Timeout, 15 );
     835
     836                $error = '';
     837                try {
     838                        $ok = $phpmailer->smtpConnect();
     839                } catch ( \Exception $e ) {
     840                        $ok    = false;
     841                        $error = $e->getMessage();
     842                }
     843
     844                // Connecting (and signing in, when enabled) is not proof that mail can be sent:
     845                // e.g. Microsoft 365 accepts the connection without AUTH but rejects every message
     846                // with "530 5.7.57 Client not authenticated". Walk the envelope a real send uses —
     847                // MAIL FROM and RCPT TO — then RSET before DATA, so nothing is delivered.
     848                if ( $ok ) {
     849                        $envelope_error = $this->test_envelope( $phpmailer );
     850                        if ( '' !== $envelope_error ) {
     851                                $ok    = false;
     852                                $error = $envelope_error;
     853                        }
     854                }
     855                $phpmailer->smtpClose();
     856
     857                if ( $ok ) {
     858                        return array( 'success' => true, 'error_msg' => null, 'failure_type' => null );
     859                }
     860
     861                $msg = $this->describe_failure( $error ?: ( $phpmailer->ErrorInfo ?: __( 'Could not connect to the SMTP server.', 'bcs-smtp-manager' ) ) );
     862                return $fail( $msg, $this->classify_failure( $msg ) );
     863        }
     864
     865        /**
     866         * Ask the server to accept a sender and a recipient on an open connection, then reset
     867         * the transaction before any message data is sent.
     868         *
     869         * @param \PHPMailer\PHPMailer\PHPMailer $phpmailer Connected (and authenticated, if enabled) mailer.
     870         * @return string Empty when the server accepts the envelope, otherwise an error with the server's reply.
     871         */
     872        private function test_envelope( $phpmailer ) {
     873                $smtp = $phpmailer->getSMTPInstance();
     874                if ( ! $smtp || ! $smtp->connected() ) {
     875                        return '';
     876                }
     877
     878                $from = $phpmailer->From;
     879                if ( ! is_email( $from ) ) {
     880                        $from = get_option( 'bcs_from_email', '' );
     881                }
     882                if ( ! is_email( $from ) ) {
     883                        $from = get_option( 'admin_email' );
     884                }
     885                $to = get_option( 'admin_email' );
     886
     887                $no_auth = ! get_option( 'bcs_smtp_auth', false );
     888                $prefix  = $no_auth
     889                        ? __( 'The server refused to send mail without signing in. Turn on "My server needs a username and password" and enter your credentials.', 'bcs-smtp-manager' ) . ' '
     890                        : '';
     891
     892                if ( ! $smtp->mail( $from ) ) {
     893                        $reply = trim( $smtp->getLastReply() );
     894                        $smtp->reset();
     895                        /* translators: 1: sender email address, 2: the SMTP server's reply. */
     896                        return $prefix . sprintf( __( 'The server would not accept mail from %1$s. Server reply: %2$s', 'bcs-smtp-manager' ), $from, $reply ? $reply : __( 'no response', 'bcs-smtp-manager' ) );
     897                }
     898                if ( is_email( $to ) && ! $smtp->recipient( $to ) ) {
     899                        $reply = trim( $smtp->getLastReply() );
     900                        $smtp->reset();
     901                        /* translators: 1: recipient email address, 2: the SMTP server's reply. */
     902                        return $prefix . sprintf( __( 'The server would not deliver to %1$s. Server reply: %2$s', 'bcs-smtp-manager' ), $to, $reply ? $reply : __( 'no response', 'bcs-smtp-manager' ) );
     903                }
     904                $smtp->reset();
     905                return '';
     906        }
     907
     908        /** @return \PHPMailer\PHPMailer\PHPMailer|null A fresh, exception-throwing PHPMailer instance. */
     909        private function create_phpmailer() {
     910                if ( ! class_exists( '\PHPMailer\PHPMailer\PHPMailer' ) && file_exists( ABSPATH . WPINC . '/PHPMailer/PHPMailer.php' ) ) {
     911                        require_once ABSPATH . WPINC . '/PHPMailer/PHPMailer.php';
     912                        require_once ABSPATH . WPINC . '/PHPMailer/SMTP.php';
     913                        require_once ABSPATH . WPINC . '/PHPMailer/Exception.php';
     914                }
     915                return class_exists( '\PHPMailer\PHPMailer\PHPMailer' ) ? new \PHPMailer\PHPMailer\PHPMailer( true ) : null;
     916        }
     917
    686918        /* ─────────────────────────────────────────────────────────────────── */
    687919        /* Manual Test Email Send                                               */
    … …  
    711943
    712944                $headers = array( 'Content-Type: text/html; charset=UTF-8' );
     945
     946                // Fail fast with the real reason instead of PHPMailer's generic "Could not authenticate."
     947                if ( get_option( 'bcs_smtp_enabled' ) && get_option( 'bcs_smtp_auth' ) && $this->is_password_unreadable() ) {
     948                        return array(
     949                                'success'      => false,
     950                                'error_msg'    => $this->unreadable_password_message(),
     951                                'failure_type' => 'auth_failure',
     952                                'smtp_host'    => $info['host'],
     953                                'smtp_port'    => $info['port'],
     954                                'mailbox'      => $info['mailbox'],
     955                                'source_ip'    => $this->get_source_ip(),
     956                        );
     957                }
    713958
    714959                $captured_error = '';
    … …  
    745990                }
    746991
     992                $error_msg = $captured_error ? $this->describe_failure( $captured_error ) : 'wp_mail() returned false — no additional error details.';
    747993                return array(
    748994                        'success'      => false,
    749                         'error_msg'    => $captured_error ?: 'wp_mail() returned false — no additional error details.',
    750                         'failure_type' => $this->classify_failure( $captured_error ),
     995                        'error_msg'    => $error_msg,
     996                        'failure_type' => $this->classify_failure( $error_msg ),
    751997                        'smtp_host'    => $info['host'],
    752998                        'smtp_port'    => $info['port'],
    … …  
    7971043
    7981044        private function build_test_email_html( $site_name, $site_url, $timestamp, $info, $to ) {
    799                 return BCS_SMTP_Email_Template::wrapper_open( 'SMTP Test', 580, 14, '.08' ) . '
    800   <!-- Header — bgcolor attr = Outlook fallback; background-color = Yahoo/other fallback; background-image = gradient for supporting clients -->
    801   <tr><td bgcolor="#10b981" style="background-color:#10b981;background-image:linear-gradient(135deg,#10b981 0%,#059669 100%);padding:36px 40px;text-align:center">
    802     <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center"><![endif]-->
    803     <div style="width:56px;height:56px;background:#34d399;border-radius:50%;margin:0 auto 16px">
    804       <span style="font-size:28px;line-height:56px;display:block;text-align:center">✓</span>
    805     </div>
    806     <!--[if mso]></td></tr></table><![endif]-->
    807     <h1 style="margin:0;font-size:24px;font-weight:700;color:#ffffff;letter-spacing:-0.3px">Email Delivery Working</h1>
    808     <p style="margin:8px 0 0;font-size:14px;color:#d1fae5">Your SMTP configuration is set up correctly</p>
    809   </td></tr>
    810 
    811   <!-- Body -->
    812   <tr><td style="padding:36px 40px">
    813     <p style="margin:0 0 24px;font-size:15px;color:#374151;line-height:1.6">
    814       This is a test email from <strong>' . esc_html( $site_name ) . '</strong> to confirm that email delivery is working as expected through ByteCoreStack SMTP.
    815     </p>
    816 
    817     <!-- Info grid -->
    818     <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden;margin-bottom:24px">
    819       <tr style="border-bottom:1px solid #e2e8f0">
    820         <td style="padding:13px 18px;font-size:12px;color:#6b7280;font-weight:600;text-transform:uppercase;letter-spacing:.5px;width:38%;background:#f1f5f9">Recipient</td>
    821         <td style="padding:13px 18px;font-size:13px;color:#1e293b;font-family:monospace">' . esc_html( $to ) . '</td>
    822       </tr>
    823       <tr style="border-bottom:1px solid #e2e8f0">
    824         <td style="padding:13px 18px;font-size:12px;color:#6b7280;font-weight:600;text-transform:uppercase;letter-spacing:.5px;background:#f1f5f9">SMTP Host</td>
    825         <td style="padding:13px 18px;font-size:13px;color:#1e293b;font-family:monospace">' . esc_html( $info['host'] ?: 'PHP mail()' ) . ( $info['port'] ? ':' . esc_html( $info['port'] ) : '' ) . '</td>
    826       </tr>
    827       <tr style="border-bottom:1px solid #e2e8f0">
    828         <td style="padding:13px 18px;font-size:12px;color:#6b7280;font-weight:600;text-transform:uppercase;letter-spacing:.5px;background:#f1f5f9">Plugin</td>
    829         <td style="padding:13px 18px;font-size:13px;color:#1e293b">' . esc_html( $info['plugin'] ) . '</td>
    830       </tr>
    831       <tr>
    832         <td style="padding:13px 18px;font-size:12px;color:#6b7280;font-weight:600;text-transform:uppercase;letter-spacing:.5px;background:#f1f5f9">Sent At</td>
    833         <td style="padding:13px 18px;font-size:13px;color:#1e293b">' . esc_html( $timestamp ) . '</td>
    834       </tr>
    835     </table>
    836 
    837     <div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:14px 18px;margin-bottom:0">
    838       <p style="margin:0;font-size:13px;color:#15803d;line-height:1.5">
    839         <strong>All good!</strong> If you received this email, your WordPress site can send emails successfully. Check the Email Log tab in ByteCoreStack SMTP for delivery history.
    840       </p>
    841     </div>
    842   </td></tr>
    843 
    844   <!-- Footer -->
    845   <tr><td style="padding:20px 40px 28px;border-top:1px solid #f1f5f9;text-align:center">
    846     ' . BCS_SMTP_Email_Template::brand_footer_line( $site_url, $site_name ) . '
    847   </td></tr>
    848 ' . BCS_SMTP_Email_Template::wrapper_close();
     1045                $T      = 'BCS_SMTP_Email_Template';
     1046                $server = $info['host'] ? $info['host'] . ( $info['port'] ? ':' . $info['port'] : '' ) : __( 'PHP mail() — no SMTP server', 'bcs-smtp-manager' );
     1047
     1048                $body = '
     1049      <tr><td class="bcs-pad" style="padding:36px 40px 0">
     1050        ' . $T::pill( __( 'Delivered', 'bcs-smtp-manager' ), 'ok' ) . '
     1051        <h1 class="bcs-ink bcs-h1" style="margin:18px 0 10px;font-size:28px;line-height:1.2;font-weight:700;letter-spacing:-.4px;color:#0f172a">' . esc_html__( 'Your site can send email', 'bcs-smtp-manager' ) . '</h1>
     1052        <p class="bcs-ink2" style="margin:0;font-size:15px;line-height:1.65;color:#475569">' .
     1053                        /* translators: %s: site name. */
     1054                        esc_html( sprintf( __( '%s sent this message through your mail server. Contact forms, password resets and every other WordPress email go out the same way.', 'bcs-smtp-manager' ), $site_name ) ) . '</p>
     1055      </td></tr>
     1056      <tr><td class="bcs-pad" style="padding:26px 40px 0">
     1057        ' . $T::details( array(
     1058                        array( __( 'Sent to', 'bcs-smtp-manager' ), $to, true ),
     1059                        array( __( 'Mail server', 'bcs-smtp-manager' ), $server, true ),
     1060                        array( __( 'Sent by', 'bcs-smtp-manager' ), $info['plugin'] ),
     1061                        array( __( 'Time', 'bcs-smtp-manager' ), $timestamp ),
     1062                ) ) . '
     1063      </td></tr>
     1064      <tr><td class="bcs-pad" style="padding:28px 40px 36px">
     1065        ' . $T::button( $T::settings_url( 'email-log' ), __( 'Open the email log', 'bcs-smtp-manager' ) ) . '
     1066      </td></tr>';
     1067
     1068                return $T::document( array(
     1069                        'title'     => __( 'Test email', 'bcs-smtp-manager' ),
     1070                        /* translators: %s: site name. */
     1071                        'preheader' => sprintf( __( 'Good news: %s can send email. This test arrived through your mail server.', 'bcs-smtp-manager' ), $site_name ),
     1072                        'label'     => __( 'Test email', 'bcs-smtp-manager' ),
     1073                        'body'      => $body,
     1074                        'footer'    => $T::footer( $T::settings_url( 'connection' ), __( 'Connection settings', 'bcs-smtp-manager' ) ),
     1075                ) );
    8491076        }
    8501077
    … …  
    8621089                }
    8631090                // Loopback or missing SERVER_ADDR — try external lookup (e.g. local dev / Docker).
     1091                // Cached for a day so health checks every 15 minutes don't each make a request.
     1092                $cached = get_transient( 'bcs_smtp_source_ip' );
     1093                if ( false !== $cached ) {
     1094                        return $cached;
     1095                }
     1096                $ip       = 'unknown';
    8641097                $response = wp_remote_get( 'https://api.ipify.org', array( 'timeout' => 5 ) );
    8651098                if ( ! is_wp_error( $response ) ) {
    866                         $ip = trim( wp_remote_retrieve_body( $response ) );
    867                         if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) { return $ip; }
    868                 }
    869                 return 'unknown';
     1099                        $body = trim( wp_remote_retrieve_body( $response ) );
     1100                        if ( filter_var( $body, FILTER_VALIDATE_IP ) ) { $ip = $body; }
     1101                }
     1102                set_transient( 'bcs_smtp_source_ip', $ip, DAY_IN_SECONDS );
     1103                return $ip;
    8701104        }
    8711105}
  • bcs-smtp-manager/trunk/includes/class-bcs-smtp-monitor.php

    r3579822 r3762759  
    4444
    4545        private function get_interval_minutes() {
    46                 $mins = (int) get_option( BCS_SMTP_OPT_CHECK_INTERVAL, 5 );
    47                 return max( 1, min( 1440, $mins ) );
     46                return (int) BCS_SMTP_CHECK_INTERVAL_MINUTES;
     47        }
     48
     49        /**
     50         * Health checks run whenever the plugin is sending through a configured SMTP server.
     51         *
     52         * @return bool
     53         */
     54        public static function is_active() {
     55                return (bool) get_option( 'bcs_smtp_enabled', false ) && '' !== trim( (string) get_option( 'bcs_smtp_host', '' ) );
     56        }
     57
     58        /**
     59         * Channels that receive failure alerts. Older versions stored a single channel as a string.
     60         *
     61         * @return string[]
     62         */
     63        public static function get_channels() {
     64                $saved = get_option( BCS_SMTP_OPT_ALERT_CHANNEL, array() );
     65                $saved = is_array( $saved ) ? $saved : array( $saved );
     66                return array_values( array_intersect( array_keys( self::channel_labels() ), $saved ) );
     67        }
     68
     69        /**
     70         * @return array Channel key => display name.
     71         */
     72        public static function channel_labels() {
     73                return array(
     74                        'slack'       => __( 'Slack', 'bcs-smtp-manager' ),
     75                        'teams'       => __( 'Microsoft Teams', 'bcs-smtp-manager' ),
     76                        'google_chat' => __( 'Google Chat', 'bcs-smtp-manager' ),
     77                        'discord'     => __( 'Discord', 'bcs-smtp-manager' ),
     78                        'custom'      => __( 'Custom webhook', 'bcs-smtp-manager' ),
     79                );
     80        }
     81
     82        /**
     83         * Human list of the given channels, e.g. "Slack and Discord".
     84         *
     85         * @param  string[] $channels
     86         * @return string
     87         */
     88        public static function channel_list_label( $channels ) {
     89                $labels = self::channel_labels();
     90                $names  = array();
     91                foreach ( $channels as $ch ) {
     92                        if ( isset( $labels[ $ch ] ) ) {
     93                                $names[] = $labels[ $ch ];
     94                        }
     95                }
     96                return wp_sprintf_l( '%l', $names );
     97        }
     98
     99        /**
     100         * Book the next scheduled check one interval after a check that just ran. Every place that
     101         * records a check calls this (cron runs late, and manual checks happen any time), so
     102         * "last check" and "next check" always add up to the interval.
     103         */
     104        public function schedule_next_check() {
     105                wp_clear_scheduled_hook( 'bcs_smtp_health_check' );
     106                if ( self::is_active() ) {
     107                        wp_schedule_event( time() + $this->get_interval_minutes() * 60, 'bcs_smtp_monitor', 'bcs_smtp_health_check' );
     108                }
    48109        }
    49110
    50111        public function maybe_schedule_event() {
    51                 $enabled = get_option( BCS_SMTP_OPT_MONITOR_ENABLED, false );
     112                $enabled = self::is_active();
    52113                $next    = wp_next_scheduled( 'bcs_smtp_health_check' );
    53114
    … …  
    64125        /* ─────────────────────────────────────────────────────────────────── */
    65126
    66         public function run_health_check() {
    67                 if ( ! get_option( BCS_SMTP_OPT_MONITOR_ENABLED, false ) ) {
    68                         return;
     127        /**
     128         * @param  bool $force True for a manual "Run Health Check Now" — runs even when scheduled
     129         *                     monitoring is off or a cron check holds the lock.
     130         * @return array|null  The connection-test result, or null when the check was skipped.
     131         */
     132        public function run_health_check( $force = false ) {
     133                if ( ! $force && ! self::is_active() ) {
     134                        return null;
    69135                }
    70136
    71137                // Prevent concurrent runs (WP-Cron + manual AJAX can overlap).
    72                 if ( get_transient( 'bcs_smtp_check_lock' ) ) {
    73                         return;
     138                if ( ! $force && get_transient( 'bcs_smtp_check_lock' ) ) {
     139                        return null;
    74140                }
    75141                $mins = $this->get_interval_minutes();
    … …  
    81147                update_option( BCS_SMTP_OPT_LAST_CHECK, time() );
    82148                update_option( BCS_SMTP_OPT_LAST_CHECK_SUCCESS, $result['success'] );
     149                $this->schedule_next_check();
    83150
    84151                if ( $result['success'] ) {
    … …  
    89156
    90157                delete_transient( 'bcs_smtp_check_lock' );
     158                return $result;
    91159        }
    92160
    … …  
    104172                update_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, $failures );
    105173
    106                 $threshold = (int) get_option( BCS_SMTP_OPT_FAILURE_THRESHOLD, 2 );
     174                $threshold = (int) BCS_SMTP_FAILURE_THRESHOLD;
    107175
    108176                if ( $failures >= $threshold ) {
    … …  
    149217        /* ─────────────────────────────────────────────────────────────────── */
    150218
    151         public function send_alert( $result, $failures, $external_down = false ) {
     219        /**
     220         * Dispatch a failure alert to every selected channel.
     221         *
     222         * @param  array $result
     223         * @param  int   $failures
     224         * @param  bool  $external_down
     225         * @param  bool  $is_test True for "Send test alert": ignores the one-per-outage limit and doesn't count toward it.
     226         * @return array|\WP_Error Channel key => true or WP_Error, or a WP_Error when nothing was attempted.
     227         */
     228        public function send_alert( $result, $failures, $external_down = false, $is_test = false ) {
    152229                if ( ! get_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED, false ) ) {
    153                         return;
    154                 }
    155 
    156                 $channel = get_option( BCS_SMTP_OPT_ALERT_CHANNEL, 'none' );
    157                 if ( 'none' === $channel ) {
    158                         return;
    159                 }
    160 
    161                 // Respect the max notification cap (0 = unlimited).
    162                 $max_count  = (int) get_option( BCS_SMTP_OPT_MAX_ALERT_COUNT, 0 );
     230                        return new \WP_Error( 'bcs_alert_disabled', __( 'Failure alerts are turned off.', 'bcs-smtp-manager' ) );
     231                }
     232
     233                $channels = self::get_channels();
     234                if ( ! $channels ) {
     235                        return new \WP_Error( 'bcs_alert_no_channel', __( 'No alert channel selected.', 'bcs-smtp-manager' ) );
     236                }
     237
     238                // One alert per outage; the counter resets when a check passes again.
     239                $max_count  = (int) BCS_SMTP_MAX_ALERTS_PER_OUTAGE;
    163240                $sent_count = (int) get_option( BCS_SMTP_OPT_ALERT_SENT_COUNT, 0 );
    164                 if ( $max_count > 0 && $sent_count >= $max_count ) {
    165                         return;
    166                 }
    167 
    168                 $payload = $this->build_alert_payload( $result, $failures, $external_down );
    169 
    170                 switch ( $channel ) {
    171                         case 'slack':
    172                                 $this->send_slack( $payload );
    173                                 break;
    174                         case 'teams':
    175                                 $this->send_teams( $payload );
    176                                 break;
    177                         case 'google_chat':
    178                                 $this->send_google_chat( $payload );
    179                                 break;
    180                         case 'discord':
    181                                 $this->send_discord( $payload );
    182                                 break;
    183                         case 'custom':
    184                                 $this->send_custom_webhook( $payload );
    185                                 break;
    186                 }
    187 
    188                 update_option( BCS_SMTP_OPT_ALERT_SENT_COUNT, $sent_count + 1 );
     241                if ( ! $is_test && $max_count > 0 && $sent_count >= $max_count ) {
     242                        return new \WP_Error( 'bcs_alert_capped', __( 'Notification limit reached for this failure cycle.', 'bcs-smtp-manager' ) );
     243                }
     244
     245                $payload = $this->build_alert_payload( $result, $failures, $external_down, $is_test );
     246                $senders = array(
     247                        'slack'       => 'send_slack',
     248                        'teams'       => 'send_teams',
     249                        'google_chat' => 'send_google_chat',
     250                        'discord'     => 'send_discord',
     251                        'custom'      => 'send_custom_webhook',
     252                );
     253
     254                $results   = array();
     255                $delivered = false;
     256                foreach ( $channels as $channel ) {
     257                        $results[ $channel ] = $this->{ $senders[ $channel ] }( $payload );
     258                        $delivered           = $delivered || true === $results[ $channel ];
     259                }
     260
     261                // Only a delivered alert counts toward the limit, so a webhook outage doesn't use it up.
     262                if ( $delivered && ! $is_test ) {
     263                        update_option( BCS_SMTP_OPT_ALERT_SENT_COUNT, $sent_count + 1 );
     264                }
     265                return $results;
     266        }
     267
     268        /**
     269         * POST a JSON body to a webhook and turn the response into true or a descriptive WP_Error.
     270         *
     271         * @param  string $webhook
     272         * @param  array  $body
     273         * @return true|\WP_Error
     274         */
     275        private function post_webhook( $webhook, $body ) {
     276                $response = wp_safe_remote_post( $webhook, array(
     277                        'body'    => wp_json_encode( $body ),
     278                        'headers' => array( 'Content-Type' => 'application/json' ),
     279                        'timeout' => 10,
     280                ) );
     281                if ( is_wp_error( $response ) ) {
     282                        /* translators: %s: transport error message. */
     283                        return new \WP_Error( 'bcs_alert_transport', sprintf( __( 'Webhook request failed: %s', 'bcs-smtp-manager' ), $response->get_error_message() ) );
     284                }
     285                $code = (int) wp_remote_retrieve_response_code( $response );
     286                if ( $code < 200 || $code >= 300 ) {
     287                        $detail = trim( wp_strip_all_tags( (string) wp_remote_retrieve_body( $response ) ) );
     288                        /* translators: 1: HTTP status code, 2: response body excerpt. */
     289                        return new \WP_Error( 'bcs_alert_http', sprintf( __( 'Webhook returned HTTP %1$d. %2$s', 'bcs-smtp-manager' ), $code, mb_substr( $detail, 0, 200 ) ) );
     290                }
     291                return true;
     292        }
     293
     294        /** @return \WP_Error For a channel whose webhook URL is missing or not on the provider's domain. */
     295        private function missing_webhook_error( $label ) {
     296                /* translators: %s: channel name, e.g. Slack. */
     297                return new \WP_Error( 'bcs_alert_no_url', sprintf( __( 'No valid %s webhook URL is saved — enter it in the Alerts tab and click Save Alert Settings.', 'bcs-smtp-manager' ), $label ) );
    189298        }
    190299
    … …  
    213322                if ( 'teams' === $channel ) {
    214323                        // Microsoft has used several domains for Incoming Webhooks / Workflows over time.
    215                         foreach ( array( 'webhook.office.com', 'office.com', 'logic.azure.com' ) as $suffix ) {
     324                        foreach ( array( 'webhook.office.com', 'office.com', 'logic.azure.com', 'api.powerplatform.com' ) as $suffix ) {
    216325                                if ( $host === $suffix || ( strlen( $host ) > strlen( $suffix ) + 1 && '.' . $suffix === substr( $host, -( strlen( $suffix ) + 1 ) ) ) ) {
    217326                                        return true;
    … …  
    250359        /* ─────────────────────────────────────────────────────────────────── */
    251360
    252         public function build_alert_payload( $result, $failures, $external_down = false ) {
     361        /**
     362         * Everything an alert needs, in one place. The chat channels all show the same message
     363         * (title, explanation, fields, link); the custom webhook keeps its machine-readable keys.
     364         *
     365         * @param  array $result        Connection-test result.
     366         * @param  int   $failures      Consecutive failed checks.
     367         * @param  bool  $external_down True when the provider's status page reports a problem.
     368         * @param  bool  $is_test       True for "Send test alert".
     369         * @return array
     370         */
     371        public function build_alert_payload( $result, $failures, $external_down = false, $is_test = false ) {
    253372                $info       = $this->mailer->detect_smtp_plugin();
    254373                $site_name  = get_bloginfo( 'name' );
    255374                $site_url   = get_bloginfo( 'url' );
    256                 $timestamp  = current_time( 'M j, Y \a\t g:i A' );
    257                 $fail_label = $this->mailer->label_failure_type( isset( $result['failure_type'] ) ? $result['failure_type'] : '' );
    258                 $ext_note   = $external_down ? ' (External SMTP service may be down — check provider status.)' : '';
     375                $type       = isset( $result['failure_type'] ) ? $result['failure_type'] : '';
     376                $mailbox    = isset( $result['mailbox'] ) ? $result['mailbox'] : $info['mailbox'];
     377                $smtp_host  = isset( $result['smtp_host'] ) ? $result['smtp_host'] : $info['host'];
     378                $smtp_port  = isset( $result['smtp_port'] ) ? $result['smtp_port'] : $info['port'];
     379                $error_msg  = isset( $result['error_msg'] ) ? $result['error_msg'] : __( 'No details available.', 'bcs-smtp-manager' );
     380                $when       = wp_date( get_option( 'date_format', 'M j, Y' ) . ' ' . get_option( 'time_format', 'g:i a' ) );
     381                $server     = $smtp_host ? $smtp_host . ( $smtp_port ? ':' . $smtp_port : '' ) : __( 'Not set', 'bcs-smtp-manager' );
     382
     383                if ( $is_test ) {
     384                        /* translators: %s: site name. */
     385                        $title   = sprintf( __( 'Test alert from %s', 'bcs-smtp-manager' ), $site_name );
     386                        $summary = __( 'Your email alerts are working. If email stops sending on this site, an alert like this one will arrive here. No action needed.', 'bcs-smtp-manager' );
     387                        $fields  = array(
     388                                __( 'Mail server', 'bcs-smtp-manager' ) => $server,
     389                                __( 'Sending as', 'bcs-smtp-manager' )  => $mailbox,
     390                                __( 'Sent', 'bcs-smtp-manager' )        => $when,
     391                        );
     392                } else {
     393                        /* translators: %s: site name. */
     394                        $title   = sprintf( __( 'Email is failing on %s', 'bcs-smtp-manager' ), $site_name );
     395                        $summary = __( 'WordPress can’t send email through your mail server, so emails from your site may not arrive. Check the error below.', 'bcs-smtp-manager' );
     396                        $fields  = array(
     397                                __( 'Problem', 'bcs-smtp-manager' )     => $this->problem_label( $type ),
     398                                __( 'Error', 'bcs-smtp-manager' )       => $error_msg,
     399                                __( 'Mail server', 'bcs-smtp-manager' ) => $server,
     400                                __( 'Sending as', 'bcs-smtp-manager' )  => $mailbox,
     401                                __( 'Detected', 'bcs-smtp-manager' )    => $when,
     402                        );
     403                }
    259404
    260405                return array(
    261                         'site_name'     => $site_name,
    262                         'site_url'      => $site_url,
    263                         'timestamp'     => $timestamp,
    264                         'mailbox'       => isset( $result['mailbox'] )   ? $result['mailbox']   : $info['mailbox'],
    265                         'smtp_host'     => isset( $result['smtp_host'] ) ? $result['smtp_host'] : $info['host'],
    266                         'smtp_port'     => isset( $result['smtp_port'] ) ? $result['smtp_port'] : $info['port'],
    267                         'plugin'        => $info['plugin'],
    268                         'error_msg'     => isset( $result['error_msg'] ) ? $result['error_msg'] : 'No details available.',
    269                         'failure_type'  => $fail_label,
    270                         'source_ip'     => isset( $result['source_ip'] ) ? $result['source_ip'] : '—',
    271                         'failures'      => $failures,
    272                         'external_down' => $external_down,
    273                         'ext_note'      => $ext_note,
    274                 );
     406                        // Shared message for the chat channels.
     407                        'is_test'        => $is_test,
     408                        'title'          => $title,
     409                        'summary'        => $summary,
     410                        'fields'         => $fields,
     411                        'note'           => $external_down ? __( 'Your email provider’s status page also reports a problem, so this may be an outage on their side.', 'bcs-smtp-manager' ) : '',
     412                        'footer'         => sprintf( '%s · %s', $site_name, preg_replace( '#^https?://#', '', $site_url ) ),
     413                        // Machine-readable values for the custom webhook (keys unchanged since 1.1).
     414                        'site_name'      => $site_name,
     415                        'site_url'       => $site_url,
     416                        'timestamp'      => current_time( 'M j, Y \a\t g:i A' ),
     417                        'mailbox'        => $mailbox,
     418                        'smtp_host'      => $smtp_host,
     419                        'smtp_port'      => $smtp_port,
     420                        'plugin'         => $info['plugin'],
     421                        'error_msg'      => $error_msg,
     422                        'failure_type'   => $this->mailer->label_failure_type( $type ),
     423                        'source_ip'      => isset( $result['source_ip'] ) ? $result['source_ip'] : '',
     424                        'failures'       => $failures,
     425                        'external_down'  => $external_down,
     426                );
     427        }
     428
     429        /**
     430         * What went wrong, in plain words, for the Problem line of an alert.
     431         *
     432         * @param  string $type Failure type from the connection test.
     433         * @return string
     434         */
     435        private function problem_label( $type ) {
     436                $labels = array(
     437                        'auth_failure'     => __( 'Sign-in failed: the username or password wasn’t accepted', 'bcs-smtp-manager' ),
     438                        'smtp_rejection'   => __( 'The mail server rejected the email', 'bcs-smtp-manager' ),
     439                        'plugin_failure'   => __( 'Couldn’t connect to the mail server', 'bcs-smtp-manager' ),
     440                        'mail_unavailable' => __( 'PHP mail() isn’t available on this server', 'bcs-smtp-manager' ),
     441                        'not_configured'   => __( 'SMTP isn’t set up yet', 'bcs-smtp-manager' ),
     442                );
     443                return isset( $labels[ $type ] ) ? $labels[ $type ] : __( 'Email couldn’t be sent', 'bcs-smtp-manager' );
     444        }
     445
     446        /**
     447         * Slack treats <, > and & as control characters in message text.
     448         *
     449         * @param  string $text
     450         * @return string
     451         */
     452        private static function slack_escape( $text ) {
     453                return str_replace( array( '&', '<', '>' ), array( '&amp;', '&lt;', '&gt;' ), (string) $text );
    275454        }
    276455
    … …  
    282461                $webhook = get_option( BCS_SMTP_OPT_SLACK_WEBHOOK_URL, '' );
    283462                if ( empty( $webhook ) || ! self::is_allowed_webhook_host( $webhook, 'slack' ) ) {
    284                         return;
    285                 }
    286 
    287                 $text = sprintf(
    288                         ":rotating_light: *WordPress SMTP Alert — %s*\n" .
    289                         ">*Time:* %s\n" .
    290                         ">*Site:* <%s|%s>\n" .
    291                         ">*Sending mailbox:* `%s`\n" .
    292                         ">*SMTP host:* `%s:%s`\n" .
    293                         ">*Plugin:* %s\n" .
    294                         ">*Error:* %s\n" .
    295                         ">*Failure type:* %s\n" .
    296                         ">*Source IP:* %s\n" .
    297                         ">*Consecutive failures:* %d%s",
    298                         $payload['site_name'],
    299                         $payload['timestamp'],
    300                         $payload['site_url'], $payload['site_name'],
    301                         $payload['mailbox'],
    302                         $payload['smtp_host'], $payload['smtp_port'],
    303                         $payload['plugin'],
    304                         $payload['error_msg'],
    305                         $payload['failure_type'],
    306                         $payload['source_ip'],
    307                         $payload['failures'],
    308                         $payload['ext_note']
    309                 );
    310 
    311                 wp_safe_remote_post( $webhook, array(
    312                         'body'    => wp_json_encode( array( 'text' => $text ) ),
    313                         'headers' => array( 'Content-Type' => 'application/json' ),
    314                         'timeout' => 10,
     463                        return $this->missing_webhook_error( 'Slack' );
     464                }
     465
     466                $icon  = $payload['is_test'] ? '✅' : '🔴';
     467                $lines = array();
     468                foreach ( $payload['fields'] as $label => $value ) {
     469                        $lines[] = '*' . self::slack_escape( $label ) . ':* ' . self::slack_escape( $value );
     470                }
     471
     472                $blocks = array(
     473                        array( 'type' => 'header', 'text' => array( 'type' => 'plain_text', 'text' => $icon . ' ' . $payload['title'], 'emoji' => true ) ),
     474                        array( 'type' => 'section', 'text' => array( 'type' => 'mrkdwn', 'text' => self::slack_escape( $payload['summary'] ) ) ),
     475                        array( 'type' => 'divider' ),
     476                        array( 'type' => 'section', 'text' => array( 'type' => 'mrkdwn', 'text' => implode( "\n", $lines ) ) ),
     477                );
     478                if ( $payload['note'] ) {
     479                        $blocks[] = array( 'type' => 'section', 'text' => array( 'type' => 'mrkdwn', 'text' => '⚠️ ' . self::slack_escape( $payload['note'] ) ) );
     480                }
     481                $blocks[] = array( 'type' => 'context', 'elements' => array( array( 'type' => 'mrkdwn', 'text' => self::slack_escape( $payload['footer'] ) ) ) );
     482
     483                // "text" is the notification preview and the fallback for clients without blocks.
     484                return $this->post_webhook( $webhook, array(
     485                        'text'   => $icon . ' ' . $payload['title'],
     486                        'blocks' => $blocks,
    315487                ) );
    316488        }
    … …  
    323495                $webhook = get_option( BCS_SMTP_OPT_TEAMS_WEBHOOK_URL, '' );
    324496                if ( empty( $webhook ) || ! self::is_allowed_webhook_host( $webhook, 'teams' ) ) {
    325                         return;
    326                 }
    327 
    328                 $card = array(
    329                         '@type'      => 'MessageCard',
    330                         '@context'   => 'http://schema.org/extensions',
    331                         'themeColor' => 'FF0000',
    332                         'summary'    => 'WordPress SMTP Failure Alert',
    333                         'sections'   => array(
     497                        return $this->missing_webhook_error( 'Teams' );
     498                }
     499
     500                // Adaptive Card in a message envelope — accepted by Power Automate "Workflows" webhooks
     501                // (the replacement for the retired Office 365 connectors) and by legacy connector URLs alike.
     502                $facts = array();
     503                foreach ( $payload['fields'] as $title => $value ) {
     504                        $facts[] = array( 'title' => (string) $title, 'value' => (string) $value );
     505                }
     506
     507                $body = array(
     508                        array(
     509                                'type'   => 'TextBlock',
     510                                'text'   => ( $payload['is_test'] ? '✅ ' : '🔴 ' ) . $payload['title'],
     511                                'weight' => 'Bolder',
     512                                'size'   => 'Large',
     513                                'color'  => $payload['is_test'] ? 'Good' : 'Attention',
     514                                'wrap'   => true,
     515                        ),
     516                        array( 'type' => 'TextBlock', 'text' => $payload['summary'], 'wrap' => true, 'spacing' => 'Small' ),
     517                        array( 'type' => 'FactSet', 'facts' => $facts, 'spacing' => 'Medium' ),
     518                );
     519                if ( $payload['note'] ) {
     520                        $body[] = array( 'type' => 'TextBlock', 'text' => '⚠️ ' . $payload['note'], 'wrap' => true, 'color' => 'Warning' );
     521                }
     522                $body[] = array( 'type' => 'TextBlock', 'text' => $payload['footer'], 'isSubtle' => true, 'size' => 'Small', 'wrap' => true );
     523
     524                return $this->post_webhook( $webhook, array(
     525                        'type'        => 'message',
     526                        'attachments' => array(
    334527                                array(
    335                                         'activityTitle'    => sprintf( '🚨 SMTP Failure — %s', $payload['site_name'] ),
    336                                         'activitySubtitle' => $payload['timestamp'],
    337                                         'facts'            => array(
    338                                                 array( 'name' => 'Site',                 'value' => $payload['site_url'] ),
    339                                                 array( 'name' => 'Sending Mailbox',      'value' => $payload['mailbox'] ),
    340                                                 array( 'name' => 'SMTP Host',            'value' => $payload['smtp_host'] . ':' . $payload['smtp_port'] ),
    341                                                 array( 'name' => 'Plugin Used',          'value' => $payload['plugin'] ),
    342                                                 array( 'name' => 'Error',                'value' => $payload['error_msg'] ),
    343                                                 array( 'name' => 'Failure Type',         'value' => $payload['failure_type'] ),
    344                                                 array( 'name' => 'Source IP',            'value' => $payload['source_ip'] ),
    345                                                 array( 'name' => 'Consecutive Failures', 'value' => (string) $payload['failures'] ),
     528                                        'contentType' => 'application/vnd.microsoft.card.adaptive',
     529                                        'contentUrl'  => null,
     530                                        'content'     => array(
     531                                                '$schema' => 'http://adaptivecards.io/schemas/adaptive-card.json',
     532                                                'type'    => 'AdaptiveCard',
     533                                                'version' => '1.4',
     534                                                'msteams' => array( 'width' => 'Full' ),
     535                                                'body'    => $body,
    346536                                        ),
    347                                         'markdown' => true,
    348537                                ),
    349538                        ),
    350                 );
    351 
    352                 if ( $payload['external_down'] ) {
    353                         $card['sections'][] = array(
    354                                 'text' => '⚠️ External SMTP service may be down — check your provider\'s status page before escalating.',
    355                         );
    356                 }
    357 
    358                 wp_safe_remote_post( $webhook, array(
    359                         'body'    => wp_json_encode( $card ),
    360                         'headers' => array( 'Content-Type' => 'application/json' ),
    361                         'timeout' => 10,
    362539                ) );
    363540        }
    … …  
    370547                $webhook = get_option( BCS_SMTP_OPT_GOOGLE_CHAT_WEBHOOK_URL, '' );
    371548                if ( empty( $webhook ) || ! self::is_allowed_webhook_host( $webhook, 'google_chat' ) ) {
    372                         return;
    373                 }
    374 
    375                 $text = sprintf(
    376                         "🚨 *WordPress SMTP Alert — %s*\n" .
    377                         "*Time:* %s\n" .
    378                         "*Site:* <%s|%s>\n" .
    379                         "*Sending mailbox:* `%s`\n" .
    380                         "*SMTP host:* `%s:%s`\n" .
    381                         "*Plugin:* %s\n" .
    382                         "*Error:* %s\n" .
    383                         "*Failure type:* %s\n" .
    384                         "*Source IP:* %s\n" .
    385                         "*Consecutive failures:* %d%s",
    386                         $payload['site_name'],
    387                         $payload['timestamp'],
    388                         $payload['site_url'], $payload['site_name'],
    389                         $payload['mailbox'],
    390                         $payload['smtp_host'], $payload['smtp_port'],
    391                         $payload['plugin'],
    392                         $payload['error_msg'],
    393                         $payload['failure_type'],
    394                         $payload['source_ip'],
    395                         $payload['failures'],
    396                         $payload['ext_note']
    397                 );
    398 
    399                 wp_safe_remote_post( $webhook, array(
    400                         'body'    => wp_json_encode( array( 'text' => $text ) ),
    401                         'headers' => array( 'Content-Type' => 'application/json' ),
    402                         'timeout' => 10,
    403                 ) );
     549                        return $this->missing_webhook_error( 'Google Chat' );
     550                }
     551
     552                $lines = array( ( $payload['is_test'] ? '✅ ' : '🔴 ' ) . '*' . $payload['title'] . '*', $payload['summary'], '' );
     553                foreach ( $payload['fields'] as $label => $value ) {
     554                        $lines[] = '*' . $label . ':* ' . $value;
     555                }
     556                if ( $payload['note'] ) {
     557                        $lines[] = '';
     558                        $lines[] = '⚠️ ' . $payload['note'];
     559                }
     560                $lines[] = '';
     561                $lines[] = $payload['footer'];
     562
     563                return $this->post_webhook( $webhook, array( 'text' => implode( "\n", $lines ) ) );
    404564        }
    405565
    … …  
    411571                $webhook = get_option( BCS_SMTP_OPT_DISCORD_WEBHOOK_URL, '' );
    412572                if ( empty( $webhook ) || ! self::is_allowed_webhook_host( $webhook, 'discord' ) ) {
    413                         return;
    414                 }
    415 
    416                 // Discord messages don't support Slack-style <url|text> link aliasing — the
    417                 // site URL is appended plainly so it still autolinks.
    418                 $content = sprintf(
    419                         "🚨 **WordPress SMTP Alert — %s**\n" .
    420                         "**Time:** %s\n" .
    421                         "**Site:** %s\n" .
    422                         "**Sending mailbox:** `%s`\n" .
    423                         "**SMTP host:** `%s:%s`\n" .
    424                         "**Plugin:** %s\n" .
    425                         "**Error:** %s\n" .
    426                         "**Failure type:** %s\n" .
    427                         "**Source IP:** %s\n" .
    428                         "**Consecutive failures:** %d%s",
    429                         $payload['site_name'],
    430                         $payload['timestamp'],
    431                         $payload['site_url'],
    432                         $payload['mailbox'],
    433                         $payload['smtp_host'], $payload['smtp_port'],
    434                         $payload['plugin'],
    435                         $payload['error_msg'],
    436                         $payload['failure_type'],
    437                         $payload['source_ip'],
    438                         $payload['failures'],
    439                         $payload['ext_note']
    440                 );
    441 
    442                 wp_safe_remote_post( $webhook, array(
    443                         'body'    => wp_json_encode( array( 'content' => $content ) ),
    444                         'headers' => array( 'Content-Type' => 'application/json' ),
    445                         'timeout' => 10,
     573                        return $this->missing_webhook_error( 'Discord' );
     574                }
     575
     576                $fields = array();
     577                foreach ( $payload['fields'] as $label => $value ) {
     578                        $fields[] = array( 'name' => (string) $label, 'value' => (string) $value, 'inline' => false );
     579                }
     580                $description = $payload['summary'] . ( $payload['note'] ? "\n\n⚠️ " . $payload['note'] : '' );
     581
     582                // An embed shows as a card with a coloured edge: red for a failure, green for a test.
     583                return $this->post_webhook( $webhook, array(
     584                        'embeds' => array(
     585                                array(
     586                                        'title'       => ( $payload['is_test'] ? '✅ ' : '🔴 ' ) . $payload['title'],
     587                                        'description' => $description,
     588                                        'color'       => $payload['is_test'] ? 0x2EA44F : 0xE5484D,
     589                                        'fields'      => $fields,
     590                                        'footer'      => array( 'text' => $payload['footer'] ),
     591                                ),
     592                        ),
    446593                ) );
    447594        }
    … …  
    459606                $webhook = get_option( BCS_SMTP_OPT_CUSTOM_WEBHOOK_URL, '' );
    460607                if ( empty( $webhook ) || ! self::is_allowed_webhook_host( $webhook, 'custom' ) ) {
    461                         return;
     608                        return $this->missing_webhook_error( 'custom' );
    462609                }
    463610
    464611                $body = array(
    465612                        'event'                 => 'smtp_failure',
     613                        'test'                  => $payload['is_test'],
    466614                        'site_name'             => $payload['site_name'],
    467615                        'site_url'              => $payload['site_url'],
    … …  
    478626                );
    479627
    480                 wp_safe_remote_post( $webhook, array(
    481                         'body'    => wp_json_encode( $body ),
    482                         'headers' => array( 'Content-Type' => 'application/json' ),
    483                         'timeout' => 10,
    484                 ) );
     628                return $this->post_webhook( $webhook, $body );
    485629        }
    486630}
  • bcs-smtp-manager/trunk/includes/class-bcs-smtp-wp-cli.php

    r3652946 r3762759  
    1010
    1111/**
    12  * Manage ByteCoreStack SMTP email sending, resending, and the email log.
     12 * Manage ByteCoreStack SMTP email sending and the email log.
    1313 */
    1414class BCS_SMTP_WP_CLI_Command {
    … …  
    3838                // registered the real mailer's wp_mail/phpmailer_init/wp_mail_failed hooks, which is what
    3939                // actually logs this send. Calling init_hooks() again on a second instance would register
    40                 // a duplicate set of hooks and log two rows per send (see resend() below for the same note).
     40                // a duplicate set of hooks and log two rows per send.
    4141                $mailer = new BCS_SMTP_Mailer();
    4242                $result = $mailer->send_test_email( $to );
    … …  
    5050
    5151        /**
    52          * Resend a previously logged, failed email using its stored content.
    53          *
    54          * Requires "Store failed email content" to have been enabled (Config tab)
    55          * at the time the original email failed — otherwise no content was stored.
     52         * List recent email log entries.
    5653         *
    5754         * ## OPTIONS
    5855         *
    59          * <log_id>
    60          * : The email log entry ID to resend (see `wp bcs-smtp log list`).
    61          *
    62          * ## EXAMPLES
    63          *
    64          *     wp bcs-smtp resend 482
    65          *
    66          * @when after_wp_load
    67          */
    68         public function resend( $args ) {
    69                 $log_id = isset( $args[0] ) ? absint( $args[0] ) : 0;
    70                 if ( ! $log_id ) {
    71                         WP_CLI::error( 'Please provide a log entry ID.' );
    72                         return;
    73                 }
    74 
    75                 // No init_hooks() here either — see the note in test() above. resend_stored_email()
    76                 // only needs $db directly (to read/update the log row); the actual send is logged
    77                 // by the already-registered global mailer instance's hooks, same as any other wp_mail() call.
    78                 $db     = new BCS_SMTP_DB();
    79                 $mailer = new BCS_SMTP_Mailer( $db );
    80                 $result = $mailer->resend_stored_email( $log_id );
    81 
    82                 if ( $result['success'] ) {
    83                         WP_CLI::success( $result['message'] );
    84                 } else {
    85                         WP_CLI::error( $result['message'] );
    86                 }
    87         }
    88 
    89         /**
    90          * List recent email log entries.
    91          *
    92          * ## OPTIONS
     56         * [<action>]
     57         * : What to do with the log.
     58         * ---
     59         * default: list
     60         * options:
     61         *   - list
     62         * ---
    9363         *
    9464         * [--status=<status>]
  • bcs-smtp-manager/trunk/readme.txt

    r3652946 r3762759  
    11=== ByteCoreStack - SMTP & Health Monitor ===
    22Contributors: bytecorestack
    3 Tags: smtp, wordpress smtp, email log, smtp alerts, email monitoring
     3Tags: smtp, email log, email, email alerts, email monitoring
    44Requires at least: 5.3
    5 Tested up to: 7.0
     5Tested up to: 7.1
    66Requires PHP: 7.2
    7 Stable tag: 1.2.1
     7Stable tag: 1.3.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1010
    11 The WordPress SMTP plugin that logs every email, monitors delivery health, and alerts you on Slack, Teams, Discord & Google Chat when it fails.
     11Fix WordPress email with SMTP, log every email with its full message, and get Slack, Teams, Discord or Google Chat alerts when sending fails.
    1212
    1313== Description ==
    1414
    15 **Is your WordPress site silently failing to send emails?** Order confirmations, password resets, contact form notifications, shipping updates — when they don't land in the inbox, you're usually the last person to find out, often from an angry customer. ByteCoreStack - SMTP & Health Monitor fixes email deliverability at the source, logs every single send, and alerts your team the instant delivery breaks again — on Slack, Microsoft Teams, Google Chat, or Discord, at no extra cost.
    16 
    17 This plugin doesn't stop the moment your email starts sending again. It keeps going: continuous SMTP health monitoring, a fully searchable email log with CSV export, scheduled delivery reports, and real-time failure alerts, all included.
    18 
    19 = Why your WordPress emails are failing =
    20 
    21 By default, WordPress sends email through your server's PHP `mail()` function. PHP mail has no authentication — no SPF, no DKIM, nothing that proves to Gmail, Outlook, or Yahoo that the message is legitimate. Most inbox providers either bin it as spam or drop it outright. That's why "WordPress not sending email" is one of the most common WordPress problems there is, and why every serious site needs proper SMTP — authenticated, encrypted mail delivery through a real mail server or transactional email provider.
    22 
    23 = How this plugin fixes it =
    24 
    25 The plugin reconfigures `wp_mail()` to send through the authenticated SMTP server you configure — your hosting provider, Gmail, Microsoft 365, or any transactional email service (SendGrid, Mailgun, Amazon SES, Brevo, Zoho, SMTP.com, and others all work over standard SMTP). Every email is logged with its delivery status, and an automated health check keeps testing your connection in the background — so instead of finding out about a broken mail server from an angry customer, you find out from a Slack message.
    26 
    27 = ✅ Core features =
    28 
    29 * **Universal SMTP connection** — host, port, and encryption (SSL / TLS / none) for any standard SMTP provider, with auto-TLS upgrade support
    30 * **Built-in quick-reference for popular providers** — Gmail / Google Workspace, Microsoft 365 / Outlook, Yahoo Mail, SendGrid, Amazon SES, and Mailgun host/port/encryption settings, right inside the settings page
    31 * **Full email log** — date, subject, sender, recipient, delivery status, and the actual error message for every email `wp_mail()` sends
    32 * **AJAX-driven search and filtering** — live search-as-you-type plus date-range presets (today through all time), no page reloads
    33 * **Resend failed emails** — an opt-in setting stores the content of an email only if it fails, so you can resend it straight from the Email Log instead of it being a dead end. Off by default; stored content auto-purges after a configurable number of days
    34 * **CSV export** of the filtered log for record-keeping or support tickets
    35 * **One-click "Send Test Email"** to verify your configuration instantly, with failure-type classification (authentication failure, SMTP rejection, or plugin/system error) so you know exactly what's wrong
    36 * **Scheduled HTML delivery reports** — daily, weekly, or monthly summary emails with a visual chart of sent/failed/unconfirmed volume
    37 * **Automatic log retention** — keep the last 30 days or the last 1,000 records, pruned automatically
    38 * **WordPress Dashboard widget** — SMTP health and 30-day send volume at a glance, without opening the plugin's settings page
    39 * **WP-CLI support** — `wp bcs-smtp test`, `wp bcs-smtp resend <id>`, and `wp bcs-smtp log list` for scripting test sends, resends, and log lookups from the command line or a CI/deploy pipeline
    40 * **Encrypted password storage** — your SMTP password is encrypted at rest, not stored as plaintext
    41 * **Dark-mode admin UI** built for clarity, not just decoration
    42 
    43 = 🔔 Real-time failure alerts =
    44 
    45 Included at no extra cost:
    46 
    47 * **Automated SMTP health checks** via WordPress Cron — a real connection + EHLO + STARTTLS + AUTH test on the interval you choose, with no test email sent on success
    48 * **Five alert channels**: Slack, Microsoft Teams, Google Chat, Discord, and a Custom Webhook option that POSTs a structured JSON payload (`event`, `site_name`, `error_msg`, `consecutive_failures`, and more) for Zapier, Make, n8n, or your own endpoint
    49 * **Configurable failure threshold** — alert after 1 failure or wait for N consecutive failures, your choice
    50 * **Notification cap** so a prolonged outage doesn't spam your channel forever
    51 * **External provider status check** — when an alert fires, the plugin checks whether the failure looks like an outage on your provider's end before notifying you
    52 * All webhook URLs are validated against the provider's real domain and sent over `wp_safe_remote_post()`, so a misconfigured webhook can't be used to reach internal network addresses
    53 
    54 = 🔌 Works with everything that uses wp_mail() =
    55 
    56 WooCommerce, Contact Form 7, WPForms, Gravity Forms, Elementor Forms, user registration/password reset emails, and literally any other plugin or theme that sends mail through WordPress's standard `wp_mail()` function — because that's exactly what this plugin hooks into. Nothing to reconfigure on the form/store plugin side.
    57 
    58 = 🌐 Supported SMTP providers =
    59 
    60 Anything that speaks standard SMTP with host/port/username/password authentication works, because the plugin talks SMTP directly rather than locking you into a specific provider's proprietary API:
    61 
    62 * [Gmail / Google Workspace](https://myaccount.google.com/apppasswords) (App Passwords, or [Workspace SMTP relay](https://support.google.com/a/answer/2956491) for higher volume)
    63 * [Microsoft 365 / Outlook](https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/how-to-set-up-a-multifunction-device-or-application-to-send-email-using-microsoft-365-or-office-365)
     15**Is your WordPress site quietly failing to send email?** Order confirmations, password resets and contact form messages that never arrive usually go unnoticed until a customer complains. ByteCoreStack - SMTP & Health Monitor sends your WordPress email through a real, authenticated SMTP server, keeps a searchable log of every email with its full message, and tells your team on Slack, Microsoft Teams, Google Chat or Discord the moment sending breaks.
     16
     17= Why WordPress emails fail =
     18
     19Out of the box, WordPress sends email with PHP `mail()`. It has no authentication, so Gmail, Outlook and Yahoo often send those messages to spam or reject them, and many hosts disable it entirely. That's why "WordPress not sending email" is one of the most common WordPress problems. The fix is SMTP: sending through a real mail server that signs in, such as Gmail, Microsoft 365, your host's mail server or a transactional email service.
     20
     21= Set up in about a minute =
     22
     231. Pick your provider. The server, port and encryption fill in for you, with sign-in tips for that provider.
     242. Enter your username and password (or app password / API key) and click **Save and test connection**. The plugin signs in to your server right away and tells you whether it worked.
     253. Send a test email to confirm a real message arrives.
     26
     27= Features =
     28
     29**Reliable sending**
     30
     31* Send all WordPress email through any SMTP server: SSL, TLS or no encryption, with automatic TLS upgrade
     32* Ready-made settings for Gmail / Google Workspace, Microsoft 365 / Outlook, SendGrid, Amazon SES, Mailgun, Brevo, Zoho Mail and Yahoo Mail, plus any other SMTP server
     33* Test email with a plain explanation when it fails: sign-in failed, rejected by the server, or couldn't connect, along with the server's own reply
     34* SMTP password encrypted at rest
     35
     36**Email log**
     37
     38* Every email WordPress sends, grouped by day, with its delivery status and the server's reason for any failure
     39* Open any email on its own page: a safe preview of the message, its HTML source, a text version, every header and attachment names, with a phone-width preview to check mobile layout
     40* Search, filter by status (with counts) and date range, and export to CSV
     41* Keeps the last 90 days and removes older entries automatically
     42
     43**Health checks and failure alerts**
     44
     45* Automatic health check every 15 minutes once SMTP is set up: a real sign-in and sender check, with no email sent
     46* Alerts to Slack, Microsoft Teams, Google Chat, Discord and a custom webhook (for Zapier, Make or n8n), as many at once as you like
     47* An alert on the first failed check, and one alert per outage, so a long outage never floods your channel
     48* Checks whether Microsoft 365 reports an outage before alerting, when you send through Microsoft 365
     49
     50**Reports**
     51
     52* Delivery chart and totals for the last 7, 30 or 90 days, with highlights such as your busiest day, failure-free days and how many people your email reached
     53* Monthly summary email on the 1st of every month at 9:00, covering the previous month, sent to whoever you choose
     54
     55**Built for WordPress**
     56
     57* Overview screen showing whether mail is flowing, when the last and next checks run, and what's left to set up
     58* Dashboard widget with SMTP health and 30-day totals
     59* WP-CLI commands: `wp bcs-smtp test` and `wp bcs-smtp log list`
     60* Dark, responsive admin that works on phones and tablets, with keyboard-accessible controls and bundled fonts (no external requests)
     61
     62= Works with any plugin that sends email =
     63
     64WooCommerce, Contact Form 7, WPForms, Gravity Forms, Elementor Forms, Easy Digital Downloads, membership and booking plugins, and WordPress's own password reset and new user emails all send through `wp_mail()`, so they use your SMTP connection automatically. There's nothing to change in those plugins.
     65
     66= Supported SMTP providers =
     67
     68Any provider that offers standard SMTP sign-in works:
     69
     70* [Gmail / Google Workspace](https://myaccount.google.com/apppasswords) (app password, or the [Workspace SMTP relay](https://support.google.com/a/answer/2956491) for higher volume)
     71* [Microsoft 365 / Outlook](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission)
    6472* [SendGrid](https://docs.sendgrid.com/for-developers/sending-email/getting-started-smtp)
     73* [Amazon SES](https://docs.aws.amazon.com/ses/latest/dg/send-email-smtp.html)
    6574* [Mailgun](https://documentation.mailgun.com/docs/mailgun/user-manual/sending-messages/)
    66 * [Amazon SES](https://docs.aws.amazon.com/ses/latest/dg/send-email-smtp.html)
    6775* [Brevo](https://help.brevo.com/hc/en-us/articles/209462765)
    68 * [Zoho Mail](https://www.zoho.com/mail/help/)
    69 * Yahoo Mail, SMTP.com, or your own hosting provider's mail server
    70 
    71 = 🆚 What sets this plugin apart =
    72 
    73 Getting your email sending again is only half the job. If the connection breaks next month — expired app password, host changed something, provider outage — you need to know before a customer tells you. ByteCoreStack - SMTP & Health Monitor treats failure alerting as core functionality, included for every user, because a delivery plugin that can't tell you when delivery breaks isn't finished doing its job.
    74 
    75 = Documentation & support =
    76 
    77 Full plugin details: [bytecorestack.com/plugins/smtp-manager](https://bytecorestack.com/plugins/smtp-manager/). For SMTP protocol background, see the [WordPress wp_mail() developer reference](https://developer.wordpress.org/reference/functions/wp_mail/).
     76* [Zoho Mail](https://www.zoho.com/mail/help/zoho-smtp.html)
     77* [Yahoo Mail](https://help.yahoo.com/kb/SLN15241.html), SMTP.com, or your web host's mail server
     78
     79= Privacy =
     80
     81Everything the plugin stores stays in your own WordPress database: your SMTP settings, and the email log (each email's date, subject, sender, recipients, delivery status, message body, headers and attachment file names). Attached files themselves are never copied. Log entries are deleted automatically after 90 days, only administrators can see them, and uninstalling the plugin removes all of its data. Nothing is sent to ByteCore Stack. See **External services** below for the services the plugin contacts.
     82
     83= Documentation and support =
     84
     85Plugin home page: [bytecorestack.com/plugins/smtp-manager](https://bytecorestack.com/plugins/smtp-manager/). For questions or bug reports, use the support forum on this plugin's WordPress.org page.
     86
     87= Credits =
     88
     89The admin screens use two fonts bundled with the plugin, both under the SIL Open Font License 1.1 (license files included in `assets/fonts`): [Outfit](https://github.com/Outfitio/Outfit-Fonts) and [Geist Mono](https://github.com/vercel/geist-font).
    7890
    7991== Installation ==
    8092
    81 1. Upload the `bcs-smtp-manager` folder to `/wp-content/plugins/`, or install directly from the WordPress Plugins screen.
    82 2. Activate the plugin through the **Plugins** menu in WordPress.
    83 3. Go to **Settings → SMTP Manager** and enter your SMTP host, port, encryption, and credentials (use the built-in provider quick-reference if you're not sure what to enter).
    84 4. Click **Save Settings** — the plugin runs a connection test immediately and tells you if it worked.
    85 5. Use **Send Test Email** to confirm an actual message is delivered end-to-end.
    86 6. Open the **Alerts** tab, enable a channel (Slack, Teams, Google Chat, Discord, or a custom webhook), and turn on automated health monitoring so you're notified the next time something breaks.
     931. In WordPress, go to **Plugins → Add New**, search for "ByteCoreStack SMTP" and click **Install Now**, or upload the `bcs-smtp-manager` folder to `/wp-content/plugins/`.
     942. Activate the plugin.
     953. Go to **Settings → SMTP Manager** and open the **Connection** tab. Pick your provider (Gmail, Microsoft 365, SendGrid, Amazon SES, Mailgun, Brevo, Zoho, Yahoo or another SMTP server).
     964. Enter your username and password (or app password / API key), then click **Save and test connection**.
     975. Click **Send test email** to confirm a real message arrives.
     986. Optional: open the **Alerts** tab and choose where failure alerts go, and turn on the monthly report on the **Reports** tab. Health checks start automatically.
    8799
    88100== Frequently Asked Questions ==
    89101
    90 = What is SMTP and why do I need it for WordPress? =
    91 SMTP (Simple Mail Transfer Protocol) is the standard, authenticated way to send email. WordPress's default `mail()` function has no authentication, which is why its messages are frequently marked as spam or rejected outright by Gmail, Outlook, and Yahoo. Routing through a real SMTP connection — your host, Gmail, Microsoft 365, or a transactional provider — fixes that.
    92 
    93 = Why isn't my WordPress site sending emails right now? =
    94 The most common causes are: your host has disabled the PHP `mail()` function, your SMTP credentials are wrong or expired (Gmail/Microsoft App Passwords expire or get revoked), the wrong port/encryption combination is selected, or your host blocks outbound SMTP ports entirely. Use **Send Test Email** in this plugin — it classifies the failure (authentication, rejection, or system error) so you know which of these it is instead of guessing.
    95 
    96 = Does this work with Gmail? =
    97 Yes. You must use a Gmail App Password — Google no longer allows regular account passwords for third-party SMTP clients. Enable 2-Step Verification on your Google account, then generate an App Password under [Security → App Passwords](https://myaccount.google.com/apppasswords).
    98 
    99 = Does this work with Outlook / Microsoft 365? =
    100 Yes. Use your Microsoft 365 account password or an app password. Set the host to `smtp.office365.com`, port `587`, encryption `TLS`. See Microsoft's [official SMTP relay setup guide](https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/how-to-set-up-a-multifunction-device-or-application-to-send-email-using-microsoft-365-or-office-365) if you need app-specific credentials or hit a sending limit.
    101 
    102 = Does this work with Google Workspace (not just regular Gmail)? =
    103 Yes. Google Workspace also supports a [dedicated SMTP relay service](https://support.google.com/a/answer/2956491) for higher sending limits than a personal Gmail App Password allows — useful if you're sending a high volume of transactional email.
    104 
    105 = Does this work with SendGrid, Mailgun, Amazon SES, Brevo, or Zoho? =
    106 Yes — any provider that offers SMTP relay credentials works, since this plugin speaks standard SMTP rather than a provider-specific API. Grab your SMTP host/port/username/password from each provider's own setup docs:
    107 * [SendGrid SMTP setup](https://docs.sendgrid.com/for-developers/sending-email/getting-started-smtp)
    108 * [Mailgun SMTP setup](https://documentation.mailgun.com/docs/mailgun/user-manual/sending-messages/)
    109 * [Amazon SES SMTP setup](https://docs.aws.amazon.com/ses/latest/dg/send-email-smtp.html)
    110 * [Brevo SMTP relay](https://help.brevo.com/hc/en-us/articles/209462765)
    111 * [Zoho Mail help center](https://www.zoho.com/mail/help/)
    112 
    113 = Does this work with WooCommerce? =
    114 Yes. WooCommerce order emails, and any other plugin that sends email through `wp_mail()`, automatically use the SMTP settings configured here — no separate configuration needed on the WooCommerce side.
    115 
    116 = Does this work with Contact Form 7, WPForms, Gravity Forms, or Elementor Forms? =
    117 Yes. Any plugin that uses the standard WordPress `wp_mail()` function routes through your configured SMTP settings automatically.
    118 
    119 = Can I use this for bulk email marketing or newsletters? =
    120 No. This plugin is built for transactional email — order confirmations, password resets, contact form notifications, account emails. For bulk marketing sends, use a dedicated service such as Mailchimp, Klaviyo, or Brevo's marketing product.
    121 
    122 = Can I switch from another SMTP plugin to this one? =
    123 Yes, and there's nothing destructive about it. Deactivate your current SMTP plugin (or leave it active and just enable this plugin's "Enable SMTP" toggle — whichever hooks `phpmailer_init` last wins), enter your SMTP credentials here, and send a test email to confirm. Nothing about switching SMTP plugins affects your site's actual email content or past history.
    124 
    125 = How does the free real-time alerting work? =
    126 Enable monitoring in the Alerts tab and pick a channel — Slack, Microsoft Teams, Google Chat, Discord, or a Custom Webhook. WordPress Cron then opens a real connection to your SMTP server at the interval you set (EHLO, STARTTLS, and AUTH are all tested, not just a TCP port check) with no email actually sent on a successful check. If it fails for as many consecutive checks as your configured threshold, an alert goes out immediately.
    127 
    128 = How do I set up Slack alerts? =
    129 In the Alerts tab, enable the Alert Channel toggle, select Slack, paste your Incoming Webhook URL, and save. Create the webhook at [api.slack.com/messaging/webhooks](https://api.slack.com/messaging/webhooks).
    130 
    131 = How do I set up Microsoft Teams alerts? =
    132 In the Alerts tab, enable the Alert Channel toggle, select Microsoft Teams, paste your Incoming Webhook URL, and save. Follow Microsoft's [official guide to adding an Incoming Webhook to a Teams channel](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook) to create the URL.
    133 
    134 = How do I set up Google Chat alerts? =
    135 In the Alerts tab, enable the Alert Channel toggle, select Google Chat, paste your Incoming Webhook URL, and save. Follow Google's [official Chat webhooks quickstart](https://developers.google.com/workspace/chat/quickstart/webhooks) to create the URL from your Chat space's Apps & Integrations menu.
    136 
    137 = How do I set up Discord alerts? =
    138 In the Alerts tab, enable the Alert Channel toggle, select Discord, paste your Incoming Webhook URL, and save. Follow Discord's [official "Intro to Webhooks" guide](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) to create the URL from your channel's Server Settings → Integrations → Webhooks.
    139 
    140 = How do I set up a custom webhook for Zapier, Make, or n8n? =
    141 In the Alerts tab, enable the Alert Channel toggle, select Custom Webhook, and paste the URL of your endpoint. Unlike the chat-app channels, this sends a structured JSON payload — `event`, `site_name`, `error_msg`, `consecutive_failures`, and more — built for automation tools to parse rather than for a human to read directly. Docs for the most common automation tools:
    142 * [Webhooks by Zapier](https://zapier.com/page/webhooks/) — trigger a Zap from this plugin's alert payload
    143 * [Make (Integromat) webhooks](https://www.make.com/en/help/tools/webhooks) — use a "Custom webhook" trigger module
    144 * [n8n Webhook node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/) — for self-hosted automation
    145 
    146 = Does this support multisite networks? =
    147 The plugin activates and works on individual sites within a multisite network, but there's currently no centralized network-wide settings screen — each site is configured independently.
    148 
    149 = How does log pruning work? =
    150 Log entries are pruned automatically once per day via WordPress Cron. Choose to keep either the last 30 days or the last 1,000 records — older entries beyond your choice are removed automatically so the table doesn't grow unbounded.
    151 
    152 = Can I resend a failed email? =
    153 Yes, if you've enabled "Store failed email content" in the Config tab first. This is off by default — email bodies are never stored otherwise, matching the plugin's normal metadata-only logging. When enabled, the content of an email is stored *only if it fails to send*, so a "Resend" button appears on that row in the Email Log. Resending creates a fresh log entry with its own delivery status; stored content is auto-purged after the number of days you configure (default 7), independent of your overall log retention setting.
    154 
    155 = Can I send test emails, resend failed emails, or check the log from the command line? =
    156 Yes. The plugin includes WP-CLI support: `wp bcs-smtp test you@example.com` sends a test email, `wp bcs-smtp resend <id>` resends a failed email by its log ID (same requirements as the admin Resend button — "Store failed email content" must have been enabled when it failed), and `wp bcs-smtp log list` lists recent log entries with `--status`, `--days`, and `--format` options. Handy for CI/deploy pipelines or server automation.
     102= Why is my WordPress site not sending emails? =
     103
     104The usual causes: your host has disabled PHP `mail()`, your SMTP password is wrong or has expired (Gmail and Microsoft app passwords can be revoked), the port and encryption don't match, or your host blocks outgoing SMTP ports. Send a test email from this plugin. It tells you which of these it is and shows the mail server's own reply.
     105
     106= What is SMTP and why do I need it? =
     107
     108SMTP is the standard, authenticated way to send email. WordPress's default PHP `mail()` doesn't sign in to anything, so its messages are often marked as spam or rejected. Sending through a real SMTP server (your host, Gmail, Microsoft 365 or a transactional email service) fixes that.
     109
     110= Does it work with Gmail? =
     111
     112Yes. Use a Gmail app password: Google no longer accepts your normal account password for SMTP. Turn on 2-Step Verification, then create an app password under [Security → App passwords](https://myaccount.google.com/apppasswords). Google Workspace accounts can also use the [SMTP relay service](https://support.google.com/a/answer/2956491) for higher sending limits.
     113
     114= Does it work with Microsoft 365 / Outlook? =
     115
     116Yes. Pick Microsoft 365 / Outlook and the server (`smtp.office365.com`, port 587, TLS) fills in. SMTP sign-in (SMTP AUTH) must be allowed for the mailbox in your Microsoft 365 admin center; see Microsoft's [authenticated SMTP guide](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission).
     117
     118= Does it work with SendGrid, Mailgun, Amazon SES, Brevo or Zoho? =
     119
     120Yes. Any service that gives you SMTP credentials works. For Amazon SES, copy the SMTP endpoint for your region from the SES console, and use your SES SMTP credentials rather than your AWS access keys.
     121
     122= Does it work with WooCommerce, Contact Form 7, WPForms or Gravity Forms? =
     123
     124Yes. Any plugin that sends email through WordPress's `wp_mail()` function uses your SMTP connection automatically, with nothing to configure on their side.
     125
     126= Can I see the full content of an email that was sent? =
     127
     128Yes. Click **View** on any row of the Email log to open the email: a safe preview of the message, its HTML source, a plain-text version, every header and the names of any attachments. Failed emails also show the mail server's complete reply.
     129
     130= What does the email log store, and for how long? =
     131
     132Each entry keeps the date, subject, sender, recipients, delivery status, any error, the message body, headers and attachment file names (never the files themselves). Entries are deleted automatically after 90 days. Saved emails can include sensitive details such as password reset links or form submissions, so only administrators can view the log.
     133
     134= How do failure alerts work? =
     135
     136Health checks run automatically every 15 minutes once SMTP is set up. Each check signs in to your mail server and checks your sender address, without sending an email. If a check fails, one alert goes to every channel you picked on the Alerts tab. You won't get another until mail has recovered and broken again.
     137
     138= How do I set up Slack, Microsoft Teams, Google Chat or Discord alerts? =
     139
     140On the Alerts tab, turn on failure alerts, tick the channel and paste its incoming webhook URL. Each channel has a setup guide link next to its field:
     141* [Slack incoming webhooks](https://api.slack.com/messaging/webhooks)
     142* [Microsoft Teams incoming webhooks](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook)
     143* [Google Chat webhooks](https://developers.google.com/workspace/chat/quickstart/webhooks)
     144* [Discord webhooks](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks)
     145
     146= Can I send alerts to Zapier, Make or n8n? =
     147
     148Yes. Use the custom webhook channel. It sends a JSON payload (`event`, `site_name`, `error_msg`, `consecutive_failures` and more) that automation tools can read: [Zapier webhooks](https://zapier.com/page/webhooks/), [Make webhooks](https://www.make.com/en/help/tools/webhooks), [n8n Webhook node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/).
     149
     150= When is the monthly report sent? =
     151
     152On the 1st of every month at 9:00 (your site's time zone), covering the whole previous month. "Send a report now" sends the current month so far.
     153
     154= Can I use it for newsletters or bulk email? =
     155
     156No. It's built for transactional email such as orders, password resets, form notifications and account emails. Use a dedicated email marketing service for newsletters.
     157
     158= Can I switch from another SMTP plugin? =
     159
     160Yes. Set up your connection here and send a test email, then deactivate the other SMTP plugin. Switching doesn't affect your emails or other plugins.
    157161
    158162= Is my SMTP password stored securely? =
    159 Yes. The password is encrypted at rest using a key derived from your WordPress installation's own salts before it's saved, not stored as plaintext.
    160 
    161 = Where are my SMTP credentials stored, and are they sent anywhere? =
    162 Credentials are stored in the WordPress options table (`wp_options`) on your own server using the standard `update_option()` API. They are never transmitted to ByteCore Stack or any third party — the only network connection your credentials are used for is the SMTP server you configured them for.
    163 
    164 = What external services does this plugin use? =
    165 See the "External Services" section below for a full breakdown — your SMTP server, and optionally whichever alert channel(s) you choose to enable.
    166 
    167 = I found a bug or have a feature request — where do I report it? =
    168 Use the Support forum on this plugin's WordPress.org page, or reach out via [bytecorestack.com](https://bytecorestack.com/).
    169 
    170 == External Services ==
    171 
    172 This plugin may contact the following external services. All connections are either user-initiated or require explicit configuration by the site administrator.
    173 
    174 **1. Your SMTP server (user-configured)**
    175 The plugin routes all WordPress emails through the SMTP server credentials you enter in Settings → SMTP Manager. The hostname, port, username, and password are provided entirely by you. No data is sent to any ByteCore Stack server.
    176 
    177 **2. Slack Incoming Webhooks (optional)**
    178 If you enable Slack alerts, the plugin will POST a JSON failure notification to the Slack Incoming Webhook URL you provide when consecutive SMTP failures are detected. This URL is specific to your Slack workspace and is created by you.
     163
     164Yes. It's encrypted before it's saved, using a random key the plugin creates for your site (or a `BCS_SMTP_CRYPTO_KEY` constant you can define in `wp-config.php`), and it's never sent anywhere except your own SMTP server.
     165
     166= Does it work on multisite? =
     167
     168Yes, on each site of a network. Each site is set up separately; there's no network-wide settings screen yet.
     169
     170= Can I use it from the command line? =
     171
     172Yes. `wp bcs-smtp test you@example.com` sends a test email, and `wp bcs-smtp log list` lists recent log entries, with `--status`, `--days` and `--format` options.
     173
     174== External services ==
     175
     176The plugin contacts the services below. None of them receive data from ByteCore Stack, and nothing is sent to ByteCore Stack.
     177
     178**1. Your SMTP server (set up by you)**
     179All email WordPress sends goes through the SMTP server you enter on the Connection tab, using the credentials you provide. Health checks, run every 15 minutes once SMTP is set up, sign in to the same server without sending an email.
     180
     181**2. Slack (optional)**
     182If you turn on Slack alerts, the plugin sends a failure notification (site name and address, time, the problem and the mail server's error, mail server and sending address) to the Slack incoming webhook URL you provide, only when a health check fails or you click "Send test alert".
    179183* Service: Slack Technologies, LLC
     184* Terms of service: https://slack.com/terms-of-service
    180185* Privacy policy: https://slack.com/privacy-policy
    181 * Terms of service: https://slack.com/terms-of-service
    182 
    183 **3. Microsoft Teams Incoming Webhooks (optional)**
    184 If you enable Teams alerts, the plugin will POST a JSON failure notification to the Teams Incoming Webhook URL you provide when consecutive SMTP failures are detected. This URL is specific to your Teams channel and is created by you.
     186
     187**3. Microsoft Teams (optional)**
     188If you turn on Teams alerts, the same notification is sent to the Teams webhook URL you provide, only when a health check fails or you click "Send test alert".
    185189* Service: Microsoft Corporation
     190* Terms of service: https://www.microsoft.com/en-us/servicesagreement
    186191* Privacy policy: https://privacy.microsoft.com/en-us/privacystatement
     192
     193**4. Google Chat (optional)**
     194If you turn on Google Chat alerts, the same notification is sent to the Google Chat webhook URL you provide, only when a health check fails or you click "Send test alert".
     195* Service: Google LLC
     196* Terms of service: https://policies.google.com/terms
     197* Privacy policy: https://policies.google.com/privacy
     198
     199**5. Discord (optional)**
     200If you turn on Discord alerts, the same notification is sent to the Discord webhook URL you provide, only when a health check fails or you click "Send test alert".
     201* Service: Discord Inc.
     202* Terms of service: https://discord.com/terms
     203* Privacy policy: https://discord.com/privacy
     204
     205**6. Custom webhook (optional)**
     206If you turn on the custom webhook, the notification is sent as JSON to the URL you provide (the same details, plus your server's IP address and the number of failed checks), only when a health check fails or you click "Send test alert". The URL is entirely your choice, such as Zapier, Make, n8n or your own server.
     207
     208**7. ipify (automatic, only on some servers)**
     209The server's outgoing IP address is included in diagnostics and alerts. If your server doesn't report its own address (common on local, Docker or some proxied setups), the plugin asks the ipify public API for it, at most once a day. The request carries no site or email data; the reply is just an IP address.
     210* Service: ipify
     211* Website and terms: https://www.ipify.org/
     212
     213**8. Microsoft 365 service status (automatic, only for Microsoft 365)**
     214When a health check fails and your SMTP server is Microsoft 365 / Outlook, the plugin reads Microsoft's public service status page to tell you whether Microsoft is reporting an outage. No site or email data is sent.
     215* Service: Microsoft Corporation
    187216* Terms of service: https://www.microsoft.com/en-us/servicesagreement
    188 
    189 **4. Google Chat Incoming Webhooks (optional)**
    190 If you enable Google Chat alerts, the plugin will POST a JSON failure notification to the Google Chat Incoming Webhook URL you provide when consecutive SMTP failures are detected. This URL is specific to your Chat space and is created by you.
    191 * Service: Google LLC
    192 * Privacy policy: https://policies.google.com/privacy
    193 * Terms of service: https://policies.google.com/terms
    194 
    195 **5. Discord Incoming Webhooks (optional)**
    196 If you enable Discord alerts, the plugin will POST a JSON failure notification to the Discord Incoming Webhook URL you provide when consecutive SMTP failures are detected. This URL is specific to your Discord channel and is created by you.
    197 * Service: Discord Inc.
    198 * Privacy policy: https://discord.com/privacy
    199 * Terms of service: https://discord.com/terms
    200 
    201 **6. Custom Webhook (optional)**
    202 If you enable the Custom Webhook channel, the plugin will POST a structured JSON failure notification to the URL you provide when consecutive SMTP failures are detected. This URL is entirely user-supplied — point it at Zapier, Make, n8n, or your own server. No data is sent unless you configure this channel.
    203 
    204 **7. ipify.org (automatic, no configuration)**
    205 When determining the server's outbound IP address for connection diagnostics (used only if the server's own `SERVER_ADDR` is unavailable, e.g. in some local/Docker/proxy environments), the plugin makes a GET request to the ipify.org public IP-lookup API. No data is sent beyond the request itself; the response is just an IP address string.
    206 * Service: ipify (Randomize.io)
    207 * Privacy policy: https://www.ipify.org/
    208 
    209 **8. Microsoft 365 Service Status API (automatic, only when your configured SMTP host is Microsoft 365/Outlook)**
    210 When the automated SMTP health check (see "Real-time failure alerts") detects a connection failure and your configured SMTP host is `office365` or `outlook`, the plugin queries Microsoft's public service status API to check whether the failure is a known Microsoft-side outage before sending you an alert. No site or email data is sent — only an outbound GET request is made.
    211 * Service: Microsoft Corporation
    212217* Privacy policy: https://privacy.microsoft.com/en-us/privacystatement
    213218
    214 No data is collected, transmitted to, or processed by ByteCore Stack servers.
    215 
    216219== Screenshots ==
    217220
    218 1. Dashboard overview — SMTP authentication status and recent email log activity at a glance
    219 2. SMTP server connection panel — host, port, encryption, and credentials in one place
    220 3. Email log — full delivery history with search, status filters, and automatic log retention
    221 4. Email report — visual delivery chart with scheduled report configuration
    222 5. Send Test Email — verify your SMTP configuration and delivery instantly
    223 6. Alerts tab — enabling automated SMTP health monitoring
    224 7. Alert channel setup — Slack, Microsoft Teams, Google Chat, Discord, or a custom webhook
    225 8. SMTP configuration — encryption, authentication, and provider-specific settings
    226 9. A live failure alert delivered to a chat channel — real-time SMTP notification in action
    227 10. Email Log — a Resend button on a failed row
     2211. Overview — whether mail is flowing, last and next health check, email activity chart and what's left to set up
     2222. Connection — pick your provider and the server details fill in; send a test email right alongside
     2233. Alerts — automatic health checks and failure alerts to Slack, Teams, Google Chat, Discord or a webhook
     2244. Reports — delivery chart, totals, highlights and the monthly report email
     2255. Email log — grouped by day, with status filters, counts and search; open any email to see the full message
     2266. Email details — the full message as it was sent, with delivery status, recipients and headers
    228227
    229228== Changelog ==
     229
     230= 1.3.0 =
     231**Redesigned admin.** A new Overview shows whether mail is flowing, which server and sender address are in use, when the last and next health checks run, where alerts go, a 7, 30 or 90-day activity chart, quick actions and what's left to set up. Tabs are now Overview, Connection, Alerts, Reports and Email log, and every screen works on phones and tablets.
     232
     233**One-minute setup.** Pick your provider (Gmail / Google Workspace, Microsoft 365, SendGrid, Amazon SES, Mailgun, Brevo, Zoho, Yahoo or any other SMTP server) and the server, port and encryption fill in, with sign-in tips for that provider. The password field is labelled for that provider (for example "App password" for Gmail). Testing sits right next to the connection form, and you're warned before leaving with unsaved changes.
     234
     235**View the full email.** Every email is now saved with its body, headers and attachment names for 90 days. Click **View** on any row of the Email log to open it on its own page: a safe preview, the HTML source, a text version, a phone-width preview, every header and the mail server's reply. Emails saved by 1.2.1's "store failed emails" option are kept and can be viewed too.
     236
     237**Email log.** Grouped by day (Today, Yesterday, then by date), with status filters that show counts, search, date ranges, 10 to 100 rows per page and CSV export. Rows stay short and easy to scan; the server's full reason for a failure is on the email's own page. The log keeps the last 90 days. If you're updating from a version that kept everything, older entries stay for 30 days after the update, and the Email log offers an export of all entries before they go.
     238
     239**Clearer alerts.** Every alert now opens with a plain title ("Email is failing on your site"), and says what's wrong in plain words. Slack, Teams and Discord get properly formatted cards, and test alerts say clearly that they're a test.
     240
     241**Alerts to several channels at once.** Pick any mix of Slack, Microsoft Teams, Google Chat, Discord and a custom webhook. Each one gets the alert, and "Send test alert" reports how each did. Webhook URLs are checked as you type.
     242
     243**Always-on health checks.** Checks run every 15 minutes once SMTP is set up, alert on the first failure and send one alert per outage. The interval, threshold and alert-limit settings are gone. The health check now also checks your sender address, so a server that accepts the connection but rejects your email is reported as failing.
     244
     245**Monthly report.** The report email now goes out on the 1st of every month at 9:00 (site time) and covers the whole previous month, instead of 24 hours after saving and drifting from there. Your daily or weekly report is switched to monthly when you update. "Send a report now" sends the current month so far. Recipients are entered as tags, the next send date is shown, and turning "Email me this report" off hides the report options.
     246
     247**Redesigned emails.** The test email and the monthly report have a cleaner layout that fits phones and supports dark mode. The report leads with a clear verdict and the delivery rate, then shows sent, failed and unconfirmed totals and a day-by-day chart for every day of the month, with a button to open the full report. The test email confirms which server and sender it went through and when.
     248
     249**Reports.** Delivery chart for 7, 30 or 90 days with totals and highlights: average per day, busiest day, failure-free days, longest failure-free run and how many people your email reached (not counting your own team). Charts are drawn by the plugin, so the bundled Chart.js library (about 200 KB) is no longer loaded.
     250
     251**Fixes** (including fixes prepared for 1.2.2, which wasn't released separately):
     252* Fixed "Could not authenticate" errors when a security plugin rotates the WordPress salts (All-In-One Security, Solid Security, Sucuri) or the site moves: the SMTP password now uses its own encryption key, or a `BCS_SMTP_CRYPTO_KEY` constant in `wp-config.php`. Existing passwords are moved over automatically.
     253* Fixed the health check reporting "healthy" when the password was empty, unreadable or not accepted.
     254* Fixed a "translation loading was triggered too early" notice on WordPress 6.7 and later.
     255* Errors now include the mail server's actual reply, such as Microsoft 365's `535 5.7.139` codes, and a blocked SMTP port times out after 20 seconds instead of hanging.
     256* Microsoft Teams alerts are sent as Adaptive Cards and accept Teams Workflows (Power Automate) webhook URLs.
     257* The "last check" and "next check" times always add up to the 15-minute interval.
     258* Amazon SES no longer pre-fills a region: paste the SMTP endpoint for your own region.
     259* CSV export now protects against spreadsheet formula injection: a subject or address starting with =, +, - or @ (which can come from a contact form) is exported as plain text.
     260* Uninstalling on a multisite network now removes the plugin's data from every site, not just the main one.
     261* A password saved by 1.0.0, which was stored as plain text, is encrypted when you update.
     262
     263**Removed.** The Settings tab, the opt-in "resend failed emails" feature and `wp bcs-smtp resend`.
     264
     265**Under the hood.** Bundled Outfit and Geist Mono fonts (no external requests), the outgoing IP lookup is cached for a day, the email log loads only the columns it shows, uninstalling also removes the plugin's temporary data, and the readme now documents every external service.
    230266
    231267= 1.2.1 =
    … …  
    234270**Renamed** from "SMTP Manager – Email Logs, Monitoring & Alerts" to "ByteCoreStack - SMTP & Health Monitor" for a clearer, more distinctive plugin name. Display name only — the plugin slug (`bcs-smtp-manager`) and all your settings are unchanged.
    235271
    236 **Resend failed emails** — a new opt-in "Store failed email content" setting (Config tab, off by default) retains the body of an email only when it fails to send, so a "Resend" button now appears on failed rows in the Email Log. Resending creates a fresh log entry with its own delivery status; stored content auto-purges after a configurable number of days (default 7), independently of your overall log retention setting. Successful and unconfirmed sends are never affected — this plugin's metadata-only logging is unchanged unless you explicitly enable this setting.
     272**Resend failed emails** — a new opt-in "Store failed email content" setting (Config tab, off by default) retains the body of an email only when it fails to send, so a "Resend" button now appears on failed rows in the Email Log. Resending creates a fresh log entry with its own delivery status; stored content auto-purges after a configurable number of days (default 7), independently of your overall log retention setting.
    237273
    238274**WP-CLI support** — `wp bcs-smtp test <email>`, `wp bcs-smtp resend <log_id>`, and `wp bcs-smtp log list` for scripted test sends, resends, and log lookups from the command line or a CI/deploy pipeline.
    239275
    240 **Directory compliance** — rewrote parts of the description and admin UI to remove comparative claims about other SMTP plugins, and confirmed the plugin's use of external services (SMTP server, alert webhooks, ipify.org, Microsoft 365 status API) is fully documented in the External Services section below, per WordPress.org directory guidelines.
    241 
    242 **Self-hosted admin fonts** — the admin UI no longer loads DM Sans/DM Mono from the Google Fonts CDN (`@import` in `admin.css`); it now uses the system font stack instead. Fixes a Plugin Check "offloaded content" error and removes an external request that was previously made on every visit to the settings page.
     276**Directory compliance** — rewrote parts of the description and admin UI to remove comparative claims about other SMTP plugins, and documented the plugin's use of external services (SMTP server, alert webhooks, ipify.org, Microsoft 365 status API) in the External Services section, per WordPress.org directory guidelines.
     277
     278**Self-hosted admin fonts** — the admin UI no longer loads DM Sans/DM Mono from the Google Fonts CDN; it uses the system font stack instead. Fixes a Plugin Check "offloaded content" error and removes an external request made on every visit to the settings page.
    243279
    244280**Readme fix** — `Tested up to` now uses major.minor format (`7.0`) instead of including a patch version, per the WordPress.org readme spec.
    245281
    246 = 1.1.0 =
    247 **Renamed** from "SMTP Manager by ByteCore Stack" to "SMTP Manager – Email Logs, Monitoring & Alerts" — display name only, the plugin slug and all your settings are unchanged.
    248 
    249 **New alert channels** — Google Chat and Discord join Slack and Microsoft Teams, plus a Custom Webhook channel for Zapier/Make/n8n/your own endpoint. The Custom Webhook channel POSTs a structured JSON payload (`event`, `site_name`, `error_msg`, `consecutive_failures`, etc.) instead of a formatted chat message. All webhook URLs are validated against the provider's real domain and sent via `wp_safe_remote_post()`.
    250 
    251 **New WordPress Dashboard widget** — SMTP health status and a 30-day sent/failed/unconfirmed volume summary, visible without opening the plugin's settings page.
    252 
    253 **Admin UI redesign** — full-width layout, boxed/gradient header, cards split into header/body sections, icon tiles on stat boxes, a "Common SMTP Providers" quick-reference card, full mobile/tablet responsive pass (dropdowns, pagination, button widths, tab bar scrolling), a show/hide eye icon on the password field, and AJAX-driven Email Log filtering with a skeleton loading state.
    254 
    255 **Security**
    256 * The SMTP password is now encrypted at rest instead of stored in plaintext (existing installs migrate automatically on next save).
    257 * All alert webhook URLs are validated against the provider's real domain and sent via `wp_safe_remote_post()`, closing an SSRF vector where a malicious/misconfigured webhook URL could make the server POST to internal addresses.
    258 * Stopped storing full email message bodies and headers in the Email Log table — only delivery metadata is retained, matching what the UI has always claimed.
    259 * Added a 30-second rate limit to "Run Health Check Now" to match the other test/send actions.
    260 
    261 **Reliability**
    262 * Fixed a bug where the automated SMTP health check could report "healthy" after a bare EHLO even when the server's STARTTLS upgrade failed, TLS was selected but STARTTLS wasn't offered, or SMTP auth was never actually verified over an unencrypted connection.
    263 * Scheduled/manual delivery reports now report exactly which recipients succeeded and which failed, instead of one pass/fail flag for the whole batch.
    264 
    265 **Performance**: composite database index on (status, sent_at) for the common Email Log filter combination; CSV export capped at 50,000 rows.
    266 
    267 **Compatibility**: bumped minimum WordPress version to 5.3 (`wp_timezone_string()`/`wp_date()` require it — the plugin previously declared 5.0 but used functions that didn't exist yet).
    268 
    269 **Accessibility & i18n**: AJAX status messages use `role="status" aria-live="polite"`; nearly every user-facing string is now wrapped for translation (previously ~2 out of hundreds, despite declaring a text domain).
    270 
    271 = 1.0.0 =
    272 * Initial release.
     282Older versions are listed in changelog.txt, included with the plugin.
    273283
    274284== Upgrade Notice ==
     285
     286= 1.3.0 =
     287Redesigned admin, full email viewer, multi-channel alerts and automatic health checks. Your settings and email log are kept. The log now keeps 90 days: older entries stay for 30 days after updating so you can export them.
    275288
    276289= 1.2.1 =
    277290First update since 1.1.0. Renamed to "ByteCoreStack - SMTP & Health Monitor" (slug and settings unchanged), adds opt-in "Resend failed emails" and WP-CLI support (`wp bcs-smtp test|resend|log`). No settings change automatically.
    278 
    279 = 1.1.0 =
    280 Major update: full admin UI redesign, renamed to "SMTP Manager – Email Logs, Monitoring & Alerts" (slug unchanged), 3 new alert channels (Google Chat, Discord, Custom Webhook), a Dashboard widget, password encryption, and SSRF-hardened webhooks. Settings migrate automatically.
    281 
    282 = 1.0.0 =
    283 Initial release.
  • bcs-smtp-manager/trunk/uninstall.php

    r3652946 r3762759  
    11<?php
    22/**
    3  * Fired when the plugin is uninstalled — removes all data.
     3 * Fired when the plugin is uninstalled — removes all data, on every site of a network.
    44 *
    55 * @package BCS_SMTP_Manager
    … …  
    1010}
    1111
    12 // Unschedule cron
    13 $bcs_smtp_timestamp = wp_next_scheduled( 'bcs_smtp_health_check' );
    14 if ( $bcs_smtp_timestamp ) {
    15         wp_unschedule_event( $bcs_smtp_timestamp, 'bcs_smtp_health_check' );
     12/**
     13 * Remove the plugin's scheduled events, tables, options and temporary values from the current site.
     14 */
     15function bcs_smtp_uninstall_site() {
     16        global $wpdb;
     17
     18        // Unschedule cron
     19        foreach ( array( 'bcs_smtp_health_check', 'bcs_smtp_prune_log', 'bcs_smtp_send_report' ) as $bcs_smtp_hook ) {
     20                wp_clear_scheduled_hook( $bcs_smtp_hook );
     21        }
     22
     23        // Drop custom tables
     24        // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,PluginCheck.Security.DirectDB.UnescapedDBParameter -- uninstall routine; table names are hardcoded plugin prefixes, not user input.
     25        $bcs_smtp_tables = array(
     26                $wpdb->prefix . 'bcs_smtp_email_log',
     27                $wpdb->prefix . 'bch_smtp_email_log',
     28                $wpdb->prefix . 'bptsm_incidents',
     29        );
     30        foreach ( $bcs_smtp_tables as $bcs_smtp_table ) {
     31                $wpdb->query( "DROP TABLE IF EXISTS {$bcs_smtp_table}" );
     32        }
     33        // phpcs:enable
     34
     35        // Delete all options
     36        $bcs_smtp_options = array(
     37                'bcs_smtp_enabled',
     38                'bcs_from_email',
     39                'bcs_from_name',
     40                'bcs_smtp_host',
     41                'bcs_smtp_port',
     42                'bcs_smtp_encryption',
     43                'bcs_auto_tls',
     44                'bcs_smtp_auth',
     45                'bcs_smtp_username',
     46                'bcs_smtp_custom_server',
     47                'bcs_smtp_password',
     48                'bcs_smtp_crypto_key',
     49                'bcs_smtp_snapshot',
     50                'bcs_monitor_enabled',
     51                'bcs_check_interval',
     52                'bcs_failure_threshold',
     53                'bcs_consecutive_failures',
     54                'bcs_last_check',
     55                'bcs_last_check_success',
     56                'bcs_alert_channel_enabled',
     57                'bcs_alert_channel',
     58                'bcs_alert_webhook_url',
     59                'bcs_slack_webhook_url',
     60                'bcs_teams_webhook_url',
     61                'bcs_google_chat_webhook_url',
     62                'bcs_discord_webhook_url',
     63                'bcs_custom_webhook_url',
     64                'bcs_max_alert_count',
     65                'bcs_alert_sent_count',
     66                'bcs_report_schedule_enabled',
     67                'bcs_report_emails',
     68                'bcs_report_frequency',
     69                'bcs_log_prune_mode',
     70                'bcs_log_prune_value',
     71                'bcs_smtp_migrated_v2',
     72                'bcs_db_version',
     73                'bcs_store_failed_body',
     74                'bcs_failed_body_retention_days',
     75                'bcs_smtp_migrated_1_3',
     76                'bcs_smtp_prune_after',
     77                'bcs_smtp_monitor_enabled',
     78        );
     79        foreach ( $bcs_smtp_options as $bcs_smtp_option ) {
     80                delete_option( $bcs_smtp_option );
     81        }
     82
     83        // Temporary values: the health-check lock, the cached server IP and per-user rate limits.
     84        delete_transient( 'bcs_smtp_check_lock' );
     85        delete_transient( 'bcs_smtp_source_ip' );
     86        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- uninstall routine; removes this plugin's own rate-limit transients.
     87        $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", $wpdb->esc_like( '_transient_bcs_smtp_rl_' ) . '%', $wpdb->esc_like( '_transient_timeout_bcs_smtp_rl_' ) . '%' ) );
    1688}
    1789
    18 // Drop custom tables
    19 global $wpdb;
    20 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,PluginCheck.Security.DirectDB.UnescapedDBParameter -- uninstall routine; table names are hardcoded plugin prefixes, not user input.
    21 $bcs_smtp_tables = array(
    22         $wpdb->prefix . 'bcs_smtp_email_log',
    23         $wpdb->prefix . 'bch_smtp_email_log',
    24         $wpdb->prefix . 'bptsm_incidents',
    25 );
    26 foreach ( $bcs_smtp_tables as $bcs_smtp_table ) {
    27         $wpdb->query( "DROP TABLE IF EXISTS {$bcs_smtp_table}" );
     90// On a network each site has its own log table and settings, so clean every one of them.
     91if ( is_multisite() ) {
     92        foreach ( get_sites( array( 'fields' => 'ids', 'number' => 0 ) ) as $bcs_smtp_site_id ) {
     93                switch_to_blog( $bcs_smtp_site_id );
     94                bcs_smtp_uninstall_site();
     95                restore_current_blog();
     96        }
     97} else {
     98        bcs_smtp_uninstall_site();
    2899}
    29 // phpcs:enable
    30 
    31 // Delete all options
    32 $bcs_smtp_options = array(
    33         'bcs_smtp_enabled',
    34         'bcs_from_email',
    35         'bcs_from_name',
    36         'bcs_smtp_host',
    37         'bcs_smtp_port',
    38         'bcs_smtp_encryption',
    39         'bcs_auto_tls',
    40         'bcs_smtp_auth',
    41         'bcs_smtp_username',
    42         'bcs_smtp_password',
    43         'bcs_monitor_enabled',
    44         'bcs_check_interval',
    45         'bcs_failure_threshold',
    46         'bcs_consecutive_failures',
    47         'bcs_last_check',
    48         'bcs_last_check_success',
    49         'bcs_alert_channel_enabled',
    50         'bcs_alert_channel',
    51         'bcs_alert_webhook_url',
    52         'bcs_slack_webhook_url',
    53         'bcs_teams_webhook_url',
    54         'bcs_google_chat_webhook_url',
    55         'bcs_discord_webhook_url',
    56         'bcs_custom_webhook_url',
    57         'bcs_max_alert_count',
    58         'bcs_alert_sent_count',
    59         'bcs_report_schedule_enabled',
    60         'bcs_report_emails',
    61         'bcs_report_frequency',
    62         'bcs_log_prune_mode',
    63         'bcs_log_prune_value',
    64         'bcs_smtp_migrated_v2',
    65         'bcs_db_version',
    66         'bcs_store_failed_body',
    67         'bcs_failed_body_retention_days',
    68 );
    69 foreach ( $bcs_smtp_options as $bcs_smtp_option ) {
    70         delete_option( $bcs_smtp_option );
    71 }
Note: See TracChangeset for help on using the changeset viewer.