Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/fluent-smtp/app/Bindings.php
+++ b/fluent-smtp/app/Bindings.php
@@ -16,6 +16,7 @@
'elasticmail' => 'FluentMailAppServicesMailerProvidersElasticMailHandler',
'smtp2go' => 'FluentMailAppServicesMailerProvidersSmtp2GoHandler',
'tosend' => 'FluentMailAppServicesMailerProvidersToSendHandler',
+ 'cloudflare' => 'FluentMailAppServicesMailerProvidersCloudflareHandler',
];
foreach ($singletons as $key => $className) {
--- a/fluent-smtp/app/Functions/helpers.php
+++ b/fluent-smtp/app/Functions/helpers.php
@@ -17,6 +17,49 @@
}
}
+if (!function_exists('fluentMailManageCapability')) {
+ /**
+ * The capability required to view and change FluentSMTP's settings.
+ *
+ * Every administrative surface in the plugin is guarded by this: the AJAX
+ * controllers, the settings page, the dashboard widget and the admin notices.
+ * It is a single value on purpose, so a site cannot end up with a settings
+ * screen that opens and an API that refuses, or the reverse.
+ *
+ * The default is unchanged, so nothing differs on an ordinary installation.
+ * The filter exists for hosts and multi-tenant products where the people who
+ * administer email are deliberately not WordPress administrators — granting
+ * them `manage_options` to reach this screen would hand over the whole site.
+ *
+ * Note for anyone filtering this: `manage_options` does not imply a custom
+ * capability, so a site administrator loses access unless the replacement is
+ * also granted to them.
+ *
+ * @since 2.3.0
+ *
+ * @param string $capability Capability name. Default `manage_options`.
+ * @return string
+ */
+ function fluentMailManageCapability()
+ {
+ return apply_filters('fluent_mail/manage_capability', 'manage_options');
+ }
+}
+
+if (!function_exists('fluentMailCurrentUserCanManage')) {
+ /**
+ * Whether the current user may administer FluentSMTP.
+ *
+ * @since 2.3.0
+ *
+ * @return bool
+ */
+ function fluentMailCurrentUserCanManage()
+ {
+ return current_user_can(fluentMailManageCapability());
+ }
+}
+
if (!function_exists('fluentMailMix')) {
/**
* Generate the mixed URL for the given asset path.
@@ -194,7 +237,7 @@
return $drivers[$connection['sender_email']];
}
- $region = 'email.' . $connection['region'] . '.amazonaws.com';
+ $region = SimpleEmailService::regionToHost($connection['region']);
$ses = new SimpleEmailService(
$connection['access_key'],
@@ -213,11 +256,30 @@
/**
* Sends an email using the wp_mail() function with additional filters and pre-send checks.
*
+ * Mirrors WordPress core's wp_mail(), which this replaces, so that hooks and
+ * arguments behave identically for anything that calls wp_mail().
+ *
+ * When using the `$embeds` parameter to embed images for use in HTML emails,
+ * reference the embedded file in your HTML with a `cid:` URL whose value
+ * matches the file's Content-ID. By default, the Content-ID (`cid`) used for
+ * each embedded file is the key in the embeds array, unless modified via the
+ * {@see 'wp_mail_embed_args'} filter. For example:
+ *
+ * `<img src="cid:0" alt="Logo">`
+ * `<img src="cid:my-image" alt="Image">`
+ *
+ * Note: embedded images are applied to the PHPMailer instance, so they work on
+ * SMTP connections. Providers that send over their own HTTP API build their
+ * payload separately and currently ignore embeds.
+ *
+ * @since 6.9.0 The `$embeds` parameter was added, matching WP core.
+ *
* @param string|array $to Array or comma-separated list of email addresses to send the message to.
* @param string $subject The subject of the email.
* @param string $message The message content of the email.
* @param string|array $headers Additional headers for the email.
- * @param array $attachments Paths to files to attach to the email.
+ * @param string|array $attachments Paths to files to attach to the email.
+ * @param string|array $embeds Optional. Paths to files to embed.
*
* @return bool|null Returns true if the email was successfully sent, false if sending was preempted, or null if there was an error.
*
@@ -225,19 +287,20 @@
* @filter wp_mail
* @filter pre_wp_mail
*/
- function fluentMailSend($to, $subject, $message, $headers = '', $attachments = array())
+ function fluentMailSend($to, $subject, $message, $headers = '', $attachments = array(), $embeds = array())
{
// Compact the input, apply the filters, and extract them back out.
/**
* Filters the wp_mail() arguments.
*
* @param array $args A compacted array of wp_mail() arguments, including the "to" email,
- * subject, message, headers, and attachments values.
+ * subject, message, headers, attachments and embeds values.
* @since 2.2.0
+ * @since 6.9.0 The `$embeds` element was added to the `$args` array.
*
*/
$atts = apply_filters(
- 'wp_mail', compact('to', 'subject', 'message', 'headers', 'attachments')
+ 'wp_mail', compact('to', 'subject', 'message', 'headers', 'attachments', 'embeds')
);
/**
@@ -256,6 +319,7 @@
* @type string $message Message contents.
* @type string|string[] $headers Additional headers.
* @type string|string[] $attachments Paths to files to attach.
+ * @type string|string[] $embeds Paths to files to embed.
* }
* @since 5.7.0
*
@@ -294,6 +358,14 @@
$attachments = explode("n", str_replace("rn", "n", $attachments));
}
+ if (isset($atts['embeds'])) {
+ $embeds = $atts['embeds'];
+ }
+
+ if (!is_array($embeds)) {
+ $embeds = explode("n", str_replace("rn", "n", $embeds));
+ }
+
global $phpmailer;
// (Re)create it, if it's gone missing.
@@ -301,7 +373,18 @@
require_once ABSPATH . WPINC . '/PHPMailer/PHPMailer.php';
require_once ABSPATH . WPINC . '/PHPMailer/SMTP.php';
require_once ABSPATH . WPINC . '/PHPMailer/Exception.php';
- $phpmailer = new PHPMailerPHPMailerPHPMailer(true);
+
+ // WP 6.8+ ships WP_PHPMailer, which routes PHPMailer's error strings
+ // through WordPress translations. Guarded on the file, not a version
+ // number, because this plugin still supports WP 5.5 where it is absent.
+ $wpPhpMailerFile = ABSPATH . WPINC . '/class-wp-phpmailer.php';
+
+ if (file_exists($wpPhpMailerFile)) {
+ require_once $wpPhpMailerFile;
+ $phpmailer = new WP_PHPMailer(true);
+ } else {
+ $phpmailer = new PHPMailerPHPMailerPHPMailer(true);
+ }
$phpmailer::$validator = static function ($email) {
return (bool)is_email($email);
@@ -350,7 +433,7 @@
if (false !== $bracket_pos) {
// Text before the bracketed email is the "From" name.
if ($bracket_pos > 0) {
- $from_name = substr($content, 0, $bracket_pos - 1);
+ $from_name = substr($content, 0, $bracket_pos);
$from_name = str_replace('"', '', $from_name);
$from_name = trim($from_name);
}
@@ -373,6 +456,12 @@
} elseif (false !== stripos($charset_content, 'boundary=')) {
$boundary = trim(str_replace(array('BOUNDARY=', 'boundary=', '"'), '', $charset_content));
$charset = '';
+ // Normalize the subtype and re-attach the boundary, so a
+ // header like "Content-Type: multipart/MIXED; boundary=x"
+ // yields a well-formed lowercase type. Matches WP 6.9.
+ if (preg_match('~^multipart/(S+)~', $content_type, $matches)) {
+ $content_type = 'multipart/' . strtolower($matches[1]) . '; boundary="' . $boundary . '"';
+ }
}
// Avoid setting an empty $content_type.
@@ -405,6 +494,24 @@
$phpmailer->clearReplyTos();
$phpmailer->Body = '';
$phpmailer->AltBody = '';
+ // The instance now persists across sends, so the envelope sender must
+ // reset like a fresh instance would. This runs before phpmailer_init,
+ // so listeners (and the return_path handlers) still set it per send.
+ $phpmailer->Sender = '';
+
+ /*
+ * Reset encoding to 8-bit, as it may have been automatically downgraded
+ * to 7-bit by PHPMailer (based on the body contents) in a previous call
+ * to wp_mail().
+ *
+ * Matches WP core. This matters more here than it does in core: the
+ * PHPMailer instance is now deliberately kept alive across sends for
+ * SMTP connection reuse, so without this reset one plain-ASCII email
+ * would downgrade the encoding for every later email in the request.
+ *
+ * See https://core.trac.wordpress.org/ticket/33972
+ */
+ $phpmailer->Encoding = PHPMailerPHPMailerPHPMailer::ENCODING_8BIT;
/*
* If we don't have an email from the input headers, default to wordpress@$sitename
@@ -573,9 +680,13 @@
}
}
- if (false !== stripos($content_type, 'multipart') && !empty($boundary)) {
- $phpmailer->addCustomHeader(sprintf('Content-Type: %s; boundary="%s"', $content_type, $boundary));
- }
+ /*
+ * The boundary is no longer re-attached here. As of WP 6.9 the boundary is
+ * folded into $content_type while parsing the Content-Type header above, and
+ * that value is assigned to $phpmailer->ContentType. Emitting it again as a
+ * custom header would produce a duplicate Content-Type carrying the boundary
+ * twice.
+ */
}
@@ -591,6 +702,51 @@
}
}
+ if (!empty($embeds)) {
+ foreach ($embeds as $key => $embed_path) {
+ /**
+ * Filters the arguments for PHPMailer's addEmbeddedImage() method.
+ *
+ * @since 6.9.0
+ *
+ * @param array $args {
+ * An array of arguments for PHPMailer's addEmbeddedImage() method.
+ *
+ * @type string $path The path to the file.
+ * @type string $cid The Content-ID of the image. Default: The key in the embeds array.
+ * @type string $name The filename of the image.
+ * @type string $encoding The encoding of the image. Default: 'base64'.
+ * @type string $type The MIME type of the image. Default: empty string, which lets PHPMailer auto-detect.
+ * @type string $disposition The disposition of the image. Default: 'inline'.
+ * }
+ */
+ $embed_args = apply_filters(
+ 'wp_mail_embed_args',
+ array(
+ 'path' => $embed_path,
+ 'cid' => (string)$key,
+ 'name' => basename($embed_path),
+ 'encoding' => 'base64',
+ 'type' => '',
+ 'disposition' => 'inline',
+ )
+ );
+
+ try {
+ $phpmailer->addEmbeddedImage(
+ $embed_args['path'],
+ $embed_args['cid'],
+ $embed_args['name'],
+ $embed_args['encoding'],
+ $embed_args['type'],
+ $embed_args['disposition']
+ );
+ } catch (PHPMailerPHPMailerException $e) {
+ continue;
+ }
+ }
+ }
+
/**
* Fires after PHPMailer is initialized.
*
@@ -612,13 +768,20 @@
}
}
- $mail_data = compact('to', 'subject', 'message', 'headers', 'attachments');
+ $mail_data = compact('to', 'subject', 'message', 'headers', 'attachments', 'embeds');
// Send!
try {
- // Trap the fluentSMTPMail mailer here
- $phpmailer = new FluentMailAppServicesMailerFluentPHPMailer($phpmailer);
- $send = $phpmailer->send();
+ // Trap the fluentSMTPMail mailer here. Deliberately a LOCAL
+ // variable: assigning the wrapper to the global $phpmailer made
+ // the next wp_mail() call fail its instanceof PHPMailer check and
+ // rebuild a fresh instance for every email — destroying the
+ // instance (and closing any kept-alive SMTP connection) between
+ // sends. Keeping the raw PHPMailer in the global matches WP core
+ // behavior and lets a FluentCRM bulk sending session reuse one
+ // SMTP connection across a whole run (BulkSendSessionHandler).
+ $fluentMailer = new FluentMailAppServicesMailerFluentPHPMailer($phpmailer);
+ $send = $fluentMailer->send();
/**
* Fires after a successful email is sent using the wp_mail() function.
@@ -687,6 +850,7 @@
'gmail' => 'client_secret',
'outlook' => 'client_secret',
'tosend' => 'api_key',
+ 'cloudflare' => 'api_key',
];
if (!empty($settings['connections']) && is_array($settings['connections'])) {
foreach ($settings['connections'] as $key => $connection) {
@@ -710,6 +874,25 @@
}
}
+ /*
+ * Sender names reach us HTML-escaped when they were taken from the site
+ * title, which WordPress stores that way. Left alone, a site called
+ * "Tom & Jerry" sends every email from "Tom & Jerry". Decoding here
+ * rather than only on save fixes installs that already hold an escaped
+ * name, without asking anyone to re-save a connection, and covers every
+ * reader — this function is the one path settings travel through.
+ */
+ if (!empty($settings['connections']) && is_array($settings['connections'])) {
+ foreach ($settings['connections'] as $key => $connection) {
+ if (!empty($connection['provider_settings']['sender_name'])) {
+ $settings['connections'][$key]['provider_settings']['sender_name'] = wp_specialchars_decode(
+ $connection['provider_settings']['sender_name'],
+ ENT_QUOTES
+ );
+ }
+ }
+ }
+
$cachedSettings = $settings;
return $settings;
@@ -753,6 +936,7 @@
'gmail' => 'client_secret',
'outlook' => 'client_secret',
'tosend' => 'api_key',
+ 'cloudflare' => 'api_key',
];
if (!empty($settings['connections']) && is_array($settings['connections'])) {
foreach ($settings['connections'] as $key => $connection) {
@@ -874,11 +1058,57 @@
return FluentSmtpDb();
}
+if (!function_exists('fluentMailDebugLog')) {
+ /**
+ * Write a diagnostic line to the PHP error log, but only while debugging.
+ *
+ * Every one of these sites reports a swallowed failure — a log row that
+ * could not be written, an attachment that could not be read. They recur:
+ * whatever broke once breaks again on every send, so an ungated
+ * error_log() grows the PHP error log for as long as the site keeps
+ * sending. That is noise a production admin never asked for and cannot
+ * turn off, while anyone actually chasing the failure has WP_DEBUG on.
+ *
+ * @param string $message Message to record, prefixed with the plugin name.
+ * @return void
+ */
+ function fluentMailDebugLog($message)
+ {
+ if (!defined('WP_DEBUG') || !WP_DEBUG) {
+ return;
+ }
+
+ error_log('FluentSMTP: ' . $message);
+ }
+}
+
+if (!function_exists('fluentMailSiteTitle')) {
+ /**
+ * The site title as the admin typed it, safe to drop into plain text.
+ *
+ * WordPress stores blogname HTML-escaped — sanitize_option() runs
+ * esc_html() over it — so a site called "Tom & Jerry" comes back from
+ * get_bloginfo('name') as "Tom & Jerry". That is correct for HTML
+ * output and wrong everywhere else: an email subject, a Slack message or
+ * a push notification is plain text, and the entity shows up literally.
+ *
+ * Core has the same problem and solves it the same way — see the
+ * wp_specialchars_decode() calls around get_option('blogname') in
+ * pluggable.php. Use this anywhere the title is not being written into
+ * HTML; keep esc_html(fluentMailSiteTitle()) where it is.
+ *
+ * @return string
+ */
+ function fluentMailSiteTitle()
+ {
+ return wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES);
+ }
+}
function fluentMailFuncCouldNotBeLoadedRecheckPluginsLoad()
{
add_action('admin_notices', function () {
- if (!current_user_can('manage_options')) {
+ if (!fluentMailCurrentUserCanManage()) {
return;
}
$details = new ReflectionFunction('wp_mail');
@@ -891,7 +1121,7 @@
disable it for <strong>FluentSMTP</strong> to work!</p>
<p style="color: red;">
<?php esc_html_e('Possible Conflict: ', 'fluent-smtp'); ?>
- <?php echo $hints; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
+ <?php echo esc_html($hints); ?>
</p>
</div>
<?php
--- a/fluent-smtp/app/Hooks/Handlers/ActionsRegistrar.php
+++ b/fluent-smtp/app/Hooks/Handlers/ActionsRegistrar.php
@@ -52,6 +52,25 @@
$this->registerSiteInitialization();
$this->registerCustomActions();
$this->registerRestRoutes();
+
+ // SMTP connection reuse across FluentCRM bulk sending sessions.
+ (new BulkSendSessionHandler())->register();
+
+ $this->registerCliCommands();
+ }
+
+ /**
+ * Register the `wp fluent-smtp` commands.
+ *
+ * @return void
+ */
+ protected function registerCliCommands()
+ {
+ if (!defined('WP_CLI') || !WP_CLI || !class_exists('WP_CLI')) {
+ return;
+ }
+
+ WP_CLI::add_command('fluent-smtp', 'FluentMailAppServicesCliHandler');
}
/**
--- a/fluent-smtp/app/Hooks/Handlers/AdminMenuHandler.php
+++ b/fluent-smtp/app/Hooks/Handlers/AdminMenuHandler.php
@@ -77,20 +77,19 @@
<div
style="background-color: #fff;border: 1px solid #dcdcde;box-sizing: border-box;padding: 20px;margin: 15px 0;"
class="fluent_smtp_box">
- <h3 style="margin: 0;"><?php __('For SMTP, you already have FluentSMTP Installed', 'fluent-smtp'); ?></h3>
- <p><?php __('You seem to be looking for an SMTP plugin, but there's no need for another one — FluentSMTP is already installed on your site. FluentSMTP is a comprehensive, free, and open-source plugin with full features available without any upsell', 'fluent-smtp'); ?>
- (<a href="https://fluentsmtp.com/why-we-built-fluentsmtp-plugin/"><?php __('learn why it's free', 'fluent-smtp'); ?></a>)<?php __('. It's compatible with various SMTP services, including Amazon SES, SendGrid, MailGun, ElasticEmail, SendInBlue, Google, Microsoft, and others, providing you with a wide range of options for your email needs.', 'fluent-smtp'); ?>
+ <h3 style="margin: 0;"><?php esc_html_e('For SMTP, you already have FluentSMTP Installed', 'fluent-smtp'); ?></h3>
+ <p><?php esc_html_e('You seem to be looking for an SMTP plugin, but there's no need for another one — FluentSMTP is already installed on your site. FluentSMTP is a comprehensive, free, and open-source plugin with full features available without any upsell', 'fluent-smtp'); ?>
+ (<a href="https://fluentsmtp.com/articles/why-we-built-fluentsmtp-plugin/"><?php esc_html_e('learn why it's free', 'fluent-smtp'); ?></a>)<?php esc_html_e('. It's compatible with various SMTP services, including Amazon SES, SendGrid, MailGun, ElasticEmail, SendInBlue, Google, Microsoft, and others, providing you with a wide range of options for your email needs.', 'fluent-smtp'); ?>
</p><a href="<?php echo esc_url(admin_url('options-general.php?page=fluent-mail#/')); ?>"
- class="wp-core-ui button button-primary"><?php __('Go To FluentSMTP Settings', 'fluent-smtp'); ?></a>
- <p style="font-size: 80%; margin: 15px 0 0;"><?php __('This notice is from FluentSMTP plugin to prevent plugin
- conflict.', 'fluent-smtp') ?></p>
+ class="wp-core-ui button button-primary"><?php esc_html_e('Go To FluentSMTP Settings', 'fluent-smtp'); ?></a>
+ <p style="font-size: 80%; margin: 15px 0 0;"><?php esc_html_e('This notice is from FluentSMTP plugin to prevent plugin conflict.', 'fluent-smtp'); ?></p>
</div>
<?php
}, 1);
add_action('wp_ajax_fluent_smtp_get_dashboard_html', function () {
// This widget should be displayed for certain high-level users only.
- if (!current_user_can('manage_options') || apply_filters('fluent_mail_disable_dashboard_widget', false)) {
+ if (!fluentMailCurrentUserCanManage() || apply_filters('fluent_mail_disable_dashboard_widget', false)) {
wp_send_json([
'html' => __('You do not have permission to see this data', 'fluent-smtp')
]);
@@ -111,7 +110,7 @@
'options-general.php',
$title,
$title,
- 'manage_options',
+ fluentMailManageCapability(),
'fluent-mail',
[$this, 'renderApp'],
16
@@ -183,7 +182,9 @@
$user = get_user_by('ID', get_current_user_id());
- $disable_recommendation = wp_is_file_mod_allowed('install_plugins');
+ // wp_is_file_mod_allowed() answers "are mods ALLOWED"; this flag is the
+ // inverse — it hides the one-click install button — so it must be negated.
+ $disable_installation = !wp_is_file_mod_allowed('install_plugins');
$settings = $this->getMailerSettings();
@@ -210,7 +211,7 @@
'require_optin' => $this->isRequireOptin(),
'has_ninja_tables' => defined('NINJA_TABLES_VERSION'),
'disable_recommendation' => apply_filters('fluentmail_disable_recommendation', false),
- 'disable_installation' => $disable_recommendation,
+ 'disable_installation' => $disable_installation,
'plugin_url' => 'https://fluentsmtp.com/?utm_source=wp&utm_medium=install&utm_campaign=dashboard',
'trans' => $this->getTrans(),
'recommended' => $recommendedSettings,
@@ -231,7 +232,7 @@
return sprintf(
__('%1$s is a free plugin & it will be always free %2$s. %3$s', 'fluent-smtp'),
'<b>FluentSMTP</b>',
- '<a href="https://fluentsmtp.com/why-we-built-fluentsmtp-plugin/" target="_blank" rel="noopener noreferrer">'. esc_html__('(Learn why it's free)', 'fluent-smtp') .'</a>',
+ '<a href="https://fluentsmtp.com/articles/why-we-built-fluentsmtp-plugin/" target="_blank" rel="noopener noreferrer">'. esc_html__('(Learn why it's free)', 'fluent-smtp') .'</a>',
'<a href="https://wordpress.org/support/plugin/fluent-smtp/reviews/?filter=5" target="_blank" rel="noopener noreferrer">'. esc_html__('Write a review ★★★★★', 'fluent-smtp') .'</a>'
);
});
@@ -263,7 +264,7 @@
public function maybeAdminNotice()
{
- if (!current_user_can('manage_options')) {
+ if (!fluentMailCurrentUserCanManage()) {
return;
}
@@ -298,7 +299,7 @@
public function addSimulationBar($adminBar)
{
- if (!current_user_can('manage_options')) {
+ if (!fluentMailCurrentUserCanManage()) {
return;
}
@@ -337,7 +338,7 @@
public function initAdminWidget()
{
// This widget should be displayed for certain high-level users only.
- if (!current_user_can('manage_options') || apply_filters('fluent_mail_disable_dashboard_widget', false)) {
+ if (!fluentMailCurrentUserCanManage() || apply_filters('fluent_mail_disable_dashboard_widget', false)) {
return;
}
@@ -434,9 +435,9 @@
<tbody>
<?php foreach ($stats as $stat): ?>
<tr>
- <td><?php echo $stat['title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
- <td><?php echo $stat['sent']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
- <td class="<?php echo ($stat['failed']) ? 'fstmp_failed' : ''; ?>"><?php echo $stat['failed']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
+ <td><?php echo esc_html($stat['title']); ?></td>
+ <td><?php echo absint($stat['sent']); ?></td>
+ <td class="<?php echo absint($stat['failed']) ? 'fstmp_failed' : ''; ?>"><?php echo absint($stat['failed']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
--- a/fluent-smtp/app/Hooks/Handlers/BulkSendSessionHandler.php
+++ b/fluent-smtp/app/Hooks/Handlers/BulkSendSessionHandler.php
@@ -0,0 +1,222 @@
+<?php
+
+namespace FluentMailAppHooksHandlers;
+
+/**
+ * Reuses one SMTP connection across a FluentCRM bulk sending session.
+ *
+ * FluentCRM fires fluent_crm/email_sender_session_started / _ended around each
+ * lock-winning sender run (up to ~50 seconds spanning many claimed batches).
+ * For the SMTP provider, the per-email connect + EHLO + STARTTLS + AUTH + QUIT
+ * handshake is typically 100-300ms — often more than the MAIL/RCPT/DATA
+ * exchange itself — so holding the connection open for the session is a 2-5x
+ * throughput win on SMTP relays. Sockets are still recycled after
+ * MAX_CONNECTION_AGE seconds: the handshake savings only need tens of sends
+ * per connection, and short-lived sockets stay clear of relay idle timeouts,
+ * message-per-connection caps, and any state an old connection could carry.
+ *
+ * The HTTP API providers need nothing here: their connections are already
+ * reused via statically cached service/request objects holding persistent
+ * cURL handles (see fluentMailSesConnection() and the ToSend handler).
+ *
+ * The session actions may fire repeatedly or unpaired (FluentCRM documents
+ * them as such), so every method here is idempotent. Keep-alive itself is
+ * enabled per send by the SMTP provider handler — the only path that
+ * declares a connection identity via ensureConnectionFor() — so mail sent
+ * by other phpmailer_init integrations never rides an unguarded kept-alive
+ * socket. A shutdown close is registered as a crash net; an orphaned socket
+ * dies with the PHP process anyway, and one-off emails sent outside a
+ * session keep the normal connect-per-send behavior.
+ */
+class BulkSendSessionHandler
+{
+ /**
+ * Hard cap on how long one kept-alive socket may be reused, in seconds.
+ */
+ const MAX_CONNECTION_AGE = 15;
+
+ protected static $active = false;
+
+ /**
+ * Unix timestamp of the most recent session start, for self-expiry.
+ */
+ protected static $activeSince = 0;
+
+ protected static $hooked = false;
+
+ /**
+ * Identity of the SMTP connection the kept-alive socket belongs to.
+ * FluentSMTP routes per From address, so one bulk session can interleave
+ * emails bound for DIFFERENT relays/credentials — and PHPMailer's
+ * smtpConnect() reuses an open socket without re-checking Host or auth.
+ * Reusing across identities would send mail through the wrong relay.
+ */
+ protected static $connectionFingerprint = null;
+
+ /**
+ * Unix timestamp of when the current kept-alive connection was opened,
+ * for the MAX_CONNECTION_AGE recycle.
+ */
+ protected static $connectionOpenedAt = 0;
+
+ /**
+ * Whether the current send is riding a socket left open by a previous
+ * send. The SMTP handler's dead-socket retry only re-sends when the
+ * failure came from a reused connection — a fresh-connect failure is a
+ * real error (relay down, bad credentials) a retry would not fix.
+ */
+ protected static $socketReused = false;
+
+ public function register()
+ {
+ add_action('fluent_crm/email_sender_session_started', [$this, 'startSession']);
+ add_action('fluent_crm/email_sender_session_ended', [$this, 'endSession']);
+ }
+
+ public function startSession()
+ {
+ // Kill switch: some relays cap messages-per-connection, so sites must
+ // be able to disable connection reuse at runtime. Everything else in
+ // this class is inert while no session is marked active.
+ if (!apply_filters('fluentmail_smtp_bulk_keep_alive', true)) {
+ return;
+ }
+
+ self::$active = true;
+ self::$activeSince = time();
+
+ if (self::$hooked) {
+ return;
+ }
+ self::$hooked = true;
+
+ register_shutdown_function([$this, 'endSession']);
+ }
+
+ public function endSession()
+ {
+ // Deliberately checks the raw flag (not isActive()): a session_ended
+ // arriving after the 5-minute self-expiry must still close the socket.
+ if (!self::$active) {
+ return;
+ }
+
+ self::$active = false;
+
+ self::closeConnection();
+ }
+
+ /**
+ * Whether a FluentCRM bulk sending session is currently active.
+ *
+ * Sessions self-expire after 5 minutes: real sessions last ~50s and every
+ * new lock-winning sender run re-fires session_started (refreshing the
+ * stamp), so only an orphaned session — an unpaired start in a long-lived
+ * process that never received its session_ended — can reach the limit.
+ * Expiry degrades gracefully to connect-per-send, and the next declared
+ * send's hygiene pass in ensureConnectionFor() closes whatever socket
+ * the expired session left open.
+ *
+ * @return bool
+ */
+ public static function isActive()
+ {
+ return self::$active && (time() - self::$activeSince) < 300;
+ }
+
+ /**
+ * Guard the kept-alive socket against connection switches mid-session.
+ *
+ * The SMTP provider calls this with its resolved connection settings
+ * before each send. Same identity as the open socket -> keep reusing it.
+ * Different identity (another relay or credentials, routed by From
+ * address) -> close first, so PHPMailer reconnects with the new settings
+ * instead of pushing mail through the previous relay's session. A streak
+ * of same-connection emails keeps the full keep-alive benefit; alternating
+ * connections degrade gracefully to connect-per-send.
+ *
+ * @param array $config Connection-identifying settings (host, port,
+ * username, encryption, auth, auto_tls, and a hash
+ * of the credential so rotations force a reconnect).
+ */
+ public static function ensureConnectionFor($config)
+ {
+ global $phpmailer;
+
+ $connected = $phpmailer
+ && method_exists($phpmailer, 'getSMTPInstance')
+ && $phpmailer->getSMTPInstance()->connected();
+
+ // Hygiene runs BEFORE the active check on purpose: PHPMailer's
+ // smtpConnect() silently reuses any connected socket without
+ // re-checking Host or credentials, so a socket left open by an
+ // ended/expired session — or one past its age cap — must be closed
+ // here, before this send gets a chance to adopt it.
+ if ($connected && (!self::isActive() || (time() - self::$connectionOpenedAt) >= self::MAX_CONNECTION_AGE)) {
+ self::closeConnection();
+ $connected = false;
+ }
+
+ if (!self::isActive()) {
+ return;
+ }
+
+ $fingerprint = md5(wp_json_encode($config));
+
+ // Close on ANY identity change — including from the unknown (null)
+ // state, so a socket some other integration left open is never
+ // adopted as ours. Closing an already-closed socket is a cheap no-op.
+ if (self::$connectionFingerprint !== $fingerprint) {
+ self::closeConnection();
+ $connected = false;
+ }
+
+ self::$connectionFingerprint = $fingerprint;
+ self::$socketReused = $connected;
+
+ if (!$connected) {
+ // This send opens a fresh connection right after this call;
+ // stamp its birth so the age cap above can recycle it.
+ self::$connectionOpenedAt = time();
+ }
+ }
+
+ /**
+ * Whether the send being declared reuses an already-open kept-alive
+ * socket (vs establishing a fresh connection).
+ *
+ * @return bool
+ */
+ public static function wasSocketReused()
+ {
+ return self::$socketReused;
+ }
+
+ /**
+ * Close the kept-alive SMTP connection, if one is open.
+ *
+ * Also called by the SMTP provider handler when a send fails mid-session:
+ * a relay may drop the idle socket in a way PHPMailer only notices on the
+ * next command, so closing here makes the following email reconnect fresh
+ * instead of failing on the same dead connection.
+ */
+ public static function closeConnection()
+ {
+ // Whatever socket existed no longer does; the next send establishes
+ // (and re-fingerprints) its own connection.
+ self::$connectionFingerprint = null;
+ self::$connectionOpenedAt = 0;
+ self::$socketReused = false;
+
+ global $phpmailer;
+
+ if ($phpmailer && method_exists($phpmailer, 'smtpClose')) {
+ try {
+ $phpmailer->SMTPKeepAlive = false;
+ $phpmailer->smtpClose();
+ } catch (Throwable $e) {
+ // Closing a dead socket must never break the send loop.
+ }
+ }
+ }
+}
--- a/fluent-smtp/app/Hooks/Handlers/SchedulerHandler.php
+++ b/fluent-smtp/app/Hooks/Handlers/SchedulerHandler.php
@@ -4,6 +4,7 @@
use FluentMailAppModelsLogger;
use FluentMailAppModelsSettings;
+use FluentMailAppServicesConnectionHealth;
use FluentMailAppServicesNotificationHelper;
use FluentMailAppServicesNotificationManager as NotificationManager;
use FluentMailIncludesSupportArr;
@@ -18,14 +19,29 @@
add_filter('fluentmail_email_sending_failed', array($this, 'maybeHandleFallbackConnection'), 10, 4);
add_action('fluentsmtp_renew_gmail_token', array($this, 'renewGmailToken'));
+ add_action('fluentsmtp_renew_outlook_token', array($this, 'renewOutlookToken'));
add_action('fluentmail_email_sending_failed_no_fallback', array($this, 'maybeSendNotification'), 10, 3);
+
+ add_action('fluentmail_connection_health_failed', array($this, 'notifyUnhealthyConnections'));
}
public function handleScheduledJobs()
{
$this->deleteOldEmails();
$this->sendDailyDigest();
+ $this->checkConnectionHealth();
+ }
+
+ /**
+ * Doubles as the safety net for token renewal. The single events that keep
+ * the OAuth tokens fresh are only ever armed by a successful renewal, so a
+ * single failed one used to end the chain silently - this daily pass picks
+ * it back up.
+ */
+ private function checkConnectionHealth()
+ {
+ (new ConnectionHealth())->checkAll();
}
private function deleteOldEmails()
@@ -215,6 +231,106 @@
}
}
+ /**
+ * Announce connections that have just started failing their health check,
+ * over whichever channels the site has already set up.
+ *
+ * @param array $failing
+ * @return void
+ */
+ public function notifyUnhealthyConnections($failing)
+ {
+ if (!$failing) {
+ return;
+ }
+
+ $notificationManager = new NotificationManager();
+ $channels = $notificationManager->getActiveChannels();
+
+ if (!$channels) {
+ return;
+ }
+
+ foreach ($failing as $connection) {
+ $provider = Arr::get($connection, 'provider');
+ $errorMessage = Arr::get($connection, 'message');
+
+ $message = sprintf(
+ /* translators: 1: site name, 2: provider name, 3: sender email, 4: error message */
+ __('FluentSMTP on %1$s cannot use the %2$s connection for %3$s any more: %4$s', 'fluent-smtp'),
+ fluentMailSiteTitle(),
+ $provider,
+ Arr::get($connection, 'sender_email'),
+ $errorMessage
+ );
+
+ foreach ($channels as $channel) {
+ $driver = $channel['driver'];
+ $channelSettings = Arr::get($channel, 'settings', []);
+
+ if ($driver == 'telegram') {
+ NotificationHelper::sendFailedNotificationTele([
+ 'token_id' => Arr::get($channelSettings, 'token'),
+ 'provider' => $provider,
+ 'error_message' => $errorMessage
+ ]);
+ continue;
+ }
+
+ if ($driver == 'slack') {
+ NotificationHelper::sendSlackMessage($message, Arr::get($channelSettings, 'webhook_url'), false);
+ continue;
+ }
+
+ if ($driver == 'discord') {
+ NotificationHelper::sendDiscordMessage($message, Arr::get($channelSettings, 'webhook_url'), false);
+ continue;
+ }
+
+ if ($driver == 'pushover') {
+ NotificationHelper::sendPushoverMessage(
+ $message,
+ Arr::get($channelSettings, 'api_token'),
+ Arr::get($channelSettings, 'user_key'),
+ false,
+ 1
+ );
+ continue;
+ }
+ }
+ }
+ }
+
+ public function renewOutlookToken()
+ {
+ $settings = fluentMailGetSettings();
+
+ if (!$settings) {
+ return;
+ }
+
+ $connections = Arr::get($settings, 'connections', []);
+
+ foreach ($connections as $connection) {
+ $providerSettings = Arr::get($connection, 'provider_settings', []);
+
+ if (Arr::get($providerSettings, 'provider') != 'outlook') {
+ continue;
+ }
+
+ if (empty($providerSettings['refresh_token'])) {
+ continue;
+ }
+
+ if ((Arr::get($providerSettings, 'expire_stamp') - 480) >= time()) {
+ continue;
+ }
+
+ $handler = new FluentMailAppServicesMailerProvidersOutlookHandler();
+ $handler->renewToken($providerSettings);
+ }
+ }
+
public function callGmailApiForNewToken($settings)
{
if (Arr::get($settings, 'key_store') == 'wp_config') {
@@ -246,7 +362,16 @@
$result = $this->saveNewGmailTokens($settings, $newTokens);
if (!$result) {
- return new WP_Error('api_error', __('Failed to renew the token', 'fluent-smtp'));
+ // Google says why in error_description - usually that the
+ // grant was revoked or expired, which only re-authenticating
+ // fixes. Reporting "failed" alone leaves nothing to act on.
+ $errorDescription = Arr::get($newTokens, 'error_description');
+
+ if (!$errorDescription) {
+ $errorDescription = __('Failed to renew the token with the Gmail API', 'fluent-smtp');
+ }
+
+ return new WP_Error('api_error', $errorDescription);
}
return true;
--- a/fluent-smtp/app/Http/Controllers/Controller.php
+++ b/fluent-smtp/app/Http/Controllers/Controller.php
@@ -45,7 +45,7 @@
public function verify()
{
- $permission = 'manage_options';
+ $permission = fluentMailManageCapability();
if(!current_user_can($permission)) {
wp_send_json_error([
'message' => __('You do not have permission to do this action', 'fluent-smtp')
--- a/fluent-smtp/app/Http/Controllers/DashboardController.php
+++ b/fluent-smtp/app/Http/Controllers/DashboardController.php
@@ -3,6 +3,7 @@
namespace FluentMailAppHttpControllers;
use FluentMailAppModelsLogger;
+use FluentMailAppServicesConnectionHealth;
use FluentMailAppServicesMailerManager;
use FluentMailAppServicesReporting;
use FluentMailIncludesRequestRequest;
@@ -17,8 +18,9 @@
$connections = $manager->getSettings('connections', []);
return $this->send([
- 'stats' => $logger->getStats(),
- 'settings_stat' => [
+ 'stats' => $logger->getStats(),
+ 'unhealthy_settings' => array_values((new ConnectionHealth())->getFailing()),
+ 'settings_stat' => [
'connection_counts' => count($connections),
'active_senders' => count($manager->getSettings('mappings', [])),
'auto_delete_days' => $manager->getSettings('misc.log_saved_interval_days'),
@@ -31,40 +33,47 @@
{
$this->verify();
+ // Validate and sanitize input with absint() and constrain to reasonable range
$lastDay = 0;
if (isset($_REQUEST['last_day'])) {
- $lastDay = (int)$_REQUEST['last_day'];
+ $lastDay = absint($_REQUEST['last_day']);
+ // Constrain to reasonable range: 0-365 days
+ $lastDay = min(max($lastDay, 0), 365);
}
global $wpdb;
+ $tableName = $wpdb->prefix . 'fsmpt_email_logs';
+
if ($lastDay > 6) {
- $results = $wpdb->get_results("SELECT
- DAYNAME(created_at) AS day_of_week,
- HOUR(created_at) AS hour_of_day,
- COUNT(*) AS count
-FROM
- {$wpdb->prefix}fsmpt_email_logs
-WHERE
- created_at >= NOW() - INTERVAL {$lastDay} DAY
-GROUP BY
- DAYNAME(created_at),
- HOUR(created_at)
-ORDER BY
- FIELD(DAYNAME(created_at), 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'),
- HOUR(created_at)");
+ // Use wpdb->prepare() with proper placeholder for the interval value
+ $results = $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT
+ DAYNAME(created_at) AS day_of_week,
+ HOUR(created_at) AS hour_of_day,
+ COUNT(*) AS count
+ FROM {$tableName}
+ WHERE created_at >= NOW() - INTERVAL %d DAY
+ GROUP BY
+ DAYNAME(created_at),
+ HOUR(created_at)",
+ $lastDay
+ )
+ );
} else {
- $results = $wpdb->get_results("SELECT
- DAYNAME(created_at) AS day_of_week,
- HOUR(created_at) AS hour_of_day,
- COUNT(*) AS count
-FROM
- {$wpdb->prefix}fsmpt_email_logs
-GROUP BY
- DAYNAME(created_at),
- HOUR(created_at)
-ORDER BY
- FIELD(DAYNAME(created_at), 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'),
- HOUR(created_at)");
+ // Query for all time data when lastDay <= 6
+ // Table name is safe - constructed from WordPress prefix and hard-coded table suffix
+ // No user input in this query, so prepare() is not needed
+ $results = $wpdb->get_results(
+ "SELECT
+ DAYNAME(created_at) AS day_of_week,
+ HOUR(created_at) AS hour_of_day,
+ COUNT(*) AS count
+ FROM {$tableName}
+ GROUP BY
+ DAYNAME(created_at),
+ HOUR(created_at)"
+ );
}
// Assuming $results is the array of records fetched from the database.
--- a/fluent-smtp/app/Http/Controllers/DiscordController.php
+++ b/fluent-smtp/app/Http/Controllers/DiscordController.php
@@ -49,6 +49,8 @@
public function sendTestMessage(Request $request)
{
+ $this->verify();
+
// Let's update the notification status
$settings = (new Settings())->notificationSettings();
@@ -77,6 +79,8 @@
public function disconnect()
{
+ $this->verify();
+
NotificationHelper::updateChannelSettings('discord', [
'status' => 'no',
'webhook_url' => '',
--- a/fluent-smtp/app/Http/Controllers/LoggerController.php
+++ b/fluent-smtp/app/Http/Controllers/LoggerController.php
@@ -63,10 +63,29 @@
], $response->get_error_code());
});
- if ($email = $logger->resendEmailFromLog($request->get('id'), $request->get('type'))) {
+ $recipients = $this->sanitizeRecipients($request->get('recipients'));
+
+ $resentEmail = $logger->resendEmailFromLog(
+ $request->get('id'),
+ $request->get('type'),
+ $recipients
+ );
+
+ if ($resentEmail) {
+ $message = __('Email sent successfully.', 'fluent-smtp');
+
+ if (!empty($recipients)) {
+ $message = sprintf(
+ /* translators: %s: comma separated list of email addresses */
+ __('Email sent successfully to %s.', 'fluent-smtp'),
+ implode(', ', $recipients)
+ );
+ }
+
return $this->sendSuccess([
- 'email' => $email,
- 'message' => __('Email sent successfully.', 'fluent-smtp')
+ 'email' => $resentEmail,
+ 'recipients' => $recipients,
+ 'message' => $message
]);
}
@@ -79,6 +98,83 @@
}
}
+ /**
+ * Sanitize and validate a list of recipient email addresses coming
+ * from the resend dialog. Returns an array of valid email addresses
+ * or an empty array when none are provided / valid.
+ *
+ * @param mixed $rawRecipients
+ * @return array<int, string>
+ * @throws Exception When an invalid email address is supplied.
+ */
+ protected function sanitizeRecipients($rawRecipients)
+ {
+ if (empty($rawRecipients)) {
+ return [];
+ }
+
+ if (is_string($rawRecipients)) {
+ $rawRecipients = preg_split('/[s,;]+/', $rawRecipients);
+ }
+
+ /*
+ * Refuse rather than fall through. Returning an empty list here would
+ * mean a malformed request quietly resends to the ORIGINAL recipient
+ * instead of the one that was asked for - the worst possible answer,
+ * since the caller is told the send succeeded and never learns it went
+ * somewhere else.
+ */
+ if (!is_array($rawRecipients)) {
+ throw new Exception(
+ esc_html__('Could not read the recipient list.', 'fluent-smtp'),
+ 422
+ );
+ }
+
+ /*
+ * A resend is a manual, one-at-a-time action. A list this long is a
+ * mistake or a pasted address book, and either way it is better caught
+ * than delivered.
+ */
+ $maxRecipients = apply_filters('fluentsmtp_max_resend_recipients', 25);
+
+ if (count($rawRecipients) > $maxRecipients) {
+ throw new Exception(
+ sprintf(
+ /* translators: %d: maximum number of recipients allowed */
+ esc_html__('Please enter no more than %d email addresses.', 'fluent-smtp'),
+ (int) $maxRecipients
+ ),
+ 422
+ );
+ }
+
+ $recipients = [];
+
+ foreach ($rawRecipients as $recipient) {
+ $recipient = sanitize_email(trim((string) $recipient));
+
+ if ($recipient === '') {
+ continue;
+ }
+
+ if (!is_email($recipient)) {
+ throw new Exception(
+ sprintf(
+ /* translators: %s: email address */
+ esc_html__('Invalid email address: %s', 'fluent-smtp'),
+ esc_html($recipient)
+ ),
+ 422
+ );
+ }
+
+ $recipients[] = $recipient;
+ }
+
+ return array_values(array_unique($recipients));
+ }
+
public function retryBulk(Request $request, Logger $logger)
{
$this->verify();
--- a/fluent-smtp/app/Http/Controllers/PushoverController.php
+++ b/fluent-smtp/app/Http/Controllers/PushoverController.php
@@ -40,6 +40,8 @@
public function sendTestMessage(Request $request)
{
+ $this->verify();
+
$settings = (new Settings())->notificationSettings();
if (Arr::get($settings, 'pushover.status') != 'yes') {
@@ -68,6 +70,8 @@
public function disconnect()
{
+ $this->verify();
+
NotificationHelper::updateChannelSettings('pushover', [
'status' => 'no',
'api_token' => '',
--- a/fluent-smtp/app/Http/Controllers/SettingsController.php
+++ b/fluent-smtp/app/Http/Controllers/SettingsController.php
@@ -75,6 +75,15 @@
if (is_string($value) && $value) {
$connection[$index] = sanitize_text_field($value);
+
+ // Store the name the admin typed. A sender name copied from
+ // the site title arrives HTML-escaped, and it is plain text
+ // everywhere it is used. fluentMailGetSettings() decodes on
+ // read as well, so installs that already hold an escaped
+ // name are fixed whether or not they ever save again.
+ if ($index === 'sender_name') {
+ $connection[$index] = wp_specialchars_decode($connection[$index], ENT_QUOTES);
+ }
}
}
@@ -158,16 +167,57 @@
define('FLUENTMAIL_EMAIL_TESTING', true);
}
+ $startedAt = microtime(true);
+
$settings->sendTestEmail($data, $settings->get());
+ /*
+ * The handover to the provider is synchronous, so this covers the whole
+ * round trip: connection/handshake, the API call or SMTP conversation and
+ * the provider's response. It is not the time until the mail lands in the
+ * inbox - that part is out of our hands.
+ */
+ $timeTaken = microtime(true) - $startedAt;
+
return $this->sendSuccess([
- 'message' => __('Email delivered successfully.', 'fluent-smtp')
- ]);
- } catch (Exception $e) {
+ 'message' => __('Email delivered successfully.', 'fluent-smtp'),
+ 'time_taken' => round($timeTaken, 3),
+ 'time_taken_human' => $this->formatDuration($timeTaken)
+ ]);
+ } catch (Throwable $e) {
+ /*
+ * Throwable, not Exception. A missing PHP extension, a type error or
+ * any other engine-level failure raised while sending is an Error,
+ * which catch(Exception) lets through — the AJAX request then died
+ * with no JSON body and the UI span forever with no message shown.
+ *
+ * getCode() is meaningless on an Error (almost always 0) and an HTTP
+ * status of 0 is not valid, so only a sane positive code is honoured.
+ */
+ $code = (int)$e->getCode();
+ if ($code < 400 || $code > 599) {
+ $code = 422;
+ }
+
return $this->sendError([
'message' => $e->getMessage()
- ], $e->getCode());
+ ], $code);
+ }
+ }
+
+ protected function formatDuration($seconds)
+ {
+ if ($seconds < 1) {
+ return sprintf(
+ __('Delivered in %s milliseconds', 'fluent-smtp'),
+ number_format_i18n($seconds * 1000)
+ );
}
+
+ return sprintf(
+ __('Delivered in %s seconds', 'fluent-smtp'),
+ number_format_i18n($seconds, 2)
+ );
}
public function onFail($response)
@@ -296,7 +346,34 @@
public function installPlugin(Request $request)
{
$this->verify();
- $pluginSlug = $request->get('plugin_slug');
+
+ // Sanitize plugin slug input
+ $pluginSlug = sanitize_key($request->get('plugin_slug'));
+
+ // Define whitelist of allowed plugins
+ $allowedPlugins = ['fluentform', 'fluent-crm', 'ninja-tables'];
+
+ // Validate plugin slug against whitelist with strict comparison
+ if (!in_array($pluginSlug, $allowedPlugins, true)) {
+ return $this->sendError([
+ 'message' => __('Invalid plugin specified. Only approved plugins can be installed.', 'fluent-smtp')
+ ]);
+ }
+
+ // Verify user has permission to install plugins
+ if (!current_user_can('install_plugins')) {
+ return $this->sendError([
+ 'message' => __('Sorry, you do not have permission to install plugins.', 'fluent-smtp')
+ ]);
+ }
+
+ // Verify file modifications are allowed
+ if (!wp_is_file_mod_allowed('install_plugins')) {
+ return $this->sendError([
+ 'message' => __('Plugin installation is disabled on this site.', 'fluent-smtp')
+ ]);
+ }
+
$plugin = [
'name' => $pluginSlug,
'repo-slug' => $pluginSlug,
@@ -318,20 +395,14 @@
]
];
- if (!isset($UrlMaps[$pluginSlug]) || !wp_is_file_mod_allowed('install_plugins')) {
- $this->sendError([
- 'message' => __('Sorry, You can not install this plugin', 'fluent-smtp')
- ]);
- }
-
try {
$this->backgroundInstaller($plugin);
- $this->send([
+ return $this->send([
'message' => __('Plugin has been successfully installed.', 'fluent-smtp'),
'info' => $UrlMaps[$pluginSlug]
]);
} catch (Exception $exception) {
- $this->sendError([
+ return $this->sendError([
'message' => $exception->getMessage()
]);
}
@@ -453,23 +524,23 @@
public function subscribe()
{
$this->verify();
- $email = sanitize_text_field($_REQUEST['email']); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
- $displayName = '';
+ // Properly sanitize email input with sanitize_email() instead of sanitize_text_field()
+ $email = isset($_REQUEST['email']) ? sanitize_email($_REQUEST['email']) : '';
- if (isset($_REQUEST['display_name'])) {
- $displayName = sanitize_text_field($_REQUEST['display_name']);
- }
+ // Sanitize display name
+ $displayName = isset($_REQUEST['display_name']) ? sanitize_text_field($_REQUEST['display_name']) : '';
+ // Validate email format
if (!is_email($email)) {
return $this->sendError([
- 'message' => __('Sorry! The provider email is not valid', 'fluent-smtp')
+ 'message' => __('Sorry! The provided email is not valid', 'fluent-smtp')
], 422);
}
+ // Properly validate share_essentials with isset() check and strict comparison
$shareEssentials = 'no';
-
- if ($_REQUEST['share_essentials'] == 'yes') {
+ if (isset($_REQUEST['share_essentials']) && $_REQUEST['share_essentials'] === 'yes') {
update_option('_fluentsmtp_sub_update', 'shared', 'no');
$shareEssentials = 'yes';
} else {
--- a/fluent-smtp/app/Http/Controllers/SlackController.php
+++ b/fluent-smtp/app/Http/Controllers/SlackController.php
@@ -29,7 +29,7 @@
'admin_email' => $userEmail,
'smtp_url' => admin_url('options-general.php?_slacK_nonce=' . $nonce . '&page=fluent-mail#/'),
'site_url' => site_url(),
- 'site_title' => get_bloginfo('name'),
+ 'site_title' => fluentMailSiteTitle(),
'site_lang' => get_bloginfo('language'),
];
@@ -57,6 +57,8 @@
public function sendTestMessage(Request $request)
{
+ $this->verify();
+
// Let's update the notification status
$settings = (new Settings())->notificationSettings();
@@ -84,6 +86,8 @@
public function disconnect()
{
+ $this->verify();
+
NotificationHelper::updateChannelSettings('slack', [
'status' => 'no',
'webhook_url' => '',
--- a/fluent-smtp/app/Http/Controllers/TelegramController.php
+++ b/fluent-smtp/app/Http/Controllers/TelegramController.php
@@ -28,7 +28,7 @@
'admin_email' => $userEmail,
'smtp_url' => admin_url('options-general.php?page=fluent-mail#/'),
'site_url' => site_url(),
- 'site_title' => get_bloginfo('name'),
+ 'site_title' => fluentMailSiteTitle(),
'site_lang' => get_bloginfo('language'),
];
@@ -115,6 +115,8 @@
public function sendTestMessage(Request $request)
{
+ $this->verify();
+
// Let's update the notification status
$settings = (new Settings())->notificationSettings();
@@ -140,6 +142,8 @@
public function disconnect()
{
+ $this->verify();
+
$settings = (new Settings())->notificationSettings();
$token = Arr::get($settings, 'telegram.token');
--- a/fluent-smtp/app/Models/Logger.php
+++ b/fluent-smtp/app/Models/Logger.php
@@ -163,9 +163,15 @@
$result[$key]['id'] = (int)$result[$key]['id'];
$result[$key]['retries'] = (int