$type, 'message' => $message, 'extra_url' => $extra_url, ), 180 ); } public function aio_handle_oauth_callback() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['page'] ) || 'aio-settings' !== $_GET['page'] ) { return; } if ( ! current_user_can( 'manage_options' ) ) { return; } $redirect_uri = admin_url( 'admin.php?page=aio-settings&tab=smtp' ); // Handle provider-returned OAuth error (e.g. access_denied) // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['error'] ) && isset( $_GET['state'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $err_code = sanitize_text_field( wp_unslash( $_GET['error'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $err_desc = isset( $_GET['error_description'] ) ? sanitize_text_field( wp_unslash( $_GET['error_description'] ) ) : $err_code; $this->set_oauth_notice( 'error', sprintf( /* translators: %s: OAuth provider error description */ __( 'OAuth authorization failed: %s', 'aio-tools' ), $err_desc ) ); wp_safe_redirect( $redirect_uri ); exit; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['code'] ) || ! isset( $_GET['state'] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $code = sanitize_text_field( wp_unslash( $_GET['code'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $raw_state = sanitize_text_field( wp_unslash( $_GET['state'] ) ); $parts = explode( '|', $raw_state, 2 ); $provider = $parts[0] ?? ''; $nonce = $parts[1] ?? ''; if ( ! in_array( $provider, array( 'google', 'microsoft' ), true ) || empty( $nonce ) || ! wp_verify_nonce( $nonce, 'aio_smtp_oauth_' . $provider ) ) { $this->set_oauth_notice( 'error', __( 'Invalid or expired OAuth state token. Please try authenticating again.', 'aio-tools' ) ); wp_safe_redirect( $redirect_uri ); exit; } $settings = get_option( 'aio_options', array() ); $creds = AIO_Mailer::get_oauth_credentials( $provider, $settings ); if ( '' === $creds['client_id'] || '' === $creds['client_secret'] ) { $this->set_oauth_notice( 'error', __( 'Missing OAuth Client ID or Client Secret. Please save your credentials first.', 'aio-tools' ) ); wp_safe_redirect( $redirect_uri ); exit; } $token_url = 'google' === $provider ? AIO_Mailer::GOOGLE_TOKEN_URL : AIO_Mailer::MS_TOKEN_URL; $response = wp_remote_post( $token_url, array( 'timeout' => 15, 'body' => array( 'client_id' => $creds['client_id'], 'client_secret' => $creds['client_secret'], 'code' => $code, 'redirect_uri' => $redirect_uri, 'grant_type' => 'authorization_code', ), ) ); if ( is_wp_error( $response ) ) { $this->set_oauth_notice( 'error', sprintf( /* translators: %s: HTTP connection error */ __( 'Failed to connect to OAuth token server: %s', 'aio-tools' ), $response->get_error_message() ) ); wp_safe_redirect( $redirect_uri ); exit; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); $access_token = isset( $body['access_token'] ) ? (string) $body['access_token'] : ''; $refresh_token = isset( $body['refresh_token'] ) ? (string) $body['refresh_token'] : ''; // Fallback to existing saved refresh_token if provider omitted it on quick re-consent if ( '' === $refresh_token && '' !== $creds['refresh_token'] && '' !== $access_token ) { $refresh_token = $creds['refresh_token']; } if ( '' !== $refresh_token ) { AIO_Mailer::clear_cached_access_token( $provider, $settings ); $settings['opt-mailer'] = $provider; $settings['opt-module-smtp'] = true; $settings[ 'optOAuthRefreshToken_' . $provider ] = AIO_Crypto::encrypt( $refresh_token ); $id_token_email = ! empty( $body['id_token'] ) ? AIO_Mailer::extract_email_from_id_token( (string) $body['id_token'] ) : ''; $granted_scopes = ! empty( $body['scope'] ) ? sanitize_text_field( (string) $body['scope'] ) : ''; $connected_email = $id_token_email; $profile = array( 'api_ready' => true, 'enable_api_url' => '', ); if ( '' !== $access_token ) { $ttl = isset( $body['expires_in'] ) ? max( 60, (int) $body['expires_in'] - 120 ) : 3000; $cache_key = AIO_Mailer::get_token_cache_key( $provider, $creds['client_id'], $refresh_token ); set_transient( $cache_key, AIO_Crypto::encrypt( $access_token ), $ttl ); $profile = AIO_Mailer::discover_account_profile( $provider, $access_token, $id_token_email ); if ( ! empty( $profile['email'] ) ) { $connected_email = $profile['email']; } if ( ! empty( $profile['scopes'] ) ) { $granted_scopes = sanitize_text_field( $profile['scopes'] ); } } if ( '' !== $connected_email ) { $settings[ 'optOAuthEmail_' . $provider ] = $connected_email; if ( empty( $settings['txtSmtpMail'] ) ) { $settings['txtSmtpMail'] = $connected_email; } } if ( '' !== $granted_scopes ) { $settings[ 'optOAuthScopes_' . $provider ] = $granted_scopes; } update_option( 'aio_options', $settings ); if ( 'google' === $provider && isset( $profile['api_ready'] ) && false === $profile['api_ready'] && ! empty( $profile['enable_api_url'] ) ) { $this->set_oauth_notice( 'warning', sprintf( /* translators: %s: connected Google email address */ __( 'Connected to %s, but Gmail API is currently disabled in your Google Cloud project. Click the link below to enable Gmail API before sending emails:', 'aio-tools' ), '' !== $connected_email ? $connected_email : 'Google' ), $profile['enable_api_url'] ); } else { $this->set_oauth_notice( 'success', '' !== $connected_email ? sprintf( /* translators: %s: connected OAuth email address */ __( 'OAuth 2.0 connected successfully as %s! Ready to send via HTTPS REST API.', 'aio-tools' ), $connected_email ) : __( 'OAuth 2.0 account connected successfully!', 'aio-tools' ) ); } } else { $api_err = $body['error_description'] ?? ( $body['error'] ?? __( 'No refresh_token returned. Revoke app access in your Google/Microsoft account and re-authorize with consent.', 'aio-tools' ) ); $this->set_oauth_notice( 'error', sprintf( /* translators: %s: OAuth token exchange error */ __( 'OAuth token exchange failed: %s', 'aio-tools' ), sanitize_text_field( (string) $api_err ) ) ); } wp_safe_redirect( $redirect_uri ); exit; } /** * Live AJAX verification of OAuth 2.0 token, connected mailbox email, and Gmail/Graph REST API status. */ public function aio_verify_oauth_connection_ajax() { check_ajax_referer( 'aio-settings-nonce', 'security' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Unauthorized', 'aio-tools' ) ); } $provider = isset( $_POST['provider'] ) ? sanitize_key( wp_unslash( $_POST['provider'] ) ) : 'google'; if ( ! in_array( $provider, array( 'google', 'microsoft' ), true ) ) { wp_send_json_error( __( 'Invalid OAuth provider.', 'aio-tools' ) ); } $tok = AIO_Mailer::get_valid_access_token( $provider, true ); if ( empty( $tok['ok'] ) ) { wp_send_json_error( array( 'message' => $tok['error'] ?? __( 'Failed to verify OAuth token.', 'aio-tools' ), ) ); } $profile = AIO_Mailer::discover_account_profile( $provider, (string) $tok['access_token'], (string) ( $tok['id_token_email'] ?? '' ) ); wp_send_json_success( array( 'email' => $profile['email'] ?: ( $tok['connected_email'] ?? '' ), 'scopes' => $profile['scopes'] ?: ( $tok['scope'] ?? '' ), 'api_ready' => (bool) $profile['api_ready'], 'api_status_msg' => $profile['api_status_msg'], 'enable_api_url' => $profile['enable_api_url'], ) ); } public function w2wSmtpCheckHandler() { check_ajax_referer( 'aio-smtp-check-nonce', 'security' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'You do not have sufficient permissions to access this page.', 'aio-tools' ) ); } $recipient = isset( $_POST['your_email'] ) ? sanitize_email( wp_unslash( $_POST['your_email'] ) ) : ''; if ( ! is_email( $recipient ) ) { wp_send_json_error( __( 'Invalid email address.', 'aio-tools' ) ); } // Ensure Mailer hooks are active for this test even if SMTP module was toggled OFF if ( file_exists( AIO_MODULES_PATH . 'smtp/public-smtp.php' ) ) { include_once AIO_MODULES_PATH . 'smtp/public-smtp.php'; } AIO_Mailer::init_hooks(); AIO_Mailer::reset_diagnostics(); $module_active = function_exists( 'aio_is_module_active' ) ? aio_is_module_active( 'opt-module-smtp' ) : true; if ( ! $module_active ) { AIO_Mailer::log_trace( 'WARNING: SMTP Module is currently OFF on Dashboard. Test will run using your saved configuration, but enable the SMTP Module so live site emails also use it.' ); } $options = get_option( 'aio_options', array() ); $mailer_type = isset( $options['opt-mailer'] ) ? (string) $options['opt-mailer'] : 'smtp'; $subject = __( 'AIO Tools: SMTP & Mail API Test Connection', 'aio-tools' ); $headers = array( 'Content-Type: text/html; charset=UTF-8' ); $transport_badge = 'smtp' === $mailer_type ? 'Standard ESMTP' : ( 'google' === $mailer_type ? 'Google Gmail REST API (OAuth 2.0)' : 'Microsoft Graph API (OAuth 2.0)' ); $message = '
'; $smtp_lines = array(); $wp_mail_err = ''; $capture_err_cb = function ( $wp_error ) use ( &$wp_mail_err ) { if ( is_wp_error( $wp_error ) ) { $wp_mail_err = $wp_error->get_error_message(); } }; $debug_cb = function ( $phpmailer ) use ( &$smtp_lines ) { $phpmailer->SMTPDebug = 2; $phpmailer->Debugoutput = function ( $str ) use ( &$smtp_lines ) { $smtp_lines[] = rtrim( (string) $str ); }; }; add_action( 'phpmailer_init', $debug_cb, 999 ); add_action( 'wp_mail_failed', $capture_err_cb, 999 ); try { $result = wp_mail( $recipient, $subject, $message, $headers ); } catch ( \Throwable $e ) { $result = false; $wp_mail_err = $e->getMessage(); $smtp_lines[] = 'Exception: ' . $e->getMessage(); } remove_action( 'phpmailer_init', $debug_cb, 999 ); remove_action( 'wp_mail_failed', $capture_err_cb, 999 ); $api_trace = AIO_Mailer::get_last_trace_text(); $smtp_trace = ! empty( $smtp_lines ) ? AIO_Mailer::format_smtp_debug_log( implode( "\n", $smtp_lines ), $wp_mail_err ) : ''; $debug_log = trim( $api_trace . ( '' !== $api_trace && '' !== $smtp_trace ? "\n\n" : '' ) . $smtp_trace ); if ( ! $result ) { $human_error = AIO_Mailer::get_last_error(); if ( '' === $human_error ) { $human_error = '' !== $wp_mail_err ? $wp_mail_err : __( 'Failed to send email. Please review the diagnostic log below.', 'aio-tools' ); } wp_send_json_error( array( 'message' => $human_error, 'debug_log' => '' !== $debug_log ? $debug_log : $human_error, ) ); } wp_send_json_success( array( 'message' => __( 'Test email delivered successfully! Please check your inbox.', 'aio-tools' ), 'debug_log' => '' !== $debug_log ? $debug_log : __( 'Delivered successfully via wp_mail.', 'aio-tools' ), ) ); } public function aio_get_email_logs_ajax() { check_ajax_referer( 'aio-settings-nonce', 'security' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Unauthorized', 'aio-tools' ) ); } global $wpdb; $table = $wpdb->prefix . 'aio_email_logs'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); if ( $exists !== $table ) { wp_send_json_success( array( 'logs' => array(), 'sent' => 0, 'failed' => 0 ) ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $logs = $wpdb->get_results( "SELECT id, sent_at, recipient, subject, status, error_msg, mailer FROM {$table} ORDER BY id DESC LIMIT 50", ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $sent = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE status = 'sent'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $fail = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE status = 'failed'" ); wp_send_json_success( array( 'logs' => is_array( $logs ) ? $logs : array(), 'sent' => $sent, 'failed' => $fail, ) ); } public function aio_resend_email_log_ajax() { check_ajax_referer( 'aio-settings-nonce', 'security' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Unauthorized', 'aio-tools' ) ); } $log_id = isset( $_POST['log_id'] ) ? absint( $_POST['log_id'] ) : 0; if ( ! $log_id ) { wp_send_json_error( __( 'Invalid log ID.', 'aio-tools' ) ); } global $wpdb; $table = $wpdb->prefix . 'aio_email_logs'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $log_id ), ARRAY_A ); if ( ! $row ) { wp_send_json_error( __( 'Email log entry not found.', 'aio-tools' ) ); } if ( strpos( (string) $row['message'], '[REDACTED]' ) !== false ) { wp_send_json_error( __( 'For security, emails containing one-time password reset tokens cannot be resent. Please trigger a new reset request.', 'aio-tools' ) ); } if ( file_exists( AIO_MODULES_PATH . 'smtp/public-smtp.php' ) ) { include_once AIO_MODULES_PATH . 'smtp/public-smtp.php'; } AIO_Mailer::init_hooks(); $headers = array(); if ( ! empty( $row['headers'] ) ) { $headers = array_filter( array_map( 'trim', preg_split( '/\r\n|\r|\n/', (string) $row['headers'] ) ) ); } if ( empty( $headers ) && preg_match( '/<[a-z][\s\S]*>/i', (string) $row['message'] ) ) { $headers[] = 'Content-Type: text/html; charset=UTF-8'; } $sent = wp_mail( $row['recipient'], $row['subject'], $row['message'], $headers ); if ( $sent ) { wp_send_json_success( __( 'Email resent successfully!', 'aio-tools' ) ); } else { $err = AIO_Mailer::get_last_error(); wp_send_json_error( '' !== $err ? $err : __( 'Failed to resend email. Please check your SMTP configuration.', 'aio-tools' ) ); } } public function aio_clear_email_logs_ajax() { check_ajax_referer( 'aio-settings-nonce', 'security' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Unauthorized', 'aio-tools' ) ); } global $wpdb; $table = $wpdb->prefix . 'aio_email_logs'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "TRUNCATE TABLE {$table}" ); wp_send_json_success( __( 'Email delivery logs cleared.', 'aio-tools' ) ); } }