get( 'general', self::SETTINGS_SLUG ) ); } /** * Load usage tracking functionality. * * @since 2.3.0 */ public function load() { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks // Check if loading the usage tracking functionality is allowed. if ( ! (bool) apply_filters( 'wp_mail_smtp_usage_tracking_load_allowed', true ) ) { return; } // Deregister the action if option is disabled. add_action( 'wp_mail_smtp_options_set_after', function () { if ( ! $this->is_enabled() ) { ( new SendUsageTask() )->cancel(); } } ); // Register the action handler and error stats tracking only if enabled. if ( $this->is_enabled() ) { /** * Filter whether to enable error stats collection. * * @since 4.8.0 * * @param bool $enabled Whether error stats collection is enabled. Default true. */ if ( apply_filters( 'wp_mail_smtp_usage_tracking_error_stats_enabled', true ) ) { // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName ( new ErrorStats() )->hooks(); } /** * Filter whether to enable sent stats collection. * * @since 4.10.0 * * @param bool $enabled Whether sent stats collection is enabled. Default true. */ if ( apply_filters( 'wp_mail_smtp_usage_tracking_sent_stats_enabled', true ) ) { // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName ( new SentStats() )->hooks(); } add_action( 'admin_init', [ $this, 'track_admin_activity' ] ); add_action( 'wp_mail_smtp_test_email_send_after', [ $this, 'track_test_email' ] ); add_filter( 'wp_mail_smtp_tasks_get_tasks', static function ( $tasks ) { $tasks[] = SendUsageTask::class; return $tasks; } ); } } /** * Stamp the last WP Mail SMTP admin page visit, at most once per day. * * Hooked to admin_init and gated to the plugin's own admin pages. * * @since 4.10.0 */ public function track_admin_activity() { if ( ! wp_mail_smtp()->get_admin()->is_admin_page() ) { return; } $last = (int) get_option( self::LAST_ADMIN_ACTIVITY_OPTION, 0 ); if ( ( time() - $last ) < DAY_IN_SECONDS ) { return; } update_option( self::LAST_ADMIN_ACTIVITY_OPTION, time(), false ); } /** * Stamp the last test email attempt, whatever its outcome. * * @since 4.10.0 */ public function track_test_email() { update_option( self::LAST_TEST_EMAIL_OPTION, time(), false ); } /** * Get the User Agent string that will be sent to the API. * * @since 2.3.0 * * @return string */ public function get_user_agent() { return Helpers::get_default_user_agent(); } /** * Get data for sending to the server. * * @since 2.3.0 * * @return array */ public function get_data() { global $wpdb; $theme_data = wp_get_theme(); $options = Options::init(); $mailer = wp_mail_smtp()->get_providers()->get_mailer( $options->get( 'mail', 'mailer' ), wp_mail_smtp()->get_processor()->get_phpmailer() ); $setup_wizard_stats = SetupWizardStats::get(); $data = array_merge( $this->get_required_data(), $this->get_additional_data(), [ // Generic data (environment). 'mysql_version' => $wpdb->db_version(), 'server_version' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '', 'is_ssl' => is_ssl(), 'is_multisite' => is_multisite(), 'sites_count' => $this->get_sites_total(), 'theme_name' => $theme_data->name, 'theme_version' => $theme_data->version, 'locale' => get_locale(), 'timezone_offset' => $this->get_timezone_offset(), // WP Mail SMTP - specific data. 'wp_mail_smtp_version' => WPMS_PLUGIN_VER, 'wp_mail_smtp_activated' => get_option( 'wp_mail_smtp_activated_time', 0 ), 'wp_mail_smtp_mailer' => $options->get( 'mail', 'mailer' ), 'wp_mail_smtp_setup_type' => $this->get_setup_type( $options ), 'wp_mail_smtp_from_email_force' => (bool) $options->get( 'mail', 'from_email_force' ), 'wp_mail_smtp_from_name_force' => (bool) $options->get( 'mail', 'from_name_force' ), 'wp_mail_smtp_return_path' => (bool) $options->get( 'mail', 'return_path' ), 'wp_mail_smtp_do_not_send' => (bool) $options->get( 'general', 'do_not_send' ), 'wp_mail_smtp_is_white_labeled' => wp_mail_smtp()->is_white_labeled(), 'wp_mail_smtp_is_const_enabled' => (bool) $options->is_const_enabled(), 'wp_mail_smtp_conflicts_is_detected' => ( new Conflicts() )->is_detected(), 'wp_mail_smtp_is_mailer_complete' => empty( $mailer ) ? false : $mailer->is_mailer_complete(), 'wp_mail_smtp_setup_wizard_launched_time' => (int) $setup_wizard_stats['launched_time'], 'wp_mail_smtp_setup_wizard_completed_time' => (int) $setup_wizard_stats['completed_time'], 'wp_mail_smtp_setup_wizard_completed_successfully' => ! empty( $setup_wizard_stats['was_successful'] ), 'wp_mail_smtp_setup_wizard_mailer' => $setup_wizard_stats['mailer'], 'wp_mail_smtp_setup_wizard_type' => $setup_wizard_stats['type'], 'wp_mail_smtp_source' => sanitize_title( get_option( 'wp_mail_smtp_source', '' ) ), 'wp_mail_smtp_optimize_email_sending' => (bool) OptimizedEmailSending::is_enabled(), 'wp_mail_smtp_last_admin_activity' => (int) get_option( self::LAST_ADMIN_ACTIVITY_OPTION, 0 ), 'wp_mail_smtp_last_test_email_time' => (int) get_option( self::LAST_TEST_EMAIL_OPTION, 0 ), 'wp_mail_smtp_wpvibe_state' => $this->get_wpvibe_state(), 'wp_mail_smtp_recommended_plugins_activated_count' => count( (array) get_option( RecommendedPlugins::ACTIVATED_OPTION, [] ) ), 'wp_mail_smtp_wpcode_active' => function_exists( 'wpcode_get_library_snippets_by_username' ), 'wp_mail_smtp_abilities_api_available' => wp_mail_smtp()->get_abilities_registrar()->allow_load(), ] ); if ( 'smtp' === $options->get( 'mail', 'mailer' ) ) { $data['wp_mail_smtp_other_smtp_host'] = $options->get( 'smtp', 'host' ); $data['wp_mail_smtp_other_smtp_encryption'] = $options->get( 'smtp', 'encryption' ); $data['wp_mail_smtp_other_smtp_port'] = $options->get( 'smtp', 'port' ); $data['wp_mail_smtp_other_smtp_auth'] = (bool) $options->get( 'smtp', 'auth' ); $data['wp_mail_smtp_other_smtp_autotls'] = (bool) $options->get( 'smtp', 'autotls' ); } if ( is_multisite() ) { $use_global_settings = WP::use_global_plugin_settings(); $data['wp_mail_smtp_multisite_network_wide'] = $use_global_settings; if ( ! $use_global_settings ) { $data['wp_mail_smtp_multisite_is_subsite'] = ! is_main_site(); } } return apply_filters( 'wp_mail_smtp_usage_tracking_get_data', $data ); } /** * Resolve the WPVibe install state: not installed, installed but inactive, or active. * * Duplicates the detection in Admin\Pages\AiMcpTab until partner plugin * state helpers are consolidated into a shared class. * * @since 4.10.0 * * @return string One of 'not_installed', 'installed_inactive', 'active'. */ private function get_wpvibe_state() { $wpvibe_basename = 'vibe-ai/vibe-ai.php'; if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $plugins = get_plugins(); if ( ! array_key_exists( $wpvibe_basename, $plugins ) ) { return 'not_installed'; } if ( ! is_plugin_active( $wpvibe_basename ) ) { return 'installed_inactive'; } return 'active'; } /** * How the active mailer was set up: 'quick_connect' (Gmail/Outlook One-Click or * SendLayer Quick Connect), 'manual' for any other configured mailer, or '' when none is set. * * @since 4.9.0 * * @param Options $options Plugin options. * * @return string */ private function get_setup_type( Options $options ) { $mailer = $options->get( 'mail', 'mailer' ); if ( empty( $mailer ) || $mailer === 'mail' ) { return ''; } $is_quick_connect = ( in_array( $mailer, [ 'gmail', 'outlook' ], true ) && $options->get( $mailer, 'one_click_setup_enabled' ) ) || ( $mailer === 'sendlayer' && $options->get( 'sendlayer', 'quick_connect' ) ); return $is_quick_connect ? 'quick_connect' : 'manual'; } /** * Get the required request data. * * @since 3.1.0 * * @return array */ private function get_required_data() { return [ 'url' => home_url(), 'php_version' => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION, 'wp_version' => get_bloginfo( 'version' ), 'active_plugins' => $this->get_active_plugins(), ]; } /** * Get the additional data required by the usage tracking API. * * @since 3.1.0 * * @return array */ private function get_additional_data() { $activated_dates = get_option( 'wp_mail_smtp_activated', [] ); return [ 'wp_mail_smtp_license_key' => wp_mail_smtp()->get_license_key(), 'wp_mail_smtp_license_type' => wp_mail_smtp()->get_license_type(), 'wp_mail_smtp_is_pro' => wp_mail_smtp()->is_pro(), 'wp_mail_smtp_lite_installed_date' => $this->get_installed( $activated_dates, 'lite' ), 'wp_mail_smtp_pro_installed_date' => $this->get_installed( $activated_dates, 'pro' ), ]; } /** * Get timezone offset. * We use `wp_timezone_string()` when it's available (WP 5.3+), * otherwise fallback to the same code, copy-pasted. * * @since 2.3.0 * * @return string */ private function get_timezone_offset() { // It was added in WordPress 5.3. if ( function_exists( 'wp_timezone_string' ) ) { return wp_timezone_string(); } /* * The code below is basically a copy-paste from that function. */ $timezone_string = get_option( 'timezone_string' ); if ( $timezone_string ) { return $timezone_string; } $offset = (float) get_option( 'gmt_offset' ); $hours = (int) $offset; $minutes = ( $offset - $hours ); $sign = ( $offset < 0 ) ? '-' : '+'; $abs_hour = abs( $hours ); $abs_mins = abs( $minutes * 60 ); return sprintf( '%s%02d:%02d', $sign, $abs_hour, $abs_mins ); } /** * Get the list of active plugins. * * @since 2.3.0 * * @return array */ private function get_active_plugins() { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $active_plugins = []; foreach ( get_mu_plugins() as $path => $plugin ) { $active_plugins[ $path ] = isset( $plugin['Version'] ) ? $plugin['Version'] : 'Not Set'; } foreach ( get_plugins() as $path => $plugin ) { if ( is_plugin_active( $path ) ) { $active_plugins[ $path ] = isset( $plugin['Version'] ) ? $plugin['Version'] : 'Not Set'; } } return $active_plugins; } /** * Installed date. * * @since 2.3.0 * * @param array $activated_dates Input array with dates. * @param string $key Input key what you want to get. * * @return mixed */ private function get_installed( $activated_dates, $key ) { if ( ! empty( $activated_dates[ $key ] ) ) { return $activated_dates[ $key ]; } return false; } /** * Total number of sites. * * @since 2.3.0 * * @return int */ private function get_sites_total() { return function_exists( 'get_blog_count' ) ? (int) get_blog_count() : 1; } /** * Send failed Setup Wizard usage tracking data, if usage tracking is enabled. * * @since 3.1.0 * * @param null|DomainChecker $domain_checker The optional DomainChecker object. */ public function send_failed_setup_wizard_usage_tracking_data( $domain_checker = null ) { if ( ! $this->is_enabled() ) { return; } $options = Options::init(); $data = array_merge( $this->get_required_data(), $this->get_additional_data(), [ 'wp_mail_smtp_mailer' => $options->get( 'mail', 'mailer' ), 'wp_mail_smtp_mail_error' => EmailSendingDebug::get_message( 'primary' ), 'wp_mail_smtp_mail_error_code' => $this->get_failed_setup_wizard_error_code( $domain_checker ), ], $this->get_domain_checker_results( $domain_checker ) ); wp_remote_post( self::FAILED_SETUP_WIZARD_DATA_URL, [ 'timeout' => 5, 'redirection' => 5, 'httpversion' => '1.1', 'blocking' => true, 'body' => $data, 'user-agent' => $this->get_user_agent(), ] ); } /** * Get the normalized error code for a failed Setup Wizard mailer test. * * Prefers the composite error key captured on the send-failure path: unlike the * raw PHPMailer message it is the same for the same error on any site locale. * Failures that never reach the send path have no key, so they fall back to a * `wizard:{category}:-` code - a wizard failure is never reported without one. * * @since 4.10.0 * * @param DomainChecker|null $domain_checker The Domain Checker object. * * @return string */ private function get_failed_setup_wizard_error_code( $domain_checker ) { $record = EmailSendingDebug::get( 'primary' ); if ( ! empty( $record['error_key'] ) ) { return $record['error_key']; } if ( is_a( $domain_checker, DomainChecker::class ) && $domain_checker->has_errors() ) { return 'wizard:domain_check:-'; } return 'wizard:unknown:-'; } /** * Reformat the domain checker results, so it can be submitted to the usage tracking API. * * @since 3.1.0 * * @param DomainChecker|null $domain_checker The Domain Checker object. * * @return array */ private function get_domain_checker_results( $domain_checker ) { if ( ! is_a( $domain_checker, DomainChecker::class ) ) { return []; } $data = []; $results = $domain_checker->get_results(); $data['wp_mail_smtp_domain_checker_success'] = isset( $results['success'] ) ? (bool) $results['success'] : false; $data['wp_mail_smtp_domain_checker_message'] = isset( $results['message'] ) ? $results['message'] : ''; // Return early if checks are not available. if ( empty( $results['checks'] ) || ! is_array( $results['checks'] ) ) { return $data; } foreach ( $results['checks'] as $check ) { if ( empty( $check['type'] ) || empty( $check['state'] ) ) { continue; } $data[ 'wp_mail_smtp_domain_checker_check_' . $check['type'] ] = $check['state']; } return $data; } }