Changeset 3762759
- Timestamp:
- 10/24/2026 05:26:32 AM (2 hours ago)
- Location:
- bcs-smtp-manager
- Files:
-
- 14 added
- 5 deleted
- 30 edited
- 1 copied
-
assets/screenshot-1.png (modified) (1 prop) ( previous)
-
assets/screenshot-2.png (modified) (1 prop) ( previous)
-
assets/screenshot-3.png (modified) (1 prop) ( previous)
-
assets/screenshot-4.png (modified) (1 prop) ( previous)
-
assets/screenshot-5.png (modified) (1 prop) ( previous)
-
assets/screenshot-6.png (modified) (1 prop) ( previous)
-
assets/screenshot-7.png (deleted)
-
assets/screenshot-8.png (deleted)
-
assets/screenshot-9.png (deleted)
-
tags/1.3.0 (copied) (copied from bcs-smtp-manager/trunk )
-
tags/1.3.0/admin/class-bcs-smtp-admin.php (modified) (30 diffs)
-
tags/1.3.0/assets/css/admin.css (modified) (1 diff)
-
tags/1.3.0/assets/fonts (added)
-
tags/1.3.0/assets/fonts/OFL-GeistMono.txt (added)
-
tags/1.3.0/assets/fonts/OFL-Outfit.txt (added)
-
tags/1.3.0/assets/fonts/geist-mono-variable.woff2 (added)
-
tags/1.3.0/assets/fonts/index.php (added)
-
tags/1.3.0/assets/fonts/outfit-variable.woff2 (added)
-
tags/1.3.0/assets/js/admin.js (modified) (6 diffs)
-
tags/1.3.0/assets/js/chart.umd.min.js (deleted)
-
tags/1.3.0/bcs-smtp-manager.php (modified) (4 diffs)
-
tags/1.3.0/changelog.txt (added)
-
tags/1.3.0/includes/class-bcs-smtp-core.php (modified) (3 diffs)
-
tags/1.3.0/includes/class-bcs-smtp-db.php (modified) (13 diffs)
-
tags/1.3.0/includes/class-bcs-smtp-email-template.php (modified) (3 diffs)
-
tags/1.3.0/includes/class-bcs-smtp-mailer.php (modified) (23 diffs)
-
tags/1.3.0/includes/class-bcs-smtp-monitor.php (modified) (14 diffs)
-
tags/1.3.0/includes/class-bcs-smtp-wp-cli.php (modified) (3 diffs)
-
tags/1.3.0/readme.txt (modified) (2 diffs)
-
tags/1.3.0/uninstall.php (modified) (2 diffs)
-
trunk/admin/class-bcs-smtp-admin.php (modified) (30 diffs)
-
trunk/assets/css/admin.css (modified) (1 diff)
-
trunk/assets/fonts (added)
-
trunk/assets/fonts/OFL-GeistMono.txt (added)
-
trunk/assets/fonts/OFL-Outfit.txt (added)
-
trunk/assets/fonts/geist-mono-variable.woff2 (added)
-
trunk/assets/fonts/index.php (added)
-
trunk/assets/fonts/outfit-variable.woff2 (added)
-
trunk/assets/js/admin.js (modified) (6 diffs)
-
trunk/assets/js/chart.umd.min.js (deleted)
-
trunk/bcs-smtp-manager.php (modified) (4 diffs)
-
trunk/changelog.txt (added)
-
trunk/includes/class-bcs-smtp-core.php (modified) (3 diffs)
-
trunk/includes/class-bcs-smtp-db.php (modified) (13 diffs)
-
trunk/includes/class-bcs-smtp-email-template.php (modified) (3 diffs)
-
trunk/includes/class-bcs-smtp-mailer.php (modified) (23 diffs)
-
trunk/includes/class-bcs-smtp-monitor.php (modified) (14 diffs)
-
trunk/includes/class-bcs-smtp-wp-cli.php (modified) (3 diffs)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/uninstall.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
bcs-smtp-manager/assets/screenshot-1.png
-
Property svn:mime-type
changed from
application/octet-streamtoimage/png
-
Property svn:mime-type
changed from
-
bcs-smtp-manager/assets/screenshot-2.png
-
Property svn:mime-type
changed from
application/octet-streamtoimage/png
-
Property svn:mime-type
changed from
-
bcs-smtp-manager/assets/screenshot-3.png
-
Property svn:mime-type
changed from
application/octet-streamtoimage/png
-
Property svn:mime-type
changed from
-
bcs-smtp-manager/assets/screenshot-4.png
-
Property svn:mime-type
changed from
application/octet-streamtoimage/png
-
Property svn:mime-type
changed from
-
bcs-smtp-manager/assets/screenshot-5.png
-
Property svn:mime-type
changed from
application/octet-streamtoimage/png
-
Property svn:mime-type
changed from
-
bcs-smtp-manager/assets/screenshot-6.png
-
Property svn:mime-type
changed from
application/octet-streamtoimage/png
-
Property svn:mime-type
changed from
-
bcs-smtp-manager/tags/1.3.0/admin/class-bcs-smtp-admin.php
r3652946 r3762759 146 146 wp_enqueue_style( 'bcs-smtp-admin', BCS_SMTP_PLUGIN_URL . 'assets/css/admin.css', array(), BCS_SMTP_VERSION ); 147 147 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 ); 150 149 151 150 wp_localize_script( 'bcs-smtp-admin', 'bcsSmtpData', array( … … 158 157 'smtpAuth' => (bool) get_option( 'bcs_smtp_auth', true ), 159 158 'smtpEncryption' => get_option( 'bcs_smtp_encryption', 'tls' ), 160 'monitorEnabled' => (bool) get_option( BCS_SMTP_OPT_MONITOR_ENABLED, false ),161 159 'alertChannelEnabled' => (bool) get_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED, false ), 162 'alertChannel' => get_option( BCS_SMTP_OPT_ALERT_CHANNEL, 'none' ),163 160 '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 ), 164 227 ) ); 165 228 } … … 226 289 update_option( 'bcs_smtp_username', $username ); 227 290 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 228 298 // Only overwrite the stored password if the user submitted a non-empty value. 229 299 // Encrypted at rest — see BCS_SMTP_Mailer::encrypt_password()/get_smtp_password(). … … 241 311 update_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, 0 ); 242 312 } 313 // This test counts as a check, so the next scheduled one is counted from it. 314 $this->monitor->schedule_next_check(); 243 315 } 244 316 … … 254 326 /* translators: 1: SMTP host, 2: SMTP port. */ 255 327 ? 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' ) ), 257 329 ); 258 330 } … … 271 343 check_ajax_referer( 'bcs_smtp_nonce', 'nonce' ); 272 344 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' ) ); }274 345 275 346 $to = isset( $_POST['to'] ) ? sanitize_email( wp_unslash( $_POST['to'] ) ) : ''; … … 278 349 wp_send_json_error( __( 'Invalid recipient email address.', 'bcs-smtp-manager' ) ); 279 350 } 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' ) ); } 280 352 281 353 $result = $this->mailer->send_test_email( $to ); … … 296 368 $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' ); 297 369 } else { 298 $hint = ' ' . __( ' Configure and enable SMTP in the Settingstab — 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' ); 299 371 } 300 372 } 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' ); 302 374 } 303 375 wp_send_json_error( array( 304 'message' => $result['error_msg']. $hint,376 'message' => esc_html( $result['error_msg'] ) . $hint, 305 377 'failure_type' => $type, 306 378 ) ); … … 317 389 if ( $this->is_rate_limited( 'check_now', 30 ) ) { wp_send_json_error( __( 'Please wait 30 seconds before running another health check.', 'bcs-smtp-manager' ) ); } 318 390 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 } 323 401 324 402 wp_send_json_success( array( 325 403 'success' => $success, 326 404 '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, 331 406 ) ); 332 407 } … … 334 409 /* ─────────────────────────────────────────────────────────────────── */ 335 410 /* 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 370 411 /* ─────────────────────────────────────────────────────────────────── */ 371 412 /* AJAX — Save alert channel settings */ … … 377 418 378 419 $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 ) ); 382 423 $slack_url = isset( $_POST['slack_webhook_url'] ) ? esc_url_raw( wp_unslash( $_POST['slack_webhook_url'] ) ) : ''; 383 424 $teams_url = isset( $_POST['teams_webhook_url'] ) ? esc_url_raw( wp_unslash( $_POST['teams_webhook_url'] ) ) : ''; … … 391 432 } 392 433 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 Appshttps:// 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' ) ); 394 435 } 395 436 if ( '' !== $google_chat_url && ! BCS_SMTP_Monitor::is_allowed_webhook_host( $google_chat_url, 'google_chat' ) ) { … … 403 444 } 404 445 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 405 457 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 ); 407 459 update_option( BCS_SMTP_OPT_SLACK_WEBHOOK_URL, $slack_url ); 408 460 update_option( BCS_SMTP_OPT_TEAMS_WEBHOOK_URL, $teams_url ); … … 427 479 } 428 480 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 }433 481 434 482 $mock_result = array( … … 442 490 ); 443 491 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 ) ) ); 449 514 } 450 515 … … 469 534 ob_start(); 470 535 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>'; 472 538 } else { 539 $last_day = ''; 473 540 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 } 474 546 $this->render_email_log_row( $row ); 475 547 } … … 478 550 479 551 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'], 484 557 ) ); 485 558 } … … 495 568 $date_range = isset( $_POST['date_range'] ) ? sanitize_key( wp_unslash( $_POST['date_range'] ) ) : '30days'; 496 569 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 ); 498 572 $chart_data = $this->db->get_email_chart_data( $date_range ); 499 573 … … 509 583 /* ─────────────────────────────────────────────────────────────────── */ 510 584 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 511 598 public function handle_export_csv() { 512 599 if ( ! isset( $_GET['bcs_smtp_action'] ) || 'export_csv' !== $_GET['bcs_smtp_action'] ) { 513 600 return; 514 601 } 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 }518 602 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 ) ); 520 607 } 521 608 … … 537 624 $out = fopen( 'php://output', 'w' ); 538 625 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' ), ',', '"', '\\' ); 540 628 foreach ( $rows as $row ) { 541 629 fputcsv( $out, array( 542 630 $row['id'], 543 631 $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'] ), 547 635 $row['status'], 548 $row['error_msg'],549 ) );636 self::csv_cell( $row['error_msg'] ), 637 ), ',', '"', '\\' ); 550 638 } 551 639 fclose( $out ); … … 555 643 556 644 /* ─────────────────────────────────────────────────────────────────── */ 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 */ 580 646 /* ─────────────────────────────────────────────────────────────────── */ 581 647 582 648 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 } 634 701 635 702 public function ajax_save_report_schedule() { … … 637 704 if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); } 638 705 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'] ); 646 716 647 717 update_option( 'bcs_report_schedule_enabled', $enabled ); 648 718 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 ) ); 662 728 } 663 729 … … 669 735 check_ajax_referer( 'bcs_smtp_nonce', 'nonce' ); 670 736 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 } 671 744 if ( $this->is_rate_limited( 'send_report', 120 ) ) { wp_send_json_error( __( 'Please wait 2 minutes before sending another report.', 'bcs-smtp-manager' ) ); } 672 745 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'; 681 749 682 750 $result = $this->send_report_to( $emails, $date_range ); … … 699 767 public function send_scheduled_report() { 700 768 if ( ! get_option( 'bcs_report_schedule_enabled', false ) ) { 769 wp_clear_scheduled_hook( 'bcs_smtp_send_report' ); 701 770 return; 702 771 } 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', '' ); 707 773 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 ) ); 710 840 } 711 841 … … 723 853 $summary = $this->db->get_email_summary( $date_range ); 724 854 $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 ) ); 726 857 $body = $this->build_report_html_email( $summary, $chart_data, $date_range ); 727 858 … … 739 870 } 740 871 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 741 912 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"> </td>'; 947 } 948 } 949 } else { 950 $bar = '<td class="bcs-track" bgcolor="#e8ecf2" style="background:#e8ecf2;height:8px;font-size:0;line-height:0"> </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] . '">●</span> ' . 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"> </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"> </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 ] . '">■</span> ' . 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' ) ), 755 1070 ); 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; 770 1263 } 771 1264 } 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 ) . ' • 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 — ' . 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> • <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'; 889 1266 } 890 1267 … … 893 1270 /* ─────────────────────────────────────────────────────────────────── */ 894 1271 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><<?php echo esc_html( $from_email ); ?>></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 895 1554 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'] ); 906 1559 ?> 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> 936 1579 </td> 937 1580 </tr> … … 939 1582 } 940 1583 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 941 1715 /* ═══════════════════════════════════════════════════════════════════ 942 1716 PAGE RENDER … … 946 1720 if ( ! current_user_can( 'manage_options' ) ) { return; } 947 1721 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 949 1728 950 1729 // SMTP settings 1730 $smtp_host = get_option( 'bcs_smtp_host', '' ); 1731 $is_first_run = '' === trim( $smtp_host ); 951 1732 $smtp_enabled = (bool) get_option( 'bcs_smtp_enabled', false ); 952 1733 $from_email = get_option( 'bcs_from_email', get_option( 'admin_email' ) ); 953 1734 $from_name = get_option( 'bcs_from_name', get_option( 'blogname' ) ); 954 $smtp_host = get_option( 'bcs_smtp_host', '' );955 1735 $smtp_port = (int) get_option( 'bcs_smtp_port', 587 ); 956 1736 $smtp_enc = get_option( 'bcs_smtp_encryption', 'tls' ); … … 958 1738 $smtp_auth = (bool) get_option( 'bcs_smtp_auth', true ); 959 1739 $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']; 960 1745 961 1746 // 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; 967 1752 968 1753 // 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' ); 977 1768 978 1769 // Scheduled report settings 979 1770 $report_schedule_enabled = (bool) get_option( 'bcs_report_schedule_enabled', false ); 980 1771 $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' ); 986 1799 ?> 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> 995 1807 <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> 998 1817 </div> 999 1818 </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> 1002 1823 </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> 1016 1970 </div> 1017 1971 </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> 1099 2008 </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> 1104 2055 </div> 1105 2056 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> 1112 2089 </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"> 1116 2138 </div> 1117 2139 </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 -->1145 2140 <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> 1152 2147 </label> 1153 2148 <?php endforeach; ?> 1154 2149 </div> 1155 2150 </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 ← <?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> 1237 2158 </div> 1238 2159 </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> 1245 2177 </div> 1246 2178 </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> 1247 2221 </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 • 587 • 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 • 587 • 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 • 587 • 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 • 587 • 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 • 587 • 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 • 587 • 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; ?> 1326 2235 </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> 1334 2245 </div> 1335 2246 1336 2247 <div class="bcs-smtp-table-wrap"> 1337 2248 <table class="bcs-smtp-table"> 2249 <caption class="screen-reader-text"><?php esc_html_e( 'Emails sent by WordPress', 'bcs-smtp-manager' ); ?></caption> 1338 2250 <thead> 1339 2251 <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> 1347 2257 </tr> 1348 2258 </thead> 1349 2259 <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> 1351 2261 </tbody> 1352 2262 </table> 1353 2263 </div> 1354 2264 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> 1362 2271 <option value="50">50</option> 1363 2272 <option value="100">100</option> 1364 2273 </select> 1365 2274 </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>← <?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' ); ?> →</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> 1372 2280 </div> 1373 2281 </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"> 1376 2429 <?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 ); 1378 2435 ?> 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> 1407 2452 </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> 1408 2524 </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> 1951 2526 1952 2527 </div><!-- /.bcs-smtp-wrap --> -
bcs-smtp-manager/tags/1.3.0/assets/css/admin.css
r3652946 r3762759 1 1 /* ===================================================================== 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). */ 41 html.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)} 21 44 22 45 .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 } 25 52 .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} 27 64 28 65 /* ===================================================================== 29 66 Header 30 67 ===================================================================== */ 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)} 54 105 .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)} 89 127 90 128 /* ===================================================================== 91 129 Buttons 92 130 ===================================================================== */ 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} 103 139 .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 } 307 button.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 161 354 .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} 425 td.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} 445 td.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 */ 2 2 ( function ( $ ) { 3 3 'use strict'; 4 4 5 /* Always dark mode */ 6 document.documentElement.setAttribute( 'data-bcs-smtp-theme', 'dark' ); 5 var i18n = ( bcsSmtpData && bcsSmtpData.i18n ) || {}; 6 var T = function ( key, fallback ) { return i18n[ key ] || fallback || ''; }; 7 8 /* Minimal sprintf for %s / %1$s placeholders in translated strings. */ 9 function 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. */ 18 var 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 }; 7 22 8 23 /* ===================================================================== 9 Tab Navigation — with persistence24 Busy-state + message helpers 10 25 ===================================================================== */ 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' ) ); 26 function 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 } 30 function 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 35 function 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 42 function 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 51 function 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 58 function 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 } 65 function 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 ===================================================================== */ 73 var healthDirty = false; 74 var overviewShown = false; 75 76 function 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. */ 88 function markHealthDirty() { 89 refreshHealthBanner(); 90 } 91 92 /* The Connection tab's dot mirrors the Overview banner: green flowing, red failing, amber otherwise */ 93 function 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 */ 102 function 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 ); } ); 114 if ( 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 ===================================================================== */ 119 var TAB_KEY = 'bcs_smtp_tab_v2'; 120 var emailLogLoaded = false; 121 122 function 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 29 144 $( '.bcs-smtp-tab-panel' ).removeClass( 'active' ); 30 145 $( '#bcs-smtp-panel-' + panel ).addClass( 'active' ); 31 146 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(); } 41 151 } 42 152 … … 45 155 } ); 46 156 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 ); } 52 188 } ); 53 189 54 190 /* ===================================================================== 55 Accordion toggle191 CONNECTION: provider presets 56 192 ===================================================================== */ 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 ===================================================================== */ 193 var ENC_PORTS = { ssl: 465, tls: 587, none: 25 }; 194 var PRESET_HOSTS = $( '.bcs-smtp-provider-radio' ).not( '[value="other"]' ).map( function () { return $( this ).data( 'host' ); } ).get().filter( Boolean ); 195 var PRESET_USERS = $( '.bcs-smtp-provider-radio' ).map( function () { return $( this ).data( 'user' ); } ).get().filter( Boolean ); 196 197 function 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 204 function 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. */ 215 var providerMemory = {}; 216 var currentProvider = $( '.bcs-smtp-provider-radio:checked' ).val() || ''; 217 218 function 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 227 function 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 */ 64 235 $( 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 */ 241 function 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 } 248 function 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. */ 261 var 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) */ 70 352 $( 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' ); 74 354 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 ); 80 361 81 362 /* Auth toggle → show/hide username + password */ 363 /* Warn when sign-in is off for a provider that always requires it */ 364 function 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 82 373 $( 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 */ 380 function 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 ); 84 405 } ); 85 406 86 407 /* 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 87 413 $( document ).on( 'click', '#bcs-smtp-save-smtp-btn', function () { 88 414 var $btn = $( this ); 89 var hasHost = $ ( '#bcs-smtp-smtp-host' ).val().trim() !== '';415 var hasHost = $.trim( $( '#bcs-smtp-smtp-host' ).val() ) !== ''; 90 416 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' ) ); 97 420 98 421 $.post( bcsSmtpData.ajaxurl, { … … 108 431 smtp_auth : $( '#bcs-smtp-smtp-auth' ).is( ':checked' ) ? 1 : 0, 109 432 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() || '', 111 435 }, function ( r ) { 112 $btn.prop( 'disabled', false ).html( btnLabel ); 113 436 clearBusy( $btn ); 114 437 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' ) ) ); 117 439 return; 118 440 } 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 139 444 var msg = '<strong>' + r.data.message + '</strong>'; 140 141 // Show connection test result if returned142 445 if ( r.data.connection ) { 143 446 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>'; 150 450 showInlineMsg( '#bcs-smtp-smtp-msg', conn.success ? 'success' : 'warning', msg ); 451 markHealthDirty(); 452 if ( conn.success ) { $( '#bcs-smtp-send-test-btn' ).trigger( 'focus' ); } 151 453 } else { 152 454 showInlineMsg( '#bcs-smtp-smtp-msg', 'success', msg ); 153 455 } 154 456 } ).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 ) ); 158 459 } ); 159 460 } ); 160 461 161 462 /* ===================================================================== 162 EMAIL LOG TAB463 Test email + health check 163 464 ===================================================================== */ 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 ===================================================================== */ 165 513 var emailLogPage = 1; 166 514 var emailLogSearchTmr = null; 167 515 168 516 function 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>'; 172 520 } ).join( '' ) + '</tr>'; 173 521 return new Array( 6 ).fill( row ).join( '' ); … … 178 526 emailLogPage = page; 179 527 var $tbody = $( '#bcs-smtp-log-tbody' ); 180 $tbody. html( emailLogSkeletonHtml() );528 $tbody.attr( 'aria-busy', 'true' ).html( emailLogSkeletonHtml() ); 181 529 182 530 $.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(), 190 538 }, function ( r ) { 539 $tbody.removeAttr( 'aria-busy' ); 191 540 if ( r && r.success ) { 192 541 $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 ) ); 195 546 $( '#bcs-smtp-log-prev' ).prop( 'disabled', page <= 1 ); 196 547 $( '#bcs-smtp-log-next' ).prop( 'disabled', page >= r.data.pages ); 197 548 } 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>' ); 199 550 } 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 */ 557 function 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 } ); 204 599 205 600 $( document ).on( 'input', '#bcs-smtp-log-search', function () { 206 601 clearTimeout( emailLogSearchTmr ); 207 emailLogSearchTmr = setTimeout( function () { loadEmailLog( 1 ); }, 400 );602 emailLogSearchTmr = setTimeout( function () { loadEmailLog( 1 ); }, 350 ); 208 603 } ); 209 604 $( document ).on( 'keydown', '#bcs-smtp-log-search', function ( e ) { … … 214 609 } 215 610 } ); 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() ); 217 613 loadEmailLog( 1 ); 218 614 } ); 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 } ); 226 631 227 632 /* ===================================================================== 228 EMAIL REPORT TAB633 OVERVIEW — email activity line chart (dependency-free SVG) 229 634 ===================================================================== */ 230 var emailReportLoaded = false; 231 var reportChart = null; 635 var SVGNS = 'http://www.w3.org/2000/svg'; 636 637 function 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. */ 644 function 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. */ 653 function 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 677 function 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 684 function 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. */ 715 function 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 728 LineChart.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 793 LineChart.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 801 LineChart.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 830 LineChart.prototype.hide = function () { 831 this.idx = -1; 832 this.el.classList.remove( 'is-hover' ); 833 if ( this.tip ) { this.tip.hidden = true; } 834 }; 835 836 var ovChart = null; 837 838 function 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 861 function 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 ===================================================================== */ 873 var reportChart = null; 874 var reportRange = '30days'; 875 876 /* Facts derived from the daily series: no extra queries needed. */ 877 var RANGE_DAYS = { '7days': 7, '30days': 30, '3months': 90 }; 878 879 function 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 887 function 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 923 function 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 } 232 937 233 938 function loadEmailReport() { 939 var $chart = $( '#bcs-smtp-report-chart' ).attr( 'aria-busy', 'true' ); 234 940 $.post( bcsSmtpData.ajaxurl, { 235 941 action : 'bcs_smtp_get_email_report', 236 942 nonce : bcsSmtpData.nonce, 237 date_range : $( '#bcs-smtp-report-date-range' ).val(),943 date_range : reportRange, 238 944 }, 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 950 function 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. */ 972 var 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 ===================================================================== */ 978 var $chipsWrap = $( '#bcs-smtp-report-chips' ); 979 var $chipList = $chipsWrap.find( '.bcs-smtp-chip-list' ); 980 var $chipEntry = $( '#bcs-smtp-report-emails-entry' ); 981 var $chipHidden = $( '#bcs-smtp-report-emails' ); 982 var CHIP_PLACEHOLDER = $chipEntry.attr( 'placeholder' ) || ''; 983 984 function chipValues() { 985 return String( $chipHidden.val() || '' ).split( ',' ).map( function ( v ) { return $.trim( v ); } ).filter( Boolean ); 986 } 987 988 function 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 1003 function 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. */ 1017 function 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' ); 263 1040 } 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 theme275 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();310 1041 return; 311 1042 } 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 1061 function 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 361 1087 $( 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' ) ); 365 1091 $.post( bcsSmtpData.ajaxurl, { 366 1092 action : 'bcs_smtp_save_report_schedule', … … 368 1094 enabled : $( '#bcs-smtp-report-schedule-enabled' ).is( ':checked' ) ? 1 : 0, 369 1095 emails : $( '#bcs-smtp-report-emails' ).val(), 370 frequency : $( '#bcs-smtp-report-frequency' ).val(),371 1096 }, function ( r ) { 372 $btn.prop( 'disabled', false ).html( btnHtml);1097 clearBusy( $btn ); 373 1098 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 ); 375 1103 } 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' ) ) ); 431 1105 } 432 1106 } ).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 () { 439 1114 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' ) ); 442 1117 $.post( bcsSmtpData.ajaxurl, { 443 action : 'bcs_smtp_ check_now',1118 action : 'bcs_smtp_send_report_now', 444 1119 nonce : bcsSmtpData.nonce, 1120 emails : $( '#bcs-smtp-report-emails' ).val(), 445 1121 }, function ( r ) { 446 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-update"></span> Run Health Check Now');1122 clearBusy( $btn ); 447 1123 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' ) ) ); 450 1127 } 451 1128 } ).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 ) ); 454 1131 } ); 455 1132 } ); 456 1133 457 1134 /* ===================================================================== 458 ALERTS TAB: toggles + channel1135 ALERTS 459 1136 ===================================================================== */ 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 */467 1137 $( document ).on( 'change', '#bcs-smtp-alert-channel-enabled', function () { 468 1138 $( '#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. */ 1144 var WEBHOOK_ROWS = { slack: 'slack', teams: 'teams', google_chat: 'google-chat', discord: 'discord', custom: 'custom' }; 1145 var activeWebhook = null; 1146 1147 function webhookInput( key ) { 1148 return $( '#bcs-smtp-' + WEBHOOK_ROWS[ key ] + '-webhook-url' ); 1149 } 1150 1151 function 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 472 1157 function 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 */ 1178 var 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 1185 function 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 */ 1197 function 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 */ 1210 function 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 1225 function 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 1259 function 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 1277 simpleSave( '#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 537 1300 $( document ).on( 'click', '#bcs-smtp-test-alert-btn', function () { 538 1301 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 ); } ); 555 1313 } ); 556 1314 557 1315 /* ===================================================================== 558 SETTINGS TAB: Change Password button1316 SETTINGS 559 1317 ===================================================================== */ 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 original610 // row's status stays 'failed' (the resend created a separate log entry), so a611 // 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 helper648 ===================================================================== */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 CSV665 ===================================================================== */666 $( document ).on( 'click', '#bcs-smtp-export-csv-btn', function ( e ) {667 e.preventDefault();668 var url = bcsSmtpData.exportUrl669 + '&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-prune678 ===================================================================== */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 708 1318 /* ===================================================================== 709 1319 Init 710 1320 ===================================================================== */ 711 1321 $( 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 } ); 712 1334 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' ) ); 723 1344 } 724 1345 } ); -
bcs-smtp-manager/tags/1.3.0/bcs-smtp-manager.php
r3652946 r3762759 4 4 * Plugin URI: https://bytecorestack.com/plugins/smtp-manager/ 5 5 * 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.16 * Version: 1.3.0 7 7 * Author: ByteCore Stack 8 8 * Author URI: https://bytecorestack.com/ … … 21 21 22 22 // Define plugin constants 23 define( 'BCS_SMTP_VERSION', '1. 2.1' );23 define( 'BCS_SMTP_VERSION', '1.3.0' ); 24 24 define( 'BCS_SMTP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 25 25 define( 'BCS_SMTP_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); 26 26 define( 'BCS_SMTP_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); 27 27 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. 34 29 define( 'BCS_SMTP_OPT_CONSECUTIVE_FAILURES', 'bcs_consecutive_failures' ); 35 30 define( 'BCS_SMTP_OPT_LAST_CHECK', 'bcs_last_check' ); … … 42 37 define( 'BCS_SMTP_OPT_DISCORD_WEBHOOK_URL', 'bcs_discord_webhook_url' ); 43 38 define( 'BCS_SMTP_OPT_CUSTOM_WEBHOOK_URL', 'bcs_custom_webhook_url' ); 44 define( 'BCS_SMTP_OPT_MAX_ALERT_COUNT', 'bcs_max_alert_count' );45 39 define( 'BCS_SMTP_OPT_ALERT_SENT_COUNT', 'bcs_alert_sent_count' ); 46 40 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. 42 define( 'BCS_SMTP_LOG_RETENTION_DAYS', 90 ); 43 44 // Bumped whenever the log table's columns change, so dbDelta runs again on existing sites. 45 define( 'BCS_SMTP_DB_VERSION', '1.3.0.1' ); 46 47 // Health checks are always on once SMTP is set up, with a fixed policy. 48 define( 'BCS_SMTP_CHECK_INTERVAL_MINUTES', 15 ); // Minutes between checks. 49 define( 'BCS_SMTP_FAILURE_THRESHOLD', 1 ); // Alert on the first failed check. 50 define( '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()). 53 define( 'BCS_SMTP_OPT_CRYPTO_KEY', 'bcs_smtp_crypto_key' ); 50 54 51 55 // Load required files … … 62 66 } 63 67 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 */ 74 function 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 } 79 add_filter( 'plugin_action_links_' . BCS_SMTP_PLUGIN_BASENAME, 'bcs_smtp_plugin_action_links' ); 80 81 /** 82 * Initialize the plugin. 83 */ 84 function bcs_smtp_init() { 85 $plugin = new BCS_SMTP_Core(); 86 $plugin->run(); 87 } 88 add_action( 'plugins_loaded', 'bcs_smtp_init' ); 89 90 /** 91 * Activation hook — creates tables and sets default options. 92 * 93 * @since 1.0 94 */ 95 function 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 } 130 register_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 */ 136 function 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 } 141 register_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 */ 146 function 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 ); 136 157 } 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 } 163 add_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 */ 171 function 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 } 216 add_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 55 55 add_action( 'wp_ajax_bcs_smtp_send_test', array( $this->admin, 'ajax_send_test' ) ); 56 56 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' ) ); 58 58 add_action( 'wp_ajax_bcs_smtp_save_alerts', array( $this->admin, 'ajax_save_alerts' ) ); 59 59 add_action( 'wp_ajax_bcs_smtp_send_test_alert', array( $this->admin, 'ajax_send_test_alert' ) ); … … 62 62 add_action( 'wp_ajax_bcs_smtp_save_report_schedule', array( $this->admin, 'ajax_save_report_schedule' ) ); 63 63 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' ) );67 64 } 68 65 … … 75 72 public function run() { 76 73 $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() { 77 81 $this->monitor->maybe_schedule_event(); 82 $this->admin->ensure_report_scheduled(); 78 83 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. 80 85 if ( ! wp_next_scheduled( 'bcs_smtp_prune_log' ) ) { 81 86 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 69 69 public function maybe_create_tables() { 70 70 $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 ) { 72 72 $this->create_tables(); 73 update_option( 'bcs_db_version', BCS_SMTP_ VERSION );73 update_option( 'bcs_db_version', BCS_SMTP_DB_VERSION ); 74 74 } 75 75 } … … 80 80 require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 81 81 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(). 88 85 $sql = "CREATE TABLE {$this->email_log_table()} ( 89 86 id bigint(20) unsigned NOT NULL AUTO_INCREMENT, … … 98 95 body longtext, 99 96 body_format varchar(10), 97 mail_headers text, 98 attachments text, 100 99 resend_count smallint(5) unsigned NOT NULL DEFAULT 0, 101 100 last_resend_at datetime NULL, … … 114 113 // Migration: drop the legacy `headers` column from installs that pre-date 1.2 (dbDelta 115 114 // 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 above117 // 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). 118 117 $table = esc_sql( $this->email_log_table() ); 119 118 foreach ( array( 'headers' ) as $col ) { … … 158 157 'message_id' => isset( $data['message_id'] ) ? $data['message_id'] : null, 159 158 '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, 160 163 ), 161 array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )164 array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' ) 162 165 ); 163 166 return (int) $wpdb->insert_id; … … 258 261 } 259 262 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 $id265 * @param string $body266 * @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.NoCaching271 $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 $id284 */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.NoCaching289 "UPDATE {$table} SET resend_count = resend_count + 1, last_resend_at = %s WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared290 current_time( 'mysql' ),291 (int) $id292 ) );293 }294 295 /**296 * Clear stored bodies of failed emails older than $days — independent of (and typically297 * shorter than) the overall log retention window, since bodies are more sensitive than metadata.298 *299 * @param int $days300 */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.NoCaching306 "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.InterpolatedNotPrepared307 $days308 ) );309 }310 311 263 /* ─────────────────────────────────────────────────────────────────── */ 312 264 /* Email log — read */ … … 324 276 * per_page int Rows per page 325 277 * } 278 * Rows carry the delivery columns only, never message bodies, so a page of the log stays light. 279 * 326 280 * @return array { rows, total, pages, page, per_page } 327 281 */ … … 367 321 $wpdb->prepare( 368 322 // 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", 370 324 (int) $args['per_page'], 371 325 $offset … … 381 335 $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare 382 336 // 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", 384 338 array_merge( $params, array( (int) $args['per_page'], $offset ) ) 385 339 ), … … 395 349 'per_page' => (int) $args['per_page'], 396 350 ); 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; 397 472 } 398 473 … … 426 501 427 502 /** 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. 429 504 * 430 505 * @param string $date_range … … 591 666 $s = gmdate( 'Y-m-01 00:00:00', $now ); 592 667 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}'"; 593 672 case '3months': 594 673 $s = gmdate( 'Y-m-d 00:00:00', strtotime( '-3 months', $now ) ); … … 615 694 'last_week' => 14, 616 695 'this_month' => 30, 696 'last_month' => 62, 617 697 '3months' => 90, 618 698 '6months' => 180, -
bcs-smtp-manager/tags/1.3.0/includes/class-bcs-smtp-email-template.php
r3652946 r3762759 1 1 <?php 2 2 /** 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). 6 8 * 7 9 * @package BCS_SMTP_Manager … … 13 15 class BCS_SMTP_Email_Template { 14 16 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 15 29 /** 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. 18 31 * 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 23 40 */ 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( '͏‌ ', 40 ); 46 25 47 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> 56 body{margin:0;padding:0;-webkit-text-size-adjust:100%} 57 a{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"> </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> 38 95 </table> 96 <!--[if mso]></td></tr></table><![endif]--> 39 97 </td></tr> 40 98 </table> … … 44 102 45 103 /** 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. 49 108 */ 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 . '">●</span> ' . 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 ) . ' →</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 . ' · <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 : '' ); 55 166 } 56 167 } -
bcs-smtp-manager/tags/1.3.0/includes/class-bcs-smtp-mailer.php
r3652946 r3762759 17 17 public $pending_log_id = null; 18 18 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(); 22 21 23 22 public function __construct( $db = null ) { … … 76 75 */ 77 76 public function capture_outgoing_email( $args ) { 77 $this->smtp_transcript = array(); 78 78 79 if ( ! $this->db ) { 79 80 return $args; … … 108 109 $args['headers'] = $headers; 109 110 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( 118 112 'subject' => isset( $args['subject'] ) ? $args['subject'] : '', 119 113 'from_address' => $from_address, … … 121 115 'status' => 'pending', 122 116 '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 ); 124 123 125 124 return $args; … … 132 131 * @param \PHPMailer\PHPMailer\PHPMailer $phpmailer 133 132 */ 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 134 186 public function on_phpmailer_init( $phpmailer ) { 135 187 $smtp_enabled = (bool) get_option( 'bcs_smtp_enabled' ); … … 137 189 if ( $smtp_enabled ) { 138 190 $this->configure_phpmailer( $phpmailer ); 191 $this->attach_transcript_capture( $phpmailer ); 139 192 } 140 193 … … 165 218 public function on_email_failed( $error ) { 166 219 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' ); 168 221 $this->db->update_email_log_status( $this->pending_log_id, 'failed', $msg ); 169 222 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; 227 299 } 228 300 … … 232 304 233 305 /** 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). 236 330 * 237 331 * @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. 239 333 */ 240 334 public function encrypt_password( $plain ) { … … 242 336 return $plain; 243 337 } 244 $key = hash( 'sha256', wp_salt( 'auth' ), true);338 $key = $this->get_crypto_key(); 245 339 $iv = random_bytes( 16 ); 246 340 $cipher = openssl_encrypt( $plain, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv ); … … 248 342 return $plain; 249 343 } 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. 251 346 } 252 347 253 348 /** 254 349 * 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. 257 354 * 258 355 * @param string $stored 259 * @return string 356 * @return string Plaintext, or '' when the value can't be decrypted. 260 357 */ 261 358 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:' ) ) { 263 381 return $stored; 264 382 } … … 274 392 $key = hash( 'sha256', wp_salt( 'auth' ), true ); 275 393 $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; 277 402 } 278 403 … … 280 405 public function get_smtp_password() { 281 406 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' ); 282 421 } 283 422 … … 307 446 $phpmailer->Host = $host; 308 447 $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 } 309 454 310 455 switch ( $encryption ) { … … 339 484 340 485 /* ─────────────────────────────────────────────────────────────────── */ 341 /* SMTP Plugin Detection (used by Config tab)*/486 /* SMTP Plugin Detection (used by the Settings tab) */ 342 487 /* ─────────────────────────────────────────────────────────────────── */ 343 488 … … 503 648 return array_merge( $base, array( 504 649 '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' ), 506 651 'failure_type' => 'not_configured', 507 652 ) ); … … 513 658 return array_merge( $base, array( 514 659 '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' ), 516 661 'failure_type' => 'mail_unavailable', 517 662 ) ); … … 524 669 } 525 670 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. 526 678 $encryption = strtolower( $info['encryption'] ?: 'tls' ); 527 679 $timeout = 8; … … 630 782 } 631 783 632 // Test SMTP AUTH LOGIN when credentials are configured. Reachable for SSL, successfully-upgraded633 // TLS, and also plain/'none' connections — matching exactly what a real send would attempt, so a634 // 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 675 784 fwrite( $socket, "QUIT\r\n" ); 676 785 fclose( $socket ); … … 684 793 } 685 794 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 686 918 /* ─────────────────────────────────────────────────────────────────── */ 687 919 /* Manual Test Email Send */ … … 711 943 712 944 $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 } 713 958 714 959 $captured_error = ''; … … 745 990 } 746 991 992 $error_msg = $captured_error ? $this->describe_failure( $captured_error ) : 'wp_mail() returned false — no additional error details.'; 747 993 return array( 748 994 '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 ), 751 997 'smtp_host' => $info['host'], 752 998 'smtp_port' => $info['port'], … … 797 1043 798 1044 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 ) ); 849 1076 } 850 1077 … … 862 1089 } 863 1090 // 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'; 864 1097 $response = wp_remote_get( 'https://api.ipify.org', array( 'timeout' => 5 ) ); 865 1098 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; 870 1104 } 871 1105 } -
bcs-smtp-manager/tags/1.3.0/includes/class-bcs-smtp-monitor.php
r3579822 r3762759 44 44 45 45 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 } 48 109 } 49 110 50 111 public function maybe_schedule_event() { 51 $enabled = get_option( BCS_SMTP_OPT_MONITOR_ENABLED, false);112 $enabled = self::is_active(); 52 113 $next = wp_next_scheduled( 'bcs_smtp_health_check' ); 53 114 … … 64 125 /* ─────────────────────────────────────────────────────────────────── */ 65 126 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; 69 135 } 70 136 71 137 // 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; 74 140 } 75 141 $mins = $this->get_interval_minutes(); … … 81 147 update_option( BCS_SMTP_OPT_LAST_CHECK, time() ); 82 148 update_option( BCS_SMTP_OPT_LAST_CHECK_SUCCESS, $result['success'] ); 149 $this->schedule_next_check(); 83 150 84 151 if ( $result['success'] ) { … … 89 156 90 157 delete_transient( 'bcs_smtp_check_lock' ); 158 return $result; 91 159 } 92 160 … … 104 172 update_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, $failures ); 105 173 106 $threshold = (int) get_option( BCS_SMTP_OPT_FAILURE_THRESHOLD, 2 );174 $threshold = (int) BCS_SMTP_FAILURE_THRESHOLD; 107 175 108 176 if ( $failures >= $threshold ) { … … 149 217 /* ─────────────────────────────────────────────────────────────────── */ 150 218 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 ) { 152 229 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; 163 240 $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 ) ); 189 298 } 190 299 … … 213 322 if ( 'teams' === $channel ) { 214 323 // 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 ) { 216 325 if ( $host === $suffix || ( strlen( $host ) > strlen( $suffix ) + 1 && '.' . $suffix === substr( $host, -( strlen( $suffix ) + 1 ) ) ) ) { 217 326 return true; … … 250 359 /* ─────────────────────────────────────────────────────────────────── */ 251 360 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 ) { 253 372 $info = $this->mailer->detect_smtp_plugin(); 254 373 $site_name = get_bloginfo( 'name' ); 255 374 $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 } 259 404 260 405 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( '&', '<', '>' ), (string) $text ); 275 454 } 276 455 … … 282 461 $webhook = get_option( BCS_SMTP_OPT_SLACK_WEBHOOK_URL, '' ); 283 462 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, 315 487 ) ); 316 488 } … … 323 495 $webhook = get_option( BCS_SMTP_OPT_TEAMS_WEBHOOK_URL, '' ); 324 496 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( 334 527 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, 346 536 ), 347 'markdown' => true,348 537 ), 349 538 ), 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,362 539 ) ); 363 540 } … … 370 547 $webhook = get_option( BCS_SMTP_OPT_GOOGLE_CHAT_WEBHOOK_URL, '' ); 371 548 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 ) ) ); 404 564 } 405 565 … … 411 571 $webhook = get_option( BCS_SMTP_OPT_DISCORD_WEBHOOK_URL, '' ); 412 572 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 ), 446 593 ) ); 447 594 } … … 459 606 $webhook = get_option( BCS_SMTP_OPT_CUSTOM_WEBHOOK_URL, '' ); 460 607 if ( empty( $webhook ) || ! self::is_allowed_webhook_host( $webhook, 'custom' ) ) { 461 return ;608 return $this->missing_webhook_error( 'custom' ); 462 609 } 463 610 464 611 $body = array( 465 612 'event' => 'smtp_failure', 613 'test' => $payload['is_test'], 466 614 'site_name' => $payload['site_name'], 467 615 'site_url' => $payload['site_url'], … … 478 626 ); 479 627 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 ); 485 629 } 486 630 } -
bcs-smtp-manager/tags/1.3.0/includes/class-bcs-smtp-wp-cli.php
r3652946 r3762759 10 10 11 11 /** 12 * Manage ByteCoreStack SMTP email sending , resending,and the email log.12 * Manage ByteCoreStack SMTP email sending and the email log. 13 13 */ 14 14 class BCS_SMTP_WP_CLI_Command { … … 38 38 // registered the real mailer's wp_mail/phpmailer_init/wp_mail_failed hooks, which is what 39 39 // 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. 41 41 $mailer = new BCS_SMTP_Mailer(); 42 42 $result = $mailer->send_test_email( $to ); … … 50 50 51 51 /** 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. 56 53 * 57 54 * ## OPTIONS 58 55 * 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 * --- 93 63 * 94 64 * [--status=<status>] -
bcs-smtp-manager/tags/1.3.0/readme.txt
r3652946 r3762759 1 1 === ByteCoreStack - SMTP & Health Monitor === 2 2 Contributors: bytecorestack 3 Tags: smtp, wordpress smtp, email log, smtpalerts, email monitoring3 Tags: smtp, email log, email, email alerts, email monitoring 4 4 Requires at least: 5.3 5 Tested up to: 7. 05 Tested up to: 7.1 6 6 Requires PHP: 7.2 7 Stable tag: 1. 2.17 Stable tag: 1.3.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 The WordPress SMTP plugin that logs every email, monitors delivery health, and alerts you on Slack, Teams, Discord & Google Chat when itfails.11 Fix WordPress email with SMTP, log every email with its full message, and get Slack, Teams, Discord or Google Chat alerts when sending fails. 12 12 13 13 == Description == 14 14 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 19 Out 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 23 1. Pick your provider. The server, port and encryption fill in for you, with sign-in tips for that provider. 24 2. 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. 25 3. 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 64 WooCommerce, 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 68 Any 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) 64 72 * [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) 65 74 * [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)67 75 * [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 81 Everything 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 85 Plugin 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 89 The 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). 78 90 79 91 == Installation == 80 92 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. Op en 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.93 1. 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/`. 94 2. Activate the plugin. 95 3. 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). 96 4. Enter your username and password (or app password / API key), then click **Save and test connection**. 97 5. Click **Send test email** to confirm a real message arrives. 98 6. 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. 87 99 88 100 == Frequently Asked Questions == 89 101 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 104 The 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 108 SMTP 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 112 Yes. 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 116 Yes. 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 120 Yes. 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 124 Yes. 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 128 Yes. 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 132 Each 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 136 Health 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 140 On 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 148 Yes. 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 152 On 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 156 No. 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 160 Yes. 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. 157 161 158 162 = 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 Th is 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 164 Yes. 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 168 Yes, 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 172 Yes. `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 176 The 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)** 179 All 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)** 182 If 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". 179 183 * Service: Slack Technologies, LLC 184 * Terms of service: https://slack.com/terms-of-service 180 185 * 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)** 188 If 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". 185 189 * Service: Microsoft Corporation 190 * Terms of service: https://www.microsoft.com/en-us/servicesagreement 186 191 * Privacy policy: https://privacy.microsoft.com/en-us/privacystatement 192 193 **4. Google Chat (optional)** 194 If 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)** 200 If 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)** 206 If 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)** 209 The 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)** 214 When 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 187 216 * 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 LLC192 * Privacy policy: https://policies.google.com/privacy193 * Terms of service: https://policies.google.com/terms194 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/privacy199 * Terms of service: https://discord.com/terms200 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 Corporation212 217 * Privacy policy: https://privacy.microsoft.com/en-us/privacystatement 213 218 214 No data is collected, transmitted to, or processed by ByteCore Stack servers.215 216 219 == Screenshots == 217 220 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 221 1. Overview — whether mail is flowing, last and next health check, email activity chart and what's left to set up 222 2. Connection — pick your provider and the server details fill in; send a test email right alongside 223 3. Alerts — automatic health checks and failure alerts to Slack, Teams, Google Chat, Discord or a webhook 224 4. Reports — delivery chart, totals, highlights and the monthly report email 225 5. Email log — grouped by day, with status filters, counts and search; open any email to see the full message 226 6. Email details — the full message as it was sent, with delivery status, recipients and headers 228 227 229 228 == 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. 230 266 231 267 = 1.2.1 = … … 234 270 **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. 235 271 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. 237 273 238 274 **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. 239 275 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 previouslymade 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. 243 279 244 280 **Readme fix** — `Tested up to` now uses major.minor format (`7.0`) instead of including a patch version, per the WordPress.org readme spec. 245 281 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. 282 Older versions are listed in changelog.txt, included with the plugin. 273 283 274 284 == Upgrade Notice == 285 286 = 1.3.0 = 287 Redesigned 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. 275 288 276 289 = 1.2.1 = 277 290 First 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 1 1 <?php 2 2 /** 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. 4 4 * 5 5 * @package BCS_SMTP_Manager … … 10 10 } 11 11 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 */ 15 function 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_' ) . '%' ) ); 16 88 } 17 89 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. 91 if ( 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(); 28 99 } 29 // phpcs:enable30 31 // Delete all options32 $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 146 146 wp_enqueue_style( 'bcs-smtp-admin', BCS_SMTP_PLUGIN_URL . 'assets/css/admin.css', array(), BCS_SMTP_VERSION ); 147 147 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 ); 150 149 151 150 wp_localize_script( 'bcs-smtp-admin', 'bcsSmtpData', array( … … 158 157 'smtpAuth' => (bool) get_option( 'bcs_smtp_auth', true ), 159 158 'smtpEncryption' => get_option( 'bcs_smtp_encryption', 'tls' ), 160 'monitorEnabled' => (bool) get_option( BCS_SMTP_OPT_MONITOR_ENABLED, false ),161 159 'alertChannelEnabled' => (bool) get_option( BCS_SMTP_OPT_ALERT_CHANNEL_ENABLED, false ), 162 'alertChannel' => get_option( BCS_SMTP_OPT_ALERT_CHANNEL, 'none' ),163 160 '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 ), 164 227 ) ); 165 228 } … … 226 289 update_option( 'bcs_smtp_username', $username ); 227 290 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 228 298 // Only overwrite the stored password if the user submitted a non-empty value. 229 299 // Encrypted at rest — see BCS_SMTP_Mailer::encrypt_password()/get_smtp_password(). … … 241 311 update_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, 0 ); 242 312 } 313 // This test counts as a check, so the next scheduled one is counted from it. 314 $this->monitor->schedule_next_check(); 243 315 } 244 316 … … 254 326 /* translators: 1: SMTP host, 2: SMTP port. */ 255 327 ? 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' ) ), 257 329 ); 258 330 } … … 271 343 check_ajax_referer( 'bcs_smtp_nonce', 'nonce' ); 272 344 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' ) ); }274 345 275 346 $to = isset( $_POST['to'] ) ? sanitize_email( wp_unslash( $_POST['to'] ) ) : ''; … … 278 349 wp_send_json_error( __( 'Invalid recipient email address.', 'bcs-smtp-manager' ) ); 279 350 } 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' ) ); } 280 352 281 353 $result = $this->mailer->send_test_email( $to ); … … 296 368 $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' ); 297 369 } else { 298 $hint = ' ' . __( ' Configure and enable SMTP in the Settingstab — 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' ); 299 371 } 300 372 } 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' ); 302 374 } 303 375 wp_send_json_error( array( 304 'message' => $result['error_msg']. $hint,376 'message' => esc_html( $result['error_msg'] ) . $hint, 305 377 'failure_type' => $type, 306 378 ) ); … … 317 389 if ( $this->is_rate_limited( 'check_now', 30 ) ) { wp_send_json_error( __( 'Please wait 30 seconds before running another health check.', 'bcs-smtp-manager' ) ); } 318 390 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 } 323 401 324 402 wp_send_json_success( array( 325 403 'success' => $success, 326 404 '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, 331 406 ) ); 332 407 } … … 334 409 /* ─────────────────────────────────────────────────────────────────── */ 335 410 /* 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 370 411 /* ─────────────────────────────────────────────────────────────────── */ 371 412 /* AJAX — Save alert channel settings */ … … 377 418 378 419 $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 ) ); 382 423 $slack_url = isset( $_POST['slack_webhook_url'] ) ? esc_url_raw( wp_unslash( $_POST['slack_webhook_url'] ) ) : ''; 383 424 $teams_url = isset( $_POST['teams_webhook_url'] ) ? esc_url_raw( wp_unslash( $_POST['teams_webhook_url'] ) ) : ''; … … 391 432 } 392 433 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 Appshttps:// 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' ) ); 394 435 } 395 436 if ( '' !== $google_chat_url && ! BCS_SMTP_Monitor::is_allowed_webhook_host( $google_chat_url, 'google_chat' ) ) { … … 403 444 } 404 445 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 405 457 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 ); 407 459 update_option( BCS_SMTP_OPT_SLACK_WEBHOOK_URL, $slack_url ); 408 460 update_option( BCS_SMTP_OPT_TEAMS_WEBHOOK_URL, $teams_url ); … … 427 479 } 428 480 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 }433 481 434 482 $mock_result = array( … … 442 490 ); 443 491 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 ) ) ); 449 514 } 450 515 … … 469 534 ob_start(); 470 535 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>'; 472 538 } else { 539 $last_day = ''; 473 540 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 } 474 546 $this->render_email_log_row( $row ); 475 547 } … … 478 550 479 551 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'], 484 557 ) ); 485 558 } … … 495 568 $date_range = isset( $_POST['date_range'] ) ? sanitize_key( wp_unslash( $_POST['date_range'] ) ) : '30days'; 496 569 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 ); 498 572 $chart_data = $this->db->get_email_chart_data( $date_range ); 499 573 … … 509 583 /* ─────────────────────────────────────────────────────────────────── */ 510 584 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 511 598 public function handle_export_csv() { 512 599 if ( ! isset( $_GET['bcs_smtp_action'] ) || 'export_csv' !== $_GET['bcs_smtp_action'] ) { 513 600 return; 514 601 } 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 }518 602 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 ) ); 520 607 } 521 608 … … 537 624 $out = fopen( 'php://output', 'w' ); 538 625 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' ), ',', '"', '\\' ); 540 628 foreach ( $rows as $row ) { 541 629 fputcsv( $out, array( 542 630 $row['id'], 543 631 $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'] ), 547 635 $row['status'], 548 $row['error_msg'],549 ) );636 self::csv_cell( $row['error_msg'] ), 637 ), ',', '"', '\\' ); 550 638 } 551 639 fclose( $out ); … … 555 643 556 644 /* ─────────────────────────────────────────────────────────────────── */ 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 */ 580 646 /* ─────────────────────────────────────────────────────────────────── */ 581 647 582 648 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 } 634 701 635 702 public function ajax_save_report_schedule() { … … 637 704 if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'bcs-smtp-manager' ) ); } 638 705 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'] ); 646 716 647 717 update_option( 'bcs_report_schedule_enabled', $enabled ); 648 718 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 ) ); 662 728 } 663 729 … … 669 735 check_ajax_referer( 'bcs_smtp_nonce', 'nonce' ); 670 736 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 } 671 744 if ( $this->is_rate_limited( 'send_report', 120 ) ) { wp_send_json_error( __( 'Please wait 2 minutes before sending another report.', 'bcs-smtp-manager' ) ); } 672 745 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'; 681 749 682 750 $result = $this->send_report_to( $emails, $date_range ); … … 699 767 public function send_scheduled_report() { 700 768 if ( ! get_option( 'bcs_report_schedule_enabled', false ) ) { 769 wp_clear_scheduled_hook( 'bcs_smtp_send_report' ); 701 770 return; 702 771 } 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', '' ); 707 773 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 ) ); 710 840 } 711 841 … … 723 853 $summary = $this->db->get_email_summary( $date_range ); 724 854 $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 ) ); 726 857 $body = $this->build_report_html_email( $summary, $chart_data, $date_range ); 727 858 … … 739 870 } 740 871 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 741 912 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"> </td>'; 947 } 948 } 949 } else { 950 $bar = '<td class="bcs-track" bgcolor="#e8ecf2" style="background:#e8ecf2;height:8px;font-size:0;line-height:0"> </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] . '">●</span> ' . 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"> </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"> </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 ] . '">■</span> ' . 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' ) ), 755 1070 ); 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; 770 1263 } 771 1264 } 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 ) . ' • 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 — ' . 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> • <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'; 889 1266 } 890 1267 … … 893 1270 /* ─────────────────────────────────────────────────────────────────── */ 894 1271 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><<?php echo esc_html( $from_email ); ?>></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 895 1554 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'] ); 906 1559 ?> 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> 936 1579 </td> 937 1580 </tr> … … 939 1582 } 940 1583 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 941 1715 /* ═══════════════════════════════════════════════════════════════════ 942 1716 PAGE RENDER … … 946 1720 if ( ! current_user_can( 'manage_options' ) ) { return; } 947 1721 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 949 1728 950 1729 // SMTP settings 1730 $smtp_host = get_option( 'bcs_smtp_host', '' ); 1731 $is_first_run = '' === trim( $smtp_host ); 951 1732 $smtp_enabled = (bool) get_option( 'bcs_smtp_enabled', false ); 952 1733 $from_email = get_option( 'bcs_from_email', get_option( 'admin_email' ) ); 953 1734 $from_name = get_option( 'bcs_from_name', get_option( 'blogname' ) ); 954 $smtp_host = get_option( 'bcs_smtp_host', '' );955 1735 $smtp_port = (int) get_option( 'bcs_smtp_port', 587 ); 956 1736 $smtp_enc = get_option( 'bcs_smtp_encryption', 'tls' ); … … 958 1738 $smtp_auth = (bool) get_option( 'bcs_smtp_auth', true ); 959 1739 $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']; 960 1745 961 1746 // 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; 967 1752 968 1753 // 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' ); 977 1768 978 1769 // Scheduled report settings 979 1770 $report_schedule_enabled = (bool) get_option( 'bcs_report_schedule_enabled', false ); 980 1771 $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' ); 986 1799 ?> 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> 995 1807 <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> 998 1817 </div> 999 1818 </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> 1002 1823 </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> 1016 1970 </div> 1017 1971 </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> 1099 2008 </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> 1104 2055 </div> 1105 2056 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> 1112 2089 </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"> 1116 2138 </div> 1117 2139 </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 -->1145 2140 <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> 1152 2147 </label> 1153 2148 <?php endforeach; ?> 1154 2149 </div> 1155 2150 </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 ← <?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> 1237 2158 </div> 1238 2159 </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> 1245 2177 </div> 1246 2178 </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> 1247 2221 </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 • 587 • 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 • 587 • 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 • 587 • 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 • 587 • 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 • 587 • 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 • 587 • 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; ?> 1326 2235 </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> 1334 2245 </div> 1335 2246 1336 2247 <div class="bcs-smtp-table-wrap"> 1337 2248 <table class="bcs-smtp-table"> 2249 <caption class="screen-reader-text"><?php esc_html_e( 'Emails sent by WordPress', 'bcs-smtp-manager' ); ?></caption> 1338 2250 <thead> 1339 2251 <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> 1347 2257 </tr> 1348 2258 </thead> 1349 2259 <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> 1351 2261 </tbody> 1352 2262 </table> 1353 2263 </div> 1354 2264 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> 1362 2271 <option value="50">50</option> 1363 2272 <option value="100">100</option> 1364 2273 </select> 1365 2274 </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>← <?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' ); ?> →</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> 1372 2280 </div> 1373 2281 </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"> 1376 2429 <?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 ); 1378 2435 ?> 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> 1407 2452 </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> 1408 2524 </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> 1951 2526 1952 2527 </div><!-- /.bcs-smtp-wrap --> -
bcs-smtp-manager/trunk/assets/css/admin.css
r3652946 r3762759 1 1 /* ===================================================================== 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). */ 41 html.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)} 21 44 22 45 .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 } 25 52 .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} 27 64 28 65 /* ===================================================================== 29 66 Header 30 67 ===================================================================== */ 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)} 54 105 .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)} 89 127 90 128 /* ===================================================================== 91 129 Buttons 92 130 ===================================================================== */ 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} 103 139 .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 } 307 button.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 161 354 .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} 425 td.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} 445 td.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 */ 2 2 ( function ( $ ) { 3 3 'use strict'; 4 4 5 /* Always dark mode */ 6 document.documentElement.setAttribute( 'data-bcs-smtp-theme', 'dark' ); 5 var i18n = ( bcsSmtpData && bcsSmtpData.i18n ) || {}; 6 var T = function ( key, fallback ) { return i18n[ key ] || fallback || ''; }; 7 8 /* Minimal sprintf for %s / %1$s placeholders in translated strings. */ 9 function 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. */ 18 var 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 }; 7 22 8 23 /* ===================================================================== 9 Tab Navigation — with persistence24 Busy-state + message helpers 10 25 ===================================================================== */ 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' ) ); 26 function 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 } 30 function 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 35 function 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 42 function 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 51 function 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 58 function 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 } 65 function 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 ===================================================================== */ 73 var healthDirty = false; 74 var overviewShown = false; 75 76 function 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. */ 88 function markHealthDirty() { 89 refreshHealthBanner(); 90 } 91 92 /* The Connection tab's dot mirrors the Overview banner: green flowing, red failing, amber otherwise */ 93 function 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 */ 102 function 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 ); } ); 114 if ( 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 ===================================================================== */ 119 var TAB_KEY = 'bcs_smtp_tab_v2'; 120 var emailLogLoaded = false; 121 122 function 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 29 144 $( '.bcs-smtp-tab-panel' ).removeClass( 'active' ); 30 145 $( '#bcs-smtp-panel-' + panel ).addClass( 'active' ); 31 146 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(); } 41 151 } 42 152 … … 45 155 } ); 46 156 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 ); } 52 188 } ); 53 189 54 190 /* ===================================================================== 55 Accordion toggle191 CONNECTION: provider presets 56 192 ===================================================================== */ 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 ===================================================================== */ 193 var ENC_PORTS = { ssl: 465, tls: 587, none: 25 }; 194 var PRESET_HOSTS = $( '.bcs-smtp-provider-radio' ).not( '[value="other"]' ).map( function () { return $( this ).data( 'host' ); } ).get().filter( Boolean ); 195 var PRESET_USERS = $( '.bcs-smtp-provider-radio' ).map( function () { return $( this ).data( 'user' ); } ).get().filter( Boolean ); 196 197 function 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 204 function 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. */ 215 var providerMemory = {}; 216 var currentProvider = $( '.bcs-smtp-provider-radio:checked' ).val() || ''; 217 218 function 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 227 function 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 */ 64 235 $( 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 */ 241 function 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 } 248 function 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. */ 261 var 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) */ 70 352 $( 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' ); 74 354 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 ); 80 361 81 362 /* Auth toggle → show/hide username + password */ 363 /* Warn when sign-in is off for a provider that always requires it */ 364 function 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 82 373 $( 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 */ 380 function 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 ); 84 405 } ); 85 406 86 407 /* 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 87 413 $( document ).on( 'click', '#bcs-smtp-save-smtp-btn', function () { 88 414 var $btn = $( this ); 89 var hasHost = $ ( '#bcs-smtp-smtp-host' ).val().trim() !== '';415 var hasHost = $.trim( $( '#bcs-smtp-smtp-host' ).val() ) !== ''; 90 416 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' ) ); 97 420 98 421 $.post( bcsSmtpData.ajaxurl, { … … 108 431 smtp_auth : $( '#bcs-smtp-smtp-auth' ).is( ':checked' ) ? 1 : 0, 109 432 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() || '', 111 435 }, function ( r ) { 112 $btn.prop( 'disabled', false ).html( btnLabel ); 113 436 clearBusy( $btn ); 114 437 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' ) ) ); 117 439 return; 118 440 } 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 139 444 var msg = '<strong>' + r.data.message + '</strong>'; 140 141 // Show connection test result if returned142 445 if ( r.data.connection ) { 143 446 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>'; 150 450 showInlineMsg( '#bcs-smtp-smtp-msg', conn.success ? 'success' : 'warning', msg ); 451 markHealthDirty(); 452 if ( conn.success ) { $( '#bcs-smtp-send-test-btn' ).trigger( 'focus' ); } 151 453 } else { 152 454 showInlineMsg( '#bcs-smtp-smtp-msg', 'success', msg ); 153 455 } 154 456 } ).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 ) ); 158 459 } ); 159 460 } ); 160 461 161 462 /* ===================================================================== 162 EMAIL LOG TAB463 Test email + health check 163 464 ===================================================================== */ 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 ===================================================================== */ 165 513 var emailLogPage = 1; 166 514 var emailLogSearchTmr = null; 167 515 168 516 function 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>'; 172 520 } ).join( '' ) + '</tr>'; 173 521 return new Array( 6 ).fill( row ).join( '' ); … … 178 526 emailLogPage = page; 179 527 var $tbody = $( '#bcs-smtp-log-tbody' ); 180 $tbody. html( emailLogSkeletonHtml() );528 $tbody.attr( 'aria-busy', 'true' ).html( emailLogSkeletonHtml() ); 181 529 182 530 $.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(), 190 538 }, function ( r ) { 539 $tbody.removeAttr( 'aria-busy' ); 191 540 if ( r && r.success ) { 192 541 $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 ) ); 195 546 $( '#bcs-smtp-log-prev' ).prop( 'disabled', page <= 1 ); 196 547 $( '#bcs-smtp-log-next' ).prop( 'disabled', page >= r.data.pages ); 197 548 } 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>' ); 199 550 } 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 */ 557 function 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 } ); 204 599 205 600 $( document ).on( 'input', '#bcs-smtp-log-search', function () { 206 601 clearTimeout( emailLogSearchTmr ); 207 emailLogSearchTmr = setTimeout( function () { loadEmailLog( 1 ); }, 400 );602 emailLogSearchTmr = setTimeout( function () { loadEmailLog( 1 ); }, 350 ); 208 603 } ); 209 604 $( document ).on( 'keydown', '#bcs-smtp-log-search', function ( e ) { … … 214 609 } 215 610 } ); 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() ); 217 613 loadEmailLog( 1 ); 218 614 } ); 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 } ); 226 631 227 632 /* ===================================================================== 228 EMAIL REPORT TAB633 OVERVIEW — email activity line chart (dependency-free SVG) 229 634 ===================================================================== */ 230 var emailReportLoaded = false; 231 var reportChart = null; 635 var SVGNS = 'http://www.w3.org/2000/svg'; 636 637 function 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. */ 644 function 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. */ 653 function 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 677 function 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 684 function 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. */ 715 function 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 728 LineChart.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 793 LineChart.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 801 LineChart.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 830 LineChart.prototype.hide = function () { 831 this.idx = -1; 832 this.el.classList.remove( 'is-hover' ); 833 if ( this.tip ) { this.tip.hidden = true; } 834 }; 835 836 var ovChart = null; 837 838 function 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 861 function 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 ===================================================================== */ 873 var reportChart = null; 874 var reportRange = '30days'; 875 876 /* Facts derived from the daily series: no extra queries needed. */ 877 var RANGE_DAYS = { '7days': 7, '30days': 30, '3months': 90 }; 878 879 function 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 887 function 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 923 function 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 } 232 937 233 938 function loadEmailReport() { 939 var $chart = $( '#bcs-smtp-report-chart' ).attr( 'aria-busy', 'true' ); 234 940 $.post( bcsSmtpData.ajaxurl, { 235 941 action : 'bcs_smtp_get_email_report', 236 942 nonce : bcsSmtpData.nonce, 237 date_range : $( '#bcs-smtp-report-date-range' ).val(),943 date_range : reportRange, 238 944 }, 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 950 function 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. */ 972 var 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 ===================================================================== */ 978 var $chipsWrap = $( '#bcs-smtp-report-chips' ); 979 var $chipList = $chipsWrap.find( '.bcs-smtp-chip-list' ); 980 var $chipEntry = $( '#bcs-smtp-report-emails-entry' ); 981 var $chipHidden = $( '#bcs-smtp-report-emails' ); 982 var CHIP_PLACEHOLDER = $chipEntry.attr( 'placeholder' ) || ''; 983 984 function chipValues() { 985 return String( $chipHidden.val() || '' ).split( ',' ).map( function ( v ) { return $.trim( v ); } ).filter( Boolean ); 986 } 987 988 function 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 1003 function 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. */ 1017 function 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' ); 263 1040 } 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 theme275 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();310 1041 return; 311 1042 } 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 1061 function 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 361 1087 $( 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' ) ); 365 1091 $.post( bcsSmtpData.ajaxurl, { 366 1092 action : 'bcs_smtp_save_report_schedule', … … 368 1094 enabled : $( '#bcs-smtp-report-schedule-enabled' ).is( ':checked' ) ? 1 : 0, 369 1095 emails : $( '#bcs-smtp-report-emails' ).val(), 370 frequency : $( '#bcs-smtp-report-frequency' ).val(),371 1096 }, function ( r ) { 372 $btn.prop( 'disabled', false ).html( btnHtml);1097 clearBusy( $btn ); 373 1098 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 ); 375 1103 } 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' ) ) ); 431 1105 } 432 1106 } ).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 () { 439 1114 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' ) ); 442 1117 $.post( bcsSmtpData.ajaxurl, { 443 action : 'bcs_smtp_ check_now',1118 action : 'bcs_smtp_send_report_now', 444 1119 nonce : bcsSmtpData.nonce, 1120 emails : $( '#bcs-smtp-report-emails' ).val(), 445 1121 }, function ( r ) { 446 $btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-update"></span> Run Health Check Now');1122 clearBusy( $btn ); 447 1123 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' ) ) ); 450 1127 } 451 1128 } ).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 ) ); 454 1131 } ); 455 1132 } ); 456 1133 457 1134 /* ===================================================================== 458 ALERTS TAB: toggles + channel1135 ALERTS 459 1136 ===================================================================== */ 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 */467 1137 $( document ).on( 'change', '#bcs-smtp-alert-channel-enabled', function () { 468 1138 $( '#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. */ 1144 var WEBHOOK_ROWS = { slack: 'slack', teams: 'teams', google_chat: 'google-chat', discord: 'discord', custom: 'custom' }; 1145 var activeWebhook = null; 1146 1147 function webhookInput( key ) { 1148 return $( '#bcs-smtp-' + WEBHOOK_ROWS[ key ] + '-webhook-url' ); 1149 } 1150 1151 function 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 472 1157 function 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 */ 1178 var 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 1185 function 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 */ 1197 function 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 */ 1210 function 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 1225 function 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 1259 function 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 1277 simpleSave( '#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 537 1300 $( document ).on( 'click', '#bcs-smtp-test-alert-btn', function () { 538 1301 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 ); } ); 555 1313 } ); 556 1314 557 1315 /* ===================================================================== 558 SETTINGS TAB: Change Password button1316 SETTINGS 559 1317 ===================================================================== */ 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 original610 // row's status stays 'failed' (the resend created a separate log entry), so a611 // 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 helper648 ===================================================================== */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 CSV665 ===================================================================== */666 $( document ).on( 'click', '#bcs-smtp-export-csv-btn', function ( e ) {667 e.preventDefault();668 var url = bcsSmtpData.exportUrl669 + '&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-prune678 ===================================================================== */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 708 1318 /* ===================================================================== 709 1319 Init 710 1320 ===================================================================== */ 711 1321 $( 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 } ); 712 1334 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' ) ); 723 1344 } 724 1345 } ); -
bcs-smtp-manager/trunk/bcs-smtp-manager.php
r3652946 r3762759 4 4 * Plugin URI: https://bytecorestack.com/plugins/smtp-manager/ 5 5 * 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.16 * Version: 1.3.0 7 7 * Author: ByteCore Stack 8 8 * Author URI: https://bytecorestack.com/ … … 21 21 22 22 // Define plugin constants 23 define( 'BCS_SMTP_VERSION', '1. 2.1' );23 define( 'BCS_SMTP_VERSION', '1.3.0' ); 24 24 define( 'BCS_SMTP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 25 25 define( 'BCS_SMTP_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); 26 26 define( 'BCS_SMTP_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); 27 27 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. 34 29 define( 'BCS_SMTP_OPT_CONSECUTIVE_FAILURES', 'bcs_consecutive_failures' ); 35 30 define( 'BCS_SMTP_OPT_LAST_CHECK', 'bcs_last_check' ); … … 42 37 define( 'BCS_SMTP_OPT_DISCORD_WEBHOOK_URL', 'bcs_discord_webhook_url' ); 43 38 define( 'BCS_SMTP_OPT_CUSTOM_WEBHOOK_URL', 'bcs_custom_webhook_url' ); 44 define( 'BCS_SMTP_OPT_MAX_ALERT_COUNT', 'bcs_max_alert_count' );45 39 define( 'BCS_SMTP_OPT_ALERT_SENT_COUNT', 'bcs_alert_sent_count' ); 46 40 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. 42 define( 'BCS_SMTP_LOG_RETENTION_DAYS', 90 ); 43 44 // Bumped whenever the log table's columns change, so dbDelta runs again on existing sites. 45 define( 'BCS_SMTP_DB_VERSION', '1.3.0.1' ); 46 47 // Health checks are always on once SMTP is set up, with a fixed policy. 48 define( 'BCS_SMTP_CHECK_INTERVAL_MINUTES', 15 ); // Minutes between checks. 49 define( 'BCS_SMTP_FAILURE_THRESHOLD', 1 ); // Alert on the first failed check. 50 define( '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()). 53 define( 'BCS_SMTP_OPT_CRYPTO_KEY', 'bcs_smtp_crypto_key' ); 50 54 51 55 // Load required files … … 62 66 } 63 67 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 */ 74 function 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 } 79 add_filter( 'plugin_action_links_' . BCS_SMTP_PLUGIN_BASENAME, 'bcs_smtp_plugin_action_links' ); 80 81 /** 82 * Initialize the plugin. 83 */ 84 function bcs_smtp_init() { 85 $plugin = new BCS_SMTP_Core(); 86 $plugin->run(); 87 } 88 add_action( 'plugins_loaded', 'bcs_smtp_init' ); 89 90 /** 91 * Activation hook — creates tables and sets default options. 92 * 93 * @since 1.0 94 */ 95 function 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 } 130 register_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 */ 136 function 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 } 141 register_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 */ 146 function 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 ); 136 157 } 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 } 163 add_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 */ 171 function 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 } 216 add_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 55 55 add_action( 'wp_ajax_bcs_smtp_send_test', array( $this->admin, 'ajax_send_test' ) ); 56 56 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' ) ); 58 58 add_action( 'wp_ajax_bcs_smtp_save_alerts', array( $this->admin, 'ajax_save_alerts' ) ); 59 59 add_action( 'wp_ajax_bcs_smtp_send_test_alert', array( $this->admin, 'ajax_send_test_alert' ) ); … … 62 62 add_action( 'wp_ajax_bcs_smtp_save_report_schedule', array( $this->admin, 'ajax_save_report_schedule' ) ); 63 63 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' ) );67 64 } 68 65 … … 75 72 public function run() { 76 73 $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() { 77 81 $this->monitor->maybe_schedule_event(); 82 $this->admin->ensure_report_scheduled(); 78 83 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. 80 85 if ( ! wp_next_scheduled( 'bcs_smtp_prune_log' ) ) { 81 86 wp_schedule_event( time() + DAY_IN_SECONDS, 'daily', 'bcs_smtp_prune_log' ); -
bcs-smtp-manager/trunk/includes/class-bcs-smtp-db.php
r3652946 r3762759 69 69 public function maybe_create_tables() { 70 70 $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 ) { 72 72 $this->create_tables(); 73 update_option( 'bcs_db_version', BCS_SMTP_ VERSION );73 update_option( 'bcs_db_version', BCS_SMTP_DB_VERSION ); 74 74 } 75 75 } … … 80 80 require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 81 81 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(). 88 85 $sql = "CREATE TABLE {$this->email_log_table()} ( 89 86 id bigint(20) unsigned NOT NULL AUTO_INCREMENT, … … 98 95 body longtext, 99 96 body_format varchar(10), 97 mail_headers text, 98 attachments text, 100 99 resend_count smallint(5) unsigned NOT NULL DEFAULT 0, 101 100 last_resend_at datetime NULL, … … 114 113 // Migration: drop the legacy `headers` column from installs that pre-date 1.2 (dbDelta 115 114 // 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 above117 // 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). 118 117 $table = esc_sql( $this->email_log_table() ); 119 118 foreach ( array( 'headers' ) as $col ) { … … 158 157 'message_id' => isset( $data['message_id'] ) ? $data['message_id'] : null, 159 158 '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, 160 163 ), 161 array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )164 array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' ) 162 165 ); 163 166 return (int) $wpdb->insert_id; … … 258 261 } 259 262 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 $id265 * @param string $body266 * @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.NoCaching271 $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 $id284 */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.NoCaching289 "UPDATE {$table} SET resend_count = resend_count + 1, last_resend_at = %s WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared290 current_time( 'mysql' ),291 (int) $id292 ) );293 }294 295 /**296 * Clear stored bodies of failed emails older than $days — independent of (and typically297 * shorter than) the overall log retention window, since bodies are more sensitive than metadata.298 *299 * @param int $days300 */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.NoCaching306 "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.InterpolatedNotPrepared307 $days308 ) );309 }310 311 263 /* ─────────────────────────────────────────────────────────────────── */ 312 264 /* Email log — read */ … … 324 276 * per_page int Rows per page 325 277 * } 278 * Rows carry the delivery columns only, never message bodies, so a page of the log stays light. 279 * 326 280 * @return array { rows, total, pages, page, per_page } 327 281 */ … … 367 321 $wpdb->prepare( 368 322 // 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", 370 324 (int) $args['per_page'], 371 325 $offset … … 381 335 $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare 382 336 // 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", 384 338 array_merge( $params, array( (int) $args['per_page'], $offset ) ) 385 339 ), … … 395 349 'per_page' => (int) $args['per_page'], 396 350 ); 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; 397 472 } 398 473 … … 426 501 427 502 /** 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. 429 504 * 430 505 * @param string $date_range … … 591 666 $s = gmdate( 'Y-m-01 00:00:00', $now ); 592 667 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}'"; 593 672 case '3months': 594 673 $s = gmdate( 'Y-m-d 00:00:00', strtotime( '-3 months', $now ) ); … … 615 694 'last_week' => 14, 616 695 'this_month' => 30, 696 'last_month' => 62, 617 697 '3months' => 90, 618 698 '6months' => 180, -
bcs-smtp-manager/trunk/includes/class-bcs-smtp-email-template.php
r3652946 r3762759 1 1 <?php 2 2 /** 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). 6 8 * 7 9 * @package BCS_SMTP_Manager … … 13 15 class BCS_SMTP_Email_Template { 14 16 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 15 29 /** 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. 18 31 * 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 23 40 */ 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( '͏‌ ', 40 ); 46 25 47 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> 56 body{margin:0;padding:0;-webkit-text-size-adjust:100%} 57 a{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"> </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> 38 95 </table> 96 <!--[if mso]></td></tr></table><![endif]--> 39 97 </td></tr> 40 98 </table> … … 44 102 45 103 /** 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. 49 108 */ 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 . '">●</span> ' . 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 ) . ' →</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 . ' · <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 : '' ); 55 166 } 56 167 } -
bcs-smtp-manager/trunk/includes/class-bcs-smtp-mailer.php
r3652946 r3762759 17 17 public $pending_log_id = null; 18 18 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(); 22 21 23 22 public function __construct( $db = null ) { … … 76 75 */ 77 76 public function capture_outgoing_email( $args ) { 77 $this->smtp_transcript = array(); 78 78 79 if ( ! $this->db ) { 79 80 return $args; … … 108 109 $args['headers'] = $headers; 109 110 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( 118 112 'subject' => isset( $args['subject'] ) ? $args['subject'] : '', 119 113 'from_address' => $from_address, … … 121 115 'status' => 'pending', 122 116 '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 ); 124 123 125 124 return $args; … … 132 131 * @param \PHPMailer\PHPMailer\PHPMailer $phpmailer 133 132 */ 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 134 186 public function on_phpmailer_init( $phpmailer ) { 135 187 $smtp_enabled = (bool) get_option( 'bcs_smtp_enabled' ); … … 137 189 if ( $smtp_enabled ) { 138 190 $this->configure_phpmailer( $phpmailer ); 191 $this->attach_transcript_capture( $phpmailer ); 139 192 } 140 193 … … 165 218 public function on_email_failed( $error ) { 166 219 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' ); 168 221 $this->db->update_email_log_status( $this->pending_log_id, 'failed', $msg ); 169 222 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; 227 299 } 228 300 … … 232 304 233 305 /** 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). 236 330 * 237 331 * @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. 239 333 */ 240 334 public function encrypt_password( $plain ) { … … 242 336 return $plain; 243 337 } 244 $key = hash( 'sha256', wp_salt( 'auth' ), true);338 $key = $this->get_crypto_key(); 245 339 $iv = random_bytes( 16 ); 246 340 $cipher = openssl_encrypt( $plain, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv ); … … 248 342 return $plain; 249 343 } 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. 251 346 } 252 347 253 348 /** 254 349 * 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. 257 354 * 258 355 * @param string $stored 259 * @return string 356 * @return string Plaintext, or '' when the value can't be decrypted. 260 357 */ 261 358 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:' ) ) { 263 381 return $stored; 264 382 } … … 274 392 $key = hash( 'sha256', wp_salt( 'auth' ), true ); 275 393 $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; 277 402 } 278 403 … … 280 405 public function get_smtp_password() { 281 406 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' ); 282 421 } 283 422 … … 307 446 $phpmailer->Host = $host; 308 447 $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 } 309 454 310 455 switch ( $encryption ) { … … 339 484 340 485 /* ─────────────────────────────────────────────────────────────────── */ 341 /* SMTP Plugin Detection (used by Config tab)*/486 /* SMTP Plugin Detection (used by the Settings tab) */ 342 487 /* ─────────────────────────────────────────────────────────────────── */ 343 488 … … 503 648 return array_merge( $base, array( 504 649 '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' ), 506 651 'failure_type' => 'not_configured', 507 652 ) ); … … 513 658 return array_merge( $base, array( 514 659 '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' ), 516 661 'failure_type' => 'mail_unavailable', 517 662 ) ); … … 524 669 } 525 670 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. 526 678 $encryption = strtolower( $info['encryption'] ?: 'tls' ); 527 679 $timeout = 8; … … 630 782 } 631 783 632 // Test SMTP AUTH LOGIN when credentials are configured. Reachable for SSL, successfully-upgraded633 // TLS, and also plain/'none' connections — matching exactly what a real send would attempt, so a634 // 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 675 784 fwrite( $socket, "QUIT\r\n" ); 676 785 fclose( $socket ); … … 684 793 } 685 794 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 686 918 /* ─────────────────────────────────────────────────────────────────── */ 687 919 /* Manual Test Email Send */ … … 711 943 712 944 $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 } 713 958 714 959 $captured_error = ''; … … 745 990 } 746 991 992 $error_msg = $captured_error ? $this->describe_failure( $captured_error ) : 'wp_mail() returned false — no additional error details.'; 747 993 return array( 748 994 '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 ), 751 997 'smtp_host' => $info['host'], 752 998 'smtp_port' => $info['port'], … … 797 1043 798 1044 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 ) ); 849 1076 } 850 1077 … … 862 1089 } 863 1090 // 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'; 864 1097 $response = wp_remote_get( 'https://api.ipify.org', array( 'timeout' => 5 ) ); 865 1098 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; 870 1104 } 871 1105 } -
bcs-smtp-manager/trunk/includes/class-bcs-smtp-monitor.php
r3579822 r3762759 44 44 45 45 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 } 48 109 } 49 110 50 111 public function maybe_schedule_event() { 51 $enabled = get_option( BCS_SMTP_OPT_MONITOR_ENABLED, false);112 $enabled = self::is_active(); 52 113 $next = wp_next_scheduled( 'bcs_smtp_health_check' ); 53 114 … … 64 125 /* ─────────────────────────────────────────────────────────────────── */ 65 126 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; 69 135 } 70 136 71 137 // 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; 74 140 } 75 141 $mins = $this->get_interval_minutes(); … … 81 147 update_option( BCS_SMTP_OPT_LAST_CHECK, time() ); 82 148 update_option( BCS_SMTP_OPT_LAST_CHECK_SUCCESS, $result['success'] ); 149 $this->schedule_next_check(); 83 150 84 151 if ( $result['success'] ) { … … 89 156 90 157 delete_transient( 'bcs_smtp_check_lock' ); 158 return $result; 91 159 } 92 160 … … 104 172 update_option( BCS_SMTP_OPT_CONSECUTIVE_FAILURES, $failures ); 105 173 106 $threshold = (int) get_option( BCS_SMTP_OPT_FAILURE_THRESHOLD, 2 );174 $threshold = (int) BCS_SMTP_FAILURE_THRESHOLD; 107 175 108 176 if ( $failures >= $threshold ) { … … 149 217 /* ─────────────────────────────────────────────────────────────────── */ 150 218 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 ) { 152 229 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; 163 240 $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 ) ); 189 298 } 190 299 … … 213 322 if ( 'teams' === $channel ) { 214 323 // 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 ) { 216 325 if ( $host === $suffix || ( strlen( $host ) > strlen( $suffix ) + 1 && '.' . $suffix === substr( $host, -( strlen( $suffix ) + 1 ) ) ) ) { 217 326 return true; … … 250 359 /* ─────────────────────────────────────────────────────────────────── */ 251 360 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 ) { 253 372 $info = $this->mailer->detect_smtp_plugin(); 254 373 $site_name = get_bloginfo( 'name' ); 255 374 $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 } 259 404 260 405 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( '&', '<', '>' ), (string) $text ); 275 454 } 276 455 … … 282 461 $webhook = get_option( BCS_SMTP_OPT_SLACK_WEBHOOK_URL, '' ); 283 462 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, 315 487 ) ); 316 488 } … … 323 495 $webhook = get_option( BCS_SMTP_OPT_TEAMS_WEBHOOK_URL, '' ); 324 496 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( 334 527 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, 346 536 ), 347 'markdown' => true,348 537 ), 349 538 ), 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,362 539 ) ); 363 540 } … … 370 547 $webhook = get_option( BCS_SMTP_OPT_GOOGLE_CHAT_WEBHOOK_URL, '' ); 371 548 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 ) ) ); 404 564 } 405 565 … … 411 571 $webhook = get_option( BCS_SMTP_OPT_DISCORD_WEBHOOK_URL, '' ); 412 572 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 ), 446 593 ) ); 447 594 } … … 459 606 $webhook = get_option( BCS_SMTP_OPT_CUSTOM_WEBHOOK_URL, '' ); 460 607 if ( empty( $webhook ) || ! self::is_allowed_webhook_host( $webhook, 'custom' ) ) { 461 return ;608 return $this->missing_webhook_error( 'custom' ); 462 609 } 463 610 464 611 $body = array( 465 612 'event' => 'smtp_failure', 613 'test' => $payload['is_test'], 466 614 'site_name' => $payload['site_name'], 467 615 'site_url' => $payload['site_url'], … … 478 626 ); 479 627 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 ); 485 629 } 486 630 } -
bcs-smtp-manager/trunk/includes/class-bcs-smtp-wp-cli.php
r3652946 r3762759 10 10 11 11 /** 12 * Manage ByteCoreStack SMTP email sending , resending,and the email log.12 * Manage ByteCoreStack SMTP email sending and the email log. 13 13 */ 14 14 class BCS_SMTP_WP_CLI_Command { … … 38 38 // registered the real mailer's wp_mail/phpmailer_init/wp_mail_failed hooks, which is what 39 39 // 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. 41 41 $mailer = new BCS_SMTP_Mailer(); 42 42 $result = $mailer->send_test_email( $to ); … … 50 50 51 51 /** 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. 56 53 * 57 54 * ## OPTIONS 58 55 * 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 * --- 93 63 * 94 64 * [--status=<status>] -
bcs-smtp-manager/trunk/readme.txt
r3652946 r3762759 1 1 === ByteCoreStack - SMTP & Health Monitor === 2 2 Contributors: bytecorestack 3 Tags: smtp, wordpress smtp, email log, smtpalerts, email monitoring3 Tags: smtp, email log, email, email alerts, email monitoring 4 4 Requires at least: 5.3 5 Tested up to: 7. 05 Tested up to: 7.1 6 6 Requires PHP: 7.2 7 Stable tag: 1. 2.17 Stable tag: 1.3.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 The WordPress SMTP plugin that logs every email, monitors delivery health, and alerts you on Slack, Teams, Discord & Google Chat when itfails.11 Fix WordPress email with SMTP, log every email with its full message, and get Slack, Teams, Discord or Google Chat alerts when sending fails. 12 12 13 13 == Description == 14 14 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 19 Out 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 23 1. Pick your provider. The server, port and encryption fill in for you, with sign-in tips for that provider. 24 2. 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. 25 3. 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 64 WooCommerce, 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 68 Any 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) 64 72 * [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) 65 74 * [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)67 75 * [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 81 Everything 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 85 Plugin 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 89 The 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). 78 90 79 91 == Installation == 80 92 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. Op en 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.93 1. 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/`. 94 2. Activate the plugin. 95 3. 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). 96 4. Enter your username and password (or app password / API key), then click **Save and test connection**. 97 5. Click **Send test email** to confirm a real message arrives. 98 6. 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. 87 99 88 100 == Frequently Asked Questions == 89 101 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 104 The 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 108 SMTP 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 112 Yes. 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 116 Yes. 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 120 Yes. 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 124 Yes. 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 128 Yes. 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 132 Each 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 136 Health 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 140 On 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 148 Yes. 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 152 On 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 156 No. 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 160 Yes. 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. 157 161 158 162 = 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 Th is 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 164 Yes. 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 168 Yes, 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 172 Yes. `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 176 The 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)** 179 All 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)** 182 If 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". 179 183 * Service: Slack Technologies, LLC 184 * Terms of service: https://slack.com/terms-of-service 180 185 * 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)** 188 If 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". 185 189 * Service: Microsoft Corporation 190 * Terms of service: https://www.microsoft.com/en-us/servicesagreement 186 191 * Privacy policy: https://privacy.microsoft.com/en-us/privacystatement 192 193 **4. Google Chat (optional)** 194 If 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)** 200 If 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)** 206 If 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)** 209 The 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)** 214 When 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 187 216 * 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 LLC192 * Privacy policy: https://policies.google.com/privacy193 * Terms of service: https://policies.google.com/terms194 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/privacy199 * Terms of service: https://discord.com/terms200 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 Corporation212 217 * Privacy policy: https://privacy.microsoft.com/en-us/privacystatement 213 218 214 No data is collected, transmitted to, or processed by ByteCore Stack servers.215 216 219 == Screenshots == 217 220 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 221 1. Overview — whether mail is flowing, last and next health check, email activity chart and what's left to set up 222 2. Connection — pick your provider and the server details fill in; send a test email right alongside 223 3. Alerts — automatic health checks and failure alerts to Slack, Teams, Google Chat, Discord or a webhook 224 4. Reports — delivery chart, totals, highlights and the monthly report email 225 5. Email log — grouped by day, with status filters, counts and search; open any email to see the full message 226 6. Email details — the full message as it was sent, with delivery status, recipients and headers 228 227 229 228 == 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. 230 266 231 267 = 1.2.1 = … … 234 270 **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. 235 271 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. 237 273 238 274 **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. 239 275 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 previouslymade 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. 243 279 244 280 **Readme fix** — `Tested up to` now uses major.minor format (`7.0`) instead of including a patch version, per the WordPress.org readme spec. 245 281 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. 282 Older versions are listed in changelog.txt, included with the plugin. 273 283 274 284 == Upgrade Notice == 285 286 = 1.3.0 = 287 Redesigned 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. 275 288 276 289 = 1.2.1 = 277 290 First 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 1 1 <?php 2 2 /** 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. 4 4 * 5 5 * @package BCS_SMTP_Manager … … 10 10 } 11 11 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 */ 15 function 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_' ) . '%' ) ); 16 88 } 17 89 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. 91 if ( 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(); 28 99 } 29 // phpcs:enable30 31 // Delete all options32 $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.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)