*/ public const MODES = array( 'api', 'smtp' ); /** * Ports Sendfeather accepts SMTP submissions on. * * @var array */ public const SMTP_PORTS = array( 2525, 587, 465 ); /** * Encryption choices, stored values. * * @var array */ public const ENCRYPTION = array( 'starttls', 'tls', 'none' ); /** * Default settings. * * @return array */ public static function defaults(): array { return array( 'mode' => 'api', 'api_key' => '', 'smtp_host' => 'smtp.sendfeather.com', 'smtp_port' => 2525, 'smtp_encryption' => 'starttls', 'smtp_username' => '', 'smtp_password' => '', 'from_email' => '', 'from_name' => '', 'force_from' => true, 'reply_to' => '', ); } /** * Current settings, defaults filled in. * * @return array */ public static function get(): array { static $settings = null; if ( null === $settings ) { $stored = get_option( self::OPTION, array() ); if ( ! is_array( $stored ) ) { $stored = array(); } $settings = wp_parse_args( $stored, self::defaults() ); $settings['mode'] = in_array( $settings['mode'], self::MODES, true ) ? $settings['mode'] : 'api'; $settings['smtp_port'] = (int) $settings['smtp_port']; $settings['smtp_encryption'] = in_array( $settings['smtp_encryption'], self::ENCRYPTION, true ) ? $settings['smtp_encryption'] : 'starttls'; $settings['force_from'] = (bool) $settings['force_from']; } return $settings; } /** * Hook the admin screen, the Settings API registration, and the test * email handler. */ public static function boot(): void { add_action( 'admin_menu', array( __CLASS__, 'add_page' ) ); add_action( 'admin_init', array( __CLASS__, 'register' ) ); add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) ); add_action( 'wp_ajax_sendfeather_send_test_email', array( __CLASS__, 'handle_test_email' ) ); add_filter( 'plugin_action_links_' . plugin_basename( SENDFEATHER_PLUGIN_FILE ), array( __CLASS__, 'action_links' ) ); } /** * Add the settings page under Settings. */ public static function add_page(): void { add_options_page( __( 'Sendfeather SMTP settings', 'sendfeather' ), __( 'Sendfeather', 'sendfeather' ), 'manage_options', self::PAGE, array( __CLASS__, 'render_page' ) ); } /** * "Settings" link on the Plugins list row. * * @param array $links Existing action links. * @return array */ public static function action_links( array $links ): array { $settings_link = sprintf( '%s', esc_url( admin_url( 'options-general.php?page=' . self::PAGE ) ), esc_html__( 'Settings', 'sendfeather' ) ); array_unshift( $links, $settings_link ); return $links; } /** * Register the option, sections, and fields with the Settings API. */ public static function register(): void { register_setting( 'sendfeather', self::OPTION, array( 'type' => 'array', 'sanitize_callback' => array( __CLASS__, 'sanitize' ), 'default' => self::defaults(), ) ); add_settings_section( 'sendfeather_connection', __( 'Connection', 'sendfeather' ), array( __CLASS__, 'render_connection_intro' ), 'sendfeather' ); add_settings_field( 'mode', __( 'Connection mode', 'sendfeather' ), array( __CLASS__, 'render_mode_field' ), 'sendfeather', 'sendfeather_connection' ); add_settings_field( 'api_key', __( 'API key', 'sendfeather' ), array( __CLASS__, 'render_api_key_field' ), 'sendfeather', 'sendfeather_connection', array( 'mode' => 'api', 'label_for' => 'sendfeather-api-key' ) ); add_settings_field( 'smtp_host', __( 'SMTP host', 'sendfeather' ), array( __CLASS__, 'render_smtp_host_field' ), 'sendfeather', 'sendfeather_connection', array( 'mode' => 'smtp', 'label_for' => 'sendfeather-smtp-host' ) ); add_settings_field( 'smtp_port', __( 'SMTP port', 'sendfeather' ), array( __CLASS__, 'render_smtp_port_field' ), 'sendfeather', 'sendfeather_connection', array( 'mode' => 'smtp', 'label_for' => 'sendfeather-smtp-port' ) ); add_settings_field( 'smtp_encryption', __( 'Encryption', 'sendfeather' ), array( __CLASS__, 'render_smtp_encryption_field' ), 'sendfeather', 'sendfeather_connection', array( 'mode' => 'smtp', 'label_for' => 'sendfeather-smtp-encryption' ) ); add_settings_field( 'smtp_username', __( 'SMTP username', 'sendfeather' ), array( __CLASS__, 'render_smtp_username_field' ), 'sendfeather', 'sendfeather_connection', array( 'mode' => 'smtp', 'label_for' => 'sendfeather-smtp-username' ) ); add_settings_field( 'smtp_password', __( 'SMTP password', 'sendfeather' ), array( __CLASS__, 'render_smtp_password_field' ), 'sendfeather', 'sendfeather_connection', array( 'mode' => 'smtp', 'label_for' => 'sendfeather-smtp-password' ) ); add_settings_section( 'sendfeather_sender', __( 'Sender', 'sendfeather' ), array( __CLASS__, 'render_sender_intro' ), 'sendfeather' ); add_settings_field( 'from_email', __( 'From email', 'sendfeather' ), array( __CLASS__, 'render_from_email_field' ), 'sendfeather', 'sendfeather_sender', array( 'label_for' => 'sendfeather-from-email' ) ); add_settings_field( 'from_name', __( 'From name', 'sendfeather' ), array( __CLASS__, 'render_from_name_field' ), 'sendfeather', 'sendfeather_sender', array( 'label_for' => 'sendfeather-from-name' ) ); add_settings_field( 'force_from', __( 'From address', 'sendfeather' ), array( __CLASS__, 'render_force_from_field' ), 'sendfeather', 'sendfeather_sender' ); add_settings_field( 'reply_to', __( 'Reply-to', 'sendfeather' ), array( __CLASS__, 'render_reply_to_field' ), 'sendfeather', 'sendfeather_sender', array( 'label_for' => 'sendfeather-reply-to' ) ); add_settings_section( 'sendfeather_test', __( 'Test email', 'sendfeather' ), array( __CLASS__, 'render_test_section' ), 'sendfeather' ); } /** * Sanitize and validate everything submitted from the settings form. * * Runs inside options.php, which has already verified the settings nonce * and the manage_options capability. Problems surface as settings errors * and the previous value is kept where a field is invalid. * * @param mixed $input Raw submitted option value. * @return array */ public static function sanitize( $input ): array { $current = self::get(); if ( ! is_array( $input ) ) { return $current; } $clean = $current; $mode = isset( $input['mode'] ) ? sanitize_key( $input['mode'] ) : $current['mode']; if ( ! in_array( $mode, self::MODES, true ) ) { add_settings_error( self::OPTION, 'sendfeather_mode', __( 'Unknown connection mode; kept the previous one.', 'sendfeather' ), 'error' ); $mode = $current['mode']; } $clean['mode'] = $mode; $clean['api_key'] = isset( $input['api_key'] ) ? sanitize_text_field( wp_unslash( $input['api_key'] ) ) : ''; if ( 'api' === $mode && '' === $clean['api_key'] ) { add_settings_error( self::OPTION, 'sendfeather_api_key', __( 'API key mode needs a key or email will not send. Create one under API keys in the Sendfeather dashboard.', 'sendfeather' ), 'error' ); } $host = isset( $input['smtp_host'] ) ? sanitize_text_field( wp_unslash( $input['smtp_host'] ) ) : ''; if ( '' === $host ) { $host = self::defaults()['smtp_host']; } $clean['smtp_host'] = $host; $port = isset( $input['smtp_port'] ) ? absint( $input['smtp_port'] ) : 0; if ( ! in_array( $port, self::SMTP_PORTS, true ) ) { add_settings_error( self::OPTION, 'sendfeather_smtp_port', __( 'Unknown SMTP port; kept the previous one.', 'sendfeather' ), 'error' ); $port = (int) $current['smtp_port']; } $clean['smtp_port'] = $port; $encryption = isset( $input['smtp_encryption'] ) ? sanitize_key( $input['smtp_encryption'] ) : $current['smtp_encryption']; if ( ! in_array( $encryption, self::ENCRYPTION, true ) ) { $encryption = $current['smtp_encryption']; } $clean['smtp_encryption'] = $encryption; $clean['smtp_username'] = isset( $input['smtp_username'] ) ? sanitize_text_field( wp_unslash( $input['smtp_username'] ) ) : ''; $clean['smtp_password'] = isset( $input['smtp_password'] ) ? sanitize_text_field( wp_unslash( $input['smtp_password'] ) ) : ''; if ( 'smtp' === $mode && ( '' === $clean['smtp_username'] || '' === $clean['smtp_password'] ) ) { add_settings_error( self::OPTION, 'sendfeather_smtp_credentials', __( 'SMTP mode needs a username and password or email will not send. Create an SMTP user in the Sendfeather dashboard.', 'sendfeather' ), 'error' ); } $from_email = isset( $input['from_email'] ) ? sanitize_email( wp_unslash( $input['from_email'] ) ) : ''; if ( '' !== $from_email && ! is_email( $from_email ) ) { add_settings_error( self::OPTION, 'sendfeather_from_email', __( 'The From email is not a valid address; kept the previous one.', 'sendfeather' ), 'error' ); $from_email = (string) $current['from_email']; } $clean['from_email'] = $from_email; $clean['from_name'] = isset( $input['from_name'] ) ? sanitize_text_field( wp_unslash( $input['from_name'] ) ) : ''; $clean['force_from'] = ! empty( $input['force_from'] ); $reply_to = isset( $input['reply_to'] ) ? sanitize_email( wp_unslash( $input['reply_to'] ) ) : ''; if ( '' !== $reply_to && ! is_email( $reply_to ) ) { add_settings_error( self::OPTION, 'sendfeather_reply_to', __( 'The reply-to address is not a valid address; kept the previous one.', 'sendfeather' ), 'error' ); $reply_to = (string) $current['reply_to']; } $clean['reply_to'] = $reply_to; return $clean; } /** * Enqueue the admin stylesheet and script on the plugin's own screen only. * * @param string $hook_suffix Current admin screen. */ public static function enqueue_assets( string $hook_suffix ): void { if ( 'settings_page_' . self::PAGE !== $hook_suffix ) { return; } wp_enqueue_style( 'sendfeather-admin', plugins_url( 'assets/css/admin.css', SENDFEATHER_PLUGIN_FILE ), array(), SENDFEATHER_VERSION ); wp_enqueue_script( 'sendfeather-admin', plugins_url( 'assets/js/admin.js', SENDFEATHER_PLUGIN_FILE ), array(), SENDFEATHER_VERSION, true ); wp_localize_script( 'sendfeather-admin', 'sendfeatherAdmin', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'sendfeather_test_email' ), 'messages' => array( 'sending' => __( 'Sending…', 'sendfeather' ), 'networkError' => __( 'The request failed before Sendfeather answered. Check this site’s network connection and try again.', 'sendfeather' ), 'missingEmail' => __( 'Enter an email address to send the test to.', 'sendfeather' ), 'show' => __( 'Show', 'sendfeather' ), 'hide' => __( 'Hide', 'sendfeather' ), ), ) ); } /** * Render the settings page. */ public static function render_page(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } ?>

' . esc_html__( 'Sendfeather SMTP guide', 'sendfeather' ) . '' ); ?>

' . esc_html__( 'Choose how WordPress hands messages to Sendfeather. API key mode needs one key and only outbound HTTPS; SMTP mode speaks the standard submission protocol.', 'sendfeather' ) . '

'; } /** * Sender section intro. */ public static function render_sender_intro(): void { echo '

' . esc_html__( 'What recipients see. Sendfeather only accepts mail from addresses on domains you have verified in the dashboard.', 'sendfeather' ) . '

'; } /** * Mode radio group. */ public static function render_mode_field(): void { $settings = self::get(); ?>

__( '2525 — STARTTLS (recommended; works when 587 is blocked)', 'sendfeather' ), 587 => __( '587 — STARTTLS', 'sendfeather' ), 465 => __( '465 — TLS from the first byte', 'sendfeather' ), ); ?>
__( 'STARTTLS (ports 2525 and 587)', 'sendfeather' ), 'tls' => __( 'TLS/SSL (port 465)', 'sendfeather' ), 'none' => __( 'None (not recommended)', 'sendfeather' ), ); ?>

__( 'You do not have permission to send test emails.', 'sendfeather' ) ), 403 ); } $email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : ''; if ( '' === $email || ! is_email( $email ) ) { wp_send_json_error( array( 'message' => __( 'Enter a valid email address to send the test to.', 'sendfeather' ) ) ); } $failures = array(); $collect = static function ( $error ) use ( &$failures ): void { if ( $error instanceof \WP_Error ) { foreach ( $error->get_error_messages() as $message ) { $failures[] = $message; } } }; add_action( 'wp_mail_failed', $collect ); $sent = wp_mail( $email, sprintf( /* translators: %s: site name. */ __( '[%s] Sendfeather test email', 'sendfeather' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ), sprintf( /* translators: 1: site URL, 2: current date and time. */ __( "This is a test email from %1\$s.\n\nIf you are reading it, WordPress handed the message to Sendfeather and Sendfeather accepted it. Open Sendfeather → Activity to see what happened next.\n\nSent at %2\$s.", 'sendfeather' ), home_url( '/' ), gmdate( 'Y-m-d H:i:s' ) . ' UTC' ) ); remove_action( 'wp_mail_failed', $collect ); if ( $sent ) { wp_send_json_success( array( 'message' => sprintf( /* translators: %s: recipient email address. */ __( 'Sent. WordPress accepted a message to %s. Check that inbox, then look it up in Sendfeather → Activity for the delivery timeline.', 'sendfeather' ), $email ), ) ); } $detail = $failures ? implode( ' ', array_unique( $failures ) ) : __( 'WordPress did not accept the message, and no error detail was reported.', 'sendfeather' ); wp_send_json_error( array( 'message' => sprintf( /* translators: %s: error detail from the transport. */ __( 'Not sent. %s', 'sendfeather' ), $detail ), ) ); } }