v1.2.0
This commit is contained in:
commit
c98dcb7b50
9 changed files with 5952 additions and 0 deletions
22
assets/css/admin.css
Normal file
22
assets/css/admin.css
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
.robotstxt-smtp-tools .card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.robotstxt-smtp-tools .robotstxt-smtp-tool-empty {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.robotstxt-smtp-tools .robotstxt-smtp-tool-meta {
|
||||||
|
margin: 1em 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.robotstxt-smtp-tools .robotstxt-smtp-tool-actions {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.robotstxt-smtp-tools .robotstxt-smtp-tool-result {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
52
assets/js/admin.js
Normal file
52
assets/js/admin.js
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
/**
|
||||||
|
* Admin UI behavior for Robotstxt SMTP settings.
|
||||||
|
*
|
||||||
|
* @package RobotstxtSMTP
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var initializeAdmin = function () {
|
||||||
|
var security = document.getElementById( 'robotstxt_smtp_security' );
|
||||||
|
var port = document.getElementById( 'robotstxt_smtp_port' );
|
||||||
|
|
||||||
|
if ( ! security || ! port) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultPorts = {
|
||||||
|
none: 25,
|
||||||
|
ssl: 465,
|
||||||
|
tls: 587
|
||||||
|
};
|
||||||
|
|
||||||
|
var previousValue = security.value || 'none';
|
||||||
|
|
||||||
|
security.addEventListener(
|
||||||
|
'change',
|
||||||
|
function () {
|
||||||
|
var newValue = security.value || 'none';
|
||||||
|
var previousDefault = Object.prototype.hasOwnProperty.call( defaultPorts, previousValue )
|
||||||
|
? defaultPorts[previousValue]
|
||||||
|
: null;
|
||||||
|
var newDefault = Object.prototype.hasOwnProperty.call( defaultPorts, newValue )
|
||||||
|
? defaultPorts[newValue]
|
||||||
|
: null;
|
||||||
|
var currentPort = parseInt( port.value, 10 );
|
||||||
|
|
||||||
|
if ( ! Number.isNaN( currentPort ) && null !== previousDefault && currentPort === previousDefault && null !== newDefault) {
|
||||||
|
port.value = newDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousValue = newValue;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if ('loading' !== document.readyState) {
|
||||||
|
initializeAdmin();
|
||||||
|
} else {
|
||||||
|
document.addEventListener( 'DOMContentLoaded', initializeAdmin );
|
||||||
|
}
|
||||||
|
})();
|
||||||
19
changelog.txt
Normal file
19
changelog.txt
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
= 1.2.0 =
|
||||||
|
|
||||||
|
* Logged failed email deliveries with status and error details in the log list and detail views.
|
||||||
|
* Captured the SMTP debug conversation for each email and surfaced it inside the log detail view.
|
||||||
|
* Fixed fatal errors in the SMTP bootstrap by restoring configuration and log cleanup hooks.
|
||||||
|
|
||||||
|
= 1.1.0 =
|
||||||
|
|
||||||
|
* Added Amazon SES credential fields, regional selection, and live validation helpers that appear when the Amazon SES add-on is active.
|
||||||
|
* Introduced the `robotstxt_smtp_sanitized_options` filter so add-ons can adjust sanitized settings before they are stored.
|
||||||
|
* Routed SMTP test messages and regular WordPress emails through Amazon SES whenever the add-on supplies valid credentials.
|
||||||
|
|
||||||
|
= 1.0.0 =
|
||||||
|
|
||||||
|
* Contextual help guidance in every SMTP configuration field.
|
||||||
|
* Automatic port updates when selecting an encryption method with standard values.
|
||||||
|
* Advanced tools: MX analysis, SPF/DKIM/DMARC validation, extended SMTP diagnostics, and blacklist monitoring.
|
||||||
|
* Enhanced logging with automatic cleanup by limit or age.
|
||||||
|
* Updated documentation for the 1.0.0 release with WordPress 6.7 and PHP 8.2 support.
|
||||||
664
includes/class-plugin.php
Normal file
664
includes/class-plugin.php
Normal file
|
|
@ -0,0 +1,664 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Main plugin bootstrap file.
|
||||||
|
*
|
||||||
|
* @package Robotstxt_SMTP
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Robotstxt_SMTP;
|
||||||
|
|
||||||
|
use PHPMailer\PHPMailer\PHPMailer;
|
||||||
|
use PHPMailer\PHPMailer\SMTP;
|
||||||
|
use Robotstxt_SMTP\Admin\Settings_Page;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main plugin class.
|
||||||
|
*/
|
||||||
|
class Plugin {
|
||||||
|
/**
|
||||||
|
* Custom post type used to store email logs.
|
||||||
|
*/
|
||||||
|
private const LOG_POST_TYPE = 'robotstxt_smtp_log';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cron hook used to trigger the log cleanup.
|
||||||
|
*/
|
||||||
|
private const CRON_HOOK = 'robotstxt_smtp_cleanup_logs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the class instance.
|
||||||
|
*
|
||||||
|
* @var Plugin|null
|
||||||
|
*/
|
||||||
|
private static ?Plugin $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings page handler.
|
||||||
|
*
|
||||||
|
* @var Settings_Page|null
|
||||||
|
*/
|
||||||
|
private ?Settings_Page $settings_page = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures the SMTP debug output for the current email.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
private array $current_debug_log = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the plugin instance.
|
||||||
|
*
|
||||||
|
* @return Plugin Plugin instance.
|
||||||
|
*/
|
||||||
|
public static function get_instance(): Plugin {
|
||||||
|
if ( null === self::$instance ) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether the Amazon SES integration plugin is active.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_amazon_ses_integration_active(): bool {
|
||||||
|
/**
|
||||||
|
* Filters whether the Amazon SES integration should be considered active.
|
||||||
|
*
|
||||||
|
* @since 1.0.1
|
||||||
|
*
|
||||||
|
* @param bool $is_active True when the Amazon SES add-on is active.
|
||||||
|
*/
|
||||||
|
return (bool) \apply_filters( 'robotstxt_smtp_amazon_ses_active', false );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the plugin.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function run(): void {
|
||||||
|
add_action( 'phpmailer_init', array( $this, 'configure_phpmailer' ) );
|
||||||
|
add_filter( 'wp_mail_from', array( $this, 'filter_mail_from' ) );
|
||||||
|
add_filter( 'wp_mail_from_name', array( $this, 'filter_mail_from_name' ) );
|
||||||
|
|
||||||
|
add_action( 'init', array( $this, 'register_log_post_type' ) );
|
||||||
|
add_action( 'init', array( $this, 'maybe_schedule_cleanup' ) );
|
||||||
|
add_action( 'wp_mail_succeeded', array( $this, 'handle_wp_mail_succeeded' ) );
|
||||||
|
add_action( 'wp_mail_failed', array( $this, 'handle_wp_mail_failed' ) );
|
||||||
|
add_action( self::CRON_HOOK, array( $this, 'cleanup_logs' ) );
|
||||||
|
|
||||||
|
if ( is_admin() ) {
|
||||||
|
$this->register_admin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the admin functionality.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function register_admin(): void {
|
||||||
|
if ( null === $this->settings_page ) {
|
||||||
|
$this->settings_page = new Settings_Page();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->settings_page->register_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the custom post type used to store email logs.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register_log_post_type(): void {
|
||||||
|
register_post_type(
|
||||||
|
self::LOG_POST_TYPE,
|
||||||
|
array(
|
||||||
|
'labels' => array(
|
||||||
|
'name' => esc_html__( 'SMTP Logs', 'robotstxt-smtp' ),
|
||||||
|
'singular_name' => esc_html__( 'SMTP Log', 'robotstxt-smtp' ),
|
||||||
|
),
|
||||||
|
'public' => false,
|
||||||
|
'show_ui' => false,
|
||||||
|
'show_in_menu' => false,
|
||||||
|
'show_in_nav_menus' => false,
|
||||||
|
'exclude_from_search' => true,
|
||||||
|
'publicly_queryable' => false,
|
||||||
|
'supports' => array( 'title', 'editor' ),
|
||||||
|
'capability_type' => 'post',
|
||||||
|
'map_meta_cap' => true,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules or clears the automatic log cleanup task based on settings.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function maybe_schedule_cleanup(): void {
|
||||||
|
$logging = $this->get_logging_settings();
|
||||||
|
|
||||||
|
if ( ! $logging['enabled'] ) {
|
||||||
|
wp_clear_scheduled_hook( self::CRON_HOOK );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( false === wp_next_scheduled( self::CRON_HOOK ) ) {
|
||||||
|
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', self::CRON_HOOK );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the "From" email address used by WordPress.
|
||||||
|
*
|
||||||
|
* @param string $original_email Original email address provided by WordPress.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function filter_mail_from( string $original_email ): string {
|
||||||
|
$settings = $this->get_mailer_settings();
|
||||||
|
$from_email = isset( $settings['from_email'] ) ? sanitize_email( $settings['from_email'] ) : '';
|
||||||
|
|
||||||
|
if ( empty( $from_email ) ) {
|
||||||
|
return sanitize_email( $original_email );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $from_email;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the "From" name used by WordPress.
|
||||||
|
*
|
||||||
|
* @param string $original_name Original name provided by WordPress.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function filter_mail_from_name( string $original_name ): string {
|
||||||
|
$settings = $this->get_mailer_settings();
|
||||||
|
$from_name = isset( $settings['from_name'] ) ? sanitize_text_field( $settings['from_name'] ) : '';
|
||||||
|
|
||||||
|
if ( '' === $from_name ) {
|
||||||
|
return sanitize_text_field( $original_name );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $from_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the stored mailer settings merged with defaults.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function get_mailer_settings(): array {
|
||||||
|
$defaults = Settings_Page::get_default_settings();
|
||||||
|
|
||||||
|
if ( Settings_Page::is_network_mode_enabled() ) {
|
||||||
|
$settings = get_site_option( Settings_Page::NETWORK_OPTION_NAME, array() );
|
||||||
|
} else {
|
||||||
|
$settings = get_option( Settings_Page::OPTION_NAME, array() );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! is_array( $settings ) ) {
|
||||||
|
$settings = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = wp_parse_args( $settings, $defaults );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the SMTP settings before they are consumed by the mailer.
|
||||||
|
*
|
||||||
|
* @since 1.1.1
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $settings Sanitized settings merged with defaults.
|
||||||
|
*/
|
||||||
|
return (array) \apply_filters( 'robotstxt_smtp_mailer_settings', $settings );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures PHPMailer to use the stored SMTP settings.
|
||||||
|
*
|
||||||
|
* @param PHPMailer $phpmailer Mailer instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function configure_phpmailer( PHPMailer $phpmailer ): void {
|
||||||
|
if ( self::is_amazon_ses_integration_active() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->current_debug_log = array();
|
||||||
|
|
||||||
|
$settings = $this->get_mailer_settings();
|
||||||
|
|
||||||
|
if ( empty( $settings['host'] ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$phpmailer->isSMTP();
|
||||||
|
|
||||||
|
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||||
|
$phpmailer->Host = $settings['host'];
|
||||||
|
$phpmailer->Port = (int) $settings['port'];
|
||||||
|
$phpmailer->SMTPAuth = ! empty( $settings['username'] ) || ! empty( $settings['password'] );
|
||||||
|
$phpmailer->Username = $phpmailer->SMTPAuth ? $settings['username'] : '';
|
||||||
|
$phpmailer->Password = $phpmailer->SMTPAuth ? $settings['password'] : '';
|
||||||
|
$phpmailer->SMTPAutoTLS = false;
|
||||||
|
$phpmailer->SMTPSecure = '';
|
||||||
|
// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||||
|
|
||||||
|
if ( 'ssl' === $settings['security'] ) {
|
||||||
|
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||||
|
$phpmailer->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
|
||||||
|
} elseif ( 'tls' === $settings['security'] ) {
|
||||||
|
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||||
|
$phpmailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
|
||||||
|
$phpmailer->SMTPAutoTLS = true;
|
||||||
|
// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->register_debug_logger( $phpmailer );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables SMTP debug logging for the current email.
|
||||||
|
*
|
||||||
|
* @param PHPMailer $phpmailer Mailer instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function register_debug_logger( PHPMailer $phpmailer ): void {
|
||||||
|
$logging = $this->get_logging_settings();
|
||||||
|
|
||||||
|
if ( ! $logging['enabled'] ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||||
|
$phpmailer->SMTPDebug = SMTP::DEBUG_SERVER;
|
||||||
|
$phpmailer->Debugoutput = function ( string $message, int $level ): void {
|
||||||
|
$this->current_debug_log[] = '[' . $level . '] ' . $message;
|
||||||
|
};
|
||||||
|
// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the email data when WordPress reports a successful delivery.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $mail_data Email data provided by wp_mail.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle_wp_mail_succeeded( array $mail_data ): void {
|
||||||
|
$logging = $this->get_logging_settings();
|
||||||
|
|
||||||
|
if ( ! $logging['enabled'] ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = $this->get_mailer_settings();
|
||||||
|
|
||||||
|
$subject = isset( $mail_data['subject'] ) ? sanitize_text_field( (string) $mail_data['subject'] ) : '';
|
||||||
|
$message = isset( $mail_data['message'] ) ? (string) $mail_data['message'] : '';
|
||||||
|
$headers = $this->normalize_headers( $mail_data['headers'] ?? array() );
|
||||||
|
$to = $this->normalize_recipients( $mail_data['to'] ?? array() );
|
||||||
|
$from = $this->determine_from_header( $headers, $settings );
|
||||||
|
|
||||||
|
$post_id = wp_insert_post(
|
||||||
|
array(
|
||||||
|
'post_type' => self::LOG_POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_title' => $subject,
|
||||||
|
'post_content' => $message,
|
||||||
|
'post_date' => current_time( 'mysql' ),
|
||||||
|
),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $post_id ) || 0 === $post_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_post_meta( $post_id, '_robotstxt_smtp_to', implode( ', ', $to ) );
|
||||||
|
update_post_meta( $post_id, '_robotstxt_smtp_from', $from );
|
||||||
|
update_post_meta( $post_id, '_robotstxt_smtp_headers', implode( "\n", $headers ) );
|
||||||
|
update_post_meta( $post_id, '_robotstxt_smtp_status', 'sent' );
|
||||||
|
update_post_meta( $post_id, '_robotstxt_smtp_error', '' );
|
||||||
|
|
||||||
|
if ( isset( $mail_data['attachments'] ) ) {
|
||||||
|
update_post_meta( $post_id, '_robotstxt_smtp_attachments', maybe_serialize( $mail_data['attachments'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$debug_log = $this->get_sanitized_debug_log();
|
||||||
|
|
||||||
|
if ( $debug_log ) {
|
||||||
|
update_post_meta( $post_id, '_robotstxt_smtp_debug_log', maybe_serialize( $debug_log ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( 'count' === $logging['mode'] ) {
|
||||||
|
$this->cleanup_logs_by_count( $logging['count'] );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cleanup_logs_by_days( $logging['days'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the email data when WordPress reports a failed delivery.
|
||||||
|
*
|
||||||
|
* @param WP_Error $error Error object reported by wp_mail.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle_wp_mail_failed( WP_Error $error ): void {
|
||||||
|
$logging = $this->get_logging_settings();
|
||||||
|
|
||||||
|
if ( ! $logging['enabled'] ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = $this->get_mailer_settings();
|
||||||
|
$data = $error->get_error_data();
|
||||||
|
|
||||||
|
if ( ! is_array( $data ) ) {
|
||||||
|
$data = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$subject = isset( $data['subject'] ) ? sanitize_text_field( (string) $data['subject'] ) : '';
|
||||||
|
$message = isset( $data['message'] ) ? (string) $data['message'] : '';
|
||||||
|
$headers = $this->normalize_headers( $data['headers'] ?? array() );
|
||||||
|
$to = $this->normalize_recipients( $data['to'] ?? array() );
|
||||||
|
$from = $this->determine_from_header( $headers, $settings );
|
||||||
|
$error_msg = sanitize_text_field( $error->get_error_message() );
|
||||||
|
|
||||||
|
$post_id = wp_insert_post(
|
||||||
|
array(
|
||||||
|
'post_type' => self::LOG_POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_title' => $subject,
|
||||||
|
'post_content' => $message,
|
||||||
|
'post_date' => current_time( 'mysql' ),
|
||||||
|
),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $post_id ) || 0 === $post_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_post_meta( $post_id, '_robotstxt_smtp_to', implode( ', ', $to ) );
|
||||||
|
update_post_meta( $post_id, '_robotstxt_smtp_from', $from );
|
||||||
|
update_post_meta( $post_id, '_robotstxt_smtp_headers', implode( "\n", $headers ) );
|
||||||
|
update_post_meta( $post_id, '_robotstxt_smtp_status', 'error' );
|
||||||
|
update_post_meta( $post_id, '_robotstxt_smtp_error', $error_msg );
|
||||||
|
|
||||||
|
if ( isset( $data['attachments'] ) ) {
|
||||||
|
update_post_meta( $post_id, '_robotstxt_smtp_attachments', maybe_serialize( $data['attachments'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$debug_log = $this->get_sanitized_debug_log();
|
||||||
|
|
||||||
|
if ( $debug_log ) {
|
||||||
|
update_post_meta( $post_id, '_robotstxt_smtp_debug_log', maybe_serialize( $debug_log ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( 'count' === $logging['mode'] ) {
|
||||||
|
$this->cleanup_logs_by_count( $logging['count'] );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cleanup_logs_by_days( $logging['days'] );
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Retrieves the sanitized SMTP debug output captured for the current email.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function get_sanitized_debug_log(): array {
|
||||||
|
if ( empty( $this->current_debug_log ) ) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$sanitized = array_map(
|
||||||
|
static function ( $entry ): string {
|
||||||
|
return sanitize_textarea_field( (string) $entry );
|
||||||
|
},
|
||||||
|
$this->current_debug_log
|
||||||
|
);
|
||||||
|
|
||||||
|
$sanitized = array_filter(
|
||||||
|
$sanitized,
|
||||||
|
static function ( string $line ): bool {
|
||||||
|
return '' !== trim( $line );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return array_values( $sanitized );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs log cleanup when triggered by cron.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function cleanup_logs(): void {
|
||||||
|
$logging = $this->get_logging_settings();
|
||||||
|
|
||||||
|
if ( ! $logging['enabled'] ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( 'count' === $logging['mode'] ) {
|
||||||
|
$this->cleanup_logs_by_count( $logging['count'] );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cleanup_logs_by_days( $logging['days'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all stored logs.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function clear_all_logs(): void {
|
||||||
|
do {
|
||||||
|
$posts = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => self::LOG_POST_TYPE,
|
||||||
|
'post_status' => 'any',
|
||||||
|
'fields' => 'ids',
|
||||||
|
'posts_per_page' => 100,
|
||||||
|
'orderby' => 'ID',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $posts as $post_id ) {
|
||||||
|
wp_delete_post( (int) $post_id, true );
|
||||||
|
}
|
||||||
|
} while ( ! empty( $posts ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up logs by enforcing a maximum number of stored records.
|
||||||
|
*
|
||||||
|
* @param int $limit Maximum number of logs to keep.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function cleanup_logs_by_count( int $limit ): void {
|
||||||
|
if ( $limit <= 0 ) {
|
||||||
|
$this->clear_all_logs();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs_to_delete = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => self::LOG_POST_TYPE,
|
||||||
|
'post_status' => 'any',
|
||||||
|
'fields' => 'ids',
|
||||||
|
'orderby' => 'date',
|
||||||
|
'order' => 'DESC',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'offset' => $limit,
|
||||||
|
'no_found_rows' => true,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $logs_to_delete as $post_id ) {
|
||||||
|
wp_delete_post( (int) $post_id, true );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up logs based on the maximum number of days to retain them.
|
||||||
|
*
|
||||||
|
* @param int $days Number of days to keep.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function cleanup_logs_by_days( int $days ): void {
|
||||||
|
if ( $days <= 0 ) {
|
||||||
|
$this->clear_all_logs();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cutoff = time() - ( $days * DAY_IN_SECONDS );
|
||||||
|
|
||||||
|
$logs_to_delete = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => self::LOG_POST_TYPE,
|
||||||
|
'post_status' => 'any',
|
||||||
|
'fields' => 'ids',
|
||||||
|
'date_query' => array(
|
||||||
|
array(
|
||||||
|
'column' => 'post_date_gmt',
|
||||||
|
'before' => gmdate( 'Y-m-d H:i:s', $cutoff ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'no_found_rows' => true,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $logs_to_delete as $post_id ) {
|
||||||
|
wp_delete_post( (int) $post_id, true );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes the headers array into a list of strings.
|
||||||
|
*
|
||||||
|
* @param mixed $headers Headers provided by wp_mail.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function normalize_headers( $headers ): array {
|
||||||
|
if ( empty( $headers ) ) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( is_string( $headers ) ) {
|
||||||
|
$headers = preg_split( "/\r\n|\r|\n/", $headers );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! is_array( $headers ) ) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = array();
|
||||||
|
|
||||||
|
foreach ( $headers as $header ) {
|
||||||
|
$line = trim( (string) $header );
|
||||||
|
|
||||||
|
if ( '' !== $line ) {
|
||||||
|
$normalized[] = $line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes the recipient list into an array of strings.
|
||||||
|
*
|
||||||
|
* @param mixed $recipients Recipient information from wp_mail.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function normalize_recipients( $recipients ): array {
|
||||||
|
if ( empty( $recipients ) ) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( is_string( $recipients ) ) {
|
||||||
|
$recipients = explode( ',', $recipients );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! is_array( $recipients ) ) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = array();
|
||||||
|
|
||||||
|
foreach ( $recipients as $recipient ) {
|
||||||
|
$normalized[] = sanitize_text_field( trim( (string) $recipient ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_filter( $normalized );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the "From" header for the logged message.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $headers Normalized header list.
|
||||||
|
* @param array<string, mixed> $settings Plugin settings.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function determine_from_header( array $headers, array $settings ): string {
|
||||||
|
foreach ( $headers as $header ) {
|
||||||
|
if ( 0 === stripos( $header, 'from:' ) ) {
|
||||||
|
return sanitize_text_field( trim( substr( $header, 5 ) ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$from_email = isset( $settings['from_email'] ) ? sanitize_email( $settings['from_email'] ) : '';
|
||||||
|
$from_name = isset( $settings['from_name'] ) ? sanitize_text_field( $settings['from_name'] ) : '';
|
||||||
|
|
||||||
|
if ( $from_email && $from_name ) {
|
||||||
|
return $from_name . ' <' . $from_email . '>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $from_email ) {
|
||||||
|
return $from_email;
|
||||||
|
}
|
||||||
|
|
||||||
|
return get_bloginfo( 'name', 'display' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the logging-related settings from the option store.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function get_logging_settings(): array {
|
||||||
|
$settings = $this->get_mailer_settings();
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'enabled' => ! empty( $settings['logs_enabled'] ),
|
||||||
|
'mode' => in_array( $settings['logs_retention_mode'], array( 'count', 'days' ), true )
|
||||||
|
? $settings['logs_retention_mode']
|
||||||
|
: 'count',
|
||||||
|
'count' => isset( $settings['logs_retention_count'] ) ? (int) $settings['logs_retention_count'] : 0,
|
||||||
|
'days' => isset( $settings['logs_retention_days'] ) ? (int) $settings['logs_retention_days'] : 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
4729
includes/class-settings-page.php
Normal file
4729
includes/class-settings-page.php
Normal file
File diff suppressed because it is too large
Load diff
107
includes/class-smtp-diagnostics-client.php
Normal file
107
includes/class-smtp-diagnostics-client.php
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* SMTP diagnostics client.
|
||||||
|
*
|
||||||
|
* @package Robotstxt_SMTP
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Robotstxt_SMTP\Admin;
|
||||||
|
|
||||||
|
use PHPMailer\PHPMailer\SMTP;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMTP client wrapper used for diagnostics.
|
||||||
|
*/
|
||||||
|
class SMTP_Diagnostics_Client extends SMTP {
|
||||||
|
/**
|
||||||
|
* Tracks whether the most recent command timed out.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private bool $last_command_timed_out = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes an SMTP command and returns the raw reply.
|
||||||
|
*
|
||||||
|
* @param string $name Command name.
|
||||||
|
* @param string $command_string Full command string.
|
||||||
|
* @param array<int,int> $expected_codes Expected success codes.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function execute_command( string $name, string $command_string, array $expected_codes = array( 250 ) ): array {
|
||||||
|
$this->last_command_timed_out = false;
|
||||||
|
$this->reset_error_state();
|
||||||
|
|
||||||
|
$expected = ! empty( $expected_codes ) ? array_map( 'intval', $expected_codes ) : array( 250 );
|
||||||
|
|
||||||
|
$success = parent::sendCommand( $name, $command_string, $expected );
|
||||||
|
|
||||||
|
if ( ! $success ) {
|
||||||
|
$this->last_command_timed_out = $this->error_indicates_timeout( parent::getError() );
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'success' => $success,
|
||||||
|
'reply' => $this->getLastReply(),
|
||||||
|
'timed_out' => $this->last_command_timed_out,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the last command timed out.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function did_last_command_timeout(): bool {
|
||||||
|
return $this->last_command_timed_out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates STARTTLS while tracking timeout state.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function startTLS(): bool {
|
||||||
|
$this->last_command_timed_out = false;
|
||||||
|
$this->reset_error_state();
|
||||||
|
|
||||||
|
$result = parent::startTLS();
|
||||||
|
|
||||||
|
if ( ! $result ) {
|
||||||
|
$this->last_command_timed_out = $this->error_indicates_timeout( parent::getError() );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the stored error state.
|
||||||
|
*/
|
||||||
|
private function reset_error_state(): void {
|
||||||
|
parent::setError( '' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether an error array represents a timeout.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $error Error details.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function error_indicates_timeout( array $error ): bool {
|
||||||
|
foreach ( array( 'error', 'detail', 'smtp_code_ex' ) as $key ) {
|
||||||
|
if ( empty( $error[ $key ] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = (string) $error[ $key ];
|
||||||
|
|
||||||
|
if ( false !== stripos( $value, 'timed out' ) || false !== stripos( $value, 'timed-out' ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
84
readme.txt
Normal file
84
readme.txt
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
=== SMTP (by ROBOTSTXT) ===
|
||||||
|
Contributors: robotstxt
|
||||||
|
Tags: smtp, email, mail
|
||||||
|
Requires at least: 6.0
|
||||||
|
Tested up to: 6.9
|
||||||
|
Requires PHP: 8.2
|
||||||
|
Stable tag: 1.2.0
|
||||||
|
License: GPLv3 or later
|
||||||
|
License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
|
Send every site email through a fully configurable SMTP server managed from the WordPress dashboard.
|
||||||
|
|
||||||
|
== Description ==
|
||||||
|
|
||||||
|
SMTP (by ROBOTSTXT) replaces WordPress' native mail delivery with a secure SMTP connection and detailed guidance. The plugin is built for marketing teams, agencies, and technical departments that need a clear, complete solution ready for multisite installations.
|
||||||
|
|
||||||
|
= Guided PHPMailer configuration =
|
||||||
|
|
||||||
|
* Routes `wp_mail()` through PHPMailer in SMTP mode with support for credentials, custom ports, and the `None`, `SSL`, or `TLS` encryption methods.
|
||||||
|
* Adds inline help text with practical examples in every field: host, port, username, password, sender email, and sender name.
|
||||||
|
* Automatically changes the port when it matches the default value for the selected encryption type (25, 465, or 587).
|
||||||
|
* Lets you set default sender information for the site to keep a consistent identity across outgoing messages.
|
||||||
|
|
||||||
|
= Tools for support and marketing teams =
|
||||||
|
|
||||||
|
* **Settings → SMTP → Test** screen for sending manual test emails and confirming the connection to the server.
|
||||||
|
* **Tools** panel packed with diagnostics: automatic MX lookup, SPF/DKIM/DMARC checks, extended SMTP server diagnostics, and DNS reputation monitoring.
|
||||||
|
* Caches results from each tool for 24 hours (with a manual refresh option) to streamline recurring tasks for your team.
|
||||||
|
|
||||||
|
= Comprehensive delivery logs =
|
||||||
|
|
||||||
|
* Saves the subject, recipients, headers, content, and attachments for every email sent.
|
||||||
|
* Browse logs from **Settings → SMTP → Logs** with pagination and access to the details of each delivery.
|
||||||
|
* Configure automatic cleanup by maximum entries or age in days, plus an instant "Clear all" button to wipe the history.
|
||||||
|
|
||||||
|
= Multisite-ready configuration =
|
||||||
|
|
||||||
|
* Choose whether the configuration applies to the entire network or individually per site.
|
||||||
|
* Dedicated forms in both the network dashboard and each site to edit, test, and share credentials securely.
|
||||||
|
|
||||||
|
== Installation ==
|
||||||
|
|
||||||
|
1. Upload the plugin folder to `wp-content/plugins/`.
|
||||||
|
2. Activate **SMTP (by ROBOTSTXT)** from the **Plugins** menu in the WordPress dashboard.
|
||||||
|
3. Open **Settings → SMTP** (or **Network Settings → SMTP** in multisite) to enter your connection details.
|
||||||
|
|
||||||
|
== Frequently Asked Questions ==
|
||||||
|
|
||||||
|
= Do I need credentials to send email? =
|
||||||
|
|
||||||
|
Enter a username and password only if your provider requires them. If the server accepts unauthenticated delivery, leave the fields blank and the plugin will send without credentials.
|
||||||
|
|
||||||
|
= Which encryption should I use? =
|
||||||
|
|
||||||
|
Select `None`, `SSL`, or `TLS` according to your provider's documentation. When you switch encryption, the plugin will automatically suggest the recommended port if you are using one of the standard values.
|
||||||
|
|
||||||
|
= How can I review the emails that were sent? =
|
||||||
|
|
||||||
|
Enable logging on the settings page and visit **Settings → SMTP → Logs** to open the paginated table of saved emails. From there you can inspect each entry, download attachments, and delete records.
|
||||||
|
|
||||||
|
= Does it work on a multisite network? =
|
||||||
|
|
||||||
|
Yes. From the network dashboard you can decide whether the configuration is global or site-specific. You can also run the tools and send test emails from the network or from each individual site.
|
||||||
|
|
||||||
|
== Changelog ==
|
||||||
|
|
||||||
|
= 1.2.0 =
|
||||||
|
|
||||||
|
* Logged failed email deliveries with status and error details in the log list and detail views.
|
||||||
|
* Captured the SMTP debug conversation for each email and surfaced it inside the log detail view.
|
||||||
|
|
||||||
|
= 1.1.0 =
|
||||||
|
|
||||||
|
* Added Amazon SES credential fields, regional selection, and live validation helpers that appear when the Amazon SES add-on is active.
|
||||||
|
* Introduced the `robotstxt_smtp_sanitized_options` filter so add-ons can adjust sanitized settings before they are stored.
|
||||||
|
* Routed SMTP test messages and regular WordPress emails through Amazon SES whenever the add-on supplies valid credentials.
|
||||||
|
|
||||||
|
= 1.0.0 =
|
||||||
|
|
||||||
|
* Contextual help guidance in every SMTP configuration field.
|
||||||
|
* Automatic port updates when selecting an encryption method with standard values.
|
||||||
|
* Advanced tools: MX analysis, SPF/DKIM/DMARC validation, extended SMTP diagnostics, and blacklist monitoring.
|
||||||
|
* Enhanced logging with automatic cleanup by limit or age.
|
||||||
|
* Updated documentation for the 1.0.0 release with WordPress 6.7 and PHP 8.2 support.
|
||||||
120
robotstxt-smtp.php
Normal file
120
robotstxt-smtp.php
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: SMTP (by ROBOTSTXT)
|
||||||
|
* Plugin URI: https://www.robotstxt.es/
|
||||||
|
* Description: Configure WordPress to send emails through SMTP with detailed controls.
|
||||||
|
* Version: 1.2.0
|
||||||
|
* Requires at least: 6.0
|
||||||
|
* Requires PHP: 8.2
|
||||||
|
* Author: ROBOTSTXT
|
||||||
|
* Author URI: https://www.robotstxt.es/
|
||||||
|
* Text Domain: robotstxt-smtp
|
||||||
|
* Domain Path: /languages
|
||||||
|
* License: GPL-3.0-or-later
|
||||||
|
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
* Gitea Plugin URI: ROBOTSTXT/robotstxt-smtp
|
||||||
|
* Gitea Plugin URI: https://git.robotstxt.es/ROBOTSTXT/robotstxt-smtp
|
||||||
|
* Plugin ID: did:plc:yodoiqooeu3l3lwuolyha3zs
|
||||||
|
*
|
||||||
|
* @package Robotstxt_SMTP
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! defined( 'ROBOTSTXT_SMTP_VERSION' ) ) {
|
||||||
|
define( 'ROBOTSTXT_SMTP_VERSION', '1.2.0' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! defined( 'ROBOTSTXT_SMTP_FILE' ) ) {
|
||||||
|
define( 'ROBOTSTXT_SMTP_FILE', __FILE__ );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! defined( 'ROBOTSTXT_SMTP_URL' ) ) {
|
||||||
|
define( 'ROBOTSTXT_SMTP_URL', plugin_dir_url( __FILE__ ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! defined( 'ROBOTSTXT_SMTP_PATH' ) ) {
|
||||||
|
define( 'ROBOTSTXT_SMTP_PATH', plugin_dir_path( __FILE__ ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! defined( 'ROBOTSTXT_SMTP_SLUG' ) ) {
|
||||||
|
define( 'ROBOTSTXT_SMTP_SLUG', 'robotstxt-smtp' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! defined( 'ROBOTSTXT_SMTP_BASENAME' ) ) {
|
||||||
|
define( 'ROBOTSTXT_SMTP_BASENAME', plugin_basename( ROBOTSTXT_SMTP_FILE ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! class_exists( 'PHPMailer\PHPMailer\PHPMailer' ) ) {
|
||||||
|
require_once ABSPATH . WPINC . '/PHPMailer/PHPMailer.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! class_exists( 'PHPMailer\PHPMailer\Exception' ) ) {
|
||||||
|
require_once ABSPATH . WPINC . '/PHPMailer/Exception.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! class_exists( 'PHPMailer\PHPMailer\SMTP' ) ) {
|
||||||
|
require_once ABSPATH . WPINC . '/PHPMailer/SMTP.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once ROBOTSTXT_SMTP_PATH . 'includes/class-smtp-diagnostics-client.php';
|
||||||
|
require_once ROBOTSTXT_SMTP_PATH . 'includes/class-settings-page.php';
|
||||||
|
require_once ROBOTSTXT_SMTP_PATH . 'includes/class-plugin.php';
|
||||||
|
|
||||||
|
if ( ! function_exists( 'robotstxt_smtp_dependencies_satisfied' ) ) {
|
||||||
|
/**
|
||||||
|
* Determines whether the plugin dependencies are available.
|
||||||
|
*
|
||||||
|
* @since 1.1.1
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
function robotstxt_smtp_dependencies_satisfied(): bool {
|
||||||
|
return class_exists( '\\PHPMailer\\PHPMailer\\PHPMailer' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'robotstxt_smtp_register_dependency_notice' ) ) {
|
||||||
|
/**
|
||||||
|
* Registers an admin notice when dependencies are missing.
|
||||||
|
*
|
||||||
|
* @since 1.1.1
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function robotstxt_smtp_register_dependency_notice(): void {
|
||||||
|
if ( ! is_admin() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$callback = static function (): void {
|
||||||
|
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
|
||||||
|
|
||||||
|
if ( $screen && 'plugins' !== $screen->id && 'plugins-network' !== $screen->id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = sprintf(
|
||||||
|
/* translators: %s: plugin name. */
|
||||||
|
esc_html__( 'The %s plugin requires the PHPMailer library to be available.', 'robotstxt-smtp' ),
|
||||||
|
esc_html__( 'ROBOTSTXT SMTP', 'robotstxt-smtp' )
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
<div class="notice notice-error">
|
||||||
|
<p><?php echo esc_html( $message ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
};
|
||||||
|
|
||||||
|
add_action( 'admin_notices', $callback );
|
||||||
|
add_action( 'network_admin_notices', $callback );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( robotstxt_smtp_dependencies_satisfied() ) {
|
||||||
|
\Robotstxt_SMTP\Plugin::get_instance()->run();
|
||||||
|
} else {
|
||||||
|
robotstxt_smtp_register_dependency_notice();
|
||||||
|
}
|
||||||
155
uninstall.php
Normal file
155
uninstall.php
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Uninstall routines for the core SMTP plugin.
|
||||||
|
*
|
||||||
|
* @package Robotstxt_SMTP
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'robotstxt_smtp_uninstall_cleanup_site' ) ) {
|
||||||
|
/**
|
||||||
|
* Removes all plugin data for the current site.
|
||||||
|
*
|
||||||
|
* @since 1.1.1
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function robotstxt_smtp_uninstall_cleanup_site(): void {
|
||||||
|
delete_option( 'robotstxt_smtp_options' );
|
||||||
|
|
||||||
|
robotstxt_smtp_uninstall_delete_logs();
|
||||||
|
robotstxt_smtp_uninstall_delete_transient_prefix( 'robotstxt_smtp_tool_', false );
|
||||||
|
robotstxt_smtp_uninstall_delete_transient_prefix( 'robotstxt_smtp_test_result_', false );
|
||||||
|
wp_clear_scheduled_hook( 'robotstxt_smtp_cleanup_logs' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'robotstxt_smtp_uninstall_cleanup_network' ) ) {
|
||||||
|
/**
|
||||||
|
* Removes network-wide plugin data.
|
||||||
|
*
|
||||||
|
* @since 1.1.1
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function robotstxt_smtp_uninstall_cleanup_network(): void {
|
||||||
|
delete_site_option( 'robotstxt_smtp_network_options' );
|
||||||
|
delete_site_option( 'robotstxt_smtp_configuration_scope' );
|
||||||
|
|
||||||
|
robotstxt_smtp_uninstall_delete_transient_prefix( 'robotstxt_smtp_tool_', true );
|
||||||
|
robotstxt_smtp_uninstall_delete_transient_prefix( 'robotstxt_smtp_test_result_', true );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'robotstxt_smtp_uninstall_delete_transient_prefix' ) ) {
|
||||||
|
/**
|
||||||
|
* Deletes transients that match a prefix for the current context.
|
||||||
|
*
|
||||||
|
* @since 1.1.1
|
||||||
|
*
|
||||||
|
* @param string $prefix Transient prefix without the internal storage prefix.
|
||||||
|
* @param bool $is_network Whether to delete network transients.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function robotstxt_smtp_uninstall_delete_transient_prefix( string $prefix, bool $is_network ): void {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if ( $is_network ) {
|
||||||
|
$stored_prefix = '_site_transient_' . $prefix;
|
||||||
|
$timeout_prefix = '_site_transient_timeout_' . $prefix;
|
||||||
|
$delete_callback = 'delete_site_transient';
|
||||||
|
$timeout_prefix_length = strlen( '_site_transient_timeout_' );
|
||||||
|
$stored_prefix_length = strlen( '_site_transient_' );
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||||
|
$matches = $wpdb->get_col(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT meta_key FROM {$wpdb->sitemeta} WHERE meta_key LIKE %s OR meta_key LIKE %s",
|
||||||
|
$wpdb->esc_like( $stored_prefix ) . '%',
|
||||||
|
$wpdb->esc_like( $timeout_prefix ) . '%'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$stored_prefix = '_transient_' . $prefix;
|
||||||
|
$timeout_prefix = '_transient_timeout_' . $prefix;
|
||||||
|
$delete_callback = 'delete_transient';
|
||||||
|
$timeout_prefix_length = strlen( '_transient_timeout_' );
|
||||||
|
$stored_prefix_length = strlen( '_transient_' );
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||||
|
$matches = $wpdb->get_col(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
|
||||||
|
$wpdb->esc_like( $stored_prefix ) . '%',
|
||||||
|
$wpdb->esc_like( $timeout_prefix ) . '%'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( empty( $matches ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $matches as $option_name ) {
|
||||||
|
if ( ! is_string( $option_name ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( str_starts_with( $option_name, $is_network ? '_site_transient_timeout_' : '_transient_timeout_' ) ) {
|
||||||
|
$transient_key = substr( $option_name, $timeout_prefix_length );
|
||||||
|
} else {
|
||||||
|
$transient_key = substr( $option_name, $stored_prefix_length );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( '' === $transient_key ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
call_user_func( $delete_callback, $transient_key );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'robotstxt_smtp_uninstall_delete_logs' ) ) {
|
||||||
|
/**
|
||||||
|
* Deletes all stored SMTP logs for the current site.
|
||||||
|
*
|
||||||
|
* @since 1.1.1
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function robotstxt_smtp_uninstall_delete_logs(): void {
|
||||||
|
do {
|
||||||
|
$logs = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => 'robotstxt_smtp_log',
|
||||||
|
'post_status' => 'any',
|
||||||
|
'fields' => 'ids',
|
||||||
|
'posts_per_page' => 100,
|
||||||
|
'orderby' => 'ID',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $logs as $log_id ) {
|
||||||
|
wp_delete_post( (int) $log_id, true );
|
||||||
|
}
|
||||||
|
} while ( ! empty( $logs ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
robotstxt_smtp_uninstall_cleanup_network();
|
||||||
|
|
||||||
|
if ( function_exists( 'is_multisite' ) && is_multisite() ) {
|
||||||
|
$sites = get_sites( array( 'fields' => 'ids' ) );
|
||||||
|
|
||||||
|
foreach ( $sites as $site_id ) {
|
||||||
|
switch_to_blog( (int) $site_id );
|
||||||
|
robotstxt_smtp_uninstall_cleanup_site();
|
||||||
|
restore_current_blog();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
robotstxt_smtp_uninstall_cleanup_site();
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue