feat: Add SMTP mailer configuration and integration\n\n- Adds PHPMailer dependency via Composer.\n- Adds SMTP settings section (Host, Port, Encryption, Auth, Credentials, From Address/Name, Enable toggle) to Settings > Quiztech page.\n- Implements render callbacks and sanitization for new settings.\n- Adds 'Send Test Email' button and AJAX handler for verification.\n- Hooks into 'phpmailer_init' to configure PHPMailer based on saved settings.\n- Ensures Invitations class uses wp_mail().

This commit is contained in:
Ruben Ramirez 2025-04-03 19:55:33 -05:00
parent fc3b9a0458
commit 3e80e1baa0
5 changed files with 474 additions and 8 deletions

View file

@ -10,7 +10,8 @@
}
],
"require": {
"php": ">=7.4"
"php": ">=7.4",
"phpmailer/phpmailer": "^6.8"
},
"autoload": {
"psr-4": {

86
composer.lock generated
View file

@ -4,8 +4,90 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ae3db58fab4d027fbd3814761fec769f",
"packages": [],
"content-hash": "aa1767c4b73d4c64f1f4b5531b361ba5",
"packages": [
{
"name": "phpmailer/phpmailer",
"version": "v6.9.3",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/2f5c94fe7493efc213f643c23b1b1c249d40f47e",
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-filter": "*",
"ext-hash": "*",
"php": ">=5.5.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"doctrine/annotations": "^1.2.6 || ^1.13.3",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^9.3.5",
"roave/security-advisories": "dev-latest",
"squizlabs/php_codesniffer": "^3.7.2",
"yoast/phpunit-polyfills": "^1.0.4"
},
"suggest": {
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
"psr/log": "For optional PSR-3 debug logging",
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
"thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPMailer\\PHPMailer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-only"
],
"authors": [
{
"name": "Marcus Bointon",
"email": "phpmailer@synchromedia.co.uk"
},
{
"name": "Jim Jagielski",
"email": "jimjag@gmail.com"
},
{
"name": "Andy Prevost",
"email": "codeworxtech@users.sourceforge.net"
},
{
"name": "Brent R. Matzelle"
}
],
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.3"
},
"funding": [
{
"url": "https://github.com/Synchro",
"type": "github"
}
],
"time": "2024-11-24T18:04:13+00:00"
}
],
"packages-dev": [
{
"name": "php-stubs/wordpress-stubs",

View file

@ -139,3 +139,71 @@ function quiztech_init() {
// Future core classes (e.g., Shortcode handlers) should be instantiated here.
}
add_action( 'plugins_loaded', 'quiztech_init' );
/**
* Configure PHPMailer to use SMTP settings if enabled.
*
* @param PHPMailer\PHPMailer\PHPMailer $phpmailer The PHPMailer instance.
*/
function quiztech_configure_smtp( $phpmailer ) {
$options = get_option( 'quiztech_settings', [] );
// Check if SMTP is enabled in settings
if ( empty( $options['smtp_enabled'] ) || ! $options['smtp_enabled'] ) {
return; // SMTP not enabled, do nothing
}
// Basic validation
if ( empty( $options['smtp_host'] ) || empty( $options['smtp_port'] ) ) {
// Optionally log an error or add an admin notice here
error_log('Quiztech SMTP Error: Host or Port not configured.');
return; // Cannot configure SMTP without host and port
}
// Set mailer to use SMTP
$phpmailer->isSMTP();
// Set SMTP server details
$phpmailer->Host = $options['smtp_host'];
$phpmailer->Port = (int) $options['smtp_port'];
// Set encryption type (tls, ssl, or none)
$encryption = isset( $options['smtp_encryption'] ) ? strtolower( $options['smtp_encryption'] ) : '';
if ( in_array( $encryption, [ 'ssl', 'tls' ] ) ) {
$phpmailer->SMTPSecure = $encryption;
} else {
$phpmailer->SMTPSecure = ''; // Explicitly set to none if not ssl/tls
}
// Set authentication details if enabled
$phpmailer->SMTPAuth = ! empty( $options['smtp_auth'] ) && $options['smtp_auth'];
if ( $phpmailer->SMTPAuth ) {
if ( empty( $options['smtp_username'] ) || empty( $options['smtp_password'] ) ) {
error_log('Quiztech SMTP Error: Auth enabled but Username or Password missing.');
// Decide if we should prevent sending or let PHPMailer handle the error
// For now, we proceed and let PHPMailer fail if credentials are bad
}
$phpmailer->Username = isset( $options['smtp_username'] ) ? $options['smtp_username'] : '';
$phpmailer->Password = isset( $options['smtp_password'] ) ? $options['smtp_password'] : '';
}
// Set From address (optional, but recommended)
$from_address = isset( $options['smtp_from_address'] ) ? $options['smtp_from_address'] : '';
$from_name = isset( $options['smtp_from_name'] ) ? $options['smtp_from_name'] : get_bloginfo( 'name' );
if ( ! empty( $from_address ) && is_email( $from_address ) ) {
try {
$phpmailer->setFrom( $from_address, $from_name );
} catch ( \PHPMailer\PHPMailer\Exception $e ) {
error_log( 'Quiztech SMTP Error setting From address: ' . $e->getMessage() );
// Continue without setting From if it fails
}
}
// Optional: Add debugging
// $phpmailer->SMTPDebug = 2; // Enable verbose debug output (use 0 in production)
// $phpmailer->Debugoutput = 'error_log'; // Log debug output to PHP error log
}
add_action( 'phpmailer_init', 'quiztech_configure_smtp' );

View file

@ -31,6 +31,7 @@ class SettingsPage {
public function register_hooks() {
add_action( 'admin_menu', [ $this, 'add_admin_page' ] );
add_action( 'admin_init', [ $this, 'register_settings' ] );
add_action( 'wp_ajax_quiztech_send_test_email', [ $this, 'handle_send_test_email_ajax' ] );
}
/**
@ -95,8 +96,160 @@ class SettingsPage {
]
);
// --- SMTP Section ---
add_settings_section(
'quiztech_smtp_section', // Section ID
__( 'SMTP Settings', 'quiztech' ), // Section Title
[ $this, 'render_smtp_section_description' ], // Section callback for description
$this->page_slug // Page slug where section appears
);
// SMTP Enabled Field (Checkbox)
add_settings_field(
'quiztech_smtp_enabled',
__( 'Enable SMTP', 'quiztech' ),
[ $this, 'render_checkbox_field' ], // New callback needed
$this->page_slug,
'quiztech_smtp_section',
[
'id' => 'quiztech_smtp_enabled',
'option_name' => $this->option_name,
'key' => 'smtp_enabled',
'description' => __( 'Enable sending emails via a custom SMTP server instead of the default WordPress mail function.', 'quiztech' )
]
);
// SMTP Host Field
add_settings_field(
'quiztech_smtp_host',
__( 'SMTP Host', 'quiztech' ),
[ $this, 'render_text_field' ],
$this->page_slug,
'quiztech_smtp_section',
[
'id' => 'quiztech_smtp_host',
'option_name' => $this->option_name,
'key' => 'smtp_host',
'description' => __( 'e.g., smtp.example.com', 'quiztech' )
]
);
// SMTP Port Field
add_settings_field(
'quiztech_smtp_port',
__( 'SMTP Port', 'quiztech' ),
[ $this, 'render_text_field' ], // Use text field, sanitize as number later
$this->page_slug,
'quiztech_smtp_section',
[
'id' => 'quiztech_smtp_port',
'option_name' => $this->option_name,
'key' => 'smtp_port',
'type' => 'number',
'description' => __( 'e.g., 587 (TLS) or 465 (SSL)', 'quiztech' )
]
);
// SMTP Encryption Field (Select)
add_settings_field(
'quiztech_smtp_encryption',
__( 'Encryption', 'quiztech' ),
[ $this, 'render_select_field' ], // New callback needed
$this->page_slug,
'quiztech_smtp_section',
[
'id' => 'quiztech_smtp_encryption',
'option_name' => $this->option_name,
'key' => 'smtp_encryption',
'options' => [
'' => __( 'None', 'quiztech' ),
'ssl' => __( 'SSL', 'quiztech' ),
'tls' => __( 'TLS', 'quiztech' ),
],
'description' => __( 'Select the encryption method.', 'quiztech' )
]
);
// SMTP Auth Field (Checkbox)
add_settings_field(
'quiztech_smtp_auth',
__( 'Use Authentication', 'quiztech' ),
[ $this, 'render_checkbox_field' ], // Reuse callback
$this->page_slug,
'quiztech_smtp_section',
[
'id' => 'quiztech_smtp_auth',
'option_name' => $this->option_name,
'key' => 'smtp_auth',
'description' => __( 'Enable if your SMTP server requires a username and password.', 'quiztech' )
]
);
// SMTP Username Field
add_settings_field(
'quiztech_smtp_username',
__( 'SMTP Username', 'quiztech' ),
[ $this, 'render_text_field' ],
$this->page_slug,
'quiztech_smtp_section',
[
'id' => 'quiztech_smtp_username',
'option_name' => $this->option_name,
'key' => 'smtp_username',
'description' => __( 'The username for SMTP authentication.', 'quiztech' )
]
);
// SMTP Password Field
add_settings_field(
'quiztech_smtp_password',
__( 'SMTP Password', 'quiztech' ),
[ $this, 'render_text_field' ],
$this->page_slug,
'quiztech_smtp_section',
[
'id' => 'quiztech_smtp_password',
'option_name' => $this->option_name,
'key' => 'smtp_password',
'type' => 'password',
'description' => __( 'The password for SMTP authentication. Stored as plain text in the database.', 'quiztech' )
]
);
// SMTP From Address Field
add_settings_field(
'quiztech_smtp_from_address',
__( 'From Email Address', 'quiztech' ),
[ $this, 'render_text_field' ],
$this->page_slug,
'quiztech_smtp_section',
[
'id' => 'quiztech_smtp_from_address',
'option_name' => $this->option_name,
'key' => 'smtp_from_address',
'type' => 'email',
'description' => __( 'The email address emails will be sent from.', 'quiztech' )
]
);
// SMTP From Name Field
add_settings_field(
'quiztech_smtp_from_name',
__( 'From Name', 'quiztech' ),
[ $this, 'render_text_field' ],
$this->page_slug,
'quiztech_smtp_section',
[
'id' => 'quiztech_smtp_from_name',
'option_name' => $this->option_name,
'key' => 'smtp_from_name',
'description' => __( 'The name emails will be sent from.', 'quiztech' )
]
);
// Future settings sections/fields (e.g., email, defaults) should be registered here.
}
} // End register_settings()
/**
* Renders the main settings page container and form.
@ -116,6 +269,42 @@ class SettingsPage {
// Output save settings button
submit_button( __( 'Save Settings', 'quiztech' ) );
?>
<hr />
<h2><?php esc_html_e( 'Test SMTP Settings', 'quiztech' ); ?></h2>
<p><?php esc_html_e( 'Click the button below to send a test email to the site administrator', 'quiztech' ); ?> (<?php echo esc_html( get_option('admin_email') ); ?>) <?php esc_html_e( 'using the currently saved settings.', 'quiztech' ); ?></p>
<button type="button" id="quiztech-test-email-button" class="button">
<?php esc_html_e( 'Send Test Email', 'quiztech' ); ?>
</button>
<span id="quiztech-test-email-status" style="margin-left: 10px;"></span>
<script type="text/javascript">
jQuery(document).ready(function($) {
$('#quiztech-test-email-button').on('click', function() {
var button = $(this);
var statusSpan = $('#quiztech-test-email-status');
statusSpan.text('<?php echo esc_js( __( 'Sending...', 'quiztech' ) ); ?>').css('color', '');
button.prop('disabled', true);
$.post(ajaxurl, {
action: 'quiztech_send_test_email',
_ajax_nonce: '<?php echo wp_create_nonce( 'quiztech_test_email_nonce' ); ?>'
}, function(response) {
if (response.success) {
statusSpan.text('<?php echo esc_js( __( 'Test email sent successfully!', 'quiztech' ) ); ?>').css('color', 'green');
} else {
var errorMsg = response.data && response.data.message ? response.data.message : '<?php echo esc_js( __( 'An unknown error occurred.', 'quiztech' ) ); ?>';
statusSpan.text('<?php echo esc_js( __( 'Error:', 'quiztech' ) ); ?> ' + errorMsg).css('color', 'red');
}
button.prop('disabled', false);
}).fail(function() {
statusSpan.text('<?php echo esc_js( __( 'AJAX request failed.', 'quiztech' ) ); ?>').css('color', 'red');
button.prop('disabled', false);
});
});
});
</script>
</form>
</div>
<?php
@ -145,6 +334,63 @@ class SettingsPage {
<?php
}
/**
* Renders the description for the SMTP settings section.
*/
public function render_smtp_section_description() {
echo '<p>' . esc_html__( 'Configure your SMTP server details for reliable email sending. If disabled, WordPress will use the default PHP mail function.', 'quiztech' ) . '</p>';
}
/**
* Renders a checkbox input field for a setting.
* Expects args: 'id', 'option_name', 'key', 'description' (optional)
*
* @param array $args Arguments passed from add_settings_field.
*/
public function render_checkbox_field( $args ) {
$options = get_option( $args['option_name'], [] );
$checked = isset( $options[ $args['key'] ] ) && $options[ $args['key'] ] ? 'checked' : '';
?>
<input
type="checkbox"
id="<?php echo esc_attr( $args['id'] ); ?>"
name="<?php echo esc_attr( $args['option_name'] . '[' . $args['key'] . ']' ); ?>"
value="1" <?php // Value is 1 when checked ?>
<?php echo esc_attr( $checked ); ?>
/>
<?php if ( isset( $args['description'] ) ) : ?>
<label for="<?php echo esc_attr( $args['id'] ); ?>"> <?php echo esc_html( $args['description'] ); ?></label>
<?php endif; ?>
<?php
}
/**
* Renders a select dropdown field for a setting.
* Expects args: 'id', 'option_name', 'key', 'options' (array of value => label), 'description' (optional)
*
* @param array $args Arguments passed from add_settings_field.
*/
public function render_select_field( $args ) {
$options = get_option( $args['option_name'], [] );
$value = isset( $options[ $args['key'] ] ) ? $options[ $args['key'] ] : '';
?>
<select
id="<?php echo esc_attr( $args['id'] ); ?>"
name="<?php echo esc_attr( $args['option_name'] . '[' . $args['key'] . ']' ); ?>"
>
<?php foreach ( $args['options'] as $option_value => $option_label ) : ?>
<option value="<?php echo esc_attr( $option_value ); ?>" <?php selected( $value, $option_value ); ?>>
<?php echo esc_html( $option_label ); ?>
</option>
<?php endforeach; ?>
</select>
<?php if ( isset( $args['description'] ) ) : ?>
<p class="description"><?php echo esc_html( $args['description'] ); ?></p>
<?php endif; ?>
<?php
}
/**
* Sanitizes the settings array before saving.
*
@ -161,10 +407,81 @@ class SettingsPage {
if ( isset( $input['stripe_secret_key'] ) ) {
// Basic sanitization, might need stricter validation (e.g., regex for sk_live_/sk_test_)
$sanitized_input['stripe_secret_key'] = sanitize_text_field( $input['stripe_secret_key'] );
// Sanitize SMTP settings
$smtp_fields = [
'smtp_host' => 'sanitize_text_field',
'smtp_port' => 'absint', // Sanitize as integer
'smtp_encryption' => 'sanitize_key', // 'ssl', 'tls', or ''
'smtp_username' => 'sanitize_text_field',
'smtp_password' => 'sanitize_text_field', // Keep as text, WP handles display
'smtp_from_address' => 'sanitize_email',
'smtp_from_name' => 'sanitize_text_field',
];
foreach ( $smtp_fields as $key => $sanitize_callback ) {
if ( isset( $input[ $key ] ) ) {
$sanitized_input[ $key ] = call_user_func( $sanitize_callback, $input[ $key ] );
}
}
// Checkboxes (only save if present and value is '1')
$checkbox_fields = [ 'smtp_enabled', 'smtp_auth' ];
foreach ( $checkbox_fields as $key ) {
$sanitized_input[ $key ] = ( isset( $input[ $key ] ) && $input[ $key ] === '1' ) ? 1 : 0;
}
// Validate encryption value
if ( ! in_array( $sanitized_input['smtp_encryption'], [ '', 'ssl', 'tls' ], true ) ) {
$sanitized_input['smtp_encryption'] = ''; // Default to none if invalid
}
// Future settings added should have their sanitization logic implemented here.
return $sanitized_input;
}
/**
* Handles the AJAX request to send a test email.
*/
public function handle_send_test_email_ajax() {
check_ajax_referer( 'quiztech_test_email_nonce', '_ajax_nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Permission denied.', 'quiztech' ) ], 403 );
}
$admin_email = get_option( 'admin_email' );
if ( ! is_email( $admin_email ) ) {
wp_send_json_error( [ 'message' => __( 'Invalid site administrator email address.', 'quiztech' ) ] );
}
$subject = __( 'Quiztech SMTP Test Email', 'quiztech' );
$message = __( 'This is a test email sent from the Quiztech plugin settings page to verify your SMTP configuration.', 'quiztech' );
$headers = []; // Can add headers if needed, e.g., Content-Type
// Temporarily hook into phpmailer to capture errors
$error_message = '';
add_action( 'wp_mail_failed', function ( $wp_error ) use ( &$error_message ) {
if ( is_wp_error( $wp_error ) ) {
$error_message = $wp_error->get_error_message();
}
}, 10, 1 );
$sent = wp_mail( $admin_email, $subject, $message, $headers );
// Remove the temporary hook
// Note: Removing anonymous functions requires more complex handling (e.g., storing the closure in a property).
// For simplicity in this test function, we'll leave it, but be aware it persists for the request.
// A better approach for production code might involve a dedicated class or named function.
// remove_action( 'wp_mail_failed', $this->capture_wp_mail_error_closure ); // Example if stored
if ( $sent ) {
wp_send_json_success();
} else {
$error_to_send = ! empty( $error_message ) ? $error_message : __( 'The email could not be sent. Check your SMTP settings and server logs.', 'quiztech' );
wp_send_json_error( [ 'message' => $error_to_send ] );
}
}
}

View file

@ -83,10 +83,8 @@ class Invitations {
$message = "Please click the following link to take your assessment:\n\n" . $invite_url;
// $headers = ['Content-Type: text/html; charset=UTF-8']; // If sending HTML email
// $sent = \wp_mail($applicant_email, $subject, $message); // Corrected line 76
// return $sent;
return true; // Placeholder
$sent = \wp_mail($applicant_email, $subject, $message); // Corrected line 76
return $sent;
}
/**